diff --git a/README.md b/README.md index bffd87ffa5..6267cc3a7a 100644 --- a/README.md +++ b/README.md @@ -1,43 +1,150 @@ -# ThingsBoard -[![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) +![banner](https://github.com/user-attachments/assets/3584b592-33dd-4fb4-91d4-47b62b34806c) + +
+ +# Open-source IoT platform for data collection, processing, visualization, and device management. + +
+
+
+ +💡 [Get started](https://thingsboard.io/docs/getting-started-guides/helloworld/) • 🌐 [Website](https://thingsboard.io/) • 📚 [Documentation](https://thingsboard.io/docs/) • 📔 [Blog](https://thingsboard.io/blog/) • ▶️ [Live demo](https://demo.thingsboard.io/signup) • 🔗 [LinkedIn](https://www.linkedin.com/company/thingsboard/posts/?feedView=all) + +
+ +## 🚀 Installation options + +* Install ThingsBoard [On-premise](https://thingsboard.io/docs/user-guide/install/installation-options/?ceInstallType=onPremise) +* Try [ThingsBoard Cloud](https://thingsboard.io/installations/) +* or [Use our Live demo](https://demo.thingsboard.io/signup) + +## 💡 Getting started with ThingsBoard + +Check out our [Getting Started guide](https://thingsboard.io/docs/getting-started-guides/helloworld/) or [watch the video](https://www.youtube.com/watch?v=80L0ubQLXsc) to learn the basics of ThingsBoard and create your first dashboard! You will learn to: + +* Connect devices to ThingsBoard +* Push data from devices to ThingsBoard +* Build real-time dashboards +* Create a Customer and assign the dashboard with them. +* Define thresholds and trigger alarms +* Set up notifications via email, SMS, mobile apps, or integrate with third-party services. + +## ✨ Features + + + + + + + + + + +
+
+
+ Provision and manage devices and assets +

Provision and manage
devices and assets

+
+
+

Provision, monitor and control your IoT entities in secure way using rich server-side APIs. Define relations between your devices, assets, customers or any other entities.

+
+
+
+ Read more ➜ +
+
+
+
+
+ Collect and visualize your data +

Collect and visualize
your data

+
+
+

Collect and store telemetry data in scalable and fault-tolerant way. Visualize your data with built-in or custom widgets and flexible dashboards. Share dashboards with your customers.

+
+
+
+ Read more ➜ +
+
+
+
+
+ SCADA Dashboards +

SCADA Dashboards

+
+
+

Monitor and control your industrial processes in real time with SCADA. Use SCADA symbols on dashboards to create and manage any workflow, offering full flexibility to design and oversee operations according to your requirements.

+
+
+
+ Read more ➜ +
+
+
+
+
+ Process and React +

Process and React

+
+
+

Define data processing rule chains. Transform and normalize your device data. Raise alarms on incoming telemetry events, attribute updates, device inactivity and user actions.

+
+
+
+
+ Read more ➜ +
+
+
+ +## ⚙️ Powerful IoT Rule Engine + +ThingsBoard allows you to create complex [Rule Chains](https://thingsboard.io/docs/user-guide/rule-engine-2-0/re-getting-started/) to process data from your devices and match your application specific use cases. + +[![IoT Rule Engine](https://github.com/user-attachments/assets/43d21dc9-0e18-4f1b-8f9a-b72004e12f07 "IoT Rule Engine")](https://thingsboard.io/docs/user-guide/rule-engine-2-0/re-getting-started/) + +
+ +[**Read more about Rule Engine ➜**](https://thingsboard.io/docs/user-guide/rule-engine-2-0/re-getting-started/) + +
+ +## 📦 Real-Time IoT Dashboards + +ThingsBoard is a scalable, user-friendly, and device-agnostic IoT platform that speeds up time-to-market with powerful built-in solution templates. It enables data collection and analysis from any devices, saving resources on routine tasks and letting you focus on your solution’s unique aspects. See more our Use Cases [here](https://thingsboard.io/iot-use-cases/). + +[**Smart energy**](https://thingsboard.io/use-cases/smart-energy/) + +[![Smart energy](https://github.com/user-attachments/assets/2a0abf13-6dc5-4f5e-9c30-1aea1d39af1e "Smart energy")](https://thingsboard.io/use-cases/smart-energy/) + +[**SCADA swimming pool**](https://thingsboard.io/use-cases/scada/) + +[![SCADA Swimming pool](https://github.com/user-attachments/assets/68fd9e29-99f1-4c16-8c4c-476f4ccb20c0 "SCADA Swimming pool")](https://thingsboard.io/use-cases/scada/) + +[**Fleet tracking**](https://thingsboard.io/use-cases/fleet-tracking/) + +[![Fleet tracking](https://github.com/user-attachments/assets/9e8938ba-ee0c-4599-9494-d74b7de8a63d "Fleet tracking")](https://thingsboard.io/use-cases/fleet-tracking/) + +[**Smart farming**](https://thingsboard.io/use-cases/smart-farming/) + +[![Smart farming](https://github.com/user-attachments/assets/56b84c99-ef24-44e5-a903-b925b7f9d142 "Smart farming")](https://thingsboard.io/use-cases/smart-farming/) -ThingsBoard is an open-source IoT platform for data collection, processing, visualization, and device management. - - - - -## Documentation - -ThingsBoard documentation is hosted on [thingsboard.io](https://thingsboard.io/docs). - -## IoT use cases - -[**Smart energy**](https://thingsboard.io/smart-energy/) -[![Smart energy](https://user-images.githubusercontent.com/8308069/152984256-eb48564a-645c-468d-912b-f554b63104a5.gif "Smart energy")](https://thingsboard.io/smart-energy/) - -[**SCADA Swimming pool**](https://thingsboard.io/use-cases/scada/) -[![SCADA Swimming pool](https://github.com/user-attachments/assets/0878a2f5-d358-47c5-b295-03b4533685cf "SCADA Swimming pool")](https://thingsboard.io/use-cases/scada/) - -[**Fleet tracking**](https://thingsboard.io/fleet-tracking/) -[![Fleet tracking](https://user-images.githubusercontent.com/8308069/152984528-0054ed55-8b8b-4cda-ba45-02fe95a81222.gif "Fleet tracking")](https://thingsboard.io/fleet-tracking/) - -[**Smart farming**](https://thingsboard.io/smart-farming/) -[![Smart farming](https://user-images.githubusercontent.com/8308069/152984443-a98b7d3d-ff7a-4037-9011-e71e1e6f755f.gif "Smart farming")](https://thingsboard.io/smart-farming/) +[**Smart metering**](https://thingsboard.io/smart-metering/) -[**IoT Rule Engine**](https://thingsboard.io/docs/user-guide/rule-engine-2-0/re-getting-started/) -[![IoT Rule Engine](https://img.thingsboard.io/demo/send-email-rule-chain.gif "IoT Rule Engine")](https://thingsboard.io/docs/user-guide/rule-engine-2-0/re-getting-started/) +[![Smart metering](https://github.com/user-attachments/assets/adc05e3d-397c-48ef-bed6-535bbd698455 "Smart metering")](https://thingsboard.io/smart-metering/) -[**Smart metering**](https://thingsboard.io/smart-metering/) -[![Smart metering](https://user-images.githubusercontent.com/8308069/31455788-6888a948-aec1-11e7-9819-410e0ba785e0.gif "Smart metering")](https://thingsboard.io/smart-metering/) +
-## Getting Started +[**Check more of our use cases ➜**](https://thingsboard.io/iot-use-cases/) -Collect and Visualize your IoT data in minutes by following this [guide](https://thingsboard.io/docs/getting-started-guides/helloworld/). +
-## Support +## 🫶 Support - - [Stackoverflow](http://stackoverflow.com/questions/tagged/thingsboard) +To get support, please visit our [GitHub issues page](https://github.com/thingsboard/thingsboard/issues) -## Licenses +## 📄 Licenses -This project is released under [Apache 2.0 License](./LICENSE). +This project is released under [Apache 2.0 License](./LICENSE) diff --git a/application/pom.xml b/application/pom.xml index dba7d27c1b..4cbd9c3b64 100644 --- a/application/pom.xml +++ b/application/pom.xml @@ -419,6 +419,10 @@ + + dev.langchain4j + langchain4j-ollama + diff --git a/application/src/main/data/json/edge/rule_chains/edge_root_rule_chain.json b/application/src/main/data/json/edge/rule_chains/edge_root_rule_chain.json index 81f9e6a14d..e614c9b54c 100644 --- a/application/src/main/data/json/edge/rule_chains/edge_root_rule_chain.json +++ b/application/src/main/data/json/edge/rule_chains/edge_root_rule_chain.json @@ -10,27 +10,9 @@ "externalId": null }, "metadata": { - "firstNodeIndex": 0, + "firstNodeIndex": 2, "nodes": [ { - "additionalInfo": { - "description": "Process incoming messages from devices with the alarm rules defined in the device profile. Dispatch all incoming messages with \"Success\" relation type.", - "layoutX": 187, - "layoutY": 468 - }, - "type": "org.thingsboard.rule.engine.profile.TbDeviceProfileNode", - "name": "Device Profile Node", - "configuration": { - "persistAlarmRulesState": false, - "fetchAlarmRulesStateOnStart": false - }, - "externalId": null - }, - { - "additionalInfo": { - "layoutX": 823, - "layoutY": 157 - }, "type": "org.thingsboard.rule.engine.telemetry.TbMsgTimeseriesNode", "name": "Save Timeseries", "configurationVersion": 1, @@ -41,13 +23,12 @@ "type": "ON_EVERY_MESSAGE" } }, - "externalId": null + "additionalInfo": { + "layoutX": 823, + "layoutY": 157 + } }, { - "additionalInfo": { - "layoutX": 824, - "layoutY": 52 - }, "type": "org.thingsboard.rule.engine.telemetry.TbMsgAttributesNode", "name": "Save Client Attributes", "configurationVersion": 3, @@ -60,25 +41,23 @@ "sendAttributesUpdatedNotification": false, "updateAttributesOnlyOnValueChange": true }, - "externalId": null + "additionalInfo": { + "layoutX": 824, + "layoutY": 52 + } }, { - "additionalInfo": { - "layoutX": 347, - "layoutY": 149 - }, "type": "org.thingsboard.rule.engine.filter.TbMsgTypeSwitchNode", "name": "Message Type Switch", "configuration": { "version": 0 }, - "externalId": null + "additionalInfo": { + "layoutX": 347, + "layoutY": 149 + } }, { - "additionalInfo": { - "layoutX": 825, - "layoutY": 266 - }, "type": "org.thingsboard.rule.engine.action.TbLogNode", "name": "Log RPC from Device", "configuration": { @@ -86,13 +65,12 @@ "jsScript": "return '\\nIncoming message:\\n' + JSON.stringify(msg) + '\\nIncoming metadata:\\n' + JSON.stringify(metadata);", "tbelScript": "return '\\nIncoming message:\\n' + JSON.stringify(msg) + '\\nIncoming metadata:\\n' + JSON.stringify(metadata);" }, - "externalId": null + "additionalInfo": { + "layoutX": 825, + "layoutY": 266 + } }, { - "additionalInfo": { - "layoutX": 824, - "layoutY": 378 - }, "type": "org.thingsboard.rule.engine.action.TbLogNode", "name": "Log Other", "configuration": { @@ -100,97 +78,92 @@ "jsScript": "return '\\nIncoming message:\\n' + JSON.stringify(msg) + '\\nIncoming metadata:\\n' + JSON.stringify(metadata);", "tbelScript": "return '\\nIncoming message:\\n' + JSON.stringify(msg) + '\\nIncoming metadata:\\n' + JSON.stringify(metadata);" }, - "externalId": null - }, - { "additionalInfo": { "layoutX": 824, - "layoutY": 466 - }, + "layoutY": 378 + } + }, + { "type": "org.thingsboard.rule.engine.rpc.TbSendRPCRequestNode", "name": "RPC Call Request", "configuration": { "timeoutInSeconds": 60 }, - "externalId": null + "additionalInfo": { + "layoutX": 824, + "layoutY": 466 + } }, { - "additionalInfo": { - "layoutX": 1126, - "layoutY": 104 - }, "type": "org.thingsboard.rule.engine.edge.TbMsgPushToCloudNode", "name": "Push to cloud", "configuration": { "scope": "CLIENT_SCOPE" }, - "externalId": null + "additionalInfo": { + "layoutX": 1126, + "layoutY": 104 + } }, { - "additionalInfo": { - "layoutX": 826, - "layoutY": 601 - }, "type": "org.thingsboard.rule.engine.edge.TbMsgPushToCloudNode", "name": "Push to cloud", "configuration": { "scope": "SERVER_SCOPE" }, - "externalId": null + "additionalInfo": { + "layoutX": 826, + "layoutY": 601 + } } ], "connections": [ { "fromIndex": 0, - "toIndex": 3, + "toIndex": 6, "type": "Success" }, { "fromIndex": 1, - "toIndex": 7, + "toIndex": 6, "type": "Success" }, { "fromIndex": 2, - "toIndex": 7, - "type": "Success" - }, - { - "fromIndex": 3, - "toIndex": 1, + "toIndex": 0, "type": "Post telemetry" }, { - "fromIndex": 3, - "toIndex": 2, + "fromIndex": 2, + "toIndex": 1, "type": "Post attributes" }, { - "fromIndex": 3, - "toIndex": 4, + "fromIndex": 2, + "toIndex": 3, "type": "RPC Request from Device" }, { - "fromIndex": 3, - "toIndex": 5, + "fromIndex": 2, + "toIndex": 4, "type": "Other" }, { - "fromIndex": 3, - "toIndex": 6, + "fromIndex": 2, + "toIndex": 5, "type": "RPC Request to Device" }, { - "fromIndex": 3, - "toIndex": 8, + "fromIndex": 2, + "toIndex": 7, "type": "Attributes Deleted" }, { - "fromIndex": 3, - "toIndex": 8, + "fromIndex": 2, + "toIndex": 7, "type": "Attributes Updated" } ], "ruleChainConnections": null } -} +} \ No newline at end of file diff --git a/application/src/main/data/json/system/scada_symbols/3-phase-voltage-relay-hp.svg b/application/src/main/data/json/system/scada_symbols/3-phase-voltage-relay-hp.svg index 8e1a46978e..6107f8f212 100644 --- a/application/src/main/data/json/system/scada_symbols/3-phase-voltage-relay-hp.svg +++ b/application/src/main/data/json/system/scada_symbols/3-phase-voltage-relay-hp.svg @@ -535,7 +535,7 @@ } ] }]]> -220220220v +220220220v diff --git a/application/src/main/data/json/system/scada_symbols/battery-hp.svg b/application/src/main/data/json/system/scada_symbols/battery-hp.svg index 8b63a9ab29..3ed3acfe01 100644 --- a/application/src/main/data/json/system/scada_symbols/battery-hp.svg +++ b/application/src/main/data/json/system/scada_symbols/battery-hp.svg @@ -459,7 +459,7 @@ - ON + ON diff --git a/application/src/main/data/json/system/scada_symbols/conical-tank.svg b/application/src/main/data/json/system/scada_symbols/conical-tank.svg index 592508ee30..59988fa963 100644 --- a/application/src/main/data/json/system/scada_symbols/conical-tank.svg +++ b/application/src/main/data/json/system/scada_symbols/conical-tank.svg @@ -267,7 +267,7 @@ - 1660 gal + 1660 gal diff --git a/application/src/main/data/json/system/scada_symbols/control-panel-hp.svg b/application/src/main/data/json/system/scada_symbols/control-panel-hp.svg index d9b0857156..6fb7d3a501 100644 --- a/application/src/main/data/json/system/scada_symbols/control-panel-hp.svg +++ b/application/src/main/data/json/system/scada_symbols/control-panel-hp.svg @@ -320,13 +320,13 @@ } ] }]]> -Heat pump +Heat pump - On + On - Off + Off \ No newline at end of file diff --git a/application/src/main/data/json/system/scada_symbols/cross-connector-hp.svg b/application/src/main/data/json/system/scada_symbols/cross-connector-hp.svg index 5ac0af8248..926f9bc33f 100644 --- a/application/src/main/data/json/system/scada_symbols/cross-connector-hp.svg +++ b/application/src/main/data/json/system/scada_symbols/cross-connector-hp.svg @@ -489,14 +489,13 @@ "type": "number", "default": 6, "required": true, - "subLabel": "Main", - "divider": true, + "divider": false, "fieldSuffix": "px", - "condition": "return model.mainLine;", "min": 0, "max": 99, "step": 1, - "disabled": false + "disabled": false, + "visible": true }, { "id": "lineColor", @@ -584,4 +583,4 @@ ] }]]> - \ No newline at end of file + diff --git a/application/src/main/data/json/system/scada_symbols/curcuit-breaker-hp.svg b/application/src/main/data/json/system/scada_symbols/curcuit-breaker-hp.svg index 12f8b4944f..9aca99fcd4 100644 --- a/application/src/main/data/json/system/scada_symbols/curcuit-breaker-hp.svg +++ b/application/src/main/data/json/system/scada_symbols/curcuit-breaker-hp.svg @@ -425,7 +425,7 @@ - ON + ON diff --git a/application/src/main/data/json/system/scada_symbols/cylindrical-tank.svg b/application/src/main/data/json/system/scada_symbols/cylindrical-tank.svg index 5675818fe7..231bc83efe 100644 --- a/application/src/main/data/json/system/scada_symbols/cylindrical-tank.svg +++ b/application/src/main/data/json/system/scada_symbols/cylindrical-tank.svg @@ -563,7 +563,7 @@ - 1660 gal + 1660 gal diff --git a/application/src/main/data/json/system/scada_symbols/dynamic-horizontal-scale-hp.svg b/application/src/main/data/json/system/scada_symbols/dynamic-horizontal-scale-hp.svg index 7b11968223..3e68be83c3 100644 --- a/application/src/main/data/json/system/scada_symbols/dynamic-horizontal-scale-hp.svg +++ b/application/src/main/data/json/system/scada_symbols/dynamic-horizontal-scale-hp.svg @@ -586,13 +586,13 @@ } ] }]]> -Outdoor°C +Outdoor°C 0 100 - 26 + 26 diff --git a/application/src/main/data/json/system/scada_symbols/dynamic-vertical-scale-hp.svg b/application/src/main/data/json/system/scada_symbols/dynamic-vertical-scale-hp.svg index 37ba3df7c2..779fc069df 100644 --- a/application/src/main/data/json/system/scada_symbols/dynamic-vertical-scale-hp.svg +++ b/application/src/main/data/json/system/scada_symbols/dynamic-vertical-scale-hp.svg @@ -579,19 +579,19 @@ } ] }]]> -Outdoor°C +Outdoor°C - + 100 0 - 26 + 26 diff --git a/application/src/main/data/json/system/scada_symbols/elevated-tank.svg b/application/src/main/data/json/system/scada_symbols/elevated-tank.svg index 0d82a10b41..761f9c1642 100644 --- a/application/src/main/data/json/system/scada_symbols/elevated-tank.svg +++ b/application/src/main/data/json/system/scada_symbols/elevated-tank.svg @@ -557,7 +557,7 @@ - 1660 gal + 1660 gal diff --git a/application/src/main/data/json/system/scada_symbols/energy-meter-hp.svg b/application/src/main/data/json/system/scada_symbols/energy-meter-hp.svg index 3855d66ec8..0eb47a923f 100644 --- a/application/src/main/data/json/system/scada_symbols/energy-meter-hp.svg +++ b/application/src/main/data/json/system/scada_symbols/energy-meter-hp.svg @@ -474,7 +474,7 @@ } ] }]]> -000023kWhT1 +000023kWhT1 diff --git a/application/src/main/data/json/system/scada_symbols/four-rate-energy-meter-hp.svg b/application/src/main/data/json/system/scada_symbols/four-rate-energy-meter-hp.svg index e095c1417f..2701e46684 100644 --- a/application/src/main/data/json/system/scada_symbols/four-rate-energy-meter-hp.svg +++ b/application/src/main/data/json/system/scada_symbols/four-rate-energy-meter-hp.svg @@ -877,7 +877,7 @@ } ] }]]> -T1T2T3Export000223000223000223000223kWh +T1T2T3Export000223000223000223000223kWh diff --git a/application/src/main/data/json/system/scada_symbols/heat-pump-hp.svg b/application/src/main/data/json/system/scada_symbols/heat-pump-hp.svg index b1e643698c..f8fab04123 100644 --- a/application/src/main/data/json/system/scada_symbols/heat-pump-hp.svg +++ b/application/src/main/data/json/system/scada_symbols/heat-pump-hp.svg @@ -489,7 +489,7 @@ - 27 + 27 diff --git a/application/src/main/data/json/system/scada_symbols/horizontal-curcuit-breaker-hp.svg b/application/src/main/data/json/system/scada_symbols/horizontal-curcuit-breaker-hp.svg index 17fd1ba69e..fc4456e377 100644 --- a/application/src/main/data/json/system/scada_symbols/horizontal-curcuit-breaker-hp.svg +++ b/application/src/main/data/json/system/scada_symbols/horizontal-curcuit-breaker-hp.svg @@ -423,7 +423,7 @@ }]]> - ON + ON diff --git a/application/src/main/data/json/system/scada_symbols/horizontal-energy-system-controller-hp.svg b/application/src/main/data/json/system/scada_symbols/horizontal-energy-system-controller-hp.svg index 13ee85821b..7c125465b4 100644 --- a/application/src/main/data/json/system/scada_symbols/horizontal-energy-system-controller-hp.svg +++ b/application/src/main/data/json/system/scada_symbols/horizontal-energy-system-controller-hp.svg @@ -364,7 +364,7 @@ } ] }]]> -Connected +Connected diff --git a/application/src/main/data/json/system/scada_symbols/horizontal-tank.svg b/application/src/main/data/json/system/scada_symbols/horizontal-tank.svg index c02da77258..99dde101de 100644 --- a/application/src/main/data/json/system/scada_symbols/horizontal-tank.svg +++ b/application/src/main/data/json/system/scada_symbols/horizontal-tank.svg @@ -572,7 +572,7 @@ - 1660 gal + 1660 gal diff --git a/application/src/main/data/json/system/scada_symbols/large-conical-tank.svg b/application/src/main/data/json/system/scada_symbols/large-conical-tank.svg index 6789f8f7c9..ea9405da5f 100644 --- a/application/src/main/data/json/system/scada_symbols/large-conical-tank.svg +++ b/application/src/main/data/json/system/scada_symbols/large-conical-tank.svg @@ -268,7 +268,7 @@ - 1660 gal + 1660 gal diff --git a/application/src/main/data/json/system/scada_symbols/large-cylindrical-tank.svg b/application/src/main/data/json/system/scada_symbols/large-cylindrical-tank.svg index c9d9361d7d..73e31b4798 100644 --- a/application/src/main/data/json/system/scada_symbols/large-cylindrical-tank.svg +++ b/application/src/main/data/json/system/scada_symbols/large-cylindrical-tank.svg @@ -563,7 +563,7 @@ - 1660 gal + 1660 gal diff --git a/application/src/main/data/json/system/scada_symbols/large-stand-cylindrical-tank.svg b/application/src/main/data/json/system/scada_symbols/large-stand-cylindrical-tank.svg index 8e5c057209..f9af0f9c60 100644 --- a/application/src/main/data/json/system/scada_symbols/large-stand-cylindrical-tank.svg +++ b/application/src/main/data/json/system/scada_symbols/large-stand-cylindrical-tank.svg @@ -564,7 +564,7 @@ - 1660 gal + 1660 gal diff --git a/application/src/main/data/json/system/scada_symbols/large-stand-vertical-tank.svg b/application/src/main/data/json/system/scada_symbols/large-stand-vertical-tank.svg index 9b6763e0ea..508b83088a 100644 --- a/application/src/main/data/json/system/scada_symbols/large-stand-vertical-tank.svg +++ b/application/src/main/data/json/system/scada_symbols/large-stand-vertical-tank.svg @@ -564,7 +564,7 @@ - 1660 gal + 1660 gal diff --git a/application/src/main/data/json/system/scada_symbols/large-vertical-tank.svg b/application/src/main/data/json/system/scada_symbols/large-vertical-tank.svg index 75ff5ef979..d8283a8d29 100644 --- a/application/src/main/data/json/system/scada_symbols/large-vertical-tank.svg +++ b/application/src/main/data/json/system/scada_symbols/large-vertical-tank.svg @@ -563,7 +563,7 @@ - 1660 gal + 1660 gal diff --git a/application/src/main/data/json/system/scada_symbols/left-analog-water-level-meter.svg b/application/src/main/data/json/system/scada_symbols/left-analog-water-level-meter.svg index 2cdc9e587a..89c8b64a08 100644 --- a/application/src/main/data/json/system/scada_symbols/left-analog-water-level-meter.svg +++ b/application/src/main/data/json/system/scada_symbols/left-analog-water-level-meter.svg @@ -679,7 +679,7 @@ }]]> - Water + Water diff --git a/application/src/main/data/json/system/scada_symbols/left-heat-pump.svg b/application/src/main/data/json/system/scada_symbols/left-heat-pump.svg index 6232f3dda3..f7f45f50fd 100644 --- a/application/src/main/data/json/system/scada_symbols/left-heat-pump.svg +++ b/application/src/main/data/json/system/scada_symbols/left-heat-pump.svg @@ -584,7 +584,7 @@ - 27 + 27 diff --git a/application/src/main/data/json/system/scada_symbols/meter.svg b/application/src/main/data/json/system/scada_symbols/meter.svg index 6fdd27d2fa..f426e49747 100644 --- a/application/src/main/data/json/system/scada_symbols/meter.svg +++ b/application/src/main/data/json/system/scada_symbols/meter.svg @@ -720,7 +720,7 @@ - 37% + 37% diff --git a/application/src/main/data/json/system/scada_symbols/pool.svg b/application/src/main/data/json/system/scada_symbols/pool.svg index 75f21e9457..b6f03a7b6d 100644 --- a/application/src/main/data/json/system/scada_symbols/pool.svg +++ b/application/src/main/data/json/system/scada_symbols/pool.svg @@ -232,7 +232,7 @@ - 1660 gal + 1660 gal diff --git a/application/src/main/data/json/system/scada_symbols/right-analog-water-level-meter.svg b/application/src/main/data/json/system/scada_symbols/right-analog-water-level-meter.svg index 1b1be4f007..a66f39715b 100644 --- a/application/src/main/data/json/system/scada_symbols/right-analog-water-level-meter.svg +++ b/application/src/main/data/json/system/scada_symbols/right-analog-water-level-meter.svg @@ -679,7 +679,7 @@ }]]> - Water + Water diff --git a/application/src/main/data/json/system/scada_symbols/right-heat-pump.svg b/application/src/main/data/json/system/scada_symbols/right-heat-pump.svg index a0802c6e66..954e32f36c 100644 --- a/application/src/main/data/json/system/scada_symbols/right-heat-pump.svg +++ b/application/src/main/data/json/system/scada_symbols/right-heat-pump.svg @@ -584,7 +584,7 @@ - 27 + 27 diff --git a/application/src/main/data/json/system/scada_symbols/sand-filter-hp.svg b/application/src/main/data/json/system/scada_symbols/sand-filter-hp.svg index 773837608e..0d99ba911e 100644 --- a/application/src/main/data/json/system/scada_symbols/sand-filter-hp.svg +++ b/application/src/main/data/json/system/scada_symbols/sand-filter-hp.svg @@ -621,32 +621,32 @@ - Filter + Filter - Backwash + Backwash - Rinse + Rinse - Waste + Waste - Recirculate + Recirculate - Closed + Closed \ No newline at end of file diff --git a/application/src/main/data/json/system/scada_symbols/sand-filter.svg b/application/src/main/data/json/system/scada_symbols/sand-filter.svg index 243d5ed6e8..c0f6b8b417 100644 --- a/application/src/main/data/json/system/scada_symbols/sand-filter.svg +++ b/application/src/main/data/json/system/scada_symbols/sand-filter.svg @@ -408,37 +408,37 @@ - Filter + Filter - Backwash + Backwash - Rinse + Rinse - Waste + Waste - Recirculate + Recirculate - Closed + Closed diff --git a/application/src/main/data/json/system/scada_symbols/simple-horizontal-scale-hp.svg b/application/src/main/data/json/system/scada_symbols/simple-horizontal-scale-hp.svg index 9425ed48f2..76e3bc9eef 100644 --- a/application/src/main/data/json/system/scada_symbols/simple-horizontal-scale-hp.svg +++ b/application/src/main/data/json/system/scada_symbols/simple-horizontal-scale-hp.svg @@ -490,13 +490,13 @@ } ] }]]> -Outdoor°C +Outdoor°C 0 100 - 26 + 26 diff --git a/application/src/main/data/json/system/scada_symbols/simple-vertical-scale-hp.svg b/application/src/main/data/json/system/scada_symbols/simple-vertical-scale-hp.svg index 4e14130ef3..c6fb05fd26 100644 --- a/application/src/main/data/json/system/scada_symbols/simple-vertical-scale-hp.svg +++ b/application/src/main/data/json/system/scada_symbols/simple-vertical-scale-hp.svg @@ -490,19 +490,19 @@ } ] }]]> -Outdoor°C +Outdoor°C - + 100 0 - 26 + 26 diff --git a/application/src/main/data/json/system/scada_symbols/small-cylindrical-tank.svg b/application/src/main/data/json/system/scada_symbols/small-cylindrical-tank.svg index 95c4fd3eb5..fda5351233 100644 --- a/application/src/main/data/json/system/scada_symbols/small-cylindrical-tank.svg +++ b/application/src/main/data/json/system/scada_symbols/small-cylindrical-tank.svg @@ -534,7 +534,7 @@ - 1660 gal + 1660 gal diff --git a/application/src/main/data/json/system/scada_symbols/small-left-meter.svg b/application/src/main/data/json/system/scada_symbols/small-left-meter.svg index 3d5d6fdcb9..1c70a5ee27 100644 --- a/application/src/main/data/json/system/scada_symbols/small-left-meter.svg +++ b/application/src/main/data/json/system/scada_symbols/small-left-meter.svg @@ -720,6 +720,6 @@ - 37% + 37% \ No newline at end of file diff --git a/application/src/main/data/json/system/scada_symbols/small-meter.svg b/application/src/main/data/json/system/scada_symbols/small-meter.svg index a639475227..d66d70e048 100644 --- a/application/src/main/data/json/system/scada_symbols/small-meter.svg +++ b/application/src/main/data/json/system/scada_symbols/small-meter.svg @@ -657,7 +657,7 @@ - 37% + 37% diff --git a/application/src/main/data/json/system/scada_symbols/small-right-center.svg b/application/src/main/data/json/system/scada_symbols/small-right-center.svg index 8afe7bc88e..d2f96e7848 100644 --- a/application/src/main/data/json/system/scada_symbols/small-right-center.svg +++ b/application/src/main/data/json/system/scada_symbols/small-right-center.svg @@ -669,7 +669,7 @@ - 37% + 37% diff --git a/application/src/main/data/json/system/scada_symbols/small-spherical-tank.svg b/application/src/main/data/json/system/scada_symbols/small-spherical-tank.svg index 8b0a1a20b0..ae0dea7fca 100644 --- a/application/src/main/data/json/system/scada_symbols/small-spherical-tank.svg +++ b/application/src/main/data/json/system/scada_symbols/small-spherical-tank.svg @@ -539,7 +539,7 @@ - 1660 gal + 1660 gal diff --git a/application/src/main/data/json/system/scada_symbols/spherical-tank.svg b/application/src/main/data/json/system/scada_symbols/spherical-tank.svg index 44cd98e6f9..8be8a6f0d5 100644 --- a/application/src/main/data/json/system/scada_symbols/spherical-tank.svg +++ b/application/src/main/data/json/system/scada_symbols/spherical-tank.svg @@ -569,7 +569,7 @@ - 1660 gal + 1660 gal diff --git a/application/src/main/data/json/system/scada_symbols/stand-cylindrical-tank.svg b/application/src/main/data/json/system/scada_symbols/stand-cylindrical-tank.svg index 9666f987d7..ef07d408e2 100644 --- a/application/src/main/data/json/system/scada_symbols/stand-cylindrical-tank.svg +++ b/application/src/main/data/json/system/scada_symbols/stand-cylindrical-tank.svg @@ -564,7 +564,7 @@ - 1660 gal + 1660 gal diff --git a/application/src/main/data/json/system/scada_symbols/stand-horizontal-tank.svg b/application/src/main/data/json/system/scada_symbols/stand-horizontal-tank.svg index 03995acbd5..cdb6885b3f 100644 --- a/application/src/main/data/json/system/scada_symbols/stand-horizontal-tank.svg +++ b/application/src/main/data/json/system/scada_symbols/stand-horizontal-tank.svg @@ -573,7 +573,7 @@ - 1660 gal + 1660 gal diff --git a/application/src/main/data/json/system/scada_symbols/stand-vertical-short-tank.svg b/application/src/main/data/json/system/scada_symbols/stand-vertical-short-tank.svg index b448d24463..611aecec4d 100644 --- a/application/src/main/data/json/system/scada_symbols/stand-vertical-short-tank.svg +++ b/application/src/main/data/json/system/scada_symbols/stand-vertical-short-tank.svg @@ -537,7 +537,7 @@ - 1660 gal + 1660 gal diff --git a/application/src/main/data/json/system/scada_symbols/stand-vertical-tank.svg b/application/src/main/data/json/system/scada_symbols/stand-vertical-tank.svg index c4dcc662fc..dd665214ca 100644 --- a/application/src/main/data/json/system/scada_symbols/stand-vertical-tank.svg +++ b/application/src/main/data/json/system/scada_symbols/stand-vertical-tank.svg @@ -566,7 +566,7 @@ - 1660 gal + 1660 gal diff --git a/application/src/main/data/json/system/scada_symbols/three-rate-energy-meter-hp.svg b/application/src/main/data/json/system/scada_symbols/three-rate-energy-meter-hp.svg index 526f6aa719..b35fe93c04 100644 --- a/application/src/main/data/json/system/scada_symbols/three-rate-energy-meter-hp.svg +++ b/application/src/main/data/json/system/scada_symbols/three-rate-energy-meter-hp.svg @@ -745,7 +745,7 @@ } ] }]]> -T1T2T3000223000223000223kWh +T1T2T3000223000223000223kWh diff --git a/application/src/main/data/json/system/scada_symbols/two-rate-energy-meter-hp.svg b/application/src/main/data/json/system/scada_symbols/two-rate-energy-meter-hp.svg index e87548f059..325972b596 100644 --- a/application/src/main/data/json/system/scada_symbols/two-rate-energy-meter-hp.svg +++ b/application/src/main/data/json/system/scada_symbols/two-rate-energy-meter-hp.svg @@ -613,7 +613,7 @@ } ] }]]> -T1T2000023000023kWh +T1T2000023000023kWh diff --git a/application/src/main/data/json/system/scada_symbols/vertical-energy-system-controller-hp.svg b/application/src/main/data/json/system/scada_symbols/vertical-energy-system-controller-hp.svg index 6da68556a2..15edf756bd 100644 --- a/application/src/main/data/json/system/scada_symbols/vertical-energy-system-controller-hp.svg +++ b/application/src/main/data/json/system/scada_symbols/vertical-energy-system-controller-hp.svg @@ -364,7 +364,7 @@ } ] }]]> -Connected +Connected diff --git a/application/src/main/data/json/system/scada_symbols/vertical-short-tank.svg b/application/src/main/data/json/system/scada_symbols/vertical-short-tank.svg index 5d8aa42ab5..86a7ceef05 100644 --- a/application/src/main/data/json/system/scada_symbols/vertical-short-tank.svg +++ b/application/src/main/data/json/system/scada_symbols/vertical-short-tank.svg @@ -536,7 +536,7 @@ - 1660 gal + 1660 gal diff --git a/application/src/main/data/json/system/scada_symbols/vertical-tank.svg b/application/src/main/data/json/system/scada_symbols/vertical-tank.svg index 5e51330ded..66c1cab666 100644 --- a/application/src/main/data/json/system/scada_symbols/vertical-tank.svg +++ b/application/src/main/data/json/system/scada_symbols/vertical-tank.svg @@ -566,7 +566,7 @@ - 1660 gal + 1660 gal diff --git a/application/src/main/data/json/system/scada_symbols/voltage-relay-hp.svg b/application/src/main/data/json/system/scada_symbols/voltage-relay-hp.svg index fa27214864..c943eaf15c 100644 --- a/application/src/main/data/json/system/scada_symbols/voltage-relay-hp.svg +++ b/application/src/main/data/json/system/scada_symbols/voltage-relay-hp.svg @@ -426,7 +426,7 @@ } ] }]]> -220v +220v diff --git a/application/src/main/data/json/system/scada_symbols/voltage-stabilizer-hp.svg b/application/src/main/data/json/system/scada_symbols/voltage-stabilizer-hp.svg index 2ccad581d4..c3bba6ad4d 100644 --- a/application/src/main/data/json/system/scada_symbols/voltage-stabilizer-hp.svg +++ b/application/src/main/data/json/system/scada_symbols/voltage-stabilizer-hp.svg @@ -570,7 +570,7 @@ } ] }]]> -220230inout +220230inout diff --git a/application/src/main/data/json/system/widget_bundles/home_page_widgets.json b/application/src/main/data/json/system/widget_bundles/home_page_widgets.json index a30d6ee76f..2701abcb84 100644 --- a/application/src/main/data/json/system/widget_bundles/home_page_widgets.json +++ b/application/src/main/data/json/system/widget_bundles/home_page_widgets.json @@ -13,6 +13,7 @@ "home_page_widgets.quick_links", "home_page_widgets.documentation_links", "home_page_widgets.dashboards", - "home_page_widgets.usage_info" + "home_page_widgets.usage_info", + "api_usage" ] } \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_types/alarms_table.json b/application/src/main/data/json/system/widget_types/alarms_table.json index 4f86957f1c..446129972b 100644 --- a/application/src/main/data/json/system/widget_types/alarms_table.json +++ b/application/src/main/data/json/system/widget_types/alarms_table.json @@ -12,18 +12,12 @@ "templateHtml": "\n", "templateCss": "", "controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.alarmsTableWidget.onDataUpdated();\n}\n\nself.onEditModeChanged = function() {\n self.ctx.$scope.alarmsTableWidget.onEditModeChanged();\n}\n\nself.typeParameters = function() {\n return {\n supportsUnitConversion: true\n };\n};\n\nself.actionSources = function() {\n return {\n 'actionCellButton': {\n name: 'widget-action.action-cell-button',\n multiple: true,\n hasShowCondition: true\n },\n 'rowClick': {\n name: 'widget-action.row-click',\n multiple: false\n },\n 'cellClick': {\n name: 'widget-action.cell-click',\n multiple: true\n }\n };\n}\n\nself.onDestroy = function() {\n}\n", - "settingsSchema": "", - "dataKeySettingsSchema": "", "settingsDirective": "tb-alarms-table-widget-settings", "dataKeySettingsDirective": "tb-alarms-table-key-settings", "hasBasicMode": true, "basicModeDirective": "tb-alarms-table-basic-config", - "defaultConfig": "{\"timewindow\":{\"realtime\":{\"interval\":1000,\"timewindowMs\":86400000},\"aggregation\":{\"type\":\"NONE\",\"limit\":200}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"4px\",\"settings\":{\"enableSelection\":true,\"enableSearch\":true,\"displayDetails\":true,\"allowAcknowledgment\":true,\"allowClear\":true,\"allowAssign\":true,\"displayActivity\":true,\"displayPagination\":true,\"defaultPageSize\":10,\"defaultSortOrder\":\"-createdTime\",\"enableSelectColumnDisplay\":true,\"enableStickyAction\":false,\"enableFilter\":true,\"entitiesTitle\":null,\"alarmsTitle\":\"Alarms\"},\"title\":\"Alarms table\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400,\"padding\":\"5px 10px 5px 10px\"},\"useDashboardTimewindow\":false,\"showLegend\":false,\"alarmSource\":{\"type\":\"function\",\"dataKeys\":[{\"name\":\"createdTime\",\"type\":\"alarm\",\"label\":\"Created time\",\"color\":\"#2196f3\",\"settings\":{\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.021092237451093787},{\"name\":\"originator\",\"type\":\"alarm\",\"label\":\"Originator\",\"color\":\"#4caf50\",\"settings\":{\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.2780007688856758},{\"name\":\"type\",\"type\":\"alarm\",\"label\":\"Type\",\"color\":\"#f44336\",\"settings\":{\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.7323586880398418},{\"name\":\"severity\",\"type\":\"alarm\",\"label\":\"Severity\",\"color\":\"#ffc107\",\"settings\":{\"useCellStyleFunction\":false,\"useCellContentFunction\":false},\"_hash\":0.09927019860088193},{\"name\":\"status\",\"type\":\"alarm\",\"label\":\"Status\",\"color\":\"#607d8b\",\"settings\":{\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.6588418951443418},{\"name\":\"assignee\",\"type\":\"alarm\",\"label\":\"Assignee\",\"color\":\"#9c27b0\",\"settings\":{},\"_hash\":0.5008441077416634}],\"entityAliasId\":null,\"name\":\"alarms\"},\"alarmSearchStatus\":\"ANY\",\"alarmsPollingInterval\":5,\"showTitleIcon\":false,\"titleIcon\":\"warning\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"displayTimewindow\":true,\"actions\":{},\"alarmStatusList\":[],\"alarmSeverityList\":[],\"alarmTypeList\":[],\"searchPropagatedAlarms\":false,\"configMode\":\"basic\",\"alarmFilterConfig\":null}" + "defaultConfig": "{\"timewindow\":{\"realtime\":{\"interval\":1000,\"timewindowMs\":86400000},\"aggregation\":{\"type\":\"NONE\",\"limit\":200}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"4px\",\"settings\":{\"enableSelection\":true,\"enableSearch\":true,\"displayDetails\":true,\"allowAcknowledgment\":true,\"allowClear\":true,\"allowAssign\":true,\"displayActivity\":true,\"displayPagination\":true,\"defaultPageSize\":10,\"defaultSortOrder\":\"-createdTime\",\"enableSelectColumnDisplay\":true,\"enableStickyAction\":false,\"enableFilter\":true,\"entitiesTitle\":null,\"alarmsTitle\":\"Alarms\"},\"title\":\"Alarms table\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400,\"padding\":\"5px 10px 5px 10px\"},\"useDashboardTimewindow\":false,\"showLegend\":false,\"alarmSource\":{\"type\":\"function\",\"dataKeys\":[{\"name\":\"createdTime\",\"type\":\"alarm\",\"label\":\"Created time\",\"color\":\"#2196f3\",\"settings\":{\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.021092237451093787},{\"name\":\"originatorDisplayName\",\"type\":\"alarm\",\"label\":\"Originator\",\"color\":\"#4caf50\",\"settings\":{\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.2780007688856758},{\"name\":\"type\",\"type\":\"alarm\",\"label\":\"Type\",\"color\":\"#f44336\",\"settings\":{\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.7323586880398418},{\"name\":\"severity\",\"type\":\"alarm\",\"label\":\"Severity\",\"color\":\"#ffc107\",\"settings\":{\"useCellStyleFunction\":false,\"useCellContentFunction\":false},\"_hash\":0.09927019860088193},{\"name\":\"status\",\"type\":\"alarm\",\"label\":\"Status\",\"color\":\"#607d8b\",\"settings\":{\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.6588418951443418},{\"name\":\"assignee\",\"type\":\"alarm\",\"label\":\"Assignee\",\"color\":\"#9c27b0\",\"settings\":{},\"_hash\":0.5008441077416634}],\"entityAliasId\":null,\"name\":\"alarms\"},\"alarmSearchStatus\":\"ANY\",\"alarmsPollingInterval\":5,\"showTitleIcon\":false,\"titleIcon\":\"warning\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"displayTimewindow\":true,\"actions\":{},\"alarmStatusList\":[],\"alarmSeverityList\":[],\"alarmTypeList\":[],\"searchPropagatedAlarms\":false,\"configMode\":\"basic\",\"alarmFilterConfig\":null}" }, - "tags": [ - "alert", - "alerts" - ], "resources": [ { "link": "/api/images/system/alarms_table_system_widget_image.png", @@ -36,5 +30,10 @@ "data": "iVBORw0KGgoAAAANSUhEUgAAAMgAAACgCAIAAADGnbT+AAAABmJLR0QA/wD/AP+gvaeTAAAUUElEQVR42u2dh1sUVxeH/ctsUWMSSUETY/xSTDRKNNUYG4IgmhgVC7ZYA7FEERVUNAqoCIJdaeIConSwUAQEBO73zp5l3KAsEHcV3PN78vjcnXJ35s47554ZNr8zxBjz5MmTmpqakpKSOyrVCwiEAKmtrQ2ohkBVaWlpfX19e3u7UaleQCAESOAEVENAjA86KCpvCZyAagjhS2OVyrtxC6iGMDXqWKi8K6BSsFQKlkrBUilYCpZKwVIpWCoFS6V66WDxvisxMfGv3pSRkaEDquoHWJcuXZozZ87q1avX9qzly5ezTXV19aA47fT09JCQkObmZiXgVYJ15swZoPF8GfLz89nm9u3bg+K0+3JGqtcHrFWrVq34t3z0R8zBAhZ/zd2+ffuCBQsYGa+kwrt27Tp48CCNx48f+/T0BxZYe/bs4cy/+uqrKVOm7HLKz8H65ptvvvvuu3/++ee333575513Hjx48IIdXrx48fr163IPL1q0yL+mwiVLloSGhtox7Ny5c5LnMbi1tbXkRvv37//555+3bdsm2DHcZHgsYXlHR8drAxZn98YbbyQlJdHmvBiKW7du0T59+jQnGxYWVlxcfP/+fQaEf1nO6cfFxdE4f/48Z8dy2T4hIWHHjh1sf+XKFbbhI31+/PHH48eP52NMTMyhQ4fYjN7YpampyV/Amj17Ng3GheWVlZVDhw5lIUPz7rvv7t69m1XTpk1jLfn4hx9+yNPrc/ssKyvbu3evHf+6nVFFRQVr+W3aQGNrzZo1Y8eOXbduHT+ds0POm2++yelv3ryZ8+WYP/nkE1jp7OwMDAxkeW5u7ujRoxkHiAkICOAcV65cOWrUKLYHHQaWBKO8vHzu3LmzZs0qKiqKjY2dMGECu+/cuZOR9JeIdePGjZEjR9bV1b399tvciAIWlLBq06ZNP/744927d1kSHR3N4H7//fcQ9tw+2feXX37hxhW23M8IqhYvXsyXPnz4cKCBxfWW8DN8+HD4IG4tW7Zs6tSpnOyBAwc4cQacJGzevHkEpzFjxnBGUPjpp5+yQXx8PHsxgOxoz3oClvtUyFmzGbtD1d9//+0vYDGy3E+///47Nx9MuIPFHTZz5syCggKWcGdvduro0aM9dctrNthiAuUut89IqALHqqqqgUYVl5xZT+4Eh8MxbNiwq1evMjJffPHF5i5x/Fw+kKItNxXcEMPsDYhSgGWP57NgIeYEcgnw8uLbooEOFmJ0GFPYoi1gcTwcDHcY789aWlpgjjSfu5mbmwvgoWdhi1ucKYOjLSwsHLBUIX7dO2LEiMOHD8MWw8sgkCQRqMiNIKCxsZGMSrDjcYfpj2SA9okTJxgQ7j2GiA24i54LFrfiDz/8ICnp8ePH6ZyZ8TVP3hmIX3/91f7I5QcmxtQGa8aMGYw4M8K9e/dYyKpJkyZxw3GnkmF47pxHAdgKDg7maPkXiLl+AzZ/T01N5dQ4ZfIqbjDiNyQRXUY7Zb+OIQeXfEvS/KioKGIYeRVnx/8ww0d7PGnwkUZmZiYbMAK0SdhpSwrvRy9ICTOkpTKC9lT47GuI1tbWvncIWxztgI1V3URUfvaB0fOLGPDq9VmEHiCVBk8GgOXdFLNPYHGXcxlIBo/1LCYjtuEn9N4dU7oleScVlY/uOdYLwhoeHj4oqPK1iBqEt/Xr17+C91g8lDE9zfEoyV28/j6TNzTkp+43GY/KXvkWXj0rVaihocEXP2/Rn82ofCIFS6VgqRQslYKlYKkULJWCpVKwFCyVgqVSsFQKlkqlYKkULJVfg6VSeV0asVQ6FaoULJWCpWCpFCyVgqVSsFQqBUulYKkULJVKwVIpWCoFS6V6iWBhXYKfzgGPwoopKytLB1TVD7CuXbsmZqketHDhQnxBxGVVpfLTAgIq/wILg9DNbqI+j79fn+IY6z93Fe00t3c/f+OSOFOVrGA9R5g+Xr58GaM9HEdp4Pj7kodD7IcHEFipH5hTQ01Dl7FqbY71MW3i8zeuPGXuZypYPYpYJQbuorNnz+KliZO2WKURxniSoJgAPm84whHkWEsWyKqbN2/imrxlyxbW5uXlue754mIwjYyMFB9AHGDxQGcJH3EfxdOb3cUWli/Fk5JkkQHBxBCyjdOBcsOGDcZpa8iO+H9iBMeR4NkcERHByPgcrHPjTf5a18ebq6yPLrA6TflRkx1hHJtMW521oCzBVPxjNZ40W4EtK8zcjcWszjkKe0z5EZP7m3lcpWC5qKLmB3BwIcHFOMsFUIcMx1vcpCmvQAEPfFrfe+89Hl3xQsZRGM9q9mIJ2OGrieMt2F24cGHy5Mk4vQIoDTZ49OjRZ599BqMcM/1gFIhVLra5rMLzc+vWrfv27TNO41cqFcg0jX86a1nCV+NhyVEFBQWJ664PwboVZc4EmM4npr3VnHnb+ihgFe8y5z40VUnm0kyTFWItyV5s8lY4n7bmmotBpiLRnP/M3FxjLWEb9s1fbVruKViWKKBAiCJ44Br6wQcfCFhcURpgRLCRzT766CMYYoltD8x8CgR4Wc+fP/+yU3hWM80BFkzINlhOQiFm1BgwY+XNEkoQ5OTk0HguWOKvD5FvvfWW9Ekw87p7Z3ewyo+b9P+Z6rMWQ5lfWmFJwCIsNeRbISo7zKR98hSslofm1DBrFao+Z1LGms4OC6zCbToVPgXr22+/ZcaRXP6PP/5wByslJWXp0qWyGSGHijruYGGQz8wFQwQV+2kA710bLCIcnYMLJv0Eqr6Dhcc69ZLsPtndt2CVxlvZ+vX55trP1tRWEusCq/SQSR1v7uw12eEm3Q2s5gorD3tcaS2py7La7S1OsHYoWE/BooCHzQHTX69gkX1LKKJ+GIdEaSdmUrHJZ3fcp22wmPjwRhfn9OnTp0uahae+vOblAIRjSqp0A4vemHCZOqUT3xZKscA6bFrum+TRJmWMaa0zJftdYF2bY3KXWw0Hk+PHT8HqbLf2KolzrtpoMr5wTYUKljtY5EmUVqNneJK3D57BokFlLGY9smxZRQkGZjqq7jCrkjzZYIEUFFI+hLVQKFX8oIclJP5wA3Z8Lzt2A8s43bxJ1Jhk2ZEnBp+DZWE0z2SFOt8pdIFVddqijczpwlST9IaVpNs51v0LVlp25l2TGmjqchUsl7j83aoBEBXscmc0xI4bg3z7kKj/Ick7UyH7Un3OfXd2sSv9deuckENv9kJo41HR7l8qX5BUPbsj2zAn+qic4lO1N5qONmej1frP1WjsWttsxTCQetJgPSReX2DyVrlWEbdaalyPhNbJNLl2H1Bg8egENCdPnjzds6i6wTZMHK/w3Yl7juV34pVE0kgrwR9E77H4CyAPWXN6EyWTXm3JPwrdUrrCT8FiymtwDJzD6evPZphomnuTUan6C5ZKpWCpFCyVgqVSKVgqBUulYKlUCpZKwVIpWCqVgqVSsFSvJVgqlRYQUOlUqFKwVCoFS6VgqRQslUrBUilYKgVLpVKwVAqWSsHqSZ7/b1WsDXQ0Vf0DC0+OZcuW9fq/2GO76HOTDNXrBBb2m4PCFEQ1yMBSn/dXLOaBnqYCp8VS94376KPBvgkJxjemGwMLLDzWsEGbOHEi/mk06PAlX0EMI33r0NdfNTSY4GAzfLgZOdKEhJgukzCX2trMiBEmO9tqJyYacdrZssX0cdyYXoYONTU1/hKxutlxG6evn9g92rLt1/i3gdH/1x3b3vTvC8DHbk5unIu9O+ZH9nI8SHNzcz0cm4fv9YnCwsz06da1578pU0xUVPdYRY2Zzk6rMWYMPmZWg6co9wOrq3NtYMcz+3z9GSzc9PAF/emnn7DOlnoCWEUGBwdDAEagbEaDVeLUjfEacQ7LWiy4Mbe1e8Ni9MsvvxSHSKwi6YGI6HA4sIfE65H+cZcEFPxOca39+uuvMVd+rrmt7FhUVITXMo6SHBUGpz70bwIRAtXZs66P+M7HxroYmjbNDBuG6aBFBsMCc3xkeUaG2bTJzJ5tbeZwmIkTzdixZvx4I1URtm61Po4bZ4U09vVnsGgLEPQMHwIWlrU0KDnG1CnutAEBARweYIGIc4pogzb4wAUezmQbaMNBDrCEQvTnn3+KUxxssaXpzTWZsgPGaRUGXpQRkMOLlYvtC/EVXPhnK2UAELcN9RBssNwjlg3WjBnGWfrAYBjOsBDhVqywYCJiMYFevOjXYM2aNQuSfnRqDGPXHztuDNyBEgt42R34qCTg7vPOx9DQUEpREPz6bsddVVU1atQo6fPzzz9fsWKFr8DiknPhncfTHay0NKvhASwyB6Jdt+IGSUncQ2bqVCtpS0nxa7AoVZeWltbQpb6DFR4ezksQCghQxsLenbhlg8XpwJOY3lIWrxtY1FPZu3ev6aGAwPvvv09mJn36cCokHwoIMIcPuz5y2CEhfQULMeXJNIpFLwVguMpMlw6Ha5Wfg8XLM4IWxv9UFuHSmj7YcZM8sTHXnpQfY2NctfnIORLDOAUbLJgghrEK695x48bRm3EWwoiPj6dUzokTJ5hVeS1HqZVn7bg50+joaPxOWSLzsq+0Z48Fwf79BspJj3j06wmswEDDTcWDoQ0W0yWpWHKyCQqysn4GDbA4d/zradCVv4FFqRJK39gfk5OTmbCYccDLOIs0iUs2KVSiDLT1iL2F0CIFBKKioghX2fIQbkxBQQGe73ZMolCA3Tl27SynUAAwXSTnwII/O5snA3YhthG0WMtX2EWabIb4LtYyh8bExJDP+fbBkPkrNNRwCwlMxrKu56xcIY2J2Fl8ygpOvJjAdJ6GpH1gJ0EOOuU12MmT1jYs3L7d6o1oze6+ebZ9rV6Q+rUd92B88858ATT8ufDXnkWQIB+SWPKqRNRhptOLOmjAkqIPf3kUWZEU+FOpjP4eS6VgqRQslYKlYKkULJWCpVKwFCyVgqVSsFQKlg6ESsFSKVgqBUul8j5YKpX6vKt0KlQpWCqVgqVSsFQKlkqlYKkULJWCpVIpWCoFS6VgPVeY+5zuTWqZrOofWHl5eXP6oPnz5+McpGOqMv1ym8GAykNliqysLLXjVv0XsF6CjVF5efmLO4sUFhbmuNkrYpVTWlra307w4sJI0n0JnrZimMst9HKy0nvN9y5XX5X23YaSwroiaWfdyyl7VN7f3tLLM2qaa/wULGz1QsQN8QWEy97GjRvtjxEREdjz9bcTXN3O2nbFXVkmRpLG6cBGbmCc3rjuBnFeFzDNSw1ubGukvTM3OvLKGhodpmNRemjug7z+9naoIOFO/V0Fy7iHHGwagUMM1uPi4qTBBcYJUlJA27/PA1jYG8uObE/UMU7fZQDCUguGODvcuWGlxmmdSNiT2InjMruzHNNKAYvlrMUfEOPusLAw7P/YUfxRibj4PXnr2nR0doSkL7710NFhOhelhcLTo9ZH5Y8qbNpy7ueevHPqWvV1tuTjhYqLBXWFSXeSWzpaUkrOENVSSs9kVGayO2szKy9UNFY0tjWxqryxnM2uVl/rdK6it9OlZ9PK0h21BXkPbvoFWKdOnZoxYwZXMTIyUhxHMYMEC6431rTiJrpkyZLMzEx3sPCGLO8Sho4CFo6jmIvSwLUWo0dZQrcAivlxUFAQ3w5kM2fOZNXRo0fXrLEiBFvyUAJ5s2fPFrDWrl175MgRfFA5XywqAZEDYHvZGG9SL973W7K2Jt89TaQhXO3Iic6qyb5UeXnV5UhWJRafWHkpEiCWXljOvyxZeWnV4vPhO3NiHrU2zk1duPbqhuPFJ1gCMaxdduH3S5VXmF5ZteXG1mO3ExekBYMRI7n26voN1zcfLToGvn/n7/MLsIgKUioC87cJEybU1tZihMzl5+uIFvjesgoDd/fCE4A1efLkkC5hbusBLFmClSiOusZZb0JMv22wAgMDpQiKPRUKWDTAV+ITE6L4y1OOAJS9CNaJ4pPRubuIMfFFR86WpR4qiD9UGH/AcZBVJEwEnuL6O2ywMydawDpTmirTJfQU1lo5GaDE3orrBlZtSy1LgIm4VdpQxpKGVqvORawjzl/Aoq6OOGYjggqOyNiswxOTI5eTbyTFxuq4LzmWB7CIfLbH5OjRo93BEs48gwWOhE9GQEoceFH5D/IJSFuzthFamNqIW+uubrxcZbm3p5WnsQrg1l/bRAYmYDHf2WDdrr0trOzL398NrLoW663QHze2MpPmP7gVnO4a8wP+AxbTkLgdi996Y6OVW1DXBFNuikokJCTQOGw7oXsEi4AnsWe7U30Ei1lS3L8xOH0WLGZq2QurZqqqUFvPu2A1PWmal7qQ7Iq0iTkrPGMp8xdwsIoJ8VxZmhOdAzty/vzPYD14/JCkTR4zo3N2vbZgcevbsxi98ZBP8RKuJTDZl42Km8yDxlmhc/jw4ZVS9ao3sCiVQ+kbEjW84PsOFi7wkyZNWrlyJfGyG1h8ETO11ErhIWDYsGG+8PZdfWUtc5a0Y/J2w5Zk3MduHyclIj2CGLb5z2DROOg4TCq25sq68MyI1xMsnq1uuUmKeMnbV2ZAezPe79vzo4QTd0FbjZspPtjJ3wO44zlISCVREwJ4oJOiYvRmM+Fw1m6Q4gOyhN7I8zgYMZpnud0hT6MSBUmtBDuvi4jy8HGttOtb62uan7LLE2JlUyXPdDSsA2uuJm23Dsx0EoFa2q1iyqRTklHJlk/a21klT5FkaXRI41Gb9bBZ3VRD5DtSeOzlgcUzEdBgn3+jZ1G4hm388+/ZZHs8uqbZBv+DTRtvbCaNI1ELy4hwB9fnYJWVlS1YsKDXvxUyy/hnyXFedvi28ImPRRjjBRgv9CXgvTywjLMyUWlv6lZsUuXP0t9jqRQslYKlUrAULJWCpVKwVAqWgqVSsFQKlsqvweJvq/KbXZXKKwInoBrCX+/r6+t1OFTeEr/+AKohbW1t/KUPtjRuqV48VgESONEYwmd+jQlihC81kVa9iEAIkCRC/R9f3OEsEgi6eAAAAABJRU5ErkJggg==", "public": true } + ], + "scada": false, + "tags": [ + "alert", + "alerts" ] } \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_types/api_usage.json b/application/src/main/data/json/system/widget_types/api_usage.json new file mode 100644 index 0000000000..9d6f2bc464 --- /dev/null +++ b/application/src/main/data/json/system/widget_types/api_usage.json @@ -0,0 +1,35 @@ +{ + "fqn": "api_usage", + "name": "API Usage", + "deprecated": false, + "image": "tb-image;/api/images/system/api-usage-widget.png", + "description": null, + "descriptor": { + "type": "latest", + "sizeX": 7.5, + "sizeY": 3, + "resources": [], + "templateHtml": "\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 self.ctx.$scope.apiUsageWidget.onInit();\n}\n\nself.typeParameters = function() {\n return {\n hideDataTab: true,\n hideDataSettings: true,\n datasourcesOptional: true,\n previewWidth: '400px',\n previewHeight: '300px'\n };\n}", + "settingsForm": [], + "dataKeySettingsForm": [], + "settingsDirective": "tb-api-usage-widget-settings", + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0\",\"settings\":{},\"title\":\"API usage\",\"decimals\":null,\"showTitleIcon\":false,\"titleTooltip\":\"\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"widgetCss\":\".tb-widget-header {\\n height: 48px;\\n align-items: center !important;\\n padding: 5px 10px 0 10px;\\n}\",\"titleStyle\":{},\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"actions\":{\"headerButton\":[{\"name\":\"Go back\",\"buttonType\":\"stroked\",\"showIcon\":true,\"icon\":\"undo\",\"buttonColor\":\"#305680\",\"buttonBorderColor\":\"#0000001F\",\"customButtonStyle\":{\"padding\":\"0 16px\"},\"useShowWidgetActionFunction\":true,\"showWidgetActionFunction\":\"return widgetContext.stateController.getStateId() !== widgetContext.settings.targetDashboardState && widgetContext.settings.targetDashboardState;\",\"type\":\"custom\",\"customFunction\":\"const state = widgetContext.settings.targetDashboardState?.length ? widgetContext.settings.targetDashboardState : 'default';\\nwidgetContext.stateController.updateState(state, widgetContext.stateController.getStateParams(), false);\",\"openInSeparateDialog\":false,\"openInPopover\":false,\"id\":\"1ea1cca6-47d1-3539-d051-9535129fb12b\"}]},\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":null,\"weight\":\"500\",\"style\":null,\"lineHeight\":\"21px\"},\"borderRadius\":\"4px\"}" + }, + "resources": [ + { + "link": "/api/images/system/api-usage-widget.png", + "title": "\"API Usage\" system widget image", + "type": "IMAGE", + "subType": "IMAGE", + "fileName": "api-usage-widget.png", + "publicResourceKey": "1TAYhxLWInZC20Xrn8PgbNbQs6fX8Pir", + "mediaType": "image/png", + "data": "iVBORw0KGgoAAAANSUhEUgAAAMgAAACgCAMAAAB+IdObAAAChVBMVEUAAADs7Ozd3d3f4+Xd4eLN0tLP0dTg4+Xp6en////y9ffx9/Pt7e0ZgDjl7uvh5Oawvs/7+/vx8fHO1+H9/f31+ffu7u78/f3s8fH9/v6Eu5Xg7uX6/PxHaY7i5efI0t1VdJf5+vvx9PaNornv8vVOb5NffJ262cRphaNaeJrs7/Ouvc2Wqb+BmLKmpqZjgJ9ip3ft8PXBzdmsu8yEm7Rviaf3+Pra4empqann7PHQ2eK/y9i3xNPD3stXdpi8vLyGnbVsh6VRcZV+t5A7X4ff5evW3ebU3eXN1+GzwdHOzs6pucuarcGQpbt7k66OwZ46klXc7OKcrsKx1Lytrq53kaxyjKhDZYs+Yoni5+3L4tOitMeKn7dmgqH09vn09PS6x9V1jqrm6/DT2+TEz9vX2NnV1dbHyMmltsisrKxhfp5cepuJvplLbZHj6e7d3d2hssWer8PBwcGoz7R0sojp7fLj8Oe/3MiYq8C4ubm1tbWvsLCeyauCuZQ3XIRepnTl6u/p8+zc4+rk5OTK1N/O5Na4xdSxv9CTp71+lrCSw6F4tItnq30cgTr29vbt8vTq6urh4eHU59rR5tefn6CMv5zd5OvX3+fIy8yt0riHvZj19/nm5+ja2tvR09S+v8B7to1ur4FkqXlPnmdHmWDz9fjZ4Oi8ydagscOVxaRxsYQqiUchhD/3+/jq8O/o6uzZ6t7Ly8uw07yysrKjzLCho6SVlpZao3Le4eOx1LuozbeZx6dWoW7t9e/c3uDC3czKysrDw8OytbaZm5xrrn8wjUze7eLO0NDI4c8zWYLg5uzV5t7FxcWOjo7v9vHY5+C01r7AydLKzc6nzLWBgoNCllzu8fMzg2RWAAAACXRSTlMA3Vrh2mBe3tXDgts5AAAPHUlEQVR42u3ch1sTZxzAcbr7eyEWaGw8uDR7r0JCAgkz7BFk7733kr2UvYTK0jpQqnW3bm3VWrXaveff0/cYLbXYQgiS0Hyf5y4hPjzw8T1y8X5EBweHl154zcZ74WUH3ItcZPPxX8TrsQ0cWPKywwtoW/SKw5toW/SaHWLG17qE0OVj6LWPEHrzE4SjdsWlP6PLpcdQyWV8/7JNQAK++wiVPnoHBdxC6GYHfoCbhPC94yVoZm6G++hTxP/1uE1A0Kd4Kd7BkEcfXZ6hIMe/wLtb5SV8JSq/fPMm951HNgZ5J6ntZwrSwacOr12PdilR0uWZ0i9unrAxSIny5msYculT/Bj/BJrhz6Cb/Jnfb31qK5B3jiFUXIy++Kj4i8ulCJ14DeG+TSpF7+CtDZ04dvwT24A80S682ebT75PZIf8XSHgrzgf9Wz7W+0p5BaR3yD9rSIreRsgD4fDt4h3+0gd4l1pA3XL5Cx/zKZXH0oYfw4/89Xmr1HmRtnof7jC7XasfWkG+SMZkR7CFPJSWwnTeJxF7IWZQtTM3v9JfF10tqe9ORuhgWlBqsiQIydjGojNslQKliCXpkf5sGUrx98+SqiqDuMMV7Aj0ZDvGaZaG4LhPhaSgM2d9U1Gajyw5wsjyfZsZGuHPqkB1muh8tLAiB5kosJXbzU11jgnKq4yI5Kb6eobqz9ar9GyUkBWdbWCzhOdYhejJ4mmbAeE/FRKLyoIiQ1CaNDIfFSn8+cyaXkFeChpKjlYsQaoRrx6FvN0tb1WjvORsFMYzhms09RUsFTqXlSBsbQ2riWHnoSd7+7NnuiIYMuyVtQgpqpQxm5iB+RofQb0kj4IY6/6E8PNj5ZlD2dHVPgK1pChFk18prW6tzAqvrsv3kWSm1KF/9MP8W6u3e5fZcdGqkKJQ1NuP+JEsX74v9yALva4+g5iGIT5Kl0UgHT7sC4cQkuYhfRiK5HIHIz0QS40/RcZCPrKIQaSXKWKQTq1HPs5nuciMNvU8wqxBay1ZVRGK1pP9hGg9kG/wCcDdHS106RtkRu6NcXFjO7YE8nFwcMby/a4fELp2By00fQStv0txNFzcjq2AnMTb7tkLlwaCg3d1BTfGX7uzo2vePb5xzBzILOUYCx7bohXZtf/S3ZaM+2MtXQPfdF27s/vGndngbzLMgXx46iJt7JfxuK1akf2oU3Tk8BFR1w87Gq/dCT5/J6Nxx7R5kPtuv4zT4rZoRe5TkLuNe691Ne6/f+3OwP7g+/f3N5oD2U071YlfvAVv6bPW28u7hT3fvH8nL/6wX0I2fx7ZEfzbb8GXtvw8spTtnxCRHWKH2EqvOZxz3hadc/jp9W3RTw5vwLbIyQ6xsuwQa+uZQhjAYcDChveAc8GPuQIQrgRw6IA3m4AcKC+GpACAR1GUqQ0gt+Mm0JUNbdDxsBxmHgB8V2sTkKSSY5CkJJxuUZDjkxhDeMOBE+CNbzq03t4wecs2ILdqla5JD4rbSilIO1Dhb/5bUHKSoN1V2dDX0WAZiJSF48Aa6meBGSnh28mkXO/yEgxxfbgEcQpYWJFyhjLX+3qpZSA6kyDFhODfEucAVUIMUNUZYD2VXE+it00EFM+VADTkAiyuy/X2B3CiPQCuQ8exqEkLHVpentCvEEuTxXmQwhMWkpqgTEjWqMJBXaE5ylJ4qbsDqe9fpYrJGRGrdQJJ05A4FoEZHYPVsxzEV9VU2B/KhLR0tUJfqW0lmb2e2aFsjilmKFsKTARQI9AGxvQaPNLoWTIoOz3iDFbRPyAKYKl4aZDGORtL1FdqgMmXCjyTwRA7pIEFSL8XPrQOCXndnCwZka8QZoJVtArEZDKELED6swpSEXOwLqWpOjw5moJU6wE8mIVeMWWK/m6tadgj7YzYGiGRB+HMWfDglcnJVlI3SKg1BmAm8KQQERhNhBkA8oYBoChQzcoZjkngfq3xyQtUF4FV9J/nESYfLNTAyfeogndarg/oa4foGGCZPlgas+91tGTEsz+z99A2A8J49pCd7z/7FTkkhU1I9F4cVeNOC8aAf4XkHQIqdSTYTisgHrFByac5gRVlkMnKC8zOSjey04fEIwhsohWQfQJSk5mZRUrCFJF1I2QqX54JzFC8OjbRSkglyLI0vqAYxBA5CAowhBWUXQBWGf1244UfnwJhhnk5R6aECWqWIAmBSJweKwML5ebmNgVLVTUDiBJhofjdsP6IvTTcSfqqELbpHJ2QBbLg7KEIFmSiAk26PjCBAxZqP95aDncSjuf3EFWd5z8QJdL3dJLk+fPmQA5TjlPzd59yaG1qH7q5kY/jdw/4NY9OV/n5nRcl7hG13Djv12IO5ORn47RTGRffWw1ChsKmRq3Iu7DH7/bsBb+q5p1josSuI6OiRtKsQ+vjC9c+zrhIi9uKiw97ExPdKYjb4f0tVaOziaLEFrfbGYmzu82BdNHmp/CLt8dbAYmfnr7XDN/vJDN6vu9pngJHR/h8AGDKvcecV24Xafglz/vNtn/JdPoU/mG/si2u/X6w034R25qzQ6wtJ4cDLtuiAw6Trtuiye1zaNkh1pUdYm3ZIeuvVukEtR1JLn0d5W9MKgGKywH3kAMQ0BYAJcpyp76k8omS6wBRbVYNORZwALyJqOIoeu2Due+coNwbAOjtAC5R4A0NfQwodymOivpuApRJVg0BDFHCXBQQSblzAW2ubUoAKK4F3IF2uNo246qEvodRAQ/7AiwFOcqC1SLQhiHeUFICAZ/AXGl5mxMFaSfwzrXcBd6A4ihvovZqVIn3daeNQdBXEnYmUKWzYbV6szYM+bZBmXt1pnRurrR4BjAktwEAiFsNpa4ND5VOUQHeb0SVPCif2CAkFXJCGDHkQV8M2WcqYwAAi8eC6EGeDvQ8Z899Mm1dtPwonOXpwZwmGABOE5Dr5JTLmSDwHYA+Vwri5OTkQv0JvJELHA5DS+RuDJLGKmND6lHP5HQ2YfRUlAHo2Hp2jTChTHyayYrlRYg9uotS1CxVhMD6rmyvhHSb0g4tQQpC8lWBAGWCfKOn8FCvpDAb1BSEiUfTw+x8QThYW38/tDJHgOkjwxBtqtSjAMBTTNeRFKSpWhuzDFHHwiECrK0VkKNi4GRLoyXJJp9kMFRkhwEQw8LknJGva2Ihs1LIOxTI9QJnGRko1JBgba31PCI3VHjCxnK867aG9jDWE7FuyMGyfthYjDjaWtprM+Npy0Lozx7iaAUQLlii2XHLQz6AtUOGwkBMWuZirfsa6rHAs1YhLw/KCvSDRKRJB+kmNWEIB7mO7ZUjp5+OkfPpCc4mLhFtCgNrbAWEK4io0PumsHVDKSwBR1AUKwt0hm6uVz0ZQnplyirIrwyx9Z7i8CCwxlZACpn5EhkYY2GkMp+pZwMABYGRQQgh00gI4YRANE+aLTaAVdS8Z0/zU8bT/pxQbYREIJWbIEzLzPFk8TL5X4HCF0MkYenVJAXRFzWlEWBWN1aMpzt7AL5sWfqOboAZjeKnjvHRVSGQKUyRxoZHJuTECk3EoDDfQ1eZXwEGSZOKEZatimCoIK/MI1lcv4Fh6JX4aSAy4qHqywFqzn5lGmDArKluBg332d4vt2QYeuMG+fjwu/GJtxs/rzo/eliU6Dc7Kjo8OmoO5MI4dgy8d2ErICfd3enUVHd69/7l8fR8cKe54+muYOygfbwlEEdHkoKMuXf5VYla7uJfGLjdfOV8i8gcyNj44fj3aLTGrYDgg2hKBFPNPaOiK18mdrp8Hk8/fNeR7LydAetvanw8Dv+0T9n+lcY91Jh9z3a4ZLqzpcXRfu3XirNDrK1tBKl12hbVbp8VsUOsKzvE2rJD1t9kexTgXCcBXL6YBE5b28QbJwCcGmwNoiQ6cvHNCbybO3EVSj/pa+j71RXaZ2wNgmfpxwCIdsDVXoX2XG1H3/UAl3LvrYRwPcyFzBUvQ1wnrvc1eEcVKzcBkhUBK6ObKlN8YKnYUKAKL8A7ffUg/K1+bk72f0IOKDkA7fQlyPHSq1F9DcdvEZsB8fKEQk+ttBcONmkNhVBUQUSOQJOhCfS6dH1OaKiB7yEe5oBWMSIt1OaEcwrDWQSkG1ABW032A6NID9rwwn5YvdwHBwBcShfv469aUkJMHGD0weSmQAa9hr1C/UFyMKi+wnMfs04KPsZzRq4g6HVVuCZ72FgjVByF014p6f5fhwl9QlrZBr1/jP8hgZzPhJQsr5jQtBgjC8zI0pDT9SNMkBT6QzivUg7SmGpfNQ90SFAAGBIJ7BqFLwAkJMACRADyOp4aCklVGJ+ZkwpNAvyXwIsGM7Ig5IwMKvRZ5/AIuk5ST6Q21cvz1MBS+WqgiC/woSDOBFO6DJH0Fi5AzskzYXABQqaSvf7WAPEwqsSkrzEojfD5qgDE2ewsqVBsjNRWiLM5ixB/YSyUGX0WIHJ/1SKkwD87FgIlC362pzVAAHLwpqWv+ABOk9QAHhbTRFL3EAELnYblji5P5zgk/FsE3dIR5p1HZBEbm7w5Wr6dW3BmJxzXltWPp7cNBB9amxCsHRLuA0LSMmtCt3iwKoRrqIHXtdIzEGrIAY4nvm0ClodYzmERRD/eInRFdDhjsL5fOnsCwjHWG9Nbh8We/ZJWIaEaFhZRU919Qo02hMFLVijIr+RCtV5SxgZrbAUkTGCqVHMEXqAIMqWFG6GpZnE8nQchjFQtPW1hPK0zxuwDq4j4PJ6xOkQnrNGhfdVGTqC8RufBJHrDsmT0JUi1NCeVpCAHw84yCTCraT+/HliKGm1cmYKF7vmBGWXE4WnotVUhkJItqQkaHJbXsIO8CFMFW1dUHdQNZWxpCMOX7a9egOgk+V5gXm9RELoLAEmHqmYXajzNIAFczBqGxr9PwxOruIGnvEShw2IIb1oCb4zl1yQkZ3lFT29oqjv7Yc+PwfvvVc0/zhAlxl+4MJ3xeMwcyEnKIXp8cqvm7NRU995d6t3TjvOiRLcutyNjO81akbjGU++L3t2ad08vvw187F6nX9WVz6tEiaNTjO+PuE+ZAzl1MePLd/G7dbcCMuvmNtAJflcy5qsGbs/O90xnOLp1xd+bn/3RrP9h4OJb+OjqtP0rjcE03F7S9iGQUXVERNiv/Vpvdoi1tY0gz8G26DmH5+mwDaI/7+Dw3DaQ0F91wD3/nJON9xxejz8ATqyBto+N2i0AAAAASUVORK5CYII=", + "public": true + } + ], + "scada": false, + "tags": null +} \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_types/attributes_card.json b/application/src/main/data/json/system/widget_types/attributes_card.json index 6246811769..2beedd2a76 100644 --- a/application/src/main/data/json/system/widget_types/attributes_card.json +++ b/application/src/main/data/json/system/widget_types/attributes_card.json @@ -11,7 +11,7 @@ "resources": [], "templateHtml": "", "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 self.ctx.datasourceTitleCells = [];\n self.ctx.valueCells = [];\n self.ctx.labelCells = [];\n \n for (var i=0; i < self.ctx.datasources.length; i++) {\n var tbDatasource = self.ctx.datasources[i];\n\n var datasourceId = 'tbDatasource' + i;\n self.ctx.$container.append(\n \"
\"\n );\n\n var datasourceContainer = $('#' + datasourceId,\n self.ctx.$container);\n\n datasourceContainer.append(\n \"
\" +\n tbDatasource.name + \"
\"\n );\n \n var datasourceTitleCell = $('.tbDatasource-title', datasourceContainer);\n self.ctx.datasourceTitleCells.push(datasourceTitleCell);\n \n var tableId = 'table' + i;\n datasourceContainer.append(\n \"
\"\n );\n var table = $('#' + tableId, self.ctx.$container);\n\n for (var a = 0; a < tbDatasource.dataKeys.length; a++) {\n var dataKey = tbDatasource.dataKeys[a];\n var labelCellId = 'labelCell' + a;\n var cellId = 'cell' + a;\n table.append(\"\" + dataKey.label +\n \"\");\n var labelCell = $('#' + labelCellId, table);\n self.ctx.labelCells.push(labelCell);\n var valueCell = $('#' + cellId, table);\n self.ctx.valueCells.push(valueCell);\n }\n } \n \n self.onResize();\n}\n\nself.onDataUpdated = function() {\n for (var i = 0; i < self.ctx.valueCells.length; i++) {\n var cellData = self.ctx.data[i];\n if (cellData && cellData.data && cellData.data.length > 0) {\n var tvPair = cellData.data[cellData.data.length -\n 1];\n var value = tvPair[1];\n var textValue;\n //toDo -> + IsNumber\n \n if (isNumber(value)) {\n var decimals = self.ctx.decimals;\n var units = self.ctx.units;\n if (cellData.dataKey.decimals || cellData.dataKey.decimals === 0) {\n decimals = cellData.dataKey.decimals;\n }\n if (cellData.dataKey.units) {\n units = cellData.dataKey.units;\n }\n txtValue = self.ctx.utils.formatValue(value, decimals, units, true);\n } else {\n txtValue = value;\n }\n self.ctx.valueCells[i].html(txtValue);\n }\n }\n \n function isNumber(n) {\n return !isNaN(parseFloat(n)) && isFinite(n);\n }\n}\n\nself.onResize = function() {\n var datasourceTitleFontSize = self.ctx.height/8;\n if (self.ctx.width/self.ctx.height <= 1.5) {\n datasourceTitleFontSize = self.ctx.width/12;\n }\n datasourceTitleFontSize = Math.min(datasourceTitleFontSize, 20);\n for (var i = 0; i < self.ctx.datasourceTitleCells.length; i++) {\n self.ctx.datasourceTitleCells[i].css('font-size', datasourceTitleFontSize+'px');\n }\n var valueFontSize = self.ctx.height/9;\n var labelFontSize = self.ctx.height/9;\n if (self.ctx.width/self.ctx.height <= 1.5) {\n valueFontSize = self.ctx.width/15;\n labelFontSize = self.ctx.width/15;\n }\n valueFontSize = Math.min(valueFontSize, 18);\n labelFontSize = Math.min(labelFontSize, 18);\n\n for (i = 0; i < self.ctx.valueCells; i++) {\n self.ctx.valueCells[i].css('font-size', valueFontSize+'px');\n self.ctx.valueCells[i].css('height', valueFontSize*2.5+'px');\n self.ctx.valueCells[i].css('padding', '0px ' + valueFontSize + 'px');\n self.ctx.labelCells[i].css('font-size', labelFontSize+'px');\n self.ctx.labelCells[i].css('height', labelFontSize*2.5+'px');\n self.ctx.labelCells[i].css('padding', '0px ' + labelFontSize + 'px');\n } \n}\n\nself.onDestroy = function() {\n}\n", + "controllerScript": "self.onInit = function() {\n\n self.ctx.datasourceTitleCells = [];\n self.ctx.valueCells = [];\n self.ctx.labelCells = [];\n\n for (var i = 0; i < self.ctx.datasources\n .length; i++) {\n var tbDatasource = self.ctx.datasources[i];\n\n var datasourceId = 'tbDatasource' + i;\n self.ctx.$container.append(\n \"
\"\n );\n\n var datasourceContainer = $('#' + datasourceId,\n self.ctx.$container);\n\n datasourceContainer.append(\n \"
\" +\n tbDatasource.name + \"
\"\n );\n\n var datasourceTitleCell = $(\n '.tbDatasource-title',\n datasourceContainer);\n self.ctx.datasourceTitleCells.push(\n datasourceTitleCell);\n\n var tableId = 'table' + i;\n datasourceContainer.append(\n \"
\"\n );\n var table = $('#' + tableId, self.ctx\n .$container);\n\n for (var a = 0; a < tbDatasource.dataKeys\n .length; a++) {\n var dataKey = tbDatasource.dataKeys[a];\n var labelCellId = 'labelCell' + a;\n var cellId = 'cell' + a;\n table.append(\"\" + dataKey.label +\n \"\");\n var labelCell = $('#' + labelCellId, table);\n self.ctx.labelCells.push(labelCell);\n var valueCell = $('#' + cellId, table);\n self.ctx.valueCells.push(valueCell);\n }\n }\n\n self.onResize();\n}\n\nself.onDataUpdated = function() {\n for (var i = 0; i < self.ctx.valueCells\n .length; i++) {\n var cellData = self.ctx.data[i];\n if (cellData && cellData.data && cellData.data\n .length > 0) {\n var tvPair = cellData.data[cellData.data\n .length -\n 1];\n var value = tvPair[1];\n var textValue;\n //toDo -> + IsNumber\n\n if (isNumber(value)) {\n var decimals = self.ctx.decimals;\n var units = self.ctx.units;\n if (cellData.dataKey.decimals ||\n cellData.dataKey.decimals === 0) {\n decimals = cellData.dataKey\n .decimals;\n }\n if (cellData.dataKey.units) {\n units = cellData.dataKey.units;\n }\n txtValue = self.ctx.utils.formatValue(\n value, decimals, units, true);\n } else {\n txtValue = self.ctx.utilsService\n .customTranslation(value);\n }\n self.ctx.valueCells[i].html(txtValue);\n }\n }\n\n function isNumber(n) {\n return !isNaN(parseFloat(n)) && isFinite(n);\n }\n}\n\nself.onResize = function() {\n var datasourceTitleFontSize = self.ctx.height / 8;\n if (self.ctx.width / self.ctx.height <= 1.5) {\n datasourceTitleFontSize = self.ctx.width / 12;\n }\n datasourceTitleFontSize = Math.min(\n datasourceTitleFontSize, 20);\n for (var i = 0; i < self.ctx.datasourceTitleCells\n .length; i++) {\n self.ctx.datasourceTitleCells[i].css(\n 'font-size', datasourceTitleFontSize +\n 'px');\n }\n var valueFontSize = self.ctx.height / 9;\n var labelFontSize = self.ctx.height / 9;\n if (self.ctx.width / self.ctx.height <= 1.5) {\n valueFontSize = self.ctx.width / 15;\n labelFontSize = self.ctx.width / 15;\n }\n valueFontSize = Math.min(valueFontSize, 18);\n labelFontSize = Math.min(labelFontSize, 18);\n\n for (i = 0; i < self.ctx.valueCells; i++) {\n self.ctx.valueCells[i].css('font-size',\n valueFontSize + 'px');\n self.ctx.valueCells[i].css('height',\n valueFontSize * 2.5 + 'px');\n self.ctx.valueCells[i].css('padding', '0px ' +\n valueFontSize + 'px');\n self.ctx.labelCells[i].css('font-size',\n labelFontSize + 'px');\n self.ctx.labelCells[i].css('height',\n labelFontSize * 2.5 + 'px');\n self.ctx.labelCells[i].css('padding', '0px ' +\n labelFontSize + 'px');\n }\n}\n\nself.onDestroy = function() {}", "settingsSchema": "{}", "dataKeySettingsSchema": "{}\n", "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Attributes card\",\"decimals\":null}" @@ -29,4 +29,4 @@ "public": true } ] -} \ No newline at end of file +} diff --git a/application/src/main/data/json/system/widget_types/entities_table.json b/application/src/main/data/json/system/widget_types/entities_table.json index 01034a1d1d..f58bd3f0ce 100644 --- a/application/src/main/data/json/system/widget_types/entities_table.json +++ b/application/src/main/data/json/system/widget_types/entities_table.json @@ -11,19 +11,13 @@ "resources": [], "templateHtml": "\n", "templateCss": "", - "controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.entitiesTableWidget.onDataUpdated();\n}\n\nself.onEditModeChanged = function() {\n self.ctx.$scope.entitiesTableWidget.onEditModeChanged();\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n hasDataPageLink: true,\n warnOnPageDataOverflow: false,\n dataKeysOptional: true,\n supportsUnitConversion: true,\n defaultDataKeysFunction: function() {\n return [{ name: 'name', type: 'entityField' }];\n }\n };\n}\n\nself.actionSources = function() {\n return {\n 'actionCellButton': {\n name: 'widget-action.action-cell-button',\n multiple: true,\n hasShowCondition: true\n },\n 'rowClick': {\n name: 'widget-action.row-click',\n multiple: false\n },\n 'rowDoubleClick': {\n name: 'widget-action.row-double-click',\n multiple: false\n },\n 'cellClick': {\n name: 'widget-action.cell-click',\n multiple: true\n }\n };\n}\n\nself.onDestroy = function() {\n}\n", - "settingsSchema": "", - "dataKeySettingsSchema": "", + "controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.entitiesTableWidget.onDataUpdated();\n}\n\nself.onEditModeChanged = function() {\n self.ctx.$scope.entitiesTableWidget.onEditModeChanged();\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n hasDataPageLink: true,\n warnOnPageDataOverflow: false,\n dataKeysOptional: true,\n supportsUnitConversion: true,\n defaultDataKeysFunction: function() {\n return [{ name: 'displayName', type: 'entityField' }];\n }\n };\n}\n\nself.actionSources = function() {\n return {\n 'actionCellButton': {\n name: 'widget-action.action-cell-button',\n multiple: true,\n hasShowCondition: true\n },\n 'rowClick': {\n name: 'widget-action.row-click',\n multiple: false\n },\n 'rowDoubleClick': {\n name: 'widget-action.row-double-click',\n multiple: false\n },\n 'cellClick': {\n name: 'widget-action.cell-click',\n multiple: true\n }\n };\n}\n\nself.onDestroy = function() {\n}\n", "settingsDirective": "tb-entities-table-widget-settings", "dataKeySettingsDirective": "tb-entities-table-key-settings", "hasBasicMode": true, "basicModeDirective": "tb-entities-table-basic-config", "defaultConfig": "{\"timewindow\":{\"realtime\":{\"interval\":1000,\"timewindowMs\":86400000},\"aggregation\":{\"type\":\"NONE\",\"limit\":200}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"4px\",\"settings\":{\"enableSearch\":true,\"enableSelectColumnDisplay\":true,\"enableStickyHeader\":true,\"enableStickyAction\":true,\"reserveSpaceForHiddenAction\":\"true\",\"displayEntityName\":false,\"displayEntityLabel\":false,\"displayEntityType\":false,\"displayPagination\":true,\"defaultPageSize\":10,\"defaultSortOrder\":\"name\",\"useRowStyleFunction\":false,\"entitiesTitle\":\"Entities\"},\"title\":\"Entities table\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400,\"padding\":\"5px 10px 5px 10px\"},\"useDashboardTimewindow\":false,\"showLegend\":false,\"datasources\":[{\"type\":\"function\",\"name\":\"Simulated\",\"entityAliasId\":null,\"filterId\":null,\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Entity name\",\"color\":\"#2196f3\",\"settings\":{\"columnWidth\":\"0px\",\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.472295003170325,\"funcBody\":\"return 'Simulated';\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Entity type\",\"color\":\"#607d8b\",\"settings\":{},\"_hash\":0.782057645776538,\"funcBody\":\"return 'Device';\",\"decimals\":null,\"aggregationType\":null,\"usePostProcessing\":null,\"postFuncBody\":null},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Sin\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.904797781901171,\"funcBody\":\"return Math.round(1000*Math.sin(time/5000));\",\"decimals\":0},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Cos\",\"color\":\"#f44336\",\"settings\":{},\"_hash\":0.1961430898042078,\"funcBody\":\"return Math.round(1000*Math.cos(time/5000));\",\"decimals\":0},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#ffc107\",\"settings\":{},\"_hash\":0.7678057538205878,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\",\"decimals\":2}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"displayTimewindow\":false,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":false,\"titleIcon\":\"list\",\"iconColor\":null}" }, - "tags": [ - "administration", - "management" - ], "resources": [ { "link": "/api/images/system/entities_table_system_widget_image.png", @@ -36,5 +30,10 @@ "data": "iVBORw0KGgoAAAANSUhEUgAAAMgAAACgCAAAAABslHx1AAAAAmJLR0QA/4ePzL8AAAnhSURBVHja7d3rU5NXHsBx/7KAa8WuBRahaEAIiQRFQSAKXlrUpagYAmgBJbXKin3qKlaKYAOmtFlQLpWkgCgSRC6GiwZFQAi3J/nuC9Da2RGSTNtZmXNeJWfmOfl9Juc2c855zjoWnU8HPvD01LnAukXHpMwHnuRJx+I65yRrIE061z2V1wJEfrpugDWRBgREQAREQAREQATkQ4A4X/sBkXW34IHO7d9PfqXT6XS6Tr9DztXpsv/zP7mJZf5AFEEjNCn8nEZ2WE4HWSxjfkPUxyyXAxv/IEjoAZoUsnxxx24b5ScOaaqMquwFHqSoL3u8KK1mM0h6xrT9WYXalF7mC2MODnsPKYXdxfycFFdOR2qJKkfmB83hqDK601RFc2Sc1e61HlJVeQep/NjSpJDrEvuMERRHtF9UlHZs+nk6+N8PQhu9hLSuf10V4dakdx1RY4zvyU/2HlI43LKp9rmysUXR3xhQ3RpoHQysatlc5gr5qlt9lvAs++6/35M2LHoFuVuxxayQaSw5qJCLM+hRTBBXfjdQklTnvITIYebDxWiu0a14vf2AVBDodUVVb/xEUeKhtzQvoLnxI9hSW7UNEstaAxe5GU14DcZ99CvGvIO4E6MV8g8RTZVvIeprdwIlSWrxEkLRgaBHaK7Ro5iMPiRJ0oIPVSvtCI6NVY8Cmxs/gvCam9GQWGZbL1P1KeE1GHXeQ+hdr5CLdoycVcy/hYxvKnd++8RbSJciCjSZz/O2U6Ad6pR8aSMPA7qa1/fUBzQuQfoC7jwKKZsOufYs0eAbRGmDUqU8unvLKaXj8in6lVNk/IBVG3z4hReh1O8AUH4NmpStqk5mcsKjzV5DMq7DiVNydmhmgskWC4n1XI1IzqigPSHkn69JrOdKNkPK8b9qQBzd1A+aax/8yN4WkMeagMjjYq4lIAIiIAIiIAIiIAIiIALyZ0MG10QSVUtABERABERABERABERABOQDh3TchuYGuN3x62kZ6DeMAy3lUG8wGDrp0J9aYT+Ay2Aoaf1dTleVL7G4/vVFpZupkpNLy5XdBoOhDsau1PgOeZQIGamwy14R0ADkBzhgIiINci5YLCNPtj9sC3//hoDJTdb6/SXv5ox1+QLJPTeYcZMMqUfdCXAj22LpoU9Z3u87RA6enY/WuOZC5IqkDHB9qnbAcWMapPcCLT+Cru39kGBY3OrgSXZWtysXGuvuV/K6+OBVGXtW9uNVY0noozZ3PsRDTQHAVzUAGc1+tZH0tl9yC5vaM6goiRml6ssdDpqz7qeBWvtprgw83z63IoQTNRNRXfaomaQe0jvNeg5dGfvixliUvStqdrVYTJ/XJXe6wxZo+AzgpGrrgVeEHIk/NO075JtvCu+0nJEkKr68+jU7+2McM/Hj99OgdWp+fzXM7bGxMiTvpinFYtllqzo/Fesx62c3u3nZ9f1+i2XHqrttftpbstvOqayquFMA9mFPUcHMhl6KLvoO6Tiocc1pDndSUTChfJhClOPcLmN25A2A6jPIn1WyCmRva0WSJEmDU+rai5j1M1sArqRKkrTqppTQ19j2IdfVSpeXc7qT5kLgXqbvkIXwdMiIWKSigOPRdUQ5Hlut1+O7ZiLnOF1O7iVWgdSq5K74RboWyYxxYNYT20/9d22JMg9W3coR7qTxADC0fRgZVCNUnyDhAZLRj3Ek5TpcT4WKAjpDF4hyAJ1pUKlO3udqCtRqtRfeDwnURmc+hzJV0iEXdxLBrOd+3L7UF3yt2pM5t1osDap0zWM82lQb6NqxqXXxw9jVKUkTf+SAKLu870fl37VOF4A848Vznt82y7kBz1IxM/70WmKKIiACIiACIiACIiACIiAC8kFAxDq7qFoCIiACIiACIiACIiACIiDvJL8OlM+/+bDgPyRmnPH4jp4UAKKUSqW8WH/4KsB0dN1qZU2FaGOyfneAtD7fh1AcOp1ubyyt0QlZCwDlqp0Zs0D1Nj8gW164Ekx0awEWIgFqC7MlgNzIVdftJ4PBpJHB9e4C7tK5xJk5rzDVRs+2QU5WAcPRC2RVg1PziT+QZ/svswwZ3WVrdwOlEmDLzPMKwl4bl7QJ52fD5zh/zazHFpuhdXJ+Z7w3Lwpwq52z/4DG48DkY8irgc+b/YKkR8hvID0RZ5KOLkNmtU4vIfk37Qmye8dQTgOqF2a9RzmA6XzHHo8c+3z1aOqPg3KIsuVl3L74WX7KnfULcvLYuTcQQI4cXoKcvYmXkOOmGxE6Xdhd2xf9+zHrJ8MAvo3U6UKtq0ezuwcsW1N1ZwAY1zxmYsdr/yAvZmLuLEPsfUsll0rMRWu1IVvrvIAsRA7U5kxNTS26VedrMevlYDdz45X5U1NTq/dgHakAslx4C2Am8R7cjNHGB+7yB8KTiJHuGLvd/qptz0jztvnlNoI3/8jH9ub0LxmPbB06PYUxxIVZzxHJmVPpjPz1af7qq7MHmwDaL+ycp2l8Mc1ot/cDfv0jJdPQcHPUYDAY2rEczR0EGpd2HZlX3YLhMhiKmoD+3EwLOK5DVy2ui0eroVefeXfVWFzFHsBTZJoFaXDcYDAYSoHFIjGyC4iACIiACIiACIiACIiACMj/OUSss4uqJSACIiACIiACIiACIiAfOMQ6Bcz+eOsl4G7zqaxFi6W+782Xh8McAOCY65mXr5L3NFf2APS9Ws6YvMOoxWKx2PyAvNpwBRa0pd8px7msCvQJMhkkXdr55uxoXScbAQib7jZ59/zJE7diG/hp18Y3h8ezg+mTJCnzlB+Q8jNxMHIRjluwzHzkGyQY+mO4P4irjvuDbISe8s6w6aF2fh6+UedBNlU+6Xjv4/NaN7U5tIwlL0OaMoMBSO31A6IZzegEGIwbAnyEbB52nMunwIQzigITG7Fvu1UQOG3WE3uwZreJ7OxbcXkrFlFYASxDZuKfBQN07fOjjTxKoU4PnFFmy75D/nb0s6iGdyEFlRA6bdYT68BUMB0iY1oRYkuWf4Pk354NBjjS7AckP9l4ZpML8GT+6DskGCY/WXgHktkMYdNmPbGj1BqG1awM6Y1z8hbSEWm5HWQBh9rjO2QurMVq/dzUWwLnKvyCzATNF1fgWIaUXEUOfguZC3Xx/QqQUc0gv0HuS1JZ0BXIq/aj+zUfA2zJ84n5xrhx3yEbDLlxl7inNO5dhowqiw8HvYVwYU9x1AoQVbrRaHQvQQzdwGwwjEfO+wEZeA64bR75119cADafILLV2vES6G9+1cngGDaYaHZ2yi8H6JzjxQDuh7arKxyyt1mtVqsHumdgYA6Q2+DlE/yA/Mkp52KVsvdPKvsvhcw3Vo2IuZaACIiACIiACIiAeANZMxcEr40rmyec6xbWxiXa8rq1ca25zH8BTrZIsxZexqkAAAAASUVORK5CYII=", "public": true } + ], + "scada": false, + "tags": [ + "administration", + "management" ] } \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_types/markdown_html_card.json b/application/src/main/data/json/system/widget_types/markdown_html_card.json index 64a847952a..b66e3ee88e 100644 --- a/application/src/main/data/json/system/widget_types/markdown_html_card.json +++ b/application/src/main/data/json/system/widget_types/markdown_html_card.json @@ -11,16 +11,10 @@ "resources": [], "templateHtml": "\n", "templateCss": "#container tb-markdown-widget {\n height: 100%;\n display: block;\n}\n\n#container tb-markdown-widget .tb-markdown-view {\n height: 100%;\n overflow: auto;\n}\n", - "controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.markdownWidget.onDataUpdated();\n}\n\nself.actionSources = function() {\n return {\n 'elementClick': {\n name: 'widget-action.element-click',\n multiple: true\n }\n };\n}\n\nself.typeParameters = function() {\n return {\n dataKeysOptional: true,\n datasourcesOptional: true,\n hasDataPageLink: true\n };\n}\n\nself.onDestroy = function() {\n}\n\n", - "settingsSchema": "", - "dataKeySettingsSchema": "", + "controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.markdownWidget.onDataUpdated();\n}\n\nself.actionSources = function() {\n return {\n 'elementClick': {\n name: 'widget-action.element-click',\n multiple: true\n }\n };\n}\n\nself.typeParameters = function() {\n return {\n dataKeysOptional: true,\n datasourcesOptional: true,\n hasDataPageLink: true,\n hideDataSettings: true\n };\n}\n\nself.onDestroy = function() {\n}\n\n", "settingsDirective": "tb-markdown-widget-settings", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"markdownTextPattern\":\"### Markdown/HTML card\\n - **Current entity**: ${entityName}.\\n - **Current value**: ${Random}.\",\"markdownTextFunction\":\"return '# Some title\\\\n - Entity name: ' + data[0]['entityName'];\",\"useMarkdownTextFunction\":false},\"title\":\"Markdown/HTML Card\",\"showTitleIcon\":false,\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"dropShadow\":true,\"enableFullscreen\":true,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"showLegend\":false}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temperature\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"const baseTemp = 20;\\nconst dailySwing = 10;\\nconst hourlyVariation = Math.sin((time % 24) * Math.PI / 12) * dailySwing;\\nconst randomness = (Math.random() - 0.5) * 2;\\nconst smoothingFactor = 0.8;\\nreturn (prevValue * smoothingFactor) + ((baseTemp + hourlyVariation + randomness) * (1 - smoothingFactor));\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"useMarkdownTextFunction\":false,\"markdownTextPattern\":\"### Markdown/HTML card\\n - **Current entity**: ${entityName}.\\n - **Current value**: ${Temperature}.\",\"markdownTextFunction\":\"return '# Some title\\\\n - Entity name: ' + data[0]['entityName'];\",\"applyDefaultMarkdownStyle\":true,\"markdownCss\":\"\"},\"title\":\"Markdown/HTML Card\",\"showTitleIcon\":false,\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"dropShadow\":true,\"enableFullscreen\":true,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"showLegend\":false,\"useDashboardTimewindow\":true,\"displayTimewindow\":true}" }, - "tags": [ - "web", - "markup" - ], "resources": [ { "link": "/api/images/system/markdown_html_card_system_widget_image.png", @@ -33,5 +27,10 @@ "data": "iVBORw0KGgoAAAANSUhEUgAAAMgAAACgCAMAAAB+IdObAAABdFBMVEX////u7u7g4ODf398nJydISEiCs/Tx8fE/Pz+amppgYGC6urr7+/upqan6+vrW1tbKysr19fX9/f3j4+M5OTn39/fs7OxSUlJxcXHFxcVCQkI8PDyMjIxFRUV+fn40NDTa2trp6el3d3dVVVXAwMCKiorQ0NClpaX6/P8vLy+SkpJbW1srKyvl5eWhoaHn5+dMTEytra2Hh4dlZWVPT0+3t7eenp6Pj49CjO7j7v3MzMx7e3uBgYE2NjabwvbR0dHIyMjV5vvY2NgyMjJra2tUl/A0hO3a6fzA2fqEhIQ4hu1XV1f3+v7Dw8OXl5eUlJT0+P6y0Pi0tLTc3NzV1dWysrKwsLCnyfeIt/U+ie6vr690dHRJSUkqfezy9/641Pnz8/NkoPJIj++rq6vg7fyszfh8r/Tt7e28vLySvfZZmvDR4/uNuvV2q/Pp8v1ppPJMku9oaGifxfctf+zT09NnZ2dfnvHL3/vG3fpup/Lt9P4vgO3lU88CAAAQR0lEQVR42uyby2/aQBDGJ7jINti8DOb9dAA1lEASHhIQJWp7QSiJ1JYcaA6VckivkdL/v4aPLbs1bkqLlZTmOwzKesa7P8WKP80QIq1s+P5xGeWQzWGYez+L/jFpsqFR2dz750GI5DIZe7sAohnk2wkQ8r2ArBQ6o6XCUXpEMS7TS5DmLQcyjaVNIS6UmBFdC8eQ/LSUX3psu6K0NjP2absg5538CiSRp6QpxIVKr3zGq7d/CJJQE2sz5fhWQcz7+gokeJxX3wX5iIqLRu+wkaaj1HBAdH6c+tyyQUKXTaoNC3GJSoVRifQy0Z0u51KFMEFYCedyBtEy8+0oVZXJN6pEyBi/zuV823y0HlYgtUrl0qrxERXX6sm9ek1vS8HTL3RyUIskJH/ivUrNbPBrUTKVYFcxP5+3moOUpNSOsiFUYcVMdyK0zGxmu2eKZOaDEcWQg8V0WvYIhIb7el+I0GG98rneIznYzZfoJE1EktI4IqoX5g/MPA71/mxU+dCX/KtHCCtEgQjLPFxEPdDtBo7waHkEcnU6ydxc8REVB7pp6ld7mWovG16CZK0DIrU6P9g8VtXZQaBxNZ2DJMOowgpAlpnHi9hQVbXmKYisyFY4ykdUHM+IZtXIhMhiIP7EtxxF2lH7YOfJUKgTKSnqpVLiQbBC9D5Iy8wzJfwpLkU6i9tqrxNbBUkpbzIXS5DwSUyJCREqTImmhUSgHSiWGAiFrBnlFOudQceZTJW0otkqajwIVoiC8c7+MvM68L5o0FXWurHXh5nk/hO9EOVyiESFFivRKP1K0btlZixEzdO5+8Z9ZPnp3+x/pmYmf/PheVmUZ6EdBQkW+h93ASQcOCp0dgHEVvmNuRsg9cBu/EaCmfBOgKSVwU781bq7+bobf37DrzKZzHQHQHbnhfgC8vRyAdlqLwuKkSBPQczZqC+tQD49IG7WyxJrhY4WL09ByuPbyoSBaL7DnqEhbtbLEmuFjpYoLx+tj0UGIrUtqy0hbtjLEmqxz7KjNaikrgjC3bwC0U8uGQiVG4EW4sa9LL4WQkcrEb/4eE4Q7uYVyEHg3mQgh7p+iLhxL4uvhdAIit1UDWLC3bx6tO7a+wBh+vNeFiSAkKz6RwThbt6A2L+MVubcCbJ5L8shdLRiIdKyPvyMu3kDEslPlJHwHvmLXpYodLTKmUb7RMPPuJtHj1brobX43GIvS+xoaS1ZuNvTv9mfnf4HkNbt7YDM5dwiJrgsp9xz3N2apyCRDz9A9i19QGeFbER0SpLfZernmuMUcrwEOcsGViAp9vcfTok7gHPq55LzVCDliboWBE4JUz/ptFJ4u5z6wV9hDuiWg30Gw1x/JDvdWvmQqLcnV1Nj3zZBqr2va0HglDD1k7KRo6yGqR/8FeaAbjlLb3A1HI9Up1sLJ+feQFLObrPmFpsPlrkeBBFTv/kjkffhsYG/whzQLYeZnPpld+xwawsQ5Ff07YEcZxqd00tXEEz98O7GIeGvMAd05ogg1/Xu2OnWSh2Wn1O3ByI9POgTSQCBR0LE1A+HxNQP/gpzQLccBkLUHTvdmlz8MgfJx7RkeksgkPhoMY+EiKkfDompH/wV5oBuOTzIGrfWVyw7/40/M/TohQgQeCRENvWDuKkf5oAuOY+6tYSp2VBRmTwDuYhbqnPq58lkUPK/eK1HQeCyNtHg9raFp4fmmkZ/x00hE1H0YLHNHJoIct7r9VoAgcvaCES39oloL9ldWE4lxjsuRKeQiYgcVLn0wXD1cZDxZwbC3h0bKbU/Dz20Lo55x4XoFDIRkYMqOLc/Bxle20EAgTtCtwo+Cu4I69x3rhhIqY1nohEh3nEhwlOx7lYwyDIRkYMqODd+X1RJ8dGwRuik4QxrQe5TelkEgTtCtwo+CqYC6/x3rpYgl2jAmVmNeMeFCE/FulsTi2UiIgdVcG78vqiSil+D2RY6aTjD+tGbfp8sCyBwR+hWwUcBBOvCd64AMpwSETpevONChKdi3a1WE5mIyGFV2J3fd16FlUIdnTScwQkCtfcFELgjvKnho+COsC585wogYx21F0So4kHgqfjuFjIRkYMq7M7vi6r5SlVFJw1nWAticn0teCe4I9wQPgruCOvCd64AclTBsUNEvONChKdi3a16HZmIyGFV2J3fF1X2SqIdQScNZ1gHUspwfS14J7gj3BA+Cu4I68J3rgAiZw2ycT7bgXdciPBUrLsVaCCTReSgCrvz+6LKeNdQcoROGs7wWF8L3klwR/BRcEdYF75zBRC6tWSi+6BgsGREeCp0t9ANQybiKpPtLuyLKjTSUIszbG5R4KPcV1QrfrH41Cl6Gv09T4VMxF/7txevxSRMCWMOj4SrLg6q/IVc5TGIGTx0gvglZ+cK7ghXXR3ULOluO70FCXfe99xA4H9Ej+QOgqv9CrnKU5BJffVoofuEKaEwB4TPgUfCVazIl6mKAUfEHJQGSo/lBKkV+/0BA0H3CVNCfg4InwN3xM8QE+1cLS3DEeGqrQOVvJcTZHZzVI+XfoCkiU0J+fEZfA4++RniRYOITQlxxdYUH95LBJme2AZYF0DU6s8g8DlwR/wMcZoiYlNCXLWlV8l7OUHCSuuu81UAwZQQvgj+Bz4H7oifIfriBrEpIa7aGl6T5xJBoFy+k9oTQDAlhC+C/4HPgTvCVbaSTbbZlBBXqalEyVO5/48VfnKdEkbvBI8kXo21ovBR7Gp0sr1fiCcWBe7o8at6j9z1TECenV5Anpt2CeQ7u3b0mjYQwHH8l/kTE03aNUZtrVFrO0Ps1TnbKjilYvIioxaq60v7IPRBXwvr/79LRre6MdhgGemW70vCwR33gQsEkn8kSfknSiBxK4HErQQStxJI3EogcSuBxK3/DTLR8njqvPcby28hSNEG+K6IIVbNCfddq13gefe8xFOP7q+vzj6C5mzh+ybzKCGCJci6ZPNPQPRZ/acQ9UOkkBEngG5ZAWQ7/OicN1LOpYTImz2YudnrEDK+ck6AG8m73AOObnD/eqI5O5AVjRRM4xV0Y5AxLoCTdnsQQF71nKKxDSjObIyCMbKNQoSQW68LrFmVkDbv0mwjx31OJWRlW69wS8v1JKTKsueNcWrDVPdNlE/hqp7KLGRL0UWTQ2zTCI5WfSRUT0KanlAtaphbns1K0RaurUQJeW/p8NOOhOxWTbNjS8hpannPXMlb4IINM2O7GHMKxS6bPb79RC4OpNctK/CZgez4EH2W4fAkgOyKBQwJybopOBKSHR2gK/JRHy1/wLUitJ6EoNmu2J6EXAP3LPMcaHMQPiNvKGe1mJrwulXqXJ2zDvcxGJlDdiV2Og1OdtOQkHDDc7ZM9wFYUNtiyTCmzEUNyeLOb4viTEKmwn8GEWIXmLEeQobMfFGpw1KrX5qqeA5p8h0ndzN1GkJqjyFki40QsqJVkv0FiCbSPlps7ogGcPoV0uvzBjfMyTE3NKDKAzRULi5YbmxA9JFaRsXmOoSc2SbqbGH/EPhEDbVjADqihxQF1wFky+2Me8J9glzqh7V8xkuvg7Fi7WzPCLb+kappqrzegMDnFE2KZQjR2D3KSsgb9uVVk9fKQLNXOLOOCpFC8GDpAQQfPd5lefIEwdz9oB9Z7GRdYM8mfQVQOASGXG1CDI5hqscIIYVbIaoSsvQpHiSk0HXpXQGGYD4yyGZ6ET9krvAlZYlfbVkIZxYzhWuef/tLeJlB2Et7aXSs/sxSNzb/MiHLSid9m8fXXixkswSSQF5KCSRuJZC4lUDiVgL53N699jQNxXEc/6HHTWVDGXL1Ak6Ezg0F5dISLRTsWmvFFZWuKqut28LuczO7wJv3dJTBiGgMooh8k2U77U6yT5om/ydLT1udEO/u3zOXbs1hv14yg936iR9HNt6N7xRNMnEVu/FpHFFdgxsr/BZIHyErAJ6T4K9DhsePgGQOQfQ8DpczD0Gy0nEhq4+BrsgqhYz51/sxMHfB76WQt3O9uO5/40CWJ4cDWP6KT3P0bamve+BzdwC0lWvAxOR4AE6P3s94XUjAZKBqWlGmEEEXkCvUGskcUN9GTAYkvQE466Rel5Jgc2YqwZgKzx0TErzhw8ORcBCTkScjsz3D5NnqAJn5unoL8+TeZQp5Ebq6OoQ7YWyQCdyc95PpMLkF2q3HmJy+QneCgm6v3Rnpda/IZgy1Wl5kwbOFfDQnW6JWF8EoFcgWtDKfzaNSQE6JFwwerBgvZRleiUvHhHSvbmHw9b0gNrawSN4Ok/foJQs3PgR8o4/paXpR1unx5e5Qz1Bo4QIZ85MLGBp0Ic8n0fWgBZkAPl5vQxosgwaFlIA0Dz5NFYmGWEO8wigSElGVQpo8nBcrQ6VyVsBxIcHnK2RqOoi+4PRlcn2YvKGQCLmIKbLl3CMbpAtYveKNLI9emX1/M+An/XhxyYUsjoQnP4HWvxAeDI23IXoTEFhHgHim9dY0PWZ5W0wmq5Zl7UgUUq47ypagWvwNkPXrkQ9XcTuIl0/uruxBwrNhXxeZcyDjZAo9kYd4efVSb+hJEB0QBCaC90BbCPZjcB+ilQ5DUh5bzVdsFHeSHMcxFCI2fjMkcJusO5DLQwMLZHkXMjMWWsN0eMpP/N5nT7vWIhfxgNzHF/KuEzL7ClMRH4ChBxgb3WpDOIVDpQ2hvxrFaBM5xQOUU2B0B5IvQBXbELt+bAjWRvsdyEaIfCGTLgTzkYmlm+RqyI/hy+TZDLBIFjFDBjohr27P3ngI2tLNkcHZ+TYEFcW22hAhWgLEFMDKgFQuGy1bosba+5C4oh0D0lnPJ3QW8KJVlw9H5vV1ftctwahmFnupsY5zKpwYNYGahr22mdM1omQ8rRQxG7U8P8xSsmUlvb/mTxfELaFVJPwkSU/FzofG/wsS22TwneJ5nKbOKCQhcykZgt4AwKU0FaqcTDkQoQ5niDXB5QRdaEOSKXo00Sim5NaGhiRB3gZMZ6Mute7gnMABgl7HCdcJkaKlDGuVMoYG2eYLIrgd0UMhdUPA5jaKVWhKM6+YLkQrV5oWBKWUsXlIiidtZGDngCinimneqKOh5JuGDr3MizwOdbIQhYHGMqgU0JAQiHLczjZim3Ujhz1IFtBEFyInoVYTghKDVkMhAxT2IEUNyHhQ00FfapRDMcqgs5OFGIAsAqkSEvGsWJW4KCjEKKANsQAu6kK2PVlxsyiwgJyFKAPxPQhMK8umYUhAUxd2LMuqcujsT0EKeQaGC0mx5kGIZLiQEs+gugepmQcgOZsDn6YLB5JUONrfuiLZFOo7wi6EyRkcjDpkCrFjiDddSNmEvMm5kEyJUbMZ1FKQqpwpMqpVQNqDhK0ztgZVx6H+FERWWIs1XQh4kUkpdsmBiHaZcyGmwpaUhguJWYYtZug+u0Zv9prBNrMoimy5rEOgI+6fvNk7YxJo1Tm0ahYOHGdiBz8nAiW9vS/BOFvoQpTd1cnWCfH8NIs96kyJjq/RdOexmpEtG+5nE0f0d2YtyfzR+JrAoYSKpuKHnQ+N55BT3jnktHUOOW2dIUifD2cgXx/uenEG8t5FT5/3n78mzkO0z8hjzX34BpPgTEZLPbVzAAAAAElFTkSuQmCC", "public": true } + ], + "scada": false, + "tags": [ + "web", + "markup" ] } \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_types/photo_camera_input.json b/application/src/main/data/json/system/widget_types/photo_camera_input.json index 69327395cc..fc676380ab 100644 --- a/application/src/main/data/json/system/widget_types/photo_camera_input.json +++ b/application/src/main/data/json/system/widget_types/photo_camera_input.json @@ -15,7 +15,7 @@ "settingsSchema": "", "dataKeySettingsSchema": "{}\n", "settingsDirective": "tb-photo-camera-input-widget-settings", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Photo camera input\",\"showTitleIcon\":false,\"titleIcon\":\"more_horiz\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"displayTimewindow\":true,\"showLegend\":false,\"actions\":{}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"widgetTitle\":\"\",\"saveToGallery\":true,\"usePublicGalleryLink\":false,\"imageFormat\":\"image/png\",\"imageQuality\":0.92,\"maxWidth\":640,\"maxHeight\":480},\"title\":\"Photo camera input\",\"showTitleIcon\":false,\"titleIcon\":\"more_horiz\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"showLegend\":false,\"actions\":{}}" }, "resources": [ { diff --git a/application/src/main/data/json/system/widget_types/rpc_debug_terminal.json b/application/src/main/data/json/system/widget_types/rpc_debug_terminal.json index e65e9a1204..40399f034b 100644 --- a/application/src/main/data/json/system/widget_types/rpc_debug_terminal.json +++ b/application/src/main/data/json/system/widget_types/rpc_debug_terminal.json @@ -11,7 +11,7 @@ "resources": [], "templateHtml": "
", "templateCss": ".cmd .cursor.blink {\n -webkit-animation-name: terminal-underline;\n -moz-animation-name: terminal-underline;\n -ms-animation-name: terminal-underline;\n animation-name: terminal-underline;\n}\n.terminal .inverted, .cmd .inverted {\n border-bottom-color: #aaa;\n}\n\n", - "controllerScript": "var requestTimeout = 500;\nvar requestPersistent = false;\nvar persistentPollingInterval = 5000;\n\nself.onInit = function() {\n var subscription = self.ctx.defaultSubscription;\n var rpcEnabled = subscription.rpcEnabled;\n var deviceName = 'Simulated';\n var prompt;\n if (subscription.targetEntityName && subscription.targetEntityName.length) {\n deviceName = subscription.targetEntityName;\n }\n if (self.ctx.settings.requestTimeout) {\n requestTimeout = self.ctx.settings.requestTimeout;\n }\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 var greetings = 'Welcome to ThingsBoard RPC debug terminal.\\n\\n';\n if (!rpcEnabled) {\n greetings += 'Target device is not set!\\n\\n';\n prompt = '';\n } else {\n greetings += 'Current target device for RPC commands: [[b;#fff;]' + deviceName + ']\\n\\n';\n greetings += 'Please type [[b;#fff;]\\'help\\'] to see usage.\\n';\n prompt = '[[b;#8bc34a;]' + deviceName +']> ';\n }\n \n var terminal = $('#device-terminal', self.ctx.$container).terminal(\n function(command) {\n if (command !== '') {\n try {\n var localCommand = command.trim();\n var requestUUID = uuidv4();\n if (localCommand === 'help') {\n printUsage(this);\n } else {\n var spaceIndex = localCommand.indexOf(' ');\n if (spaceIndex === -1 && !localCommand.length) {\n this.error(\"Wrong number of arguments!\");\n this.echo(' ');\n } else {\n var params;\n if (spaceIndex === -1) {\n spaceIndex = localCommand.length;\n }\n var name = localCommand.substr(0, spaceIndex);\n var args = localCommand.substr(spaceIndex + 1);\n if (args.length) {\n try {\n params = JSON.parse(args);\n } catch (e) {\n params = args;\n }\n }\n performRpc(this, name, params, requestUUID);\n }\n }\n } catch(e) {\n this.error(new String(e));\n }\n } else {\n this.echo('');\n }\n }, {\n greetings: greetings,\n prompt: prompt,\n enabled: rpcEnabled\n });\n \n if (!rpcEnabled) {\n terminal.error('No RPC target detected!').pause();\n }\n}\n\n\nfunction printUsage(terminal) {\n var commandsListText = '\\n[[b;#fff;]Usage:]\\n';\n commandsListText += ' [params body]]\\n\\n';\n commandsListText += '[[b;#fff;]Example 1:]\\n'; \n commandsListText += ' myRemoteMethod1 myText\\n\\n'; \n commandsListText += '[[b;#fff;]Example 2:]\\n'; \n commandsListText += ' myOtherRemoteMethod \"{\\\\\"key1\\\\\": 2, \\\\\"key2\\\\\": \\\\\"myVal\\\\\"}\"\\n'; \n terminal.echo(new String(commandsListText));\n}\n\n\nfunction performRpc(terminal, method, params, requestUUID) {\n terminal.pause();\n self.ctx.controlApi.sendTwoWayCommand(method, params, requestTimeout, requestPersistent, persistentPollingInterval, requestUUID).subscribe(\n function success(responseBody) {\n terminal.echo(JSON.stringify(responseBody));\n terminal.echo(' ');\n terminal.resume();\n },\n function fail() {\n var errorText = self.ctx.defaultSubscription.rpcErrorText;\n terminal.error(errorText);\n terminal.echo(' ');\n terminal.resume();\n }\n );\n}\n\n\nfunction uuidv4() {\n return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {\n var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);\n return v.toString(16);\n });\n}\n\n \nself.onDestroy = function() {\n self.ctx.controlApi.completedCommand();\n}", + "controllerScript": "var requestTimeout = 500;\nvar requestPersistent = false;\nvar persistentPollingInterval = 5000;\n\nself.onInit = function() {\n var subscription = self.ctx.defaultSubscription;\n var rpcEnabled = subscription.rpcEnabled;\n var deviceName = 'Simulated';\n var prompt;\n if (subscription.targetEntityName && subscription.targetEntityName.length) {\n deviceName = subscription.targetEntityName;\n }\n if (self.ctx.settings.requestTimeout) {\n requestTimeout = self.ctx.settings.requestTimeout;\n }\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 var greetings = 'Welcome to ThingsBoard RPC debug terminal.\\n\\n';\n if (!rpcEnabled) {\n greetings += 'Target device is not set!\\n\\n';\n prompt = '';\n } else {\n greetings += 'Current target device for RPC commands: [[b;#fff;]' + deviceName + ']\\n\\n';\n greetings += 'Please type [[b;#fff;]\\'help\\'] to see usage.\\n';\n prompt = '[[b;#8bc34a;]' + deviceName +']> ';\n }\n \n var terminal = $('#device-terminal', self.ctx.$container).terminal(\n function(command) {\n if (command !== '') {\n try {\n var localCommand = command.trim();\n var requestUUID = uuidv4();\n if (localCommand === 'help') {\n printUsage(this);\n } else {\n var spaceIndex = localCommand.indexOf(' ');\n if (spaceIndex === -1 && !localCommand.length) {\n this.error(\"Wrong number of arguments!\");\n this.echo(' ');\n } else {\n var params;\n if (spaceIndex === -1) {\n spaceIndex = localCommand.length;\n }\n var name = localCommand.substr(0, spaceIndex);\n var args = localCommand.substr(spaceIndex + 1);\n if (args.length) {\n try {\n params = JSON.parse(args);\n } catch (e) {\n params = args;\n }\n }\n performRpc(this, name, params, requestUUID);\n }\n }\n } catch(e) {\n this.error(new String(e));\n }\n } else {\n this.echo('');\n }\n }, {\n greetings: greetings,\n prompt: prompt,\n enabled: rpcEnabled\n });\n \n if (!rpcEnabled) {\n terminal.error('No RPC target detected!').pause();\n }\n}\n\n\nfunction printUsage(terminal) {\n var commandsListText = '\\n[[b;#fff;]Usage:]\\n';\n commandsListText += ' [params body]\\n\\n';\n commandsListText += '[[b;#fff;]Example 1:]\\n'; \n commandsListText += ' myRemoteMethod1 myText\\n\\n'; \n commandsListText += '[[b;#fff;]Example 2:]\\n'; \n commandsListText += ' myOtherRemoteMethod \"{\\\\\"key1\\\\\": 2, \\\\\"key2\\\\\": \\\\\"myVal\\\\\"}\"\\n'; \n terminal.echo(new String(commandsListText));\n}\n\n\nfunction performRpc(terminal, method, params, requestUUID) {\n terminal.pause();\n self.ctx.controlApi.sendTwoWayCommand(method, params, requestTimeout, requestPersistent, persistentPollingInterval, requestUUID).subscribe(\n function success(responseBody) {\n terminal.echo(JSON.stringify(responseBody));\n terminal.echo(' ');\n terminal.resume();\n },\n function fail() {\n var errorText = self.ctx.defaultSubscription.rpcErrorText;\n terminal.error(errorText);\n terminal.echo(' ');\n terminal.resume();\n }\n );\n}\n\n\nfunction uuidv4() {\n return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {\n var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);\n return v.toString(16);\n });\n}\n\n \nself.onDestroy = function() {\n self.ctx.controlApi.completedCommand();\n}", "settingsSchema": "", "dataKeySettingsSchema": "{}\n", "settingsDirective": "tb-rpc-terminal-widget-settings", @@ -43,4 +43,4 @@ "public": true } ] -} \ No newline at end of file +} diff --git a/application/src/main/data/json/system/widget_types/timeseries_table.json b/application/src/main/data/json/system/widget_types/timeseries_table.json index 8343eb918c..15779eb902 100644 --- a/application/src/main/data/json/system/widget_types/timeseries_table.json +++ b/application/src/main/data/json/system/widget_types/timeseries_table.json @@ -17,7 +17,7 @@ "latestDataKeySettingsDirective": "tb-timeseries-table-latest-key-settings", "hasBasicMode": true, "basicModeDirective": "tb-timeseries-table-basic-config", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"entityAliasId\":null,\"filterId\":null,\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temperature °C\",\"color\":\"#2196f3\",\"settings\":{\"useCellStyleFunction\":true,\"cellStyleFunction\":\"if (value) {\\n var percent = (value + 60)/120 * 100;\\n var color = tinycolor.mix('blue', 'red', percent);\\n color.setAlpha(.5);\\n return {\\n paddingLeft: '20px',\\n color: '#ffffff',\\n background: color.toRgbString(),\\n fontSize: '18px'\\n };\\n} else {\\n return {};\\n}\",\"useCellContentFunction\":false},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nvar multiplier = Math.pow(10, 1 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\",\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Humidity, %\",\"color\":\"#ffc107\",\"settings\":{\"useCellStyleFunction\":true,\"cellStyleFunction\":\"if (value) {\\n var percent = value;\\n var backgroundColor = tinycolor('blue');\\n backgroundColor.setAlpha(value/100);\\n var color = 'blue';\\n if (value > 50) {\\n color = 'white';\\n }\\n \\n return {\\n paddingLeft: '20px',\\n color: color,\\n background: backgroundColor.toRgbString(),\\n fontSize: '18px'\\n };\\n} else {\\n return {};\\n}\",\"useCellContentFunction\":false},\"_hash\":0.12775350966079668,\"funcBody\":\"var value = prevValue + Math.random() * 20 - 10;\\nvar multiplier = Math.pow(10, 1 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 5) {\\n\\tvalue = 5;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value;\"}],\"latestDataKeys\":null}],\"timewindow\":{\"realtime\":{\"interval\":1000,\"timewindowMs\":60000},\"aggregation\":{\"type\":\"NONE\",\"limit\":200}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"showTimestamp\":true,\"displayPagination\":true,\"defaultPageSize\":10},\"title\":\"Timeseries table\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400,\"padding\":\"5px 10px 5px 10px\"},\"useDashboardTimewindow\":false,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"showTitleIcon\":false,\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"displayTimewindow\":true,\"configMode\":\"basic\"}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"entityAliasId\":null,\"filterId\":null,\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temperature °C\",\"color\":\"#2196f3\",\"settings\":{\"useCellStyleFunction\":true,\"cellStyleFunction\":\"if (value) {\\n var percent = (value + 60)/120 * 100;\\n var color = tinycolor.mix('blue', 'red', percent);\\n color.setAlpha(.5);\\n return {\\n paddingLeft: '20px',\\n color: '#ffffff',\\n background: color.toRgbString(),\\n fontSize: '18px'\\n };\\n} else {\\n return {};\\n}\",\"useCellContentFunction\":false},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nvar multiplier = Math.pow(10, 1 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\",\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Humidity, %\",\"color\":\"#ffc107\",\"settings\":{\"useCellStyleFunction\":true,\"cellStyleFunction\":\"if (value) {\\n var percent = value;\\n var backgroundColor = tinycolor('blue');\\n backgroundColor.setAlpha(value/100);\\n var color = 'blue';\\n if (value > 50) {\\n color = 'white';\\n }\\n \\n return {\\n paddingLeft: '20px',\\n color: color,\\n background: backgroundColor.toRgbString(),\\n fontSize: '18px'\\n };\\n} else {\\n return {};\\n}\",\"useCellContentFunction\":false},\"_hash\":0.12775350966079668,\"funcBody\":\"var value = prevValue + Math.random() * 20 - 10;\\nvar multiplier = Math.pow(10, 1 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 5) {\\n\\tvalue = 5;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value;\"}],\"latestDataKeys\":null}],\"timewindow\":{\"realtime\":{\"interval\":1000,\"timewindowMs\":60000},\"aggregation\":{\"type\":\"NONE\",\"limit\":200}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"enableSearch\":true,\"enableSelectColumnDisplay\":true,\"enableStickyHeader\":true,\"enableStickyAction\":true,\"showCellActionsMenu\":true,\"reserveSpaceForHiddenAction\":\"true\",\"showTimestamp\":true,\"dateFormat\":{\"format\":\"yyyy-MM-dd HH:mm:ss\"},\"displayPagination\":true,\"useEntityLabel\":false,\"defaultPageSize\":10,\"pageStepCount\":3,\"pageStepIncrement\":10,\"hideEmptyLines\":false,\"disableStickyHeader\":false,\"useRowStyleFunction\":false,\"rowStyleFunction\":\"\",\"tabSortKey\":\"timestamp\"},\"title\":\"Timeseries table\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400,\"padding\":\"5px 10px 5px 10px\"},\"useDashboardTimewindow\":false,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"showTitleIcon\":false,\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"displayTimewindow\":true,\"configMode\":\"basic\"}" }, "resources": [ { @@ -32,4 +32,4 @@ "public": true } ] -} \ No newline at end of file +} diff --git a/application/src/main/data/json/tenant/device_profile/rule_chain_template.json b/application/src/main/data/json/tenant/device_profile/rule_chain_template.json index 305dc04961..8773a2d6aa 100644 --- a/application/src/main/data/json/tenant/device_profile/rule_chain_template.json +++ b/application/src/main/data/json/tenant/device_profile/rule_chain_template.json @@ -10,12 +10,12 @@ "configuration": null }, "metadata": { - "firstNodeIndex": 6, + "firstNodeIndex": 2, "nodes": [ { "additionalInfo": { - "layoutX": 822, - "layoutY": 294 + "layoutX": 824, + "layoutY": 156 }, "type": "org.thingsboard.rule.engine.telemetry.TbMsgTimeseriesNode", "name": "Save Timeseries", @@ -30,8 +30,8 @@ }, { "additionalInfo": { - "layoutX": 824, - "layoutY": 221 + "layoutX": 825, + "layoutY": 52 }, "type": "org.thingsboard.rule.engine.telemetry.TbMsgAttributesNode", "name": "Save Client Attributes", @@ -48,8 +48,8 @@ }, { "additionalInfo": { - "layoutX": 494, - "layoutY": 309 + "layoutX": 347, + "layoutY": 149 }, "type": "org.thingsboard.rule.engine.filter.TbMsgTypeSwitchNode", "name": "Message Type Switch", @@ -59,8 +59,8 @@ }, { "additionalInfo": { - "layoutX": 824, - "layoutY": 383 + "layoutX": 825, + "layoutY": 266 }, "type": "org.thingsboard.rule.engine.action.TbLogNode", "name": "Log RPC from Device", @@ -72,8 +72,8 @@ }, { "additionalInfo": { - "layoutX": 823, - "layoutY": 444 + "layoutX": 825, + "layoutY": 379 }, "type": "org.thingsboard.rule.engine.action.TbLogNode", "name": "Log Other", @@ -85,27 +85,14 @@ }, { "additionalInfo": { - "layoutX": 822, - "layoutY": 507 + "layoutX": 825, + "layoutY": 468 }, "type": "org.thingsboard.rule.engine.rpc.TbSendRPCRequestNode", "name": "RPC Call Request", "configuration": { "timeoutInSeconds": 60 } - }, - { - "additionalInfo": { - "description": "", - "layoutX": 209, - "layoutY": 307 - }, - "type": "org.thingsboard.rule.engine.profile.TbDeviceProfileNode", - "name": "Device Profile Node", - "configuration": { - "persistAlarmRulesState": false, - "fetchAlarmRulesStateOnStart": false - } } ], "connections": [ @@ -133,11 +120,6 @@ "fromIndex": 2, "toIndex": 5, "type": "RPC Request to Device" - }, - { - "fromIndex": 6, - "toIndex": 2, - "type": "Success" } ], "ruleChainConnections": null diff --git a/application/src/main/data/json/tenant/rule_chains/root_rule_chain.json b/application/src/main/data/json/tenant/rule_chains/root_rule_chain.json index a988c9d5eb..c48dab1964 100644 --- a/application/src/main/data/json/tenant/rule_chains/root_rule_chain.json +++ b/application/src/main/data/json/tenant/rule_chains/root_rule_chain.json @@ -9,7 +9,7 @@ "configuration": null }, "metadata": { - "firstNodeIndex": 6, + "firstNodeIndex": 2, "nodes": [ { "additionalInfo": { @@ -92,27 +92,9 @@ "configuration": { "timeoutInSeconds": 60 } - }, - { - "additionalInfo": { - "description": "Process incoming messages from devices with the alarm rules defined in the device profile. Dispatch all incoming messages with \"Success\" relation type.", - "layoutX": 204, - "layoutY": 240 - }, - "type": "org.thingsboard.rule.engine.profile.TbDeviceProfileNode", - "name": "Device Profile Node", - "configuration": { - "persistAlarmRulesState": false, - "fetchAlarmRulesStateOnStart": false - } } ], "connections": [ - { - "fromIndex": 6, - "toIndex": 2, - "type": "Success" - }, { "fromIndex": 2, "toIndex": 4, diff --git a/application/src/main/data/resources/dashboards/gateways_dashboard.json b/application/src/main/data/resources/dashboards/gateways_dashboard.json index 381c9f6de0..078f913570 100644 --- a/application/src/main/data/resources/dashboards/gateways_dashboard.json +++ b/application/src/main/data/resources/dashboards/gateways_dashboard.json @@ -650,7 +650,7 @@ "settings": { "useMarkdownTextFunction": true, "markdownTextPattern": "# Markdown/HTML card \\n - **Current entity**: **${entityName}**. \\n - **Current value**: **${Random}**.", - "markdownTextFunction": "var blockData = '';\nvar connectorsIndex = ctx.actionsApi.getActionDescriptors('elementClick').findIndex(action => action.name == \"Connectors\");\nvar logsIndex = ctx.actionsApi.getActionDescriptors('elementClick').findIndex(action => action.name == \"Logs\");\nfunction generateMatHeader(index) {\n if (index !== undefined && index > -1) {\n return ``\n } else {\n return \"\"\n }\n}\nfunction createDataBlock(value, label, dividerStyle, mobile, index) {\n blockData += `\n \n
\n \n ${generateMatHeader(index)}\n ${label}\n
\n ${value}\n `;\n}\ncreateDataBlock(data[0].Status, \"Status\", data[0].Status === \"Active\" ? 'divider-green' : 'divider-red');\ncreateDataBlock(data[0].Name, \"Gateway Name\", '', ctx.isMobile);\nif (data[0].Version) {\n createDataBlock(data[0].Version, \"Gateway Version\", '');\n}\ncreateDataBlock(data[0].Type, \"Gateway Type\", '');\ncreateDataBlock(\n `${(data[1] ? data[1].count : 0)} `\n + \" | \" +\n `${(data[2] ? data[2][\"count 2\"] : 0)} `\n , \"Devices (Active | Inactive)\", '');\ncreateDataBlock(\n `${(data[0].active_connectors ? JSON.parse(data[0].active_connectors).length : 0)} `\n + \" | \" +\n `${(data[0].inactive_connectors ? JSON.parse(data[0].inactive_connectors).length : 0)} `\n , \"Connectors (Enabled | Disabled)\", '', '', connectorsIndex);\ncreateDataBlock(data[0].ALL_ERRORS_COUNT || 0, \"Errors\", (data[0].ALL_ERRORS_COUNT || 0) === 0 ? 'divider-green' : 'divider-red', '', logsIndex);\nreturn `
${blockData}
`;", + "markdownTextFunction": "var blockData = '';\nvar connectorsIndex = ctx.actionsApi.getActionDescriptors('elementClick').findIndex(action => action.name == \"Connectors\");\nvar logsIndex = ctx.actionsApi.getActionDescriptors('elementClick').findIndex(action => action.name == \"Logs\");\nfunction generateMatHeader(index) {\n if (index !== undefined && index > -1) {\n return ``\n } else {\n return \"\"\n }\n}\nfunction createDataBlock(value, label, dividerStyle, mobile, index) {\n blockData += `\n \n
\n \n ${generateMatHeader(index)}\n ${label}\n
\n ${ctx.sanitizer.sanitize(1, value)}\n `;\n}\ncreateDataBlock(data[0].Status, \"Status\", data[0].Status === \"Active\" ? 'divider-green' : 'divider-red');\ncreateDataBlock(data[0].Name, \"Gateway Name\", '', ctx.isMobile);\nif (data[0].Version) {\n createDataBlock(data[0].Version, \"Gateway Version\", '');\n}\ncreateDataBlock(data[0].Type, \"Gateway Type\", '');\ncreateDataBlock(\n `${(data[1] ? data[1].count : 0)} `\n + \" | \" +\n `${(data[2] ? data[2][\"count 2\"] : 0)} `\n , \"Devices (Active | Inactive)\", '');\ncreateDataBlock(\n `${(data[0].active_connectors ? JSON.parse(data[0].active_connectors).length : 0)} `\n + \" | \" +\n `${(data[0].inactive_connectors ? JSON.parse(data[0].inactive_connectors).length : 0)} `\n , \"Connectors (Enabled | Disabled)\", '', '', connectorsIndex);\ncreateDataBlock(data[0].ALL_ERRORS_COUNT || 0, \"Errors\", (data[0].ALL_ERRORS_COUNT || 0) === 0 ? 'divider-green' : 'divider-red', '', logsIndex);\nreturn `
${blockData}
`;", "applyDefaultMarkdownStyle": false, "markdownCss": ".divider {\n position: absolute;\n width: 3px;\n top: 8px;\n border-radius: 2px;\n bottom: 8px;\n border: 1px solid rgba(31, 70, 144, 1);\n background-color: rgba(31, 70, 144, 1);\n left: 10px;\n}\n.divider-green .divider {\n border: 1px solid rgb(25,128,56);\n background-color: rgb(25,128,56);\n}\n\n.divider-green .mat-mdc-card-content {\n color: rgb(25,128,56);\n}\n\n.divider-red .divider {\n border: 1px solid rgb(203,37,48);\n background-color: rgb(203,37,48);\n}\n\n.divider-red .mat-mdc-card-content {\n color: rgb(203,37,48);\n}\n\n.mdc-card {\n position: relative;\n padding-left: 10px;\n margin-bottom: 1px;\n}\n\n.mat-mdc-card-subtitle {\n font-weight: 400;\n font-size: 12px;\n}\n\n.mat-mdc-card-header {\n padding: 8px 16px 0;\n}\n\n.mat-mdc-card-content:last-child {\n padding-bottom: 8px;\n font-size: 16px;\n}\n\n.cards-container {\n height: calc(100% - 1px);\n justify-content: stretch;\n align-items: center;\n margin-bottom: 1px;\n}\n\n::ng-deep.tb-home-widget-link > div {\n flex-grow: 1;\n cursor: pointer;\n}\n\n .tb-home-widget-link {\n width: 100%;\n }\n\n .tb-home-widget-link:hover::after{\n color: inherit;\n }\n \n .tb-home-widget-link::after{\n content: 'arrow_forward';\n display: inline-block;\n transform: rotate(315deg);\n font-family: 'Material Icons';\n font-weight: normal;\n font-style: normal;\n font-size: 18px;\n color: rgba(0, 0, 0, 0.12);\n vertical-align: bottom;\n margin-left: 6px;\n}" }, diff --git a/application/src/main/data/upgrade/basic/schema_update.sql b/application/src/main/data/upgrade/basic/schema_update.sql index add832ea6e..a7772c7b36 100644 --- a/application/src/main/data/upgrade/basic/schema_update.sql +++ b/application/src/main/data/upgrade/basic/schema_update.sql @@ -14,33 +14,73 @@ -- limitations under the License. -- --- UPDATE OTA PACKAGE EXTERNAL ID START +-- UPDATE TENANT PROFILE CONFIGURATION START -ALTER TABLE ota_package - ADD COLUMN IF NOT EXISTS external_id uuid; +UPDATE tenant_profile +SET profile_data = jsonb_set( + profile_data, + '{configuration}', + (profile_data -> 'configuration') + || jsonb_strip_nulls( + jsonb_build_object( + 'minAllowedScheduledUpdateIntervalInSecForCF', + CASE + WHEN (profile_data -> 'configuration') ? 'minAllowedScheduledUpdateIntervalInSecForCF' + THEN NULL + ELSE to_jsonb(60) + END, + 'maxRelationLevelPerCfArgument', + CASE + WHEN (profile_data -> 'configuration') ? 'maxRelationLevelPerCfArgument' + THEN NULL + ELSE to_jsonb(10) + END, + 'maxRelatedEntitiesToReturnPerCfArgument', + CASE + WHEN (profile_data -> 'configuration') ? 'maxRelatedEntitiesToReturnPerCfArgument' + THEN NULL + ELSE to_jsonb(100) + END, + 'minAllowedDeduplicationIntervalInSecForCF', + CASE + WHEN (profile_data -> 'configuration') ? 'minAllowedDeduplicationIntervalInSecForCF' + THEN NULL + ELSE to_jsonb(60) + END, + 'minAllowedAggregationIntervalInSecForCF', + CASE + WHEN (profile_data -> 'configuration') ? 'minAllowedAggregationIntervalInSecForCF' + THEN NULL + ELSE to_jsonb(60) + END + ) + ), + false + ) +WHERE NOT ( + (profile_data -> 'configuration') ? 'minAllowedScheduledUpdateIntervalInSecForCF' + AND + (profile_data -> 'configuration') ? 'maxRelationLevelPerCfArgument' + AND + (profile_data -> 'configuration') ? 'maxRelatedEntitiesToReturnPerCfArgument' + AND + (profile_data -> 'configuration') ? 'minAllowedDeduplicationIntervalInSecForCF' + AND + (profile_data -> 'configuration') ? 'minAllowedAggregationIntervalInSecForCF' + ); -DO -$$ - BEGIN - IF NOT EXISTS(SELECT 1 FROM pg_constraint WHERE conname = 'ota_package_external_id_unq_key') THEN - ALTER TABLE ota_package ADD CONSTRAINT ota_package_external_id_unq_key UNIQUE (tenant_id, external_id); - END IF; - END; -$$; +-- UPDATE TENANT PROFILE CONFIGURATION END --- UPDATE OTA PACKAGE EXTERNAL ID END +-- CALCULATED FIELD UNIQUE CONSTRAINT UPDATE START --- DROP INDEXES THAT DUPLICATE UNIQUE CONSTRAINT START +ALTER TABLE calculated_field DROP CONSTRAINT IF EXISTS calculated_field_unq_key; +ALTER TABLE calculated_field ADD CONSTRAINT calculated_field_unq_key UNIQUE (entity_id, type, name); -DROP INDEX IF EXISTS idx_device_external_id; -DROP INDEX IF EXISTS idx_device_profile_external_id; -DROP INDEX IF EXISTS idx_asset_external_id; -DROP INDEX IF EXISTS idx_entity_view_external_id; -DROP INDEX IF EXISTS idx_rule_chain_external_id; -DROP INDEX IF EXISTS idx_dashboard_external_id; -DROP INDEX IF EXISTS idx_customer_external_id; -DROP INDEX IF EXISTS idx_widgets_bundle_external_id; +-- CALCULATED FIELD UNIQUE CONSTRAINT UPDATE END --- DROP INDEXES THAT DUPLICATE UNIQUE CONSTRAINT END +-- REMOVAL OF CALCULATED FIELD LINKS PERSISTENCE START -ALTER TABLE mobile_app ADD COLUMN IF NOT EXISTS title varchar(255); \ No newline at end of file +DROP TABLE IF EXISTS calculated_field_link; +ANALYZE calculated_field; + +-- REMOVAL OF CALCULATED FIELD LINKS PERSISTENCE END diff --git a/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java b/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java index ea46ce86eb..16081a6d31 100644 --- a/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java +++ b/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java @@ -98,6 +98,7 @@ import org.thingsboard.server.dao.queue.QueueService; import org.thingsboard.server.dao.queue.QueueStatsService; import org.thingsboard.server.dao.relation.RelationService; import org.thingsboard.server.dao.resource.ResourceService; +import org.thingsboard.server.dao.resource.TbResourceDataCache; import org.thingsboard.server.dao.rule.RuleChainService; import org.thingsboard.server.dao.rule.RuleNodeStateService; import org.thingsboard.server.dao.tenant.TbTenantProfileCache; @@ -116,6 +117,7 @@ import org.thingsboard.server.service.apiusage.TbApiUsageStateService; import org.thingsboard.server.service.cf.CalculatedFieldProcessingService; import org.thingsboard.server.service.cf.CalculatedFieldQueueService; import org.thingsboard.server.service.cf.CalculatedFieldStateService; +import org.thingsboard.server.service.cf.OwnerService; import org.thingsboard.server.service.cf.ctx.state.ArgumentEntry; import org.thingsboard.server.service.component.ComponentDiscoveryService; import org.thingsboard.server.service.edge.rpc.EdgeRpcService; @@ -511,6 +513,10 @@ public class ActorSystemContext { @Getter private ResourceService resourceService; + @Autowired + @Getter + private TbResourceDataCache resourceDataCache; + @Lazy @Autowired(required = false) @Getter @@ -566,6 +572,10 @@ public class ActorSystemContext { @Getter private JobManager jobManager; + @Autowired + @Getter + private OwnerService ownerService; + @Value("${actors.session.max_concurrent_sessions_per_device:1}") @Getter private int maxConcurrentSessionsPerDevice; @@ -654,6 +664,14 @@ public class ActorSystemContext { @Getter private long cfCalculationResultTimeout; + @Value("${actors.calculated_fields.check_interval:60}") + @Getter + private long cfCheckInterval; + + @Value("${actors.alarms.reevaluation_interval:120}") + @Getter + private long alarmRulesReevaluationInterval; + @Autowired @Getter private MqttClientSettings mqttClientSettings; @@ -837,7 +855,7 @@ public class ActorSystemContext { if (arguments != null) { eventBuilder.arguments(JacksonUtil.toString( arguments.entrySet().stream() - .collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().toTbelCfArg())) + .collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().jsonValue())) )); } if (result != null) { @@ -846,8 +864,9 @@ public class ActorSystemContext { if (errorMessage != null) { eventBuilder.error(errorMessage); } - - ListenableFuture future = eventService.saveAsync(eventBuilder.build()); + CalculatedFieldDebugEvent event = eventBuilder.build(); + log.debug("Persisting calculated field debug event: {}", event); + ListenableFuture future = eventService.saveAsync(event); Futures.addCallback(future, CALCULATED_FIELD_DEBUG_EVENT_ERROR_CALLBACK, MoreExecutors.directExecutor()); } catch (IllegalArgumentException ex) { log.warn("Failed to persist calculated field debug message", ex); @@ -857,7 +876,7 @@ public class ActorSystemContext { private boolean checkLimits(TenantId tenantId) { if (debugModeRateLimitsConfig.isCalculatedFieldDebugPerTenantLimitsEnabled() && - !rateLimitService.checkRateLimit(LimitedApi.CALCULATED_FIELD_DEBUG_EVENTS, (Object) tenantId, debugModeRateLimitsConfig.getCalculatedFieldDebugPerTenantLimitsConfiguration())) { + !rateLimitService.checkRateLimit(LimitedApi.CALCULATED_FIELD_DEBUG_EVENTS, (Object) tenantId, debugModeRateLimitsConfig.getCalculatedFieldDebugPerTenantLimitsConfiguration())) { log.trace("[{}] Calculated field debug event limits exceeded!", tenantId); return false; } @@ -881,12 +900,13 @@ public class ActorSystemContext { return getScheduler().scheduleWithFixedDelay(() -> ctx.tell(msg), delayInMs, periodInMs, TimeUnit.MILLISECONDS); } - public void scheduleMsgWithDelay(TbActorRef ctx, TbActorMsg msg, long delayInMs) { + public ScheduledFuture scheduleMsgWithDelay(TbActorRef ctx, TbActorMsg msg, long delayInMs) { log.debug("Scheduling msg {} with delay {} ms", msg, delayInMs); if (delayInMs > 0) { - getScheduler().schedule(() -> ctx.tell(msg), delayInMs, TimeUnit.MILLISECONDS); + return getScheduler().schedule(() -> ctx.tell(msg), delayInMs, TimeUnit.MILLISECONDS); } else { ctx.tell(msg); + return null; } } diff --git a/application/src/main/java/org/thingsboard/server/actors/app/AppActor.java b/application/src/main/java/org/thingsboard/server/actors/app/AppActor.java index a79a182fa1..da16e55db8 100644 --- a/application/src/main/java/org/thingsboard/server/actors/app/AppActor.java +++ b/application/src/main/java/org/thingsboard/server/actors/app/AppActor.java @@ -32,6 +32,7 @@ import org.thingsboard.server.actors.tenant.TenantActor; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.TenantProfileId; import org.thingsboard.server.common.data.page.PageDataIterable; import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; import org.thingsboard.server.common.msg.MsgType; @@ -43,7 +44,6 @@ import org.thingsboard.server.common.msg.queue.QueueToRuleEngineMsg; import org.thingsboard.server.common.msg.queue.RuleEngineException; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.dao.tenant.TenantService; -import org.thingsboard.server.service.transport.msg.TransportToDeviceActorMsgWrapper; import java.util.HashSet; import java.util.Optional; @@ -89,16 +89,20 @@ public class AppActor extends ContextAwareActor { break; case PARTITION_CHANGE_MSG: case CF_PARTITIONS_CHANGE_MSG: + case CF_STATE_PARTITION_RESTORE_MSG: ctx.broadcastToChildren(msg, true); break; case COMPONENT_LIFE_CYCLE_MSG: onComponentLifecycleMsg((ComponentLifecycleMsg) msg); break; + case CF_ENTITY_ACTION_EVENT_MSG: + forwardToTenantActor((TenantAwareMsg) msg, true); + break; case QUEUE_TO_RULE_ENGINE_MSG: onQueueToRuleEngineMsg((QueueToRuleEngineMsg) msg); break; case TRANSPORT_TO_DEVICE_ACTOR_MSG: - onToDeviceActorMsg((TenantAwareMsg) msg, false); + forwardToTenantActor((TenantAwareMsg) msg, false); break; case DEVICE_ATTRIBUTES_UPDATE_TO_DEVICE_ACTOR_MSG: case DEVICE_CREDENTIALS_UPDATE_TO_DEVICE_ACTOR_MSG: @@ -108,23 +112,20 @@ public class AppActor extends ContextAwareActor { case DEVICE_RPC_RESPONSE_TO_DEVICE_ACTOR_MSG: case SERVER_RPC_RESPONSE_TO_DEVICE_ACTOR_MSG: case REMOVE_RPC_TO_DEVICE_ACTOR_MSG: - onToDeviceActorMsg((TenantAwareMsg) msg, true); + forwardToTenantActor((TenantAwareMsg) msg, true); break; case SESSION_TIMEOUT_MSG: ctx.broadcastToChildrenByType(msg, EntityType.TENANT); break; case CF_CACHE_INIT_MSG: - case CF_INIT_PROFILE_ENTITY_MSG: - case CF_INIT_MSG: - case CF_LINK_INIT_MSG: case CF_STATE_RESTORE_MSG: //TODO: use priority from the message body. For example, messages about CF lifecycle are important and Device lifecycle are not. // same for the Linked telemetry. - onToCalculatedFieldSystemActorMsg((ToCalculatedFieldSystemMsg) msg, true); + forwardToTenantActor((ToCalculatedFieldSystemMsg) msg, true); break; case CF_TELEMETRY_MSG: case CF_LINKED_TELEMETRY_MSG: - onToCalculatedFieldSystemActorMsg((ToCalculatedFieldSystemMsg) msg, false); + forwardToTenantActor((ToCalculatedFieldSystemMsg) msg, false); break; default: return false; @@ -165,7 +166,19 @@ public class AppActor extends ContextAwareActor { private void onComponentLifecycleMsg(ComponentLifecycleMsg msg) { TbActorRef target = null; if (TenantId.SYS_TENANT_ID.equals(msg.getTenantId())) { - if (!EntityType.TENANT_PROFILE.equals(msg.getEntityId().getEntityType())) { + if (systemContext.isTenantComponentsInitEnabled()) { + if (msg.getEntityId() instanceof TenantProfileId tenantProfileId) { + tenantService.findTenantIdsByTenantProfileId(tenantProfileId).forEach(tenantId -> { + getOrCreateTenantActor(tenantId).ifPresentOrElse(tenantActor -> { + log.debug("[{}] Sending component lifecycle msg for tenant.", tenantId); + tenantActor.tellWithHighPriority(msg); + }, () -> { + log.debug("Ignoring component lifecycle msg for tenant {} because it is not managed by this service", tenantId); + }); + }); + } + } + if (!msg.getEntityId().getEntityType().isOneOf(EntityType.TENANT_PROFILE, EntityType.TB_RESOURCE)) { log.warn("Message has system tenant id: {}", msg); } } else { @@ -190,7 +203,7 @@ public class AppActor extends ContextAwareActor { } } - private void onToCalculatedFieldSystemActorMsg(ToCalculatedFieldSystemMsg msg, boolean priority) { + private void forwardToTenantActor(TenantAwareMsg msg, boolean priority) { getOrCreateTenantActor(msg.getTenantId()).ifPresentOrElse(tenantActor -> { if (priority) { tenantActor.tellWithHighPriority(msg); @@ -202,21 +215,6 @@ public class AppActor extends ContextAwareActor { }); } - - private void onToDeviceActorMsg(TenantAwareMsg msg, boolean priority) { - getOrCreateTenantActor(msg.getTenantId()).ifPresentOrElse(tenantActor -> { - if (priority) { - tenantActor.tellWithHighPriority(msg); - } else { - tenantActor.tell(msg); - } - }, () -> { - if (msg instanceof TransportToDeviceActorMsgWrapper) { - ((TransportToDeviceActorMsgWrapper) msg).getCallback().onSuccess(); - } - }); - } - private Optional getOrCreateTenantActor(TenantId tenantId) { if (deletedTenants.contains(tenantId)) { return Optional.empty(); @@ -248,6 +246,7 @@ public class AppActor extends ContextAwareActor { public TbActor createActor() { return new AppActor(context); } + } } diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/cf/CalculatedFieldInitMsg.java b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldAlarmActionMsg.java similarity index 64% rename from common/message/src/main/java/org/thingsboard/server/common/msg/cf/CalculatedFieldInitMsg.java rename to application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldAlarmActionMsg.java index e453d2963c..3202296345 100644 --- a/common/message/src/main/java/org/thingsboard/server/common/msg/cf/CalculatedFieldInitMsg.java +++ b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldAlarmActionMsg.java @@ -13,22 +13,29 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.common.msg.cf; +package org.thingsboard.server.actors.calculatedField; +import lombok.Builder; import lombok.Data; -import org.thingsboard.server.common.data.cf.CalculatedField; +import org.thingsboard.server.common.data.alarm.Alarm; +import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.msg.MsgType; import org.thingsboard.server.common.msg.ToCalculatedFieldSystemMsg; +import org.thingsboard.server.common.msg.queue.TbCallback; @Data -public class CalculatedFieldInitMsg implements ToCalculatedFieldSystemMsg { +@Builder +public class CalculatedFieldAlarmActionMsg implements ToCalculatedFieldSystemMsg { private final TenantId tenantId; - private final CalculatedField cf; + private final Alarm alarm; + private final ActionType action; + private final TbCallback callback; @Override public MsgType getMsgType() { - return MsgType.CF_INIT_MSG; + return MsgType.CF_ALARM_ACTION_MSG; } + } diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/cf/CalculatedFieldInitProfileEntityMsg.java b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldArgumentResetMsg.java similarity index 69% rename from common/message/src/main/java/org/thingsboard/server/common/msg/cf/CalculatedFieldInitProfileEntityMsg.java rename to application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldArgumentResetMsg.java index 66cd2ac441..8b5927827e 100644 --- a/common/message/src/main/java/org/thingsboard/server/common/msg/cf/CalculatedFieldInitProfileEntityMsg.java +++ b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldArgumentResetMsg.java @@ -13,24 +13,25 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.common.msg.cf; +package org.thingsboard.server.actors.calculatedField; import lombok.Data; -import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.msg.MsgType; import org.thingsboard.server.common.msg.ToCalculatedFieldSystemMsg; +import org.thingsboard.server.common.msg.queue.TbCallback; +import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx; @Data -public class CalculatedFieldInitProfileEntityMsg implements ToCalculatedFieldSystemMsg { +public class CalculatedFieldArgumentResetMsg implements ToCalculatedFieldSystemMsg { private final TenantId tenantId; - private final EntityId profileEntityId; - private final EntityId entityId; + private final CalculatedFieldCtx ctx; + private final TbCallback callback; @Override public MsgType getMsgType() { - return MsgType.CF_INIT_PROFILE_ENTITY_MSG; + return MsgType.CF_ARGUMENT_RESET_MSG; } } diff --git a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityActionEventMsg.java b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityActionEventMsg.java new file mode 100644 index 0000000000..6fc191e3db --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityActionEventMsg.java @@ -0,0 +1,57 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.actors.calculatedField; + +import com.fasterxml.jackson.databind.JsonNode; +import lombok.Builder; +import lombok.Data; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.audit.ActionType; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.msg.MsgType; +import org.thingsboard.server.common.msg.ToCalculatedFieldSystemMsg; +import org.thingsboard.server.common.msg.queue.TbCallback; +import org.thingsboard.server.common.util.ProtoUtils; +import org.thingsboard.server.gen.transport.TransportProtos.EntityActionEventProto; + +@Data +@Builder +public class CalculatedFieldEntityActionEventMsg implements ToCalculatedFieldSystemMsg { + + private final TenantId tenantId; + private final EntityId entityId; + private final JsonNode entity; + private final ActionType action; + private final TbCallback callback; + + public static CalculatedFieldEntityActionEventMsg fromProto(EntityActionEventProto proto, + TbCallback callback) { + return CalculatedFieldEntityActionEventMsg.builder() + .tenantId((TenantId) ProtoUtils.fromProto(proto.getTenantId())) + .entityId(ProtoUtils.fromProto(proto.getEntityId())) + .entity(JacksonUtil.toJsonNode(proto.getEntity())) + .action(ActionType.valueOf(proto.getAction())) + .callback(callback) + .build(); + } + + @Override + public MsgType getMsgType() { + return MsgType.CF_ENTITY_ACTION_EVENT_MSG; + } + +} diff --git a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityActor.java b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityActor.java index 2959bfc8eb..160cd995d5 100644 --- a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityActor.java +++ b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityActor.java @@ -21,6 +21,7 @@ import org.thingsboard.server.actors.TbActorCtx; import org.thingsboard.server.actors.TbActorException; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.msg.CalculatedFieldStatePartitionRestoreMsg; import org.thingsboard.server.common.msg.TbActorStopReason; import org.thingsboard.server.common.msg.ToCalculatedFieldSystemMsg; import org.thingsboard.server.common.msg.cf.CalculatedFieldPartitionChangeMsg; @@ -51,7 +52,7 @@ public class CalculatedFieldEntityActor extends AbstractCalculatedFieldActor { @Override public void destroy(TbActorStopReason stopReason, Throwable cause) throws TbActorException { log.debug("[{}] Stopping CF entity actor.", processor.tenantId); - processor.stop(); + processor.stop(false); } @Override @@ -63,18 +64,33 @@ public class CalculatedFieldEntityActor extends AbstractCalculatedFieldActor { case CF_STATE_RESTORE_MSG: processor.process((CalculatedFieldStateRestoreMsg) msg); break; + case CF_STATE_PARTITION_RESTORE_MSG: + processor.process((CalculatedFieldStatePartitionRestoreMsg) msg); + break; case CF_ENTITY_INIT_CF_MSG: processor.process((EntityInitCalculatedFieldMsg) msg); break; case CF_ENTITY_DELETE_MSG: processor.process((CalculatedFieldEntityDeleteMsg) msg); break; + case CF_RELATION_ACTION_MSG: + processor.process((CalculatedFieldRelationActionMsg) msg); + break; case CF_ENTITY_TELEMETRY_MSG: processor.process((EntityCalculatedFieldTelemetryMsg) msg); break; case CF_LINKED_TELEMETRY_MSG: processor.process((EntityCalculatedFieldLinkedTelemetryMsg) msg); break; + case CF_REEVALUATE_MSG: + processor.process((CalculatedFieldReevaluateMsg) msg); + break; + case CF_ALARM_ACTION_MSG: + processor.process((CalculatedFieldAlarmActionMsg) msg); + break; + case CF_ARGUMENT_RESET_MSG: + processor.process((CalculatedFieldArgumentResetMsg) msg); + break; default: return false; } @@ -85,4 +101,5 @@ public class CalculatedFieldEntityActor extends AbstractCalculatedFieldActor { void logProcessingException(Exception e) { log.warn("[{}][{}] Processing failure", tenantId, processor.entityId, e); } + } diff --git a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java index 35539834c3..bf9a3529e5 100644 --- a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java +++ b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java @@ -21,10 +21,13 @@ import lombok.extern.slf4j.Slf4j; import org.thingsboard.common.util.DebugModeUtil; import org.thingsboard.server.actors.ActorSystemContext; import org.thingsboard.server.actors.TbActorCtx; +import org.thingsboard.server.actors.calculatedField.EntityInitCalculatedFieldMsg.StateAction; import org.thingsboard.server.actors.shared.AbstractContextAwareMsgProcessor; import org.thingsboard.server.common.data.AttributeScope; import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.StringUtils; +import org.thingsboard.server.common.data.alarm.Alarm; +import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.cf.configuration.Argument; import org.thingsboard.server.common.data.cf.configuration.ArgumentType; import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey; @@ -33,6 +36,7 @@ import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.kv.StringDataEntry; import org.thingsboard.server.common.data.msg.TbMsgType; +import org.thingsboard.server.common.msg.CalculatedFieldStatePartitionRestoreMsg; import org.thingsboard.server.common.msg.cf.CalculatedFieldPartitionChangeMsg; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.TbCallback; @@ -48,6 +52,10 @@ import org.thingsboard.server.service.cf.ctx.state.ArgumentEntry; import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx; import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldState; import org.thingsboard.server.service.cf.ctx.state.SingleValueArgumentEntry; +import org.thingsboard.server.service.cf.ctx.state.aggregation.RelatedEntitiesAggregationCalculatedFieldState; +import org.thingsboard.server.service.cf.ctx.state.alarm.AlarmCalculatedFieldState; +import org.thingsboard.server.service.cf.ctx.state.geofencing.GeofencingArgumentEntry; +import org.thingsboard.server.service.cf.ctx.state.geofencing.GeofencingCalculatedFieldState; import java.util.ArrayList; import java.util.Collection; @@ -62,6 +70,7 @@ import java.util.UUID; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; +import static org.thingsboard.server.utils.CalculatedFieldArgumentUtils.createStateByType; /** * @author Andrew Shvayka @@ -76,7 +85,7 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM final CalculatedFieldProcessingService cfService; final CalculatedFieldStateService cfStateService; - TbActorCtx ctx; + TbActorCtx actorCtx; Map states = new HashMap<>(); CalculatedFieldEntityMessageProcessor(ActorSystemContext systemContext, TenantId tenantId, EntityId entityId) { @@ -88,47 +97,74 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM } void init(TbActorCtx ctx) { - this.ctx = ctx; + this.actorCtx = ctx; } - public void stop() { - log.info("[{}][{}] Stopping entity actor.", tenantId, entityId); + public void stop(boolean partitionChanged) { + log.info(partitionChanged ? + "[{}][{}] Stopping entity actor due to change partition event." : + "[{}][{}] Stopping entity actor.", + tenantId, entityId); + states.values().forEach(this::closeState); states.clear(); - ctx.stop(ctx.getSelf()); + actorCtx.stop(actorCtx.getSelf()); } public void process(CalculatedFieldPartitionChangeMsg msg) { if (!systemContext.getPartitionService().resolve(ServiceType.TB_RULE_ENGINE, DataConstants.CF_QUEUE_NAME, tenantId, entityId).isMyPartition()) { - log.info("[{}] Stopping entity actor due to change partition event.", entityId); - ctx.stop(ctx.getSelf()); + stop(true); } } public void process(CalculatedFieldStateRestoreMsg msg) { CalculatedFieldId cfId = msg.getId().cfId(); log.debug("[{}] [{}] Processing CF state restore msg.", msg.getId().entityId(), cfId); - if (msg.getState() != null) { - states.put(cfId, msg.getState()); + CalculatedFieldState state = msg.getState(); + if (state != null) { + state.setCtx(msg.getCtx(), actorCtx); + state.setPartition(msg.getPartition()); + states.put(cfId, state); } else { - states.remove(cfId); + removeState(cfId); + } + } + + public void process(CalculatedFieldStatePartitionRestoreMsg msg) { + log.debug("Processing CF state partition restore msg: {}", msg); + for (CalculatedFieldState state : states.values()) { + if (msg.getPartition().equals(state.getPartition())) { + state.init(true); + } } } public void process(EntityInitCalculatedFieldMsg msg) throws CalculatedFieldException { - log.debug("[{}] Processing entity init CF msg.", msg.getCtx().getCfId()); + log.debug("[{}] Processing entity init CF msg: {}", msg.getCtx().getCfId(), msg); var ctx = msg.getCtx(); - if (msg.isForceReinit()) { - log.debug("Force reinitialization of CF: [{}].", ctx.getCfId()); - states.remove(ctx.getCfId()); + CalculatedFieldState state; + if (msg.getStateAction() == StateAction.RECREATE) { + removeState(ctx.getCfId()); + state = null; + } else { + state = states.get(ctx.getCfId()); } try { - var state = getOrInitState(ctx); + if (state == null) { + state = createState(ctx); + } else if (msg.getStateAction() == StateAction.REINIT) { + log.debug("Force reinitialization of CF: [{}].", ctx.getCfId()); + state.reset(); + initState(state, ctx); + } else { + state.setCtx(ctx, actorCtx); + } if (state.isSizeOk()) { - processStateIfReady(ctx, Collections.singletonList(ctx.getCfId()), state, null, null, msg.getCallback()); + processStateIfReady(state, Collections.emptyMap(), ctx, Collections.singletonList(ctx.getCfId()), null, null, msg.getCallback()); } else { throw new RuntimeException(ctx.getSizeExceedsLimitMessage()); } } catch (Exception e) { + log.debug("[{}][{}] Failed to initialize CF state", entityId, ctx.getCfId(), e); if (e instanceof CalculatedFieldException cfe) { throw cfe; } @@ -136,31 +172,110 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM } } - public void process(CalculatedFieldEntityDeleteMsg msg) { + public void process(CalculatedFieldArgumentResetMsg msg) throws CalculatedFieldException { + log.debug("[{}] Processing CF argument reset msg.", entityId); + var ctx = msg.getCtx(); + try { + Map dynamicSourceArgs = ctx.getArguments().entrySet().stream() + .filter(entry -> entry.getValue().hasOwnerSource()) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + + Map fetchedArgs = cfService.fetchArgsFromDb(tenantId, entityId, dynamicSourceArgs); + fetchedArgs.values().forEach(arg -> arg.setForceResetPrevious(true)); + + processArgumentValuesUpdate(ctx, Collections.singletonList(ctx.getCfId()), msg.getCallback(), fetchedArgs, null, null); + } catch (Exception e) { + throw CalculatedFieldException.builder().ctx(ctx).eventEntity(entityId).cause(e).build(); + } + } + + public void process(CalculatedFieldEntityDeleteMsg msg) throws CalculatedFieldException { log.debug("[{}] Processing CF entity delete msg.", msg.getEntityId()); if (this.entityId.equals(msg.getEntityId())) { if (states.isEmpty()) { msg.getCallback().onSuccess(); } else { MultipleTbCallback multipleTbCallback = new MultipleTbCallback(states.size(), msg.getCallback()); - states.forEach((cfId, state) -> cfStateService.removeState(new CalculatedFieldEntityCtxId(tenantId, cfId, entityId), multipleTbCallback)); - ctx.stop(ctx.getSelf()); + states.forEach((cfId, state) -> cfStateService.deleteState(new CalculatedFieldEntityCtxId(tenantId, cfId, entityId), multipleTbCallback)); + actorCtx.stop(actorCtx.getSelf()); } } else { var cfId = new CalculatedFieldId(msg.getEntityId().getId()); - var state = states.remove(cfId); + var state = removeState(cfId); if (state != null) { - cfStateService.removeState(new CalculatedFieldEntityCtxId(tenantId, cfId, entityId), msg.getCallback()); + cfStateService.deleteState(new CalculatedFieldEntityCtxId(tenantId, cfId, entityId), msg.getCallback()); } else { msg.getCallback().onSuccess(); } } } + public void process(CalculatedFieldRelationActionMsg msg) throws CalculatedFieldException { + log.debug("[{}] Processing CF {} related entity msg.", msg.getRelatedEntityId(), msg.getAction()); + switch (msg.getAction()) { + case UPDATED -> handleRelationUpdate(msg); + case DELETED -> handleRelationDelete(msg); + default -> msg.getCallback().onSuccess(); + } + } + + private void handleRelationUpdate(CalculatedFieldRelationActionMsg msg) throws CalculatedFieldException { + CalculatedFieldCtx ctx = msg.getCalculatedField(); + var callback = new MultipleTbCallback(CALLBACKS_PER_CF, msg.getCallback()); + var state = states.get(ctx.getCfId()); + try { + Map updatedArgs = new HashMap<>(); + if (state == null) { + state = createState(ctx); + } else { + if (state instanceof RelatedEntitiesAggregationCalculatedFieldState relatedEntitiesAggState) { + Map fetchedArgs = cfService.fetchArgsFromDb(tenantId, msg.getRelatedEntityId(), ctx.getArguments()); + updatedArgs = relatedEntitiesAggState.updateEntityData(setEntityIdToSingleEntityArguments(msg.getRelatedEntityId(), fetchedArgs)); + } + + state.checkStateSize(new CalculatedFieldEntityCtxId(tenantId, ctx.getCfId(), entityId), ctx.getMaxStateSize()); + } + if (state.isSizeOk()) { + processStateIfReady(state, updatedArgs, ctx, Collections.singletonList(ctx.getCfId()), null, null, callback); + } else { + throw CalculatedFieldException.builder().ctx(ctx).eventEntity(entityId).errorMessage(ctx.getSizeExceedsLimitMessage()).build(); + } + } catch (Exception e) { + log.debug("[{}][{}] Failed to initialize CF state", entityId, ctx.getCfId(), e); + if (e instanceof CalculatedFieldException cfe) { + throw cfe; + } + throw CalculatedFieldException.builder().ctx(ctx).eventEntity(entityId).cause(e).build(); + } + } + + private void handleRelationDelete(CalculatedFieldRelationActionMsg msg) throws CalculatedFieldException { + CalculatedFieldCtx ctx = msg.getCalculatedField(); + CalculatedFieldId cfId = ctx.getCfId(); + CalculatedFieldState state = states.get(cfId); + if (state == null) { + msg.getCallback().onSuccess(); + return; + } + if (state instanceof RelatedEntitiesAggregationCalculatedFieldState aggState) { + aggState.cleanupEntityData(msg.getRelatedEntityId()); + + state.checkStateSize(new CalculatedFieldEntityCtxId(tenantId, ctx.getCfId(), entityId), ctx.getMaxStateSize()); + + if (state.isSizeOk()) { + processStateIfReady(state, Collections.emptyMap(), ctx, Collections.singletonList(ctx.getCfId()), null, null, msg.getCallback()); + } else { + throw new RuntimeException(ctx.getSizeExceedsLimitMessage()); + } + } else { + msg.getCallback().onSuccess(); + } + } + public void process(EntityCalculatedFieldTelemetryMsg msg) throws CalculatedFieldException { - log.debug("[{}] Processing CF telemetry msg.", msg.getEntityId()); + log.trace("[{}] Processing CF telemetry msg: {}", msg.getEntityId(), msg); var proto = msg.getProto(); - var numberOfCallbacks = CALLBACKS_PER_CF * (msg.getEntityIdFields().size() + msg.getProfileIdFields().size()); + var numberOfCallbacks = msg.getEntityIdFields().size() + msg.getProfileIdFields().size(); MultipleTbCallback callback = new MultipleTbCallback(numberOfCallbacks, msg.getCallback()); List cfIdList = getCalculatedFieldIds(proto); Set cfIdSet = new HashSet<>(cfIdList); @@ -173,36 +288,37 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM } public void process(EntityCalculatedFieldLinkedTelemetryMsg msg) throws CalculatedFieldException { - log.debug("[{}] Processing CF link telemetry msg.", msg.getEntityId()); + log.trace("[{}] Processing CF link telemetry msg: {}", msg.getEntityId(), msg); var proto = msg.getProto(); var ctx = msg.getCtx(); - var callback = new MultipleTbCallback(CALLBACKS_PER_CF, msg.getCallback()); + var callback = msg.getCallback(); try { List cfIds = getCalculatedFieldIds(proto); if (cfIds.contains(ctx.getCfId())) { - callback.onSuccess(CALLBACKS_PER_CF); + callback.onSuccess(); } else { if (proto.getTsDataCount() > 0) { processArgumentValuesUpdate(ctx, cfIds, callback, mapToArguments(ctx, msg.getEntityId(), proto.getTsDataList()), toTbMsgId(proto), toTbMsgType(proto)); } else if (proto.getAttrDataCount() > 0) { processArgumentValuesUpdate(ctx, cfIds, callback, mapToArguments(ctx, msg.getEntityId(), proto.getScope(), proto.getAttrDataList()), toTbMsgId(proto), toTbMsgType(proto)); } else if (proto.getRemovedTsKeysCount() > 0) { - processArgumentValuesUpdate(ctx, cfIds, callback, mapToArgumentsWithFetchedValue(ctx, proto.getRemovedTsKeysList()), toTbMsgId(proto), toTbMsgType(proto)); + processArgumentValuesUpdate(ctx, cfIds, callback, mapToArgumentsWithFetchedValue(ctx, msg.getEntityId(), proto.getRemovedTsKeysList()), toTbMsgId(proto), toTbMsgType(proto)); } else if (proto.getRemovedAttrKeysCount() > 0) { processArgumentValuesUpdate(ctx, cfIds, callback, mapToArgumentsWithDefaultValue(ctx, msg.getEntityId(), proto.getScope(), proto.getRemovedAttrKeysList()), toTbMsgId(proto), toTbMsgType(proto)); } else { - callback.onSuccess(CALLBACKS_PER_CF); + callback.onSuccess(); } } } catch (Exception e) { + log.debug("[{}][{}] Failed to process linked CF telemetry msg: {}", entityId, ctx.getCfId(), msg, e); throw CalculatedFieldException.builder().ctx(ctx).eventEntity(entityId).cause(e).build(); } } - private void process(CalculatedFieldCtx ctx, CalculatedFieldTelemetryMsgProto proto, Collection cfIds, List cfIdList, MultipleTbCallback callback) throws CalculatedFieldException { + private void process(CalculatedFieldCtx ctx, CalculatedFieldTelemetryMsgProto proto, Collection cfIds, List cfIdList, TbCallback callback) throws CalculatedFieldException { try { if (cfIds.contains(ctx.getCfId())) { - callback.onSuccess(CALLBACKS_PER_CF); + callback.onSuccess(); } else { if (proto.getTsDataCount() > 0) { processTelemetry(ctx, proto, cfIdList, callback); @@ -213,10 +329,11 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM } else if (proto.getRemovedAttrKeysCount() > 0) { processRemovedAttributes(ctx, proto, cfIdList, callback); } else { - callback.onSuccess(CALLBACKS_PER_CF); + callback.onSuccess(); } } } catch (Exception e) { + log.debug("[{}][{}] Failed to process CF telemetry msg: {}", entityId, ctx.getCfId(), proto, e); if (e instanceof CalculatedFieldException cfe) { throw cfe; } @@ -224,71 +341,147 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM } } - private void processTelemetry(CalculatedFieldCtx ctx, CalculatedFieldTelemetryMsgProto proto, List cfIdList, MultipleTbCallback callback) throws CalculatedFieldException { + public void process(CalculatedFieldReevaluateMsg msg) throws CalculatedFieldException { + CalculatedFieldId cfId = msg.getCtx().getCfId(); + CalculatedFieldState state = states.get(cfId); + if (state == null) { + log.debug("[{}][{}] Failed to find CF state for entity to handle {}", entityId, cfId, msg); + } else { + if (state.isSizeOk()) { + log.debug("[{}][{}] Reevaluating CF state", entityId, cfId); + processStateIfReady(state, null, msg.getCtx(), Collections.singletonList(cfId), null, null, msg.getCallback()); + } else { + throw new RuntimeException(msg.getCtx().getSizeExceedsLimitMessage()); + } + } + } + + public void process(CalculatedFieldAlarmActionMsg msg) { + log.debug("[{}] Processing alarm action event msg: {}", entityId, msg); + for (CalculatedFieldState state : states.values()) { + if (state instanceof AlarmCalculatedFieldState alarmCfState) { + Alarm stateAlarm = alarmCfState.getCurrentAlarm(); + if (stateAlarm != null && stateAlarm.getId().equals(msg.getAlarm().getId())) { + alarmCfState.processAlarmAction(msg.getAlarm(), msg.getAction()); + } + } + } + msg.getCallback().onSuccess(); + } + + private void processTelemetry(CalculatedFieldCtx ctx, CalculatedFieldTelemetryMsgProto proto, List cfIdList, TbCallback callback) throws CalculatedFieldException { processArgumentValuesUpdate(ctx, cfIdList, callback, mapToArguments(ctx, proto.getTsDataList()), toTbMsgId(proto), toTbMsgType(proto)); } - private void processAttributes(CalculatedFieldCtx ctx, CalculatedFieldTelemetryMsgProto proto, List cfIdList, MultipleTbCallback callback) throws CalculatedFieldException { + private void processAttributes(CalculatedFieldCtx ctx, CalculatedFieldTelemetryMsgProto proto, List cfIdList, TbCallback callback) throws CalculatedFieldException { processArgumentValuesUpdate(ctx, cfIdList, callback, mapToArguments(ctx, proto.getScope(), proto.getAttrDataList()), toTbMsgId(proto), toTbMsgType(proto)); } - private void processRemovedTelemetry(CalculatedFieldCtx ctx, CalculatedFieldTelemetryMsgProto proto, List cfIdList, MultipleTbCallback callback) throws CalculatedFieldException { - processArgumentValuesUpdate(ctx, cfIdList, callback, mapToArgumentsWithFetchedValue(ctx, proto.getRemovedTsKeysList()), toTbMsgId(proto), toTbMsgType(proto)); + private void processRemovedTelemetry(CalculatedFieldCtx ctx, CalculatedFieldTelemetryMsgProto proto, List cfIdList, TbCallback callback) throws CalculatedFieldException { + processArgumentValuesUpdate(ctx, cfIdList, callback, mapToArgumentsWithFetchedValue(ctx, entityId, proto.getRemovedTsKeysList()), toTbMsgId(proto), toTbMsgType(proto)); } - private void processRemovedAttributes(CalculatedFieldCtx ctx, CalculatedFieldTelemetryMsgProto proto, List cfIdList, MultipleTbCallback callback) throws CalculatedFieldException { + private void processRemovedAttributes(CalculatedFieldCtx ctx, CalculatedFieldTelemetryMsgProto proto, List cfIdList, TbCallback callback) throws CalculatedFieldException { processArgumentValuesUpdate(ctx, cfIdList, callback, mapToArgumentsWithDefaultValue(ctx, proto.getScope(), proto.getRemovedAttrKeysList()), toTbMsgId(proto), toTbMsgType(proto)); } - private void processArgumentValuesUpdate(CalculatedFieldCtx ctx, List cfIdList, MultipleTbCallback callback, + private void processArgumentValuesUpdate(CalculatedFieldCtx ctx, List cfIdList, TbCallback callback, Map newArgValues, UUID tbMsgId, TbMsgType tbMsgType) throws CalculatedFieldException { if (newArgValues.isEmpty()) { log.debug("[{}] No new argument values to process for CF.", ctx.getCfId()); - callback.onSuccess(CALLBACKS_PER_CF); + callback.onSuccess(); } CalculatedFieldState state = states.get(ctx.getCfId()); boolean justRestored = false; if (state == null) { - state = getOrInitState(ctx); + state = createState(ctx); justRestored = true; + } else if (ctx.shouldFetchRelationQueryDynamicArgumentsFromDb(state)) { + log.debug("[{}][{}] Going to update dynamic arguments for CF.", entityId, ctx.getCfId()); + try { + Map dynamicArgsFromDb = cfService.fetchDynamicArgsFromDb(ctx, entityId); + dynamicArgsFromDb.forEach(newArgValues::putIfAbsent); + if (ctx.getCfType() == CalculatedFieldType.GEOFENCING) { + var geofencingState = (GeofencingCalculatedFieldState) state; + geofencingState.updateLastDynamicArgumentsRefreshTs(); + } + } catch (Exception e) { + throw CalculatedFieldException.builder().ctx(ctx).eventEntity(entityId).cause(e).build(); + } + } else if (ctx.shouldFetchEntityRelations(state)) { + log.debug("[{}][{}] Going to update related entities for CF.", entityId, ctx.getCfId()); + try { + if (state instanceof RelatedEntitiesAggregationCalculatedFieldState relatedEntitiesState) { + List relatedEntities = cfService.fetchRelatedEntities(ctx, entityId); + List missingEntities = relatedEntitiesState.checkRelatedEntities(relatedEntities); + if (!missingEntities.isEmpty()) { + missingEntities.forEach(missingEntityId -> { + Map fetchedArgs = cfService.fetchArgsFromDb(tenantId, missingEntityId, ctx.getArguments()); + relatedEntitiesState.updateEntityData(setEntityIdToSingleEntityArguments(missingEntityId, fetchedArgs)); + }); + justRestored = true; + } + } + } catch (Exception e) { + throw CalculatedFieldException.builder().ctx(ctx).eventEntity(entityId).cause(e).build(); + } } if (state.isSizeOk()) { - if (state.updateState(ctx, newArgValues) || justRestored) { + Map updatedArgs = state.update(newArgValues, ctx); + if (!updatedArgs.isEmpty() || justRestored) { cfIdList = new ArrayList<>(cfIdList); cfIdList.add(ctx.getCfId()); - processStateIfReady(ctx, cfIdList, state, tbMsgId, tbMsgType, callback); + processStateIfReady(state, updatedArgs, ctx, cfIdList, tbMsgId, tbMsgType, callback); } else { - callback.onSuccess(CALLBACKS_PER_CF); + callback.onSuccess(); } } else { throw CalculatedFieldException.builder().ctx(ctx).eventEntity(entityId).errorMessage(ctx.getSizeExceedsLimitMessage()).build(); } } - @SneakyThrows - private CalculatedFieldState getOrInitState(CalculatedFieldCtx ctx) { - CalculatedFieldState state = states.get(ctx.getCfId()); - if (state != null) { - return state; - } else { - ListenableFuture stateFuture = systemContext.getCalculatedFieldProcessingService().fetchStateFromDb(ctx, entityId); - // Ugly but necessary. We do not expect to often fetch data from DB. Only once per pair lifetime. - // This call happens while processing the CF pack from the queue consumer. So the timeout should be relatively low. - // Alternatively, we can fetch the state outside the actor system and push separate command to create this actor, - // but this will significantly complicate the code. - state = stateFuture.get(1, TimeUnit.MINUTES); - state.checkStateSize(new CalculatedFieldEntityCtxId(tenantId, ctx.getCfId(), entityId), ctx.getMaxStateSize()); - states.put(ctx.getCfId(), state); - } + private CalculatedFieldState createState(CalculatedFieldCtx ctx) { + CalculatedFieldState state = createStateByType(ctx, entityId); + initState(state, ctx); return state; } - private void processStateIfReady(CalculatedFieldCtx ctx, List cfIdList, CalculatedFieldState state, UUID tbMsgId, TbMsgType tbMsgType, TbCallback callback) throws CalculatedFieldException { + private void initState(CalculatedFieldState state, CalculatedFieldCtx ctx) { + state.setCtx(ctx, actorCtx); + state.init(false); + + if (ctx.getCfType() == CalculatedFieldType.GEOFENCING && ctx.isRelationQueryDynamicArguments()) { + GeofencingCalculatedFieldState geofencingState = (GeofencingCalculatedFieldState) state; + geofencingState.updateLastDynamicArgumentsRefreshTs(); + } + + Map arguments = fetchArguments(ctx); + state.update(arguments, ctx); + + state.checkStateSize(new CalculatedFieldEntityCtxId(tenantId, ctx.getCfId(), entityId), ctx.getMaxStateSize()); + states.put(ctx.getCfId(), state); + } + + @SneakyThrows + private Map fetchArguments(CalculatedFieldCtx ctx) { + ListenableFuture> argumentsFuture = cfService.fetchArguments(ctx, entityId); + // Ugly but necessary. We do not expect to often fetch data from DB. Only once per pair lifetime. + // This call happens while processing the CF pack from the queue consumer. So the timeout should be relatively low. + // Alternatively, we can fetch the state outside the actor system and push separate command to create this actor, + // but this will significantly complicate the code. + return argumentsFuture.get(1, TimeUnit.MINUTES); + } + + private void processStateIfReady(CalculatedFieldState state, Map updatedArgs, CalculatedFieldCtx ctx, + List cfIdList, UUID tbMsgId, TbMsgType tbMsgType, TbCallback callback) throws CalculatedFieldException { + callback = new MultipleTbCallback(CALLBACKS_PER_CF, callback); + log.trace("[{}][{}] Processing state if ready. Current args: {}, updated args: {}", entityId, ctx.getCfId(), state.getArguments(), updatedArgs); CalculatedFieldEntityCtxId ctxId = new CalculatedFieldEntityCtxId(tenantId, ctx.getCfId(), entityId); boolean stateSizeChecked = false; try { if (ctx.isInitialized() && state.isReady()) { - CalculatedFieldResult calculationResult = state.performCalculation(ctx).get(systemContext.getCfCalculationResultTimeout(), TimeUnit.SECONDS); + log.trace("[{}][{}] Performing calculation. Updated args: {}", entityId, ctx.getCfId(), updatedArgs); + CalculatedFieldResult calculationResult = state.performCalculation(updatedArgs, ctx).get(systemContext.getCfCalculationResultTimeout(), TimeUnit.SECONDS); state.checkStateSize(ctxId, ctx.getMaxStateSize()); stateSizeChecked = true; if (state.isSizeOk()) { @@ -298,13 +491,18 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM callback.onSuccess(); } if (DebugModeUtil.isDebugAllAvailable(ctx.getCalculatedField())) { - systemContext.persistCalculatedFieldDebugEvent(tenantId, ctx.getCfId(), entityId, state.getArguments(), tbMsgId, tbMsgType, calculationResult.getResult().toString(), null); + systemContext.persistCalculatedFieldDebugEvent(tenantId, ctx.getCfId(), entityId, state.getArguments(), tbMsgId, tbMsgType, calculationResult.stringValue(), null); } } } else { + if (DebugModeUtil.isDebugFailuresAvailable(ctx.getCalculatedField())) { + String errorMsg = ctx.isInitialized() ? state.getReadinessStatus().errorMsg() : "Calculated field state is not initialized!"; + systemContext.persistCalculatedFieldDebugEvent(tenantId, ctx.getCfId(), entityId, state.getArguments(), tbMsgId, tbMsgType, null, errorMsg); + } callback.onSuccess(); } } catch (Exception e) { + log.debug("[{}][{}] Failed to process CF state", entityId, ctx.getCfId(), e); throw CalculatedFieldException.builder().ctx(ctx).eventEntity(entityId).msgId(tbMsgId).msgType(tbMsgType).arguments(state.getArguments()).cause(e).build(); } finally { if (!stateSizeChecked) { @@ -313,14 +511,30 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM if (state.isSizeOk()) { cfStateService.persistState(ctxId, state, callback); } else { - removeStateAndRaiseSizeException(ctxId, CalculatedFieldException.builder().ctx(ctx).eventEntity(entityId).errorMessage(ctx.getSizeExceedsLimitMessage()).build(), callback); + deleteStateAndRaiseSizeException(ctxId, CalculatedFieldException.builder().ctx(ctx).eventEntity(entityId).errorMessage(ctx.getSizeExceedsLimitMessage()).build(), callback); + } + } + } + + private CalculatedFieldState removeState(CalculatedFieldId cfId) { + CalculatedFieldState state = states.remove(cfId); + closeState(state); + return state; + } + + private void closeState(CalculatedFieldState state) { + if (state != null) { + try { + state.close(); + } catch (Exception e) { + log.warn("[{}][{}] Failed to close CF state", tenantId, state.getEntityId(), e); } } } - private void removeStateAndRaiseSizeException(CalculatedFieldEntityCtxId ctxId, CalculatedFieldException ex, TbCallback callback) throws CalculatedFieldException { + private void deleteStateAndRaiseSizeException(CalculatedFieldEntityCtxId ctxId, CalculatedFieldException ex, TbCallback callback) throws CalculatedFieldException { // We remove the state, but remember that it is over-sized in a local map. - cfStateService.removeState(ctxId, new TbCallback() { + cfStateService.deleteState(ctxId, new TbCallback() { @Override public void onSuccess() { callback.onFailure(ex); @@ -335,101 +549,164 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM } private Map mapToArguments(CalculatedFieldCtx ctx, List data) { - return mapToArguments(ctx.getMainEntityArguments(), data); + return mapToArguments(entityId, ctx.getMainEntityArguments(), Collections.emptyMap(), data); } private Map mapToArguments(CalculatedFieldCtx ctx, EntityId entityId, List data) { - var argNames = ctx.getLinkedEntityArguments().get(entityId); - if (argNames.isEmpty()) { - return Collections.emptyMap(); - } - return mapToArguments(argNames, data); + return mapToArguments(entityId, ctx.getLinkedAndDynamicArgs(entityId), ctx.getRelatedEntityArguments(), data); } - private Map mapToArguments(Map argNames, List data) { - if (argNames.isEmpty()) { - return Collections.emptyMap(); - } + private Map mapToArguments(EntityId originator, Map> args, Map> relatedEntityArgs, List data) { Map arguments = new HashMap<>(); - for (TsKvProto item : data) { - ReferencedEntityKey key = new ReferencedEntityKey(item.getKv().getKey(), ArgumentType.TS_LATEST, null); - String argName = argNames.get(key); - if (argName != null) { - arguments.put(argName, new SingleValueArgumentEntry(item)); - } - key = new ReferencedEntityKey(item.getKv().getKey(), ArgumentType.TS_ROLLING, null); - argName = argNames.get(key); - if (argName != null) { - arguments.put(argName, new SingleValueArgumentEntry(item)); + if (!relatedEntityArgs.isEmpty() || !args.isEmpty()) { + for (TsKvProto item : data) { + ReferencedEntityKey key = new ReferencedEntityKey(item.getKv().getKey(), ArgumentType.TS_LATEST, null); + Set argNames = relatedEntityArgs.get(key); + if (argNames != null) { + argNames.forEach(argName -> { + arguments.put(argName, new SingleValueArgumentEntry(originator, item)); + }); + } + argNames = args.get(key); + if (argNames != null) { + argNames.forEach(argName -> { + arguments.put(argName, new SingleValueArgumentEntry(item)); + }); + } + key = new ReferencedEntityKey(item.getKv().getKey(), ArgumentType.TS_ROLLING, null); + argNames = args.get(key); + if (argNames != null) { + argNames.forEach(argName -> { + arguments.put(argName, new SingleValueArgumentEntry(item)); + }); + } } } return arguments; } private Map mapToArguments(CalculatedFieldCtx ctx, AttributeScopeProto scope, List attrDataList) { - return mapToArguments(ctx.getMainEntityArguments(), scope, attrDataList); + return mapToArguments(entityId, ctx.getMainEntityArguments(), ctx.getMainEntityGeofencingArgumentNames(), Collections.emptyMap(), scope, attrDataList); } private Map mapToArguments(CalculatedFieldCtx ctx, EntityId entityId, AttributeScopeProto scope, List attrDataList) { - var argNames = ctx.getLinkedEntityArguments().get(entityId); - if (argNames.isEmpty()) { - return Collections.emptyMap(); - } - return mapToArguments(argNames, scope, attrDataList); + var args = ctx.getLinkedAndDynamicArgs(entityId); + var relatedEntityArgs = ctx.getRelatedEntityArguments(); + List geofencingArgumentNames = ctx.getLinkedEntityAndCurrentOwnerGeofencingArgumentNames(); + return mapToArguments(entityId, args, geofencingArgumentNames, relatedEntityArgs, scope, attrDataList); } - private Map mapToArguments(Map argNames, AttributeScopeProto scope, List attrDataList) { + private Map mapToArguments(EntityId entityId, Map> args, List geofencingArgNames, Map> relatedEntityArgs, AttributeScopeProto scope, List attrDataList) { + if (args.isEmpty() && relatedEntityArgs.isEmpty()) { + return Collections.emptyMap(); + } Map arguments = new HashMap<>(); for (AttributeValueProto item : attrDataList) { ReferencedEntityKey key = new ReferencedEntityKey(item.getKey(), ArgumentType.ATTRIBUTE, AttributeScope.valueOf(scope.name())); - String argName = argNames.get(key); - if (argName != null) { - arguments.put(argName, new SingleValueArgumentEntry(item)); + Set argNames = relatedEntityArgs.get(key); + if (argNames != null) { + argNames.forEach(argName -> { + arguments.put(argName, new SingleValueArgumentEntry(entityId, item)); + }); } + argNames = args.get(key); + if (argNames == null) { + continue; + } + argNames.forEach(argName -> { + if (geofencingArgNames.contains(argName)) { + arguments.put(argName, new GeofencingArgumentEntry(entityId, item)); + } else { + arguments.put(argName, new SingleValueArgumentEntry(item)); + } + }); } return arguments; } private Map mapToArgumentsWithDefaultValue(CalculatedFieldCtx ctx, EntityId entityId, AttributeScopeProto scope, List removedAttrKeys) { - var argNames = ctx.getLinkedEntityArguments().get(entityId); - if (argNames.isEmpty()) { - return Collections.emptyMap(); - } - return mapToArgumentsWithDefaultValue(argNames, ctx.getArguments(), scope, removedAttrKeys); + var args = ctx.getLinkedAndDynamicArgs(entityId); + var relatedEntityArgs = ctx.getRelatedEntityArguments(); + List geofencingArgumentNames = ctx.getLinkedEntityAndCurrentOwnerGeofencingArgumentNames(); + return mapToArgumentsWithDefaultValue(entityId, args, ctx.getArguments(), geofencingArgumentNames, relatedEntityArgs, scope, removedAttrKeys); } private Map mapToArgumentsWithDefaultValue(CalculatedFieldCtx ctx, AttributeScopeProto scope, List removedAttrKeys) { - return mapToArgumentsWithDefaultValue(ctx.getMainEntityArguments(), ctx.getArguments(), scope, removedAttrKeys); + return mapToArgumentsWithDefaultValue(null, ctx.getMainEntityArguments(), ctx.getArguments(), ctx.getMainEntityGeofencingArgumentNames(), Collections.emptyMap(), scope, removedAttrKeys); } - private Map mapToArgumentsWithDefaultValue(Map argNames, Map configArguments, AttributeScopeProto scope, List removedAttrKeys) { + private Map mapToArgumentsWithDefaultValue(EntityId msgEntityId, + Map> args, + Map configArguments, + List geofencingArgNames, + Map> relatedEntityArgs, + AttributeScopeProto scope, + List removedAttrKeys) { + if (args.isEmpty() && relatedEntityArgs.isEmpty()) { + return Collections.emptyMap(); + } Map arguments = new HashMap<>(); for (String removedKey : removedAttrKeys) { ReferencedEntityKey key = new ReferencedEntityKey(removedKey, ArgumentType.ATTRIBUTE, AttributeScope.valueOf(scope.name())); - String argName = argNames.get(key); - if (argName != null) { - Argument argument = configArguments.get(argName); - String defaultValue = (argument != null) ? argument.getDefaultValue() : null; - arguments.put(argName, StringUtils.isNotEmpty(defaultValue) - ? new SingleValueArgumentEntry(System.currentTimeMillis(), new StringDataEntry(removedKey, defaultValue), null) - : new SingleValueArgumentEntry()); - + Set argNames = relatedEntityArgs.get(key); + if (argNames != null) { + argNames.forEach(argName -> { + String defaultValue = getDefaultValue(configArguments, argName); + SingleValueArgumentEntry argumentEntry = buildSingleValue(removedKey, defaultValue, System.currentTimeMillis()); + arguments.put(argName, new SingleValueArgumentEntry(msgEntityId, argumentEntry)); + }); } + argNames = args.get(key); + if (argNames == null) { + continue; + } + argNames.forEach(argName -> { + if (geofencingArgNames.contains(argName)) { + arguments.put(argName, new GeofencingArgumentEntry()); + } else { + String defaultValue = getDefaultValue(configArguments, argName); + SingleValueArgumentEntry argumentEntry = buildSingleValue(removedKey, defaultValue, System.currentTimeMillis()); + arguments.put(argName, new SingleValueArgumentEntry(argumentEntry)); + } + }); } return arguments; } - private Map mapToArgumentsWithFetchedValue(CalculatedFieldCtx ctx, List removedTelemetryKeys) { + private String getDefaultValue(Map configArguments, String argNames) { + Argument argument = configArguments.get(argNames); + return argument != null ? argument.getDefaultValue() : null; + } + + private SingleValueArgumentEntry buildSingleValue(String attrKey, String defaultValue, long ts) { + return StringUtils.isNotEmpty(defaultValue) + ? new SingleValueArgumentEntry(ts, new StringDataEntry(attrKey, defaultValue), null) + : new SingleValueArgumentEntry(); + } + + private Map mapToArgumentsWithFetchedValue(CalculatedFieldCtx ctx, EntityId entityId, List removedTelemetryKeys) { Map deletedArguments = ctx.getArguments().entrySet().stream() .filter(entry -> removedTelemetryKeys.contains(entry.getValue().getRefEntityKey().getKey())) .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); Map fetchedArgs = cfService.fetchArgsFromDb(tenantId, entityId, deletedArguments); + if (CalculatedFieldType.RELATED_ENTITIES_AGGREGATION.equals(ctx.getCfType())) { + fetchedArgs = setEntityIdToSingleEntityArguments(entityId, fetchedArgs); + } fetchedArgs.values().forEach(arg -> arg.setForceResetPrevious(true)); + return fetchedArgs; } + private Map setEntityIdToSingleEntityArguments(EntityId relatedEntityId, Map fetchedArgs) { + return fetchedArgs.entrySet().stream() + .collect(Collectors.toMap( + Map.Entry::getKey, + argEntry -> new SingleValueArgumentEntry(relatedEntityId, argEntry.getValue()) + )); + } + private static List getCalculatedFieldIds(CalculatedFieldTelemetryMsgProto proto) { List cfIds = new LinkedList<>(); for (var cfId : proto.getPreviousCalculatedFieldsList()) { diff --git a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldLinkedTelemetryMsg.java b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldLinkedTelemetryMsg.java index 3e0fba2627..f92ed2ca9e 100644 --- a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldLinkedTelemetryMsg.java +++ b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldLinkedTelemetryMsg.java @@ -22,7 +22,6 @@ import org.thingsboard.server.common.msg.MsgType; import org.thingsboard.server.common.msg.ToCalculatedFieldSystemMsg; import org.thingsboard.server.common.msg.queue.TbCallback; import org.thingsboard.server.gen.transport.TransportProtos.CalculatedFieldLinkedTelemetryMsgProto; -import org.thingsboard.server.gen.transport.TransportProtos.CalculatedFieldTelemetryMsgProto; @Data public class CalculatedFieldLinkedTelemetryMsg implements ToCalculatedFieldSystemMsg { @@ -32,9 +31,9 @@ public class CalculatedFieldLinkedTelemetryMsg implements ToCalculatedFieldSyste private final CalculatedFieldLinkedTelemetryMsgProto proto; private final TbCallback callback; - @Override public MsgType getMsgType() { return MsgType.CF_LINKED_TELEMETRY_MSG; } + } diff --git a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerActor.java b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerActor.java index 9f59a80e67..80daff07ef 100644 --- a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerActor.java +++ b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerActor.java @@ -20,13 +20,11 @@ import org.thingsboard.server.actors.ActorSystemContext; import org.thingsboard.server.actors.TbActorCtx; import org.thingsboard.server.actors.TbActorException; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.msg.CalculatedFieldStatePartitionRestoreMsg; import org.thingsboard.server.common.msg.TbActorStopReason; import org.thingsboard.server.common.msg.ToCalculatedFieldSystemMsg; import org.thingsboard.server.common.msg.cf.CalculatedFieldCacheInitMsg; import org.thingsboard.server.common.msg.cf.CalculatedFieldEntityLifecycleMsg; -import org.thingsboard.server.common.msg.cf.CalculatedFieldInitMsg; -import org.thingsboard.server.common.msg.cf.CalculatedFieldInitProfileEntityMsg; -import org.thingsboard.server.common.msg.cf.CalculatedFieldLinkInitMsg; import org.thingsboard.server.common.msg.cf.CalculatedFieldPartitionChangeMsg; /** @@ -70,21 +68,18 @@ public class CalculatedFieldManagerActor extends AbstractCalculatedFieldActor { case CF_CACHE_INIT_MSG: processor.onCacheInitMsg((CalculatedFieldCacheInitMsg) msg); break; - case CF_INIT_PROFILE_ENTITY_MSG: - processor.onProfileEntityMsg((CalculatedFieldInitProfileEntityMsg) msg); - break; - case CF_INIT_MSG: - processor.onFieldInitMsg((CalculatedFieldInitMsg) msg); - break; - case CF_LINK_INIT_MSG: - processor.onLinkInitMsg((CalculatedFieldLinkInitMsg) msg); - break; case CF_STATE_RESTORE_MSG: processor.onStateRestoreMsg((CalculatedFieldStateRestoreMsg) msg); break; + case CF_STATE_PARTITION_RESTORE_MSG: + processor.onStatePartitionRestoreMsg((CalculatedFieldStatePartitionRestoreMsg) msg); + break; case CF_ENTITY_LIFECYCLE_MSG: processor.onEntityLifecycleMsg((CalculatedFieldEntityLifecycleMsg) msg); break; + case CF_ENTITY_ACTION_EVENT_MSG: + processor.onEntityActionEventMsg((CalculatedFieldEntityActionEventMsg) msg); + break; case CF_TELEMETRY_MSG: processor.onTelemetryMsg((CalculatedFieldTelemetryMsg) msg); break; diff --git a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java index ba1ca71bda..9dc1342b23 100644 --- a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java +++ b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java @@ -16,38 +16,54 @@ package org.thingsboard.server.actors.calculatedField; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.function.TriConsumer; +import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.actors.ActorSystemContext; import org.thingsboard.server.actors.TbActorCtx; import org.thingsboard.server.actors.TbActorRef; import org.thingsboard.server.actors.TbCalculatedFieldEntityActorId; +import org.thingsboard.server.actors.calculatedField.EntityInitCalculatedFieldMsg.StateAction; import org.thingsboard.server.actors.service.DefaultActorService; import org.thingsboard.server.actors.shared.AbstractContextAwareMsgProcessor; +import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.DataConstants; +import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.ProfileEntityIdInfo; +import org.thingsboard.server.common.data.alarm.Alarm; +import org.thingsboard.server.common.data.asset.AssetProfile; +import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.cf.CalculatedField; import org.thingsboard.server.common.data.cf.CalculatedFieldLink; +import org.thingsboard.server.common.data.cf.CalculatedFieldType; +import org.thingsboard.server.common.data.cf.configuration.aggregation.RelatedEntitiesAggregationCalculatedFieldConfiguration; import org.thingsboard.server.common.data.id.AssetId; import org.thingsboard.server.common.data.id.CalculatedFieldId; 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.page.PageDataIterable; +import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; +import org.thingsboard.server.common.data.relation.EntityRelation; +import org.thingsboard.server.common.data.relation.EntityRelationPathQuery; +import org.thingsboard.server.common.data.relation.EntitySearchDirection; +import org.thingsboard.server.common.data.relation.RelationPathLevel; +import org.thingsboard.server.common.msg.CalculatedFieldStatePartitionRestoreMsg; import org.thingsboard.server.common.msg.cf.CalculatedFieldCacheInitMsg; import org.thingsboard.server.common.msg.cf.CalculatedFieldEntityLifecycleMsg; -import org.thingsboard.server.common.msg.cf.CalculatedFieldInitMsg; -import org.thingsboard.server.common.msg.cf.CalculatedFieldInitProfileEntityMsg; -import org.thingsboard.server.common.msg.cf.CalculatedFieldLinkInitMsg; import org.thingsboard.server.common.msg.cf.CalculatedFieldPartitionChangeMsg; import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.TbCallback; import org.thingsboard.server.dao.asset.AssetService; import org.thingsboard.server.dao.cf.CalculatedFieldService; +import org.thingsboard.server.dao.customer.CustomerService; import org.thingsboard.server.dao.device.DeviceService; +import org.thingsboard.server.dao.relation.RelationService; import org.thingsboard.server.queue.settings.TbQueueCalculatedFieldSettings; import org.thingsboard.server.service.cf.CalculatedFieldProcessingService; import org.thingsboard.server.service.cf.CalculatedFieldStateService; +import org.thingsboard.server.service.cf.OwnerService; import org.thingsboard.server.service.cf.cache.TenantEntityProfileCache; import org.thingsboard.server.service.cf.ctx.CalculatedFieldEntityCtxId; import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx; @@ -55,11 +71,20 @@ import org.thingsboard.server.service.profile.TbAssetProfileCache; import org.thingsboard.server.service.profile.TbDeviceProfileCache; import java.util.ArrayList; +import java.util.Collection; import java.util.Collections; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Optional; +import java.util.Set; import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import java.util.function.BiConsumer; +import java.util.function.Function; +import java.util.stream.Stream; import static org.thingsboard.server.utils.CalculatedFieldUtils.fromProto; @@ -72,15 +97,20 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware private final Map calculatedFields = new HashMap<>(); private final Map> entityIdCalculatedFields = new HashMap<>(); private final Map> entityIdCalculatedFieldLinks = new HashMap<>(); + private final Map> ownerEntities = new HashMap<>(); + private ScheduledFuture cfsReevaluationTask; private final CalculatedFieldProcessingService cfExecService; private final CalculatedFieldStateService cfStateService; private final CalculatedFieldService cfDaoService; private final DeviceService deviceService; private final AssetService assetService; + private final CustomerService customerService; + private final RelationService relationService; private final TbAssetProfileCache assetProfileCache; private final TbDeviceProfileCache deviceProfileCache; private final TenantEntityProfileCache entityProfileCache; + private final OwnerService ownerService; private final TbQueueCalculatedFieldSettings cfSettings; protected final TenantId tenantId; @@ -93,9 +123,12 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware this.cfDaoService = systemContext.getCalculatedFieldService(); this.deviceService = systemContext.getDeviceService(); this.assetService = systemContext.getAssetService(); + this.customerService = systemContext.getCustomerService(); + this.relationService = systemContext.getRelationService(); this.assetProfileCache = systemContext.getAssetProfileCache(); this.deviceProfileCache = systemContext.getDeviceProfileCache(); this.entityProfileCache = new TenantEntityProfileCache(); + this.ownerService = systemContext.getOwnerService(); this.cfSettings = systemContext.getCalculatedFieldSettings(); this.tenantId = tenantId; } @@ -106,121 +139,114 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware public void stop() { log.info("[{}] Stopping CF manager actor.", tenantId); - calculatedFields.values().forEach(CalculatedFieldCtx::stop); + calculatedFields.values().forEach(CalculatedFieldCtx::close); calculatedFields.clear(); entityIdCalculatedFields.clear(); entityIdCalculatedFieldLinks.clear(); + if (cfsReevaluationTask != null) { + cfsReevaluationTask.cancel(true); + cfsReevaluationTask = null; + } ctx.stop(ctx.getSelf()); } public void onCacheInitMsg(CalculatedFieldCacheInitMsg msg) { log.debug("[{}] Processing CF actor init message.", msg.getTenantId().getId()); - initEntityProfileCache(); + initEntitiesCache(); initCalculatedFields(); - msg.getCallback().onSuccess(); - } - - public void onProfileEntityMsg(CalculatedFieldInitProfileEntityMsg msg) { - log.debug("[{}] Processing profile entity message.", msg.getTenantId().getId()); - entityProfileCache.add(msg.getProfileEntityId(), msg.getEntityId()); - msg.getCallback().onSuccess(); - } - - public void onFieldInitMsg(CalculatedFieldInitMsg msg) throws CalculatedFieldException { - log.debug("[{}] Processing CF init message.", msg.getCf().getId()); - var cf = msg.getCf(); - var cfCtx = new CalculatedFieldCtx(cf, systemContext.getTbelInvokeService(), systemContext.getApiLimitService()); - try { - cfCtx.init(); - } catch (Exception e) { - throw CalculatedFieldException.builder().ctx(cfCtx).eventEntity(cf.getEntityId()).cause(e).errorMessage("Failed to initialize CF context").build(); - } - calculatedFields.put(cf.getId(), cfCtx); - // We use copy on write lists to safely pass the reference to another actor for the iteration. - // Alternative approach would be to use any list but avoid modifications to the list (change the complete map value instead) - entityIdCalculatedFields.computeIfAbsent(cf.getEntityId(), id -> new CopyOnWriteArrayList<>()).add(cfCtx); - msg.getCallback().onSuccess(); - } - - public void onLinkInitMsg(CalculatedFieldLinkInitMsg msg) { - log.debug("[{}] Processing CF link init message for entity [{}].", msg.getLink().getCalculatedFieldId(), msg.getLink().getEntityId()); - var link = msg.getLink(); - // We use copy on write lists to safely pass the reference to another actor for the iteration. - // Alternative approach would be to use any list but avoid modifications to the list (change the complete map value instead) - entityIdCalculatedFieldLinks.computeIfAbsent(link.getEntityId(), id -> new CopyOnWriteArrayList<>()).add(link); + scheduleCfsReevaluation(); msg.getCallback().onSuccess(); } public void onStateRestoreMsg(CalculatedFieldStateRestoreMsg msg) { var cfId = msg.getId().cfId(); - var calculatedField = calculatedFields.get(cfId); + var ctx = calculatedFields.get(cfId); - if (calculatedField != null) { - if (msg.getState() != null) { - msg.getState().setRequiredArguments(calculatedField.getArgNames()); - } + if (ctx != null) { + msg.setCtx(ctx); log.debug("Pushing CF state restore msg to specific actor [{}]", msg.getId().entityId()); getOrCreateActor(msg.getId().entityId()).tell(msg); } else { - cfStateService.removeState(msg.getId(), msg.getCallback()); + cfStateService.deleteState(msg.getId(), msg.getCallback()); } } + public void onStatePartitionRestoreMsg(CalculatedFieldStatePartitionRestoreMsg msg) { + ctx.broadcastToChildren(msg, true); + } + + private void scheduleCfsReevaluation() { + cfsReevaluationTask = systemContext.getScheduler().scheduleWithFixedDelay(() -> { + try { + calculatedFields.values().forEach(cf -> { + if (cf.isRequiresScheduledReevaluation()) { + applyToTargetCfEntityActors(cf, TbCallback.EMPTY, (entityId, callback) -> { + log.debug("[{}][{}] Pushing scheduled CF reevaluate msg", entityId, cf.getCfId()); + getOrCreateActor(entityId).tell(new CalculatedFieldReevaluateMsg(tenantId, cf)); + }); + } + }); + } catch (Exception e) { + log.warn("[{}] Failed to trigger CFs reevaluation", tenantId, e); + } + }, systemContext.getCfCheckInterval(), systemContext.getCfCheckInterval(), TimeUnit.SECONDS); + } + public void onEntityLifecycleMsg(CalculatedFieldEntityLifecycleMsg msg) throws CalculatedFieldException { - log.debug("Processing entity lifecycle event: [{}] for entity: [{}]", msg.getData().getEvent(), msg.getData().getEntityId()); - var entityType = msg.getData().getEntityId().getEntityType(); var event = msg.getData().getEvent(); + if (ComponentLifecycleEvent.RELATION_UPDATED.equals(event) || ComponentLifecycleEvent.RELATION_DELETED.equals(event)) { + log.debug("Processing relation [{}] event from entity: [{}]", event, msg.getData().getEntityId()); + onRelationChangedEvent(msg.getData(), msg.getCallback()); + return; + } + log.debug("Processing entity lifecycle event: [{}] for entity: [{}]", event, msg.getData().getEntityId()); + var entityType = msg.getData().getEntityId().getEntityType(); switch (entityType) { - case CALCULATED_FIELD: { + case CALCULATED_FIELD -> { switch (event) { - case CREATED: - onCfCreated(msg.getData(), msg.getCallback()); - break; - case UPDATED: - onCfUpdated(msg.getData(), msg.getCallback()); - break; - case DELETED: - onCfDeleted(msg.getData(), msg.getCallback()); - break; - default: - msg.getCallback().onSuccess(); - break; + case CREATED -> onCfCreated(msg.getData(), msg.getCallback()); + case UPDATED -> onCfUpdated(msg.getData(), msg.getCallback()); + case DELETED -> onCfDeleted(msg.getData(), msg.getCallback()); + default -> msg.getCallback().onSuccess(); } - break; } - case DEVICE: - case ASSET: { + case DEVICE, ASSET, CUSTOMER -> { switch (event) { - case CREATED: - onEntityCreated(msg.getData(), msg.getCallback()); - break; - case UPDATED: - onEntityUpdated(msg.getData(), msg.getCallback()); - break; - case DELETED: - onEntityDeleted(msg.getData(), msg.getCallback()); - break; - default: - msg.getCallback().onSuccess(); - break; + case CREATED -> onEntityCreated(msg.getData(), msg.getCallback()); + case UPDATED -> onEntityUpdated(msg.getData(), msg.getCallback()); + case DELETED -> onEntityDeleted(msg.getData(), msg.getCallback()); + default -> msg.getCallback().onSuccess(); } - break; } - case DEVICE_PROFILE: - case ASSET_PROFILE: { + case DEVICE_PROFILE, ASSET_PROFILE -> { switch (event) { - case DELETED: - onProfileDeleted(msg.getData(), msg.getCallback()); - break; - default: - msg.getCallback().onSuccess(); - break; + case DELETED -> onProfileDeleted(msg.getData(), msg.getCallback()); + default -> msg.getCallback().onSuccess(); } - break; } - default: { - msg.getCallback().onSuccess(); + case TENANT_PROFILE -> { + switch (event) { + case UPDATED -> onTenantProfileUpdated(msg.getData(), msg.getCallback()); + default -> msg.getCallback().onSuccess(); + } + } + default -> msg.getCallback().onSuccess(); + } + } + + public void onEntityActionEventMsg(CalculatedFieldEntityActionEventMsg msg) { + switch (msg.getAction()) { + case ALARM_ACK, ALARM_CLEAR, ALARM_DELETE -> { + Alarm alarm = JacksonUtil.treeToValue(msg.getEntity(), Alarm.class); + CalculatedFieldAlarmActionMsg alarmActionMsg = CalculatedFieldAlarmActionMsg.builder() + .tenantId(tenantId) + .alarm(alarm) + .action(msg.getAction()) + .callback(msg.getCallback()) + .build(); + getOrCreateActor(alarm.getOriginator()).tellWithHighPriority(alarmActionMsg); } + default -> msg.getCallback().onSuccess(); } } @@ -229,12 +255,22 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware callback.onSuccess(); } + private void onTenantProfileUpdated(ComponentLifecycleMsg msg, TbCallback callback) { + Stream.concat( + calculatedFields.values().stream(), + entityIdCalculatedFields.values().stream().flatMap(Collection::stream) + ).forEach(CalculatedFieldCtx::updateTenantProfileProperties); + callback.onSuccess(); + } + private void onEntityCreated(ComponentLifecycleMsg msg, TbCallback callback) { EntityId entityId = msg.getEntityId(); EntityId profileId = getProfileId(tenantId, entityId); if (profileId != null) { entityProfileCache.add(profileId, entityId); } + updateEntityOwner(entityId); + if (!isMyPartition(entityId, callback)) { return; } @@ -243,8 +279,8 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware var fieldsCount = entityIdFields.size() + profileIdFields.size(); if (fieldsCount > 0) { MultipleTbCallback multiCallback = new MultipleTbCallback(fieldsCount, callback); - entityIdFields.forEach(ctx -> initCfForEntity(entityId, ctx, true, multiCallback)); - profileIdFields.forEach(ctx -> initCfForEntity(entityId, ctx, true, multiCallback)); + entityIdFields.forEach(ctx -> initCfForEntity(entityId, ctx, StateAction.INIT, multiCallback)); + profileIdFields.forEach(ctx -> initCfForEntity(entityId, ctx, StateAction.INIT, multiCallback)); } else { callback.onSuccess(); } @@ -263,21 +299,84 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware MultipleTbCallback multiCallback = new MultipleTbCallback(fieldsCount, callback); var entityId = msg.getEntityId(); oldProfileCfs.forEach(ctx -> deleteCfForEntity(entityId, ctx.getCfId(), multiCallback)); - newProfileCfs.forEach(ctx -> initCfForEntity(entityId, ctx, true, multiCallback)); + newProfileCfs.forEach(ctx -> initCfForEntity(entityId, ctx, StateAction.INIT, multiCallback)); } else { callback.onSuccess(); } + } else if (msg.isOwnerChanged()) { + onEntityOwnerChanged(msg, callback); + } else { + callback.onSuccess(); } } private void onEntityDeleted(ComponentLifecycleMsg msg, TbCallback callback) { - entityProfileCache.removeEntityId(msg.getEntityId()); + switch (msg.getEntityId().getEntityType()) { + case DEVICE, ASSET -> entityProfileCache.removeEntityId(msg.getEntityId()); + case CUSTOMER -> ownerEntities.remove(msg.getEntityId()); + } + ownerEntities.values().forEach(entities -> entities.remove(msg.getEntityId())); if (isMyPartition(msg.getEntityId(), callback)) { log.debug("Pushing entity lifecycle msg to specific actor [{}]", msg.getEntityId()); getOrCreateActor(msg.getEntityId()).tell(new CalculatedFieldEntityDeleteMsg(tenantId, msg.getEntityId(), callback)); } } + private void onRelationChangedEvent(ComponentLifecycleMsg msg, TbCallback callback) { + Function> relationAction = switch (msg.getEvent()) { + case RELATION_UPDATED -> relatedId -> (entityId, ctx, cb) -> initRelatedEntity(entityId, relatedId, ctx, cb); + case RELATION_DELETED -> relatedId -> (entityId, ctx, cb) -> deleteRelatedEntity(entityId, relatedId, ctx, cb); + default -> null; + }; + + if (relationAction == null) { + callback.onSuccess(); + return; + } + + EntityRelation entityRelation = JacksonUtil.treeToValue(msg.getInfo(), EntityRelation.class); + EntityId toId = entityRelation.getTo(); + EntityId fromId = entityRelation.getFrom(); + String relationType = entityRelation.getType(); + + if (!(CalculatedField.isSupportedRefEntity(toId) || CalculatedField.isSupportedRefEntity(fromId))) { + callback.onSuccess(); + return; + } + + MultipleTbCallback callbackForToAndFrom = new MultipleTbCallback(2, callback); + processRelationByDirection(EntitySearchDirection.TO, relationType, toId, callbackForToAndFrom, relationAction.apply(fromId)); + processRelationByDirection(EntitySearchDirection.FROM, relationType, fromId, callbackForToAndFrom, relationAction.apply(toId)); + } + + private void processRelationByDirection(EntitySearchDirection direction, + String relationType, + EntityId mainId, + MultipleTbCallback parentCallback, + TriConsumer relationAction) { + List cfsByEntityIdAndProfile = getCalculatedFieldsByEntityIdAndProfile(mainId); + if (cfsByEntityIdAndProfile.isEmpty()) { + parentCallback.onSuccess(); + return; + } + + List matchingCfs = cfsByEntityIdAndProfile.stream() + .filter(cf -> { + if (cf.getCalculatedField().getConfiguration() instanceof RelatedEntitiesAggregationCalculatedFieldConfiguration config) { + RelationPathLevel relation = config.getRelation(); + return direction.equals(relation.direction()) && relationType.equals(relation.relationType()); + } + return false; + }) + .toList(); + + MultipleTbCallback directionCallback = new MultipleTbCallback(matchingCfs.size(), parentCallback); + + matchingCfs.forEach(ctx -> + applyToTargetCfEntityActors(ctx, directionCallback, (entityId, cb) -> relationAction.accept(entityId, ctx, cb)) + ); + } + private void onCfCreated(ComponentLifecycleMsg msg, TbCallback callback) throws CalculatedFieldException { var cfId = new CalculatedFieldId(msg.getEntityId().getId()); if (calculatedFields.containsKey(cfId)) { @@ -289,7 +388,7 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware log.debug("[{}] Failed to lookup CF by id [{}]", tenantId, cfId); callback.onSuccess(); } else { - var cfCtx = new CalculatedFieldCtx(cf, systemContext.getTbelInvokeService(), systemContext.getApiLimitService()); + var cfCtx = getCfCtx(cf); try { cfCtx.init(); } catch (Exception e) { @@ -300,11 +399,15 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware // Alternative approach would be to use any list but avoid modifications to the list (change the complete map value instead) entityIdCalculatedFields.computeIfAbsent(cf.getEntityId(), id -> new CopyOnWriteArrayList<>()).add(cfCtx); addLinks(cf); - initCf(cfCtx, callback, false); + applyToTargetCfEntityActors(cfCtx, callback, (id, cb) -> initCfForEntity(id, cfCtx, StateAction.INIT, cb)); } } } + private CalculatedFieldCtx getCfCtx(CalculatedField cf) { + return new CalculatedFieldCtx(cf, systemContext); + } + private void onCfUpdated(ComponentLifecycleMsg msg, TbCallback callback) throws CalculatedFieldException { var cfId = new CalculatedFieldId(msg.getEntityId().getId()); var oldCfCtx = calculatedFields.get(cfId); @@ -316,40 +419,59 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware log.debug("[{}] Failed to lookup CF by id [{}]", tenantId, cfId); callback.onSuccess(); } else { - var newCfCtx = new CalculatedFieldCtx(newCf, systemContext.getTbelInvokeService(), systemContext.getApiLimitService()); + var newCfCtx = getCfCtx(newCf); try { newCfCtx.init(); } catch (Exception e) { throw CalculatedFieldException.builder().ctx(newCfCtx).eventEntity(newCfCtx.getEntityId()).cause(e).errorMessage("Failed to initialize CF context").build(); - } - calculatedFields.put(newCf.getId(), newCfCtx); - List oldCfList = entityIdCalculatedFields.get(newCf.getEntityId()); - List newCfList = new CopyOnWriteArrayList<>(); - boolean found = false; - for (CalculatedFieldCtx oldCtx : oldCfList) { - if (oldCtx.getCfId().equals(newCf.getId())) { + } finally { + calculatedFields.put(newCf.getId(), newCfCtx); + List oldCfList = entityIdCalculatedFields.get(newCf.getEntityId()); + List newCfList = new CopyOnWriteArrayList<>(); + boolean found = false; + for (CalculatedFieldCtx oldCtx : oldCfList) { + if (oldCtx.getCfId().equals(newCf.getId())) { + newCfList.add(newCfCtx); + found = true; + } else { + newCfList.add(oldCtx); + } + } + if (!found) { newCfList.add(newCfCtx); - found = true; - } else { - newCfList.add(oldCtx); } + // We use copy on write lists to safely pass the reference to another actor for the iteration. + // Alternative approach would be to use any list but avoid modifications to the list (change the complete map value instead) + entityIdCalculatedFields.put(newCf.getEntityId(), newCfList); + deleteLinks(oldCfCtx); + addLinks(newCf); } - if (!found) { - newCfList.add(newCfCtx); - } - entityIdCalculatedFields.put(newCf.getEntityId(), newCfList); - - deleteLinks(oldCfCtx); - addLinks(newCf); - // We use copy on write lists to safely pass the reference to another actor for the iteration. - // Alternative approach would be to use any list but avoid modifications to the list (change the complete map value instead) - var stateChanges = newCfCtx.hasStateChanges(oldCfCtx); - if (stateChanges || newCfCtx.hasOtherSignificantChanges(oldCfCtx)) { - initCf(newCfCtx, callback, stateChanges); + StateAction stateAction; + if (newCfCtx.getCfType() != oldCfCtx.getCfType()) { + stateAction = StateAction.RECREATE; // completely recreate state, then calculate + } else if (newCfCtx.hasStateChanges(oldCfCtx)) { + stateAction = StateAction.REINIT; // refetch arguments, call state.init, then calculate + } else if (newCfCtx.hasContextOnlyChanges(oldCfCtx)) { + stateAction = StateAction.REPROCESS; // call state.setCtx, then calculate } else { callback.onSuccess(); + return; } + + applyToTargetCfEntityActors(newCfCtx, new TbCallback() { + @Override + public void onSuccess() { + oldCfCtx.close(); + callback.onSuccess(); + } + + @Override + public void onFailure(Throwable t) { + oldCfCtx.close(); + callback.onFailure(t); + } + }, (id, cb) -> initCfForEntity(id, newCfCtx, stateAction, cb)); } } } @@ -360,38 +482,30 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware if (cfCtx == null) { log.debug("[{}] CF was already deleted [{}]", tenantId, cfId); callback.onSuccess(); - } else { - entityIdCalculatedFields.get(cfCtx.getEntityId()).remove(cfCtx); - deleteLinks(cfCtx); - - EntityId entityId = cfCtx.getEntityId(); - EntityType entityType = cfCtx.getEntityId().getEntityType(); - if (isProfileEntity(entityType)) { - var entityIds = entityProfileCache.getEntityIdsByProfileId(entityId); - if (!entityIds.isEmpty()) { - //TODO: no need to do this if we cache all created actors and know which one belong to us; - var multiCallback = new MultipleTbCallback(entityIds.size(), callback); - entityIds.forEach(id -> { - if (isMyPartition(id, multiCallback)) { - deleteCfForEntity(id, cfId, multiCallback); - } - }); - } else { - callback.onSuccess(); - } - } else { - if (isMyPartition(entityId, callback)) { - deleteCfForEntity(entityId, cfId, callback); - } - } + return; } + entityIdCalculatedFields.get(cfCtx.getEntityId()).remove(cfCtx); + deleteLinks(cfCtx); + applyToTargetCfEntityActors(cfCtx, new TbCallback() { + @Override + public void onSuccess() { + cfCtx.close(); + callback.onSuccess(); + } + + @Override + public void onFailure(Throwable t) { + cfCtx.close(); + callback.onFailure(t); + } + }, (id, cb) -> deleteCfForEntity(id, cfId, cb)); } public void onTelemetryMsg(CalculatedFieldTelemetryMsg msg) { EntityId entityId = msg.getEntityId(); log.debug("Received telemetry msg from entity [{}]", entityId); - // 2 = 1 for CF processing + 1 for links processing - MultipleTbCallback callback = new MultipleTbCallback(2, msg.getCallback()); + // 4 = 1 for CF processing + 1 for links processing + 1 for owner entity processing + 1 for aggregation processing + MultipleTbCallback callback = new MultipleTbCallback(4, msg.getCallback()); // process all cfs related to entity, or it's profile; var entityIdFields = getCalculatedFieldsByEntityId(entityId); var profileIdFields = getCalculatedFieldsByEntityId(getProfileId(tenantId, entityId)); @@ -409,6 +523,60 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware } else { callback.onSuccess(); } + // process all cfs related to owner entity + if (entityId.getEntityType().isOneOf(EntityType.TENANT, EntityType.CUSTOMER)) { + List ownedEntitiesCFs = filterOwnedEntitiesCFs(msg); + if (!ownedEntitiesCFs.isEmpty()) { + cfExecService.pushMsgToLinks(msg, ownedEntitiesCFs, callback); + } else { + callback.onSuccess(); + } + } else { + callback.onSuccess(); + } + // process all aggregation cfs (if any); + List aggregationCalculatedFields = filterAggregationCfs(msg); + if (!aggregationCalculatedFields.isEmpty()) { + cfExecService.pushMsgToLinks(msg, aggregationCalculatedFields, callback); + } else { + callback.onSuccess(); + } + } + + private List filterAggregationCfs(CalculatedFieldTelemetryMsg msg) { + EntityId entityId = msg.getEntityId(); + return calculatedFields.values().stream() + .filter(cf -> CalculatedFieldType.RELATED_ENTITIES_AGGREGATION.equals(cf.getCfType())) + .filter(cf -> cf.relatedEntityMatches(msg.getProto())) + .flatMap(cf -> findRelationsForCf(entityId, cf).stream()) + .toList(); + } + + private List findRelationsForCf(EntityId entityId, CalculatedFieldCtx cf) { + List result = new ArrayList<>(); + if (cf.getCalculatedField().getConfiguration() instanceof RelatedEntitiesAggregationCalculatedFieldConfiguration configuration) { + RelationPathLevel relation = configuration.getRelation(); + EntitySearchDirection inverseDirection = switch (relation.direction()) { + case FROM -> EntitySearchDirection.TO; + case TO -> EntitySearchDirection.FROM; + }; + RelationPathLevel inverseRelation = new RelationPathLevel(inverseDirection, relation.relationType()); + List byRelationPathQuery = relationService.findByRelationPathQuery(tenantId, new EntityRelationPathQuery(entityId, List.of(inverseRelation))); + if (byRelationPathQuery != null && !byRelationPathQuery.isEmpty()) { + switch (relation.direction()) { + case FROM -> { + EntityRelation entityRelation = byRelationPathQuery.get(0); // only one supported + result.add(new CalculatedFieldEntityCtxId(tenantId, cf.getCfId(), entityRelation.getFrom())); + } + case TO -> { + byRelationPathQuery.stream() + .filter(entityRelation -> entityRelation.getTo().equals(cf.getEntityId())) + .forEach(entityRelation -> result.add(new CalculatedFieldEntityCtxId(tenantId, cf.getCfId(), entityRelation.getTo()))); + } + } + } + } + return result; } public void onLinkedTelemetryMsg(CalculatedFieldLinkedTelemetryMsg msg) { @@ -423,32 +591,35 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware } for (var linkProto : linksList) { var link = fromProto(linkProto); - var targetEntityId = link.entityId(); - var targetEntityType = targetEntityId.getEntityType(); var cf = calculatedFields.get(link.cfId()); - if (EntityType.DEVICE_PROFILE.equals(targetEntityType) || EntityType.ASSET_PROFILE.equals(targetEntityType)) { - // iterate over all entities that belong to profile and push the message for corresponding CF - var entityIds = entityProfileCache.getEntityIdsByProfileId(targetEntityId); - if (!entityIds.isEmpty()) { - MultipleTbCallback multipleCallback = new MultipleTbCallback(entityIds.size(), callback); - var newMsg = new EntityCalculatedFieldLinkedTelemetryMsg(tenantId, sourceEntityId, proto.getMsg(), cf, multipleCallback); - entityIds.forEach(entityId -> { - if (isMyPartition(entityId, multipleCallback)) { - log.debug("Pushing linked telemetry msg to specific actor [{}]", entityId); - getOrCreateActor(entityId).tell(newMsg); - } - }); + withTargetEntities(link.entityId(), callback, (ids, cb) -> { + var linkedTelemetryMsg = new EntityCalculatedFieldLinkedTelemetryMsg(tenantId, sourceEntityId, proto.getMsg(), cf, cb); + ids.forEach(id -> linkedTelemetryMsgForEntity(id, linkedTelemetryMsg)); + }); + } + } + + private void onEntityOwnerChanged(ComponentLifecycleMsg msg, TbCallback msgCallback) { + EntityId entityId = msg.getEntityId(); + log.debug("Received changed owner msg from entity [{}]", entityId); + updateEntityOwner(entityId); + List cfs = getCalculatedFieldsByEntityIdAndProfile(entityId); + if (cfs.isEmpty()) { + msgCallback.onSuccess(); + return; + } + MultipleTbCallback callback = new MultipleTbCallback(cfs.size(), msgCallback); + cfs.forEach(cf -> { + if (isMyPartition(entityId, callback)) { + if (cf.hasCurrentOwnerSourceArguments()) { + CalculatedFieldArgumentResetMsg argResetMsg = new CalculatedFieldArgumentResetMsg(tenantId, cf, callback); + log.debug("Pushing CF argument reset msg to specific actor [{}]", entityId); + getOrCreateActor(entityId).tell(argResetMsg); } else { callback.onSuccess(); } - } else { - if (isMyPartition(targetEntityId, callback)) { - log.debug("Pushing linked telemetry msg to specific actor [{}]", targetEntityId); - var newMsg = new EntityCalculatedFieldLinkedTelemetryMsg(tenantId, sourceEntityId, proto.getMsg(), cf, callback); - getOrCreateActor(targetEntityId).tell(newMsg); - } } - } + }); } private List filterCalculatedFieldLinks(CalculatedFieldTelemetryMsg msg) { @@ -456,7 +627,7 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware var proto = msg.getProto(); List result = new ArrayList<>(); for (var link : getCalculatedFieldLinksByEntityId(entityId)) { - CalculatedFieldCtx ctx = calculatedFields.get(link.getCalculatedFieldId()); + CalculatedFieldCtx ctx = calculatedFields.get(link.calculatedFieldId()); if (ctx.linkMatches(entityId, proto)) { result.add(ctx.toCalculatedFieldEntityCtxId()); } @@ -464,6 +635,27 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware return result; } + private List filterOwnedEntitiesCFs(CalculatedFieldTelemetryMsg msg) { + Set entities = getOwnedEntities(msg.getEntityId()); + var proto = msg.getProto(); + List result = new ArrayList<>(); + for (var entityId : entities) { + var ownerEntityCFs = getCalculatedFieldsByEntityId(entityId); + for (var ctx : ownerEntityCFs) { + if (ctx.dynamicSourceMatches(proto)) { + result.add(new CalculatedFieldEntityCtxId(tenantId, ctx.getCfId(), entityId)); + } + } + var ownerEntityProfileCFs = getCalculatedFieldsByEntityId(getProfileId(tenantId, entityId)); + for (var ctx : ownerEntityProfileCFs) { + if (ctx.dynamicSourceMatches(proto)) { + result.add(new CalculatedFieldEntityCtxId(tenantId, ctx.getCfId(), entityId)); + } + } + } + return result; + } + private List getCalculatedFieldsByEntityId(EntityId entityId) { if (entityId == null) { return Collections.emptyList(); @@ -475,6 +667,16 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware return result; } + private List getCalculatedFieldsByEntityIdAndProfile(EntityId entityId) { + List cfsByEntityIdAndProfile = new ArrayList<>(); + cfsByEntityIdAndProfile.addAll(getCalculatedFieldsByEntityId(entityId)); + EntityId profileId = getProfileId(tenantId, entityId); + if (profileId != null) { + cfsByEntityIdAndProfile.addAll(getCalculatedFieldsByEntityId(profileId)); + } + return cfsByEntityIdAndProfile; + } + private List getCalculatedFieldLinksByEntityId(EntityId entityId) { if (entityId == null) { return Collections.emptyList(); @@ -486,26 +688,30 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware return result; } - private void initCf(CalculatedFieldCtx cfCtx, TbCallback callback, boolean forceStateReinit) { - EntityId entityId = cfCtx.getEntityId(); - EntityType entityType = cfCtx.getEntityId().getEntityType(); - if (isProfileEntity(entityType)) { - var entityIds = entityProfileCache.getEntityIdsByProfileId(entityId); - if (!entityIds.isEmpty()) { - var multiCallback = new MultipleTbCallback(entityIds.size(), callback); - entityIds.forEach(id -> { - if (isMyPartition(id, multiCallback)) { - initCfForEntity(id, cfCtx, forceStateReinit, multiCallback); - } - }); - } else { - callback.onSuccess(); - } - } else { - if (isMyPartition(entityId, callback)) { - initCfForEntity(entityId, cfCtx, forceStateReinit, callback); - } + private Set getOwnedEntities(EntityId entityId) { + if (entityId == null) { + return Collections.emptySet(); + } + var result = ownerEntities.get(entityId); + if (result == null) { + result = Collections.emptySet(); } + return result; + } + + private void linkedTelemetryMsgForEntity(EntityId entityId, EntityCalculatedFieldLinkedTelemetryMsg msg) { + log.debug("Pushing linked telemetry msg to specific actor [{}]", entityId); + getOrCreateActor(entityId).tell(msg); + } + + private void deleteRelatedEntity(EntityId entityId, EntityId relatedEntityId, CalculatedFieldCtx cf, TbCallback callback) { + log.debug("Pushing delete related entity msg to specific actor [{}]", relatedEntityId); + getOrCreateActor(entityId).tell(new CalculatedFieldRelationActionMsg(tenantId, relatedEntityId, ActionType.DELETED, cf, callback)); + } + + private void initRelatedEntity(EntityId entityId, EntityId relatedEntityId, CalculatedFieldCtx cf, TbCallback callback) { + log.debug("Pushing init related entity msg to specific actor [{}]", relatedEntityId); + getOrCreateActor(entityId).tell(new CalculatedFieldRelationActionMsg(tenantId, relatedEntityId, ActionType.UPDATED, cf, callback)); } private void deleteCfForEntity(EntityId entityId, CalculatedFieldId cfId, TbCallback callback) { @@ -513,9 +719,9 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware getOrCreateActor(entityId).tell(new CalculatedFieldEntityDeleteMsg(tenantId, cfId, callback)); } - private void initCfForEntity(EntityId entityId, CalculatedFieldCtx cfCtx, boolean forceStateReinit, TbCallback callback) { + private void initCfForEntity(EntityId entityId, CalculatedFieldCtx cfCtx, StateAction stateAction, TbCallback callback) { log.debug("Pushing entity init CF msg to specific actor [{}]", entityId); - getOrCreateActor(entityId).tell(new EntityInitCalculatedFieldMsg(tenantId, cfCtx, callback, forceStateReinit)); + getOrCreateActor(entityId).tell(new EntityInitCalculatedFieldMsg(tenantId, cfCtx, stateAction, callback)); } private boolean isMyPartition(EntityId entityId, TbCallback callback) { @@ -533,8 +739,8 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware private EntityId getProfileId(TenantId tenantId, EntityId entityId) { return switch (entityId.getEntityType()) { - case ASSET -> assetProfileCache.get(tenantId, (AssetId) entityId).getId(); - case DEVICE -> deviceProfileCache.get(tenantId, (DeviceId) entityId).getId(); + case ASSET -> Optional.ofNullable(assetProfileCache.get(tenantId, (AssetId) entityId)).map(AssetProfile::getId).orElse(null); + case DEVICE -> Optional.ofNullable(deviceProfileCache.get(tenantId, (DeviceId) entityId)).map(DeviceProfile::getId).orElse(null); default -> null; }; } @@ -548,13 +754,13 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware private void addLinks(CalculatedField newCf) { var newLinks = newCf.getConfiguration().buildCalculatedFieldLinks(tenantId, newCf.getEntityId(), newCf.getId()); - newLinks.forEach(link -> entityIdCalculatedFieldLinks.computeIfAbsent(link.getEntityId(), id -> new CopyOnWriteArrayList<>()).add(link)); + newLinks.forEach(link -> entityIdCalculatedFieldLinks.computeIfAbsent(link.entityId(), id -> new CopyOnWriteArrayList<>()).add(link)); } private void deleteLinks(CalculatedFieldCtx cfCtx) { var oldCf = cfCtx.getCalculatedField(); var oldLinks = oldCf.getConfiguration().buildCalculatedFieldLinks(tenantId, oldCf.getEntityId(), oldCf.getId()); - oldLinks.forEach(link -> entityIdCalculatedFieldLinks.computeIfAbsent(link.getEntityId(), id -> new CopyOnWriteArrayList<>()).remove(link)); + oldLinks.forEach(link -> entityIdCalculatedFieldLinks.computeIfAbsent(link.entityId(), id -> new CopyOnWriteArrayList<>()).remove(link)); } public void onPartitionChange(CalculatedFieldPartitionChangeMsg msg) { @@ -566,39 +772,101 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware cfs.forEach(cf -> { log.trace("Processing calculated field record: {}", cf); try { - onFieldInitMsg(new CalculatedFieldInitMsg(cf.getTenantId(), cf)); + initCalculatedField(cf); + initCalculatedFieldLinks(cf); } catch (CalculatedFieldException e) { log.error("Failed to process calculated field record: {}", cf, e); } }); - calculatedFields.values().forEach(cf -> { - entityIdCalculatedFields.computeIfAbsent(cf.getEntityId(), id -> new CopyOnWriteArrayList<>()).add(cf); - }); - PageDataIterable cfls = new PageDataIterable<>(pageLink -> cfDaoService.findAllCalculatedFieldLinksByTenantId(tenantId, pageLink), cfSettings.getInitTenantFetchPackSize()); - cfls.forEach(link -> { - onLinkInitMsg(new CalculatedFieldLinkInitMsg(link.getTenantId(), link)); - }); } - private void initEntityProfileCache() { + private void initCalculatedField(CalculatedField cf) throws CalculatedFieldException { + var cfCtx = new CalculatedFieldCtx(cf, systemContext); + try { + cfCtx.init(); + } catch (Exception e) { + throw CalculatedFieldException.builder().ctx(cfCtx).eventEntity(cf.getEntityId()).cause(e).errorMessage("Failed to initialize CF context").build(); + } finally { + calculatedFields.put(cf.getId(), cfCtx); + // We use copy on write lists to safely pass the reference to another actor for the iteration. + // Alternative approach would be to use any list but avoid modifications to the list (change the complete map value instead) + entityIdCalculatedFields.computeIfAbsent(cf.getEntityId(), id -> new CopyOnWriteArrayList<>()).add(cfCtx); + } + } + + private void initCalculatedFieldLinks(CalculatedField cf) { + List links = cf.getConfiguration().buildCalculatedFieldLinks(cf.getTenantId(), cf.getEntityId(), cf.getId()); + for (CalculatedFieldLink link : links) { + // We use copy on write lists to safely pass the reference to another actor for the iteration. + // Alternative approach would be to use any list but avoid modifications to the list (change the complete map value instead) + entityIdCalculatedFieldLinks.computeIfAbsent(link.entityId(), id -> new CopyOnWriteArrayList<>()).add(link); + } + } + + private void initEntitiesCache() { PageDataIterable deviceIdInfos = new PageDataIterable<>(pageLink -> deviceService.findProfileEntityIdInfosByTenantId(tenantId, pageLink), cfSettings.getInitTenantFetchPackSize()); for (ProfileEntityIdInfo idInfo : deviceIdInfos) { log.trace("Processing device record: {}", idInfo); try { entityProfileCache.add(idInfo.getProfileId(), idInfo.getEntityId()); + ownerEntities.computeIfAbsent(idInfo.getOwnerId(), __ -> new HashSet<>()).add(idInfo.getEntityId()); } catch (Exception e) { log.error("Failed to process device record: {}", idInfo, e); } } + PageDataIterable assetIdInfos = new PageDataIterable<>(pageLink -> assetService.findProfileEntityIdInfosByTenantId(tenantId, pageLink), cfSettings.getInitTenantFetchPackSize()); for (ProfileEntityIdInfo idInfo : assetIdInfos) { log.trace("Processing asset record: {}", idInfo); try { entityProfileCache.add(idInfo.getProfileId(), idInfo.getEntityId()); + ownerEntities.computeIfAbsent(idInfo.getOwnerId(), __ -> new HashSet<>()).add(idInfo.getEntityId()); } catch (Exception e) { log.error("Failed to process asset record: {}", idInfo, e); } } + + PageDataIterable customers = new PageDataIterable<>(pageLink -> customerService.findCustomersByTenantId(tenantId, pageLink), cfSettings.getInitTenantFetchPackSize()); + for (Customer customer : customers) { + log.trace("Processing customer record: {}", customer); + try { + ownerEntities.computeIfAbsent(customer.getTenantId(), __ -> new HashSet<>()).add(customer.getId()); + } catch (Exception e) { + log.error("Failed to process customer record: {}", customer, e); + } + } + } + + private void updateEntityOwner(EntityId entityId) { + ownerEntities.values().forEach(entities -> entities.remove(entityId)); + EntityId owner = ownerService.getOwner(tenantId, entityId); + ownerEntities.computeIfAbsent(owner, ownerId -> new HashSet<>()).add(entityId); + } + + private void applyToTargetCfEntityActors(CalculatedFieldCtx ctx, + TbCallback callback, + BiConsumer action) { + withTargetEntities(ctx.getEntityId(), callback, (ids, cb) -> ids.forEach(id -> action.accept(id, cb))); + } + + private void withTargetEntities(EntityId entityId, TbCallback parentCallback, BiConsumer, TbCallback> consumer) { + if (isProfileEntity(entityId.getEntityType())) { + var ids = entityProfileCache.getEntityIdsByProfileId(entityId); + if (ids.isEmpty()) { + parentCallback.onSuccess(); + return; + } + var multiCallback = new MultipleTbCallback(ids.size(), parentCallback); + var profileEntityIds = ids.stream().filter(id -> isMyPartition(id, multiCallback)).toList(); + if (profileEntityIds.isEmpty()) { + return; + } + consumer.accept(profileEntityIds, multiCallback); + return; + } + if (isMyPartition(entityId, parentCallback)) { + consumer.accept(List.of(entityId), parentCallback); + } } } diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/cf/CalculatedFieldLinkInitMsg.java b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldReevaluateMsg.java similarity index 75% rename from common/message/src/main/java/org/thingsboard/server/common/msg/cf/CalculatedFieldLinkInitMsg.java rename to application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldReevaluateMsg.java index d142eb78d8..b617736ee0 100644 --- a/common/message/src/main/java/org/thingsboard/server/common/msg/cf/CalculatedFieldLinkInitMsg.java +++ b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldReevaluateMsg.java @@ -13,22 +13,23 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.common.msg.cf; +package org.thingsboard.server.actors.calculatedField; import lombok.Data; -import org.thingsboard.server.common.data.cf.CalculatedFieldLink; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.msg.MsgType; import org.thingsboard.server.common.msg.ToCalculatedFieldSystemMsg; +import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx; @Data -public class CalculatedFieldLinkInitMsg implements ToCalculatedFieldSystemMsg { +public class CalculatedFieldReevaluateMsg implements ToCalculatedFieldSystemMsg { private final TenantId tenantId; - private final CalculatedFieldLink link; + private final CalculatedFieldCtx ctx; @Override public MsgType getMsgType() { - return MsgType.CF_LINK_INIT_MSG; + return MsgType.CF_REEVALUATE_MSG; } + } diff --git a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldRelationActionMsg.java b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldRelationActionMsg.java new file mode 100644 index 0000000000..4d8e1cf561 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldRelationActionMsg.java @@ -0,0 +1,52 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.actors.calculatedField; + +import lombok.Data; +import org.thingsboard.server.common.data.audit.ActionType; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.msg.MsgType; +import org.thingsboard.server.common.msg.ToCalculatedFieldSystemMsg; +import org.thingsboard.server.common.msg.queue.TbCallback; +import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx; + +@Data +public class CalculatedFieldRelationActionMsg implements ToCalculatedFieldSystemMsg { + + private final TenantId tenantId; + private final EntityId relatedEntityId; + private final ActionType action; + private final CalculatedFieldCtx calculatedField; + private final TbCallback callback; + + public CalculatedFieldRelationActionMsg(TenantId tenantId, + EntityId relatedEntityId, ActionType action, + CalculatedFieldCtx calculatedField, + TbCallback callback) { + this.tenantId = tenantId; + this.relatedEntityId = relatedEntityId; + this.action = action; + this.calculatedField = calculatedField; + this.callback = callback; + } + + @Override + public MsgType getMsgType() { + return MsgType.CF_RELATION_ACTION_MSG; + } + +} diff --git a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldStateRestoreMsg.java b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldStateRestoreMsg.java index 19be7c02fa..d1c2f11aeb 100644 --- a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldStateRestoreMsg.java +++ b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldStateRestoreMsg.java @@ -19,7 +19,9 @@ import lombok.Data; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.msg.MsgType; import org.thingsboard.server.common.msg.ToCalculatedFieldSystemMsg; +import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.service.cf.ctx.CalculatedFieldEntityCtxId; +import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx; import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldState; @Data @@ -27,6 +29,8 @@ public class CalculatedFieldStateRestoreMsg implements ToCalculatedFieldSystemMs private final CalculatedFieldEntityCtxId id; private final CalculatedFieldState state; + private final TopicPartitionInfo partition; + private CalculatedFieldCtx ctx; @Override public MsgType getMsgType() { diff --git a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldTelemetryMsg.java b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldTelemetryMsg.java index 68cd149cdf..a174cff268 100644 --- a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldTelemetryMsg.java +++ b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldTelemetryMsg.java @@ -31,9 +31,9 @@ public class CalculatedFieldTelemetryMsg implements ToCalculatedFieldSystemMsg { private final CalculatedFieldTelemetryMsgProto proto; private final TbCallback callback; - @Override public MsgType getMsgType() { return MsgType.CF_TELEMETRY_MSG; } + } diff --git a/application/src/main/java/org/thingsboard/server/actors/calculatedField/EntityInitCalculatedFieldMsg.java b/application/src/main/java/org/thingsboard/server/actors/calculatedField/EntityInitCalculatedFieldMsg.java index 1e8990ff8d..1e0025988d 100644 --- a/application/src/main/java/org/thingsboard/server/actors/calculatedField/EntityInitCalculatedFieldMsg.java +++ b/application/src/main/java/org/thingsboard/server/actors/calculatedField/EntityInitCalculatedFieldMsg.java @@ -16,26 +16,29 @@ package org.thingsboard.server.actors.calculatedField; import lombok.Data; -import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.msg.MsgType; import org.thingsboard.server.common.msg.ToCalculatedFieldSystemMsg; import org.thingsboard.server.common.msg.queue.TbCallback; -import org.thingsboard.server.gen.transport.TransportProtos.CalculatedFieldTelemetryMsgProto; import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx; -import java.util.List; - @Data public class EntityInitCalculatedFieldMsg implements ToCalculatedFieldSystemMsg { private final TenantId tenantId; private final CalculatedFieldCtx ctx; + private final StateAction stateAction; private final TbCallback callback; - private final boolean forceReinit; @Override public MsgType getMsgType() { return MsgType.CF_ENTITY_INIT_CF_MSG; } + + public enum StateAction { + INIT, + REINIT, + RECREATE, + REPROCESS + } } diff --git a/application/src/main/java/org/thingsboard/server/actors/calculatedField/MultipleTbCallback.java b/application/src/main/java/org/thingsboard/server/actors/calculatedField/MultipleTbCallback.java index d1f4c9092e..493985c97a 100644 --- a/application/src/main/java/org/thingsboard/server/actors/calculatedField/MultipleTbCallback.java +++ b/application/src/main/java/org/thingsboard/server/actors/calculatedField/MultipleTbCallback.java @@ -50,7 +50,7 @@ public class MultipleTbCallback implements TbCallback { @Override public void onFailure(Throwable t) { - log.warn("[{}][{}] onFailure.", id, callback.getId()); + log.warn("[{}][{}] onFailure.", id, callback.getId(), t); callback.onFailure(t); } } diff --git a/application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java index c143214e2b..b24b803950 100644 --- a/application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java +++ b/application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java @@ -115,14 +115,9 @@ import java.util.concurrent.TimeUnit; import java.util.function.Consumer; import java.util.stream.Collectors; - -/** - * @author Andrew Shvayka - */ @Slf4j public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcessor { - static final String SESSION_TIMEOUT_MESSAGE = "session timeout!"; final TenantId tenantId; final DeviceId deviceId; final LinkedHashMapRemoveEldest sessions; @@ -178,7 +173,7 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso private EdgeId findRelatedEdgeId() { List result = systemContext.getRelationService().findByToAndType(tenantId, deviceId, EntityRelation.CONTAINS_TYPE, RelationTypeGroup.EDGE); - if (result != null && result.size() > 0) { + if (result != null && !result.isEmpty()) { EntityRelation relationToEdge = result.get(0); if (relationToEdge.getFrom() != null && relationToEdge.getFrom().getId() != null) { log.trace("[{}][{}] found edge [{}] for device", tenantId, deviceId, relationToEdge.getFrom().getId()); @@ -501,7 +496,7 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso UUID sessionId = getSessionId(sessionInfo); DeviceId deviceId = new DeviceId(new UUID(msg.getDeviceIdMSB(), msg.getDeviceIdLSB())); ListenableFuture registrationFuture = systemContext.getClaimDevicesService() - .registerClaimingInfo(tenantId, deviceId, msg.getSecretKey(), msg.getDurationMs()); + .registerClaimingInfo(tenantId, deviceId, msg.getSecretKey(), msg.getDurationMs()); Futures.addCallback(registrationFuture, new FutureCallback<>() { @Override public void onSuccess(Void result) { @@ -723,7 +718,7 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso toDeviceRpcPendingMap.remove(requestId); status = RpcStatus.FAILED; response = JacksonUtil.newObjectNode().put("error", "There was a Timeout and all retry " + - "attempts have been exhausted. Retry attempts set: " + maxRpcRetries); + "attempts have been exhausted. Retry attempts set: " + maxRpcRetries); } } else { md.setRetries(md.getRetries() + 1); diff --git a/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java b/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java index 6374e4016d..03830c1c4c 100644 --- a/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java +++ b/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java @@ -51,6 +51,7 @@ import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.HasRuleEngineProfile; +import org.thingsboard.server.common.data.HasTenantId; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.alarm.Alarm; @@ -60,6 +61,7 @@ import org.thingsboard.server.common.data.id.AssetId; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.HasId; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.RuleNodeId; import org.thingsboard.server.common.data.id.TenantId; @@ -110,6 +112,7 @@ import org.thingsboard.server.dao.ota.OtaPackageService; import org.thingsboard.server.dao.queue.QueueService; import org.thingsboard.server.dao.queue.QueueStatsService; import org.thingsboard.server.dao.relation.RelationService; +import org.thingsboard.server.dao.resource.TbResourceDataCache; import org.thingsboard.server.dao.resource.ResourceService; import org.thingsboard.server.dao.rule.RuleChainService; import org.thingsboard.server.dao.tenant.TenantService; @@ -770,6 +773,11 @@ public class DefaultTbContext implements TbContext { return mainCtx.getResourceService(); } + @Override + public TbResourceDataCache getTbResourceDataCache() { + return mainCtx.getResourceDataCache(); + } + @Override public OtaPackageService getOtaPackageService() { return mainCtx.getOtaPackageService(); @@ -1054,11 +1062,20 @@ public class DefaultTbContext implements TbContext { @Override public void checkTenantEntity(EntityId entityId) throws TbNodeException { - if (!this.getTenantId().equals(TenantIdLoader.findTenantId(this, entityId))) { + TenantId actualTenantId = TenantIdLoader.findTenantId(this, entityId); + if (!getTenantId().equals(actualTenantId)) { throw new TbNodeException("Entity with id: '" + entityId + "' specified in the configuration doesn't belong to the current tenant.", true); } } + @Override + public & HasTenantId, I extends EntityId> void checkTenantOrSystemEntity(E entity) throws TbNodeException { + TenantId actualTenantId = entity.getTenantId(); + if (!getTenantId().equals(actualTenantId) && !actualTenantId.isSysTenantId()) { + throw new TbNodeException("Entity with id: '" + entity.getId() + "' specified in the configuration doesn't belong to the current or system tenant.", true); + } + } + private static String getFailureMessage(Throwable th) { String failureMessage; if (th != null) { diff --git a/application/src/main/java/org/thingsboard/server/actors/tenant/TenantActor.java b/application/src/main/java/org/thingsboard/server/actors/tenant/TenantActor.java index 84edd064b5..11a8651026 100644 --- a/application/src/main/java/org/thingsboard/server/actors/tenant/TenantActor.java +++ b/application/src/main/java/org/thingsboard/server/actors/tenant/TenantActor.java @@ -50,6 +50,7 @@ import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.ToCalculatedFieldSystemMsg; import org.thingsboard.server.common.msg.aware.DeviceAwareMsg; import org.thingsboard.server.common.msg.aware.RuleChainAwareMsg; +import org.thingsboard.server.common.msg.aware.TenantAwareMsg; import org.thingsboard.server.common.msg.cf.CalculatedFieldCacheInitMsg; import org.thingsboard.server.common.msg.cf.CalculatedFieldEntityLifecycleMsg; import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg; @@ -155,6 +156,9 @@ public class TenantActor extends RuleChainManagerActor { case COMPONENT_LIFE_CYCLE_MSG: onComponentLifecycleMsg((ComponentLifecycleMsg) msg); break; + case CF_ENTITY_ACTION_EVENT_MSG: + forwardToCfActor((TenantAwareMsg) msg, true); + break; case QUEUE_TO_RULE_ENGINE_MSG: onQueueToRuleEngineMsg((QueueToRuleEngineMsg) msg); break; @@ -180,16 +184,14 @@ public class TenantActor extends RuleChainManagerActor { onRuleChainMsg((RuleChainAwareMsg) msg); break; case CF_CACHE_INIT_MSG: - case CF_INIT_PROFILE_ENTITY_MSG: - case CF_INIT_MSG: - case CF_LINK_INIT_MSG: case CF_STATE_RESTORE_MSG: case CF_PARTITIONS_CHANGE_MSG: - onToCalculatedFieldSystemActorMsg((ToCalculatedFieldSystemMsg) msg, true); + case CF_STATE_PARTITION_RESTORE_MSG: + forwardToCfActor((ToCalculatedFieldSystemMsg) msg, true); break; case CF_TELEMETRY_MSG: case CF_LINKED_TELEMETRY_MSG: - onToCalculatedFieldSystemActorMsg((ToCalculatedFieldSystemMsg) msg, false); + forwardToCfActor((ToCalculatedFieldSystemMsg) msg, false); break; default: return false; @@ -197,7 +199,7 @@ public class TenantActor extends RuleChainManagerActor { return true; } - private void onToCalculatedFieldSystemActorMsg(ToCalculatedFieldSystemMsg msg, boolean priority) { + private void forwardToCfActor(TenantAwareMsg msg, boolean priority) { if (cfActor == null) { if (msg instanceof CalculatedFieldStateRestoreMsg) { log.warn("[{}] CF Actor is not initialized. ToCalculatedFieldSystemMsg: [{}]", tenantId, msg); @@ -348,7 +350,7 @@ public class TenantActor extends RuleChainManagerActor { } } if (cfActor != null) { - if (msg.getEntityId().getEntityType().isOneOf(EntityType.CALCULATED_FIELD, EntityType.DEVICE, EntityType.ASSET)) { + if (msg.getEntityId().getEntityType().isOneOf(EntityType.CALCULATED_FIELD, EntityType.DEVICE, EntityType.ASSET, EntityType.CUSTOMER, EntityType.TENANT_PROFILE)) { cfActor.tellWithHighPriority(new CalculatedFieldEntityLifecycleMsg(tenantId, msg)); } } @@ -393,6 +395,7 @@ public class TenantActor extends RuleChainManagerActor { public TbActor createActor() { return new TenantActor(context, tenantId); } + } } diff --git a/application/src/main/java/org/thingsboard/server/config/SwaggerConfiguration.java b/application/src/main/java/org/thingsboard/server/config/SwaggerConfiguration.java index 067a1e98c6..410b05456e 100644 --- a/application/src/main/java/org/thingsboard/server/config/SwaggerConfiguration.java +++ b/application/src/main/java/org/thingsboard/server/config/SwaggerConfiguration.java @@ -116,6 +116,8 @@ public class SwaggerConfiguration { private String appVersion; @Value("${swagger.group_name:thingsboard}") private String groupName; + @Value("${swagger.doc_expansion:list}") + private String docExpansion; @Bean public OpenAPI thingsboardApi() { @@ -172,7 +174,7 @@ public class SwaggerConfiguration { uiProperties.setDefaultModelExpandDepth(1); uiProperties.setDefaultModelRendering("example"); uiProperties.setDisplayRequestDuration(false); - uiProperties.setDocExpansion("list"); + uiProperties.setDocExpansion(docExpansion); uiProperties.setFilter("false"); uiProperties.setMaxDisplayedTags(null); uiProperties.setOperationsSorter("alpha"); diff --git a/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java b/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java index 2fbc89a84d..ca741ea8c6 100644 --- a/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java +++ b/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java @@ -31,6 +31,7 @@ import org.springframework.security.config.annotation.method.configuration.Enabl import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer; import org.springframework.security.config.annotation.web.configurers.RequestCacheConfigurer; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestResolver; @@ -38,6 +39,7 @@ import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.AuthenticationFailureHandler; import org.springframework.security.web.authentication.AuthenticationSuccessHandler; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.security.web.header.writers.CrossOriginOpenerPolicyHeaderWriter.CrossOriginOpenerPolicy; import org.springframework.security.web.header.writers.StaticHeadersWriter; import org.springframework.web.cors.UrlBasedCorsConfigurationSource; import org.springframework.web.filter.CorsFilter; @@ -210,9 +212,8 @@ public class ThingsboardSecurityConfiguration { @Bean SecurityFilterChain filterChain(HttpSecurity http) throws Exception { - http.headers(headers -> headers - .cacheControl(config -> {}) - .frameOptions(config -> {}).disable()) + http.headers(headers -> headers.defaultsDisabled() + .crossOriginOpenerPolicy(coop -> coop.policy(CrossOriginOpenerPolicy.SAME_ORIGIN))) .cors(cors -> {}) .csrf(AbstractHttpConfigurer::disable) .exceptionHandling(config -> {}) diff --git a/application/src/main/java/org/thingsboard/server/controller/AssetController.java b/application/src/main/java/org/thingsboard/server/controller/AssetController.java index 2a4cce143a..d4223d30f1 100644 --- a/application/src/main/java/org/thingsboard/server/controller/AssetController.java +++ b/application/src/main/java/org/thingsboard/server/controller/AssetController.java @@ -34,6 +34,9 @@ import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestController; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.EntitySubtype; +import org.thingsboard.server.common.data.NameConflictPolicy; +import org.thingsboard.server.common.data.NameConflictStrategy; +import org.thingsboard.server.common.data.UniquifyStrategy; import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.asset.AssetInfo; import org.thingsboard.server.common.data.asset.AssetSearchQuery; @@ -76,6 +79,8 @@ import static org.thingsboard.server.controller.ControllerConstants.EDGE_ASSIGN_ import static org.thingsboard.server.controller.ControllerConstants.EDGE_ID_PARAM_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.EDGE_UNASSIGN_ASYNC_FIRST_STEP_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.EDGE_UNASSIGN_RECEIVE_STEP_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.NAME_CONFLICT_POLICY_DESC; +import static org.thingsboard.server.controller.ControllerConstants.UNIQUIFY_SEPARATOR_DESC; import static org.thingsboard.server.controller.ControllerConstants.PAGE_DATA_PARAMETERS; import static org.thingsboard.server.controller.ControllerConstants.PAGE_NUMBER_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.PAGE_SIZE_DESCRIPTION; @@ -83,6 +88,7 @@ import static org.thingsboard.server.controller.ControllerConstants.SORT_ORDER_D import static org.thingsboard.server.controller.ControllerConstants.SORT_PROPERTY_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.TENANT_AUTHORITY_PARAGRAPH; import static org.thingsboard.server.controller.ControllerConstants.TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH; +import static org.thingsboard.server.controller.ControllerConstants.UNIQUIFY_STRATEGY_DESC; import static org.thingsboard.server.controller.ControllerConstants.UUID_WIKI_LINK; import static org.thingsboard.server.controller.EdgeController.EDGE_ID; @@ -137,10 +143,16 @@ public class AssetController extends BaseController { @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/asset", method = RequestMethod.POST) @ResponseBody - public Asset saveAsset(@io.swagger.v3.oas.annotations.parameters.RequestBody(description = "A JSON value representing the asset.") @RequestBody Asset asset) throws Exception { + public Asset saveAsset(@io.swagger.v3.oas.annotations.parameters.RequestBody(description = "A JSON value representing the asset.") @RequestBody Asset asset, + @Parameter(description = NAME_CONFLICT_POLICY_DESC) + @RequestParam(name = "nameConflictPolicy", defaultValue = "FAIL") NameConflictPolicy nameConflictPolicy, + @Parameter(description = UNIQUIFY_SEPARATOR_DESC) + @RequestParam(name = "uniquifySeparator", defaultValue = "_") String uniquifySeparator, + @Parameter(description = UNIQUIFY_STRATEGY_DESC) + @RequestParam(name = "uniquifyStrategy", defaultValue = "RANDOM") UniquifyStrategy uniquifyStrategy) throws Exception { asset.setTenantId(getTenantId()); checkEntity(asset.getId(), asset, Resource.ASSET); - return tbAssetService.save(asset, getCurrentUser()); + return tbAssetService.save(asset, new NameConflictStrategy(nameConflictPolicy, uniquifySeparator, uniquifyStrategy), getCurrentUser()); } @ApiOperation(value = "Delete asset (deleteAsset)", diff --git a/application/src/main/java/org/thingsboard/server/controller/AuthController.java b/application/src/main/java/org/thingsboard/server/controller/AuthController.java index 0e21444431..b15b650c33 100644 --- a/application/src/main/java/org/thingsboard/server/controller/AuthController.java +++ b/application/src/main/java/org/thingsboard/server/controller/AuthController.java @@ -39,6 +39,7 @@ import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.limit.LimitedApi; +import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.common.data.security.UserCredentials; import org.thingsboard.server.common.data.security.event.UserCredentialsInvalidationEvent; import org.thingsboard.server.common.data.security.event.UserSessionInvalidationEvent; @@ -48,7 +49,9 @@ import org.thingsboard.server.common.data.security.model.UserPasswordPolicy; import org.thingsboard.server.config.annotations.ApiOperation; import org.thingsboard.server.dao.settings.SecuritySettingsService; import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.security.auth.mfa.TwoFactorAuthService; import org.thingsboard.server.service.security.auth.rest.RestAuthenticationDetails; +import org.thingsboard.server.service.security.auth.rest.RestAwareAuthenticationSuccessHandler; import org.thingsboard.server.service.security.model.ActivateUserRequest; import org.thingsboard.server.service.security.model.ChangePasswordRequest; import org.thingsboard.server.service.security.model.ResetPasswordEmailRequest; @@ -74,7 +77,8 @@ public class AuthController extends BaseController { private final SecuritySettingsService securitySettingsService; private final RateLimitService rateLimitService; private final ApplicationEventPublisher eventPublisher; - + private final TwoFactorAuthService twoFactorAuthService; + private final RestAwareAuthenticationSuccessHandler authenticationSuccessHandler; @ApiOperation(value = "Get current User (getUser)", notes = "Get the information about the User which credentials are used to perform this REST API call.") @@ -221,7 +225,13 @@ public class AuthController extends BaseController { } } - var tokenPair = tokenFactory.createTokenPair(securityUser); + JwtPair tokenPair; + if (twoFactorAuthService.isEnforceTwoFaEnabled(securityUser.getTenantId(), user)) { + tokenPair = authenticationSuccessHandler.createMfaTokenPair(securityUser, Authority.MFA_CONFIGURATION_TOKEN); + } else { + tokenPair = tokenFactory.createTokenPair(securityUser); + } + systemSecurityService.logLoginAction(user, new RestAuthenticationDetails(request), ActionType.LOGIN, null); return tokenPair; } diff --git a/application/src/main/java/org/thingsboard/server/controller/CalculatedFieldController.java b/application/src/main/java/org/thingsboard/server/controller/CalculatedFieldController.java index 5945355ef8..9a24ab91fc 100644 --- a/application/src/main/java/org/thingsboard/server/controller/CalculatedFieldController.java +++ b/application/src/main/java/org/thingsboard/server/controller/CalculatedFieldController.java @@ -44,6 +44,7 @@ import org.thingsboard.script.api.tbel.TbelInvokeService; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EventInfo; import org.thingsboard.server.common.data.cf.CalculatedField; +import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.cf.configuration.CalculatedFieldConfiguration; import org.thingsboard.server.common.data.event.EventType; import org.thingsboard.server.common.data.exception.ThingsboardException; @@ -62,10 +63,10 @@ import org.thingsboard.server.service.security.permission.Operation; import java.util.ArrayList; import java.util.Collections; -import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; +import java.util.Set; import java.util.concurrent.TimeUnit; import static org.thingsboard.server.controller.ControllerConstants.CF_TEXT_SEARCH_DESCRIPTION; @@ -159,19 +160,27 @@ public class CalculatedFieldController extends BaseController { ) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") @GetMapping(value = "/{entityType}/{entityId}/calculatedFields", params = {"pageSize", "page"}) - public PageData getCalculatedFieldsByEntityId( - @Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, schema = @Schema(defaultValue = "DEVICE")) @PathVariable("entityType") String entityType, - @Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable("entityId") String entityIdStr, - @Parameter(description = PAGE_SIZE_DESCRIPTION, required = true) @RequestParam int pageSize, - @Parameter(description = PAGE_NUMBER_DESCRIPTION, required = true) @RequestParam int page, - @Parameter(description = CF_TEXT_SEARCH_DESCRIPTION) @RequestParam(required = false) String textSearch, - @Parameter(description = SORT_PROPERTY_DESCRIPTION, schema = @Schema(allowableValues = {"createdTime", "name"})) @RequestParam(required = false) String sortProperty, - @Parameter(description = SORT_ORDER_DESCRIPTION, schema = @Schema(allowableValues = {"ASC", "DESC"})) @RequestParam(required = false) String sortOrder) throws ThingsboardException { + public PageData getCalculatedFieldsByEntityId(@Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, schema = @Schema(defaultValue = "DEVICE")) + @PathVariable("entityType") String entityType, + @Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) + @PathVariable("entityId") String entityIdStr, + @Parameter(description = PAGE_SIZE_DESCRIPTION, required = true) + @RequestParam int pageSize, + @Parameter(description = PAGE_NUMBER_DESCRIPTION, required = true) + @RequestParam int page, + @Parameter(description = "Calculated field type. If not specified, all types will be returned.") + @RequestParam(required = false) CalculatedFieldType type, + @Parameter(description = CF_TEXT_SEARCH_DESCRIPTION) + @RequestParam(required = false) String textSearch, + @Parameter(description = SORT_PROPERTY_DESCRIPTION, schema = @Schema(allowableValues = {"createdTime", "name"})) + @RequestParam(required = false) String sortProperty, + @Parameter(description = SORT_ORDER_DESCRIPTION, schema = @Schema(allowableValues = {"ASC", "DESC"})) + @RequestParam(required = false) String sortOrder) throws ThingsboardException { PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); checkParameter("entityId", entityIdStr); EntityId entityId = EntityIdFactory.getByTypeAndUuid(entityType, entityIdStr); checkEntityId(entityId, Operation.READ_CALCULATED_FIELD); - return checkNotNull(tbCalculatedFieldService.findAllByTenantIdAndEntityId(entityId, getCurrentUser(), pageLink)); + return checkNotNull(tbCalculatedFieldService.findByTenantIdAndEntityId(getTenantId(), entityId, type, pageLink)); } @ApiOperation(value = "Delete Calculated Field (deleteCalculatedField)", @@ -278,7 +287,7 @@ public class CalculatedFieldController extends BaseController { } private void checkReferencedEntities(CalculatedFieldConfiguration calculatedFieldConfig) throws ThingsboardException { - List referencedEntityIds = calculatedFieldConfig.getReferencedEntities(); + Set referencedEntityIds = calculatedFieldConfig.getReferencedEntities(); for (EntityId referencedEntityId : referencedEntityIds) { EntityType entityType = referencedEntityId.getEntityType(); switch (entityType) { @@ -289,7 +298,6 @@ public class CalculatedFieldController extends BaseController { default -> throw new IllegalArgumentException("Calculated fields do not support '" + entityType + "' for referenced entities."); } } - } } diff --git a/application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java b/application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java index a87864726b..7dfa616ab5 100644 --- a/application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java +++ b/application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java @@ -1630,11 +1630,13 @@ public class ControllerConstants { protected static final String ENTITY_VIEW_INFO_DESCRIPTION = "Entity Views Info extends the Entity View with customer title and 'is public' flag. " + ENTITY_VIEW_DESCRIPTION; protected static final String ATTRIBUTES_SCOPE_DESCRIPTION = "A string value representing the attributes scope. For example, 'SERVER_SCOPE'."; - protected static final String ATTRIBUTES_KEYS_DESCRIPTION = "A string value representing the comma-separated list of attributes keys. For example, 'active,inactivityAlarmTime'."; + protected static final String ATTRIBUTES_KEYS_DESCRIPTION = "A string value representing the comma-separated list of attributes keys. For example, 'active,inactivityAlarmTime'. " + + "If attribute keys contain comma, duplicate 'key' parameter for each key, for example '?key=my,key&key=my,second,key"; protected static final String ATTRIBUTES_JSON_REQUEST_DESCRIPTION = "A string value representing the json object. For example, '{\"key\":\"value\"}'. See API call description for more details."; protected static final String TELEMETRY_KEYS_BASE_DESCRIPTION = "A string value representing the comma-separated list of telemetry keys."; - protected static final String TELEMETRY_KEYS_DESCRIPTION = TELEMETRY_KEYS_BASE_DESCRIPTION + " If keys are not selected, the result will return all latest time series. For example, 'temperature,humidity'."; + protected static final String TELEMETRY_KEYS_DESCRIPTION = TELEMETRY_KEYS_BASE_DESCRIPTION + " If keys are not selected, the result will return all latest time series. For example, 'temperature,humidity'. " + + "If telemetry keys contain comma, duplicate 'key' parameter for each key, for example '?key=my,key&key=my,second,key"; protected static final String TELEMETRY_SCOPE_DESCRIPTION = "Value is deprecated, reserved for backward compatibility and not used in the API call implementation. Specify any scope for compatibility"; protected static final String TELEMETRY_JSON_REQUEST_DESCRIPTION = "A JSON with the telemetry values. See API call description for more details."; @@ -1744,4 +1746,18 @@ public class ControllerConstants { MARKDOWN_CODE_BLOCK_END ; protected static final String SECURITY_WRITE_CHECK = " Security check is performed to verify that the user has 'WRITE' permission for the entity (entities)."; + + public static final String NAME_CONFLICT_POLICY_DESC = "Optional value of name conflict policy. Possible values: FAIL or UNIQUIFY. " + + " If omitted, FAIL policy is applied. FAIL policy implies exception will be thrown if an entity with the same name already exists. " + + " UNIQUIFY policy appends a suffix to the entity name, if a name conflict occurs."; + + public static final String UNIQUIFY_SEPARATOR_DESC = "Optional value of name suffix separator used by UNIQUIFY policy. By default, underscore separator is used. " + + "For example, strategy is UNIQUIFY, separator is '-'; if a name conflict occurs for entity name 'test-name', " + + "created entity will have name like 'test-name-7fsh4f'."; + + public static final String UNIQUIFY_STRATEGY_DESC = "Optional value of uniquify strategy used by UNIQUIFY policy. Possible values: RANDOM or INCREMENTAL. " + + "By default, RANDOM strategy is used, which means random alphanumeric string will be added as a suffix to entity name. " + + "INCREMENTAL implies the first possible number starting from 1 will be added as a name suffix. " + + "For example, strategy is UNIQUIFY, uniquify strategy is INCREMENTAL; if a name conflict occurs for entity name 'test-name', " + + "created entity will have name like 'test-name-1."; } diff --git a/application/src/main/java/org/thingsboard/server/controller/CustomerController.java b/application/src/main/java/org/thingsboard/server/controller/CustomerController.java index 03ac9c60fa..fca7ce3e86 100644 --- a/application/src/main/java/org/thingsboard/server/controller/CustomerController.java +++ b/application/src/main/java/org/thingsboard/server/controller/CustomerController.java @@ -32,6 +32,9 @@ import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestController; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.Customer; +import org.thingsboard.server.common.data.NameConflictStrategy; +import org.thingsboard.server.common.data.NameConflictPolicy; +import org.thingsboard.server.common.data.UniquifyStrategy; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.TenantId; @@ -47,6 +50,8 @@ import static org.thingsboard.server.controller.ControllerConstants.CUSTOMER_ID; import static org.thingsboard.server.controller.ControllerConstants.CUSTOMER_ID_PARAM_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.CUSTOMER_TEXT_SEARCH_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.HOME_DASHBOARD; +import static org.thingsboard.server.controller.ControllerConstants.NAME_CONFLICT_POLICY_DESC; +import static org.thingsboard.server.controller.ControllerConstants.UNIQUIFY_SEPARATOR_DESC; import static org.thingsboard.server.controller.ControllerConstants.PAGE_DATA_PARAMETERS; import static org.thingsboard.server.controller.ControllerConstants.PAGE_NUMBER_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.PAGE_SIZE_DESCRIPTION; @@ -54,6 +59,7 @@ import static org.thingsboard.server.controller.ControllerConstants.SORT_ORDER_D import static org.thingsboard.server.controller.ControllerConstants.SORT_PROPERTY_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.TENANT_AUTHORITY_PARAGRAPH; import static org.thingsboard.server.controller.ControllerConstants.TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH; +import static org.thingsboard.server.controller.ControllerConstants.UNIQUIFY_STRATEGY_DESC; import static org.thingsboard.server.controller.ControllerConstants.UUID_WIKI_LINK; @RestController @@ -128,10 +134,16 @@ public class CustomerController extends BaseController { @PreAuthorize("hasAuthority('TENANT_ADMIN')") @RequestMapping(value = "/customer", method = RequestMethod.POST) @ResponseBody - public Customer saveCustomer(@io.swagger.v3.oas.annotations.parameters.RequestBody(description = "A JSON value representing the customer.") @RequestBody Customer customer) throws Exception { + public Customer saveCustomer(@io.swagger.v3.oas.annotations.parameters.RequestBody(description = "A JSON value representing the customer.") @RequestBody Customer customer, + @Parameter(description = NAME_CONFLICT_POLICY_DESC) + @RequestParam(name = "nameConflictPolicy", defaultValue = "FAIL") NameConflictPolicy nameConflictPolicy, + @Parameter(description = UNIQUIFY_SEPARATOR_DESC) + @RequestParam(name = "uniquifySeparator", defaultValue = "_") String uniquifySeparator, + @Parameter(description = UNIQUIFY_STRATEGY_DESC) + @RequestParam(name = "uniquifyStrategy", defaultValue = "RANDOM") UniquifyStrategy uniquifyStrategy) throws Exception { customer.setTenantId(getTenantId()); checkEntity(customer.getId(), customer, Resource.CUSTOMER); - return tbCustomerService.save(customer, getCurrentUser()); + return tbCustomerService.save(customer, new NameConflictStrategy(nameConflictPolicy, uniquifySeparator, uniquifyStrategy), getCurrentUser()); } @ApiOperation(value = "Delete Customer (deleteCustomer)", diff --git a/application/src/main/java/org/thingsboard/server/controller/DeviceController.java b/application/src/main/java/org/thingsboard/server/controller/DeviceController.java index 61864dbf63..8bbf4ae2f6 100644 --- a/application/src/main/java/org/thingsboard/server/controller/DeviceController.java +++ b/application/src/main/java/org/thingsboard/server/controller/DeviceController.java @@ -46,8 +46,11 @@ import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.DeviceInfo; import org.thingsboard.server.common.data.DeviceInfoFilter; import org.thingsboard.server.common.data.EntitySubtype; +import org.thingsboard.server.common.data.NameConflictPolicy; +import org.thingsboard.server.common.data.NameConflictStrategy; import org.thingsboard.server.common.data.SaveDeviceWithCredentialsRequest; import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.common.data.UniquifyStrategy; import org.thingsboard.server.common.data.device.DeviceSearchQuery; import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; @@ -108,6 +111,8 @@ import static org.thingsboard.server.controller.ControllerConstants.EDGE_ASSIGN_ import static org.thingsboard.server.controller.ControllerConstants.EDGE_ID_PARAM_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.EDGE_UNASSIGN_ASYNC_FIRST_STEP_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.EDGE_UNASSIGN_RECEIVE_STEP_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.NAME_CONFLICT_POLICY_DESC; +import static org.thingsboard.server.controller.ControllerConstants.UNIQUIFY_SEPARATOR_DESC; import static org.thingsboard.server.controller.ControllerConstants.PAGE_DATA_PARAMETERS; import static org.thingsboard.server.controller.ControllerConstants.PAGE_NUMBER_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.PAGE_SIZE_DESCRIPTION; @@ -117,6 +122,7 @@ import static org.thingsboard.server.controller.ControllerConstants.TENANT_AUTHO import static org.thingsboard.server.controller.ControllerConstants.TENANT_ID; import static org.thingsboard.server.controller.ControllerConstants.TENANT_ID_PARAM_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH; +import static org.thingsboard.server.controller.ControllerConstants.UNIQUIFY_STRATEGY_DESC; import static org.thingsboard.server.controller.ControllerConstants.UUID_WIKI_LINK; import static org.thingsboard.server.controller.EdgeController.EDGE_ID; @@ -177,14 +183,21 @@ public class DeviceController extends BaseController { @ResponseBody public Device saveDevice(@io.swagger.v3.oas.annotations.parameters.RequestBody(description = "A JSON value representing the device.") @RequestBody Device device, @Parameter(description = "Optional value of the device credentials to be used during device creation. " + - "If omitted, access token will be auto-generated.") @RequestParam(name = "accessToken", required = false) String accessToken) throws Exception { + "If omitted, access token will be auto-generated.") + @RequestParam(name = "accessToken", required = false) String accessToken, + @Parameter(description = NAME_CONFLICT_POLICY_DESC) + @RequestParam(name = "nameConflictPolicy", defaultValue = "FAIL") NameConflictPolicy nameConflictPolicy, + @Parameter(description = UNIQUIFY_SEPARATOR_DESC) + @RequestParam(name = "uniquifySeparator", defaultValue = "_") String uniquifySeparator, + @Parameter(description = UNIQUIFY_STRATEGY_DESC) + @RequestParam(name = "uniquifyStrategy", defaultValue = "RANDOM") UniquifyStrategy uniquifyStrategy) throws Exception { device.setTenantId(getCurrentUser().getTenantId()); if (device.getId() != null) { checkDeviceId(device.getId(), Operation.WRITE); } else { checkEntity(null, device, Resource.DEVICE); } - return tbDeviceService.save(device, accessToken, getCurrentUser()); + return tbDeviceService.save(device, accessToken, new NameConflictStrategy(nameConflictPolicy, uniquifySeparator, uniquifyStrategy), getCurrentUser()); } @ApiOperation(value = "Create Device (saveDevice) with credentials ", @@ -209,12 +222,18 @@ public class DeviceController extends BaseController { @RequestMapping(value = "/device-with-credentials", method = RequestMethod.POST) @ResponseBody public Device saveDeviceWithCredentials(@Parameter(description = "The JSON object with device and credentials. See method description above for example.") - @Valid @RequestBody SaveDeviceWithCredentialsRequest deviceAndCredentials) throws ThingsboardException { + @Valid @RequestBody SaveDeviceWithCredentialsRequest deviceAndCredentials, + @Parameter(description = NAME_CONFLICT_POLICY_DESC) + @RequestParam(name = "nameConflictPolicy", defaultValue = "FAIL") NameConflictPolicy nameConflictPolicy, + @Parameter(description = UNIQUIFY_SEPARATOR_DESC) + @RequestParam(name = "uniquifySeparator", defaultValue = "_") String uniquifySeparator, + @Parameter(description = UNIQUIFY_STRATEGY_DESC) + @RequestParam(name = "uniquifyStrategy", defaultValue = "RANDOM") UniquifyStrategy uniquifyStrategy) throws ThingsboardException { Device device = deviceAndCredentials.getDevice(); DeviceCredentials credentials = deviceAndCredentials.getCredentials(); device.setTenantId(getCurrentUser().getTenantId()); checkEntity(device.getId(), device, Resource.DEVICE); - return tbDeviceService.saveDeviceWithCredentials(device, credentials, getCurrentUser()); + return tbDeviceService.saveDeviceWithCredentials(device, credentials, new NameConflictStrategy(nameConflictPolicy, uniquifySeparator, uniquifyStrategy), getCurrentUser()); } @ApiOperation(value = "Delete device (deleteDevice)", diff --git a/application/src/main/java/org/thingsboard/server/controller/EntityViewController.java b/application/src/main/java/org/thingsboard/server/controller/EntityViewController.java index b1b6b6b1e3..67fc02ab29 100644 --- a/application/src/main/java/org/thingsboard/server/controller/EntityViewController.java +++ b/application/src/main/java/org/thingsboard/server/controller/EntityViewController.java @@ -34,6 +34,9 @@ import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.EntitySubtype; import org.thingsboard.server.common.data.EntityView; import org.thingsboard.server.common.data.EntityViewInfo; +import org.thingsboard.server.common.data.NameConflictPolicy; +import org.thingsboard.server.common.data.NameConflictStrategy; +import org.thingsboard.server.common.data.UniquifyStrategy; import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.entityview.EntityViewSearchQuery; import org.thingsboard.server.common.data.exception.ThingsboardException; @@ -69,6 +72,8 @@ import static org.thingsboard.server.controller.ControllerConstants.ENTITY_VIEW_ import static org.thingsboard.server.controller.ControllerConstants.ENTITY_VIEW_TEXT_SEARCH_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.ENTITY_VIEW_TYPE; import static org.thingsboard.server.controller.ControllerConstants.MODEL_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.NAME_CONFLICT_POLICY_DESC; +import static org.thingsboard.server.controller.ControllerConstants.UNIQUIFY_SEPARATOR_DESC; import static org.thingsboard.server.controller.ControllerConstants.PAGE_DATA_PARAMETERS; import static org.thingsboard.server.controller.ControllerConstants.PAGE_NUMBER_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.PAGE_SIZE_DESCRIPTION; @@ -76,6 +81,7 @@ import static org.thingsboard.server.controller.ControllerConstants.SORT_ORDER_D import static org.thingsboard.server.controller.ControllerConstants.SORT_PROPERTY_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.TENANT_AUTHORITY_PARAGRAPH; import static org.thingsboard.server.controller.ControllerConstants.TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH; +import static org.thingsboard.server.controller.ControllerConstants.UNIQUIFY_STRATEGY_DESC; import static org.thingsboard.server.controller.EdgeController.EDGE_ID; /** @@ -128,7 +134,13 @@ public class EntityViewController extends BaseController { @ResponseBody public EntityView saveEntityView( @Parameter(description = "A JSON object representing the entity view.") - @RequestBody EntityView entityView) throws Exception { + @RequestBody EntityView entityView, + @Parameter(description = NAME_CONFLICT_POLICY_DESC) + @RequestParam(name = "nameConflictPolicy", defaultValue = "FAIL") NameConflictPolicy nameConflictPolicy, + @Parameter(description = UNIQUIFY_SEPARATOR_DESC) + @RequestParam(name = "uniquifySeparator", defaultValue = "_") String uniquifySeparator, + @Parameter(description = UNIQUIFY_STRATEGY_DESC) + @RequestParam(name = "uniquifyStrategy", defaultValue = "RANDOM") UniquifyStrategy uniquifyStrategy) throws Exception { entityView.setTenantId(getCurrentUser().getTenantId()); EntityView existingEntityView = null; if (entityView.getId() == null) { @@ -137,7 +149,7 @@ public class EntityViewController extends BaseController { } else { existingEntityView = checkEntityViewId(entityView.getId(), Operation.WRITE); } - return tbEntityViewService.save(entityView, existingEntityView, getCurrentUser()); + return tbEntityViewService.save(entityView, existingEntityView, new NameConflictStrategy(nameConflictPolicy, uniquifySeparator, uniquifyStrategy), getCurrentUser()); } @ApiOperation(value = "Delete entity view (deleteEntityView)", diff --git a/application/src/main/java/org/thingsboard/server/controller/ImageController.java b/application/src/main/java/org/thingsboard/server/controller/ImageController.java index f9ec7fd844..9288484f86 100644 --- a/application/src/main/java/org/thingsboard/server/controller/ImageController.java +++ b/application/src/main/java/org/thingsboard/server/controller/ImageController.java @@ -300,6 +300,7 @@ public class ImageController extends BaseController { tbImageService.putETag(cacheKey, descriptor.getEtag()); var result = ResponseEntity.ok() .header("Content-Type", descriptor.getMediaType()) + .header("Content-Security-Policy", "default-src 'none'") .eTag(descriptor.getEtag()); if (!cacheKey.isPublic()) { result diff --git a/application/src/main/java/org/thingsboard/server/controller/Lwm2mController.java b/application/src/main/java/org/thingsboard/server/controller/Lwm2mController.java index f0ec727896..7e7cd84a12 100644 --- a/application/src/main/java/org/thingsboard/server/controller/Lwm2mController.java +++ b/application/src/main/java/org/thingsboard/server/controller/Lwm2mController.java @@ -27,6 +27,8 @@ import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.RestController; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.NameConflictPolicy; +import org.thingsboard.server.common.data.NameConflictStrategy; import org.thingsboard.server.common.data.SaveDeviceWithCredentialsRequest; import org.thingsboard.server.common.data.device.profile.lwm2m.bootstrap.LwM2MServerSecurityConfigDefault; import org.thingsboard.server.common.data.exception.ThingsboardException; @@ -37,6 +39,7 @@ import org.thingsboard.server.service.lwm2m.LwM2MService; import java.util.Map; +import static org.thingsboard.server.common.data.NameConflictStrategy.DEFAULT; import static org.thingsboard.server.controller.ControllerConstants.IS_BOOTSTRAP_SERVER_PARAM_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH; @@ -73,6 +76,6 @@ public class Lwm2mController extends BaseController { public Device saveDeviceWithCredentials(@RequestBody Map, Object> deviceWithDeviceCredentials) throws ThingsboardException { Device device = checkNotNull(JacksonUtil.convertValue(deviceWithDeviceCredentials.get(Device.class), Device.class)); DeviceCredentials credentials = checkNotNull(JacksonUtil.convertValue(deviceWithDeviceCredentials.get(DeviceCredentials.class), DeviceCredentials.class)); - return deviceController.saveDeviceWithCredentials(new SaveDeviceWithCredentialsRequest(device, credentials)); + return deviceController.saveDeviceWithCredentials(new SaveDeviceWithCredentialsRequest(device, credentials), DEFAULT.policy(), DEFAULT.separator(), DEFAULT.uniquifyStrategy()); } } diff --git a/application/src/main/java/org/thingsboard/server/controller/RuleEngineController.java b/application/src/main/java/org/thingsboard/server/controller/RuleEngineController.java index befd0baec5..6a66887989 100644 --- a/application/src/main/java/org/thingsboard/server/controller/RuleEngineController.java +++ b/application/src/main/java/org/thingsboard/server/controller/RuleEngineController.java @@ -20,6 +20,7 @@ import io.swagger.v3.oas.annotations.Parameter; import jakarta.annotation.Nullable; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.security.access.prepost.PreAuthorize; @@ -60,7 +61,10 @@ import static org.thingsboard.server.controller.ControllerConstants.ENTITY_TYPE_ @RequestMapping(TbUrlConstants.RULE_ENGINE_URL_PREFIX) @Slf4j public class RuleEngineController extends BaseController { - public static final int DEFAULT_TIMEOUT = 10000; + + @Value("${server.rest.rule_engine.response_timeout:10000}") + public int defaultResponseTimeout; + private static final String MSG_DESCRIPTION_PREFIX = "Creates the Message with type 'REST_API_REQUEST' and payload taken from the request body. "; private static final String MSG_DESCRIPTION = "This method allows you to extend the regular platform API with the power of Rule Engine. You may use default and custom rule nodes to handle the message. " + "The generated message contains two important metadata fields:\n\n" + @@ -85,7 +89,7 @@ public class RuleEngineController extends BaseController { public DeferredResult handleRuleEngineRequest( @Parameter(description = "A JSON value representing the message.", required = true) @RequestBody String requestBody) throws ThingsboardException { - return handleRuleEngineRequest(null, null, null, DEFAULT_TIMEOUT, requestBody); + return handleRuleEngineRequest(null, null, null, defaultResponseTimeout, requestBody); } @ApiOperation(value = "Push entity message to the rule engine (handleRuleEngineRequest)", @@ -104,7 +108,7 @@ public class RuleEngineController extends BaseController { @PathVariable("entityId") String entityIdStr, @Parameter(description = "A JSON value representing the message.", required = true) @RequestBody String requestBody) throws ThingsboardException { - return handleRuleEngineRequest(entityType, entityIdStr, null, DEFAULT_TIMEOUT, requestBody); + return handleRuleEngineRequest(entityType, entityIdStr, null, defaultResponseTimeout, requestBody); } @ApiOperation(value = "Push entity message with timeout to the rule engine (handleRuleEngineRequest)", diff --git a/application/src/main/java/org/thingsboard/server/controller/SystemInfoController.java b/application/src/main/java/org/thingsboard/server/controller/SystemInfoController.java index 29f4daa783..9c04cb92bd 100644 --- a/application/src/main/java/org/thingsboard/server/controller/SystemInfoController.java +++ b/application/src/main/java/org/thingsboard/server/controller/SystemInfoController.java @@ -162,6 +162,10 @@ public class SystemInfoController extends BaseController { } systemParams.setMaxArgumentsPerCF(tenantProfileConfiguration.getMaxArgumentsPerCF()); systemParams.setMaxDataPointsPerRollingArg(tenantProfileConfiguration.getMaxDataPointsPerRollingArg()); + systemParams.setMinAllowedScheduledUpdateIntervalInSecForCF(tenantProfileConfiguration.getMinAllowedScheduledUpdateIntervalInSecForCF()); + systemParams.setMaxRelationLevelPerCfArgument(tenantProfileConfiguration.getMaxRelationLevelPerCfArgument()); + systemParams.setMinAllowedDeduplicationIntervalInSecForCF(tenantProfileConfiguration.getMinAllowedDeduplicationIntervalInSecForCF()); + systemParams.setMinAllowedAggregationIntervalInSecForCF(tenantProfileConfiguration.getMinAllowedAggregationIntervalInSecForCF()); systemParams.setTrendzSettings(trendzSettingsService.findTrendzSettings(currentUser.getTenantId())); } systemParams.setMobileQrEnabled(Optional.ofNullable(qrCodeSettingService.findQrCodeSettings(TenantId.SYS_TENANT_ID)) diff --git a/application/src/main/java/org/thingsboard/server/controller/TbResourceController.java b/application/src/main/java/org/thingsboard/server/controller/TbResourceController.java index b23603f6a2..54d2494679 100644 --- a/application/src/main/java/org/thingsboard/server/controller/TbResourceController.java +++ b/application/src/main/java/org/thingsboard/server/controller/TbResourceController.java @@ -55,14 +55,17 @@ import org.thingsboard.server.common.data.util.ThrowingSupplier; import org.thingsboard.server.config.annotations.ApiOperation; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.resource.TbResourceService; +import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.permission.Operation; import org.thingsboard.server.service.security.permission.Resource; +import java.util.ArrayList; import java.util.Collections; import java.util.EnumSet; import java.util.HashSet; import java.util.List; import java.util.Set; +import java.util.UUID; import static org.thingsboard.server.controller.ControllerConstants.AVAILABLE_FOR_ANY_AUTHORIZED_USER; import static org.thingsboard.server.controller.ControllerConstants.LWM2M_OBJECT_DESCRIPTION; @@ -263,6 +266,20 @@ public class TbResourceController extends BaseController { } } + @ApiOperation(value = "Get Resource Infos by ids (getSystemOrTenantResourcesByIds)") + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") + @GetMapping(value = "/resource", params = {"resourceIds"}) + public List getSystemOrTenantResourcesByIds( + @Parameter(description = "A list of resource ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string"))) + @RequestParam("resourceIds") Set resourceUuids) throws ThingsboardException { + SecurityUser user = getCurrentUser(); + List resourceIds = new ArrayList<>(); + for (UUID resourceId : resourceUuids) { + resourceIds.add(new TbResourceId(resourceId)); + } + return resourceService.findSystemOrTenantResourcesByIds(user.getTenantId(), resourceIds); + } + @ApiOperation(value = "Get All Resource Infos (getAllResources)", notes = "Returns a page of Resource Info objects owned by tenant. " + PAGE_DATA_PARAMETERS + RESOURCE_INFO_DESCRIPTION + TENANT_AUTHORITY_PARAGRAPH) diff --git a/application/src/main/java/org/thingsboard/server/controller/TelemetryController.java b/application/src/main/java/org/thingsboard/server/controller/TelemetryController.java index c98ae0dcb8..2f526e2037 100644 --- a/application/src/main/java/org/thingsboard/server/controller/TelemetryController.java +++ b/application/src/main/java/org/thingsboard/server/controller/TelemetryController.java @@ -36,6 +36,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.util.MultiValueMap; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; @@ -86,6 +87,7 @@ import org.thingsboard.server.service.telemetry.TsData; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -208,10 +210,12 @@ public class TelemetryController extends BaseController { public DeferredResult getAttributes( @Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, schema = @Schema(defaultValue = "DEVICE")) @PathVariable("entityType") String entityType, @Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable("entityId") String entityIdStr, - @Parameter(description = ATTRIBUTES_KEYS_DESCRIPTION) @RequestParam(name = "keys", required = false) String keysStr) throws ThingsboardException { + @Parameter(description = ATTRIBUTES_KEYS_DESCRIPTION) @RequestParam(name = "keys", required = false) String keysStr, + @RequestParam MultiValueMap params) throws ThingsboardException { + List keys = getKeys(keysStr, params); SecurityUser user = getCurrentUser(); return accessValidator.validateEntityAndCallback(getCurrentUser(), Operation.READ_ATTRIBUTES, entityType, entityIdStr, - (result, tenantId, entityId) -> getAttributeValuesCallback(result, user, entityId, null, keysStr)); + (result, tenantId, entityId) -> getAttributeValuesCallback(result, user, entityId, null, keys)); } @@ -231,10 +235,12 @@ public class TelemetryController extends BaseController { @Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, schema = @Schema(defaultValue = "DEVICE")) @PathVariable("entityType") String entityType, @Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable("entityId") String entityIdStr, @Parameter(description = ATTRIBUTES_SCOPE_DESCRIPTION, schema = @Schema(allowableValues = {"SERVER_SCOPE", "SHARED_SCOPE", "CLIENT_SCOPE"}, requiredMode = Schema.RequiredMode.REQUIRED)) @PathVariable("scope") AttributeScope scope, - @Parameter(description = ATTRIBUTES_KEYS_DESCRIPTION) @RequestParam(name = "keys", required = false) String keysStr) throws ThingsboardException { + @Parameter(description = ATTRIBUTES_KEYS_DESCRIPTION) @RequestParam(name = "keys", required = false) String keysStr, + @RequestParam MultiValueMap params) throws ThingsboardException { + List keys = getKeys(keysStr, params); SecurityUser user = getCurrentUser(); return accessValidator.validateEntityAndCallback(getCurrentUser(), Operation.READ_ATTRIBUTES, entityType, entityIdStr, - (result, tenantId, entityId) -> getAttributeValuesCallback(result, user, entityId, scope, keysStr)); + (result, tenantId, entityId) -> getAttributeValuesCallback(result, user, entityId, scope, keys)); } @ApiOperation(value = "Get time series keys (getTimeseriesKeys)", @@ -270,10 +276,12 @@ public class TelemetryController extends BaseController { @Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable("entityId") String entityIdStr, @Parameter(description = TELEMETRY_KEYS_DESCRIPTION) @RequestParam(name = "keys", required = false) String keysStr, @Parameter(description = STRICT_DATA_TYPES_DESCRIPTION) - @RequestParam(name = "useStrictDataTypes", required = false, defaultValue = "false") Boolean useStrictDataTypes) throws ThingsboardException { + @RequestParam(name = "useStrictDataTypes", required = false, defaultValue = "false") Boolean useStrictDataTypes, + @RequestParam MultiValueMap params) throws ThingsboardException { + List keys = getKeys(keysStr, params); SecurityUser user = getCurrentUser(); return accessValidator.validateEntityAndCallback(getCurrentUser(), Operation.READ_TELEMETRY, entityType, entityIdStr, - (result, tenantId, entityId) -> getLatestTimeseriesValuesCallback(result, user, entityId, keysStr, useStrictDataTypes)); + (result, tenantId, entityId) -> getLatestTimeseriesValuesCallback(result, user, entityId, keys, useStrictDataTypes)); } @ApiOperation(value = "Get time series data (getTimeseries)", @@ -291,7 +299,7 @@ public class TelemetryController extends BaseController { public DeferredResult getTimeseries( @Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, schema = @Schema(defaultValue = "DEVICE")) @PathVariable("entityType") String entityType, @Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable("entityId") String entityIdStr, - @Parameter(description = TELEMETRY_KEYS_BASE_DESCRIPTION, required = true) @RequestParam(name = "keys") String keys, + @Parameter(description = TELEMETRY_KEYS_BASE_DESCRIPTION) @RequestParam(name = "keys", required = false) String keysStr, @Parameter(description = "A long value representing the start timestamp of the time range in milliseconds, UTC.") @RequestParam(name = "startTs") Long startTs, @Parameter(description = "A long value representing the end timestamp of the time range in milliseconds, UTC.") @@ -312,9 +320,11 @@ public class TelemetryController extends BaseController { @Parameter(description = SORT_ORDER_DESCRIPTION, schema = @Schema(allowableValues = {"ASC", "DESC"})) @RequestParam(name = "orderBy", defaultValue = "DESC") String orderBy, @Parameter(description = STRICT_DATA_TYPES_DESCRIPTION) - @RequestParam(name = "useStrictDataTypes", required = false, defaultValue = "false") Boolean useStrictDataTypes) throws ThingsboardException { + @RequestParam(name = "useStrictDataTypes", required = false, defaultValue = "false") Boolean useStrictDataTypes, + @RequestParam MultiValueMap params) throws ThingsboardException { + List keys = getKeys(keysStr, params); DeferredResult response = new DeferredResult<>(); - Futures.addCallback(tbTelemetryService.getTimeseries(EntityIdFactory.getByTypeAndId(entityType, entityIdStr), toKeysList(keys), startTs, endTs, + Futures.addCallback(tbTelemetryService.getTimeseries(EntityIdFactory.getByTypeAndId(entityType, entityIdStr), keys, startTs, endTs, intervalType, interval, timeZone, limit, Aggregation.valueOf(aggStr), orderBy, useStrictDataTypes, getCurrentUser()), getTsKvListCallback(response, useStrictDataTypes), MoreExecutors.directExecutor()); return response; @@ -466,7 +476,7 @@ public class TelemetryController extends BaseController { public DeferredResult deleteEntityTimeseries( @Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, schema = @Schema(defaultValue = "DEVICE")) @PathVariable("entityType") String entityType, @Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable("entityId") String entityIdStr, - @Parameter(description = TELEMETRY_KEYS_DESCRIPTION, required = true) @RequestParam(name = "keys") String keysStr, + @Parameter(description = TELEMETRY_KEYS_DESCRIPTION) @RequestParam(name = "keys", required = false) String keysStr, @Parameter(description = "A boolean value to specify if should be deleted all data for selected keys or only data that are in the selected time range.") @RequestParam(name = "deleteAllDataForKeys", defaultValue = "false") boolean deleteAllDataForKeys, @Parameter(description = "A long value representing the start timestamp of removal time range in milliseconds.") @@ -476,16 +486,17 @@ public class TelemetryController extends BaseController { @Parameter(description = "If the parameter is set to true, the latest telemetry can be removed, otherwise, in case that parameter is set to false the latest value will not removed.") @RequestParam(name = "deleteLatest", required = false, defaultValue = "true") boolean deleteLatest, @Parameter(description = "If the parameter is set to true, the latest telemetry will be rewritten in case that current latest value was removed, otherwise, in case that parameter is set to false the new latest value will not set.") - @RequestParam(name = "rewriteLatestIfDeleted", defaultValue = "false") boolean rewriteLatestIfDeleted) throws ThingsboardException { + @RequestParam(name = "rewriteLatestIfDeleted", defaultValue = "false") boolean rewriteLatestIfDeleted, + @RequestParam MultiValueMap params) throws ThingsboardException { + List keys = getKeys(keysStr, params); EntityId entityId = EntityIdFactory.getByTypeAndId(entityType, entityIdStr); - return deleteTimeseries(entityId, keysStr, deleteAllDataForKeys, startTs, endTs, rewriteLatestIfDeleted, deleteLatest); + return deleteTimeseries(entityId, keys, deleteAllDataForKeys, startTs, endTs, rewriteLatestIfDeleted, deleteLatest); } - private DeferredResult deleteTimeseries(EntityId entityIdStr, String keysStr, boolean deleteAllDataForKeys, + private DeferredResult deleteTimeseries(EntityId entityIdStr, List keys, boolean deleteAllDataForKeys, Long startTs, Long endTs, boolean rewriteLatestIfDeleted, boolean deleteLatest) throws ThingsboardException { - List keys = toKeysList(keysStr); if (keys.isEmpty()) { - return getImmediateDeferredResult("Empty keys: " + keysStr, HttpStatus.BAD_REQUEST); + return getImmediateDeferredResult("Empty keys: " + keys, HttpStatus.BAD_REQUEST); } SecurityUser user = getCurrentUser(); @@ -547,9 +558,11 @@ public class TelemetryController extends BaseController { public DeferredResult deleteDeviceAttributes( @Parameter(description = DEVICE_ID_PARAM_DESCRIPTION, required = true) @PathVariable(DEVICE_ID) String deviceIdStr, @Parameter(description = ATTRIBUTES_SCOPE_DESCRIPTION, schema = @Schema(allowableValues = {"SERVER_SCOPE", "SHARED_SCOPE", "CLIENT_SCOPE"}, requiredMode = Schema.RequiredMode.REQUIRED)) @PathVariable("scope") AttributeScope scope, - @Parameter(description = ATTRIBUTES_KEYS_DESCRIPTION, required = true) @RequestParam(name = "keys") String keysStr) throws ThingsboardException { + @Parameter(description = ATTRIBUTES_KEYS_DESCRIPTION) @RequestParam(name = "keys", required = false) String keysStr, + @RequestParam MultiValueMap params) throws ThingsboardException { + List keys = getKeys(keysStr, params); EntityId entityId = EntityIdFactory.getByTypeAndUuid(EntityType.DEVICE, deviceIdStr); - return deleteAttributes(entityId, scope, keysStr); + return deleteAttributes(entityId, scope, keys); } @ApiOperation(value = "Delete entity attributes (deleteEntityAttributes)", @@ -570,15 +583,20 @@ public class TelemetryController extends BaseController { @Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, schema = @Schema(defaultValue = "DEVICE")) @PathVariable("entityType") String entityType, @Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable("entityId") String entityIdStr, @Parameter(description = ATTRIBUTES_SCOPE_DESCRIPTION, required = true, schema = @Schema(allowableValues = {"SERVER_SCOPE", "SHARED_SCOPE", "CLIENT_SCOPE"})) @PathVariable("scope") AttributeScope scope, - @Parameter(description = ATTRIBUTES_KEYS_DESCRIPTION, required = true) @RequestParam(name = "keys") String keysStr) throws ThingsboardException { + @Parameter(description = ATTRIBUTES_KEYS_DESCRIPTION) @RequestParam(name = "keys", required = false) String keysStr, + @RequestParam MultiValueMap params) throws ThingsboardException { + List keys = getKeys(keysStr, params); EntityId entityId = EntityIdFactory.getByTypeAndId(entityType, entityIdStr); - return deleteAttributes(entityId, scope, keysStr); + return deleteAttributes(entityId, scope, keys); + } + + private List getKeys(String keysStr, MultiValueMap params) { + return params.get("key") != null ? params.get("key") : toKeysList(keysStr); } - private DeferredResult deleteAttributes(EntityId entityIdSrc, AttributeScope scope, String keysStr) throws ThingsboardException { - List keys = toKeysList(keysStr); + private DeferredResult deleteAttributes(EntityId entityIdSrc, AttributeScope scope, List keys) throws ThingsboardException { if (keys.isEmpty()) { - return getImmediateDeferredResult("Empty keys: " + keysStr, HttpStatus.BAD_REQUEST); + return getImmediateDeferredResult("Empty keys: " + keys, HttpStatus.BAD_REQUEST); } SecurityUser user = getCurrentUser(); @@ -709,30 +727,29 @@ public class TelemetryController extends BaseController { }); } - private void getLatestTimeseriesValuesCallback(@Nullable DeferredResult result, SecurityUser user, EntityId entityId, String keys, Boolean useStrictDataTypes) { + private void getLatestTimeseriesValuesCallback(@Nullable DeferredResult result, SecurityUser user, EntityId entityId, List keys, Boolean useStrictDataTypes) { ListenableFuture> future; - if (StringUtils.isEmpty(keys)) { + if (keys.isEmpty()) { future = tsService.findAllLatest(user.getTenantId(), entityId); } else { - future = tsService.findLatest(user.getTenantId(), entityId, toKeysList(keys)); + future = tsService.findLatest(user.getTenantId(), entityId, keys); } Futures.addCallback(future, getTsKvListCallback(result, useStrictDataTypes), MoreExecutors.directExecutor()); } - private void getAttributeValuesCallback(@Nullable DeferredResult result, SecurityUser user, EntityId entityId, AttributeScope scope, String keys) { - List keyList = toKeysList(keys); - FutureCallback> callback = getAttributeValuesToResponseCallback(result, user, scope, entityId, keyList); + private void getAttributeValuesCallback(@Nullable DeferredResult result, SecurityUser user, EntityId entityId, AttributeScope scope, List keys) { + FutureCallback> callback = getAttributeValuesToResponseCallback(result, user, scope, entityId, keys); if (scope != null) { - if (keyList != null && !keyList.isEmpty()) { - Futures.addCallback(attributesService.find(user.getTenantId(), entityId, scope, keyList), callback, MoreExecutors.directExecutor()); + if (keys != null && !keys.isEmpty()) { + Futures.addCallback(attributesService.find(user.getTenantId(), entityId, scope, keys), callback, MoreExecutors.directExecutor()); } else { Futures.addCallback(attributesService.findAll(user.getTenantId(), entityId, scope), callback, MoreExecutors.directExecutor()); } } else { List>> futures = new ArrayList<>(); for (AttributeScope tmpScope : AttributeScope.values()) { - if (keyList != null && !keyList.isEmpty()) { - futures.add(attributesService.find(user.getTenantId(), entityId, tmpScope, keyList)); + if (keys != null && !keys.isEmpty()) { + futures.add(attributesService.find(user.getTenantId(), entityId, tmpScope, keys)); } else { futures.add(attributesService.findAll(user.getTenantId(), entityId, tmpScope)); } @@ -872,11 +889,11 @@ public class TelemetryController extends BaseController { } private List toKeysList(String keys) { - List keyList = null; if (!StringUtils.isEmpty(keys)) { - keyList = Arrays.asList(keys.split(",")); + return Arrays.asList(keys.split(",")); + } else { + return Collections.emptyList(); } - return keyList; } private DeferredResult getImmediateDeferredResult(String message, HttpStatus status) { diff --git a/application/src/main/java/org/thingsboard/server/controller/TenantController.java b/application/src/main/java/org/thingsboard/server/controller/TenantController.java index 3ded7b7171..2f8be6589f 100644 --- a/application/src/main/java/org/thingsboard/server/controller/TenantController.java +++ b/application/src/main/java/org/thingsboard/server/controller/TenantController.java @@ -115,7 +115,7 @@ public class TenantController extends BaseController { @ApiOperation(value = "Delete Tenant (deleteTenant)", notes = "Deletes the tenant, it's customers, rule chains, devices and all other related entities. Referencing non-existing tenant Id will cause an error." + SYSTEM_AUTHORITY_PARAGRAPH) - @PreAuthorize("hasAuthority('SYS_ADMIN')") + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @RequestMapping(value = "/tenant/{tenantId}", method = RequestMethod.DELETE) @ResponseStatus(value = HttpStatus.OK) public void deleteTenant(@Parameter(description = TENANT_ID_PARAM_DESCRIPTION) diff --git a/application/src/main/java/org/thingsboard/server/controller/TenantProfileController.java b/application/src/main/java/org/thingsboard/server/controller/TenantProfileController.java index 1a3bdce1f6..6c0f70e17c 100644 --- a/application/src/main/java/org/thingsboard/server/controller/TenantProfileController.java +++ b/application/src/main/java/org/thingsboard/server/controller/TenantProfileController.java @@ -164,9 +164,14 @@ public class TenantProfileController extends BaseController { " \"warnThreshold\": 0,\n" + " \"maxCalculatedFieldsPerEntity\": 5,\n" + " \"maxArgumentsPerCF\": 10,\n" + + " \"minAllowedScheduledUpdateIntervalInSecForCF\": 60,\n" + + " \"maxRelationLevelPerCfArgument\": 10,\n" + + " \"maxRelatedEntitiesToReturnPerCfArgument\": 100,\n" + " \"maxDataPointsPerRollingArg\": 1000,\n" + " \"maxStateSizeInKBytes\": 32,\n" + " \"maxSingleValueArgumentSizeInKBytes\": 2" + + " \"minAllowedDeduplicationIntervalInSecForCF\": 60" + + " \"minAllowedAggregationIntervalInSecForCF\": 60" + " }\n" + " },\n" + " \"default\": false\n" + diff --git a/application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthConfigController.java b/application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthConfigController.java index 6aae853636..eef086452f 100644 --- a/application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthConfigController.java +++ b/application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthConfigController.java @@ -56,7 +56,6 @@ public class TwoFactorAuthConfigController extends BaseController { private final TwoFaConfigManager twoFaConfigManager; private final TwoFactorAuthService twoFactorAuthService; - @ApiOperation(value = "Get account 2FA settings (getAccountTwoFaSettings)", notes = "Get user's account 2FA configuration. Configuration contains configs for different 2FA providers." + NEW_LINE + "Example:\n" + @@ -67,13 +66,12 @@ public class TwoFactorAuthConfigController extends BaseController { " }\n}\n```" + ControllerConstants.AVAILABLE_FOR_ANY_AUTHORIZED_USER) @GetMapping("/account/settings") - @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER', 'MFA_CONFIGURATION_TOKEN')") public AccountTwoFaSettings getAccountTwoFaSettings() throws ThingsboardException { SecurityUser user = getCurrentUser(); - return twoFaConfigManager.getAccountTwoFaSettings(user.getTenantId(), user.getId()).orElse(null); + return twoFaConfigManager.getAccountTwoFaSettings(user.getTenantId(), user).orElse(null); } - @ApiOperation(value = "Generate 2FA account config (generateTwoFaAccountConfig)", notes = "Generate new 2FA account config template for specified provider type. " + NEW_LINE + "For TOTP, this will return a corresponding account config template " + @@ -99,7 +97,7 @@ public class TwoFactorAuthConfigController extends BaseController { "Will throw an error (Bad Request) if the provider is not configured for usage. " + ControllerConstants.AVAILABLE_FOR_ANY_AUTHORIZED_USER) @PostMapping("/account/config/generate") - @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER', 'MFA_CONFIGURATION_TOKEN')") public TwoFaAccountConfig generateTwoFaAccountConfig(@Parameter(description = "2FA provider type to generate new account config for", schema = @Schema(defaultValue = "TOTP", requiredMode = Schema.RequiredMode.REQUIRED)) @RequestParam TwoFaProviderType providerType) throws Exception { SecurityUser user = getCurrentUser(); @@ -127,7 +125,7 @@ public class TwoFactorAuthConfigController extends BaseController { "or if the provider is not configured for usage. " + ControllerConstants.AVAILABLE_FOR_ANY_AUTHORIZED_USER) @PostMapping("/account/config/submit") - @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER', 'MFA_CONFIGURATION_TOKEN')") public void submitTwoFaAccountConfig(@Valid @RequestBody TwoFaAccountConfig accountConfig) throws Exception { SecurityUser user = getCurrentUser(); twoFactorAuthService.prepareVerificationCode(user, accountConfig, false); @@ -139,11 +137,11 @@ public class TwoFactorAuthConfigController extends BaseController { "Will throw an error (Bad Request) if the provider is not configured for usage. " + ControllerConstants.AVAILABLE_FOR_ANY_AUTHORIZED_USER) @PostMapping("/account/config") - @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER', 'MFA_CONFIGURATION_TOKEN')") public AccountTwoFaSettings verifyAndSaveTwoFaAccountConfig(@Valid @RequestBody TwoFaAccountConfig accountConfig, @RequestParam(required = false) String verificationCode) throws Exception { SecurityUser user = getCurrentUser(); - if (twoFaConfigManager.getTwoFaAccountConfig(user.getTenantId(), user.getId(), accountConfig.getProviderType()).isPresent()) { + if (twoFaConfigManager.getTwoFaAccountConfig(user.getTenantId(), user, accountConfig.getProviderType()).isPresent()) { throw new IllegalArgumentException("2FA provider is already configured"); } @@ -154,7 +152,7 @@ public class TwoFactorAuthConfigController extends BaseController { verificationSuccess = true; } if (verificationSuccess) { - return twoFaConfigManager.saveTwoFaAccountConfig(user.getTenantId(), user.getId(), accountConfig); + return twoFaConfigManager.saveTwoFaAccountConfig(user.getTenantId(), user, accountConfig); } else { throw new IllegalArgumentException("Verification code is incorrect"); } @@ -162,42 +160,41 @@ public class TwoFactorAuthConfigController extends BaseController { @ApiOperation(value = "Update 2FA account config (updateTwoFaAccountConfig)", notes = "Update config for a given provider type. \n" + - "Update request example:\n" + - "```\n{\n \"useByDefault\": true\n}\n```\n" + - "Returns whole account's 2FA settings object.\n" + - ControllerConstants.AVAILABLE_FOR_ANY_AUTHORIZED_USER) + "Update request example:\n" + + "```\n{\n \"useByDefault\": true\n}\n```\n" + + "Returns whole account's 2FA settings object.\n" + + ControllerConstants.AVAILABLE_FOR_ANY_AUTHORIZED_USER) @PutMapping("/account/config") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") public AccountTwoFaSettings updateTwoFaAccountConfig(@RequestParam TwoFaProviderType providerType, @RequestBody TwoFaAccountConfigUpdateRequest updateRequest) throws ThingsboardException { SecurityUser user = getCurrentUser(); - TwoFaAccountConfig accountConfig = twoFaConfigManager.getTwoFaAccountConfig(user.getTenantId(), user.getId(), providerType) + TwoFaAccountConfig accountConfig = twoFaConfigManager.getTwoFaAccountConfig(user.getTenantId(), user, providerType) .orElseThrow(() -> new IllegalArgumentException("Config for " + providerType + " 2FA provider not found")); accountConfig.setUseByDefault(updateRequest.isUseByDefault()); - return twoFaConfigManager.saveTwoFaAccountConfig(user.getTenantId(), user.getId(), accountConfig); + return twoFaConfigManager.saveTwoFaAccountConfig(user.getTenantId(), user, accountConfig); } @ApiOperation(value = "Delete 2FA account config (deleteTwoFaAccountConfig)", notes = "Delete 2FA config for a given 2FA provider type. \n" + - "Returns whole account's 2FA settings object.\n" + - ControllerConstants.AVAILABLE_FOR_ANY_AUTHORIZED_USER) + "Returns whole account's 2FA settings object.\n" + + ControllerConstants.AVAILABLE_FOR_ANY_AUTHORIZED_USER) @DeleteMapping("/account/config") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") public AccountTwoFaSettings deleteTwoFaAccountConfig(@RequestParam TwoFaProviderType providerType) throws ThingsboardException { SecurityUser user = getCurrentUser(); - return twoFaConfigManager.deleteTwoFaAccountConfig(user.getTenantId(), user.getId(), providerType); + return twoFaConfigManager.deleteTwoFaAccountConfig(user.getTenantId(), user, providerType); } - @ApiOperation(value = "Get available 2FA providers (getAvailableTwoFaProviders)", notes = "Get the list of provider types available for user to use (the ones configured by tenant or sysadmin).\n" + - "Example of response:\n" + - "```\n[\n \"TOTP\",\n \"EMAIL\",\n \"SMS\"\n]\n```" + - ControllerConstants.AVAILABLE_FOR_ANY_AUTHORIZED_USER + "Example of response:\n" + + "```\n[\n \"TOTP\",\n \"EMAIL\",\n \"SMS\"\n]\n```" + + ControllerConstants.AVAILABLE_FOR_ANY_AUTHORIZED_USER ) @GetMapping("/providers") - @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER', 'MFA_CONFIGURATION_TOKEN')") public List getAvailableTwoFaProviders() throws ThingsboardException { return twoFaConfigManager.getPlatformTwoFaSettings(getTenantId(), true) .map(PlatformTwoFaSettings::getProviders).orElse(Collections.emptyList()).stream() @@ -205,7 +202,6 @@ public class TwoFactorAuthConfigController extends BaseController { .collect(Collectors.toList()); } - @ApiOperation(value = "Get platform 2FA settings (getPlatformTwoFaSettings)", notes = "Get platform settings for 2FA. The settings are described for savePlatformTwoFaSettings API method. " + "If 2FA is not configured, then an empty response will be returned." + @@ -260,11 +256,10 @@ public class TwoFactorAuthConfigController extends BaseController { @PostMapping("/settings") @PreAuthorize("hasAnyAuthority('SYS_ADMIN')") public PlatformTwoFaSettings savePlatformTwoFaSettings(@Parameter(description = "Settings value", required = true) - @RequestBody PlatformTwoFaSettings twoFaSettings) throws ThingsboardException { + @RequestBody PlatformTwoFaSettings twoFaSettings) throws ThingsboardException { return twoFaConfigManager.savePlatformTwoFaSettings(getTenantId(), twoFaSettings); } - @Data public static class TwoFaAccountConfigUpdateRequest { private boolean useByDefault; diff --git a/application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthController.java b/application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthController.java index 80c88bfc9b..0d2af1a4f1 100644 --- a/application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthController.java +++ b/application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthController.java @@ -28,7 +28,6 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.audit.ActionType; -import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.security.model.JwtPair; import org.thingsboard.server.common.data.security.model.mfa.PlatformTwoFaSettings; @@ -64,7 +63,6 @@ public class TwoFactorAuthController extends BaseController { private final SystemSecurityService systemSecurityService; private final UserService userService; - @ApiOperation(value = "Request 2FA verification code (requestTwoFaVerificationCode)", notes = "Request 2FA verification code." + NEW_LINE + "To make a request to this endpoint, you need an access token with the scope of PRE_VERIFICATION_TOKEN, " + @@ -92,30 +90,28 @@ public class TwoFactorAuthController extends BaseController { SecurityUser user = getCurrentUser(); boolean verificationSuccess = twoFactorAuthService.checkVerificationCode(user, providerType, verificationCode, true); if (verificationSuccess) { - systemSecurityService.logLoginAction(user, new RestAuthenticationDetails(servletRequest), ActionType.LOGIN, null); - user = new SecurityUser(userService.findUserById(user.getTenantId(), user.getId()), true, user.getUserPrincipal()); - return tokenFactory.createTokenPair(user); + logLogInAction(servletRequest, user, null); + return createTokenPair(user); } else { - ThingsboardException error = new ThingsboardException("Verification code is incorrect", ThingsboardErrorCode.BAD_REQUEST_PARAMS); - systemSecurityService.logLoginAction(user, new RestAuthenticationDetails(servletRequest), ActionType.LOGIN, error); + IllegalArgumentException error = new IllegalArgumentException("Verification code is incorrect"); + logLogInAction(servletRequest, user, error); throw error; } } - @ApiOperation(value = "Get available 2FA providers (getAvailableTwoFaProviders)", notes = "Get the list of 2FA provider infos available for user to use. Example:\n" + - "```\n[\n" + - " {\n \"type\": \"EMAIL\",\n \"default\": true,\n \"contact\": \"ab*****ko@gmail.com\"\n },\n" + - " {\n \"type\": \"TOTP\",\n \"default\": false,\n \"contact\": null\n },\n" + - " {\n \"type\": \"SMS\",\n \"default\": false,\n \"contact\": \"+38********12\"\n }\n" + - "]\n```") + "```\n[\n" + + " {\n \"type\": \"EMAIL\",\n \"default\": true,\n \"contact\": \"ab*****ko@gmail.com\"\n },\n" + + " {\n \"type\": \"TOTP\",\n \"default\": false,\n \"contact\": null\n },\n" + + " {\n \"type\": \"SMS\",\n \"default\": false,\n \"contact\": \"+38********12\"\n }\n" + + "]\n```") @GetMapping("/providers") @PreAuthorize("hasAuthority('PRE_VERIFICATION_TOKEN')") public List getAvailableTwoFaProviders() throws ThingsboardException { SecurityUser user = getCurrentUser(); Optional platformTwoFaSettings = twoFaConfigManager.getPlatformTwoFaSettings(user.getTenantId(), true); - return twoFaConfigManager.getAccountTwoFaSettings(user.getTenantId(), user.getId()) + return twoFaConfigManager.getAccountTwoFaSettings(user.getTenantId(), user) .map(settings -> settings.getConfigs().values()).orElse(Collections.emptyList()) .stream().map(config -> { String contact = null; @@ -139,6 +135,32 @@ public class TwoFactorAuthController extends BaseController { .collect(Collectors.toList()); } + @ApiOperation(value = "Get regular token pair after successfully configuring 2FA", + notes = "Checks 2FA is configured, returning token pair on success.") + @PostMapping("/login") + @PreAuthorize("hasAuthority('MFA_CONFIGURATION_TOKEN')") + public JwtPair authenticateByTwoFaConfigurationToken(HttpServletRequest servletRequest) throws ThingsboardException { + SecurityUser user = getCurrentUser(); + if (twoFactorAuthService.isTwoFaEnabled(user.getTenantId(), user)) { + logLogInAction(servletRequest, user, null); + return createTokenPair(user); + } else { + IllegalArgumentException error = new IllegalArgumentException("2FA is not configured"); + logLogInAction(servletRequest, user, error); + throw error; + } + } + + private JwtPair createTokenPair(SecurityUser user) { + log.debug("[{}][{}] Creating token pair for user", user.getTenantId(), user.getId()); + user = new SecurityUser(userService.findUserById(user.getTenantId(), user.getId()), true, user.getUserPrincipal()); + return tokenFactory.createTokenPair(user); + } + + private void logLogInAction(HttpServletRequest servletRequest, SecurityUser user, Exception error) { + systemSecurityService.logLoginAction(user, new RestAuthenticationDetails(servletRequest), ActionType.LOGIN, error); + } + @Data @AllArgsConstructor @Builder diff --git a/application/src/main/java/org/thingsboard/server/controller/UserController.java b/application/src/main/java/org/thingsboard/server/controller/UserController.java index b9cb0c88b3..a2a7c993e9 100644 --- a/application/src/main/java/org/thingsboard/server/controller/UserController.java +++ b/application/src/main/java/org/thingsboard/server/controller/UserController.java @@ -266,6 +266,9 @@ public class UserController extends BaseController { if (user.getAuthority() == Authority.SYS_ADMIN && getCurrentUser().getId().equals(userId)) { throw new ThingsboardException("Sysadmin is not allowed to delete himself", ThingsboardErrorCode.PERMISSION_DENIED); } + if (user.getAuthority() == Authority.TENANT_ADMIN && userService.countTenantAdmins(user.getTenantId()) == 1) { + throw new ThingsboardException("At least one tenant administrator must remain!", ThingsboardErrorCode.BAD_REQUEST_PARAMS); + } tbUserService.delete(getTenantId(), getCurrentUser().getCustomerId(), user, getCurrentUser()); } diff --git a/application/src/main/java/org/thingsboard/server/service/ai/AiChatModelServiceImpl.java b/application/src/main/java/org/thingsboard/server/service/ai/AiChatModelServiceImpl.java index d6252f57a6..639e2025fe 100644 --- a/application/src/main/java/org/thingsboard/server/service/ai/AiChatModelServiceImpl.java +++ b/application/src/main/java/org/thingsboard/server/service/ai/AiChatModelServiceImpl.java @@ -15,7 +15,13 @@ */ package org.thingsboard.server.service.ai; +import com.fasterxml.jackson.core.io.JsonStringEncoder; import com.google.common.util.concurrent.FluentFuture; +import dev.langchain4j.data.message.ChatMessage; +import dev.langchain4j.data.message.Content; +import dev.langchain4j.data.message.TextContent; +import dev.langchain4j.data.message.UserMessage; +import dev.langchain4j.model.ModelProvider; import dev.langchain4j.model.chat.ChatModel; import dev.langchain4j.model.chat.request.ChatRequest; import dev.langchain4j.model.chat.response.ChatResponse; @@ -24,6 +30,9 @@ import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.ai.model.chat.AiChatModelConfig; import org.thingsboard.server.common.data.ai.model.chat.Langchain4jChatModelConfigurer; +import java.util.List; +import java.util.stream.Collectors; + @Service @RequiredArgsConstructor class AiChatModelServiceImpl implements AiChatModelService { @@ -34,7 +43,39 @@ class AiChatModelServiceImpl implements AiChatModelService { @Override public > FluentFuture sendChatRequestAsync(AiChatModelConfig chatModelConfig, ChatRequest chatRequest) { ChatModel langChainChatModel = chatModelConfig.configure(chatModelConfigurer); + if (langChainChatModel.provider() == ModelProvider.GITHUB_MODELS) { + chatRequest = prepareGithubChatRequest(chatRequest); + } return aiRequestsExecutor.sendChatRequestAsync(langChainChatModel, chatRequest); } + private ChatRequest prepareGithubChatRequest(ChatRequest chatRequest) { + List messages = chatRequest.messages().stream() + .map(this::prepareUserMessage) + .collect(Collectors.toList()); + + return ChatRequest.builder() + .messages(messages) + .responseFormat(chatRequest.responseFormat()) + .build(); + } + + private ChatMessage prepareUserMessage(ChatMessage message) { + if (message instanceof UserMessage userMessage) { + List newContents = userMessage.contents().stream() + .map(this::prepareContent) + .collect(Collectors.toList()); + + return UserMessage.from(newContents); + } + return message; + } + + private Content prepareContent(Content content) { + if (content instanceof TextContent txt) { + return new TextContent(new String(JsonStringEncoder.getInstance().quoteAsString(txt.text()))); + } + return content; + } + } diff --git a/application/src/main/java/org/thingsboard/server/service/ai/Langchain4jChatModelConfigurerImpl.java b/application/src/main/java/org/thingsboard/server/service/ai/Langchain4jChatModelConfigurerImpl.java index 69dd98f47f..c631f1a3d0 100644 --- a/application/src/main/java/org/thingsboard/server/service/ai/Langchain4jChatModelConfigurerImpl.java +++ b/application/src/main/java/org/thingsboard/server/service/ai/Langchain4jChatModelConfigurerImpl.java @@ -32,8 +32,10 @@ import dev.langchain4j.model.chat.request.ChatRequestParameters; import dev.langchain4j.model.github.GitHubModelsChatModel; import dev.langchain4j.model.googleai.GoogleAiGeminiChatModel; import dev.langchain4j.model.mistralai.MistralAiChatModel; +import dev.langchain4j.model.ollama.OllamaChatModel; import dev.langchain4j.model.openai.OpenAiChatModel; import dev.langchain4j.model.vertexai.gemini.VertexAiGeminiChatModel; +import org.springframework.http.HttpHeaders; import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.ai.model.chat.AmazonBedrockChatModelConfig; import org.thingsboard.server.common.data.ai.model.chat.AnthropicChatModelConfig; @@ -43,10 +45,12 @@ import org.thingsboard.server.common.data.ai.model.chat.GoogleAiGeminiChatModelC import org.thingsboard.server.common.data.ai.model.chat.GoogleVertexAiGeminiChatModelConfig; import org.thingsboard.server.common.data.ai.model.chat.Langchain4jChatModelConfigurer; import org.thingsboard.server.common.data.ai.model.chat.MistralAiChatModelConfig; +import org.thingsboard.server.common.data.ai.model.chat.OllamaChatModelConfig; import org.thingsboard.server.common.data.ai.model.chat.OpenAiChatModelConfig; import org.thingsboard.server.common.data.ai.provider.AmazonBedrockProviderConfig; import org.thingsboard.server.common.data.ai.provider.AzureOpenAiProviderConfig; import org.thingsboard.server.common.data.ai.provider.GoogleVertexAiGeminiProviderConfig; +import org.thingsboard.server.common.data.ai.provider.OllamaProviderConfig; import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; import software.amazon.awssdk.regions.Region; @@ -54,7 +58,11 @@ import software.amazon.awssdk.services.bedrockruntime.BedrockRuntimeClient; import java.io.ByteArrayInputStream; import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.time.Duration; +import java.util.Base64; + +import static java.util.Collections.singletonMap; @Component class Langchain4jChatModelConfigurerImpl implements Langchain4jChatModelConfigurer { @@ -62,6 +70,7 @@ class Langchain4jChatModelConfigurerImpl implements Langchain4jChatModelConfigur @Override public ChatModel configureChatModel(OpenAiChatModelConfig chatModelConfig) { return OpenAiChatModel.builder() + .baseUrl(chatModelConfig.providerConfig().baseUrl()) .apiKey(chatModelConfig.providerConfig().apiKey()) .modelName(chatModelConfig.modelId()) .temperature(chatModelConfig.temperature()) @@ -134,7 +143,7 @@ class Langchain4jChatModelConfigurerImpl implements Langchain4jChatModelConfigur // set request timeout from model config if (chatModelConfig.timeoutSeconds() != null) { - retrySettings.setTotalTimeout(org.threeten.bp.Duration.ofSeconds(chatModelConfig.timeoutSeconds())); + retrySettings.setTotalTimeoutDuration(Duration.ofSeconds(chatModelConfig.timeoutSeconds())); } // set updated retry settings @@ -262,6 +271,35 @@ class Langchain4jChatModelConfigurerImpl implements Langchain4jChatModelConfigur .build(); } + @Override + public ChatModel configureChatModel(OllamaChatModelConfig chatModelConfig) { + var builder = OllamaChatModel.builder() + .baseUrl(chatModelConfig.providerConfig().baseUrl()) + .modelName(chatModelConfig.modelId()) + .temperature(chatModelConfig.temperature()) + .topP(chatModelConfig.topP()) + .topK(chatModelConfig.topK()) + .numCtx(chatModelConfig.contextLength()) + .numPredict(chatModelConfig.maxOutputTokens()) + .timeout(toDuration(chatModelConfig.timeoutSeconds())) + .maxRetries(chatModelConfig.maxRetries()); + + var auth = chatModelConfig.providerConfig().auth(); + if (auth instanceof OllamaProviderConfig.OllamaAuth.Basic basicAuth) { + String credentials = basicAuth.username() + ":" + basicAuth.password(); + String encodedCredentials = Base64.getEncoder().encodeToString(credentials.getBytes(StandardCharsets.UTF_8)); + builder.customHeaders(singletonMap(HttpHeaders.AUTHORIZATION, "Basic " + encodedCredentials)); + } else if (auth instanceof OllamaProviderConfig.OllamaAuth.Token tokenAuth) { + builder.customHeaders(singletonMap(HttpHeaders.AUTHORIZATION, "Bearer " + tokenAuth.token())); + } else if (auth instanceof OllamaProviderConfig.OllamaAuth.None) { + // do nothing + } else { + throw new UnsupportedOperationException("Unknown authentication type: " + auth.getClass().getSimpleName()); + } + + return builder.build(); + } + private static Duration toDuration(Integer timeoutSeconds) { return timeoutSeconds != null ? Duration.ofSeconds(timeoutSeconds) : null; } diff --git a/application/src/main/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateService.java b/application/src/main/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateService.java index 3dedea999c..84025b3cee 100644 --- a/application/src/main/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateService.java +++ b/application/src/main/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateService.java @@ -442,13 +442,13 @@ public class DefaultTbApiUsageStateService extends AbstractPartitionBasedService boolean check(long threshold, long warnThreshold, long value); } - private void checkStartOfNextCycle() { + public void checkStartOfNextCycle() { updateLock.lock(); try { long now = System.currentTimeMillis(); myUsageStates.values().forEach(state -> { if ((state.getNextCycleTs() < now) && (now - state.getNextCycleTs() < TimeUnit.HOURS.toMillis(1))) { - state.setCycles(state.getNextCycleTs(), SchedulerUtils.getStartOfNextNextMonth()); + state.setCycles(state.getNextCycleTs(), SchedulerUtils.getStartOfNextMonth()); if (log.isTraceEnabled()) { log.trace("[{}][{}] Updating state cycles (currentCycleTs={},nextCycleTs={})", state.getTenantId(), state.getEntityId(), state.getCurrentCycleTs(), state.getNextCycleTs()); } diff --git a/application/src/main/java/org/thingsboard/server/service/cf/AbstractCalculatedFieldProcessingService.java b/application/src/main/java/org/thingsboard/server/service/cf/AbstractCalculatedFieldProcessingService.java new file mode 100644 index 0000000000..c22623e109 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/cf/AbstractCalculatedFieldProcessingService.java @@ -0,0 +1,373 @@ +/** + * Copyright © 2016-2025 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.cf; + +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.ListeningExecutorService; +import com.google.common.util.concurrent.MoreExecutors; +import jakarta.annotation.PostConstruct; +import jakarta.annotation.PreDestroy; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import org.thingsboard.common.util.ThingsBoardExecutors; +import org.thingsboard.server.common.data.cf.CalculatedField; +import org.thingsboard.server.common.data.cf.configuration.Argument; +import org.thingsboard.server.common.data.cf.configuration.ArgumentType; +import org.thingsboard.server.common.data.cf.configuration.RelationPathQueryDynamicSourceConfiguration; +import org.thingsboard.server.common.data.cf.configuration.aggregation.AggFunction; +import org.thingsboard.server.common.data.cf.configuration.aggregation.AggMetric; +import org.thingsboard.server.common.data.cf.configuration.aggregation.RelatedEntitiesAggregationCalculatedFieldConfiguration; +import org.thingsboard.server.common.data.cf.configuration.aggregation.single.EntityAggregationCalculatedFieldConfiguration; +import org.thingsboard.server.common.data.cf.configuration.aggregation.single.interval.AggInterval; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.kv.Aggregation; +import org.thingsboard.server.common.data.kv.AttributeKvEntry; +import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry; +import org.thingsboard.server.common.data.kv.BaseReadTsKvQuery; +import org.thingsboard.server.common.data.kv.BasicTsKvEntry; +import org.thingsboard.server.common.data.kv.ReadTsKvQuery; +import org.thingsboard.server.common.data.kv.TsKvEntry; +import org.thingsboard.server.common.data.relation.EntityRelation; +import org.thingsboard.server.common.data.relation.EntityRelationPathQuery; +import org.thingsboard.server.common.data.relation.RelationPathLevel; +import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; +import org.thingsboard.server.dao.attributes.AttributesService; +import org.thingsboard.server.dao.relation.RelationService; +import org.thingsboard.server.dao.timeseries.TimeseriesService; +import org.thingsboard.server.dao.usagerecord.ApiLimitService; +import org.thingsboard.server.service.cf.ctx.state.ArgumentEntry; +import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx; +import org.thingsboard.server.service.cf.ctx.state.SingleValueArgumentEntry; +import org.thingsboard.server.service.cf.ctx.state.aggregation.single.AggIntervalEntry; + +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.ExecutionException; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +import static org.thingsboard.server.common.data.cf.CalculatedFieldType.PROPAGATION; +import static org.thingsboard.server.common.data.cf.configuration.PropagationCalculatedFieldConfiguration.PROPAGATION_CONFIG_ARGUMENT; +import static org.thingsboard.server.common.data.cf.configuration.geofencing.EntityCoordinates.ENTITY_ID_LATITUDE_ARGUMENT_KEY; +import static org.thingsboard.server.common.data.cf.configuration.geofencing.EntityCoordinates.ENTITY_ID_LONGITUDE_ARGUMENT_KEY; +import static org.thingsboard.server.utils.CalculatedFieldArgumentUtils.createDefaultAttributeEntry; +import static org.thingsboard.server.utils.CalculatedFieldArgumentUtils.createDefaultKvEntry; +import static org.thingsboard.server.utils.CalculatedFieldArgumentUtils.transformAggMetricArgument; +import static org.thingsboard.server.utils.CalculatedFieldArgumentUtils.transformAggregationArgument; +import static org.thingsboard.server.utils.CalculatedFieldArgumentUtils.transformSingleValueArgument; +import static org.thingsboard.server.utils.CalculatedFieldArgumentUtils.transformTsRollingArgument; + +@Data +@Slf4j +public abstract class AbstractCalculatedFieldProcessingService { + + protected final AttributesService attributesService; + protected final TimeseriesService timeseriesService; + protected final ApiLimitService apiLimitService; + protected final RelationService relationService; + protected final OwnerService ownerService; + + protected ListeningExecutorService calculatedFieldCallbackExecutor; + + @PostConstruct + public void init() { + calculatedFieldCallbackExecutor = MoreExecutors.listeningDecorator(ThingsBoardExecutors.newWorkStealingPool( + Math.max(4, Runtime.getRuntime().availableProcessors()), getExecutorNamePrefix())); + } + + @PreDestroy + public void stop() { + if (calculatedFieldCallbackExecutor != null) { + calculatedFieldCallbackExecutor.shutdownNow(); + } + } + + protected abstract String getExecutorNamePrefix(); + + protected ListenableFuture> fetchArguments(CalculatedFieldCtx ctx, EntityId entityId, long ts) { + Map> argFutures = switch (ctx.getCfType()) { + case GEOFENCING -> fetchGeofencingCalculatedFieldArguments(ctx, entityId, false, ts); + case SIMPLE, SCRIPT, ALARM, PROPAGATION -> getBaseCalculatedFieldArguments(ctx, entityId, ts); + case RELATED_ENTITIES_AGGREGATION -> fetchRelatedEntitiesAggArguments(ctx, entityId, ts); + case ENTITY_AGGREGATION -> fetchEntityAggArguments(ctx, entityId, ts); + }; + if (ctx.getCfType() == PROPAGATION) { + argFutures.put(PROPAGATION_CONFIG_ARGUMENT, fetchPropagationCalculatedFieldArgument(ctx, entityId)); + } + return Futures.whenAllComplete(argFutures.values()) + .call(() -> resolveArgumentFutures(argFutures), + MoreExecutors.directExecutor()); + } + + private Map> getBaseCalculatedFieldArguments(CalculatedFieldCtx ctx, EntityId entityId, long ts) { + Map> futures = new HashMap<>(); + for (var entry : ctx.getArguments().entrySet()) { + var argEntityId = resolveEntityId(ctx.getTenantId(), entityId, entry.getValue()); + var argValueFuture = fetchArgumentValue(ctx.getTenantId(), argEntityId, entry.getValue(), ts); + futures.put(entry.getKey(), argValueFuture); + } + return futures; + } + + protected EntityId resolveEntityId(TenantId tenantId, EntityId entityId, Argument argument) { + if (argument.getRefEntityId() != null) { + return argument.getRefEntityId(); + } + if (!argument.hasOwnerSource()) { + return entityId; + } + return resolveOwnerArgument(tenantId, entityId); + } + + protected Map resolveArgumentFutures(Map> argFutures) { + return argFutures.entrySet().stream() + .collect(Collectors.toMap( + Map.Entry::getKey, // Keep the key as is + entry -> resolveArgumentValue(entry.getKey(), entry.getValue()) + )); + } + + protected ArgumentEntry resolveArgumentValue(String key, ListenableFuture future) { + try { + return future.get(); + } catch (ExecutionException e) { + Throwable cause = e.getCause(); + throw new RuntimeException("Failed to fetch " + key + ": " + cause.getMessage(), cause); + } catch (InterruptedException e) { + throw new RuntimeException("Failed to fetch" + key, e); + } + } + + protected ListenableFuture fetchPropagationCalculatedFieldArgument(CalculatedFieldCtx ctx, EntityId entityId) { + ListenableFuture> propagationEntityIds = fromDynamicSource(ctx.getTenantId(), entityId, ctx.getPropagationArgument()); + return Futures.transform(propagationEntityIds, ArgumentEntry::createPropagationArgument, MoreExecutors.directExecutor()); + } + + protected Map> fetchGeofencingCalculatedFieldArguments(CalculatedFieldCtx ctx, EntityId entityId, boolean dynamicArgumentsOnly, long startTs) { + Map> argFutures = new HashMap<>(); + Set> entries = ctx.getArguments().entrySet(); + if (dynamicArgumentsOnly) { + entries = entries.stream() + .filter(entry -> entry.getValue().hasRelationQuerySource()) + .collect(Collectors.toSet()); + } + for (var entry : entries) { + switch (entry.getKey()) { + case ENTITY_ID_LATITUDE_ARGUMENT_KEY, ENTITY_ID_LONGITUDE_ARGUMENT_KEY -> + argFutures.put(entry.getKey(), fetchArgumentValue(ctx.getTenantId(), entityId, entry.getValue(), startTs)); + default -> { + var resolvedEntityIdsFuture = resolveGeofencingEntityIds(ctx.getTenantId(), entityId, entry); + argFutures.put(entry.getKey(), Futures.transformAsync(resolvedEntityIdsFuture, resolvedEntityIds -> + fetchGeofencingKvEntry(ctx.getTenantId(), resolvedEntityIds, entry.getValue()), MoreExecutors.directExecutor())); + } + } + } + return argFutures; + } + + protected Map> fetchRelatedEntitiesAggArguments(CalculatedFieldCtx ctx, EntityId entityId, long ts) { + if (!(ctx.getCalculatedField().getConfiguration() instanceof RelatedEntitiesAggregationCalculatedFieldConfiguration config)) { + return Collections.emptyMap(); + } + ListenableFuture> relatedEntitiesFut = resolveRelatedEntities(ctx.getTenantId(), entityId, config.getRelation()); + + return config.getArguments().entrySet().stream() + .collect(Collectors.toMap( + Map.Entry::getKey, + entry -> Futures.transformAsync(relatedEntitiesFut, relatedEntities -> fetchRelatedEntitiesArgumentEntry(ctx.getTenantId(), relatedEntities, entry.getValue(), ts), MoreExecutors.directExecutor()) + )); + } + + protected Map> fetchEntityAggArguments(CalculatedFieldCtx ctx, EntityId entityId, long ts) { + if (!(ctx.getCalculatedField().getConfiguration() instanceof EntityAggregationCalculatedFieldConfiguration config)) { + return Collections.emptyMap(); + } + return config.getArguments().entrySet().stream() + .collect(Collectors.toMap( + Map.Entry::getKey, + entry -> fetchTimeSeries(ctx.getTenantId(), entityId, entry.getValue(), config.getInterval(), ts) + )); + } + + protected ListenableFuture> resolveRelatedEntities(TenantId tenantId, EntityId entityId, RelationPathLevel relation) { + Predicate filter = entityRelation -> CalculatedField.isSupportedRefEntity(entityRelation.getFrom()) && CalculatedField.isSupportedRefEntity(entityRelation.getTo()); + ListenableFuture> relationsFut = relationService.findFilteredRelationsByPathQueryAsync(tenantId, new EntityRelationPathQuery(entityId, List.of(relation)), filter); + + return Futures.transform(relationsFut, relations -> { + if (relations == null) { + return Collections.emptyList(); + } + + return switch (relation.direction()) { + case FROM -> relations.stream() + .map(EntityRelation::getTo) + .toList(); + case TO -> relations.stream() + .map(EntityRelation::getFrom) + .findFirst() + .map(List::of) + .orElseGet(Collections::emptyList); + }; + }, calculatedFieldCallbackExecutor); + } + + private ListenableFuture> resolveGeofencingEntityIds(TenantId tenantId, EntityId entityId, Map.Entry entry) { + Argument value = entry.getValue(); + if (value.getRefEntityId() != null) { + return Futures.immediateFuture(List.of(value.getRefEntityId())); + } + if (!value.hasDynamicSource()) { + return Futures.immediateFuture(List.of(entityId)); + } + return fromDynamicSource(tenantId, entityId, value); + } + + private ListenableFuture> fromDynamicSource(TenantId tenantId, EntityId entityId, Argument value) { + var refDynamicSourceConfiguration = value.getRefDynamicSourceConfiguration(); + return switch (refDynamicSourceConfiguration.getType()) { + case CURRENT_OWNER -> Futures.immediateFuture(List.of(resolveOwnerArgument(tenantId, entityId))); + case RELATION_PATH_QUERY -> { + var configuration = (RelationPathQueryDynamicSourceConfiguration) refDynamicSourceConfiguration; + Predicate filter = entityRelation -> CalculatedField.isSupportedRefEntity(entityRelation.getFrom()) && CalculatedField.isSupportedRefEntity(entityRelation.getTo()); + yield Futures.transform(relationService.findFilteredRelationsByPathQueryAsync(tenantId, configuration.toRelationPathQuery(entityId), filter), + configuration::resolveEntityIds, calculatedFieldCallbackExecutor); + } + }; + } + + private EntityId resolveOwnerArgument(TenantId tenantId, EntityId entityId) { + return ownerService.getOwner(tenantId, entityId); + } + + private ListenableFuture fetchGeofencingKvEntry(TenantId tenantId, List geofencingEntities, Argument argument) { + if (argument.getRefEntityKey().getType() != ArgumentType.ATTRIBUTE) { + throw new IllegalStateException("Unsupported argument key type: " + argument.getRefEntityKey().getType()); + } + List>> kvFutures = geofencingEntities.stream() + .map(entityId -> { + var attributesFuture = attributesService.find( + tenantId, + entityId, + argument.getRefEntityKey().getScope(), + argument.getRefEntityKey().getKey() + ); + return Futures.transform(attributesFuture, resultOpt -> + Map.entry(entityId, resultOpt.orElseGet(() -> createDefaultAttributeEntry(argument, System.currentTimeMillis()))), + calculatedFieldCallbackExecutor + ); + }).collect(Collectors.toList()); + + ListenableFuture>> allFutures = Futures.allAsList(kvFutures); + + return Futures.transform(allFutures, entries -> ArgumentEntry.createGeofencingValueArgument(entries.stream() + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue))), MoreExecutors.directExecutor()); + } + + public ListenableFuture fetchRelatedEntitiesArgumentEntry(TenantId tenantId, List aggEntities, Argument argument, long startTs) { + List>> futures = aggEntities.stream() + .map(entityId -> { + ListenableFuture argumentEntryFut = fetchArgumentValue(tenantId, entityId, argument, startTs); + return Futures.transform(argumentEntryFut, argumentEntry -> Map.entry(entityId, ArgumentEntry.createSingleValueArgument(entityId, argumentEntry)), MoreExecutors.directExecutor()); + }) + .toList(); + + ListenableFuture>> allFutures = Futures.allAsList(futures); + + return Futures.transform(allFutures, + entries -> ArgumentEntry.createAggArgument( + entries.stream().collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)) + ), + MoreExecutors.directExecutor()); + } + + protected ListenableFuture fetchArgumentValue(TenantId tenantId, EntityId entityId, Argument argument, long startTs) { + return switch (argument.getRefEntityKey().getType()) { + case TS_ROLLING -> fetchTsRolling(tenantId, entityId, argument, startTs); + case ATTRIBUTE -> fetchAttribute(tenantId, entityId, argument, startTs); + case TS_LATEST -> fetchTsLatest(tenantId, entityId, argument, startTs); + }; + } + + protected ArgumentEntry fetchMetricDuringInterval(TenantId tenantId, EntityId entityId, String argKey, AggMetric metric, AggIntervalEntry interval) { + AggFunction function = metric.getFunction(); + long intervalMs = interval.getEndTs() - interval.getStartTs(); + BaseReadTsKvQuery query = new BaseReadTsKvQuery(argKey, interval.getStartTs(), interval.getEndTs(), intervalMs, 1, Aggregation.valueOf(function.name())); + ListenableFuture argumentEntryFut = fetchTimeSeriesInternal(tenantId, entityId, query, timeSeries -> transformAggMetricArgument(timeSeries, argKey, metric)); + return resolveArgumentValue(argKey, argumentEntryFut); + } + + private ListenableFuture fetchTimeSeries(TenantId tenantId, EntityId entityId, Argument argument, AggInterval interval, long queryEndTs) { + long intervalStartTs = interval.getCurrentIntervalStartTs(); + long intervalEndTs = interval.getCurrentIntervalEndTs(); + ReadTsKvQuery query = new BaseReadTsKvQuery(argument.getRefEntityKey().getKey(), intervalStartTs, queryEndTs, 0, 1, Aggregation.NONE); + return fetchTimeSeriesInternal(tenantId, entityId, query, timeSeries -> transformAggregationArgument(timeSeries, intervalStartTs, intervalEndTs)); + } + + private ListenableFuture fetchTsRolling(TenantId tenantId, EntityId entityId, Argument argument, long queryEndTs) { + long argTimeWindow = argument.getTimeWindow() == 0 ? queryEndTs : argument.getTimeWindow(); + long startInterval = queryEndTs - argTimeWindow; + ReadTsKvQuery query = buildTsRollingQuery(tenantId, argument, startInterval, queryEndTs); + return fetchTimeSeriesInternal(tenantId, entityId, query, tsRolling -> transformTsRollingArgument(tsRolling, query.getLimit(), argTimeWindow)); + } + + private ListenableFuture fetchAttribute(TenantId tenantId, EntityId entityId, Argument argument, long defaultLastUpdateTs) { + log.trace("[{}][{}] Fetching attribute for key {}", tenantId, entityId, argument.getRefEntityKey()); + var attributeOptFuture = attributesService.find(tenantId, entityId, argument.getRefEntityKey().getScope(), argument.getRefEntityKey().getKey()); + + return Futures.transform(attributeOptFuture, attrOpt -> { + log.debug("[{}][{}] Fetched attribute for key {}: {}", tenantId, entityId, argument.getRefEntityKey(), attrOpt); + AttributeKvEntry attributeKvEntry = attrOpt.orElseGet(() -> new BaseAttributeKvEntry(createDefaultKvEntry(argument), defaultLastUpdateTs, SingleValueArgumentEntry.DEFAULT_VERSION)); + return transformSingleValueArgument(Optional.of(attributeKvEntry)); + }, calculatedFieldCallbackExecutor); + } + + protected ListenableFuture fetchTsLatest(TenantId tenantId, EntityId entityId, Argument argument, long defaultTs) { + String timeseriesKey = argument.getRefEntityKey().getKey(); + log.trace("[{}][{}] Fetching latest timeseries {}", tenantId, entityId, timeseriesKey); + return transformSingleValueArgument( + Futures.transform( + timeseriesService.findLatest(tenantId, entityId, timeseriesKey), + result -> { + log.debug("[{}][{}] Fetched latest timeseries {}: {}", tenantId, entityId, timeseriesKey, result); + return result.or(() -> Optional.of(new BasicTsKvEntry(defaultTs, createDefaultKvEntry(argument), SingleValueArgumentEntry.DEFAULT_VERSION))); + }, calculatedFieldCallbackExecutor)); + } + + private ListenableFuture fetchTimeSeriesInternal(TenantId tenantId, EntityId entityId, ReadTsKvQuery query, Function, ArgumentEntry> transformArgument) { + log.trace("[{}][{}] Fetching timeseries for query {}", tenantId, entityId, query); + ListenableFuture> tsRollingFuture = timeseriesService.findAll(tenantId, entityId, List.of(query)); + return Futures.transform(tsRollingFuture, tsRolling -> { + log.debug("[{}][{}] Fetched {} timeseries for query {}", tenantId, entityId, tsRolling == null ? 0 : tsRolling.size(), query); + return transformArgument.apply(tsRolling); + }, calculatedFieldCallbackExecutor); + } + + private ReadTsKvQuery buildTsRollingQuery(TenantId tenantId, Argument argument, long startTs, long endTs) { + long maxDataPoints = apiLimitService.getLimit( + tenantId, DefaultTenantProfileConfiguration::getMaxDataPointsPerRollingArg); + int argumentLimit = argument.getLimit(); + int limit = argumentLimit == 0 || argumentLimit > maxDataPoints ? (int) maxDataPoints : argumentLimit; + return new BaseReadTsKvQuery(argument.getRefEntityKey().getKey(), startTs, endTs, 0, limit, Aggregation.NONE); + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/cf/AbstractCalculatedFieldStateService.java b/application/src/main/java/org/thingsboard/server/service/cf/AbstractCalculatedFieldStateService.java index 70b41f069e..e673577742 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/AbstractCalculatedFieldStateService.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/AbstractCalculatedFieldStateService.java @@ -15,10 +15,15 @@ */ package org.thingsboard.server.service.cf; +import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Lazy; import org.thingsboard.server.actors.ActorSystemContext; import org.thingsboard.server.actors.calculatedField.CalculatedFieldStateRestoreMsg; +import org.thingsboard.server.common.data.DataConstants; +import org.thingsboard.server.common.data.exception.TenantNotFoundException; +import org.thingsboard.server.common.msg.CalculatedFieldStatePartitionRestoreMsg; +import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.TbCallback; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.exception.CalculatedFieldStateException; @@ -37,6 +42,7 @@ import java.util.stream.Collectors; import static org.thingsboard.server.utils.CalculatedFieldUtils.fromProto; import static org.thingsboard.server.utils.CalculatedFieldUtils.toProto; +@Slf4j public abstract class AbstractCalculatedFieldStateService implements CalculatedFieldStateService { @Autowired @@ -56,25 +62,44 @@ public abstract class AbstractCalculatedFieldStateService implements CalculatedF protected abstract void doPersist(CalculatedFieldEntityCtxId stateId, CalculatedFieldStateProto stateMsgProto, TbCallback callback); @Override - public final void removeState(CalculatedFieldEntityCtxId stateId, TbCallback callback) { + public final void deleteState(CalculatedFieldEntityCtxId stateId, TbCallback callback) { doRemove(stateId, callback); } protected abstract void doRemove(CalculatedFieldEntityCtxId stateId, TbCallback callback); - protected void processRestoredState(CalculatedFieldStateProto stateMsg) { + protected void processRestoredState(CalculatedFieldStateProto stateMsg, TopicPartitionInfo partition) { var id = fromProto(stateMsg.getId()); - var state = fromProto(stateMsg); - processRestoredState(id, state); + if (partition == null) { + try { + partition = actorSystemContext.resolve(ServiceType.TB_RULE_ENGINE, DataConstants.CF_QUEUE_NAME, id.tenantId(), id.entityId()); + } catch (TenantNotFoundException e) { + log.debug("Skipping CF state msg for non-existing tenant {}", id.tenantId()); + return; + } + } + var state = fromProto(id, stateMsg); + processRestoredState(id, state, partition); } - protected void processRestoredState(CalculatedFieldEntityCtxId id, CalculatedFieldState state) { - actorSystemContext.tell(new CalculatedFieldStateRestoreMsg(id, state)); + protected void processRestoredState(CalculatedFieldEntityCtxId id, CalculatedFieldState state, TopicPartitionInfo partition) { + partition = partition.withTopic(DataConstants.CF_STATES_QUEUE_NAME); + actorSystemContext.tell(new CalculatedFieldStateRestoreMsg(id, state, partition)); } @Override public void restore(QueueKey queueKey, Set partitions) { - stateService.update(queueKey, partitions, null); + stateService.update(queueKey, partitions, new QueueStateService.RestoreCallback() { + @Override + public void onAllPartitionsRestored() { + } + + @Override + public void onPartitionRestored(TopicPartitionInfo partition) { + partition = partition.withTopic(DataConstants.CF_STATES_QUEUE_NAME); + actorSystemContext.tellWithHighPriority(new CalculatedFieldStatePartitionRestoreMsg(partition)); + } + }); } @Override diff --git a/application/src/main/java/org/thingsboard/server/service/cf/AlarmCalculatedFieldResult.java b/application/src/main/java/org/thingsboard/server/service/cf/AlarmCalculatedFieldResult.java new file mode 100644 index 0000000000..498a215e17 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/cf/AlarmCalculatedFieldResult.java @@ -0,0 +1,82 @@ +/** + * Copyright © 2016-2025 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.cf; + +import lombok.Builder; +import lombok.Data; +import lombok.RequiredArgsConstructor; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.rule.engine.action.TbAlarmResult; +import org.thingsboard.server.common.data.DataConstants; +import org.thingsboard.server.common.data.id.CalculatedFieldId; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.msg.TbMsgType; +import org.thingsboard.server.common.msg.TbMsg; +import org.thingsboard.server.common.msg.TbMsgMetaData; + +import java.util.List; + +@Data +@Builder +@RequiredArgsConstructor +public class AlarmCalculatedFieldResult implements CalculatedFieldResult { + + private final TbAlarmResult alarmResult; + + @Override + public TbMsg toTbMsg(EntityId entityId, List cfIds) { + TbMsgType msgType; + TbMsgMetaData metaData = new TbMsgMetaData(); + if (alarmResult.isCreated()) { + msgType = TbMsgType.ALARM_CREATED; + metaData.putValue(DataConstants.IS_NEW_ALARM, Boolean.TRUE.toString()); + } else if (alarmResult.isUpdated()) { + msgType = TbMsgType.ALARM_UPDATED; + metaData.putValue(DataConstants.IS_EXISTING_ALARM, Boolean.TRUE.toString()); + } else if (alarmResult.isSeverityUpdated()) { + msgType = TbMsgType.ALARM_SEVERITY_UPDATED; + metaData.putValue(DataConstants.IS_EXISTING_ALARM, Boolean.TRUE.toString()); + metaData.putValue(DataConstants.IS_SEVERITY_UPDATED_ALARM, Boolean.TRUE.toString()); + } else { + msgType = TbMsgType.ALARM_CLEAR; + metaData.putValue(DataConstants.IS_CLEARED_ALARM, Boolean.TRUE.toString()); + } + if (alarmResult.getConditionRepeats() != null) { + metaData.putValue(DataConstants.ALARM_CONDITION_REPEATS, String.valueOf(alarmResult.getConditionRepeats())); + } + if (alarmResult.getConditionDuration() != null) { + metaData.putValue(DataConstants.ALARM_CONDITION_DURATION, String.valueOf(alarmResult.getConditionDuration())); + } + + return TbMsg.newMsg() + .type(msgType) + .originator(entityId) + .data(JacksonUtil.toString(alarmResult.getAlarm())) + .metaData(metaData) + .build(); + } + + @Override + public String stringValue() { + return alarmResult != null ? JacksonUtil.toString(alarmResult) : null; + } + + @Override + public boolean isEmpty() { + return alarmResult == null; + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/cf/CalculatedFieldCache.java b/application/src/main/java/org/thingsboard/server/service/cf/CalculatedFieldCache.java index fb63432fed..54ddedcc42 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/CalculatedFieldCache.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/CalculatedFieldCache.java @@ -17,12 +17,17 @@ package org.thingsboard.server.service.cf; import org.thingsboard.server.common.data.cf.CalculatedField; import org.thingsboard.server.common.data.cf.CalculatedFieldLink; +import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.id.CalculatedFieldId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.TenantProfileId; import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx; import java.util.List; +import java.util.Set; +import java.util.function.Predicate; +import java.util.stream.Stream; public interface CalculatedFieldCache { @@ -36,10 +41,28 @@ public interface CalculatedFieldCache { List getCalculatedFieldCtxsByEntityId(EntityId entityId); + Stream getCalculatedFieldCtxsByType(CalculatedFieldType cfType); + + boolean hasCalculatedFields(TenantId tenantId, EntityId entityId, Predicate filter); + void addCalculatedField(TenantId tenantId, CalculatedFieldId calculatedFieldId); void updateCalculatedField(TenantId tenantId, CalculatedFieldId calculatedFieldId); void evict(CalculatedFieldId calculatedFieldId); + void handleTenantProfileUpdate(TenantProfileId tenantProfileId); + + EntityId getProfileId(TenantId tenantId, EntityId entityId); + + Set getDynamicEntities(TenantId tenantId, EntityId entityId); + + void updateOwnerEntity(TenantId tenantId, EntityId entityId); + + void addOwnerEntity(TenantId tenantId, EntityId entityId); + + void evictEntity(EntityId entityId); + + void evictOwner(EntityId owner); + } diff --git a/application/src/main/java/org/thingsboard/server/service/cf/CalculatedFieldProcessingService.java b/application/src/main/java/org/thingsboard/server/service/cf/CalculatedFieldProcessingService.java index 847caccaff..804f94341b 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/CalculatedFieldProcessingService.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/CalculatedFieldProcessingService.java @@ -18,6 +18,7 @@ package org.thingsboard.server.service.cf; import com.google.common.util.concurrent.ListenableFuture; import org.thingsboard.server.actors.calculatedField.CalculatedFieldTelemetryMsg; import org.thingsboard.server.common.data.cf.configuration.Argument; +import org.thingsboard.server.common.data.cf.configuration.aggregation.AggMetric; import org.thingsboard.server.common.data.id.CalculatedFieldId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; @@ -25,18 +26,24 @@ import org.thingsboard.server.common.msg.queue.TbCallback; import org.thingsboard.server.service.cf.ctx.CalculatedFieldEntityCtxId; import org.thingsboard.server.service.cf.ctx.state.ArgumentEntry; import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx; -import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldState; +import org.thingsboard.server.service.cf.ctx.state.aggregation.single.AggIntervalEntry; import java.util.List; import java.util.Map; public interface CalculatedFieldProcessingService { - ListenableFuture fetchStateFromDb(CalculatedFieldCtx ctx, EntityId entityId); + ListenableFuture> fetchArguments(CalculatedFieldCtx ctx, EntityId entityId); + + Map fetchDynamicArgsFromDb(CalculatedFieldCtx ctx, EntityId entityId); + + List fetchRelatedEntities(CalculatedFieldCtx ctx, EntityId entityId); Map fetchArgsFromDb(TenantId tenantId, EntityId entityId, Map arguments); - void pushMsgToRuleEngine(TenantId tenantId, EntityId entityId, CalculatedFieldResult calculationResult, List cfIds, TbCallback callback); + ArgumentEntry fetchMetricDuringInterval(TenantId tenantId, EntityId entityId, String argKey, AggMetric metric, AggIntervalEntry interval); + + void pushMsgToRuleEngine(TenantId tenantId, EntityId entityId, CalculatedFieldResult result, List cfIds, TbCallback callback); void pushMsgToLinks(CalculatedFieldTelemetryMsg msg, List linkedCalculatedFields, TbCallback callback); diff --git a/application/src/main/java/org/thingsboard/server/service/cf/CalculatedFieldResult.java b/application/src/main/java/org/thingsboard/server/service/cf/CalculatedFieldResult.java index 49acf6917c..c62d5dc6d5 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/CalculatedFieldResult.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/CalculatedFieldResult.java @@ -15,23 +15,18 @@ */ package org.thingsboard.server.service.cf; -import com.fasterxml.jackson.databind.JsonNode; -import lombok.Data; -import org.thingsboard.server.common.data.AttributeScope; -import org.thingsboard.server.common.data.cf.configuration.OutputType; +import org.thingsboard.server.common.data.id.CalculatedFieldId; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.msg.TbMsg; -@Data -public final class CalculatedFieldResult { +import java.util.List; - private final OutputType type; - private final AttributeScope scope; - private final JsonNode result; +public interface CalculatedFieldResult { - public boolean isEmpty() { - return result == null || result.isMissingNode() || result.isNull() || - (result.isObject() && result.isEmpty()) || - (result.isArray() && result.isEmpty()) || - (result.isTextual() && result.asText().isEmpty()); - } + TbMsg toTbMsg(EntityId entityId, List cfIds); + + String stringValue(); + + boolean isEmpty(); } diff --git a/application/src/main/java/org/thingsboard/server/service/cf/CalculatedFieldStateService.java b/application/src/main/java/org/thingsboard/server/service/cf/CalculatedFieldStateService.java index d0b34f18e8..10276ac421 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/CalculatedFieldStateService.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/CalculatedFieldStateService.java @@ -33,7 +33,7 @@ public interface CalculatedFieldStateService { void persistState(CalculatedFieldEntityCtxId stateId, CalculatedFieldState state, TbCallback callback) throws CalculatedFieldStateException; - void removeState(CalculatedFieldEntityCtxId stateId, TbCallback callback); + void deleteState(CalculatedFieldEntityCtxId stateId, TbCallback callback); void restore(QueueKey queueKey, Set partitions); diff --git a/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldCache.java b/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldCache.java index 95fa7aebb1..f2b8d4d9db 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldCache.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldCache.java @@ -22,29 +22,37 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; import org.springframework.util.ConcurrentReferenceHashMap; -import org.thingsboard.script.api.tbel.TbelInvokeService; import org.thingsboard.server.actors.ActorSystemContext; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.cf.CalculatedField; import org.thingsboard.server.common.data.cf.CalculatedFieldLink; +import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.cf.configuration.CalculatedFieldConfiguration; +import org.thingsboard.server.common.data.id.AssetId; import org.thingsboard.server.common.data.id.CalculatedFieldId; +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.id.TenantProfileId; import org.thingsboard.server.common.data.page.PageDataIterable; -import org.thingsboard.server.common.msg.cf.CalculatedFieldInitMsg; -import org.thingsboard.server.common.msg.cf.CalculatedFieldLinkInitMsg; import org.thingsboard.server.dao.cf.CalculatedFieldService; -import org.thingsboard.server.dao.usagerecord.ApiLimitService; +import org.thingsboard.server.dao.tenant.TbTenantProfileCache; import org.thingsboard.server.queue.util.AfterStartUp; import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx; +import org.thingsboard.server.service.profile.TbAssetProfileCache; +import org.thingsboard.server.service.profile.TbDeviceProfileCache; import java.util.Collections; import java.util.List; +import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; +import java.util.function.Predicate; +import java.util.stream.Stream; @Service @Slf4j @@ -54,10 +62,12 @@ public class DefaultCalculatedFieldCache implements CalculatedFieldCache { private final ConcurrentReferenceHashMap calculatedFieldFetchLocks = new ConcurrentReferenceHashMap<>(); private final CalculatedFieldService calculatedFieldService; - private final TbelInvokeService tbelInvokeService; - private final ApiLimitService apiLimitService; + private final TbAssetProfileCache assetProfileCache; + private final TbDeviceProfileCache deviceProfileCache; + private final TbTenantProfileCache tenantProfileCache; @Lazy - private final ActorSystemContext actorSystemContext; + private final ActorSystemContext systemContext; + private final OwnerService ownerService; private final ConcurrentMap calculatedFields = new ConcurrentHashMap<>(); private final ConcurrentMap> entityIdCalculatedFields = new ConcurrentHashMap<>(); @@ -65,6 +75,8 @@ public class DefaultCalculatedFieldCache implements CalculatedFieldCache { private final ConcurrentMap> entityIdCalculatedFieldLinks = new ConcurrentHashMap<>(); private final ConcurrentMap calculatedFieldsCtx = new ConcurrentHashMap<>(); + private final ConcurrentMap> ownerEntities = new ConcurrentHashMap<>(); + @Value("${queue.calculated_fields.init_fetch_pack_size:50000}") @Getter private int initFetchPackSize; @@ -75,21 +87,17 @@ public class DefaultCalculatedFieldCache implements CalculatedFieldCache { cfs.forEach(cf -> { if (cf != null) { calculatedFields.putIfAbsent(cf.getId(), cf); - actorSystemContext.tell(new CalculatedFieldInitMsg(cf.getTenantId(), cf)); + List links = cf.getConfiguration().buildCalculatedFieldLinks(cf.getTenantId(), cf.getEntityId(), cf.getId()); + calculatedFieldLinks.put(cf.getId(), new CopyOnWriteArrayList<>(links)); } }); calculatedFields.values().forEach(cf -> { entityIdCalculatedFields.computeIfAbsent(cf.getEntityId(), id -> new CopyOnWriteArrayList<>()).add(cf); }); - PageDataIterable cfls = new PageDataIterable<>(calculatedFieldService::findAllCalculatedFieldLinks, initFetchPackSize); - cfls.forEach(link -> { - calculatedFieldLinks.computeIfAbsent(link.getCalculatedFieldId(), id -> new CopyOnWriteArrayList<>()).add(link); - actorSystemContext.tell(new CalculatedFieldLinkInitMsg(link.getTenantId(), link)); - }); calculatedFieldLinks.values().stream() .flatMap(List::stream) .forEach(link -> - entityIdCalculatedFieldLinks.computeIfAbsent(link.getEntityId(), id -> new CopyOnWriteArrayList<>()).add(link) + entityIdCalculatedFieldLinks.computeIfAbsent(link.entityId(), id -> new CopyOnWriteArrayList<>()).add(link) ); } @@ -119,7 +127,7 @@ public class DefaultCalculatedFieldCache implements CalculatedFieldCache { if (ctx == null) { CalculatedField calculatedField = getCalculatedField(calculatedFieldId); if (calculatedField != null) { - ctx = new CalculatedFieldCtx(calculatedField, tbelInvokeService, apiLimitService); + ctx = new CalculatedFieldCtx(calculatedField, systemContext); calculatedFieldsCtx.put(calculatedFieldId, ctx); log.debug("[{}] Put calculated field ctx into cache: {}", calculatedFieldId, ctx); } @@ -142,6 +150,38 @@ public class DefaultCalculatedFieldCache implements CalculatedFieldCache { .toList(); } + @Override + public Stream getCalculatedFieldCtxsByType(CalculatedFieldType cfType) { + return calculatedFields.values().stream() + .filter(cf -> cfType.equals(cf.getType())) + .map(cf -> getCalculatedFieldCtx(cf.getId())); + } + + @Override + public boolean hasCalculatedFields(TenantId tenantId, EntityId entityId, Predicate filter) { + List entityCfs = getCalculatedFieldCtxsByEntityId(entityId); + for (CalculatedFieldCtx ctx : entityCfs) { + if (filter.test(ctx)) { + return true; + } + } + + return hasCalculatedFieldsByProfile(tenantId, entityId, filter); + } + + public boolean hasCalculatedFieldsByProfile(TenantId tenantId, EntityId entityId, Predicate filter) { + EntityId profileId = getProfileId(tenantId, entityId); + if (profileId != null) { + List profileCfs = getCalculatedFieldCtxsByEntityId(profileId); + for (CalculatedFieldCtx ctx : profileCfs) { + if (filter.test(ctx)) { + return true; + } + } + } + return false; + } + @Override public void addCalculatedField(TenantId tenantId, CalculatedFieldId calculatedFieldId) { Lock lock = getFetchLock(calculatedFieldId); @@ -187,10 +227,67 @@ public class DefaultCalculatedFieldCache implements CalculatedFieldCache { log.debug("[{}] evict calculated field links from cache: {}", calculatedFieldId, oldCalculatedField); calculatedFieldsCtx.remove(calculatedFieldId); log.debug("[{}] evict calculated field ctx from cache: {}", calculatedFieldId, oldCalculatedField); - entityIdCalculatedFieldLinks.forEach((entityId, calculatedFieldLinks) -> calculatedFieldLinks.removeIf(link -> link.getCalculatedFieldId().equals(calculatedFieldId))); + entityIdCalculatedFieldLinks.forEach((entityId, calculatedFieldLinks) -> calculatedFieldLinks.removeIf(link -> link.calculatedFieldId().equals(calculatedFieldId))); log.debug("[{}] evict calculated field links from cached links by entity id: {}", calculatedFieldId, oldCalculatedField); } + @Override + public void handleTenantProfileUpdate(TenantProfileId tenantProfileId) { + calculatedFieldsCtx.values().stream() + .filter(ctx -> { + TenantProfile tenantProfile = tenantProfileCache.get(ctx.getTenantId()); + return tenantProfile != null && tenantProfileId.equals(tenantProfile.getId()); + }) + .forEach(CalculatedFieldCtx::updateTenantProfileProperties); + } + + @Override + public EntityId getProfileId(TenantId tenantId, EntityId entityId) { + return switch (entityId.getEntityType()) { + case ASSET -> assetProfileCache.get(tenantId, (AssetId) entityId).getId(); + case DEVICE -> deviceProfileCache.get(tenantId, (DeviceId) entityId).getId(); + default -> null; + }; + } + + @Override + public Set getDynamicEntities(TenantId tenantId, EntityId entityId) { + if (entityId != null && entityId.getEntityType().isOneOf(EntityType.CUSTOMER, EntityType.TENANT)) { + return getOwnedEntities(tenantId, entityId); + } + return Collections.emptySet(); + } + + @Override + public void addOwnerEntity(TenantId tenantId, EntityId entityId) { + EntityId owner = ownerService.getOwner(tenantId, entityId); + getOwnedEntities(tenantId, owner).add(entityId); + } + + @Override + public void updateOwnerEntity(TenantId tenantId, EntityId entityId) { + evictEntity(entityId); + addOwnerEntity(tenantId, entityId); + } + + @Override + public void evictEntity(EntityId entityId) { + ownerEntities.values().forEach(entities -> entities.remove(entityId)); + } + + @Override + public void evictOwner(EntityId owner) { + ownerEntities.remove(owner); + } + + private Set getOwnedEntities(TenantId tenantId, EntityId ownerId) { + return ownerEntities.computeIfAbsent(ownerId, owner -> { + Set entities = ConcurrentHashMap.newKeySet(); + entities.addAll(ownerService.getOwnedEntities(tenantId, ownerId)); + return entities; + }); + } + private Lock getFetchLock(CalculatedFieldId id) { return calculatedFieldFetchLocks.computeIfAbsent(id, __ -> new ReentrantLock()); } diff --git a/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldProcessingService.java b/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldProcessingService.java index f2a6916751..9033b21fd4 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldProcessingService.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldProcessingService.java @@ -15,46 +15,26 @@ */ package org.thingsboard.server.service.cf; -import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; -import com.google.common.util.concurrent.ListeningExecutorService; -import com.google.common.util.concurrent.MoreExecutors; -import jakarta.annotation.PostConstruct; -import jakarta.annotation.PreDestroy; -import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang3.math.NumberUtils; import org.springframework.stereotype.Service; -import org.thingsboard.common.util.ThingsBoardExecutors; import org.thingsboard.server.actors.calculatedField.CalculatedFieldTelemetryMsg; import org.thingsboard.server.actors.calculatedField.MultipleTbCallback; import org.thingsboard.server.cluster.TbClusterService; import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.EntityType; -import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.cf.configuration.Argument; -import org.thingsboard.server.common.data.cf.configuration.OutputType; +import org.thingsboard.server.common.data.cf.configuration.aggregation.AggMetric; +import org.thingsboard.server.common.data.cf.configuration.aggregation.RelatedEntitiesAggregationCalculatedFieldConfiguration; import org.thingsboard.server.common.data.id.CalculatedFieldId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.kv.Aggregation; -import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry; -import org.thingsboard.server.common.data.kv.BaseReadTsKvQuery; -import org.thingsboard.server.common.data.kv.BasicTsKvEntry; -import org.thingsboard.server.common.data.kv.BooleanDataEntry; -import org.thingsboard.server.common.data.kv.DoubleDataEntry; -import org.thingsboard.server.common.data.kv.KvEntry; -import org.thingsboard.server.common.data.kv.ReadTsKvQuery; -import org.thingsboard.server.common.data.kv.StringDataEntry; -import org.thingsboard.server.common.data.kv.TsKvEntry; -import org.thingsboard.server.common.data.msg.TbMsgType; -import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; import org.thingsboard.server.common.msg.TbMsg; -import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.TbCallback; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.dao.attributes.AttributesService; +import org.thingsboard.server.dao.relation.RelationService; import org.thingsboard.server.dao.timeseries.TimeseriesService; import org.thingsboard.server.dao.usagerecord.ApiLimitService; import org.thingsboard.server.gen.transport.TransportProtos.CalculatedFieldLinkedTelemetryMsgProto; @@ -69,112 +49,121 @@ import org.thingsboard.server.queue.util.TbRuleEngineComponent; import org.thingsboard.server.service.cf.ctx.CalculatedFieldEntityCtxId; import org.thingsboard.server.service.cf.ctx.state.ArgumentEntry; import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx; -import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldState; -import org.thingsboard.server.service.cf.ctx.state.ScriptCalculatedFieldState; -import org.thingsboard.server.service.cf.ctx.state.SimpleCalculatedFieldState; -import org.thingsboard.server.service.cf.ctx.state.SingleValueArgumentEntry; -import org.thingsboard.server.service.cf.ctx.state.TsRollingArgumentEntry; +import org.thingsboard.server.service.cf.ctx.state.aggregation.single.AggIntervalEntry; import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Map.Entry; -import java.util.Optional; import java.util.UUID; import java.util.concurrent.ExecutionException; -import java.util.stream.Collectors; -import static org.thingsboard.server.common.data.DataConstants.SCOPE; +import static org.thingsboard.server.common.data.cf.configuration.PropagationCalculatedFieldConfiguration.PROPAGATION_CONFIG_ARGUMENT; import static org.thingsboard.server.utils.CalculatedFieldUtils.toProto; @TbRuleEngineComponent @Service @Slf4j -@RequiredArgsConstructor -public class DefaultCalculatedFieldProcessingService implements CalculatedFieldProcessingService { +public class DefaultCalculatedFieldProcessingService extends AbstractCalculatedFieldProcessingService implements CalculatedFieldProcessingService { - private final AttributesService attributesService; - private final TimeseriesService timeseriesService; private final TbClusterService clusterService; - private final ApiLimitService apiLimitService; private final PartitionService partitionService; - private ListeningExecutorService calculatedFieldCallbackExecutor; + public DefaultCalculatedFieldProcessingService(AttributesService attributesService, + TimeseriesService timeseriesService, + ApiLimitService apiLimitService, + RelationService relationService, + OwnerService ownerService, + TbClusterService clusterService, + PartitionService partitionService) { + super(attributesService, timeseriesService, apiLimitService, relationService, ownerService); + this.clusterService = clusterService; + this.partitionService = partitionService; + } - @PostConstruct - public void init() { - calculatedFieldCallbackExecutor = MoreExecutors.listeningDecorator(ThingsBoardExecutors.newWorkStealingPool( - Math.max(4, Runtime.getRuntime().availableProcessors()), "calculated-field-callback")); + @Override + protected String getExecutorNamePrefix() { + return "calculated-field-callback"; } - @PreDestroy - public void stop() { - if (calculatedFieldCallbackExecutor != null) { - calculatedFieldCallbackExecutor.shutdownNow(); - } + @Override + public ListenableFuture> fetchArguments(CalculatedFieldCtx ctx, EntityId entityId) { + return super.fetchArguments(ctx, entityId, System.currentTimeMillis()); } @Override - public ListenableFuture fetchStateFromDb(CalculatedFieldCtx ctx, EntityId entityId) { - Map> argFutures = new HashMap<>(); - for (var entry : ctx.getArguments().entrySet()) { - var argEntityId = entry.getValue().getRefEntityId() != null ? entry.getValue().getRefEntityId() : entityId; - var argValueFuture = fetchKvEntry(ctx.getTenantId(), argEntityId, entry.getValue()); - argFutures.put(entry.getKey(), argValueFuture); + public Map fetchDynamicArgsFromDb(CalculatedFieldCtx ctx, EntityId entityId) { + return switch (ctx.getCfType()) { + case GEOFENCING -> resolveArgumentFutures(fetchGeofencingCalculatedFieldArguments(ctx, entityId, true, System.currentTimeMillis())); + case PROPAGATION -> resolveArgumentFutures(Map.of(PROPAGATION_CONFIG_ARGUMENT, fetchPropagationCalculatedFieldArgument(ctx, entityId))); + default -> Collections.emptyMap(); + }; + } + + @Override + public List fetchRelatedEntities(CalculatedFieldCtx ctx, EntityId entityId) { + try { + if (ctx.getCalculatedField().getConfiguration() instanceof RelatedEntitiesAggregationCalculatedFieldConfiguration config) { + return resolveRelatedEntities(ctx.getTenantId(), entityId, config.getRelation()).get(); + } + return Collections.emptyList(); + } catch (ExecutionException | InterruptedException e) { + Throwable cause = e.getCause(); + throw new RuntimeException("Failed to fetch related entities for entity [" + entityId + "]: " + cause.getMessage(), cause); } - return Futures.whenAllComplete(argFutures.values()).call(() -> { - var result = createStateByType(ctx); - result.updateState(ctx, argFutures.entrySet().stream() - .collect(Collectors.toMap( - Entry::getKey, // Keep the key as is - entry -> { - try { - // Resolve the future to get the value - return entry.getValue().get(); - } catch (ExecutionException | InterruptedException e) { - throw new RuntimeException("Error getting future result for key: " + entry.getKey(), e); - } - } - ))); - return result; - }, calculatedFieldCallbackExecutor); } @Override public Map fetchArgsFromDb(TenantId tenantId, EntityId entityId, Map arguments) { Map> argFutures = new HashMap<>(); for (var entry : arguments.entrySet()) { - var argEntityId = entry.getValue().getRefEntityId() != null ? entry.getValue().getRefEntityId() : entityId; - var argValueFuture = fetchKvEntry(tenantId, argEntityId, entry.getValue()); + if (entry.getValue().hasRelationQuerySource()) { + continue; + } + var argEntityId = resolveEntityId(tenantId, entityId, entry.getValue()); + var argValueFuture = fetchArgumentValue(tenantId, argEntityId, entry.getValue(), System.currentTimeMillis()); argFutures.put(entry.getKey(), argValueFuture); } - return argFutures.entrySet().stream() - .collect(Collectors.toMap( - Entry::getKey, // Keep the key as is - entry -> { - try { - // Resolve the future to get the value - return entry.getValue().get(); - } catch (ExecutionException | InterruptedException e) { - throw new RuntimeException("Error getting future result for key: " + entry.getKey(), e); - } - } - )); + return resolveArgumentFutures(argFutures); + } + + @Override + public ArgumentEntry fetchMetricDuringInterval(TenantId tenantId, EntityId entityId, String argKey, AggMetric metric, AggIntervalEntry interval) { + return super.fetchMetricDuringInterval(tenantId, entityId, argKey, metric, interval); } @Override - public void pushMsgToRuleEngine(TenantId tenantId, EntityId entityId, CalculatedFieldResult calculatedFieldResult, List cfIds, TbCallback callback) { + public void pushMsgToRuleEngine(TenantId tenantId, EntityId entityId, CalculatedFieldResult result, List cfIds, TbCallback callback) { + if (!(result instanceof PropagationCalculatedFieldResult propagationCalculatedFieldResult)) { + TbMsg msg = result.toTbMsg(entityId, cfIds); + sendMsgToRuleEngine(tenantId, entityId, callback, msg); + return; + } + List propagationEntityIds = propagationCalculatedFieldResult.getPropagationEntityIds(); + if (propagationEntityIds.isEmpty()) { + callback.onSuccess(); + } + if (propagationEntityIds.size() == 1) { + EntityId propagationEntityId = propagationEntityIds.get(0); + TbMsg msg = result.toTbMsg(propagationEntityId, cfIds); + sendMsgToRuleEngine(tenantId, propagationEntityId, callback, msg); + return; + } + MultipleTbCallback multipleTbCallback = new MultipleTbCallback(propagationEntityIds.size(), callback); + for (var propagationEntityId : propagationEntityIds) { + TbMsg msg = result.toTbMsg(propagationEntityId, cfIds); + sendMsgToRuleEngine(tenantId, propagationEntityId, multipleTbCallback, msg); + } + } + + private void sendMsgToRuleEngine(TenantId tenantId, EntityId entityId, TbCallback callback, TbMsg msg) { try { - OutputType type = calculatedFieldResult.getType(); - TbMsgType msgType = OutputType.ATTRIBUTES.equals(type) ? TbMsgType.POST_ATTRIBUTES_REQUEST : TbMsgType.POST_TELEMETRY_REQUEST; - TbMsgMetaData md = OutputType.ATTRIBUTES.equals(type) ? new TbMsgMetaData(Map.of(SCOPE, calculatedFieldResult.getScope().name())) : TbMsgMetaData.EMPTY; - TbMsg msg = TbMsg.newMsg().type(msgType).originator(entityId).previousCalculatedFieldIds(cfIds).metaData(md).data(calculatedFieldResult.getResult().toString()).build(); clusterService.pushMsgToRuleEngine(tenantId, entityId, msg, new TbQueueCallback() { @Override public void onSuccess(TbQueueMsgMetadata metadata) { - callback.onSuccess(); log.trace("[{}][{}] Pushed message to rule engine: {} ", tenantId, entityId, msg); + callback.onSuccess(); } @Override @@ -183,7 +172,7 @@ public class DefaultCalculatedFieldProcessingService implements CalculatedFieldP } }); } catch (Exception e) { - log.warn("[{}][{}] Failed to push message to rule engine. CalculatedFieldResult: {}", tenantId, entityId, calculatedFieldResult, e); + log.warn("[{}][{}] Failed to push message to rule engine: {}", tenantId, entityId, msg, e); callback.onFailure(e); } } @@ -241,69 +230,6 @@ public class DefaultCalculatedFieldProcessingService implements CalculatedFieldP return builder.build(); } - private ListenableFuture fetchKvEntry(TenantId tenantId, EntityId entityId, Argument argument) { - return switch (argument.getRefEntityKey().getType()) { - case TS_ROLLING -> fetchTsRolling(tenantId, entityId, argument); - case ATTRIBUTE -> transformSingleValueArgument( - Futures.transform( - attributesService.find(tenantId, entityId, argument.getRefEntityKey().getScope(), argument.getRefEntityKey().getKey()), - result -> result.or(() -> Optional.of(new BaseAttributeKvEntry(createDefaultKvEntry(argument), System.currentTimeMillis(), 0L))), - calculatedFieldCallbackExecutor) - ); - case TS_LATEST -> transformSingleValueArgument( - Futures.transform( - timeseriesService.findLatest(tenantId, entityId, argument.getRefEntityKey().getKey()), - result -> result.or(() -> Optional.of(new BasicTsKvEntry(System.currentTimeMillis(), createDefaultKvEntry(argument), 0L))), - calculatedFieldCallbackExecutor)); - }; - } - - private ListenableFuture transformSingleValueArgument(ListenableFuture> kvEntryFuture) { - return Futures.transform(kvEntryFuture, kvEntry -> { - if (kvEntry.isPresent() && kvEntry.get().getValue() != null) { - return ArgumentEntry.createSingleValueArgument(kvEntry.get()); - } else { - return new SingleValueArgumentEntry(); - } - }, calculatedFieldCallbackExecutor); - } - - private ListenableFuture fetchTsRolling(TenantId tenantId, EntityId entityId, Argument argument) { - long currentTime = System.currentTimeMillis(); - long timeWindow = argument.getTimeWindow() == 0 ? System.currentTimeMillis() : argument.getTimeWindow(); - long startTs = currentTime - timeWindow; - long maxDataPoints = apiLimitService.getLimit(tenantId, DefaultTenantProfileConfiguration::getMaxDataPointsPerRollingArg); - int argumentLimit = argument.getLimit(); - int limit = argumentLimit == 0 || argumentLimit > maxDataPoints ? (int) maxDataPoints : argument.getLimit(); - - ReadTsKvQuery query = new BaseReadTsKvQuery(argument.getRefEntityKey().getKey(), startTs, currentTime, 0, limit, Aggregation.NONE); - ListenableFuture> tsRollingFuture = timeseriesService.findAll(tenantId, entityId, List.of(query)); - - return Futures.transform(tsRollingFuture, tsRolling -> tsRolling == null ? new TsRollingArgumentEntry(limit, timeWindow) : ArgumentEntry.createTsRollingArgument(tsRolling, limit, timeWindow), calculatedFieldCallbackExecutor); - } - - private KvEntry createDefaultKvEntry(Argument argument) { - String key = argument.getRefEntityKey().getKey(); - String defaultValue = argument.getDefaultValue(); - if (StringUtils.isBlank(defaultValue)) { - return new StringDataEntry(key, null); - } - if (NumberUtils.isParsable(defaultValue)) { - return new DoubleDataEntry(key, Double.parseDouble(defaultValue)); - } - if ("true".equalsIgnoreCase(defaultValue) || "false".equalsIgnoreCase(defaultValue)) { - return new BooleanDataEntry(key, Boolean.parseBoolean(defaultValue)); - } - return new StringDataEntry(key, defaultValue); - } - - private CalculatedFieldState createStateByType(CalculatedFieldCtx ctx) { - return switch (ctx.getCfType()) { - case SIMPLE -> new SimpleCalculatedFieldState(ctx.getArgNames()); - case SCRIPT -> new ScriptCalculatedFieldState(ctx.getArgNames()); - }; - } - private static class TbCallbackWrapper implements TbQueueCallback { private final TbCallback callback; @@ -320,6 +246,7 @@ public class DefaultCalculatedFieldProcessingService implements CalculatedFieldP public void onFailure(Throwable t) { callback.onFailure(t); } + } } diff --git a/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldQueueService.java b/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldQueueService.java index fc5d75be56..5af6b542c1 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldQueueService.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldQueueService.java @@ -25,10 +25,11 @@ import org.thingsboard.rule.engine.api.TimeseriesDeleteRequest; import org.thingsboard.rule.engine.api.TimeseriesSaveRequest; import org.thingsboard.server.cluster.TbClusterService; import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.cf.CalculatedField; import org.thingsboard.server.common.data.cf.CalculatedFieldLink; -import org.thingsboard.server.common.data.id.AssetId; +import org.thingsboard.server.common.data.cf.CalculatedFieldType; +import org.thingsboard.server.common.data.cf.configuration.aggregation.RelatedEntitiesAggregationCalculatedFieldConfiguration; import org.thingsboard.server.common.data.id.CalculatedFieldId; -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.kv.AttributeKvEntry; @@ -36,7 +37,12 @@ import org.thingsboard.server.common.data.kv.AttributesSaveResult; import org.thingsboard.server.common.data.kv.TimeseriesSaveResult; import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.common.data.msg.TbMsgType; +import org.thingsboard.server.common.data.relation.EntityRelation; +import org.thingsboard.server.common.data.relation.EntityRelationPathQuery; +import org.thingsboard.server.common.data.relation.EntitySearchDirection; +import org.thingsboard.server.common.data.relation.RelationPathLevel; import org.thingsboard.server.common.util.ProtoUtils; +import org.thingsboard.server.dao.relation.RelationService; import org.thingsboard.server.gen.transport.TransportProtos.AttributeScopeProto; import org.thingsboard.server.gen.transport.TransportProtos.AttributeValueProto; import org.thingsboard.server.gen.transport.TransportProtos.CalculatedFieldTelemetryMsgProto; @@ -45,13 +51,9 @@ import org.thingsboard.server.gen.transport.TransportProtos.TsKvProto; import org.thingsboard.server.queue.TbQueueCallback; import org.thingsboard.server.queue.TbQueueMsgMetadata; import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx; -import org.thingsboard.server.service.profile.TbAssetProfileCache; -import org.thingsboard.server.service.profile.TbDeviceProfileCache; import java.util.Collections; -import java.util.EnumSet; import java.util.List; -import java.util.Set; import java.util.UUID; import java.util.function.Predicate; import java.util.function.Supplier; @@ -74,14 +76,9 @@ public class DefaultCalculatedFieldQueueService implements CalculatedFieldQueueS } }; - private final TbAssetProfileCache assetProfileCache; - private final TbDeviceProfileCache deviceProfileCache; private final CalculatedFieldCache calculatedFieldCache; private final TbClusterService clusterService; - - private static final Set supportedReferencedEntities = EnumSet.of( - EntityType.DEVICE, EntityType.ASSET, EntityType.CUSTOMER, EntityType.TENANT - ); + private final RelationService relationService; @Override public void pushRequestToQueue(TimeseriesSaveRequest request, TimeseriesSaveResult result, FutureCallback callback) { @@ -91,6 +88,8 @@ public class DefaultCalculatedFieldQueueService implements CalculatedFieldQueueS checkEntityAndPushToQueue(tenantId, entityId, cf -> cf.matches(entries), cf -> cf.linkMatches(entityId, entries), + cf -> cf.dynamicSourceMatches(request.getEntries()), + cf -> cf.relatedEntityMatches(entries), () -> toCalculatedFieldTelemetryMsgProto(request, result), callback); } @@ -108,6 +107,8 @@ public class DefaultCalculatedFieldQueueService implements CalculatedFieldQueueS checkEntityAndPushToQueue(tenantId, entityId, cf -> cf.matches(entries, scope), cf -> cf.linkMatches(entityId, entries, scope), + cf -> cf.dynamicSourceMatches(request.getEntries(), request.getScope()), + cf -> cf.relatedEntityMatches(entries, scope), () -> toCalculatedFieldTelemetryMsgProto(request, result), callback); } @@ -124,6 +125,8 @@ public class DefaultCalculatedFieldQueueService implements CalculatedFieldQueueS checkEntityAndPushToQueue(tenantId, entityId, cf -> cf.matchesKeys(result, scope), cf -> cf.linkMatchesAttrKeys(entityId, result, scope), + cf -> cf.matchesDynamicSourceKeys(result, request.getScope()), + cf -> cf.matchesRelatedEntityKeys(result, scope), () -> toCalculatedFieldTelemetryMsgProto(request, result), callback); } @@ -134,16 +137,21 @@ public class DefaultCalculatedFieldQueueService implements CalculatedFieldQueueS checkEntityAndPushToQueue(tenantId, entityId, cf -> cf.matchesKeys(result), cf -> cf.linkMatchesTsKeys(entityId, result), + cf -> cf.matchesDynamicSourceKeys(result), + cf -> cf.matchesRelatedEntityKeys(result), () -> toCalculatedFieldTelemetryMsgProto(request, result), callback); } private void checkEntityAndPushToQueue(TenantId tenantId, EntityId entityId, - Predicate mainEntityFilter, Predicate linkedEntityFilter, + Predicate mainEntityFilter, + Predicate linkedEntityFilter, + Predicate dynamicSourceFilter, + Predicate relatedEntityFilter, Supplier msg, FutureCallback callback) { if (EntityType.TENANT.equals(entityId.getEntityType())) { tenantId = (TenantId) entityId; } - boolean send = checkEntityForCalculatedFields(tenantId, entityId, mainEntityFilter, linkedEntityFilter); + boolean send = checkEntityForCalculatedFields(tenantId, entityId, mainEntityFilter, linkedEntityFilter, dynamicSourceFilter, relatedEntityFilter); if (send) { ToCalculatedFieldMsg calculatedFieldMsg = msg.get(); clusterService.pushMsgToCalculatedFields(tenantId, entityId, calculatedFieldMsg, wrap(callback)); @@ -154,44 +162,63 @@ public class DefaultCalculatedFieldQueueService implements CalculatedFieldQueueS } } - private boolean checkEntityForCalculatedFields(TenantId tenantId, EntityId entityId, Predicate filter, Predicate linkedEntityFilter) { - if (!supportedReferencedEntities.contains(entityId.getEntityType())) { + private boolean checkEntityForCalculatedFields(TenantId tenantId, EntityId entityId, Predicate filter, Predicate linkedEntityFilter, Predicate dynamicSourceFilter, Predicate relatedEntityFilter) { + if (!CalculatedField.isSupportedRefEntity(entityId)) { return false; } - List entityCfs = calculatedFieldCache.getCalculatedFieldCtxsByEntityId(entityId); - for (CalculatedFieldCtx ctx : entityCfs) { - if (filter.test(ctx)) { - return true; - } - } - EntityId profileId = getProfileId(tenantId, entityId); - if (profileId != null) { - List profileCfs = calculatedFieldCache.getCalculatedFieldCtxsByEntityId(profileId); - for (CalculatedFieldCtx ctx : profileCfs) { - if (filter.test(ctx)) { - return true; - } - } + if (calculatedFieldCache.hasCalculatedFields(tenantId, entityId, filter)) { + return true; } List links = calculatedFieldCache.getCalculatedFieldLinksByEntityId(entityId); for (CalculatedFieldLink link : links) { - CalculatedFieldCtx ctx = calculatedFieldCache.getCalculatedFieldCtx(link.getCalculatedFieldId()); + CalculatedFieldCtx ctx = calculatedFieldCache.getCalculatedFieldCtx(link.calculatedFieldId()); if (ctx != null && linkedEntityFilter.test(ctx)) { return true; } } - return false; - } + for (EntityId dynamicEntity : calculatedFieldCache.getDynamicEntities(tenantId, entityId)) { + if (calculatedFieldCache.getCalculatedFieldCtxsByEntityId(dynamicEntity).stream().anyMatch(dynamicSourceFilter)) { + return true; + } + EntityId dynamicEntityProfileId = calculatedFieldCache.getProfileId(tenantId, dynamicEntity); + if (calculatedFieldCache.getCalculatedFieldCtxsByEntityId(dynamicEntityProfileId).stream().anyMatch(dynamicSourceFilter)) { + return true; + } + } + + boolean hasMatchesEntityAggCfs = calculatedFieldCache.getCalculatedFieldCtxsByType(CalculatedFieldType.ENTITY_AGGREGATION).anyMatch(filter); + if (hasMatchesEntityAggCfs) { + return true; + } + + List relatedEntitiesAggregationCfs = calculatedFieldCache.getCalculatedFieldCtxsByType(CalculatedFieldType.RELATED_ENTITIES_AGGREGATION) + .filter(relatedEntityFilter) + .toList(); + for (CalculatedFieldCtx cfCtx : relatedEntitiesAggregationCfs) { + if (cfCtx.getCalculatedField().getConfiguration() instanceof RelatedEntitiesAggregationCalculatedFieldConfiguration aggConfig) { + RelationPathLevel relation = aggConfig.getRelation(); + EntitySearchDirection inverseDirection = switch (relation.direction()) { + case FROM -> EntitySearchDirection.TO; + case TO -> EntitySearchDirection.FROM; + }; + RelationPathLevel inverseRelation = new RelationPathLevel(inverseDirection, relation.relationType()); + List byRelationPathQuery = relationService.findByRelationPathQuery(tenantId, new EntityRelationPathQuery(entityId, List.of(inverseRelation))); + if (!byRelationPathQuery.isEmpty()) { + EntityId cfEntityId = cfCtx.getEntityId(); + for (EntityRelation entityRelation : byRelationPathQuery) { + EntityId relatedId = (inverseDirection == EntitySearchDirection.FROM) ? entityRelation.getTo() : entityRelation.getFrom(); + if (cfEntityId.equals(relatedId) || cfEntityId.equals(calculatedFieldCache.getProfileId(tenantId, relatedId))) { + return true; + } + } + } + } + } - private EntityId getProfileId(TenantId tenantId, EntityId entityId) { - return switch (entityId.getEntityType()) { - case ASSET -> assetProfileCache.get(tenantId, (AssetId) entityId).getId(); - case DEVICE -> deviceProfileCache.get(tenantId, (DeviceId) entityId).getId(); - default -> null; - }; + return false; } private ToCalculatedFieldMsg toCalculatedFieldTelemetryMsgProto(TimeseriesSaveRequest request, TimeseriesSaveResult result) { @@ -305,6 +332,7 @@ public class DefaultCalculatedFieldQueueService implements CalculatedFieldQueueS public void onFailure(Throwable t) { callback.onFailure(t); } + } } diff --git a/application/src/main/java/org/thingsboard/server/service/cf/OwnerService.java b/application/src/main/java/org/thingsboard/server/service/cf/OwnerService.java new file mode 100644 index 0000000000..269d90e5e4 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/cf/OwnerService.java @@ -0,0 +1,76 @@ +/** + * Copyright © 2016-2025 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.cf; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.thingsboard.server.common.data.Customer; +import org.thingsboard.server.common.data.DeviceInfo; +import org.thingsboard.server.common.data.DeviceInfoFilter; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.asset.Asset; +import org.thingsboard.server.common.data.id.AssetId; +import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.page.PageDataIterable; +import org.thingsboard.server.dao.asset.AssetService; +import org.thingsboard.server.dao.customer.CustomerService; +import org.thingsboard.server.dao.device.DeviceService; + +import java.util.HashSet; +import java.util.Set; + +@Service +@RequiredArgsConstructor +public class OwnerService { + + private final DeviceService deviceService; + private final AssetService assetService; + private final CustomerService customerService; + + public EntityId getOwner(TenantId tenantId, EntityId entityId) { + return switch (entityId.getEntityType()) { + case DEVICE -> deviceService.findDeviceById(tenantId, (DeviceId) entityId).getOwnerId(); + case ASSET -> assetService.findAssetById(tenantId, (AssetId) entityId).getOwnerId(); + case CUSTOMER -> tenantId; + default -> throw new UnsupportedOperationException(); + }; + } + + public Set getOwnedEntities(TenantId tenantId, EntityId ownerId) { + Set ownedEntities = new HashSet<>(); + if (EntityType.CUSTOMER.equals(ownerId.getEntityType())) { + PageDataIterable deviceIdInfos = new PageDataIterable<>(pageLink -> deviceService.findDeviceInfosByFilter(DeviceInfoFilter.builder().tenantId(tenantId).customerId((CustomerId) ownerId).build(), pageLink), 1000); + deviceIdInfos.forEach(deviceInfo -> ownedEntities.add(deviceInfo.getId())); + + PageDataIterable assets = new PageDataIterable<>(pageLink -> assetService.findAssetsByTenantIdAndCustomerId(tenantId, (CustomerId) ownerId, pageLink), 1000); + assets.forEach(asset -> ownedEntities.add(asset.getId())); + } else if (EntityType.TENANT.equals(ownerId.getEntityType())) { + PageDataIterable deviceIdInfos = new PageDataIterable<>(pageLink -> deviceService.findDeviceInfosByFilter(DeviceInfoFilter.builder().tenantId((TenantId) ownerId).customerId(new CustomerId(CustomerId.NULL_UUID)).build(), pageLink), 1000); + deviceIdInfos.forEach(deviceInfo -> ownedEntities.add(deviceInfo.getId())); + + PageDataIterable assets = new PageDataIterable<>(pageLink -> assetService.findAssetsByTenantIdAndCustomerId((TenantId) ownerId, new CustomerId(CustomerId.NULL_UUID), pageLink), 1000); + assets.forEach(asset -> ownedEntities.add(asset.getId())); + + PageDataIterable customers = new PageDataIterable<>(pageLink -> customerService.findCustomersByTenantId((TenantId) ownerId, pageLink), 1000); + customers.forEach(customer -> ownedEntities.add(customer.getId())); + } + return ownedEntities; + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/cf/PropagationCalculatedFieldResult.java b/application/src/main/java/org/thingsboard/server/service/cf/PropagationCalculatedFieldResult.java new file mode 100644 index 0000000000..780fd220a7 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/cf/PropagationCalculatedFieldResult.java @@ -0,0 +1,49 @@ +/** + * Copyright © 2016-2025 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.cf; + +import lombok.Builder; +import lombok.Data; +import org.thingsboard.server.common.data.id.CalculatedFieldId; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.util.CollectionsUtil; +import org.thingsboard.server.common.msg.TbMsg; + +import java.util.List; + +@Data +@Builder +public final class PropagationCalculatedFieldResult implements CalculatedFieldResult { + + private final List propagationEntityIds; + private final TelemetryCalculatedFieldResult result; + + @Override + public TbMsg toTbMsg(EntityId entityId, List cfIds) { + return result.toTbMsg(entityId, cfIds); + } + + @Override + public String stringValue() { + return result.stringValue(); + } + + @Override + public boolean isEmpty() { + return CollectionsUtil.isEmpty(propagationEntityIds) || result.isEmpty(); + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/cf/TelemetryCalculatedFieldResult.java b/application/src/main/java/org/thingsboard/server/service/cf/TelemetryCalculatedFieldResult.java new file mode 100644 index 0000000000..e71e381807 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/cf/TelemetryCalculatedFieldResult.java @@ -0,0 +1,76 @@ +/** + * Copyright © 2016-2025 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.cf; + +import com.fasterxml.jackson.databind.JsonNode; +import lombok.Builder; +import lombok.Data; +import org.thingsboard.server.common.data.AttributeScope; +import org.thingsboard.server.common.data.cf.configuration.OutputType; +import org.thingsboard.server.common.data.id.CalculatedFieldId; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.msg.TbMsgType; +import org.thingsboard.server.common.msg.TbMsg; +import org.thingsboard.server.common.msg.TbMsgMetaData; + +import java.util.List; +import java.util.Map; + +import static org.thingsboard.server.common.data.DataConstants.SCOPE; + +@Data +@Builder +public final class TelemetryCalculatedFieldResult implements CalculatedFieldResult { + + private final OutputType type; + private final AttributeScope scope; + private final JsonNode result; + + public static final TelemetryCalculatedFieldResult EMPTY = TelemetryCalculatedFieldResult.builder().result(null).build(); + + @Override + public TbMsg toTbMsg(EntityId entityId, List cfIds) { + TbMsgType msgType = switch (type) { + case ATTRIBUTES -> TbMsgType.POST_ATTRIBUTES_REQUEST; + case TIME_SERIES -> TbMsgType.POST_TELEMETRY_REQUEST; + }; + TbMsgMetaData metaData = switch (type) { + case ATTRIBUTES -> new TbMsgMetaData(Map.of(SCOPE, scope.name())); + case TIME_SERIES -> TbMsgMetaData.EMPTY; + }; + return TbMsg.newMsg() + .type(msgType) + .originator(entityId) + .previousCalculatedFieldIds(cfIds) + .data(stringValue()) + .metaData(metaData) + .build(); + } + + @Override + public String stringValue() { + return result == null ? null : result.toString(); + } + + @Override + public boolean isEmpty() { + return result == null || result.isMissingNode() || result.isNull() || + (result.isObject() && result.isEmpty()) || + (result.isArray() && result.isEmpty()) || + (result.isTextual() && result.asText().isEmpty()); + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/ArgumentEntry.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/ArgumentEntry.java index 83e10b8194..dc23ffa979 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/ArgumentEntry.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/ArgumentEntry.java @@ -18,11 +18,19 @@ package org.thingsboard.server.service.cf.ctx.state; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.databind.JsonNode; +import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.script.api.tbel.TbelCfArg; +import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.kv.KvEntry; import org.thingsboard.server.common.data.kv.TsKvEntry; +import org.thingsboard.server.service.cf.ctx.state.aggregation.RelatedEntitiesArgumentEntry; +import org.thingsboard.server.service.cf.ctx.state.aggregation.single.EntityAggregationArgumentEntry; +import org.thingsboard.server.service.cf.ctx.state.geofencing.GeofencingArgumentEntry; +import org.thingsboard.server.service.cf.ctx.state.propagation.PropagationArgumentEntry; import java.util.List; +import java.util.Map; @JsonTypeInfo( use = JsonTypeInfo.Id.NAME, @@ -31,7 +39,11 @@ import java.util.List; ) @JsonSubTypes({ @JsonSubTypes.Type(value = SingleValueArgumentEntry.class, name = "SINGLE_VALUE"), - @JsonSubTypes.Type(value = TsRollingArgumentEntry.class, name = "TS_ROLLING") + @JsonSubTypes.Type(value = TsRollingArgumentEntry.class, name = "TS_ROLLING"), + @JsonSubTypes.Type(value = GeofencingArgumentEntry.class, name = "GEOFENCING"), + @JsonSubTypes.Type(value = PropagationArgumentEntry.class, name = "PROPAGATION"), + @JsonSubTypes.Type(value = RelatedEntitiesArgumentEntry.class, name = "RELATED_ENTITIES"), + @JsonSubTypes.Type(value = EntityAggregationArgumentEntry.class, name = "ENTITY_AGGREGATION") }) public interface ArgumentEntry { @@ -44,6 +56,10 @@ public interface ArgumentEntry { boolean isEmpty(); + default JsonNode jsonValue() { + return JacksonUtil.valueToTree(toTbelCfArg()); + } + TbelCfArg toTbelCfArg(); boolean isForceResetPrevious(); @@ -54,8 +70,24 @@ public interface ArgumentEntry { return new SingleValueArgumentEntry(kvEntry); } + static ArgumentEntry createSingleValueArgument(EntityId entityId, ArgumentEntry argumentEntry) { + return new SingleValueArgumentEntry(entityId, argumentEntry); + } + static ArgumentEntry createTsRollingArgument(List kvEntries, int limit, long timeWindow) { return new TsRollingArgumentEntry(kvEntries, limit, timeWindow); } + static ArgumentEntry createGeofencingValueArgument(Map entityIdkvEntryMap) { + return new GeofencingArgumentEntry(entityIdkvEntryMap); + } + + static ArgumentEntry createPropagationArgument(List entityIds) { + return new PropagationArgumentEntry(entityIds); + } + + static ArgumentEntry createAggArgument(Map entityIdkvEntryMap) { + return new RelatedEntitiesArgumentEntry(entityIdkvEntryMap, false); + } + } diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/ArgumentEntryType.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/ArgumentEntryType.java index 68f973c7c1..457dd79686 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/ArgumentEntryType.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/ArgumentEntryType.java @@ -16,5 +16,5 @@ package org.thingsboard.server.service.cf.ctx.state; public enum ArgumentEntryType { - SINGLE_VALUE, TS_ROLLING + SINGLE_VALUE, TS_ROLLING, GEOFENCING, PROPAGATION, RELATED_ENTITIES, ENTITY_AGGREGATION } diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/BaseCalculatedFieldState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/BaseCalculatedFieldState.java index e21d56b6d2..e8174967a5 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/BaseCalculatedFieldState.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/BaseCalculatedFieldState.java @@ -15,44 +15,60 @@ */ package org.thingsboard.server.service.cf.ctx.state; -import lombok.AllArgsConstructor; -import lombok.Data; +import com.fasterxml.jackson.databind.node.ObjectNode; +import lombok.Getter; +import lombok.Setter; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.actors.TbActorRef; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.service.cf.ctx.CalculatedFieldEntityCtxId; +import org.thingsboard.server.service.cf.ctx.state.aggregation.RelatedEntitiesArgumentEntry; +import org.thingsboard.server.service.cf.ctx.state.aggregation.single.EntityAggregationArgumentEntry; import org.thingsboard.server.utils.CalculatedFieldUtils; +import java.io.Closeable; import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; -import static org.thingsboard.server.utils.CalculatedFieldUtils.toSingleValueArgumentProto; - -@Data -@AllArgsConstructor -public abstract class BaseCalculatedFieldState implements CalculatedFieldState { +@Getter +public abstract class BaseCalculatedFieldState implements CalculatedFieldState, Closeable { + protected final EntityId entityId; + protected CalculatedFieldCtx ctx; + protected TbActorRef actorCtx; protected List requiredArguments; - protected Map arguments; - protected boolean sizeExceedsLimit; + protected Map arguments = new HashMap<>(); + protected boolean sizeExceedsLimit; protected long latestTimestamp = -1; + protected ReadinessStatus readinessStatus; - public BaseCalculatedFieldState(List requiredArguments) { - this.requiredArguments = requiredArguments; - this.arguments = new HashMap<>(); + @Setter + private TopicPartitionInfo partition; + + public BaseCalculatedFieldState(EntityId entityId) { + this.entityId = entityId; } - public BaseCalculatedFieldState() { - this(new ArrayList<>(), new HashMap<>(), false, -1); + @Override + public void setCtx(CalculatedFieldCtx ctx, TbActorRef actorCtx) { + this.ctx = ctx; + this.actorCtx = actorCtx; + this.requiredArguments = ctx.getArgNames(); + this.readinessStatus = checkReadiness(requiredArguments, arguments); } @Override - public boolean updateState(CalculatedFieldCtx ctx, Map argumentValues) { - if (arguments == null) { - arguments = new HashMap<>(); - } + public void init(boolean restored) { + } - boolean stateUpdated = false; + @Override + public Map update(Map argumentValues, CalculatedFieldCtx ctx) { + Map updatedArguments = null; for (Map.Entry entry : argumentValues.entrySet()) { String key = entry.getKey(); @@ -64,27 +80,47 @@ public abstract class BaseCalculatedFieldState implements CalculatedFieldState { boolean entryUpdated; if (existingEntry == null || newEntry.isForceResetPrevious()) { - validateNewEntry(newEntry); - arguments.put(key, newEntry); + validateNewEntry(key, newEntry); + if (existingEntry instanceof RelatedEntitiesArgumentEntry relatedEntitiesArgumentEntry) { + relatedEntitiesArgumentEntry.updateEntry(newEntry); + } else if (existingEntry instanceof EntityAggregationArgumentEntry entityAggArgumentEntry) { + entityAggArgumentEntry.updateEntry(newEntry); + } else { + arguments.put(key, newEntry); + } entryUpdated = true; } else { entryUpdated = existingEntry.updateEntry(newEntry); } if (entryUpdated) { - stateUpdated = true; + if (updatedArguments == null) { + updatedArguments = new HashMap<>(argumentValues.size()); + } + updatedArguments.put(key, newEntry); updateLastUpdateTimestamp(newEntry); } } - return stateUpdated; + if (updatedArguments == null) { + return Collections.emptyMap(); + } + readinessStatus = checkReadiness(requiredArguments, arguments); + return updatedArguments; + } + + @Override + public void reset() { // must reset everything dependent on arguments + requiredArguments = null; + arguments.clear(); + sizeExceedsLimit = false; + latestTimestamp = -1; } @Override public boolean isReady() { - return arguments.keySet().containsAll(requiredArguments) && - arguments.values().stream().noneMatch(ArgumentEntry::isEmpty); + return readinessStatus.ready(); } @Override @@ -96,19 +132,26 @@ public abstract class BaseCalculatedFieldState implements CalculatedFieldState { } @Override - public void checkArgumentSize(String name, ArgumentEntry entry, CalculatedFieldCtx ctx) { - if (entry instanceof TsRollingArgumentEntry) { - return; + public void close() { + } + + protected void validateNewEntry(String key, ArgumentEntry newEntry) { + } + + protected ObjectNode toSimpleResult(boolean useLatestTs, ObjectNode valuesNode) { + if (!useLatestTs) { + return valuesNode; } - if (entry instanceof SingleValueArgumentEntry singleValueArgumentEntry) { - if (ctx.getMaxSingleValueArgumentSize() > 0 && toSingleValueArgumentProto(name, singleValueArgumentEntry).getSerializedSize() > ctx.getMaxSingleValueArgumentSize()) { - throw new IllegalArgumentException("Single value size exceeds the maximum allowed limit. The argument will not be used for calculation."); - } + long latestTs = getLatestTimestamp(); + if (latestTs == -1) { + return valuesNode; } + ObjectNode resultNode = JacksonUtil.newObjectNode(); + resultNode.put("ts", latestTs); + resultNode.set("values", valuesNode); + return resultNode; } - protected abstract void validateNewEntry(ArgumentEntry newEntry); - private void updateLastUpdateTimestamp(ArgumentEntry entry) { long newTs = this.latestTimestamp; if (entry instanceof SingleValueArgumentEntry singleValueArgumentEntry) { @@ -120,4 +163,21 @@ public abstract class BaseCalculatedFieldState implements CalculatedFieldState { this.latestTimestamp = Math.max(this.latestTimestamp, newTs); } + protected ReadinessStatus checkReadiness(List requiredArguments, Map currentArguments) { + if (currentArguments == null) { + return ReadinessStatus.from(requiredArguments); + } + List emptyArguments = null; + for (String requiredArgumentKey : requiredArguments) { + ArgumentEntry argumentEntry = currentArguments.get(requiredArgumentKey); + if (argumentEntry == null || argumentEntry.isEmpty()) { + if (emptyArguments == null) { + emptyArguments = new ArrayList<>(); + } + emptyArguments.add(requiredArgumentKey); + } + } + return ReadinessStatus.from(emptyArguments); + } + } diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java index 2e3321eece..1d2e2f505a 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java @@ -15,40 +15,71 @@ */ package org.thingsboard.server.service.cf.ctx.state; +import com.google.common.util.concurrent.ListenableFuture; import lombok.Data; +import lombok.extern.slf4j.Slf4j; import net.objecthunter.exp4j.Expression; -import net.objecthunter.exp4j.ExpressionBuilder; import org.mvel2.MVEL; +import org.thingsboard.common.util.ExpressionUtils; +import org.thingsboard.script.api.tbel.TbelCfArg; +import org.thingsboard.script.api.tbel.TbelCfCtx; +import org.thingsboard.script.api.tbel.TbelCfSingleValueArg; import org.thingsboard.script.api.tbel.TbelInvokeService; +import org.thingsboard.server.actors.ActorSystemContext; +import org.thingsboard.server.actors.TbActorRef; +import org.thingsboard.server.actors.calculatedField.CalculatedFieldReevaluateMsg; import org.thingsboard.server.common.data.AttributeScope; +import org.thingsboard.server.common.data.alarm.rule.AlarmRule; +import org.thingsboard.server.common.data.alarm.rule.condition.expression.TbelAlarmConditionExpression; import org.thingsboard.server.common.data.cf.CalculatedField; import org.thingsboard.server.common.data.cf.CalculatedFieldType; +import org.thingsboard.server.common.data.cf.configuration.AlarmCalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.Argument; import org.thingsboard.server.common.data.cf.configuration.ArgumentType; -import org.thingsboard.server.common.data.cf.configuration.CalculatedFieldConfiguration; +import org.thingsboard.server.common.data.cf.configuration.ArgumentsBasedCalculatedFieldConfiguration; +import org.thingsboard.server.common.data.cf.configuration.ExpressionBasedCalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.Output; +import org.thingsboard.server.common.data.cf.configuration.PropagationCalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey; +import org.thingsboard.server.common.data.cf.configuration.ScheduledUpdateSupportedCalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.SimpleCalculatedFieldConfiguration; +import org.thingsboard.server.common.data.cf.configuration.aggregation.AggFunctionInput; +import org.thingsboard.server.common.data.cf.configuration.aggregation.RelatedEntitiesAggregationCalculatedFieldConfiguration; +import org.thingsboard.server.common.data.cf.configuration.aggregation.single.EntityAggregationCalculatedFieldConfiguration; +import org.thingsboard.server.common.data.cf.configuration.aggregation.single.interval.Watermark; +import org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingCalculatedFieldConfiguration; import org.thingsboard.server.common.data.id.CalculatedFieldId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.kv.AttributeKvEntry; +import org.thingsboard.server.common.data.kv.BasicKvEntry; import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; +import org.thingsboard.server.common.data.util.CollectionsUtil; import org.thingsboard.server.common.util.ProtoUtils; -import org.thingsboard.server.dao.usagerecord.ApiLimitService; +import org.thingsboard.server.dao.relation.RelationService; import org.thingsboard.server.gen.transport.TransportProtos.CalculatedFieldTelemetryMsgProto; +import org.thingsboard.server.service.cf.CalculatedFieldProcessingService; import org.thingsboard.server.service.cf.ctx.CalculatedFieldEntityCtxId; +import org.thingsboard.server.service.cf.ctx.state.aggregation.RelatedEntitiesAggregationCalculatedFieldState; +import org.thingsboard.server.service.cf.ctx.state.geofencing.GeofencingCalculatedFieldState; +import org.thingsboard.server.service.telemetry.AlarmSubscriptionService; +import java.io.Closeable; import java.util.ArrayList; import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; - -import static org.thingsboard.common.util.ExpressionFunctionsUtil.userDefinedFunctions; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; @Data -public class CalculatedFieldCtx { +@Slf4j +public class CalculatedFieldCtx implements Closeable { private CalculatedField calculatedField; @@ -57,83 +88,288 @@ public class CalculatedFieldCtx { private EntityId entityId; private CalculatedFieldType cfType; private final Map arguments; - private final Map mainEntityArguments; - private final Map> linkedEntityArguments; + private final Map> mainEntityArguments; + private final Map>> linkedEntityArguments; + private final Map> dynamicEntityArguments; + private final Map> relatedEntityArguments; private final List argNames; private Output output; private String expression; private boolean useLatestTs; + + private long lastReevaluationTs; + + private ActorSystemContext systemContext; private TbelInvokeService tbelInvokeService; - private CalculatedFieldScriptEngine calculatedFieldScriptEngine; - private ThreadLocal customExpression; + private RelationService relationService; + private AlarmSubscriptionService alarmService; + private CalculatedFieldProcessingService cfProcessingService; + + private Map tbelExpressions; + private Map> simpleExpressions; private boolean initialized; - private long maxDataPointsPerRollingArg; private long maxStateSize; private long maxSingleValueArgumentSize; - public CalculatedFieldCtx(CalculatedField calculatedField, TbelInvokeService tbelInvokeService, ApiLimitService apiLimitService) { + private boolean relationQueryDynamicArguments; + private List mainEntityGeofencingArgumentNames; + private List linkedEntityAndCurrentOwnerGeofencingArgumentNames; + private List relatedEntityArgumentNames; + + private long scheduledUpdateIntervalMillis; + + private Argument propagationArgument; + private boolean applyExpressionForResolvedArguments; + + public CalculatedFieldCtx(CalculatedField calculatedField, + ActorSystemContext systemContext) { this.calculatedField = calculatedField; this.cfId = calculatedField.getId(); this.tenantId = calculatedField.getTenantId(); this.entityId = calculatedField.getEntityId(); this.cfType = calculatedField.getType(); - CalculatedFieldConfiguration configuration = calculatedField.getConfiguration(); - this.arguments = configuration.getArguments(); + this.arguments = new HashMap<>(); this.mainEntityArguments = new HashMap<>(); this.linkedEntityArguments = new HashMap<>(); - for (Map.Entry entry : arguments.entrySet()) { - var refId = entry.getValue().getRefEntityId(); - var refKey = entry.getValue().getRefEntityKey(); - if (refId == null || refId.equals(calculatedField.getEntityId())) { - mainEntityArguments.put(refKey, entry.getKey()); - } else { - linkedEntityArguments.computeIfAbsent(refId, key -> new HashMap<>()).put(refKey, entry.getKey()); + this.dynamicEntityArguments = new HashMap<>(); + this.relatedEntityArguments = new HashMap<>(); + this.argNames = new ArrayList<>(); + this.mainEntityGeofencingArgumentNames = new ArrayList<>(); + this.linkedEntityAndCurrentOwnerGeofencingArgumentNames = new ArrayList<>(); + this.relatedEntityArgumentNames = new ArrayList<>(); + this.output = calculatedField.getConfiguration().getOutput(); + if (calculatedField.getConfiguration() instanceof ArgumentsBasedCalculatedFieldConfiguration argBasedConfig) { + this.arguments.putAll(argBasedConfig.getArguments()); + for (Map.Entry entry : arguments.entrySet()) { + var refId = entry.getValue().getRefEntityId(); + var refKey = entry.getValue().getRefEntityKey(); + if (refId == null) { + if (CalculatedFieldType.RELATED_ENTITIES_AGGREGATION.equals(cfType)) { + relatedEntityArguments.compute(refKey, (key, existingNames) -> CollectionsUtil.addToSet(existingNames, entry.getKey())); + continue; + } + if (entry.getValue().hasRelationQuerySource()) { + relationQueryDynamicArguments = true; + continue; + } + if (entry.getValue().hasOwnerSource()) { + dynamicEntityArguments.compute(refKey, (key, existingNames) -> CollectionsUtil.addToSet(existingNames, entry.getKey())); + } else { + mainEntityArguments.compute(refKey, (key, existingNames) -> CollectionsUtil.addToSet(existingNames, entry.getKey())); + } + } else if (refId.equals(calculatedField.getEntityId())) { + mainEntityArguments.compute(refKey, (key, existingNames) -> CollectionsUtil.addToSet(existingNames, entry.getKey())); + } else { + linkedEntityArguments.computeIfAbsent(refId, key -> new HashMap<>()) + .compute(refKey, (key, existingNames) -> CollectionsUtil.addToSet(existingNames, entry.getKey())); + } + } + this.argNames.addAll(arguments.keySet()); + this.relatedEntityArgumentNames = relatedEntityArguments.values().stream() + .flatMap(Set::stream) + .collect(Collectors.toList()); + if (argBasedConfig instanceof ExpressionBasedCalculatedFieldConfiguration expressionBasedConfig) { + this.expression = expressionBasedConfig.getExpression(); + this.useLatestTs = CalculatedFieldType.SIMPLE.equals(calculatedField.getType()) && ((SimpleCalculatedFieldConfiguration) argBasedConfig).isUseLatestTs(); + } + if (calculatedField.getConfiguration() instanceof GeofencingCalculatedFieldConfiguration geofencingConfig) { + geofencingConfig.getZoneGroups().forEach((zoneGroupName, config) -> { + if (config.isCfEntitySource(entityId)) { + mainEntityGeofencingArgumentNames.add(zoneGroupName); + return; + } + if (config.isLinkedCfEntitySource(entityId) || config.hasCurrentOwnerSource()) { + linkedEntityAndCurrentOwnerGeofencingArgumentNames.add(zoneGroupName); + } + }); + } + if (calculatedField.getConfiguration() instanceof PropagationCalculatedFieldConfiguration propagationConfig) { + propagationArgument = propagationConfig.toPropagationArgument(); + applyExpressionForResolvedArguments = propagationConfig.isApplyExpressionToResolvedArguments(); + relationQueryDynamicArguments = true; } } - this.argNames = new ArrayList<>(arguments.keySet()); - this.output = configuration.getOutput(); - this.expression = configuration.getExpression(); - this.useLatestTs = CalculatedFieldType.SIMPLE.equals(calculatedField.getType()) && ((SimpleCalculatedFieldConfiguration) configuration).isUseLatestTs(); - this.tbelInvokeService = tbelInvokeService; + if (calculatedField.getConfiguration() instanceof ScheduledUpdateSupportedCalculatedFieldConfiguration scheduledConfig) { + this.scheduledUpdateIntervalMillis = scheduledConfig.isScheduledUpdateEnabled() ? TimeUnit.SECONDS.toMillis(scheduledConfig.getScheduledUpdateInterval()) : -1L; + } + if (calculatedField.getConfiguration() instanceof RelatedEntitiesAggregationCalculatedFieldConfiguration aggConfig) { + this.useLatestTs = aggConfig.isUseLatestTs(); + } + this.systemContext = systemContext; + this.tbelInvokeService = systemContext.getTbelInvokeService(); + this.relationService = systemContext.getRelationService(); + this.alarmService = systemContext.getAlarmService(); + this.cfProcessingService = systemContext.getCalculatedFieldProcessingService(); + + this.maxStateSize = systemContext.getApiLimitService().getLimit(tenantId, DefaultTenantProfileConfiguration::getMaxStateSizeInKBytes) * 1024; + this.maxSingleValueArgumentSize = systemContext.getApiLimitService().getLimit(tenantId, DefaultTenantProfileConfiguration::getMaxSingleValueArgumentSizeInKBytes) * 1024; + } - this.maxDataPointsPerRollingArg = apiLimitService.getLimit(tenantId, DefaultTenantProfileConfiguration::getMaxDataPointsPerRollingArg); - this.maxStateSize = apiLimitService.getLimit(tenantId, DefaultTenantProfileConfiguration::getMaxStateSizeInKBytes) * 1024; - this.maxSingleValueArgumentSize = apiLimitService.getLimit(tenantId, DefaultTenantProfileConfiguration::getMaxSingleValueArgumentSizeInKBytes) * 1024; + public boolean isRequiresScheduledReevaluation() { + long now = System.currentTimeMillis(); + long cfCheckIntervalMillis = TimeUnit.SECONDS.toMillis(systemContext.getCfCheckInterval()); + if (calculatedField.getConfiguration() instanceof EntityAggregationCalculatedFieldConfiguration entityAggregationConfig) { + Watermark watermark = entityAggregationConfig.getWatermark(); + if (watermark != null && watermark.getDuration() > 0) { + return true; + } + long intervalEndTs = entityAggregationConfig.getInterval().getCurrentIntervalEndTs(); + if (now + cfCheckIntervalMillis >= intervalEndTs) { + return true; + } + } + boolean requiresScheduledReevaluation = calculatedField.getConfiguration().requiresScheduledReevaluation(); + if (calculatedField.getConfiguration() instanceof AlarmCalculatedFieldConfiguration) { + long reevaluationIntervalMillis = TimeUnit.SECONDS.toMillis(systemContext.getAlarmRulesReevaluationInterval()); + if (requiresScheduledReevaluation) { + if (now + cfCheckIntervalMillis >= lastReevaluationTs + reevaluationIntervalMillis) { + lastReevaluationTs = now; + return true; + } + return false; + } + } + return requiresScheduledReevaluation; } public void init() { - if (CalculatedFieldType.SCRIPT.equals(cfType)) { - try { - this.calculatedFieldScriptEngine = initEngine(tenantId, expression, tbelInvokeService); + switch (cfType) { + case SCRIPT -> { + initTbelExpression(expression); initialized = true; - } catch (Exception e) { - throw new RuntimeException("Failed to init calculated field ctx. Invalid expression syntax.", e); } - } else { - if (isValidExpression(expression)) { - this.customExpression = ThreadLocal.withInitial(() -> - new ExpressionBuilder(expression) - .functions(userDefinedFunctions) - .implicitMultiplication(true) - .variables(this.arguments.keySet()) - .build() - ); + case GEOFENCING -> initialized = true; + case SIMPLE -> { + initSimpleExpression(expression); + initialized = true; + } + case ALARM -> { + AlarmCalculatedFieldConfiguration configuration = (AlarmCalculatedFieldConfiguration) calculatedField.getConfiguration(); + configuration.getAllRules().map(rule -> rule.getValue().getCondition().getExpression()) + .forEach(expression -> { + if (expression instanceof TbelAlarmConditionExpression tbelExpression) { + initTbelExpression(tbelExpression.getExpression()); + } + }); initialized = true; + } + case PROPAGATION -> { + if (applyExpressionForResolvedArguments) { + initTbelExpression(expression); + } + initialized = true; + } + case RELATED_ENTITIES_AGGREGATION -> { + RelatedEntitiesAggregationCalculatedFieldConfiguration configuration = (RelatedEntitiesAggregationCalculatedFieldConfiguration) calculatedField.getConfiguration(); + configuration.getMetrics().forEach((key, metric) -> { + if (metric.getInput() instanceof AggFunctionInput functionInput) { + initTbelExpression(functionInput.getFunction()); + } + String filter = metric.getFilter(); + if (filter != null && !filter.isEmpty()) { + initTbelExpression(filter); + } + }); + initialized = true; + } + case ENTITY_AGGREGATION -> initialized = true; + } + } + + public void updateTenantProfileProperties() { + this.maxStateSize = systemContext.getApiLimitService().getLimit(tenantId, DefaultTenantProfileConfiguration::getMaxStateSizeInKBytes) * 1024; + this.maxSingleValueArgumentSize = systemContext.getApiLimitService().getLimit(tenantId, DefaultTenantProfileConfiguration::getMaxSingleValueArgumentSizeInKBytes) * 1024; + } + + public double evaluateSimpleExpression(Expression expression, CalculatedFieldState state) { + for (Map.Entry entry : state.getArguments().entrySet()) { + try { + BasicKvEntry kvEntry = ((SingleValueArgumentEntry) entry.getValue()).getKvEntryValue(); + double value = switch (kvEntry.getDataType()) { + case LONG -> kvEntry.getLongValue().map(Long::doubleValue).orElseThrow(); + case DOUBLE -> kvEntry.getDoubleValue().orElseThrow(); + case BOOLEAN -> kvEntry.getBooleanValue().map(b -> b ? 1.0 : 0.0).orElseThrow(); + case STRING, JSON -> Double.parseDouble(kvEntry.getValueAsString()); + }; + expression.setVariable(entry.getKey(), value); + } catch (NumberFormatException e) { + throw new IllegalArgumentException("Argument '" + entry.getKey() + "' is not a number."); + } + } + return expression.evaluate(); + } + + public ListenableFuture evaluateTbelExpression(String expression, CalculatedFieldState state) { + return evaluateTbelExpression(tbelExpressions.get(expression), state.getArguments(), state.getLatestTimestamp()); + } + + public ListenableFuture evaluateTbelExpression(CalculatedFieldScriptEngine expression, CalculatedFieldState state) { + return evaluateTbelExpression(expression, state.getArguments(), state.getLatestTimestamp()); + } + + public ListenableFuture evaluateTbelExpression(String expression, Map entries, long latestTimestamp) { + return evaluateTbelExpression(tbelExpressions.get(expression), entries, latestTimestamp); + } + + public ListenableFuture evaluateTbelExpression(CalculatedFieldScriptEngine expression, Map entries, long latestTimestamp) { + Map arguments = new LinkedHashMap<>(); + List args = new ArrayList<>(argNames.size() + 1); + args.add(new Object()); // first element is a ctx, but we will set it later; + for (String argName : argNames) { + var arg = toTbelArgument(argName, entries); + arguments.put(argName, arg); + if (arg instanceof TbelCfSingleValueArg svArg) { + args.add(svArg.getValue()); } else { - throw new RuntimeException("Failed to init calculated field ctx. Invalid expression syntax."); + args.add(arg); } } + args.set(0, new TbelCfCtx(arguments, latestTimestamp)); + + return expression.executeScriptAsync(args.toArray()); + } + + public ScheduledFuture scheduleReevaluation(long delayMs, TbActorRef actorCtx) { + log.debug("[{}] Scheduling CF reevaluation in {} ms", cfId, delayMs); + return systemContext.scheduleMsgWithDelay(actorCtx, new CalculatedFieldReevaluateMsg(tenantId, this), delayMs); + } + + private TbelCfArg toTbelArgument(String key, Map arguments) { + return arguments.get(key).toTbelCfArg(); + } + + private void initTbelExpression(String expression) { + if (tbelExpressions == null) { + tbelExpressions = new HashMap<>(); + } else if (tbelExpressions.containsKey(expression)) { + return; + } + try { + CalculatedFieldScriptEngine engine = initEngine(tenantId, expression, tbelInvokeService); + tbelExpressions.put(expression, engine); + } catch (Exception e) { + initialized = false; + throw new RuntimeException("Failed to init calculated field ctx. Invalid expression syntax.", e); + } } - public void stop() { - if (calculatedFieldScriptEngine != null) { - calculatedFieldScriptEngine.destroy(); + private void initSimpleExpression(String expression) { + if (simpleExpressions == null) { + simpleExpressions = new HashMap<>(); + } else if (simpleExpressions.containsKey(expression)) { + return; } - if (customExpression != null) { - customExpression.remove(); + if (isValidExpression(expression)) { + ThreadLocal compiledExpression = ThreadLocal.withInitial(() -> + ExpressionUtils.createExpression(expression, this.arguments.keySet()) + ); + simpleExpressions.put(expression, compiledExpression); + } else { + initialized = false; + throw new RuntimeException("Failed to init calculated field ctx. Invalid expression syntax."); } } @@ -180,7 +416,15 @@ public class CalculatedFieldCtx { return map != null && matchesTimeSeries(map, values); } - private boolean matchesAttributes(Map argMap, List values, AttributeScope scope) { + public boolean dynamicSourceMatches(List values) { + return matchesTimeSeries(dynamicEntityArguments, values); + } + + public boolean dynamicSourceMatches(List values, AttributeScope scope) { + return matchesAttributes(dynamicEntityArguments, values, scope); + } + + private boolean matchesAttributes(Map> argMap, List values, AttributeScope scope) { if (argMap.isEmpty() || values.isEmpty()) { return false; } @@ -194,7 +438,7 @@ public class CalculatedFieldCtx { return false; } - private boolean matchesTimeSeries(Map argMap, List values) { + private boolean matchesTimeSeries(Map> argMap, List values) { if (argMap.isEmpty() || values.isEmpty()) { return false; } @@ -223,7 +467,15 @@ public class CalculatedFieldCtx { return matchesTimeSeriesKeys(mainEntityArguments, keys); } - private boolean matchesAttributesKeys(Map argMap, List keys, AttributeScope scope) { + public boolean matchesDynamicSourceKeys(List keys, AttributeScope scope) { + return matchesAttributesKeys(dynamicEntityArguments, keys, scope); + } + + public boolean matchesDynamicSourceKeys(List keys) { + return matchesTimeSeriesKeys(dynamicEntityArguments, keys); + } + + private boolean matchesAttributesKeys(Map> argMap, List keys, AttributeScope scope) { if (argMap.isEmpty() || keys.isEmpty()) { return false; } @@ -238,7 +490,7 @@ public class CalculatedFieldCtx { return false; } - private boolean matchesTimeSeriesKeys(Map argMap, List keys) { + private boolean matchesTimeSeriesKeys(Map> argMap, List keys) { if (argMap.isEmpty() || keys.isEmpty()) { return false; } @@ -269,6 +521,60 @@ public class CalculatedFieldCtx { return map != null && matchesTimeSeriesKeys(map, keys); } + public boolean relatedEntityMatches(List values) { + return matchesTimeSeries(relatedEntityArguments, values); + } + + public boolean relatedEntityMatches(List values, AttributeScope scope) { + return matchesAttributes(relatedEntityArguments, values, scope); + } + + public boolean matchesRelatedEntityKeys(List keys, AttributeScope scope) { + return matchesAttributesKeys(relatedEntityArguments, keys, scope); + } + + public boolean matchesRelatedEntityKeys(List keys) { + return matchesTimeSeriesKeys(relatedEntityArguments, keys); + } + + public boolean relatedEntityMatches(CalculatedFieldTelemetryMsgProto proto) { + if (!proto.getTsDataList().isEmpty()) { + List updatedTelemetry = proto.getTsDataList().stream() + .map(ProtoUtils::fromProto) + .toList(); + return relatedEntityMatches(updatedTelemetry); + } else if (!proto.getAttrDataList().isEmpty()) { + AttributeScope scope = AttributeScope.valueOf(proto.getScope().name()); + List updatedTelemetry = proto.getAttrDataList().stream() + .map(ProtoUtils::fromProto) + .toList(); + return relatedEntityMatches(updatedTelemetry, scope); + } else if (!proto.getRemovedTsKeysList().isEmpty()) { + return matchesRelatedEntityKeys(proto.getRemovedTsKeysList()); + } else { + return matchesRelatedEntityKeys(proto.getRemovedAttrKeysList(), AttributeScope.valueOf(proto.getScope().name())); + } + } + + public boolean dynamicSourceMatches(CalculatedFieldTelemetryMsgProto proto) { + if (!proto.getTsDataList().isEmpty()) { + List updatedTelemetry = proto.getTsDataList().stream() + .map(ProtoUtils::fromProto) + .toList(); + return dynamicSourceMatches(updatedTelemetry); + } else if (!proto.getAttrDataList().isEmpty()) { + AttributeScope scope = AttributeScope.valueOf(proto.getScope().name()); + List updatedTelemetry = proto.getAttrDataList().stream() + .map(ProtoUtils::fromProto) + .toList(); + return dynamicSourceMatches(updatedTelemetry, scope); + } else if (!proto.getRemovedTsKeysList().isEmpty()) { + return matchesDynamicSourceKeys(proto.getRemovedTsKeysList()); + } else { + return matchesDynamicSourceKeys(proto.getRemovedAttrKeysList(), AttributeScope.valueOf(proto.getScope().name())); + } + } + public boolean linkMatches(EntityId entityId, CalculatedFieldTelemetryMsgProto proto) { if (!proto.getTsDataList().isEmpty()) { List updatedTelemetry = proto.getTsDataList().stream() @@ -288,24 +594,180 @@ public class CalculatedFieldCtx { } } + public Map> getLinkedAndDynamicArgs(EntityId entityId) { + var argNames = new HashMap>(); + var linkedArgNames = linkedEntityArguments.get(entityId); + if (linkedArgNames != null && !linkedArgNames.isEmpty()) { + argNames.putAll(linkedArgNames); + } + if (dynamicEntityArguments != null && !dynamicEntityArguments.isEmpty()) { + argNames.putAll(dynamicEntityArguments); + } + return argNames; + } + public CalculatedFieldEntityCtxId toCalculatedFieldEntityCtxId() { return new CalculatedFieldEntityCtxId(tenantId, cfId, entityId); } - public boolean hasOtherSignificantChanges(CalculatedFieldCtx other) { - boolean expressionChanged = !expression.equals(other.expression); - boolean outputChanged = !output.equals(other.output); - return expressionChanged || outputChanged; + public boolean hasContextOnlyChanges(CalculatedFieldCtx other) { // has changes that do not require state reinit and will be picked up by the state on the fly + if (calculatedField.getConfiguration() instanceof ExpressionBasedCalculatedFieldConfiguration && !Objects.equals(expression, other.expression)) { + return true; + } + if (!Objects.equals(output, other.output)) { + return true; + } + if (calculatedField.getConfiguration() instanceof SimpleCalculatedFieldConfiguration thisConfig + && other.calculatedField.getConfiguration() instanceof SimpleCalculatedFieldConfiguration otherConfig + && thisConfig.isUseLatestTs() != otherConfig.isUseLatestTs()) { + return true; + } + if (cfType == CalculatedFieldType.ALARM) { + if (!calculatedField.getName().equals(other.getCalculatedField().getName())) { + return true; + } + + var thisConfig = (AlarmCalculatedFieldConfiguration) calculatedField.getConfiguration(); + var otherConfig = (AlarmCalculatedFieldConfiguration) other.getCalculatedField().getConfiguration(); + if (!thisConfig.rulesEqual(otherConfig, AlarmRule::equals)) { + // if the rules have any changes not tracked by hasStateChanges + return true; + } + } + if (scheduledUpdateIntervalMillis != other.scheduledUpdateIntervalMillis) { + return true; + } + if (calculatedField.getConfiguration() instanceof RelatedEntitiesAggregationCalculatedFieldConfiguration thisConfig + && other.getCalculatedField().getConfiguration() instanceof RelatedEntitiesAggregationCalculatedFieldConfiguration otherConfig + && (thisConfig.getDeduplicationIntervalInSec() != otherConfig.getDeduplicationIntervalInSec() || !thisConfig.getMetrics().equals(otherConfig.getMetrics()))) { + return true; + } + if (calculatedField.getConfiguration() instanceof EntityAggregationCalculatedFieldConfiguration thisConfig + && other.getCalculatedField().getConfiguration() instanceof EntityAggregationCalculatedFieldConfiguration otherConfig) { + boolean metricsChanged = thisConfig.getMetrics().equals(otherConfig.getMetrics()); + boolean watermarkChanged = thisConfig.getWatermark().equals(otherConfig.getWatermark()); + return metricsChanged || watermarkChanged; + } + return false; } public boolean hasStateChanges(CalculatedFieldCtx other) { - boolean typeChanged = !cfType.equals(other.cfType); - boolean argumentsChanged = !arguments.equals(other.arguments); - return typeChanged || argumentsChanged; + if (!arguments.equals(other.arguments)) { + return true; + } + if (cfType == CalculatedFieldType.ALARM) { + var thisConfig = (AlarmCalculatedFieldConfiguration) calculatedField.getConfiguration(); + var otherConfig = (AlarmCalculatedFieldConfiguration) other.getCalculatedField().getConfiguration(); + if (!thisConfig.rulesEqual(otherConfig, (thisRule, otherRule) -> { + return thisRule.getCondition().getType() == otherRule.getCondition().getType(); + })) { + // reinitializing only if the rule list changed, or if a condition type changed for any rule + return true; + } + } + if (hasGeofencingZoneGroupConfigurationChanges(other)) { + return true; + } + if (hasRelatedEntitiesAggregationConfigurationChanges(other)) { + return true; + } + if (hasEntityAggregationConfigurationChanges(other)) { + return true; + } + return false; + } + + private boolean hasGeofencingZoneGroupConfigurationChanges(CalculatedFieldCtx other) { + if (calculatedField.getConfiguration() instanceof GeofencingCalculatedFieldConfiguration thisConfig + && other.calculatedField.getConfiguration() instanceof GeofencingCalculatedFieldConfiguration otherConfig) { + return !thisConfig.getZoneGroups().equals(otherConfig.getZoneGroups()); + } + return false; + } + + private boolean hasRelatedEntitiesAggregationConfigurationChanges(CalculatedFieldCtx other) { + if (calculatedField.getConfiguration() instanceof RelatedEntitiesAggregationCalculatedFieldConfiguration thisConfig + && other.calculatedField.getConfiguration() instanceof RelatedEntitiesAggregationCalculatedFieldConfiguration otherConfig) { + return !thisConfig.getRelation().equals(otherConfig.getRelation()); + } + return false; + } + + private boolean hasEntityAggregationConfigurationChanges(CalculatedFieldCtx other) { + if (calculatedField.getConfiguration() instanceof EntityAggregationCalculatedFieldConfiguration thisConfig + && other.calculatedField.getConfiguration() instanceof EntityAggregationCalculatedFieldConfiguration otherConfig) { + return !thisConfig.getInterval().equals(otherConfig.getInterval()); + } + return false; + } + + private boolean isScheduledUpdateEnabled() { + return scheduledUpdateIntervalMillis != -1; + } + + public boolean shouldFetchRelationQueryDynamicArgumentsFromDb(CalculatedFieldState state) { + if (!relationQueryDynamicArguments) { + return false; + } + return switch (cfType) { + case PROPAGATION -> true; + case GEOFENCING -> { + if (!isScheduledUpdateEnabled()) { + yield false; + } + var geofencingState = (GeofencingCalculatedFieldState) state; + if (geofencingState.getLastDynamicArgumentsRefreshTs() == -1L) { + yield true; + } + yield geofencingState.getLastDynamicArgumentsRefreshTs() < + System.currentTimeMillis() - scheduledUpdateIntervalMillis; + } + default -> false; + }; + } + + public boolean shouldFetchEntityRelations(CalculatedFieldState state) { + if (!(state instanceof RelatedEntitiesAggregationCalculatedFieldState relatedEntitiesAggState)) { + return false; + } + if (!isScheduledUpdateEnabled()) { + return false; + } + if (relatedEntitiesAggState.getLastRelatedEntitiesRefreshTs() == -1L) { + return true; + } + return relatedEntitiesAggState.getLastRelatedEntitiesRefreshTs() < System.currentTimeMillis() - scheduledUpdateIntervalMillis; + } + + @Override + public void close() { + try { + if (tbelExpressions != null) { + tbelExpressions.values().forEach(CalculatedFieldScriptEngine::destroy); + } + if (simpleExpressions != null) { + simpleExpressions.values().forEach(ThreadLocal::remove); + } + } catch (Exception e) { + log.warn("Failed to stop {}", this, e); + } } public String getSizeExceedsLimitMessage() { return "Failed to init CF state. State size exceeds limit of " + (maxStateSize / 1024) + "Kb!"; } + public boolean hasCurrentOwnerSourceArguments() { + return !dynamicEntityArguments.isEmpty(); + } + + @Override + public String toString() { + return "CalculatedFieldCtx{" + + "cfId=" + cfId + + ", cfType=" + cfType + + ", entityId=" + entityId + + '}'; + } + } diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldState.java index 0de354bbb0..e3914cc125 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldState.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldState.java @@ -17,42 +17,65 @@ package org.thingsboard.server.service.cf.ctx.state; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonSubTypes.Type; import com.fasterxml.jackson.annotation.JsonTypeInfo; import com.google.common.util.concurrent.ListenableFuture; +import org.thingsboard.server.actors.TbActorRef; import org.thingsboard.server.common.data.cf.CalculatedFieldType; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.util.CollectionsUtil; +import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.service.cf.CalculatedFieldResult; import org.thingsboard.server.service.cf.ctx.CalculatedFieldEntityCtxId; +import org.thingsboard.server.service.cf.ctx.state.aggregation.RelatedEntitiesAggregationCalculatedFieldState; +import org.thingsboard.server.service.cf.ctx.state.aggregation.single.EntityAggregationCalculatedFieldState; +import org.thingsboard.server.service.cf.ctx.state.alarm.AlarmCalculatedFieldState; +import org.thingsboard.server.service.cf.ctx.state.geofencing.GeofencingArgumentEntry; +import org.thingsboard.server.service.cf.ctx.state.geofencing.GeofencingCalculatedFieldState; +import org.thingsboard.server.service.cf.ctx.state.propagation.PropagationCalculatedFieldState; +import java.io.Closeable; import java.util.List; import java.util.Map; -@JsonTypeInfo( - use = JsonTypeInfo.Id.NAME, - include = JsonTypeInfo.As.PROPERTY, - property = "type" -) +import static org.thingsboard.server.utils.CalculatedFieldUtils.toSingleValueArgumentProto; + +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") @JsonSubTypes({ - @JsonSubTypes.Type(value = SimpleCalculatedFieldState.class, name = "SIMPLE"), - @JsonSubTypes.Type(value = ScriptCalculatedFieldState.class, name = "SCRIPT"), + @Type(value = SimpleCalculatedFieldState.class, name = "SIMPLE"), + @Type(value = ScriptCalculatedFieldState.class, name = "SCRIPT"), + @Type(value = GeofencingCalculatedFieldState.class, name = "GEOFENCING"), + @Type(value = AlarmCalculatedFieldState.class, name = "ALARM"), + @Type(value = PropagationCalculatedFieldState.class, name = "PROPAGATION"), + @Type(value = RelatedEntitiesAggregationCalculatedFieldState.class, name = "RELATED_ENTITIES_AGGREGATION"), + @Type(value = EntityAggregationCalculatedFieldState.class, name = "ENTITY_AGGREGATION") }) -public interface CalculatedFieldState { +public interface CalculatedFieldState extends Closeable { @JsonIgnore CalculatedFieldType getType(); + EntityId getEntityId(); + Map getArguments(); long getLatestTimestamp(); - void setRequiredArguments(List requiredArguments); + void setCtx(CalculatedFieldCtx ctx, TbActorRef actorCtx); + + void init(boolean restored); - boolean updateState(CalculatedFieldCtx ctx, Map argumentValues); + Map update(Map arguments, CalculatedFieldCtx ctx); - ListenableFuture performCalculation(CalculatedFieldCtx ctx); + void reset(); + + ListenableFuture performCalculation(Map updatedArgs, CalculatedFieldCtx ctx) throws Exception; @JsonIgnore boolean isReady(); + ReadinessStatus getReadinessStatus(); + boolean isSizeExceedsLimit(); @JsonIgnore @@ -60,8 +83,34 @@ public interface CalculatedFieldState { return !isSizeExceedsLimit(); } + TopicPartitionInfo getPartition(); + + void setPartition(TopicPartitionInfo partition); + void checkStateSize(CalculatedFieldEntityCtxId ctxId, long maxStateSize); - void checkArgumentSize(String name, ArgumentEntry entry, CalculatedFieldCtx ctx); + default void checkArgumentSize(String name, ArgumentEntry entry, CalculatedFieldCtx ctx) { + if (entry instanceof TsRollingArgumentEntry || entry instanceof GeofencingArgumentEntry) { + return; + } + if (entry instanceof SingleValueArgumentEntry singleValueArgumentEntry) { + if (ctx.getMaxSingleValueArgumentSize() > 0 && toSingleValueArgumentProto(name, singleValueArgumentEntry).getSerializedSize() > ctx.getMaxSingleValueArgumentSize()) { + throw new IllegalArgumentException("Single value size exceeds the maximum allowed limit. The argument will not be used for calculation."); + } + } + } + + record ReadinessStatus(boolean ready, String errorMsg) { + + private static final String ERROR_MESSAGE = "Required arguments are missing: "; + private static final ReadinessStatus READY = new ReadinessStatus(true, null); + + public static ReadinessStatus from(List emptyOrMissingArguments) { + if (CollectionsUtil.isEmpty(emptyOrMissingArguments)) { + return ReadinessStatus.READY; + } + return new ReadinessStatus(false, ERROR_MESSAGE + String.join(", ", emptyOrMissingArguments)); + } + } } diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/KafkaCalculatedFieldStateService.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/KafkaCalculatedFieldStateService.java index 2b52892744..e8174bfd57 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/KafkaCalculatedFieldStateService.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/KafkaCalculatedFieldStateService.java @@ -43,6 +43,7 @@ import org.thingsboard.server.queue.provider.TbRuleEngineQueueFactory; import org.thingsboard.server.service.cf.AbstractCalculatedFieldStateService; import org.thingsboard.server.service.cf.ctx.CalculatedFieldEntityCtxId; +import java.util.Set; import java.util.concurrent.atomic.AtomicInteger; import static org.thingsboard.server.queue.common.AbstractTbQueueTemplate.bytesToString; @@ -77,9 +78,9 @@ public class KafkaCalculatedFieldStateService extends AbstractCalculatedFieldSta for (TbProtoQueueMsg msg : msgs) { try { if (msg.getValue() != null) { - processRestoredState(msg.getValue()); + processRestoredState(msg.getValue(), consumerKey.partition()); } else { - processRestoredState(getStateId(msg.getHeaders()), null); + processRestoredState(getStateId(msg.getHeaders()), null, consumerKey.partition()); } } catch (Throwable t) { log.error("Failed to process state message: {}", msg, t); @@ -104,6 +105,11 @@ public class KafkaCalculatedFieldStateService extends AbstractCalculatedFieldSta this.stateProducer = (TbKafkaProducerTemplate>) queueFactory.createCalculatedFieldStateProducer(); } + @Override + public void restore(QueueKey queueKey, Set partitions) { + stateService.update(queueKey, partitions, null); + } + @Override protected void doPersist(CalculatedFieldEntityCtxId stateId, CalculatedFieldStateProto stateMsgProto, TbCallback callback) { TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_RULE_ENGINE, DataConstants.CF_STATES_QUEUE_NAME, stateId.tenantId(), stateId.entityId()); diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/RocksDBCalculatedFieldStateService.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/RocksDBCalculatedFieldStateService.java index 9dc6139ca5..05bfb8b717 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/RocksDBCalculatedFieldStateService.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/RocksDBCalculatedFieldStateService.java @@ -15,7 +15,6 @@ */ package org.thingsboard.server.service.cf.ctx.state; -import com.google.protobuf.InvalidProtocolBufferException; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; @@ -64,8 +63,8 @@ public class RocksDBCalculatedFieldStateService extends AbstractCalculatedFieldS if (stateService.getPartitions().isEmpty()) { cfRocksDb.forEach((key, value) -> { try { - processRestoredState(CalculatedFieldStateProto.parseFrom(value)); - } catch (InvalidProtocolBufferException e) { + processRestoredState(CalculatedFieldStateProto.parseFrom(value), null); + } catch (Exception e) { log.error("[{}] Failed to process restored state", key, e); } }); diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/ScriptCalculatedFieldState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/ScriptCalculatedFieldState.java index 84dce627ae..c52c01549f 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/ScriptCalculatedFieldState.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/ScriptCalculatedFieldState.java @@ -15,68 +15,54 @@ */ package org.thingsboard.server.service.cf.ctx.state; -import com.fasterxml.jackson.databind.JsonNode; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.MoreExecutors; -import lombok.Data; -import lombok.NoArgsConstructor; +import lombok.EqualsAndHashCode; import lombok.extern.slf4j.Slf4j; -import org.thingsboard.script.api.tbel.TbelCfArg; -import org.thingsboard.script.api.tbel.TbelCfCtx; -import org.thingsboard.script.api.tbel.TbelCfSingleValueArg; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.actors.TbActorRef; import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.cf.configuration.Output; +import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.service.cf.CalculatedFieldResult; +import org.thingsboard.server.service.cf.TelemetryCalculatedFieldResult; -import java.util.ArrayList; -import java.util.LinkedHashMap; -import java.util.List; import java.util.Map; -@Data @Slf4j -@NoArgsConstructor +@EqualsAndHashCode(callSuper = true) public class ScriptCalculatedFieldState extends BaseCalculatedFieldState { - public ScriptCalculatedFieldState(List requiredArguments) { - super(requiredArguments); - } + protected CalculatedFieldScriptEngine tbelExpression; - @Override - public CalculatedFieldType getType() { - return CalculatedFieldType.SCRIPT; + public ScriptCalculatedFieldState(EntityId entityId) { + super(entityId); } @Override - protected void validateNewEntry(ArgumentEntry newEntry) { + public void setCtx(CalculatedFieldCtx ctx, TbActorRef actorCtx) { + super.setCtx(ctx, actorCtx); + this.tbelExpression = ctx.getTbelExpressions().get(ctx.getExpression()); } @Override - public ListenableFuture performCalculation(CalculatedFieldCtx ctx) { - Map arguments = new LinkedHashMap<>(); - List args = new ArrayList<>(ctx.getArgNames().size() + 1); - args.add(new Object()); // first element is a ctx, but we will set it later; - for (String argName : ctx.getArgNames()) { - var arg = toTbelArgument(argName); - arguments.put(argName, arg); - if (arg instanceof TbelCfSingleValueArg svArg) { - args.add(svArg.getValue()); - } else { - args.add(arg); - } - } - args.set(0, new TbelCfCtx(arguments, getLatestTimestamp())); - ListenableFuture resultFuture = ctx.getCalculatedFieldScriptEngine().executeJsonAsync(args.toArray()); + public ListenableFuture performCalculation(Map updatedArgs, CalculatedFieldCtx ctx) { + ListenableFuture resultFuture = ctx.evaluateTbelExpression(tbelExpression, this); Output output = ctx.getOutput(); return Futures.transform(resultFuture, - result -> new CalculatedFieldResult(output.getType(), output.getScope(), result), + result -> TelemetryCalculatedFieldResult.builder() + .type(output.getType()) + .scope(output.getScope()) + .result(JacksonUtil.valueToTree(result)) + .build(), MoreExecutors.directExecutor() ); } - private TbelCfArg toTbelArgument(String key) { - return arguments.get(key).toTbelCfArg(); + @Override + public CalculatedFieldType getType() { + return CalculatedFieldType.SCRIPT; } } diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldState.java index 577ff80219..ab0ed26dfe 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldState.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldState.java @@ -19,74 +19,47 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ObjectNode; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; -import lombok.Data; -import lombok.NoArgsConstructor; +import lombok.EqualsAndHashCode; +import net.objecthunter.exp4j.Expression; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.script.api.tbel.TbUtils; +import org.thingsboard.server.actors.TbActorRef; import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.cf.configuration.Output; -import org.thingsboard.server.common.data.kv.BasicKvEntry; +import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.service.cf.CalculatedFieldResult; +import org.thingsboard.server.service.cf.TelemetryCalculatedFieldResult; -import java.util.List; import java.util.Map; -@Data -@NoArgsConstructor +@EqualsAndHashCode(callSuper = true) public class SimpleCalculatedFieldState extends BaseCalculatedFieldState { - public SimpleCalculatedFieldState(List requiredArguments) { - super(requiredArguments); - } + private ThreadLocal expression; - @Override - public CalculatedFieldType getType() { - return CalculatedFieldType.SIMPLE; + public SimpleCalculatedFieldState(EntityId entityId) { + super(entityId); } @Override - protected void validateNewEntry(ArgumentEntry newEntry) { - if (newEntry instanceof TsRollingArgumentEntry) { - throw new IllegalArgumentException("Rolling argument entry is not supported for simple calculated fields."); - } + public void setCtx(CalculatedFieldCtx ctx, TbActorRef actorCtx) { + super.setCtx(ctx, actorCtx); + this.expression = ctx.getSimpleExpressions().get(ctx.getExpression()); } @Override - public ListenableFuture performCalculation(CalculatedFieldCtx ctx) { - var expr = ctx.getCustomExpression().get(); - - for (Map.Entry entry : this.arguments.entrySet()) { - try { - BasicKvEntry kvEntry = ((SingleValueArgumentEntry) entry.getValue()).getKvEntryValue(); - double value = switch (kvEntry.getDataType()) { - case LONG -> kvEntry.getLongValue().map(Long::doubleValue).orElseThrow(); - case DOUBLE -> kvEntry.getDoubleValue().orElseThrow(); - case BOOLEAN -> kvEntry.getBooleanValue().map(b -> b ? 1.0 : 0.0).orElseThrow(); - case STRING, JSON -> Double.parseDouble(kvEntry.getValueAsString()); - }; - expr.setVariable(entry.getKey(), value); - } catch (NumberFormatException e) { - throw new IllegalArgumentException("Argument '" + entry.getKey() + "' is not a number."); - } - } - - double expressionResult = expr.evaluate(); + public ListenableFuture performCalculation(Map updatedArgs, CalculatedFieldCtx ctx) { + double expressionResult = ctx.evaluateSimpleExpression(expression.get(), this); Output output = ctx.getOutput(); - Object result = formatResult(expressionResult, output.getDecimalsByDefault()); + Object result = TbUtils.roundResult(expressionResult, output.getDecimalsByDefault()); JsonNode outputResult = createResultJson(ctx.isUseLatestTs(), output.getName(), result); - return Futures.immediateFuture(new CalculatedFieldResult(output.getType(), output.getScope(), outputResult)); - } - - private Object formatResult(double expressionResult, Integer decimals) { - if (decimals == null) { - return expressionResult; - } - if (decimals.equals(0)) { - return TbUtils.toInt(expressionResult); - } - return TbUtils.toFixed(expressionResult, decimals); + return Futures.immediateFuture(TelemetryCalculatedFieldResult.builder() + .type(output.getType()) + .scope(output.getScope()) + .result(outputResult) + .build()); } private JsonNode createResultJson(boolean useLatestTs, String outputName, Object result) { @@ -98,16 +71,20 @@ public class SimpleCalculatedFieldState extends BaseCalculatedFieldState { } else { valuesNode.set(outputName, JacksonUtil.valueToTree(result)); } + return toSimpleResult(useLatestTs, valuesNode); + } - long latestTs = getLatestTimestamp(); - if (useLatestTs && latestTs != -1) { - ObjectNode resultNode = JacksonUtil.newObjectNode(); - resultNode.put("ts", latestTs); - resultNode.set("values", valuesNode); - return resultNode; - } else { - return valuesNode; + @Override + protected void validateNewEntry(String key, ArgumentEntry newEntry) { + if (newEntry instanceof TsRollingArgumentEntry) { + throw new IllegalArgumentException("Unsupported argument type detected for argument: " + key + ". " + + "Rolling argument entry is not supported for simple calculated fields."); } } + @Override + public CalculatedFieldType getType() { + return CalculatedFieldType.SIMPLE; + } + } diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/SingleValueArgumentEntry.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/SingleValueArgumentEntry.java index 1585c9b2a9..d8e4fcf5d4 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/SingleValueArgumentEntry.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/SingleValueArgumentEntry.java @@ -20,9 +20,11 @@ import com.fasterxml.jackson.core.type.TypeReference; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; +import org.springframework.lang.Nullable; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.script.api.tbel.TbelCfArg; import org.thingsboard.script.api.tbel.TbelCfSingleValueArg; +import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.kv.AttributeKvEntry; import org.thingsboard.server.common.data.kv.BasicKvEntry; import org.thingsboard.server.common.data.kv.JsonDataEntry; @@ -37,11 +39,35 @@ import org.thingsboard.server.gen.transport.TransportProtos.TsKvProto; @AllArgsConstructor public class SingleValueArgumentEntry implements ArgumentEntry { - private long ts; - private BasicKvEntry kvEntryValue; - private Long version; + @Nullable + protected EntityId entityId; - private boolean forceResetPrevious; + protected long ts; + protected BasicKvEntry kvEntryValue; + protected Long version; + + protected boolean forceResetPrevious; + + public static final Long DEFAULT_VERSION = -1L; + + public SingleValueArgumentEntry(EntityId entityId, ArgumentEntry entry) { + this(entry); + this.entityId = entityId; + } + + public SingleValueArgumentEntry(ArgumentEntry entry) { + if (entry instanceof SingleValueArgumentEntry singleValueArgumentEntry) { + this.ts = singleValueArgumentEntry.ts; + this.kvEntryValue = singleValueArgumentEntry.kvEntryValue; + this.version = singleValueArgumentEntry.version; + this.forceResetPrevious = singleValueArgumentEntry.forceResetPrevious; + } + } + + public SingleValueArgumentEntry(EntityId entityId, TsKvProto entry) { + this(entry); + this.entityId = entityId; + } public SingleValueArgumentEntry(TsKvProto entry) { this.ts = entry.getTs(); @@ -51,6 +77,11 @@ public class SingleValueArgumentEntry implements ArgumentEntry { this.kvEntryValue = ProtoUtils.fromProto(entry.getKv()); } + public SingleValueArgumentEntry(EntityId entityId, AttributeValueProto entry) { + this(entry); + this.entityId = entityId; + } + public SingleValueArgumentEntry(AttributeValueProto entry) { this.ts = entry.getLastUpdateTs(); if (entry.hasVersion()) { @@ -59,6 +90,11 @@ public class SingleValueArgumentEntry implements ArgumentEntry { this.kvEntryValue = ProtoUtils.basicKvEntryFromProto(entry); } + public SingleValueArgumentEntry(EntityId entityId, KvEntry entry) { + this(entry); + this.entityId = entityId; + } + public SingleValueArgumentEntry(KvEntry entry) { if (entry instanceof TsKvEntry tsKvEntry) { this.ts = tsKvEntry.getTs(); @@ -70,6 +106,11 @@ public class SingleValueArgumentEntry implements ArgumentEntry { this.kvEntryValue = ProtoUtils.basicKvEntryFromKvEntry(entry); } + public SingleValueArgumentEntry(EntityId entityId, long ts, BasicKvEntry kvEntryValue, Long version) { + this(ts, kvEntryValue, version); + this.entityId = entityId; + } + public SingleValueArgumentEntry(long ts, BasicKvEntry kvEntryValue, Long version) { this.ts = ts; this.kvEntryValue = kvEntryValue; @@ -93,6 +134,9 @@ public class SingleValueArgumentEntry implements ArgumentEntry { @Override public TbelCfArg toTbelCfArg() { + if (isEmpty()) { + return new TbelCfSingleValueArg(ts, null); + } Object value = kvEntryValue.getValue(); if (kvEntryValue instanceof JsonDataEntry) { try { @@ -101,14 +145,21 @@ public class SingleValueArgumentEntry implements ArgumentEntry { } catch (Exception e) { } } + if (value instanceof Long longValue) { + if (longValue >= Integer.MIN_VALUE && longValue <= Integer.MAX_VALUE) { + value = longValue.intValue(); + } + } return new TbelCfSingleValueArg(ts, value); } @Override public boolean updateEntry(ArgumentEntry entry) { if (entry instanceof SingleValueArgumentEntry singleValueEntry) { - if (singleValueEntry.getTs() <= this.ts) { - return false; + if (singleValueEntry.getTs() < this.ts) { + if (!isDefaultValue()) { + return false; + } } Long newVersion = singleValueEntry.getVersion(); @@ -123,4 +174,9 @@ public class SingleValueArgumentEntry implements ArgumentEntry { } return false; } + + public boolean isDefaultValue() { + return DEFAULT_VERSION.equals(this.version); + } + } diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/RelatedEntitiesAggregationCalculatedFieldState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/RelatedEntitiesAggregationCalculatedFieldState.java new file mode 100644 index 0000000000..8159b1db67 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/RelatedEntitiesAggregationCalculatedFieldState.java @@ -0,0 +1,249 @@ +/** + * Copyright © 2016-2025 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.cf.ctx.state.aggregation; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import lombok.Getter; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.actors.TbActorRef; +import org.thingsboard.server.common.data.cf.CalculatedFieldType; +import org.thingsboard.server.common.data.cf.configuration.Output; +import org.thingsboard.server.common.data.cf.configuration.aggregation.AggFunctionInput; +import org.thingsboard.server.common.data.cf.configuration.aggregation.AggInput; +import org.thingsboard.server.common.data.cf.configuration.aggregation.AggKeyInput; +import org.thingsboard.server.common.data.cf.configuration.aggregation.AggMetric; +import org.thingsboard.server.common.data.cf.configuration.aggregation.RelatedEntitiesAggregationCalculatedFieldConfiguration; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.service.cf.CalculatedFieldResult; +import org.thingsboard.server.service.cf.TelemetryCalculatedFieldResult; +import org.thingsboard.server.service.cf.ctx.state.ArgumentEntry; +import org.thingsboard.server.service.cf.ctx.state.BaseCalculatedFieldState; +import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx; +import org.thingsboard.server.service.cf.ctx.state.aggregation.function.AggEntry; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.concurrent.ScheduledFuture; + +import static java.util.concurrent.TimeUnit.SECONDS; + +@Slf4j +@Getter +public class RelatedEntitiesAggregationCalculatedFieldState extends BaseCalculatedFieldState { + + @Setter + private long lastArgsRefreshTs = -1; + @Setter + private long lastMetricsEvalTs = -1; + @Setter + private long lastRelatedEntitiesRefreshTs = -1; + private long deduplicationIntervalMs = -1; + private Map metrics; + + private ScheduledFuture reevaluationFuture; + + public RelatedEntitiesAggregationCalculatedFieldState(EntityId entityId) { + super(entityId); + } + + @Override + public void setCtx(CalculatedFieldCtx ctx, TbActorRef actorCtx) { + super.setCtx(ctx, actorCtx); + var configuration = (RelatedEntitiesAggregationCalculatedFieldConfiguration) ctx.getCalculatedField().getConfiguration(); + metrics = configuration.getMetrics(); + deduplicationIntervalMs = SECONDS.toMillis(configuration.getDeduplicationIntervalInSec()); + } + + @Override + public void init(boolean restored) { + super.init(restored); + if (restored) { + scheduleReevaluation(); + } + } + + @Override + public void close() { + super.close(); + if (reevaluationFuture != null) { + reevaluationFuture.cancel(true); + reevaluationFuture = null; + } + } + + @Override + public void reset() { // must reset everything dependent on arguments + super.reset(); + lastArgsRefreshTs = -1; + lastMetricsEvalTs = -1; + lastRelatedEntitiesRefreshTs = -1; + metrics = null; + } + + public void updateLastRelatedEntitiesRefreshTs() { + lastRelatedEntitiesRefreshTs = System.currentTimeMillis(); + } + + @Override + public CalculatedFieldType getType() { + return CalculatedFieldType.RELATED_ENTITIES_AGGREGATION; + } + + @Override + public Map update(Map argumentValues, CalculatedFieldCtx ctx) { + lastArgsRefreshTs = System.currentTimeMillis(); + return super.update(argumentValues, ctx); + } + + public List checkRelatedEntities(List relatedEntities) { + Map> entityInputs = prepareInputs(); + findOutdatedEntities(entityInputs, relatedEntities).forEach(this::cleanupEntityData); + updateLastRelatedEntitiesRefreshTs(); + return findMissingEntities(entityInputs, relatedEntities); + } + + private List findMissingEntities(Map> entityInputs, List relatedEntities) { + List missing = new ArrayList<>(); + relatedEntities.forEach(entityId -> { + if (!entityInputs.containsKey(entityId)) { + missing.add(entityId); + log.warn("[{}] Missing related entity inputs for {}", ctx.getCfId(), entityId); + } + }); + return missing; + } + + private List findOutdatedEntities(Map> entityInputs, List relatedEntities) { + List outdated = new ArrayList<>(); + entityInputs.keySet().forEach(entityId -> { + if (!relatedEntities.contains(entityId)) { + outdated.add(entityId); + log.warn("[{}] CF state keeps outdated related entity {}", ctx.getCfId(), entityId); + } + }); + return outdated; + } + + public Map updateEntityData(Map fetchedArgs) { + lastMetricsEvalTs = -1; + return update(fetchedArgs, ctx); + } + + public void cleanupEntityData(EntityId relatedEntityId) { + arguments.values().forEach(argEntry -> { + RelatedEntitiesArgumentEntry aggEntry = (RelatedEntitiesArgumentEntry) argEntry; + aggEntry.getEntityInputs().remove(relatedEntityId); + }); + lastMetricsEvalTs = -1; + lastArgsRefreshTs = System.currentTimeMillis(); + } + + public void scheduleReevaluation() { + ScheduledFuture future = ctx.scheduleReevaluation(deduplicationIntervalMs, actorCtx); + if (future != null) { + reevaluationFuture = future; + } + } + + @Override + public ListenableFuture performCalculation(Map updatedArgs, CalculatedFieldCtx ctx) throws Exception { + boolean cfUpdated = updatedArgs != null && updatedArgs.isEmpty(); + if (shouldRecalculate() || cfUpdated) { + Output output = ctx.getOutput(); + ObjectNode aggResult = aggregateMetrics(output); + lastMetricsEvalTs = System.currentTimeMillis(); + scheduleReevaluation(); + return Futures.immediateFuture(TelemetryCalculatedFieldResult.builder() + .type(output.getType()) + .scope(output.getScope()) + .result(toSimpleResult(ctx.isUseLatestTs(), aggResult)) + .build()); + } else { + return Futures.immediateFuture(TelemetryCalculatedFieldResult.EMPTY); + } + } + + private boolean shouldRecalculate() { + boolean intervalPassed = lastMetricsEvalTs <= System.currentTimeMillis() - deduplicationIntervalMs; + boolean argsUpdatedDuringInterval = lastArgsRefreshTs > lastMetricsEvalTs; + return intervalPassed && argsUpdatedDuringInterval; + } + + private Map> prepareInputs() { + Map> inputs = new HashMap<>(); + for (Map.Entry argEntry : arguments.entrySet()) { + String key = argEntry.getKey(); + RelatedEntitiesArgumentEntry relatedEntitiesArgumentEntry = (RelatedEntitiesArgumentEntry) argEntry.getValue(); + relatedEntitiesArgumentEntry.getEntityInputs().forEach((entityId, argumentEntry) -> { + inputs.computeIfAbsent(entityId, k -> new HashMap<>()).put(key, argumentEntry); + }); + } + return inputs; + } + + private ObjectNode aggregateMetrics(Output output) throws Exception { + ObjectNode aggResult = JacksonUtil.newObjectNode(); + Map> inputs = prepareInputs(); + for (Entry entry : metrics.entrySet()) { + String metricKey = entry.getKey(); + AggMetric metric = entry.getValue(); + + AggEntry aggMetricEntry = AggEntry.createAggFunction(metric.getFunction()); + aggregateMetric(metric, aggMetricEntry, inputs); + aggMetricEntry.result(output.getDecimalsByDefault()).ifPresent(result -> { + aggResult.set(metricKey, JacksonUtil.valueToTree(result)); + }); + } + return aggResult; + } + + private void aggregateMetric(AggMetric metric, AggEntry aggEntry, Map> inputs) throws Exception { + for (Map entityInputs : inputs.values()) { + if (applyAggregation(metric.getFilter(), entityInputs)) { + Object arg = resolveAggregationInput(metric.getInput(), entityInputs); + if (arg != null) { + aggEntry.update(arg); + } + } + } + } + + private boolean applyAggregation(String filter, Map entityInputs) throws Exception { + if (filter == null || filter.isEmpty()) { + return true; + } else { + Object filterResult = ctx.evaluateTbelExpression(filter, entityInputs, getLatestTimestamp()).get(); + return filterResult instanceof Boolean booleanResult && booleanResult; + } + } + + private Object resolveAggregationInput(AggInput aggInput, Map entityInputs) throws Exception { + if (aggInput instanceof AggFunctionInput functionInput) { + return ctx.evaluateTbelExpression(functionInput.getFunction(), entityInputs, getLatestTimestamp()).get(); + } else { + String inputKey = ((AggKeyInput) aggInput).getKey(); + return entityInputs.get(inputKey).getValue(); + } + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/RelatedEntitiesArgumentEntry.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/RelatedEntitiesArgumentEntry.java new file mode 100644 index 0000000000..2abe78d243 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/RelatedEntitiesArgumentEntry.java @@ -0,0 +1,86 @@ +/** + * Copyright © 2016-2025 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.cf.ctx.state.aggregation; + +import lombok.AllArgsConstructor; +import lombok.Data; +import org.thingsboard.script.api.tbel.TbelCfArg; +import org.thingsboard.script.api.tbel.TbelCfRelatedEntitiesArgumentValue; +import org.thingsboard.script.api.tbel.TbelCfSingleValueArg; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.service.cf.ctx.state.ArgumentEntry; +import org.thingsboard.server.service.cf.ctx.state.ArgumentEntryType; +import org.thingsboard.server.service.cf.ctx.state.SingleValueArgumentEntry; + +import java.util.Map; +import java.util.stream.Collectors; + +@Data +@AllArgsConstructor +public class RelatedEntitiesArgumentEntry implements ArgumentEntry { + + private final Map entityInputs; + + private boolean forceResetPrevious; + + @Override + public ArgumentEntryType getType() { + return ArgumentEntryType.RELATED_ENTITIES; + } + + @Override + public Object getValue() { + return entityInputs; + } + + @Override + public boolean updateEntry(ArgumentEntry entry) { + if (entry instanceof RelatedEntitiesArgumentEntry relatedEntitiesArgumentEntry) { + entityInputs.putAll(relatedEntitiesArgumentEntry.entityInputs); + return true; + } else if (entry instanceof SingleValueArgumentEntry singleValueArgumentEntry) { + if (entry.isForceResetPrevious()) { + entityInputs.put(singleValueArgumentEntry.getEntityId(), singleValueArgumentEntry); + return true; + } + ArgumentEntry argumentEntry = entityInputs.get(singleValueArgumentEntry.getEntityId()); + if (argumentEntry != null) { + argumentEntry.updateEntry(singleValueArgumentEntry); + } else { + entityInputs.put(singleValueArgumentEntry.getEntityId(), singleValueArgumentEntry); + } + return true; + } else { + throw new IllegalArgumentException("Unsupported argument entry type for aggregation argument entry: " + entry.getType()); + } + } + + @Override + public boolean isEmpty() { + return entityInputs.isEmpty(); + } + + @Override + public TbelCfArg toTbelCfArg() { + var inputs = entityInputs.entrySet().stream() + .collect(Collectors.toMap( + e -> e.getKey().getId(), + e -> (TbelCfSingleValueArg) e.getValue().toTbelCfArg() + )); + return new TbelCfRelatedEntitiesArgumentValue(inputs); + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/function/AggEntry.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/function/AggEntry.java new file mode 100644 index 0000000000..c4b93fd91d --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/function/AggEntry.java @@ -0,0 +1,58 @@ +/** + * Copyright © 2016-2025 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.cf.ctx.state.aggregation.function; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import org.thingsboard.server.common.data.cf.configuration.aggregation.AggFunction; + +import java.util.Optional; + +@JsonTypeInfo( + use = JsonTypeInfo.Id.NAME, + include = JsonTypeInfo.As.PROPERTY, + property = "type" +) +@JsonSubTypes({ + @JsonSubTypes.Type(value = AvgAggEntry.class, name = "AVG"), + @JsonSubTypes.Type(value = CountAggEntry.class, name = "COUNT"), + @JsonSubTypes.Type(value = CountUniqueAggEntry.class, name = "COUNT_UNIQUE"), + @JsonSubTypes.Type(value = MaxAggEntry.class, name = "MAX"), + @JsonSubTypes.Type(value = MinAggEntry.class, name = "MIN"), + @JsonSubTypes.Type(value = SumAggEntry.class, name = "SUM") +}) +public interface AggEntry { + + @JsonIgnore + AggFunction getType(); + + void update(Object value); + + Optional result(Integer precision); + + static AggEntry createAggFunction(AggFunction function) { + return switch (function) { + case MIN -> new MinAggEntry(); + case MAX -> new MaxAggEntry(); + case SUM -> new SumAggEntry(); + case AVG -> new AvgAggEntry(); + case COUNT -> new CountAggEntry(); + case COUNT_UNIQUE -> new CountUniqueAggEntry(); + }; + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/function/AvgAggEntry.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/function/AvgAggEntry.java new file mode 100644 index 0000000000..e063ff2ea2 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/function/AvgAggEntry.java @@ -0,0 +1,47 @@ +/** + * Copyright © 2016-2025 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.cf.ctx.state.aggregation.function; + +import org.thingsboard.script.api.tbel.TbUtils; +import org.thingsboard.server.common.data.cf.configuration.aggregation.AggFunction; + +import java.math.BigDecimal; +import java.math.RoundingMode; + +public class AvgAggEntry extends BaseAggEntry { + + private BigDecimal sum = BigDecimal.ZERO; + private long count = 0L; + + @Override + protected void doUpdate(double value) { + if (value != 0.0) { + sum = sum.add(BigDecimal.valueOf(value)); + } + this.count++; + } + + @Override + protected Object prepareResult(Integer precision) { + double result = sum.divide(BigDecimal.valueOf(count), RoundingMode.HALF_UP).doubleValue(); + return TbUtils.roundResult(result, precision); + } + + @Override + public AggFunction getType() { + return AggFunction.AVG; + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/function/BaseAggEntry.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/function/BaseAggEntry.java new file mode 100644 index 0000000000..8ca523938d --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/function/BaseAggEntry.java @@ -0,0 +1,55 @@ +/** + * Copyright © 2016-2025 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.cf.ctx.state.aggregation.function; + +import java.util.Optional; + +public abstract class BaseAggEntry implements AggEntry { + + private boolean hasResult = false; + + @Override + public void update(Object value) { + doUpdate(extractDoubleValue(value)); + hasResult = true; + } + + @Override + public Optional result(Integer precision) { + if (hasResult) { + hasResult = false; + return Optional.of(prepareResult(precision)); + } else { + return Optional.empty(); + } + } + + protected abstract void doUpdate(double value); + + protected abstract Object prepareResult(Integer precision); + + protected double extractDoubleValue(Object value) { + try { + if (value instanceof Number number) { + return number.doubleValue(); + } + return Double.parseDouble(value.toString()); + } catch (Exception e) { + throw new NumberFormatException("Cannot parse value " + value.toString()); + } + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/function/CountAggEntry.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/function/CountAggEntry.java new file mode 100644 index 0000000000..09116985d2 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/function/CountAggEntry.java @@ -0,0 +1,41 @@ +/** + * Copyright © 2016-2025 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.cf.ctx.state.aggregation.function; + +import org.thingsboard.server.common.data.cf.configuration.aggregation.AggFunction; + +import java.util.Optional; + +public class CountAggEntry implements AggEntry { + + private long count = 0L; + + @Override + public void update(Object value) { + count++; + } + + @Override + public Optional result(Integer precision) { + return Optional.of(count); + } + + @Override + public AggFunction getType() { + return AggFunction.COUNT; + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/function/CountUniqueAggEntry.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/function/CountUniqueAggEntry.java new file mode 100644 index 0000000000..3c14d7c9b9 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/function/CountUniqueAggEntry.java @@ -0,0 +1,43 @@ +/** + * Copyright © 2016-2025 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.cf.ctx.state.aggregation.function; + +import org.thingsboard.server.common.data.cf.configuration.aggregation.AggFunction; + +import java.util.Optional; +import java.util.Set; + +public class CountUniqueAggEntry implements AggEntry { + + private Set items; + + @Override + public void update(Object value) { + if (value != null) { + items.add(String.valueOf(value)); + } + } + + @Override + public Optional result(Integer precision) { + return Optional.of(items.size()); + } + + @Override + public AggFunction getType() { + return AggFunction.COUNT_UNIQUE; + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/function/MaxAggEntry.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/function/MaxAggEntry.java new file mode 100644 index 0000000000..6d734a5a08 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/function/MaxAggEntry.java @@ -0,0 +1,41 @@ +/** + * Copyright © 2016-2025 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.cf.ctx.state.aggregation.function; + +import org.thingsboard.script.api.tbel.TbUtils; +import org.thingsboard.server.common.data.cf.configuration.aggregation.AggFunction; + +public class MaxAggEntry extends BaseAggEntry { + + private double max = Double.MIN_VALUE; + + @Override + protected void doUpdate(double value) { + if (value > max) { + max = value; + } + } + + @Override + protected Object prepareResult(Integer precision) { + return TbUtils.roundResult(max, precision); + } + + @Override + public AggFunction getType() { + return AggFunction.MAX; + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/function/MinAggEntry.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/function/MinAggEntry.java new file mode 100644 index 0000000000..e517ad305f --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/function/MinAggEntry.java @@ -0,0 +1,41 @@ +/** + * Copyright © 2016-2025 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.cf.ctx.state.aggregation.function; + +import org.thingsboard.script.api.tbel.TbUtils; +import org.thingsboard.server.common.data.cf.configuration.aggregation.AggFunction; + +public class MinAggEntry extends BaseAggEntry { + + private double min = Double.MAX_VALUE; + + @Override + protected void doUpdate(double value) { + if (value < min) { + min = value; + } + } + + @Override + protected Object prepareResult(Integer precision) { + return TbUtils.roundResult(min, precision); + } + + @Override + public AggFunction getType() { + return AggFunction.MIN; + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/function/SumAggEntry.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/function/SumAggEntry.java new file mode 100644 index 0000000000..fe29d27b7e --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/function/SumAggEntry.java @@ -0,0 +1,43 @@ +/** + * Copyright © 2016-2025 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.cf.ctx.state.aggregation.function; + +import org.thingsboard.script.api.tbel.TbUtils; +import org.thingsboard.server.common.data.cf.configuration.aggregation.AggFunction; + +import java.math.BigDecimal; + +public class SumAggEntry extends BaseAggEntry { + + private BigDecimal sum = BigDecimal.ZERO; + + @Override + protected void doUpdate(double value) { + if (value != 0.0) { + sum = sum.add(BigDecimal.valueOf(value)); + } + } + + @Override + protected Object prepareResult(Integer precision) { + return TbUtils.roundResult(sum.doubleValue(), precision); + } + + @Override + public AggFunction getType() { + return AggFunction.SUM; + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/single/AggIntervalEntry.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/single/AggIntervalEntry.java new file mode 100644 index 0000000000..338e667dd2 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/single/AggIntervalEntry.java @@ -0,0 +1,36 @@ +/** + * Copyright © 2016-2025 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.cf.ctx.state.aggregation.single; + +import lombok.AllArgsConstructor; +import lombok.Data; + +@Data +@AllArgsConstructor +public class AggIntervalEntry { + + private Long startTs; + private Long endTs; + + public boolean belongsToInterval(long ts) { + return ts >= startTs && ts < endTs; + } + + public long getIntervalDuration() { + return endTs - startTs; + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/single/AggIntervalEntryStatus.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/single/AggIntervalEntryStatus.java new file mode 100644 index 0000000000..fbf344e5d3 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/single/AggIntervalEntryStatus.java @@ -0,0 +1,45 @@ +/** + * Copyright © 2016-2025 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.cf.ctx.state.aggregation.single; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class AggIntervalEntryStatus { + + private long lastArgsRefreshTs = -1; + + private long lastMetricsEvalTs = -1; + + public AggIntervalEntryStatus(long lastArgsRefreshTs) { + this.lastArgsRefreshTs = lastArgsRefreshTs; + } + + public boolean intervalPassed(long checkInterval) { + return lastMetricsEvalTs <= System.currentTimeMillis() - checkInterval; + } + + @JsonIgnore + public boolean argsUpdated() { + return lastArgsRefreshTs > -1; + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/single/EntityAggregationArgumentEntry.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/single/EntityAggregationArgumentEntry.java new file mode 100644 index 0000000000..7ec5098bc3 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/single/EntityAggregationArgumentEntry.java @@ -0,0 +1,87 @@ +/** + * Copyright © 2016-2025 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.cf.ctx.state.aggregation.single; + +import com.fasterxml.jackson.databind.JsonNode; +import lombok.Data; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.script.api.tbel.TbelCfArg; +import org.thingsboard.server.service.cf.ctx.state.ArgumentEntry; +import org.thingsboard.server.service.cf.ctx.state.ArgumentEntryType; +import org.thingsboard.server.service.cf.ctx.state.SingleValueArgumentEntry; + +import java.util.Map; + +@Data +public class EntityAggregationArgumentEntry implements ArgumentEntry { + + private Map aggIntervals; + + private boolean forceResetPrevious; + + public EntityAggregationArgumentEntry(Map aggIntervals) { + this.aggIntervals = aggIntervals; + } + + @Override + public ArgumentEntryType getType() { + return ArgumentEntryType.ENTITY_AGGREGATION; + } + + @Override + public Object getValue() { + return aggIntervals; + } + + @Override + public boolean updateEntry(ArgumentEntry entry) { + boolean updated = false; + if (entry instanceof EntityAggregationArgumentEntry entityAggEntry) { + aggIntervals.putAll(entityAggEntry.getAggIntervals()); + } else if (entry instanceof SingleValueArgumentEntry singleValueArgEntry) { + long entryTs = singleValueArgEntry.getTs(); + long argUpdateTs = System.currentTimeMillis(); + for (Map.Entry aggIntervalEntry : aggIntervals.entrySet()) { + if (singleValueArgEntry.isForceResetPrevious()) { + aggIntervalEntry.getValue().setLastArgsRefreshTs(argUpdateTs); + updated = true; + continue; + } + if (aggIntervalEntry.getKey().belongsToInterval(entryTs)) { + aggIntervalEntry.getValue().setLastArgsRefreshTs(argUpdateTs); + return true; + } + } + } + return updated; + } + + @Override + public boolean isEmpty() { + return aggIntervals.isEmpty(); + } + + @Override + public JsonNode jsonValue() { + return JacksonUtil.valueToTree(aggIntervals); + } + + @Override + public TbelCfArg toTbelCfArg() { + return null; + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/single/EntityAggregationCalculatedFieldState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/single/EntityAggregationCalculatedFieldState.java new file mode 100644 index 0000000000..b07600695c --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/single/EntityAggregationCalculatedFieldState.java @@ -0,0 +1,271 @@ +/** + * Copyright © 2016-2025 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.cf.ctx.state.aggregation.single; + +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.script.api.tbel.TbUtils; +import org.thingsboard.server.actors.TbActorRef; +import org.thingsboard.server.common.data.cf.CalculatedFieldType; +import org.thingsboard.server.common.data.cf.configuration.Output; +import org.thingsboard.server.common.data.cf.configuration.aggregation.AggKeyInput; +import org.thingsboard.server.common.data.cf.configuration.aggregation.AggMetric; +import org.thingsboard.server.common.data.cf.configuration.aggregation.single.EntityAggregationCalculatedFieldConfiguration; +import org.thingsboard.server.common.data.cf.configuration.aggregation.single.interval.AggInterval; +import org.thingsboard.server.common.data.cf.configuration.aggregation.single.interval.Watermark; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.service.cf.CalculatedFieldProcessingService; +import org.thingsboard.server.service.cf.CalculatedFieldResult; +import org.thingsboard.server.service.cf.TelemetryCalculatedFieldResult; +import org.thingsboard.server.service.cf.ctx.state.ArgumentEntry; +import org.thingsboard.server.service.cf.ctx.state.BaseCalculatedFieldState; +import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx; + +import java.time.Instant; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +import static org.thingsboard.server.utils.CalculatedFieldArgumentUtils.createDefaultMetricArgumentEntry; + +public class EntityAggregationCalculatedFieldState extends BaseCalculatedFieldState { + + private AggInterval interval; + private long watermarkDuration; + private long checkInterval; + private Map metrics; + + private CalculatedFieldProcessingService cfProcessingService; + + public EntityAggregationCalculatedFieldState(EntityId entityId) { + super(entityId); + } + + @Override + public void setCtx(CalculatedFieldCtx ctx, TbActorRef actorCtx) { + super.setCtx(ctx, actorCtx); + this.cfProcessingService = ctx.getCfProcessingService(); + var configuration = (EntityAggregationCalculatedFieldConfiguration) ctx.getCalculatedField().getConfiguration(); + Watermark watermark = configuration.getWatermark(); + watermarkDuration = watermark == null ? 0 : TimeUnit.SECONDS.toMillis(watermark.getDuration()); + checkInterval = TimeUnit.SECONDS.toMillis(ctx.getSystemContext().getCfCheckInterval()); + interval = configuration.getInterval(); + metrics = configuration.getMetrics(); + } + + @Override + public void init(boolean restored) { + super.init(restored); + if (restored) { + fillMissingIntervals(); + } + } + + @Override + public CalculatedFieldType getType() { + return CalculatedFieldType.ENTITY_AGGREGATION; + } + + @Override + public ListenableFuture performCalculation(Map updatedArgs, CalculatedFieldCtx ctx) throws Exception { + createIntervalIfNotExist(); + long now = System.currentTimeMillis(); + + Map> results = new HashMap<>(); + List expiredIntervals = new ArrayList<>(); + getIntervals().forEach((intervalEntry, argIntervalStatuses) -> { + processInterval(now, intervalEntry, argIntervalStatuses, expiredIntervals, results); + }); + removeExpiredIntervals(expiredIntervals); + + Output output = ctx.getOutput(); + ArrayNode result = toResult(results, output.getDecimalsByDefault()); + if (result.isEmpty()) { + return Futures.immediateFuture(TelemetryCalculatedFieldResult.EMPTY); + } + return Futures.immediateFuture(TelemetryCalculatedFieldResult.builder() + .type(output.getType()) + .scope(output.getScope()) + .result(result) + .build()); + } + + private void removeExpiredIntervals(List expiredIntervals) { + expiredIntervals.forEach(expiredInterval -> { + arguments.values().stream() + .map(EntityAggregationArgumentEntry.class::cast) + .forEach(arg -> arg.getAggIntervals().remove(expiredInterval)); + }); + } + + private void createIntervalIfNotExist() { + AggIntervalEntry currentInterval = new AggIntervalEntry(interval.getCurrentIntervalStartTs(), interval.getCurrentIntervalEndTs()); + arguments.forEach((argName, argumentEntry) -> { + var entityAggEntry = (EntityAggregationArgumentEntry) argumentEntry; + entityAggEntry.getAggIntervals().computeIfAbsent(currentInterval, current -> new AggIntervalEntryStatus()); + }); + } + + private void fillMissingIntervals() { + ZoneId zoneId = interval.getZoneId(); + long currentIntervalEndTs = interval.getCurrentIntervalEndTs(); + + Map> intervals = getIntervals(); + AggIntervalEntry lastIntervalEntry = intervals.keySet().stream().max(Comparator.comparing(AggIntervalEntry::getEndTs)).orElse(null); + if (lastIntervalEntry == null) { + return; + } + + ZonedDateTime nextStart = Instant.ofEpochMilli(lastIntervalEntry.getEndTs()).atZone(zoneId); + ZonedDateTime nextEnd = interval.getNextIntervalStart(nextStart); + + while (nextEnd.toInstant().toEpochMilli() <= currentIntervalEndTs) { + long nextStartTs = nextStart.toInstant().toEpochMilli(); + long nextEndTs = nextEnd.toInstant().toEpochMilli(); + AggIntervalEntry missing = new AggIntervalEntry(nextStartTs, nextEndTs); + + arguments.forEach((argName, argumentEntry) -> { + var entityAggEntry = (EntityAggregationArgumentEntry) argumentEntry; + AggIntervalEntryStatus intervalEntryStatus = new AggIntervalEntryStatus(System.currentTimeMillis()); + entityAggEntry.getAggIntervals().computeIfAbsent(missing, missingInterval -> intervalEntryStatus); + }); + + nextStart = nextEnd; + nextEnd = interval.getNextIntervalStart(nextStart); + } + } + + private Map> getIntervals() { + Map> intervals = new HashMap<>(); + arguments.forEach((argName, entry) -> { + var argEntry = (EntityAggregationArgumentEntry) entry; + argEntry.getAggIntervals().forEach((intervalEntry, status) -> + intervals.computeIfAbsent(intervalEntry, i -> new HashMap<>()).put(argName, status) + ); + }); + return intervals; + } + + private void processInterval(long now, + AggIntervalEntry intervalEntry, + Map args, + List expiredIntervals, + Map> results) { + long startTs = intervalEntry.getStartTs(); + long endTs = intervalEntry.getEndTs(); + + if (now - endTs > watermarkDuration) { + handleExpiredInterval(intervalEntry, args, results); + expiredIntervals.add(intervalEntry); + } else if (now - startTs >= intervalEntry.getIntervalDuration()) { + handleActiveInterval(intervalEntry, args, results); + } + } + + private void handleExpiredInterval(AggIntervalEntry intervalEntry, + Map args, + Map> results) { + args.forEach((argName, argEntryIntervalStatus) -> { + if (argEntryIntervalStatus.getLastArgsRefreshTs() > argEntryIntervalStatus.getLastMetricsEvalTs()) { + argEntryIntervalStatus.setLastMetricsEvalTs(System.currentTimeMillis()); + processMetric(intervalEntry, argName, false, results); + } else if (argEntryIntervalStatus.getLastMetricsEvalTs() == -1) { + argEntryIntervalStatus.setLastMetricsEvalTs(System.currentTimeMillis()); + processMetric(intervalEntry, argName, true, results); + } + }); + } + + private void handleActiveInterval(AggIntervalEntry intervalEntry, + Map args, + Map> results) { + args.forEach((argName, argEntryIntervalStatus) -> { + if (argEntryIntervalStatus.intervalPassed(checkInterval)) { + if (argEntryIntervalStatus.argsUpdated()) { + argEntryIntervalStatus.setLastMetricsEvalTs(System.currentTimeMillis()); + argEntryIntervalStatus.setLastArgsRefreshTs(-1); + processMetric(intervalEntry, argName, false, results); + } else if (argEntryIntervalStatus.getLastMetricsEvalTs() == -1) { + argEntryIntervalStatus.setLastMetricsEvalTs(System.currentTimeMillis()); + processMetric(intervalEntry, argName, true, results); + } + } + }); + } + + private void processMetric(AggIntervalEntry intervalEntry, + String argName, + boolean useDefault, + Map> results) { + String metricName = findMetricName(argName); + if (metricName != null) { + AggMetric metric = metrics.get(metricName); + String argKey = ctx.getArguments().get(argName).getRefEntityKey().getKey(); + ArgumentEntry metricEntry = useDefault + ? createDefaultMetricArgumentEntry(argKey, metric) + : cfProcessingService.fetchMetricDuringInterval(ctx.getTenantId(), entityId, argKey, metric, intervalEntry); + if (!metricEntry.isEmpty()) { + results.computeIfAbsent(intervalEntry, i -> new HashMap<>()).put(metricName, metricEntry); + } + } + } + + private String findMetricName(String argName) { + return metrics.entrySet().stream() + .filter(e -> ((AggKeyInput) e.getValue().getInput()).getKey().equals(argName)) + .map(Map.Entry::getKey) + .findFirst() + .orElse(null); + } + + protected ArrayNode toResult(Map> results, Integer precision) { + ArrayNode result = JacksonUtil.newArrayNode(); + results.forEach((interval, args) -> { + ObjectNode metricsNode = JacksonUtil.newObjectNode(); + for (Map.Entry entry : args.entrySet()) { + String metricName = entry.getKey(); + ArgumentEntry argumentEntry = entry.getValue(); + if (!argumentEntry.isEmpty()) { + Object resultValue = argumentEntry.getValue() instanceof Number number + ? TbUtils.roundResult(number.doubleValue(), precision) + : argumentEntry.getValue(); + metricsNode.put(metricName, JacksonUtil.toString(resultValue)); + } + } + if (!metricsNode.isEmpty()) { + ObjectNode resultNode = JacksonUtil.newObjectNode(); + resultNode.put("ts", interval.getEndTs() - 1); + resultNode.set("values", metricsNode); + result.add(resultNode); + } + }); + return result; + } + + @Override + public boolean isReady() { + return true; + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/alarm/AlarmCalculatedFieldState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/alarm/AlarmCalculatedFieldState.java new file mode 100644 index 0000000000..342f7534c2 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/alarm/AlarmCalculatedFieldState.java @@ -0,0 +1,552 @@ +/** + * Copyright © 2016-2025 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.cf.ctx.state.alarm; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.common.util.KvUtil; +import org.thingsboard.rule.engine.action.TbAlarmResult; +import org.thingsboard.server.actors.TbActorRef; +import org.thingsboard.server.common.data.StringUtils; +import org.thingsboard.server.common.data.alarm.Alarm; +import org.thingsboard.server.common.data.alarm.AlarmApiCallResult; +import org.thingsboard.server.common.data.alarm.AlarmCreateOrUpdateActiveRequest; +import org.thingsboard.server.common.data.alarm.AlarmSeverity; +import org.thingsboard.server.common.data.alarm.AlarmUpdateRequest; +import org.thingsboard.server.common.data.alarm.rule.AlarmRule; +import org.thingsboard.server.common.data.alarm.rule.condition.AlarmCondition; +import org.thingsboard.server.common.data.alarm.rule.condition.AlarmConditionType; +import org.thingsboard.server.common.data.alarm.rule.condition.AlarmConditionValue; +import org.thingsboard.server.common.data.alarm.rule.condition.expression.AlarmConditionExpression; +import org.thingsboard.server.common.data.alarm.rule.condition.expression.AlarmConditionFilter; +import org.thingsboard.server.common.data.alarm.rule.condition.expression.ComplexOperation; +import org.thingsboard.server.common.data.alarm.rule.condition.expression.SimpleAlarmConditionExpression; +import org.thingsboard.server.common.data.alarm.rule.condition.expression.TbelAlarmConditionExpression; +import org.thingsboard.server.common.data.alarm.rule.condition.expression.predicate.BooleanFilterPredicate; +import org.thingsboard.server.common.data.alarm.rule.condition.expression.predicate.ComplexFilterPredicate; +import org.thingsboard.server.common.data.alarm.rule.condition.expression.predicate.KeyFilterPredicate; +import org.thingsboard.server.common.data.alarm.rule.condition.expression.predicate.NumericFilterPredicate; +import org.thingsboard.server.common.data.alarm.rule.condition.expression.predicate.StringFilterPredicate; +import org.thingsboard.server.common.data.audit.ActionType; +import org.thingsboard.server.common.data.cf.CalculatedFieldType; +import org.thingsboard.server.common.data.cf.configuration.AlarmCalculatedFieldConfiguration; +import org.thingsboard.server.common.data.id.DashboardId; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.kv.KvEntry; +import org.thingsboard.server.service.cf.AlarmCalculatedFieldResult; +import org.thingsboard.server.service.cf.CalculatedFieldResult; +import org.thingsboard.server.service.cf.ctx.state.ArgumentEntry; +import org.thingsboard.server.service.cf.ctx.state.BaseCalculatedFieldState; +import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx; +import org.thingsboard.server.service.cf.ctx.state.SingleValueArgumentEntry; + +import java.util.Comparator; +import java.util.Map; +import java.util.TreeMap; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Function; + +import static org.thingsboard.server.common.data.StringUtils.equalsAny; +import static org.thingsboard.server.common.data.StringUtils.splitByCommaWithoutQuotes; +import static org.thingsboard.server.service.cf.ctx.state.alarm.AlarmEvalResult.Status.FALSE; +import static org.thingsboard.server.service.cf.ctx.state.alarm.AlarmEvalResult.Status.NOT_YET_TRUE; +import static org.thingsboard.server.service.cf.ctx.state.alarm.AlarmEvalResult.Status.TRUE; + +@EqualsAndHashCode(callSuper = true) +@Slf4j +public class AlarmCalculatedFieldState extends BaseCalculatedFieldState { + + private AlarmCalculatedFieldConfiguration configuration; + private String alarmType; + + @Getter + private final Map createRuleStates = new TreeMap<>(Comparator.comparing(Enum::ordinal)); + @Getter + @Setter + private AlarmRuleState clearRuleState; + + @Getter + private Alarm currentAlarm; + private boolean initialFetchDone; + + // TODO: deprecate device profile node, describe the differences and improvements + + public AlarmCalculatedFieldState(EntityId entityId) { + super(entityId); + } + + @Override + public void setCtx(CalculatedFieldCtx ctx, TbActorRef actorCtx) { + super.setCtx(ctx, actorCtx); + this.configuration = getConfiguration(ctx); + this.alarmType = ctx.getCalculatedField().getName(); + + Map createRules = configuration.getCreateRules(); + createRules.forEach((severity, rule) -> { + AlarmRuleState ruleState = createRuleStates.get(severity); + if (ruleState != null) { + ruleState.setAlarmRule(rule); + } + }); + AlarmRule clearRule = configuration.getClearRule(); + if (clearRule != null && clearRuleState != null) { + clearRuleState.setAlarmRule(clearRule); + } + + if (currentAlarm != null && !currentAlarm.getType().equals(alarmType)) { + currentAlarm = null; + initialFetchDone = false; + } + } + + @Override + public void init(boolean restored) { + super.init(restored); + AtomicBoolean reevalNeeded = new AtomicBoolean(false); + Map createRules = configuration.getCreateRules(); + for (AlarmSeverity severity : AlarmSeverity.values()) { + AlarmRule rule = createRules.get(severity); + if (rule != null) { + createRuleStates.compute(severity, (__, ruleState) -> { + return initRuleState(severity, rule, ruleState, reevalNeeded); + }); + } else { + AlarmRuleState state = createRuleStates.remove(severity); + if (state != null) { + clearState(state); + } + } + } + + AlarmRule clearRule = configuration.getClearRule(); + if (clearRule != null) { + clearRuleState = initRuleState(null, clearRule, clearRuleState, reevalNeeded); + } else { + if (clearRuleState != null) { + clearState(clearRuleState); + clearRuleState = null; + } + } + log.debug("Initialized create rule states {} and clear rule state {} for {}", createRuleStates, clearRuleState, configuration); + + if (reevalNeeded.get()) { + initCurrentAlarm(ctx); + createOrClearAlarms(state -> { + if (state.getCondition().getType() == AlarmConditionType.DURATION) { + AlarmEvalResult evalResult = state.reeval(System.currentTimeMillis(), ctx); + if (evalResult.getStatus() == TRUE || evalResult.getStatus() == NOT_YET_TRUE) { + ScheduledFuture future = ctx.scheduleReevaluation(evalResult.getLeftDuration(), actorCtx); + if (future != null) { + state.setDurationCheckFuture(future); + } + } + } + return AlarmEvalResult.NOT_YET_TRUE; + }, ctx); + } + } + + private AlarmRuleState initRuleState(AlarmSeverity severity, AlarmRule rule, AlarmRuleState ruleState, AtomicBoolean reevalNeeded) { + if (ruleState == null) { + ruleState = new AlarmRuleState(severity, rule, this); + } else { + // when restored + ruleState.setAlarmRule(rule); + ruleState.setActive(null); + AlarmCondition condition = rule.getCondition(); + if (condition.hasSchedule() || (condition.getType() == AlarmConditionType.DURATION && !ruleState.isEmpty())) { + reevalNeeded.set(true); + } + } + return ruleState; + } + + @Override + public void reset() { + super.reset(); + configuration = null; + } + + @Override + public void close() { + super.close(); + for (AlarmRuleState state : createRuleStates.values()) { + clearState(state); + } + clearState(clearRuleState); + } + + @Override + public ListenableFuture performCalculation(Map updatedArgs, CalculatedFieldCtx ctx) { + initCurrentAlarm(ctx); + TbAlarmResult result = createOrClearAlarms(state -> { + if (updatedArgs != null) { + boolean newEvent = !updatedArgs.isEmpty(); + AlarmEvalResult evalResult = state.eval(newEvent, ctx); + if (evalResult.getStatus() == NOT_YET_TRUE && evalResult.getLeftDuration() > 0) { + long leftDuration = evalResult.getLeftDuration(); + ScheduledFuture future = ctx.scheduleReevaluation(leftDuration, actorCtx); + if (future != null) { + state.setDurationCheckFuture(future); + } + } + return evalResult; + } else { + return state.reeval(System.currentTimeMillis(), ctx); + } + }, ctx); + return Futures.immediateFuture(AlarmCalculatedFieldResult.builder() + .alarmResult(result) + .build()); + } + + public void processAlarmAction(Alarm alarm, ActionType action) { + switch (action) { + case ALARM_ACK -> processAlarmAck(alarm); + case ALARM_CLEAR -> processAlarmClear(alarm); + case ALARM_DELETE -> processAlarmDelete(alarm); + } + } + + private void processAlarmClear(Alarm alarm) { + currentAlarm = null; + createRuleStates.values().forEach(this::clearState); + clearState(clearRuleState); + } + + private void processAlarmAck(Alarm alarm) { + currentAlarm.setAcknowledged(alarm.isAcknowledged()); + currentAlarm.setAckTs(alarm.getAckTs()); + } + + private void processAlarmDelete(Alarm alarm) { + processAlarmClear(alarm); + } + + private TbAlarmResult createOrClearAlarms(Function evalFunction, + CalculatedFieldCtx ctx) { + TbAlarmResult result = null; + AlarmRuleState resultState = null; + AlarmRuleState.StateInfo resultStateInfo = null; + + for (AlarmRuleState state : createRuleStates.values()) { + AlarmEvalResult evalResult = evalFunction.apply(state); + log.debug("Evaluated create rule {} with args {}. Result: {}", state, arguments, evalResult); + if (evalResult.getStatus() == TRUE) { + resultState = state; + break; + } else if (evalResult.getStatus() == FALSE) { + clearState(state); + } + } + + if (resultState != null) { + result = calculateAlarmResult(resultState, ctx); + resultStateInfo = resultState.getStateInfo(); + log.debug("Alarm result for state {}: {}", resultState, result); + clearState(clearRuleState); + } else if (currentAlarm != null && clearRuleState != null) { + AlarmEvalResult evalResult = evalFunction.apply(clearRuleState); + log.debug("Evaluated clear rule {} with args {}. Result: {}", clearRuleState, arguments, evalResult); + if (evalResult.getStatus() == TRUE) { + resultStateInfo = clearRuleState.getStateInfo(); + clearState(clearRuleState); + for (AlarmRuleState state : createRuleStates.values()) { + clearState(state); + } + AlarmApiCallResult clearResult = ctx.getAlarmService().clearAlarm( + ctx.getTenantId(), currentAlarm.getId(), System.currentTimeMillis(), createDetails(clearRuleState), false + ); + if (clearResult.isCleared()) { + result = TbAlarmResult.builder() + .isCleared(true) + .alarm(clearResult.getAlarm()) + .build(); + resultState = clearRuleState; + } + currentAlarm = null; + } else if (evalResult.getStatus() == FALSE) { + clearState(clearRuleState); + } + } + if (result != null && resultState != null) { + result.setConditionRepeats(resultStateInfo.eventCount()); + result.setConditionDuration(resultStateInfo.duration()); + } + return result; + } + + private void clearState(AlarmRuleState state) { + if (state != null) { + log.debug("Clearing rule state {}", state); + state.clear(); + } + } + + private void initCurrentAlarm(CalculatedFieldCtx ctx) { + if (!initialFetchDone) { + Alarm alarm = ctx.getAlarmService().findLatestActiveByOriginatorAndType(ctx.getTenantId(), entityId, alarmType); + if (alarm != null && !alarm.getStatus().isCleared()) { + currentAlarm = alarm; + } + initialFetchDone = true; + } + } + + private TbAlarmResult calculateAlarmResult(AlarmRuleState ruleState, CalculatedFieldCtx ctx) { + AlarmSeverity severity = ruleState.getSeverity(); + if (currentAlarm != null) { + currentAlarm.setEndTs(System.currentTimeMillis()); + AlarmSeverity oldSeverity = currentAlarm.getSeverity(); + // Skip update if severity is decreased. + if (severity.ordinal() <= oldSeverity.ordinal()) { + currentAlarm.setDetails(createDetails(ruleState)); + currentAlarm.setSeverity(severity); + AlarmApiCallResult result = ctx.getAlarmService().updateAlarm(AlarmUpdateRequest.fromAlarm(currentAlarm)); + currentAlarm = result.getAlarm(); + return TbAlarmResult.fromAlarmResult(result); + } else { + return null; + } + } else { + var newAlarm = new Alarm(); + newAlarm.setType(alarmType); + newAlarm.setAcknowledged(false); + newAlarm.setCleared(false); + newAlarm.setSeverity(severity); + long startTs = latestTimestamp; + long currentTime = System.currentTimeMillis(); + if (startTs == 0L || startTs > currentTime) { + startTs = currentTime; + } + newAlarm.setStartTs(startTs); + newAlarm.setEndTs(startTs); + newAlarm.setDetails(createDetails(ruleState)); + newAlarm.setOriginator(entityId); + newAlarm.setTenantId(ctx.getTenantId()); + newAlarm.setPropagate(configuration.isPropagate()); + newAlarm.setPropagateToOwner(configuration.isPropagateToOwner()); + newAlarm.setPropagateToTenant(configuration.isPropagateToTenant()); + if (configuration.getPropagateRelationTypes() != null) { + newAlarm.setPropagateRelationTypes(configuration.getPropagateRelationTypes()); + } + AlarmApiCallResult result = ctx.getAlarmService().createAlarm(AlarmCreateOrUpdateActiveRequest.fromAlarm(newAlarm)); + currentAlarm = result.getAlarm(); + return TbAlarmResult.fromAlarmResult(result); + } + } + + private JsonNode createDetails(AlarmRuleState ruleState) { + JsonNode alarmDetails; + String alarmDetailsStr = ruleState.getAlarmRule().getAlarmDetails(); + DashboardId dashboardId = ruleState.getAlarmRule().getDashboardId(); + + if (StringUtils.isNotEmpty(alarmDetailsStr) || dashboardId != null) { + ObjectNode newDetails = JacksonUtil.newObjectNode(); + if (StringUtils.isNotEmpty(alarmDetailsStr)) { + for (Map.Entry entry : arguments.entrySet()) { + String key = entry.getKey(); + ArgumentEntry value = entry.getValue(); + alarmDetailsStr = alarmDetailsStr.replaceAll(String.format("\\$\\{%s}", key), String.valueOf(value.getValue())); + } + newDetails.put("data", alarmDetailsStr); + } + if (dashboardId != null) { + newDetails.put("dashboardId", dashboardId.getId().toString()); + } + alarmDetails = newDetails; + } else if (currentAlarm != null) { + alarmDetails = currentAlarm.getDetails(); + } else { + alarmDetails = JacksonUtil.newObjectNode(); + } + + return alarmDetails; + } + + @SneakyThrows + public boolean eval(AlarmConditionExpression expression, CalculatedFieldCtx ctx) { + if (expression instanceof TbelAlarmConditionExpression tbelExpression) { + Object result = ctx.evaluateTbelExpression(tbelExpression.getExpression(), this).get(); + if (result instanceof Boolean booleanResult) { + return booleanResult; + } else { + throw new IllegalStateException("Condition expression returned non-boolean value: '" + result + "'"); + } + } else { + SimpleAlarmConditionExpression simpleExpression = (SimpleAlarmConditionExpression) expression; + ComplexOperation operation = simpleExpression.getOperation(); + if (operation == null) { + operation = ComplexOperation.AND; + } + return switch (operation) { + case AND -> simpleExpression.getFilters().stream() + .allMatch(filter -> eval(getArgument(filter.getArgument()), filter)); + case OR -> simpleExpression.getFilters().stream() + .anyMatch(filter -> eval(getArgument(filter.getArgument()), filter)); + }; + } + } + + private boolean eval(SingleValueArgumentEntry argument, AlarmConditionFilter filter) { + ComplexOperation operation = filter.getOperation(); + if (operation == null) { + operation = ComplexOperation.AND; + } + return switch (operation) { + case AND -> filter.getPredicates().stream() + .allMatch(predicate -> eval(argument, predicate)); + case OR -> filter.getPredicates().stream() + .anyMatch(predicate -> eval(argument, predicate)); + }; + } + + private boolean eval(SingleValueArgumentEntry argument, KeyFilterPredicate predicate) { + return switch (predicate.getType()) { + case STRING -> evalStrPredicate(argument, (StringFilterPredicate) predicate); + case NUMERIC -> evalNumPredicate(argument, (NumericFilterPredicate) predicate); + case BOOLEAN -> evalBooleanPredicate(argument, (BooleanFilterPredicate) predicate); + case COMPLEX -> evalComplexPredicate(argument, (ComplexFilterPredicate) predicate); + }; + } + + private boolean evalComplexPredicate(SingleValueArgumentEntry argument, ComplexFilterPredicate complexPredicate) { + return switch (complexPredicate.getOperation()) { + case OR -> { + for (KeyFilterPredicate predicate : complexPredicate.getPredicates()) { + if (eval(argument, predicate)) { + yield true; + } + } + yield false; + } + case AND -> { + for (KeyFilterPredicate predicate : complexPredicate.getPredicates()) { + if (!eval(argument, predicate)) { + yield false; + } + } + yield true; + } + }; + } + + private boolean evalBooleanPredicate(SingleValueArgumentEntry argument, BooleanFilterPredicate predicate) { + Boolean value = KvUtil.getBoolValue(argument.getKvEntryValue()); + if (value == null) { + return false; + } + Boolean predicateValue = resolveValue(predicate.getValue(), KvUtil::getBoolValue); + if (predicateValue == null) { + return false; + } + return switch (predicate.getOperation()) { + case EQUAL -> value.equals(predicateValue); + case NOT_EQUAL -> !value.equals(predicateValue); + }; + } + + private boolean evalNumPredicate(SingleValueArgumentEntry argument, NumericFilterPredicate predicate) { + Double value = KvUtil.getDoubleValue(argument.getKvEntryValue()); + if (value == null) { + return false; + } + Double predicateValue = resolveValue(predicate.getValue(), KvUtil::getDoubleValue); + if (predicateValue == null) { + return false; + } + return switch (predicate.getOperation()) { + case NOT_EQUAL -> !value.equals(predicateValue); + case EQUAL -> value.equals(predicateValue); + case GREATER -> value > predicateValue; + case GREATER_OR_EQUAL -> value >= predicateValue; + case LESS -> value < predicateValue; + case LESS_OR_EQUAL -> value <= predicateValue; + }; + } + + private boolean evalStrPredicate(SingleValueArgumentEntry argument, StringFilterPredicate predicate) { + String value = KvUtil.getStringValue(argument.getKvEntryValue()); + if (value == null) { + return false; + } + String predicateValue = resolveValue(predicate.getValue(), KvUtil::getStringValue); + if (predicateValue == null) { + return false; + } + if (predicate.isIgnoreCase()) { + value = value.toLowerCase(); + predicateValue = predicateValue.toLowerCase(); + } + return switch (predicate.getOperation()) { + case CONTAINS -> value.contains(predicateValue); + case EQUAL -> value.equals(predicateValue); + case STARTS_WITH -> value.startsWith(predicateValue); + case ENDS_WITH -> value.endsWith(predicateValue); + case NOT_EQUAL -> !value.equals(predicateValue); + case NOT_CONTAINS -> !value.contains(predicateValue); + case IN -> equalsAny(value, splitByCommaWithoutQuotes(predicateValue)); + case NOT_IN -> !equalsAny(value, splitByCommaWithoutQuotes(predicateValue)); + }; + } + + protected T resolveValue(AlarmConditionValue conditionValue, Function mapper) { + T value = conditionValue.getStaticValue(); + if (value == null) { + String argument = conditionValue.getDynamicValueArgument(); + SingleValueArgumentEntry entry = getArgument(argument); + value = mapper.apply(entry.getKvEntryValue()); + if (value == null) { + throw new IllegalArgumentException("No proper value found for argument " + argument); + } + } + return value; + } + + protected SingleValueArgumentEntry getArgument(String key) { + SingleValueArgumentEntry entry = (SingleValueArgumentEntry) arguments.get(key); + if (entry == null) { + throw new IllegalArgumentException("Argument '" + key + "' is missing"); + } + return entry; + } + + private AlarmCalculatedFieldConfiguration getConfiguration(CalculatedFieldCtx ctx) { + return (AlarmCalculatedFieldConfiguration) ctx.getCalculatedField().getConfiguration(); + } + + @Override + protected void validateNewEntry(String key, ArgumentEntry newEntry) { + if (!(newEntry instanceof SingleValueArgumentEntry)) { + throw new IllegalArgumentException("Only single value arguments supported"); + } + } + + @Override + public CalculatedFieldType getType() { + return CalculatedFieldType.ALARM; + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/alarm/AlarmEvalResult.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/alarm/AlarmEvalResult.java new file mode 100644 index 0000000000..424a977c75 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/alarm/AlarmEvalResult.java @@ -0,0 +1,45 @@ +/** + * Copyright © 2016-2025 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.cf.ctx.state.alarm; + +import lombok.Data; +import lombok.RequiredArgsConstructor; + +@Data +@RequiredArgsConstructor +public class AlarmEvalResult { + + public static final AlarmEvalResult TRUE = new AlarmEvalResult(Status.TRUE); + public static final AlarmEvalResult FALSE = new AlarmEvalResult(Status.FALSE); + public static final AlarmEvalResult NOT_YET_TRUE = new AlarmEvalResult(Status.NOT_YET_TRUE); + + private final Status status; + private final long leftDuration; + private final long leftEvents; + + public AlarmEvalResult(Status status) { + this(status, 0, 0); + } + + public static AlarmEvalResult notYetTrue(long leftEvents, long leftDuration) { + return new AlarmEvalResult(Status.NOT_YET_TRUE, leftDuration, leftEvents); + } + + public enum Status { + FALSE, NOT_YET_TRUE, TRUE; + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/alarm/AlarmRuleState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/alarm/AlarmRuleState.java new file mode 100644 index 0000000000..8612607dfb --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/alarm/AlarmRuleState.java @@ -0,0 +1,344 @@ +/** + * Copyright © 2016-2025 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.cf.ctx.state.alarm; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.common.util.KvUtil; +import org.thingsboard.server.common.data.alarm.AlarmSeverity; +import org.thingsboard.server.common.data.alarm.rule.AlarmRule; +import org.thingsboard.server.common.data.alarm.rule.condition.AlarmCondition; +import org.thingsboard.server.common.data.alarm.rule.condition.AlarmConditionType; +import org.thingsboard.server.common.data.alarm.rule.condition.AlarmConditionValue; +import org.thingsboard.server.common.data.alarm.rule.condition.DurationAlarmCondition; +import org.thingsboard.server.common.data.alarm.rule.condition.RepeatingAlarmCondition; +import org.thingsboard.server.common.data.alarm.rule.condition.expression.AlarmConditionExpression; +import org.thingsboard.server.common.data.alarm.rule.condition.schedule.AlarmSchedule; +import org.thingsboard.server.common.data.alarm.rule.condition.schedule.AlarmScheduleType; +import org.thingsboard.server.common.data.alarm.rule.condition.schedule.AnyTimeSchedule; +import org.thingsboard.server.common.data.alarm.rule.condition.schedule.CustomTimeSchedule; +import org.thingsboard.server.common.data.alarm.rule.condition.schedule.CustomTimeScheduleItem; +import org.thingsboard.server.common.data.alarm.rule.condition.schedule.SpecificTimeSchedule; +import org.thingsboard.server.common.msg.tools.SchedulerUtils; +import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx; + +import java.time.Instant; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.util.Optional; +import java.util.concurrent.ScheduledFuture; + +@Data +@Slf4j +public class AlarmRuleState { + + private final AlarmSeverity severity; + private AlarmRule alarmRule; + private AlarmCalculatedFieldState state; + + private AlarmCondition condition; + + private long eventCount; + private long firstEventTs; // when duration condition started + private long lastEventTs; + private transient long duration; + private ScheduledFuture durationCheckFuture; + private Boolean active; + + public AlarmRuleState(AlarmSeverity severity, AlarmRule alarmRule, AlarmCalculatedFieldState state) { + this.severity = severity; + if (alarmRule != null) { + setAlarmRule(alarmRule); + } + this.state = state; + } + + public AlarmEvalResult eval(boolean newEvent, CalculatedFieldCtx ctx) { // on event or config change + long ts = newEvent ? state.getLatestTimestamp() : System.currentTimeMillis(); + active = isActive(ts); + if (!active) { + return AlarmEvalResult.FALSE; + } + return doEval(newEvent, ctx); + } + + public AlarmEvalResult reeval(long ts, CalculatedFieldCtx ctx) { // on scheduled duration check or periodic re-eval for rules with schedule + boolean active = isActive(ts); + switch (condition.getType()) { + case SIMPLE, REPEATING -> { + if (this.active == null || active != this.active) { + this.active = active; + if (active) { + return doEval(false, ctx); + } + } + if (active) { + return AlarmEvalResult.NOT_YET_TRUE; + } else { + return AlarmEvalResult.FALSE; + } + } + case DURATION -> { + if (!active) { + return AlarmEvalResult.FALSE; + } + long requiredDuration = getRequiredDurationInMs(); + if (requiredDuration > 0 && lastEventTs > 0 && ts > lastEventTs) { + duration = ts - firstEventTs; + long leftDuration = requiredDuration - duration; + if (leftDuration <= 0) { + return AlarmEvalResult.TRUE; + } else { + return AlarmEvalResult.notYetTrue(0, leftDuration); + } + } + } + } + return AlarmEvalResult.FALSE; + } + + public AlarmEvalResult doEval(boolean newEvent, CalculatedFieldCtx ctx) { + return switch (condition.getType()) { + case SIMPLE -> evalSimple(ctx); + case DURATION -> evalDuration(ctx); + case REPEATING -> evalRepeating(newEvent, ctx); + }; + } + + private AlarmEvalResult evalSimple(CalculatedFieldCtx ctx) { + return eval(condition.getExpression(), ctx) ? AlarmEvalResult.TRUE : AlarmEvalResult.FALSE; + } + + private AlarmEvalResult evalRepeating(boolean newEvent, CalculatedFieldCtx ctx) { + if (eval(condition.getExpression(), ctx)) { + if (newEvent) { + eventCount++; + } + long requiredRepeats = getIntValue(((RepeatingAlarmCondition) condition).getCount()); + if (requiredRepeats > 0) { + long leftRepeats = requiredRepeats - eventCount; + return leftRepeats <= 0 ? AlarmEvalResult.TRUE : AlarmEvalResult.notYetTrue(leftRepeats, 0); + } else { + return AlarmEvalResult.NOT_YET_TRUE; + } + } else { + return AlarmEvalResult.FALSE; + } + } + + private AlarmEvalResult evalDuration(CalculatedFieldCtx ctx) { + if (eval(condition.getExpression(), ctx)) { + long eventTs = state.getLatestTimestamp(); + if (lastEventTs > 0) { + if (eventTs > lastEventTs) { + if (firstEventTs == 0) { + firstEventTs = lastEventTs; + } + lastEventTs = eventTs; + } + } else { + firstEventTs = eventTs; + lastEventTs = eventTs; + } + duration = lastEventTs - firstEventTs; + long requiredDuration = getRequiredDurationInMs(); + if (requiredDuration > 0) { + long leftDuration = requiredDuration - duration; + if (leftDuration <= 0) { + return AlarmEvalResult.TRUE; + } else { + return AlarmEvalResult.notYetTrue(0, leftDuration); + } + } else { + return AlarmEvalResult.NOT_YET_TRUE; + } + } else { + return AlarmEvalResult.FALSE; + } + } + + private boolean isActive(long eventTs) { + if (condition.getSchedule() == null) { + return true; + } + AlarmSchedule schedule = state.resolveValue(condition.getSchedule(), entry -> Optional.ofNullable(KvUtil.getStringValue(entry)) + .map(this::parseSchedule).orElse(null)); + boolean active = switch (schedule.getType()) { + case ANY_TIME -> true; + case SPECIFIC_TIME -> isActiveSpecific((SpecificTimeSchedule) schedule, eventTs); + case CUSTOM -> isActiveCustom((CustomTimeSchedule) schedule, eventTs); + }; + log.trace("Alarm rule active = {} for schedule {}", active, schedule); + return active; + } + + private boolean isActiveSpecific(SpecificTimeSchedule schedule, long eventTs) { + ZoneId zoneId = SchedulerUtils.getZoneId(schedule.getTimezone()); + ZonedDateTime zdt = ZonedDateTime.ofInstant(Instant.ofEpochMilli(eventTs), zoneId); + if (schedule.getDaysOfWeek().size() != 7) { + int dayOfWeek = zdt.getDayOfWeek().getValue(); + if (!schedule.getDaysOfWeek().contains(dayOfWeek)) { + return false; + } + } + long endsOn = schedule.getEndsOn(); + if (endsOn == 0) { + // 24 hours in milliseconds + endsOn = 86400000; + } + + return isActive(eventTs, zoneId, zdt, schedule.getStartsOn(), endsOn); + } + + private boolean isActiveCustom(CustomTimeSchedule schedule, long eventTs) { + ZoneId zoneId = SchedulerUtils.getZoneId(schedule.getTimezone()); + ZonedDateTime zdt = ZonedDateTime.ofInstant(Instant.ofEpochMilli(eventTs), zoneId); + int dayOfWeek = zdt.toLocalDate().getDayOfWeek().getValue(); + for (CustomTimeScheduleItem item : schedule.getItems()) { + if (item.getDayOfWeek() == dayOfWeek) { + if (item.isEnabled()) { + long endsOn = item.getEndsOn(); + if (endsOn == 0) { + // 24 hours in milliseconds + endsOn = 86400000; + } + return isActive(eventTs, zoneId, zdt, item.getStartsOn(), endsOn); + } else { + return false; + } + } + } + return false; + } + + private boolean isActive(long eventTs, ZoneId zoneId, ZonedDateTime zdt, long startsOn, long endsOn) { + long startOfDay = zdt.toLocalDate().atStartOfDay(zoneId).toInstant().toEpochMilli(); + long msFromStartOfDay = eventTs - startOfDay; + if (startsOn <= endsOn) { + return startsOn <= msFromStartOfDay && endsOn > msFromStartOfDay; + } else { + return startsOn < msFromStartOfDay || (0 < msFromStartOfDay && msFromStartOfDay < endsOn); + } + } + + public void clear() { + clearRepeatingConditionState(); + clearDurationConditionState(); + } + + private void clearRepeatingConditionState() { + eventCount = 0L; + } + + private void clearDurationConditionState() { + firstEventTs = 0L; + lastEventTs = 0L; + duration = 0L; + if (durationCheckFuture != null) { + durationCheckFuture.cancel(true); + durationCheckFuture = null; + } + } + + public boolean isEmpty() { + return eventCount == 0L && firstEventTs == 0L && lastEventTs == 0L && durationCheckFuture == null; + } + + private AlarmSchedule parseSchedule(String str) { + ObjectNode json = (ObjectNode) JacksonUtil.toJsonNode(str); + if (json.isEmpty()) { + return new AnyTimeSchedule(); // only if valid json, fail otherwise + } + + if (!json.hasNonNull("type")) { + // deducting the schedule type + AlarmScheduleType type; + if (json.hasNonNull("daysOfWeek")) { + type = AlarmScheduleType.SPECIFIC_TIME; + } else if (json.hasNonNull("items")) { + type = AlarmScheduleType.CUSTOM; + } else { + throw new IllegalArgumentException("Failed to parse alarm schedule from '" + str + "'"); + } + json.put("type", type.name()); + } + + return JacksonUtil.treeToValue(json, AlarmSchedule.class); + } + + private Integer getIntValue(AlarmConditionValue value) { + return state.resolveValue(value, entry -> Optional.ofNullable(KvUtil.getLongValue(entry)).map(Long::intValue).orElse(null)); + } + + private long getRequiredDurationInMs() { + DurationAlarmCondition durationCondition = (DurationAlarmCondition) condition; + return durationCondition.getUnit().toMillis(state.resolveValue(durationCondition.getValue(), KvUtil::getLongValue)); + } + + private boolean eval(AlarmConditionExpression expression, CalculatedFieldCtx ctx) { + return state.eval(expression, ctx); + } + + public void setAlarmRule(AlarmRule alarmRule) { + this.alarmRule = alarmRule; + this.condition = alarmRule.getCondition(); + + // clearing state for other condition types (possibly left from a previous condition type) + switch (condition.getType()) { + case SIMPLE -> { + clearRepeatingConditionState(); + clearDurationConditionState(); + } + case REPEATING -> { + clearDurationConditionState(); + } + case DURATION -> { + clearRepeatingConditionState(); + } + } + } + + public StateInfo getStateInfo() { + if (condition.getType() == AlarmConditionType.REPEATING) { + return new StateInfo(eventCount, null); + } else if (condition.getType() == AlarmConditionType.DURATION) { + return new StateInfo(null, duration); + } else { + return StateInfo.EMPTY; + } + } + + @Override + public String toString() { + return "AlarmRuleState{" + + "severity=" + severity + + ", condition=" + condition + + ", eventCount=" + eventCount + + ", firstEventTs=" + firstEventTs + + ", lastEventTs=" + lastEventTs + + ", duration=" + duration + + ", durationCheckFuture=" + durationCheckFuture + + '}'; + } + + public record StateInfo(Long eventCount, Long duration) { + static final StateInfo EMPTY = new StateInfo(null, null); + + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/geofencing/GeofencingArgumentEntry.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/geofencing/GeofencingArgumentEntry.java new file mode 100644 index 0000000000..bcc4d3ffcd --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/geofencing/GeofencingArgumentEntry.java @@ -0,0 +1,111 @@ +/** + * Copyright © 2016-2025 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.cf.ctx.state.geofencing; + +import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import org.thingsboard.script.api.tbel.TbelCfArg; +import org.thingsboard.script.api.tbel.TbelCfGeofencingArg; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.kv.KvEntry; +import org.thingsboard.server.common.util.ProtoUtils; +import org.thingsboard.server.gen.transport.TransportProtos; +import org.thingsboard.server.service.cf.ctx.state.ArgumentEntry; +import org.thingsboard.server.service.cf.ctx.state.ArgumentEntryType; + +import java.util.Map; +import java.util.stream.Collectors; + +@Data +@Slf4j +public class GeofencingArgumentEntry implements ArgumentEntry { + + private Map zoneStates; + + private boolean forceResetPrevious; + + public GeofencingArgumentEntry() { + } + + public GeofencingArgumentEntry(EntityId entityId, TransportProtos.AttributeValueProto entry) { + this(Map.of(entityId, ProtoUtils.fromProto(entry))); + } + + public GeofencingArgumentEntry(Map entityIdkvEntryMap) { + this.zoneStates = toZones(entityIdkvEntryMap); + } + + @Override + public ArgumentEntryType getType() { + return ArgumentEntryType.GEOFENCING; + } + + @Override + public Object getValue() { + return zoneStates; + } + + @Override + public boolean updateEntry(ArgumentEntry entry) { + if (!(entry instanceof GeofencingArgumentEntry geofencingArgumentEntry)) { + throw new IllegalArgumentException("Unsupported argument entry type for geofencing argument entry: " + entry.getType()); + } + if (geofencingArgumentEntry.isEmpty()) { + zoneStates.clear(); + return true; + } + boolean updated = false; + for (var zoneEntry : geofencingArgumentEntry.getZoneStates().entrySet()) { + if (updateZone(zoneEntry)) { + updated = true; + } + } + return updated; + } + + @Override + public boolean isEmpty() { + return zoneStates == null || zoneStates.isEmpty(); + } + + @Override + public TbelCfArg toTbelCfArg() { + return new TbelCfGeofencingArg(zoneStates); + } + + private Map toZones(Map entityIdKvEntryMap) { + return entityIdKvEntryMap.entrySet().stream() + .collect(Collectors.toMap(Map.Entry::getKey, + entry -> new GeofencingZoneState(entry.getKey(), entry.getValue()))); + } + + private boolean updateZone(Map.Entry zoneEntry) { + EntityId zoneId = zoneEntry.getKey(); + GeofencingZoneState newZoneState = zoneEntry.getValue(); + + GeofencingZoneState existingZoneState = zoneStates.get(zoneId); + if (existingZoneState == null) { + zoneStates.put(zoneId, newZoneState); + return true; + } + if (newZoneState.getPerimeterDefinition() == null) { + zoneStates.remove(zoneId); + return true; + } + return existingZoneState.update(newZoneState); + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/geofencing/GeofencingCalculatedFieldState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/geofencing/GeofencingCalculatedFieldState.java new file mode 100644 index 0000000000..51110df2bb --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/geofencing/GeofencingCalculatedFieldState.java @@ -0,0 +1,197 @@ +/** + * Copyright © 2016-2025 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.cf.ctx.state.geofencing; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.MoreExecutors; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.common.util.geo.Coordinates; +import org.thingsboard.server.common.data.cf.CalculatedFieldType; +import org.thingsboard.server.common.data.cf.configuration.OutputType; +import org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingCalculatedFieldConfiguration; +import org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingReportStrategy; +import org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingTransitionEvent; +import org.thingsboard.server.common.data.cf.configuration.geofencing.ZoneGroupConfiguration; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.relation.EntityRelation; +import org.thingsboard.server.service.cf.CalculatedFieldResult; +import org.thingsboard.server.service.cf.TelemetryCalculatedFieldResult; +import org.thingsboard.server.service.cf.ctx.state.ArgumentEntry; +import org.thingsboard.server.service.cf.ctx.state.ArgumentEntryType; +import org.thingsboard.server.service.cf.ctx.state.BaseCalculatedFieldState; +import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx; +import org.thingsboard.server.service.cf.ctx.state.SingleValueArgumentEntry; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import static org.thingsboard.server.common.data.cf.configuration.geofencing.EntityCoordinates.ENTITY_ID_LATITUDE_ARGUMENT_KEY; +import static org.thingsboard.server.common.data.cf.configuration.geofencing.EntityCoordinates.ENTITY_ID_LONGITUDE_ARGUMENT_KEY; +import static org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingPresenceStatus.INSIDE; +import static org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingPresenceStatus.OUTSIDE; + +@Getter +@Setter +@Slf4j +@EqualsAndHashCode(callSuper = true) +public class GeofencingCalculatedFieldState extends BaseCalculatedFieldState { + + private long lastDynamicArgumentsRefreshTs = -1; + + public GeofencingCalculatedFieldState(EntityId entityId) { + super(entityId); + } + + @Override + public CalculatedFieldType getType() { + return CalculatedFieldType.GEOFENCING; + } + + @Override + protected void validateNewEntry(String key, ArgumentEntry newEntry) { + switch (key) { + case ENTITY_ID_LATITUDE_ARGUMENT_KEY, ENTITY_ID_LONGITUDE_ARGUMENT_KEY -> { + if (!(newEntry instanceof SingleValueArgumentEntry)) { + throw new IllegalArgumentException("Unsupported argument entry type for " + key + " argument: " + newEntry.getType() + ". " + + "Only SINGLE_VALUE type is allowed."); + } + } + default -> { + if (!(newEntry instanceof GeofencingArgumentEntry)) { + throw new IllegalArgumentException("Unsupported argument entry type for " + key + " argument: " + newEntry.getType() + ". " + + "Only GEOFENCING type is allowed."); + } + } + } + } + + @Override + public ListenableFuture performCalculation(Map updatedArgs, CalculatedFieldCtx ctx) { + double latitude = (double) arguments.get(ENTITY_ID_LATITUDE_ARGUMENT_KEY).getValue(); + double longitude = (double) arguments.get(ENTITY_ID_LONGITUDE_ARGUMENT_KEY).getValue(); + Coordinates entityCoordinates = new Coordinates(latitude, longitude); + + var geofencingCfg = (GeofencingCalculatedFieldConfiguration) ctx.getCalculatedField().getConfiguration(); + Map zoneGroups = geofencingCfg.getZoneGroups(); + + ObjectNode valuesNode = JacksonUtil.newObjectNode(); + List> relationFutures = new ArrayList<>(); + + getGeofencingArguments().forEach((argumentKey, argumentEntry) -> { + ZoneGroupConfiguration zoneGroupCfg = zoneGroups.get(argumentKey); + if (zoneGroupCfg == null) { + throw new RuntimeException("Zone group configuration is missing for the: " + entityId); + } + boolean createRelationsWithMatchedZones = zoneGroupCfg.isCreateRelationsWithMatchedZones(); + List zoneResults = new ArrayList<>(argumentEntry.getZoneStates().size()); + argumentEntry.getZoneStates().forEach((zoneId, zoneState) -> { + GeofencingEvalResult eval = zoneState.evaluate(entityCoordinates); + zoneResults.add(eval); + if (createRelationsWithMatchedZones) { + GeofencingTransitionEvent transitionEvent = eval.transition(); + if (transitionEvent == null) { + return; + } + EntityRelation relation = switch (zoneGroupCfg.getDirection()) { + case TO -> new EntityRelation(zoneId, entityId, zoneGroupCfg.getRelationType()); + case FROM -> new EntityRelation(entityId, zoneId, zoneGroupCfg.getRelationType()); + }; + ListenableFuture f = switch (transitionEvent) { + case ENTERED -> ctx.getRelationService().saveRelationAsync(ctx.getTenantId(), relation); + case LEFT -> ctx.getRelationService().deleteRelationAsync(ctx.getTenantId(), relation); + }; + relationFutures.add(f); + } + }); + updateValuesNode(argumentKey, zoneResults, zoneGroupCfg.getReportStrategy(), valuesNode); + }); + + OutputType outputType = ctx.getOutput().getType(); + var result = TelemetryCalculatedFieldResult.builder() + .type(outputType) + .scope(ctx.getOutput().getScope()) + .result(toResultNode(outputType, valuesNode)) + .build(); + if (relationFutures.isEmpty()) { + return Futures.immediateFuture(result); + } + return Futures.whenAllComplete(relationFutures).call(() -> result, MoreExecutors.directExecutor()); + } + + @Override + public void reset() { + super.reset(); + lastDynamicArgumentsRefreshTs = -1; + } + + public void updateLastDynamicArgumentsRefreshTs() { + lastDynamicArgumentsRefreshTs = System.currentTimeMillis(); + } + + private Map getGeofencingArguments() { + return arguments.entrySet() + .stream() + .filter(entry -> entry.getValue().getType().equals(ArgumentEntryType.GEOFENCING)) + .collect(Collectors.toMap(Map.Entry::getKey, entry -> (GeofencingArgumentEntry) entry.getValue())); + } + + private void updateValuesNode(String argumentKey, List zoneResults, GeofencingReportStrategy geofencingReportStrategy, ObjectNode resultNode) { + GeofencingEvalResult aggregationResult = aggregateZoneGroup(zoneResults); + final String eventKey = argumentKey + "Event"; + final String statusKey = argumentKey + "Status"; + switch (geofencingReportStrategy) { + case REPORT_TRANSITION_EVENTS_ONLY -> addTransitionEventIfExists(resultNode, aggregationResult, eventKey); + case REPORT_PRESENCE_STATUS_ONLY -> resultNode.put(statusKey, aggregationResult.status().name()); + case REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS -> { + addTransitionEventIfExists(resultNode, aggregationResult, eventKey); + resultNode.put(statusKey, aggregationResult.status().name()); + } + } + } + + private JsonNode toResultNode(OutputType outputType, ObjectNode valuesNode) { + return toSimpleResult(outputType == OutputType.TIME_SERIES, valuesNode); + } + + private GeofencingEvalResult aggregateZoneGroup(List zoneResults) { + boolean nowInside = zoneResults.stream().anyMatch(r -> INSIDE.equals(r.status())); + boolean prevInside = zoneResults.stream() + .anyMatch(r -> GeofencingTransitionEvent.LEFT.equals(r.transition()) || r.transition() == null && r.status() == INSIDE); + GeofencingTransitionEvent transition = null; + if (!prevInside && nowInside) { + transition = GeofencingTransitionEvent.ENTERED; + } else if (prevInside && !nowInside) { + transition = GeofencingTransitionEvent.LEFT; + } + return new GeofencingEvalResult(transition, nowInside ? INSIDE : OUTSIDE); + } + + private void addTransitionEventIfExists(ObjectNode resultNode, GeofencingEvalResult aggregationResult, String eventKey) { + if (aggregationResult.transition() != null) { + resultNode.put(eventKey, aggregationResult.transition().name()); + } + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/geofencing/GeofencingEvalResult.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/geofencing/GeofencingEvalResult.java new file mode 100644 index 0000000000..c6bf3dd65e --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/geofencing/GeofencingEvalResult.java @@ -0,0 +1,24 @@ +/** + * Copyright © 2016-2025 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.cf.ctx.state.geofencing; + +import jakarta.annotation.Nullable; +import org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingPresenceStatus; +import org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingTransitionEvent; + +public record GeofencingEvalResult(@Nullable GeofencingTransitionEvent transition, + GeofencingPresenceStatus status) { +} diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/geofencing/GeofencingZoneState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/geofencing/GeofencingZoneState.java new file mode 100644 index 0000000000..c849f5d169 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/geofencing/GeofencingZoneState.java @@ -0,0 +1,106 @@ +/** + * Copyright © 2016-2025 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.cf.ctx.state.geofencing; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.common.util.geo.Coordinates; +import org.thingsboard.common.util.geo.PerimeterDefinition; +import org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingPresenceStatus; +import org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingTransitionEvent; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.kv.AttributeKvEntry; +import org.thingsboard.server.common.data.kv.KvEntry; +import org.thingsboard.server.common.util.ProtoUtils; +import org.thingsboard.server.gen.transport.TransportProtos.GeofencingZoneProto; + +import static org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingPresenceStatus.INSIDE; +import static org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingPresenceStatus.OUTSIDE; + +@Data +public class GeofencingZoneState { + + private final EntityId zoneId; + + private long ts; + private Long version; + private PerimeterDefinition perimeterDefinition; + + @EqualsAndHashCode.Exclude + private GeofencingPresenceStatus lastPresence; + + public GeofencingZoneState(EntityId zoneId, KvEntry entry) { + this.zoneId = zoneId; + if (!(entry instanceof AttributeKvEntry attributeKvEntry)) { + throw new IllegalArgumentException("Unsupported KvEntry type for geofencing zone state: " + entry.getClass().getSimpleName()); + } + this.ts = attributeKvEntry.getLastUpdateTs(); + this.version = attributeKvEntry.getVersion(); + this.perimeterDefinition = JacksonUtil.fromString(entry.getValueAsString(), PerimeterDefinition.class); + } + + public GeofencingZoneState(GeofencingZoneProto proto) { + this.zoneId = ProtoUtils.fromProto(proto.getZoneId()); + this.ts = proto.getTs(); + this.version = proto.getVersion(); + this.perimeterDefinition = JacksonUtil.fromString(proto.getPerimeterDefinition(), PerimeterDefinition.class); + if (proto.hasInside()) { + this.lastPresence = proto.getInside() ? INSIDE : OUTSIDE; + } + } + + public boolean update(GeofencingZoneState newZoneState) { + if (newZoneState.getTs() <= this.ts) { + return false; + } + Long newVersion = newZoneState.getVersion(); + if (newVersion == null || this.version == null || newVersion > this.version) { + this.ts = newZoneState.getTs(); + this.version = newVersion; + this.perimeterDefinition = newZoneState.getPerimeterDefinition(); + this.lastPresence = null; + return true; + } + return false; + } + + public GeofencingEvalResult evaluate(Coordinates entityCoordinates) { + boolean nowInside = perimeterDefinition.checkMatches(entityCoordinates); + + GeofencingPresenceStatus status = nowInside ? INSIDE : OUTSIDE; + + // first evaluation + if (this.lastPresence == null) { + this.lastPresence = status; + GeofencingTransitionEvent transition = null; + if (status == GeofencingPresenceStatus.INSIDE) { + transition = GeofencingTransitionEvent.ENTERED; + } + return new GeofencingEvalResult(transition, status); + } + // State changed + if (this.lastPresence != status) { + this.lastPresence = status; + GeofencingTransitionEvent transition = (status == GeofencingPresenceStatus.INSIDE) ? + GeofencingTransitionEvent.ENTERED : GeofencingTransitionEvent.LEFT; + return new GeofencingEvalResult(transition, status); + } + // State unchanged + return new GeofencingEvalResult(null, status); + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/propagation/PropagationArgumentEntry.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/propagation/PropagationArgumentEntry.java new file mode 100644 index 0000000000..81009da5e5 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/propagation/PropagationArgumentEntry.java @@ -0,0 +1,73 @@ +/** + * Copyright © 2016-2025 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.cf.ctx.state.propagation; + +import lombok.Data; +import org.thingsboard.script.api.tbel.TbelCfArg; +import org.thingsboard.script.api.tbel.TbelCfPropagationArg; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.util.CollectionsUtil; +import org.thingsboard.server.service.cf.ctx.state.ArgumentEntry; +import org.thingsboard.server.service.cf.ctx.state.ArgumentEntryType; + +import java.util.ArrayList; +import java.util.List; + +@Data +public class PropagationArgumentEntry implements ArgumentEntry { + + private List propagationEntityIds; + + private boolean forceResetPrevious; + + public PropagationArgumentEntry(List propagationEntityIds) { + this.propagationEntityIds = new ArrayList<>(propagationEntityIds); + } + + @Override + public ArgumentEntryType getType() { + return ArgumentEntryType.PROPAGATION; + } + + @Override + public Object getValue() { + return propagationEntityIds; + } + + @Override + public boolean updateEntry(ArgumentEntry entry) { + if (!(entry instanceof PropagationArgumentEntry propagationArgumentEntry)) { + throw new IllegalArgumentException("Unsupported argument entry type for propagation argument entry: " + entry.getType()); + } + if (propagationArgumentEntry.isEmpty()) { + propagationEntityIds.clear(); + } else { + propagationEntityIds = propagationArgumentEntry.getPropagationEntityIds(); + } + return true; + } + + @Override + public boolean isEmpty() { + return CollectionsUtil.isEmpty(propagationEntityIds); + } + + @Override + public TbelCfArg toTbelCfArg() { + return new TbelCfPropagationArg(propagationEntityIds); + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/propagation/PropagationCalculatedFieldState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/propagation/PropagationCalculatedFieldState.java new file mode 100644 index 0000000000..4f589572e8 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/propagation/PropagationCalculatedFieldState.java @@ -0,0 +1,107 @@ +/** + * Copyright © 2016-2025 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.cf.ctx.state.propagation; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.MoreExecutors; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.actors.TbActorRef; +import org.thingsboard.server.common.data.cf.CalculatedFieldType; +import org.thingsboard.server.common.data.cf.configuration.Output; +import org.thingsboard.server.common.data.cf.configuration.OutputType; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.service.cf.CalculatedFieldResult; +import org.thingsboard.server.service.cf.PropagationCalculatedFieldResult; +import org.thingsboard.server.service.cf.TelemetryCalculatedFieldResult; +import org.thingsboard.server.service.cf.ctx.state.ArgumentEntry; +import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx; +import org.thingsboard.server.service.cf.ctx.state.ScriptCalculatedFieldState; +import org.thingsboard.server.service.cf.ctx.state.SingleValueArgumentEntry; + +import java.util.ArrayList; +import java.util.Map; + +import static org.thingsboard.server.common.data.cf.configuration.PropagationCalculatedFieldConfiguration.PROPAGATION_CONFIG_ARGUMENT; + +public class PropagationCalculatedFieldState extends ScriptCalculatedFieldState { + + public PropagationCalculatedFieldState(EntityId entityId) { + super(entityId); + } + + @Override + public void setCtx(CalculatedFieldCtx ctx, TbActorRef actorCtx) { + this.ctx = ctx; + this.actorCtx = actorCtx; + this.requiredArguments = new ArrayList<>(ctx.getArgNames()); + requiredArguments.add(PROPAGATION_CONFIG_ARGUMENT); + this.readinessStatus = checkReadiness(requiredArguments, arguments); + if (ctx.isApplyExpressionForResolvedArguments()) { + this.tbelExpression = ctx.getTbelExpressions().get(ctx.getExpression()); + } + } + + @Override + public CalculatedFieldType getType() { + return CalculatedFieldType.PROPAGATION; + } + + @Override + public ListenableFuture performCalculation(Map updatedArgs, CalculatedFieldCtx ctx) { + ArgumentEntry argumentEntry = arguments.get(PROPAGATION_CONFIG_ARGUMENT); + if (!(argumentEntry instanceof PropagationArgumentEntry propagationArgumentEntry) || propagationArgumentEntry.isEmpty()) { + return Futures.immediateFuture(PropagationCalculatedFieldResult.builder().build()); + } + if (ctx.isApplyExpressionForResolvedArguments()) { + return Futures.transform(super.performCalculation(updatedArgs, ctx), telemetryCfResult -> + PropagationCalculatedFieldResult.builder() + .propagationEntityIds(propagationArgumentEntry.getPropagationEntityIds()) + .result((TelemetryCalculatedFieldResult) telemetryCfResult) + .build(), + MoreExecutors.directExecutor()); + } + return Futures.immediateFuture(PropagationCalculatedFieldResult.builder() + .propagationEntityIds(propagationArgumentEntry.getPropagationEntityIds()) + .result(toTelemetryResult(ctx)) + .build()); + } + + private TelemetryCalculatedFieldResult toTelemetryResult(CalculatedFieldCtx ctx) { + Output output = ctx.getOutput(); + TelemetryCalculatedFieldResult.TelemetryCalculatedFieldResultBuilder telemetryCfBuilder = + TelemetryCalculatedFieldResult.builder() + .type(output.getType()) + .scope(output.getScope()); + ObjectNode valuesNode = JacksonUtil.newObjectNode(); + arguments.forEach((outputKey, argumentEntry) -> { + if (argumentEntry instanceof PropagationArgumentEntry) { + return; + } + if (argumentEntry instanceof SingleValueArgumentEntry singleArgumentEntry) { + JacksonUtil.addKvEntry(valuesNode, singleArgumentEntry.getKvEntryValue(), outputKey); + return; + } + throw new IllegalArgumentException("Unsupported argument type: " + argumentEntry.getType() + " detected for argument: " + outputKey + ". " + + "Only Latest telemetry or Attribute arguments supported for 'Arguments Only' propagation mode!"); + }); + ObjectNode result = toSimpleResult(output.getType() == OutputType.TIME_SERIES, valuesNode); + telemetryCfBuilder.result(result); + return telemetryCfBuilder.build(); + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/device/DeviceBulkImportService.java b/application/src/main/java/org/thingsboard/server/service/device/DeviceBulkImportService.java index d042fb2657..782104cc9b 100644 --- a/application/src/main/java/org/thingsboard/server/service/device/DeviceBulkImportService.java +++ b/application/src/main/java/org/thingsboard/server/service/device/DeviceBulkImportService.java @@ -259,7 +259,7 @@ public class DeviceBulkImportService extends AbstractBulkImportService { Lwm2mDeviceProfileTransportConfiguration transportConfiguration = new Lwm2mDeviceProfileTransportConfiguration(); transportConfiguration.setBootstrap(Collections.emptyList()); transportConfiguration.setClientLwM2mSettings(new OtherConfiguration(false,1, 1, 1, PowerMode.DRX, null, null, null, null, null, V1_0.toString())); - transportConfiguration.setObserveAttr(new TelemetryMappingConfiguration(Collections.emptyMap(), Collections.emptySet(), Collections.emptySet(), Collections.emptySet(), Collections.emptyMap(), SINGLE)); + transportConfiguration.setObserveAttr(new TelemetryMappingConfiguration(Collections.emptyMap(), Collections.emptySet(), Collections.emptySet(), Collections.emptySet(), Collections.emptyMap(), false, SINGLE)); DeviceProfileData deviceProfileData = new DeviceProfileData(); DefaultDeviceProfileConfiguration configuration = new DefaultDeviceProfileConfiguration(); diff --git a/application/src/main/java/org/thingsboard/server/service/device/DeviceProvisionServiceImpl.java b/application/src/main/java/org/thingsboard/server/service/device/DeviceProvisionServiceImpl.java index 0778d61ee7..ffd16c1287 100644 --- a/application/src/main/java/org/thingsboard/server/service/device/DeviceProvisionServiceImpl.java +++ b/application/src/main/java/org/thingsboard/server/service/device/DeviceProvisionServiceImpl.java @@ -186,9 +186,14 @@ public class DeviceProvisionServiceImpl implements DeviceProvisionService { try { Optional provisionState = attributesService.find(device.getTenantId(), device.getId(), AttributeScope.SERVER_SCOPE, DEVICE_PROVISION_STATE).get(); - if (provisionState != null && provisionState.isPresent() && !provisionState.get().getValueAsString().equals(PROVISIONED_STATE)) { - notify(device, provisionRequest, TbMsgType.PROVISION_FAILURE, false); - throw new ProvisionFailedException(ProvisionResponseStatus.FAILURE.name()); + if (provisionState != null && provisionState.isPresent()) { + if (provisionState.get().getValueAsString().equals(PROVISIONED_STATE)) { + notify(device, provisionRequest, TbMsgType.PROVISION_FAILURE, false); + throw new ProvisionFailedException(ProvisionResponseStatus.FAILURE.name()); + } else { + log.error("[{}][{}] Unknown provision state: {}!", device.getName(), DEVICE_PROVISION_STATE, provisionState.get().getValueAsString()); + throw new ProvisionFailedException(ProvisionResponseStatus.FAILURE.name()); + } } else { saveProvisionStateAttribute(device).get(); notify(device, provisionRequest, TbMsgType.PROVISION_SUCCESS, true); diff --git a/application/src/main/java/org/thingsboard/server/service/edge/EdgeContextComponent.java b/application/src/main/java/org/thingsboard/server/service/edge/EdgeContextComponent.java index 5c5eea32fd..c6bfef4971 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/EdgeContextComponent.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/EdgeContextComponent.java @@ -24,6 +24,7 @@ import org.thingsboard.server.cache.limits.RateLimitService; import org.thingsboard.server.cluster.TbClusterService; import org.thingsboard.server.common.data.edge.EdgeEventType; import org.thingsboard.server.common.msg.notification.NotificationRuleProcessor; +import org.thingsboard.server.dao.ai.AiModelService; import org.thingsboard.server.dao.alarm.AlarmCommentService; import org.thingsboard.server.dao.alarm.AlarmService; import org.thingsboard.server.dao.asset.AssetProfileService; @@ -59,6 +60,7 @@ import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.edge.rpc.EdgeEventStorageSettings; import org.thingsboard.server.service.edge.rpc.EdgeRpcService; import org.thingsboard.server.service.edge.rpc.processor.EdgeProcessor; +import org.thingsboard.server.service.edge.rpc.processor.ai.AiModelProcessor; import org.thingsboard.server.service.edge.rpc.processor.alarm.AlarmProcessor; import org.thingsboard.server.service.edge.rpc.processor.alarm.comment.AlarmCommentProcessor; import org.thingsboard.server.service.edge.rpc.processor.asset.AssetEdgeProcessor; @@ -261,6 +263,11 @@ public class EdgeContextComponent { @Autowired private CalculatedFieldProcessor calculatedFieldProcessor; + @Autowired + private AiModelService aiModelService; + @Autowired + private AiModelProcessor aiModelProcessor; + public EdgeProcessor getProcessor(EdgeEventType edgeEventType) { EdgeProcessor processor = processorMap.get(edgeEventType); if (processor == null) { diff --git a/application/src/main/java/org/thingsboard/server/service/edge/EdgeEventSourcingListener.java b/application/src/main/java/org/thingsboard/server/service/edge/EdgeEventSourcingListener.java index fda9a1d17b..825b911403 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/EdgeEventSourcingListener.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/EdgeEventSourcingListener.java @@ -113,7 +113,7 @@ public class EdgeEventSourcingListener { return; } try { - if (EntityType.TENANT == entityType || EntityType.EDGE == entityType || EntityType.AI_MODEL == entityType) { + if (EntityType.TENANT == entityType || EntityType.EDGE == entityType) { return; } log.trace("[{}] DeleteEntityEvent called: {}", tenantId, event); @@ -227,7 +227,7 @@ public class EdgeEventSourcingListener { break; case TENANT: return !event.getCreated(); - case API_USAGE_STATE, EDGE, AI_MODEL: + case API_USAGE_STATE, EDGE: return false; case DOMAIN: if (entity instanceof Domain domain) { diff --git a/application/src/main/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtils.java b/application/src/main/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtils.java index 192c56692d..505b03a1cd 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtils.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtils.java @@ -44,6 +44,7 @@ import org.thingsboard.server.common.data.TbResource; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.ai.AiModel; import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.alarm.AlarmComment; import org.thingsboard.server.common.data.asset.Asset; @@ -52,6 +53,7 @@ import org.thingsboard.server.common.data.cf.CalculatedField; import org.thingsboard.server.common.data.domain.DomainInfo; import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.edge.EdgeEventActionType; +import org.thingsboard.server.common.data.id.AiModelId; import org.thingsboard.server.common.data.id.AssetId; import org.thingsboard.server.common.data.id.AssetProfileId; import org.thingsboard.server.common.data.id.CalculatedFieldId; @@ -86,6 +88,7 @@ import org.thingsboard.server.common.data.security.DeviceCredentials; import org.thingsboard.server.common.data.security.UserCredentials; import org.thingsboard.server.common.data.widget.WidgetTypeDetails; import org.thingsboard.server.common.data.widget.WidgetsBundle; +import org.thingsboard.server.gen.edge.v1.AiModelUpdateMsg; import org.thingsboard.server.gen.edge.v1.AlarmCommentUpdateMsg; import org.thingsboard.server.gen.edge.v1.AlarmUpdateMsg; import org.thingsboard.server.gen.edge.v1.AssetProfileUpdateMsg; @@ -654,4 +657,17 @@ public class EdgeMsgConstructorUtils { .setIdLSB(calculatedFieldId.getId().getLeastSignificantBits()).build(); } + public static AiModelUpdateMsg constructAiModelUpdatedMsg(UpdateMsgType msgType, AiModel aiModel) { + return AiModelUpdateMsg.newBuilder().setMsgType(msgType).setEntity(JacksonUtil.toString(aiModel)) + .setIdMSB(aiModel.getId().getId().getMostSignificantBits()) + .setIdLSB(aiModel.getId().getId().getLeastSignificantBits()).build(); + } + + public static AiModelUpdateMsg constructAiModelDeleteMsg(AiModelId aiModelId) { + return AiModelUpdateMsg.newBuilder() + .setMsgType(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE) + .setIdMSB(aiModelId.getId().getMostSignificantBits()) + .setIdLSB(aiModelId.getId().getLeastSignificantBits()).build(); + } + } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/RelatedEdgesSourcingListener.java b/application/src/main/java/org/thingsboard/server/service/edge/RelatedEdgesSourcingListener.java index 8a111e4d9d..d942dc2277 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/RelatedEdgesSourcingListener.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/RelatedEdgesSourcingListener.java @@ -54,16 +54,18 @@ public class RelatedEdgesSourcingListener { @TransactionalEventListener(fallbackExecution = true) public void handleEvent(ActionEntityEvent event) { - executorService.submit(() -> { - log.trace("[{}] ActionEntityEvent called: {}", event.getTenantId(), event); - try { - switch (event.getActionType()) { - case ASSIGNED_TO_EDGE, UNASSIGNED_FROM_EDGE -> relatedEdgesService.publishRelatedEdgeIdsEvictEvent(event.getTenantId(), event.getEntityId()); - } - } catch (Exception e) { - log.error("[{}] failed to process ActionEntityEvent: {}", event.getTenantId(), event, e); + switch (event.getActionType()) { + case ASSIGNED_TO_EDGE, UNASSIGNED_FROM_EDGE -> { + executorService.submit(() -> { + log.trace("[{}] ActionEntityEvent called: {}", event.getTenantId(), event); + try { + relatedEdgesService.publishRelatedEdgeIdsEvictEvent(event.getTenantId(), event.getEntityId()); + } catch (Exception e) { + log.error("[{}] failed to process ActionEntityEvent: {}", event.getTenantId(), event, e); + } + }); } - }); + } } @TransactionalEventListener( diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeEventStorageSettings.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeEventStorageSettings.java index 89d19438bd..618aee5e00 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeEventStorageSettings.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeEventStorageSettings.java @@ -29,4 +29,6 @@ public class EdgeEventStorageSettings { private long noRecordsSleepInterval; @Value("${edges.storage.sleep_between_batches}") private long sleepIntervalBetweenBatches; + @Value("${edges.storage.misordering_compensation_millis:60000}") + private long misorderingCompensationMillis; } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java index eaef1f7c7d..9fcb7425b2 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java @@ -59,8 +59,7 @@ import org.thingsboard.server.gen.edge.v1.RequestMsg; import org.thingsboard.server.gen.edge.v1.ResponseMsg; import org.thingsboard.server.queue.discovery.TbServiceInfoProvider; import org.thingsboard.server.queue.discovery.TopicService; -import org.thingsboard.server.queue.kafka.TbKafkaSettings; -import org.thingsboard.server.queue.kafka.TbKafkaTopicConfigs; +import org.thingsboard.server.queue.kafka.KafkaAdmin; import org.thingsboard.server.queue.provider.TbCoreQueueFactory; import org.thingsboard.server.queue.util.AfterStartUp; import org.thingsboard.server.queue.util.TbCoreComponent; @@ -94,14 +93,13 @@ import static org.thingsboard.server.service.state.DefaultDeviceStateService.LAS @TbCoreComponent public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase implements EdgeRpcService { - private static final int DESTROY_SESSION_MAX_ATTEMPTS = 10; - private final ConcurrentMap sessions = new ConcurrentHashMap<>(); private final ConcurrentMap sessionNewEventsLocks = new ConcurrentHashMap<>(); private final Map sessionNewEvents = new HashMap<>(); private final ConcurrentMap> sessionEdgeEventChecks = new ConcurrentHashMap<>(); private final ConcurrentMap> localSyncEdgeRequests = new ConcurrentHashMap<>(); private final ConcurrentMap edgeEventsMigrationProcessed = new ConcurrentHashMap<>(); + private final List zombieSessions = new ArrayList<>(); @Value("${edges.rpc.port}") private int rpcPort; @@ -153,10 +151,7 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i private TbCoreQueueFactory tbCoreQueueFactory; @Autowired - private Optional kafkaSettings; - - @Autowired - private Optional kafkaTopicConfigs; + private Optional kafkaAdmin; private Server server; @@ -197,7 +192,7 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i this.edgeEventProcessingExecutorService = ThingsBoardExecutors.newScheduledThreadPool(schedulerPoolSize, "edge-event-check-scheduler"); this.sendDownlinkExecutorService = ThingsBoardExecutors.newScheduledThreadPool(sendSchedulerPoolSize, "edge-send-scheduler"); this.executorService = ThingsBoardExecutors.newSingleThreadScheduledExecutor("edge-service"); - this.executorService.scheduleAtFixedRate(this::destroyKafkaSessionIfDisconnectedAndConsumerActive, 60, 60, TimeUnit.SECONDS); + this.executorService.scheduleAtFixedRate(this::cleanupZombieSessions, 60, 60, TimeUnit.SECONDS); log.info("Edge RPC service initialized!"); } @@ -232,8 +227,8 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i } private EdgeGrpcSession createEdgeGrpcSession(StreamObserver outputStream) { - return kafkaSettings.isPresent() && kafkaTopicConfigs.isPresent() - ? new KafkaEdgeGrpcSession(ctx, topicService, tbCoreQueueFactory, kafkaSettings.get(), kafkaTopicConfigs.get(), outputStream, this::onEdgeConnect, this::onEdgeDisconnect, + return kafkaAdmin.isPresent() + ? new KafkaEdgeGrpcSession(ctx, topicService, tbCoreQueueFactory, kafkaAdmin.get(), outputStream, this::onEdgeConnect, this::onEdgeDisconnect, sendDownlinkExecutorService, maxInboundMessageSize, maxHighPriorityQueueSizePerSession) : new PostgresEdgeGrpcSession(ctx, outputStream, this::onEdgeConnect, this::onEdgeDisconnect, sendDownlinkExecutorService, maxInboundMessageSize, maxHighPriorityQueueSizePerSession); @@ -522,14 +517,10 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i private void destroySession(EdgeGrpcSession session) { try (session) { - for (int i = 0; i < DESTROY_SESSION_MAX_ATTEMPTS; i++) { - if (session.destroy()) { - break; - } else { - try { - Thread.sleep(100); - } catch (InterruptedException ignored) {} - } + if (!session.destroy()) { + log.warn("[{}][{}] Session destroy failed for edge [{}] with session id [{}]. Adding to zombie queue for later cleanup.", + session.getTenantId(), session.getEdge().getId(), session.getEdge().getName(), session.getSessionId()); + zombieSessions.add(session); } } } @@ -638,15 +629,15 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i } } - private void destroyKafkaSessionIfDisconnectedAndConsumerActive() { + private void cleanupZombieSessions() { try { List toRemove = new ArrayList<>(); for (EdgeGrpcSession session : sessions.values()) { if (session instanceof KafkaEdgeGrpcSession kafkaSession && - !kafkaSession.isConnected() && - kafkaSession.getConsumer() != null && - kafkaSession.getConsumer().getConsumer() != null && - !kafkaSession.getConsumer().getConsumer().isStopped()) { + !kafkaSession.isConnected() && + kafkaSession.getConsumer() != null && + kafkaSession.getConsumer().getConsumer() != null && + !kafkaSession.getConsumer().getConsumer().isStopped()) { toRemove.add(kafkaSession.getEdge().getId()); } } @@ -659,6 +650,17 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i } } } + zombieSessions.removeIf(zombie -> { + if (zombie.destroy()) { + log.info("[{}][{}] Successfully cleaned up zombie session [{}] for edge [{}].", + zombie.getTenantId(), zombie.getEdge().getId(), zombie.getSessionId(), zombie.getEdge().getName()); + return true; + } else { + log.warn("[{}][{}] Failed to remove zombie session [{}] for edge [{}].", + zombie.getTenantId(), zombie.getEdge().getId(), zombie.getSessionId(), zombie.getEdge().getName()); + return false; + } + }); } catch (Exception e) { log.warn("Failed to cleanup kafka sessions", e); } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java index 521730741f..e4297e9bfc 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java @@ -46,6 +46,7 @@ import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.page.TimePageLink; import org.thingsboard.server.common.msg.edge.EdgeEventUpdateMsg; import org.thingsboard.server.dao.edge.stats.EdgeStatsKey; +import org.thingsboard.server.gen.edge.v1.AiModelUpdateMsg; import org.thingsboard.server.gen.edge.v1.AlarmCommentUpdateMsg; import org.thingsboard.server.gen.edge.v1.AlarmUpdateMsg; import org.thingsboard.server.gen.edge.v1.AssetProfileUpdateMsg; @@ -589,7 +590,8 @@ public abstract class EdgeGrpcSession implements Closeable { previousStartSeqId, false, Integer.toUnsignedLong(ctx.getEdgeEventStorageSettings().getMaxReadRecordsCount()), - ctx.getEdgeEventService()); + ctx.getEdgeEventService(), + ctx.getEdgeEventStorageSettings().getMisorderingCompensationMillis()); log.trace("[{}][{}] starting processing edge events, previousStartTs = {}, previousStartSeqId = {}", tenantId, edge.getId(), previousStartTs, previousStartSeqId); Futures.addCallback(startProcessingEdgeEvents(fetcher), new FutureCallback<>() { @@ -933,6 +935,11 @@ public abstract class EdgeGrpcSession implements Closeable { result.add(ctx.getCalculatedFieldProcessor().processCalculatedFieldMsgFromEdge(edge.getTenantId(), edge, calculatedFieldUpdateMsg)); } } + if (uplinkMsg.getAiModelUpdateMsgCount() > 0) { + for (AiModelUpdateMsg aiModelUpdateMsg : uplinkMsg.getAiModelUpdateMsgList()) { + result.add(ctx.getAiModelProcessor().processAiModelMsgFromEdge(edge.getTenantId(), edge, aiModelUpdateMsg)); + } + } } catch (Exception e) { String failureMsg = String.format("Can't process uplink msg [%s] from edge", uplinkMsg); log.trace("[{}][{}] Can't process uplink msg [{}]", tenantId, edge.getId(), uplinkMsg, e); diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/KafkaEdgeGrpcSession.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/KafkaEdgeGrpcSession.java index ab0b42abb4..d165be33d4 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/KafkaEdgeGrpcSession.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/KafkaEdgeGrpcSession.java @@ -32,9 +32,7 @@ import org.thingsboard.server.queue.TbQueueConsumer; import org.thingsboard.server.queue.common.TbProtoQueueMsg; import org.thingsboard.server.queue.common.consumer.QueueConsumerManager; import org.thingsboard.server.queue.discovery.TopicService; -import org.thingsboard.server.queue.kafka.TbKafkaAdmin; -import org.thingsboard.server.queue.kafka.TbKafkaSettings; -import org.thingsboard.server.queue.kafka.TbKafkaTopicConfigs; +import org.thingsboard.server.queue.kafka.KafkaAdmin; import org.thingsboard.server.queue.provider.TbCoreQueueFactory; import org.thingsboard.server.service.edge.EdgeContextComponent; @@ -51,9 +49,7 @@ public class KafkaEdgeGrpcSession extends EdgeGrpcSession { private final TopicService topicService; private final TbCoreQueueFactory tbCoreQueueFactory; - - private final TbKafkaSettings kafkaSettings; - private final TbKafkaTopicConfigs kafkaTopicConfigs; + private final KafkaAdmin kafkaAdmin; private volatile boolean isHighPriorityProcessing; @@ -63,21 +59,20 @@ public class KafkaEdgeGrpcSession extends EdgeGrpcSession { private ExecutorService consumerExecutor; public KafkaEdgeGrpcSession(EdgeContextComponent ctx, TopicService topicService, TbCoreQueueFactory tbCoreQueueFactory, - TbKafkaSettings kafkaSettings, TbKafkaTopicConfigs kafkaTopicConfigs, StreamObserver outputStream, + KafkaAdmin kafkaAdmin, StreamObserver outputStream, BiConsumer sessionOpenListener, BiConsumer sessionCloseListener, ScheduledExecutorService sendDownlinkExecutorService, int maxInboundMessageSize, int maxHighPriorityQueueSizePerSession) { super(ctx, outputStream, sessionOpenListener, sessionCloseListener, sendDownlinkExecutorService, maxInboundMessageSize, maxHighPriorityQueueSizePerSession); this.topicService = topicService; this.tbCoreQueueFactory = tbCoreQueueFactory; - this.kafkaSettings = kafkaSettings; - this.kafkaTopicConfigs = kafkaTopicConfigs; + this.kafkaAdmin = kafkaAdmin; } private void processMsgs(List> msgs, TbQueueConsumer> consumer) { log.trace("[{}][{}] starting processing edge events", tenantId, edge.getId()); if (!isConnected() || isSyncInProgress() || isHighPriorityProcessing) { log.debug("[{}][{}] edge not connected, edge sync is not completed or high priority processing in progress, " + - "connected = {}, sync in progress = {}, high priority in progress = {}. Skipping iteration", + "connected = {}, sync in progress = {}, high priority in progress = {}. Skipping iteration", tenantId, edge.getId(), isConnected(), isSyncInProgress(), isHighPriorityProcessing); return; } @@ -159,7 +154,6 @@ public class KafkaEdgeGrpcSession extends EdgeGrpcSession { @Override public void cleanUp() { String topic = topicService.buildEdgeEventNotificationsTopicPartitionInfo(tenantId, edge.getId()).getTopic(); - TbKafkaAdmin kafkaAdmin = new TbKafkaAdmin(kafkaSettings, kafkaTopicConfigs.getEdgeEventConfigs()); kafkaAdmin.deleteTopic(topic); kafkaAdmin.deleteConsumerGroup(topic); } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/GeneralEdgeEventFetcher.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/GeneralEdgeEventFetcher.java index 5d7df601b5..b4f894c422 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/GeneralEdgeEventFetcher.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/GeneralEdgeEventFetcher.java @@ -26,13 +26,9 @@ import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.page.TimePageLink; import org.thingsboard.server.dao.edge.EdgeEventService; -import java.util.concurrent.TimeUnit; - @AllArgsConstructor @Slf4j public class GeneralEdgeEventFetcher implements EdgeEventFetcher { - // Subtract from queueStartTs to ensure no data is lost due to potential misordering of edge events by created_time. - private static final long MISORDERING_COMPENSATION_MILLIS = TimeUnit.SECONDS.toMillis(60); private final Long queueStartTs; private Long seqIdStart; @@ -40,6 +36,10 @@ public class GeneralEdgeEventFetcher implements EdgeEventFetcher { private boolean seqIdNewCycleStarted; private Long maxReadRecordsCount; private final EdgeEventService edgeEventService; + // Subtract from queueStartTs to compensate for possible misalignment between `created_time` and `seqId`. + // This ensures early events with lower seqId are not skipped due to partitioning by `created_time`. + // See: edge_event is partitioned by created_time but sorted by seqId during retrieval. + private final long misorderingCompensationMillis; @Override public PageLink getPageLink(int pageSize) { @@ -48,7 +48,7 @@ public class GeneralEdgeEventFetcher implements EdgeEventFetcher { 0, null, null, - queueStartTs > 0 ? queueStartTs - MISORDERING_COMPENSATION_MILLIS : 0, + queueStartTs > 0 ? queueStartTs - misorderingCompensationMillis : 0, System.currentTimeMillis()); } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessor.java index a2243a88d2..d6bfeae0b7 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessor.java @@ -139,7 +139,7 @@ public abstract class BaseEdgeProcessor implements EdgeProcessor { UPDATED_COMMENT, DELETED -> true; default -> switch (type) { case ALARM, ALARM_COMMENT, RULE_CHAIN, RULE_CHAIN_METADATA, USER, CUSTOMER, TENANT, TENANT_PROFILE, - WIDGETS_BUNDLE, WIDGET_TYPE, ADMIN_SETTINGS, OTA_PACKAGE, QUEUE, RELATION, CALCULATED_FIELD, NOTIFICATION_TEMPLATE, + WIDGETS_BUNDLE, WIDGET_TYPE, ADMIN_SETTINGS, OTA_PACKAGE, QUEUE, RELATION, CALCULATED_FIELD, AI_MODEL, NOTIFICATION_TEMPLATE, NOTIFICATION_TARGET, NOTIFICATION_RULE -> true; default -> false; }; diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/ai/AiModelEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/ai/AiModelEdgeProcessor.java new file mode 100644 index 0000000000..74ca7ac27a --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/ai/AiModelEdgeProcessor.java @@ -0,0 +1,130 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.edge.rpc.processor.ai; + +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.util.Pair; +import org.springframework.stereotype.Component; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.EdgeUtils; +import org.thingsboard.server.common.data.ai.AiModel; +import org.thingsboard.server.common.data.edge.Edge; +import org.thingsboard.server.common.data.edge.EdgeEvent; +import org.thingsboard.server.common.data.edge.EdgeEventActionType; +import org.thingsboard.server.common.data.edge.EdgeEventType; +import org.thingsboard.server.common.data.id.AiModelId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.msg.TbMsgType; +import org.thingsboard.server.common.msg.TbMsgMetaData; +import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.gen.edge.v1.AiModelUpdateMsg; +import org.thingsboard.server.gen.edge.v1.DownlinkMsg; +import org.thingsboard.server.gen.edge.v1.EdgeVersion; +import org.thingsboard.server.gen.edge.v1.UpdateMsgType; +import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.edge.EdgeMsgConstructorUtils; + +import java.util.Optional; +import java.util.UUID; + +@Slf4j +@Component +@TbCoreComponent +public class AiModelEdgeProcessor extends BaseAiModelProcessor implements AiModelProcessor { + + @Override + public ListenableFuture processAiModelMsgFromEdge(TenantId tenantId, Edge edge, AiModelUpdateMsg aiModelUpdateMsg) { + AiModelId aiModelId = new AiModelId(new UUID(aiModelUpdateMsg.getIdMSB(), aiModelUpdateMsg.getIdLSB())); + try { + edgeSynchronizationManager.getEdgeId().set(edge.getId()); + + switch (aiModelUpdateMsg.getMsgType()) { + case ENTITY_CREATED_RPC_MESSAGE: + case ENTITY_UPDATED_RPC_MESSAGE: + processAiModel(tenantId, aiModelId, aiModelUpdateMsg, edge); + return Futures.immediateFuture(null); + case UNRECOGNIZED: + default: + return handleUnsupportedMsgType(aiModelUpdateMsg.getMsgType()); + } + } catch (DataValidationException e) { + return Futures.immediateFailedFuture(e); + } finally { + edgeSynchronizationManager.getEdgeId().remove(); + } + } + + @Override + public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) { + AiModelId aiModelId = new AiModelId(edgeEvent.getEntityId()); + switch (edgeEvent.getAction()) { + case ADDED, UPDATED -> { + Optional aiModel = edgeCtx.getAiModelService().findAiModelById(edgeEvent.getTenantId(), aiModelId); + if (aiModel.isPresent()) { + UpdateMsgType msgType = getUpdateMsgType(edgeEvent.getAction()); + AiModelUpdateMsg aiModelUpdateMsg = EdgeMsgConstructorUtils.constructAiModelUpdatedMsg(msgType, aiModel.get()); + return DownlinkMsg.newBuilder() + .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) + .addAiModelUpdateMsg(aiModelUpdateMsg) + .build(); + } + } + case DELETED -> { + AiModelUpdateMsg aiModelUpdateMsg = EdgeMsgConstructorUtils.constructAiModelDeleteMsg(aiModelId); + return DownlinkMsg.newBuilder() + .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) + .addAiModelUpdateMsg(aiModelUpdateMsg) + .build(); + } + } + return null; + } + + @Override + public EdgeEventType getEdgeEventType() { + return EdgeEventType.AI_MODEL; + } + + private void processAiModel(TenantId tenantId, AiModelId aiModelId, AiModelUpdateMsg aiModelUpdateMsg, Edge edge) { + Pair resultPair = super.saveOrUpdateAiModel(tenantId, aiModelId, aiModelUpdateMsg); + Boolean wasCreated = resultPair.getFirst(); + if (wasCreated) { + pushAiModelCreatedEventToRuleEngine(tenantId, edge, aiModelId); + } + Boolean nameWasUpdated = resultPair.getSecond(); + if (nameWasUpdated) { + saveEdgeEvent(tenantId, edge.getId(), EdgeEventType.AI_MODEL, EdgeEventActionType.UPDATED, aiModelId, null); + } + } + + private void pushAiModelCreatedEventToRuleEngine(TenantId tenantId, Edge edge, AiModelId aiModelId) { + try { + Optional aiModel = edgeCtx.getAiModelService().findAiModelById(tenantId, aiModelId); + if (aiModel.isPresent()) { + String aiModelAsString = JacksonUtil.toString(aiModel.get()); + TbMsgMetaData msgMetaData = getEdgeActionTbMsgMetaData(edge, edge.getCustomerId()); + pushEntityEventToRuleEngine(tenantId, aiModelId, edge.getCustomerId(), TbMsgType.ENTITY_CREATED, aiModelAsString, msgMetaData); + } else { + log.warn("[{}][{}] Failed to find aiModel", tenantId, aiModelId); + } + } catch (Exception e) { + log.warn("[{}][{}] Failed to push aiModel action to rule engine: {}", tenantId, aiModelId, TbMsgType.ENTITY_CREATED.name(), e); + } + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/ai/AiModelProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/ai/AiModelProcessor.java new file mode 100644 index 0000000000..f66421167a --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/ai/AiModelProcessor.java @@ -0,0 +1,28 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.edge.rpc.processor.ai; + +import com.google.common.util.concurrent.ListenableFuture; +import org.thingsboard.server.common.data.edge.Edge; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.gen.edge.v1.AiModelUpdateMsg; +import org.thingsboard.server.service.edge.rpc.processor.EdgeProcessor; + +public interface AiModelProcessor extends EdgeProcessor { + + ListenableFuture processAiModelMsgFromEdge(TenantId tenantId, Edge edge, AiModelUpdateMsg aiModelUpdateMsg); + +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/ai/BaseAiModelProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/ai/BaseAiModelProcessor.java new file mode 100644 index 0000000000..cb1d27e0a1 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/ai/BaseAiModelProcessor.java @@ -0,0 +1,81 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.edge.rpc.processor.ai; + +import com.datastax.oss.driver.api.core.uuid.Uuids; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.util.Pair; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.StringUtils; +import org.thingsboard.server.common.data.ai.AiModel; +import org.thingsboard.server.common.data.id.AiModelId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.dao.service.DataValidator; +import org.thingsboard.server.gen.edge.v1.AiModelUpdateMsg; +import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor; + +import java.util.Optional; + +@Slf4j +public abstract class BaseAiModelProcessor extends BaseEdgeProcessor { + + @Autowired + private DataValidator aiModelValidator; + + protected Pair saveOrUpdateAiModel(TenantId tenantId, AiModelId aiModelId, AiModelUpdateMsg aiModelUpdateMsg) { + boolean isCreated = false; + boolean isNameUpdated = false; + try { + AiModel aiModel = JacksonUtil.fromString(aiModelUpdateMsg.getEntity(), AiModel.class, true); + if (aiModel == null) { + throw new RuntimeException("[{" + tenantId + "}] aiModelUpdateMsg {" + aiModelUpdateMsg + " } cannot be converted to aiModel"); + } + + Optional aiModelById = edgeCtx.getAiModelService().findAiModelById(tenantId, aiModelId); + if (aiModelById.isEmpty()) { + aiModel.setCreatedTime(Uuids.unixTimestamp(aiModelId.getId())); + isCreated = true; + aiModel.setId(null); + } else { + aiModel.setId(aiModelId); + } + + String aiModelName = aiModel.getName(); + Optional aiModelByName = edgeCtx.getAiModelService().findAiModelByTenantIdAndName(aiModel.getTenantId(), aiModelName); + if (aiModelByName.isPresent() && !aiModelByName.get().getId().equals(aiModelId)) { + aiModelName = aiModelName + "_" + StringUtils.randomAlphabetic(15); + log.warn("[{}] aiModel with name {} already exists. Renaming aiModel name to {}", + tenantId, aiModel.getName(), aiModelByName.get().getName()); + isNameUpdated = true; + } + aiModel.setName(aiModelName); + + aiModelValidator.validate(aiModel, AiModel::getTenantId); + + if (isCreated) { + aiModel.setId(aiModelId); + } + + edgeCtx.getAiModelService().save(aiModel, false); + } catch (Exception e) { + log.error("[{}] Failed to process aiModel update msg [{}]", tenantId, aiModelUpdateMsg, e); + throw e; + } + return Pair.of(isCreated, isNameUpdated); + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/alarm/BaseAlarmProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/alarm/BaseAlarmProcessor.java index e8c2f65975..d6fd2f6968 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/alarm/BaseAlarmProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/alarm/BaseAlarmProcessor.java @@ -77,7 +77,7 @@ public abstract class BaseAlarmProcessor extends BaseEdgeProcessor { case ALARM_CLEAR_RPC_MESSAGE: Alarm alarmToClear = edgeCtx.getAlarmService().findAlarmById(tenantId, alarmId); if (alarmToClear != null) { - edgeCtx.getAlarmService().clearAlarm(tenantId, alarmId, alarm.getClearTs(), alarm.getDetails()); + edgeCtx.getAlarmService().clearAlarm(tenantId, alarmId, alarm.getClearTs(), alarm.getDetails(), true); } break; case ENTITY_DELETED_RPC_MESSAGE: diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/cf/BaseCalculatedFieldProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/cf/BaseCalculatedFieldProcessor.java index 4ef6ec7ba2..c3f9300e24 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/cf/BaseCalculatedFieldProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/cf/BaseCalculatedFieldProcessor.java @@ -53,7 +53,7 @@ public abstract class BaseCalculatedFieldProcessor extends BaseEdgeProcessor { } String calculatedFieldName = calculatedField.getName(); - CalculatedField calculatedFieldByName = edgeCtx.getCalculatedFieldService().findByEntityIdAndName(calculatedField.getEntityId(), calculatedFieldName); + CalculatedField calculatedFieldByName = edgeCtx.getCalculatedFieldService().findByEntityIdAndTypeAndName(calculatedField.getEntityId(), calculatedField.getType(), calculatedFieldName); if (calculatedFieldByName != null && !calculatedFieldByName.getId().equals(calculatedFieldId)) { calculatedFieldName = calculatedFieldName + "_" + StringUtils.randomAlphabetic(15); log.warn("[{}] calculatedField with name {} already exists. Renaming calculatedField name to {}", diff --git a/application/src/main/java/org/thingsboard/server/service/edge/stats/EdgeStatsService.java b/application/src/main/java/org/thingsboard/server/service/edge/stats/EdgeStatsService.java index 3fc391ec72..48b2a47cfb 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/stats/EdgeStatsService.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/stats/EdgeStatsService.java @@ -35,7 +35,7 @@ import org.thingsboard.server.dao.edge.stats.EdgeStatsCounterService; import org.thingsboard.server.dao.edge.stats.MsgCounters; import org.thingsboard.server.dao.timeseries.TimeseriesService; import org.thingsboard.server.queue.discovery.TopicService; -import org.thingsboard.server.queue.kafka.TbKafkaAdmin; +import org.thingsboard.server.queue.kafka.KafkaAdmin; import org.thingsboard.server.queue.util.TbCoreComponent; import java.util.Collections; @@ -63,7 +63,7 @@ public class EdgeStatsService { private final TimeseriesService tsService; private final EdgeStatsCounterService statsCounterService; private final TopicService topicService; - private final Optional tbKafkaAdmin; + private final Optional kafkaAdmin; @Value("${edges.stats.ttl:30}") private int edgesStatsTtlDays; @@ -81,12 +81,12 @@ public class EdgeStatsService { long ts = now - (now % reportIntervalMillis); Map countersByEdge = statsCounterService.getCounterByEdge(); - Map lagByEdgeId = tbKafkaAdmin.isPresent() ? getEdgeLagByEdgeId(countersByEdge) : Collections.emptyMap(); + Map lagByEdgeId = kafkaAdmin.isPresent() ? getEdgeLagByEdgeId(countersByEdge) : Collections.emptyMap(); Map countersByEdgeSnapshot = new HashMap<>(statsCounterService.getCounterByEdge()); countersByEdgeSnapshot.forEach((edgeId, counters) -> { TenantId tenantId = counters.getTenantId(); - if (tbKafkaAdmin.isPresent()) { + if (kafkaAdmin.isPresent()) { counters.getMsgsLag().set(lagByEdgeId.getOrDefault(edgeId, 0L)); } List statsEntries = List.of( @@ -109,7 +109,7 @@ public class EdgeStatsService { e -> topicService.buildEdgeEventNotificationsTopicPartitionInfo(e.getValue().getTenantId(), e.getKey()).getTopic() )); - Map lagByTopic = tbKafkaAdmin.get().getTotalLagForGroupsBulk(new HashSet<>(edgeToTopicMap.values())); + Map lagByTopic = kafkaAdmin.get().getTotalLagForGroupsBulk(new HashSet<>(edgeToTopicMap.values())); return edgeToTopicMap.entrySet().stream() .collect(Collectors.toMap( diff --git a/application/src/main/java/org/thingsboard/server/service/edqs/KafkaEdqsSyncService.java b/application/src/main/java/org/thingsboard/server/service/edqs/KafkaEdqsSyncService.java index 43b0c575a0..5ded4f66c6 100644 --- a/application/src/main/java/org/thingsboard/server/service/edqs/KafkaEdqsSyncService.java +++ b/application/src/main/java/org/thingsboard/server/service/edqs/KafkaEdqsSyncService.java @@ -20,10 +20,8 @@ import org.springframework.stereotype.Service; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.queue.discovery.TopicService; import org.thingsboard.server.queue.edqs.EdqsConfig; -import org.thingsboard.server.queue.kafka.TbKafkaAdmin; -import org.thingsboard.server.queue.kafka.TbKafkaSettings; +import org.thingsboard.server.queue.kafka.KafkaAdmin; -import java.util.Collections; import java.util.stream.Collectors; import java.util.stream.IntStream; @@ -33,8 +31,7 @@ public class KafkaEdqsSyncService extends EdqsSyncService { private final boolean syncNeeded; - public KafkaEdqsSyncService(TbKafkaSettings kafkaSettings, TopicService topicService, EdqsConfig edqsConfig) { - TbKafkaAdmin kafkaAdmin = new TbKafkaAdmin(kafkaSettings, Collections.emptyMap()); + public KafkaEdqsSyncService(KafkaAdmin kafkaAdmin, TopicService topicService, EdqsConfig edqsConfig) { this.syncNeeded = kafkaAdmin.areAllTopicsEmpty(IntStream.range(0, edqsConfig.getPartitions()) .mapToObj(partition -> TopicPartitionInfo.builder() .topic(topicService.buildTopicName(edqsConfig.getEventsTopic())) diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/EntityStateSourcingListener.java b/application/src/main/java/org/thingsboard/server/service/entitiy/EntityStateSourcingListener.java index 03ab77ac09..9a6a96155f 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/EntityStateSourcingListener.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/EntityStateSourcingListener.java @@ -22,8 +22,10 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; import org.springframework.transaction.event.TransactionalEventListener; import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.rule.engine.api.JobManager; import org.thingsboard.server.cluster.TbClusterService; import org.thingsboard.server.common.data.ApiUsageState; +import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.EntityType; @@ -31,9 +33,11 @@ import org.thingsboard.server.common.data.TbResource; import org.thingsboard.server.common.data.TbResourceInfo; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.TenantProfile; +import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.cf.CalculatedField; +import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.edge.EdgeEvent; import org.thingsboard.server.common.data.id.DeviceId; @@ -44,6 +48,7 @@ import org.thingsboard.server.common.data.job.Job; import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.data.notification.NotificationRequest; import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; +import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.rule.RuleChain; import org.thingsboard.server.common.data.rule.RuleChainType; import org.thingsboard.server.common.data.security.DeviceCredentials; @@ -53,13 +58,18 @@ import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.common.msg.edge.EdgeEventUpdateMsg; import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg; import org.thingsboard.server.common.msg.rule.engine.DeviceCredentialsUpdateNotificationMsg; +import org.thingsboard.server.common.util.ProtoUtils; import org.thingsboard.server.dao.edge.EdgeSynchronizationManager; import org.thingsboard.server.dao.eventsourcing.ActionEntityEvent; import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent; +import org.thingsboard.server.dao.eventsourcing.RelationActionEvent; import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent; import org.thingsboard.server.dao.tenant.TenantService; +import org.thingsboard.server.gen.transport.TransportProtos.EntityActionEventProto; +import org.thingsboard.server.gen.transport.TransportProtos.ToCalculatedFieldMsg; import org.thingsboard.server.queue.TbQueueCallback; -import org.thingsboard.rule.engine.api.JobManager; +import org.thingsboard.server.queue.TbQueueMsgMetadata; +import org.thingsboard.server.service.cf.CalculatedFieldCache; import java.util.Set; @@ -72,6 +82,7 @@ public class EntityStateSourcingListener { private final TbClusterService tbClusterService; private final EdgeSynchronizationManager edgeSynchronizationManager; private final JobManager jobManager; + private final CalculatedFieldCache calculatedFieldCache; @PostConstruct public void init() { @@ -140,6 +151,9 @@ public class EntityStateSourcingListener { case JOB -> { onJobUpdate((Job) event.getEntity()); } + case CUSTOMER -> { + tbClusterService.onCustomerUpdated((Customer) event.getEntity(), (Customer) event.getOldEntity()); + } default -> { } } @@ -153,7 +167,7 @@ public class EntityStateSourcingListener { return; } EntityType entityType = entityId.getEntityType(); - if (!tenantId.isSysTenantId() && entityType != EntityType.TENANT && !tenantService.tenantExists(tenantId)) { + if (entityType != EntityType.TENANT && !tenantExists(tenantId)) { log.debug("[{}] Ignoring DeleteEntityEvent because tenant does not exist: {}", tenantId, event); return; } @@ -216,18 +230,57 @@ public class EntityStateSourcingListener { @TransactionalEventListener(fallbackExecution = true) public void handleEvent(ActionEntityEvent event) { - log.trace("[{}] ActionEntityEvent called: {}", event.getTenantId(), event); - if (ActionType.CREDENTIALS_UPDATED.equals(event.getActionType()) && - EntityType.DEVICE.equals(event.getEntityId().getEntityType()) - && event.getEntity() instanceof DeviceCredentials) { - tbClusterService.pushMsgToCore(new DeviceCredentialsUpdateNotificationMsg(event.getTenantId(), - (DeviceId) event.getEntityId(), (DeviceCredentials) event.getEntity()), null); - } else if (ActionType.ASSIGNED_TO_TENANT.equals(event.getActionType()) && event.getEntity() instanceof Device device) { - Tenant tenant = JacksonUtil.fromString(event.getBody(), Tenant.class); - if (tenant != null) { - tbClusterService.onDeviceAssignedToTenant(tenant.getId(), device); + TenantId tenantId = event.getTenantId(); + log.trace("[{}] ActionEntityEvent called: {}", tenantId, event); + switch (event.getActionType()) { + case CREDENTIALS_UPDATED -> { + if (EntityType.DEVICE.equals(event.getEntityId().getEntityType()) && + event.getEntity() instanceof DeviceCredentials deviceCredentials) { + tbClusterService.pushMsgToCore(new DeviceCredentialsUpdateNotificationMsg(tenantId, + (DeviceId) event.getEntityId(), deviceCredentials), null); + } + } + case ASSIGNED_TO_TENANT -> { + if (event.getEntity() instanceof Device device) { + Tenant tenant = JacksonUtil.fromString(event.getBody(), Tenant.class); + if (tenant != null) { + tbClusterService.onDeviceAssignedToTenant(tenant.getId(), device); + } + pushAssignedFromNotification(tenant, tenantId, device); + } + } + case ALARM_ACK, ALARM_CLEAR, ALARM_DELETE -> { + if (event.getActionType() == ActionType.ALARM_DELETE && !tenantExists(tenantId)) { + return; + } + Alarm alarm = (Alarm) event.getEntity(); + if (calculatedFieldCache.hasCalculatedFields(tenantId, alarm.getOriginator(), ctx -> ctx.getCfType() == CalculatedFieldType.ALARM)) { + ToCalculatedFieldMsg msg = ToCalculatedFieldMsg.newBuilder() + .setEventMsg(toProto(event)) + .build(); + tbClusterService.pushMsgToCalculatedFields(tenantId, alarm.getOriginator(), msg, new TbQueueCallback() { + @Override + public void onSuccess(TbQueueMsgMetadata metadata) {} + + @Override + public void onFailure(Throwable t) { + log.error("[{}] Failed to push alarm event to CF queue: {}", tenantId, event, t); + } + }); + } + } + } + } + + @TransactionalEventListener(fallbackExecution = true) + public void handleEvent(RelationActionEvent relationEvent) { + EntityRelation relation = relationEvent.getRelation(); + if (CalculatedField.isSupportedRefEntity(relation.getFrom()) && CalculatedField.isSupportedRefEntity(relation.getTo())) { + if (relationEvent.getActionType() == ActionType.RELATION_ADD_OR_UPDATE) { + tbClusterService.onRelationUpdated(relationEvent.getTenantId(), relation, TbQueueCallback.EMPTY); + } else if (relationEvent.getActionType() == ActionType.RELATION_DELETED) { + tbClusterService.onRelationDeleted(relationEvent.getTenantId(), relation, TbQueueCallback.EMPTY); } - pushAssignedFromNotification(tenant, event.getTenantId(), device); } } @@ -338,6 +391,10 @@ public class EntityStateSourcingListener { } } + private boolean tenantExists(TenantId tenantId) { + return tenantId.isSysTenantId() || tenantService.tenantExists(tenantId); + } + private TbMsgMetaData getMetaDataForAssignedFrom(Tenant tenant) { TbMsgMetaData metaData = new TbMsgMetaData(); metaData.putValue("assignedFromTenantId", tenant.getId().getId().toString()); @@ -345,4 +402,13 @@ public class EntityStateSourcingListener { return metaData; } + private EntityActionEventProto toProto(ActionEntityEvent event) { + return EntityActionEventProto.newBuilder() + .setTenantId(ProtoUtils.toProto(event.getTenantId())) + .setEntityId(ProtoUtils.toProto(event.getEntityId())) + .setAction(event.getActionType().name()) + .setEntity(event.getEntity() != null ? JacksonUtil.toString(event.getEntity()) : "") + .build(); + } + } diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/asset/DefaultTbAssetService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/asset/DefaultTbAssetService.java index 3f69d00276..ef630c2df4 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/asset/DefaultTbAssetService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/asset/DefaultTbAssetService.java @@ -20,6 +20,7 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.NameConflictStrategy; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.audit.ActionType; @@ -40,10 +41,15 @@ public class DefaultTbAssetService extends AbstractTbEntityService implements Tb @Override public Asset save(Asset asset, User user) throws Exception { + return save(asset, NameConflictStrategy.DEFAULT, user); + } + + @Override + public Asset save(Asset asset, NameConflictStrategy nameConflictStrategy, User user) throws Exception { ActionType actionType = asset.getId() == null ? ActionType.ADDED : ActionType.UPDATED; TenantId tenantId = asset.getTenantId(); try { - Asset savedAsset = checkNotNull(assetService.saveAsset(asset)); + Asset savedAsset = checkNotNull(assetService.saveAsset(asset, nameConflictStrategy)); autoCommit(user, savedAsset.getId()); logEntityActionService.logEntityAction(tenantId, savedAsset.getId(), savedAsset, asset.getCustomerId(), actionType, user); diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/asset/TbAssetService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/asset/TbAssetService.java index a2af8ffcdc..42fce5213e 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/asset/TbAssetService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/asset/TbAssetService.java @@ -16,6 +16,7 @@ package org.thingsboard.server.service.entitiy.asset; import org.thingsboard.server.common.data.Customer; +import org.thingsboard.server.common.data.NameConflictStrategy; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.edge.Edge; @@ -27,6 +28,8 @@ public interface TbAssetService { Asset save(Asset asset, User user) throws Exception; + Asset save(Asset asset, NameConflictStrategy nameConflictStrategy, User user) throws Exception; + void delete(Asset asset, User user); Asset assignAssetToCustomer(TenantId tenantId, AssetId assetId, Customer customer, User user) throws ThingsboardException; diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/cf/DefaultTbCalculatedFieldService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/cf/DefaultTbCalculatedFieldService.java index 4dfaec91cf..63f8a9bf2d 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/cf/DefaultTbCalculatedFieldService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/cf/DefaultTbCalculatedFieldService.java @@ -22,6 +22,7 @@ import org.springframework.transaction.annotation.Transactional; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.cf.CalculatedField; +import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.CalculatedFieldId; import org.thingsboard.server.common.data.id.EntityId; @@ -33,7 +34,7 @@ import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.entitiy.AbstractTbEntityService; import org.thingsboard.server.service.security.model.SecurityUser; -import java.util.Optional; +import java.util.Set; @TbCoreComponent @Service @@ -52,7 +53,7 @@ public class DefaultTbCalculatedFieldService extends AbstractTbEntityService imp CalculatedField existingCf = calculatedFieldService.findById(tenantId, calculatedField.getId()); checkForEntityChange(existingCf, calculatedField); } - checkEntityExistence(tenantId, calculatedField.getEntityId()); + checkEntity(tenantId, calculatedField.getEntityId(), calculatedField.getType()); CalculatedField savedCalculatedField = checkNotNull(calculatedFieldService.save(calculatedField)); logEntityActionService.logEntityAction(tenantId, savedCalculatedField.getId(), savedCalculatedField, actionType, user); return savedCalculatedField; @@ -68,10 +69,9 @@ public class DefaultTbCalculatedFieldService extends AbstractTbEntityService imp } @Override - public PageData findAllByTenantIdAndEntityId(EntityId entityId, SecurityUser user, PageLink pageLink) { - TenantId tenantId = user.getTenantId(); - checkEntityExistence(tenantId, entityId); - return calculatedFieldService.findAllCalculatedFieldsByEntityId(tenantId, entityId, pageLink); + public PageData findByTenantIdAndEntityId(TenantId tenantId, EntityId entityId, CalculatedFieldType type, PageLink pageLink) { + checkEntity(tenantId, entityId, type); + return calculatedFieldService.findCalculatedFieldsByEntityId(tenantId, entityId, type, pageLink); } @Override @@ -95,11 +95,15 @@ public class DefaultTbCalculatedFieldService extends AbstractTbEntityService imp } } - private void checkEntityExistence(TenantId tenantId, EntityId entityId) { - switch (entityId.getEntityType()) { - case ASSET, DEVICE, ASSET_PROFILE, DEVICE_PROFILE -> Optional.ofNullable(entityService.fetchEntity(tenantId, entityId)) - .orElseThrow(() -> new IllegalArgumentException(entityId.getEntityType().getNormalName() + " with id [" + entityId.getId() + "] does not exist.")); - default -> throw new IllegalArgumentException("Entity type '" + entityId.getEntityType() + "' does not support calculated fields."); + private void checkEntity(TenantId tenantId, EntityId entityId, CalculatedFieldType type) { + EntityType entityType = entityId.getEntityType(); + Set supportedTypes = CalculatedField.SUPPORTED_ENTITIES.get(entityType); + if (supportedTypes == null || supportedTypes.isEmpty()) { + throw new IllegalArgumentException("Entity type '" + entityType + "' does not support calculated fields"); + } else if (type != null && !supportedTypes.contains(type)) { + throw new IllegalArgumentException("Entity type '" + entityType + "' does not support '" + type + "' calculated fields"); + } else if (entityService.fetchEntity(tenantId, entityId).isEmpty()) { + throw new IllegalArgumentException(entityType.getNormalName() + " with id [" + entityId.getId() + "] does not exist."); } } diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/cf/TbCalculatedFieldService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/cf/TbCalculatedFieldService.java index 1e04a14a08..20705aaaff 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/cf/TbCalculatedFieldService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/cf/TbCalculatedFieldService.java @@ -16,9 +16,11 @@ package org.thingsboard.server.service.entitiy.cf; import org.thingsboard.server.common.data.cf.CalculatedField; +import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.CalculatedFieldId; import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.service.security.model.SecurityUser; @@ -29,7 +31,7 @@ public interface TbCalculatedFieldService { CalculatedField findById(CalculatedFieldId calculatedFieldId, SecurityUser user); - PageData findAllByTenantIdAndEntityId(EntityId entityId, SecurityUser user, PageLink pageLink); + PageData findByTenantIdAndEntityId(TenantId tenantId, EntityId entityId, CalculatedFieldType type, PageLink pageLink); void delete(CalculatedField calculatedField, SecurityUser user); diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/customer/DefaultTbCustomerService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/customer/DefaultTbCustomerService.java index c070aea087..a2a0e6846d 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/customer/DefaultTbCustomerService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/customer/DefaultTbCustomerService.java @@ -19,6 +19,7 @@ import lombok.AllArgsConstructor; import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.NameConflictStrategy; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.id.CustomerId; @@ -32,10 +33,15 @@ public class DefaultTbCustomerService extends AbstractTbEntityService implements @Override public Customer save(Customer customer, SecurityUser user) throws Exception { + return save(customer, NameConflictStrategy.DEFAULT, user); + } + + @Override + public Customer save(Customer customer, NameConflictStrategy nameConflictStrategy, SecurityUser user) throws Exception { ActionType actionType = customer.getId() == null ? ActionType.ADDED : ActionType.UPDATED; TenantId tenantId = customer.getTenantId(); try { - Customer savedCustomer = checkNotNull(customerService.saveCustomer(customer)); + Customer savedCustomer = checkNotNull(customerService.saveCustomer(customer, nameConflictStrategy)); autoCommit(user, savedCustomer.getId()); logEntityActionService.logEntityAction(tenantId, savedCustomer.getId(), savedCustomer, null, actionType, user); return savedCustomer; diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/customer/TbCustomerService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/customer/TbCustomerService.java index f77e990620..5884ae2e48 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/customer/TbCustomerService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/customer/TbCustomerService.java @@ -16,8 +16,12 @@ package org.thingsboard.server.service.entitiy.customer; import org.thingsboard.server.common.data.Customer; +import org.thingsboard.server.common.data.NameConflictStrategy; import org.thingsboard.server.service.entitiy.SimpleTbEntityService; +import org.thingsboard.server.service.security.model.SecurityUser; public interface TbCustomerService extends SimpleTbEntityService { + Customer save(Customer customer, NameConflictStrategy nameConflictStrategy, SecurityUser user) throws Exception; + } diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/device/DefaultTbDeviceService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/device/DefaultTbDeviceService.java index f182894239..e982adbdf1 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/device/DefaultTbDeviceService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/device/DefaultTbDeviceService.java @@ -25,6 +25,7 @@ import org.springframework.transaction.annotation.Transactional; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.NameConflictStrategy; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.audit.ActionType; @@ -56,10 +57,15 @@ public class DefaultTbDeviceService extends AbstractTbEntityService implements T @Override public Device save(Device device, String accessToken, User user) throws Exception { + return save(device, accessToken, NameConflictStrategy.DEFAULT, user); + } + + @Override + public Device save(Device device, String accessToken, NameConflictStrategy nameConflictStrategy, User user) throws Exception { ActionType actionType = device.getId() == null ? ActionType.ADDED : ActionType.UPDATED; TenantId tenantId = device.getTenantId(); try { - Device savedDevice = checkNotNull(deviceService.saveDeviceWithAccessToken(device, accessToken)); + Device savedDevice = checkNotNull(deviceService.saveDeviceWithAccessToken(device, accessToken, nameConflictStrategy)); autoCommit(user, savedDevice.getId()); logEntityActionService.logEntityAction(tenantId, savedDevice.getId(), savedDevice, savedDevice.getCustomerId(), actionType, user); @@ -73,10 +79,15 @@ public class DefaultTbDeviceService extends AbstractTbEntityService implements T @Override public Device saveDeviceWithCredentials(Device device, DeviceCredentials credentials, User user) throws ThingsboardException { + return saveDeviceWithCredentials(device, credentials, NameConflictStrategy.DEFAULT, user); + } + + @Override + public Device saveDeviceWithCredentials(Device device, DeviceCredentials credentials, NameConflictStrategy nameConflictStrategy, User user) throws ThingsboardException { ActionType actionType = device.getId() == null ? ActionType.ADDED : ActionType.UPDATED; TenantId tenantId = device.getTenantId(); try { - Device savedDevice = checkNotNull(deviceService.saveDeviceWithCredentials(device, credentials)); + Device savedDevice = checkNotNull(deviceService.saveDeviceWithCredentials(device, credentials, nameConflictStrategy)); logEntityActionService.logEntityAction(tenantId, savedDevice.getId(), savedDevice, savedDevice.getCustomerId(), actionType, user); diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/device/TbDeviceService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/device/TbDeviceService.java index c234b3b597..e217d8de7b 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/device/TbDeviceService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/device/TbDeviceService.java @@ -18,6 +18,7 @@ package org.thingsboard.server.service.entitiy.device; import com.google.common.util.concurrent.ListenableFuture; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.NameConflictStrategy; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.edge.Edge; @@ -33,8 +34,12 @@ public interface TbDeviceService { Device save(Device device, String accessToken, User user) throws Exception; + Device save(Device device, String accessToken, NameConflictStrategy nameConflictStrategy, User user) throws Exception; + Device saveDeviceWithCredentials(Device device, DeviceCredentials deviceCredentials, User user) throws ThingsboardException; + Device saveDeviceWithCredentials(Device device, DeviceCredentials deviceCredentials, NameConflictStrategy nameConflictStrategy, User user) throws ThingsboardException; + void delete(Device device, User user); Device assignDeviceToCustomer(TenantId tenantId, DeviceId deviceId, Customer customer, User user) throws ThingsboardException; diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/entityview/DefaultTbEntityViewService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/entityview/DefaultTbEntityViewService.java index 8fb99bfb0e..54699e0995 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/entityview/DefaultTbEntityViewService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/entityview/DefaultTbEntityViewService.java @@ -33,6 +33,7 @@ import org.thingsboard.server.common.data.AttributeScope; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityView; +import org.thingsboard.server.common.data.NameConflictStrategy; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.edge.Edge; @@ -80,10 +81,15 @@ public class DefaultTbEntityViewService extends AbstractTbEntityService implemen @Override public EntityView save(EntityView entityView, EntityView existingEntityView, User user) throws Exception { + return save(entityView, existingEntityView, NameConflictStrategy.DEFAULT, user); + } + + @Override + public EntityView save(EntityView entityView, EntityView existingEntityView, NameConflictStrategy nameConflictStrategy, User user) throws Exception { ActionType actionType = entityView.getId() == null ? ActionType.ADDED : ActionType.UPDATED; TenantId tenantId = entityView.getTenantId(); try { - EntityView savedEntityView = checkNotNull(entityViewService.saveEntityView(entityView)); + EntityView savedEntityView = checkNotNull(entityViewService.saveEntityView(entityView, nameConflictStrategy)); this.updateEntityViewAttributes(tenantId, savedEntityView, existingEntityView, user); autoCommit(user, savedEntityView.getId()); logEntityActionService.logEntityAction(savedEntityView.getTenantId(), savedEntityView.getId(), savedEntityView, diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/entityview/TbEntityViewService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/entityview/TbEntityViewService.java index 3aec924f75..bb4bcf9cad 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/entityview/TbEntityViewService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/entityview/TbEntityViewService.java @@ -18,6 +18,7 @@ package org.thingsboard.server.service.entitiy.entityview; import com.google.common.util.concurrent.ListenableFuture; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.EntityView; +import org.thingsboard.server.common.data.NameConflictStrategy; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.exception.ThingsboardException; @@ -33,6 +34,8 @@ public interface TbEntityViewService extends ComponentLifecycleListener { EntityView save(EntityView entityView, EntityView existingEntityView, User user) throws Exception; + EntityView save(EntityView entityView, EntityView existingEntityView, NameConflictStrategy nameConflictStrategy, User user) throws Exception; + void updateEntityViewAttributes(TenantId tenantId, EntityView savedEntityView, EntityView oldEntityView, User user) throws ThingsboardException; void delete(EntityView entity, User user) throws ThingsboardException; diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/queue/DefaultTbQueueService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/queue/DefaultTbQueueService.java index 0d4cc26ce7..26f0d81182 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/queue/DefaultTbQueueService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/queue/DefaultTbQueueService.java @@ -176,8 +176,8 @@ public class DefaultTbQueueService extends AbstractTbEntityService implements Tb for (int i = oldPartitions; i < newPartitions; i++) { tbQueueAdmin.createTopicIfNotExists( new TopicPartitionInfo(queue.getTopic(), queue.getTenantId(), i, false).getFullTopicName(), - queue.getCustomProperties() - ); + queue.getCustomProperties(), + true); // forcing topic creation because the topic may still be cached on some nodes } } diff --git a/application/src/main/java/org/thingsboard/server/service/install/DefaultDatabaseSchemaSettingsService.java b/application/src/main/java/org/thingsboard/server/service/install/DefaultDatabaseSchemaSettingsService.java index e5bd026fb7..500ad60df0 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/DefaultDatabaseSchemaSettingsService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/DefaultDatabaseSchemaSettingsService.java @@ -17,7 +17,6 @@ package org.thingsboard.server.service.install; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.context.annotation.Profile; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Service; import org.thingsboard.server.service.install.update.DefaultDataUpdateService; @@ -25,14 +24,13 @@ import org.thingsboard.server.service.install.update.DefaultDataUpdateService; import java.util.List; @Service -@Profile("install") @Slf4j @RequiredArgsConstructor public class DefaultDatabaseSchemaSettingsService implements DatabaseSchemaSettingsService { - // This list should include all versions which are compatible for the upgrade. - // The compatibility cycle usually breaks when we have some scripts written in Java that may not work after new release. - private static final List SUPPORTED_VERSIONS_FOR_UPGRADE = List.of("4.1.0"); + // This list should include all versions that are compatible for the upgrade in 4 digits format (like 4.2.0.0, etc.). + // The compatibility cycle usually breaks when we have some scripts written in Java that may not work after a new release. + private static final List SUPPORTED_VERSIONS_FOR_UPGRADE = List.of("4.2.1.0"); private final ProjectInfo projectInfo; private final JdbcTemplate jdbcTemplate; @@ -80,7 +78,7 @@ public class DefaultDatabaseSchemaSettingsService implements DatabaseSchemaSetti @Override public String getPackageSchemaVersion() { if (packageSchemaVersion == null) { - packageSchemaVersion = projectInfo.getProjectVersion(); + packageSchemaVersion = normalizeVersion(projectInfo.getProjectVersion()); } return packageSchemaVersion; } @@ -88,17 +86,28 @@ public class DefaultDatabaseSchemaSettingsService implements DatabaseSchemaSetti @Override public String getDbSchemaVersion() { if (schemaVersionFromDb == null) { - Long version = getSchemaVersionFromDb(); - if (version == null) { + Long dbVersion = getSchemaVersionFromDb(); + if (dbVersion == null) { onSchemaSettingsError("Upgrade failed: the database schema version is missing."); } @SuppressWarnings("DataFlowIssue") - long major = version / 1000000; - long minor = (version % 1000000) / 1000; - long patch = version % 1000; - - schemaVersionFromDb = major + "." + minor + "." + patch; + long version = dbVersion; + + if (version < 1_000_000_000) { + // Old format: MMM mmm ppp (e.g., 4002001 = 4.2.1) + long major = version / 1_000_000; + long minor = (version % 1_000_000) / 1000; + long maintenance = version % 1000; + schemaVersionFromDb = major + "." + minor + "." + maintenance + ".0"; + } else { + // New format: MMM mmm mmm ppp (e.g., 4002001001 = 4.2.1.1) + long major = version / 1_000_000_000; + long minor = (version % 1_000_000_000) / 1_000_000; + long maintenance = (version % 1_000_000) / 1000; + long patch = version % 1000; + schemaVersionFromDb = major + "." + minor + "." + maintenance + "." + patch; + } } return schemaVersionFromDb; } @@ -116,13 +125,26 @@ public class DefaultDatabaseSchemaSettingsService implements DatabaseSchemaSetti long major = Integer.parseInt(versionParts[0]); long minor = Integer.parseInt(versionParts[1]); - long patch = versionParts.length > 2 ? Integer.parseInt(versionParts[2]) : 0; + long maintenance = Integer.parseInt(versionParts[2]); + long patch = Integer.parseInt(versionParts[3]); - return major * 1000000 + minor * 1000 + patch; + return major * 1_000_000_000L + minor * 1_000_000L + maintenance * 1000L + patch; } private void onSchemaSettingsError(String message) { Runtime.getRuntime().addShutdownHook(new Thread(() -> log.error(message))); throw new RuntimeException(message); } + + private String normalizeVersion(String version) { + String[] parts = version.split("\\."); + + int major = Integer.parseInt(parts[0]); + int minor = parts.length > 1 ? Integer.parseInt(parts[1]) : 0; + int maintenance = parts.length > 2 ? Integer.parseInt(parts[2]) : 0; + int patch = parts.length > 3 ? Integer.parseInt(parts[3]) : 0; + + return major + "." + minor + "." + maintenance + "." + patch; + } + } diff --git a/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java b/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java index d580175aa0..a55832c823 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java @@ -49,17 +49,25 @@ import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.alarm.AlarmSeverity; -import org.thingsboard.server.common.data.device.profile.AlarmCondition; -import org.thingsboard.server.common.data.device.profile.AlarmConditionFilter; -import org.thingsboard.server.common.data.device.profile.AlarmConditionFilterKey; -import org.thingsboard.server.common.data.device.profile.AlarmConditionKeyType; -import org.thingsboard.server.common.data.device.profile.AlarmRule; +import org.thingsboard.server.common.data.alarm.rule.AlarmRule; +import org.thingsboard.server.common.data.alarm.rule.condition.AlarmConditionValue; +import org.thingsboard.server.common.data.alarm.rule.condition.SimpleAlarmCondition; +import org.thingsboard.server.common.data.alarm.rule.condition.expression.AlarmConditionFilter; +import org.thingsboard.server.common.data.alarm.rule.condition.expression.ComplexOperation; +import org.thingsboard.server.common.data.alarm.rule.condition.expression.SimpleAlarmConditionExpression; +import org.thingsboard.server.common.data.alarm.rule.condition.expression.predicate.BooleanFilterPredicate; +import org.thingsboard.server.common.data.alarm.rule.condition.expression.predicate.NumericFilterPredicate; +import org.thingsboard.server.common.data.cf.CalculatedField; +import org.thingsboard.server.common.data.cf.CalculatedFieldType; +import org.thingsboard.server.common.data.cf.configuration.AlarmCalculatedFieldConfiguration; +import org.thingsboard.server.common.data.cf.configuration.Argument; +import org.thingsboard.server.common.data.cf.configuration.ArgumentType; +import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey; +import org.thingsboard.server.common.data.debug.DebugSettings; import org.thingsboard.server.common.data.device.profile.DefaultDeviceProfileConfiguration; import org.thingsboard.server.common.data.device.profile.DefaultDeviceProfileTransportConfiguration; -import org.thingsboard.server.common.data.device.profile.DeviceProfileAlarm; import org.thingsboard.server.common.data.device.profile.DeviceProfileData; import org.thingsboard.server.common.data.device.profile.DisabledDeviceProfileProvisionConfiguration; -import org.thingsboard.server.common.data.device.profile.SimpleAlarmConditionSpec; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.DeviceProfileId; @@ -74,12 +82,7 @@ import org.thingsboard.server.common.data.kv.TimeseriesSaveResult; import org.thingsboard.server.common.data.mobile.app.MobileApp; import org.thingsboard.server.common.data.page.PageDataIterable; import org.thingsboard.server.common.data.page.PageLink; -import org.thingsboard.server.common.data.query.BooleanFilterPredicate; -import org.thingsboard.server.common.data.query.DynamicValue; -import org.thingsboard.server.common.data.query.DynamicValueSourceType; import org.thingsboard.server.common.data.query.EntityKeyValueType; -import org.thingsboard.server.common.data.query.FilterPredicateValue; -import org.thingsboard.server.common.data.query.NumericFilterPredicate; import org.thingsboard.server.common.data.queue.ProcessingStrategy; import org.thingsboard.server.common.data.queue.ProcessingStrategyType; import org.thingsboard.server.common.data.queue.Queue; @@ -94,6 +97,7 @@ import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileCon import org.thingsboard.server.common.data.tenant.profile.TenantProfileData; import org.thingsboard.server.common.data.tenant.profile.TenantProfileQueueConfiguration; import org.thingsboard.server.dao.attributes.AttributesService; +import org.thingsboard.server.dao.cf.CalculatedFieldService; import org.thingsboard.server.dao.customer.CustomerService; import org.thingsboard.server.dao.device.DeviceConnectivityConfiguration; import org.thingsboard.server.dao.device.DeviceCredentialsService; @@ -117,7 +121,7 @@ import java.util.Arrays; import java.util.Base64; import java.util.Collections; import java.util.List; -import java.util.TreeMap; +import java.util.Map; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; @@ -155,6 +159,7 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService { private final MobileAppDao mobileAppDao; private final NotificationSettingsService notificationSettingsService; private final NotificationTargetService notificationTargetService; + private final CalculatedFieldService calculatedFieldService; @Autowired private BCryptPasswordEncoder passwordEncoder; @@ -306,8 +311,8 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService { if (invalidSignKey) { log.warn("WARNING: {}. A new JWT Signing Key has been added automatically. " + - "You can change the JWT Signing Key using the Web UI: " + - "Navigate to \"System settings -> Security settings\" while logged in as a System Administrator.", warningMessage); + "You can change the JWT Signing Key using the Web UI: " + + "Navigate to \"System settings -> Security settings\" while logged in as a System Administrator.", warningMessage); jwtSettings.setTokenSigningKey(generateRandomKey()); jwtSettingsService.saveJwtSettings(jwtSettings); @@ -319,9 +324,9 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService { .filter(mobileApp -> !validateKeyLength(mobileApp.getAppSecret())) .forEach(mobileApp -> { log.warn("WARNING: The App secret is shorter than 512 bits, which is a security risk. " + - "A new Application Secret has been added automatically for Mobile Application [{}]. " + - "You can change the Application Secret using the Web UI: " + - "Navigate to \"Security settings -> OAuth2 -> Mobile applications\" while logged in as a System Administrator.", mobileApp.getPkgName()); + "A new Application Secret has been added automatically for Mobile Application [{}]. " + + "You can change the Application Secret using the Web UI: " + + "Navigate to \"Security settings -> OAuth2 -> Mobile applications\" while logged in as a System Administrator.", mobileApp.getPkgName()); mobileApp.setAppSecret(generateRandomKey()); mobileAppDao.save(TenantId.SYS_TENANT_ID, mobileApp); }); @@ -372,11 +377,11 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService { createDevice(demoTenant.getId(), customerB.getId(), defaultDeviceProfile.getId(), "Test Device B1", "B1_TEST_TOKEN", null); createDevice(demoTenant.getId(), customerC.getId(), defaultDeviceProfile.getId(), "Test Device C1", "C1_TEST_TOKEN", null); - createDevice(demoTenant.getId(), null, defaultDeviceProfile.getId(), "DHT11 Demo Device", "DHT11_DEMO_TOKEN", "Demo device that is used in sample " + - "applications that upload data from DHT11 temperature and humidity sensor"); + createDevice(demoTenant.getId(), null, defaultDeviceProfile.getId(), "DHT11 Demo Device", "DHT11_DEMO_TOKEN", + "Demo device that is used in sample applications that upload data from DHT11 temperature and humidity sensor"); - createDevice(demoTenant.getId(), null, defaultDeviceProfile.getId(), "Raspberry Pi Demo Device", "RASPBERRY_PI_DEMO_TOKEN", "Demo device that is used in " + - "Raspberry Pi GPIO control sample application"); + createDevice(demoTenant.getId(), null, defaultDeviceProfile.getId(), "Raspberry Pi Demo Device", "RASPBERRY_PI_DEMO_TOKEN", + "Demo device that is used in Raspberry Pi GPIO control sample application"); DeviceProfile thermostatDeviceProfile = new DeviceProfile(); thermostatDeviceProfile.setTenantId(demoTenant.getId()); @@ -398,110 +403,8 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService { deviceProfileData.setProvisionConfiguration(provisionConfiguration); thermostatDeviceProfile.setProfileData(deviceProfileData); - DeviceProfileAlarm highTemperature = new DeviceProfileAlarm(); - highTemperature.setId("highTemperatureAlarmID"); - highTemperature.setAlarmType("High Temperature"); - AlarmRule temperatureRule = new AlarmRule(); - AlarmCondition temperatureCondition = new AlarmCondition(); - temperatureCondition.setSpec(new SimpleAlarmConditionSpec()); - - AlarmConditionFilter temperatureAlarmFlagAttributeFilter = new AlarmConditionFilter(); - temperatureAlarmFlagAttributeFilter.setKey(new AlarmConditionFilterKey(AlarmConditionKeyType.ATTRIBUTE, "temperatureAlarmFlag")); - temperatureAlarmFlagAttributeFilter.setValueType(EntityKeyValueType.BOOLEAN); - BooleanFilterPredicate temperatureAlarmFlagAttributePredicate = new BooleanFilterPredicate(); - temperatureAlarmFlagAttributePredicate.setOperation(BooleanFilterPredicate.BooleanOperation.EQUAL); - temperatureAlarmFlagAttributePredicate.setValue(new FilterPredicateValue<>(Boolean.TRUE)); - temperatureAlarmFlagAttributeFilter.setPredicate(temperatureAlarmFlagAttributePredicate); - - AlarmConditionFilter temperatureTimeseriesFilter = new AlarmConditionFilter(); - temperatureTimeseriesFilter.setKey(new AlarmConditionFilterKey(AlarmConditionKeyType.TIME_SERIES, "temperature")); - temperatureTimeseriesFilter.setValueType(EntityKeyValueType.NUMERIC); - NumericFilterPredicate temperatureTimeseriesFilterPredicate = new NumericFilterPredicate(); - temperatureTimeseriesFilterPredicate.setOperation(NumericFilterPredicate.NumericOperation.GREATER); - FilterPredicateValue temperatureTimeseriesPredicateValue = - new FilterPredicateValue<>(25.0, null, - new DynamicValue<>(DynamicValueSourceType.CURRENT_DEVICE, "temperatureAlarmThreshold")); - temperatureTimeseriesFilterPredicate.setValue(temperatureTimeseriesPredicateValue); - temperatureTimeseriesFilter.setPredicate(temperatureTimeseriesFilterPredicate); - temperatureCondition.setCondition(Arrays.asList(temperatureAlarmFlagAttributeFilter, temperatureTimeseriesFilter)); - temperatureRule.setAlarmDetails("Current temperature = ${temperature}"); - temperatureRule.setCondition(temperatureCondition); - highTemperature.setCreateRules(new TreeMap<>(Collections.singletonMap(AlarmSeverity.MAJOR, temperatureRule))); - - AlarmRule clearTemperatureRule = new AlarmRule(); - AlarmCondition clearTemperatureCondition = new AlarmCondition(); - clearTemperatureCondition.setSpec(new SimpleAlarmConditionSpec()); - - AlarmConditionFilter clearTemperatureTimeseriesFilter = new AlarmConditionFilter(); - clearTemperatureTimeseriesFilter.setKey(new AlarmConditionFilterKey(AlarmConditionKeyType.TIME_SERIES, "temperature")); - clearTemperatureTimeseriesFilter.setValueType(EntityKeyValueType.NUMERIC); - NumericFilterPredicate clearTemperatureTimeseriesFilterPredicate = new NumericFilterPredicate(); - clearTemperatureTimeseriesFilterPredicate.setOperation(NumericFilterPredicate.NumericOperation.LESS_OR_EQUAL); - FilterPredicateValue clearTemperatureTimeseriesPredicateValue = - new FilterPredicateValue<>(25.0, null, - new DynamicValue<>(DynamicValueSourceType.CURRENT_DEVICE, "temperatureAlarmThreshold")); - - clearTemperatureTimeseriesFilterPredicate.setValue(clearTemperatureTimeseriesPredicateValue); - clearTemperatureTimeseriesFilter.setPredicate(clearTemperatureTimeseriesFilterPredicate); - clearTemperatureCondition.setCondition(Collections.singletonList(clearTemperatureTimeseriesFilter)); - clearTemperatureRule.setCondition(clearTemperatureCondition); - clearTemperatureRule.setAlarmDetails("Current temperature = ${temperature}"); - highTemperature.setClearRule(clearTemperatureRule); - - DeviceProfileAlarm lowHumidity = new DeviceProfileAlarm(); - lowHumidity.setId("lowHumidityAlarmID"); - lowHumidity.setAlarmType("Low Humidity"); - AlarmRule humidityRule = new AlarmRule(); - AlarmCondition humidityCondition = new AlarmCondition(); - humidityCondition.setSpec(new SimpleAlarmConditionSpec()); - - AlarmConditionFilter humidityAlarmFlagAttributeFilter = new AlarmConditionFilter(); - humidityAlarmFlagAttributeFilter.setKey(new AlarmConditionFilterKey(AlarmConditionKeyType.ATTRIBUTE, "humidityAlarmFlag")); - humidityAlarmFlagAttributeFilter.setValueType(EntityKeyValueType.BOOLEAN); - BooleanFilterPredicate humidityAlarmFlagAttributePredicate = new BooleanFilterPredicate(); - humidityAlarmFlagAttributePredicate.setOperation(BooleanFilterPredicate.BooleanOperation.EQUAL); - humidityAlarmFlagAttributePredicate.setValue(new FilterPredicateValue<>(Boolean.TRUE)); - humidityAlarmFlagAttributeFilter.setPredicate(humidityAlarmFlagAttributePredicate); - - AlarmConditionFilter humidityTimeseriesFilter = new AlarmConditionFilter(); - humidityTimeseriesFilter.setKey(new AlarmConditionFilterKey(AlarmConditionKeyType.TIME_SERIES, "humidity")); - humidityTimeseriesFilter.setValueType(EntityKeyValueType.NUMERIC); - NumericFilterPredicate humidityTimeseriesFilterPredicate = new NumericFilterPredicate(); - humidityTimeseriesFilterPredicate.setOperation(NumericFilterPredicate.NumericOperation.LESS); - FilterPredicateValue humidityTimeseriesPredicateValue = - new FilterPredicateValue<>(60.0, null, - new DynamicValue<>(DynamicValueSourceType.CURRENT_DEVICE, "humidityAlarmThreshold")); - humidityTimeseriesFilterPredicate.setValue(humidityTimeseriesPredicateValue); - humidityTimeseriesFilter.setPredicate(humidityTimeseriesFilterPredicate); - humidityCondition.setCondition(Arrays.asList(humidityAlarmFlagAttributeFilter, humidityTimeseriesFilter)); - - humidityRule.setCondition(humidityCondition); - humidityRule.setAlarmDetails("Current humidity = ${humidity}"); - lowHumidity.setCreateRules(new TreeMap<>(Collections.singletonMap(AlarmSeverity.MINOR, humidityRule))); - - AlarmRule clearHumidityRule = new AlarmRule(); - AlarmCondition clearHumidityCondition = new AlarmCondition(); - clearHumidityCondition.setSpec(new SimpleAlarmConditionSpec()); - - AlarmConditionFilter clearHumidityTimeseriesFilter = new AlarmConditionFilter(); - clearHumidityTimeseriesFilter.setKey(new AlarmConditionFilterKey(AlarmConditionKeyType.TIME_SERIES, "humidity")); - clearHumidityTimeseriesFilter.setValueType(EntityKeyValueType.NUMERIC); - NumericFilterPredicate clearHumidityTimeseriesFilterPredicate = new NumericFilterPredicate(); - clearHumidityTimeseriesFilterPredicate.setOperation(NumericFilterPredicate.NumericOperation.GREATER_OR_EQUAL); - FilterPredicateValue clearHumidityTimeseriesPredicateValue = - new FilterPredicateValue<>(60.0, null, - new DynamicValue<>(DynamicValueSourceType.CURRENT_DEVICE, "humidityAlarmThreshold")); - - clearHumidityTimeseriesFilterPredicate.setValue(clearHumidityTimeseriesPredicateValue); - clearHumidityTimeseriesFilter.setPredicate(clearHumidityTimeseriesFilterPredicate); - clearHumidityCondition.setCondition(Collections.singletonList(clearHumidityTimeseriesFilter)); - clearHumidityRule.setCondition(clearHumidityCondition); - clearHumidityRule.setAlarmDetails("Current humidity = ${humidity}"); - lowHumidity.setClearRule(clearHumidityRule); - - deviceProfileData.setAlarms(Arrays.asList(highTemperature, lowHumidity)); - DeviceProfile savedThermostatDeviceProfile = deviceProfileService.saveDeviceProfile(thermostatDeviceProfile); + createAlarmRules(demoTenant.getId(), savedThermostatDeviceProfile.getId()); DeviceId t1Id = createDevice(demoTenant.getId(), null, savedThermostatDeviceProfile.getId(), "Thermostat T1", "T1_TEST_TOKEN", "Demo device for Thermostats dashboard").getId(); DeviceId t2Id = createDevice(demoTenant.getId(), null, savedThermostatDeviceProfile.getId(), "Thermostat T2", "T2_TEST_TOKEN", "Demo device for Thermostats dashboard").getId(); @@ -526,6 +429,136 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService { installScripts.createDefaultTenantDashboards(demoTenant.getId(), null); } + private void createAlarmRules(TenantId tenantId, DeviceProfileId deviceProfileId) { + CalculatedField highTemperature = new CalculatedField(); + highTemperature.setName("High Temperature"); + highTemperature.setType(CalculatedFieldType.ALARM); + highTemperature.setTenantId(tenantId); + highTemperature.setEntityId(deviceProfileId); + highTemperature.setDebugSettings(DebugSettings.all()); + AlarmCalculatedFieldConfiguration highTemperatureConfig = new AlarmCalculatedFieldConfiguration(); + highTemperature.setConfiguration(highTemperatureConfig); + + Argument temperatureArgument = new Argument(); + temperatureArgument.setRefEntityKey(new ReferencedEntityKey("temperature", ArgumentType.TS_LATEST, null)); + Argument temperatureThresholdArgument = new Argument(); + temperatureThresholdArgument.setRefEntityKey(new ReferencedEntityKey("temperatureAlarmThreshold", ArgumentType.ATTRIBUTE, AttributeScope.SERVER_SCOPE)); + temperatureThresholdArgument.setDefaultValue("25"); + Argument temperatureAlarmFlagArgument = new Argument(); + temperatureAlarmFlagArgument.setRefEntityKey(new ReferencedEntityKey("temperatureAlarmFlag", ArgumentType.ATTRIBUTE, AttributeScope.SERVER_SCOPE)); + highTemperatureConfig.setArguments(Map.of( + "temperature", temperatureArgument, + "temperatureAlarmThreshold", temperatureThresholdArgument, + "temperatureAlarmFlag", temperatureAlarmFlagArgument + )); + + AlarmRule temperatureRule = new AlarmRule(); + SimpleAlarmCondition temperatureCondition = new SimpleAlarmCondition(); + + AlarmConditionFilter temperatureAlarmFlagFilter = new AlarmConditionFilter(); + temperatureAlarmFlagFilter.setArgument("temperatureAlarmFlag"); + temperatureAlarmFlagFilter.setValueType(EntityKeyValueType.BOOLEAN); + BooleanFilterPredicate temperatureAlarmFlagAttributePredicate = new BooleanFilterPredicate(); + temperatureAlarmFlagAttributePredicate.setOperation(BooleanFilterPredicate.BooleanOperation.EQUAL); + temperatureAlarmFlagAttributePredicate.setValue(new AlarmConditionValue<>(Boolean.TRUE, null)); + temperatureAlarmFlagFilter.setPredicates(List.of(temperatureAlarmFlagAttributePredicate)); + + AlarmConditionFilter temperatureFilter = new AlarmConditionFilter(); + temperatureFilter.setArgument("temperature"); + temperatureFilter.setValueType(EntityKeyValueType.NUMERIC); + NumericFilterPredicate temperatureFilterPredicate = new NumericFilterPredicate(); + temperatureFilterPredicate.setOperation(NumericFilterPredicate.NumericOperation.GREATER); + temperatureFilterPredicate.setValue(new AlarmConditionValue<>(null, "temperatureAlarmThreshold")); + temperatureFilter.setPredicates(List.of(temperatureFilterPredicate)); + temperatureCondition.setExpression(new SimpleAlarmConditionExpression(List.of(temperatureAlarmFlagFilter, temperatureFilter), ComplexOperation.AND)); + temperatureRule.setCondition(temperatureCondition); + temperatureRule.setAlarmDetails("Current temperature = ${temperature}"); + highTemperatureConfig.setCreateRules(Map.of( + AlarmSeverity.MAJOR, temperatureRule + )); + + AlarmRule clearTemperatureRule = new AlarmRule(); + SimpleAlarmCondition clearTemperatureCondition = new SimpleAlarmCondition(); + + AlarmConditionFilter clearTemperatureFilter = new AlarmConditionFilter(); + clearTemperatureFilter.setArgument("temperature"); + clearTemperatureFilter.setValueType(EntityKeyValueType.NUMERIC); + NumericFilterPredicate clearTemperatureFilterPredicate = new NumericFilterPredicate(); + clearTemperatureFilterPredicate.setOperation(NumericFilterPredicate.NumericOperation.LESS_OR_EQUAL); + clearTemperatureFilterPredicate.setValue(new AlarmConditionValue<>(null, "temperatureAlarmThreshold")); + clearTemperatureFilter.setPredicates(List.of(clearTemperatureFilterPredicate)); + clearTemperatureCondition.setExpression(new SimpleAlarmConditionExpression(List.of(clearTemperatureFilter), ComplexOperation.AND)); + clearTemperatureRule.setCondition(clearTemperatureCondition); + clearTemperatureRule.setAlarmDetails("Current temperature = ${temperature}"); + highTemperatureConfig.setClearRule(clearTemperatureRule); + + calculatedFieldService.save(highTemperature); + + CalculatedField lowHumidity = new CalculatedField(); + lowHumidity.setName("Low Humidity"); + lowHumidity.setType(CalculatedFieldType.ALARM); + lowHumidity.setTenantId(tenantId); + lowHumidity.setEntityId(deviceProfileId); + lowHumidity.setDebugSettings(DebugSettings.all()); + AlarmCalculatedFieldConfiguration lowHumidityConfig = new AlarmCalculatedFieldConfiguration(); + lowHumidity.setConfiguration(lowHumidityConfig); + + Argument humidityArgument = new Argument(); + humidityArgument.setRefEntityKey(new ReferencedEntityKey("humidity", ArgumentType.TS_LATEST, null)); + Argument humidityThresholdArgument = new Argument(); + humidityThresholdArgument.setRefEntityKey(new ReferencedEntityKey("humidityAlarmThreshold", ArgumentType.ATTRIBUTE, AttributeScope.SERVER_SCOPE)); + humidityThresholdArgument.setDefaultValue("60"); + Argument humidityAlarmFlagArgument = new Argument(); + humidityAlarmFlagArgument.setRefEntityKey(new ReferencedEntityKey("humidityAlarmFlag", ArgumentType.ATTRIBUTE, AttributeScope.SERVER_SCOPE)); + lowHumidityConfig.setArguments(Map.of( + "humidity", humidityArgument, + "humidityAlarmThreshold", humidityThresholdArgument, + "humidityAlarmFlag", humidityAlarmFlagArgument + )); + + AlarmRule humidityRule = new AlarmRule(); + SimpleAlarmCondition humidityCondition = new SimpleAlarmCondition(); + + AlarmConditionFilter humidityAlarmFlagAttributeFilter = new AlarmConditionFilter(); + humidityAlarmFlagAttributeFilter.setArgument("humidityAlarmFlag"); + humidityAlarmFlagAttributeFilter.setValueType(EntityKeyValueType.BOOLEAN); + BooleanFilterPredicate humidityAlarmFlagPredicate = new BooleanFilterPredicate(); + humidityAlarmFlagPredicate.setOperation(BooleanFilterPredicate.BooleanOperation.EQUAL); + humidityAlarmFlagPredicate.setValue(new AlarmConditionValue<>(Boolean.TRUE, null)); + humidityAlarmFlagAttributeFilter.setPredicates(List.of(humidityAlarmFlagPredicate)); + + AlarmConditionFilter humidityFilter = new AlarmConditionFilter(); + humidityFilter.setArgument("humidity"); + humidityFilter.setValueType(EntityKeyValueType.NUMERIC); + NumericFilterPredicate humidityFilterPredicate = new NumericFilterPredicate(); + humidityFilterPredicate.setOperation(NumericFilterPredicate.NumericOperation.LESS); + humidityFilterPredicate.setValue(new AlarmConditionValue<>(null, "humidityAlarmThreshold")); + humidityFilter.setPredicates(List.of(humidityFilterPredicate)); + humidityCondition.setExpression(new SimpleAlarmConditionExpression(List.of(humidityAlarmFlagAttributeFilter, humidityFilter), ComplexOperation.AND)); + humidityRule.setCondition(humidityCondition); + humidityRule.setAlarmDetails("Current humidity = ${humidity}"); + lowHumidityConfig.setCreateRules(Map.of( + AlarmSeverity.MINOR, humidityRule + )); + + AlarmRule clearHumidityRule = new AlarmRule(); + SimpleAlarmCondition clearHumidityCondition = new SimpleAlarmCondition(); + + AlarmConditionFilter clearHumidityFilter = new AlarmConditionFilter(); + clearHumidityFilter.setArgument("humidity"); + clearHumidityFilter.setValueType(EntityKeyValueType.NUMERIC); + NumericFilterPredicate clearHumidityFilterPredicate = new NumericFilterPredicate(); + clearHumidityFilterPredicate.setOperation(NumericFilterPredicate.NumericOperation.GREATER_OR_EQUAL); + clearHumidityFilterPredicate.setValue(new AlarmConditionValue<>(null, "humidityAlarmThreshold")); + clearHumidityFilter.setPredicates(List.of(clearHumidityFilterPredicate)); + clearHumidityCondition.setExpression(new SimpleAlarmConditionExpression(List.of(clearHumidityFilter), ComplexOperation.AND)); + clearHumidityRule.setCondition(clearHumidityCondition); + clearHumidityRule.setAlarmDetails("Current humidity = ${humidity}"); + lowHumidityConfig.setClearRule(clearHumidityRule); + + calculatedFieldService.save(lowHumidity); + } + @Override public void loadSystemWidgets() throws Exception { installScripts.loadSystemWidgets(); @@ -609,6 +642,7 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService { public void onFailure(Throwable t) { log.warn("[{}] Failed to update attribute [{}] with value [{}]", deviceId, key, value, t); } + } private void addTsCallback(ListenableFuture saveFuture, final FutureCallback callback) { diff --git a/application/src/main/java/org/thingsboard/server/service/install/InstallScripts.java b/application/src/main/java/org/thingsboard/server/service/install/InstallScripts.java index 4e44517ca0..f9b5c282a8 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/InstallScripts.java +++ b/application/src/main/java/org/thingsboard/server/service/install/InstallScripts.java @@ -65,9 +65,6 @@ import java.util.stream.Stream; import static org.thingsboard.server.utils.LwM2mObjectModelUtils.toLwm2mResource; -/** - * Created by ashvayka on 18.04.18. - */ @Component @Slf4j public class InstallScripts { @@ -134,6 +131,10 @@ public class InstallScripts { return Paths.get(getDataDir(), JSON_DIR, EDGE_DIR, RULE_CHAINS_DIR); } + public Path getWidgetTypesDir() { + return Paths.get(getDataDir(), JSON_DIR, SYSTEM_DIR, WIDGET_TYPES_DIR); + } + public String getDataDir() { if (!StringUtils.isEmpty(dataDir)) { if (!Paths.get(this.dataDir).toFile().isDirectory()) { @@ -237,7 +238,7 @@ public class InstallScripts { } ); } - Path widgetTypesDir = Paths.get(getDataDir(), JSON_DIR, SYSTEM_DIR, WIDGET_TYPES_DIR); + Path widgetTypesDir = getWidgetTypesDir(); if (Files.exists(widgetTypesDir)) { try (Stream dirStream = listDir(widgetTypesDir).filter(path -> path.toString().endsWith(JSON_EXT))) { dirStream.forEach( diff --git a/application/src/main/java/org/thingsboard/server/service/install/ProjectInfo.java b/application/src/main/java/org/thingsboard/server/service/install/ProjectInfo.java index 4422d952a5..8b7218a981 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/ProjectInfo.java +++ b/application/src/main/java/org/thingsboard/server/service/install/ProjectInfo.java @@ -19,14 +19,17 @@ import lombok.RequiredArgsConstructor; import org.springframework.boot.info.BuildProperties; import org.springframework.stereotype.Component; +import java.util.Optional; + @Component @RequiredArgsConstructor public class ProjectInfo { - private final BuildProperties buildProperties; + private final Optional buildProperties; public String getProjectVersion() { - return buildProperties.getVersion().replaceAll("[^\\d.]", ""); + return buildProperties.orElseThrow(() -> new IllegalStateException("Build properties are missing. Please rebuild the project with maven")) + .getVersion().replaceAll("[^\\d.]", ""); } public String getProductType() { diff --git a/application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java b/application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java index 972d5ff36c..7ef691dc6d 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java @@ -15,8 +15,6 @@ */ package org.thingsboard.server.service.install.update; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.node.ObjectNode; import com.google.common.collect.Lists; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; @@ -24,12 +22,9 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.context.annotation.Profile; import org.springframework.stereotype.Service; -import org.thingsboard.server.common.data.alarm.AlarmSeverity; import org.thingsboard.server.common.data.id.RuleNodeId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageDataIterable; -import org.thingsboard.server.common.data.query.DynamicValue; -import org.thingsboard.server.common.data.query.FilterPredicateValue; import org.thingsboard.server.dao.rule.RuleChainService; import org.thingsboard.server.service.component.ComponentDiscoveryService; import org.thingsboard.server.service.component.RuleNodeClassInfo; @@ -129,60 +124,6 @@ public class DefaultDataUpdateService implements DataUpdateService { return ruleNodeIds; } - boolean convertDeviceProfileForVersion330(JsonNode profileData) { - boolean isUpdated = false; - if (profileData.has("alarms") && !profileData.get("alarms").isNull()) { - JsonNode alarms = profileData.get("alarms"); - for (JsonNode alarm : alarms) { - if (alarm.has("createRules")) { - JsonNode createRules = alarm.get("createRules"); - for (AlarmSeverity severity : AlarmSeverity.values()) { - if (createRules.has(severity.name())) { - JsonNode spec = createRules.get(severity.name()).get("condition").get("spec"); - if (convertDeviceProfileAlarmRulesForVersion330(spec)) { - isUpdated = true; - } - } - } - } - if (alarm.has("clearRule") && !alarm.get("clearRule").isNull()) { - JsonNode spec = alarm.get("clearRule").get("condition").get("spec"); - if (convertDeviceProfileAlarmRulesForVersion330(spec)) { - isUpdated = true; - } - } - } - } - return isUpdated; - } - - boolean convertDeviceProfileAlarmRulesForVersion330(JsonNode spec) { - if (spec != null) { - if (spec.has("type") && spec.get("type").asText().equals("DURATION")) { - if (spec.has("value")) { - long value = spec.get("value").asLong(); - var predicate = new FilterPredicateValue<>( - value, null, new DynamicValue<>(null, null, false) - ); - ((ObjectNode) spec).remove("value"); - ((ObjectNode) spec).putPOJO("predicate", predicate); - return true; - } - } else if (spec.has("type") && spec.get("type").asText().equals("REPEATING")) { - if (spec.has("count")) { - int count = spec.get("count").asInt(); - var predicate = new FilterPredicateValue<>( - count, null, new DynamicValue<>(null, null, false) - ); - ((ObjectNode) spec).remove("count"); - ((ObjectNode) spec).putPOJO("predicate", predicate); - return true; - } - } - } - return false; - } - public static boolean getEnv(String name, boolean defaultValue) { String env = System.getenv(name); if (env == null) { diff --git a/application/src/main/java/org/thingsboard/server/service/notification/provider/DefaultFirebaseService.java b/application/src/main/java/org/thingsboard/server/service/notification/provider/DefaultFirebaseService.java index d1307fcd5d..8056b9fde2 100644 --- a/application/src/main/java/org/thingsboard/server/service/notification/provider/DefaultFirebaseService.java +++ b/application/src/main/java/org/thingsboard/server/service/notification/provider/DefaultFirebaseService.java @@ -140,8 +140,10 @@ public class DefaultFirebaseService implements FirebaseService { } public void destroy() { - app.delete(); - app = null; + if (app != null) { + app.delete(); + app = null; + } messaging = null; log.debug("[{}] Destroyed FirebaseContext", key); } diff --git a/application/src/main/java/org/thingsboard/server/service/notification/provider/DefaultSlackService.java b/application/src/main/java/org/thingsboard/server/service/notification/provider/DefaultSlackService.java index 85f0642c55..ec835d152f 100644 --- a/application/src/main/java/org/thingsboard/server/service/notification/provider/DefaultSlackService.java +++ b/application/src/main/java/org/thingsboard/server/service/notification/provider/DefaultSlackService.java @@ -15,20 +15,25 @@ */ package org.thingsboard.server.service.notification.provider; +import com.fasterxml.jackson.databind.node.ObjectNode; import com.github.benmanes.caffeine.cache.Cache; import com.github.benmanes.caffeine.cache.Caffeine; import com.slack.api.Slack; import com.slack.api.methods.MethodsClient; import com.slack.api.methods.SlackApiRequest; import com.slack.api.methods.SlackApiTextResponse; +import com.slack.api.methods.SlackFilesUploadV2Exception; import com.slack.api.methods.request.chat.ChatPostMessageRequest; import com.slack.api.methods.request.conversations.ConversationsListRequest; +import com.slack.api.methods.request.conversations.ConversationsOpenRequest; +import com.slack.api.methods.request.files.FilesUploadV2Request; import com.slack.api.methods.request.users.UsersListRequest; import com.slack.api.methods.response.conversations.ConversationsListResponse; import com.slack.api.methods.response.users.UsersListResponse; import com.slack.api.model.ConversationType; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; +import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.rule.engine.api.notification.SlackService; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod; @@ -36,6 +41,8 @@ import org.thingsboard.server.common.data.notification.settings.NotificationSett import org.thingsboard.server.common.data.notification.settings.SlackNotificationDeliveryMethodConfig; import org.thingsboard.server.common.data.notification.targets.slack.SlackConversation; import org.thingsboard.server.common.data.notification.targets.slack.SlackConversationType; +import org.thingsboard.server.common.data.notification.targets.slack.SlackFile; +import org.thingsboard.server.common.data.util.CollectionsUtil; import org.thingsboard.server.common.data.util.ThrowingBiFunction; import org.thingsboard.server.dao.notification.NotificationSettingsService; @@ -58,11 +65,40 @@ public class DefaultSlackService implements SlackService { @Override public void sendMessage(TenantId tenantId, String token, String conversationId, String message) { - ChatPostMessageRequest request = ChatPostMessageRequest.builder() - .channel(conversationId) - .text(message) - .build(); - sendRequest(token, request, MethodsClient::chatPostMessage); + sendMessage(tenantId, token, conversationId, message, null); + } + + @Override + public void sendMessage(TenantId tenantId, String token, String conversationId, String message, List files) { + if (CollectionsUtil.isNotEmpty(files)) { + if (conversationId.startsWith("U")) { // direct message + /* + * files.uploadV2 requires an existing channel ID, while chat.postMessage auto‑opens DMs + * */ + conversationId = sendRequest(token, ConversationsOpenRequest.builder() + .users(List.of(conversationId)) + .build(), MethodsClient::conversationsOpen).getChannel().getId(); + } + + FilesUploadV2Request request = FilesUploadV2Request.builder() + .initialComment(message) + .channel(conversationId) + .uploadFiles(files.stream() + .map(file -> FilesUploadV2Request.UploadFile.builder() + .filename(file.getName()) + .title(file.getName()) + .fileData(file.getData()) + .build()) + .toList()) + .build(); + sendRequest(token, request, MethodsClient::filesUploadV2); + } else { + ChatPostMessageRequest request = ChatPostMessageRequest.builder() + .channel(conversationId) + .text(message) + .build(); + sendRequest(token, request, MethodsClient::chatPostMessage); + } } @Override @@ -128,22 +164,52 @@ public class DefaultSlackService implements SlackService { R response; try { response = method.apply(client, request); + } catch (SlackFilesUploadV2Exception e) { + if (e.getGetURLResponses() != null) { + e.getGetURLResponses().forEach(this::checkResponse); + } + if (e.getCompleteResponse() != null) { + checkResponse(e.getCompleteResponse()); + } + if (e.getFileInfoResponses() != null) { + e.getFileInfoResponses().forEach(this::checkResponse); + } + throw new RuntimeException("Failed to upload Slack file: " + e.toString(), e); } catch (Exception e) { throw new RuntimeException(e.getMessage(), e); } - if (!response.isOk()) { - String error = response.getError(); - if (error == null) { - error = "unknown error"; - } else if (error.contains("missing_scope")) { - String neededScope = response.getNeeded(); - error = "bot token scope '" + neededScope + "' is needed"; - } - throw new RuntimeException("Slack API error: " + error); + checkResponse(response); + return response; + } + + + private void checkResponse(SlackApiTextResponse response) { + if (response.isOk()) { + return; } - return response; + String error = response.getError(); + if (error != null) { + switch (error) { + case "missing_scope" -> { + String neededScope = response.getNeeded(); + error = "bot token scope '" + neededScope + "' is needed"; + } + case "not_in_channel" -> { + error = "app needs to be added to the channel"; + } + default -> { + error = null; + } + } + } + if (error == null) { + ObjectNode responseJson = (ObjectNode) JacksonUtil.valueToTree(response); + responseJson.remove("httpResponseHeaders"); + error = responseJson.toString(); + } + throw new RuntimeException("Slack API error: " + error); } } diff --git a/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/AlarmAssignmentTriggerProcessor.java b/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/AlarmAssignmentTriggerProcessor.java index ca3667b6ef..f9646409c6 100644 --- a/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/AlarmAssignmentTriggerProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/AlarmAssignmentTriggerProcessor.java @@ -62,6 +62,7 @@ public class AlarmAssignmentTriggerProcessor implements NotificationRuleTriggerP .alarmType(alarmInfo.getType()) .alarmOriginator(alarmInfo.getOriginator()) .alarmOriginatorName(alarmInfo.getOriginatorName()) + .alarmOriginatorLabel(alarmInfo.getOriginatorLabel()) .alarmSeverity(alarmInfo.getSeverity()) .alarmStatus(alarmInfo.getStatus()) .alarmCustomerId(alarmInfo.getCustomerId()) diff --git a/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/AlarmCommentTriggerProcessor.java b/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/AlarmCommentTriggerProcessor.java index 8cca98f7fd..a1df9b3e58 100644 --- a/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/AlarmCommentTriggerProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/AlarmCommentTriggerProcessor.java @@ -22,6 +22,7 @@ import org.thingsboard.server.common.data.alarm.AlarmCommentType; import org.thingsboard.server.common.data.alarm.AlarmInfo; import org.thingsboard.server.common.data.alarm.AlarmStatusFilter; import org.thingsboard.server.common.data.audit.ActionType; +import org.thingsboard.server.common.data.id.NameLabelAndCustomerDetails; import org.thingsboard.server.common.data.notification.info.AlarmCommentNotificationInfo; import org.thingsboard.server.common.data.notification.info.RuleOriginatedNotificationInfo; import org.thingsboard.server.common.data.notification.rule.trigger.AlarmCommentTrigger; @@ -57,11 +58,14 @@ public class AlarmCommentTriggerProcessor implements NotificationRuleTriggerProc @Override public RuleOriginatedNotificationInfo constructNotificationInfo(AlarmCommentTrigger trigger) { Alarm alarm = trigger.getAlarm(); - String originatorName; + String originatorName, originatorLabel; if (alarm instanceof AlarmInfo) { originatorName = ((AlarmInfo) alarm).getOriginatorName(); + originatorLabel = ((AlarmInfo) alarm).getOriginatorLabel(); } else { - originatorName = entityService.fetchEntityName(trigger.getTenantId(), alarm.getOriginator()).orElse(""); + var infoOpt = entityService.fetchNameLabelAndCustomerDetails(trigger.getTenantId(), alarm.getOriginator()); + originatorName = infoOpt.map(NameLabelAndCustomerDetails::getName).orElse(null); + originatorLabel = infoOpt.map(NameLabelAndCustomerDetails::getLabel).orElse(null); } return AlarmCommentNotificationInfo.builder() .comment(trigger.getComment().getComment().get("text").asText()) @@ -73,6 +77,7 @@ public class AlarmCommentTriggerProcessor implements NotificationRuleTriggerProc .alarmType(alarm.getType()) .alarmOriginator(alarm.getOriginator()) .alarmOriginatorName(originatorName) + .alarmOriginatorLabel(originatorLabel) .alarmSeverity(alarm.getSeverity()) .alarmStatus(alarm.getStatus()) .alarmCustomerId(alarm.getCustomerId()) diff --git a/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/AlarmTriggerProcessor.java b/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/AlarmTriggerProcessor.java index 7d6959dc4f..5d4be59b97 100644 --- a/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/AlarmTriggerProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/AlarmTriggerProcessor.java @@ -15,7 +15,9 @@ */ package org.thingsboard.server.service.notification.rule.trigger; +import com.fasterxml.jackson.databind.JsonNode; import org.springframework.stereotype.Service; +import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.alarm.AlarmApiCallResult; import org.thingsboard.server.common.data.alarm.AlarmInfo; @@ -28,6 +30,9 @@ import org.thingsboard.server.common.data.notification.rule.trigger.config.Alarm import org.thingsboard.server.common.data.notification.rule.trigger.config.AlarmNotificationRuleTriggerConfig.ClearRule; import org.thingsboard.server.common.data.notification.rule.trigger.config.NotificationRuleTriggerType; +import java.util.HashMap; +import java.util.Map; + import static org.apache.commons.collections4.CollectionUtils.isNotEmpty; import static org.thingsboard.server.common.data.util.CollectionsUtil.emptyOrContains; @@ -106,15 +111,27 @@ public class AlarmTriggerProcessor implements NotificationRuleTriggerProcessor toDetailsTemplateMap(JsonNode details) { + Map infoMap = JacksonUtil.toFlatMap(details); + Map result = new HashMap<>(); + for (Map.Entry entry : infoMap.entrySet()) { + String key = "details." + entry.getKey(); + result.put(key, entry.getValue()); + } + return result; + } + @Override public NotificationRuleTriggerType getTriggerType() { return NotificationRuleTriggerType.ALARM; diff --git a/application/src/main/java/org/thingsboard/server/service/ota/DefaultOtaPackageStateService.java b/application/src/main/java/org/thingsboard/server/service/ota/DefaultOtaPackageStateService.java index 4d9e145711..0b4d3bec6f 100644 --- a/application/src/main/java/org/thingsboard/server/service/ota/DefaultOtaPackageStateService.java +++ b/application/src/main/java/org/thingsboard/server/service/ota/DefaultOtaPackageStateService.java @@ -328,7 +328,7 @@ public class DefaultOtaPackageStateService implements OtaPackageStateService { attributes.add(new BaseAttributeKvEntry(ts, new LongDataEntry(getAttributeKey(otaPackageType, SIZE), otaPackage.getDataSize()))); } - if (otaPackage.getChecksumAlgorithm() != null) { + if (otaPackage.getChecksumAlgorithm() == null) { attrToRemove.add(getAttributeKey(otaPackageType, CHECKSUM_ALGORITHM)); } else { attributes.add(new BaseAttributeKvEntry(ts, new StringDataEntry(getAttributeKey(otaPackageType, CHECKSUM_ALGORITHM), otaPackage.getChecksumAlgorithm().name()))); diff --git a/application/src/main/java/org/thingsboard/server/service/query/DefaultEntityQueryService.java b/application/src/main/java/org/thingsboard/server/service/query/DefaultEntityQueryService.java index b3fe76f154..f39769526a 100644 --- a/application/src/main/java/org/thingsboard/server/service/query/DefaultEntityQueryService.java +++ b/application/src/main/java/org/thingsboard/server/service/query/DefaultEntityQueryService.java @@ -57,6 +57,7 @@ import org.thingsboard.server.dao.alarm.AlarmService; import org.thingsboard.server.dao.attributes.AttributesService; import org.thingsboard.server.dao.entity.EntityService; import org.thingsboard.server.dao.model.ModelConstants; +import org.thingsboard.server.dao.sql.query.EntityKeyMapping; import org.thingsboard.server.dao.timeseries.TimeseriesService; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.executors.DbCallbackExecutorService; @@ -224,7 +225,7 @@ public class DefaultEntityQueryService implements EntityQueryService { private EntityDataQuery buildEntityDataQuery(AlarmCountQuery query) { EntityDataPageLink edpl = new EntityDataPageLink(maxEntitiesPerAlarmSubscription, 0, null, - new EntityDataSortOrder(new EntityKey(EntityKeyType.ENTITY_FIELD, ModelConstants.CREATED_TIME_PROPERTY))); + new EntityDataSortOrder(new EntityKey(EntityKeyType.ENTITY_FIELD, EntityKeyMapping.CREATED_TIME))); return new EntityDataQuery(query.getEntityFilter(), edpl, null, null, query.getKeyFilters()); } @@ -232,12 +233,12 @@ public class DefaultEntityQueryService implements EntityQueryService { EntityDataSortOrder sortOrder = query.getPageLink().getSortOrder(); EntityDataSortOrder entitiesSortOrder; if (sortOrder == null || sortOrder.getKey().getType().equals(EntityKeyType.ALARM_FIELD)) { - entitiesSortOrder = new EntityDataSortOrder(new EntityKey(EntityKeyType.ENTITY_FIELD, ModelConstants.CREATED_TIME_PROPERTY)); + entitiesSortOrder = new EntityDataSortOrder(new EntityKey(EntityKeyType.ENTITY_FIELD, EntityKeyMapping.CREATED_TIME)); } else { entitiesSortOrder = sortOrder; } EntityDataPageLink edpl = new EntityDataPageLink(maxEntitiesPerAlarmSubscription, 0, null, entitiesSortOrder); - return new EntityDataQuery(query.getEntityFilter(), edpl, null, null, query.getKeyFilters()); + return new EntityDataQuery(query.getEntityFilter(), edpl, query.getEntityFields(), query.getLatestValues(), query.getKeyFilters()); } @Override diff --git a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCalculatedFieldConsumerService.java b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCalculatedFieldConsumerService.java index 8d4ab25578..0449d116c8 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCalculatedFieldConsumerService.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCalculatedFieldConsumerService.java @@ -22,6 +22,7 @@ import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.event.EventListener; import org.springframework.stereotype.Service; import org.thingsboard.server.actors.ActorSystemContext; +import org.thingsboard.server.actors.calculatedField.CalculatedFieldEntityActionEventMsg; import org.thingsboard.server.actors.calculatedField.CalculatedFieldLinkedTelemetryMsg; import org.thingsboard.server.actors.calculatedField.CalculatedFieldTelemetryMsg; import org.thingsboard.server.common.data.DataConstants; @@ -35,6 +36,7 @@ import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.TbCallback; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; +import org.thingsboard.server.dao.resource.TbResourceDataCache; import org.thingsboard.server.dao.tenant.TbTenantProfileCache; import org.thingsboard.server.gen.transport.TransportProtos.CalculatedFieldLinkedTelemetryMsgProto; import org.thingsboard.server.gen.transport.TransportProtos.CalculatedFieldTelemetryMsgProto; @@ -83,6 +85,7 @@ public class DefaultTbCalculatedFieldConsumerService extends AbstractPartitionBa ActorSystemContext actorContext, TbDeviceProfileCache deviceProfileCache, TbAssetProfileCache assetProfileCache, + TbResourceDataCache tbResourceDataCache, TbTenantProfileCache tenantProfileCache, TbApiUsageStateService apiUsageStateService, PartitionService partitionService, @@ -90,7 +93,7 @@ public class DefaultTbCalculatedFieldConsumerService extends AbstractPartitionBa JwtSettingsService jwtSettingsService, CalculatedFieldCache calculatedFieldCache, CalculatedFieldStateService stateService) { - super(actorContext, tenantProfileCache, deviceProfileCache, assetProfileCache, calculatedFieldCache, apiUsageStateService, partitionService, + super(actorContext, tenantProfileCache, deviceProfileCache, assetProfileCache, tbResourceDataCache, calculatedFieldCache, apiUsageStateService, partitionService, eventPublisher, jwtSettingsService); this.queueFactory = tbQueueFactory; this.stateService = stateService; @@ -158,12 +161,7 @@ public class DefaultTbCalculatedFieldConsumerService extends AbstractPartitionBa try { ToCalculatedFieldMsg toCfMsg = msg.getValue(); pendingMsgHolder.setMsg(toCfMsg); - if (toCfMsg.hasTelemetryMsg()) { - log.trace("[{}] Forwarding regular telemetry message for processing {}", id, toCfMsg.getTelemetryMsg()); - forwardToActorSystem(toCfMsg.getTelemetryMsg(), callback); - } else if (toCfMsg.hasLinkedTelemetryMsg()) { - forwardToActorSystem(toCfMsg.getLinkedTelemetryMsg(), callback); - } + processMsg(toCfMsg, id, callback); } catch (Throwable e) { log.warn("[{}] Failed to process message: {}", id, msg, e); callback.onFailure(e); @@ -181,6 +179,17 @@ public class DefaultTbCalculatedFieldConsumerService extends AbstractPartitionBa consumer.commit(); } + private void processMsg(ToCalculatedFieldMsg toCfMsg, UUID id, TbCallback callback) { + if (toCfMsg.hasTelemetryMsg()) { + log.trace("[{}] Forwarding regular telemetry message for processing {}", id, toCfMsg.getTelemetryMsg()); + forwardToActorSystem(toCfMsg.getTelemetryMsg(), callback); + } else if (toCfMsg.hasLinkedTelemetryMsg()) { + forwardToActorSystem(toCfMsg.getLinkedTelemetryMsg(), callback); + } else if (toCfMsg.hasEventMsg()) { + actorContext.tell(CalculatedFieldEntityActionEventMsg.fromProto(toCfMsg.getEventMsg(), callback)); + } + } + @Override protected ServiceType getServiceType() { return ServiceType.TB_RULE_ENGINE; diff --git a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java index 265f14c4e2..b54a7fb83a 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java @@ -26,6 +26,7 @@ import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.cache.TbTransactionalCache; import org.thingsboard.server.cluster.TbClusterService; import org.thingsboard.server.common.data.ApiUsageState; +import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.DeviceProfile; @@ -48,12 +49,14 @@ import org.thingsboard.server.common.data.id.DeviceProfileId; import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.RuleChainId; +import org.thingsboard.server.common.data.id.TbResourceId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; import org.thingsboard.server.common.data.queue.Queue; +import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.ToDeviceActorNotificationMsg; import org.thingsboard.server.common.msg.edge.EdgeEventUpdateMsg; @@ -435,8 +438,9 @@ public class DefaultTbClusterService implements TbClusterService { @Override public void onResourceChange(TbResourceInfo resource, TbQueueCallback callback) { + TenantId tenantId = resource.getTenantId(); + TbResourceId resourceId = resource.getId(); if (resource.getResourceType() == ResourceType.LWM2M_MODEL) { - TenantId tenantId = resource.getTenantId(); log.trace("[{}][{}][{}] Processing change resource", tenantId, resource.getResourceType(), resource.getResourceKey()); ResourceUpdateMsg resourceUpdateMsg = ResourceUpdateMsg.newBuilder() .setTenantIdMSB(tenantId.getId().getMostSignificantBits()) @@ -447,6 +451,7 @@ public class DefaultTbClusterService implements TbClusterService { ToTransportMsg transportMsg = ToTransportMsg.newBuilder().setResourceUpdateMsg(resourceUpdateMsg).build(); broadcast(transportMsg, DataConstants.LWM2M_TRANSPORT_NAME, callback); } + broadcastEntityStateChangeEvent(tenantId, resourceId, ComponentLifecycleEvent.UPDATED); } @Override @@ -462,6 +467,18 @@ public class DefaultTbClusterService implements TbClusterService { ToTransportMsg transportMsg = ToTransportMsg.newBuilder().setResourceDeleteMsg(resourceDeleteMsg).build(); broadcast(transportMsg, DataConstants.LWM2M_TRANSPORT_NAME, callback); } + broadcastEntityStateChangeEvent(resource.getTenantId(), resource.getId(), ComponentLifecycleEvent.DELETED); + } + + @Override + public void onCustomerUpdated(Customer customer, Customer oldCustomer) { + ComponentLifecycleMsg msg = ComponentLifecycleMsg.builder() + .tenantId(customer.getTenantId()) + .entityId(customer.getId()) + .event(oldCustomer == null ? ComponentLifecycleEvent.CREATED : ComponentLifecycleEvent.UPDATED) + .ownerChanged(false) // for compatibility with PE + .build(); + broadcast(msg); } private void broadcastEntityChangeToTransport(TenantId tenantId, EntityId entityid, T entity, TbQueueCallback callback) { @@ -592,7 +609,9 @@ public class DefaultTbClusterService implements TbClusterService { EntityType.TENANT_PROFILE, EntityType.DEVICE_PROFILE, EntityType.ASSET_PROFILE, - EntityType.JOB) + EntityType.JOB, + EntityType.TB_RESOURCE, + EntityType.CUSTOMER) || (entityType == EntityType.ASSET && msg.getEvent() == ComponentLifecycleEvent.UPDATED) || (entityType == EntityType.DEVICE && msg.getEvent() == ComponentLifecycleEvent.UPDATED) ) { @@ -668,6 +687,7 @@ public class DefaultTbClusterService implements TbClusterService { } msg.event(ComponentLifecycleEvent.UPDATED) .oldProfileId(old.getDeviceProfileId()) + .ownerChanged(!entity.getOwnerId().equals(old.getOwnerId())) .oldName(old.getName()); } broadcast(msg.build()); @@ -688,6 +708,7 @@ public class DefaultTbClusterService implements TbClusterService { } else { msg.event(ComponentLifecycleEvent.UPDATED) .oldProfileId(old.getAssetProfileId()) + .ownerChanged(!entity.getOwnerId().equals(old.getOwnerId())) .oldName(old.getName()); } broadcast(msg.build()); @@ -703,6 +724,28 @@ public class DefaultTbClusterService implements TbClusterService { broadcastEntityStateChangeEvent(calculatedField.getTenantId(), calculatedField.getId(), ComponentLifecycleEvent.DELETED); } + @Override + public void onRelationUpdated(TenantId tenantId, EntityRelation entityRelation, TbQueueCallback callback) { + ComponentLifecycleMsg msg = ComponentLifecycleMsg.builder() + .tenantId(tenantId) + .entityId(entityRelation.getFrom()) + .event(ComponentLifecycleEvent.RELATION_UPDATED) + .info(JacksonUtil.valueToTree(entityRelation)) + .build(); + broadcast(msg); + } + + @Override + public void onRelationDeleted(TenantId tenantId, EntityRelation entityRelation, TbQueueCallback callback) { + ComponentLifecycleMsg msg = ComponentLifecycleMsg.builder() + .tenantId(tenantId) + .entityId(entityRelation.getFrom()) + .event(ComponentLifecycleEvent.RELATION_DELETED) + .info(JacksonUtil.valueToTree(entityRelation)) + .build(); + broadcast(msg); + } + @Override public void sendNotificationMsgToEdge(TenantId tenantId, EdgeId edgeId, EntityId entityId, String body, EdgeEventType type, EdgeEventActionType action, EdgeId originatorEdgeId) { if (!edgesEnabled) { diff --git a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java index f0b1a4d7d2..9ab5a062eb 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java @@ -55,6 +55,7 @@ import org.thingsboard.server.common.stats.StatsFactory; import org.thingsboard.server.common.util.KvProtoUtil; import org.thingsboard.server.common.util.ProtoUtils; import org.thingsboard.server.dao.resource.ImageCacheKey; +import org.thingsboard.server.dao.resource.TbResourceDataCache; import org.thingsboard.server.dao.tenant.TbTenantProfileCache; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos.DeviceStateServiceMsgProto; @@ -176,10 +177,11 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService> msgs, TbQueueConsumer> consumer, - Object consumerKey, + ConsumerKey consumerKey, Queue queue) throws Exception { TbRuleEngineSubmitStrategy submitStrategy = getSubmitStrategy(queue); TbRuleEngineProcessingStrategy ackStrategy = getProcessingStrategy(queue); diff --git a/application/src/main/java/org/thingsboard/server/service/resource/DefaultTbResourceService.java b/application/src/main/java/org/thingsboard/server/service/resource/DefaultTbResourceService.java index 634190c1d7..28ec9af4b2 100644 --- a/application/src/main/java/org/thingsboard/server/service/resource/DefaultTbResourceService.java +++ b/application/src/main/java/org/thingsboard/server/service/resource/DefaultTbResourceService.java @@ -102,7 +102,6 @@ public class DefaultTbResourceService extends AbstractTbEntityService implements if (result.isSuccess()) { logEntityActionService.logEntityAction(tenantId, resourceId, tbResource, actionType, user, resourceId.toString()); } - return result; } catch (Exception e) { logEntityActionService.logEntityAction(tenantId, emptyId(EntityType.TB_RESOURCE), diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/AuthExceptionHandler.java b/application/src/main/java/org/thingsboard/server/service/security/auth/AuthExceptionHandler.java index 27ed2996d1..ff39038453 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/AuthExceptionHandler.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/AuthExceptionHandler.java @@ -18,8 +18,10 @@ package org.thingsboard.server.service.security.auth; import jakarta.servlet.FilterChain; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; +import lombok.Getter; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; import org.springframework.security.core.AuthenticationException; import org.springframework.stereotype.Component; import org.springframework.web.filter.OncePerRequestFilter; @@ -32,6 +34,10 @@ public class AuthExceptionHandler extends OncePerRequestFilter { private final ThingsboardErrorResponseHandler errorResponseHandler; + @Value("${server.log_controller_error_stack_trace}") + @Getter + private boolean logControllerErrorStackTrace; + @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) { try { @@ -39,8 +45,15 @@ public class AuthExceptionHandler extends OncePerRequestFilter { } catch (AuthenticationException e) { throw e; } catch (Exception e) { + log(e); errorResponseHandler.handle(e, response); } } + private void log(Exception e) { + if (logControllerErrorStackTrace) { + log.error("Auth error", e); + } + } + } diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/MfaConfigurationToken.java b/application/src/main/java/org/thingsboard/server/service/security/auth/MfaConfigurationToken.java new file mode 100644 index 0000000000..b52404bac2 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/MfaConfigurationToken.java @@ -0,0 +1,24 @@ +/** + * Copyright © 2016-2025 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.security.auth; + +import org.thingsboard.server.service.security.model.SecurityUser; + +public class MfaConfigurationToken extends AbstractJwtAuthenticationToken { + public MfaConfigurationToken(SecurityUser securityUser) { + super(securityUser); + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/DefaultTwoFactorAuthService.java b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/DefaultTwoFactorAuthService.java index 0bf314a145..325203314f 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/DefaultTwoFactorAuthService.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/DefaultTwoFactorAuthService.java @@ -28,6 +28,7 @@ import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.limit.LimitedApi; +import org.thingsboard.server.common.data.notification.targets.platform.SystemLevelUsersFilter; import org.thingsboard.server.common.data.security.model.mfa.PlatformTwoFaSettings; import org.thingsboard.server.common.data.security.model.mfa.account.TwoFaAccountConfig; import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFaProviderConfig; @@ -61,12 +62,25 @@ public class DefaultTwoFactorAuthService implements TwoFactorAuthService { private static final ThingsboardException TOO_MANY_REQUESTS_ERROR = new ThingsboardException("Too many requests", ThingsboardErrorCode.TOO_MANY_REQUESTS); @Override - public boolean isTwoFaEnabled(TenantId tenantId, UserId userId) { - return configManager.getAccountTwoFaSettings(tenantId, userId) + public boolean isTwoFaEnabled(TenantId tenantId, User user) { + return configManager.getAccountTwoFaSettings(tenantId, user) .map(settings -> !settings.getConfigs().isEmpty()) .orElse(false); } + @Override + public boolean isEnforceTwoFaEnabled(TenantId tenantId, User user) { + SystemLevelUsersFilter enforcedUsersFilter = configManager.getPlatformTwoFaSettings(TenantId.SYS_TENANT_ID, true) + .filter(PlatformTwoFaSettings::isEnforceTwoFa) + .map(PlatformTwoFaSettings::getEnforcedUsersFilter) + .orElse(null); + if (enforcedUsersFilter == null) { + return false; + } + + return userService.matchesFilter(tenantId, enforcedUsersFilter, user); + } + @Override public void checkProvider(TenantId tenantId, TwoFaProviderType providerType) throws ThingsboardException { getTwoFaProvider(providerType).check(tenantId); @@ -75,7 +89,7 @@ public class DefaultTwoFactorAuthService implements TwoFactorAuthService { @Override public void prepareVerificationCode(SecurityUser user, TwoFaProviderType providerType, boolean checkLimits) throws Exception { - TwoFaAccountConfig accountConfig = configManager.getTwoFaAccountConfig(user.getTenantId(), user.getId(), providerType) + TwoFaAccountConfig accountConfig = configManager.getTwoFaAccountConfig(user.getTenantId(), user, providerType) .orElseThrow(() -> ACCOUNT_NOT_CONFIGURED_ERROR); prepareVerificationCode(user, accountConfig, checkLimits); } @@ -104,7 +118,7 @@ public class DefaultTwoFactorAuthService implements TwoFactorAuthService { @Override public boolean checkVerificationCode(SecurityUser user, TwoFaProviderType providerType, String verificationCode, boolean checkLimits) throws ThingsboardException { - TwoFaAccountConfig accountConfig = configManager.getTwoFaAccountConfig(user.getTenantId(), user.getId(), providerType) + TwoFaAccountConfig accountConfig = configManager.getTwoFaAccountConfig(user.getTenantId(), user, providerType) .orElseThrow(() -> ACCOUNT_NOT_CONFIGURED_ERROR); return checkVerificationCode(user, verificationCode, accountConfig, checkLimits); } diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/TwoFactorAuthService.java b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/TwoFactorAuthService.java index e85916db2d..62fdb973d2 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/TwoFactorAuthService.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/TwoFactorAuthService.java @@ -18,17 +18,17 @@ package org.thingsboard.server.service.security.auth.mfa; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.security.model.mfa.account.TwoFaAccountConfig; import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFaProviderType; import org.thingsboard.server.service.security.model.SecurityUser; public interface TwoFactorAuthService { - boolean isTwoFaEnabled(TenantId tenantId, UserId userId); + boolean isTwoFaEnabled(TenantId tenantId, User user); - void checkProvider(TenantId tenantId, TwoFaProviderType providerType) throws ThingsboardException; + boolean isEnforceTwoFaEnabled(TenantId tenantId, User user); + void checkProvider(TenantId tenantId, TwoFaProviderType providerType) throws ThingsboardException; void prepareVerificationCode(SecurityUser user, TwoFaProviderType providerType, boolean checkLimits) throws Exception; diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/DefaultTwoFaConfigManager.java b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/DefaultTwoFaConfigManager.java index 60dc9bd6f2..ad04d3e962 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/DefaultTwoFaConfigManager.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/DefaultTwoFaConfigManager.java @@ -21,15 +21,16 @@ import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.AdminSettings; +import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.security.UserAuthSettings; import org.thingsboard.server.common.data.security.model.mfa.PlatformTwoFaSettings; import org.thingsboard.server.common.data.security.model.mfa.account.AccountTwoFaSettings; import org.thingsboard.server.common.data.security.model.mfa.account.TwoFaAccountConfig; import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFaProviderConfig; import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFaProviderType; +import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.service.ConstraintValidator; import org.thingsboard.server.dao.settings.AdminSettingsDao; import org.thingsboard.server.dao.settings.AdminSettingsService; @@ -55,9 +56,9 @@ public class DefaultTwoFaConfigManager implements TwoFaConfigManager { @Override - public Optional getAccountTwoFaSettings(TenantId tenantId, UserId userId) { + public Optional getAccountTwoFaSettings(TenantId tenantId, User user) { PlatformTwoFaSettings platformTwoFaSettings = getPlatformTwoFaSettings(tenantId, true).orElse(null); - return Optional.ofNullable(userAuthSettingsDao.findByUserId(userId)) + return Optional.ofNullable(userAuthSettingsDao.findByUserId(user.getId())) .map(userAuthSettings -> { AccountTwoFaSettings twoFaSettings = userAuthSettings.getTwoFaSettings(); if (twoFaSettings == null) return null; @@ -79,17 +80,22 @@ public class DefaultTwoFaConfigManager implements TwoFaConfigManager { } if (updateNeeded) { - twoFaSettings = saveAccountTwoFaSettings(tenantId, userId, twoFaSettings); + twoFaSettings = saveAccountTwoFaSettings(tenantId, user, twoFaSettings); } return twoFaSettings; }); } - protected AccountTwoFaSettings saveAccountTwoFaSettings(TenantId tenantId, UserId userId, AccountTwoFaSettings settings) { - UserAuthSettings userAuthSettings = Optional.ofNullable(userAuthSettingsDao.findByUserId(userId)) + protected AccountTwoFaSettings saveAccountTwoFaSettings(TenantId tenantId, User user, AccountTwoFaSettings settings) { + if (settings.getConfigs().isEmpty()) { + if (twoFactorAuthService.isEnforceTwoFaEnabled(tenantId, user)) { + throw new DataValidationException("At least one 2FA provider is required"); + } + } + UserAuthSettings userAuthSettings = Optional.ofNullable(userAuthSettingsDao.findByUserId(user.getId())) .orElseGet(() -> { UserAuthSettings newUserAuthSettings = new UserAuthSettings(); - newUserAuthSettings.setUserId(userId); + newUserAuthSettings.setUserId(user.getId()); return newUserAuthSettings; }); userAuthSettings.setTwoFaSettings(settings); @@ -101,18 +107,18 @@ public class DefaultTwoFaConfigManager implements TwoFaConfigManager { @Override - public Optional getTwoFaAccountConfig(TenantId tenantId, UserId userId, TwoFaProviderType providerType) { - return getAccountTwoFaSettings(tenantId, userId) + public Optional getTwoFaAccountConfig(TenantId tenantId, User user, TwoFaProviderType providerType) { + return getAccountTwoFaSettings(tenantId, user) .map(AccountTwoFaSettings::getConfigs) .flatMap(configs -> Optional.ofNullable(configs.get(providerType))); } @Override - public AccountTwoFaSettings saveTwoFaAccountConfig(TenantId tenantId, UserId userId, TwoFaAccountConfig accountConfig) { + public AccountTwoFaSettings saveTwoFaAccountConfig(TenantId tenantId, User user, TwoFaAccountConfig accountConfig) { getTwoFaProviderConfig(tenantId, accountConfig.getProviderType()) .orElseThrow(() -> new IllegalArgumentException("2FA provider is not configured")); - AccountTwoFaSettings settings = getAccountTwoFaSettings(tenantId, userId).orElseGet(() -> { + AccountTwoFaSettings settings = getAccountTwoFaSettings(tenantId, user).orElseGet(() -> { AccountTwoFaSettings newSettings = new AccountTwoFaSettings(); newSettings.setConfigs(new LinkedHashMap<>()); return newSettings; @@ -128,12 +134,12 @@ public class DefaultTwoFaConfigManager implements TwoFaConfigManager { if (configs.values().stream().noneMatch(TwoFaAccountConfig::isUseByDefault)) { configs.values().stream().findFirst().ifPresent(config -> config.setUseByDefault(true)); } - return saveAccountTwoFaSettings(tenantId, userId, settings); + return saveAccountTwoFaSettings(tenantId, user, settings); } @Override - public AccountTwoFaSettings deleteTwoFaAccountConfig(TenantId tenantId, UserId userId, TwoFaProviderType providerType) { - AccountTwoFaSettings settings = getAccountTwoFaSettings(tenantId, userId) + public AccountTwoFaSettings deleteTwoFaAccountConfig(TenantId tenantId, User user, TwoFaProviderType providerType) { + AccountTwoFaSettings settings = getAccountTwoFaSettings(tenantId, user) .orElseThrow(() -> new IllegalArgumentException("2FA not configured")); settings.getConfigs().remove(providerType); if (settings.getConfigs().size() == 1) { @@ -145,7 +151,7 @@ public class DefaultTwoFaConfigManager implements TwoFaConfigManager { .min(Comparator.comparing(TwoFaAccountConfig::getProviderType)) .ifPresent(config -> config.setUseByDefault(true)); } - return saveAccountTwoFaSettings(tenantId, userId, settings); + return saveAccountTwoFaSettings(tenantId, user, settings); } @@ -166,6 +172,19 @@ public class DefaultTwoFaConfigManager implements TwoFaConfigManager { for (TwoFaProviderConfig providerConfig : twoFactorAuthSettings.getProviders()) { twoFactorAuthService.checkProvider(tenantId, providerConfig.getProviderType()); } + if (tenantId.isSysTenantId()) { + if (twoFactorAuthSettings.isEnforceTwoFa()) { + if (twoFactorAuthSettings.getProviders().isEmpty()) { + throw new DataValidationException("At least one 2FA provider is required if enforcing is enabled"); + } + if (twoFactorAuthSettings.getEnforcedUsersFilter() == null) { + throw new DataValidationException("Users filter to enforce 2FA for is required"); + } + } + } else { + twoFactorAuthSettings.setEnforceTwoFa(false); + twoFactorAuthSettings.setEnforcedUsersFilter(null); + } AdminSettings settings = Optional.ofNullable(adminSettingsService.findAdminSettingsByKey(tenantId, TWO_FACTOR_AUTH_SETTINGS_KEY)) .orElseGet(() -> { diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/TwoFaConfigManager.java b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/TwoFaConfigManager.java index 92ac638298..e0ffd6e510 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/TwoFaConfigManager.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/TwoFaConfigManager.java @@ -15,9 +15,9 @@ */ package org.thingsboard.server.service.security.auth.mfa.config; +import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.security.model.mfa.PlatformTwoFaSettings; import org.thingsboard.server.common.data.security.model.mfa.account.AccountTwoFaSettings; import org.thingsboard.server.common.data.security.model.mfa.account.TwoFaAccountConfig; @@ -27,14 +27,14 @@ import java.util.Optional; public interface TwoFaConfigManager { - Optional getAccountTwoFaSettings(TenantId tenantId, UserId userId); + Optional getAccountTwoFaSettings(TenantId tenantId, User user); - Optional getTwoFaAccountConfig(TenantId tenantId, UserId userId, TwoFaProviderType providerType); + Optional getTwoFaAccountConfig(TenantId tenantId, User user, TwoFaProviderType providerType); - AccountTwoFaSettings saveTwoFaAccountConfig(TenantId tenantId, UserId userId, TwoFaAccountConfig accountConfig); + AccountTwoFaSettings saveTwoFaAccountConfig(TenantId tenantId, User user, TwoFaAccountConfig accountConfig); - AccountTwoFaSettings deleteTwoFaAccountConfig(TenantId tenantId, UserId userId, TwoFaProviderType providerType); + AccountTwoFaSettings deleteTwoFaAccountConfig(TenantId tenantId, User user, TwoFaProviderType providerType); Optional getPlatformTwoFaSettings(TenantId tenantId, boolean sysadminSettingsAsDefault); diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/impl/BackupCodeTwoFaProvider.java b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/impl/BackupCodeTwoFaProvider.java index 745b651408..672db5dddd 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/impl/BackupCodeTwoFaProvider.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/impl/BackupCodeTwoFaProvider.java @@ -58,7 +58,7 @@ public class BackupCodeTwoFaProvider implements TwoFaProvider Optional.ofNullable(settings.getTotalAllowedTimeForVerification()) - .filter(time -> time > 0)) - .orElse((int) TimeUnit.MINUTES.toSeconds(30)); - tokenPair.setToken(tokenFactory.createPreVerificationToken(securityUser, preVerificationTokenLifetime).getToken()); - tokenPair.setRefreshToken(null); - tokenPair.setScope(Authority.PRE_VERIFICATION_TOKEN); + tokenPair = createMfaTokenPair(securityUser, Authority.PRE_VERIFICATION_TOKEN); + } else if (authentication instanceof MfaConfigurationToken) { + tokenPair = createMfaTokenPair(securityUser, Authority.MFA_CONFIGURATION_TOKEN); } else { tokenPair = tokenFactory.createTokenPair(securityUser); } @@ -69,6 +66,19 @@ public class RestAwareAuthenticationSuccessHandler implements AuthenticationSucc clearAuthenticationAttributes(request); } + public JwtPair createMfaTokenPair(SecurityUser securityUser, Authority scope) { + log.debug("[{}][{}] Creating {} token", securityUser.getTenantId(), securityUser.getId(), scope); + JwtPair tokenPair = new JwtPair(); + int preVerificationTokenLifetime = twoFaConfigManager.getPlatformTwoFaSettings(securityUser.getTenantId(), true) + .flatMap(settings -> Optional.ofNullable(settings.getTotalAllowedTimeForVerification()) + .filter(time -> time > 0)) + .orElse((int) TimeUnit.MINUTES.toSeconds(30)); + tokenPair.setToken(tokenFactory.createMfaToken(securityUser, scope, preVerificationTokenLifetime).getToken()); + tokenPair.setRefreshToken(null); + tokenPair.setScope(scope); + return tokenPair; + } + /** * Removes temporary authentication-related data which may have been stored * in the session during the authentication process.. diff --git a/application/src/main/java/org/thingsboard/server/service/security/model/token/JwtTokenFactory.java b/application/src/main/java/org/thingsboard/server/service/security/model/token/JwtTokenFactory.java index f9aa6db0f5..e7699e4950 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/model/token/JwtTokenFactory.java +++ b/application/src/main/java/org/thingsboard/server/service/security/model/token/JwtTokenFactory.java @@ -115,13 +115,16 @@ public class JwtTokenFactory { throw new IllegalArgumentException("JWT Token doesn't have any scopes"); } + Authority authority = Authority.parse(scopes.get(0)); + SecurityUser securityUser = new SecurityUser(new UserId(UUID.fromString(claims.get(USER_ID, String.class)))); securityUser.setEmail(subject); - securityUser.setAuthority(Authority.parse(scopes.get(0))); + securityUser.setAuthority(authority); String tenantId = claims.get(TENANT_ID, String.class); + if (tenantId != null) { securityUser.setTenantId(TenantId.fromUUID(UUID.fromString(tenantId))); - } else if (securityUser.getAuthority() == Authority.SYS_ADMIN) { + } else if (authority == Authority.SYS_ADMIN) { securityUser.setTenantId(TenantId.SYS_TENANT_ID); } String customerId = claims.get(CUSTOMER_ID, String.class); @@ -132,18 +135,15 @@ public class JwtTokenFactory { securityUser.setSessionId(claims.get(SESSION_ID, String.class)); } - UserPrincipal principal; - if (securityUser.getAuthority() != Authority.PRE_VERIFICATION_TOKEN) { + boolean isPublic = false; + if (authority != Authority.PRE_VERIFICATION_TOKEN && authority != Authority.MFA_CONFIGURATION_TOKEN) { securityUser.setFirstName(claims.get(FIRST_NAME, String.class)); securityUser.setLastName(claims.get(LAST_NAME, String.class)); securityUser.setEnabled(claims.get(ENABLED, Boolean.class)); - boolean isPublic = claims.get(IS_PUBLIC, Boolean.class); - principal = new UserPrincipal(isPublic ? UserPrincipal.Type.PUBLIC_ID : UserPrincipal.Type.USER_NAME, subject); - } else { - principal = new UserPrincipal(UserPrincipal.Type.USER_NAME, subject); + isPublic = claims.get(IS_PUBLIC, Boolean.class); } + UserPrincipal principal = new UserPrincipal(isPublic ? UserPrincipal.Type.PUBLIC_ID : UserPrincipal.Type.USER_NAME, subject); securityUser.setUserPrincipal(principal); - return securityUser; } @@ -179,8 +179,8 @@ public class JwtTokenFactory { return securityUser; } - public JwtToken createPreVerificationToken(SecurityUser user, Integer expirationTime) { - JwtBuilder jwtBuilder = setUpToken(user, Collections.singletonList(Authority.PRE_VERIFICATION_TOKEN.name()), expirationTime) + public JwtToken createMfaToken(SecurityUser user, Authority scope, Integer expirationTime) { + JwtBuilder jwtBuilder = setUpToken(user, Collections.singletonList(scope.name()), expirationTime) .claim(TENANT_ID, user.getTenantId().toString()); if (user.getCustomerId() != null) { jwtBuilder.claim(CUSTOMER_ID, user.getCustomerId().toString()); diff --git a/application/src/main/java/org/thingsboard/server/service/security/permission/CustomerUserPermissions.java b/application/src/main/java/org/thingsboard/server/service/security/permission/CustomerUserPermissions.java index 8124671cd7..8c71bab9bf 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/permission/CustomerUserPermissions.java +++ b/application/src/main/java/org/thingsboard/server/service/security/permission/CustomerUserPermissions.java @@ -28,7 +28,7 @@ import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.service.security.model.SecurityUser; -@Component(value = "customerUserPermissions") +@Component public class CustomerUserPermissions extends AbstractPermissions { public CustomerUserPermissions() { diff --git a/application/src/main/java/org/thingsboard/server/service/security/permission/DefaultAccessControlService.java b/application/src/main/java/org/thingsboard/server/service/security/permission/DefaultAccessControlService.java index a5feb1c502..6c54bbe68f 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/permission/DefaultAccessControlService.java +++ b/application/src/main/java/org/thingsboard/server/service/security/permission/DefaultAccessControlService.java @@ -16,7 +16,6 @@ package org.thingsboard.server.service.security.permission; import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.HasTenantId; import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; @@ -33,18 +32,18 @@ import java.util.Optional; @Slf4j public class DefaultAccessControlService implements AccessControlService { - private static final String INCORRECT_TENANT_ID = "Incorrect tenantId "; private static final String YOU_DON_T_HAVE_PERMISSION_TO_PERFORM_THIS_OPERATION = "You don't have permission to perform this operation!"; private final Map authorityPermissions = new HashMap<>(); - public DefaultAccessControlService( - @Qualifier("sysAdminPermissions") Permissions sysAdminPermissions, - @Qualifier("tenantAdminPermissions") Permissions tenantAdminPermissions, - @Qualifier("customerUserPermissions") Permissions customerUserPermissions) { + public DefaultAccessControlService(SysAdminPermissions sysAdminPermissions, + TenantAdminPermissions tenantAdminPermissions, + CustomerUserPermissions customerUserPermissions, + MfaConfigurationPermissions mfaConfigurationPermissions) { authorityPermissions.put(Authority.SYS_ADMIN, sysAdminPermissions); authorityPermissions.put(Authority.TENANT_ADMIN, tenantAdminPermissions); authorityPermissions.put(Authority.CUSTOMER_USER, customerUserPermissions); + authorityPermissions.put(Authority.MFA_CONFIGURATION_TOKEN, mfaConfigurationPermissions); } @Override @@ -58,7 +57,7 @@ public class DefaultAccessControlService implements AccessControlService { @Override @SuppressWarnings("unchecked") public void checkPermission(SecurityUser user, Resource resource, - Operation operation, I entityId, T entity) throws ThingsboardException { + Operation operation, I entityId, T entity) throws ThingsboardException { PermissionChecker permissionChecker = getPermissionChecker(user.getAuthority(), resource); if (!permissionChecker.hasPermission(user, operation, entityId, entity)) { permissionDenied(); diff --git a/application/src/main/java/org/thingsboard/server/service/security/permission/MfaConfigurationPermissions.java b/application/src/main/java/org/thingsboard/server/service/security/permission/MfaConfigurationPermissions.java new file mode 100644 index 0000000000..b901030a67 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/security/permission/MfaConfigurationPermissions.java @@ -0,0 +1,28 @@ +/** + * Copyright © 2016-2025 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.security.permission; + +import org.springframework.stereotype.Component; + +@Component +public class MfaConfigurationPermissions extends AbstractPermissions { + + public MfaConfigurationPermissions() { + super(); + // for compatibility with PE + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/security/permission/SysAdminPermissions.java b/application/src/main/java/org/thingsboard/server/service/security/permission/SysAdminPermissions.java index 6bd7aacf54..2593040a12 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/permission/SysAdminPermissions.java +++ b/application/src/main/java/org/thingsboard/server/service/security/permission/SysAdminPermissions.java @@ -23,7 +23,7 @@ import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.service.security.model.SecurityUser; -@Component(value = "sysAdminPermissions") +@Component public class SysAdminPermissions extends AbstractPermissions { public SysAdminPermissions() { diff --git a/application/src/main/java/org/thingsboard/server/service/security/permission/TenantAdminPermissions.java b/application/src/main/java/org/thingsboard/server/service/security/permission/TenantAdminPermissions.java index 7a824ca735..f37b865ae2 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/permission/TenantAdminPermissions.java +++ b/application/src/main/java/org/thingsboard/server/service/security/permission/TenantAdminPermissions.java @@ -25,7 +25,7 @@ import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.service.security.model.SecurityUser; -@Component(value = "tenantAdminPermissions") +@Component public class TenantAdminPermissions extends AbstractPermissions { public TenantAdminPermissions() { @@ -73,7 +73,7 @@ public class TenantAdminPermissions extends AbstractPermissions { }; private static final PermissionChecker tenantPermissionChecker = - new PermissionChecker.GenericPermissionChecker(Operation.READ, Operation.READ_ATTRIBUTES, Operation.READ_TELEMETRY) { + new PermissionChecker.GenericPermissionChecker(Operation.READ, Operation.READ_ATTRIBUTES, Operation.READ_TELEMETRY, Operation.DELETE) { @Override @SuppressWarnings("unchecked") diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DefaultEntityExportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DefaultEntityExportService.java index 5c2fcc7dc6..5d634c4178 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DefaultEntityExportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DefaultEntityExportService.java @@ -24,6 +24,8 @@ import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.ExportableEntity; import org.thingsboard.server.common.data.HasVersion; import org.thingsboard.server.common.data.cf.CalculatedField; +import org.thingsboard.server.common.data.cf.configuration.ArgumentsBasedCalculatedFieldConfiguration; +import org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingCalculatedFieldConfiguration; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityIdFactory; @@ -153,11 +155,21 @@ public class DefaultEntityExportService calculatedFields = calculatedFieldService.findCalculatedFieldsByEntityId(ctx.getTenantId(), entityId); calculatedFields.forEach(calculatedField -> { calculatedField.setEntityId(getExternalIdOrElseInternal(ctx, entityId)); - calculatedField.getConfiguration().getArguments().values().forEach(argument -> { - if (argument.getRefEntityId() != null) { - argument.setRefEntityId(getExternalIdOrElseInternal(ctx, argument.getRefEntityId())); + if (calculatedField.getConfiguration() instanceof ArgumentsBasedCalculatedFieldConfiguration argBasedConfig) { + if (argBasedConfig instanceof GeofencingCalculatedFieldConfiguration geofencingCfg) { + geofencingCfg.getZoneGroups().values().forEach(zoneGroupConfiguration -> { + if (zoneGroupConfiguration.getRefEntityId() != null) { + zoneGroupConfiguration.setRefEntityId(getExternalIdOrElseInternal(ctx, zoneGroupConfiguration.getRefEntityId())); + } + }); + } else { + argBasedConfig.getArguments().values().forEach(argument -> { + if (argument.getRefEntityId() != null) { + argument.setRefEntityId(getExternalIdOrElseInternal(ctx, argument.getRefEntityId())); + } + }); } - }); + } }); return calculatedFields; } diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/csv/AbstractBulkImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/csv/AbstractBulkImportService.java index 9850e2d1a1..bdea24bb2a 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/csv/AbstractBulkImportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/csv/AbstractBulkImportService.java @@ -17,6 +17,7 @@ package org.thingsboard.server.service.sync.ie.importing.csv; import com.fasterxml.jackson.databind.node.ObjectNode; import com.google.common.util.concurrent.FutureCallback; +import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.JsonPrimitive; import jakarta.annotation.Nullable; @@ -183,10 +184,16 @@ public abstract class AbstractBulkImportService dataEntry.getKey().getType() == kvType && StringUtils.isNotEmpty(dataEntry.getKey().getKey())) - .forEach(dataEntry -> kvs.add(dataEntry.getKey().getKey(), dataEntry.getValue().toJsonPrimitive())); + .forEach(dataEntry -> { + ParsedValue value = dataEntry.getValue(); + JsonElement kvValue = (value.getDataType() == DataType.JSON) + ? (JsonElement) value.getValue() + : value.toJsonPrimitive(); + kvs.add(dataEntry.getKey().getKey(), kvValue); + }); return Map.entry(kvType, kvs); }) - .filter(kvsEntry -> kvsEntry.getValue().entrySet().size() > 0) + .filter(kvsEntry -> !kvsEntry.getValue().entrySet().isEmpty()) .forEach(kvsEntry -> { BulkImportColumnType kvType = kvsEntry.getKey(); if (kvType == BulkImportColumnType.SHARED_ATTRIBUTE || kvType == BulkImportColumnType.SERVER_ATTRIBUTE) { diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/BaseEntityImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/BaseEntityImportService.java index 92fdcb09c4..c95fffc205 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/BaseEntityImportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/BaseEntityImportService.java @@ -35,6 +35,8 @@ import org.thingsboard.server.common.data.HasVersion; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.cf.CalculatedField; +import org.thingsboard.server.common.data.cf.configuration.ArgumentsBasedCalculatedFieldConfiguration; +import org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingCalculatedFieldConfiguration; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityIdFactory; @@ -321,11 +323,21 @@ public abstract class BaseEntityImportService { calculatedField.setTenantId(ctx.getTenantId()); calculatedField.setEntityId(savedEntity.getId()); - calculatedField.getConfiguration().getArguments().values().forEach(argument -> { - if (argument.getRefEntityId() != null) { - argument.setRefEntityId(idProvider.getInternalId(argument.getRefEntityId(), ctx.isFinalImportAttempt())); + if (calculatedField.getConfiguration() instanceof ArgumentsBasedCalculatedFieldConfiguration argBasedConfig) { + if (argBasedConfig instanceof GeofencingCalculatedFieldConfiguration geofencingCfg) { + geofencingCfg.getZoneGroups().values().forEach(zoneGroupConfiguration -> { + if (zoneGroupConfiguration.getRefEntityId() != null) { + zoneGroupConfiguration.setRefEntityId(idProvider.getInternalId(zoneGroupConfiguration.getRefEntityId(), ctx.isFinalImportAttempt())); + } + }); + } else { + argBasedConfig.getArguments().values().forEach(argument -> { + if (argument.getRefEntityId() != null) { + argument.setRefEntityId(idProvider.getInternalId(argument.getRefEntityId(), ctx.isFinalImportAttempt())); + } + }); } - }); + } }).toList(); for (CalculatedField existingField : existing) { diff --git a/application/src/main/java/org/thingsboard/server/service/system/SystemInfoService.java b/application/src/main/java/org/thingsboard/server/service/system/SystemInfoService.java index faae80a942..e2edd8a9cc 100644 --- a/application/src/main/java/org/thingsboard/server/service/system/SystemInfoService.java +++ b/application/src/main/java/org/thingsboard/server/service/system/SystemInfoService.java @@ -19,7 +19,9 @@ import org.thingsboard.server.common.data.FeaturesInfo; import org.thingsboard.server.common.data.SystemInfo; public interface SystemInfoService { + SystemInfo getSystemInfo(); FeaturesInfo getFeaturesInfo(); + } diff --git a/application/src/main/java/org/thingsboard/server/service/system/SystemPatchApplier.java b/application/src/main/java/org/thingsboard/server/service/system/SystemPatchApplier.java new file mode 100644 index 0000000000..204d2b5d6c --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/system/SystemPatchApplier.java @@ -0,0 +1,272 @@ +/** + * Copyright © 2016-2025 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.system; + +import com.fasterxml.jackson.databind.JsonNode; +import com.google.common.hash.Hashing; +import jakarta.annotation.PostConstruct; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.stereotype.Component; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.common.util.ThingsBoardThreadFactory; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.widget.WidgetTypeDetails; +import org.thingsboard.server.dao.widget.WidgetTypeService; +import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.install.DatabaseSchemaSettingsService; +import org.thingsboard.server.service.install.InstallScripts; +import org.thingsboard.server.service.install.update.DefaultDataUpdateService; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.Files; +import java.nio.file.NoSuchFileException; +import java.nio.file.Path; +import java.util.Objects; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Stream; + +/** + * Runs at application startup and applies no-downtime data updates + * when the package PATCH version increases (e.g., 4.2.1.0 -> 4.2.1.1). + */ +@Slf4j +@Component +@TbCoreComponent +@RequiredArgsConstructor +public class SystemPatchApplier { + + private static final long ADVISORY_LOCK_ID = 7536891047216478431L; + + private final JdbcTemplate jdbcTemplate; + private final InstallScripts installScripts; + private final DatabaseSchemaSettingsService schemaSettingsService; + private final WidgetTypeService widgetTypeService; + + @PostConstruct + private void init() { + ExecutorService executor = Executors.newSingleThreadExecutor(ThingsBoardThreadFactory.forName("system-patch-applier")); + executor.submit(() -> { + try { + applyPatchIfNeeded(); + } catch (Exception e) { + log.error("Failed to apply system data patch updates", e); + } finally { + executor.shutdown(); + } + }); + } + + private void applyPatchIfNeeded() { + boolean skipVersionCheck = DefaultDataUpdateService.getEnv("SKIP_PATCH_VERSION_CHECK", false); + if (!skipVersionCheck && !isVersionChanged()) { + return; + } + + if (!acquireAdvisoryLock()) { + log.trace("Could not acquire advisory lock. Another node is processing patch updates."); + return; + } + + try { + int updated = updateWidgetTypes(); + log.info("Updated {} widget types", updated); + + schemaSettingsService.updateSchemaVersion(); + log.info("System data patch update completed successfully"); + + } finally { + releaseAdvisoryLock(); + } + } + + private boolean isVersionChanged() { + String packageVersion = schemaSettingsService.getPackageSchemaVersion(); + String dbVersion = schemaSettingsService.getDbSchemaVersion(); + + log.trace("Package version: {}, DB schema version: {}", packageVersion, dbVersion); + + VersionInfo packageVersionInfo = parseVersion(packageVersion); + VersionInfo dbVersionInfo = parseVersion(dbVersion); + + if (packageVersionInfo == null || dbVersionInfo == null) { + log.warn("Unable to parse versions. Package: {}, DB: {}", packageVersion, dbVersion); + return false; + } + + if (!isPatchVersionChanged(packageVersionInfo, dbVersionInfo)) { + return false; + } + + log.info("Patch version increased from {} to {}. Starting system data update.", dbVersion, packageVersion); + return true; + } + + private boolean isPatchVersionChanged(VersionInfo packageVersion, VersionInfo dbVersion) { + return packageVersion.major == dbVersion.major && packageVersion.minor == dbVersion.minor + && packageVersion.maintenance == dbVersion.maintenance && packageVersion.patch > dbVersion.patch; + } + + private int updateWidgetTypes() { + AtomicInteger updated = new AtomicInteger(); + Path widgetTypesDir = installScripts.getWidgetTypesDir(); + + if (!Files.exists(widgetTypesDir)) { + log.trace("Widget types directory does not exist: {}", widgetTypesDir); + return 0; + } + + try (Stream dirStream = listDir(widgetTypesDir).filter(path -> path.toString().endsWith(InstallScripts.JSON_EXT))) { + dirStream.forEach( + path -> { + try { + if (updateWidgetTypeFromFile(path)) { + updated.incrementAndGet(); + } + } catch (Exception e) { + log.error("Unable to update widget type from json: [{}]", path.toString()); + throw new RuntimeException("Unable to update widget type from json", e); + } + } + ); + } + + return updated.get(); + } + + private boolean updateWidgetTypeFromFile(Path filePath) { + JsonNode json = JacksonUtil.toJsonNode(filePath.toFile()); + WidgetTypeDetails fileWidgetType = JacksonUtil.treeToValue(json, WidgetTypeDetails.class); + String fqn = fileWidgetType.getFqn(); + + WidgetTypeDetails existingWidgetType = widgetTypeService.findWidgetTypeDetailsByTenantIdAndFqn(TenantId.SYS_TENANT_ID, fqn); + if (existingWidgetType == null) { + // We expect only update here, so it's probably never happening, but for test purpose leave it like this: + throw new RuntimeException("Widget type not found: " + fqn); + } + if (isWidgetTypeChanged(existingWidgetType, fileWidgetType)) { + existingWidgetType.setDescription(fileWidgetType.getDescription()); + existingWidgetType.setName(fileWidgetType.getName()); + existingWidgetType.setDescriptor(fileWidgetType.getDescriptor()); + widgetTypeService.saveWidgetType(existingWidgetType); + log.trace("Updated widget type: {}", fqn); + return true; + } + + log.trace("Widget type unchanged: {}", fqn); + return false; + } + + private boolean isWidgetTypeChanged(WidgetTypeDetails existing, WidgetTypeDetails file) { + if (!isDescriptorEqual(existing.getDescriptor(), file.getDescriptor())) { + return true; + } + + if (!Objects.equals(existing.getName(), file.getName())) { + return true; + } + + return !Objects.equals(existing.getDescription(), file.getDescription()); + } + + private boolean isDescriptorEqual(JsonNode desc1, JsonNode desc2) { + if (desc1 == null && desc2 == null) { + return true; + } + if (desc1 == null || desc2 == null) { + return false; + } + + try { + String hash1 = computeChecksum(desc1); + String hash2 = computeChecksum(desc2); + return Objects.equals(hash1, hash2); + } catch (Exception e) { + log.warn("Failed to compare descriptors using checksum, falling back to equals", e); + return desc1.equals(desc2); + } + } + + private String computeChecksum(JsonNode node) { + String canonicalString = JacksonUtil.toCanonicalString(node); + if (canonicalString == null) { + return null; + } + return Hashing.sha256().hashBytes(canonicalString.getBytes()).toString(); + } + + private boolean acquireAdvisoryLock() { + try { + Boolean acquired = jdbcTemplate.queryForObject( + "SELECT pg_try_advisory_lock(?)", + Boolean.class, + ADVISORY_LOCK_ID + ); + if (Boolean.TRUE.equals(acquired)) { + log.trace("Acquired advisory lock"); + return true; + } + return false; + } catch (Exception e) { + log.error("Failed to acquire advisory lock", e); + return false; + } + } + + private void releaseAdvisoryLock() { + try { + jdbcTemplate.queryForObject( + "SELECT pg_advisory_unlock(?)", + Boolean.class, + ADVISORY_LOCK_ID + ); + log.debug("Released advisory lock"); + } catch (Exception e) { + log.error("Failed to release advisory lock", e); + } + } + + private VersionInfo parseVersion(String version) { + try { + String[] parts = version.split("\\."); + int major = Integer.parseInt(parts[0]); + int minor = parts.length > 1 ? Integer.parseInt(parts[1]) : 0; + int maintenance = parts.length > 2 ? Integer.parseInt(parts[2]) : 0; + int patch = parts.length > 3 ? Integer.parseInt(parts[3]) : 0; + return new VersionInfo(major, minor, maintenance, patch); + } catch (Exception e) { + log.error("Failed to parse version: {}", version, e); + return null; + } + } + + private Stream listDir(Path dir) { + try { + return Files.list(dir); + } catch (NoSuchFileException e) { + return Stream.empty(); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + public record VersionInfo(int major, int minor, int maintenance, int patch) {} + +} diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultAlarmSubscriptionService.java b/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultAlarmSubscriptionService.java index 8c4a375fae..b68f604460 100644 --- a/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultAlarmSubscriptionService.java +++ b/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultAlarmSubscriptionService.java @@ -102,7 +102,12 @@ public class DefaultAlarmSubscriptionService extends AbstractSubscriptionService @Override public AlarmApiCallResult clearAlarm(TenantId tenantId, AlarmId alarmId, long clearTs, JsonNode details) { - return withWsCallback(alarmService.clearAlarm(tenantId, alarmId, clearTs, details)); + return clearAlarm(tenantId, alarmId, clearTs, details, true); + } + + @Override + public AlarmApiCallResult clearAlarm(TenantId tenantId, AlarmId alarmId, long clearTs, JsonNode details, boolean pushEvent) { + return withWsCallback(alarmService.clearAlarm(tenantId, alarmId, clearTs, details, pushEvent)); } @Override diff --git a/application/src/main/java/org/thingsboard/server/service/ttl/KafkaEdgeTopicsCleanUpService.java b/application/src/main/java/org/thingsboard/server/service/ttl/KafkaEdgeTopicsCleanUpService.java index 6d06c85585..3354c63f9f 100644 --- a/application/src/main/java/org/thingsboard/server/service/ttl/KafkaEdgeTopicsCleanUpService.java +++ b/application/src/main/java/org/thingsboard/server/service/ttl/KafkaEdgeTopicsCleanUpService.java @@ -30,9 +30,7 @@ import org.thingsboard.server.dao.edge.EdgeService; import org.thingsboard.server.dao.tenant.TenantService; import org.thingsboard.server.queue.discovery.PartitionService; import org.thingsboard.server.queue.discovery.TopicService; -import org.thingsboard.server.queue.kafka.TbKafkaAdmin; -import org.thingsboard.server.queue.kafka.TbKafkaSettings; -import org.thingsboard.server.queue.kafka.TbKafkaTopicConfigs; +import org.thingsboard.server.queue.kafka.KafkaAdmin; import org.thingsboard.server.queue.util.TbCoreComponent; import java.time.Instant; @@ -57,7 +55,7 @@ public class KafkaEdgeTopicsCleanUpService extends AbstractCleanUpService { private final TenantService tenantService; private final EdgeService edgeService; private final AttributesService attributesService; - private final TbKafkaAdmin kafkaAdmin; + private final KafkaAdmin kafkaAdmin; @Value("${sql.ttl.edge_events.edge_events_ttl:2628000}") private long ttlSeconds; @@ -67,13 +65,13 @@ public class KafkaEdgeTopicsCleanUpService extends AbstractCleanUpService { public KafkaEdgeTopicsCleanUpService(PartitionService partitionService, EdgeService edgeService, TenantService tenantService, AttributesService attributesService, - TopicService topicService, TbKafkaSettings kafkaSettings, TbKafkaTopicConfigs kafkaTopicConfigs) { + TopicService topicService, KafkaAdmin kafkaAdmin) { super(partitionService); this.topicService = topicService; this.tenantService = tenantService; this.edgeService = edgeService; this.attributesService = attributesService; - this.kafkaAdmin = new TbKafkaAdmin(kafkaSettings, kafkaTopicConfigs.getEdgeEventConfigs()); + this.kafkaAdmin = kafkaAdmin; } @Scheduled(initialDelayString = "#{T(org.apache.commons.lang3.RandomUtils).nextLong(0, ${sql.ttl.edge_events.execution_interval_ms})}", fixedDelayString = "${sql.ttl.edge_events.execution_interval_ms}") @@ -82,8 +80,8 @@ public class KafkaEdgeTopicsCleanUpService extends AbstractCleanUpService { return; } - Set topics = kafkaAdmin.getAllTopics(); - if (topics == null || topics.isEmpty()) { + Set topics = kafkaAdmin.listTopics(); + if (topics.isEmpty()) { return; } diff --git a/application/src/main/java/org/thingsboard/server/utils/CalculatedFieldArgumentUtils.java b/application/src/main/java/org/thingsboard/server/utils/CalculatedFieldArgumentUtils.java new file mode 100644 index 0000000000..97d53ac252 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/utils/CalculatedFieldArgumentUtils.java @@ -0,0 +1,129 @@ +/** + * Copyright © 2016-2025 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.utils; + +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.MoreExecutors; +import org.apache.commons.lang3.math.NumberUtils; +import org.thingsboard.server.common.data.StringUtils; +import org.thingsboard.server.common.data.cf.configuration.Argument; +import org.thingsboard.server.common.data.cf.configuration.aggregation.AggMetric; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.kv.AttributeKvEntry; +import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry; +import org.thingsboard.server.common.data.kv.BooleanDataEntry; +import org.thingsboard.server.common.data.kv.DoubleDataEntry; +import org.thingsboard.server.common.data.kv.KvEntry; +import org.thingsboard.server.common.data.kv.StringDataEntry; +import org.thingsboard.server.common.data.kv.TsKvEntry; +import org.thingsboard.server.service.cf.ctx.state.ArgumentEntry; +import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx; +import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldState; +import org.thingsboard.server.service.cf.ctx.state.ScriptCalculatedFieldState; +import org.thingsboard.server.service.cf.ctx.state.SimpleCalculatedFieldState; +import org.thingsboard.server.service.cf.ctx.state.SingleValueArgumentEntry; +import org.thingsboard.server.service.cf.ctx.state.aggregation.RelatedEntitiesAggregationCalculatedFieldState; +import org.thingsboard.server.service.cf.ctx.state.aggregation.single.AggIntervalEntry; +import org.thingsboard.server.service.cf.ctx.state.aggregation.single.AggIntervalEntryStatus; +import org.thingsboard.server.service.cf.ctx.state.aggregation.single.EntityAggregationArgumentEntry; +import org.thingsboard.server.service.cf.ctx.state.aggregation.single.EntityAggregationCalculatedFieldState; +import org.thingsboard.server.service.cf.ctx.state.alarm.AlarmCalculatedFieldState; +import org.thingsboard.server.service.cf.ctx.state.geofencing.GeofencingCalculatedFieldState; +import org.thingsboard.server.service.cf.ctx.state.propagation.PropagationCalculatedFieldState; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +public class CalculatedFieldArgumentUtils { + + public static ListenableFuture transformSingleValueArgument(ListenableFuture> kvEntryFuture) { + return Futures.transform(kvEntryFuture, CalculatedFieldArgumentUtils::transformSingleValueArgument, MoreExecutors.directExecutor()); + } + + public static ArgumentEntry transformSingleValueArgument(Optional kvEntry) { + if (kvEntry.isPresent() && kvEntry.get().getValue() != null) { + return ArgumentEntry.createSingleValueArgument(kvEntry.get()); + } else { + return new SingleValueArgumentEntry(); + } + } + + public static ArgumentEntry transformTsRollingArgument(List tsRolling, int limit, long argTimeWindow) { + return ArgumentEntry.createTsRollingArgument(tsRolling, limit, argTimeWindow); + } + + public static ArgumentEntry transformAggMetricArgument(List timeSeries, String argKey, AggMetric aggMetric) { + if (timeSeries == null || timeSeries.isEmpty()) { + return createDefaultMetricArgumentEntry(argKey, aggMetric); + } + return ArgumentEntry.createSingleValueArgument(timeSeries.get(0)); + } + + public static ArgumentEntry createDefaultMetricArgumentEntry(String argKey, AggMetric metric) { + Long defaultValue = metric.getDefaultValue(); + if (defaultValue != null) { + return ArgumentEntry.createSingleValueArgument(new DoubleDataEntry(argKey, defaultValue.doubleValue())); + } + return new SingleValueArgumentEntry(); + } + + public static ArgumentEntry transformAggregationArgument(List timeSeries, long startIntervalTs, long endIntervalTs) { + Map aggIntervals = new HashMap<>(); + AggIntervalEntry aggIntervalEntry = new AggIntervalEntry(startIntervalTs, endIntervalTs); + if (timeSeries == null || timeSeries.isEmpty()) { + aggIntervals.put(aggIntervalEntry, new AggIntervalEntryStatus()); + } else { + aggIntervals.put(aggIntervalEntry, new AggIntervalEntryStatus(System.currentTimeMillis())); + } + return new EntityAggregationArgumentEntry(aggIntervals); + } + + public static KvEntry createDefaultKvEntry(Argument argument) { + String key = argument.getRefEntityKey().getKey(); + String defaultValue = argument.getDefaultValue(); + if (StringUtils.isBlank(defaultValue)) { + return new StringDataEntry(key, null); + } + if (NumberUtils.isParsable(defaultValue)) { + return new DoubleDataEntry(key, Double.parseDouble(defaultValue)); + } + if ("true".equalsIgnoreCase(defaultValue) || "false".equalsIgnoreCase(defaultValue)) { + return new BooleanDataEntry(key, Boolean.parseBoolean(defaultValue)); + } + return new StringDataEntry(key, defaultValue); + } + + public static AttributeKvEntry createDefaultAttributeEntry(Argument argument, long ts) { + KvEntry kvEntry = createDefaultKvEntry(argument); + return new BaseAttributeKvEntry(kvEntry, ts, 0L); + } + + public static CalculatedFieldState createStateByType(CalculatedFieldCtx ctx, EntityId entityId) { + return switch (ctx.getCfType()) { + case SIMPLE -> new SimpleCalculatedFieldState(entityId); + case SCRIPT -> new ScriptCalculatedFieldState(entityId); + case GEOFENCING -> new GeofencingCalculatedFieldState(entityId); + case ALARM -> new AlarmCalculatedFieldState(entityId); + case PROPAGATION -> new PropagationCalculatedFieldState(entityId); + case RELATED_ENTITIES_AGGREGATION -> new RelatedEntitiesAggregationCalculatedFieldState(entityId); + case ENTITY_AGGREGATION -> new EntityAggregationCalculatedFieldState(entityId); + }; + } + +} diff --git a/application/src/main/java/org/thingsboard/server/utils/CalculatedFieldUtils.java b/application/src/main/java/org/thingsboard/server/utils/CalculatedFieldUtils.java index 77080c28c8..710ce48ef6 100644 --- a/application/src/main/java/org/thingsboard/server/utils/CalculatedFieldUtils.java +++ b/application/src/main/java/org/thingsboard/server/utils/CalculatedFieldUtils.java @@ -15,31 +15,57 @@ */ package org.thingsboard.server.utils; +import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.StringUtils; +import org.thingsboard.server.common.data.alarm.AlarmSeverity; import org.thingsboard.server.common.data.cf.CalculatedFieldType; +import org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingPresenceStatus; import org.thingsboard.server.common.data.id.CalculatedFieldId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityIdFactory; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.kv.BasicKvEntry; import org.thingsboard.server.common.util.KvProtoUtil; +import org.thingsboard.server.common.util.ProtoUtils; +import org.thingsboard.server.gen.transport.TransportProtos.AlarmRuleStateProto; +import org.thingsboard.server.gen.transport.TransportProtos.AlarmStateProto; +import org.thingsboard.server.gen.transport.TransportProtos.ArgumentIntervalProto; import org.thingsboard.server.gen.transport.TransportProtos.CalculatedFieldEntityCtxIdProto; import org.thingsboard.server.gen.transport.TransportProtos.CalculatedFieldIdProto; import org.thingsboard.server.gen.transport.TransportProtos.CalculatedFieldStateProto; +import org.thingsboard.server.gen.transport.TransportProtos.GeofencingArgumentProto; +import org.thingsboard.server.gen.transport.TransportProtos.GeofencingZoneProto; import org.thingsboard.server.gen.transport.TransportProtos.SingleValueArgumentProto; import org.thingsboard.server.gen.transport.TransportProtos.TsDoubleValProto; import org.thingsboard.server.gen.transport.TransportProtos.TsRollingArgumentProto; import org.thingsboard.server.gen.transport.TransportProtos.TsValueProto; import org.thingsboard.server.service.cf.ctx.CalculatedFieldEntityCtxId; +import org.thingsboard.server.service.cf.ctx.state.ArgumentEntry; import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldState; import org.thingsboard.server.service.cf.ctx.state.ScriptCalculatedFieldState; import org.thingsboard.server.service.cf.ctx.state.SimpleCalculatedFieldState; import org.thingsboard.server.service.cf.ctx.state.SingleValueArgumentEntry; import org.thingsboard.server.service.cf.ctx.state.TsRollingArgumentEntry; +import org.thingsboard.server.service.cf.ctx.state.aggregation.RelatedEntitiesAggregationCalculatedFieldState; +import org.thingsboard.server.service.cf.ctx.state.aggregation.RelatedEntitiesArgumentEntry; +import org.thingsboard.server.service.cf.ctx.state.aggregation.single.AggIntervalEntry; +import org.thingsboard.server.service.cf.ctx.state.aggregation.single.AggIntervalEntryStatus; +import org.thingsboard.server.service.cf.ctx.state.aggregation.single.EntityAggregationArgumentEntry; +import org.thingsboard.server.service.cf.ctx.state.aggregation.single.EntityAggregationCalculatedFieldState; +import org.thingsboard.server.service.cf.ctx.state.alarm.AlarmCalculatedFieldState; +import org.thingsboard.server.service.cf.ctx.state.alarm.AlarmRuleState; +import org.thingsboard.server.service.cf.ctx.state.geofencing.GeofencingArgumentEntry; +import org.thingsboard.server.service.cf.ctx.state.geofencing.GeofencingCalculatedFieldState; +import org.thingsboard.server.service.cf.ctx.state.geofencing.GeofencingZoneState; +import org.thingsboard.server.service.cf.ctx.state.propagation.PropagationCalculatedFieldState; +import java.util.HashMap; +import java.util.Map; import java.util.Optional; import java.util.TreeMap; import java.util.UUID; +import java.util.function.Function; +import java.util.stream.Collectors; public class CalculatedFieldUtils { @@ -75,15 +101,55 @@ public class CalculatedFieldUtils { .setType(state.getType().name()); state.getArguments().forEach((argName, argEntry) -> { - if (argEntry instanceof SingleValueArgumentEntry singleValueArgumentEntry) { - builder.addSingleValueArguments(toSingleValueArgumentProto(argName, singleValueArgumentEntry)); - } else if (argEntry instanceof TsRollingArgumentEntry rollingArgumentEntry) { - builder.addRollingValueArguments(toRollingArgumentProto(argName, rollingArgumentEntry)); + switch (argEntry.getType()) { + case SINGLE_VALUE -> builder.addSingleValueArguments(toSingleValueArgumentProto(argName, (SingleValueArgumentEntry) argEntry)); + case TS_ROLLING -> builder.addRollingValueArguments(toRollingArgumentProto(argName, (TsRollingArgumentEntry) argEntry)); + case GEOFENCING -> builder.addGeofencingArguments(toGeofencingArgumentProto(argName, (GeofencingArgumentEntry) argEntry)); + case RELATED_ENTITIES -> { + RelatedEntitiesArgumentEntry relatedEntitiesArgumentEntry = (RelatedEntitiesArgumentEntry) argEntry; + relatedEntitiesArgumentEntry.getEntityInputs() + .forEach((entityId, entry) -> builder.addSingleValueArguments(toSingleValueArgumentProto(argName, (SingleValueArgumentEntry) entry))); + } + case ENTITY_AGGREGATION -> { + EntityAggregationArgumentEntry entityAggregationArgumentEntry = (EntityAggregationArgumentEntry) argEntry; + entityAggregationArgumentEntry.getAggIntervals().forEach((interval, argumentStatus) -> builder.addAggregationArguments(toArgumentIntervalProto(argName, interval, argumentStatus))); + } } }); + if (state instanceof AlarmCalculatedFieldState alarmState) { + AlarmStateProto.Builder alarmStateProto = AlarmStateProto.newBuilder(); + alarmState.getCreateRuleStates().forEach((severity, ruleState) -> { + alarmStateProto.addCreateRuleStates(toAlarmRuleStateProto(ruleState)); + }); + if (alarmState.getClearRuleState() != null) { + alarmStateProto.setClearRuleState(toAlarmRuleStateProto(alarmState.getClearRuleState())); + } + } + if (state instanceof RelatedEntitiesAggregationCalculatedFieldState aggState) { + builder.setLastArgsUpdateTs(aggState.getLastArgsRefreshTs()); + builder.setLastMetricsEvalTs(aggState.getLastMetricsEvalTs()); + } return builder.build(); } + private static AlarmRuleStateProto toAlarmRuleStateProto(AlarmRuleState ruleState) { + return AlarmRuleStateProto.newBuilder() + .setSeverity(Optional.ofNullable(ruleState.getSeverity()).map(Enum::name).orElse("")) + .setEventCount(ruleState.getEventCount()) + .setFirstEventTs(ruleState.getFirstEventTs()) + .setLastEventTs(ruleState.getLastEventTs()) + .build(); + } + + private static AlarmRuleState fromAlarmRuleStateProto(AlarmRuleStateProto proto, AlarmCalculatedFieldState state) { + AlarmSeverity severity = StringUtils.isNotEmpty(proto.getSeverity()) ? AlarmSeverity.valueOf(proto.getSeverity()) : null; + AlarmRuleState ruleState = new AlarmRuleState(severity, null, state); + ruleState.setEventCount(proto.getEventCount()); + ruleState.setFirstEventTs(proto.getFirstEventTs()); + ruleState.setLastEventTs(proto.getLastEventTs()); + return ruleState; + } + public static SingleValueArgumentProto toSingleValueArgumentProto(String argName, SingleValueArgumentEntry entry) { SingleValueArgumentProto.Builder builder = SingleValueArgumentProto.newBuilder() .setArgName(argName); @@ -94,9 +160,23 @@ public class CalculatedFieldUtils { Optional.ofNullable(entry.getVersion()).ifPresent(builder::setVersion); + if (entry.getEntityId() != null) { + builder.setEntityId(ProtoUtils.toProto(entry.getEntityId())); + } + return builder.build(); } + public static ArgumentIntervalProto toArgumentIntervalProto(String argName, AggIntervalEntry intervalEntry, AggIntervalEntryStatus argumentStatus) { + return ArgumentIntervalProto.newBuilder() + .setArgName(argName) + .setStartTs(intervalEntry.getStartTs()) + .setEndTs(intervalEntry.getEndTs()) + .setLastArgsRefreshTs(argumentStatus.getLastArgsRefreshTs()) + .setLastMetricsEvalTs(argumentStatus.getLastMetricsEvalTs()) + .build(); + } + public static TsRollingArgumentProto toRollingArgumentProto(String argName, TsRollingArgumentEntry entry) { TsRollingArgumentProto.Builder builder = TsRollingArgumentProto.newBuilder() .setKey(argName) @@ -108,7 +188,28 @@ public class CalculatedFieldUtils { return builder.build(); } - public static CalculatedFieldState fromProto(CalculatedFieldStateProto proto) { + private static GeofencingArgumentProto toGeofencingArgumentProto(String argName, GeofencingArgumentEntry geofencingArgumentEntry) { + Map zoneStates = geofencingArgumentEntry.getZoneStates(); + GeofencingArgumentProto.Builder builder = GeofencingArgumentProto.newBuilder() + .setArgName(argName); + zoneStates.forEach((entityId, zoneState) -> + builder.addZones(toGeofencingZoneProto(entityId, zoneState))); + return builder.build(); + } + + private static GeofencingZoneProto toGeofencingZoneProto(EntityId entityId, GeofencingZoneState zoneState) { + GeofencingZoneProto.Builder builder = GeofencingZoneProto.newBuilder() + .setZoneId(ProtoUtils.toProto(entityId)) + .setTs(zoneState.getTs()) + .setVersion(zoneState.getVersion()) + .setPerimeterDefinition(JacksonUtil.toString(zoneState.getPerimeterDefinition())); + if (zoneState.getLastPresence() != null) { + builder.setInside(zoneState.getLastPresence().equals(GeofencingPresenceStatus.INSIDE)); + } + return builder.build(); + } + + public static CalculatedFieldState fromProto(CalculatedFieldEntityCtxId id, CalculatedFieldStateProto proto) { if (StringUtils.isEmpty(proto.getType())) { return null; } @@ -116,16 +217,68 @@ public class CalculatedFieldUtils { CalculatedFieldType type = CalculatedFieldType.valueOf(proto.getType()); CalculatedFieldState state = switch (type) { - case SIMPLE -> new SimpleCalculatedFieldState(); - case SCRIPT -> new ScriptCalculatedFieldState(); + case SIMPLE -> new SimpleCalculatedFieldState(id.entityId()); + case SCRIPT -> new ScriptCalculatedFieldState(id.entityId()); + case GEOFENCING -> new GeofencingCalculatedFieldState(id.entityId()); + case ALARM -> new AlarmCalculatedFieldState(id.entityId()); + case PROPAGATION -> new PropagationCalculatedFieldState(id.entityId()); + case RELATED_ENTITIES_AGGREGATION -> new RelatedEntitiesAggregationCalculatedFieldState(id.entityId()); + case ENTITY_AGGREGATION -> new EntityAggregationCalculatedFieldState(id.entityId()); }; + if (state instanceof RelatedEntitiesAggregationCalculatedFieldState relatedEntitiesAggState) { + Map> arguments = new HashMap<>(); + proto.getSingleValueArgumentsList().forEach(argProto -> { + SingleValueArgumentEntry entry = fromSingleValueArgumentProto(argProto); + arguments.computeIfAbsent(argProto.getArgName(), name -> new HashMap<>()).put(entry.getEntityId(), entry); + }); + arguments.forEach((argName, entityInputs) -> { + relatedEntitiesAggState.getArguments().put(argName, new RelatedEntitiesArgumentEntry(entityInputs, false)); + }); + relatedEntitiesAggState.setLastArgsRefreshTs(proto.getLastArgsUpdateTs()); + relatedEntitiesAggState.setLastMetricsEvalTs(proto.getLastMetricsEvalTs()); + + return relatedEntitiesAggState; + } + + if (state instanceof EntityAggregationCalculatedFieldState entityAggregationState) { + Map arguments = new HashMap<>(); + + proto.getAggregationArgumentsList().forEach(argProto -> { + AggIntervalEntry intervalEntry = new AggIntervalEntry(argProto.getStartTs(), argProto.getEndTs()); + AggIntervalEntryStatus intervalStatus = new AggIntervalEntryStatus(argProto.getLastArgsRefreshTs(), argProto.getLastMetricsEvalTs()); + EntityAggregationArgumentEntry argEntry = arguments.computeIfAbsent(argProto.getArgName(), name -> new EntityAggregationArgumentEntry(new HashMap<>())); + argEntry.getAggIntervals().put(intervalEntry, intervalStatus); + }); + + entityAggregationState.getArguments().putAll(arguments); + + return entityAggregationState; + } + proto.getSingleValueArgumentsList().forEach(argProto -> state.getArguments().put(argProto.getArgName(), fromSingleValueArgumentProto(argProto))); - if (CalculatedFieldType.SCRIPT.equals(type)) { - proto.getRollingValueArgumentsList().forEach(argProto -> - state.getArguments().put(argProto.getKey(), fromRollingArgumentProto(argProto))); + switch (type) { + case SCRIPT -> { + proto.getRollingValueArgumentsList().forEach(argProto -> + state.getArguments().put(argProto.getKey(), fromRollingArgumentProto(argProto))); + } + case GEOFENCING -> { + proto.getGeofencingArgumentsList().forEach(argProto -> + state.getArguments().put(argProto.getArgName(), fromGeofencingArgumentProto(argProto))); + } + case ALARM -> { + AlarmCalculatedFieldState alarmState = (AlarmCalculatedFieldState) state; + AlarmStateProto alarmStateProto = proto.getAlarmState(); + for (AlarmRuleStateProto ruleStateProto : alarmStateProto.getCreateRuleStatesList()) { + AlarmRuleState ruleState = fromAlarmRuleStateProto(ruleStateProto, alarmState); + alarmState.getCreateRuleStates().put(ruleState.getSeverity(), ruleState); + } + if (alarmStateProto.hasClearRuleState()) { + alarmState.setClearRuleState(fromAlarmRuleStateProto(alarmStateProto.getClearRuleState(), alarmState)); + } + } } return state; @@ -136,11 +289,14 @@ public class CalculatedFieldUtils { return new SingleValueArgumentEntry(); } TsValueProto tsValueProto = proto.getValue(); - return new SingleValueArgumentEntry( - tsValueProto.getTs(), - (BasicKvEntry) KvProtoUtil.fromTsValueProto(proto.getArgName(), tsValueProto), - proto.getVersion() - ); + BasicKvEntry kvEntry = (BasicKvEntry) KvProtoUtil.fromTsValueProto(proto.getArgName(), tsValueProto); + long ts = tsValueProto.getTs(); + long version = proto.getVersion(); + if (proto.hasEntityId()) { + EntityId entityId = ProtoUtils.fromProto(proto.getEntityId()); + return new SingleValueArgumentEntry(entityId, ts, kvEntry, version); + } + return new SingleValueArgumentEntry(ts, kvEntry, version); } public static TsRollingArgumentEntry fromRollingArgumentProto(TsRollingArgumentProto proto) { @@ -149,4 +305,15 @@ public class CalculatedFieldUtils { return new TsRollingArgumentEntry(tsRecords, proto.getLimit(), proto.getTimeWindow()); } + + private static ArgumentEntry fromGeofencingArgumentProto(GeofencingArgumentProto proto) { + Map zoneStates = proto.getZonesList() + .stream() + .map(GeofencingZoneState::new) + .collect(Collectors.toMap(GeofencingZoneState::getZoneId, Function.identity())); + GeofencingArgumentEntry geofencingArgumentEntry = new GeofencingArgumentEntry(); + geofencingArgumentEntry.setZoneStates(zoneStates); + return geofencingArgumentEntry; + } + } diff --git a/application/src/main/java/org/thingsboard/server/utils/CsvUtils.java b/application/src/main/java/org/thingsboard/server/utils/CsvUtils.java index 3f75a4918f..0fde72a3b0 100644 --- a/application/src/main/java/org/thingsboard/server/utils/CsvUtils.java +++ b/application/src/main/java/org/thingsboard/server/utils/CsvUtils.java @@ -17,10 +17,15 @@ package org.thingsboard.server.utils; import lombok.AccessLevel; import lombok.NoArgsConstructor; +import lombok.SneakyThrows; import org.apache.commons.csv.CSVFormat; +import org.apache.commons.csv.CSVPrinter; import org.apache.commons.csv.CSVRecord; import org.apache.commons.io.input.CharSequenceReader; +import java.io.ByteArrayOutputStream; +import java.io.OutputStreamWriter; +import java.nio.charset.StandardCharsets; import java.util.List; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -43,4 +48,18 @@ public class CsvUtils { .collect(Collectors.toList()); } + @SneakyThrows + public static byte[] generateCsv(List> rows) { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + try (OutputStreamWriter writer = new OutputStreamWriter(out, StandardCharsets.UTF_8); + CSVPrinter csvPrinter = new CSVPrinter(writer, CSVFormat.DEFAULT)) { + + for (List row : rows) { + csvPrinter.printRecord(row); + } + csvPrinter.flush(); + } + return out.toByteArray(); + } + } diff --git a/application/src/main/resources/logback.xml b/application/src/main/resources/logback.xml index c37be2e620..8e1a49faef 100644 --- a/application/src/main/resources/logback.xml +++ b/application/src/main/resources/logback.xml @@ -56,6 +56,9 @@ + + + diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index c54553fff8..51b1c0ae26 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -100,6 +100,9 @@ server: rate_limits: # Limit that prohibits resetting the password for the user too often. The value of the rate limit. By default, no more than 5 requests per hour reset_password_per_user: "${RESET_PASSWORD_PER_USER_RATE_LIMIT_CONFIGURATION:5:3600}" + rule_engine: + # Default timeout for waiting response of REST API request to Rule Engine in milliseconds + response_timeout: "${DEFAULT_RULE_ENGINE_RESPONSE_TIMEOUT:10000}" # Application info parameters app: @@ -303,8 +306,11 @@ cassandra: default_fetch_size: "${CASSANDRA_DEFAULT_FETCH_SIZE:2000}" # Specify partitioning size for timestamp key-value storage. Example: MINUTES, HOURS, DAYS, MONTHS, INDEFINITE ts_key_value_partitioning: "${TS_KV_PARTITIONING:MONTHS}" - # Enable/Disable timestamp key-value partioning on read queries + # Enable/Disable timestamp key-value partitioning on read queries use_ts_key_value_partitioning_on_read: "${USE_TS_KV_PARTITIONING_ON_READ:true}" + # Safety trigger to fall back to use_ts_key_value_partitioning_on_read as true if estimated partitions count is greater than safety trigger value. + # It helps to prevent building huge partition list (OOM) for corner cases (like from 0 to infinity) and prefer fewer reads strategy from NoSQL database + use_ts_key_value_partitioning_on_read_max_estimated_partition_count: "${USE_TS_KV_PARTITIONING_ON_READ_MAX_ESTIMATED_PARTITION_COUNT:40}" # The number of partitions that are cached in memory of each service. It is useful to decrease the load of re-inserting the same partitions again ts_key_value_partitions_max_cache_size: "${TS_KV_PARTITIONS_MAX_CACHE_SIZE:100000}" # Timeseries Time To Live (in seconds) for Cassandra Record. 0 - record has never expired @@ -523,6 +529,11 @@ actors: configuration: "${ACTORS_CALCULATED_FIELD_DEBUG_MODE_RATE_LIMITS_PER_TENANT_CONFIGURATION:50000:3600}" # Time in seconds to receive calculation result. calculation_timeout: "${ACTORS_CALCULATION_TIMEOUT_SEC:5}" + # Interval in seconds to check calculated fields for re-evaluation interval. 1 minute by default. + check_interval: "${ACTORS_CALCULATED_FIELDS_CHECK_INTERVAL_SEC:60}" + alarms: + # Interval in seconds to re-evaluate Alarm rules that have a time schedule. 2 minutes by default. + reevaluation_interval: "${ACTORS_ALARMS_REEVALUATION_INTERVAL_SEC:120}" debug: settings: @@ -676,6 +687,9 @@ cache: maxSize: "${CACHE_SPECS_IMAGE_ETAGS_MAX_SIZE:10000}" # 0 means the cache is disabled systemImagesBrowserTtlInMinutes: "${CACHE_SPECS_IMAGE_SYSTEM_BROWSER_TTL:0}" # Browser cache TTL for system images in minutes. 0 means the cache is disabled tenantImagesBrowserTtlInMinutes: "${CACHE_SPECS_IMAGE_TENANT_BROWSER_TTL:0}" # Browser cache TTL for tenant images in minutes. 0 means the cache is disabled + tbResourceData: + timeToLiveInMinutes: "${CACHE_SPECS_RESOURCE_DATA_TTL:10080}" # TB resource data cache TTL + maxSize: "${CACHE_SPECS_RESOURCE_DATA_MAX_SIZE:100000}" # 0 means the cache is disabled # Spring data parameters spring.data.redis.repositories.enabled: false # Disable this because it is not required. @@ -742,9 +756,9 @@ redis: # Minumum number of idle connections that can be maintained in the pool without being closed minIdle: "${REDIS_POOL_CONFIG_MIN_IDLE:16}" # Enable/Disable PING command sent when a connection is borrowed - testOnBorrow: "${REDIS_POOL_CONFIG_TEST_ON_BORROW:true}" + testOnBorrow: "${REDIS_POOL_CONFIG_TEST_ON_BORROW:false}" # The property is used to specify whether to test the connection before returning it to the connection pool. - testOnReturn: "${REDIS_POOL_CONFIG_TEST_ON_RETURN:true}" + testOnReturn: "${REDIS_POOL_CONFIG_TEST_ON_RETURN:false}" # The property is used in the context of connection pooling in Redis testWhileIdle: "${REDIS_POOL_CONFIG_TEST_WHILE_IDLE:true}" # Minimum time that an idle connection should be idle before it can be evicted from the connection pool. The value is set in milliseconds @@ -1145,7 +1159,7 @@ transport: # - A value of 0 means we accept using CID but will not generate one for foreign peer (enables support but not for incoming traffic). # - A value between 0 and <= 4: SingleNodeConnectionIdGenerator is used # - A value that are > 4: MultiNodeConnectionIdGenerator is used - connection_id_length: "${LWM2M_DTLS_CONNECTION_ID_LENGTH:}" + connection_id_length: "${LWM2M_DTLS_CONNECTION_ID_LENGTH:8}" server: # LwM2M Server ID id: "${LWM2M_SERVER_ID:123}" @@ -1190,7 +1204,7 @@ transport: # Enable/disable Bootstrap Server enabled: "${LWM2M_ENABLED_BS:true}" # Default value in LwM2M client after start in mode Bootstrap for the object : name "LWM2M Security" field: "Short Server ID" (deviceProfile: Bootstrap.BOOTSTRAP SERVER.Short ID) - id: "${LWM2M_SERVER_ID_BS:111}" + id: "${LWM2M_SERVER_ID_BS:0}" # LwM2M bootstrap server bind address. Bind to all interfaces by default bind_address: "${LWM2M_BS_BIND_ADDRESS:0.0.0.0}" # LwM2M bootstrap server bind port @@ -1333,7 +1347,7 @@ coap: # - A value of 0 means we accept using CID but will not generate one for foreign peer (enables support but not for incoming traffic). # - A value between 0 and <= 4: SingleNodeConnectionIdGenerator is used # - A value that are > 4: MultiNodeConnectionIdGenerator is used - connection_id_length: "${COAP_DTLS_CONNECTION_ID_LENGTH:}" + connection_id_length: "${COAP_DTLS_CONNECTION_ID_LENGTH:8}" # Specify the MTU (Maximum Transmission Unit). # Should be used if LAN MTU is not used, e.g. if IP tunnels are used or if the client uses a smaller value than the LAN MTU. # Default = 1024 @@ -1480,6 +1494,11 @@ edges: no_read_records_sleep: "${EDGES_NO_READ_RECORDS_SLEEP:1000}" # Number of milliseconds to wait before resending failed batch of edge events to edge sleep_between_batches: "${EDGES_SLEEP_BETWEEN_BATCHES:60000}" + # Time (in milliseconds) to subtract from the start timestamp when fetching edge events. + # This compensates for possible misordering between `created_time` (used for partitioning) + # and `seqId` (used for sorting). Without this, events with smaller seqId but larger created_time + # might be skipped, especially across partition boundaries. + misordering_compensation_millis: "${EDGES_MISORDERING_COMPENSATION_MILLIS:60000}" # Max number of high priority edge events per edge session. No persistence - stored in memory max_high_priority_queue_size_per_session: "${EDGES_MAX_HIGH_PRIORITY_QUEUE_SIZE_PER_SESSION:10000}" # Number of threads that are used to check DB for edge events @@ -1534,6 +1553,8 @@ swagger: version: "${SWAGGER_VERSION:}" # The group name (definition) on the API doc UI page. group_name: "${SWAGGER_GROUP_NAME:thingsboard}" + # Control the initial display state of API operations and tags (none or list) + doc_expansion: "${SWAGGER_DOC_EXPANSION:list}" # Queue configuration parameters queue: diff --git a/application/src/test/java/org/thingsboard/server/cf/AlarmRulesTest.java b/application/src/test/java/org/thingsboard/server/cf/AlarmRulesTest.java new file mode 100644 index 0000000000..2ac5d59b3a --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/cf/AlarmRulesTest.java @@ -0,0 +1,908 @@ +/** + * Copyright © 2016-2025 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.cf; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import org.junit.Before; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.bean.override.mockito.MockitoSpyBean; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.rule.engine.action.TbAlarmResult; +import org.thingsboard.server.actors.ActorSystemContext; +import org.thingsboard.server.common.data.AttributeScope; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.alarm.Alarm; +import org.thingsboard.server.common.data.alarm.AlarmInfo; +import org.thingsboard.server.common.data.alarm.AlarmSeverity; +import org.thingsboard.server.common.data.alarm.AlarmStatus; +import org.thingsboard.server.common.data.alarm.rule.AlarmRule; +import org.thingsboard.server.common.data.alarm.rule.condition.AlarmConditionValue; +import org.thingsboard.server.common.data.alarm.rule.condition.DurationAlarmCondition; +import org.thingsboard.server.common.data.alarm.rule.condition.RepeatingAlarmCondition; +import org.thingsboard.server.common.data.alarm.rule.condition.SimpleAlarmCondition; +import org.thingsboard.server.common.data.alarm.rule.condition.expression.AlarmConditionExpression; +import org.thingsboard.server.common.data.alarm.rule.condition.expression.AlarmConditionFilter; +import org.thingsboard.server.common.data.alarm.rule.condition.expression.ComplexOperation; +import org.thingsboard.server.common.data.alarm.rule.condition.expression.SimpleAlarmConditionExpression; +import org.thingsboard.server.common.data.alarm.rule.condition.expression.TbelAlarmConditionExpression; +import org.thingsboard.server.common.data.alarm.rule.condition.expression.predicate.NumericFilterPredicate; +import org.thingsboard.server.common.data.alarm.rule.condition.expression.predicate.NumericFilterPredicate.NumericOperation; +import org.thingsboard.server.common.data.alarm.rule.condition.expression.predicate.StringFilterPredicate; +import org.thingsboard.server.common.data.alarm.rule.condition.expression.predicate.StringFilterPredicate.StringOperation; +import org.thingsboard.server.common.data.alarm.rule.condition.schedule.AlarmSchedule; +import org.thingsboard.server.common.data.alarm.rule.condition.schedule.SpecificTimeSchedule; +import org.thingsboard.server.common.data.cf.CalculatedField; +import org.thingsboard.server.common.data.cf.CalculatedFieldType; +import org.thingsboard.server.common.data.cf.configuration.AlarmCalculatedFieldConfiguration; +import org.thingsboard.server.common.data.cf.configuration.Argument; +import org.thingsboard.server.common.data.cf.configuration.ArgumentType; +import org.thingsboard.server.common.data.cf.configuration.CurrentOwnerDynamicSourceConfiguration; +import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey; +import org.thingsboard.server.common.data.debug.DebugSettings; +import org.thingsboard.server.common.data.event.CalculatedFieldDebugEvent; +import org.thingsboard.server.common.data.event.EventType; +import org.thingsboard.server.common.data.id.CalculatedFieldId; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.EventId; +import org.thingsboard.server.common.data.query.EntityKeyValueType; +import org.thingsboard.server.controller.AbstractControllerTest; +import org.thingsboard.server.dao.event.EventDao; +import org.thingsboard.server.dao.service.DaoSqlTest; + +import java.time.Duration; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.temporal.ChronoUnit; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; +import java.util.function.Predicate; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.testcontainers.shaded.org.awaitility.Awaitility.await; + +@Slf4j +@DaoSqlTest +@TestPropertySource(properties = { + "actors.calculated_fields.check_interval=1", + "actors.alarms.reevaluation_interval=1" +}) +public class AlarmRulesTest extends AbstractControllerTest { + + @MockitoSpyBean + private ActorSystemContext actorSystemContext; + + @Autowired + private EventDao eventDao; + + private Device device; + private DeviceId deviceId; + private EntityId originatorId; + private EventId latestEventId; + + @Before + public void beforeEach() throws Exception { + loginTenantAdmin(); + device = createDevice("Device A", "aaa"); + deviceId = device.getId(); + originatorId = deviceId; + } + + @Test + public void testCreateAlarm_severityUpdate_clear() throws Exception { + Argument temperatureArgument = new Argument(); + temperatureArgument.setRefEntityKey(new ReferencedEntityKey("temperature", ArgumentType.TS_LATEST, null)); + temperatureArgument.setDefaultValue("0"); + Map arguments = Map.of( + "temperature", temperatureArgument + ); + + Map createRules = Map.of( + AlarmSeverity.MAJOR, new Condition("return temperature >= 50;", null, null), + AlarmSeverity.CRITICAL, new Condition("return temperature >= 100;", null, null) + ); + + Condition clearRule = new Condition("return temperature <= 25;", null, null); + CalculatedField calculatedField = createAlarmCf(deviceId, "High Temperature Alarm", + arguments, createRules, clearRule); + + postTelemetry(deviceId, "{\"temperature\":50}"); + checkAlarmResult(calculatedField, alarmResult -> { + assertThat(alarmResult.isCreated()).isTrue(); + assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.MAJOR); + assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK); + }); + + postTelemetry(deviceId, "{\"temperature\":100}"); + checkAlarmResult(calculatedField, alarmResult -> { + assertThat(alarmResult.isSeverityUpdated()).isTrue(); + assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.CRITICAL); + assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK); + }); + + postTelemetry(deviceId, "{\"temperature\":101}"); + checkAlarmResult(calculatedField, alarmResult -> { + assertThat(alarmResult.isUpdated()).isTrue(); + assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.CRITICAL); + assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK); + }); + + postTelemetry(deviceId, "{\"temperature\":20}"); + checkAlarmResult(calculatedField, alarmResult -> { + assertThat(alarmResult.isCleared()).isTrue(); + assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.CRITICAL); + assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.CLEARED_UNACK); + }); + } + + @Test + public void testCreateAlarm_simpleConditionExpression() throws Exception { + Argument temperatureArgument = new Argument(); + temperatureArgument.setRefEntityKey(new ReferencedEntityKey("temperature", ArgumentType.TS_LATEST, null)); + temperatureArgument.setDefaultValue("0"); + Map arguments = Map.of( + "temperature", temperatureArgument + ); + + SimpleAlarmConditionExpression simpleExpression = new SimpleAlarmConditionExpression(); + AlarmConditionFilter filter = new AlarmConditionFilter(); + filter.setArgument("temperature"); + filter.setValueType(EntityKeyValueType.NUMERIC); + NumericFilterPredicate predicate = new NumericFilterPredicate(); + predicate.setOperation(NumericOperation.GREATER_OR_EQUAL); + AlarmConditionValue thresholdValue = new AlarmConditionValue<>(); + thresholdValue.setStaticValue(100.0); + predicate.setValue(thresholdValue); + filter.setPredicates(List.of(predicate)); + simpleExpression.setFilters(List.of(filter)); + simpleExpression.setOperation(ComplexOperation.AND); + Map createRules = Map.of( + AlarmSeverity.CRITICAL, new Condition(simpleExpression, null, null) + ); + + CalculatedField calculatedField = createAlarmCf(deviceId, "High Temperature Alarm", + arguments, createRules, null); + + postTelemetry(deviceId, "{\"temperature\":100}"); + checkAlarmResult(calculatedField, alarmResult -> { + assertThat(alarmResult.isCreated()).isTrue(); + assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.CRITICAL); + assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK); + }); + } + + @Test + public void testCreateAlarm_repeatingCondition() throws Exception { + Argument temperatureArgument = new Argument(); + temperatureArgument.setRefEntityKey(new ReferencedEntityKey("temperature", ArgumentType.TS_LATEST, null)); + temperatureArgument.setDefaultValue("0"); + Map arguments = Map.of( + "temperature", temperatureArgument + ); + + int eventsCountMajor = 5; + int eventsCountCritical = 10; + Map createRules = Map.of( + AlarmSeverity.MAJOR, new Condition("return temperature >= 50;", eventsCountMajor, null), + AlarmSeverity.CRITICAL, new Condition("return temperature >= 50;", eventsCountCritical, null) + ); + + CalculatedField calculatedField = createAlarmCf(deviceId, "High Temperature Alarm", + arguments, createRules, null); + for (int i = 0; i < 4; i++) { + postTelemetry(deviceId, "{\"temperature\":50}"); + Thread.sleep(10); + } + assertThat(getLatestAlarmResult(calculatedField.getId())).isNull(); + postTelemetry(deviceId, "{\"temperature\":50}"); + checkAlarmResult(calculatedField, alarmResult -> { + assertThat(alarmResult.isCreated()).isTrue(); + assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.MAJOR); + assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK); + assertThat(alarmResult.getConditionRepeats()).isEqualTo(5); + }); + + for (int i = 0; i < 4; i++) { + postTelemetry(deviceId, "{\"temperature\":50}"); + Thread.sleep(10); + } + checkAlarmResult(calculatedField, alarmResult -> alarmResult.getConditionRepeats() == 9, alarmResult -> { + assertThat(alarmResult.isUpdated()).isTrue(); + assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.MAJOR); + }); + postTelemetry(deviceId, "{\"temperature\":50}"); + checkAlarmResult(calculatedField, alarmResult -> { + assertThat(alarmResult.isSeverityUpdated()).isTrue(); + assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.CRITICAL); + assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK); + assertThat(alarmResult.getConditionRepeats()).isEqualTo(10); + }); + } + + @Test + public void testCreateAlarm_dynamicRepeatingCondition() throws Exception { + Argument temperatureArgument = new Argument(); + temperatureArgument.setRefEntityKey(new ReferencedEntityKey("temperature", ArgumentType.TS_LATEST, null)); + temperatureArgument.setDefaultValue("0"); + + Argument eventsCountArgument = new Argument(); + eventsCountArgument.setRefEntityKey(new ReferencedEntityKey("eventsCount", ArgumentType.ATTRIBUTE, AttributeScope.SERVER_SCOPE)); + eventsCountArgument.setDefaultValue("0"); + Map arguments = Map.of( + "temperature", temperatureArgument, + "eventsCount", eventsCountArgument + ); + + int eventsCount = 5; + Map createRules = Map.of( + AlarmSeverity.CRITICAL, new Condition("return temperature >= 50;", null, + new AlarmConditionValue<>(null, "eventsCount"), null, null) + ); + + CalculatedField calculatedField = createAlarmCf(deviceId, "High Temperature Alarm", + arguments, createRules, null); + postAttributes(deviceId, AttributeScope.SERVER_SCOPE, "{\"eventsCount\":" + eventsCount + "}"); + for (int i = 0; i < eventsCount; i++) { + postTelemetry(deviceId, "{\"temperature\":50}"); + Thread.sleep(10); + } + checkAlarmResult(calculatedField, alarmResult -> { + assertThat(alarmResult.isCreated()).isTrue(); + assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.CRITICAL); + assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK); + assertThat(alarmResult.getConditionRepeats()).isEqualTo(eventsCount); + }); + } + + @Test + public void testCreateAlarm_durationCondition() throws Exception { + Argument argument = new Argument(); + argument.setRefEntityKey(new ReferencedEntityKey("powerConsumption", ArgumentType.TS_LATEST, null)); + argument.setDefaultValue("0"); + Map arguments = Map.of( + "powerConsumption", argument + ); + + long createDurationMs = 5000L; + long clearDurationMs = 3000L; + Map createRules = Map.of( + AlarmSeverity.CRITICAL, new Condition("return powerConsumption >= 3000;", null, createDurationMs) + ); + Condition clearRule = new Condition("return powerConsumption < 3000;", null, clearDurationMs); + + CalculatedField calculatedField = createAlarmCf(deviceId, "High power consumption during 5 seconds", + arguments, createRules, clearRule); + postTelemetry(deviceId, "{\"powerConsumption\":3500}"); + Thread.sleep(createDurationMs - 2000); + assertThat(getLatestAlarmResult(calculatedField.getId())).isNull(); + + checkAlarmResult(calculatedField, alarmResult -> { + assertThat(alarmResult.isCreated()).isTrue(); + assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.CRITICAL); + assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK); + assertThat(alarmResult.getConditionDuration()).isBetween(createDurationMs, createDurationMs + 2000); + }); + + postTelemetry(deviceId, "{\"powerConsumption\":2000}"); + Thread.sleep(clearDurationMs - 2000); + assertThat(getLatestAlarmResult(calculatedField.getId())).isNull(); + + checkAlarmResult(calculatedField, alarmResult -> { + assertThat(alarmResult.isCleared()).isTrue(); + assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.CLEARED_UNACK); + assertThat(alarmResult.getConditionDuration()).isBetween(clearDurationMs, clearDurationMs + 2000); + }); + } + + @Test + public void testCreateAlarm_dynamicDurationCondition() throws Exception { + Argument powerConsumptionArgument = new Argument(); + powerConsumptionArgument.setRefEntityKey(new ReferencedEntityKey("powerConsumption", ArgumentType.TS_LATEST, null)); + powerConsumptionArgument.setDefaultValue("0"); + + Argument durationArgument = new Argument(); + durationArgument.setRefEntityKey(new ReferencedEntityKey("duration", ArgumentType.ATTRIBUTE, AttributeScope.SERVER_SCOPE)); + durationArgument.setDefaultValue("-1"); + Map arguments = Map.of( + "powerConsumption", powerConsumptionArgument, + "duration", durationArgument + ); + + long createDurationMs = 2000L; + Map createRules = Map.of( + AlarmSeverity.CRITICAL, new Condition("return powerConsumption >= 3000;", null, null, + new AlarmConditionValue(null, "duration"), null) + ); + + CalculatedField calculatedField = createAlarmCf(deviceId, "High power consumption during 2 seconds", + arguments, createRules, null); + postTelemetry(deviceId, "{\"powerConsumption\":3500}"); + postAttributes(deviceId, AttributeScope.SERVER_SCOPE, "{\"duration\":" + createDurationMs + "}"); + + checkAlarmResult(calculatedField, alarmResult -> { + assertThat(alarmResult.isCreated()).isTrue(); + assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.CRITICAL); + assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK); + assertThat(alarmResult.getConditionDuration()).isBetween(createDurationMs, createDurationMs + 2000); + }); + } + + @Test + public void testCreateAlarm_currentOwnerArgument() throws Exception { + Argument temperatureArgument = new Argument(); + temperatureArgument.setRefEntityKey(new ReferencedEntityKey("temperature", ArgumentType.TS_LATEST, null)); + temperatureArgument.setDefaultValue("0"); + + Argument temperatureThresholdArgument = new Argument(); + temperatureThresholdArgument.setRefEntityKey(new ReferencedEntityKey("temperatureThreshold", ArgumentType.ATTRIBUTE, AttributeScope.SERVER_SCOPE)); + temperatureThresholdArgument.setRefDynamicSourceConfiguration(new CurrentOwnerDynamicSourceConfiguration()); + temperatureThresholdArgument.setDefaultValue("1000"); + + Map arguments = Map.of( + "temperature", temperatureArgument, + "temperatureThreshold", temperatureThresholdArgument + ); + + Map createRules = Map.of( + AlarmSeverity.CRITICAL, new Condition("return temperature >= temperatureThreshold;", null, null) + ); + + device.setCustomerId(customerId); + device = doPost("/api/device", device, Device.class); + CalculatedField calculatedField = createAlarmCf(deviceId, "High Temperature Alarm", + arguments, createRules, null); + postAttributes(customerId, AttributeScope.SERVER_SCOPE, "{\"temperatureThreshold\":50}"); + + postTelemetry(deviceId, "{\"temperature\":51}"); + checkAlarmResult(calculatedField, alarmResult -> { + assertThat(alarmResult.isCreated()).isTrue(); + assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.CRITICAL); + assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK); + }); + } + + @Test + public void testCreateAndClearAlarm_customerAlarmRule_simpleExpression() throws Exception { + Argument locationArgument = new Argument(); + locationArgument.setRefEntityKey(new ReferencedEntityKey("location", ArgumentType.ATTRIBUTE, AttributeScope.SERVER_SCOPE)); + locationArgument.setDefaultValue("unknown"); + originatorId = customerId; + + Argument locationFilterArgument = new Argument(); + locationFilterArgument.setRefEntityKey(new ReferencedEntityKey("locationFilter", ArgumentType.ATTRIBUTE, AttributeScope.SERVER_SCOPE)); + locationFilterArgument.setRefDynamicSourceConfiguration(new CurrentOwnerDynamicSourceConfiguration()); + locationFilterArgument.setDefaultValue("None"); + + Map arguments = Map.of( + "location", locationArgument, + "locationFilter", locationFilterArgument + ); + + Map createRules = Map.of( + AlarmSeverity.INDETERMINATE, new Condition(createSimpleExpression( + "location", StringOperation.CONTAINS, new AlarmConditionValue<>(null, "locationFilter") + ), null, null) + ); + Condition clearRule = new Condition(createSimpleExpression( + "location", StringOperation.NOT_CONTAINS, new AlarmConditionValue<>(null, "locationFilter") + ), null, null); + + CalculatedField calculatedField = createAlarmCf(customerId, "New resident", + arguments, createRules, clearRule); + + loginSysAdmin(); + postAttributes(tenantId, AttributeScope.SERVER_SCOPE, "{\"locationFilter\":\"Kyiv\"}"); + loginTenantAdmin(); + postAttributes(customerId, AttributeScope.SERVER_SCOPE, "{\"location\":\"Ukraine, Kyiv\"}"); + checkAlarmResult(calculatedField, alarmResult -> { + assertThat(alarmResult.isCreated()).isTrue(); + assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.INDETERMINATE); + assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK); + }); + + postAttributes(customerId, AttributeScope.SERVER_SCOPE, "{\"location\":\"Ukraine, Lviv\"}"); + checkAlarmResult(calculatedField, alarmResult -> { + assertThat(alarmResult.isCleared()).isTrue(); + assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.INDETERMINATE); + assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.CLEARED_UNACK); + }); + } + + @Test + public void testCreateAlarm_dynamicSchedule() throws Exception { + Argument temperatureArgument = new Argument(); + temperatureArgument.setRefEntityKey(new ReferencedEntityKey("temperature", ArgumentType.TS_LATEST, null)); + temperatureArgument.setDefaultValue("0"); + Argument scheduleArgument = new Argument(); + scheduleArgument.setRefEntityKey(new ReferencedEntityKey("schedule", ArgumentType.ATTRIBUTE, AttributeScope.SERVER_SCOPE)); + scheduleArgument.setDefaultValue("None"); + Map arguments = Map.of( + "temperature", temperatureArgument, + "schedule", scheduleArgument + ); + + Map createRules = Map.of( + AlarmSeverity.CRITICAL, new Condition("return temperature >= 50;", null, null, null, + new AlarmConditionValue<>(null, "schedule")) + ); + + CalculatedField calculatedField = createAlarmCf(deviceId, "High Temperature Alarm", + arguments, createRules, null); + String schedule = """ + {"timezone":"Europe/Kiev","items":[{"enabled":false,"dayOfWeek":1,"startsOn":0,"endsOn":0},{"enabled":false,"dayOfWeek":2,"startsOn":0,"endsOn":0},{"enabled":false,"dayOfWeek":3,"startsOn":0,"endsOn":0},{"enabled":false,"dayOfWeek":4,"startsOn":0,"endsOn":0},{"enabled":false,"dayOfWeek":5,"startsOn":0,"endsOn":0},{"enabled":false,"dayOfWeek":6,"startsOn":0,"endsOn":0},{"enabled":false,"dayOfWeek":7,"startsOn":0,"endsOn":0}]} + """; + postAttributes(deviceId, AttributeScope.SERVER_SCOPE, "{\"schedule\":" + schedule + "}"); + postTelemetry(deviceId, "{\"temperature\":50}"); + + Thread.sleep(1000); + assertThat(getLatestAlarmResult(calculatedField.getId())).isNull(); + + schedule = schedule.replace("\"enabled\":false", "\"enabled\":true"); + postAttributes(deviceId, AttributeScope.SERVER_SCOPE, "{\"schedule\":" + schedule + "}"); + + await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> { + // checking multiple debug events due to scheduled reevaluation (which also produces debug events) + CalculatedFieldDebugEvent debugEvent = getDebugEvents(calculatedField.getId(), 5).stream() + .filter(event -> event.getResult() != null) + .findFirst().orElse(null); + assertThat(debugEvent).isNotNull(); + TbAlarmResult alarmResult = JacksonUtil.fromString(debugEvent.getResult(), TbAlarmResult.class); + assertThat(alarmResult.isCreated()).isTrue(); + assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.CRITICAL); + assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK); + }); + } + + @Test + public void testChangeAlarmType() throws Exception { + Argument temperatureArgument = new Argument(); + temperatureArgument.setRefEntityKey(new ReferencedEntityKey("temperature", ArgumentType.TS_LATEST, null)); + temperatureArgument.setDefaultValue("0"); + Map arguments = Map.of( + "temperature", temperatureArgument + ); + + Map createRules = Map.of( + AlarmSeverity.CRITICAL, new Condition("return temperature >= 50;", null, null) + ); + + CalculatedField calculatedField = createAlarmCf(deviceId, "High Temperature Alarm", + arguments, createRules, null); + + postTelemetry(deviceId, "{\"temperature\":50}"); + checkAlarmResult(calculatedField, alarmResult -> { + assertThat(alarmResult.isCreated()).isTrue(); + assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.CRITICAL); + assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK); + }); + + calculatedField.setName("New alarm type"); + calculatedField = saveCalculatedField(calculatedField); + checkAlarmResult(calculatedField, alarmResult -> { + assertThat(alarmResult.isCreated()).isTrue(); + assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.CRITICAL); + assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK); + }); + } + + @Test + public void testChangeRuleExpression() throws Exception { + Argument temperatureArgument = new Argument(); + temperatureArgument.setRefEntityKey(new ReferencedEntityKey("temperature", ArgumentType.TS_LATEST, null)); + temperatureArgument.setDefaultValue("0"); + Map arguments = Map.of( + "temperature", temperatureArgument + ); + + Map createRules = Map.of( + AlarmSeverity.CRITICAL, new Condition("return temperature >= 100;", null, null) + ); + + CalculatedField calculatedField = createAlarmCf(deviceId, "High Temperature Alarm", + arguments, createRules, null); + + postTelemetry(deviceId, "{\"temperature\":50}"); + Thread.sleep(1000); + assertThat(getLatestAlarmResult(calculatedField.getId())).isNull(); + + AlarmCalculatedFieldConfiguration configuration = (AlarmCalculatedFieldConfiguration) calculatedField.getConfiguration(); + ((TbelAlarmConditionExpression) configuration.getCreateRules().get(AlarmSeverity.CRITICAL).getCondition().getExpression()) + .setExpression("return temperature >= 50;"); + calculatedField = saveCalculatedField(calculatedField); + checkAlarmResult(calculatedField, alarmResult -> { + assertThat(alarmResult.isCreated()).isTrue(); + assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.CRITICAL); + assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK); + }); + } + + @Test + public void testChangeRequiredEventsCountForRepeatingCondition() throws Exception { + Argument temperatureArgument = new Argument(); + temperatureArgument.setRefEntityKey(new ReferencedEntityKey("temperature", ArgumentType.TS_LATEST, null)); + temperatureArgument.setDefaultValue("0"); + Map arguments = Map.of( + "temperature", temperatureArgument + ); + + int eventsCountMajor = 5; + int eventsCountCritical = 10; + Map createRules = Map.of( + AlarmSeverity.MAJOR, new Condition("return temperature >= 50;", eventsCountMajor, null), + AlarmSeverity.CRITICAL, new Condition("return temperature >= 50;", eventsCountCritical, null) + ); + + CalculatedField calculatedField = createAlarmCf(deviceId, "High Temperature Alarm", + arguments, createRules, null); + for (int i = 0; i < eventsCountMajor; i++) { + postTelemetry(deviceId, "{\"temperature\":50}"); + Thread.sleep(10); + } + checkAlarmResult(calculatedField, alarmResult -> { + assertThat(alarmResult.isCreated()).isTrue(); + assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.MAJOR); + assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK); + assertThat(alarmResult.getConditionRepeats()).isEqualTo(5); + }); + + postTelemetry(deviceId, "{\"temperature\":50}"); + checkAlarmResult(calculatedField, alarmResult -> { + assertThat(alarmResult.isUpdated()).isTrue(); + assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.MAJOR); + assertThat(alarmResult.getConditionRepeats()).isEqualTo(6); + }); + + // decreasing required events count for critical rule + AlarmCalculatedFieldConfiguration configuration = (AlarmCalculatedFieldConfiguration) calculatedField.getConfiguration(); + ((RepeatingAlarmCondition) configuration.getCreateRules().get(AlarmSeverity.CRITICAL).getCondition()) + .setCount(new AlarmConditionValue<>(6, null)); + calculatedField = saveCalculatedField(calculatedField); + checkAlarmResult(calculatedField, alarmResult -> { + assertThat(alarmResult.isSeverityUpdated()).isTrue(); + assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.CRITICAL); + assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK); + assertThat(alarmResult.getConditionRepeats()).isEqualTo(6); + }); + } + + @Test + public void testChangeConditionArgumentSource() throws Exception { + Argument temperatureArgument = new Argument(); + temperatureArgument.setRefEntityKey(new ReferencedEntityKey("temperature", ArgumentType.TS_LATEST, null)); + temperatureArgument.setDefaultValue("0"); + + Argument temperatureThresholdArgument = new Argument(); + temperatureThresholdArgument.setRefEntityKey(new ReferencedEntityKey("temperatureThreshold", ArgumentType.ATTRIBUTE, AttributeScope.SERVER_SCOPE)); + temperatureThresholdArgument.setRefDynamicSourceConfiguration(new CurrentOwnerDynamicSourceConfiguration()); + temperatureThresholdArgument.setDefaultValue("100"); + loginSysAdmin(); + postAttributes(tenantId, AttributeScope.SERVER_SCOPE, "{\"temperatureThreshold\":100}"); + loginTenantAdmin(); + postAttributes(deviceId, AttributeScope.SERVER_SCOPE, "{\"temperatureThreshold\":50}"); + + Map arguments = Map.of( + "temperature", temperatureArgument, + "temperatureThreshold", temperatureThresholdArgument + ); + + Map createRules = Map.of( + AlarmSeverity.CRITICAL, new Condition("return temperature >= temperatureThreshold;", null, null) + ); + + CalculatedField calculatedField = createAlarmCf(deviceId, "High Temperature Alarm", + arguments, createRules, null); + + postTelemetry(deviceId, "{\"temperature\":50}"); + Thread.sleep(1000); + // not created because tenant's threshold 100 is used + assertThat(getLatestAlarmResult(calculatedField.getId())).isNull(); + + ((AlarmCalculatedFieldConfiguration) calculatedField.getConfiguration()).getArguments().get("temperatureThreshold") + .setRefDynamicSourceConfiguration(null); + // using threshold 50 on device level + calculatedField = saveCalculatedField(calculatedField); + + checkAlarmResult(calculatedField, alarmResult -> { + assertThat(alarmResult.isCreated()).isTrue(); + assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.CRITICAL); + assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK); + }); + } + + @Test + public void testAlarmDetails() throws Exception { + Argument temperatureArgument = new Argument(); + temperatureArgument.setRefEntityKey(new ReferencedEntityKey("temperature", ArgumentType.TS_LATEST, null)); + temperatureArgument.setDefaultValue("0"); + Argument humidityArgument = new Argument(); + humidityArgument.setRefEntityKey(new ReferencedEntityKey("humidity", ArgumentType.ATTRIBUTE, AttributeScope.SERVER_SCOPE)); + humidityArgument.setDefaultValue("0"); + Map arguments = Map.of( + "temperature", temperatureArgument, + "humidity", humidityArgument + ); + + Map createRules = Map.of( + AlarmSeverity.CRITICAL, new Condition("return temperature >= 50 && humidity >= 50;", null, null) + ); + CalculatedField calculatedField = createAlarmCf(deviceId, "High Temperature and Humidity Alarm", + arguments, createRules, null, configuration -> { + configuration.getCreateRules().get(AlarmSeverity.CRITICAL).setAlarmDetails( + "temperature is ${temperature}, humidity is ${humidity}" + ); + }); + + postTelemetry(deviceId, "{\"temperature\":50}"); + postAttributes(deviceId, AttributeScope.SERVER_SCOPE, "{\"humidity\":50}"); + + checkAlarmResult(calculatedField, alarmResult -> { + assertThat(alarmResult.isCreated()).isTrue(); + assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.CRITICAL); + assertThat(alarmResult.getAlarm().getDetails().get("data").asText()) + .isEqualTo("temperature is 50, humidity is 50"); + }); + + ((AlarmCalculatedFieldConfiguration) calculatedField.getConfiguration()).getCreateRules().get(AlarmSeverity.CRITICAL).setAlarmDetails( + "UPDATED temperature is ${temperature}, humidity is ${humidity}" + ); + calculatedField = saveCalculatedField(calculatedField); + checkAlarmResult(calculatedField, alarmResult -> { + assertThat(alarmResult.isCreated()).isFalse(); + assertThat(alarmResult.isUpdated()).isTrue(); + assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.CRITICAL); + assertThat(alarmResult.getAlarm().getDetails().get("data").asText()) + .isEqualTo("UPDATED temperature is 50, humidity is 50"); + }); + } + + @Test + public void testCreateAlarm_scheduleStarted() throws Exception { + Argument parkingSpotOccupiedArgument = new Argument(); + parkingSpotOccupiedArgument.setRefEntityKey(new ReferencedEntityKey("parkingSpotOccupied", ArgumentType.ATTRIBUTE, AttributeScope.SERVER_SCOPE)); + parkingSpotOccupiedArgument.setDefaultValue("false"); + Map arguments = Map.of( + "parkingSpotOccupied", parkingSpotOccupiedArgument + ); + + SpecificTimeSchedule schedule = new SpecificTimeSchedule(); + schedule.setTimezone(ZoneId.systemDefault().getId()); + schedule.setDaysOfWeek(Set.of(1, 2, 3, 4, 5, 6, 7)); + long startsOn = Duration.between(LocalDate.now().atStartOfDay(), LocalDateTime.now()) + .plus(15, ChronoUnit.SECONDS).toMillis(); + schedule.setStartsOn(startsOn); + Map createRules = Map.of( + AlarmSeverity.CRITICAL, new Condition("return parkingSpotOccupied == true;", null, null, null, + new AlarmConditionValue<>(schedule, null)) + ); + + CalculatedField calculatedField = createAlarmCf(deviceId, "Illegal parking alarm", + arguments, createRules, null); + + postAttributes(deviceId, AttributeScope.SERVER_SCOPE, "{\"parkingSpotOccupied\":true}"); + + Thread.sleep(10000); + assertThat(getLatestAlarmResult(calculatedField.getId())).isNull(); + + await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> { + CalculatedFieldDebugEvent debugEvent = getDebugEvents(calculatedField.getId(), 5).stream() + .filter(event -> event.getResult() != null) + .findFirst().orElse(null); + assertThat(debugEvent).isNotNull(); + TbAlarmResult alarmResult = JacksonUtil.fromString(debugEvent.getResult(), TbAlarmResult.class); + assertThat(alarmResult.isCreated()).isTrue(); + assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.CRITICAL); + assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK); + }); + } + + @Test + public void testManualClearAlarm() throws Exception { + Argument temperatureArgument = new Argument(); + temperatureArgument.setRefEntityKey(new ReferencedEntityKey("temperature", ArgumentType.TS_LATEST, null)); + temperatureArgument.setDefaultValue("0"); + Map arguments = Map.of( + "temperature", temperatureArgument + ); + + Map createRules = Map.of( + AlarmSeverity.CRITICAL, new Condition("return temperature >= 50;", null, null) + ); + + CalculatedField calculatedField = createAlarmCf(deviceId, "High Temperature Alarm", + arguments, createRules, null); + + postTelemetry(deviceId, "{\"temperature\":50}"); + Alarm alarm = checkAlarmResult(calculatedField, alarmResult -> { + assertThat(alarmResult.isCreated()).isTrue(); + assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.CRITICAL); + assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK); + }).getAlarm(); + + doPost("/api/alarm/" + alarm.getId() + "/clear", AlarmInfo.class); + Thread.sleep(1000); + postTelemetry(deviceId, "{\"temperature\":50}"); + checkAlarmResult(calculatedField, alarmResult -> { + assertThat(alarmResult.getAlarm().getId()).isNotEqualTo(alarm.getId()); + assertThat(alarmResult.isCreated()).isTrue(); + assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.CRITICAL); + assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK); + }); + } + + // TODO: MSA tests + + private TbAlarmResult checkAlarmResult(CalculatedField calculatedField, Consumer assertion) { + return checkAlarmResult(calculatedField, null, assertion); + } + + private TbAlarmResult checkAlarmResult(CalculatedField calculatedField, + Predicate waitFor, + Consumer assertion) { + TbAlarmResult alarmResult = await().atMost(TIMEOUT, TimeUnit.SECONDS) + .until(() -> getLatestAlarmResult(calculatedField.getId()), result -> + result != null && (waitFor == null || waitFor.test(result))); + assertion.accept(alarmResult); + + Alarm alarm = alarmResult.getAlarm(); + assertThat(alarm.getOriginator()).isEqualTo(originatorId); + assertThat(alarm.getType()).isEqualTo(calculatedField.getName()); + return alarmResult; + } + + private TbAlarmResult getLatestAlarmResult(CalculatedFieldId calculatedFieldId) { + List debugEvents = getDebugEvents(calculatedFieldId, 1); + if (debugEvents.isEmpty()) { + return null; + } + CalculatedFieldDebugEvent debugEvent = debugEvents.get(0); + if (debugEvent.getError() != null) { + throw new RuntimeException(debugEvent.getError()); + } + if (debugEvent.getId().equals(latestEventId)) { + return null; + } + latestEventId = debugEvent.getId(); + return JacksonUtil.fromString(debugEvent.getResult(), TbAlarmResult.class); + } + + private CalculatedField createAlarmCf(EntityId entityId, + String alarmType, + Map arguments, + Map createConditions, + Condition clearCondition, + Consumer... modifier) { + Map createRules = new HashMap<>(); + createConditions.forEach((severity, condition) -> { + createRules.put(severity, toAlarmRule(condition)); + }); + AlarmRule clearRule = clearCondition != null ? toAlarmRule(clearCondition) : null; + + CalculatedField calculatedField = new CalculatedField(); + calculatedField.setEntityId(entityId); + calculatedField.setName(alarmType); + calculatedField.setType(CalculatedFieldType.ALARM); + AlarmCalculatedFieldConfiguration configuration = new AlarmCalculatedFieldConfiguration(); + configuration.setArguments(arguments); + configuration.setCreateRules(createRules); + configuration.setClearRule(clearRule); + calculatedField.setConfiguration(configuration); + calculatedField.setDebugSettings(DebugSettings.all()); + if (modifier.length > 0) { + modifier[0].accept(configuration); + } + CalculatedField savedCalculatedField = saveCalculatedField(calculatedField); + + CalculatedFieldDebugEvent debugEvent = await().atMost(TIMEOUT, TimeUnit.SECONDS) + .until(() -> getDebugEvents(savedCalculatedField.getId(), 1), + events -> !events.isEmpty()).get(0); + latestEventId = debugEvent.getId(); + return savedCalculatedField; + } + + private AlarmRule toAlarmRule(Condition condition) { + AlarmRule rule = new AlarmRule(); + AlarmConditionExpression expression; + if (condition.getTbelExpression() != null) { + TbelAlarmConditionExpression tbelExpression = new TbelAlarmConditionExpression(); + tbelExpression.setExpression(condition.getTbelExpression()); + expression = tbelExpression; + } else { + expression = condition.getSimpleExpression(); + } + if (condition.getEventsCount() != null) { + RepeatingAlarmCondition alarmCondition = new RepeatingAlarmCondition(); + alarmCondition.setExpression(expression); + alarmCondition.setCount(condition.getEventsCount()); + rule.setCondition(alarmCondition); + } else if (condition.getDuration() != null) { + DurationAlarmCondition alarmCondition = new DurationAlarmCondition(); + alarmCondition.setExpression(expression); + alarmCondition.setUnit(TimeUnit.MILLISECONDS); + alarmCondition.setValue(condition.getDuration()); + rule.setCondition(alarmCondition); + } else { + SimpleAlarmCondition alarmCondition = new SimpleAlarmCondition(); + alarmCondition.setExpression(expression); + rule.setCondition(alarmCondition); + } + if (condition.getSchedule() != null) { + rule.getCondition().setSchedule(condition.getSchedule()); + } + return rule; + } + + private SimpleAlarmConditionExpression createSimpleExpression(String argument, StringOperation stringOperation, AlarmConditionValue conditionValue) { + SimpleAlarmConditionExpression simpleExpression = new SimpleAlarmConditionExpression(); + AlarmConditionFilter filter = new AlarmConditionFilter(); + filter.setArgument(argument); + filter.setValueType(EntityKeyValueType.STRING); + StringFilterPredicate predicate = new StringFilterPredicate(); + predicate.setOperation(stringOperation); + predicate.setValue(conditionValue); + filter.setPredicates(List.of(predicate)); + simpleExpression.setFilters(List.of(filter)); + return simpleExpression; + } + + private List getDebugEvents(CalculatedFieldId calculatedFieldId, int limit) { + return eventDao.findLatestEvents(tenantId.getId(), calculatedFieldId.getId(), EventType.DEBUG_CALCULATED_FIELD, limit).stream() + .map(e -> (CalculatedFieldDebugEvent) e).toList(); + } + + @Getter + @AllArgsConstructor + private static final class Condition { + + private final String tbelExpression; + private final SimpleAlarmConditionExpression simpleExpression; + private AlarmConditionValue eventsCount; + private AlarmConditionValue duration; + private AlarmConditionValue schedule; + + private Condition(String tbelExpression, Integer eventsCount, Long durationMs) { + this.tbelExpression = tbelExpression; + this.simpleExpression = null; + if (eventsCount != null) { + this.eventsCount = new AlarmConditionValue<>(eventsCount, null); + } + if (durationMs != null) { + this.duration = new AlarmConditionValue<>(durationMs, null); + } + } + + private Condition(SimpleAlarmConditionExpression simpleExpression, Integer eventsCount, Long durationMs) { + this.tbelExpression = null; + this.simpleExpression = simpleExpression; + if (eventsCount != null) { + this.eventsCount = new AlarmConditionValue<>(eventsCount, null); + } + if (durationMs != null) { + this.duration = new AlarmConditionValue<>(durationMs, null); + } + } + + } + +} diff --git a/application/src/test/java/org/thingsboard/server/cf/CalculatedFieldCurrentOwnerTest.java b/application/src/test/java/org/thingsboard/server/cf/CalculatedFieldCurrentOwnerTest.java new file mode 100644 index 0000000000..d2f9621064 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/cf/CalculatedFieldCurrentOwnerTest.java @@ -0,0 +1,200 @@ +/** + * Copyright © 2016-2025 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.cf; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import org.junit.Test; +import org.thingsboard.server.common.data.AttributeScope; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.asset.Asset; +import org.thingsboard.server.common.data.asset.AssetProfile; +import org.thingsboard.server.common.data.cf.CalculatedField; +import org.thingsboard.server.common.data.cf.CalculatedFieldType; +import org.thingsboard.server.common.data.cf.configuration.Argument; +import org.thingsboard.server.common.data.cf.configuration.ArgumentType; +import org.thingsboard.server.common.data.cf.configuration.CurrentOwnerDynamicSourceConfiguration; +import org.thingsboard.server.common.data.cf.configuration.Output; +import org.thingsboard.server.common.data.cf.configuration.OutputType; +import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey; +import org.thingsboard.server.common.data.cf.configuration.SimpleCalculatedFieldConfiguration; +import org.thingsboard.server.common.data.id.AssetProfileId; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.controller.AbstractControllerTest; +import org.thingsboard.server.dao.service.DaoSqlTest; + +import java.util.Map; +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 +public class CalculatedFieldCurrentOwnerTest extends AbstractControllerTest { + + public static final int TIMEOUT = 60; + public static final int POLL_INTERVAL = 1; + + @Test + public void testCreateCFWithCurrentOwner() throws Exception { + loginTenantAdmin(); + + postAttributes(customerId, AttributeScope.SERVER_SCOPE, "{\"attrKey\":5}"); + + Device testDevice = createDevice("Test device", "1234567890"); + + doPost("/api/customer/" + customerId.getId() + "/device/" + testDevice.getId().getId()).andExpect(status().isOk()); + + CalculatedField savedCalculatedField = doPost("/api/calculatedField", buildCalculatedField(testDevice.getId()), CalculatedField.class); + + await().alias("create CF -> perform initial calculation").atMost(TIMEOUT, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + ObjectNode fahrenheitTemp = getLatestTelemetry(testDevice.getId(), "result"); + assertThat(fahrenheitTemp).isNotNull(); + assertThat(fahrenheitTemp.get("result").get(0).get("value").asText()).isEqualTo("105"); + }); + + postAttributes(customerId, AttributeScope.SERVER_SCOPE, "{\"attrKey\":10}"); + + await().alias("update telemetry -> perform calculation").atMost(TIMEOUT, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + ObjectNode fahrenheitTemp = getLatestTelemetry(testDevice.getId(), "result"); + assertThat(fahrenheitTemp).isNotNull(); + assertThat(fahrenheitTemp.get("result").get(0).get("value").asText()).isEqualTo("110"); + }); + } + + @Test + public void testChangeOwner() throws Exception { + loginSysAdmin(); + + postAttributes(tenantId, AttributeScope.SERVER_SCOPE, "{\"attrKey\":50}"); + + loginTenantAdmin(); + + postAttributes(customerId, AttributeScope.SERVER_SCOPE, "{\"attrKey\":5}"); + Device testDevice = createDevice("Test device", "1234567890"); + doPost("/api/customer/" + customerId.getId() + "/device/" + testDevice.getId().getId()).andExpect(status().isOk()); + + + CalculatedField savedCalculatedField = doPost("/api/calculatedField", buildCalculatedField(testDevice.getId()), CalculatedField.class); + + await().alias("create CF -> perform initial calculation").atMost(TIMEOUT, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + ObjectNode fahrenheitTemp = getLatestTelemetry(testDevice.getId(), "result"); + assertThat(fahrenheitTemp).isNotNull(); + assertThat(fahrenheitTemp.get("result").get(0).get("value").asText()).isEqualTo("105"); + }); + + doDelete("/api/customer/device/" + testDevice.getId().getId()).andExpect(status().isOk()); + + await().alias("change owner -> perform calculation").atMost(TIMEOUT, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + ObjectNode fahrenheitTemp = getLatestTelemetry(testDevice.getId(), "result"); + assertThat(fahrenheitTemp).isNotNull(); + assertThat(fahrenheitTemp.get("result").get(0).get("value").asText()).isEqualTo("150"); + }); + } + + @Test + public void testCreateCFWithCurrentOwnerWhenEntityIsProfile() throws Exception { + loginSysAdmin(); + + postAttributes(tenantId, AttributeScope.SERVER_SCOPE, "{\"attrKey\":50}"); + + loginTenantAdmin(); + + postAttributes(customerId, AttributeScope.SERVER_SCOPE, "{\"attrKey\":5}"); + + AssetProfile assetProfile = doPost("/api/assetProfile", createAssetProfile("Test Asset Profile"), AssetProfile.class); + + Asset asset1 = createAsset("Test asset 1", assetProfile.getId()); + doPost("/api/customer/" + customerId.getId() + "/asset/" + asset1.getId().getId()).andExpect(status().isOk()); + + Asset asset2 = createAsset("Test asset 2", assetProfile.getId()); // owner - TENANT + + CalculatedField savedCalculatedField = doPost("/api/calculatedField", buildCalculatedField(assetProfile.getId()), CalculatedField.class); + + await().alias("create CF -> perform initial calculation").atMost(TIMEOUT, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + // result of asset 1 + ObjectNode result1 = getLatestTelemetry(asset1.getId(), "result"); + assertThat(result1).isNotNull(); + assertThat(result1.get("result").get(0).get("value").asText()).isEqualTo("105"); + + // result of asset 2 + ObjectNode result2 = getLatestTelemetry(asset2.getId(), "result"); + assertThat(result2).isNotNull(); + assertThat(result2.get("result").get(0).get("value").asText()).isEqualTo("150"); + }); + + doPost("/api/customer/" + customerId.getId() + "/asset/" + asset2.getId().getId()).andExpect(status().isOk()); + + await().alias("change asset2 owner -> recalculate state for asset 2").atMost(TIMEOUT, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + // result of asset 2 + ObjectNode result2 = getLatestTelemetry(asset2.getId(), "result"); + assertThat(result2).isNotNull(); + assertThat(result2.get("result").get(0).get("value").asText()).isEqualTo("105"); + }); + } + + private CalculatedField buildCalculatedField(EntityId entityId) { + CalculatedField calculatedField = new CalculatedField(); + calculatedField.setEntityId(entityId); + calculatedField.setType(CalculatedFieldType.SIMPLE); + calculatedField.setName("Test Calculated Field"); + + SimpleCalculatedFieldConfiguration config = new SimpleCalculatedFieldConfiguration(); + + Argument argument = new Argument(); + ReferencedEntityKey refEntityKey = new ReferencedEntityKey("attrKey", ArgumentType.ATTRIBUTE, AttributeScope.SERVER_SCOPE); + argument.setRefEntityKey(refEntityKey); + argument.setRefDynamicSourceConfiguration(new CurrentOwnerDynamicSourceConfiguration()); + + config.setArguments(Map.of("a", argument)); + + config.setExpression("a + 100"); + + Output output = new Output(); + output.setName("result"); + output.setType(OutputType.TIME_SERIES); + output.setDecimalsByDefault(0); + + config.setOutput(output); + + calculatedField.setConfiguration(config); + return calculatedField; + } + + private ObjectNode getLatestTelemetry(EntityId entityId, String... keys) throws Exception { + return doGetAsync("/api/plugins/telemetry/" + entityId.getEntityType() + "/" + entityId.getId() + "/values/timeseries?keys=" + String.join(",", keys), ObjectNode.class); + } + + private Asset createAsset(String name, AssetProfileId assetProfileId) { + Asset asset = new Asset(); + asset.setName(name); + asset.setAssetProfileId(assetProfileId); + return doPost("/api/asset", asset, Asset.class); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java b/application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java index c8b8b0244b..b6b006b16c 100644 --- a/application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java @@ -15,36 +15,55 @@ */ package org.thingsboard.server.cf; +import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.NullNode; import com.fasterxml.jackson.databind.node.ObjectNode; import org.junit.Test; -import org.junit.jupiter.api.BeforeEach; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.AttributeScope; import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.EntityInfo; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.asset.AssetProfile; import org.thingsboard.server.common.data.cf.CalculatedField; import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.cf.configuration.Argument; import org.thingsboard.server.common.data.cf.configuration.ArgumentType; +import org.thingsboard.server.common.data.cf.configuration.CalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.Output; import org.thingsboard.server.common.data.cf.configuration.OutputType; +import org.thingsboard.server.common.data.cf.configuration.PropagationCalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey; +import org.thingsboard.server.common.data.cf.configuration.RelationPathQueryDynamicSourceConfiguration; import org.thingsboard.server.common.data.cf.configuration.ScriptCalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.SimpleCalculatedFieldConfiguration; +import org.thingsboard.server.common.data.cf.configuration.geofencing.EntityCoordinates; +import org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingCalculatedFieldConfiguration; +import org.thingsboard.server.common.data.cf.configuration.geofencing.ZoneGroupConfiguration; import org.thingsboard.server.common.data.debug.DebugSettings; import org.thingsboard.server.common.data.id.AssetProfileId; import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.relation.EntityRelation; +import org.thingsboard.server.common.data.relation.EntitySearchDirection; +import org.thingsboard.server.common.data.relation.RelationPathLevel; import org.thingsboard.server.controller.CalculatedFieldControllerTest; import org.thingsboard.server.dao.service.DaoSqlTest; +import java.util.HashMap; +import java.util.List; import java.util.Map; 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; +import static org.thingsboard.server.common.data.cf.configuration.geofencing.EntityCoordinates.ENTITY_ID_LATITUDE_ARGUMENT_KEY; +import static org.thingsboard.server.common.data.cf.configuration.geofencing.EntityCoordinates.ENTITY_ID_LONGITUDE_ARGUMENT_KEY; +import static org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingReportStrategy.REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS; @DaoSqlTest public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTest { @@ -52,15 +71,10 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes public static final int TIMEOUT = 60; public static final int POLL_INTERVAL = 1; - @BeforeEach - void setUp() throws Exception { - loginTenantAdmin(); - } - @Test public void testSimpleCalculatedFieldWhenAllTelemetryPresent() throws Exception { Device testDevice = createDevice("Test device", "1234567890"); - doPost("/api/plugins/telemetry/DEVICE/" + testDevice.getUuidId() + "/timeseries/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode("{\"temperature\":25}")); + postTelemetry(testDevice.getId(), "{\"temperature\":25}"); doPost("/api/plugins/telemetry/DEVICE/" + testDevice.getUuidId() + "/attributes/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode("{\"deviceTemperature\":40}")); CalculatedField calculatedField = new CalculatedField(); @@ -97,7 +111,7 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes assertThat(fahrenheitTemp.get("fahrenheitTemp").get(0).get("value").asText()).isEqualTo("77.0"); }); - doPost("/api/plugins/telemetry/DEVICE/" + testDevice.getUuidId() + "/timeseries/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode("{\"temperature\":30}")); + postTelemetry(testDevice.getId(), "{\"temperature\":30}"); await().alias("update telemetry -> recalculate state").atMost(TIMEOUT, TimeUnit.SECONDS) .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) @@ -118,10 +132,11 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes .untilAsserted(() -> { ArrayNode temperatureF = getServerAttributes(testDevice.getId(), "temperatureF"); assertThat(temperatureF).isNotNull(); + assertThat(temperatureF.get(0)).isNotNull(); assertThat(temperatureF.get(0).get("value").asText()).isEqualTo("86.0"); }); - Argument savedArgument = savedCalculatedField.getConfiguration().getArguments().get("T"); + Argument savedArgument = ((SimpleCalculatedFieldConfiguration) savedCalculatedField.getConfiguration()).getArguments().get("T"); savedArgument.setRefEntityKey(new ReferencedEntityKey("deviceTemperature", ArgumentType.ATTRIBUTE, AttributeScope.SERVER_SCOPE)); savedCalculatedField = doPost("/api/calculatedField", savedCalculatedField, CalculatedField.class); @@ -133,7 +148,7 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes assertThat(temperatureF.get(0).get("value").asText()).isEqualTo("104.0"); }); - savedCalculatedField.getConfiguration().setExpression("1.8 * T + 32"); + ((SimpleCalculatedFieldConfiguration) savedCalculatedField.getConfiguration()).setExpression("1.8 * T + 32"); savedCalculatedField = doPost("/api/calculatedField", savedCalculatedField, CalculatedField.class); await().alias("update CF expression -> perform calculation with new expression").atMost(TIMEOUT, TimeUnit.SECONDS) @@ -182,7 +197,7 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes assertThat(fahrenheitTemp.get("fahrenheitTemp").get(0).get("value").isNull()).isTrue(); }); - doPost("/api/plugins/telemetry/DEVICE/" + testDevice.getUuidId() + "/timeseries/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode("{\"temperature\":30}")); + postTelemetry(testDevice.getId(), "{\"temperature\":30}"); await().alias("update telemetry -> perform calculation").atMost(TIMEOUT, TimeUnit.SECONDS) .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) @@ -231,7 +246,7 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes assertThat(fahrenheitTemp.get("fahrenheitTemp").get(0).get("value").asText()).isEqualTo("53.6"); }); - doPost("/api/plugins/telemetry/DEVICE/" + testDevice.getUuidId() + "/timeseries/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode("{\"temperature\":30}")); + postTelemetry(testDevice.getId(), "{\"temperature\":30}"); await().alias("update telemetry -> recalculate state").atMost(TIMEOUT, TimeUnit.SECONDS) .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) @@ -416,7 +431,7 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes @Test public void testSimpleCalculatedFieldWhenExpressionIsInvalid() throws Exception { Device testDevice = createDevice("Test device", "1234567890"); - doPost("/api/plugins/telemetry/DEVICE/" + testDevice.getUuidId() + "/timeseries/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode("{\"temperature\":25}")); + postTelemetry(testDevice.getId(), "{\"temperature\":25}"); CalculatedField calculatedField = new CalculatedField(); calculatedField.setEntityId(testDevice.getId()); @@ -452,7 +467,7 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes assertThat(fahrenheitTemp.get("fahrenheitTemp").get(0).get("value").isNull()).isTrue(); }); - doPost("/api/plugins/telemetry/DEVICE/" + testDevice.getUuidId() + "/timeseries/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode("{\"temperature\":30}")); + postTelemetry(testDevice.getId(), "{\"temperature\":30}"); await().alias("update telemetry -> ctx is not initialized -> no calculation perform").atMost(TIMEOUT, TimeUnit.SECONDS) .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) @@ -467,7 +482,7 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes public void testSimpleCalculatedFieldWhenUseLatestTsIsTrue() throws Exception { Device testDevice = createDevice("Test device", "1234567890"); long ts = System.currentTimeMillis() - 300000L; - doPost("/api/plugins/telemetry/DEVICE/" + testDevice.getUuidId() + "/timeseries/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode(String.format("{\"ts\": %s, \"values\": {\"temperature\":30}}", ts))); + postTelemetry(testDevice.getId(), String.format("{\"ts\": %s, \"values\": {\"temperature\":30}}", ts)); CalculatedField calculatedField = new CalculatedField(); calculatedField.setEntityId(testDevice.getId()); @@ -511,10 +526,10 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes long ts = System.currentTimeMillis(); long tsA = ts - 300000L; - doPost("/api/plugins/telemetry/DEVICE/" + testDevice.getUuidId() + "/timeseries/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode(String.format("{\"ts\": %s, \"values\": {\"a\":1}}", tsA))); + postTelemetry(testDevice.getId(), String.format("{\"ts\": %s, \"values\": {\"a\":1}}", tsA)); long tsB = ts - 300L; - doPost("/api/plugins/telemetry/DEVICE/" + testDevice.getUuidId() + "/timeseries/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode(String.format("{\"ts\": %s, \"values\": {\"b\":5}}", tsB))); + postTelemetry(testDevice.getId(), String.format("{\"ts\": %s, \"values\": {\"b\":5}}", tsB)); CalculatedField calculatedField = new CalculatedField(); calculatedField.setEntityId(testDevice.getId()); @@ -555,7 +570,7 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes }); long tsABeforeTsB = tsB - 300L; - doPost("/api/plugins/telemetry/DEVICE/" + testDevice.getUuidId() + "/timeseries/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode(String.format("{\"ts\": %s, \"values\": {\"a\":10}}", tsABeforeTsB))); + postTelemetry(testDevice.getId(), String.format("{\"ts\": %s, \"values\": {\"a\":10}}", tsABeforeTsB)); await().alias("update telemetry with ts less than latest -> save result with latest ts").atMost(TIMEOUT, TimeUnit.SECONDS) .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) @@ -571,7 +586,7 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes public void testScriptCalculatedFieldWhenUsedLatestTsInScript() throws Exception { Device testDevice = createDevice("Test device", "1234567890"); long ts = System.currentTimeMillis() - 300000L; - doPost("/api/plugins/telemetry/DEVICE/" + testDevice.getUuidId() + "/timeseries/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode(String.format("{\"ts\": %s, \"values\": {\"temperature\":30}}", ts))); + postTelemetry(testDevice.getId(), String.format("{\"ts\": %s, \"values\": {\"temperature\":30}}", ts)); CalculatedField calculatedField = new CalculatedField(); calculatedField.setEntityId(testDevice.getId()); @@ -606,6 +621,595 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes }); } + @Test + public void testSimpleCalculatedFieldWhenCtxBecameUninitialized() throws Exception { + Device testDevice = createDevice("Test device", "1234567890"); + + CalculatedField calculatedField = new CalculatedField(); + calculatedField.setEntityId(testDevice.getId()); + calculatedField.setType(CalculatedFieldType.SIMPLE); + calculatedField.setName("M + 1"); + calculatedField.setDebugSettings(DebugSettings.all()); + + SimpleCalculatedFieldConfiguration config = new SimpleCalculatedFieldConfiguration(); + + Argument argument = new Argument(); + ReferencedEntityKey refEntityKey = new ReferencedEntityKey("m", ArgumentType.TS_LATEST, null); + argument.setRefEntityKey(refEntityKey); + config.setArguments(Map.of("m", argument)); + config.setExpression("m + 1"); + + Output output = new Output(); + output.setName("m1"); + output.setType(OutputType.TIME_SERIES); + output.setDecimalsByDefault(0); + config.setOutput(output); + + calculatedField.setConfiguration(config); + + calculatedField = doPost("/api/calculatedField", calculatedField, CalculatedField.class); + + doPost("/api/plugins/telemetry/DEVICE/" + testDevice.getUuidId() + "/timeseries/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode("{\"m\":1}")); + + await().alias("create CF -> ctx is initialized -> perform calculation").atMost(TIMEOUT, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + ObjectNode m1 = getLatestTelemetry(testDevice.getId(), "m1"); + assertThat(m1).isNotNull(); + assertThat(m1.get("m1").get(0).get("value").asText()).isEqualTo("2"); + }); + + config.setExpression("m m"); + calculatedField.setConfiguration(config); + calculatedField = doPost("/api/calculatedField", calculatedField, CalculatedField.class); + + doPost("/api/plugins/telemetry/DEVICE/" + testDevice.getUuidId() + "/timeseries/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode("{\"m\":2}")); + + await().alias("update CF -> ctx is not initialized -> no calculation performed").atMost(TIMEOUT, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + ObjectNode m1 = getLatestTelemetry(testDevice.getId(), "m1"); + assertThat(m1).isNotNull(); + assertThat(m1.get("m1").get(0).get("value").asText()).isEqualTo("2"); + }); + } + + @Test + public void testGeofencingCalculatedField_withZonesCreatedOnDevice() throws Exception { + // --- Arrange entities --- + Device device = createDevice("GF Test Device", "sn-geo-2"); + + // Allowed zone polygon (square) + String allowedPolygon = "[[50.472000, 30.504000], [50.472000, 30.506000], [50.474000, 30.506000], [50.474000, 30.504000]]"; + // Restricted zone polygon (square) + String restrictedPolygon = "[[50.475000, 30.510000], [50.475000, 30.512000], [50.477000, 30.512000], [50.477000, 30.510000]]"; + + doPost("/api/plugins/telemetry/DEVICE/" + device.getUuidId() + "/attributes/" + DataConstants.SERVER_SCOPE, + JacksonUtil.toJsonNode("{\"allowedZone\":" + allowedPolygon + "}")).andExpect(status().isOk()); + + doPost("/api/plugins/telemetry/DEVICE/" + device.getUuidId() + "/attributes/" + DataConstants.SERVER_SCOPE, + JacksonUtil.toJsonNode("{\"restrictedZone\":" + restrictedPolygon + "}")).andExpect(status().isOk()); + + // Initial device coordinates (inside Allowed, outside Restricted) + doPost("/api/plugins/telemetry/DEVICE/" + device.getUuidId() + "/timeseries/unusedScope", + JacksonUtil.toJsonNode("{\"latitude\":50.4730,\"longitude\":30.5050}")); + + // --- Build CF: GEOFENCING --- + CalculatedField cf = new CalculatedField(); + cf.setEntityId(device.getDeviceProfileId()); + cf.setType(CalculatedFieldType.GEOFENCING); + cf.setName("Geofencing CF"); + cf.setDebugSettings(DebugSettings.off()); + + GeofencingCalculatedFieldConfiguration cfg = new GeofencingCalculatedFieldConfiguration(); + + // Coordinates: TS_LATEST on the device + EntityCoordinates entityCoordinates = new EntityCoordinates("latitude", "longitude"); + cfg.setEntityCoordinates(entityCoordinates); + + // Zone groups: ATTRIBUTE on the device + ZoneGroupConfiguration allowedZonesGroup = new ZoneGroupConfiguration("allowedZone", REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS, false); + ZoneGroupConfiguration restrictedZonesGroup = new ZoneGroupConfiguration("restrictedZone", REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS, false); + + cfg.setZoneGroups(Map.of("allowedZones", allowedZonesGroup, "restrictedZones", restrictedZonesGroup)); + + // Output to server attributes + Output out = new Output(); + out.setType(OutputType.ATTRIBUTES); + out.setScope(AttributeScope.SERVER_SCOPE); + cfg.setOutput(out); + + cf.setConfiguration(cfg); + + doPost("/api/calculatedField", cf, CalculatedField.class); + + // --- Assert initial evaluation (ENTERED / OUTSIDE) --- + await().alias("initial geofencing evaluation") + .atMost(TIMEOUT, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + ArrayNode attrs = getServerAttributes(device.getId(), + "allowedZonesEvent", "allowedZonesStatus", "restrictedZonesStatus", "restrictedZonesEvent"); + // --- no restrictedZonesEvent as no transition happened yet + assertThat(attrs).isNotNull().isNotEmpty().hasSize(3); + Map m = kv(attrs); + assertThat(m).containsEntry("allowedZonesEvent", "ENTERED") + .containsEntry("allowedZonesStatus", "INSIDE") + .containsEntry("restrictedZonesStatus", "OUTSIDE"); + }); + + // --- delete attributes reported in previous evaluation + doDelete("/api/plugins/telemetry/DEVICE/" + device.getUuidId() + "/SERVER_SCOPE?keys=allowedZonesEvent,allowedZonesStatus,restrictedZonesStatus", String.class); + + // --- Update restrictedZone by 'restrictedZone' attribute update + doPost("/api/plugins/telemetry/DEVICE/" + device.getUuidId() + "/attributes/" + DataConstants.SERVER_SCOPE, + JacksonUtil.toJsonNode("{\"restrictedZone\":" + restrictedPolygon + "}")).andExpect(status().isOk()); + + // --- Assert no transition --- + // --- Assert attributes updated with the same values for restrictedZones --- + // --- Assert attributes updated with the new values for allowedZones --- + await().alias("evaluation after version bump of geo argument") + .atMost(TIMEOUT, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + ArrayNode attrs = getServerAttributes(device.getId(), + "allowedZonesEvent", "allowedZonesStatus", + "restrictedZonesEvent", "restrictedZonesStatus"); + assertThat(attrs).isNotNull().isNotEmpty().hasSize(2); + Map m = kv(attrs); + assertThat(m).containsEntry("allowedZonesStatus", "INSIDE") + .containsEntry("restrictedZonesStatus", "OUTSIDE"); + }); + } + + @Test + public void testGeofencingCalculatedField_withoutRelationsCreationAndDynamicRefresh() throws Exception { + // --- Arrange entities --- + Device device = createDevice("GF Device", "sn-geo-1"); + + // Allowed zone polygon (square) + String allowedPolygon = "[[50.472000, 30.504000], [50.472000, 30.506000], [50.474000, 30.506000], [50.474000, 30.504000]]"; + // Restricted zone polygon (square) + String restrictedPolygon = "[[50.475000, 30.510000], [50.475000, 30.512000], [50.477000, 30.512000], [50.477000, 30.510000]]"; + + Asset allowedZoneAsset = createAsset("Allowed Zone", null); + doPost("/api/plugins/telemetry/ASSET/" + allowedZoneAsset.getUuidId() + "/attributes/" + DataConstants.SERVER_SCOPE, + JacksonUtil.toJsonNode("{\"zone\":" + allowedPolygon + "}")).andExpect(status().isOk()); + + Asset restrictedZoneAsset = createAsset("Restricted Zone", null); + doPost("/api/plugins/telemetry/ASSET/" + restrictedZoneAsset.getUuidId() + "/attributes/" + DataConstants.SERVER_SCOPE, + JacksonUtil.toJsonNode("{\"zone\":" + restrictedPolygon + "}")).andExpect(status().isOk()); + + // Relations from device to zones + EntityRelation deviceToAllowedZoneRelation = new EntityRelation(); + deviceToAllowedZoneRelation.setFrom(device.getId()); + deviceToAllowedZoneRelation.setTo(allowedZoneAsset.getId()); + deviceToAllowedZoneRelation.setType("AllowedZone"); + + EntityRelation deviceToRestrictedZoneRelation = new EntityRelation(); + deviceToRestrictedZoneRelation.setFrom(device.getId()); + deviceToRestrictedZoneRelation.setTo(restrictedZoneAsset.getId()); + deviceToRestrictedZoneRelation.setType("RestrictedZone"); + + doPost("/api/relation", deviceToAllowedZoneRelation).andExpect(status().isOk()); + doPost("/api/relation", deviceToRestrictedZoneRelation).andExpect(status().isOk()); + + // Initial device coordinates (inside Allowed, outside Restricted) + doPost("/api/plugins/telemetry/DEVICE/" + device.getUuidId() + "/timeseries/unusedScope", + JacksonUtil.toJsonNode("{\"latitude\":50.4730,\"longitude\":30.5050}")); + + // --- Build CF: GEOFENCING --- + CalculatedField cf = new CalculatedField(); + cf.setEntityId(device.getId()); + cf.setType(CalculatedFieldType.GEOFENCING); + cf.setName("Geofencing CF"); + cf.setDebugSettings(DebugSettings.off()); + + GeofencingCalculatedFieldConfiguration cfg = new GeofencingCalculatedFieldConfiguration(); + + // Coordinates: TS_LATEST on the device + EntityCoordinates entityCoordinates = new EntityCoordinates("latitude", "longitude"); + cfg.setEntityCoordinates(entityCoordinates); + + // Zone groups: ATTRIBUTE on specific assets (one zone per group) + ZoneGroupConfiguration allowedZonesGroup = new ZoneGroupConfiguration("zone", REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS, false); + var allowedZoneDynamicSourceConfiguration = new RelationPathQueryDynamicSourceConfiguration(); + allowedZoneDynamicSourceConfiguration.setLevels(List.of(new RelationPathLevel(EntitySearchDirection.FROM, "AllowedZone"))); + allowedZonesGroup.setRefDynamicSourceConfiguration(allowedZoneDynamicSourceConfiguration); + + ZoneGroupConfiguration restrictedZonesGroup = new ZoneGroupConfiguration("zone", REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS, false); + var restrictedZoneDynamicSourceConfiguration = new RelationPathQueryDynamicSourceConfiguration(); + restrictedZoneDynamicSourceConfiguration.setLevels(List.of(new RelationPathLevel(EntitySearchDirection.FROM, "RestrictedZone"))); + restrictedZonesGroup.setRefDynamicSourceConfiguration(restrictedZoneDynamicSourceConfiguration); + + cfg.setZoneGroups(Map.of("allowedZones", allowedZonesGroup, "restrictedZones", restrictedZonesGroup)); + + // Output to server attributes + Output out = new Output(); + out.setType(OutputType.ATTRIBUTES); + out.setScope(AttributeScope.SERVER_SCOPE); + cfg.setOutput(out); + + cf.setConfiguration(cfg); + + doPost("/api/calculatedField", cf, CalculatedField.class); + + // --- Assert initial evaluation (ENTERED / OUTSIDE) --- + await().alias("initial geofencing evaluation") + .atMost(TIMEOUT, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + ArrayNode attrs = getServerAttributes(device.getId(), + "allowedZonesEvent", "allowedZonesStatus", "restrictedZonesStatus"); + assertThat(attrs).isNotNull().isNotEmpty().hasSize(3); + Map m = kv(attrs); + assertThat(m).containsEntry("allowedZonesEvent", "ENTERED") + .containsEntry("allowedZonesStatus", "INSIDE") + .containsEntry("restrictedZonesStatus", "OUTSIDE"); + }); + + // --- Move the device into Restricted zone (and outside Allowed) --- + doPost("/api/plugins/telemetry/DEVICE/" + device.getUuidId() + "/timeseries/unusedScope", + JacksonUtil.toJsonNode("{\"latitude\":50.4760,\"longitude\":30.5110}")); + + // --- Assert transition (LEFT / ENTERED) --- + await().alias("transition evaluation after movement") + .atMost(TIMEOUT, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + ArrayNode attrs = getServerAttributes(device.getId(), + "allowedZonesEvent", "allowedZonesStatus", + "restrictedZonesEvent", "restrictedZonesStatus"); + assertThat(attrs).isNotNull().isNotEmpty().hasSize(4); + Map m = kv(attrs); + assertThat(m).containsEntry("allowedZonesEvent", "LEFT") + .containsEntry("restrictedZonesEvent", "ENTERED") + .containsEntry("allowedZonesStatus", "OUTSIDE") + .containsEntry("restrictedZonesStatus", "INSIDE"); + }); + } + + @Test + public void testGeofencingCalculatedField_DynamicRefresh_RebindsZoneArguments() throws Exception { + // --- Update min allowed scheduled update intervals for CFs --- + loginSysAdmin(); + EntityInfo tenantProfileEntityInfo = doGet("/api/tenantProfileInfo/default", EntityInfo.class); + assertThat(tenantProfileEntityInfo).isNotNull(); + TenantProfile foundTenantProfile = doGet("/api/tenantProfile/" + tenantProfileEntityInfo.getId().getId().toString(), TenantProfile.class); + assertThat(foundTenantProfile).isNotNull(); + assertThat(foundTenantProfile.getDefaultProfileConfiguration()).isNotNull(); + int minAllowedScheduledUpdateIntervalInSecForCF = TIMEOUT / 10; + foundTenantProfile.getDefaultProfileConfiguration().setMinAllowedScheduledUpdateIntervalInSecForCF(minAllowedScheduledUpdateIntervalInSecForCF); + TenantProfile savedTenantProfile = doPost("/api/tenantProfile", foundTenantProfile, TenantProfile.class); + assertThat(savedTenantProfile).isNotNull(); + assertThat(savedTenantProfile.getDefaultProfileConfiguration().getMinAllowedScheduledUpdateIntervalInSecForCF()).isEqualTo(minAllowedScheduledUpdateIntervalInSecForCF); + loginTenantAdmin(); + + // --- Arrange entities --- + Device device = createDevice("GF Device dyn", "sn-geo-dyn-1"); + + // Allowed Zone A: covers initial point (ENTERED) + String allowedPolygonA = "[[50.472000, 30.504000], [50.472000, 30.506000], [50.474000, 30.506000], [50.474000, 30.504000]]"; + + Asset allowedZoneA = createAsset("Allowed Zone A", null); + doPost("/api/plugins/telemetry/ASSET/" + allowedZoneA.getUuidId() + "/attributes/" + DataConstants.SERVER_SCOPE, + JacksonUtil.toJsonNode("{\"zone\":" + allowedPolygonA + "}")).andExpect(status().isOk()); + + // Relation from device to Allowed Zone A + EntityRelation relAllowedA = new EntityRelation(); + relAllowedA.setFrom(device.getId()); + relAllowedA.setTo(allowedZoneA.getId()); + relAllowedA.setType("AllowedZone"); + doPost("/api/relation", relAllowedA).andExpect(status().isOk()); + + // Initial device coordinates: INSIDE Zone A + doPost("/api/plugins/telemetry/DEVICE/" + device.getUuidId() + "/timeseries/unusedScope", + JacksonUtil.toJsonNode("{\"latitude\":50.4730,\"longitude\":30.5050}")).andExpect(status().isOk()); + + // --- Build CF: GEOFENCING with dynamic 'allowedZones' and short scheduled refresh --- + CalculatedField cf = new CalculatedField(); + cf.setEntityId(device.getId()); + cf.setType(CalculatedFieldType.GEOFENCING); + cf.setName("Geofencing CF (dynamic refresh)"); + cf.setDebugSettings(DebugSettings.off()); + + GeofencingCalculatedFieldConfiguration cfg = new GeofencingCalculatedFieldConfiguration(); + cfg.setEntityCoordinates(new EntityCoordinates(ENTITY_ID_LATITUDE_ARGUMENT_KEY, ENTITY_ID_LONGITUDE_ARGUMENT_KEY)); + + var allowedZonesGroup = new ZoneGroupConfiguration("zone", REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS, false); + var allowedZoneDynamicSourceConfiguration = new RelationPathQueryDynamicSourceConfiguration(); + allowedZoneDynamicSourceConfiguration.setLevels(List.of(new RelationPathLevel(EntitySearchDirection.FROM, "AllowedZone"))); + allowedZonesGroup.setRefDynamicSourceConfiguration(allowedZoneDynamicSourceConfiguration); + cfg.setZoneGroups(Map.of("allowedZones", allowedZonesGroup)); + + // Server attributes output + Output out = new Output(); + out.setType(OutputType.ATTRIBUTES); + out.setScope(AttributeScope.SERVER_SCOPE); + cfg.setOutput(out); + + // Enable scheduled refresh with a 6-second interval + cfg.setScheduledUpdateInterval(minAllowedScheduledUpdateIntervalInSecForCF); + cfg.setScheduledUpdateEnabled(true); + + cf.setConfiguration(cfg); + CalculatedField savedCalculatedField = doPost("/api/calculatedField", cf, CalculatedField.class); + assertThat(savedCalculatedField).isNotNull(); + CalculatedFieldConfiguration configuration = savedCalculatedField.getConfiguration(); + assertThat(configuration).isInstanceOf(GeofencingCalculatedFieldConfiguration.class); + var geofencingConfiguration = (GeofencingCalculatedFieldConfiguration) configuration; + assertThat(geofencingConfiguration.isScheduledUpdateEnabled()).isTrue(); + + // --- Assert initial evaluation (ENTERED) --- + await().alias("initial geofencing evaluation") + .atMost(TIMEOUT, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + ArrayNode attrs = getServerAttributes(device.getId(), "allowedZonesEvent", "allowedZonesStatus"); + assertThat(attrs).isNotNull().isNotEmpty().hasSize(2); + Map m = kv(attrs); + assertThat(m).containsEntry("allowedZonesEvent", "ENTERED") + .containsEntry("allowedZonesStatus", "INSIDE"); + }); + + // --- Move device OUTSIDE Zone A (expect LEFT) --- + doPost("/api/plugins/telemetry/DEVICE/" + device.getUuidId() + "/timeseries/unusedScope", + JacksonUtil.toJsonNode("{\"latitude\":50.4760,\"longitude\":30.5110}")).andExpect(status().isOk()); + + await().alias("outside zone A (LEFT)") + .atMost(TIMEOUT, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + ArrayNode attrs = getServerAttributes(device.getId(), "allowedZonesEvent", "allowedZonesStatus"); + assertThat(attrs).isNotNull().isNotEmpty().hasSize(2); + Map m = kv(attrs); + assertThat(m).containsEntry("allowedZonesEvent", "LEFT") + .containsEntry("allowedZonesStatus", "OUTSIDE"); + }); + + // --- Create Allowed Zone B covering the CURRENT location --- + String allowedPolygonB = "[[50.475500, 30.510500], [50.475500, 30.511500], [50.476500, 30.511500], [50.476500, 30.510500]]"; + + Asset allowedZoneB = createAsset("Allowed Zone B", null); + doPost("/api/plugins/telemetry/ASSET/" + allowedZoneB.getUuidId() + "/attributes/" + DataConstants.SERVER_SCOPE, + JacksonUtil.toJsonNode("{\"zone\":" + allowedPolygonB + "}")).andExpect(status().isOk()); + + // Add a new relation + EntityRelation relAllowedB = new EntityRelation(); + relAllowedB.setFrom(device.getId()); + relAllowedB.setTo(allowedZoneB.getId()); + relAllowedB.setType("AllowedZone"); + doPost("/api/relation", relAllowedB).andExpect(status().isOk()); + + awaitForCalculatedFieldEntityMessageProcessorToRegisterCfStateAsReadyToRefreshDynamicArguments(device.getId(), savedCalculatedField.getId(), minAllowedScheduledUpdateIntervalInSecForCF); + + // --- Same coordinates as before, but now we expect ENTERED since a new zone is registered --- + doPost("/api/plugins/telemetry/DEVICE/" + device.getUuidId() + "/timeseries/unusedScope", + JacksonUtil.toJsonNode("{\"latitude\":50.4760,\"longitude\":30.5110}")).andExpect(status().isOk()); + + // --- Assert dynamic refresh picks up new relation and flips event back to ENTERED on the next telemetry update --- + await().alias("dynamic refresh rebinds allowedZones") + .atMost(TIMEOUT, TimeUnit.SECONDS) + .pollInterval(1, TimeUnit.SECONDS) + .untilAsserted(() -> { + ArrayNode attrs = getServerAttributes(device.getId(), "allowedZonesEvent", "allowedZonesStatus"); + assertThat(attrs).isNotNull().isNotEmpty().hasSize(2); + Map m = kv(attrs); + assertThat(m).containsEntry("allowedZonesEvent", "ENTERED") + .containsEntry("allowedZonesStatus", "INSIDE"); + }); + } + + @Test + public void testPropagationCalculatedField_withExpression() throws Exception { + // --- Arrange entities --- + Device device = createDevice("Propagation Device With Expression", "sn-prop-1"); + Asset asset1 = createAsset("Propagated Asset 1", null); + Asset asset2 = createAsset("Propagated Asset 2", null); + + // Create relations FROM assets TO device + EntityRelation rel1 = new EntityRelation(asset1.getId(), device.getId(), EntityRelation.CONTAINS_TYPE); + EntityRelation rel2 = new EntityRelation(asset2.getId(), device.getId(), EntityRelation.CONTAINS_TYPE); + doPost("/api/relation", rel1).andExpect(status().isOk()); + doPost("/api/relation", rel2).andExpect(status().isOk()); + + // Telemetry on device + doPost("/api/plugins/telemetry/DEVICE/" + device.getUuidId() + "/timeseries/unusedScope", + JacksonUtil.toJsonNode("{\"temperature\":12.5}")).andExpect(status().isOk()); + + // --- Build CF: PROPAGATION with expression --- + CalculatedField cf = new CalculatedField(); + cf.setEntityId(device.getId()); + cf.setType(CalculatedFieldType.PROPAGATION); + cf.setName("Propagation CF (expr)"); + cf.setConfigurationVersion(1); + + PropagationCalculatedFieldConfiguration cfg = new PropagationCalculatedFieldConfiguration(); + cfg.setRelation(new RelationPathLevel(EntitySearchDirection.TO, EntityRelation.CONTAINS_TYPE)); + cfg.setApplyExpressionToResolvedArguments(true); + + Argument arg = new Argument(); + arg.setRefEntityKey(new ReferencedEntityKey("temperature", ArgumentType.TS_LATEST, null)); + cfg.setArguments(Map.of("t", arg)); + + cfg.setExpression("{\"testResult\": t * 2}"); + + Output output = new Output(); + output.setType(OutputType.ATTRIBUTES); + output.setScope(AttributeScope.SERVER_SCOPE); + cfg.setOutput(output); + + cf.setConfiguration(cfg); + + doPost("/api/calculatedField", cf, CalculatedField.class); + + // --- Assert propagated calculation (expression applied) --- + await().alias("propagation expr mode evaluation") + .atMost(TIMEOUT, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + ArrayNode attrs1 = getServerAttributes(asset1.getId(), "testResult"); + ArrayNode attrs2 = getServerAttributes(asset2.getId(), "testResult"); + assertThat(attrs1).isNotNull(); + assertThat(attrs2).isNotNull(); + assertThat(attrs1.get(0).get("value").asDouble()).isEqualTo(25.0); + assertThat(attrs2.get(0).get("value").asDouble()).isEqualTo(25.0); + }); + + String deleteUrl = String.format("/api/v2/relation?fromId=%s&fromType=%s&relationType=%s&toId=%s&toType=%s", + asset1.getId().getId(), EntityType.ASSET, + EntityRelation.CONTAINS_TYPE, device.getId().getId(), EntityType.DEVICE + ); + doDelete(deleteUrl).andExpect(status().isOk()); + doDelete("/api/plugins/telemetry/ASSET/" + asset1.getId() + "/SERVER_SCOPE?keys=testResult").andExpect(status().isOk()); + + doPost("/api/plugins/telemetry/DEVICE/" + device.getUuidId() + "/timeseries/unusedScope", + JacksonUtil.toJsonNode("{\"temperature\":25}")).andExpect(status().isOk()); + + // --- Assert propagated calculation (expression applied with new temperature argument and one relation removed) --- + await().alias("propagation expr mode evaluation after temperature update") + .atMost(TIMEOUT, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + ArrayNode attrs1 = getServerAttributes(asset1.getId(), "testResult"); + ArrayNode attrs2 = getServerAttributes(asset2.getId(), "testResult"); + assertThat(attrs1).isNullOrEmpty(); + assertThat(attrs2).isNotNull(); + assertThat(attrs2.get(0).get("value").asDouble()).isEqualTo(50); + }); + } + + @Test + public void testPropagationCalculatedField_withoutExpression() throws Exception { + // --- Arrange entities --- + Device device = createDevice("Propagation Device Without Expression", "sn-prop-2"); + Asset asset1 = createAsset("Propagated Asset 1", null); + Asset asset2 = createAsset("Propagated Asset 2", null); + + // Create relations FROM assets TO device + EntityRelation rel1 = new EntityRelation(asset1.getId(), device.getId(), EntityRelation.CONTAINS_TYPE); + EntityRelation rel2 = new EntityRelation(asset2.getId(), device.getId(), EntityRelation.CONTAINS_TYPE); + doPost("/api/relation", rel1).andExpect(status().isOk()); + doPost("/api/relation", rel2).andExpect(status().isOk()); + + // Telemetry on device + long ts = System.currentTimeMillis() - 300000L; + postTelemetry(device.getId(), String.format("{\"ts\": %s, \"values\": {\"temperature\":12.5}}", ts)); + + // --- Build CF: PROPAGATION without expression --- + CalculatedField cf = new CalculatedField(); + cf.setEntityId(device.getId()); + cf.setType(CalculatedFieldType.PROPAGATION); + cf.setName("Propagation CF (args-only)"); + cf.setConfigurationVersion(1); + + PropagationCalculatedFieldConfiguration cfg = new PropagationCalculatedFieldConfiguration(); + cfg.setRelation(new RelationPathLevel(EntitySearchDirection.TO, EntityRelation.CONTAINS_TYPE)); + cfg.setApplyExpressionToResolvedArguments(false); // arguments-only mode + + Argument arg = new Argument(); + arg.setRefEntityKey(new ReferencedEntityKey("temperature", ArgumentType.TS_LATEST, null)); + cfg.setArguments(Map.of("temperatureComputed", arg)); + + Output output = new Output(); + output.setType(OutputType.TIME_SERIES); + cfg.setOutput(output); + + cf.setConfiguration(cfg); + + doPost("/api/calculatedField", cf, CalculatedField.class); + + // --- Assert propagated calculation (arguments-only mode) --- + await().alias("propagation args-only evaluation") + .atMost(TIMEOUT, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + ObjectNode telemetry1 = getLatestTelemetry(asset1.getId(), "temperatureComputed"); + ObjectNode telemetry2 = getLatestTelemetry(asset2.getId(), "temperatureComputed"); + assertThat(telemetry1).isNotNull(); + assertThat(telemetry2).isNotNull(); + assertThat(telemetry1.get("temperatureComputed").get(0).get("ts").asText()).isEqualTo(Long.toString(ts)); + assertThat(telemetry1.get("temperatureComputed").get(0).get("value").asDouble()).isEqualTo(12.5); + assertThat(telemetry2.get("temperatureComputed").get(0).get("ts").asText()).isEqualTo(Long.toString(ts)); + assertThat(telemetry2.get("temperatureComputed").get(0).get("value").asDouble()).isEqualTo(12.5); + }); + + String deleteUrl = String.format("/api/v2/relation?fromId=%s&fromType=%s&relationType=%s&toId=%s&toType=%s", + asset1.getId().getId(), EntityType.ASSET, + EntityRelation.CONTAINS_TYPE, device.getId().getId(), EntityType.DEVICE + ); + doDelete(deleteUrl).andExpect(status().isOk()); + doDelete("/api/plugins/telemetry/ASSET/" + asset1.getId() + "/timeseries/delete?keys=temperatureComputed&deleteAllDataForKeys=true").andExpect(status().isOk()); + + // Update telemetry on device + long newTs = System.currentTimeMillis() - 300000L; + postTelemetry(device.getId(), String.format("{\"ts\": %s, \"values\": {\"temperature\":25}}", newTs)); + + // --- Assert propagated calculation (arguments-only mode after update) --- + await().alias("propagation args-only evaluation after temperature update") + .atMost(TIMEOUT, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + ObjectNode telemetry1 = getLatestTelemetry(asset1.getId(), "temperatureComputed"); + ObjectNode telemetry2 = getLatestTelemetry(asset2.getId(), "temperatureComputed"); + assertThat(telemetry1).isNotNull(); + assertThat(telemetry2).isNotNull(); + assertThat(telemetry1.get("temperatureComputed").get(0).get("value")).isEqualTo(NullNode.instance); + assertThat(telemetry2.get("temperatureComputed").get(0).get("ts").asText()).isEqualTo(Long.toString(newTs)); + assertThat(telemetry2.get("temperatureComputed").get(0).get("value").asDouble()).isEqualTo(25); + }); + } + + @Test + public void testCalculatedFieldWhenTheSameTelemetryKeysUsed() throws Exception { + Device testDevice = createDevice("Test device", "1234567890"); + doPost("/api/plugins/telemetry/DEVICE/" + testDevice.getUuidId() + "/timeseries/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode("{\"a\":5}")); + + CalculatedField calculatedField = new CalculatedField(); + calculatedField.setEntityId(testDevice.getId()); + calculatedField.setType(CalculatedFieldType.SIMPLE); + calculatedField.setName("a + b"); + calculatedField.setDebugSettings(DebugSettings.all()); + + SimpleCalculatedFieldConfiguration config = new SimpleCalculatedFieldConfiguration(); + + ReferencedEntityKey refEntityKey = new ReferencedEntityKey("a", ArgumentType.TS_LATEST, null); + Argument argumentA = new Argument(); + argumentA.setRefEntityKey(refEntityKey); + Argument argumentB = new Argument(); + argumentB.setRefEntityKey(refEntityKey); + config.setArguments(Map.of("a", argumentA, "b", argumentB)); + config.setExpression("a + b"); + + Output output = new Output(); + output.setName("c"); + output.setType(OutputType.TIME_SERIES); + output.setDecimalsByDefault(0); + config.setOutput(output); + + calculatedField.setConfiguration(config); + + doPost("/api/calculatedField", calculatedField, CalculatedField.class); + + await().alias("create CF -> perform initial calculation").atMost(TIMEOUT, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + ObjectNode c = getLatestTelemetry(testDevice.getId(), "c"); + assertThat(c).isNotNull(); + assertThat(c.get("c").get(0).get("value").asText()).isEqualTo("10"); + }); + + doPost("/api/plugins/telemetry/DEVICE/" + testDevice.getUuidId() + "/timeseries/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode("{\"a\":10}")); + + await().alias("update telemetry -> recalculate state").atMost(TIMEOUT, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + ObjectNode c = getLatestTelemetry(testDevice.getId(), "c"); + assertThat(c).isNotNull(); + assertThat(c.get("c").get(0).get("value").asText()).isEqualTo("20"); + }); + } + private ObjectNode getLatestTelemetry(EntityId entityId, String... keys) throws Exception { return doGetAsync("/api/plugins/telemetry/" + entityId.getEntityType() + "/" + entityId.getId() + "/values/timeseries?keys=" + String.join(",", keys), ObjectNode.class); } @@ -621,4 +1225,12 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes return doPost("/api/asset", asset, Asset.class); } + private static Map kv(ArrayNode attrs) { + Map m = new HashMap<>(); + for (JsonNode n : attrs) { + m.put(n.get("key").asText(), n.get("value").asText()); + } + return m; + } + } diff --git a/application/src/test/java/org/thingsboard/server/cf/EntityAggregationCalculatedFieldTest.java b/application/src/test/java/org/thingsboard/server/cf/EntityAggregationCalculatedFieldTest.java new file mode 100644 index 0000000000..e479c4959e --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/cf/EntityAggregationCalculatedFieldTest.java @@ -0,0 +1,255 @@ +/** + * Copyright © 2016-2025 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.cf; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.TestPropertySource; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.cf.CalculatedField; +import org.thingsboard.server.common.data.cf.CalculatedFieldType; +import org.thingsboard.server.common.data.cf.configuration.Argument; +import org.thingsboard.server.common.data.cf.configuration.ArgumentType; +import org.thingsboard.server.common.data.cf.configuration.Output; +import org.thingsboard.server.common.data.cf.configuration.OutputType; +import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey; +import org.thingsboard.server.common.data.cf.configuration.aggregation.AggFunction; +import org.thingsboard.server.common.data.cf.configuration.aggregation.AggKeyInput; +import org.thingsboard.server.common.data.cf.configuration.aggregation.AggMetric; +import org.thingsboard.server.common.data.cf.configuration.aggregation.single.EntityAggregationCalculatedFieldConfiguration; +import org.thingsboard.server.common.data.cf.configuration.aggregation.single.interval.AggInterval; +import org.thingsboard.server.common.data.cf.configuration.aggregation.single.interval.CustomInterval; +import org.thingsboard.server.common.data.cf.configuration.aggregation.single.interval.Watermark; +import org.thingsboard.server.common.data.debug.DebugSettings; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.security.Authority; +import org.thingsboard.server.controller.AbstractControllerTest; +import org.thingsboard.server.dao.service.DaoSqlTest; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; +import static org.thingsboard.server.cf.CalculatedFieldIntegrationTest.POLL_INTERVAL; + +@DaoSqlTest +@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD) +@TestPropertySource(properties = { + "actors.calculated_fields.check_interval=1" +}) +public class EntityAggregationCalculatedFieldTest extends AbstractControllerTest { + + private Tenant savedTenant; + + @Before + public void beforeEach() throws Exception { + loginSysAdmin(); + + updateDefaultTenantProfileConfig(tenantProfileConfig -> { + tenantProfileConfig.setMinAllowedDeduplicationIntervalInSecForCF(1); + tenantProfileConfig.setMinAllowedAggregationIntervalInSecForCF(1); + }); + + Tenant tenant = new Tenant(); + tenant.setTitle("My tenant"); + savedTenant = saveTenant(tenant); + assertThat(savedTenant).isNotNull(); + + User tenantAdmin = new User(); + tenantAdmin.setAuthority(Authority.TENANT_ADMIN); + tenantAdmin.setTenantId(savedTenant.getId()); + tenantAdmin.setEmail("tenant@thingsboard.org"); + tenantAdmin.setFirstName("John"); + tenantAdmin.setLastName("Doe"); + + createUserAndLogin(tenantAdmin, "testPassword"); + } + + @After + public void afterTest() throws Exception { + loginSysAdmin(); + + deleteTenant(savedTenant.getId()); + } + + @Test + public void testCreateCfAndNoTelemetryDuringInterval_checkAggregation() throws Exception { + Device device = createDevice("Device", "1234567890111"); + + CustomInterval customInterval = new CustomInterval("Europe/Kyiv", 0L, 5L); + long intervalEndTs = customInterval.getCurrentIntervalEndTs(); + + CalculatedField totalConsumptionCF = createTotalConsumptionCF(device.getId(), customInterval, null); + long interval = customInterval.getCurrentIntervalDurationMillis(); + + await().alias("create CF and no telemetry during interval -> save metric with default value") + .atMost(2 * interval, TimeUnit.MILLISECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + ObjectNode result = getLatestTelemetry(device.getId(), "consumption"); + assertThat(result).isNotNull(); + assertThat(result.get("consumption").get(0).get("value").asText()).isEqualTo("9999"); + }); + } + + @Test + public void testCreateCfWithoutWatermark_checkAggregation() throws Exception { + Device device = createDevice("Device", "1234567890111"); + + CustomInterval customInterval = new CustomInterval("Europe/Kyiv", 0L, 5L); + long currentIntervalStartTs = customInterval.getCurrentIntervalStartTs(); + long currentIntervalEndTs = customInterval.getCurrentIntervalEndTs(); + + long tsBeforeInterval = currentIntervalStartTs - 1000; + long tsInInterval_1 = currentIntervalStartTs + 1000; + long tsInInterval_2 = currentIntervalStartTs + 500; + long tsInInterval_3 = currentIntervalStartTs + 200; + postTelemetry(device.getId(), String.format("{\"ts\": \"%s\", \"values\": {\"energy\":120}}", tsBeforeInterval)); + postTelemetry(device.getId(), String.format("{\"ts\": \"%s\", \"values\": {\"energy\":100}}", tsInInterval_1)); + postTelemetry(device.getId(), String.format("{\"ts\": \"%s\", \"values\": {\"energy\":180}}", tsInInterval_2)); + postTelemetry(device.getId(), String.format("{\"ts\": \"%s\", \"values\": {\"energy\":120}}", tsInInterval_3)); + + long interval = customInterval.getCurrentIntervalDurationMillis(); + CalculatedField totalConsumptionCF = createTotalConsumptionCF(device.getId(), customInterval, null); + + await().alias("create CF -> perform aggregation after interval end") + .atMost(2 * interval, TimeUnit.MILLISECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + ObjectNode result = getLatestTelemetry(device.getId(), "consumption"); + assertThat(result).isNotNull(); + assertThat(result.get("consumption").get(0).get("value").asText()).isEqualTo("400"); + }); + + postTelemetry(device.getId(), String.format("{\"ts\": \"%s\", \"values\": {\"energy\":500}}", tsInInterval_1)); + + await().alias("update telemetry that belongs to previous interval -> no aggregation since watermark is not set ") + .atMost(2 * interval, TimeUnit.MILLISECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + ObjectNode result = getLatestTelemetry(device.getId(), "consumption"); + assertThat(result).isNotNull(); + assertThat(result.get("consumption").get(0).get("value").asText()).isEqualTo("400"); + }); + } + + @Test + public void testCreateCfWithWatermark_checkAggregationDuringWatermark() throws Exception { + Device device = createDevice("Device", "1234567890111"); + + CustomInterval customInterval = new CustomInterval("Europe/Kyiv", 0L, 5L); + long currentIntervalStartTs = customInterval.getCurrentIntervalStartTs(); + long currentIntervalEndTs = customInterval.getCurrentIntervalEndTs(); + + long tsBeforeInterval = currentIntervalStartTs - 1000L; + long tsInInterval_1 = currentIntervalStartTs + 1000L; + long tsInInterval_2 = currentIntervalStartTs + 500L; + long tsInInterval_3 = currentIntervalStartTs + 200L; + postTelemetry(device.getId(), String.format("{\"ts\": \"%s\", \"values\": {\"energy\":120}}", tsBeforeInterval)); + postTelemetry(device.getId(), String.format("{\"ts\": \"%s\", \"values\": {\"energy\":100}}", tsInInterval_1)); + postTelemetry(device.getId(), String.format("{\"ts\": \"%s\", \"values\": {\"energy\":180}}", tsInInterval_2)); + postTelemetry(device.getId(), String.format("{\"ts\": \"%s\", \"values\": {\"energy\":120}}", tsInInterval_3)); + + long interval = customInterval.getCurrentIntervalDurationMillis(); + Watermark watermark = new Watermark(10); + CalculatedField totalConsumptionCF = createTotalConsumptionCF(device.getId(), customInterval, watermark); + + await().alias("create CF -> perform aggregation after interval end") + .atMost(2 * interval, TimeUnit.MILLISECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + ObjectNode result = getLatestTelemetry(device.getId(), "consumption"); + assertThat(result).isNotNull(); + assertThat(result.get("consumption").get(0).get("value").asText()).isEqualTo("400"); + }); + + postTelemetry(device.getId(), String.format("{\"ts\": \"%s\", \"values\": {\"energy\":300}}", tsInInterval_1)); + + await().alias("update telemetry during watermark -> perform aggregation") + .atMost(2 * 10, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + ObjectNode result = getLatestTelemetry(device.getId(), "consumption"); + assertThat(result).isNotNull(); + assertThat(result.get("consumption").get(0).get("value").asText()).isEqualTo("600"); + }); + } + + private CalculatedField createTotalConsumptionCF(EntityId entityId, AggInterval aggInterval, Watermark watermark) { + Map arguments = new HashMap<>(); + Argument argument = new Argument(); + argument.setRefEntityKey(new ReferencedEntityKey("energy", ArgumentType.TS_LATEST, null)); + arguments.put("en", argument); + + Map aggMetrics = new HashMap<>(); + + AggMetric consumption = new AggMetric(); + consumption.setFunction(AggFunction.SUM); + consumption.setInput(new AggKeyInput("en")); + consumption.setDefaultValue(9999L); + aggMetrics.put("consumption", consumption); + + Output output = new Output(); + output.setType(OutputType.TIME_SERIES); + output.setDecimalsByDefault(0); + + return createAggCf("Consumption per minute", entityId, + aggInterval, + watermark, + arguments, + aggMetrics, + output); + } + + private CalculatedField createAggCf(String name, + EntityId entityId, + AggInterval aggInterval, + Watermark watermark, + Map inputs, + Map metrics, + Output output) { + CalculatedField calculatedField = new CalculatedField(); + calculatedField.setName(name); + calculatedField.setEntityId(entityId); + calculatedField.setType(CalculatedFieldType.ENTITY_AGGREGATION); + + EntityAggregationCalculatedFieldConfiguration configuration = new EntityAggregationCalculatedFieldConfiguration(); + + configuration.setArguments(inputs); + configuration.setMetrics(metrics); + configuration.setInterval(aggInterval); + if (watermark != null) { + configuration.setWatermark(watermark); + } + configuration.setOutput(output); + + calculatedField.setConfiguration(configuration); + calculatedField.setDebugSettings(DebugSettings.all()); + return saveCalculatedField(calculatedField); + } + + private ObjectNode getLatestTelemetry(EntityId entityId, String... keys) throws Exception { + return doGetAsync("/api/plugins/telemetry/" + entityId.getEntityType() + "/" + entityId.getId() + "/values/timeseries?keys=" + String.join(",", keys), ObjectNode.class); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/cf/RelatedEntitiesAggregationCalculatedFieldTest.java b/application/src/test/java/org/thingsboard/server/cf/RelatedEntitiesAggregationCalculatedFieldTest.java new file mode 100644 index 0000000000..48c4e67608 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/cf/RelatedEntitiesAggregationCalculatedFieldTest.java @@ -0,0 +1,846 @@ +/** + * Copyright © 2016-2025 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.cf; + +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.springframework.test.annotation.DirtiesContext; +import org.thingsboard.server.common.data.AttributeScope; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.asset.Asset; +import org.thingsboard.server.common.data.asset.AssetProfile; +import org.thingsboard.server.common.data.cf.CalculatedField; +import org.thingsboard.server.common.data.cf.CalculatedFieldType; +import org.thingsboard.server.common.data.cf.configuration.Argument; +import org.thingsboard.server.common.data.cf.configuration.ArgumentType; +import org.thingsboard.server.common.data.cf.configuration.Output; +import org.thingsboard.server.common.data.cf.configuration.OutputType; +import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey; +import org.thingsboard.server.common.data.cf.configuration.aggregation.AggFunction; +import org.thingsboard.server.common.data.cf.configuration.aggregation.AggFunctionInput; +import org.thingsboard.server.common.data.cf.configuration.aggregation.AggKeyInput; +import org.thingsboard.server.common.data.cf.configuration.aggregation.AggMetric; +import org.thingsboard.server.common.data.cf.configuration.aggregation.RelatedEntitiesAggregationCalculatedFieldConfiguration; +import org.thingsboard.server.common.data.debug.DebugSettings; +import org.thingsboard.server.common.data.device.data.DefaultDeviceConfiguration; +import org.thingsboard.server.common.data.device.data.DefaultDeviceTransportConfiguration; +import org.thingsboard.server.common.data.device.data.DeviceData; +import org.thingsboard.server.common.data.id.AssetProfileId; +import org.thingsboard.server.common.data.id.DeviceProfileId; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.relation.EntityRelation; +import org.thingsboard.server.common.data.relation.EntitySearchDirection; +import org.thingsboard.server.common.data.relation.RelationPathLevel; +import org.thingsboard.server.common.data.relation.RelationTypeGroup; +import org.thingsboard.server.common.data.rule.RuleChain; +import org.thingsboard.server.common.data.security.Authority; +import org.thingsboard.server.controller.AbstractControllerTest; +import org.thingsboard.server.dao.service.DaoSqlTest; + +import java.util.HashMap; +import java.util.Map; +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; +import static org.thingsboard.server.cf.CalculatedFieldIntegrationTest.POLL_INTERVAL; + +@DaoSqlTest +@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD) +public class RelatedEntitiesAggregationCalculatedFieldTest extends AbstractControllerTest { + + private Tenant savedTenant; + + private DeviceProfile deviceProfile; + private Device device1; + private String accessToken1 = "1234567890111"; + private Device device2; + private String accessToken2 = "1234567890222"; + + private AssetProfile assetProfile; + private Asset asset; + + private final long deduplicationInterval = 5; + + @Before + public void beforeEach() throws Exception { + loginSysAdmin(); + + updateDefaultTenantProfileConfig(tenantProfileConfig -> { + tenantProfileConfig.setMinAllowedDeduplicationIntervalInSecForCF(1); + tenantProfileConfig.setMinAllowedScheduledUpdateIntervalInSecForCF(1); + }); + + Tenant tenant = new Tenant(); + tenant.setTitle("My tenant"); + savedTenant = saveTenant(tenant); + assertThat(savedTenant).isNotNull(); + + User tenantAdmin = new User(); + tenantAdmin.setAuthority(Authority.TENANT_ADMIN); + tenantAdmin.setTenantId(savedTenant.getId()); + tenantAdmin.setEmail("tenant@thingsboard.org"); + tenantAdmin.setFirstName("John"); + tenantAdmin.setLastName("Doe"); + + createUserAndLogin(tenantAdmin, "testPassword"); + + deviceProfile = doPost("/api/deviceProfile", createDeviceProfile("Device Profile"), DeviceProfile.class); + device1 = createDevice("Device 1", deviceProfile.getId(), accessToken1); + device2 = createDevice("Device 2", deviceProfile.getId(), accessToken2); + + postTelemetry(device1.getId(), "{\"occupied\":true}"); + postTelemetry(device2.getId(), "{\"occupied\":false}"); + + assetProfile = doPost("/api/assetProfile", createAssetProfile("Asset Profile"), AssetProfile.class); + asset = createAsset("Asset", assetProfile.getId()); + + createEntityRelation(asset.getId(), device1.getId(), "Contains"); + createEntityRelation(asset.getId(), device2.getId(), "Contains"); + } + + @After + public void afterTest() throws Exception { + loginSysAdmin(); + + deleteTenant(savedTenant.getId()); + } + + @Test + public void testCreateCfOnProfile_checkInitialAggregation() throws Exception { + Asset asset2 = createAsset("Asset 2", assetProfile.getId()); + Device device3 = createDevice("Device 3", "1234567890333"); + Device device4 = createDevice("Device 4", "1234567890444"); + + createEntityRelation(asset2.getId(), device3.getId(), "Contains"); + createEntityRelation(asset2.getId(), device4.getId(), "Contains"); + + createOccupancyCF(assetProfile.getId()); + + await().alias("create CF and perform initial aggregation").atMost(TIMEOUT, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + verifyTelemetry(asset.getId(), Map.of( + "freeSpaces", "1", + "occupiedSpaces", "1", + "totalSpaces", "2" + )); + + verifyTelemetry(asset2.getId(), Map.of( + "freeSpaces", "2", + "occupiedSpaces", "0", + "totalSpaces", "2" + )); + }); + + postTelemetry(device3.getId(), "{\"occupied\":true}"); + + await().alias("update telemetry and perform aggregation") + .atLeast(deduplicationInterval / 2, TimeUnit.SECONDS) + .atMost(TIMEOUT, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + verifyTelemetry(asset2.getId(), Map.of( + "freeSpaces", "1", + "occupiedSpaces", "1", + "totalSpaces", "2" + )); + }); + } + + @Test + public void testAddEntityToProfile_checkAggregation() throws Exception { + createOccupancyCF(assetProfile.getId()); + + Device device3 = createDevice("Device 3", "1234567890333"); + Device device4 = createDevice("Device 4", "1234567890444"); + postTelemetry(device3.getId(), "{\"occupied\":true}"); + postTelemetry(device4.getId(), "{\"occupied\":true}"); + + Asset asset2 = createAsset("Asset 2", assetProfile.getId()); + + await().alias("add entity to profile with no related entities and perform aggregation").atMost(deduplicationInterval * 2, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + ObjectNode occupancy = getLatestTelemetry(asset2.getId(), "freeSpaces", "occupiedSpaces", "totalSpaces"); + assertThat(occupancy).isNotNull(); + assertThat(occupancy.get("freeSpaces").get(0).get("value").isNull()).isTrue(); + assertThat(occupancy.get("occupiedSpaces").get(0).get("value").isNull()).isTrue(); + assertThat(occupancy.get("totalSpaces").get(0).get("value").isNull()).isTrue(); + }); + + createEntityRelation(asset2.getId(), device3.getId(), "Contains"); + createEntityRelation(asset2.getId(), device4.getId(), "Contains"); + + await().alias("create relations and perform aggregation").atMost(deduplicationInterval * 2, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + verifyTelemetry(asset2.getId(), Map.of( + "freeSpaces", "0", + "occupiedSpaces", "2", + "totalSpaces", "2" + )); + }); + + postTelemetry(device3.getId(), "{\"occupied\":false}"); + + await().alias("update telemetry and perform aggregation").atMost(deduplicationInterval * 2, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + verifyTelemetry(asset2.getId(), Map.of( + "freeSpaces", "1", + "occupiedSpaces", "1", + "totalSpaces", "2" + )); + }); + } + + @Test + public void testChangeEntityProfile_checkAggregation() throws Exception { + Asset asset2 = createAsset("Asset 2", assetProfile.getId()); + Device device3 = createDevice("Device 3", "1234567890333"); + Device device4 = createDevice("Device 4", "1234567890444"); + + createEntityRelation(asset2.getId(), device3.getId(), "Contains"); + createEntityRelation(asset2.getId(), device4.getId(), "Contains"); + + createOccupancyCF(assetProfile.getId()); + + await().alias("create CF and perform initial aggregation").atMost(deduplicationInterval * 2, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + verifyTelemetry(asset.getId(), Map.of( + "freeSpaces", "1", + "occupiedSpaces", "1", + "totalSpaces", "2" + )); + + verifyTelemetry(asset2.getId(), Map.of( + "freeSpaces", "2", + "occupiedSpaces", "0", + "totalSpaces", "2" + )); + }); + + AssetProfile newAssetProfile = createAssetProfile("New Asset Profile"); + asset2.setAssetProfileId(newAssetProfile.getId()); + doPost("/api/asset", asset2, Asset.class); + + postTelemetry(device3.getId(), "{\"occupied\":true}"); + + await().alias("change profile and no aggregation").atMost(deduplicationInterval * 2, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + verifyTelemetry(asset2.getId(), Map.of( + "freeSpaces", "2", + "occupiedSpaces", "0", + "totalSpaces", "2" + )); + }); + } + + @Test + public void testCreateCfOnAssetAndNoTelemetryOnDevices_checkDefaultValueUsed() throws Exception { + Asset asset2 = createAsset("Asset 2", assetProfile.getId()); + Device device3 = createDevice("Device 3", "1234567890333"); + Device device4 = createDevice("Device 4", "1234567890444"); + + createEntityRelation(asset2.getId(), device3.getId(), "Contains"); + createEntityRelation(asset2.getId(), device4.getId(), "Contains"); + + createOccupancyCF(asset2.getId()); + + await().alias("create CF and perform aggregation with default values").atMost(deduplicationInterval * 2, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + verifyTelemetry(asset2.getId(), Map.of( + "freeSpaces", "2", + "occupiedSpaces", "0", + "totalSpaces", "2" + )); + }); + } + + @Test + public void testCreateCfAndUpdateTelemetry_checkAggregation() throws Exception { + createOccupancyCF(asset.getId()); + checkInitialCalculation(); + + postTelemetry(device1.getId(), "{\"occupied\":false}"); + + await().alias("update telemetry and perform aggregation") + .atLeast(deduplicationInterval / 2, TimeUnit.SECONDS) + .atMost(TIMEOUT, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + verifyTelemetry(asset.getId(), Map.of( + "freeSpaces", "2", + "occupiedSpaces", "0", + "totalSpaces", "2" + )); + }); + } + + @Test + public void testCreateCfAndRelationToRuleChain_checkAggregation() throws Exception { + Asset asset2 = createAsset("Asset 2", assetProfile.getId()); + Device device3 = createDevice("Device 3", "1234567890333"); + postTelemetry(device3.getId(), "{\"occupied\":true}"); + + RuleChain ruleChain = new RuleChain(); + ruleChain.setName("RuleChain"); + ruleChain = doPost("/api/ruleChain", ruleChain, RuleChain.class); + postTelemetry(ruleChain.getId(), "{\"occupied\":true}"); + + createEntityRelation(asset2.getId(), device3.getId(), "Contains"); + createEntityRelation(asset2.getId(), ruleChain.getId(), "Contains"); + + createOccupancyCF(asset2.getId()); + + await().alias("create CF and perform initial aggregation").atMost(deduplicationInterval * 2, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + verifyTelemetry(asset2.getId(), Map.of( + "freeSpaces", "0", + "occupiedSpaces", "1", + "totalSpaces", "1" + )); + }); + + postTelemetry(ruleChain.getId(), "{\"occupied\":true}"); + + await().alias("update telemetry on rule chain and no aggregation performed").atMost(deduplicationInterval * 2, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + verifyTelemetry(asset2.getId(), Map.of( + "freeSpaces", "0", + "occupiedSpaces", "1", + "totalSpaces", "1" + )); + }); + } + + @Test + public void testDeleteCf_checkNoAggregation() throws Exception { + CalculatedField cf = createOccupancyCF(asset.getId()); + checkInitialCalculation(); + + doDelete("/api/calculatedField/" + cf.getId().getId().toString()) + .andExpect(status().isOk()); + + postTelemetry(device1.getId(), "{\"occupied\":false}"); + + await().alias("delete cf and update telemetry and no aggregation").atMost(deduplicationInterval * 2, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + verifyTelemetry(asset.getId(), Map.of( + "freeSpaces", "1", + "occupiedSpaces", "1", + "totalSpaces", "2" + )); + }); + } + + @Test + public void testUpdateTelemetry_checkAggregationNotExecutedUntilDeduplicationInterval() throws Exception { + createOccupancyCF(asset.getId()); + checkInitialCalculation(); + + postTelemetry(device1.getId(), "{\"occupied\":false}"); + + await().alias("update telemetry -> no changes").atMost(TIMEOUT, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(this::checkInitialCalculationValues); + + postTelemetry(device2.getId(), "{\"occupied\":false}"); + + await().alias("create CF and perform initial calculation") + .atLeast(deduplicationInterval / 2, TimeUnit.SECONDS) + .atMost(TIMEOUT, TimeUnit.SECONDS) + .untilAsserted(() -> { + verifyTelemetry(asset.getId(), Map.of( + "freeSpaces", "2", + "occupiedSpaces", "0", + "totalSpaces", "2" + )); + }); + } + + @Test + public void testDeleteTelemetry_checkAggregationWithPreviousValuesOrDefault() throws Exception { + Asset asset2 = createAsset("Asset 2", assetProfile.getId()); + Device device3 = createDevice("Device 3", "1234567890333"); + Device device4 = createDevice("Device 4", "1234567890444"); + + createEntityRelation(asset2.getId(), device3.getId(), "Contains"); + createEntityRelation(asset2.getId(), device4.getId(), "Contains"); + + long currentTime = System.currentTimeMillis(); + long firstTs = currentTime - 10; + long secondTs = currentTime - 10; + long thirdTs = currentTime - 5; + postTelemetry(device3.getId(), "{\"ts\": " + firstTs + ", \"values\": {\"occupied\":true}}"); + postTelemetry(device4.getId(), "{\"ts\": " + secondTs + ", \"values\": {\"occupied\":true}}"); + postTelemetry(device3.getId(), "{\"ts\": " + thirdTs + ", \"values\": {\"occupied\":true}}"); + + createOccupancyCF(asset2.getId()); + + await().alias("create CF and perform aggregation").atMost(deduplicationInterval * 2, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + verifyTelemetry(asset2.getId(), Map.of( + "freeSpaces", "0", + "occupiedSpaces", "2", + "totalSpaces", "2" + )); + }); + + doDelete("/api/plugins/telemetry/DEVICE/" + device3.getId() + "/timeseries/delete?keys=occupied&deleteAllDataForKeys=false&rewriteLatestIfDeleted=true&deleteLatest=true&startTs=" + thirdTs + "&endTs=" + thirdTs + 1, String.class); + doDelete("/api/plugins/telemetry/DEVICE/" + device4.getId() + "/timeseries/delete?keys=occupied&deleteAllDataForKeys=false&rewriteLatestIfDeleted=true&deleteLatest=true&startTs=" + secondTs + "&endTs=" + secondTs + 1, String.class); + + await().alias("delete latest telemetry and perform aggregation with previous or default values").atMost(deduplicationInterval * 2, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + verifyTelemetry(asset2.getId(), Map.of( + "freeSpaces", "1", + "occupiedSpaces", "1", + "totalSpaces", "2" + )); + }); + } + + @Test + public void testDeleteAttr_checkAggregationWithDefault() throws Exception { + Asset asset2 = createAsset("Asset 2", assetProfile.getId()); + Device device3 = createDevice("Device 3", "1234567890333"); + Device device4 = createDevice("Device 4", "1234567890444"); + + createEntityRelation(asset2.getId(), device3.getId(), "Contains"); + createEntityRelation(asset2.getId(), device4.getId(), "Contains"); + + postAttributes(device3.getId(), AttributeScope.SERVER_SCOPE, "{\"occupied\":true}"); + postAttributes(device4.getId(), AttributeScope.SERVER_SCOPE, "{\"occupied\":true}"); + + createOccupancyCFWithAttr(asset2.getId()); + + await().alias("create CF and perform aggregation").atMost(deduplicationInterval * 2, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + verifyTelemetry(asset2.getId(), Map.of( + "freeSpaces", "0", + "occupiedSpaces", "2", + "totalSpaces", "2" + )); + }); + + doDelete("/api/plugins/telemetry/DEVICE/" + device3.getUuidId() + "/SERVER_SCOPE?keys=occupied", String.class); + doDelete("/api/plugins/telemetry/DEVICE/" + device4.getUuidId() + "/SERVER_SCOPE?keys=occupied", String.class); + + await().alias("delete attribute and perform aggregation with default values").atMost(deduplicationInterval * 2, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + verifyTelemetry(asset2.getId(), Map.of( + "freeSpaces", "2", + "occupiedSpaces", "0", + "totalSpaces", "2" + )); + }); + } + + @Test + public void testCreateRelation_checkAggregation() throws Exception { + createOccupancyCF(asset.getId()); + checkInitialCalculation(); + + Device device3 = createDevice("Device 3", deviceProfile.getId(), "1234567890333"); + + postTelemetry(device3.getId(), "{\"occupied\":true}"); + + createEntityRelation(asset.getId(), device3.getId(), "Contains"); + + await().alias("create relation and perform aggregation").atMost(deduplicationInterval * 2, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + verifyTelemetry(asset.getId(), Map.of( + "freeSpaces", "1", + "occupiedSpaces", "2", + "totalSpaces", "3" + )); + }); + } + + @Test + public void testDeleteRelation_checkAggregation() throws Exception { + createOccupancyCF(asset.getId()); + checkInitialCalculation(); + + deleteEntityRelation(new EntityRelation(asset.getId(), device1.getId(), "Contains", RelationTypeGroup.COMMON)); + + await().alias("create relation and perform aggregation").atMost(deduplicationInterval * 2, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + verifyTelemetry(asset.getId(), Map.of( + "freeSpaces", "1", + "occupiedSpaces", "0", + "totalSpaces", "1" + )); + }); + } + + @Test + public void testDeleteEntityByRelation_checkAggregation() throws Exception { + createOccupancyCF(asset.getId()); + checkInitialCalculation(); + + doDelete("/api/device/" + device1.getId()).andExpect(status().isOk()); + + await().alias("create relation and perform aggregation").atMost(deduplicationInterval * 2, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + verifyTelemetry(asset.getId(), Map.of( + "freeSpaces", "1", + "occupiedSpaces", "0", + "totalSpaces", "1" + )); + }); + } + + @Test + public void testUpdateRelationPath_checkAggregation() throws Exception { + CalculatedField cf = createOccupancyCF(asset.getId()); + checkInitialCalculation(); + + Device device3 = createDevice("Device 3", "1234567890333"); + createEntityRelation(asset.getId(), device3.getId(), "Has"); + postTelemetry(device3.getId(), "{\"occupied\":true}"); + + var configuration = (RelatedEntitiesAggregationCalculatedFieldConfiguration) cf.getConfiguration(); + configuration.setRelation(new RelationPathLevel(EntitySearchDirection.FROM, "Has")); + saveCalculatedField(cf); + + await().alias("update relation path and perform aggregation").atMost(deduplicationInterval * 2, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + verifyTelemetry(asset.getId(), Map.of( + "freeSpaces", "0", + "occupiedSpaces", "1", + "totalSpaces", "1" + )); + }); + } + + @Test + public void testUpdateArguments_checkAggregation() throws Exception { + CalculatedField cf = createOccupancyCF(asset.getId()); + checkInitialCalculation(); + + postTelemetry(device1.getId(), "{\"occupiedStatus\":false}"); + postTelemetry(device2.getId(), "{\"occupiedStatus\":false}"); + + var configuration = (RelatedEntitiesAggregationCalculatedFieldConfiguration) cf.getConfiguration(); + Argument argument = new Argument(); + argument.setRefEntityKey(new ReferencedEntityKey("oc", ArgumentType.TS_LATEST, null)); + argument.setDefaultValue("false"); + configuration.setArguments(Map.of("oc", argument)); + saveCalculatedField(cf); + + await().alias("update arguments and perform aggregation").atMost(deduplicationInterval * 2, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + verifyTelemetry(asset.getId(), Map.of( + "freeSpaces", "2", + "occupiedSpaces", "0", + "totalSpaces", "2" + )); + }); + } + + @Test + public void testUpdateMetrics_checkAggregation() throws Exception { + postTelemetry(device1.getId(), "{\"temperature\":24.2}"); + postTelemetry(device2.getId(), "{\"temperature\":19.6}"); + CalculatedField cf = createAvgTemperatureCF(asset.getId()); + + await().alias("create avg temp cf and perform initial aggregation").atMost(TIMEOUT, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + verifyTelemetry(asset.getId(), Map.of("avgTemperature", "24")); + }); + + var configuration = (RelatedEntitiesAggregationCalculatedFieldConfiguration) cf.getConfiguration(); + AggMetric aggMetric = new AggMetric(); + aggMetric.setInput(new AggKeyInput("temp")); + aggMetric.setFilter("return temp < 100;"); + aggMetric.setFunction(AggFunction.MAX); + configuration.setMetrics(Map.of("maxTemperature", aggMetric)); + saveCalculatedField(cf); + + await().alias("update metrics and perform aggregation").atMost(TIMEOUT, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + verifyTelemetry(asset.getId(), Map.of("maxTemperature", "24")); + }); + + postTelemetry(device1.getId(), "{\"temperature\":101.3}"); + postTelemetry(device2.getId(), "{\"temperature\":25.8}"); + + await().alias("update telemetry and perform aggregation") + .atLeast(deduplicationInterval / 2, TimeUnit.SECONDS) + .atMost(TIMEOUT, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + verifyTelemetry(asset.getId(), Map.of("maxTemperature", "26")); + }); + } + + @Test + public void testUpdateOutput_checkAggregation() throws Exception { + postTelemetry(device1.getId(), "{\"temperature\":24.2}"); + postTelemetry(device2.getId(), "{\"temperature\":19.6}"); + CalculatedField cf = createAvgTemperatureCF(asset.getId()); + + await().alias("create avg temp cf and perform initial aggregation").atMost(deduplicationInterval * 2, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + verifyTelemetry(asset.getId(), Map.of("avgTemperature", "24")); + }); + + var configuration = (RelatedEntitiesAggregationCalculatedFieldConfiguration) cf.getConfiguration(); + Output output = new Output(); + output.setType(OutputType.ATTRIBUTES); + output.setScope(AttributeScope.SERVER_SCOPE); + configuration.setOutput(output); + saveCalculatedField(cf); + + await().alias("update output and perform aggregation").atMost(deduplicationInterval * 2, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + ArrayNode avgTemperature = getServerAttributes(asset.getId(), "avgTemperature"); + assertThat(avgTemperature).isNotNull(); + assertThat(avgTemperature.get(0)).isNotNull(); + assertThat(avgTemperature.get(0).get("value").asText()).isEqualTo("24.2"); + }); + } + + @Test + public void testUpdateDeduplicationInterval_checkAggregationNotExecutedUntilDeduplicationInterval() throws Exception { + postTelemetry(device1.getId(), "{\"temperature\":24.2}"); + postTelemetry(device2.getId(), "{\"temperature\":19.6}"); + CalculatedField cf = createAvgTemperatureCF(asset.getId()); + + await().alias("create avg temp cf and perform initial aggregation").atMost(deduplicationInterval * 2, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + verifyTelemetry(asset.getId(), Map.of("avgTemperature", "24")); + }); + + var configuration = (RelatedEntitiesAggregationCalculatedFieldConfiguration) cf.getConfiguration(); + configuration.setDeduplicationIntervalInSec(2 * deduplicationInterval); + saveCalculatedField(cf); + + await().alias("update deduplication interval and perform aggregation").atMost(deduplicationInterval / 2, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + verifyTelemetry(asset.getId(), Map.of("avgTemperature", "24")); + }); + + postTelemetry(device2.getId(), "{\"temperature\":32.1}"); + + await().alias("update telemetry and perform aggregation").atMost(2 * deduplicationInterval + 10, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + verifyTelemetry(asset.getId(), Map.of("avgTemperature", "28")); + }); + } + + private void checkInitialCalculation() { + await().alias("create CF and perform initial aggregation").atMost(deduplicationInterval * 2, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(this::checkInitialCalculationValues); + } + + private void checkInitialCalculationValues() throws Exception { + ObjectNode occupancy = getLatestTelemetry(asset.getId(), "freeSpaces", "occupiedSpaces", "totalSpaces"); + assertThat(occupancy).isNotNull(); + assertThat(occupancy.get("freeSpaces").get(0).get("value").asText()).isEqualTo("1"); + assertThat(occupancy.get("occupiedSpaces").get(0).get("value").asText()).isEqualTo("1"); + assertThat(occupancy.get("totalSpaces").get(0).get("value").asText()).isEqualTo("2"); + } + + private CalculatedField createAvgTemperatureCF(EntityId entityId) { + Map arguments = new HashMap<>(); + Argument argument = new Argument(); + argument.setRefEntityKey(new ReferencedEntityKey("temperature", ArgumentType.TS_LATEST, null)); + argument.setDefaultValue("20"); + arguments.put("temp", argument); + + Map aggMetrics = new HashMap<>(); + + AggMetric avgMetric = new AggMetric(); + avgMetric.setFunction(AggFunction.AVG); + avgMetric.setFilter("return temp >= 20;"); + avgMetric.setInput(new AggKeyInput("temp")); + aggMetrics.put("avgTemperature", avgMetric); + + Output output = new Output(); + output.setType(OutputType.TIME_SERIES); + output.setDecimalsByDefault(0); + + return createAggCf("Average temperature", entityId, + new RelationPathLevel(EntitySearchDirection.FROM, "Contains"), + arguments, + aggMetrics, + output); + } + + private CalculatedField createOccupancyCF(EntityId entityId) { + Map arguments = new HashMap<>(); + Argument argument = new Argument(); + argument.setRefEntityKey(new ReferencedEntityKey("occupied", ArgumentType.TS_LATEST, null)); + argument.setDefaultValue("false"); + arguments.put("oc", argument); + + Map aggMetrics = new HashMap<>(); + + AggMetric freeSpaces = new AggMetric(); + freeSpaces.setFunction(AggFunction.COUNT); + freeSpaces.setFilter("return oc == false;"); + freeSpaces.setInput(new AggKeyInput("oc")); + aggMetrics.put("freeSpaces", freeSpaces); + + AggMetric occupiedSpaces = new AggMetric(); + occupiedSpaces.setFunction(AggFunction.COUNT); + occupiedSpaces.setFilter("return oc == true;"); + occupiedSpaces.setInput(new AggKeyInput("oc")); + aggMetrics.put("occupiedSpaces", occupiedSpaces); + + AggMetric totalSpaces = new AggMetric(); + totalSpaces.setFunction(AggFunction.COUNT); + totalSpaces.setInput(new AggFunctionInput("return 1;")); + aggMetrics.put("totalSpaces", totalSpaces); + + Output output = new Output(); + output.setType(OutputType.TIME_SERIES); + output.setDecimalsByDefault(0); + + return createAggCf("Occupied spaces", entityId, + new RelationPathLevel(EntitySearchDirection.FROM, "Contains"), + arguments, + aggMetrics, + output); + } + + private CalculatedField createOccupancyCFWithAttr(EntityId entityId) { + Map arguments = new HashMap<>(); + Argument argument = new Argument(); + argument.setRefEntityKey(new ReferencedEntityKey("occupied", ArgumentType.ATTRIBUTE, AttributeScope.SERVER_SCOPE)); + argument.setDefaultValue("false"); + arguments.put("oc", argument); + + Map aggMetrics = new HashMap<>(); + + AggMetric freeSpaces = new AggMetric(); + freeSpaces.setFunction(AggFunction.COUNT); + freeSpaces.setFilter("return oc == false;"); + freeSpaces.setInput(new AggKeyInput("oc")); + aggMetrics.put("freeSpaces", freeSpaces); + + AggMetric occupiedSpaces = new AggMetric(); + occupiedSpaces.setFunction(AggFunction.COUNT); + occupiedSpaces.setFilter("return oc == true;"); + occupiedSpaces.setInput(new AggKeyInput("oc")); + aggMetrics.put("occupiedSpaces", occupiedSpaces); + + AggMetric totalSpaces = new AggMetric(); + totalSpaces.setFunction(AggFunction.COUNT); + totalSpaces.setInput(new AggFunctionInput("return 1;")); + aggMetrics.put("totalSpaces", totalSpaces); + + Output output = new Output(); + output.setType(OutputType.TIME_SERIES); + output.setDecimalsByDefault(0); + + return createAggCf("Occupied spaces", entityId, + new RelationPathLevel(EntitySearchDirection.FROM, "Contains"), + arguments, + aggMetrics, + output); + } + + private CalculatedField createAggCf(String name, + EntityId entityId, + RelationPathLevel relation, + Map inputs, + Map metrics, + Output output) { + CalculatedField calculatedField = new CalculatedField(); + calculatedField.setName(name); + calculatedField.setEntityId(entityId); + calculatedField.setType(CalculatedFieldType.RELATED_ENTITIES_AGGREGATION); + + RelatedEntitiesAggregationCalculatedFieldConfiguration configuration = new RelatedEntitiesAggregationCalculatedFieldConfiguration(); + configuration.setRelation(relation); + configuration.setArguments(inputs); + configuration.setDeduplicationIntervalInSec(deduplicationInterval); + configuration.setScheduledUpdateInterval(10); + configuration.setMetrics(metrics); + configuration.setOutput(output); + + calculatedField.setConfiguration(configuration); + calculatedField.setDebugSettings(DebugSettings.all()); + return saveCalculatedField(calculatedField); + } + + private Device createDevice(String name, DeviceProfileId deviceProfileId, String accessToken) { + Device device = new Device(); + device.setName(name); + device.setDeviceProfileId(deviceProfileId); + DeviceData deviceData = new DeviceData(); + deviceData.setTransportConfiguration(new DefaultDeviceTransportConfiguration()); + deviceData.setConfiguration(new DefaultDeviceConfiguration()); + device.setDeviceData(deviceData); + return doPost("/api/device?accessToken=" + accessToken, device, Device.class); + } + + private Asset createAsset(String name, AssetProfileId assetProfileId) { + Asset asset = new Asset(); + asset.setName(name); + asset.setAssetProfileId(assetProfileId); + return doPost("/api/asset", asset, Asset.class); + } + + private void verifyTelemetry(EntityId entityId, Map expectedResults) throws Exception { + ObjectNode result = getLatestTelemetry(entityId, expectedResults.keySet().toArray(new String[0])); + assertThat(result).isNotNull(); + expectedResults.forEach((key, value) -> assertThat(result.get(key).get(0).get("value").asText()).isEqualTo(value)); + } + + private ObjectNode getLatestTelemetry(EntityId entityId, String... keys) throws Exception { + return doGetAsync("/api/plugins/telemetry/" + entityId.getEntityType() + "/" + entityId.getId() + "/values/timeseries?keys=" + String.join(",", keys), ObjectNode.class); + } + + private ArrayNode getServerAttributes(EntityId entityId, String... keys) throws Exception { + return doGetAsync("/api/plugins/telemetry/" + entityId.getEntityType() + "/" + entityId.getId() + "/values/attributes/SERVER_SCOPE?keys=" + String.join(",", keys), ArrayNode.class); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/controller/AbstractNotifyEntityTest.java b/application/src/test/java/org/thingsboard/server/controller/AbstractNotifyEntityTest.java index 7d356af068..d63ca42c62 100644 --- a/application/src/test/java/org/thingsboard/server/controller/AbstractNotifyEntityTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/AbstractNotifyEntityTest.java @@ -128,11 +128,17 @@ public abstract class AbstractNotifyEntityTest extends AbstractWebTest { protected void testNotifyEntityAllOneTimeLogEntityActionEntityEqClass(HasName entity, EntityId entityId, EntityId originatorId, TenantId tenantId, CustomerId customerId, UserId userId, String userName, ActionType actionType, ActionType actionTypeEdge, Object... additionalInfo) { + testNotifyEntityAllOneTimeLogEntityActionEntityEqClass(tenantId, entity, entityId, originatorId, tenantId, customerId, userId, userName, actionType, actionTypeEdge, additionalInfo); + } + + protected void testNotifyEntityAllOneTimeLogEntityActionEntityEqClass(TenantId entityTenantId, HasName entity, EntityId entityId, EntityId originatorId, + TenantId authTenantId, CustomerId customerId, UserId userId, String userName, + ActionType actionType, ActionType actionTypeEdge, Object... additionalInfo) { int cntTime = 1; - testNotificationMsgToEdgeServiceTime(entityId, tenantId, actionTypeEdge, cntTime); - testLogEntityActionEntityEqClass(entity, originatorId, tenantId, customerId, userId, userName, actionType, cntTime, additionalInfo); + testNotificationMsgToEdgeServiceTime(entityId, entityTenantId, actionTypeEdge, cntTime); + testLogEntityActionEntityEqClass(entity, originatorId, authTenantId, customerId, userId, userName, actionType, cntTime, additionalInfo); ArgumentMatcher matcherOriginatorId = argument -> argument.equals(originatorId); - testPushMsgToRuleEngineTime(matcherOriginatorId, tenantId, entity, cntTime); + testPushMsgToRuleEngineTime(matcherOriginatorId, authTenantId, entity, cntTime); Mockito.reset(tbClusterService, auditLogService); } @@ -159,17 +165,26 @@ public abstract class AbstractNotifyEntityTest extends AbstractWebTest { TenantId tenantId, CustomerId customerId, UserId userId, String userName, ActionType actionType, int cntTime, int cntTimeEdge, int cntTimeRuleEngine, Object... additionalInfo) { + testNotifyManyEntityManyTimeMsgToEdgeServiceEntityEqAny(tenantId, entity, originator, tenantId, customerId, userId, userName, actionType, + cntTime, cntTimeEdge, cntTimeRuleEngine, additionalInfo); + } + + protected void testNotifyManyEntityManyTimeMsgToEdgeServiceEntityEqAny(TenantId entityTenantId, HasName entity, HasName originator, + TenantId authTenantId, CustomerId customerId, UserId userId, String userName, + ActionType actionType, + int cntTime, int cntTimeEdge, int cntTimeRuleEngine, Object... additionalInfo) { EntityId originatorId = createEntityId_NULL_UUID(originator); - testSendNotificationMsgToEdgeServiceTimeEntityEqAny(tenantId, actionType, cntTimeEdge); + testSendNotificationMsgToEdgeServiceTimeEntityEqAny(entityTenantId, actionType, cntTimeEdge); ArgumentMatcher matcherEntityClassEquals = argument -> argument.getClass().equals(entity.getClass()); ArgumentMatcher matcherOriginatorId = argument -> argument.getClass().equals(originatorId.getClass()); ArgumentMatcher matcherCustomerId = customerId == null ? argument -> argument.getClass().equals(CustomerId.class) : argument -> argument.equals(customerId); ArgumentMatcher matcherUserId = userId == null ? argument -> argument.getClass().equals(UserId.class) : argument -> argument.equals(userId); - testLogEntityActionAdditionalInfo(matcherEntityClassEquals, matcherOriginatorId, tenantId, matcherCustomerId, matcherUserId, userName, actionType, cntTime, + testLogEntityActionAdditionalInfo(matcherEntityClassEquals, matcherOriginatorId, authTenantId, matcherCustomerId, matcherUserId, userName, actionType, cntTime, extractMatcherAdditionalInfoClass(additionalInfo)); - testPushMsgToRuleEngineTime(matcherOriginatorId, tenantId, entity, cntTimeRuleEngine); + testPushMsgToRuleEngineTime(matcherOriginatorId, authTenantId, entity, cntTimeRuleEngine); + } protected void testNotifyManyEntityManyTimeMsgToEdgeServiceEntityEqAnyAdditionalInfoAny(HasName entity, HasName originator, diff --git a/application/src/test/java/org/thingsboard/server/controller/AbstractRuleEngineControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/AbstractRuleEngineControllerTest.java index cf9d2feb23..89b2681015 100644 --- a/application/src/test/java/org/thingsboard/server/controller/AbstractRuleEngineControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/AbstractRuleEngineControllerTest.java @@ -15,19 +15,13 @@ */ package org.thingsboard.server.controller; -import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.JsonNode; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.context.TestPropertySource; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.EventInfo; -import org.thingsboard.server.common.data.event.EventType; -import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.RuleChainId; -import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.msg.TbMsgType; -import org.thingsboard.server.common.data.page.PageData; -import org.thingsboard.server.common.data.page.TimePageLink; import org.thingsboard.server.common.data.rule.RuleChain; import org.thingsboard.server.common.data.rule.RuleChainMetaData; import org.thingsboard.server.dao.rule.RuleChainService; @@ -61,18 +55,6 @@ public abstract class AbstractRuleEngineControllerTest extends AbstractControlle return doGet("/api/ruleChain/metadata/" + ruleChainId.getId().toString(), RuleChainMetaData.class); } - protected PageData getDebugEvents(TenantId tenantId, EntityId entityId, int limit) throws Exception { - return getEvents(tenantId, entityId, EventType.DEBUG_RULE_NODE.getOldName(), limit); - } - - protected PageData getEvents(TenantId tenantId, EntityId entityId, String eventType, int limit) throws Exception { - TimePageLink pageLink = new TimePageLink(limit); - return doGetTypedWithTimePageLink("/api/events/{entityType}/{entityId}/{eventType}?tenantId={tenantId}&", - new TypeReference>() { - }, pageLink, entityId.getEntityType(), entityId.getId(), eventType, tenantId.getId()); - } - - protected JsonNode getMetadata(EventInfo outEvent) { String metaDataStr = outEvent.getBody().get("metadata").asText(); try { diff --git a/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java b/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java index b93ff0f623..76434b5766 100644 --- a/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java @@ -68,17 +68,23 @@ import org.thingsboard.rule.engine.api.MailService; import org.thingsboard.server.actors.DefaultTbActorSystem; import org.thingsboard.server.actors.TbActorId; import org.thingsboard.server.actors.TbActorMailbox; +import org.thingsboard.server.actors.TbCalculatedFieldEntityActorId; import org.thingsboard.server.actors.TbEntityActorId; +import org.thingsboard.server.actors.calculatedField.CalculatedFieldEntityActor; +import org.thingsboard.server.actors.calculatedField.CalculatedFieldEntityMessageProcessor; import org.thingsboard.server.actors.device.DeviceActor; import org.thingsboard.server.actors.device.DeviceActorMessageProcessor; import org.thingsboard.server.actors.device.SessionInfo; import org.thingsboard.server.actors.device.ToDeviceRpcRequestMetadata; import org.thingsboard.server.actors.service.DefaultActorService; +import org.thingsboard.server.common.data.AttributeScope; import org.thingsboard.server.common.data.Customer; +import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.DeviceProfileType; import org.thingsboard.server.common.data.DeviceTransportType; +import org.thingsboard.server.common.data.EventInfo; import org.thingsboard.server.common.data.SaveDeviceWithCredentialsRequest; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.TbResourceInfo; @@ -86,6 +92,8 @@ import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.asset.AssetProfile; +import org.thingsboard.server.common.data.cf.CalculatedField; +import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.device.data.DefaultDeviceConfiguration; import org.thingsboard.server.common.data.device.data.DefaultDeviceTransportConfiguration; import org.thingsboard.server.common.data.device.data.DeviceData; @@ -98,7 +106,9 @@ import org.thingsboard.server.common.data.device.profile.MqttTopics; import org.thingsboard.server.common.data.device.profile.ProtoTransportPayloadConfiguration; import org.thingsboard.server.common.data.device.profile.TransportPayloadTypeConfiguration; import org.thingsboard.server.common.data.edge.Edge; +import org.thingsboard.server.common.data.event.EventType; import org.thingsboard.server.common.data.exception.ThingsboardException; +import org.thingsboard.server.common.data.id.CalculatedFieldId; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.EntityId; @@ -138,6 +148,7 @@ import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.common.data.security.DeviceCredentials; import org.thingsboard.server.common.data.security.DeviceCredentialsType; +import org.thingsboard.server.common.data.security.model.JwtPair; import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; import org.thingsboard.server.common.data.tenant.profile.TenantProfileData; import org.thingsboard.server.common.msg.session.FeatureType; @@ -150,6 +161,8 @@ import org.thingsboard.server.dao.tenant.TenantProfileService; import org.thingsboard.server.dao.timeseries.TimeseriesService; import org.thingsboard.server.queue.memory.InMemoryStorage; import org.thingsboard.server.service.cf.CfRocksDb; +import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldState; +import org.thingsboard.server.service.cf.ctx.state.geofencing.GeofencingCalculatedFieldState; import org.thingsboard.server.service.entitiy.tenant.profile.TbTenantProfileService; import org.thingsboard.server.service.security.auth.jwt.RefreshTokenRequest; import org.thingsboard.server.service.security.auth.rest.LoginRequest; @@ -197,13 +210,13 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest { protected static final String TEST_DIFFERENT_TENANT_NAME = "TEST DIFFERENT TENANT"; protected static final String SYS_ADMIN_EMAIL = "sysadmin@thingsboard.org"; - private static final String SYS_ADMIN_PASSWORD = "sysadmin"; + protected static final String SYS_ADMIN_PASSWORD = "sysadmin"; protected static final String TENANT_ADMIN_EMAIL = "testtenant@thingsboard.org"; protected static final String TENANT_ADMIN_PASSWORD = "tenant"; protected static final String DIFFERENT_TENANT_ADMIN_EMAIL = "testdifftenant@thingsboard.org"; - private static final String DIFFERENT_TENANT_ADMIN_PASSWORD = "difftenant"; + protected static final String DIFFERENT_TENANT_ADMIN_PASSWORD = "difftenant"; protected static final String CUSTOMER_USER_EMAIL = "testcustomer@thingsboard.org"; private static final String CUSTOMER_USER_PASSWORD = "customer"; @@ -596,8 +609,13 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest { Assert.assertNotNull(tokenInfo); Assert.assertTrue(tokenInfo.has("token")); Assert.assertTrue(tokenInfo.has("refreshToken")); - String token = tokenInfo.get("token").asText(); - String refreshToken = tokenInfo.get("refreshToken").asText(); + validateAndSetJwtToken(JacksonUtil.treeToValue(tokenInfo, JwtPair.class), username); + } + + protected void validateAndSetJwtToken(JwtPair jwtPair, String username) { + Assert.assertNotNull(jwtPair); + String token = jwtPair.getToken(); + String refreshToken = jwtPair.getRefreshToken(); validateJwtToken(token, username); validateJwtToken(refreshToken, username); this.token = token; @@ -668,9 +686,14 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest { } protected Device createDevice(String name, String accessToken) throws Exception { + return createDevice(name, "default", null, accessToken); + } + + protected Device createDevice(String name, String type, String label, String accessToken) throws Exception { Device device = new Device(); device.setName(name); - device.setType("default"); + device.setType(type); + device.setLabel(label); DeviceData deviceData = new DeviceData(); deviceData.setTransportConfiguration(new DefaultDeviceTransportConfiguration()); deviceData.setConfiguration(new DefaultDeviceConfiguration()); @@ -726,6 +749,12 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest { return doPost("/api/device-with-credentials", request, Device.class); } + protected ResultActions doGetAsync(String urlTemplate, MultiValueMap params) throws Exception { + MockHttpServletRequestBuilder getRequest = get(urlTemplate).params(params); + setJwtToken(getRequest); + return mockMvc.perform(asyncDispatch(mockMvc.perform(getRequest).andExpect(request().asyncStarted()).andReturn())); + } + protected ResultActions doGet(String urlTemplate, Object... urlVariables) throws Exception { MockHttpServletRequestBuilder getRequest = get(urlTemplate, urlVariables); setJwtToken(getRequest); @@ -925,6 +954,15 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest { return mockMvc.perform(deleteRequest); } + protected ResultActions doDeleteAsync(String urlTemplate, MultiValueMap params) throws Exception { + MockHttpServletRequestBuilder deleteRequest = delete(urlTemplate) + .params(params); + setJwtToken(deleteRequest); + MvcResult result = mockMvc.perform(deleteRequest).andReturn(); + result.getAsyncResult(DEFAULT_TIMEOUT); + return mockMvc.perform(asyncDispatch(result)); + } + protected ResultActions doDeleteAsync(String urlTemplate, Long timeout, String... params) throws Exception { MockHttpServletRequestBuilder deleteRequest = delete(urlTemplate, params); setJwtToken(deleteRequest); @@ -1051,6 +1089,16 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest { doPost("/api/relation", relation); } + protected void deleteEntityRelation(EntityRelation entityRelation) throws Exception { + String url = String.format("/api/relation?fromId=%s&fromType=%s&relationType=%s&toId=%s&toType=%s", + entityRelation.getFrom().getId(), + entityRelation.getFrom().getEntityType(), + entityRelation.getType(), + entityRelation.getTo().getId(), + entityRelation.getTo().getEntityType()); + doDelete(url); + } + protected List findRelationsByTo(EntityId entityId) throws Exception { String url = String.format("/api/relations?toId=%s&toType=%s", entityId.getId(), entityId.getEntityType().name()); MvcResult mvcResult = doGet(url).andReturn(); @@ -1099,6 +1147,18 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest { }); } + protected void awaitForCalculatedFieldEntityMessageProcessorToRegisterCfStateAsReadyToRefreshDynamicArguments(EntityId entityId, CalculatedFieldId cfId, int scheduledUpdateInterval) { + CalculatedFieldEntityMessageProcessor processor = getCalculatedFieldEntityMessageProcessor(entityId); + Map statesMap = (Map) ReflectionTestUtils.getField(processor, "states"); + Awaitility.await("CF state for entity actor ready to refresh dynamic arguments").atMost(TIMEOUT, TimeUnit.SECONDS).until(() -> { + CalculatedFieldState calculatedFieldState = statesMap.get(cfId); + boolean isReady = calculatedFieldState != null && ((GeofencingCalculatedFieldState) calculatedFieldState).getLastDynamicArgumentsRefreshTs() + < System.currentTimeMillis() - TimeUnit.SECONDS.toMillis(scheduledUpdateInterval); + log.warn("entityId {}, cfId {}, state ready to refresh == {}", entityId, cfId, isReady); + return isReady; + }); + } + protected static String getMapName(FeatureType featureType) { switch (featureType) { case ATTRIBUTES: @@ -1120,6 +1180,16 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest { return (DeviceActorMessageProcessor) ReflectionTestUtils.getField(actor, "processor"); } + protected CalculatedFieldEntityMessageProcessor getCalculatedFieldEntityMessageProcessor(EntityId entityId) { + DefaultTbActorSystem actorSystem = (DefaultTbActorSystem) ReflectionTestUtils.getField(actorService, "system"); + ConcurrentMap actors = (ConcurrentMap) ReflectionTestUtils.getField(actorSystem, "actors"); + Awaitility.await("CF entity actor was created").atMost(TIMEOUT, TimeUnit.SECONDS) + .until(() -> actors.containsKey(new TbCalculatedFieldEntityActorId(entityId))); + TbActorMailbox actorMailbox = actors.get(new TbCalculatedFieldEntityActorId(entityId)); + CalculatedFieldEntityActor actor = (CalculatedFieldEntityActor) ReflectionTestUtils.getField(actorMailbox, "actor"); + return (CalculatedFieldEntityMessageProcessor) ReflectionTestUtils.getField(actor, "processor"); + } + protected void updateDefaultTenantProfileConfig(Consumer updater) throws ThingsboardException { updateDefaultTenantProfile(tenantProfile -> { TenantProfileData profileData = tenantProfile.getProfileData(); @@ -1274,7 +1344,7 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest { protected List findJobs(List types, List entities) throws Exception { return doGetTypedWithPageLink("/api/jobs?types=" + types.stream().map(Enum::name).collect(Collectors.joining(",")) + - "&entities=" + entities.stream().map(UUID::toString).collect(Collectors.joining(",")) + "&", + "&entities=" + entities.stream().map(UUID::toString).collect(Collectors.joining(",")) + "&", new TypeReference>() {}, new PageLink(100, 0, null, new SortOrder("createdTime", SortOrder.Direction.DESC))).getData(); } @@ -1286,4 +1356,34 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest { doPost("/api/job/" + jobId + "/reprocess").andExpect(status().isOk()); } + protected void postTelemetry(EntityId entityId, String payload) throws Exception { + doPostAsync("/api/plugins/telemetry/" + entityId.getEntityType() + "/" + entityId.getId() + + "/timeseries/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode(payload), 30_000L).andExpect(status().isOk()); + } + + protected void postAttributes(EntityId entityId, AttributeScope scope, String payload) throws Exception { + doPostAsync("/api/plugins/telemetry/" + entityId.getEntityType() + "/" + entityId.getId() + + "/attributes/" + scope, JacksonUtil.toJsonNode(payload), 30_000L).andExpect(status().isOk()); + } + + protected CalculatedField saveCalculatedField(CalculatedField calculatedField) { + return doPost("/api/calculatedField", calculatedField, CalculatedField.class); + } + + protected PageData getCalculatedFields(EntityId entityId, CalculatedFieldType type, PageLink pageLink) throws Exception { + return doGetTypedWithPageLink("/api/" + entityId.getEntityType() + "/" + entityId.getId() + "/calculatedFields" + + (type != null ? "?type=" + type.name() + "&" : "?"), new TypeReference<>() {}, pageLink); + } + + protected PageData getDebugEvents(TenantId tenantId, EntityId entityId, int limit) throws Exception { + return getEvents(tenantId, entityId, EventType.DEBUG_RULE_NODE, limit); + } + + protected PageData getEvents(TenantId tenantId, EntityId entityId, EventType eventType, int limit) throws Exception { + TimePageLink pageLink = new TimePageLink(limit); + return doGetTypedWithTimePageLink("/api/events/{entityType}/{entityId}/{eventType}?tenantId={tenantId}&", + new TypeReference>() { + }, pageLink, entityId.getEntityType(), entityId.getId(), eventType, tenantId.getId()); + } + } diff --git a/application/src/test/java/org/thingsboard/server/controller/AiModelControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/AiModelControllerTest.java index ae2972b0cc..53648af441 100644 --- a/application/src/test/java/org/thingsboard/server/controller/AiModelControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/AiModelControllerTest.java @@ -104,7 +104,10 @@ public class AiModelControllerTest extends AbstractControllerTest { var model = doPost("/api/ai/model", constructValidOpenAiModel("Test model"), AiModel.class); var newModelConfig = OpenAiChatModelConfig.builder() - .providerConfig(new OpenAiProviderConfig("test-api-key-updated")) + .providerConfig(OpenAiProviderConfig.builder() + .baseUrl(OpenAiProviderConfig.OPENAI_OFFICIAL_BASE_URL) + .apiKey("test-api-key-updated") + .build()) .modelId("o4-mini") .temperature(0.2) .topP(0.4) @@ -270,7 +273,7 @@ public class AiModelControllerTest extends AbstractControllerTest { .tenantId(tenantId) .name("Test model 1") .configuration(OpenAiChatModelConfig.builder() - .providerConfig(new OpenAiProviderConfig("test-api-key")) + .providerConfig(OpenAiProviderConfig.builder().apiKey("test-api-key").build()) .modelId("o3-pro") .build()) .build(), AiModel.class); @@ -594,7 +597,10 @@ public class AiModelControllerTest extends AbstractControllerTest { private AiModel constructValidOpenAiModel(String name) { var modelConfig = OpenAiChatModelConfig.builder() - .providerConfig(new OpenAiProviderConfig("test-api-key")) + .providerConfig(OpenAiProviderConfig.builder() + .baseUrl(OpenAiProviderConfig.OPENAI_OFFICIAL_BASE_URL) + .apiKey("test-api-key") + .build()) .modelId("gpt-4o") .temperature(0.5) .topP(0.3) diff --git a/application/src/test/java/org/thingsboard/server/controller/AssetControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/AssetControllerTest.java index 90b5cbeef2..e160fff59e 100644 --- a/application/src/test/java/org/thingsboard/server/controller/AssetControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/AssetControllerTest.java @@ -1080,6 +1080,30 @@ public class AssetControllerTest extends AbstractControllerTest { testEntityDaoWithRelationsTransactionalException(assetDao, savedTenant.getId(), assetId, "/api/asset/" + assetId); } + @Test + public void testSaveAssetWithUniquifyStrategy() throws Exception { + Asset asset = new Asset(); + asset.setName("My unique asset"); + asset.setType("default"); + doPost("/api/asset", asset, Asset.class); + + doPost("/api/asset", asset).andExpect(status().isBadRequest()); + + doPost("/api/asset?nameConflictPolicy=FAIL", asset).andExpect(status().isBadRequest()); + + Asset secondAsset = doPost("/api/asset?nameConflictPolicy=UNIQUIFY", asset, Asset.class); + assertThat(secondAsset.getName()).startsWith("My unique asset_"); + + Asset thirdAsset = doPost("/api/asset?nameConflictPolicy=UNIQUIFY&uniquifySeparator=-", asset, Asset.class); + assertThat(thirdAsset.getName()).startsWith("My unique asset-"); + + Asset fourthAsset = doPost("/api/asset?nameConflictPolicy=UNIQUIFY&uniquifyStrategy=INCREMENTAL", asset, Asset.class); + assertThat(fourthAsset.getName()).isEqualTo("My unique asset_1"); + + Asset fifthAsset = doPost("/api/asset?nameConflictPolicy=UNIQUIFY&uniquifyStrategy=INCREMENTAL", asset, Asset.class); + assertThat(fifthAsset.getName()).isEqualTo("My unique asset_2"); + } + private Asset createAsset(String name) { Asset asset = new Asset(); asset.setName(name); diff --git a/application/src/test/java/org/thingsboard/server/controller/CalculatedFieldControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/CalculatedFieldControllerTest.java index af43b34558..61fc7a9e48 100644 --- a/application/src/test/java/org/thingsboard/server/controller/CalculatedFieldControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/CalculatedFieldControllerTest.java @@ -28,16 +28,34 @@ import org.thingsboard.server.common.data.cf.configuration.ArgumentType; import org.thingsboard.server.common.data.cf.configuration.CalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.Output; import org.thingsboard.server.common.data.cf.configuration.OutputType; +import org.thingsboard.server.common.data.cf.configuration.PropagationCalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey; +import org.thingsboard.server.common.data.cf.configuration.RelationPathQueryDynamicSourceConfiguration; import org.thingsboard.server.common.data.cf.configuration.SimpleCalculatedFieldConfiguration; -import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.cf.configuration.aggregation.AggKeyInput; +import org.thingsboard.server.common.data.cf.configuration.aggregation.AggMetric; +import org.thingsboard.server.common.data.cf.configuration.aggregation.single.EntityAggregationCalculatedFieldConfiguration; +import org.thingsboard.server.common.data.cf.configuration.aggregation.single.interval.HourInterval; +import org.thingsboard.server.common.data.cf.configuration.aggregation.single.interval.Watermark; +import org.thingsboard.server.common.data.cf.configuration.geofencing.EntityCoordinates; +import org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingCalculatedFieldConfiguration; +import org.thingsboard.server.common.data.cf.configuration.geofencing.ZoneGroupConfiguration; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.common.data.relation.EntityRelation; +import org.thingsboard.server.common.data.relation.EntitySearchDirection; +import org.thingsboard.server.common.data.relation.RelationPathLevel; import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.dao.service.DaoSqlTest; +import java.util.List; import java.util.Map; +import java.util.concurrent.TimeUnit; import static org.assertj.core.api.Assertions.assertThat; +import static org.hamcrest.Matchers.containsString; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingReportStrategy.REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS; @DaoSqlTest public class CalculatedFieldControllerTest extends AbstractControllerTest { @@ -73,7 +91,7 @@ public class CalculatedFieldControllerTest extends AbstractControllerTest { @Test public void testSaveCalculatedField() throws Exception { Device testDevice = createDevice("Test device", "1234567890"); - CalculatedField calculatedField = getCalculatedField(testDevice.getId()); + CalculatedField calculatedField = getSimpleCalculatedField(testDevice.getId()); CalculatedField savedCalculatedField = doPost("/api/calculatedField", calculatedField, CalculatedField.class); @@ -84,7 +102,7 @@ public class CalculatedFieldControllerTest extends AbstractControllerTest { assertThat(savedCalculatedField.getEntityId()).isEqualTo(calculatedField.getEntityId()); assertThat(savedCalculatedField.getType()).isEqualTo(calculatedField.getType()); assertThat(savedCalculatedField.getName()).isEqualTo(calculatedField.getName()); - assertThat(savedCalculatedField.getConfiguration()).isEqualTo(getCalculatedFieldConfig()); + assertThat(savedCalculatedField.getConfiguration()).isEqualTo(getSimpleCalculatedFieldConfig()); assertThat(savedCalculatedField.getVersion()).isEqualTo(1L); savedCalculatedField.setName("Test CF"); @@ -98,10 +116,104 @@ public class CalculatedFieldControllerTest extends AbstractControllerTest { .andExpect(status().isOk()); } + @Test + public void testSaveGeofencingCalculatedField() throws Exception { + Device testDevice = createDevice("Test device", "1234567890"); + CalculatedField calculatedField = getCalculatedField(testDevice.getId(), CalculatedFieldType.GEOFENCING); + + CalculatedField savedCalculatedField = doPost("/api/calculatedField", calculatedField, CalculatedField.class); + + assertThat(savedCalculatedField).isNotNull(); + assertThat(savedCalculatedField.getId()).isNotNull(); + assertThat(savedCalculatedField.getCreatedTime()).isGreaterThan(0); + assertThat(savedCalculatedField.getTenantId()).isEqualTo(savedTenant.getId()); + assertThat(savedCalculatedField.getEntityId()).isEqualTo(calculatedField.getEntityId()); + assertThat(savedCalculatedField.getType()).isEqualTo(calculatedField.getType()); + assertThat(savedCalculatedField.getName()).isEqualTo(calculatedField.getName()); + assertThat(savedCalculatedField.getConfiguration()).isEqualTo(getGeofencingCalculatedFieldConfig()); + assertThat(savedCalculatedField.getVersion()).isEqualTo(1L); + + savedCalculatedField.setName("Test CF"); + + CalculatedField updatedCalculatedField = doPost("/api/calculatedField", savedCalculatedField, CalculatedField.class); + + assertThat(updatedCalculatedField.getName()).isEqualTo(savedCalculatedField.getName()); + assertThat(updatedCalculatedField.getVersion()).isEqualTo(savedCalculatedField.getVersion() + 1); + + doDelete("/api/calculatedField/" + savedCalculatedField.getId().getId().toString()) + .andExpect(status().isOk()); + } + + @Test + public void testSavePropagationCalculatedField() throws Exception { + Device testDevice = createDevice("Test device", "1234567890"); + CalculatedField calculatedField = getCalculatedField(testDevice.getId(), CalculatedFieldType.PROPAGATION); + + CalculatedField savedCalculatedField = doPost("/api/calculatedField", calculatedField, CalculatedField.class); + + assertThat(savedCalculatedField).isNotNull(); + assertThat(savedCalculatedField.getId()).isNotNull(); + assertThat(savedCalculatedField.getCreatedTime()).isGreaterThan(0); + assertThat(savedCalculatedField.getTenantId()).isEqualTo(savedTenant.getId()); + assertThat(savedCalculatedField.getEntityId()).isEqualTo(calculatedField.getEntityId()); + assertThat(savedCalculatedField.getType()).isEqualTo(calculatedField.getType()); + assertThat(savedCalculatedField.getName()).isEqualTo(calculatedField.getName()); + assertThat(savedCalculatedField.getConfiguration()).isEqualTo(getPropagationCalculatedFieldConfig()); + assertThat(savedCalculatedField.getVersion()).isEqualTo(1L); + + savedCalculatedField.setName("Test CF"); + + CalculatedField updatedCalculatedField = doPost("/api/calculatedField", savedCalculatedField, CalculatedField.class); + + assertThat(updatedCalculatedField.getName()).isEqualTo(savedCalculatedField.getName()); + assertThat(updatedCalculatedField.getVersion()).isEqualTo(savedCalculatedField.getVersion() + 1); + + doDelete("/api/calculatedField/" + savedCalculatedField.getId().getId().toString()) + .andExpect(status().isOk()); + } + + @Test + public void testSaveEntityAggregationCalculatedField() throws Exception { + Device testDevice = createDevice("Test device", "1234567890"); + CalculatedField calculatedField = getCalculatedField(testDevice.getId(), CalculatedFieldType.ENTITY_AGGREGATION); + + CalculatedField savedCalculatedField = doPost("/api/calculatedField", calculatedField, CalculatedField.class); + + assertThat(savedCalculatedField).isNotNull(); + assertThat(savedCalculatedField.getId()).isNotNull(); + assertThat(savedCalculatedField.getCreatedTime()).isGreaterThan(0); + assertThat(savedCalculatedField.getTenantId()).isEqualTo(savedTenant.getId()); + assertThat(savedCalculatedField.getEntityId()).isEqualTo(calculatedField.getEntityId()); + assertThat(savedCalculatedField.getType()).isEqualTo(calculatedField.getType()); + assertThat(savedCalculatedField.getName()).isEqualTo(calculatedField.getName()); + assertThat(savedCalculatedField.getConfiguration()).isEqualTo(getEntityAggregationCalculatedFieldConfig()); + assertThat(savedCalculatedField.getVersion()).isEqualTo(1L); + + savedCalculatedField.setName("Test CF"); + + CalculatedField updatedCalculatedField = doPost("/api/calculatedField", savedCalculatedField, CalculatedField.class); + + assertThat(updatedCalculatedField.getName()).isEqualTo(savedCalculatedField.getName()); + assertThat(updatedCalculatedField.getVersion()).isEqualTo(savedCalculatedField.getVersion() + 1); + + doDelete("/api/calculatedField/" + savedCalculatedField.getId().getId().toString()) + .andExpect(status().isOk()); + } + + @Test + public void testSavePropagationCalculatedFieldWithNullArguments() throws Exception { + Device testDevice = createDevice("Test device", "1234567890"); + CalculatedField calculatedField = getCalculatedField(testDevice.getId(), CalculatedFieldType.PROPAGATION, getPropagationCalculatedFieldConfig(null)); + + doPost("/api/calculatedField", calculatedField) + .andExpect(status().isBadRequest()) + .andExpect(statusReason(containsString("arguments must not be empty"))); + } + @Test public void testGetCalculatedFieldById() throws Exception { Device testDevice = createDevice("Test device", "1234567890"); - CalculatedField calculatedField = getCalculatedField(testDevice.getId()); + CalculatedField calculatedField = getSimpleCalculatedField(testDevice.getId()); CalculatedField savedCalculatedField = doPost("/api/calculatedField", calculatedField, CalculatedField.class); CalculatedField fetchedCalculatedField = doGet("/api/calculatedField/" + savedCalculatedField.getId().getId(), CalculatedField.class); @@ -113,10 +225,22 @@ public class CalculatedFieldControllerTest extends AbstractControllerTest { .andExpect(status().isOk()); } + @Test + public void testGetCalculatedFields() throws Exception { + Device testDevice = createDevice("Test device", "1234567890"); + CalculatedField calculatedField = getSimpleCalculatedField(testDevice.getId()); + calculatedField = doPost("/api/calculatedField", calculatedField, CalculatedField.class); + + assertThat(getCalculatedFields(testDevice.getId(), null, new PageLink(10)).getData()) + .singleElement().isEqualTo(calculatedField); + assertThat(getCalculatedFields(testDevice.getId(), CalculatedFieldType.SIMPLE, new PageLink(10)).getData()) + .singleElement().isEqualTo(calculatedField); + } + @Test public void testDeleteCalculatedField() throws Exception { Device testDevice = createDevice("Test device", "1234567890"); - CalculatedField calculatedField = getCalculatedField(testDevice.getId()); + CalculatedField calculatedField = getSimpleCalculatedField(testDevice.getId()); CalculatedField savedCalculatedField = doPost("/api/calculatedField", calculatedField, CalculatedField.class); @@ -127,18 +251,100 @@ public class CalculatedFieldControllerTest extends AbstractControllerTest { doGet("/api/calculatedField/" + savedCalculatedField.getId().getId()).andExpect(status().isNotFound()); } - private CalculatedField getCalculatedField(DeviceId deviceId) { + private CalculatedField getSimpleCalculatedField(EntityId entityId) { + return getCalculatedField(entityId, CalculatedFieldType.SIMPLE); + } + + private CalculatedField getCalculatedField(EntityId entityId, CalculatedFieldType cfType) { + return getCalculatedField(entityId, cfType, null); + } + + private CalculatedField getCalculatedField(EntityId entityId, CalculatedFieldType cfType, CalculatedFieldConfiguration customConfiguration) { CalculatedField calculatedField = new CalculatedField(); - calculatedField.setEntityId(deviceId); - calculatedField.setType(CalculatedFieldType.SIMPLE); + calculatedField.setEntityId(entityId); + calculatedField.setType(cfType); calculatedField.setName("Test Calculated Field"); calculatedField.setConfigurationVersion(1); - calculatedField.setConfiguration(getCalculatedFieldConfig()); + if (customConfiguration != null) { + calculatedField.setConfiguration(customConfiguration); + } else switch (cfType) { + case SIMPLE -> calculatedField.setConfiguration(getSimpleCalculatedFieldConfig()); + case GEOFENCING -> calculatedField.setConfiguration(getGeofencingCalculatedFieldConfig()); + case PROPAGATION -> calculatedField.setConfiguration(getPropagationCalculatedFieldConfig()); + case ENTITY_AGGREGATION -> calculatedField.setConfiguration(getEntityAggregationCalculatedFieldConfig()); + } calculatedField.setVersion(1L); return calculatedField; } - private CalculatedFieldConfiguration getCalculatedFieldConfig() { + private CalculatedFieldConfiguration getGeofencingCalculatedFieldConfig() { + var config = new GeofencingCalculatedFieldConfiguration(); + + var refDynamicSourceConfiguration = new RelationPathQueryDynamicSourceConfiguration(); + refDynamicSourceConfiguration.setLevels(List.of(new RelationPathLevel(EntitySearchDirection.TO, "FromSafeArea"))); + + var zoneGroupConfiguration = new ZoneGroupConfiguration("perimeter", REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS, false); + zoneGroupConfiguration.setRefDynamicSourceConfiguration(refDynamicSourceConfiguration); + + Output output = new Output(); + output.setType(OutputType.TIME_SERIES); + + config.setEntityCoordinates(new EntityCoordinates("latitide", "longitude")); + config.setZoneGroups(Map.of("safeArea", zoneGroupConfiguration)); + config.setScheduledUpdateEnabled(false); + config.setOutput(output); + + return config; + } + + private CalculatedFieldConfiguration getPropagationCalculatedFieldConfig() { + Argument arg = new Argument(); + arg.setRefEntityKey(new ReferencedEntityKey("temperature", ArgumentType.TS_LATEST, null)); + return getPropagationCalculatedFieldConfig(Map.of("t", arg)); + } + + private CalculatedFieldConfiguration getPropagationCalculatedFieldConfig(Map arguments) { + var config = new PropagationCalculatedFieldConfiguration(); + + config.setRelation(new RelationPathLevel(EntitySearchDirection.TO, EntityRelation.CONTAINS_TYPE)); + + config.setApplyExpressionToResolvedArguments(false); + config.setExpression(null); + + Output output = new Output(); + output.setType(OutputType.TIME_SERIES); + config.setOutput(output); + + Argument arg = new Argument(); + arg.setRefEntityKey(new ReferencedEntityKey("temperature", ArgumentType.TS_LATEST, null)); + config.setArguments(arguments); + + return config; + } + + private CalculatedFieldConfiguration getEntityAggregationCalculatedFieldConfig() { + var config = new EntityAggregationCalculatedFieldConfiguration(); + + Argument energyArgument = new Argument(); + energyArgument.setRefEntityKey(new ReferencedEntityKey("energy", ArgumentType.TS_LATEST, null)); + config.setArguments(Map.of("en", energyArgument)); + + AggMetric metric = new AggMetric(); + metric.setInput(new AggKeyInput("en")); + metric.setDefaultValue(9999L); + config.setMetrics(Map.of("consumption", metric)); + + config.setWatermark(new Watermark(TimeUnit.DAYS.toSeconds(1))); + config.setInterval(new HourInterval("Europe/Kiev", TimeUnit.MINUTES.toSeconds(15))); + + Output output = new Output(); + output.setType(OutputType.TIME_SERIES); + config.setOutput(output); + + return config; + } + + private CalculatedFieldConfiguration getSimpleCalculatedFieldConfig() { SimpleCalculatedFieldConfiguration config = new SimpleCalculatedFieldConfiguration(); Argument argument = new Argument(); diff --git a/application/src/test/java/org/thingsboard/server/controller/CustomerControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/CustomerControllerTest.java index f312e49f71..f4e91f993e 100644 --- a/application/src/test/java/org/thingsboard/server/controller/CustomerControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/CustomerControllerTest.java @@ -33,6 +33,7 @@ import org.springframework.context.annotation.Primary; import org.springframework.test.context.ContextConfiguration; import org.thingsboard.common.util.ThingsBoardExecutors; import org.thingsboard.server.common.data.Customer; +import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.User; @@ -462,6 +463,27 @@ public class CustomerControllerTest extends AbstractControllerTest { testEntityDaoWithRelationsTransactionalException(customerDao, savedTenant.getId(), customerId, "/api/customer/" + customerId); } + @Test + public void testSaveCustomerWithUniquifyStrategy() throws Exception { + Customer customer = new Customer(); + customer.setTitle("My unique customer"); + Customer savedCustomer = doPost("/api/customer", customer, Customer.class); + + doPost("/api/customer?nameConflictPolicy=FAIL", customer).andExpect(status().isBadRequest()); + + Customer secondCustomer = doPost("/api/customer?nameConflictPolicy=UNIQUIFY", customer, Customer.class); + assertThat(secondCustomer.getName()).startsWith("My unique customer_"); + + Customer thirdCustomer = doPost("/api/customer?nameConflictPolicy=UNIQUIFY&uniquifySeparator=-", customer, Customer.class); + assertThat(thirdCustomer.getName()).startsWith("My unique customer-"); + + Customer fourthCustomer = doPost("/api/customer?nameConflictPolicy=UNIQUIFY&uniquifyStrategy=INCREMENTAL", customer, Customer.class); + assertThat(fourthCustomer.getName()).isEqualTo("My unique customer_1"); + + Customer fifthCustomer = doPost("/api/customer?nameConflictPolicy=UNIQUIFY&uniquifyStrategy=INCREMENTAL", customer, Customer.class); + assertThat(fifthCustomer.getName()).isEqualTo("My unique customer_2"); + } + private Customer createCustomer(String title) { Customer customer = new Customer(); customer.setTitle(title); diff --git a/application/src/test/java/org/thingsboard/server/controller/DeviceControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/DeviceControllerTest.java index 36cede9e84..78679948ab 100644 --- a/application/src/test/java/org/thingsboard/server/controller/DeviceControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/DeviceControllerTest.java @@ -38,6 +38,7 @@ import org.springframework.test.context.ContextConfiguration; import org.testcontainers.shaded.org.awaitility.Awaitility; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.ThingsBoardExecutors; +import org.thingsboard.server.common.data.AttributeScope; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.DeviceInfo; @@ -59,6 +60,7 @@ import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DeviceCredentialsId; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.kv.AttributeKvEntry; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.relation.EntityRelation; @@ -77,10 +79,15 @@ import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.service.gateway_device.GatewayNotificationsService; import org.thingsboard.server.service.state.DeviceStateService; +import org.thingsboard.server.utils.CsvUtils; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; +import java.util.Arrays; +import java.util.LinkedList; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.concurrent.TimeUnit; import static org.assertj.core.api.Assertions.assertThat; @@ -387,6 +394,48 @@ public class DeviceControllerTest extends AbstractControllerTest { .andExpect(statusReason(containsString("Device can`t be referencing to device profile from different tenant!"))); } + @Test + public void testSaveDeviceWithFirmware() throws Exception { + loginTenantAdmin(); + DeviceProfile profile = createDeviceProfile("Profile to test ota updates"); + profile = doPost("/api/deviceProfile", profile, DeviceProfile.class); + + SaveOtaPackageInfoRequest firmwareInfo = new SaveOtaPackageInfoRequest(); + firmwareInfo.setDeviceProfileId(profile.getId()); + firmwareInfo.setType(FIRMWARE); + String title = "title"; + firmwareInfo.setTitle(title); + String fwVersion = "1.0"; + firmwareInfo.setVersion(fwVersion); + String url = "test.url"; + firmwareInfo.setUrl(url); + firmwareInfo.setUsesUrl(true); + OtaPackageInfo savedFw = doPost("/api/otaPackage", firmwareInfo, OtaPackageInfo.class); + + Device device = new Device(); + device.setName("My ota device"); + device.setDeviceProfileId(profile.getId()); + device.setFirmwareId(savedFw.getId()); + device = doPost("/api/device", device, Device.class); + + //check shared attributes + Device finalDevice = device; + await().atMost(TIMEOUT, TimeUnit.SECONDS).until(() -> { + List> attributes = doGetAsyncTyped("/api/plugins/telemetry/DEVICE/" + finalDevice.getId() + + "/values/attributes/SHARED_SCOPE", new TypeReference>>() { + }); + return findAttrValue("fw_version", attributes).equals(fwVersion) && + findAttrValue("fw_title", attributes).equals(title) && + findAttrValue("fw_url", attributes).equals(url); + }); + } + + private static Object findAttrValue(String key, List> attributes) { + Optional> attr = attributes.stream() + .filter(att -> att.get("key").equals(key)).findFirst(); + return attr.isPresent() ? attr.get().get("value") : ""; + } + @Test public void testSaveDeviceWithFirmwareFromDifferentTenant() throws Exception { loginDifferentTenant(); @@ -1586,6 +1635,57 @@ public class DeviceControllerTest extends AbstractControllerTest { Assert.assertEquals(newAttributeValue, actualAttribute.get("value")); } + @Test + public void testBulkImportDeviceWithJsonAttr() throws Exception { + String deviceName = "some_device"; + String deviceType = "some_type"; + String deviceAttr = "{\"threshold\":45}"; + + List> content = new LinkedList<>(); + content.add(Arrays.asList("NAME", "TYPE", "ATTR")); + content.add(Arrays.asList(deviceName, deviceType, deviceAttr)); + + byte[] bytes = CsvUtils.generateCsv(content); + BulkImportRequest request = new BulkImportRequest(); + request.setFile(new String(bytes, StandardCharsets.UTF_8)); + BulkImportRequest.Mapping mapping = new BulkImportRequest.Mapping(); + BulkImportRequest.ColumnMapping name = new BulkImportRequest.ColumnMapping(); + name.setType(BulkImportColumnType.NAME); + BulkImportRequest.ColumnMapping type = new BulkImportRequest.ColumnMapping(); + type.setType(BulkImportColumnType.TYPE); + BulkImportRequest.ColumnMapping attr = new BulkImportRequest.ColumnMapping(); + attr.setType(BulkImportColumnType.SERVER_ATTRIBUTE); + attr.setKey("attr"); + List columns = new ArrayList<>(); + columns.add(name); + columns.add(type); + columns.add(attr); + + mapping.setColumns(columns); + mapping.setDelimiter(','); + mapping.setUpdate(true); + mapping.setHeader(true); + request.setMapping(mapping); + + BulkImportResult deviceBulkImportResult = doPostWithTypedResponse("/api/device/bulk_import", request, new TypeReference<>() {}); + + Assert.assertEquals(1, deviceBulkImportResult.getCreated().get()); + Assert.assertEquals(0, deviceBulkImportResult.getErrors().get()); + Assert.assertEquals(0, deviceBulkImportResult.getUpdated().get()); + Assert.assertTrue(deviceBulkImportResult.getErrorsList().isEmpty()); + + Device savedDevice = doGet("/api/tenant/devices?deviceName=" + deviceName, Device.class); + + Assert.assertNotNull(savedDevice); + Assert.assertEquals(deviceName, savedDevice.getName()); + Assert.assertEquals(deviceType, savedDevice.getType()); + + Optional retrieved = await().atMost(5, TimeUnit.SECONDS) + .until(() -> attributesService.find(tenantId, savedDevice.getId(), AttributeScope.SERVER_SCOPE, "attr").get(), Optional::isPresent); + assertThat(retrieved.get().getJsonValue().get()).isEqualTo(deviceAttr); + assertThat(retrieved.get().getStrValue()).isNotPresent(); + } + @Test public void testSaveDeviceWithOutdatedVersion() throws Exception { Device device = createDevice("Device v1.0"); @@ -1608,6 +1708,30 @@ public class DeviceControllerTest extends AbstractControllerTest { assertThat(device.getVersion()).isEqualTo(3); } + @Test + public void testSaveDeviceWithUniquifyStrategy() throws Exception { + Device device = new Device(); + device.setName("My unique device"); + device.setType("default"); + Device savedDevice = doPost("/api/device", device, Device.class); + + doPost("/api/device", device).andExpect(status().isBadRequest()); + + doPost("/api/device?nameConflictPolicy=FAIL", device).andExpect(status().isBadRequest()); + + Device secondDevice = doPost("/api/device?nameConflictPolicy=UNIQUIFY", device, Device.class); + assertThat(secondDevice.getName()).startsWith("My unique device_"); + + Device thirdDevice = doPost("/api/device?nameConflictPolicy=UNIQUIFY&uniquifySeparator=-", device, Device.class); + assertThat(thirdDevice.getName()).startsWith("My unique device-"); + + Device fourthDevice = doPost("/api/device?nameConflictPolicy=UNIQUIFY&uniquifyStrategy=INCREMENTAL", device, Device.class); + assertThat(fourthDevice.getName()).isEqualTo("My unique device_1"); + + Device fifthDevice = doPost("/api/device?nameConflictPolicy=UNIQUIFY&uniquifyStrategy=INCREMENTAL", device, Device.class); + assertThat(fifthDevice.getName()).isEqualTo("My unique device_2"); + } + private Device createDevice(String name) { Device device = new Device(); device.setName(name); diff --git a/application/src/test/java/org/thingsboard/server/controller/EdqsEntityQueryControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/EdqsEntityQueryControllerTest.java index 89bbe5e499..655ab417e6 100644 --- a/application/src/test/java/org/thingsboard/server/controller/EdqsEntityQueryControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/EdqsEntityQueryControllerTest.java @@ -25,6 +25,9 @@ import org.thingsboard.server.common.data.edqs.EdqsState; import org.thingsboard.server.common.data.edqs.EdqsState.EdqsApiMode; import org.thingsboard.server.common.data.edqs.ToCoreEdqsRequest; import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.query.AlarmCountQuery; +import org.thingsboard.server.common.data.query.AlarmData; +import org.thingsboard.server.common.data.query.AlarmDataQuery; import org.thingsboard.server.common.data.query.EntityCountQuery; import org.thingsboard.server.common.data.query.EntityData; import org.thingsboard.server.common.data.query.EntityDataQuery; @@ -70,12 +73,24 @@ public class EdqsEntityQueryControllerTest extends EntityQueryControllerTest { result -> result.getTotalElements() == expectedResultSize); } + @Override + protected PageData findAlarmsByQueryAndCheck(AlarmDataQuery query, int expectedResultSize) { + return await().atMost(TIMEOUT, TimeUnit.SECONDS).until(() -> findAlarmsByQuery(query), + result -> result.getTotalElements() == expectedResultSize); + } + @Override protected Long countByQueryAndCheck(EntityCountQuery query, long expectedResult) { return await().atMost(TIMEOUT, TimeUnit.SECONDS).until(() -> countByQuery(query), result -> result == expectedResult); } + @Override + protected Long countAlarmsByQueryAndCheck(AlarmCountQuery query, long expectedResult) { + return await().atMost(TIMEOUT, TimeUnit.SECONDS).until(() -> countAlarmsByQuery(query), + result -> result == expectedResult); + } + @Test public void testEdqsState() throws Exception { loginSysAdmin(); diff --git a/application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java index ee18543796..775df230b6 100644 --- a/application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java @@ -77,6 +77,7 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.concurrent.TimeUnit; +import java.util.function.BiConsumer; import java.util.stream.Collectors; import static org.assertj.core.api.Assertions.assertThat; @@ -278,8 +279,7 @@ public class EntityQueryControllerTest extends AbstractControllerTest { assetTypeFilter.setEntityType(EntityType.ASSET); AlarmCountQuery assetAlarmQuery = new AlarmCountQuery(assetTypeFilter); - Long assetAlamCount = doPostWithResponse("/api/alarmsQuery/count", assetAlarmQuery, Long.class); - Assert.assertEquals(assets.size(), assetAlamCount.longValue()); + countAlarmsByQueryAndCheck(assetAlarmQuery, assets.size()); KeyFilter nameFilter = buildStringKeyFilter(EntityKeyType.ENTITY_FIELD, "name", StringFilterPredicate.StringOperation.STARTS_WITH, "Asset1"); List keyFilters = Collections.singletonList(nameFilter); @@ -369,8 +369,7 @@ public class EntityQueryControllerTest extends AbstractControllerTest { assetTypeFilter.setEntityType(EntityType.ASSET); AlarmCountQuery assetAlarmQuery = new AlarmCountQuery(assetTypeFilter); - Long assetAlamCount = doPostWithResponse("/api/alarmsQuery/count", assetAlarmQuery, Long.class); - Assert.assertEquals(10, assetAlamCount.longValue()); + countAlarmsByQueryAndCheck(assetAlarmQuery, 10); KeyFilter nameFilter = buildStringKeyFilter(EntityKeyType.ENTITY_FIELD, "name", StringFilterPredicate.StringOperation.STARTS_WITH, "Asset1"); List keyFilters = Collections.singletonList(nameFilter); @@ -433,16 +432,17 @@ public class EntityQueryControllerTest extends AbstractControllerTest { List alarmFields = new ArrayList<>(); alarmFields.add(new EntityKey(EntityKeyType.ALARM_FIELD, "type")); + alarmFields.add(new EntityKey(EntityKeyType.ALARM_FIELD, "originatorDisplayName")); EntityTypeFilter assetTypeFilter = new EntityTypeFilter(); assetTypeFilter.setEntityType(EntityType.ASSET); AlarmDataQuery assetAlarmQuery = new AlarmDataQuery(assetTypeFilter, pageLink, null, null, null, alarmFields); - PageData alarmPageData = doPostWithTypedResponse("/api/alarmsQuery/find", assetAlarmQuery, new TypeReference<>() { - }); - Assert.assertEquals(10, alarmPageData.getTotalElements()); - List retrievedAlarmTypes = alarmPageData.getData().stream().map(Alarm::getType).toList(); + PageData alarmPageData = findAlarmsByQueryAndCheck(assetAlarmQuery, 10); + List retrievedAlarmTypes = alarmPageData.getData().stream().map(AlarmData::getType).toList(); assertThat(retrievedAlarmTypes).containsExactlyInAnyOrderElementsOf(assetAlarmTypes); + List retrievedAlarmDisplayName = alarmPageData.getData().stream().map(AlarmData::getOriginatorDisplayName).toList(); + assertThat(retrievedAlarmDisplayName).containsExactlyInAnyOrderElementsOf(assets.stream().map(Asset::getLabel).toList()); KeyFilter nameFilter = buildStringKeyFilter(EntityKeyType.ENTITY_FIELD, "name", StringFilterPredicate.StringOperation.STARTS_WITH, "Asset1"); List keyFilters = Collections.singletonList(nameFilter); @@ -511,9 +511,7 @@ public class EntityQueryControllerTest extends AbstractControllerTest { assetTypeFilter.setEntityType(EntityType.ASSET); AlarmDataQuery assetAlarmQuery = new AlarmDataQuery(assetTypeFilter, pageLink, null, null, null, Collections.emptyList()); - PageData alarmPageData = doPostWithTypedResponse("/api/alarmsQuery/find", assetAlarmQuery, new TypeReference<>() { - }); - Assert.assertEquals(10, alarmPageData.getTotalElements()); + PageData alarmPageData = findAlarmsByQueryAndCheck(assetAlarmQuery, 10); List retrievedAlarmTypes = alarmPageData.getData().stream().map(Alarm::getType).toList(); assertThat(retrievedAlarmTypes).containsExactlyInAnyOrderElementsOf(assetAlarmTypes); @@ -525,6 +523,67 @@ public class EntityQueryControllerTest extends AbstractControllerTest { Assert.assertEquals(1, filteredAssetAlamData.getTotalElements()); } + @Test + public void testFindAlarmsWithEntityFilterAndLatestValues() throws Exception { + loginTenantAdmin(); + List devices = new ArrayList<>(); + List temps = new ArrayList<>(); + List deviceNames = new ArrayList<>(); + for (int i = 0; i < 10; i++) { + Device device = new Device(); + device.setCustomerId(customerId); + device.setName("Device" + i); + device.setType("default"); + device.setLabel("testLabel" + (int) (Math.random() * 1000)); + device = doPost("/api/device", device, Device.class); + devices.add(device); + deviceNames.add(device.getName()); + + int temp = i * 10; + temps.add(String.valueOf(temp)); + JsonNode content = JacksonUtil.toJsonNode("{\"temperature\": " + temp + "}"); + doPost("/api/plugins/telemetry/" + EntityType.DEVICE.name() + "/" + device.getUuidId() + "/timeseries/SERVER_SCOPE", content) + .andExpect(status().isOk()); + Thread.sleep(1); + } + + for (int i = 0; i < devices.size(); i++) { + Alarm alarm = new Alarm(); + alarm.setCustomerId(customerId); + alarm.setOriginator(devices.get(i).getId()); + String type = "device alarm" + i; + alarm.setType(type); + alarm.setSeverity(AlarmSeverity.WARNING); + doPost("/api/alarm", alarm, Alarm.class); + Thread.sleep(1); + } + + AlarmDataPageLink pageLink = new AlarmDataPageLink(); + pageLink.setPage(0); + pageLink.setPageSize(100); + pageLink.setSortOrder(new EntityDataSortOrder(new EntityKey(EntityKeyType.ALARM_FIELD, "created_time"))); + + List alarmFields = new ArrayList<>(); + alarmFields.add(new EntityKey(EntityKeyType.ALARM_FIELD, "type")); + + List entityFields = new ArrayList<>(); + entityFields.add(new EntityKey(EntityKeyType.ENTITY_FIELD, "name")); + + List latestValues = new ArrayList<>(); + latestValues.add(new EntityKey(EntityKeyType.TIME_SERIES, "temperature")); + + EntityTypeFilter deviceTypeFilter = new EntityTypeFilter(); + deviceTypeFilter.setEntityType(EntityType.DEVICE); + AlarmDataQuery deviceAlarmQuery = new AlarmDataQuery(deviceTypeFilter, pageLink, entityFields, latestValues, null, alarmFields); + + PageData alarmPageData = findAlarmsByQueryAndCheck(deviceAlarmQuery, 10); + List retrievedAlarmTemps = alarmPageData.getData().stream().map(alarmData -> alarmData.getLatest().get(EntityKeyType.TIME_SERIES).get("temperature").getValue()).toList(); + assertThat(retrievedAlarmTemps).containsExactlyInAnyOrderElementsOf(temps); + + List retrievedDeviceNames = alarmPageData.getData().stream().map(alarmData -> alarmData.getLatest().get(EntityKeyType.ENTITY_FIELD).get("name").getValue()).toList(); + assertThat(retrievedDeviceNames).containsExactlyInAnyOrderElementsOf(deviceNames); + } + private void testCountAlarmsByQuery(List alarms) throws Exception { AlarmCountQuery countQuery = new AlarmCountQuery(); @@ -924,7 +983,7 @@ public class EntityQueryControllerTest extends AbstractControllerTest { List queueStatsList = new ArrayList<>(); for (int i = 0; i < 97; i++) { QueueStats queueStats = new QueueStats(); - queueStats.setQueueName(StringUtils.randomAlphabetic(5)); + queueStats.setQueueName("test" + StringUtils.randomAlphabetic(5)); queueStats.setServiceId(StringUtils.randomAlphabetic(5)); queueStats.setTenantId(savedTenant.getTenantId()); queueStatsList.add(queueStatsService.save(savedTenant.getId(), queueStats)); @@ -940,8 +999,11 @@ public class EntityQueryControllerTest extends AbstractControllerTest { EntityDataPageLink pageLink = new EntityDataPageLink(10, 0, null, sortOrder); List entityFields = Arrays.asList(new EntityKey(EntityKeyType.ENTITY_FIELD, "name"), new EntityKey(EntityKeyType.ENTITY_FIELD, "queueName"), new EntityKey(EntityKeyType.ENTITY_FIELD, "serviceId")); + List keyFilters = Collections.singletonList( + getEntityFieldStartsWithFilter("queueName", "test") + ); - EntityDataQuery query = new EntityDataQuery(entityTypeFilter, pageLink, entityFields, null, null); + EntityDataQuery query = new EntityDataQuery(entityTypeFilter, pageLink, entityFields, null, keyFilters); PageData data = findByQueryAndCheck(query, 97); Assert.assertEquals(10, data.getTotalPages()); @@ -956,7 +1018,7 @@ public class EntityQueryControllerTest extends AbstractControllerTest { }); - EntityCountQuery countQuery = new EntityCountQuery(entityTypeFilter); + EntityCountQuery countQuery = new EntityCountQuery(entityTypeFilter, keyFilters); countByQueryAndCheck(countQuery, 97); } @@ -983,10 +1045,10 @@ public class EntityQueryControllerTest extends AbstractControllerTest { KeyFilter activeAlarmTimeFilter = getServerAttributeNumericGreaterThanKeyFilter("alarmActiveTime", 5); KeyFilter activeAlarmTimeToLongFilter = getServerAttributeNumericGreaterThanKeyFilter("alarmActiveTime", 30); - KeyFilter tenantOwnerNameFilter = getEntityFieldStringEqualToKeyFilter("ownerName", TEST_TENANT_NAME); - KeyFilter wrongOwnerNameFilter = getEntityFieldStringEqualToKeyFilter("ownerName", "wrongName"); - KeyFilter tenantOwnerTypeFilter = getEntityFieldStringEqualToKeyFilter("ownerType", "TENANT"); - KeyFilter customerOwnerTypeFilter = getEntityFieldStringEqualToKeyFilter("ownerType", "CUSTOMER"); + KeyFilter tenantOwnerNameFilter = getEntityFieldEqualFilter("ownerName", TEST_TENANT_NAME); + KeyFilter wrongOwnerNameFilter = getEntityFieldEqualFilter("ownerName", "wrongName"); + KeyFilter tenantOwnerTypeFilter = getEntityFieldEqualFilter("ownerType", "TENANT"); + KeyFilter customerOwnerTypeFilter = getEntityFieldEqualFilter("ownerType", "CUSTOMER"); // all devices with ownerName = TEST TENANT EntityCountQuery query = new EntityCountQuery(filter, List.of(activeAlarmTimeFilter, tenantOwnerNameFilter)); @@ -1010,6 +1072,119 @@ public class EntityQueryControllerTest extends AbstractControllerTest { countByQueryAndCheck(customerEntitiesQuery, 0); } + @Test + public void testFindDevicesByDisplayName() throws Exception { + loginTenantAdmin(); + int numOfDevices = 3; + + for (int i = 0; i < numOfDevices; i++) { + Device device = new Device(); + String name = "Device" + i; + device.setName(name); + device.setLabel("Device Label " + i); + device.setType("testFindDevicesByDisplayName"); + + Device savedDevice = doPost("/api/device?accessToken=" + name, device, Device.class); + } + + DeviceTypeFilter filter = new DeviceTypeFilter(); + filter.setDeviceTypes(List.of("testFindDevicesByDisplayName")); + filter.setDeviceNameFilter(""); + + KeyFilter displayNameFilter = getEntityFieldEqualFilter("displayName", "Device Label " + 0); + + EntityDataSortOrder sortOrder = new EntityDataSortOrder( + new EntityKey(EntityKeyType.ENTITY_FIELD, "displayName"), EntityDataSortOrder.Direction.ASC + ); + EntityDataPageLink pageLink = new EntityDataPageLink(10, 0, null, sortOrder); + List entityFields = List.of(new EntityKey(EntityKeyType.ENTITY_FIELD, "name"), new EntityKey(EntityKeyType.ENTITY_FIELD, "displayName")); + + // all devices with ownerName = TEST TENANT + EntityDataQuery query = new EntityDataQuery(filter, pageLink, entityFields, Collections.emptyList(), Collections.emptyList()); + checkEntitiesByQuery(query, numOfDevices, (i, entity) -> { + String name = entity.getLatest().get(EntityKeyType.ENTITY_FIELD).getOrDefault("name", new TsValue(0, "Invalid")).getValue(); + String displayName = entity.getLatest().get(EntityKeyType.ENTITY_FIELD).getOrDefault("displayName", new TsValue(0, "Invalid")).getValue(); + Assert.assertEquals("Device" + i, name); + Assert.assertEquals("Device Label " + i, displayName); + }); + + // all devices with ownerName = TEST TENANT + EntityDataQuery displayNameFilterQuery = new EntityDataQuery(filter, pageLink, entityFields, Collections.emptyList(), List.of(displayNameFilter)); + checkEntitiesByQuery(displayNameFilterQuery, 1, (i, entity) -> { + String name = entity.getLatest().get(EntityKeyType.ENTITY_FIELD).getOrDefault("name", new TsValue(0, "Invalid")).getValue(); + String displayName = entity.getLatest().get(EntityKeyType.ENTITY_FIELD).getOrDefault("displayName", new TsValue(0, "Invalid")).getValue(); + Assert.assertEquals("Device" + i, name); + Assert.assertEquals("Device Label " + i, displayName); + }); + } + + @Test + public void testFindUsersByDisplayName() throws Exception { + loginTenantAdmin(); + + User userA = new User(); + userA.setAuthority(Authority.TENANT_ADMIN); + userA.setFirstName("John"); + userA.setLastName("Doe"); + userA.setEmail("john.doe@tb.org"); + userA = doPost("/api/user", userA, User.class); + var aId = userA.getId(); + + User userB = new User(); + userB.setAuthority(Authority.TENANT_ADMIN); + userB.setFirstName("John"); + userB.setEmail("john@tb.org"); + userB = doPost("/api/user", userB, User.class); + var bId = userB.getId(); + + User userC = new User(); + userC.setAuthority(Authority.TENANT_ADMIN); + userC.setLastName("Doe"); + userC.setEmail("doe@tb.org"); + userC = doPost("/api/user", userC, User.class); + var cId = userC.getId(); + + User userD = new User(); + userD.setAuthority(Authority.TENANT_ADMIN); + userD.setEmail("noname@tb.org"); + userD = doPost("/api/user", userD, User.class); + var dId = userD.getId(); + + EntityTypeFilter filter = new EntityTypeFilter(); + filter.setEntityType(EntityType.USER); + + EntityDataSortOrder sortOrder = new EntityDataSortOrder( + new EntityKey(EntityKeyType.ENTITY_FIELD, "displayName"), EntityDataSortOrder.Direction.ASC + ); + EntityDataPageLink pageLink = new EntityDataPageLink(10, 0, null, sortOrder); + List entityFields = List.of(new EntityKey(EntityKeyType.ENTITY_FIELD, "displayName")); + + EntityDataQuery query = new EntityDataQuery(filter, pageLink, entityFields, Collections.emptyList(), List.of(getEntityFieldEqualFilter("displayName", "John Doe"))); + checkEntitiesByQuery(query, 1, (i, entity) -> { + Assert.assertEquals(aId, entity.getEntityId()); + String displayName = entity.getLatest().get(EntityKeyType.ENTITY_FIELD).getOrDefault("displayName", new TsValue(0, "Invalid")).getValue(); + Assert.assertEquals("John Doe", displayName); + }); + query = new EntityDataQuery(filter, pageLink, entityFields, Collections.emptyList(), List.of(getEntityFieldEqualFilter("displayName", "John"))); + checkEntitiesByQuery(query, 1, (i, entity) -> { + Assert.assertEquals(bId, entity.getEntityId()); + String displayName = entity.getLatest().get(EntityKeyType.ENTITY_FIELD).getOrDefault("displayName", new TsValue(0, "Invalid")).getValue(); + Assert.assertEquals("John", displayName); + }); + query = new EntityDataQuery(filter, pageLink, entityFields, Collections.emptyList(), List.of(getEntityFieldEqualFilter("displayName", "Doe"))); + checkEntitiesByQuery(query, 1, (i, entity) -> { + Assert.assertEquals(cId, entity.getEntityId()); + String displayName = entity.getLatest().get(EntityKeyType.ENTITY_FIELD).getOrDefault("displayName", new TsValue(0, "Invalid")).getValue(); + Assert.assertEquals("Doe", displayName); + }); + query = new EntityDataQuery(filter, pageLink, entityFields, Collections.emptyList(), List.of(getEntityFieldEqualFilter("displayName", "noname@tb.org"))); + checkEntitiesByQuery(query, 1, (i, entity) -> { + Assert.assertEquals(dId, entity.getEntityId()); + String displayName = entity.getLatest().get(EntityKeyType.ENTITY_FIELD).getOrDefault("displayName", new TsValue(0, "Invalid")).getValue(); + Assert.assertEquals("noname@tb.org", displayName); + }); + } + @Test public void testFindDevicesByOwnerNameAndOwnerType() throws Exception { loginTenantAdmin(); @@ -1032,10 +1207,10 @@ public class EntityQueryControllerTest extends AbstractControllerTest { filter.setDeviceNameFilter(""); KeyFilter activeAlarmTimeFilter = getServerAttributeNumericGreaterThanKeyFilter("alarmActiveTime", 5); - KeyFilter tenantOwnerNameFilter = getEntityFieldStringEqualToKeyFilter("ownerName", TEST_TENANT_NAME); - KeyFilter wrongOwnerNameFilter = getEntityFieldStringEqualToKeyFilter("ownerName", "wrongName"); - KeyFilter tenantOwnerTypeFilter = getEntityFieldStringEqualToKeyFilter("ownerType", "TENANT"); - KeyFilter customerOwnerTypeFilter = getEntityFieldStringEqualToKeyFilter("ownerType", "CUSTOMER"); + KeyFilter tenantOwnerNameFilter = getEntityFieldEqualFilter("ownerName", TEST_TENANT_NAME); + KeyFilter wrongOwnerNameFilter = getEntityFieldEqualFilter("ownerName", "wrongName"); + KeyFilter tenantOwnerTypeFilter = getEntityFieldEqualFilter("ownerType", "TENANT"); + KeyFilter customerOwnerTypeFilter = getEntityFieldEqualFilter("ownerType", "CUSTOMER"); EntityDataSortOrder sortOrder = new EntityDataSortOrder( new EntityKey(EntityKeyType.ENTITY_FIELD, "createdTime"), EntityDataSortOrder.Direction.ASC @@ -1047,19 +1222,30 @@ public class EntityQueryControllerTest extends AbstractControllerTest { // all devices with ownerName = TEST TENANT EntityDataQuery query = new EntityDataQuery(filter, pageLink, entityFields, latestValues, List.of(activeAlarmTimeFilter, tenantOwnerNameFilter)); - checkEntitiesByQuery(query, numOfDevices, TEST_TENANT_NAME, "TENANT"); + BiConsumer checkFunction = (i, entity) -> { + String name = entity.getLatest().get(EntityKeyType.ENTITY_FIELD).getOrDefault("name", new TsValue(0, "Invalid")).getValue(); + String ownerName = entity.getLatest().get(EntityKeyType.ENTITY_FIELD).getOrDefault("ownerName", new TsValue(0, "Invalid")).getValue(); + String ownerType = entity.getLatest().get(EntityKeyType.ENTITY_FIELD).getOrDefault("ownerType", new TsValue(0, "Invalid")).getValue(); + String alarmActiveTime = entity.getLatest().get(EntityKeyType.ATTRIBUTE).getOrDefault("alarmActiveTime", new TsValue(0, "-1")).getValue(); + + Assert.assertEquals("Device" + i, name); + Assert.assertEquals(TEST_TENANT_NAME, ownerName); + Assert.assertEquals("TENANT", ownerType); + Assert.assertEquals("1" + i, alarmActiveTime); + }; + checkEntitiesByQuery(query, numOfDevices, checkFunction); // all devices with wrong ownerName EntityDataQuery wrongTenantNameQuery = new EntityDataQuery(filter, pageLink, entityFields, latestValues, List.of(activeAlarmTimeFilter, wrongOwnerNameFilter)); - checkEntitiesByQuery(wrongTenantNameQuery, 0, null, null); + checkEntitiesByQuery(wrongTenantNameQuery, 0, null); // all devices with owner type = TENANT EntityDataQuery tenantEntitiesQuery = new EntityDataQuery(filter, pageLink, entityFields, latestValues, List.of(activeAlarmTimeFilter, tenantOwnerTypeFilter)); - checkEntitiesByQuery(tenantEntitiesQuery, numOfDevices, TEST_TENANT_NAME, "TENANT"); + checkEntitiesByQuery(tenantEntitiesQuery, numOfDevices, checkFunction); // all devices with owner type = CUSTOMER EntityDataQuery customerEntitiesQuery = new EntityDataQuery(filter, pageLink, entityFields, latestValues, List.of(activeAlarmTimeFilter, customerOwnerTypeFilter)); - checkEntitiesByQuery(customerEntitiesQuery, 0, null, null); + checkEntitiesByQuery(customerEntitiesQuery, 0, null); } @Test @@ -1105,6 +1291,28 @@ public class EntityQueryControllerTest extends AbstractControllerTest { findByQueryAndCheck(query, 0); } + private void checkEntitiesByQuery(EntityDataQuery query, int expectedNumOfDevices, BiConsumer checkFunction) throws Exception { + await() + .alias("data by query") + .atMost(30, TimeUnit.SECONDS) + .until(() -> { + var data = findByQuery(query); + var loadedEntities = new ArrayList<>(data.getData()); + return loadedEntities.size() == expectedNumOfDevices; + }); + if (expectedNumOfDevices == 0) { + return; + } + var data = findByQuery(query); + var loadedEntities = new ArrayList<>(data.getData()); + + Assert.assertEquals(expectedNumOfDevices, loadedEntities.size()); + + for (int i = 0; i < expectedNumOfDevices; i++) { + checkFunction.accept(i, loadedEntities.get(i)); + } + } + private void checkEntitiesByQuery(EntityDataQuery query, int expectedNumOfDevices, String expectedOwnerName, String expectedOwnerType) throws Exception { await() .alias("data by query") @@ -1141,31 +1349,59 @@ public class EntityQueryControllerTest extends AbstractControllerTest { }); } + protected PageData findAlarmsByQuery(AlarmDataQuery query) throws Exception { + return doPostWithTypedResponse("/api/alarmsQuery/find", query, new TypeReference<>() {}); + } + protected PageData findByQueryAndCheck(EntityDataQuery query, int expectedResultSize) throws Exception { PageData result = findByQuery(query); assertThat(result.getTotalElements()).isEqualTo(expectedResultSize); return result; } + protected PageData findAlarmsByQueryAndCheck(AlarmDataQuery query, int expectedResultSize) throws Exception { + PageData result = findAlarmsByQuery(query); + assertThat(result.getTotalElements()).isEqualTo(expectedResultSize); + return result; + } + protected Long countByQuery(EntityCountQuery countQuery) throws Exception { return doPostWithResponse("/api/entitiesQuery/count", countQuery, Long.class); } + protected Long countAlarmsByQuery(AlarmCountQuery countQuery) throws Exception { + return doPostWithResponse("/api/alarmsQuery/count", countQuery, Long.class); + } + protected Long countByQueryAndCheck(EntityCountQuery query, long expectedResult) throws Exception { Long result = countByQuery(query); assertThat(result).isEqualTo(expectedResult); return result; } - private KeyFilter getEntityFieldStringEqualToKeyFilter(String keyName, String value) { - KeyFilter tenantOwnerNameFilter = new KeyFilter(); - tenantOwnerNameFilter.setKey(new EntityKey(EntityKeyType.ENTITY_FIELD, keyName)); - tenantOwnerNameFilter.setValueType(EntityKeyValueType.STRING); - StringFilterPredicate ownerNamePredicate = new StringFilterPredicate(); - ownerNamePredicate.setValue(FilterPredicateValue.fromString(value)); - ownerNamePredicate.setOperation(StringFilterPredicate.StringOperation.EQUAL); - tenantOwnerNameFilter.setPredicate(ownerNamePredicate); - return tenantOwnerNameFilter; + protected Long countAlarmsByQueryAndCheck(AlarmCountQuery query, long expectedResult) throws Exception { + Long result = countAlarmsByQuery(query); + assertThat(result).isEqualTo(expectedResult); + return result; + } + + private KeyFilter getEntityFieldEqualFilter(String keyName, String value) { + return getEntityFieldKeyFilter(keyName, value, StringFilterPredicate.StringOperation.EQUAL); + } + + private KeyFilter getEntityFieldStartsWithFilter(String keyName, String value) { + return getEntityFieldKeyFilter(keyName, value, StringFilterPredicate.StringOperation.STARTS_WITH); + } + + private KeyFilter getEntityFieldKeyFilter(String keyName, String value, StringFilterPredicate.StringOperation operation) { + KeyFilter filter = new KeyFilter(); + filter.setKey(new EntityKey(EntityKeyType.ENTITY_FIELD, keyName)); + filter.setValueType(EntityKeyValueType.STRING); + StringFilterPredicate predicate = new StringFilterPredicate(); + predicate.setValue(FilterPredicateValue.fromString(value)); + predicate.setOperation(operation); + filter.setPredicate(predicate); + return filter; } private KeyFilter getServerAttributeNumericGreaterThanKeyFilter(String attribute, int value) { diff --git a/application/src/test/java/org/thingsboard/server/controller/EntityViewControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/EntityViewControllerTest.java index 10b49dfe1d..167048a969 100644 --- a/application/src/test/java/org/thingsboard/server/controller/EntityViewControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/EntityViewControllerTest.java @@ -853,4 +853,29 @@ public class EntityViewControllerTest extends AbstractControllerTest { EntityViewId entityViewId = getNewSavedEntityView("EntityView for Test WithRelations Transactional Exception").getId(); testEntityDaoWithRelationsTransactionalException(entityViewDao, tenantId, entityViewId, "/api/entityView/" + entityViewId); } + + @Test + public void testSaveEntityViewWithUniquifyStrategy() throws Exception { + EntityView view = new EntityView(); + view.setEntityId(testDevice.getId()); + view.setTenantId(tenantId); + view.setType("default"); + view.setName("My unique view"); + + EntityView savedView = doPost("/api/entityView", view, EntityView.class); + + doPost("/api/entityView?nameConflictPolicy=FAIL", view).andExpect(status().isBadRequest()); + + EntityView secondView = doPost("/api/entityView?nameConflictPolicy=UNIQUIFY", view, EntityView.class); + assertThat(secondView.getName()).startsWith("My unique view_"); + + EntityView thirdView = doPost("/api/entityView?nameConflictPolicy=UNIQUIFY&uniquifySeparator=-", view, EntityView.class); + assertThat(thirdView.getName()).startsWith("My unique view-"); + + EntityView fourthView = doPost("/api/entityView?nameConflictPolicy=UNIQUIFY&uniquifyStrategy=INCREMENTAL", view, EntityView.class); + assertThat(fourthView.getName()).isEqualTo("My unique view_1"); + + EntityView fifthEntityView = doPost("/api/entityView?nameConflictPolicy=UNIQUIFY&uniquifyStrategy=INCREMENTAL", view, EntityView.class); + assertThat(fifthEntityView.getName()).isEqualTo("My unique view_2"); + } } diff --git a/application/src/test/java/org/thingsboard/server/controller/TbResourceControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/TbResourceControllerTest.java index 3b194de7b4..d88aaa2756 100644 --- a/application/src/test/java/org/thingsboard/server/controller/TbResourceControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/TbResourceControllerTest.java @@ -31,6 +31,7 @@ import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.Dashboard; import org.thingsboard.server.common.data.DashboardInfo; import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.EntityInfo; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.ResourceType; import org.thingsboard.server.common.data.StringUtils; @@ -321,18 +322,15 @@ public class TbResourceControllerTest extends AbstractControllerTest { var referenceValues = JacksonUtil.toJsonNode(deleteResponse).get("references"); Assert.assertNotNull(referenceValues); - var widgetTypeInfos = JacksonUtil.readValue(referenceValues.toString(), new TypeReference>>() { + var widgetTypeInfos = JacksonUtil.readValue(referenceValues.toString(), new TypeReference>>() { }); Assert.assertNotNull(widgetTypeInfos); Assert.assertFalse(widgetTypeInfos.isEmpty()); Assert.assertEquals(1, widgetTypeInfos.size()); - var dashboardInfo = widgetTypeInfos.get(EntityType.WIDGET_TYPE.name()).get(0); - Assert.assertNotNull(dashboardInfo); - - WidgetTypeInfo foundedWidgetType = doGet("/api/widgetTypeInfo/" + savedWidgetType.getId().getId().toString(), WidgetTypeInfo.class); - Assert.assertNotNull(foundedWidgetType); - Assert.assertEquals(foundedWidgetType, dashboardInfo); + var widgetTypeInfo = widgetTypeInfos.get(EntityType.WIDGET_TYPE.name()).get(0); + Assert.assertNotNull(widgetTypeInfo); + Assert.assertEquals(new EntityInfo(savedWidgetType.getId(), savedWidgetType.getName()), widgetTypeInfo); } @Test @@ -372,7 +370,7 @@ public class TbResourceControllerTest extends AbstractControllerTest { Assert.assertTrue(isSuccess); var referenceValues = JacksonUtil.toJsonNode(deleteResponse).get("references"); - var widgetTypeInfos = JacksonUtil.readValue(referenceValues.toString(), new TypeReference>>() { + var widgetTypeInfos = JacksonUtil.readValue(referenceValues.toString(), new TypeReference>>() { }); Assert.assertNull(widgetTypeInfos); } @@ -417,7 +415,7 @@ public class TbResourceControllerTest extends AbstractControllerTest { var referenceValues = JacksonUtil.toJsonNode(deleteResponse).get("references"); Assert.assertNotNull(referenceValues); - var dashboardInfos = JacksonUtil.readValue(referenceValues.toString(), new TypeReference>>() { + var dashboardInfos = JacksonUtil.readValue(referenceValues.toString(), new TypeReference>>() { }); Assert.assertNotNull(dashboardInfos); Assert.assertFalse(dashboardInfos.isEmpty()); @@ -425,10 +423,7 @@ public class TbResourceControllerTest extends AbstractControllerTest { var dashboardInfo = dashboardInfos.get(EntityType.DASHBOARD.name()).get(0); Assert.assertNotNull(dashboardInfo); - - DashboardInfo foundDashboard = doGet("/api/dashboard/info/" + savedDashboard.getId().getId().toString(), DashboardInfo.class); - Assert.assertNotNull(foundDashboard); - Assert.assertEquals(foundDashboard, dashboardInfo); + Assert.assertEquals(new EntityInfo(savedDashboard.getId(), savedDashboard.getName()), dashboardInfo); } @Test @@ -469,7 +464,7 @@ public class TbResourceControllerTest extends AbstractControllerTest { Assert.assertTrue(isSuccess); var referenceValues = JacksonUtil.toJsonNode(deleteResponse).get("references"); - var dashboardInfos = JacksonUtil.readValue(referenceValues.toString(), new TypeReference>>() { + var dashboardInfos = JacksonUtil.readValue(referenceValues.toString(), new TypeReference>>() { }); Assert.assertNull(dashboardInfos); } @@ -948,7 +943,7 @@ public class TbResourceControllerTest extends AbstractControllerTest { private List loadLwm2mResources() throws Exception { - var models = List.of("1", "2", "3", "5", "6", "9", "19", "3303"); + var models = List.of("1", "2", "3-1_2", "5", "6", "9", "19", "3303"); List resources = new ArrayList<>(models.size()); diff --git a/application/src/test/java/org/thingsboard/server/controller/TelemetryControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/TelemetryControllerTest.java index ad3c6d5312..57ee84a507 100644 --- a/application/src/test/java/org/thingsboard/server/controller/TelemetryControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/TelemetryControllerTest.java @@ -19,7 +19,10 @@ import com.fasterxml.jackson.databind.node.ObjectNode; import org.junit.Assert; import org.junit.Test; import org.springframework.test.context.TestPropertySource; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; +import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.SaveDeviceWithCredentialsRequest; import org.thingsboard.server.common.data.kv.BasicTsKvEntry; @@ -110,6 +113,13 @@ public class TelemetryControllerTest extends AbstractControllerTest { var monthResult = result.get("t").get(0); Assert.assertEquals(22L, monthResult.get("value").asLong()); Assert.assertEquals(middleOfTheInterval, monthResult.get("ts").asLong()); + + // get all latest (without keys parameter) + ObjectNode allLatest = doGetAsync("/api/plugins/telemetry/DEVICE/" + device.getId() + + "/values/timeseries?startTs={startTs}&endTs={endTs}&agg={agg}&intervalType={intervalType}&timeZone={timeZone}", + ObjectNode.class, startTs, endTs, "SUM", "WEEK_ISO", "Europe/Kyiv"); + Assert.assertNotNull(allLatest); + Assert.assertNotNull(allLatest.get("t")); } @Test @@ -232,6 +242,63 @@ public class TelemetryControllerTest extends AbstractControllerTest { doPostAsync("/api/plugins/telemetry/DEVICE/" + device.getId() + "/timeseries/smth", invalidRequestBody2, String.class, status().isBadRequest()); } + @Test + public void testDeleteTelemetryByKeyWithComma() throws Exception { + loginTenantAdmin(); + Device device = createDevice(); + + String tsKey = "key1,key2"; + String testBody = JacksonUtil.newObjectNode() + .put(tsKey, "value") + .toString(); + doPostAsync("/api/plugins/telemetry/DEVICE/" + device.getId() + "/timeseries/smth", testBody, String.class, status().isOk()); + + MultiValueMap params = new LinkedMultiValueMap<>(); + params.add("key", tsKey); + params.add("deleteAllDataForKeys", "true"); + + ObjectNode tsData = readResponse(doGetAsync("/api/plugins/telemetry/DEVICE/" + device.getId() + "/values/timeseries", params), ObjectNode.class); + assertThat(tsData.get("key1,key2").get(0).get("value").asText()).isEqualTo("value"); + + doDeleteAsync("/api/plugins/telemetry/DEVICE/" + device.getId() + "/timeseries/delete", params); + + ObjectNode tsDataAfterDeletion = readResponse(doGetAsync("/api/plugins/telemetry/DEVICE/" + device.getId() + "/values/timeseries", params), ObjectNode.class); + Assert.assertTrue(tsDataAfterDeletion.get("key1,key2").get(0).get("value").isNull()); + } + + @Test + public void testDeleteTelemetryByKeysWithComma() throws Exception { + loginTenantAdmin(); + Device device = createDevice(); + + String keyWithComma = "key1,key2"; + String testBody = JacksonUtil.newObjectNode() + .put(keyWithComma, "value") + .toString(); + doPostAsync("/api/plugins/telemetry/DEVICE/" + device.getId() + "/timeseries/smth", testBody, String.class, status().isOk()); + + String key = "key3"; + String testBody2 = JacksonUtil.newObjectNode() + .put(key, "value") + .toString(); + MultiValueMap params = new LinkedMultiValueMap<>(); + params.add("key", keyWithComma); + params.add("key", key); + params.add("deleteAllDataForKeys", "true"); + + doPostAsync("/api/plugins/telemetry/DEVICE/" + device.getId() + "/timeseries/smth", testBody2, String.class, status().isOk()); + + ObjectNode tsData = readResponse(doGetAsync("/api/plugins/telemetry/DEVICE/" + device.getId() + "/values/timeseries", params), ObjectNode.class); + assertThat(tsData.get("key1,key2").get(0).get("value").asText()).isEqualTo("value"); + assertThat(tsData.get("key3").get(0).get("value").asText()).isEqualTo("value"); + + doDeleteAsync("/api/plugins/telemetry/DEVICE/" + device.getId() + "/timeseries/delete", params); + + ObjectNode tsDataAfterDeletion = readResponse(doGetAsync("/api/plugins/telemetry/DEVICE/" + device.getId() + "/values/timeseries", params), ObjectNode.class); + Assert.assertTrue(tsDataAfterDeletion.get("key1,key2").get(0).get("value").isNull()); + Assert.assertTrue(tsDataAfterDeletion.get("key3").get(0).get("value").isNull()); + } + private Device createDevice() throws Exception { String testToken = "TEST_TOKEN"; diff --git a/application/src/test/java/org/thingsboard/server/controller/TenantControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/TenantControllerTest.java index ba45be8e28..a33e8ca411 100644 --- a/application/src/test/java/org/thingsboard/server/controller/TenantControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/TenantControllerTest.java @@ -243,6 +243,29 @@ public class TenantControllerTest extends AbstractControllerTest { .andExpect(statusReason(containsString(msgErrorNoFound("Tenant", tenantIdStr)))); } + @Test + public void testDeleteTenantByTenantAdmin() throws Exception { + loginSysAdmin(); + Tenant tenant = new Tenant(); + tenant.setTitle("My tenant"); + Tenant savedTenant = saveTenant(tenant); + + //login as tenant admin + User tenantAdminUser = new User(); + tenantAdminUser.setAuthority(Authority.TENANT_ADMIN); + tenantAdminUser.setTenantId(savedTenant.getId()); + tenantAdminUser.setEmail("tenantToDelete@thingsboard.io"); + + createUserAndLogin(tenantAdminUser, TENANT_ADMIN_PASSWORD); + + String tenantIdStr = savedTenant.getId().getId().toString(); + deleteTenant(savedTenant.getId()); + loginSysAdmin(); + doGet("/api/tenant/" + tenantIdStr) + .andExpect(status().isNotFound()) + .andExpect(statusReason(containsString(msgErrorNoFound("Tenant", tenantIdStr)))); + } + @Test public void testFindTenants() throws Exception { loginSysAdmin(); diff --git a/application/src/test/java/org/thingsboard/server/controller/TwoFactorAuthConfigTest.java b/application/src/test/java/org/thingsboard/server/controller/TwoFactorAuthConfigTest.java index 3611409cb7..af238fa23b 100644 --- a/application/src/test/java/org/thingsboard/server/controller/TwoFactorAuthConfigTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/TwoFactorAuthConfigTest.java @@ -30,6 +30,8 @@ import org.springframework.web.util.UriComponentsBuilder; import org.thingsboard.rule.engine.api.SmsService; import org.thingsboard.server.common.data.CacheConstants; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.notification.targets.platform.AllUsersFilter; +import org.thingsboard.server.common.data.notification.targets.platform.TenantAdministratorsFilter; import org.thingsboard.server.common.data.security.model.mfa.PlatformTwoFaSettings; import org.thingsboard.server.common.data.security.model.mfa.account.AccountTwoFaSettings; import org.thingsboard.server.common.data.security.model.mfa.account.SmsTwoFaAccountConfig; @@ -48,6 +50,7 @@ import org.thingsboard.server.service.security.auth.mfa.provider.impl.TotpTwoFaP import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.Set; import java.util.stream.Collectors; import static org.assertj.core.api.Assertions.assertThat; @@ -85,7 +88,6 @@ public class TwoFactorAuthConfigTest extends AbstractControllerTest { twoFaConfigManager.deletePlatformTwoFaSettings(tenantId); } - @Test public void testSavePlatformTwoFaSettings() throws Exception { loginSysAdmin(); @@ -102,15 +104,32 @@ public class TwoFactorAuthConfigTest extends AbstractControllerTest { twoFaSettings.setVerificationCodeCheckRateLimit("3:900"); twoFaSettings.setMaxVerificationFailuresBeforeUserLockout(10); twoFaSettings.setTotalAllowedTimeForVerification(3600); + twoFaSettings.setEnforceTwoFa(true); + twoFaSettings.setEnforcedUsersFilter(new AllUsersFilter()); - doPost("/api/2fa/settings", twoFaSettings).andExpect(status().isOk()); + saveTwoFaSettings(twoFaSettings); - PlatformTwoFaSettings savedTwoFaSettings = readResponse(doGet("/api/2fa/settings").andExpect(status().isOk()), PlatformTwoFaSettings.class); + PlatformTwoFaSettings savedTwoFaSettings = findTwoFaSettings(); assertThat(savedTwoFaSettings.getProviders()).hasSize(2); assertThat(savedTwoFaSettings.getProviders()).contains(totpTwoFaProviderConfig, smsTwoFaProviderConfig); } + @Test + public void testSavePlatformTwoFaSettingsWithEnforceTwoFaWithoutProviders() throws Exception { + loginSysAdmin(); + + PlatformTwoFaSettings twoFaSettings = new PlatformTwoFaSettings(); + twoFaSettings.setProviders(List.of()); + twoFaSettings.setMinVerificationCodeSendPeriod(5); + twoFaSettings.setVerificationCodeCheckRateLimit("3:900"); + twoFaSettings.setMaxVerificationFailuresBeforeUserLockout(10); + twoFaSettings.setTotalAllowedTimeForVerification(3600); + twoFaSettings.setEnforceTwoFa(true); + + doPost("/api/2fa/settings", twoFaSettings).andExpect(status().isBadRequest()); + } + @Test public void testSavePlatformTwoFaSettings_validationError() throws Exception { loginSysAdmin(); @@ -157,17 +176,6 @@ public class TwoFactorAuthConfigTest extends AbstractControllerTest { assertThat(errorResponse).containsIgnoringCase("verificationCodeLifetime is required"); } - private String savePlatformTwoFaSettingsAndGetError(TwoFaProviderConfig invalidTwoFaProviderConfig) throws Exception { - PlatformTwoFaSettings twoFaSettings = new PlatformTwoFaSettings(); - twoFaSettings.setProviders(Collections.singletonList(invalidTwoFaProviderConfig)); - twoFaSettings.setMinVerificationCodeSendPeriod(5); - twoFaSettings.setTotalAllowedTimeForVerification(100); - - return getErrorMessage(doPost("/api/2fa/settings", twoFaSettings) - .andExpect(status().isBadRequest())); - } - - @Test public void testSaveTwoFaAccountConfig_providerNotConfigured() throws Exception { configureSmsTwoFaProvider("${code}"); @@ -268,24 +276,6 @@ public class TwoFactorAuthConfigTest extends AbstractControllerTest { assertThat(errorMessage).containsIgnoringCase("verification code is incorrect"); } - private TotpTwoFaAccountConfig generateTotpTwoFaAccountConfig(TotpTwoFaProviderConfig totpTwoFaProviderConfig) throws Exception { - TwoFaAccountConfig generatedTwoFaAccountConfig = readResponse(doPost("/api/2fa/account/config/generate?providerType=TOTP") - .andExpect(status().isOk()), TwoFaAccountConfig.class); - assertThat(generatedTwoFaAccountConfig).isInstanceOf(TotpTwoFaAccountConfig.class); - - assertThat(((TotpTwoFaAccountConfig) generatedTwoFaAccountConfig)).satisfies(accountConfig -> { - UriComponents otpAuthUrl = UriComponentsBuilder.fromUriString(accountConfig.getAuthUrl()).build(); - assertThat(otpAuthUrl.getScheme()).isEqualTo("otpauth"); - assertThat(otpAuthUrl.getHost()).isEqualTo("totp"); - assertThat(otpAuthUrl.getQueryParams().getFirst("issuer")).isEqualTo(totpTwoFaProviderConfig.getIssuerName()); - assertThat(otpAuthUrl.getPath()).isEqualTo("/%s:%s", totpTwoFaProviderConfig.getIssuerName(), TENANT_ADMIN_EMAIL); - assertThat(otpAuthUrl.getQueryParams().getFirst("secret")).satisfies(secretKey -> { - assertDoesNotThrow(() -> Base32.decode(secretKey)); - }); - }); - return (TotpTwoFaAccountConfig) generatedTwoFaAccountConfig; - } - @Test public void testGetTwoFaAccountConfig_whenProviderNotConfigured() throws Exception { testVerifyAndSaveTotpTwoFaAccountConfig(); @@ -419,6 +409,56 @@ public class TwoFactorAuthConfigTest extends AbstractControllerTest { assertThat(accountConfig).isEqualTo(initialSmsTwoFaAccountConfig); } + @Test + public void testIsTwoFaEnabled() throws Exception { + configureSmsTwoFaProvider("${code}"); + SmsTwoFaAccountConfig accountConfig = new SmsTwoFaAccountConfig(); + accountConfig.setPhoneNumber("+38050505050"); + twoFaConfigManager.saveTwoFaAccountConfig(tenantId, tenantAdminUser, accountConfig); + + assertThat(twoFactorAuthService.isTwoFaEnabled(tenantId, tenantAdminUser)).isTrue(); + } + + @Test + public void testDeleteTwoFaAccountConfig() throws Exception { + configureSmsTwoFaProvider("${code}"); + loginTenantAdmin(); + SmsTwoFaAccountConfig accountConfig = new SmsTwoFaAccountConfig(); + accountConfig.setPhoneNumber("+38050505050"); + twoFaConfigManager.saveTwoFaAccountConfig(tenantId, tenantAdminUser, accountConfig); + + AccountTwoFaSettings accountTwoFaSettings = readResponse(doGet("/api/2fa/account/settings").andExpect(status().isOk()), AccountTwoFaSettings.class); + TwoFaAccountConfig savedAccountConfig = accountTwoFaSettings.getConfigs().get(TwoFaProviderType.SMS); + assertThat(savedAccountConfig).isEqualTo(accountConfig); + + PlatformTwoFaSettings twoFaSettings = twoFaConfigManager.getPlatformTwoFaSettings(TenantId.SYS_TENANT_ID, true).get(); + twoFaSettings.setEnforceTwoFa(true); + TenantAdministratorsFilter enforcedUsersFilter = new TenantAdministratorsFilter(); + enforcedUsersFilter.setTenantsIds(Set.of(tenantId.getId())); + twoFaSettings.setEnforcedUsersFilter(enforcedUsersFilter); + twoFaConfigManager.savePlatformTwoFaSettings(TenantId.SYS_TENANT_ID, twoFaSettings); + + String errorMessage = getErrorMessage(doDelete("/api/2fa/account/config?providerType=SMS") + .andExpect(status().isBadRequest())); + assertThat(errorMessage).isEqualTo("At least one 2FA provider is required"); + + twoFaSettings.setEnforceTwoFa(false); + twoFaConfigManager.savePlatformTwoFaSettings(TenantId.SYS_TENANT_ID, twoFaSettings); + + doDelete("/api/2fa/account/config?providerType=SMS").andExpect(status().isOk()); + + assertThat(readResponse(doGet("/api/2fa/account/settings").andExpect(status().isOk()), AccountTwoFaSettings.class).getConfigs()) + .doesNotContainKey(TwoFaProviderType.SMS); + } + + private PlatformTwoFaSettings findTwoFaSettings() throws Exception { + return doGet("/api/2fa/settings", PlatformTwoFaSettings.class); + } + + private void saveTwoFaSettings(PlatformTwoFaSettings twoFaSettings) throws Exception { + doPost("/api/2fa/settings", twoFaSettings).andExpect(status().isOk()); + } + private TotpTwoFaProviderConfig configureTotpTwoFaProvider() throws Exception { TotpTwoFaProviderConfig totpTwoFaProviderConfig = new TotpTwoFaProviderConfig(); totpTwoFaProviderConfig.setIssuerName("tb"); @@ -441,37 +481,35 @@ public class TwoFactorAuthConfigTest extends AbstractControllerTest { twoFaSettings.setProviders(Arrays.stream(providerConfigs).collect(Collectors.toList())); twoFaSettings.setMinVerificationCodeSendPeriod(5); twoFaSettings.setTotalAllowedTimeForVerification(100); - doPost("/api/2fa/settings", twoFaSettings).andExpect(status().isOk()); + saveTwoFaSettings(twoFaSettings); } - @Test - public void testIsTwoFaEnabled() throws Exception { - configureSmsTwoFaProvider("${code}"); - SmsTwoFaAccountConfig accountConfig = new SmsTwoFaAccountConfig(); - accountConfig.setPhoneNumber("+38050505050"); - twoFaConfigManager.saveTwoFaAccountConfig(tenantId, tenantAdminUserId, accountConfig); + private TotpTwoFaAccountConfig generateTotpTwoFaAccountConfig(TotpTwoFaProviderConfig totpTwoFaProviderConfig) throws Exception { + TwoFaAccountConfig generatedTwoFaAccountConfig = readResponse(doPost("/api/2fa/account/config/generate?providerType=TOTP") + .andExpect(status().isOk()), TwoFaAccountConfig.class); + assertThat(generatedTwoFaAccountConfig).isInstanceOf(TotpTwoFaAccountConfig.class); - assertThat(twoFactorAuthService.isTwoFaEnabled(tenantId, tenantAdminUserId)).isTrue(); + assertThat(((TotpTwoFaAccountConfig) generatedTwoFaAccountConfig)).satisfies(accountConfig -> { + UriComponents otpAuthUrl = UriComponentsBuilder.fromUriString(accountConfig.getAuthUrl()).build(); + assertThat(otpAuthUrl.getScheme()).isEqualTo("otpauth"); + assertThat(otpAuthUrl.getHost()).isEqualTo("totp"); + assertThat(otpAuthUrl.getQueryParams().getFirst("issuer")).isEqualTo(totpTwoFaProviderConfig.getIssuerName()); + assertThat(otpAuthUrl.getPath()).isEqualTo("/%s:%s", totpTwoFaProviderConfig.getIssuerName(), TENANT_ADMIN_EMAIL); + assertThat(otpAuthUrl.getQueryParams().getFirst("secret")).satisfies(secretKey -> { + assertDoesNotThrow(() -> Base32.decode(secretKey)); + }); + }); + return (TotpTwoFaAccountConfig) generatedTwoFaAccountConfig; } - @Test - public void testDeleteTwoFaAccountConfig() throws Exception { - configureSmsTwoFaProvider("${code}"); - SmsTwoFaAccountConfig accountConfig = new SmsTwoFaAccountConfig(); - accountConfig.setPhoneNumber("+38050505050"); - - loginTenantAdmin(); - - twoFaConfigManager.saveTwoFaAccountConfig(tenantId, tenantAdminUserId, accountConfig); - - AccountTwoFaSettings accountTwoFaSettings = readResponse(doGet("/api/2fa/account/settings").andExpect(status().isOk()), AccountTwoFaSettings.class); - TwoFaAccountConfig savedAccountConfig = accountTwoFaSettings.getConfigs().get(TwoFaProviderType.SMS); - assertThat(savedAccountConfig).isEqualTo(accountConfig); - - doDelete("/api/2fa/account/config?providerType=SMS").andExpect(status().isOk()); + private String savePlatformTwoFaSettingsAndGetError(TwoFaProviderConfig invalidTwoFaProviderConfig) throws Exception { + PlatformTwoFaSettings twoFaSettings = new PlatformTwoFaSettings(); + twoFaSettings.setProviders(Collections.singletonList(invalidTwoFaProviderConfig)); + twoFaSettings.setMinVerificationCodeSendPeriod(5); + twoFaSettings.setTotalAllowedTimeForVerification(100); - assertThat(readResponse(doGet("/api/2fa/account/settings").andExpect(status().isOk()), AccountTwoFaSettings.class).getConfigs()) - .doesNotContainKey(TwoFaProviderType.SMS); + return getErrorMessage(doPost("/api/2fa/settings", twoFaSettings) + .andExpect(status().isBadRequest())); } } diff --git a/application/src/test/java/org/thingsboard/server/controller/TwoFactorAuthTest.java b/application/src/test/java/org/thingsboard/server/controller/TwoFactorAuthTest.java index 10d06150dc..daa213dc6c 100644 --- a/application/src/test/java/org/thingsboard/server/controller/TwoFactorAuthTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/TwoFactorAuthTest.java @@ -25,6 +25,7 @@ import org.mockito.ArgumentCaptor; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.boot.test.mock.mockito.SpyBean; +import org.springframework.web.util.UriComponentsBuilder; import org.thingsboard.rule.engine.api.SmsService; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.User; @@ -33,6 +34,8 @@ import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.audit.AuditLog; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.notification.targets.platform.AllUsersFilter; +import org.thingsboard.server.common.data.notification.targets.platform.TenantAdministratorsFilter; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.page.SortOrder; import org.thingsboard.server.common.data.page.TimePageLink; @@ -58,6 +61,7 @@ import java.time.Duration; import java.util.Arrays; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.concurrent.TimeUnit; import java.util.function.Consumer; import java.util.stream.Collectors; @@ -116,7 +120,7 @@ public class TwoFactorAuthTest extends AbstractControllerTest { public void testTwoFa_totp() throws Exception { TotpTwoFaAccountConfig totpTwoFaAccountConfig = configureTotpTwoFa(); - logInWithPreVerificationToken(username, password); + logInWithMfaToken(username, password, Authority.PRE_VERIFICATION_TOKEN); doPost("/api/auth/2fa/verification/send?providerType=TOTP") .andExpect(status().isOk()); @@ -136,7 +140,7 @@ public class TwoFactorAuthTest extends AbstractControllerTest { public void testTwoFa_sms() throws Exception { configureSmsTwoFa(); - logInWithPreVerificationToken(username, password); + logInWithMfaToken(username, password, Authority.PRE_VERIFICATION_TOKEN); doPost("/api/auth/2fa/verification/send?providerType=SMS") .andExpect(status().isOk()); @@ -160,7 +164,7 @@ public class TwoFactorAuthTest extends AbstractControllerTest { twoFaSettings.setTotalAllowedTimeForVerification(65); }); - logInWithPreVerificationToken(username, password); + logInWithMfaToken(username, password, Authority.PRE_VERIFICATION_TOKEN); await("expiration of the pre-verification token") .atLeast(Duration.ofSeconds(30).plusMillis(500)) @@ -177,7 +181,7 @@ public class TwoFactorAuthTest extends AbstractControllerTest { twoFaSettings.setMaxVerificationFailuresBeforeUserLockout(10); }); - logInWithPreVerificationToken(username, password); + logInWithMfaToken(username, password, Authority.PRE_VERIFICATION_TOKEN); Stream.generate(() -> StringUtils.randomNumeric(6)) .limit(9) @@ -206,7 +210,7 @@ public class TwoFactorAuthTest extends AbstractControllerTest { twoFaSettings.setMinVerificationCodeSendPeriod(10); }); - logInWithPreVerificationToken(username, password); + logInWithMfaToken(username, password, Authority.PRE_VERIFICATION_TOKEN); doPost("/api/auth/2fa/verification/send?providerType=TOTP") .andExpect(status().isOk()); @@ -230,7 +234,7 @@ public class TwoFactorAuthTest extends AbstractControllerTest { twoFaSettings.setVerificationCodeCheckRateLimit("3:10"); }); - logInWithPreVerificationToken(username, password); + logInWithMfaToken(username, password, Authority.PRE_VERIFICATION_TOKEN); for (int i = 0; i < 3; i++) { String incorrectVerificationCodeError = getErrorMessage(doPost("/api/auth/2fa/verification/check?providerType=TOTP&verificationCode=incorrect") @@ -258,7 +262,7 @@ public class TwoFactorAuthTest extends AbstractControllerTest { @Test public void testCheckVerificationCode_invalidVerificationCode() throws Exception { configureTotpTwoFa(); - logInWithPreVerificationToken(username, password); + logInWithMfaToken(username, password, Authority.PRE_VERIFICATION_TOKEN); for (String invalidVerificationCode : new String[]{"1234567", "ab1212", "12311 ", "oewkriwejqf"}) { String errorMessage = getErrorMessage(doPost("/api/auth/2fa/verification/check?providerType=TOTP&verificationCode=" + invalidVerificationCode) @@ -273,7 +277,7 @@ public class TwoFactorAuthTest extends AbstractControllerTest { smsTwoFaProviderConfig.setVerificationCodeLifetime(10); }); - logInWithPreVerificationToken(username, password); + logInWithMfaToken(username, password, Authority.PRE_VERIFICATION_TOKEN); ArgumentCaptor verificationCodeCaptor = ArgumentCaptor.forClass(String.class); doPost("/api/auth/2fa/verification/send?providerType=SMS").andExpect(status().isOk()); @@ -296,7 +300,7 @@ public class TwoFactorAuthTest extends AbstractControllerTest { public void testTwoFa_logLoginAction() throws Exception { TotpTwoFaAccountConfig totpTwoFaAccountConfig = configureTotpTwoFa(); - logInWithPreVerificationToken(username, password); + logInWithMfaToken(username, password, Authority.PRE_VERIFICATION_TOKEN); await("async audit log saving").during(1, TimeUnit.SECONDS); doPost("/api/auth/2fa/verification/check?providerType=TOTP&verificationCode=incorrect") @@ -334,7 +338,7 @@ public class TwoFactorAuthTest extends AbstractControllerTest { @Test public void testAuthWithoutTwoFaAccountConfig() throws ThingsboardException { configureTotpTwoFa(); - twoFaConfigManager.deleteTwoFaAccountConfig(tenantId, user.getId(), TwoFaProviderType.TOTP); + twoFaConfigManager.deleteTwoFaAccountConfig(tenantId, user, TwoFaProviderType.TOTP); assertDoesNotThrow(() -> { login(username, password); @@ -368,17 +372,17 @@ public class TwoFactorAuthTest extends AbstractControllerTest { TotpTwoFaAccountConfig totpTwoFaAccountConfig = (TotpTwoFaAccountConfig) twoFactorAuthService.generateNewAccountConfig(twoFaUser, TwoFaProviderType.TOTP); totpTwoFaAccountConfig.setUseByDefault(true); - twoFaConfigManager.saveTwoFaAccountConfig(tenantId, twoFaUser.getId(), totpTwoFaAccountConfig); + twoFaConfigManager.saveTwoFaAccountConfig(tenantId, twoFaUser, totpTwoFaAccountConfig); SmsTwoFaAccountConfig smsTwoFaAccountConfig = new SmsTwoFaAccountConfig(); smsTwoFaAccountConfig.setPhoneNumber("+38012312322"); - twoFaConfigManager.saveTwoFaAccountConfig(tenantId, twoFaUser.getId(), smsTwoFaAccountConfig); + twoFaConfigManager.saveTwoFaAccountConfig(tenantId, twoFaUser, smsTwoFaAccountConfig); EmailTwoFaAccountConfig emailTwoFaAccountConfig = new EmailTwoFaAccountConfig(); emailTwoFaAccountConfig.setEmail(twoFaUser.getEmail()); - twoFaConfigManager.saveTwoFaAccountConfig(tenantId, twoFaUser.getId(), emailTwoFaAccountConfig); + twoFaConfigManager.saveTwoFaAccountConfig(tenantId, twoFaUser, emailTwoFaAccountConfig); - logInWithPreVerificationToken(twoFaUser.getEmail(), "12345678"); + logInWithMfaToken(twoFaUser.getEmail(), "12345678", Authority.PRE_VERIFICATION_TOKEN); Map providersInfos = readResponse(doGet("/api/auth/2fa/providers").andExpect(status().isOk()), new TypeReference>() {}).stream() .collect(Collectors.toMap(TwoFactorAuthController.TwoFaProviderInfo::getType, v -> v)); @@ -395,13 +399,79 @@ public class TwoFactorAuthTest extends AbstractControllerTest { assertThat(providersInfos.get(TwoFaProviderType.EMAIL).isDefault()).isFalse(); } - private void logInWithPreVerificationToken(String username, String password) throws Exception { + @Test + public void testEnforceTwoFa() throws Exception { + TotpTwoFaProviderConfig totpTwoFaProviderConfig = new TotpTwoFaProviderConfig(); + totpTwoFaProviderConfig.setIssuerName("tb"); + + PlatformTwoFaSettings twoFaSettings = new PlatformTwoFaSettings(); + twoFaSettings.setProviders(Arrays.stream(new TwoFaProviderConfig[]{totpTwoFaProviderConfig}).collect(Collectors.toList())); + twoFaSettings.setMinVerificationCodeSendPeriod(5); + twoFaSettings.setTotalAllowedTimeForVerification(100); + twoFaSettings.setEnforceTwoFa(true); + TenantAdministratorsFilter enforcedUsersFilter = new TenantAdministratorsFilter(); + enforcedUsersFilter.setTenantsIds(Set.of(tenantId.getId())); + twoFaSettings.setEnforcedUsersFilter(enforcedUsersFilter); + twoFaSettings = twoFaConfigManager.savePlatformTwoFaSettings(TenantId.SYS_TENANT_ID, twoFaSettings); + + logInWithMfaToken(username, password, Authority.MFA_CONFIGURATION_TOKEN); + + TotpTwoFaAccountConfig totpTwoFaAccountConfig = doPost("/api/2fa/account/config/generate?providerType=" + totpTwoFaProviderConfig.getProviderType(), TotpTwoFaAccountConfig.class); + + String secret = UriComponentsBuilder.fromUriString(totpTwoFaAccountConfig.getAuthUrl()).build() + .getQueryParams().getFirst("secret"); + String verificationCode = new Totp(secret).now(); + readResponse(doPost("/api/2fa/account/config?verificationCode=" + verificationCode, totpTwoFaAccountConfig).andExpect(status().isOk()), JsonNode.class); + + JwtPair tokenPair = readResponse(doPost("/api/auth/2fa/login").andExpect(status().isOk()), JwtPair.class); + assertThat(tokenPair.getToken()).isNotEmpty(); + assertThat(tokenPair.getRefreshToken()).isNotEmpty(); + validateAndSetJwtToken(tokenPair, username); + + doGet("/api/user/" + user.getId()).andExpect(status().isOk()); + + // verifying enforced users filter + createDifferentTenant(); + doGet("/api/user/" + savedDifferentTenantUser.getId()).andExpect(status().isOk()); + } + + @Test + public void testEnforceTwoFa_sysadmin() throws Exception { + TotpTwoFaProviderConfig totpTwoFaProviderConfig = new TotpTwoFaProviderConfig(); + totpTwoFaProviderConfig.setIssuerName("tb"); + + PlatformTwoFaSettings twoFaSettings = new PlatformTwoFaSettings(); + twoFaSettings.setProviders(Arrays.stream(new TwoFaProviderConfig[]{totpTwoFaProviderConfig}).collect(Collectors.toList())); + twoFaSettings.setMinVerificationCodeSendPeriod(5); + twoFaSettings.setTotalAllowedTimeForVerification(100); + twoFaSettings.setEnforceTwoFa(true); + AllUsersFilter enforcedUsersFilter = new AllUsersFilter(); + twoFaSettings.setEnforcedUsersFilter(enforcedUsersFilter); + twoFaSettings = twoFaConfigManager.savePlatformTwoFaSettings(TenantId.SYS_TENANT_ID, twoFaSettings); + + logInWithMfaToken(SYS_ADMIN_EMAIL, SYS_ADMIN_PASSWORD, Authority.MFA_CONFIGURATION_TOKEN); + + TotpTwoFaAccountConfig totpTwoFaAccountConfig = doPost("/api/2fa/account/config/generate?providerType=" + totpTwoFaProviderConfig.getProviderType(), TotpTwoFaAccountConfig.class); + String secret = UriComponentsBuilder.fromUriString(totpTwoFaAccountConfig.getAuthUrl()).build() + .getQueryParams().getFirst("secret"); + String verificationCode = new Totp(secret).now(); + readResponse(doPost("/api/2fa/account/config?verificationCode=" + verificationCode, totpTwoFaAccountConfig).andExpect(status().isOk()), JsonNode.class); + + JwtPair tokenPair = readResponse(doPost("/api/auth/2fa/login").andExpect(status().isOk()), JwtPair.class); + assertThat(tokenPair.getToken()).isNotEmpty(); + assertThat(tokenPair.getRefreshToken()).isNotEmpty(); + validateAndSetJwtToken(tokenPair, SYS_ADMIN_EMAIL); + + doGet("/api/user/" + user.getId()).andExpect(status().isOk()); + } + + private void logInWithMfaToken(String username, String password, Authority expectedScope) throws Exception { LoginRequest loginRequest = new LoginRequest(username, password); JwtPair response = readResponse(doPost("/api/auth/login", loginRequest).andExpect(status().isOk()), JwtPair.class); assertThat(response.getToken()).isNotNull(); assertThat(response.getRefreshToken()).isNull(); - assertThat(response.getScope()).isEqualTo(Authority.PRE_VERIFICATION_TOKEN); + assertThat(response.getScope()).isEqualTo(expectedScope); this.token = response.getToken(); } @@ -418,7 +488,7 @@ public class TwoFactorAuthTest extends AbstractControllerTest { twoFaConfigManager.savePlatformTwoFaSettings(TenantId.SYS_TENANT_ID, twoFaSettings); TotpTwoFaAccountConfig totpTwoFaAccountConfig = (TotpTwoFaAccountConfig) twoFactorAuthService.generateNewAccountConfig(user, TwoFaProviderType.TOTP); - twoFaConfigManager.saveTwoFaAccountConfig(tenantId, user.getId(), totpTwoFaAccountConfig); + twoFaConfigManager.saveTwoFaAccountConfig(tenantId, user, totpTwoFaAccountConfig); return totpTwoFaAccountConfig; } @@ -436,7 +506,7 @@ public class TwoFactorAuthTest extends AbstractControllerTest { SmsTwoFaAccountConfig smsTwoFaAccountConfig = new SmsTwoFaAccountConfig(); smsTwoFaAccountConfig.setPhoneNumber("+38050505050"); - twoFaConfigManager.saveTwoFaAccountConfig(tenantId, user.getId(), smsTwoFaAccountConfig); + twoFaConfigManager.saveTwoFaAccountConfig(tenantId, user, smsTwoFaAccountConfig); return smsTwoFaAccountConfig; } diff --git a/application/src/test/java/org/thingsboard/server/controller/UserControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/UserControllerTest.java index d7aa6f5770..28d43699fd 100644 --- a/application/src/test/java/org/thingsboard/server/controller/UserControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/UserControllerTest.java @@ -116,7 +116,7 @@ public class UserControllerTest extends AbstractControllerTest { foundUser.setAdditionalInfo(savedUser.getAdditionalInfo()); Assert.assertEquals(foundUser, savedUser); - testNotifyManyEntityManyTimeMsgToEdgeServiceEntityEqAny(foundUser, foundUser, + testNotifyManyEntityManyTimeMsgToEdgeServiceEntityEqAny(user.getTenantId(), foundUser, foundUser, SYSTEM_TENANT, customerNUULId, null, SYS_ADMIN_EMAIL, ActionType.ADDED, 1, 1, 1); Mockito.reset(tbClusterService, auditLogService); @@ -155,7 +155,7 @@ public class UserControllerTest extends AbstractControllerTest { doDelete("/api/user/" + savedUser.getId().getId().toString()) .andExpect(status().isOk()); - testNotifyEntityAllOneTimeLogEntityActionEntityEqClass(foundUser, foundUser.getId(), foundUser.getId(), + testNotifyEntityAllOneTimeLogEntityActionEntityEqClass(user.getTenantId(), foundUser, foundUser.getId(), foundUser.getId(), SYSTEM_TENANT, customerNUULId, null, SYS_ADMIN_EMAIL, ActionType.DELETED, ActionType.DELETED, SYSTEM_TENANT.getId().toString()); } @@ -284,6 +284,26 @@ public class UserControllerTest extends AbstractControllerTest { ActionType.ADDED, new DataValidationException(msgError)); } + @Test + public void testShouldNotDeleteLastTenantAdmin() throws Exception { + loginSysAdmin(); + + User tenantAdmin2 = new User(); + tenantAdmin2.setAuthority(Authority.TENANT_ADMIN); + tenantAdmin2.setTenantId(tenantId); + tenantAdmin2.setEmail("tenant2@thingsboard.io"); + tenantAdmin2 = doPost("/api/user", tenantAdmin2, User.class); + + // delete second tenant admin - ok + doDelete("/api/user/" + tenantAdmin2.getId().getId().toString()) + .andExpect(status().isOk()); + + // delete last tenant admin - forbidden + doDelete("/api/user/" + tenantAdminUser.getId().getId().toString()) + .andExpect(status().isBadRequest()) + .andExpect(statusReason(containsString("At least one tenant administrator must remain!"))); + } + @Test public void testSaveUserWithInvalidEmail() throws Exception { loginSysAdmin(); @@ -394,7 +414,7 @@ public class UserControllerTest extends AbstractControllerTest { User testManyUser = new User(); testManyUser.setTenantId(tenantId); - testNotifyManyEntityManyTimeMsgToEdgeServiceEntityEqAny(testManyUser, testManyUser, + testNotifyManyEntityManyTimeMsgToEdgeServiceEntityEqAny(tenantId, testManyUser, testManyUser, SYSTEM_TENANT, customerNUULId, null, SYS_ADMIN_EMAIL, ActionType.ADDED, cntEntity, cntEntity, cntEntity); @@ -506,7 +526,7 @@ public class UserControllerTest extends AbstractControllerTest { } User testManyUser = new User(); testManyUser.setTenantId(tenantId); - testNotifyManyEntityManyTimeMsgToEdgeServiceEntityEqAny(testManyUser, testManyUser, + testNotifyManyEntityManyTimeMsgToEdgeServiceEntityEqAny(tenantId, testManyUser, testManyUser, SYSTEM_TENANT, customerNUULId, null, SYS_ADMIN_EMAIL, ActionType.DELETED, cntEntity, NUMBER_OF_USERS, cntEntity, ""); diff --git a/application/src/test/java/org/thingsboard/server/edge/AiModelEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/AiModelEdgeTest.java new file mode 100644 index 0000000000..30c8448f5b --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/edge/AiModelEdgeTest.java @@ -0,0 +1,196 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.edge; + +import com.datastax.oss.driver.api.core.uuid.Uuids; +import com.google.protobuf.AbstractMessage; +import com.google.protobuf.InvalidProtocolBufferException; +import org.junit.Assert; +import org.junit.Test; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.ai.AiModel; +import org.thingsboard.server.common.data.ai.model.chat.OpenAiChatModelConfig; +import org.thingsboard.server.common.data.ai.provider.OpenAiProviderConfig; +import org.thingsboard.server.dao.service.DaoSqlTest; +import org.thingsboard.server.gen.edge.v1.AiModelUpdateMsg; +import org.thingsboard.server.gen.edge.v1.UpdateMsgType; +import org.thingsboard.server.gen.edge.v1.UplinkMsg; +import org.thingsboard.server.gen.edge.v1.UplinkResponseMsg; + +import java.util.Optional; +import java.util.UUID; + +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@DaoSqlTest +public class AiModelEdgeTest extends AbstractEdgeTest { + + private static final String DEFAULT_AI_MODEL_NAME = "Edge Test AiModel"; + private static final String UPDATED_AI_MODEL_NAME = "Updated Edge Test AiModel"; + + @Test + public void testAiModel_create_update_delete() throws Exception { + // create AiModel + AiModel aiModel = createSimpleAiModel(DEFAULT_AI_MODEL_NAME); + + edgeImitator.expectMessageAmount(1); + AiModel savedAiModel = doPost("/api/ai/model", aiModel, AiModel.class); + Assert.assertTrue(edgeImitator.waitForMessages()); + + AbstractMessage latestMessage = edgeImitator.getLatestMessage(); + Assert.assertTrue(latestMessage instanceof AiModelUpdateMsg); + AiModelUpdateMsg aiModelUpdateMsg = (AiModelUpdateMsg) latestMessage; + Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, aiModelUpdateMsg.getMsgType()); + Assert.assertEquals(savedAiModel.getUuidId().getMostSignificantBits(), aiModelUpdateMsg.getIdMSB()); + Assert.assertEquals(savedAiModel.getUuidId().getLeastSignificantBits(), aiModelUpdateMsg.getIdLSB()); + AiModel aiModelFromMsg = JacksonUtil.fromString(aiModelUpdateMsg.getEntity(), AiModel.class, true); + Assert.assertNotNull(aiModelFromMsg); + + Assert.assertEquals(DEFAULT_AI_MODEL_NAME, aiModelFromMsg.getName()); + Assert.assertEquals(savedAiModel.getTenantId(), aiModelFromMsg.getTenantId()); + + // update AiModel + edgeImitator.expectMessageAmount(1); + savedAiModel.setName(UPDATED_AI_MODEL_NAME); + savedAiModel = doPost("/api/ai/model", savedAiModel, AiModel.class); + Assert.assertTrue(edgeImitator.waitForMessages()); + + latestMessage = edgeImitator.getLatestMessage(); + Assert.assertTrue(latestMessage instanceof AiModelUpdateMsg); + aiModelUpdateMsg = (AiModelUpdateMsg) latestMessage; + aiModelFromMsg = JacksonUtil.fromString(aiModelUpdateMsg.getEntity(), AiModel.class, true); + Assert.assertNotNull(aiModelFromMsg); + Assert.assertEquals(UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE, aiModelUpdateMsg.getMsgType()); + Assert.assertEquals(UPDATED_AI_MODEL_NAME, aiModelFromMsg.getName()); + + // delete AiModel + edgeImitator.expectMessageAmount(1); + doDelete("/api/ai/model/" + savedAiModel.getUuidId()) + .andExpect(status().isOk()); + Assert.assertTrue(edgeImitator.waitForMessages()); + + latestMessage = edgeImitator.getLatestMessage(); + Assert.assertTrue(latestMessage instanceof AiModelUpdateMsg); + aiModelUpdateMsg = (AiModelUpdateMsg) latestMessage; + Assert.assertEquals(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE, aiModelUpdateMsg.getMsgType()); + Assert.assertEquals(savedAiModel.getUuidId().getMostSignificantBits(), aiModelUpdateMsg.getIdMSB()); + Assert.assertEquals(savedAiModel.getUuidId().getLeastSignificantBits(), aiModelUpdateMsg.getIdLSB()); + } + + @Test + public void testSendAiModelToCloud() throws Exception { + AiModel aiModel = createSimpleAiModel(DEFAULT_AI_MODEL_NAME); + UUID uuid = Uuids.timeBased(); + UplinkMsg uplinkMsg = getUplinkMsg(uuid, aiModel, UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE); + + checkAiModelOnCloud(uplinkMsg, uuid, aiModel.getName()); + } + + @Test + public void testUpdateAiModelNameOnCloud() throws Exception { + AiModel aiModel = createSimpleAiModel(DEFAULT_AI_MODEL_NAME); + UUID uuid = Uuids.timeBased(); + UplinkMsg uplinkMsg = getUplinkMsg(uuid, aiModel, UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE); + + checkAiModelOnCloud(uplinkMsg, uuid, aiModel.getName()); + + aiModel.setName(UPDATED_AI_MODEL_NAME); + UplinkMsg updatedUplinkMsg = getUplinkMsg(uuid, aiModel, UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE); + + checkAiModelOnCloud(updatedUplinkMsg, uuid, aiModel.getName()); + } + + @Test + public void testAiModelToCloudWithNameThatAlreadyExistsOnCloud() throws Exception { + AiModel aiModel = createSimpleAiModel(DEFAULT_AI_MODEL_NAME); + + edgeImitator.expectMessageAmount(1); + AiModel savedAiModel = doPost("/api/ai/model", aiModel, AiModel.class); + Assert.assertTrue(edgeImitator.waitForMessages()); + + UUID uuid = Uuids.timeBased(); + + UplinkMsg uplinkMsg = getUplinkMsg(uuid, aiModel, UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE); + + edgeImitator.expectResponsesAmount(1); + edgeImitator.expectMessageAmount(1); + edgeImitator.sendUplinkMsg(uplinkMsg); + + Assert.assertTrue(edgeImitator.waitForResponses()); + Assert.assertTrue(edgeImitator.waitForMessages()); + + Optional aiModelUpdateMsgOpt = edgeImitator.findMessageByType(AiModelUpdateMsg.class); + Assert.assertTrue(aiModelUpdateMsgOpt.isPresent()); + AiModelUpdateMsg latestAiModelUpdateMsg = aiModelUpdateMsgOpt.get(); + AiModel aiModelFromMsg = JacksonUtil.fromString(latestAiModelUpdateMsg.getEntity(), AiModel.class, true); + Assert.assertNotNull(aiModelFromMsg); + Assert.assertNotEquals(DEFAULT_AI_MODEL_NAME, aiModelFromMsg.getName()); + + Assert.assertNotEquals(savedAiModel.getUuidId(), uuid); + + AiModel aiModelFromCloud = doGet("/api/ai/model/" + uuid, AiModel.class); + Assert.assertNotNull(aiModelFromCloud); + Assert.assertNotEquals(DEFAULT_AI_MODEL_NAME, aiModelFromCloud.getName()); + } + + private AiModel createSimpleAiModel(String name) { + AiModel aiModel = new AiModel(); + aiModel.setTenantId(tenantId); + aiModel.setName(name); + aiModel.setConfiguration(OpenAiChatModelConfig.builder() + .providerConfig(new OpenAiProviderConfig(null, "test-api-key")) + .modelId("gpt-4o") + .temperature(0.5) + .topP(0.3) + .frequencyPenalty(0.1) + .presencePenalty(0.2) + .maxOutputTokens(1000) + .timeoutSeconds(60) + .maxRetries(2) + .build()); + return aiModel; + } + + private UplinkMsg getUplinkMsg(UUID uuid, AiModel aiModel, UpdateMsgType updateMsgType) throws InvalidProtocolBufferException { + UplinkMsg.Builder uplinkMsgBuilder = UplinkMsg.newBuilder(); + AiModelUpdateMsg.Builder aiModelUpdateMsgBuilder = AiModelUpdateMsg.newBuilder(); + aiModelUpdateMsgBuilder.setIdMSB(uuid.getMostSignificantBits()); + aiModelUpdateMsgBuilder.setIdLSB(uuid.getLeastSignificantBits()); + aiModelUpdateMsgBuilder.setEntity(JacksonUtil.toString(aiModel)); + aiModelUpdateMsgBuilder.setMsgType(updateMsgType); + testAutoGeneratedCodeByProtobuf(aiModelUpdateMsgBuilder); + uplinkMsgBuilder.addAiModelUpdateMsg(aiModelUpdateMsgBuilder.build()); + + testAutoGeneratedCodeByProtobuf(uplinkMsgBuilder); + + return uplinkMsgBuilder.build(); + } + + private void checkAiModelOnCloud(UplinkMsg uplinkMsg, UUID uuid, String resourceTitle) throws Exception { + edgeImitator.expectResponsesAmount(1); + edgeImitator.sendUplinkMsg(uplinkMsg); + + Assert.assertTrue(edgeImitator.waitForResponses()); + + UplinkResponseMsg latestResponseMsg = edgeImitator.getLatestResponseMsg(); + Assert.assertTrue(latestResponseMsg.getSuccess()); + + AiModel aiModel = doGet("/api/ai/model/" + uuid, AiModel.class); + Assert.assertNotNull(aiModel); + Assert.assertEquals(resourceTitle, aiModel.getName()); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/edge/imitator/EdgeImitator.java b/application/src/test/java/org/thingsboard/server/edge/imitator/EdgeImitator.java index 16d10d9b6e..4594435626 100644 --- a/application/src/test/java/org/thingsboard/server/edge/imitator/EdgeImitator.java +++ b/application/src/test/java/org/thingsboard/server/edge/imitator/EdgeImitator.java @@ -29,6 +29,7 @@ import org.thingsboard.edge.rpc.EdgeGrpcClient; import org.thingsboard.edge.rpc.EdgeRpcClient; import org.thingsboard.server.controller.AbstractWebTest; import org.thingsboard.server.gen.edge.v1.AdminSettingsUpdateMsg; +import org.thingsboard.server.gen.edge.v1.AiModelUpdateMsg; import org.thingsboard.server.gen.edge.v1.AlarmCommentUpdateMsg; import org.thingsboard.server.gen.edge.v1.AlarmUpdateMsg; import org.thingsboard.server.gen.edge.v1.AssetProfileUpdateMsg; @@ -358,6 +359,11 @@ public class EdgeImitator { result.add(saveDownlinkMsg(calculatedFieldUpdateMsg)); } } + if (downlinkMsg.getAiModelUpdateMsgCount() > 0) { + for (AiModelUpdateMsg aiModelUpdateMsg : downlinkMsg.getAiModelUpdateMsgList()) { + result.add(saveDownlinkMsg(aiModelUpdateMsg)); + } + } if (downlinkMsg.hasEdgeConfiguration()) { result.add(saveDownlinkMsg(downlinkMsg.getEdgeConfiguration())); } diff --git a/application/src/test/java/org/thingsboard/server/rules/lifecycle/AbstractRuleEngineLifecycleIntegrationTest.java b/application/src/test/java/org/thingsboard/server/rules/lifecycle/AbstractRuleEngineLifecycleIntegrationTest.java index 3e29f212f0..fb99d6ad0e 100644 --- a/application/src/test/java/org/thingsboard/server/rules/lifecycle/AbstractRuleEngineLifecycleIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/rules/lifecycle/AbstractRuleEngineLifecycleIntegrationTest.java @@ -118,7 +118,7 @@ public abstract class AbstractRuleEngineLifecycleIntegrationTest extends Abstrac .pollInterval(10, MILLISECONDS) .atMost(TIMEOUT, TimeUnit.SECONDS) .until(() -> { - List debugEvents = getEvents(tenantId, ruleChainFinal.getFirstRuleNodeId(), EventType.LC_EVENT.getOldName(), 1000) + List debugEvents = getEvents(tenantId, ruleChainFinal.getFirstRuleNodeId(), EventType.LC_EVENT, 1000) .getData().stream().filter(e -> { var body = e.getBody(); return body.has("event") && body.get("event").asText().equals("STARTED") diff --git a/application/src/test/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateServiceTest.java b/application/src/test/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateServiceTest.java index 20f8aaf881..969f6de9f5 100644 --- a/application/src/test/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateServiceTest.java @@ -20,9 +20,12 @@ import org.junit.Before; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; import org.thingsboard.server.common.data.ApiUsageRecordKey; +import org.thingsboard.server.common.data.ApiUsageState; import org.thingsboard.server.common.data.ApiUsageStateValue; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.TenantProfile; +import org.thingsboard.server.common.data.id.ApiUsageStateId; +import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; import org.thingsboard.server.common.data.tenant.profile.TenantProfileData; @@ -33,8 +36,17 @@ import org.thingsboard.server.dao.usagerecord.ApiUsageStateService; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.queue.common.TbProtoQueueMsg; +import java.lang.reflect.Field; +import java.time.LocalDate; +import java.util.HashMap; +import java.util.Map; import java.util.UUID; +import java.util.concurrent.TimeUnit; +import static java.time.ZoneOffset.UTC; +import static java.time.temporal.ChronoField.DAY_OF_MONTH; +import static java.time.temporal.ChronoUnit.MONTHS; +import static org.assertj.core.api.Assertions.assertThat; import static org.junit.Assert.assertEquals; @DaoSqlTest @@ -48,6 +60,7 @@ public class DefaultTbApiUsageStateServiceTest extends AbstractControllerTest { private TenantId tenantId; private Tenant savedTenant; + private TenantProfile savedTenantProfile; private static final int MAX_ENABLE_VALUE = 5000; private static final long VALUE_WARNING = 4500L; @@ -59,7 +72,7 @@ public class DefaultTbApiUsageStateServiceTest extends AbstractControllerTest { loginSysAdmin(); TenantProfile tenantProfile = createTenantProfile(); - TenantProfile savedTenantProfile = doPost("/api/tenantProfile", tenantProfile, TenantProfile.class); + savedTenantProfile = doPost("/api/tenantProfile", tenantProfile, TenantProfile.class); Assert.assertNotNull(savedTenantProfile); Tenant tenant = new Tenant(); @@ -109,6 +122,41 @@ public class DefaultTbApiUsageStateServiceTest extends AbstractControllerTest { assertEquals(ApiUsageStateValue.DISABLED, apiUsageStateService.findTenantApiUsageState(tenantId).getDbStorageState()); } + @Test + public void checkStartOfNextCycle_setsNextCycleToNextMonth() throws Exception { + ApiUsageState apiUsageState = new ApiUsageState(new ApiUsageStateId(UUID.randomUUID())); + apiUsageState.setDbStorageState(ApiUsageStateValue.ENABLED); + apiUsageState.setAlarmExecState(ApiUsageStateValue.ENABLED); + apiUsageState.setSmsExecState(ApiUsageStateValue.ENABLED); + apiUsageState.setTbelExecState(ApiUsageStateValue.ENABLED); + apiUsageState.setReExecState(ApiUsageStateValue.ENABLED); + apiUsageState.setTransportState(ApiUsageStateValue.ENABLED); + apiUsageState.setEmailExecState(ApiUsageStateValue.ENABLED); + apiUsageState.setJsExecState(ApiUsageStateValue.ENABLED); + apiUsageState.setTenantId(tenantId); + apiUsageState.setEntityId(tenantId); + + long now = System.currentTimeMillis(); + long currentCycleTs = now - TimeUnit.DAYS.toMillis(30); + long nextCycleTs = now - TimeUnit.MINUTES.toMillis(5); // < 1h ago + TenantApiUsageState tenantApiUsageState = new TenantApiUsageState(savedTenantProfile, apiUsageState); + tenantApiUsageState.setCycles(currentCycleTs, nextCycleTs); + Map map = new HashMap<>(); + map.put(tenantId, tenantApiUsageState); + + Field fieldToSet = DefaultTbApiUsageStateService.class.getDeclaredField("myUsageStates"); + fieldToSet.setAccessible(true); + fieldToSet.set(service, map); + + service.checkStartOfNextCycle(); + + long firstOfNextMonth = LocalDate.now() + .with((temporal) -> temporal.with(DAY_OF_MONTH, 1) + .plus(1, MONTHS)) + .atStartOfDay(UTC).toInstant().toEpochMilli(); + assertThat(tenantApiUsageState.getNextCycleTs()).isEqualTo(firstOfNextMonth); + } + private TenantProfile createTenantProfile() { TenantProfile tenantProfile = new TenantProfile(); tenantProfile.setName("Tenant Profile"); diff --git a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldStateTest.java b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldStateTest.java new file mode 100644 index 0000000000..cc2d9c8437 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldStateTest.java @@ -0,0 +1,489 @@ +/** + * Copyright © 2016-2025 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.cf.ctx.state; + +import com.google.common.util.concurrent.Futures; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.actors.ActorSystemContext; +import org.thingsboard.server.common.data.cf.CalculatedField; +import org.thingsboard.server.common.data.cf.CalculatedFieldType; +import org.thingsboard.server.common.data.cf.configuration.CalculatedFieldConfiguration; +import org.thingsboard.server.common.data.cf.configuration.Output; +import org.thingsboard.server.common.data.cf.configuration.OutputType; +import org.thingsboard.server.common.data.cf.configuration.RelationPathQueryDynamicSourceConfiguration; +import org.thingsboard.server.common.data.cf.configuration.geofencing.EntityCoordinates; +import org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingCalculatedFieldConfiguration; +import org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingReportStrategy; +import org.thingsboard.server.common.data.cf.configuration.geofencing.ZoneGroupConfiguration; +import org.thingsboard.server.common.data.id.AssetId; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry; +import org.thingsboard.server.common.data.kv.DoubleDataEntry; +import org.thingsboard.server.common.data.kv.JsonDataEntry; +import org.thingsboard.server.common.data.relation.EntityRelation; +import org.thingsboard.server.common.data.relation.EntitySearchDirection; +import org.thingsboard.server.common.data.relation.RelationPathLevel; +import org.thingsboard.server.dao.relation.RelationService; +import org.thingsboard.server.dao.usagerecord.ApiLimitService; +import org.thingsboard.server.service.cf.TelemetryCalculatedFieldResult; +import org.thingsboard.server.service.cf.ctx.state.geofencing.GeofencingArgumentEntry; +import org.thingsboard.server.service.cf.ctx.state.geofencing.GeofencingCalculatedFieldState; + +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ExecutionException; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.thingsboard.server.common.data.cf.configuration.geofencing.EntityCoordinates.ENTITY_ID_LATITUDE_ARGUMENT_KEY; +import static org.thingsboard.server.common.data.cf.configuration.geofencing.EntityCoordinates.ENTITY_ID_LONGITUDE_ARGUMENT_KEY; +import static org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingReportStrategy.REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS; + +@ExtendWith(MockitoExtension.class) +public class GeofencingCalculatedFieldStateTest { + + private final TenantId TENANT_ID = TenantId.fromUUID(UUID.fromString("8f83eeca-b5cd-4955-9241-09d1393768c6")); + private final DeviceId DEVICE_ID = new DeviceId(UUID.fromString("688b529d-cfbe-4430-91c5-60b4f4e5d3cf")); + private final AssetId ZONE_1_ID = new AssetId(UUID.fromString("c0e3031c-7df1-45e4-9590-cfd621a4d714")); + private final AssetId ZONE_2_ID = new AssetId(UUID.fromString("e7da6200-2096-4038-a343-ade9ea4fa3e4")); + + private final SingleValueArgumentEntry latitudeArgEntry = new SingleValueArgumentEntry(System.currentTimeMillis() - 10, new DoubleDataEntry("latitude", 50.4730), 145L); + private final SingleValueArgumentEntry longitudeArgEntry = new SingleValueArgumentEntry(System.currentTimeMillis() - 6, new DoubleDataEntry("longitude", 30.5050), 165L); + + private final JsonDataEntry allowedZoneDataEntry = new JsonDataEntry("zone", "[[50.472000, 30.504000], [50.472000, 30.506000], [50.474000, 30.506000], [50.474000, 30.504000]]"); + private final BaseAttributeKvEntry allowedZoneAttributeKvEntry = new BaseAttributeKvEntry(allowedZoneDataEntry, System.currentTimeMillis(), 0L); + private final GeofencingArgumentEntry geofencingAllowedZoneArgEntry = new GeofencingArgumentEntry(Map.of(ZONE_1_ID, allowedZoneAttributeKvEntry)); + + private final JsonDataEntry restrictedZoneDataEntry = new JsonDataEntry("zone", "[[50.475000, 30.510000], [50.475000, 30.512000], [50.477000, 30.512000], [50.477000, 30.510000]]"); + private final BaseAttributeKvEntry restrictedZoneAttributeKvEntry = new BaseAttributeKvEntry(restrictedZoneDataEntry, System.currentTimeMillis(), 0L); + private final GeofencingArgumentEntry geofencingRestrictedZoneArgEntry = new GeofencingArgumentEntry(Map.of(ZONE_2_ID, restrictedZoneAttributeKvEntry)); + + + private GeofencingCalculatedFieldState state; + private CalculatedFieldCtx ctx; + + @Mock + private ApiLimitService apiLimitService; + @Mock + private RelationService relationService; + @InjectMocks + private ActorSystemContext systemContext; + + @BeforeEach + void setUp() { + when(apiLimitService.getLimit(any(), any())).thenReturn(1000L); + ctx = new CalculatedFieldCtx(getCalculatedField(), systemContext); + ctx.init(); + state = new GeofencingCalculatedFieldState(ctx.getEntityId()); + state.setCtx(ctx, null); + state.init(false); + } + + @Test + void testType() { + assertThat(state.getType()).isEqualTo(CalculatedFieldType.GEOFENCING); + } + + @Test + void testUpdateState() { + state.arguments = new HashMap<>(Map.of( + ENTITY_ID_LATITUDE_ARGUMENT_KEY, latitudeArgEntry, + ENTITY_ID_LONGITUDE_ARGUMENT_KEY, longitudeArgEntry + )); + + Map newArgs = Map.of("allowedZones", geofencingAllowedZoneArgEntry); + boolean stateUpdated = !state.update(newArgs, ctx).isEmpty(); + + assertThat(stateUpdated).isTrue(); + assertThat(state.getArguments()).containsExactlyInAnyOrderEntriesOf( + Map.of( + ENTITY_ID_LATITUDE_ARGUMENT_KEY, latitudeArgEntry, + ENTITY_ID_LONGITUDE_ARGUMENT_KEY, longitudeArgEntry, + "allowedZones", geofencingAllowedZoneArgEntry + ) + ); + } + + @Test + void testUpdateStateWithInvalidArgumentTypeForLatitudeArgument() { + assertThatThrownBy(() -> state.update(Map.of(ENTITY_ID_LATITUDE_ARGUMENT_KEY, geofencingAllowedZoneArgEntry), ctx)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Unsupported argument entry type for latitude argument: GEOFENCING. Only SINGLE_VALUE type is allowed."); + } + + @Test + void testUpdateStateWithInvalidArgumentTypeForLongitudeArgument() { + assertThatThrownBy(() -> state.update(Map.of(ENTITY_ID_LONGITUDE_ARGUMENT_KEY, geofencingAllowedZoneArgEntry), ctx)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Unsupported argument entry type for longitude argument: GEOFENCING. Only SINGLE_VALUE type is allowed."); + } + + @Test + void testUpdateStateWithInvalidArgumentTypeForGeofencingArgument() { + assertThatThrownBy(() -> state.update(Map.of("someArgumentName", latitudeArgEntry), ctx)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Unsupported argument entry type for someArgumentName argument: SINGLE_VALUE. Only GEOFENCING type is allowed."); + } + + @Test + void testUpdateStateWhenUpdateExistingSingleValueArgumentEntry() { + state.arguments = new HashMap<>(Map.of(ENTITY_ID_LATITUDE_ARGUMENT_KEY, latitudeArgEntry)); + + SingleValueArgumentEntry newArgEntry = new SingleValueArgumentEntry(System.currentTimeMillis(), new DoubleDataEntry("latitude", 50.4760), 190L); + Map newArgs = Map.of("latitude", newArgEntry); + boolean stateUpdated = !state.update(newArgs, ctx).isEmpty(); + + assertThat(stateUpdated).isTrue(); + assertThat(state.getArguments()).isEqualTo(newArgs); + } + + @Test + void testUpdateStateWhenUpdateExistingGeofencingValueArgumentEntryWithTheSameValue() { + state.arguments = new HashMap<>(Map.of("allowedZones", geofencingAllowedZoneArgEntry)); + + Map newArgs = Map.of("allowedZones", geofencingAllowedZoneArgEntry); + + boolean stateUpdated = !state.update(newArgs, ctx).isEmpty(); + + assertThat(stateUpdated).isFalse(); + assertThat(state.getArguments()).isEqualTo(newArgs); + } + + @Test + void testUpdateStateWhenUpdateExistingSingleValueArgumentEntryWithValueOfAnotherType() { + state.arguments = new HashMap<>(Map.of(ENTITY_ID_LATITUDE_ARGUMENT_KEY, latitudeArgEntry)); + + assertThatThrownBy(() -> state.update(Map.of(ENTITY_ID_LATITUDE_ARGUMENT_KEY, geofencingAllowedZoneArgEntry), ctx)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Unsupported argument entry type for single value argument entry: GEOFENCING"); + } + + + @Test + void testUpdateStateWhenUpdateExistingGeofencingValueArgumentEntryWithValueOfAnotherType() { + state.arguments = new HashMap<>(Map.of("allowedZones", geofencingAllowedZoneArgEntry)); + + assertThatThrownBy(() -> state.update(Map.of("allowedZones", latitudeArgEntry), ctx)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Unsupported argument entry type for geofencing argument entry: SINGLE_VALUE"); + } + + @Test + void testIsReadyWhenNotAllArgPresent() { + assertThat(state.isReady()).isFalse(); + assertThat(state.getReadinessStatus().errorMsg()).contains(state.getRequiredArguments()); + } + + @Test + void testIsReadyWhenAllArgPresent() { + state.update(Map.of( + ENTITY_ID_LATITUDE_ARGUMENT_KEY, latitudeArgEntry, + ENTITY_ID_LONGITUDE_ARGUMENT_KEY, longitudeArgEntry, + "allowedZones", geofencingAllowedZoneArgEntry, + "restrictedZones", geofencingRestrictedZoneArgEntry + ), ctx); + assertThat(state.isReady()).isTrue(); + assertThat(state.getReadinessStatus().errorMsg()).isNull(); + } + + @Test + void testIsReadyWhenEmptyEntryPresents() { + state.update(Map.of( + ENTITY_ID_LATITUDE_ARGUMENT_KEY, latitudeArgEntry, + ENTITY_ID_LONGITUDE_ARGUMENT_KEY, longitudeArgEntry, + "allowedZones", geofencingAllowedZoneArgEntry, + "restrictedZones", new GeofencingArgumentEntry() + ), ctx); + assertThat(state.isReady()).isFalse(); + assertThat(state.getReadinessStatus().errorMsg()).contains("restrictedZones"); + } + + @Test + void testPerformCalculation() throws ExecutionException, InterruptedException { + state.arguments = new HashMap<>(Map.of( + ENTITY_ID_LATITUDE_ARGUMENT_KEY, latitudeArgEntry, + ENTITY_ID_LONGITUDE_ARGUMENT_KEY, longitudeArgEntry, + "allowedZones", geofencingAllowedZoneArgEntry, + "restrictedZones", geofencingRestrictedZoneArgEntry + )); + + Output output = ctx.getOutput(); + var configuration = (GeofencingCalculatedFieldConfiguration) ctx.getCalculatedField().getConfiguration(); + + when(relationService.saveRelationAsync(any(), any())).thenReturn(Futures.immediateFuture(true)); + when(relationService.deleteRelationAsync(any(), any())).thenReturn(Futures.immediateFuture(true)); + + TelemetryCalculatedFieldResult result = performCalculation(); + + assertThat(result).isNotNull(); + assertThat(result.getType()).isEqualTo(output.getType()); + assertThat(result.getScope()).isEqualTo(output.getScope()); + assertThat(result.getResult()).isEqualTo( + JacksonUtil.newObjectNode() + .put("allowedZonesEvent", "ENTERED") + .put("allowedZonesStatus", "INSIDE") + .put("restrictedZonesStatus", "OUTSIDE") + ); + + SingleValueArgumentEntry newLatitude = new SingleValueArgumentEntry(System.currentTimeMillis(), new DoubleDataEntry("latitude", 50.4760), 146L); + SingleValueArgumentEntry newLongitude = new SingleValueArgumentEntry(System.currentTimeMillis(), new DoubleDataEntry("longitude", 30.5110), 166L); + + // move the device to new coordinates → leaves allowed, enters restricted + state.update(Map.of(ENTITY_ID_LATITUDE_ARGUMENT_KEY, newLatitude, ENTITY_ID_LONGITUDE_ARGUMENT_KEY, newLongitude), ctx); + + TelemetryCalculatedFieldResult result2 = performCalculation(); + + assertThat(result2).isNotNull(); + assertThat(result2.getType()).isEqualTo(output.getType()); + assertThat(result2.getScope()).isEqualTo(output.getScope()); + assertThat(result2.getResult().get("values")).isEqualTo( + JacksonUtil.newObjectNode() + .put("allowedZonesEvent", "LEFT") + .put("allowedZonesStatus", "OUTSIDE") + .put("restrictedZonesEvent", "ENTERED") + .put("restrictedZonesStatus", "INSIDE") + ); + + // Check relations are created and deleted correctly for both iterations. + ArgumentCaptor saveCaptor = ArgumentCaptor.forClass(EntityRelation.class); + verify(relationService, times(2)).saveRelationAsync(eq(ctx.getTenantId()), saveCaptor.capture()); + List saveValues = saveCaptor.getAllValues(); + assertThat(saveValues).hasSize(2); + + EntityRelation relationFromFirstIteration = saveValues.get(0); + assertThat(relationFromFirstIteration.getTo()).isEqualTo(ctx.getEntityId()); + assertThat(relationFromFirstIteration.getFrom()).isEqualTo(ZONE_1_ID); + assertThat(relationFromFirstIteration.getType()).isEqualTo("CurrentZone"); + + EntityRelation relationFromSecondIteration = saveValues.get(1); + assertThat(relationFromSecondIteration.getTo()).isEqualTo(ctx.getEntityId()); + assertThat(relationFromSecondIteration.getFrom()).isEqualTo(ZONE_2_ID); + assertThat(relationFromSecondIteration.getType()).isEqualTo("CurrentZone"); + + ArgumentCaptor deleteCaptor = ArgumentCaptor.forClass(EntityRelation.class); + verify(relationService).deleteRelationAsync(eq(ctx.getTenantId()), deleteCaptor.capture()); + EntityRelation leftRelation = deleteCaptor.getValue(); + assertThat(leftRelation.getFrom()).isEqualTo(ZONE_1_ID); + assertThat(leftRelation.getTo()).isEqualTo(ctx.getEntityId()); + } + + @Test + void testPerformCalculationWithOnlyTransitionEventsReportingStrategy() throws ExecutionException, InterruptedException { + state.arguments = new HashMap<>(Map.of( + ENTITY_ID_LATITUDE_ARGUMENT_KEY, latitudeArgEntry, + ENTITY_ID_LONGITUDE_ARGUMENT_KEY, longitudeArgEntry, + "allowedZones", geofencingAllowedZoneArgEntry, + "restrictedZones", geofencingRestrictedZoneArgEntry + )); + + Output output = ctx.getOutput(); + + var calculatedFieldConfig = getCalculatedFieldConfig(GeofencingReportStrategy.REPORT_TRANSITION_EVENTS_ONLY); + + ctx.setCalculatedField(getCalculatedField(calculatedFieldConfig)); + ctx.init(); + + var configuration = (GeofencingCalculatedFieldConfiguration) ctx.getCalculatedField().getConfiguration(); + + when(relationService.saveRelationAsync(any(), any())).thenReturn(Futures.immediateFuture(true)); + when(relationService.deleteRelationAsync(any(), any())).thenReturn(Futures.immediateFuture(true)); + + TelemetryCalculatedFieldResult result = performCalculation(); + + assertThat(result).isNotNull(); + assertThat(result.getType()).isEqualTo(output.getType()); + assertThat(result.getScope()).isEqualTo(output.getScope()); + assertThat(result.getResult()).isEqualTo( + JacksonUtil.newObjectNode().put("allowedZonesEvent", "ENTERED") + ); + + SingleValueArgumentEntry newLatitude = new SingleValueArgumentEntry(System.currentTimeMillis(), new DoubleDataEntry("latitude", 50.4760), 146L); + SingleValueArgumentEntry newLongitude = new SingleValueArgumentEntry(System.currentTimeMillis(), new DoubleDataEntry("longitude", 30.5110), 166L); + + // move the device to new coordinates → leaves allowed, enters restricted + state.update(Map.of(ENTITY_ID_LATITUDE_ARGUMENT_KEY, newLatitude, ENTITY_ID_LONGITUDE_ARGUMENT_KEY, newLongitude), ctx); + + TelemetryCalculatedFieldResult result2 = performCalculation(); + + assertThat(result2).isNotNull(); + assertThat(result2.getType()).isEqualTo(output.getType()); + assertThat(result2.getScope()).isEqualTo(output.getScope()); + assertThat(result2.getResult().get("values")).isEqualTo( + JacksonUtil.newObjectNode() + .put("allowedZonesEvent", "LEFT") + .put("restrictedZonesEvent", "ENTERED") + ); + + // Check relations are created and deleted correctly for both iterations. + ArgumentCaptor saveCaptor = ArgumentCaptor.forClass(EntityRelation.class); + verify(relationService, times(2)).saveRelationAsync(eq(ctx.getTenantId()), saveCaptor.capture()); + List saveValues = saveCaptor.getAllValues(); + assertThat(saveValues).hasSize(2); + + EntityRelation relationFromFirstIteration = saveValues.get(0); + assertThat(relationFromFirstIteration.getTo()).isEqualTo(ctx.getEntityId()); + assertThat(relationFromFirstIteration.getFrom()).isEqualTo(ZONE_1_ID); + assertThat(relationFromFirstIteration.getType()).isEqualTo("CurrentZone"); + + EntityRelation relationFromSecondIteration = saveValues.get(1); + assertThat(relationFromSecondIteration.getTo()).isEqualTo(ctx.getEntityId()); + assertThat(relationFromSecondIteration.getFrom()).isEqualTo(ZONE_2_ID); + assertThat(relationFromSecondIteration.getType()).isEqualTo("CurrentZone"); + + ArgumentCaptor deleteCaptor = ArgumentCaptor.forClass(EntityRelation.class); + verify(relationService).deleteRelationAsync(eq(ctx.getTenantId()), deleteCaptor.capture()); + EntityRelation leftRelation = deleteCaptor.getValue(); + assertThat(leftRelation.getFrom()).isEqualTo(ZONE_1_ID); + assertThat(leftRelation.getTo()).isEqualTo(ctx.getEntityId()); + } + + @Test + void testPerformCalculationWithOnlyPresenceStatusReportingStrategy() throws ExecutionException, InterruptedException { + state.arguments = new HashMap<>(Map.of( + ENTITY_ID_LATITUDE_ARGUMENT_KEY, latitudeArgEntry, + ENTITY_ID_LONGITUDE_ARGUMENT_KEY, longitudeArgEntry, + "allowedZones", geofencingAllowedZoneArgEntry, + "restrictedZones", geofencingRestrictedZoneArgEntry + )); + + Output output = ctx.getOutput(); + + var calculatedFieldConfig = getCalculatedFieldConfig(GeofencingReportStrategy.REPORT_PRESENCE_STATUS_ONLY); + + ctx.setCalculatedField(getCalculatedField(calculatedFieldConfig)); + ctx.init(); + + var configuration = (GeofencingCalculatedFieldConfiguration) ctx.getCalculatedField().getConfiguration(); + + when(relationService.saveRelationAsync(any(), any())).thenReturn(Futures.immediateFuture(true)); + when(relationService.deleteRelationAsync(any(), any())).thenReturn(Futures.immediateFuture(true)); + + TelemetryCalculatedFieldResult result = performCalculation(); + + assertThat(result).isNotNull(); + assertThat(result.getType()).isEqualTo(output.getType()); + assertThat(result.getScope()).isEqualTo(output.getScope()); + assertThat(result.getResult()).isEqualTo( + JacksonUtil.newObjectNode() + .put("allowedZonesStatus", "INSIDE") + .put("restrictedZonesStatus", "OUTSIDE") + ); + + SingleValueArgumentEntry newLatitude = new SingleValueArgumentEntry(System.currentTimeMillis(), new DoubleDataEntry("latitude", 50.4760), 146L); + SingleValueArgumentEntry newLongitude = new SingleValueArgumentEntry(System.currentTimeMillis(), new DoubleDataEntry("longitude", 30.5110), 166L); + + // move the device to new coordinates → leaves allowed, enters restricted + state.update(Map.of(ENTITY_ID_LATITUDE_ARGUMENT_KEY, newLatitude, ENTITY_ID_LONGITUDE_ARGUMENT_KEY, newLongitude), ctx); + + TelemetryCalculatedFieldResult result2 = performCalculation(); + + assertThat(result2).isNotNull(); + assertThat(result2.getType()).isEqualTo(output.getType()); + assertThat(result2.getScope()).isEqualTo(output.getScope()); + assertThat(result2.getResult().get("values")).isEqualTo( + JacksonUtil.newObjectNode() + .put("allowedZonesStatus", "OUTSIDE") + .put("restrictedZonesStatus", "INSIDE") + ); + + // Check relations are created and deleted correctly for both iterations. + ArgumentCaptor saveCaptor = ArgumentCaptor.forClass(EntityRelation.class); + verify(relationService, times(2)).saveRelationAsync(eq(ctx.getTenantId()), saveCaptor.capture()); + List saveValues = saveCaptor.getAllValues(); + assertThat(saveValues).hasSize(2); + + EntityRelation relationFromFirstIteration = saveValues.get(0); + assertThat(relationFromFirstIteration.getTo()).isEqualTo(ctx.getEntityId()); + assertThat(relationFromFirstIteration.getFrom()).isEqualTo(ZONE_1_ID); + assertThat(relationFromFirstIteration.getType()).isEqualTo("CurrentZone"); + + EntityRelation relationFromSecondIteration = saveValues.get(1); + assertThat(relationFromSecondIteration.getTo()).isEqualTo(ctx.getEntityId()); + assertThat(relationFromSecondIteration.getFrom()).isEqualTo(ZONE_2_ID); + assertThat(relationFromSecondIteration.getType()).isEqualTo("CurrentZone"); + + ArgumentCaptor deleteCaptor = ArgumentCaptor.forClass(EntityRelation.class); + verify(relationService).deleteRelationAsync(eq(ctx.getTenantId()), deleteCaptor.capture()); + EntityRelation leftRelation = deleteCaptor.getValue(); + assertThat(leftRelation.getFrom()).isEqualTo(ZONE_1_ID); + assertThat(leftRelation.getTo()).isEqualTo(ctx.getEntityId()); + } + + private CalculatedField getCalculatedField() { + return getCalculatedField(getCalculatedFieldConfig(REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS)); + } + + private CalculatedField getCalculatedField(CalculatedFieldConfiguration configuration) { + CalculatedField calculatedField = new CalculatedField(); + calculatedField.setTenantId(TENANT_ID); + calculatedField.setEntityId(DEVICE_ID); + calculatedField.setType(CalculatedFieldType.GEOFENCING); + calculatedField.setName("Test Geofencing Calculated Field"); + calculatedField.setConfigurationVersion(1); + calculatedField.setConfiguration(configuration); + calculatedField.setVersion(1L); + return calculatedField; + } + + private CalculatedFieldConfiguration getCalculatedFieldConfig(GeofencingReportStrategy reportStrategy) { + var config = new GeofencingCalculatedFieldConfiguration(); + + EntityCoordinates entityCoordinates = new EntityCoordinates("latitude", "longitude"); + config.setEntityCoordinates(entityCoordinates); + + ZoneGroupConfiguration allowedZonesGroup = new ZoneGroupConfiguration("zone", reportStrategy, true); + var allowedZoneDynamicSourceConfiguration = new RelationPathQueryDynamicSourceConfiguration(); + allowedZoneDynamicSourceConfiguration.setLevels(List.of(new RelationPathLevel(EntitySearchDirection.TO, "AllowedZone"))); + allowedZonesGroup.setRefDynamicSourceConfiguration(allowedZoneDynamicSourceConfiguration); + allowedZonesGroup.setRelationType("CurrentZone"); + allowedZonesGroup.setDirection(EntitySearchDirection.TO); + + ZoneGroupConfiguration restrictedZonesGroup = new ZoneGroupConfiguration("zone", reportStrategy, true); + var restrictedZoneDynamicSourceConfiguration = new RelationPathQueryDynamicSourceConfiguration(); + restrictedZoneDynamicSourceConfiguration.setLevels(List.of(new RelationPathLevel(EntitySearchDirection.TO, "RestrictedZone"))); + restrictedZonesGroup.setRefDynamicSourceConfiguration(restrictedZoneDynamicSourceConfiguration); + restrictedZonesGroup.setRelationType("CurrentZone"); + restrictedZonesGroup.setDirection(EntitySearchDirection.TO); + + config.setZoneGroups(Map.of("allowedZones", allowedZonesGroup, "restrictedZones", restrictedZonesGroup)); + + Output output = new Output(); + output.setType(OutputType.TIME_SERIES); + config.setOutput(output); + return config; + } + + private TelemetryCalculatedFieldResult performCalculation() throws InterruptedException, ExecutionException { + return (TelemetryCalculatedFieldResult) state.performCalculation(Collections.emptyMap(), ctx).get(); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingValueArgumentEntryTest.java b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingValueArgumentEntryTest.java new file mode 100644 index 0000000000..6da4bdc882 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingValueArgumentEntryTest.java @@ -0,0 +1,184 @@ +/** + * Copyright © 2016-2025 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.cf.ctx.state; + +import io.hypersistence.utils.hibernate.type.json.internal.JacksonUtil; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.thingsboard.common.util.geo.PerimeterDefinition; +import org.thingsboard.server.common.data.id.AssetId; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry; +import org.thingsboard.server.common.data.kv.JsonDataEntry; +import org.thingsboard.server.common.data.kv.StringDataEntry; +import org.thingsboard.server.service.cf.ctx.state.geofencing.GeofencingArgumentEntry; +import org.thingsboard.server.service.cf.ctx.state.geofencing.GeofencingZoneState; + +import java.util.Map; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +public class GeofencingValueArgumentEntryTest { + + private final AssetId ZONE_1_ID = new AssetId(UUID.fromString("c0e3031c-7df1-45e4-9590-cfd621a4d714")); + private final AssetId ZONE_2_ID = new AssetId(UUID.fromString("e7da6200-2096-4038-a343-ade9ea4fa3e4")); + + private final JsonDataEntry allowedZoneDataEntry = new JsonDataEntry("zone", "[[50.472000, 30.504000], [50.472000, 30.506000], [50.474000, 30.506000], [50.474000, 30.504000]]"); + private final BaseAttributeKvEntry allowedZoneAttributeKvEntry = new BaseAttributeKvEntry(allowedZoneDataEntry, 363L, 155L); + + private final JsonDataEntry restrictedZoneDataEntry = new JsonDataEntry("zone", "[[50.475000, 30.510000], [50.475000, 30.512000], [50.477000, 30.512000], [50.477000, 30.510000]]"); + private final BaseAttributeKvEntry restrictedZoneAttributeKvEntry = new BaseAttributeKvEntry(restrictedZoneDataEntry, 363L, 155L); + + private GeofencingArgumentEntry entry; + + @BeforeEach + void setUp() { + entry = new GeofencingArgumentEntry(Map.of(ZONE_1_ID, allowedZoneAttributeKvEntry, ZONE_2_ID, restrictedZoneAttributeKvEntry)); + } + + @Test + void testArgumentEntryType() { + assertThat(entry.getType()).isEqualTo(ArgumentEntryType.GEOFENCING); + } + + @Test + void testUpdateEntryWhenSingleEntryPassed() { + assertThatThrownBy(() -> entry.updateEntry(new SingleValueArgumentEntry())) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Unsupported argument entry type for geofencing argument entry: SINGLE_VALUE"); + } + + @Test + void testUpdateEntryWhenRollingEntryPassed() { + assertThatThrownBy(() -> entry.updateEntry(new TsRollingArgumentEntry(5, 30000L))) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Unsupported argument entry type for geofencing argument entry: TS_ROLLING"); + } + + @Test + void testUpdateEntryWithTheSameTs() { + BaseAttributeKvEntry differentValueSameTs = new BaseAttributeKvEntry(new JsonDataEntry("zone", "[[50.472001, 30.504001], [50.472001, 30.506001], [50.474001, 30.506001], [50.474001, 30.504001]]"), 363L, 156L); + var updated = new GeofencingArgumentEntry(Map.of(ZONE_1_ID, differentValueSameTs, ZONE_2_ID, restrictedZoneAttributeKvEntry)); + assertThat(entry.updateEntry(updated)).isFalse(); + } + + @Test + @SuppressWarnings("unchecked") + void testUpdateEntryWhenNewVersionIsNull() { + BaseAttributeKvEntry differentValueNewVersionIsNull = new BaseAttributeKvEntry(new JsonDataEntry("zone", "[[50.472001, 30.504001], [50.472001, 30.506001], [50.474001, 30.506001], [50.474001, 30.504001]]"), 364L, null); + var updated = new GeofencingArgumentEntry(Map.of(ZONE_1_ID, differentValueNewVersionIsNull, ZONE_2_ID, restrictedZoneAttributeKvEntry)); + + assertThat(entry.updateEntry(updated)).isTrue(); + assertThat(entry.getValue()).isInstanceOf(Map.class); + + Map value = (Map) entry.getValue(); + assertThat(value).hasSize(2); + assertThat(value.get(ZONE_1_ID).getVersion()).isNull(); + assertThat(value.get(ZONE_1_ID).getTs()).isEqualTo(364L); + assertThat(value.get(ZONE_1_ID).getPerimeterDefinition()) + .isEqualTo(JacksonUtil.fromString(differentValueNewVersionIsNull.getJsonValue().get(), PerimeterDefinition.class)); + + assertThat(value.get(ZONE_2_ID).getVersion()).isEqualTo(155L); + assertThat(value.get(ZONE_2_ID).getTs()).isEqualTo(363L); + assertThat(value.get(ZONE_2_ID).getPerimeterDefinition()) + .isEqualTo(JacksonUtil.fromString(restrictedZoneAttributeKvEntry.getJsonValue().get(), PerimeterDefinition.class)); + } + + @Test + @SuppressWarnings("unchecked") + void testUpdateEntryWhenNewVersionIsGreaterThanCurrent() { + BaseAttributeKvEntry differentValueNewVersionIsSet = new BaseAttributeKvEntry(new JsonDataEntry("zone", "[[50.472001, 30.504001], [50.472001, 30.506001], [50.474001, 30.506001], [50.474001, 30.504001]]"), 364L, 156L); + var updated = new GeofencingArgumentEntry(Map.of(ZONE_1_ID, differentValueNewVersionIsSet, ZONE_2_ID, restrictedZoneAttributeKvEntry)); + + assertThat(entry.updateEntry(updated)).isTrue(); + assertThat(entry.getValue()).isInstanceOf(Map.class); + + Map value = (Map) entry.getValue(); + assertThat(value).hasSize(2); + assertThat(value.get(ZONE_1_ID).getVersion()).isEqualTo(156L); + assertThat(value.get(ZONE_1_ID).getTs()).isEqualTo(364L); + assertThat(value.get(ZONE_1_ID).getPerimeterDefinition()) + .isEqualTo(JacksonUtil.fromString(differentValueNewVersionIsSet.getJsonValue().get(), PerimeterDefinition.class)); + + assertThat(value.get(ZONE_2_ID).getVersion()).isEqualTo(155L); + assertThat(value.get(ZONE_2_ID).getTs()).isEqualTo(363L); + assertThat(value.get(ZONE_2_ID).getPerimeterDefinition()) + .isEqualTo(JacksonUtil.fromString(restrictedZoneAttributeKvEntry.getJsonValue().get(), PerimeterDefinition.class)); + } + + @Test + void testUpdateEntryWhenNewVersionIsLessThanCurrent() { + BaseAttributeKvEntry differentValueNewVersionIsSet = new BaseAttributeKvEntry(new JsonDataEntry("zone", "[[50.472001, 30.504001], [50.472001, 30.506001], [50.474001, 30.506001], [50.474001, 30.504001]]"), 364L, 154L); + var updated = new GeofencingArgumentEntry(Map.of(ZONE_1_ID, differentValueNewVersionIsSet, ZONE_2_ID, restrictedZoneAttributeKvEntry)); + + assertThat(entry.updateEntry(updated)).isFalse(); + } + + @Test + void testUpdateEntryWhenNewTsAndVersionIsGreaterThenCurrentAndValueWasNotChanged() { + BaseAttributeKvEntry newTsAndTheSameValue = new BaseAttributeKvEntry(allowedZoneDataEntry, 364L, 156L); + var updated = new GeofencingArgumentEntry(Map.of(ZONE_1_ID, newTsAndTheSameValue, ZONE_2_ID, restrictedZoneAttributeKvEntry)); + + assertThat(entry.updateEntry(updated)).isTrue(); + } + + @Test + void testUpdateEntryWithOldTs() { + BaseAttributeKvEntry oldTsAndTheSameValue = new BaseAttributeKvEntry(allowedZoneDataEntry, 362L, 156L); + var updated = new GeofencingArgumentEntry(Map.of(ZONE_1_ID, oldTsAndTheSameValue, ZONE_2_ID, restrictedZoneAttributeKvEntry)); + + assertThat(entry.updateEntry(updated)).isFalse(); + } + + @Test + void testUpdateEntryWithNewZone() { + final AssetId NEW_ZONE_ID = new AssetId(UUID.fromString("a3eacf1a-6af3-4e9f-87c4-502bb25c7dc3")); + BaseAttributeKvEntry newZone = new BaseAttributeKvEntry(new JsonDataEntry("zone", "[[50.472001, 30.504001], [50.472001, 30.506001], [50.474001, 30.506001], [50.474001, 30.504001]]"), 364L, 156L); + var updated = new GeofencingArgumentEntry(Map.of(ZONE_1_ID, allowedZoneAttributeKvEntry, ZONE_2_ID, restrictedZoneAttributeKvEntry, NEW_ZONE_ID, newZone)); + assertThat(entry.updateEntry(updated)).isTrue(); + } + + @Test + void testIsEmpty() { + GeofencingArgumentEntry geofencingArgumentEntry = new GeofencingArgumentEntry(); + assertThat(geofencingArgumentEntry.isEmpty()).isTrue(); + } + + @Test + void testIsEmptyWithEmptyMap() { + GeofencingArgumentEntry geofencingArgumentEntry = new GeofencingArgumentEntry(Map.of()); + assertThat(geofencingArgumentEntry.isEmpty()).isTrue(); + } + + @Test + void testInvalidKvEntryDataTypeForZoneResultInEmptyArgument() { + BaseAttributeKvEntry invalidZoneEntry = new BaseAttributeKvEntry(new StringDataEntry("zone", "someString"), 363L, 155L); + assertThatThrownBy(() -> new GeofencingArgumentEntry(Map.of(ZONE_1_ID, invalidZoneEntry))) + .isExactlyInstanceOf(IllegalArgumentException.class) + .hasMessage("The given string value cannot be transformed to Json object: someString"); + } + + @Test + void testNotParsableToPerimeterJsonKvEntryResultInExceptionTrowed() { + BaseAttributeKvEntry invalidZoneEntry = new BaseAttributeKvEntry(new JsonDataEntry("zone", "\"{}\""), 363L, 155L); + assertThatThrownBy(() -> new GeofencingArgumentEntry(Map.of(ZONE_1_ID, invalidZoneEntry))) + .isExactlyInstanceOf(IllegalArgumentException.class) + .hasMessage("The given string value cannot be transformed to Json object: \"{}\""); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingZoneStateTest.java b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingZoneStateTest.java new file mode 100644 index 0000000000..f6c6778ced --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingZoneStateTest.java @@ -0,0 +1,169 @@ +/** + * Copyright © 2016-2025 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.cf.ctx.state; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.thingsboard.common.util.geo.Coordinates; +import org.thingsboard.server.common.data.id.AssetId; +import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry; +import org.thingsboard.server.common.data.kv.JsonDataEntry; +import org.thingsboard.server.service.cf.ctx.state.geofencing.GeofencingEvalResult; +import org.thingsboard.server.service.cf.ctx.state.geofencing.GeofencingZoneState; + +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingPresenceStatus.INSIDE; +import static org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingPresenceStatus.OUTSIDE; +import static org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingTransitionEvent.ENTERED; +import static org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingTransitionEvent.LEFT; + +public class GeofencingZoneStateTest { + + private final AssetId ZONE_ID = new AssetId(UUID.fromString("628730fd-d625-417f-9c6d-ae9fe4addbdb")); + + private GeofencingZoneState state; + + @BeforeEach + void setUp() { + String POLYGON = "[[50.472000, 30.504000], [50.472000, 30.506000], [50.474000, 30.506000], [50.474000, 30.504000]]"; + state = new GeofencingZoneState(ZONE_ID, new BaseAttributeKvEntry(new JsonDataEntry("zone", POLYGON), 100L, 1L)); + } + + @Test + void evaluate_initialInside_thenInsideAgain() { + var inside = new Coordinates(50.4730, 30.5050); + // first evaluation: no prior state -> ENTERED + assertThat(state.evaluate(inside)).isEqualTo(new GeofencingEvalResult(ENTERED, INSIDE)); + // same position again -> INSIDE (steady state) + assertThat(state.evaluate(inside)).isEqualTo(new GeofencingEvalResult(null, INSIDE)); + } + + @Test + void evaluate_initialOutside_thenOutsideAgain() { + var outside = new Coordinates(50.4760, 30.5110); + // first evaluation: no prior state -> OUTSIDE + assertThat(state.evaluate(outside)).isEqualTo(new GeofencingEvalResult(null, OUTSIDE)); + // same position again -> OUTSIDE (steady state) + assertThat(state.evaluate(outside)).isEqualTo(new GeofencingEvalResult(null, OUTSIDE)); + } + + @Test + void evaluate_inside_thenLeave() { + var inside = new Coordinates(50.4730, 30.5050); + var outside = new Coordinates(50.4760, 30.5110); + // enter + assertThat(state.evaluate(inside)).isEqualTo(new GeofencingEvalResult(ENTERED, INSIDE)); + // leave -> LEFT + assertThat(state.evaluate(outside)).isEqualTo(new GeofencingEvalResult(LEFT, OUTSIDE)); + // still outside -> OUTSIDE + assertThat(state.evaluate(outside)).isEqualTo(new GeofencingEvalResult(null, OUTSIDE)); + } + + @Test + void evaluate_outside_thenEnter() { + var outside = new Coordinates(50.4760, 30.5110); + var inside = new Coordinates(50.4730, 30.5050); + // start outside + assertThat(state.evaluate(outside)).isEqualTo(new GeofencingEvalResult(null, OUTSIDE)); + // cross boundary -> ENTERED + assertThat(state.evaluate(inside)).isEqualTo(new GeofencingEvalResult(ENTERED, INSIDE)); + // remain inside -> INSIDE + assertThat(state.evaluate(inside)).isEqualTo(new GeofencingEvalResult(null, INSIDE)); + } + + @Test + void update_withNewerVersion_updatesState_andResetsPresence() { + // arrange: establish a prior presence to ensure it’s reset on update + var inside = new Coordinates(50.4730, 30.5050); + assertThat(state.evaluate(inside)).isNotNull(); // sets lastPresence internally + + String NEW_POLYGON = "[[50.470000, 30.502000], [50.470000, 30.503000], [50.471000, 30.503000], [50.471000, 30.502000]]"; + GeofencingZoneState newer = new GeofencingZoneState( + ZONE_ID, + new BaseAttributeKvEntry(new JsonDataEntry("zone", NEW_POLYGON), 200L, 2L) + ); + + // act + boolean changed = state.update(newer); + + // assert + assertThat(changed).isTrue(); + assertThat(state.getTs()).isEqualTo(200L); + assertThat(state.getVersion()).isEqualTo(2L); + assertThat(state.getPerimeterDefinition()).isNotNull(); + assertThat(state.getLastPresence()).isNull(); // must be reset on successful update + } + + @Test + void update_withEqualVersion_doesNothing() { + // arrange: same version (1L) but different ts/polygon should still be ignored + String SOME_POLYGON = "[[50.472500, 30.504500], [50.472500, 30.505500], [50.473500, 30.505500], [50.473500, 30.504500]]"; + GeofencingZoneState sameVersion = new GeofencingZoneState( + ZONE_ID, + new BaseAttributeKvEntry(new JsonDataEntry("zone", SOME_POLYGON), 300L, 1L) + ); + + // act + boolean changed = state.update(sameVersion); + + // assert: nothing changes + assertThat(changed).isFalse(); + assertThat(state.getTs()).isEqualTo(100L); + assertThat(state.getVersion()).isEqualTo(1L); + } + + @Test + void update_withNullNewVersion_alwaysApplies_andCopiesNull() { + // arrange: the implementation updates if newVersion == null + String OTHER_POLYGON = "[[50.471000, 30.506000], [50.471000, 30.507000], [50.472000, 30.507000], [50.472000, 30.506000]]"; + GeofencingZoneState nullVersion = new GeofencingZoneState( + ZONE_ID, + new BaseAttributeKvEntry(new JsonDataEntry("zone", OTHER_POLYGON), 400L, null) + ); + + // act + boolean changed = state.update(nullVersion); + + // assert: applied and version copied as null + assertThat(changed).isTrue(); + assertThat(state.getTs()).isEqualTo(400L); + assertThat(state.getVersion()).isNull(); + assertThat(state.getLastPresence()).isNull(); + } + + @Test + void update_withNewVersionWhenExistingIsNull_alwaysApplies_andCopiesNew() { + // arrange: the implementation updates if newVersion == null + String OTHER_POLYGON = "[[50.471000, 30.506000], [50.471000, 30.507000], [50.472000, 30.507000], [50.472000, 30.506000]]"; + GeofencingZoneState newVersion = new GeofencingZoneState( + ZONE_ID, + new BaseAttributeKvEntry(new JsonDataEntry("zone", OTHER_POLYGON), 400L, 2L) + ); + state.setVersion(null); + + // act + boolean changed = state.update(newVersion); + + // assert: applied and version copied as null + assertThat(changed).isTrue(); + assertThat(state.getTs()).isEqualTo(400L); + assertThat(state.getVersion()).isEqualTo(2); + assertThat(state.getLastPresence()).isNull(); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/PropagationArgumentEntryTest.java b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/PropagationArgumentEntryTest.java new file mode 100644 index 0000000000..14a1b629c1 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/PropagationArgumentEntryTest.java @@ -0,0 +1,127 @@ +/** + * Copyright © 2016-2025 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.cf.ctx.state; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.thingsboard.script.api.tbel.TbelCfArg; +import org.thingsboard.script.api.tbel.TbelCfPropagationArg; +import org.thingsboard.server.common.data.id.AssetId; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.service.cf.ctx.state.propagation.PropagationArgumentEntry; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +public class PropagationArgumentEntryTest { + + private final AssetId ENTITY_1_ID = new AssetId(UUID.fromString("b0a8637d-6d67-43d5-a483-c0e391afe805")); + private final AssetId ENTITY_2_ID = new AssetId(UUID.fromString("7bd85073-ded5-414f-a2ef-bd56ad3dbf6a")); + private final AssetId ENTITY_3_ID = new AssetId(UUID.fromString("d64f3e51-2ec2-472f-b475-b095ef8bdc70")); + + private PropagationArgumentEntry entry; + + @BeforeEach + void setUp() { + List propagationEntityIds = new ArrayList<>(); + propagationEntityIds.add(ENTITY_1_ID); + propagationEntityIds.add(ENTITY_2_ID); + entry = new PropagationArgumentEntry(propagationEntityIds); + } + + @Test + void testArgumentEntryType() { + assertThat(entry.getType()).isEqualTo(ArgumentEntryType.PROPAGATION); + } + + @Test + void testIsEmpty() { + PropagationArgumentEntry emptyEntry = new PropagationArgumentEntry(List.of()); + assertThat(emptyEntry.isEmpty()).isTrue(); + } + + @Test + void testGetValueReturnsPropagationIds() { + assertThat(entry.getValue()).isInstanceOf(List.class); + @SuppressWarnings("unchecked") + List value = (List) entry.getValue(); + assertThat(value).containsExactly(ENTITY_1_ID, ENTITY_2_ID); + } + + @Test + void testUpdateEntryWhenSingleEntryPassed() { + assertThatThrownBy(() -> entry.updateEntry(new SingleValueArgumentEntry())) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Unsupported argument entry type for propagation argument entry: SINGLE_VALUE"); + } + + @Test + void testUpdateEntryWhenRollingEntryPassed() { + assertThatThrownBy(() -> entry.updateEntry(new TsRollingArgumentEntry(5, 30000L))) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Unsupported argument entry type for propagation argument entry: TS_ROLLING"); + } + + @Test + void testUpdateEntryReplacesWithNewIds() { + var newIds = new ArrayList(List.of(ENTITY_3_ID, ENTITY_1_ID)); + var updated = new PropagationArgumentEntry(newIds); + + boolean changed = entry.updateEntry(updated); + + assertThat(changed).isTrue(); + assertThat(entry.getPropagationEntityIds()).containsExactlyElementsOf(newIds); + } + + @Test + void testUpdateEntryClearsWhenNewEntryIsEmpty() { + var updatedEmpty = new PropagationArgumentEntry(List.of()); + + boolean changed = entry.updateEntry(updatedEmpty); + + assertThat(changed).isTrue(); + assertThat(entry.getPropagationEntityIds()).isEmpty(); + } + + @Test + @SuppressWarnings("unchecked") + void testToTbelCfArgWithValues() { + TbelCfArg arg = entry.toTbelCfArg(); + assertThat(arg).isInstanceOf(TbelCfPropagationArg.class); + + TbelCfPropagationArg tbelCfPropagationArg = (TbelCfPropagationArg) arg; + assertThat(tbelCfPropagationArg.getValue()).isInstanceOf(List.class); + assertThat((List) tbelCfPropagationArg.getValue()).containsExactly(ENTITY_1_ID, ENTITY_2_ID); + } + + + @Test + @SuppressWarnings("unchecked") + void testToTbelCfArgWithEmptyValues() { + var empty = new PropagationArgumentEntry(List.of()); + TbelCfArg emptyArg = empty.toTbelCfArg(); + assertThat(emptyArg).isInstanceOf(TbelCfPropagationArg.class); + + TbelCfPropagationArg tbelCfPropagationArg = (TbelCfPropagationArg) emptyArg; + assertThat(tbelCfPropagationArg.getValue()).isInstanceOf(List.class); + assertThat((List) tbelCfPropagationArg.getValue()).isEmpty(); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/PropagationCalculatedFieldStateTest.java b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/PropagationCalculatedFieldStateTest.java new file mode 100644 index 0000000000..88cc6972b8 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/PropagationCalculatedFieldStateTest.java @@ -0,0 +1,249 @@ +/** + * Copyright © 2016-2025 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.cf.ctx.state; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.script.api.tbel.DefaultTbelInvokeService; +import org.thingsboard.script.api.tbel.TbelInvokeService; +import org.thingsboard.server.actors.ActorSystemContext; +import org.thingsboard.server.common.data.AttributeScope; +import org.thingsboard.server.common.data.cf.CalculatedField; +import org.thingsboard.server.common.data.cf.CalculatedFieldType; +import org.thingsboard.server.common.data.cf.configuration.Argument; +import org.thingsboard.server.common.data.cf.configuration.ArgumentType; +import org.thingsboard.server.common.data.cf.configuration.CalculatedFieldConfiguration; +import org.thingsboard.server.common.data.cf.configuration.Output; +import org.thingsboard.server.common.data.cf.configuration.OutputType; +import org.thingsboard.server.common.data.cf.configuration.PropagationCalculatedFieldConfiguration; +import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey; +import org.thingsboard.server.common.data.id.AssetId; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.kv.DoubleDataEntry; +import org.thingsboard.server.common.data.relation.EntityRelation; +import org.thingsboard.server.common.data.relation.EntitySearchDirection; +import org.thingsboard.server.common.data.relation.RelationPathLevel; +import org.thingsboard.server.common.stats.DefaultStatsFactory; +import org.thingsboard.server.dao.usagerecord.ApiLimitService; +import org.thingsboard.server.service.cf.PropagationCalculatedFieldResult; +import org.thingsboard.server.service.cf.TelemetryCalculatedFieldResult; +import org.thingsboard.server.service.cf.ctx.state.propagation.PropagationArgumentEntry; +import org.thingsboard.server.service.cf.ctx.state.propagation.PropagationCalculatedFieldState; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ExecutionException; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; +import static org.thingsboard.server.common.data.cf.configuration.PropagationCalculatedFieldConfiguration.PROPAGATION_CONFIG_ARGUMENT; + +@SpringBootTest(classes = {SimpleMeterRegistry.class, DefaultStatsFactory.class, DefaultTbelInvokeService.class}) +public class PropagationCalculatedFieldStateTest { + + private static final String TEMPERATURE_ARGUMENT_NAME = "t"; + private static final String TEST_RESULT_EXPRESSION_KEY = "testResult"; + private static final double TEMPERATURE_VALUE = 12.5; + + private final TenantId TENANT_ID = TenantId.fromUUID(UUID.fromString("6c3513cb-85e7-4510-8746-1ba01859a8ce")); + private final DeviceId DEVICE_ID = new DeviceId(UUID.fromString("be960a50-c029-4698-b2ec-c56a543c561c")); + private final AssetId ASSET_ID_1 = new AssetId(UUID.fromString("d26f0e5b-7d7d-4a61-9f5e-08ab97b30734")); + private final AssetId ASSET_ID_2 = new AssetId(UUID.fromString("1933a317-4df5-4d36-9800-68aded74579b")); + + private final SingleValueArgumentEntry singleValueArgEntry = + new SingleValueArgumentEntry(System.currentTimeMillis(), new DoubleDataEntry("temperature", TEMPERATURE_VALUE), 99L); + + private final PropagationArgumentEntry propagationArgEntry = + new PropagationArgumentEntry(new ArrayList<>(List.of(ASSET_ID_2, ASSET_ID_1))); + + private PropagationCalculatedFieldState state; + private CalculatedFieldCtx ctx; + + @Autowired + private TbelInvokeService tbelInvokeService; + + @MockitoBean + private ApiLimitService apiLimitService; + + @MockitoBean + private ActorSystemContext actorSystemContext; + + @BeforeEach + void setUp() { + when(actorSystemContext.getTbelInvokeService()).thenReturn(tbelInvokeService); + when(actorSystemContext.getApiLimitService()).thenReturn(apiLimitService); + when(apiLimitService.getLimit(any(), any())).thenReturn(1000L); + } + + void initCtxAndState(boolean applyExpressionToResolvedArguments) { + ctx = new CalculatedFieldCtx(getCalculatedField(applyExpressionToResolvedArguments), actorSystemContext); + ctx.init(); + + state = new PropagationCalculatedFieldState(ctx.getEntityId()); + state.setCtx(ctx, null); + state.init(false); + } + + @Test + void testType() { + initCtxAndState(false); + assertThat(state.getType()).isEqualTo(CalculatedFieldType.PROPAGATION); + } + + @Test + void testInitAddsRequiredArgument() { + initCtxAndState(false); + assertThat(state.getRequiredArguments()).containsExactlyInAnyOrder(TEMPERATURE_ARGUMENT_NAME, PROPAGATION_CONFIG_ARGUMENT); + } + + @Test + void testIsReadyReturnFalseWhenNoArgumentsSet() { + initCtxAndState(false); + assertThat(state.isReady()).isFalse(); + } + + @Test + void testIsReadyWhenPropagationArgIsNull() { + initCtxAndState(false); + state.update(Map.of(TEMPERATURE_ARGUMENT_NAME, singleValueArgEntry), ctx); + assertThat(state.isReady()).isFalse(); + assertThat(state.getReadinessStatus().errorMsg()).contains(PROPAGATION_CONFIG_ARGUMENT); + } + + @Test + void testIsReadyWhenPropagationArgIsEmpty() { + initCtxAndState(false); + state.update(Map.of(TEMPERATURE_ARGUMENT_NAME, singleValueArgEntry, + PROPAGATION_CONFIG_ARGUMENT, new PropagationArgumentEntry(Collections.emptyList())), ctx); + assertThat(state.isReady()).isFalse(); + assertThat(state.getReadinessStatus().errorMsg()).contains(PROPAGATION_CONFIG_ARGUMENT); + } + + @Test + void testIsReadyWhenPropagationArgHasEntities() { + initCtxAndState(false); + state.update(Map.of(TEMPERATURE_ARGUMENT_NAME, singleValueArgEntry, PROPAGATION_CONFIG_ARGUMENT, propagationArgEntry), ctx); + assertThat(state.isReady()).isTrue(); + assertThat(state.getReadinessStatus().errorMsg()).isNull(); + } + + + @Test + void testPerformCalculationWithEmptyPropagationArg() throws Exception { + initCtxAndState(false); + state.getArguments().put(PROPAGATION_CONFIG_ARGUMENT, new PropagationArgumentEntry(Collections.emptyList())); + + PropagationCalculatedFieldResult result = performCalculation(); + + assertThat(result).isNotNull(); + assertThat(result.isEmpty()).isTrue(); + assertThat(result.getPropagationEntityIds()).isNullOrEmpty(); + } + + @Test + void testPerformCalculationWithArgumentsOnlyMode() throws Exception { + initCtxAndState(false); + state.getArguments().put(PROPAGATION_CONFIG_ARGUMENT, propagationArgEntry); + state.getArguments().put(TEMPERATURE_ARGUMENT_NAME, singleValueArgEntry); + + PropagationCalculatedFieldResult propagationResult = performCalculation(); + + assertThat(propagationResult).isNotNull(); + assertThat(propagationResult.isEmpty()).isFalse(); + assertThat(propagationResult.getPropagationEntityIds()).containsExactly(ASSET_ID_2, ASSET_ID_1); + + TelemetryCalculatedFieldResult result = propagationResult.getResult(); + assertThat(result).isNotNull(); + assertThat(result.getType()).isEqualTo(OutputType.ATTRIBUTES); + assertThat(result.getScope()).isEqualTo(AttributeScope.SERVER_SCOPE); + + ObjectNode expectedNode = JacksonUtil.newObjectNode(); + JacksonUtil.addKvEntry(expectedNode, singleValueArgEntry.getKvEntryValue(), TEMPERATURE_ARGUMENT_NAME); + + assertThat(result.getResult()).isEqualTo(expectedNode); + } + + @Test + void testPerformCalculationWithExpressionResultMode() throws Exception { + initCtxAndState(true); + state.getArguments().put(PROPAGATION_CONFIG_ARGUMENT, propagationArgEntry); + state.getArguments().put(TEMPERATURE_ARGUMENT_NAME, singleValueArgEntry); + + PropagationCalculatedFieldResult propagationResult = performCalculation(); + + assertThat(propagationResult).isNotNull(); + assertThat(propagationResult.isEmpty()).isFalse(); + assertThat(propagationResult.getPropagationEntityIds()).containsExactly(ASSET_ID_2, ASSET_ID_1); + + TelemetryCalculatedFieldResult result = propagationResult.getResult(); + assertThat(result).isNotNull(); + assertThat(result.getType()).isEqualTo(OutputType.ATTRIBUTES); + assertThat(result.getScope()).isEqualTo(AttributeScope.SERVER_SCOPE); + + ObjectNode expectedNode = JacksonUtil.newObjectNode(); + expectedNode.put(TEST_RESULT_EXPRESSION_KEY, TEMPERATURE_VALUE * 2); + + assertThat(result.getResult()).isEqualTo(expectedNode); + } + + private CalculatedField getCalculatedField(boolean applyExpressionToResolvedArguments) { + CalculatedField calculatedField = new CalculatedField(); + calculatedField.setTenantId(TENANT_ID); + calculatedField.setEntityId(DEVICE_ID); + calculatedField.setType(CalculatedFieldType.PROPAGATION); + calculatedField.setName("Test Propagation CF"); + calculatedField.setConfigurationVersion(1); + calculatedField.setConfiguration(getCalculatedFieldConfig(applyExpressionToResolvedArguments)); + calculatedField.setVersion(1L); + return calculatedField; + } + + private CalculatedFieldConfiguration getCalculatedFieldConfig(boolean applyExpressionToResolvedArguments) { + var config = new PropagationCalculatedFieldConfiguration(); + + config.setRelation(new RelationPathLevel(EntitySearchDirection.TO, EntityRelation.CONTAINS_TYPE)); + config.setApplyExpressionToResolvedArguments(applyExpressionToResolvedArguments); + + Argument temperatureArg = new Argument(); + ReferencedEntityKey tempKey = new ReferencedEntityKey("temperature", ArgumentType.TS_LATEST, null); + temperatureArg.setRefEntityKey(tempKey); + + config.setArguments(Map.of(TEMPERATURE_ARGUMENT_NAME, temperatureArg)); + config.setExpression("{" + TEST_RESULT_EXPRESSION_KEY + ": " + TEMPERATURE_ARGUMENT_NAME + " * 2}"); + + Output output = new Output(); + output.setType(OutputType.ATTRIBUTES); + output.setScope(AttributeScope.SERVER_SCOPE); + config.setOutput(output); + + return config; + } + + private PropagationCalculatedFieldResult performCalculation() throws ExecutionException, InterruptedException { + return (PropagationCalculatedFieldResult) state.performCalculation(Collections.emptyMap(), ctx).get(); + } +} diff --git a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/RelatedEntitiesArgumentEntryTest.java b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/RelatedEntitiesArgumentEntryTest.java new file mode 100644 index 0000000000..cc60b249ac --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/RelatedEntitiesArgumentEntryTest.java @@ -0,0 +1,100 @@ +/** + * Copyright © 2016-2025 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.cf.ctx.state; + +import org.junit.jupiter.api.BeforeEach; +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.kv.BasicTsKvEntry; +import org.thingsboard.server.common.data.kv.LongDataEntry; +import org.thingsboard.server.service.cf.ctx.state.aggregation.RelatedEntitiesArgumentEntry; + +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +public class RelatedEntitiesArgumentEntryTest { + + private RelatedEntitiesArgumentEntry entry; + + private final DeviceId device1 = new DeviceId(UUID.fromString("1984e5f4-9ff0-4187-84ae-e4438bba4c8a")); + private final DeviceId device2 = new DeviceId(UUID.fromString("937fc062-1a9d-438f-aa22-55a93fc908b7")); + + private final long ts = System.currentTimeMillis(); + + @BeforeEach + void setUp() { + Map aggInputs = new HashMap<>(); + aggInputs.put(device1, new SingleValueArgumentEntry(device1, new BasicTsKvEntry(ts - 100, new LongDataEntry("key", 12L), 1L))); + aggInputs.put(device2, new SingleValueArgumentEntry(device2, new BasicTsKvEntry(ts - 150, new LongDataEntry("key", 16L), 6L))); + + entry = new RelatedEntitiesArgumentEntry(aggInputs, false); + } + + @Test + void testUpdateEntryWhenNotAggEntryPassed() { + assertThatThrownBy(() -> entry.updateEntry(new TsRollingArgumentEntry(5, 30000L))) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Unsupported argument entry type for aggregation argument entry: " + ArgumentEntryType.TS_ROLLING); + } + + @Test + void testUpdateEntryWhenAggArgumentEntryPasser() { + DeviceId device3 = new DeviceId(UUID.randomUUID()); + DeviceId device4 = new DeviceId(UUID.randomUUID()); + + RelatedEntitiesArgumentEntry relatedEntitiesArgumentEntry = new RelatedEntitiesArgumentEntry(Map.of( + device3, new SingleValueArgumentEntry(device3, new BasicTsKvEntry(ts - 50, new LongDataEntry("key", 16L), 13L)), + device4, new SingleValueArgumentEntry(device4, new BasicTsKvEntry(ts - 60, new LongDataEntry("key", 23L), 7L)) + ), false); + + assertThat(entry.updateEntry(relatedEntitiesArgumentEntry)).isTrue(); + + Map aggInputs = entry.getEntityInputs(); + assertThat(aggInputs.size()).isEqualTo(4); + assertThat(aggInputs.get(device3)).isEqualTo(relatedEntitiesArgumentEntry.getEntityInputs().get(device3)); + assertThat(aggInputs.get(device4)).isEqualTo(relatedEntitiesArgumentEntry.getEntityInputs().get(device4)); + } + + @Test + void testUpdateEntryWhenSingleValueArgumentEntryPassedAndNoEntriesById() { + DeviceId device3 = new DeviceId(UUID.randomUUID()); + + SingleValueArgumentEntry singleEntityArgumentEntry = new SingleValueArgumentEntry(device3, new BasicTsKvEntry(ts - 50, new LongDataEntry("key", 18L), 10L)); + + assertThat(entry.updateEntry(singleEntityArgumentEntry)).isTrue(); + + Map aggInputs = entry.getEntityInputs(); + assertThat(aggInputs.size()).isEqualTo(3); + assertThat(aggInputs.get(device3)).isEqualTo(singleEntityArgumentEntry); + } + + @Test + void testUpdateEntryWhenSingleValueArgumentEntryPassedAndEntryByIdExist() { + SingleValueArgumentEntry singleEntityArgumentEntry = new SingleValueArgumentEntry(device2, new BasicTsKvEntry(ts - 50, new LongDataEntry("key", 18L), 10L)); + + assertThat(entry.updateEntry(singleEntityArgumentEntry)).isTrue(); + + Map aggInputs = entry.getEntityInputs(); + assertThat(aggInputs.size()).isEqualTo(2); + assertThat(aggInputs.get(device2)).isEqualTo(singleEntityArgumentEntry); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/ScriptCalculatedFieldStateTest.java b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/ScriptCalculatedFieldStateTest.java index 91eced64f6..9691f4a02d 100644 --- a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/ScriptCalculatedFieldStateTest.java +++ b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/ScriptCalculatedFieldStateTest.java @@ -18,12 +18,14 @@ package org.thingsboard.server.service.cf.ctx.state; import io.micrometer.core.instrument.simple.SimpleMeterRegistry; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.mockito.Mockito; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.script.api.tbel.DefaultTbelInvokeService; import org.thingsboard.script.api.tbel.TbelInvokeService; +import org.thingsboard.server.actors.ActorSystemContext; import org.thingsboard.server.common.data.AttributeScope; import org.thingsboard.server.common.data.cf.CalculatedField; import org.thingsboard.server.common.data.cf.CalculatedFieldType; @@ -41,8 +43,9 @@ import org.thingsboard.server.common.data.kv.DoubleDataEntry; import org.thingsboard.server.common.data.kv.LongDataEntry; import org.thingsboard.server.common.stats.DefaultStatsFactory; import org.thingsboard.server.dao.usagerecord.ApiLimitService; -import org.thingsboard.server.service.cf.CalculatedFieldResult; +import org.thingsboard.server.service.cf.TelemetryCalculatedFieldResult; +import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.TreeMap; @@ -60,7 +63,7 @@ public class ScriptCalculatedFieldStateTest { private final DeviceId DEVICE_ID = new DeviceId(UUID.fromString("5512071d-5abc-411d-a907-4cdb6539c2eb")); private final AssetId ASSET_ID = new AssetId(UUID.fromString("5bc010ae-bcfd-46c8-98b9-8ee8c8955a76")); - private final SingleValueArgumentEntry assetHumidityArgEntry = new SingleValueArgumentEntry(System.currentTimeMillis() - 10, new DoubleDataEntry("assetHumidity", 43.0), 122L); + private final SingleValueArgumentEntry assetHumidityArgEntry = new SingleValueArgumentEntry(System.currentTimeMillis() - 10, new DoubleDataEntry("assetHumidity", 86.0), 122L); private final TsRollingArgumentEntry deviceTemperatureArgEntry = createRollingArgEntry(); private final long ts = System.currentTimeMillis(); @@ -71,15 +74,21 @@ public class ScriptCalculatedFieldStateTest { @Autowired private TbelInvokeService tbelInvokeService; - @MockBean + @MockitoBean private ApiLimitService apiLimitService; @BeforeEach void setUp() { + ActorSystemContext systemContext = Mockito.mock(ActorSystemContext.class); + when(systemContext.getTbelInvokeService()).thenReturn(tbelInvokeService); + when(systemContext.getApiLimitService()).thenReturn(apiLimitService); + when(apiLimitService.getLimit(any(), any())).thenReturn(1000L); - ctx = new CalculatedFieldCtx(getCalculatedField(), tbelInvokeService, apiLimitService); + ctx = new CalculatedFieldCtx(getCalculatedField(), systemContext); ctx.init(); - state = new ScriptCalculatedFieldState(ctx.getArgNames()); + state = new ScriptCalculatedFieldState(ctx.getEntityId()); + state.setCtx(ctx, null); + state.init(false); } @Test @@ -92,7 +101,7 @@ public class ScriptCalculatedFieldStateTest { state.arguments = new HashMap<>(Map.of("assetHumidity", assetHumidityArgEntry)); Map newArgs = Map.of("deviceTemperature", deviceTemperatureArgEntry); - boolean stateUpdated = state.updateState(ctx, newArgs); + boolean stateUpdated = !state.update(newArgs, ctx).isEmpty(); assertThat(stateUpdated).isTrue(); assertThat(state.getArguments()).containsExactlyInAnyOrderEntriesOf( @@ -109,7 +118,7 @@ public class ScriptCalculatedFieldStateTest { SingleValueArgumentEntry newArgEntry = new SingleValueArgumentEntry(ts, new LongDataEntry("assetHumidity", 41L), 349L); Map newArgs = Map.of("assetHumidity", newArgEntry); - boolean stateUpdated = state.updateState(ctx, newArgs); + boolean stateUpdated = !state.update(newArgs, ctx).isEmpty(); assertThat(stateUpdated).isTrue(); assertThat(state.getArguments()).containsExactlyInAnyOrderEntriesOf( @@ -124,7 +133,7 @@ public class ScriptCalculatedFieldStateTest { void testPerformCalculation() throws ExecutionException, InterruptedException { state.arguments = new HashMap<>(Map.of("deviceTemperature", deviceTemperatureArgEntry, "assetHumidity", assetHumidityArgEntry)); - CalculatedFieldResult result = state.performCalculation(ctx).get(); + TelemetryCalculatedFieldResult result = performCalculation(); assertThat(result).isNotNull(); Output output = getCalculatedFieldConfig().getOutput(); @@ -133,23 +142,40 @@ public class ScriptCalculatedFieldStateTest { assertThat(result.getResult()).isEqualTo(JacksonUtil.valueToTree(Map.of("maxDeviceTemperature", 17.0, "assetHumidity", 43.0))); } + @Test + void testPerformCalculationWithLongEntry() throws ExecutionException, InterruptedException { + state.arguments = new HashMap<>(Map.of( + "deviceTemperature", deviceTemperatureArgEntry, + "assetHumidity", new SingleValueArgumentEntry(System.currentTimeMillis() - 10, new LongDataEntry("a", 45L), 10L) + )); + + TelemetryCalculatedFieldResult result = performCalculation(); + + assertThat(result).isNotNull(); + Output output = getCalculatedFieldConfig().getOutput(); + assertThat(result.getType()).isEqualTo(output.getType()); + assertThat(result.getScope()).isEqualTo(output.getScope()); + assertThat(result.getResult()).isEqualTo(JacksonUtil.valueToTree(Map.of("maxDeviceTemperature", 17.0, "assetHumidity", 22.5))); + } + @Test void testIsReadyWhenNotAllArgPresent() { assertThat(state.isReady()).isFalse(); + assertThat(state.getReadinessStatus().errorMsg()).contains(state.getRequiredArguments()); } @Test void testIsReadyWhenAllArgPresent() { - state.arguments = new HashMap<>(Map.of("deviceTemperature", deviceTemperatureArgEntry, "assetHumidity", assetHumidityArgEntry)); - + state.update(Map.of("deviceTemperature", deviceTemperatureArgEntry, "assetHumidity", assetHumidityArgEntry), ctx); assertThat(state.isReady()).isTrue(); + assertThat(state.getReadinessStatus().errorMsg()).isNull(); } @Test void testIsReadyWhenEmptyEntryPresents() { - state.arguments = new HashMap<>(Map.of("deviceTemperature", new TsRollingArgumentEntry(5, 30000L), "assetHumidity", assetHumidityArgEntry)); - + state.update(Map.of("deviceTemperature", new TsRollingArgumentEntry(5, 30000L), "assetHumidity", assetHumidityArgEntry), ctx); assertThat(state.isReady()).isFalse(); + assertThat(state.getReadinessStatus().errorMsg()).contains("deviceTemperature"); } private TsRollingArgumentEntry createRollingArgEntry() { @@ -193,7 +219,7 @@ public class ScriptCalculatedFieldStateTest { config.setArguments(Map.of("deviceTemperature", argument1, "assetHumidity", argument2)); - config.setExpression("return {\"maxDeviceTemperature\": deviceTemperature.max(), \"assetHumidity\": assetHumidity}"); + config.setExpression("return {\"maxDeviceTemperature\": deviceTemperature.max(), \"assetHumidity\": assetHumidity / 2 }"); Output output = new Output(); output.setType(OutputType.ATTRIBUTES); @@ -204,4 +230,8 @@ public class ScriptCalculatedFieldStateTest { return config; } + private TelemetryCalculatedFieldResult performCalculation() throws InterruptedException, ExecutionException { + return (TelemetryCalculatedFieldResult) state.performCalculation(Collections.emptyMap(), ctx).get(); + } + } \ No newline at end of file diff --git a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldStateTest.java b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldStateTest.java index c1616a85db..6b25643cdf 100644 --- a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldStateTest.java +++ b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldStateTest.java @@ -18,9 +18,11 @@ package org.thingsboard.server.service.cf.ctx.state; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.actors.ActorSystemContext; import org.thingsboard.server.common.data.AttributeScope; import org.thingsboard.server.common.data.cf.CalculatedField; import org.thingsboard.server.common.data.cf.CalculatedFieldType; @@ -39,8 +41,9 @@ import org.thingsboard.server.common.data.kv.DoubleDataEntry; import org.thingsboard.server.common.data.kv.LongDataEntry; import org.thingsboard.server.common.data.kv.StringDataEntry; import org.thingsboard.server.dao.usagerecord.ApiLimitService; -import org.thingsboard.server.service.cf.CalculatedFieldResult; +import org.thingsboard.server.service.cf.TelemetryCalculatedFieldResult; +import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.UUID; @@ -67,13 +70,17 @@ public class SimpleCalculatedFieldStateTest { @Mock private ApiLimitService apiLimitService; + @InjectMocks + private ActorSystemContext systemContext; @BeforeEach void setUp() { when(apiLimitService.getLimit(any(), any())).thenReturn(1000L); - ctx = new CalculatedFieldCtx(getCalculatedField(), null, apiLimitService); + ctx = new CalculatedFieldCtx(getCalculatedField(), systemContext); ctx.init(); - state = new SimpleCalculatedFieldState(ctx.getArgNames()); + state = new SimpleCalculatedFieldState(ctx.getEntityId()); + state.setCtx(ctx, null); + state.init(false); } @Test @@ -89,7 +96,7 @@ public class SimpleCalculatedFieldStateTest { )); Map newArgs = Map.of("key3", key3ArgEntry); - boolean stateUpdated = state.updateState(ctx, newArgs); + boolean stateUpdated = !state.update(newArgs, ctx).isEmpty(); assertThat(stateUpdated).isTrue(); assertThat(state.getArguments()).containsExactlyInAnyOrderEntriesOf( @@ -107,7 +114,7 @@ public class SimpleCalculatedFieldStateTest { SingleValueArgumentEntry newArgEntry = new SingleValueArgumentEntry(System.currentTimeMillis(), new LongDataEntry("key1", 18L), 190L); Map newArgs = Map.of("key1", newArgEntry); - boolean stateUpdated = state.updateState(ctx, newArgs); + boolean stateUpdated = !state.update(newArgs, ctx).isEmpty(); assertThat(stateUpdated).isTrue(); assertThat(state.getArguments()).containsExactlyInAnyOrderEntriesOf(Map.of("key1", newArgEntry)); @@ -121,9 +128,10 @@ public class SimpleCalculatedFieldStateTest { )); Map newArgs = Map.of("key3", new TsRollingArgumentEntry(10, 30000L)); - assertThatThrownBy(() -> state.updateState(ctx, newArgs)) + assertThatThrownBy(() -> state.update(newArgs, ctx)) .isInstanceOf(IllegalArgumentException.class) - .hasMessage("Rolling argument entry is not supported for simple calculated fields."); + .hasMessage("Unsupported argument type detected for argument: key3. " + + "Rolling argument entry is not supported for simple calculated fields."); } @Test @@ -134,7 +142,7 @@ public class SimpleCalculatedFieldStateTest { "key3", key3ArgEntry )); - CalculatedFieldResult result = state.performCalculation(ctx).get(); + TelemetryCalculatedFieldResult result = performCalculation(); assertThat(result).isNotNull(); Output output = getCalculatedFieldConfig().getOutput(); @@ -151,7 +159,7 @@ public class SimpleCalculatedFieldStateTest { "key3", key3ArgEntry )); - assertThatThrownBy(() -> state.performCalculation(ctx)) + assertThatThrownBy(() -> state.performCalculation(Collections.emptyMap(), ctx)) .isInstanceOf(IllegalArgumentException.class) .hasMessage("Argument 'key2' is not a number."); } @@ -164,7 +172,7 @@ public class SimpleCalculatedFieldStateTest { "key3", key3ArgEntry )); - CalculatedFieldResult result = state.performCalculation(ctx).get(); + TelemetryCalculatedFieldResult result = performCalculation(); assertThat(result).isNotNull(); Output output = getCalculatedFieldConfig().getOutput(); @@ -185,7 +193,7 @@ public class SimpleCalculatedFieldStateTest { output.setDecimalsByDefault(3); ctx.setOutput(output); - CalculatedFieldResult result = state.performCalculation(ctx).get(); + TelemetryCalculatedFieldResult result = performCalculation(); assertThat(result).isNotNull(); assertThat(result.getType()).isEqualTo(output.getType()); @@ -196,28 +204,29 @@ public class SimpleCalculatedFieldStateTest { @Test void testIsReadyWhenNotAllArgPresent() { assertThat(state.isReady()).isFalse(); + assertThat(state.getReadinessStatus().errorMsg()).contains(state.getRequiredArguments()); } @Test void testIsReadyWhenAllArgPresent() { - state.arguments = new HashMap<>(Map.of( + state.update(Map.of( "key1", key1ArgEntry, "key2", key2ArgEntry, "key3", key3ArgEntry - )); - + ), ctx); assertThat(state.isReady()).isTrue(); + assertThat(state.getReadinessStatus().errorMsg()).isNull(); } @Test void testIsReadyWhenEmptyEntryPresents() { - state.arguments = new HashMap<>(Map.of( + state.update(Map.of( "key1", key1ArgEntry, - "key2", key2ArgEntry - )); - state.getArguments().put("key3", new SingleValueArgumentEntry()); - + "key2", key2ArgEntry, + "key3", new SingleValueArgumentEntry() + ), ctx); assertThat(state.isReady()).isFalse(); + assertThat(state.getReadinessStatus().errorMsg()).contains("key3"); } private CalculatedField getCalculatedField() { @@ -265,4 +274,8 @@ public class SimpleCalculatedFieldStateTest { return config; } + private TelemetryCalculatedFieldResult performCalculation() throws InterruptedException, ExecutionException { + return (TelemetryCalculatedFieldResult) state.performCalculation(Collections.emptyMap(), ctx).get(); + } + } \ No newline at end of file diff --git a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/SingleValueArgumentEntryTest.java b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/SingleValueArgumentEntryTest.java index 5ea3808468..4ada355054 100644 --- a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/SingleValueArgumentEntryTest.java +++ b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/SingleValueArgumentEntryTest.java @@ -57,6 +57,11 @@ public class SingleValueArgumentEntryTest { assertThat(entry.updateEntry(new SingleValueArgumentEntry(ts, new LongDataEntry("key", 13L), 363L))).isFalse(); } + @Test + void testUpdateEntryWithTheSameTsAndDifferentVersion() { + assertThat(entry.updateEntry(new SingleValueArgumentEntry(ts, new LongDataEntry("key", 13L), 364L))).isTrue(); + } + @Test void testUpdateEntryWhenNewVersionIsNull() { assertThat(entry.updateEntry(new SingleValueArgumentEntry(ts + 16, new LongDataEntry("key", 13L), null))).isTrue(); @@ -115,4 +120,5 @@ public class SingleValueArgumentEntryTest { expectedList.add(Map.of("test2", 20)); assertThat(singleValueArg.getValue()).isEqualTo(expectedList); } + } diff --git a/application/src/test/java/org/thingsboard/server/service/edge/EdgeStatsTest.java b/application/src/test/java/org/thingsboard/server/service/edge/EdgeStatsTest.java index 933318ae59..25ff0f1b5d 100644 --- a/application/src/test/java/org/thingsboard/server/service/edge/EdgeStatsTest.java +++ b/application/src/test/java/org/thingsboard/server/service/edge/EdgeStatsTest.java @@ -33,7 +33,7 @@ import org.thingsboard.server.dao.edge.stats.EdgeStatsCounterService; import org.thingsboard.server.dao.edge.stats.MsgCounters; import org.thingsboard.server.dao.timeseries.TimeseriesService; import org.thingsboard.server.queue.discovery.TopicService; -import org.thingsboard.server.queue.kafka.TbKafkaAdmin; +import org.thingsboard.server.queue.kafka.KafkaAdmin; import org.thingsboard.server.service.edge.stats.EdgeStatsService; import java.util.List; @@ -141,7 +141,7 @@ public class EdgeStatsTest { TopicPartitionInfo partitionInfo = new TopicPartitionInfo(topic, tenantId, 0, false); when(topicService.buildEdgeEventNotificationsTopicPartitionInfo(tenantId, edgeId)).thenReturn(partitionInfo); - TbKafkaAdmin kafkaAdmin = mock(TbKafkaAdmin.class); + KafkaAdmin kafkaAdmin = mock(KafkaAdmin.class); when(kafkaAdmin.getTotalLagForGroupsBulk(Set.of(topic))) .thenReturn(Map.of(topic, 15L)); diff --git a/application/src/test/java/org/thingsboard/server/service/entitiy/EntityServiceTest.java b/application/src/test/java/org/thingsboard/server/service/entitiy/EntityServiceTest.java index 14f90822bf..8e95a68110 100644 --- a/application/src/test/java/org/thingsboard/server/service/entitiy/EntityServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/entitiy/EntityServiceTest.java @@ -79,6 +79,7 @@ import org.thingsboard.server.common.data.query.RelationsQueryFilter; import org.thingsboard.server.common.data.query.SingleEntityFilter; import org.thingsboard.server.common.data.query.StringFilterPredicate; import org.thingsboard.server.common.data.query.StringFilterPredicate.StringOperation; +import org.thingsboard.server.common.data.query.TsValue; import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.EntitySearchDirection; import org.thingsboard.server.common.data.relation.RelationEntityTypeFilter; @@ -118,8 +119,10 @@ import java.util.stream.Collectors; import java.util.stream.Stream; import static org.assertj.core.api.Assertions.assertThat; +import static org.thingsboard.server.common.data.AttributeScope.SERVER_SCOPE; import static org.thingsboard.server.common.data.query.EntityKeyType.ATTRIBUTE; import static org.thingsboard.server.common.data.query.EntityKeyType.ENTITY_FIELD; +import static org.thingsboard.server.common.data.query.EntityKeyType.SERVER_ATTRIBUTE; @Slf4j @DaoSqlTest @@ -604,7 +607,7 @@ public class EntityServiceTest extends AbstractControllerTest { List> attributeFutures = new ArrayList<>(); for (int i = 0; i < assets.size(); i++) { Asset asset = assets.get(i); - attributeFutures.add(saveLongAttribute(asset.getId(), "consumption", consumptions.get(i), AttributeScope.SERVER_SCOPE)); + attributeFutures.add(saveLongAttribute(asset.getId(), "consumption", consumptions.get(i), SERVER_SCOPE)); } Futures.allAsList(attributeFutures).get(); @@ -1745,6 +1748,33 @@ public class EntityServiceTest extends AbstractControllerTest { deviceService.deleteDevicesByTenantId(tenantId); } + @Test + public void testFindTenantTelemetry() { + // save timeseries by sys admin + BasicTsKvEntry timeseries = new BasicTsKvEntry(42L, new DoubleDataEntry("temperature", 45.5)); + timeseriesService.save(TenantId.SYS_TENANT_ID, tenantId, timeseries); + + AttributeKvEntry attr = new BaseAttributeKvEntry(new LongDataEntry("attr", 10L), 42L); + attributesService.save(TenantId.SYS_TENANT_ID, tenantId, SERVER_SCOPE, List.of(attr)); + + SingleEntityFilter singleEntityFilter = new SingleEntityFilter(); + singleEntityFilter.setSingleEntity(AliasEntityId.fromEntityId(tenantId)); + + List entityFields = List.of( + new EntityKey(ENTITY_FIELD, "name") + ); + List latestValues = List.of( + new EntityKey(EntityKeyType.TIME_SERIES, "temperature"), + new EntityKey(EntityKeyType.SERVER_ATTRIBUTE, "attr") + ); + + EntityDataPageLink pageLink = new EntityDataPageLink(1000, 0, null, null); + EntityDataQuery query = new EntityDataQuery(singleEntityFilter, pageLink, entityFields, latestValues, null); + + findByQueryAndCheckTelemetry(query, EntityKeyType.TIME_SERIES, "temperature", List.of("45.5")); + findByQueryAndCheckTelemetry(query, EntityKeyType.SERVER_ATTRIBUTE, "attr", List.of("10")); + } + @Test public void testBuildStringPredicateQueryOperations() throws ExecutionException, InterruptedException { diff --git a/application/src/test/java/org/thingsboard/server/service/entitiy/ai/DefaultTbAiModelServiceTest.java b/application/src/test/java/org/thingsboard/server/service/entitiy/ai/DefaultTbAiModelServiceTest.java index 2321446b44..b6be136f82 100644 --- a/application/src/test/java/org/thingsboard/server/service/entitiy/ai/DefaultTbAiModelServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/entitiy/ai/DefaultTbAiModelServiceTest.java @@ -253,7 +253,7 @@ class DefaultTbAiModelServiceTest { private static AiModelConfig constructValidOpenAiModelConfig() { return OpenAiChatModelConfig.builder() - .providerConfig(new OpenAiProviderConfig("test-api-key")) + .providerConfig(OpenAiProviderConfig.builder().apiKey("test-api-key").build()) .modelId("gpt-4o") .temperature(0.5) .topP(0.3) diff --git a/application/src/test/java/org/thingsboard/server/service/install/update/DefaultDataUpdateServiceTest.java b/application/src/test/java/org/thingsboard/server/service/install/update/DefaultDataUpdateServiceTest.java deleted file mode 100644 index abfab84491..0000000000 --- a/application/src/test/java/org/thingsboard/server/service/install/update/DefaultDataUpdateServiceTest.java +++ /dev/null @@ -1,95 +0,0 @@ -/** - * Copyright © 2016-2025 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.install.update; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.JsonNode; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.mock.mockito.MockBean; -import org.springframework.test.context.ActiveProfiles; -import org.thingsboard.common.util.JacksonUtil; - -import java.io.IOException; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.BDDMockito.willCallRealMethod; - -@ActiveProfiles("install") -@SpringBootTest(classes = DefaultDataUpdateService.class) -class DefaultDataUpdateServiceTest { - - @MockBean - DefaultDataUpdateService service; - - @BeforeEach - void setUp() { - willCallRealMethod().given(service).convertDeviceProfileAlarmRulesForVersion330(any()); - willCallRealMethod().given(service).convertDeviceProfileForVersion330(any()); - } - - JsonNode readFromResource(String resourceName) throws IOException { - return JacksonUtil.OBJECT_MAPPER.readTree(this.getClass().getClassLoader().getResourceAsStream(resourceName)); - } - - @Test - void convertDeviceProfileAlarmRulesForVersion330FirstRun() throws IOException { - JsonNode spec = readFromResource("update/330/device_profile_001_in.json"); - JsonNode expected = readFromResource("update/330/device_profile_001_out.json"); - - assertThat(service.convertDeviceProfileForVersion330(spec.get("profileData"))).isTrue(); - assertThat(spec.toPrettyString()).isEqualTo(expected.toPrettyString()); // use IDE feature - } - - @Test - void convertDeviceProfileAlarmRulesForVersion330SecondRun() throws IOException { - JsonNode spec = readFromResource("update/330/device_profile_001_out.json"); - JsonNode expected = readFromResource("update/330/device_profile_001_out.json"); - - assertThat(service.convertDeviceProfileForVersion330(spec.get("profileData"))).isFalse(); - assertThat(spec.toPrettyString()).isEqualTo(expected.toPrettyString()); // use IDE feature - } - - @Test - void convertDeviceProfileAlarmRulesForVersion330EmptyJson() throws JsonProcessingException { - JsonNode spec = JacksonUtil.toJsonNode("{ }"); - JsonNode expected = JacksonUtil.toJsonNode("{ }"); - - assertThat(service.convertDeviceProfileForVersion330(spec)).isFalse(); - assertThat(spec.toPrettyString()).isEqualTo(expected.toPrettyString()); - } - - @Test - void convertDeviceProfileAlarmRulesForVersion330AlarmNodeNull() throws JsonProcessingException { - JsonNode spec = JacksonUtil.toJsonNode("{ \"alarms\" : null }"); - JsonNode expected = JacksonUtil.toJsonNode("{ \"alarms\" : null }"); - - assertThat(service.convertDeviceProfileForVersion330(spec)).isFalse(); - assertThat(spec.toPrettyString()).isEqualTo(expected.toPrettyString()); - } - - @Test - void convertDeviceProfileAlarmRulesForVersion330NoAlarmNode() throws JsonProcessingException { - JsonNode spec = JacksonUtil.toJsonNode("{ \"configuration\": { \"type\": \"DEFAULT\" } }"); - JsonNode expected = JacksonUtil.toJsonNode("{ \"configuration\": { \"type\": \"DEFAULT\" } }"); - - assertThat(service.convertDeviceProfileForVersion330(spec)).isFalse(); - assertThat(spec.toPrettyString()).isEqualTo(expected.toPrettyString()); - } - -} diff --git a/application/src/test/java/org/thingsboard/server/service/job/JobManagerTest.java b/application/src/test/java/org/thingsboard/server/service/job/JobManagerTest.java index 0069310b5d..7806215a51 100644 --- a/application/src/test/java/org/thingsboard/server/service/job/JobManagerTest.java +++ b/application/src/test/java/org/thingsboard/server/service/job/JobManagerTest.java @@ -98,7 +98,7 @@ public class JobManagerTest extends AbstractControllerTest { await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> { Job job = findJobById(jobId); assertThat(job.getStatus()).isEqualTo(JobStatus.RUNNING); - assertThat(job.getResult().getSuccessfulCount()).isBetween(1, tasksCount - 1); + assertThat(job.getResult().getSuccessfulCount()).isBetween(0, tasksCount - 1); assertThat(job.getResult().getTotalCount()).isEqualTo(tasksCount); }); await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> { diff --git a/application/src/test/java/org/thingsboard/server/service/job/JobManagerTest_EntityPartitioningStrategy.java b/application/src/test/java/org/thingsboard/server/service/job/JobManagerTest_EntityPartitioningStrategy.java index 59093ad802..c1b8f911ff 100644 --- a/application/src/test/java/org/thingsboard/server/service/job/JobManagerTest_EntityPartitioningStrategy.java +++ b/application/src/test/java/org/thingsboard/server/service/job/JobManagerTest_EntityPartitioningStrategy.java @@ -27,17 +27,16 @@ import org.thingsboard.server.dao.service.DaoSqlTest; public class JobManagerTest_EntityPartitioningStrategy extends JobManagerTest { /* - * Some tests are overridden because they are based on - * tenant partitioning strategy (subsequent tasks processing within a tenant) - * */ + * Some tests are overridden because they are based on + * tenant partitioning strategy (subsequent tasks processing within a tenant) + * */ @Override - public void testCancelJob_simulateTaskProcessorRestart() throws Exception { + public void testCancelJob_simulateTaskProcessorRestart() { } @Override public void testSubmitJob_generalError() { - } } diff --git a/application/src/test/java/org/thingsboard/server/service/notification/NotificationRuleApiTest.java b/application/src/test/java/org/thingsboard/server/service/notification/NotificationRuleApiTest.java index bab70ea505..ff49a68d66 100644 --- a/application/src/test/java/org/thingsboard/server/service/notification/NotificationRuleApiTest.java +++ b/application/src/test/java/org/thingsboard/server/service/notification/NotificationRuleApiTest.java @@ -22,12 +22,12 @@ import org.junit.Before; import org.junit.Test; import org.junit.function.ThrowingRunnable; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.mock.mockito.SpyBean; import org.springframework.data.util.Pair; import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.bean.override.mockito.MockitoSpyBean; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.cache.limits.RateLimitService; -import org.thingsboard.server.common.data.DataConstants; +import org.thingsboard.server.common.data.AttributeScope; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.EntityType; @@ -39,17 +39,19 @@ import org.thingsboard.server.common.data.alarm.AlarmCommentType; import org.thingsboard.server.common.data.alarm.AlarmSearchStatus; import org.thingsboard.server.common.data.alarm.AlarmSeverity; import org.thingsboard.server.common.data.alarm.AlarmStatus; +import org.thingsboard.server.common.data.alarm.rule.AlarmRule; +import org.thingsboard.server.common.data.alarm.rule.condition.SimpleAlarmCondition; +import org.thingsboard.server.common.data.alarm.rule.condition.expression.TbelAlarmConditionExpression; import org.thingsboard.server.common.data.asset.Asset; +import org.thingsboard.server.common.data.cf.CalculatedField; +import org.thingsboard.server.common.data.cf.CalculatedFieldType; +import org.thingsboard.server.common.data.cf.configuration.AlarmCalculatedFieldConfiguration; +import org.thingsboard.server.common.data.cf.configuration.Argument; +import org.thingsboard.server.common.data.cf.configuration.ArgumentType; +import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey; import org.thingsboard.server.common.data.device.data.DefaultDeviceConfiguration; import org.thingsboard.server.common.data.device.data.DefaultDeviceTransportConfiguration; import org.thingsboard.server.common.data.device.data.DeviceData; -import org.thingsboard.server.common.data.device.profile.AlarmCondition; -import org.thingsboard.server.common.data.device.profile.AlarmConditionFilter; -import org.thingsboard.server.common.data.device.profile.AlarmConditionFilterKey; -import org.thingsboard.server.common.data.device.profile.AlarmConditionKeyType; -import org.thingsboard.server.common.data.device.profile.AlarmRule; -import org.thingsboard.server.common.data.device.profile.DeviceProfileAlarm; -import org.thingsboard.server.common.data.device.profile.SimpleAlarmConditionSpec; import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.id.AlarmId; import org.thingsboard.server.common.data.id.DeviceId; @@ -87,9 +89,6 @@ import org.thingsboard.server.common.data.notification.targets.platform.SystemAd import org.thingsboard.server.common.data.notification.template.NotificationTemplate; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; -import org.thingsboard.server.common.data.query.BooleanFilterPredicate; -import org.thingsboard.server.common.data.query.EntityKeyValueType; -import org.thingsboard.server.common.data.query.FilterPredicateValue; import org.thingsboard.server.common.data.rule.RuleChain; import org.thingsboard.server.common.data.rule.RuleChainMetaData; import org.thingsboard.server.common.data.security.Authority; @@ -106,12 +105,10 @@ import org.thingsboard.server.service.system.DefaultSystemInfoService; import org.thingsboard.server.service.telemetry.AlarmSubscriptionService; import java.lang.reflect.Method; -import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; -import java.util.TreeMap; import java.util.UUID; import java.util.concurrent.Callable; import java.util.concurrent.TimeUnit; @@ -137,7 +134,7 @@ import static org.thingsboard.server.common.data.notification.rule.trigger.confi }) public class NotificationRuleApiTest extends AbstractNotificationApiTest { - @SpyBean + @MockitoSpyBean private AlarmSubscriptionService alarmSubscriptionService; @Autowired private DefaultSystemInfoService systemInfoService; @@ -193,7 +190,7 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest { @Test public void testNotificationRuleProcessing_alarmTrigger() throws Exception { String notificationSubject = "Alarm type: ${alarmType}, status: ${alarmStatus}, " + - "severity: ${alarmSeverity}, deviceId: ${alarmOriginatorId}"; + "severity: ${alarmSeverity}, deviceId: ${alarmOriginatorId}, details: ${details.data}."; String notificationText = "Status: ${alarmStatus}, severity: ${alarmSeverity}"; NotificationTemplate notificationTemplate = createNotificationTemplate(NotificationType.ALARM, notificationSubject, notificationText, NotificationDeliveryMethod.WEB); @@ -221,12 +218,12 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest { clients.put(delay, userAndClient.getSecond()); } notificationRule.setRecipientsConfig(recipientsConfig); - notificationRule = saveNotificationRule(notificationRule); + saveNotificationRule(notificationRule); String alarmType = "myBoolIsTrue"; DeviceProfile deviceProfile = createDeviceProfileWithAlarmRules(alarmType); - Device device = createDevice("Device 1", deviceProfile.getName(), "1234"); + Device device = createDevice("Device 1", deviceProfile.getName(), "label", "1234"); clients.values().forEach(wsClient -> { wsClient.subscribeForUnreadNotifications(10).waitForReply(true); @@ -234,8 +231,8 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest { }); JsonNode attr = JacksonUtil.newObjectNode() - .set("bool", BooleanNode.TRUE); - doPost("/api/plugins/telemetry/" + device.getId() + "/" + DataConstants.SHARED_SCOPE, attr); + .set("createAlarm", BooleanNode.TRUE); + postAttributes(device.getId(), AttributeScope.SERVER_SCOPE, attr.toString()); await().atMost(10, TimeUnit.SECONDS) .until(() -> alarmSubscriptionService.findLatestByOriginatorAndType(tenantId, device.getId(), alarmType) != null); @@ -250,7 +247,7 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest { assertThat(actualDelay).isCloseTo(expectedDelay, offset(2.0)); assertThat(notification.getSubject()).isEqualTo("Alarm type: " + alarmType + ", status: " + AlarmStatus.ACTIVE_UNACK + ", " + - "severity: " + AlarmSeverity.CRITICAL.toString().toLowerCase() + ", deviceId: " + device.getId()); + "severity: " + AlarmSeverity.CRITICAL.toString().toLowerCase() + ", deviceId: " + device.getId() + ", details: attribute is true."); assertThat(notification.getText()).isEqualTo("Status: " + AlarmStatus.ACTIVE_UNACK + ", severity: " + AlarmSeverity.CRITICAL.toString().toLowerCase()); assertThat(notification.getType()).isEqualTo(NotificationType.ALARM); @@ -270,7 +267,7 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest { wsClient.waitForUpdate(true); Notification updatedNotification = wsClient.getLastDataUpdate().getUpdate(); assertThat(updatedNotification.getSubject()).isEqualTo("Alarm type: " + alarmType + ", status: " + expectedStatus + ", " + - "severity: " + expectedSeverity.toString().toLowerCase() + ", deviceId: " + device.getId()); + "severity: " + expectedSeverity.toString().toLowerCase() + ", deviceId: " + device.getId() + ", details: attribute is true."); assertThat(updatedNotification.getText()).isEqualTo("Status: " + expectedStatus + ", severity: " + expectedSeverity.toString().toLowerCase()); wsClient.close(); @@ -341,8 +338,8 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest { getWsClient().subscribeForUnreadNotifications(10).waitForReply(true); getWsClient().registerWaitForUpdate(); JsonNode attr = JacksonUtil.newObjectNode() - .set("bool", BooleanNode.TRUE); - doPost("/api/plugins/telemetry/" + device.getId() + "/" + DataConstants.SHARED_SCOPE, attr); + .set("createAlarm", BooleanNode.TRUE); + postAttributes(device.getId(), AttributeScope.SERVER_SCOPE, attr.toString()); await().atMost(10, TimeUnit.SECONDS) .until(() -> alarmSubscriptionService.findLatestByOriginatorAndType(tenantId, device.getId(), alarmType) != null); @@ -516,10 +513,10 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest { .notifyOn(Set.of(ASSIGNED, UNASSIGNED)) .build(); NotificationTarget target = createNotificationTarget(tenantAdminUserId); - String template = "${userEmail} ${action} alarm on ${alarmOriginatorEntityType} '${alarmOriginatorName}'. Assignee: ${assigneeEmail}"; + String template = "${userEmail} ${action} alarm on ${alarmOriginatorEntityType} '${alarmOriginatorName}' with label '${alarmOriginatorLabel}'. Assignee: ${assigneeEmail}"; createNotificationRule(triggerConfig, "Test", template, target.getId()); - Device device = createDevice("Device A", "123"); + Device device = createDevice("Device A", "default", "test", "123"); Alarm alarm = Alarm.builder() .tenantId(tenantId) .originator(device.getId()) @@ -536,7 +533,7 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest { doPost("/api/alarm/" + alarmId + "/assign/" + tenantAdminUserId).andExpect(status().isOk()); }, notification -> { assertThat(notification.getText()).isEqualTo( - TENANT_ADMIN_EMAIL + " assigned alarm on Device 'Device A'. Assignee: " + TENANT_ADMIN_EMAIL + TENANT_ADMIN_EMAIL + " assigned alarm on Device 'Device A' with label 'test'. Assignee: " + TENANT_ADMIN_EMAIL ); }); @@ -544,7 +541,7 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest { doDelete("/api/alarm/" + alarmId + "/assign").andExpect(status().isOk()); }, notification -> { assertThat(notification.getText()).isEqualTo( - TENANT_ADMIN_EMAIL + " unassigned alarm on Device 'Device A'. Assignee: " + TENANT_ADMIN_EMAIL + " unassigned alarm on Device 'Device A' with label 'test'. Assignee: " ); }); } @@ -944,35 +941,28 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest { private DeviceProfile createDeviceProfileWithAlarmRules(String alarmType) { DeviceProfile deviceProfile = createDeviceProfile("For notification rule test"); deviceProfile.setTenantId(tenantId); + deviceProfile = doPost("/api/deviceProfile", deviceProfile, DeviceProfile.class); - List alarms = new ArrayList<>(); - DeviceProfileAlarm alarm = new DeviceProfileAlarm(); - alarm.setAlarmType(alarmType); - alarm.setId(alarmType); + CalculatedField alarmCf = new CalculatedField(); + alarmCf.setType(CalculatedFieldType.ALARM); + alarmCf.setEntityId(deviceProfile.getId()); + alarmCf.setName(alarmType); + AlarmCalculatedFieldConfiguration configuration = new AlarmCalculatedFieldConfiguration(); + Argument argument = new Argument(); + argument.setRefEntityKey(new ReferencedEntityKey("createAlarm", ArgumentType.ATTRIBUTE, AttributeScope.SERVER_SCOPE)); + configuration.setArguments(Map.of("createAlarm", argument)); AlarmRule alarmRule = new AlarmRule(); - alarmRule.setAlarmDetails("Details"); - AlarmCondition alarmCondition = new AlarmCondition(); - alarmCondition.setSpec(new SimpleAlarmConditionSpec()); - List condition = new ArrayList<>(); - - AlarmConditionFilter alarmConditionFilter = new AlarmConditionFilter(); - alarmConditionFilter.setKey(new AlarmConditionFilterKey(AlarmConditionKeyType.ATTRIBUTE, "bool")); - BooleanFilterPredicate predicate = new BooleanFilterPredicate(); - predicate.setOperation(BooleanFilterPredicate.BooleanOperation.EQUAL); - predicate.setValue(new FilterPredicateValue<>(true)); - - alarmConditionFilter.setPredicate(predicate); - alarmConditionFilter.setValueType(EntityKeyValueType.BOOLEAN); - condition.add(alarmConditionFilter); - alarmCondition.setCondition(condition); - alarmRule.setCondition(alarmCondition); - TreeMap createRules = new TreeMap<>(); - createRules.put(AlarmSeverity.CRITICAL, alarmRule); - alarm.setCreateRules(createRules); - alarms.add(alarm); - - deviceProfile.getProfileData().setAlarms(alarms); - deviceProfile = doPost("/api/deviceProfile", deviceProfile, DeviceProfile.class); + alarmRule.setAlarmDetails("attribute is ${createAlarm}"); + SimpleAlarmCondition condition = new SimpleAlarmCondition(); + TbelAlarmConditionExpression expression = new TbelAlarmConditionExpression(); + expression.setExpression("return createAlarm == true;"); + condition.setExpression(expression); + alarmRule.setCondition(condition); + configuration.setCreateRules(Map.of( + AlarmSeverity.CRITICAL, alarmRule + )); + alarmCf.setConfiguration(configuration); + saveCalculatedField(alarmCf); return deviceProfile; } diff --git a/application/src/test/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineQueueConsumerManagerTest.java b/application/src/test/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineQueueConsumerManagerTest.java index 09bd02e5a4..ace27c08c1 100644 --- a/application/src/test/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineQueueConsumerManagerTest.java +++ b/application/src/test/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineQueueConsumerManagerTest.java @@ -507,7 +507,7 @@ public class TbRuleEngineQueueConsumerManagerTest { consumerManager.delete(true); - await().atMost(2, TimeUnit.SECONDS) + await().atMost(5, TimeUnit.SECONDS) .untilAsserted(() -> { verify(ruleEngineMsgProducer).send(any(), any(), any()); }); @@ -595,7 +595,7 @@ public class TbRuleEngineQueueConsumerManagerTest { await().atMost(5, TimeUnit.SECONDS).until(() -> { for (TopicPartitionInfo partition : expectedPartitions) { if (consumers.stream().noneMatch(consumer -> consumer.subscribed && - consumer.pollingStarted && Set.of(partition).equals(consumer.getPartitions()))) { + consumer.pollingStarted && Set.of(partition).equals(consumer.getPartitions()))) { return false; } } @@ -605,7 +605,7 @@ public class TbRuleEngineQueueConsumerManagerTest { await().atMost(5, TimeUnit.SECONDS).until(() -> { return consumers.size() == 1 && consumers.stream() .anyMatch(consumer -> consumer.subscribed && consumer.pollingStarted && - expectedPartitions.equals(consumer.getPartitions())); + expectedPartitions.equals(consumer.getPartitions())); }); } Mockito.reset(ruleEngineConsumerContext.getSubmitStrategyFactory()); @@ -667,8 +667,8 @@ public class TbRuleEngineQueueConsumerManagerTest { return await().atMost(5, TimeUnit.SECONDS) .until(() -> consumers.stream() .filter(consumer -> consumer.getPartitions() != null && - consumer.getPartitions().size() == 1 && - consumer.getPartitions().contains(tpi)) + consumer.getPartitions().size() == 1 && + consumer.getPartitions().contains(tpi)) .findFirst().orElse(null), Objects::nonNull); } @@ -676,9 +676,9 @@ public class TbRuleEngineQueueConsumerManagerTest { return await().atMost(5, TimeUnit.SECONDS) .until(() -> consumers.stream() .filter(consumer -> consumer.getPartitions() != null && - consumer.getPartitions().size() == 1 && - consumer.getPartitions().stream() - .anyMatch(tpi -> tpi.getPartition().get().equals(partition))) + consumer.getPartitions().size() == 1 && + consumer.getPartitions().stream() + .anyMatch(tpi -> tpi.getPartition().get().equals(partition))) .findFirst().orElse(null), Objects::nonNull); } @@ -778,10 +778,6 @@ public class TbRuleEngineQueueConsumerManagerTest { return false; } - public Set getPartitions() { - return partitions; - } - public void setUpTestMsg() { testMsg = TbMsg.newMsg() .type(TbMsgType.POST_TELEMETRY_REQUEST) @@ -790,6 +786,7 @@ public class TbRuleEngineQueueConsumerManagerTest { .data("{}") .build(); } + } } diff --git a/application/src/test/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineStrategyTest.java b/application/src/test/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineStrategyTest.java index 1106fad5b6..9bd9bb2e7a 100644 --- a/application/src/test/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineStrategyTest.java +++ b/application/src/test/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineStrategyTest.java @@ -43,6 +43,7 @@ import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg; import org.thingsboard.server.queue.TbQueueConsumer; import org.thingsboard.server.queue.common.TbProtoQueueMsg; +import org.thingsboard.server.queue.common.consumer.TbQueueConsumerTask.ConsumerKey; import org.thingsboard.server.queue.discovery.QueueKey; import org.thingsboard.server.service.queue.processing.TbRuleEngineProcessingStrategyFactory; import org.thingsboard.server.service.queue.processing.TbRuleEngineSubmitStrategyFactory; @@ -191,6 +192,7 @@ public class TbRuleEngineStrategyTest { queue.setProcessingStrategy(processingStrategy); QueueKey queueKey = new QueueKey(ServiceType.TB_RULE_ENGINE, queue); + ConsumerKey consumerKey = new ConsumerKey(queueKey, null); var consumerManager = TbRuleEngineQueueConsumerManager.create() .ctx(ruleEngineConsumerContext) .queueKey(queueKey) @@ -238,7 +240,7 @@ public class TbRuleEngineStrategyTest { .map(this::toProto) .toList(); - consumerManager.processMsgs(protoMsgs, consumer, queueKey, queue); + consumerManager.processMsgs(protoMsgs, consumer, consumerKey, queue); processingData.forEach(data -> { verify(actorContext, times(data.attempts)).tell(argThat(msg -> diff --git a/application/src/test/java/org/thingsboard/server/service/resource/DefaultResourceDataCacheTest.java b/application/src/test/java/org/thingsboard/server/service/resource/DefaultResourceDataCacheTest.java new file mode 100644 index 0000000000..f12a8d3c5d --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/service/resource/DefaultResourceDataCacheTest.java @@ -0,0 +1,83 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.resource; + +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.bean.override.mockito.MockitoSpyBean; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.GeneralFileDescriptor; +import org.thingsboard.server.common.data.ResourceType; +import org.thingsboard.server.common.data.TbResource; +import org.thingsboard.server.common.data.TbResourceDataInfo; +import org.thingsboard.server.common.data.TbResourceInfo; +import org.thingsboard.server.controller.AbstractControllerTest; +import org.thingsboard.server.dao.resource.ResourceService; +import org.thingsboard.server.dao.resource.TbResourceDataCache; +import org.thingsboard.server.dao.service.DaoSqlTest; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.clearInvocations; +import static org.mockito.Mockito.timeout; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; + +@DaoSqlTest +public class DefaultResourceDataCacheTest extends AbstractControllerTest { + + @MockitoSpyBean + private ResourceService resourceService; + @Autowired + private TbResourceService tbResourceService; + @MockitoSpyBean + private TbResourceDataCache resourceDataCache; + + @Test + public void testGetCachedResourceData() throws Exception { + loginTenantAdmin(); + + TbResource resource = new TbResource(); + resource.setTenantId(tenantId); + resource.setTitle("File for AI request"); + resource.setResourceType(ResourceType.GENERAL); + resource.setFileName("myTestJson.json"); + GeneralFileDescriptor descriptor = new GeneralFileDescriptor("application/json"); + resource.setDescriptorValue(descriptor); + byte[] data = "This is a test prompt for AI request.".getBytes(); + resource.setData(data); + TbResourceInfo savedResource = tbResourceService.save(resource); + verify(resourceDataCache, timeout(2000).times(1)).evictResourceData(tenantId, savedResource.getId()); + + TbResourceDataInfo cachedData = resourceDataCache.getResourceDataInfoAsync(tenantId, savedResource.getId()).get(); + assertThat(cachedData.getData()).isEqualTo(data); + assertThat(JacksonUtil.treeToValue(cachedData.getDescriptor(), GeneralFileDescriptor.class)).isEqualTo(descriptor); + verify(resourceService).getResourceDataInfo(tenantId, savedResource.getId()); + + // retrieve resource data second time + clearInvocations(resourceService); + TbResourceDataInfo cachedData2 = resourceDataCache.getResourceDataInfoAsync(tenantId, savedResource.getId()).get(); + assertThat(cachedData2.getData()).isEqualTo(data); + verifyNoMoreInteractions(resourceService); + + // delete resource, check cache + TbResource resourceById = resourceService.findResourceById(tenantId, savedResource.getId()); + tbResourceService.delete(resourceById, true, null); + verify(resourceDataCache, timeout(2000).times(2)).evictResourceData(tenantId, savedResource.getId()); + TbResourceDataInfo cachedDataAfterDeletion = resourceDataCache.getResourceDataInfoAsync(tenantId, savedResource.getId()).get(); + assertThat(cachedDataAfterDeletion).isEqualTo(null); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/service/resource/sql/BaseTbResourceServiceTest.java b/application/src/test/java/org/thingsboard/server/service/resource/sql/BaseTbResourceServiceTest.java index fe416eacd5..a146730465 100644 --- a/application/src/test/java/org/thingsboard/server/service/resource/sql/BaseTbResourceServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/resource/sql/BaseTbResourceServiceTest.java @@ -17,6 +17,7 @@ package org.thingsboard.server.service.resource.sql; import com.datastax.oss.driver.api.core.uuid.Uuids; import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.databind.node.TextNode; import org.junit.After; import org.junit.Assert; import org.junit.Before; @@ -24,8 +25,10 @@ import org.junit.Test; import org.junit.jupiter.api.Assertions; import org.springframework.beans.factory.annotation.Autowired; import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.rule.engine.ai.TbAiNode; +import org.thingsboard.rule.engine.ai.TbAiNodeConfiguration; +import org.thingsboard.rule.engine.ai.TbResponseFormat; import org.thingsboard.server.common.data.Dashboard; -import org.thingsboard.server.common.data.DashboardInfo; import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.EntityInfo; import org.thingsboard.server.common.data.EntityType; @@ -37,26 +40,40 @@ import org.thingsboard.server.common.data.TbResourceInfoFilter; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.ai.AiModel; +import org.thingsboard.server.common.data.ai.model.chat.OpenAiChatModelConfig; +import org.thingsboard.server.common.data.ai.provider.OpenAiProviderConfig; +import org.thingsboard.server.common.data.debug.DebugSettings; import org.thingsboard.server.common.data.exception.ThingsboardException; +import org.thingsboard.server.common.data.id.RuleChainId; +import org.thingsboard.server.common.data.id.TbResourceId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.common.data.rule.RuleChain; +import org.thingsboard.server.common.data.rule.RuleChainMetaData; +import org.thingsboard.server.common.data.rule.RuleChainType; +import org.thingsboard.server.common.data.rule.RuleNode; import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; import org.thingsboard.server.common.data.widget.WidgetTypeDetails; -import org.thingsboard.server.common.data.widget.WidgetTypeInfo; import org.thingsboard.server.controller.AbstractControllerTest; +import org.thingsboard.server.dao.ai.AiModelService; import org.thingsboard.server.dao.dashboard.DashboardService; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.resource.ResourceService; +import org.thingsboard.server.dao.rule.RuleChainService; import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.dao.widget.WidgetTypeService; import org.thingsboard.server.service.resource.TbResourceService; import java.util.ArrayList; +import java.util.Arrays; import java.util.Base64; import java.util.Collections; import java.util.List; +import java.util.Set; +import java.util.function.Function; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; @@ -135,6 +152,10 @@ public class BaseTbResourceServiceTest extends AbstractControllerTest { private WidgetTypeService widgetTypeService; @Autowired private DashboardService dashboardService; + @Autowired + private RuleChainService ruleChainService; + @Autowired + private AiModelService aiModelService; private Tenant savedTenant; private User tenantAdmin; @@ -453,11 +474,9 @@ public class BaseTbResourceServiceTest extends AbstractControllerTest { Assert.assertFalse(result.getReferences().isEmpty()); Assert.assertEquals(1, result.getReferences().size()); - WidgetTypeInfo widgetTypeInfo = (WidgetTypeInfo) result.getReferences().get(EntityType.WIDGET_TYPE.name()).get(0); - WidgetTypeInfo foundWidgetTypeInfo = new WidgetTypeInfo(foundWidgetType); + EntityInfo widgetTypeInfo = (EntityInfo) result.getReferences().get(EntityType.WIDGET_TYPE.name()).get(0); Assert.assertNotNull(widgetTypeInfo); - Assert.assertNotNull(foundWidgetTypeInfo); - Assert.assertEquals(widgetTypeInfo, foundWidgetTypeInfo); + Assert.assertEquals(widgetTypeInfo, new EntityInfo(foundWidgetType.getId(), foundWidgetType.getName())); TbResourceInfo foundResourceInfo = resourceService.findResourceInfoById(savedTenant.getId(), savedResource.getId()); Assert.assertNotNull(foundResource); @@ -546,11 +565,9 @@ public class BaseTbResourceServiceTest extends AbstractControllerTest { Assert.assertNotNull(result.getReferences()); Assert.assertEquals(1, result.getReferences().size()); - DashboardInfo dashboardInfo = (DashboardInfo) result.getReferences().get(EntityType.DASHBOARD.name()).get(0); - DashboardInfo foundDashboardInfo = dashboardService.findDashboardInfoById(savedTenant.getId(), savedDashboard.getId()); + EntityInfo dashboardInfo = (EntityInfo) result.getReferences().get(EntityType.DASHBOARD.name()).get(0); Assert.assertNotNull(dashboardInfo); - Assert.assertNotNull(foundDashboardInfo); - Assert.assertEquals(foundDashboardInfo, dashboardInfo); + Assert.assertEquals(new EntityInfo(savedDashboard.getId(), savedDashboard.getName()), dashboardInfo); foundResource = resourceService.findResourceById(savedTenant.getId(), savedResource.getId()); Assert.assertNotNull(foundResource); @@ -598,6 +615,94 @@ public class BaseTbResourceServiceTest extends AbstractControllerTest { Assert.assertNull(foundResource); } + @Test + public void testShouldNotDeleteResourceIfUsedInAiNode() throws Exception { + TbResource resource = new TbResource(); + resource.setResourceType(ResourceType.GENERAL); + resource.setTitle("My resource"); + resource.setFileName("test.json"); + resource.setTenantId(savedTenant.getId()); + resource.setData("".getBytes()); + TbResourceInfo savedResource = tbResourceService.save(resource); + RuleChainMetaData ruleChain = createRuleChainReferringResource(savedResource.getId()); + + TbResourceDeleteResult result = tbResourceService.delete(savedResource, false, null); + assertThat(result).isNotNull(); + assertThat(result.isSuccess()).isFalse(); + assertThat(result.getReferences()).isNotEmpty().hasSize(1); + EntityInfo entityInfo = (EntityInfo) result.getReferences().get(EntityType.RULE_CHAIN.name()).get(0); + assertThat(entityInfo).isEqualTo(new EntityInfo(ruleChain.getRuleChainId(), "Test")); + + TbResource foundResource = resourceService.findResourceById(savedTenant.getId(), savedResource.getId()); + assertThat(foundResource).isNotNull(); + + // force delete + TbResourceDeleteResult deleteResult = tbResourceService.delete(savedResource, true, null); + assertThat(deleteResult).isNotNull(); + assertThat(deleteResult.isSuccess()).isTrue(); + + TbResource resourceAfterDeletion = resourceService.findResourceById(savedTenant.getId(), savedResource.getId()); + assertThat(resourceAfterDeletion).isNull(); + } + + private RuleChainMetaData createRuleChainReferringResource(TbResourceId resourceId) { + AiModel model = constructValidOpenAiModel("Test model"); + AiModel saved = aiModelService.save(model); + + RuleChain ruleChain = new RuleChain(); + ruleChain.setTenantId(tenantId); + ruleChain.setName("Test"); + ruleChain.setType(RuleChainType.CORE); + ruleChain.setDebugMode(true); + ruleChain.setConfiguration(JacksonUtil.newObjectNode().set("a", new TextNode("b"))); + ruleChain = ruleChainService.saveRuleChain(ruleChain); + RuleChainId ruleChainId = ruleChain.getId(); + + RuleChainMetaData metaData = new RuleChainMetaData(); + metaData.setRuleChainId(ruleChainId); + + RuleNode aiNode = new RuleNode(); + aiNode.setName("Ai request"); + aiNode.setType(org.thingsboard.rule.engine.ai.TbAiNode.class.getName()); + aiNode.setConfigurationVersion(TbAiNode.class.getAnnotation(org.thingsboard.rule.engine.api.RuleNode.class).version()); + aiNode.setDebugSettings(DebugSettings.all()); + TbAiNodeConfiguration configuration = new TbAiNodeConfiguration(); + configuration.setResourceIds(Set.of(resourceId.getId())); + configuration.setModelId(saved.getId()); + configuration.setResponseFormat(new TbResponseFormat.TbJsonResponseFormat()); + configuration.setTimeoutSeconds(1); + configuration.setUserPrompt("What is temp"); + aiNode.setConfiguration(JacksonUtil.valueToTree(configuration)); + + metaData.setNodes(Arrays.asList(aiNode)); + metaData.setFirstNodeIndex(0); + ruleChainService.saveRuleChainMetaData(tenantId, metaData, Function.identity()); + return ruleChainService.loadRuleChainMetaData(tenantId, ruleChainId); + } + + private AiModel constructValidOpenAiModel(String name) { + var modelConfig = OpenAiChatModelConfig.builder() + .providerConfig(OpenAiProviderConfig.builder() + .baseUrl(OpenAiProviderConfig.OPENAI_OFFICIAL_BASE_URL) + .apiKey("test-api-key") + .build()) + .modelId("gpt-4o") + .temperature(0.5) + .topP(0.3) + .frequencyPenalty(0.1) + .presencePenalty(0.2) + .maxOutputTokens(1000) + .timeoutSeconds(60) + .maxRetries(2) + .build(); + + return AiModel.builder() + .tenantId(tenantId) + .name(name) + .configuration(modelConfig) + .build(); + } + @Test public void testFindTenantResourcesByTenantId() throws Exception { loginSysAdmin(); diff --git a/application/src/test/java/org/thingsboard/server/service/security/auth/JwtTokenFactoryTest.java b/application/src/test/java/org/thingsboard/server/service/security/auth/JwtTokenFactoryTest.java index 3eeab8b7d9..440d87d768 100644 --- a/application/src/test/java/org/thingsboard/server/service/security/auth/JwtTokenFactoryTest.java +++ b/application/src/test/java/org/thingsboard/server/service/security/auth/JwtTokenFactoryTest.java @@ -125,7 +125,7 @@ public class JwtTokenFactoryTest { public void testCreateAndParsePreVerificationJwtToken() { SecurityUser securityUser = createSecurityUser(); int tokenLifetime = (int) TimeUnit.MINUTES.toSeconds(30); - JwtToken preVerificationToken = tokenFactory.createPreVerificationToken(securityUser, tokenLifetime); + JwtToken preVerificationToken = tokenFactory.createMfaToken(securityUser, Authority.PRE_VERIFICATION_TOKEN, tokenLifetime); checkExpirationTime(preVerificationToken, tokenLifetime); SecurityUser parsedSecurityUser = tokenFactory.parseAccessJwtToken(preVerificationToken.getToken()); diff --git a/application/src/test/java/org/thingsboard/server/service/sync/vc/VersionControlTest.java b/application/src/test/java/org/thingsboard/server/service/sync/vc/VersionControlTest.java index 461ca5a2ec..73f67739f3 100644 --- a/application/src/test/java/org/thingsboard/server/service/sync/vc/VersionControlTest.java +++ b/application/src/test/java/org/thingsboard/server/service/sync/vc/VersionControlTest.java @@ -244,7 +244,7 @@ public class VersionControlTest extends AbstractControllerTest { DeviceProfile deviceProfile = createDeviceProfile(null, null, "Device profile v1.0"); OtaPackage firmware = createOtaPackage(tenantId1, deviceProfile.getId(), OtaPackageType.FIRMWARE); OtaPackage software = createOtaPackage(tenantId1, deviceProfile.getId(), OtaPackageType.SOFTWARE); - Device device = createDevice(null, deviceProfile.getId(), "Device v1.0", "test1", newDevice -> { + Device device = createDevice(deviceProfile.getId(), "Device v1.0", "test1", newDevice -> { newDevice.setFirmwareId(firmware.getId()); newDevice.setSoftwareId(software.getId()); }); @@ -267,7 +267,7 @@ public class VersionControlTest extends AbstractControllerTest { createVersion("profiles", EntityType.DEVICE_PROFILE); OtaPackage firmware = createOtaPackage(tenantId1, deviceProfile.getId(), OtaPackageType.FIRMWARE); OtaPackage software = createOtaPackage(tenantId1, deviceProfile.getId(), OtaPackageType.SOFTWARE); - Device device = createDevice(null, deviceProfile.getId(), "Device of tenant 1", "test1", newDevice -> { + Device device = createDevice(deviceProfile.getId(), "Device of tenant 1", "test1", newDevice -> { newDevice.setFirmwareId(firmware.getId()); newDevice.setSoftwareId(software.getId()); }); @@ -528,7 +528,7 @@ public class VersionControlTest extends AbstractControllerTest { @Test public void testVcWithRelations_betweenTenants() throws Exception { Asset asset = createAsset(null, null, "Asset 1"); - Device device = createDevice(null, null, "Device 1", "test1"); + Device device = createDevice("Device 1", "test1"); EntityRelation relation = createRelation(asset.getId(), device.getId()); String versionId = createVersion("assets and devices", EntityType.ASSET, EntityType.DEVICE, EntityType.DEVICE_PROFILE, EntityType.ASSET_PROFILE); @@ -554,11 +554,11 @@ public class VersionControlTest extends AbstractControllerTest { @Test public void testVcWithRelations_sameTenant() throws Exception { Asset asset = createAsset(null, null, "Asset 1"); - Device device1 = createDevice(null, null, "Device 1", "test1"); + Device device1 = createDevice("Device 1", "test1"); EntityRelation relation1 = createRelation(device1.getId(), asset.getId()); String versionId = createVersion("assets", EntityType.ASSET); - Device device2 = createDevice(null, null, "Device 2", "test2"); + Device device2 = createDevice("Device 2", "test2"); EntityRelation relation2 = createRelation(device2.getId(), asset.getId()); List relations = findRelationsByTo(asset.getId()); assertThat(relations).contains(relation1, relation2); @@ -591,7 +591,7 @@ public class VersionControlTest extends AbstractControllerTest { @Test public void testVcWithCalculatedFields_betweenTenants() throws Exception { Asset asset = createAsset(null, null, "Asset 1"); - Device device = createDevice(null, null, "Device 1", "test1"); + Device device = createDevice("Device 1", "test1"); CalculatedField calculatedField = createCalculatedField("CalculatedField1", device.getId(), asset.getId()); String versionId = createVersion("calculated fields of asset and device", EntityType.ASSET, EntityType.DEVICE, EntityType.DEVICE_PROFILE, EntityType.ASSET_PROFILE); @@ -617,7 +617,7 @@ public class VersionControlTest extends AbstractControllerTest { @Test public void testVcWithReferencedCalculatedFields_betweenTenants() throws Exception { Asset asset = createAsset(null, null, "Asset 1"); - Device device = createDevice(null, null, "Device 1", "test1"); + Device device = createDevice("Device 1", "test1"); CalculatedField deviceCalculatedField = createCalculatedField("CalculatedField1", device.getId(), asset.getId()); CalculatedField assetCalculatedField = createCalculatedField("CalculatedField2", asset.getId(), device.getId()); String versionId = createVersion("calculated fields of asset and device", EntityType.ASSET, EntityType.DEVICE, EntityType.DEVICE_PROFILE, EntityType.ASSET_PROFILE); @@ -638,7 +638,9 @@ public class VersionControlTest extends AbstractControllerTest { assertThat(importedField.getName()).isEqualTo(deviceCalculatedField.getName()); assertThat(importedField.getType()).isEqualTo(deviceCalculatedField.getType()); assertThat(importedField.getId()).isNotEqualTo(deviceCalculatedField.getId()); - assertThat(importedField.getConfiguration().getArguments().get("T").getRefEntityId()).isEqualTo(importedAsset.getId()); + assertThat(importedField.getConfiguration()).isInstanceOf(SimpleCalculatedFieldConfiguration.class); + SimpleCalculatedFieldConfiguration simpleCfg = (SimpleCalculatedFieldConfiguration) importedField.getConfiguration(); + assertThat(simpleCfg.getArguments().get("T").getRefEntityId()).isEqualTo(importedAsset.getId()); }); List importedAssetCalculatedFields = findCalculatedFieldsByEntityId(importedAsset.getId()); @@ -647,7 +649,9 @@ public class VersionControlTest extends AbstractControllerTest { assertThat(importedField.getName()).isEqualTo(assetCalculatedField.getName()); assertThat(importedField.getType()).isEqualTo(assetCalculatedField.getType()); assertThat(importedField.getId()).isNotEqualTo(assetCalculatedField.getId()); - assertThat(importedField.getConfiguration().getArguments().get("T").getRefEntityId()).isEqualTo(importedDevice.getId()); + assertThat(importedField.getConfiguration()).isInstanceOf(SimpleCalculatedFieldConfiguration.class); + SimpleCalculatedFieldConfiguration simpleCfg = (SimpleCalculatedFieldConfiguration) importedField.getConfiguration(); + assertThat(simpleCfg.getArguments().get("T").getRefEntityId()).isEqualTo(importedDevice.getId()); }); } @@ -907,9 +911,8 @@ public class VersionControlTest extends AbstractControllerTest { login(tenantAdmin2.getEmail(), tenantAdmin2.getEmail()); } - private Device createDevice(CustomerId customerId, DeviceProfileId deviceProfileId, String name, String accessToken, Consumer... modifiers) { + private Device createDevice(DeviceProfileId deviceProfileId, String name, String accessToken, Consumer... modifiers) { Device device = new Device(); - device.setCustomerId(customerId); device.setName(name); device.setLabel("lbl"); device.setDeviceProfileId(deviceProfileId); diff --git a/application/src/test/java/org/thingsboard/server/system/BaseHttpDeviceApiTest.java b/application/src/test/java/org/thingsboard/server/system/BaseHttpDeviceApiTest.java index dbfca2e9f1..65633c6bd9 100644 --- a/application/src/test/java/org/thingsboard/server/system/BaseHttpDeviceApiTest.java +++ b/application/src/test/java/org/thingsboard/server/system/BaseHttpDeviceApiTest.java @@ -37,9 +37,6 @@ import static org.springframework.test.web.servlet.request.MockMvcRequestBuilder import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.request; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -/** - * @author Andrew Shvayka - */ @TestPropertySource(properties = { "transport.http.enabled=true", "transport.http.max_payload_size=/api/v1/*/rpc/**=10000;/api/v1/**=20000" diff --git a/application/src/test/java/org/thingsboard/server/system/BaseRestApiLimitsTest.java b/application/src/test/java/org/thingsboard/server/system/BaseRestApiLimitsTest.java index 84a366b139..fea9d5086c 100644 --- a/application/src/test/java/org/thingsboard/server/system/BaseRestApiLimitsTest.java +++ b/application/src/test/java/org/thingsboard/server/system/BaseRestApiLimitsTest.java @@ -45,10 +45,6 @@ import java.util.concurrent.TimeoutException; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -/** - * @author Illia Barkov - */ - @Slf4j public abstract class BaseRestApiLimitsTest extends AbstractControllerTest { diff --git a/application/src/test/java/org/thingsboard/server/system/RestTemplateConvertersTest.java b/application/src/test/java/org/thingsboard/server/system/RestTemplateConvertersTest.java index 6ff57a7ee0..f976156a86 100644 --- a/application/src/test/java/org/thingsboard/server/system/RestTemplateConvertersTest.java +++ b/application/src/test/java/org/thingsboard/server/system/RestTemplateConvertersTest.java @@ -29,7 +29,6 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo; import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess; - @Slf4j public class RestTemplateConvertersTest { diff --git a/application/src/test/java/org/thingsboard/server/system/SystemPatchApplierTest.java b/application/src/test/java/org/thingsboard/server/system/SystemPatchApplierTest.java new file mode 100644 index 0000000000..3c65f19d65 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/system/SystemPatchApplierTest.java @@ -0,0 +1,410 @@ +/** + * Copyright © 2016-2025 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.system; + +import com.fasterxml.jackson.databind.JsonNode; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.io.TempDir; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.test.util.ReflectionTestUtils; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.WidgetTypeId; +import org.thingsboard.server.common.data.widget.WidgetTypeDetails; +import org.thingsboard.server.dao.widget.WidgetTypeService; +import org.thingsboard.server.service.install.InstallScripts; +import org.thingsboard.server.service.system.SystemPatchApplier; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.UUID; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.ArgumentMatchers.contains; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +public class SystemPatchApplierTest { + + @Mock + private JdbcTemplate jdbcTemplate; + + @Mock + private InstallScripts installScripts; + + @Mock + private WidgetTypeService widgetTypeService; + + @InjectMocks + private SystemPatchApplier reconciler; + + @TempDir + Path tempDir; + + @ParameterizedTest(name = "Parse version {0} should return major={1}, minor={2}, patch={3}") + @CsvSource({ + "4.2.1, 4, 2, 1, 0", + "4.2.0, 4, 2, 0, 0", + "4.2, 4, 2, 0, 0", + "4.0.1.2, 4, 0, 1, 2", + "4, 4, 0, 0, 0", + "1.0.5.7, 1, 0, 5, 7", + "10.20.30.40, 10, 20, 30, 40", + "0.0.1, 0, 0, 1, 0" + }) + void testParseVersion(String versionString, int expectedMajor, int expectedMinor, int expectedMaintenance, int expectedPatch) { + SystemPatchApplier.VersionInfo version = ReflectionTestUtils.invokeMethod(reconciler, "parseVersion", versionString); + + assertNotNull(version, "Version should not be null for: " + versionString); + assertEquals(expectedMajor, version.major(), "Major version mismatch"); + assertEquals(expectedMinor, version.minor(), "Minor version mismatch"); + assertEquals(expectedMaintenance, version.maintenance(), "Maintenance version mismatch"); + assertEquals(expectedPatch, version.patch(), "Patch version mismatch"); + } + + @ParameterizedTest(name = "Parse invalid version: {0}") + @CsvSource({ + "invalid", + "a.b.c", + "1.2.y.x", + "''", + "1.x.3" + }) + void testParseInvalidVersion(String invalidVersion) { + SystemPatchApplier.VersionInfo version = ReflectionTestUtils.invokeMethod(reconciler, "parseVersion", invalidVersion); + assertNull(version, "Version should be null for invalid input: " + invalidVersion); + } + + @Test + void whenLockIsNotAcquired_thenAcquiredIsSuccess() { + when(jdbcTemplate.queryForObject(anyString(), eq(Boolean.class), anyLong())).thenReturn(true); + + Boolean acquired = ReflectionTestUtils.invokeMethod(reconciler, "acquireAdvisoryLock"); + + assertEquals(Boolean.TRUE, acquired); + verify(jdbcTemplate).queryForObject(contains("pg_try_advisory_lock"), eq(Boolean.class), anyLong()); + } + + @Test + void whenLockIsAlreadyAcquired_thenAcquiredIsFailed() { + when(jdbcTemplate.queryForObject(anyString(), eq(Boolean.class), anyLong())).thenReturn(false); + + Boolean acquired = ReflectionTestUtils.invokeMethod(reconciler, "acquireAdvisoryLock"); + + assertNotEquals(Boolean.TRUE, acquired); + } + + @Test + void testReleaseAdvisoryLock() { + when(jdbcTemplate.queryForObject(anyString(), eq(Boolean.class), anyLong())) + .thenReturn(true); + + ReflectionTestUtils.invokeMethod(reconciler, "releaseAdvisoryLock"); + + verify(jdbcTemplate).queryForObject( + contains("pg_advisory_unlock"), eq(Boolean.class), anyLong()); + } + + @Test + void whenWidgetNotFound_thenThrowException() throws Exception { + Path widgetTypesDir = tempDir.resolve("widget_types"); + Files.createDirectories(widgetTypesDir); + when(installScripts.getWidgetTypesDir()).thenReturn(widgetTypesDir); + + WidgetTypeDetails testWidget = createTestWidgetType("test_widget", "Test Widget"); + String json = JacksonUtil.toString(testWidget); + assertNotNull(json); + Files.writeString(widgetTypesDir.resolve("test_widget.json"), json); + + when(widgetTypeService.findWidgetTypeDetailsByTenantIdAndFqn(TenantId.SYS_TENANT_ID, "test_widget")).thenReturn(null); + + assertThrows(RuntimeException.class, () -> ReflectionTestUtils.invokeMethod(reconciler, "updateWidgetTypes")); + } + + @Test + void whenDescriptorChanged_thenUpdateTheExistingWidget() throws Exception { + Path widgetTypesDir = tempDir.resolve("widget_types"); + Files.createDirectories(widgetTypesDir); + when(installScripts.getWidgetTypesDir()).thenReturn(widgetTypesDir); + + WidgetTypeDetails fileWidget = createTestWidgetType("test_widget", "Test Widget"); + fileWidget.setDescriptor(JacksonUtil.toJsonNode("{\"type\":\"latest\",\"version\":2}")); + String json = JacksonUtil.toString(fileWidget); + assertNotNull(json); + Files.writeString(widgetTypesDir.resolve("test_widget.json"), json); + + WidgetTypeDetails existingWidget = createTestWidgetType("test_widget", "Test Widget"); + existingWidget.setId(new WidgetTypeId(UUID.randomUUID())); + existingWidget.setDescriptor(JacksonUtil.toJsonNode("{\"type\":\"latest\",\"version\":1}")); + + when(widgetTypeService.findWidgetTypeDetailsByTenantIdAndFqn(TenantId.SYS_TENANT_ID, "test_widget")) + .thenReturn(existingWidget); + + Integer updated = ReflectionTestUtils.invokeMethod(reconciler, "updateWidgetTypes"); + + assertEquals(1, updated); + verify(widgetTypeService).saveWidgetType(argThat(w -> + w.getDescriptor().get("version").asInt() == 2 + )); + } + + @Test + void whenNameChanged_thenUpdateTheExistingWidget() throws Exception { + Path widgetTypesDir = tempDir.resolve("widget_types"); + Files.createDirectories(widgetTypesDir); + when(installScripts.getWidgetTypesDir()).thenReturn(widgetTypesDir); + + WidgetTypeDetails fileWidget = createTestWidgetType("test_widget", "New Name"); + String json = JacksonUtil.toString(fileWidget); + assertNotNull(json); + Files.writeString(widgetTypesDir.resolve("test_widget.json"), json); + + WidgetTypeDetails existingWidget = createTestWidgetType("test_widget", "Old Name"); + existingWidget.setId(new WidgetTypeId(UUID.randomUUID())); + + when(widgetTypeService.findWidgetTypeDetailsByTenantIdAndFqn(TenantId.SYS_TENANT_ID, "test_widget")) + .thenReturn(existingWidget); + + Integer updated = ReflectionTestUtils.invokeMethod(reconciler, "updateWidgetTypes"); + + assertEquals(1, updated); + verify(widgetTypeService).saveWidgetType(argThat(w -> "New Name".equals(w.getName()))); + } + + @Test + void whenNothingChanged_thenSkipTheUpdateOfTheExistingWidget() throws Exception { + Path widgetTypesDir = tempDir.resolve("widget_types"); + Files.createDirectories(widgetTypesDir); + when(installScripts.getWidgetTypesDir()).thenReturn(widgetTypesDir); + + WidgetTypeDetails fileWidget = createTestWidgetType("test_widget", "Test Widget"); + String json = JacksonUtil.toString(fileWidget); + assertNotNull(json); + Files.writeString(widgetTypesDir.resolve("test_widget.json"), json); + + WidgetTypeDetails existingWidget = createTestWidgetType("test_widget", "Test Widget"); + existingWidget.setId(new WidgetTypeId(UUID.randomUUID())); + + when(widgetTypeService.findWidgetTypeDetailsByTenantIdAndFqn(TenantId.SYS_TENANT_ID, "test_widget")) + .thenReturn(existingWidget); + + Integer updated = ReflectionTestUtils.invokeMethod(reconciler, "updateWidgetTypes"); + + assertEquals(0, updated); + verify(widgetTypeService, never()).saveWidgetType(any()); + } + + @ParameterizedTest(name = "{0}") + @MethodSource("provideDescriptorComparisonTestCases") + void testIfDescriptorsAreEqual(String testName, JsonNode desc1, JsonNode desc2, boolean expectedEqual) { + Boolean result = ReflectionTestUtils.invokeMethod(reconciler, "isDescriptorEqual", desc1, desc2); + assertEquals(expectedEqual, result, testName); + } + + @Test + void whenDescriptorChanged_thenReturnWidgetTypeChanged() { + WidgetTypeDetails existing = createTestWidgetType("test", "Test"); + existing.setDescriptor(JacksonUtil.toJsonNode("{\"version\":1}")); + + WidgetTypeDetails file = createTestWidgetType("test", "Test"); + file.setDescriptor(JacksonUtil.toJsonNode("{\"version\":2}")); + + boolean result = Boolean.TRUE.equals(ReflectionTestUtils.invokeMethod(reconciler, "isWidgetTypeChanged", existing, file)); + assertTrue(result); + } + + @Test + void whenNameChanged_thenReturnWidgetTypeChanged() { + WidgetTypeDetails existing = createTestWidgetType("test", "Old Name"); + WidgetTypeDetails file = createTestWidgetType("test", "New Name"); + + boolean result = Boolean.TRUE.equals(ReflectionTestUtils.invokeMethod(reconciler, "isWidgetTypeChanged", existing, file)); + assertTrue(result); + } + + @Test + void whenDescriptionChanged_thenReturnWidgetTypeChanged() { + WidgetTypeDetails existing = createTestWidgetType("test", "Test"); + existing.setDescription("Old description"); + + WidgetTypeDetails file = createTestWidgetType("test", "Test"); + file.setDescription("New description"); + + boolean result = Boolean.TRUE.equals(ReflectionTestUtils.invokeMethod(reconciler, "isWidgetTypeChanged", existing, file)); + assertTrue(result); + } + + @Test + void whenWidgetTypeAreIdentical_thenNoUpdateIsPerformed() { + WidgetTypeDetails existing = createTestWidgetType("test", "Test"); + WidgetTypeDetails file = createTestWidgetType("test", "Test"); + + boolean result = Boolean.TRUE.equals(ReflectionTestUtils.invokeMethod(reconciler, "isWidgetTypeChanged", existing, file)); + assertFalse(result); + } + + @Test + void whenLockIsHeldByOneThread_thenSecondThreadCannotAcquireLock() throws Exception { + CountDownLatch lockAcquired = new CountDownLatch(1); + CountDownLatch startSecondThread = new CountDownLatch(1); + CountDownLatch testComplete = new CountDownLatch(1); + + AtomicBoolean firstThreadAcquiredLock = new AtomicBoolean(false); + AtomicBoolean secondThreadAcquiredLock = new AtomicBoolean(false); + AtomicBoolean firstThreadSavedWidget = new AtomicBoolean(false); + AtomicBoolean secondThreadSavedWidget = new AtomicBoolean(false); + + Path widgetTypesDir = tempDir.resolve("widget_types"); + Files.createDirectories(widgetTypesDir); + when(installScripts.getWidgetTypesDir()).thenReturn(widgetTypesDir); + + WidgetTypeDetails fileWidget = createTestWidgetType("test_widget", "Test Widget"); + fileWidget.setDescriptor(JacksonUtil.toJsonNode("{\"type\":\"latest\",\"version\":2}")); + String toString = JacksonUtil.toCanonicalString(fileWidget); + assertNotNull(toString); + Files.writeString(widgetTypesDir.resolve("test_widget.json"), toString); + + WidgetTypeDetails existingWidget = createTestWidgetType("test_widget", "Test Widget"); + existingWidget.setId(new WidgetTypeId(UUID.randomUUID())); + existingWidget.setDescriptor(JacksonUtil.toJsonNode("{\"type\":\"latest\",\"version\":1}")); + + when(widgetTypeService.findWidgetTypeDetailsByTenantIdAndFqn(TenantId.SYS_TENANT_ID, "test_widget")).thenReturn(existingWidget); + + when(jdbcTemplate.queryForObject(contains("pg_try_advisory_lock"), eq(Boolean.class), anyLong())) + .thenReturn(true) + .thenReturn(false); + + when(jdbcTemplate.queryForObject(contains("pg_advisory_unlock"), eq(Boolean.class), anyLong())) + .thenReturn(true); + + // The first thread-acquires lock and performs update + Thread firstThread = new Thread(() -> { + try { + Boolean acquired = ReflectionTestUtils.invokeMethod(reconciler, "acquireAdvisoryLock"); + firstThreadAcquiredLock.set(Boolean.TRUE.equals(acquired)); + + if (firstThreadAcquiredLock.get()) { + lockAcquired.countDown(); + startSecondThread.await(5, TimeUnit.SECONDS); + + // Simulate work while holding lock + Thread.sleep(100); + + Integer updated = ReflectionTestUtils.invokeMethod(reconciler, "updateWidgetTypes"); + firstThreadSavedWidget.set(updated != null && updated > 0); + + ReflectionTestUtils.invokeMethod(reconciler, "releaseAdvisoryLock"); + } + } catch (Exception ignored) { + } finally { + testComplete.countDown(); + } + }); + + // Second thread - attempts to acquire lock but fails + Thread secondThread = new Thread(() -> { + try { + lockAcquired.await(5, TimeUnit.SECONDS); + startSecondThread.countDown(); + + Boolean acquired = ReflectionTestUtils.invokeMethod(reconciler, "acquireAdvisoryLock"); + secondThreadAcquiredLock.set(Boolean.TRUE.equals(acquired)); + + if (secondThreadAcquiredLock.get()) { + Integer updated = ReflectionTestUtils.invokeMethod(reconciler, "updateWidgetTypes"); + secondThreadSavedWidget.set(updated != null && updated > 0); + + ReflectionTestUtils.invokeMethod(reconciler, "releaseAdvisoryLock"); + } + } catch (Exception ignored) {} + }); + + firstThread.start(); + secondThread.start(); + + assertTrue(testComplete.await(10, TimeUnit.SECONDS), "Test should complete within timeout"); + firstThread.join(1000); + secondThread.join(1000); + + assertTrue(firstThreadAcquiredLock.get(), "First thread should acquire lock"); + assertFalse(secondThreadAcquiredLock.get(), "Second thread should NOT acquire lock"); + assertTrue(firstThreadSavedWidget.get(), "First thread should save widget"); + assertFalse(secondThreadSavedWidget.get(), "Second thread should NOT save widget"); + + verify(widgetTypeService, times(1)).saveWidgetType(any()); + } + + private static Stream provideDescriptorComparisonTestCases() { + return Stream.of( + Arguments.of("Both null", null, null, true), + Arguments.of("First null", null, JacksonUtil.newObjectNode(), false), + Arguments.of("Second null", JacksonUtil.newObjectNode(), null, false), + Arguments.of("Same content", + JacksonUtil.toJsonNode("{\"type\":\"latest\",\"version\":1}"), + JacksonUtil.toJsonNode("{\"type\":\"latest\",\"version\":1}"), + true), + Arguments.of("Different content", + JacksonUtil.toJsonNode("{\"type\":\"latest\",\"version\":1}"), + JacksonUtil.toJsonNode("{\"type\":\"latest\",\"version\":2}"), + false), + Arguments.of("Different key order but same content", + JacksonUtil.toJsonNode("{\"version\":1,\"type\":\"latest\"}"), + JacksonUtil.toJsonNode("{\"type\":\"latest\",\"version\":1}"), + true), + Arguments.of("Empty objects", + JacksonUtil.toJsonNode("{}"), + JacksonUtil.toJsonNode("{}"), + true) + ); + } + + private WidgetTypeDetails createTestWidgetType(String fqn, String name) { + WidgetTypeDetails widget = new WidgetTypeDetails(); + widget.setFqn(fqn); + widget.setName(name); + widget.setDescription("Test description"); + widget.setTenantId(TenantId.SYS_TENANT_ID); + widget.setDescriptor(JacksonUtil.toJsonNode("{\"type\":\"latest\"}")); + return widget; + } + +} diff --git a/application/src/test/java/org/thingsboard/server/system/sql/DeviceApiSqlTest.java b/application/src/test/java/org/thingsboard/server/system/sql/DeviceApiSqlTest.java index 74f3cbafd9..27e5237fd9 100644 --- a/application/src/test/java/org/thingsboard/server/system/sql/DeviceApiSqlTest.java +++ b/application/src/test/java/org/thingsboard/server/system/sql/DeviceApiSqlTest.java @@ -18,9 +18,6 @@ package org.thingsboard.server.system.sql; import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.system.BaseHttpDeviceApiTest; -/** - * Created by Valerii Sosliuk on 6/27/2017. - */ @DaoSqlTest public class DeviceApiSqlTest extends BaseHttpDeviceApiTest { } diff --git a/application/src/test/java/org/thingsboard/server/system/sql/RestApiLimitsSqlTest.java b/application/src/test/java/org/thingsboard/server/system/sql/RestApiLimitsSqlTest.java index 56c26fb3d3..932fb32328 100644 --- a/application/src/test/java/org/thingsboard/server/system/sql/RestApiLimitsSqlTest.java +++ b/application/src/test/java/org/thingsboard/server/system/sql/RestApiLimitsSqlTest.java @@ -18,7 +18,6 @@ package org.thingsboard.server.system.sql; import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.system.BaseRestApiLimitsTest; - @DaoSqlTest public class RestApiLimitsSqlTest extends BaseRestApiLimitsTest { } diff --git a/application/src/test/java/org/thingsboard/server/transport/coap/security/AbstractCoapSecurityIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/coap/security/AbstractCoapSecurityIntegrationTest.java index 72f8cc5b4f..839b8c7cad 100644 --- a/application/src/test/java/org/thingsboard/server/transport/coap/security/AbstractCoapSecurityIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/coap/security/AbstractCoapSecurityIntegrationTest.java @@ -64,6 +64,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. "coap.server.enabled=true", "coap.dtls.enabled=true", "coap.dtls.credentials.pem.cert_file=coap/credentials/server/cert.pem", + "coap.dtls.x509.skip_validity_check_for_client_cert=true", "device.connectivity.coaps.enabled=true", "service.integrations.supported=ALL", "transport.coap.enabled=true", diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/AbstractLwM2MIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/AbstractLwM2MIntegrationTest.java index ddf8bca43f..a7d4cea8d4 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/AbstractLwM2MIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/AbstractLwM2MIntegrationTest.java @@ -21,6 +21,7 @@ import com.google.gson.JsonArray; import com.google.gson.JsonElement; import lombok.extern.slf4j.Slf4j; import org.apache.commons.io.IOUtils; +import org.awaitility.core.ConditionTimeoutException; import org.eclipse.leshan.client.LeshanClient; import org.eclipse.leshan.client.object.Security; import org.eclipse.leshan.client.servers.LwM2mServer; @@ -84,6 +85,7 @@ import org.thingsboard.server.transport.lwm2m.server.client.ResourceUpdateResult import org.thingsboard.server.transport.lwm2m.server.uplink.DefaultLwM2mUplinkMsgHandler; import org.thingsboard.server.transport.lwm2m.server.uplink.LwM2mUplinkMsgHandler; +import java.io.IOException; import java.net.ServerSocket; import java.util.ArrayList; import java.util.Arrays; @@ -94,6 +96,7 @@ import java.util.Map; import java.util.Set; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; import static org.awaitility.Awaitility.await; import static org.eclipse.leshan.client.object.Security.noSec; @@ -118,13 +121,14 @@ import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MClient import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MClientState.ON_UPDATE_SUCCESS; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MProfileBootstrapConfigType; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MProfileBootstrapConfigType.NONE; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.lwm2mClientResources; import static org.thingsboard.server.transport.lwm2m.ota.AbstractOtaLwM2MIntegrationTest.CLIENT_LWM2M_SETTINGS_19; -@TestPropertySource(properties = { - "transport.lwm2m.enabled=true", -}) @Slf4j @DaoSqlTest +@TestPropertySource(properties = { + "transport.lwm2m.enabled=true" +}) public abstract class AbstractLwM2MIntegrationTest extends AbstractTransportIntegrationTest { @SpyBean @@ -145,9 +149,6 @@ public abstract class AbstractLwM2MIntegrationTest extends AbstractTransportInte public static final String host = "localhost"; public static final String hostBs = "localhost"; public static final Integer shortServerId = 123; - public static final Integer shortServerIdBs0 = 0; - public static final int serverId = 1; - public static final int serverIdBs = 0; public static final String COAP = "coap://"; public static final String COAPS = "coaps://"; @@ -306,7 +307,7 @@ public abstract class AbstractLwM2MIntegrationTest extends AbstractTransportInte protected final Set expectedStatusesRegistrationBsSuccess = new HashSet<>(Arrays.asList(ON_BOOTSTRAP_STARTED, ON_BOOTSTRAP_SUCCESS, ON_REGISTRATION_STARTED, ON_REGISTRATION_SUCCESS)); protected ScheduledExecutorService executor; protected LwM2MTestClient lwM2MTestClient; - private String[] resources; + private String[] resources = lwm2mClientResources; protected String deviceId; protected boolean supportFormatOnly_SenMLJSON_SenMLCBOR = false; @@ -317,7 +318,7 @@ public abstract class AbstractLwM2MIntegrationTest extends AbstractTransportInte @After public void after() throws Exception { - this.clientDestroy(true); + this.clientDestroy(); if (executor != null && !executor.isShutdown()) { executor.shutdownNow(); } @@ -548,7 +549,9 @@ public abstract class AbstractLwM2MIntegrationTest extends AbstractTransportInte } public void setResources(String[] resources) { - this.resources = resources; + if (this.resources == null || !Arrays.equals(this.resources, resources)) { + this.resources = resources; + } } public void createNewClient(Security security, Security securityBs, boolean isRpc, @@ -564,7 +567,7 @@ public abstract class AbstractLwM2MIntegrationTest extends AbstractTransportInte public void createNewClient(Security security, Security securityBs, boolean isRpc, String endpoint, Integer clientDtlsCidLength, boolean queueMode, String deviceIdStr, Integer value3_0_9) throws Exception { - this.clientDestroy(false); + this.clientDestroy(); lwM2MTestClient = new LwM2MTestClient(this.executor, endpoint, resources); try (ServerSocket socket = new ServerSocket(0)) { @@ -651,13 +654,30 @@ public abstract class AbstractLwM2MIntegrationTest extends AbstractTransportInte } - private void clientDestroy(boolean isAfter) { + private void clientDestroy() { try { if (lwM2MTestClient != null && lwM2MTestClient.getLeshanClient() != null) { - if (isAfter) { - sendObserveCancelAllWithAwait(lwM2MTestClient.getDeviceIdStr()); - awaitDeleteDevice(lwM2MTestClient.getDeviceIdStr()); + boolean serverAlive = false; + for (int port = AbstractLwM2MIntegrationTest.port; port <= securityPortBs; port++) { + try (ServerSocket socket = new ServerSocket(port)) { + log.info("Port {} is free.", port); + } catch (IOException e) { + log.debug("Port {} is busy — CoAP server still active.", port); + serverAlive = true; + break; + } + } + if (serverAlive) { + try { + sendObserveCancelAllWithAwait(lwM2MTestClient.getDeviceIdStr()); + awaitDeleteDevice(lwM2MTestClient.getDeviceIdStr()); + } catch (Exception e) { + log.warn("Failed to cleanup LwM2M observations before destroy: {}", e.getMessage()); + } + } else { + log.info("No active CoAP server found on ports 5685–5688. Skipping observe cleanup."); } + lwM2MTestClient.destroy(); } } catch (Exception e) { @@ -705,10 +725,10 @@ public abstract class AbstractLwM2MIntegrationTest extends AbstractTransportInte return bootstrap; } - private AbstractLwM2MBootstrapServerCredential getBootstrapServerCredentialNoSec(boolean isBootstrap) { + protected AbstractLwM2MBootstrapServerCredential getBootstrapServerCredentialNoSec(boolean isBootstrap) { AbstractLwM2MBootstrapServerCredential bootstrapServerCredential = new NoSecLwM2MBootstrapServerCredential(); bootstrapServerCredential.setServerPublicKey(""); - bootstrapServerCredential.setShortServerId(isBootstrap ? shortServerIdBs0 : shortServerId); + bootstrapServerCredential.setShortServerId(isBootstrap ? null : shortServerId); bootstrapServerCredential.setBootstrapServerIs(isBootstrap); bootstrapServerCredential.setHost(isBootstrap ? hostBs : host); bootstrapServerCredential.setPort(isBootstrap ? portBs : port); @@ -726,11 +746,19 @@ public abstract class AbstractLwM2MIntegrationTest extends AbstractTransportInte return credentials; } - protected void awaitObserveReadAll(int cntObserve, String deviceIdStr) throws Exception { - await("ObserveReadAll: countObserve " + cntObserve) - .atMost(40, TimeUnit.SECONDS) - .until(() -> cntObserve == getCntObserveAll(deviceIdStr)); + + protected void awaitObserveReadAll(int cntObserve, String deviceIdStr) throws Exception { + try { + await("ObserveReadAll: countObserve " + cntObserve) + .atMost(40, TimeUnit.SECONDS) + .until(() -> cntObserve == getCntObserveAll(deviceIdStr)); + } catch (ConditionTimeoutException e) { + int current = getCntObserveAll(deviceIdStr); + log.error("Condition or device {} with alias 'ObserveReadAll: countObserve {}, but received {}", deviceIdStr, cntObserve, current); + throw e; + } } + protected void awaitDeleteDevice(String deviceIdStr) throws Exception { await("Delete device with id: " + deviceIdStr) .atMost(40, TimeUnit.SECONDS) @@ -741,6 +769,19 @@ public abstract class AbstractLwM2MIntegrationTest extends AbstractTransportInte }); } + protected void updateRegAtLeastOnceAfterAction() { + long initialInvocationCount = countUpdateReg(); + AtomicLong newInvocationCount = new AtomicLong(initialInvocationCount); + log.trace("updateRegAtLeastOnceAfterAction: initialInvocationCount [{}]", initialInvocationCount); + await("Update Registration at-least-once after action") + .atMost(50, TimeUnit.SECONDS) + .until(() -> { + newInvocationCount.set(countUpdateReg()); + return newInvocationCount.get() > initialInvocationCount; + }); + log.trace("updateRegAtLeastOnceAfterAction: newInvocationCount [{}]", newInvocationCount.get()); + } + protected Integer getCntObserveAll(String deviceIdStr) throws Exception { String actualResult = sendObserveOK("ObserveReadAll", null, deviceIdStr); ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/Lwm2mTestHelper.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/Lwm2mTestHelper.java index 6159400bb6..c5233b378b 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/Lwm2mTestHelper.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/Lwm2mTestHelper.java @@ -17,15 +17,13 @@ package org.thingsboard.server.transport.lwm2m; public class Lwm2mTestHelper { - public static final String[] lwm2mClientResources = new String[]{"3.xml", "5.xml", "6.xml", "9.xml", "19.xml", "3303.xml"}; + public static final String[] lwm2mClientResources = new String[]{"3-1_2.xml", "5.xml", "6.xml", "9.xml", "19.xml", "3303.xml"}; // Models public static final int BINARY_APP_DATA_CONTAINER = 19; public static final int TEMPERATURE_SENSOR = 3303; // Ids in Client - public static final int OBJECT_ID_0 = 0; - public static final int OBJECT_ID_1 = 1; public static final int OBJECT_INSTANCE_ID_0 = 0; public static final int OBJECT_INSTANCE_ID_1 = 1; public static final int OBJECT_INSTANCE_ID_2 = 2; diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/FwLwM2MDevice.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/FwLwM2MDevice.java index 051a68c9ab..8e247a8814 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/FwLwM2MDevice.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/FwLwM2MDevice.java @@ -16,6 +16,7 @@ package org.thingsboard.server.transport.lwm2m.client; import lombok.extern.slf4j.Slf4j; +import org.eclipse.leshan.client.LeshanClient; import org.eclipse.leshan.client.resource.BaseInstanceEnabler; import org.eclipse.leshan.client.servers.LwM2mServer; import org.eclipse.leshan.core.model.ObjectModel; @@ -32,6 +33,9 @@ import java.util.List; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicInteger; +import static org.thingsboard.server.dao.service.OtaPackageServiceTest.TARGET_FW_VERSION; +import static org.thingsboard.server.dao.service.OtaPackageServiceTest.TITLE; @Slf4j public class FwLwM2MDevice extends BaseInstanceEnabler implements Destroyable { @@ -44,6 +48,12 @@ public class FwLwM2MDevice extends BaseInstanceEnabler implements Destroyable { private final AtomicInteger updateResult = new AtomicInteger(0); + private LeshanClient leshanClient; + private String pkgNameDef = "firmware"; + private String pkgName; + private String pkgVersionDef = "1.0.0"; + private String pkgVersion; + @Override public ReadResponse read(LwM2mServer identity, int resourceId) { if (!identity.isSystem()) @@ -74,7 +84,7 @@ public class FwLwM2MDevice extends BaseInstanceEnabler implements Destroyable { switch (resourceId) { case 2: - startUpdating(); + startUpdating(identity); return ExecuteResponse.success(); default: return super.execute(identity, resourceId, arguments); @@ -106,11 +116,13 @@ public class FwLwM2MDevice extends BaseInstanceEnabler implements Destroyable { } private String getPkgName() { - return "firmware"; + this.pkgName = this.pkgName == null ? this.pkgNameDef : this.pkgName; + return this.pkgName; } private String getPkgVersion() { - return "1.0.0"; + this.pkgVersion = this.pkgVersion == null ? this.pkgVersionDef : this.pkgVersion; + return this.pkgVersion; } private int getFirmwareUpdateDeliveryMethod() { @@ -140,7 +152,7 @@ public class FwLwM2MDevice extends BaseInstanceEnabler implements Destroyable { }, 100, TimeUnit.MILLISECONDS); } - private void startUpdating() { + private void startUpdating(LwM2mServer identity) { scheduler.schedule(() -> { try { state.set(3); @@ -148,9 +160,25 @@ public class FwLwM2MDevice extends BaseInstanceEnabler implements Destroyable { Thread.sleep(100); updateResult.set(1); fireResourceChange(5); + this.pkgName = TITLE; + fireResourceChange(6); + this.pkgVersion = TARGET_FW_VERSION; + fireResourceChange(7); + if (this.leshanClient != null) { + log.info("Stop/reboot LwM2M client {}", this.leshanClient.getEndpoint(identity)); + this.leshanClient.stop(false); + log.info("Start after update fw LwM2M client {}", this.leshanClient.getEndpoint(identity)); + this.leshanClient.start(); + this.pkgName = this.pkgNameDef; + this.pkgVersion = this.pkgVersionDef; + } } catch (Exception e) { } }, 100, TimeUnit.MILLISECONDS); } + protected void setLeshanClient(LeshanClient leshanClient) { + this.leshanClient = leshanClient; + } + } diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/LwM2MTestClient.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/LwM2MTestClient.java index 2ae1432cac..d2543f84d7 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/LwM2MTestClient.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/LwM2MTestClient.java @@ -68,6 +68,9 @@ import org.thingsboard.server.transport.lwm2m.server.uplink.LwM2mUplinkMsgHandle import org.thingsboard.server.transport.lwm2m.utils.LwM2mValueConverterImpl; import java.io.IOException; +import java.io.InputStream; +import java.lang.reflect.Field; +import java.lang.reflect.Method; import java.net.InetSocketAddress; import java.util.ArrayList; import java.util.HashMap; @@ -77,6 +80,7 @@ import java.util.Map; import java.util.Set; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; import static org.eclipse.californium.scandium.config.DtlsConfig.DTLS_CONNECTION_ID_LENGTH; import static org.eclipse.californium.scandium.config.DtlsConfig.DTLS_RECOMMENDED_CIPHER_SUITES_ONLY; @@ -88,10 +92,7 @@ import static org.eclipse.leshan.core.LwM2mId.SECURITY; import static org.eclipse.leshan.core.LwM2mId.SERVER; import static org.eclipse.leshan.core.LwM2mId.SOFTWARE_MANAGEMENT; import static org.eclipse.leshan.core.node.codec.DefaultLwM2mEncoder.getDefaultPathEncoder; -import static org.thingsboard.server.transport.lwm2m.AbstractLwM2MIntegrationTest.serverId; -import static org.thingsboard.server.transport.lwm2m.AbstractLwM2MIntegrationTest.serverIdBs; import static org.thingsboard.server.transport.lwm2m.AbstractLwM2MIntegrationTest.shortServerId; -import static org.thingsboard.server.transport.lwm2m.AbstractLwM2MIntegrationTest.shortServerIdBs0; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.BINARY_APP_DATA_CONTAINER; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MClientState; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MClientState.ON_BOOTSTRAP_FAILURE; @@ -141,7 +142,7 @@ public class LwM2MTestClient { private LwM2mTemperatureSensor lwM2mTemperatureSensor12; private String deviceIdStr; - public void init(Security security, Security securityBs, int port, boolean isRpc, + public void init(Security securityLwm2m, Security securityBs, int port, boolean isRpc, LwM2mUplinkMsgHandler defaultLwM2mUplinkMsgHandler, LwM2mClientContext clientContext, Integer cIdLength, boolean queueMode, boolean supportFormatOnly_SenMLJSON_SenMLCBOR, Integer value3_0_9) throws InvalidDDFFileException, IOException { @@ -149,55 +150,33 @@ public class LwM2MTestClient { this.defaultLwM2mUplinkMsgHandlerTest = defaultLwM2mUplinkMsgHandler; this.clientContext = clientContext; - List models = ObjectLoader.loadAllDefault(); - for (String resourceName : lwm2mClientResources) { - models.addAll(ObjectLoader.loadDdfFile(LwM2MTestClient.class.getClassLoader().getResourceAsStream("lwm2m/" + resourceName), resourceName)); + ObjectsInitializer initializer = createFreshInitializer(); + + // SECURITY + if (securityLwm2m != null && securityLwm2m.getId() != null) { + forceNullSecurityId(securityLwm2m); } - if (this.modelResources != null) { - List modelsRes = new ArrayList<>(); - for (String resourceName : this.modelResources) { - modelsRes.addAll(ObjectLoader.loadDdfFile(LwM2MTestClient.class.getClassLoader().getResourceAsStream("lwm2m/" + resourceName), resourceName)); - } - Set idsToRemove = new HashSet<>(); - for (ObjectModel model : modelsRes) { - idsToRemove.add(model.id); - } - models.removeIf(model -> idsToRemove.contains(model.id)); - models.addAll(modelsRes); + if (securityBs!= null && securityBs.getId() != null) { + forceNullSecurityId(securityBs); } - - LwM2mModel model = new StaticModel(models); - ObjectsInitializer initializer = new ObjectsInitializer(model); - if (securityBs != null && security != null) { - // SECURITY - security.setId(serverId); - securityBs.setId(serverIdBs); - LwM2mInstanceEnabler[] instances = new LwM2mInstanceEnabler[]{securityBs, security}; - initializer.setClassForObject(SECURITY, Security.class); - initializer.setInstancesForObject(SECURITY, instances); - // SERVER - Server lwm2mServer = new Server(shortServerId, TimeUnit.MINUTES.toSeconds(60)); - lwm2mServer.setId(serverId); - Server serverBs = new Server(shortServerIdBs0, TimeUnit.MINUTES.toSeconds(60)); - serverBs.setId(serverIdBs); - instances = new LwM2mInstanceEnabler[]{serverBs, lwm2mServer}; - initializer.setClassForObject(SERVER, Server.class); - initializer.setInstancesForObject(SERVER, instances); + if (securityBs != null && securityLwm2m != null) { + log.warn("Security Both: securityBs: [{}] and security Lwm2m [{}]", securityBs.getId(), securityLwm2m.getId()); + initializer.setInstancesForObject(SECURITY, securityBs, securityLwm2m); } else if (securityBs != null) { - // SECURITY + log.warn("Security BS only: securityBs: [{}] ", securityBs.getId()); initializer.setInstancesForObject(SECURITY, securityBs); - // SERVER - initializer.setClassForObject(SERVER, Server.class); - } else { + } else if (securityLwm2m != null){ // SECURITY - initializer.setInstancesForObject(SECURITY, security); - // SERVER - Server lwm2mServer = new Server(shortServerId, TimeUnit.MINUTES.toSeconds(60)); - lwm2mServer.setId(serverId); - initializer.setInstancesForObject(SERVER, lwm2mServer); + log.warn("Security Lwm2m only: security Lwm2m [{}]", securityLwm2m.getId()); + initializer.setInstancesForObject(SECURITY, securityLwm2m); } - + // SERVER + Server serverLwm2m = new Server(shortServerId, TimeUnit.MINUTES.toSeconds(60)); + initializer.setInstancesForObject(SERVER, serverLwm2m); + // DEVICE initializer.setInstancesForObject(DEVICE, lwM2MDevice = new SimpleLwM2MDevice(executor, value3_0_9)); + + // OTHER t initializer.setInstancesForObject(FIRMWARE, fwLwM2MDevice = new FwLwM2MDevice()); initializer.setInstancesForObject(SOFTWARE_MANAGEMENT, swLwM2MDevice = new SwLwM2MDevice()); initializer.setClassForObject(ACCESS_CONTROL, DummyInstanceEnabler.class); @@ -444,25 +423,43 @@ public class LwM2MTestClient { public void destroy() { if (leshanClient != null) { - leshanClient.destroy(true); - } - if (lwM2MDevice != null) { - lwM2MDevice.destroy(); - } - if (fwLwM2MDevice != null) { - fwLwM2MDevice.destroy(); - } - if (swLwM2MDevice != null) { - swLwM2MDevice.destroy(); - } - if (lwM2MBinaryAppDataContainer != null) { - lwM2MBinaryAppDataContainer.destroy(); + try { + leshanClient.destroy(true); + } catch (Exception e) { + log.warn("Failed to destroy Leshan client", e); + } finally { + leshanClient = null; + } } - if (lwM2MTemperatureSensor != null) { - lwM2MTemperatureSensor.destroy(); + + // ThingsBoard custom LwM2M objects + destroySafe(lwM2MDevice); + destroySafe(fwLwM2MDevice); + destroySafe(swLwM2MDevice); + destroySafe(lwM2MBinaryAppDataContainer); + destroySafe(lwM2MTemperatureSensor); + + lwM2MDevice = null; + fwLwM2MDevice = null; + swLwM2MDevice = null; + lwM2MBinaryAppDataContainer = null; + lwM2MTemperatureSensor = null; + } + + + private void destroySafe(Object obj) { + if (obj == null) return; + try { + Method destroy = obj.getClass().getMethod("destroy"); + destroy.invoke(obj); + } catch (NoSuchMethodException e) { + // не має destroy() — ігноруємо + } catch (Exception e) { + log.warn("Failed to destroy {}", obj.getClass().getSimpleName(), e); } } + public void start(boolean isStartLw) { if (leshanClient != null) { leshanClient.start(); @@ -470,6 +467,7 @@ public class LwM2MTestClient { this.awaitClientAfterStartConnectLw(); } lwM2mTemperatureSensor12.setLeshanClient(leshanClient); + fwLwM2MDevice.setLeshanClient(leshanClient); } } @@ -483,4 +481,59 @@ public class LwM2MTestClient { LwM2mClient lwM2MClient = this.clientContext.getClientByEndpoint(endpoint); Mockito.doAnswer(invocationOnMock -> null).when(defaultLwM2mUplinkMsgHandlerTest).initAttributes(lwM2MClient, true); } + + private ObjectsInitializer createFreshInitializer() { + List models = new ArrayList<>(ObjectLoader.loadAllDefault()); + for (String resourceName : lwm2mClientResources) { + try (InputStream in = LwM2MTestClient.class.getClassLoader() + .getResourceAsStream("lwm2m/" + resourceName)) { + models.addAll(ObjectLoader.loadDdfFile(in, resourceName)); + } catch (IOException | InvalidDDFFileException e) { + log.warn("Failed to load resource {}", resourceName, e); + } + } + if (this.modelResources != null) { + List modelsRes = new ArrayList<>(); + for (String resourceName : this.modelResources) { + try (InputStream in = LwM2MTestClient.class.getClassLoader() + .getResourceAsStream("lwm2m/" + resourceName)) { + modelsRes.addAll(ObjectLoader.loadDdfFile(in, resourceName)); + } catch (IOException | InvalidDDFFileException e) { + log.warn("Failed to load resource {}", resourceName, e); + } + } + Set idsToRemove = modelsRes.stream() + .map(m -> m.id) + .collect(Collectors.toSet()); + models.removeIf(m -> idsToRemove.contains(m.id)); + models.addAll(modelsRes); + } + LwM2mModel model = new StaticModel(models); + return new ObjectsInitializer(model); + } + + private void forceNullSecurityId(Security security) { + if (security == null) { + return; + } + try { + Field field = security.getClass().getDeclaredField("id"); + field.setAccessible(true); + field.set(security, null); + log.info("[forceNullSecurityId] Set id=null for {}", security); + } catch (NoSuchFieldException e) { + try { + //(SecurityObjectInstance) + Field field = security.getClass().getSuperclass().getDeclaredField("id"); + field.setAccessible(true); + field.set(security, null); + log.info("[forceNullSecurityId] Set id=null for {} (via superclass)", security); + } catch (Exception ex) { + log.error("[forceNullSecurityId] Field 'id' not found for {}", security.getClass(), ex); + } + } catch (Exception e) { + log.error("[forceNullSecurityId] Failed to set id=null for {}", security.getClass(), e); + } + } } + diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/ota/AbstractOtaLwM2MIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/ota/AbstractOtaLwM2MIntegrationTest.java index a67f3cf00f..cff56694e5 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/ota/AbstractOtaLwM2MIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/ota/AbstractOtaLwM2MIntegrationTest.java @@ -54,7 +54,6 @@ import static org.thingsboard.server.transport.lwm2m.server.ota.DefaultLwM2MOtaU @DaoSqlTest public abstract class AbstractOtaLwM2MIntegrationTest extends AbstractLwM2MIntegrationTest { - private final String[] RESOURCES_OTA = new String[]{"3.xml", "5.xml", "9.xml", "19.xml"}; 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_OTA9 = "Ota9_Device"; @@ -186,10 +185,6 @@ public abstract class AbstractOtaLwM2MIntegrationTest extends AbstractLwM2MInteg " \"attributeLwm2m\": {}\n" + " }"; - public AbstractOtaLwM2MIntegrationTest() { - setResources(this.RESOURCES_OTA); - } - protected OtaPackageInfo createFirmware(String version, DeviceProfileId deviceProfileId) throws Exception { String CHECKSUM = "4bf5122f344554c53bde2ebb8cd2b7e3d1600ad631c385a5d7cce23c7785459a"; diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/ota/sql/Ota5LwM2MIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/ota/sql/Ota5LwM2MIntegrationTest.java index 3065255687..e4db6f70bf 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/ota/sql/Ota5LwM2MIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/ota/sql/Ota5LwM2MIntegrationTest.java @@ -45,6 +45,7 @@ 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.UPDATED; import static org.thingsboard.server.common.data.ota.OtaPackageUpdateStatus.UPDATING; +import static org.thingsboard.server.dao.service.OtaPackageServiceTest.TARGET_FW_VERSION; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.BINARY_APP_DATA_CONTAINER; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MProfileBootstrapConfigType.NONE; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_0; @@ -91,13 +92,14 @@ public class Ota5LwM2MIntegrationTest extends AbstractOtaLwM2MIntegrationTest { @Test public void testFirmwareUpdateByObject5_Ok() throws Exception { Lwm2mDeviceProfileTransportConfiguration transportConfiguration = getTransportConfiguration(OBSERVE_ATTRIBUTES_WITH_PARAMS_OTA5, getBootstrapServerCredentialsNoSec(NONE)); - DeviceProfile deviceProfile = createLwm2mDeviceProfile("profileFor" + this.CLIENT_ENDPOINT_OTA5, transportConfiguration); - LwM2MDeviceCredentials deviceCredentials = getDeviceCredentialsNoSec(createNoSecClientCredentials(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, device.getId().getId().toString()); + DeviceProfile deviceProfile = createLwm2mDeviceProfile("profileFor" + this.CLIENT_ENDPOINT_OTA5 + "Ok", transportConfiguration); + String endpoint = this.CLIENT_ENDPOINT_OTA5 + "Ok"; + LwM2MDeviceCredentials deviceCredentials = getDeviceCredentialsNoSec(createNoSecClientCredentials(endpoint)); + final Device device = createLwm2mDevice(deviceCredentials, endpoint, deviceProfile.getId()); + createNewClient(SECURITY_NO_SEC, null, false, endpoint, device.getId().getId().toString()); awaitObserveReadAll(5, device.getId().getId().toString()); - device.setFirmwareId(createFirmware("fw.v.1.5.0-update", deviceProfile.getId()).getId()); + device.setFirmwareId(createFirmware(TARGET_FW_VERSION, deviceProfile.getId()).getId()); final Device savedDevice = doPost("/api/device", device, Device.class); assertThat(savedDevice).as("saved device").isNotNull(); @@ -110,7 +112,6 @@ public class Ota5LwM2MIntegrationTest extends AbstractOtaLwM2MIntegrationTest { log.warn("Object5: Got the ts: {}", ts); } - /** * ObjectId = 19/65533/0 * { @@ -133,13 +134,14 @@ public class Ota5LwM2MIntegrationTest extends AbstractOtaLwM2MIntegrationTest { @Test public void testFirmwareUpdateByObject5WithObject19_Ok() throws Exception { Lwm2mDeviceProfileTransportConfiguration transportConfiguration = getTransportConfiguration19(OBSERVE_ATTRIBUTES_WITH_PARAMS_OTA5_19, getBootstrapServerCredentialsNoSec(NONE)); - DeviceProfile deviceProfile = createLwm2mDeviceProfile("profileFor" + this.CLIENT_ENDPOINT_OTA5, transportConfiguration); - LwM2MDeviceCredentials deviceCredentials = getDeviceCredentialsNoSec(createNoSecClientCredentials(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, device.getId().getId().toString()); + DeviceProfile deviceProfile = createLwm2mDeviceProfile("profileFor" + this.CLIENT_ENDPOINT_OTA5 + "19_Ok", transportConfiguration); + String endpoint = this.CLIENT_ENDPOINT_OTA5 + "19_Ok"; + LwM2MDeviceCredentials deviceCredentials = getDeviceCredentialsNoSec(createNoSecClientCredentials(endpoint)); + final Device device = createLwm2mDevice(deviceCredentials, endpoint, deviceProfile.getId()); + createNewClient(SECURITY_NO_SEC, null, false, endpoint, device.getId().getId().toString()); awaitObserveReadAll(6, device.getId().getId().toString()); - OtaPackageInfo otaPackageInfo = createFirmware("fw.v.1.5.0-update", deviceProfile.getId()); + OtaPackageInfo otaPackageInfo = createFirmware(TARGET_FW_VERSION, deviceProfile.getId()); device.setFirmwareId(otaPackageInfo.getId()); final Device savedDevice = doPost("/api/device", device, Device.class); @@ -154,6 +156,6 @@ public class Ota5LwM2MIntegrationTest extends AbstractOtaLwM2MIntegrationTest { String ver_Id_19 = lwM2MTestClient.getLeshanClient().getObjectTree().getModel().getObjectModel(BINARY_APP_DATA_CONTAINER).version; String resourceIdVer = "/" + BINARY_APP_DATA_CONTAINER + "_" + ver_Id_19 + "/" + FW_INSTANCE_ID + "/" + RESOURCE_ID_0; resultReadOtaParams_19(resourceIdVer, otaPackageInfo); - log.warn("Object5: Got the ts: {}", ts); + log.warn("Object5 with Object19: Got the ts: {}", ts); } } diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/AbstractRpcLwM2MIntegrationObserve_Ver_1_0_Test.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/AbstractRpcLwM2MIntegrationObserve_Ver_1_0_Test.java index 7e9efbbf75..f333fd012c 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/AbstractRpcLwM2MIntegrationObserve_Ver_1_0_Test.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/AbstractRpcLwM2MIntegrationObserve_Ver_1_0_Test.java @@ -20,9 +20,8 @@ import org.thingsboard.server.dao.service.DaoSqlTest; @DaoSqlTest public abstract class AbstractRpcLwM2MIntegrationObserve_Ver_1_0_Test extends AbstractRpcLwM2MIntegrationTest{ - public AbstractRpcLwM2MIntegrationObserve_Ver_1_0_Test() { - String[] RESOURCES_RPC_VER_1_1 = new String[]{"3-1_0.xml", "5.xml", "6.xml", "9.xml", "19.xml"}; - setResources(RESOURCES_RPC_VER_1_1); + public AbstractRpcLwM2MIntegrationObserve_Ver_1_0_Test() throws Exception { + String[] RESOURCES_RPC_VER_1_0 = new String[]{"3-1_0.xml", "5.xml", "6.xml", "9.xml", "19.xml"}; + setResources(RESOURCES_RPC_VER_1_0); } } - diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/AbstractRpcLwM2MIntegrationObserve_Ver_1_1_Test.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/AbstractRpcLwM2MIntegrationObserve_Ver_1_1_Test.java index f21c857e8a..b39b3d1130 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/AbstractRpcLwM2MIntegrationObserve_Ver_1_1_Test.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/AbstractRpcLwM2MIntegrationObserve_Ver_1_1_Test.java @@ -20,7 +20,7 @@ import org.thingsboard.server.dao.service.DaoSqlTest; @DaoSqlTest public abstract class AbstractRpcLwM2MIntegrationObserve_Ver_1_1_Test extends AbstractRpcLwM2MIntegrationTest{ - public AbstractRpcLwM2MIntegrationObserve_Ver_1_1_Test() { + public AbstractRpcLwM2MIntegrationObserve_Ver_1_1_Test() throws Exception { String[] RESOURCES_RPC_VER_1_1 = new String[]{"3-1_1.xml", "5.xml", "6.xml", "9.xml", "19.xml"}; setResources(RESOURCES_RPC_VER_1_1); } diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/AbstractRpcLwM2MIntegrationObserve_Ver_1_2_Test.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/AbstractRpcLwM2MIntegrationObserve_Ver_1_2_Test.java index 347d04a900..8c07379004 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/AbstractRpcLwM2MIntegrationObserve_Ver_1_2_Test.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/AbstractRpcLwM2MIntegrationObserve_Ver_1_2_Test.java @@ -20,9 +20,9 @@ import org.thingsboard.server.dao.service.DaoSqlTest; @DaoSqlTest public abstract class AbstractRpcLwM2MIntegrationObserve_Ver_1_2_Test extends AbstractRpcLwM2MIntegrationTest{ - public AbstractRpcLwM2MIntegrationObserve_Ver_1_2_Test() { - String[] RESOURCES_RPC_VER_1_1 = new String[]{"3.xml", "5.xml", "6.xml", "9.xml", "19.xml"}; - setResources(RESOURCES_RPC_VER_1_1); + public AbstractRpcLwM2MIntegrationObserve_Ver_1_2_Test() throws Exception { + String[] RESOURCES_RPC_VER_1_2 = new String[]{"3-1_2.xml", "5.xml", "6.xml", "9.xml", "19.xml"}; + setResources(RESOURCES_RPC_VER_1_2); } } diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/AbstractRpcLwM2MIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/AbstractRpcLwM2MIntegrationTest.java index be85294f08..6c75f7161b 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/AbstractRpcLwM2MIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/AbstractRpcLwM2MIntegrationTest.java @@ -15,7 +15,9 @@ */ package org.thingsboard.server.transport.lwm2m.rpc; +import com.fasterxml.jackson.databind.node.ObjectNode; import lombok.extern.slf4j.Slf4j; +import org.eclipse.leshan.core.ResponseCode; import org.eclipse.leshan.core.link.LinkParser; import org.eclipse.leshan.core.link.lwm2m.DefaultLwM2mLinkParser; import org.junit.Before; @@ -40,15 +42,17 @@ import java.util.concurrent.atomic.AtomicLong; import java.util.function.Predicate; import static org.awaitility.Awaitility.await; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import static org.eclipse.leshan.core.LwM2mId.ACCESS_CONTROL; import static org.eclipse.leshan.core.LwM2mId.DEVICE; import static org.eclipse.leshan.core.LwM2mId.FIRMWARE; +import static org.eclipse.leshan.core.LwM2mId.SECURITY; import static org.eclipse.leshan.core.LwM2mId.SERVER; import static org.eclipse.leshan.core.LwM2mId.SOFTWARE_MANAGEMENT; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.BINARY_APP_DATA_CONTAINER; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MProfileBootstrapConfigType.NONE; -import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.OBJECT_ID_0; -import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.OBJECT_ID_1; 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_12; @@ -102,10 +106,6 @@ public abstract class AbstractRpcLwM2MIntegrationTest extends AbstractLwM2MInteg @SpyBean protected LwM2mTransportServerHelper lwM2mTransportServerHelperTest; - public AbstractRpcLwM2MIntegrationTest() { - setResources(lwm2mClientResources); - } - @Before public void startInitRPC() throws Exception { if (this.getClass().getSimpleName().equals("RpcLwm2mIntegrationWriteCborTest")) { @@ -115,6 +115,10 @@ public abstract class AbstractRpcLwM2MIntegrationTest extends AbstractLwM2MInteg initRpc(0); } else if (this.getClass().getSimpleName().equals("RpcLwm2mIntegrationReadCollectedValueTest")) { initRpc(3303); + } else if (this.getClass().getSimpleName().equals("RpcLwm2mIntegrationInitReadCompositeAllTest")) { + initRpc(2); + }else if (this.getClass().getSimpleName().equals("RpcLwm2mIntegrationInitReadCompositeByObjectTest")) { + initRpc(3); } else { initRpc(1); } @@ -140,10 +144,10 @@ public abstract class AbstractRpcLwM2MIntegrationTest extends AbstractLwM2MInteg }); } }); - String ver_Id_0 = lwM2MTestClient.getLeshanClient().getObjectTree().getModel().getObjectModel(OBJECT_ID_0).version; - String ver_Id_1 = lwM2MTestClient.getLeshanClient().getObjectTree().getModel().getObjectModel(OBJECT_ID_1).version; - objectIdVer_0 = "/" + OBJECT_ID_0 + "_" + ver_Id_0; - objectIdVer_1 = "/" + OBJECT_ID_1 + "_" + ver_Id_1; + String ver_Id_0 = lwM2MTestClient.getLeshanClient().getObjectTree().getModel().getObjectModel(SECURITY).version; + String ver_Id_1 = lwM2MTestClient.getLeshanClient().getObjectTree().getModel().getObjectModel(SERVER).version; + objectIdVer_0 = "/" + SECURITY + "_" + ver_Id_0; + objectIdVer_1 = "/" + SERVER + "_" + ver_Id_1; objectIdVer_2 = (String) expectedObjectIdVers.stream().filter(path -> ((String) path).startsWith("/" + ACCESS_CONTROL)).findFirst().get(); objectIdVer_3 = (String) expectedObjectIdVers.stream().filter(PREDICATE_3).findFirst().get(); objectIdVer_19 = (String) expectedObjectIdVers.stream().filter(path -> ((String) path).startsWith("/" + BINARY_APP_DATA_CONTAINER)).findFirst().get(); @@ -221,10 +225,67 @@ public abstract class AbstractRpcLwM2MIntegrationTest extends AbstractLwM2MInteg " ],\n" + " \"attributeLwm2m\": {}\n" + " }"; + String INIT_READ_TELEMETRY_ATTRIBUTE_AS_OBSERVE_STRATEGY_ALL = + " {\n" + + " \"keyName\": {\n" + + " \"/3_1.2/0/9\": \"batteryLevel\",\n" + + " \"/3_1.2/0/20\": \"batteryStatus\",\n" + + " \"/19_1.1/0/2\": \"dataCreationTime\",\n" + + " \"/5_1.2/0/6\": \"pkgname\",\n" + + " \"/5_1.2/0/7\": \"pkgversion\",\n" + + " \"/5_1.2/0/9\": \"firmwareUpdateDeliveryMethod\"\n" + + " },\n" + + " \"observe\": [\n" + + " \"/3_1.2/0/20\"\n" + + " ],\n" + + " \"attribute\": [\n" + + " \"/5_1.2/0/6\",\n" + + " \"/5_1.2/0/7\"\n" + + " ],\n" + + " \"telemetry\": [\n" + + " \"/3_1.2/0/9\",\n" + + " \"/3_1.2/0/20\",\n" + + " \"/5_1.2/0/9\",\n" + + " \"/19_1.1/0/2\"\n" + + " ],\n" + + " \"attributeLwm2m\": {},\n" + + " \"initAttrTelAsObsStrategy\": true,\n" + + " \"observeStrategy\": 1\n" + + " }"; + String INIT_READ_TELEMETRY_ATTRIBUTE_AS_OBSERVE_STRATEGY_BY_OBJECT = + " {\n" + + " \"keyName\": {\n" + + " \"/3_1.2/0/9\": \"batteryLevel\",\n" + + " \"/3_1.2/0/20\": \"batteryStatus\",\n" + + " \"/19_1.1/0/2\": \"dataCreationTime\",\n" + + " \"/5_1.2/0/6\": \"pkgname\",\n" + + " \"/5_1.2/0/7\": \"pkgversion\",\n" + + " \"/5_1.2/0/9\": \"firmwareUpdateDeliveryMethod\"\n" + + " },\n" + + " \"observe\": [\n" + + " \"/3_1.2/0/9\"\n" + + " ],\n" + + " \"attribute\": [\n" + + " \"/5_1.2/0/6\",\n" + + " \"/5_1.2/0/7\"\n" + + " ],\n" + + " \"telemetry\": [\n" + + " \"/3_1.2/0/9\",\n" + + " \"/3_1.2/0/20\",\n" + + " \"/5_1.2/0/9\",\n" + + " \"/19_1.1/0/2\"\n" + + " ],\n" + + " \"attributeLwm2m\": {},\n" + + " \"initAttrTelAsObsStrategy\": true,\n" + + " \"observeStrategy\": 2\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 2 -> INIT_READ_TELEMETRY_ATTRIBUTE_AS_OBSERVE_STRATEGY_ALL; + case 3 -> INIT_READ_TELEMETRY_ATTRIBUTE_AS_OBSERVE_STRATEGY_BY_OBJECT; case 3303 -> TELEMETRY_WITH_PARAMS_RPC_COLLECTED_VALUE; default -> throw new IllegalStateException("Unexpected value: " + typeConfigProfile); }; @@ -264,19 +325,6 @@ public abstract class AbstractRpcLwM2MIntegrationTest extends AbstractLwM2MInteg .count(); } - protected void updateRegAtLeastOnceAfterAction() { - long initialInvocationCount = countUpdateReg(); - AtomicLong newInvocationCount = new AtomicLong(initialInvocationCount); - log.trace("updateRegAtLeastOnceAfterAction: initialInvocationCount [{}]", initialInvocationCount); - await("Update Registration at-least-once after action") - .atMost(50, TimeUnit.SECONDS) - .until(() -> { - newInvocationCount.set(countUpdateReg()); - return newInvocationCount.get() > initialInvocationCount; - }); - log.trace("updateRegAtLeastOnceAfterAction: newInvocationCount [{}]", newInvocationCount.get()); - } - protected long countSendParametersOnThingsboardTelemetryResource(String rezName) { return Mockito.mockingDetails(lwM2mTransportServerHelperTest) .getInvocations().stream() @@ -290,4 +338,36 @@ public abstract class AbstractRpcLwM2MIntegrationTest extends AbstractLwM2MInteg ) .count(); } + + protected String sendDiscover(String path) throws Exception { + String setRpcRequest = "{\"method\": \"Discover\", \"params\": {\"id\": \"" + path + "\"}}"; + return doPostAsync("/api/plugins/rpc/twoway/" + lwM2MTestClient.getDeviceIdStr(), setRpcRequest, String.class, status().isOk()); + } + + protected String sendRpcObserveReadAllWithResult() throws Exception { + ObjectNode rpcActualResult = sendRpcObserveWithResult("ObserveReadAll", null); + assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText()); + return rpcActualResult.get("value").asText(); + } + + protected String sendRpcObserveReadAllWithResult(String params) throws Exception { + sendRpcObserveOk("Observe", params); + ObjectNode rpcActualResult = sendRpcObserveWithResult("ObserveReadAll", null); + assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText()); + return rpcActualResult.get("value").asText(); + } + + protected void testObserveOneResourceValue_Count_4_CancelAll_Reboot_After_Observe_Count_4(String expectedIdVer) throws Exception { + String expectedIdObserve = "SingleObservation:/3/0/9"; + sendObserveCancelAllWithAwait(lwM2MTestClient.getDeviceIdStr()); + updateRegAtLeastOnceAfterAction(); + lwM2MTestClient.getLeshanClient().stop(false); + lwM2MTestClient.getLeshanClient().start(); + updateRegAtLeastOnceAfterAction(); + awaitObserveReadAll(4,lwM2MTestClient.getDeviceIdStr()); + String actualIdVer = sendDiscover(objectIdVer_3); + assertTrue(actualIdVer.contains(expectedIdVer)); + String actualAllObserve = sendRpcObserveReadAllWithResult(); + assertTrue(actualAllObserve.contains(expectedIdObserve)); + } } diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationDiscoverTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationDiscoverTest.java index dceadfb7b5..77960b1c54 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationDiscoverTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationDiscoverTest.java @@ -192,11 +192,6 @@ public class RpcLwm2mIntegrationDiscoverTest extends AbstractRpcLwM2MIntegration assertTrue(rpcActualResult.get("error").asText().contains(expected)); } - private String sendDiscover(String path) throws Exception { - String setRpcRequest = "{\"method\": \"Discover\", \"params\": {\"id\": \"" + path + "\"}}"; - return doPostAsync("/api/plugins/rpc/twoway/" + lwM2MTestClient.getDeviceIdStr(), setRpcRequest, String.class, status().isOk()); - } - private String convertObjectIdToVerId(String path, String ver) { ver = ver != null ? ver : TbLwM2mVersion.VERSION_1_0.getVersion().toString(); try { diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationDiscoverWriteAttributesTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationDiscoverWriteAttributesTest.java index bd1216eb06..2a4a401107 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationDiscoverWriteAttributesTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationDiscoverWriteAttributesTest.java @@ -166,9 +166,4 @@ public class RpcLwm2mIntegrationDiscoverWriteAttributesTest extends AbstractRpcL String setRpcRequest = "{\"method\": \"WriteAttributes\", \"params\": {\"id\": \"" + path + "\", \"attributes\": " + value + " }}"; return doPostAsync("/api/plugins/rpc/twoway/" + lwM2MTestClient.getDeviceIdStr(), setRpcRequest, String.class, status().isOk()); } - - private String sendDiscover(String path) throws Exception { - String setRpcRequest = "{\"method\": \"Discover\", \"params\": {\"id\": \"" + path + "\"}}"; - return doPostAsync("/api/plugins/rpc/twoway/" + lwM2MTestClient.getDeviceIdStr(), setRpcRequest, String.class, status().isOk()); - } } diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationInitReadCompositeAllTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationInitReadCompositeAllTest.java new file mode 100644 index 0000000000..62af7f00f3 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationInitReadCompositeAllTest.java @@ -0,0 +1,92 @@ +/** + * Copyright © 2016-2025 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.core.type.TypeReference; +import com.fasterxml.jackson.databind.node.ObjectNode; +import lombok.extern.slf4j.Slf4j; +import org.junit.Test; +import org.thingsboard.server.transport.lwm2m.rpc.AbstractRpcLwM2MIntegrationTest; + +import java.util.List; +import java.util.concurrent.atomic.AtomicReference; + +import static java.util.concurrent.TimeUnit.SECONDS; +import static org.awaitility.Awaitility.await; +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_6; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_7; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_9; + +@Slf4j +public class RpcLwm2mIntegrationInitReadCompositeAllTest extends AbstractRpcLwM2MIntegrationTest { + + /** + " \"/3_1.2/0/9\": \"batteryLevel\", - Telemetry + " \"/3_1.2/0/20\": \"batteryStatus\" - Observe, Telemetry + " \"/5_1.2/0/6\": \"pkgname\" - Attributes + " \"/5_1.2/0/7\": \"pkgversion\" - Attributes + " \"/5_1.2/0/9\": \"firmwareUpdateDeliveryMethod\"\ - Telemetry + " \"/19_1.1/0/2\": \"dataCreationTime\" - Telemetry + * "observeStrategy": 1 + */ + @Test + public void testInitReadCompositeAsObserveStrategyCompositeAll() throws Exception { + + + // init test + String RESOURCE_3_9 = "batteryLevel"; + String RESOURCE_3_20 = "batteryStatus"; + String RESOURCE_5_6 = "pkgname"; + String RESOURCE_5_7 = "pkgversion"; + String RESOURCE_5_9 = "firmwareUpdateDeliveryMethod"; + String RESOURCE_19_2 = "dataCreationTime"; + + String idVwr_3_0_20 = idVer_3_0_9 = objectIdVer_3 + "/" + OBJECT_INSTANCE_ID_0 + "/" + 20; + String IdVer5_0_6 = objectInstanceIdVer_5 + "/" + RESOURCE_ID_6; + String IdVer5_0_7 = objectInstanceIdVer_5 + "/" + RESOURCE_ID_7; + String IdVer5_0_9 = objectInstanceIdVer_5 + "/" + RESOURCE_ID_9; + String idVer_19_0_2 = objectIdVer_19 + "/" + OBJECT_INSTANCE_ID_0 + "/" + RESOURCE_ID_2; + countUpdateAttrTelemetryResource(idVer_3_0_9); + countUpdateAttrTelemetryResource(idVwr_3_0_20); + countUpdateAttrTelemetryResource(IdVer5_0_6); + countUpdateAttrTelemetryResource(IdVer5_0_7); + countUpdateAttrTelemetryResource(IdVer5_0_9); + countUpdateAttrTelemetryResource(idVer_19_0_2); + + + AtomicReference actualValues = new AtomicReference<>(); + await().atMost(40, SECONDS).until(() -> { + actualValues.set(doGetAsync( + "/api/plugins/telemetry/DEVICE/" + lwM2MTestClient.getDeviceIdStr() + "/values/timeseries?keys=" + + RESOURCE_3_9 + "," + RESOURCE_3_20 + "," + RESOURCE_5_9 + "," + RESOURCE_19_2, ObjectNode.class)); + return actualValues.get() != null && !actualValues.get().isEmpty() + && !actualValues.get().get(RESOURCE_3_9).isEmpty() + && !actualValues.get().get(RESOURCE_3_20).isEmpty() + && !actualValues.get().get(RESOURCE_5_9).isEmpty() + && !actualValues.get().get(RESOURCE_19_2).isEmpty(); + }); + + AtomicReference> actualKeys =new AtomicReference<>(); + await().atMost(40, SECONDS).until(() -> { + actualKeys.set(doGetAsyncTyped("/api/plugins/telemetry/DEVICE/" + lwM2MTestClient.getDeviceIdStr() + "/keys/attributes/CLIENT_SCOPE", new TypeReference<>() { + })); + return actualKeys.get() != null && !actualKeys.get().isEmpty() && !actualKeys.get().isEmpty() + && actualKeys.get().contains(RESOURCE_5_6)&& actualKeys.get().contains(RESOURCE_5_7); + }); + } +} diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationInitReadCompositeByObjectTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationInitReadCompositeByObjectTest.java new file mode 100644 index 0000000000..aea9755d0c --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationInitReadCompositeByObjectTest.java @@ -0,0 +1,92 @@ +/** + * Copyright © 2016-2025 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.core.type.TypeReference; +import com.fasterxml.jackson.databind.node.ObjectNode; +import lombok.extern.slf4j.Slf4j; +import org.junit.Test; +import org.thingsboard.server.transport.lwm2m.rpc.AbstractRpcLwM2MIntegrationTest; + +import java.util.List; +import java.util.concurrent.atomic.AtomicReference; + +import static java.util.concurrent.TimeUnit.SECONDS; +import static org.awaitility.Awaitility.await; +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_6; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_7; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_9; + +@Slf4j +public class RpcLwm2mIntegrationInitReadCompositeByObjectTest extends AbstractRpcLwM2MIntegrationTest { + + /** + " \"/3_1.2/0/9\": \"batteryLevel\", - Telemetry + " \"/3_1.2/0/20\": \"batteryStatus\" - Observe, Telemetry + " \"/5_1.2/0/6\": \"pkgname\" - Attributes + " \"/5_1.2/0/7\": \"pkgversion\" - Attributes + " \"/5_1.2/0/9\": \"firmwareUpdateDeliveryMethod\"\ - Telemetry + " \"/19_1.1/0/2\": \"dataCreationTime\" - Telemetry + * "observeStrategy": 2 + */ + @Test + public void testInitReadCompositeAsObserveStrategyCompositeByObject() throws Exception { + + + // init test + String RESOURCE_3_9 = "batteryLevel"; + String RESOURCE_3_20 = "batteryStatus"; + String RESOURCE_5_6 = "pkgname"; + String RESOURCE_5_7 = "pkgversion"; + String RESOURCE_5_9 = "firmwareUpdateDeliveryMethod"; + String RESOURCE_19_2 = "dataCreationTime"; + + String idVwr_3_0_20 = idVer_3_0_9 = objectIdVer_3 + "/" + OBJECT_INSTANCE_ID_0 + "/" + 20; + String IdVer5_0_6 = objectInstanceIdVer_5 + "/" + RESOURCE_ID_6; + String IdVer5_0_7 = objectInstanceIdVer_5 + "/" + RESOURCE_ID_7; + String IdVer5_0_9 = objectInstanceIdVer_5 + "/" + RESOURCE_ID_9; + String idVer_19_0_2 = objectIdVer_19 + "/" + OBJECT_INSTANCE_ID_0 + "/" + RESOURCE_ID_2; + countUpdateAttrTelemetryResource(idVer_3_0_9); + countUpdateAttrTelemetryResource(idVwr_3_0_20); + countUpdateAttrTelemetryResource(IdVer5_0_6); + countUpdateAttrTelemetryResource(IdVer5_0_7); + countUpdateAttrTelemetryResource(IdVer5_0_9); + countUpdateAttrTelemetryResource(idVer_19_0_2); + + + AtomicReference actualValues = new AtomicReference<>(); + await().atMost(40, SECONDS).until(() -> { + actualValues.set(doGetAsync( + "/api/plugins/telemetry/DEVICE/" + lwM2MTestClient.getDeviceIdStr() + "/values/timeseries?keys=" + + RESOURCE_3_9 + "," + RESOURCE_3_20 + "," + RESOURCE_5_9 + "," + RESOURCE_19_2, ObjectNode.class)); + return actualValues.get() != null && !actualValues.get().isEmpty() + && !actualValues.get().get(RESOURCE_3_9).isEmpty() + && !actualValues.get().get(RESOURCE_3_20).isEmpty() + && !actualValues.get().get(RESOURCE_5_9).isEmpty() + && !actualValues.get().get(RESOURCE_19_2).isEmpty(); + }); + + AtomicReference> actualKeys =new AtomicReference<>(); + await().atMost(40, SECONDS).until(() -> { + actualKeys.set(doGetAsyncTyped("/api/plugins/telemetry/DEVICE/" + lwM2MTestClient.getDeviceIdStr() + "/keys/attributes/CLIENT_SCOPE", new TypeReference<>() { + })); + return actualKeys.get() != null && !actualKeys.get().isEmpty() && !actualKeys.get().isEmpty() + && actualKeys.get().contains(RESOURCE_5_6)&& actualKeys.get().contains(RESOURCE_5_7); + }); + } +} diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationObserveTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationObserveTest.java index 483442382d..505cebff69 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationObserveTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationObserveTest.java @@ -335,12 +335,5 @@ public class RpcLwm2mIntegrationObserveTest extends AbstractRpcLwM2MIntegrationT sendRpcObserveOk("Observe", expectedId_1); sendRpcObserveOk("Observe", expectedId_2); } - - private String sendRpcObserveReadAllWithResult(String params) throws Exception { - sendRpcObserveOk("Observe", params); - ObjectNode rpcActualResult = sendRpcObserveWithResult("ObserveReadAll", null); - assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText()); - return rpcActualResult.get("value").asText(); - } } diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationObserve_Ver_1_0_Test.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationObserveVer10Test.java similarity index 71% rename from application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationObserve_Ver_1_0_Test.java rename to application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationObserveVer10Test.java index 3d3640cb2e..e6f035e5cc 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationObserve_Ver_1_0_Test.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationObserveVer10Test.java @@ -19,13 +19,14 @@ import lombok.extern.slf4j.Slf4j; import org.junit.Before; import org.junit.Test; import org.thingsboard.server.transport.lwm2m.rpc.AbstractRpcLwM2MIntegrationObserve_Ver_1_0_Test; -import org.thingsboard.server.transport.lwm2m.rpc.AbstractRpcLwM2MIntegrationObserve_Ver_1_1_Test; - import static org.junit.Assert.assertTrue; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_NAME_3_9; @Slf4j -public class RpcLwm2mIntegrationObserve_Ver_1_0_Test extends AbstractRpcLwM2MIntegrationObserve_Ver_1_0_Test { +public class RpcLwm2mIntegrationObserveVer10Test extends AbstractRpcLwM2MIntegrationObserve_Ver_1_0_Test { + + public RpcLwm2mIntegrationObserveVer10Test() throws Exception { + } @Before public void setupObserveTest() throws Exception { @@ -44,5 +45,21 @@ public class RpcLwm2mIntegrationObserve_Ver_1_0_Test extends AbstractRpcLwM2MInt updateRegAtLeastOnceAfterAction(); long lastSendTelemetryAtCount = countSendParametersOnThingsboardTelemetryResource(RESOURCE_ID_NAME_3_9); assertTrue(lastSendTelemetryAtCount > initSendTelemetryAtCount); + awaitObserveReadAll(1,lwM2MTestClient.getDeviceIdStr()); + } + + /** + * "3_1.0/0/9" + * Observe count 4 + * CancelAll Observe + * Reboot + * Observe count 4 contains + * "/3_1.0" - Discover Object - find ver + * @throws Exception + */ + @Test + public void testObserveOneResourceValue_Count_4_CancelAll_Reboot_After_Observe_Count_4_ObjectVer_1_0() throws Exception { + String expectedIdVer = ";ver=1.0"; + testObserveOneResourceValue_Count_4_CancelAll_Reboot_After_Observe_Count_4(expectedIdVer); } } diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationObserve_Ver_1_1_Test.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationObserveVer11Test.java similarity index 72% rename from application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationObserve_Ver_1_1_Test.java rename to application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationObserveVer11Test.java index a4f7727773..a64ff43b92 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationObserve_Ver_1_1_Test.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationObserveVer11Test.java @@ -23,7 +23,10 @@ import static org.junit.Assert.assertTrue; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_NAME_3_9; @Slf4j -public class RpcLwm2mIntegrationObserve_Ver_1_1_Test extends AbstractRpcLwM2MIntegrationObserve_Ver_1_1_Test { +public class RpcLwm2mIntegrationObserveVer11Test extends AbstractRpcLwM2MIntegrationObserve_Ver_1_1_Test { + + public RpcLwm2mIntegrationObserveVer11Test() throws Exception { + } @Before public void setupObserveTest() throws Exception { @@ -43,4 +46,19 @@ public class RpcLwm2mIntegrationObserve_Ver_1_1_Test extends AbstractRpcLwM2MInt long lastSendTelemetryAtCount = countSendParametersOnThingsboardTelemetryResource(RESOURCE_ID_NAME_3_9); assertTrue(lastSendTelemetryAtCount > initSendTelemetryAtCount); } + + /** + * "3_1.1/0/9" + * Observe count 4 + * CancelAll Observe + * Reboot + * Observe count 4 contains + * "/3" - Discover Object - find ver (lwm2mVersion == 1.1) + * @throws Exception + */ + @Test + public void testObserveOneResourceValue_Count_4_CancelAll_Reboot_After_Observe_Count_4_ObjectVer_1_1() throws Exception { + String expectedIdVer = ""; + testObserveOneResourceValue_Count_4_CancelAll_Reboot_After_Observe_Count_4(expectedIdVer); + } } diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationObserve_Ver_1_2_Test.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationObserveVer12Test.java similarity index 73% rename from application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationObserve_Ver_1_2_Test.java rename to application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationObserveVer12Test.java index a166e0139b..89062dd303 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationObserve_Ver_1_2_Test.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationObserveVer12Test.java @@ -18,14 +18,16 @@ package org.thingsboard.server.transport.lwm2m.rpc.sql; import lombok.extern.slf4j.Slf4j; import org.junit.Before; import org.junit.Test; -import org.thingsboard.server.transport.lwm2m.rpc.AbstractRpcLwM2MIntegrationObserve_Ver_1_0_Test; import org.thingsboard.server.transport.lwm2m.rpc.AbstractRpcLwM2MIntegrationObserve_Ver_1_2_Test; import static org.junit.Assert.assertTrue; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_NAME_3_9; @Slf4j -public class RpcLwm2mIntegrationObserve_Ver_1_2_Test extends AbstractRpcLwM2MIntegrationObserve_Ver_1_2_Test { +public class RpcLwm2mIntegrationObserveVer12Test extends AbstractRpcLwM2MIntegrationObserve_Ver_1_2_Test { + + public RpcLwm2mIntegrationObserveVer12Test() throws Exception { + } @Before public void setupObserveTest() throws Exception { @@ -45,4 +47,19 @@ public class RpcLwm2mIntegrationObserve_Ver_1_2_Test extends AbstractRpcLwM2MInt long lastSendTelemetryAtCount = countSendParametersOnThingsboardTelemetryResource(RESOURCE_ID_NAME_3_9); assertTrue(lastSendTelemetryAtCount > initSendTelemetryAtCount); } + + /** + * "3_1.2/0/9" + * Observe count 4 + * CancelAll Observe + * Reboot + * Observe count 4 contains + * "/3_1.2" - Discover Object - find ver + * @throws Exception + */ + @Test + public void testObserveOneResourceValue_Count_4_CancelAll_Reboot_After_Observe_Count_4_ObjectVer_1_2() throws Exception { + String expectedIdVer = ";ver=1.2"; + testObserveOneResourceValue_Count_4_CancelAll_Reboot_After_Observe_Count_4(expectedIdVer); + } } diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationReadTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationReadTest.java index 90b373b16c..c0779ad944 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationReadTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationReadTest.java @@ -21,8 +21,10 @@ import org.eclipse.leshan.core.ResponseCode; import org.eclipse.leshan.core.node.LwM2mPath; import org.junit.Test; import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.transport.lwm2m.rpc.AbstractRpcLwM2MIntegrationTest; +import static org.eclipse.leshan.core.LwM2mId.ACCESS_CONTROL; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -53,18 +55,18 @@ public class RpcLwm2mIntegrationReadTest extends AbstractRpcLwM2MIntegrationTest try { expectedObjectIdVers.forEach(expected -> { try { - String actualResult = sendRPCById((String) expected); String expectedObjectId = pathIdVerToObjectId((String) expected); LwM2mPath expectedPath = new LwM2mPath(expectedObjectId); - ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); - assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText()); - String expectedObjectInstances = "LwM2mObject [id=" + expectedPath.getObjectId() + ", instances={0=LwM2mObjectInstance [id=0, resources="; - if (expectedPath.getObjectId() == 1) { - expectedObjectInstances = "LwM2mObject [id=1, instances={1="; - } else if (expectedPath.getObjectId() == 2) { - expectedObjectInstances = "LwM2mObject [id=2, instances={}]"; + if (expectedPath.getObjectId() > ACCESS_CONTROL) { + String actualResult = sendRPCByIdSync((String) expected); + if (StringUtils.isNoneBlank(actualResult)) { + log.warn(" expectedPath: [{}]", expectedPath); + ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); + assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText()); + String expectedObjectInstances = "LwM2mObject [id=" + expectedPath.getObjectId() + ", instances={0=LwM2mObjectInstance [id=0, resources="; + assertTrue(rpcActualResult.get("value").asText().contains(expectedObjectInstances)); + } } - assertTrue(rpcActualResult.get("value").asText().contains(expectedObjectInstances)); } catch (Exception e) { e.printStackTrace(); } @@ -83,7 +85,7 @@ public class RpcLwm2mIntegrationReadTest extends AbstractRpcLwM2MIntegrationTest public void testReadAllInstancesInClientById_Result_CONTENT_Value_IsInstances_IsResources() throws Exception { expectedObjectIdVerInstances.forEach(expected -> { try { - String actualResult = sendRPCById((String) expected); + String actualResult = sendRPCByIdAsync((String) expected); String expectedObjectId = pathIdVerToObjectId((String) expected); LwM2mPath expectedPath = new LwM2mPath(expectedObjectId); ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); @@ -104,7 +106,7 @@ public class RpcLwm2mIntegrationReadTest extends AbstractRpcLwM2MIntegrationTest @Test public void testReadMultipleResourceById_Result_CONTENT_Value_IsLwM2mMultipleResource() throws Exception { String expectedIdVer = objectInstanceIdVer_3 + "/" + RESOURCE_ID_11; - String actualResult = sendRPCById(expectedIdVer); + String actualResult = sendRPCByIdAsync(expectedIdVer); ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText()); String expected = "LwM2mMultipleResource [id=" + RESOURCE_ID_11 + ", values={"; @@ -117,7 +119,7 @@ public class RpcLwm2mIntegrationReadTest extends AbstractRpcLwM2MIntegrationTest @Test public void testReadSingleResourceById_Result_CONTENT_Value_IsLwM2mSingleResource() throws Exception { String expectedIdVer = objectInstanceIdVer_3 + "/" + RESOURCE_ID_14; - String actualResult = sendRPCById(expectedIdVer); + String actualResult = sendRPCByIdAsync(expectedIdVer); ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText()); String expected = "LwM2mSingleResource [id=" + RESOURCE_ID_14 + ", value="; @@ -228,10 +230,14 @@ public class RpcLwm2mIntegrationReadTest extends AbstractRpcLwM2MIntegrationTest assertEquals(actualValue, expectedValue); } - private String sendRPCById(String path) throws Exception { + private String sendRPCByIdAsync(String path) throws Exception { String setRpcRequest = "{\"method\": \"Read\", \"params\": {\"id\": \"" + path + "\"}}"; return doPostAsync("/api/plugins/rpc/twoway/" + lwM2MTestClient.getDeviceIdStr(), setRpcRequest, String.class, status().isOk()); } + private String sendRPCByIdSync(String path) throws Exception { + String setRpcRequest = "{\"method\": \"Read\", \"params\": {\"id\": \"" + path + "\"}}"; + return doPost("/api/plugins/rpc/twoway/" + lwM2MTestClient.getDeviceIdStr(), setRpcRequest, String.class, status().isOk()); + } private String sendRPCByKey(String key) throws Exception { String setRpcRequest = "{\"method\": \"Read\", \"params\": {\"key\": \"" + key + "\"}}"; diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/AbstractSecurityLwM2MIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/AbstractSecurityLwM2MIntegrationTest.java index 6dda2de9fe..e642035b32 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/AbstractSecurityLwM2MIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/AbstractSecurityLwM2MIntegrationTest.java @@ -22,6 +22,7 @@ import org.eclipse.leshan.client.object.Security; import org.eclipse.leshan.core.ResponseCode; import org.eclipse.leshan.core.util.Hex; import org.junit.Assert; +import org.junit.Before; import org.springframework.test.web.servlet.MvcResult; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.Device; @@ -69,6 +70,7 @@ import java.util.concurrent.TimeUnit; import static org.awaitility.Awaitility.await; import static org.eclipse.leshan.client.object.Security.noSecBootstrap; import static org.eclipse.leshan.client.object.Security.psk; +import static org.eclipse.leshan.core.LwM2mId.SERVER; import static org.junit.Assert.assertEquals; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import static org.thingsboard.server.common.data.device.credentials.lwm2m.LwM2MSecurityMode.PSK; @@ -77,7 +79,7 @@ import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MClient import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MClientState.ON_REGISTRATION_STARTED; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MClientState.ON_REGISTRATION_SUCCESS; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MClientState.ON_UPDATE_SUCCESS; -import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.OBJECT_ID_1; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.OBJECT_INSTANCE_ID_0; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_9; @DaoSqlTest @@ -95,7 +97,6 @@ public abstract class AbstractSecurityLwM2MIntegrationTest extends AbstractLwM2M protected static final String SERVER_STORE_PWD = "server_ks_password"; protected static final String SERVER_CERT_ALIAS = "server"; protected static final String SERVER_CERT_ALIAS_BS = "bootstrap"; - protected static final Security SECURITY_NO_SEC_BS = noSecBootstrap(URI_BS);; protected final X509Certificate serverX509Cert; // server certificate signed by rootCA protected final X509Certificate serverX509CertBs; // serverBs certificate signed by rootCA protected final PublicKey serverPublicKeyFromCert; // server public key used for RPK @@ -119,7 +120,6 @@ public abstract class AbstractSecurityLwM2MIntegrationTest extends AbstractLwM2M protected final PrivateKey clientPrivateKeyFromCertTrust; // client private key used for X509 and RPK protected final X509Certificate clientX509CertTrustNo; // client certificate signed by intermediate, rootCA with a good CN ("host name") protected final PrivateKey clientPrivateKeyFromCertTrustNo; // client private key used for X509 and RPK - private final String[] RESOURCES_SECURITY = new String[]{"1.xml", "2.xml", "3.xml", "5.xml", "9.xml", "19.xml"}; private final LwM2MBootstrapClientCredentials defaultBootstrapCredentials; @@ -134,7 +134,6 @@ public abstract class AbstractSecurityLwM2MIntegrationTest extends AbstractLwM2M public AbstractSecurityLwM2MIntegrationTest() { // create client credentials - setResources(this.RESOURCES_SECURITY); try { // Get certificates from key store char[] clientKeyStorePwd = CLIENT_STORE_PWD.toCharArray(); @@ -178,20 +177,26 @@ public abstract class AbstractSecurityLwM2MIntegrationTest extends AbstractLwM2M defaultBootstrapCredentials.setLwm2mServer(serverCredentials); } - public void basicTestConnectionBefore(String clientEndpoint, - String awaitAlias, - LwM2MProfileBootstrapConfigType type, - Set expectedStatuses, - LwM2MClientState finishState) throws Exception { + @Before + public void init() throws Exception { + String[] RESOURCES_SECURITY = new String[]{"3-1_2.xml", "5.xml", "6.xml", "9.xml", "19.xml"}; + setResources(RESOURCES_SECURITY); + } + + public void basicTestConnectionStartBS(String clientEndpoint, + String awaitAlias, + LwM2MProfileBootstrapConfigType type, + Set expectedStatuses, + LwM2MClientState finishState) throws Exception { Lwm2mDeviceProfileTransportConfiguration transportConfiguration = getTransportConfiguration(OBSERVE_ATTRIBUTES_WITHOUT_PARAMS, getBootstrapServerCredentialsNoSec(type)); LwM2MDeviceCredentials deviceCredentials = getDeviceCredentialsNoSec(createNoSecClientCredentials(clientEndpoint)); - this.basicTestConnection(null , SECURITY_NO_SEC_BS, + this.basicTestConnection(null , noSecBootstrap(URI_BS), deviceCredentials, clientEndpoint, transportConfiguration, awaitAlias, expectedStatuses, - true, + false, finishState, false); } @@ -231,12 +236,19 @@ public abstract class AbstractSecurityLwM2MIntegrationTest extends AbstractLwM2M } - public void basicTestConnectionBootstrapRequestTriggerBefore(String clientEndpoint, String awaitAlias, LwM2MProfileBootstrapConfigType type) throws Exception { - Lwm2mDeviceProfileTransportConfiguration transportConfiguration = getTransportConfiguration(OBSERVE_ATTRIBUTES_WITHOUT_PARAMS, getBootstrapServerCredentialsNoSec(type)); + public void basicTestConnectionBootstrapRequestTriggerBefore(String clientEndpoint, String awaitAlias, LwM2MProfileBootstrapConfigType type, int cnt) throws Exception { + List bootstrapServerCredentialsNoSec = getBootstrapServerCredentialsNoSec(type); + for (int i = 2; i <= cnt; i++) { + AbstractLwM2MBootstrapServerCredential bsCredential = getBootstrapServerCredentialNoSec(false); + bsCredential.setHost("0.0.0." + i); + bsCredential.setShortServerId(bsCredential.getShortServerId() + i); + bootstrapServerCredentialsNoSec.add(bsCredential); + } + Lwm2mDeviceProfileTransportConfiguration transportConfiguration = getTransportConfiguration(OBSERVE_ATTRIBUTES_WITHOUT_PARAMS, bootstrapServerCredentialsNoSec); LwM2MDeviceCredentials deviceCredentials = getDeviceCredentialsNoSec(createNoSecClientCredentials(clientEndpoint)); this.basicTestConnectionBootstrapRequestTrigger( SECURITY_NO_SEC, - SECURITY_NO_SEC_BS, + noSecBootstrap(URI_BS), deviceCredentials, clientEndpoint, transportConfiguration, @@ -275,8 +287,8 @@ public abstract class AbstractSecurityLwM2MIntegrationTest extends AbstractLwM2M }); Assert.assertTrue(lwM2MTestClient.getClientStates().containsAll(expectedStatusesLwm2m)); - String executedPath = "/" + OBJECT_ID_1 + "_" + lwM2MTestClient.getLeshanClient().getObjectTree().getModel().getObjectModel(OBJECT_ID_1).version - + "/" +serverId + "/" + RESOURCE_ID_9; + String executedPath = "/" + SERVER + "_" + lwM2MTestClient.getLeshanClient().getObjectTree().getModel().getObjectModel(SERVER).version + + "/" + OBJECT_INSTANCE_ID_0 + "/" + RESOURCE_ID_9; lwM2MTestClient.setClientStates(new HashSet<>()); String actualResult = sendRPCSecurityExecuteById(executedPath, deviceIdStr, endpoint); ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); @@ -352,7 +364,7 @@ public abstract class AbstractSecurityLwM2MIntegrationTest extends AbstractLwM2M default: throw new IllegalStateException("Unexpected value: " + mode); } - bootstrapServerCredential.setShortServerId(isBootstrap ? shortServerIdBs0 : shortServerId); + bootstrapServerCredential.setShortServerId(isBootstrap ? null : shortServerId); bootstrapServerCredential.setBootstrapServerIs(isBootstrap); bootstrapServerCredential.setHost(isBootstrap ? hostBs : host); bootstrapServerCredential.setPort(isBootstrap ? securityPortBs : securityPort); diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/cid/AbstractSecurityLwM2MIntegrationDtlsCidLength0Test.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/cid/AbstractSecurityLwM2MIntegrationDtlsCidLength0Test.java index 7a0b0f8580..a3229a7c19 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/cid/AbstractSecurityLwM2MIntegrationDtlsCidLength0Test.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/cid/AbstractSecurityLwM2MIntegrationDtlsCidLength0Test.java @@ -29,10 +29,12 @@ import org.thingsboard.server.dao.service.DaoSqlTest; public abstract class AbstractSecurityLwM2MIntegrationDtlsCidLength0Test extends AbstractSecurityLwM2MIntegrationDtlsCidLengthTest { + private static final Integer serverDtlsCidLength = 0; + protected void testNoSecDtlsCidLength(Integer dtlsCidLength) throws Exception { - testNoSecDtlsCidLength(dtlsCidLength, 0); + testNoSecDtlsCidLength(dtlsCidLength, serverDtlsCidLength); } protected void testPskDtlsCidLength(Integer dtlsCidLength) throws Exception { - testPskDtlsCidLength(dtlsCidLength, 0); + testPskDtlsCidLength(dtlsCidLength, serverDtlsCidLength); } } diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/cid/AbstractSecurityLwM2MIntegrationDtlsCidLength16Test.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/cid/AbstractSecurityLwM2MIntegrationDtlsCidLength16Test.java new file mode 100644 index 0000000000..1f5d2d9f19 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/cid/AbstractSecurityLwM2MIntegrationDtlsCidLength16Test.java @@ -0,0 +1,40 @@ +/** + * Copyright © 2016-2025 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.security.cid; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.test.context.TestPropertySource; +import org.thingsboard.server.dao.service.DaoSqlTest; + + +@TestPropertySource(properties = { + "transport.lwm2m.dtls.connection_id_length=16" +}) + +@DaoSqlTest +@Slf4j +public abstract class AbstractSecurityLwM2MIntegrationDtlsCidLength16Test extends AbstractSecurityLwM2MIntegrationDtlsCidLengthTest { + + private static final Integer serverDtlsCidLength = 16; + + protected void testNoSecDtlsCidLength(Integer clientDtlsCidLength) throws Exception { + testNoSecDtlsCidLength(clientDtlsCidLength, serverDtlsCidLength); + } + + protected void testPskDtlsCidLength(Integer clientDtlsCidLength) throws Exception { + testPskDtlsCidLength(clientDtlsCidLength, serverDtlsCidLength); + } +} diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/cid/AbstractSecurityLwM2MIntegrationDtlsCidLength3Test.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/cid/AbstractSecurityLwM2MIntegrationDtlsCidLength1Test.java similarity index 78% rename from application/src/test/java/org/thingsboard/server/transport/lwm2m/security/cid/AbstractSecurityLwM2MIntegrationDtlsCidLength3Test.java rename to application/src/test/java/org/thingsboard/server/transport/lwm2m/security/cid/AbstractSecurityLwM2MIntegrationDtlsCidLength1Test.java index 8a65e28975..a36a618bcf 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/cid/AbstractSecurityLwM2MIntegrationDtlsCidLength3Test.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/cid/AbstractSecurityLwM2MIntegrationDtlsCidLength1Test.java @@ -21,17 +21,20 @@ import org.thingsboard.server.dao.service.DaoSqlTest; @TestPropertySource(properties = { - "transport.lwm2m.dtls.connection_id_length=3" + "transport.lwm2m.dtls.connection_id_length=1" }) @DaoSqlTest @Slf4j -public abstract class AbstractSecurityLwM2MIntegrationDtlsCidLength3Test extends AbstractSecurityLwM2MIntegrationDtlsCidLengthTest { +public abstract class AbstractSecurityLwM2MIntegrationDtlsCidLength1Test extends AbstractSecurityLwM2MIntegrationDtlsCidLengthTest { + + + private static final Integer serverDtlsCidLength = 1; protected void testNoSecDtlsCidLength(Integer dtlsCidLength) throws Exception { - testNoSecDtlsCidLength(dtlsCidLength, 3); + testNoSecDtlsCidLength(dtlsCidLength, serverDtlsCidLength); } protected void testPskDtlsCidLength(Integer dtlsCidLength) throws Exception { - testPskDtlsCidLength(dtlsCidLength, 3); + testPskDtlsCidLength(dtlsCidLength, serverDtlsCidLength); } } diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/cid/AbstractSecurityLwM2MIntegrationDtlsCidLength2Test.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/cid/AbstractSecurityLwM2MIntegrationDtlsCidLength2Test.java new file mode 100644 index 0000000000..1cb657e4a4 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/cid/AbstractSecurityLwM2MIntegrationDtlsCidLength2Test.java @@ -0,0 +1,39 @@ +/** + * Copyright © 2016-2025 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.security.cid; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.test.context.TestPropertySource; +import org.thingsboard.server.dao.service.DaoSqlTest; + + +@TestPropertySource(properties = { + "transport.lwm2m.dtls.connection_id_length=2" +}) + +@DaoSqlTest +@Slf4j +public abstract class AbstractSecurityLwM2MIntegrationDtlsCidLength2Test extends AbstractSecurityLwM2MIntegrationDtlsCidLengthTest { + + private static final Integer serverDtlsCidLength = 2; + + protected void testNoSecDtlsCidLength(Integer dtlsCidLength) throws Exception { + testNoSecDtlsCidLength(dtlsCidLength, serverDtlsCidLength); + } + protected void testPskDtlsCidLength(Integer dtlsCidLength) throws Exception { + testPskDtlsCidLength(dtlsCidLength, serverDtlsCidLength); + } +} diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/cid/AbstractSecurityLwM2MIntegrationDtlsCidLength4Test.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/cid/AbstractSecurityLwM2MIntegrationDtlsCidLength4Test.java new file mode 100644 index 0000000000..56e544243f --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/cid/AbstractSecurityLwM2MIntegrationDtlsCidLength4Test.java @@ -0,0 +1,39 @@ +/** + * Copyright © 2016-2025 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.security.cid; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.test.context.TestPropertySource; +import org.thingsboard.server.dao.service.DaoSqlTest; + + +@TestPropertySource(properties = { + "transport.lwm2m.dtls.connection_id_length=4" +}) + +@DaoSqlTest +@Slf4j +public abstract class AbstractSecurityLwM2MIntegrationDtlsCidLength4Test extends AbstractSecurityLwM2MIntegrationDtlsCidLengthTest { + + private static final Integer serverDtlsCidLength = 4; + + protected void testNoSecDtlsCidLength(Integer dtlsCidLength) throws Exception { + testNoSecDtlsCidLength(dtlsCidLength, serverDtlsCidLength); + } + protected void testPskDtlsCidLength(Integer dtlsCidLength) throws Exception { + testPskDtlsCidLength(dtlsCidLength, serverDtlsCidLength); + } +} diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/cid/AbstractSecurityLwM2MIntegrationDtlsCidLengthNullTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/cid/AbstractSecurityLwM2MIntegrationDtlsCidLengthNullTest.java index 9d52920072..6dea2f54d0 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/cid/AbstractSecurityLwM2MIntegrationDtlsCidLengthNullTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/cid/AbstractSecurityLwM2MIntegrationDtlsCidLengthNullTest.java @@ -28,11 +28,12 @@ import org.thingsboard.server.dao.service.DaoSqlTest; @Slf4j public abstract class AbstractSecurityLwM2MIntegrationDtlsCidLengthNullTest extends AbstractSecurityLwM2MIntegrationDtlsCidLengthTest { + private static final Integer serverDtlsCidLength = null; protected void testNoSecDtlsCidLength(Integer dtlsCidLength) throws Exception { - testNoSecDtlsCidLength(dtlsCidLength, null); + testNoSecDtlsCidLength(dtlsCidLength, serverDtlsCidLength); } protected void testPskDtlsCidLength(Integer dtlsCidLength) throws Exception { - testPskDtlsCidLength(dtlsCidLength, null); + testPskDtlsCidLength(dtlsCidLength, serverDtlsCidLength); } } diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/cid/AbstractSecurityLwM2MIntegrationDtlsCidLengthTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/cid/AbstractSecurityLwM2MIntegrationDtlsCidLengthTest.java index eb17f2cf7e..04afef0c61 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/cid/AbstractSecurityLwM2MIntegrationDtlsCidLengthTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/cid/AbstractSecurityLwM2MIntegrationDtlsCidLengthTest.java @@ -17,14 +17,23 @@ package org.thingsboard.server.transport.lwm2m.security.cid; import lombok.extern.slf4j.Slf4j; import org.eclipse.californium.elements.config.Configuration; +import org.eclipse.californium.scandium.DTLSConnector; +import org.eclipse.californium.scandium.dtls.Connection; +import org.eclipse.californium.scandium.dtls.ConnectionId; +import org.eclipse.californium.scandium.dtls.InMemoryReadWriteLockConnectionStore; +import org.eclipse.californium.scandium.dtls.ResumptionSupportingConnectionStore; import org.eclipse.leshan.client.californium.endpoint.CaliforniumClientEndpoint; import org.eclipse.leshan.client.californium.endpoint.CaliforniumClientEndpointsProvider; +import org.eclipse.leshan.client.servers.LwM2mServer; +import org.eclipse.leshan.core.peer.IpPeer; import org.junit.Assert; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.transport.lwm2m.security.AbstractSecurityLwM2MIntegrationTest; +import java.lang.reflect.Field; +import java.net.InetSocketAddress; import java.util.concurrent.TimeUnit; import static org.awaitility.Awaitility.await; @@ -39,13 +48,13 @@ public abstract class AbstractSecurityLwM2MIntegrationDtlsCidLengthTest extends protected String awaitAlias; - protected void testNoSecDtlsCidLength(Integer dtlsCidLength, Integer serverDtlsCidLength) throws Exception { + protected void testNoSecDtlsCidLength(Integer clientDtlsCidLength, Integer serverDtlsCidLength) throws Exception { initDeviceCredentialsNoSek(); - basicTestConnectionDtlsCidLength(dtlsCidLength, serverDtlsCidLength); + basicTestConnectionDtlsCidLength(clientDtlsCidLength, serverDtlsCidLength); } - protected void testPskDtlsCidLength(Integer dtlsCidLength, Integer serverDtlsCidLength) throws Exception { + protected void testPskDtlsCidLength(Integer clientDtlsCidLength, Integer serverDtlsCidLength) throws Exception { initDeviceCredentialsPsk(); - basicTestConnectionDtlsCidLength(dtlsCidLength, serverDtlsCidLength); + basicTestConnectionDtlsCidLength(clientDtlsCidLength, serverDtlsCidLength); } protected void basicTestConnectionDtlsCidLength(Integer clientDtlsCidLength, @@ -69,19 +78,55 @@ public abstract class AbstractSecurityLwM2MIntegrationDtlsCidLengthTest extends Assert.assertTrue(lwM2MTestClient.getClientDtlsCid().isEmpty()); } else { Assert.assertEquals(2L, lwM2MTestClient.getClientDtlsCid().size()); - Assert.assertTrue(lwM2MTestClient.getClientDtlsCid().keySet().contains(ON_READ_CONNECTION_ID)); - Assert.assertTrue(lwM2MTestClient.getClientDtlsCid().keySet().contains(ON_WRITE_CONNECTION_ID)); - if (serverDtlsCidLength == null) { + Assert.assertTrue(lwM2MTestClient.getClientDtlsCid().containsKey(ON_READ_CONNECTION_ID)); + Assert.assertTrue(lwM2MTestClient.getClientDtlsCid().containsKey(ON_WRITE_CONNECTION_ID)); + + LwM2mServer lwM2mServer = lwM2MTestClient.getLeshanClient().getRegisteredServers().entrySet().stream().findFirst().get().getValue(); + CaliforniumClientEndpoint lwM2mClientEndpoint = (CaliforniumClientEndpoint) lwM2MTestClient.getLeshanClient().getEndpoint(lwM2mServer); + Connection connection = getConnection(lwM2mClientEndpoint, lwM2mServer); + ConnectionId clientCid = connection.getConnectionId(); + ConnectionId readCid = connection.getEstablishedDtlsContext().getReadConnectionId(); + ConnectionId serverCid = connection.getEstablishedDtlsContext().getWriteConnectionId(); + if (serverDtlsCidLength == null || clientDtlsCidLength == null) { + // cid is not used Assert.assertNull(lwM2MTestClient.getClientDtlsCid().get(ON_WRITE_CONNECTION_ID)); Assert.assertNull(lwM2MTestClient.getClientDtlsCid().get(ON_READ_CONNECTION_ID)); + Assert.assertNull(readCid); + Assert.assertNull(serverCid); } else { + Assert.assertEquals(serverDtlsCidLength, lwM2MTestClient.getClientDtlsCid().get(ON_WRITE_CONNECTION_ID)); Assert.assertEquals(clientDtlsCidLength, lwM2MTestClient.getClientDtlsCid().get(ON_READ_CONNECTION_ID)); - if (clientDtlsCidLength == null) { - Assert.assertNull(lwM2MTestClient.getClientDtlsCid().get(ON_READ_CONNECTION_ID)); + // cid used + Assert.assertNotNull(clientCid); + Assert.assertNotNull(readCid); + if (clientDtlsCidLength > 0) { + Assert.assertEquals(clientCid, readCid); + } + Assert.assertNotNull(serverCid); + int actualServerCidLength = serverCid.getBytes().length; + int expectedServerCidLength = serverDtlsCidLength; + Assert.assertEquals(expectedServerCidLength, actualServerCidLength); + } + + if (clientCid != null) { + int actualClientCidLength = clientCid.getBytes().length; + int expectedClientCidLength; + if (clientDtlsCidLength == null || clientDtlsCidLength == 0) { + expectedClientCidLength = 3; } else { - Assert.assertEquals(Integer.valueOf(serverDtlsCidLength), lwM2MTestClient.getClientDtlsCid().get(ON_WRITE_CONNECTION_ID)); + expectedClientCidLength = clientDtlsCidLength; } + Assert.assertEquals(expectedClientCidLength, actualClientCidLength); } } } + + private static Connection getConnection(CaliforniumClientEndpoint lwM2mClientEndpoint, LwM2mServer lwM2mServer) throws NoSuchFieldException, IllegalAccessException { + DTLSConnector connector = (DTLSConnector) lwM2mClientEndpoint.getCoapEndpoint().getConnector(); + Field field = DTLSConnector.class.getDeclaredField("connectionStore"); + field.setAccessible(true); + ResumptionSupportingConnectionStore connectionStore = (InMemoryReadWriteLockConnectionStore) field.get(connector); + InetSocketAddress serverAddr = ((IpPeer) lwM2mServer.getTransportData()).getSocketAddress(); + return connectionStore.get(serverAddr); + } } diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/cid/serverDtlsCidLength_0/NoSecLwM2MIntegrationDtlsCidLengthTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/cid/serverDtlsCidLength_0/NoSecLwM2MIntegrationDtlsCidLengthTest.java index 6d529b3c08..2b5cccd7be 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/cid/serverDtlsCidLength_0/NoSecLwM2MIntegrationDtlsCidLengthTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/cid/serverDtlsCidLength_0/NoSecLwM2MIntegrationDtlsCidLengthTest.java @@ -27,7 +27,7 @@ public class NoSecLwM2MIntegrationDtlsCidLengthTest extends AbstractSecurityLwM2 @Before public void setUpNoSecDtlsCidLength() { transportConfiguration = getTransportConfiguration(OBSERVE_ATTRIBUTES_WITHOUT_PARAMS, getBootstrapServerCredentialsSecure(NO_SEC, NONE)); - awaitAlias = "await on client state (NoSec_Lwm2m) DtlsCidLength = 0"; + awaitAlias = "await on client state (NoSec_Lwm2m) serverDtlsCidLength = 0"; } @Test @@ -40,8 +40,23 @@ public class NoSecLwM2MIntegrationDtlsCidLengthTest extends AbstractSecurityLwM2 testNoSecDtlsCidLength(0); } + @Test + public void testWithNoSecConnectLwm2mSuccessClientDtlsCidLength_1() throws Exception { + testNoSecDtlsCidLength(1); + } + @Test public void testWithNoSecConnectLwm2mSuccessClientDtlsCidLength_2() throws Exception { - testNoSecDtlsCidLength(2); + testNoSecDtlsCidLength(1); + } + + @Test + public void testWithNoSecConnectLwm2mSuccessClientDtlsCidLength_4() throws Exception { + testNoSecDtlsCidLength(4); + } + + @Test + public void testWithNoSecConnectLwm2mSuccessClientDtlsCidLength_16() throws Exception { + testNoSecDtlsCidLength(16); } } diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/cid/serverDtlsCidLength_0/PskLwm2mIntegrationDtlsCidLengthTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/cid/serverDtlsCidLength_0/PskLwm2mIntegrationDtlsCidLengthTest.java index f478a18777..c08eeeaa79 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/cid/serverDtlsCidLength_0/PskLwm2mIntegrationDtlsCidLengthTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/cid/serverDtlsCidLength_0/PskLwm2mIntegrationDtlsCidLengthTest.java @@ -27,7 +27,7 @@ public class PskLwm2mIntegrationDtlsCidLengthTest extends AbstractSecurityLwM2MI @Before public void createProfileRpc() { transportConfiguration = getTransportConfiguration(OBSERVE_ATTRIBUTES_WITHOUT_PARAMS, getBootstrapServerCredentialsSecure(PSK, NONE)); - awaitAlias = "await on client state (Psk_Lwm2m) DtlsCidLength = 0"; + awaitAlias = "await on client state (Psk_Lwm2m) serverDtlsCidLength = 0"; } @Test @@ -39,10 +39,24 @@ public class PskLwm2mIntegrationDtlsCidLengthTest extends AbstractSecurityLwM2MI public void testWithPskConnectLwm2mSuccessClientDtlsCidLength_0() throws Exception { testPskDtlsCidLength(0); } + @Test + public void testWithPskConnectLwm2mSuccessClientDtlsCidLength_1() throws Exception { + testPskDtlsCidLength(1); + } @Test public void testWithPskConnectLwm2mSuccessClientDtlsCidLength_2() throws Exception { testPskDtlsCidLength(2); } + + @Test + public void testWithPskConnectLwm2mSuccessClientDtlsCidLength_4() throws Exception { + testPskDtlsCidLength(4); + } + + @Test + public void testWithPskConnectLwm2mSuccessClientDtlsCidLength_16() throws Exception { + testPskDtlsCidLength(16); + } } diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/cid/serverDtlsCidLength_1/PskLwm2mIntegrationDtlsCidLengthTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/cid/serverDtlsCidLength_1/PskLwm2mIntegrationDtlsCidLengthTest.java new file mode 100644 index 0000000000..b2c06495fc --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/cid/serverDtlsCidLength_1/PskLwm2mIntegrationDtlsCidLengthTest.java @@ -0,0 +1,64 @@ +/** + * Copyright © 2016-2025 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.security.cid.serverDtlsCidLength_1; + +import org.junit.Before; +import org.junit.Test; +import org.thingsboard.server.transport.lwm2m.security.cid.AbstractSecurityLwM2MIntegrationDtlsCidLength0Test; +import org.thingsboard.server.transport.lwm2m.security.cid.AbstractSecurityLwM2MIntegrationDtlsCidLength1Test; + +import static org.thingsboard.server.common.data.device.credentials.lwm2m.LwM2MSecurityMode.PSK; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MProfileBootstrapConfigType.NONE; + +public class PskLwm2mIntegrationDtlsCidLengthTest extends AbstractSecurityLwM2MIntegrationDtlsCidLength1Test { + + @Before + public void createProfileRpc() { + transportConfiguration = getTransportConfiguration(OBSERVE_ATTRIBUTES_WITHOUT_PARAMS, getBootstrapServerCredentialsSecure(PSK, NONE)); + awaitAlias = "await on client state (Psk_Lwm2m) serverDtlsCidLength = 1"; + } + + @Test + public void testWithPskConnectLwm2mSuccessClientDtlsCidLength_Null() throws Exception { + testPskDtlsCidLength(null); + } + + @Test + public void testWithPskConnectLwm2mSuccessClientDtlsCidLength_0() throws Exception { + testPskDtlsCidLength(0); + } + + @Test + public void testWithPskConnectLwm2mSuccessClientDtlsCidLength_1() throws Exception { + testPskDtlsCidLength(1); + } + + @Test + public void testWithPskConnectLwm2mSuccessClientDtlsCidLength_2() throws Exception { + testPskDtlsCidLength(2); + } + + @Test + public void testWithPskConnectLwm2mSuccessClientDtlsCidLength_4() throws Exception { + testPskDtlsCidLength(4); + } + + @Test + public void testWithPskConnectLwm2mSuccessClientDtlsCidLength_16() throws Exception { + testPskDtlsCidLength(16); + } +} + diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/cid/serverDtlsCidLength_3/NoSecLwM2MIntegrationDtlsCidLengthTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/cid/serverDtlsCidLength_16/NoSecLwM2MIntegrationDtlsCidLengthTest.java similarity index 69% rename from application/src/test/java/org/thingsboard/server/transport/lwm2m/security/cid/serverDtlsCidLength_3/NoSecLwM2MIntegrationDtlsCidLengthTest.java rename to application/src/test/java/org/thingsboard/server/transport/lwm2m/security/cid/serverDtlsCidLength_16/NoSecLwM2MIntegrationDtlsCidLengthTest.java index a395f2e7e3..872608145c 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/cid/serverDtlsCidLength_3/NoSecLwM2MIntegrationDtlsCidLengthTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/cid/serverDtlsCidLength_16/NoSecLwM2MIntegrationDtlsCidLengthTest.java @@ -13,21 +13,22 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.transport.lwm2m.security.cid.serverDtlsCidLength_3; +package org.thingsboard.server.transport.lwm2m.security.cid.serverDtlsCidLength_16; import org.junit.Before; import org.junit.Test; -import org.thingsboard.server.transport.lwm2m.security.cid.AbstractSecurityLwM2MIntegrationDtlsCidLength3Test; +import org.thingsboard.server.transport.lwm2m.security.cid.AbstractSecurityLwM2MIntegrationDtlsCidLength16Test; +import org.thingsboard.server.transport.lwm2m.security.cid.AbstractSecurityLwM2MIntegrationDtlsCidLength4Test; import static org.thingsboard.server.common.data.device.credentials.lwm2m.LwM2MSecurityMode.NO_SEC; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MProfileBootstrapConfigType.NONE; -public class NoSecLwM2MIntegrationDtlsCidLengthTest extends AbstractSecurityLwM2MIntegrationDtlsCidLength3Test { +public class NoSecLwM2MIntegrationDtlsCidLengthTest extends AbstractSecurityLwM2MIntegrationDtlsCidLength16Test { @Before public void setUpNoSecDtlsCidLength() { transportConfiguration = getTransportConfiguration(OBSERVE_ATTRIBUTES_WITHOUT_PARAMS, getBootstrapServerCredentialsSecure(NO_SEC, NONE)); - awaitAlias = "await on client state (NoSec_Lwm2m) DtlsCidLength = 3"; + awaitAlias = "await on client state (NoSec_Lwm2m) serverDtlsCidLength = 16"; } @Test @@ -40,8 +41,23 @@ public class NoSecLwM2MIntegrationDtlsCidLengthTest extends AbstractSecurityLwM2 testNoSecDtlsCidLength(0); } + @Test + public void testWithNoSecConnectLwm2mSuccessClientDtlsCidLength_1() throws Exception { + testNoSecDtlsCidLength(1); + } + @Test public void testWithNoSecConnectLwm2mSuccessClientDtlsCidLength_2() throws Exception { testNoSecDtlsCidLength(2); } + + @Test + public void testWithNoSecConnectLwm2mSuccessClientDtlsCidLength_4() throws Exception { + testNoSecDtlsCidLength(4); + } + + @Test + public void testWithNoSecConnectLwm2mSuccessClientDtlsCidLength_16() throws Exception { + testNoSecDtlsCidLength(16); + } } diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/cid/serverDtlsCidLength_16/PskLwm2mIntegrationDtlsCidLengthTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/cid/serverDtlsCidLength_16/PskLwm2mIntegrationDtlsCidLengthTest.java new file mode 100644 index 0000000000..579614d98e --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/cid/serverDtlsCidLength_16/PskLwm2mIntegrationDtlsCidLengthTest.java @@ -0,0 +1,64 @@ +/** + * Copyright © 2016-2025 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.security.cid.serverDtlsCidLength_16; + +import org.junit.Before; +import org.junit.Test; +import org.thingsboard.server.transport.lwm2m.security.cid.AbstractSecurityLwM2MIntegrationDtlsCidLength16Test; +import org.thingsboard.server.transport.lwm2m.security.cid.AbstractSecurityLwM2MIntegrationDtlsCidLength4Test; + +import static org.thingsboard.server.common.data.device.credentials.lwm2m.LwM2MSecurityMode.PSK; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MProfileBootstrapConfigType.NONE; + +public class PskLwm2mIntegrationDtlsCidLengthTest extends AbstractSecurityLwM2MIntegrationDtlsCidLength16Test { + + @Before + public void createProfileRpc() { + transportConfiguration = getTransportConfiguration(OBSERVE_ATTRIBUTES_WITHOUT_PARAMS, getBootstrapServerCredentialsSecure(PSK, NONE)); + awaitAlias = "await on client state (Psk_Lwm2m) serverDtlsCidLength = 16"; + } + + @Test + public void testWithPskConnectLwm2mSuccessClientDtlsCidLength_Null() throws Exception { + testPskDtlsCidLength(null); + } + + @Test + public void testWithPskConnectLwm2mSuccessClientDtlsCidLength_0() throws Exception { + testPskDtlsCidLength(0); + } + + @Test + public void testWithPskConnectLwm2mSuccessClientDtlsCidLength_1() throws Exception { + testPskDtlsCidLength(1); + } + + @Test + public void testWithPskConnectLwm2mSuccessClientDtlsCidLength_2() throws Exception { + testPskDtlsCidLength(2); + } + + @Test + public void testWithPskConnectLwm2mSuccessClientDtlsCidLength_4() throws Exception { + testPskDtlsCidLength(4); + } + + @Test + public void testWithPskConnectLwm2mSuccessClientDtlsCidLength_16() throws Exception { + testPskDtlsCidLength(16); + } +} + diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/cid/serverDtlsCidLength_3/PskLwm2mIntegrationDtlsCidLengthTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/cid/serverDtlsCidLength_2/PskLwm2mIntegrationDtlsCidLengthTest.java similarity index 73% rename from application/src/test/java/org/thingsboard/server/transport/lwm2m/security/cid/serverDtlsCidLength_3/PskLwm2mIntegrationDtlsCidLengthTest.java rename to application/src/test/java/org/thingsboard/server/transport/lwm2m/security/cid/serverDtlsCidLength_2/PskLwm2mIntegrationDtlsCidLengthTest.java index 868a146ed7..2d68d057d8 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/cid/serverDtlsCidLength_3/PskLwm2mIntegrationDtlsCidLengthTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/cid/serverDtlsCidLength_2/PskLwm2mIntegrationDtlsCidLengthTest.java @@ -13,21 +13,21 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.transport.lwm2m.security.cid.serverDtlsCidLength_3; +package org.thingsboard.server.transport.lwm2m.security.cid.serverDtlsCidLength_2; import org.junit.Before; import org.junit.Test; -import org.thingsboard.server.transport.lwm2m.security.cid.AbstractSecurityLwM2MIntegrationDtlsCidLength3Test; +import org.thingsboard.server.transport.lwm2m.security.cid.AbstractSecurityLwM2MIntegrationDtlsCidLength2Test; import static org.thingsboard.server.common.data.device.credentials.lwm2m.LwM2MSecurityMode.PSK; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MProfileBootstrapConfigType.NONE; -public class PskLwm2mIntegrationDtlsCidLengthTest extends AbstractSecurityLwM2MIntegrationDtlsCidLength3Test { +public class PskLwm2mIntegrationDtlsCidLengthTest extends AbstractSecurityLwM2MIntegrationDtlsCidLength2Test { @Before public void createProfileRpc() { transportConfiguration = getTransportConfiguration(OBSERVE_ATTRIBUTES_WITHOUT_PARAMS, getBootstrapServerCredentialsSecure(PSK, NONE)); - awaitAlias = "await on client state (Psk_Lwm2m) DtlsCidLength = 3"; + awaitAlias = "await on client state (Psk_Lwm2m) serverDtlsCidLength = 2"; } @Test @@ -40,9 +40,24 @@ public class PskLwm2mIntegrationDtlsCidLengthTest extends AbstractSecurityLwM2MI testPskDtlsCidLength(0); } + @Test + public void testWithPskConnectLwm2mSuccessClientDtlsCidLength_1() throws Exception { + testPskDtlsCidLength(1); + } + @Test public void testWithPskConnectLwm2mSuccessClientDtlsCidLength_2() throws Exception { testPskDtlsCidLength(2); } + + @Test + public void testWithPskConnectLwm2mSuccessClientDtlsCidLength_4() throws Exception { + testPskDtlsCidLength(4); + } + + @Test + public void testWithPskConnectLwm2mSuccessClientDtlsCidLength_16() throws Exception { + testPskDtlsCidLength(16); + } } diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/cid/serverDtlsCidLength_4/PskLwm2mIntegrationDtlsCidLengthTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/cid/serverDtlsCidLength_4/PskLwm2mIntegrationDtlsCidLengthTest.java new file mode 100644 index 0000000000..6994e19fbf --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/cid/serverDtlsCidLength_4/PskLwm2mIntegrationDtlsCidLengthTest.java @@ -0,0 +1,63 @@ +/** + * Copyright © 2016-2025 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.security.cid.serverDtlsCidLength_4; + +import org.junit.Before; +import org.junit.Test; +import org.thingsboard.server.transport.lwm2m.security.cid.AbstractSecurityLwM2MIntegrationDtlsCidLength4Test; + +import static org.thingsboard.server.common.data.device.credentials.lwm2m.LwM2MSecurityMode.PSK; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MProfileBootstrapConfigType.NONE; + +public class PskLwm2mIntegrationDtlsCidLengthTest extends AbstractSecurityLwM2MIntegrationDtlsCidLength4Test { + + @Before + public void createProfileRpc() { + transportConfiguration = getTransportConfiguration(OBSERVE_ATTRIBUTES_WITHOUT_PARAMS, getBootstrapServerCredentialsSecure(PSK, NONE)); + awaitAlias = "await on client state (Psk_Lwm2m) serverDtlsCidLength = 4"; + } + + @Test + public void testWithPskConnectLwm2mSuccessClientDtlsCidLength_Null() throws Exception { + testPskDtlsCidLength(null); + } + + @Test + public void testWithPskConnectLwm2mSuccessClientDtlsCidLength_0() throws Exception { + testPskDtlsCidLength(0); + } + + @Test + public void testWithPskConnectLwm2mSuccessClientDtlsCidLength_1() throws Exception { + testPskDtlsCidLength(1); + } + + @Test + public void testWithPskConnectLwm2mSuccessClientDtlsCidLength_2() throws Exception { + testPskDtlsCidLength(2); + } + + @Test + public void testWithPskConnectLwm2mSuccessClientDtlsCidLength_4() throws Exception { + testPskDtlsCidLength(4); + } + + @Test + public void testWithPskConnectLwm2mSuccessClientDtlsCidLength_16() throws Exception { + testPskDtlsCidLength(16); + } +} + diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/cid/serverDtlsCidLength_null/NoSecLwM2MIntegrationDtlsCidLengthTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/cid/serverDtlsCidLength_null/NoSecLwM2MIntegrationDtlsCidLengthTest.java index 9e7424743a..e85e03dfed 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/cid/serverDtlsCidLength_null/NoSecLwM2MIntegrationDtlsCidLengthTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/cid/serverDtlsCidLength_null/NoSecLwM2MIntegrationDtlsCidLengthTest.java @@ -27,7 +27,7 @@ public class NoSecLwM2MIntegrationDtlsCidLengthTest extends AbstractSecurityLwM2 @Before public void setUpNoSecDtlsCidLength() { transportConfiguration = getTransportConfiguration(OBSERVE_ATTRIBUTES_WITHOUT_PARAMS, getBootstrapServerCredentialsSecure(NO_SEC, NONE)); - awaitAlias = "await on client state (NoSec_Lwm2m) DtlsCidLength = Null"; + awaitAlias = "await on client state (NoSec_Lwm2m) serverDtlsCidLength = Null"; } @Test @@ -41,7 +41,22 @@ public class NoSecLwM2MIntegrationDtlsCidLengthTest extends AbstractSecurityLwM2 } @Test - public void testWithNoSecConnectLwm2mSuccessClientDtlsCidLength_2() throws Exception { - testNoSecDtlsCidLength(2); + public void testWithNoSecConnectLwm2mSuccessClientDtlsCidLength_1() throws Exception { + testNoSecDtlsCidLength(1); + } + + @Test + public void testWithNoSecConnectLwm2mSuccessClientDtlsCidLength_4() throws Exception { + testNoSecDtlsCidLength(4); + } + + @Test + public void testWithNoSecConnectLwm2mSuccessClientDtlsCidLength_8() throws Exception { + testNoSecDtlsCidLength(8); + } + + @Test + public void testWithNoSecConnectLwm2mSuccessClientDtlsCidLength_16() throws Exception { + testNoSecDtlsCidLength(16); } } diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/cid/serverDtlsCidLength_null/PskLwm2mIntegrationDtlsCidLengthTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/cid/serverDtlsCidLength_null/PskLwm2mIntegrationDtlsCidLengthTest.java index 8a8f01b3ab..e482e1b106 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/cid/serverDtlsCidLength_null/PskLwm2mIntegrationDtlsCidLengthTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/cid/serverDtlsCidLength_null/PskLwm2mIntegrationDtlsCidLengthTest.java @@ -27,7 +27,7 @@ public class PskLwm2mIntegrationDtlsCidLengthTest extends AbstractSecurityLwM2MI @Before public void createProfileRpc() { transportConfiguration = getTransportConfiguration(OBSERVE_ATTRIBUTES_WITHOUT_PARAMS, getBootstrapServerCredentialsSecure(PSK, NONE)); - awaitAlias = "await on client state (Psk_Lwm2m) DtlsCidLength = Null"; + awaitAlias = "await on client state (Psk_Lwm2m) serverDtlsCidLength = Null"; } @Test @@ -40,9 +40,24 @@ public class PskLwm2mIntegrationDtlsCidLengthTest extends AbstractSecurityLwM2MI testPskDtlsCidLength(0); } + @Test + public void testWithPskConnectLwm2mSuccessClientDtlsCidLength_1() throws Exception { + testPskDtlsCidLength(1); + } + @Test public void testWithPskConnectLwm2mSuccessClientDtlsCidLength_2() throws Exception { testPskDtlsCidLength(2); } + + @Test + public void testWithPskConnectLwm2mSuccessClientDtlsCidLength_4() throws Exception { + testPskDtlsCidLength(4); + } + + @Test + public void testWithPskConnectLwm2mSuccessClientDtlsCidLength_16() throws Exception { + testPskDtlsCidLength(16); + } } diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/NoSecLwM2MIntegrationBS3SectionTriggerTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/NoSecLwM2MIntegrationBS3SectionTriggerTest.java new file mode 100644 index 0000000000..af8284484d --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/NoSecLwM2MIntegrationBS3SectionTriggerTest.java @@ -0,0 +1,29 @@ +/** + * Copyright © 2016-2025 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.security.sql; + +import org.junit.Test; +import org.thingsboard.server.transport.lwm2m.security.AbstractSecurityLwM2MIntegrationTest; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MProfileBootstrapConfigType.LWM2M_ONLY; +public class NoSecLwM2MIntegrationBS3SectionTriggerTest extends AbstractSecurityLwM2MIntegrationTest { + + @Test + public void testWithNoSecConnectLwm2mSuccessBootstrapRequestTrigger_3_ConnectBsSuccess_UpdateLwm2mSection_3_AndLm2m_1_ConnectLwm2mSuccess() throws Exception { + String clientEndpoint = CLIENT_ENDPOINT_NO_SEC_BS + "Trigger_3" + LWM2M_ONLY.name(); + String awaitAlias = "await on client state (NoSecBS Trigger Lwm2m section)"; + basicTestConnectionBootstrapRequestTriggerBefore(clientEndpoint, awaitAlias, LWM2M_ONLY, 3); + } +} diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/NoSecLwM2MIntegrationBSLwm2mOnlyNoneTriggerOneSectionTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/NoSecLwM2MIntegrationBSLwm2mOnlyNoneTriggerOneSectionTest.java new file mode 100644 index 0000000000..4fceba60f3 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/NoSecLwM2MIntegrationBSLwm2mOnlyNoneTriggerOneSectionTest.java @@ -0,0 +1,37 @@ +/** + * Copyright © 2016-2025 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.security.sql; + +import org.junit.Test; +import org.thingsboard.server.transport.lwm2m.security.AbstractSecurityLwM2MIntegrationTest; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MProfileBootstrapConfigType.LWM2M_ONLY; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MProfileBootstrapConfigType.NONE; +public class NoSecLwM2MIntegrationBSLwm2mOnlyNoneTriggerOneSectionTest extends AbstractSecurityLwM2MIntegrationTest { + + @Test + public void testWithNoSecConnectLwm2mSuccessBootstrapRequestTriggerConnectBsSuccess_UpdateLwm2mSectionAndLm2m_ConnectLwm2mSuccess() throws Exception { + String clientEndpoint = CLIENT_ENDPOINT_NO_SEC_BS + "Trigger" + LWM2M_ONLY.name(); + String awaitAlias = "await on client state (NoSecBS Trigger Lwm2m section)"; + basicTestConnectionBootstrapRequestTriggerBefore(clientEndpoint, awaitAlias, LWM2M_ONLY, 1); + } + + @Test + public void testWithNoSecConnectLwm2mSuccessBootstrapRequestTriggerConnectBsSuccess_UpdateNoneSectionAndLm2m_ConnectLwm2mSuccess() throws Exception { + String clientEndpoint = CLIENT_ENDPOINT_NO_SEC_BS + "Trigger" + NONE.name(); + String awaitAlias = "await on client state (NoSecBS Trigger None section)"; + basicTestConnectionBootstrapRequestTriggerBefore(clientEndpoint, awaitAlias, NONE, 1); + } +} diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/NoSecLwM2MIntegrationBSNoTriggerTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/NoSecLwM2MIntegrationBSNoTriggerTest.java new file mode 100644 index 0000000000..b218c39aec --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/NoSecLwM2MIntegrationBSNoTriggerTest.java @@ -0,0 +1,39 @@ +/** + * Copyright © 2016-2025 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.security.sql; + +import org.junit.Test; +import org.thingsboard.server.transport.lwm2m.security.AbstractSecurityLwM2MIntegrationTest; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MClientState.ON_REGISTRATION_SUCCESS; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MProfileBootstrapConfigType.BOTH; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MProfileBootstrapConfigType.LWM2M_ONLY; + +public class NoSecLwM2MIntegrationBSNoTriggerTest extends AbstractSecurityLwM2MIntegrationTest { + + @Test + public void testWithNoSecConnectBsSuccess_UpdateTwoSectionsBootstrapAndLm2m_ConnectLwm2mSuccess() throws Exception { + String clientEndpoint = CLIENT_ENDPOINT_NO_SEC_BS + "NoTrigger" + BOTH.name(); + String awaitAlias = "await on client state (NoSecBS two section)"; + basicTestConnectionStartBS(clientEndpoint, awaitAlias, BOTH, expectedStatusesRegistrationBsSuccess, ON_REGISTRATION_SUCCESS); + } + + @Test + public void testWithNoSecConnectBsSuccess_UpdateLwm2mSectionAndLm2m_ConnectLwm2mSuccess() throws Exception { + String clientEndpoint = CLIENT_ENDPOINT_NO_SEC_BS + "NoTrigger" + LWM2M_ONLY.name(); + String awaitAlias = "await on client state (NoSecBS Lwm2m section)"; + basicTestConnectionStartBS(clientEndpoint, awaitAlias, LWM2M_ONLY, expectedStatusesRegistrationBsSuccess, ON_REGISTRATION_SUCCESS); + } +} diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/NoSecLwM2MIntegrationBSOnlyTriggerOneSectionTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/NoSecLwM2MIntegrationBSOnlyTriggerOneSectionTest.java new file mode 100644 index 0000000000..5510cdf614 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/NoSecLwM2MIntegrationBSOnlyTriggerOneSectionTest.java @@ -0,0 +1,29 @@ +/** + * Copyright © 2016-2025 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.security.sql; + +import org.junit.Test; +import org.thingsboard.server.transport.lwm2m.security.AbstractSecurityLwM2MIntegrationTest; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MProfileBootstrapConfigType.BOOTSTRAP_ONLY; +public class NoSecLwM2MIntegrationBSOnlyTriggerOneSectionTest extends AbstractSecurityLwM2MIntegrationTest { + + @Test + public void testWithNoSecConnectLwm2mSuccessBootstrapRequestTriggerConnectBsSuccess_UpdateBootstrapSectionAndLm2m_ConnectLwm2mSuccess() throws Exception { + String clientEndpoint = CLIENT_ENDPOINT_NO_SEC_BS + "Trigger" + BOOTSTRAP_ONLY.name(); + String awaitAlias = "await on client state (NoSecBS Trigger Bootstrap section)"; + basicTestConnectionBootstrapRequestTriggerBefore(clientEndpoint, awaitAlias, BOOTSTRAP_ONLY, 1); + } +} diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/NoSecLwM2MIntegrationBSTriggerTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/NoSecLwM2MIntegrationBSTriggerTest.java new file mode 100644 index 0000000000..b007d16bde --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/NoSecLwM2MIntegrationBSTriggerTest.java @@ -0,0 +1,31 @@ +/** + * Copyright © 2016-2025 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.security.sql; + +import org.junit.Test; +import org.thingsboard.server.transport.lwm2m.security.AbstractSecurityLwM2MIntegrationTest; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MProfileBootstrapConfigType.BOTH; + +public class NoSecLwM2MIntegrationBSTriggerTest extends AbstractSecurityLwM2MIntegrationTest { + + @Test + public void testWithNoSecConnectLwm2mSuccessBootstrapRequestTriggerConnectBsSuccess_UpdateTwoSectionAndLm2m_ConnectLwm2mSuccess() throws Exception { + String clientEndpoint = CLIENT_ENDPOINT_NO_SEC_BS + "Trigger" + BOTH.name(); + String awaitAlias = "await on client state (NoSecBS Trigger Two section)"; + basicTestConnectionBootstrapRequestTriggerBefore(clientEndpoint, awaitAlias, BOTH, 1); + } +} + diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/NoSecLwM2MIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/NoSecLwM2MIntegrationTest.java index d4a464b9c6..50bb87467f 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/NoSecLwM2MIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/NoSecLwM2MIntegrationTest.java @@ -19,12 +19,6 @@ import org.junit.Test; import org.thingsboard.server.common.data.device.credentials.lwm2m.LwM2MDeviceCredentials; import org.thingsboard.server.transport.lwm2m.security.AbstractSecurityLwM2MIntegrationTest; -import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MClientState.ON_REGISTRATION_SUCCESS; -import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MProfileBootstrapConfigType.BOOTSTRAP_ONLY; -import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MProfileBootstrapConfigType.BOTH; -import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MProfileBootstrapConfigType.LWM2M_ONLY; -import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MProfileBootstrapConfigType.NONE; - public class NoSecLwM2MIntegrationTest extends AbstractSecurityLwM2MIntegrationTest { //Lwm2m only @@ -34,48 +28,4 @@ public class NoSecLwM2MIntegrationTest extends AbstractSecurityLwM2MIntegrationT LwM2MDeviceCredentials clientCredentials = getDeviceCredentialsNoSec(createNoSecClientCredentials(clientEndpoint)); super.basicTestConnectionObserveSingleTelemetry(SECURITY_NO_SEC, clientCredentials, clientEndpoint, false, false); } - - // Bootstrap + Lwm2m - @Test - public void testWithNoSecConnectBsSuccess_UpdateTwoSectionsBootstrapAndLm2m_ConnectLwm2mSuccess() throws Exception { - String clientEndpoint = CLIENT_ENDPOINT_NO_SEC_BS + BOTH.name(); - String awaitAlias = "await on client state (NoSecBS two section)"; - basicTestConnectionBefore(clientEndpoint, awaitAlias, BOTH, expectedStatusesRegistrationBsSuccess, ON_REGISTRATION_SUCCESS); - } - - @Test - public void testWithNoSecConnectBsSuccess_UpdateLwm2mSectionAndLm2m_ConnectLwm2mSuccess() throws Exception { - String clientEndpoint = CLIENT_ENDPOINT_NO_SEC_BS + LWM2M_ONLY.name(); - String awaitAlias = "await on client state (NoSecBS Lwm2m section)"; - basicTestConnectionBefore(clientEndpoint, awaitAlias, LWM2M_ONLY, expectedStatusesRegistrationBsSuccess, ON_REGISTRATION_SUCCESS); - } - - // Bs trigger - @Test - public void testWithNoSecConnectLwm2mSuccessBootstrapRequestTriggerConnectBsSuccess_UpdateTwoSectionAndLm2m_ConnectLwm2mSuccess() throws Exception { - String clientEndpoint = CLIENT_ENDPOINT_NO_SEC_BS + "Trigger" + BOTH.name(); - String awaitAlias = "await on client state (NoSecBS Trigger Two section)"; - basicTestConnectionBootstrapRequestTriggerBefore(clientEndpoint, awaitAlias, BOTH); - } - - @Test - public void testWithNoSecConnectLwm2mSuccessBootstrapRequestTriggerConnectBsSuccess_UpdateBootstrapSectionAndLm2m_ConnectLwm2mSuccess() throws Exception { - String clientEndpoint = CLIENT_ENDPOINT_NO_SEC_BS + "Trigger" + BOOTSTRAP_ONLY.name(); - String awaitAlias = "await on client state (NoSecBS Trigger Bootstrap section)"; - basicTestConnectionBootstrapRequestTriggerBefore(clientEndpoint, awaitAlias, BOOTSTRAP_ONLY); - } - - @Test - public void testWithNoSecConnectLwm2mSuccessBootstrapRequestTriggerConnectBsSuccess_UpdateLwm2mSectionAndLm2m_ConnectLwm2mSuccess() throws Exception { - String clientEndpoint = CLIENT_ENDPOINT_NO_SEC_BS + "Trigger" + LWM2M_ONLY.name(); - String awaitAlias = "await on client state (NoSecBS Trigger Lwm2m section)"; - basicTestConnectionBootstrapRequestTriggerBefore(clientEndpoint, awaitAlias, LWM2M_ONLY); - } - - @Test - public void testWithNoSecConnectLwm2mSuccessBootstrapRequestTriggerConnectBsSuccess_UpdateNoneSectionAndLm2m_ConnectLwm2mSuccess() throws Exception { - String clientEndpoint = CLIENT_ENDPOINT_NO_SEC_BS + "Trigger" + NONE.name(); - String awaitAlias = "await on client state (NoSecBS Trigger None section)"; - basicTestConnectionBootstrapRequestTriggerBefore(clientEndpoint, awaitAlias, NONE); - } } diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/PskLwm2mIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/PskLwm2mIntegrationTest.java index 3b61dfe49f..a35b663052 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/PskLwm2mIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/PskLwm2mIntegrationTest.java @@ -58,8 +58,7 @@ public class PskLwm2mIntegrationTest extends AbstractSecurityLwM2MIntegrationTes Hex.decodeHex(keyPsk.toCharArray())); Lwm2mDeviceProfileTransportConfiguration transportConfiguration = getTransportConfiguration(OBSERVE_ATTRIBUTES_WITHOUT_PARAMS, getBootstrapServerCredentialsSecure(PSK, NONE)); LwM2MDeviceCredentials deviceCredentials = getDeviceCredentialsSecure(clientCredentials, null, null, PSK, false); - this.basicTestConnection(security, - null, + this.basicTestConnection(security, null, deviceCredentials, clientEndpoint, transportConfiguration, @@ -69,10 +68,12 @@ public class PskLwm2mIntegrationTest extends AbstractSecurityLwM2MIntegrationTes ON_REGISTRATION_SUCCESS, true); } + @Test public void testWithPskConnectLwm2mOneObserveSuccessUpdateProfileManyObserveUpdateRegistrationSuccess() throws Exception { - String clientEndpoint = CLIENT_ENDPOINT_PSK; - String identity = CLIENT_PSK_IDENTITY; + String suf = "UpdateReg"; + String clientEndpoint = CLIENT_ENDPOINT_PSK + "_" + suf; + String identity = CLIENT_PSK_IDENTITY + "_" + suf; String keyPsk = CLIENT_PSK_KEY; PSKClientCredential clientCredentials = new PSKClientCredential(); clientCredentials.setEndpoint(clientEndpoint); @@ -85,8 +86,7 @@ public class PskLwm2mIntegrationTest extends AbstractSecurityLwM2MIntegrationTes Lwm2mDeviceProfileTransportConfiguration transportConfiguration = getTransportConfiguration(TELEMETRY_WITH_ONE_OBSERVE, getBootstrapServerCredentialsSecure(PSK, NONE)); LwM2MDeviceCredentials deviceCredentials = getDeviceCredentialsSecure(clientCredentials, null, null, PSK, false); String awaitAlias = "await on client state (Psk_Lwm2m)"; - Device lwm2mDevice = this.basicTestConnection(security, - null, + Device lwm2mDevice = this.basicTestConnection(security, null, deviceCredentials, clientEndpoint, transportConfiguration, @@ -105,10 +105,12 @@ public class PskLwm2mIntegrationTest extends AbstractSecurityLwM2MIntegrationTes awaitObserveReadAll(2, lwm2mDevice.getId().getId().toString()); awaitUpdateReg(3); } + @Test public void testWithPskConnectLwm2mSuccessObserveSuccessUnRegClientUpdateProfileObserveConnectLwm2mSuccessOWithNewObserve() throws Exception { - String clientEndpoint = CLIENT_ENDPOINT_PSK; - String identity = CLIENT_PSK_IDENTITY; + String suf = "UnReg"; + String clientEndpoint = CLIENT_ENDPOINT_PSK + "_" + suf; + String identity = CLIENT_PSK_IDENTITY + "_" + suf; String keyPsk = CLIENT_PSK_KEY; PSKClientCredential clientCredentials = new PSKClientCredential(); clientCredentials.setEndpoint(clientEndpoint); @@ -121,8 +123,7 @@ public class PskLwm2mIntegrationTest extends AbstractSecurityLwM2MIntegrationTes Lwm2mDeviceProfileTransportConfiguration transportConfiguration = getTransportConfiguration(TELEMETRY_WITH_ONE_OBSERVE, getBootstrapServerCredentialsSecure(PSK, NONE)); LwM2MDeviceCredentials deviceCredentials = getDeviceCredentialsSecure(clientCredentials, null, null, PSK, false); String awaitAlias = "await on client state (Psk_Lwm2m)"; - Device lwm2mDevice = this.basicTestConnection(security, - null, + Device lwm2mDevice = this.basicTestConnection(security, null, deviceCredentials, clientEndpoint, transportConfiguration, @@ -142,7 +143,7 @@ public class PskLwm2mIntegrationTest extends AbstractSecurityLwM2MIntegrationTes Assert.assertNotNull(lwm2mDeviceProfileManyParams); lwM2MTestClient.start(true); - awaitObserveReadAll(2, lwm2mDevice.getId().getId().toString()); + awaitObserveReadAll(1, lwm2mDevice.getId().getId().toString()); awaitUpdateReg(3); } @@ -175,12 +176,12 @@ public class PskLwm2mIntegrationTest extends AbstractSecurityLwM2MIntegrationTes clientCredentials.setEndpoint(clientEndpoint); clientCredentials.setIdentity(identity); clientCredentials.setKey(keyPsk); - Security securityBs = pskBootstrap(SECURE_URI_BS, + Security securityPskBs = pskBootstrap(SECURE_URI_BS, identity.getBytes(StandardCharsets.UTF_8), Hex.decodeHex(keyPsk.toCharArray())); Lwm2mDeviceProfileTransportConfiguration transportConfiguration = getTransportConfiguration(OBSERVE_ATTRIBUTES_WITHOUT_PARAMS, getBootstrapServerCredentialsSecure(PSK, BOTH)); LwM2MDeviceCredentials deviceCredentials = getDeviceCredentialsSecure(clientCredentials, null, null, PSK, false); - this.basicTestConnection(null, securityBs, + this.basicTestConnection(null, securityPskBs, deviceCredentials, clientEndpoint, transportConfiguration, diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/RpkLwM2MIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/RpkLwM2MIntegrationTest.java index e94b8a6319..fbe84a28b2 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/RpkLwM2MIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/RpkLwM2MIntegrationTest.java @@ -113,13 +113,13 @@ public class RpkLwM2MIntegrationTest extends AbstractSecurityLwM2MIntegrationTes RPKClientCredential clientCredentials = new RPKClientCredential(); clientCredentials.setEndpoint(clientEndpoint); clientCredentials.setKey(Base64.encodeBase64String(certificate.getPublicKey().getEncoded())); - Security securityBs = rpkBootstrap(SECURE_URI_BS, + Security securityRpkBs = rpkBootstrap(SECURE_URI_BS, certificate.getPublicKey().getEncoded(), privateKey.getEncoded(), serverX509CertBs.getPublicKey().getEncoded()); Lwm2mDeviceProfileTransportConfiguration transportConfiguration = getTransportConfiguration(OBSERVE_ATTRIBUTES_WITHOUT_PARAMS, getBootstrapServerCredentialsSecure(RPK, BOTH)); LwM2MDeviceCredentials deviceCredentials = getDeviceCredentialsSecure(clientCredentials, clientPrivateKeyFromCertTrust, certificate, RPK, false); - this.basicTestConnection(null, securityBs, + this.basicTestConnection(null, securityRpkBs, deviceCredentials, clientEndpoint, transportConfiguration, diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/X509_NoTrustLwM2MIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/X509_NoTrustLwM2MIntegrationTest.java index 1ba408ac70..fd8a51b041 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/X509_NoTrustLwM2MIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/X509_NoTrustLwM2MIntegrationTest.java @@ -51,15 +51,14 @@ public class X509_NoTrustLwM2MIntegrationTest extends AbstractSecurityLwM2MInteg X509ClientCredential clientCredentials = new X509ClientCredential(); clientCredentials.setEndpoint(clientEndpoint); clientCredentials.setCert(Base64.getEncoder().encodeToString(certificate.getEncoded())); - Security security = x509(SECURE_URI, + Security securityX509 = x509(SECURE_URI, shortServerId, certificate.getEncoded(), privateKey.getEncoded(), serverX509Cert.getEncoded()); Lwm2mDeviceProfileTransportConfiguration transportConfiguration = getTransportConfiguration(OBSERVE_ATTRIBUTES_WITHOUT_PARAMS, getBootstrapServerCredentialsSecure(X509, NONE)); LwM2MDeviceCredentials deviceCredentials = getDeviceCredentialsSecure(clientCredentials, privateKey, certificate, X509, false); - this.basicTestConnection(security, - null, + this.basicTestConnection(securityX509, null, deviceCredentials, clientEndpoint, transportConfiguration, @@ -119,8 +118,7 @@ public class X509_NoTrustLwM2MIntegrationTest extends AbstractSecurityLwM2MInteg serverX509CertBs.getEncoded()); Lwm2mDeviceProfileTransportConfiguration transportConfiguration = getTransportConfiguration(OBSERVE_ATTRIBUTES_WITHOUT_PARAMS, getBootstrapServerCredentialsSecure(X509, BOTH)); LwM2MDeviceCredentials deviceCredentials = getDeviceCredentialsSecure(clientCredentials, privateKey, certificate, X509, false); - this.basicTestConnection(security, - null, + this.basicTestConnection(security, null, deviceCredentials, clientEndpoint, transportConfiguration, diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/X509_TrustLwM2MIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/X509_TrustLwM2MIntegrationTest.java index 81b708ba33..b7a6290741 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/X509_TrustLwM2MIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/X509_TrustLwM2MIntegrationTest.java @@ -50,8 +50,7 @@ public class X509_TrustLwM2MIntegrationTest extends AbstractSecurityLwM2MIntegra serverX509Cert.getEncoded()); Lwm2mDeviceProfileTransportConfiguration transportConfiguration = getTransportConfiguration(OBSERVE_ATTRIBUTES_WITHOUT_PARAMS, getBootstrapServerCredentialsSecure(X509, NONE)); LwM2MDeviceCredentials deviceCredentials = getDeviceCredentialsSecure(clientCredentials, privateKey, certificate, X509, false); - this.basicTestConnection(security, - null, + this.basicTestConnection(security, null, deviceCredentials, clientEndpoint, transportConfiguration, @@ -77,8 +76,7 @@ public class X509_TrustLwM2MIntegrationTest extends AbstractSecurityLwM2MIntegra serverX509CertBs.getEncoded()); Lwm2mDeviceProfileTransportConfiguration transportConfiguration = getTransportConfiguration(OBSERVE_ATTRIBUTES_WITHOUT_PARAMS, getBootstrapServerCredentialsSecure(X509, BOTH)); LwM2MDeviceCredentials deviceCredentials = getDeviceCredentialsSecure(clientCredentials, privateKey, certificate, X509, false); - this.basicTestConnection(security, - null, + this.basicTestConnection(security,null, deviceCredentials, clientEndpoint, transportConfiguration, diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/telemetry/timeseries/AbstractMqttTimeseriesIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/telemetry/timeseries/AbstractMqttTimeseriesIntegrationTest.java index 6543955ed5..9856aa18cb 100644 --- a/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/telemetry/timeseries/AbstractMqttTimeseriesIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/telemetry/timeseries/AbstractMqttTimeseriesIntegrationTest.java @@ -25,10 +25,10 @@ import org.springframework.boot.test.mock.mockito.SpyBean; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.msg.gateway.metrics.GatewayMetadata; import org.thingsboard.server.transport.mqtt.AbstractMqttIntegrationTest; import org.thingsboard.server.transport.mqtt.MqttTestConfigProperties; import org.thingsboard.server.transport.mqtt.gateway.GatewayMetricsService; -import org.thingsboard.server.common.msg.gateway.metrics.GatewayMetadata; import org.thingsboard.server.transport.mqtt.gateway.metrics.GatewayMetricsState; import org.thingsboard.server.transport.mqtt.mqttv3.MqttTestCallback; import org.thingsboard.server.transport.mqtt.mqttv3.MqttTestClient; @@ -43,6 +43,7 @@ import java.util.Random; import java.util.Set; import java.util.concurrent.TimeUnit; +import static org.awaitility.Awaitility.await; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.mockito.Mockito.any; @@ -112,6 +113,42 @@ public abstract class AbstractMqttTimeseriesIntegrationTest extends AbstractMqtt processGatewayTelemetryTest(GATEWAY_TELEMETRY_TOPIC, expectedKeys, payload.getBytes(), deviceName1, deviceName2); } + @Test + public void testAckIsReceivedOnFailedPublishMessage() throws Exception { + String devicePayload = "[{\"ts\": 10000, \"values\": " + PAYLOAD_VALUES_STR + "}]"; + String payloadA = "{\"Device A\": " + devicePayload + "}"; + + String deviceBPayload = "[{\"ts\": 10000, \"values\": " + PAYLOAD_VALUES_STR + "}]"; + String payloadB = "{\"Device B\": " + deviceBPayload + "}"; + + testAckIsReceivedOnFailedPublishMessage("Device A", payloadA.getBytes(), "Device B", payloadB.getBytes()); + } + + protected void testAckIsReceivedOnFailedPublishMessage(String deviceName1, byte[] payload1, String deviceName2, byte[] payload2) throws Exception { + updateDefaultTenantProfileConfig(profileConfiguration -> { + profileConfiguration.setMaxDevices(3); + }); + + MqttTestClient client = new MqttTestClient(); + client.connectAndWait(gatewayAccessToken); + client.publishAndWait(GATEWAY_TELEMETRY_TOPIC, payload1); + + // check device is created + await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> { + assertNotNull(doGet("/api/tenant/devices?deviceName=" + deviceName1, Device.class)); + }); + + client.publishAndWait(GATEWAY_TELEMETRY_TOPIC, payload2); + client.disconnectAndWait(); + + // check device was not created due to limit + doGet("/api/tenant/devices?deviceName=" + deviceName2).andExpect(status().isNotFound()); + + updateDefaultTenantProfileConfig(profileConfiguration -> { + profileConfiguration.setMaxDevices(0); + }); + } + @Test public void testGatewayConnect() throws Exception { String payload = "{\"device\":\"Device A\"}"; diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/telemetry/timeseries/AbstractMqttTimeseriesJsonIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/telemetry/timeseries/AbstractMqttTimeseriesJsonIntegrationTest.java index a23506a160..92b35d896c 100644 --- a/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/telemetry/timeseries/AbstractMqttTimeseriesJsonIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/telemetry/timeseries/AbstractMqttTimeseriesJsonIntegrationTest.java @@ -186,4 +186,15 @@ public abstract class AbstractMqttTimeseriesJsonIntegrationTest extends Abstract assertFalse(callback.isPubAckReceived()); } + @Override + public void testAckIsReceivedOnFailedPublishMessage() throws Exception { + MqttTestConfigProperties configProperties = MqttTestConfigProperties.builder() + .deviceName("Test Post Telemetry device json payload") + .gatewayName("Test Post Telemetry gateway json payload") + .transportPayloadType(TransportPayloadType.JSON) + .telemetryTopicFilter(POST_DATA_TELEMETRY_TOPIC) + .build(); + processBeforeTest(configProperties); + super.testAckIsReceivedOnFailedPublishMessage(); + } } diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/telemetry/timeseries/AbstractMqttTimeseriesProtoIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/telemetry/timeseries/AbstractMqttTimeseriesProtoIntegrationTest.java index 1b6c247d96..60e5857c04 100644 --- a/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/telemetry/timeseries/AbstractMqttTimeseriesProtoIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/telemetry/timeseries/AbstractMqttTimeseriesProtoIntegrationTest.java @@ -459,6 +459,31 @@ public abstract class AbstractMqttTimeseriesProtoIntegrationTest extends Abstrac assertFalse(callback.isPubAckReceived()); } + @Override + public void testAckIsReceivedOnFailedPublishMessage() throws Exception { + MqttTestConfigProperties configProperties = MqttTestConfigProperties.builder() + .deviceName("Test Post Telemetry device proto payload") + .gatewayName("Test Post Telemetry gateway proto payload") + .transportPayloadType(TransportPayloadType.PROTOBUF) + .telemetryTopicFilter(POST_DATA_TELEMETRY_TOPIC) + .build(); + processBeforeTest(configProperties); + + TransportApiProtos.GatewayTelemetryMsg.Builder gatewayTelemetryMsgProtoBuilder = TransportApiProtos.GatewayTelemetryMsg.newBuilder(); + List expectedKeys = Arrays.asList("key1", "key2", "key3", "key4", "key5"); + String deviceName1 = "Device A"; + String deviceName2 = "Device B"; + TransportApiProtos.TelemetryMsg deviceATelemetryMsgProto = getDeviceTelemetryMsgProto(deviceName1, expectedKeys, 10000, 20000); + gatewayTelemetryMsgProtoBuilder.addAllMsg(List.of(deviceATelemetryMsgProto)); + TransportApiProtos.GatewayTelemetryMsg payload1 = gatewayTelemetryMsgProtoBuilder.build(); + + TransportApiProtos.TelemetryMsg deviceBTelemetryMsgProto = getDeviceTelemetryMsgProto(deviceName2, expectedKeys, 10000, 20000); + TransportApiProtos.GatewayTelemetryMsg payload2 = TransportApiProtos.GatewayTelemetryMsg.newBuilder() + .addAllMsg(List.of(deviceBTelemetryMsgProto)) + .build(); + super.testAckIsReceivedOnFailedPublishMessage(deviceName1, payload1.toByteArray(), deviceName2, payload2.toByteArray()); + } + private DynamicSchema getDynamicSchema() { DeviceProfileTransportConfiguration transportConfiguration = deviceProfile.getProfileData().getTransportConfiguration(); assertTrue(transportConfiguration instanceof MqttDeviceProfileTransportConfiguration); diff --git a/application/src/test/java/org/thingsboard/server/utils/CalculatedFieldUtilsTest.java b/application/src/test/java/org/thingsboard/server/utils/CalculatedFieldUtilsTest.java new file mode 100644 index 0000000000..64b2fb032e --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/utils/CalculatedFieldUtilsTest.java @@ -0,0 +1,167 @@ +/** + * Copyright © 2016-2025 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.utils; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; +import org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingPresenceStatus; +import org.thingsboard.server.common.data.id.AssetId; +import org.thingsboard.server.common.data.id.CalculatedFieldId; +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.kv.BaseAttributeKvEntry; +import org.thingsboard.server.common.data.kv.JsonDataEntry; +import org.thingsboard.server.common.data.kv.StringDataEntry; +import org.thingsboard.server.gen.transport.TransportProtos.CalculatedFieldStateProto; +import org.thingsboard.server.service.cf.ctx.CalculatedFieldEntityCtxId; +import org.thingsboard.server.service.cf.ctx.state.ArgumentEntry; +import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx; +import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldState; +import org.thingsboard.server.service.cf.ctx.state.SingleValueArgumentEntry; +import org.thingsboard.server.service.cf.ctx.state.geofencing.GeofencingArgumentEntry; +import org.thingsboard.server.service.cf.ctx.state.geofencing.GeofencingCalculatedFieldState; +import org.thingsboard.server.service.cf.ctx.state.geofencing.GeofencingZoneState; +import org.thingsboard.server.service.cf.ctx.state.propagation.PropagationArgumentEntry; +import org.thingsboard.server.service.cf.ctx.state.propagation.PropagationCalculatedFieldState; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.thingsboard.server.common.data.cf.configuration.PropagationCalculatedFieldConfiguration.PROPAGATION_CONFIG_ARGUMENT; +import static org.thingsboard.server.utils.CalculatedFieldUtils.toProto; + +@ExtendWith(MockitoExtension.class) +class CalculatedFieldUtilsTest { + + private static final TenantId TENANT_ID = TenantId.fromUUID(UUID.fromString("0a69e1e2-fcbc-4234-a4cd-3844bf54035c")); + private static final CalculatedFieldId CF_ID = CalculatedFieldId.fromString("ec0e91b9-6f27-4e93-946a-5fbc2707d8bc"); + private static final DeviceId DEVICE_ID = DeviceId.fromString("1e03bd38-2010-4739-9362-160c288e36c4"); + + @Test + void toProtoAndFromProto_shouldMapGeofencingArgumentsAndZones() { + // given + CalculatedFieldEntityCtxId stateId = mock(CalculatedFieldEntityCtxId.class); + given(stateId.tenantId()).willReturn(TENANT_ID); + given(stateId.cfId()).willReturn(CF_ID); + given(stateId.entityId()).willReturn(DEVICE_ID); + + // Build a geofencing argument with two zones (one with inside=true, one with inside=null) + GeofencingArgumentEntry geofencingArgumentEntry = new GeofencingArgumentEntry(); + Map zoneStates = new LinkedHashMap<>(); + + UUID zoneId1 = UUID.fromString("624a8fff-71a2-4847-a100-ff1cf52dbe71"); + UUID zoneId2 = UUID.fromString("e2adf6ce-9478-40b1-b0e9-4a6860cc46bb"); + + AssetId z1 = new AssetId(zoneId1); + AssetId z2 = new AssetId(zoneId2); + + JsonDataEntry zone1 = new JsonDataEntry("zone", "[[50.472000, 30.504000], [50.472000, 30.506000], [50.474000, 30.506000], [50.474000, 30.504000]]"); + JsonDataEntry zone2 = new JsonDataEntry("zone", "[[50.475000, 30.510000], [50.475000, 30.512000], [50.477000, 30.512000], [50.477000, 30.510000]]"); + + BaseAttributeKvEntry zone1PerimeterAttribute = new BaseAttributeKvEntry(zone1, System.currentTimeMillis(), 0L); + BaseAttributeKvEntry zone2PerimeterAttribute = new BaseAttributeKvEntry(zone2, System.currentTimeMillis(), 0L); + + GeofencingZoneState s1 = new GeofencingZoneState(z1, zone1PerimeterAttribute); + s1.setLastPresence(GeofencingPresenceStatus.INSIDE); + GeofencingZoneState s2 = new GeofencingZoneState(z2, zone2PerimeterAttribute); + + zoneStates.put(z1, s1); + zoneStates.put(z2, s2); + geofencingArgumentEntry.setZoneStates(zoneStates); + + // Create cf state with the geofencing argument and add it to the state map + CalculatedFieldState state = new GeofencingCalculatedFieldState(DEVICE_ID); + + CalculatedFieldCtx cfCtxMock = mock(CalculatedFieldCtx.class); + when(cfCtxMock.getArgNames()).thenReturn(List.of("geofencingArgumentTest")); + + state.setCtx(cfCtxMock, null); + + Map updatedArguments = state.update(Map.of("geofencingArgumentTest", geofencingArgumentEntry), cfCtxMock); + assertThat(updatedArguments).hasSize(1); + assertThat(updatedArguments.get("geofencingArgumentTest")).isEqualTo(geofencingArgumentEntry); + + CalculatedFieldStateProto proto = toProto(stateId, state); + CalculatedFieldState fromProto = CalculatedFieldUtils.fromProto(stateId, proto); + + assertThat(fromProto) + .usingRecursiveComparison() + .ignoringFields("ctx", "requiredArguments", "readinessStatus") + .isEqualTo(state); + + ArgumentEntry fromProtoArgument = fromProto.getArguments().get("geofencingArgumentTest"); + assertThat(fromProtoArgument).isInstanceOf(GeofencingArgumentEntry.class); + GeofencingArgumentEntry fromProtoGeoArgument = (GeofencingArgumentEntry) fromProtoArgument; + assertThat(fromProtoGeoArgument.getZoneStates()).hasSize(2); + assertThat(fromProtoGeoArgument.getZoneStates().get(z1).getLastPresence()).isEqualTo(GeofencingPresenceStatus.INSIDE); + assertThat(fromProtoGeoArgument.getZoneStates().get(z2).getLastPresence()).isNull(); + } + + @Test + void toProtoAndFromProto_shouldCreatePropagationStateWithoutPropagationArgument() { + // given + CalculatedFieldEntityCtxId stateId = mock(CalculatedFieldEntityCtxId.class); + given(stateId.tenantId()).willReturn(TENANT_ID); + given(stateId.cfId()).willReturn(CF_ID); + given(stateId.entityId()).willReturn(DEVICE_ID); + + AssetId propagationAssetId = new AssetId(UUID.fromString("17bbf99c-3b87-4d21-b07d-da7409bb2bb7")); + PropagationArgumentEntry propagationArgumentEntry = new PropagationArgumentEntry(List.of(propagationAssetId)); + + long lastUpdateTs = System.currentTimeMillis(); + SingleValueArgumentEntry singleValueArgumentEntry = new SingleValueArgumentEntry(new BaseAttributeKvEntry(new StringDataEntry("state", "active"), lastUpdateTs, 1L)); + + CalculatedFieldCtx cfCtxMock = mock(CalculatedFieldCtx.class); + when(cfCtxMock.getArgNames()).thenReturn(List.of("state")); + + CalculatedFieldState state = new PropagationCalculatedFieldState(DEVICE_ID); + + state.setCtx(cfCtxMock, null); + + Map updatedArguments = state.update(Map.of(PROPAGATION_CONFIG_ARGUMENT, propagationArgumentEntry, "state", singleValueArgumentEntry), cfCtxMock); + assertThat(updatedArguments).hasSize(2); + assertThat(updatedArguments.get(PROPAGATION_CONFIG_ARGUMENT)).isEqualTo(propagationArgumentEntry); + assertThat(updatedArguments.get("state")).isEqualTo(singleValueArgumentEntry); + + // when + CalculatedFieldStateProto proto = toProto(stateId, state); + + // then + CalculatedFieldState restored = CalculatedFieldUtils.fromProto(stateId, proto); + + // Propagation argument is not persisted -> should be absent after restore + assertThat(restored).isNotNull(); + assertThat(restored).isInstanceOf(PropagationCalculatedFieldState.class); + + PropagationCalculatedFieldState propagationState = (PropagationCalculatedFieldState) restored; + + assertThat(propagationState.getEntityId()).isEqualTo(DEVICE_ID); + assertThat(propagationState.getArguments()).isNotNull(); + assertThat(propagationState.getArguments().get(PROPAGATION_CONFIG_ARGUMENT)).isNull(); + assertThat(propagationState.getArguments().get("state")).isNotNull().isEqualTo(singleValueArgumentEntry); + assertThat(propagationState.getRequiredArguments()).isNull(); + assertThat(propagationState.getReadinessStatus()).isNull(); + } + +} diff --git a/application/src/test/resources/lwm2m/3.xml b/application/src/test/resources/lwm2m/3-1_2.xml similarity index 100% rename from application/src/test/resources/lwm2m/3.xml rename to application/src/test/resources/lwm2m/3-1_2.xml diff --git a/common/cache/src/main/java/org/thingsboard/server/cache/TBRedisCacheConfiguration.java b/common/cache/src/main/java/org/thingsboard/server/cache/TBRedisCacheConfiguration.java index 8e06913d2c..8f51afed66 100644 --- a/common/cache/src/main/java/org/thingsboard/server/cache/TBRedisCacheConfiguration.java +++ b/common/cache/src/main/java/org/thingsboard/server/cache/TBRedisCacheConfiguration.java @@ -79,10 +79,10 @@ public abstract class TBRedisCacheConfiguration { @Value("${redis.pool_config.minIdle:16}") private int minIdle; - @Value("${redis.pool_config.testOnBorrow:true}") + @Value("${redis.pool_config.testOnBorrow:false}") private boolean testOnBorrow; - @Value("${redis.pool_config.testOnReturn:true}") + @Value("${redis.pool_config.testOnReturn:false}") private boolean testOnReturn; @Value("${redis.pool_config.testWhileIdle:true}") diff --git a/common/cluster-api/src/main/java/org/thingsboard/server/cluster/TbClusterService.java b/common/cluster-api/src/main/java/org/thingsboard/server/cluster/TbClusterService.java index 6da6fcf8a8..6ce3d5bde9 100644 --- a/common/cluster-api/src/main/java/org/thingsboard/server/cluster/TbClusterService.java +++ b/common/cluster-api/src/main/java/org/thingsboard/server/cluster/TbClusterService.java @@ -16,6 +16,7 @@ package org.thingsboard.server.cluster; import org.thingsboard.server.common.data.ApiUsageState; +import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.TbResourceInfo; @@ -29,6 +30,7 @@ import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; +import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.ToDeviceActorNotificationMsg; import org.thingsboard.server.common.msg.edge.EdgeEventUpdateMsg; @@ -38,7 +40,6 @@ import org.thingsboard.server.common.msg.edge.ToEdgeSyncRequest; import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.common.msg.rpc.FromDeviceRpcResponse; -import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos.RestApiCallResponseMsgProto; import org.thingsboard.server.gen.transport.TransportProtos.ToCalculatedFieldMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToCalculatedFieldNotificationMsg; @@ -81,7 +82,7 @@ public interface TbClusterService extends TbQueueClusterService { void pushNotificationToTransport(String targetServiceId, ToTransportMsg response, TbQueueCallback callback); - void pushMsgToCalculatedFields(TenantId tenantId, EntityId entityId, TransportProtos.ToCalculatedFieldMsg msg, TbQueueCallback callback); + void pushMsgToCalculatedFields(TenantId tenantId, EntityId entityId, ToCalculatedFieldMsg msg, TbQueueCallback callback); void pushMsgToCalculatedFields(TopicPartitionInfo tpi, UUID msgId, ToCalculatedFieldMsg msg, TbQueueCallback callback); @@ -131,8 +132,14 @@ public interface TbClusterService extends TbQueueClusterService { void sendNotificationMsgToEdge(TenantId tenantId, EdgeId edgeId, EntityId entityId, String body, EdgeEventType type, EdgeEventActionType action, EdgeId sourceEdgeId); + void onCustomerUpdated(Customer customer, Customer oldCustomer); + void onCalculatedFieldUpdated(CalculatedField calculatedField, CalculatedField oldCalculatedField, TbQueueCallback callback); void onCalculatedFieldDeleted(CalculatedField calculatedField, TbQueueCallback callback); + void onRelationUpdated(TenantId tenantId, EntityRelation entityRelation, TbQueueCallback callback); + + void onRelationDeleted(TenantId tenantId, EntityRelation entityRelation, TbQueueCallback callback); + } diff --git a/common/cluster-api/src/main/java/org/thingsboard/server/queue/TbEdgeQueueAdmin.java b/common/cluster-api/src/main/java/org/thingsboard/server/queue/TbEdgeQueueAdmin.java index 9be50bb145..9210d4c1a8 100644 --- a/common/cluster-api/src/main/java/org/thingsboard/server/queue/TbEdgeQueueAdmin.java +++ b/common/cluster-api/src/main/java/org/thingsboard/server/queue/TbEdgeQueueAdmin.java @@ -16,7 +16,7 @@ package org.thingsboard.server.queue; public interface TbEdgeQueueAdmin extends TbQueueAdmin { + void syncEdgeNotificationsOffsets(String fatGroupId, String newGroupId); - void deleteConsumerGroup(String consumerGroupId); } diff --git a/common/cluster-api/src/main/java/org/thingsboard/server/queue/TbQueueAdmin.java b/common/cluster-api/src/main/java/org/thingsboard/server/queue/TbQueueAdmin.java index 48d9b3c34f..0b9925765c 100644 --- a/common/cluster-api/src/main/java/org/thingsboard/server/queue/TbQueueAdmin.java +++ b/common/cluster-api/src/main/java/org/thingsboard/server/queue/TbQueueAdmin.java @@ -18,12 +18,13 @@ package org.thingsboard.server.queue; public interface TbQueueAdmin { default void createTopicIfNotExists(String topic) { - createTopicIfNotExists(topic, null); + createTopicIfNotExists(topic, null, false); } - void createTopicIfNotExists(String topic, String properties); + void createTopicIfNotExists(String topic, String properties, boolean force); void destroy(); void deleteTopic(String topic); + } diff --git a/common/cluster-api/src/main/java/org/thingsboard/server/queue/TbQueueConsumer.java b/common/cluster-api/src/main/java/org/thingsboard/server/queue/TbQueueConsumer.java index f9483965cc..3e1462b445 100644 --- a/common/cluster-api/src/main/java/org/thingsboard/server/queue/TbQueueConsumer.java +++ b/common/cluster-api/src/main/java/org/thingsboard/server/queue/TbQueueConsumer.java @@ -38,6 +38,8 @@ public interface TbQueueConsumer { boolean isStopped(); + Set getPartitions(); + List getFullTopicNames(); } diff --git a/common/coap-server/src/main/java/org/thingsboard/server/coapserver/TbCoapDtlsSettings.java b/common/coap-server/src/main/java/org/thingsboard/server/coapserver/TbCoapDtlsSettings.java index 56705b5608..f98dc7d3c6 100644 --- a/common/coap-server/src/main/java/org/thingsboard/server/coapserver/TbCoapDtlsSettings.java +++ b/common/coap-server/src/main/java/org/thingsboard/server/coapserver/TbCoapDtlsSettings.java @@ -66,7 +66,7 @@ public class TbCoapDtlsSettings { @Value("${coap.dtls.retransmission_timeout:9000}") private int dtlsRetransmissionTimeout; - @Value("${coap.dtls.connection_id_length:}") + @Value("${coap.dtls.connection_id_length:8}") private Integer cIdLength; @Value("${coap.dtls.max_transmission_unit:1024}") diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/ai/AiModelService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/ai/AiModelService.java index 3ad12048cf..55abc80998 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/ai/AiModelService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/ai/AiModelService.java @@ -29,6 +29,8 @@ public interface AiModelService extends EntityDaoService { AiModel save(AiModel model); + AiModel save(AiModel model, boolean doValidate); + Optional findAiModelById(TenantId tenantId, AiModelId modelId); PageData findAiModelsByTenantId(TenantId tenantId, PageLink pageLink); @@ -37,6 +39,8 @@ public interface AiModelService extends EntityDaoService { FluentFuture> findAiModelByTenantIdAndIdAsync(TenantId tenantId, AiModelId modelId); + Optional findAiModelByTenantIdAndName(TenantId tenantId, String name); + boolean deleteByTenantIdAndId(TenantId tenantId, AiModelId modelId); } diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/alarm/AlarmService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/alarm/AlarmService.java index e26955d465..05abc4b0c7 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/alarm/AlarmService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/alarm/AlarmService.java @@ -51,10 +51,6 @@ import java.util.UUID; public interface AlarmService extends EntityDaoService { - /* - * New API, since 3.5. - */ - /** * Designed for atomic operations over active alarms. * Only one active alarm may exist for the pair {originatorId, alarmType} @@ -74,7 +70,7 @@ public interface AlarmService extends EntityDaoService { AlarmApiCallResult acknowledgeAlarm(TenantId tenantId, AlarmId alarmId, long ackTs); - AlarmApiCallResult clearAlarm(TenantId tenantId, AlarmId alarmId, long clearTs, JsonNode details); + AlarmApiCallResult clearAlarm(TenantId tenantId, AlarmId alarmId, long clearTs, JsonNode details, boolean pushEvent); AlarmApiCallResult assignAlarm(TenantId tenantId, AlarmId alarmId, UserId assigneeId, long ts); diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/asset/AssetService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/asset/AssetService.java index c22c9c4140..59c17fb4bd 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/asset/AssetService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/asset/AssetService.java @@ -17,6 +17,7 @@ package org.thingsboard.server.dao.asset; import com.google.common.util.concurrent.ListenableFuture; import org.thingsboard.server.common.data.EntitySubtype; +import org.thingsboard.server.common.data.NameConflictStrategy; import org.thingsboard.server.common.data.ProfileEntityIdInfo; import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.asset.AssetInfo; @@ -48,6 +49,8 @@ public interface AssetService extends EntityDaoService { Asset saveAsset(Asset asset); + Asset saveAsset(Asset asset, NameConflictStrategy nameConflictStrategy); + Asset assignAssetToCustomer(TenantId tenantId, AssetId assetId, CustomerId customerId); Asset unassignAssetFromCustomer(TenantId tenantId, AssetId assetId); diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/cf/CalculatedFieldService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/cf/CalculatedFieldService.java index 85cd8d24fd..bef4675e03 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/cf/CalculatedFieldService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/cf/CalculatedFieldService.java @@ -16,9 +16,8 @@ package org.thingsboard.server.dao.cf; import org.thingsboard.server.common.data.cf.CalculatedField; -import org.thingsboard.server.common.data.cf.CalculatedFieldLink; +import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.id.CalculatedFieldId; -import org.thingsboard.server.common.data.id.CalculatedFieldLinkId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; @@ -35,7 +34,7 @@ public interface CalculatedFieldService extends EntityDaoService { CalculatedField findById(TenantId tenantId, CalculatedFieldId calculatedFieldId); - CalculatedField findByEntityIdAndName(EntityId entityId, String name); + CalculatedField findByEntityIdAndTypeAndName(EntityId entityId, CalculatedFieldType type, String name); List findCalculatedFieldIdsByEntityId(TenantId tenantId, EntityId entityId); @@ -45,24 +44,12 @@ public interface CalculatedFieldService extends EntityDaoService { PageData findCalculatedFieldsByTenantId(TenantId tenantId, PageLink pageLink); - PageData findAllCalculatedFieldsByEntityId(TenantId tenantId, EntityId entityId, PageLink pageLink); + PageData findCalculatedFieldsByEntityId(TenantId tenantId, EntityId entityId, CalculatedFieldType type, PageLink pageLink); void deleteCalculatedField(TenantId tenantId, CalculatedFieldId calculatedFieldId); int deleteAllCalculatedFieldsByEntityId(TenantId tenantId, EntityId entityId); - CalculatedFieldLink saveCalculatedFieldLink(TenantId tenantId, CalculatedFieldLink calculatedFieldLink); - - CalculatedFieldLink findCalculatedFieldLinkById(TenantId tenantId, CalculatedFieldLinkId calculatedFieldLinkId); - - List findAllCalculatedFieldLinksById(TenantId tenantId, CalculatedFieldId calculatedFieldId); - - List findAllCalculatedFieldLinksByEntityId(TenantId tenantId, EntityId entityId); - - PageData findAllCalculatedFieldLinksByTenantId(TenantId tenantId, PageLink pageLink); - - PageData findAllCalculatedFieldLinks(PageLink pageLink); - boolean referencedInAnyCalculatedField(TenantId tenantId, EntityId referencedEntityId); } diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/customer/CustomerService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/customer/CustomerService.java index d478e2099a..1cd5ff9ff1 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/customer/CustomerService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/customer/CustomerService.java @@ -17,6 +17,7 @@ package org.thingsboard.server.dao.customer; import com.google.common.util.concurrent.ListenableFuture; import org.thingsboard.server.common.data.Customer; +import org.thingsboard.server.common.data.NameConflictStrategy; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; @@ -37,6 +38,8 @@ public interface CustomerService extends EntityDaoService { Customer saveCustomer(Customer customer); + Customer saveCustomer(Customer customer, NameConflictStrategy nameConflictStrategy); + void deleteCustomer(TenantId tenantId, CustomerId customerId); Customer findOrCreatePublicCustomer(TenantId tenantId); diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceService.java index 9eb258f182..759ab3a708 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceService.java @@ -23,6 +23,7 @@ import org.thingsboard.server.common.data.DeviceInfoFilter; import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.DeviceTransportType; import org.thingsboard.server.common.data.EntitySubtype; +import org.thingsboard.server.common.data.NameConflictStrategy; import org.thingsboard.server.common.data.ProfileEntityIdInfo; import org.thingsboard.server.common.data.device.DeviceSearchQuery; import org.thingsboard.server.common.data.id.CustomerId; @@ -58,8 +59,12 @@ public interface DeviceService extends EntityDaoService { Device saveDeviceWithAccessToken(Device device, String accessToken); + Device saveDeviceWithAccessToken(Device device, String accessToken, NameConflictStrategy nameConflictStrategy); + Device saveDeviceWithCredentials(Device device, DeviceCredentials deviceCredentials); + Device saveDeviceWithCredentials(Device device, DeviceCredentials deviceCredentials, NameConflictStrategy nameConflictStrategy); + Device saveDevice(ProvisionRequest provisionRequest, DeviceProfile profile); Device assignDeviceToCustomer(TenantId tenantId, DeviceId deviceId, CustomerId customerId); diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/entity/EntityDaoService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/entity/EntityDaoService.java index e9350842f8..dca2e81f29 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/entity/EntityDaoService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/entity/EntityDaoService.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.dao.entity; +import com.google.common.util.concurrent.FluentFuture; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.HasId; @@ -26,6 +27,8 @@ public interface EntityDaoService { Optional> findEntity(TenantId tenantId, EntityId entityId); + FluentFuture>> findEntityAsync(TenantId tenantId, EntityId entityId); + default long countByTenantId(TenantId tenantId) { throw new IllegalArgumentException("Not implemented for " + getEntityType()); } diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/entity/EntityService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/entity/EntityService.java index 9adf703e0b..0e41e0b031 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/entity/EntityService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/entity/EntityService.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.dao.entity; +import com.google.common.util.concurrent.FluentFuture; import org.thingsboard.server.common.data.EntityInfo; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.EntityId; @@ -38,6 +39,8 @@ public interface EntityService { Optional fetchEntityCustomerId(TenantId tenantId, EntityId entityId); + FluentFuture> fetchEntityCustomerIdAsync(TenantId tenantId, EntityId entityId); + Optional> fetchEntity(TenantId tenantId, EntityId entityId); Map fetchEntityInfos(TenantId tenantId, CustomerId customerId, Set entityIds); @@ -47,4 +50,5 @@ public interface EntityService { long countEntitiesByQuery(TenantId tenantId, CustomerId customerId, EntityCountQuery query); PageData findEntityDataByQuery(TenantId tenantId, CustomerId customerId, EntityDataQuery query); + } diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/entityview/EntityViewService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/entityview/EntityViewService.java index 6e557106f8..a1bf54b1ee 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/entityview/EntityViewService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/entityview/EntityViewService.java @@ -19,6 +19,7 @@ import com.google.common.util.concurrent.ListenableFuture; import org.thingsboard.server.common.data.EntitySubtype; import org.thingsboard.server.common.data.EntityView; import org.thingsboard.server.common.data.EntityViewInfo; +import org.thingsboard.server.common.data.NameConflictStrategy; import org.thingsboard.server.common.data.entityview.EntityViewSearchQuery; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.EdgeId; @@ -31,13 +32,12 @@ import org.thingsboard.server.dao.entity.EntityDaoService; import java.util.List; -/** - * Created by Victor Basanets on 8/27/2017. - */ public interface EntityViewService extends EntityDaoService { EntityView saveEntityView(EntityView entityView); + EntityView saveEntityView(EntityView entityView, NameConflictStrategy nameConflictStrategy); + EntityView saveEntityView(EntityView entityView, boolean doValidate); EntityView assignEntityViewToCustomer(TenantId tenantId, EntityViewId entityViewId, CustomerId customerId); @@ -52,6 +52,8 @@ public interface EntityViewService extends EntityDaoService { EntityView findEntityViewById(TenantId tenantId, EntityViewId entityViewId, boolean putInCache); + ListenableFuture findEntityViewByIdAsync(TenantId tenantId, EntityViewId entityViewId); + EntityView findEntityViewByTenantIdAndName(TenantId tenantId, String name); ListenableFuture findEntityViewByTenantIdAndNameAsync(TenantId tenantId, String name); @@ -74,8 +76,6 @@ public interface EntityViewService extends EntityDaoService { ListenableFuture> findEntityViewsByQuery(TenantId tenantId, EntityViewSearchQuery query); - ListenableFuture findEntityViewByIdAsync(TenantId tenantId, EntityViewId entityViewId); - ListenableFuture> findEntityViewsByTenantIdAndEntityIdAsync(TenantId tenantId, EntityId entityId); List findEntityViewsByTenantIdAndEntityId(TenantId tenantId, EntityId entityId); @@ -95,4 +95,5 @@ public interface EntityViewService extends EntityDaoService { PageData findEntityViewsByTenantIdAndEdgeId(TenantId tenantId, EdgeId edgeId, PageLink pageLink); PageData findEntityViewsByTenantIdAndEdgeIdAndType(TenantId tenantId, EdgeId edgeId, String type, PageLink pageLink); + } diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/relation/RelationService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/relation/RelationService.java index b61bab9ef3..8cd6f04425 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/relation/RelationService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/relation/RelationService.java @@ -20,11 +20,13 @@ import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.EntityRelationInfo; +import org.thingsboard.server.common.data.relation.EntityRelationPathQuery; import org.thingsboard.server.common.data.relation.EntityRelationsQuery; import org.thingsboard.server.common.data.relation.RelationTypeGroup; import org.thingsboard.server.common.data.rule.RuleChainType; import java.util.List; +import java.util.function.Predicate; public interface RelationService { @@ -80,6 +82,12 @@ public interface RelationService { List findRuleNodeToRuleChainRelations(TenantId tenantId, RuleChainType ruleChainType, int limit); + ListenableFuture> findByRelationPathQueryAsync(TenantId tenantId, EntityRelationPathQuery relationPathQuery); + + ListenableFuture> findFilteredRelationsByPathQueryAsync(TenantId tenantId, EntityRelationPathQuery relationPathQuery, Predicate relationFilter); + + List findByRelationPathQuery(TenantId tenantId, EntityRelationPathQuery relationPathQuery); + // TODO: This method may be useful for some validations in the future // ListenableFuture checkRecursiveRelation(EntityId from, EntityId to); diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/resource/ResourceService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/resource/ResourceService.java index ee187db46b..65211ec17a 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/resource/ResourceService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/resource/ResourceService.java @@ -21,6 +21,7 @@ import org.thingsboard.server.common.data.ResourceExportData; import org.thingsboard.server.common.data.ResourceSubType; import org.thingsboard.server.common.data.ResourceType; import org.thingsboard.server.common.data.TbResource; +import org.thingsboard.server.common.data.TbResourceDataInfo; import org.thingsboard.server.common.data.TbResourceDeleteResult; import org.thingsboard.server.common.data.TbResourceInfo; import org.thingsboard.server.common.data.TbResourceInfoFilter; @@ -46,6 +47,8 @@ public interface ResourceService extends EntityDaoService { byte[] getResourceData(TenantId tenantId, TbResourceId resourceId); + TbResourceDataInfo getResourceDataInfo(TenantId tenantId, TbResourceId resourceId); + ResourceExportData exportResource(TbResourceInfo resourceInfo); List exportResources(TenantId tenantId, Collection resources); @@ -90,4 +93,6 @@ public interface ResourceService extends EntityDaoService { TbResource createOrUpdateSystemResource(ResourceType resourceType, ResourceSubType resourceSubType, String resourceKey, byte[] data); + List findSystemOrTenantResourcesByIds(TenantId tenantId, List resourceIds); + } diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/resource/TbResourceDataCache.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/resource/TbResourceDataCache.java new file mode 100644 index 0000000000..23485684af --- /dev/null +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/resource/TbResourceDataCache.java @@ -0,0 +1,28 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.resource; + +import com.google.common.util.concurrent.FluentFuture; +import org.thingsboard.server.common.data.TbResourceDataInfo; +import org.thingsboard.server.common.data.id.TbResourceId; +import org.thingsboard.server.common.data.id.TenantId; + +public interface TbResourceDataCache { + + FluentFuture getResourceDataInfoAsync(TenantId tenantId, TbResourceId resourceId); + + void evictResourceData(TenantId tenantId, TbResourceId resourceId); +} diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/tenant/TbTenantProfileCache.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/tenant/TbTenantProfileCache.java index 8acfbca542..273b52810a 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/tenant/TbTenantProfileCache.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/tenant/TbTenantProfileCache.java @@ -15,7 +15,6 @@ */ package org.thingsboard.server.dao.tenant; -import org.thingsboard.server.common.data.SystemParams; import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/user/UserService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/user/UserService.java index c016631064..20a09d88ab 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/user/UserService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/user/UserService.java @@ -23,6 +23,8 @@ import org.thingsboard.server.common.data.id.TenantProfileId; import org.thingsboard.server.common.data.id.UserCredentialsId; import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.mobile.MobileSessionInfo; +import org.thingsboard.server.common.data.notification.targets.platform.SystemLevelUsersFilter; +import org.thingsboard.server.common.data.notification.targets.platform.UsersFilter; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.security.UserCredentials; @@ -109,4 +111,9 @@ public interface UserService extends EntityDaoService { void removeMobileSession(TenantId tenantId, String mobileToken); + int countTenantAdmins(TenantId tenantId); + + PageData findUsersByFilter(TenantId tenantId, UsersFilter filter, PageLink pageLink); + + boolean matchesFilter(TenantId tenantId, SystemLevelUsersFilter filter, User user); } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/Device.java b/common/data/src/main/java/org/thingsboard/server/common/data/Device.java index 57a0d24466..e170dbc467 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/Device.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/Device.java @@ -28,6 +28,7 @@ import org.thingsboard.server.common.data.device.data.DeviceData; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.DeviceProfileId; +import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.OtaPackageId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.validation.Length; @@ -142,6 +143,11 @@ public class Device extends BaseDataWithAdditionalInfo implements HasL this.customerId = customerId; } + @JsonIgnore + public EntityId getOwnerId() { + return customerId != null && !customerId.isNullUid() ? customerId : tenantId; + } + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "Unique Device Name in scope of Tenant", example = "A4B72CCDFF33") @Override public String getName() { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/EntityType.java b/common/data/src/main/java/org/thingsboard/server/common/data/EntityType.java index 110052b57f..6a37f9fe1b 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/EntityType.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/EntityType.java @@ -23,6 +23,7 @@ import java.util.EnumSet; import java.util.List; public enum EntityType { + TENANT(1), CUSTOMER(2), USER(3, "tb_user"), @@ -61,7 +62,7 @@ public enum EntityType { MOBILE_APP(37), MOBILE_APP_BUNDLE(38), CALCULATED_FIELD(39), - CALCULATED_FIELD_LINK(40), + // CALCULATED_FIELD_LINK(40), - was removed in 4.3 JOB(41), ADMIN_SETTINGS(42), AI_MODEL(43, "ai_model") { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/GeneralFileDescriptor.java b/common/data/src/main/java/org/thingsboard/server/common/data/GeneralFileDescriptor.java new file mode 100644 index 0000000000..94edd4fa01 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/GeneralFileDescriptor.java @@ -0,0 +1,29 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +@Data +@EqualsAndHashCode +@AllArgsConstructor +@NoArgsConstructor +public class GeneralFileDescriptor { + private String mediaType; +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/HasCustomerId.java b/common/data/src/main/java/org/thingsboard/server/common/data/HasCustomerId.java index 6a5501840d..d7900e96ed 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/HasCustomerId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/HasCustomerId.java @@ -20,4 +20,5 @@ import org.thingsboard.server.common.data.id.CustomerId; public interface HasCustomerId { CustomerId getCustomerId(); + } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/AlarmStateUpdateResult.java b/common/data/src/main/java/org/thingsboard/server/common/data/NameConflictPolicy.java similarity index 82% rename from rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/AlarmStateUpdateResult.java rename to common/data/src/main/java/org/thingsboard/server/common/data/NameConflictPolicy.java index ba7dc5dce8..1685dbc933 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/AlarmStateUpdateResult.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/NameConflictPolicy.java @@ -13,10 +13,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.rule.engine.profile; +package org.thingsboard.server.common.data; -enum AlarmStateUpdateResult { +public enum NameConflictPolicy { - NONE, CREATED, UPDATED, SEVERITY_UPDATED, CLEARED; + FAIL, + UNIQUIFY; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/NameConflictStrategy.java b/common/data/src/main/java/org/thingsboard/server/common/data/NameConflictStrategy.java new file mode 100644 index 0000000000..9624b8c978 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/NameConflictStrategy.java @@ -0,0 +1,25 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data; + +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema +public record NameConflictStrategy(NameConflictPolicy policy, String separator, UniquifyStrategy uniquifyStrategy) { + + public static final NameConflictStrategy DEFAULT = new NameConflictStrategy(NameConflictPolicy.FAIL, null, null); + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ProfileEntityIdInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/ProfileEntityIdInfo.java index 22934de813..1fb49a46f7 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ProfileEntityIdInfo.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ProfileEntityIdInfo.java @@ -34,21 +34,23 @@ public class ProfileEntityIdInfo implements Serializable, HasTenantId { private static final long serialVersionUID = 8532058281983868003L; private final TenantId tenantId; + private final EntityId ownerId; private final EntityId profileId; private final EntityId entityId; - private ProfileEntityIdInfo(UUID tenantId, EntityId profileId, EntityId entityId) { + private ProfileEntityIdInfo(UUID tenantId, EntityId ownerId, EntityId profileId, EntityId entityId) { this.tenantId = TenantId.fromUUID(tenantId); + this.ownerId = ownerId; this.profileId = profileId; this.entityId = entityId; } - public static ProfileEntityIdInfo create(UUID tenantId, DeviceProfileId profileId, DeviceId entityId) { - return new ProfileEntityIdInfo(tenantId, profileId, entityId); + public static ProfileEntityIdInfo create(UUID tenantId, EntityId ownerId, DeviceProfileId profileId, DeviceId entityId) { + return new ProfileEntityIdInfo(tenantId, ownerId, profileId, entityId); } - public static ProfileEntityIdInfo create(UUID tenantId, AssetProfileId profileId, AssetId entityId) { - return new ProfileEntityIdInfo(tenantId, profileId, entityId); + public static ProfileEntityIdInfo create(UUID tenantId, EntityId ownerId, AssetProfileId profileId, AssetId entityId) { + return new ProfileEntityIdInfo(tenantId, ownerId, profileId, entityId); } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ResourceType.java b/common/data/src/main/java/org/thingsboard/server/common/data/ResourceType.java index 77b17198e9..f7579b6878 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ResourceType.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ResourceType.java @@ -25,7 +25,8 @@ public enum ResourceType { PKCS_12("application/x-pkcs12", false, false), JS_MODULE("application/javascript", true, true), IMAGE(null, true, true), - DASHBOARD("application/json", true, true); + DASHBOARD("application/json", true, true), + GENERAL(null, false, true); @Getter private final String mediaType; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/SystemParams.java b/common/data/src/main/java/org/thingsboard/server/common/data/SystemParams.java index fe3eb4e4d8..0fa9b2dd78 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/SystemParams.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/SystemParams.java @@ -38,5 +38,9 @@ public class SystemParams { String calculatedFieldDebugPerTenantLimitsConfiguration; long maxArgumentsPerCF; long maxDataPointsPerRollingArg; + int minAllowedScheduledUpdateIntervalInSecForCF; + int maxRelationLevelPerCfArgument; + long minAllowedDeduplicationIntervalInSecForCF; + long minAllowedAggregationIntervalInSecForCF; TrendzSettings trendzSettings; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/TbResource.java b/common/data/src/main/java/org/thingsboard/server/common/data/TbResource.java index ba37067106..457d30e263 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/TbResource.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/TbResource.java @@ -16,6 +16,7 @@ package org.thingsboard.server.common.data; import com.fasterxml.jackson.annotation.JsonGetter; +import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonSetter; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; @@ -86,6 +87,11 @@ public class TbResource extends TbResourceInfo { .orElse(null); } + @JsonIgnore + public TbResourceDataInfo toResourceDataInfo() { + return new TbResourceDataInfo(data, getDescriptor()); + } + @Override public String toString() { return super.toString(); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/TbResourceDataInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/TbResourceDataInfo.java new file mode 100644 index 0000000000..039478470d --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/TbResourceDataInfo.java @@ -0,0 +1,31 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data; + +import com.fasterxml.jackson.databind.JsonNode; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class TbResourceDataInfo { + + private byte[] data; + private JsonNode descriptor; + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/TbResourceDeleteResult.java b/common/data/src/main/java/org/thingsboard/server/common/data/TbResourceDeleteResult.java index edc5a2f539..76945a97ed 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/TbResourceDeleteResult.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/TbResourceDeleteResult.java @@ -17,7 +17,6 @@ package org.thingsboard.server.common.data; import lombok.Builder; import lombok.Data; -import org.thingsboard.server.common.data.id.HasId; import java.util.List; import java.util.Map; @@ -27,6 +26,6 @@ import java.util.Map; public class TbResourceDeleteResult { private boolean success; - private Map>> references; + private Map> references; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/UniquifyStrategy.java b/common/data/src/main/java/org/thingsboard/server/common/data/UniquifyStrategy.java new file mode 100644 index 0000000000..5c9841f096 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/UniquifyStrategy.java @@ -0,0 +1,23 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data; + +public enum UniquifyStrategy { + + RANDOM, + INCREMENTAL; + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/dto/TbChatResponse.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/dto/TbChatResponse.java index 2cc17e4553..73e6557fb5 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/dto/TbChatResponse.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/dto/TbChatResponse.java @@ -22,7 +22,7 @@ import io.swagger.v3.oas.annotations.media.Schema; @JsonTypeInfo( use = JsonTypeInfo.Id.NAME, property = "status", - include = JsonTypeInfo.As.PROPERTY, + include = JsonTypeInfo.As.EXISTING_PROPERTY, visible = true ) @JsonSubTypes({ @@ -51,9 +51,7 @@ public sealed interface TbChatResponse permits TbChatResponse.Success, TbChatRes } record Failure( - @Schema( - description = "A string containing details about the failure" - ) + @Schema(description = "A string containing details about the failure") String errorDetails ) implements TbChatResponse { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/AiModelConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/AiModelConfig.java index 0a2b41a91f..f18429e7cf 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/AiModelConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/AiModelConfig.java @@ -24,6 +24,7 @@ import org.thingsboard.server.common.data.ai.model.chat.GitHubModelsChatModelCon import org.thingsboard.server.common.data.ai.model.chat.GoogleAiGeminiChatModelConfig; import org.thingsboard.server.common.data.ai.model.chat.GoogleVertexAiGeminiChatModelConfig; import org.thingsboard.server.common.data.ai.model.chat.MistralAiChatModelConfig; +import org.thingsboard.server.common.data.ai.model.chat.OllamaChatModelConfig; import org.thingsboard.server.common.data.ai.model.chat.OpenAiChatModelConfig; import org.thingsboard.server.common.data.ai.provider.AiProvider; import org.thingsboard.server.common.data.ai.provider.AiProviderConfig; @@ -34,11 +35,12 @@ import org.thingsboard.server.common.data.ai.provider.GitHubModelsProviderConfig import org.thingsboard.server.common.data.ai.provider.GoogleAiGeminiProviderConfig; import org.thingsboard.server.common.data.ai.provider.GoogleVertexAiGeminiProviderConfig; import org.thingsboard.server.common.data.ai.provider.MistralAiProviderConfig; +import org.thingsboard.server.common.data.ai.provider.OllamaProviderConfig; import org.thingsboard.server.common.data.ai.provider.OpenAiProviderConfig; @JsonTypeInfo( use = JsonTypeInfo.Id.NAME, - include = JsonTypeInfo.As.PROPERTY, + include = JsonTypeInfo.As.EXISTING_PROPERTY, property = "provider", visible = true ) @@ -50,7 +52,8 @@ import org.thingsboard.server.common.data.ai.provider.OpenAiProviderConfig; @JsonSubTypes.Type(value = MistralAiChatModelConfig.class, name = "MISTRAL_AI"), @JsonSubTypes.Type(value = AnthropicChatModelConfig.class, name = "ANTHROPIC"), @JsonSubTypes.Type(value = AmazonBedrockChatModelConfig.class, name = "AMAZON_BEDROCK"), - @JsonSubTypes.Type(value = GitHubModelsChatModelConfig.class, name = "GITHUB_MODELS") + @JsonSubTypes.Type(value = GitHubModelsChatModelConfig.class, name = "GITHUB_MODELS"), + @JsonSubTypes.Type(value = OllamaChatModelConfig.class, name = "OLLAMA") }) public interface AiModelConfig { @@ -69,7 +72,8 @@ public interface AiModelConfig { @JsonSubTypes.Type(value = MistralAiProviderConfig.class, name = "MISTRAL_AI"), @JsonSubTypes.Type(value = AnthropicProviderConfig.class, name = "ANTHROPIC"), @JsonSubTypes.Type(value = AmazonBedrockProviderConfig.class, name = "AMAZON_BEDROCK"), - @JsonSubTypes.Type(value = GitHubModelsProviderConfig.class, name = "GITHUB_MODELS") + @JsonSubTypes.Type(value = GitHubModelsProviderConfig.class, name = "GITHUB_MODELS"), + @JsonSubTypes.Type(value = OllamaProviderConfig.class, name = "OLLAMA") }) AiProviderConfig providerConfig(); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AiChatModelConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AiChatModelConfig.java index 2bc28cfce0..49126c1861 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AiChatModelConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AiChatModelConfig.java @@ -24,7 +24,7 @@ public sealed interface AiChatModelConfig> extend permits OpenAiChatModelConfig, AzureOpenAiChatModelConfig, GoogleAiGeminiChatModelConfig, GoogleVertexAiGeminiChatModelConfig, MistralAiChatModelConfig, AnthropicChatModelConfig, - AmazonBedrockChatModelConfig, GitHubModelsChatModelConfig { + AmazonBedrockChatModelConfig, GitHubModelsChatModelConfig, OllamaChatModelConfig { ChatModel configure(Langchain4jChatModelConfigurer configurer); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AmazonBedrockChatModelConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AmazonBedrockChatModelConfig.java index 2bb4de5aa8..d2ab72086a 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AmazonBedrockChatModelConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AmazonBedrockChatModelConfig.java @@ -33,7 +33,7 @@ public record AmazonBedrockChatModelConfig( @NotBlank String modelId, @PositiveOrZero Double temperature, @Positive @Max(1) Double topP, - @Positive Integer maxOutputTokens, + Integer maxOutputTokens, @With @Positive Integer timeoutSeconds, @With @PositiveOrZero Integer maxRetries ) implements AiChatModelConfig { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AnthropicChatModelConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AnthropicChatModelConfig.java index 69b5578fb3..6d505f75a6 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AnthropicChatModelConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AnthropicChatModelConfig.java @@ -34,7 +34,7 @@ public record AnthropicChatModelConfig( @PositiveOrZero Double temperature, @Positive @Max(1) Double topP, @PositiveOrZero Integer topK, - @Positive Integer maxOutputTokens, + Integer maxOutputTokens, @With @Positive Integer timeoutSeconds, @With @PositiveOrZero Integer maxRetries ) implements AiChatModelConfig { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AzureOpenAiChatModelConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AzureOpenAiChatModelConfig.java index 47e7e96c37..f70f2af539 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AzureOpenAiChatModelConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AzureOpenAiChatModelConfig.java @@ -35,7 +35,7 @@ public record AzureOpenAiChatModelConfig( @Positive @Max(1) Double topP, Double frequencyPenalty, Double presencePenalty, - @Positive Integer maxOutputTokens, + Integer maxOutputTokens, @With @Positive Integer timeoutSeconds, @With @PositiveOrZero Integer maxRetries ) implements AiChatModelConfig { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GitHubModelsChatModelConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GitHubModelsChatModelConfig.java index b509254f77..0aafd72197 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GitHubModelsChatModelConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GitHubModelsChatModelConfig.java @@ -35,7 +35,7 @@ public record GitHubModelsChatModelConfig( @Positive @Max(1) Double topP, Double frequencyPenalty, Double presencePenalty, - @Positive Integer maxOutputTokens, + Integer maxOutputTokens, @With @Positive Integer timeoutSeconds, @With @PositiveOrZero Integer maxRetries ) implements AiChatModelConfig { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleAiGeminiChatModelConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleAiGeminiChatModelConfig.java index fe11a11460..b5c3d4263d 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleAiGeminiChatModelConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleAiGeminiChatModelConfig.java @@ -36,7 +36,7 @@ public record GoogleAiGeminiChatModelConfig( @PositiveOrZero Integer topK, Double frequencyPenalty, Double presencePenalty, - @Positive Integer maxOutputTokens, + Integer maxOutputTokens, @With @Positive Integer timeoutSeconds, @With @PositiveOrZero Integer maxRetries ) implements AiChatModelConfig { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleVertexAiGeminiChatModelConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleVertexAiGeminiChatModelConfig.java index 609e14f86e..944963ee27 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleVertexAiGeminiChatModelConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleVertexAiGeminiChatModelConfig.java @@ -36,7 +36,7 @@ public record GoogleVertexAiGeminiChatModelConfig( @PositiveOrZero Integer topK, Double frequencyPenalty, Double presencePenalty, - @Positive Integer maxOutputTokens, + Integer maxOutputTokens, @With @Positive Integer timeoutSeconds, @With @PositiveOrZero Integer maxRetries ) implements AiChatModelConfig { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/Langchain4jChatModelConfigurer.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/Langchain4jChatModelConfigurer.java index c9c1bc3173..828256dcdc 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/Langchain4jChatModelConfigurer.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/Langchain4jChatModelConfigurer.java @@ -35,4 +35,6 @@ public interface Langchain4jChatModelConfigurer { ChatModel configureChatModel(GitHubModelsChatModelConfig chatModelConfig); + ChatModel configureChatModel(OllamaChatModelConfig chatModelConfig); + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/MistralAiChatModelConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/MistralAiChatModelConfig.java index f603e99c53..8f67d93398 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/MistralAiChatModelConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/MistralAiChatModelConfig.java @@ -35,7 +35,7 @@ public record MistralAiChatModelConfig( @Positive @Max(1) Double topP, Double frequencyPenalty, Double presencePenalty, - @Positive Integer maxOutputTokens, + Integer maxOutputTokens, @With @Positive Integer timeoutSeconds, @With @PositiveOrZero Integer maxRetries ) implements AiChatModelConfig { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/OllamaChatModelConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/OllamaChatModelConfig.java new file mode 100644 index 0000000000..ea48670b63 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/OllamaChatModelConfig.java @@ -0,0 +1,58 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.ai.model.chat; + +import dev.langchain4j.model.chat.ChatModel; +import jakarta.validation.Valid; +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Positive; +import jakarta.validation.constraints.PositiveOrZero; +import lombok.Builder; +import lombok.With; +import org.thingsboard.server.common.data.ai.provider.AiProvider; +import org.thingsboard.server.common.data.ai.provider.OllamaProviderConfig; + +@Builder +public record OllamaChatModelConfig( + @NotNull @Valid OllamaProviderConfig providerConfig, + @NotBlank String modelId, + @PositiveOrZero Double temperature, + @Positive @Max(1) Double topP, + @PositiveOrZero Integer topK, + Integer contextLength, + Integer maxOutputTokens, + @With @Positive Integer timeoutSeconds, + @With @PositiveOrZero Integer maxRetries +) implements AiChatModelConfig { + + @Override + public AiProvider provider() { + return AiProvider.OLLAMA; + } + + @Override + public ChatModel configure(Langchain4jChatModelConfigurer configurer) { + return configurer.configureChatModel(this); + } + + @Override + public boolean supportsJsonMode() { + return true; + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/OpenAiChatModelConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/OpenAiChatModelConfig.java index 00b5115d7d..23db9accc2 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/OpenAiChatModelConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/OpenAiChatModelConfig.java @@ -35,7 +35,7 @@ public record OpenAiChatModelConfig( @Positive @Max(1) Double topP, Double frequencyPenalty, Double presencePenalty, - @Positive Integer maxOutputTokens, + Integer maxOutputTokens, @With @Positive Integer timeoutSeconds, @With @PositiveOrZero Integer maxRetries ) implements AiChatModelConfig { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/AiProvider.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/AiProvider.java index d0a5bd0510..a9a6af4de8 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/AiProvider.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/AiProvider.java @@ -24,6 +24,7 @@ public enum AiProvider { MISTRAL_AI, ANTHROPIC, AMAZON_BEDROCK, - GITHUB_MODELS + GITHUB_MODELS, + OLLAMA } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/AiProviderConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/AiProviderConfig.java index bd32c88efb..5423b24410 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/AiProviderConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/AiProviderConfig.java @@ -19,4 +19,4 @@ public sealed interface AiProviderConfig permits OpenAiProviderConfig, AzureOpenAiProviderConfig, GoogleAiGeminiProviderConfig, GoogleVertexAiGeminiProviderConfig, MistralAiProviderConfig, AnthropicProviderConfig, - AmazonBedrockProviderConfig, GitHubModelsProviderConfig {} + AmazonBedrockProviderConfig, GitHubModelsProviderConfig, OllamaProviderConfig {} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/OllamaProviderConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/OllamaProviderConfig.java new file mode 100644 index 0000000000..39bb57834c --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/OllamaProviderConfig.java @@ -0,0 +1,48 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.ai.provider; + +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; + +public record OllamaProviderConfig( + @NotNull String baseUrl, + @NotNull @Valid OllamaAuth auth +) implements AiProviderConfig { + + @JsonTypeInfo( + use = JsonTypeInfo.Id.NAME, + include = JsonTypeInfo.As.PROPERTY, + property = "type" + ) + @JsonSubTypes({ + @JsonSubTypes.Type(value = OllamaAuth.None.class, name = "NONE"), + @JsonSubTypes.Type(value = OllamaAuth.Basic.class, name = "BASIC"), + @JsonSubTypes.Type(value = OllamaAuth.Token.class, name = "TOKEN") + }) + public sealed interface OllamaAuth { + + record None() implements OllamaAuth {} + + record Basic(@NotNull String username, @NotNull String password) implements OllamaAuth {} + + record Token(@NotNull String token) implements OllamaAuth {} + + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/OpenAiProviderConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/OpenAiProviderConfig.java index 09ffda837b..f7864db6e3 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/OpenAiProviderConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/OpenAiProviderConfig.java @@ -15,8 +15,32 @@ */ package org.thingsboard.server.common.data.ai.provider; -import jakarta.validation.constraints.NotNull; +import com.fasterxml.jackson.annotation.JsonIgnore; +import jakarta.validation.constraints.AssertTrue; +import lombok.Builder; +import org.apache.commons.lang3.StringUtils; +import java.util.Objects; + +@Builder public record OpenAiProviderConfig( - @NotNull String apiKey -) implements AiProviderConfig {} + String baseUrl, + String apiKey +) implements AiProviderConfig { + + public static final String OPENAI_OFFICIAL_BASE_URL = "https://api.openai.com/v1"; + + public OpenAiProviderConfig { + baseUrl = Objects.requireNonNullElse(baseUrl, OPENAI_OFFICIAL_BASE_URL); + } + + @JsonIgnore + @AssertTrue(message = "API key is required when using the official OpenAI API") + public boolean isValid() { + if (baseUrl.equals(OPENAI_OFFICIAL_BASE_URL)) { + return StringUtils.isNotBlank(apiKey); + } + return true; + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmInfo.java index 7a80a09891..70032327b9 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmInfo.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmInfo.java @@ -21,11 +21,14 @@ import lombok.Getter; import lombok.Setter; import lombok.ToString; +import java.io.Serial; + @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) @Schema public class AlarmInfo extends Alarm { + @Serial private static final long serialVersionUID = 2807343093519543363L; @Getter @@ -38,6 +41,11 @@ public class AlarmInfo extends Alarm { @Schema(description = "Alarm originator label", example = "Thermostat label") private String originatorLabel; + @Getter + @Setter + @Schema(description = "Originator display name", example = "Thermostat") + private String originatorDisplayName; + @Getter @Setter @Schema(description = "Alarm assignee") @@ -53,8 +61,8 @@ public class AlarmInfo extends Alarm { public AlarmInfo(AlarmInfo alarmInfo) { super(alarmInfo); - this.originatorName = alarmInfo.originatorName; - this.originatorLabel = alarmInfo.originatorLabel; + this.originatorName = alarmInfo.getOriginatorName(); + this.originatorLabel = alarmInfo.getOriginatorLabel(); this.assignee = alarmInfo.getAssignee(); } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/AlarmRule.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/AlarmRule.java new file mode 100644 index 0000000000..9a4e875154 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/AlarmRule.java @@ -0,0 +1,39 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.alarm.rule; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import org.thingsboard.server.common.data.alarm.rule.condition.AlarmCondition; +import org.thingsboard.server.common.data.id.DashboardId; + +@Data +public class AlarmRule { + + @Valid + @NotNull + private AlarmCondition condition; + private String alarmDetails; + private DashboardId dashboardId; + + @JsonIgnore + public boolean requiresScheduledReevaluation() { + return condition.hasSchedule(); + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/AlarmCondition.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/AlarmCondition.java new file mode 100644 index 0000000000..9bb549994b --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/AlarmCondition.java @@ -0,0 +1,56 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.alarm.rule.condition; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonSubTypes.Type; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import jakarta.validation.Valid; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.jetbrains.annotations.NotNull; +import org.thingsboard.server.common.data.alarm.rule.condition.expression.AlarmConditionExpression; +import org.thingsboard.server.common.data.alarm.rule.condition.schedule.AlarmSchedule; +import org.thingsboard.server.common.data.alarm.rule.condition.schedule.AnyTimeSchedule; + +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") +@JsonSubTypes({ + @Type(name = "SIMPLE", value = SimpleAlarmCondition.class), + @Type(name = "DURATION", value = DurationAlarmCondition.class), + @Type(name = "REPEATING", value = RepeatingAlarmCondition.class), +}) +@Data +@NoArgsConstructor +public abstract class AlarmCondition { + + @NotNull + @Valid + private AlarmConditionExpression expression; + @Valid + private AlarmConditionValue schedule; + + @JsonIgnore + public boolean hasSchedule() { + return schedule != null && !(schedule.getStaticValue() instanceof AnyTimeSchedule); + } + + @JsonIgnore + public abstract AlarmConditionType getType(); + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/AlarmConditionType.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/AlarmConditionType.java new file mode 100644 index 0000000000..fd98ed2984 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/AlarmConditionType.java @@ -0,0 +1,22 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.alarm.rule.condition; + +public enum AlarmConditionType { + SIMPLE, + DURATION, + REPEATING +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/AlarmConditionValue.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/AlarmConditionValue.java new file mode 100644 index 0000000000..fab3a78ab3 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/AlarmConditionValue.java @@ -0,0 +1,38 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.alarm.rule.condition; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import jakarta.validation.constraints.AssertTrue; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class AlarmConditionValue { + + private T staticValue; + private String dynamicValueArgument; + + @JsonIgnore + @AssertTrue(message = "Either staticValue or dynamicValueArgument must be set") + public boolean isValid() { + return staticValue != null ^ dynamicValueArgument != null; + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/DurationAlarmCondition.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/DurationAlarmCondition.java new file mode 100644 index 0000000000..22733ab78d --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/DurationAlarmCondition.java @@ -0,0 +1,42 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.alarm.rule.condition; + +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import java.util.concurrent.TimeUnit; + +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class DurationAlarmCondition extends AlarmCondition { + + @NotNull + private TimeUnit unit; + @Valid + @NotNull + private AlarmConditionValue value; + + @Override + public AlarmConditionType getType() { + return AlarmConditionType.DURATION; + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/RepeatingAlarmCondition.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/RepeatingAlarmCondition.java new file mode 100644 index 0000000000..7919a6a22a --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/RepeatingAlarmCondition.java @@ -0,0 +1,38 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.alarm.rule.condition; + +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class RepeatingAlarmCondition extends AlarmCondition { + + @Valid + @NotNull + private AlarmConditionValue count; + + @Override + public AlarmConditionType getType() { + return AlarmConditionType.REPEATING; + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/SimpleAlarmCondition.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/SimpleAlarmCondition.java new file mode 100644 index 0000000000..8e2a7593b0 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/SimpleAlarmCondition.java @@ -0,0 +1,25 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.alarm.rule.condition; + +public class SimpleAlarmCondition extends AlarmCondition { + + @Override + public AlarmConditionType getType() { + return AlarmConditionType.SIMPLE; + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/AlarmConditionExpression.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/AlarmConditionExpression.java new file mode 100644 index 0000000000..e855f8efd3 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/AlarmConditionExpression.java @@ -0,0 +1,35 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.alarm.rule.condition.expression; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonSubTypes.Type; +import com.fasterxml.jackson.annotation.JsonTypeInfo; + +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") +@JsonSubTypes({ + @Type(name = "SIMPLE", value = SimpleAlarmConditionExpression.class), + @Type(name = "TBEL", value = TbelAlarmConditionExpression.class), +}) +public interface AlarmConditionExpression { + + @JsonIgnore + AlarmConditionExpressionType getType(); + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/AlarmConditionExpressionType.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/AlarmConditionExpressionType.java new file mode 100644 index 0000000000..f0b8f5253d --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/AlarmConditionExpressionType.java @@ -0,0 +1,21 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.alarm.rule.condition.expression; + +public enum AlarmConditionExpressionType { + SIMPLE, + TBEL +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/AlarmConditionFilter.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/AlarmConditionFilter.java new file mode 100644 index 0000000000..e99849ea82 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/AlarmConditionFilter.java @@ -0,0 +1,40 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.alarm.rule.condition.expression; + +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import org.thingsboard.server.common.data.alarm.rule.condition.expression.predicate.KeyFilterPredicate; +import org.thingsboard.server.common.data.query.EntityKeyValueType; + +import java.io.Serializable; +import java.util.List; + +@Data +public class AlarmConditionFilter implements Serializable { + + @NotBlank + private String argument; + @NotNull + private EntityKeyValueType valueType; + private ComplexOperation operation; + @Valid + @NotNull + private List predicates; + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/ComplexOperation.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/ComplexOperation.java new file mode 100644 index 0000000000..21c28fa552 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/ComplexOperation.java @@ -0,0 +1,21 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.alarm.rule.condition.expression; + +public enum ComplexOperation { + AND, + OR +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/SimpleAlarmConditionExpression.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/SimpleAlarmConditionExpression.java new file mode 100644 index 0000000000..8c27400961 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/SimpleAlarmConditionExpression.java @@ -0,0 +1,41 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.alarm.rule.condition.expression; + +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotEmpty; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class SimpleAlarmConditionExpression implements AlarmConditionExpression { + + @Valid + @NotEmpty + private List filters; + private ComplexOperation operation; + + @Override + public AlarmConditionExpressionType getType() { + return AlarmConditionExpressionType.SIMPLE; + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/TbelAlarmConditionExpression.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/TbelAlarmConditionExpression.java new file mode 100644 index 0000000000..50f73e887b --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/TbelAlarmConditionExpression.java @@ -0,0 +1,32 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.alarm.rule.condition.expression; + +import jakarta.validation.constraints.NotBlank; +import lombok.Data; + +@Data +public class TbelAlarmConditionExpression implements AlarmConditionExpression { + + @NotBlank + private String expression; + + @Override + public AlarmConditionExpressionType getType() { + return AlarmConditionExpressionType.TBEL; + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/BooleanFilterPredicate.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/BooleanFilterPredicate.java new file mode 100644 index 0000000000..94dced5fe4 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/BooleanFilterPredicate.java @@ -0,0 +1,42 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.alarm.rule.condition.expression.predicate; + +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import org.thingsboard.server.common.data.alarm.rule.condition.AlarmConditionValue; + +@Data +public class BooleanFilterPredicate implements SimpleKeyFilterPredicate { + + @NotNull + private BooleanOperation operation; + @Valid + @NotNull + private AlarmConditionValue value; + + @Override + public FilterPredicateType getType() { + return FilterPredicateType.BOOLEAN; + } + + public enum BooleanOperation { + EQUAL, + NOT_EQUAL + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/ComplexFilterPredicate.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/ComplexFilterPredicate.java new file mode 100644 index 0000000000..4e24ea28ba --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/ComplexFilterPredicate.java @@ -0,0 +1,34 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.alarm.rule.condition.expression.predicate; + +import lombok.Data; +import org.thingsboard.server.common.data.alarm.rule.condition.expression.ComplexOperation; + +import java.util.List; + +@Data +public class ComplexFilterPredicate implements KeyFilterPredicate { + + private ComplexOperation operation; + private List predicates; + + @Override + public FilterPredicateType getType() { + return FilterPredicateType.COMPLEX; + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/FilterPredicateType.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/FilterPredicateType.java new file mode 100644 index 0000000000..af7c45ac5b --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/FilterPredicateType.java @@ -0,0 +1,23 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.alarm.rule.condition.expression.predicate; + +public enum FilterPredicateType { + STRING, + NUMERIC, + BOOLEAN, + COMPLEX +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/KeyFilterPredicate.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/KeyFilterPredicate.java new file mode 100644 index 0000000000..58355c627d --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/KeyFilterPredicate.java @@ -0,0 +1,36 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.alarm.rule.condition.expression.predicate; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonSubTypes.Type; +import com.fasterxml.jackson.annotation.JsonTypeInfo; + +import java.io.Serializable; + +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") +@JsonSubTypes({ + @Type(value = StringFilterPredicate.class, name = "STRING"), + @Type(value = NumericFilterPredicate.class, name = "NUMERIC"), + @Type(value = BooleanFilterPredicate.class, name = "BOOLEAN"), + @Type(value = ComplexFilterPredicate.class, name = "COMPLEX")}) +public interface KeyFilterPredicate extends Serializable { + + @JsonIgnore + FilterPredicateType getType(); + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/NumericFilterPredicate.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/NumericFilterPredicate.java new file mode 100644 index 0000000000..65316eda88 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/NumericFilterPredicate.java @@ -0,0 +1,46 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.alarm.rule.condition.expression.predicate; + +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import org.thingsboard.server.common.data.alarm.rule.condition.AlarmConditionValue; + +@Data +public class NumericFilterPredicate implements SimpleKeyFilterPredicate { + + @NotNull + private NumericOperation operation; + @Valid + @NotNull + private AlarmConditionValue value; + + @Override + public FilterPredicateType getType() { + return FilterPredicateType.NUMERIC; + } + + public enum NumericOperation { + EQUAL, + NOT_EQUAL, + GREATER, + LESS, + GREATER_OR_EQUAL, + LESS_OR_EQUAL + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/SimpleKeyFilterPredicate.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/SimpleKeyFilterPredicate.java new file mode 100644 index 0000000000..0ea4cbf1eb --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/SimpleKeyFilterPredicate.java @@ -0,0 +1,24 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.alarm.rule.condition.expression.predicate; + +import org.thingsboard.server.common.data.alarm.rule.condition.AlarmConditionValue; + +public interface SimpleKeyFilterPredicate extends KeyFilterPredicate { + + AlarmConditionValue getValue(); + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/StringFilterPredicate.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/StringFilterPredicate.java new file mode 100644 index 0000000000..913c12ca1c --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/StringFilterPredicate.java @@ -0,0 +1,49 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.alarm.rule.condition.expression.predicate; + +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import org.thingsboard.server.common.data.alarm.rule.condition.AlarmConditionValue; + +@Data +public class StringFilterPredicate implements SimpleKeyFilterPredicate { + + @NotNull + private StringOperation operation; + @Valid + @NotNull + private AlarmConditionValue value; + private boolean ignoreCase; + + @Override + public FilterPredicateType getType() { + return FilterPredicateType.STRING; + } + + public enum StringOperation { + EQUAL, + NOT_EQUAL, + STARTS_WITH, + ENDS_WITH, + CONTAINS, + NOT_CONTAINS, + IN, + NOT_IN + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/schedule/AlarmSchedule.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/schedule/AlarmSchedule.java new file mode 100644 index 0000000000..e7394c94bd --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/schedule/AlarmSchedule.java @@ -0,0 +1,38 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.alarm.rule.condition.schedule; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonSubTypes.Type; +import com.fasterxml.jackson.annotation.JsonTypeInfo; + +import java.io.Serializable; + +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") +@JsonSubTypes({ + @Type(value = AnyTimeSchedule.class, name = "ANY_TIME"), + @Type(value = SpecificTimeSchedule.class, name = "SPECIFIC_TIME"), + @Type(value = CustomTimeSchedule.class, name = "CUSTOM") +}) +public interface AlarmSchedule extends Serializable { + + @JsonIgnore + AlarmScheduleType getType(); + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/schedule/AlarmScheduleType.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/schedule/AlarmScheduleType.java new file mode 100644 index 0000000000..d18d92834e --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/schedule/AlarmScheduleType.java @@ -0,0 +1,22 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.alarm.rule.condition.schedule; + +public enum AlarmScheduleType { + ANY_TIME, + SPECIFIC_TIME, + CUSTOM +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/schedule/AnyTimeSchedule.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/schedule/AnyTimeSchedule.java new file mode 100644 index 0000000000..e84f767f5b --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/schedule/AnyTimeSchedule.java @@ -0,0 +1,25 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.alarm.rule.condition.schedule; + +public class AnyTimeSchedule implements AlarmSchedule { + + @Override + public AlarmScheduleType getType() { + return AlarmScheduleType.ANY_TIME; + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/schedule/CustomTimeSchedule.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/schedule/CustomTimeSchedule.java new file mode 100644 index 0000000000..b084494d28 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/schedule/CustomTimeSchedule.java @@ -0,0 +1,33 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.alarm.rule.condition.schedule; + +import lombok.Data; + +import java.util.List; + +@Data +public class CustomTimeSchedule implements AlarmSchedule { + + private String timezone; + private List items; + + @Override + public AlarmScheduleType getType() { + return AlarmScheduleType.CUSTOM; + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/schedule/CustomTimeScheduleItem.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/schedule/CustomTimeScheduleItem.java new file mode 100644 index 0000000000..8a2bb97c39 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/schedule/CustomTimeScheduleItem.java @@ -0,0 +1,30 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.alarm.rule.condition.schedule; + +import lombok.Data; + +import java.io.Serializable; + +@Data +public class CustomTimeScheduleItem implements Serializable { + + private boolean enabled; + private int dayOfWeek; + private long startsOn; + private long endsOn; + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/schedule/SpecificTimeSchedule.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/schedule/SpecificTimeSchedule.java new file mode 100644 index 0000000000..7242d2c9cd --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/schedule/SpecificTimeSchedule.java @@ -0,0 +1,35 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.alarm.rule.condition.schedule; + +import lombok.Data; + +import java.util.Set; + +@Data +public class SpecificTimeSchedule implements AlarmSchedule { + + private String timezone; + private Set daysOfWeek; + private long startsOn; + private long endsOn; + + @Override + public AlarmScheduleType getType() { + return AlarmScheduleType.SPECIFIC_TIME; + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/asset/Asset.java b/common/data/src/main/java/org/thingsboard/server/common/data/asset/Asset.java index e732049118..a34b58a4da 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/asset/Asset.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/asset/Asset.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.common.data.asset; +import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.databind.JsonNode; import io.swagger.v3.oas.annotations.media.Schema; import lombok.EqualsAndHashCode; @@ -29,6 +30,7 @@ import org.thingsboard.server.common.data.HasVersion; import org.thingsboard.server.common.data.id.AssetId; import org.thingsboard.server.common.data.id.AssetProfileId; import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.validation.Length; import org.thingsboard.server.common.data.validation.NoXss; @@ -125,6 +127,11 @@ public class Asset extends BaseDataWithAdditionalInfo implements HasLab this.customerId = customerId; } + @JsonIgnore + public EntityId getOwnerId() { + return customerId != null && !customerId.isNullUid() ? customerId : tenantId; + } + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "Unique Asset Name in scope of Tenant", example = "Empire State Building") @Override public String getName() { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/CalculatedField.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/CalculatedField.java index 3b2ddf0627..8c5adefcf8 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/CalculatedField.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/CalculatedField.java @@ -18,11 +18,14 @@ package org.thingsboard.server.common.data.cf; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonSetter; import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.Setter; import org.thingsboard.server.common.data.BaseData; +import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.HasDebugSettings; import org.thingsboard.server.common.data.HasName; import org.thingsboard.server.common.data.HasTenantId; @@ -37,6 +40,10 @@ import org.thingsboard.server.common.data.validation.Length; import org.thingsboard.server.common.data.validation.NoXss; import java.io.Serial; +import java.util.Collections; +import java.util.EnumSet; +import java.util.Map; +import java.util.Set; @Schema @Data @@ -46,6 +53,22 @@ public class CalculatedField extends BaseData implements HasN @Serial private static final long serialVersionUID = 4491966747773381420L; + public static final Map> SUPPORTED_ENTITIES = Map.of( + EntityType.DEVICE, CalculatedFieldType.all, + EntityType.ASSET, CalculatedFieldType.all, + EntityType.DEVICE_PROFILE, CalculatedFieldType.all, + EntityType.ASSET_PROFILE, CalculatedFieldType.all, + EntityType.CUSTOMER, Set.of(CalculatedFieldType.ALARM) + ); + + public static final Set SUPPORTED_REFERENCED_ENTITIES = Collections.unmodifiableSet(EnumSet.of( + EntityType.DEVICE, EntityType.ASSET, EntityType.CUSTOMER, EntityType.TENANT + )); + + public static boolean isSupportedRefEntity(EntityId entity) { + return SUPPORTED_REFERENCED_ENTITIES.contains(entity.getEntityType()); + } + private TenantId tenantId; private EntityId entityId; @@ -64,6 +87,8 @@ public class CalculatedField extends BaseData implements HasN @Schema(description = "Version of calculated field configuration.", example = "0") private int configurationVersion; @Schema(implementation = SimpleCalculatedFieldConfiguration.class) + @Valid + @NotNull private CalculatedFieldConfiguration configuration; @Getter @Setter diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/CalculatedFieldLink.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/CalculatedFieldLink.java index 3f048815da..71f5917c34 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/CalculatedFieldLink.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/CalculatedFieldLink.java @@ -15,52 +15,8 @@ */ package org.thingsboard.server.common.data.cf; -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Data; -import lombok.EqualsAndHashCode; -import org.thingsboard.server.common.data.BaseData; import org.thingsboard.server.common.data.id.CalculatedFieldId; -import org.thingsboard.server.common.data.id.CalculatedFieldLinkId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; -@Schema -@Data -@EqualsAndHashCode(callSuper = true) -public class CalculatedFieldLink extends BaseData { - - private static final long serialVersionUID = 6492846246722091530L; - - private TenantId tenantId; - private EntityId entityId; - - @Schema(description = "JSON object with the Calculated Field Id. ", accessMode = Schema.AccessMode.READ_ONLY) - private CalculatedFieldId calculatedFieldId; - - public CalculatedFieldLink() { - super(); - } - - public CalculatedFieldLink(CalculatedFieldLinkId id) { - super(id); - } - - public CalculatedFieldLink(TenantId tenantId, EntityId entityId, CalculatedFieldId calculatedFieldId) { - this.tenantId = tenantId; - this.entityId = entityId; - this.calculatedFieldId = calculatedFieldId; - } - - @Override - public String toString() { - return new StringBuilder() - .append("CalculatedFieldLink[") - .append("tenantId=").append(tenantId) - .append(", entityId=").append(entityId) - .append(", calculatedFieldId=").append(calculatedFieldId) - .append(", createdTime=").append(createdTime) - .append(", id=").append(id).append(']') - .toString(); - } - -} +public record CalculatedFieldLink(TenantId tenantId, EntityId entityId, CalculatedFieldId calculatedFieldId) {} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/CalculatedFieldType.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/CalculatedFieldType.java index acef67a041..de36b43a40 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/CalculatedFieldType.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/CalculatedFieldType.java @@ -15,8 +15,20 @@ */ package org.thingsboard.server.common.data.cf; +import java.util.Collections; +import java.util.EnumSet; +import java.util.Set; + public enum CalculatedFieldType { - SIMPLE, SCRIPT + SIMPLE, + SCRIPT, + GEOFENCING, + ALARM, + PROPAGATION, + RELATED_ENTITIES_AGGREGATION, + ENTITY_AGGREGATION; + + public static final Set all = Collections.unmodifiableSet(EnumSet.allOf(CalculatedFieldType.class)); } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/AlarmCalculatedFieldConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/AlarmCalculatedFieldConfiguration.java new file mode 100644 index 0000000000..d36ba33849 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/AlarmCalculatedFieldConfiguration.java @@ -0,0 +1,90 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.cf.configuration; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotEmpty; +import lombok.Data; +import org.apache.commons.lang3.tuple.Pair; +import org.thingsboard.server.common.data.alarm.AlarmSeverity; +import org.thingsboard.server.common.data.alarm.rule.AlarmRule; +import org.thingsboard.server.common.data.cf.CalculatedFieldType; +import org.thingsboard.server.common.data.util.CollectionsUtil; + +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.function.BiPredicate; +import java.util.stream.Stream; + +import static java.util.Map.Entry.comparingByKey; + +@Data +public class AlarmCalculatedFieldConfiguration implements ArgumentsBasedCalculatedFieldConfiguration { + + private Map arguments; + + @Valid + @NotEmpty + private Map createRules; + @Valid + private AlarmRule clearRule; + + private boolean propagate; + private boolean propagateToOwner; + private boolean propagateToTenant; + private List propagateRelationTypes; + + @Override + public CalculatedFieldType getType() { + return CalculatedFieldType.ALARM; + } + + @Override + public Output getOutput() { + return null; + } + + @JsonIgnore + @Override + public boolean requiresScheduledReevaluation() { + return getAllRules().anyMatch(entry -> entry.getValue().requiresScheduledReevaluation()); + } + + @JsonIgnore + public Stream> getAllRules() { + Stream> rules = createRules.entrySet().stream() + .map(entry -> Pair.of(entry.getKey(), entry.getValue())); + if (clearRule != null) { + rules = Stream.concat(rules, Stream.of(Pair.of(null, clearRule))); + } + return rules.sorted(comparingByKey(Comparator.nullsLast(Comparator.naturalOrder()))); + } + + public boolean rulesEqual(AlarmCalculatedFieldConfiguration other, BiPredicate equalityCheck) { + List> thisRules = this.getAllRules().toList(); + List> otherRules = other.getAllRules().toList(); + return CollectionsUtil.elementsEqual(thisRules, otherRules, (thisRule, otherRule) -> { + if (!Objects.equals(thisRule.getKey(), otherRule.getKey())) { + return false; + } + return equalityCheck.test(thisRule.getValue(), otherRule.getValue()); + }); + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/Argument.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/Argument.java index e7daa70b1b..8d4a831cf9 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/Argument.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/Argument.java @@ -26,10 +26,27 @@ public class Argument { @Nullable private EntityId refEntityId; + private CfArgumentDynamicSourceConfiguration refDynamicSourceConfiguration; private ReferencedEntityKey refEntityKey; private String defaultValue; private Integer limit; private Long timeWindow; + public boolean hasDynamicSource() { + return refDynamicSourceConfiguration != null; + } + + public boolean hasRelationQuerySource() { + return hasDynamicSource() && refDynamicSourceConfiguration.getType() == CFArgumentDynamicSourceType.RELATION_PATH_QUERY; + } + + public boolean hasOwnerSource() { + return hasDynamicSource() && refDynamicSourceConfiguration.getType() == CFArgumentDynamicSourceType.CURRENT_OWNER; + } + + public boolean hasTsRollingArgument() { + return ArgumentType.TS_ROLLING.equals(refEntityKey.getType()); + } + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/ArgumentsBasedCalculatedFieldConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/ArgumentsBasedCalculatedFieldConfiguration.java new file mode 100644 index 0000000000..294335e665 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/ArgumentsBasedCalculatedFieldConfiguration.java @@ -0,0 +1,46 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.cf.configuration; + +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotEmpty; +import org.thingsboard.server.common.data.id.EntityId; + +import java.util.Collections; +import java.util.Map; +import java.util.Objects; +import java.util.Set; + +import static java.util.stream.Collectors.toSet; + +public interface ArgumentsBasedCalculatedFieldConfiguration extends CalculatedFieldConfiguration { + + @Valid + @NotEmpty + Map getArguments(); + + default Set getReferencedEntities() { + var args = getArguments(); + if (args == null) { + return Collections.emptySet(); + } + return args.values().stream() + .map(Argument::getRefEntityId) + .filter(Objects::nonNull) + .collect(toSet()); + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/BaseCalculatedFieldConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/BaseCalculatedFieldConfiguration.java index 8227ff4603..6913b1ed63 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/BaseCalculatedFieldConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/BaseCalculatedFieldConfiguration.java @@ -16,46 +16,28 @@ package org.thingsboard.server.common.data.cf.configuration; import lombok.Data; -import org.thingsboard.server.common.data.cf.CalculatedFieldLink; -import org.thingsboard.server.common.data.id.CalculatedFieldId; -import org.thingsboard.server.common.data.id.EntityId; -import org.thingsboard.server.common.data.id.TenantId; -import java.util.List; import java.util.Map; -import java.util.Objects; -import java.util.stream.Collectors; @Data -public abstract class BaseCalculatedFieldConfiguration implements CalculatedFieldConfiguration { +public abstract class BaseCalculatedFieldConfiguration implements ExpressionBasedCalculatedFieldConfiguration { protected Map arguments; protected String expression; protected Output output; @Override - public List getReferencedEntities() { - return arguments.values().stream() - .map(Argument::getRefEntityId) - .filter(Objects::nonNull) - .collect(Collectors.toList()); + public void validate() { + baseCalculatedFieldRestriction(); + if (arguments.values().stream().anyMatch(Argument::hasRelationQuerySource)) { + throw new IllegalArgumentException("Calculated field with type: '" + getType() + "' doesn't support relation query configuration!"); + } } - @Override - public List buildCalculatedFieldLinks(TenantId tenantId, EntityId cfEntityId, CalculatedFieldId calculatedFieldId) { - return getReferencedEntities().stream() - .filter(referencedEntity -> !referencedEntity.equals(cfEntityId)) - .map(referencedEntityId -> buildCalculatedFieldLink(tenantId, referencedEntityId, calculatedFieldId)) - .collect(Collectors.toList()); - } - - @Override - public CalculatedFieldLink buildCalculatedFieldLink(TenantId tenantId, EntityId referencedEntityId, CalculatedFieldId calculatedFieldId) { - CalculatedFieldLink link = new CalculatedFieldLink(); - link.setTenantId(tenantId); - link.setEntityId(referencedEntityId); - link.setCalculatedFieldId(calculatedFieldId); - return link; + protected void baseCalculatedFieldRestriction() { + if (arguments.containsKey("ctx")) { + throw new IllegalArgumentException("Argument name 'ctx' is reserved and cannot be used."); + } } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/CFArgumentDynamicSourceType.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/CFArgumentDynamicSourceType.java new file mode 100644 index 0000000000..a9d374015b --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/CFArgumentDynamicSourceType.java @@ -0,0 +1,23 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.cf.configuration; + +public enum CFArgumentDynamicSourceType { + + CURRENT_OWNER, + RELATION_PATH_QUERY + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/CalculatedFieldConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/CalculatedFieldConfiguration.java index ad3d4373ad..886b304aed 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/CalculatedFieldConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/CalculatedFieldConfiguration.java @@ -18,15 +18,21 @@ package org.thingsboard.server.common.data.cf.configuration; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonSubTypes.Type; import com.fasterxml.jackson.annotation.JsonTypeInfo; import org.thingsboard.server.common.data.cf.CalculatedFieldLink; import org.thingsboard.server.common.data.cf.CalculatedFieldType; +import org.thingsboard.server.common.data.cf.configuration.aggregation.single.EntityAggregationCalculatedFieldConfiguration; +import org.thingsboard.server.common.data.cf.configuration.aggregation.RelatedEntitiesAggregationCalculatedFieldConfiguration; +import org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingCalculatedFieldConfiguration; import org.thingsboard.server.common.data.id.CalculatedFieldId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; +import java.util.Collections; import java.util.List; -import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; @JsonTypeInfo( use = JsonTypeInfo.Id.NAME, @@ -34,8 +40,13 @@ import java.util.Map; property = "type" ) @JsonSubTypes({ - @JsonSubTypes.Type(value = SimpleCalculatedFieldConfiguration.class, name = "SIMPLE"), - @JsonSubTypes.Type(value = ScriptCalculatedFieldConfiguration.class, name = "SCRIPT") + @Type(value = SimpleCalculatedFieldConfiguration.class, name = "SIMPLE"), + @Type(value = ScriptCalculatedFieldConfiguration.class, name = "SCRIPT"), + @Type(value = GeofencingCalculatedFieldConfiguration.class, name = "GEOFENCING"), + @Type(value = AlarmCalculatedFieldConfiguration.class, name = "ALARM"), + @Type(value = PropagationCalculatedFieldConfiguration.class, name = "PROPAGATION"), + @Type(value = RelatedEntitiesAggregationCalculatedFieldConfiguration.class, name = "RELATED_ENTITIES_AGGREGATION"), + @Type(value = EntityAggregationCalculatedFieldConfiguration.class, name = "ENTITY_AGGREGATION") }) @JsonIgnoreProperties(ignoreUnknown = true) public interface CalculatedFieldConfiguration { @@ -43,19 +54,29 @@ public interface CalculatedFieldConfiguration { @JsonIgnore CalculatedFieldType getType(); - Map getArguments(); - - String getExpression(); - - void setExpression(String expression); - Output getOutput(); + default void validate() {} + @JsonIgnore - List getReferencedEntities(); + default Set getReferencedEntities() { + return Collections.emptySet(); + } + + default CalculatedFieldLink buildCalculatedFieldLink(TenantId tenantId, EntityId referencedEntityId, CalculatedFieldId calculatedFieldId) { + return new CalculatedFieldLink(tenantId, referencedEntityId, calculatedFieldId); + } - List buildCalculatedFieldLinks(TenantId tenantId, EntityId cfEntityId, CalculatedFieldId calculatedFieldId); + default List buildCalculatedFieldLinks(TenantId tenantId, EntityId cfEntityId, CalculatedFieldId calculatedFieldId) { + return getReferencedEntities().stream() + .filter(referencedEntity -> !referencedEntity.equals(cfEntityId)) + .map(referencedEntityId -> buildCalculatedFieldLink(tenantId, referencedEntityId, calculatedFieldId)) + .collect(Collectors.toList()); + } - CalculatedFieldLink buildCalculatedFieldLink(TenantId tenantId, EntityId referencedEntityId, CalculatedFieldId calculatedFieldId); + @JsonIgnore + default boolean requiresScheduledReevaluation() { + return false; + } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/CfArgumentDynamicSourceConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/CfArgumentDynamicSourceConfiguration.java new file mode 100644 index 0000000000..6a0f0c25ca --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/CfArgumentDynamicSourceConfiguration.java @@ -0,0 +1,40 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.cf.configuration; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; + +@JsonTypeInfo( + use = JsonTypeInfo.Id.NAME, + include = JsonTypeInfo.As.PROPERTY, + property = "type" +) +@JsonSubTypes({ + @JsonSubTypes.Type(value = RelationPathQueryDynamicSourceConfiguration.class, name = "RELATION_PATH_QUERY"), + @JsonSubTypes.Type(value = CurrentOwnerDynamicSourceConfiguration.class, name = "CURRENT_OWNER") +}) +@JsonIgnoreProperties(ignoreUnknown = true) +public interface CfArgumentDynamicSourceConfiguration { + + @JsonIgnore + CFArgumentDynamicSourceType getType(); + + default void validate() {} + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/CurrentOwnerDynamicSourceConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/CurrentOwnerDynamicSourceConfiguration.java new file mode 100644 index 0000000000..be9a519f1f --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/CurrentOwnerDynamicSourceConfiguration.java @@ -0,0 +1,28 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.cf.configuration; + +import lombok.Data; + +@Data +public class CurrentOwnerDynamicSourceConfiguration implements CfArgumentDynamicSourceConfiguration { + + @Override + public CFArgumentDynamicSourceType getType() { + return CFArgumentDynamicSourceType.CURRENT_OWNER; + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/ExpressionBasedCalculatedFieldConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/ExpressionBasedCalculatedFieldConfiguration.java new file mode 100644 index 0000000000..17e6a0ba80 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/ExpressionBasedCalculatedFieldConfiguration.java @@ -0,0 +1,24 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.cf.configuration; + +public interface ExpressionBasedCalculatedFieldConfiguration extends ArgumentsBasedCalculatedFieldConfiguration { + + String getExpression(); + + void setExpression(String expression); + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/PropagationCalculatedFieldConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/PropagationCalculatedFieldConfiguration.java new file mode 100644 index 0000000000..61d4542eb9 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/PropagationCalculatedFieldConfiguration.java @@ -0,0 +1,94 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.cf.configuration; + +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.thingsboard.server.common.data.StringUtils; +import org.thingsboard.server.common.data.cf.CalculatedFieldType; +import org.thingsboard.server.common.data.relation.RelationPathLevel; + +import java.util.List; + +@Data +@EqualsAndHashCode(callSuper = true) +public class PropagationCalculatedFieldConfiguration extends BaseCalculatedFieldConfiguration { + + public static final String PROPAGATION_CONFIG_ARGUMENT = "propagationCtx"; + + @Valid + @NotNull + private RelationPathLevel relation; + + private boolean applyExpressionToResolvedArguments; + + @Override + public CalculatedFieldType getType() { + return CalculatedFieldType.PROPAGATION; + } + + @Override + public void validate() { + baseCalculatedFieldRestriction(); + propagationRestriction(); + if (!applyExpressionToResolvedArguments) { + arguments.forEach((name, argument) -> { + if (!currentEntitySource(argument)) { + throw new IllegalArgumentException("Arguments in 'Arguments only' propagation mode support only the 'Current entity' source entity type!"); + } + if (argument.getRefEntityKey() == null) { + throw new IllegalArgumentException("Argument: '" + name + "' doesn't have reference entity key configured!"); + } + if (argument.getRefEntityKey().getType() == ArgumentType.TS_ROLLING) { + throw new IllegalArgumentException("Argument type: 'Time series rolling' detected for argument: '" + name + "'. " + + "Only 'Attribute' or 'Latest telemetry' arguments are allowed for 'Arguments only' propagation mode!"); + } + }); + } else { + boolean noneMatchCurrentEntitySource = arguments.entrySet() + .stream() + .noneMatch(entry -> currentEntitySource(entry.getValue())); + if (noneMatchCurrentEntitySource) { + throw new IllegalArgumentException("At least one argument must be configured with the 'Current entity' " + + "source entity type for 'Expression result' propagation mode!"); + } + if (StringUtils.isBlank(expression)) { + throw new IllegalArgumentException("Expression must be specified for 'Expression result' propagation mode!"); + } + } + } + + public Argument toPropagationArgument() { + var refDynamicSourceConfiguration = new RelationPathQueryDynamicSourceConfiguration(); + refDynamicSourceConfiguration.setLevels(List.of(relation)); + var propagationArgument = new Argument(); + propagationArgument.setRefDynamicSourceConfiguration(refDynamicSourceConfiguration); + return propagationArgument; + } + + private void propagationRestriction() { + if (arguments.entrySet().stream().anyMatch(entry -> entry.getKey().equals(PROPAGATION_CONFIG_ARGUMENT))) { + throw new IllegalArgumentException("Argument name '" + PROPAGATION_CONFIG_ARGUMENT + "' is reserved and cannot be used."); + } + } + + private boolean currentEntitySource(Argument argument) { + return argument.getRefEntityId() == null && argument.getRefDynamicSourceConfiguration() == null; + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/RelationPathQueryDynamicSourceConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/RelationPathQueryDynamicSourceConfiguration.java new file mode 100644 index 0000000000..dc92ff3685 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/RelationPathQueryDynamicSourceConfiguration.java @@ -0,0 +1,71 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.cf.configuration; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import lombok.Data; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.relation.EntityRelation; +import org.thingsboard.server.common.data.relation.EntityRelationPathQuery; +import org.thingsboard.server.common.data.relation.EntitySearchDirection; +import org.thingsboard.server.common.data.relation.RelationPathLevel; +import org.thingsboard.server.common.data.util.CollectionsUtil; + +import java.util.List; +import java.util.NoSuchElementException; + +@Data +public class RelationPathQueryDynamicSourceConfiguration implements CfArgumentDynamicSourceConfiguration { + + private List levels; + + @Override + public CFArgumentDynamicSourceType getType() { + return CFArgumentDynamicSourceType.RELATION_PATH_QUERY; + } + + @Override + public void validate() { + if (CollectionsUtil.isEmpty(levels)) { + throw new IllegalArgumentException("At least one relation level must be specified!"); + } + levels.forEach(RelationPathLevel::validate); + } + + public List resolveEntityIds(List relations) { + EntitySearchDirection lastLevelDirection = getLastLevel().direction(); + return switch (lastLevelDirection) { + case FROM -> relations.stream().map(EntityRelation::getTo).toList(); + case TO -> relations.stream().map(EntityRelation::getFrom).toList(); + }; + } + + public void validateMaxRelationLevel(String argumentName, int maxAllowedRelationLevel) { + if (levels.size() > maxAllowedRelationLevel) { + throw new IllegalArgumentException("Max relation level is greater than configured " + + "maximum allowed relation level in tenant profile: " + maxAllowedRelationLevel + " for argument: " + argumentName); + } + } + + public EntityRelationPathQuery toRelationPathQuery(EntityId entityId) { + return new EntityRelationPathQuery(entityId, levels); + } + + private RelationPathLevel getLastLevel() { + return levels.get(levels.size() - 1); + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/ScheduledUpdateSupportedCalculatedFieldConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/ScheduledUpdateSupportedCalculatedFieldConfiguration.java new file mode 100644 index 0000000000..e1e8ca1a9b --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/ScheduledUpdateSupportedCalculatedFieldConfiguration.java @@ -0,0 +1,35 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.cf.configuration; + +import jakarta.validation.constraints.PositiveOrZero; + +public interface ScheduledUpdateSupportedCalculatedFieldConfiguration extends CalculatedFieldConfiguration { + + boolean isScheduledUpdateEnabled(); + + @PositiveOrZero + int getScheduledUpdateInterval(); + + void setScheduledUpdateInterval(int interval); + + default void validate(long minAllowedScheduledUpdateInterval) { + if (getScheduledUpdateInterval() < minAllowedScheduledUpdateInterval) { + throw new IllegalArgumentException("Scheduled update interval is less than configured " + + "minimum allowed interval in tenant profile: " + minAllowedScheduledUpdateInterval); + } + } +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/ScriptCalculatedFieldConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/ScriptCalculatedFieldConfiguration.java index c2dde43b8e..0d38059a45 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/ScriptCalculatedFieldConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/ScriptCalculatedFieldConfiguration.java @@ -21,7 +21,7 @@ import org.thingsboard.server.common.data.cf.CalculatedFieldType; @Data @EqualsAndHashCode(callSuper = true) -public class ScriptCalculatedFieldConfiguration extends BaseCalculatedFieldConfiguration implements CalculatedFieldConfiguration { +public class ScriptCalculatedFieldConfiguration extends BaseCalculatedFieldConfiguration implements ArgumentsBasedCalculatedFieldConfiguration { @Override public CalculatedFieldType getType() { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/SimpleCalculatedFieldConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/SimpleCalculatedFieldConfiguration.java index 86c7b9e9b6..471bac3653 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/SimpleCalculatedFieldConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/SimpleCalculatedFieldConfiguration.java @@ -21,7 +21,7 @@ import org.thingsboard.server.common.data.cf.CalculatedFieldType; @Data @EqualsAndHashCode(callSuper = true) -public class SimpleCalculatedFieldConfiguration extends BaseCalculatedFieldConfiguration implements CalculatedFieldConfiguration { +public class SimpleCalculatedFieldConfiguration extends BaseCalculatedFieldConfiguration implements ExpressionBasedCalculatedFieldConfiguration { private boolean useLatestTs; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/AggFunction.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/AggFunction.java new file mode 100644 index 0000000000..cd0e3f66d4 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/AggFunction.java @@ -0,0 +1,20 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.cf.configuration.aggregation; + +public enum AggFunction { + MIN, MAX, SUM, AVG, COUNT, COUNT_UNIQUE +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/AggFunctionInput.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/AggFunctionInput.java new file mode 100644 index 0000000000..f6df80952e --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/AggFunctionInput.java @@ -0,0 +1,34 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.cf.configuration.aggregation; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class AggFunctionInput implements AggInput { + + private String function; + + @Override + public String getType() { + return "function"; + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/AggInput.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/AggInput.java new file mode 100644 index 0000000000..06929de81c --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/AggInput.java @@ -0,0 +1,38 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.cf.configuration.aggregation; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; + +@JsonTypeInfo( + use = JsonTypeInfo.Id.NAME, + include = JsonTypeInfo.As.PROPERTY, + property = "type" +) +@JsonSubTypes({ + @JsonSubTypes.Type(value = AggKeyInput.class, name = "key"), + @JsonSubTypes.Type(value = AggFunctionInput.class, name = "function") +}) +@JsonIgnoreProperties(ignoreUnknown = true) +public interface AggInput { + + @JsonIgnore + String getType(); + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/AggKeyInput.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/AggKeyInput.java new file mode 100644 index 0000000000..1a00a18a9d --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/AggKeyInput.java @@ -0,0 +1,34 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.cf.configuration.aggregation; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class AggKeyInput implements AggInput { + + private String key; + + @Override + public String getType() { + return "key"; + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/AggMetric.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/AggMetric.java new file mode 100644 index 0000000000..355ca2c72d --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/AggMetric.java @@ -0,0 +1,32 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.cf.configuration.aggregation; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import lombok.Data; + +@Data +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public class AggMetric { + + private AggFunction function; + private String filter; + private AggInput input; + private Long defaultValue; + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/RelatedEntitiesAggregationCalculatedFieldConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/RelatedEntitiesAggregationCalculatedFieldConfiguration.java new file mode 100644 index 0000000000..cf7040c4bb --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/RelatedEntitiesAggregationCalculatedFieldConfiguration.java @@ -0,0 +1,67 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.cf.configuration.aggregation; + +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import org.thingsboard.server.common.data.cf.CalculatedFieldType; +import org.thingsboard.server.common.data.cf.configuration.Argument; +import org.thingsboard.server.common.data.cf.configuration.ArgumentsBasedCalculatedFieldConfiguration; +import org.thingsboard.server.common.data.cf.configuration.Output; +import org.thingsboard.server.common.data.cf.configuration.ScheduledUpdateSupportedCalculatedFieldConfiguration; +import org.thingsboard.server.common.data.relation.RelationPathLevel; + +import java.util.Map; + +@Data +public class RelatedEntitiesAggregationCalculatedFieldConfiguration implements ArgumentsBasedCalculatedFieldConfiguration, ScheduledUpdateSupportedCalculatedFieldConfiguration { + + @NotNull + private RelationPathLevel relation; + private Map arguments; + private long deduplicationIntervalInSec; + @Valid + @NotEmpty + private Map metrics; + private Output output; + private boolean useLatestTs; + + private int scheduledUpdateInterval; + + @Override + public CalculatedFieldType getType() { + return CalculatedFieldType.RELATED_ENTITIES_AGGREGATION; + } + + @Override + public boolean isScheduledUpdateEnabled() { + return true; + } + + @Override + public void validate() { + relation.validate(); + if (arguments.containsKey("ctx")) { + throw new IllegalArgumentException("Argument name 'ctx' is reserved and cannot be used."); + } + if (arguments.values().stream().anyMatch(Argument::hasTsRollingArgument)) { + throw new IllegalArgumentException("Calculated field with type: '" + getType() + "' doesn't support TS_ROLLING arguments."); + } + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/single/EntityAggregationCalculatedFieldConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/single/EntityAggregationCalculatedFieldConfiguration.java new file mode 100644 index 0000000000..f6095d41a7 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/single/EntityAggregationCalculatedFieldConfiguration.java @@ -0,0 +1,96 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.cf.configuration.aggregation.single; + +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import org.thingsboard.server.common.data.cf.CalculatedFieldType; +import org.thingsboard.server.common.data.cf.configuration.Argument; +import org.thingsboard.server.common.data.cf.configuration.ArgumentType; +import org.thingsboard.server.common.data.cf.configuration.ArgumentsBasedCalculatedFieldConfiguration; +import org.thingsboard.server.common.data.cf.configuration.Output; +import org.thingsboard.server.common.data.cf.configuration.aggregation.AggKeyInput; +import org.thingsboard.server.common.data.cf.configuration.aggregation.AggMetric; +import org.thingsboard.server.common.data.cf.configuration.aggregation.single.interval.AggInterval; +import org.thingsboard.server.common.data.cf.configuration.aggregation.single.interval.Watermark; + +import java.util.Map; + +@Data +public class EntityAggregationCalculatedFieldConfiguration implements ArgumentsBasedCalculatedFieldConfiguration { + + private Map arguments; + @Valid + @NotEmpty + private Map metrics; + @Valid + @NotNull + private AggInterval interval; + @Valid + private Watermark watermark; + @Valid + @NotNull + private Output output; + + @Override + public CalculatedFieldType getType() { + return CalculatedFieldType.ENTITY_AGGREGATION; + } + + @Override + public void validate() { + validateArguments(); + validateMetrics(); + validateInterval(); + } + + private void validateArguments() { + if (arguments.containsKey("ctx")) { + throw new IllegalArgumentException("Argument name 'ctx' is reserved and cannot be used."); + } + if (arguments.values().stream().anyMatch(argument -> !ArgumentType.TS_LATEST.equals(argument.getRefEntityKey().getType()))) { + throw new IllegalArgumentException("Calculated field with type: '" + getType() + "' support only TS_LATEST arguments."); + } + } + + private void validateMetrics() { + if (metrics == null || metrics.isEmpty()) { + throw new IllegalArgumentException("Metrics map cannot be empty."); + } + + for (AggMetric metric : metrics.values()) { + if (metric.getInput() instanceof AggKeyInput aggKeyInput) { + if (!arguments.containsKey(aggKeyInput.getKey())) { + throw new IllegalArgumentException( + "Metric references unknown argument: '" + aggKeyInput.getKey() + "'." + ); + } + } else { + throw new IllegalArgumentException("Metric key can only refer to argument."); + } + } + } + + private void validateInterval() { + if (interval == null) { + throw new IllegalArgumentException("Interval must be defined."); + } + interval.validate(); + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/single/interval/AggInterval.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/single/interval/AggInterval.java new file mode 100644 index 0000000000..3d38ebc1f6 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/single/interval/AggInterval.java @@ -0,0 +1,67 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.cf.configuration.aggregation.single.interval; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; + +import java.time.ZoneId; +import java.time.ZonedDateTime; + +@JsonTypeInfo( + use = JsonTypeInfo.Id.NAME, + include = JsonTypeInfo.As.PROPERTY, + property = "type" +) +@JsonSubTypes({ + @JsonSubTypes.Type(value = HourInterval.class, name = "HOUR"), + @JsonSubTypes.Type(value = DayInterval.class, name = "DAY"), + @JsonSubTypes.Type(value = WeekInterval.class, name = "WEEK"), + @JsonSubTypes.Type(value = WeekSunSatInterval.class, name = "WEEK_SUN_SAT"), + @JsonSubTypes.Type(value = MonthInterval.class, name = "MONTH"), + @JsonSubTypes.Type(value = QuarterInterval.class, name = "QUARTER"), + @JsonSubTypes.Type(value = YearInterval.class, name = "YEAR"), + @JsonSubTypes.Type(value = CustomInterval.class, name = "CUSTOM") +}) +@JsonIgnoreProperties(ignoreUnknown = true) +public interface AggInterval { + + @JsonIgnore + AggIntervalType getType(); + + @JsonIgnore + ZoneId getZoneId(); + + @JsonIgnore + long getCurrentIntervalDurationMillis(); + + @JsonIgnore + long getCurrentIntervalStartTs(); + + long getDateTimeIntervalStartTs(ZonedDateTime dateTime); + + @JsonIgnore + long getCurrentIntervalEndTs(); + + long getDateTimeIntervalEndTs(ZonedDateTime dateTime); + + ZonedDateTime getNextIntervalStart(ZonedDateTime currentStart); + + void validate(); + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/single/interval/AggIntervalType.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/single/interval/AggIntervalType.java new file mode 100644 index 0000000000..1d39f14a03 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/single/interval/AggIntervalType.java @@ -0,0 +1,29 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.cf.configuration.aggregation.single.interval; + +public enum AggIntervalType { + + HOUR, + DAY, + WEEK, + WEEK_SUN_SAT, + MONTH, + QUARTER, + YEAR, + CUSTOM + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/single/interval/BaseAggInterval.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/single/interval/BaseAggInterval.java new file mode 100644 index 0000000000..0c0801dea9 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/single/interval/BaseAggInterval.java @@ -0,0 +1,108 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.cf.configuration.aggregation.single.interval; + +import com.fasterxml.jackson.annotation.JsonInclude; +import jakarta.validation.constraints.NotBlank; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.util.concurrent.TimeUnit; + +@Data +@JsonInclude(JsonInclude.Include.NON_NULL) +@AllArgsConstructor +@NoArgsConstructor +public abstract class BaseAggInterval implements AggInterval { + + @NotBlank + protected String tz; + protected Long offsetSec; // delay seconds since start of interval + + @Override + public ZoneId getZoneId() { + return ZoneId.of(tz); + } + + protected long getOffsetSafe() { + return offsetSec != null ? offsetSec : 0L; + } + + @Override + public long getCurrentIntervalDurationMillis() { + return getCurrentIntervalEndTs() - getCurrentIntervalStartTs(); + } + + @Override + public long getCurrentIntervalStartTs() { + ZoneId zoneId = getZoneId(); + ZonedDateTime now = ZonedDateTime.now(zoneId); + return getDateTimeIntervalStartTs(now); + } + + @Override + public long getDateTimeIntervalStartTs(ZonedDateTime dateTime) { + long offset = getOffsetSafe(); + ZonedDateTime shiftedNow = dateTime.minusSeconds(offset); + ZonedDateTime alignedStart = getAlignedBoundary(shiftedNow, false); + ZonedDateTime actualStart = alignedStart.plusSeconds(offset); + return actualStart.toInstant().toEpochMilli(); + } + + @Override + public long getCurrentIntervalEndTs() { + ZoneId zoneId = getZoneId(); + ZonedDateTime now = ZonedDateTime.now(zoneId); + return getDateTimeIntervalEndTs(now); + } + + @Override + public long getDateTimeIntervalEndTs(ZonedDateTime dateTime) { + long offset = getOffsetSafe(); + ZonedDateTime shiftedNow = dateTime.minusSeconds(offset); + ZonedDateTime alignedEnd = getAlignedBoundary(shiftedNow, true); + ZonedDateTime actualEnd = alignedEnd.plusSeconds(offset); + return actualEnd.toInstant().toEpochMilli(); + } + + protected abstract ZonedDateTime alignToIntervalStart(ZonedDateTime reference); + + protected ZonedDateTime getAlignedBoundary(ZonedDateTime reference, boolean next) { + ZonedDateTime base = alignToIntervalStart(reference); + return next ? getNextIntervalStart(base) : base; + } + + @Override + public void validate() { + try { + getZoneId(); + } catch (Exception ex) { + throw new IllegalArgumentException("Invalid timezone in interval: " + ex.getMessage()); + } + if (offsetSec != null) { + if (offsetSec < 0) { + throw new IllegalArgumentException("Offset cannot be negative."); + } + if (TimeUnit.SECONDS.toMillis(offsetSec) >= getCurrentIntervalDurationMillis()) { + throw new IllegalArgumentException("Offset must be greater than interval duration."); + } + } + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/single/interval/CustomInterval.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/single/interval/CustomInterval.java new file mode 100644 index 0000000000..24bfa26d8d --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/single/interval/CustomInterval.java @@ -0,0 +1,68 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.cf.configuration.aggregation.single.interval; + +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +import java.time.Duration; +import java.time.ZonedDateTime; + +@EqualsAndHashCode(callSuper = true) +@Data +@NoArgsConstructor +public class CustomInterval extends BaseAggInterval { + + @NotNull + @Min(1) + private Long durationSec; + + public CustomInterval(String tz, Long offsetSec, Long durationSec) { + super(tz, offsetSec); + this.durationSec = durationSec; + } + + @Override + public AggIntervalType getType() { + return AggIntervalType.CUSTOM; + } + + @Override + public long getCurrentIntervalDurationMillis() { + return getDurationMillis(); + } + + private long getDurationMillis() { + return Duration.ofSeconds(durationSec).toMillis(); + } + + @Override + protected ZonedDateTime alignToIntervalStart(ZonedDateTime reference) { + ZonedDateTime localMidnight = reference.toLocalDate().atStartOfDay(reference.getZone()); + long secondsFromMidnight = Duration.between(localMidnight, reference).getSeconds(); + long alignedSecondsFromMidnight = (secondsFromMidnight / durationSec) * durationSec; + return localMidnight.plusSeconds(alignedSecondsFromMidnight); + } + + @Override + public ZonedDateTime getNextIntervalStart(ZonedDateTime currentStart) { + return currentStart.plusSeconds(durationSec); + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/single/interval/DayInterval.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/single/interval/DayInterval.java new file mode 100644 index 0000000000..2ad0ce24d3 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/single/interval/DayInterval.java @@ -0,0 +1,47 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.cf.configuration.aggregation.single.interval; + +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.ZonedDateTime; +import java.time.temporal.ChronoUnit; + +@Data +@NoArgsConstructor +public class DayInterval extends BaseAggInterval { + + @Override + public AggIntervalType getType() { + return AggIntervalType.DAY; + } + + public DayInterval(String tz, Long offsetSec) { + super(tz, offsetSec); + } + + @Override + protected ZonedDateTime alignToIntervalStart(ZonedDateTime reference) { + return reference.truncatedTo(ChronoUnit.DAYS); + } + + @Override + public ZonedDateTime getNextIntervalStart(ZonedDateTime currentStart) { + return currentStart.plusDays(1); + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/single/interval/HourInterval.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/single/interval/HourInterval.java new file mode 100644 index 0000000000..3ce366bc75 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/single/interval/HourInterval.java @@ -0,0 +1,49 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.cf.configuration.aggregation.single.interval; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +import java.time.ZonedDateTime; +import java.time.temporal.ChronoUnit; + +@EqualsAndHashCode(callSuper = true) +@Data +@NoArgsConstructor +public class HourInterval extends BaseAggInterval { + + public HourInterval(String tz, Long offsetSec) { + super(tz, offsetSec); + } + + @Override + public AggIntervalType getType() { + return AggIntervalType.HOUR; + } + + @Override + protected ZonedDateTime alignToIntervalStart(ZonedDateTime reference) { + return reference.truncatedTo(ChronoUnit.HOURS); + } + + @Override + public ZonedDateTime getNextIntervalStart(ZonedDateTime currentStart) { + return currentStart.plusHours(1); + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/single/interval/MonthInterval.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/single/interval/MonthInterval.java new file mode 100644 index 0000000000..3ce121459b --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/single/interval/MonthInterval.java @@ -0,0 +1,47 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.cf.configuration.aggregation.single.interval; + +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.ZonedDateTime; +import java.time.temporal.ChronoUnit; + +@Data +@NoArgsConstructor +public class MonthInterval extends BaseAggInterval { + + @Override + public AggIntervalType getType() { + return AggIntervalType.MONTH; + } + + public MonthInterval(String tz, Long offsetSec) { + super(tz, offsetSec); + } + + @Override + protected ZonedDateTime alignToIntervalStart(ZonedDateTime reference) { + return reference.withDayOfMonth(1).truncatedTo(ChronoUnit.DAYS); + } + + @Override + public ZonedDateTime getNextIntervalStart(ZonedDateTime currentStart) { + return currentStart.plusMonths(1); + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/single/interval/QuarterInterval.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/single/interval/QuarterInterval.java new file mode 100644 index 0000000000..96fcfc0dc8 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/single/interval/QuarterInterval.java @@ -0,0 +1,53 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.cf.configuration.aggregation.single.interval; + +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDate; +import java.time.LocalTime; +import java.time.ZonedDateTime; + +@Data +@NoArgsConstructor +public class QuarterInterval extends BaseAggInterval { + + @Override + public AggIntervalType getType() { + return AggIntervalType.QUARTER; + } + + public QuarterInterval(String tz, Long offsetSec) { + super(tz, offsetSec); + } + + @Override + protected ZonedDateTime alignToIntervalStart(ZonedDateTime reference) { + int month = reference.getMonthValue(); + int quarterStartMonth = ((month - 1) / 3) * 3 + 1; // 1, 4, 7, 10 + return ZonedDateTime.of( + LocalDate.of(reference.getYear(), quarterStartMonth, 1), + LocalTime.MIDNIGHT, + reference.getZone()); + } + + @Override + public ZonedDateTime getNextIntervalStart(ZonedDateTime currentStart) { + return currentStart.plusMonths(3); + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/single/interval/Watermark.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/single/interval/Watermark.java new file mode 100644 index 0000000000..b07e6a3012 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/single/interval/Watermark.java @@ -0,0 +1,31 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.cf.configuration.aggregation.single.interval; + +import jakarta.validation.constraints.Min; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class Watermark { + + @Min(0) + private long duration; + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/single/interval/WeekInterval.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/single/interval/WeekInterval.java new file mode 100644 index 0000000000..0f70adc5ad --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/single/interval/WeekInterval.java @@ -0,0 +1,49 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.cf.configuration.aggregation.single.interval; + +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.DayOfWeek; +import java.time.ZonedDateTime; +import java.time.temporal.ChronoUnit; +import java.time.temporal.TemporalAdjusters; + +@Data +@NoArgsConstructor +public class WeekInterval extends BaseAggInterval { + + @Override + public AggIntervalType getType() { + return AggIntervalType.WEEK; + } + + public WeekInterval(String tz, Long offsetSec) { + super(tz, offsetSec); + } + + @Override + protected ZonedDateTime alignToIntervalStart(ZonedDateTime reference) { + return reference.with(TemporalAdjusters.previousOrSame(DayOfWeek.MONDAY)).truncatedTo(ChronoUnit.DAYS); + } + + @Override + public ZonedDateTime getNextIntervalStart(ZonedDateTime currentStart) { + return currentStart.plusWeeks(1); + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/single/interval/WeekSunSatInterval.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/single/interval/WeekSunSatInterval.java new file mode 100644 index 0000000000..2c4482f0b4 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/single/interval/WeekSunSatInterval.java @@ -0,0 +1,49 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.cf.configuration.aggregation.single.interval; + +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.DayOfWeek; +import java.time.ZonedDateTime; +import java.time.temporal.ChronoUnit; +import java.time.temporal.TemporalAdjusters; + +@Data +@NoArgsConstructor +public class WeekSunSatInterval extends BaseAggInterval { + + @Override + public AggIntervalType getType() { + return AggIntervalType.WEEK_SUN_SAT; + } + + public WeekSunSatInterval(String tz, Long offsetSec) { + super(tz, offsetSec); + } + + @Override + protected ZonedDateTime alignToIntervalStart(ZonedDateTime reference) { + return reference.with(TemporalAdjusters.previousOrSame(DayOfWeek.SUNDAY)).truncatedTo(ChronoUnit.DAYS); + } + + @Override + public ZonedDateTime getNextIntervalStart(ZonedDateTime currentStart) { + return currentStart.plusWeeks(1); + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/single/interval/YearInterval.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/single/interval/YearInterval.java new file mode 100644 index 0000000000..441f3913a9 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/single/interval/YearInterval.java @@ -0,0 +1,51 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.cf.configuration.aggregation.single.interval; + +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDate; +import java.time.LocalTime; +import java.time.ZonedDateTime; + +@Data +@NoArgsConstructor +public class YearInterval extends BaseAggInterval { + + @Override + public AggIntervalType getType() { + return AggIntervalType.YEAR; + } + + public YearInterval(String tz, Long offsetSec) { + super(tz, offsetSec); + } + + @Override + protected ZonedDateTime alignToIntervalStart(ZonedDateTime reference) { + return ZonedDateTime.of( + LocalDate.of(reference.getYear(), 1, 1), + LocalTime.MIDNIGHT, + reference.getZone()); + } + + @Override + public ZonedDateTime getNextIntervalStart(ZonedDateTime currentStart) { + return currentStart.plusYears(1); + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/geofencing/EntityCoordinates.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/geofencing/EntityCoordinates.java new file mode 100644 index 0000000000..ad31293061 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/geofencing/EntityCoordinates.java @@ -0,0 +1,50 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.cf.configuration.geofencing; + + +import jakarta.validation.constraints.NotBlank; +import lombok.Data; +import org.thingsboard.server.common.data.cf.configuration.Argument; +import org.thingsboard.server.common.data.cf.configuration.ArgumentType; +import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey; + +import java.util.Map; + +@Data +public class EntityCoordinates { + + public static final String ENTITY_ID_LATITUDE_ARGUMENT_KEY = "latitude"; + public static final String ENTITY_ID_LONGITUDE_ARGUMENT_KEY = "longitude"; + + @NotBlank + private final String latitudeKeyName; + @NotBlank + private final String longitudeKeyName; + + public Map toArguments() { + return Map.of( + ENTITY_ID_LATITUDE_ARGUMENT_KEY, toArgument(latitudeKeyName), + ENTITY_ID_LONGITUDE_ARGUMENT_KEY, toArgument(longitudeKeyName) + ); + } + + private Argument toArgument(String keyName) { + var argument = new Argument(); + argument.setRefEntityKey(new ReferencedEntityKey(keyName, ArgumentType.TS_LATEST, null)); + return argument; + } +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/geofencing/GeofencingCalculatedFieldConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/geofencing/GeofencingCalculatedFieldConfiguration.java new file mode 100644 index 0000000000..acefd5ff26 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/geofencing/GeofencingCalculatedFieldConfiguration.java @@ -0,0 +1,85 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.cf.configuration.geofencing; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import org.thingsboard.server.common.data.cf.CalculatedFieldType; +import org.thingsboard.server.common.data.cf.configuration.Argument; +import org.thingsboard.server.common.data.cf.configuration.ArgumentsBasedCalculatedFieldConfiguration; +import org.thingsboard.server.common.data.cf.configuration.Output; +import org.thingsboard.server.common.data.cf.configuration.ScheduledUpdateSupportedCalculatedFieldConfiguration; +import org.thingsboard.server.common.data.id.EntityId; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.Set; + +import static java.util.stream.Collectors.toSet; + +@Data +public class GeofencingCalculatedFieldConfiguration implements ArgumentsBasedCalculatedFieldConfiguration, ScheduledUpdateSupportedCalculatedFieldConfiguration { + + @Valid + @NotNull + private EntityCoordinates entityCoordinates; + + @Valid + @NotNull + private Map zoneGroups; + + private boolean scheduledUpdateEnabled; + private int scheduledUpdateInterval; + + private Output output; + + @Override + public CalculatedFieldType getType() { + return CalculatedFieldType.GEOFENCING; + } + + @Override + @JsonIgnore + public Map getArguments() { + Map args = new HashMap<>(entityCoordinates.toArguments()); + zoneGroups.forEach((zgName, zgConfig) -> args.put(zgName, zgConfig.toArgument())); + return args; + } + + + @Override + public Set getReferencedEntities() { + return zoneGroups == null ? Collections.emptySet() : zoneGroups.values().stream() + .map(ZoneGroupConfiguration::getRefEntityId) + .filter(Objects::nonNull) + .collect(toSet()); + } + + @Override + public Output getOutput() { + return output; + } + + @Override + public void validate() { + zoneGroups.forEach((key, value) -> value.validate(key)); + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/geofencing/GeofencingEvent.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/geofencing/GeofencingEvent.java new file mode 100644 index 0000000000..a6ee0cfcd6 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/geofencing/GeofencingEvent.java @@ -0,0 +1,19 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.cf.configuration.geofencing; + +public sealed interface GeofencingEvent + permits GeofencingTransitionEvent, GeofencingPresenceStatus { } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/geofencing/GeofencingPresenceStatus.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/geofencing/GeofencingPresenceStatus.java new file mode 100644 index 0000000000..38977cb650 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/geofencing/GeofencingPresenceStatus.java @@ -0,0 +1,25 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.cf.configuration.geofencing; + +import lombok.Getter; + +@Getter +public enum GeofencingPresenceStatus implements GeofencingEvent { + + INSIDE, OUTSIDE; + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/geofencing/GeofencingReportStrategy.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/geofencing/GeofencingReportStrategy.java new file mode 100644 index 0000000000..a7937bb93c --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/geofencing/GeofencingReportStrategy.java @@ -0,0 +1,24 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.cf.configuration.geofencing; + +public enum GeofencingReportStrategy { + + REPORT_TRANSITION_EVENTS_ONLY, + REPORT_PRESENCE_STATUS_ONLY, + REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/geofencing/GeofencingTransitionEvent.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/geofencing/GeofencingTransitionEvent.java new file mode 100644 index 0000000000..d7cf996fa7 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/geofencing/GeofencingTransitionEvent.java @@ -0,0 +1,20 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.cf.configuration.geofencing; + +public enum GeofencingTransitionEvent implements GeofencingEvent { + ENTERED, LEFT +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/geofencing/ZoneGroupConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/geofencing/ZoneGroupConfiguration.java new file mode 100644 index 0000000000..a06cb242cf --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/geofencing/ZoneGroupConfiguration.java @@ -0,0 +1,98 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.cf.configuration.geofencing; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import org.springframework.lang.Nullable; +import org.thingsboard.server.common.data.AttributeScope; +import org.thingsboard.server.common.data.StringUtils; +import org.thingsboard.server.common.data.cf.configuration.Argument; +import org.thingsboard.server.common.data.cf.configuration.ArgumentType; +import org.thingsboard.server.common.data.cf.configuration.CfArgumentDynamicSourceConfiguration; +import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.relation.EntitySearchDirection; + +@Data +@JsonInclude(JsonInclude.Include.NON_NULL) +public class ZoneGroupConfiguration { + + @Nullable + private EntityId refEntityId; + private CfArgumentDynamicSourceConfiguration refDynamicSourceConfiguration; + + @NotBlank + private final String perimeterKeyName; + + @NotNull + private final GeofencingReportStrategy reportStrategy; + private final boolean createRelationsWithMatchedZones; + + private String relationType; + private EntitySearchDirection direction; + + public void validate(String name) { + if (EntityCoordinates.ENTITY_ID_LATITUDE_ARGUMENT_KEY.equals(name) || EntityCoordinates.ENTITY_ID_LONGITUDE_ARGUMENT_KEY.equals(name)) { + throw new IllegalArgumentException("Name '" + name + "' is reserved and cannot be used for zone group!"); + } + if (refDynamicSourceConfiguration != null) { + refDynamicSourceConfiguration.validate(); + } + if (!createRelationsWithMatchedZones) { + return; + } + if (StringUtils.isBlank(relationType)) { + throw new IllegalArgumentException("Relation type must be specified for '" + name + "' zone group!"); + } + if (direction == null) { + throw new IllegalArgumentException("Relation direction must be specified for '" + name + "' zone group!"); + } + } + + public boolean hasRelationQuerySource() { + return toArgument().hasRelationQuerySource(); + } + + public boolean hasCurrentOwnerSource() { + return toArgument().hasOwnerSource(); + } + + @JsonIgnore + public boolean isCfEntitySource(EntityId cfEntityId) { + if (refEntityId == null && refDynamicSourceConfiguration == null) { + return true; + } + return refEntityId != null && refEntityId.equals(cfEntityId); + } + + @JsonIgnore + public boolean isLinkedCfEntitySource(EntityId cfEntityId) { + return refEntityId != null && !refEntityId.equals(cfEntityId); + } + + public Argument toArgument() { + var argument = new Argument(); + argument.setRefEntityId(refEntityId); + argument.setRefDynamicSourceConfiguration(refDynamicSourceConfiguration); + argument.setRefEntityKey(new ReferencedEntityKey(perimeterKeyName, ArgumentType.ATTRIBUTE, AttributeScope.SERVER_SCOPE)); + return argument; + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/credentials/lwm2m/Lwm2mServerIdentifier.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/credentials/lwm2m/Lwm2mServerIdentifier.java new file mode 100644 index 0000000000..f4f020776b --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/credentials/lwm2m/Lwm2mServerIdentifier.java @@ -0,0 +1,112 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.device.credentials.lwm2m; + +/** + * Enum representing predefined LwM2M Short Server Identifiers. + *

+ * See OMA Lightweight M2M Specification for details about the server identifier space. + */ +public enum Lwm2mServerIdentifier { + + /** + * Not used for identifying an LwM2M Server (0). + */ + NOT_USED_IDENTIFYING_LWM2M_SERVER_MIN(0, "Bootstrap Short Server ID", false), + + /** + * Primary LwM2M Server Short Server ID (1). + * Upper boundary for valid LwM2M Server Identifiers (1–65534). + */ + PRIMARY_LWM2M_SERVER(1, "LwM2M Server Short Server ID", true), + + /** + * Maximum valid LwM2M Server ID (65534). + * Upper boundary for valid LwM2M Server Identifiers (1–65534). + */ + LWM2M_SERVER_MAX(65534, "LwM2M Server Short Server ID", true), + + /** + * Not used for identifying an LwM2M Server (65535). + * Reserved sentinel value representing "no server associated" or "invalid ID". + * MUST NOT be assigned to any LwM2M Server according to OMA-TS-LightweightM2M-Core, §6.2.1. + * OMA LwM2M Core / v1.2: Server / Short Server ID): «MAX_ID 65535 is a reserved value and MUST NOT be used for identifying an Object» + */ + NOT_USED_IDENTIFYING_LWM2M_SERVER_MAX(65535, "Reserved sentinel value (no active server)", false); + + private final Integer id; + private final String description; + private final boolean isLwm2mServer; + + Lwm2mServerIdentifier(Integer id, String description, boolean isLwm2mServer) { + this.id = id; + this.description = description; + this.isLwm2mServer = isLwm2mServer; + } + + /** + * @return the integer value of this Short Server ID. + */ + public Integer getId() { + return id; + } + + /** + * @return a human-readable description of this Server ID. + */ + public String getDescription() { + return description; + } + + /** + * @return true if this ID represents a Lwm2m Server. + */ + public boolean isLwm2mServer() { + return isLwm2mServer; + } + + /** + * Checks whether a given ID represents a valid LwM2M Server (1–65534). + * @param id Short Server ID value. + * @return true if the ID belongs to a standard LwM2M Server. + */ + public static boolean isLwm2mServer(Integer id) { + return id != null && id >= PRIMARY_LWM2M_SERVER.id && id <= LWM2M_SERVER_MAX.id; + } + public static boolean isNotLwm2mServer(Integer id) { + return id == null || id < PRIMARY_LWM2M_SERVER.id || id > LWM2M_SERVER_MAX.id; + } + + /** + * Returns a {@link Lwm2mServerIdentifier} instance matching the given ID. + * @param id numeric ID. + * @return corresponding enum constant. + * @throws IllegalArgumentException if no constant matches the given ID. + */ + public static Lwm2mServerIdentifier fromId(Integer id) { + for (Lwm2mServerIdentifier s : values()) { + if (s.id == id) { + return s; + } + } + throw new IllegalArgumentException("Unknown Lwm2mServerIdentifier: " + id); + } + + @Override + public String toString() { + return name() + "(" + id + ") - " + description; + } +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmCondition.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmCondition.java index 07c42eb31b..f840886834 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmCondition.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmCondition.java @@ -26,6 +26,7 @@ import java.util.List; @Schema @Data @JsonIgnoreProperties(ignoreUnknown = true) +@Deprecated public class AlarmCondition implements Serializable { @Valid diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmConditionFilter.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmConditionFilter.java index 210193fc01..6e7e6ab321 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmConditionFilter.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmConditionFilter.java @@ -26,6 +26,7 @@ import java.io.Serializable; @Schema @Data +@Deprecated public class AlarmConditionFilter implements Serializable { @Valid diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmConditionFilterKey.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmConditionFilterKey.java index d31e6710ef..3c6e5252a0 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmConditionFilterKey.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmConditionFilterKey.java @@ -23,6 +23,7 @@ import java.io.Serializable; @Schema @Data +@Deprecated public class AlarmConditionFilterKey implements Serializable { @Schema(description = "The key type", example = "TIME_SERIES") diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmConditionKeyType.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmConditionKeyType.java index 9eef80e312..6f451a1abc 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmConditionKeyType.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmConditionKeyType.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.common.data.device.profile; +@Deprecated public enum AlarmConditionKeyType { ATTRIBUTE, TIME_SERIES, diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmConditionSpec.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmConditionSpec.java index 37b2a9d7c5..f3f969f641 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmConditionSpec.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmConditionSpec.java @@ -31,6 +31,7 @@ import java.io.Serializable; @JsonSubTypes.Type(value = SimpleAlarmConditionSpec.class, name = "SIMPLE"), @JsonSubTypes.Type(value = DurationAlarmConditionSpec.class, name = "DURATION"), @JsonSubTypes.Type(value = RepeatingAlarmConditionSpec.class, name = "REPEATING")}) +@Deprecated public interface AlarmConditionSpec extends Serializable { @JsonIgnore diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmConditionSpecType.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmConditionSpecType.java index adef445914..229be24b42 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmConditionSpecType.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmConditionSpecType.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.common.data.device.profile; +@Deprecated public enum AlarmConditionSpecType { SIMPLE, diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmRule.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmRule.java index 16850e3669..633cde766e 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmRule.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmRule.java @@ -21,12 +21,17 @@ import lombok.Data; import org.thingsboard.server.common.data.id.DashboardId; import org.thingsboard.server.common.data.validation.NoXss; +import java.io.Serial; import java.io.Serializable; @Schema @Data +@Deprecated public class AlarmRule implements Serializable { + @Serial + private static final long serialVersionUID = -7617427132423304707L; + @Valid @Schema(description = "JSON object representing the alarm rule condition") private AlarmCondition condition; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmSchedule.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmSchedule.java index 09e8d3c146..4bfa2ef9f6 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmSchedule.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmSchedule.java @@ -31,6 +31,7 @@ import java.io.Serializable; @JsonSubTypes.Type(value = AnyTimeSchedule.class, name = "ANY_TIME"), @JsonSubTypes.Type(value = SpecificTimeSchedule.class, name = "SPECIFIC_TIME"), @JsonSubTypes.Type(value = CustomTimeSchedule.class, name = "CUSTOM")}) +@Deprecated public interface AlarmSchedule extends Serializable { AlarmScheduleType getType(); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmScheduleType.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmScheduleType.java index f50a3b47db..ab06cb9335 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmScheduleType.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmScheduleType.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.common.data.device.profile; +@Deprecated public enum AlarmScheduleType { ANY_TIME, diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AnyTimeSchedule.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AnyTimeSchedule.java index 426430481a..87766d685c 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AnyTimeSchedule.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AnyTimeSchedule.java @@ -17,6 +17,7 @@ package org.thingsboard.server.common.data.device.profile; import org.thingsboard.server.common.data.query.DynamicValue; +@Deprecated public class AnyTimeSchedule implements AlarmSchedule { @Override diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/CustomTimeSchedule.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/CustomTimeSchedule.java index b372a2fa07..6b07341b30 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/CustomTimeSchedule.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/CustomTimeSchedule.java @@ -21,6 +21,7 @@ import org.thingsboard.server.common.data.query.DynamicValue; import java.util.List; @Data +@Deprecated public class CustomTimeSchedule implements AlarmSchedule { private String timezone; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/CustomTimeScheduleItem.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/CustomTimeScheduleItem.java index abcbec4e32..b0781e3ad1 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/CustomTimeScheduleItem.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/CustomTimeScheduleItem.java @@ -20,6 +20,7 @@ import lombok.Data; import java.io.Serializable; @Data +@Deprecated public class CustomTimeScheduleItem implements Serializable { private boolean enabled; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DeviceProfileAlarm.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DeviceProfileAlarm.java index fb8488c58e..fcbece4d4b 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DeviceProfileAlarm.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DeviceProfileAlarm.java @@ -28,6 +28,7 @@ import java.util.TreeMap; @Schema @Data +@Deprecated public class DeviceProfileAlarm implements Serializable { @Schema(description = "String value representing the alarm rule id", example = "highTemperatureAlarmID") diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DurationAlarmConditionSpec.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DurationAlarmConditionSpec.java index e114ec1ddc..361ba1e4b3 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DurationAlarmConditionSpec.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DurationAlarmConditionSpec.java @@ -23,6 +23,7 @@ import java.util.concurrent.TimeUnit; @Data @JsonIgnoreProperties(ignoreUnknown = true) +@Deprecated public class DurationAlarmConditionSpec implements AlarmConditionSpec { private TimeUnit unit; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/RepeatingAlarmConditionSpec.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/RepeatingAlarmConditionSpec.java index f9e3fd6d05..75c07dbe02 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/RepeatingAlarmConditionSpec.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/RepeatingAlarmConditionSpec.java @@ -21,6 +21,7 @@ import org.thingsboard.server.common.data.query.FilterPredicateValue; @Data @JsonIgnoreProperties(ignoreUnknown = true) +@Deprecated public class RepeatingAlarmConditionSpec implements AlarmConditionSpec { private FilterPredicateValue predicate; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/SimpleAlarmConditionSpec.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/SimpleAlarmConditionSpec.java index 05c8d0df70..4243946a30 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/SimpleAlarmConditionSpec.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/SimpleAlarmConditionSpec.java @@ -20,6 +20,7 @@ import lombok.Data; @Data @JsonIgnoreProperties(ignoreUnknown = true) +@Deprecated public class SimpleAlarmConditionSpec implements AlarmConditionSpec { @Override public AlarmConditionSpecType getType() { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/SpecificTimeSchedule.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/SpecificTimeSchedule.java index e46d5edbf3..a8b47db1ab 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/SpecificTimeSchedule.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/SpecificTimeSchedule.java @@ -21,6 +21,7 @@ import org.thingsboard.server.common.data.query.DynamicValue; import java.util.Set; @Data +@Deprecated public class SpecificTimeSchedule implements AlarmSchedule { private String timezone; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/lwm2m/TelemetryMappingConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/lwm2m/TelemetryMappingConfiguration.java index 1669e1a101..bdf47a9e9c 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/lwm2m/TelemetryMappingConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/lwm2m/TelemetryMappingConfiguration.java @@ -36,6 +36,7 @@ public class TelemetryMappingConfiguration implements Serializable { private Set attribute; private Set telemetry; private Map attributeLwm2m; + private Boolean initAttrTelAsObsStrategy; private TelemetryObserveStrategy observeStrategy; @JsonCreator @@ -45,6 +46,7 @@ public class TelemetryMappingConfiguration implements Serializable { @JsonProperty("attribute") Set attribute, @JsonProperty("telemetry") Set telemetry, @JsonProperty("attributeLwm2m") Map attributeLwm2m, + @JsonProperty("initAttrTelAsObsStrategy") Boolean initAttrTelAsObsStrategy, @JsonProperty("observeStrategy") TelemetryObserveStrategy observeStrategy) { this.keyName = keyName != null ? keyName : Collections.emptyMap(); @@ -52,6 +54,7 @@ public class TelemetryMappingConfiguration implements Serializable { this.attribute = attribute != null ? attribute : Collections.emptySet(); this.telemetry = telemetry != null ? telemetry : Collections.emptySet(); this.attributeLwm2m = attributeLwm2m != null ? attributeLwm2m : Collections.emptyMap(); + this.initAttrTelAsObsStrategy = initAttrTelAsObsStrategy != null ? initAttrTelAsObsStrategy : false; this.observeStrategy = observeStrategy != null ? observeStrategy : TelemetryObserveStrategy.SINGLE; } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/lwm2m/bootstrap/LwM2MServerSecurityConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/lwm2m/bootstrap/LwM2MServerSecurityConfig.java index 29c130b902..e5eb4be902 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/lwm2m/bootstrap/LwM2MServerSecurityConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/lwm2m/bootstrap/LwM2MServerSecurityConfig.java @@ -26,7 +26,7 @@ public class LwM2MServerSecurityConfig implements Serializable { @Schema(description = "Server short Id. Used as link to associate server Object Instance. This identifier uniquely identifies each LwM2M Server configured for the LwM2M Client. " + "This Resource MUST be set when the Bootstrap-Server Resource has a value of 'false'. " + - "The values ID:1 and ID:65534 values MUST NOT be used for identifying the LwM2M Server.", example = "123", accessMode = Schema.AccessMode.READ_ONLY) + "The values ID:0 and ID:65535 values MUST NOT be used for identifying the LwM2M Server.", example = "123", accessMode = Schema.AccessMode.READ_ONLY) protected Integer shortServerId = 123; /** Security -> ObjectId = 0 'LWM2M Security' */ @Schema(description = "Is Bootstrap Server or Lwm2m Server. " + diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/edge/EdgeEventType.java b/common/data/src/main/java/org/thingsboard/server/common/data/edge/EdgeEventType.java index 0d5c3f34ad..fadb44207a 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/edge/EdgeEventType.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/edge/EdgeEventType.java @@ -47,7 +47,8 @@ public enum EdgeEventType { TB_RESOURCE(true, EntityType.TB_RESOURCE), OAUTH2_CLIENT(true, EntityType.OAUTH2_CLIENT), DOMAIN(true, EntityType.DOMAIN), - CALCULATED_FIELD(false, EntityType.CALCULATED_FIELD); + CALCULATED_FIELD(false, EntityType.CALCULATED_FIELD), + AI_MODEL(true, EntityType.AI_MODEL); private final boolean allEdgesRelated; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/UserFields.java b/common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/UserFields.java index 6f4c7643c0..31e0f4118c 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/UserFields.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/UserFields.java @@ -37,7 +37,7 @@ public class UserFields extends AbstractEntityFields { @Override public String getName() { - return super.getEmail(); + return getEmail(); } public UserFields(UUID id, long createdTime, UUID tenantId, UUID customerId, diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/event/CalculatedFieldDebugEvent.java b/common/data/src/main/java/org/thingsboard/server/common/data/event/CalculatedFieldDebugEvent.java index 0424eabeb6..acc5cf6205 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/event/CalculatedFieldDebugEvent.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/event/CalculatedFieldDebugEvent.java @@ -29,7 +29,7 @@ import org.thingsboard.server.common.data.id.TenantId; import java.util.UUID; -@ToString +@ToString(callSuper = true) @EqualsAndHashCode(callSuper = true) public class CalculatedFieldDebugEvent extends Event { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java index fc1c1cd1a9..3b8959624d 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java @@ -80,7 +80,6 @@ public class EntityIdFactory { case DOMAIN -> new DomainId(uuid); case MOBILE_APP_BUNDLE -> new MobileAppBundleId(uuid); case CALCULATED_FIELD -> new CalculatedFieldId(uuid); - case CALCULATED_FIELD_LINK -> new CalculatedFieldLinkId(uuid); case JOB -> new JobId(uuid); case ADMIN_SETTINGS -> new AdminSettingsId(uuid); case AI_MODEL -> new AiModelId(uuid); @@ -113,6 +112,7 @@ public class EntityIdFactory { case OAUTH2_CLIENT -> new OAuth2ClientId(uuid); case DOMAIN -> new DomainId(uuid); case CALCULATED_FIELD -> new CalculatedFieldId(uuid); + case AI_MODEL -> new AiModelId(uuid); default -> throw new IllegalArgumentException("EdgeEventType " + edgeEventType + " is not supported!"); }; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/job/JobResult.java b/common/data/src/main/java/org/thingsboard/server/common/data/job/JobResult.java index bd2c73d0d8..6f6d924deb 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/job/JobResult.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/job/JobResult.java @@ -55,11 +55,17 @@ public abstract class JobResult implements Serializable { public void processTaskResult(TaskResult taskResult) { if (taskResult.isSuccess()) { - successfulCount++; + if (totalCount == null || successfulCount < totalCount) { + successfulCount++; + } } else if (taskResult.isDiscarded()) { - discardedCount++; + if (totalCount == null || discardedCount < totalCount) { + discardedCount++; + } } else { - failedCount++; + if (totalCount == null || failedCount < totalCount) { + failedCount++; + } if (results.size() < 100) { // preserving only first 100 errors, not reprocessing if there are more failures results.add(taskResult); } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/bundle/MobileAppBundle.java b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/bundle/MobileAppBundle.java index 2190972d7e..b023ba89c3 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/bundle/MobileAppBundle.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/bundle/MobileAppBundle.java @@ -30,6 +30,7 @@ import org.thingsboard.server.common.data.id.MobileAppId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.mobile.layout.MobileLayoutConfig; import org.thingsboard.server.common.data.validation.Length; +import org.thingsboard.server.common.data.validation.NoXss; @EqualsAndHashCode(callSuper = true) @Data @@ -40,9 +41,11 @@ public class MobileAppBundle extends BaseData implements HasT private TenantId tenantId; @Schema(description = "Application bundle title. Cannot be empty", requiredMode = Schema.RequiredMode.REQUIRED) @NotBlank + @NoXss @Length(fieldName = "title") private String title; @Schema(description = "Application bundle description.") + @NoXss @Length(fieldName = "description") private String description; @Schema(description = "Android application id") diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/msg/TbMsgType.java b/common/data/src/main/java/org/thingsboard/server/common/data/msg/TbMsgType.java index f942bc2196..720dc5b790 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/msg/TbMsgType.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/msg/TbMsgType.java @@ -38,7 +38,10 @@ public enum TbMsgType { ENTITY_UNASSIGNED("Entity Unassigned"), ATTRIBUTES_UPDATED("Attributes Updated"), ATTRIBUTES_DELETED("Attributes Deleted"), - ALARM, + ALARM("Alarm"), + ALARM_CREATED("Alarm Created"), + ALARM_UPDATED("Alarm Updated"), + ALARM_SEVERITY_UPDATED("Alarm Severity Updated"), ALARM_ACK("Alarm Acknowledged"), ALARM_CLEAR("Alarm Cleared"), ALARM_DELETE, diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/info/AlarmAssignmentNotificationInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/info/AlarmAssignmentNotificationInfo.java index f29a54d17a..086b326ddd 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/info/AlarmAssignmentNotificationInfo.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/info/AlarmAssignmentNotificationInfo.java @@ -53,6 +53,7 @@ public class AlarmAssignmentNotificationInfo implements RuleOriginatedNotificati private UUID alarmId; private EntityId alarmOriginator; private String alarmOriginatorName; + private String alarmOriginatorLabel; private AlarmSeverity alarmSeverity; private AlarmStatus alarmStatus; private CustomerId alarmCustomerId; @@ -77,7 +78,8 @@ public class AlarmAssignmentNotificationInfo implements RuleOriginatedNotificati "alarmStatus", alarmStatus.toString(), "alarmOriginatorEntityType", alarmOriginator.getEntityType().getNormalName(), "alarmOriginatorId", alarmOriginator.getId().toString(), - "alarmOriginatorName", alarmOriginatorName + "alarmOriginatorName", alarmOriginatorName, + "alarmOriginatorLabel", alarmOriginatorLabel ); } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/info/AlarmCommentNotificationInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/info/AlarmCommentNotificationInfo.java index e46d539399..202812921f 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/info/AlarmCommentNotificationInfo.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/info/AlarmCommentNotificationInfo.java @@ -48,6 +48,7 @@ public class AlarmCommentNotificationInfo implements RuleOriginatedNotificationI private UUID alarmId; private EntityId alarmOriginator; private String alarmOriginatorName; + private String alarmOriginatorLabel; private AlarmSeverity alarmSeverity; private AlarmStatus alarmStatus; private CustomerId alarmCustomerId; @@ -68,7 +69,8 @@ public class AlarmCommentNotificationInfo implements RuleOriginatedNotificationI "alarmStatus", alarmStatus.toString(), "alarmOriginatorEntityType", alarmOriginator.getEntityType().getNormalName(), "alarmOriginatorId", alarmOriginator.getId().toString(), - "alarmOriginatorName", alarmOriginatorName + "alarmOriginatorName", alarmOriginatorName, + "alarmOriginatorLabel", alarmOriginatorLabel ); } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/info/AlarmNotificationInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/info/AlarmNotificationInfo.java index a1a9a34a36..5ad33b1e6f 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/info/AlarmNotificationInfo.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/info/AlarmNotificationInfo.java @@ -25,11 +25,10 @@ import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DashboardId; import org.thingsboard.server.common.data.id.EntityId; +import java.util.HashMap; import java.util.Map; import java.util.UUID; -import static org.thingsboard.server.common.data.util.CollectionsUtil.mapOf; - @Data @NoArgsConstructor @AllArgsConstructor @@ -41,25 +40,28 @@ public class AlarmNotificationInfo implements RuleOriginatedNotificationInfo { private UUID alarmId; private EntityId alarmOriginator; private String alarmOriginatorName; + private String alarmOriginatorLabel; private AlarmSeverity alarmSeverity; private AlarmStatus alarmStatus; private boolean acknowledged; private boolean cleared; private CustomerId alarmCustomerId; private DashboardId dashboardId; + private Map details; @Override public Map getTemplateData() { - return mapOf( - "alarmType", alarmType, - "action", action, - "alarmId", alarmId.toString(), - "alarmSeverity", alarmSeverity.name().toLowerCase(), - "alarmStatus", alarmStatus.toString(), - "alarmOriginatorEntityType", alarmOriginator.getEntityType().getNormalName(), - "alarmOriginatorName", alarmOriginatorName, - "alarmOriginatorId", alarmOriginator.getId().toString() - ); + Map templateData = details != null ? new HashMap<>(details) : new HashMap<>(); + templateData.put("alarmType", alarmType); + templateData.put("action", action); + templateData.put("alarmId", alarmId.toString()); + templateData.put("alarmSeverity", alarmSeverity.name().toLowerCase()); + templateData.put("alarmStatus", alarmStatus.toString()); + templateData.put("alarmOriginatorEntityType", alarmOriginator.getEntityType().getNormalName()); + templateData.put("alarmOriginatorName", alarmOriginatorName); + templateData.put("alarmOriginatorLabel", alarmOriginatorLabel); + templateData.put("alarmOriginatorId", alarmOriginator.getId().toString()); + return templateData; } @Override diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/NotificationRule.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/NotificationRule.java index 81b5e0ccfb..fe87e0f966 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/NotificationRule.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/NotificationRule.java @@ -62,6 +62,7 @@ public class NotificationRule extends BaseData implements Ha @Valid private NotificationRuleRecipientsConfig recipientsConfig; + @Valid private NotificationRuleConfig additionalConfig; private NotificationRuleId externalId; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/NotificationRuleConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/NotificationRuleConfig.java index 9103086b7c..013c0ae662 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/NotificationRuleConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/NotificationRuleConfig.java @@ -16,12 +16,14 @@ package org.thingsboard.server.common.data.notification.rule; import lombok.Data; +import org.thingsboard.server.common.data.validation.NoXss; import java.io.Serializable; @Data public class NotificationRuleConfig implements Serializable { + @NoXss private String description; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/AlarmCommentTrigger.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/AlarmCommentTrigger.java index a9cee05707..e6298691ea 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/AlarmCommentTrigger.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/AlarmCommentTrigger.java @@ -25,10 +25,15 @@ import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.notification.rule.trigger.config.NotificationRuleTriggerType; +import java.io.Serial; + @Data @Builder public class AlarmCommentTrigger implements NotificationRuleTrigger { + @Serial + private static final long serialVersionUID = -8614770559491757202L; + private final TenantId tenantId; private final AlarmComment comment; private final Alarm alarm; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/AlarmTrigger.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/AlarmTrigger.java index a64bf13266..275c3b1e18 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/AlarmTrigger.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/AlarmTrigger.java @@ -22,10 +22,15 @@ import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.notification.rule.trigger.config.NotificationRuleTriggerType; +import java.io.Serial; + @Data @Builder public class AlarmTrigger implements NotificationRuleTrigger { + @Serial + private static final long serialVersionUID = -466810297904938644L; + private final TenantId tenantId; private final AlarmApiCallResult alarmUpdate; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/AlarmAssignmentNotificationRuleTriggerConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/AlarmAssignmentNotificationRuleTriggerConfig.java index a63a2d0cdf..cf701b5221 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/AlarmAssignmentNotificationRuleTriggerConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/AlarmAssignmentNotificationRuleTriggerConfig.java @@ -23,6 +23,7 @@ import lombok.NoArgsConstructor; import org.thingsboard.server.common.data.alarm.AlarmSearchStatus; import org.thingsboard.server.common.data.alarm.AlarmSeverity; +import java.io.Serial; import java.util.Set; @Data @@ -31,6 +32,9 @@ import java.util.Set; @Builder public class AlarmAssignmentNotificationRuleTriggerConfig implements NotificationRuleTriggerConfig { + @Serial + private static final long serialVersionUID = -5313556049809972096L; + private Set alarmTypes; private Set alarmSeverities; private Set alarmStatuses; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/AlarmCommentNotificationRuleTriggerConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/AlarmCommentNotificationRuleTriggerConfig.java index e2da4f5291..62762cce6e 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/AlarmCommentNotificationRuleTriggerConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/AlarmCommentNotificationRuleTriggerConfig.java @@ -22,6 +22,7 @@ import lombok.NoArgsConstructor; import org.thingsboard.server.common.data.alarm.AlarmSearchStatus; import org.thingsboard.server.common.data.alarm.AlarmSeverity; +import java.io.Serial; import java.util.Set; @Data @@ -30,6 +31,9 @@ import java.util.Set; @Builder public class AlarmCommentNotificationRuleTriggerConfig implements NotificationRuleTriggerConfig { + @Serial + private static final long serialVersionUID = -9164282098882339645L; + private Set alarmTypes; private Set alarmSeverities; private Set alarmStatuses; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/AlarmNotificationRuleTriggerConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/AlarmNotificationRuleTriggerConfig.java index 53f160b8ad..5006e63abd 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/AlarmNotificationRuleTriggerConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/AlarmNotificationRuleTriggerConfig.java @@ -23,6 +23,7 @@ import lombok.NoArgsConstructor; import org.thingsboard.server.common.data.alarm.AlarmSearchStatus; import org.thingsboard.server.common.data.alarm.AlarmSeverity; +import java.io.Serial; import java.io.Serializable; import java.util.Set; @@ -32,6 +33,9 @@ import java.util.Set; @Builder public class AlarmNotificationRuleTriggerConfig implements NotificationRuleTriggerConfig { + @Serial + private static final long serialVersionUID = -7382883720381542344L; + private Set alarmTypes; private Set alarmSeverities; @NotEmpty @@ -46,6 +50,8 @@ public class AlarmNotificationRuleTriggerConfig implements NotificationRuleTrigg @Data public static class ClearRule implements Serializable { + @Serial + private static final long serialVersionUID = 7922533150038105124L; private Set alarmStatuses; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/platform/AllUsersFilter.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/platform/AllUsersFilter.java index 79c7dafb1d..0d629fe970 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/platform/AllUsersFilter.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/platform/AllUsersFilter.java @@ -18,7 +18,7 @@ package org.thingsboard.server.common.data.notification.targets.platform; import lombok.Data; @Data -public class AllUsersFilter implements UsersFilter { +public class AllUsersFilter implements SystemLevelUsersFilter { @Override public UsersFilterType getType() { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/platform/SystemAdministratorsFilter.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/platform/SystemAdministratorsFilter.java index c178ee1159..4d35488ec1 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/platform/SystemAdministratorsFilter.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/platform/SystemAdministratorsFilter.java @@ -18,7 +18,7 @@ package org.thingsboard.server.common.data.notification.targets.platform; import lombok.Data; @Data -public class SystemAdministratorsFilter implements UsersFilter { +public class SystemAdministratorsFilter implements SystemLevelUsersFilter { @Override public UsersFilterType getType() { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/platform/SystemLevelUsersFilter.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/platform/SystemLevelUsersFilter.java new file mode 100644 index 0000000000..20233f7e49 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/platform/SystemLevelUsersFilter.java @@ -0,0 +1,19 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.notification.targets.platform; + +public interface SystemLevelUsersFilter extends UsersFilter { +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/platform/TenantAdministratorsFilter.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/platform/TenantAdministratorsFilter.java index 1f78790788..2f72e66076 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/platform/TenantAdministratorsFilter.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/platform/TenantAdministratorsFilter.java @@ -21,7 +21,7 @@ import java.util.Set; import java.util.UUID; @Data -public class TenantAdministratorsFilter implements UsersFilter { +public class TenantAdministratorsFilter implements SystemLevelUsersFilter { private Set tenantsIds; private Set tenantProfilesIds; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/slack/SlackFile.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/slack/SlackFile.java new file mode 100644 index 0000000000..1be73221c8 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/slack/SlackFile.java @@ -0,0 +1,29 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.notification.targets.slack; + +import lombok.Builder; +import lombok.Data; + +@Data +@Builder +public class SlackFile { + + private final String name; + private final String type; // one of https://api.slack.com/types/file#file_types + private final byte[] data; + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/plugin/ComponentLifecycleEvent.java b/common/data/src/main/java/org/thingsboard/server/common/data/plugin/ComponentLifecycleEvent.java index 5d13db2348..31cab71e0e 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/plugin/ComponentLifecycleEvent.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/plugin/ComponentLifecycleEvent.java @@ -32,7 +32,9 @@ public enum ComponentLifecycleEvent implements Serializable { STOPPED(5), DELETED(6), FAILED(7), - DEACTIVATED(8); + DEACTIVATED(8), + RELATION_UPDATED(9), + RELATION_DELETED(10); @Getter private final int protoNumber; // corresponds to ComponentLifecycleEvent proto diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/relation/EntityRelationPathQuery.java b/common/data/src/main/java/org/thingsboard/server/common/data/relation/EntityRelationPathQuery.java new file mode 100644 index 0000000000..a81e3e0c86 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/relation/EntityRelationPathQuery.java @@ -0,0 +1,24 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.relation; + +import org.thingsboard.server.common.data.id.EntityId; + +import java.util.List; + +public record EntityRelationPathQuery(EntityId rootEntityId, List levels) { + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/relation/RelationPathLevel.java b/common/data/src/main/java/org/thingsboard/server/common/data/relation/RelationPathLevel.java new file mode 100644 index 0000000000..c04ca09e79 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/relation/RelationPathLevel.java @@ -0,0 +1,32 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.relation; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import org.thingsboard.server.common.data.StringUtils; + +public record RelationPathLevel(@NotNull EntitySearchDirection direction, @NotBlank String relationType) { + + public void validate() { + if (direction == null) { + throw new IllegalArgumentException("Direction must be specified!"); + } + if (StringUtils.isBlank(relationType)) { + throw new IllegalArgumentException("Relation type must be specified!"); + } + } +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/security/Authority.java b/common/data/src/main/java/org/thingsboard/server/common/data/security/Authority.java index 1b832c847d..deaaf263ca 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/security/Authority.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/security/Authority.java @@ -20,8 +20,10 @@ public enum Authority { SYS_ADMIN(0), TENANT_ADMIN(1), CUSTOMER_USER(2), + REFRESH_TOKEN(10), - PRE_VERIFICATION_TOKEN(11); + PRE_VERIFICATION_TOKEN(11), + MFA_CONFIGURATION_TOKEN(12); private int code; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/security/DeviceCredentials.java b/common/data/src/main/java/org/thingsboard/server/common/data/security/DeviceCredentials.java index ad73510d9e..6d1dbdcb4a 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/security/DeviceCredentials.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/security/DeviceCredentials.java @@ -24,11 +24,15 @@ import org.thingsboard.server.common.data.HasVersion; import org.thingsboard.server.common.data.id.DeviceCredentialsId; import org.thingsboard.server.common.data.id.DeviceId; +import java.io.Serial; + @Schema @EqualsAndHashCode(callSuper = true) public class DeviceCredentials extends BaseData implements DeviceCredentialsFilter, HasVersion { + @Serial private static final long serialVersionUID = -7869261127032877765L; + private DeviceId deviceId; private DeviceCredentialsType credentialsType; private String credentialsId; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/PlatformTwoFaSettings.java b/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/PlatformTwoFaSettings.java index 49b7e707cf..c1c632f016 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/PlatformTwoFaSettings.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/PlatformTwoFaSettings.java @@ -21,6 +21,7 @@ import jakarta.validation.constraints.Min; import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Pattern; import lombok.Data; +import org.thingsboard.server.common.data.notification.targets.platform.SystemLevelUsersFilter; import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFaProviderConfig; import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFaProviderType; @@ -46,6 +47,8 @@ public class PlatformTwoFaSettings { @Min(value = 60) private Integer totalAllowedTimeForVerification; + private boolean enforceTwoFa; + private SystemLevelUsersFilter enforcedUsersFilter; public Optional getProviderConfig(TwoFaProviderType providerType) { return Optional.ofNullable(providers) diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java index 246fe46791..7587f563ab 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java @@ -172,6 +172,12 @@ public class DefaultTenantProfileConfiguration implements TenantProfileConfigura private long maxCalculatedFieldsPerEntity = 5; @Schema(example = "10") private long maxArgumentsPerCF = 10; + @Schema(example = "60") + private int minAllowedScheduledUpdateIntervalInSecForCF = 60; + @Schema(example = "10") + private int maxRelationLevelPerCfArgument = 10; + @Schema(example = "100") + private int maxRelatedEntitiesToReturnPerCfArgument = 100; @Builder.Default @Min(value = 1, message = "must be at least 1") @Schema(example = "1000") @@ -180,6 +186,10 @@ public class DefaultTenantProfileConfiguration implements TenantProfileConfigura private long maxStateSizeInKBytes = 32; @Schema(example = "2") private long maxSingleValueArgumentSizeInKBytes = 2; + @Schema(example = "60") + private long minAllowedDeduplicationIntervalInSecForCF = 60; + @Schema(example = "60") + private long minAllowedAggregationIntervalInSecForCF = 60; @Override public long getProfileThreshold(ApiUsageRecordKey key) { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/util/CollectionsUtil.java b/common/data/src/main/java/org/thingsboard/server/common/data/util/CollectionsUtil.java index 71c5256203..0d69db556d 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/util/CollectionsUtil.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/util/CollectionsUtil.java @@ -18,9 +18,11 @@ package org.thingsboard.server.common.data.util; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; +import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.function.BiPredicate; import java.util.stream.Collectors; public class CollectionsUtil { @@ -95,4 +97,44 @@ public class CollectionsUtil { return false; } + public static boolean elementsEqual(Iterable iterable1, Iterable iterable2, BiPredicate equalityCheck) { + if (iterable1 instanceof Collection collection1 && iterable2 instanceof Collection collection2) { + if (collection1.size() != collection2.size()) { + return false; + } + } + + Iterator iterator1 = iterable1.iterator(); + Iterator iterator2 = iterable2.iterator(); + while (true) { + if (iterator1.hasNext()) { + if (!iterator2.hasNext()) { + return false; + } + + T o1 = iterator1.next(); + T o2 = iterator2.next(); + if (equalityCheck.test(o1, o2)) { + continue; + } else { + return false; + } + } + return !iterator2.hasNext(); + } + } + + public static Set addToSet(Set existing, T value) { + if (existing == null || existing.isEmpty()) { + return Set.of(value); + } + if (existing.contains(value)) { + return existing; + } + Set newSet = new HashSet<>(existing.size() + 1); + newSet.addAll(existing); + newSet.add(value); + return (Set) Set.of(newSet.toArray()); + } + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/util/TypeCastUtil.java b/common/data/src/main/java/org/thingsboard/server/common/data/util/TypeCastUtil.java index 85071b3cc9..82de579d3d 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/util/TypeCastUtil.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/util/TypeCastUtil.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.common.data.util; +import com.google.gson.JsonParser; import org.apache.commons.lang3.math.NumberUtils; import org.apache.commons.lang3.tuple.Pair; import org.thingsboard.server.common.data.kv.DataType; @@ -40,6 +41,11 @@ public class TypeCastUtil { } catch (RuntimeException ignored) {} } else if (value.equalsIgnoreCase("true") || value.equalsIgnoreCase("false")) { return Pair.of(DataType.BOOLEAN, Boolean.parseBoolean(value)); + } else if (looksLikeJson(value)) { + try { + return Pair.of(DataType.JSON, JsonParser.parseString(value)); + } catch (Exception ignored) { + } } return Pair.of(DataType.STRING, value); } @@ -70,4 +76,10 @@ public class TypeCastUtil { return valueAsString.contains(".") && !valueAsString.contains("E") && !valueAsString.contains("e"); } + private static boolean looksLikeJson(String value) { + String trimmed = value.trim(); + return (trimmed.startsWith("{") && trimmed.endsWith("}")) || + (trimmed.startsWith("[") && trimmed.endsWith("]")); + } + } diff --git a/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/ArgumentTest.java b/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/ArgumentTest.java new file mode 100644 index 0000000000..ec99b29fe8 --- /dev/null +++ b/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/ArgumentTest.java @@ -0,0 +1,49 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.cf.configuration; + + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class ArgumentTest { + + @Test + void validateShouldReturnFalseIfDynamicSourceConfigurationIsNull() { + var argument = new Argument(); + assertThat(argument.hasDynamicSource()).isFalse(); + } + + @Test + void validateWhenRelationQuerySourceConfigurationIsNotNull() { + var argument = new Argument(); + argument.setRefDynamicSourceConfiguration(new RelationPathQueryDynamicSourceConfiguration()); + assertThat(argument.hasDynamicSource()).isTrue(); + assertThat(argument.hasRelationQuerySource()).isTrue(); + assertThat(argument.hasOwnerSource()).isFalse(); + } + + @Test + void validateWhenCurrentOwnerSourceConfigurationIsNotNull() { + var argument = new Argument(); + argument.setRefDynamicSourceConfiguration(new CurrentOwnerDynamicSourceConfiguration()); + assertThat(argument.hasDynamicSource()).isTrue(); + assertThat(argument.hasOwnerSource()).isTrue(); + assertThat(argument.hasRelationQuerySource()).isFalse(); + } + +} diff --git a/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/PropagationCalculatedFieldConfigurationTest.java b/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/PropagationCalculatedFieldConfigurationTest.java new file mode 100644 index 0000000000..9c77f1bd21 --- /dev/null +++ b/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/PropagationCalculatedFieldConfigurationTest.java @@ -0,0 +1,154 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.cf.configuration; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; +import org.thingsboard.server.common.data.cf.CalculatedFieldType; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.relation.EntityRelation; +import org.thingsboard.server.common.data.relation.EntitySearchDirection; +import org.thingsboard.server.common.data.relation.RelationPathLevel; + +import java.util.Map; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.thingsboard.server.common.data.cf.configuration.PropagationCalculatedFieldConfiguration.PROPAGATION_CONFIG_ARGUMENT; + +@ExtendWith(MockitoExtension.class) +public class PropagationCalculatedFieldConfigurationTest { + + @Test + void typeShouldBePropagation() { + var cfg = new PropagationCalculatedFieldConfiguration(); + assertThat(cfg.getType()).isEqualTo(CalculatedFieldType.PROPAGATION); + } + + @Test + void validateShouldThrowWhenConfigurationDisallowArgumentsWithReferencedEntity() { + var cfg = new PropagationCalculatedFieldConfiguration(); + Argument argumentWithRefEntityIdSet = new Argument(); + argumentWithRefEntityIdSet.setRefEntityId(new DeviceId(UUID.fromString("bda14084-f40e-4acc-9b85-9d1dd209bb64"))); + cfg.setArguments(Map.of("argumentWithRefEntityIdSet", argumentWithRefEntityIdSet)); + assertThatThrownBy(cfg::validate) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Arguments in 'Arguments only' propagation mode support only the 'Current entity' source entity type!"); + } + + @Test + void validateShouldThrowWhenConfigurationDisallowArgumentsWithDynamicReferenceConfiguration() { + var cfg = new PropagationCalculatedFieldConfiguration(); + Argument argumentWithDynamicRefEntitySource = new Argument(); + argumentWithDynamicRefEntitySource.setRefDynamicSourceConfiguration(new CurrentOwnerDynamicSourceConfiguration()); + cfg.setArguments(Map.of("argumentWithDynamicRefEntitySource", argumentWithDynamicRefEntitySource)); + assertThatThrownBy(cfg::validate) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Arguments in 'Arguments only' propagation mode support only the 'Current entity' source entity type!"); + } + + @Test + void validateShouldThrowWhenConfigurationHasNoArgumentsWithCurrentEntitySource() { + var cfg = new PropagationCalculatedFieldConfiguration(); + Argument argumentWithRefEntityIdSet = new Argument(); + argumentWithRefEntityIdSet.setRefEntityId(new DeviceId(UUID.fromString("3703e895-3f9b-4b75-a715-b68f1ad51944"))); + cfg.setArguments(Map.of("argumentWithRefEntityIdSet", argumentWithRefEntityIdSet)); + cfg.setApplyExpressionToResolvedArguments(true); + assertThatThrownBy(cfg::validate) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("At least one argument must be configured with the 'Current entity' " + + "source entity type for 'Expression result' propagation mode!"); + } + + @Test + void validateShouldThrowWhenUsedReservedPropagationArgumentName() { + var cfg = new PropagationCalculatedFieldConfiguration(); + cfg.setArguments(Map.of(PROPAGATION_CONFIG_ARGUMENT, new Argument())); + assertThatThrownBy(cfg::validate) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Argument name '" + PROPAGATION_CONFIG_ARGUMENT + "' is reserved and cannot be used."); + } + + @Test + void validateShouldThrowWhenUsedReservedCtxArgumentName() { + var cfg = new PropagationCalculatedFieldConfiguration(); + cfg.setArguments(Map.of("ctx", new Argument())); + assertThatThrownBy(cfg::validate) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Argument name 'ctx' is reserved and cannot be used."); + } + + @Test + void validateShouldThrowWhenReferencedEntityKeyIsNotSet() { + var cfg = new PropagationCalculatedFieldConfiguration(); + cfg.setRelation(new RelationPathLevel(EntitySearchDirection.TO, EntityRelation.CONTAINS_TYPE)); + Argument argument = new Argument(); + cfg.setArguments(Map.of("someArgumentName", argument)); + assertThatThrownBy(cfg::validate) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Argument: 'someArgumentName' doesn't have reference entity key configured!"); + } + + @Test + void validateShouldThrowWhenReferencedEntityKeyTypeIsTsRolling() { + var cfg = new PropagationCalculatedFieldConfiguration(); + ReferencedEntityKey referencedEntityKey = new ReferencedEntityKey("someKey", ArgumentType.TS_ROLLING, null); + Argument argument = new Argument(); + argument.setRefEntityKey(referencedEntityKey); + cfg.setArguments(Map.of("someArgumentName", argument)); + assertThatThrownBy(cfg::validate) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Argument type: 'Time series rolling' detected for argument: 'someArgumentName'. " + + "Only 'Attribute' or 'Latest telemetry' arguments are allowed for 'Arguments only' propagation mode!"); + } + + @Test + void validateShouldThrowWhenExpressionIsNotSet() { + var cfg = new PropagationCalculatedFieldConfiguration(); + cfg.setArguments(Map.of("someArgumentName", new Argument())); + cfg.setApplyExpressionToResolvedArguments(true); + assertThatThrownBy(cfg::validate) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Expression must be specified for 'Expression result' propagation mode!"); + } + + @Test + void validateToPropagationArgumentMethodCallReturnCorrectArgument() { + var cfg = new PropagationCalculatedFieldConfiguration(); + cfg.setRelation(new RelationPathLevel(EntitySearchDirection.TO, EntityRelation.CONTAINS_TYPE)); + + Argument propagationArgument = cfg.toPropagationArgument(); + assertThat(propagationArgument).isNotNull(); + assertThat(propagationArgument.getRefEntityId()).isNull(); + assertThat(propagationArgument.getRefEntityKey()).isNull(); + assertThat(propagationArgument.getDefaultValue()).isNull(); + assertThat(propagationArgument.getTimeWindow()).isNull(); + assertThat(propagationArgument.getLimit()).isNull(); + + assertThat(propagationArgument.getRefDynamicSourceConfiguration()) + .isNotNull() + .isInstanceOf(RelationPathQueryDynamicSourceConfiguration.class); + var refDynamicSourceConfiguration = (RelationPathQueryDynamicSourceConfiguration) propagationArgument.getRefDynamicSourceConfiguration(); + assertThat(refDynamicSourceConfiguration.getLevels()).isNotEmpty().hasSize(1); + + var relationPathLevel = refDynamicSourceConfiguration.getLevels().get(0); + assertThat(relationPathLevel.direction()).isEqualTo(EntitySearchDirection.TO); + assertThat(relationPathLevel.relationType()).isEqualTo(EntityRelation.CONTAINS_TYPE); + } + +} diff --git a/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/RelationPathQueryDynamicSourceConfigurationTest.java b/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/RelationPathQueryDynamicSourceConfigurationTest.java new file mode 100644 index 0000000000..1a6ea5de3b --- /dev/null +++ b/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/RelationPathQueryDynamicSourceConfigurationTest.java @@ -0,0 +1,126 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.cf.configuration; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.NullAndEmptySource; +import org.mockito.junit.jupiter.MockitoExtension; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.relation.EntityRelation; +import org.thingsboard.server.common.data.relation.EntitySearchDirection; +import org.thingsboard.server.common.data.relation.RelationPathLevel; + +import java.util.ArrayList; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +public class RelationPathQueryDynamicSourceConfigurationTest { + + @Test + void typeShouldBeRelationQuery() { + var cfg = new RelationPathQueryDynamicSourceConfiguration(); + assertThat(cfg.getType()).isEqualTo(CFArgumentDynamicSourceType.RELATION_PATH_QUERY); + } + + @ParameterizedTest + @NullAndEmptySource + void validateShouldThrowWhenLevelsIsNull(List levels) { + var cfg = new RelationPathQueryDynamicSourceConfiguration(); + cfg.setLevels(levels); + + assertThatThrownBy(cfg::validate) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("At least one relation level must be specified!"); + } + + @Test + void validateShouldCallValidateForPathLevels() { + List levels = new ArrayList<>(); + + RelationPathLevel lvl1 = mock(RelationPathLevel.class); + RelationPathLevel lvl2 = mock(RelationPathLevel.class); + levels.add(lvl1); + levels.add(lvl2); + + var cfg = new RelationPathQueryDynamicSourceConfiguration(); + cfg.setLevels(levels); + + assertThatCode(cfg::validate).doesNotThrowAnyException(); + + verify(lvl1).validate(); + verify(lvl2).validate(); + } + + @Test + void resolveEntityIds_whenDirectionFROM_thenReturnsToIds() { + List levels = new ArrayList<>(); + + RelationPathLevel lvl1 = mock(RelationPathLevel.class); + RelationPathLevel lvl2 = mock(RelationPathLevel.class); + levels.add(lvl1); + levels.add(lvl2); + + when(lvl2.direction()).thenReturn(EntitySearchDirection.FROM); + + EntityRelation rel1 = mock(EntityRelation.class); + EntityRelation rel2 = mock(EntityRelation.class); + + when(rel1.getTo()).thenReturn(mock(EntityId.class)); + when(rel2.getTo()).thenReturn(mock(EntityId.class)); + + var cfg = new RelationPathQueryDynamicSourceConfiguration(); + cfg.setLevels(levels); + + var out = cfg.resolveEntityIds(List.of(rel1, rel2)); + + assertThat(out).containsExactly(rel1.getTo(), rel2.getTo()); + } + + @Test + void resolveEntityIds_whenDirectionTO_thenReturnsFromIds() { + List levels = new ArrayList<>(); + + RelationPathLevel lvl1 = mock(RelationPathLevel.class); + RelationPathLevel lvl2 = mock(RelationPathLevel.class); + levels.add(lvl1); + levels.add(lvl2); + + when(lvl2.direction()).thenReturn(EntitySearchDirection.TO); + + EntityRelation rel1 = mock(EntityRelation.class); + EntityRelation rel2 = mock(EntityRelation.class); + + when(rel1.getFrom()).thenReturn(mock(EntityId.class)); + when(rel2.getFrom()).thenReturn(mock(EntityId.class)); + + var cfg = new RelationPathQueryDynamicSourceConfiguration(); + cfg.setLevels(levels); + + var out = cfg.resolveEntityIds(List.of(rel1, rel2)); + + assertThat(out).containsExactly(rel1.getFrom(), rel2.getFrom()); + } + +} diff --git a/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/ScheduledUpdateSupportedCalculatedFieldConfigurationTest.java b/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/ScheduledUpdateSupportedCalculatedFieldConfigurationTest.java new file mode 100644 index 0000000000..15a191be97 --- /dev/null +++ b/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/ScheduledUpdateSupportedCalculatedFieldConfigurationTest.java @@ -0,0 +1,54 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.cf.configuration; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; +import org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingCalculatedFieldConfiguration; + +import java.util.concurrent.TimeUnit; + +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +@ExtendWith(MockitoExtension.class) +public class ScheduledUpdateSupportedCalculatedFieldConfigurationTest { + + @Test + void validateDoesNotThrowAnyExceptionWhenScheduledUpdateIntervalIsGreaterThanMinAllowedIntervalInTenantProfile() { + int scheduledUpdateInterval = 60; + int minAllowedInterval = scheduledUpdateInterval - 1; + + var cfg = new GeofencingCalculatedFieldConfiguration(); + cfg.setScheduledUpdateInterval(scheduledUpdateInterval); + assertThatCode(() -> cfg.validate(minAllowedInterval)).doesNotThrowAnyException(); + } + + @Test + void validateShouldThrowWhenScheduledUpdateIntervalIsLessThanMinAllowedIntervalInTenantProfile() { + int minAllowedInterval = (int) TimeUnit.HOURS.toSeconds(2); + + var cfg = new GeofencingCalculatedFieldConfiguration(); + cfg.setScheduledUpdateInterval(1); + + assertThatThrownBy(() -> cfg.validate(minAllowedInterval)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Scheduled update interval is less than configured " + + "minimum allowed interval in tenant profile: " + minAllowedInterval); + } + +} diff --git a/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/aggregation/single/EntityAggregationCalculatedFieldConfigurationTest.java b/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/aggregation/single/EntityAggregationCalculatedFieldConfigurationTest.java new file mode 100644 index 0000000000..3884b5a214 --- /dev/null +++ b/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/aggregation/single/EntityAggregationCalculatedFieldConfigurationTest.java @@ -0,0 +1,128 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.cf.configuration.aggregation.single; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.thingsboard.server.common.data.cf.CalculatedFieldType; +import org.thingsboard.server.common.data.cf.configuration.Argument; +import org.thingsboard.server.common.data.cf.configuration.ArgumentType; +import org.thingsboard.server.common.data.cf.configuration.Output; +import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey; +import org.thingsboard.server.common.data.cf.configuration.aggregation.AggFunctionInput; +import org.thingsboard.server.common.data.cf.configuration.aggregation.AggKeyInput; +import org.thingsboard.server.common.data.cf.configuration.aggregation.AggMetric; +import org.thingsboard.server.common.data.cf.configuration.aggregation.single.interval.HourInterval; + +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +public class EntityAggregationCalculatedFieldConfigurationTest { + + @Test + void typeShouldBeEntityAggregation() { + var cfg = new EntityAggregationCalculatedFieldConfiguration(); + assertThat(cfg.getType()).isEqualTo(CalculatedFieldType.ENTITY_AGGREGATION); + } + + @ParameterizedTest + @ValueSource(strings = {"ATTRIBUTE", "TS_ROLLING"}) + void validateShouldThrowWhenNotTsLatestArgumentUsed(String argumentType) { + var cfg = new EntityAggregationCalculatedFieldConfiguration(); + cfg.setArguments(Map.of("k", validArgument(ArgumentType.valueOf(argumentType)))); + assertThatThrownBy(cfg::validate) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Calculated field with type: '" + cfg.getType() + "' support only TS_LATEST arguments."); + } + + @Test + void validateShouldThrowWhenMetricMapIsEmpty() { + var cfg = new EntityAggregationCalculatedFieldConfiguration(); + + cfg.setArguments(Map.of("k", validArgument(ArgumentType.TS_LATEST))); + cfg.setMetrics(Map.of()); + + assertThatThrownBy(cfg::validate) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Metrics map cannot be empty."); + } + + @Test + void validateShouldThrowWhenMetricInputIsNotAggKeyInput() { + var cfg = new EntityAggregationCalculatedFieldConfiguration(); + + cfg.setArguments(Map.of("k", validArgument(ArgumentType.TS_LATEST))); + + AggMetric metric = new AggMetric(); + metric.setInput(new AggFunctionInput()); // cannot be function + cfg.setMetrics(Map.of("m", metric)); + + cfg.setInterval(new HourInterval("Europe/Kiev", null)); + cfg.setOutput(new Output()); + + assertThatThrownBy(cfg::validate) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Metric key can only refer to argument."); + } + + @Test + void validateShouldThrowWhenMetricReferencesUnknownArgument() { + var cfg = new EntityAggregationCalculatedFieldConfiguration(); + + cfg.setArguments(Map.of("k", validArgument(ArgumentType.TS_LATEST))); + + AggMetric metric = new AggMetric(); + metric.setInput(new AggKeyInput("unknown")); + cfg.setMetrics(Map.of("m", metric)); + + cfg.setInterval(new HourInterval("Europe/Kiev", null)); + cfg.setOutput(new Output()); + + assertThatThrownBy(cfg::validate) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Metric references unknown argument: 'unknown'."); + } + + @Test + void validateShouldThrowWhenIntervalIsNull() { + var cfg = new EntityAggregationCalculatedFieldConfiguration(); + + cfg.setArguments(Map.of("k", validArgument(ArgumentType.TS_LATEST))); + cfg.setMetrics(Map.of("m", validMetric())); + cfg.setInterval(null); + cfg.setOutput(new Output()); + + assertThatThrownBy(cfg::validate) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Interval must be defined."); + } + + private Argument validArgument(ArgumentType type) { + Argument a = new Argument(); + a.setRefEntityKey(new ReferencedEntityKey("key", type, null)); + return a; + } + + private AggMetric validMetric() { + AggMetric metric = new AggMetric(); + metric.setInput(new AggKeyInput("k")); + return metric; + } + +} diff --git a/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/aggregation/single/interval/AggIntervalTest.java b/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/aggregation/single/interval/AggIntervalTest.java new file mode 100644 index 0000000000..b439c44fef --- /dev/null +++ b/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/aggregation/single/interval/AggIntervalTest.java @@ -0,0 +1,168 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.cf.configuration.aggregation.single.interval; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.time.Duration; +import java.time.Instant; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.util.concurrent.TimeUnit; +import java.util.function.Function; +import java.util.function.LongFunction; +import java.util.stream.Stream; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +public class AggIntervalTest { + + private static final String TZ = "Europe/Kiev"; + + @Test + void validateShouldThrowWhenInvalidTimZone() { + AggInterval interval = new HourInterval("TimeZone", null); + + assertThatThrownBy(interval::validate) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("Invalid timezone in interval: "); + } + + @Test + void validateShouldThrowWhenOffsetIsNegative() { + AggInterval interval = new CustomInterval(TZ, -100L, TimeUnit.HOURS.toSeconds(2)); + + assertThatThrownBy(interval::validate) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Offset cannot be negative."); + } + + @Test + void validateShouldThrowWhenOffsetGreaterThanIntervalDuration() { + AggInterval interval = new CustomInterval(TZ, TimeUnit.HOURS.toSeconds(2), TimeUnit.HOURS.toSeconds(2)); + + assertThatThrownBy(interval::validate) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Offset must be greater than interval duration."); + } + + @ParameterizedTest + @MethodSource("intervals") + void testGetStartAndEndWithoutOffset(LongFunction intervalCreator) { + AggInterval interval = intervalCreator.apply(0L); + + ZonedDateTime dateTime = ZonedDateTime.of( + // 2025.11.11 00:00:00 + 2025, 11, 11, 0, 0, 0, 0, ZoneId.of(TZ) + ); + long startTs = interval.getDateTimeIntervalStartTs(dateTime); + long endTs = interval.getDateTimeIntervalEndTs(dateTime); + + assertThat(endTs).isGreaterThan(startTs); + assertThat(endTs - startTs).isEqualTo(interval.getCurrentIntervalDurationMillis()); + } + + @ParameterizedTest + @MethodSource("intervals") + void testApplyOffset(LongFunction intervalCreator) { + long offsetSec = TimeUnit.MINUTES.toSeconds(15); + AggInterval intervalWithOffset = intervalCreator.apply(offsetSec); + AggInterval intervalNoOffset = intervalCreator.apply(0L); + + ZonedDateTime dateTime = ZonedDateTime.of( + // 2025.11.11 11:20:00 - chosen so 15m offset shifts into a new interval + 2025, 11, 11, 11, 20, 0, 0, ZoneId.of(TZ) + ); + + long startWithOffsetTs = intervalWithOffset.getDateTimeIntervalStartTs(dateTime); + long startNoOffsetTs = intervalNoOffset.getDateTimeIntervalStartTs(dateTime); + + ZonedDateTime startWithOffset = Instant.ofEpochMilli(startWithOffsetTs).atZone(intervalWithOffset.getZoneId()); + ZonedDateTime startNoOffset = Instant.ofEpochMilli(startNoOffsetTs).atZone(intervalNoOffset.getZoneId()); + + long actualOffset = Duration.between(startNoOffset, startWithOffset).toSeconds(); + assertThat(actualOffset).isEqualTo(offsetSec); + } + + private static Stream intervals() { + return Stream.of( + Arguments.of((LongFunction) offset -> new HourInterval(TZ, offset)), + Arguments.of((LongFunction) offset -> new DayInterval(TZ, offset)), + Arguments.of((LongFunction) offset -> new WeekInterval(TZ, offset)), + Arguments.of((LongFunction) offset -> new WeekSunSatInterval(TZ, offset)), + Arguments.of((LongFunction) offset -> new MonthInterval(TZ, offset)), + Arguments.of((LongFunction) offset -> new QuarterInterval(TZ, offset)), + Arguments.of((LongFunction) offset -> new YearInterval(TZ, offset)), + Arguments.of((LongFunction) offset -> new CustomInterval(TZ, offset, TimeUnit.HOURS.toSeconds(4))) + ); + } + + @ParameterizedTest + @MethodSource("nextIntervalFromExactDate") + void testNextIntervalFromExactDate(LongFunction intervalCreator, Function expectedDateTimeFunction) { + AggInterval interval = intervalCreator.apply(0L); + + ZonedDateTime currentStart = ZonedDateTime.of( + 2025, 11, 11, 0, 0, 0, 0, ZoneId.of(TZ) + ); + + ZonedDateTime nextStart = interval.getNextIntervalStart(currentStart); + + assertThat(nextStart).isEqualTo(expectedDateTimeFunction.apply(currentStart)); + } + + private static Stream nextIntervalFromExactDate() { + return Stream.of( + Arguments.of( + (LongFunction) offset -> new HourInterval(TZ, offset), + (Function) currentInterval -> currentInterval.plusHours(1) + ), + Arguments.of( + (LongFunction) offset -> new DayInterval(TZ, offset), + (Function) currentInterval -> currentInterval.plusDays(1) + ), + Arguments.of( + (LongFunction) offset -> new WeekInterval(TZ, offset), + (Function) currentInterval -> currentInterval.plusWeeks(1) + ), + Arguments.of( + (LongFunction) offset -> new WeekSunSatInterval(TZ, offset), + (Function) currentInterval -> currentInterval.plusWeeks(1) + ), + Arguments.of( + (LongFunction) offset -> new MonthInterval(TZ, offset), + (Function) currentInterval -> currentInterval.plusMonths(1) + ), + Arguments.of( + (LongFunction) offset -> new QuarterInterval(TZ, offset), + (Function) currentInterval -> currentInterval.plusMonths(3) + ), + Arguments.of( + (LongFunction) offset -> new YearInterval(TZ, offset), + (Function) currentInterval -> currentInterval.plusYears(1) + ), + Arguments.of( + (LongFunction) offset -> new CustomInterval(TZ, offset, TimeUnit.HOURS.toSeconds(4)), + (Function) currentInterval -> currentInterval.plusHours(4) + ) + ); + } + +} diff --git a/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/geofencing/EntityCoordinatesTest.java b/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/geofencing/EntityCoordinatesTest.java new file mode 100644 index 0000000000..a8ee18c7d7 --- /dev/null +++ b/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/geofencing/EntityCoordinatesTest.java @@ -0,0 +1,49 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.cf.configuration.geofencing; + +import org.junit.jupiter.api.Test; +import org.thingsboard.server.common.data.cf.configuration.Argument; +import org.thingsboard.server.common.data.cf.configuration.ArgumentType; +import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.thingsboard.server.common.data.cf.configuration.geofencing.EntityCoordinates.ENTITY_ID_LATITUDE_ARGUMENT_KEY; +import static org.thingsboard.server.common.data.cf.configuration.geofencing.EntityCoordinates.ENTITY_ID_LONGITUDE_ARGUMENT_KEY; + +public class EntityCoordinatesTest { + + @Test + void validateToArgumentsMethodCallWithoutRefEntityId() { + var entityCoordinates = new EntityCoordinates("xPos", "yPos"); + + var arguments = entityCoordinates.toArguments(); + assertThat(arguments).isNotNull().hasSize(2); + + Argument latitudeArgument = arguments.get(ENTITY_ID_LATITUDE_ARGUMENT_KEY); + assertThat(latitudeArgument).isNotNull(); + assertThat(latitudeArgument.getRefEntityKey()).isEqualTo(new ReferencedEntityKey("xPos", ArgumentType.TS_LATEST, null)); + assertThat(latitudeArgument.getRefEntityId()).isNull(); + assertThat(latitudeArgument.getRefDynamicSourceConfiguration()).isNull(); + + Argument longitudeArgument = arguments.get(ENTITY_ID_LONGITUDE_ARGUMENT_KEY); + assertThat(longitudeArgument).isNotNull(); + assertThat(longitudeArgument.getRefEntityKey()).isEqualTo(new ReferencedEntityKey("yPos", ArgumentType.TS_LATEST, null)); + assertThat(longitudeArgument.getRefEntityId()).isNull(); + assertThat(longitudeArgument.getRefDynamicSourceConfiguration()).isNull(); + } + +} diff --git a/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/geofencing/GeofencingCalculatedFieldConfigurationTest.java b/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/geofencing/GeofencingCalculatedFieldConfigurationTest.java new file mode 100644 index 0000000000..2e9d5e4a88 --- /dev/null +++ b/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/geofencing/GeofencingCalculatedFieldConfigurationTest.java @@ -0,0 +1,106 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.cf.configuration.geofencing; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; +import org.thingsboard.server.common.data.AttributeScope; +import org.thingsboard.server.common.data.cf.CalculatedFieldType; +import org.thingsboard.server.common.data.cf.configuration.Argument; +import org.thingsboard.server.common.data.cf.configuration.ArgumentType; +import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey; + +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.thingsboard.server.common.data.cf.configuration.geofencing.EntityCoordinates.ENTITY_ID_LATITUDE_ARGUMENT_KEY; +import static org.thingsboard.server.common.data.cf.configuration.geofencing.EntityCoordinates.ENTITY_ID_LONGITUDE_ARGUMENT_KEY; + +@ExtendWith(MockitoExtension.class) +public class GeofencingCalculatedFieldConfigurationTest { + + @Test + void typeShouldBeGeofencing() { + var cfg = new GeofencingCalculatedFieldConfiguration(); + assertThat(cfg.getType()).isEqualTo(CalculatedFieldType.GEOFENCING); + } + + @Test + void validateShouldCallValidateOnZoneGroups() { + var cfg = new GeofencingCalculatedFieldConfiguration(); + EntityCoordinates entityCoordinatesMock = mock(EntityCoordinates.class); + cfg.setEntityCoordinates(entityCoordinatesMock); + var zoneGroupConfiguration = mock(ZoneGroupConfiguration.class); + cfg.setZoneGroups(Map.of("someGroupName", zoneGroupConfiguration)); + + cfg.validate(); + verify(zoneGroupConfiguration).validate("someGroupName"); + } + + @Test + void validateShouldCallValidateOnZoneGroupsWithoutAnyExceptions() { + var cfg = new GeofencingCalculatedFieldConfiguration(); + EntityCoordinates entityCoordinatesMock = mock(EntityCoordinates.class); + cfg.setEntityCoordinates(entityCoordinatesMock); + var zoneGroupConfigurationA = mock(ZoneGroupConfiguration.class); + var zoneGroupConfigurationB = mock(ZoneGroupConfiguration.class); + + String zoneGroupAName = "zoneGroupA"; + String zoneGroupBName = "zoneGroupB"; + + cfg.setZoneGroups(Map.of("zoneGroupA", zoneGroupConfigurationA, "zoneGroupB", zoneGroupConfigurationB)); + + assertThatCode(cfg::validate).doesNotThrowAnyException(); + + verify(zoneGroupConfigurationA).validate(zoneGroupAName); + verify(zoneGroupConfigurationB).validate(zoneGroupBName); + } + + @Test + void testGetArgumentsOverride() { + var cfg = new GeofencingCalculatedFieldConfiguration(); + cfg.setEntityCoordinates(new EntityCoordinates(ENTITY_ID_LATITUDE_ARGUMENT_KEY, ENTITY_ID_LONGITUDE_ARGUMENT_KEY)); + cfg.setZoneGroups(Map.of("allowedZones", new ZoneGroupConfiguration("perimeter", GeofencingReportStrategy.REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS, false))); + + Map arguments = cfg.getArguments(); + + assertThat(arguments).isNotNull().hasSize(3); + assertThat(arguments).containsKeys(ENTITY_ID_LATITUDE_ARGUMENT_KEY, ENTITY_ID_LONGITUDE_ARGUMENT_KEY, "allowedZones"); + + Argument latitudeArgument = arguments.get(ENTITY_ID_LATITUDE_ARGUMENT_KEY); + assertThat(latitudeArgument).isNotNull(); + assertThat(latitudeArgument.getRefDynamicSourceConfiguration()).isNull(); + assertThat(latitudeArgument.getRefEntityId()).isNull(); + assertThat(latitudeArgument.getRefEntityKey()).isEqualTo(new ReferencedEntityKey(ENTITY_ID_LATITUDE_ARGUMENT_KEY, ArgumentType.TS_LATEST, null)); + + Argument longitudeArgument = arguments.get(ENTITY_ID_LONGITUDE_ARGUMENT_KEY); + assertThat(longitudeArgument).isNotNull(); + assertThat(longitudeArgument.getRefDynamicSourceConfiguration()).isNull(); + assertThat(longitudeArgument.getRefEntityId()).isNull(); + assertThat(longitudeArgument.getRefEntityKey()).isEqualTo(new ReferencedEntityKey(ENTITY_ID_LONGITUDE_ARGUMENT_KEY, ArgumentType.TS_LATEST, null)); + + Argument allowedZonesArgument = arguments.get("allowedZones"); + assertThat(allowedZonesArgument).isNotNull(); + assertThat(allowedZonesArgument.getRefDynamicSourceConfiguration()).isNull(); + assertThat(allowedZonesArgument.getRefEntityId()).isNull(); + assertThat(allowedZonesArgument.getRefEntityKey()).isEqualTo(new ReferencedEntityKey("perimeter", ArgumentType.ATTRIBUTE, AttributeScope.SERVER_SCOPE)); + } + +} diff --git a/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/geofencing/ZoneGroupConfigurationTest.java b/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/geofencing/ZoneGroupConfigurationTest.java new file mode 100644 index 0000000000..e354a05af9 --- /dev/null +++ b/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/geofencing/ZoneGroupConfigurationTest.java @@ -0,0 +1,112 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.cf.configuration.geofencing; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.NullAndEmptySource; +import org.junit.jupiter.params.provider.ValueSource; +import org.thingsboard.server.common.data.AttributeScope; +import org.thingsboard.server.common.data.cf.configuration.Argument; +import org.thingsboard.server.common.data.cf.configuration.ArgumentType; +import org.thingsboard.server.common.data.cf.configuration.CurrentOwnerDynamicSourceConfiguration; +import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey; +import org.thingsboard.server.common.data.cf.configuration.RelationPathQueryDynamicSourceConfiguration; +import org.thingsboard.server.common.data.relation.EntityRelation; +import org.thingsboard.server.common.data.relation.EntitySearchDirection; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.mock; +import static org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingReportStrategy.REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS; + +public class ZoneGroupConfigurationTest { + + @ParameterizedTest + @ValueSource(strings = {EntityCoordinates.ENTITY_ID_LATITUDE_ARGUMENT_KEY, EntityCoordinates.ENTITY_ID_LONGITUDE_ARGUMENT_KEY}) + void validateShouldThrowWhenUsedReservedEntityCoordinateNames(String name) { + var zoneGroupConfiguration = new ZoneGroupConfiguration("perimeter", REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS, false); + assertThatThrownBy(() -> zoneGroupConfiguration.validate(name)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Name '" + name + "' is reserved and cannot be used for zone group!"); + } + + @ParameterizedTest + @ValueSource(strings = " ") + @NullAndEmptySource + void validateShouldThrowWhenRelationCreationEnabledAndRelationTypeIsNullEmptyOrBlank(String relationType) { + var zoneGroupConfiguration = new ZoneGroupConfiguration("perimeter", REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS, true); + zoneGroupConfiguration.setRelationType(relationType); + assertThatThrownBy(() -> zoneGroupConfiguration.validate("allowedZonesGroup")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Relation type must be specified for 'allowedZonesGroup' zone group!"); + } + + @Test + void validateShouldThrowWhenRelationCreationEnabledAndDirectionIsNull() { + var zoneGroupConfiguration = new ZoneGroupConfiguration("perimeter", REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS, true); + zoneGroupConfiguration.setRelationType(EntityRelation.CONTAINS_TYPE); + zoneGroupConfiguration.setDirection(null); + assertThatThrownBy(() -> zoneGroupConfiguration.validate("allowedZonesGroup")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Relation direction must be specified for 'allowedZonesGroup' zone group!"); + } + + @Test + void validateShouldDoesNotThrowAnyExceptionWhenRelationCreationDisabledAndConfigValid() { + var zoneGroupConfiguration = new ZoneGroupConfiguration("perimeter", REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS, false); + assertThatCode(() -> zoneGroupConfiguration.validate("allowedZonesGroup")).doesNotThrowAnyException(); + } + + @Test + void validateShouldDoesNotThrowAnyExceptionWhenRelationCreationEnabledAndConfigValid() { + var zoneGroupConfiguration = new ZoneGroupConfiguration("perimeter", REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS, true); + zoneGroupConfiguration.setRelationType(EntityRelation.CONTAINS_TYPE); + zoneGroupConfiguration.setDirection(EntitySearchDirection.TO); + assertThatCode(() -> zoneGroupConfiguration.validate("allowedZonesGroup")).doesNotThrowAnyException(); + } + + @Test + void whenHasRelationQuerySourceCalled_shouldReturnTrueIfRelationQuerySourceConfigurationIsNotNull() { + var zoneGroupConfiguration = new ZoneGroupConfiguration("perimeter", REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS, false); + zoneGroupConfiguration.setRefDynamicSourceConfiguration(new RelationPathQueryDynamicSourceConfiguration()); + assertThat(zoneGroupConfiguration.hasRelationQuerySource()).isTrue(); + } + + @Test + void whenHasRelationQuerySourceCalled_shouldReturnFalseIfRelationQuerySourceConfigurationIsNull() { + var zoneGroupConfiguration = mock(ZoneGroupConfiguration.class); + assertThat(zoneGroupConfiguration.getRefDynamicSourceConfiguration()).isNull(); + assertThat(zoneGroupConfiguration.hasRelationQuerySource()).isFalse(); + } + + @Test + void whenHasRelationQuerySourceCalled_shouldReturnFalseIfCurrentOwnerSourceConfigured() { + var zoneGroupConfiguration = mock(ZoneGroupConfiguration.class); + zoneGroupConfiguration.setRefDynamicSourceConfiguration(new CurrentOwnerDynamicSourceConfiguration()); + assertThat(zoneGroupConfiguration.hasRelationQuerySource()).isFalse(); + } + + @Test + void validateToArgumentsMethodCallWithoutRefEntityId() { + var zoneGroupConfiguration = new ZoneGroupConfiguration("perimeter", REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS, false); + Argument zoneGroupArgument = zoneGroupConfiguration.toArgument(); + assertThat(zoneGroupArgument).isNotNull(); + assertThat(zoneGroupArgument.getRefEntityKey()).isEqualTo(new ReferencedEntityKey("perimeter", ArgumentType.ATTRIBUTE, AttributeScope.SERVER_SCOPE)); + } + +} diff --git a/common/data/src/test/java/org/thingsboard/server/common/data/msg/TbMsgTypeTest.java b/common/data/src/test/java/org/thingsboard/server/common/data/msg/TbMsgTypeTest.java index 563c6015ef..54238c8751 100644 --- a/common/data/src/test/java/org/thingsboard/server/common/data/msg/TbMsgTypeTest.java +++ b/common/data/src/test/java/org/thingsboard/server/common/data/msg/TbMsgTypeTest.java @@ -20,7 +20,6 @@ import org.junit.jupiter.api.Test; import java.util.List; import static org.assertj.core.api.Assertions.assertThat; -import static org.thingsboard.server.common.data.msg.TbMsgType.ALARM; import static org.thingsboard.server.common.data.msg.TbMsgType.ALARM_DELETE; import static org.thingsboard.server.common.data.msg.TbMsgType.DEDUPLICATION_TIMEOUT_SELF_MSG; import static org.thingsboard.server.common.data.msg.TbMsgType.DELAY_TIMEOUT_SELF_MSG; @@ -39,7 +38,6 @@ import static org.thingsboard.server.common.data.msg.TbMsgType.SEND_EMAIL; class TbMsgTypeTest { private static final List typesWithNullRuleNodeConnection = List.of( - ALARM, ALARM_DELETE, ENTITY_ASSIGNED_TO_EDGE, ENTITY_UNASSIGNED_FROM_EDGE, diff --git a/common/edge-api/src/main/java/org/thingsboard/edge/rpc/EdgeGrpcClient.java b/common/edge-api/src/main/java/org/thingsboard/edge/rpc/EdgeGrpcClient.java index 0eff7ad053..dbaef23431 100644 --- a/common/edge-api/src/main/java/org/thingsboard/edge/rpc/EdgeGrpcClient.java +++ b/common/edge-api/src/main/java/org/thingsboard/edge/rpc/EdgeGrpcClient.java @@ -136,7 +136,7 @@ public class EdgeGrpcClient implements EdgeRpcClient { .setConnectRequestMsg(ConnectRequestMsg.newBuilder() .setEdgeRoutingKey(edgeKey) .setEdgeSecret(edgeSecret) - .setEdgeVersion(EdgeVersion.V_4_2_0) + .setEdgeVersion(EdgeVersion.V_4_3_0) .setMaxInboundMessageSize(maxInboundMessageSize) .build()) .build()); diff --git a/common/edge-api/src/main/proto/edge.proto b/common/edge-api/src/main/proto/edge.proto index dbda462a99..5a0aeac977 100644 --- a/common/edge-api/src/main/proto/edge.proto +++ b/common/edge-api/src/main/proto/edge.proto @@ -44,6 +44,7 @@ enum EdgeVersion { V_4_0_0 = 10; V_4_1_0 = 11; V_4_2_0 = 12; + V_4_3_0 = 13; V_LATEST = 999; } @@ -133,6 +134,12 @@ message CalculatedFieldUpdateMsg{ string entity = 4; } +message AiModelUpdateMsg{ + UpdateMsgType msgType = 1; + int64 idMSB = 2; + int64 idLSB = 3; + string entity = 4; +} message EntityDataProto { int64 entityIdMSB = 1; @@ -441,6 +448,7 @@ message UplinkMsg { repeated RuleChainMetadataUpdateMsg ruleChainMetadataUpdateMsg = 24; repeated CalculatedFieldUpdateMsg calculatedFieldUpdateMsg = 25; repeated CalculatedFieldRequestMsg calculatedFieldRequestMsg = 26; + repeated AiModelUpdateMsg aiModelUpdateMsg = 27; } message UplinkResponseMsg { @@ -491,4 +499,5 @@ message DownlinkMsg { repeated NotificationTemplateUpdateMsg notificationTemplateUpdateMsg = 33; repeated OAuth2DomainUpdateMsg oAuth2DomainUpdateMsg = 34; repeated CalculatedFieldUpdateMsg calculatedFieldUpdateMsg = 35; + repeated AiModelUpdateMsg aiModelUpdateMsg = 36; } diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/data/BaseEntityData.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/data/BaseEntityData.java index 21b9ff0c4f..b75ce6a315 100644 --- a/common/edqs/src/main/java/org/thingsboard/server/edqs/data/BaseEntityData.java +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/data/BaseEntityData.java @@ -20,6 +20,7 @@ import lombok.Setter; import lombok.ToString; import org.thingsboard.server.common.data.AttributeScope; import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.edqs.fields.EntityFields; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.permission.QueryContext; @@ -139,11 +140,32 @@ public abstract class BaseEntityData implements EntityDa case "name" -> getEntityName(); case "ownerName" -> getOwnerName(); case "ownerType" -> getOwnerType(); + case "displayName" -> getDisplayName(); case "entityType" -> Optional.ofNullable(getEntityType()).map(EntityType::name).orElse(""); default -> fields.getAsString(name); }; } + public String getDisplayName(){ + return switch (getEntityType()) { + case DEVICE, ASSET -> StringUtils.isNotBlank(fields.getLabel()) ? fields.getLabel() : fields.getName(); + case USER -> { + boolean firstNameSet = StringUtils.isNotBlank(fields.getFirstName()); + boolean lastNameSet = StringUtils.isNotBlank(fields.getLastName()); + if(firstNameSet && lastNameSet) { + yield fields.getFirstName() + " " + fields.getLastName(); + } else if(firstNameSet) { + yield fields.getFirstName(); + } else if (lastNameSet) { + yield fields.getLastName(); + } else { + yield fields.getEmail(); + } + } + default -> fields.getName(); + }; + } + public String getEntityName() { return getFields().getName(); } diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/data/DeviceData.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/data/DeviceData.java index 3a3e5c5792..f47bfcb28a 100644 --- a/common/edqs/src/main/java/org/thingsboard/server/edqs/data/DeviceData.java +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/data/DeviceData.java @@ -18,6 +18,7 @@ package org.thingsboard.server.edqs.data; import lombok.ToString; import org.thingsboard.server.common.data.AttributeScope; import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.edqs.fields.DeviceFields; import org.thingsboard.server.common.data.query.EntityKeyType; import org.thingsboard.server.common.data.edqs.DataPoint; diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/state/KafkaEdqsStateService.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/state/KafkaEdqsStateService.java index 7e2e99e662..b34abe5363 100644 --- a/common/edqs/src/main/java/org/thingsboard/server/edqs/state/KafkaEdqsStateService.java +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/state/KafkaEdqsStateService.java @@ -43,7 +43,7 @@ import org.thingsboard.server.queue.edqs.EdqsConfig; import org.thingsboard.server.queue.edqs.EdqsExecutors; import org.thingsboard.server.queue.edqs.KafkaEdqsComponent; import org.thingsboard.server.queue.edqs.KafkaEdqsQueueFactory; -import org.thingsboard.server.queue.kafka.TbKafkaAdmin; +import org.thingsboard.server.queue.kafka.KafkaAdmin; import org.thingsboard.server.queue.kafka.TbKafkaConsumerTemplate; import java.util.HashMap; @@ -68,6 +68,7 @@ public class KafkaEdqsStateService implements EdqsStateService { private final EdqsExecutors edqsExecutors; private final EdqsMapper mapper; private final TopicService topicService; + private final KafkaAdmin kafkaAdmin; @Autowired @Lazy private EdqsProcessor edqsProcessor; @@ -86,7 +87,6 @@ public class KafkaEdqsStateService implements EdqsStateService { @Override public void init(PartitionedQueueConsumerManager> eventConsumer, List> otherConsumers) { versionsStore = new VersionsStore(config.getVersionsCacheTtl()); - TbKafkaAdmin queueAdmin = queueFactory.getEdqsQueueAdmin(); stateConsumer = PartitionedQueueConsumerManager.>create() .queueKey(new QueueKey(ServiceType.EDQS, config.getStateTopic())) .topic(topicService.buildTopicName(config.getStateTopic())) @@ -106,7 +106,7 @@ public class KafkaEdqsStateService implements EdqsStateService { consumer.commit(); }) .consumerCreator((config, tpi) -> queueFactory.createEdqsStateConsumer()) - .queueAdmin(queueAdmin) + .queueAdmin(queueFactory.getEdqsQueueAdmin()) .consumerExecutor(edqsExecutors.getConsumersExecutor()) .taskExecutor(edqsExecutors.getConsumerTaskExecutor()) .scheduler(edqsExecutors.getScheduler()) @@ -174,7 +174,7 @@ public class KafkaEdqsStateService implements EdqsStateService { // (because we need to be able to consume the same topic-partition by multiple instances) Map offsets = new HashMap<>(); try { - queueAdmin.getConsumerGroupOffsets(eventsToBackupKafkaConsumer.getGroupId()) + kafkaAdmin.getConsumerGroupOffsets(eventsToBackupKafkaConsumer.getGroupId()) .forEach((topicPartition, offsetAndMetadata) -> { offsets.put(topicPartition.topic(), offsetAndMetadata.offset()); }); diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/CalculatedFieldStatePartitionRestoreMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/CalculatedFieldStatePartitionRestoreMsg.java new file mode 100644 index 0000000000..b16e2adb85 --- /dev/null +++ b/common/message/src/main/java/org/thingsboard/server/common/msg/CalculatedFieldStatePartitionRestoreMsg.java @@ -0,0 +1,37 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.msg; + +import lombok.Data; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; + +@Data +public class CalculatedFieldStatePartitionRestoreMsg implements ToCalculatedFieldSystemMsg { + + private final TopicPartitionInfo partition; + + @Override + public TenantId getTenantId() { + return TenantId.SYS_TENANT_ID; + } + + @Override + public MsgType getMsgType() { + return MsgType.CF_STATE_PARTITION_RESTORE_MSG; + } + +} diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/MsgType.java b/common/message/src/main/java/org/thingsboard/server/common/msg/MsgType.java index f1c404ce16..85ce75c829 100644 --- a/common/message/src/main/java/org/thingsboard/server/common/msg/MsgType.java +++ b/common/message/src/main/java/org/thingsboard/server/common/msg/MsgType.java @@ -137,20 +137,25 @@ public enum MsgType { CF_CACHE_INIT_MSG, // Sent to init caches for CF actor; - CF_INIT_PROFILE_ENTITY_MSG, // Sent to init profile entities cache; - CF_INIT_MSG, // Sent to init particular calculated field; - CF_LINK_INIT_MSG, // Sent to init particular calculated field; CF_STATE_RESTORE_MSG, // Sent to restore particular calculated field entity state; + CF_STATE_PARTITION_RESTORE_MSG, CF_PARTITIONS_CHANGE_MSG, // Sent when cluster event occures; CF_ENTITY_LIFECYCLE_MSG, // Sent on CF/Device/Asset create/update/delete; + CF_ENTITY_ACTION_EVENT_MSG, + CF_ALARM_ACTION_MSG, CF_TELEMETRY_MSG, // Sent from queue to actor system; CF_LINKED_TELEMETRY_MSG, // Sent from queue to actor system; /* CF Manager Actor -> CF Entity actor */ CF_ENTITY_TELEMETRY_MSG, CF_ENTITY_INIT_CF_MSG, - CF_ENTITY_DELETE_MSG; + CF_ENTITY_DELETE_MSG, + + CF_RELATION_ACTION_MSG, + + CF_ARGUMENT_RESET_MSG, // Sent to reset argument; + CF_REEVALUATE_MSG; @Getter private final boolean ignoreOnStart; diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/TbMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/TbMsg.java index 1d8d9497a9..7c9f51a3ef 100644 --- a/common/message/src/main/java/org/thingsboard/server/common/msg/TbMsg.java +++ b/common/message/src/main/java/org/thingsboard/server/common/msg/TbMsg.java @@ -41,9 +41,6 @@ import java.util.Objects; import java.util.UUID; import java.util.concurrent.CopyOnWriteArrayList; -/** - * Created by ashvayka on 13.01.18. - */ @Data @Slf4j public final class TbMsg implements Serializable { @@ -500,11 +497,11 @@ public final class TbMsg implements Serializable { public String toString() { return "TbMsg.TbMsgBuilder(queueName=" + this.queueName + ", id=" + this.id + ", ts=" + this.ts + - ", type=" + this.type + ", internalType=" + this.internalType + ", originator=" + this.originator + - ", customerId=" + this.customerId + ", metaData=" + this.metaData + ", dataType=" + this.dataType + - ", data=" + this.data + ", ruleChainId=" + this.ruleChainId + ", ruleNodeId=" + this.ruleNodeId + - ", correlationId=" + this.correlationId + ", partition=" + this.partition + ", previousCalculatedFields=" + this.previousCalculatedFieldIds + - ", ctx=" + this.ctx + ", callback=" + this.callback + ")"; + ", type=" + this.type + ", internalType=" + this.internalType + ", originator=" + this.originator + + ", customerId=" + this.customerId + ", metaData=" + this.metaData + ", dataType=" + this.dataType + + ", data=" + this.data + ", ruleChainId=" + this.ruleChainId + ", ruleNodeId=" + this.ruleNodeId + + ", correlationId=" + this.correlationId + ", partition=" + this.partition + ", previousCalculatedFields=" + this.previousCalculatedFieldIds + + ", ctx=" + this.ctx + ", callback=" + this.callback + ")"; } } diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/ToCalculatedFieldSystemMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/ToCalculatedFieldSystemMsg.java index c05c0f121e..869ad659ac 100644 --- a/common/message/src/main/java/org/thingsboard/server/common/msg/ToCalculatedFieldSystemMsg.java +++ b/common/message/src/main/java/org/thingsboard/server/common/msg/ToCalculatedFieldSystemMsg.java @@ -16,12 +16,7 @@ package org.thingsboard.server.common.msg; import org.thingsboard.server.common.msg.aware.TenantAwareMsg; -import org.thingsboard.server.common.msg.queue.TbCallback; public interface ToCalculatedFieldSystemMsg extends TenantAwareMsg { - default TbCallback getCallback() { - return TbCallback.EMPTY; - } - } diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/aware/TenantAwareMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/aware/TenantAwareMsg.java index 4161940398..54ad749ceb 100644 --- a/common/message/src/main/java/org/thingsboard/server/common/msg/aware/TenantAwareMsg.java +++ b/common/message/src/main/java/org/thingsboard/server/common/msg/aware/TenantAwareMsg.java @@ -17,9 +17,14 @@ package org.thingsboard.server.common.msg.aware; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.msg.TbActorMsg; +import org.thingsboard.server.common.msg.queue.TbCallback; public interface TenantAwareMsg extends TbActorMsg { TenantId getTenantId(); - + + default TbCallback getCallback() { + return TbCallback.EMPTY; + } + } diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/plugin/ComponentLifecycleMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/plugin/ComponentLifecycleMsg.java index d57301fd10..23b9fe08e3 100644 --- a/common/message/src/main/java/org/thingsboard/server/common/msg/plugin/ComponentLifecycleMsg.java +++ b/common/message/src/main/java/org/thingsboard/server/common/msg/plugin/ComponentLifecycleMsg.java @@ -46,14 +46,15 @@ public class ComponentLifecycleMsg implements TenantAwareMsg, ToAllNodesMsg { private final String name; private final EntityId oldProfileId; private final EntityId profileId; + private final boolean ownerChanged; private final JsonNode info; public ComponentLifecycleMsg(TenantId tenantId, EntityId entityId, ComponentLifecycleEvent event) { - this(tenantId, entityId, event, null, null, null, null, null); + this(tenantId, entityId, event, null, null, null, null, false, null); } @Builder - private ComponentLifecycleMsg(TenantId tenantId, EntityId entityId, ComponentLifecycleEvent event, String oldName, String name, EntityId oldProfileId, EntityId profileId, JsonNode info) { + private ComponentLifecycleMsg(TenantId tenantId, EntityId entityId, ComponentLifecycleEvent event, String oldName, String name, EntityId oldProfileId, EntityId profileId, boolean ownerChanged, JsonNode info) { this.tenantId = tenantId; this.entityId = entityId; this.event = event; @@ -61,6 +62,7 @@ public class ComponentLifecycleMsg implements TenantAwareMsg, ToAllNodesMsg { this.name = name; this.oldProfileId = oldProfileId; this.profileId = profileId; + this.ownerChanged = ownerChanged; this.info = info; } diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/rule/engine/DeviceAttributesEventNotificationMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/rule/engine/DeviceAttributesEventNotificationMsg.java index efee3dcb97..f647714c63 100644 --- a/common/message/src/main/java/org/thingsboard/server/common/msg/rule/engine/DeviceAttributesEventNotificationMsg.java +++ b/common/message/src/main/java/org/thingsboard/server/common/msg/rule/engine/DeviceAttributesEventNotificationMsg.java @@ -23,16 +23,15 @@ import org.thingsboard.server.common.data.kv.AttributeKvEntry; import org.thingsboard.server.common.msg.MsgType; import org.thingsboard.server.common.msg.ToDeviceActorNotificationMsg; +import java.io.Serial; import java.util.HashSet; import java.util.List; import java.util.Set; -/** - * @author Andrew Shvayka - */ @Data public class DeviceAttributesEventNotificationMsg implements ToDeviceActorNotificationMsg { + @Serial private static final long serialVersionUID = 2422071590415277039L; private final TenantId tenantId; @@ -56,4 +55,5 @@ public class DeviceAttributesEventNotificationMsg implements ToDeviceActorNotifi public MsgType getMsgType() { return MsgType.DEVICE_ATTRIBUTES_UPDATE_TO_DEVICE_ACTOR_MSG; } + } diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/tools/SchedulerUtils.java b/common/message/src/main/java/org/thingsboard/server/common/msg/tools/SchedulerUtils.java index e0805c27aa..5b8a241a1a 100644 --- a/common/message/src/main/java/org/thingsboard/server/common/msg/tools/SchedulerUtils.java +++ b/common/message/src/main/java/org/thingsboard/server/common/msg/tools/SchedulerUtils.java @@ -60,16 +60,4 @@ public class SchedulerUtils { return LocalDate.now(UTC).with(TemporalAdjusters.firstDayOfNextMonth()).atStartOfDay(zoneId).toInstant().toEpochMilli(); } - public static long getStartOfNextNextMonth() { - return getStartOfNextNextMonth(UTC); - } - - public static long getStartOfNextNextMonth(ZoneId zoneId) { - return LocalDate.now(UTC).with(firstDayOfNextNextMonth()).atStartOfDay(zoneId).toInstant().toEpochMilli(); - } - - public static TemporalAdjuster firstDayOfNextNextMonth() { - return (temporal) -> temporal.with(DAY_OF_MONTH, 1).plus(2, MONTHS); - } - } diff --git a/common/proto/src/main/java/org/thingsboard/server/common/util/ProtoUtils.java b/common/proto/src/main/java/org/thingsboard/server/common/util/ProtoUtils.java index be407bd3db..26a64c7f8a 100644 --- a/common/proto/src/main/java/org/thingsboard/server/common/util/ProtoUtils.java +++ b/common/proto/src/main/java/org/thingsboard/server/common/util/ProtoUtils.java @@ -129,6 +129,7 @@ public class ProtoUtils { builder.setOldProfileIdMSB(msg.getOldProfileId().getId().getMostSignificantBits()); builder.setOldProfileIdLSB(msg.getOldProfileId().getId().getLeastSignificantBits()); } + builder.setOwnerChanged(msg.isOwnerChanged()); if (msg.getName() != null) { builder.setName(msg.getName()); } @@ -165,6 +166,7 @@ public class ProtoUtils { var profileType = EntityType.DEVICE.equals(entityId.getEntityType()) ? EntityType.DEVICE_PROFILE : EntityType.ASSET_PROFILE; builder.oldProfileId(EntityIdFactory.getByTypeAndUuid(profileType, new UUID(proto.getOldProfileIdMSB(), proto.getOldProfileIdLSB()))); } + builder.ownerChanged(proto.getOwnerChanged()); if (proto.hasInfo()) { builder.info(JacksonUtil.toJsonNode(proto.getInfo())); } @@ -1377,6 +1379,18 @@ public class ProtoUtils { return TbMsg.fromProto(queueName, getTbMsgProto(ruleEngineMsg), callback); } + public static TransportProtos.EntityIdProto toProto(EntityId entityId) { + return TransportProtos.EntityIdProto.newBuilder() + .setEntityIdMSB(getMsb(entityId)) + .setEntityIdLSB(getLsb(entityId)) + .setType(toProto(entityId.getEntityType())) + .build(); + } + + public static EntityId fromProto(TransportProtos.EntityIdProto entityIdProto) { + return EntityIdFactory.getByTypeAndUuid(fromProto(entityIdProto.getType()), new UUID(entityIdProto.getEntityIdMSB(), entityIdProto.getEntityIdLSB())); + } + private static boolean isNotNull(Object obj) { return obj != null; } diff --git a/common/proto/src/main/proto/queue.proto b/common/proto/src/main/proto/queue.proto index 8372753df4..557eda324f 100644 --- a/common/proto/src/main/proto/queue.proto +++ b/common/proto/src/main/proto/queue.proto @@ -62,7 +62,7 @@ enum EntityTypeProto { MOBILE_APP = 37; MOBILE_APP_BUNDLE = 38; CALCULATED_FIELD = 39; - CALCULATED_FIELD_LINK = 40; + // CALCULATED_FIELD_LINK = 40; - was removed in 4.3 JOB = 41; ADMIN_SETTINGS = 42; AI_MODEL = 43; @@ -82,6 +82,12 @@ enum ApiUsageRecordKeyProto { INACTIVE_DEVICES = 10; } +message EntityIdProto { + int64 entityIdMSB = 1; + int64 entityIdLSB = 2; + EntityTypeProto type = 4; +} + /** * Service Discovery Data Structures; */ @@ -882,6 +888,7 @@ message SingleValueArgumentProto { string argName = 1; TsValueProto value = 2; int64 version = 3; + EntityIdProto entityId = 4; } message TsDoubleValProto { @@ -896,11 +903,37 @@ message TsRollingArgumentProto { repeated TsDoubleValProto tsValue = 4; } +message GeofencingZoneProto { + EntityIdProto zoneId = 1; + int64 ts = 2; + string perimeterDefinition = 3; + int64 version = 4; + optional bool inside = 5; +} + +message GeofencingArgumentProto { + string argName = 1; + repeated GeofencingZoneProto zones = 2; +} + +message ArgumentIntervalProto { + string argName = 1; + int64 startTs = 2; + int64 endTs = 3; + int64 lastArgsRefreshTs = 4; + int64 lastMetricsEvalTs = 5; +} + message CalculatedFieldStateProto { CalculatedFieldEntityCtxIdProto id = 1; string type = 2; repeated SingleValueArgumentProto singleValueArguments = 3; repeated TsRollingArgumentProto rollingValueArguments = 4; + repeated GeofencingArgumentProto geofencingArguments = 5; + AlarmStateProto alarmState = 6; + int64 lastArgsUpdateTs = 7; + int64 lastMetricsEvalTs = 8; + repeated ArgumentIntervalProto aggregationArguments = 9; } //Used to report session state to tb-Service and persist this state in the cache on the tb-Service level. @@ -1253,6 +1286,8 @@ enum ComponentLifecycleEvent { DELETED = 6; FAILED = 7; DEACTIVATED = 8; + RELATION_UPDATED = 9; + RELATION_DELETED = 10; } message ComponentLifecycleMsgProto { @@ -1270,6 +1305,7 @@ message ComponentLifecycleMsgProto { int64 profileIdMSB = 11; int64 profileIdLSB = 12; optional string info = 13; + bool ownerChanged = 100; } message EdgeEventMsgProto { @@ -1701,6 +1737,7 @@ message ToEdgeEventNotificationMsg { message ToCalculatedFieldMsg { CalculatedFieldTelemetryMsgProto telemetryMsg = 1; CalculatedFieldLinkedTelemetryMsgProto linkedTelemetryMsg = 2; + EntityActionEventProto eventMsg = 3; } message ToCalculatedFieldNotificationMsg { @@ -1874,3 +1911,22 @@ message JobStatsMsg { message TaskResultProto { string value = 1; } + +message EntityActionEventProto { + EntityIdProto tenantId = 1; + EntityIdProto entityId = 2; + string entity = 3; + string action = 4; +} + +message AlarmStateProto { + repeated AlarmRuleStateProto createRuleStates = 1; + AlarmRuleStateProto clearRuleState = 2; +} + +message AlarmRuleStateProto { + string severity = 1; + int64 eventCount = 2; + int64 firstEventTs = 3; + int64 lastEventTs = 4; +} diff --git a/common/proto/src/test/java/org/thingsboard/server/common/util/ProtoUtilsTest.java b/common/proto/src/test/java/org/thingsboard/server/common/util/ProtoUtilsTest.java index a46e489268..0f4733cafb 100644 --- a/common/proto/src/test/java/org/thingsboard/server/common/util/ProtoUtilsTest.java +++ b/common/proto/src/test/java/org/thingsboard/server/common/util/ProtoUtilsTest.java @@ -22,6 +22,7 @@ import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; import org.junit.jupiter.params.provider.NullAndEmptySource; import org.junit.jupiter.params.provider.ValueSource; import org.thingsboard.common.util.JacksonUtil; @@ -43,6 +44,7 @@ import org.thingsboard.server.common.data.edge.EdgeEventType; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.EntityIdFactory; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.kv.AttributeKey; @@ -344,4 +346,24 @@ class ProtoUtilsTest { assertThat(toDeviceRpcRequestActorMsg.getMsg()).isEqualTo(request); } + @ParameterizedTest + @EnumSource(EntityType.class) + void testEntityIdProto_toProto_fromProto(EntityType entityType) { + UUID uuid = UUID.fromString("51a514d7-ea8f-496d-b567-f6e76f0f9b83"); + + EntityId original = EntityIdFactory.getByTypeAndUuid(entityType, uuid); + assertThat(original).isNotNull(); + + // toProto + TransportProtos.EntityIdProto proto = ProtoUtils.toProto(original); + assertThat(proto).isNotNull(); + assertThat(proto.getType().getNumber()).isEqualTo(entityType.getProtoNumber()); + assertThat(proto.getEntityIdMSB()).isEqualTo(uuid.getMostSignificantBits()); + assertThat(proto.getEntityIdLSB()).isEqualTo(uuid.getLeastSignificantBits()); + + // fromProto + EntityId restored = ProtoUtils.fromProto(proto); + assertThat(restored).isNotNull().isEqualTo(original); + } + } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/RuleEngineTbQueueAdminFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/RuleEngineTbQueueAdminFactory.java index 6d78fa8b4f..ecb2a2b771 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/RuleEngineTbQueueAdminFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/RuleEngineTbQueueAdminFactory.java @@ -43,7 +43,7 @@ public class RuleEngineTbQueueAdminFactory { return new TbQueueAdmin() { @Override - public void createTopicIfNotExists(String topic, String properties) { + public void createTopicIfNotExists(String topic, String properties, boolean force) { } @Override diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/common/AbstractTbQueueConsumerTemplate.java b/common/queue/src/main/java/org/thingsboard/server/queue/common/AbstractTbQueueConsumerTemplate.java index 7e7de64a5c..04fe2443ef 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/common/AbstractTbQueueConsumerTemplate.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/common/AbstractTbQueueConsumerTemplate.java @@ -194,6 +194,11 @@ public abstract class AbstractTbQueueConsumerTemplate i abstract protected void doUnsubscribe(); + @Override + public Set getPartitions() { + return partitions; + } + @Override public List getFullTopicNames() { if (partitions == null) { diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/MainQueueConsumerManager.java b/common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/MainQueueConsumerManager.java index db5bac7170..b300e8c1b2 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/MainQueueConsumerManager.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/MainQueueConsumerManager.java @@ -25,6 +25,7 @@ import org.thingsboard.server.queue.TbQueueConsumer; import org.thingsboard.server.queue.TbQueueMsg; import org.thingsboard.server.queue.common.consumer.TbQueueConsumerManagerTask.UpdateConfigTask; import org.thingsboard.server.queue.common.consumer.TbQueueConsumerManagerTask.UpdatePartitionsTask; +import org.thingsboard.server.queue.common.consumer.TbQueueConsumerTask.ConsumerKey; import org.thingsboard.server.queue.kafka.TbKafkaConsumerTemplate; import java.util.Collection; @@ -218,7 +219,7 @@ public class MainQueueConsumerManager consumer) { + private void consumerLoop(ConsumerKey consumerKey, TbQueueConsumer consumer) { try { while (!stopped && !consumer.isStopped()) { try { @@ -250,7 +251,7 @@ public class MainQueueConsumerManager msgs, TbQueueConsumer consumer, Object consumerKey, C config) throws Exception { + protected void processMsgs(List msgs, TbQueueConsumer consumer, ConsumerKey consumerKey, C config) throws Exception { log.trace("Processing {} messages", msgs.size()); msgPackProcessor.process(msgs, consumer, consumerKey, config); log.trace("Processed {} messages", msgs.size()); @@ -273,7 +274,7 @@ public class MainQueueConsumerManager { - void process(List msgs, TbQueueConsumer consumer, Object consumerKey, C config) throws Exception; + void process(List msgs, TbQueueConsumer consumer, ConsumerKey consumerKey, C config) throws Exception; } public interface ConsumerWrapper { @@ -285,6 +286,7 @@ public class MainQueueConsumerManager { + private final Map> consumers = new HashMap<>(); @Override @@ -307,8 +309,7 @@ public class MainQueueConsumerManager partitions, Consumer onStop, Function startOffsetProvider) { partitions.forEach(tpi -> { - Integer partitionId = tpi.getPartition().orElse(-1); - String key = queueKey + "-" + partitionId; + ConsumerKey key = new ConsumerKey(queueKey, tpi); Runnable callback = onStop != null ? () -> onStop.accept(tpi) : null; TbQueueConsumerTask consumer = new TbQueueConsumerTask<>(key, () -> { @@ -328,9 +329,11 @@ public class MainQueueConsumerManager> getConsumers() { return consumers.values(); } + } class SingleConsumerWrapper implements ConsumerWrapper { + private TbQueueConsumerTask consumer; @Override @@ -346,7 +349,7 @@ public class MainQueueConsumerManager(queueKey, () -> consumerCreator.apply(config, null), null); // no partitionId passed + consumer = new TbQueueConsumerTask<>(new ConsumerKey(queueKey, null), () -> consumerCreator.apply(config, null), null); // no partitionId passed } consumer.subscribe(partitions); if (!consumer.isRunning()) { @@ -361,5 +364,7 @@ public class MainQueueConsumerManager { @Getter - private final Object key; + private final ConsumerKey key; private volatile TbQueueConsumer consumer; private volatile Supplier> consumerSupplier; @Getter @@ -41,7 +41,7 @@ public class TbQueueConsumerTask { @Setter private Future task; - public TbQueueConsumerTask(Object key, Supplier> consumerSupplier, Runnable callback) { + public TbQueueConsumerTask(ConsumerKey key, Supplier> consumerSupplier, Runnable callback) { this.key = key; this.consumer = null; this.consumerSupplier = consumerSupplier; @@ -97,4 +97,18 @@ public class TbQueueConsumerTask { return task != null; } + public record ConsumerKey(Object queueKey, TopicPartitionInfo partition) { + + @Override + public String toString() { + if (partition != null) { + Integer partitionId = partition.getPartition().orElse(-1); + return queueKey + "-" + partitionId; + } else { + return queueKey.toString(); + } + } + + } + } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/common/state/DefaultQueueStateService.java b/common/queue/src/main/java/org/thingsboard/server/queue/common/state/DefaultQueueStateService.java index be379fb76d..6bcc87af38 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/common/state/DefaultQueueStateService.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/common/state/DefaultQueueStateService.java @@ -15,10 +15,15 @@ */ package org.thingsboard.server.queue.common.state; +import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.queue.TbQueueMsg; import org.thingsboard.server.queue.common.consumer.PartitionedQueueConsumerManager; +import org.thingsboard.server.queue.discovery.QueueKey; import java.util.Collections; +import java.util.Set; + +import static org.thingsboard.server.common.msg.queue.TopicPartitionInfo.withTopic; public class DefaultQueueStateService extends QueueStateService { @@ -26,4 +31,18 @@ public class DefaultQueueStateService partitions, RestoreCallback callback) { + if (callback != null) { + for (TopicPartitionInfo partition : partitions) { + callback.onPartitionRestored(partition); + } + callback.onAllPartitionsRestored(); + } + eventConsumer.addPartitions(partitions); + for (PartitionedQueueConsumerManager consumer : otherConsumers) { + consumer.addPartitions(withTopic(partitions, consumer.getTopic())); + } + } + } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/common/state/KafkaQueueStateService.java b/common/queue/src/main/java/org/thingsboard/server/queue/common/state/KafkaQueueStateService.java index 2a38c9a86c..60cfe4c98f 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/common/state/KafkaQueueStateService.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/common/state/KafkaQueueStateService.java @@ -50,7 +50,7 @@ public class KafkaQueueStateService } @Override - protected void addPartitions(QueueKey queueKey, Set partitions, Runnable whenAllProcessed) { + protected void addPartitions(QueueKey queueKey, Set partitions, RestoreCallback callback) { Map eventsStartOffsets = eventsStartOffsetsProvider != null ? eventsStartOffsetsProvider.get() : null; // remembering the offsets before subscribing to states Set statePartitions = withTopic(partitions, stateConsumer.getTopic()); @@ -61,10 +61,13 @@ public class KafkaQueueStateService try { partitionsInProgress.remove(statePartition); log.info("Finished partition {} (still in progress: {})", statePartition, partitionsInProgress); + if (callback != null) { + callback.onPartitionRestored(statePartition); + } if (partitionsInProgress.isEmpty()) { log.info("All partitions processed"); - if (whenAllProcessed != null) { - whenAllProcessed.run(); + if (callback != null) { + callback.onAllPartitionsRestored(); } } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/common/state/QueueStateService.java b/common/queue/src/main/java/org/thingsboard/server/queue/common/state/QueueStateService.java index e58d5eb036..e98e8dd7e4 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/common/state/QueueStateService.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/common/state/QueueStateService.java @@ -49,7 +49,7 @@ public abstract class QueueStateService newPartitions, Runnable whenAllProcessed) { + public void update(QueueKey queueKey, Set newPartitions, RestoreCallback callback) { newPartitions = withTopic(newPartitions, eventConsumer.getTopic()); var writeLock = partitionsLock.writeLock(); writeLock.lock(); @@ -71,23 +71,15 @@ public abstract class QueueStateService partitions, Runnable whenAllProcessed) { - if (whenAllProcessed != null) { - whenAllProcessed.run(); - } - eventConsumer.addPartitions(partitions); - for (PartitionedQueueConsumerManager consumer : otherConsumers) { - consumer.addPartitions(withTopic(partitions, consumer.getTopic())); - } - } + protected abstract void addPartitions(QueueKey queueKey, Set partitions, RestoreCallback callback) ; protected void removePartitions(QueueKey queueKey, Set partitions) { eventConsumer.removePartitions(partitions); @@ -122,4 +114,12 @@ public abstract class QueueStateService adminClient; + private final CachedValue> topics; + + public KafkaAdmin(@Lazy TbKafkaSettings settings) { + this.settings = settings; + this.adminClient = LazyInitializer.builder() + .setInitializer(() -> AdminClient.create(settings.toAdminProps())) + .get(); + this.topics = new CachedValue<>(() -> { + Set topics = ConcurrentHashMap.newKeySet(); + topics.addAll(listTopics()); + return topics; + }, topicsCacheTtlMs); + } + + public void createTopicIfNotExists(String topic, Map properties, boolean force) { + Set topics = getTopics(); + if (!force && topics.contains(topic)) { + log.trace("Topic {} already present in cache", topic); + return; + } + + log.debug("Creating topic {} with properties {}", topic, properties); + String numPartitionsStr = properties.remove(TbKafkaTopicConfigs.NUM_PARTITIONS_SETTING); + int partitions = numPartitionsStr != null ? Integer.parseInt(numPartitionsStr) : 1; + NewTopic newTopic = new NewTopic(topic, partitions, settings.getReplicationFactor()).configs(properties); + + try { + getClient().createTopics(List.of(newTopic)).all().get(requestTimeoutMs, TimeUnit.MILLISECONDS); + topics.add(topic); + } catch (ExecutionException ee) { + log.trace("Failed to create topic {} with properties {}", topic, properties, ee); + if (ee.getCause() instanceof TopicExistsException) { + //do nothing + } else { + log.warn("[{}] Failed to create topic", topic, ee); + throw new RuntimeException(ee); + } + } catch (Exception e) { + log.warn("[{}] Failed to create topic", topic, e); + throw new RuntimeException(e); + } + } + + public void deleteTopic(String topic) { + log.debug("Deleting topic {}", topic); + try { + getClient().deleteTopics(List.of(topic)).all().get(requestTimeoutMs, TimeUnit.MILLISECONDS); + } catch (Exception e) { + log.error("Failed to delete kafka topic [{}].", topic, e); + } + } + + private Set getTopics() { + return topics.get(); + } + + public Set listTopics() { + try { + Set topics = getClient().listTopics().names().get(requestTimeoutMs, TimeUnit.MILLISECONDS); + log.trace("Listed topics: {}", topics); + return topics; + } catch (Exception e) { + log.error("Failed to get all topics.", e); + return Collections.emptySet(); + } + } + + public Map getTotalLagForGroupsBulk(Set groupIds) { + Map result = new HashMap<>(); + for (String groupId : groupIds) { + result.put(groupId, getTotalConsumerGroupLag(groupId)); + } + return result; + } + + public long getTotalConsumerGroupLag(String groupId) { + try { + Map committedOffsets = getConsumerGroupOffsets(groupId); + if (committedOffsets.isEmpty()) { + return 0L; + } + + Map latestOffsetsSpec = committedOffsets.keySet().stream() + .collect(Collectors.toMap(tp -> tp, tp -> OffsetSpec.latest())); + + Map endOffsets = + getClient().listOffsets(latestOffsetsSpec).all().get(requestTimeoutMs, TimeUnit.MILLISECONDS); + + return committedOffsets.entrySet().stream() + .mapToLong(entry -> { + TopicPartition tp = entry.getKey(); + long committed = entry.getValue().offset(); + long end = endOffsets.getOrDefault(tp, + new ListOffsetsResult.ListOffsetsResultInfo(0L, 0L, Optional.empty())).offset(); + return end - committed; + }).sum(); + + } catch (Exception e) { + log.error("Failed to get total lag for consumer group: {}", groupId, e); + return 0L; + } + } + + @SneakyThrows + public Map getConsumerGroupOffsets(String groupId) { + return getClient().listConsumerGroupOffsets(groupId).partitionsToOffsetAndMetadata().get(requestTimeoutMs, TimeUnit.MILLISECONDS); + } + + /** + * Sync offsets from a fat group to a single-partition group + * Migration back from single-partition consumer to a fat group is not supported + * TODO: The best possible approach to synchronize the offsets is to do the synchronization as a part of the save Queue parameters with stop all consumers + * */ + public void syncOffsets(String fatGroupId, String newGroupId, Integer partitionId) { + try { + log.info("syncOffsets [{}][{}][{}]", fatGroupId, newGroupId, partitionId); + if (partitionId == null) { + return; + } + syncOffsetsUnsafe(fatGroupId, newGroupId, "." + partitionId); + } catch (Exception e) { + log.warn("Failed to syncOffsets from {} to {} partitionId {}", fatGroupId, newGroupId, partitionId, e); + } + } + + public void syncOffsetsUnsafe(String fatGroupId, String newGroupId, String topicSuffix) throws ExecutionException, InterruptedException, TimeoutException { + Map oldOffsets = getConsumerGroupOffsets(fatGroupId); + if (oldOffsets.isEmpty()) { + return; + } + + for (var consumerOffset : oldOffsets.entrySet()) { + var tp = consumerOffset.getKey(); + if (!tp.topic().endsWith(topicSuffix)) { + continue; + } + var om = consumerOffset.getValue(); + Map newOffsets = getConsumerGroupOffsets(newGroupId); + + var existingOffset = newOffsets.get(tp); + if (existingOffset == null) { + log.info("[{}] topic offset does not exists in the new node group {}, all found offsets {}", tp, newGroupId, newOffsets); + } else if (existingOffset.offset() >= om.offset()) { + log.info("[{}] topic offset {} >= than old node group offset {}", tp, existingOffset.offset(), om.offset()); + break; + } else { + log.info("[{}] SHOULD alter topic offset [{}] less than old node group offset [{}]", tp, existingOffset.offset(), om.offset()); + } + getClient().alterConsumerGroupOffsets(newGroupId, Map.of(tp, om)).all().get(requestTimeoutMs, TimeUnit.MILLISECONDS); + log.info("[{}] altered new consumer groupId {}", tp, newGroupId); + break; + } + } + + public boolean isTopicEmpty(String topic) { + return areAllTopicsEmpty(Set.of(topic)); + } + + public boolean areAllTopicsEmpty(Set topics) { + try { + List existingTopics = getTopics().stream().filter(topics::contains).toList(); + if (existingTopics.isEmpty()) { + return true; + } + + List allPartitions = getClient().describeTopics(existingTopics).allTopicNames().get(requestTimeoutMs, TimeUnit.MILLISECONDS) + .entrySet().stream() + .flatMap(entry -> { + String topic = entry.getKey(); + TopicDescription topicDescription = entry.getValue(); + return topicDescription.partitions().stream().map(partitionInfo -> new TopicPartition(topic, partitionInfo.partition())); + }) + .toList(); + + Map beginningOffsets = getClient().listOffsets(allPartitions.stream() + .collect(Collectors.toMap(partition -> partition, partition -> OffsetSpec.earliest()))).all().get(requestTimeoutMs, TimeUnit.MILLISECONDS); + Map endOffsets = getClient().listOffsets(allPartitions.stream() + .collect(Collectors.toMap(partition -> partition, partition -> OffsetSpec.latest()))).all().get(requestTimeoutMs, TimeUnit.MILLISECONDS); + + for (TopicPartition partition : allPartitions) { + long beginningOffset = beginningOffsets.get(partition).offset(); + long endOffset = endOffsets.get(partition).offset(); + + if (beginningOffset != endOffset) { + log.debug("Partition [{}] of topic [{}] is not empty. Returning false.", partition.partition(), partition.topic()); + return false; + } + } + return true; + } catch (Exception e) { + log.error("Failed to check if topics [{}] empty.", topics, e); + return false; + } + } + + public void deleteConsumerGroup(String consumerGroupId) { + try { + getClient().deleteConsumerGroups(List.of(consumerGroupId)).all().get(requestTimeoutMs, TimeUnit.MILLISECONDS); + } catch (Exception e) { + log.warn("Failed to delete consumer group {}", consumerGroupId, e); + } + } + + public AdminClient getClient() { + try { + return adminClient.get(); + } catch (ConcurrentException e) { + throw new RuntimeException("Failed to initialize Kafka admin client", e); + } + } + + @PreDestroy + private void destroy() throws Exception { + if (adminClient.isInitialized()) { + adminClient.get().close(); + } + } + +} diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaAdmin.java b/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaAdmin.java index 47a7ef13e3..65e9d5e4c4 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaAdmin.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaAdmin.java @@ -16,31 +16,12 @@ package org.thingsboard.server.queue.kafka; import lombok.Getter; -import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; -import org.apache.kafka.clients.admin.CreateTopicsResult; -import org.apache.kafka.clients.admin.ListOffsetsResult; -import org.apache.kafka.clients.admin.NewTopic; -import org.apache.kafka.clients.admin.OffsetSpec; -import org.apache.kafka.clients.admin.TopicDescription; -import org.apache.kafka.clients.consumer.OffsetAndMetadata; -import org.apache.kafka.common.TopicPartition; -import org.apache.kafka.common.errors.TopicExistsException; import org.thingsboard.server.queue.TbEdgeQueueAdmin; import org.thingsboard.server.queue.TbQueueAdmin; import org.thingsboard.server.queue.util.PropertyUtils; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; import java.util.Map; -import java.util.Optional; -import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; -import java.util.stream.Collectors; /** * Created by ashvayka on 24.09.18. @@ -52,251 +33,42 @@ public class TbKafkaAdmin implements TbQueueAdmin, TbEdgeQueueAdmin { private final Map topicConfigs; @Getter private final int numPartitions; - private volatile Set topics; - - private final short replicationFactor; public TbKafkaAdmin(TbKafkaSettings settings, Map topicConfigs) { this.settings = settings; this.topicConfigs = topicConfigs; - String numPartitionsStr = topicConfigs.get(TbKafkaTopicConfigs.NUM_PARTITIONS_SETTING); if (numPartitionsStr != null) { numPartitions = Integer.parseInt(numPartitionsStr); } else { numPartitions = 1; } - replicationFactor = settings.getReplicationFactor(); } @Override - public void createTopicIfNotExists(String topic, String properties) { - Set topics = getTopics(); - if (topics.contains(topic)) { - return; - } - try { - Map configs = PropertyUtils.getProps(topicConfigs, properties); - configs.remove(TbKafkaTopicConfigs.NUM_PARTITIONS_SETTING); - NewTopic newTopic = new NewTopic(topic, numPartitions, replicationFactor).configs(configs); - createTopic(newTopic).values().get(topic).get(); - topics.add(topic); - } catch (ExecutionException ee) { - if (ee.getCause() instanceof TopicExistsException) { - //do nothing - } else { - log.warn("[{}] Failed to create topic", topic, ee); - throw new RuntimeException(ee); - } - } catch (Exception e) { - log.warn("[{}] Failed to create topic", topic, e); - throw new RuntimeException(e); - } + public void createTopicIfNotExists(String topic, String properties, boolean force) { + settings.getAdmin().createTopicIfNotExists(topic, PropertyUtils.getProps(topicConfigs, properties), force); } @Override public void deleteTopic(String topic) { - Set topics = getTopics(); - if (topics.remove(topic)) { - settings.getAdminClient().deleteTopics(Collections.singletonList(topic)); - } else { - try { - if (settings.getAdminClient().listTopics().names().get().contains(topic)) { - settings.getAdminClient().deleteTopics(Collections.singletonList(topic)); - } else { - log.warn("Kafka topic [{}] does not exist.", topic); - } - } catch (InterruptedException | ExecutionException e) { - log.error("Failed to delete kafka topic [{}].", topic, e); - } - } - } - - private Set getTopics() { - if (topics == null) { - synchronized (this) { - if (topics == null) { - topics = ConcurrentHashMap.newKeySet(); - try { - topics.addAll(settings.getAdminClient().listTopics().names().get()); - } catch (InterruptedException | ExecutionException e) { - log.error("Failed to get all topics.", e); - } - } - } - } - return topics; - } - - public Set getAllTopics() { - try { - return settings.getAdminClient().listTopics().names().get(); - } catch (InterruptedException | ExecutionException e) { - log.error("Failed to get all topics.", e); - } - return null; - } - - public CreateTopicsResult createTopic(NewTopic topic) { - return settings.getAdminClient().createTopics(Collections.singletonList(topic)); + settings.getAdmin().deleteTopic(topic); } @Override public void destroy() { } - /** - * Sync offsets from a fat group to a single-partition group - * Migration back from single-partition consumer to a fat group is not supported - * TODO: The best possible approach to synchronize the offsets is to do the synchronization as a part of the save Queue parameters with stop all consumers - * */ - public void syncOffsets(String fatGroupId, String newGroupId, Integer partitionId) { - try { - log.info("syncOffsets [{}][{}][{}]", fatGroupId, newGroupId, partitionId); - if (partitionId == null) { - return; - } - syncOffsetsUnsafe(fatGroupId, newGroupId, "." + partitionId); - } catch (Exception e) { - log.warn("Failed to syncOffsets from {} to {} partitionId {}", fatGroupId, newGroupId, partitionId, e); - } - } - /** * Sync edge notifications offsets from a fat group to a single group per edge * */ public void syncEdgeNotificationsOffsets(String fatGroupId, String newGroupId) { try { log.info("syncEdgeNotificationsOffsets [{}][{}]", fatGroupId, newGroupId); - syncOffsetsUnsafe(fatGroupId, newGroupId, newGroupId); + settings.getAdmin().syncOffsetsUnsafe(fatGroupId, newGroupId, newGroupId); } catch (Exception e) { log.warn("Failed to syncEdgeNotificationsOffsets from {} to {}", fatGroupId, newGroupId, e); } } - @Override - public void deleteConsumerGroup(String consumerGroupId) { - try { - settings.getAdminClient().deleteConsumerGroups(Collections.singletonList(consumerGroupId)); - } catch (Exception e) { - log.warn("Failed to delete consumer group {}", consumerGroupId, e); - } - } - - void syncOffsetsUnsafe(String fatGroupId, String newGroupId, String topicSuffix) throws ExecutionException, InterruptedException, TimeoutException { - Map oldOffsets = getConsumerGroupOffsets(fatGroupId); - if (oldOffsets.isEmpty()) { - return; - } - - for (var consumerOffset : oldOffsets.entrySet()) { - var tp = consumerOffset.getKey(); - if (!tp.topic().endsWith(topicSuffix)) { - continue; - } - var om = consumerOffset.getValue(); - Map newOffsets = getConsumerGroupOffsets(newGroupId); - - var existingOffset = newOffsets.get(tp); - if (existingOffset == null) { - log.info("[{}] topic offset does not exists in the new node group {}, all found offsets {}", tp, newGroupId, newOffsets); - } else if (existingOffset.offset() >= om.offset()) { - log.info("[{}] topic offset {} >= than old node group offset {}", tp, existingOffset.offset(), om.offset()); - break; - } else { - log.info("[{}] SHOULD alter topic offset [{}] less than old node group offset [{}]", tp, existingOffset.offset(), om.offset()); - } - settings.getAdminClient().alterConsumerGroupOffsets(newGroupId, Map.of(tp, om)).all().get(10, TimeUnit.SECONDS); - log.info("[{}] altered new consumer groupId {}", tp, newGroupId); - break; - } - } - - @SneakyThrows - public Map getConsumerGroupOffsets(String groupId) { - return settings.getAdminClient().listConsumerGroupOffsets(groupId).partitionsToOffsetAndMetadata().get(10, TimeUnit.SECONDS); - } - - public boolean isTopicEmpty(String topic) { - return areAllTopicsEmpty(Set.of(topic)); - } - - public boolean areAllTopicsEmpty(Set topics) { - try { - List existingTopics = getTopics().stream().filter(topics::contains).toList(); - if (existingTopics.isEmpty()) { - return true; - } - - List allPartitions = settings.getAdminClient().describeTopics(existingTopics).topicNameValues().entrySet().stream() - .flatMap(entry -> { - String topic = entry.getKey(); - TopicDescription topicDescription; - try { - topicDescription = entry.getValue().get(); - } catch (InterruptedException | ExecutionException e) { - throw new RuntimeException(e); - } - return topicDescription.partitions().stream().map(partitionInfo -> new TopicPartition(topic, partitionInfo.partition())); - }) - .toList(); - - Map beginningOffsets = settings.getAdminClient().listOffsets(allPartitions.stream() - .collect(Collectors.toMap(partition -> partition, partition -> OffsetSpec.earliest()))).all().get(); - Map endOffsets = settings.getAdminClient().listOffsets(allPartitions.stream() - .collect(Collectors.toMap(partition -> partition, partition -> OffsetSpec.latest()))).all().get(); - - for (TopicPartition partition : allPartitions) { - long beginningOffset = beginningOffsets.get(partition).offset(); - long endOffset = endOffsets.get(partition).offset(); - - if (beginningOffset != endOffset) { - log.debug("Partition [{}] of topic [{}] is not empty. Returning false.", partition.partition(), partition.topic()); - return false; - } - } - return true; - } catch (InterruptedException | ExecutionException e) { - log.error("Failed to check if topics [{}] empty.", topics, e); - return false; - } - } - - public Map getTotalLagForGroupsBulk(Set groupIds) { - Map result = new HashMap<>(); - for (String groupId : groupIds) { - result.put(groupId, getTotalConsumerGroupLag(groupId)); - } - return result; - } - - public long getTotalConsumerGroupLag(String groupId) { - try { - Map committedOffsets = getConsumerGroupOffsets(groupId); - if (committedOffsets.isEmpty()) { - return 0L; - } - - Map latestOffsetsSpec = committedOffsets.keySet().stream() - .collect(Collectors.toMap(tp -> tp, tp -> OffsetSpec.latest())); - - Map endOffsets = - settings.getAdminClient().listOffsets(latestOffsetsSpec) - .all().get(10, TimeUnit.SECONDS); - - return committedOffsets.entrySet().stream() - .mapToLong(entry -> { - TopicPartition tp = entry.getKey(); - long committed = entry.getValue().offset(); - long end = endOffsets.getOrDefault(tp, - new ListOffsetsResult.ListOffsetsResultInfo(0L, 0L, Optional.empty())).offset(); - return end - committed; - }).sum(); - - } catch (Exception e) { - log.error("Failed to get total lag for consumer group: {}", groupId, e); - return 0L; - } - } - } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaConsumerStatisticConfig.java b/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaConsumerStatisticConfig.java index d44f9ee700..12a3ea8fa6 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaConsumerStatisticConfig.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaConsumerStatisticConfig.java @@ -19,11 +19,11 @@ import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.stereotype.Component; +import org.thingsboard.server.queue.util.TbKafkaComponent; @Component -@ConditionalOnProperty(prefix = "queue", value = "type", havingValue = "kafka") +@TbKafkaComponent @Getter @AllArgsConstructor @NoArgsConstructor diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaConsumerStatsService.java b/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaConsumerStatsService.java index 7a9c01b72f..e1cfb995c8 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaConsumerStatsService.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaConsumerStatsService.java @@ -26,10 +26,10 @@ import org.apache.kafka.clients.consumer.ConsumerConfig; import org.apache.kafka.clients.consumer.KafkaConsumer; import org.apache.kafka.clients.consumer.OffsetAndMetadata; import org.apache.kafka.common.TopicPartition; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.stereotype.Component; import org.thingsboard.common.util.ThingsBoardExecutors; import org.thingsboard.server.common.data.StringUtils; +import org.thingsboard.server.queue.util.TbKafkaComponent; import java.time.Duration; import java.util.ArrayList; @@ -44,11 +44,12 @@ import java.util.concurrent.TimeUnit; @Slf4j @Component @RequiredArgsConstructor -@ConditionalOnProperty(prefix = "queue", value = "type", havingValue = "kafka") +@TbKafkaComponent public class TbKafkaConsumerStatsService { private final Set monitoredGroups = ConcurrentHashMap.newKeySet(); private final TbKafkaSettings kafkaSettings; + private final KafkaAdmin kafkaAdmin; private final TbKafkaConsumerStatisticConfig statsConfig; private Consumer consumer; @@ -77,7 +78,7 @@ public class TbKafkaConsumerStatsService { } for (String groupId : monitoredGroups) { try { - Map groupOffsets = kafkaSettings.getAdminClient().listConsumerGroupOffsets(groupId).partitionsToOffsetAndMetadata() + Map groupOffsets = kafkaSettings.getAdmin().getClient().listConsumerGroupOffsets(groupId).partitionsToOffsetAndMetadata() .get(statsConfig.getKafkaResponseTimeoutMs(), TimeUnit.MILLISECONDS); Map endOffsets = consumer.endOffsets(groupOffsets.keySet(), timeoutDuration); @@ -159,12 +160,14 @@ public class TbKafkaConsumerStatsService { @Override public String toString() { return "[" + - "topic=[" + topic + ']' + - ", partition=[" + partition + "]" + - ", committedOffset=[" + committedOffset + "]" + - ", endOffset=[" + endOffset + "]" + - ", lag=[" + lag + "]" + - "]"; + "topic=[" + topic + ']' + + ", partition=[" + partition + "]" + + ", committedOffset=[" + committedOffset + "]" + + ", endOffset=[" + endOffset + "]" + + ", lag=[" + lag + "]" + + "]"; } + } + } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaSettings.java b/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaSettings.java index 06ccfe3a69..11736f68cf 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaSettings.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaSettings.java @@ -16,12 +16,10 @@ package org.thingsboard.server.queue.kafka; import jakarta.annotation.PostConstruct; -import jakarta.annotation.PreDestroy; import lombok.Getter; import lombok.Setter; import lombok.extern.slf4j.Slf4j; import org.apache.kafka.clients.CommonClientConfigs; -import org.apache.kafka.clients.admin.AdminClient; import org.apache.kafka.clients.admin.AdminClientConfig; import org.apache.kafka.clients.consumer.ConsumerConfig; import org.apache.kafka.clients.producer.ProducerConfig; @@ -30,12 +28,13 @@ import org.apache.kafka.common.serialization.ByteArrayDeserializer; import org.apache.kafka.common.serialization.ByteArraySerializer; import org.apache.kafka.common.serialization.StringDeserializer; import org.apache.kafka.common.serialization.StringSerializer; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.TbProperty; import org.thingsboard.server.queue.util.PropertyUtils; +import org.thingsboard.server.queue.util.TbKafkaComponent; import java.util.HashMap; import java.util.LinkedHashMap; @@ -47,7 +46,7 @@ import java.util.Properties; * Created by ashvayka on 25.09.18. */ @Slf4j -@ConditionalOnProperty(prefix = "queue", value = "type", havingValue = "kafka") +@TbKafkaComponent @ConfigurationProperties(prefix = "queue.kafka") @Component public class TbKafkaSettings { @@ -143,6 +142,9 @@ public class TbKafkaSettings { @Value("${queue.kafka.consumer-properties-per-topic-inline:}") private String consumerPropertiesPerTopicInline; + @Autowired + private KafkaAdmin kafkaAdmin; + @Deprecated @Setter private List other; @@ -150,8 +152,6 @@ public class TbKafkaSettings { @Setter private Map> consumerPropertiesPerTopic = new HashMap<>(); - private volatile AdminClient adminClient; - @PostConstruct public void initInlineTopicProperties() { Map> inlineProps = parseTopicPropertyList(consumerPropertiesPerTopicInline); @@ -240,15 +240,12 @@ public class TbKafkaSettings { } } - public AdminClient getAdminClient() { - if (adminClient == null) { - synchronized (this) { - if (adminClient == null) { - adminClient = AdminClient.create(toAdminProps()); - } - } - } - return adminClient; + /* + * Temporary solution to avoid major code changes. + * FIXME: use single instance of Kafka queue admin, don't create a separate one for each consumer/producer + * */ + public KafkaAdmin getAdmin() { + return kafkaAdmin; } protected Properties toAdminProps() { @@ -279,11 +276,4 @@ public class TbKafkaSettings { return result; } - @PreDestroy - private void destroy() { - if (adminClient != null) { - adminClient.close(); - } - } - } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaTopicConfigs.java b/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaTopicConfigs.java index 5d5834d20a..c50fd0d720 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaTopicConfigs.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaTopicConfigs.java @@ -18,14 +18,14 @@ package org.thingsboard.server.queue.kafka; import jakarta.annotation.PostConstruct; import lombok.Getter; import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.stereotype.Component; import org.thingsboard.server.queue.util.PropertyUtils; +import org.thingsboard.server.queue.util.TbKafkaComponent; import java.util.Map; @Component -@ConditionalOnProperty(prefix = "queue", value = "type", havingValue = "kafka") +@TbKafkaComponent public class TbKafkaTopicConfigs { public static final String NUM_PARTITIONS_SETTING = "partitions"; diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/memory/InMemoryTbQueueConsumer.java b/common/queue/src/main/java/org/thingsboard/server/queue/memory/InMemoryTbQueueConsumer.java index 5cd55c6cf5..cf91f67993 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/memory/InMemoryTbQueueConsumer.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/memory/InMemoryTbQueueConsumer.java @@ -109,6 +109,11 @@ public class InMemoryTbQueueConsumer implements TbQueueCon return stopped; } + @Override + public Set getPartitions() { + return partitions; + } + @Override public List getFullTopicNames() { return partitions.stream().map(TopicPartitionInfo::getFullTopicName).collect(Collectors.toList()); diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/InMemoryTbTransportQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/InMemoryTbTransportQueueFactory.java index 75d53d9830..2c932534b1 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/InMemoryTbTransportQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/InMemoryTbTransportQueueFactory.java @@ -78,7 +78,7 @@ public class InMemoryTbTransportQueueFactory implements TbTransportQueueFactory templateBuilder.queueAdmin(new TbQueueAdmin() { @Override - public void createTopicIfNotExists(String topic, String properties) {} + public void createTopicIfNotExists(String topic, String properties, boolean force) {} @Override public void destroy() {} diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaMonolithQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaMonolithQueueFactory.java index 866f8d235e..245330cc3e 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaMonolithQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaMonolithQueueFactory.java @@ -58,6 +58,7 @@ import org.thingsboard.server.queue.common.TbProtoQueueMsg; import org.thingsboard.server.queue.discovery.TbServiceInfoProvider; import org.thingsboard.server.queue.discovery.TopicService; import org.thingsboard.server.queue.edqs.EdqsConfig; +import org.thingsboard.server.queue.kafka.KafkaAdmin; import org.thingsboard.server.queue.kafka.TbKafkaAdmin; import org.thingsboard.server.queue.kafka.TbKafkaConsumerStatsService; import org.thingsboard.server.queue.kafka.TbKafkaConsumerTemplate; @@ -83,6 +84,7 @@ public class KafkaMonolithQueueFactory implements TbCoreQueueFactory, TbRuleEngi private final TopicService topicService; private final TbKafkaSettings kafkaSettings; + private final KafkaAdmin kafkaAdmin; private final TbServiceInfoProvider serviceInfoProvider; private final TbQueueCoreSettings coreSettings; private final TbQueueRuleEngineSettings ruleEngineSettings; @@ -118,7 +120,9 @@ public class KafkaMonolithQueueFactory implements TbCoreQueueFactory, TbRuleEngi private final AtomicLong consumerCount = new AtomicLong(); private final AtomicLong edgeConsumerCount = new AtomicLong(); - public KafkaMonolithQueueFactory(TopicService topicService, TbKafkaSettings kafkaSettings, + public KafkaMonolithQueueFactory(TopicService topicService, + TbKafkaSettings kafkaSettings, + KafkaAdmin kafkaAdmin, TbServiceInfoProvider serviceInfoProvider, TbQueueCoreSettings coreSettings, TbQueueRuleEngineSettings ruleEngineSettings, @@ -134,6 +138,7 @@ public class KafkaMonolithQueueFactory implements TbCoreQueueFactory, TbRuleEngi TasksQueueConfig tasksQueueConfig) { this.topicService = topicService; this.kafkaSettings = kafkaSettings; + this.kafkaAdmin = kafkaAdmin; this.serviceInfoProvider = serviceInfoProvider; this.coreSettings = coreSettings; this.ruleEngineSettings = ruleEngineSettings; @@ -240,7 +245,7 @@ public class KafkaMonolithQueueFactory implements TbCoreQueueFactory, TbRuleEngi String queueName = configuration.getName(); String groupId = topicService.buildConsumerGroupId("re-", configuration.getTenantId(), queueName, partitionId); - ruleEngineAdmin.syncOffsets(topicService.buildConsumerGroupId("re-", configuration.getTenantId(), queueName, null), // the fat groupId + kafkaAdmin.syncOffsets(topicService.buildConsumerGroupId("re-", configuration.getTenantId(), queueName, null), // the fat groupId groupId, partitionId); TbKafkaConsumerTemplate.TbKafkaConsumerTemplateBuilder> consumerBuilder = TbKafkaConsumerTemplate.builder(); diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbRuleEngineQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbRuleEngineQueueFactory.java index 3b67ea4f9f..dbf4ab1aba 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbRuleEngineQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbRuleEngineQueueFactory.java @@ -52,6 +52,7 @@ import org.thingsboard.server.queue.common.TbProtoQueueMsg; import org.thingsboard.server.queue.discovery.TbServiceInfoProvider; import org.thingsboard.server.queue.discovery.TopicService; import org.thingsboard.server.queue.edqs.EdqsConfig; +import org.thingsboard.server.queue.kafka.KafkaAdmin; import org.thingsboard.server.queue.kafka.TbKafkaAdmin; import org.thingsboard.server.queue.kafka.TbKafkaConsumerStatsService; import org.thingsboard.server.queue.kafka.TbKafkaConsumerTemplate; @@ -74,6 +75,7 @@ public class KafkaTbRuleEngineQueueFactory implements TbRuleEngineQueueFactory { private final TopicService topicService; private final TbKafkaSettings kafkaSettings; + private final KafkaAdmin kafkaAdmin; private final TbServiceInfoProvider serviceInfoProvider; private final TbQueueCoreSettings coreSettings; private final TbQueueRuleEngineSettings ruleEngineSettings; @@ -99,6 +101,7 @@ public class KafkaTbRuleEngineQueueFactory implements TbRuleEngineQueueFactory { private final AtomicLong consumerCount = new AtomicLong(); public KafkaTbRuleEngineQueueFactory(TopicService topicService, TbKafkaSettings kafkaSettings, + KafkaAdmin kafkaAdmin, TbServiceInfoProvider serviceInfoProvider, TbQueueCoreSettings coreSettings, TbQueueRuleEngineSettings ruleEngineSettings, @@ -111,6 +114,7 @@ public class KafkaTbRuleEngineQueueFactory implements TbRuleEngineQueueFactory { TbKafkaTopicConfigs kafkaTopicConfigs) { this.topicService = topicService; this.kafkaSettings = kafkaSettings; + this.kafkaAdmin = kafkaAdmin; this.serviceInfoProvider = serviceInfoProvider; this.coreSettings = coreSettings; this.ruleEngineSettings = ruleEngineSettings; @@ -234,7 +238,7 @@ public class KafkaTbRuleEngineQueueFactory implements TbRuleEngineQueueFactory { String queueName = configuration.getName(); String groupId = topicService.buildConsumerGroupId("re-", configuration.getTenantId(), queueName, partitionId); - ruleEngineAdmin.syncOffsets(topicService.buildConsumerGroupId("re-", configuration.getTenantId(), queueName, null), // the fat groupId + kafkaAdmin.syncOffsets(topicService.buildConsumerGroupId("re-", configuration.getTenantId(), queueName, null), // the fat groupId groupId, partitionId); TbKafkaConsumerTemplate.TbKafkaConsumerTemplateBuilder> consumerBuilder = TbKafkaConsumerTemplate.builder(); diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/util/TbKafkaComponent.java b/common/queue/src/main/java/org/thingsboard/server/queue/util/TbKafkaComponent.java new file mode 100644 index 0000000000..ad4862e36d --- /dev/null +++ b/common/queue/src/main/java/org/thingsboard/server/queue/util/TbKafkaComponent.java @@ -0,0 +1,29 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.queue.util; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; + +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Inherited +@Retention(RetentionPolicy.RUNTIME) +@Target({java.lang.annotation.ElementType.TYPE, java.lang.annotation.ElementType.METHOD}) +@ConditionalOnProperty(prefix = "queue", value = "type", havingValue = "kafka") +public @interface TbKafkaComponent {} diff --git a/common/queue/src/test/java/org/thingsboard/server/queue/kafka/TbKafkaSettingsTest.java b/common/queue/src/test/java/org/thingsboard/server/queue/kafka/TbKafkaSettingsTest.java index ad026c63aa..bc37982245 100644 --- a/common/queue/src/test/java/org/thingsboard/server/queue/kafka/TbKafkaSettingsTest.java +++ b/common/queue/src/test/java/org/thingsboard/server/queue/kafka/TbKafkaSettingsTest.java @@ -28,7 +28,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.spy; -@SpringBootTest(classes = TbKafkaSettings.class) +@SpringBootTest(classes = {TbKafkaSettings.class, KafkaAdmin.class}) @TestPropertySource(properties = { "queue.type=kafka", "queue.kafka.bootstrap.servers=localhost:9092", diff --git a/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbUtils.java b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbUtils.java index 072a17835d..39a32310a6 100644 --- a/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbUtils.java +++ b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbUtils.java @@ -264,6 +264,8 @@ public class TbUtils { float.class, int.class))); parserConfig.addImport("toInt", new MethodStub(TbUtils.class.getMethod("toInt", double.class))); + parserConfig.addImport("roundResult", new MethodStub(TbUtils.class.getMethod("roundResult", + double.class, Integer.class))); parserConfig.addImport("isNaN", new MethodStub(TbUtils.class.getMethod("isNaN", double.class))); parserConfig.addImport("hexToBytes", new MethodStub(TbUtils.class.getMethod("hexToBytes", @@ -1186,6 +1188,16 @@ public class TbUtils { return BigDecimal.valueOf(value).setScale(0, RoundingMode.HALF_UP).intValue(); } + public static Object roundResult(double value, Integer precision) { + if (precision == null) { + return value; + } + if (precision.equals(0)) { + return toInt(value); + } + return toFixed(value, precision); + } + public static boolean isNaN(double value) { return Double.isNaN(value); } diff --git a/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelCfArg.java b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelCfArg.java index f95b08195e..4f2719fb75 100644 --- a/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelCfArg.java +++ b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelCfArg.java @@ -26,7 +26,10 @@ import com.fasterxml.jackson.annotation.JsonTypeInfo; ) @JsonSubTypes({ @JsonSubTypes.Type(value = TbelCfSingleValueArg.class, name = "SINGLE_VALUE"), - @JsonSubTypes.Type(value = TbelCfTsRollingArg.class, name = "TS_ROLLING") + @JsonSubTypes.Type(value = TbelCfTsRollingArg.class, name = "TS_ROLLING"), + @JsonSubTypes.Type(value = TbelCfGeofencingArg.class, name = "GEOFENCING_CF_ARGUMENT_VALUE"), + @JsonSubTypes.Type(value = TbelCfPropagationArg.class, name = "PROPAGATION_CF_ARGUMENT_VALUE"), + @JsonSubTypes.Type(value = TbelCfRelatedEntitiesArgumentValue.class, name = "RELATED_ENTITIES_ARGUMENT_VALUE") }) public interface TbelCfArg extends TbelCfObject { diff --git a/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelCfGeofencingArg.java b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelCfGeofencingArg.java new file mode 100644 index 0000000000..0fa0f4a5bf --- /dev/null +++ b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelCfGeofencingArg.java @@ -0,0 +1,43 @@ +/** + * Copyright © 2016-2025 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.script.api.tbel; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +@Data +public class TbelCfGeofencingArg implements TbelCfArg { + + private final Object value; + + @JsonCreator + public TbelCfGeofencingArg(@JsonProperty("value") Object value) { + this.value = value; + } + + @Override + public String getType() { + return "GEOFENCING_CF_ARGUMENT_VALUE"; + } + + + @Override + public long memorySize() { + return OBJ_SIZE; + } + +} diff --git a/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelCfPropagationArg.java b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelCfPropagationArg.java new file mode 100644 index 0000000000..83d7e81a86 --- /dev/null +++ b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelCfPropagationArg.java @@ -0,0 +1,42 @@ +/** + * Copyright © 2016-2025 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.script.api.tbel; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +@Data +public class TbelCfPropagationArg implements TbelCfArg { + + private final Object value; + + @JsonCreator + public TbelCfPropagationArg(@JsonProperty("value") Object value) { + this.value = value; + } + + @Override + public String getType() { + return "PROPAGATION_CF_ARGUMENT_VALUE"; + } + + @Override + public long memorySize() { + return OBJ_SIZE; + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/CalculatedFieldLinkId.java b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelCfRelatedEntitiesArgumentValue.java similarity index 50% rename from common/data/src/main/java/org/thingsboard/server/common/data/id/CalculatedFieldLinkId.java rename to common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelCfRelatedEntitiesArgumentValue.java index 6a0c680bb6..02d641d576 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/CalculatedFieldLinkId.java +++ b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelCfRelatedEntitiesArgumentValue.java @@ -13,33 +13,33 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.common.data.id; +package org.thingsboard.script.api.tbel; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; -import io.swagger.v3.oas.annotations.media.Schema; -import org.thingsboard.server.common.data.EntityType; +import lombok.Data; +import java.util.Collections; +import java.util.Map; import java.util.UUID; -@Schema -public class CalculatedFieldLinkId extends UUIDBased implements EntityId { +@Data +public class TbelCfRelatedEntitiesArgumentValue implements TbelCfArg { - private static final long serialVersionUID = 1L; + private final Map entityInputs; @JsonCreator - public CalculatedFieldLinkId(@JsonProperty("id") UUID id) { - super(id); + public TbelCfRelatedEntitiesArgumentValue(@JsonProperty("entityInputs") Map values) { + this.entityInputs = Collections.unmodifiableMap(values); } - public static CalculatedFieldLinkId fromString(String calculatedFieldLinkId) { - return new CalculatedFieldLinkId(UUID.fromString(calculatedFieldLinkId)); + @Override + public String getType() { + return "RELATED_ENTITIES_ARGUMENT_VALUE"; } - @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "CALCULATED_FIELD_LINK", allowableValues = "CALCULATED_FIELD_LINK") @Override - public EntityType getEntityType() { - return EntityType.CALCULATED_FIELD_LINK; + public long memorySize() { + return OBJ_SIZE; } - } diff --git a/common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbUtilsTest.java b/common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbUtilsTest.java index 4dcbd2d69c..38e69246c5 100644 --- a/common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbUtilsTest.java +++ b/common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbUtilsTest.java @@ -1154,6 +1154,13 @@ public class TbUtilsTest { Assertions.assertEquals(28, TbUtils.toInt(28.0)); } + @Test + public void roundResult() { + Assertions.assertEquals(1729.1729, TbUtils.roundResult(doubleVal, null)); + Assertions.assertEquals(1729, TbUtils.roundResult(doubleVal, 0)); + Assertions.assertEquals(1729.17, TbUtils.roundResult(doubleVal, 2)); + } + @Test public void isNaN() { assertFalse(TbUtils.isNaN(doubleVal)); diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/secure/LwM2MBootstrapConfig.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/secure/LwM2MBootstrapConfig.java index 517bcabac7..aaca3374a2 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/secure/LwM2MBootstrapConfig.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/secure/LwM2MBootstrapConfig.java @@ -127,7 +127,9 @@ public class LwM2MBootstrapConfig implements Serializable { private BootstrapConfig.ServerConfig setServerConfig (AbstractLwM2MBootstrapServerCredential serverCredential) { BootstrapConfig.ServerConfig serverConfig = new BootstrapConfig.ServerConfig(); - serverConfig.shortId = serverCredential.getShortServerId(); + if (serverCredential.getShortServerId() != null) { + serverConfig.shortId = serverCredential.getShortServerId(); + } serverConfig.lifetime = serverCredential.getLifetime(); serverConfig.defaultMinPeriod = serverCredential.getDefaultMinPeriod(); serverConfig.notifIfDisabled = serverCredential.isNotifIfDisabled(); diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/secure/LwM2mDefaultBootstrapSessionManager.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/secure/LwM2mDefaultBootstrapSessionManager.java index 516f118630..9adceb2860 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/secure/LwM2mDefaultBootstrapSessionManager.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/secure/LwM2mDefaultBootstrapSessionManager.java @@ -16,6 +16,7 @@ package org.thingsboard.server.transport.lwm2m.bootstrap.secure; import lombok.extern.slf4j.Slf4j; +import org.eclipse.californium.core.coap.Request; import org.eclipse.leshan.core.peer.IpPeer; import org.eclipse.leshan.core.peer.LwM2mPeer; import org.eclipse.leshan.core.peer.PskIdentity; @@ -112,8 +113,10 @@ public class LwM2mDefaultBootstrapSessionManager extends DefaultBootstrapSession } catch (InvalidConfigurationException e){ log.error("Failed put to lwM2MBootstrapSessionClients by endpoint [{}]", request.getEndpointName(), e); } + String msg = String.format("Bootstrap session started... %s", ((Request) request.getCoapRequest()).getLocalAddress().toString()); + log.warn(String.format("%s: %s", request.getEndpointName(), msg)); this.sendLogs(request.getEndpointName(), - String.format("%s: Bootstrap session started...", LOG_LWM2M_INFO, request.getEndpointName())); + String.format("%s: %s", LOG_LWM2M_INFO, msg)); } return session; } @@ -135,7 +138,7 @@ public class LwM2mDefaultBootstrapSessionManager extends DefaultBootstrapSession session.setModel(modelProvider.getObjectModel(session, tasks.supportedObjects)); // set Requests to Send - log.info("tasks.requestsToSend = [{}]", tasks.requestsToSend); + log.warn("tasks.requestsToSend = [{}]", tasks.requestsToSend); session.setRequests(tasks.requestsToSend); // prepare list where we will store Responses @@ -182,14 +185,16 @@ public class LwM2mDefaultBootstrapSessionManager extends DefaultBootstrapSession session.getResponses().add(response); String msg = String.format("%s: receives success response for: %s %s %s", LOG_LWM2M_INFO, request.getClass().getSimpleName(), request.getPath().toString(), response.toString()); + log.warn(msg); this.sendLogs(bsSession.getEndpoint(), msg); // on success for NOT bootstrap finish request we send next request return BootstrapPolicy.continueWith(nextRequest(bsSession)); } else { // on success for bootstrap finish request we stop the session - this.sendLogs(bsSession.getEndpoint(), - String.format("%s: receives success response for bootstrap finish.", LOG_LWM2M_INFO)); + String msg = String.format("%s: receives success response for bootstrap finish.", LOG_LWM2M_INFO); + log.info(msg); + this.sendLogs(bsSession.getEndpoint(), msg); this.tasksProvider.remove(bsSession.getEndpoint()); return BootstrapPolicy.finished(); } @@ -228,7 +233,9 @@ public class LwM2mDefaultBootstrapSessionManager extends DefaultBootstrapSession @Override public void end(BootstrapSession bsSession) { - this.sendLogs(bsSession.getEndpoint(), String.format("%s: Bootstrap session finished.", LOG_LWM2M_INFO)); + String msg = String.format("%s: Bootstrap session finished.", LOG_LWM2M_INFO); + log.warn(msg); + this.sendLogs(bsSession.getEndpoint(), msg); this.tasksProvider.remove(bsSession.getEndpoint()); } diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/store/LwM2MBootstrapConfigStoreTaskProvider.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/store/LwM2MBootstrapConfigStoreTaskProvider.java index f24f068365..ab69632956 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/store/LwM2MBootstrapConfigStoreTaskProvider.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/store/LwM2MBootstrapConfigStoreTaskProvider.java @@ -17,15 +17,12 @@ package org.thingsboard.server.transport.lwm2m.bootstrap.store; import lombok.extern.slf4j.Slf4j; import org.eclipse.leshan.core.link.Link; -import org.eclipse.leshan.core.node.LwM2mObject; import org.eclipse.leshan.core.node.LwM2mPath; import org.eclipse.leshan.core.request.BootstrapDeleteRequest; import org.eclipse.leshan.core.request.BootstrapDiscoverRequest; import org.eclipse.leshan.core.request.BootstrapDownlinkRequest; -import org.eclipse.leshan.core.request.BootstrapReadRequest; import org.eclipse.leshan.core.request.ContentFormat; import org.eclipse.leshan.core.response.BootstrapDiscoverResponse; -import org.eclipse.leshan.core.response.BootstrapReadResponse; import org.eclipse.leshan.core.response.LwM2mResponse; import org.eclipse.leshan.server.bootstrap.BootstrapConfig; import org.eclipse.leshan.server.bootstrap.BootstrapConfigStore; @@ -33,7 +30,6 @@ import org.eclipse.leshan.server.bootstrap.BootstrapSession; import org.eclipse.leshan.server.bootstrap.BootstrapUtil; import org.eclipse.leshan.server.bootstrap.InvalidConfigurationException; -import java.math.BigInteger; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; @@ -43,14 +39,18 @@ import java.util.Map; import java.util.Set; import java.util.TreeMap; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; -import java.util.stream.Collectors; -import static org.eclipse.leshan.core.model.ResourceModel.Type.OPAQUE; +import static org.eclipse.leshan.core.LwM2mId.ACCESS_CONTROL; +import static org.eclipse.leshan.core.LwM2mId.SECURITY; +import static org.eclipse.leshan.core.LwM2mId.SERVER; import static org.eclipse.leshan.server.bootstrap.BootstrapUtil.toWriteRequest; -import static org.thingsboard.server.transport.lwm2m.utils.LwM2MTransportUtil.BOOTSTRAP_DEFAULT_SHORT_ID_0; +import static org.thingsboard.server.common.data.device.credentials.lwm2m.Lwm2mServerIdentifier.LWM2M_SERVER_MAX; +import static org.thingsboard.server.common.data.device.credentials.lwm2m.Lwm2mServerIdentifier.PRIMARY_LWM2M_SERVER; +import static org.thingsboard.server.common.data.device.credentials.lwm2m.Lwm2mServerIdentifier.isLwm2mServer; @Slf4j public class LwM2MBootstrapConfigStoreTaskProvider implements LwM2MBootstrapTaskProvider { @@ -77,11 +77,11 @@ public class LwM2MBootstrapConfigStoreTaskProvider implements LwM2MBootstrapTask @Override public Tasks getTasks(BootstrapSession session, List previousResponse) { // BootstrapConfig config = store.get(session.getEndpoint(), session.getClientTransportData().getIdentity(), session); - BootstrapConfig config = store.get(session); - if (config == null) { + BootstrapConfig configNew = store.get(session); + if (configNew == null) { return null; } - if (previousResponse == null && shouldStartWithDiscover(config)) { + if (previousResponse == null && shouldStartWithDiscover(configNew)) { Tasks tasks = new Tasks(); tasks.requestsToSend = new ArrayList<>(1); tasks.requestsToSend.add(new BootstrapDiscoverRequest()); @@ -96,47 +96,27 @@ public class LwM2MBootstrapConfigStoreTaskProvider implements LwM2MBootstrapTask tasks.supportedObjects = this.supportedObjects; // handle bootstrap discover response if (previousResponse != null) { - if (previousResponse.get(0) instanceof BootstrapDiscoverResponse) { - BootstrapDiscoverResponse discoverResponse = (BootstrapDiscoverResponse) previousResponse.get(0); + if (previousResponse.get(0) instanceof BootstrapDiscoverResponse discoverResponse) { if (discoverResponse.isSuccess()) { - this.initAfterBootstrapDiscover(discoverResponse); - findSecurityInstanceId(discoverResponse.getObjectLinks(), session.getEndpoint()); - } else { + this.initAfterBootstrapDiscover(discoverResponse); + /// Short Server Ids - in old config + findInstancesIdOldByServerId(discoverResponse, session.getEndpoint()); log.warn( - "Bootstrap Discover return error {} : to continue bootstrap session without autoIdForSecurityObject mode. {}", + "Bootstrap server instance successfully found in Security Object (0) in response {}. Continuing bootstrap session. Session: {}", discoverResponse, session); - } - if (this.lwM2MBootstrapSessionClients.get(session.getEndpoint()).getSecurityInstances().get(BOOTSTRAP_DEFAULT_SHORT_ID_0) == null) { - log.error( - "Unable to find bootstrap server instance in Security Object (0) in response {}: unable to continue bootstrap session with autoIdForSecurityObject mode. {}", + } else { + log.warn( + "Unable to find bootstrap server instance in Security Object (0) in response {}. Continuing bootstrap session with autoIdForSecurityObject mode, ignoring information from discoverResponse. Session: {}", discoverResponse, session); - return null; - } - tasks.requestsToSend = new ArrayList<>(1); - tasks.requestsToSend.add(new BootstrapReadRequest("/1")); - tasks.last = false; - return tasks; - } - BootstrapReadResponse readResponse = (BootstrapReadResponse) previousResponse.get(0); - Integer bootstrapServerIdOld = null; - if (readResponse.isSuccess()) { - findServerInstanceId(readResponse, session.getEndpoint()); - if (this.lwM2MBootstrapSessionClients.get(session.getEndpoint()).getSecurityInstances().size() > 0 && this.lwM2MBootstrapSessionClients.get(session.getEndpoint()).getServerInstances().size() > 0) { - bootstrapServerIdOld = this.findBootstrapServerId(session.getEndpoint()); } - } else { - log.warn( - "Bootstrap ReadResponse return error {} : to continue bootstrap session without find Server Instance Id. {}", - readResponse, session); } // create requests from config - tasks.requestsToSend = this.toRequests(config, - config.contentFormat != null ? config.contentFormat : session.getContentFormat(), - bootstrapServerIdOld, session.getEndpoint()); + tasks.requestsToSend = this.toRequests(configNew, + configNew.contentFormat != null ? configNew.contentFormat : session.getContentFormat(), session.getEndpoint()); } else { // create requests from config - tasks.requestsToSend = BootstrapUtil.toRequests(config, - config.contentFormat != null ? config.contentFormat : session.getContentFormat()); + tasks.requestsToSend = BootstrapUtil.toRequests(configNew, + configNew.contentFormat != null ? configNew.contentFormat : session.getContentFormat()); } return tasks; } @@ -148,81 +128,57 @@ public class LwM2MBootstrapConfigStoreTaskProvider implements LwM2MBootstrapTask /** * "Short Server ID": This Resource MUST be set when the Bootstrap-Server Resource has a value of 'false'. - * The values ID:0 and ID:65535 values MUST NOT be used for identifying the LwM2M Server. - * "Short Server ID": + * "Short Lwm2m Server ID": * - Link Instance (lwm2m Server) hase linkParams with key = "ssid" value = "shortId" (ver lvm2m = 1.1). - * - Link Instance (bootstrap Server) hase not linkParams with key = "ssid" (ver lvm2m = 1.0). + * The values ID:0 values MUST NOT be used for identifying the LwM2M Server only BS. */ - protected void findSecurityInstanceId(Link[] objectLinks, String endpoint) { - log.info("Object after discover: [{}]", objectLinks); - for (Link link : objectLinks) { - if (link.getUriReference().startsWith("/0/")) { - try { - LwM2mPath path = new LwM2mPath(link.getUriReference()); - if (path.isObjectInstance()) { - if (link.getAttributes().get("ssid") != null) { - int serverId = Integer.parseInt(link.getAttributes().get("ssid").getCoreLinkValue()); - if (!lwM2MBootstrapSessionClients.get(endpoint).getSecurityInstances().containsKey(serverId)) { - lwM2MBootstrapSessionClients.get(endpoint).getSecurityInstances().put(serverId, path.getObjectInstanceId()); - } else { - log.error("Invalid lwm2mSecurityInstance by [{}]", path.getObjectInstanceId()); - } - lwM2MBootstrapSessionClients.get(endpoint).getSecurityInstances().put(serverId, path.getObjectInstanceId()); + protected void findInstancesIdOldByServerId(BootstrapDiscoverResponse discoverResponses, String endpoint) { + log.info("Object after discover: [{}]", Arrays.toString(discoverResponses.getObjectLinks())); + for (Link link : discoverResponses.getObjectLinks()) { + LwM2mPath path = new LwM2mPath(link.getUriReference()); + if (path.isObjectInstance()) { + int lwm2mShortServerId = 0; + if (path.getObjectId() == 0) { + if (link.getAttributes().get("ssid") != null) { + lwm2mShortServerId = Integer.parseInt(link.getAttributes().get("ssid").getCoreLinkValue()); + if (validateLwm2mShortServerId(lwm2mShortServerId)) { + this.lwM2MBootstrapSessionClients.get(endpoint).getSecurityInstances().putIfAbsent(lwm2mShortServerId, path.getObjectInstanceId()); + } else { + log.error("Invalid lwm2mSecurityInstance [{}] by short server id [{}]", path.getObjectInstanceId(), lwm2mShortServerId); + } + } else { + this.lwM2MBootstrapSessionClients.get(endpoint).getSecurityInstances().putIfAbsent(null, path.getObjectInstanceId()); + } + } else if (path.getObjectId() == 1) { + if (link.getAttributes().get("ssid") != null) { + lwm2mShortServerId = Integer.parseInt(link.getAttributes().get("ssid").getCoreLinkValue()); + if (validateLwm2mShortServerId(lwm2mShortServerId)) { + this.lwM2MBootstrapSessionClients.get(endpoint).getServerInstances().putIfAbsent(lwm2mShortServerId, path.getObjectInstanceId()); } else { - if (!this.lwM2MBootstrapSessionClients.get(endpoint).getSecurityInstances().containsKey(0)) { - this.lwM2MBootstrapSessionClients.get(endpoint).getSecurityInstances().put(BOOTSTRAP_DEFAULT_SHORT_ID_0, path.getObjectInstanceId()); - } else { - log.error("Invalid bootstrapSecurityInstance by [{}]", path.getObjectInstanceId()); - } + log.error("Invalid lwm2mServerInstance [{}] by short server id [{}]", path.getObjectInstanceId(), lwm2mShortServerId); } } - } catch (Exception e) { - // ignore if this is not a LWM2M path - log.error("Invalid LwM2MPath starting by \"/0/\""); } } } } - protected void findServerInstanceId(BootstrapReadResponse readResponse, String endpoint) { - try { - ((LwM2mObject) readResponse.getContent()).getInstances().values().forEach(instance -> { - var shId = OPAQUE.equals(instance.getResource(0).getType()) ? new BigInteger((byte[]) instance.getResource(0).getValue()).intValue() : instance.getResource(0).getValue(); - int shortId; - if (shId instanceof Long) { - shortId = ((Long) shId).intValue(); - } else { - shortId = (int) shId; - } - this.lwM2MBootstrapSessionClients.get(endpoint).getServerInstances().put(shortId, instance.getId()); - }); - } catch (Exception e) { - log.error("Failed find Server Instance Id. ", e); - } - } - - protected Integer findBootstrapServerId(String endpoint) { - Integer bootstrapServerIdOld = null; - Map filteredMap = this.lwM2MBootstrapSessionClients.get(endpoint).getServerInstances().entrySet() - .stream().filter(x -> !this.lwM2MBootstrapSessionClients.get(endpoint).getSecurityInstances().containsKey(x.getKey())) - .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); - if (filteredMap.size() > 0) { - bootstrapServerIdOld = filteredMap.keySet().stream().findFirst().get(); - } - return bootstrapServerIdOld; - } - public BootstrapConfigStore getStore() { return this.store; } private void initAfterBootstrapDiscover(BootstrapDiscoverResponse response) { Link[] links = response.getObjectLinks(); + AtomicReference verDefault = new AtomicReference<>("1.0"); Arrays.stream(links).forEach(link -> { LwM2mPath path = new LwM2mPath(link.getUriReference()); - if (!path.isRoot() && path.getObjectId() < 3) { + if (path.isRoot()) { + if (link.hasAttribute() && link.getAttributes().get("lwm2m") != null) { + verDefault.set(link.getAttributes().get("lwm2m").getValue().toString()); + } + } else if (path.getObjectId() <= ACCESS_CONTROL) { if (path.isObject()) { - String ver = link.getAttributes().get("ver") != null ? link.getAttributes().get("ver").getCoreLinkValue() : "1.0"; + String ver = (link.hasAttribute() && link.getAttributes().get("ver") != null) ? link.getAttributes().get("ver").getCoreLinkValue() : verDefault.get(); this.supportedObjects.put(path.getObjectId(), ver); } } @@ -230,94 +186,124 @@ public class LwM2MBootstrapConfigStoreTaskProvider implements LwM2MBootstrapTask } - public List> toRequests(BootstrapConfig bootstrapConfig, + /** Map => LwM2MBootstrapClientInstanceIds + * 1) Both + * - (Short) Server ID == null bs) + * SECURITY = 0; InstanceId = 0 + * - Short Server ID == 1 - 65534 lwm2m) + * SECURITY = 0; InstanceId = 1 + * SERVER = 1; InstanceId = 0 + * 2) Only BS Server + * - Short Server ID == null bs) + * SECURITY = 0; InstanceId = 0 + * 3) Only Lwm2m Server + * - Short Server ID == 1 - 65534 lwm2m) + * SECURITY = 0; InstanceId = 0 + * SERVER = 1; InstanceId = 0 + * */ + public List> toRequests(BootstrapConfig bootstrapConfigNew, ContentFormat contentFormat, - Integer bootstrapServerIdOld, String endpoint) { + Integer bootstrapSecurityInstanceId = this.lwM2MBootstrapSessionClients.get(endpoint).getSecurityInstances().get(null) == null ? + -2 : this.lwM2MBootstrapSessionClients.get(endpoint).getSecurityInstances().get(null); List> requests = new ArrayList<>(); Set pathsDelete = new HashSet<>(); - List> requestsWrite = new ArrayList<>(); - boolean isBsServer = false; - boolean isLwServer = false; - /** Map */ - Map instances = new HashMap<>(); - Integer bootstrapServerIdNew = null; - // handle security - int lwm2mSecurityInstanceId = 0; - int bootstrapSecurityInstanceId = this.lwM2MBootstrapSessionClients.get(endpoint).getSecurityInstances().get(BOOTSTRAP_DEFAULT_SHORT_ID_0); - for (BootstrapConfig.ServerSecurity security : new TreeMap<>(bootstrapConfig.security).values()) { - if (security.bootstrapServer) { - requestsWrite.add(toWriteRequest(bootstrapSecurityInstanceId, security, contentFormat)); - isBsServer = true; - bootstrapServerIdNew = security.serverId; - instances.put(security.serverId, bootstrapSecurityInstanceId); - } else { - if (lwm2mSecurityInstanceId == bootstrapSecurityInstanceId) { - lwm2mSecurityInstanceId++; - } - requestsWrite.add(toWriteRequest(lwm2mSecurityInstanceId, security, contentFormat)); - instances.put(security.serverId, lwm2mSecurityInstanceId); - isLwServer = true; - if (!isBsServer && this.lwM2MBootstrapSessionClients.get(endpoint).getSecurityInstances().containsKey(security.serverId) && - lwm2mSecurityInstanceId != this.lwM2MBootstrapSessionClients.get(endpoint).getSecurityInstances().get(security.serverId)) { - pathsDelete.add("/0/" + this.lwM2MBootstrapSessionClients.get(endpoint).getSecurityInstances().get(security.serverId)); - } - /** - * If there is an instance in the serverInstances with serverId which we replace in the securityInstances - */ - // find serverId in securityInstances by id (instance) - Integer serverIdOld = null; - for (Map.Entry entry : this.lwM2MBootstrapSessionClients.get(endpoint).getSecurityInstances().entrySet()) { - if (entry.getValue().equals(lwm2mSecurityInstanceId)) { - serverIdOld = entry.getKey(); - } - } - if (!isBsServer && serverIdOld != null && this.lwM2MBootstrapSessionClients.get(endpoint).getServerInstances().containsKey(serverIdOld)) { - pathsDelete.add("/1/" + this.lwM2MBootstrapSessionClients.get(endpoint).getServerInstances().get(serverIdOld)); - } - lwm2mSecurityInstanceId++; + ConcurrentHashMap> requestsWrite = new ConcurrentHashMap<>(); + + /// handle security & handle + // bootstrap Security new - There can only be one instance of bootstrap at a time. + /// bs: handle security only + for (BootstrapConfig.ServerSecurity security : new TreeMap<>(bootstrapConfigNew.security).values()) { + if (security.bootstrapServer && bootstrapSecurityInstanceId > -1) { + // delete old bootstrap Security + String path = "/" + SECURITY + "/" + bootstrapSecurityInstanceId; + pathsDelete.add(path); + security.serverId = null; + requestsWrite.put(path, toWriteRequest(bootstrapSecurityInstanceId, security, contentFormat)); } } - // handle server - for (Map.Entry server : bootstrapConfig.servers.entrySet()) { - int securityInstanceId = instances.get(server.getValue().shortId); - requestsWrite.add(toWriteRequest(securityInstanceId, server.getValue(), contentFormat)); - if (!isBsServer) { - /** Delete instance if bootstrapServerIdNew not equals bootstrapServerIdOld or securityInstanceBsIdNew not equals serverInstanceBsIdOld */ - if (bootstrapServerIdNew != null && server.getValue().shortId == bootstrapServerIdNew && - (bootstrapServerIdNew != bootstrapServerIdOld || securityInstanceId != this.lwM2MBootstrapSessionClients.get(endpoint).getServerInstances().get(bootstrapServerIdOld))) { - pathsDelete.add("/1/" + this.lwM2MBootstrapSessionClients.get(endpoint).getServerInstances().get(bootstrapServerIdOld)); - /** Delete instance if serverIdNew is present in serverInstances and securityInstanceIdOld by serverIdNew not equals serverInstanceIdOld */ - } else if (this.lwM2MBootstrapSessionClients.get(endpoint).getServerInstances().containsKey(server.getValue().shortId) && - securityInstanceId != this.lwM2MBootstrapSessionClients.get(endpoint).getServerInstances().get(server.getValue().shortId)) { - pathsDelete.add("/1/" + this.lwM2MBootstrapSessionClients.get(endpoint).getServerInstances().get(server.getValue().shortId)); - } + + /** lwm2m servers: Multiple instances of lwm2m servers can run simultaneously by SHORT_ID + if update -> delete and write by InstanceId + if new -> only write with InstanceIdMax++ + */ + + /// lwm2m server: handle security & server + //max Lwm2m Security instance old id if new + int lwm2mSecurityInstanceIdMax = -1; + for (Integer shortId : this.lwM2MBootstrapSessionClients.get(endpoint).getSecurityInstances().keySet()) { + if (isLwm2mServer(shortId)) { + lwm2mSecurityInstanceIdMax = Math.max( + this.lwM2MBootstrapSessionClients.get(endpoint).getSecurityInstances().get(shortId), + lwm2mSecurityInstanceIdMax); } } - // handle acl - for (Map.Entry acl : bootstrapConfig.acls.entrySet()) { - requestsWrite.add(toWriteRequest(acl.getKey(), acl.getValue(), contentFormat)); + //max Lwm2m Server instance old id if new + int lwm2mServerInstanceIdMax = -1; + for (Integer shortId : this.lwM2MBootstrapSessionClients.get(endpoint).getServerInstances().keySet()) { + if (isLwm2mServer(shortId)) { + lwm2mServerInstanceIdMax = Math.max( + this.lwM2MBootstrapSessionClients.get(endpoint).getServerInstances().get(shortId), + lwm2mServerInstanceIdMax); + } } - // handle delete - if (isBsServer && isLwServer) { - requests.add(new BootstrapDeleteRequest("/0")); - requests.add(new BootstrapDeleteRequest("/1")); - } else { - pathsDelete.forEach(pathDelete -> requests.add(new BootstrapDeleteRequest(pathDelete))); + // Lwm2m update or new + for (BootstrapConfig.ServerSecurity security : new TreeMap<>(bootstrapConfigNew.security).values()) { + if (!security.bootstrapServer) { + // Security + Integer secureInstanceId = this.lwM2MBootstrapSessionClients.get(endpoint).getSecurityInstances().get(security.serverId); + if (secureInstanceId != null) { + pathsDelete.add("/" + SECURITY + "/" + secureInstanceId); + requestsWrite.put("/" + SECURITY + "/" + secureInstanceId, toWriteRequest(secureInstanceId, security, contentFormat)); + } else { + secureInstanceId = ++lwm2mSecurityInstanceIdMax; + if (bootstrapSecurityInstanceId.equals(secureInstanceId)) { + secureInstanceId = ++lwm2mSecurityInstanceIdMax; + } + requestsWrite.put("/" + SECURITY + "/" + secureInstanceId, toWriteRequest(secureInstanceId, security, contentFormat)); + } + Integer serverInstanceId = this.lwM2MBootstrapSessionClients.get(endpoint).getServerInstances().get(security.serverId); + if (serverInstanceId != null) { + pathsDelete.add("/" + SERVER + "/" + serverInstanceId); + } else { + serverInstanceId = ++lwm2mServerInstanceIdMax; + } + Integer finalServerInstanceId = serverInstanceId; + new TreeMap<>(bootstrapConfigNew.servers).values().stream() + .filter(server -> server.shortId == security.serverId) + .findFirst() + .ifPresent(server -> + requestsWrite.put( + "/" + SERVER + "/" + finalServerInstanceId, + toWriteRequest(finalServerInstanceId, server, contentFormat) + ) + ); + } } - // handle write - if (requestsWrite.size() > 0) { - requests.addAll(requestsWrite); + + /// handle acl + for (Map.Entry acl : bootstrapConfigNew.acls.entrySet()) { + requestsWrite.put("/" + ACCESS_CONTROL + "/" + acl.getKey(), toWriteRequest(acl.getKey(), acl.getValue(), contentFormat)); + } + /// handle delete + pathsDelete.forEach(pathDelete -> requests.add(new BootstrapDeleteRequest(pathDelete))); + + /// handle write + if (!requestsWrite.isEmpty()) { + requests.addAll(requestsWrite.values()); } return (requests); } - private void initSupportedObjectsDefault() { this.supportedObjects = new HashMap<>(); - this.supportedObjects.put(0, "1.1"); - this.supportedObjects.put(1, "1.1"); - this.supportedObjects.put(2, "1.0"); + this.supportedObjects.put(SECURITY, "1.1"); + this.supportedObjects.put(SERVER, "1.1"); + this.supportedObjects.put(ACCESS_CONTROL, "1.0"); + } + + private boolean validateLwm2mShortServerId(int id){ + return id >= PRIMARY_LWM2M_SERVER.getId() && id <= LWM2M_SERVER_MAX.getId(); } @Override diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/store/LwM2MConfigurationChecker.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/store/LwM2MConfigurationChecker.java index aba29aed24..3916849da3 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/store/LwM2MConfigurationChecker.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/store/LwM2MConfigurationChecker.java @@ -21,6 +21,10 @@ import org.eclipse.leshan.server.bootstrap.InvalidConfigurationException; import java.util.Map; +import static org.thingsboard.server.common.data.device.credentials.lwm2m.Lwm2mServerIdentifier.NOT_USED_IDENTIFYING_LWM2M_SERVER_MIN; +import static org.thingsboard.server.common.data.device.credentials.lwm2m.Lwm2mServerIdentifier.NOT_USED_IDENTIFYING_LWM2M_SERVER_MAX; +import static org.thingsboard.server.common.data.device.credentials.lwm2m.Lwm2mServerIdentifier.isNotLwm2mServer; + public class LwM2MConfigurationChecker extends ConfigurationChecker { @Override @@ -74,15 +78,16 @@ public class LwM2MConfigurationChecker extends ConfigurationChecker { * This Resource MUST be set when the Bootstrap-Server Resource has false value. * Specific ID:0 and ID:65535 values MUST NOT be used for identifying the LwM2M Server (Section 6.3 of the LwM2M version 1.0 specification). */ - if (!security.bootstrapServer && (srvCfg.shortId < 1 && srvCfg.shortId > 65534 )) { - throw new InvalidConfigurationException("Specific ID:0 and ID:65535 values MUST NOT be used for identifying the LwM2M Server"); + if (!security.bootstrapServer && isNotLwm2mServer(srvCfg.shortId)) { + throw new InvalidConfigurationException("Specific ID:" + NOT_USED_IDENTIFYING_LWM2M_SERVER_MIN.getId() + " and ID:" + NOT_USED_IDENTIFYING_LWM2M_SERVER_MAX.getId() + " values MUST NOT be used for identifying the LwM2M Server"); } } } protected static BootstrapConfig.ServerSecurity getSecurityEntry(BootstrapConfig config, int shortId) { for (Map.Entry es : config.security.entrySet()) { - if (es.getValue().serverId == shortId) { + if ((es.getValue().serverId == null && shortId == 0) || + (es.getValue().serverId != null && es.getValue().serverId == shortId)) { return es.getValue(); } } diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/config/LwM2MTransportServerConfig.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/config/LwM2MTransportServerConfig.java index c1af8f553a..bf65b95dd2 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/config/LwM2MTransportServerConfig.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/config/LwM2MTransportServerConfig.java @@ -43,7 +43,7 @@ public class LwM2MTransportServerConfig implements LwM2MSecureServerConfig { private int dtlsRetransmissionTimeout; @Getter - @Value("${transport.lwm2m.dtls.connection_id_length:}") + @Value("${transport.lwm2m.dtls.connection_id_length:8}") private Integer dtlsCidLength; @Getter diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/DefaultLwM2mTransportService.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/DefaultLwM2mTransportService.java index 795f40aa20..789c8b6aac 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/DefaultLwM2mTransportService.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/DefaultLwM2mTransportService.java @@ -20,7 +20,6 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.eclipse.californium.core.CoapResource; import org.eclipse.californium.core.CoapServer; -import org.eclipse.californium.core.config.CoapConfig; import org.eclipse.californium.elements.config.Configuration; import org.eclipse.californium.scandium.config.DtlsConfig; import org.eclipse.californium.scandium.dtls.cipher.CipherSuite; @@ -196,14 +195,13 @@ public class DefaultLwM2mTransportService implements LwM2MTransportService { root = new CoapResource(""); coapServer.add(root); } - root.add(new LwM2mTransportCoapResource(otaPackageDataCache, FIRMWARE_UPDATE_COAP_RESOURCE, serverCoapConfig.get(CoapConfig.PREFERRED_BLOCK_SIZE), serverCoapConfig.get(CoapConfig.MAX_RESOURCE_BODY_SIZE))); - root.add(new LwM2mTransportCoapResource(otaPackageDataCache, SOFTWARE_UPDATE_COAP_RESOURCE,serverCoapConfig.get(CoapConfig.PREFERRED_BLOCK_SIZE), serverCoapConfig.get(CoapConfig.MAX_RESOURCE_BODY_SIZE))); + root.add(new LwM2mTransportCoapResource(otaPackageDataCache, FIRMWARE_UPDATE_COAP_RESOURCE)); + root.add(new LwM2mTransportCoapResource(otaPackageDataCache, SOFTWARE_UPDATE_COAP_RESOURCE)); } return leshanServer; } private void setServerWithCredentials(LeshanServerBuilder builder) { -// private void setServerWithCredentials(LeshanServerBuilder builder) { if (this.config.getSslCredentials() != null) { SslCredentials sslCredentials = this.config.getSslCredentials(); builder.setPublicKey(sslCredentials.getPublicKey()); diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportCoapResource.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportCoapResource.java index c46b57fb3f..752b888267 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportCoapResource.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportCoapResource.java @@ -34,21 +34,15 @@ import java.util.concurrent.atomic.AtomicInteger; import static org.thingsboard.server.transport.lwm2m.server.ota.DefaultLwM2MOtaUpdateService.FIRMWARE_UPDATE_COAP_RESOURCE; import static org.thingsboard.server.transport.lwm2m.server.ota.DefaultLwM2MOtaUpdateService.SOFTWARE_UPDATE_COAP_RESOURCE; -import static org.thingsboard.server.transport.lwm2m.utils.LwM2MTransportUtil.calculateSzx; @Slf4j public class LwM2mTransportCoapResource extends AbstractLwM2mTransportResource { - private final ConcurrentMap tokenToObserveRelationMap = new ConcurrentHashMap<>(); private final ConcurrentMap tokenToObserveNotificationSeqMap = new ConcurrentHashMap<>(); private final OtaPackageDataCache otaPackageDataCache; - private final int chunkSize; - private final int maxResourceBodySize; - public LwM2mTransportCoapResource(OtaPackageDataCache otaPackageDataCache, String name, int chunkSize, int maxResourceBodySize) { + public LwM2mTransportCoapResource(OtaPackageDataCache otaPackageDataCache, String name) { super(name); this.otaPackageDataCache = otaPackageDataCache; - this.chunkSize = chunkSize; - this.maxResourceBodySize = maxResourceBodySize; this.setObservable(true); // enable observing this.addObserver(new CoapResourceObserver()); } @@ -141,29 +135,23 @@ public class LwM2mTransportCoapResource extends AbstractLwM2mTransportResource { String idStr = exchange.getRequestOptions().getUriPath().get(exchange.getRequestOptions().getUriPath().size() - 1 ); UUID currentId = UUID.fromString(idStr); - log.info("Start Read ota data (path): [{}]", exchange.getRequestOptions().getUriPath().toString()); Response response = new Response(CoAP.ResponseCode.CONTENT); byte[] otaData = this.getOtaData(currentId); if (otaData != null && otaData.length > 0) { - if (otaData.length <= this.maxResourceBodySize) { - log.info("Read ota data (length): [{}]", otaData.length); - response.setPayload(otaData); - int chunkSize = calculateSzx(this.chunkSize); - if (exchange.getRequestOptions().hasBlock2()) { - chunkSize = exchange.getRequestOptions().getBlock2().getSzx(); - } else if (exchange.getRequestOptions().hasBlock1()) { - chunkSize = exchange.getRequestOptions().getBlock1().getSzx(); - } - log.info("With block2 Send currentId: [{}], length: [{}], chunkSize [{}], moreFlag [{}]", currentId.toString(), otaData.length, chunkSize, false); - boolean lastFlag = otaData.length <= this.chunkSize; - response.getOptions().setBlock2(chunkSize, lastFlag, 0); - response.setType(CoAP.Type.CON); - exchange.respond(response); + log.debug("Read ota data (length): [{}]", otaData.length); + response.setPayload(otaData); + if (exchange.getRequestOptions().getBlock2() != null) { + int szx = exchange.getRequestOptions().getBlock2().getSzx(); + int chunkSize = exchange.getRequestOptions().getBlock2().getSize(); + boolean lastFlag = otaData.length <= chunkSize; + response.getOptions().setBlock2(szx, lastFlag, 0); + log.trace("With block2 Send currentId: [{}], length: [{}], chunkSize [{}], szx [{}], moreFlag [{}]", currentId, otaData.length, chunkSize, szx, lastFlag); } else { - log.info("Ota package size: [{}] is larger than server's MAX_RESOURCE_BODY_SIZE [{}]", otaData.length, this.maxResourceBodySize); + log.trace("With block1 Send currentId: [{}], length: [{}], ", currentId, otaData.length); } + exchange.respond(response); } else { - log.info("Ota packaged currentId: [{}] is not found.", currentId.toString()); + log.trace("Ota packaged currentId: [{}] is not found.", currentId); } } diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportServerHelper.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportServerHelper.java index ea3358de60..416547ef36 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportServerHelper.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportServerHelper.java @@ -38,6 +38,7 @@ import java.io.ByteArrayInputStream; import java.io.IOException; import java.time.Instant; import java.util.ArrayList; +import java.util.Date; import java.util.List; import java.util.Map; import java.util.concurrent.atomic.AtomicLong; @@ -180,8 +181,16 @@ public class LwM2mTransportServerHelper { case BOOLEAN: kvProto.setType(BOOLEAN_V).setBoolV((Boolean) value).build(); break; - case STRING: case TIME: + if (value instanceof Date) { + kvProto.setType(TransportProtos.KeyValueType.LONG_V).setLongV(((Date) value).getTime()); + } else if (value instanceof Integer || value instanceof Long) { + kvProto.setType(TransportProtos.KeyValueType.LONG_V).setLongV((long) (value)); + } else { + kvProto.setType(TransportProtos.KeyValueType.STRING_V).setStringV(value.toString()); + } + break; + case STRING: case OPAQUE: case OBJLNK: kvProto.setType(TransportProtos.KeyValueType.STRING_V).setStringV((String) value); diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClient.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClient.java index 64597df9c5..884ba7e033 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClient.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClient.java @@ -66,7 +66,9 @@ import java.util.concurrent.locks.ReentrantLock; import java.util.stream.Collectors; import static org.thingsboard.server.common.data.lwm2m.LwM2mConstants.LWM2M_SEPARATOR_PATH; +import static org.thingsboard.server.transport.lwm2m.utils.LwM2MTransportUtil.BOOTSTRAP_TRIGGER_PARAMS_ID; import static org.thingsboard.server.transport.lwm2m.utils.LwM2MTransportUtil.LWM2M_OBJECT_VERSION_DEFAULT; +import static org.thingsboard.server.transport.lwm2m.utils.LwM2MTransportUtil.REGISTRATION_TRIGGER_PARAMS_ID; import static org.thingsboard.server.transport.lwm2m.utils.LwM2MTransportUtil.convertMultiResourceValuesFromRpcBody; import static org.thingsboard.server.transport.lwm2m.utils.LwM2MTransportUtil.equalsResourceTypeGetSimpleName; import static org.thingsboard.server.transport.lwm2m.utils.LwM2MTransportUtil.fromVersionedIdToObjectId; @@ -342,6 +344,10 @@ public class LwM2mClient { public String isValidObjectVersion(String path) { LwM2mPath pathIds = getLwM2mPathFromString(path); + if (pathIds.isResource() && (pathIds.toString().equals(REGISTRATION_TRIGGER_PARAMS_ID ) || + pathIds.toString().equals(BOOTSTRAP_TRIGGER_PARAMS_ID))) { + return ""; + } LwM2m.Version verSupportedObject = this.getSupportedObjectVersion(pathIds.getObjectId()); if (verSupportedObject == null) { return String.format("Specified object id %s absent in the list supported objects of the client or is security object!", pathIds.getObjectId()); diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/downlink/composite/TbLwM2MObserveCompositeCallback.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/downlink/composite/TbLwM2MObserveCompositeCallback.java index 660cf303f0..54164abacc 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/downlink/composite/TbLwM2MObserveCompositeCallback.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/downlink/composite/TbLwM2MObserveCompositeCallback.java @@ -23,6 +23,8 @@ import org.thingsboard.server.transport.lwm2m.server.downlink.TbLwM2MUplinkTarge import org.thingsboard.server.transport.lwm2m.server.log.LwM2MTelemetryLogService; import org.thingsboard.server.transport.lwm2m.server.uplink.LwM2mUplinkMsgHandler; +import java.util.concurrent.CountDownLatch; + @Slf4j public class TbLwM2MObserveCompositeCallback extends TbLwM2MUplinkTargetedCallback { diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/uplink/DefaultLwM2mUplinkMsgHandler.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/uplink/DefaultLwM2mUplinkMsgHandler.java index 431b623da9..b7e56c139a 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/uplink/DefaultLwM2mUplinkMsgHandler.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/uplink/DefaultLwM2mUplinkMsgHandler.java @@ -94,6 +94,8 @@ import org.thingsboard.server.transport.lwm2m.server.downlink.TbLwM2MWriteAttrib import org.thingsboard.server.transport.lwm2m.server.downlink.TbLwM2MWriteAttributesRequest; import org.thingsboard.server.transport.lwm2m.server.downlink.composite.TbLwM2MObserveCompositeCallback; import org.thingsboard.server.transport.lwm2m.server.downlink.composite.TbLwM2MObserveCompositeRequest; +import org.thingsboard.server.transport.lwm2m.server.downlink.composite.TbLwM2MReadCompositeCallback; +import org.thingsboard.server.transport.lwm2m.server.downlink.composite.TbLwM2MReadCompositeRequest; import org.thingsboard.server.transport.lwm2m.server.log.LwM2MTelemetryLogService; import org.thingsboard.server.transport.lwm2m.server.model.LwM2MModelConfig; import org.thingsboard.server.transport.lwm2m.server.model.LwM2MModelConfigService; @@ -222,8 +224,11 @@ public class DefaultLwM2mUplinkMsgHandler extends LwM2MExecutorAwareService impl log.info("[{}] Closing old session: {}", registration.getEndpoint(), new UUID(oldSessionInfo.get().getSessionIdMSB(), oldSessionInfo.get().getSessionIdLSB())); sessionManager.deregister(oldSessionInfo.get()); } - logService.log(lwM2MClient, LOG_LWM2M_INFO + ": Client registered with registration id: " + registration.getId() + " version: " - + registration.getLwM2mVersion() + " and modes: " + registration.getQueueMode() + ", " + registration.getBindingMode()); + String msgLogService = String.format(""" + %s: Endpoint [%s] Client registered with registration id: [%s] LwM2mVersion: [%s], SupportedObjectIdVer [%s] QueueMode [%s], BindingMode %s + """, LOG_LWM2M_INFO, registration.getEndpoint(), registration.getId(), registration.getLwM2mVersion(), registration.getSupportedObject(), registration.getQueueMode(), registration.getBindingMode()); + logService.log(lwM2MClient, msgLogService); + log.debug(msgLogService); sessionManager.register(lwM2MClient.getSession()); this.initClientTelemetry(lwM2MClient); this.initAttributes(lwM2MClient, true); @@ -242,7 +247,7 @@ public class DefaultLwM2mUplinkMsgHandler extends LwM2MExecutorAwareService impl logService.log(lwM2MClient, LOG_LWM2M_WARN + ": Client registration failed due to invalid state: " + stateException.getState()); } } catch (Throwable t) { - log.error("[{}] endpoint [{}] error Unable registration.", registration.getEndpoint(), t); + log.error("Endpoint [{}], Error Unable registration: [{}].", registration.getEndpoint(), t.getMessage(), t); logService.log(lwM2MClient, LOG_LWM2M_WARN + ": Client registration failed due to: " + t.getMessage()); } }); @@ -288,7 +293,6 @@ public class DefaultLwM2mUplinkMsgHandler extends LwM2MExecutorAwareService impl clientContext.unregister(client, registration); SessionInfoProto sessionInfo = client.getSession(); if (sessionInfo != null) { - securityStore.remove(client.getEndpoint(), client.getRegistration().getId()); sessionManager.deregister(sessionInfo); sessionStore.remove(registration.getEndpoint()); log.info("Client close session: [{}] unReg [{}] name [{}] profile ", registration.getId(), registration.getEndpoint(), sessionInfo.getDeviceType()); @@ -483,9 +487,9 @@ public class DefaultLwM2mUplinkMsgHandler extends LwM2MExecutorAwareService impl private void initClientTelemetry(LwM2mClient lwM2MClient) { Lwm2mDeviceProfileTransportConfiguration profile = clientContext.getProfile(lwM2MClient.getRegistration()); Set supportedObjects = clientContext.getSupportedIdVerInClient(lwM2MClient); - if (supportedObjects != null && supportedObjects.size() > 0) { - this.sendReadRequests(lwM2MClient, profile, supportedObjects); + if (supportedObjects != null && !supportedObjects.isEmpty()) { this.sendInitObserveRequests(lwM2MClient, profile, supportedObjects); + this.sendReadRequests(lwM2MClient, profile, supportedObjects); this.sendWriteAttributeRequests(lwM2MClient, profile, supportedObjects); // Removed. Used only for debug. // this.sendDiscoverRequests(lwM2MClient, profile, supportedObjects); @@ -495,14 +499,30 @@ public class DefaultLwM2mUplinkMsgHandler extends LwM2MExecutorAwareService impl private void sendReadRequests(LwM2mClient lwM2MClient, Lwm2mDeviceProfileTransportConfiguration profile, Set supportedObjects) { try { Set targetIds = new HashSet<>(profile.getObserveAttr().getAttribute()); + Boolean initAttrTelAsObsStrategy = profile.getObserveAttr().getInitAttrTelAsObsStrategy(); targetIds.addAll(profile.getObserveAttr().getTelemetry()); targetIds = diffSets(profile.getObserveAttr().getObserve(), targetIds); targetIds = targetIds.stream().filter(target -> isSupportedTargetId(supportedObjects, target)).collect(Collectors.toSet()); - - CountDownLatch latch = new CountDownLatch(targetIds.size()); - targetIds.forEach(versionedId -> sendReadRequest(lwM2MClient, versionedId, - new TbLwM2MLatchCallback<>(latch, new TbLwM2MReadCallback(this, logService, lwM2MClient, versionedId)))); - latch.await(config.getTimeout(), TimeUnit.MILLISECONDS); + if (!targetIds.isEmpty()) { + TelemetryObserveStrategy observeStrategy = profile.getObserveAttr().getObserveStrategy(); + long timeoutMs = config.getTimeout(); + if (initAttrTelAsObsStrategy && observeStrategy != SINGLE) { + switch (observeStrategy) { + case COMPOSITE_ALL -> { + sendReadCompositeRequest(lwM2MClient, targetIds.toArray(new String[0])); + } + case COMPOSITE_BY_OBJECT -> { + Map versionedObjectIds = groupByObjectIdVersionedIds(targetIds); + versionedObjectIds.forEach((k, v) -> sendReadCompositeRequest(lwM2MClient, v)); + } + } + } else { + CountDownLatch latch = new CountDownLatch(targetIds.size()); + targetIds.forEach(versionedId -> sendReadSingleRequest(lwM2MClient, versionedId, + new TbLwM2MLatchCallback<>(latch, new TbLwM2MReadCallback(this, logService, lwM2MClient, versionedId)))); + latch.await(timeoutMs, TimeUnit.MILLISECONDS); + } + } } catch (InterruptedException e) { log.error("[{}] Failed to await Read requests!", lwM2MClient.getEndpoint(), e); } catch (Exception e) { @@ -529,17 +549,11 @@ public class DefaultLwM2mUplinkMsgHandler extends LwM2MExecutorAwareService impl if (!completed) log.trace("[{}] Timeout occurred during SINGLE observe init", lwM2MClient.getEndpoint()); } case COMPOSITE_ALL -> { - CountDownLatch latch = new CountDownLatch(targetIds.size()); sendObserveCompositeRequest(lwM2MClient, targetIds.toArray(new String[0])); - boolean completed = latch.await(timeoutMs, TimeUnit.MILLISECONDS); - if (!completed) log.trace("[{}] Timeout occurred during COMPOSITE_ALL observe init", lwM2MClient.getEndpoint()); } case COMPOSITE_BY_OBJECT -> { Map versionedObjectIds = groupByObjectIdVersionedIds(targetIds); - CountDownLatch latch = new CountDownLatch(versionedObjectIds.size()); versionedObjectIds.forEach((k, v) -> sendObserveCompositeRequest(lwM2MClient, v)); - boolean completed = latch.await(timeoutMs, TimeUnit.MILLISECONDS); - if (!completed) log.trace("[{}] Timeout occurred during COMPOSITE_BY_OBJECT observe init", lwM2MClient.getEndpoint()); } } } @@ -562,23 +576,22 @@ public class DefaultLwM2mUplinkMsgHandler extends LwM2MExecutorAwareService impl } } - private void sendReadRequest(LwM2mClient lwM2MClient, String versionedId) { - sendReadRequest(lwM2MClient, versionedId, new TbLwM2MReadCallback(this, logService, lwM2MClient, versionedId)); - } - - private void sendReadRequest(LwM2mClient lwM2MClient, String versionedId, DownlinkRequestCallback callback) { + private void sendReadSingleRequest(LwM2mClient lwM2MClient, String versionedId, DownlinkRequestCallback callback) { TbLwM2MReadRequest request = TbLwM2MReadRequest.builder().versionedId(versionedId).timeout(clientContext.getRequestTimeout(lwM2MClient)).build(); defaultLwM2MDownlinkMsgHandler.sendReadRequest(lwM2MClient, request, callback); } - private void sendObserveRequest(LwM2mClient lwM2MClient, String versionedId) { - sendObserveRequest(lwM2MClient, versionedId, new TbLwM2MObserveCallback(this, logService, lwM2MClient, versionedId)); + private void sendReadCompositeRequest(LwM2mClient lwM2MClient, String[] versionedIds) { + TbLwM2MReadCompositeRequest request = TbLwM2MReadCompositeRequest.builder().versionedIds(versionedIds).timeout(clientContext.getRequestTimeout(lwM2MClient)).build(); + var mainCallback = new TbLwM2MReadCompositeCallback(this, logService, lwM2MClient, versionedIds); + defaultLwM2MDownlinkMsgHandler.sendReadCompositeRequest(lwM2MClient, request, mainCallback ); } private void sendObserveRequest(LwM2mClient lwM2MClient, String versionedId, DownlinkRequestCallback callback) { TbLwM2MObserveRequest request = TbLwM2MObserveRequest.builder().versionedId(versionedId).timeout(clientContext.getRequestTimeout(lwM2MClient)).build(); defaultLwM2MDownlinkMsgHandler.sendObserveRequest(lwM2MClient, request, callback); } + private void sendObserveCompositeRequest(LwM2mClient lwM2MClient, String[] versionedIds) { TbLwM2MObserveCompositeRequest request = TbLwM2MObserveCompositeRequest.builder().versionedIds(versionedIds).timeout(clientContext.getRequestTimeout(lwM2MClient)).build(); @@ -853,7 +866,8 @@ public class DefaultLwM2mUplinkMsgHandler extends LwM2MExecutorAwareService impl ResourceUpdateResult updateResource = new ResourceUpdateResult(lwM2MClient); request.getObjectInstances().forEach(instance -> instance.getResources().forEach((resId, lwM2mResource) ->{ - this.updateResourcesValue(updateResource, lwM2mResource, versionId + "/" + resId, Mode.REPLACE, 0); + String path = versionId.endsWith("/") ? versionId + resId : versionId + "/" + resId; + this.updateResourcesValue(updateResource, lwM2mResource, path, Mode.REPLACE, 0); }) ); clientContext.update(lwM2MClient); diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/utils/LwM2MTransportUtil.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/utils/LwM2MTransportUtil.java index 160ca3d905..75ac177969 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/utils/LwM2MTransportUtil.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/utils/LwM2MTransportUtil.java @@ -81,7 +81,8 @@ public class LwM2MTransportUtil { public static final String LOG_LWM2M_INFO = "info"; public static final String LOG_LWM2M_ERROR = "error"; public static final String LOG_LWM2M_WARN = "warn"; - public static final int BOOTSTRAP_DEFAULT_SHORT_ID_0 = 0; + public static final String REGISTRATION_TRIGGER_PARAMS_ID = "/1/0/8"; + public static final String BOOTSTRAP_TRIGGER_PARAMS_ID = "/1/0/9";; public static LwM2mOtaConvert convertOtaUpdateValueToString(String pathIdVer, Object value, ResourceModel.Type currentType) { String path = fromVersionedIdToObjectId(pathIdVer); @@ -120,10 +121,6 @@ public class LwM2MTransportUtil { } } - public static List getBootstrapParametersFromThingsboard(DeviceProfile deviceProfile) { - return toLwM2MClientProfile(deviceProfile).getBootstrap(); - } - public static String fromVersionedIdToObjectId(String pathIdVer) { try { if (pathIdVer == null) { @@ -398,13 +395,6 @@ public class LwM2MTransportUtil { } } - public static int calculateSzx(int size) { - if (size < 16 || size > 1024 || (size & (size - 1)) != 0) { - throw new IllegalArgumentException("Size must be a power of 2 between 16 and 1024."); - } - return (int) (Math.log(size / 16) / Math.log(2)); - } - public static ConcurrentHashMap groupByObjectIdVersionedIds(Set targetIds) { return targetIds.stream() .collect(Collectors.groupingBy( diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/utils/LwM2mValueConverterImpl.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/utils/LwM2mValueConverterImpl.java index 91d55305da..9d496a2f88 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/utils/LwM2mValueConverterImpl.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/utils/LwM2mValueConverterImpl.java @@ -33,6 +33,7 @@ import java.util.Date; import static org.eclipse.leshan.core.model.ResourceModel.Type.NONE; import static org.eclipse.leshan.core.model.ResourceModel.Type.OPAQUE; +import static org.eclipse.leshan.core.model.ResourceModel.Type.TIME; @Slf4j public class LwM2mValueConverterImpl implements LwM2mValueConverter { @@ -58,7 +59,7 @@ public class LwM2mValueConverterImpl implements LwM2mValueConverter { currentType = OPAQUE; } - if (currentType == expectedType || currentType == NONE) { + if (currentType == expectedType || currentType == NONE || currentType == TIME) { /** expected type */ return value; } @@ -135,7 +136,7 @@ public class LwM2mValueConverterImpl implements LwM2mValueConverter { **/ } catch (IllegalArgumentException e) { log.debug("Unable to convert string to date", e); - throw new CodecException("Unable to convert string (%s) to date for resource %s", value, + throw new CodecException("Unable to convert string (%s) to %s for resource %s", value, TIME.name(), resourcePath); } default: @@ -149,7 +150,7 @@ public class LwM2mValueConverterImpl implements LwM2mValueConverter { case FLOAT: return String.valueOf(value); case TIME: - String DATE_FORMAT = "MMM d, yyyy HH:mm a"; + String DATE_FORMAT = "yyyy-MM-dd[[ ]['T']HH:mm[:ss[.SSS]][ ][XXX][Z][z][VV][O]]"; Long timeValue; try { timeValue = ((Date) value).getTime(); diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttSslHandlerProvider.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttSslHandlerProvider.java index a756b84fe8..867b39cd23 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttSslHandlerProvider.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttSslHandlerProvider.java @@ -46,9 +46,6 @@ import java.security.cert.X509Certificate; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; -/** - * Created by valerii.sosliuk on 11/6/16. - */ @Slf4j @Component("MqttSslHandlerProvider") @ConditionalOnProperty(prefix = "transport.mqtt.ssl", value = "enabled", havingValue = "true", matchIfMissing = false) diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java index 7f9da9e0b9..4397802951 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java @@ -133,9 +133,6 @@ import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugMetr import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugTopic.parseTopic; import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugTopicService.parseTopicPublish; -/** - * @author Andrew Shvayka - */ @Slf4j public class MqttTransportHandler extends ChannelInboundHandlerAdapter implements GenericFutureListener>, SessionMsgListener { diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportServerInitializer.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportServerInitializer.java index c630def89f..5d127fccb7 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportServerInitializer.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportServerInitializer.java @@ -25,9 +25,6 @@ import io.netty.handler.ssl.SslHandler; import org.thingsboard.server.transport.mqtt.limits.IpFilter; import org.thingsboard.server.transport.mqtt.limits.ProxyIpFilter; -/** - * @author Andrew Shvayka - */ public class MqttTransportServerInitializer extends ChannelInitializer { private final MqttTransportContext context; diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportService.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportService.java index c7ff8912aa..d62f35d561 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportService.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportService.java @@ -35,9 +35,6 @@ import org.thingsboard.server.common.data.TbTransportService; import java.net.InetSocketAddress; -/** - * @author Andrew Shvayka - */ @Service("MqttTransportService") @TbMqttTransportComponent @Slf4j diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/JsonMqttAdaptor.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/JsonMqttAdaptor.java index 64f7e5df4a..61a886a7db 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/JsonMqttAdaptor.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/JsonMqttAdaptor.java @@ -45,10 +45,6 @@ import java.util.UUID; import static org.thingsboard.server.common.data.device.profile.MqttTopics.DEVICE_SOFTWARE_FIRMWARE_RESPONSES_TOPIC_FORMAT; - -/** - * @author Andrew Shvayka - */ @Component @Slf4j public class JsonMqttAdaptor implements MqttTransportAdaptor { @@ -122,7 +118,7 @@ public class JsonMqttAdaptor implements MqttTransportAdaptor { public Optional convertToGatewayPublish(MqttDeviceAwareSessionContext ctx, String deviceName, TransportProtos.GetAttributeResponseMsg responseMsg) throws AdaptorException { return processConvertFromGatewayAttributeResponseMsg(ctx, deviceName, responseMsg); } - + @Override public Optional convertToPublish(MqttDeviceAwareSessionContext ctx, TransportProtos.AttributeUpdateNotificationMsg notificationMsg, String topic) { return Optional.of(createMqttPublishMsg(ctx, topic, JsonConverter.toJson(notificationMsg))); diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/MqttTransportAdaptor.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/MqttTransportAdaptor.java index 012709c31f..356004246f 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/MqttTransportAdaptor.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/MqttTransportAdaptor.java @@ -41,9 +41,6 @@ import org.thingsboard.server.transport.mqtt.session.MqttDeviceAwareSessionConte import java.util.Optional; -/** - * @author Andrew Shvayka - */ public interface MqttTransportAdaptor { ByteBufAllocator ALLOCATOR = new UnpooledByteBufAllocator(false); @@ -90,4 +87,5 @@ public interface MqttTransportAdaptor { payload.writeBytes(payloadInBytes); return new MqttPublishMessage(mqttFixedHeader, header, payload); } + } diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/ProtoMqttAdaptor.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/ProtoMqttAdaptor.java index fd10ad750e..4472bb77d2 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/ProtoMqttAdaptor.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/ProtoMqttAdaptor.java @@ -48,7 +48,7 @@ public class ProtoMqttAdaptor implements MqttTransportAdaptor { public TransportProtos.PostTelemetryMsg convertToPostTelemetry(MqttDeviceAwareSessionContext ctx, MqttPublishMessage inbound) throws AdaptorException { DeviceSessionCtx deviceSessionCtx = (DeviceSessionCtx) ctx; byte[] bytes = toBytes(inbound.payload()); - Descriptors.Descriptor telemetryDynamicMsgDescriptor = ProtoConverter.validateDescriptor(deviceSessionCtx.getTelemetryDynamicMsgDescriptor()); + Descriptors.Descriptor telemetryDynamicMsgDescriptor = ProtoConverter.validateDescriptor(deviceSessionCtx.getTelemetryDynamicMessageDescriptor()); try { return JsonConverter.convertToTelemetryProto(JsonParser.parseString(ProtoConverter.dynamicMsgToJson(bytes, telemetryDynamicMsgDescriptor))); } catch (Exception e) { @@ -228,4 +228,5 @@ public class ProtoMqttAdaptor implements MqttTransportAdaptor { private int getRequestId(String topicName, String topic) { return Integer.parseInt(topicName.substring(topic.length())); } + } diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/AbstractGatewayDeviceSessionContext.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/AbstractGatewayDeviceSessionContext.java index 8ce0fc5c1e..5bb3c9c28f 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/AbstractGatewayDeviceSessionContext.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/AbstractGatewayDeviceSessionContext.java @@ -33,9 +33,6 @@ import org.thingsboard.server.gen.transport.TransportProtos.SessionInfoProto; import java.util.UUID; import java.util.concurrent.ConcurrentMap; -/** - * Created by ashvayka on 19.01.17. - */ @ToString(callSuper = true) @Slf4j public abstract class AbstractGatewayDeviceSessionContext extends MqttDeviceAwareSessionContext implements SessionMsgListener { diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/AbstractGatewaySessionHandler.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/AbstractGatewaySessionHandler.java index 91e7dedbf2..6542492238 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/AbstractGatewaySessionHandler.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/AbstractGatewaySessionHandler.java @@ -80,6 +80,8 @@ import java.util.Set; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; import java.util.function.Consumer; @@ -94,9 +96,6 @@ import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugConn import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugMessageType.STATE; import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugMessageType.messageName; -/** - * Created by ashvayka on 19.01.17. - */ @Slf4j public abstract class AbstractGatewaySessionHandler { @@ -114,6 +113,7 @@ public abstract class AbstractGatewaySessionHandler deviceCreationLockMap; + @Getter private final ConcurrentMap devices; private final ConcurrentMap> deviceFutures; protected final ConcurrentMap mqttQoSMap; @@ -405,18 +405,34 @@ public abstract class AbstractGatewaySessionHandler deviceEntry : json.getAsJsonObject().entrySet()) { - if (!deviceEntry.getValue().isJsonArray()) { - log.warn("{}[{}]", CAN_T_PARSE_VALUE, json); - continue; - } + + List> deviceEntries = json.getAsJsonObject().entrySet().stream() + .filter(entry -> { + final boolean isArray = entry.getValue().isJsonArray(); + if (!isArray) { + log.warn("{} device='{}' value={}", CAN_T_PARSE_VALUE, entry.getKey(), entry.getValue()); + } + return isArray; + }) + .toList(); + + if (deviceEntries.isEmpty()) { + log.debug("[{}][{}][{}] Devices telemetry message is empty", gateway.getTenantId(), gateway.getDeviceId(), sessionId); + throw new IllegalArgumentException("[" + sessionId + "] Devices telemetry message is empty for [" + gateway.getDeviceId() + "]"); + } + + AtomicInteger remaining = new AtomicInteger(deviceEntries.size()); + AtomicBoolean ackSent = new AtomicBoolean(false); + + for (Map.Entry deviceEntry : deviceEntries) { String deviceName = deviceEntry.getKey(); - process(deviceName, deviceCtx -> processPostTelemetryMsg(deviceCtx, deviceEntry.getValue(), deviceName, msgId), - t -> failedToProcessLog(deviceName, TELEMETRY, t)); + process(deviceName, deviceCtx -> processPostTelemetryMsg(deviceCtx, deviceEntry.getValue(), deviceName, msgId, + remaining, ackSent), + t -> processFailure(msgId, deviceName, TELEMETRY, ackSent, t)); } } - private void processPostTelemetryMsg(T deviceCtx, JsonElement msg, String deviceName, int msgId) { + private void processPostTelemetryMsg(T deviceCtx, JsonElement msg, String deviceName, int msgId, AtomicInteger remaining, AtomicBoolean ackSent) { try { long systemTs = System.currentTimeMillis(); TbPair> gatewayPayloadPair = JsonConverter.convertToGatewayTelemetry(msg.getAsJsonArray(), systemTs); @@ -425,10 +441,10 @@ public abstract class AbstractGatewaySessionHandler { String deviceName = checkDeviceName(telemetryMsg.getDeviceName()); - process(deviceName, deviceCtx -> processPostTelemetryMsg(deviceCtx, telemetryMsg.getMsg(), deviceName, msgId), - t -> failedToProcessLog(deviceName, TELEMETRY, t)); + process(deviceName, deviceCtx -> processPostTelemetryMsg(deviceCtx, telemetryMsg.getMsg(), deviceName, msgId, + remaining, ackSent), + t -> processFailure(msgId, deviceName, TELEMETRY, ackSent, t)); }); } catch (RuntimeException | InvalidProtocolBufferException e) { throw new AdaptorException(e); } } - protected void processPostTelemetryMsg(MqttDeviceAwareSessionContext deviceCtx, TransportProtos.PostTelemetryMsg msg, String deviceName, int msgId) { + protected void processPostTelemetryMsg(MqttDeviceAwareSessionContext deviceCtx, TransportProtos.PostTelemetryMsg msg, String deviceName, int msgId, + AtomicInteger remaining, AtomicBoolean ackSent) { try { TransportProtos.PostTelemetryMsg postTelemetryMsg = ProtoConverter.validatePostTelemetryMsg(msg.toByteArray()); - transportService.process(deviceCtx.getSessionInfo(), postTelemetryMsg, getPubAckCallback(channel, deviceName, msgId, postTelemetryMsg)); + transportService.process(deviceCtx.getSessionInfo(), postTelemetryMsg, getAggregatePubAckCallback(channel, msgId, deviceName, postTelemetryMsg, remaining, ackSent)); } catch (Throwable e) { log.warn("[{}][{}][{}] Failed to convert telemetry: [{}]", gateway.getTenantId(), gateway.getDeviceId(), deviceName, msg, e); - ackOrClose(msgId); + ackOrClose(msgId, ackSent); } } @@ -475,26 +496,42 @@ public abstract class AbstractGatewaySessionHandler deviceEntry : json.getAsJsonObject().entrySet()) { - if (!deviceEntry.getValue().isJsonObject()) { - log.warn("{}[{}]", CAN_T_PARSE_VALUE, json); - continue; - } + List> deviceEntries = json.getAsJsonObject().entrySet().stream() + .filter(entry -> { + boolean isJsonObject = entry.getValue().isJsonObject(); + if (!isJsonObject) { + log.warn("{} device='{}' value={}", CAN_T_PARSE_VALUE, entry.getKey(), entry.getValue()); + } + return isJsonObject; + }) + .toList(); + + if (deviceEntries.isEmpty()) { + log.debug("[{}][{}][{}] Devices claim message is empty", gateway.getTenantId(), gateway.getDeviceId(), sessionId); + throw new IllegalArgumentException("[" + sessionId + "] Devices claim message is empty for [" + gateway.getDeviceId() + "]"); + } + + AtomicInteger remaining = new AtomicInteger(deviceEntries.size()); + AtomicBoolean ackSent = new AtomicBoolean(false); + + for (Map.Entry deviceEntry : deviceEntries) { String deviceName = deviceEntry.getKey(); - process(deviceName, deviceCtx -> processClaimDeviceMsg(deviceCtx, deviceEntry.getValue(), deviceName, msgId), - t -> failedToProcessLog(deviceName, CLAIMING, t)); + process(deviceName, deviceCtx -> processClaimDeviceMsg(deviceCtx, deviceEntry.getValue(), deviceName, msgId, + remaining, ackSent), + t -> processFailure(msgId, deviceName, CLAIMING, ackSent, t)); } } - private void processClaimDeviceMsg(MqttDeviceAwareSessionContext deviceCtx, JsonElement claimRequest, String deviceName, int msgId) { + private void processClaimDeviceMsg(MqttDeviceAwareSessionContext deviceCtx, JsonElement claimRequest, String deviceName, int msgId, + AtomicInteger remaining, AtomicBoolean ackSent) { try { DeviceId deviceId = deviceCtx.getDeviceId(); TransportProtos.ClaimDeviceMsg claimDeviceMsg = JsonConverter.convertToClaimDeviceProto(deviceId, claimRequest); - transportService.process(deviceCtx.getSessionInfo(), claimDeviceMsg, getPubAckCallback(channel, deviceName, msgId, claimDeviceMsg)); + transportService.process(deviceCtx.getSessionInfo(), claimDeviceMsg, getAggregatePubAckCallback(channel, msgId, deviceName, claimDeviceMsg, remaining, ackSent)); } catch (Throwable e) { log.warn("[{}][{}][{}] Failed to convert claim message: [{}]", gateway.getTenantId(), gateway.getDeviceId(), deviceName, claimRequest, e); - ackOrClose(msgId); + ackOrClose(msgId, ackSent); } } @@ -507,49 +544,70 @@ public abstract class AbstractGatewaySessionHandler { String deviceName = checkDeviceName(claimDeviceMsg.getDeviceName()); - process(deviceName, deviceCtx -> processClaimDeviceMsg(deviceCtx, claimDeviceMsg.getClaimRequest(), deviceName, msgId), - t -> failedToProcessLog(deviceName, CLAIMING, t)); + process(deviceName, deviceCtx -> processClaimDeviceMsg(deviceCtx, claimDeviceMsg.getClaimRequest(), deviceName, msgId, + remaining, ackSent), + t -> processFailure(msgId, deviceName, CLAIMING, ackSent, t)); }); } catch (RuntimeException | InvalidProtocolBufferException e) { throw new AdaptorException(e); } } - private void processClaimDeviceMsg(MqttDeviceAwareSessionContext deviceCtx, TransportApiProtos.ClaimDevice claimRequest, String deviceName, int msgId) { + private void processClaimDeviceMsg(MqttDeviceAwareSessionContext deviceCtx, TransportApiProtos.ClaimDevice claimRequest, String deviceName, int msgId, + AtomicInteger remaining, AtomicBoolean ackSent) { try { DeviceId deviceId = deviceCtx.getDeviceId(); TransportProtos.ClaimDeviceMsg claimDeviceMsg = ProtoConverter.convertToClaimDeviceProto(deviceId, claimRequest.toByteArray()); - transportService.process(deviceCtx.getSessionInfo(), claimDeviceMsg, getPubAckCallback(channel, deviceName, msgId, claimDeviceMsg)); + transportService.process(deviceCtx.getSessionInfo(), claimDeviceMsg, getAggregatePubAckCallback(channel, msgId, deviceName, claimDeviceMsg, remaining, ackSent)); } catch (Throwable e) { log.warn("[{}][{}][{}] Failed to convert claim message: [{}]", gateway.getTenantId(), gateway.getDeviceId(), deviceName, claimRequest, e); - ackOrClose(msgId); + ackOrClose(msgId, ackSent); } } private void onDeviceAttributesJson(int msgId, ByteBuf payload) throws AdaptorException { JsonElement json = JsonMqttAdaptor.validateJsonPayload(sessionId, payload); validateJsonObject(json); - for (Map.Entry deviceEntry : json.getAsJsonObject().entrySet()) { - if (!deviceEntry.getValue().isJsonObject()) { - log.warn("{}[{}]", CAN_T_PARSE_VALUE, json); - continue; - } + List> deviceEntries = json.getAsJsonObject().entrySet().stream() + .filter(entry -> { + boolean isJsonObject = entry.getValue().isJsonObject(); + if (!isJsonObject) { + log.warn("{} device='{}' value={}", CAN_T_PARSE_VALUE, entry.getKey(), entry.getValue()); + } + return isJsonObject; + }) + .toList(); + + if (deviceEntries.isEmpty()) { + log.debug("[{}][{}][{}] Devices attribute message is empty", gateway.getTenantId(), gateway.getDeviceId(), sessionId); + throw new IllegalArgumentException("[" + sessionId + "] Devices attribute message is empty for [" + gateway.getDeviceId() + "]"); + } + + AtomicInteger remaining = new AtomicInteger(deviceEntries.size()); + AtomicBoolean ackSent = new AtomicBoolean(false); + + for (Map.Entry deviceEntry : deviceEntries) { String deviceName = deviceEntry.getKey(); - process(deviceName, deviceCtx -> processPostAttributesMsg(deviceCtx, deviceEntry.getValue(), deviceName, msgId), - t -> failedToProcessLog(deviceName, ATTRIBUTE, t)); + process(deviceName, deviceCtx -> processPostAttributesMsg(deviceCtx, deviceEntry.getValue(), deviceName, msgId, + remaining, ackSent), + t -> processFailure(msgId, deviceName, ATTRIBUTE, ackSent, t)); } } - private void processPostAttributesMsg(MqttDeviceAwareSessionContext deviceCtx, JsonElement msg, String deviceName, int msgId) { + private void processPostAttributesMsg(MqttDeviceAwareSessionContext deviceCtx, JsonElement msg, String deviceName, int msgId, + AtomicInteger remaining, AtomicBoolean ackSent) { try { TransportProtos.PostAttributeMsg postAttributeMsg = JsonConverter.convertToAttributesProto(msg.getAsJsonObject()); - transportService.process(deviceCtx.getSessionInfo(), postAttributeMsg, getPubAckCallback(channel, deviceName, msgId, postAttributeMsg)); + transportService.process(deviceCtx.getSessionInfo(), postAttributeMsg, getAggregatePubAckCallback(channel, msgId, deviceName, postAttributeMsg, remaining, ackSent)); } catch (Throwable e) { log.warn("[{}][{}][{}] Failed to process device attributes command: [{}]", gateway.getTenantId(), gateway.getDeviceId(), deviceName, msg, e); - ackOrClose(msgId); + ackOrClose(msgId, ackSent); } } @@ -562,23 +620,28 @@ public abstract class AbstractGatewaySessionHandler { String deviceName = checkDeviceName(attributesMsg.getDeviceName()); - process(deviceName, deviceCtx -> processPostAttributesMsg(deviceCtx, attributesMsg.getMsg(), deviceName, msgId), - t -> failedToProcessLog(deviceName, ATTRIBUTE, t)); + process(deviceName, deviceCtx -> processPostAttributesMsg(deviceCtx, attributesMsg.getMsg(), deviceName, msgId, + remaining, ackSent), + t -> processFailure(msgId, deviceName, ATTRIBUTE, ackSent, t)); }); } catch (RuntimeException | InvalidProtocolBufferException e) { throw new AdaptorException(e); } } - protected void processPostAttributesMsg(MqttDeviceAwareSessionContext deviceCtx, TransportProtos.PostAttributeMsg kvListProto, String deviceName, int msgId) { + protected void processPostAttributesMsg(MqttDeviceAwareSessionContext deviceCtx, TransportProtos.PostAttributeMsg kvListProto, String deviceName, int msgId, + AtomicInteger remaining, AtomicBoolean ackSent) { try { TransportProtos.PostAttributeMsg postAttributeMsg = ProtoConverter.validatePostAttributeMsg(kvListProto); - transportService.process(deviceCtx.getSessionInfo(), postAttributeMsg, getPubAckCallback(channel, deviceName, msgId, postAttributeMsg)); + transportService.process(deviceCtx.getSessionInfo(), postAttributeMsg, getAggregatePubAckCallback(channel, msgId, deviceName, postAttributeMsg, remaining, ackSent)); } catch (Throwable e) { log.warn("[{}][{}][{}] Failed to process device attributes command: [{}]", gateway.getTenantId(), gateway.getDeviceId(), deviceName, kvListProto, e); - ackOrClose(msgId); + ackOrClose(msgId, ackSent); } } @@ -647,27 +710,34 @@ public abstract class AbstractGatewaySessionHandler processRpcResponseMsg(deviceCtx, requestId, data, deviceName, msgId), - t -> failedToProcessLog(deviceName, RPC_RESPONSE, t)); + AtomicInteger remaining = new AtomicInteger(1); + AtomicBoolean ackSent = new AtomicBoolean(false); + process(deviceName, deviceCtx -> processRpcResponseMsg(deviceCtx, requestId, data, deviceName, msgId, remaining, ackSent), + t -> processFailure(msgId, deviceName, RPC_RESPONSE, ackSent, t)); } - private void processRpcResponseMsg(MqttDeviceAwareSessionContext deviceCtx, Integer requestId, String data, String deviceName, int msgId) { + private void processRpcResponseMsg(MqttDeviceAwareSessionContext deviceCtx, Integer requestId, String data, String deviceName, + int msgId, AtomicInteger remaining, AtomicBoolean ackSent) { TransportProtos.ToDeviceRpcResponseMsg rpcResponseMsg = TransportProtos.ToDeviceRpcResponseMsg.newBuilder() .setRequestId(requestId).setPayload(data).build(); - transportService.process(deviceCtx.getSessionInfo(), rpcResponseMsg, getPubAckCallback(channel, deviceName, msgId, rpcResponseMsg)); + transportService.process(deviceCtx.getSessionInfo(), rpcResponseMsg, + getAggregatePubAckCallback(channel, msgId, deviceName, rpcResponseMsg, remaining, ackSent)); } private void processGetAttributeRequestMessage(MqttPublishMessage mqttMsg, String deviceName, TransportProtos.GetAttributeRequestMsg requestMsg) { int msgId = getMsgId(mqttMsg); - process(deviceName, deviceCtx -> processGetAttributeRequestMessage(deviceCtx, requestMsg, deviceName, msgId), - t -> { - failedToProcessLog(deviceName, ATTRIBUTES_REQUEST, t); - ack(mqttMsg, MqttReasonCodes.PubAck.IMPLEMENTATION_SPECIFIC_ERROR); - }); + AtomicInteger remaining = new AtomicInteger(1); + AtomicBoolean ackSent = new AtomicBoolean(false); + process(deviceName, deviceCtx -> { + processGetAttributeRequestMessage(deviceCtx, requestMsg, deviceName, msgId, remaining, ackSent); + }, + t -> processFailure(msgId, deviceName, ATTRIBUTES_REQUEST, ackSent, MqttReasonCodes.PubAck.IMPLEMENTATION_SPECIFIC_ERROR, t)); } - private void processGetAttributeRequestMessage(T deviceCtx, TransportProtos.GetAttributeRequestMsg requestMsg, String deviceName, int msgId) { - transportService.process(deviceCtx.getSessionInfo(), requestMsg, getPubAckCallback(channel, deviceName, msgId, requestMsg)); + private void processGetAttributeRequestMessage(T deviceCtx, TransportProtos.GetAttributeRequestMsg requestMsg, + String deviceName, int msgId, AtomicInteger remaining, AtomicBoolean ackSent) { + transportService.process(deviceCtx.getSessionInfo(), requestMsg, + getAggregatePubAckCallback(channel, msgId, deviceName, requestMsg, remaining, ackSent)); } private TransportProtos.GetAttributeRequestMsg toGetAttributeRequestMsg(int requestId, boolean clientScope, Set keys) { @@ -718,9 +788,11 @@ public abstract class AbstractGatewaySessionHandler pubAckCallback = getAggregatePubAckCallback(channel, -1, deviceName, postTelemetryMsg, + new AtomicInteger(1), new AtomicBoolean(false)); + transportService.process(sessionInfo, postTelemetryMsg, pubAckCallback); } - public ConcurrentMap getDevices () { - return this.devices; - } + protected TransportServiceCallback getAggregatePubAckCallback( + final ChannelHandlerContext ctx, + final int msgId, + final String deviceName, + final T msg, + final AtomicInteger remaining, + final AtomicBoolean ackSent) { - private TransportServiceCallback getPubAckCallback(final ChannelHandlerContext ctx, final String deviceName, final int msgId, final T msg) { return new TransportServiceCallback() { @Override public void onSuccess(Void dummy) { log.trace("[{}][{}][{}][{}] Published msg: [{}]", gateway.getTenantId(), gateway.getDeviceId(), sessionId, deviceName, msg); - if (msgId > 0) { - ctx.writeAndFlush(MqttTransportHandler.createMqttPubAckMsg(deviceSessionCtx, msgId, MqttReasonCodes.PubAck.SUCCESS.byteValue())); - } else { - log.trace("[{}][{}][{}] Wrong msg id: [{}]", gateway.getTenantId(), gateway.getDeviceId(), sessionId, msg); - ctx.writeAndFlush(MqttTransportHandler.createMqttPubAckMsg(deviceSessionCtx, msgId, MqttReasonCodes.PubAck.UNSPECIFIED_ERROR.byteValue())); + if (remaining.decrementAndGet() == 0 && ackSent.compareAndSet(false, true)) { + if (msgId > 0) { + ctx.writeAndFlush(MqttTransportHandler.createMqttPubAckMsg( + deviceSessionCtx, msgId, MqttReasonCodes.PubAck.SUCCESS.byteValue())); + } else { + log.trace("[{}][{}][{}] Wrong msg id: [{}]", gateway.getTenantId(), gateway.getDeviceId(), sessionId, msgId); + ctx.writeAndFlush(MqttTransportHandler.createMqttPubAckMsg( + deviceSessionCtx, msgId, MqttReasonCodes.PubAck.UNSPECIFIED_ERROR.byteValue())); + } + } + if (msgId <= 0) { closeDeviceSession(deviceName, MqttReasonCodes.Disconnect.MALFORMED_PACKET); } } @@ -767,11 +850,20 @@ public abstract class AbstractGatewaySessionHandler getDefaultAdaptor(); + case V2_JSON -> context.getJsonMqttAdaptor(); + case V2_PROTO -> context.getProtoMqttAdaptor(); + default -> useJsonPayloadFormatForDefaultDownlinkTopics ? context.getJsonMqttAdaptor() : getDefaultAdaptor(); + }; } private MqttTransportAdaptor getDefaultAdaptor() { @@ -269,7 +246,7 @@ public class DeviceSessionCtx extends MqttDeviceAwareSessionContext { } } - public Collection getMsgQueueSnapshot(){ + public Collection getMsgQueueSnapshot() { return Collections.unmodifiableCollection(msgQueue); } diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewayDeviceSessionContext.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewayDeviceSessionContext.java index a48ee50054..24d7888a81 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewayDeviceSessionContext.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewayDeviceSessionContext.java @@ -22,9 +22,6 @@ import org.thingsboard.server.common.transport.auth.TransportDeviceInfo; import java.util.concurrent.ConcurrentMap; -/** - * Created by nickAS21 on 26.12.22 - */ @ToString(callSuper = true) public class GatewayDeviceSessionContext extends AbstractGatewayDeviceSessionContext { diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewaySessionHandler.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewaySessionHandler.java index 2980b2ad60..487fe1a541 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewaySessionHandler.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewaySessionHandler.java @@ -28,9 +28,6 @@ import org.thingsboard.server.gen.transport.TransportProtos; import java.util.Optional; import java.util.UUID; -/** - * Created by nickAS21 on 26.12.22 - */ @Slf4j public class GatewaySessionHandler extends AbstractGatewaySessionHandler { diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/MqttDeviceAwareSessionContext.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/MqttDeviceAwareSessionContext.java index 9db81a4d8b..24d23d7bdc 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/MqttDeviceAwareSessionContext.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/MqttDeviceAwareSessionContext.java @@ -25,9 +25,6 @@ import java.util.UUID; import java.util.concurrent.ConcurrentMap; import java.util.stream.Collectors; -/** - * Created by ashvayka on 30.08.18. - */ @ToString(callSuper = true) public abstract class MqttDeviceAwareSessionContext extends DeviceAwareSessionContext { @@ -47,7 +44,7 @@ public abstract class MqttDeviceAwareSessionContext extends DeviceAwareSessionCo .stream() .filter(entry -> entry.getKey().matches(topic)) .map(Map.Entry::getValue) - .collect(Collectors.toList()); + .toList(); if (!qosList.isEmpty()) { return MqttQoS.valueOf(qosList.get(0)); } else { diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/MqttTopicMatcher.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/MqttTopicMatcher.java index 950a1f5bd9..ad3c7b9897 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/MqttTopicMatcher.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/MqttTopicMatcher.java @@ -15,26 +15,25 @@ */ package org.thingsboard.server.transport.mqtt.session; +import lombok.Getter; + import java.util.regex.Pattern; public class MqttTopicMatcher { + @Getter private final String topic; private final Pattern topicRegex; public MqttTopicMatcher(String topic) { - if(topic == null){ + if (topic == null) { throw new NullPointerException("topic"); } this.topic = topic; this.topicRegex = Pattern.compile(topic.replace("+", "[^/]+").replace("#", ".+") + "$"); } - public String getTopic() { - return topic; - } - - public boolean matches(String topic){ + public boolean matches(String topic) { return this.topicRegex.matcher(topic).matches(); } @@ -52,4 +51,5 @@ public class MqttTopicMatcher { public int hashCode() { return topic.hashCode(); } + } diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/SparkplugNodeSessionHandler.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/SparkplugNodeSessionHandler.java index cdb562c063..9ba4c3f989 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/SparkplugNodeSessionHandler.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/SparkplugNodeSessionHandler.java @@ -21,6 +21,7 @@ import com.google.common.util.concurrent.MoreExecutors; import com.google.gson.JsonSyntaxException; import io.netty.handler.codec.mqtt.MqttMessage; import io.netty.handler.codec.mqtt.MqttPublishMessage; +import io.netty.handler.codec.mqtt.MqttReasonCodes; import io.netty.handler.codec.mqtt.MqttTopicSubscription; import lombok.Getter; import lombok.extern.slf4j.Slf4j; @@ -48,6 +49,8 @@ import java.util.Optional; import java.util.Set; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugConnectionState.ONLINE; import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugMessageType.DBIRTH; @@ -61,9 +64,6 @@ import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugMetr import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugTopicService.TOPIC_SPLIT_REGEXP; import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugTopicService.TOPIC_STATE_REGEXP; -/** - * Created by nickAS21 on 12.12.22 - */ @Slf4j @SpecVersion(spec = "sparkplug", version = "3.0.0") public class SparkplugNodeSessionHandler extends AbstractGatewaySessionHandler { @@ -144,37 +144,49 @@ public class SparkplugNodeSessionHandler extends AbstractGatewaySessionHandler contextListenableFuture, int msgId, List postTelemetryMsgList, String deviceName) { + if (CollectionUtils.isEmpty(postTelemetryMsgList)) { + log.debug("[{}] Device telemetry list is empty for: [{}]", sessionId, gateway.getDeviceId()); + } + + AtomicInteger remaining = new AtomicInteger(postTelemetryMsgList.size()); + AtomicBoolean ackSent = new AtomicBoolean(false); + process(contextListenableFuture, deviceCtx -> { for (TransportProtos.PostTelemetryMsg telemetryMsg : postTelemetryMsgList) { try { - processPostTelemetryMsg(deviceCtx, telemetryMsg, deviceName, msgId); + processPostTelemetryMsg(deviceCtx, telemetryMsg, deviceName, msgId, remaining, ackSent); } catch (Throwable e) { log.warn("[{}][{}] Failed to convert telemetry: {}", gateway.getDeviceId(), deviceName, telemetryMsg, e); - ackOrClose(msgId); + ackOrClose(msgId, ackSent); } } }, - t -> log.debug("[{}] Failed to process device telemetry command: {}", sessionId, deviceName, t)); + t -> processFailure(msgId, deviceName, "Failed to process device telemetry command", ackSent, t)); } private void onDeviceAttributesProto(ListenableFuture contextListenableFuture, int msgId, List attributesMsgList, String deviceName) throws AdaptorException { try { if (CollectionUtils.isEmpty(attributesMsgList)) { - log.debug("[{}] Devices attributes keys list is empty for: [{}]", sessionId, gateway.getDeviceId()); + log.debug("[{}] Device attribute list is empty for: [{}]", sessionId, gateway.getDeviceId()); } + + AtomicInteger remaining = new AtomicInteger(attributesMsgList.size()); + AtomicBoolean ackSent = new AtomicBoolean(false); + process(contextListenableFuture, deviceCtx -> { for (TransportApiProtos.AttributesMsg attributesMsg : attributesMsgList) { TransportProtos.PostAttributeMsg kvListProto = attributesMsg.getMsg(); try { TransportProtos.PostAttributeMsg postAttributeMsg = ProtoConverter.validatePostAttributeMsg(kvListProto); - processPostAttributesMsg(deviceCtx, postAttributeMsg, deviceName, msgId); + processPostAttributesMsg(deviceCtx, postAttributeMsg, deviceName, msgId, remaining, ackSent); } catch (Throwable e) { log.warn("[{}][{}] Failed to process device attributes command: {}", gateway.getDeviceId(), deviceName, kvListProto, e); + ackOrClose(msgId, ackSent); } } }, - t -> log.debug("[{}] Failed to process device attributes command: {}", sessionId, deviceName, t)); + t -> processFailure(msgId, deviceName, "Failed to process device attributes command", ackSent, t)); } catch (RuntimeException e) { throw new AdaptorException(e); } diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/config/ssl/SslCredentialsWebServerCustomizer.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/config/ssl/SslCredentialsWebServerCustomizer.java index 6c73bb03de..fc64be6e8b 100644 --- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/config/ssl/SslCredentialsWebServerCustomizer.java +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/config/ssl/SslCredentialsWebServerCustomizer.java @@ -59,6 +59,7 @@ public class SslCredentialsWebServerCustomizer implements WebServerFactoryCustom public void customize(ConfigurableServletWebServerFactory factory) { SslCredentials sslCredentials = this.httpServerSslCredentialsConfig.getCredentials(); Ssl ssl = serverProperties.getSsl(); + ssl.setBundle("default"); ssl.setKeyAlias(sslCredentials.getKeyAlias()); ssl.setKeyPassword(sslCredentials.getKeyPassword()); factory.setSsl(ssl); diff --git a/common/util/src/main/java/org/thingsboard/common/util/CachedValue.java b/common/util/src/main/java/org/thingsboard/common/util/CachedValue.java new file mode 100644 index 0000000000..b0a41c2a42 --- /dev/null +++ b/common/util/src/main/java/org/thingsboard/common/util/CachedValue.java @@ -0,0 +1,38 @@ +/** + * Copyright © 2016-2025 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.common.util; + +import com.github.benmanes.caffeine.cache.Caffeine; +import com.github.benmanes.caffeine.cache.LoadingCache; + +import java.util.concurrent.TimeUnit; +import java.util.function.Supplier; + +public class CachedValue { + + private final LoadingCache cache; + + public CachedValue(Supplier supplier, long valueTtlMs) { + this.cache = Caffeine.newBuilder() + .expireAfterWrite(valueTtlMs, TimeUnit.MILLISECONDS) + .build(__ -> supplier.get()); + } + + public V get() { + return cache.get(this); + } + +} diff --git a/common/util/src/main/java/org/thingsboard/common/util/DonAsynchron.java b/common/util/src/main/java/org/thingsboard/common/util/DonAsynchron.java index 0f1a56cb17..79b8181548 100644 --- a/common/util/src/main/java/org/thingsboard/common/util/DonAsynchron.java +++ b/common/util/src/main/java/org/thingsboard/common/util/DonAsynchron.java @@ -15,12 +15,15 @@ */ package org.thingsboard.common.util; +import com.google.common.util.concurrent.FluentFuture; import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.MoreExecutors; +import com.google.common.util.concurrent.SettableFuture; import java.util.concurrent.Callable; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.Executor; import java.util.function.Consumer; @@ -71,4 +74,16 @@ public class DonAsynchron { return future; } + public static FluentFuture toFluentFuture(CompletableFuture completable) { + SettableFuture future = SettableFuture.create(); + completable.whenComplete((result, exception) -> { + if (exception != null) { + future.setException(exception); + } else { + future.set(result); + } + }); + return FluentFuture.from(future); + } + } diff --git a/common/util/src/main/java/org/thingsboard/common/util/ExpressionFunctionsUtil.java b/common/util/src/main/java/org/thingsboard/common/util/ExpressionUtils.java similarity index 87% rename from common/util/src/main/java/org/thingsboard/common/util/ExpressionFunctionsUtil.java rename to common/util/src/main/java/org/thingsboard/common/util/ExpressionUtils.java index b1753e7a17..96b45123a1 100644 --- a/common/util/src/main/java/org/thingsboard/common/util/ExpressionFunctionsUtil.java +++ b/common/util/src/main/java/org/thingsboard/common/util/ExpressionUtils.java @@ -15,13 +15,16 @@ */ package org.thingsboard.common.util; +import net.objecthunter.exp4j.Expression; +import net.objecthunter.exp4j.ExpressionBuilder; import net.objecthunter.exp4j.function.Function; import net.objecthunter.exp4j.function.Functions; import java.util.ArrayList; import java.util.List; +import java.util.Set; -public class ExpressionFunctionsUtil { +public class ExpressionUtils { public static final List userDefinedFunctions = new ArrayList<>(); @@ -75,4 +78,13 @@ public class ExpressionFunctionsUtil { userDefinedFunctions.add(Functions.getBuiltinFunction("signum")); } + public static Expression createExpression(String expression, Set variables) { + return new ExpressionBuilder(expression) + .functions(userDefinedFunctions) + .implicitMultiplication(true) + .operator() + .variables(variables) + .build(); + } + } diff --git a/common/util/src/main/java/org/thingsboard/common/util/JacksonUtil.java b/common/util/src/main/java/org/thingsboard/common/util/JacksonUtil.java index cd61c7ac20..e3ae70c9a5 100644 --- a/common/util/src/main/java/org/thingsboard/common/util/JacksonUtil.java +++ b/common/util/src/main/java/org/thingsboard/common/util/JacksonUtil.java @@ -83,6 +83,12 @@ public class JacksonUtil { .addModule(new Jdk8Module()) .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) .build(); + public static final ObjectMapper CANONICAL_JSON_MAPPER = JsonMapper.builder() + .addModule(new Jdk8Module()) + .configure(SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS, true) + .configure(MapperFeature.SORT_PROPERTIES_ALPHABETICALLY, true) + .serializationInclusion(com.fasterxml.jackson.annotation.JsonInclude.Include.NON_NULL) + .build(); public static ObjectMapper getObjectMapperWithJavaTimeModule() { return JsonMapper.builder() @@ -207,6 +213,23 @@ public class JacksonUtil { return data; } + public static String toCanonicalString(Object value) { + try { + if (value == null) { + return null; + } + + if (value instanceof JsonNode) { + Object pojo = CANONICAL_JSON_MAPPER.convertValue(value, Object.class); + return CANONICAL_JSON_MAPPER.writeValueAsString(pojo); + } + + return CANONICAL_JSON_MAPPER.writeValueAsString(value); + } catch (Exception e) { + throw new IllegalArgumentException("The given Json object value cannot be transformed to a canonical String: " + value, e); + } + } + public static T treeToValue(JsonNode node, Class clazz) { try { return OBJECT_MAPPER.treeToValue(node, clazz); diff --git a/common/util/src/main/java/org/thingsboard/common/util/KvUtil.java b/common/util/src/main/java/org/thingsboard/common/util/KvUtil.java index a924b0228e..0d9b8494f6 100644 --- a/common/util/src/main/java/org/thingsboard/common/util/KvUtil.java +++ b/common/util/src/main/java/org/thingsboard/common/util/KvUtil.java @@ -61,6 +61,37 @@ public class KvUtil { } } + public static Long getLongValue(KvEntry entry) { + switch (entry.getDataType()) { + case LONG -> { + return entry.getLongValue().orElse(null); + } + case DOUBLE -> { + return entry.getDoubleValue().map(Double::longValue).orElse(null); + } + case BOOLEAN -> { + return entry.getBooleanValue().map(b -> b ? 1L : 0L).orElse(null); + } + case STRING -> { + try { + return Long.parseLong(entry.getStrValue().orElse("")); + } catch (RuntimeException e) { + return null; + } + } + case JSON -> { + try { + return Long.parseLong(entry.getJsonValue().orElse("")); + } catch (RuntimeException e) { + return null; + } + } + default -> { + return null; + } + } + } + public static Boolean getBoolValue(KvEntry entry) { switch (entry.getDataType()) { case LONG: diff --git a/common/util/src/main/java/org/thingsboard/common/util/geo/CirclePerimeterDefinition.java b/common/util/src/main/java/org/thingsboard/common/util/geo/CirclePerimeterDefinition.java new file mode 100644 index 0000000000..4d5390a27a --- /dev/null +++ b/common/util/src/main/java/org/thingsboard/common/util/geo/CirclePerimeterDefinition.java @@ -0,0 +1,38 @@ +/** + * Copyright © 2016-2025 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.common.util.geo; + +import lombok.Data; + +@Data +public class CirclePerimeterDefinition implements PerimeterDefinition { + + private final Double latitude; + private final Double longitude; + private final Double radius; + + @Override + public PerimeterType getType() { + return PerimeterType.CIRCLE; + } + + @Override + public boolean checkMatches(Coordinates entityCoordinates) { + Coordinates perimeterCoordinates = new Coordinates(latitude, longitude); + return radius > GeoUtil.distance(entityCoordinates, perimeterCoordinates, RangeUnit.METER); + } + +} diff --git a/common/util/src/main/java/org/thingsboard/common/util/geo/PerimeterDefinition.java b/common/util/src/main/java/org/thingsboard/common/util/geo/PerimeterDefinition.java new file mode 100644 index 0000000000..7c7f2cd1a3 --- /dev/null +++ b/common/util/src/main/java/org/thingsboard/common/util/geo/PerimeterDefinition.java @@ -0,0 +1,35 @@ +/** + * Copyright © 2016-2025 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.common.util.geo; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; + +import java.io.Serializable; + +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonDeserialize(using = PerimeterDefinitionDeserializer.class) +@JsonSerialize(using = PerimeterDefinitionSerializer.class) +public interface PerimeterDefinition extends Serializable { + + @JsonIgnore + PerimeterType getType(); + + @JsonIgnore + boolean checkMatches(Coordinates entityCoordinates); +} diff --git a/common/util/src/main/java/org/thingsboard/common/util/geo/PerimeterDefinitionDeserializer.java b/common/util/src/main/java/org/thingsboard/common/util/geo/PerimeterDefinitionDeserializer.java new file mode 100644 index 0000000000..e60e314fe0 --- /dev/null +++ b/common/util/src/main/java/org/thingsboard/common/util/geo/PerimeterDefinitionDeserializer.java @@ -0,0 +1,48 @@ +/** + * Copyright © 2016-2025 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.common.util.geo; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.ObjectCodec; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; + +import java.io.IOException; + +public class PerimeterDefinitionDeserializer extends JsonDeserializer { + + @Override + public PerimeterDefinition deserialize(JsonParser p, DeserializationContext ctx) throws IOException { + ObjectCodec codec = p.getCodec(); + JsonNode node = codec.readTree(p); + + if (node.isObject()) { + double latitude = node.get("latitude").asDouble(); + double longitude = node.get("longitude").asDouble(); + double radius = node.get("radius").asDouble(); + return new CirclePerimeterDefinition(latitude, longitude, radius); + } + if (node.isArray()) { + ObjectMapper mapper = (ObjectMapper) p.getCodec(); + String polygonStrDefinition = mapper.writeValueAsString(node); + return new PolygonPerimeterDefinition(polygonStrDefinition); + } + throw new IOException("Failed to deserialize PerimeterDefinition from node: " + node); + } + +} diff --git a/common/util/src/main/java/org/thingsboard/common/util/geo/PerimeterDefinitionSerializer.java b/common/util/src/main/java/org/thingsboard/common/util/geo/PerimeterDefinitionSerializer.java new file mode 100644 index 0000000000..d27aafecc0 --- /dev/null +++ b/common/util/src/main/java/org/thingsboard/common/util/geo/PerimeterDefinitionSerializer.java @@ -0,0 +1,49 @@ +/** + * Copyright © 2016-2025 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.common.util.geo; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializerProvider; +import org.thingsboard.server.common.data.StringUtils; + +import java.io.IOException; + +public class PerimeterDefinitionSerializer extends JsonSerializer { + + @Override + public void serialize(PerimeterDefinition value, JsonGenerator gen, SerializerProvider serializers) throws IOException { + if (value instanceof CirclePerimeterDefinition c) { + gen.writeStartObject(); + gen.writeNumberField("latitude", c.getLatitude()); + gen.writeNumberField("longitude", c.getLongitude()); + gen.writeNumberField("radius", c.getRadius()); + gen.writeEndObject(); + return; + } + if (value instanceof PolygonPerimeterDefinition p) { + String raw = p.getPolygonDefinition(); + if (StringUtils.isBlank(raw)) { + throw new IOException("Failed to serialize PolygonPerimeterDefinition with blank: " + value); + } + ObjectMapper mapper = (ObjectMapper) gen.getCodec(); + gen.writeTree(mapper.readTree(raw)); + return; + } + throw new IOException("Failed to serialize PerimeterDefinition from value: " + value); + } +} diff --git a/common/util/src/main/java/org/thingsboard/common/util/geo/PolygonPerimeterDefinition.java b/common/util/src/main/java/org/thingsboard/common/util/geo/PolygonPerimeterDefinition.java new file mode 100644 index 0000000000..2d8ca6ef56 --- /dev/null +++ b/common/util/src/main/java/org/thingsboard/common/util/geo/PolygonPerimeterDefinition.java @@ -0,0 +1,35 @@ +/** + * Copyright © 2016-2025 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.common.util.geo; + +import lombok.Data; + +@Data +public class PolygonPerimeterDefinition implements PerimeterDefinition { + + private final String polygonDefinition; + + @Override + public PerimeterType getType() { + return PerimeterType.POLYGON; + } + + @Override + public boolean checkMatches(Coordinates entityCoordinates) { + return GeoUtil.contains(polygonDefinition, entityCoordinates); + } + +} diff --git a/common/util/src/test/java/org/thingsboard/common/util/geo/PerimeterDefinitionDeserializerTest.java b/common/util/src/test/java/org/thingsboard/common/util/geo/PerimeterDefinitionDeserializerTest.java new file mode 100644 index 0000000000..5c00847861 --- /dev/null +++ b/common/util/src/test/java/org/thingsboard/common/util/geo/PerimeterDefinitionDeserializerTest.java @@ -0,0 +1,50 @@ +/** + * Copyright © 2016-2025 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.common.util.geo; + +import org.junit.jupiter.api.Test; +import org.thingsboard.common.util.JacksonUtil; + +import static org.assertj.core.api.Assertions.assertThat; + +public class PerimeterDefinitionDeserializerTest { + + @Test + void shouldDeserializeCircle() { + String json = """ + {"latitude":50.45,"longitude":30.52,"radius":100.0}"""; + + PerimeterDefinition def = JacksonUtil.fromString(json, PerimeterDefinition.class); + + assertThat(def).isNotNull().isInstanceOf(CirclePerimeterDefinition.class); + + CirclePerimeterDefinition circle = (CirclePerimeterDefinition) def; + assertThat(circle.getLatitude()).isEqualTo(50.45); + assertThat(circle.getLongitude()).isEqualTo(30.52); + assertThat(circle.getRadius()).isEqualTo(100.0); + } + + @Test + void shouldDeserializePolygon() { + String json = "[[50.45,30.52],[50.46,30.53],[50.44,30.54]]"; + + PerimeterDefinition def = JacksonUtil.fromString(json, PerimeterDefinition.class); + + assertThat(def).isInstanceOf(PolygonPerimeterDefinition.class); + PolygonPerimeterDefinition poly = (PolygonPerimeterDefinition) def; + assertThat(poly.getPolygonDefinition()).isEqualTo(json); + } +} diff --git a/common/util/src/test/java/org/thingsboard/common/util/geo/PerimeterDefinitionSerializerTest.java b/common/util/src/test/java/org/thingsboard/common/util/geo/PerimeterDefinitionSerializerTest.java new file mode 100644 index 0000000000..d316d1c398 --- /dev/null +++ b/common/util/src/test/java/org/thingsboard/common/util/geo/PerimeterDefinitionSerializerTest.java @@ -0,0 +1,52 @@ +/** + * Copyright © 2016-2025 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.common.util.geo; + +import com.fasterxml.jackson.databind.JsonNode; +import org.junit.jupiter.api.Test; +import org.thingsboard.common.util.JacksonUtil; + +import static org.assertj.core.api.Assertions.assertThat; + +public class PerimeterDefinitionSerializerTest { + + @Test + void shouldSerializeCircle() { + PerimeterDefinition circle = new CirclePerimeterDefinition(50.45, 30.52, 120.0); + + String json = JacksonUtil.writeValueAsString(circle); + + JsonNode actual = JacksonUtil.toJsonNode(json); + assertThat(actual.get("latitude").asDouble()).isEqualTo(50.45); + assertThat(actual.get("longitude").asDouble()).isEqualTo(30.52); + assertThat(actual.get("radius").asDouble()).isEqualTo(120.0); + } + + @Test + void shouldSerializePolygon() throws Exception { + String rawArray = "[[50.45,30.52],[50.46,30.53],[50.44,30.54]]"; + PerimeterDefinition polygon = new PolygonPerimeterDefinition(rawArray); + + String json = JacksonUtil.writeValueAsString(polygon); + + JsonNode actual = JacksonUtil.toJsonNode(json); + JsonNode expected = JacksonUtil.toJsonNode(rawArray); + assertThat(actual).isEqualTo(expected); + assertThat(actual.isArray()).isTrue(); + assertThat(actual.size()).isEqualTo(3); + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/sync/DefaultGitSyncService.java b/common/version-control/src/main/java/org/thingsboard/server/service/sync/DefaultGitSyncService.java similarity index 100% rename from application/src/main/java/org/thingsboard/server/service/sync/DefaultGitSyncService.java rename to common/version-control/src/main/java/org/thingsboard/server/service/sync/DefaultGitSyncService.java diff --git a/application/src/main/java/org/thingsboard/server/service/sync/GitSyncService.java b/common/version-control/src/main/java/org/thingsboard/server/service/sync/GitSyncService.java similarity index 100% rename from application/src/main/java/org/thingsboard/server/service/sync/GitSyncService.java rename to common/version-control/src/main/java/org/thingsboard/server/service/sync/GitSyncService.java diff --git a/dao/src/main/java/org/thingsboard/server/dao/Dao.java b/dao/src/main/java/org/thingsboard/server/dao/Dao.java index 72883c55ef..4934059cd5 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/Dao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/Dao.java @@ -16,6 +16,7 @@ package org.thingsboard.server.dao; import com.google.common.util.concurrent.ListenableFuture; +import org.thingsboard.server.common.data.EntityInfo; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.edqs.fields.EntityFields; import org.thingsboard.server.common.data.id.TenantId; @@ -32,6 +33,10 @@ public interface Dao { ListenableFuture findByIdAsync(TenantId tenantId, UUID id); + default List findEntityInfosByNamePrefix(TenantId tenantId, String name) { + throw new UnsupportedOperationException(); + } + boolean existsById(TenantId tenantId, UUID id); ListenableFuture existsByIdAsync(TenantId tenantId, UUID id); diff --git a/dao/src/main/java/org/thingsboard/server/dao/ResourceContainerDao.java b/dao/src/main/java/org/thingsboard/server/dao/ResourceContainerDao.java index 6a952fd501..93cc64b2db 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/ResourceContainerDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/ResourceContainerDao.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.dao; +import org.thingsboard.server.common.data.EntityInfo; import org.thingsboard.server.common.data.id.HasId; import org.thingsboard.server.common.data.id.TenantId; @@ -22,8 +23,8 @@ import java.util.List; public interface ResourceContainerDao> { - List findByTenantIdAndResourceLink(TenantId tenantId, String link, int limit); + List findByTenantIdAndResource(TenantId tenantId, String reference, int limit); - List findByResourceLink(String link, int limit); + List findByResource(String reference, int limit); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/ai/AiModelServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/ai/AiModelServiceImpl.java index b091a29247..89240647ba 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/ai/AiModelServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/ai/AiModelServiceImpl.java @@ -29,13 +29,17 @@ import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.dao.entity.CachedVersionedEntityService; +import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent; +import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent; import org.thingsboard.server.dao.model.sql.AiModelEntity; import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.dao.sql.JpaExecutorService; import java.util.Optional; import java.util.Set; +import java.util.UUID; +import static com.google.common.util.concurrent.MoreExecutors.directExecutor; import static org.thingsboard.server.dao.service.Validator.validatePageLink; @Service @@ -63,11 +67,23 @@ class AiModelServiceImpl extends CachedVersionedEntityService findAiModelByTenantIdAndId(tenantId, modelId))); } + @Override + public Optional findAiModelByTenantIdAndName(TenantId tenantId, String name) { + return Optional.ofNullable(aiModelDao.findByTenantIdAndName(tenantId.getId(), name)); + } + @Override @Transactional public boolean deleteByTenantIdAndId(TenantId tenantId, AiModelId modelId) { - return deleteByTenantIdAndIdInternal(tenantId, modelId); + return deleteByTenantIdAndIdInternal(tenantId, modelId.getId()); } @Override @@ -115,6 +136,12 @@ class AiModelServiceImpl extends CachedVersionedEntityService model); // necessary to cast to HasId } + @Override + public FluentFuture>> findEntityAsync(TenantId tenantId, EntityId entityId) { + return findAiModelByTenantIdAndIdAsync(tenantId, new AiModelId(entityId.getId())) + .transform(modelOpt -> modelOpt.map(model -> model), directExecutor()); // necessary to cast to HasId + } + @Override public long countByTenantId(TenantId tenantId) { return aiModelDao.countByTenantId(tenantId); @@ -123,14 +150,21 @@ class AiModelServiceImpl extends CachedVersionedEntityService>> findEntityAsync(TenantId tenantId, EntityId entityId) { + return FluentFuture.from(findAlarmByIdAsync(tenantId, new AlarmId(entityId.getId()))) + .transform(Optional::ofNullable, directExecutor()); + } + @Override public EntityType getEntityType() { return EntityType.ALARM; diff --git a/dao/src/main/java/org/thingsboard/server/dao/asset/AssetProfileServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/asset/AssetProfileServiceImpl.java index 6bf44e4da1..4ddbabb5b6 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/asset/AssetProfileServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/asset/AssetProfileServiceImpl.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.dao.asset; +import com.google.common.util.concurrent.FluentFuture; import lombok.extern.slf4j.Slf4j; import org.hibernate.exception.ConstraintViolationException; import org.springframework.beans.factory.annotation.Autowired; @@ -49,6 +50,7 @@ import java.util.Map; import java.util.Optional; import java.util.stream.Collectors; +import static com.google.common.util.concurrent.MoreExecutors.directExecutor; import static org.thingsboard.server.dao.service.Validator.validateId; @Service("AssetProfileDaoService") @@ -323,6 +325,12 @@ public class AssetProfileServiceImpl extends CachedVersionedEntityService>> findEntityAsync(TenantId tenantId, EntityId entityId) { + return FluentFuture.from(assetProfileDao.findByIdAsync(tenantId, entityId.getId())) + .transform(Optional::ofNullable, directExecutor()); + } + @Override public EntityType getEntityType() { return EntityType.ASSET_PROFILE; diff --git a/dao/src/main/java/org/thingsboard/server/dao/asset/BaseAssetService.java b/dao/src/main/java/org/thingsboard/server/dao/asset/BaseAssetService.java index fed201d403..7d8442fd3a 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/asset/BaseAssetService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/asset/BaseAssetService.java @@ -15,7 +15,7 @@ */ package org.thingsboard.server.dao.asset; - +import com.google.common.util.concurrent.FluentFuture; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.MoreExecutors; @@ -26,6 +26,8 @@ import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.event.TransactionalEventListener; import org.thingsboard.server.common.data.EntitySubtype; import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.NameConflictPolicy; +import org.thingsboard.server.common.data.NameConflictStrategy; import org.thingsboard.server.common.data.ProfileEntityIdInfo; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.asset.Asset; @@ -62,6 +64,7 @@ import java.util.List; import java.util.Optional; import java.util.stream.Collectors; +import static com.google.common.util.concurrent.MoreExecutors.directExecutor; import static org.thingsboard.server.dao.DaoUtil.toUUIDs; import static org.thingsboard.server.dao.service.Validator.validateId; import static org.thingsboard.server.dao.service.Validator.validateIds; @@ -93,8 +96,8 @@ public class BaseAssetService extends AbstractCachedEntityService keys = new ArrayList<>(2); keys.add(new AssetCacheKey(event.getTenantId(), event.getNewName())); @@ -146,14 +149,24 @@ public class BaseAssetService extends AbstractCachedEntityService saveAsset(asset, true, nameConflictStrategy)); + } + @Override public Asset saveAsset(Asset asset, boolean doValidate) { + return saveEntity(asset, () -> saveAsset(asset, doValidate, NameConflictStrategy.DEFAULT)); + } + + private Asset saveAsset(Asset asset, boolean doValidate, NameConflictStrategy nameConflictStrategy) { log.trace("Executing saveAsset [{}]", asset); - Asset oldAsset = null; + Asset oldAsset = (asset.getId() != null) ? assetDao.findById(asset.getTenantId(), asset.getId().getId()) : null; + if (nameConflictStrategy.policy() == NameConflictPolicy.UNIQUIFY && (oldAsset == null || !oldAsset.getName().equals(asset.getName()))) { + uniquifyEntityName(asset, oldAsset, asset::setName, EntityType.ASSET, nameConflictStrategy); + } if (doValidate) { - oldAsset = assetValidator.validate(asset, Asset::getTenantId); - } else if (asset.getId() != null) { - oldAsset = findAssetById(asset.getTenantId(), asset.getId()); + assetValidator.validate(asset, Asset::getTenantId); } AssetCacheEvictEvent evictEvent = new AssetCacheEvictEvent(asset.getTenantId(), asset.getName(), oldAsset != null ? oldAsset.getName() : null); Asset savedAsset; @@ -519,6 +532,12 @@ public class BaseAssetService extends AbstractCachedEntityService>> findEntityAsync(TenantId tenantId, EntityId entityId) { + return FluentFuture.from(findAssetByIdAsync(tenantId, new AssetId(entityId.getId()))) + .transform(Optional::ofNullable, directExecutor()); + } + @Override public long countByTenantId(TenantId tenantId) { return assetDao.countByTenantId(tenantId); diff --git a/dao/src/main/java/org/thingsboard/server/dao/attributes/BaseAttributesService.java b/dao/src/main/java/org/thingsboard/server/dao/attributes/BaseAttributesService.java index 9803670d4b..62b35caa7f 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/attributes/BaseAttributesService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/attributes/BaseAttributesService.java @@ -26,6 +26,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.context.annotation.Primary; import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.AttributeScope; +import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.ObjectType; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.edqs.AttributeKv; @@ -118,7 +119,8 @@ public class BaseAttributesService implements AttributesService { List> futures = new ArrayList<>(attributes.size()); for (AttributeKvEntry attribute : attributes) { ListenableFuture future = Futures.transform(attributesDao.save(tenantId, entityId, scope, attribute), version -> { - edqsService.onUpdate(tenantId, ObjectType.ATTRIBUTE_KV, new AttributeKv(entityId, scope, attribute, version)); + TenantId edqsTenantId = entityId.getEntityType() == EntityType.TENANT ? (TenantId) entityId : tenantId; + edqsService.onUpdate(edqsTenantId, ObjectType.ATTRIBUTE_KV, new AttributeKv(entityId, scope, attribute, version)); return version; }, MoreExecutors.directExecutor()); futures.add(future); @@ -136,7 +138,8 @@ public class BaseAttributesService implements AttributesService { String key = keyVersionPair.getFirst(); Long version = keyVersionPair.getSecond(); if (version != null) { - edqsService.onDelete(tenantId, ObjectType.ATTRIBUTE_KV, new AttributeKv(entityId, scope, key, version)); + TenantId edqsTenantId = entityId.getEntityType() == EntityType.TENANT ? (TenantId) entityId : tenantId; + edqsService.onDelete(edqsTenantId, ObjectType.ATTRIBUTE_KV, new AttributeKv(entityId, scope, key, version)); } keys.add(key); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/attributes/CachedAttributesService.java b/dao/src/main/java/org/thingsboard/server/dao/attributes/CachedAttributesService.java index d99413f13a..44b2daaf8e 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/attributes/CachedAttributesService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/attributes/CachedAttributesService.java @@ -30,6 +30,7 @@ import org.springframework.stereotype.Service; import org.thingsboard.server.cache.TbCacheValueWrapper; import org.thingsboard.server.cache.VersionedTbCache; import org.thingsboard.server.common.data.AttributeScope; +import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.ObjectType; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.edqs.AttributeKv; @@ -239,7 +240,8 @@ public class CachedAttributesService implements AttributesService { ListenableFuture future = Futures.transform(attributesDao.save(tenantId, entityId, scope, attribute), version -> { BaseAttributeKvEntry attributeKvEntry = new BaseAttributeKvEntry(((BaseAttributeKvEntry) attribute).getKv(), attribute.getLastUpdateTs(), version); put(entityId, scope, attributeKvEntry); - edqsService.onUpdate(tenantId, ObjectType.ATTRIBUTE_KV, new AttributeKv(entityId, scope, attributeKvEntry, version)); + TenantId edqsTenantId = entityId.getEntityType() == EntityType.TENANT ? (TenantId) entityId : tenantId; + edqsService.onUpdate(edqsTenantId, ObjectType.ATTRIBUTE_KV, new AttributeKv(entityId, scope, attributeKvEntry, version)); return version; }, cacheExecutor); futures.add(future); @@ -263,7 +265,8 @@ public class CachedAttributesService implements AttributesService { Long version = keyVersionPair.getSecond(); cache.evict(new AttributeCacheKey(scope, entityId, key), version); if (version != null) { - edqsService.onDelete(tenantId, ObjectType.ATTRIBUTE_KV, new AttributeKv(entityId, scope, key, version)); + TenantId edqsTenantId = entityId.getEntityType() == EntityType.TENANT ? (TenantId) entityId : tenantId; + edqsService.onDelete(edqsTenantId, ObjectType.ATTRIBUTE_KV, new AttributeKv(entityId, scope, key, version)); } return key; }, cacheExecutor)).toList()); diff --git a/dao/src/main/java/org/thingsboard/server/dao/cf/BaseCalculatedFieldService.java b/dao/src/main/java/org/thingsboard/server/dao/cf/BaseCalculatedFieldService.java index c0cb886747..d3ca3ccff3 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/cf/BaseCalculatedFieldService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/cf/BaseCalculatedFieldService.java @@ -15,15 +15,15 @@ */ package org.thingsboard.server.dao.cf; +import com.google.common.util.concurrent.FluentFuture; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.cf.CalculatedField; -import org.thingsboard.server.common.data.cf.CalculatedFieldLink; +import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.cf.configuration.CalculatedFieldConfiguration; import org.thingsboard.server.common.data.id.CalculatedFieldId; -import org.thingsboard.server.common.data.id.CalculatedFieldLinkId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.HasId; import org.thingsboard.server.common.data.id.TenantId; @@ -33,11 +33,14 @@ import org.thingsboard.server.dao.entity.AbstractEntityService; import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent; import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent; import org.thingsboard.server.dao.exception.IncorrectParameterException; -import org.thingsboard.server.dao.service.DataValidator; +import org.thingsboard.server.dao.service.validator.CalculatedFieldDataValidator; +import java.util.EnumSet; import java.util.List; import java.util.Optional; +import java.util.Set; +import static com.google.common.util.concurrent.MoreExecutors.directExecutor; import static org.thingsboard.server.dao.service.Validator.validateId; import static org.thingsboard.server.dao.service.Validator.validatePageLink; @@ -51,14 +54,11 @@ public class BaseCalculatedFieldService extends AbstractEntityService implements public static final String INCORRECT_ENTITY_ID = "Incorrect entityId "; private final CalculatedFieldDao calculatedFieldDao; - private final CalculatedFieldLinkDao calculatedFieldLinkDao; - private final DataValidator calculatedFieldDataValidator; - private final DataValidator calculatedFieldLinkDataValidator; + private final CalculatedFieldDataValidator calculatedFieldDataValidator; @Override public CalculatedField save(CalculatedField calculatedField) { - CalculatedField oldCalculatedField = calculatedFieldDataValidator.validate(calculatedField, CalculatedField::getTenantId); - return doSave(calculatedField, oldCalculatedField); + return save(calculatedField, true); } @Override @@ -79,14 +79,19 @@ public class BaseCalculatedFieldService extends AbstractEntityService implements log.trace("Executing save calculated field, [{}]", calculatedField); updateDebugSettings(tenantId, calculatedField, System.currentTimeMillis()); CalculatedField savedCalculatedField = calculatedFieldDao.save(tenantId, calculatedField); - createOrUpdateCalculatedFieldLink(tenantId, savedCalculatedField); - eventPublisher.publishEvent(SaveEntityEvent.builder().tenantId(savedCalculatedField.getTenantId()).entityId(savedCalculatedField.getId()) - .entity(savedCalculatedField).oldEntity(oldCalculatedField).created(calculatedField.getId() == null).build()); + eventPublisher.publishEvent(SaveEntityEvent.builder() + .tenantId(savedCalculatedField.getTenantId()) + .entityId(savedCalculatedField.getId()) + .entity(savedCalculatedField) + .oldEntity(oldCalculatedField) + .created(calculatedField.getId() == null) + .build()); return savedCalculatedField; } catch (Exception e) { checkConstraintViolation(e, - "calculated_field_unq_key", "Calculated Field with such name is already in exists!", - "calculated_field_external_id_unq_key", "Calculated Field with such external id already exists!"); + "calculated_field_unq_key", calculatedField.getType() == CalculatedFieldType.ALARM ? + "Alarm rule with such type already exists" : "Calculated field with such name and type already exists", + "calculated_field_external_id_unq_key", "Calculated field with such external id already exists"); throw e; } } @@ -100,10 +105,10 @@ public class BaseCalculatedFieldService extends AbstractEntityService implements } @Override - public CalculatedField findByEntityIdAndName(EntityId entityId, String name) { - log.trace("Executing findByEntityIdAndName [{}], calculatedFieldName[{}]", entityId, name); + public CalculatedField findByEntityIdAndTypeAndName(EntityId entityId, CalculatedFieldType type, String name) { + log.trace("Executing findByEntityIdAndTypeAndName entityId [{}], type [{}], name [{}]", entityId, type, name); validateId(entityId.getId(), id -> INCORRECT_ENTITY_ID + id); - return calculatedFieldDao.findByEntityIdAndName(entityId, name); + return calculatedFieldDao.findByEntityIdAndTypeAndName(entityId, type, name); } @Override @@ -136,11 +141,18 @@ public class BaseCalculatedFieldService extends AbstractEntityService implements } @Override - public PageData findAllCalculatedFieldsByEntityId(TenantId tenantId, EntityId entityId, PageLink pageLink) { + public PageData findCalculatedFieldsByEntityId(TenantId tenantId, EntityId entityId, CalculatedFieldType type, PageLink pageLink) { log.trace("Executing findAllByEntityId, entityId [{}], pageLink [{}]", entityId, pageLink); validateId(entityId.getId(), id -> INCORRECT_ENTITY_ID + id); validatePageLink(pageLink); - return calculatedFieldDao.findAllByEntityId(tenantId, entityId, pageLink); + Set types; + if (type == null) { + types = EnumSet.allOf(CalculatedFieldType.class); + types.remove(CalculatedFieldType.ALARM); + } else { + types = Set.of(type); + } + return calculatedFieldDao.findByEntityIdAndTypes(tenantId, entityId, types, pageLink); } @Override @@ -178,48 +190,6 @@ public class BaseCalculatedFieldService extends AbstractEntityService implements return calculatedFields.size(); } - @Override - public CalculatedFieldLink saveCalculatedFieldLink(TenantId tenantId, CalculatedFieldLink calculatedFieldLink) { - calculatedFieldLinkDataValidator.validate(calculatedFieldLink, CalculatedFieldLink::getTenantId); - log.trace("Executing save calculated field link, [{}]", calculatedFieldLink); - return calculatedFieldLinkDao.save(tenantId, calculatedFieldLink); - } - - @Override - public CalculatedFieldLink findCalculatedFieldLinkById(TenantId tenantId, CalculatedFieldLinkId calculatedFieldLinkId) { - log.trace("Executing findCalculatedFieldLinkById, tenantId [{}], calculatedFieldLinkId [{}]", tenantId, calculatedFieldLinkId); - validateId(tenantId, id -> INCORRECT_TENANT_ID + id); - validateId(calculatedFieldLinkId, id -> "Incorrect calculatedFieldLinkId " + id); - return calculatedFieldLinkDao.findById(tenantId, calculatedFieldLinkId.getId()); - } - - @Override - public List findAllCalculatedFieldLinksById(TenantId tenantId, CalculatedFieldId calculatedFieldId) { - log.trace("Executing findAllCalculatedFieldLinksById, calculatedFieldId [{}]", calculatedFieldId); - return calculatedFieldLinkDao.findCalculatedFieldLinksByCalculatedFieldId(tenantId, calculatedFieldId); - } - - @Override - public List findAllCalculatedFieldLinksByEntityId(TenantId tenantId, EntityId entityId) { - log.trace("Executing findAllCalculatedFieldLinksByEntityId, entityId [{}]", entityId); - return calculatedFieldLinkDao.findCalculatedFieldLinksByEntityId(tenantId, entityId); - } - - @Override - public PageData findAllCalculatedFieldLinksByTenantId(TenantId tenantId, PageLink pageLink) { - log.trace("Executing findAllCalculatedFieldLinksByTenantId, tenantId[{}] pageLink [{}]", tenantId, pageLink); - validateId(tenantId, id -> INCORRECT_TENANT_ID + id); - validatePageLink(pageLink); - return calculatedFieldLinkDao.findAllByTenantId(tenantId, pageLink); - } - - @Override - public PageData findAllCalculatedFieldLinks(PageLink pageLink) { - log.trace("Executing findAllCalculatedFieldLinks, pageLink [{}]", pageLink); - validatePageLink(pageLink); - return calculatedFieldLinkDao.findAll(pageLink); - } - @Override public boolean referencedInAnyCalculatedField(TenantId tenantId, EntityId referencedEntityId) { return calculatedFieldDao.findAllByTenantId(tenantId).stream() @@ -235,13 +205,14 @@ public class BaseCalculatedFieldService extends AbstractEntityService implements } @Override - public EntityType getEntityType() { - return EntityType.CALCULATED_FIELD; + public FluentFuture>> findEntityAsync(TenantId tenantId, EntityId entityId) { + return FluentFuture.from(calculatedFieldDao.findByIdAsync(tenantId, entityId.getId())) + .transform(Optional::ofNullable, directExecutor()); } - private void createOrUpdateCalculatedFieldLink(TenantId tenantId, CalculatedField calculatedField) { - List links = calculatedField.getConfiguration().buildCalculatedFieldLinks(tenantId, calculatedField.getEntityId(), calculatedField.getId()); - links.forEach(link -> saveCalculatedFieldLink(tenantId, link)); + @Override + public EntityType getEntityType() { + return EntityType.CALCULATED_FIELD; } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/cf/CalculatedFieldDao.java b/dao/src/main/java/org/thingsboard/server/dao/cf/CalculatedFieldDao.java index d5465cb8a1..9e8ee61d4c 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/cf/CalculatedFieldDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/cf/CalculatedFieldDao.java @@ -16,6 +16,7 @@ package org.thingsboard.server.dao.cf; import org.thingsboard.server.common.data.cf.CalculatedField; +import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.id.CalculatedFieldId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; @@ -24,6 +25,7 @@ import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.dao.Dao; import java.util.List; +import java.util.Set; public interface CalculatedFieldDao extends Dao { @@ -35,16 +37,16 @@ public interface CalculatedFieldDao extends Dao { List findAll(); - CalculatedField findByEntityIdAndName(EntityId entityId, String name); + CalculatedField findByEntityIdAndTypeAndName(EntityId entityId, CalculatedFieldType type, String name); PageData findAll(PageLink pageLink); PageData findAllByTenantId(TenantId tenantId, PageLink pageLink); - PageData findAllByEntityId(TenantId tenantId, EntityId entityId, PageLink pageLink); + PageData findByEntityIdAndTypes(TenantId tenantId, EntityId entityId, Set types, PageLink pageLink); List removeAllByEntityId(TenantId tenantId, EntityId entityId); - long countCFByEntityId(TenantId tenantId, EntityId entityId); + long countByEntityIdAndTypeNot(TenantId tenantId, EntityId entityId, CalculatedFieldType type); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/cf/CalculatedFieldLinkDao.java b/dao/src/main/java/org/thingsboard/server/dao/cf/CalculatedFieldLinkDao.java deleted file mode 100644 index dd184289ed..0000000000 --- a/dao/src/main/java/org/thingsboard/server/dao/cf/CalculatedFieldLinkDao.java +++ /dev/null @@ -1,42 +0,0 @@ -/** - * Copyright © 2016-2025 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.dao.cf; - -import org.thingsboard.server.common.data.cf.CalculatedFieldLink; -import org.thingsboard.server.common.data.id.CalculatedFieldId; -import org.thingsboard.server.common.data.id.EntityId; -import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.page.PageData; -import org.thingsboard.server.common.data.page.PageLink; -import org.thingsboard.server.dao.Dao; - -import java.util.List; - -public interface CalculatedFieldLinkDao extends Dao { - - List findCalculatedFieldLinksByCalculatedFieldId(TenantId tenantId, CalculatedFieldId calculatedFieldId); - - List findCalculatedFieldLinksByEntityId(TenantId tenantId, EntityId entityId); - - List findCalculatedFieldLinksByTenantId(TenantId tenantId); - - List findAll(); - - PageData findAll(PageLink pageLink); - - PageData findAllByTenantId(TenantId tenantId, PageLink pageLink); - -} diff --git a/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerServiceImpl.java index b06902d95e..4187a96acc 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerServiceImpl.java @@ -16,6 +16,7 @@ package org.thingsboard.server.dao.customer; import com.fasterxml.jackson.databind.JsonNode; +import com.google.common.util.concurrent.FluentFuture; import com.google.common.util.concurrent.ListenableFuture; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; @@ -28,6 +29,8 @@ import org.thingsboard.server.cache.customer.CustomerCacheEvictEvent; import org.thingsboard.server.cache.customer.CustomerCacheKey; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.NameConflictPolicy; +import org.thingsboard.server.common.data.NameConflictStrategy; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.EntityId; @@ -55,6 +58,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Optional; +import static com.google.common.util.concurrent.MoreExecutors.directExecutor; import static org.thingsboard.server.dao.service.Validator.validateId; @Service("CustomerDaoService") @@ -139,19 +143,29 @@ public class CustomerServiceImpl extends AbstractCachedEntityService saveCustomer(customer, true, nameConflictStrategy)); } private Customer saveCustomer(Customer customer, boolean doValidate) { + return saveCustomer(customer, doValidate, NameConflictStrategy.DEFAULT); + } + + private Customer saveCustomer(Customer customer, boolean doValidate, NameConflictStrategy nameConflictStrategy) { log.trace("Executing saveCustomer [{}]", customer); - String oldCustomerTitle = null; + Customer oldCustomer = (customer.getId() != null) ? customerDao.findById(customer.getTenantId(), customer.getId().getId()) : null; + if (nameConflictStrategy.policy() == NameConflictPolicy.UNIQUIFY && (oldCustomer == null || !oldCustomer.getTitle().equals(customer.getTitle()))) { + uniquifyEntityName(customer, oldCustomer, customer::setTitle, EntityType.CUSTOMER, nameConflictStrategy); + } if (doValidate) { - Customer oldCustomer = customerValidator.validate(customer, Customer::getTenantId); - if (oldCustomer != null) { - oldCustomerTitle = oldCustomer.getTitle(); - } + customerValidator.validate(customer, Customer::getTenantId); } - var evictEvent = new CustomerCacheEvictEvent(customer.getTenantId(), customer.getTitle(), oldCustomerTitle); + var evictEvent = new CustomerCacheEvictEvent(customer.getTenantId(), customer.getTitle(), oldCustomer != null ? oldCustomer.getTitle() : null); try { Customer savedCustomer = customerDao.saveAndFlush(customer.getTenantId(), customer); if (!savedCustomer.isPublic()) { @@ -161,8 +175,13 @@ public class CustomerServiceImpl extends AbstractCachedEntityService>> findEntityAsync(TenantId tenantId, EntityId entityId) { + return FluentFuture.from(findCustomerByIdAsync(tenantId, new CustomerId(entityId.getId()))) + .transform(Optional::ofNullable, directExecutor()); + } + @Override public long countByTenantId(TenantId tenantId) { return customerDao.countByTenantId(tenantId); diff --git a/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java index 1b21310237..32e15f12fa 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.dao.dashboard; +import com.google.common.util.concurrent.FluentFuture; import com.google.common.util.concurrent.ListenableFuture; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -61,6 +62,7 @@ import java.util.List; import java.util.Map; import java.util.Optional; +import static com.google.common.util.concurrent.MoreExecutors.directExecutor; import static org.thingsboard.server.dao.service.Validator.validateId; @Service("DashboardDaoService") @@ -70,6 +72,7 @@ public class DashboardServiceImpl extends AbstractEntityService implements Dashb public static final String INCORRECT_DASHBOARD_ID = "Incorrect dashboardId "; public static final String INCORRECT_TENANT_ID = "Incorrect tenantId "; + @Autowired private DashboardDao dashboardDao; @@ -157,6 +160,10 @@ public class DashboardServiceImpl extends AbstractEntityService implements Dashb @Override public Dashboard saveDashboard(Dashboard dashboard, boolean doValidate) { + return saveEntity(dashboard, () -> doSaveDashboard(dashboard, doValidate)); + } + + private Dashboard doSaveDashboard(Dashboard dashboard, boolean doValidate) { log.trace("Executing saveDashboard [{}]", dashboard); if (doValidate) { dashboardValidator.validate(dashboard, DashboardInfo::getTenantId); @@ -424,6 +431,12 @@ public class DashboardServiceImpl extends AbstractEntityService implements Dashb return Optional.ofNullable(findDashboardById(tenantId, new DashboardId(entityId.getId()))); } + @Override + public FluentFuture>> findEntityAsync(TenantId tenantId, EntityId entityId) { + return FluentFuture.from(findDashboardByIdAsync(tenantId, new DashboardId(entityId.getId()))) + .transform(Optional::ofNullable, directExecutor()); + } + @Override public long countByTenantId(TenantId tenantId) { return dashboardDao.countByTenantId(tenantId); diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsEvictEvent.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsEvictEvent.java index ca265e78b1..33b410bcfe 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsEvictEvent.java +++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsEvictEvent.java @@ -18,9 +18,9 @@ package org.thingsboard.server.dao.device; import lombok.Data; @Data -class DeviceCredentialsEvictEvent { +public class DeviceCredentialsEvictEvent { - private final String newCedentialsId; + private final String newCredentialsId; private final String oldCredentialsId; } diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsServiceImpl.java index 12859129a2..3423a74543 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsServiceImpl.java @@ -47,6 +47,8 @@ import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.exception.DeviceCredentialsValidationException; import org.thingsboard.server.dao.service.validator.DeviceCredentialsDataValidator; +import java.util.Objects; + import static org.thingsboard.server.dao.service.Validator.validateId; import static org.thingsboard.server.dao.service.Validator.validateString; @@ -61,8 +63,8 @@ public class DeviceCredentialsServiceImpl extends AbstractCachedEntityService JacksonUtil.valueToTree(deviceCredentials.getCredentialsId()); + case X509_CERTIFICATE -> JacksonUtil.valueToTree(deviceCredentials.getCredentialsValue()); + default -> JacksonUtil.fromString(deviceCredentials.getCredentialsValue(), JsonNode.class); + }; } private void formatSimpleMqttCredentials(DeviceCredentials deviceCredentials) { @@ -407,4 +407,11 @@ public class DeviceCredentialsServiceImpl extends AbstractCachedEntityService>> findEntityAsync(TenantId tenantId, EntityId entityId) { + return FluentFuture.from(deviceProfileDao.findByIdAsync(tenantId, entityId.getId())) + .transform(Optional::ofNullable, directExecutor()); + } + @Override public EntityType getEntityType() { return EntityType.DEVICE_PROFILE; @@ -432,8 +440,7 @@ public class DeviceProfileServiceImpl extends CachedVersionedEntityService 1) { return EncryptionUtil.certTrimNewLinesForChainInDeviceProfile(certificateValue); } - } catch (CertificateException ignored) { - } + } catch (CertificateException ignored) {} return EncryptionUtil.certTrimNewLines(certificateValue); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java index 6d993f3e3d..8474f4f326 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java @@ -16,6 +16,7 @@ package org.thingsboard.server.dao.device; import com.fasterxml.jackson.databind.node.ObjectNode; +import com.google.common.util.concurrent.FluentFuture; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.MoreExecutors; @@ -39,6 +40,8 @@ import org.thingsboard.server.common.data.DeviceTransportType; import org.thingsboard.server.common.data.EntitySubtype; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityView; +import org.thingsboard.server.common.data.NameConflictPolicy; +import org.thingsboard.server.common.data.NameConflictStrategy; import org.thingsboard.server.common.data.ProfileEntityIdInfo; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.Tenant; @@ -89,6 +92,7 @@ import java.util.List; import java.util.Optional; import java.util.UUID; +import static com.google.common.util.concurrent.MoreExecutors.directExecutor; import static org.thingsboard.server.dao.DaoUtil.toUUIDs; import static org.thingsboard.server.dao.service.Validator.validateId; import static org.thingsboard.server.dao.service.Validator.validateIds; @@ -126,23 +130,21 @@ public class DeviceServiceImpl extends CachedVersionedEntityService INCORRECT_DEVICE_ID + id); - if (TenantId.SYS_TENANT_ID.equals(tenantId)) { - return cache.get(new DeviceCacheKey(deviceId), - () -> deviceDao.findById(tenantId, deviceId.getId())); - } else { - return cache.get(new DeviceCacheKey(tenantId, deviceId), - () -> deviceDao.findDeviceByTenantIdAndId(tenantId, deviceId.getId())); - } + return findDeviceByIdInternal(tenantId, deviceId); } @Override public ListenableFuture findDeviceByIdAsync(TenantId tenantId, DeviceId deviceId) { log.trace("Executing findDeviceByIdAsync [{}]", deviceId); validateId(deviceId, id -> INCORRECT_DEVICE_ID + id); + return executor.submit(() -> findDeviceByIdInternal(tenantId, deviceId)); + } + + private Device findDeviceByIdInternal(TenantId tenantId, DeviceId deviceId) { if (TenantId.SYS_TENANT_ID.equals(tenantId)) { - return deviceDao.findByIdAsync(tenantId, deviceId.getId()); + return cache.get(new DeviceCacheKey(deviceId), () -> deviceDao.findById(tenantId, deviceId.getId())); } else { - return deviceDao.findDeviceByTenantIdAndIdAsync(tenantId, deviceId.getId()); + return cache.get(new DeviceCacheKey(tenantId, deviceId), () -> deviceDao.findDeviceByTenantIdAndId(tenantId, deviceId.getId())); } } @@ -167,6 +169,12 @@ public class DeviceServiceImpl extends CachedVersionedEntityService doSaveDeviceWithoutCredentials(device, doValidate, nameConflictStrategy)); + } + + private Device doSaveDeviceWithoutCredentials(Device device, boolean doValidate, NameConflictStrategy nameConflictStrategy) { log.trace("Executing saveDevice [{}]", device); - Device oldDevice = null; + Device oldDevice = (device.getId() != null) ? deviceDao.findById(device.getTenantId(), device.getId().getId()) : null; + if (nameConflictStrategy.policy() == NameConflictPolicy.UNIQUIFY && (oldDevice == null || !oldDevice.getName().equals(device.getName()))) { + uniquifyEntityName(device, oldDevice, device::setName, EntityType.DEVICE, nameConflictStrategy); + } if (doValidate) { - oldDevice = deviceValidator.validate(device, Device::getTenantId); - } else if (device.getId() != null) { - oldDevice = findDeviceById(device.getTenantId(), device.getId()); + deviceValidator.validate(device, Device::getTenantId); } DeviceCacheEvictEvent deviceCacheEvictEvent = new DeviceCacheEvictEvent(device.getTenantId(), device.getId(), device.getName(), oldDevice != null ? oldDevice.getName() : null); try { @@ -256,8 +279,8 @@ public class DeviceServiceImpl extends CachedVersionedEntityService toEvict = new ArrayList<>(3); toEvict.add(new DeviceCacheKey(event.getTenantId(), event.getNewName())); @@ -729,6 +752,12 @@ public class DeviceServiceImpl extends CachedVersionedEntityService>> findEntityAsync(TenantId tenantId, EntityId entityId) { + return FluentFuture.from(findDeviceByIdAsync(tenantId, new DeviceId(entityId.getId()))) + .transform(Optional::ofNullable, directExecutor()); + } + @Override public EntityType getEntityType() { return EntityType.DEVICE; diff --git a/dao/src/main/java/org/thingsboard/server/dao/domain/DomainServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/domain/DomainServiceImpl.java index c12d4d915e..e4e569c00a 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/domain/DomainServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/domain/DomainServiceImpl.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.dao.domain; +import com.google.common.util.concurrent.FluentFuture; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @@ -39,17 +40,16 @@ import org.thingsboard.server.dao.service.validator.DomainDataValidator; import java.util.Comparator; import java.util.List; -import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; +import static com.google.common.util.concurrent.MoreExecutors.directExecutor; + @Slf4j @Service public class DomainServiceImpl extends AbstractEntityService implements DomainService { - public static final String INCORRECT_TENANT_ID = "Incorrect tenantId "; - @Autowired private OAuth2ClientDao oauth2ClientDao; @Autowired @@ -66,8 +66,7 @@ public class DomainServiceImpl extends AbstractEntityService implements DomainSe eventPublisher.publishEvent(SaveEntityEvent.builder().tenantId(tenantId).entityId(savedDomain.getId()).entity(savedDomain).build()); return savedDomain; } catch (Exception e) { - checkConstraintViolation(e, - Map.of("domain_name_key", "Domain with such name and scheme already exists!")); + checkConstraintViolation(e, "domain_name_key", "Domain with such name and scheme already exists!"); throw e; } } @@ -142,6 +141,12 @@ public class DomainServiceImpl extends AbstractEntityService implements DomainSe return Optional.ofNullable(findDomainById(tenantId, new DomainId(entityId.getId()))); } + @Override + public FluentFuture>> findEntityAsync(TenantId tenantId, EntityId entityId) { + return FluentFuture.from(domainDao.findByIdAsync(tenantId, entityId.getId())) + .transform(Optional::ofNullable, directExecutor()); + } + @Override @Transactional public void deleteEntity(TenantId tenantId, EntityId id, boolean force) { @@ -163,4 +168,5 @@ public class DomainServiceImpl extends AbstractEntityService implements DomainSe public EntityType getEntityType() { return EntityType.DOMAIN; } + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeServiceImpl.java index 0655d05572..708876c393 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeServiceImpl.java @@ -17,6 +17,7 @@ package org.thingsboard.server.dao.edge; import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ObjectNode; +import com.google.common.util.concurrent.FluentFuture; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.MoreExecutors; @@ -83,6 +84,7 @@ import java.util.Optional; import java.util.UUID; import java.util.stream.Collectors; +import static com.google.common.util.concurrent.MoreExecutors.directExecutor; import static org.thingsboard.server.dao.DaoUtil.toUUIDs; import static org.thingsboard.server.dao.edge.BaseRelatedEdgesService.RELATED_EDGES_CACHE_ITEMS; import static org.thingsboard.server.dao.service.Validator.validateId; @@ -137,8 +139,8 @@ public class EdgeServiceImpl extends AbstractCachedEntityService keys = new ArrayList<>(2); keys.add(new EdgeCacheKey(event.getTenantId(), event.getNewName())); @@ -201,6 +203,10 @@ public class EdgeServiceImpl extends AbstractCachedEntityService doSaveEdge(edge)); + } + + private Edge doSaveEdge(Edge edge) { log.trace("Executing saveEdge [{}]", edge); Edge oldEdge = edgeValidator.validate(edge, Edge::getTenantId); EdgeCacheEvictEvent evictEvent = new EdgeCacheEvictEvent(edge.getTenantId(), edge.getName(), oldEdge != null ? oldEdge.getName() : null); @@ -629,6 +635,12 @@ public class EdgeServiceImpl extends AbstractCachedEntityService>> findEntityAsync(TenantId tenantId, EntityId entityId) { + return FluentFuture.from(findEdgeByIdAsync(tenantId, new EdgeId(entityId.getId()))) + .transform(Optional::ofNullable, directExecutor()); + } + @Override public EntityType getEntityType() { return EntityType.EDGE; diff --git a/dao/src/main/java/org/thingsboard/server/dao/entity/AbstractEntityService.java b/dao/src/main/java/org/thingsboard/server/dao/entity/AbstractEntityService.java index 7560c7fb76..5e0a0df5ad 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/entity/AbstractEntityService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/entity/AbstractEntityService.java @@ -21,16 +21,25 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.annotation.Lazy; +import org.springframework.util.ConcurrentReferenceHashMap; import org.thingsboard.common.util.DebugModeUtil; +import org.thingsboard.server.common.data.EntityInfo; +import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityView; import org.thingsboard.server.common.data.HasDebugSettings; +import org.thingsboard.server.common.data.HasTenantId; +import org.thingsboard.server.common.data.HasName; +import org.thingsboard.server.common.data.HasTenantId; +import org.thingsboard.server.common.data.NameConflictStrategy; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.debug.DebugSettings; import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.HasId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.RelationTypeGroup; +import org.thingsboard.server.dao.Dao; import org.thingsboard.server.dao.alarm.AlarmService; import org.thingsboard.server.dao.cf.CalculatedFieldService; import org.thingsboard.server.dao.edge.EdgeService; @@ -44,7 +53,15 @@ import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.concurrent.ConcurrentMap; +import java.util.Set; import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.ReentrantLock; +import java.util.function.Supplier; +import java.util.function.Consumer; +import java.util.stream.Collectors; + +import static org.thingsboard.server.common.data.UniquifyStrategy.RANDOM; @Slf4j public abstract class AbstractEntityService { @@ -52,6 +69,8 @@ public abstract class AbstractEntityService { public static final String INCORRECT_EDGE_ID = "Incorrect edgeId "; public static final String INCORRECT_PAGE_LINK = "Incorrect page link "; + private final ConcurrentMap entityCreationLocks = new ConcurrentReferenceHashMap<>(16); + @Autowired protected ApplicationEventPublisher eventPublisher; @@ -81,11 +100,28 @@ public abstract class AbstractEntityService { @Autowired @Lazy - private TbTenantProfileCache tbTenantProfileCache; + protected TbTenantProfileCache tbTenantProfileCache; + + @Autowired + protected EntityDaoRegistry entityDaoRegistry; @Value("${debug.settings.default_duration:15}") private int defaultDebugDurationMinutes; + protected E saveEntity(E entity, Supplier saveFunction) { + if (entity.getId() == null) { + ReentrantLock lock = entityCreationLocks.computeIfAbsent(entity.getTenantId(), id -> new ReentrantLock()); + lock.lock(); + try { + return saveFunction.get(); + } finally { + lock.unlock(); + } + } else { + return saveFunction.get(); + } + } + protected void createRelation(TenantId tenantId, EntityRelation relation) { log.debug("Creating relation: {}", relation); relationService.saveRelation(tenantId, relation); @@ -155,4 +191,33 @@ public abstract class AbstractEntityService { private long getMaxDebugAllUntil(TenantId tenantId, long now) { return now + TimeUnit.MINUTES.toMillis(DebugModeUtil.getMaxDebugAllDuration(tbTenantProfileCache.get(tenantId).getDefaultProfileConfiguration().getMaxDebugModeDurationMinutes(), defaultDebugDurationMinutes)); } + + protected & HasTenantId & HasName> void uniquifyEntityName(E entity, E oldEntity, Consumer setName, EntityType entityType, NameConflictStrategy strategy) { + Dao dao = entityDaoRegistry.getDao(entityType); + List existingEntities = dao.findEntityInfosByNamePrefix(entity.getTenantId(), entity.getName()); + Set existingNames = existingEntities.stream() + .filter(e -> (oldEntity == null || !e.getId().equals(oldEntity.getId()))) + .map(EntityInfo::getName) + .collect(Collectors.toSet()); + + if (existingNames.contains(entity.getName())) { + String uniqueName = generateUniqueName(entity.getName(), existingNames, strategy); + setName.accept(uniqueName); + } + } + + private String generateUniqueName(String baseName, Set existingNames, NameConflictStrategy strategy) { + String newName; + int index = 1; + String separator = strategy.separator(); + boolean isRandom = strategy.uniquifyStrategy() == RANDOM; + + do { + String suffix = isRandom ? StringUtils.randomAlphanumeric(6) : String.valueOf(index++); + newName = baseName + separator + suffix; + } while (existingNames.contains(newName)); + + return newName; + } + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/entity/BaseEntityService.java b/dao/src/main/java/org/thingsboard/server/dao/entity/BaseEntityService.java index 2fca546fc9..b512f353f5 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/entity/BaseEntityService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/entity/BaseEntityService.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.dao.entity; +import com.google.common.util.concurrent.FluentFuture; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Lazy; @@ -67,15 +68,13 @@ import java.util.concurrent.ExecutionException; import java.util.function.Function; import java.util.stream.Collectors; +import static com.google.common.util.concurrent.MoreExecutors.directExecutor; import static org.thingsboard.server.common.data.id.EntityId.NULL_UUID; import static org.thingsboard.server.common.data.query.EntityFilterType.ENTITY_NAME; import static org.thingsboard.server.common.data.query.EntityFilterType.ENTITY_TYPE; import static org.thingsboard.server.dao.service.Validator.validateEntityDataPageLink; import static org.thingsboard.server.dao.service.Validator.validateId; -/** - * Created by ashvayka on 04.05.17. - */ @Service @Slf4j public class BaseEntityService extends AbstractEntityService implements EntityService { @@ -93,7 +92,7 @@ public class BaseEntityService extends AbstractEntityService implements EntitySe @Autowired @Lazy - EntityServiceRegistry entityServiceRegistry; + private EntityServiceRegistry entityServiceRegistry; @Autowired private EdqsService edqsService; @@ -198,6 +197,11 @@ public class BaseEntityService extends AbstractEntityService implements EntitySe return fetchAndConvert(tenantId, entityId, this::getCustomerId); } + @Override + public FluentFuture> fetchEntityCustomerIdAsync(TenantId tenantId, EntityId entityId) { + return fetchAndConvertAsync(tenantId, entityId, this::getCustomerId); + } + @Override public Optional fetchNameLabelAndCustomerDetails(TenantId tenantId, EntityId entityId) { log.trace("Executing fetchNameLabelAndCustomerDetails [{}]", entityId); @@ -239,6 +243,12 @@ public class BaseEntityService extends AbstractEntityService implements EntitySe return entityOpt.map(converter); } + private FluentFuture> fetchAndConvertAsync(TenantId tenantId, EntityId entityId, Function, T> converter) { + EntityDaoService entityDaoService = entityServiceRegistry.getServiceByEntityType(entityId.getEntityType()); + return entityDaoService.findEntityAsync(tenantId, entityId) + .transform(entityOpt -> entityOpt.map(converter), directExecutor()); + } + private String getName(HasId entity) { return entity instanceof HasName ? ((HasName) entity).getName() : null; } @@ -329,7 +339,7 @@ public class BaseEntityService extends AbstractEntityService implements EntitySe } if ((query.getEntityFields() == null || query.getEntityFields().isEmpty()) && - (query.getLatestValues() == null || query.getLatestValues().isEmpty())) { + (query.getLatestValues() == null || query.getLatestValues().isEmpty())) { return false; } diff --git a/dao/src/main/java/org/thingsboard/server/dao/entity/DefaultEntityServiceRegistry.java b/dao/src/main/java/org/thingsboard/server/dao/entity/DefaultEntityServiceRegistry.java index 9c50ad621f..a795bddffa 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/entity/DefaultEntityServiceRegistry.java +++ b/dao/src/main/java/org/thingsboard/server/dao/entity/DefaultEntityServiceRegistry.java @@ -43,9 +43,6 @@ public class DefaultEntityServiceRegistry implements EntityServiceRegistry { if (EntityType.RULE_CHAIN.equals(entityType)) { entityDaoServicesMap.put(EntityType.RULE_NODE, entityDaoService); } - if (EntityType.CALCULATED_FIELD.equals(entityType)) { - entityDaoServicesMap.put(EntityType.CALCULATED_FIELD_LINK, entityDaoService); - } }); log.debug("Initialized EntityServiceRegistry total [{}] entries", entityDaoServicesMap.size()); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewServiceImpl.java index 0e742e20db..617782cb97 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewServiceImpl.java @@ -16,6 +16,7 @@ package org.thingsboard.server.dao.entityview; import com.google.common.base.Function; +import com.google.common.util.concurrent.FluentFuture; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.MoreExecutors; @@ -29,6 +30,8 @@ import org.thingsboard.server.common.data.EntitySubtype; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityView; import org.thingsboard.server.common.data.EntityViewInfo; +import org.thingsboard.server.common.data.NameConflictPolicy; +import org.thingsboard.server.common.data.NameConflictStrategy; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.edge.Edge; @@ -60,13 +63,11 @@ import java.util.List; import java.util.Optional; import java.util.stream.Collectors; +import static com.google.common.util.concurrent.MoreExecutors.directExecutor; import static org.thingsboard.server.dao.service.Validator.validateId; import static org.thingsboard.server.dao.service.Validator.validatePageLink; import static org.thingsboard.server.dao.service.Validator.validateString; -/** - * Created by Victor Basanets on 8/28/2017. - */ @Service("EntityViewDaoService") @Slf4j public class EntityViewServiceImpl extends CachedVersionedEntityService implements EntityViewService { @@ -110,14 +111,24 @@ public class EntityViewServiceImpl extends CachedVersionedEntityService INCORRECT_ENTITY_VIEW_ID + id); + return findEntityViewByIdInternal(tenantId, entityViewId, putInCache); + } + + @Override + public ListenableFuture findEntityViewByIdAsync(TenantId tenantId, EntityViewId entityViewId) { + log.trace("Executing findEntityViewByIdAsync [{}]", entityViewId); + validateId(entityViewId, id -> INCORRECT_ENTITY_VIEW_ID + id); + return service.submit(() -> findEntityViewByIdInternal(tenantId, entityViewId, true)); + } + + private EntityView findEntityViewByIdInternal(TenantId tenantId, EntityViewId entityViewId, boolean putInCache) { EntityViewCacheValue value = cache.get(EntityViewCacheKey.byId(entityViewId), () -> { EntityView entityView = entityViewDao.findById(tenantId, entityViewId.getId()); return new EntityViewCacheValue(entityView, null); @@ -190,7 +212,6 @@ public class EntityViewServiceImpl extends CachedVersionedEntityService entityViewDao.findEntityViewByTenantIdAndName(tenantId.getId(), name).orElse(null) , EntityViewCacheValue::getEntityView, v -> new EntityViewCacheValue(v, null), true); - } @Override @@ -307,13 +328,6 @@ public class EntityViewServiceImpl extends CachedVersionedEntityService findEntityViewByIdAsync(TenantId tenantId, EntityViewId entityViewId) { - log.trace("Executing findEntityViewByIdAsync [{}]", entityViewId); - validateId(entityViewId, id -> INCORRECT_ENTITY_VIEW_ID + id); - return entityViewDao.findByIdAsync(tenantId, entityViewId.getId()); - } - @Override public ListenableFuture> findEntityViewsByTenantIdAndEntityIdAsync(TenantId tenantId, EntityId entityId) { log.trace("Executing findEntityViewsByTenantIdAndEntityIdAsync, tenantId [{}], entityId [{}]", tenantId, entityId); @@ -479,6 +493,12 @@ public class EntityViewServiceImpl extends CachedVersionedEntityService>> findEntityAsync(TenantId tenantId, EntityId entityId) { + return FluentFuture.from(findEntityViewByIdAsync(tenantId, new EntityViewId(entityId.getId()))) + .transform(Optional::ofNullable, directExecutor()); + } + @Override public EntityType getEntityType() { return EntityType.ENTITY_VIEW; diff --git a/dao/src/main/java/org/thingsboard/server/dao/job/DefaultJobService.java b/dao/src/main/java/org/thingsboard/server/dao/job/DefaultJobService.java index 360aa0063b..06f219ca3d 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/job/DefaultJobService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/job/DefaultJobService.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.dao.job; +import com.google.common.util.concurrent.FluentFuture; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; @@ -44,6 +45,7 @@ import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; +import static com.google.common.util.concurrent.MoreExecutors.directExecutor; import static org.thingsboard.server.common.data.job.JobStatus.CANCELLED; import static org.thingsboard.server.common.data.job.JobStatus.COMPLETED; import static org.thingsboard.server.common.data.job.JobStatus.FAILED; @@ -87,11 +89,7 @@ public class DefaultJobService extends AbstractEntityService implements JobServi } job.getResult().setCancellationTs(System.currentTimeMillis()); JobStatus prevStatus = job.getStatus(); - if (job.getStatus() == QUEUED) { - job.setStatus(CANCELLED); // setting cancelled status right away, because we don't expect stats for cancelled tasks - } else if (job.getStatus() == PENDING) { - job.setStatus(RUNNING); - } + job.setStatus(CANCELLED); saveJob(tenantId, job, true, prevStatus); } @@ -145,7 +143,7 @@ public class DefaultJobService extends AbstractEntityService implements JobServi } } - if (job.getStatus() == RUNNING) { + if (job.getStatus().isOneOf(RUNNING, CANCELLED)) { if (result.getTotalCount() != null && result.getCompletedCount() >= result.getTotalCount()) { if (result.getCancellationTs() > 0) { job.setStatus(CANCELLED); @@ -193,7 +191,7 @@ public class DefaultJobService extends AbstractEntityService implements JobServi private void checkWaitingJobs(TenantId tenantId, JobType jobType) { Job queuedJob = jobDao.findOldestByTenantIdAndTypeAndStatusForUpdate(tenantId, jobType, QUEUED); - if (queuedJob == null) { + if (queuedJob == null || jobDao.existsByTenantIdAndTypeAndStatusOneOf(tenantId, jobType, PENDING, RUNNING)) { return; } queuedJob.setStatus(PENDING); @@ -245,6 +243,12 @@ public class DefaultJobService extends AbstractEntityService implements JobServi return Optional.ofNullable(findJobById(tenantId, (JobId) entityId)); } + @Override + public FluentFuture>> findEntityAsync(TenantId tenantId, EntityId entityId) { + return FluentFuture.from(jobDao.findByIdAsync(tenantId, entityId.getId())) + .transform(Optional::ofNullable, directExecutor()); + } + @Override public void deleteEntity(TenantId tenantId, EntityId id, boolean force) { jobDao.removeById(tenantId, id.getId()); diff --git a/dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppBundleServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppBundleServiceImpl.java index d20623ad66..98f4a612e7 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppBundleServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppBundleServiceImpl.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.dao.mobile; +import com.google.common.util.concurrent.FluentFuture; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @@ -40,11 +41,11 @@ import org.thingsboard.server.dao.service.DataValidator; import java.util.Comparator; import java.util.List; -import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; +import static com.google.common.util.concurrent.MoreExecutors.directExecutor; import static org.thingsboard.server.dao.service.Validator.checkNotNull; @Slf4j @@ -60,7 +61,6 @@ public class MobileAppBundleServiceImpl extends AbstractEntityService implements @Autowired private DataValidator mobileAppBundleDataValidator; - @Override public MobileAppBundle saveMobileAppBundle(TenantId tenantId, MobileAppBundle mobileAppBundle) { log.trace("Executing saveMobileAppBundle [{}]", mobileAppBundle); @@ -71,8 +71,8 @@ public class MobileAppBundleServiceImpl extends AbstractEntityService implements return savedMobileApp; } catch (Exception e) { checkConstraintViolation(e, - Map.of("mobile_app_bundle_android_app_id_key", "Android mobile app is already configured in another bundle!", - "mobile_app_bundle_ios_app_id_key", "IOS mobile app is already configured in another bundle!")); + "mobile_app_bundle_android_app_id_key", "Android mobile app is already configured in another bundle!", + "mobile_app_bundle_ios_app_id_key", "IOS mobile app is already configured in another bundle!"); throw e; } } @@ -149,6 +149,12 @@ public class MobileAppBundleServiceImpl extends AbstractEntityService implements return Optional.ofNullable(findMobileAppBundleById(tenantId, new MobileAppBundleId(entityId.getId()))); } + @Override + public FluentFuture>> findEntityAsync(TenantId tenantId, EntityId entityId) { + return FluentFuture.from(mobileAppBundleDao.findByIdAsync(tenantId, entityId.getId())) + .transform(Optional::ofNullable, directExecutor()); + } + @Override @Transactional public void deleteEntity(TenantId tenantId, EntityId id, boolean force) { @@ -167,4 +173,5 @@ public class MobileAppBundleServiceImpl extends AbstractEntityService implements .collect(Collectors.toList()); mobileAppBundleInfo.setOauth2ClientInfos(clients); } + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppServiceImpl.java index f4e803b28e..b7785496d4 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppServiceImpl.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.dao.mobile; +import com.google.common.util.concurrent.FluentFuture; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @@ -35,9 +36,10 @@ import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent; import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.dao.service.Validator; -import java.util.Map; import java.util.Optional; +import static com.google.common.util.concurrent.MoreExecutors.directExecutor; + @Slf4j @Service public class MobileAppServiceImpl extends AbstractEntityService implements MobileAppService { @@ -58,8 +60,7 @@ public class MobileAppServiceImpl extends AbstractEntityService implements Mobil eventPublisher.publishEvent(SaveEntityEvent.builder().tenantId(tenantId).entity(savedMobileApp).build()); return savedMobileApp; } catch (Exception e) { - checkConstraintViolation(e, - Map.of("mobile_app_pkg_name_platform_unq_key", "Mobile app with such package name and platform already exists!")); + checkConstraintViolation(e, "mobile_app_pkg_name_platform_unq_key", "Mobile app with such package name and platform already exists!"); throw e; } } @@ -88,6 +89,12 @@ public class MobileAppServiceImpl extends AbstractEntityService implements Mobil return Optional.ofNullable(findMobileAppById(tenantId, new MobileAppId(entityId.getId()))); } + @Override + public FluentFuture>> findEntityAsync(TenantId tenantId, EntityId entityId) { + return FluentFuture.from(mobileAppDao.findByIdAsync(tenantId, entityId.getId())) + .transform(Optional::ofNullable, directExecutor()); + } + @Override @Transactional public void deleteEntity(TenantId tenantId, EntityId id, boolean force) { @@ -117,4 +124,5 @@ public class MobileAppServiceImpl extends AbstractEntityService implements Mobil public EntityType getEntityType() { return EntityType.MOBILE_APP; } + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java index 3960c67525..aee8356af4 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java @@ -26,8 +26,7 @@ import java.util.UUID; public class ModelConstants { - private ModelConstants() { - } + private ModelConstants() {} public static final UUID NULL_UUID = Uuids.startOf(0); public static final TenantId SYSTEM_TENANT = TenantId.fromUUID(ModelConstants.NULL_UUID); @@ -731,15 +730,6 @@ public class ModelConstants { public static final String CALCULATED_FIELD_CONFIGURATION = "configuration"; public static final String CALCULATED_FIELD_VERSION = "version"; - /** - * Calculated field links constants. - */ - public static final String CALCULATED_FIELD_LINK_TABLE_NAME = "calculated_field_link"; - public static final String CALCULATED_FIELD_LINK_TENANT_ID_COLUMN = TENANT_ID_COLUMN; - public static final String CALCULATED_FIELD_LINK_ENTITY_TYPE = ENTITY_TYPE_COLUMN; - public static final String CALCULATED_FIELD_LINK_ENTITY_ID = ENTITY_ID_COLUMN; - public static final String CALCULATED_FIELD_LINK_CALCULATED_FIELD_ID = "calculated_field_id"; - /** * Tasks constants. */ diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/CalculatedFieldLinkEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/CalculatedFieldLinkEntity.java deleted file mode 100644 index 0f2a6455ec..0000000000 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/CalculatedFieldLinkEntity.java +++ /dev/null @@ -1,79 +0,0 @@ -/** - * Copyright © 2016-2025 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.dao.model.sql; - -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.Table; -import lombok.Data; -import lombok.EqualsAndHashCode; -import org.thingsboard.server.common.data.cf.CalculatedFieldLink; -import org.thingsboard.server.common.data.id.CalculatedFieldId; -import org.thingsboard.server.common.data.id.CalculatedFieldLinkId; -import org.thingsboard.server.common.data.id.EntityIdFactory; -import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.dao.model.BaseEntity; -import org.thingsboard.server.dao.model.BaseSqlEntity; - -import java.util.UUID; - -import static org.thingsboard.server.dao.model.ModelConstants.CALCULATED_FIELD_LINK_CALCULATED_FIELD_ID; -import static org.thingsboard.server.dao.model.ModelConstants.CALCULATED_FIELD_LINK_ENTITY_ID; -import static org.thingsboard.server.dao.model.ModelConstants.CALCULATED_FIELD_LINK_ENTITY_TYPE; -import static org.thingsboard.server.dao.model.ModelConstants.CALCULATED_FIELD_LINK_TABLE_NAME; -import static org.thingsboard.server.dao.model.ModelConstants.CALCULATED_FIELD_LINK_TENANT_ID_COLUMN; - -@Data -@EqualsAndHashCode(callSuper = true) -@Entity -@Table(name = CALCULATED_FIELD_LINK_TABLE_NAME) -public class CalculatedFieldLinkEntity extends BaseSqlEntity implements BaseEntity { - - @Column(name = CALCULATED_FIELD_LINK_TENANT_ID_COLUMN) - private UUID tenantId; - - @Column(name = CALCULATED_FIELD_LINK_ENTITY_TYPE) - private String entityType; - - @Column(name = CALCULATED_FIELD_LINK_ENTITY_ID) - private UUID entityId; - - @Column(name = CALCULATED_FIELD_LINK_CALCULATED_FIELD_ID) - private UUID calculatedFieldId; - - public CalculatedFieldLinkEntity() { - super(); - } - - public CalculatedFieldLinkEntity(CalculatedFieldLink calculatedFieldLink) { - super(calculatedFieldLink); - this.tenantId = calculatedFieldLink.getTenantId().getId(); - this.entityType = calculatedFieldLink.getEntityId().getEntityType().name(); - this.entityId = calculatedFieldLink.getEntityId().getId(); - this.calculatedFieldId = calculatedFieldLink.getCalculatedFieldId().getId(); - } - - @Override - public CalculatedFieldLink toData() { - CalculatedFieldLink calculatedFieldLink = new CalculatedFieldLink(new CalculatedFieldLinkId(id)); - calculatedFieldLink.setCreatedTime(createdTime); - calculatedFieldLink.setTenantId(TenantId.fromUUID(tenantId)); - calculatedFieldLink.setEntityId(EntityIdFactory.getByTypeAndUuid(entityType, entityId)); - calculatedFieldLink.setCalculatedFieldId(new CalculatedFieldId(calculatedFieldId)); - return calculatedFieldLink; - } - -} diff --git a/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationRequestService.java b/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationRequestService.java index ffed043fe2..35c7839429 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationRequestService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationRequestService.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.dao.notification; +import com.google.common.util.concurrent.FluentFuture; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.context.ApplicationEventPublisher; @@ -38,6 +39,8 @@ import org.thingsboard.server.dao.service.DataValidator; import java.util.List; import java.util.Optional; +import static com.google.common.util.concurrent.MoreExecutors.directExecutor; + @Service @Slf4j @RequiredArgsConstructor @@ -129,13 +132,17 @@ public class DefaultNotificationRequestService implements NotificationRequestSer return Optional.ofNullable(findNotificationRequestById(tenantId, new NotificationRequestId(entityId.getId()))); } + @Override + public FluentFuture>> findEntityAsync(TenantId tenantId, EntityId entityId) { + return FluentFuture.from(notificationRequestDao.findByIdAsync(tenantId, entityId.getId())) + .transform(Optional::ofNullable, directExecutor()); + } + @Override public EntityType getEntityType() { return EntityType.NOTIFICATION_REQUEST; } - private static class NotificationRequestValidator extends DataValidator { - - } + private static class NotificationRequestValidator extends DataValidator {} } diff --git a/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationRuleService.java b/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationRuleService.java index c349b84f0a..b2f79c9555 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationRuleService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationRuleService.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.dao.notification; +import com.google.common.util.concurrent.FluentFuture; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.EntityType; @@ -33,9 +34,10 @@ import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent; import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent; import java.util.List; -import java.util.Map; import java.util.Optional; +import static com.google.common.util.concurrent.MoreExecutors.directExecutor; + @Service @RequiredArgsConstructor public class DefaultNotificationRuleService extends AbstractEntityService implements NotificationRuleService, EntityDaoService { @@ -56,9 +58,7 @@ public class DefaultNotificationRuleService extends AbstractEntityService implem .created(notificationRule.getId() == null).build()); return savedRule; } catch (Exception e) { - checkConstraintViolation(e, Map.of( - "uq_notification_rule_name", "Notification rule with such name already exists" - )); + checkConstraintViolation(e, "uq_notification_rule_name", "Notification rule with such name already exists"); throw e; } } @@ -114,6 +114,12 @@ public class DefaultNotificationRuleService extends AbstractEntityService implem return Optional.ofNullable(findNotificationRuleById(tenantId, new NotificationRuleId(entityId.getId()))); } + @Override + public FluentFuture>> findEntityAsync(TenantId tenantId, EntityId entityId) { + return FluentFuture.from(notificationRuleDao.findByIdAsync(tenantId, entityId.getId())) + .transform(Optional::ofNullable, directExecutor()); + } + @Override public EntityType getEntityType() { return EntityType.NOTIFICATION_RULE; diff --git a/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationService.java b/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationService.java index 99efd18aee..8c5a051771 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationService.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.dao.notification; +import com.google.common.util.concurrent.FluentFuture; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; @@ -37,6 +38,8 @@ import org.thingsboard.server.dao.sql.query.EntityKeyMapping; import java.util.Optional; import java.util.Set; +import static com.google.common.util.concurrent.MoreExecutors.directExecutor; + @Service @Slf4j @RequiredArgsConstructor @@ -95,6 +98,12 @@ public class DefaultNotificationService implements NotificationService, EntityDa return Optional.ofNullable(findNotificationById(tenantId, new NotificationId(entityId.getId()))); } + @Override + public FluentFuture>> findEntityAsync(TenantId tenantId, EntityId entityId) { + return FluentFuture.from(notificationDao.findByIdAsync(tenantId, entityId.getId())) + .transform(Optional::ofNullable, directExecutor()); + } + @Override public EntityType getEntityType() { return EntityType.NOTIFICATION; diff --git a/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationTargetService.java b/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationTargetService.java index e873b18c76..637067d51e 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationTargetService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationTargetService.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.dao.notification; +import com.google.common.util.concurrent.FluentFuture; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; @@ -25,17 +26,13 @@ import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.HasId; import org.thingsboard.server.common.data.id.NotificationTargetId; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.id.TenantProfileId; import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.notification.NotificationRequestStatus; import org.thingsboard.server.common.data.notification.NotificationType; import org.thingsboard.server.common.data.notification.info.RuleOriginatedNotificationInfo; import org.thingsboard.server.common.data.notification.targets.NotificationTarget; import org.thingsboard.server.common.data.notification.targets.NotificationTargetConfig; -import org.thingsboard.server.common.data.notification.targets.platform.CustomerUsersFilter; import org.thingsboard.server.common.data.notification.targets.platform.PlatformUsersNotificationTargetConfig; -import org.thingsboard.server.common.data.notification.targets.platform.TenantAdministratorsFilter; -import org.thingsboard.server.common.data.notification.targets.platform.UserListFilter; import org.thingsboard.server.common.data.notification.targets.platform.UsersFilter; import org.thingsboard.server.common.data.notification.targets.platform.UsersFilterType; import org.thingsboard.server.common.data.page.PageData; @@ -47,12 +44,10 @@ import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent; import org.thingsboard.server.dao.user.UserService; import java.util.List; -import java.util.Map; import java.util.Objects; import java.util.Optional; -import java.util.stream.Collectors; -import static org.apache.commons.collections4.CollectionUtils.isNotEmpty; +import static com.google.common.util.concurrent.MoreExecutors.directExecutor; @Service @Slf4j @@ -72,9 +67,7 @@ public class DefaultNotificationTargetService extends AbstractEntityService impl .created(notificationTarget.getId() == null).build()); return savedTarget; } catch (Exception e) { - checkConstraintViolation(e, Map.of( - "uq_notification_target_name", "Recipients group with such name already exists" - )); + checkConstraintViolation(e, "uq_notification_target_name", "Recipients group with such name already exists"); throw e; } } @@ -115,49 +108,7 @@ public class DefaultNotificationTargetService extends AbstractEntityService impl @Override public PageData findRecipientsForNotificationTargetConfig(TenantId tenantId, PlatformUsersNotificationTargetConfig targetConfig, PageLink pageLink) { UsersFilter usersFilter = targetConfig.getUsersFilter(); - switch (usersFilter.getType()) { - case USER_LIST: { - List users = ((UserListFilter) usersFilter).getUsersIds().stream() - .limit(pageLink.getPageSize()) - .map(UserId::new).map(userId -> userService.findUserById(tenantId, userId)) - .filter(Objects::nonNull).collect(Collectors.toList()); - return new PageData<>(users, 1, users.size(), false); - } - case CUSTOMER_USERS: { - if (tenantId.equals(TenantId.SYS_TENANT_ID)) { - throw new IllegalArgumentException("Customer users target is not supported for system administrator"); - } - CustomerUsersFilter filter = (CustomerUsersFilter) usersFilter; - return userService.findCustomerUsers(tenantId, new CustomerId(filter.getCustomerId()), pageLink); - } - case TENANT_ADMINISTRATORS: { - TenantAdministratorsFilter filter = (TenantAdministratorsFilter) usersFilter; - if (!tenantId.equals(TenantId.SYS_TENANT_ID)) { - return userService.findTenantAdmins(tenantId, pageLink); - } else { - if (isNotEmpty(filter.getTenantsIds())) { - return userService.findTenantAdminsByTenantsIds(filter.getTenantsIds().stream() - .map(TenantId::fromUUID).collect(Collectors.toList()), pageLink); - } else if (isNotEmpty(filter.getTenantProfilesIds())) { - return userService.findTenantAdminsByTenantProfilesIds(filter.getTenantProfilesIds().stream() - .map(TenantProfileId::new).collect(Collectors.toList()), pageLink); - } else { - return userService.findAllTenantAdmins(pageLink); - } - } - } - case SYSTEM_ADMINISTRATORS: - return userService.findSysAdmins(pageLink); - case ALL_USERS: { - if (!tenantId.equals(TenantId.SYS_TENANT_ID)) { - return userService.findUsersByTenantId(tenantId, pageLink); - } else { - return userService.findAllUsers(pageLink); - } - } - default: - throw new IllegalArgumentException("Recipient type not supported"); - } + return userService.findUsersByFilter(tenantId, usersFilter, pageLink); } @Override @@ -229,6 +180,12 @@ public class DefaultNotificationTargetService extends AbstractEntityService impl return Optional.ofNullable(findNotificationTargetById(tenantId, new NotificationTargetId(entityId.getId()))); } + @Override + public FluentFuture>> findEntityAsync(TenantId tenantId, EntityId entityId) { + return FluentFuture.from(notificationTargetDao.findByIdAsync(tenantId, entityId.getId())) + .transform(Optional::ofNullable, directExecutor()); + } + @Override public EntityType getEntityType() { return EntityType.NOTIFICATION_TARGET; diff --git a/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationTemplateService.java b/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationTemplateService.java index 8e98714b85..4218e06881 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationTemplateService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationTemplateService.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.dao.notification; +import com.google.common.util.concurrent.FluentFuture; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.EntityType; @@ -37,6 +38,8 @@ import java.util.List; import java.util.Map; import java.util.Optional; +import static com.google.common.util.concurrent.MoreExecutors.directExecutor; + @Service @RequiredArgsConstructor public class DefaultNotificationTemplateService extends AbstractEntityService implements NotificationTemplateService, EntityDaoService { @@ -144,6 +147,12 @@ public class DefaultNotificationTemplateService extends AbstractEntityService im return Optional.ofNullable(findNotificationTemplateById(tenantId, new NotificationTemplateId(entityId.getId()))); } + @Override + public FluentFuture>> findEntityAsync(TenantId tenantId, EntityId entityId) { + return FluentFuture.from(notificationTemplateDao.findByIdAsync(tenantId, entityId.getId())) + .transform(Optional::ofNullable, directExecutor()); + } + @Override public EntityType getEntityType() { return EntityType.NOTIFICATION_TEMPLATE; diff --git a/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ClientServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ClientServiceImpl.java index 861633ec3b..a373d36580 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ClientServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ClientServiceImpl.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.dao.oauth2; +import com.google.common.util.concurrent.FluentFuture; import jakarta.transaction.Transactional; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; @@ -41,6 +42,7 @@ import java.util.List; import java.util.Optional; import java.util.stream.Collectors; +import static com.google.common.util.concurrent.MoreExecutors.directExecutor; @Slf4j @Service("OAuth2ClientService") @@ -107,7 +109,6 @@ public class OAuth2ClientServiceImpl extends AbstractEntityService implements OA .tenantId(tenantId) .entityId(oAuth2ClientId) .build()); - } @Override @@ -149,6 +150,12 @@ public class OAuth2ClientServiceImpl extends AbstractEntityService implements OA return Optional.ofNullable(findOAuth2ClientById(tenantId, new OAuth2ClientId(entityId.getId()))); } + @Override + public FluentFuture>> findEntityAsync(TenantId tenantId, EntityId entityId) { + return FluentFuture.from(oauth2ClientDao.findByIdAsync(tenantId, entityId.getId())) + .transform(Optional::ofNullable, directExecutor()); + } + @Override @Transactional public void deleteEntity(TenantId tenantId, EntityId id, boolean force) { diff --git a/dao/src/main/java/org/thingsboard/server/dao/ota/BaseOtaPackageService.java b/dao/src/main/java/org/thingsboard/server/dao/ota/BaseOtaPackageService.java index 343a2485ce..16894fe56c 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/ota/BaseOtaPackageService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/ota/BaseOtaPackageService.java @@ -17,6 +17,7 @@ package org.thingsboard.server.dao.ota; import com.google.common.hash.HashFunction; import com.google.common.hash.Hashing; +import com.google.common.util.concurrent.FluentFuture; import com.google.common.util.concurrent.ListenableFuture; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -47,6 +48,7 @@ import org.thingsboard.server.dao.service.PaginatedRemover; import java.nio.ByteBuffer; import java.util.Optional; +import static com.google.common.util.concurrent.MoreExecutors.directExecutor; import static org.thingsboard.server.dao.service.Validator.validateId; import static org.thingsboard.server.dao.service.Validator.validatePageLink; @@ -254,6 +256,12 @@ public class BaseOtaPackageService extends AbstractCachedEntityService>> findEntityAsync(TenantId tenantId, EntityId entityId) { + return FluentFuture.from(findOtaPackageInfoByIdAsync(tenantId, new OtaPackageId(entityId.getId()))) + .transform(Optional::ofNullable, directExecutor()); + } + @Override public EntityType getEntityType() { return EntityType.OTA_PACKAGE; diff --git a/dao/src/main/java/org/thingsboard/server/dao/queue/BaseQueueService.java b/dao/src/main/java/org/thingsboard/server/dao/queue/BaseQueueService.java index d36d5f2a15..248326a722 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/queue/BaseQueueService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/queue/BaseQueueService.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.dao.queue; +import com.google.common.util.concurrent.FluentFuture; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.hibernate.exception.ConstraintViolationException; @@ -42,6 +43,9 @@ import org.thingsboard.server.dao.tenant.TbTenantProfileCache; import java.util.List; import java.util.Optional; +import static com.google.common.util.concurrent.MoreExecutors.directExecutor; +import static org.thingsboard.server.dao.service.Validator.validateId; + @Service("QueueDaoService") @Slf4j @RequiredArgsConstructor @@ -127,7 +131,7 @@ public class BaseQueueService extends AbstractEntityService implements QueueServ @Override public void deleteQueuesByTenantId(TenantId tenantId) { - Validator.validateId(tenantId, "Incorrect tenant id for delete queues request."); + validateId(tenantId, __ -> "Incorrect tenant id for delete queues request."); tenantQueuesRemover.removeEntities(tenantId, tenantId); } @@ -141,24 +145,30 @@ public class BaseQueueService extends AbstractEntityService implements QueueServ return Optional.ofNullable(findQueueById(tenantId, new QueueId(entityId.getId()))); } + @Override + public FluentFuture>> findEntityAsync(TenantId tenantId, EntityId entityId) { + return FluentFuture.from(queueDao.findByIdAsync(tenantId, entityId.getId())) + .transform(Optional::ofNullable, directExecutor()); + } + @Override public EntityType getEntityType() { return EntityType.QUEUE; } - private PaginatedRemover tenantQueuesRemover = - new PaginatedRemover<>() { + private final PaginatedRemover tenantQueuesRemover = new PaginatedRemover<>() { - @Override - protected PageData findEntities(TenantId tenantId, TenantId id, PageLink pageLink) { - return queueDao.findQueuesByTenantId(id, pageLink); - } + @Override + protected PageData findEntities(TenantId tenantId, TenantId id, PageLink pageLink) { + return queueDao.findQueuesByTenantId(id, pageLink); + } - @Override - protected void removeEntity(TenantId tenantId, Queue entity) { - deleteQueue(tenantId, entity.getId()); - } - }; + @Override + protected void removeEntity(TenantId tenantId, Queue entity) { + deleteQueue(tenantId, entity.getId()); + } + + }; private TenantId getSystemOrIsolatedTenantId(TenantId tenantId) { if (!tenantId.equals(TenantId.SYS_TENANT_ID)) { @@ -167,7 +177,7 @@ public class BaseQueueService extends AbstractEntityService implements QueueServ return tenantId; } } - return TenantId.SYS_TENANT_ID; } + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/queue/BaseQueueStatsService.java b/dao/src/main/java/org/thingsboard/server/dao/queue/BaseQueueStatsService.java index 9e4c7136d5..4091daac20 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/queue/BaseQueueStatsService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/queue/BaseQueueStatsService.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.dao.queue; +import com.google.common.util.concurrent.FluentFuture; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; @@ -35,6 +36,7 @@ import org.thingsboard.server.dao.service.Validator; import java.util.List; import java.util.Optional; +import static com.google.common.util.concurrent.MoreExecutors.directExecutor; import static org.thingsboard.server.dao.service.Validator.validateId; import static org.thingsboard.server.dao.service.Validator.validateIds; @@ -106,6 +108,12 @@ public class BaseQueueStatsService extends AbstractEntityService implements Queu return Optional.ofNullable(findQueueStatsById(tenantId, new QueueStatsId(entityId.getId()))); } + @Override + public FluentFuture>> findEntityAsync(TenantId tenantId, EntityId entityId) { + return FluentFuture.from(queueStatsDao.findByIdAsync(tenantId, entityId.getId())) + .transform(Optional::ofNullable, directExecutor()); + } + @Override public EntityType getEntityType() { return EntityType.QUEUE_STATS; diff --git a/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java b/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java index c16fa4a6cd..90dd63f7da 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java @@ -31,26 +31,32 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.event.TransactionalEventListener; import org.springframework.transaction.support.TransactionSynchronizationManager; +import org.springframework.util.CollectionUtils; import org.thingsboard.common.util.ThingsBoardExecutors; import org.thingsboard.server.cache.TbTransactionalCache; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.UUIDBased; import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.EntityRelationInfo; +import org.thingsboard.server.common.data.relation.EntityRelationPathQuery; import org.thingsboard.server.common.data.relation.EntityRelationsQuery; import org.thingsboard.server.common.data.relation.EntitySearchDirection; import org.thingsboard.server.common.data.relation.RelationEntityTypeFilter; +import org.thingsboard.server.common.data.relation.RelationPathLevel; import org.thingsboard.server.common.data.relation.RelationTypeGroup; import org.thingsboard.server.common.data.relation.RelationsSearchParameters; import org.thingsboard.server.common.data.rule.RuleChainType; +import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; import org.thingsboard.server.dao.entity.EntityService; import org.thingsboard.server.dao.eventsourcing.RelationActionEvent; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.service.ConstraintValidator; import org.thingsboard.server.dao.sql.JpaExecutorService; import org.thingsboard.server.dao.sql.relation.JpaRelationQueryExecutorService; +import org.thingsboard.server.dao.usagerecord.ApiLimitService; import java.util.ArrayList; import java.util.Collections; @@ -64,9 +70,11 @@ import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import java.util.function.BiConsumer; +import java.util.function.Predicate; import static com.google.common.util.concurrent.MoreExecutors.directExecutor; import static org.thingsboard.server.dao.service.Validator.validateId; +import static org.thingsboard.server.dao.service.Validator.validatePositiveNumber; @Slf4j @Service @@ -78,6 +86,8 @@ class BaseRelationService implements RelationService { private final ApplicationEventPublisher eventPublisher; private final JpaExecutorService executor; private final JpaRelationQueryExecutorService relationsExecutor; + private final ApiLimitService apiLimitService; + private ScheduledExecutorService timeoutExecutorService; @Value("${sql.relations.query_timeout:20}") @@ -86,13 +96,14 @@ class BaseRelationService implements RelationService { public BaseRelationService(RelationDao relationDao, @Lazy EntityService entityService, TbTransactionalCache cache, ApplicationEventPublisher eventPublisher, JpaExecutorService executor, - JpaRelationQueryExecutorService relationsExecutor) { + JpaRelationQueryExecutorService relationsExecutor, ApiLimitService apiLimitService) { this.relationDao = relationDao; this.entityService = entityService; this.cache = cache; this.eventPublisher = eventPublisher; this.executor = executor; this.relationsExecutor = relationsExecutor; + this.apiLimitService = apiLimitService; } @PostConstruct @@ -484,6 +495,70 @@ class BaseRelationService implements RelationService { return relationDao.findRuleNodeToRuleChainRelations(ruleChainType, limit); } + @Override + public ListenableFuture> findByRelationPathQueryAsync(TenantId tenantId, EntityRelationPathQuery relationPathQuery) { + return findFilteredRelationsByPathQueryAsync(tenantId, relationPathQuery, null); + } + + @Override + public ListenableFuture> findFilteredRelationsByPathQueryAsync(TenantId tenantId, EntityRelationPathQuery relationPathQuery, Predicate relationFilter) { + log.trace("Executing findByRelationPathQuery, tenantId [{}], relationPathQuery {}", tenantId, relationPathQuery); + validateId(tenantId, id -> "Invalid tenant id: " + id); + validate(relationPathQuery); + int limit = (int) apiLimitService.getLimit(tenantId, DefaultTenantProfileConfiguration::getMaxRelatedEntitiesToReturnPerCfArgument); + validatePositiveNumber(limit, "Max related entities limit for relation path query must be positive!"); + if (relationPathQuery.levels().size() == 1) { + RelationPathLevel relationPathLevel = relationPathQuery.levels().get(0); + var relationsFuture = switch (relationPathLevel.direction()) { + case FROM -> findByFromAndTypeAsync(tenantId, relationPathQuery.rootEntityId(), relationPathLevel.relationType(), RelationTypeGroup.COMMON); + case TO -> findByToAndTypeAsync(tenantId, relationPathQuery.rootEntityId(), relationPathLevel.relationType(), RelationTypeGroup.COMMON); + }; + return Futures.transform(relationsFuture, entityRelations -> { + if (entityRelations == null || entityRelations.isEmpty()) { + return Collections.emptyList(); + } + List relations = relationFilter != null ? filterRelations(entityRelations, relationFilter) : entityRelations; + return relations.size() > limit ? relations.subList(0, limit) : relations; + }, directExecutor()); + } + return executor.submit(() -> { + List entityRelations = relationDao.findByRelationPathQuery(tenantId, relationPathQuery, limit); + return relationFilter != null ? filterRelations(entityRelations, relationFilter) : entityRelations; + }); + } + + private List filterRelations(List entityRelations, Predicate relationFilter) { + return entityRelations.stream() + .filter(relationFilter) + .toList(); + } + + @Override + public List findByRelationPathQuery(TenantId tenantId, EntityRelationPathQuery relationPathQuery) { + log.trace("Executing findByRelationPathQuery, tenantId [{}], relationPathQuery {}", tenantId, relationPathQuery); + validateId(tenantId, id -> "Invalid tenant id: " + id); + validate(relationPathQuery); + int limit = (int) apiLimitService.getLimit(tenantId, DefaultTenantProfileConfiguration::getMaxRelatedEntitiesToReturnPerCfArgument); + if (relationPathQuery.levels().size() == 1) { + RelationPathLevel relationPathLevel = relationPathQuery.levels().get(0); + var relations = switch (relationPathLevel.direction()) { + case FROM -> findByFromAndType(tenantId, relationPathQuery.rootEntityId(), relationPathLevel.relationType(), RelationTypeGroup.COMMON); + case TO -> findByToAndType(tenantId, relationPathQuery.rootEntityId(), relationPathLevel.relationType(), RelationTypeGroup.COMMON); + }; + return relations.size() > limit ? relations.subList(0, limit) : relations; + } + return relationDao.findByRelationPathQuery(tenantId, relationPathQuery, limit); + } + + private void validate(EntityRelationPathQuery relationPathQuery) { + validateId((UUIDBased) relationPathQuery.rootEntityId(), id -> "Invalid root entity id: " + id); + List levels = relationPathQuery.levels(); + if (CollectionUtils.isEmpty(levels)) { + throw new DataValidationException("Validation error: relation path levels should be specified!"); + } + levels.forEach(RelationPathLevel::validate); + } + private static void validate(EntityRelation relation) { if (relation == null) { throw new DataValidationException("Validation error: relation must not be null"); diff --git a/dao/src/main/java/org/thingsboard/server/dao/relation/RelationDao.java b/dao/src/main/java/org/thingsboard/server/dao/relation/RelationDao.java index c061faed5e..2ec23a0d74 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/relation/RelationDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/relation/RelationDao.java @@ -19,6 +19,7 @@ import com.google.common.util.concurrent.ListenableFuture; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.relation.EntityRelation; +import org.thingsboard.server.common.data.relation.EntityRelationPathQuery; import org.thingsboard.server.common.data.relation.RelationTypeGroup; import org.thingsboard.server.common.data.rule.RuleChainType; @@ -71,4 +72,6 @@ public interface RelationDao { List findRuleNodeToRuleChainRelations(RuleChainType ruleChainType, int limit); + List findByRelationPathQuery(TenantId tenantId, EntityRelationPathQuery relationPathQuery, int limit); + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/resource/BaseImageService.java b/dao/src/main/java/org/thingsboard/server/dao/resource/BaseImageService.java index 3be7f5b91c..25ef83d556 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/resource/BaseImageService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/resource/BaseImageService.java @@ -17,11 +17,11 @@ package org.thingsboard.server.dao.resource; import com.fasterxml.jackson.databind.JsonNode; import jakarta.annotation.PostConstruct; -import lombok.Data; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.RandomStringUtils; import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Strings; import org.apache.commons.lang3.exception.ExceptionUtils; import org.apache.commons.lang3.tuple.Pair; import org.springframework.stereotype.Service; @@ -50,6 +50,7 @@ import org.thingsboard.server.dao.ImageContainerDao; import org.thingsboard.server.dao.asset.AssetProfileDao; import org.thingsboard.server.dao.dashboard.DashboardInfoDao; import org.thingsboard.server.dao.device.DeviceProfileDao; +import org.thingsboard.server.dao.rule.RuleChainDao; import org.thingsboard.server.dao.service.Validator; import org.thingsboard.server.dao.service.validator.ResourceDataValidator; import org.thingsboard.server.dao.util.ImageUtils; @@ -109,8 +110,8 @@ public class BaseImageService extends BaseResourceService implements ImageServic public BaseImageService(TbResourceDao resourceDao, TbResourceInfoDao resourceInfoDao, ResourceDataValidator resourceValidator, AssetProfileDao assetProfileDao, DeviceProfileDao deviceProfileDao, WidgetsBundleDao widgetsBundleDao, - WidgetTypeDao widgetTypeDao, DashboardInfoDao dashboardInfoDao) { - super(resourceDao, resourceInfoDao, resourceValidator, widgetTypeDao, dashboardInfoDao); + WidgetTypeDao widgetTypeDao, DashboardInfoDao dashboardInfoDao, RuleChainDao ruleChainDao) { + super(resourceDao, resourceInfoDao, resourceValidator, widgetTypeDao, dashboardInfoDao, ruleChainDao); this.assetProfileDao = assetProfileDao; this.deviceProfileDao = deviceProfileDao; this.widgetsBundleDao = widgetsBundleDao; @@ -355,8 +356,8 @@ public class BaseImageService extends BaseResourceService implements ImageServic imageName = imageName + type + " image"; UpdateResult result = convertToImageUrl(entity.getTenantId(), imageName, entity.getImage(), Collections.emptyMap()); - entity.setImage(result.getValue()); - return result.isUpdated(); + entity.setImage(result.value()); + return result.updated(); } @Transactional(noRollbackFor = Exception.class) // we don't want transaction to rollback in case of an image processing failure @@ -372,8 +373,8 @@ public class BaseImageService extends BaseResourceService implements ImageServic Map imagesLinks = getResourcesLinks(widgetTypeDetails.getResources()); UpdateResult result = convertToImageUrl(tenantId, prefix + " image", widgetTypeDetails.getImage(), imagesLinks); - boolean updated = result.isUpdated(); - widgetTypeDetails.setImage(result.getValue()); + boolean updated = result.updated(); + widgetTypeDetails.setImage(result.value()); if (widgetTypeDetails.getDescriptor().isObject()) { JsonNode defaultConfig = widgetTypeDetails.getDefaultConfig(); @@ -396,8 +397,8 @@ public class BaseImageService extends BaseResourceService implements ImageServic Map imagesLinks = getResourcesLinks(dashboard.getResources()); var result = convertToImageUrl(tenantId, prefix + " image", dashboard.getImage(), imagesLinks); - boolean updated = result.isUpdated(); - dashboard.setImage(result.getValue()); + boolean updated = result.updated(); + dashboard.setImage(result.value()); updated |= convertToImageUrlsByMapping(tenantId, DASHBOARD_BASE64_MAPPING, Collections.singletonMap("prefix", prefix), dashboard.getConfiguration(), imagesLinks); updated |= convertToImageUrls(tenantId, prefix, dashboard.getConfiguration(), imagesLinks); @@ -408,10 +409,10 @@ public class BaseImageService extends BaseResourceService implements ImageServic AtomicBoolean updated = new AtomicBoolean(false); JacksonUtil.replaceAllByMapping(configuration, mapping, templateParams, (name, value) -> { UpdateResult result = convertToImageUrl(tenantId, name, value, links); - if (result.isUpdated()) { + if (result.updated()) { updated.set(true); } - return result.getValue(); + return result.value(); }); return updated.get(); } @@ -513,10 +514,10 @@ public class BaseImageService extends BaseResourceService implements ImageServic AtomicBoolean updated = new AtomicBoolean(false); JacksonUtil.replaceAll(root, title, (path, value) -> { UpdateResult result = convertToImageUrl(tenantId, path, value, true, links); - if (result.isUpdated()) { + if (result.updated()) { updated.set(true); } - return result.getValue(); + return result.value(); }); return updated.get(); } @@ -677,16 +678,18 @@ public class BaseImageService extends BaseResourceService implements ImageServic private String getImageLink(String value) { if (value.startsWith(DataConstants.TB_IMAGE_PREFIX + "/api/images")) { - return StringUtils.removeStart(value, DataConstants.TB_IMAGE_PREFIX); + return Strings.CS.removeStart(value, DataConstants.TB_IMAGE_PREFIX); } else { return null; } } - @Data(staticConstructor = "of") - private static class UpdateResult { - private final boolean updated; - private final String value; + private record UpdateResult(boolean updated, String value) { + + static UpdateResult of(boolean updated, String value) { + return new UpdateResult(updated, value); + } + } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/resource/BaseResourceService.java b/dao/src/main/java/org/thingsboard/server/dao/resource/BaseResourceService.java index bf941256f5..1eed9a6b59 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/resource/BaseResourceService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/resource/BaseResourceService.java @@ -18,6 +18,7 @@ package org.thingsboard.server.dao.resource; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.TextNode; import com.google.common.hash.Hashing; +import com.google.common.util.concurrent.FluentFuture; import com.google.common.util.concurrent.ListenableFuture; import jakarta.annotation.PostConstruct; import lombok.RequiredArgsConstructor; @@ -35,11 +36,13 @@ import org.thingsboard.server.cache.resourceInfo.ResourceInfoCacheKey; import org.thingsboard.server.cache.resourceInfo.ResourceInfoEvictEvent; import org.thingsboard.server.common.data.Dashboard; import org.thingsboard.server.common.data.DataConstants; +import org.thingsboard.server.common.data.EntityInfo; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.ResourceExportData; import org.thingsboard.server.common.data.ResourceSubType; import org.thingsboard.server.common.data.ResourceType; import org.thingsboard.server.common.data.TbResource; +import org.thingsboard.server.common.data.TbResourceDataInfo; import org.thingsboard.server.common.data.TbResourceDeleteResult; import org.thingsboard.server.common.data.TbResourceInfo; import org.thingsboard.server.common.data.TbResourceInfoFilter; @@ -56,6 +59,7 @@ import org.thingsboard.server.dao.entity.AbstractCachedEntityService; import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent; import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent; import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.dao.rule.RuleChainDao; import org.thingsboard.server.dao.service.PaginatedRemover; import org.thingsboard.server.dao.service.Validator; import org.thingsboard.server.dao.service.validator.ResourceDataValidator; @@ -76,6 +80,7 @@ import java.util.UUID; import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.UnaryOperator; +import static com.google.common.util.concurrent.MoreExecutors.directExecutor; import static org.thingsboard.server.common.data.StringUtils.isNotEmpty; import static org.thingsboard.server.dao.device.DeviceServiceImpl.INCORRECT_TENANT_ID; import static org.thingsboard.server.dao.service.Validator.validateId; @@ -92,16 +97,20 @@ public class BaseResourceService extends AbstractCachedEntityService> resourceContainerDaoMap = new HashMap<>(); + protected final RuleChainDao ruleChainDao; + private final Map> resourceLinkContainerDaoMap = new HashMap<>(); + private final Map> generalResourceContainerDaoMap = new HashMap<>(); protected static final int MAX_ENTITIES_TO_FIND = 10; @PostConstruct public void init() { - resourceContainerDaoMap.put(EntityType.WIDGET_TYPE, widgetTypeDao); - resourceContainerDaoMap.put(EntityType.DASHBOARD, dashboardInfoDao); + resourceLinkContainerDaoMap.put(EntityType.WIDGET_TYPE, widgetTypeDao); + resourceLinkContainerDaoMap.put(EntityType.DASHBOARD, dashboardInfoDao); + generalResourceContainerDaoMap.put(EntityType.RULE_CHAIN, ruleChainDao); } - @Autowired @Lazy + @Autowired + @Lazy private ImageService imageService; private static final Map DASHBOARD_RESOURCES_MAPPING = Map.of( @@ -206,6 +215,12 @@ public class BaseResourceService extends AbstractCachedEntityService>> affectedEntities = new HashMap<>(); - - resourceContainerDaoMap.forEach((entityType, resourceContainerDao) -> { - var entities = tenantId.isSysTenantId() ? resourceContainerDao.findByResourceLink(link, MAX_ENTITIES_TO_FIND) : - resourceContainerDao.findByTenantIdAndResourceLink(tenantId, link, MAX_ENTITIES_TO_FIND); - if (!entities.isEmpty()) { - affectedEntities.put(entityType.name(), entities); - } - }); - - if (!affectedEntities.isEmpty()) { - success = false; - result.references(affectedEntities); - } + Map> references = findResourceReferences(tenantId, resource); + if (!references.isEmpty()) { + success = false; + result.references(references); } } if (success) { resourceDao.removeById(tenantId, resourceId.getId()); + publishEvictEvent(new ResourceInfoEvictEvent(tenantId, resourceId)); eventPublisher.publishEvent(DeleteEntityEvent.builder().tenantId(tenantId).entity(resource).entityId(resourceId).build()); } return result.success(success).build(); } + private Map> findResourceReferences(TenantId tenantId, TbResourceInfo resource) { + Map> references = new HashMap<>(); + + if (resource.getResourceType() == ResourceType.JS_MODULE) { + var ref = resource.getLink(); + findReferences(tenantId, references, ref, resourceLinkContainerDaoMap); + } + + if (resource.getResourceType() == ResourceType.GENERAL) { + var ref = resource.getId().getId().toString(); + findReferences(tenantId, references, ref, generalResourceContainerDaoMap); + } + + return references; + } + + private void findReferences(TenantId tenantId, Map> references, String ref, Map> resourceLinkContainerDaoMap) { + resourceLinkContainerDaoMap.forEach((entityType, dao) -> { + List entities = tenantId.isSysTenantId() + ? dao.findByResource(ref, MAX_ENTITIES_TO_FIND) + : dao.findByTenantIdAndResource(tenantId, ref, MAX_ENTITIES_TO_FIND); + if (!entities.isEmpty()) { + references.put(entityType.name(), entities); + } + }); + } + @Override public void deleteEntity(TenantId tenantId, EntityId id, boolean force) { deleteResource(tenantId, (TbResourceId) id, force); @@ -429,6 +460,12 @@ public class BaseResourceService extends AbstractCachedEntityService>> findEntityAsync(TenantId tenantId, EntityId entityId) { + return FluentFuture.from(findResourceInfoByIdAsync(tenantId, new TbResourceId(entityId.getId()))) + .transform(Optional::ofNullable, directExecutor()); + } + @Override public EntityType getEntityType() { return EntityType.TB_RESOURCE; @@ -663,6 +700,12 @@ public class BaseResourceService extends AbstractCachedEntityService findSystemOrTenantResourcesByIds(TenantId tenantId, List resourceIds) { + log.trace("Executing findSystemOrTenantResourcesByIds, tenantId [{}], resourceIds [{}]", tenantId, resourceIds); + return resourceInfoDao.findSystemOrTenantResourcesByIds(tenantId, resourceIds); + } + @Override public String calculateEtag(byte[] data) { return Hashing.sha256().hashBytes(data).toString(); diff --git a/dao/src/main/java/org/thingsboard/server/dao/resource/DefaultTbResourceDataCache.java b/dao/src/main/java/org/thingsboard/server/dao/resource/DefaultTbResourceDataCache.java new file mode 100644 index 0000000000..452f86b1a6 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/resource/DefaultTbResourceDataCache.java @@ -0,0 +1,72 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.resource; + +import com.github.benmanes.caffeine.cache.AsyncLoadingCache; +import com.github.benmanes.caffeine.cache.Caffeine; +import com.google.common.util.concurrent.FluentFuture; +import jakarta.annotation.PostConstruct; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.thingsboard.common.util.DonAsynchron; +import org.thingsboard.server.common.data.TbResourceDataInfo; +import org.thingsboard.server.common.data.id.TbResourceId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.dao.sql.JpaExecutorService; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; + +@Service +@RequiredArgsConstructor +@Slf4j +public class DefaultTbResourceDataCache implements TbResourceDataCache { + + private final ResourceService resourceService; + private final JpaExecutorService executorService; + + @Value("${cache.tbResourceData.maxSize:100000}") + private int cacheMaxSize; + @Value("${cache.tbResourceData.timeToLiveInMinutes:44640}") + private int cacheValueTtl; + private AsyncLoadingCache cache; + + @PostConstruct + private void init() { + cache = Caffeine.newBuilder() + .maximumSize(cacheMaxSize) + .expireAfterAccess(cacheValueTtl, TimeUnit.MINUTES) + .executor(executorService) + .buildAsync((key, executor) -> CompletableFuture.supplyAsync(() -> resourceService.getResourceDataInfo(key.tenantId(), key.resourceId()), executor)); + } + + @Override + public FluentFuture getResourceDataInfoAsync(TenantId tenantId, TbResourceId resourceId) { + log.trace("Retrieving resource data info by id [{}], tenant id [{}] from cache", resourceId, tenantId); + return DonAsynchron.toFluentFuture(cache.get(new ResourceDataKey(tenantId, resourceId))); + } + + @Override + public void evictResourceData(TenantId tenantId, TbResourceId resourceId) { + cache.asMap().remove(new ResourceDataKey(tenantId, resourceId)); + log.trace("Evicted resource data info with id [{}], tenant id [{}]", resourceId, tenantId); + } + + record ResourceDataKey (TenantId tenantId, TbResourceId resourceId) {} + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/resource/TbResourceDao.java b/dao/src/main/java/org/thingsboard/server/dao/resource/TbResourceDao.java index 23b59b5658..1b9f250521 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/resource/TbResourceDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/resource/TbResourceDao.java @@ -18,6 +18,7 @@ package org.thingsboard.server.dao.resource; import org.thingsboard.server.common.data.ResourceSubType; import org.thingsboard.server.common.data.ResourceType; import org.thingsboard.server.common.data.TbResource; +import org.thingsboard.server.common.data.TbResourceDataInfo; import org.thingsboard.server.common.data.id.TbResourceId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; @@ -51,4 +52,5 @@ public interface TbResourceDao extends Dao, TenantEntityWithDataDao, long getResourceSize(TenantId tenantId, TbResourceId resourceId); + TbResourceDataInfo getResourceDataInfo(TenantId tenantId, TbResourceId resourceId); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/resource/TbResourceInfoDao.java b/dao/src/main/java/org/thingsboard/server/dao/resource/TbResourceInfoDao.java index 8e97738501..f4fe02843d 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/resource/TbResourceInfoDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/resource/TbResourceInfoDao.java @@ -18,6 +18,7 @@ package org.thingsboard.server.dao.resource; import org.thingsboard.server.common.data.ResourceType; import org.thingsboard.server.common.data.TbResourceInfo; import org.thingsboard.server.common.data.TbResourceInfoFilter; +import org.thingsboard.server.common.data.id.TbResourceId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; @@ -46,4 +47,5 @@ public interface TbResourceInfoDao extends Dao { TbResourceInfo findPublicResourceByKey(ResourceType resourceType, String publicResourceKey); + List findSystemOrTenantResourcesByIds(TenantId tenantId, List resourceIds); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/rpc/BaseRpcService.java b/dao/src/main/java/org/thingsboard/server/dao/rpc/BaseRpcService.java index 15b9717f47..f06e2e45df 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/rpc/BaseRpcService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/rpc/BaseRpcService.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.dao.rpc; +import com.google.common.util.concurrent.FluentFuture; import com.google.common.util.concurrent.ListenableFuture; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -33,6 +34,7 @@ import org.thingsboard.server.dao.service.PaginatedRemover; import java.util.Optional; +import static com.google.common.util.concurrent.MoreExecutors.directExecutor; import static org.thingsboard.server.dao.service.Validator.validateId; import static org.thingsboard.server.dao.service.Validator.validatePageLink; @@ -40,6 +42,7 @@ import static org.thingsboard.server.dao.service.Validator.validatePageLink; @Slf4j @RequiredArgsConstructor public class BaseRpcService implements RpcService { + public static final String INCORRECT_TENANT_ID = "Incorrect tenantId "; public static final String INCORRECT_RPC_ID = "Incorrect rpcId "; @@ -113,21 +116,29 @@ public class BaseRpcService implements RpcService { return Optional.ofNullable(findById(tenantId, new RpcId(entityId.getId()))); } + @Override + public FluentFuture>> findEntityAsync(TenantId tenantId, EntityId entityId) { + return FluentFuture.from(findRpcByIdAsync(tenantId, new RpcId(entityId.getId()))) + .transform(Optional::ofNullable, directExecutor()); + } + @Override public EntityType getEntityType() { return EntityType.RPC; } - private PaginatedRemover tenantRpcRemover = - new PaginatedRemover<>() { - @Override - protected PageData findEntities(TenantId tenantId, TenantId id, PageLink pageLink) { - return rpcDao.findAllRpcByTenantId(id, pageLink); - } - - @Override - protected void removeEntity(TenantId tenantId, Rpc entity) { - deleteRpc(tenantId, entity.getId()); - } - }; + private final PaginatedRemover tenantRpcRemover = new PaginatedRemover<>() { + + @Override + protected PageData findEntities(TenantId tenantId, TenantId id, PageLink pageLink) { + return rpcDao.findAllRpcByTenantId(id, pageLink); + } + + @Override + protected void removeEntity(TenantId tenantId, Rpc entity) { + deleteRpc(tenantId, entity.getId()); + } + + }; + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java b/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java index 8538bd9492..c10c601884 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java @@ -18,6 +18,7 @@ package org.thingsboard.server.dao.rule; import com.datastax.oss.driver.api.core.uuid.Uuids; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ObjectNode; +import com.google.common.util.concurrent.FluentFuture; import com.google.common.util.concurrent.ListenableFuture; import lombok.extern.slf4j.Slf4j; import org.apache.commons.collections4.CollectionUtils; @@ -28,6 +29,7 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.BaseData; +import org.thingsboard.server.common.data.BaseDataWithAdditionalInfo; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.edge.Edge; @@ -38,6 +40,7 @@ import org.thingsboard.server.common.data.id.HasId; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.RuleNodeId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.UUIDBased; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageDataIterable; import org.thingsboard.server.common.data.page.PageLink; @@ -80,6 +83,7 @@ import java.util.UUID; import java.util.function.Function; import java.util.stream.Collectors; +import static com.google.common.util.concurrent.MoreExecutors.directExecutor; import static org.thingsboard.server.common.data.DataConstants.TENANT; import static org.thingsboard.server.dao.service.Validator.validateId; import static org.thingsboard.server.dao.service.Validator.validateIds; @@ -87,9 +91,6 @@ import static org.thingsboard.server.dao.service.Validator.validatePageLink; import static org.thingsboard.server.dao.service.Validator.validatePositiveNumber; import static org.thingsboard.server.dao.service.Validator.validateString; -/** - * Created by igor on 3/12/18. - */ @Service("RuleChainDaoService") @Slf4j public class BaseRuleChainService extends AbstractEntityService implements RuleChainService { @@ -98,6 +99,7 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC public static final String INCORRECT_TENANT_ID = "Incorrect tenantId "; public static final String TB_RULE_CHAIN_INPUT_NODE = "org.thingsboard.rule.engine.flow.TbRuleChainInputNode"; + @Autowired private RuleChainDao ruleChainDao; @@ -125,6 +127,10 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC @Override @Transactional public RuleChain saveRuleChain(RuleChain ruleChain, boolean publishSaveEvent, boolean doValidate) { + return saveEntity(ruleChain, () -> doSaveRuleChain(ruleChain, publishSaveEvent, doValidate)); + } + + private RuleChain doSaveRuleChain(RuleChain ruleChain, boolean publishSaveEvent, boolean doValidate) { log.trace("Executing doSaveRuleChain [{}]", ruleChain); if (doValidate) { ruleChainValidator.validate(ruleChain, RuleChain::getTenantId); @@ -260,7 +266,7 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC firstRuleNodeId = nodes.get(ruleChainMetaData.getFirstNodeIndex()).getId(); } if ((ruleChain.getFirstRuleNodeId() != null && !ruleChain.getFirstRuleNodeId().equals(firstRuleNodeId)) - || (ruleChain.getFirstRuleNodeId() == null && firstRuleNodeId != null)) { + || (ruleChain.getFirstRuleNodeId() == null && firstRuleNodeId != null)) { ruleChain.setFirstRuleNodeId(firstRuleNodeId); } if (ruleChainMetaData.getConnections() != null) { @@ -876,6 +882,17 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC return Optional.ofNullable(hasId); } + @Override + public FluentFuture>> findEntityAsync(TenantId tenantId, EntityId entityId) { + ListenableFuture> future; + if (entityId.getEntityType() == EntityType.RULE_NODE) { + future = findRuleNodeByIdAsync(tenantId, new RuleNodeId(entityId.getId())); + } else { + future = findRuleChainByIdAsync(tenantId, new RuleChainId(entityId.getId())); + } + return FluentFuture.from(future).transform(Optional::ofNullable, directExecutor()); + } + @Override public long countByTenantId(TenantId tenantId) { return ruleChainDao.countByTenantId(tenantId); @@ -919,18 +936,11 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC ComponentClusteringMode nodeConfigType = ReflectionUtils.getAnnotationProperty(ruleNode.getType(), "org.thingsboard.rule.engine.api.RuleNode", "clusteringMode"); - switch (nodeConfigType) { - case ENABLED: - singletonMode = false; - break; - case SINGLETON: - singletonMode = true; - break; - case USER_PREFERENCE: - default: - singletonMode = ruleNode.isSingletonMode(); - break; - } + singletonMode = switch (nodeConfigType) { + case ENABLED -> false; + case SINGLETON -> true; + default -> ruleNode.isSingletonMode(); + }; } catch (Exception e) { log.warn("Failed to get clustering mode: {}", ExceptionUtils.getRootCauseMessage(e)); singletonMode = false; diff --git a/dao/src/main/java/org/thingsboard/server/dao/rule/RuleChainDao.java b/dao/src/main/java/org/thingsboard/server/dao/rule/RuleChainDao.java index 5b09eec42a..ac716bb4fc 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/rule/RuleChainDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/rule/RuleChainDao.java @@ -21,8 +21,10 @@ import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.rule.RuleChain; import org.thingsboard.server.common.data.rule.RuleChainType; +import org.thingsboard.server.common.data.rule.RuleNode; import org.thingsboard.server.dao.Dao; import org.thingsboard.server.dao.ExportableEntityDao; +import org.thingsboard.server.dao.ResourceContainerDao; import org.thingsboard.server.dao.TenantEntityDao; import java.util.Collection; @@ -31,7 +33,7 @@ import java.util.UUID; /** * Created by igor on 3/12/18. */ -public interface RuleChainDao extends Dao, TenantEntityDao, ExportableEntityDao { +public interface RuleChainDao extends Dao, TenantEntityDao, ExportableEntityDao, ResourceContainerDao { /** * Find rule chains by tenantId and page link. diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/NoXssValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/NoXssValidator.java index 1fbe682831..ff71be4298 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/NoXssValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/NoXssValidator.java @@ -26,9 +26,13 @@ import org.owasp.validator.html.ScanException; import org.thingsboard.server.common.data.validation.NoXss; import java.util.Optional; +import java.util.regex.Pattern; @Slf4j public class NoXssValidator implements ConstraintValidator { + + private static final Pattern JS_TEMPLATE_PATTERN = Pattern.compile("\\{\\{.*}}", Pattern.DOTALL); + private static final AntiSamy xssChecker = new AntiSamy(); private static final Policy xssPolicy; @@ -59,6 +63,9 @@ public class NoXssValidator implements ConstraintValidator { if (stringValue.isEmpty()) { return true; } + if (JS_TEMPLATE_PATTERN.matcher(stringValue).find()) { + return false; + } try { return xssChecker.scan(stringValue, xssPolicy).getNumberOfErrors() == 0; } catch (ScanException | PolicyException e) { diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/CalculatedFieldDataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/CalculatedFieldDataValidator.java index c9c7af1a89..c10da4e6c6 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/validator/CalculatedFieldDataValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/validator/CalculatedFieldDataValidator.java @@ -18,7 +18,12 @@ package org.thingsboard.server.dao.service.validator; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.cf.CalculatedField; -import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.cf.CalculatedFieldType; +import org.thingsboard.server.common.data.cf.configuration.ArgumentsBasedCalculatedFieldConfiguration; +import org.thingsboard.server.common.data.cf.configuration.RelationPathQueryDynamicSourceConfiguration; +import org.thingsboard.server.common.data.cf.configuration.ScheduledUpdateSupportedCalculatedFieldConfiguration; +import org.thingsboard.server.common.data.cf.configuration.aggregation.RelatedEntitiesAggregationCalculatedFieldConfiguration; +import org.thingsboard.server.common.data.cf.configuration.aggregation.single.EntityAggregationCalculatedFieldConfiguration; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; import org.thingsboard.server.dao.cf.CalculatedFieldDao; @@ -26,6 +31,10 @@ import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.dao.usagerecord.ApiLimitService; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + @Component public class CalculatedFieldDataValidator extends DataValidator { @@ -36,46 +45,110 @@ public class CalculatedFieldDataValidator extends DataValidator private ApiLimitService apiLimitService; @Override - protected void validateCreate(TenantId tenantId, CalculatedField calculatedField) { - validateNumberOfCFsPerEntity(tenantId, calculatedField.getEntityId()); + protected void validateDataImpl(TenantId tenantId, CalculatedField calculatedField) { validateNumberOfArgumentsPerCF(tenantId, calculatedField); - validateArgumentNames(calculatedField); + validateCalculatedFieldConfiguration(calculatedField); + validateSchedulingConfiguration(tenantId, calculatedField); + validateRelationQuerySourceArguments(tenantId, calculatedField); + validateAggregationConfiguration(tenantId, calculatedField); + validateEntityAggregationConfiguration(tenantId, calculatedField); } @Override - protected CalculatedField validateUpdate(TenantId tenantId, CalculatedField calculatedField) { - CalculatedField old = calculatedFieldDao.findById(calculatedField.getTenantId(), calculatedField.getId().getId()); - if (old == null) { - throw new DataValidationException("Can't update non existing calculated field!"); + protected void validateCreate(TenantId tenantId, CalculatedField calculatedField) { + if (calculatedField.getType() == CalculatedFieldType.ALARM) { + return; } - validateNumberOfArgumentsPerCF(tenantId, calculatedField); - validateArgumentNames(calculatedField); - return old; - } - - private void validateNumberOfCFsPerEntity(TenantId tenantId, EntityId entityId) { long maxCFsPerEntity = apiLimitService.getLimit(tenantId, DefaultTenantProfileConfiguration::getMaxCalculatedFieldsPerEntity); if (maxCFsPerEntity <= 0) { return; } - if (calculatedFieldDao.countCFByEntityId(tenantId, entityId) >= maxCFsPerEntity) { + if (calculatedFieldDao.countByEntityIdAndTypeNot(tenantId, calculatedField.getEntityId(), CalculatedFieldType.ALARM) >= maxCFsPerEntity) { throw new DataValidationException("Calculated fields per entity limit reached!"); } } + @Override + protected CalculatedField validateUpdate(TenantId tenantId, CalculatedField calculatedField) { + CalculatedField old = calculatedFieldDao.findById(calculatedField.getTenantId(), calculatedField.getId().getId()); + if (old == null) { + throw new DataValidationException("Can't update non existing calculated field!"); + } + return old; + } + private void validateNumberOfArgumentsPerCF(TenantId tenantId, CalculatedField calculatedField) { + if (!(calculatedField instanceof ArgumentsBasedCalculatedFieldConfiguration argumentsBasedCfg)) { + return; + } long maxArgumentsPerCF = apiLimitService.getLimit(tenantId, DefaultTenantProfileConfiguration::getMaxArgumentsPerCF); if (maxArgumentsPerCF <= 0) { return; } - if (calculatedField.getConfiguration().getArguments().size() > maxArgumentsPerCF) { + if (argumentsBasedCfg.getArguments().size() > maxArgumentsPerCF) { throw new DataValidationException("Calculated field arguments limit reached!"); } } - private void validateArgumentNames(CalculatedField calculatedField) { - if (calculatedField.getConfiguration().getArguments().containsKey("ctx")) { - throw new DataValidationException("Argument name 'ctx' is reserved and cannot be used."); + private void validateCalculatedFieldConfiguration(CalculatedField calculatedField) { + wrapAsDataValidation(calculatedField.getConfiguration()::validate); + } + + private void validateSchedulingConfiguration(TenantId tenantId, CalculatedField calculatedField) { + if (!(calculatedField.getConfiguration() instanceof ScheduledUpdateSupportedCalculatedFieldConfiguration scheduledUpdateCfg) + || !scheduledUpdateCfg.isScheduledUpdateEnabled()) { + return; + } + long minAllowedScheduledUpdateInterval = apiLimitService.getLimit(tenantId, DefaultTenantProfileConfiguration::getMinAllowedScheduledUpdateIntervalInSecForCF); + wrapAsDataValidation(() -> scheduledUpdateCfg.validate(minAllowedScheduledUpdateInterval)); + } + + private void validateRelationQuerySourceArguments(TenantId tenantId, CalculatedField calculatedField) { + if (!(calculatedField.getConfiguration() instanceof ArgumentsBasedCalculatedFieldConfiguration argumentsBasedCfg)) { + return; + } + Map relationQueryBasedArguments = argumentsBasedCfg.getArguments().entrySet() + .stream() + .filter(entry -> entry.getValue().hasRelationQuerySource()) + .collect(Collectors.toMap(Map.Entry::getKey, entry -> (RelationPathQueryDynamicSourceConfiguration) entry.getValue().getRefDynamicSourceConfiguration())); + if (relationQueryBasedArguments.isEmpty()) { + return; + } + int maxRelationLevel = (int) apiLimitService.getLimit(tenantId, DefaultTenantProfileConfiguration::getMaxRelationLevelPerCfArgument); + relationQueryBasedArguments.forEach((argumentName, relationQueryDynamicSourceConfiguration) -> + wrapAsDataValidation(() -> relationQueryDynamicSourceConfiguration.validateMaxRelationLevel(argumentName, maxRelationLevel))); + } + + private void validateAggregationConfiguration(TenantId tenantId, CalculatedField calculatedField) { + if (!(calculatedField.getConfiguration() instanceof RelatedEntitiesAggregationCalculatedFieldConfiguration aggConfiguration)) { + return; + } + long minDeduplicationInterval = apiLimitService.getLimit(tenantId, DefaultTenantProfileConfiguration::getMinAllowedDeduplicationIntervalInSecForCF); + if (aggConfiguration.getDeduplicationIntervalInSec() < minDeduplicationInterval) { + throw new IllegalArgumentException("Deduplication interval is less than configured " + + "minimum allowed interval in tenant profile: " + minDeduplicationInterval); + } + } + + private void validateEntityAggregationConfiguration(TenantId tenantId, CalculatedField calculatedField) { + if (!(calculatedField.getConfiguration() instanceof EntityAggregationCalculatedFieldConfiguration aggConfiguration)) { + return; + } + long minAggregationIntervalInSec = apiLimitService.getLimit(tenantId, DefaultTenantProfileConfiguration::getMinAllowedAggregationIntervalInSecForCF); + if (minAggregationIntervalInSec <= 0) { + return; + } + if (aggConfiguration.getInterval().getCurrentIntervalDurationMillis() < TimeUnit.SECONDS.toMillis(minAggregationIntervalInSec)) { + throw new IllegalArgumentException("Aggregation interval duration is less than configured " + + "minimum allowed aggregation interval in tenant profile: " + minAggregationIntervalInSec + " sec."); + } + } + + private static void wrapAsDataValidation(Runnable validation) { + try { + validation.run(); + } catch (IllegalArgumentException e) { + throw new DataValidationException(e.getMessage(), e); } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/CalculatedFieldLinkDataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/CalculatedFieldLinkDataValidator.java deleted file mode 100644 index aaba200c92..0000000000 --- a/dao/src/main/java/org/thingsboard/server/dao/service/validator/CalculatedFieldLinkDataValidator.java +++ /dev/null @@ -1,41 +0,0 @@ -/** - * Copyright © 2016-2025 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.dao.service.validator; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Component; -import org.thingsboard.server.common.data.cf.CalculatedFieldLink; -import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.dao.cf.CalculatedFieldLinkDao; -import org.thingsboard.server.dao.exception.DataValidationException; -import org.thingsboard.server.dao.service.DataValidator; - -@Component -public class CalculatedFieldLinkDataValidator extends DataValidator { - - @Autowired - private CalculatedFieldLinkDao calculatedFieldLinkDao; - - @Override - protected CalculatedFieldLink validateUpdate(TenantId tenantId, CalculatedFieldLink calculatedFieldLink) { - CalculatedFieldLink old = calculatedFieldLinkDao.findById(calculatedFieldLink.getTenantId(), calculatedFieldLink.getId().getId()); - if (old == null) { - throw new DataValidationException("Can't update non existing calculated field link!"); - } - return old; - } - -} diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/DeviceProfileDataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/DeviceProfileDataValidator.java index dfd0ee82bc..401b199598 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/validator/DeviceProfileDataValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/validator/DeviceProfileDataValidator.java @@ -69,6 +69,10 @@ import java.util.HashSet; import java.util.List; import java.util.Set; +import static org.thingsboard.server.common.data.device.credentials.lwm2m.Lwm2mServerIdentifier.LWM2M_SERVER_MAX; +import static org.thingsboard.server.common.data.device.credentials.lwm2m.Lwm2mServerIdentifier.PRIMARY_LWM2M_SERVER; +import static org.thingsboard.server.common.data.device.credentials.lwm2m.Lwm2mServerIdentifier.isNotLwm2mServer; + @Slf4j @Component public class DeviceProfileDataValidator extends AbstractHasOtaPackageValidator { @@ -337,19 +341,22 @@ public class DeviceProfileDataValidator extends AbstractHasOtaPackageValidator 65535) { - throw new DeviceCredentialsValidationException("Bootstrap Server ShortServerId must be in range [0 - 65535]!"); - } - } else { - if (serverConfig.getShortServerId() < 1 || serverConfig.getShortServerId() > 65534) { - throw new DeviceCredentialsValidationException("LwM2M Server ShortServerId must be in range [1 - 65534]!"); + if (serverConfig.isBootstrapServerIs()){ + if (serverConfig.getShortServerId() != null) { + if (serverConfig.getShortServerId() == 0) { + serverConfig.setShortServerId(null); + } else { + throw new DeviceCredentialsValidationException("Bootstrap Server ShortServerId must be null!"); } } } else { - String serverName = serverConfig.isBootstrapServerIs() ? "Bootstrap Server" : "LwM2M Server"; - throw new DeviceCredentialsValidationException(serverName + " ShortServerId must not be null!"); + if (serverConfig.getShortServerId() != null) { + if (isNotLwm2mServer(serverConfig.getShortServerId())) { + throw new DeviceCredentialsValidationException("LwM2M Server ShortServerId must be in range [" + PRIMARY_LWM2M_SERVER.getId() + " - " + LWM2M_SERVER_MAX.getId() + "]!"); + } + } else { + throw new DeviceCredentialsValidationException("LwM2M Server ShortServerId must not be null!"); + } } String server = serverConfig.isBootstrapServerIs() ? "Bootstrap Server" : "LwM2M Server"; diff --git a/dao/src/main/java/org/thingsboard/server/dao/settings/AdminSettingsServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/settings/AdminSettingsServiceImpl.java index 604a37f4bb..7440481700 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/settings/AdminSettingsServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/settings/AdminSettingsServiceImpl.java @@ -17,6 +17,7 @@ package org.thingsboard.server.dao.settings; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ObjectNode; +import com.google.common.util.concurrent.FluentFuture; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @@ -31,6 +32,8 @@ import org.thingsboard.server.dao.service.Validator; import java.util.Optional; +import static com.google.common.util.concurrent.MoreExecutors.directExecutor; + @Service @Slf4j public class AdminSettingsServiceImpl implements AdminSettingsService { @@ -106,6 +109,12 @@ public class AdminSettingsServiceImpl implements AdminSettingsService { return Optional.ofNullable(adminSettingsDao.findById(tenantId, entityId.getId())); } + @Override + public FluentFuture>> findEntityAsync(TenantId tenantId, EntityId entityId) { + return FluentFuture.from(adminSettingsDao.findByIdAsync(tenantId, entityId.getId())) + .transform(Optional::ofNullable, directExecutor()); + } + @Override public EntityType getEntityType() { return EntityType.ADMIN_SETTINGS; diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/asset/AssetRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/asset/AssetRepository.java index e475864684..aa9c29df49 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/asset/AssetRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/asset/AssetRepository.java @@ -21,6 +21,7 @@ import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; +import org.thingsboard.server.common.data.EntityInfo; import org.thingsboard.server.common.data.edqs.fields.AssetFields; import org.thingsboard.server.common.data.util.TbPair; import org.thingsboard.server.dao.ExportableEntityRepository; @@ -103,6 +104,10 @@ public interface AssetRepository extends JpaRepository, Expor AssetEntity findByTenantIdAndName(UUID tenantId, String name); + @Query("SELECT new org.thingsboard.server.common.data.EntityInfo(a.id, 'ASSET', a.name) " + + "FROM AssetEntity a WHERE a.tenantId = :tenantId AND a.name LIKE CONCAT(:prefix, '%')") + List findEntityInfosByNamePrefix(UUID tenantId, String prefix); + @Query("SELECT a FROM AssetEntity a WHERE a.tenantId = :tenantId " + "AND a.type = :type " + "AND (:textSearch IS NULL OR ilike(a.name, CONCAT('%', :textSearch, '%')) = true " + diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/asset/JpaAssetDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/asset/JpaAssetDao.java index 4b55884792..593a672d8a 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/asset/JpaAssetDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/asset/JpaAssetDao.java @@ -21,6 +21,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Limit; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.EntityInfo; import org.thingsboard.server.common.data.EntitySubtype; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.ProfileEntityIdInfo; @@ -267,6 +268,12 @@ public class JpaAssetDao extends JpaAbstractDao implements A return nativeAssetRepository.findProfileEntityIdInfosByTenantId(tenantId, DaoUtil.toPageable(pageLink)); } + @Override + public List findEntityInfosByNamePrefix(TenantId tenantId, String name) { + log.debug("Find asset entity infos by name [{}]", name); + return assetRepository.findEntityInfosByNamePrefix(tenantId.getId(), name); + } + @Override public Long countByTenantId(TenantId tenantId) { return assetRepository.countByTenantId(tenantId.getId()); diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/cf/CalculatedFieldLinkRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/cf/CalculatedFieldLinkRepository.java deleted file mode 100644 index 6f6a0775f3..0000000000 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/cf/CalculatedFieldLinkRepository.java +++ /dev/null @@ -1,36 +0,0 @@ -/** - * Copyright © 2016-2025 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.dao.sql.cf; - -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; -import org.springframework.data.jpa.repository.JpaRepository; -import org.thingsboard.server.dao.model.sql.CalculatedFieldLinkEntity; - -import java.util.List; -import java.util.UUID; - -public interface CalculatedFieldLinkRepository extends JpaRepository { - - List findAllByTenantIdAndCalculatedFieldId(UUID tenantId, UUID calculatedFieldId); - - List findAllByTenantIdAndEntityId(UUID tenantId, UUID entityId); - - List findAllByTenantId(UUID tenantId); - - Page findAllByTenantId(UUID tenantId, Pageable pageable); - -} diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/cf/CalculatedFieldRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/cf/CalculatedFieldRepository.java index 8ccdb88db0..6725387fba 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/cf/CalculatedFieldRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/cf/CalculatedFieldRepository.java @@ -18,6 +18,7 @@ package org.thingsboard.server.dao.sql.cf; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; import org.thingsboard.server.common.data.id.CalculatedFieldId; import org.thingsboard.server.dao.model.sql.CalculatedFieldEntity; @@ -28,7 +29,7 @@ public interface CalculatedFieldRepository extends JpaRepository findCalculatedFieldIdsByTenantIdAndEntityId(UUID tenantId, UUID entityId); @@ -36,12 +37,15 @@ public interface CalculatedFieldRepository extends JpaRepository findAllByTenantId(UUID tenantId, Pageable pageable); - Page findAllByTenantIdAndEntityId(UUID tenantId, UUID entityId, Pageable pageable); + @Query("SELECT cf FROM CalculatedFieldEntity cf WHERE cf.tenantId = :tenantId " + + "AND cf.entityId = :entityId AND cf.type IN :types " + + "AND (:textSearch IS NULL OR ilike(cf.name, CONCAT('%', :textSearch, '%')) = true)") + Page findByTenantIdAndEntityIdAndTypes(UUID tenantId, UUID entityId, List types, String textSearch, Pageable pageable); List findAllByTenantId(UUID tenantId); List removeAllByTenantIdAndEntityId(UUID tenantId, UUID entityId); - long countByTenantIdAndEntityId(UUID tenantId, UUID entityId); + long countByTenantIdAndEntityIdAndTypeNot(UUID tenantId, UUID entityId, String type); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/cf/DefaultNativeCalculatedFieldRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/cf/DefaultNativeCalculatedFieldRepository.java index bbce4e2721..f2b565131e 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/cf/DefaultNativeCalculatedFieldRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/cf/DefaultNativeCalculatedFieldRepository.java @@ -25,12 +25,10 @@ import org.springframework.transaction.support.TransactionTemplate; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.cf.CalculatedField; -import org.thingsboard.server.common.data.cf.CalculatedFieldLink; import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.cf.configuration.CalculatedFieldConfiguration; import org.thingsboard.server.common.data.debug.DebugSettings; import org.thingsboard.server.common.data.id.CalculatedFieldId; -import org.thingsboard.server.common.data.id.CalculatedFieldLinkId; import org.thingsboard.server.common.data.id.EntityIdFactory; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; @@ -49,9 +47,6 @@ public class DefaultNativeCalculatedFieldRepository implements NativeCalculatedF private final String CF_COUNT_QUERY = "SELECT count(id) FROM calculated_field;"; private final String CF_QUERY = "SELECT * FROM calculated_field ORDER BY created_time ASC LIMIT %s OFFSET %s"; - private final String CFL_COUNT_QUERY = "SELECT count(id) FROM calculated_field_link;"; - private final String CFL_QUERY = "SELECT * FROM calculated_field_link ORDER BY created_time ASC LIMIT %s OFFSET %s"; - private final NamedParameterJdbcTemplate jdbcTemplate; private final TransactionTemplate transactionTemplate; @@ -103,37 +98,4 @@ public class DefaultNativeCalculatedFieldRepository implements NativeCalculatedF }); } - @Override - public PageData findCalculatedFieldLinks(Pageable pageable) { - return transactionTemplate.execute(status -> { - long startTs = System.currentTimeMillis(); - int totalElements = jdbcTemplate.queryForObject(CFL_COUNT_QUERY, Collections.emptyMap(), Integer.class); - log.debug("Count query took {} ms", System.currentTimeMillis() - startTs); - startTs = System.currentTimeMillis(); - List> rows = jdbcTemplate.queryForList(String.format(CFL_QUERY, pageable.getPageSize(), pageable.getOffset()), Collections.emptyMap()); - log.debug("Main query took {} ms", System.currentTimeMillis() - startTs); - int totalPages = pageable.getPageSize() > 0 ? (int) Math.ceil((float) totalElements / pageable.getPageSize()) : 1; - boolean hasNext = pageable.getPageSize() > 0 && totalElements > pageable.getOffset() + rows.size(); - var data = rows.stream().map(row -> { - - UUID id = (UUID) row.get("id"); - long createdTime = (long) row.get("created_time"); - UUID tenantId = (UUID) row.get("tenant_id"); - EntityType entityType = EntityType.valueOf((String) row.get("entity_type")); - UUID entityId = (UUID) row.get("entity_id"); - UUID calculatedFieldId = (UUID) row.get("calculated_field_id"); - - CalculatedFieldLink calculatedFieldLink = new CalculatedFieldLink(); - calculatedFieldLink.setId(new CalculatedFieldLinkId(id)); - calculatedFieldLink.setCreatedTime(createdTime); - calculatedFieldLink.setTenantId(new TenantId(tenantId)); - calculatedFieldLink.setEntityId(EntityIdFactory.getByTypeAndUuid(entityType, entityId)); - calculatedFieldLink.setCalculatedFieldId(new CalculatedFieldId(calculatedFieldId)); - - return calculatedFieldLink; - }).collect(Collectors.toList()); - return new PageData<>(data, totalPages, totalElements, hasNext); - }); - } - } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/cf/JpaCalculatedFieldDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/cf/JpaCalculatedFieldDao.java index 2632b0237b..3cd9285d78 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/cf/JpaCalculatedFieldDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/cf/JpaCalculatedFieldDao.java @@ -22,6 +22,7 @@ import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.cf.CalculatedField; +import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.id.CalculatedFieldId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; @@ -34,6 +35,7 @@ import org.thingsboard.server.dao.sql.JpaAbstractDao; import org.thingsboard.server.dao.util.SqlDao; import java.util.List; +import java.util.Set; import java.util.UUID; @Slf4j @@ -66,8 +68,8 @@ public class JpaCalculatedFieldDao extends JpaAbstractDao findAllByEntityId(TenantId tenantId, EntityId entityId, PageLink pageLink) { - log.debug("Try to find calculated fields by entityId[{}] and pageLink [{}]", entityId, pageLink); - return DaoUtil.toPageData(calculatedFieldRepository.findAllByTenantIdAndEntityId(tenantId.getId(), entityId.getId(), DaoUtil.toPageable(pageLink))); + public PageData findByEntityIdAndTypes(TenantId tenantId, EntityId entityId, Set types, PageLink pageLink) { + log.debug("Try to find calculated fields by entityId [{}] and type [{}] and pageLink [{}]", entityId, types, pageLink); + return DaoUtil.toPageData(calculatedFieldRepository.findByTenantIdAndEntityIdAndTypes(tenantId.getId(), entityId.getId(), + types.stream().map(Enum::name).toList(), pageLink.getTextSearch(), DaoUtil.toPageable(pageLink))); } @Override @@ -95,8 +98,8 @@ public class JpaCalculatedFieldDao extends JpaAbstractDao implements CalculatedFieldLinkDao { - - private final CalculatedFieldLinkRepository calculatedFieldLinkRepository; - private final NativeCalculatedFieldRepository nativeCalculatedFieldRepository; - - @Override - public List findCalculatedFieldLinksByCalculatedFieldId(TenantId tenantId, CalculatedFieldId calculatedFieldId) { - return DaoUtil.convertDataList(calculatedFieldLinkRepository.findAllByTenantIdAndCalculatedFieldId(tenantId.getId(), calculatedFieldId.getId())); - } - - @Override - public List findCalculatedFieldLinksByEntityId(TenantId tenantId, EntityId entityId) { - return DaoUtil.convertDataList(calculatedFieldLinkRepository.findAllByTenantIdAndEntityId(tenantId.getId(), entityId.getId())); - } - - @Override - public List findCalculatedFieldLinksByTenantId(TenantId tenantId) { - return DaoUtil.convertDataList(calculatedFieldLinkRepository.findAllByTenantId(tenantId.getId())); - } - - @Override - public List findAll() { - return DaoUtil.convertDataList(calculatedFieldLinkRepository.findAll()); - } - - @Override - public PageData findAll(PageLink pageLink) { - log.debug("Try to find calculated field links by pageLink [{}]", pageLink); - return nativeCalculatedFieldRepository.findCalculatedFieldLinks(DaoUtil.toPageable(pageLink)); - } - - @Override - public PageData findAllByTenantId(TenantId tenantId, PageLink pageLink) { - log.debug("Try to find calculated field links by tenantId [{}], pageLink [{}]", tenantId, pageLink); - return DaoUtil.toPageData(calculatedFieldLinkRepository.findAllByTenantId(tenantId.getId(), DaoUtil.toPageable(pageLink))); - } - - @Override - protected Class getEntityClass() { - return CalculatedFieldLinkEntity.class; - } - - @Override - protected JpaRepository getRepository() { - return calculatedFieldLinkRepository; - } - - @Override - public EntityType getEntityType() { - return EntityType.CALCULATED_FIELD_LINK; - } - -} diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/cf/NativeCalculatedFieldRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/cf/NativeCalculatedFieldRepository.java index f37a5764a0..7cc3507076 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/cf/NativeCalculatedFieldRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/cf/NativeCalculatedFieldRepository.java @@ -17,13 +17,10 @@ package org.thingsboard.server.dao.sql.cf; import org.springframework.data.domain.Pageable; import org.thingsboard.server.common.data.cf.CalculatedField; -import org.thingsboard.server.common.data.cf.CalculatedFieldLink; import org.thingsboard.server.common.data.page.PageData; public interface NativeCalculatedFieldRepository { PageData findCalculatedFields(Pageable pageable); - PageData findCalculatedFieldLinks(Pageable pageable); - } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/customer/CustomerRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/customer/CustomerRepository.java index 8ad7311423..9c196a5072 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/customer/CustomerRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/customer/CustomerRepository.java @@ -21,6 +21,7 @@ import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; +import org.thingsboard.server.common.data.EntityInfo; import org.thingsboard.server.common.data.edqs.fields.CustomerFields; import org.thingsboard.server.dao.ExportableEntityRepository; import org.thingsboard.server.dao.model.sql.CustomerEntity; @@ -41,6 +42,10 @@ public interface CustomerRepository extends JpaRepository, CustomerEntity findByTenantIdAndTitle(UUID tenantId, String title); + @Query("SELECT new org.thingsboard.server.common.data.EntityInfo(a.id, 'CUSTOMER', a.title) " + + "FROM CustomerEntity a WHERE a.tenantId = :tenantId AND a.title LIKE CONCAT(:prefix, '%')") + List findEntityInfosByNamePrefix(UUID tenantId, String prefix); + @Query(value = "SELECT * FROM customer c WHERE c.tenant_id = :tenantId " + "AND c.is_public IS TRUE ORDER BY c.id ASC LIMIT 1", nativeQuery = true) CustomerEntity findPublicCustomerByTenantId(@Param("tenantId") UUID tenantId); diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/customer/JpaCustomerDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/customer/JpaCustomerDao.java index 75e7179391..2e1d75a738 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/customer/JpaCustomerDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/customer/JpaCustomerDao.java @@ -20,6 +20,7 @@ import org.springframework.data.domain.Limit; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.Customer; +import org.thingsboard.server.common.data.EntityInfo; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.edqs.fields.CustomerFields; import org.thingsboard.server.common.data.id.CustomerId; @@ -117,6 +118,11 @@ public class JpaCustomerDao extends JpaAbstractDao imp return customerRepository.findNextBatch(id, Limit.of(batchSize)); } + @Override + public List findEntityInfosByNamePrefix(TenantId tenantId, String name) { + return customerRepository.findEntityInfosByNamePrefix(tenantId.getId(), name); + } + @Override public EntityType getEntityType() { return EntityType.CUSTOMER; diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/DashboardInfoRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/DashboardInfoRepository.java index 7624ddc738..32ac596562 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/DashboardInfoRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/DashboardInfoRepository.java @@ -20,6 +20,7 @@ import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; +import org.thingsboard.server.common.data.EntityInfo; import org.thingsboard.server.dao.model.sql.DashboardInfoEntity; import java.util.List; @@ -87,12 +88,15 @@ public interface DashboardInfoRepository extends JpaRepository findByImageLink(@Param("imageLink") String imageLink, @Param("limit") int limit); - @Query(value = "SELECT * FROM dashboard d WHERE d.tenant_id = :tenantId and d.configuration ILIKE CONCAT('%', :link, '%') limit :limit", - nativeQuery = true) - List findDashboardInfosByTenantIdAndResourceLink(@Param("tenantId") UUID tenantId, @Param("link") String link, @Param("limit") int limit); + @Query("SELECT new org.thingsboard.server.common.data.EntityInfo(d.id, 'DASHBOARD', d.title) " + + "FROM DashboardEntity d WHERE d.tenantId = :tenantId AND ilike(cast(d.configuration as string), CONCAT('%', :link, '%')) = true") + List findDashboardInfosByTenantIdAndResourceLink(@Param("tenantId") UUID tenantId, + @Param("link") String link, + Pageable pageable); - @Query(value = "SELECT * FROM dashboard d WHERE d.configuration ILIKE CONCAT('%', :link, '%') limit :limit", - nativeQuery = true) - List findDashboardInfosByResourceLink(@Param("link") String link, @Param("limit") int limit); + @Query("SELECT new org.thingsboard.server.common.data.EntityInfo(d.id, 'DASHBOARD', d.title) " + + "FROM DashboardEntity d WHERE ilike(cast(d.configuration as string), CONCAT('%', :link, '%')) = true") + List findDashboardInfosByResourceLink(@Param("link") String link, + Pageable pageable); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/JpaDashboardInfoDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/JpaDashboardInfoDao.java index bc07139725..0e04c94a46 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/JpaDashboardInfoDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/JpaDashboardInfoDao.java @@ -17,9 +17,11 @@ package org.thingsboard.server.dao.sql.dashboard; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.PageRequest; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.DashboardInfo; +import org.thingsboard.server.common.data.EntityInfo; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; @@ -135,13 +137,13 @@ public class JpaDashboardInfoDao extends JpaAbstractDao findByTenantIdAndResourceLink(TenantId tenantId, String url, int limit) { - return DaoUtil.convertDataList(dashboardInfoRepository.findDashboardInfosByTenantIdAndResourceLink(tenantId.getId(), url, limit)); + public List findByTenantIdAndResource(TenantId tenantId, String reference, int limit) { + return dashboardInfoRepository.findDashboardInfosByTenantIdAndResourceLink(tenantId.getId(), reference, PageRequest.of(0, limit)); } @Override - public List findByResourceLink(String link, int limit) { - return DaoUtil.convertDataList(dashboardInfoRepository.findDashboardInfosByResourceLink(link, limit)); + public List findByResource(String reference, int limit) { + return dashboardInfoRepository.findDashboardInfosByResourceLink(reference, PageRequest.of(0, limit)); } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/device/DefaultNativeAssetRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/device/DefaultNativeAssetRepository.java index ec47da3499..3057b0d30f 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/device/DefaultNativeAssetRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/device/DefaultNativeAssetRepository.java @@ -23,9 +23,12 @@ import org.springframework.transaction.support.TransactionTemplate; import org.thingsboard.server.common.data.ProfileEntityIdInfo; import org.thingsboard.server.common.data.id.AssetId; import org.thingsboard.server.common.data.id.AssetProfileId; +import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; +import java.util.Map; import java.util.UUID; @Repository @@ -40,23 +43,24 @@ public class DefaultNativeAssetRepository extends AbstractNativeRepository imple @Override public PageData findProfileEntityIdInfos(Pageable pageable) { - String PROFILE_ASSET_ID_INFO_QUERY = "SELECT tenant_id as tenantId, asset_profile_id as profileId, id as id FROM asset ORDER BY created_time ASC LIMIT %s OFFSET %s"; - return find(COUNT_QUERY, PROFILE_ASSET_ID_INFO_QUERY, pageable, row -> { - AssetId id = new AssetId((UUID) row.get("id")); - AssetProfileId profileId = new AssetProfileId((UUID) row.get("profileId")); - var tenantIdObj = row.get("tenantId"); - return ProfileEntityIdInfo.create(tenantIdObj != null ? (UUID) tenantIdObj : TenantId.SYS_TENANT_ID.getId(), profileId, id); - }); + String PROFILE_ASSET_ID_INFO_QUERY = "SELECT tenant_id as tenantId, customer_id as customerId, asset_profile_id as profileId, id as id FROM asset ORDER BY created_time ASC LIMIT %s OFFSET %s"; + return find(COUNT_QUERY, PROFILE_ASSET_ID_INFO_QUERY, pageable, DefaultNativeAssetRepository::toInfo); } @Override public PageData findProfileEntityIdInfosByTenantId(UUID tenantId, Pageable pageable) { - String PROFILE_ASSET_ID_INFO_QUERY = String.format("SELECT tenant_id as tenantId, asset_profile_id as profileId, id as id FROM asset WHERE tenant_id = '%s' ORDER BY created_time ASC LIMIT %%s OFFSET %%s", tenantId); - return find(COUNT_QUERY, PROFILE_ASSET_ID_INFO_QUERY, pageable, row -> { - AssetId id = new AssetId((UUID) row.get("id")); - AssetProfileId profileId = new AssetProfileId((UUID) row.get("profileId")); - var tenantIdObj = row.get("tenantId"); - return ProfileEntityIdInfo.create(tenantIdObj != null ? (UUID) tenantIdObj : TenantId.SYS_TENANT_ID.getId(), profileId, id); - }); + String PROFILE_ASSET_ID_INFO_QUERY = String.format("SELECT tenant_id as tenantId, customer_id as customerId, asset_profile_id as profileId, id as id FROM asset WHERE tenant_id = '%s' ORDER BY created_time ASC LIMIT %%s OFFSET %%s", tenantId); + return find(COUNT_QUERY, PROFILE_ASSET_ID_INFO_QUERY, pageable, DefaultNativeAssetRepository::toInfo); } + + private static ProfileEntityIdInfo toInfo(Map row) { + var tenantIdObj = row.get("tenantId"); + UUID tenantId = tenantIdObj != null ? (UUID) tenantIdObj : TenantId.SYS_TENANT_ID.getId(); + AssetId id = new AssetId((UUID) row.get("id")); + CustomerId customerId = new CustomerId((UUID) row.get("customerId")); + EntityId ownerId = !customerId.isNullUid() ? customerId : TenantId.fromUUID(tenantId); + AssetProfileId profileId = new AssetProfileId((UUID) row.get("profileId")); + return ProfileEntityIdInfo.create(tenantId, ownerId, profileId, id); + } + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/device/DefaultNativeDeviceRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/device/DefaultNativeDeviceRepository.java index 78ee2795b0..49062f829f 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/device/DefaultNativeDeviceRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/device/DefaultNativeDeviceRepository.java @@ -22,11 +22,14 @@ import org.springframework.stereotype.Repository; import org.springframework.transaction.support.TransactionTemplate; import org.thingsboard.server.common.data.DeviceIdInfo; import org.thingsboard.server.common.data.ProfileEntityIdInfo; +import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.DeviceProfileId; +import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; +import java.util.Map; import java.util.UUID; @Repository @@ -52,24 +55,24 @@ public class DefaultNativeDeviceRepository extends AbstractNativeRepository impl @Override public PageData findProfileEntityIdInfos(Pageable pageable) { - String PROFILE_DEVICE_ID_INFO_QUERY = "SELECT tenant_id as tenantId, device_profile_id as profileId, id as id FROM device ORDER BY created_time ASC LIMIT %s OFFSET %s"; - return find(COUNT_QUERY, PROFILE_DEVICE_ID_INFO_QUERY, pageable, row -> { - DeviceId id = new DeviceId((UUID) row.get("id")); - DeviceProfileId profileId = new DeviceProfileId((UUID) row.get("profileId")); - var tenantIdObj = row.get("tenantId"); - return ProfileEntityIdInfo.create(tenantIdObj != null ? (UUID) tenantIdObj : TenantId.SYS_TENANT_ID.getId(), profileId, id); - }); + String PROFILE_DEVICE_ID_INFO_QUERY = "SELECT tenant_id as tenantId, customer_id as customerId, device_profile_id as profileId, id as id FROM device ORDER BY created_time ASC LIMIT %s OFFSET %s"; + return find(COUNT_QUERY, PROFILE_DEVICE_ID_INFO_QUERY, pageable, DefaultNativeDeviceRepository::toInfo); } @Override public PageData findProfileEntityIdInfosByTenantId(UUID tenantId, Pageable pageable) { - String PROFILE_DEVICE_ID_INFO_QUERY = String.format("SELECT tenant_id as tenantId, device_profile_id as profileId, id as id FROM device WHERE tenant_id = '%s' ORDER BY created_time ASC LIMIT %%s OFFSET %%s", tenantId); - return find(COUNT_QUERY, PROFILE_DEVICE_ID_INFO_QUERY, pageable, row -> { - DeviceId id = new DeviceId((UUID) row.get("id")); - DeviceProfileId profileId = new DeviceProfileId((UUID) row.get("profileId")); - var tenantIdObj = row.get("tenantId"); - return ProfileEntityIdInfo.create(tenantIdObj != null ? (UUID) tenantIdObj : TenantId.SYS_TENANT_ID.getId(), profileId, id); - }); + String PROFILE_DEVICE_ID_INFO_QUERY = String.format("SELECT tenant_id as tenantId, customer_id as customerId, device_profile_id as profileId, id as id FROM device WHERE tenant_id = '%s' ORDER BY created_time ASC LIMIT %%s OFFSET %%s", tenantId); + return find(COUNT_QUERY, PROFILE_DEVICE_ID_INFO_QUERY, pageable, DefaultNativeDeviceRepository::toInfo); + } + + private static ProfileEntityIdInfo toInfo(Map row) { + var tenantIdObj = row.get("tenantId"); + UUID tenantId = tenantIdObj != null ? (UUID) tenantIdObj : TenantId.SYS_TENANT_ID.getId(); + DeviceId id = new DeviceId((UUID) row.get("id")); + CustomerId customerId = new CustomerId((UUID) row.get("customerId")); + EntityId ownerId = !customerId.isNullUid() ? customerId : TenantId.fromUUID(tenantId); + DeviceProfileId profileId = new DeviceProfileId((UUID) row.get("profileId")); + return ProfileEntityIdInfo.create(tenantId, ownerId, profileId, id); } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/device/DeviceRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/device/DeviceRepository.java index f4c2fed9fa..a9ec9d1d36 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/device/DeviceRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/device/DeviceRepository.java @@ -22,6 +22,7 @@ import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; import org.thingsboard.server.common.data.DeviceTransportType; +import org.thingsboard.server.common.data.EntityInfo; import org.thingsboard.server.common.data.edqs.fields.DeviceFields; import org.thingsboard.server.dao.ExportableEntityRepository; import org.thingsboard.server.dao.model.sql.DeviceEntity; @@ -151,6 +152,10 @@ public interface DeviceRepository extends JpaRepository, Exp DeviceEntity findByTenantIdAndName(UUID tenantId, String name); + @Query("SELECT new org.thingsboard.server.common.data.EntityInfo(a.id, 'DEVICE', a.name) " + + "FROM DeviceEntity a WHERE a.tenantId = :tenantId AND a.name LIKE CONCAT(:prefix, '%')") + List findEntityInfosByNamePrefix(UUID tenantId, String prefix); + List findDevicesByTenantIdAndCustomerIdAndIdIn(UUID tenantId, UUID customerId, List deviceIds); List findDevicesByTenantIdAndIdIn(UUID tenantId, List deviceIds); diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceDao.java index 9c79637e39..beb3b7c913 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceDao.java @@ -28,6 +28,7 @@ import org.thingsboard.server.common.data.DeviceIdInfo; import org.thingsboard.server.common.data.DeviceInfo; import org.thingsboard.server.common.data.DeviceInfoFilter; import org.thingsboard.server.common.data.DeviceTransportType; +import org.thingsboard.server.common.data.EntityInfo; import org.thingsboard.server.common.data.EntitySubtype; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.ProfileEntityIdInfo; @@ -114,6 +115,11 @@ public class JpaDeviceDao extends JpaAbstractDao implement DaoUtil.toPageable(pageLink))); } + @Override + public List findEntityInfosByNamePrefix(TenantId tenantId, String name) { + return deviceRepository.findEntityInfosByNamePrefix(tenantId.getId(), name); + } + @Override public ListenableFuture> findDevicesByTenantIdAndIdsAsync(UUID tenantId, List deviceIds) { return service.submit(() -> DaoUtil.convertDataList(deviceRepository.findDevicesByTenantIdAndIdIn(tenantId, deviceIds))); diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/entityview/EntityViewRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/entityview/EntityViewRepository.java index 6094e9b171..9d51d024b8 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/entityview/EntityViewRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/entityview/EntityViewRepository.java @@ -21,6 +21,7 @@ import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; +import org.thingsboard.server.common.data.EntityInfo; import org.thingsboard.server.common.data.edqs.fields.EntityViewFields; import org.thingsboard.server.dao.ExportableEntityRepository; import org.thingsboard.server.dao.model.sql.EntityViewEntity; @@ -118,6 +119,10 @@ public interface EntityViewRepository extends JpaRepository findEntityInfosByNamePrefix(UUID tenantId, String prefix); + List findAllByTenantIdAndEntityId(UUID tenantId, UUID entityId); boolean existsByTenantIdAndEntityId(UUID tenantId, UUID entityId); diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/entityview/JpaEntityViewDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/entityview/JpaEntityViewDao.java index 44d8a09ff4..27400961ef 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/entityview/JpaEntityViewDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/entityview/JpaEntityViewDao.java @@ -21,6 +21,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Limit; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.EntityInfo; import org.thingsboard.server.common.data.EntitySubtype; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityView; @@ -230,6 +231,11 @@ public class JpaEntityViewDao extends JpaAbstractDao findEntityInfosByNamePrefix(TenantId tenantId, String name) { + return entityViewRepository.findEntityInfosByNamePrefix(tenantId.getId(), name); + } + @Override public EntityType getEntityType() { return EntityType.ENTITY_VIEW; diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/query/AlarmDataAdapter.java b/dao/src/main/java/org/thingsboard/server/dao/sql/query/AlarmDataAdapter.java index b250fc7337..f3e2ba83a6 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/query/AlarmDataAdapter.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/query/AlarmDataAdapter.java @@ -121,6 +121,7 @@ public class AlarmDataAdapter { AlarmData alarmData = new AlarmData(alarm, entityId); alarmData.setOriginatorName(originatorName); alarmData.setOriginatorLabel(originatorLabel); + alarmData.setOriginatorDisplayName(StringUtils.isBlank(originatorLabel) ? originatorName : originatorLabel); if (alarm.getAssigneeId() != null) { alarmData.setAssignee(new AlarmAssignee(alarm.getAssigneeId(), assigneeFirstName, assigneeLastName, assigneeEmail)); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/query/EntityKeyMapping.java b/dao/src/main/java/org/thingsboard/server/dao/sql/query/EntityKeyMapping.java index 9755201fe9..34bc719639 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/query/EntityKeyMapping.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/query/EntityKeyMapping.java @@ -65,6 +65,7 @@ public class EntityKeyMapping { public static final String NAME = "name"; public static final String TYPE = "type"; public static final String LABEL = "label"; + public static final String DISPLAY_NAME = "displayName"; public static final String FIRST_NAME = "firstName"; public static final String LAST_NAME = "lastName"; public static final String EMAIL = "email"; @@ -83,6 +84,8 @@ public class EntityKeyMapping { public static final String SERVICE_ID = "serviceId"; public static final String OWNER_NAME = "ownerName"; public static final String OWNER_TYPE = "ownerType"; + public static final String LABELED_ENTITY_DISPLAY_NAME_SELECT_QUERY = "COALESCE(NULLIF(TRIM(e." + LABEL + "), ''), e." + NAME + ")"; + public static final String USER_DISPLAY_NAME_SELECT_QUERY = "COALESCE(NULLIF(TRIM(CONCAT_WS(' ', e.first_name, e.last_name)), ''), e.email)"; public static final String OWNER_NAME_SELECT_QUERY = "case when e.customer_id = '" + NULL_UUID + "' " + "then (select title from tenant where id = e.tenant_id) " + "else (select title from customer where id = e.customer_id) end"; @@ -94,6 +97,16 @@ public class EntityKeyMapping { OWNER_NAME, OWNER_NAME_SELECT_QUERY, OWNER_TYPE, OWNER_TYPE_SELECT_QUERY ); + public static final Map labeledPropertiesFunctions = Map.of( + OWNER_NAME, OWNER_NAME_SELECT_QUERY, + OWNER_TYPE, OWNER_TYPE_SELECT_QUERY, + DISPLAY_NAME, LABELED_ENTITY_DISPLAY_NAME_SELECT_QUERY + ); + public static final Map userPropertiesFunctions = Map.of( + OWNER_NAME, OWNER_NAME_SELECT_QUERY, + OWNER_TYPE, OWNER_TYPE_SELECT_QUERY, + DISPLAY_NAME, USER_DISPLAY_NAME_SELECT_QUERY + ); public static final Map queueStatsPropertiesFunctions = Map.of(NAME, QUEUE_STATS_NAME_QUERY); public static final List typedEntityFields = Arrays.asList(CREATED_TIME, ENTITY_TYPE, NAME, TYPE, ADDITIONAL_INFO); @@ -153,20 +166,24 @@ public class EntityKeyMapping { Map contactBasedAliases = new HashMap<>(); contactBasedAliases.put(NAME, TITLE); contactBasedAliases.put(LABEL, TITLE); + contactBasedAliases.put(DISPLAY_NAME, TITLE); aliases.put(EntityType.TENANT, contactBasedAliases); aliases.put(EntityType.CUSTOMER, contactBasedAliases); aliases.put(EntityType.DASHBOARD, contactBasedAliases); + Map deviceAndAssetAliases = new HashMap<>(); + deviceAndAssetAliases.put(TITLE, NAME); + aliases.put(EntityType.DEVICE, deviceAndAssetAliases); + aliases.put(EntityType.ASSET, deviceAndAssetAliases); Map commonEntityAliases = new HashMap<>(); commonEntityAliases.put(TITLE, NAME); - aliases.put(EntityType.DEVICE, commonEntityAliases); - aliases.put(EntityType.ASSET, commonEntityAliases); + commonEntityAliases.put(DISPLAY_NAME, NAME); aliases.put(EntityType.ENTITY_VIEW, commonEntityAliases); aliases.put(EntityType.WIDGETS_BUNDLE, commonEntityAliases); - propertiesFunctions.put(EntityType.DEVICE, ownerPropertiesFunctions); - propertiesFunctions.put(EntityType.ASSET, ownerPropertiesFunctions); + propertiesFunctions.put(EntityType.DEVICE, labeledPropertiesFunctions); + propertiesFunctions.put(EntityType.ASSET, labeledPropertiesFunctions); propertiesFunctions.put(EntityType.ENTITY_VIEW, ownerPropertiesFunctions); - propertiesFunctions.put(EntityType.USER, ownerPropertiesFunctions); + propertiesFunctions.put(EntityType.USER, userPropertiesFunctions); propertiesFunctions.put(EntityType.DASHBOARD, ownerPropertiesFunctions); propertiesFunctions.put(EntityType.QUEUE_STATS, queueStatsPropertiesFunctions); diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/relation/JpaRelationDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/relation/JpaRelationDao.java index 7417418f54..3ab382d2a4 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/relation/JpaRelationDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/relation/JpaRelationDao.java @@ -25,6 +25,9 @@ import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityIdFactory; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.relation.EntityRelation; +import org.thingsboard.server.common.data.relation.EntityRelationPathQuery; +import org.thingsboard.server.common.data.relation.EntitySearchDirection; +import org.thingsboard.server.common.data.relation.RelationPathLevel; import org.thingsboard.server.common.data.relation.RelationTypeGroup; import org.thingsboard.server.common.data.rule.RuleChainType; import org.thingsboard.server.dao.DaoUtil; @@ -43,6 +46,7 @@ import java.util.stream.Collectors; import static org.thingsboard.server.dao.model.ModelConstants.RELATION_FROM_ID_PROPERTY; import static org.thingsboard.server.dao.model.ModelConstants.RELATION_FROM_TYPE_PROPERTY; +import static org.thingsboard.server.dao.model.ModelConstants.RELATION_TABLE_NAME; import static org.thingsboard.server.dao.model.ModelConstants.RELATION_TO_ID_PROPERTY; import static org.thingsboard.server.dao.model.ModelConstants.RELATION_TO_TYPE_PROPERTY; import static org.thingsboard.server.dao.model.ModelConstants.RELATION_TYPE_GROUP_PROPERTY; @@ -293,4 +297,107 @@ public class JpaRelationDao extends JpaAbstractDaoListeningExecutorService imple public List findRuleNodeToRuleChainRelations(RuleChainType ruleChainType, int limit) { return DaoUtil.convertDataList(relationRepository.findRuleNodeToRuleChainRelations(ruleChainType, PageRequest.of(0, limit))); } + + @Override + public List findByRelationPathQuery(TenantId tenantId, EntityRelationPathQuery query, int limit) { + List levels = query.levels(); + if (levels == null || levels.isEmpty()) { + return List.of(); + } + if (limit <= 0) { + return List.of(); + } + String sql = buildRelationPathSql(query); + Object[] params = buildRelationPathParams(query, limit); + + log.trace("[{}] relation path query: {}", tenantId, sql); + + return jdbcTemplate.queryForList(sql, params).stream() + .map(row -> { + var entityRelation = new EntityRelation(); + var fromId = (UUID) row.get(RELATION_FROM_ID_PROPERTY); + var fromType = (String) row.get(RELATION_FROM_TYPE_PROPERTY); + var toId = (UUID) row.get(RELATION_TO_ID_PROPERTY); + var toType = (String) row.get(RELATION_TO_TYPE_PROPERTY); + var grp = (String) row.get(RELATION_TYPE_GROUP_PROPERTY); + var type = (String) row.get(RELATION_TYPE_PROPERTY); + var version = (Long) row.get(VERSION_COLUMN); + + entityRelation.setFrom(EntityIdFactory.getByTypeAndUuid(fromType, fromId)); + entityRelation.setTo(EntityIdFactory.getByTypeAndUuid(toType, toId)); + entityRelation.setType(type); + entityRelation.setTypeGroup(RelationTypeGroup.valueOf(grp)); + entityRelation.setVersion(version); + return entityRelation; + }) + .collect(Collectors.toList()); + } + + private Object[] buildRelationPathParams(EntityRelationPathQuery query, int limit) { + final List params = new ArrayList<>(); + // seed + params.add(query.rootEntityId().getId()); + params.add(query.rootEntityId().getEntityType().name()); + + // levels + for (var lvl : query.levels()) { + params.add(lvl.relationType()); + } + + // limit + params.add(limit); + + return params.toArray(); + } + + private static String buildRelationPathSql(EntityRelationPathQuery query) { + List levels = query.levels(); + StringBuilder sb = new StringBuilder(); + + sb.append("WITH seed AS (\n") + .append(" SELECT ?::uuid AS id, ?::varchar AS type\n") + .append(")"); + + String prev = "seed"; + for (int i = 0; i < levels.size() - 1; i++) { + RelationPathLevel lvl = levels.get(i); + boolean down = lvl.direction() == EntitySearchDirection.FROM; + + String cur = "lvl" + (i + 1); + String joinCond = down + ? "r.from_id = p.id AND r.from_type = p.type" + : "r.to_id = p.id AND r.to_type = p.type"; + String selectNext = down + ? "r.to_id AS id, r.to_type AS type" + : "r.from_id AS id, r.from_type AS type"; + + sb.append(",\n").append(cur).append(" AS (\n") + .append(" SELECT ").append(selectNext).append("\n") + .append(" FROM ").append(RELATION_TABLE_NAME).append(" r\n") + .append(" JOIN ").append(prev).append(" p ON ").append(joinCond).append("\n") + .append(" WHERE r.relation_type_group = '").append(RelationTypeGroup.COMMON).append("'\n") + .append(" AND r.relation_type = ?\n") + .append(")"); + prev = cur; + } + + RelationPathLevel last = levels.get(levels.size() - 1); + boolean lastDown = last.direction() == EntitySearchDirection.FROM; + String prevForLast = (levels.size() == 1) ? "seed" : prev; + String lastJoin = lastDown + ? "r.from_id = p.id AND r.from_type = p.type" + : "r.to_id = p.id AND r.to_type = p.type"; + + sb.append("\n") + .append("SELECT r.from_id, r.from_type, r.to_id, r.to_type,\n") + .append(" r.relation_type_group, r.relation_type, r.version\n") + .append("FROM ").append(RELATION_TABLE_NAME).append(" r\n") + .append("JOIN ").append(prevForLast).append(" p ON ").append(lastJoin).append("\n") + .append("WHERE r.relation_type_group = '").append(RelationTypeGroup.COMMON).append("'\n") + .append(" AND r.relation_type = ?\n") + .append("LIMIT ?"); + + return sb.toString(); + } + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/relation/RelationRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/relation/RelationRepository.java index b4e8a21372..4b879f9d95 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/relation/RelationRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/relation/RelationRepository.java @@ -15,7 +15,6 @@ */ package org.thingsboard.server.dao.sql.relation; -import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaSpecificationExecutor; @@ -96,4 +95,5 @@ public interface RelationRepository @Param("toId") UUID toId, @Param("toType") String toType, @Param("batchSize") int batchSize); + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/resource/JpaTbResourceDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/resource/JpaTbResourceDao.java index 6cce9d76c2..48e41c8553 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/resource/JpaTbResourceDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/resource/JpaTbResourceDao.java @@ -22,6 +22,7 @@ import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.ResourceSubType; import org.thingsboard.server.common.data.ResourceType; import org.thingsboard.server.common.data.TbResource; +import org.thingsboard.server.common.data.TbResourceDataInfo; import org.thingsboard.server.common.data.id.TbResourceId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; @@ -115,6 +116,11 @@ public class JpaTbResourceDao extends JpaAbstractDao findSystemOrTenantResourcesByIds(TenantId tenantId, List resourceIds) { + return DaoUtil.convertDataList(resourceInfoRepository.findSystemOrTenantResourcesByIdIn(tenantId.getId(), TenantId.NULL_UUID, toUUIDs(resourceIds))); + } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/resource/TbResourceInfoRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/resource/TbResourceInfoRepository.java index 6eea20a287..97b1e56527 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/resource/TbResourceInfoRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/resource/TbResourceInfoRepository.java @@ -79,4 +79,10 @@ public interface TbResourceInfoRepository extends JpaRepository findSystemOrTenantResourcesByIdIn(@Param("tenantId") UUID tenantId, + @Param("systemTenantId") UUID systemTenantId, + @Param("resourceIds") List resourceIds); + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/resource/TbResourceRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/resource/TbResourceRepository.java index 1c642d2069..4aa699174f 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/resource/TbResourceRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/resource/TbResourceRepository.java @@ -20,6 +20,7 @@ import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; +import org.thingsboard.server.common.data.TbResourceDataInfo; import org.thingsboard.server.dao.ExportableEntityRepository; import org.thingsboard.server.dao.model.sql.TbResourceEntity; @@ -101,4 +102,6 @@ public interface TbResourceRepository extends JpaRepository findIdsByTenantId(@Param("tenantId") UUID tenantId, Pageable pageable); + @Query("SELECT new org.thingsboard.server.common.data.TbResourceDataInfo(r.data, r.descriptor) FROM TbResourceEntity r WHERE r.id = :id") + TbResourceDataInfo getDataInfoById(UUID id); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/rule/JpaRuleChainDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/rule/JpaRuleChainDao.java index 77044d41dc..4a6427a7e5 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/rule/JpaRuleChainDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/rule/JpaRuleChainDao.java @@ -18,8 +18,10 @@ package org.thingsboard.server.dao.sql.rule; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Limit; +import org.springframework.data.domain.PageRequest; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.EntityInfo; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.edqs.fields.RuleChainFields; import org.thingsboard.server.common.data.id.RuleChainId; @@ -141,6 +143,16 @@ public class JpaRuleChainDao extends JpaAbstractDao return findRuleChainsByTenantId(tenantId.getId(), pageLink); } + @Override + public List findByTenantIdAndResource(TenantId tenantId, String reference, int limit) { + return ruleChainRepository.findRuleChainsByTenantIdAndResource(tenantId.getId(), reference, PageRequest.of(0, limit)); + } + + @Override + public List findByResource(String reference, int limit) { + return ruleChainRepository.findRuleChainsByResource(reference, PageRequest.of(0, limit)); + } + @Override public List findNextBatch(UUID id, int batchSize) { return ruleChainRepository.findNextBatch(id, Limit.of(batchSize)); diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/rule/RuleChainRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/rule/RuleChainRepository.java index cfa06caf14..4bf648cbbd 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/rule/RuleChainRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/rule/RuleChainRepository.java @@ -17,10 +17,12 @@ package org.thingsboard.server.dao.sql.rule; import org.springframework.data.domain.Limit; import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; +import org.thingsboard.server.common.data.EntityInfo; import org.thingsboard.server.common.data.edqs.fields.RuleChainFields; import org.thingsboard.server.common.data.rule.RuleChainType; import org.thingsboard.server.dao.ExportableEntityRepository; @@ -72,6 +74,19 @@ public interface RuleChainRepository extends JpaRepository findRuleChainsByTenantIdAndResource(@Param("tenantId") UUID tenantId, + @Param("resourceId") String resourceId, + PageRequest of); + + @Query("SELECT new org.thingsboard.server.common.data.EntityInfo(rc.id, 'RULE_CHAIN', rc.name) " + + "FROM RuleChainEntity rc WHERE EXISTS " + + "(SELECT 1 FROM RuleNodeEntity rn WHERE rn.ruleChainId = rc.id AND cast(rn.configuration as string) LIKE CONCAT('%', :resourceId, '%'))") + List findRuleChainsByResource(@Param("resourceId") String resourceId, + Pageable pageable); + @Query("SELECT new org.thingsboard.server.common.data.edqs.fields.RuleChainFields(r.id, r.createdTime, r.tenantId," + "r.name, r.version, r.additionalInfo) FROM RuleChainEntity r WHERE r.id > :id ORDER BY r.id") List findNextBatch(@Param("id") UUID id, Limit limit); diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/user/JpaUserDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/user/JpaUserDao.java index 35d15bab51..753955089c 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/user/JpaUserDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/user/JpaUserDao.java @@ -136,6 +136,11 @@ public class JpaUserDao extends JpaAbstractDao implements User DaoUtil.toPageable(pageLink))); } + @Override + public int countTenantAdmins(UUID tenantId) { + return userRepository.countByTenantIdAndAuthority(tenantId, Authority.TENANT_ADMIN); + } + @Override public Long countByTenantId(TenantId tenantId) { return userRepository.countByTenantId(tenantId.getId()); diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/user/UserRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/user/UserRepository.java index 0a30a859c6..2254377af3 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/user/UserRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/user/UserRepository.java @@ -78,4 +78,6 @@ public interface UserRepository extends JpaRepository { "u.customerId, u.version, u.firstName, u.lastName, u.email, u.phone, u.additionalInfo) " + "FROM UserEntity u WHERE u.id > :id ORDER BY u.id") List findNextBatch(@Param("id") UUID id, Limit limit); + + int countByTenantIdAndAuthority(UUID tenantId, Authority authority); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/widget/JpaWidgetTypeDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/widget/JpaWidgetTypeDao.java index c728f5d006..18fb544dcb 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/widget/JpaWidgetTypeDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/widget/JpaWidgetTypeDao.java @@ -17,8 +17,10 @@ package org.thingsboard.server.dao.sql.widget; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Limit; +import org.springframework.data.domain.PageRequest; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.EntityInfo; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.edqs.fields.WidgetTypeFields; import org.thingsboard.server.common.data.id.TenantId; @@ -269,13 +271,13 @@ public class JpaWidgetTypeDao extends JpaAbstractDao findByTenantIdAndResourceLink(TenantId tenantId, String link, int limit) { - return DaoUtil.convertDataList(widgetTypeInfoRepository.findWidgetTypeInfosByTenantIdAndResourceLink(tenantId.getId(), link, limit)); + public List findByTenantIdAndResource(TenantId tenantId, String reference, int limit) { + return widgetTypeInfoRepository.findWidgetTypeInfosByTenantIdAndResourceLink(tenantId.getId(), reference, PageRequest.of(0, limit)); } @Override - public List findByResourceLink(String link, int limit) { - return DaoUtil.convertDataList(widgetTypeInfoRepository.findWidgetTypeInfosByResourceLink(link, limit)); + public List findByResource(String reference, int limit) { + return widgetTypeInfoRepository.findWidgetTypeInfosByResourceLink(reference, PageRequest.of(0, limit)); } @Override diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/widget/WidgetTypeInfoRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/widget/WidgetTypeInfoRepository.java index dc79280bcf..b97b42a6b9 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/widget/WidgetTypeInfoRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/widget/WidgetTypeInfoRepository.java @@ -20,6 +20,7 @@ import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; +import org.thingsboard.server.common.data.EntityInfo; import org.thingsboard.server.dao.model.sql.WidgetTypeInfoEntity; import java.util.List; @@ -214,10 +215,14 @@ public interface WidgetTypeInfoRepository extends JpaRepository findByImageUrl(@Param("imageLink") String imageLink, @Param("limit") int limit); - @Query(value = "SELECT * FROM widget_type_info_view w WHERE w.tenant_id = :tenantId AND w.descriptor ILIKE CONCAT('%', :link, '%') LIMIT :limit ", nativeQuery = true) - List findWidgetTypeInfosByTenantIdAndResourceLink(@Param("tenantId") UUID tenantId, @Param("link") String link, @Param("limit") int limit); - - @Query(value = "SELECT * FROM widget_type_info_view w WHERE w.descriptor ILIKE CONCAT('%', :link, '%') LIMIT :limit ", nativeQuery = true) - List findWidgetTypeInfosByResourceLink(@Param("link") String link, @Param("limit") int limit); + @Query("SELECT new org.thingsboard.server.common.data.EntityInfo(w.id, 'WIDGET_TYPE', w.name) " + + "FROM WidgetTypeEntity w WHERE w.tenantId = :tenantId AND ilike(cast(w.descriptor as string), CONCAT('%', :link, '%')) = true") + List findWidgetTypeInfosByTenantIdAndResourceLink(@Param("tenantId") UUID tenantId, + @Param("link") String link, + Pageable pageable); + @Query("SELECT new org.thingsboard.server.common.data.EntityInfo(w.id, 'WIDGET_TYPE', w.name) " + + "FROM WidgetTypeEntity w WHERE ilike(cast(w.descriptor as string), CONCAT('%', :link, '%')) = true") + List findWidgetTypeInfosByResourceLink(@Param("link") String link, + Pageable pageable); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantProfileServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantProfileServiceImpl.java index e165d6cfe6..b9172306bb 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantProfileServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantProfileServiceImpl.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.dao.tenant; +import com.google.common.util.concurrent.FluentFuture; import lombok.extern.slf4j.Slf4j; import org.hibernate.exception.ConstraintViolationException; import org.springframework.beans.factory.annotation.Autowired; @@ -44,6 +45,7 @@ import java.util.List; import java.util.Optional; import java.util.UUID; +import static com.google.common.util.concurrent.MoreExecutors.directExecutor; import static org.thingsboard.common.util.DebugModeUtil.DEBUG_MODE_DEFAULT_DURATION_MINUTES; import static org.thingsboard.server.dao.service.Validator.validateId; @@ -230,23 +232,29 @@ public class TenantProfileServiceImpl extends AbstractCachedEntityService>> findEntityAsync(TenantId tenantId, EntityId entityId) { + return FluentFuture.from(tenantProfileDao.findByIdAsync(tenantId, entityId.getId())) + .transform(Optional::ofNullable, directExecutor()); + } + @Override public EntityType getEntityType() { return EntityType.TENANT_PROFILE; } - private final PaginatedRemover tenantProfilesRemover = - new PaginatedRemover<>() { + private final PaginatedRemover tenantProfilesRemover = new PaginatedRemover<>() { - @Override - protected PageData findEntities(TenantId tenantId, String id, PageLink pageLink) { - return tenantProfileDao.findTenantProfiles(tenantId, pageLink); - } + @Override + protected PageData findEntities(TenantId tenantId, String id, PageLink pageLink) { + return tenantProfileDao.findTenantProfiles(tenantId, pageLink); + } + + @Override + protected void removeEntity(TenantId tenantId, TenantProfile entity) { + removeTenantProfile(tenantId, entity, entity.isDefault()); + } - @Override - protected void removeEntity(TenantId tenantId, TenantProfile entity) { - removeTenantProfile(tenantId, entity, entity.isDefault()); - } - }; + }; } diff --git a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java index 0df7c36527..b6c5762d60 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.dao.tenant; +import com.google.common.util.concurrent.FluentFuture; import com.google.common.util.concurrent.ListenableFuture; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; @@ -51,6 +52,7 @@ import java.util.List; import java.util.Optional; import java.util.function.Consumer; +import static com.google.common.util.concurrent.MoreExecutors.directExecutor; import static org.thingsboard.server.dao.service.Validator.validateId; @Service("TenantDaoService") @@ -75,6 +77,7 @@ public class TenantServiceImpl extends AbstractCachedEntityService existsTenantCache; - @TransactionalEventListener(classes = TenantEvictEvent.class) @Override + @TransactionalEventListener public void handleEvictEvent(TenantEvictEvent event) { TenantId tenantId = event.getTenantId(); cache.evict(tenantId); @@ -245,6 +248,12 @@ public class TenantServiceImpl extends AbstractCachedEntityService>> findEntityAsync(TenantId tenantId, EntityId entityId) { + return FluentFuture.from(findTenantByIdAsync(tenantId, TenantId.fromUUID(entityId.getId()))) + .transform(Optional::ofNullable, directExecutor()); + } + @Override public EntityType getEntityType() { return EntityType.TENANT; diff --git a/dao/src/main/java/org/thingsboard/server/dao/timeseries/BaseTimeseriesService.java b/dao/src/main/java/org/thingsboard/server/dao/timeseries/BaseTimeseriesService.java index cecf4ab587..ceb7fcf822 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/timeseries/BaseTimeseriesService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/timeseries/BaseTimeseriesService.java @@ -196,7 +196,8 @@ public class BaseTimeseriesService implements TimeseriesService { if (saveLatest) { latestFutures.add(Futures.transform(timeseriesLatestDao.saveLatest(tenantId, entityId, tsKvEntry), version -> { if (version != null) { - edqsService.onUpdate(tenantId, ObjectType.LATEST_TS_KV, new LatestTsKv(entityId, tsKvEntry, version)); + TenantId edqsTenantId = entityId.getEntityType() == EntityType.TENANT ? (TenantId) entityId : tenantId; + edqsService.onUpdate(edqsTenantId, ObjectType.LATEST_TS_KV, new LatestTsKv(entityId, tsKvEntry, version)); } return version; }, MoreExecutors.directExecutor())); @@ -276,7 +277,8 @@ public class BaseTimeseriesService implements TimeseriesService { return Futures.transform(timeseriesLatestDao.removeLatest(tenantId, entityId, query), result -> { if (result.isRemoved()) { Long version = result.getVersion(); - edqsService.onDelete(tenantId, ObjectType.LATEST_TS_KV, new LatestTsKv(entityId, query.getKey(), version)); + TenantId edqsTenantId = entityId.getEntityType() == EntityType.TENANT ? (TenantId) entityId : tenantId; + edqsService.onDelete(edqsTenantId, ObjectType.LATEST_TS_KV, new LatestTsKv(entityId, query.getKey(), version)); } return result; }, MoreExecutors.directExecutor()); diff --git a/dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDao.java b/dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDao.java index f7bd64bef8..6818994629 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDao.java @@ -113,6 +113,10 @@ public class CassandraBaseTimeseriesDao extends AbstractCassandraBaseTimeseriesD @Value("${cassandra.query.use_ts_key_value_partitioning_on_read:true}") private boolean useTsKeyValuePartitioningOnRead; + @Getter + @Value("${cassandra.query.use_ts_key_value_partitioning_on_read_max_estimated_partition_count:40}") // 3+ years for MONTHS + private int useTsKeyValuePartitioningOnReadMaxEstimatedPartitionCount; + @Value("${cassandra.query.ts_key_value_partitions_max_cache_size:100000}") private long partitionsCacheSize; @@ -415,22 +419,41 @@ public class CassandraBaseTimeseriesDao extends AbstractCassandraBaseTimeseriesD readResultsProcessingExecutor); } - private ListenableFuture> getPartitionsFuture(TenantId tenantId, TsKvQuery query, EntityId entityId, long minPartition, long maxPartition) { + ListenableFuture> getPartitionsFuture(TenantId tenantId, TsKvQuery query, EntityId entityId, long minPartition, long maxPartition) { if (isFixedPartitioning()) { //no need to fetch partitions from DB return Futures.immediateFuture(FIXED_PARTITION); } if (!isUseTsKeyValuePartitioningOnRead()) { - return Futures.immediateFuture(calculatePartitions(minPartition, maxPartition)); + final long estimatedPartitionCount = estimatePartitionCount(minPartition, maxPartition); + if (estimatedPartitionCount <= useTsKeyValuePartitioningOnReadMaxEstimatedPartitionCount) { + return Futures.immediateFuture(calculatePartitions(minPartition, maxPartition, (int) estimatedPartitionCount)); + } } + return getPartitionsFromDB(tenantId, query, entityId, minPartition, maxPartition); + } + + ListenableFuture> getPartitionsFromDB(TenantId tenantId, TsKvQuery query, EntityId entityId, long minPartition, long maxPartition) { TbResultSetFuture partitionsFuture = fetchPartitions(tenantId, entityId, query.getKey(), minPartition, maxPartition); return Futures.transformAsync(partitionsFuture, getPartitionsArrayFunction(), readResultsProcessingExecutor); } + // Optimistic estimation of partition count, expected to be never called for infinite partitioning + long estimatePartitionCount(long minPartition, long maxPartition) { + if (maxPartition > minPartition) { + return (maxPartition - minPartition) / tsFormat.getDurationMs() + 2; //at least 2 partitions, at max 2 partitions overestimated + } + return 1; // 1 or 0, but 1 is more optimistic + } + List calculatePartitions(long minPartition, long maxPartition) { + return calculatePartitions(minPartition, maxPartition, 0); + } + + List calculatePartitions(long minPartition, long maxPartition, int estimatedPartitionCount) { if (minPartition == maxPartition) { return Collections.singletonList(minPartition); } - List partitions = new ArrayList<>(); + List partitions = estimatedPartitionCount > 0 ? new ArrayList<>(estimatedPartitionCount) : new ArrayList<>(); long currentPartition = minPartition; LocalDateTime currentPartitionTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(currentPartition), ZoneOffset.UTC); diff --git a/dao/src/main/java/org/thingsboard/server/dao/timeseries/NoSqlTsPartitionDate.java b/dao/src/main/java/org/thingsboard/server/dao/timeseries/NoSqlTsPartitionDate.java index 873c33d4e3..c403a46df1 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/timeseries/NoSqlTsPartitionDate.java +++ b/dao/src/main/java/org/thingsboard/server/dao/timeseries/NoSqlTsPartitionDate.java @@ -15,32 +15,29 @@ */ package org.thingsboard.server.dao.timeseries; +import lombok.Getter; + import java.time.LocalDateTime; import java.time.ZoneOffset; import java.time.temporal.ChronoUnit; import java.time.temporal.TemporalUnit; import java.util.Optional; +import java.util.concurrent.TimeUnit; +@Getter public enum NoSqlTsPartitionDate { MINUTES("yyyy-MM-dd-HH-mm", ChronoUnit.MINUTES), HOURS("yyyy-MM-dd-HH", ChronoUnit.HOURS), DAYS("yyyy-MM-dd", ChronoUnit.DAYS), MONTHS("yyyy-MM", ChronoUnit.MONTHS), YEARS("yyyy", ChronoUnit.YEARS),INDEFINITE("",ChronoUnit.FOREVER); private final String pattern; private final transient TemporalUnit truncateUnit; + private final transient long durationMs; public final static LocalDateTime EPOCH_START = LocalDateTime.ofEpochSecond(0,0, ZoneOffset.UTC); NoSqlTsPartitionDate(String pattern, TemporalUnit truncateUnit) { this.pattern = pattern; this.truncateUnit = truncateUnit; - } - - - public String getPattern() { - return pattern; - } - - public TemporalUnit getTruncateUnit() { - return truncateUnit; + this.durationMs = TimeUnit.SECONDS.toMillis(this.truncateUnit.getDuration().getSeconds()); } public LocalDateTime truncatedTo(LocalDateTime time) { diff --git a/dao/src/main/java/org/thingsboard/server/dao/usagerecord/ApiUsageStateServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/usagerecord/ApiUsageStateServiceImpl.java index 1282493650..49ee56e7be 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/usagerecord/ApiUsageStateServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/usagerecord/ApiUsageStateServiceImpl.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.dao.usagerecord; +import com.google.common.util.concurrent.FluentFuture; import lombok.extern.slf4j.Slf4j; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; @@ -48,11 +49,13 @@ import java.util.List; import java.util.Objects; import java.util.Optional; +import static com.google.common.util.concurrent.MoreExecutors.directExecutor; import static org.thingsboard.server.dao.service.Validator.validateId; @Service("ApiUsageStateDaoService") @Slf4j public class ApiUsageStateServiceImpl extends AbstractEntityService implements ApiUsageStateService { + public static final String INCORRECT_TENANT_ID = "Incorrect tenantId "; private final ApiUsageStateDao apiUsageStateDao; @@ -161,7 +164,7 @@ public class ApiUsageStateServiceImpl extends AbstractEntityService implements A public ApiUsageState update(ApiUsageState apiUsageState) { log.trace("Executing save [{}]", apiUsageState.getTenantId()); validateId(apiUsageState.getTenantId(), id -> INCORRECT_TENANT_ID + id); - validateId(apiUsageState.getId(), "Can't save new usage state. Only update is allowed!"); + validateId(apiUsageState.getId(), __ -> "Can't save new usage state. Only update is allowed!"); apiUsageState.setVersion(null); ApiUsageState savedState = apiUsageStateDao.save(apiUsageState.getTenantId(), apiUsageState); eventPublisher.publishEvent(SaveEntityEvent.builder().tenantId(savedState.getTenantId()).entityId(savedState.getId()) @@ -195,6 +198,12 @@ public class ApiUsageStateServiceImpl extends AbstractEntityService implements A return Optional.ofNullable(findApiUsageStateById(tenantId, new ApiUsageStateId(entityId.getId()))); } + @Override + public FluentFuture>> findEntityAsync(TenantId tenantId, EntityId entityId) { + return FluentFuture.from(apiUsageStateDao.findByIdAsync(tenantId, entityId.getId())) + .transform(Optional::ofNullable, directExecutor()); + } + @Transactional @Override public void deleteByTenantId(TenantId tenantId) { diff --git a/dao/src/main/java/org/thingsboard/server/dao/user/UserDao.java b/dao/src/main/java/org/thingsboard/server/dao/user/UserDao.java index b60b263ac8..127aa6141a 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/user/UserDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/user/UserDao.java @@ -101,4 +101,5 @@ public interface UserDao extends Dao, TenantEntityDao { PageData findByAuthorityAndTenantProfilesIds(Authority authority, List tenantProfilesIds, PageLink pageLink); + int countTenantAdmins(UUID tenantId); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java index 5c94ba1891..c9157dbf1a 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java @@ -18,6 +18,7 @@ package org.thingsboard.server.dao.user; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ObjectNode; +import com.google.common.util.concurrent.FluentFuture; import com.google.common.util.concurrent.ListenableFuture; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -44,6 +45,11 @@ import org.thingsboard.server.common.data.id.UserCredentialsId; import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.mobile.MobileSessionInfo; import org.thingsboard.server.common.data.mobile.UserMobileSessionInfo; +import org.thingsboard.server.common.data.notification.targets.platform.CustomerUsersFilter; +import org.thingsboard.server.common.data.notification.targets.platform.SystemLevelUsersFilter; +import org.thingsboard.server.common.data.notification.targets.platform.TenantAdministratorsFilter; +import org.thingsboard.server.common.data.notification.targets.platform.UserListFilter; +import org.thingsboard.server.common.data.notification.targets.platform.UsersFilter; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.security.Authority; @@ -62,6 +68,7 @@ import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.dao.service.PaginatedRemover; import org.thingsboard.server.dao.settings.SecuritySettingsService; import org.thingsboard.server.dao.sql.JpaExecutorService; +import org.thingsboard.server.dao.tenant.TbTenantProfileCache; import java.util.ArrayList; import java.util.Collections; @@ -71,7 +78,10 @@ import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; +import static com.google.common.util.concurrent.MoreExecutors.directExecutor; +import static org.apache.commons.collections4.CollectionUtils.isNotEmpty; import static org.thingsboard.server.common.data.StringUtils.generateSafeToken; import static org.thingsboard.server.dao.service.Validator.validateId; import static org.thingsboard.server.dao.service.Validator.validatePageLink; @@ -97,14 +107,15 @@ public class UserServiceImpl extends AbstractCachedEntityService userValidator; private final DataValidator userCredentialsValidator; private final ApplicationEventPublisher eventPublisher; private final EntityCountService countService; private final JpaExecutorService executor; - @TransactionalEventListener(classes = UserCacheEvictEvent.class) @Override + @TransactionalEventListener public void handleEvictEvent(UserCacheEvictEvent event) { List keys = new ArrayList<>(2); keys.add(new UserCacheKey(event.tenantId(), event.newEmail())); @@ -159,6 +170,10 @@ public class UserServiceImpl extends AbstractCachedEntityService doSaveUser(tenantId, user)); + } + + private User doSaveUser(TenantId tenantId, User user) { log.trace("Executing saveUser [{}]", user); User oldUser = userValidator.validate(user, User::getTenantId); if (!userLoginCaseSensitive) { @@ -179,7 +194,7 @@ public class UserServiceImpl extends AbstractCachedEntityService findMobileSessionInfo(TenantId tenantId, UserId userId) { return Optional.ofNullable(userSettingsService.findUserSettings(tenantId, userId, UserSettingsType.MOBILE)) .map(UserSettings::getSettings).map(settings -> JacksonUtil.treeToValue(settings, UserMobileSessionInfo.class)); @@ -496,6 +516,80 @@ public class UserServiceImpl extends AbstractCachedEntityService findUsersByFilter(TenantId tenantId, UsersFilter filter, PageLink pageLink) { + switch (filter.getType()) { + case USER_LIST -> { + List users = ((UserListFilter) filter).getUsersIds().stream() + .limit(pageLink.getPageSize()) + .map(UserId::new).map(userId -> findUserById(tenantId, userId)) + .filter(Objects::nonNull).collect(Collectors.toList()); + return new PageData<>(users, 1, users.size(), false); + } + case CUSTOMER_USERS -> { + if (tenantId.equals(TenantId.SYS_TENANT_ID)) { + throw new IllegalArgumentException("Customer users target is not supported for system administrator"); + } + CustomerUsersFilter customerUsersFilter = (CustomerUsersFilter) filter; + return findCustomerUsers(tenantId, new CustomerId(customerUsersFilter.getCustomerId()), pageLink); + } + case TENANT_ADMINISTRATORS -> { + TenantAdministratorsFilter tenantAdministratorsFilter = (TenantAdministratorsFilter) filter; + if (!tenantId.equals(TenantId.SYS_TENANT_ID)) { + return findTenantAdmins(tenantId, pageLink); + } else { + if (isNotEmpty(tenantAdministratorsFilter.getTenantsIds())) { + return findTenantAdminsByTenantsIds(tenantAdministratorsFilter.getTenantsIds().stream() + .map(TenantId::fromUUID).collect(Collectors.toList()), pageLink); + } else if (isNotEmpty(tenantAdministratorsFilter.getTenantProfilesIds())) { + return findTenantAdminsByTenantProfilesIds(tenantAdministratorsFilter.getTenantProfilesIds().stream() + .map(TenantProfileId::new).collect(Collectors.toList()), pageLink); + } else { + return findAllTenantAdmins(pageLink); + } + } + } + case SYSTEM_ADMINISTRATORS -> { + return findSysAdmins(pageLink); + } + case ALL_USERS -> { + if (!tenantId.equals(TenantId.SYS_TENANT_ID)) { + return findUsersByTenantId(tenantId, pageLink); + } else { + return findAllUsers(pageLink); + } + } + default -> throw new IllegalArgumentException("Recipient type not supported"); + } + } + + @Override + public boolean matchesFilter(TenantId tenantId, SystemLevelUsersFilter filter, User user) { + switch (filter.getType()) { + case TENANT_ADMINISTRATORS -> { + if (user.isSystemAdmin() || user.isCustomerUser()) { + return false; + } + TenantAdministratorsFilter tenantAdministratorsFilter = (TenantAdministratorsFilter) filter; + if (isNotEmpty(tenantAdministratorsFilter.getTenantsIds())) { + return tenantAdministratorsFilter.getTenantsIds().contains(user.getTenantId().getId()); + } else if (isNotEmpty(tenantAdministratorsFilter.getTenantProfilesIds())) { + return tenantAdministratorsFilter.getTenantProfilesIds().contains(tenantProfileCache.get(user.getTenantId()).getUuidId()); + } else { + return user.getAuthority() == Authority.TENANT_ADMIN; + } + } + case SYSTEM_ADMINISTRATORS -> { + return user.getAuthority() == Authority.SYS_ADMIN; + } + case ALL_USERS -> { + return true; + } + default -> throw new IllegalArgumentException("Recipient type not supported"); + } + + } + private void updatePasswordHistory(UserCredentials userCredentials) { JsonNode additionalInfo = userCredentials.getAdditionalInfo(); if (!(additionalInfo instanceof ObjectNode)) { @@ -563,6 +657,12 @@ public class UserServiceImpl extends AbstractCachedEntityService>> findEntityAsync(TenantId tenantId, EntityId entityId) { + return FluentFuture.from(findUserByIdAsync(tenantId, new UserId(entityId.getId()))) + .transform(Optional::ofNullable, directExecutor()); + } + @Override public long countByTenantId(TenantId tenantId) { return userDao.countByTenantId(tenantId); diff --git a/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetTypeServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetTypeServiceImpl.java index 7e74c2a18b..db8cd875e3 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetTypeServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetTypeServiceImpl.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.dao.widget; +import com.google.common.util.concurrent.FluentFuture; import lombok.extern.slf4j.Slf4j; import org.apache.commons.collections4.CollectionUtils; import org.springframework.beans.factory.annotation.Autowired; @@ -47,6 +48,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Optional; +import static com.google.common.util.concurrent.MoreExecutors.directExecutor; import static org.thingsboard.server.dao.service.Validator.validateIds; @Service("WidgetTypeDaoService") @@ -54,8 +56,6 @@ import static org.thingsboard.server.dao.service.Validator.validateIds; public class WidgetTypeServiceImpl implements WidgetTypeService { public static final String INCORRECT_TENANT_ID = "Incorrect tenantId "; - public static final String INCORRECT_RESOURCE_ID = "Incorrect resourceId "; - public static final String INCORRECT_BUNDLE_ALIAS = "Incorrect bundleAlias "; public static final String INCORRECT_WIDGETS_BUNDLE_ID = "Incorrect widgetsBundleId "; @Autowired @@ -281,6 +281,12 @@ public class WidgetTypeServiceImpl implements WidgetTypeService { return Optional.ofNullable(findWidgetTypeById(tenantId, new WidgetTypeId(entityId.getId()))); } + @Override + public FluentFuture>> findEntityAsync(TenantId tenantId, EntityId entityId) { + return FluentFuture.from(widgetTypeDao.findByIdAsync(tenantId, entityId.getId())) + .transform(Optional::ofNullable, directExecutor()); + } + @Override public EntityType getEntityType() { return EntityType.WIDGET_TYPE; @@ -303,6 +309,7 @@ public class WidgetTypeServiceImpl implements WidgetTypeService { protected void removeEntity(TenantId tenantId, WidgetTypeInfo entity) { deleteWidgetType(tenantId, new WidgetTypeId(entity.getUuidId())); } + }; private final PaginatedRemover bundleWidgetTypesRemover = new PaginatedRemover<>() { @@ -316,6 +323,7 @@ public class WidgetTypeServiceImpl implements WidgetTypeService { protected void removeEntity(TenantId tenantId, WidgetTypeInfo widgetTypeInfo) { deleteWidgetType(tenantId, widgetTypeInfo.getId()); } + }; } diff --git a/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetsBundleServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetsBundleServiceImpl.java index f057ce22ca..5b5930fb0a 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetsBundleServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetsBundleServiceImpl.java @@ -16,6 +16,7 @@ package org.thingsboard.server.dao.widget; import com.fasterxml.jackson.databind.JsonNode; +import com.google.common.util.concurrent.FluentFuture; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationEventPublisher; @@ -33,12 +34,10 @@ import org.thingsboard.server.common.data.widget.WidgetType; import org.thingsboard.server.common.data.widget.WidgetTypeDetails; import org.thingsboard.server.common.data.widget.WidgetsBundle; import org.thingsboard.server.common.data.widget.WidgetsBundleFilter; -import org.thingsboard.server.dao.entity.AbstractCachedEntityService; import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent; import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent; import org.thingsboard.server.dao.exception.IncorrectParameterException; import org.thingsboard.server.dao.resource.ImageService; -import org.thingsboard.server.dao.resource.ResourceService; import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.dao.service.PaginatedRemover; import org.thingsboard.server.dao.service.Validator; @@ -48,13 +47,15 @@ import java.util.List; import java.util.Optional; import java.util.stream.Stream; +import static com.google.common.util.concurrent.MoreExecutors.directExecutor; +import static org.thingsboard.server.dao.entity.AbstractEntityService.checkConstraintViolation; + @Service("WidgetsBundleDaoService") @Slf4j public class WidgetsBundleServiceImpl implements WidgetsBundleService { private static final int DEFAULT_WIDGETS_BUNDLE_LIMIT = 300; public static final String INCORRECT_TENANT_ID = "Incorrect tenantId "; - public static final String INCORRECT_PAGE_LINK = "Incorrect page link "; @Autowired private WidgetsBundleDao widgetsBundleDao; @@ -71,9 +72,6 @@ public class WidgetsBundleServiceImpl implements WidgetsBundleService { @Autowired private ImageService imageService; - @Autowired - private ResourceService resourceService; - @Override public WidgetsBundle findWidgetsBundleById(TenantId tenantId, WidgetsBundleId widgetsBundleId) { log.trace("Executing findWidgetsBundleById [{}]", widgetsBundleId); @@ -92,9 +90,9 @@ public class WidgetsBundleServiceImpl implements WidgetsBundleService { .entityId(result.getId()).created(widgetsBundle.getId() == null).build()); return result; } catch (Exception e) { - AbstractCachedEntityService.checkConstraintViolation(e, - "uq_widgets_bundle_alias", "Widgets Bundle with such alias already exists!"); - AbstractCachedEntityService.checkConstraintViolation(e, "widgets_bundle_external_id_unq_key", "Widgets Bundle with such external id already exists!"); + checkConstraintViolation(e, + "uq_widgets_bundle_alias", "Widgets Bundle with such alias already exists!", + "widgets_bundle_external_id_unq_key", "Widgets Bundle with such external id already exists!"); throw e; } } @@ -248,9 +246,7 @@ public class WidgetsBundleServiceImpl implements WidgetsBundleService { } if (widgetsBundleDescriptor.has("widgetTypeFqns")) { JsonNode widgetFqnsArrayJson = widgetsBundleDescriptor.get("widgetTypeFqns"); - widgetFqnsArrayJson.forEach(fqnJson -> { - widgetTypeFqns.add(fqnJson.asText()); - }); + widgetFqnsArrayJson.forEach(fqnJson -> widgetTypeFqns.add(fqnJson.asText())); } widgetTypeService.updateWidgetsBundleWidgetFqns(TenantId.SYS_TENANT_ID, widgetsBundle.getId(), widgetTypeFqns); }); @@ -274,23 +270,29 @@ public class WidgetsBundleServiceImpl implements WidgetsBundleService { return Optional.ofNullable(findWidgetsBundleById(tenantId, new WidgetsBundleId(entityId.getId()))); } + @Override + public FluentFuture>> findEntityAsync(TenantId tenantId, EntityId entityId) { + return FluentFuture.from(widgetsBundleDao.findByIdAsync(tenantId, entityId.getId())) + .transform(Optional::ofNullable, directExecutor()); + } + @Override public EntityType getEntityType() { return EntityType.WIDGETS_BUNDLE; } - private PaginatedRemover tenantWidgetsBundleRemover = - new PaginatedRemover() { + private final PaginatedRemover tenantWidgetsBundleRemover = new PaginatedRemover<>() { - @Override - protected PageData findEntities(TenantId tenantId, TenantId id, PageLink pageLink) { - return widgetsBundleDao.findTenantWidgetsBundlesByTenantId(id.getId(), pageLink); - } + @Override + protected PageData findEntities(TenantId tenantId, TenantId id, PageLink pageLink) { + return widgetsBundleDao.findTenantWidgetsBundlesByTenantId(id.getId(), pageLink); + } + + @Override + protected void removeEntity(TenantId tenantId, WidgetsBundle entity) { + deleteWidgetsBundle(tenantId, new WidgetsBundleId(entity.getUuidId())); + } - @Override - protected void removeEntity(TenantId tenantId, WidgetsBundle entity) { - deleteWidgetsBundle(tenantId, new WidgetsBundleId(entity.getUuidId())); - } - }; + }; } diff --git a/dao/src/main/resources/sql/schema-entities.sql b/dao/src/main/resources/sql/schema-entities.sql index 6ccf2f6d95..4722d4dafa 100644 --- a/dao/src/main/resources/sql/schema-entities.sql +++ b/dao/src/main/resources/sql/schema-entities.sql @@ -923,17 +923,7 @@ CREATE TABLE IF NOT EXISTS calculated_field ( configuration varchar(1000000), version BIGINT DEFAULT 1, debug_settings varchar(1024), - CONSTRAINT calculated_field_unq_key UNIQUE (entity_id, name) -); - -CREATE TABLE IF NOT EXISTS calculated_field_link ( - id uuid NOT NULL CONSTRAINT calculated_field_link_pkey PRIMARY KEY, - created_time bigint NOT NULL, - tenant_id uuid NOT NULL, - entity_type VARCHAR(32), - entity_id uuid NOT NULL, - calculated_field_id uuid NOT NULL, - CONSTRAINT fk_calculated_field_id FOREIGN KEY (calculated_field_id) REFERENCES calculated_field(id) ON DELETE CASCADE + CONSTRAINT calculated_field_unq_key UNIQUE (entity_id, type, name) ); CREATE TABLE IF NOT EXISTS cf_debug_event ( diff --git a/dao/src/test/java/org/thingsboard/server/dao/AbstractRedisClusterContainer.java b/dao/src/test/java/org/thingsboard/server/dao/AbstractRedisClusterContainer.java index a1009cd255..5ffb9f90f7 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/AbstractRedisClusterContainer.java +++ b/dao/src/test/java/org/thingsboard/server/dao/AbstractRedisClusterContainer.java @@ -31,7 +31,7 @@ import java.util.concurrent.TimeUnit; public class AbstractRedisClusterContainer { static final String NODES = "127.0.0.1:6371,127.0.0.1:6372,127.0.0.1:6373,127.0.0.1:6374,127.0.0.1:6375,127.0.0.1:6376"; - static final String IMAGE = "bitnami/valkey-cluster:8.0"; + static final String IMAGE = "bitnamilegacy/valkey-cluster:8.0"; static final Map ENVS = Map.of( "VALKEY_CLUSTER_ANNOUNCE_IP", "127.0.0.1", "VALKEY_CLUSTER_DYNAMIC_IPS", "no", diff --git a/dao/src/test/java/org/thingsboard/server/dao/AbstractRedisContainer.java b/dao/src/test/java/org/thingsboard/server/dao/AbstractRedisContainer.java index 58843a4651..4b5729fcc6 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/AbstractRedisContainer.java +++ b/dao/src/test/java/org/thingsboard/server/dao/AbstractRedisContainer.java @@ -27,7 +27,7 @@ import java.util.List; public class AbstractRedisContainer { @ClassRule(order = 0) - public static GenericContainer redis = new GenericContainer("bitnami/valkey:8.0") + public static GenericContainer redis = new GenericContainer("bitnamilegacy/valkey:8.0") .withEnv("ALLOW_EMPTY_PASSWORD","yes") .withLogConsumer(s -> log.warn(((OutputFrame) s).getUtf8String().trim())) .withExposedPorts(6379); diff --git a/dao/src/test/java/org/thingsboard/server/dao/RedisJUnit5Test.java b/dao/src/test/java/org/thingsboard/server/dao/RedisJUnit5Test.java index 5d43ffb4ae..0def73f9e9 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/RedisJUnit5Test.java +++ b/dao/src/test/java/org/thingsboard/server/dao/RedisJUnit5Test.java @@ -33,7 +33,7 @@ import static org.assertj.core.api.Assertions.assertThat; public class RedisJUnit5Test { @Container - private static final GenericContainer REDIS = new GenericContainer("bitnami/valkey:8.0") + private static final GenericContainer REDIS = new GenericContainer("bitnamilegacy/valkey:8.0") .withEnv("ALLOW_EMPTY_PASSWORD","yes") .withLogConsumer(s -> log.error(((OutputFrame) s).getUtf8String().trim())) .withExposedPorts(6379); diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/AbstractServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/AbstractServiceTest.java index 25339244fd..0d2b9b5d9f 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/AbstractServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/AbstractServiceTest.java @@ -48,6 +48,7 @@ import org.thingsboard.server.common.data.id.DeviceProfileId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.HasId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.TenantProfileId; import org.thingsboard.server.common.data.oauth2.MapperType; import org.thingsboard.server.common.data.oauth2.OAuth2Client; import org.thingsboard.server.common.data.oauth2.OAuth2CustomMapperConfig; @@ -185,8 +186,13 @@ public abstract class AbstractServiceTest { } public Tenant createTenant() { + return createTenant(null); + } + + public Tenant createTenant(TenantProfileId tenantProfileId) { Tenant tenant = new Tenant(); tenant.setTitle("My tenant " + UUID.randomUUID()); + tenant.setTenantProfileId(tenantProfileId); Tenant savedTenant = tenantService.saveTenant(tenant); assertNotNull(savedTenant); return savedTenant; diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/AlarmServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/AlarmServiceTest.java index 849528b091..1b8a08defc 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/AlarmServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/AlarmServiceTest.java @@ -211,7 +211,7 @@ public class AlarmServiceTest extends AbstractServiceTest { Assert.assertNotNull(alarms.getData()); Assert.assertEquals(0, alarms.getData().size()); - alarmService.clearAlarm(tenantId, created.getId(), System.currentTimeMillis(), null); + alarmService.clearAlarm(tenantId, created.getId(), System.currentTimeMillis(), null, true); created = alarmService.findAlarmInfoById(tenantId, created.getId()); alarms = alarmService.findAlarms(tenantId, AlarmQuery.builder() @@ -319,7 +319,7 @@ public class AlarmServiceTest extends AbstractServiceTest { Assert.assertNotNull(alarms.getData()); Assert.assertEquals(0, alarms.getData().size()); - alarmService.clearAlarm(tenantId, created.getId(), System.currentTimeMillis(), null); + alarmService.clearAlarm(tenantId, created.getId(), System.currentTimeMillis(), null, true); created = alarmService.findAlarmInfoById(tenantId, created.getId()); alarms = alarmService.findAlarmsV2(tenantId, AlarmQueryV2.builder() @@ -622,7 +622,7 @@ public class AlarmServiceTest extends AbstractServiceTest { .severity(AlarmSeverity.MAJOR) .startTs(System.currentTimeMillis()).build()); AlarmInfo alarm1 = result.getAlarm(); - alarmService.clearAlarm(tenantId, alarm1.getId(), System.currentTimeMillis(), null); + alarmService.clearAlarm(tenantId, alarm1.getId(), System.currentTimeMillis(), null, true); result = alarmService.createAlarm(AlarmCreateOrUpdateActiveRequest.builder() .tenantId(tenantId) @@ -632,7 +632,7 @@ public class AlarmServiceTest extends AbstractServiceTest { .startTs(System.currentTimeMillis()).build()); AlarmInfo alarm2 = result.getAlarm(); alarmService.acknowledgeAlarm(tenantId, alarm2.getId(), System.currentTimeMillis()); - alarmService.clearAlarm(tenantId, alarm2.getId(), System.currentTimeMillis(), null); + alarmService.clearAlarm(tenantId, alarm2.getId(), System.currentTimeMillis(), null, true); result = alarmService.createAlarm(AlarmCreateOrUpdateActiveRequest.builder() .tenantId(tenantId) @@ -852,7 +852,7 @@ public class AlarmServiceTest extends AbstractServiceTest { Assert.assertEquals(1, alarmsCount); - alarmService.clearAlarm(tenantId, created.getId(), System.currentTimeMillis(), null); + alarmService.clearAlarm(tenantId, created.getId(), System.currentTimeMillis(), null, true); created = alarmService.findAlarmInfoById(tenantId, created.getId()); alarmsCount = alarmService.countAlarmsByQuery(tenantId, null, countQuery); @@ -980,7 +980,7 @@ public class AlarmServiceTest extends AbstractServiceTest { alarmsCount = alarmService.countAlarmsByQuery(tenantId, null, countQuery, List.of(childId)); Assert.assertEquals(0, alarmsCount); - alarmService.clearAlarm(tenantId, created.getId(), System.currentTimeMillis(), null); + alarmService.clearAlarm(tenantId, created.getId(), System.currentTimeMillis(), null, true); countQuery.setStatusList(List.of(AlarmSearchStatus.CLEARED)); alarmsCount = alarmService.countAlarmsByQuery(tenantId, null, countQuery, List.of(childId)); diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/AssetServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/AssetServiceTest.java index 462e7a894c..b7f208b8c7 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/AssetServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/AssetServiceTest.java @@ -16,17 +16,25 @@ package org.thingsboard.server.dao.service; import com.datastax.oss.driver.api.core.uuid.Uuids; +import com.google.common.util.concurrent.ListeningExecutorService; +import com.google.common.util.concurrent.MoreExecutors; +import org.junit.AfterClass; import org.junit.Assert; +import org.junit.BeforeClass; import org.junit.Test; import org.junit.jupiter.api.Assertions; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.TransactionStatus; import org.springframework.transaction.support.DefaultTransactionDefinition; +import org.testcontainers.shaded.org.apache.commons.lang3.RandomStringUtils; +import org.testcontainers.shaded.org.awaitility.Awaitility; +import org.thingsboard.common.util.ThingsBoardThreadFactory; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.EntitySubtype; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.asset.AssetInfo; import org.thingsboard.server.common.data.asset.AssetProfile; @@ -44,6 +52,8 @@ import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.RelationTypeGroup; +import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; +import org.thingsboard.server.common.data.tenant.profile.TenantProfileData; import org.thingsboard.server.dao.asset.AssetDao; import org.thingsboard.server.dao.asset.AssetProfileService; import org.thingsboard.server.dao.asset.AssetService; @@ -51,12 +61,16 @@ import org.thingsboard.server.dao.cf.CalculatedFieldService; import org.thingsboard.server.dao.customer.CustomerService; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.relation.RelationService; +import org.thingsboard.server.dao.tenant.TenantProfileService; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.thingsboard.server.dao.model.ModelConstants.NULL_UUID; @@ -72,13 +86,28 @@ public class AssetServiceTest extends AbstractServiceTest { @Autowired RelationService relationService; @Autowired + TenantProfileService tenantProfileService; + @Autowired private AssetProfileService assetProfileService; @Autowired private CalculatedFieldService calculatedFieldService; @Autowired private PlatformTransactionManager platformTransactionManager; + private static ListeningExecutorService executor; + private IdComparator idComparator = new IdComparator<>(); + private TenantId anotherTenantId; + + @BeforeClass + public static void before() { + executor = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(10, ThingsBoardThreadFactory.forName("AssetServiceTestScope"))); + } + + @AfterClass + public static void after() { + executor.shutdownNow(); + } @Test public void testSaveAsset() { @@ -105,6 +134,39 @@ public class AssetServiceTest extends AbstractServiceTest { assetService.deleteAsset(tenantId, savedAsset.getId()); } + @Test + public void testAssetLimitOnTenantProfileLevel() throws InterruptedException { + TenantProfile tenantProfile = new TenantProfile(); + tenantProfile.setName("Test profile"); + tenantProfile.setDescription("Test"); + TenantProfileData profileData = new TenantProfileData(); + profileData.setConfiguration(DefaultTenantProfileConfiguration.builder().maxAssets(5l).build()); + tenantProfile.setProfileData(profileData); + tenantProfile.setDefault(false); + tenantProfile.setIsolatedTbRuleEngine(false); + + tenantProfile = tenantProfileService.saveTenantProfile(anotherTenantId, tenantProfile); + anotherTenantId = createTenant(tenantProfile.getId()).getId(); + + for (int i = 0; i < 20; i++) { + executor.submit(() -> { + Asset asset = new Asset(); + asset.setTenantId(anotherTenantId); + asset.setName(RandomStringUtils.randomAlphabetic(10)); + asset.setType("default"); + assetService.saveAsset(asset); + }); + } + + Awaitility.await().atMost(10, TimeUnit.SECONDS).until(() -> { + long countByTenantId = assetService.countByTenantId(anotherTenantId); + return countByTenantId == 5; + }); + + Thread.sleep(2000); + assertThat(assetService.countByTenantId(anotherTenantId)).isEqualTo(5); + } + @Test public void testShouldNotPutInCacheRolledbackAssetProfile() { AssetProfile assetProfile = new AssetProfile(); diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/CalculatedFieldServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/CalculatedFieldServiceTest.java index 2985aa7620..c904852fbe 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/CalculatedFieldServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/CalculatedFieldServiceTest.java @@ -15,13 +15,8 @@ */ package org.thingsboard.server.dao.service; -import com.google.common.util.concurrent.ListeningExecutorService; -import com.google.common.util.concurrent.MoreExecutors; -import org.junit.After; -import org.junit.Before; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.thingsboard.common.util.ThingsBoardExecutors; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.cf.CalculatedField; import org.thingsboard.server.common.data.cf.CalculatedFieldType; @@ -31,18 +26,28 @@ import org.thingsboard.server.common.data.cf.configuration.CalculatedFieldConfig import org.thingsboard.server.common.data.cf.configuration.Output; import org.thingsboard.server.common.data.cf.configuration.OutputType; import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey; +import org.thingsboard.server.common.data.cf.configuration.RelationPathQueryDynamicSourceConfiguration; import org.thingsboard.server.common.data.cf.configuration.SimpleCalculatedFieldConfiguration; -import org.thingsboard.server.common.data.id.CalculatedFieldId; +import org.thingsboard.server.common.data.cf.configuration.geofencing.EntityCoordinates; +import org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingCalculatedFieldConfiguration; +import org.thingsboard.server.common.data.cf.configuration.geofencing.ZoneGroupConfiguration; import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.relation.EntityRelation; +import org.thingsboard.server.common.data.relation.EntitySearchDirection; +import org.thingsboard.server.common.data.relation.RelationPathLevel; import org.thingsboard.server.dao.cf.CalculatedFieldService; import org.thingsboard.server.dao.device.DeviceService; import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.dao.tenant.TbTenantProfileCache; +import java.util.ArrayList; +import java.util.List; import java.util.Map; -import java.util.UUID; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.mock; +import static org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingReportStrategy.REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS; @DaoSqlTest public class CalculatedFieldServiceTest extends AbstractServiceTest { @@ -51,18 +56,8 @@ public class CalculatedFieldServiceTest extends AbstractServiceTest { private CalculatedFieldService calculatedFieldService; @Autowired private DeviceService deviceService; - - private ListeningExecutorService executor; - - @Before - public void before() { - executor = MoreExecutors.listeningDecorator(ThingsBoardExecutors.newWorkStealingPool(8, getClass())); - } - - @After - public void after() { - executor.shutdownNow(); - } + @Autowired + private TbTenantProfileCache tbTenantProfileCache; @Test public void testSaveCalculatedField() { @@ -90,6 +85,145 @@ public class CalculatedFieldServiceTest extends AbstractServiceTest { calculatedFieldService.deleteCalculatedField(tenantId, savedCalculatedField.getId()); } + @Test + public void testSaveGeofencingCalculatedField_shouldThrowWhenScheduledIntervalLessThanMinAllowedIntervalInTenantProfile() { + // Arrange a device + Device device = createTestDevice(); + + // Build a valid Geofencing configuration + GeofencingCalculatedFieldConfiguration cfg = new GeofencingCalculatedFieldConfiguration(); + + // Coordinates: TS_LATEST, no dynamic source + EntityCoordinates entityCoordinates = new EntityCoordinates("latitude", "longitude"); + cfg.setEntityCoordinates(entityCoordinates); + + // Zone-group argument (ATTRIBUTE) — make it DYNAMIC so scheduling is enabled + ZoneGroupConfiguration zoneGroupConfiguration = new ZoneGroupConfiguration("allowed", REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS, false); + var dynamicSourceConfiguration = new RelationPathQueryDynamicSourceConfiguration(); + dynamicSourceConfiguration.setLevels(List.of(new RelationPathLevel(EntitySearchDirection.FROM, EntityRelation.CONTAINS_TYPE))); + zoneGroupConfiguration.setRefDynamicSourceConfiguration(dynamicSourceConfiguration); + cfg.setZoneGroups(Map.of("allowed", zoneGroupConfiguration)); + + // Get tenant profile min. + int min = tbTenantProfileCache.get(tenantId) + .getDefaultProfileConfiguration() + .getMinAllowedScheduledUpdateIntervalInSecForCF(); + int valueFromConfig = min - 10; + + // Enable scheduling with an interval below tenant min + cfg.setScheduledUpdateEnabled(true); + cfg.setScheduledUpdateInterval(valueFromConfig); + + // Create & save Calculated Field + CalculatedField cf = new CalculatedField(); + cf.setTenantId(tenantId); + cf.setEntityId(device.getId()); + cf.setType(CalculatedFieldType.GEOFENCING); + cf.setName("GF min allowed scheduled update interval test"); + cf.setConfigurationVersion(0); + cf.setConfiguration(cfg); + + assertThatThrownBy(() -> calculatedFieldService.save(cf)) + .isInstanceOf(DataValidationException.class) + .hasCauseInstanceOf(IllegalArgumentException.class) + .hasMessageStartingWith("Scheduled update interval is less than configured " + + "minimum allowed interval in tenant profile: "); + } + + @Test + public void testSaveGeofencingCalculatedField_shouldThrowWhenRelationLevelGreaterThanMaxAllowedRelationLevelInTenantProfile() { + // Arrange a device + Device device = createTestDevice(); + + GeofencingCalculatedFieldConfiguration cfg = new GeofencingCalculatedFieldConfiguration(); + + // Coordinates: TS_LATEST, no dynamic source + EntityCoordinates entityCoordinates = new EntityCoordinates("latitude", "longitude"); + cfg.setEntityCoordinates(entityCoordinates); + + int maxRelationLevel = tbTenantProfileCache.get(tenantId) + .getDefaultProfileConfiguration() + .getMaxRelationLevelPerCfArgument(); + + // Zone-group argument (ATTRIBUTE) + ZoneGroupConfiguration zoneGroupConfiguration = new ZoneGroupConfiguration("allowed", REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS, false); + var dynamicSourceConfiguration = new RelationPathQueryDynamicSourceConfiguration(); + + List levels = new ArrayList<>(); + for (int i = 0; i < maxRelationLevel + 1; i++) { + levels.add(mock(RelationPathLevel.class)); + } + + dynamicSourceConfiguration.setLevels(levels); + zoneGroupConfiguration.setRefDynamicSourceConfiguration(dynamicSourceConfiguration); + cfg.setZoneGroups(Map.of("allowed", zoneGroupConfiguration)); + + // Create & save Calculated Field + CalculatedField cf = new CalculatedField(); + cf.setTenantId(tenantId); + cf.setEntityId(device.getId()); + cf.setType(CalculatedFieldType.GEOFENCING); + cf.setName("GF max relation level test"); + cf.setConfigurationVersion(0); + cf.setConfiguration(cfg); + + assertThatThrownBy(() -> calculatedFieldService.save(cf)) + .isInstanceOf(DataValidationException.class) + .hasCauseInstanceOf(IllegalArgumentException.class) + .hasMessageStartingWith("Max relation level is greater than configured maximum allowed relation level in tenant profile"); + } + + @Test + public void testSaveGeofencingCalculatedField_shouldSaveWithoutDataValidationExceptionOnScheduledUpdateInterval() { + // Arrange a device + Device device = createTestDevice(); + + // Build a valid Geofencing configuration + GeofencingCalculatedFieldConfiguration cfg = new GeofencingCalculatedFieldConfiguration(); + + // Coordinates: TS_LATEST, no dynamic source + EntityCoordinates entityCoordinates = new EntityCoordinates("latitude", "longitude"); + cfg.setEntityCoordinates(entityCoordinates); + + // Zone-group argument (ATTRIBUTE) — make it DYNAMIC so scheduling is enabled + ZoneGroupConfiguration zoneGroupConfiguration = new ZoneGroupConfiguration("allowed", REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS, false); + var dynamicSourceConfiguration = new RelationPathQueryDynamicSourceConfiguration(); + dynamicSourceConfiguration.setLevels(List.of(new RelationPathLevel(EntitySearchDirection.FROM, EntityRelation.CONTAINS_TYPE))); + zoneGroupConfiguration.setRefDynamicSourceConfiguration(dynamicSourceConfiguration); + cfg.setZoneGroups(Map.of("allowed", zoneGroupConfiguration)); + + // Get tenant profile min. + int min = tbTenantProfileCache.get(tenantId) + .getDefaultProfileConfiguration() + .getMinAllowedScheduledUpdateIntervalInSecForCF(); + int valueFromConfig = min + 100; + + // Enable scheduling with an interval greater than tenant min + cfg.setScheduledUpdateEnabled(true); + cfg.setScheduledUpdateInterval(valueFromConfig); + + // Create & save Calculated Field + CalculatedField cf = new CalculatedField(); + cf.setTenantId(tenantId); + cf.setEntityId(device.getId()); + cf.setType(CalculatedFieldType.GEOFENCING); + cf.setName("GF no validation error test"); + cf.setConfigurationVersion(0); + cf.setConfiguration(cfg); + + CalculatedField saved = calculatedFieldService.save(cf); + + assertThat(saved).isNotNull(); + assertThat(saved.getConfiguration()).isInstanceOf(GeofencingCalculatedFieldConfiguration.class); + + var geofencingCalculatedFieldConfiguration = (GeofencingCalculatedFieldConfiguration) saved.getConfiguration(); + + int savedInterval = geofencingCalculatedFieldConfiguration.getScheduledUpdateInterval(); + assertThat(savedInterval).isEqualTo(valueFromConfig); + + calculatedFieldService.deleteCalculatedField(tenantId, saved.getId()); + } + @Test public void testSaveCalculatedFieldWithExistingName() { Device device = createTestDevice(); @@ -98,7 +232,7 @@ public class CalculatedFieldServiceTest extends AbstractServiceTest { assertThatThrownBy(() -> calculatedFieldService.save(calculatedField)) .isInstanceOf(DataValidationException.class) - .hasMessage("Calculated Field with such name is already in exists!"); + .hasMessage("Calculated field with such name and type already exists"); } @Test diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/DeviceCredentialsServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/DeviceCredentialsServiceTest.java index 636b1a2ba5..d5c969b6f3 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/DeviceCredentialsServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/DeviceCredentialsServiceTest.java @@ -19,7 +19,10 @@ import com.datastax.oss.driver.api.core.uuid.Uuids; import org.junit.Assert; import org.junit.Test; import org.junit.jupiter.api.Assertions; +import org.mockito.Mockito; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.id.DeviceCredentialsId; import org.thingsboard.server.common.data.id.DeviceId; @@ -27,6 +30,7 @@ import org.thingsboard.server.common.data.security.DeviceCredentials; import org.thingsboard.server.common.data.security.DeviceCredentialsType; import org.thingsboard.server.dao.device.DeviceCredentialsService; import org.thingsboard.server.dao.device.DeviceService; +import org.thingsboard.server.dao.eventsourcing.ActionEntityEvent; import org.thingsboard.server.dao.exception.DataValidationException; @DaoSqlTest @@ -36,6 +40,8 @@ public class DeviceCredentialsServiceTest extends AbstractServiceTest { DeviceCredentialsService deviceCredentialsService; @Autowired DeviceService deviceService; + @MockitoBean + ApplicationEventPublisher eventPublisher; @Test public void testCreateDeviceCredentials() { @@ -185,5 +191,39 @@ public class DeviceCredentialsServiceTest extends AbstractServiceTest { Assert.assertEquals(deviceCredentials, foundDeviceCredentials); deviceService.deleteDevice(tenantId, savedDevice.getId()); } -} + @Test + public void testUpdateDeviceCredentialsWithSameValuesDoesNotPublishEvent() { + Device device = new Device(); + device.setTenantId(tenantId); + device.setName("My device"); + device.setType("default"); + Device savedDevice = deviceService.saveDevice(device); + + try { + DeviceCredentials deviceCredentials = deviceCredentialsService.findDeviceCredentialsByDeviceId(tenantId, savedDevice.getId()); + Assert.assertNotNull(deviceCredentials); + + DeviceCredentials updatedCredentials = new DeviceCredentials(deviceCredentials.getId()); + updatedCredentials.setDeviceId(deviceCredentials.getDeviceId()); + updatedCredentials.setCredentialsType(deviceCredentials.getCredentialsType()); + updatedCredentials.setCredentialsId(deviceCredentials.getCredentialsId()); + updatedCredentials.setCredentialsValue(deviceCredentials.getCredentialsValue()); + + Mockito.reset(eventPublisher); + + DeviceCredentials result = deviceCredentialsService.updateDeviceCredentials(tenantId, updatedCredentials); + + Assert.assertEquals(deviceCredentials.getCredentialsId(), result.getCredentialsId()); + Assert.assertEquals(deviceCredentials.getCredentialsType(), result.getCredentialsType()); + Assert.assertEquals(deviceCredentials.getCredentialsValue(), result.getCredentialsValue()); + Assert.assertEquals(deviceCredentials.getDeviceId(), result.getDeviceId()); + + Mockito.verify(eventPublisher, Mockito.never()).publishEvent(Mockito.any(ActionEntityEvent.class)); + + } finally { + deviceService.deleteDevice(tenantId, savedDevice.getId()); + } + } + +} diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/DeviceServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/DeviceServiceTest.java index bbbd48aa49..1ad1ec082a 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/DeviceServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/DeviceServiceTest.java @@ -16,9 +16,13 @@ package org.thingsboard.server.dao.service; import com.datastax.oss.driver.api.core.uuid.Uuids; +import com.google.common.util.concurrent.ListeningExecutorService; +import com.google.common.util.concurrent.MoreExecutors; import org.junit.After; +import org.junit.AfterClass; import org.junit.Assert; import org.junit.Before; +import org.junit.BeforeClass; import org.junit.Test; import org.junit.jupiter.api.Assertions; import org.mockito.Mockito; @@ -27,6 +31,8 @@ import org.springframework.boot.test.mock.mockito.SpyBean; import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.TransactionStatus; import org.springframework.transaction.support.DefaultTransactionDefinition; +import org.testcontainers.shaded.org.awaitility.Awaitility; +import org.thingsboard.common.util.ThingsBoardThreadFactory; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.DeviceInfo; @@ -74,7 +80,10 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.ArgumentMatchers.any; import static org.thingsboard.server.common.data.ota.OtaPackageType.FIRMWARE; @@ -105,6 +114,17 @@ public class DeviceServiceTest extends AbstractServiceTest { private IdComparator idComparator = new IdComparator<>(); private TenantId anotherTenantId; + private static ListeningExecutorService executor; + + @BeforeClass + public static void beforeClass() { + executor = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(10, ThingsBoardThreadFactory.forName("DeviceServiceTestScope"))); + } + + @AfterClass + public static void afterClass() { + executor.shutdownNow(); + } @Before public void before() { @@ -136,6 +156,31 @@ public class DeviceServiceTest extends AbstractServiceTest { deleteDevice(tenantId, device); } + @Test + public void testDeviceLimitOnTenantProfileLevel() throws InterruptedException { + TenantProfile defaultTenantProfile = tenantProfileService.findDefaultTenantProfile(tenantId); + defaultTenantProfile.getProfileData().setConfiguration(DefaultTenantProfileConfiguration.builder().maxDevices(5l).build()); + tenantProfileService.saveTenantProfile(tenantId, defaultTenantProfile); + + for (int i = 0; i < 20; i++) { + executor.submit(() -> { + Device device = new Device(); + device.setTenantId(tenantId); + device.setName(StringUtils.randomAlphabetic(10)); + device.setType("default"); + deviceService.saveDevice(device, true); + }); + } + + Awaitility.await().atMost(10, TimeUnit.SECONDS).until(() -> { + long countByTenantId = deviceService.countByTenantId(tenantId); + return countByTenantId == 5; + }); + + Thread.sleep(2000); + assertThat(deviceService.countByTenantId(tenantId)).isEqualTo(5); + } + @Test public void testSaveDevicesWithMaxDeviceOutOfLimit() { TenantProfile defaultTenantProfile = tenantProfileService.findDefaultTenantProfile(tenantId); @@ -486,6 +531,17 @@ public class DeviceServiceTest extends AbstractServiceTest { }); } + @Test + public void testSaveDeviceWithJSInjection_thenDataValidationException() { + Device device = new Device(); + device.setType("default"); + device.setTenantId(tenantId); + device.setName("{{constructor.constructor('location.href=\"https://evil.com\"')()}}"); + Assertions.assertThrows(DataValidationException.class, () -> { + deviceService.saveDevice(device); + }); + } + @Test public void testSaveDeviceWithInvalidTenant() { Device device = new Device(); diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/EntityServiceRegistryTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/EntityServiceRegistryTest.java index 9503f4e23f..0e7797ce56 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/EntityServiceRegistryTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/EntityServiceRegistryTest.java @@ -20,7 +20,6 @@ import org.junit.Assert; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; import org.thingsboard.server.common.data.EntityType; -import org.thingsboard.server.dao.cf.CalculatedFieldService; import org.thingsboard.server.dao.entity.EntityDaoService; import org.thingsboard.server.dao.entity.EntityServiceRegistry; import org.thingsboard.server.dao.rule.RuleChainService; @@ -45,8 +44,4 @@ public class EntityServiceRegistryTest extends AbstractServiceTest { Assert.assertTrue(entityServiceRegistry.getServiceByEntityType(EntityType.RULE_NODE) instanceof RuleChainService); } - @Test - public void givenCalculatedFieldLinkEntityType_whenGetServiceByEntityTypeCalled_thenReturnedCalculatedFieldService() { - Assert.assertTrue(entityServiceRegistry.getServiceByEntityType(EntityType.CALCULATED_FIELD_LINK) instanceof CalculatedFieldService); - } } diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/NoXssValidatorTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/NoXssValidatorTest.java index 4877938d89..34da80e2db 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/NoXssValidatorTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/NoXssValidatorTest.java @@ -35,7 +35,14 @@ public class NoXssValidatorTest { "

Link!!!

1221", "

Please log in to proceed

Username:

Password:



", " ", - "123 bebe" + "123 bebe", + "{{constructor.constructor('location.href=\"https://evil.com\"')()}}", + " {{constructor.constructor('alert(1)')()}}", + "{{}}", + "{{{constructor.constructor('location.href=\"https://evil.com\"')()}}}", + "test {{constructor.constructor('location.href=\"https://evil.com\"')()}} test", + "{{#if user}}Hello, {{user.name}}{{/if}}", + "{{ user.name }}" }) public void givenEntityWithMaliciousPropertyValue_thenReturnValidationError(String maliciousString) { Asset invalidAsset = new Asset(); diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/OtaPackageServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/OtaPackageServiceTest.java index 39fb69ffce..70f2ff119d 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/OtaPackageServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/OtaPackageServiceTest.java @@ -53,6 +53,7 @@ import static org.thingsboard.server.common.data.ota.OtaPackageType.FIRMWARE; public class OtaPackageServiceTest extends AbstractServiceTest { public static final String TITLE = "My firmware"; + public static final String TARGET_FW_VERSION = "fw.v.1.5.0-update"; private static final String FILE_NAME = "filename.txt"; private static final String VERSION = "v1.0"; private static final String CONTENT_TYPE = "text/plain"; diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/RelationServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/RelationServiceTest.java index 063e35ae80..e3827d6a6e 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/RelationServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/RelationServiceTest.java @@ -28,13 +28,16 @@ import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.id.AssetId; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.relation.EntityRelation; +import org.thingsboard.server.common.data.relation.EntityRelationPathQuery; import org.thingsboard.server.common.data.relation.EntityRelationsQuery; import org.thingsboard.server.common.data.relation.EntitySearchDirection; import org.thingsboard.server.common.data.relation.RelationEntityTypeFilter; +import org.thingsboard.server.common.data.relation.RelationPathLevel; import org.thingsboard.server.common.data.relation.RelationTypeGroup; import org.thingsboard.server.common.data.relation.RelationsSearchParameters; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.relation.RelationService; +import org.thingsboard.server.dao.tenant.TbTenantProfileCache; import java.util.ArrayList; import java.util.Collections; @@ -42,12 +45,17 @@ import java.util.LinkedList; import java.util.List; import java.util.concurrent.ExecutionException; +import static org.assertj.core.api.Assertions.assertThat; + @DaoSqlTest public class RelationServiceTest extends AbstractServiceTest { @Autowired RelationService relationService; + @Autowired + private TbTenantProfileCache tbTenantProfileCache; + @Before public void before() { } @@ -348,14 +356,14 @@ public class RelationServiceTest extends AbstractServiceTest { query.setFilters(Collections.singletonList(new RelationEntityTypeFilter(EntityRelation.CONTAINS_TYPE, Collections.singletonList(EntityType.ASSET)))); List relations = relationService.findByQuery(SYSTEM_TENANT_ID, query).get(); Assert.assertEquals(expected.size(), relations.size()); - for(EntityRelation r : expected){ + for (EntityRelation r : expected) { Assert.assertTrue(relations.contains(r)); } //Test from cache relations = relationService.findByQuery(SYSTEM_TENANT_ID, query).get(); Assert.assertEquals(expected.size(), relations.size()); - for(EntityRelation r : expected){ + for (EntityRelation r : expected) { Assert.assertTrue(relations.contains(r)); } } @@ -623,6 +631,114 @@ public class RelationServiceTest extends AbstractServiceTest { Assert.assertTrue(relations.contains(relationF)); } + @Test + public void testFindByPathQueryWithoutExceedingLimit() throws Exception { + /* + A + └──[firstLevel, TO]→ B + └──[secondLevel, TO]→ C + ├──[thirdLevel, FROM]→ D1 + ├──[thirdLevel, FROM]→ D2 + ├──[thirdLevel, FROM]→ ... + └──[thirdLevel, FROM]→ D{N - 1}, where N is the limit + */ + AssetId assetA = new AssetId(Uuids.timeBased()); + AssetId assetB = new AssetId(Uuids.timeBased()); + AssetId assetC = new AssetId(Uuids.timeBased()); + + // create first and second level + saveRelation(new EntityRelation(assetB, assetA, "firstLevel")); + saveRelation(new EntityRelation(assetC, assetB, "secondLevel")); + + int limit = tbTenantProfileCache.get(tenantId) + .getDefaultProfileConfiguration() + .getMaxRelatedEntitiesToReturnPerCfArgument(); + + int totalCreated = limit - 1; + + List allThirdLevelRelations = new ArrayList<>(); + for (int i = 0; i < totalCreated; i++) { + AssetId leaf = new AssetId(Uuids.timeBased()); + allThirdLevelRelations.add(saveRelation(new EntityRelation(assetC, leaf, "thirdLevel"))); + } + + EntityRelationPathQuery query = new EntityRelationPathQuery(assetA, List.of( + new RelationPathLevel(EntitySearchDirection.TO, "firstLevel"), + new RelationPathLevel(EntitySearchDirection.TO, "secondLevel"), + new RelationPathLevel(EntitySearchDirection.FROM, "thirdLevel") + )); + + // call a method that applies the default limit internally + List result = relationService.findByRelationPathQueryAsync(tenantId, query).get(); + + // verify that limit has been applied + assertThat(result).hasSize(totalCreated); + + // verify all returned are valid third-level relations under C + assertThat(result) + .allSatisfy(rel -> { + assertThat(rel.getType()).isEqualTo("thirdLevel"); + assertThat(rel.getFrom()).isEqualTo(assetC); + }); + + // verify the returned subset is part of all created relations + assertThat(result).isEqualTo(allThirdLevelRelations); + } + + @Test + public void testFindByPathQueryWithExceedingLimit() throws Exception { + /* + A + └──[firstLevel, TO]→ B + └──[secondLevel, TO]→ C + ├──[thirdLevel, FROM]→ D1 + ├──[thirdLevel, FROM]→ D2 + ├──[thirdLevel, FROM]→ ... + └──[thirdLevel, FROM]→ D{N + 20}, where N is the limit + */ + AssetId assetA = new AssetId(Uuids.timeBased()); + AssetId assetB = new AssetId(Uuids.timeBased()); + AssetId assetC = new AssetId(Uuids.timeBased()); + + // create first and second level + saveRelation(new EntityRelation(assetB, assetA, "firstLevel")); + saveRelation(new EntityRelation(assetC, assetB, "secondLevel")); + + int limit = tbTenantProfileCache.get(tenantId) + .getDefaultProfileConfiguration() + .getMaxRelatedEntitiesToReturnPerCfArgument(); + + int totalCreated = limit + 20; + + List allThirdLevelRelations = new ArrayList<>(); + for (int i = 0; i < totalCreated; i++) { + AssetId leaf = new AssetId(Uuids.timeBased()); + allThirdLevelRelations.add(saveRelation(new EntityRelation(assetC, leaf, "thirdLevel"))); + } + + EntityRelationPathQuery query = new EntityRelationPathQuery(assetA, List.of( + new RelationPathLevel(EntitySearchDirection.TO, "firstLevel"), + new RelationPathLevel(EntitySearchDirection.TO, "secondLevel"), + new RelationPathLevel(EntitySearchDirection.FROM, "thirdLevel") + )); + + // call a method that applies the default limit internally + List result = relationService.findByRelationPathQueryAsync(tenantId, query).get(); + + // verify that limit has been applied + assertThat(result).hasSize(limit); + + // verify all returned are valid third-level relations under C + assertThat(result) + .allSatisfy(rel -> { + assertThat(rel.getType()).isEqualTo("thirdLevel"); + assertThat(rel.getFrom()).isEqualTo(assetC); + }); + + // verify the returned subset is part of all created relations + assertThat(result).isSubsetOf(allThirdLevelRelations); + } + @Test public void testFindByQueryLargeHierarchyFetchAllWithUnlimLvl() throws Exception { AssetId rootAsset = new AssetId(Uuids.timeBased()); diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/validator/CalculatedFieldDataValidatorTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/validator/CalculatedFieldDataValidatorTest.java index 43fb44431e..0d52f999de 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/validator/CalculatedFieldDataValidatorTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/validator/CalculatedFieldDataValidatorTest.java @@ -17,8 +17,8 @@ package org.thingsboard.server.dao.service.validator; import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.mock.mockito.MockBean; -import org.springframework.boot.test.mock.mockito.SpyBean; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.springframework.test.context.bean.override.mockito.MockitoSpyBean; import org.thingsboard.server.common.data.cf.CalculatedField; import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.id.CalculatedFieldId; @@ -38,11 +38,11 @@ public class CalculatedFieldDataValidatorTest { private final TenantId TENANT_ID = TenantId.fromUUID(UUID.fromString("7b5229e9-166e-41a9-a257-3b1dafad1b04")); private final CalculatedFieldId CALCULATED_FIELD_ID = new CalculatedFieldId(UUID.fromString("060fbe45-fbb2-4549-abf3-f72a6be3cb9f")); - @MockBean + @MockitoBean private CalculatedFieldDao calculatedFieldDao; - @MockBean + @MockitoBean private DefaultApiLimitService apiLimitService; - @SpyBean + @MockitoSpyBean private CalculatedFieldDataValidator validator; @Test diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/validator/CalculatedFieldLinkDataValidatorTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/validator/CalculatedFieldLinkDataValidatorTest.java deleted file mode 100644 index c477498602..0000000000 --- a/dao/src/test/java/org/thingsboard/server/dao/service/validator/CalculatedFieldLinkDataValidatorTest.java +++ /dev/null @@ -1,57 +0,0 @@ -/** - * Copyright © 2016-2025 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.dao.service.validator; - -import org.junit.jupiter.api.Test; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.mock.mockito.MockBean; -import org.springframework.boot.test.mock.mockito.SpyBean; -import org.thingsboard.server.common.data.cf.CalculatedFieldLink; -import org.thingsboard.server.common.data.id.CalculatedFieldId; -import org.thingsboard.server.common.data.id.CalculatedFieldLinkId; -import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.dao.cf.CalculatedFieldLinkDao; -import org.thingsboard.server.dao.exception.DataValidationException; - -import java.util.UUID; - -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.mockito.BDDMockito.given; - -@SpringBootTest(classes = CalculatedFieldLinkDataValidator.class) -public class CalculatedFieldLinkDataValidatorTest { - - private final TenantId TENANT_ID = TenantId.fromUUID(UUID.fromString("2ba09d99-6143-43dc-b645-381fc0c43ebe")); - private final CalculatedFieldLinkId CALCULATED_FIELD_LINK_ID = new CalculatedFieldLinkId(UUID.fromString("a5609ef4-cb42-43ce-9b23-e090a4878d1c")); - - @MockBean - private CalculatedFieldLinkDao calculatedFieldLinkDao; - @SpyBean - private CalculatedFieldLinkDataValidator validator; - - @Test - public void testUpdateNonExistingCalculatedField() { - CalculatedFieldLink calculatedFieldLink = new CalculatedFieldLink(CALCULATED_FIELD_LINK_ID); - calculatedFieldLink.setCalculatedFieldId(new CalculatedFieldId(UUID.fromString("136477af-fd07-4498-b9c9-54fe50e82992"))); - - given(calculatedFieldLinkDao.findById(TENANT_ID, CALCULATED_FIELD_LINK_ID.getId())).willReturn(null); - - assertThatThrownBy(() -> validator.validateUpdate(TENANT_ID, calculatedFieldLink)) - .isInstanceOf(DataValidationException.class) - .hasMessage("Can't update non existing calculated field link!"); - } - -} diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/validator/DeviceProfileDataValidatorTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/validator/DeviceProfileDataValidatorTest.java index b69165be7c..f9d6c035dc 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/validator/DeviceProfileDataValidatorTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/validator/DeviceProfileDataValidatorTest.java @@ -48,6 +48,9 @@ import java.util.UUID; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.BDDMockito.willReturn; import static org.mockito.Mockito.verify; +import static org.thingsboard.server.common.data.device.credentials.lwm2m.Lwm2mServerIdentifier.LWM2M_SERVER_MAX; +import static org.thingsboard.server.common.data.device.credentials.lwm2m.Lwm2mServerIdentifier.NOT_USED_IDENTIFYING_LWM2M_SERVER_MAX; +import static org.thingsboard.server.common.data.device.credentials.lwm2m.Lwm2mServerIdentifier.PRIMARY_LWM2M_SERVER; @SpringBootTest(classes = DeviceProfileDataValidator.class) class DeviceProfileDataValidatorTest { @@ -74,8 +77,8 @@ class DeviceProfileDataValidatorTest { " \"clientOnlyObserveAfterConnect\": 1\n" + " }"; - private static final String msgErrorLwm2mRange = "LwM2M Server ShortServerId must be in range [1 - 65534]!"; - private static final String msgErrorBsRange = "Bootstrap Server ShortServerId must be in range [0 - 65535]!"; + private static final String msgErrorLwm2mRange = "LwM2M Server ShortServerId must be in range [" + PRIMARY_LWM2M_SERVER.getId() + " - " + LWM2M_SERVER_MAX.getId() + "]!"; + private static final String msgErrorBsRange = "Bootstrap Server ShortServerId must be null!"; private static final String msgErrorNotNull = " Server ShortServerId must not be null!"; private static final String host = "localhost"; private static final String hostBs = "localhost"; @@ -124,7 +127,7 @@ class DeviceProfileDataValidatorTest { @Test void testValidateDeviceProfile_Lwm2mBootstrap_ShortServerId_Ok() { Integer shortServerId = 123; - Integer shortServerIdBs = 0; + Integer shortServerIdBs = null; DeviceProfile deviceProfile = getDeviceProfile(shortServerId, shortServerIdBs); validator.validateDataImpl(tenantId, deviceProfile); @@ -132,8 +135,13 @@ class DeviceProfileDataValidatorTest { } @Test - void testValidateDeviceProfile_Lwm2mShortServerId_Ok_BootstrapShortServerId_null_Error() { - verifyValidationError(123, null, "Bootstrap" + msgErrorNotNull); + void testValidateDeviceProfile_Lwm2mShortServerId_Ok_BootstrapShortServerId_validate_0_to_null_Ok() { + Integer shortServerId = 123; + Integer shortServerIdBs = 0; + DeviceProfile deviceProfile = getDeviceProfile(shortServerId, shortServerIdBs); + + validator.validateDataImpl(tenantId, deviceProfile); + verify(validator).validateString("Device profile name", deviceProfile.getName()); } @Test @@ -153,7 +161,7 @@ class DeviceProfileDataValidatorTest { @Test void testValidateDeviceProfile_Lwm2mShortServerId_More_65534_Error_BootstrapShortServerId_Ok() { - verifyValidationError(65535, 111, msgErrorLwm2mRange); + verifyValidationError(NOT_USED_IDENTIFYING_LWM2M_SERVER_MAX.getId(), 111, msgErrorLwm2mRange); } @Test diff --git a/dao/src/test/java/org/thingsboard/server/dao/sql/rule/JpaRuleNodeDaoTest.java b/dao/src/test/java/org/thingsboard/server/dao/sql/rule/JpaRuleNodeDaoTest.java index f5aa8a3af5..59c0db9eee 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/sql/rule/JpaRuleNodeDaoTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/sql/rule/JpaRuleNodeDaoTest.java @@ -24,6 +24,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.RuleNodeId; +import org.thingsboard.server.common.data.id.TbResourceId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; diff --git a/dao/src/test/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDaoPartitioningMinutesAlwaysExistsTest.java b/dao/src/test/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDaoPartitioningMinutesAlwaysExistsTest.java index cc76484d0b..8ec8c493af 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDaoPartitioningMinutesAlwaysExistsTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDaoPartitioningMinutesAlwaysExistsTest.java @@ -117,4 +117,16 @@ public class CassandraBaseTimeseriesDaoPartitioningMinutesAlwaysExistsTest { ISO_8601_EXTENDED_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-10T00:10:00Z").getTime())); } + @Test + public void testEstimatePartitionCount() throws ParseException { + assertThat(tsDao.estimatePartitionCount(0, Long.MAX_VALUE)).as("centuries").isEqualTo(153_722_867_280_914L); + assertThat(tsDao.estimatePartitionCount(0, 0)).as("single").isEqualTo(1L); + long startTs = tsDao.toPartitionTs( + ISO_8601_EXTENDED_DATETIME_TIME_ZONE_FORMAT.parse("2019-12-12T00:00:00Z").getTime()); + long endTs = tsDao.toPartitionTs( + ISO_8601_EXTENDED_DATETIME_TIME_ZONE_FORMAT.parse("2021-01-31T23:59:59Z").getTime()); + assertThat(tsDao.estimatePartitionCount(startTs, endTs)).as("600,479 minutes + 2 spare periods").isEqualTo(600479 + 2); + assertThat(tsDao.estimatePartitionCount(endTs, startTs)).as("wrong period estimated as 1").isEqualTo(1L); + } + } diff --git a/dao/src/test/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDaoPartitioningMonthsAlwaysExistsTest.java b/dao/src/test/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDaoPartitioningMonthsAlwaysExistsTest.java index eb60000404..65ed8019c0 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDaoPartitioningMonthsAlwaysExistsTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDaoPartitioningMonthsAlwaysExistsTest.java @@ -15,25 +15,35 @@ */ package org.thingsboard.server.dao.timeseries; +import com.google.common.util.concurrent.ListenableFuture; import lombok.extern.slf4j.Slf4j; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Answers; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.bean.override.mockito.MockitoSpyBean; import org.springframework.test.context.junit4.SpringRunner; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.kv.TsKvQuery; import org.thingsboard.server.dao.cassandra.CassandraCluster; import org.thingsboard.server.dao.nosql.CassandraBufferedRateReadExecutor; import org.thingsboard.server.dao.nosql.CassandraBufferedRateWriteExecutor; import java.text.ParseException; import java.util.List; +import java.util.UUID; import static org.apache.commons.lang3.time.DateFormatUtils.ISO_8601_EXTENDED_DATETIME_TIME_ZONE_FORMAT; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.willReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; @RunWith(SpringRunner.class) @SpringBootTest(classes = CassandraBaseTimeseriesDao.class) @@ -50,7 +60,7 @@ import static org.assertj.core.api.Assertions.assertThat; @Slf4j public class CassandraBaseTimeseriesDaoPartitioningMonthsAlwaysExistsTest { - @Autowired + @MockitoSpyBean CassandraBaseTimeseriesDao tsDao; @MockBean(answer = Answers.RETURNS_MOCKS) @@ -131,4 +141,53 @@ public class CassandraBaseTimeseriesDaoPartitioningMonthsAlwaysExistsTest { ISO_8601_EXTENDED_DATETIME_TIME_ZONE_FORMAT.parse("2021-01-01T00:00:00Z").getTime())); } + @Test + public void testEstimatePartitionCount() throws ParseException { + assertThat(tsDao.estimatePartitionCount(0, Long.MAX_VALUE)).as("centuries").isEqualTo(3_507_324_297L); + assertThat(tsDao.estimatePartitionCount(0, 0)).as("single").isEqualTo(1L); + long startTs = tsDao.toPartitionTs( + ISO_8601_EXTENDED_DATETIME_TIME_ZONE_FORMAT.parse("2019-12-12T00:00:00Z").getTime()); + long endTs = tsDao.toPartitionTs( + ISO_8601_EXTENDED_DATETIME_TIME_ZONE_FORMAT.parse("2021-01-31T23:59:59Z").getTime()); + assertThat(tsDao.estimatePartitionCount(startTs, endTs)).as("13 month + 2 spare periods").isEqualTo(13 + 2); + assertThat(tsDao.estimatePartitionCount(endTs, startTs)).as("wrong period estimated as 1").isEqualTo(1L); + } + + @Test + public void testGetPartitionsFutureModeratePartitionsCount() throws ParseException { + TenantId tenantId = TenantId.fromUUID(UUID.randomUUID()); + TsKvQuery query = mock(TsKvQuery.class); + long startTs = tsDao.toPartitionTs( + ISO_8601_EXTENDED_DATETIME_TIME_ZONE_FORMAT.parse("2019-12-12T00:00:00Z").getTime()); + long endTs = tsDao.toPartitionTs( + ISO_8601_EXTENDED_DATETIME_TIME_ZONE_FORMAT.parse("2021-01-31T23:59:59Z").getTime()); + + willReturn(mock(ListenableFuture.class)).given(tsDao).getPartitionsFromDB(tenantId, query, tenantId, startTs, endTs); + + tsDao.getPartitionsFuture(tenantId, query, tenantId, startTs, endTs); + + verify(tsDao).estimatePartitionCount(startTs, endTs); + verify(tsDao).calculatePartitions(eq(startTs), eq(endTs), anyInt()); + verify(tsDao, never()).getPartitionsFromDB(tenantId, query, tenantId, startTs, endTs); + } + + @Test + public void testGetPartitionsFutureHugePartitionsCountPreventOOMFallbackToDB() throws ParseException { + + TenantId tenantId = TenantId.fromUUID(UUID.randomUUID()); + TsKvQuery query = mock(TsKvQuery.class); + long startTs = tsDao.toPartitionTs( + ISO_8601_EXTENDED_DATETIME_TIME_ZONE_FORMAT.parse("2000-12-12T00:00:00Z").getTime()); + long endTs = tsDao.toPartitionTs( + ISO_8601_EXTENDED_DATETIME_TIME_ZONE_FORMAT.parse("3000-01-31T23:59:59Z").getTime()); + + willReturn(mock(ListenableFuture.class)).given(tsDao).getPartitionsFromDB(tenantId, query, tenantId, startTs, endTs); + + tsDao.getPartitionsFuture(tenantId, query, tenantId, startTs, endTs); + + verify(tsDao).estimatePartitionCount(startTs, endTs); + verify(tsDao, never()).calculatePartitions(eq(startTs), eq(endTs), anyInt()); + verify(tsDao).getPartitionsFromDB(tenantId, query, tenantId, startTs, endTs); + } + } diff --git a/dao/src/test/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDaoPartitioningYearsAlwaysExistsTest.java b/dao/src/test/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDaoPartitioningYearsAlwaysExistsTest.java index 0425b23a1e..aa3e48c73f 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDaoPartitioningYearsAlwaysExistsTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDaoPartitioningYearsAlwaysExistsTest.java @@ -112,4 +112,16 @@ public class CassandraBaseTimeseriesDaoPartitioningYearsAlwaysExistsTest { ISO_8601_EXTENDED_DATETIME_TIME_ZONE_FORMAT.parse("2025-01-01T00:00:00Z").getTime())); } + @Test + public void testEstimatePartitionCount() throws ParseException { + assertThat(tsDao.estimatePartitionCount(0, Long.MAX_VALUE)).as("centuries").isEqualTo(292_277_026L); + assertThat(tsDao.estimatePartitionCount(0, 0)).as("single").isEqualTo(1L); + long startTs = tsDao.toPartitionTs( + ISO_8601_EXTENDED_DATETIME_TIME_ZONE_FORMAT.parse("2019-12-12T00:00:00Z").getTime()); + long endTs = tsDao.toPartitionTs( + ISO_8601_EXTENDED_DATETIME_TIME_ZONE_FORMAT.parse("2021-01-31T23:59:59Z").getTime()); + assertThat(tsDao.estimatePartitionCount(startTs, endTs)).as("2 years + 2 spare periods").isEqualTo(2 + 2); + assertThat(tsDao.estimatePartitionCount(endTs, startTs)).as("wrong period estimated as 1").isEqualTo(1L); + } + } diff --git a/dao/src/test/java/org/thingsboard/server/dao/timeseries/NoSqlTsPartitionDateTest.java b/dao/src/test/java/org/thingsboard/server/dao/timeseries/NoSqlTsPartitionDateTest.java new file mode 100644 index 0000000000..f860d6ad51 --- /dev/null +++ b/dao/src/test/java/org/thingsboard/server/dao/timeseries/NoSqlTsPartitionDateTest.java @@ -0,0 +1,41 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.timeseries; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; + +import static org.assertj.core.api.Assertions.assertThat; + +class NoSqlTsPartitionDateTest { + + @ParameterizedTest + @EnumSource(NoSqlTsPartitionDate.class) + void getDurationMsTest(NoSqlTsPartitionDate tsPartitionDate) throws Exception { + final Long durationMs = switch (tsPartitionDate) { + case MINUTES -> 60000L; + case HOURS -> 3600000L; + case DAYS -> 86400000L; + case MONTHS -> 2629746000L; + case YEARS -> 31556952000L; + case INDEFINITE -> Long.MAX_VALUE; + default -> null; //should be here in case a new enum value will be added in future + }; + assertThat(durationMs).isNotNull(); + assertThat(tsPartitionDate.getDurationMs()).isEqualTo(durationMs); + } + +} diff --git a/docker/docker-compose.kafka.yml b/docker/docker-compose.kafka.yml index 026070cc89..06bafafc61 100644 --- a/docker/docker-compose.kafka.yml +++ b/docker/docker-compose.kafka.yml @@ -17,7 +17,7 @@ services: kafka: restart: always - image: "bitnami/kafka:4.0" + image: "bitnamilegacy/kafka:4.0" ports: - "9092:9092" env_file: diff --git a/docker/docker-compose.valkey-cluster.yml b/docker/docker-compose.valkey-cluster.yml index 242fabf44b..b8ce0f96ec 100644 --- a/docker/docker-compose.valkey-cluster.yml +++ b/docker/docker-compose.valkey-cluster.yml @@ -18,7 +18,7 @@ services: # Valkey cluster # The latest version of Valkey compatible with ThingsBoard is 8.0 valkey-node-0: - image: bitnami/valkey-cluster:8.0 + image: bitnamilegacy/valkey-cluster:8.0 volumes: - ./tb-node/valkey-cluster-data-0:/bitnami/valkey/data environment: @@ -26,7 +26,7 @@ services: - 'VALKEY_NODES=valkey-node-0 valkey-node-1 valkey-node-2 valkey-node-3 valkey-node-4 valkey-node-5' valkey-node-1: - image: bitnami/valkey-cluster:8.0 + image: bitnamilegacy/valkey-cluster:8.0 volumes: - ./tb-node/valkey-cluster-data-1:/bitnami/valkey/data depends_on: @@ -36,7 +36,7 @@ services: - 'VALKEY_NODES=valkey-node-0 valkey-node-1 valkey-node-2 valkey-node-3 valkey-node-4 valkey-node-5' valkey-node-2: - image: bitnami/valkey-cluster:8.0 + image: bitnamilegacy/valkey-cluster:8.0 volumes: - ./tb-node/valkey-cluster-data-2:/bitnami/valkey/data depends_on: @@ -46,7 +46,7 @@ services: - 'VALKEY_NODES=valkey-node-0 valkey-node-1 valkey-node-2 valkey-node-3 valkey-node-4 valkey-node-5' valkey-node-3: - image: bitnami/valkey-cluster:8.0 + image: bitnamilegacy/valkey-cluster:8.0 volumes: - ./tb-node/valkey-cluster-data-3:/bitnami/valkey/data depends_on: @@ -56,7 +56,7 @@ services: - 'VALKEY_NODES=valkey-node-0 valkey-node-1 valkey-node-2 valkey-node-3 valkey-node-4 valkey-node-5' valkey-node-4: - image: bitnami/valkey-cluster:8.0 + image: bitnamilegacy/valkey-cluster:8.0 volumes: - ./tb-node/valkey-cluster-data-4:/bitnami/valkey/data depends_on: @@ -66,7 +66,7 @@ services: - 'VALKEY_NODES=valkey-node-0 valkey-node-1 valkey-node-2 valkey-node-3 valkey-node-4 valkey-node-5' valkey-node-5: - image: bitnami/valkey-cluster:8.0 + image: bitnamilegacy/valkey-cluster:8.0 volumes: - ./tb-node/valkey-cluster-data-5:/bitnami/valkey/data depends_on: diff --git a/docker/docker-compose.valkey-sentinel.yml b/docker/docker-compose.valkey-sentinel.yml index 3827e0358b..e528a1bb9a 100644 --- a/docker/docker-compose.valkey-sentinel.yml +++ b/docker/docker-compose.valkey-sentinel.yml @@ -18,7 +18,7 @@ services: # Valkey sentinel # The latest version of Valkey compatible with ThingsBoard is 8.0 valkey-primary: - image: 'bitnami/valkey:8.0' + image: 'bitnamilegacy/valkey:8.0' volumes: - ./tb-node/valkey-sentinel-data-primary:/bitnami/valkey/data environment: @@ -26,7 +26,7 @@ services: - 'VALKEY_PASSWORD=thingsboard' valkey-replica: - image: 'bitnami/valkey:8.0' + image: 'bitnamilegacy/valkey:8.0' volumes: - ./tb-node/valkey-sentinel-data-replica:/bitnami/valkey/data environment: @@ -38,7 +38,7 @@ services: - valkey-primary valkey-sentinel: - image: 'bitnami/valkey-sentinel:8.0' + image: 'bitnamilegacy/valkey-sentinel:8.0' volumes: - ./tb-node/valkey-sentinel-data-sentinel:/bitnami/valkey/data environment: diff --git a/docker/docker-compose.valkey.yml b/docker/docker-compose.valkey.yml index d4a620820d..3eba086a9b 100644 --- a/docker/docker-compose.valkey.yml +++ b/docker/docker-compose.valkey.yml @@ -19,7 +19,7 @@ services: # The latest version of Valkey compatible with ThingsBoard is 8.0 valkey: restart: always - image: bitnami/valkey:8.0 + image: bitnamilegacy/valkey:8.0 environment: # ALLOW_EMPTY_PASSWORD is recommended only for development. ALLOW_EMPTY_PASSWORD: "yes" diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/TestRestClient.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/TestRestClient.java index 102f4b82ea..7a4d3f1cfe 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/TestRestClient.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/TestRestClient.java @@ -16,6 +16,7 @@ package org.thingsboard.server.msa; import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ObjectNode; import io.restassured.RestAssured; import io.restassured.common.mapper.TypeRef; @@ -231,9 +232,9 @@ public class TestRestClient { .statusCode(anyOf(is(HTTP_OK), is(HTTP_NOT_FOUND))); } - public ValidatableResponse postTelemetryAttribute(EntityId entityId, String scope, JsonNode attribute) { + public ValidatableResponse postTelemetryAttribute(EntityId entityId, AttributeScope scope, JsonNode attribute) { return given().spec(requestSpec).body(attribute) - .post("/api/plugins/telemetry/{entityType}/{entityId}/attributes/{scope}", entityId.getEntityType(), entityId.getId(), scope) + .post("/api/plugins/telemetry/{entityType}/{entityId}/attributes/{scope}", entityId.getEntityType(), entityId.getId(), scope.name()) .then() .statusCode(HTTP_OK); } @@ -256,13 +257,40 @@ public class TestRestClient { .as(JsonNode.class); } - public JsonNode getAttributes(EntityId entityId, AttributeScope scope, String keys) { + public ArrayNode getAttributes(EntityId entityId, AttributeScope scope, String keys) { return given().spec(requestSpec) .get("/api/plugins/telemetry/{entityType}/{entityId}/values/attributes/{scope}?keys={keys}", entityId.getEntityType(), entityId.getId(), scope, keys) .then() .statusCode(HTTP_OK) .extract() - .as(JsonNode.class); + .as(ArrayNode.class); + } + + + public ValidatableResponse deleteEntityAttributes(EntityId entityId, AttributeScope scope, String keys) { + Map pathParams = new HashMap<>(); + pathParams.put("entityId", entityId.getId().toString()); + pathParams.put("entityType", entityId.getEntityType().name()); + pathParams.put("scope", scope.name()); + return given().spec(requestSpec) + .pathParams(pathParams) + .queryParam("keys", keys) + .delete("/api/plugins/telemetry/{entityType}/{entityId}/{scope}") + .then() + .statusCode(HTTP_OK); + } + + public ValidatableResponse deleteEntityTimeseries(EntityId entityId, String keys, boolean deleteAllDataForKeys) { + Map pathParams = new HashMap<>(); + pathParams.put("entityType", entityId.getEntityType().name()); + pathParams.put("entityId", entityId.getId().toString()); + return given().spec(requestSpec) + .pathParams(pathParams) + .queryParam("keys", keys) + .queryParam("deleteAllDataForKeys", Boolean.toString(deleteAllDataForKeys)) + .delete("/api/plugins/telemetry/{entityType}/{entityId}/timeseries/delete") + .then() + .statusCode(HTTP_OK); } public JsonNode getLatestTelemetry(EntityId entityId) { @@ -367,6 +395,33 @@ public class TestRestClient { }); } + public EntityRelation postEntityRelation(EntityRelation entityRelation) { + return given().spec(requestSpec) + .body(entityRelation) + .post("/api/v2/relation") + .then() + .statusCode(HTTP_OK) + .extract() + .as(EntityRelation.class); + } + + + public EntityRelation deleteEntityRelation(EntityId fromId, String relationType, EntityId toId) { + Map queryParams = new HashMap<>(); + queryParams.put("fromId", fromId.getId().toString()); + queryParams.put("fromType", fromId.getEntityType().name()); + queryParams.put("relationType", relationType); + queryParams.put("toId", toId.getId().toString()); + queryParams.put("toType", toId.getEntityType().name()); + return given().spec(requestSpec) + .queryParams(queryParams) + .delete("/api/v2/relation") + .then() + .statusCode(HTTP_OK) + .extract() + .as(EntityRelation.class); + } + public JsonNode postServerSideRpc(DeviceId deviceId, JsonNode serverRpcPayload) { return given().spec(requestSpec) .body(serverRpcPayload) diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/cf/CalculatedFieldTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/cf/CalculatedFieldTest.java index 2593e18a55..9c043eee8d 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/cf/CalculatedFieldTest.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/cf/CalculatedFieldTest.java @@ -16,14 +16,14 @@ package org.thingsboard.server.msa.cf; import com.fasterxml.jackson.databind.JsonNode; -import org.testcontainers.shaded.org.apache.commons.lang3.RandomStringUtils; +import com.fasterxml.jackson.databind.node.ArrayNode; +import org.apache.commons.lang3.RandomStringUtils; import org.testng.annotations.AfterClass; import org.testng.annotations.BeforeClass; import org.testng.annotations.BeforeMethod; import org.testng.annotations.Test; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.AttributeScope; -import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.asset.Asset; @@ -33,9 +33,14 @@ import org.thingsboard.server.common.data.cf.configuration.Argument; import org.thingsboard.server.common.data.cf.configuration.ArgumentType; import org.thingsboard.server.common.data.cf.configuration.Output; import org.thingsboard.server.common.data.cf.configuration.OutputType; +import org.thingsboard.server.common.data.cf.configuration.PropagationCalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey; +import org.thingsboard.server.common.data.cf.configuration.RelationPathQueryDynamicSourceConfiguration; import org.thingsboard.server.common.data.cf.configuration.ScriptCalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.SimpleCalculatedFieldConfiguration; +import org.thingsboard.server.common.data.cf.configuration.geofencing.EntityCoordinates; +import org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingCalculatedFieldConfiguration; +import org.thingsboard.server.common.data.cf.configuration.geofencing.ZoneGroupConfiguration; import org.thingsboard.server.common.data.debug.DebugSettings; import org.thingsboard.server.common.data.device.data.DefaultDeviceConfiguration; import org.thingsboard.server.common.data.device.data.DefaultDeviceTransportConfiguration; @@ -45,14 +50,21 @@ import org.thingsboard.server.common.data.id.DeviceProfileId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.UserId; +import org.thingsboard.server.common.data.relation.EntityRelation; +import org.thingsboard.server.common.data.relation.EntitySearchDirection; +import org.thingsboard.server.common.data.relation.RelationPathLevel; import org.thingsboard.server.msa.AbstractContainerTest; import org.thingsboard.server.msa.ui.utils.EntityPrototypes; +import java.util.HashMap; +import java.util.List; import java.util.Map; 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.AttributeScope.SERVER_SCOPE; +import static org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingReportStrategy.REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS; import static org.thingsboard.server.msa.ui.utils.EntityPrototypes.defaultAssetProfile; import static org.thingsboard.server.msa.ui.utils.EntityPrototypes.defaultDeviceProfile; import static org.thingsboard.server.msa.ui.utils.EntityPrototypes.defaultTenantAdmin; @@ -65,17 +77,17 @@ public class CalculatedFieldTest extends AbstractContainerTest { private final String deviceToken = "zmzURIVRsq3lvnTP2XBE"; private final String exampleScript = "var avgTemperature = temperature.mean(); // Get average temperature\n" + - " var temperatureK = (avgTemperature - 32) * (5 / 9) + 273.15; // Convert Fahrenheit to Kelvin\n" + - "\n" + - " // Estimate air pressure based on altitude\n" + - " var pressure = 101325 * Math.pow((1 - 2.25577e-5 * altitude), 5.25588);\n" + - "\n" + - " // Air density formula\n" + - " var airDensity = pressure / (287.05 * temperatureK);\n" + - "\n" + - " return {\n" + - " \"airDensity\": toFixed(airDensity, 2)\n" + - " };"; + " var temperatureK = (avgTemperature - 32) * (5 / 9) + 273.15; // Convert Fahrenheit to Kelvin\n" + + "\n" + + " // Estimate air pressure based on altitude\n" + + " var pressure = 101325 * Math.pow((1 - 2.25577e-5 * altitude), 5.25588);\n" + + "\n" + + " // Air density formula\n" + + " var airDensity = pressure / (287.05 * temperatureK);\n" + + "\n" + + " return {\n" + + " \"airDensity\": toFixed(airDensity, 2)\n" + + " };"; private TenantId tenantId; private UserId tenantAdminId; @@ -98,13 +110,13 @@ public class CalculatedFieldTest extends AbstractContainerTest { asset = testRestClient.postAsset(createAsset("Asset 1", assetProfileId)); testRestClient.postTelemetry(deviceToken, JacksonUtil.toJsonNode("{\"temperature\":25}")); - testRestClient.postTelemetryAttribute(device.getId(), DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode("{\"deviceTemperature\":40}")); + testRestClient.postTelemetryAttribute(device.getId(), SERVER_SCOPE, JacksonUtil.toJsonNode("{\"deviceTemperature\":40}")); testRestClient.postTelemetry(deviceToken, JacksonUtil.toJsonNode("{\"temperatureInF\":72.32}")); testRestClient.postTelemetry(deviceToken, JacksonUtil.toJsonNode("{\"temperatureInF\":72.86}")); testRestClient.postTelemetry(deviceToken, JacksonUtil.toJsonNode("{\"temperatureInF\":73.58}")); - testRestClient.postTelemetryAttribute(asset.getId(), DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode("{\"altitude\":1035}")); + testRestClient.postTelemetryAttribute(asset.getId(), SERVER_SCOPE, JacksonUtil.toJsonNode("{\"altitude\":1035}")); } @BeforeMethod @@ -133,6 +145,7 @@ public class CalculatedFieldTest extends AbstractContainerTest { .untilAsserted(() -> { JsonNode fahrenheitTemp = testRestClient.getLatestTelemetry(device.getId()); assertThat(fahrenheitTemp).isNotNull(); + assertThat(fahrenheitTemp.get("fahrenheitTemp")).isNotNull(); assertThat(fahrenheitTemp.get("fahrenheitTemp").get(0).get("value").asText()).isEqualTo("77.0"); }); @@ -145,9 +158,10 @@ public class CalculatedFieldTest extends AbstractContainerTest { testRestClient.getAndSetUserToken(tenantAdminId); CalculatedField savedCalculatedField = createSimpleCalculatedField(); + assertThat(savedCalculatedField.getConfiguration() instanceof SimpleCalculatedFieldConfiguration).isTrue(); - Argument savedArgument = savedCalculatedField.getConfiguration().getArguments().get("T"); - savedArgument.setRefEntityKey(new ReferencedEntityKey("deviceTemperature", ArgumentType.ATTRIBUTE, AttributeScope.SERVER_SCOPE)); + Argument savedArgument = ((SimpleCalculatedFieldConfiguration) savedCalculatedField.getConfiguration()).getArguments().get("T"); + savedArgument.setRefEntityKey(new ReferencedEntityKey("deviceTemperature", ArgumentType.ATTRIBUTE, SERVER_SCOPE)); testRestClient.postCalculatedField(savedCalculatedField); await().alias("update CF argument -> perform calculation with new argument").atMost(TIMEOUT, TimeUnit.SECONDS) @@ -155,6 +169,7 @@ public class CalculatedFieldTest extends AbstractContainerTest { .untilAsserted(() -> { JsonNode fahrenheitTemp = testRestClient.getLatestTelemetry(device.getId()); assertThat(fahrenheitTemp).isNotNull(); + assertThat(fahrenheitTemp.get("fahrenheitTemp")).isNotNull(); assertThat(fahrenheitTemp.get("fahrenheitTemp").get(0).get("value").asText()).isEqualTo("104.0"); }); @@ -170,15 +185,16 @@ public class CalculatedFieldTest extends AbstractContainerTest { Output savedOutput = savedCalculatedField.getConfiguration().getOutput(); savedOutput.setType(OutputType.ATTRIBUTES); - savedOutput.setScope(AttributeScope.SERVER_SCOPE); + savedOutput.setScope(SERVER_SCOPE); savedOutput.setName("temperatureF"); testRestClient.postCalculatedField(savedCalculatedField); await().alias("update CF output -> perform calculation with updated output").atMost(TIMEOUT, TimeUnit.SECONDS) .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) .untilAsserted(() -> { - JsonNode temperatureF = testRestClient.getAttributes(device.getId(), AttributeScope.SERVER_SCOPE, "temperatureF"); + ArrayNode temperatureF = testRestClient.getAttributes(device.getId(), SERVER_SCOPE, "temperatureF"); assertThat(temperatureF).isNotNull(); + assertThat(temperatureF.get(0)).isNotNull(); assertThat(temperatureF.get(0).get("value").asText()).isEqualTo("77.0"); }); @@ -191,9 +207,10 @@ public class CalculatedFieldTest extends AbstractContainerTest { testRestClient.getAndSetUserToken(tenantAdminId); CalculatedField savedCalculatedField = createSimpleCalculatedField(); + assertThat(savedCalculatedField.getConfiguration() instanceof SimpleCalculatedFieldConfiguration).isTrue(); savedCalculatedField.setName("F to C"); - savedCalculatedField.getConfiguration().setExpression("(T - 32) / 1.8"); + ((SimpleCalculatedFieldConfiguration) savedCalculatedField.getConfiguration()).setExpression("(T - 32) / 1.8"); testRestClient.postCalculatedField(savedCalculatedField); await().alias("update CF expression -> perform calculation with new expression").atMost(TIMEOUT, TimeUnit.SECONDS) @@ -201,6 +218,7 @@ public class CalculatedFieldTest extends AbstractContainerTest { .untilAsserted(() -> { JsonNode fahrenheitTemp = testRestClient.getLatestTelemetry(device.getId()); assertThat(fahrenheitTemp).isNotNull(); + assertThat(fahrenheitTemp.get("fahrenheitTemp")).isNotNull(); assertThat(fahrenheitTemp.get("fahrenheitTemp").get(0).get("value").asText()).isEqualTo("-3.89"); }); @@ -221,6 +239,7 @@ public class CalculatedFieldTest extends AbstractContainerTest { .untilAsserted(() -> { JsonNode fahrenheitTemp = testRestClient.getLatestTelemetry(device.getId()); assertThat(fahrenheitTemp).isNotNull(); + assertThat(fahrenheitTemp.get("fahrenheitTemp")).isNotNull(); assertThat(fahrenheitTemp.get("fahrenheitTemp").get(0).get("value").asText()).isEqualTo("86.0"); }); @@ -239,6 +258,7 @@ public class CalculatedFieldTest extends AbstractContainerTest { .untilAsserted(() -> { JsonNode fahrenheitTemp = testRestClient.getLatestTelemetry(device.getId()); assertThat(fahrenheitTemp).isNotNull(); + assertThat(fahrenheitTemp.get("fahrenheitTemp")).isNotNull(); assertThat(fahrenheitTemp.get("fahrenheitTemp").get(0).get("value").asText()).isEqualTo("77.0"); }); @@ -261,6 +281,7 @@ public class CalculatedFieldTest extends AbstractContainerTest { // used default value since telemetry is not present JsonNode fahrenheitTemp = testRestClient.getLatestTelemetry(newDevice.getId()); assertThat(fahrenheitTemp).isNotNull(); + assertThat(fahrenheitTemp.get("fahrenheitTemp")).isNotNull(); assertThat(fahrenheitTemp.get("fahrenheitTemp").get(0).get("value").asText()).isEqualTo("53.6"); }); @@ -275,6 +296,7 @@ public class CalculatedFieldTest extends AbstractContainerTest { .untilAsserted(() -> { JsonNode fahrenheitTemp = testRestClient.getLatestTelemetry(newDevice.getId()); assertThat(fahrenheitTemp).isNotNull(); + assertThat(fahrenheitTemp.get("fahrenheitTemp")).isNotNull(); assertThat(fahrenheitTemp.get("fahrenheitTemp").get(0).get("value").asText()).isEqualTo("53.6"); }); @@ -286,29 +308,290 @@ public class CalculatedFieldTest extends AbstractContainerTest { // login tenant admin testRestClient.getAndSetUserToken(tenantAdminId); - CalculatedField savedCalculatedField = createScriptCalculatedField(deviceProfileId); + CalculatedField savedCalculatedField = createScriptCalculatedField(deviceProfileId, asset.getId()); await().alias("create CF -> perform initial calculation for device by profile").atMost(TIMEOUT, TimeUnit.SECONDS) .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) .untilAsserted(() -> { JsonNode airDensity = testRestClient.getLatestTelemetry(device.getId()); assertThat(airDensity).isNotNull(); + assertThat(airDensity.get("airDensity")).isNotNull(); assertThat(airDensity.get("airDensity").get(0).get("value").asText()).isEqualTo("1.05"); }); - testRestClient.postTelemetryAttribute(asset.getId(), DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode("{\"altitude\":1531}")); + testRestClient.postTelemetryAttribute(asset.getId(), SERVER_SCOPE, JacksonUtil.toJsonNode("{\"altitude\":1531}")); await().alias("create CF -> update telemetry for common entity").atMost(TIMEOUT, TimeUnit.SECONDS) .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) .untilAsserted(() -> { JsonNode airDensity = testRestClient.getLatestTelemetry(device.getId()); assertThat(airDensity).isNotNull(); + assertThat(airDensity.get("airDensity")).isNotNull(); assertThat(airDensity.get("airDensity").get(0).get("value").asText()).isEqualTo("0.99"); }); testRestClient.deleteCalculatedFieldIfExists(savedCalculatedField.getId()); } + @Test + public void testPerformSerialsOfCalculationsForGeofencingType() { + // login tenant admin + testRestClient.getAndSetUserToken(tenantAdminId); + + // Device and initial coords (inside Allowed, outside Restricted) + String deviceToken = "geoDeviceTokenA"; + Device device = testRestClient.postDevice(deviceToken, createDevice("GF Device", deviceProfileId)); + testRestClient.postTelemetry(deviceToken, JacksonUtil.toJsonNode("{\"latitude\":50.4730,\"longitude\":30.5050}")); + + // Create zones + Asset allowed = testRestClient.postAsset(createAsset("Allowed Zone", null)); + testRestClient.postTelemetryAttribute(allowed.getId(), SERVER_SCOPE, + JacksonUtil.toJsonNode("{\"zone\":[[50.472000,30.504000],[50.472000,30.506000],[50.474000,30.506000],[50.474000,30.504000]]}")); + + Asset restricted = testRestClient.postAsset(createAsset("Restricted Zone", null)); + testRestClient.postTelemetryAttribute(restricted.getId(), SERVER_SCOPE, + JacksonUtil.toJsonNode("{\"zone\":[[50.475000,30.510000],[50.475000,30.512000],[50.477000,30.512000],[50.477000,30.510000]]}")); + + // Relations FROM device + testRestClient.postEntityRelation(new EntityRelation(device.getId(), allowed.getId(), "AllowedZone")); + testRestClient.postEntityRelation(new EntityRelation(device.getId(), restricted.getId(), "RestrictedZone")); + + // Build CF: GEOFENCING -> attributes output + CalculatedField cf = new CalculatedField(); + cf.setEntityId(device.getId()); + cf.setType(CalculatedFieldType.GEOFENCING); + cf.setName("Geofencing CF"); + cf.setDebugSettings(DebugSettings.off()); + + GeofencingCalculatedFieldConfiguration cfg = new GeofencingCalculatedFieldConfiguration(); + + EntityCoordinates entityCoordinates = new EntityCoordinates("latitude", "longitude"); + cfg.setEntityCoordinates(entityCoordinates); + + // Dynamic groups via relations + ZoneGroupConfiguration allowedZoneGroupConfiguration = new ZoneGroupConfiguration("zone", REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS, false); + var allowedDynamicSourceConfiguration = new RelationPathQueryDynamicSourceConfiguration(); + allowedDynamicSourceConfiguration.setLevels(List.of(new RelationPathLevel(EntitySearchDirection.FROM, "AllowedZone"))); + allowedZoneGroupConfiguration.setRefDynamicSourceConfiguration(allowedDynamicSourceConfiguration); + + ZoneGroupConfiguration restrictedZoneGroupConfiguration = new ZoneGroupConfiguration("zone", REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS, false); + var restrictedDynamicSourceConfiguration = new RelationPathQueryDynamicSourceConfiguration(); + restrictedDynamicSourceConfiguration.setLevels(List.of(new RelationPathLevel(EntitySearchDirection.FROM, "RestrictedZone"))); + restrictedZoneGroupConfiguration.setRefDynamicSourceConfiguration(restrictedDynamicSourceConfiguration); + + cfg.setZoneGroups(Map.of("allowedZones", allowedZoneGroupConfiguration, "restrictedZones", restrictedZoneGroupConfiguration)); + + Output out = new Output(); + out.setType(OutputType.ATTRIBUTES); + out.setScope(SERVER_SCOPE); + cfg.setOutput(out); + cf.setConfiguration(cfg); + + CalculatedField saved = testRestClient.postCalculatedField(cf); + + // Initial ENTERED/INSIDE and OUTSIDE + await().alias("initial geofencing evaluation").atMost(TIMEOUT, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + ArrayNode attrs = testRestClient.getAttributes(device.getId(), SERVER_SCOPE, + "allowedZonesEvent,allowedZonesStatus,restrictedZonesStatus"); + assertThat(attrs).isNotNull().hasSize(3); + Map m = kv(attrs); + assertThat(m).containsEntry("allowedZonesEvent", "ENTERED") + .containsEntry("allowedZonesStatus", "INSIDE") + .containsEntry("restrictedZonesStatus", "OUTSIDE"); + }); + + // Move device into Restricted zone -> expect LEFT/ENTERED and statuses flipped + testRestClient.postTelemetry(deviceToken, JacksonUtil.toJsonNode("{\"latitude\":50.4760,\"longitude\":30.5110}")); + + await().alias("transition after movement").atMost(TIMEOUT, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + ArrayNode attrs = testRestClient.getAttributes(device.getId(), SERVER_SCOPE, + "allowedZonesEvent,allowedZonesStatus,restrictedZonesEvent,restrictedZonesStatus"); + assertThat(attrs).isNotNull().hasSize(4); + Map m = kv(attrs); + assertThat(m).containsEntry("allowedZonesEvent", "LEFT") + .containsEntry("allowedZonesStatus", "OUTSIDE") + .containsEntry("restrictedZonesEvent", "ENTERED") + .containsEntry("restrictedZonesStatus", "INSIDE"); + }); + + testRestClient.deleteCalculatedFieldIfExists(saved.getId()); + } + + @Test + public void testPropagationCalculatedField_withExpression() { + // login tenant admin + testRestClient.getAndSetUserToken(tenantAdminId); + + // --- Arrange entities --- + String deviceToken = "propagationDeviceTokenA"; + Device device = testRestClient.postDevice(deviceToken, createDevice("Propagation Device With Expression", deviceProfileId)); + Asset asset1 = testRestClient.postAsset(createAsset("Propagated Asset 1", null)); + Asset asset2 = testRestClient.postAsset(createAsset("Propagated Asset 2", null)); + + // Create relations FROM assets TO device + EntityRelation rel1 = new EntityRelation(asset1.getId(), device.getId(), EntityRelation.CONTAINS_TYPE); + EntityRelation rel2 = new EntityRelation(asset2.getId(), device.getId(), EntityRelation.CONTAINS_TYPE); + testRestClient.postEntityRelation(rel1); + testRestClient.postEntityRelation(rel2); + + // Telemetry on device + testRestClient.postTelemetry(deviceToken, JacksonUtil.toJsonNode("{\"temperature\":12.5}")); + + // --- Build CF: PROPAGATION with expression --- + CalculatedField cf = new CalculatedField(); + cf.setEntityId(device.getId()); + cf.setType(CalculatedFieldType.PROPAGATION); + cf.setName("Propagation CF (expr)"); + cf.setConfigurationVersion(1); + + PropagationCalculatedFieldConfiguration cfg = new PropagationCalculatedFieldConfiguration(); + cfg.setRelation(new RelationPathLevel(EntitySearchDirection.TO, EntityRelation.CONTAINS_TYPE)); + cfg.setApplyExpressionToResolvedArguments(true); + + Argument arg = new Argument(); + arg.setRefEntityKey(new ReferencedEntityKey("temperature", ArgumentType.TS_LATEST, null)); + cfg.setArguments(Map.of("t", arg)); + + cfg.setExpression("{\"testResult\": t * 2}"); + + Output output = new Output(); + output.setType(OutputType.ATTRIBUTES); + output.setScope(AttributeScope.SERVER_SCOPE); + cfg.setOutput(output); + + cf.setConfiguration(cfg); + + CalculatedField saved = testRestClient.postCalculatedField(cf); + + // --- Assert propagated calculation (expression applied) --- + await().alias("propagation expr mode evaluation") + .atMost(TIMEOUT, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + ArrayNode attrs1 = testRestClient.getAttributes(asset1.getId(), SERVER_SCOPE, "testResult"); + assertThat(attrs1).isNotNull().hasSize(1); + Map m1 = intKv(attrs1); + assertThat(m1).containsEntry("testResult", 25); + + ArrayNode attrs2 = testRestClient.getAttributes(asset2.getId(), SERVER_SCOPE, "testResult"); + assertThat(attrs2).isNotNull().hasSize(1); + Map m2 = intKv(attrs2); + assertThat(m2).containsEntry("testResult", 25); + }); + + testRestClient.deleteEntityRelation(asset1.getId(), EntityRelation.CONTAINS_TYPE, device.getId()); + testRestClient.deleteEntityAttributes(asset1.getId(), SERVER_SCOPE, "testResult"); + + testRestClient.postTelemetry(deviceToken, JacksonUtil.toJsonNode("{\"temperature\":25}")); + + // --- Assert propagated calculation (expression applied with new temperature argument and one relation removed) --- + await().alias("propagation expr mode evaluation after temperature update") + .atMost(TIMEOUT, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + ArrayNode attrs1 = testRestClient.getAttributes(asset1.getId(), SERVER_SCOPE, "testResult"); + assertThat(attrs1).isNullOrEmpty(); + + ArrayNode attrs2 = testRestClient.getAttributes(asset2.getId(), SERVER_SCOPE, "testResult"); + assertThat(attrs2).isNotNull().hasSize(1); + Map m2 = intKv(attrs2); + assertThat(m2).containsEntry("testResult", 50); + }); + + testRestClient.deleteCalculatedFieldIfExists(saved.getId()); + } + + @Test + public void testPropagationCalculatedField_withoutExpression() { + // login tenant admin + testRestClient.getAndSetUserToken(tenantAdminId); + + // --- Arrange entities --- + String deviceToken = "propagationDeviceTokenB"; + Device device = testRestClient.postDevice(deviceToken, createDevice("Propagation Device Without Expression", deviceProfileId)); + Asset asset1 = testRestClient.postAsset(createAsset("Propagated Asset 3", null)); + Asset asset2 = testRestClient.postAsset(createAsset("Propagated Asset 4", null)); + + // Create relations FROM assets TO device + EntityRelation rel1 = new EntityRelation(asset1.getId(), device.getId(), EntityRelation.CONTAINS_TYPE); + EntityRelation rel2 = new EntityRelation(asset2.getId(), device.getId(), EntityRelation.CONTAINS_TYPE); + testRestClient.postEntityRelation(rel1); + testRestClient.postEntityRelation(rel2); + + // Telemetry on device + long ts = System.currentTimeMillis() - 300000L; + testRestClient.postTelemetry(deviceToken, JacksonUtil.toJsonNode(String.format("{\"ts\": %s, \"values\": {\"temperature\":12.5}}", ts))); + + // --- Build CF: PROPAGATION without expression --- + CalculatedField cf = new CalculatedField(); + cf.setEntityId(device.getId()); + cf.setType(CalculatedFieldType.PROPAGATION); + cf.setName("Propagation CF (args-only)"); + cf.setConfigurationVersion(1); + + PropagationCalculatedFieldConfiguration cfg = new PropagationCalculatedFieldConfiguration(); + cfg.setRelation(new RelationPathLevel(EntitySearchDirection.TO, EntityRelation.CONTAINS_TYPE)); + cfg.setApplyExpressionToResolvedArguments(false); // arguments-only mode + + Argument arg = new Argument(); + arg.setRefEntityKey(new ReferencedEntityKey("temperature", ArgumentType.TS_LATEST, null)); + cfg.setArguments(Map.of("temperatureComputed", arg)); + + Output output = new Output(); + output.setType(OutputType.TIME_SERIES); + cfg.setOutput(output); + + cf.setConfiguration(cfg); + + CalculatedField saved = testRestClient.postCalculatedField(cf); + + // --- Assert propagated calculation (arguments-only mode) --- + await().alias("propagation args-only evaluation") + .atMost(TIMEOUT, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + JsonNode temperature1 = testRestClient.getLatestTelemetry(asset1.getId()); + assertThat(temperature1).isNotNull(); + assertThat(temperature1.get("temperatureComputed")).isNotNull(); + assertThat(temperature1.get("temperatureComputed").get(0).get("ts").asText()).isEqualTo(Long.toString(ts)); + assertThat(temperature1.get("temperatureComputed").get(0).get("value").asText()).isEqualTo("12.5"); + + JsonNode temperature2 = testRestClient.getLatestTelemetry(asset2.getId()); + assertThat(temperature2).isNotNull(); + assertThat(temperature2.get("temperatureComputed")).isNotNull(); + assertThat(temperature2.get("temperatureComputed").get(0).get("ts").asText()).isEqualTo(Long.toString(ts)); + assertThat(temperature2.get("temperatureComputed").get(0).get("value").asText()).isEqualTo("12.5"); + }); + + testRestClient.deleteEntityRelation(asset1.getId(), EntityRelation.CONTAINS_TYPE, device.getId()); + testRestClient.deleteEntityTimeseries(asset1.getId(), "temperatureComputed", true); + + // Update telemetry on device + long newTs = System.currentTimeMillis() - 300000L; + testRestClient.postTelemetry(deviceToken, JacksonUtil.toJsonNode(String.format("{\"ts\": %s, \"values\": {\"temperature\":25}}", newTs))); + + // --- Assert propagated calculation (arguments-only mode after update) --- + await().alias("propagation args-only evaluation after temperature update") + .atMost(TIMEOUT, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + JsonNode temperature1 = testRestClient.getLatestTelemetry(asset1.getId()); + assertThat(temperature1).isNullOrEmpty(); + + JsonNode temperature2 = testRestClient.getLatestTelemetry(asset2.getId()); + assertThat(temperature2).isNotNull(); + assertThat(temperature2.get("temperatureComputed")).isNotNull(); + assertThat(temperature2.get("temperatureComputed").get(0).get("ts").asText()).isEqualTo(Long.toString(newTs)); + assertThat(temperature2.get("temperatureComputed").get(0).get("value").asInt()).isEqualTo(25); + }); + + testRestClient.deleteCalculatedFieldIfExists(saved.getId()); + } + private CalculatedField createSimpleCalculatedField() { return createSimpleCalculatedField(device.getId()); } @@ -317,7 +600,7 @@ public class CalculatedFieldTest extends AbstractContainerTest { CalculatedField calculatedField = new CalculatedField(); calculatedField.setEntityId(entityId); calculatedField.setType(CalculatedFieldType.SIMPLE); - calculatedField.setName("C to F" + RandomStringUtils.randomAlphabetic(5)); + calculatedField.setName("C to F" + RandomStringUtils.insecure().nextAlphabetic(5)); calculatedField.setDebugSettings(DebugSettings.all()); SimpleCalculatedFieldConfiguration config = new SimpleCalculatedFieldConfiguration(); @@ -341,26 +624,22 @@ public class CalculatedFieldTest extends AbstractContainerTest { return testRestClient.postCalculatedField(calculatedField); } - private CalculatedField createScriptCalculatedField() { - return createScriptCalculatedField(device.getId()); - } - - private CalculatedField createScriptCalculatedField(EntityId entityId) { + private CalculatedField createScriptCalculatedField(EntityId entityId, EntityId refEntityId) { CalculatedField calculatedField = new CalculatedField(); calculatedField.setEntityId(entityId); calculatedField.setType(CalculatedFieldType.SCRIPT); - calculatedField.setName("Air density" + RandomStringUtils.randomAlphabetic(5)); + calculatedField.setName("Air density" + RandomStringUtils.insecure().nextAlphabetic(5)); calculatedField.setDebugSettings(DebugSettings.all()); ScriptCalculatedFieldConfiguration config = new ScriptCalculatedFieldConfiguration(); Argument argument1 = new Argument(); - argument1.setRefEntityId(asset.getId()); - ReferencedEntityKey refEntityKey1 = new ReferencedEntityKey("altitude", ArgumentType.ATTRIBUTE, AttributeScope.SERVER_SCOPE); + argument1.setRefEntityId(refEntityId); + ReferencedEntityKey refEntityKey1 = new ReferencedEntityKey("altitude", ArgumentType.ATTRIBUTE, SERVER_SCOPE); argument1.setRefEntityKey(refEntityKey1); Argument argument2 = new Argument(); ReferencedEntityKey refEntityKey2 = new ReferencedEntityKey("temperatureInF", ArgumentType.TS_ROLLING, null); - argument2.setTimeWindow(30000L); + argument2.setTimeWindow(300000L); argument2.setLimit(5); argument2.setRefEntityKey(refEntityKey2); @@ -396,4 +675,20 @@ public class CalculatedFieldTest extends AbstractContainerTest { return asset; } + private static Map kv(ArrayNode attrs) { + Map m = new HashMap<>(); + for (JsonNode n : attrs) { + m.put(n.get("key").asText(), n.get("value").asText()); + } + return m; + } + + private static Map intKv(ArrayNode attrs) { + Map m = new HashMap<>(); + for (JsonNode n : attrs) { + m.put(n.get("key").asText(), n.get("value").asInt()); + } + return m; + } + } diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/CoapClientTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/CoapClientTest.java index 995b90e529..e2c4b25809 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/CoapClientTest.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/CoapClientTest.java @@ -21,6 +21,7 @@ import org.testng.annotations.AfterMethod; import org.testng.annotations.BeforeMethod; import org.testng.annotations.Test; import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.AttributeScope; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.DeviceProfileProvisionType; @@ -28,6 +29,8 @@ import org.thingsboard.server.common.data.security.DeviceCredentials; import org.thingsboard.server.msa.AbstractCoapClientTest; import org.thingsboard.server.msa.DisableUIListeners; +import java.util.Map; + import static org.assertj.core.api.Assertions.assertThat; import static org.thingsboard.server.msa.prototypes.DevicePrototypes.defaultDevicePrototype; @@ -52,14 +55,27 @@ public class CoapClientTest extends AbstractCoapClientTest{ DeviceProfile deviceProfile = testRestClient.getDeviceProfileById(device.getDeviceProfileId()); deviceProfile = updateDeviceProfileWithProvisioningStrategy(deviceProfile, DeviceProfileProvisionType.CHECK_PRE_PROVISIONED_DEVICES); - DeviceCredentials expectedDeviceCredentials = testRestClient.getDeviceCredentialsByDeviceId(device.getId()); + DeviceCredentials deviceCreds = testRestClient.getDeviceCredentialsByDeviceId(device.getId()); JsonNode provisionResponse = JacksonUtil.fromBytes(createCoapClientAndPublish(device.getName())); - assertThat(provisionResponse.get("credentialsType").asText()).isEqualTo(expectedDeviceCredentials.getCredentialsType().name()); - assertThat(provisionResponse.get("credentialsValue").asText()).isEqualTo(expectedDeviceCredentials.getCredentialsId()); + assertThat(provisionResponse.get("credentialsType").asText()).isEqualTo(deviceCreds.getCredentialsType().name()); + assertThat(provisionResponse.get("credentialsValue").asText()).isEqualTo(deviceCreds.getCredentialsId()); assertThat(provisionResponse.get("status").asText()).isEqualTo("SUCCESS"); + JsonNode attributes = testRestClient.getAttributes(device.getId(), AttributeScope.SERVER_SCOPE, "provisionState"); + assertThat(attributes.get(0).get("value").asText()).isEqualTo("provisioned"); + + // provision second time should fail + JsonNode provisionResponse2 = JacksonUtil.fromBytes(createCoapClientAndPublish(device.getName())); + assertThat(provisionResponse2.get("status").asText()).isEqualTo("FAILURE"); + + // update provision attribute to non-valid value + testRestClient.postTelemetryAttribute(device.getId(), AttributeScope.SERVER_SCOPE, JacksonUtil.valueToTree(Map.of("provisionState", "non-valid"))); + + JsonNode provisionResponse3 = JacksonUtil.fromBytes(createCoapClientAndPublish(device.getName())); + assertThat(provisionResponse3.get("status").asText()).isEqualTo("FAILURE"); + updateDeviceProfileWithProvisioningStrategy(deviceProfile, DeviceProfileProvisionType.DISABLED); } diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/HttpClientTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/HttpClientTest.java index 4e28340f9d..306ca5adf4 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/HttpClientTest.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/HttpClientTest.java @@ -35,8 +35,7 @@ import java.util.Arrays; import java.util.concurrent.TimeUnit; import static org.assertj.core.api.Assertions.assertThat; -import static org.thingsboard.server.common.data.DataConstants.DEVICE; -import static org.thingsboard.server.common.data.DataConstants.SHARED_SCOPE; +import static org.thingsboard.server.common.data.AttributeScope.SHARED_SCOPE; import static org.thingsboard.server.msa.prototypes.DevicePrototypes.defaultDevicePrototype; @DisableUIListeners diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/JavaRestClientTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/JavaRestClientTest.java new file mode 100644 index 0000000000..636c80d5b3 --- /dev/null +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/JavaRestClientTest.java @@ -0,0 +1,128 @@ +/** + * Copyright © 2016-2025 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.msa.connectivity; + +import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; +import org.apache.hc.client5.http.impl.classic.HttpClients; +import org.apache.hc.client5.http.io.HttpClientConnectionManager; +import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder; +import org.apache.hc.client5.http.ssl.DefaultClientTlsStrategy; +import org.apache.hc.client5.http.ssl.HostnameVerificationPolicy; +import org.apache.hc.client5.http.ssl.NoopHostnameVerifier; +import org.apache.hc.core5.ssl.SSLContexts; +import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; +import org.springframework.web.client.RestTemplate; +import org.testcontainers.shaded.org.apache.commons.lang3.RandomStringUtils; +import org.testng.annotations.AfterMethod; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; +import org.thingsboard.rest.client.RestClient; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.alarm.Alarm; +import org.thingsboard.server.common.data.alarm.AlarmInfo; +import org.thingsboard.server.common.data.alarm.AlarmSearchStatus; +import org.thingsboard.server.common.data.alarm.AlarmSeverity; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.TimePageLink; +import org.thingsboard.server.msa.AbstractContainerTest; +import org.thingsboard.server.msa.TestProperties; + +import javax.net.ssl.SSLContext; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.thingsboard.server.msa.prototypes.DevicePrototypes.defaultDevicePrototype; + +public class JavaRestClientTest extends AbstractContainerTest { + + private RestClient restClient; + + @BeforeClass + public void beforeClass() throws Exception { + SSLContext ssl = SSLContexts.custom() + .loadTrustMaterial((chain, authType) -> true) + .build(); + + var tls = new DefaultClientTlsStrategy( + ssl, + HostnameVerificationPolicy.CLIENT, + NoopHostnameVerifier.INSTANCE + ); + + HttpClientConnectionManager cm = PoolingHttpClientConnectionManagerBuilder.create() + .setTlsSocketStrategy(tls) + .build(); + + CloseableHttpClient httpClient = HttpClients.custom() + .setConnectionManager(cm) + .build(); + + RestTemplate rt = new RestTemplate(new HttpComponentsClientHttpRequestFactory(httpClient)); + restClient = new RestClient(rt, TestProperties.getBaseUrl()); + } + + @BeforeMethod + public void setUp() throws Exception { + restClient.login("tenant@thingsboard.org", "tenant"); + } + + @AfterMethod + public void tearDown() { + } + + @Test + public void testGetAlarmsV2() { + Device device = restClient.saveDevice(defaultDevicePrototype(RandomStringUtils.randomAlphabetic(5))); + assertThat(device).isNotNull(); + + String type = "High temp" + RandomStringUtils.randomAlphabetic(5); + Alarm alarm = Alarm.builder() + .originator(device.getId()) + .severity(AlarmSeverity.CRITICAL) + .type(type) + .build(); + restClient.saveAlarm(alarm); + + // get /api/v2/alarm + PageData alarmsV2 = restClient.getAlarmsV2(device.getId(), null, null, List.of(type), null, new TimePageLink(10, 0)); + assertThat(alarmsV2.getData()).hasSize(1); + + PageData activeAlarms = restClient.getAlarmsV2(device.getId(), List.of(AlarmSearchStatus.ACTIVE), null, List.of(type), null, new TimePageLink(10, 0)); + assertThat(activeAlarms.getData()).hasSize(1); + + PageData cleared = restClient.getAlarmsV2(device.getId(), List.of(AlarmSearchStatus.CLEARED), null, List.of(type), null, new TimePageLink(10, 0)); + assertThat(cleared.getData()).hasSize(0); + + PageData activeAndClearedAlarms = restClient.getAlarmsV2(device.getId(), List.of(AlarmSearchStatus.CLEARED, AlarmSearchStatus.ACTIVE), null, null, null, new TimePageLink(10, 0)); + assertThat(activeAndClearedAlarms.getData()).hasSize(1); + + // get /api/v2/alarms + PageData allAlarmsV2 = restClient.getAllAlarmsV2(List.of(AlarmSearchStatus.ACTIVE), null, List.of(type), null, new TimePageLink(10, 0)); + assertThat(allAlarmsV2.getData()).hasSize(1); + + PageData allClearedAlarmsV2 = restClient.getAllAlarmsV2(List.of(AlarmSearchStatus.CLEARED), null, List.of(type), null, new TimePageLink(10, 0)); + assertThat(allClearedAlarmsV2.getData()).hasSize(0); + + // get /api/alarms + PageData allAlarms = restClient.getAllAlarms(AlarmSearchStatus.ACTIVE, null, new TimePageLink(10, 0), null); + assertThat(allAlarms.getData()).hasSize(1); + + PageData allClearedAlarms = restClient.getAllAlarms(AlarmSearchStatus.CLEARED, null, new TimePageLink(10, 0), null); + assertThat(allClearedAlarms.getData()).hasSize(0); + + } +} diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/MqttClientTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/MqttClientTest.java index eb30ef8b9c..3e580379df 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/MqttClientTest.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/MqttClientTest.java @@ -81,7 +81,7 @@ import java.util.concurrent.TimeoutException; import static org.assertj.core.api.Assertions.assertThat; import static org.testng.Assert.assertNotNull; import static org.testng.Assert.fail; -import static org.thingsboard.server.common.data.DataConstants.SHARED_SCOPE; +import static org.thingsboard.server.common.data.AttributeScope.SHARED_SCOPE; import static org.thingsboard.server.msa.prototypes.DevicePrototypes.defaultDevicePrototype; @DisableUIListeners @@ -577,7 +577,7 @@ public class MqttClientTest extends AbstractContainerTest { Awaitility .await() .alias("Check device disconnect.") - .atMost(TIMEOUT*timeoutMultiplier, TimeUnit.SECONDS) + .atMost(TIMEOUT * timeoutMultiplier, TimeUnit.SECONDS) .until(() -> !returnCodeByteValue.isEmpty()); assertThat(returnCodeByteValueSecondClient).isEmpty(); diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/MqttGatewayClientTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/MqttGatewayClientTest.java index fd9d4f557d..5265bf9cc6 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/MqttGatewayClientTest.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/MqttGatewayClientTest.java @@ -64,7 +64,7 @@ import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import static org.assertj.core.api.Assertions.assertThat; -import static org.thingsboard.server.common.data.DataConstants.SHARED_SCOPE; +import static org.thingsboard.server.common.data.AttributeScope.SHARED_SCOPE; import static org.thingsboard.server.msa.prototypes.DevicePrototypes.defaultGatewayPrototype; @DisableUIListeners diff --git a/msa/black-box-tests/src/test/resources/connectivity.xml b/msa/black-box-tests/src/test/resources/connectivity.xml index 425fbd67eb..7253d87e63 100644 --- a/msa/black-box-tests/src/test/resources/connectivity.xml +++ b/msa/black-box-tests/src/test/resources/connectivity.xml @@ -21,8 +21,10 @@ - - + + + + \ No newline at end of file diff --git a/msa/black-box-tests/src/test/resources/docker-compose.valkey-ssl.yml b/msa/black-box-tests/src/test/resources/docker-compose.valkey-ssl.yml index 8c3029cda1..710dab4690 100644 --- a/msa/black-box-tests/src/test/resources/docker-compose.valkey-ssl.yml +++ b/msa/black-box-tests/src/test/resources/docker-compose.valkey-ssl.yml @@ -19,7 +19,7 @@ services: # The latest version of Valkey compatible with ThingsBoard is 8.0 valkey: restart: always - image: bitnami/valkey:8.0 + image: bitnamilegacy/valkey:8.0 environment: # ALLOW_EMPTY_PASSWORD is recommended only for development. - 'ALLOW_EMPTY_PASSWORD=yes' diff --git a/msa/js-executor/docker/Dockerfile b/msa/js-executor/docker/Dockerfile index de5869a5d1..51651fa235 100644 --- a/msa/js-executor/docker/Dockerfile +++ b/msa/js-executor/docker/Dockerfile @@ -14,7 +14,7 @@ # limitations under the License. # -FROM thingsboard/node:20.18.0-bookworm-slim +FROM thingsboard/node:22.18.0-bookworm-slim ENV NODE_ENV production ENV DOCKER_MODE true diff --git a/msa/js-executor/package.json b/msa/js-executor/package.json index 3ee1614e87..fe56867dbe 100644 --- a/msa/js-executor/package.json +++ b/msa/js-executor/package.json @@ -6,20 +6,20 @@ "main": "server.ts", "bin": "server.js", "scripts": { - "pkg": "tsc && pkg -t node18-linux-x64,node18-win-x64 --out-path ./target ./target/src && node install.js", + "pkg": "tsc && pkg -t node22-linux-x64,node22-win-x64 --out-path ./target ./target/src && node install.js", "test": "echo \"Error: no test specified\" && exit 1", "start": "nodemon --watch '.' --ext 'ts' --exec 'ts-node server.ts'", "start-prod": "nodemon --watch '.' --ext 'ts' --exec 'NODE_ENV=production ts-node server.ts'", "build": "tsc" }, "dependencies": { - "config": "^3.3.12", - "express": "^4.21.1", + "config": "^4.1.1", + "express": "^5.1.0", "js-yaml": "^4.1.0", "kafkajs": "^2.2.4", - "long": "^5.2.3", + "long": "^5.3.2", "uuid-parse": "^1.1.0", - "winston": "^3.16.0", + "winston": "^3.17.0", "winston-daily-rotate-file": "^5.0.0" }, "nyc": { @@ -32,14 +32,14 @@ }, "devDependencies": { "@types/config": "^3.3.5", - "@types/express": "~4.17.21", - "@types/node": "~20.17.6", + "@types/express": "~5.0.3", + "@types/node": "~22.17.2", "@types/uuid-parse": "^1.0.2", - "fs-extra": "^11.2.0", - "nodemon": "^3.1.7", - "pkg": "^5.8.1", + "@yao-pkg/pkg": "^6.6.0", + "fs-extra": "^11.3.1", + "nodemon": "^3.1.10", "ts-node": "^10.9.2", - "typescript": "5.5.4" + "typescript": "5.9.2" }, "pkg": { "assets": [ diff --git a/msa/js-executor/pom.xml b/msa/js-executor/pom.xml index d7bac12742..9730e7f33b 100644 --- a/msa/js-executor/pom.xml +++ b/msa/js-executor/pom.xml @@ -71,7 +71,7 @@ install-node-and-yarn - v20.18.0 + v22.18.0 v1.22.22 diff --git a/msa/js-executor/yarn.lock b/msa/js-executor/yarn.lock index 7008959b89..25ed229141 100644 --- a/msa/js-executor/yarn.lock +++ b/msa/js-executor/yarn.lock @@ -2,46 +2,41 @@ # yarn lockfile v1 -"@babel/generator@7.18.2": - version "7.18.2" - resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.18.2.tgz#33873d6f89b21efe2da63fe554460f3df1c5880d" - integrity sha512-W1lG5vUwFvfMd8HVXqdfbuG7RuaSrTCCD8cl8fP8wOivdbtbIg2Db3IWUcgvfxKbbn6ZBGYRW/Zk1MIwK49mgw== - dependencies: - "@babel/types" "^7.18.2" - "@jridgewell/gen-mapping" "^0.3.0" - jsesc "^2.5.1" - -"@babel/helper-string-parser@^7.18.10", "@babel/helper-string-parser@^7.25.9": - version "7.25.9" - resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz#1aabb72ee72ed35789b4bbcad3ca2862ce614e8c" - integrity sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA== - -"@babel/helper-validator-identifier@^7.18.6", "@babel/helper-validator-identifier@^7.25.9": - version "7.25.9" - resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz#24b64e2c3ec7cd3b3c547729b8d16871f22cbdc7" - integrity sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ== - -"@babel/parser@7.18.4": - version "7.18.4" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.18.4.tgz#6774231779dd700e0af29f6ad8d479582d7ce5ef" - integrity sha512-FDge0dFazETFcxGw/EXzOkN8uJp0PC7Qbm+Pe9T+av2zlBpOgunFHkQPPn+eRuClU73JF+98D531UgayY89tow== - -"@babel/types@7.19.0": - version "7.19.0" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.19.0.tgz#75f21d73d73dc0351f3368d28db73465f4814600" - integrity sha512-YuGopBq3ke25BVSiS6fgF49Ul9gH1x70Bcr6bqRLjWCkcX8Hre1/5+z+IiWOIerRMSSEfGZVB9z9kyq7wVs9YA== - dependencies: - "@babel/helper-string-parser" "^7.18.10" - "@babel/helper-validator-identifier" "^7.18.6" - to-fast-properties "^2.0.0" - -"@babel/types@^7.18.2": - version "7.26.0" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.26.0.tgz#deabd08d6b753bc8e0f198f8709fb575e31774ff" - integrity sha512-Z/yiTPj+lDVnF7lWeKCIJzaIkI0vYO87dMpZ4bg4TDrFe4XXLFWL1TbXU27gBP3QccxV9mZICCrnjnYlJjXHOA== - dependencies: - "@babel/helper-string-parser" "^7.25.9" - "@babel/helper-validator-identifier" "^7.25.9" +"@babel/generator@^7.23.0": + version "7.28.3" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.28.3.tgz#9626c1741c650cbac39121694a0f2d7451b8ef3e" + integrity sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw== + dependencies: + "@babel/parser" "^7.28.3" + "@babel/types" "^7.28.2" + "@jridgewell/gen-mapping" "^0.3.12" + "@jridgewell/trace-mapping" "^0.3.28" + jsesc "^3.0.2" + +"@babel/helper-string-parser@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz#54da796097ab19ce67ed9f88b47bb2ec49367687" + integrity sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA== + +"@babel/helper-validator-identifier@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz#a7054dcc145a967dd4dc8fee845a57c1316c9df8" + integrity sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow== + +"@babel/parser@^7.23.0", "@babel/parser@^7.28.3": + version "7.28.3" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.28.3.tgz#d2d25b814621bca5fe9d172bc93792547e7a2a71" + integrity sha512-7+Ey1mAgYqFAx2h0RuoxcQT5+MlG3GTV0TQrgr7/ZliKsm/MNDxVVutlWaziMq7wJNAz8MTqz55XLpWvva6StA== + dependencies: + "@babel/types" "^7.28.2" + +"@babel/types@^7.23.0", "@babel/types@^7.28.2": + version "7.28.2" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.28.2.tgz#da9db0856a9a88e0a13b019881d7513588cf712b" + integrity sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ== + dependencies: + "@babel/helper-string-parser" "^7.27.1" + "@babel/helper-validator-identifier" "^7.27.1" "@colors/colors@1.6.0", "@colors/colors@^1.6.0": version "1.6.0" @@ -64,13 +59,19 @@ enabled "2.0.x" kuler "^2.0.0" -"@jridgewell/gen-mapping@^0.3.0": - version "0.3.5" - resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz#dcce6aff74bdf6dad1a95802b69b04a2fcb1fb36" - integrity sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg== +"@isaacs/fs-minipass@^4.0.0": + version "4.0.1" + resolved "https://registry.yarnpkg.com/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz#2d59ae3ab4b38fb4270bfa23d30f8e2e86c7fe32" + integrity sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w== dependencies: - "@jridgewell/set-array" "^1.2.1" - "@jridgewell/sourcemap-codec" "^1.4.10" + minipass "^7.0.4" + +"@jridgewell/gen-mapping@^0.3.12": + version "0.3.13" + resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz#6342a19f44347518c93e43b1ac69deb3c4656a1f" + integrity sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA== + dependencies: + "@jridgewell/sourcemap-codec" "^1.5.0" "@jridgewell/trace-mapping" "^0.3.24" "@jridgewell/resolve-uri@^3.0.3", "@jridgewell/resolve-uri@^3.1.0": @@ -78,15 +79,10 @@ resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz#7a0ee601f60f99a20c7c7c5ff0c80388c1189bd6" integrity sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw== -"@jridgewell/set-array@^1.2.1": - version "1.2.1" - resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.2.1.tgz#558fb6472ed16a4c850b889530e6b36438c49280" - integrity sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A== - -"@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@^1.4.14": - version "1.5.0" - resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz#3188bcb273a414b0d215fd22a58540b989b9409a" - integrity sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ== +"@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@^1.4.14", "@jridgewell/sourcemap-codec@^1.5.0": + version "1.5.5" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz#6912b00d2c631c0d15ce1a7ab57cd657f2a8f8ba" + integrity sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og== "@jridgewell/trace-mapping@0.3.9": version "0.3.9" @@ -96,35 +92,14 @@ "@jridgewell/resolve-uri" "^3.0.3" "@jridgewell/sourcemap-codec" "^1.4.10" -"@jridgewell/trace-mapping@^0.3.24": - version "0.3.25" - resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz#15f190e98895f3fc23276ee14bc76b675c2e50f0" - integrity sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ== +"@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.28": + version "0.3.30" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.30.tgz#4a76c4daeee5df09f5d3940e087442fb36ce2b99" + integrity sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q== dependencies: "@jridgewell/resolve-uri" "^3.1.0" "@jridgewell/sourcemap-codec" "^1.4.14" -"@nodelib/fs.scandir@2.1.5": - version "2.1.5" - resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" - integrity sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g== - dependencies: - "@nodelib/fs.stat" "2.0.5" - run-parallel "^1.1.9" - -"@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2": - version "2.0.5" - resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz#5bd262af94e9d25bd1e71b05deed44876a222e8b" - integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== - -"@nodelib/fs.walk@^1.2.3": - version "1.2.8" - resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz#e95737e8bb6746ddedf69c556953494f196fe69a" - integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg== - dependencies: - "@nodelib/fs.scandir" "2.1.5" - fastq "^1.6.0" - "@tsconfig/node10@^1.0.7": version "1.0.11" resolved "https://registry.yarnpkg.com/@tsconfig/node10/-/node10-1.0.11.tgz#6ee46400685f130e278128c7b38b7e031ff5b2f2" @@ -146,9 +121,9 @@ integrity sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA== "@types/body-parser@*": - version "1.19.5" - resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.5.tgz#04ce9a3b677dc8bd681a17da1ab9835dc9d3ede4" - integrity sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg== + version "1.19.6" + resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.6.tgz#1859bebb8fd7dac9918a45d54c1971ab8b5af474" + integrity sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g== dependencies: "@types/connect" "*" "@types/node" "*" @@ -165,30 +140,29 @@ dependencies: "@types/node" "*" -"@types/express-serve-static-core@^4.17.33": - version "4.19.6" - resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.19.6.tgz#e01324c2a024ff367d92c66f48553ced0ab50267" - integrity sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A== +"@types/express-serve-static-core@^5.0.0": + version "5.0.7" + resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-5.0.7.tgz#2fa94879c9d46b11a5df4c74ac75befd6b283de6" + integrity sha512-R+33OsgWw7rOhD1emjU7dzCDHucJrgJXMA5PYCzJxVil0dsyx5iBEPHqpPfiKNJQb7lZ1vxwoLR4Z87bBUpeGQ== dependencies: "@types/node" "*" "@types/qs" "*" "@types/range-parser" "*" "@types/send" "*" -"@types/express@~4.17.21": - version "4.17.21" - resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.21.tgz#c26d4a151e60efe0084b23dc3369ebc631ed192d" - integrity sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ== +"@types/express@~5.0.3": + version "5.0.3" + resolved "https://registry.yarnpkg.com/@types/express/-/express-5.0.3.tgz#6c4bc6acddc2e2a587142e1d8be0bce20757e956" + integrity sha512-wGA0NX93b19/dZC1J18tKWVIYWyyF2ZjT9vin/NRu0qzzvfVzWjs04iq2rQ3H65vCTQYlRqs3YHfY7zjdV+9Kw== dependencies: "@types/body-parser" "*" - "@types/express-serve-static-core" "^4.17.33" - "@types/qs" "*" + "@types/express-serve-static-core" "^5.0.0" "@types/serve-static" "*" "@types/http-errors@*": - version "2.0.4" - resolved "https://registry.yarnpkg.com/@types/http-errors/-/http-errors-2.0.4.tgz#7eb47726c391b7345a6ec35ad7f4de469cf5ba4f" - integrity sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA== + version "2.0.5" + resolved "https://registry.yarnpkg.com/@types/http-errors/-/http-errors-2.0.5.tgz#5b749ab2b16ba113423feb1a64a95dcd30398472" + integrity sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg== "@types/mime@^1": version "1.3.5" @@ -196,23 +170,23 @@ integrity sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w== "@types/node@*": - version "22.8.7" - resolved "https://registry.yarnpkg.com/@types/node/-/node-22.8.7.tgz#04ab7a073d95b4a6ee899f235d43f3c320a976f4" - integrity sha512-LidcG+2UeYIWcMuMUpBKOnryBWG/rnmOHQR5apjn8myTQcx3rinFRn7DcIFhMnS0PPFSC6OafdIKEad0lj6U0Q== + version "24.3.0" + resolved "https://registry.yarnpkg.com/@types/node/-/node-24.3.0.tgz#89b09f45cb9a8ee69466f18ee5864e4c3eb84dec" + integrity sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow== dependencies: - undici-types "~6.19.8" + undici-types "~7.10.0" -"@types/node@~20.17.6": - version "20.17.6" - resolved "https://registry.yarnpkg.com/@types/node/-/node-20.17.6.tgz#6e4073230c180d3579e8c60141f99efdf5df0081" - integrity sha512-VEI7OdvK2wP7XHnsuXbAJnEpEkF6NjSN45QJlL4VGqZSXsnicpesdTWsg9RISeSdYd3yeRj/y3k5KGjUXYnFwQ== +"@types/node@~22.17.2": + version "22.17.2" + resolved "https://registry.yarnpkg.com/@types/node/-/node-22.17.2.tgz#47a93d6f4b79327da63af727e7c54e8cab8c4d33" + integrity sha512-gL6z5N9Jm9mhY+U2KXZpteb+09zyffliRkZyZOHODGATyC5B1Jt/7TzuuiLkFsSUMLbS1OLmlj/E+/3KF4Q/4w== dependencies: - undici-types "~6.19.2" + undici-types "~6.21.0" "@types/qs@*": - version "6.9.16" - resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.16.tgz#52bba125a07c0482d26747d5d4947a64daf8f794" - integrity sha512-7i+zxXdPD0T4cKDuxCUXJ4wHcsJLwENa6Z3dCu8cfCK743OGy5Nu1RmAGqDPsoTDINVEcdXKRvR/zre+P2Ku1A== + version "6.14.0" + resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.14.0.tgz#d8b60cecf62f2db0fb68e5e006077b9178b85de5" + integrity sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ== "@types/range-parser@*": version "1.2.7" @@ -220,17 +194,17 @@ integrity sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ== "@types/send@*": - version "0.17.4" - resolved "https://registry.yarnpkg.com/@types/send/-/send-0.17.4.tgz#6619cd24e7270793702e4e6a4b958a9010cfc57a" - integrity sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA== + version "0.17.5" + resolved "https://registry.yarnpkg.com/@types/send/-/send-0.17.5.tgz#d991d4f2b16f2b1ef497131f00a9114290791e74" + integrity sha512-z6F2D3cOStZvuk2SaP6YrwkNO65iTZcwA2ZkSABegdkAh/lf+Aa/YQndZVfmEXT5vgAp6zv06VQ3ejSVjAny4w== dependencies: "@types/mime" "^1" "@types/node" "*" "@types/serve-static@*": - version "1.15.7" - resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.15.7.tgz#22174bbd74fb97fe303109738e9b5c2f3064f714" - integrity sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw== + version "1.15.8" + resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.15.8.tgz#8180c3fbe4a70e8f00b9f70b9ba7f08f35987877" + integrity sha512-roei0UY3LhpOJvjbIP6ZZFngyLKl5dskOtDhxY5THRSpO+ZI+nzJ+m5yUMzGrp89YRa7lvknKkMYjqQFGwA7Sg== dependencies: "@types/http-errors" "*" "@types/node" "*" @@ -248,20 +222,47 @@ dependencies: "@types/node" "*" -abort-controller@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/abort-controller/-/abort-controller-3.0.0.tgz#eaf54d53b62bae4138e809ca225c8439a6efb392" - integrity sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg== +"@yao-pkg/pkg-fetch@3.5.24": + version "3.5.24" + resolved "https://registry.yarnpkg.com/@yao-pkg/pkg-fetch/-/pkg-fetch-3.5.24.tgz#37a671f077fe6446aec0758e6fc7961d183c3445" + integrity sha512-FPESCH1uXCYui6jeDp2aayWuFHR39w+uU1r88nI6JWRvPYOU64cHPUV/p6GSFoQdpna7ip92HnrZKbBC60l0gA== dependencies: - event-target-shim "^5.0.0" + https-proxy-agent "^5.0.0" + node-fetch "^2.6.6" + picocolors "^1.1.0" + progress "^2.0.3" + semver "^7.3.5" + tar-fs "^2.1.1" + yargs "^16.2.0" -accepts@~1.3.8: - version "1.3.8" - resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e" - integrity sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw== +"@yao-pkg/pkg@^6.6.0": + version "6.6.0" + resolved "https://registry.yarnpkg.com/@yao-pkg/pkg/-/pkg-6.6.0.tgz#e8c38ed5824381c676e6688f93e27f39e1752701" + integrity sha512-3/oiaSm7fS0Fc7dzp22r9B7vFaguGhO9vERgEReRYj2EUzdi5ssyYhe1uYJG4ec/dmo2GG6RRHOUAT8savl79Q== dependencies: - mime-types "~2.1.34" - negotiator "0.6.3" + "@babel/generator" "^7.23.0" + "@babel/parser" "^7.23.0" + "@babel/types" "^7.23.0" + "@yao-pkg/pkg-fetch" "3.5.24" + into-stream "^6.0.0" + minimist "^1.2.6" + multistream "^4.1.0" + picocolors "^1.1.0" + picomatch "^4.0.2" + prebuild-install "^7.1.1" + resolve "^1.22.10" + stream-meter "^1.0.4" + tar "^7.4.3" + tinyglobby "^0.2.11" + unzipper "^0.12.3" + +accepts@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/accepts/-/accepts-2.0.0.tgz#bbcf4ba5075467f3f2131eab3cffc73c2f5d7895" + integrity sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng== + dependencies: + mime-types "^3.0.0" + negotiator "^1.0.0" acorn-walk@^8.1.1: version "8.3.4" @@ -271,9 +272,9 @@ acorn-walk@^8.1.1: acorn "^8.11.0" acorn@^8.11.0, acorn@^8.4.1: - version "8.14.0" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.14.0.tgz#063e2c70cac5fb4f6467f0b11152e04c682795b0" - integrity sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA== + version "8.15.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.15.0.tgz#a360898bc415edaac46c8241f6383975b930b816" + integrity sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg== agent-base@6: version "6.0.2" @@ -287,7 +288,7 @@ ansi-regex@^5.0.1: resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== -ansi-styles@^4.0.0, ansi-styles@^4.1.0: +ansi-styles@^4.0.0: version "4.3.0" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== @@ -312,26 +313,11 @@ argparse@^2.0.1: resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== -array-flatten@1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" - integrity sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg== - -array-union@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d" - integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== - async@^3.2.3: version "3.2.6" resolved "https://registry.yarnpkg.com/async/-/async-3.2.6.tgz#1b0728e14929d51b85b449b7f06e27c1145e38ce" integrity sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA== -at-least-node@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/at-least-node/-/at-least-node-1.0.0.tgz#602cd4b46e844ad4effc92a8011a3c46e0238dc2" - integrity sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg== - balanced-match@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" @@ -356,33 +342,35 @@ bl@^4.0.3: inherits "^2.0.4" readable-stream "^3.4.0" -body-parser@1.20.3: - version "1.20.3" - resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.3.tgz#1953431221c6fb5cd63c4b36d53fab0928e548c6" - integrity sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g== - dependencies: - bytes "3.1.2" - content-type "~1.0.5" - debug "2.6.9" - depd "2.0.0" - destroy "1.2.0" - http-errors "2.0.0" - iconv-lite "0.4.24" - on-finished "2.4.1" - qs "6.13.0" - raw-body "2.5.2" - type-is "~1.6.18" - unpipe "1.0.0" +bluebird@~3.7.2: + version "3.7.2" + resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" + integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== + +body-parser@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-2.2.0.tgz#f7a9656de305249a715b549b7b8fd1ab9dfddcfa" + integrity sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg== + dependencies: + bytes "^3.1.2" + content-type "^1.0.5" + debug "^4.4.0" + http-errors "^2.0.0" + iconv-lite "^0.6.3" + on-finished "^2.4.1" + qs "^6.14.0" + raw-body "^3.0.0" + type-is "^2.0.0" brace-expansion@^1.1.7: - version "1.1.11" - resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" - integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== + version "1.1.12" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.12.tgz#ab9b454466e5a8cc3a187beaad580412a9c5b843" + integrity sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg== dependencies: balanced-match "^1.0.0" concat-map "0.0.1" -braces@^3.0.3, braces@~3.0.2: +braces@~3.0.2: version "3.0.3" resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789" integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA== @@ -397,37 +385,26 @@ buffer@^5.5.0: base64-js "^1.3.1" ieee754 "^1.1.13" -buffer@^6.0.3: - version "6.0.3" - resolved "https://registry.yarnpkg.com/buffer/-/buffer-6.0.3.tgz#2ace578459cc8fbe2a70aaa8f52ee63b6a74c6c6" - integrity sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA== - dependencies: - base64-js "^1.3.1" - ieee754 "^1.2.1" - -bytes@3.1.2: +bytes@3.1.2, bytes@^3.1.2: version "3.1.2" resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== -call-bind@^1.0.7: - version "1.0.7" - resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.7.tgz#06016599c40c56498c18769d2730be242b6fa3b9" - integrity sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w== +call-bind-apply-helpers@^1.0.1, call-bind-apply-helpers@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz#4b5428c222be985d79c3d82657479dbe0b59b2d6" + integrity sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ== dependencies: - es-define-property "^1.0.0" es-errors "^1.3.0" function-bind "^1.1.2" - get-intrinsic "^1.2.4" - set-function-length "^1.2.1" -chalk@^4.1.2: - version "4.1.2" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" - integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== +call-bound@^1.0.2: + version "1.0.4" + resolved "https://registry.yarnpkg.com/call-bound/-/call-bound-1.0.4.tgz#238de935d2a2a692928c538c7ccfa91067fd062a" + integrity sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg== dependencies: - ansi-styles "^4.1.0" - supports-color "^7.1.0" + call-bind-apply-helpers "^1.0.2" + get-intrinsic "^1.3.0" chokidar@^3.5.2: version "3.6.0" @@ -449,6 +426,11 @@ chownr@^1.1.1: resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b" integrity sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg== +chownr@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/chownr/-/chownr-3.0.0.tgz#9855e64ecd240a9cc4267ce8a4aa5d24a1da15e4" + integrity sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g== + cliui@^7.0.2: version "7.0.4" resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.4.tgz#a0265ee655476fc807aea9df3df8df7783808b4f" @@ -511,34 +493,34 @@ concat-map@0.0.1: resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== -config@^3.3.12: - version "3.3.12" - resolved "https://registry.yarnpkg.com/config/-/config-3.3.12.tgz#a10ae66efcc3e48c1879fbb657c86c4ef6c7b25e" - integrity sha512-Vmx389R/QVM3foxqBzXO8t2tUikYZP64Q6vQxGrsMpREeJc/aWRnPRERXWsYzOHAumx/AOoILWe6nU3ZJL+6Sw== +config@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/config/-/config-4.1.1.tgz#798f636e2a5b7baa7050280d1eb21f8e10abb8c9" + integrity sha512-jljfwqNZ7QHwAW9Z9NDZdJARFiu5pjLqQO0K4ooY22iY/bIY78n0afI4ANEawfgQOxri0K/3oTayX8XIauWcLA== dependencies: json5 "^2.2.3" -content-disposition@0.5.4: - version "0.5.4" - resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.4.tgz#8b82b4efac82512a02bb0b1dcec9d2c5e8eb5bfe" - integrity sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ== +content-disposition@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-1.0.0.tgz#844426cb398f934caefcbb172200126bc7ceace2" + integrity sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg== dependencies: safe-buffer "5.2.1" -content-type@~1.0.4, content-type@~1.0.5: +content-type@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.5.tgz#8b773162656d1d1086784c8f23a54ce6d73d7918" integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA== -cookie-signature@1.0.6: - version "1.0.6" - resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" - integrity sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ== +cookie-signature@^1.2.1: + version "1.2.2" + resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.2.2.tgz#57c7fc3cc293acab9fec54d73e15690ebe4a1793" + integrity sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg== -cookie@0.7.1: - version "0.7.1" - resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.7.1.tgz#2f73c42142d5d5cf71310a74fc4ae61670e5dbc9" - integrity sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w== +cookie@^0.7.1: + version "0.7.2" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.7.2.tgz#556369c472a2ba910f2979891b526b3436237ed7" + integrity sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w== core-util-is@~1.0.0: version "1.0.3" @@ -550,17 +532,10 @@ create-require@^1.1.0: resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333" integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== -debug@2.6.9: - version "2.6.9" - resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" - integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== - dependencies: - ms "2.0.0" - -debug@4, debug@^4: - version "4.3.7" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.7.tgz#87945b4151a011d76d95a198d7111c865c360a52" - integrity sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ== +debug@4, debug@^4, debug@^4.3.5, debug@^4.4.0: + version "4.4.1" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.1.tgz#e5a8bc6cbc4c6cd3e64308b0693a3d4fa550189b" + integrity sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ== dependencies: ms "^2.1.3" @@ -576,41 +551,36 @@ deep-extend@^0.6.0: resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac" integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA== -define-data-property@^1.1.4: - version "1.1.4" - resolved "https://registry.yarnpkg.com/define-data-property/-/define-data-property-1.1.4.tgz#894dc141bb7d3060ae4366f6a0107e68fbe48c5e" - integrity sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A== - dependencies: - es-define-property "^1.0.0" - es-errors "^1.3.0" - gopd "^1.0.1" - -depd@2.0.0: +depd@2.0.0, depd@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== -destroy@1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015" - integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg== - detect-libc@^2.0.0: - version "2.0.3" - resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.0.3.tgz#f0cd503b40f9939b894697d19ad50895e30cf700" - integrity sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw== + version "2.0.4" + resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.0.4.tgz#f04715b8ba815e53b4d8109655b6508a6865a7e8" + integrity sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA== diff@^4.0.1: version "4.0.2" resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== -dir-glob@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f" - integrity sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA== +dunder-proto@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/dunder-proto/-/dunder-proto-1.0.1.tgz#d7ae667e1dc83482f8b70fd0f6eefc50da30f58a" + integrity sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A== + dependencies: + call-bind-apply-helpers "^1.0.1" + es-errors "^1.3.0" + gopd "^1.2.0" + +duplexer2@~0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/duplexer2/-/duplexer2-0.1.4.tgz#8b12dab878c0d69e3e7891051662a32fc6bddcc1" + integrity sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA== dependencies: - path-type "^4.0.0" + readable-stream "^2.0.2" ee-first@1.1.1: version "1.1.1" @@ -627,119 +597,92 @@ enabled@2.0.x: resolved "https://registry.yarnpkg.com/enabled/-/enabled-2.0.0.tgz#f9dd92ec2d6f4bbc0d5d1e64e21d61cd4665e7c2" integrity sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ== -encodeurl@~1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" - integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w== - -encodeurl@~2.0.0: +encodeurl@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-2.0.0.tgz#7b8ea898077d7e409d3ac45474ea38eaf0857a58" integrity sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg== end-of-stream@^1.1.0, end-of-stream@^1.4.1: - version "1.4.4" - resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" - integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== + version "1.4.5" + resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.5.tgz#7344d711dea40e0b74abc2ed49778743ccedb08c" + integrity sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg== dependencies: once "^1.4.0" -es-define-property@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.0.tgz#c7faefbdff8b2696cf5f46921edfb77cc4ba3845" - integrity sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ== - dependencies: - get-intrinsic "^1.2.4" +es-define-property@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.1.tgz#983eb2f9a6724e9303f61addf011c72e09e0b0fa" + integrity sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g== es-errors@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f" integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== +es-object-atoms@^1.0.0, es-object-atoms@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz#1c4f2c4837327597ce69d2ca190a7fdd172338c1" + integrity sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA== + dependencies: + es-errors "^1.3.0" + escalade@^3.1.1: version "3.2.0" resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.2.0.tgz#011a3f69856ba189dffa7dc8fcce99d2a87903e5" integrity sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA== -escape-html@~1.0.3: +escape-html@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow== -etag@~1.8.1: +etag@^1.8.1: version "1.8.1" resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg== -event-target-shim@^5.0.0: - version "5.0.1" - resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789" - integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ== - -events@^3.3.0: - version "3.3.0" - resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" - integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== - expand-template@^2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/expand-template/-/expand-template-2.0.3.tgz#6e14b3fcee0f3a6340ecb57d2e8918692052a47c" integrity sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg== -express@^4.21.1: - version "4.21.1" - resolved "https://registry.yarnpkg.com/express/-/express-4.21.1.tgz#9dae5dda832f16b4eec941a4e44aa89ec481b281" - integrity sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ== - dependencies: - accepts "~1.3.8" - array-flatten "1.1.1" - body-parser "1.20.3" - content-disposition "0.5.4" - content-type "~1.0.4" - cookie "0.7.1" - cookie-signature "1.0.6" - debug "2.6.9" - depd "2.0.0" - encodeurl "~2.0.0" - escape-html "~1.0.3" - etag "~1.8.1" - finalhandler "1.3.1" - fresh "0.5.2" - http-errors "2.0.0" - merge-descriptors "1.0.3" - methods "~1.1.2" - on-finished "2.4.1" - parseurl "~1.3.3" - path-to-regexp "0.1.10" - proxy-addr "~2.0.7" - qs "6.13.0" - range-parser "~1.2.1" - safe-buffer "5.2.1" - send "0.19.0" - serve-static "1.16.2" - setprototypeof "1.2.0" - statuses "2.0.1" - type-is "~1.6.18" - utils-merge "1.0.1" - vary "~1.1.2" - -fast-glob@^3.2.9: - version "3.3.2" - resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.2.tgz#a904501e57cfdd2ffcded45e99a54fef55e46129" - integrity sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow== - dependencies: - "@nodelib/fs.stat" "^2.0.2" - "@nodelib/fs.walk" "^1.2.3" - glob-parent "^5.1.2" - merge2 "^1.3.0" - micromatch "^4.0.4" - -fastq@^1.6.0: - version "1.17.1" - resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.17.1.tgz#2a523f07a4e7b1e81a42b91b8bf2254107753b47" - integrity sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w== - dependencies: - reusify "^1.0.4" +express@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/express/-/express-5.1.0.tgz#d31beaf715a0016f0d53f47d3b4d7acf28c75cc9" + integrity sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA== + dependencies: + accepts "^2.0.0" + body-parser "^2.2.0" + content-disposition "^1.0.0" + content-type "^1.0.5" + cookie "^0.7.1" + cookie-signature "^1.2.1" + debug "^4.4.0" + encodeurl "^2.0.0" + escape-html "^1.0.3" + etag "^1.8.1" + finalhandler "^2.1.0" + fresh "^2.0.0" + http-errors "^2.0.0" + merge-descriptors "^2.0.0" + mime-types "^3.0.0" + on-finished "^2.4.1" + once "^1.4.0" + parseurl "^1.3.3" + proxy-addr "^2.0.7" + qs "^6.14.0" + range-parser "^1.2.1" + router "^2.2.0" + send "^1.1.0" + serve-static "^2.2.0" + statuses "^2.0.1" + type-is "^2.0.1" + vary "^1.1.2" + +fdir@^6.4.4: + version "6.5.0" + resolved "https://registry.yarnpkg.com/fdir/-/fdir-6.5.0.tgz#ed2ab967a331ade62f18d077dae192684d50d350" + integrity sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg== fecha@^4.2.0: version "4.2.3" @@ -760,18 +703,17 @@ fill-range@^7.1.1: dependencies: to-regex-range "^5.0.1" -finalhandler@1.3.1: - version "1.3.1" - resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.3.1.tgz#0c575f1d1d324ddd1da35ad7ece3df7d19088019" - integrity sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ== +finalhandler@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-2.1.0.tgz#72306373aa89d05a8242ed569ed86a1bff7c561f" + integrity sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q== dependencies: - debug "2.6.9" - encodeurl "~2.0.0" - escape-html "~1.0.3" - on-finished "2.4.1" - parseurl "~1.3.3" - statuses "2.0.1" - unpipe "~1.0.0" + debug "^4.4.0" + encodeurl "^2.0.0" + escape-html "^1.0.3" + on-finished "^2.4.1" + parseurl "^1.3.3" + statuses "^2.0.1" fn.name@1.x.x: version "1.1.0" @@ -783,10 +725,10 @@ forwarded@0.2.0: resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow== -fresh@0.5.2: - version "0.5.2" - resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" - integrity sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q== +fresh@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/fresh/-/fresh-2.0.0.tgz#8dd7df6a1b3a1b3a5cf186c05a5dd267622635a4" + integrity sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A== from2@^2.3.0: version "2.3.0" @@ -801,25 +743,15 @@ fs-constants@^1.0.0: resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad" integrity sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow== -fs-extra@^11.2.0: - version "11.2.0" - resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-11.2.0.tgz#e70e17dfad64232287d01929399e0ea7c86b0e5b" - integrity sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw== +fs-extra@^11.2.0, fs-extra@^11.3.1: + version "11.3.1" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-11.3.1.tgz#ba7a1f97a85f94c6db2e52ff69570db3671d5a74" + integrity sha512-eXvGGwZ5CL17ZSwHWd3bbgk7UUpF6IFHtP57NYYakPvHOs8GDgDe5KJI36jIJzDkJ6eJjuzRA8eBQb6SkKue0g== dependencies: graceful-fs "^4.2.0" jsonfile "^6.0.1" universalify "^2.0.0" -fs-extra@^9.1.0: - version "9.1.0" - resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-9.1.0.tgz#5954460c764a8da2094ba3554bf839e6b9a7c86d" - integrity sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ== - dependencies: - at-least-node "^1.0.0" - graceful-fs "^4.2.0" - jsonfile "^6.0.1" - universalify "^2.0.0" - fsevents@~2.3.2: version "2.3.3" resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" @@ -835,49 +767,48 @@ get-caller-file@^2.0.5: resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== -get-intrinsic@^1.1.3, get-intrinsic@^1.2.4: - version "1.2.4" - resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.4.tgz#e385f5a4b5227d449c3eabbad05494ef0abbeadd" - integrity sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ== +get-intrinsic@^1.2.5, get-intrinsic@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz#743f0e3b6964a93a5491ed1bffaae054d7f98d01" + integrity sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ== dependencies: + call-bind-apply-helpers "^1.0.2" + es-define-property "^1.0.1" es-errors "^1.3.0" + es-object-atoms "^1.1.1" function-bind "^1.1.2" - has-proto "^1.0.1" - has-symbols "^1.0.3" - hasown "^2.0.0" + get-proto "^1.0.1" + gopd "^1.2.0" + has-symbols "^1.1.0" + hasown "^2.0.2" + math-intrinsics "^1.1.0" + +get-proto@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/get-proto/-/get-proto-1.0.1.tgz#150b3f2743869ef3e851ec0c49d15b1d14d00ee1" + integrity sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g== + dependencies: + dunder-proto "^1.0.1" + es-object-atoms "^1.0.0" github-from-package@0.0.0: version "0.0.0" resolved "https://registry.yarnpkg.com/github-from-package/-/github-from-package-0.0.0.tgz#97fb5d96bfde8973313f20e8288ef9a167fa64ce" integrity sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw== -glob-parent@^5.1.2, glob-parent@~5.1.2: +glob-parent@~5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== dependencies: is-glob "^4.0.1" -globby@^11.1.0: - version "11.1.0" - resolved "https://registry.yarnpkg.com/globby/-/globby-11.1.0.tgz#bd4be98bb042f83d796f7e3811991fbe82a0d34b" - integrity sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g== - dependencies: - array-union "^2.1.0" - dir-glob "^3.0.1" - fast-glob "^3.2.9" - ignore "^5.2.0" - merge2 "^1.4.1" - slash "^3.0.0" - -gopd@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.0.1.tgz#29ff76de69dac7489b7c0918a5788e56477c332c" - integrity sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA== - dependencies: - get-intrinsic "^1.1.3" +gopd@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.2.0.tgz#89f56b8217bdbc8802bd299df6d7f1081d7e51a1" + integrity sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg== -graceful-fs@^4.1.6, graceful-fs@^4.2.0: +graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.2: version "4.2.11" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== @@ -887,41 +818,19 @@ has-flag@^3.0.0: resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" integrity sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw== -has-flag@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" - integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== - -has-property-descriptors@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz#963ed7d071dc7bf5f084c5bfbe0d1b6222586854" - integrity sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg== - dependencies: - es-define-property "^1.0.0" - -has-proto@^1.0.1: - version "1.0.3" - resolved "https://registry.yarnpkg.com/has-proto/-/has-proto-1.0.3.tgz#b31ddfe9b0e6e9914536a6ab286426d0214f77fd" - integrity sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q== - -has-symbols@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" - integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== - -has@^1.0.3: - version "1.0.4" - resolved "https://registry.yarnpkg.com/has/-/has-1.0.4.tgz#2eb2860e000011dae4f1406a86fe80e530fb2ec6" - integrity sha512-qdSAmqLF6209RFj4VVItywPMbm3vWylknmB3nvNiUIs72xAimcM8nVYxYr7ncvZq5qzk9MKIZR8ijqD/1QuYjQ== +has-symbols@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.1.0.tgz#fc9c6a783a084951d0b971fe1018de813707a338" + integrity sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ== -hasown@^2.0.0, hasown@^2.0.2: +hasown@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003" integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ== dependencies: function-bind "^1.1.2" -http-errors@2.0.0: +http-errors@2.0.0, http-errors@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.0.tgz#b7774a1486ef73cf7667ac9ae0858c012c57b9d3" integrity sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ== @@ -940,14 +849,14 @@ https-proxy-agent@^5.0.0: agent-base "6" debug "4" -iconv-lite@0.4.24: - version "0.4.24" - resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" - integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== +iconv-lite@0.6.3, iconv-lite@^0.6.3: + version "0.6.3" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501" + integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw== dependencies: - safer-buffer ">= 2.1.2 < 3" + safer-buffer ">= 2.1.2 < 3.0.0" -ieee754@^1.1.13, ieee754@^1.2.1: +ieee754@^1.1.13: version "1.2.1" resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== @@ -957,11 +866,6 @@ ignore-by-default@^1.0.1: resolved "https://registry.yarnpkg.com/ignore-by-default/-/ignore-by-default-1.0.1.tgz#48ca6d72f6c6a3af00a9ad4ae6876be3889e2b09" integrity sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA== -ignore@^5.2.0: - version "5.3.2" - resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.2.tgz#3cd40e729f3643fd87cb04e50bf0eb722bc596f5" - integrity sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g== - inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.3: version "2.0.4" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" @@ -997,17 +901,10 @@ is-binary-path@~2.1.0: dependencies: binary-extensions "^2.0.0" -is-core-module@2.9.0: - version "2.9.0" - resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.9.0.tgz#e1c34429cd51c6dd9e09e0799e396e27b19a9c69" - integrity sha512-+5FPy5PnwmO3lvfMb0AsoPaBG+5KHUI0wYFXOtYPnVVVspTFUuMZNfNaNVRt3FZadstu2c8x23vykRW/NBoU6A== - dependencies: - has "^1.0.3" - -is-core-module@^2.13.0: - version "2.15.1" - resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.15.1.tgz#a7363a25bee942fefab0de13bf6aa372c82dcc37" - integrity sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ== +is-core-module@^2.16.0: + version "2.16.1" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.16.1.tgz#2a98801a849f43e2add644fbb6bc6229b19a4ef4" + integrity sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w== dependencies: hasown "^2.0.2" @@ -1033,6 +930,11 @@ is-number@^7.0.0: resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== +is-promise@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-4.0.0.tgz#42ff9f84206c1991d26debf520dd5c01042dd2f3" + integrity sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ== + is-stream@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077" @@ -1050,10 +952,10 @@ js-yaml@^4.1.0: dependencies: argparse "^2.0.1" -jsesc@^2.5.1: - version "2.5.2" - resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4" - integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA== +jsesc@^3.0.2: + version "3.1.0" + resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-3.1.0.tgz#74d335a234f67ed19907fdadfac7ccf9d409825d" + integrity sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA== json5@^2.2.3: version "2.2.3" @@ -1061,9 +963,9 @@ json5@^2.2.3: integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== jsonfile@^6.0.1: - version "6.1.0" - resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.1.0.tgz#bc55b2634793c679ec6403094eb13698a6ec0aae" - integrity sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ== + version "6.2.0" + resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.2.0.tgz#7c265bd1b65de6977478300087c99f1c84383f62" + integrity sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg== dependencies: universalify "^2.0.0" optionalDependencies: @@ -1079,10 +981,10 @@ kuler@^2.0.0: resolved "https://registry.yarnpkg.com/kuler/-/kuler-2.0.0.tgz#e2c570a3800388fb44407e851531c1d670b061b3" integrity sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A== -logform@^2.6.0, logform@^2.6.1: - version "2.6.1" - resolved "https://registry.yarnpkg.com/logform/-/logform-2.6.1.tgz#71403a7d8cae04b2b734147963236205db9b3df0" - integrity sha512-CdaO738xRapbKIMVn2m4F6KTj4j7ooJ8POVnebSgKo3KBz5axNXRAL7ZdRjIV6NOr2Uf4vjtRkxrFETOioCqSA== +logform@^2.7.0: + version "2.7.0" + resolved "https://registry.yarnpkg.com/logform/-/logform-2.7.0.tgz#cfca97528ef290f2e125a08396805002b2d060d1" + integrity sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ== dependencies: "@colors/colors" "1.6.0" "@types/triple-beam" "^1.3.2" @@ -1091,60 +993,42 @@ logform@^2.6.0, logform@^2.6.1: safe-stable-stringify "^2.3.1" triple-beam "^1.3.0" -long@^5.2.3: - version "5.2.3" - resolved "https://registry.yarnpkg.com/long/-/long-5.2.3.tgz#a3ba97f3877cf1d778eccbcb048525ebb77499e1" - integrity sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q== +long@^5.3.2: + version "5.3.2" + resolved "https://registry.yarnpkg.com/long/-/long-5.3.2.tgz#1d84463095999262d7d7b7f8bfd4a8cc55167f83" + integrity sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA== make-error@^1.1.1: version "1.3.6" resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2" integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== -media-typer@0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" - integrity sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ== - -merge-descriptors@1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.3.tgz#d80319a65f3c7935351e5cfdac8f9318504dbed5" - integrity sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ== - -merge2@^1.3.0, merge2@^1.4.1: - version "1.4.1" - resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" - integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== +math-intrinsics@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz#a0dd74be81e2aa5c2f27e65ce283605ee4e2b7f9" + integrity sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g== -methods@~1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" - integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w== +media-typer@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-1.1.0.tgz#6ab74b8f2d3320f2064b2a87a38e7931ff3a5561" + integrity sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw== -micromatch@^4.0.4: - version "4.0.8" - resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.8.tgz#d66fa18f3a47076789320b9b1af32bd86d9fa202" - integrity sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA== - dependencies: - braces "^3.0.3" - picomatch "^2.3.1" +merge-descriptors@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-2.0.0.tgz#ea922f660635a2249ee565e0449f951e6b603808" + integrity sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g== -mime-db@1.52.0: - version "1.52.0" - resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" - integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== +mime-db@^1.54.0: + version "1.54.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.54.0.tgz#cddb3ee4f9c64530dff640236661d42cb6a314f5" + integrity sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ== -mime-types@~2.1.24, mime-types@~2.1.34: - version "2.1.35" - resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" - integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== +mime-types@^3.0.0, mime-types@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-3.0.1.tgz#b1d94d6997a9b32fd69ebaed0db73de8acb519ce" + integrity sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA== dependencies: - mime-db "1.52.0" - -mime@1.6.0: - version "1.6.0" - resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" - integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== + mime-db "^1.54.0" mimic-response@^3.1.0: version "3.1.0" @@ -1163,22 +1047,34 @@ minimist@^1.2.0, minimist@^1.2.3, minimist@^1.2.6: resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== +minipass@^7.0.4, minipass@^7.1.2: + version "7.1.2" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.1.2.tgz#93a9626ce5e5e66bd4db86849e7515e92340a707" + integrity sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw== + +minizlib@^3.0.1: + version "3.0.2" + resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-3.0.2.tgz#f33d638eb279f664439aa38dc5f91607468cb574" + integrity sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA== + dependencies: + minipass "^7.1.2" + mkdirp-classic@^0.5.2, mkdirp-classic@^0.5.3: version "0.5.3" resolved "https://registry.yarnpkg.com/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz#fa10c9115cc6d8865be221ba47ee9bed78601113" integrity sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A== +mkdirp@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-3.0.1.tgz#e44e4c5607fb279c168241713cc6e0fea9adcb50" + integrity sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg== + moment@^2.29.1: version "2.30.1" resolved "https://registry.yarnpkg.com/moment/-/moment-2.30.1.tgz#f8c91c07b7a786e30c59926df530b4eac96974ae" integrity sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how== -ms@2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" - integrity sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A== - -ms@2.1.3, ms@^2.1.1, ms@^2.1.3: +ms@^2.1.1, ms@^2.1.3: version "2.1.3" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== @@ -1191,20 +1087,20 @@ multistream@^4.1.0: once "^1.4.0" readable-stream "^3.6.0" -napi-build-utils@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/napi-build-utils/-/napi-build-utils-1.0.2.tgz#b1fddc0b2c46e380a0b7a76f984dd47c41a13806" - integrity sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg== +napi-build-utils@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/napi-build-utils/-/napi-build-utils-2.0.0.tgz#13c22c0187fcfccce1461844136372a47ddc027e" + integrity sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA== -negotiator@0.6.3: - version "0.6.3" - resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd" - integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg== +negotiator@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-1.0.0.tgz#b6c91bb47172d69f93cfd7c357bbb529019b5f6a" + integrity sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg== node-abi@^3.3.0: - version "3.71.0" - resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-3.71.0.tgz#52d84bbcd8575efb71468fbaa1f9a49b2c242038" - integrity sha512-SZ40vRiy/+wRTf21hxkkEjPJZpARzUMVcJoQse2EF8qkUWbbO2z7vd5oA/H6bVH6SZQ5STGcu0KRDS7biNRfxw== + version "3.75.0" + resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-3.75.0.tgz#2f929a91a90a0d02b325c43731314802357ed764" + integrity sha512-OhYaY5sDsIka7H7AtijtI9jwGYLyl29eQn/W623DiN/MIv5sUqc4g7BIDThX+gb7di9f6xK02nkp8sdfFWZLTg== dependencies: semver "^7.3.5" @@ -1215,10 +1111,15 @@ node-fetch@^2.6.6: dependencies: whatwg-url "^5.0.0" -nodemon@^3.1.7: - version "3.1.7" - resolved "https://registry.yarnpkg.com/nodemon/-/nodemon-3.1.7.tgz#07cb1f455f8bece6a499e0d72b5e029485521a54" - integrity sha512-hLj7fuMow6f0lbB0cD14Lz2xNjwsyruH251Pk4t/yIitCFJbmY1myuLlHm/q06aST4jg6EgAh74PIBBrRqpVAQ== +node-int64@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b" + integrity sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw== + +nodemon@^3.1.10: + version "3.1.10" + resolved "https://registry.yarnpkg.com/nodemon/-/nodemon-3.1.10.tgz#5015c5eb4fffcb24d98cf9454df14f4fecec9bc1" + integrity sha512-WDjw3pJ0/0jMFmyNDp3gvY2YizjLmmOUQo6DEBY+JgdvW/yQ9mEeSw6H5ythl5Ny2ytb7f9C2nIbjSxMNzbJXw== dependencies: chokidar "^3.5.2" debug "^4" @@ -1241,12 +1142,12 @@ object-hash@^3.0.0: resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-3.0.0.tgz#73f97f753e7baffc0e2cc9d6e079079744ac82e9" integrity sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw== -object-inspect@^1.13.1: - version "1.13.2" - resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.2.tgz#dea0088467fb991e67af4058147a24824a3043ff" - integrity sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g== +object-inspect@^1.13.3: + version "1.13.4" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.4.tgz#8375265e21bc20d0fa582c22e1b13485d6e00213" + integrity sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew== -on-finished@2.4.1: +on-finished@^2.4.1: version "2.4.1" resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f" integrity sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg== @@ -1272,7 +1173,7 @@ p-is-promise@^3.0.0: resolved "https://registry.yarnpkg.com/p-is-promise/-/p-is-promise-3.0.0.tgz#58e78c7dfe2e163cf2a04ff869e7c1dba64a5971" integrity sha512-Wo8VsW4IRQSKVXsJCn7TomUaVtyfjVDn3nUP7kE967BQk0CwFpdbZs0X0uk5sW9mkBa9eNM7hCMaG93WUAwxYQ== -parseurl@~1.3.3: +parseurl@^1.3.3: version "1.3.3" resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== @@ -1282,66 +1183,37 @@ path-parse@^1.0.7: resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== -path-to-regexp@0.1.10: - version "0.1.10" - resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.10.tgz#67e9108c5c0551b9e5326064387de4763c4d5f8b" - integrity sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w== +path-to-regexp@^8.0.0: + version "8.2.0" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-8.2.0.tgz#73990cc29e57a3ff2a0d914095156df5db79e8b4" + integrity sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ== -path-type@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" - integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== +picocolors@^1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b" + integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA== -picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.3.1: +picomatch@^2.0.4, picomatch@^2.2.1: version "2.3.1" resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== -pkg-fetch@3.4.2: - version "3.4.2" - resolved "https://registry.yarnpkg.com/pkg-fetch/-/pkg-fetch-3.4.2.tgz#6f68ebc54842b73f8c0808959a9df3739dcb28b7" - integrity sha512-0+uijmzYcnhC0hStDjm/cl2VYdrmVVBpe7Q8k9YBojxmR5tG8mvR9/nooQq3QSXiQqORDVOTY3XqMEqJVIzkHA== - dependencies: - chalk "^4.1.2" - fs-extra "^9.1.0" - https-proxy-agent "^5.0.0" - node-fetch "^2.6.6" - progress "^2.0.3" - semver "^7.3.5" - tar-fs "^2.1.1" - yargs "^16.2.0" - -pkg@^5.8.1: - version "5.8.1" - resolved "https://registry.yarnpkg.com/pkg/-/pkg-5.8.1.tgz#862020f3c0575638ef7d1146f951a54d65ddc984" - integrity sha512-CjBWtFStCfIiT4Bde9QpJy0KeH19jCfwZRJqHFDFXfhUklCx8JoFmMj3wgnEYIwGmZVNkhsStPHEOnrtrQhEXA== - dependencies: - "@babel/generator" "7.18.2" - "@babel/parser" "7.18.4" - "@babel/types" "7.19.0" - chalk "^4.1.2" - fs-extra "^9.1.0" - globby "^11.1.0" - into-stream "^6.0.0" - is-core-module "2.9.0" - minimist "^1.2.6" - multistream "^4.1.0" - pkg-fetch "3.4.2" - prebuild-install "7.1.1" - resolve "^1.22.0" - stream-meter "^1.0.4" +picomatch@^4.0.2: + version "4.0.3" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-4.0.3.tgz#796c76136d1eead715db1e7bad785dedd695a042" + integrity sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q== -prebuild-install@7.1.1: - version "7.1.1" - resolved "https://registry.yarnpkg.com/prebuild-install/-/prebuild-install-7.1.1.tgz#de97d5b34a70a0c81334fd24641f2a1702352e45" - integrity sha512-jAXscXWMcCK8GgCoHOfIr0ODh5ai8mj63L2nWrjuAgXE6tDyYGnx4/8o/rCgU+B4JSyZBKbeZqzhtwtC3ovxjw== +prebuild-install@^7.1.1: + version "7.1.3" + resolved "https://registry.yarnpkg.com/prebuild-install/-/prebuild-install-7.1.3.tgz#d630abad2b147443f20a212917beae68b8092eec" + integrity sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug== dependencies: detect-libc "^2.0.0" expand-template "^2.0.3" github-from-package "0.0.0" minimist "^1.2.3" mkdirp-classic "^0.5.3" - napi-build-utils "^1.0.1" + napi-build-utils "^2.0.0" node-abi "^3.3.0" pump "^3.0.0" rc "^1.2.7" @@ -1354,17 +1226,12 @@ process-nextick-args@~2.0.0: resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== -process@^0.11.10: - version "0.11.10" - resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" - integrity sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A== - progress@^2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8" integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA== -proxy-addr@~2.0.7: +proxy-addr@^2.0.7: version "2.0.7" resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025" integrity sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg== @@ -1378,38 +1245,33 @@ pstree.remy@^1.1.8: integrity sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w== pump@^3.0.0: - version "3.0.2" - resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.2.tgz#836f3edd6bc2ee599256c924ffe0d88573ddcbf8" - integrity sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw== + version "3.0.3" + resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.3.tgz#151d979f1a29668dc0025ec589a455b53282268d" + integrity sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA== dependencies: end-of-stream "^1.1.0" once "^1.3.1" -qs@6.13.0: - version "6.13.0" - resolved "https://registry.yarnpkg.com/qs/-/qs-6.13.0.tgz#6ca3bd58439f7e245655798997787b0d88a51906" - integrity sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg== +qs@^6.14.0: + version "6.14.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.14.0.tgz#c63fa40680d2c5c941412a0e899c89af60c0a930" + integrity sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w== dependencies: - side-channel "^1.0.6" - -queue-microtask@^1.2.2: - version "1.2.3" - resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" - integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== + side-channel "^1.1.0" -range-parser@~1.2.1: +range-parser@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== -raw-body@2.5.2: - version "2.5.2" - resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.2.tgz#99febd83b90e08975087e8f1f9419a149366b68a" - integrity sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA== +raw-body@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-3.0.0.tgz#25b3476f07a51600619dae3fe82ddc28a36e5e0f" + integrity sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g== dependencies: bytes "3.1.2" http-errors "2.0.0" - iconv-lite "0.4.24" + iconv-lite "0.6.3" unpipe "1.0.0" rc@^1.2.7: @@ -1422,7 +1284,7 @@ rc@^1.2.7: minimist "^1.2.0" strip-json-comments "~2.0.1" -readable-stream@^2.0.0, readable-stream@^2.1.4: +readable-stream@^2.0.0, readable-stream@^2.0.2, readable-stream@^2.1.4: version "2.3.8" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.8.tgz#91125e8042bba1b9887f49345f6277027ce8be9b" integrity sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA== @@ -1435,7 +1297,7 @@ readable-stream@^2.0.0, readable-stream@^2.1.4: string_decoder "~1.1.1" util-deprecate "~1.0.1" -readable-stream@^3.1.1, readable-stream@^3.4.0, readable-stream@^3.6.0: +readable-stream@^3.1.1, readable-stream@^3.4.0, readable-stream@^3.6.0, readable-stream@^3.6.2: version "3.6.2" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.2.tgz#56a9b36ea965c00c5a93ef31eb111a0f11056967" integrity sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA== @@ -1444,17 +1306,6 @@ readable-stream@^3.1.1, readable-stream@^3.4.0, readable-stream@^3.6.0: string_decoder "^1.1.1" util-deprecate "^1.0.1" -readable-stream@^4.5.2: - version "4.5.2" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-4.5.2.tgz#9e7fc4c45099baeed934bff6eb97ba6cf2729e09" - integrity sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g== - dependencies: - abort-controller "^3.0.0" - buffer "^6.0.3" - events "^3.3.0" - process "^0.11.10" - string_decoder "^1.3.0" - readdirp@~3.6.0: version "3.6.0" resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" @@ -1467,26 +1318,25 @@ require-directory@^2.1.1: resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" integrity sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q== -resolve@^1.22.0: - version "1.22.8" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.8.tgz#b6c87a9f2aa06dfab52e3d70ac8cde321fa5a48d" - integrity sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw== +resolve@^1.22.10: + version "1.22.10" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.10.tgz#b663e83ffb09bbf2386944736baae803029b8b39" + integrity sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w== dependencies: - is-core-module "^2.13.0" + is-core-module "^2.16.0" path-parse "^1.0.7" supports-preserve-symlinks-flag "^1.0.0" -reusify@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" - integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== - -run-parallel@^1.1.9: - version "1.2.0" - resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee" - integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA== +router@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/router/-/router-2.2.0.tgz#019be620b711c87641167cc79b99090f00b146ef" + integrity sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ== dependencies: - queue-microtask "^1.2.2" + debug "^4.4.0" + depd "^2.0.0" + is-promise "^4.0.0" + parseurl "^1.3.3" + path-to-regexp "^8.0.0" safe-buffer@5.2.1, safe-buffer@^5.0.1, safe-buffer@~5.2.0: version "5.2.1" @@ -1503,71 +1353,87 @@ safe-stable-stringify@^2.3.1: resolved "https://registry.yarnpkg.com/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz#4ca2f8e385f2831c432a719b108a3bf7af42a1dd" integrity sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA== -"safer-buffer@>= 2.1.2 < 3": +"safer-buffer@>= 2.1.2 < 3.0.0": version "2.1.2" resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== semver@^7.3.5, semver@^7.5.3: - version "7.6.3" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.3.tgz#980f7b5550bc175fb4dc09403085627f9eb33143" - integrity sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A== + version "7.7.2" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.2.tgz#67d99fdcd35cec21e6f8b87a7fd515a33f982b58" + integrity sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA== -send@0.19.0: - version "0.19.0" - resolved "https://registry.yarnpkg.com/send/-/send-0.19.0.tgz#bbc5a388c8ea6c048967049dbeac0e4a3f09d7f8" - integrity sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw== - dependencies: - debug "2.6.9" - depd "2.0.0" - destroy "1.2.0" - encodeurl "~1.0.2" - escape-html "~1.0.3" - etag "~1.8.1" - fresh "0.5.2" - http-errors "2.0.0" - mime "1.6.0" - ms "2.1.3" - on-finished "2.4.1" - range-parser "~1.2.1" - statuses "2.0.1" - -serve-static@1.16.2: - version "1.16.2" - resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.16.2.tgz#b6a5343da47f6bdd2673848bf45754941e803296" - integrity sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw== - dependencies: - encodeurl "~2.0.0" - escape-html "~1.0.3" - parseurl "~1.3.3" - send "0.19.0" +send@^1.1.0, send@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/send/-/send-1.2.0.tgz#32a7554fb777b831dfa828370f773a3808d37212" + integrity sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw== + dependencies: + debug "^4.3.5" + encodeurl "^2.0.0" + escape-html "^1.0.3" + etag "^1.8.1" + fresh "^2.0.0" + http-errors "^2.0.0" + mime-types "^3.0.1" + ms "^2.1.3" + on-finished "^2.4.1" + range-parser "^1.2.1" + statuses "^2.0.1" -set-function-length@^1.2.1: - version "1.2.2" - resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.2.2.tgz#aac72314198eaed975cf77b2c3b6b880695e5449" - integrity sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg== +serve-static@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-2.2.0.tgz#9c02564ee259bdd2251b82d659a2e7e1938d66f9" + integrity sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ== dependencies: - define-data-property "^1.1.4" - es-errors "^1.3.0" - function-bind "^1.1.2" - get-intrinsic "^1.2.4" - gopd "^1.0.1" - has-property-descriptors "^1.0.2" + encodeurl "^2.0.0" + escape-html "^1.0.3" + parseurl "^1.3.3" + send "^1.2.0" setprototypeof@1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== -side-channel@^1.0.6: - version "1.0.6" - resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.6.tgz#abd25fb7cd24baf45466406b1096b7831c9215f2" - integrity sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA== +side-channel-list@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/side-channel-list/-/side-channel-list-1.0.0.tgz#10cb5984263115d3b7a0e336591e290a830af8ad" + integrity sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA== dependencies: - call-bind "^1.0.7" es-errors "^1.3.0" - get-intrinsic "^1.2.4" - object-inspect "^1.13.1" + object-inspect "^1.13.3" + +side-channel-map@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/side-channel-map/-/side-channel-map-1.0.1.tgz#d6bb6b37902c6fef5174e5f533fab4c732a26f42" + integrity sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA== + dependencies: + call-bound "^1.0.2" + es-errors "^1.3.0" + get-intrinsic "^1.2.5" + object-inspect "^1.13.3" + +side-channel-weakmap@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz#11dda19d5368e40ce9ec2bdc1fb0ecbc0790ecea" + integrity sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A== + dependencies: + call-bound "^1.0.2" + es-errors "^1.3.0" + get-intrinsic "^1.2.5" + object-inspect "^1.13.3" + side-channel-map "^1.0.1" + +side-channel@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.1.0.tgz#c3fcff9c4da932784873335ec9765fa94ff66bc9" + integrity sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw== + dependencies: + es-errors "^1.3.0" + object-inspect "^1.13.3" + side-channel-list "^1.0.0" + side-channel-map "^1.0.1" + side-channel-weakmap "^1.0.2" simple-concat@^1.0.0: version "1.0.1" @@ -1597,11 +1463,6 @@ simple-update-notifier@^2.0.0: dependencies: semver "^7.5.3" -slash@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" - integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== - stack-trace@0.0.x: version "0.0.10" resolved "https://registry.yarnpkg.com/stack-trace/-/stack-trace-0.0.10.tgz#547c70b347e8d32b4e108ea1a2a159e5fdde19c0" @@ -1612,6 +1473,11 @@ statuses@2.0.1: resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63" integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== +statuses@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.2.tgz#8f75eecef765b5e1cfcdc080da59409ed424e382" + integrity sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw== + stream-meter@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/stream-meter/-/stream-meter-1.0.4.tgz#52af95aa5ea760a2491716704dbff90f73afdd1d" @@ -1628,7 +1494,7 @@ string-width@^4.1.0, string-width@^4.2.0: is-fullwidth-code-point "^3.0.0" strip-ansi "^6.0.1" -string_decoder@^1.1.1, string_decoder@^1.3.0: +string_decoder@^1.1.1: version "1.3.0" resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== @@ -1661,22 +1527,15 @@ supports-color@^5.5.0: dependencies: has-flag "^3.0.0" -supports-color@^7.1.0: - version "7.2.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" - integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== - dependencies: - has-flag "^4.0.0" - supports-preserve-symlinks-flag@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== tar-fs@^2.0.0, tar-fs@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-2.1.1.tgz#489a15ab85f1f0befabb370b7de4f9eb5cbe8784" - integrity sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng== + version "2.1.3" + resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-2.1.3.tgz#fb3b8843a26b6f13a08e606f7922875eb1fbbf92" + integrity sha512-090nwYJDmlhwFwEW3QQl+vaNnxsO2yVsd45eTKRBzSzu+hlb1w2K9inVq5b0ngXuLVqQ4ApvsUHHnu/zQNkWAg== dependencies: chownr "^1.1.1" mkdirp-classic "^0.5.2" @@ -1694,15 +1553,30 @@ tar-stream@^2.1.4: inherits "^2.0.3" readable-stream "^3.1.1" +tar@^7.4.3: + version "7.4.3" + resolved "https://registry.yarnpkg.com/tar/-/tar-7.4.3.tgz#88bbe9286a3fcd900e94592cda7a22b192e80571" + integrity sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw== + dependencies: + "@isaacs/fs-minipass" "^4.0.0" + chownr "^3.0.0" + minipass "^7.1.2" + minizlib "^3.0.1" + mkdirp "^3.0.1" + yallist "^5.0.0" + text-hex@1.0.x: version "1.0.0" resolved "https://registry.yarnpkg.com/text-hex/-/text-hex-1.0.0.tgz#69dc9c1b17446ee79a92bf5b884bb4b9127506f5" integrity sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg== -to-fast-properties@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e" - integrity sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog== +tinyglobby@^0.2.11: + version "0.2.14" + resolved "https://registry.yarnpkg.com/tinyglobby/-/tinyglobby-0.2.14.tgz#5280b0cf3f972b050e74ae88406c0a6a58f4079d" + integrity sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ== + dependencies: + fdir "^6.4.4" + picomatch "^4.0.2" to-regex-range@^5.0.1: version "5.0.1" @@ -1757,49 +1631,61 @@ tunnel-agent@^0.6.0: dependencies: safe-buffer "^5.0.1" -type-is@~1.6.18: - version "1.6.18" - resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" - integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== +type-is@^2.0.0, type-is@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/type-is/-/type-is-2.0.1.tgz#64f6cf03f92fce4015c2b224793f6bdd4b068c97" + integrity sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw== dependencies: - media-typer "0.3.0" - mime-types "~2.1.24" + content-type "^1.0.5" + media-typer "^1.1.0" + mime-types "^3.0.0" -typescript@5.5.4: - version "5.5.4" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.5.4.tgz#d9852d6c82bad2d2eda4fd74a5762a8f5909e9ba" - integrity sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q== +typescript@5.9.2: + version "5.9.2" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.9.2.tgz#d93450cddec5154a2d5cabe3b8102b83316fb2a6" + integrity sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A== undefsafe@^2.0.5: version "2.0.5" resolved "https://registry.yarnpkg.com/undefsafe/-/undefsafe-2.0.5.tgz#38733b9327bdcd226db889fb723a6efd162e6e2c" integrity sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA== -undici-types@~6.19.2, undici-types@~6.19.8: - version "6.19.8" - resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.19.8.tgz#35111c9d1437ab83a7cdc0abae2f26d88eda0a02" - integrity sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw== +undici-types@~6.21.0: + version "6.21.0" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.21.0.tgz#691d00af3909be93a7faa13be61b3a5b50ef12cb" + integrity sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ== + +undici-types@~7.10.0: + version "7.10.0" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-7.10.0.tgz#4ac2e058ce56b462b056e629cc6a02393d3ff350" + integrity sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag== universalify@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.1.tgz#168efc2180964e6386d061e094df61afe239b18d" integrity sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw== -unpipe@1.0.0, unpipe@~1.0.0: +unpipe@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ== +unzipper@^0.12.3: + version "0.12.3" + resolved "https://registry.yarnpkg.com/unzipper/-/unzipper-0.12.3.tgz#31958f5eed7368ed8f57deae547e5a673e984f87" + integrity sha512-PZ8hTS+AqcGxsaQntl3IRBw65QrBI6lxzqDEL7IAo/XCEqRTKGfOX56Vea5TH9SZczRVxuzk1re04z/YjuYCJA== + dependencies: + bluebird "~3.7.2" + duplexer2 "~0.1.4" + fs-extra "^11.2.0" + graceful-fs "^4.2.2" + node-int64 "^0.4.0" + util-deprecate@^1.0.1, util-deprecate@~1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== -utils-merge@1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" - integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA== - uuid-parse@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/uuid-parse/-/uuid-parse-1.1.0.tgz#7061c5a1384ae0e1f943c538094597e1b5f3a65b" @@ -1810,7 +1696,7 @@ v8-compile-cache-lib@^3.0.1: resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz#6336e8d71965cb3d35a1bbb7868445a7c05264bf" integrity sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg== -vary@~1.1.2: +vary@^1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg== @@ -1838,31 +1724,31 @@ winston-daily-rotate-file@^5.0.0: triple-beam "^1.4.1" winston-transport "^4.7.0" -winston-transport@^4.7.0: - version "4.8.0" - resolved "https://registry.yarnpkg.com/winston-transport/-/winston-transport-4.8.0.tgz#a15080deaeb80338455ac52c863418c74fcf38ea" - integrity sha512-qxSTKswC6llEMZKgCQdaWgDuMJQnhuvF5f2Nk3SNXc4byfQ+voo2mX1Px9dkNOuR8p0KAjfPG29PuYUSIb+vSA== +winston-transport@^4.7.0, winston-transport@^4.9.0: + version "4.9.0" + resolved "https://registry.yarnpkg.com/winston-transport/-/winston-transport-4.9.0.tgz#3bba345de10297654ea6f33519424560003b3bf9" + integrity sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A== dependencies: - logform "^2.6.1" - readable-stream "^4.5.2" + logform "^2.7.0" + readable-stream "^3.6.2" triple-beam "^1.3.0" -winston@^3.16.0: - version "3.16.0" - resolved "https://registry.yarnpkg.com/winston/-/winston-3.16.0.tgz#d11caabada87b7d4b59aba9a94b882121b773f9b" - integrity sha512-xz7+cyGN5M+4CmmD4Npq1/4T+UZaz7HaeTlAruFUTjk79CNMq+P6H30vlE4z0qfqJ01VHYQwd7OZo03nYm/+lg== +winston@^3.17.0: + version "3.17.0" + resolved "https://registry.yarnpkg.com/winston/-/winston-3.17.0.tgz#74b8665ce9b4ea7b29d0922cfccf852a08a11423" + integrity sha512-DLiFIXYC5fMPxaRg832S6F5mJYvePtmO5G9v9IgUFPhXm9/GkXarH/TUrBAVzhTCzAj9anE/+GjrgXp/54nOgw== dependencies: "@colors/colors" "^1.6.0" "@dabh/diagnostics" "^2.0.2" async "^3.2.3" is-stream "^2.0.0" - logform "^2.6.0" + logform "^2.7.0" one-time "^1.0.0" readable-stream "^3.4.0" safe-stable-stringify "^2.3.1" stack-trace "0.0.x" triple-beam "^1.3.0" - winston-transport "^4.7.0" + winston-transport "^4.9.0" wrap-ansi@^7.0.0: version "7.0.0" @@ -1883,6 +1769,11 @@ y18n@^5.0.5: resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55" integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA== +yallist@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-5.0.0.tgz#00e2de443639ed0d78fd87de0d27469fbcffb533" + integrity sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw== + yargs-parser@^20.2.2: version "20.2.9" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee" diff --git a/msa/web-ui/docker/Dockerfile b/msa/web-ui/docker/Dockerfile index 4c6f30841e..f807819201 100644 --- a/msa/web-ui/docker/Dockerfile +++ b/msa/web-ui/docker/Dockerfile @@ -14,7 +14,7 @@ # limitations under the License. # -FROM thingsboard/node:20.18.0-bookworm-slim +FROM thingsboard/node:22.18.0-bookworm-slim ENV NODE_ENV production ENV DOCKER_MODE true diff --git a/msa/web-ui/package.json b/msa/web-ui/package.json index 3cdea8fb72..1eacf42683 100644 --- a/msa/web-ui/package.json +++ b/msa/web-ui/package.json @@ -6,21 +6,21 @@ "main": "server.ts", "bin": "server.js", "scripts": { - "pkg": "tsc && pkg -t node18-linux-x64,node18-win-x64 --out-path ./target ./target/src && node install.js", + "pkg": "tsc && pkg -t node22-linux-x64,node22-win-x64 --out-path ./target ./target/src && node install.js", "test": "echo \"Error: no test specified\" && exit 1", "start": "nodemon --watch '.' --ext 'ts' --exec 'WEB_FOLDER=./target/web ts-node server.ts'", "start-prod": "nodemon --watch '.' --ext 'ts' --exec 'WEB_FOLDER=./target/web NODE_ENV=production ts-node server.ts'", "build": "tsc" }, "dependencies": { - "compression": "^1.7.5", + "compression": "^1.8.1", "config": "^3.3.12", - "connect-history-api-fallback": "^1.6.0", - "express": "^4.21.1", + "connect-history-api-fallback": "1.6.0", + "express": "^5.1.0", "http": "0.0.0", "http-proxy": "^1.18.1", "js-yaml": "^4.1.0", - "winston": "^3.16.0", + "winston": "^3.17.0", "winston-daily-rotate-file": "^5.0.0" }, "nyc": { @@ -32,17 +32,17 @@ ] }, "devDependencies": { - "@types/compression": "^1.7.5", + "@types/compression": "^1.8.1", "@types/config": "^3.3.5", - "@types/connect-history-api-fallback": "^1.5.4", - "@types/express": "~4.17.21", - "@types/http-proxy": "^1.17.15", - "@types/node": "~20.17.6", - "fs-extra": "^11.2.0", - "nodemon": "^3.1.7", - "pkg": "^5.8.1", + "@types/connect-history-api-fallback": "1.5.4", + "@types/express": "~5.0.3", + "@types/http-proxy": "^1.17.16", + "@types/node": "~22.17.2", + "@yao-pkg/pkg": "^6.6.0", + "fs-extra": "^11.3.1", + "nodemon": "^3.1.10", "ts-node": "^10.9.2", - "typescript": "5.5.4" + "typescript": "5.9.2" }, "pkg": { "assets": [ diff --git a/msa/web-ui/pom.xml b/msa/web-ui/pom.xml index ce3475b113..db7ed1424a 100644 --- a/msa/web-ui/pom.xml +++ b/msa/web-ui/pom.xml @@ -80,7 +80,7 @@ install-node-and-yarn - v20.18.0 + v22.18.0 v1.22.22 diff --git a/msa/web-ui/yarn.lock b/msa/web-ui/yarn.lock index 83e0d79084..7af758435e 100644 --- a/msa/web-ui/yarn.lock +++ b/msa/web-ui/yarn.lock @@ -2,46 +2,41 @@ # yarn lockfile v1 -"@babel/generator@7.18.2": - version "7.18.2" - resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.18.2.tgz#33873d6f89b21efe2da63fe554460f3df1c5880d" - integrity sha512-W1lG5vUwFvfMd8HVXqdfbuG7RuaSrTCCD8cl8fP8wOivdbtbIg2Db3IWUcgvfxKbbn6ZBGYRW/Zk1MIwK49mgw== - dependencies: - "@babel/types" "^7.18.2" - "@jridgewell/gen-mapping" "^0.3.0" - jsesc "^2.5.1" - -"@babel/helper-string-parser@^7.18.10", "@babel/helper-string-parser@^7.25.9": - version "7.25.9" - resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz#1aabb72ee72ed35789b4bbcad3ca2862ce614e8c" - integrity sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA== - -"@babel/helper-validator-identifier@^7.18.6", "@babel/helper-validator-identifier@^7.25.9": - version "7.25.9" - resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz#24b64e2c3ec7cd3b3c547729b8d16871f22cbdc7" - integrity sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ== - -"@babel/parser@7.18.4": - version "7.18.4" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.18.4.tgz#6774231779dd700e0af29f6ad8d479582d7ce5ef" - integrity sha512-FDge0dFazETFcxGw/EXzOkN8uJp0PC7Qbm+Pe9T+av2zlBpOgunFHkQPPn+eRuClU73JF+98D531UgayY89tow== - -"@babel/types@7.19.0": - version "7.19.0" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.19.0.tgz#75f21d73d73dc0351f3368d28db73465f4814600" - integrity sha512-YuGopBq3ke25BVSiS6fgF49Ul9gH1x70Bcr6bqRLjWCkcX8Hre1/5+z+IiWOIerRMSSEfGZVB9z9kyq7wVs9YA== - dependencies: - "@babel/helper-string-parser" "^7.18.10" - "@babel/helper-validator-identifier" "^7.18.6" - to-fast-properties "^2.0.0" - -"@babel/types@^7.18.2": - version "7.26.0" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.26.0.tgz#deabd08d6b753bc8e0f198f8709fb575e31774ff" - integrity sha512-Z/yiTPj+lDVnF7lWeKCIJzaIkI0vYO87dMpZ4bg4TDrFe4XXLFWL1TbXU27gBP3QccxV9mZICCrnjnYlJjXHOA== - dependencies: - "@babel/helper-string-parser" "^7.25.9" - "@babel/helper-validator-identifier" "^7.25.9" +"@babel/generator@^7.23.0": + version "7.28.3" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.28.3.tgz#9626c1741c650cbac39121694a0f2d7451b8ef3e" + integrity sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw== + dependencies: + "@babel/parser" "^7.28.3" + "@babel/types" "^7.28.2" + "@jridgewell/gen-mapping" "^0.3.12" + "@jridgewell/trace-mapping" "^0.3.28" + jsesc "^3.0.2" + +"@babel/helper-string-parser@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz#54da796097ab19ce67ed9f88b47bb2ec49367687" + integrity sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA== + +"@babel/helper-validator-identifier@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz#a7054dcc145a967dd4dc8fee845a57c1316c9df8" + integrity sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow== + +"@babel/parser@^7.23.0", "@babel/parser@^7.28.3": + version "7.28.3" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.28.3.tgz#d2d25b814621bca5fe9d172bc93792547e7a2a71" + integrity sha512-7+Ey1mAgYqFAx2h0RuoxcQT5+MlG3GTV0TQrgr7/ZliKsm/MNDxVVutlWaziMq7wJNAz8MTqz55XLpWvva6StA== + dependencies: + "@babel/types" "^7.28.2" + +"@babel/types@^7.23.0", "@babel/types@^7.28.2": + version "7.28.2" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.28.2.tgz#da9db0856a9a88e0a13b019881d7513588cf712b" + integrity sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ== + dependencies: + "@babel/helper-string-parser" "^7.27.1" + "@babel/helper-validator-identifier" "^7.27.1" "@colors/colors@1.6.0", "@colors/colors@^1.6.0": version "1.6.0" @@ -64,13 +59,19 @@ enabled "2.0.x" kuler "^2.0.0" -"@jridgewell/gen-mapping@^0.3.0": - version "0.3.5" - resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz#dcce6aff74bdf6dad1a95802b69b04a2fcb1fb36" - integrity sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg== +"@isaacs/fs-minipass@^4.0.0": + version "4.0.1" + resolved "https://registry.yarnpkg.com/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz#2d59ae3ab4b38fb4270bfa23d30f8e2e86c7fe32" + integrity sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w== dependencies: - "@jridgewell/set-array" "^1.2.1" - "@jridgewell/sourcemap-codec" "^1.4.10" + minipass "^7.0.4" + +"@jridgewell/gen-mapping@^0.3.12": + version "0.3.13" + resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz#6342a19f44347518c93e43b1ac69deb3c4656a1f" + integrity sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA== + dependencies: + "@jridgewell/sourcemap-codec" "^1.5.0" "@jridgewell/trace-mapping" "^0.3.24" "@jridgewell/resolve-uri@^3.0.3", "@jridgewell/resolve-uri@^3.1.0": @@ -78,15 +79,10 @@ resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz#7a0ee601f60f99a20c7c7c5ff0c80388c1189bd6" integrity sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw== -"@jridgewell/set-array@^1.2.1": - version "1.2.1" - resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.2.1.tgz#558fb6472ed16a4c850b889530e6b36438c49280" - integrity sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A== - -"@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@^1.4.14": - version "1.5.0" - resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz#3188bcb273a414b0d215fd22a58540b989b9409a" - integrity sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ== +"@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@^1.4.14", "@jridgewell/sourcemap-codec@^1.5.0": + version "1.5.5" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz#6912b00d2c631c0d15ce1a7ab57cd657f2a8f8ba" + integrity sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og== "@jridgewell/trace-mapping@0.3.9": version "0.3.9" @@ -96,35 +92,14 @@ "@jridgewell/resolve-uri" "^3.0.3" "@jridgewell/sourcemap-codec" "^1.4.10" -"@jridgewell/trace-mapping@^0.3.24": - version "0.3.25" - resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz#15f190e98895f3fc23276ee14bc76b675c2e50f0" - integrity sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ== +"@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.28": + version "0.3.30" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.30.tgz#4a76c4daeee5df09f5d3940e087442fb36ce2b99" + integrity sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q== dependencies: "@jridgewell/resolve-uri" "^3.1.0" "@jridgewell/sourcemap-codec" "^1.4.14" -"@nodelib/fs.scandir@2.1.5": - version "2.1.5" - resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" - integrity sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g== - dependencies: - "@nodelib/fs.stat" "2.0.5" - run-parallel "^1.1.9" - -"@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2": - version "2.0.5" - resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz#5bd262af94e9d25bd1e71b05deed44876a222e8b" - integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== - -"@nodelib/fs.walk@^1.2.3": - version "1.2.8" - resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz#e95737e8bb6746ddedf69c556953494f196fe69a" - integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg== - dependencies: - "@nodelib/fs.scandir" "2.1.5" - fastq "^1.6.0" - "@tsconfig/node10@^1.0.7": version "1.0.11" resolved "https://registry.yarnpkg.com/@tsconfig/node10/-/node10-1.0.11.tgz#6ee46400685f130e278128c7b38b7e031ff5b2f2" @@ -146,26 +121,27 @@ integrity sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA== "@types/body-parser@*": - version "1.19.5" - resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.5.tgz#04ce9a3b677dc8bd681a17da1ab9835dc9d3ede4" - integrity sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg== + version "1.19.6" + resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.6.tgz#1859bebb8fd7dac9918a45d54c1971ab8b5af474" + integrity sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g== dependencies: "@types/connect" "*" "@types/node" "*" -"@types/compression@^1.7.5": - version "1.7.5" - resolved "https://registry.yarnpkg.com/@types/compression/-/compression-1.7.5.tgz#0f80efef6eb031be57b12221c4ba6bc3577808f7" - integrity sha512-AAQvK5pxMpaT+nDvhHrsBhLSYG5yQdtkaJE1WYieSNY2mVFKAgmU4ks65rkZD5oqnGCFLyQpUr1CqI4DmUMyDg== +"@types/compression@^1.8.1": + version "1.8.1" + resolved "https://registry.yarnpkg.com/@types/compression/-/compression-1.8.1.tgz#57cd1a5c0c585aca56124ab4daef1d254d6f5a7d" + integrity sha512-kCFuWS0ebDbmxs0AXYn6e2r2nrGAb5KwQhknjSPSPgJcGd8+HVSILlUyFhGqML2gk39HcG7D1ydW9/qpYkN00Q== dependencies: "@types/express" "*" + "@types/node" "*" "@types/config@^3.3.5": version "3.3.5" resolved "https://registry.yarnpkg.com/@types/config/-/config-3.3.5.tgz#91f0a52b10212b9c4254fb0bbc4bedd77cd389b8" integrity sha512-itq2HtXQBrNUKwMNZnb9mBRE3T99VYCdl1gjST9rq+9kFaB1iMMGuDeZnP88qid73DnpAMKH9ZolqDpS1Lz7+w== -"@types/connect-history-api-fallback@^1.5.4": +"@types/connect-history-api-fallback@1.5.4": version "1.5.4" resolved "https://registry.yarnpkg.com/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.5.4.tgz#7de71645a103056b48ac3ce07b3520b819c1d5b3" integrity sha512-n6Cr2xS1h4uAulPRdlw6Jl6s1oG8KrVilPN2yUITEs+K48EzMJJ3W1xy8K5eWuFvjp3R74AOIGSmp2UfBJ8HFw== @@ -180,35 +156,34 @@ dependencies: "@types/node" "*" -"@types/express-serve-static-core@^4.17.33", "@types/express-serve-static-core@*": - version "4.19.6" - resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.19.6.tgz#e01324c2a024ff367d92c66f48553ced0ab50267" - integrity sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A== +"@types/express-serve-static-core@*", "@types/express-serve-static-core@^5.0.0": + version "5.0.7" + resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-5.0.7.tgz#2fa94879c9d46b11a5df4c74ac75befd6b283de6" + integrity sha512-R+33OsgWw7rOhD1emjU7dzCDHucJrgJXMA5PYCzJxVil0dsyx5iBEPHqpPfiKNJQb7lZ1vxwoLR4Z87bBUpeGQ== dependencies: "@types/node" "*" "@types/qs" "*" "@types/range-parser" "*" "@types/send" "*" -"@types/express@*", "@types/express@~4.17.21": - version "4.17.21" - resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.21.tgz#c26d4a151e60efe0084b23dc3369ebc631ed192d" - integrity sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ== +"@types/express@*", "@types/express@~5.0.3": + version "5.0.3" + resolved "https://registry.yarnpkg.com/@types/express/-/express-5.0.3.tgz#6c4bc6acddc2e2a587142e1d8be0bce20757e956" + integrity sha512-wGA0NX93b19/dZC1J18tKWVIYWyyF2ZjT9vin/NRu0qzzvfVzWjs04iq2rQ3H65vCTQYlRqs3YHfY7zjdV+9Kw== dependencies: "@types/body-parser" "*" - "@types/express-serve-static-core" "^4.17.33" - "@types/qs" "*" + "@types/express-serve-static-core" "^5.0.0" "@types/serve-static" "*" "@types/http-errors@*": - version "2.0.4" - resolved "https://registry.yarnpkg.com/@types/http-errors/-/http-errors-2.0.4.tgz#7eb47726c391b7345a6ec35ad7f4de469cf5ba4f" - integrity sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA== + version "2.0.5" + resolved "https://registry.yarnpkg.com/@types/http-errors/-/http-errors-2.0.5.tgz#5b749ab2b16ba113423feb1a64a95dcd30398472" + integrity sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg== -"@types/http-proxy@^1.17.15": - version "1.17.15" - resolved "https://registry.yarnpkg.com/@types/http-proxy/-/http-proxy-1.17.15.tgz#12118141ce9775a6499ecb4c01d02f90fc839d36" - integrity sha512-25g5atgiVNTIv0LBDTg1H74Hvayx0ajtJPLLcYE3whFv75J0pWNtOBzaXJQgDTmrX1bx5U9YC2w/n65BN1HwRQ== +"@types/http-proxy@^1.17.16": + version "1.17.16" + resolved "https://registry.yarnpkg.com/@types/http-proxy/-/http-proxy-1.17.16.tgz#dee360707b35b3cc85afcde89ffeebff7d7f9240" + integrity sha512-sdWoUajOB1cd0A8cRRQ1cfyWNbmFKLAqBB89Y8x5iYyG/mkJHc0YUH8pdWBy2omi9qtCpiIgGjuwO0dQST2l5w== dependencies: "@types/node" "*" @@ -218,23 +193,23 @@ integrity sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w== "@types/node@*": - version "22.8.7" - resolved "https://registry.yarnpkg.com/@types/node/-/node-22.8.7.tgz#04ab7a073d95b4a6ee899f235d43f3c320a976f4" - integrity sha512-LidcG+2UeYIWcMuMUpBKOnryBWG/rnmOHQR5apjn8myTQcx3rinFRn7DcIFhMnS0PPFSC6OafdIKEad0lj6U0Q== + version "24.3.0" + resolved "https://registry.yarnpkg.com/@types/node/-/node-24.3.0.tgz#89b09f45cb9a8ee69466f18ee5864e4c3eb84dec" + integrity sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow== dependencies: - undici-types "~6.19.8" + undici-types "~7.10.0" -"@types/node@~20.17.6": - version "20.17.6" - resolved "https://registry.yarnpkg.com/@types/node/-/node-20.17.6.tgz#6e4073230c180d3579e8c60141f99efdf5df0081" - integrity sha512-VEI7OdvK2wP7XHnsuXbAJnEpEkF6NjSN45QJlL4VGqZSXsnicpesdTWsg9RISeSdYd3yeRj/y3k5KGjUXYnFwQ== +"@types/node@~22.17.2": + version "22.17.2" + resolved "https://registry.yarnpkg.com/@types/node/-/node-22.17.2.tgz#47a93d6f4b79327da63af727e7c54e8cab8c4d33" + integrity sha512-gL6z5N9Jm9mhY+U2KXZpteb+09zyffliRkZyZOHODGATyC5B1Jt/7TzuuiLkFsSUMLbS1OLmlj/E+/3KF4Q/4w== dependencies: - undici-types "~6.19.2" + undici-types "~6.21.0" "@types/qs@*": - version "6.9.16" - resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.16.tgz#52bba125a07c0482d26747d5d4947a64daf8f794" - integrity sha512-7i+zxXdPD0T4cKDuxCUXJ4wHcsJLwENa6Z3dCu8cfCK743OGy5Nu1RmAGqDPsoTDINVEcdXKRvR/zre+P2Ku1A== + version "6.14.0" + resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.14.0.tgz#d8b60cecf62f2db0fb68e5e006077b9178b85de5" + integrity sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ== "@types/range-parser@*": version "1.2.7" @@ -242,17 +217,17 @@ integrity sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ== "@types/send@*": - version "0.17.4" - resolved "https://registry.yarnpkg.com/@types/send/-/send-0.17.4.tgz#6619cd24e7270793702e4e6a4b958a9010cfc57a" - integrity sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA== + version "0.17.5" + resolved "https://registry.yarnpkg.com/@types/send/-/send-0.17.5.tgz#d991d4f2b16f2b1ef497131f00a9114290791e74" + integrity sha512-z6F2D3cOStZvuk2SaP6YrwkNO65iTZcwA2ZkSABegdkAh/lf+Aa/YQndZVfmEXT5vgAp6zv06VQ3ejSVjAny4w== dependencies: "@types/mime" "^1" "@types/node" "*" "@types/serve-static@*": - version "1.15.7" - resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.15.7.tgz#22174bbd74fb97fe303109738e9b5c2f3064f714" - integrity sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw== + version "1.15.8" + resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.15.8.tgz#8180c3fbe4a70e8f00b9f70b9ba7f08f35987877" + integrity sha512-roei0UY3LhpOJvjbIP6ZZFngyLKl5dskOtDhxY5THRSpO+ZI+nzJ+m5yUMzGrp89YRa7lvknKkMYjqQFGwA7Sg== dependencies: "@types/http-errors" "*" "@types/node" "*" @@ -263,20 +238,47 @@ resolved "https://registry.yarnpkg.com/@types/triple-beam/-/triple-beam-1.3.5.tgz#74fef9ffbaa198eb8b588be029f38b00299caa2c" integrity sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw== -abort-controller@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/abort-controller/-/abort-controller-3.0.0.tgz#eaf54d53b62bae4138e809ca225c8439a6efb392" - integrity sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg== +"@yao-pkg/pkg-fetch@3.5.24": + version "3.5.24" + resolved "https://registry.yarnpkg.com/@yao-pkg/pkg-fetch/-/pkg-fetch-3.5.24.tgz#37a671f077fe6446aec0758e6fc7961d183c3445" + integrity sha512-FPESCH1uXCYui6jeDp2aayWuFHR39w+uU1r88nI6JWRvPYOU64cHPUV/p6GSFoQdpna7ip92HnrZKbBC60l0gA== dependencies: - event-target-shim "^5.0.0" + https-proxy-agent "^5.0.0" + node-fetch "^2.6.6" + picocolors "^1.1.0" + progress "^2.0.3" + semver "^7.3.5" + tar-fs "^2.1.1" + yargs "^16.2.0" -accepts@~1.3.8: - version "1.3.8" - resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e" - integrity sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw== +"@yao-pkg/pkg@^6.6.0": + version "6.6.0" + resolved "https://registry.yarnpkg.com/@yao-pkg/pkg/-/pkg-6.6.0.tgz#e8c38ed5824381c676e6688f93e27f39e1752701" + integrity sha512-3/oiaSm7fS0Fc7dzp22r9B7vFaguGhO9vERgEReRYj2EUzdi5ssyYhe1uYJG4ec/dmo2GG6RRHOUAT8savl79Q== dependencies: - mime-types "~2.1.34" - negotiator "0.6.3" + "@babel/generator" "^7.23.0" + "@babel/parser" "^7.23.0" + "@babel/types" "^7.23.0" + "@yao-pkg/pkg-fetch" "3.5.24" + into-stream "^6.0.0" + minimist "^1.2.6" + multistream "^4.1.0" + picocolors "^1.1.0" + picomatch "^4.0.2" + prebuild-install "^7.1.1" + resolve "^1.22.10" + stream-meter "^1.0.4" + tar "^7.4.3" + tinyglobby "^0.2.11" + unzipper "^0.12.3" + +accepts@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/accepts/-/accepts-2.0.0.tgz#bbcf4ba5075467f3f2131eab3cffc73c2f5d7895" + integrity sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng== + dependencies: + mime-types "^3.0.0" + negotiator "^1.0.0" acorn-walk@^8.1.1: version "8.3.4" @@ -286,9 +288,9 @@ acorn-walk@^8.1.1: acorn "^8.11.0" acorn@^8.11.0, acorn@^8.4.1: - version "8.14.0" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.14.0.tgz#063e2c70cac5fb4f6467f0b11152e04c682795b0" - integrity sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA== + version "8.15.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.15.0.tgz#a360898bc415edaac46c8241f6383975b930b816" + integrity sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg== agent-base@6: version "6.0.2" @@ -302,7 +304,7 @@ ansi-regex@^5.0.1: resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== -ansi-styles@^4.0.0, ansi-styles@^4.1.0: +ansi-styles@^4.0.0: version "4.3.0" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== @@ -327,26 +329,11 @@ argparse@^2.0.1: resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== -array-flatten@1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" - integrity sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg== - -array-union@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d" - integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== - async@^3.2.3: version "3.2.6" resolved "https://registry.yarnpkg.com/async/-/async-3.2.6.tgz#1b0728e14929d51b85b449b7f06e27c1145e38ce" integrity sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA== -at-least-node@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/at-least-node/-/at-least-node-1.0.0.tgz#602cd4b46e844ad4effc92a8011a3c46e0238dc2" - integrity sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg== - balanced-match@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" @@ -371,33 +358,35 @@ bl@^4.0.3: inherits "^2.0.4" readable-stream "^3.4.0" -body-parser@1.20.3: - version "1.20.3" - resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.3.tgz#1953431221c6fb5cd63c4b36d53fab0928e548c6" - integrity sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g== - dependencies: - bytes "3.1.2" - content-type "~1.0.5" - debug "2.6.9" - depd "2.0.0" - destroy "1.2.0" - http-errors "2.0.0" - iconv-lite "0.4.24" - on-finished "2.4.1" - qs "6.13.0" - raw-body "2.5.2" - type-is "~1.6.18" - unpipe "1.0.0" +bluebird@~3.7.2: + version "3.7.2" + resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" + integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== + +body-parser@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-2.2.0.tgz#f7a9656de305249a715b549b7b8fd1ab9dfddcfa" + integrity sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg== + dependencies: + bytes "^3.1.2" + content-type "^1.0.5" + debug "^4.4.0" + http-errors "^2.0.0" + iconv-lite "^0.6.3" + on-finished "^2.4.1" + qs "^6.14.0" + raw-body "^3.0.0" + type-is "^2.0.0" brace-expansion@^1.1.7: - version "1.1.11" - resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" - integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== + version "1.1.12" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.12.tgz#ab9b454466e5a8cc3a187beaad580412a9c5b843" + integrity sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg== dependencies: balanced-match "^1.0.0" concat-map "0.0.1" -braces@^3.0.3, braces@~3.0.2: +braces@~3.0.2: version "3.0.3" resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789" integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA== @@ -412,37 +401,26 @@ buffer@^5.5.0: base64-js "^1.3.1" ieee754 "^1.1.13" -buffer@^6.0.3: - version "6.0.3" - resolved "https://registry.yarnpkg.com/buffer/-/buffer-6.0.3.tgz#2ace578459cc8fbe2a70aaa8f52ee63b6a74c6c6" - integrity sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA== - dependencies: - base64-js "^1.3.1" - ieee754 "^1.2.1" - -bytes@3.1.2: +bytes@3.1.2, bytes@^3.1.2: version "3.1.2" resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== -call-bind@^1.0.7: - version "1.0.7" - resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.7.tgz#06016599c40c56498c18769d2730be242b6fa3b9" - integrity sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w== +call-bind-apply-helpers@^1.0.1, call-bind-apply-helpers@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz#4b5428c222be985d79c3d82657479dbe0b59b2d6" + integrity sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ== dependencies: - es-define-property "^1.0.0" es-errors "^1.3.0" function-bind "^1.1.2" - get-intrinsic "^1.2.4" - set-function-length "^1.2.1" -chalk@^4.1.2: - version "4.1.2" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" - integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== +call-bound@^1.0.2: + version "1.0.4" + resolved "https://registry.yarnpkg.com/call-bound/-/call-bound-1.0.4.tgz#238de935d2a2a692928c538c7ccfa91067fd062a" + integrity sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg== dependencies: - ansi-styles "^4.1.0" - supports-color "^7.1.0" + call-bind-apply-helpers "^1.0.2" + get-intrinsic "^1.3.0" chokidar@^3.5.2: version "3.6.0" @@ -464,6 +442,11 @@ chownr@^1.1.1: resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b" integrity sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg== +chownr@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/chownr/-/chownr-3.0.0.tgz#9855e64ecd240a9cc4267ce8a4aa5d24a1da15e4" + integrity sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g== + cliui@^7.0.2: version "7.0.4" resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.4.tgz#a0265ee655476fc807aea9df3df8df7783808b4f" @@ -528,16 +511,16 @@ compressible@~2.0.18: dependencies: mime-db ">= 1.43.0 < 2" -compression@^1.7.5: - version "1.7.5" - resolved "https://registry.yarnpkg.com/compression/-/compression-1.7.5.tgz#fdd256c0a642e39e314c478f6c2cd654edd74c93" - integrity sha512-bQJ0YRck5ak3LgtnpKkiabX5pNF7tMUh1BSy2ZBOTh0Dim0BUu6aPPwByIns6/A5Prh8PufSPerMDUklpzes2Q== +compression@^1.8.1: + version "1.8.1" + resolved "https://registry.yarnpkg.com/compression/-/compression-1.8.1.tgz#4a45d909ac16509195a9a28bd91094889c180d79" + integrity sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w== dependencies: bytes "3.1.2" compressible "~2.0.18" debug "2.6.9" negotiator "~0.6.4" - on-headers "~1.0.2" + on-headers "~1.1.0" safe-buffer "5.2.1" vary "~1.1.2" @@ -553,32 +536,32 @@ config@^3.3.12: dependencies: json5 "^2.2.3" -connect-history-api-fallback@^1.6.0: +connect-history-api-fallback@1.6.0: version "1.6.0" resolved "https://registry.yarnpkg.com/connect-history-api-fallback/-/connect-history-api-fallback-1.6.0.tgz#8b32089359308d111115d81cad3fceab888f97bc" integrity sha512-e54B99q/OUoH64zYYRf3HBP5z24G38h5D3qXu23JGRoigpX5Ss4r9ZnDk3g0Z8uQC2x2lPaJ+UlWBc1ZWBWdLg== -content-disposition@0.5.4: - version "0.5.4" - resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.4.tgz#8b82b4efac82512a02bb0b1dcec9d2c5e8eb5bfe" - integrity sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ== +content-disposition@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-1.0.0.tgz#844426cb398f934caefcbb172200126bc7ceace2" + integrity sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg== dependencies: safe-buffer "5.2.1" -content-type@~1.0.4, content-type@~1.0.5: +content-type@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.5.tgz#8b773162656d1d1086784c8f23a54ce6d73d7918" integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA== -cookie-signature@1.0.6: - version "1.0.6" - resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" - integrity sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ== +cookie-signature@^1.2.1: + version "1.2.2" + resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.2.2.tgz#57c7fc3cc293acab9fec54d73e15690ebe4a1793" + integrity sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg== -cookie@0.7.1: - version "0.7.1" - resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.7.1.tgz#2f73c42142d5d5cf71310a74fc4ae61670e5dbc9" - integrity sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w== +cookie@^0.7.1: + version "0.7.2" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.7.2.tgz#556369c472a2ba910f2979891b526b3436237ed7" + integrity sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w== core-util-is@~1.0.0: version "1.0.3" @@ -597,10 +580,10 @@ debug@2.6.9: dependencies: ms "2.0.0" -debug@4, debug@^4: - version "4.3.7" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.7.tgz#87945b4151a011d76d95a198d7111c865c360a52" - integrity sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ== +debug@4, debug@^4, debug@^4.3.5, debug@^4.4.0: + version "4.4.1" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.1.tgz#e5a8bc6cbc4c6cd3e64308b0693a3d4fa550189b" + integrity sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ== dependencies: ms "^2.1.3" @@ -616,41 +599,36 @@ deep-extend@^0.6.0: resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac" integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA== -define-data-property@^1.1.4: - version "1.1.4" - resolved "https://registry.yarnpkg.com/define-data-property/-/define-data-property-1.1.4.tgz#894dc141bb7d3060ae4366f6a0107e68fbe48c5e" - integrity sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A== - dependencies: - es-define-property "^1.0.0" - es-errors "^1.3.0" - gopd "^1.0.1" - -depd@2.0.0: +depd@2.0.0, depd@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== -destroy@1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015" - integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg== - detect-libc@^2.0.0: - version "2.0.3" - resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.0.3.tgz#f0cd503b40f9939b894697d19ad50895e30cf700" - integrity sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw== + version "2.0.4" + resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.0.4.tgz#f04715b8ba815e53b4d8109655b6508a6865a7e8" + integrity sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA== diff@^4.0.1: version "4.0.2" resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== -dir-glob@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f" - integrity sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA== +dunder-proto@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/dunder-proto/-/dunder-proto-1.0.1.tgz#d7ae667e1dc83482f8b70fd0f6eefc50da30f58a" + integrity sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A== + dependencies: + call-bind-apply-helpers "^1.0.1" + es-errors "^1.3.0" + gopd "^1.2.0" + +duplexer2@~0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/duplexer2/-/duplexer2-0.1.4.tgz#8b12dab878c0d69e3e7891051662a32fc6bddcc1" + integrity sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA== dependencies: - path-type "^4.0.0" + readable-stream "^2.0.2" ee-first@1.1.1: version "1.1.1" @@ -667,124 +645,97 @@ enabled@2.0.x: resolved "https://registry.yarnpkg.com/enabled/-/enabled-2.0.0.tgz#f9dd92ec2d6f4bbc0d5d1e64e21d61cd4665e7c2" integrity sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ== -encodeurl@~1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" - integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w== - -encodeurl@~2.0.0: +encodeurl@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-2.0.0.tgz#7b8ea898077d7e409d3ac45474ea38eaf0857a58" integrity sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg== end-of-stream@^1.1.0, end-of-stream@^1.4.1: - version "1.4.4" - resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" - integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== + version "1.4.5" + resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.5.tgz#7344d711dea40e0b74abc2ed49778743ccedb08c" + integrity sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg== dependencies: once "^1.4.0" -es-define-property@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.0.tgz#c7faefbdff8b2696cf5f46921edfb77cc4ba3845" - integrity sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ== - dependencies: - get-intrinsic "^1.2.4" +es-define-property@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.1.tgz#983eb2f9a6724e9303f61addf011c72e09e0b0fa" + integrity sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g== es-errors@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f" integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== +es-object-atoms@^1.0.0, es-object-atoms@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz#1c4f2c4837327597ce69d2ca190a7fdd172338c1" + integrity sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA== + dependencies: + es-errors "^1.3.0" + escalade@^3.1.1: version "3.2.0" resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.2.0.tgz#011a3f69856ba189dffa7dc8fcce99d2a87903e5" integrity sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA== -escape-html@~1.0.3: +escape-html@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow== -etag@~1.8.1: +etag@^1.8.1: version "1.8.1" resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg== -event-target-shim@^5.0.0: - version "5.0.1" - resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789" - integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ== - eventemitter3@^4.0.0: version "4.0.7" resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f" integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw== -events@^3.3.0: - version "3.3.0" - resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" - integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== - expand-template@^2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/expand-template/-/expand-template-2.0.3.tgz#6e14b3fcee0f3a6340ecb57d2e8918692052a47c" integrity sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg== -express@^4.21.1: - version "4.21.1" - resolved "https://registry.yarnpkg.com/express/-/express-4.21.1.tgz#9dae5dda832f16b4eec941a4e44aa89ec481b281" - integrity sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ== - dependencies: - accepts "~1.3.8" - array-flatten "1.1.1" - body-parser "1.20.3" - content-disposition "0.5.4" - content-type "~1.0.4" - cookie "0.7.1" - cookie-signature "1.0.6" - debug "2.6.9" - depd "2.0.0" - encodeurl "~2.0.0" - escape-html "~1.0.3" - etag "~1.8.1" - finalhandler "1.3.1" - fresh "0.5.2" - http-errors "2.0.0" - merge-descriptors "1.0.3" - methods "~1.1.2" - on-finished "2.4.1" - parseurl "~1.3.3" - path-to-regexp "0.1.10" - proxy-addr "~2.0.7" - qs "6.13.0" - range-parser "~1.2.1" - safe-buffer "5.2.1" - send "0.19.0" - serve-static "1.16.2" - setprototypeof "1.2.0" - statuses "2.0.1" - type-is "~1.6.18" - utils-merge "1.0.1" - vary "~1.1.2" - -fast-glob@^3.2.9: - version "3.3.2" - resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.2.tgz#a904501e57cfdd2ffcded45e99a54fef55e46129" - integrity sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow== - dependencies: - "@nodelib/fs.stat" "^2.0.2" - "@nodelib/fs.walk" "^1.2.3" - glob-parent "^5.1.2" - merge2 "^1.3.0" - micromatch "^4.0.4" - -fastq@^1.6.0: - version "1.17.1" - resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.17.1.tgz#2a523f07a4e7b1e81a42b91b8bf2254107753b47" - integrity sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w== - dependencies: - reusify "^1.0.4" +express@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/express/-/express-5.1.0.tgz#d31beaf715a0016f0d53f47d3b4d7acf28c75cc9" + integrity sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA== + dependencies: + accepts "^2.0.0" + body-parser "^2.2.0" + content-disposition "^1.0.0" + content-type "^1.0.5" + cookie "^0.7.1" + cookie-signature "^1.2.1" + debug "^4.4.0" + encodeurl "^2.0.0" + escape-html "^1.0.3" + etag "^1.8.1" + finalhandler "^2.1.0" + fresh "^2.0.0" + http-errors "^2.0.0" + merge-descriptors "^2.0.0" + mime-types "^3.0.0" + on-finished "^2.4.1" + once "^1.4.0" + parseurl "^1.3.3" + proxy-addr "^2.0.7" + qs "^6.14.0" + range-parser "^1.2.1" + router "^2.2.0" + send "^1.1.0" + serve-static "^2.2.0" + statuses "^2.0.1" + type-is "^2.0.1" + vary "^1.1.2" + +fdir@^6.4.4: + version "6.5.0" + resolved "https://registry.yarnpkg.com/fdir/-/fdir-6.5.0.tgz#ed2ab967a331ade62f18d077dae192684d50d350" + integrity sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg== fecha@^4.2.0: version "4.2.3" @@ -805,18 +756,17 @@ fill-range@^7.1.1: dependencies: to-regex-range "^5.0.1" -finalhandler@1.3.1: - version "1.3.1" - resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.3.1.tgz#0c575f1d1d324ddd1da35ad7ece3df7d19088019" - integrity sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ== +finalhandler@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-2.1.0.tgz#72306373aa89d05a8242ed569ed86a1bff7c561f" + integrity sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q== dependencies: - debug "2.6.9" - encodeurl "~2.0.0" - escape-html "~1.0.3" - on-finished "2.4.1" - parseurl "~1.3.3" - statuses "2.0.1" - unpipe "~1.0.0" + debug "^4.4.0" + encodeurl "^2.0.0" + escape-html "^1.0.3" + on-finished "^2.4.1" + parseurl "^1.3.3" + statuses "^2.0.1" fn.name@1.x.x: version "1.1.0" @@ -824,19 +774,19 @@ fn.name@1.x.x: integrity sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw== follow-redirects@^1.0.0: - version "1.15.9" - resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.9.tgz#a604fa10e443bf98ca94228d9eebcc2e8a2c8ee1" - integrity sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ== + version "1.15.11" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.11.tgz#777d73d72a92f8ec4d2e410eb47352a56b8e8340" + integrity sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ== forwarded@0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow== -fresh@0.5.2: - version "0.5.2" - resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" - integrity sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q== +fresh@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/fresh/-/fresh-2.0.0.tgz#8dd7df6a1b3a1b3a5cf186c05a5dd267622635a4" + integrity sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A== from2@^2.3.0: version "2.3.0" @@ -851,25 +801,15 @@ fs-constants@^1.0.0: resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad" integrity sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow== -fs-extra@^11.2.0: - version "11.2.0" - resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-11.2.0.tgz#e70e17dfad64232287d01929399e0ea7c86b0e5b" - integrity sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw== +fs-extra@^11.2.0, fs-extra@^11.3.1: + version "11.3.1" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-11.3.1.tgz#ba7a1f97a85f94c6db2e52ff69570db3671d5a74" + integrity sha512-eXvGGwZ5CL17ZSwHWd3bbgk7UUpF6IFHtP57NYYakPvHOs8GDgDe5KJI36jIJzDkJ6eJjuzRA8eBQb6SkKue0g== dependencies: graceful-fs "^4.2.0" jsonfile "^6.0.1" universalify "^2.0.0" -fs-extra@^9.1.0: - version "9.1.0" - resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-9.1.0.tgz#5954460c764a8da2094ba3554bf839e6b9a7c86d" - integrity sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ== - dependencies: - at-least-node "^1.0.0" - graceful-fs "^4.2.0" - jsonfile "^6.0.1" - universalify "^2.0.0" - fsevents@~2.3.2: version "2.3.3" resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" @@ -885,49 +825,48 @@ get-caller-file@^2.0.5: resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== -get-intrinsic@^1.1.3, get-intrinsic@^1.2.4: - version "1.2.4" - resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.4.tgz#e385f5a4b5227d449c3eabbad05494ef0abbeadd" - integrity sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ== +get-intrinsic@^1.2.5, get-intrinsic@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz#743f0e3b6964a93a5491ed1bffaae054d7f98d01" + integrity sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ== dependencies: + call-bind-apply-helpers "^1.0.2" + es-define-property "^1.0.1" es-errors "^1.3.0" + es-object-atoms "^1.1.1" function-bind "^1.1.2" - has-proto "^1.0.1" - has-symbols "^1.0.3" - hasown "^2.0.0" + get-proto "^1.0.1" + gopd "^1.2.0" + has-symbols "^1.1.0" + hasown "^2.0.2" + math-intrinsics "^1.1.0" + +get-proto@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/get-proto/-/get-proto-1.0.1.tgz#150b3f2743869ef3e851ec0c49d15b1d14d00ee1" + integrity sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g== + dependencies: + dunder-proto "^1.0.1" + es-object-atoms "^1.0.0" github-from-package@0.0.0: version "0.0.0" resolved "https://registry.yarnpkg.com/github-from-package/-/github-from-package-0.0.0.tgz#97fb5d96bfde8973313f20e8288ef9a167fa64ce" integrity sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw== -glob-parent@^5.1.2, glob-parent@~5.1.2: +glob-parent@~5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== dependencies: is-glob "^4.0.1" -globby@^11.1.0: - version "11.1.0" - resolved "https://registry.yarnpkg.com/globby/-/globby-11.1.0.tgz#bd4be98bb042f83d796f7e3811991fbe82a0d34b" - integrity sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g== - dependencies: - array-union "^2.1.0" - dir-glob "^3.0.1" - fast-glob "^3.2.9" - ignore "^5.2.0" - merge2 "^1.4.1" - slash "^3.0.0" - -gopd@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.0.1.tgz#29ff76de69dac7489b7c0918a5788e56477c332c" - integrity sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA== - dependencies: - get-intrinsic "^1.1.3" +gopd@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.2.0.tgz#89f56b8217bdbc8802bd299df6d7f1081d7e51a1" + integrity sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg== -graceful-fs@^4.1.6, graceful-fs@^4.2.0: +graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.2: version "4.2.11" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== @@ -937,41 +876,19 @@ has-flag@^3.0.0: resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" integrity sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw== -has-flag@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" - integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== - -has-property-descriptors@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz#963ed7d071dc7bf5f084c5bfbe0d1b6222586854" - integrity sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg== - dependencies: - es-define-property "^1.0.0" - -has-proto@^1.0.1: - version "1.0.3" - resolved "https://registry.yarnpkg.com/has-proto/-/has-proto-1.0.3.tgz#b31ddfe9b0e6e9914536a6ab286426d0214f77fd" - integrity sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q== - -has-symbols@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" - integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== - -has@^1.0.3: - version "1.0.4" - resolved "https://registry.yarnpkg.com/has/-/has-1.0.4.tgz#2eb2860e000011dae4f1406a86fe80e530fb2ec6" - integrity sha512-qdSAmqLF6209RFj4VVItywPMbm3vWylknmB3nvNiUIs72xAimcM8nVYxYr7ncvZq5qzk9MKIZR8ijqD/1QuYjQ== +has-symbols@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.1.0.tgz#fc9c6a783a084951d0b971fe1018de813707a338" + integrity sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ== -hasown@^2.0.0, hasown@^2.0.2: +hasown@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003" integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ== dependencies: function-bind "^1.1.2" -http-errors@2.0.0: +http-errors@2.0.0, http-errors@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.0.tgz#b7774a1486ef73cf7667ac9ae0858c012c57b9d3" integrity sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ== @@ -1004,14 +921,14 @@ https-proxy-agent@^5.0.0: agent-base "6" debug "4" -iconv-lite@0.4.24: - version "0.4.24" - resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" - integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== +iconv-lite@0.6.3, iconv-lite@^0.6.3: + version "0.6.3" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501" + integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw== dependencies: - safer-buffer ">= 2.1.2 < 3" + safer-buffer ">= 2.1.2 < 3.0.0" -ieee754@^1.1.13, ieee754@^1.2.1: +ieee754@^1.1.13: version "1.2.1" resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== @@ -1021,11 +938,6 @@ ignore-by-default@^1.0.1: resolved "https://registry.yarnpkg.com/ignore-by-default/-/ignore-by-default-1.0.1.tgz#48ca6d72f6c6a3af00a9ad4ae6876be3889e2b09" integrity sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA== -ignore@^5.2.0: - version "5.3.2" - resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.2.tgz#3cd40e729f3643fd87cb04e50bf0eb722bc596f5" - integrity sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g== - inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.3: version "2.0.4" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" @@ -1061,17 +973,10 @@ is-binary-path@~2.1.0: dependencies: binary-extensions "^2.0.0" -is-core-module@2.9.0: - version "2.9.0" - resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.9.0.tgz#e1c34429cd51c6dd9e09e0799e396e27b19a9c69" - integrity sha512-+5FPy5PnwmO3lvfMb0AsoPaBG+5KHUI0wYFXOtYPnVVVspTFUuMZNfNaNVRt3FZadstu2c8x23vykRW/NBoU6A== - dependencies: - has "^1.0.3" - -is-core-module@^2.13.0: - version "2.15.1" - resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.15.1.tgz#a7363a25bee942fefab0de13bf6aa372c82dcc37" - integrity sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ== +is-core-module@^2.16.0: + version "2.16.1" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.16.1.tgz#2a98801a849f43e2add644fbb6bc6229b19a4ef4" + integrity sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w== dependencies: hasown "^2.0.2" @@ -1097,6 +1002,11 @@ is-number@^7.0.0: resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== +is-promise@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-4.0.0.tgz#42ff9f84206c1991d26debf520dd5c01042dd2f3" + integrity sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ== + is-stream@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077" @@ -1114,10 +1024,10 @@ js-yaml@^4.1.0: dependencies: argparse "^2.0.1" -jsesc@^2.5.1: - version "2.5.2" - resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4" - integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA== +jsesc@^3.0.2: + version "3.1.0" + resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-3.1.0.tgz#74d335a234f67ed19907fdadfac7ccf9d409825d" + integrity sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA== json5@^2.2.3: version "2.2.3" @@ -1125,9 +1035,9 @@ json5@^2.2.3: integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== jsonfile@^6.0.1: - version "6.1.0" - resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.1.0.tgz#bc55b2634793c679ec6403094eb13698a6ec0aae" - integrity sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ== + version "6.2.0" + resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.2.0.tgz#7c265bd1b65de6977478300087c99f1c84383f62" + integrity sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg== dependencies: universalify "^2.0.0" optionalDependencies: @@ -1138,10 +1048,10 @@ kuler@^2.0.0: resolved "https://registry.yarnpkg.com/kuler/-/kuler-2.0.0.tgz#e2c570a3800388fb44407e851531c1d670b061b3" integrity sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A== -logform@^2.6.0, logform@^2.6.1: - version "2.6.1" - resolved "https://registry.yarnpkg.com/logform/-/logform-2.6.1.tgz#71403a7d8cae04b2b734147963236205db9b3df0" - integrity sha512-CdaO738xRapbKIMVn2m4F6KTj4j7ooJ8POVnebSgKo3KBz5axNXRAL7ZdRjIV6NOr2Uf4vjtRkxrFETOioCqSA== +logform@^2.7.0: + version "2.7.0" + resolved "https://registry.yarnpkg.com/logform/-/logform-2.7.0.tgz#cfca97528ef290f2e125a08396805002b2d060d1" + integrity sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ== dependencies: "@colors/colors" "1.6.0" "@types/triple-beam" "^1.3.2" @@ -1155,55 +1065,32 @@ make-error@^1.1.1: resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2" integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== -media-typer@0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" - integrity sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ== - -merge-descriptors@1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.3.tgz#d80319a65f3c7935351e5cfdac8f9318504dbed5" - integrity sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ== - -merge2@^1.3.0, merge2@^1.4.1: - version "1.4.1" - resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" - integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== - -methods@~1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" - integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w== +math-intrinsics@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz#a0dd74be81e2aa5c2f27e65ce283605ee4e2b7f9" + integrity sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g== -micromatch@^4.0.4: - version "4.0.8" - resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.8.tgz#d66fa18f3a47076789320b9b1af32bd86d9fa202" - integrity sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA== - dependencies: - braces "^3.0.3" - picomatch "^2.3.1" +media-typer@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-1.1.0.tgz#6ab74b8f2d3320f2064b2a87a38e7931ff3a5561" + integrity sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw== -mime-db@1.52.0: - version "1.52.0" - resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" - integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== +merge-descriptors@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-2.0.0.tgz#ea922f660635a2249ee565e0449f951e6b603808" + integrity sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g== -"mime-db@>= 1.43.0 < 2": - version "1.53.0" - resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.53.0.tgz#3cb63cd820fc29896d9d4e8c32ab4fcd74ccb447" - integrity sha512-oHlN/w+3MQ3rba9rqFr6V/ypF10LSkdwUysQL7GkXoTgIWeV+tcXGA852TBxH+gsh8UWoyhR1hKcoMJTuWflpg== +"mime-db@>= 1.43.0 < 2", mime-db@^1.54.0: + version "1.54.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.54.0.tgz#cddb3ee4f9c64530dff640236661d42cb6a314f5" + integrity sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ== -mime-types@~2.1.24, mime-types@~2.1.34: - version "2.1.35" - resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" - integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== +mime-types@^3.0.0, mime-types@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-3.0.1.tgz#b1d94d6997a9b32fd69ebaed0db73de8acb519ce" + integrity sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA== dependencies: - mime-db "1.52.0" - -mime@1.6.0: - version "1.6.0" - resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" - integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== + mime-db "^1.54.0" mimic-response@^3.1.0: version "3.1.0" @@ -1222,11 +1109,28 @@ minimist@^1.2.0, minimist@^1.2.3, minimist@^1.2.6: resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== +minipass@^7.0.4, minipass@^7.1.2: + version "7.1.2" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.1.2.tgz#93a9626ce5e5e66bd4db86849e7515e92340a707" + integrity sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw== + +minizlib@^3.0.1: + version "3.0.2" + resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-3.0.2.tgz#f33d638eb279f664439aa38dc5f91607468cb574" + integrity sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA== + dependencies: + minipass "^7.1.2" + mkdirp-classic@^0.5.2, mkdirp-classic@^0.5.3: version "0.5.3" resolved "https://registry.yarnpkg.com/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz#fa10c9115cc6d8865be221ba47ee9bed78601113" integrity sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A== +mkdirp@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-3.0.1.tgz#e44e4c5607fb279c168241713cc6e0fea9adcb50" + integrity sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg== + moment@^2.29.1: version "2.30.1" resolved "https://registry.yarnpkg.com/moment/-/moment-2.30.1.tgz#f8c91c07b7a786e30c59926df530b4eac96974ae" @@ -1237,7 +1141,7 @@ ms@2.0.0: resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" integrity sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A== -ms@2.1.3, ms@^2.1.1, ms@^2.1.3: +ms@^2.1.1, ms@^2.1.3: version "2.1.3" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== @@ -1250,15 +1154,15 @@ multistream@^4.1.0: once "^1.4.0" readable-stream "^3.6.0" -napi-build-utils@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/napi-build-utils/-/napi-build-utils-1.0.2.tgz#b1fddc0b2c46e380a0b7a76f984dd47c41a13806" - integrity sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg== +napi-build-utils@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/napi-build-utils/-/napi-build-utils-2.0.0.tgz#13c22c0187fcfccce1461844136372a47ddc027e" + integrity sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA== -negotiator@0.6.3: - version "0.6.3" - resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd" - integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg== +negotiator@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-1.0.0.tgz#b6c91bb47172d69f93cfd7c357bbb529019b5f6a" + integrity sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg== negotiator@~0.6.4: version "0.6.4" @@ -1266,9 +1170,9 @@ negotiator@~0.6.4: integrity sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w== node-abi@^3.3.0: - version "3.71.0" - resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-3.71.0.tgz#52d84bbcd8575efb71468fbaa1f9a49b2c242038" - integrity sha512-SZ40vRiy/+wRTf21hxkkEjPJZpARzUMVcJoQse2EF8qkUWbbO2z7vd5oA/H6bVH6SZQ5STGcu0KRDS7biNRfxw== + version "3.75.0" + resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-3.75.0.tgz#2f929a91a90a0d02b325c43731314802357ed764" + integrity sha512-OhYaY5sDsIka7H7AtijtI9jwGYLyl29eQn/W623DiN/MIv5sUqc4g7BIDThX+gb7di9f6xK02nkp8sdfFWZLTg== dependencies: semver "^7.3.5" @@ -1279,10 +1183,15 @@ node-fetch@^2.6.6: dependencies: whatwg-url "^5.0.0" -nodemon@^3.1.7: - version "3.1.7" - resolved "https://registry.yarnpkg.com/nodemon/-/nodemon-3.1.7.tgz#07cb1f455f8bece6a499e0d72b5e029485521a54" - integrity sha512-hLj7fuMow6f0lbB0cD14Lz2xNjwsyruH251Pk4t/yIitCFJbmY1myuLlHm/q06aST4jg6EgAh74PIBBrRqpVAQ== +node-int64@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b" + integrity sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw== + +nodemon@^3.1.10: + version "3.1.10" + resolved "https://registry.yarnpkg.com/nodemon/-/nodemon-3.1.10.tgz#5015c5eb4fffcb24d98cf9454df14f4fecec9bc1" + integrity sha512-WDjw3pJ0/0jMFmyNDp3gvY2YizjLmmOUQo6DEBY+JgdvW/yQ9mEeSw6H5ythl5Ny2ytb7f9C2nIbjSxMNzbJXw== dependencies: chokidar "^3.5.2" debug "^4" @@ -1305,22 +1214,22 @@ object-hash@^3.0.0: resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-3.0.0.tgz#73f97f753e7baffc0e2cc9d6e079079744ac82e9" integrity sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw== -object-inspect@^1.13.1: - version "1.13.2" - resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.2.tgz#dea0088467fb991e67af4058147a24824a3043ff" - integrity sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g== +object-inspect@^1.13.3: + version "1.13.4" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.4.tgz#8375265e21bc20d0fa582c22e1b13485d6e00213" + integrity sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew== -on-finished@2.4.1: +on-finished@^2.4.1: version "2.4.1" resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f" integrity sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg== dependencies: ee-first "1.1.1" -on-headers@~1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.0.2.tgz#772b0ae6aaa525c399e489adfad90c403eb3c28f" - integrity sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA== +on-headers@~1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.1.0.tgz#59da4f91c45f5f989c6e4bcedc5a3b0aed70ff65" + integrity sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A== once@^1.3.1, once@^1.4.0: version "1.4.0" @@ -1341,7 +1250,7 @@ p-is-promise@^3.0.0: resolved "https://registry.yarnpkg.com/p-is-promise/-/p-is-promise-3.0.0.tgz#58e78c7dfe2e163cf2a04ff869e7c1dba64a5971" integrity sha512-Wo8VsW4IRQSKVXsJCn7TomUaVtyfjVDn3nUP7kE967BQk0CwFpdbZs0X0uk5sW9mkBa9eNM7hCMaG93WUAwxYQ== -parseurl@~1.3.3: +parseurl@^1.3.3: version "1.3.3" resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== @@ -1351,66 +1260,37 @@ path-parse@^1.0.7: resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== -path-to-regexp@0.1.10: - version "0.1.10" - resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.10.tgz#67e9108c5c0551b9e5326064387de4763c4d5f8b" - integrity sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w== +path-to-regexp@^8.0.0: + version "8.2.0" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-8.2.0.tgz#73990cc29e57a3ff2a0d914095156df5db79e8b4" + integrity sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ== -path-type@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" - integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== +picocolors@^1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b" + integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA== -picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.3.1: +picomatch@^2.0.4, picomatch@^2.2.1: version "2.3.1" resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== -pkg-fetch@3.4.2: - version "3.4.2" - resolved "https://registry.yarnpkg.com/pkg-fetch/-/pkg-fetch-3.4.2.tgz#6f68ebc54842b73f8c0808959a9df3739dcb28b7" - integrity sha512-0+uijmzYcnhC0hStDjm/cl2VYdrmVVBpe7Q8k9YBojxmR5tG8mvR9/nooQq3QSXiQqORDVOTY3XqMEqJVIzkHA== - dependencies: - chalk "^4.1.2" - fs-extra "^9.1.0" - https-proxy-agent "^5.0.0" - node-fetch "^2.6.6" - progress "^2.0.3" - semver "^7.3.5" - tar-fs "^2.1.1" - yargs "^16.2.0" - -pkg@^5.8.1: - version "5.8.1" - resolved "https://registry.yarnpkg.com/pkg/-/pkg-5.8.1.tgz#862020f3c0575638ef7d1146f951a54d65ddc984" - integrity sha512-CjBWtFStCfIiT4Bde9QpJy0KeH19jCfwZRJqHFDFXfhUklCx8JoFmMj3wgnEYIwGmZVNkhsStPHEOnrtrQhEXA== - dependencies: - "@babel/generator" "7.18.2" - "@babel/parser" "7.18.4" - "@babel/types" "7.19.0" - chalk "^4.1.2" - fs-extra "^9.1.0" - globby "^11.1.0" - into-stream "^6.0.0" - is-core-module "2.9.0" - minimist "^1.2.6" - multistream "^4.1.0" - pkg-fetch "3.4.2" - prebuild-install "7.1.1" - resolve "^1.22.0" - stream-meter "^1.0.4" +picomatch@^4.0.2: + version "4.0.3" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-4.0.3.tgz#796c76136d1eead715db1e7bad785dedd695a042" + integrity sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q== -prebuild-install@7.1.1: - version "7.1.1" - resolved "https://registry.yarnpkg.com/prebuild-install/-/prebuild-install-7.1.1.tgz#de97d5b34a70a0c81334fd24641f2a1702352e45" - integrity sha512-jAXscXWMcCK8GgCoHOfIr0ODh5ai8mj63L2nWrjuAgXE6tDyYGnx4/8o/rCgU+B4JSyZBKbeZqzhtwtC3ovxjw== +prebuild-install@^7.1.1: + version "7.1.3" + resolved "https://registry.yarnpkg.com/prebuild-install/-/prebuild-install-7.1.3.tgz#d630abad2b147443f20a212917beae68b8092eec" + integrity sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug== dependencies: detect-libc "^2.0.0" expand-template "^2.0.3" github-from-package "0.0.0" minimist "^1.2.3" mkdirp-classic "^0.5.3" - napi-build-utils "^1.0.1" + napi-build-utils "^2.0.0" node-abi "^3.3.0" pump "^3.0.0" rc "^1.2.7" @@ -1423,17 +1303,12 @@ process-nextick-args@~2.0.0: resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== -process@^0.11.10: - version "0.11.10" - resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" - integrity sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A== - progress@^2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8" integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA== -proxy-addr@~2.0.7: +proxy-addr@^2.0.7: version "2.0.7" resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025" integrity sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg== @@ -1447,38 +1322,33 @@ pstree.remy@^1.1.8: integrity sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w== pump@^3.0.0: - version "3.0.2" - resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.2.tgz#836f3edd6bc2ee599256c924ffe0d88573ddcbf8" - integrity sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw== + version "3.0.3" + resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.3.tgz#151d979f1a29668dc0025ec589a455b53282268d" + integrity sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA== dependencies: end-of-stream "^1.1.0" once "^1.3.1" -qs@6.13.0: - version "6.13.0" - resolved "https://registry.yarnpkg.com/qs/-/qs-6.13.0.tgz#6ca3bd58439f7e245655798997787b0d88a51906" - integrity sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg== +qs@^6.14.0: + version "6.14.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.14.0.tgz#c63fa40680d2c5c941412a0e899c89af60c0a930" + integrity sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w== dependencies: - side-channel "^1.0.6" - -queue-microtask@^1.2.2: - version "1.2.3" - resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" - integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== + side-channel "^1.1.0" -range-parser@~1.2.1: +range-parser@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== -raw-body@2.5.2: - version "2.5.2" - resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.2.tgz#99febd83b90e08975087e8f1f9419a149366b68a" - integrity sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA== +raw-body@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-3.0.0.tgz#25b3476f07a51600619dae3fe82ddc28a36e5e0f" + integrity sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g== dependencies: bytes "3.1.2" http-errors "2.0.0" - iconv-lite "0.4.24" + iconv-lite "0.6.3" unpipe "1.0.0" rc@^1.2.7: @@ -1491,7 +1361,7 @@ rc@^1.2.7: minimist "^1.2.0" strip-json-comments "~2.0.1" -readable-stream@^2.0.0, readable-stream@^2.1.4: +readable-stream@^2.0.0, readable-stream@^2.0.2, readable-stream@^2.1.4: version "2.3.8" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.8.tgz#91125e8042bba1b9887f49345f6277027ce8be9b" integrity sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA== @@ -1504,7 +1374,7 @@ readable-stream@^2.0.0, readable-stream@^2.1.4: string_decoder "~1.1.1" util-deprecate "~1.0.1" -readable-stream@^3.1.1, readable-stream@^3.4.0, readable-stream@^3.6.0: +readable-stream@^3.1.1, readable-stream@^3.4.0, readable-stream@^3.6.0, readable-stream@^3.6.2: version "3.6.2" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.2.tgz#56a9b36ea965c00c5a93ef31eb111a0f11056967" integrity sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA== @@ -1513,17 +1383,6 @@ readable-stream@^3.1.1, readable-stream@^3.4.0, readable-stream@^3.6.0: string_decoder "^1.1.1" util-deprecate "^1.0.1" -readable-stream@^4.5.2: - version "4.5.2" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-4.5.2.tgz#9e7fc4c45099baeed934bff6eb97ba6cf2729e09" - integrity sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g== - dependencies: - abort-controller "^3.0.0" - buffer "^6.0.3" - events "^3.3.0" - process "^0.11.10" - string_decoder "^1.3.0" - readdirp@~3.6.0: version "3.6.0" resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" @@ -1541,26 +1400,25 @@ requires-port@^1.0.0: resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" integrity sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ== -resolve@^1.22.0: - version "1.22.8" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.8.tgz#b6c87a9f2aa06dfab52e3d70ac8cde321fa5a48d" - integrity sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw== +resolve@^1.22.10: + version "1.22.10" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.10.tgz#b663e83ffb09bbf2386944736baae803029b8b39" + integrity sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w== dependencies: - is-core-module "^2.13.0" + is-core-module "^2.16.0" path-parse "^1.0.7" supports-preserve-symlinks-flag "^1.0.0" -reusify@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" - integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== - -run-parallel@^1.1.9: - version "1.2.0" - resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee" - integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA== +router@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/router/-/router-2.2.0.tgz#019be620b711c87641167cc79b99090f00b146ef" + integrity sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ== dependencies: - queue-microtask "^1.2.2" + debug "^4.4.0" + depd "^2.0.0" + is-promise "^4.0.0" + parseurl "^1.3.3" + path-to-regexp "^8.0.0" safe-buffer@5.2.1, safe-buffer@^5.0.1, safe-buffer@~5.2.0: version "5.2.1" @@ -1577,71 +1435,87 @@ safe-stable-stringify@^2.3.1: resolved "https://registry.yarnpkg.com/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz#4ca2f8e385f2831c432a719b108a3bf7af42a1dd" integrity sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA== -"safer-buffer@>= 2.1.2 < 3": +"safer-buffer@>= 2.1.2 < 3.0.0": version "2.1.2" resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== semver@^7.3.5, semver@^7.5.3: - version "7.6.3" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.3.tgz#980f7b5550bc175fb4dc09403085627f9eb33143" - integrity sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A== + version "7.7.2" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.2.tgz#67d99fdcd35cec21e6f8b87a7fd515a33f982b58" + integrity sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA== -send@0.19.0: - version "0.19.0" - resolved "https://registry.yarnpkg.com/send/-/send-0.19.0.tgz#bbc5a388c8ea6c048967049dbeac0e4a3f09d7f8" - integrity sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw== - dependencies: - debug "2.6.9" - depd "2.0.0" - destroy "1.2.0" - encodeurl "~1.0.2" - escape-html "~1.0.3" - etag "~1.8.1" - fresh "0.5.2" - http-errors "2.0.0" - mime "1.6.0" - ms "2.1.3" - on-finished "2.4.1" - range-parser "~1.2.1" - statuses "2.0.1" - -serve-static@1.16.2: - version "1.16.2" - resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.16.2.tgz#b6a5343da47f6bdd2673848bf45754941e803296" - integrity sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw== - dependencies: - encodeurl "~2.0.0" - escape-html "~1.0.3" - parseurl "~1.3.3" - send "0.19.0" +send@^1.1.0, send@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/send/-/send-1.2.0.tgz#32a7554fb777b831dfa828370f773a3808d37212" + integrity sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw== + dependencies: + debug "^4.3.5" + encodeurl "^2.0.0" + escape-html "^1.0.3" + etag "^1.8.1" + fresh "^2.0.0" + http-errors "^2.0.0" + mime-types "^3.0.1" + ms "^2.1.3" + on-finished "^2.4.1" + range-parser "^1.2.1" + statuses "^2.0.1" -set-function-length@^1.2.1: - version "1.2.2" - resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.2.2.tgz#aac72314198eaed975cf77b2c3b6b880695e5449" - integrity sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg== +serve-static@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-2.2.0.tgz#9c02564ee259bdd2251b82d659a2e7e1938d66f9" + integrity sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ== dependencies: - define-data-property "^1.1.4" - es-errors "^1.3.0" - function-bind "^1.1.2" - get-intrinsic "^1.2.4" - gopd "^1.0.1" - has-property-descriptors "^1.0.2" + encodeurl "^2.0.0" + escape-html "^1.0.3" + parseurl "^1.3.3" + send "^1.2.0" setprototypeof@1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== -side-channel@^1.0.6: - version "1.0.6" - resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.6.tgz#abd25fb7cd24baf45466406b1096b7831c9215f2" - integrity sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA== +side-channel-list@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/side-channel-list/-/side-channel-list-1.0.0.tgz#10cb5984263115d3b7a0e336591e290a830af8ad" + integrity sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA== + dependencies: + es-errors "^1.3.0" + object-inspect "^1.13.3" + +side-channel-map@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/side-channel-map/-/side-channel-map-1.0.1.tgz#d6bb6b37902c6fef5174e5f533fab4c732a26f42" + integrity sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA== + dependencies: + call-bound "^1.0.2" + es-errors "^1.3.0" + get-intrinsic "^1.2.5" + object-inspect "^1.13.3" + +side-channel-weakmap@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz#11dda19d5368e40ce9ec2bdc1fb0ecbc0790ecea" + integrity sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A== + dependencies: + call-bound "^1.0.2" + es-errors "^1.3.0" + get-intrinsic "^1.2.5" + object-inspect "^1.13.3" + side-channel-map "^1.0.1" + +side-channel@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.1.0.tgz#c3fcff9c4da932784873335ec9765fa94ff66bc9" + integrity sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw== dependencies: - call-bind "^1.0.7" es-errors "^1.3.0" - get-intrinsic "^1.2.4" - object-inspect "^1.13.1" + object-inspect "^1.13.3" + side-channel-list "^1.0.0" + side-channel-map "^1.0.1" + side-channel-weakmap "^1.0.2" simple-concat@^1.0.0: version "1.0.1" @@ -1671,11 +1545,6 @@ simple-update-notifier@^2.0.0: dependencies: semver "^7.5.3" -slash@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" - integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== - stack-trace@0.0.x: version "0.0.10" resolved "https://registry.yarnpkg.com/stack-trace/-/stack-trace-0.0.10.tgz#547c70b347e8d32b4e108ea1a2a159e5fdde19c0" @@ -1686,6 +1555,11 @@ statuses@2.0.1: resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63" integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== +statuses@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.2.tgz#8f75eecef765b5e1cfcdc080da59409ed424e382" + integrity sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw== + stream-meter@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/stream-meter/-/stream-meter-1.0.4.tgz#52af95aa5ea760a2491716704dbff90f73afdd1d" @@ -1702,7 +1576,7 @@ string-width@^4.1.0, string-width@^4.2.0: is-fullwidth-code-point "^3.0.0" strip-ansi "^6.0.1" -string_decoder@^1.1.1, string_decoder@^1.3.0: +string_decoder@^1.1.1: version "1.3.0" resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== @@ -1735,22 +1609,15 @@ supports-color@^5.5.0: dependencies: has-flag "^3.0.0" -supports-color@^7.1.0: - version "7.2.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" - integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== - dependencies: - has-flag "^4.0.0" - supports-preserve-symlinks-flag@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== tar-fs@^2.0.0, tar-fs@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-2.1.1.tgz#489a15ab85f1f0befabb370b7de4f9eb5cbe8784" - integrity sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng== + version "2.1.3" + resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-2.1.3.tgz#fb3b8843a26b6f13a08e606f7922875eb1fbbf92" + integrity sha512-090nwYJDmlhwFwEW3QQl+vaNnxsO2yVsd45eTKRBzSzu+hlb1w2K9inVq5b0ngXuLVqQ4ApvsUHHnu/zQNkWAg== dependencies: chownr "^1.1.1" mkdirp-classic "^0.5.2" @@ -1768,15 +1635,30 @@ tar-stream@^2.1.4: inherits "^2.0.3" readable-stream "^3.1.1" +tar@^7.4.3: + version "7.4.3" + resolved "https://registry.yarnpkg.com/tar/-/tar-7.4.3.tgz#88bbe9286a3fcd900e94592cda7a22b192e80571" + integrity sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw== + dependencies: + "@isaacs/fs-minipass" "^4.0.0" + chownr "^3.0.0" + minipass "^7.1.2" + minizlib "^3.0.1" + mkdirp "^3.0.1" + yallist "^5.0.0" + text-hex@1.0.x: version "1.0.0" resolved "https://registry.yarnpkg.com/text-hex/-/text-hex-1.0.0.tgz#69dc9c1b17446ee79a92bf5b884bb4b9127506f5" integrity sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg== -to-fast-properties@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e" - integrity sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog== +tinyglobby@^0.2.11: + version "0.2.14" + resolved "https://registry.yarnpkg.com/tinyglobby/-/tinyglobby-0.2.14.tgz#5280b0cf3f972b050e74ae88406c0a6a58f4079d" + integrity sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ== + dependencies: + fdir "^6.4.4" + picomatch "^4.0.2" to-regex-range@^5.0.1: version "5.0.1" @@ -1831,55 +1713,67 @@ tunnel-agent@^0.6.0: dependencies: safe-buffer "^5.0.1" -type-is@~1.6.18: - version "1.6.18" - resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" - integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== +type-is@^2.0.0, type-is@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/type-is/-/type-is-2.0.1.tgz#64f6cf03f92fce4015c2b224793f6bdd4b068c97" + integrity sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw== dependencies: - media-typer "0.3.0" - mime-types "~2.1.24" + content-type "^1.0.5" + media-typer "^1.1.0" + mime-types "^3.0.0" -typescript@5.5.4: - version "5.5.4" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.5.4.tgz#d9852d6c82bad2d2eda4fd74a5762a8f5909e9ba" - integrity sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q== +typescript@5.9.2: + version "5.9.2" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.9.2.tgz#d93450cddec5154a2d5cabe3b8102b83316fb2a6" + integrity sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A== undefsafe@^2.0.5: version "2.0.5" resolved "https://registry.yarnpkg.com/undefsafe/-/undefsafe-2.0.5.tgz#38733b9327bdcd226db889fb723a6efd162e6e2c" integrity sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA== -undici-types@~6.19.2, undici-types@~6.19.8: - version "6.19.8" - resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.19.8.tgz#35111c9d1437ab83a7cdc0abae2f26d88eda0a02" - integrity sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw== +undici-types@~6.21.0: + version "6.21.0" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.21.0.tgz#691d00af3909be93a7faa13be61b3a5b50ef12cb" + integrity sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ== + +undici-types@~7.10.0: + version "7.10.0" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-7.10.0.tgz#4ac2e058ce56b462b056e629cc6a02393d3ff350" + integrity sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag== universalify@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.1.tgz#168efc2180964e6386d061e094df61afe239b18d" integrity sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw== -unpipe@1.0.0, unpipe@~1.0.0: +unpipe@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ== +unzipper@^0.12.3: + version "0.12.3" + resolved "https://registry.yarnpkg.com/unzipper/-/unzipper-0.12.3.tgz#31958f5eed7368ed8f57deae547e5a673e984f87" + integrity sha512-PZ8hTS+AqcGxsaQntl3IRBw65QrBI6lxzqDEL7IAo/XCEqRTKGfOX56Vea5TH9SZczRVxuzk1re04z/YjuYCJA== + dependencies: + bluebird "~3.7.2" + duplexer2 "~0.1.4" + fs-extra "^11.2.0" + graceful-fs "^4.2.2" + node-int64 "^0.4.0" + util-deprecate@^1.0.1, util-deprecate@~1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== -utils-merge@1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" - integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA== - v8-compile-cache-lib@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz#6336e8d71965cb3d35a1bbb7868445a7c05264bf" integrity sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg== -vary@~1.1.2: +vary@^1.1.2, vary@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg== @@ -1907,31 +1801,31 @@ winston-daily-rotate-file@^5.0.0: triple-beam "^1.4.1" winston-transport "^4.7.0" -winston-transport@^4.7.0: - version "4.8.0" - resolved "https://registry.yarnpkg.com/winston-transport/-/winston-transport-4.8.0.tgz#a15080deaeb80338455ac52c863418c74fcf38ea" - integrity sha512-qxSTKswC6llEMZKgCQdaWgDuMJQnhuvF5f2Nk3SNXc4byfQ+voo2mX1Px9dkNOuR8p0KAjfPG29PuYUSIb+vSA== +winston-transport@^4.7.0, winston-transport@^4.9.0: + version "4.9.0" + resolved "https://registry.yarnpkg.com/winston-transport/-/winston-transport-4.9.0.tgz#3bba345de10297654ea6f33519424560003b3bf9" + integrity sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A== dependencies: - logform "^2.6.1" - readable-stream "^4.5.2" + logform "^2.7.0" + readable-stream "^3.6.2" triple-beam "^1.3.0" -winston@^3.16.0: - version "3.16.0" - resolved "https://registry.yarnpkg.com/winston/-/winston-3.16.0.tgz#d11caabada87b7d4b59aba9a94b882121b773f9b" - integrity sha512-xz7+cyGN5M+4CmmD4Npq1/4T+UZaz7HaeTlAruFUTjk79CNMq+P6H30vlE4z0qfqJ01VHYQwd7OZo03nYm/+lg== +winston@^3.17.0: + version "3.17.0" + resolved "https://registry.yarnpkg.com/winston/-/winston-3.17.0.tgz#74b8665ce9b4ea7b29d0922cfccf852a08a11423" + integrity sha512-DLiFIXYC5fMPxaRg832S6F5mJYvePtmO5G9v9IgUFPhXm9/GkXarH/TUrBAVzhTCzAj9anE/+GjrgXp/54nOgw== dependencies: "@colors/colors" "^1.6.0" "@dabh/diagnostics" "^2.0.2" async "^3.2.3" is-stream "^2.0.0" - logform "^2.6.0" + logform "^2.7.0" one-time "^1.0.0" readable-stream "^3.4.0" safe-stable-stringify "^2.3.1" stack-trace "0.0.x" triple-beam "^1.3.0" - winston-transport "^4.7.0" + winston-transport "^4.9.0" wrap-ansi@^7.0.0: version "7.0.0" @@ -1952,6 +1846,11 @@ y18n@^5.0.5: resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55" integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA== +yallist@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-5.0.0.tgz#00e2de443639ed0d78fd87de0d27469fbcffb533" + integrity sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw== + yargs-parser@^20.2.2: version "20.2.9" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee" diff --git a/pom.xml b/pom.xml index 9357a8e0ad..b0768966bd 100755 --- a/pom.xml +++ b/pom.xml @@ -38,7 +38,7 @@ ${project.name} /var/log/${pkg.name} /usr/share/${pkg.name} - 3.4.8 + 3.4.10 2.4.0-b180830.0359 5.1.5 0.12.5 @@ -129,7 +129,7 @@ 1.20.6 1.0.2 1.12 - 5.8.0 + 6.1.0 2.27.0 2.12.0 @@ -146,6 +146,7 @@ 9.2.0 1.1.10.5 9.10.0 + 4.1.125.Final @@ -895,6 +896,13 @@ + + io.netty + netty-bom + ${netty.version} + pom + import + org.springframework.boot spring-boot-dependencies @@ -909,7 +917,6 @@ pom import - org.thingsboard netty-mqtt @@ -1922,9 +1929,6 @@ Typesafe Repository https://repo.typesafe.com/typesafe/releases/ - - sonatype - https://oss.sonatype.org/content/groups/public - + diff --git a/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java b/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java index 67e11430bd..875747195c 100644 --- a/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java +++ b/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java @@ -74,6 +74,7 @@ import org.thingsboard.server.common.data.UpdateMessage; import org.thingsboard.server.common.data.UsageInfo; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.UserEmailInfo; +import org.thingsboard.server.common.data.ai.AiModel; import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.alarm.AlarmComment; import org.thingsboard.server.common.data.alarm.AlarmCommentInfo; @@ -98,6 +99,7 @@ import org.thingsboard.server.common.data.edge.EdgeInfo; import org.thingsboard.server.common.data.edge.EdgeInstructions; import org.thingsboard.server.common.data.edge.EdgeSearchQuery; import org.thingsboard.server.common.data.entityview.EntityViewSearchQuery; +import org.thingsboard.server.common.data.id.AiModelId; import org.thingsboard.server.common.data.id.AlarmCommentId; import org.thingsboard.server.common.data.id.AlarmId; import org.thingsboard.server.common.data.id.AssetId; @@ -113,6 +115,8 @@ import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityViewId; import org.thingsboard.server.common.data.id.MobileAppBundleId; import org.thingsboard.server.common.data.id.MobileAppId; +import org.thingsboard.server.common.data.id.NotificationId; +import org.thingsboard.server.common.data.id.NotificationRequestId; import org.thingsboard.server.common.data.id.OAuth2ClientId; import org.thingsboard.server.common.data.id.OAuth2ClientRegistrationTemplateId; import org.thingsboard.server.common.data.id.OtaPackageId; @@ -127,10 +131,18 @@ import org.thingsboard.server.common.data.id.WidgetTypeId; import org.thingsboard.server.common.data.id.WidgetsBundleId; import org.thingsboard.server.common.data.kv.Aggregation; import org.thingsboard.server.common.data.kv.AttributeKvEntry; +import org.thingsboard.server.common.data.kv.IntervalType; import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.common.data.mobile.app.MobileApp; import org.thingsboard.server.common.data.mobile.bundle.MobileAppBundle; import org.thingsboard.server.common.data.mobile.bundle.MobileAppBundleInfo; +import org.thingsboard.server.common.data.notification.Notification; +import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod; +import org.thingsboard.server.common.data.notification.NotificationRequest; +import org.thingsboard.server.common.data.notification.NotificationRequestInfo; +import org.thingsboard.server.common.data.notification.NotificationRequestPreview; +import org.thingsboard.server.common.data.notification.settings.NotificationSettings; +import org.thingsboard.server.common.data.notification.settings.UserNotificationSettings; import org.thingsboard.server.common.data.oauth2.OAuth2Client; import org.thingsboard.server.common.data.oauth2.OAuth2ClientInfo; import org.thingsboard.server.common.data.oauth2.OAuth2ClientLoginInfo; @@ -508,6 +520,99 @@ public class RestClient implements Closeable { params).getBody(); } + public PageData getAllAlarms(AlarmSearchStatus searchStatus, AlarmStatus status, TimePageLink pageLink, Boolean fetchOriginator) { + String urlSecondPart = "/api/alarms?"; + Map params = new HashMap<>(); + if (fetchOriginator != null) { + params.put("fetchOriginator", String.valueOf(fetchOriginator)); + urlSecondPart += "&fetchOriginator={fetchOriginator}"; + } + if (searchStatus != null) { + params.put("searchStatus", searchStatus.name()); + urlSecondPart += "&searchStatus={searchStatus}"; + } + if (status != null) { + params.put("status", status.name()); + urlSecondPart += "&status={status}"; + } + + addTimePageLinkToParam(params, pageLink); + + return restTemplate.exchange( + baseURL + urlSecondPart + "&" + getTimeUrlParams(pageLink), + HttpMethod.GET, + HttpEntity.EMPTY, + new ParameterizedTypeReference>() { + }, + params).getBody(); + } + + public PageData getAlarmsV2(EntityId entityId, List statusList, List severityList, + List typeList, String assignedId, TimePageLink pageLink) { + String urlSecondPart = "/api/v2/alarm/{entityType}/{entityId}?"; + Map params = new HashMap<>(); + params.put("entityType", entityId.getEntityType().name()); + params.put("entityId", entityId.getId().toString()); + if (!CollectionUtils.isEmpty(statusList)) { + params.put("statusList", listEnumToString(statusList)); + urlSecondPart += "&statusList={statusList}"; + } + if (!CollectionUtils.isEmpty(severityList)) { + params.put("severityList", listEnumToString(severityList)); + urlSecondPart += "&severityList={severityList}"; + } + if (!CollectionUtils.isEmpty(typeList)) { + params.put("typeList", String.join(",", typeList)); + urlSecondPart += "&typeList={typeList}"; + } + if (assignedId != null) { + params.put("assignedId", assignedId); + urlSecondPart += "&assignedId={assignedId}"; + } + + addTimePageLinkToParam(params, pageLink); + + return restTemplate.exchange( + baseURL + urlSecondPart + "&" + getTimeUrlParams(pageLink), + HttpMethod.GET, + HttpEntity.EMPTY, + new ParameterizedTypeReference>() { + }, + params).getBody(); + } + + public PageData getAllAlarmsV2(List statusList, List severityList, + List typeList, String assignedId, TimePageLink pageLink) { + String urlSecondPart = "/api/v2/alarms?"; + Map params = new HashMap<>(); + if (!CollectionUtils.isEmpty(statusList)) { + params.put("statusList", listEnumToString(statusList)); + urlSecondPart += "&statusList={statusList}"; + } + if (!CollectionUtils.isEmpty(severityList)) { + params.put("severityList", listEnumToString(severityList)); + urlSecondPart += "&severityList={severityList}"; + } + if (!CollectionUtils.isEmpty(typeList)) { + params.put("typeList", String.join(",", typeList)); + urlSecondPart += "&typeList={typeList}"; + } + if (assignedId != null) { + params.put("assignedId", assignedId); + urlSecondPart += "&assignedId={assignedId}"; + } + + addTimePageLinkToParam(params, pageLink); + + return restTemplate.exchange( + baseURL + urlSecondPart + "&" + getTimeUrlParams(pageLink), + HttpMethod.GET, + HttpEntity.EMPTY, + new ParameterizedTypeReference>() { + }, + params).getBody(); + } + public Optional getHighestAlarmSeverity(EntityId entityId, AlarmSearchStatus searchStatus, AlarmStatus status) { Map params = new HashMap<>(); params.put("entityType", entityId.getEntityType().name()); @@ -1709,6 +1814,14 @@ public class RestClient implements Closeable { }).getBody(); } + public JsonNode findEntityTimeseriesAndAttributesKeysByQuery(EntityDataQuery query) { + return restTemplate.exchange( + baseURL + "/api/entitiesQuery/find/keys", + HttpMethod.POST, new HttpEntity<>(query), + new ParameterizedTypeReference() { + }).getBody(); + } + public PageData findAlarmDataByQuery(AlarmDataQuery query) { return restTemplate.exchange( baseURL + "/api/alarmsQuery/find", @@ -2157,9 +2270,11 @@ public class RestClient implements Closeable { restTemplate.delete(baseURL + "/api/oauth2/client/{id}", oAuth2ClientId.getId()); } - public PageData getTenantDomainInfos() { + public PageData getTenantDomainInfos(PageLink pageLink) { + Map params = new HashMap<>(); + addPageLinkToParam(params, pageLink); return restTemplate.exchange( - baseURL + "/api/domain/infos", + baseURL + "/api/domain/infos?" + getUrlParams(pageLink), HttpMethod.GET, HttpEntity.EMPTY, new ParameterizedTypeReference>() { @@ -2191,9 +2306,11 @@ public class RestClient implements Closeable { restTemplate.postForLocation(baseURL + "/api/domain/{id}/oauth2Clients", oauth2ClientIds, domainId.getId()); } - public PageData getTenantMobileApps() { + public PageData getTenantMobileApps(PageLink pageLink) { + Map params = new HashMap<>(); + addPageLinkToParam(params, pageLink); return restTemplate.exchange( - baseURL + "/api/mobile/app", + baseURL + "/api/mobile/app?" + getUrlParams(pageLink), HttpMethod.GET, HttpEntity.EMPTY, new ParameterizedTypeReference>() { @@ -2221,9 +2338,11 @@ public class RestClient implements Closeable { restTemplate.delete(baseURL + "/api/mobile/app/{id}", mobileAppId.getId()); } - public PageData getTenantMobileBundleInfos() { + public PageData getTenantMobileBundleInfos(PageLink pageLink) { + Map params = new HashMap<>(); + addPageLinkToParam(params, pageLink); return restTemplate.exchange( - baseURL + "/api/mobile/bundle/infos", + baseURL + "/api/mobile/bundle/infos?" + getUrlParams(pageLink), HttpMethod.GET, HttpEntity.EMPTY, new ParameterizedTypeReference>() { @@ -2476,7 +2595,12 @@ public class RestClient implements Closeable { return getTimeseries(entityId, keys, interval, agg, sortOrder != null ? sortOrder.getDirection() : null, pageLink.getStartTime(), pageLink.getEndTime(), 100, useStrictDataTypes); } + @Deprecated public List getTimeseries(EntityId entityId, List keys, Long interval, Aggregation agg, SortOrder.Direction sortOrder, Long startTime, Long endTime, Integer limit, boolean useStrictDataTypes) { + return getTimeseries(entityId, keys, interval, null, null, agg, sortOrder, startTime, endTime, limit, useStrictDataTypes); + } + + public List getTimeseries(EntityId entityId, List keys, Long interval, IntervalType intervalType, String timeZone, Aggregation agg, SortOrder.Direction sortOrder, Long startTime, Long endTime, Integer limit, boolean useStrictDataTypes) { Map params = new HashMap<>(); params.put("entityType", entityId.getEntityType().name()); params.put("entityId", entityId.getId().toString()); @@ -2490,6 +2614,16 @@ public class RestClient implements Closeable { StringBuilder urlBuilder = new StringBuilder(baseURL); urlBuilder.append("/api/plugins/telemetry/{entityType}/{entityId}/values/timeseries?keys={keys}&interval={interval}&limit={limit}&agg={agg}&useStrictDataTypes={useStrictDataTypes}&orderBy={orderBy}"); + if (intervalType != null) { + urlBuilder.append("&intervalType={intervalType}"); + params.put("intervalType", intervalType.name()); + } + + if (timeZone != null) { + urlBuilder.append("&timeZone={timeZone}"); + params.put("timeZone", timeZone); + } + if (startTime != null) { urlBuilder.append("&startTs={startTs}"); params.put("startTs", String.valueOf(startTime)); @@ -2830,6 +2964,17 @@ public class RestClient implements Closeable { }, params).getBody(); } + public PageData getUsersByQuery(PageLink pageLink) { + Map params = new HashMap<>(); + addPageLinkToParam(params, pageLink); + return restTemplate.exchange( + baseURL + "/api/users/info?" + getUrlParams(pageLink), + HttpMethod.GET, + HttpEntity.EMPTY, + new ParameterizedTypeReference>() { + }, params).getBody(); + } + public PageData getTenantAdmins(TenantId tenantId, PageLink pageLink) { Map params = new HashMap<>(); params.put("tenantId", tenantId.getId().toString()); @@ -2990,7 +3135,7 @@ public class RestClient implements Closeable { addWidgetInfoFiltersToParams(tenantOnly, fullSearch, deprecatedFilter, widgetTypeList, params); return restTemplate.exchange( baseURL + "/api/widgetTypes?" + getUrlParams(pageLink) + - getWidgetTypeInfoPageRequestUrlParams(tenantOnly, fullSearch, deprecatedFilter, widgetTypeList), + getWidgetTypeInfoPageRequestUrlParams(tenantOnly, fullSearch, deprecatedFilter, widgetTypeList), HttpMethod.GET, HttpEntity.EMPTY, new ParameterizedTypeReference>() { @@ -3078,7 +3223,7 @@ public class RestClient implements Closeable { addWidgetInfoFiltersToParams(tenantOnly, fullSearch, deprecatedFilter, widgetTypeList, params); return restTemplate.exchange( baseURL + "/api/widgetTypesInfos?widgetsBundleId={widgetsBundleId}&" + getUrlParams(pageLink) + - getWidgetTypeInfoPageRequestUrlParams(tenantOnly, fullSearch, deprecatedFilter, widgetTypeList), + getWidgetTypeInfoPageRequestUrlParams(tenantOnly, fullSearch, deprecatedFilter, widgetTypeList), HttpMethod.GET, HttpEntity.EMPTY, new ParameterizedTypeReference>() { @@ -4128,6 +4273,161 @@ public class RestClient implements Closeable { } } + public PageData getNotifications(PageLink pageLink) { + Map params = new HashMap<>(); + addPageLinkToParam(params, pageLink); + return restTemplate.exchange( + baseURL + "/api/notifications?" + getUrlParams(pageLink), + HttpMethod.GET, + HttpEntity.EMPTY, + new ParameterizedTypeReference>() { + }, params).getBody(); + } + + public Integer getUnreadNotificationsCount(NotificationDeliveryMethod deliveryMethod) { + String uri = "/api/notifications/unread/count?"; + Map params = new HashMap<>(); + if (deliveryMethod != null) { + params.put("deliveryMethod", deliveryMethod.name()); + uri += "&deliveryMethod={deliveryMethod}"; + } + return restTemplate.exchange( + baseURL + uri, + HttpMethod.GET, + HttpEntity.EMPTY, + Integer.class, params).getBody(); + } + + public void markNotificationAsRead(NotificationId notificationId) { + restTemplate.exchange( + baseURL + "/api/notification/{id}/read", + HttpMethod.PUT, + HttpEntity.EMPTY, + Void.class, + notificationId.getId()); + } + + public void markAllNotificationsAsRead(NotificationDeliveryMethod deliveryMethod) { + String uri = "/api/notifications/read?"; + Map params = new HashMap<>(); + if (deliveryMethod != null) { + params.put("deliveryMethod", deliveryMethod.name()); + uri += "&deliveryMethod={deliveryMethod}"; + } + restTemplate.exchange( + baseURL + uri, + HttpMethod.PUT, + HttpEntity.EMPTY, + Void.class); + } + + + public void deleteNotification(NotificationId notificationId) { + restTemplate.delete(baseURL + "/api/notification/{id}", notificationId.getId()); + } + + public NotificationRequest createNotificationRequest(NotificationRequest notificationRequest) { + return restTemplate.postForEntity(baseURL + "/api/notification/request", notificationRequest, NotificationRequest.class).getBody(); + } + + public NotificationRequestPreview getNotificationRequestPreview(NotificationRequest notificationRequest, int recipientsPreviewSize) { + return restTemplate.postForEntity(baseURL + "/api/notification/request/preview?recipientsPreviewSize={recipientsPreviewSize}", notificationRequest, NotificationRequestPreview.class, recipientsPreviewSize).getBody(); + } + + public Optional getNotificationRequestById(NotificationRequestId notificationRequestId) { + try { + ResponseEntity notificationRequest = restTemplate.getForEntity(baseURL + "/api/notification/request/{id}", NotificationRequestInfo.class, notificationRequestId.getId()); + return Optional.ofNullable(notificationRequest.getBody()); + } catch (HttpClientErrorException exception) { + if (exception.getStatusCode() == HttpStatus.NOT_FOUND) { + return Optional.empty(); + } else { + throw exception; + } + } + } + + public PageData getNotificationRequests(PageLink pageLink) { + Map params = new HashMap<>(); + addPageLinkToParam(params, pageLink); + return restTemplate.exchange( + baseURL + "/api/notification/requests?" + getUrlParams(pageLink), + HttpMethod.GET, + HttpEntity.EMPTY, + new ParameterizedTypeReference>() { + }, params).getBody(); + } + + public void deleteNotificationRequest(NotificationRequestId notificationRequestId) { + restTemplate.delete(baseURL + "/api/notification/request/{id}", notificationRequestId.getId()); + } + + public NotificationSettings saveNotificationSettings(NotificationSettings notificationSettings) { + return restTemplate.postForEntity(baseURL + "/api/notification/settings", notificationSettings, NotificationSettings.class).getBody(); + } + + public Optional getNotificationSettings() { + try { + ResponseEntity notificationSettings = restTemplate.getForEntity(baseURL + "/api/notification/settings", NotificationSettings.class); + return Optional.ofNullable(notificationSettings.getBody()); + } catch (HttpClientErrorException exception) { + if (exception.getStatusCode() == HttpStatus.NOT_FOUND) { + return Optional.empty(); + } else { + throw exception; + } + } + } + + public List getAvailableDeliveryMethods() { + return restTemplate.exchange(URI.create( + baseURL + "/api/notification/deliveryMethods"), + HttpMethod.GET, + HttpEntity.EMPTY, + new ParameterizedTypeReference>() { + }).getBody(); + } + + public UserNotificationSettings saveUserNotificationSettings(UserNotificationSettings userNotificationSettings) { + return restTemplate.postForEntity(baseURL + "/api/notification/settings/user", userNotificationSettings, UserNotificationSettings.class).getBody(); + } + + public Optional getUserNotificationSettings() { + try { + ResponseEntity userNotificationSettings = restTemplate.getForEntity(baseURL + "/api/notification/settings/user", UserNotificationSettings.class); + return Optional.ofNullable(userNotificationSettings.getBody()); + } catch (HttpClientErrorException exception) { + if (exception.getStatusCode() == HttpStatus.NOT_FOUND) { + return Optional.empty(); + } else { + throw exception; + } + } + } + + public AiModel saveAiModel(AiModel aiModel) { + return restTemplate.postForEntity(baseURL + "/api/ai/model", aiModel, AiModel.class).getBody(); + } + + public Optional getAiModel(AiModelId aiModelId) { + try { + ResponseEntity response = restTemplate.getForEntity( + baseURL + "/api/aiModel/{aiModelId}", AiModel.class, aiModelId.getId()); + return Optional.ofNullable(response.getBody()); + } catch (HttpClientErrorException exception) { + if (exception.getStatusCode() == HttpStatus.NOT_FOUND) { + return Optional.empty(); + } else { + throw exception; + } + } + } + + public void deleteAiModel(AiModelId aiModelId) { + restTemplate.delete(baseURL + "/api/aiModel/{aiModelId}", aiModelId.getId()); + } + + private String getTimeUrlParams(TimePageLink pageLink) { String urlParams = getUrlParams(pageLink); if (pageLink.getStartTime() != null) { diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleEngineAlarmService.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleEngineAlarmService.java index edcbffcfbc..0a1b80f3d4 100644 --- a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleEngineAlarmService.java +++ b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleEngineAlarmService.java @@ -63,6 +63,8 @@ public interface RuleEngineAlarmService { AlarmApiCallResult clearAlarm(TenantId tenantId, AlarmId alarmId, long clearTs, JsonNode details); + AlarmApiCallResult clearAlarm(TenantId tenantId, AlarmId alarmId, long clearTs, JsonNode details, boolean pushEvent); + AlarmApiCallResult assignAlarm(TenantId tenantId, AlarmId alarmId, UserId assigneeId, long assignTs); AlarmApiCallResult unassignAlarm(TenantId tenantId, AlarmId alarmId, long assignTs); diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleEngineTelemetryService.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleEngineTelemetryService.java index 678f3a015d..27cf108d1f 100644 --- a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleEngineTelemetryService.java +++ b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleEngineTelemetryService.java @@ -15,9 +15,6 @@ */ package org.thingsboard.rule.engine.api; -/** - * Created by ashvayka on 02.04.18. - */ public interface RuleEngineTelemetryService { void saveTimeseries(TimeseriesSaveRequest request); diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java index d2687a1b10..e703a7b256 100644 --- a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java +++ b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java @@ -24,6 +24,7 @@ import org.thingsboard.server.cluster.TbClusterService; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.server.common.data.HasTenantId; import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.asset.Asset; @@ -32,6 +33,7 @@ import org.thingsboard.server.common.data.id.AssetId; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.HasId; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.RuleNodeId; import org.thingsboard.server.common.data.id.TenantId; @@ -78,6 +80,7 @@ import org.thingsboard.server.dao.queue.QueueService; import org.thingsboard.server.dao.queue.QueueStatsService; import org.thingsboard.server.dao.relation.RelationService; import org.thingsboard.server.dao.resource.ResourceService; +import org.thingsboard.server.dao.resource.TbResourceDataCache; import org.thingsboard.server.dao.rule.RuleChainService; import org.thingsboard.server.dao.tenant.TenantService; import org.thingsboard.server.dao.timeseries.TimeseriesService; @@ -252,6 +255,8 @@ public interface TbContext { void checkTenantEntity(EntityId entityId) throws TbNodeException; + & HasTenantId, I extends EntityId> void checkTenantOrSystemEntity(E entity) throws TbNodeException; + boolean isLocalEntity(EntityId entityId); RuleNodeId getSelfId(); @@ -308,6 +313,8 @@ public interface TbContext { ResourceService getResourceService(); + TbResourceDataCache getTbResourceDataCache(); + OtaPackageService getOtaPackageService(); RuleEngineDeviceProfileCache getDeviceProfileCache(); diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/notification/SlackService.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/notification/SlackService.java index e68310fce9..1ac800f488 100644 --- a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/notification/SlackService.java +++ b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/notification/SlackService.java @@ -18,6 +18,7 @@ package org.thingsboard.rule.engine.api.notification; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.notification.targets.slack.SlackConversation; import org.thingsboard.server.common.data.notification.targets.slack.SlackConversationType; +import org.thingsboard.server.common.data.notification.targets.slack.SlackFile; import java.util.List; @@ -25,6 +26,8 @@ public interface SlackService { void sendMessage(TenantId tenantId, String token, String conversationId, String message); + void sendMessage(TenantId tenantId, String token, String conversationId, String message, List files); + List listConversations(TenantId tenantId, String token, SlackConversationType conversationType); String getToken(TenantId tenantId); diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAlarmResult.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAlarmResult.java index 6f5898fe64..d25846c984 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAlarmResult.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAlarmResult.java @@ -16,19 +16,27 @@ package org.thingsboard.rule.engine.action; import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Data; +import lombok.NoArgsConstructor; import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.alarm.AlarmApiCallResult; @Data @AllArgsConstructor +@NoArgsConstructor +@Builder public class TbAlarmResult { + boolean isCreated; boolean isUpdated; boolean isSeverityUpdated; boolean isCleared; Alarm alarm; + Long conditionRepeats; + Long conditionDuration; + public TbAlarmResult(boolean isCreated, boolean isUpdated, boolean isCleared, Alarm alarm) { this.isCreated = isCreated; this.isUpdated = isUpdated; @@ -38,11 +46,13 @@ public class TbAlarmResult { public static TbAlarmResult fromAlarmResult(AlarmApiCallResult result) { boolean isSeverityChanged = result.isSeverityChanged(); - return new TbAlarmResult( - result.isCreated(), - result.isModified() && !isSeverityChanged, - isSeverityChanged, - result.isCleared(), - result.getAlarm()); + return TbAlarmResult.builder() + .isCreated(result.isCreated()) + .isUpdated(result.isModified() && !isSeverityChanged) + .isSeverityUpdated(isSeverityChanged) + .isCleared(result.isCleared()) + .alarm(result.getAlarm()) + .build(); } + } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAssignToCustomerNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAssignToCustomerNode.java index 35c3ce7683..6f18e7c1dd 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAssignToCustomerNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAssignToCustomerNode.java @@ -18,7 +18,6 @@ package org.thingsboard.rule.engine.action; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.MoreExecutors; -import lombok.extern.slf4j.Slf4j; import org.thingsboard.rule.engine.api.RuleNode; import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNodeConfiguration; @@ -32,7 +31,6 @@ import org.thingsboard.server.common.data.id.EntityViewId; import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.msg.TbMsg; -@Slf4j @RuleNode( type = ComponentType.ACTION, name = "assign to customer", @@ -42,7 +40,8 @@ import org.thingsboard.server.common.msg.TbMsg; "Rule node will create a new customer if it doesn't exist, and 'Create new customer if it doesn't exist' enabled.", configDirective = "tbActionNodeAssignToCustomerConfig", icon = "add_circle", - version = 1 + version = 1, + docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/action/assign-to-customer/" ) public class TbAssignToCustomerNode extends TbAbstractCustomerActionNode { diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbClearAlarmNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbClearAlarmNode.java index 6a806e7bc1..bd7eca9d4f 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbClearAlarmNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbClearAlarmNode.java @@ -16,9 +16,7 @@ package org.thingsboard.rule.engine.action; import com.fasterxml.jackson.databind.JsonNode; -import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; -import lombok.extern.slf4j.Slf4j; import org.thingsboard.rule.engine.api.RuleNode; import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNodeConfiguration; @@ -31,20 +29,24 @@ import org.thingsboard.server.common.data.id.AlarmId; import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.msg.TbMsg; -@Slf4j +import static com.google.common.util.concurrent.Futures.immediateFuture; +import static com.google.common.util.concurrent.Futures.transform; +import static com.google.common.util.concurrent.Futures.transformAsync; + @RuleNode( type = ComponentType.ACTION, name = "clear alarm", relationTypes = {"Cleared", "False"}, configClazz = TbClearAlarmNodeConfiguration.class, nodeDescription = "Clear Alarm", - nodeDetails = - "Details - JS function that creates JSON object based on incoming message. This object will be added into Alarm.details field.\n" + - "Node output:\n" + - "If alarm was not cleared, original message is returned. Otherwise new Message returned with type 'ALARM', Alarm object in 'msg' property and 'metadata' will contains 'isClearedAlarm' property. " + - "Message payload can be accessed via msg property. For example 'temperature = ' + msg.temperature ;. " + - "Message metadata can be accessed via metadata property. For example 'name = ' + metadata.customerName;.", + nodeDetails = """ + Details - JS function that creates JSON object based on incoming message. This object will be added into Alarm.details field. + Node output: + If alarm was not cleared, original message is returned. Otherwise new Message returned with type 'ALARM', Alarm object in 'msg' property and 'metadata' will contains 'isClearedAlarm' property. + Message payload can be accessed via msg property. For example 'temperature = ' + msg.temperature ;. + Message metadata can be accessed via metadata property. For example 'name = ' + metadata.customerName;.""", configDirective = "tbActionNodeClearAlarmConfig", - icon = "notifications_off" + icon = "notifications_off", + docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/action/clear-alarm/" ) public class TbClearAlarmNode extends TbAbstractAlarmNode { @@ -55,22 +57,26 @@ public class TbClearAlarmNode extends TbAbstractAlarmNode processAlarm(TbContext ctx, TbMsg msg) { - String alarmType = TbNodeUtils.processPattern(this.config.getAlarmType(), msg); - Alarm alarm; - if (msg.getOriginator().getEntityType().equals(EntityType.ALARM)) { - alarm = ctx.getAlarmService().findAlarmById(ctx.getTenantId(), new AlarmId(msg.getOriginator().getId())); + String alarmType = TbNodeUtils.processPattern(config.getAlarmType(), msg); + + ListenableFuture alarmFuture; + if (msg.getOriginator().getEntityType() == EntityType.ALARM) { + alarmFuture = ctx.getAlarmService().findAlarmByIdAsync(ctx.getTenantId(), new AlarmId(msg.getOriginator().getId())); } else { - alarm = ctx.getAlarmService().findLatestActiveByOriginatorAndType(ctx.getTenantId(), msg.getOriginator(), alarmType); + alarmFuture = ctx.getAlarmService().findLatestActiveByOriginatorAndTypeAsync(ctx.getTenantId(), msg.getOriginator(), alarmType); } - if (alarm != null && !alarm.getStatus().isCleared()) { - return clearAlarm(ctx, msg, alarm); - } - return Futures.immediateFuture(new TbAlarmResult(false, false, false, null)); + + return transformAsync(alarmFuture, alarm -> { + if (alarm != null && !alarm.getStatus().isCleared()) { + return clearAlarmAsync(ctx, msg, alarm); + } + return immediateFuture(new TbAlarmResult(false, false, false, null)); + }, ctx.getDbCallbackExecutor()); } - private ListenableFuture clearAlarm(TbContext ctx, TbMsg msg, Alarm alarm) { + private ListenableFuture clearAlarmAsync(TbContext ctx, TbMsg msg, Alarm alarm) { ListenableFuture asyncDetails = buildAlarmDetails(msg, alarm.getDetails()); - return Futures.transform(asyncDetails, details -> { + return transform(asyncDetails, details -> { AlarmApiCallResult result = ctx.getAlarmService().clearAlarm(ctx.getTenantId(), alarm.getId(), System.currentTimeMillis(), details); if (result.isSuccessful()) { return new TbAlarmResult(false, false, result.isCleared(), result.getAlarm()); @@ -79,4 +85,5 @@ public class TbClearAlarmNode extends TbAbstractAlarmNode getFutureCallback(TbContext ctx, TbMsg msg, EntityView entityView) { - return new FutureCallback() { + return new FutureCallback<>() { @Override public void onSuccess(@Nullable Void result) { transformAndTellNext(ctx, msg, entityView); } @Override - public void onFailure(Throwable t) { + public void onFailure(@NonNull Throwable t) { ctx.tellFailure(msg, t); } }; @@ -157,18 +151,11 @@ public class TbCopyAttributesToEntityViewNode implements TbNode { private boolean attributeContainsInEntityView(AttributeScope scope, String attrKey, EntityView entityView) { AttributesEntityView attributesEntityView = entityView.getKeys().getAttributes(); - List keys = null; - switch (scope) { - case CLIENT_SCOPE: - keys = attributesEntityView.getCs(); - break; - case SERVER_SCOPE: - keys = attributesEntityView.getSs(); - break; - case SHARED_SCOPE: - keys = attributesEntityView.getSh(); - break; - } + List keys = switch (scope) { + case CLIENT_SCOPE -> attributesEntityView.getCs(); + case SERVER_SCOPE -> attributesEntityView.getSs(); + case SHARED_SCOPE -> attributesEntityView.getSh(); + }; return CollectionsUtil.contains(keys, attrKey); } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCreateAlarmNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCreateAlarmNode.java index bd576be936..600c276901 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCreateAlarmNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCreateAlarmNode.java @@ -19,7 +19,6 @@ import com.fasterxml.jackson.databind.JsonNode; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.MoreExecutors; -import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.EnumUtils; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.rule.engine.api.RuleNode; @@ -39,7 +38,6 @@ import org.thingsboard.server.common.msg.TbMsg; import java.io.IOException; import java.util.List; -@Slf4j @RuleNode( type = ComponentType.ACTION, name = "create alarm", relationTypes = {"Created", "Updated", "False"}, @@ -52,7 +50,8 @@ import java.util.List; "Message payload can be accessed via msg property. For example 'temperature = ' + msg.temperature ;. " + "Message metadata can be accessed via metadata property. For example 'name = ' + metadata.customerName;.", configDirective = "tbActionNodeCreateAlarmConfig", - icon = "notifications_active" + icon = "notifications_active", + docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/action/create-alarm/" ) public class TbCreateAlarmNode extends TbAbstractAlarmNode { @@ -62,10 +61,10 @@ public class TbCreateAlarmNode extends TbAbstractAlarmNodeSuccess - if the relation already exists or successfully created, otherwise Failure.", configDirective = "tbActionNodeCreateRelationConfig", icon = "add_circle", - version = 1 + version = 1, + docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/action/create-relation/" ) public class TbCreateRelationNode extends TbAbstractRelationActionNode { diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbDeleteRelationNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbDeleteRelationNode.java index 82c3a0fab5..6a643aee75 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbDeleteRelationNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbDeleteRelationNode.java @@ -18,7 +18,6 @@ package org.thingsboard.rule.engine.action; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.MoreExecutors; -import lombok.extern.slf4j.Slf4j; import org.thingsboard.rule.engine.api.RuleNode; import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNodeConfiguration; @@ -32,8 +31,6 @@ import org.thingsboard.server.common.msg.TbMsg; import static org.thingsboard.common.util.DonAsynchron.withCallback; - -@Slf4j @RuleNode( type = ComponentType.ACTION, name = "delete relation", @@ -55,7 +52,8 @@ import static org.thingsboard.common.util.DonAsynchron.withCallback; "Output connections: Success - If the relation(s) successfully deleted, otherwise Failure.", configDirective = "tbActionNodeDeleteRelationConfig", icon = "remove_circle", - version = 1 + version = 1, + docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/action/delete-relation/" ) public class TbDeleteRelationNode extends TbAbstractRelationActionNode { diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbDeviceStateNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbDeviceStateNode.java index c9a50cf88d..caff3fc832 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbDeviceStateNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbDeviceStateNode.java @@ -58,7 +58,8 @@ import java.util.Set; "This node is particularly useful when device isn't using transports to receive data, such as when fetching data from external API or computing new data within the rule chain.", configClazz = TbDeviceStateNodeConfiguration.class, relationTypes = {TbNodeConnectionType.SUCCESS, TbNodeConnectionType.FAILURE, "Rate limited"}, - configDirective = "tbActionNodeDeviceStateConfig" + configDirective = "tbActionNodeDeviceStateConfig", + docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/action/device-state/" ) public class TbDeviceStateNode implements TbNode { diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbLogNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbLogNode.java index 47ecac154f..d4a598cf1b 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbLogNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbLogNode.java @@ -19,6 +19,7 @@ import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.MoreExecutors; import lombok.extern.slf4j.Slf4j; +import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.rule.engine.api.RuleNode; @@ -44,19 +45,19 @@ import java.util.Objects; "Message payload can be accessed via msg property. For example 'temperature = ' + msg.temperature ;. " + "Message metadata can be accessed via metadata property. For example 'name = ' + metadata.customerName;.", configDirective = "tbActionNodeLogConfig", - icon = "menu" + icon = "menu", + docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/action/log/" ) public class TbLogNode implements TbNode { - private TbLogNodeConfiguration config; private ScriptEngine scriptEngine; private boolean standard; @Override public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException { - this.config = TbNodeUtils.convert(configuration, TbLogNodeConfiguration.class); - this.standard = isStandard(config); - this.scriptEngine = this.standard ? null : createScriptEngine(ctx, config); + var config = TbNodeUtils.convert(configuration, TbLogNodeConfiguration.class); + standard = isStandard(config); + scriptEngine = standard ? null : createScriptEngine(ctx, config); } ScriptEngine createScriptEngine(TbContext ctx, TbLogNodeConfiguration config) { @@ -75,7 +76,7 @@ public class TbLogNode implements TbNode { return; } - Futures.addCallback(scriptEngine.executeToStringAsync(msg), new FutureCallback() { + Futures.addCallback(scriptEngine.executeToStringAsync(msg), new FutureCallback<>() { @Override public void onSuccess(@Nullable String result) { log.info(result); @@ -83,7 +84,7 @@ public class TbLogNode implements TbNode { } @Override - public void onFailure(Throwable t) { + public void onFailure(@NonNull Throwable t) { ctx.tellFailure(msg, t); } }, MoreExecutors.directExecutor()); //usually js responses runs on js callback executor @@ -120,4 +121,5 @@ public class TbLogNode implements TbNode { scriptEngine.destroy(); } } + } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbMsgCountNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbMsgCountNode.java index 3d93bea8a2..4488a95e31 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbMsgCountNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbMsgCountNode.java @@ -17,7 +17,6 @@ package org.thingsboard.rule.engine.action; import com.google.gson.Gson; import com.google.gson.JsonObject; -import lombok.extern.slf4j.Slf4j; import org.thingsboard.rule.engine.api.RuleNode; import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNode; @@ -34,7 +33,6 @@ import java.util.UUID; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicLong; -@Slf4j @RuleNode( type = ComponentType.ACTION, name = "message count", @@ -42,7 +40,8 @@ import java.util.concurrent.atomic.AtomicLong; nodeDescription = "Count incoming messages", nodeDetails = "Count incoming messages for specified interval and produces POST_TELEMETRY_REQUEST msg with messages count", icon = "functions", - configDirective = "tbActionNodeMsgCountConfig" + configDirective = "tbActionNodeMsgCountConfig", + docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/action/message-count/" ) public class TbMsgCountNode implements TbNode { @@ -59,7 +58,6 @@ public class TbMsgCountNode implements TbNode { this.delay = TimeUnit.SECONDS.toMillis(config.getInterval()); this.telemetryPrefix = config.getTelemetryPrefix(); scheduleTickMsg(ctx, null); - } @Override diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbSaveToCustomCassandraTableNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbSaveToCustomCassandraTableNode.java index e56a3459f8..6aa152403e 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbSaveToCustomCassandraTableNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbSaveToCustomCassandraTableNode.java @@ -16,21 +16,16 @@ package org.thingsboard.rule.engine.action; import com.datastax.oss.driver.api.core.ConsistencyLevel; -import com.datastax.oss.driver.api.core.cql.AsyncResultSet; import com.datastax.oss.driver.api.core.cql.BoundStatement; import com.datastax.oss.driver.api.core.cql.BoundStatementBuilder; import com.datastax.oss.driver.api.core.cql.PreparedStatement; import com.datastax.oss.driver.api.core.cql.Statement; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ObjectNode; -import com.google.common.base.Function; -import com.google.common.util.concurrent.Futures; -import com.google.common.util.concurrent.ListenableFuture; import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.JsonParser; import com.google.gson.JsonPrimitive; -import jakarta.annotation.Nullable; import lombok.extern.slf4j.Slf4j; import org.thingsboard.rule.engine.api.RuleNode; import org.thingsboard.rule.engine.api.TbContext; @@ -50,14 +45,13 @@ import org.thingsboard.server.dao.nosql.TbResultSetFuture; import java.util.ArrayList; import java.util.List; import java.util.Map; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; import java.util.concurrent.atomic.AtomicInteger; import static org.thingsboard.common.util.DonAsynchron.withCallback; @Slf4j -@RuleNode(type = ComponentType.ACTION, +@RuleNode( + type = ComponentType.ACTION, name = "save to custom table", configClazz = TbSaveToCustomCassandraTableNodeConfiguration.class, version = 1, @@ -71,7 +65,9 @@ import static org.thingsboard.common.util.DonAsynchron.withCallback; " otherwise, the message will be routed via success chain.", configDirective = "tbActionNodeCustomTableConfig", icon = "file_upload", - ruleChainTypes = RuleChainType.CORE) + ruleChainTypes = RuleChainType.CORE, + docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/action/save-to-custom-table/" +) public class TbSaveToCustomCassandraTableNode implements TbNode { private static final String TABLE_PREFIX = "cs_tb_"; @@ -82,7 +78,6 @@ public class TbSaveToCustomCassandraTableNode implements TbNode { private CassandraCluster cassandraCluster; private ConsistencyLevel defaultWriteLevel; private PreparedStatement saveStmt; - private ExecutorService readResultsProcessingExecutor; private Map fieldsMap; @Override @@ -95,31 +90,19 @@ public class TbSaveToCustomCassandraTableNode implements TbNode { if (!isTableExists()) { throw new TbNodeException("Table '" + TABLE_PREFIX + config.getTableName() + "' does not exist in Cassandra cluster."); } - startExecutor(); saveStmt = getSaveStmt(); } @Override public void onMsg(TbContext ctx, TbMsg msg) { - withCallback(save(msg, ctx), aVoid -> ctx.tellSuccess(msg), e -> ctx.tellFailure(msg, e), ctx.getDbCallbackExecutor()); + withCallback(save(msg, ctx), success -> ctx.tellSuccess(msg), e -> ctx.tellFailure(msg, e), ctx.getDbCallbackExecutor()); } @Override public void destroy() { - stopExecutor(); saveStmt = null; } - private void startExecutor() { - readResultsProcessingExecutor = Executors.newCachedThreadPool(); - } - - private void stopExecutor() { - if (readResultsProcessingExecutor != null) { - readResultsProcessingExecutor.shutdownNow(); - } - } - private boolean isTableExists() { var keyspaceMdOpt = getSession().getMetadata().getKeyspace(cassandraCluster.getKeyspaceName()); return keyspaceMdOpt.map(keyspaceMetadata -> @@ -180,7 +163,7 @@ public class TbSaveToCustomCassandraTableNode implements TbNode { return query.toString(); } - private ListenableFuture save(TbMsg msg, TbContext ctx) { + private TbResultSetFuture save(TbMsg msg, TbContext ctx) { JsonElement data = JsonParser.parseString(msg.getData()); if (!data.isJsonObject()) { throw new IllegalStateException("Invalid message structure, it is not a JSON Object: " + data); @@ -221,7 +204,7 @@ public class TbSaveToCustomCassandraTableNode implements TbNode { if (config.getDefaultTtl() > 0) { stmtBuilder.setInt(i.get(), config.getDefaultTtl()); } - return getFuture(executeAsyncWrite(ctx, stmtBuilder.build()), rs -> null); + return executeAsyncWrite(ctx, stmtBuilder.build()); } } @@ -251,16 +234,6 @@ public class TbSaveToCustomCassandraTableNode implements TbNode { } } - private ListenableFuture getFuture(TbResultSetFuture future, java.util.function.Function transformer) { - return Futures.transform(future, new Function() { - @Nullable - @Override - public T apply(@Nullable AsyncResultSet input) { - return transformer.apply(input); - } - }, readResultsProcessingExecutor); - } - @Override public TbPair upgrade(int fromVersion, JsonNode oldConfiguration) throws TbNodeException { boolean hasChanges = false; diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbSaveToCustomCassandraTableNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbSaveToCustomCassandraTableNodeConfiguration.java index 48e0f6d679..7b71c8f705 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbSaveToCustomCassandraTableNodeConfiguration.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbSaveToCustomCassandraTableNodeConfiguration.java @@ -24,12 +24,10 @@ import java.util.Map; @Data public class TbSaveToCustomCassandraTableNodeConfiguration implements NodeConfiguration { - private String tableName; private Map fieldsMapping; private int defaultTtl; - @Override public TbSaveToCustomCassandraTableNodeConfiguration defaultConfiguration() { TbSaveToCustomCassandraTableNodeConfiguration configuration = new TbSaveToCustomCassandraTableNodeConfiguration(); @@ -40,4 +38,5 @@ public class TbSaveToCustomCassandraTableNodeConfiguration implements NodeConfig configuration.setFieldsMapping(map); return configuration; } + } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbUnassignFromCustomerNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbUnassignFromCustomerNode.java index e889e3a970..0ff79dee39 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbUnassignFromCustomerNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbUnassignFromCustomerNode.java @@ -44,7 +44,8 @@ import org.thingsboard.server.common.msg.TbMsg; "Other entities can be assigned only to one customer, so specified customer title in the configuration will be ignored if the originator isn't a dashboard.", configDirective = "tbActionNodeUnAssignToCustomerConfig", icon = "remove_circle", - version = 1 + version = 1, + docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/action/unassign-from-customer/" ) public class TbUnassignFromCustomerNode extends TbAbstractCustomerActionNode { diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNode.java index 3497795771..a9ebb7ecb7 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNode.java @@ -18,12 +18,20 @@ package org.thingsboard.rule.engine.ai; import com.fasterxml.jackson.databind.JsonNode; import com.google.common.util.concurrent.FluentFuture; import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.MoreExecutors; import dev.langchain4j.data.message.ChatMessage; +import dev.langchain4j.data.message.Content; +import dev.langchain4j.data.message.ImageContent; +import dev.langchain4j.data.message.PdfFileContent; import dev.langchain4j.data.message.SystemMessage; +import dev.langchain4j.data.message.TextContent; import dev.langchain4j.data.message.UserMessage; import dev.langchain4j.model.chat.request.ChatRequest; import dev.langchain4j.model.chat.request.ResponseFormat; import dev.langchain4j.model.chat.response.ChatResponse; +import lombok.extern.slf4j.Slf4j; import org.checkerframework.checker.nullness.qual.NonNull; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.rule.engine.api.RuleNode; @@ -33,24 +41,38 @@ import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.rule.engine.api.util.TbNodeUtils; import org.thingsboard.rule.engine.external.TbAbstractExternalNode; +import org.thingsboard.server.common.data.GeneralFileDescriptor; +import org.thingsboard.server.common.data.ResourceType; +import org.thingsboard.server.common.data.TbResourceDataInfo; +import org.thingsboard.server.common.data.TbResourceInfo; import org.thingsboard.server.common.data.ai.AiModel; import org.thingsboard.server.common.data.ai.model.AiModelType; import org.thingsboard.server.common.data.ai.model.chat.AiChatModelConfig; import org.thingsboard.server.common.data.id.AiModelId; +import org.thingsboard.server.common.data.id.TbResourceId; +import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.data.rule.RuleChainType; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.dao.resource.TbResourceDataCache; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; +import java.util.Base64; +import java.util.HashSet; import java.util.List; import java.util.NoSuchElementException; +import java.util.Objects; import java.util.Optional; +import java.util.Set; +import java.util.UUID; import static com.google.common.util.concurrent.MoreExecutors.directExecutor; import static org.thingsboard.rule.engine.ai.TbResponseFormat.TbResponseFormatType; import static org.thingsboard.server.dao.service.ConstraintValidator.validateFields; +@Slf4j @RuleNode( type = ComponentType.EXTERNAL, name = "AI request", @@ -70,13 +92,13 @@ import static org.thingsboard.server.dao.service.ConstraintValidator.validateFie configClazz = TbAiNodeConfiguration.class, configDirective = "tbExternalNodeAiConfig", iconUrl = "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDkiIGhlaWdodD0iNDgiIHZpZXdCb3g9IjAgMCA0OSA0OCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik0zOC42MzExIDE3LjA3OTVDNDAuMTcwNSAxNy4wNzk2IDQxLjY1MTggMTcuNjg3MiA0Mi43NDc4IDE4Ljc3NjNDNDMuODQ0OCAxOS44NjYzIDQ0LjQ2NTkgMjEuMzUwMSA0NC40NjU5IDIyLjkwMjlWMzUuNDY1MkM0NC40NjU5IDM2LjM1MDkgNDQuMzU2NyAzNy4wNzY5IDQ0LjA5NzMgMzcuNzUxN0M0My44NDE0IDM4LjQxNjcgNDMuNDY1MSAzOC45NjE0IDQzLjA0NDggMzkuNTAyOEM0Mi40NjY3IDQwLjI0NzIgNDEuNjU2MyA0MC42ODU5IDQwLjg5MTkgNDAuOTM4OEM0MC4xMjExIDQxLjE5MzcgMzkuMzE0MyA0MS4yODg1IDM4LjYzMTEgNDEuMjg4NUgzMS4wMjU5TDIzLjM4MTIgNDUuODQ2NEMyMy4wNDMxIDQ2LjA0NzggMjIuNjI0MSA0Ni4wNTA3IDIyLjI4MzkgNDUuODUyOUMyMS45NDM3IDQ1LjY1NDcgMjEuNzMzOCA0NS4yODU5IDIxLjczMzcgNDQuODg3MlY0MS4yODg1SDE5LjY2NjNDMTguMTI2OSA0MS4yODg0IDE2LjY0NTUgNDAuNjgwOSAxNS41NDk2IDM5LjU5MThDMTQuNDUyNyAzOC41MDE5IDEzLjgzMTUgMzcuMDE3OSAxMy44MzE1IDM1LjQ2NTJWMjIuOTAyOUMxMy44MzE1IDIyLjMyMDIgMTMuOTE4NSAyMS43NDY4IDE0LjA4NTggMjEuMjAwN0wxNi4yODg5IDIxLjgxMDFMMTcuMjA5OSAyNS4yNTAyQzE3Ljk0MTYgMjcuOTg0NSAyMS43NTYyIDI3Ljk4NDQgMjIuNDg4IDI1LjI1MDJMMjMuNDA3OSAyMS44MTAxTDI2Ljc5MTcgMjAuODc0OUMyOC41NzkxIDIwLjM4MDUgMjkuMTc3IDE4LjUwMjYgMjguNTg4OCAxNy4wNzk1SDM4LjYzMTFaTTIyLjU4NDIgMzEuNTM5NUMyMS45OCAzMS41Mzk3IDIxLjQ5MDEgMzIuMDM3NiAyMS40OTAxIDMyLjY1MTlDMjEuNDkwMiAzMy4yNjYgMjEuOTgwMSAzMy43NjQgMjIuNTg0MiAzMy43NjQySDM0LjYxOTFDMzUuMjIzMyAzMy43NjQyIDM1LjcxMzEgMzMuMjY2MSAzNS43MTMyIDMyLjY1MTlDMzUuNzEzMiAzMi4wMzc1IDM1LjIyMzQgMzEuNTM5NSAzNC42MTkxIDMxLjUzOTVIMjIuNTg0MlpNMjQuNzcyMyAyNC44NjU3QzI0LjE2ODIgMjQuODY1OCAyMy42NzgzIDI1LjM2MzggMjMuNjc4MyAyNS45NzhDMjMuNjc4NCAyNi41OTIyIDI0LjE2ODMgMjcuMDkwMiAyNC43NzIzIDI3LjA5MDNIMzcuOTAxNEMzOC41MDU1IDI3LjA5MDMgMzguOTk1MyAyNi41OTIyIDM4Ljk5NTQgMjUuOTc4QzM4Ljk5NTQgMjUuMzYzNyAzOC41MDU2IDI0Ljg2NTcgMzcuOTAxNCAyNC44NjU3SDI0Ljc3MjNaIiBmaWxsPSJibGFjayIgZmlsbC1vcGFjaXR5PSIwLjc2Ii8+CjxwYXRoIGQ9Ik0xOC43ODkxIDExLjI5NzVDMTkuMDY5MSAxMC4xODA4IDIwLjYyOTkgMTAuMTgwOCAyMC45MDk5IDExLjI5NzVMMjEuOTE0MyAxNS4zMDM2QzIyLjAxMTYgMTUuNjkxOCAyMi4zMDY1IDE1Ljk5NzggMjIuNjg2NyAxNi4xMDNMMjYuMzYxMSAxNy4xMTg3QzI3LjQzNyAxNy40MTYyIDI3LjQzNyAxOC45Njc2IDI2LjM2MTEgMTkuMjY1MUwyMi42NzYxIDIwLjI4NEMyMi4zMDE4IDIwLjM4NzQgMjIuMDA4NyAyMC42ODQ1IDIxLjkwNjggMjEuMDY1TDIwLjkwNDYgMjQuODEyNUMyMC42MTE3IDI1LjkwNTggMTkuMDg2MSAyNS45MDU5IDE4Ljc5MzMgMjQuODEyNUwxNy43OTExIDIxLjA2NUMxNy42ODkzIDIwLjY4NDcgMTcuMzk3IDIwLjM4NzUgMTcuMDIyOSAyMC4yODRMMTMuMzM2OCAxOS4yNjUxQzEyLjI2MTQgMTguOTY3MyAxMi4yNjE1IDE3LjQxNjUgMTMuMzM2OCAxNy4xMTg3TDE3LjAxMTIgMTYuMTAzQzE3LjM5MTYgMTUuOTk3OCAxNy42ODc0IDE1LjY5MTkgMTcuNzg0NyAxNS4zMDM2TDE4Ljc4OTEgMTEuMjk3NVoiIGZpbGw9ImJsYWNrIiBmaWxsLW9wYWNpdHk9IjAuNzYiLz4KPHBhdGggZD0iTTEwLjAzNDMgNy4wMjQyNUMxMC4zMDY4IDUuODk0NDQgMTEuODg2OCA1Ljg5NDQ0IDEyLjE1OTQgNy4wMjQyNUwxMi42OTg5IDkuMjYyOThDMTIuNzkyNyA5LjY1MTc0IDEzLjA4NTEgOS45NTg4NyAxMy40NjQgMTAuMDY3OUwxNS41NzczIDEwLjY3NTFDMTYuNjM5MyAxMC45ODAzIDE2LjYzOTMgMTIuNTEwOSAxNS41NzczIDEyLjgxNjFMMTMuNDUzMyAxMy40MjY1QzEzLjA4MDIgMTMuNTMzOCAxMi43OTA4IDEzLjgzMzkgMTIuNjkyNSAxNC4yMTUxTDEyLjE1NTEgMTYuMzA0QzExLjg3IDE3LjQxMTYgMTAuMzIzNiAxNy40MTE2IDEwLjAzODUgMTYuMzA0TDkuNTAwMDMgMTQuMjE1MUM5LjQwMTczIDEzLjgzMzkgOS4xMTIzNSAxMy41MzM3IDguNzM5MyAxMy40MjY1TDYuNjE1MjQgMTIuODE2MUM1LjU1Mzc4IDEyLjUxMDYgNS41NTM2NCAxMC45ODA0IDYuNjE1MjQgMTAuNjc1MUw4LjcyODYyIDEwLjA2NzlDOS4xMDc2IDkuOTU4OTggOS4zOTk3OCA5LjY1MTg0IDkuNDkzNjIgOS4yNjI5OEwxMC4wMzQzIDcuMDI0MjVaIiBmaWxsPSJibGFjayIgZmlsbC1vcGFjaXR5PSIwLjc2Ii8+CjxwYXRoIGQ9Ik0yNS45MDI4IDYuNzMzMTNDMjYuMTg3OCA1LjYyNTQxIDI3LjczNDMgNS42MjU0MSAyOC4wMTkzIDYuNzMzMTNMMjguMjAzMSA3LjQ0Njc5QzI4LjMwMyA3LjgzNDMxIDI4LjYwMDEgOC4xMzcwNSAyOC45ODA5IDguMjM5NzVMMjkuNTM0NCA4LjM4OTY1QzMwLjYxOTIgOC42ODIxMiAzMC42MTkzIDEwLjI0NjkgMjkuNTM0NCAxMC41MzkzTDI4Ljk2OTIgMTAuNjkxNEMyOC41OTQ0IDEwLjc5MjUgMjguMjk5OSAxMS4wODgzIDI4LjE5NTYgMTEuNDY4TDI4LjAxNTEgMTIuMTI4NUMyNy43MTc0IDEzLjIxMjggMjYuMjA0NyAxMy4yMTI4IDI1LjkwNyAxMi4xMjg1TDI1LjcyNTQgMTEuNDY4QzI1LjYyMTEgMTEuMDg4MiAyNS4zMjY4IDEwLjc5MjQgMjQuOTUxOCAxMC42OTE0TDI0LjM4NzcgMTAuNTM5M0MyMy4zMDI2IDEwLjI0NyAyMy4zMDI2IDguNjgxOTggMjQuMzg3NyA4LjM4OTY1TDI0Ljk0MDEgOC4yMzk3NUMyNS4zMjExIDguMTM3MDkgMjUuNjE5MSA3LjgzNDQ2IDI1LjcxOSA3LjQ0Njc5TDI1LjkwMjggNi43MzMxM1oiIGZpbGw9ImJsYWNrIiBmaWxsLW9wYWNpdHk9IjAuNzYiLz4KPC9zdmc+Cg==", - docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/external-nodes/#ai-request-node", - ruleChainTypes = RuleChainType.CORE + docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/external/ai-request/" ) public final class TbAiNode extends TbAbstractExternalNode implements TbNode { private String systemPrompt; private String userPrompt; + private Set resourceIds; private ResponseFormat responseFormat; private int timeoutSeconds; private AiModelId modelId; @@ -111,6 +133,14 @@ public final class TbAiNode extends TbAbstractExternalNode implements TbNode { // LangChain4j AnthropicChatModel rejects requests with non-null ResponseFormat even if ResponseFormatType is TEXT responseFormat = config.getResponseFormat().toLangChainResponseFormat(); } + if (config.getResourceIds() != null && !config.getResourceIds().isEmpty()) { + resourceIds = new HashSet<>(config.getResourceIds().size()); + for (UUID resourceId : config.getResourceIds()) { + TbResourceId tbResourceId = new TbResourceId(resourceId); + validateResource(ctx, tbResourceId); + resourceIds.add(tbResourceId); + } + } systemPrompt = config.getSystemPrompt(); userPrompt = config.getUserPrompt(); @@ -126,12 +156,42 @@ public final class TbAiNode extends TbAbstractExternalNode implements TbNode { @Override public void onMsg(TbContext ctx, TbMsg msg) { var ackedMsg = ackIfNeeded(ctx, msg); + final String processedUserPrompt = TbNodeUtils.processPattern(this.userPrompt, ackedMsg); + + final ListenableFuture userMessageFuture = + resourceIds == null + ? Futures.immediateFuture(UserMessage.from(processedUserPrompt)) + : Futures.transform( + loadResources(ctx), + resources -> UserMessage.from(buildContents(processedUserPrompt, resources)), + ctx.getDbCallbackExecutor() + ); + + Futures.addCallback( + userMessageFuture, + new FutureCallback<>() { + @Override + public void onSuccess(UserMessage userMessage) { + buildAndSendRequest(ctx, ackedMsg, userMessage); + } + @Override + public void onFailure(Throwable t) { + tellFailure(ctx, ackedMsg, t); + } + }, + MoreExecutors.directExecutor() + ); + } + + private void buildAndSendRequest(TbContext ctx, TbMsg ackedMsg, UserMessage userMessage) { List chatMessages = new ArrayList<>(2); - if (systemPrompt != null) { + + if (systemPrompt != null && !systemPrompt.isBlank()) { chatMessages.add(SystemMessage.from(TbNodeUtils.processPattern(systemPrompt, ackedMsg))); } - chatMessages.add(UserMessage.from(TbNodeUtils.processPattern(userPrompt, ackedMsg))); + + chatMessages.add(userMessage); var chatRequest = ChatRequest.builder() .messages(chatMessages) @@ -192,11 +252,67 @@ public final class TbAiNode extends TbAbstractExternalNode implements TbNode { return JacksonUtil.newObjectNode().put("response", response).toString(); } + private void validateResource(TbContext ctx, TbResourceId tbResourceId) throws TbNodeException { + TbResourceInfo resource = ctx.getResourceService().findResourceInfoById(ctx.getTenantId(), tbResourceId); + if (resource == null) { + throw new TbNodeException("[" + ctx.getTenantId() + "] Resource with ID: [" + tbResourceId + "] was not found", true); + } + if (!ResourceType.GENERAL.equals(resource.getResourceType())) { + throw new TbNodeException("[" + ctx.getTenantId() + "] Resource with ID: [" + tbResourceId + "] has unsupported resource type: " + resource.getResourceType(), true); + } + ctx.checkTenantOrSystemEntity(resource); + } + + private ListenableFuture> loadResources(TbContext ctx) { + final TenantId tenantId = ctx.getTenantId(); + final TbResourceDataCache cache = ctx.getTbResourceDataCache(); + List> futures = resourceIds.stream() + .map(id -> cache.getResourceDataInfoAsync(tenantId, id)) + .toList(); + return Futures.allAsList(futures); + } + + private List buildContents(String userPrompt, List resources) { + List contents = new ArrayList<>(1 + resources.size()); + contents.add(new TextContent(userPrompt)); // user prompt first + + resources.stream() + .filter(Objects::nonNull) + .map(this::toContent) + .forEach(contents::add); + + return contents; + } + + private Content toContent(TbResourceDataInfo resource) { + if (resource.getDescriptor() == null) { + throw new RuntimeException("Missing descriptor for resource"); + } + GeneralFileDescriptor descriptor = JacksonUtil.treeToValue(resource.getDescriptor(), GeneralFileDescriptor.class); + String mediaType = descriptor.getMediaType(); + if (mediaType == null) { + throw new RuntimeException("Missing mediaType in resource descriptor " + resource.getDescriptor()); + } + byte[] data = resource.getData(); + if (mediaType.startsWith("text/")) { + return new TextContent(new String(data, StandardCharsets.UTF_8)); + } + if (mediaType.equals("application/pdf")) { + return new PdfFileContent(Base64.getEncoder().encodeToString(data), mediaType); + } + if (mediaType.startsWith("image/")) { + return new ImageContent(Base64.getEncoder().encodeToString(data), mediaType); + } + log.debug("Trying to create text content for {}", resource.getDescriptor()); + return new TextContent(new String(data, StandardCharsets.UTF_8)); + } + @Override public void destroy() { super.destroy(); systemPrompt = null; userPrompt = null; + resourceIds = null; responseFormat = null; modelId = null; } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNodeConfiguration.java index 10bb24199e..f51983ecb1 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNodeConfiguration.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNodeConfiguration.java @@ -20,12 +20,14 @@ import jakarta.validation.constraints.Max; import jakarta.validation.constraints.Min; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; -import jakarta.validation.constraints.Pattern; import lombok.Data; import org.thingsboard.rule.engine.api.NodeConfiguration; import org.thingsboard.server.common.data.id.AiModelId; import org.thingsboard.server.common.data.validation.Length; +import java.util.Set; +import java.util.UUID; + import static org.thingsboard.rule.engine.ai.TbResponseFormat.TbJsonResponseFormat; @Data @@ -34,14 +36,15 @@ public class TbAiNodeConfiguration implements NodeConfiguration resourceIds; + @NotNull @Valid private TbResponseFormat responseFormat; diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbResponseFormat.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbResponseFormat.java index 5c891a9c74..5107e613a4 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbResponseFormat.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbResponseFormat.java @@ -60,9 +60,7 @@ public sealed interface TbResponseFormat permits TbTextResponseFormat, TbJsonRes @Override public ResponseFormat toLangChainResponseFormat() { - return ResponseFormat.builder() - .type(ResponseFormatType.TEXT) - .build(); + return ResponseFormat.TEXT; } } @@ -76,9 +74,7 @@ public sealed interface TbResponseFormat permits TbTextResponseFormat, TbJsonRes @Override public ResponseFormat toLangChainResponseFormat() { - return ResponseFormat.builder() - .type(ResponseFormatType.JSON) - .build(); + return ResponseFormat.JSON; } } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/aws/lambda/TbAwsLambdaNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/aws/lambda/TbAwsLambdaNode.java index 82aa667319..c4e448e2ee 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/aws/lambda/TbAwsLambdaNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/aws/lambda/TbAwsLambdaNode.java @@ -54,7 +54,8 @@ import static org.thingsboard.server.dao.service.ConstraintValidator.validateFie "The node uses a pre-configured client and specified function to run.

" + "Output connections: Success, Failure.", configDirective = "tbExternalNodeLambdaConfig", - iconUrl = "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgd2lkdGg9IjQ4IiBoZWlnaHQ9IjQ4Ij48cGF0aCBkPSJNMTMuMjMgMTAuNTZWMTBjLTEuOTQgMC0zLjk5LjM5LTMuOTkgMi42NyAwIDEuMTYuNjEgMS45NSAxLjYzIDEuOTUuNzYgMCAxLjQzLS40NyAxLjg2LTEuMjIuNTItLjkzLjUtMS44LjUtMi44NG0yLjcgNi41M2MtLjE4LjE2LS40My4xNy0uNjMuMDYtLjg5LS43NC0xLjA1LTEuMDgtMS41NC0xLjc5LTEuNDcgMS41LTIuNTEgMS45NS00LjQyIDEuOTUtMi4yNSAwLTQuMDEtMS4zOS00LjAxLTQuMTcgMC0yLjE4IDEuMTctMy42NCAyLjg2LTQuMzggMS40Ni0uNjQgMy40OS0uNzYgNS4wNC0uOTNWNy41YzAtLjY2LjA1LTEuNDEtLjMzLTEuOTYtLjMyLS40OS0uOTUtLjctMS41LS43LTEuMDIgMC0xLjkzLjUzLTIuMTUgMS42MS0uMDUuMjQtLjI1LjQ4LS40Ny40OWwtMi42LS4yOGMtLjIyLS4wNS0uNDYtLjIyLS40LS41Ni42LTMuMTUgMy40NS00LjEgNi00LjEgMS4zIDAgMyAuMzUgNC4wMyAxLjMzQzE3LjExIDQuNTUgMTcgNi4xOCAxNyA3Ljk1djQuMTdjMCAxLjI1LjUgMS44MSAxIDIuNDguMTcuMjUuMjEuNTQgMCAuNzFsLTIuMDYgMS43OGgtLjAxIj48L3BhdGg+PHBhdGggZD0iTTIwLjE2IDE5LjU0QzE4IDIxLjE0IDE0LjgyIDIyIDEyLjEgMjJjLTMuODEgMC03LjI1LTEuNDEtOS44NS0zLjc2LS4yLS4xOC0uMDItLjQzLjI1LS4yOSAyLjc4IDEuNjMgNi4yNSAyLjYxIDkuODMgMi42MSAyLjQxIDAgNS4wNy0uNSA3LjUxLTEuNTMuMzctLjE2LjY2LjI0LjMyLjUxIj48L3BhdGg+PHBhdGggZD0iTTIxLjA3IDE4LjVjLS4yOC0uMzYtMS44NS0uMTctMi41Ny0uMDgtLjE5LjAyLS4yMi0uMTYtLjAzLS4zIDEuMjQtLjg4IDMuMjktLjYyIDMuNTMtLjMzLjI0LjMtLjA3IDIuMzUtMS4yNCAzLjMyLS4xOC4xNi0uMzUuMDctLjI2LS4xMS4yNi0uNjcuODUtMi4xNC41Ny0yLjV6Ij48L3BhdGg+PC9zdmc+" + iconUrl = "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgd2lkdGg9IjQ4IiBoZWlnaHQ9IjQ4Ij48cGF0aCBkPSJNMTMuMjMgMTAuNTZWMTBjLTEuOTQgMC0zLjk5LjM5LTMuOTkgMi42NyAwIDEuMTYuNjEgMS45NSAxLjYzIDEuOTUuNzYgMCAxLjQzLS40NyAxLjg2LTEuMjIuNTItLjkzLjUtMS44LjUtMi44NG0yLjcgNi41M2MtLjE4LjE2LS40My4xNy0uNjMuMDYtLjg5LS43NC0xLjA1LTEuMDgtMS41NC0xLjc5LTEuNDcgMS41LTIuNTEgMS45NS00LjQyIDEuOTUtMi4yNSAwLTQuMDEtMS4zOS00LjAxLTQuMTcgMC0yLjE4IDEuMTctMy42NCAyLjg2LTQuMzggMS40Ni0uNjQgMy40OS0uNzYgNS4wNC0uOTNWNy41YzAtLjY2LjA1LTEuNDEtLjMzLTEuOTYtLjMyLS40OS0uOTUtLjctMS41LS43LTEuMDIgMC0xLjkzLjUzLTIuMTUgMS42MS0uMDUuMjQtLjI1LjQ4LS40Ny40OWwtMi42LS4yOGMtLjIyLS4wNS0uNDYtLjIyLS40LS41Ni42LTMuMTUgMy40NS00LjEgNi00LjEgMS4zIDAgMyAuMzUgNC4wMyAxLjMzQzE3LjExIDQuNTUgMTcgNi4xOCAxNyA3Ljk1djQuMTdjMCAxLjI1LjUgMS44MSAxIDIuNDguMTcuMjUuMjEuNTQgMCAuNzFsLTIuMDYgMS43OGgtLjAxIj48L3BhdGg+PHBhdGggZD0iTTIwLjE2IDE5LjU0QzE4IDIxLjE0IDE0LjgyIDIyIDEyLjEgMjJjLTMuODEgMC03LjI1LTEuNDEtOS44NS0zLjc2LS4yLS4xOC0uMDItLjQzLjI1LS4yOSAyLjc4IDEuNjMgNi4yNSAyLjYxIDkuODMgMi42MSAyLjQxIDAgNS4wNy0uNSA3LjUxLTEuNTMuMzctLjE2LjY2LjI0LjMyLjUxIj48L3BhdGg+PHBhdGggZD0iTTIxLjA3IDE4LjVjLS4yOC0uMzYtMS44NS0uMTctMi41Ny0uMDgtLjE5LjAyLS4yMi0uMTYtLjAzLS4zIDEuMjQtLjg4IDMuMjktLjYyIDMuNTMtLjMzLjI0LjMtLjA3IDIuMzUtMS4yNCAzLjMyLS4xOC4xNi0uMzUuMDctLjI2LS4xMS4yNi0uNjcuODUtMi4xNC41Ny0yLjV6Ij48L3BhdGg+PC9zdmc+", + docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/external/aws-lambda/" ) public class TbAwsLambdaNode extends TbAbstractExternalNode { @@ -156,4 +157,5 @@ public class TbAwsLambdaNode extends TbAbstractExternalNode { } } } + } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/aws/sns/TbSnsNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/aws/sns/TbSnsNode.java index a13478c717..1877aff733 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/aws/sns/TbSnsNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/aws/sns/TbSnsNode.java @@ -47,7 +47,8 @@ import static org.thingsboard.common.util.DonAsynchron.withCallback; "(messageId, requestId) in the Message Metadata from the AWS SNS. " + "For example requestId field can be accessed with metadata.requestId.", configDirective = "tbExternalNodeSnsConfig", - iconUrl = "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgd2lkdGg9IjQ4IiBoZWlnaHQ9IjQ4Ij48cGF0aCBkPSJNMTMuMjMgMTAuNTZWMTBjLTEuOTQgMC0zLjk5LjM5LTMuOTkgMi42NyAwIDEuMTYuNjEgMS45NSAxLjYzIDEuOTUuNzYgMCAxLjQzLS40NyAxLjg2LTEuMjIuNTItLjkzLjUtMS44LjUtMi44NG0yLjcgNi41M2MtLjE4LjE2LS40My4xNy0uNjMuMDYtLjg5LS43NC0xLjA1LTEuMDgtMS41NC0xLjc5LTEuNDcgMS41LTIuNTEgMS45NS00LjQyIDEuOTUtMi4yNSAwLTQuMDEtMS4zOS00LjAxLTQuMTcgMC0yLjE4IDEuMTctMy42NCAyLjg2LTQuMzggMS40Ni0uNjQgMy40OS0uNzYgNS4wNC0uOTNWNy41YzAtLjY2LjA1LTEuNDEtLjMzLTEuOTYtLjMyLS40OS0uOTUtLjctMS41LS43LTEuMDIgMC0xLjkzLjUzLTIuMTUgMS42MS0uMDUuMjQtLjI1LjQ4LS40Ny40OWwtMi42LS4yOGMtLjIyLS4wNS0uNDYtLjIyLS40LS41Ni42LTMuMTUgMy40NS00LjEgNi00LjEgMS4zIDAgMyAuMzUgNC4wMyAxLjMzQzE3LjExIDQuNTUgMTcgNi4xOCAxNyA3Ljk1djQuMTdjMCAxLjI1LjUgMS44MSAxIDIuNDguMTcuMjUuMjEuNTQgMCAuNzFsLTIuMDYgMS43OGgtLjAxIj48L3BhdGg+PHBhdGggZD0iTTIwLjE2IDE5LjU0QzE4IDIxLjE0IDE0LjgyIDIyIDEyLjEgMjJjLTMuODEgMC03LjI1LTEuNDEtOS44NS0zLjc2LS4yLS4xOC0uMDItLjQzLjI1LS4yOSAyLjc4IDEuNjMgNi4yNSAyLjYxIDkuODMgMi42MSAyLjQxIDAgNS4wNy0uNSA3LjUxLTEuNTMuMzctLjE2LjY2LjI0LjMyLjUxIj48L3BhdGg+PHBhdGggZD0iTTIxLjA3IDE4LjVjLS4yOC0uMzYtMS44NS0uMTctMi41Ny0uMDgtLjE5LjAyLS4yMi0uMTYtLjAzLS4zIDEuMjQtLjg4IDMuMjktLjYyIDMuNTMtLjMzLjI0LjMtLjA3IDIuMzUtMS4yNCAzLjMyLS4xOC4xNi0uMzUuMDctLjI2LS4xMS4yNi0uNjcuODUtMi4xNC41Ny0yLjV6Ij48L3BhdGg+PC9zdmc+" + iconUrl = "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgd2lkdGg9IjQ4IiBoZWlnaHQ9IjQ4Ij48cGF0aCBkPSJNMTMuMjMgMTAuNTZWMTBjLTEuOTQgMC0zLjk5LjM5LTMuOTkgMi42NyAwIDEuMTYuNjEgMS45NSAxLjYzIDEuOTUuNzYgMCAxLjQzLS40NyAxLjg2LTEuMjIuNTItLjkzLjUtMS44LjUtMi44NG0yLjcgNi41M2MtLjE4LjE2LS40My4xNy0uNjMuMDYtLjg5LS43NC0xLjA1LTEuMDgtMS41NC0xLjc5LTEuNDcgMS41LTIuNTEgMS45NS00LjQyIDEuOTUtMi4yNSAwLTQuMDEtMS4zOS00LjAxLTQuMTcgMC0yLjE4IDEuMTctMy42NCAyLjg2LTQuMzggMS40Ni0uNjQgMy40OS0uNzYgNS4wNC0uOTNWNy41YzAtLjY2LjA1LTEuNDEtLjMzLTEuOTYtLjMyLS40OS0uOTUtLjctMS41LS43LTEuMDIgMC0xLjkzLjUzLTIuMTUgMS42MS0uMDUuMjQtLjI1LjQ4LS40Ny40OWwtMi42LS4yOGMtLjIyLS4wNS0uNDYtLjIyLS40LS41Ni42LTMuMTUgMy40NS00LjEgNi00LjEgMS4zIDAgMyAuMzUgNC4wMyAxLjMzQzE3LjExIDQuNTUgMTcgNi4xOCAxNyA3Ljk1djQuMTdjMCAxLjI1LjUgMS44MSAxIDIuNDguMTcuMjUuMjEuNTQgMCAuNzFsLTIuMDYgMS43OGgtLjAxIj48L3BhdGg+PHBhdGggZD0iTTIwLjE2IDE5LjU0QzE4IDIxLjE0IDE0LjgyIDIyIDEyLjEgMjJjLTMuODEgMC03LjI1LTEuNDEtOS44NS0zLjc2LS4yLS4xOC0uMDItLjQzLjI1LS4yOSAyLjc4IDEuNjMgNi4yNSAyLjYxIDkuODMgMi42MSAyLjQxIDAgNS4wNy0uNSA3LjUxLTEuNTMuMzctLjE2LjY2LjI0LjMyLjUxIj48L3BhdGg+PHBhdGggZD0iTTIxLjA3IDE4LjVjLS4yOC0uMzYtMS44NS0uMTctMi41Ny0uMDgtLjE5LjAyLS4yMi0uMTYtLjAzLS4zIDEuMjQtLjg4IDMuMjktLjYyIDMuNTMtLjMzLjI0LjMtLjA3IDIuMzUtMS4yNCAzLjMyLS4xOC4xNi0uMzUuMDctLjI2LS4xMS4yNi0uNjcuODUtMi4xNC41Ny0yLjV6Ij48L3BhdGg+PC9zdmc+", + docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/external/aws-sns/" ) public class TbSnsNode extends TbAbstractExternalNode { @@ -125,4 +126,5 @@ public class TbSnsNode extends TbAbstractExternalNode { } } } + } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/aws/sqs/TbSqsNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/aws/sqs/TbSqsNode.java index 5d538df932..2183f20c1c 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/aws/sqs/TbSqsNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/aws/sqs/TbSqsNode.java @@ -53,7 +53,8 @@ import static org.thingsboard.common.util.DonAsynchron.withCallback; ", sequenceNumber) in the Message Metadata from the AWS SQS." + " For example requestId field can be accessed with metadata.requestId.", configDirective = "tbExternalNodeSqsConfig", - iconUrl = "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgd2lkdGg9IjQ4IiBoZWlnaHQ9IjQ4Ij48cGF0aCBkPSJNMTMuMjMgMTAuNTZWMTBjLTEuOTQgMC0zLjk5LjM5LTMuOTkgMi42NyAwIDEuMTYuNjEgMS45NSAxLjYzIDEuOTUuNzYgMCAxLjQzLS40NyAxLjg2LTEuMjIuNTItLjkzLjUtMS44LjUtMi44NG0yLjcgNi41M2MtLjE4LjE2LS40My4xNy0uNjMuMDYtLjg5LS43NC0xLjA1LTEuMDgtMS41NC0xLjc5LTEuNDcgMS41LTIuNTEgMS45NS00LjQyIDEuOTUtMi4yNSAwLTQuMDEtMS4zOS00LjAxLTQuMTcgMC0yLjE4IDEuMTctMy42NCAyLjg2LTQuMzggMS40Ni0uNjQgMy40OS0uNzYgNS4wNC0uOTNWNy41YzAtLjY2LjA1LTEuNDEtLjMzLTEuOTYtLjMyLS40OS0uOTUtLjctMS41LS43LTEuMDIgMC0xLjkzLjUzLTIuMTUgMS42MS0uMDUuMjQtLjI1LjQ4LS40Ny40OWwtMi42LS4yOGMtLjIyLS4wNS0uNDYtLjIyLS40LS41Ni42LTMuMTUgMy40NS00LjEgNi00LjEgMS4zIDAgMyAuMzUgNC4wMyAxLjMzQzE3LjExIDQuNTUgMTcgNi4xOCAxNyA3Ljk1djQuMTdjMCAxLjI1LjUgMS44MSAxIDIuNDguMTcuMjUuMjEuNTQgMCAuNzFsLTIuMDYgMS43OGgtLjAxIj48L3BhdGg+PHBhdGggZD0iTTIwLjE2IDE5LjU0QzE4IDIxLjE0IDE0LjgyIDIyIDEyLjEgMjJjLTMuODEgMC03LjI1LTEuNDEtOS44NS0zLjc2LS4yLS4xOC0uMDItLjQzLjI1LS4yOSAyLjc4IDEuNjMgNi4yNSAyLjYxIDkuODMgMi42MSAyLjQxIDAgNS4wNy0uNSA3LjUxLTEuNTMuMzctLjE2LjY2LjI0LjMyLjUxIj48L3BhdGg+PHBhdGggZD0iTTIxLjA3IDE4LjVjLS4yOC0uMzYtMS44NS0uMTctMi41Ny0uMDgtLjE5LjAyLS4yMi0uMTYtLjAzLS4zIDEuMjQtLjg4IDMuMjktLjYyIDMuNTMtLjMzLjI0LjMtLjA3IDIuMzUtMS4yNCAzLjMyLS4xOC4xNi0uMzUuMDctLjI2LS4xMS4yNi0uNjcuODUtMi4xNC41Ny0yLjV6Ij48L3BhdGg+PC9zdmc+" + iconUrl = "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgd2lkdGg9IjQ4IiBoZWlnaHQ9IjQ4Ij48cGF0aCBkPSJNMTMuMjMgMTAuNTZWMTBjLTEuOTQgMC0zLjk5LjM5LTMuOTkgMi42NyAwIDEuMTYuNjEgMS45NSAxLjYzIDEuOTUuNzYgMCAxLjQzLS40NyAxLjg2LTEuMjIuNTItLjkzLjUtMS44LjUtMi44NG0yLjcgNi41M2MtLjE4LjE2LS40My4xNy0uNjMuMDYtLjg5LS43NC0xLjA1LTEuMDgtMS41NC0xLjc5LTEuNDcgMS41LTIuNTEgMS45NS00LjQyIDEuOTUtMi4yNSAwLTQuMDEtMS4zOS00LjAxLTQuMTcgMC0yLjE4IDEuMTctMy42NCAyLjg2LTQuMzggMS40Ni0uNjQgMy40OS0uNzYgNS4wNC0uOTNWNy41YzAtLjY2LjA1LTEuNDEtLjMzLTEuOTYtLjMyLS40OS0uOTUtLjctMS41LS43LTEuMDIgMC0xLjkzLjUzLTIuMTUgMS42MS0uMDUuMjQtLjI1LjQ4LS40Ny40OWwtMi42LS4yOGMtLjIyLS4wNS0uNDYtLjIyLS40LS41Ni42LTMuMTUgMy40NS00LjEgNi00LjEgMS4zIDAgMyAuMzUgNC4wMyAxLjMzQzE3LjExIDQuNTUgMTcgNi4xOCAxNyA3Ljk1djQuMTdjMCAxLjI1LjUgMS44MSAxIDIuNDguMTcuMjUuMjEuNTQgMCAuNzFsLTIuMDYgMS43OGgtLjAxIj48L3BhdGg+PHBhdGggZD0iTTIwLjE2IDE5LjU0QzE4IDIxLjE0IDE0LjgyIDIyIDEyLjEgMjJjLTMuODEgMC03LjI1LTEuNDEtOS44NS0zLjc2LS4yLS4xOC0uMDItLjQzLjI1LS4yOSAyLjc4IDEuNjMgNi4yNSAyLjYxIDkuODMgMi42MSAyLjQxIDAgNS4wNy0uNSA3LjUxLTEuNTMuMzctLjE2LjY2LjI0LjMyLjUxIj48L3BhdGg+PHBhdGggZD0iTTIxLjA3IDE4LjVjLS4yOC0uMzYtMS44NS0uMTctMi41Ny0uMDgtLjE5LjAyLS4yMi0uMTYtLjAzLS4zIDEuMjQtLjg4IDMuMjktLjYyIDMuNTMtLjMzLjI0LjMtLjA3IDIuMzUtMS4yNCAzLjMyLS4xOC4xNi0uMzUuMDctLjI2LS4xMS4yNi0uNjcuODUtMi4xNC41Ny0yLjV6Ij48L3BhdGg+PC9zdmc+", + docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/external/aws-sqs/" ) public class TbSqsNode extends TbAbstractExternalNode { @@ -156,4 +157,5 @@ public class TbSqsNode extends TbAbstractExternalNode { } } } + } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/debug/TbMsgGeneratorNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/debug/TbMsgGeneratorNode.java index 90ee0c9048..34514fd644 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/debug/TbMsgGeneratorNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/debug/TbMsgGeneratorNode.java @@ -63,9 +63,9 @@ import static org.thingsboard.server.common.data.DataConstants.QUEUE_NAME; nodeDetails = "Generates messages with configurable period. Javascript function used for message generation.", inEnabled = false, configDirective = "tbActionNodeGeneratorConfig", - icon = "repeat" + icon = "repeat", + docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/action/generator/" ) - public class TbMsgGeneratorNode implements TbNode { private static final Set supportedEntityTypes = EnumSet.of(EntityType.DEVICE, EntityType.ASSET, EntityType.ENTITY_VIEW, diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/deduplication/TbMsgDeduplicationNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/deduplication/TbMsgDeduplicationNode.java index ce5fba102a..3a78004820 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/deduplication/TbMsgDeduplicationNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/deduplication/TbMsgDeduplicationNode.java @@ -46,6 +46,7 @@ import java.util.concurrent.TimeUnit; import static org.thingsboard.server.common.data.DataConstants.QUEUE_NAME; +@Slf4j @RuleNode( type = ComponentType.TRANSFORMATION, name = "deduplication", @@ -59,9 +60,9 @@ import static org.thingsboard.server.common.data.DataConstants.QUEUE_NAME; "
  • ALL - return all messages as a single JSON array message. " + "Where each element represents object with msg and metadata inner properties.
  • ", icon = "content_copy", - configDirective = "tbTransformationNodeDeduplicationConfig" + configDirective = "tbTransformationNodeDeduplicationConfig", + docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/transformation/deduplication/" ) -@Slf4j public class TbMsgDeduplicationNode implements TbNode { public static final long TB_MSG_DEDUPLICATION_RETRY_DELAY = 10L; diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/delay/TbMsgDelayNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/delay/TbMsgDelayNode.java index 3507f7c500..4cda58e290 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/delay/TbMsgDelayNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/delay/TbMsgDelayNode.java @@ -15,7 +15,6 @@ */ package org.thingsboard.rule.engine.delay; -import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.math.NumberUtils; import org.thingsboard.rule.engine.api.RuleNode; import org.thingsboard.rule.engine.api.TbContext; @@ -34,7 +33,6 @@ import java.util.Map; import java.util.UUID; import java.util.concurrent.TimeUnit; -@Slf4j @RuleNode( type = ComponentType.ACTION, name = "delay (deprecated)", @@ -45,7 +43,8 @@ import java.util.concurrent.TimeUnit; "Deprecated because the acknowledged message still stays in memory (to be delayed) and this " + "does not guarantee that message will be processed even if the \"retry failures and timeouts\" processing strategy will be chosen.", icon = "pause", - configDirective = "tbActionNodeMsgDelayConfig" + configDirective = "tbActionNodeMsgDelayConfig", + docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/action/delay/" ) public class TbMsgDelayNode implements TbNode { @@ -109,4 +108,5 @@ public class TbMsgDelayNode implements TbNode { public void destroy() { pendingMsgs.clear(); } + } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/edge/AbstractTbMsgPushNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/edge/AbstractTbMsgPushNode.java index d1016ee4fe..04ec95e60f 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/edge/AbstractTbMsgPushNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/edge/AbstractTbMsgPushNode.java @@ -41,6 +41,9 @@ import static org.thingsboard.server.common.data.msg.TbMsgType.ACTIVITY_EVENT; import static org.thingsboard.server.common.data.msg.TbMsgType.ALARM; import static org.thingsboard.server.common.data.msg.TbMsgType.ALARM_ACK; import static org.thingsboard.server.common.data.msg.TbMsgType.ALARM_CLEAR; +import static org.thingsboard.server.common.data.msg.TbMsgType.ALARM_CREATED; +import static org.thingsboard.server.common.data.msg.TbMsgType.ALARM_SEVERITY_UPDATED; +import static org.thingsboard.server.common.data.msg.TbMsgType.ALARM_UPDATED; import static org.thingsboard.server.common.data.msg.TbMsgType.ATTRIBUTES_DELETED; import static org.thingsboard.server.common.data.msg.TbMsgType.ATTRIBUTES_UPDATED; import static org.thingsboard.server.common.data.msg.TbMsgType.CONNECT_EVENT; @@ -78,7 +81,7 @@ public abstract class AbstractTbMsgPushNodeSuccess route.", configDirective = "tbActionNodePushToCloudConfig", icon = "cloud_upload", - ruleChainTypes = RuleChainType.EDGE + ruleChainTypes = RuleChainType.EDGE, + docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/action/push-to-cloud/" ) public class TbMsgPushToCloudNode extends AbstractTbMsgPushNode { @@ -80,7 +79,6 @@ public class TbMsgPushToCloudNode extends AbstractTbMsgPushNodeSuccess route.", configDirective = "tbActionNodePushToEdgeConfig", icon = "cloud_download", - ruleChainTypes = RuleChainType.CORE + ruleChainTypes = RuleChainType.CORE, + docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/action/push-to-edge/" ) public class TbMsgPushToEdgeNode extends AbstractTbMsgPushNode { @@ -113,7 +115,7 @@ public class TbMsgPushToEdgeNode extends AbstractTbMsgPushNodeAsset profile name or Failure", - configDirective = "tbNodeEmptyConfig") + configDirective = "tbNodeEmptyConfig", + docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/filter/asset-profile-switch/" +) public class TbAssetTypeSwitchNode extends TbAbstractTypeSwitchNode { @Override diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbCheckAlarmStatusNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbCheckAlarmStatusNode.java index 9fb60ca671..8f0f35ea46 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbCheckAlarmStatusNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbCheckAlarmStatusNode.java @@ -43,7 +43,9 @@ import java.util.Objects; nodeDescription = "Checks alarm status.", nodeDetails = "Checks the alarm status to match one of the specified statuses.

    " + "Output connections: True, False, Failure.", - configDirective = "tbFilterNodeCheckAlarmStatusConfig") + configDirective = "tbFilterNodeCheckAlarmStatusConfig", + docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/filter/alarm-status-filter/" +) public class TbCheckAlarmStatusNode implements TbNode { private TbCheckAlarmStatusNodeConfig config; diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbCheckMessageNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbCheckMessageNode.java index d59f2bb0e4..8e23dae658 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbCheckMessageNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbCheckMessageNode.java @@ -16,7 +16,6 @@ package org.thingsboard.rule.engine.filter; import com.google.gson.Gson; -import lombok.extern.slf4j.Slf4j; import org.thingsboard.rule.engine.api.RuleNode; import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNode; @@ -30,7 +29,6 @@ import org.thingsboard.server.common.msg.TbMsg; import java.util.List; import java.util.Map; -@Slf4j @RuleNode( type = ComponentType.FILTER, name = "check fields presence", @@ -40,7 +38,9 @@ import java.util.Map; nodeDetails = "By default, the rule node checks that all specified fields are present. " + "Uncheck the 'Check that all selected fields are present' if the presence of at least one field is sufficient.

    " + "Output connections: True, False, Failure", - configDirective = "tbFilterNodeCheckMessageConfig") + configDirective = "tbFilterNodeCheckMessageConfig", + docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/filter/check-fields-presence/" +) public class TbCheckMessageNode implements TbNode { private static final Gson gson = new Gson(); @@ -127,4 +127,4 @@ public class TbCheckMessageNode implements TbNode { return (Map) gson.fromJson(msg.getData(), Map.class); } -} \ No newline at end of file +} diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbCheckRelationNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbCheckRelationNode.java index a5f694373a..6186fe80df 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbCheckRelationNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbCheckRelationNode.java @@ -19,7 +19,6 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ObjectNode; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; -import lombok.extern.slf4j.Slf4j; import org.thingsboard.rule.engine.api.RuleNode; import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNode; @@ -41,10 +40,6 @@ import java.util.List; import static org.thingsboard.common.util.DonAsynchron.withCallback; -/** - * Created by ashvayka on 19.01.18. - */ -@Slf4j @RuleNode( type = ComponentType.FILTER, name = "check relation presence", @@ -56,7 +51,9 @@ import static org.thingsboard.common.util.DonAsynchron.withCallback; "Otherwise, the rule node checks the presence of a relation to any entity. " + "In both cases, relation lookup is based on configured direction and type.

    " + "Output connections: True, False, Failure", - configDirective = "tbFilterNodeCheckRelationConfig") + configDirective = "tbFilterNodeCheckRelationConfig", + docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/filter/check-relation-presence/" +) public class TbCheckRelationNode implements TbNode { private TbCheckRelationNodeConfiguration config; diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbDeviceTypeSwitchNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbDeviceTypeSwitchNode.java index 8f6954956a..d12055c422 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbDeviceTypeSwitchNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbDeviceTypeSwitchNode.java @@ -15,7 +15,6 @@ */ package org.thingsboard.rule.engine.filter; -import lombok.extern.slf4j.Slf4j; import org.thingsboard.rule.engine.api.EmptyNodeConfiguration; import org.thingsboard.rule.engine.api.RuleNode; import org.thingsboard.rule.engine.api.TbContext; @@ -26,7 +25,6 @@ import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.plugin.ComponentType; -@Slf4j @RuleNode( type = ComponentType.FILTER, name = "device profile switch", @@ -36,7 +34,9 @@ import org.thingsboard.server.common.data.plugin.ComponentType; nodeDescription = "Route incoming messages based on the name of the device profile", nodeDetails = "Route incoming messages based on the name of the device profile. The device profile name is case-sensitive

    " + "Output connections: Device profile name or Failure", - configDirective = "tbNodeEmptyConfig") + configDirective = "tbNodeEmptyConfig", + docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/filter/device-profile-switch/" +) public class TbDeviceTypeSwitchNode extends TbAbstractTypeSwitchNode { @Override diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsFilterNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsFilterNode.java index e36494a2b6..5165eba35f 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsFilterNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsFilterNode.java @@ -15,7 +15,6 @@ */ package org.thingsboard.rule.engine.filter; -import lombok.extern.slf4j.Slf4j; import org.thingsboard.rule.engine.api.RuleNode; import org.thingsboard.rule.engine.api.ScriptEngine; import org.thingsboard.rule.engine.api.TbContext; @@ -30,7 +29,6 @@ import org.thingsboard.server.common.msg.TbMsg; import static org.thingsboard.common.util.DonAsynchron.withCallback; -@Slf4j @RuleNode( type = ComponentType.FILTER, name = "script", @@ -44,18 +42,17 @@ import static org.thingsboard.common.util.DonAsynchron.withCallback; "Message metadata can be accessed via metadata property. For example metadata.customerName === 'John';
    " + "Message type can be accessed via msgType property.

    " + "Output connections: True, False, Failure", - configDirective = "tbFilterNodeScriptConfig" + configDirective = "tbFilterNodeScriptConfig", + docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/filter/script/" ) public class TbJsFilterNode implements TbNode { - private TbJsFilterNodeConfiguration config; private ScriptEngine scriptEngine; @Override public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException { - this.config = TbNodeUtils.convert(configuration, TbJsFilterNodeConfiguration.class); - scriptEngine = ctx.createScriptEngine(config.getScriptLang(), - ScriptLanguage.TBEL.equals(config.getScriptLang()) ? config.getTbelScript() : config.getJsScript()); + var config = TbNodeUtils.convert(configuration, TbJsFilterNodeConfiguration.class); + scriptEngine = ctx.createScriptEngine(config.getScriptLang(), ScriptLanguage.TBEL.equals(config.getScriptLang()) ? config.getTbelScript() : config.getJsScript()); } @Override @@ -75,4 +72,5 @@ public class TbJsFilterNode implements TbNode { scriptEngine.destroy(); } } + } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsSwitchNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsSwitchNode.java index e039eaeb14..87bb2113d9 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsSwitchNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsSwitchNode.java @@ -18,7 +18,6 @@ package org.thingsboard.rule.engine.filter; import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.MoreExecutors; -import lombok.extern.slf4j.Slf4j; import org.checkerframework.checker.nullness.qual.Nullable; import org.thingsboard.rule.engine.api.RuleNode; import org.thingsboard.rule.engine.api.ScriptEngine; @@ -33,7 +32,6 @@ import org.thingsboard.server.common.msg.TbMsg; import java.util.Set; -@Slf4j @RuleNode( type = ComponentType.FILTER, name = "switch", customRelations = true, @@ -46,17 +44,17 @@ import java.util.Set; "Message metadata can be accessed via metadata property. For example metadata.customerName === 'John';
    " + "Message type can be accessed via msgType property.

    " + "Output connections: Custom connection(s) defined by switch node or Failure", - configDirective = "tbFilterNodeSwitchConfig") + configDirective = "tbFilterNodeSwitchConfig", + docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/filter/switch/" +) public class TbJsSwitchNode implements TbNode { - private TbJsSwitchNodeConfiguration config; private ScriptEngine scriptEngine; @Override public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException { - this.config = TbNodeUtils.convert(configuration, TbJsSwitchNodeConfiguration.class); - this.scriptEngine = ctx.createScriptEngine(config.getScriptLang(), - ScriptLanguage.TBEL.equals(config.getScriptLang()) ? config.getTbelScript() : config.getJsScript()); + var config = TbNodeUtils.convert(configuration, TbJsSwitchNodeConfiguration.class); + scriptEngine = ctx.createScriptEngine(config.getScriptLang(), ScriptLanguage.TBEL.equals(config.getScriptLang()) ? config.getTbelScript() : config.getJsScript()); } @Override @@ -84,4 +82,5 @@ public class TbJsSwitchNode implements TbNode { scriptEngine.destroy(); } } + } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbMsgTypeFilterNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbMsgTypeFilterNode.java index 0e26a89afb..09479de488 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbMsgTypeFilterNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbMsgTypeFilterNode.java @@ -15,7 +15,6 @@ */ package org.thingsboard.rule.engine.filter; -import lombok.extern.slf4j.Slf4j; import org.thingsboard.rule.engine.api.RuleNode; import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNode; @@ -26,10 +25,6 @@ import org.thingsboard.server.common.data.msg.TbNodeConnectionType; import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.msg.TbMsg; -/** - * Created by ashvayka on 19.01.18. - */ -@Slf4j @RuleNode( type = ComponentType.FILTER, name = "message type filter", @@ -38,14 +33,16 @@ import org.thingsboard.server.common.msg.TbMsg; nodeDescription = "Filter incoming messages by Message Type", nodeDetails = "If incoming message type is expected - send Message via True chain, otherwise False chain is used.

    " + "Output connections: True, False, Failure", - configDirective = "tbFilterNodeMessageTypeConfig") + configDirective = "tbFilterNodeMessageTypeConfig", + docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/filter/message-type-filter/" +) public class TbMsgTypeFilterNode implements TbNode { - TbMsgTypeFilterNodeConfiguration config; + private TbMsgTypeFilterNodeConfiguration config; @Override public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException { - this.config = TbNodeUtils.convert(configuration, TbMsgTypeFilterNodeConfiguration.class); + config = TbNodeUtils.convert(configuration, TbMsgTypeFilterNodeConfiguration.class); } @Override diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbMsgTypeSwitchNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbMsgTypeSwitchNode.java index f8d8278cbd..b82eba4e50 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbMsgTypeSwitchNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbMsgTypeSwitchNode.java @@ -15,18 +15,14 @@ */ package org.thingsboard.rule.engine.filter; -import lombok.extern.slf4j.Slf4j; import org.thingsboard.rule.engine.api.EmptyNodeConfiguration; import org.thingsboard.rule.engine.api.RuleNode; import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNode; import org.thingsboard.rule.engine.api.TbNodeConfiguration; -import org.thingsboard.rule.engine.api.TbNodeException; -import org.thingsboard.rule.engine.api.util.TbNodeUtils; import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.msg.TbMsg; -@Slf4j @RuleNode( type = ComponentType.FILTER, name = "message type switch", @@ -36,15 +32,13 @@ import org.thingsboard.server.common.msg.TbMsg; nodeDetails = "Sends messages with message types \"Post attributes\", \"Post telemetry\", \"RPC Request\"" + " etc. via corresponding chain, otherwise Other chain is used.

    " + "Output connections: Message type connection, Other - if message type is custom or Failure", - configDirective = "tbNodeEmptyConfig") + configDirective = "tbNodeEmptyConfig", + docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/filter/message-type-switch/" +) public class TbMsgTypeSwitchNode implements TbNode { - EmptyNodeConfiguration config; - @Override - public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException { - this.config = TbNodeUtils.convert(configuration, EmptyNodeConfiguration.class); - } + public void init(TbContext ctx, TbNodeConfiguration configuration) {} @Override public void onMsg(TbContext ctx, TbMsg msg) { diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbOriginatorTypeFilterNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbOriginatorTypeFilterNode.java index ffaea592f0..57cce66e21 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbOriginatorTypeFilterNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbOriginatorTypeFilterNode.java @@ -15,7 +15,6 @@ */ package org.thingsboard.rule.engine.filter; -import lombok.extern.slf4j.Slf4j; import org.thingsboard.rule.engine.api.RuleNode; import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNode; @@ -27,7 +26,6 @@ import org.thingsboard.server.common.data.msg.TbNodeConnectionType; import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.msg.TbMsg; -@Slf4j @RuleNode( type = ComponentType.FILTER, name = "entity type filter", @@ -36,14 +34,16 @@ import org.thingsboard.server.common.msg.TbMsg; nodeDescription = "Filter incoming messages by the type of message originator entity", nodeDetails = "Checks that the entity type of the incoming message originator matches one of the values specified in the filter.

    " + "Output connections: True, False, Failure", - configDirective = "tbFilterNodeOriginatorTypeConfig") + configDirective = "tbFilterNodeOriginatorTypeConfig", + docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/filter/entity-type-filter/" +) public class TbOriginatorTypeFilterNode implements TbNode { - TbOriginatorTypeFilterNodeConfiguration config; + private TbOriginatorTypeFilterNodeConfiguration config; @Override public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException { - this.config = TbNodeUtils.convert(configuration, TbOriginatorTypeFilterNodeConfiguration.class); + config = TbNodeUtils.convert(configuration, TbOriginatorTypeFilterNodeConfiguration.class); } @Override diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbOriginatorTypeSwitchNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbOriginatorTypeSwitchNode.java index 5c2a887b1d..5cee36cf14 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbOriginatorTypeSwitchNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbOriginatorTypeSwitchNode.java @@ -15,14 +15,12 @@ */ package org.thingsboard.rule.engine.filter; -import lombok.extern.slf4j.Slf4j; import org.thingsboard.rule.engine.api.EmptyNodeConfiguration; import org.thingsboard.rule.engine.api.RuleNode; import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.plugin.ComponentType; -@Slf4j @RuleNode( type = ComponentType.FILTER, name = "entity type switch", @@ -31,7 +29,9 @@ import org.thingsboard.server.common.data.plugin.ComponentType; nodeDescription = "Route incoming messages by Message Originator Type", nodeDetails = "Routes messages to chain according to the entity type ('Device', 'Asset', etc.).

    " + "Output connections: Message originator type or Failure", - configDirective = "tbNodeEmptyConfig") + configDirective = "tbNodeEmptyConfig", + docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/filter/entity-type-switch/" +) public class TbOriginatorTypeSwitchNode extends TbAbstractTypeSwitchNode { @Override diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/flow/TbAckNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/flow/TbAckNode.java index 8692cd6c55..b3d1ca71cf 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/flow/TbAckNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/flow/TbAckNode.java @@ -15,34 +15,27 @@ */ package org.thingsboard.rule.engine.flow; -import lombok.extern.slf4j.Slf4j; import org.thingsboard.rule.engine.api.EmptyNodeConfiguration; import org.thingsboard.rule.engine.api.RuleNode; import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNode; import org.thingsboard.rule.engine.api.TbNodeConfiguration; -import org.thingsboard.rule.engine.api.TbNodeException; -import org.thingsboard.rule.engine.api.util.TbNodeUtils; import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.msg.TbMsg; -@Slf4j @RuleNode( type = ComponentType.FLOW, name = "acknowledge", configClazz = EmptyNodeConfiguration.class, nodeDescription = "Acknowledges the incoming message", nodeDetails = "After acknowledgement, the message is pushed to related rule nodes. Useful if you don't care what happens to this message next.", - configDirective = "tbNodeEmptyConfig" + configDirective = "tbNodeEmptyConfig", + docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/flow/acknowledge/" ) public class TbAckNode implements TbNode { - EmptyNodeConfiguration config; - @Override - public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException { - this.config = TbNodeUtils.convert(configuration, EmptyNodeConfiguration.class); - } + public void init(TbContext ctx, TbNodeConfiguration configuration) {} @Override public void onMsg(TbContext ctx, TbMsg msg) { diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/flow/TbCheckpointNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/flow/TbCheckpointNode.java index d7da783172..dee340af13 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/flow/TbCheckpointNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/flow/TbCheckpointNode.java @@ -17,7 +17,6 @@ package org.thingsboard.rule.engine.flow; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ObjectNode; -import lombok.extern.slf4j.Slf4j; import org.thingsboard.rule.engine.api.EmptyNodeConfiguration; import org.thingsboard.rule.engine.api.RuleNode; import org.thingsboard.rule.engine.api.TbContext; @@ -31,7 +30,6 @@ import org.thingsboard.server.common.msg.TbMsg; import static org.thingsboard.server.common.data.DataConstants.QUEUE_NAME; -@Slf4j @RuleNode( type = ComponentType.FLOW, name = "checkpoint", @@ -40,7 +38,8 @@ import static org.thingsboard.server.common.data.DataConstants.QUEUE_NAME; hasQueueName = true, nodeDescription = "transfers the message to another queue", nodeDetails = "After successful transfer incoming message is automatically acknowledged. Queue name is configurable.", - configDirective = "tbNodeEmptyConfig" + configDirective = "tbNodeEmptyConfig", + docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/flow/checkpoint/" ) public class TbCheckpointNode implements TbNode { @@ -48,7 +47,7 @@ public class TbCheckpointNode implements TbNode { @Override public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException { - this.queueName = ctx.getQueueName(); + queueName = ctx.getQueueName(); } @Override diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/flow/TbRuleChainInputNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/flow/TbRuleChainInputNode.java index 3c72699884..a8efa6ddd7 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/flow/TbRuleChainInputNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/flow/TbRuleChainInputNode.java @@ -17,7 +17,6 @@ package org.thingsboard.rule.engine.flow; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ObjectNode; -import lombok.extern.slf4j.Slf4j; import org.thingsboard.rule.engine.api.RuleNode; import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNode; @@ -34,7 +33,6 @@ import org.thingsboard.server.common.msg.TbMsg; import java.util.Optional; import java.util.UUID; -@Slf4j @RuleNode( type = ComponentType.FLOW, name = "rule chain", @@ -49,7 +47,8 @@ import java.util.UUID; configDirective = "tbFlowNodeRuleChainInputConfig", relationTypes = {}, ruleChainNode = true, - customRelations = true + customRelations = true, + docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/flow/rule-chain/" ) public class TbRuleChainInputNode implements TbNode { @@ -106,4 +105,5 @@ public class TbRuleChainInputNode implements TbNode { default -> null; }); } + } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/flow/TbRuleChainOutputNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/flow/TbRuleChainOutputNode.java index 968ba741a3..88eba3573b 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/flow/TbRuleChainOutputNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/flow/TbRuleChainOutputNode.java @@ -15,17 +15,14 @@ */ package org.thingsboard.rule.engine.flow; -import lombok.extern.slf4j.Slf4j; import org.thingsboard.rule.engine.api.EmptyNodeConfiguration; import org.thingsboard.rule.engine.api.RuleNode; import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNode; import org.thingsboard.rule.engine.api.TbNodeConfiguration; -import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.msg.TbMsg; -@Slf4j @RuleNode( type = ComponentType.FLOW, name = "output", @@ -35,13 +32,13 @@ import org.thingsboard.server.common.msg.TbMsg; "The output is forwarded to the caller rule chain, as an output of the corresponding \"input\" rule node. " + "The output rule node name corresponds to the relation type of the output message, and it is used to forward messages to other rule nodes in the caller rule chain. ", configDirective = "tbFlowNodeRuleChainOutputConfig", - outEnabled = false + outEnabled = false, + docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/flow/output/" ) public class TbRuleChainOutputNode implements TbNode { @Override - public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException { - } + public void init(TbContext ctx, TbNodeConfiguration configuration) {} @Override public void onMsg(TbContext ctx, TbMsg msg) { diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/gcp/pubsub/TbPubSubNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/gcp/pubsub/TbPubSubNode.java index ea893cc71b..2c170f7a2d 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/gcp/pubsub/TbPubSubNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/gcp/pubsub/TbPubSubNode.java @@ -53,7 +53,8 @@ import java.util.concurrent.TimeUnit; "(messageId in the Message Metadata from the GCP PubSub. " + "messageId field can be accessed with metadata.messageId.", configDirective = "tbExternalNodePubSubConfig", - iconUrl = "data:image/svg+xml;base64,PHN2ZyBpZD0iTGF5ZXJfMSIgZGF0YS1uYW1lPSJMYXllciAxIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxMjgiIGhlaWdodD0iMTI4IiB2aWV3Qm94PSIwIDAgMTI4IDEyOCI+Cjx0aXRsZT5DbG91ZCBQdWJTdWI8L3RpdGxlPgo8Zz4KPHBhdGggZD0iTTEyNi40Nyw1OC4xMmwtMjYuMy00NS43NEExMS41NiwxMS41NiwwLDAsMCw5MC4zMSw2LjVIMzcuN2ExMS41NSwxMS41NSwwLDAsMC05Ljg2LDUuODhMMS41Myw1OGExMS40OCwxMS40OCwwLDAsMCwwLDExLjQ0bDI2LjMsNDZhMTEuNzcsMTEuNzcsMCwwLDAsOS44Niw2LjA5SDkwLjNhMTEuNzMsMTEuNzMsMCwwLDAsOS44Ny02LjA2bDI2LjMtNDUuNzRBMTEuNzMsMTEuNzMsMCwwLDAsMTI2LjQ3LDU4LjEyWiIgc3R5bGU9ImZpbGw6ICM3MzViMmYiLz4KPHBhdGggZD0iTTg5LjIyLDQ3Ljc0LDgzLjM2LDQ5bC0xNC42LTE0LjZMNjQuMDksNDMuMSw2MS41NSw1My4ybDQuMjksNC4yOUw1Ny42LDU5LjE4LDQ2LjMsNDcuODhsLTcuNjcsNy4zOEw1Mi43Niw2OS4zN2wtMTUsMTEuOUw3OCwxMjEuNUg5MC4zYTExLjczLDExLjczLDAsMCwwLDkuODctNi4wNmwyMC43Mi0zNloiIHN0eWxlPSJvcGFjaXR5OiAwLjA3MDAwMDAwMDI5ODAyMztpc29sYXRpb246IGlzb2xhdGUiLz4KPHBhdGggZD0iTTgyLjg2LDQ3YTUuMzIsNS4zMiwwLDEsMS0xLjk1LDcuMjdBNS4zMiw1LjMyLDAsMCwxLDgyLjg2LDQ3IiBzdHlsZT0iZmlsbDogI2ZmZiIvPgo8cGF0aCBkPSJNMzkuODIsNTYuMThhNS4zMiw1LjMyLDAsMSwxLDcuMjctMS45NSw1LjMyLDUuMzIsMCwwLDEtNy4yNywxLjk1IiBzdHlsZT0iZmlsbDogI2ZmZiIvPgo8cGF0aCBkPSJNNjkuMzIsODguODVBNS4zMiw1LjMyLDAsMSwxLDY0LDgzLjUyYTUuMzIsNS4zMiwwLDAsMSw1LjMyLDUuMzIiIHN0eWxlPSJmaWxsOiAjZmZmIi8+CjxnPgo8cGF0aCBkPSJNNjQsNTIuOTRhMTEuMDYsMTEuMDYsMCwwLDEsMi40Ni4yOFYzOS4xNUg2MS41NFY1My4yMkExMS4wNiwxMS4wNiwwLDAsMSw2NCw1Mi45NFoiIHN0eWxlPSJmaWxsOiAjZmZmIi8+CjxwYXRoIGQ9Ik03NC41Nyw2Ny4yNmExMSwxMSwwLDAsMS0yLjQ3LDQuMjVsMTIuMTksNywyLjQ2LTQuMjZaIiBzdHlsZT0iZmlsbDogI2ZmZiIvPgo8cGF0aCBkPSJNNTMuNDMsNjcuMjZsLTEyLjE4LDcsMi40Niw0LjI2LDEyLjE5LTdBMTEsMTEsMCwwLDEsNTMuNDMsNjcuMjZaIiBzdHlsZT0iZmlsbDogI2ZmZiIvPgo8L2c+CjxwYXRoIGQ9Ik03Mi42LDY0QTguNiw4LjYsMCwxLDEsNjQsNTUuNCw4LjYsOC42LDAsMCwxLDcyLjYsNjQiIHN0eWxlPSJmaWxsOiAjZmZmIi8+CjxwYXRoIGQ9Ik0zOS4xLDcwLjU3YTYuNzYsNi43NiwwLDEsMS0yLjQ3LDkuMjMsNi43Niw2Ljc2LDAsMCwxLDIuNDctOS4yMyIgc3R5bGU9ImZpbGw6ICNmZmYiLz4KPHBhdGggZD0iTTgyLjE0LDgyLjI3YTYuNzYsNi43NiwwLDEsMSw5LjIzLTIuNDcsNi43NSw2Ljc1LDAsMCwxLTkuMjMsMi40NyIgc3R5bGU9ImZpbGw6ICNmZmYiLz4KPHBhdGggZD0iTTcwLjc2LDM5LjE1QTYuNzYsNi43NiwwLDEsMSw2NCwzMi4zOWE2Ljc2LDYuNzYsMCwwLDEsNi43Niw2Ljc2IiBzdHlsZT0iZmlsbDogI2ZmZiIvPgo8L2c+Cjwvc3ZnPgo=" + iconUrl = "data:image/svg+xml;base64,PHN2ZyBpZD0iTGF5ZXJfMSIgZGF0YS1uYW1lPSJMYXllciAxIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxMjgiIGhlaWdodD0iMTI4IiB2aWV3Qm94PSIwIDAgMTI4IDEyOCI+Cjx0aXRsZT5DbG91ZCBQdWJTdWI8L3RpdGxlPgo8Zz4KPHBhdGggZD0iTTEyNi40Nyw1OC4xMmwtMjYuMy00NS43NEExMS41NiwxMS41NiwwLDAsMCw5MC4zMSw2LjVIMzcuN2ExMS41NSwxMS41NSwwLDAsMC05Ljg2LDUuODhMMS41Myw1OGExMS40OCwxMS40OCwwLDAsMCwwLDExLjQ0bDI2LjMsNDZhMTEuNzcsMTEuNzcsMCwwLDAsOS44Niw2LjA5SDkwLjNhMTEuNzMsMTEuNzMsMCwwLDAsOS44Ny02LjA2bDI2LjMtNDUuNzRBMTEuNzMsMTEuNzMsMCwwLDAsMTI2LjQ3LDU4LjEyWiIgc3R5bGU9ImZpbGw6ICM3MzViMmYiLz4KPHBhdGggZD0iTTg5LjIyLDQ3Ljc0LDgzLjM2LDQ5bC0xNC42LTE0LjZMNjQuMDksNDMuMSw2MS41NSw1My4ybDQuMjksNC4yOUw1Ny42LDU5LjE4LDQ2LjMsNDcuODhsLTcuNjcsNy4zOEw1Mi43Niw2OS4zN2wtMTUsMTEuOUw3OCwxMjEuNUg5MC4zYTExLjczLDExLjczLDAsMCwwLDkuODctNi4wNmwyMC43Mi0zNloiIHN0eWxlPSJvcGFjaXR5OiAwLjA3MDAwMDAwMDI5ODAyMztpc29sYXRpb246IGlzb2xhdGUiLz4KPHBhdGggZD0iTTgyLjg2LDQ3YTUuMzIsNS4zMiwwLDEsMS0xLjk1LDcuMjdBNS4zMiw1LjMyLDAsMCwxLDgyLjg2LDQ3IiBzdHlsZT0iZmlsbDogI2ZmZiIvPgo8cGF0aCBkPSJNMzkuODIsNTYuMThhNS4zMiw1LjMyLDAsMSwxLDcuMjctMS45NSw1LjMyLDUuMzIsMCwwLDEtNy4yNywxLjk1IiBzdHlsZT0iZmlsbDogI2ZmZiIvPgo8cGF0aCBkPSJNNjkuMzIsODguODVBNS4zMiw1LjMyLDAsMSwxLDY0LDgzLjUyYTUuMzIsNS4zMiwwLDAsMSw1LjMyLDUuMzIiIHN0eWxlPSJmaWxsOiAjZmZmIi8+CjxnPgo8cGF0aCBkPSJNNjQsNTIuOTRhMTEuMDYsMTEuMDYsMCwwLDEsMi40Ni4yOFYzOS4xNUg2MS41NFY1My4yMkExMS4wNiwxMS4wNiwwLDAsMSw2NCw1Mi45NFoiIHN0eWxlPSJmaWxsOiAjZmZmIi8+CjxwYXRoIGQ9Ik03NC41Nyw2Ny4yNmExMSwxMSwwLDAsMS0yLjQ3LDQuMjVsMTIuMTksNywyLjQ2LTQuMjZaIiBzdHlsZT0iZmlsbDogI2ZmZiIvPgo8cGF0aCBkPSJNNTMuNDMsNjcuMjZsLTEyLjE4LDcsMi40Niw0LjI2LDEyLjE5LTdBMTEsMTEsMCwwLDEsNTMuNDMsNjcuMjZaIiBzdHlsZT0iZmlsbDogI2ZmZiIvPgo8L2c+CjxwYXRoIGQ9Ik03Mi42LDY0QTguNiw4LjYsMCwxLDEsNjQsNTUuNCw4LjYsOC42LDAsMCwxLDcyLjYsNjQiIHN0eWxlPSJmaWxsOiAjZmZmIi8+CjxwYXRoIGQ9Ik0zOS4xLDcwLjU3YTYuNzYsNi43NiwwLDEsMS0yLjQ3LDkuMjMsNi43Niw2Ljc2LDAsMCwxLDIuNDctOS4yMyIgc3R5bGU9ImZpbGw6ICNmZmYiLz4KPHBhdGggZD0iTTgyLjE0LDgyLjI3YTYuNzYsNi43NiwwLDEsMSw5LjIzLTIuNDcsNi43NSw2Ljc1LDAsMCwxLTkuMjMsMi40NyIgc3R5bGU9ImZpbGw6ICNmZmYiLz4KPHBhdGggZD0iTTcwLjc2LDM5LjE1QTYuNzYsNi43NiwwLDEsMSw2NCwzMi4zOWE2Ljc2LDYuNzYsMCwwLDEsNi43Niw2Ljc2IiBzdHlsZT0iZmlsbDogI2ZmZiIvPgo8L2c+Cjwvc3ZnPgo=", + docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/external/gcp-pubsub/" ) public class TbPubSubNode extends TbAbstractExternalNode { @@ -155,4 +156,5 @@ public class TbPubSubNode extends TbAbstractExternalNode { .setExecutorProvider(FixedExecutorProvider.create(ctx.getPubSubRuleNodeExecutorProvider().getExecutor())) .build(); } + } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/geo/TbGpsGeofencingActionNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/geo/TbGpsGeofencingActionNode.java index b362b60b8e..488ee2123e 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/geo/TbGpsGeofencingActionNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/geo/TbGpsGeofencingActionNode.java @@ -20,7 +20,6 @@ import com.fasterxml.jackson.databind.node.ObjectNode; import com.google.gson.Gson; import com.google.gson.JsonObject; import com.google.gson.JsonParser; -import lombok.extern.slf4j.Slf4j; import org.thingsboard.rule.engine.api.RuleNode; import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNodeException; @@ -34,10 +33,10 @@ import org.thingsboard.server.common.data.util.TbPair; import org.thingsboard.server.common.msg.TbMsg; import java.util.Collections; -import java.util.HashMap; import java.util.List; -import java.util.Map; import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; @@ -47,10 +46,6 @@ import static org.thingsboard.rule.engine.util.GpsGeofencingEvents.INSIDE; import static org.thingsboard.rule.engine.util.GpsGeofencingEvents.LEFT; import static org.thingsboard.rule.engine.util.GpsGeofencingEvents.OUTSIDE; -/** - * Created by ashvayka on 19.01.18. - */ -@Slf4j @RuleNode( type = ComponentType.ACTION, name = "gps geofencing events", @@ -66,12 +61,13 @@ import static org.thingsboard.rule.engine.util.GpsGeofencingEvents.OUTSIDE; "If the presence monitoring strategy \"On each message\" is selected, sends messages via rule node connection type Inside or Outside every time the geofencing condition is satisfied. " + "

    " + "Output connections: Entered, Left, Inside, Outside, Success", - configDirective = "tbActionNodeGpsGeofencingConfig" + configDirective = "tbActionNodeGpsGeofencingConfig", + docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/action/gps-geofencing-events/" ) public class TbGpsGeofencingActionNode extends AbstractGeofencingNode { private static final String REPORT_PRESENCE_STATUS_ON_EACH_MESSAGE = "reportPresenceStatusOnEachMessage"; - private final Map entityStates = new HashMap<>(); + private final ConcurrentMap entityStates = new ConcurrentHashMap<>(); private final Gson gson = new Gson(); @Override diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/geo/TbGpsGeofencingFilterNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/geo/TbGpsGeofencingFilterNode.java index 19c901483c..7c1a561492 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/geo/TbGpsGeofencingFilterNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/geo/TbGpsGeofencingFilterNode.java @@ -15,7 +15,6 @@ */ package org.thingsboard.rule.engine.geo; -import lombok.extern.slf4j.Slf4j; import org.thingsboard.rule.engine.api.RuleNode; import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNodeException; @@ -23,10 +22,6 @@ import org.thingsboard.server.common.data.msg.TbNodeConnectionType; import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.msg.TbMsg; -/** - * Created by ashvayka on 19.01.18. - */ -@Slf4j @RuleNode( type = ComponentType.FILTER, name = "gps geofencing filter", @@ -60,7 +55,9 @@ import org.thingsboard.server.common.msg.TbMsg; "

    " + "Available radius units: METER, KILOMETER, FOOT, MILE, NAUTICAL_MILE;

    " + "Output connections: True, False, Failure", - configDirective = "tbFilterNodeGpsGeofencingConfig") + configDirective = "tbFilterNodeGpsGeofencingConfig", + docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/filter/gps-geofencing-filter/" +) public class TbGpsGeofencingFilterNode extends AbstractGeofencingNode { @Override @@ -72,4 +69,5 @@ public class TbGpsGeofencingFilterNode extends AbstractGeofencingNode getConfigClazz() { return TbGpsGeofencingFilterNodeConfiguration.class; } + } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/kafka/TbKafkaNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/kafka/TbKafkaNode.java index d544d0647d..a6549dbd15 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/kafka/TbKafkaNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/kafka/TbKafkaNode.java @@ -58,7 +58,8 @@ import java.util.Properties; "Outbound message will contain response fields (offset, partition, topic)" + " from the Kafka in the Message Metadata. For example partition field can be accessed with metadata.partition.", configDirective = "tbExternalNodeKafkaConfig", - iconUrl = "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTUzOCIgaGVpZ2h0PSIyNTAwIiB2aWV3Qm94PSIwIDAgMjU2IDQxNiIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiBwcmVzZXJ2ZUFzcGVjdFJhdGlvPSJ4TWlkWU1pZCI+PHBhdGggZD0iTTIwMS44MTYgMjMwLjIxNmMtMTYuMTg2IDAtMzAuNjk3IDcuMTcxLTQwLjYzNCAxOC40NjFsLTI1LjQ2My0xOC4wMjZjMi43MDMtNy40NDIgNC4yNTUtMTUuNDMzIDQuMjU1LTIzLjc5NyAwLTguMjE5LTEuNDk4LTE2LjA3Ni00LjExMi0yMy40MDhsMjUuNDA2LTE3LjgzNWM5LjkzNiAxMS4yMzMgMjQuNDA5IDE4LjM2NSA0MC41NDggMTguMzY1IDI5Ljg3NSAwIDU0LjE4NC0yNC4zMDUgNTQuMTg0LTU0LjE4NCAwLTI5Ljg3OS0yNC4zMDktNTQuMTg0LTU0LjE4NC01NC4xODQtMjkuODc1IDAtNTQuMTg0IDI0LjMwNS01NC4xODQgNTQuMTg0IDAgNS4zNDguODA4IDEwLjUwNSAyLjI1OCAxNS4zODlsLTI1LjQyMyAxNy44NDRjLTEwLjYyLTEzLjE3NS0yNS45MTEtMjIuMzc0LTQzLjMzMy0yNS4xODJ2LTMwLjY0YzI0LjU0NC01LjE1NSA0My4wMzctMjYuOTYyIDQzLjAzNy01My4wMTlDMTI0LjE3MSAyNC4zMDUgOTkuODYyIDAgNjkuOTg3IDAgNDAuMTEyIDAgMTUuODAzIDI0LjMwNSAxNS44MDMgNTQuMTg0YzAgMjUuNzA4IDE4LjAxNCA0Ny4yNDYgNDIuMDY3IDUyLjc2OXYzMS4wMzhDMjUuMDQ0IDE0My43NTMgMCAxNzIuNDAxIDAgMjA2Ljg1NGMwIDM0LjYyMSAyNS4yOTIgNjMuMzc0IDU4LjM1NSA2OC45NHYzMi43NzRjLTI0LjI5OSA1LjM0MS00Mi41NTIgMjcuMDExLTQyLjU1MiA1Mi44OTQgMCAyOS44NzkgMjQuMzA5IDU0LjE4NCA1NC4xODQgNTQuMTg0IDI5Ljg3NSAwIDU0LjE4NC0yNC4zMDUgNTQuMTg0LTU0LjE4NCAwLTI1Ljg4My0xOC4yNTMtNDcuNTUzLTQyLjU1Mi01Mi44OTR2LTMyLjc3NWE2OS45NjUgNjkuOTY1IDAgMCAwIDQyLjYtMjQuNzc2bDI1LjYzMyAxOC4xNDNjLTEuNDIzIDQuODQtMi4yMiA5Ljk0Ni0yLjIyIDE1LjI0IDAgMjkuODc5IDI0LjMwOSA1NC4xODQgNTQuMTg0IDU0LjE4NCAyOS44NzUgMCA1NC4xODQtMjQuMzA1IDU0LjE4NC01NC4xODQgMC0yOS44NzktMjQuMzA5LTU0LjE4NC01NC4xODQtNTQuMTg0em0wLTEyNi42OTVjMTQuNDg3IDAgMjYuMjcgMTEuNzg4IDI2LjI3IDI2LjI3MXMtMTEuNzgzIDI2LjI3LTI2LjI3IDI2LjI3LTI2LjI3LTExLjc4Ny0yNi4yNy0yNi4yN2MwLTE0LjQ4MyAxMS43ODMtMjYuMjcxIDI2LjI3LTI2LjI3MXptLTE1OC4xLTQ5LjMzN2MwLTE0LjQ4MyAxMS43ODQtMjYuMjcgMjYuMjcxLTI2LjI3czI2LjI3IDExLjc4NyAyNi4yNyAyNi4yN2MwIDE0LjQ4My0xMS43ODMgMjYuMjctMjYuMjcgMjYuMjdzLTI2LjI3MS0xMS43ODctMjYuMjcxLTI2LjI3em01Mi41NDEgMzA3LjI3OGMwIDE0LjQ4My0xMS43ODMgMjYuMjctMjYuMjcgMjYuMjdzLTI2LjI3MS0xMS43ODctMjYuMjcxLTI2LjI3YzAtMTQuNDgzIDExLjc4NC0yNi4yNyAyNi4yNzEtMjYuMjdzMjYuMjcgMTEuNzg3IDI2LjI3IDI2LjI3em0tMjYuMjcyLTExNy45N2MtMjAuMjA1IDAtMzYuNjQyLTE2LjQzNC0zNi42NDItMzYuNjM4IDAtMjAuMjA1IDE2LjQzNy0zNi42NDIgMzYuNjQyLTM2LjY0MiAyMC4yMDQgMCAzNi42NDEgMTYuNDM3IDM2LjY0MSAzNi42NDIgMCAyMC4yMDQtMTYuNDM3IDM2LjYzOC0zNi42NDEgMzYuNjM4em0xMzEuODMxIDY3LjE3OWMtMTQuNDg3IDAtMjYuMjctMTEuNzg4LTI2LjI3LTI2LjI3MXMxMS43ODMtMjYuMjcgMjYuMjctMjYuMjcgMjYuMjcgMTEuNzg3IDI2LjI3IDI2LjI3YzAgMTQuNDgzLTExLjc4MyAyNi4yNzEtMjYuMjcgMjYuMjcxeiIvPjwvc3ZnPg==" + iconUrl = "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTUzOCIgaGVpZ2h0PSIyNTAwIiB2aWV3Qm94PSIwIDAgMjU2IDQxNiIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiBwcmVzZXJ2ZUFzcGVjdFJhdGlvPSJ4TWlkWU1pZCI+PHBhdGggZD0iTTIwMS44MTYgMjMwLjIxNmMtMTYuMTg2IDAtMzAuNjk3IDcuMTcxLTQwLjYzNCAxOC40NjFsLTI1LjQ2My0xOC4wMjZjMi43MDMtNy40NDIgNC4yNTUtMTUuNDMzIDQuMjU1LTIzLjc5NyAwLTguMjE5LTEuNDk4LTE2LjA3Ni00LjExMi0yMy40MDhsMjUuNDA2LTE3LjgzNWM5LjkzNiAxMS4yMzMgMjQuNDA5IDE4LjM2NSA0MC41NDggMTguMzY1IDI5Ljg3NSAwIDU0LjE4NC0yNC4zMDUgNTQuMTg0LTU0LjE4NCAwLTI5Ljg3OS0yNC4zMDktNTQuMTg0LTU0LjE4NC01NC4xODQtMjkuODc1IDAtNTQuMTg0IDI0LjMwNS01NC4xODQgNTQuMTg0IDAgNS4zNDguODA4IDEwLjUwNSAyLjI1OCAxNS4zODlsLTI1LjQyMyAxNy44NDRjLTEwLjYyLTEzLjE3NS0yNS45MTEtMjIuMzc0LTQzLjMzMy0yNS4xODJ2LTMwLjY0YzI0LjU0NC01LjE1NSA0My4wMzctMjYuOTYyIDQzLjAzNy01My4wMTlDMTI0LjE3MSAyNC4zMDUgOTkuODYyIDAgNjkuOTg3IDAgNDAuMTEyIDAgMTUuODAzIDI0LjMwNSAxNS44MDMgNTQuMTg0YzAgMjUuNzA4IDE4LjAxNCA0Ny4yNDYgNDIuMDY3IDUyLjc2OXYzMS4wMzhDMjUuMDQ0IDE0My43NTMgMCAxNzIuNDAxIDAgMjA2Ljg1NGMwIDM0LjYyMSAyNS4yOTIgNjMuMzc0IDU4LjM1NSA2OC45NHYzMi43NzRjLTI0LjI5OSA1LjM0MS00Mi41NTIgMjcuMDExLTQyLjU1MiA1Mi44OTQgMCAyOS44NzkgMjQuMzA5IDU0LjE4NCA1NC4xODQgNTQuMTg0IDI5Ljg3NSAwIDU0LjE4NC0yNC4zMDUgNTQuMTg0LTU0LjE4NCAwLTI1Ljg4My0xOC4yNTMtNDcuNTUzLTQyLjU1Mi01Mi44OTR2LTMyLjc3NWE2OS45NjUgNjkuOTY1IDAgMCAwIDQyLjYtMjQuNzc2bDI1LjYzMyAxOC4xNDNjLTEuNDIzIDQuODQtMi4yMiA5Ljk0Ni0yLjIyIDE1LjI0IDAgMjkuODc5IDI0LjMwOSA1NC4xODQgNTQuMTg0IDU0LjE4NCAyOS44NzUgMCA1NC4xODQtMjQuMzA1IDU0LjE4NC01NC4xODQgMC0yOS44NzktMjQuMzA5LTU0LjE4NC01NC4xODQtNTQuMTg0em0wLTEyNi42OTVjMTQuNDg3IDAgMjYuMjcgMTEuNzg4IDI2LjI3IDI2LjI3MXMtMTEuNzgzIDI2LjI3LTI2LjI3IDI2LjI3LTI2LjI3LTExLjc4Ny0yNi4yNy0yNi4yN2MwLTE0LjQ4MyAxMS43ODMtMjYuMjcxIDI2LjI3LTI2LjI3MXptLTE1OC4xLTQ5LjMzN2MwLTE0LjQ4MyAxMS43ODQtMjYuMjcgMjYuMjcxLTI2LjI3czI2LjI3IDExLjc4NyAyNi4yNyAyNi4yN2MwIDE0LjQ4My0xMS43ODMgMjYuMjctMjYuMjcgMjYuMjdzLTI2LjI3MS0xMS43ODctMjYuMjcxLTI2LjI3em01Mi41NDEgMzA3LjI3OGMwIDE0LjQ4My0xMS43ODMgMjYuMjctMjYuMjcgMjYuMjdzLTI2LjI3MS0xMS43ODctMjYuMjcxLTI2LjI3YzAtMTQuNDgzIDExLjc4NC0yNi4yNyAyNi4yNzEtMjYuMjdzMjYuMjcgMTEuNzg3IDI2LjI3IDI2LjI3em0tMjYuMjcyLTExNy45N2MtMjAuMjA1IDAtMzYuNjQyLTE2LjQzNC0zNi42NDItMzYuNjM4IDAtMjAuMjA1IDE2LjQzNy0zNi42NDIgMzYuNjQyLTM2LjY0MiAyMC4yMDQgMCAzNi42NDEgMTYuNDM3IDM2LjY0MSAzNi42NDIgMCAyMC4yMDQtMTYuNDM3IDM2LjYzOC0zNi42NDEgMzYuNjM4em0xMzEuODMxIDY3LjE3OWMtMTQuNDg3IDAtMjYuMjctMTEuNzg4LTI2LjI3LTI2LjI3MXMxMS43ODMtMjYuMjcgMjYuMjctMjYuMjcgMjYuMjcgMTEuNzg3IDI2LjI3IDI2LjI3YzAgMTQuNDgzLTExLjc4MyAyNi4yNzEtMjYuMjcgMjYuMjcxeiIvPjwvc3ZnPg==", + docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/external/kafka/" ) public class TbKafkaNode extends TbAbstractExternalNode { diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mail/TbMsgToEmailNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mail/TbMsgToEmailNode.java index f20aeca55f..4867735a2b 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mail/TbMsgToEmailNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mail/TbMsgToEmailNode.java @@ -16,7 +16,6 @@ package org.thingsboard.rule.engine.mail; import com.fasterxml.jackson.core.type.TypeReference; -import lombok.extern.slf4j.Slf4j; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.rule.engine.api.RuleNode; import org.thingsboard.rule.engine.api.TbContext; @@ -34,7 +33,6 @@ import org.thingsboard.server.common.msg.TbMsg; import java.util.HashMap; import java.util.Map; -@Slf4j @RuleNode( type = ComponentType.TRANSFORMATION, name = "to email", @@ -43,7 +41,8 @@ import java.util.Map; nodeDetails = "Transforms message to email message. If transformation completed successfully output message type will be set to SEND_EMAIL.

    " + "Output connections: Success, Failure.", configDirective = "tbTransformationNodeToEmailConfig", - icon = "email" + icon = "email", + docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/transformation/to-email/" ) public class TbMsgToEmailNode implements TbNode { @@ -55,20 +54,15 @@ public class TbMsgToEmailNode implements TbNode { @Override public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException { - this.config = TbNodeUtils.convert(configuration, TbMsgToEmailNodeConfiguration.class); - this.dynamicMailBodyType = DYNAMIC.equals(this.config.getMailBodyType()); - } + config = TbNodeUtils.convert(configuration, TbMsgToEmailNodeConfiguration.class); + dynamicMailBodyType = DYNAMIC.equals(config.getMailBodyType()); + } @Override public void onMsg(TbContext ctx, TbMsg msg) { - try { - TbEmail email = convert(msg); - TbMsg emailMsg = buildEmailMsg(ctx, msg, email); - ctx.tellNext(emailMsg, TbNodeConnectionType.SUCCESS); - } catch (Exception ex) { - log.warn("Can not convert message to email " + ex.getMessage()); - ctx.tellFailure(msg, ex); - } + TbEmail email = convert(msg); + TbMsg emailMsg = buildEmailMsg(ctx, msg, email); + ctx.tellNext(emailMsg, TbNodeConnectionType.SUCCESS); } private TbMsg buildEmailMsg(TbContext ctx, TbMsg msg, TbEmail email) { diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mail/TbSendEmailNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mail/TbSendEmailNode.java index 630d87cb54..198808297e 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mail/TbSendEmailNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mail/TbSendEmailNode.java @@ -45,7 +45,8 @@ import static org.thingsboard.common.util.DonAsynchron.withCallback; " where created using to Email transformation Node, please connect this Node " + "with to Email Node using Successful chain.", configDirective = "tbExternalNodeSendEmailConfig", - icon = "send" + icon = "send", + docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/external/send-email/" ) public class TbSendEmailNode extends TbAbstractExternalNode { @@ -91,7 +92,7 @@ public class TbSendEmailNode extends TbAbstractExternalNode { } } - private TbEmail getEmail(TbMsg msg) throws IOException { + private TbEmail getEmail(TbMsg msg) { TbEmail email = JacksonUtil.fromString(msg.getData(), TbEmail.class); if (StringUtils.isBlank(email.getTo())) { throw new IllegalStateException("Email destination can not be blank [" + email.getTo() + "]"); @@ -141,4 +142,5 @@ public class TbSendEmailNode extends TbAbstractExternalNode { } return javaMailProperties; } + } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/math/TbMathNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/math/TbMathNode.java index 76ba71163a..82dd20cff4 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/math/TbMathNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/math/TbMathNode.java @@ -20,10 +20,9 @@ import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.MoreExecutors; import com.google.common.util.concurrent.SettableFuture; -import lombok.extern.slf4j.Slf4j; import net.objecthunter.exp4j.Expression; -import net.objecthunter.exp4j.ExpressionBuilder; import org.springframework.util.ConcurrentReferenceHashMap; +import org.thingsboard.common.util.ExpressionUtils; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.rule.engine.api.AttributesSaveRequest; import org.thingsboard.rule.engine.api.RuleNode; @@ -53,11 +52,8 @@ import java.util.function.BiFunction; import java.util.function.Function; import java.util.stream.Collectors; -import static org.thingsboard.common.util.ExpressionFunctionsUtil.userDefinedFunctions; import static org.thingsboard.rule.engine.math.TbMathArgumentType.CONSTANT; -@SuppressWarnings("UnstableApiUsage") -@Slf4j @RuleNode( type = ComponentType.ACTION, name = "math function", @@ -78,8 +74,8 @@ import static org.thingsboard.rule.engine.math.TbMathArgumentType.CONSTANT; "The execution is synchronized in scope of message originator (e.g. device) and server node. " + "If you have rule nodes in different rule chains, they will process messages from the same originator synchronously in the scope of the server node.", configDirective = "tbActionNodeMathFunctionConfig", - icon = "calculate" - + icon = "calculate", + docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/action/math-function/" ) public class TbMathNode implements TbNode { @@ -310,11 +306,8 @@ public class TbMathNode implements TbNode { case CUSTOM: var expr = customExpression.get(); if (expr == null) { - expr = new ExpressionBuilder(config.getCustomFunction()) - .functions(userDefinedFunctions) - .implicitMultiplication(true) - .variables(config.getArguments().stream().map(TbMathArgument::getName).collect(Collectors.toSet())) - .build(); + expr = ExpressionUtils.createExpression(config.getCustomFunction(), config.getArguments().stream() + .map(TbMathArgument::getName).collect(Collectors.toSet())); customExpression.set(expr); } for (int i = 0; i < config.getArguments().size(); i++) { diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/CalculateDeltaNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/CalculateDeltaNode.java index 29c7dc51e5..dd1888f3de 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/CalculateDeltaNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/CalculateDeltaNode.java @@ -20,7 +20,6 @@ import com.fasterxml.jackson.databind.node.ObjectNode; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.MoreExecutors; -import lombok.extern.slf4j.Slf4j; import org.springframework.util.ConcurrentReferenceHashMap; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.rule.engine.api.RuleNode; @@ -43,8 +42,8 @@ import java.math.BigDecimal; import java.math.RoundingMode; import java.util.Map; -@Slf4j -@RuleNode(type = ComponentType.ENRICHMENT, +@RuleNode( + type = ComponentType.ENRICHMENT, name = "calculate delta", version = 1, relationTypes = {TbNodeConnectionType.SUCCESS, TbNodeConnectionType.FAILURE, TbNodeConnectionType.OTHER}, @@ -53,7 +52,9 @@ import java.util.Map; "and current value for this key from the incoming message", nodeDetails = "Useful for metering use cases, when you need to calculate consumption based on pulse counter reading.

    " + "Output connections: Success, Other or Failure.", - configDirective = "tbEnrichmentNodeCalculateDeltaConfig") + configDirective = "tbEnrichmentNodeCalculateDeltaConfig", + docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/enrichment/calculate-delta/" +) public class CalculateDeltaNode implements TbNode { private Map cache; @@ -189,7 +190,6 @@ public class CalculateDeltaNode implements TbNode { return fetchLatestValueAsync(ctx, originator); } - private record ValueWithTs(long ts, double value) { - } + private record ValueWithTs(long ts, double value) {} } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbAbstractGetEntityDataNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbAbstractGetEntityDataNode.java index ebf32bf6b1..1982596e91 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbAbstractGetEntityDataNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbAbstractGetEntityDataNode.java @@ -58,15 +58,9 @@ public abstract class TbAbstractGetEntityDataNode extends Tb protected void processDataAndTell(TbContext ctx, TbMsg msg, T entityId, ObjectNode msgDataAsJsonNode) { DataToFetch dataToFetch = config.getDataToFetch(); switch (dataToFetch) { - case ATTRIBUTES: - processAttributesKvEntryData(ctx, msg, entityId, msgDataAsJsonNode); - break; - case LATEST_TELEMETRY: - processTsKvEntryData(ctx, msg, entityId, msgDataAsJsonNode); - break; - case FIELDS: - processFieldsData(ctx, msg, entityId, msgDataAsJsonNode, true); - break; + case ATTRIBUTES -> processAttributesKvEntryData(ctx, msg, entityId, msgDataAsJsonNode); + case LATEST_TELEMETRY -> processTsKvEntryData(ctx, msg, entityId, msgDataAsJsonNode); + case FIELDS -> processFieldsData(ctx, msg, entityId, msgDataAsJsonNode, true); } } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbAbstractNodeWithFetchTo.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbAbstractNodeWithFetchTo.java index 189ec47ef1..a11e3333ff 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbAbstractNodeWithFetchTo.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbAbstractNodeWithFetchTo.java @@ -17,8 +17,7 @@ package org.thingsboard.rule.engine.metadata; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ObjectNode; -import com.google.common.util.concurrent.AsyncFunction; -import com.google.common.util.concurrent.Futures; +import com.google.common.base.Function; import lombok.extern.slf4j.Slf4j; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.rule.engine.api.TbContext; @@ -54,12 +53,12 @@ public abstract class TbAbstractNodeWithFetchTo AsyncFunction checkIfEntityIsPresentOrThrow(String message) { + protected Function checkIfEntityIsPresentOrThrow(String message) { return id -> { if (id == null || id.isNullUid()) { - return Futures.immediateFailedFuture(new NoSuchElementException(message)); + throw new NoSuchElementException(message); } - return Futures.immediateFuture(id); + return id; }; } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbFetchDeviceCredentialsNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbFetchDeviceCredentialsNode.java index 294354279a..421da9c768 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbFetchDeviceCredentialsNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbFetchDeviceCredentialsNode.java @@ -16,7 +16,6 @@ package org.thingsboard.rule.engine.metadata; import com.fasterxml.jackson.databind.JsonNode; -import lombok.extern.slf4j.Slf4j; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.rule.engine.api.RuleNode; import org.thingsboard.rule.engine.api.TbContext; @@ -33,7 +32,6 @@ import org.thingsboard.server.common.msg.TbMsg; import java.util.concurrent.ExecutionException; -@Slf4j @RuleNode( type = ComponentType.ENRICHMENT, name = "fetch device credentials", @@ -45,7 +43,9 @@ import java.util.concurrent.ExecutionException; "Useful when you need to fetch device credentials and use them for further message processing. " + "For example, use device credentials to interact with external systems.

    " + "Output connections: Success, Failure.", - configDirective = "tbEnrichmentNodeFetchDeviceCredentialsConfig") + configDirective = "tbEnrichmentNodeFetchDeviceCredentialsConfig", + docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/enrichment/fetch-device-credentials/" +) public class TbFetchDeviceCredentialsNode extends TbAbstractNodeWithFetchTo { private static final String CREDENTIALS = "credentials"; diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetAttributesNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetAttributesNode.java index 68ec3bb373..8af44f67f0 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetAttributesNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetAttributesNode.java @@ -18,7 +18,6 @@ package org.thingsboard.rule.engine.metadata; import com.fasterxml.jackson.databind.JsonNode; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; -import lombok.extern.slf4j.Slf4j; import org.thingsboard.rule.engine.api.RuleNode; import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNodeConfiguration; @@ -30,11 +29,8 @@ import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.data.util.TbPair; import org.thingsboard.server.common.msg.TbMsg; -/** - * Created by ashvayka on 19.01.18. - */ -@Slf4j -@RuleNode(type = ComponentType.ENRICHMENT, +@RuleNode( + type = ComponentType.ENRICHMENT, name = "originator attributes", configClazz = TbGetAttributesNodeConfiguration.class, version = 1, @@ -43,7 +39,9 @@ import org.thingsboard.server.common.msg.TbMsg; "that are not included in the incoming message to use them for further message processing. " + "For example to filter messages based on the threshold value stored in the attributes.

    " + "Output connections: Success, Failure.", - configDirective = "tbEnrichmentNodeOriginatorAttributesConfig") + configDirective = "tbEnrichmentNodeOriginatorAttributesConfig", + docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/enrichment/originator-attributes/" +) public class TbGetAttributesNode extends TbAbstractGetAttributesNode { @Override diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetCustomerAttributeNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetCustomerAttributeNode.java index 60ca50f2b3..02a1330a53 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetCustomerAttributeNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetCustomerAttributeNode.java @@ -16,21 +16,22 @@ package org.thingsboard.rule.engine.metadata; import com.fasterxml.jackson.databind.JsonNode; -import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; -import lombok.extern.slf4j.Slf4j; import org.thingsboard.rule.engine.api.RuleNode; import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.rule.engine.api.util.TbNodeUtils; -import org.thingsboard.rule.engine.util.EntitiesCustomerIdAsyncLoader; +import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.data.util.TbPair; -@Slf4j +import java.util.NoSuchElementException; + +import static com.google.common.util.concurrent.Futures.immediateFuture; + @RuleNode( type = ComponentType.ENRICHMENT, name = "customer attributes", @@ -41,11 +42,11 @@ import org.thingsboard.server.common.data.util.TbPair; "that is stored as customer attributes or telemetry data and used for dynamic message filtering, transformation, " + "or actions such as alarm creation if the threshold is exceeded.

    " + "Output connections: Success, Failure.", - configDirective = "tbEnrichmentNodeCustomerAttributesConfig") + configDirective = "tbEnrichmentNodeCustomerAttributesConfig", + docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/enrichment/customer-attributes/" +) public class TbGetCustomerAttributeNode extends TbAbstractGetEntityDataNode { - private static final String CUSTOMER_NOT_FOUND_MESSAGE = "Failed to find customer for entity with id: %s and type: %s"; - @Override protected TbGetEntityDataNodeConfiguration loadNodeConfiguration(TbNodeConfiguration configuration) throws TbNodeException { var config = TbNodeUtils.convert(configuration, TbGetEntityDataNodeConfiguration.class); @@ -56,10 +57,19 @@ public class TbGetCustomerAttributeNode extends TbAbstractGetEntityDataNode findEntityAsync(TbContext ctx, EntityId originator) { - return Futures.transformAsync(EntitiesCustomerIdAsyncLoader.findEntityIdAsync(ctx, originator), - checkIfEntityIsPresentOrThrow(String.format(CUSTOMER_NOT_FOUND_MESSAGE, originator.getId(), originator.getEntityType().getNormalName())), - ctx.getDbCallbackExecutor() - ); + if (originator.getEntityType() == EntityType.CUSTOMER) { + return immediateFuture((CustomerId) originator); + } + return ctx.getEntityService().fetchEntityCustomerIdAsync(ctx.getTenantId(), originator) + .transform(customerIdOpt -> { + if (customerIdOpt.isEmpty()) { + throw new NoSuchElementException("Originator not found"); + } + if (customerIdOpt.get().isNullUid()) { + throw new IllegalStateException("Originator is not assigned to any customer"); + } + return customerIdOpt.get(); + }, ctx.getDbCallbackExecutor()); } @Override diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetCustomerDetailsNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetCustomerDetailsNode.java index 55def65d46..7848c3c1c6 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetCustomerDetailsNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetCustomerDetailsNode.java @@ -18,7 +18,6 @@ package org.thingsboard.rule.engine.metadata; import com.fasterxml.jackson.databind.JsonNode; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; -import lombok.extern.slf4j.Slf4j; import org.thingsboard.rule.engine.api.RuleNode; import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNodeConfiguration; @@ -41,8 +40,8 @@ import org.thingsboard.server.common.msg.TbMsg; import java.util.NoSuchElementException; -@Slf4j -@RuleNode(type = ComponentType.ENRICHMENT, +@RuleNode( + type = ComponentType.ENRICHMENT, name = "customer details", configClazz = TbGetCustomerDetailsNodeConfiguration.class, version = 1, @@ -50,7 +49,9 @@ import java.util.NoSuchElementException; nodeDetails = "Useful in multi-customer solutions where we need dynamically use customer contact information " + "such as email, phone, address, etc., for notifications via email, SMS, and other notification providers.

    " + "Output connections: Success, Failure.", - configDirective = "tbEnrichmentNodeEntityDetailsConfig") + configDirective = "tbEnrichmentNodeEntityDetailsConfig", + docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/enrichment/customer-details/" +) public class TbGetCustomerDetailsNode extends TbAbstractGetEntityDetailsNode { private static final String CUSTOMER_PREFIX = "customer_"; diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetDeviceAttrNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetDeviceAttrNode.java index 031aae8859..9dbdba8ebe 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetDeviceAttrNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetDeviceAttrNode.java @@ -18,7 +18,6 @@ package org.thingsboard.rule.engine.metadata; import com.fasterxml.jackson.databind.JsonNode; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; -import lombok.extern.slf4j.Slf4j; import org.thingsboard.rule.engine.api.RuleNode; import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNodeConfiguration; @@ -31,8 +30,8 @@ import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.data.util.TbPair; import org.thingsboard.server.common.msg.TbMsg; -@Slf4j -@RuleNode(type = ComponentType.ENRICHMENT, +@RuleNode( + type = ComponentType.ENRICHMENT, name = "related device attributes", configClazz = TbGetDeviceAttrNodeConfiguration.class, version = 1, @@ -42,7 +41,9 @@ import org.thingsboard.server.common.msg.TbMsg; "Useful when you need to retrieve attributes and/or latest telemetry values from device that has a relation " + "to the message originator and use them for further message processing.

    " + "Output connections: Success, Failure.", - configDirective = "tbEnrichmentNodeDeviceAttributesConfig") + configDirective = "tbEnrichmentNodeDeviceAttributesConfig", + docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/enrichment/related-device-attributes/" +) public class TbGetDeviceAttrNode extends TbAbstractGetAttributesNode { private static final String RELATED_DEVICE_NOT_FOUND_MESSAGE = "Failed to find related device to message originator using relation query specified in the configuration!"; @@ -54,7 +55,7 @@ public class TbGetDeviceAttrNode extends TbAbstractGetAttributesNode findEntityIdAsync(TbContext ctx, TbMsg msg) { - return Futures.transformAsync( + return Futures.transform( EntitiesRelatedDeviceIdAsyncLoader.findDeviceAsync(ctx, msg.getOriginator(), config.getDeviceRelationsQuery()), checkIfEntityIsPresentOrThrow(RELATED_DEVICE_NOT_FOUND_MESSAGE), ctx.getDbCallbackExecutor()); diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetOriginatorFieldsNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetOriginatorFieldsNode.java index 816b198836..1c3269db9d 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetOriginatorFieldsNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetOriginatorFieldsNode.java @@ -17,7 +17,6 @@ package org.thingsboard.rule.engine.metadata; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ObjectNode; -import lombok.extern.slf4j.Slf4j; import org.thingsboard.rule.engine.api.RuleNode; import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNodeConfiguration; @@ -31,11 +30,8 @@ import org.thingsboard.server.common.msg.TbMsg; import java.util.concurrent.ExecutionException; -/** - * Created by ashvayka on 19.01.18. - */ -@Slf4j -@RuleNode(type = ComponentType.ENRICHMENT, +@RuleNode( + type = ComponentType.ENRICHMENT, name = "originator fields", configClazz = TbGetOriginatorFieldsConfiguration.class, version = 1, @@ -43,7 +39,9 @@ import java.util.concurrent.ExecutionException; nodeDetails = "Fetches fields values specified in the mapping. If specified field is not part of originator fields it will be ignored. " + "Useful when you need to retrieve originator fields and use them for further message processing.

    " + "Output connections: Success, Failure.", - configDirective = "tbEnrichmentNodeOriginatorFieldsConfig") + configDirective = "tbEnrichmentNodeOriginatorFieldsConfig", + docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/enrichment/originator-fields/" +) public class TbGetOriginatorFieldsNode extends TbAbstractGetMappedDataNode { protected final static String DATA_MAPPING_PROPERTY_NAME = "dataMapping"; diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetRelatedAttributeNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetRelatedAttributeNode.java index d6df8123c2..fcd11a08f1 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetRelatedAttributeNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetRelatedAttributeNode.java @@ -18,7 +18,6 @@ package org.thingsboard.rule.engine.metadata; import com.fasterxml.jackson.databind.JsonNode; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; -import lombok.extern.slf4j.Slf4j; import org.thingsboard.rule.engine.api.RuleNode; import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNodeConfiguration; @@ -31,7 +30,6 @@ import org.thingsboard.server.common.data.util.TbPair; import java.util.Arrays; -@Slf4j @RuleNode( type = ComponentType.ENRICHMENT, name = "related entity data", @@ -42,7 +40,9 @@ import java.util.Arrays; "If multiple related entities are found, only first entity is used for message enrichment, other entities are discarded. " + "Useful when you need to retrieve data from an entity that has a relation to the message originator and use them for further message processing.

    " + "Output connections: Success, Failure.", - configDirective = "tbEnrichmentNodeRelatedAttributesConfig") + configDirective = "tbEnrichmentNodeRelatedAttributesConfig", + docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/enrichment/related-entity-data/" +) public class TbGetRelatedAttributeNode extends TbAbstractGetEntityDataNode { private static final String RELATED_ENTITY_NOT_FOUND_MESSAGE = "Failed to find related entity to message originator using relation query specified in the configuration!"; @@ -58,7 +58,7 @@ public class TbGetRelatedAttributeNode extends TbAbstractGetEntityDataNode findEntityAsync(TbContext ctx, EntityId originator) { var relatedAttrConfig = (TbGetRelatedDataNodeConfiguration) config; - return Futures.transformAsync( + return Futures.transform( EntitiesRelatedEntityIdAsyncLoader.findEntityAsync(ctx, originator, relatedAttrConfig.getRelationsQuery()), checkIfEntityIsPresentOrThrow(RELATED_ENTITY_NOT_FOUND_MESSAGE), ctx.getDbCallbackExecutor()); diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetTelemetryNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetTelemetryNode.java index cc6988580a..3102b3b3fd 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetTelemetryNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetTelemetryNode.java @@ -21,7 +21,6 @@ import com.fasterxml.jackson.databind.node.ObjectNode; import com.google.common.util.concurrent.ListenableFuture; import lombok.Data; import lombok.NoArgsConstructor; -import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.math.NumberUtils; import org.thingsboard.common.util.DonAsynchron; import org.thingsboard.common.util.JacksonUtil; @@ -45,11 +44,8 @@ import java.util.List; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; -/** - * Created by mshvayka on 04.09.18. - */ -@Slf4j -@RuleNode(type = ComponentType.ENRICHMENT, +@RuleNode( + type = ComponentType.ENRICHMENT, name = "originator telemetry", configClazz = TbGetTelemetryNodeConfiguration.class, version = 2, @@ -58,7 +54,9 @@ import java.util.stream.Collectors; "instead of fetching just the latest telemetry or if you need to get the closest telemetry to the fetch interval start or end. " + "Also, this node can be used for telemetry aggregation within configured fetch interval.

    " + "Output connections: Success, Failure.", - configDirective = "tbEnrichmentNodeGetTelemetryFromDatabase") + configDirective = "tbEnrichmentNodeGetTelemetryFromDatabase", + docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/enrichment/originator-telemetry/" +) public class TbGetTelemetryNode implements TbNode { private TbGetTelemetryNodeConfiguration config; diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetTenantAttributeNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetTenantAttributeNode.java index 2fa065abf4..cc7327074f 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetTenantAttributeNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetTenantAttributeNode.java @@ -18,7 +18,6 @@ package org.thingsboard.rule.engine.metadata; import com.fasterxml.jackson.databind.JsonNode; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; -import lombok.extern.slf4j.Slf4j; import org.thingsboard.rule.engine.api.RuleNode; import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNodeConfiguration; @@ -29,7 +28,6 @@ import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.data.util.TbPair; -@Slf4j @RuleNode( type = ComponentType.ENRICHMENT, name = "tenant attributes", @@ -39,7 +37,9 @@ import org.thingsboard.server.common.data.util.TbPair; nodeDetails = "Useful when you need to retrieve some common configuration or threshold set " + "that is stored as tenant attributes or telemetry data and use it for further message processing.

    " + "Output connections: Success, Failure.", - configDirective = "tbEnrichmentNodeTenantAttributesConfig") + configDirective = "tbEnrichmentNodeTenantAttributesConfig", + docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/enrichment/tenant-attributes/" +) public class TbGetTenantAttributeNode extends TbAbstractGetEntityDataNode { @Override diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetTenantDetailsNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetTenantDetailsNode.java index 4808529436..3eafb8d165 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetTenantDetailsNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetTenantDetailsNode.java @@ -17,7 +17,6 @@ package org.thingsboard.rule.engine.metadata; import com.fasterxml.jackson.databind.JsonNode; import com.google.common.util.concurrent.ListenableFuture; -import lombok.extern.slf4j.Slf4j; import org.thingsboard.rule.engine.api.RuleNode; import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNodeConfiguration; @@ -30,8 +29,8 @@ import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.data.util.TbPair; import org.thingsboard.server.common.msg.TbMsg; -@Slf4j -@RuleNode(type = ComponentType.ENRICHMENT, +@RuleNode( + type = ComponentType.ENRICHMENT, name = "tenant details", configClazz = TbGetTenantDetailsNodeConfiguration.class, version = 1, @@ -39,7 +38,9 @@ import org.thingsboard.server.common.msg.TbMsg; nodeDetails = "Useful when we need to retrieve contact information from your tenant " + "such as email, phone, address, etc., for notifications via email, SMS, and other notification providers.

    " + "Output connections: Success, Failure.", - configDirective = "tbEnrichmentNodeEntityDetailsConfig") + configDirective = "tbEnrichmentNodeEntityDetailsConfig", + docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/enrichment/tenant-details/" +) public class TbGetTenantDetailsNode extends TbAbstractGetEntityDetailsNode { private static final String TENANT_PREFIX = "tenant_"; diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/TbMqttNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/TbMqttNode.java index 87643ae46d..e4a1c2c1c4 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/TbMqttNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/TbMqttNode.java @@ -22,7 +22,6 @@ import io.netty.handler.codec.mqtt.MqttQoS; import io.netty.handler.codec.mqtt.MqttVersion; import io.netty.handler.ssl.SslContext; import io.netty.util.concurrent.Promise; -import lombok.extern.slf4j.Slf4j; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.mqtt.MqttClient; import org.thingsboard.mqtt.MqttClientConfig; @@ -49,7 +48,6 @@ import java.nio.charset.StandardCharsets; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; -@Slf4j @RuleNode( type = ComponentType.EXTERNAL, name = "mqtt", @@ -59,7 +57,8 @@ import java.util.concurrent.TimeoutException; nodeDescription = "Publish messages to the MQTT broker", nodeDetails = "Will publish message payload to the MQTT broker with QoS AT_LEAST_ONCE.", configDirective = "tbExternalNodeMqttConfig", - icon = "call_split" + icon = "call_split", + docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/external/mqtt/" ) public class TbMqttNode extends TbAbstractExternalNode { diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/azure/TbAzureIotHubNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/azure/TbAzureIotHubNode.java index 26c5b3fa42..34d06f7192 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/azure/TbAzureIotHubNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/azure/TbAzureIotHubNode.java @@ -19,7 +19,6 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ObjectNode; import com.google.common.annotations.VisibleForTesting; import io.netty.handler.codec.mqtt.MqttVersion; -import lombok.extern.slf4j.Slf4j; import org.thingsboard.common.util.AzureIotHubUtil; import org.thingsboard.mqtt.MqttClient; import org.thingsboard.mqtt.MqttClientConfig; @@ -39,7 +38,6 @@ import org.thingsboard.server.common.data.util.TbPair; import java.time.Clock; -@Slf4j @RuleNode( type = ComponentType.EXTERNAL, name = "azure iot hub", @@ -48,7 +46,8 @@ import java.time.Clock; clusteringMode = ComponentClusteringMode.SINGLETON, nodeDescription = "Publish messages to the Azure IoT Hub", nodeDetails = "Will publish message payload to the Azure IoT Hub with QoS AT_LEAST_ONCE.", - configDirective = "tbExternalNodeAzureIotHubConfig" + configDirective = "tbExternalNodeAzureIotHubConfig", + docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/external/azure-iot-hub/" ) public class TbAzureIotHubNode extends TbMqttNode { diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/notification/TbNotificationNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/notification/TbNotificationNode.java index 6343191aa2..9838610d5b 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/notification/TbNotificationNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/notification/TbNotificationNode.java @@ -42,7 +42,8 @@ import java.util.concurrent.ExecutionException; nodeDescription = "Sends notification to targets using the template", nodeDetails = "Will send notification to the specified targets using the template", configDirective = "tbExternalNodeNotificationConfig", - icon = "notifications" + icon = "notifications", + docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/external/send-notification/" ) public class TbNotificationNode extends TbAbstractExternalNode { @@ -51,7 +52,7 @@ public class TbNotificationNode extends TbAbstractExternalNode { @Override public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException { super.init(ctx); - this.config = TbNodeUtils.convert(configuration, TbNotificationNodeConfiguration.class); + config = TbNodeUtils.convert(configuration, TbNotificationNodeConfiguration.class); } @Override diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/notification/TbSlackNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/notification/TbSlackNode.java index 0abbc48974..f6303a51f2 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/notification/TbSlackNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/notification/TbSlackNode.java @@ -34,7 +34,8 @@ import java.util.concurrent.ExecutionException; nodeDescription = "Send message via Slack", nodeDetails = "Sends message to a Slack channel or user", configDirective = "tbExternalNodeSlackConfig", - iconUrl = "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+PHBhdGggZD0iTTYsMTVBMiwyIDAgMCwxIDQsMTdBMiwyIDAgMCwxIDIsMTVBMiwyIDAgMCwxIDQsMTNINlYxNU03LDE1QTIsMiAwIDAsMSA5LDEzQTIsMiAwIDAsMSAxMSwxNVYyMEEyLDIgMCAwLDEgOSwyMkEyLDIgMCAwLDEgNywyMFYxNU05LDdBMiwyIDAgMCwxIDcsNUEyLDIgMCAwLDEgOSwzQTIsMiAwIDAsMSAxMSw1VjdIOU05LDhBMiwyIDAgMCwxIDExLDEwQTIsMiAwIDAsMSA5LDEySDRBMiwyIDAgMCwxIDIsMTBBMiwyIDAgMCwxIDQsOEg5TTE3LDEwQTIsMiAwIDAsMSAxOSw4QTIsMiAwIDAsMSAyMSwxMEEyLDIgMCAwLDEgMTksMTJIMTdWMTBNMTYsMTBBMiwyIDAgMCwxIDE0LDEyQTIsMiAwIDAsMSAxMiwxMFY1QTIsMiAwIDAsMSAxNCwzQTIsMiAwIDAsMSAxNiw1VjEwTTE0LDE4QTIsMiAwIDAsMSAxNiwyMEEyLDIgMCAwLDEgMTQsMjJBMiwyIDAgMCwxIDEyLDIwVjE4SDE0TTE0LDE3QTIsMiAwIDAsMSAxMiwxNUEyLDIgMCAwLDEgMTQsMTNIMTlBMiwyIDAgMCwxIDIxLDE1QTIsMiAwIDAsMSAxOSwxN0gxNFoiIC8+PC9zdmc+" + iconUrl = "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+PHBhdGggZD0iTTYsMTVBMiwyIDAgMCwxIDQsMTdBMiwyIDAgMCwxIDIsMTVBMiwyIDAgMCwxIDQsMTNINlYxNU03LDE1QTIsMiAwIDAsMSA5LDEzQTIsMiAwIDAsMSAxMSwxNVYyMEEyLDIgMCAwLDEgOSwyMkEyLDIgMCAwLDEgNywyMFYxNU05LDdBMiwyIDAgMCwxIDcsNUEyLDIgMCAwLDEgOSwzQTIsMiAwIDAsMSAxMSw1VjdIOU05LDhBMiwyIDAgMCwxIDExLDEwQTIsMiAwIDAsMSA5LDEySDRBMiwyIDAgMCwxIDIsMTBBMiwyIDAgMCwxIDQsOEg5TTE3LDEwQTIsMiAwIDAsMSAxOSw4QTIsMiAwIDAsMSAyMSwxMEEyLDIgMCAwLDEgMTksMTJIMTdWMTBNMTYsMTBBMiwyIDAgMCwxIDE0LDEyQTIsMiAwIDAsMSAxMiwxMFY1QTIsMiAwIDAsMSAxNCwzQTIsMiAwIDAsMSAxNiw1VjEwTTE0LDE4QTIsMiAwIDAsMSAxNiwyMEEyLDIgMCAwLDEgMTQsMjJBMiwyIDAgMCwxIDEyLDIwVjE4SDE0TTE0LDE3QTIsMiAwIDAsMSAxMiwxNUEyLDIgMCAwLDEgMTQsMTNIMTlBMiwyIDAgMCwxIDIxLDE1QTIsMiAwIDAsMSAxOSwxN0gxNFoiIC8+PC9zdmc+", + docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/external/send-to-slack/" ) public class TbSlackNode extends TbAbstractExternalNode { @@ -43,7 +44,7 @@ public class TbSlackNode extends TbAbstractExternalNode { @Override public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException { super.init(ctx); - this.config = TbNodeUtils.convert(configuration, TbSlackNodeConfiguration.class); + config = TbNodeUtils.convert(configuration, TbSlackNodeConfiguration.class); } @Override diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/AlarmEvalResult.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/AlarmEvalResult.java index 6062b9ea9c..68e3c7e2e7 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/AlarmEvalResult.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/AlarmEvalResult.java @@ -15,6 +15,7 @@ */ package org.thingsboard.rule.engine.profile; +@Deprecated public enum AlarmEvalResult { FALSE, NOT_YET_TRUE, TRUE; diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/AlarmRuleState.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/AlarmRuleState.java index a2a714a6df..1707f64bc7 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/AlarmRuleState.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/AlarmRuleState.java @@ -115,16 +115,12 @@ class AlarmRuleState { public AlarmEvalResult eval(DataSnapshot data) { boolean active = isActive(data, data.getTs()); - switch (spec.getType()) { - case SIMPLE: - return (active && eval(alarmRule.getCondition(), data)) ? AlarmEvalResult.TRUE : AlarmEvalResult.FALSE; - case DURATION: - return evalDuration(data, active); - case REPEATING: - return evalRepeating(data, active); - default: - return AlarmEvalResult.FALSE; - } + return switch (spec.getType()) { + case SIMPLE -> (active && eval(alarmRule.getCondition(), data)) ? + AlarmEvalResult.TRUE : AlarmEvalResult.FALSE; + case DURATION -> evalDuration(data, active); + case REPEATING -> evalRepeating(data, active); + }; } private boolean isActive(DataSnapshot data, long eventTs) { @@ -600,4 +596,5 @@ class AlarmRuleState { return null; } } + } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/AlarmState.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/AlarmState.java index c6cc39916e..219918c485 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/AlarmState.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/AlarmState.java @@ -48,6 +48,7 @@ import java.util.function.BiFunction; @Data @Slf4j +@Deprecated class AlarmState { public static final String ERROR_MSG = "Failed to process alarm rule for Device [%s]: %s"; diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/DataSnapshot.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/DataSnapshot.java index 33a6fe1631..b9ca93377f 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/DataSnapshot.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/DataSnapshot.java @@ -26,6 +26,7 @@ import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; +@Deprecated class DataSnapshot { private volatile boolean ready; diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/DeviceState.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/DeviceState.java index 4bd81050db..51654c4c31 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/DeviceState.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/DeviceState.java @@ -17,6 +17,7 @@ package org.thingsboard.rule.engine.profile; import com.google.gson.JsonElement; import com.google.gson.JsonParser; +import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.rule.engine.api.TbContext; @@ -29,9 +30,14 @@ import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.alarm.Alarm; +import org.thingsboard.server.common.data.device.profile.AlarmCondition; import org.thingsboard.server.common.data.device.profile.AlarmConditionFilterKey; import org.thingsboard.server.common.data.device.profile.AlarmConditionKeyType; +import org.thingsboard.server.common.data.device.profile.AlarmConditionSpec; +import org.thingsboard.server.common.data.device.profile.AlarmConditionSpecType; +import org.thingsboard.server.common.data.device.profile.AlarmRule; import org.thingsboard.server.common.data.device.profile.DeviceProfileAlarm; +import org.thingsboard.server.common.data.device.profile.DurationAlarmConditionSpec; import org.thingsboard.server.common.data.exception.ApiUsageLimitsExceededException; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.DeviceProfileId; @@ -39,6 +45,8 @@ import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.kv.AttributeKvEntry; import org.thingsboard.server.common.data.kv.KvEntry; import org.thingsboard.server.common.data.kv.TsKvEntry; +import org.thingsboard.server.common.data.query.DynamicValue; +import org.thingsboard.server.common.data.query.DynamicValueSourceType; import org.thingsboard.server.common.data.query.EntityKey; import org.thingsboard.server.common.data.query.EntityKeyType; import org.thingsboard.server.common.data.rule.RuleNodeState; @@ -56,6 +64,7 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.ExecutionException; import java.util.stream.Collectors; +import java.util.stream.Stream; import static org.thingsboard.server.common.data.msg.TbMsgType.ACTIVITY_EVENT; import static org.thingsboard.server.common.data.msg.TbMsgType.ALARM_ACK; @@ -71,6 +80,7 @@ import static org.thingsboard.server.common.data.msg.TbMsgType.POST_TELEMETRY_RE import static org.thingsboard.server.common.data.msg.TbMsgType.TIMESERIES_UPDATED; @Slf4j +@Deprecated class DeviceState { private final boolean persistState; @@ -87,6 +97,10 @@ class DeviceState { this.deviceId = deviceId; this.deviceProfile = deviceProfile; + if (hasDurationRulesWithDynamicValueFromCurrentDevice(deviceProfile)) { + latestValues = fetchLatestValues(ctx, deviceId); + } + this.dynamicPredicateValueCtx = new DynamicPredicateValueCtxImpl(ctx.getTenantId(), deviceId, ctx); if (config.isPersistAlarmRulesState()) { @@ -116,7 +130,10 @@ class DeviceState { public void updateProfile(TbContext ctx, DeviceProfile deviceProfile) throws ExecutionException, InterruptedException { Set oldKeys = Set.copyOf(this.deviceProfile.getEntityKeys()); this.deviceProfile.updateDeviceProfile(deviceProfile); - if (latestValues != null) { + + if (latestValues == null && hasDurationRulesWithDynamicValueFromCurrentDevice(this.deviceProfile)) { + latestValues = fetchLatestValues(ctx, deviceId); + } else if (latestValues != null) { Set keysToFetch = new HashSet<>(this.deviceProfile.getEntityKeys()); keysToFetch.removeAll(oldKeys); if (!keysToFetch.isEmpty()) { @@ -134,10 +151,31 @@ class DeviceState { } } + private static boolean hasDurationRulesWithDynamicValueFromCurrentDevice(ProfileState deviceProfile) { + return deviceProfile.getAlarmSettings().stream().anyMatch(DeviceState::isDurationRuleWithDynamicValueFromCurrentDevice); + } + + private static boolean isDurationRuleWithDynamicValueFromCurrentDevice(DeviceProfileAlarm alarm) { + return Stream.concat(alarm.getCreateRules().values().stream(), Stream.ofNullable(alarm.getClearRule())) + .map(AlarmRule::getCondition) + .map(AlarmCondition::getSpec) + .anyMatch(spec -> isDurationRule(spec) && hasDynamicDurationValueFromCurrentDevice((DurationAlarmConditionSpec) spec)); + } + + private static boolean isDurationRule(AlarmConditionSpec spec) { + return spec instanceof DurationAlarmConditionSpec durationSpec && durationSpec.getType() == AlarmConditionSpecType.DURATION; + } + + private static boolean hasDynamicDurationValueFromCurrentDevice(DurationAlarmConditionSpec spec) { + DynamicValue dynamicValue = spec.getPredicate().getDynamicValue(); + return dynamicValue != null && dynamicValue.getSourceType() == DynamicValueSourceType.CURRENT_DEVICE; + } + public void harvestAlarms(TbContext ctx, long ts) throws ExecutionException, InterruptedException { log.debug("[{}] Going to harvest alarms: {}", ctx.getSelfId(), ts); boolean stateChanged = false; for (AlarmState state : alarmStates.values()) { + state.setDataSnapshot(latestValues); stateChanged |= state.process(ctx, ts); } if (persistState && stateChanged) { @@ -347,7 +385,8 @@ class DeviceState { return EntityKeyType.ATTRIBUTE; } - private DataSnapshot fetchLatestValues(TbContext ctx, EntityId originator) throws ExecutionException, InterruptedException { + @SneakyThrows + private DataSnapshot fetchLatestValues(TbContext ctx, EntityId originator) { Set entityKeysToFetch = deviceProfile.getEntityKeys(); DataSnapshot result = new DataSnapshot(entityKeysToFetch); addEntityKeysToSnapshot(ctx, originator, entityKeysToFetch, result); diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/DynamicPredicateValueCtx.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/DynamicPredicateValueCtx.java index 3c2884d616..f6cdc9a781 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/DynamicPredicateValueCtx.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/DynamicPredicateValueCtx.java @@ -15,6 +15,7 @@ */ package org.thingsboard.rule.engine.profile; +@Deprecated public interface DynamicPredicateValueCtx { EntityKeyValue getTenantValue(String key); diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/DynamicPredicateValueCtxImpl.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/DynamicPredicateValueCtxImpl.java index 18fa81f63a..8962dff463 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/DynamicPredicateValueCtxImpl.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/DynamicPredicateValueCtxImpl.java @@ -29,6 +29,7 @@ import java.util.Optional; import java.util.concurrent.ExecutionException; @Slf4j +@Deprecated public class DynamicPredicateValueCtxImpl implements DynamicPredicateValueCtx { private final TenantId tenantId; private CustomerId customerId; diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/EntityKeyValue.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/EntityKeyValue.java index 7561c51386..9742ffa234 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/EntityKeyValue.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/EntityKeyValue.java @@ -20,6 +20,7 @@ import lombok.Getter; import org.thingsboard.server.common.data.kv.DataType; @EqualsAndHashCode +@Deprecated class EntityKeyValue { @Getter diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/ProfileState.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/ProfileState.java index fffc66d02a..982a7f3b36 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/ProfileState.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/ProfileState.java @@ -45,6 +45,7 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; +@Deprecated class ProfileState { private DeviceProfile deviceProfile; diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/SnapshotUpdate.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/SnapshotUpdate.java index d41a42efa3..08af665038 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/SnapshotUpdate.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/SnapshotUpdate.java @@ -21,6 +21,7 @@ import org.thingsboard.server.common.data.device.profile.AlarmConditionKeyType; import java.util.Set; +@Deprecated class SnapshotUpdate { @Getter diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/TbDeviceProfileNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/TbDeviceProfileNode.java index 9a1d04eefa..c40ac70618 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/TbDeviceProfileNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/TbDeviceProfileNode.java @@ -51,16 +51,18 @@ import java.util.concurrent.TimeUnit; @Slf4j @RuleNode( type = ComponentType.ACTION, - name = "device profile", + name = "device profile (deprecated)", customRelations = true, relationTypes = {"Alarm Created", "Alarm Updated", "Alarm Severity Updated", "Alarm Cleared", "Success", "Failure"}, version = 1, configClazz = TbDeviceProfileNodeConfiguration.class, - nodeDescription = "Process device messages based on device profile settings", + nodeDescription = "Process device messages based on device profile settings (deprecated)", nodeDetails = "Create and clear alarms based on alarm rules defined in device profile. The output relation type is either " + - "'Alarm Created', 'Alarm Updated', 'Alarm Severity Updated' and 'Alarm Cleared' or simply 'Success' if no alarms were affected.", - configDirective = "tbActionNodeDeviceProfileConfig" + "'Alarm Created', 'Alarm Updated', 'Alarm Severity Updated' and 'Alarm Cleared' or simply 'Success' if no alarms were affected.", + configDirective = "tbActionNodeDeviceProfileConfig", + docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/action/device-profile/" ) +@Deprecated public class TbDeviceProfileNode implements TbNode { private TbDeviceProfileNodeConfiguration config; @@ -137,7 +139,7 @@ public class TbDeviceProfileNode implements TbNode { if (deviceState != null) { deviceState.process(ctx, msg); } else { - log.info("Device was not found! Most probably device [" + deviceId + "] has been removed from the database. Acknowledging msg."); + log.info("Device was not found! Most probably device [{}] has been removed from the database. Acknowledging msg.", deviceId); ctx.ack(msg); } } @@ -160,7 +162,7 @@ public class TbDeviceProfileNode implements TbNode { deviceStates.clear(); } - protected DeviceState getOrCreateDeviceState(TbContext ctx, DeviceId deviceId, RuleNodeState rns, boolean printNewlyAddedDeviceStates) { + private DeviceState getOrCreateDeviceState(TbContext ctx, DeviceId deviceId, RuleNodeState rns, boolean printNewlyAddedDeviceStates) { DeviceState deviceState = deviceStates.get(deviceId); if (deviceState == null) { DeviceProfile deviceProfile = cache.get(ctx.getTenantId(), deviceId); diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/TbDeviceProfileNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/TbDeviceProfileNodeConfiguration.java index a3180893d1..0605eed516 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/TbDeviceProfileNodeConfiguration.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/TbDeviceProfileNodeConfiguration.java @@ -21,6 +21,7 @@ import org.thingsboard.rule.engine.api.NodeConfiguration; @Data @JsonIgnoreProperties(ignoreUnknown = true) +@Deprecated public class TbDeviceProfileNodeConfiguration implements NodeConfiguration { private boolean persistAlarmRulesState; diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/state/PersistedAlarmRuleState.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/state/PersistedAlarmRuleState.java index 30aa4c443b..035d564f65 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/state/PersistedAlarmRuleState.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/state/PersistedAlarmRuleState.java @@ -22,6 +22,7 @@ import lombok.NoArgsConstructor; @Data @NoArgsConstructor @AllArgsConstructor +@Deprecated public class PersistedAlarmRuleState { private long lastEventTs; diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/state/PersistedAlarmState.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/state/PersistedAlarmState.java index dba8ba17a8..16d11485df 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/state/PersistedAlarmState.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/state/PersistedAlarmState.java @@ -21,6 +21,7 @@ import org.thingsboard.server.common.data.alarm.AlarmSeverity; import java.util.Map; @Data +@Deprecated public class PersistedAlarmState { private Map createRuleStates; diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/state/PersistedDeviceState.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/state/PersistedDeviceState.java index 46f8a3b2ca..d4307e3955 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/state/PersistedDeviceState.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/state/PersistedDeviceState.java @@ -20,6 +20,7 @@ import lombok.Data; import java.util.Map; @Data +@Deprecated public class PersistedDeviceState { Map alarmStates; diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rabbitmq/TbRabbitMqNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rabbitmq/TbRabbitMqNode.java index a067bb43bd..6ae0f48658 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rabbitmq/TbRabbitMqNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rabbitmq/TbRabbitMqNode.java @@ -46,7 +46,8 @@ import static org.thingsboard.common.util.DonAsynchron.withCallback; nodeDescription = "Publish messages to the RabbitMQ", nodeDetails = "Will publish message payload to RabbitMQ queue.", configDirective = "tbExternalNodeRabbitMqConfig", - iconUrl = "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbDpzcGFjZT0icHJlc2VydmUiIHZlcnNpb249IjEuMSIgeT0iMHB4IiB4PSIwcHgiIHZpZXdCb3g9IjAgMCAxMDAwIDEwMDAiPjxwYXRoIHN0cm9rZS13aWR0aD0iLjg0OTU2IiBkPSJtODYwLjQ3IDQxNi4zMmgtMjYyLjAxYy0xMi45MTMgMC0yMy42MTgtMTAuNzA0LTIzLjYxOC0yMy42MTh2LTI3Mi43MWMwLTIwLjMwNS0xNi4yMjctMzYuMjc2LTM2LjI3Ni0zNi4yNzZoLTkzLjc5MmMtMjAuMzA1IDAtMzYuMjc2IDE2LjIyNy0zNi4yNzYgMzYuMjc2djI3MC44NGMtMC4yNTQ4NyAxNC4xMDMtMTEuNDY5IDI1LjU3Mi0yNS43NDIgMjUuNTcybC04NS42MzYgMC42Nzk2NWMtMTQuMTAzIDAtMjUuNTcyLTExLjQ2OS0yNS41NzItMjUuNTcybDAuNjc5NjUtMjcxLjUyYzAtMjAuMzA1LTE2LjIyNy0zNi4yNzYtMzYuMjc2LTM2LjI3NmgtOTMuNTM3Yy0yMC4zMDUgMC0zNi4yNzYgMTYuMjI3LTM2LjI3NiAzNi4yNzZ2NzYzLjg0YzAgMTguMDk2IDE0Ljc4MiAzMi40NTMgMzIuNDUzIDMyLjQ1M2g3MjIuODFjMTguMDk2IDAgMzIuNDUzLTE0Ljc4MiAzMi40NTMtMzIuNDUzdi00MzUuMzFjLTEuMTg5NC0xOC4xODEtMTUuMjkyLTMyLjE5OC0zMy4zODgtMzIuMTk4em0tMTIyLjY4IDI4Ny4wN2MwIDIzLjYxOC0xOC44NiA0Mi40NzgtNDIuNDc4IDQyLjQ3OGgtNzMuOTk3Yy0yMy42MTggMC00Mi40NzgtMTguODYtNDIuNDc4LTQyLjQ3OHYtNzQuMjUyYzAtMjMuNjE4IDE4Ljg2LTQyLjQ3OCA0Mi40NzgtNDIuNDc4aDczLjk5N2MyMy42MTggMCA0Mi40NzggMTguODYgNDIuNDc4IDQyLjQ3OHoiLz48L3N2Zz4=" + iconUrl = "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbDpzcGFjZT0icHJlc2VydmUiIHZlcnNpb249IjEuMSIgeT0iMHB4IiB4PSIwcHgiIHZpZXdCb3g9IjAgMCAxMDAwIDEwMDAiPjxwYXRoIHN0cm9rZS13aWR0aD0iLjg0OTU2IiBkPSJtODYwLjQ3IDQxNi4zMmgtMjYyLjAxYy0xMi45MTMgMC0yMy42MTgtMTAuNzA0LTIzLjYxOC0yMy42MTh2LTI3Mi43MWMwLTIwLjMwNS0xNi4yMjctMzYuMjc2LTM2LjI3Ni0zNi4yNzZoLTkzLjc5MmMtMjAuMzA1IDAtMzYuMjc2IDE2LjIyNy0zNi4yNzYgMzYuMjc2djI3MC44NGMtMC4yNTQ4NyAxNC4xMDMtMTEuNDY5IDI1LjU3Mi0yNS43NDIgMjUuNTcybC04NS42MzYgMC42Nzk2NWMtMTQuMTAzIDAtMjUuNTcyLTExLjQ2OS0yNS41NzItMjUuNTcybDAuNjc5NjUtMjcxLjUyYzAtMjAuMzA1LTE2LjIyNy0zNi4yNzYtMzYuMjc2LTM2LjI3NmgtOTMuNTM3Yy0yMC4zMDUgMC0zNi4yNzYgMTYuMjI3LTM2LjI3NiAzNi4yNzZ2NzYzLjg0YzAgMTguMDk2IDE0Ljc4MiAzMi40NTMgMzIuNDUzIDMyLjQ1M2g3MjIuODFjMTguMDk2IDAgMzIuNDUzLTE0Ljc4MiAzMi40NTMtMzIuNDUzdi00MzUuMzFjLTEuMTg5NC0xOC4xODEtMTUuMjkyLTMyLjE5OC0zMy4zODgtMzIuMTk4em0tMTIyLjY4IDI4Ny4wN2MwIDIzLjYxOC0xOC44NiA0Mi40NzgtNDIuNDc4IDQyLjQ3OGgtNzMuOTk3Yy0yMy42MTggMC00Mi40NzgtMTguODYtNDIuNDc4LTQyLjQ3OHYtNzQuMjUyYzAtMjMuNjE4IDE4Ljg2LTQyLjQ3OCA0Mi40NzgtNDIuNDc4aDczLjk5N2MyMy42MTggMCA0Mi40NzggMTguODYgNDIuNDc4IDQyLjQ3OHoiLz48L3N2Zz4=", + docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/external/rabbitmq/" ) public class TbRabbitMqNode extends TbAbstractExternalNode { @@ -99,10 +100,10 @@ public class TbRabbitMqNode extends TbAbstractExternalNode { } private ListenableFuture publishMessageAsync(TbContext ctx, TbMsg msg) { - return ctx.getExternalCallExecutor().executeAsync(() -> publishMessage(ctx, msg)); + return ctx.getExternalCallExecutor().executeAsync(() -> publishMessage(msg)); } - private TbMsg publishMessage(TbContext ctx, TbMsg msg) throws Exception { + private TbMsg publishMessage(TbMsg msg) throws Exception { String exchangeName = ""; if (!StringUtils.isEmpty(this.config.getExchangeNamePattern())) { exchangeName = TbNodeUtils.processPattern(this.config.getExchangeNamePattern(), msg); @@ -143,23 +144,15 @@ public class TbRabbitMqNode extends TbAbstractExternalNode { } static AMQP.BasicProperties convert(String name) throws TbNodeException { - switch (name) { - case "BASIC": - return MessageProperties.BASIC; - case "TEXT_PLAIN": - return MessageProperties.TEXT_PLAIN; - case "MINIMAL_BASIC": - return MessageProperties.MINIMAL_BASIC; - case "MINIMAL_PERSISTENT_BASIC": - return MessageProperties.MINIMAL_PERSISTENT_BASIC; - case "PERSISTENT_BASIC": - return MessageProperties.PERSISTENT_BASIC; - case "PERSISTENT_TEXT_PLAIN": - return MessageProperties.PERSISTENT_TEXT_PLAIN; - default: - throw new TbNodeException("Undefined message properties type '" + name + - "'! Only " + supportedPropertiesStr + " message properties types are supported!"); - } + return switch (name) { + case "BASIC" -> MessageProperties.BASIC; + case "TEXT_PLAIN" -> MessageProperties.TEXT_PLAIN; + case "MINIMAL_BASIC" -> MessageProperties.MINIMAL_BASIC; + case "MINIMAL_PERSISTENT_BASIC" -> MessageProperties.MINIMAL_PERSISTENT_BASIC; + case "PERSISTENT_BASIC" -> MessageProperties.PERSISTENT_BASIC; + case "PERSISTENT_TEXT_PLAIN" -> MessageProperties.PERSISTENT_TEXT_PLAIN; + default -> throw new TbNodeException("Undefined message properties type '" + name + "'! Only " + supportedPropertiesStr + " message properties types are supported!"); + }; } -} +} diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbRestApiCallNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbRestApiCallNode.java index 8341547ef0..a2290a4eda 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbRestApiCallNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbRestApiCallNode.java @@ -17,7 +17,6 @@ package org.thingsboard.rule.engine.rest; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ObjectNode; -import lombok.extern.slf4j.Slf4j; import org.thingsboard.rule.engine.api.RuleNode; import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNodeConfiguration; @@ -30,7 +29,6 @@ import org.thingsboard.server.common.msg.TbMsg; import java.util.List; -@Slf4j @RuleNode( type = ComponentType.EXTERNAL, name = "rest api call", @@ -46,7 +44,8 @@ import java.util.List; "
    Note- if you use system proxy properties, the next system proxy properties should be added: \"http.proxyHost\" and \"http.proxyPort\" or \"https.proxyHost\" and \"https.proxyPort\" or \"socksProxyHost\" and \"socksProxyPort\"," + "and if your proxy with auth, the next ones should be added: \"tb.proxy.user\" and \"tb.proxy.password\" to the thingsboard.conf file.", configDirective = "tbExternalNodeRestApiCallConfig", - iconUrl = "data:image/svg+xml;base64,PHN2ZyBzdHlsZT0iZW5hYmxlLWJhY2tncm91bmQ6bmV3IDAgMCA1MTIgNTEyIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbDpzcGFjZT0icHJlc2VydmUiIHZpZXdCb3g9IjAgMCA1MTIgNTEyIiB2ZXJzaW9uPSIxLjEiIHk9IjBweCIgeD0iMHB4Ij48ZyB0cmFuc2Zvcm09Im1hdHJpeCguOTQ5NzUgMCAwIC45NDk3NSAxNy4xMiAyNi40OTIpIj48cGF0aCBkPSJtMTY5LjExIDEwOC41NGMtOS45MDY2IDAuMDczNC0xOS4wMTQgNi41NzI0LTIyLjAxNCAxNi40NjlsLTY5Ljk5MyAyMzEuMDhjLTMuNjkwNCAxMi4xODEgMy4yODkyIDI1LjIyIDE1LjQ2OSAyOC45MSAyLjIyNTkgMC42NzQ4MSA0LjQ5NjkgMSA2LjcyODUgMSA5Ljk3MjEgMCAxOS4xNjUtNi41MTUzIDIyLjE4Mi0xNi40NjdhNi41MjI0IDYuNTIyNCAwIDAgMCAwLjAwMiAtMC4wMDJsNjkuOTktMjMxLjA3YTYuNTIyNCA2LjUyMjQgMCAwIDAgMCAtMC4wMDJjMy42ODU1LTEyLjE4MS0zLjI4Ny0yNS4yMjUtMTUuNDcxLTI4LjkxMi0yLjI4MjUtMC42OTE0NS00LjYxMTYtMS4wMTY5LTYuODk4NC0xem04NC45ODggMGMtOS45MDQ4IDAuMDczNC0xOS4wMTggNi41Njc1LTIyLjAxOCAxNi40NjlsLTY5Ljk4NiAyMzEuMDhjLTMuNjg5OCAxMi4xNzkgMy4yODUzIDI1LjIxNyAxNS40NjUgMjguOTA4IDIuMjI5NyAwLjY3NjQ3IDQuNTAwOCAxLjAwMiA2LjczMjQgMS4wMDIgOS45NzIxIDAgMTkuMTY1LTYuNTE1MyAyMi4xODItMTYuNDY3YTYuNTIyNCA2LjUyMjQgMCAwIDAgMC4wMDIgLTAuMDAybDY5Ljk4OC0yMzEuMDdjMy42OTA4LTEyLjE4MS0zLjI4NTItMjUuMjIzLTE1LjQ2Ny0yOC45MTItMi4yODE0LTAuNjkyMzEtNC42MTA4LTEuMDE4OS02Ljg5ODQtMS4wMDJ6bS0yMTcuMjkgNDIuMjNjLTEyLjcyOS0wLjAwMDg3LTIzLjE4OCAxMC40NTYtMjMuMTg4IDIzLjE4NiAwLjAwMSAxMi43MjggMTAuNDU5IDIzLjE4NiAyMy4xODggMjMuMTg2IDEyLjcyNy0wLjAwMSAyMy4xODMtMTAuNDU5IDIzLjE4NC0yMy4xODYgMC4wMDA4NzYtMTIuNzI4LTEwLjQ1Ni0yMy4xODUtMjMuMTg0LTIzLjE4NnptMCAxNDYuNjRjLTEyLjcyNy0wLjAwMDg3LTIzLjE4NiAxMC40NTUtMjMuMTg4IDIzLjE4NC0wLjAwMDg3MyAxMi43MjkgMTAuNDU4IDIzLjE4OCAyMy4xODggMjMuMTg4IDEyLjcyOC0wLjAwMSAyMy4xODQtMTAuNDYgMjMuMTg0LTIzLjE4OC0wLjAwMS0xMi43MjYtMTAuNDU3LTIzLjE4My0yMy4xODQtMjMuMTg0em0yNzAuNzkgNDIuMjExYy0xMi43MjcgMC0yMy4xODQgMTAuNDU3LTIzLjE4NCAyMy4xODRzMTAuNDU1IDIzLjE4OCAyMy4xODQgMjMuMTg4aDE1NC45OGMxMi43MjkgMCAyMy4xODYtMTAuNDYgMjMuMTg2LTIzLjE4OCAwLjAwMS0xMi43MjgtMTAuNDU4LTIzLjE4NC0yMy4xODYtMjMuMTg0eiIgdHJhbnNmb3JtPSJtYXRyaXgoMS4wMzc2IDAgMCAxLjAzNzYgLTcuNTY3NiAtMTQuOTI1KSIgc3Ryb2tlLXdpZHRoPSIxLjI2OTMiLz48L2c+PC9zdmc+" + iconUrl = "data:image/svg+xml;base64,PHN2ZyBzdHlsZT0iZW5hYmxlLWJhY2tncm91bmQ6bmV3IDAgMCA1MTIgNTEyIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbDpzcGFjZT0icHJlc2VydmUiIHZpZXdCb3g9IjAgMCA1MTIgNTEyIiB2ZXJzaW9uPSIxLjEiIHk9IjBweCIgeD0iMHB4Ij48ZyB0cmFuc2Zvcm09Im1hdHJpeCguOTQ5NzUgMCAwIC45NDk3NSAxNy4xMiAyNi40OTIpIj48cGF0aCBkPSJtMTY5LjExIDEwOC41NGMtOS45MDY2IDAuMDczNC0xOS4wMTQgNi41NzI0LTIyLjAxNCAxNi40NjlsLTY5Ljk5MyAyMzEuMDhjLTMuNjkwNCAxMi4xODEgMy4yODkyIDI1LjIyIDE1LjQ2OSAyOC45MSAyLjIyNTkgMC42NzQ4MSA0LjQ5NjkgMSA2LjcyODUgMSA5Ljk3MjEgMCAxOS4xNjUtNi41MTUzIDIyLjE4Mi0xNi40NjdhNi41MjI0IDYuNTIyNCAwIDAgMCAwLjAwMiAtMC4wMDJsNjkuOTktMjMxLjA3YTYuNTIyNCA2LjUyMjQgMCAwIDAgMCAtMC4wMDJjMy42ODU1LTEyLjE4MS0zLjI4Ny0yNS4yMjUtMTUuNDcxLTI4LjkxMi0yLjI4MjUtMC42OTE0NS00LjYxMTYtMS4wMTY5LTYuODk4NC0xem04NC45ODggMGMtOS45MDQ4IDAuMDczNC0xOS4wMTggNi41Njc1LTIyLjAxOCAxNi40NjlsLTY5Ljk4NiAyMzEuMDhjLTMuNjg5OCAxMi4xNzkgMy4yODUzIDI1LjIxNyAxNS40NjUgMjguOTA4IDIuMjI5NyAwLjY3NjQ3IDQuNTAwOCAxLjAwMiA2LjczMjQgMS4wMDIgOS45NzIxIDAgMTkuMTY1LTYuNTE1MyAyMi4xODItMTYuNDY3YTYuNTIyNCA2LjUyMjQgMCAwIDAgMC4wMDIgLTAuMDAybDY5Ljk4OC0yMzEuMDdjMy42OTA4LTEyLjE4MS0zLjI4NTItMjUuMjIzLTE1LjQ2Ny0yOC45MTItMi4yODE0LTAuNjkyMzEtNC42MTA4LTEuMDE4OS02Ljg5ODQtMS4wMDJ6bS0yMTcuMjkgNDIuMjNjLTEyLjcyOS0wLjAwMDg3LTIzLjE4OCAxMC40NTYtMjMuMTg4IDIzLjE4NiAwLjAwMSAxMi43MjggMTAuNDU5IDIzLjE4NiAyMy4xODggMjMuMTg2IDEyLjcyNy0wLjAwMSAyMy4xODMtMTAuNDU5IDIzLjE4NC0yMy4xODYgMC4wMDA4NzYtMTIuNzI4LTEwLjQ1Ni0yMy4xODUtMjMuMTg0LTIzLjE4NnptMCAxNDYuNjRjLTEyLjcyNy0wLjAwMDg3LTIzLjE4NiAxMC40NTUtMjMuMTg4IDIzLjE4NC0wLjAwMDg3MyAxMi43MjkgMTAuNDU4IDIzLjE4OCAyMy4xODggMjMuMTg4IDEyLjcyOC0wLjAwMSAyMy4xODQtMTAuNDYgMjMuMTg0LTIzLjE4OC0wLjAwMS0xMi43MjYtMTAuNDU3LTIzLjE4My0yMy4xODQtMjMuMTg0em0yNzAuNzkgNDIuMjExYy0xMi43MjcgMC0yMy4xODQgMTAuNDU3LTIzLjE4NCAyMy4xODRzMTAuNDU1IDIzLjE4OCAyMy4xODQgMjMuMTg4aDE1NC45OGMxMi43MjkgMCAyMy4xODYtMTAuNDYgMjMuMTg2LTIzLjE4OCAwLjAwMS0xMi43MjgtMTAuNDU4LTIzLjE4NC0yMy4xODYtMjMuMTg0eiIgdHJhbnNmb3JtPSJtYXRyaXgoMS4wMzc2IDAgMCAxLjAzNzYgLTcuNTY3NiAtMTQuOTI1KSIgc3Ryb2tlLXdpZHRoPSIxLjI2OTMiLz48L2c+PC9zdmc+", + docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/external/rest-api-call/" ) public class TbRestApiCallNode extends TbAbstractExternalNode { diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbSendRestApiCallReplyNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbSendRestApiCallReplyNode.java index 5be2768a0f..8148e5ea58 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbSendRestApiCallReplyNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbSendRestApiCallReplyNode.java @@ -15,7 +15,6 @@ */ package org.thingsboard.rule.engine.rest; -import lombok.extern.slf4j.Slf4j; import org.thingsboard.rule.engine.api.RuleNode; import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNode; @@ -28,7 +27,6 @@ import org.thingsboard.server.common.msg.TbMsg; import java.util.UUID; -@Slf4j @RuleNode( type = ComponentType.ACTION, name = "rest call reply", @@ -36,7 +34,8 @@ import java.util.UUID; nodeDescription = "Sends reply to REST API call to rule engine", nodeDetails = "Expects messages with any message type. Forwards incoming message as a reply to REST API call sent to rule engine.", configDirective = "tbActionNodeSendRestApiCallReplyConfig", - icon = "call_merge" + icon = "call_merge", + docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/action/rest-call-reply/" ) public class TbSendRestApiCallReplyNode implements TbNode { @@ -44,7 +43,7 @@ public class TbSendRestApiCallReplyNode implements TbNode { @Override public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException { - this.config = TbNodeUtils.convert(configuration, TbSendRestApiCallReplyNodeConfiguration.class); + config = TbNodeUtils.convert(configuration, TbSendRestApiCallReplyNodeConfiguration.class); } @Override @@ -62,4 +61,5 @@ public class TbSendRestApiCallReplyNode implements TbNode { ctx.tellSuccess(msg); } } + } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rpc/TbSendRPCReplyNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rpc/TbSendRPCReplyNode.java index a8e7142cfb..0f136772d4 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rpc/TbSendRPCReplyNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rpc/TbSendRPCReplyNode.java @@ -19,7 +19,7 @@ import com.fasterxml.jackson.databind.node.ObjectNode; import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; -import lombok.extern.slf4j.Slf4j; +import org.checkerframework.checker.nullness.qual.NonNull; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.rule.engine.api.RuleNode; import org.thingsboard.rule.engine.api.TbContext; @@ -41,7 +41,6 @@ import org.thingsboard.server.common.msg.TbMsg; import java.util.UUID; -@Slf4j @RuleNode( type = ComponentType.ACTION, name = "rpc call reply", @@ -49,7 +48,8 @@ import java.util.UUID; nodeDescription = "Sends reply to RPC call from device", nodeDetails = "Expects messages with any message type. Will forward message body to the device.", configDirective = "tbActionNodeRpcReplyConfig", - icon = "call_merge" + icon = "call_merge", + docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/action/rpc-call-reply/" ) public class TbSendRPCReplyNode implements TbNode { @@ -57,7 +57,7 @@ public class TbSendRPCReplyNode implements TbNode { @Override public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException { - this.config = TbNodeUtils.convert(configuration, TbSendRpcReplyNodeConfiguration.class); + config = TbNodeUtils.convert(configuration, TbSendRpcReplyNodeConfiguration.class); } @Override @@ -103,7 +103,7 @@ public class TbSendRPCReplyNode implements TbNode { body.put("requestId", requestIdStr); body.put("response", msg.getData()); EdgeEvent edgeEvent = EdgeUtils.constructEdgeEvent(ctx.getTenantId(), edgeId, EdgeEventType.DEVICE, - EdgeEventActionType.RPC_CALL, deviceId, JacksonUtil.valueToTree(body)); + EdgeEventActionType.RPC_CALL, deviceId, JacksonUtil.valueToTree(body)); ListenableFuture future = ctx.getEdgeEventService().saveAsync(edgeEvent); Futures.addCallback(future, new FutureCallback<>() { @Override @@ -112,9 +112,10 @@ public class TbSendRPCReplyNode implements TbNode { } @Override - public void onFailure(Throwable t) { + public void onFailure(@NonNull Throwable t) { ctx.tellFailure(msg, t); } }, ctx.getDbCallbackExecutor()); } + } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rpc/TbSendRPCRequestNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rpc/TbSendRPCRequestNode.java index 51f5166653..ead79a3793 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rpc/TbSendRPCRequestNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rpc/TbSendRPCRequestNode.java @@ -20,7 +20,6 @@ import com.google.gson.Gson; import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.JsonParser; -import lombok.extern.slf4j.Slf4j; import org.thingsboard.rule.engine.api.RuleEngineDeviceRpcRequest; import org.thingsboard.rule.engine.api.RuleNode; import org.thingsboard.rule.engine.api.TbContext; @@ -41,7 +40,6 @@ import java.util.Random; import java.util.UUID; import java.util.concurrent.TimeUnit; -@Slf4j @RuleNode( type = ComponentType.ACTION, name = "rpc call request", @@ -50,17 +48,18 @@ import java.util.concurrent.TimeUnit; nodeDetails = "Expects messages with \"method\" and \"params\". Will forward response from device to next nodes." + "If the RPC call request is originated by REST API call from user, will forward the response to user immediately.", configDirective = "tbActionNodeRpcRequestConfig", - icon = "call_made" + icon = "call_made", + docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/action/rpc-call-request/" ) public class TbSendRPCRequestNode implements TbNode { - private Random random = new Random(); - private Gson gson = new Gson(); + private final Random random = new Random(); + private final Gson gson = new Gson(); private TbSendRpcRequestNodeConfiguration config; @Override public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException { - this.config = TbNodeUtils.convert(configuration, TbSendRpcRequestNodeConfiguration.class); + config = TbNodeUtils.convert(configuration, TbSendRpcRequestNodeConfiguration.class); } @Override diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/sms/TbSendSmsNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/sms/TbSendSmsNode.java index d5583037ac..3033c9d382 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/sms/TbSendSmsNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/sms/TbSendSmsNode.java @@ -15,7 +15,6 @@ */ package org.thingsboard.rule.engine.sms; -import lombok.extern.slf4j.Slf4j; import org.thingsboard.rule.engine.api.RuleNode; import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNodeConfiguration; @@ -28,7 +27,6 @@ import org.thingsboard.server.common.msg.TbMsg; import static org.thingsboard.common.util.DonAsynchron.withCallback; -@Slf4j @RuleNode( type = ComponentType.EXTERNAL, name = "send sms", @@ -36,7 +34,8 @@ import static org.thingsboard.common.util.DonAsynchron.withCallback; nodeDescription = "Sends SMS message via SMS provider.", nodeDetails = "Will send SMS message by populating target phone numbers and sms message fields using values derived from message metadata.", configDirective = "tbExternalNodeSendSmsConfig", - icon = "sms" + icon = "sms", + docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/external/send-sms/" ) public class TbSendSmsNode extends TbAbstractExternalNode { diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbCalculatedFieldsNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbCalculatedFieldsNode.java index e703e9dd25..9d91802f8b 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbCalculatedFieldsNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbCalculatedFieldsNode.java @@ -16,16 +16,13 @@ package org.thingsboard.rule.engine.telemetry; import com.google.gson.JsonParser; -import lombok.extern.slf4j.Slf4j; import org.thingsboard.rule.engine.api.AttributesSaveRequest; import org.thingsboard.rule.engine.api.EmptyNodeConfiguration; import org.thingsboard.rule.engine.api.RuleNode; import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNode; import org.thingsboard.rule.engine.api.TbNodeConfiguration; -import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.rule.engine.api.TimeseriesSaveRequest; -import org.thingsboard.rule.engine.api.util.TbNodeUtils; import org.thingsboard.server.common.adaptor.JsonConverter; import org.thingsboard.server.common.data.AttributeScope; import org.thingsboard.server.common.data.kv.AttributeKvEntry; @@ -41,27 +38,23 @@ import java.util.Map; import static org.thingsboard.server.common.data.DataConstants.SCOPE; -@Slf4j @RuleNode( type = ComponentType.ACTION, - name = "calculated fields", + name = "calculated fields and alarm rules", configClazz = EmptyNodeConfiguration.class, - nodeDescription = "Pushes incoming messages to calculated fields service", - nodeDetails = "Node enables the processing of calculated fields without persisting incoming messages to the database. " + - "By default, the processing of calculated fields is triggered by the save attributes and save time series nodes. " + + nodeDescription = "Pushes incoming messages to calculated fields and alarm rules services", + nodeDetails = "Node enables the processing of calculated fields and alarm rules without persisting incoming messages to the database. " + + "By default, the processing of calculated fields and alarm rules is triggered by the save attributes and save time series nodes. " + "This rule node accepts the same messages as these nodes but allows you to trigger the processing of calculated " + - "fields independently, ensuring that derived data can be computed and utilized in real time without storing the original message in the database.", + "fields or alarm rules independently, ensuring that derived data can be computed and utilized in real time without storing the original message in the database.", configDirective = "tbNodeEmptyConfig", - icon = "published_with_changes" + icon = "published_with_changes", + docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/action/calculated-fields/" ) public class TbCalculatedFieldsNode implements TbNode { - private EmptyNodeConfiguration config; - @Override - public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException { - this.config = TbNodeUtils.convert(configuration, EmptyNodeConfiguration.class); - } + public void init(TbContext ctx, TbNodeConfiguration configuration) {} @Override public void onMsg(TbContext ctx, TbMsg msg) { diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgAttributesNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgAttributesNode.java index 280d8de824..533f7d13dd 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgAttributesNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgAttributesNode.java @@ -21,7 +21,6 @@ import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.MoreExecutors; import com.google.gson.JsonParser; -import lombok.extern.slf4j.Slf4j; import org.thingsboard.common.util.DonAsynchron; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.rule.engine.api.AttributesSaveRequest; @@ -56,7 +55,6 @@ import static org.thingsboard.server.common.data.DataConstants.NOTIFY_DEVICE_MET import static org.thingsboard.server.common.data.DataConstants.SCOPE; import static org.thingsboard.server.common.data.msg.TbMsgType.POST_ATTRIBUTES_REQUEST; -@Slf4j @RuleNode( type = ComponentType.ACTION, name = "save attributes", @@ -107,7 +105,8 @@ import static org.thingsboard.server.common.data.msg.TbMsgType.POST_ATTRIBUTES_R Output connections: Success, Failure. """, configDirective = "tbActionNodeAttributesConfig", - icon = "file_upload" + icon = "file_upload", + docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/action/save-attributes/" ) public class TbMsgAttributesNode implements TbNode { diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgDeleteAttributesNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgDeleteAttributesNode.java index 32d700dc1d..331c28b686 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgDeleteAttributesNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgDeleteAttributesNode.java @@ -15,7 +15,6 @@ */ package org.thingsboard.rule.engine.telemetry; -import lombok.extern.slf4j.Slf4j; import org.thingsboard.rule.engine.api.AttributesDeleteRequest; import org.thingsboard.rule.engine.api.RuleNode; import org.thingsboard.rule.engine.api.TbContext; @@ -35,7 +34,6 @@ import java.util.stream.Collectors; import static org.thingsboard.server.common.data.DataConstants.NOTIFY_DEVICE_METADATA_KEY; import static org.thingsboard.server.common.data.DataConstants.SCOPE; -@Slf4j @RuleNode( type = ComponentType.ACTION, name = "delete attributes", @@ -46,7 +44,8 @@ import static org.thingsboard.server.common.data.DataConstants.SCOPE; " rule node will send the \"Attributes Deleted\" event to the root chain of the message originator and " + " send the incoming message via Success chain, otherwise, Failure chain is used.", configDirective = "tbActionNodeDeleteAttributesConfig", - icon = "remove_circle" + icon = "remove_circle", + docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/action/delete-attributes/" ) public class TbMsgDeleteAttributesNode implements TbNode { @@ -55,8 +54,8 @@ public class TbMsgDeleteAttributesNode implements TbNode { @Override public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException { - this.config = TbNodeUtils.convert(configuration, TbMsgDeleteAttributesNodeConfiguration.class); - this.keys = config.getKeys(); + config = TbNodeUtils.convert(configuration, TbMsgDeleteAttributesNodeConfiguration.class); + keys = config.getKeys(); } @Override diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgTimeseriesNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgTimeseriesNode.java index 8017be9a04..13dab98c54 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgTimeseriesNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgTimeseriesNode.java @@ -18,7 +18,6 @@ package org.thingsboard.rule.engine.telemetry; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ObjectNode; import com.google.gson.JsonParser; -import lombok.extern.slf4j.Slf4j; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.rule.engine.api.RuleNode; import org.thingsboard.rule.engine.api.TbContext; @@ -52,7 +51,6 @@ import static org.thingsboard.rule.engine.telemetry.settings.TimeseriesProcessin import static org.thingsboard.rule.engine.telemetry.settings.TimeseriesProcessingSettings.WebSocketsOnly; import static org.thingsboard.server.common.data.msg.TbMsgType.POST_TELEMETRY_REQUEST; -@Slf4j @RuleNode( type = ComponentType.ACTION, name = "save time series", @@ -103,7 +101,8 @@ import static org.thingsboard.server.common.data.msg.TbMsgType.POST_TELEMETRY_RE """, configDirective = "tbActionNodeTimeseriesConfig", icon = "file_upload", - version = 1 + version = 1, + docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/action/save-timeseries/" ) public class TbMsgTimeseriesNode implements TbNode { diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNode.java index eff08d09e7..a97cc22d5b 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNode.java @@ -25,7 +25,6 @@ import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.rule.engine.api.util.TbNodeUtils; import org.thingsboard.rule.engine.util.EntitiesAlarmOriginatorIdAsyncLoader; import org.thingsboard.rule.engine.util.EntitiesByNameAndTypeLoader; -import org.thingsboard.rule.engine.util.EntitiesCustomerIdAsyncLoader; import org.thingsboard.rule.engine.util.EntitiesRelatedEntityIdAsyncLoader; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.StringUtils; @@ -36,6 +35,7 @@ import org.thingsboard.server.common.msg.TbMsg; import java.util.List; import java.util.NoSuchElementException; +import static com.google.common.util.concurrent.Futures.immediateFuture; import static org.thingsboard.rule.engine.transform.OriginatorSource.ENTITY; import static org.thingsboard.rule.engine.transform.OriginatorSource.RELATED; @@ -55,7 +55,8 @@ import static org.thingsboard.rule.engine.transform.OriginatorSource.RELATED; "'Device', 'Asset', 'Entity View', 'Edge' or 'User'." + "Output connections: Success, Failure.", configDirective = "tbTransformationNodeChangeOriginatorConfig", - icon = "find_replace" + icon = "find_replace", + docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/transformation/change-originator/" ) public class TbChangeOriginatorNode extends TbAbstractTransformNode { @@ -73,16 +74,20 @@ public class TbChangeOriginatorNode extends TbAbstractTransformNode getNewOriginator(TbContext ctx, TbMsg msg) { switch (config.getOriginatorSource()) { case CUSTOMER: - return EntitiesCustomerIdAsyncLoader.findEntityIdAsync(ctx, msg.getOriginator()); + if (msg.getOriginator().getEntityType() == EntityType.CUSTOMER) { + return immediateFuture(msg.getOriginator()); + } + return ctx.getEntityService().fetchEntityCustomerIdAsync(ctx.getTenantId(), msg.getOriginator()) + .transform(customerIdOpt -> customerIdOpt.orElse(null), ctx.getDbCallbackExecutor()); case TENANT: - return Futures.immediateFuture(ctx.getTenantId()); + return immediateFuture(ctx.getTenantId()); case RELATED: return EntitiesRelatedEntityIdAsyncLoader.findEntityAsync(ctx, msg.getOriginator(), config.getRelationsQuery()); case ALARM_ORIGINATOR: @@ -92,7 +97,7 @@ public class TbChangeOriginatorNode extends TbAbstractTransformNode
    " + "Output connections: Success, Failure.", configDirective = "tbTransformationNodeCopyKeysConfig", - icon = "content_copy" + icon = "content_copy", + docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/transformation/copy-key-value-pairs/" ) public class TbCopyKeysNode extends TbAbstractTransformNodeWithTbMsgSource { - private TbCopyKeysNodeConfiguration config; private TbMsgSource copyFrom; private List compiledKeyPatterns; @Override public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException { - this.config = TbNodeUtils.convert(configuration, TbCopyKeysNodeConfiguration.class); - this.copyFrom = config.getCopyFrom(); + var config = TbNodeUtils.convert(configuration, TbCopyKeysNodeConfiguration.class); + copyFrom = config.getCopyFrom(); if (copyFrom == null) { throw new TbNodeException("CopyFrom can't be null! Allowed values: " + Arrays.toString(TbMsgSource.values())); } - this.compiledKeyPatterns = config.getKeys().stream().map(Pattern::compile).collect(Collectors.toList()); + compiledKeyPatterns = config.getKeys().stream().map(Pattern::compile).collect(Collectors.toList()); } @Override diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbDeleteKeysNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbDeleteKeysNode.java index 91f16f0b3b..c2eead99fe 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbDeleteKeysNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbDeleteKeysNode.java @@ -47,22 +47,22 @@ import java.util.stream.Collectors; "keys and/or regular expressions.

    " + "Output connections: Success, Failure.", configDirective = "tbTransformationNodeDeleteKeysConfig", - icon = "remove_circle" + icon = "remove_circle", + docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/transformation/delete-key-value-pairs/" ) public class TbDeleteKeysNode extends TbAbstractTransformNodeWithTbMsgSource { - private TbDeleteKeysNodeConfiguration config; private TbMsgSource deleteFrom; private List compiledKeyPatterns; @Override public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException { - this.config = TbNodeUtils.convert(configuration, TbDeleteKeysNodeConfiguration.class); - this.deleteFrom = config.getDeleteFrom(); + var config = TbNodeUtils.convert(configuration, TbDeleteKeysNodeConfiguration.class); + deleteFrom = config.getDeleteFrom(); if (deleteFrom == null) { throw new TbNodeException("DeleteFrom can't be null! Allowed values: " + Arrays.toString(TbMsgSource.values())); } - this.compiledKeyPatterns = config.getKeys().stream().map(Pattern::compile).collect(Collectors.toList()); + compiledKeyPatterns = config.getKeys().stream().map(Pattern::compile).collect(Collectors.toList()); } @Override @@ -76,7 +76,7 @@ public class TbDeleteKeysNode extends TbAbstractTransformNodeWithTbMsgSource { var mdKeysToDelete = metaDataMap.keySet() .stream() .filter(this::matches) - .collect(Collectors.toList()); + .toList(); mdKeysToDelete.forEach(metaDataMap::remove); metaDataCopy = new TbMsgMetaData(metaDataMap); hasNoChanges = mdKeysToDelete.isEmpty(); diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbJsonPathNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbJsonPathNode.java index eb6c0b7a68..717bdf70f2 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbJsonPathNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbJsonPathNode.java @@ -19,7 +19,6 @@ import com.jayway.jsonpath.Configuration; import com.jayway.jsonpath.JsonPath; import com.jayway.jsonpath.PathNotFoundException; import com.jayway.jsonpath.spi.json.JacksonJsonNodeJsonProvider; -import lombok.extern.slf4j.Slf4j; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.rule.engine.api.RuleNode; import org.thingsboard.rule.engine.api.TbContext; @@ -32,7 +31,6 @@ import org.thingsboard.server.common.msg.TbMsg; import java.util.concurrent.ExecutionException; -@Slf4j @RuleNode( type = ComponentType.TRANSFORMATION, name = "json path", @@ -41,32 +39,32 @@ import java.util.concurrent.ExecutionException; nodeDetails = "JSONPath expression specifies a path to an element or a set of elements in a JSON structure.

    " + "Output connections: Success, Failure.", icon = "functions", - configDirective = "tbTransformationNodeJsonPathConfig" + configDirective = "tbTransformationNodeJsonPathConfig", + docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/transformation/json-path/" ) public class TbJsonPathNode implements TbNode { - private TbJsonPathNodeConfiguration config; private Configuration configurationJsonPath; private JsonPath jsonPath; private String jsonPathValue; @Override public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException { - this.config = TbNodeUtils.convert(configuration, TbJsonPathNodeConfiguration.class); - this.jsonPathValue = config.getJsonPath(); - if (!TbJsonPathNodeConfiguration.DEFAULT_JSON_PATH.equals(this.jsonPathValue)) { - this.configurationJsonPath = Configuration.builder() + var config = TbNodeUtils.convert(configuration, TbJsonPathNodeConfiguration.class); + jsonPathValue = config.getJsonPath(); + if (!TbJsonPathNodeConfiguration.DEFAULT_JSON_PATH.equals(jsonPathValue)) { + configurationJsonPath = Configuration.builder() .jsonProvider(new JacksonJsonNodeJsonProvider()) .build(); - this.jsonPath = JsonPath.compile(config.getJsonPath()); + jsonPath = JsonPath.compile(config.getJsonPath()); } } @Override public void onMsg(TbContext ctx, TbMsg msg) throws ExecutionException, InterruptedException, TbNodeException { - if (!TbJsonPathNodeConfiguration.DEFAULT_JSON_PATH.equals(this.jsonPathValue)) { + if (!TbJsonPathNodeConfiguration.DEFAULT_JSON_PATH.equals(jsonPathValue)) { try { - Object jsonPathData = jsonPath.read(msg.getData(), this.configurationJsonPath); + Object jsonPathData = jsonPath.read(msg.getData(), configurationJsonPath); ctx.tellSuccess(msg.transform() .data(JacksonUtil.toString(jsonPathData)) .build()); @@ -77,4 +75,5 @@ public class TbJsonPathNode implements TbNode { ctx.tellSuccess(msg); } } + } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbRenameKeysNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbRenameKeysNode.java index f2865bfc0b..9966ff6bb4 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbRenameKeysNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbRenameKeysNode.java @@ -44,19 +44,19 @@ import java.util.concurrent.ExecutionException; "If key to rename doesn't exist in the specified source (message or message metadata) it will be ignored.

    " + "Output connections: Success, Failure.", configDirective = "tbTransformationNodeRenameKeysConfig", - icon = "find_replace" + icon = "find_replace", + docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/transformation/rename-keys/" ) public class TbRenameKeysNode extends TbAbstractTransformNodeWithTbMsgSource { - private TbRenameKeysNodeConfiguration config; private Map renameKeysMapping; private TbMsgSource renameIn; @Override public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException { - this.config = TbNodeUtils.convert(configuration, TbRenameKeysNodeConfiguration.class); - this.renameIn = config.getRenameIn(); - this.renameKeysMapping = config.getRenameKeysMapping(); + var config = TbNodeUtils.convert(configuration, TbRenameKeysNodeConfiguration.class); + renameIn = config.getRenameIn(); + renameKeysMapping = config.getRenameKeysMapping(); if (renameIn == null) { throw new TbNodeException("RenameIn can't be null! Allowed values: " + Arrays.toString(TbMsgSource.values())); } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbSplitArrayMsgNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbSplitArrayMsgNode.java index 0eac8b073d..a1d94782fc 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbSplitArrayMsgNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbSplitArrayMsgNode.java @@ -17,7 +17,6 @@ package org.thingsboard.rule.engine.transform; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ArrayNode; -import lombok.extern.slf4j.Slf4j; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.rule.engine.api.EmptyNodeConfiguration; import org.thingsboard.rule.engine.api.RuleNode; @@ -25,7 +24,6 @@ import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNode; import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; -import org.thingsboard.rule.engine.api.util.TbNodeUtils; import org.thingsboard.server.common.data.msg.TbNodeConnectionType; import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.msg.TbMsg; @@ -34,7 +32,6 @@ import org.thingsboard.server.common.msg.queue.TbMsgCallback; import java.util.concurrent.ExecutionException; -@Slf4j @RuleNode( type = ComponentType.TRANSFORMATION, name = "split array msg", @@ -44,16 +41,13 @@ import java.util.concurrent.ExecutionException; "All outbound messages will have the same type and metadata as the original array message.

    " + "Output connections: Success, Failure.", icon = "content_copy", - configDirective = "tbNodeEmptyConfig" + configDirective = "tbNodeEmptyConfig", + docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/transformation/split-array-msg/" ) public class TbSplitArrayMsgNode implements TbNode { - private EmptyNodeConfiguration config; - @Override - public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException { - this.config = TbNodeUtils.convert(configuration, EmptyNodeConfiguration.class); - } + public void init(TbContext ctx, TbNodeConfiguration configuration) {} @Override public void onMsg(TbContext ctx, TbMsg msg) throws ExecutionException, InterruptedException, TbNodeException { @@ -89,4 +83,5 @@ public class TbSplitArrayMsgNode implements TbNode { ctx.tellFailure(msg, new RuntimeException("Msg data is not a JSON Array!")); } } + } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbTransformMsgNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbTransformMsgNode.java index 9373149cf5..487d92a2b0 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbTransformMsgNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbTransformMsgNode.java @@ -41,7 +41,8 @@ import java.util.List; "{ msg: new payload,
       metadata: new metadata,
       msgType: new msgType }

    " + "All fields in resulting object are optional and will be taken from original message if not specified.

    " + "Output connections: Success, Failure.", - configDirective = "tbTransformationNodeScriptConfig" + configDirective = "tbTransformationNodeScriptConfig", + docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/transformation/script/" ) public class TbTransformMsgNode extends TbAbstractTransformNode { @@ -71,4 +72,5 @@ public class TbTransformMsgNode extends TbAbstractTransformNode findEntityIdAsync(TbContext ctx, EntityId originator) { - switch (originator.getEntityType()) { - case CUSTOMER: - return Futures.immediateFuture((CustomerId) originator); - case USER: - return toCustomerIdAsync(ctx, ctx.getUserService().findUserByIdAsync(ctx.getTenantId(), (UserId) originator)); - case ASSET: - return toCustomerIdAsync(ctx, ctx.getAssetService().findAssetByIdAsync(ctx.getTenantId(), (AssetId) originator)); - case DEVICE: - return toCustomerIdAsync(ctx, Futures.immediateFuture(ctx.getDeviceService().findDeviceById(ctx.getTenantId(), (DeviceId) originator))); - default: - return Futures.immediateFailedFuture(new TbNodeException("Unexpected originator EntityType: " + originator.getEntityType())); - } - } - - private static ListenableFuture toCustomerIdAsync(TbContext ctx, ListenableFuture future) { - return Futures.transform(future, in -> in != null ? in.getCustomerId() : null, ctx.getDbCallbackExecutor()); - } - -} diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/TenantIdLoader.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/TenantIdLoader.java index 8ea0f70a3f..cd0b11bb25 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/TenantIdLoader.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/TenantIdLoader.java @@ -18,14 +18,12 @@ package org.thingsboard.rule.engine.util; import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.HasTenantId; -import org.thingsboard.server.common.data.cf.CalculatedFieldLink; import org.thingsboard.server.common.data.id.AiModelId; import org.thingsboard.server.common.data.id.AlarmId; import org.thingsboard.server.common.data.id.ApiUsageStateId; import org.thingsboard.server.common.data.id.AssetId; import org.thingsboard.server.common.data.id.AssetProfileId; import org.thingsboard.server.common.data.id.CalculatedFieldId; -import org.thingsboard.server.common.data.id.CalculatedFieldLinkId; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DashboardId; import org.thingsboard.server.common.data.id.DeviceId; @@ -170,14 +168,6 @@ public class TenantIdLoader { case CALCULATED_FIELD: tenantEntity = ctx.getCalculatedFieldService().findById(ctxTenantId, new CalculatedFieldId(id)); break; - case CALCULATED_FIELD_LINK: - CalculatedFieldLink calculatedFieldLink = ctx.getCalculatedFieldService().findCalculatedFieldLinkById(ctxTenantId, new CalculatedFieldLinkId(id)); - if (calculatedFieldLink != null) { - tenantEntity = ctx.getCalculatedFieldService().findById(ctxTenantId, calculatedFieldLink.getCalculatedFieldId()); - } else { - tenantEntity = null; - } - break; case JOB: tenantEntity = ctx.getJobService().findJobById(ctxTenantId, new JobId(id)); break; diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/TestDbCallbackExecutor.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/TestDbCallbackExecutor.java index b3afec500c..f27ea73ce6 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/TestDbCallbackExecutor.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/TestDbCallbackExecutor.java @@ -28,7 +28,7 @@ public class TestDbCallbackExecutor implements ListeningExecutor { try { return Futures.immediateFuture(task.call()); } catch (Exception e) { - throw new RuntimeException(e); + return Futures.immediateFailedFuture(e); } } diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbClearAlarmNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbClearAlarmNodeTest.java index ad2c6100bf..ccf950b80c 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbClearAlarmNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbClearAlarmNodeTest.java @@ -17,7 +17,7 @@ package org.thingsboard.rule.engine.action; import com.datastax.oss.driver.api.core.uuid.Uuids; import com.fasterxml.jackson.databind.JsonNode; -import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.FluentFuture; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -49,6 +49,7 @@ import org.thingsboard.server.common.msg.TbMsgMetaData; import java.util.function.Consumer; +import static com.google.common.util.concurrent.Futures.immediateFuture; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyLong; @@ -112,8 +113,8 @@ class TbClearAlarmNodeTest { .endTs(oldEndDate) .build(); - when(alarmDetailsScriptMock.executeJsonAsync(msg)).thenReturn(Futures.immediateFuture(null)); - when(alarmServiceMock.findLatestActiveByOriginatorAndType(tenantId, msgOriginator, "SomeType")).thenReturn(activeAlarm); + when(alarmDetailsScriptMock.executeJsonAsync(msg)).thenReturn(immediateFuture(null)); + when(alarmServiceMock.findLatestActiveByOriginatorAndTypeAsync(tenantId, msgOriginator, "SomeType")).thenReturn(FluentFuture.from(immediateFuture(activeAlarm))); when(alarmServiceMock.clearAlarm(eq(activeAlarm.getTenantId()), eq(activeAlarm.getId()), anyLong(), nullable(JsonNode.class))) .thenReturn(AlarmApiCallResult.builder() .successful(true) @@ -172,8 +173,8 @@ class TbClearAlarmNodeTest { .build(); expectedAlarm.setId(id); - when(alarmDetailsScriptMock.executeJsonAsync(msg)).thenReturn(Futures.immediateFuture(null)); - when(alarmServiceMock.findAlarmById(tenantId, id)).thenReturn(activeAlarm); + when(alarmDetailsScriptMock.executeJsonAsync(msg)).thenReturn(immediateFuture(null)); + when(alarmServiceMock.findAlarmByIdAsync(tenantId, id)).thenReturn(immediateFuture(activeAlarm)); when(alarmServiceMock.clearAlarm(eq(activeAlarm.getTenantId()), eq(activeAlarm.getId()), anyLong(), nullable(JsonNode.class))) .thenReturn(AlarmApiCallResult.builder() .successful(true) diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/ai/TbAiNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/ai/TbAiNodeTest.java index 6eb7b6233b..a786cdd536 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/ai/TbAiNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/ai/TbAiNodeTest.java @@ -17,8 +17,11 @@ package org.thingsboard.rule.engine.ai; import com.fasterxml.jackson.databind.node.ObjectNode; import com.google.common.util.concurrent.FluentFuture; +import com.google.common.util.concurrent.Futures; import dev.langchain4j.data.message.AiMessage; +import dev.langchain4j.data.message.ImageContent; import dev.langchain4j.data.message.SystemMessage; +import dev.langchain4j.data.message.TextContent; import dev.langchain4j.data.message.UserMessage; import dev.langchain4j.model.chat.request.ResponseFormat; import dev.langchain4j.model.chat.request.ResponseFormatType; @@ -32,6 +35,7 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; import org.junit.jupiter.params.provider.ValueSource; +import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.thingsboard.common.util.JacksonUtil; @@ -43,6 +47,10 @@ import org.thingsboard.rule.engine.api.RuleEngineAiChatModelService; import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; +import org.thingsboard.server.common.data.GeneralFileDescriptor; +import org.thingsboard.server.common.data.ResourceType; +import org.thingsboard.server.common.data.TbResource; +import org.thingsboard.server.common.data.TbResourceDataInfo; import org.thingsboard.server.common.data.ai.AiModel; import org.thingsboard.server.common.data.ai.model.AiModelConfig; import org.thingsboard.server.common.data.ai.model.chat.AnthropicChatModelConfig; @@ -52,6 +60,7 @@ import org.thingsboard.server.common.data.ai.provider.OpenAiProviderConfig; import org.thingsboard.server.common.data.id.AiModelId; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.RuleNodeId; +import org.thingsboard.server.common.data.id.TbResourceId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.msg.TbNodeConnectionType; import org.thingsboard.server.common.data.rule.RuleNode; @@ -59,9 +68,14 @@ import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.dao.ai.AiModelService; import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.dao.resource.ResourceService; +import org.thingsboard.server.dao.resource.TbResourceDataCache; +import java.util.Base64; +import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.Set; import java.util.UUID; import java.util.stream.Stream; @@ -76,16 +90,23 @@ import static org.mockito.BDDMockito.given; import static org.mockito.BDDMockito.then; import static org.mockito.Mockito.lenient; import static org.mockito.Mockito.never; +import static org.thingsboard.server.common.data.ResourceType.GENERAL; @ExtendWith(MockitoExtension.class) class TbAiNodeTest { + private static final byte[] PNG_IMAGE = Base64.getDecoder().decode("iVBORw0KGgoAAAANSUhEUgAAAMgAAACgCAMAAAB+IdObAAAC9FBMVEUAAAABAQEBAgICAgICAwMCBAQDAwMDBQUDBgYEBAQEBwcECAgFCQkFCgoGBgYGCwsGDAwHBwcHDQ0HDg4ICAgIDw8IEBAJCQkJEREKEhIKExMLFBQLFRUMFhYMFxcNDQ0NGBgNGRkODg4OGhoOGxsPDw8PHBwPHR0QEBAQHh4QHx8RERERICARISESEhISIiITExMTIyMTJCQUJSUUJiYVKCgWFhYWKSkXFxcXGhwYGBgYLC0ZGRkaMDEaMTIbGxsbMjMcMzQdNTYfOTogICAgOzwiP0AiQEEjIyMjQkMkQ0QnJycnSEkoS0wpKSkrUFErUVIsLCwvV1gvWFkwWlszMzMzYGE1NTU2NjY3Zmc4aWo5OTk5ams5a2w6Ojo6bG07bm88cXI9cnM9c3Q/dndAQEBAeHlBeXpCQkJCe3xCfH1DQ0NEREREf4FFRUVFgIJGg4VHhYdISEhIhohJh4lLi41LjI5MTExMjpBNj5FNkJJOkpRQUFBQlZdRUVFSUlJTU1NTmpxUVFRUnZ9VVVVVnqBWVlZYpadZWVlZp6laqKpbW1tbqatbqqxcXFxcrK5dra9drrBeXl5er7FfsbNfsrRgs7VhYWFiYmJiuLpjubtku71lvL5lvb9mvsBnwcNowsRpxMZpxcdra2tryctsysxubm5uzc9vb29vz9Fw0dNx0tVy1Ndy1dhz1tlz19p0dHR02Nt02dx12t1229523N93d3d33eB33uF5eXl54eR6enp64+Z65Od75eh75ul8fHx85+p86Ot96ex96u2AgICA7vGA7/KB8fSC8/aD9PeD9fiEhISE9/qF+PuF+fyGhoaG+v2G+/6Hh4eH/P+IiIiMjIyNjY2Ojo6QkJCRkZGSkpKTk5Obm5ucnJyfn5+lpaWnp6eoqKipqamqqqqwsLCzs7O1tbW4uLi5ubm6urq7u7u8vLy/v7/BwcHCwsLFxcXGxsbPz8/Y2Nji4uLj4+Pv7+/4+Pj5+fn+/v7/75T///+GLm1tAAAAAWJLR0T7omo23AAABJtJREFUeNrt3Wd8E3UYB/CH0oqm1dJaS5N0IKu0qQSVinXG4gKlKFi3uMC9FVwoVQnQqCBgBVxFnKCoFFFExFGhliWt/zoYLuIMKEpB7b3xuf9dQu+MvAjXcsTf7/PJk/ul1/S+TS53r3KkNFfk0V6evDHbFGruQ3EQTzNVUFxkHOXFB6QbIQiCIAiC/GeSs/QkR6vkCPeUaNUeSUjkkdR1npCp6a7VV7U6P1dbKfNFrS89rJNas/T6rlZtkUS/i2evhw99Q92y9/r7nVzzw7VfeDX3y2qv893plTVb1uW+uw6xiyNpspAQ8bjLy8l5REiImOlUq3Pniunyxw8Ib+vqF7aB5AgdItLVmit0iOgc9W0owhDt1RSAABL3EGeDDqmXhwRXgw6pj3qESFhtgHC1DYSGrJCQjweFq4SEqzkD67zGah8Inay+p1yl4XqKWt2lF69UDxQrzzevXZprrDn2gfTIUs85Iv/oHpny8HKHdugeVZhpXNudu6u6J1P8lmpIX1ys10X6myVfPeLl919UZFi74JXjWtfCecfa5sj+odx908XSg9Taqdaw+3I1QuYLA6RG2AbiEDpE9JJnvcYP1BRhgiw3QuoAASTuIQnP6JCF8hQlcbYBwrWIKgPDIg9UGSGP2QdCnZ+QkDneKQs4swqe1CDJ09RaXfBUETWKm3a+gFMMEMc0+0AoJVX9nM1+VDsCznLurz64b5VWq7nWLLi81QfygYZfNlU7nAUP0nOwrLnGiiAIgiAIgiAIgiDI/zstLS3tMEtKSiycgAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIBYAkEQBEEQBEEQBEEQBGmrdLwuyLmhg703km8Z63k7N2Tw0jnqFt/f0bROn69WBYOfbuxiyR+8MXC9vB8QCBTQkEAgMOG2gVyvDmTzdAWuifFp077m8f503vwZr/PSd28Hg+uaTjVDlOFEIxVrINVijfwi4glCHE1XioXPz6kX9xHNFIUkvyM/xqeduIPHup95bGni8edYotOUqJCrrII0iMv4LnNFg4Sczd/9/Zw4abchD0Ygv0pIBVFZG0Nq587lu/PE02EIXSQuaSfI92l88bfNFkHqLxUnEM1+bXQEMloMY8hgn893esyQIzbzWHtveXn51GW89AtfTeyATWZIWm919s6wBtLYdfXdVCyuuEdCHhoxwr/mAzdDtMQKoaP4duQmRVG+kUtyu83X3OuylX09f+9r0c6eOvkjx82fdPdLiHrdjsrD1Z39LP5W06ExQ475g8eqSR6PZ+oXvLSVNWk/nmmGKNcSXaBYBXEPFkMXV1GlhFyYlSof3t19ZOxfPJp+4/HTeh47JhGdqLQxJDtpyRJxBgUi+0g7QkYSlVsHoVtFrcNiyO0SsoXHDxIykej4v/8F+XxDKLRxmXWQfo2jyGJIh894PDs9FArNeIGXvlwbCn37Upl5rXObOMPtf1K4z5u8ne/sx0tl6hbfgtNkBEGQPZs4uUBwTxoTH5DxtM0TD46+20lpHrfXX7e52/jtyj9kFKbIT2L3FQAAAABJRU5ErkJggg=="); + @Mock TbContext ctxMock; @Mock AiModelService aiModelServiceMock; @Mock RuleEngineAiChatModelService aiChatModelServiceMock; + @Mock + TbResourceDataCache tbResourceDataCacheMock; + @Mock + ResourceService resourceServiceMock; TbAiNode aiNode; TbAiNodeConfiguration config; @@ -108,7 +129,10 @@ class TbAiNodeTest { config = new TbAiNodeConfiguration(); modelConfig = OpenAiChatModelConfig.builder() - .providerConfig(new OpenAiProviderConfig("test-api-key")) + .providerConfig(OpenAiProviderConfig.builder() + .baseUrl(OpenAiProviderConfig.OPENAI_OFFICIAL_BASE_URL) + .apiKey("test-api-key") + .build()) .modelId("gpt-4o") .temperature(0.5) .topP(0.3) @@ -141,6 +165,8 @@ class TbAiNodeTest { lenient().when(ctxMock.getAiModelService()).thenReturn(aiModelServiceMock); lenient().when(ctxMock.getAiChatModelService()).thenReturn(aiChatModelServiceMock); lenient().when(ctxMock.getDbCallbackExecutor()).thenReturn(new TestDbCallbackExecutor()); + lenient().when(ctxMock.getTbResourceDataCache()).thenReturn(tbResourceDataCacheMock); + lenient().when(ctxMock.getResourceService()).thenReturn(resourceServiceMock); } @Test @@ -158,6 +184,7 @@ class TbAiNodeTest { assertThat(config.getResponseFormat()).isEqualTo(new TbJsonResponseFormat()); assertThat(config.getTimeoutSeconds()).isEqualTo(60); assertThat(config.isForceAck()).isTrue(); + assertThat(config.getResourceIds()).isNull(); } /* -- Node initialization tests -- */ @@ -193,10 +220,9 @@ class TbAiNodeTest { } static Stream invalidSystemPrompts() { - String tooLongString = "a".repeat(10_001); + String tooLongString = "a".repeat(500_001); return Stream.of( Arguments.of(""), - Arguments.of(" "), Arguments.of(tooLongString) ); } @@ -213,12 +239,17 @@ class TbAiNodeTest { } static Stream validSystemPrompts() { - String longString = "a".repeat(10_000); + String longString = "a".repeat(500_000); return Stream.of( Arguments.of((String) null), Arguments.of("a"), Arguments.of("Test system prompt"), - Arguments.of(longString) + Arguments.of(longString), + Arguments.of(""" + first sentence + + second sentence + """) ); } @@ -239,7 +270,7 @@ class TbAiNodeTest { } static Stream invalidUserPrompts() { - String tooLongString = "a".repeat(10_001); + String tooLongString = "a".repeat(500_001); return Stream.of( Arguments.of((String) null), Arguments.of(""), @@ -260,11 +291,16 @@ class TbAiNodeTest { } static Stream validUserPrompts() { - String longString = "a".repeat(10_000); + String longString = "a".repeat(500_000); return Stream.of( Arguments.of("a"), Arguments.of("Test user prompt"), - Arguments.of(longString) + Arguments.of(longString), + Arguments.of(""" + first sentence + + second sentence + """) ); } @@ -364,6 +400,36 @@ class TbAiNodeTest { .matches(e -> ((TbNodeException) e).isUnrecoverable()); } + @Test + void givenNotExistingResources_whenInit_thenThrowsException() { + // GIVEN + config = constructValidConfig(); + UUID resourceId = UUID.randomUUID(); + config.setResourceIds(Set.of(resourceId)); + + // WHEN-THEN + assertThatThrownBy(() -> aiNode.init(ctxMock, new TbNodeConfiguration(JacksonUtil.valueToTree(config)))) + .isInstanceOf(TbNodeException.class) + .hasMessageContaining("[" + tenantId + "] Resource with ID: [" + resourceId + "] was not found"); + } + + @Test + void givenResourceOfWrongType_whenInit_thenThrowsException() { + // GIVEN + config = constructValidConfig(); + UUID resourceId = UUID.randomUUID(); + config.setResourceIds(Set.of(resourceId)); + + // WHEN-THEN + TbResource tbResource = new TbResource(); + tbResource.setResourceType(ResourceType.DASHBOARD); + given(resourceServiceMock.findResourceInfoById(any(), any())).willReturn(tbResource); + + assertThatThrownBy(() -> aiNode.init(ctxMock, new TbNodeConfiguration(JacksonUtil.valueToTree(config)))) + .isInstanceOf(TbNodeException.class) + .hasMessageContaining("[" + tenantId + "] Resource with ID: [" + resourceId + "] has unsupported resource type: " + ResourceType.DASHBOARD); + } + /* -- Message processing tests -- */ @Test @@ -551,6 +617,166 @@ class TbAiNodeTest { ); } + @Test + void givenSystemPromptAndUserPromptAndResourcesConfigured_whenOnMsg_thenRequestContainsSystemAndUserAndResourceContent() throws TbNodeException { + String systemPrompt = "Respond with valid JSON"; + String userPrompt = "Tell me a joke"; + String textData = "Text resource content for AI request."; + String xmlData = ""; + + // GIVEN + config = constructValidConfig(); + config.setSystemPrompt(systemPrompt); + config.setUserPrompt(userPrompt); + UUID resourceId = UUID.randomUUID(); + UUID resourceId2 = UUID.randomUUID(); + UUID resourceId3 = UUID.randomUUID(); + + config.setResourceIds(Set.of(resourceId, resourceId2, resourceId3)); + + // WHEN-THEN + TbResource textResource = buildGeneralResource(textData.getBytes(), "text/plain"); + TbResource xmlResource = buildGeneralResource(xmlData.getBytes(), "application/xml"); + TbResource imageResource = buildGeneralResource(PNG_IMAGE, "image/png"); + + given(resourceServiceMock.findResourceInfoById(any(), eq(new TbResourceId(resourceId)))).willReturn(textResource); + given(resourceServiceMock.findResourceInfoById(any(), eq(new TbResourceId(resourceId2)))).willReturn(xmlResource); + given(resourceServiceMock.findResourceInfoById(any(), eq(new TbResourceId(resourceId3)))).willReturn(imageResource); + + given(tbResourceDataCacheMock.getResourceDataInfoAsync(any(), eq(new TbResourceId(resourceId)))).willReturn(FluentFuture.from(Futures.immediateFuture(textResource.toResourceDataInfo()))); + given(tbResourceDataCacheMock.getResourceDataInfoAsync(any(), eq(new TbResourceId(resourceId2)))).willReturn(FluentFuture.from(Futures.immediateFuture(xmlResource.toResourceDataInfo()))); + given(tbResourceDataCacheMock.getResourceDataInfoAsync(any(), eq(new TbResourceId(resourceId3)))).willReturn(FluentFuture.from(Futures.immediateFuture(imageResource.toResourceDataInfo()))); + + aiNode.init(ctxMock, new TbNodeConfiguration(JacksonUtil.valueToTree(config))); + + var msg = TbMsg.newMsg() + .originator(deviceId) + .data(TbMsg.EMPTY_JSON_OBJECT) + .metaData(TbMsgMetaData.EMPTY) + .build(); + + var chatResponse = ChatResponse.builder() + .aiMessage(AiMessage.from("{\"type\":\"joke\",\"setup\":\"Why did the scarecrow win an award?\",\"punchline\":\"Because he was outstanding in his field.\"}")) + .build(); + + given(aiChatModelServiceMock.sendChatRequestAsync(any(), any())).willReturn(FluentFuture.from(immediateFuture(chatResponse))); + + // WHEN + aiNode.onMsg(ctxMock, msg); + + // THEN + then(aiChatModelServiceMock).should().sendChatRequestAsync(any(), + argThat(actualChatRequest -> { + assertThat(actualChatRequest.messages()).hasSize(2); + assertThat(actualChatRequest.messages().get(0)).isEqualTo(SystemMessage.from(systemPrompt)); + assertThat(((UserMessage)actualChatRequest.messages().get(1)).contents()) + .containsAll(List.of(new TextContent(userPrompt), new TextContent(textData), + new TextContent(xmlData), new ImageContent(Base64.getEncoder().encodeToString(PNG_IMAGE), "image/png"))); + return true; + }) + ); + } + + @Test + void givenNullResource_whenOnMsg_thenRequestContainsSystemAndUserPrompt() throws TbNodeException { + // GIVEN + config = constructValidConfig(); + UUID resourceId = UUID.randomUUID(); + config.setResourceIds(Set.of(resourceId)); + + // WHEN-THEN + TbResource tbResource = buildGeneralResource("Text resource content for AI request.".getBytes(), "text/plain"); + + given(resourceServiceMock.findResourceInfoById(any(), eq(new TbResourceId(resourceId)))).willReturn(tbResource); + given(tbResourceDataCacheMock.getResourceDataInfoAsync(any(), eq(new TbResourceId(resourceId)))).willReturn(FluentFuture.from(Futures.immediateFuture(null))); + + aiNode.init(ctxMock, new TbNodeConfiguration(JacksonUtil.valueToTree(config))); + + var msg = TbMsg.newMsg() + .originator(deviceId) + .data(TbMsg.EMPTY_JSON_OBJECT) + .metaData(TbMsgMetaData.EMPTY) + .build(); + + // WHEN + aiNode.onMsg(ctxMock, msg); + + // THEN + then(aiChatModelServiceMock).should().sendChatRequestAsync(any(), + argThat(actualChatRequest -> { + assertThat(actualChatRequest.messages()).hasSize(2); + assertThat(actualChatRequest.messages().get(0)).isEqualTo(SystemMessage.from(config.getSystemPrompt())); + assertThat(((UserMessage)actualChatRequest.messages().get(1)).contents()) + .containsAll(List.of(new TextContent(config.getUserPrompt()))); + return true; + }) + ); + } + + @Test + void givenResourceWithNoDescriptor_whenOnMsg_thenEnqueueForTellFailure() throws TbNodeException { + // GIVEN + config = constructValidConfig(); + UUID resourceId = UUID.randomUUID(); + config.setResourceIds(Set.of(resourceId)); + + // WHEN-THEN + TbResource tbResource = buildGeneralResource("Text resource content for AI request.".getBytes(), "text/plain"); + TbResourceDataInfo resourceDataInfo = new TbResourceDataInfo(tbResource.getData(), null); + + given(resourceServiceMock.findResourceInfoById(any(), eq(new TbResourceId(resourceId)))).willReturn(tbResource); + given(tbResourceDataCacheMock.getResourceDataInfoAsync(any(), eq(new TbResourceId(resourceId)))).willReturn(FluentFuture.from(Futures.immediateFuture(resourceDataInfo))); + + aiNode.init(ctxMock, new TbNodeConfiguration(JacksonUtil.valueToTree(config))); + + var msg = TbMsg.newMsg() + .originator(deviceId) + .data(TbMsg.EMPTY_JSON_OBJECT) + .metaData(TbMsgMetaData.EMPTY) + .build(); + + // WHEN + aiNode.onMsg(ctxMock, msg); + + // THEN + var exceptionCaptor = ArgumentCaptor.forClass(Throwable.class); + then(ctxMock).should().enqueueForTellFailure(any(), exceptionCaptor.capture()); + Throwable actualException = exceptionCaptor.getValue(); + assertThat(actualException.getMessage()).isEqualTo("Missing descriptor for resource"); + } + + @Test + void givenResourceWithNoMediaType_whenOnMsg_thenEnqueueForTellFailure() throws TbNodeException { + // GIVEN + config = constructValidConfig(); + UUID resourceId = UUID.randomUUID(); + config.setResourceIds(Set.of(resourceId)); + + // WHEN-THEN + TbResource tbResource = buildGeneralResource("Text resource content for AI request.".getBytes(), "text/plain"); + TbResourceDataInfo resourceDataInfo = new TbResourceDataInfo(tbResource.getData(), JacksonUtil.newObjectNode()); + + given(resourceServiceMock.findResourceInfoById(any(), eq(new TbResourceId(resourceId)))).willReturn(tbResource); + given(tbResourceDataCacheMock.getResourceDataInfoAsync(any(), eq(new TbResourceId(resourceId)))).willReturn(FluentFuture.from(Futures.immediateFuture(resourceDataInfo))); + + aiNode.init(ctxMock, new TbNodeConfiguration(JacksonUtil.valueToTree(config))); + + var msg = TbMsg.newMsg() + .originator(deviceId) + .data(TbMsg.EMPTY_JSON_OBJECT) + .metaData(TbMsgMetaData.EMPTY) + .build(); + + // WHEN + aiNode.onMsg(ctxMock, msg); + + // THEN + var exceptionCaptor = ArgumentCaptor.forClass(Throwable.class); + then(ctxMock).should().enqueueForTellFailure(any(), exceptionCaptor.capture()); + Throwable actualException = exceptionCaptor.getValue(); + assertThat(actualException.getMessage()).isEqualTo("Missing mediaType in resource descriptor {}"); + } + @Test void givenTemplatedPrompts_whenOnMsg_thenRequestContainsSubstitutedMessages() throws TbNodeException { // GIVEN @@ -941,4 +1167,13 @@ class TbAiNodeTest { then(ctxMock).should(never()).tellFailure(any(), any()); } + private TbResource buildGeneralResource(byte[] data, String mediaType) { + TbResource tbResource = new TbResource(); + tbResource.setResourceType(GENERAL); + GeneralFileDescriptor descriptor = new GeneralFileDescriptor(mediaType); + tbResource.setDescriptorValue(descriptor); + tbResource.setData(data); + return tbResource; + } + } diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/geo/GpsGeofencingActionTestCase.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/geo/GpsGeofencingActionTestCase.java index bab470f3d7..fc6af82052 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/geo/GpsGeofencingActionTestCase.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/geo/GpsGeofencingActionTestCase.java @@ -18,14 +18,14 @@ package org.thingsboard.rule.engine.geo; import lombok.Data; import org.thingsboard.server.common.data.id.EntityId; -import java.util.HashMap; -import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; @Data public class GpsGeofencingActionTestCase { private EntityId entityId; - private Map entityStates; + private ConcurrentMap entityStates; private boolean msgInside; private boolean reportPresenceStatusOnEachMessage; @@ -33,7 +33,8 @@ public class GpsGeofencingActionTestCase { this.entityId = entityId; this.msgInside = msgInside; this.reportPresenceStatusOnEachMessage = reportPresenceStatusOnEachMessage; - this.entityStates = new HashMap<>(); + this.entityStates = new ConcurrentHashMap<>(); this.entityStates.put(entityId, entityGeofencingState); } + } diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetCustomerAttributeNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetCustomerAttributeNodeTest.java index 3f2c58f191..942b1ab5ce 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetCustomerAttributeNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetCustomerAttributeNodeTest.java @@ -16,7 +16,7 @@ package org.thingsboard.rule.engine.metadata; import com.fasterxml.jackson.databind.JsonNode; -import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.FluentFuture; import lombok.RequiredArgsConstructor; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; @@ -53,19 +53,19 @@ import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.data.util.TbPair; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; -import org.thingsboard.server.dao.asset.AssetService; import org.thingsboard.server.dao.attributes.AttributesService; -import org.thingsboard.server.dao.device.DeviceService; +import org.thingsboard.server.dao.entity.EntityService; import org.thingsboard.server.dao.timeseries.TimeseriesService; -import org.thingsboard.server.dao.user.UserService; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.NoSuchElementException; +import java.util.Optional; import java.util.UUID; +import static com.google.common.util.concurrent.Futures.immediateFuture; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertInstanceOf; @@ -73,19 +73,20 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.lenient; import static org.mockito.Mockito.never; -import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) public class TbGetCustomerAttributeNodeTest { - private static final DeviceId DUMMY_DEVICE_ORIGINATOR = new DeviceId(UUID.randomUUID()); - private static final TenantId TENANT_ID = new TenantId(UUID.randomUUID()); - private static final CustomerId CUSTOMER_ID = new CustomerId(UUID.randomUUID()); - private static final ListeningExecutor DB_EXECUTOR = new TestDbCallbackExecutor(); + private final DeviceId DUMMY_DEVICE_ORIGINATOR = new DeviceId(UUID.randomUUID()); + private final TenantId TENANT_ID = TenantId.fromUUID(UUID.randomUUID()); + private final CustomerId CUSTOMER_ID = new CustomerId(UUID.randomUUID()); + private final ListeningExecutor DB_EXECUTOR = new TestDbCallbackExecutor(); + @Mock private TbContext ctxMock; @Mock @@ -93,21 +94,24 @@ public class TbGetCustomerAttributeNodeTest { @Mock private TimeseriesService timeseriesServiceMock; @Mock - private UserService userServiceMock; - @Mock - private AssetService assetServiceMock; - @Mock - private DeviceService deviceServiceMock; + private EntityService entityServiceMock; + private TbGetCustomerAttributeNode node; private TbGetEntityDataNodeConfiguration config; private TbNodeConfiguration nodeConfiguration; private TbMsg msg; @BeforeEach - public void setUp() { + public void setup() { node = new TbGetCustomerAttributeNode(); config = new TbGetEntityDataNodeConfiguration().defaultConfiguration(); nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); + + lenient().when(ctxMock.getTenantId()).thenReturn(TENANT_ID); + lenient().when(ctxMock.getDbCallbackExecutor()).thenReturn(DB_EXECUTOR); + lenient().when(ctxMock.getAttributesService()).thenReturn(attributesServiceMock); + lenient().when(ctxMock.getTimeseriesService()).thenReturn(timeseriesServiceMock); + lenient().when(ctxMock.getEntityService()).thenReturn(entityServiceMock); } @Test @@ -237,12 +241,9 @@ public class TbGetCustomerAttributeNodeTest { .data(TbMsg.EMPTY_JSON_OBJECT) .build(); - when(ctxMock.getTenantId()).thenReturn(TENANT_ID); - - when(ctxMock.getUserService()).thenReturn(userServiceMock); - doReturn(Futures.immediateFuture(null)).when(userServiceMock).findUserByIdAsync(eq(TENANT_ID), eq(userId)); - - when(ctxMock.getDbCallbackExecutor()).thenReturn(DB_EXECUTOR); + when(entityServiceMock.fetchEntityCustomerIdAsync(TENANT_ID, userId)).thenReturn( + FluentFuture.from(immediateFuture(Optional.empty())) + ); // WHEN node.onMsg(ctxMock, msg); @@ -252,18 +253,13 @@ public class TbGetCustomerAttributeNodeTest { var actualExceptionCaptor = ArgumentCaptor.forClass(Throwable.class); verify(ctxMock, never()).tellSuccess(any()); - verify(ctxMock, times(1)) - .tellFailure(actualMessageCaptor.capture(), actualExceptionCaptor.capture()); + verify(ctxMock).tellFailure(actualMessageCaptor.capture(), actualExceptionCaptor.capture()); var actualMessage = actualMessageCaptor.getValue(); var actualException = actualExceptionCaptor.getValue(); - var expectedExceptionMessage = String.format( - "Failed to find customer for entity with id: %s and type: %s", - userId.getId(), userId.getEntityType().getNormalName()); - assertEquals(msg, actualMessage); - assertEquals(expectedExceptionMessage, actualException.getMessage()); + assertEquals("Originator not found", actualException.getMessage()); assertInstanceOf(NoSuchElementException.class, actualException); } @@ -282,16 +278,12 @@ public class TbGetCustomerAttributeNodeTest { ); var expectedPatternProcessedKeysList = List.of("sourceKey1", "sourceKey2", "sourceKey3"); - when(ctxMock.getTenantId()).thenReturn(TENANT_ID); - - when(ctxMock.getDeviceService()).thenReturn(deviceServiceMock); - doReturn(device).when(deviceServiceMock).findDeviceById(eq(TENANT_ID), eq(device.getId())); + when(entityServiceMock.fetchEntityCustomerIdAsync(TENANT_ID, device.getId())).thenReturn( + FluentFuture.from(immediateFuture(Optional.of(CUSTOMER_ID))) + ); - when(ctxMock.getAttributesService()).thenReturn(attributesServiceMock); when(attributesServiceMock.find(eq(TENANT_ID), eq(CUSTOMER_ID), eq(AttributeScope.SERVER_SCOPE), argThat(new ListMatcher<>(expectedPatternProcessedKeysList)))) - .thenReturn(Futures.immediateFuture(attributesList)); - - when(ctxMock.getDbCallbackExecutor()).thenReturn(DB_EXECUTOR); + .thenReturn(immediateFuture(attributesList)); // WHEN node.onMsg(ctxMock, msg); @@ -299,7 +291,7 @@ public class TbGetCustomerAttributeNodeTest { // THEN var actualMessageCaptor = ArgumentCaptor.forClass(TbMsg.class); - verify(ctxMock, times(1)).tellSuccess(actualMessageCaptor.capture()); + verify(ctxMock).tellSuccess(actualMessageCaptor.capture()); verify(ctxMock, never()).tellFailure(any(), any()); var expectedMsgData = "{\"temp\":42," + @@ -329,16 +321,12 @@ public class TbGetCustomerAttributeNodeTest { ); var expectedPatternProcessedKeysList = List.of("sourceKey1", "sourceKey2", "sourceKey3"); - when(ctxMock.getTenantId()).thenReturn(TENANT_ID); - - when(ctxMock.getUserService()).thenReturn(userServiceMock); - doReturn(Futures.immediateFuture(user)).when(userServiceMock).findUserByIdAsync(eq(TENANT_ID), eq(user.getId())); + when(entityServiceMock.fetchEntityCustomerIdAsync(TENANT_ID, user.getId())).thenReturn( + FluentFuture.from(immediateFuture(Optional.of(CUSTOMER_ID))) + ); - when(ctxMock.getAttributesService()).thenReturn(attributesServiceMock); when(attributesServiceMock.find(eq(TENANT_ID), eq(CUSTOMER_ID), eq(AttributeScope.SERVER_SCOPE), argThat(new ListMatcher<>(expectedPatternProcessedKeysList)))) - .thenReturn(Futures.immediateFuture(attributesList)); - - when(ctxMock.getDbCallbackExecutor()).thenReturn(DB_EXECUTOR); + .thenReturn(immediateFuture(attributesList)); // WHEN node.onMsg(ctxMock, msg); @@ -346,7 +334,7 @@ public class TbGetCustomerAttributeNodeTest { // THEN var actualMessageCaptor = ArgumentCaptor.forClass(TbMsg.class); - verify(ctxMock, times(1)).tellSuccess(actualMessageCaptor.capture()); + verify(ctxMock).tellSuccess(actualMessageCaptor.capture()); verify(ctxMock, never()).tellFailure(any(), any()); var expectedMsgMetaData = new TbMsgMetaData(Map.of( @@ -375,21 +363,18 @@ public class TbGetCustomerAttributeNodeTest { ); var expectedPatternProcessedKeysList = List.of("sourceKey1", "sourceKey2", "sourceKey3"); - when(ctxMock.getTenantId()).thenReturn(TENANT_ID); - - when(ctxMock.getTimeseriesService()).thenReturn(timeseriesServiceMock); when(timeseriesServiceMock.findLatest(eq(TENANT_ID), eq(customer.getId()), argThat(new ListMatcher<>(expectedPatternProcessedKeysList)))) - .thenReturn(Futures.immediateFuture(timeseriesList)); - - when(ctxMock.getDbCallbackExecutor()).thenReturn(DB_EXECUTOR); + .thenReturn(immediateFuture(timeseriesList)); // WHEN node.onMsg(ctxMock, msg); // THEN + verifyNoInteractions(entityServiceMock); + var actualMessageCaptor = ArgumentCaptor.forClass(TbMsg.class); - verify(ctxMock, times(1)).tellSuccess(actualMessageCaptor.capture()); + verify(ctxMock).tellSuccess(actualMessageCaptor.capture()); verify(ctxMock, never()).tellFailure(any(), any()); var expectedMsgData = "{\"temp\":42," + @@ -408,7 +393,7 @@ public class TbGetCustomerAttributeNodeTest { public void givenFetchTelemetryToMetaData_whenOnMsg_thenShouldFetchTelemetryToMetaData() { // GIVEN var asset = new Asset(new AssetId(UUID.randomUUID())); - asset.setCustomerId(new CustomerId(UUID.randomUUID())); + asset.setCustomerId(CUSTOMER_ID); prepareMsgAndConfig(TbMsgSource.METADATA, DataToFetch.LATEST_TELEMETRY, asset.getId()); @@ -419,16 +404,12 @@ public class TbGetCustomerAttributeNodeTest { ); var expectedPatternProcessedKeysList = List.of("sourceKey1", "sourceKey2", "sourceKey3"); - when(ctxMock.getTenantId()).thenReturn(TENANT_ID); - - when(ctxMock.getAssetService()).thenReturn(assetServiceMock); - doReturn(Futures.immediateFuture(asset)).when(assetServiceMock).findAssetByIdAsync(eq(TENANT_ID), eq(asset.getId())); - - when(ctxMock.getTimeseriesService()).thenReturn(timeseriesServiceMock); - when(timeseriesServiceMock.findLatest(eq(TENANT_ID), eq(asset.getCustomerId()), argThat(new ListMatcher<>(expectedPatternProcessedKeysList)))) - .thenReturn(Futures.immediateFuture(timeseriesList)); + when(entityServiceMock.fetchEntityCustomerIdAsync(TENANT_ID, asset.getId())).thenReturn( + FluentFuture.from(immediateFuture(Optional.of(CUSTOMER_ID))) + ); - when(ctxMock.getDbCallbackExecutor()).thenReturn(DB_EXECUTOR); + when(timeseriesServiceMock.findLatest(eq(TENANT_ID), eq(CUSTOMER_ID), argThat(new ListMatcher<>(expectedPatternProcessedKeysList)))) + .thenReturn(immediateFuture(timeseriesList)); // WHEN node.onMsg(ctxMock, msg); @@ -436,7 +417,7 @@ public class TbGetCustomerAttributeNodeTest { // THEN var actualMessageCaptor = ArgumentCaptor.forClass(TbMsg.class); - verify(ctxMock, times(1)).tellSuccess(actualMessageCaptor.capture()); + verify(ctxMock).tellSuccess(actualMessageCaptor.capture()); verify(ctxMock, never()).tellFailure(any(), any()); var expectedMsgMetaData = new TbMsgMetaData(Map.of( diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/profile/TbDeviceProfileNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/profile/TbDeviceProfileNodeTest.java index 90c35e59b1..04e5372bf7 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/profile/TbDeviceProfileNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/profile/TbDeviceProfileNodeTest.java @@ -16,8 +16,8 @@ package org.thingsboard.rule.engine.profile; import com.fasterxml.jackson.databind.node.ObjectNode; -import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.params.provider.Arguments; @@ -58,8 +58,11 @@ import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.DeviceProfileId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.kv.AttributeKvEntry; +import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry; +import org.thingsboard.server.common.data.kv.BasicTsKvEntry; +import org.thingsboard.server.common.data.kv.DoubleDataEntry; +import org.thingsboard.server.common.data.kv.LongDataEntry; import org.thingsboard.server.common.data.kv.TsKvEntry; -import org.thingsboard.server.common.data.kv.TsKvEntryAggWrapper; import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.data.query.BooleanFilterPredicate; import org.thingsboard.server.common.data.query.DynamicValue; @@ -82,19 +85,24 @@ import java.math.BigDecimal; import java.math.RoundingMode; import java.util.ArrayList; import java.util.Arrays; -import java.util.Collections; import java.util.List; import java.util.Optional; -import java.util.Set; import java.util.TreeMap; import java.util.UUID; import java.util.concurrent.TimeUnit; import java.util.stream.Stream; +import static com.google.common.util.concurrent.Futures.immediateFuture; +import static java.util.Collections.emptyList; +import static java.util.Collections.singleton; +import static java.util.Collections.singletonList; +import static java.util.Collections.singletonMap; +import static org.assertj.core.api.Assertions.assertThatNoException; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.anySet; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.lenient; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import static org.thingsboard.server.common.data.device.profile.AlarmConditionKeyType.ATTRIBUTE; @@ -126,16 +134,26 @@ public class TbDeviceProfileNodeTest extends AbstractRuleNodeUpgradeTest { private final CustomerId customerId = new CustomerId(UUID.randomUUID()); private final DeviceProfileId deviceProfileId = new DeviceProfileId(UUID.randomUUID()); + @BeforeEach + public void setup() { + lenient().when(ctx.getTenantId()).thenReturn(tenantId); + lenient().when(ctx.getDeviceProfileCache()).thenReturn(cache); + lenient().when(ctx.getTimeseriesService()).thenReturn(timeseriesService); + lenient().when(ctx.getAlarmService()).thenReturn(alarmService); + lenient().when(ctx.getDeviceService()).thenReturn(deviceService); + lenient().when(ctx.getAttributesService()).thenReturn(attributesService); + } + @Test public void testRandomMessageType() throws Exception { init(); DeviceProfile deviceProfile = new DeviceProfile(); DeviceProfileData deviceProfileData = new DeviceProfileData(); - deviceProfileData.setAlarms(Collections.emptyList()); + deviceProfileData.setAlarms(emptyList()); deviceProfile.setProfileData(deviceProfileData); - Mockito.when(cache.get(tenantId, deviceId)).thenReturn(deviceProfile); + when(cache.get(tenantId, deviceId)).thenReturn(deviceProfile); ObjectNode data = JacksonUtil.newObjectNode(); data.put("temperature", 42); TbMsg msg = TbMsg.newMsg() @@ -156,10 +174,10 @@ public class TbDeviceProfileNodeTest extends AbstractRuleNodeUpgradeTest { DeviceProfile deviceProfile = new DeviceProfile(); DeviceProfileData deviceProfileData = new DeviceProfileData(); - deviceProfileData.setAlarms(Collections.emptyList()); + deviceProfileData.setAlarms(emptyList()); deviceProfile.setProfileData(deviceProfileData); - Mockito.when(cache.get(tenantId, deviceId)).thenReturn(deviceProfile); + when(cache.get(tenantId, deviceId)).thenReturn(deviceProfile); ObjectNode data = JacksonUtil.newObjectNode(); data.put("temperature", 42); TbMsg msg = TbMsg.newMsg() @@ -187,20 +205,20 @@ public class TbDeviceProfileNodeTest extends AbstractRuleNodeUpgradeTest { DeviceProfileAlarm dpa = new DeviceProfileAlarm(); dpa.setId("highTemperatureAlarmID"); dpa.setAlarmType("highTemperatureAlarm"); - dpa.setCreateRules(new TreeMap<>(Collections.singletonMap(AlarmSeverity.CRITICAL, alarmRule))); + dpa.setCreateRules(new TreeMap<>(singletonMap(AlarmSeverity.CRITICAL, alarmRule))); AlarmRule clearRule = new AlarmRule(); AlarmCondition clearCondition = getNumericAlarmCondition(TIME_SERIES, "temperature", LESS, 10.0); clearRule.setCondition(clearCondition); dpa.setClearRule(clearRule); - deviceProfileData.setAlarms(Collections.singletonList(dpa)); + deviceProfileData.setAlarms(singletonList(dpa)); deviceProfile.setProfileData(deviceProfileData); - Mockito.when(cache.get(tenantId, deviceId)).thenReturn(deviceProfile); - Mockito.when(timeseriesService.findLatest(tenantId, deviceId, Collections.singleton("temperature"))) - .thenReturn(Futures.immediateFuture(Collections.emptyList())); - Mockito.when(alarmService.findLatestActiveByOriginatorAndType(tenantId, deviceId, "highTemperatureAlarm")).thenReturn(null); + when(cache.get(tenantId, deviceId)).thenReturn(deviceProfile); + when(timeseriesService.findLatest(tenantId, deviceId, singleton("temperature"))) + .thenReturn(immediateFuture(emptyList())); + when(alarmService.findLatestActiveByOriginatorAndType(tenantId, deviceId, "highTemperatureAlarm")).thenReturn(null); registerCreateAlarmMock(alarmService.createAlarm(any()), true); TbMsg theMsg = TbMsg.newMsg() @@ -246,7 +264,6 @@ public class TbDeviceProfileNodeTest extends AbstractRuleNodeUpgradeTest { node.onMsg(ctx, msg2); verify(ctx).tellSuccess(msg2); verify(ctx).enqueueForTellNext(theMsg2, "Alarm Updated"); - } @Test @@ -262,7 +279,7 @@ public class TbDeviceProfileNodeTest extends AbstractRuleNodeUpgradeTest { AlarmConditionFilter highTempFilter = getAlarmConditionFilter(TIME_SERIES, "temperature", GREATER, 50.0); AlarmCondition alarmHighTempCondition = new AlarmCondition(); - alarmHighTempCondition.setCondition(Collections.singletonList(highTempFilter)); + alarmHighTempCondition.setCondition(singletonList(highTempFilter)); AlarmRule alarmHighTempRule = new AlarmRule(); alarmHighTempRule.setCondition(alarmHighTempCondition); DeviceProfileAlarm dpa = new DeviceProfileAlarm(); @@ -276,13 +293,13 @@ public class TbDeviceProfileNodeTest extends AbstractRuleNodeUpgradeTest { dpa.setCreateRules(createRules); - deviceProfileData.setAlarms(Collections.singletonList(dpa)); + deviceProfileData.setAlarms(singletonList(dpa)); deviceProfile.setProfileData(deviceProfileData); - Mockito.when(cache.get(tenantId, deviceId)).thenReturn(deviceProfile); - Mockito.when(timeseriesService.findLatest(tenantId, deviceId, Collections.singleton("temperature"))) - .thenReturn(Futures.immediateFuture(Collections.emptyList())); - Mockito.when(alarmService.findLatestActiveByOriginatorAndType(tenantId, deviceId, "highTemperatureAlarm1")).thenReturn(null); + when(cache.get(tenantId, deviceId)).thenReturn(deviceProfile); + when(timeseriesService.findLatest(tenantId, deviceId, singleton("temperature"))) + .thenReturn(immediateFuture(emptyList())); + when(alarmService.findLatestActiveByOriginatorAndType(tenantId, deviceId, "highTemperatureAlarm1")).thenReturn(null); registerCreateAlarmMock(alarmService.createAlarm(any()), true); TbMsg theMsg = TbMsg.newMsg() @@ -366,7 +383,7 @@ public class TbDeviceProfileNodeTest extends AbstractRuleNodeUpgradeTest { attributeKvEntity.setLastUpdateTs(System.currentTimeMillis()); AttributeKvEntry entry = attributeKvEntity.toData(); - ListenableFuture> attrListListenableFuture = Futures.immediateFuture(Collections.singletonList(entry)); + ListenableFuture> attrListListenableFuture = immediateFuture(singletonList(entry)); AlarmConditionFilter alarmEnabledFilter = new AlarmConditionFilter(); alarmEnabledFilter.setKey(new AlarmConditionFilterKey(AlarmConditionKeyType.CONSTANT, "alarmEnabled")); @@ -396,19 +413,19 @@ public class TbDeviceProfileNodeTest extends AbstractRuleNodeUpgradeTest { DeviceProfileAlarm dpa = new DeviceProfileAlarm(); dpa.setId("alarmEnabledAlarmID"); dpa.setAlarmType("alarmEnabledAlarm"); - dpa.setCreateRules(new TreeMap<>(Collections.singletonMap(AlarmSeverity.CRITICAL, alarmRule))); + dpa.setCreateRules(new TreeMap<>(singletonMap(AlarmSeverity.CRITICAL, alarmRule))); - deviceProfileData.setAlarms(Collections.singletonList(dpa)); + deviceProfileData.setAlarms(singletonList(dpa)); deviceProfile.setProfileData(deviceProfileData); - Mockito.when(cache.get(tenantId, deviceId)).thenReturn(deviceProfile); - Mockito.when(timeseriesService.findLatest(tenantId, deviceId, Collections.singleton("temperature"))) - .thenReturn(Futures.immediateFuture(Collections.emptyList())); - Mockito.when(alarmService.findLatestActiveByOriginatorAndType(tenantId, deviceId, "alarmEnabledAlarm")) + when(cache.get(tenantId, deviceId)).thenReturn(deviceProfile); + when(timeseriesService.findLatest(tenantId, deviceId, singleton("temperature"))) + .thenReturn(immediateFuture(emptyList())); + when(alarmService.findLatestActiveByOriginatorAndType(tenantId, deviceId, "alarmEnabledAlarm")) .thenReturn(null); registerCreateAlarmMock(alarmService.createAlarm(any()), true); - Mockito.when(ctx.getAttributesService()).thenReturn(attributesService); - Mockito.when(attributesService.find(eq(tenantId), eq(deviceId), Mockito.any(AttributeScope.class), Mockito.anySet())) + when(ctx.getAttributesService()).thenReturn(attributesService); + when(attributesService.find(eq(tenantId), eq(deviceId), Mockito.any(AttributeScope.class), Mockito.anySet())) .thenReturn(attrListListenableFuture); TbMsg theMsg = TbMsg.newMsg() @@ -417,7 +434,7 @@ public class TbDeviceProfileNodeTest extends AbstractRuleNodeUpgradeTest { .copyMetaData(TbMsgMetaData.EMPTY) .data(TbMsg.EMPTY_STRING) .build(); - Mockito.when(ctx.newMsg(Mockito.any(), Mockito.any(TbMsgType.class), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.anyString())) + when(ctx.newMsg(Mockito.any(), Mockito.any(TbMsgType.class), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.anyString())) .thenReturn(theMsg); ObjectNode data = JacksonUtil.newObjectNode(); @@ -459,7 +476,7 @@ public class TbDeviceProfileNodeTest extends AbstractRuleNodeUpgradeTest { attributeKvEntity.setLastUpdateTs(System.currentTimeMillis()); AttributeKvEntry entry = attributeKvEntity.toData(); - ListenableFuture> attrListListenableFuture = Futures.immediateFuture(Optional.of(entry)); + ListenableFuture> attrListListenableFuture = immediateFuture(Optional.of(entry)); AlarmConditionFilter alarmEnabledFilter = new AlarmConditionFilter(); alarmEnabledFilter.setKey(new AlarmConditionFilterKey(AlarmConditionKeyType.CONSTANT, "alarmEnabled")); @@ -489,24 +506,24 @@ public class TbDeviceProfileNodeTest extends AbstractRuleNodeUpgradeTest { DeviceProfileAlarm dpa = new DeviceProfileAlarm(); dpa.setId("alarmEnabledAlarmID"); dpa.setAlarmType("alarmEnabledAlarm"); - dpa.setCreateRules(new TreeMap<>(Collections.singletonMap(AlarmSeverity.CRITICAL, alarmRule))); + dpa.setCreateRules(new TreeMap<>(singletonMap(AlarmSeverity.CRITICAL, alarmRule))); - deviceProfileData.setAlarms(Collections.singletonList(dpa)); + deviceProfileData.setAlarms(singletonList(dpa)); deviceProfile.setProfileData(deviceProfileData); - Mockito.when(deviceService.findDeviceById(tenantId, deviceId)).thenReturn(device); - Mockito.when(cache.get(tenantId, deviceId)).thenReturn(deviceProfile); - Mockito.when(timeseriesService.findLatest(tenantId, deviceId, Collections.singleton("temperature"))) - .thenReturn(Futures.immediateFuture(Collections.emptyList())); - Mockito.when(alarmService.findLatestActiveByOriginatorAndType(tenantId, deviceId, "alarmEnabledAlarm")) + when(deviceService.findDeviceById(tenantId, deviceId)).thenReturn(device); + when(cache.get(tenantId, deviceId)).thenReturn(deviceProfile); + when(timeseriesService.findLatest(tenantId, deviceId, singleton("temperature"))) + .thenReturn(immediateFuture(emptyList())); + when(alarmService.findLatestActiveByOriginatorAndType(tenantId, deviceId, "alarmEnabledAlarm")) .thenReturn(null); registerCreateAlarmMock(alarmService.createAlarm(any()), true); - Mockito.when(ctx.getAttributesService()).thenReturn(attributesService); - Mockito.when(attributesService.find(eq(tenantId), eq(deviceId), Mockito.any(AttributeScope.class), Mockito.anySet())) - .thenReturn(Futures.immediateFuture(Collections.emptyList())); - Mockito.when(attributesService.find(eq(tenantId), eq(customerId), Mockito.any(AttributeScope.class), Mockito.anyString())) - .thenReturn(Futures.immediateFuture(Optional.empty())); - Mockito.when(attributesService.find(eq(tenantId), eq(tenantId), Mockito.any(AttributeScope.class), Mockito.anyString())) + when(ctx.getAttributesService()).thenReturn(attributesService); + when(attributesService.find(eq(tenantId), eq(deviceId), Mockito.any(AttributeScope.class), Mockito.anySet())) + .thenReturn(immediateFuture(emptyList())); + when(attributesService.find(eq(tenantId), eq(customerId), Mockito.any(AttributeScope.class), Mockito.anyString())) + .thenReturn(immediateFuture(Optional.empty())); + when(attributesService.find(eq(tenantId), eq(tenantId), Mockito.any(AttributeScope.class), Mockito.anyString())) .thenReturn(attrListListenableFuture); TbMsg theMsg = TbMsg.newMsg() @@ -554,7 +571,7 @@ public class TbDeviceProfileNodeTest extends AbstractRuleNodeUpgradeTest { AttributeKvEntry entry = attributeKvEntity.toData(); ListenableFuture> listListenableFutureWithLess = - Futures.immediateFuture(Collections.singletonList(entry)); + immediateFuture(singletonList(entry)); AlarmConditionFilter highTempFilter = new AlarmConditionFilter(); highTempFilter.setKey(new AlarmConditionFilterKey(TIME_SERIES, "temperature")); @@ -568,25 +585,25 @@ public class TbDeviceProfileNodeTest extends AbstractRuleNodeUpgradeTest { )); highTempFilter.setPredicate(highTemperaturePredicate); AlarmCondition alarmCondition = new AlarmCondition(); - alarmCondition.setCondition(Collections.singletonList(highTempFilter)); + alarmCondition.setCondition(singletonList(highTempFilter)); AlarmRule alarmRule = new AlarmRule(); alarmRule.setCondition(alarmCondition); DeviceProfileAlarm dpa = new DeviceProfileAlarm(); dpa.setId("highTemperatureAlarmID"); dpa.setAlarmType("highTemperatureAlarm"); - dpa.setCreateRules(new TreeMap<>(Collections.singletonMap(AlarmSeverity.CRITICAL, alarmRule))); + dpa.setCreateRules(new TreeMap<>(singletonMap(AlarmSeverity.CRITICAL, alarmRule))); - deviceProfileData.setAlarms(Collections.singletonList(dpa)); + deviceProfileData.setAlarms(singletonList(dpa)); deviceProfile.setProfileData(deviceProfileData); - Mockito.when(cache.get(tenantId, deviceId)).thenReturn(deviceProfile); - Mockito.when(timeseriesService.findLatest(tenantId, deviceId, Collections.singleton("temperature"))) - .thenReturn(Futures.immediateFuture(Collections.emptyList())); - Mockito.when(alarmService.findLatestActiveByOriginatorAndType(tenantId, deviceId, "highTemperatureAlarm")) + when(cache.get(tenantId, deviceId)).thenReturn(deviceProfile); + when(timeseriesService.findLatest(tenantId, deviceId, singleton("temperature"))) + .thenReturn(immediateFuture(emptyList())); + when(alarmService.findLatestActiveByOriginatorAndType(tenantId, deviceId, "highTemperatureAlarm")) .thenReturn(null); registerCreateAlarmMock(alarmService.createAlarm(any()), true); - Mockito.when(ctx.getAttributesService()).thenReturn(attributesService); - Mockito.when(attributesService.find(eq(tenantId), eq(deviceId), Mockito.any(AttributeScope.class), Mockito.anySet())) + when(ctx.getAttributesService()).thenReturn(attributesService); + when(attributesService.find(eq(tenantId), eq(deviceId), Mockito.any(AttributeScope.class), Mockito.anySet())) .thenReturn(listListenableFutureWithLess); TbMsg theMsg = TbMsg.newMsg() @@ -648,7 +665,7 @@ public class TbDeviceProfileNodeTest extends AbstractRuleNodeUpgradeTest { AttributeKvEntry alarmDelayAttributeKvEntry = alarmDelayAttributeKvEntity.toData(); ListenableFuture> listListenableFuture = - Futures.immediateFuture(Arrays.asList(entry, alarmDelayAttributeKvEntry)); + immediateFuture(Arrays.asList(entry, alarmDelayAttributeKvEntry)); AlarmConditionFilter highTempFilter = new AlarmConditionFilter(); highTempFilter.setKey(new AlarmConditionFilterKey(TIME_SERIES, "temperature")); @@ -662,7 +679,7 @@ public class TbDeviceProfileNodeTest extends AbstractRuleNodeUpgradeTest { )); highTempFilter.setPredicate(highTemperaturePredicate); AlarmCondition alarmCondition = new AlarmCondition(); - alarmCondition.setCondition(Collections.singletonList(highTempFilter)); + alarmCondition.setCondition(singletonList(highTempFilter)); FilterPredicateValue filterPredicateValue = new FilterPredicateValue<>( 10L, @@ -680,19 +697,19 @@ public class TbDeviceProfileNodeTest extends AbstractRuleNodeUpgradeTest { DeviceProfileAlarm dpa = new DeviceProfileAlarm(); dpa.setId("highTemperatureAlarmID"); dpa.setAlarmType("highTemperatureAlarm"); - dpa.setCreateRules(new TreeMap<>(Collections.singletonMap(AlarmSeverity.CRITICAL, alarmRule))); + dpa.setCreateRules(new TreeMap<>(singletonMap(AlarmSeverity.CRITICAL, alarmRule))); - deviceProfileData.setAlarms(Collections.singletonList(dpa)); + deviceProfileData.setAlarms(singletonList(dpa)); deviceProfile.setProfileData(deviceProfileData); - Mockito.when(cache.get(tenantId, deviceId)).thenReturn(deviceProfile); - Mockito.when(timeseriesService.findLatest(tenantId, deviceId, Collections.singleton("temperature"))) - .thenReturn(Futures.immediateFuture(Collections.emptyList())); - Mockito.when(alarmService.findLatestActiveByOriginatorAndType(tenantId, deviceId, "highTemperatureAlarm")) + when(cache.get(tenantId, deviceId)).thenReturn(deviceProfile); + when(timeseriesService.findLatest(tenantId, deviceId, singleton("temperature"))) + .thenReturn(immediateFuture(emptyList())); + when(alarmService.findLatestActiveByOriginatorAndType(tenantId, deviceId, "highTemperatureAlarm")) .thenReturn(null); registerCreateAlarmMock(alarmService.createAlarm(any()), true); - Mockito.when(ctx.getAttributesService()).thenReturn(attributesService); - Mockito.when(attributesService.find(eq(tenantId), eq(deviceId), Mockito.any(AttributeScope.class), Mockito.anySet())) + when(ctx.getAttributesService()).thenReturn(attributesService); + when(attributesService.find(eq(tenantId), eq(deviceId), Mockito.any(AttributeScope.class), Mockito.anySet())) .thenReturn(listListenableFuture); TbMsg theMsg = TbMsg.newMsg() @@ -740,6 +757,105 @@ public class TbDeviceProfileNodeTest extends AbstractRuleNodeUpgradeTest { verify(ctx, Mockito.never()).tellFailure(Mockito.any(), Mockito.any()); } + @Test + public void testCurrentDeviceAttributeForDynamicDurationValue_noMessagesReceivedFromDeviceBeforeAlarmHarvesting() throws Exception { + init(); + + // 1. Setup device profile that has no alarm rules + var deviceProfileData = new DeviceProfileData(); + deviceProfileData.setAlarms(emptyList()); + + var deviceProfile = new DeviceProfile(deviceProfileId); + deviceProfile.setTenantId(tenantId); + deviceProfile.setName("default"); + deviceProfile.setProfileData(deviceProfileData); + + given(cache.get(tenantId, deviceId)).willReturn(deviceProfile); + + // 2. Initialize device state by sending ENTITY_CREATED event + var device = new Device(deviceId); + device.setTenantId(tenantId); + device.setName("device"); + device.setDeviceProfileId(deviceProfileId); + device.setType("default"); + + var entityCreatedEvent = TbMsg.newMsg() + .type(TbMsgType.ENTITY_CREATED) + .originator(deviceId) + .data(JacksonUtil.toString(device)) + .metaData(TbMsgMetaData.EMPTY) + .build(); + + node.onMsg(ctx, entityCreatedEvent); + + // 3. Update device profile so it now has dynamic duration rule with value taken from current device + var predicate = new NumericFilterPredicate(); + predicate.setOperation(NumericOperation.GREATER_OR_EQUAL); + predicate.setValue(new FilterPredicateValue<>(100.0)); + + var filter = new AlarmConditionFilter(); + filter.setKey(new AlarmConditionFilterKey(AlarmConditionKeyType.TIME_SERIES, "temperature")); + filter.setValueType(EntityKeyValueType.NUMERIC); + filter.setPredicate(predicate); + + var durationSpec = new DurationAlarmConditionSpec(); + durationSpec.setUnit(TimeUnit.SECONDS); + durationSpec.setPredicate( + new FilterPredicateValue<>(10L, null, new DynamicValue<>(DynamicValueSourceType.CURRENT_DEVICE, "duration", false)) + ); + + var alarmCondition = new AlarmCondition(); + alarmCondition.setCondition(singletonList(filter)); + alarmCondition.setSpec(durationSpec); + + var alarmRule = new AlarmRule(); + alarmRule.setCondition(alarmCondition); + + var dpa = new DeviceProfileAlarm(); + dpa.setId("c4486528-84f2-bd72-589e-2f9a60f89c17"); + dpa.setAlarmType("Test alarm"); + dpa.setCreateRules(new TreeMap<>(singletonMap(AlarmSeverity.CRITICAL, alarmRule))); + + deviceProfileData.setAlarms(singletonList(dpa)); + + // 4. Mock DB calls for keys used in alarm rule + given(timeseriesService.findLatest(tenantId, deviceId, singleton("temperature"))).willReturn(immediateFuture( + List.of(new BasicTsKvEntry(123L, new DoubleDataEntry("temperature", 55.6))) + )); + + given(attributesService.find(tenantId, deviceId, AttributeScope.CLIENT_SCOPE, singleton("duration"))).willReturn(immediateFuture(emptyList())); + given(attributesService.find(tenantId, deviceId, AttributeScope.SHARED_SCOPE, singleton("duration"))).willReturn(immediateFuture(emptyList())); + given(attributesService.find(tenantId, deviceId, AttributeScope.SERVER_SCOPE, singleton("duration"))).willReturn(immediateFuture( + List.of(new BaseAttributeKvEntry(123L, new LongDataEntry("duration", 20L))) + )); + + // 5. Send DEVICE_PROFILE_UPDATE_SELF_MSG so alarm state (inside device state) gets initialized + given(cache.get(tenantId, deviceProfileId)).willReturn(deviceProfile); + + var deviceProfileUpdateMsg = TbMsg.newMsg() + .originator(tenantId) + .type(TbMsgType.DEVICE_PROFILE_UPDATE_SELF_MSG) + .data(deviceProfileId.toString()) + .metaData(TbMsgMetaData.EMPTY) + .build(); + + node.onMsg(ctx, deviceProfileUpdateMsg); + + // 6. Not sending anything else to simulate no activity + + // 7. Simulate periodic alarm harvesting by manually sending DEVICE_PROFILE_PERIODIC_SELF_MSG message + var periodicCheck = TbMsg.newMsg() + .type(TbMsgType.DEVICE_PROFILE_PERIODIC_SELF_MSG) + .originator(tenantId) + .customerId(customerId) + .metaData(TbMsgMetaData.EMPTY) + .data(TbMsg.EMPTY_JSON_OBJECT) + .build(); + + // NPE should NOT happen here: dynamic value of duration condition should be correctly resolved + assertThatNoException().isThrownBy(() -> node.onMsg(ctx, periodicCheck)); + } + @Test public void testInheritTenantAttributeForDuration() throws Exception { init(); @@ -779,11 +895,11 @@ public class TbDeviceProfileNodeTest extends AbstractRuleNodeUpgradeTest { AttributeKvEntry alarmDelayAttributeKvEntry = alarmDelayAttributeKvEntity.toData(); ListenableFuture> optionalDurationAttribute = - Futures.immediateFuture(Optional.of(alarmDelayAttributeKvEntry)); + immediateFuture(Optional.of(alarmDelayAttributeKvEntry)); ListenableFuture> listNoDurationAttribute = - Futures.immediateFuture(Collections.singletonList(entry)); + immediateFuture(singletonList(entry)); ListenableFuture> emptyOptional = - Futures.immediateFuture(Optional.empty()); + immediateFuture(Optional.empty()); AlarmConditionFilter highTempFilter = new AlarmConditionFilter(); highTempFilter.setKey(new AlarmConditionFilterKey(TIME_SERIES, "temperature")); @@ -797,7 +913,7 @@ public class TbDeviceProfileNodeTest extends AbstractRuleNodeUpgradeTest { )); highTempFilter.setPredicate(highTemperaturePredicate); AlarmCondition alarmCondition = new AlarmCondition(); - alarmCondition.setCondition(Collections.singletonList(highTempFilter)); + alarmCondition.setCondition(singletonList(highTempFilter)); FilterPredicateValue filterPredicateValue = new FilterPredicateValue<>( 10L, @@ -815,25 +931,25 @@ public class TbDeviceProfileNodeTest extends AbstractRuleNodeUpgradeTest { DeviceProfileAlarm dpa = new DeviceProfileAlarm(); dpa.setId("highTemperatureAlarmID"); dpa.setAlarmType("highTemperatureAlarm"); - dpa.setCreateRules(new TreeMap<>(Collections.singletonMap(AlarmSeverity.CRITICAL, alarmRule))); + dpa.setCreateRules(new TreeMap<>(singletonMap(AlarmSeverity.CRITICAL, alarmRule))); - deviceProfileData.setAlarms(Collections.singletonList(dpa)); + deviceProfileData.setAlarms(singletonList(dpa)); deviceProfile.setProfileData(deviceProfileData); - Mockito.when(cache.get(tenantId, deviceId)).thenReturn(deviceProfile); - Mockito.when(timeseriesService.findLatest(tenantId, deviceId, Collections.singleton("temperature"))) - .thenReturn(Futures.immediateFuture(Collections.emptyList())); - Mockito.when(alarmService.findLatestActiveByOriginatorAndType(tenantId, deviceId, "highTemperatureAlarm")) + when(cache.get(tenantId, deviceId)).thenReturn(deviceProfile); + when(timeseriesService.findLatest(tenantId, deviceId, singleton("temperature"))) + .thenReturn(immediateFuture(emptyList())); + when(alarmService.findLatestActiveByOriginatorAndType(tenantId, deviceId, "highTemperatureAlarm")) .thenReturn(null); registerCreateAlarmMock(alarmService.createAlarm(any()), true); - Mockito.when(ctx.getAttributesService()).thenReturn(attributesService); - Mockito.when(attributesService.find(eq(tenantId), eq(tenantId), Mockito.any(AttributeScope.class), Mockito.anyString())) + when(ctx.getAttributesService()).thenReturn(attributesService); + when(attributesService.find(eq(tenantId), eq(tenantId), Mockito.any(AttributeScope.class), Mockito.anyString())) .thenReturn(optionalDurationAttribute); - Mockito.when(ctx.getDeviceService().findDeviceById(tenantId, deviceId)) + when(ctx.getDeviceService().findDeviceById(tenantId, deviceId)) .thenReturn(device); - Mockito.when(attributesService.find(eq(tenantId), eq(customerId), eq(AttributeScope.SERVER_SCOPE), Mockito.anyString())) + when(attributesService.find(eq(tenantId), eq(customerId), eq(AttributeScope.SERVER_SCOPE), Mockito.anyString())) .thenReturn(emptyOptional); - Mockito.when(attributesService.find(eq(tenantId), eq(deviceId), Mockito.any(AttributeScope.class), Mockito.anySet())) + when(attributesService.find(eq(tenantId), eq(deviceId), Mockito.any(AttributeScope.class), Mockito.anySet())) .thenReturn(listNoDurationAttribute); TbMsg theMsg = TbMsg.newMsg() @@ -915,7 +1031,7 @@ public class TbDeviceProfileNodeTest extends AbstractRuleNodeUpgradeTest { AttributeKvEntry alarmDelayAttributeKvEntry = alarmDelayAttributeKvEntity.toData(); ListenableFuture> listListenableFuture = - Futures.immediateFuture(Arrays.asList(entry, alarmDelayAttributeKvEntry)); + immediateFuture(Arrays.asList(entry, alarmDelayAttributeKvEntry)); AlarmConditionFilter highTempFilter = new AlarmConditionFilter(); highTempFilter.setKey(new AlarmConditionFilterKey(TIME_SERIES, "temperature")); @@ -929,7 +1045,7 @@ public class TbDeviceProfileNodeTest extends AbstractRuleNodeUpgradeTest { )); highTempFilter.setPredicate(highTemperaturePredicate); AlarmCondition alarmCondition = new AlarmCondition(); - alarmCondition.setCondition(Collections.singletonList(highTempFilter)); + alarmCondition.setCondition(singletonList(highTempFilter)); FilterPredicateValue filterPredicateValue = new FilterPredicateValue<>( 10, @@ -947,19 +1063,19 @@ public class TbDeviceProfileNodeTest extends AbstractRuleNodeUpgradeTest { DeviceProfileAlarm dpa = new DeviceProfileAlarm(); dpa.setId("highTemperatureAlarmID"); dpa.setAlarmType("highTemperatureAlarm"); - dpa.setCreateRules(new TreeMap<>(Collections.singletonMap(AlarmSeverity.CRITICAL, alarmRule))); + dpa.setCreateRules(new TreeMap<>(singletonMap(AlarmSeverity.CRITICAL, alarmRule))); - deviceProfileData.setAlarms(Collections.singletonList(dpa)); + deviceProfileData.setAlarms(singletonList(dpa)); deviceProfile.setProfileData(deviceProfileData); - Mockito.when(cache.get(tenantId, deviceId)).thenReturn(deviceProfile); - Mockito.when(timeseriesService.findLatest(tenantId, deviceId, Collections.singleton("temperature"))) - .thenReturn(Futures.immediateFuture(Collections.emptyList())); - Mockito.when(alarmService.findLatestActiveByOriginatorAndType(tenantId, deviceId, "highTemperatureAlarm")) + when(cache.get(tenantId, deviceId)).thenReturn(deviceProfile); + when(timeseriesService.findLatest(tenantId, deviceId, singleton("temperature"))) + .thenReturn(immediateFuture(emptyList())); + when(alarmService.findLatestActiveByOriginatorAndType(tenantId, deviceId, "highTemperatureAlarm")) .thenReturn(null); registerCreateAlarmMock(alarmService.createAlarm(any()), true); - Mockito.when(ctx.getAttributesService()).thenReturn(attributesService); - Mockito.when(attributesService.find(eq(tenantId), eq(deviceId), Mockito.any(AttributeScope.class), Mockito.anySet())) + when(ctx.getAttributesService()).thenReturn(attributesService); + when(attributesService.find(eq(tenantId), eq(deviceId), Mockito.any(AttributeScope.class), Mockito.anySet())) .thenReturn(listListenableFuture); TbMsg theMsg = TbMsg.newMsg() @@ -1039,11 +1155,11 @@ public class TbDeviceProfileNodeTest extends AbstractRuleNodeUpgradeTest { AttributeKvEntry alarmDelayAttributeKvEntry = alarmDelayAttributeKvEntity.toData(); ListenableFuture> optionalDurationAttribute = - Futures.immediateFuture(Optional.of(alarmDelayAttributeKvEntry)); + immediateFuture(Optional.of(alarmDelayAttributeKvEntry)); ListenableFuture> listNoDurationAttribute = - Futures.immediateFuture(Collections.singletonList(entry)); + immediateFuture(singletonList(entry)); ListenableFuture> emptyOptional = - Futures.immediateFuture(Optional.empty()); + immediateFuture(Optional.empty()); AlarmConditionFilter highTempFilter = new AlarmConditionFilter(); highTempFilter.setKey(new AlarmConditionFilterKey(TIME_SERIES, "temperature")); @@ -1057,7 +1173,7 @@ public class TbDeviceProfileNodeTest extends AbstractRuleNodeUpgradeTest { )); highTempFilter.setPredicate(highTemperaturePredicate); AlarmCondition alarmCondition = new AlarmCondition(); - alarmCondition.setCondition(Collections.singletonList(highTempFilter)); + alarmCondition.setCondition(singletonList(highTempFilter)); FilterPredicateValue filterPredicateValue = new FilterPredicateValue<>( 10, @@ -1074,25 +1190,25 @@ public class TbDeviceProfileNodeTest extends AbstractRuleNodeUpgradeTest { DeviceProfileAlarm dpa = new DeviceProfileAlarm(); dpa.setId("highTemperatureAlarmID"); dpa.setAlarmType("highTemperatureAlarm"); - dpa.setCreateRules(new TreeMap<>(Collections.singletonMap(AlarmSeverity.CRITICAL, alarmRule))); + dpa.setCreateRules(new TreeMap<>(singletonMap(AlarmSeverity.CRITICAL, alarmRule))); - deviceProfileData.setAlarms(Collections.singletonList(dpa)); + deviceProfileData.setAlarms(singletonList(dpa)); deviceProfile.setProfileData(deviceProfileData); - Mockito.when(cache.get(tenantId, deviceId)).thenReturn(deviceProfile); - Mockito.when(timeseriesService.findLatest(tenantId, deviceId, Collections.singleton("temperature"))) - .thenReturn(Futures.immediateFuture(Collections.emptyList())); - Mockito.when(alarmService.findLatestActiveByOriginatorAndType(tenantId, deviceId, "highTemperatureAlarm")) + when(cache.get(tenantId, deviceId)).thenReturn(deviceProfile); + when(timeseriesService.findLatest(tenantId, deviceId, singleton("temperature"))) + .thenReturn(immediateFuture(emptyList())); + when(alarmService.findLatestActiveByOriginatorAndType(tenantId, deviceId, "highTemperatureAlarm")) .thenReturn(null); registerCreateAlarmMock(alarmService.createAlarm(any()), true); - Mockito.when(ctx.getAttributesService()).thenReturn(attributesService); - Mockito.when(attributesService.find(eq(tenantId), eq(tenantId), Mockito.any(AttributeScope.class), Mockito.anyString())) + when(ctx.getAttributesService()).thenReturn(attributesService); + when(attributesService.find(eq(tenantId), eq(tenantId), Mockito.any(AttributeScope.class), Mockito.anyString())) .thenReturn(optionalDurationAttribute); - Mockito.when(ctx.getDeviceService().findDeviceById(tenantId, deviceId)) + when(ctx.getDeviceService().findDeviceById(tenantId, deviceId)) .thenReturn(device); - Mockito.when(attributesService.find(eq(tenantId), eq(customerId), eq(AttributeScope.SERVER_SCOPE), Mockito.anyString())) + when(attributesService.find(eq(tenantId), eq(customerId), eq(AttributeScope.SERVER_SCOPE), Mockito.anyString())) .thenReturn(emptyOptional); - Mockito.when(attributesService.find(eq(tenantId), eq(deviceId), Mockito.any(AttributeScope.class), Mockito.anySet())) + when(attributesService.find(eq(tenantId), eq(deviceId), Mockito.any(AttributeScope.class), Mockito.anySet())) .thenReturn(listNoDurationAttribute); TbMsg theMsg = TbMsg.newMsg() @@ -1160,7 +1276,7 @@ public class TbDeviceProfileNodeTest extends AbstractRuleNodeUpgradeTest { AttributeKvEntry entry = attributeKvEntity.toData(); ListenableFuture> listListenableFuture = - Futures.immediateFuture(Collections.singletonList(entry)); + immediateFuture(singletonList(entry)); AlarmConditionFilter highTempFilter = new AlarmConditionFilter(); highTempFilter.setKey(new AlarmConditionFilterKey(TIME_SERIES, "temperature")); @@ -1174,7 +1290,7 @@ public class TbDeviceProfileNodeTest extends AbstractRuleNodeUpgradeTest { )); highTempFilter.setPredicate(highTemperaturePredicate); AlarmCondition alarmCondition = new AlarmCondition(); - alarmCondition.setCondition(Collections.singletonList(highTempFilter)); + alarmCondition.setCondition(singletonList(highTempFilter)); FilterPredicateValue filterPredicateValue = new FilterPredicateValue<>( alarmDelayInSeconds, @@ -1192,19 +1308,19 @@ public class TbDeviceProfileNodeTest extends AbstractRuleNodeUpgradeTest { DeviceProfileAlarm dpa = new DeviceProfileAlarm(); dpa.setId("highTemperatureAlarmID"); dpa.setAlarmType("highTemperatureAlarm"); - dpa.setCreateRules(new TreeMap<>(Collections.singletonMap(AlarmSeverity.CRITICAL, alarmRule))); + dpa.setCreateRules(new TreeMap<>(singletonMap(AlarmSeverity.CRITICAL, alarmRule))); - deviceProfileData.setAlarms(Collections.singletonList(dpa)); + deviceProfileData.setAlarms(singletonList(dpa)); deviceProfile.setProfileData(deviceProfileData); - Mockito.when(cache.get(tenantId, deviceId)).thenReturn(deviceProfile); - Mockito.when(timeseriesService.findLatest(tenantId, deviceId, Collections.singleton("temperature"))) - .thenReturn(Futures.immediateFuture(Collections.emptyList())); - Mockito.when(alarmService.findLatestActiveByOriginatorAndType(tenantId, deviceId, "highTemperatureAlarm")) + when(cache.get(tenantId, deviceId)).thenReturn(deviceProfile); + when(timeseriesService.findLatest(tenantId, deviceId, singleton("temperature"))) + .thenReturn(immediateFuture(emptyList())); + when(alarmService.findLatestActiveByOriginatorAndType(tenantId, deviceId, "highTemperatureAlarm")) .thenReturn(null); registerCreateAlarmMock(alarmService.createAlarm(any()), true); - Mockito.when(ctx.getAttributesService()).thenReturn(attributesService); - Mockito.when(attributesService.find(eq(tenantId), eq(deviceId), Mockito.any(AttributeScope.class), Mockito.anySet())) + when(ctx.getAttributesService()).thenReturn(attributesService); + when(attributesService.find(eq(tenantId), eq(deviceId), Mockito.any(AttributeScope.class), Mockito.anySet())) .thenReturn(listListenableFuture); TbMsg theMsg = TbMsg.newMsg() @@ -1277,7 +1393,7 @@ public class TbDeviceProfileNodeTest extends AbstractRuleNodeUpgradeTest { AttributeKvEntry entry = attributeKvEntity.toData(); ListenableFuture> listListenableFuture = - Futures.immediateFuture(Collections.singletonList(entry)); + immediateFuture(singletonList(entry)); AlarmConditionFilter highTempFilter = new AlarmConditionFilter(); highTempFilter.setKey(new AlarmConditionFilterKey(TIME_SERIES, "temperature")); @@ -1291,7 +1407,7 @@ public class TbDeviceProfileNodeTest extends AbstractRuleNodeUpgradeTest { )); highTempFilter.setPredicate(highTemperaturePredicate); AlarmCondition alarmCondition = new AlarmCondition(); - alarmCondition.setCondition(Collections.singletonList(highTempFilter)); + alarmCondition.setCondition(singletonList(highTempFilter)); RepeatingAlarmConditionSpec repeating = new RepeatingAlarmConditionSpec(); repeating.setPredicate(new FilterPredicateValue<>( @@ -1306,19 +1422,19 @@ public class TbDeviceProfileNodeTest extends AbstractRuleNodeUpgradeTest { DeviceProfileAlarm dpa = new DeviceProfileAlarm(); dpa.setId("highTemperatureAlarmID"); dpa.setAlarmType("highTemperatureAlarm"); - dpa.setCreateRules(new TreeMap<>(Collections.singletonMap(AlarmSeverity.CRITICAL, alarmRule))); + dpa.setCreateRules(new TreeMap<>(singletonMap(AlarmSeverity.CRITICAL, alarmRule))); - deviceProfileData.setAlarms(Collections.singletonList(dpa)); + deviceProfileData.setAlarms(singletonList(dpa)); deviceProfile.setProfileData(deviceProfileData); - Mockito.when(cache.get(tenantId, deviceId)).thenReturn(deviceProfile); - Mockito.when(timeseriesService.findLatest(tenantId, deviceId, Collections.singleton("temperature"))) - .thenReturn(Futures.immediateFuture(Collections.emptyList())); - Mockito.when(alarmService.findLatestActiveByOriginatorAndType(tenantId, deviceId, "highTemperatureAlarm")) + when(cache.get(tenantId, deviceId)).thenReturn(deviceProfile); + when(timeseriesService.findLatest(tenantId, deviceId, singleton("temperature"))) + .thenReturn(immediateFuture(emptyList())); + when(alarmService.findLatestActiveByOriginatorAndType(tenantId, deviceId, "highTemperatureAlarm")) .thenReturn(null); registerCreateAlarmMock(alarmService.createAlarm(any()), true); - Mockito.when(ctx.getAttributesService()).thenReturn(attributesService); - Mockito.when(attributesService.find(eq(tenantId), eq(deviceId), Mockito.any(AttributeScope.class), Mockito.anySet())) + when(ctx.getAttributesService()).thenReturn(attributesService); + when(attributesService.find(eq(tenantId), eq(deviceId), Mockito.any(AttributeScope.class), Mockito.anySet())) .thenReturn(listListenableFuture); TbMsg theMsg = TbMsg.newMsg() @@ -1373,7 +1489,7 @@ public class TbDeviceProfileNodeTest extends AbstractRuleNodeUpgradeTest { AttributeKvEntry entryActiveSchedule = attributeKvEntityActiveSchedule.toData(); ListenableFuture> listListenableFutureActiveSchedule = - Futures.immediateFuture(Collections.singletonList(entryActiveSchedule)); + immediateFuture(singletonList(entryActiveSchedule)); AlarmConditionFilter highTempFilter = new AlarmConditionFilter(); highTempFilter.setKey(new AlarmConditionFilterKey(TIME_SERIES, "temperature")); @@ -1387,10 +1503,10 @@ public class TbDeviceProfileNodeTest extends AbstractRuleNodeUpgradeTest { )); highTempFilter.setPredicate(highTemperaturePredicate); AlarmCondition alarmCondition = new AlarmCondition(); - alarmCondition.setCondition(Collections.singletonList(highTempFilter)); + alarmCondition.setCondition(singletonList(highTempFilter)); CustomTimeSchedule schedule = new CustomTimeSchedule(); - schedule.setItems(Collections.emptyList()); + schedule.setItems(emptyList()); schedule.setDynamicValue(new DynamicValue<>(DynamicValueSourceType.CURRENT_DEVICE, "dynamicValueActiveSchedule", false)); AlarmRule alarmRule = new AlarmRule(); @@ -1399,19 +1515,19 @@ public class TbDeviceProfileNodeTest extends AbstractRuleNodeUpgradeTest { DeviceProfileAlarm deviceProfileAlarmActiveSchedule = new DeviceProfileAlarm(); deviceProfileAlarmActiveSchedule.setId("highTemperatureAlarmID"); deviceProfileAlarmActiveSchedule.setAlarmType("highTemperatureAlarm"); - deviceProfileAlarmActiveSchedule.setCreateRules(new TreeMap<>(Collections.singletonMap(AlarmSeverity.CRITICAL, alarmRule))); + deviceProfileAlarmActiveSchedule.setCreateRules(new TreeMap<>(singletonMap(AlarmSeverity.CRITICAL, alarmRule))); - deviceProfileData.setAlarms(Collections.singletonList(deviceProfileAlarmActiveSchedule)); + deviceProfileData.setAlarms(singletonList(deviceProfileAlarmActiveSchedule)); deviceProfile.setProfileData(deviceProfileData); - Mockito.when(cache.get(tenantId, deviceId)).thenReturn(deviceProfile); - Mockito.when(timeseriesService.findLatest(tenantId, deviceId, Collections.singleton("temperature"))) - .thenReturn(Futures.immediateFuture(Collections.emptyList())); - Mockito.when(alarmService.findLatestActiveByOriginatorAndType(tenantId, deviceId, "highTemperatureAlarm")) + when(cache.get(tenantId, deviceId)).thenReturn(deviceProfile); + when(timeseriesService.findLatest(tenantId, deviceId, singleton("temperature"))) + .thenReturn(immediateFuture(emptyList())); + when(alarmService.findLatestActiveByOriginatorAndType(tenantId, deviceId, "highTemperatureAlarm")) .thenReturn(null); registerCreateAlarmMock(alarmService.createAlarm(any()), true); - Mockito.when(ctx.getAttributesService()).thenReturn(attributesService); - Mockito.when(attributesService.find(eq(tenantId), eq(deviceId), Mockito.any(AttributeScope.class), Mockito.anySet())) + when(ctx.getAttributesService()).thenReturn(attributesService); + when(attributesService.find(eq(tenantId), eq(deviceId), Mockito.any(AttributeScope.class), Mockito.anySet())) .thenReturn(listListenableFutureActiveSchedule); TbMsg theMsg = TbMsg.newMsg() @@ -1470,7 +1586,7 @@ public class TbDeviceProfileNodeTest extends AbstractRuleNodeUpgradeTest { AttributeKvEntry entryInactiveSchedule = attributeKvEntityInactiveSchedule.toData(); ListenableFuture> listListenableFutureInactiveSchedule = - Futures.immediateFuture(Collections.singletonList(entryInactiveSchedule)); + immediateFuture(singletonList(entryInactiveSchedule)); AlarmConditionFilter highTempFilter = new AlarmConditionFilter(); highTempFilter.setKey(new AlarmConditionFilterKey(TIME_SERIES, "temperature")); @@ -1485,7 +1601,7 @@ public class TbDeviceProfileNodeTest extends AbstractRuleNodeUpgradeTest { highTempFilter.setPredicate(highTemperaturePredicate); AlarmCondition alarmCondition = new AlarmCondition(); - alarmCondition.setCondition(Collections.singletonList(highTempFilter)); + alarmCondition.setCondition(singletonList(highTempFilter)); CustomTimeSchedule schedule = new CustomTimeSchedule(); @@ -1508,18 +1624,18 @@ public class TbDeviceProfileNodeTest extends AbstractRuleNodeUpgradeTest { DeviceProfileAlarm deviceProfileAlarmNonactiveSchedule = new DeviceProfileAlarm(); deviceProfileAlarmNonactiveSchedule.setId("highTemperatureAlarmID"); deviceProfileAlarmNonactiveSchedule.setAlarmType("highTemperatureAlarm"); - deviceProfileAlarmNonactiveSchedule.setCreateRules(new TreeMap<>(Collections.singletonMap(AlarmSeverity.CRITICAL, alarmRule))); + deviceProfileAlarmNonactiveSchedule.setCreateRules(new TreeMap<>(singletonMap(AlarmSeverity.CRITICAL, alarmRule))); - deviceProfileData.setAlarms(Collections.singletonList(deviceProfileAlarmNonactiveSchedule)); + deviceProfileData.setAlarms(singletonList(deviceProfileAlarmNonactiveSchedule)); deviceProfile.setProfileData(deviceProfileData); - Mockito.when(cache.get(tenantId, deviceId)).thenReturn(deviceProfile); - Mockito.when(timeseriesService.findLatest(tenantId, deviceId, Collections.singleton("temperature"))) - .thenReturn(Futures.immediateFuture(Collections.emptyList())); - Mockito.when(alarmService.findLatestActiveByOriginatorAndType(tenantId, deviceId, "highTemperatureAlarm")) + when(cache.get(tenantId, deviceId)).thenReturn(deviceProfile); + when(timeseriesService.findLatest(tenantId, deviceId, singleton("temperature"))) + .thenReturn(immediateFuture(emptyList())); + when(alarmService.findLatestActiveByOriginatorAndType(tenantId, deviceId, "highTemperatureAlarm")) .thenReturn(null); - Mockito.when(ctx.getAttributesService()).thenReturn(attributesService); - Mockito.when(attributesService.find(eq(tenantId), eq(deviceId), Mockito.any(AttributeScope.class), Mockito.anySet())) + when(ctx.getAttributesService()).thenReturn(attributesService); + when(attributesService.find(eq(tenantId), eq(deviceId), Mockito.any(AttributeScope.class), Mockito.anySet())) .thenReturn(listListenableFutureInactiveSchedule); TbMsg theMsg = TbMsg.newMsg() @@ -1569,9 +1685,9 @@ public class TbDeviceProfileNodeTest extends AbstractRuleNodeUpgradeTest { AttributeKvEntry entry = attributeKvEntity.toData(); ListenableFuture> listListenableFutureWithLess = - Futures.immediateFuture(Collections.emptyList()); + immediateFuture(emptyList()); ListenableFuture> optionalListenableFutureWithLess = - Futures.immediateFuture(Optional.of(entry)); + immediateFuture(Optional.of(entry)); AlarmConditionFilter lowTempFilter = new AlarmConditionFilter(); lowTempFilter.setKey(new AlarmConditionFilterKey(TIME_SERIES, "temperature")); @@ -1586,29 +1702,29 @@ public class TbDeviceProfileNodeTest extends AbstractRuleNodeUpgradeTest { ); lowTempFilter.setPredicate(lowTempPredicate); AlarmCondition alarmCondition = new AlarmCondition(); - alarmCondition.setCondition(Collections.singletonList(lowTempFilter)); + alarmCondition.setCondition(singletonList(lowTempFilter)); AlarmRule alarmRule = new AlarmRule(); alarmRule.setCondition(alarmCondition); DeviceProfileAlarm dpa = new DeviceProfileAlarm(); dpa.setId("lesstempID"); dpa.setAlarmType("lessTemperatureAlarm"); - dpa.setCreateRules(new TreeMap<>(Collections.singletonMap(AlarmSeverity.CRITICAL, alarmRule))); + dpa.setCreateRules(new TreeMap<>(singletonMap(AlarmSeverity.CRITICAL, alarmRule))); - deviceProfileData.setAlarms(Collections.singletonList(dpa)); + deviceProfileData.setAlarms(singletonList(dpa)); deviceProfile.setProfileData(deviceProfileData); - Mockito.when(cache.get(tenantId, deviceId)).thenReturn(deviceProfile); - Mockito.when(timeseriesService.findLatest(tenantId, deviceId, Collections.singleton("temperature"))) - .thenReturn(Futures.immediateFuture(Collections.emptyList())); - Mockito.when(alarmService.findLatestActiveByOriginatorAndType(tenantId, deviceId, "lessTemperatureAlarm")) + when(cache.get(tenantId, deviceId)).thenReturn(deviceProfile); + when(timeseriesService.findLatest(tenantId, deviceId, singleton("temperature"))) + .thenReturn(immediateFuture(emptyList())); + when(alarmService.findLatestActiveByOriginatorAndType(tenantId, deviceId, "lessTemperatureAlarm")) .thenReturn(null); registerCreateAlarmMock(alarmService.createAlarm(any()), true); - Mockito.when(ctx.getAttributesService()).thenReturn(attributesService); - Mockito.when(attributesService.find(eq(tenantId), eq(deviceId), Mockito.any(AttributeScope.class), Mockito.anySet())) + when(ctx.getAttributesService()).thenReturn(attributesService); + when(attributesService.find(eq(tenantId), eq(deviceId), Mockito.any(AttributeScope.class), Mockito.anySet())) .thenReturn(listListenableFutureWithLess); - Mockito.when(ctx.getDeviceService().findDeviceById(tenantId, deviceId)) + when(ctx.getDeviceService().findDeviceById(tenantId, deviceId)) .thenReturn(device); - Mockito.when(attributesService.find(eq(tenantId), eq(customerId), eq(AttributeScope.SERVER_SCOPE), Mockito.anyString())) + when(attributesService.find(eq(tenantId), eq(customerId), eq(AttributeScope.SERVER_SCOPE), Mockito.anyString())) .thenReturn(optionalListenableFutureWithLess); TbMsg theMsg = TbMsg.newMsg() @@ -1655,9 +1771,9 @@ public class TbDeviceProfileNodeTest extends AbstractRuleNodeUpgradeTest { AttributeKvEntry entry = attributeKvEntity.toData(); ListenableFuture> listListenableFutureWithLess = - Futures.immediateFuture(Collections.emptyList()); + immediateFuture(emptyList()); ListenableFuture> optionalListenableFutureWithLess = - Futures.immediateFuture(Optional.of(entry)); + immediateFuture(Optional.of(entry)); AlarmConditionFilter lowTempFilter = new AlarmConditionFilter(); lowTempFilter.setKey(new AlarmConditionFilterKey(TIME_SERIES, "temperature")); @@ -1672,27 +1788,27 @@ public class TbDeviceProfileNodeTest extends AbstractRuleNodeUpgradeTest { ); lowTempFilter.setPredicate(lowTempPredicate); AlarmCondition alarmCondition = new AlarmCondition(); - alarmCondition.setCondition(Collections.singletonList(lowTempFilter)); + alarmCondition.setCondition(singletonList(lowTempFilter)); AlarmRule alarmRule = new AlarmRule(); alarmRule.setCondition(alarmCondition); DeviceProfileAlarm dpa = new DeviceProfileAlarm(); dpa.setId("lesstempID"); dpa.setAlarmType("lessTemperatureAlarm"); - dpa.setCreateRules(new TreeMap<>(Collections.singletonMap(AlarmSeverity.CRITICAL, alarmRule))); + dpa.setCreateRules(new TreeMap<>(singletonMap(AlarmSeverity.CRITICAL, alarmRule))); - deviceProfileData.setAlarms(Collections.singletonList(dpa)); + deviceProfileData.setAlarms(singletonList(dpa)); deviceProfile.setProfileData(deviceProfileData); - Mockito.when(cache.get(tenantId, deviceId)).thenReturn(deviceProfile); - Mockito.when(timeseriesService.findLatest(tenantId, deviceId, Collections.singleton("temperature"))) - .thenReturn(Futures.immediateFuture(Collections.emptyList())); - Mockito.when(alarmService.findLatestActiveByOriginatorAndType(tenantId, deviceId, "lessTemperatureAlarm")) + when(cache.get(tenantId, deviceId)).thenReturn(deviceProfile); + when(timeseriesService.findLatest(tenantId, deviceId, singleton("temperature"))) + .thenReturn(immediateFuture(emptyList())); + when(alarmService.findLatestActiveByOriginatorAndType(tenantId, deviceId, "lessTemperatureAlarm")) .thenReturn(null); registerCreateAlarmMock(alarmService.createAlarm(any()), true); - Mockito.when(ctx.getAttributesService()).thenReturn(attributesService); - Mockito.when(attributesService.find(eq(tenantId), eq(deviceId), Mockito.any(AttributeScope.class), Mockito.anySet())) + when(ctx.getAttributesService()).thenReturn(attributesService); + when(attributesService.find(eq(tenantId), eq(deviceId), Mockito.any(AttributeScope.class), Mockito.anySet())) .thenReturn(listListenableFutureWithLess); - Mockito.when(attributesService.find(eq(tenantId), eq(tenantId), eq(AttributeScope.SERVER_SCOPE), Mockito.anyString())) + when(attributesService.find(eq(tenantId), eq(tenantId), eq(AttributeScope.SERVER_SCOPE), Mockito.anyString())) .thenReturn(optionalListenableFutureWithLess); TbMsg theMsg = TbMsg.newMsg() @@ -1743,11 +1859,11 @@ public class TbDeviceProfileNodeTest extends AbstractRuleNodeUpgradeTest { AttributeKvEntry entry = attributeKvEntity.toData(); ListenableFuture> listListenableFutureWithLess = - Futures.immediateFuture(Collections.emptyList()); + immediateFuture(emptyList()); ListenableFuture> emptyOptionalFuture = - Futures.immediateFuture(Optional.empty()); + immediateFuture(Optional.empty()); ListenableFuture> optionalListenableFutureWithLess = - Futures.immediateFuture(Optional.of(entry)); + immediateFuture(Optional.of(entry)); AlarmConditionFilter lowTempFilter = new AlarmConditionFilter(); lowTempFilter.setKey(new AlarmConditionFilterKey(TIME_SERIES, "temperature")); @@ -1762,31 +1878,31 @@ public class TbDeviceProfileNodeTest extends AbstractRuleNodeUpgradeTest { ); lowTempFilter.setPredicate(lowTempPredicate); AlarmCondition alarmCondition = new AlarmCondition(); - alarmCondition.setCondition(Collections.singletonList(lowTempFilter)); + alarmCondition.setCondition(singletonList(lowTempFilter)); AlarmRule alarmRule = new AlarmRule(); alarmRule.setCondition(alarmCondition); DeviceProfileAlarm dpa = new DeviceProfileAlarm(); dpa.setId("lesstempID"); dpa.setAlarmType("lessTemperatureAlarm"); - dpa.setCreateRules(new TreeMap<>(Collections.singletonMap(AlarmSeverity.CRITICAL, alarmRule))); + dpa.setCreateRules(new TreeMap<>(singletonMap(AlarmSeverity.CRITICAL, alarmRule))); - deviceProfileData.setAlarms(Collections.singletonList(dpa)); + deviceProfileData.setAlarms(singletonList(dpa)); deviceProfile.setProfileData(deviceProfileData); - Mockito.when(cache.get(tenantId, deviceId)).thenReturn(deviceProfile); - Mockito.when(timeseriesService.findLatest(tenantId, deviceId, Collections.singleton("temperature"))) - .thenReturn(Futures.immediateFuture(Collections.emptyList())); - Mockito.when(alarmService.findLatestActiveByOriginatorAndType(tenantId, deviceId, "lessTemperatureAlarm")) + when(cache.get(tenantId, deviceId)).thenReturn(deviceProfile); + when(timeseriesService.findLatest(tenantId, deviceId, singleton("temperature"))) + .thenReturn(immediateFuture(emptyList())); + when(alarmService.findLatestActiveByOriginatorAndType(tenantId, deviceId, "lessTemperatureAlarm")) .thenReturn(null); registerCreateAlarmMock(alarmService.createAlarm(any()), true); - Mockito.when(ctx.getAttributesService()).thenReturn(attributesService); - Mockito.when(ctx.getDeviceService().findDeviceById(tenantId, deviceId)) + when(ctx.getAttributesService()).thenReturn(attributesService); + when(ctx.getDeviceService().findDeviceById(tenantId, deviceId)) .thenReturn(device); - Mockito.when(attributesService.find(eq(tenantId), eq(deviceId), Mockito.any(AttributeScope.class), Mockito.anySet())) + when(attributesService.find(eq(tenantId), eq(deviceId), Mockito.any(AttributeScope.class), Mockito.anySet())) .thenReturn(listListenableFutureWithLess); - Mockito.when(attributesService.find(eq(tenantId), eq(customerId), Mockito.any(AttributeScope.class), Mockito.anyString())) + when(attributesService.find(eq(tenantId), eq(customerId), Mockito.any(AttributeScope.class), Mockito.anyString())) .thenReturn(emptyOptionalFuture); - Mockito.when(attributesService.find(eq(tenantId), eq(tenantId), eq(AttributeScope.SERVER_SCOPE), Mockito.anyString())) + when(attributesService.find(eq(tenantId), eq(tenantId), eq(AttributeScope.SERVER_SCOPE), Mockito.anyString())) .thenReturn(optionalListenableFutureWithLess); TbMsg theMsg = TbMsg.newMsg() @@ -1839,11 +1955,11 @@ public class TbDeviceProfileNodeTest extends AbstractRuleNodeUpgradeTest { AttributeKvEntry entry = attributeKvEntity.toData(); ListenableFuture> listListenableFutureWithLess = - Futures.immediateFuture(Collections.emptyList()); + immediateFuture(emptyList()); ListenableFuture> emptyOptionalFuture = - Futures.immediateFuture(Optional.empty()); + immediateFuture(Optional.empty()); ListenableFuture> optionalListenableFutureWithLess = - Futures.immediateFuture(Optional.of(entry)); + immediateFuture(Optional.of(entry)); AlarmConditionFilter lowTempFilter = new AlarmConditionFilter(); lowTempFilter.setKey(new AlarmConditionFilterKey(TIME_SERIES, "temperature")); @@ -1858,31 +1974,31 @@ public class TbDeviceProfileNodeTest extends AbstractRuleNodeUpgradeTest { ); lowTempFilter.setPredicate(lowTempPredicate); AlarmCondition alarmCondition = new AlarmCondition(); - alarmCondition.setCondition(Collections.singletonList(lowTempFilter)); + alarmCondition.setCondition(singletonList(lowTempFilter)); AlarmRule alarmRule = new AlarmRule(); alarmRule.setCondition(alarmCondition); DeviceProfileAlarm dpa = new DeviceProfileAlarm(); dpa.setId("lesstempID"); dpa.setAlarmType("greaterTemperatureAlarm"); - dpa.setCreateRules(new TreeMap<>(Collections.singletonMap(AlarmSeverity.CRITICAL, alarmRule))); + dpa.setCreateRules(new TreeMap<>(singletonMap(AlarmSeverity.CRITICAL, alarmRule))); - deviceProfileData.setAlarms(Collections.singletonList(dpa)); + deviceProfileData.setAlarms(singletonList(dpa)); deviceProfile.setProfileData(deviceProfileData); - Mockito.when(cache.get(tenantId, deviceId)).thenReturn(deviceProfile); - Mockito.when(timeseriesService.findLatest(tenantId, deviceId, Collections.singleton("temperature"))) - .thenReturn(Futures.immediateFuture(Collections.emptyList())); - Mockito.when(alarmService.findLatestActiveByOriginatorAndType(tenantId, deviceId, "greaterTemperatureAlarm")) + when(cache.get(tenantId, deviceId)).thenReturn(deviceProfile); + when(timeseriesService.findLatest(tenantId, deviceId, singleton("temperature"))) + .thenReturn(immediateFuture(emptyList())); + when(alarmService.findLatestActiveByOriginatorAndType(tenantId, deviceId, "greaterTemperatureAlarm")) .thenReturn(null); registerCreateAlarmMock(alarmService.createAlarm(any()), true); - Mockito.when(ctx.getAttributesService()).thenReturn(attributesService); - Mockito.when(ctx.getDeviceService().findDeviceById(tenantId, deviceId)) + when(ctx.getAttributesService()).thenReturn(attributesService); + when(ctx.getDeviceService().findDeviceById(tenantId, deviceId)) .thenReturn(device); - Mockito.when(attributesService.find(eq(tenantId), eq(deviceId), Mockito.any(AttributeScope.class), Mockito.anySet())) + when(attributesService.find(eq(tenantId), eq(deviceId), Mockito.any(AttributeScope.class), Mockito.anySet())) .thenReturn(listListenableFutureWithLess); - Mockito.when(attributesService.find(eq(tenantId), eq(customerId), Mockito.any(AttributeScope.class), Mockito.anyString())) + when(attributesService.find(eq(tenantId), eq(customerId), Mockito.any(AttributeScope.class), Mockito.anyString())) .thenReturn(emptyOptionalFuture); - Mockito.when(attributesService.find(eq(tenantId), eq(tenantId), eq(AttributeScope.SERVER_SCOPE), Mockito.anyString())) + when(attributesService.find(eq(tenantId), eq(tenantId), eq(AttributeScope.SERVER_SCOPE), Mockito.anyString())) .thenReturn(optionalListenableFutureWithLess); TbMsg theMsg = TbMsg.newMsg() @@ -1911,15 +2027,12 @@ public class TbDeviceProfileNodeTest extends AbstractRuleNodeUpgradeTest { } private void init() throws TbNodeException { - Mockito.when(ctx.getTenantId()).thenReturn(tenantId); - Mockito.when(ctx.getDeviceProfileCache()).thenReturn(cache); - Mockito.lenient().when(ctx.getTimeseriesService()).thenReturn(timeseriesService); - Mockito.lenient().when(ctx.getAlarmService()).thenReturn(alarmService); - Mockito.when(ctx.getDeviceService()).thenReturn(deviceService); - Mockito.lenient().when(ctx.getAttributesService()).thenReturn(attributesService); - TbNodeConfiguration nodeConfiguration = new TbNodeConfiguration(JacksonUtil.newObjectNode()); + var config = new TbDeviceProfileNodeConfiguration(); + config.setFetchAlarmRulesStateOnStart(false); + config.setPersistAlarmRulesState(false); + node = new TbDeviceProfileNode(); - node.init(ctx, nodeConfiguration); + node.init(ctx, new TbNodeConfiguration(JacksonUtil.valueToTree(config))); } private void registerCreateAlarmMock(AlarmApiCallResult a, boolean created) { @@ -1991,23 +2104,23 @@ public class TbDeviceProfileNodeTest extends AbstractRuleNodeUpgradeTest { DeviceProfileAlarm dpa = new DeviceProfileAlarm(); dpa.setId("highTemperatureAlarmID"); dpa.setAlarmType("highTemperatureAlarm"); - dpa.setCreateRules(new TreeMap<>(Collections.singletonMap(AlarmSeverity.CRITICAL, createRule))); + dpa.setCreateRules(new TreeMap<>(singletonMap(AlarmSeverity.CRITICAL, createRule))); dpa.setClearRule(clearRule); - deviceProfileData.setAlarms(Collections.singletonList(dpa)); + deviceProfileData.setAlarms(singletonList(dpa)); deviceProfile.setProfileData(deviceProfileData); ListenableFuture> tsKvList = - Futures.immediateFuture(Collections.singletonList(getTsKvEntry("temperature", 35L))); + immediateFuture(singletonList(getTsKvEntry("temperature", 35L))); ListenableFuture> attrList = - Futures.immediateFuture(Collections.emptyList()); + immediateFuture(emptyList()); - Mockito.when(cache.get(tenantId, deviceId)).thenReturn(deviceProfile); - Mockito.when(timeseriesService.findLatest(tenantId, deviceId, Collections.singleton("temperature"))) + when(cache.get(tenantId, deviceId)).thenReturn(deviceProfile); + when(timeseriesService.findLatest(tenantId, deviceId, singleton("temperature"))) .thenReturn(tsKvList); - Mockito.when(attributesService.find(eq(tenantId), eq(deviceId), any(), anySet())) + when(attributesService.find(eq(tenantId), eq(deviceId), any(), anySet())) .thenReturn(attrList); - Mockito.when(alarmService.findLatestActiveByOriginatorAndType(tenantId, deviceId, "highTemperatureAlarm")).thenReturn(null); + when(alarmService.findLatestActiveByOriginatorAndType(tenantId, deviceId, "highTemperatureAlarm")).thenReturn(null); registerCreateAlarmMock(alarmService.createAlarm(any()), true); TbMsg theMsg = TbMsg.newMsg() @@ -2046,7 +2159,7 @@ public class TbDeviceProfileNodeTest extends AbstractRuleNodeUpgradeTest { private AlarmCondition getNumericAlarmCondition(AlarmConditionKeyType alarmConditionKeyType, String key, NumericOperation operation, Double value) { AlarmConditionFilter filter = getAlarmConditionFilter(alarmConditionKeyType, key, operation, value); AlarmCondition alarmCondition = new AlarmCondition(); - alarmCondition.setCondition(Collections.singletonList(filter)); + alarmCondition.setCondition(singletonList(filter)); return alarmCondition; } @@ -2076,13 +2189,13 @@ public class TbDeviceProfileNodeTest extends AbstractRuleNodeUpgradeTest { highTemperaturePredicate.setValue(new FilterPredicateValue<>(30.0)); highTempFilter.setPredicate(highTemperaturePredicate); AlarmCondition alarmCondition = new AlarmCondition(); - alarmCondition.setCondition(Collections.singletonList(highTempFilter)); + alarmCondition.setCondition(singletonList(highTempFilter)); AlarmRule alarmRule = new AlarmRule(); alarmRule.setCondition(alarmCondition); DeviceProfileAlarm dpa = new DeviceProfileAlarm(); dpa.setId("highTemperatureAlarmID"); dpa.setAlarmType("highTemperatureAlarm"); - dpa.setCreateRules(new TreeMap<>(Collections.singletonMap(AlarmSeverity.CRITICAL, alarmRule))); + dpa.setCreateRules(new TreeMap<>(singletonMap(AlarmSeverity.CRITICAL, alarmRule))); AlarmConditionFilter lowTempFilter = new AlarmConditionFilter(); lowTempFilter.setKey(new AlarmConditionFilterKey(AlarmConditionKeyType.TIME_SERIES, "temperature")); @@ -2093,17 +2206,17 @@ public class TbDeviceProfileNodeTest extends AbstractRuleNodeUpgradeTest { lowTempFilter.setPredicate(lowTemperaturePredicate); AlarmRule clearRule = new AlarmRule(); AlarmCondition clearCondition = new AlarmCondition(); - clearCondition.setCondition(Collections.singletonList(lowTempFilter)); + clearCondition.setCondition(singletonList(lowTempFilter)); clearRule.setCondition(clearCondition); dpa.setClearRule(clearRule); - deviceProfileData.setAlarms(Collections.singletonList(dpa)); + deviceProfileData.setAlarms(singletonList(dpa)); deviceProfile.setProfileData(deviceProfileData); - Mockito.when(cache.get(tenantId, deviceId)).thenReturn(deviceProfile); - Mockito.when(timeseriesService.findLatest(tenantId, deviceId, Collections.singleton("temperature"))) - .thenReturn(Futures.immediateFuture(Collections.emptyList())); - Mockito.when(alarmService.findLatestActiveByOriginatorAndType(tenantId, deviceId, "highTemperatureAlarm")).thenReturn(null); + when(cache.get(tenantId, deviceId)).thenReturn(deviceProfile); + when(timeseriesService.findLatest(tenantId, deviceId, singleton("temperature"))) + .thenReturn(immediateFuture(emptyList())); + when(alarmService.findLatestActiveByOriginatorAndType(tenantId, deviceId, "highTemperatureAlarm")).thenReturn(null); registerCreateAlarmMock(alarmService.createAlarm(any()), true); TbMsg theMsg = TbMsg.newMsg() diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNodeTest.java index 4826cfe297..da8816e824 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNodeTest.java @@ -15,8 +15,10 @@ */ package org.thingsboard.rule.engine.transform; +import com.google.common.util.concurrent.FluentFuture; import com.google.common.util.concurrent.Futures; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.params.ParameterizedTest; @@ -35,6 +37,7 @@ import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.rule.engine.api.util.TbNodeUtils; import org.thingsboard.rule.engine.data.RelationsQuery; +import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.alarm.Alarm; @@ -54,21 +57,24 @@ import org.thingsboard.server.common.data.relation.RelationsSearchParameters; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.dao.asset.AssetService; -import org.thingsboard.server.dao.device.DeviceService; +import org.thingsboard.server.dao.entity.EntityService; import org.thingsboard.server.dao.relation.RelationService; import java.util.Collections; import java.util.Map; import java.util.NoSuchElementException; +import java.util.Optional; import java.util.UUID; import java.util.stream.Stream; +import static com.google.common.util.concurrent.Futures.immediateFuture; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.BDDMockito.given; import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.lenient; import static org.thingsboard.rule.engine.transform.OriginatorSource.ALARM_ORIGINATOR; import static org.thingsboard.rule.engine.transform.OriginatorSource.CUSTOMER; import static org.thingsboard.rule.engine.transform.OriginatorSource.ENTITY; @@ -93,16 +99,23 @@ public class TbChangeOriginatorNodeTest { @Mock private AssetService assetServiceMock; @Mock - private DeviceService deviceServiceMock; - @Mock private RelationService relationServiceMock; @Mock private RuleEngineAlarmService alarmServiceMock; + @Mock + private EntityService entityServiceMock; @BeforeEach - public void before() throws TbNodeException { + public void setup() { node = new TbChangeOriginatorNode(); config = new TbChangeOriginatorNodeConfiguration().defaultConfiguration(); + + lenient().when(ctxMock.getTenantId()).thenReturn(TENANT_ID); + lenient().when(ctxMock.getDbCallbackExecutor()).thenReturn(dbExecutor); + lenient().when(ctxMock.getAssetService()).thenReturn(assetServiceMock); + lenient().when(ctxMock.getRelationService()).thenReturn(relationServiceMock); + lenient().when(ctxMock.getAlarmService()).thenReturn(alarmServiceMock); + lenient().when(ctxMock.getEntityService()).thenReturn(entityServiceMock); } @Test @@ -172,8 +185,13 @@ public class TbChangeOriginatorNodeTest { } @Test - public void givenOriginatorSourceIsCustomer_whenOnMsg_thenTellSuccess() throws TbNodeException { - Device device = new Device(DEVICE_ID); + @DisplayName(""" + Given a device assigned to a customer and node configured to change originator to customer, + when processing the message, + then should change message originator from device to customer""") + public void givenDeviceAssignedToCustomer_whenProcessingMessage_thenChangesOriginatorToCustomer() throws TbNodeException { + // GIVEN + var device = new Device(DEVICE_ID); device.setCustomerId(CUSTOMER_ID); TbMsg msg = TbMsg.newMsg() @@ -186,16 +204,52 @@ public class TbChangeOriginatorNodeTest { .originator(CUSTOMER_ID) .build(); - given(ctxMock.getDbCallbackExecutor()).willReturn(dbExecutor); - given(ctxMock.getDeviceService()).willReturn(deviceServiceMock); - given(ctxMock.getTenantId()).willReturn(TENANT_ID); - given(deviceServiceMock.findDeviceById(any(TenantId.class), any(DeviceId.class))).willReturn(device); + given(entityServiceMock.fetchEntityCustomerIdAsync(TENANT_ID, device.getId())).willReturn( + FluentFuture.from(immediateFuture(Optional.of(CUSTOMER_ID))) + ); + + given(ctxMock.transformMsgOriginator(any(TbMsg.class), any(EntityId.class))).willReturn(expectedMsg); + + node.init(ctxMock, new TbNodeConfiguration(JacksonUtil.valueToTree(config))); + + // WHEN + node.onMsg(ctxMock, msg); + + // THEN + then(ctxMock).should().transformMsgOriginator(msg, CUSTOMER_ID); + ArgumentCaptor actualMsg = ArgumentCaptor.forClass(TbMsg.class); + then(ctxMock).should().tellSuccess(actualMsg.capture()); + assertThat(actualMsg.getValue()).usingRecursiveComparison().ignoringFields("ctx").isEqualTo(expectedMsg); + } + + @Test + @DisplayName(""" + Given a customer as message originator and node configured to change originator to customer, + when processing the message, + then should keep the customer as originator""") + public void givenCustomerAsOriginator_whenProcessingMessage_thenKeepsCustomerAsOriginator() throws TbNodeException { + // GIVEN + var customer = new Customer(CUSTOMER_ID); + + var msg = TbMsg.newMsg() + .type(TbMsgType.POST_TELEMETRY_REQUEST) + .originator(customer.getId()) + .metaData(TbMsgMetaData.EMPTY) + .data(TbMsg.EMPTY_JSON_OBJECT) + .build(); + + var expectedMsg = msg.transform() + .originator(CUSTOMER_ID) + .build(); + given(ctxMock.transformMsgOriginator(any(TbMsg.class), any(EntityId.class))).willReturn(expectedMsg); node.init(ctxMock, new TbNodeConfiguration(JacksonUtil.valueToTree(config))); + + // WHEN node.onMsg(ctxMock, msg); - then(deviceServiceMock).should().findDeviceById(TENANT_ID, DEVICE_ID); + // THEN then(ctxMock).should().transformMsgOriginator(msg, CUSTOMER_ID); ArgumentCaptor actualMsg = ArgumentCaptor.forClass(TbMsg.class); then(ctxMock).should().tellSuccess(actualMsg.capture()); @@ -216,8 +270,6 @@ public class TbChangeOriginatorNodeTest { .originator(TENANT_ID) .build(); - given(ctxMock.getDbCallbackExecutor()).willReturn(dbExecutor); - given(ctxMock.getTenantId()).willReturn(TENANT_ID); given(ctxMock.transformMsgOriginator(any(TbMsg.class), any(EntityId.class))).willReturn(expectedMsg); node.init(ctxMock, new TbNodeConfiguration(JacksonUtil.valueToTree(config))); @@ -240,9 +292,6 @@ public class TbChangeOriginatorNodeTest { .data(TbMsg.EMPTY_JSON_OBJECT) .build(); - given(ctxMock.getDbCallbackExecutor()).willReturn(dbExecutor); - given(ctxMock.getRelationService()).willReturn(relationServiceMock); - given(ctxMock.getTenantId()).willReturn(TENANT_ID); given(relationServiceMock.findByQuery(any(TenantId.class), any(EntityRelationsQuery.class))).willReturn(Futures.immediateFuture(Collections.emptyList())); node.init(ctxMock, new TbNodeConfiguration(JacksonUtil.valueToTree(config))); @@ -282,9 +331,6 @@ public class TbChangeOriginatorNodeTest { .originator(DEVICE_ID) .build(); - given(ctxMock.getDbCallbackExecutor()).willReturn(dbExecutor); - given(ctxMock.getAlarmService()).willReturn(alarmServiceMock); - given(ctxMock.getTenantId()).willReturn(TENANT_ID); given(alarmServiceMock.findAlarmByIdAsync(any(TenantId.class), any(AlarmId.class))).willReturn(Futures.immediateFuture(alarm)); given(ctxMock.transformMsgOriginator(any(TbMsg.class), any(EntityId.class))).willReturn(expectedMsg); @@ -315,9 +361,6 @@ public class TbChangeOriginatorNodeTest { .originator(ASSET_ID) .build(); - given(ctxMock.getDbCallbackExecutor()).willReturn(dbExecutor); - given(ctxMock.getAssetService()).willReturn(assetServiceMock); - given(ctxMock.getTenantId()).willReturn(TENANT_ID); given(assetServiceMock.findAssetByTenantIdAndName(any(TenantId.class), any(String.class))).willReturn(new Asset(ASSET_ID)); given(ctxMock.transformMsgOriginator(any(TbMsg.class), any(EntityId.class))).willReturn(expectedMsg); @@ -355,9 +398,6 @@ public class TbChangeOriginatorNodeTest { .data(TbMsg.EMPTY_JSON_OBJECT) .build(); - given(ctxMock.getDbCallbackExecutor()).willReturn(dbExecutor); - given(ctxMock.getAssetService()).willReturn(assetServiceMock); - given(ctxMock.getTenantId()).willReturn(TENANT_ID); given(assetServiceMock.findAssetByTenantIdAndName(any(TenantId.class), any(String.class))).willReturn(null); node.init(ctxMock, new TbNodeConfiguration(JacksonUtil.valueToTree(config))); diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/util/EntitiesCustomerIdAsyncLoaderTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/util/EntitiesCustomerIdAsyncLoaderTest.java deleted file mode 100644 index 0be4e64ca2..0000000000 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/util/EntitiesCustomerIdAsyncLoaderTest.java +++ /dev/null @@ -1,154 +0,0 @@ -/** - * Copyright © 2016-2025 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.rule.engine.util; - -import com.google.common.util.concurrent.Futures; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; -import org.thingsboard.common.util.ListeningExecutor; -import org.thingsboard.rule.engine.TestDbCallbackExecutor; -import org.thingsboard.rule.engine.api.TbContext; -import org.thingsboard.rule.engine.api.TbNodeException; -import org.thingsboard.server.common.data.Customer; -import org.thingsboard.server.common.data.Device; -import org.thingsboard.server.common.data.EntityType; -import org.thingsboard.server.common.data.User; -import org.thingsboard.server.common.data.asset.Asset; -import org.thingsboard.server.common.data.id.AssetId; -import org.thingsboard.server.common.data.id.CustomerId; -import org.thingsboard.server.common.data.id.DeviceId; -import org.thingsboard.server.common.data.id.EntityIdFactory; -import org.thingsboard.server.common.data.id.UserId; -import org.thingsboard.server.dao.asset.AssetService; -import org.thingsboard.server.dao.device.DeviceService; -import org.thingsboard.server.dao.user.UserService; - -import java.util.EnumSet; -import java.util.UUID; -import java.util.concurrent.ExecutionException; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertInstanceOf; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.when; - -@ExtendWith(MockitoExtension.class) -public class EntitiesCustomerIdAsyncLoaderTest { - - private static final EnumSet SUPPORTED_ENTITY_TYPES = EnumSet.of( - EntityType.CUSTOMER, - EntityType.USER, - EntityType.ASSET, - EntityType.DEVICE - ); - private static final ListeningExecutor DB_EXECUTOR = new TestDbCallbackExecutor(); - @Mock - private TbContext ctxMock; - @Mock - private UserService userServiceMock; - @Mock - private AssetService assetServiceMock; - @Mock - private DeviceService deviceServiceMock; - - @Test - public void givenCustomerEntityType_whenFindEntityIdAsync_thenOK() throws ExecutionException, InterruptedException { - // GIVEN - var customer = new Customer(new CustomerId(UUID.randomUUID())); - - // WHEN - var actualCustomerId = EntitiesCustomerIdAsyncLoader.findEntityIdAsync(ctxMock, customer.getId()).get(); - - // THEN - assertEquals(customer.getId(), actualCustomerId); - } - - @Test - public void givenUserEntityType_whenFindEntityIdAsync_thenOK() throws ExecutionException, InterruptedException { - // GIVEN - var user = new User(new UserId(UUID.randomUUID())); - var expectedCustomerId = new CustomerId(UUID.randomUUID()); - user.setCustomerId(expectedCustomerId); - - when(ctxMock.getUserService()).thenReturn(userServiceMock); - doReturn(Futures.immediateFuture(user)).when(userServiceMock).findUserByIdAsync(any(), any()); - when(ctxMock.getDbCallbackExecutor()).thenReturn(DB_EXECUTOR); - - // WHEN - var actualCustomerId = EntitiesCustomerIdAsyncLoader.findEntityIdAsync(ctxMock, user.getId()).get(); - - // THEN - assertEquals(expectedCustomerId, actualCustomerId); - } - - @Test - public void givenAssetEntityType_whenFindEntityIdAsync_thenOK() throws ExecutionException, InterruptedException { - // GIVEN - var asset = new Asset(new AssetId(UUID.randomUUID())); - var expectedCustomerId = new CustomerId(UUID.randomUUID()); - asset.setCustomerId(expectedCustomerId); - - when(ctxMock.getAssetService()).thenReturn(assetServiceMock); - doReturn(Futures.immediateFuture(asset)).when(assetServiceMock).findAssetByIdAsync(any(), any()); - when(ctxMock.getDbCallbackExecutor()).thenReturn(DB_EXECUTOR); - - // WHEN - var actualCustomerId = EntitiesCustomerIdAsyncLoader.findEntityIdAsync(ctxMock, asset.getId()).get(); - - // THEN - assertEquals(expectedCustomerId, actualCustomerId); - } - - @Test - public void givenDeviceEntityType_whenFindEntityIdAsync_thenOK() throws ExecutionException, InterruptedException { - // GIVEN - var device = new Device(new DeviceId(UUID.randomUUID())); - var expectedCustomerId = new CustomerId(UUID.randomUUID()); - device.setCustomerId(expectedCustomerId); - - when(ctxMock.getDeviceService()).thenReturn(deviceServiceMock); - doReturn(device).when(deviceServiceMock).findDeviceById(any(), any()); - when(ctxMock.getDbCallbackExecutor()).thenReturn(DB_EXECUTOR); - - // WHEN - var actualCustomerId = EntitiesCustomerIdAsyncLoader.findEntityIdAsync(ctxMock, device.getId()).get(); - - // THEN - assertEquals(expectedCustomerId, actualCustomerId); - } - - @Test - public void givenUnsupportedEntityTypes_whenFindEntityIdAsync_thenException() { - for (var entityType : EntityType.values()) { - if (!SUPPORTED_ENTITY_TYPES.contains(entityType)) { - var entityId = EntityIdFactory.getByTypeAndUuid(entityType, UUID.randomUUID()); - - var expectedExceptionMsg = "org.thingsboard.rule.engine.api.TbNodeException: Unexpected originator EntityType: " + entityType; - - var exception = assertThrows(ExecutionException.class, - () -> EntitiesCustomerIdAsyncLoader.findEntityIdAsync(ctxMock, entityId).get()); - - assertInstanceOf(TbNodeException.class, exception.getCause()); - assertEquals(expectedExceptionMsg, exception.getMessage()); - } - } - } - -} diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/util/TenantIdLoaderTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/util/TenantIdLoaderTest.java index 16698b2841..15d513790d 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/util/TenantIdLoaderTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/util/TenantIdLoaderTest.java @@ -29,7 +29,6 @@ import org.thingsboard.rule.engine.api.RuleEngineAssetProfileCache; import org.thingsboard.rule.engine.api.RuleEngineDeviceProfileCache; import org.thingsboard.rule.engine.api.RuleEngineRpcService; import org.thingsboard.rule.engine.api.TbContext; -import org.thingsboard.server.common.data.AdminSettings; import org.thingsboard.server.common.data.ApiUsageState; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Dashboard; @@ -46,14 +45,12 @@ import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.asset.AssetProfile; import org.thingsboard.server.common.data.cf.CalculatedField; -import org.thingsboard.server.common.data.cf.CalculatedFieldLink; import org.thingsboard.server.common.data.domain.Domain; import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.id.AssetProfileId; import org.thingsboard.server.common.data.id.DeviceProfileId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityIdFactory; -import org.thingsboard.server.common.data.id.NotificationId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantProfileId; import org.thingsboard.server.common.data.job.Job; @@ -80,6 +77,7 @@ import org.thingsboard.server.dao.device.DeviceService; import org.thingsboard.server.dao.domain.DomainService; import org.thingsboard.server.dao.edge.EdgeService; import org.thingsboard.server.dao.entityview.EntityViewService; +import org.thingsboard.server.dao.job.JobService; import org.thingsboard.server.dao.mobile.MobileAppBundleService; import org.thingsboard.server.dao.mobile.MobileAppService; import org.thingsboard.server.dao.notification.NotificationRequestService; @@ -92,7 +90,6 @@ import org.thingsboard.server.dao.queue.QueueService; import org.thingsboard.server.dao.queue.QueueStatsService; import org.thingsboard.server.dao.resource.ResourceService; import org.thingsboard.server.dao.rule.RuleChainService; -import org.thingsboard.server.dao.job.JobService; import org.thingsboard.server.dao.user.UserService; import org.thingsboard.server.dao.widget.WidgetTypeService; import org.thingsboard.server.dao.widget.WidgetsBundleService; @@ -422,12 +419,6 @@ public class TenantIdLoaderTest { when(ctx.getCalculatedFieldService()).thenReturn(calculatedFieldService); doReturn(calculatedField).when(calculatedFieldService).findById(eq(tenantId), any()); break; - case CALCULATED_FIELD_LINK: - CalculatedFieldLink calculatedFieldLink = new CalculatedFieldLink(); - calculatedFieldLink.setTenantId(tenantId); - when(ctx.getCalculatedFieldService()).thenReturn(calculatedFieldService); - doReturn(calculatedFieldLink).when(calculatedFieldService).findCalculatedFieldLinkById(eq(tenantId), any()); - break; case JOB: Job job = new Job(); job.setTenantId(tenantId); diff --git a/transport/coap/src/main/resources/tb-coap-transport.yml b/transport/coap/src/main/resources/tb-coap-transport.yml index c54bf038f2..2f3942f847 100644 --- a/transport/coap/src/main/resources/tb-coap-transport.yml +++ b/transport/coap/src/main/resources/tb-coap-transport.yml @@ -114,9 +114,9 @@ redis: # Minumum number of idle connections that can be maintained in the pool without being closed minIdle: "${REDIS_POOL_CONFIG_MIN_IDLE:16}" # Enable/Disable PING command send when a connection is borrowed - testOnBorrow: "${REDIS_POOL_CONFIG_TEST_ON_BORROW:true}" + testOnBorrow: "${REDIS_POOL_CONFIG_TEST_ON_BORROW:false}" # The property is used to specify whether to test the connection before returning it to the connection pool. - testOnReturn: "${REDIS_POOL_CONFIG_TEST_ON_RETURN:true}" + testOnReturn: "${REDIS_POOL_CONFIG_TEST_ON_RETURN:false}" # The property is used in the context of connection pooling in Redis testWhileIdle: "${REDIS_POOL_CONFIG_TEST_WHILE_IDLE:true}" # Minimum amount of time that an idle connection should be idle before it can be evicted from the connection pool. Value set in milliseconds @@ -193,7 +193,7 @@ coap: # - A value of 0 means we accept using CID but will not generate one for foreign peer (enables support but not for incoming traffic). # - A value between 0 and <= 4: SingleNodeConnectionIdGenerator is used # - A value that are > 4: MultiNodeConnectionIdGenerator is used - connection_id_length: "${COAP_DTLS_CONNECTION_ID_LENGTH:}" + connection_id_length: "${COAP_DTLS_CONNECTION_ID_LENGTH:8}" # Specify the MTU (Maximum Transmission Unit). # Should be used if LAN MTU is not used, e.g. if IP tunnels are used or if the client uses a smaller value than the LAN MTU. # Default = 1024 diff --git a/transport/http/src/main/resources/tb-http-transport.yml b/transport/http/src/main/resources/tb-http-transport.yml index 3a3725edef..587894d5ce 100644 --- a/transport/http/src/main/resources/tb-http-transport.yml +++ b/transport/http/src/main/resources/tb-http-transport.yml @@ -147,9 +147,9 @@ redis: # Minumum number of idle connections that can be maintained in the pool without being closed minIdle: "${REDIS_POOL_CONFIG_MIN_IDLE:16}" # Enable/Disable PING command send when a connection is borrowed - testOnBorrow: "${REDIS_POOL_CONFIG_TEST_ON_BORROW:true}" + testOnBorrow: "${REDIS_POOL_CONFIG_TEST_ON_BORROW:false}" # The property is used to specify whether to test the connection before returning it to the connection pool. - testOnReturn: "${REDIS_POOL_CONFIG_TEST_ON_RETURN:true}" + testOnReturn: "${REDIS_POOL_CONFIG_TEST_ON_RETURN:false}" # The property is used in the context of connection pooling in Redis testWhileIdle: "${REDIS_POOL_CONFIG_TEST_WHILE_IDLE:true}" # Minimum amount of time that an idle connection should be idle before it can be evicted from the connection pool. Value set in milliseconds diff --git a/transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml b/transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml index 60568a6a4e..323f80b999 100644 --- a/transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml +++ b/transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml @@ -114,9 +114,9 @@ redis: # Minumum number of idle connections that can be maintained in the pool without being closed minIdle: "${REDIS_POOL_CONFIG_MIN_IDLE:16}" # Enable/Disable PING command send when a connection is borrowed - testOnBorrow: "${REDIS_POOL_CONFIG_TEST_ON_BORROW:true}" + testOnBorrow: "${REDIS_POOL_CONFIG_TEST_ON_BORROW:false}" # The property is used to specify whether to test the connection before returning it to the connection pool. - testOnReturn: "${REDIS_POOL_CONFIG_TEST_ON_RETURN:true}" + testOnReturn: "${REDIS_POOL_CONFIG_TEST_ON_RETURN:false}" # The property is used in the context of connection pooling in Redis testWhileIdle: "${REDIS_POOL_CONFIG_TEST_WHILE_IDLE:true}" # Minimum amount of time that an idle connection should be idle before it can be evicted from the connection pool. Value set in milliseconds @@ -173,7 +173,7 @@ transport: # - A value of 0 means we accept using CID but will not generate one for foreign peer (enables support but not for incoming traffic). # - A value between 0 and <= 4: SingleNodeConnectionIdGenerator is used # - A value that are > 4: MultiNodeConnectionIdGenerator is used - connection_id_length: "${LWM2M_DTLS_CONNECTION_ID_LENGTH:}" + connection_id_length: "${LWM2M_DTLS_CONNECTION_ID_LENGTH:8}" server: # LwM2M Server ID id: "${LWM2M_SERVER_ID:123}" diff --git a/transport/mqtt/src/main/resources/tb-mqtt-transport.yml b/transport/mqtt/src/main/resources/tb-mqtt-transport.yml index 55781abeeb..fae10cc892 100644 --- a/transport/mqtt/src/main/resources/tb-mqtt-transport.yml +++ b/transport/mqtt/src/main/resources/tb-mqtt-transport.yml @@ -115,9 +115,9 @@ redis: # Minumum number of idle connections that can be maintained in the pool without being closed minIdle: "${REDIS_POOL_CONFIG_MIN_IDLE:16}" # Enable/Disable PING command send when a connection is borrowed - testOnBorrow: "${REDIS_POOL_CONFIG_TEST_ON_BORROW:true}" + testOnBorrow: "${REDIS_POOL_CONFIG_TEST_ON_BORROW:false}" # The property is used to specify whether to test the connection before returning it to the connection pool. - testOnReturn: "${REDIS_POOL_CONFIG_TEST_ON_RETURN:true}" + testOnReturn: "${REDIS_POOL_CONFIG_TEST_ON_RETURN:false}" # The property is used in the context of connection pooling in Redis testWhileIdle: "${REDIS_POOL_CONFIG_TEST_WHILE_IDLE:true}" # Minimum amount of time that an idle connection should be idle before it can be evicted from the connection pool. Value set in milliseconds diff --git a/transport/snmp/src/main/resources/tb-snmp-transport.yml b/transport/snmp/src/main/resources/tb-snmp-transport.yml index 746e8f2173..79aee31921 100644 --- a/transport/snmp/src/main/resources/tb-snmp-transport.yml +++ b/transport/snmp/src/main/resources/tb-snmp-transport.yml @@ -114,9 +114,9 @@ redis: # Minumum number of idle connections that can be maintained in the pool without being closed minIdle: "${REDIS_POOL_CONFIG_MIN_IDLE:16}" # Enable/Disable PING command send when a connection is borrowed - testOnBorrow: "${REDIS_POOL_CONFIG_TEST_ON_BORROW:true}" + testOnBorrow: "${REDIS_POOL_CONFIG_TEST_ON_BORROW:false}" # The property is used to specify whether to test the connection before returning it to the connection pool. - testOnReturn: "${REDIS_POOL_CONFIG_TEST_ON_RETURN:true}" + testOnReturn: "${REDIS_POOL_CONFIG_TEST_ON_RETURN:false}" # The property is used in the context of connection pooling in Redis testWhileIdle: "${REDIS_POOL_CONFIG_TEST_WHILE_IDLE:true}" # Minimum amount of time that an idle connection should be idle before it can be evicted from the connection pool. Value set in milliseconds diff --git a/ui-ngx/patches/@iplab+ngx-color-picker+18.0.1.patch b/ui-ngx/patches/@iplab+ngx-color-picker+18.0.1.patch new file mode 100644 index 0000000000..c8090076cc --- /dev/null +++ b/ui-ngx/patches/@iplab+ngx-color-picker+18.0.1.patch @@ -0,0 +1,97 @@ +diff --git a/node_modules/@iplab/ngx-color-picker/esm2022/lib/components/parts/inputs/hex-input/hex-input.component.mjs b/node_modules/@iplab/ngx-color-picker/esm2022/lib/components/parts/inputs/hex-input/hex-input.component.mjs +index bf5480b..02c8246 100644 +--- a/node_modules/@iplab/ngx-color-picker/esm2022/lib/components/parts/inputs/hex-input/hex-input.component.mjs ++++ b/node_modules/@iplab/ngx-color-picker/esm2022/lib/components/parts/inputs/hex-input/hex-input.component.mjs +@@ -29,10 +29,10 @@ export class HexComponent { + } + } + static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.0.0", ngImport: i0, type: HexComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); } +- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.0.0", type: HexComponent, isStandalone: true, selector: "hex-input-component", inputs: { color: { classPropertyName: "color", publicName: "color", isSignal: true, isRequired: true, transformFunction: null }, labelVisible: { classPropertyName: "labelVisible", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, prefixValue: { classPropertyName: "prefixValue", publicName: "prefix", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { color: "colorChange" }, ngImport: i0, template: "
    \r\n \r\n @if (labelVisible()) {\r\n HEX\r\n }\r\n
    ", styles: [":host,:host ::ng-deep *{padding:0;margin:0;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}\n", ":host{display:table;width:100%;text-align:center;color:#b4b4b4;font-size:11px}.column{display:table-cell;padding:0 2px}input{width:100%;border:1px solid rgb(218,218,218);color:#272727;text-align:center;font-size:12px;-webkit-appearance:none;border-radius:0;margin:0 0 6px;height:26px;outline:none}\n", ""], changeDetection: i0.ChangeDetectionStrategy.OnPush }); } ++ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.0.0", type: HexComponent, isStandalone: true, selector: "hex-input-component", inputs: { color: { classPropertyName: "color", publicName: "color", isSignal: true, isRequired: true, transformFunction: null }, labelVisible: { classPropertyName: "labelVisible", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, prefixValue: { classPropertyName: "prefixValue", publicName: "prefix", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { color: "colorChange" }, ngImport: i0, template: "
    \r\n \r\n @if (labelVisible()) {\r\n HEX\r\n }\r\n
    ", styles: [":host,:host ::ng-deep *{padding:0;margin:0;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}\n", ":host{display:table;width:100%;text-align:center;color:#b4b4b4;font-size:11px}.column{display:table-cell;padding:0 2px}input{width:100%;border:1px solid rgb(218,218,218);color:#272727;text-align:center;font-size:12px;-webkit-appearance:none;border-radius:0;margin:0 0 6px;height:26px;outline:none}\n", ""], changeDetection: i0.ChangeDetectionStrategy.OnPush }); } + } + i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.0.0", ngImport: i0, type: HexComponent, decorators: [{ + type: Component, +- args: [{ selector: `hex-input-component`, changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, template: "
    \r\n \r\n @if (labelVisible()) {\r\n HEX\r\n }\r\n
    ", styles: [":host,:host ::ng-deep *{padding:0;margin:0;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}\n", ":host{display:table;width:100%;text-align:center;color:#b4b4b4;font-size:11px}.column{display:table-cell;padding:0 2px}input{width:100%;border:1px solid rgb(218,218,218);color:#272727;text-align:center;font-size:12px;-webkit-appearance:none;border-radius:0;margin:0 0 6px;height:26px;outline:none}\n"] }] ++ args: [{ selector: `hex-input-component`, changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, template: "
    \r\n \r\n @if (labelVisible()) {\r\n HEX\r\n }\r\n
    ", styles: [":host,:host ::ng-deep *{padding:0;margin:0;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}\n", ":host{display:table;width:100%;text-align:center;color:#b4b4b4;font-size:11px}.column{display:table-cell;padding:0 2px}input{width:100%;border:1px solid rgb(218,218,218);color:#272727;text-align:center;font-size:12px;-webkit-appearance:none;border-radius:0;margin:0 0 6px;height:26px;outline:none}\n"] }] + }] }); + //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaGV4LWlucHV0LmNvbXBvbmVudC5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uLy4uLy4uLy4uLy4uLy4uLy4uL3Byb2plY3RzL2lwbGFiL25neC1jb2xvci1waWNrZXIvc3JjL2xpYi9jb21wb25lbnRzL3BhcnRzL2lucHV0cy9oZXgtaW5wdXQvaGV4LWlucHV0LmNvbXBvbmVudC50cyIsIi4uLy4uLy4uLy4uLy4uLy4uLy4uLy4uL3Byb2plY3RzL2lwbGFiL25neC1jb2xvci1waWNrZXIvc3JjL2xpYi9jb21wb25lbnRzL3BhcnRzL2lucHV0cy9oZXgtaW5wdXQvaGV4LWlucHV0LmNvbXBvbmVudC5odG1sIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLE9BQU8sRUFBRSxTQUFTLEVBQUUsdUJBQXVCLEVBQUUsZ0JBQWdCLEVBQWUsS0FBSyxFQUFFLEtBQUssRUFBZSxNQUFNLGVBQWUsQ0FBQztBQUM3SCxPQUFPLEVBQUUsS0FBSyxFQUFFLE1BQU0saUNBQWlDLENBQUM7O0FBY3hELE1BQU0sT0FBTyxZQUFZO0lBWHpCO1FBYVcsVUFBSyxHQUF1QixLQUFLLENBQUMsUUFBUSxFQUFTLENBQUM7UUFFcEQsaUJBQVksR0FBeUIsS0FBSyxDQUFtQixLQUFLLEVBQUUsRUFBRSxLQUFLLEVBQUUsT0FBTyxFQUFFLFNBQVMsRUFBRSxnQkFBZ0IsRUFBRSxDQUFDLENBQUM7UUFFckgsZ0JBQVcsR0FBd0IsS0FBSyxDQUFTLEVBQUUsRUFBRSxFQUFFLEtBQUssRUFBRSxRQUFRLEVBQUUsQ0FBQyxDQUFDO0tBMkJwRjtJQXpCRyxJQUFXLEtBQUs7UUFDWixPQUFPLElBQUksQ0FBQyxXQUFXLEVBQUUsR0FBRyxDQUFDLElBQUksQ0FBQyxLQUFLLEVBQUUsQ0FBQyxDQUFDLENBQUMsSUFBSSxDQUFDLEtBQUssRUFBRSxDQUFDLFdBQVcsQ0FBQyxJQUFJLENBQUMsS0FBSyxFQUFFLENBQUMsT0FBTyxFQUFFLENBQUMsS0FBSyxHQUFHLENBQUMsQ0FBQyxDQUFDLE9BQU8sQ0FBQyxHQUFHLEVBQUUsRUFBRSxDQUFDLENBQUMsQ0FBQyxDQUFDLEVBQUUsQ0FBQyxDQUFDO0lBQ2xJLENBQUM7SUFFTSxhQUFhLENBQUMsS0FBb0IsRUFBRSxVQUFrQjtRQUN6RCxNQUFNLEtBQUssR0FBRyxVQUFVLENBQUMsV0FBVyxFQUFFLENBQUMsT0FBTyxDQUFDLEdBQUcsRUFBRSxFQUFFLENBQUMsQ0FBQztRQUV4RCxJQUNBLENBQUMsQ0FBQyxLQUFLLENBQUMsT0FBTyxLQUFLLEVBQUUsSUFBSSxLQUFLLENBQUMsR0FBRyxDQUFDLFdBQVcsRUFBRSxLQUFLLE9BQU8sQ0FBQyxJQUFJLEtBQUssQ0FBQyxNQUFNLEtBQUssQ0FBQyxDQUFDO2VBQ2xGLEtBQUssQ0FBQyxNQUFNLEtBQUssQ0FBQyxJQUFJLEtBQUssQ0FBQyxNQUFNLEtBQUssQ0FBQyxFQUFFLENBQUM7WUFDMUMsTUFBTSxHQUFHLEdBQUcsUUFBUSxDQUFDLEtBQUssRUFBRSxFQUFFLENBQUMsQ0FBQztZQUNoQyxNQUFNLE1BQU0sR0FBRyxHQUFHLENBQUMsUUFBUSxDQUFDLEVBQUUsQ0FBQyxDQUFDO1lBRWhDOzs7OztlQUtHO1lBQ0gsSUFBSSxNQUFNLENBQUMsUUFBUSxDQUFDLEtBQUssQ0FBQyxNQUFNLEVBQUUsR0FBRyxDQUFDLEtBQUssS0FBSyxJQUFJLElBQUksQ0FBQyxLQUFLLEtBQUssS0FBSyxFQUFFLENBQUM7Z0JBQ3ZFLE1BQU0sUUFBUSxHQUFHLElBQUksS0FBSyxDQUFDLElBQUksS0FBSyxFQUFFLENBQUMsQ0FBQztnQkFDeEMsSUFBSSxDQUFDLEtBQUssQ0FBQyxHQUFHLENBQUMsUUFBUSxDQUFDLENBQUM7WUFDN0IsQ0FBQztRQUNMLENBQUM7SUFDTCxDQUFDOzhHQWhDUSxZQUFZO2tHQUFaLFlBQVksZ2dCQ2Z6Qiw0TUFLTTs7MkZEVU8sWUFBWTtrQkFYeEIsU0FBUzsrQkFDSSxxQkFBcUIsbUJBT2QsdUJBQXVCLENBQUMsTUFBTSxjQUNuQyxJQUFJIiwic291cmNlc0NvbnRlbnQiOlsiaW1wb3J0IHsgQ29tcG9uZW50LCBDaGFuZ2VEZXRlY3Rpb25TdHJhdGVneSwgYm9vbGVhbkF0dHJpYnV0ZSwgSW5wdXRTaWduYWwsIGlucHV0LCBtb2RlbCwgTW9kZWxTaWduYWwgfSBmcm9tICdAYW5ndWxhci9jb3JlJztcclxuaW1wb3J0IHsgQ29sb3IgfSBmcm9tICcuLi8uLi8uLi8uLi9oZWxwZXJzL2NvbG9yLmNsYXNzJztcclxuXHJcblxyXG5AQ29tcG9uZW50KHtcclxuICAgIHNlbGVjdG9yOiBgaGV4LWlucHV0LWNvbXBvbmVudGAsXHJcbiAgICB0ZW1wbGF0ZVVybDogYC4vaGV4LWlucHV0LmNvbXBvbmVudC5odG1sYCxcclxuICAgIHN0eWxlVXJsczogW1xyXG4gICAgICAgIGAuLy4uLy4uL2Jhc2Uuc3R5bGUuc2Nzc2AsXHJcbiAgICAgICAgYC4vLi4vaW5wdXQuY29tcG9uZW50LnNjc3NgLFxyXG4gICAgICAgIGAuL2hleC1pbnB1dC5jb21wb25lbnQuc2Nzc2BcclxuICAgIF0sXHJcbiAgICBjaGFuZ2VEZXRlY3Rpb246IENoYW5nZURldGVjdGlvblN0cmF0ZWd5Lk9uUHVzaCxcclxuICAgIHN0YW5kYWxvbmU6IHRydWVcclxufSlcclxuZXhwb3J0IGNsYXNzIEhleENvbXBvbmVudCB7XHJcblxyXG4gICAgcHVibGljIGNvbG9yOiBNb2RlbFNpZ25hbDxDb2xvcj4gPSBtb2RlbC5yZXF1aXJlZDxDb2xvcj4oKTtcclxuXHJcbiAgICBwdWJsaWMgbGFiZWxWaXNpYmxlOiBJbnB1dFNpZ25hbDxib29sZWFuPiA9IGlucHV0PGJvb2xlYW4sIGJvb2xlYW4+KGZhbHNlLCB7IGFsaWFzOiAnbGFiZWwnLCB0cmFuc2Zvcm06IGJvb2xlYW5BdHRyaWJ1dGUgfSk7XHJcblxyXG4gICAgcHVibGljIHByZWZpeFZhbHVlOiBJbnB1dFNpZ25hbDxzdHJpbmc+ID0gaW5wdXQ8c3RyaW5nPignJywgeyBhbGlhczogJ3ByZWZpeCcgfSk7XHJcblxyXG4gICAgcHVibGljIGdldCB2YWx1ZSgpIHtcclxuICAgICAgICByZXR1cm4gdGhpcy5wcmVmaXhWYWx1ZSgpICsgKHRoaXMuY29sb3IoKSA/IHRoaXMuY29sb3IoKS50b0hleFN0cmluZyh0aGlzLmNvbG9yKCkuZ2V0UmdiYSgpLmFscGhhIDwgMSkucmVwbGFjZSgnIycsICcnKSA6ICcnKTtcclxuICAgIH1cclxuXHJcbiAgICBwdWJsaWMgb25JbnB1dENoYW5nZShldmVudDogS2V5Ym9hcmRFdmVudCwgaW5wdXRWYWx1ZTogc3RyaW5nKTogdm9pZCB7XHJcbiAgICAgICAgY29uc3QgdmFsdWUgPSBpbnB1dFZhbHVlLnRvTG93ZXJDYXNlKCkucmVwbGFjZSgnIycsICcnKTtcclxuXHJcbiAgICAgICAgaWYgKFxyXG4gICAgICAgICgoZXZlbnQua2V5Q29kZSA9PT0gMTMgfHwgZXZlbnQua2V5LnRvTG93ZXJDYXNlKCkgPT09ICdlbnRlcicpICYmIHZhbHVlLmxlbmd0aCA9PT0gMylcclxuICAgICAgICB8fCB2YWx1ZS5sZW5ndGggPT09IDYgfHwgdmFsdWUubGVuZ3RoID09PSA4KSB7XHJcbiAgICAgICAgICAgIGNvbnN0IGhleCA9IHBhcnNlSW50KHZhbHVlLCAxNik7XHJcbiAgICAgICAgICAgIGNvbnN0IGhleFN0ciA9IGhleC50b1N0cmluZygxNik7XHJcblxyXG4gICAgICAgICAgICAvKipcclxuICAgICAgICAgICAgICogaWYgdmFsdWUgaXMgdmFsaWRcclxuICAgICAgICAgICAgICogY2hhbmdlIGNvbG9yIGVsc2UgZG8gbm90aGluZ1xyXG4gICAgICAgICAgICAgKiBhZnRlciBwYXJzaW5nIG51bWJlciBsZWFkaW5nIDAgaXMgcmVtb3ZlZCxcclxuICAgICAgICAgICAgICogY29tcGFyZSBsZW5ndGggYW5kIGFkZCBsZWFkaW5nIDAgYmVmb3JlIGNvbXBhcmluZyB0d28gdmFsdWVzXHJcbiAgICAgICAgICAgICAqL1xyXG4gICAgICAgICAgICBpZiAoaGV4U3RyLnBhZFN0YXJ0KHZhbHVlLmxlbmd0aCwgJzAnKSA9PT0gdmFsdWUgJiYgdGhpcy52YWx1ZSAhPT0gdmFsdWUpIHtcclxuICAgICAgICAgICAgICAgIGNvbnN0IG5ld0NvbG9yID0gbmV3IENvbG9yKGAjJHt2YWx1ZX1gKTtcclxuICAgICAgICAgICAgICAgIHRoaXMuY29sb3Iuc2V0KG5ld0NvbG9yKTtcclxuICAgICAgICAgICAgfVxyXG4gICAgICAgIH1cclxuICAgIH1cclxufVxyXG4iLCI8ZGl2IGNsYXNzPVwiY29sdW1uXCI+XHJcbiAgICA8aW5wdXQgI2VsUmVmIHR5cGU9XCJ0ZXh0XCIgW3ZhbHVlXT1cInZhbHVlXCIgKGtleXVwKT1cIm9uSW5wdXRDaGFuZ2UoJGV2ZW50LCBlbFJlZi52YWx1ZSlcIiAvPlxyXG4gICAgQGlmIChsYWJlbFZpc2libGUoKSkge1xyXG4gICAgICAgIDxzcGFuPkhFWDwvc3Bhbj5cclxuICAgIH1cclxuPC9kaXY+Il19 +diff --git a/node_modules/@iplab/ngx-color-picker/esm2022/lib/components/parts/inputs/hsla-input/hsla-input.component.mjs b/node_modules/@iplab/ngx-color-picker/esm2022/lib/components/parts/inputs/hsla-input/hsla-input.component.mjs +index 2454b27..dd007ce 100644 +--- a/node_modules/@iplab/ngx-color-picker/esm2022/lib/components/parts/inputs/hsla-input/hsla-input.component.mjs ++++ b/node_modules/@iplab/ngx-color-picker/esm2022/lib/components/parts/inputs/hsla-input/hsla-input.component.mjs +@@ -21,10 +21,10 @@ export class HslaComponent { + this.color.set(newColor); + } + static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.0.0", ngImport: i0, type: HslaComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); } +- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.0.0", type: HslaComponent, isStandalone: true, selector: "hsla-input-component", inputs: { color: { classPropertyName: "color", publicName: "color", isSignal: true, isRequired: true, transformFunction: null }, labelVisible: { classPropertyName: "labelVisible", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, isAlphaVisible: { classPropertyName: "isAlphaVisible", publicName: "alpha", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { color: "colorChange" }, ngImport: i0, template: "
    \r\n \r\n @if (labelVisible()) {\r\n H\r\n }\r\n
    \r\n
    \r\n \r\n @if (labelVisible()) {\r\n S\r\n }\r\n
    \r\n
    \r\n \r\n @if (labelVisible()) {\r\n L\r\n }\r\n
    \r\n@if (isAlphaVisible()) {\r\n
    \r\n \r\n @if (labelVisible()) {\r\n A\r\n }\r\n
    \r\n}", styles: [":host,:host ::ng-deep *{padding:0;margin:0;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}\n", ":host{display:table;width:100%;text-align:center;color:#b4b4b4;font-size:11px}.column{display:table-cell;padding:0 2px}input{width:100%;border:1px solid rgb(218,218,218);color:#272727;text-align:center;font-size:12px;-webkit-appearance:none;border-radius:0;margin:0 0 6px;height:26px;outline:none}\n", ""], dependencies: [{ kind: "directive", type: ColorPickerInputDirective, selector: "[inputChange]", inputs: ["min", "max"], outputs: ["inputChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); } ++ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.0.0", type: HslaComponent, isStandalone: true, selector: "hsla-input-component", inputs: { color: { classPropertyName: "color", publicName: "color", isSignal: true, isRequired: true, transformFunction: null }, labelVisible: { classPropertyName: "labelVisible", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, isAlphaVisible: { classPropertyName: "isAlphaVisible", publicName: "alpha", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { color: "colorChange" }, ngImport: i0, template: "
    \r\n \r\n @if (labelVisible()) {\r\n H\r\n }\r\n
    \r\n
    \r\n \r\n @if (labelVisible()) {\r\n S\r\n }\r\n
    \r\n
    \r\n \r\n @if (labelVisible()) {\r\n L\r\n }\r\n
    \r\n@if (isAlphaVisible()) {\r\n
    \r\n \r\n @if (labelVisible()) {\r\n A\r\n }\r\n
    \r\n}", styles: [":host,:host ::ng-deep *{padding:0;margin:0;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}\n", ":host{display:table;width:100%;text-align:center;color:#b4b4b4;font-size:11px}.column{display:table-cell;padding:0 2px}input{width:100%;border:1px solid rgb(218,218,218);color:#272727;text-align:center;font-size:12px;-webkit-appearance:none;border-radius:0;margin:0 0 6px;height:26px;outline:none}\n", ""], dependencies: [{ kind: "directive", type: ColorPickerInputDirective, selector: "[inputChange]", inputs: ["min", "max"], outputs: ["inputChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); } + } + i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.0.0", ngImport: i0, type: HslaComponent, decorators: [{ + type: Component, +- args: [{ selector: `hsla-input-component`, changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, imports: [ColorPickerInputDirective], template: "
    \r\n \r\n @if (labelVisible()) {\r\n H\r\n }\r\n
    \r\n
    \r\n \r\n @if (labelVisible()) {\r\n S\r\n }\r\n
    \r\n
    \r\n \r\n @if (labelVisible()) {\r\n L\r\n }\r\n
    \r\n@if (isAlphaVisible()) {\r\n
    \r\n \r\n @if (labelVisible()) {\r\n A\r\n }\r\n
    \r\n}", styles: [":host,:host ::ng-deep *{padding:0;margin:0;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}\n", ":host{display:table;width:100%;text-align:center;color:#b4b4b4;font-size:11px}.column{display:table-cell;padding:0 2px}input{width:100%;border:1px solid rgb(218,218,218);color:#272727;text-align:center;font-size:12px;-webkit-appearance:none;border-radius:0;margin:0 0 6px;height:26px;outline:none}\n"] }] ++ args: [{ selector: `hsla-input-component`, changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, imports: [ColorPickerInputDirective], template: "
    \r\n \r\n @if (labelVisible()) {\r\n H\r\n }\r\n
    \r\n
    \r\n \r\n @if (labelVisible()) {\r\n S\r\n }\r\n
    \r\n
    \r\n \r\n @if (labelVisible()) {\r\n L\r\n }\r\n
    \r\n@if (isAlphaVisible()) {\r\n
    \r\n \r\n @if (labelVisible()) {\r\n A\r\n }\r\n
    \r\n}", styles: [":host,:host ::ng-deep *{padding:0;margin:0;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}\n", ":host{display:table;width:100%;text-align:center;color:#b4b4b4;font-size:11px}.column{display:table-cell;padding:0 2px}input{width:100%;border:1px solid rgb(218,218,218);color:#272727;text-align:center;font-size:12px;-webkit-appearance:none;border-radius:0;margin:0 0 6px;height:26px;outline:none}\n"] }] + }] }); + //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaHNsYS1pbnB1dC5jb21wb25lbnQuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi8uLi8uLi8uLi8uLi8uLi9wcm9qZWN0cy9pcGxhYi9uZ3gtY29sb3ItcGlja2VyL3NyYy9saWIvY29tcG9uZW50cy9wYXJ0cy9pbnB1dHMvaHNsYS1pbnB1dC9oc2xhLWlucHV0LmNvbXBvbmVudC50cyIsIi4uLy4uLy4uLy4uLy4uLy4uLy4uLy4uL3Byb2plY3RzL2lwbGFiL25neC1jb2xvci1waWNrZXIvc3JjL2xpYi9jb21wb25lbnRzL3BhcnRzL2lucHV0cy9oc2xhLWlucHV0L2hzbGEtaW5wdXQuY29tcG9uZW50Lmh0bWwiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsT0FBTyxFQUFFLFNBQVMsRUFBRSx1QkFBdUIsRUFBRSxnQkFBZ0IsRUFBZSxLQUFLLEVBQUUsS0FBSyxFQUFlLE1BQU0sZUFBZSxDQUFDO0FBQzdILE9BQU8sRUFBRSxLQUFLLEVBQUUsTUFBTSxtQ0FBbUMsQ0FBQztBQUMxRCxPQUFPLEVBQUUseUJBQXlCLEVBQUUsTUFBTSxxREFBcUQsQ0FBQzs7QUFlaEcsTUFBTSxPQUFPLGFBQWE7SUFaMUI7UUFjVyxVQUFLLEdBQXVCLEtBQUssQ0FBQyxRQUFRLEVBQVMsQ0FBQztRQUVwRCxpQkFBWSxHQUF5QixLQUFLLENBQW1CLEtBQUssRUFBRSxFQUFFLEtBQUssRUFBRSxPQUFPLEVBQUUsU0FBUyxFQUFFLGdCQUFnQixFQUFFLENBQUMsQ0FBQztRQUVySCxtQkFBYyxHQUF5QixLQUFLLENBQW1CLElBQUksRUFBRSxFQUFFLEtBQUssRUFBRSxPQUFPLEVBQUUsU0FBUyxFQUFFLGdCQUFnQixFQUFFLENBQUMsQ0FBQztLQWdCaEk7SUFkRyxJQUFXLEtBQUs7UUFDWixPQUFPLElBQUksQ0FBQyxLQUFLLEVBQUUsRUFBRSxPQUFPLEVBQUUsQ0FBQztJQUNuQyxDQUFDO0lBRU0sYUFBYSxDQUFDLFFBQWdCLEVBQUUsS0FBNEI7UUFDL0QsTUFBTSxLQUFLLEdBQUcsSUFBSSxDQUFDLEtBQUssQ0FBQztRQUN6QixNQUFNLEdBQUcsR0FBRyxLQUFLLEtBQUssR0FBRyxDQUFDLENBQUMsQ0FBQyxRQUFRLENBQUMsQ0FBQyxDQUFDLEtBQUssQ0FBQyxHQUFHLENBQUM7UUFDakQsTUFBTSxVQUFVLEdBQUcsS0FBSyxLQUFLLEdBQUcsQ0FBQyxDQUFDLENBQUMsUUFBUSxDQUFDLENBQUMsQ0FBQyxLQUFLLENBQUMsVUFBVSxDQUFDO1FBQy9ELE1BQU0sU0FBUyxHQUFHLEtBQUssS0FBSyxHQUFHLENBQUMsQ0FBQyxDQUFDLFFBQVEsQ0FBQyxDQUFDLENBQUMsS0FBSyxDQUFDLFNBQVMsQ0FBQztRQUM3RCxNQUFNLEtBQUssR0FBRyxLQUFLLEtBQUssR0FBRyxDQUFDLENBQUMsQ0FBQyxRQUFRLENBQUMsQ0FBQyxDQUFDLEtBQUssQ0FBQyxLQUFLLENBQUM7UUFFckQsTUFBTSxRQUFRLEdBQUcsSUFBSSxLQUFLLEVBQUUsQ0FBQyxPQUFPLENBQUMsR0FBRyxFQUFFLFVBQVUsRUFBRSxTQUFTLEVBQUUsS0FBSyxDQUFDLENBQUM7UUFDeEUsSUFBSSxDQUFDLEtBQUssQ0FBQyxHQUFHLENBQUMsUUFBUSxDQUFDLENBQUM7SUFDN0IsQ0FBQzs4R0FyQlEsYUFBYTtrR0FBYixhQUFhLHNnQkNqQjFCLGlsQ0F5QkMseWVEVmEseUJBQXlCOzsyRkFFMUIsYUFBYTtrQkFaekIsU0FBUzsrQkFDSSxzQkFBc0IsbUJBT2YsdUJBQXVCLENBQUMsTUFBTSxjQUNuQyxJQUFJLFdBQ1AsQ0FBQyx5QkFBeUIsQ0FBQyIsInNvdXJjZXNDb250ZW50IjpbImltcG9ydCB7IENvbXBvbmVudCwgQ2hhbmdlRGV0ZWN0aW9uU3RyYXRlZ3ksIGJvb2xlYW5BdHRyaWJ1dGUsIElucHV0U2lnbmFsLCBpbnB1dCwgbW9kZWwsIE1vZGVsU2lnbmFsIH0gZnJvbSAnQGFuZ3VsYXIvY29yZSc7XHJcbmltcG9ydCB7IENvbG9yIH0gZnJvbSAnLi8uLi8uLi8uLi8uLi9oZWxwZXJzL2NvbG9yLmNsYXNzJztcclxuaW1wb3J0IHsgQ29sb3JQaWNrZXJJbnB1dERpcmVjdGl2ZSB9IGZyb20gJy4uLy4uLy4uLy4uL2RpcmVjdGl2ZXMvY29sb3ItcGlja2VyLWlucHV0LmRpcmVjdGl2ZSc7XHJcblxyXG5cclxuQENvbXBvbmVudCh7XHJcbiAgICBzZWxlY3RvcjogYGhzbGEtaW5wdXQtY29tcG9uZW50YCxcclxuICAgIHRlbXBsYXRlVXJsOiBgLi9oc2xhLWlucHV0LmNvbXBvbmVudC5odG1sYCxcclxuICAgIHN0eWxlVXJsczogW1xyXG4gICAgICAgIGAuLy4uLy4uL2Jhc2Uuc3R5bGUuc2Nzc2AsXHJcbiAgICAgICAgYC4vLi4vaW5wdXQuY29tcG9uZW50LnNjc3NgLFxyXG4gICAgICAgIGAuL2hzbGEtaW5wdXQuY29tcG9uZW50LnNjc3NgXHJcbiAgICBdLFxyXG4gICAgY2hhbmdlRGV0ZWN0aW9uOiBDaGFuZ2VEZXRlY3Rpb25TdHJhdGVneS5PblB1c2gsXHJcbiAgICBzdGFuZGFsb25lOiB0cnVlLFxyXG4gICAgaW1wb3J0czogW0NvbG9yUGlja2VySW5wdXREaXJlY3RpdmVdXHJcbn0pXHJcbmV4cG9ydCBjbGFzcyBIc2xhQ29tcG9uZW50IHtcclxuXHJcbiAgICBwdWJsaWMgY29sb3I6IE1vZGVsU2lnbmFsPENvbG9yPiA9IG1vZGVsLnJlcXVpcmVkPENvbG9yPigpO1xyXG5cclxuICAgIHB1YmxpYyBsYWJlbFZpc2libGU6IElucHV0U2lnbmFsPGJvb2xlYW4+ID0gaW5wdXQ8Ym9vbGVhbiwgYm9vbGVhbj4oZmFsc2UsIHsgYWxpYXM6ICdsYWJlbCcsIHRyYW5zZm9ybTogYm9vbGVhbkF0dHJpYnV0ZSB9KTtcclxuXHJcbiAgICBwdWJsaWMgaXNBbHBoYVZpc2libGU6IElucHV0U2lnbmFsPGJvb2xlYW4+ID0gaW5wdXQ8Ym9vbGVhbiwgYm9vbGVhbj4odHJ1ZSwgeyBhbGlhczogJ2FscGhhJywgdHJhbnNmb3JtOiBib29sZWFuQXR0cmlidXRlIH0pO1xyXG5cclxuICAgIHB1YmxpYyBnZXQgdmFsdWUoKSB7XHJcbiAgICAgICAgcmV0dXJuIHRoaXMuY29sb3IoKT8uZ2V0SHNsYSgpO1xyXG4gICAgfVxyXG5cclxuICAgIHB1YmxpYyBvbklucHV0Q2hhbmdlKG5ld1ZhbHVlOiBudW1iZXIsIGNvbG9yOiAnSCcgfCAnUycgfCAnTCcgfCAnQScpIHtcclxuICAgICAgICBjb25zdCB2YWx1ZSA9IHRoaXMudmFsdWU7XHJcbiAgICAgICAgY29uc3QgaHVlID0gY29sb3IgPT09ICdIJyA/IG5ld1ZhbHVlIDogdmFsdWUuaHVlO1xyXG4gICAgICAgIGNvbnN0IHNhdHVyYXRpb24gPSBjb2xvciA9PT0gJ1MnID8gbmV3VmFsdWUgOiB2YWx1ZS5zYXR1cmF0aW9uO1xyXG4gICAgICAgIGNvbnN0IGxpZ2h0bmVzcyA9IGNvbG9yID09PSAnTCcgPyBuZXdWYWx1ZSA6IHZhbHVlLmxpZ2h0bmVzcztcclxuICAgICAgICBjb25zdCBhbHBoYSA9IGNvbG9yID09PSAnQScgPyBuZXdWYWx1ZSA6IHZhbHVlLmFscGhhO1xyXG5cclxuICAgICAgICBjb25zdCBuZXdDb2xvciA9IG5ldyBDb2xvcigpLnNldEhzbGEoaHVlLCBzYXR1cmF0aW9uLCBsaWdodG5lc3MsIGFscGhhKTtcclxuICAgICAgICB0aGlzLmNvbG9yLnNldChuZXdDb2xvcik7XHJcbiAgICB9XHJcbn1cclxuIiwiPGRpdiBjbGFzcz1cImNvbHVtblwiPlxyXG4gICAgPGlucHV0IHR5cGU9XCJ0ZXh0XCIgcGF0dGVybj1cIlswLTldKlwiIG1pbj1cIjBcIiBtYXg9XCIzNjBcIiBbdmFsdWVdPVwidmFsdWU/LmdldEh1ZSgpLnRvU3RyaW5nKClcIiAoaW5wdXRDaGFuZ2UpPVwib25JbnB1dENoYW5nZSgkZXZlbnQsICdIJylcIiAvPlxyXG4gICAgQGlmIChsYWJlbFZpc2libGUoKSkge1xyXG4gICAgICAgIDxzcGFuPkg8L3NwYW4+XHJcbiAgICB9XHJcbjwvZGl2PlxyXG48ZGl2IGNsYXNzPVwiY29sdW1uXCI+XHJcbiAgICA8aW5wdXQgdHlwZT1cInRleHRcIiBwYXR0ZXJuPVwiWzAtOV0qXCIgbWluPVwiMFwiIG1heD1cIjEwMFwiIFt2YWx1ZV09XCJ2YWx1ZT8uZ2V0U2F0dXJhdGlvbigpICsgJyUnXCIgKGlucHV0Q2hhbmdlKT1cIm9uSW5wdXRDaGFuZ2UoJGV2ZW50LCAnUycpXCIgLz5cclxuICAgIEBpZiAobGFiZWxWaXNpYmxlKCkpIHtcclxuICAgICAgICA8c3Bhbj5TPC9zcGFuPlxyXG4gICAgfVxyXG48L2Rpdj5cclxuPGRpdiBjbGFzcz1cImNvbHVtblwiPlxyXG4gICAgPGlucHV0IHR5cGU9XCJ0ZXh0XCIgcGF0dGVybj1cIlswLTldKlwiIG1pbj1cIjBcIiBtYXg9XCIxMDBcIiBbdmFsdWVdPVwidmFsdWU/LmdldExpZ2h0bmVzcygpICsgJyUnXCIgKGlucHV0Q2hhbmdlKT1cIm9uSW5wdXRDaGFuZ2UoJGV2ZW50LCAnTCcpXCIgLz5cclxuICAgIEBpZiAobGFiZWxWaXNpYmxlKCkpIHtcclxuICAgICAgICA8c3Bhbj5MPC9zcGFuPlxyXG4gICAgfVxyXG48L2Rpdj5cclxuQGlmIChpc0FscGhhVmlzaWJsZSgpKSB7XHJcbiAgICA8ZGl2IGNsYXNzPVwiY29sdW1uXCI+XHJcbiAgICAgICAgPGlucHV0IHR5cGU9XCJ0ZXh0XCIgcGF0dGVybj1cIlswLTldKyhbXFwuLF1bMC05XXsxLDJ9KT9cIiBtaW49XCIwXCIgbWF4PVwiMVwiIFt2YWx1ZV09XCJ2YWx1ZT8uZ2V0QWxwaGEoKS50b1N0cmluZygpXCIgKGlucHV0Q2hhbmdlKT1cIm9uSW5wdXRDaGFuZ2UoJGV2ZW50LCAnQScpXCIgLz5cclxuICAgICAgICBAaWYgKGxhYmVsVmlzaWJsZSgpKSB7XHJcbiAgICAgICAgICAgIDxzcGFuPkE8L3NwYW4+XHJcbiAgICAgICAgfVxyXG4gICAgPC9kaXY+XHJcbn0iXX0= +diff --git a/node_modules/@iplab/ngx-color-picker/esm2022/lib/components/parts/inputs/rgba-input/rgba-input.component.mjs b/node_modules/@iplab/ngx-color-picker/esm2022/lib/components/parts/inputs/rgba-input/rgba-input.component.mjs +index 6d976e8..8989aad 100644 +--- a/node_modules/@iplab/ngx-color-picker/esm2022/lib/components/parts/inputs/rgba-input/rgba-input.component.mjs ++++ b/node_modules/@iplab/ngx-color-picker/esm2022/lib/components/parts/inputs/rgba-input/rgba-input.component.mjs +@@ -21,10 +21,10 @@ export class RgbaComponent { + this.color.set(newColor); + } + static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.0.0", ngImport: i0, type: RgbaComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); } +- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.0.0", type: RgbaComponent, isStandalone: true, selector: "rgba-input-component", inputs: { color: { classPropertyName: "color", publicName: "color", isSignal: true, isRequired: true, transformFunction: null }, labelVisible: { classPropertyName: "labelVisible", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, isAlphaVisible: { classPropertyName: "isAlphaVisible", publicName: "alpha", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { color: "colorChange" }, ngImport: i0, template: "
    \r\n \r\n @if (labelVisible()) {\r\n R\r\n }\r\n
    \r\n
    \r\n \r\n @if (labelVisible()) {\r\n G\r\n }\r\n
    \r\n
    \r\n \r\n @if (labelVisible()) {\r\n B\r\n }\r\n
    \r\n@if (isAlphaVisible()) {\r\n
    \r\n \r\n @if (labelVisible()) {\r\n A\r\n }\r\n
    \r\n}", styles: [":host,:host ::ng-deep *{padding:0;margin:0;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}\n", ":host{display:table;width:100%;text-align:center;color:#b4b4b4;font-size:11px}.column{display:table-cell;padding:0 2px}input{width:100%;border:1px solid rgb(218,218,218);color:#272727;text-align:center;font-size:12px;-webkit-appearance:none;border-radius:0;margin:0 0 6px;height:26px;outline:none}\n", ""], dependencies: [{ kind: "directive", type: ColorPickerInputDirective, selector: "[inputChange]", inputs: ["min", "max"], outputs: ["inputChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); } ++ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.0.0", type: RgbaComponent, isStandalone: true, selector: "rgba-input-component", inputs: { color: { classPropertyName: "color", publicName: "color", isSignal: true, isRequired: true, transformFunction: null }, labelVisible: { classPropertyName: "labelVisible", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, isAlphaVisible: { classPropertyName: "isAlphaVisible", publicName: "alpha", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { color: "colorChange" }, ngImport: i0, template: "
    \r\n \r\n @if (labelVisible()) {\r\n R\r\n }\r\n
    \r\n
    \r\n \r\n @if (labelVisible()) {\r\n G\r\n }\r\n
    \r\n
    \r\n \r\n @if (labelVisible()) {\r\n B\r\n }\r\n
    \r\n@if (isAlphaVisible()) {\r\n
    \r\n \r\n @if (labelVisible()) {\r\n A\r\n }\r\n
    \r\n}", styles: [":host,:host ::ng-deep *{padding:0;margin:0;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}\n", ":host{display:table;width:100%;text-align:center;color:#b4b4b4;font-size:11px}.column{display:table-cell;padding:0 2px}input{width:100%;border:1px solid rgb(218,218,218);color:#272727;text-align:center;font-size:12px;-webkit-appearance:none;border-radius:0;margin:0 0 6px;height:26px;outline:none}\n", ""], dependencies: [{ kind: "directive", type: ColorPickerInputDirective, selector: "[inputChange]", inputs: ["min", "max"], outputs: ["inputChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); } + } + i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.0.0", ngImport: i0, type: RgbaComponent, decorators: [{ + type: Component, +- args: [{ selector: `rgba-input-component`, changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, imports: [ColorPickerInputDirective], template: "
    \r\n \r\n @if (labelVisible()) {\r\n R\r\n }\r\n
    \r\n
    \r\n \r\n @if (labelVisible()) {\r\n G\r\n }\r\n
    \r\n
    \r\n \r\n @if (labelVisible()) {\r\n B\r\n }\r\n
    \r\n@if (isAlphaVisible()) {\r\n
    \r\n \r\n @if (labelVisible()) {\r\n A\r\n }\r\n
    \r\n}", styles: [":host,:host ::ng-deep *{padding:0;margin:0;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}\n", ":host{display:table;width:100%;text-align:center;color:#b4b4b4;font-size:11px}.column{display:table-cell;padding:0 2px}input{width:100%;border:1px solid rgb(218,218,218);color:#272727;text-align:center;font-size:12px;-webkit-appearance:none;border-radius:0;margin:0 0 6px;height:26px;outline:none}\n"] }] ++ args: [{ selector: `rgba-input-component`, changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, imports: [ColorPickerInputDirective], template: "
    \r\n \r\n @if (labelVisible()) {\r\n R\r\n }\r\n
    \r\n
    \r\n \r\n @if (labelVisible()) {\r\n G\r\n }\r\n
    \r\n
    \r\n \r\n @if (labelVisible()) {\r\n B\r\n }\r\n
    \r\n@if (isAlphaVisible()) {\r\n
    \r\n \r\n @if (labelVisible()) {\r\n A\r\n }\r\n
    \r\n}", styles: [":host,:host ::ng-deep *{padding:0;margin:0;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}\n", ":host{display:table;width:100%;text-align:center;color:#b4b4b4;font-size:11px}.column{display:table-cell;padding:0 2px}input{width:100%;border:1px solid rgb(218,218,218);color:#272727;text-align:center;font-size:12px;-webkit-appearance:none;border-radius:0;margin:0 0 6px;height:26px;outline:none}\n"] }] + }] }); + //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicmdiYS1pbnB1dC5jb21wb25lbnQuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi8uLi8uLi8uLi8uLi8uLi9wcm9qZWN0cy9pcGxhYi9uZ3gtY29sb3ItcGlja2VyL3NyYy9saWIvY29tcG9uZW50cy9wYXJ0cy9pbnB1dHMvcmdiYS1pbnB1dC9yZ2JhLWlucHV0LmNvbXBvbmVudC50cyIsIi4uLy4uLy4uLy4uLy4uLy4uLy4uLy4uL3Byb2plY3RzL2lwbGFiL25neC1jb2xvci1waWNrZXIvc3JjL2xpYi9jb21wb25lbnRzL3BhcnRzL2lucHV0cy9yZ2JhLWlucHV0L3JnYmEtaW5wdXQuY29tcG9uZW50Lmh0bWwiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsT0FBTyxFQUFFLFNBQVMsRUFBRSx1QkFBdUIsRUFBRSxnQkFBZ0IsRUFBZSxLQUFLLEVBQUUsS0FBSyxFQUFlLE1BQU0sZUFBZSxDQUFDO0FBQzdILE9BQU8sRUFBRSxLQUFLLEVBQUUsTUFBTSxtQ0FBbUMsQ0FBQztBQUMxRCxPQUFPLEVBQUUseUJBQXlCLEVBQUUsTUFBTSxxREFBcUQsQ0FBQzs7QUFlaEcsTUFBTSxPQUFPLGFBQWE7SUFaMUI7UUFjVyxVQUFLLEdBQXVCLEtBQUssQ0FBQyxRQUFRLEVBQVMsQ0FBQztRQUVwRCxpQkFBWSxHQUF5QixLQUFLLENBQW1CLEtBQUssRUFBRSxFQUFFLEtBQUssRUFBRSxPQUFPLEVBQUUsU0FBUyxFQUFFLGdCQUFnQixFQUFFLENBQUMsQ0FBQztRQUVySCxtQkFBYyxHQUF5QixLQUFLLENBQW1CLElBQUksRUFBRSxFQUFFLEtBQUssRUFBRSxPQUFPLEVBQUUsU0FBUyxFQUFFLGdCQUFnQixFQUFFLENBQUMsQ0FBQztLQWdCaEk7SUFkRyxJQUFXLEtBQUs7UUFDWixPQUFPLElBQUksQ0FBQyxLQUFLLEVBQUUsRUFBRSxPQUFPLEVBQUUsQ0FBQztJQUNuQyxDQUFDO0lBRU0sYUFBYSxDQUFDLFFBQWdCLEVBQUUsS0FBNEI7UUFDL0QsTUFBTSxLQUFLLEdBQUcsSUFBSSxDQUFDLEtBQUssQ0FBQztRQUN6QixNQUFNLEdBQUcsR0FBRyxLQUFLLEtBQUssR0FBRyxDQUFDLENBQUMsQ0FBQyxRQUFRLENBQUMsQ0FBQyxDQUFDLEtBQUssQ0FBQyxHQUFHLENBQUM7UUFDakQsTUFBTSxLQUFLLEdBQUcsS0FBSyxLQUFLLEdBQUcsQ0FBQyxDQUFDLENBQUMsUUFBUSxDQUFDLENBQUMsQ0FBQyxLQUFLLENBQUMsS0FBSyxDQUFDO1FBQ3JELE1BQU0sSUFBSSxHQUFHLEtBQUssS0FBSyxHQUFHLENBQUMsQ0FBQyxDQUFDLFFBQVEsQ0FBQyxDQUFDLENBQUMsS0FBSyxDQUFDLElBQUksQ0FBQztRQUNuRCxNQUFNLEtBQUssR0FBRyxLQUFLLEtBQUssR0FBRyxDQUFDLENBQUMsQ0FBQyxRQUFRLENBQUMsQ0FBQyxDQUFDLEtBQUssQ0FBQyxLQUFLLENBQUM7UUFFckQsTUFBTSxRQUFRLEdBQUcsSUFBSSxLQUFLLEVBQUUsQ0FBQyxPQUFPLENBQUMsR0FBRyxFQUFFLEtBQUssRUFBRSxJQUFJLEVBQUUsS0FBSyxDQUFDLENBQUM7UUFDOUQsSUFBSSxDQUFDLEtBQUssQ0FBQyxHQUFHLENBQUMsUUFBUSxDQUFDLENBQUM7SUFDN0IsQ0FBQzs4R0FyQlEsYUFBYTtrR0FBYixhQUFhLHNnQkNqQjFCLGlsQ0F5QkMseWVEVmEseUJBQXlCOzsyRkFFMUIsYUFBYTtrQkFaekIsU0FBUzsrQkFDSSxzQkFBc0IsbUJBT2YsdUJBQXVCLENBQUMsTUFBTSxjQUNuQyxJQUFJLFdBQ1AsQ0FBQyx5QkFBeUIsQ0FBQyIsInNvdXJjZXNDb250ZW50IjpbImltcG9ydCB7IENvbXBvbmVudCwgQ2hhbmdlRGV0ZWN0aW9uU3RyYXRlZ3ksIGJvb2xlYW5BdHRyaWJ1dGUsIElucHV0U2lnbmFsLCBpbnB1dCwgbW9kZWwsIE1vZGVsU2lnbmFsIH0gZnJvbSAnQGFuZ3VsYXIvY29yZSc7XHJcbmltcG9ydCB7IENvbG9yIH0gZnJvbSAnLi8uLi8uLi8uLi8uLi9oZWxwZXJzL2NvbG9yLmNsYXNzJztcclxuaW1wb3J0IHsgQ29sb3JQaWNrZXJJbnB1dERpcmVjdGl2ZSB9IGZyb20gJy4uLy4uLy4uLy4uL2RpcmVjdGl2ZXMvY29sb3ItcGlja2VyLWlucHV0LmRpcmVjdGl2ZSc7XHJcblxyXG5cclxuQENvbXBvbmVudCh7XHJcbiAgICBzZWxlY3RvcjogYHJnYmEtaW5wdXQtY29tcG9uZW50YCxcclxuICAgIHRlbXBsYXRlVXJsOiBgLi9yZ2JhLWlucHV0LmNvbXBvbmVudC5odG1sYCxcclxuICAgIHN0eWxlVXJsczogW1xyXG4gICAgICAgIGAuLy4uLy4uL2Jhc2Uuc3R5bGUuc2Nzc2AsXHJcbiAgICAgICAgYC4vLi4vaW5wdXQuY29tcG9uZW50LnNjc3NgLFxyXG4gICAgICAgIGAuL3JnYmEtaW5wdXQuY29tcG9uZW50LnNjc3NgXHJcbiAgICBdLFxyXG4gICAgY2hhbmdlRGV0ZWN0aW9uOiBDaGFuZ2VEZXRlY3Rpb25TdHJhdGVneS5PblB1c2gsXHJcbiAgICBzdGFuZGFsb25lOiB0cnVlLFxyXG4gICAgaW1wb3J0czogW0NvbG9yUGlja2VySW5wdXREaXJlY3RpdmVdXHJcbn0pXHJcbmV4cG9ydCBjbGFzcyBSZ2JhQ29tcG9uZW50IHtcclxuXHJcbiAgICBwdWJsaWMgY29sb3I6IE1vZGVsU2lnbmFsPENvbG9yPiA9IG1vZGVsLnJlcXVpcmVkPENvbG9yPigpO1xyXG5cclxuICAgIHB1YmxpYyBsYWJlbFZpc2libGU6IElucHV0U2lnbmFsPGJvb2xlYW4+ID0gaW5wdXQ8Ym9vbGVhbiwgYm9vbGVhbj4oZmFsc2UsIHsgYWxpYXM6ICdsYWJlbCcsIHRyYW5zZm9ybTogYm9vbGVhbkF0dHJpYnV0ZSB9KTtcclxuXHJcbiAgICBwdWJsaWMgaXNBbHBoYVZpc2libGU6IElucHV0U2lnbmFsPGJvb2xlYW4+ID0gaW5wdXQ8Ym9vbGVhbiwgYm9vbGVhbj4odHJ1ZSwgeyBhbGlhczogJ2FscGhhJywgdHJhbnNmb3JtOiBib29sZWFuQXR0cmlidXRlIH0pO1xyXG5cclxuICAgIHB1YmxpYyBnZXQgdmFsdWUoKSB7XHJcbiAgICAgICAgcmV0dXJuIHRoaXMuY29sb3IoKT8uZ2V0UmdiYSgpO1xyXG4gICAgfVxyXG5cclxuICAgIHB1YmxpYyBvbklucHV0Q2hhbmdlKG5ld1ZhbHVlOiBudW1iZXIsIGNvbG9yOiAnUicgfCAnRycgfCAnQicgfCAnQScpIHtcclxuICAgICAgICBjb25zdCB2YWx1ZSA9IHRoaXMudmFsdWU7XHJcbiAgICAgICAgY29uc3QgcmVkID0gY29sb3IgPT09ICdSJyA/IG5ld1ZhbHVlIDogdmFsdWUucmVkO1xyXG4gICAgICAgIGNvbnN0IGdyZWVuID0gY29sb3IgPT09ICdHJyA/IG5ld1ZhbHVlIDogdmFsdWUuZ3JlZW47XHJcbiAgICAgICAgY29uc3QgYmx1ZSA9IGNvbG9yID09PSAnQicgPyBuZXdWYWx1ZSA6IHZhbHVlLmJsdWU7XHJcbiAgICAgICAgY29uc3QgYWxwaGEgPSBjb2xvciA9PT0gJ0EnID8gbmV3VmFsdWUgOiB2YWx1ZS5hbHBoYTtcclxuXHJcbiAgICAgICAgY29uc3QgbmV3Q29sb3IgPSBuZXcgQ29sb3IoKS5zZXRSZ2JhKHJlZCwgZ3JlZW4sIGJsdWUsIGFscGhhKTtcclxuICAgICAgICB0aGlzLmNvbG9yLnNldChuZXdDb2xvcik7XHJcbiAgICB9XHJcbn1cclxuIiwiPGRpdiBjbGFzcz1cImNvbHVtblwiPlxyXG4gICAgPGlucHV0IHR5cGU9XCJ0ZXh0XCIgcGF0dGVybj1cIlswLTldKlwiIG1pbj1cIjBcIiBtYXg9XCIyNTVcIiBbdmFsdWVdPVwidmFsdWU/LmdldFJlZCgpLnRvU3RyaW5nKClcIiAoaW5wdXRDaGFuZ2UpPVwib25JbnB1dENoYW5nZSgkZXZlbnQsICdSJylcIiAvPlxyXG4gICAgQGlmIChsYWJlbFZpc2libGUoKSkge1xyXG4gICAgICAgIDxzcGFuPlI8L3NwYW4+XHJcbiAgICB9XHJcbjwvZGl2PlxyXG48ZGl2IGNsYXNzPVwiY29sdW1uXCI+XHJcbiAgICA8aW5wdXQgdHlwZT1cInRleHRcIiBwYXR0ZXJuPVwiWzAtOV0qXCIgbWluPVwiMFwiIG1heD1cIjI1NVwiIFt2YWx1ZV09XCJ2YWx1ZT8uZ2V0R3JlZW4oKS50b1N0cmluZygpXCIgKGlucHV0Q2hhbmdlKT1cIm9uSW5wdXRDaGFuZ2UoJGV2ZW50LCAnRycpXCIgLz5cclxuICAgIEBpZiAobGFiZWxWaXNpYmxlKCkpIHtcclxuICAgICAgICA8c3Bhbj5HPC9zcGFuPlxyXG4gICAgfVxyXG48L2Rpdj5cclxuPGRpdiBjbGFzcz1cImNvbHVtblwiPlxyXG4gICAgPGlucHV0IHR5cGU9XCJ0ZXh0XCIgcGF0dGVybj1cIlswLTldKlwiIG1pbj1cIjBcIiBtYXg9XCIyNTVcIiBbdmFsdWVdPVwidmFsdWU/LmdldEJsdWUoKS50b1N0cmluZygpXCIgKGlucHV0Q2hhbmdlKT1cIm9uSW5wdXRDaGFuZ2UoJGV2ZW50LCAnQicpXCIgLz5cclxuICAgIEBpZiAobGFiZWxWaXNpYmxlKCkpIHtcclxuICAgICAgICA8c3Bhbj5CPC9zcGFuPlxyXG4gICAgfVxyXG48L2Rpdj5cclxuQGlmIChpc0FscGhhVmlzaWJsZSgpKSB7XHJcbiAgICA8ZGl2IGNsYXNzPVwiY29sdW1uXCI+XHJcbiAgICAgICAgPGlucHV0IHR5cGU9XCJ0ZXh0XCIgcGF0dGVybj1cIlswLTldKyhbXFwuLF1bMC05XXsxLDJ9KT9cIiBtaW49XCIwXCIgbWF4PVwiMVwiIFt2YWx1ZV09XCJ2YWx1ZT8uZ2V0QWxwaGEoKS50b1N0cmluZygpXCIgKGlucHV0Q2hhbmdlKT1cIm9uSW5wdXRDaGFuZ2UoJGV2ZW50LCAnQScpXCIgLz5cclxuICAgICAgICBAaWYgKGxhYmVsVmlzaWJsZSgpKSB7XHJcbiAgICAgICAgICAgIDxzcGFuPkE8L3NwYW4+XHJcbiAgICAgICAgfVxyXG4gICAgPC9kaXY+XHJcbn0iXX0= +diff --git a/node_modules/@iplab/ngx-color-picker/fesm2022/iplab-ngx-color-picker.mjs b/node_modules/@iplab/ngx-color-picker/fesm2022/iplab-ngx-color-picker.mjs +index a3b270c..40bea7f 100644 +--- a/node_modules/@iplab/ngx-color-picker/fesm2022/iplab-ngx-color-picker.mjs ++++ b/node_modules/@iplab/ngx-color-picker/fesm2022/iplab-ngx-color-picker.mjs +@@ -1123,11 +1123,11 @@ class RgbaComponent { + this.color.set(newColor); + } + static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.0.0", ngImport: i0, type: RgbaComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); } +- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.0.0", type: RgbaComponent, isStandalone: true, selector: "rgba-input-component", inputs: { color: { classPropertyName: "color", publicName: "color", isSignal: true, isRequired: true, transformFunction: null }, labelVisible: { classPropertyName: "labelVisible", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, isAlphaVisible: { classPropertyName: "isAlphaVisible", publicName: "alpha", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { color: "colorChange" }, ngImport: i0, template: "
    \r\n \r\n @if (labelVisible()) {\r\n R\r\n }\r\n
    \r\n
    \r\n \r\n @if (labelVisible()) {\r\n G\r\n }\r\n
    \r\n
    \r\n \r\n @if (labelVisible()) {\r\n B\r\n }\r\n
    \r\n@if (isAlphaVisible()) {\r\n
    \r\n \r\n @if (labelVisible()) {\r\n A\r\n }\r\n
    \r\n}", styles: [":host,:host ::ng-deep *{padding:0;margin:0;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}\n", ":host{display:table;width:100%;text-align:center;color:#b4b4b4;font-size:11px}.column{display:table-cell;padding:0 2px}input{width:100%;border:1px solid rgb(218,218,218);color:#272727;text-align:center;font-size:12px;-webkit-appearance:none;border-radius:0;margin:0 0 6px;height:26px;outline:none}\n", ""], dependencies: [{ kind: "directive", type: ColorPickerInputDirective, selector: "[inputChange]", inputs: ["min", "max"], outputs: ["inputChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); } ++ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.0.0", type: RgbaComponent, isStandalone: true, selector: "rgba-input-component", inputs: { color: { classPropertyName: "color", publicName: "color", isSignal: true, isRequired: true, transformFunction: null }, labelVisible: { classPropertyName: "labelVisible", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, isAlphaVisible: { classPropertyName: "isAlphaVisible", publicName: "alpha", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { color: "colorChange" }, ngImport: i0, template: "
    \r\n \r\n @if (labelVisible()) {\r\n R\r\n }\r\n
    \r\n
    \r\n \r\n @if (labelVisible()) {\r\n G\r\n }\r\n
    \r\n
    \r\n \r\n @if (labelVisible()) {\r\n B\r\n }\r\n
    \r\n@if (isAlphaVisible()) {\r\n
    \r\n \r\n @if (labelVisible()) {\r\n A\r\n }\r\n
    \r\n}", styles: [":host,:host ::ng-deep *{padding:0;margin:0;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}\n", ":host{display:table;width:100%;text-align:center;color:#b4b4b4;font-size:11px}.column{display:table-cell;padding:0 2px}input{width:100%;border:1px solid rgb(218,218,218);color:#272727;text-align:center;font-size:12px;-webkit-appearance:none;border-radius:0;margin:0 0 6px;height:26px;outline:none}\n", ""], dependencies: [{ kind: "directive", type: ColorPickerInputDirective, selector: "[inputChange]", inputs: ["min", "max"], outputs: ["inputChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); } + } + i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.0.0", ngImport: i0, type: RgbaComponent, decorators: [{ + type: Component, +- args: [{ selector: `rgba-input-component`, changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, imports: [ColorPickerInputDirective], template: "
    \r\n \r\n @if (labelVisible()) {\r\n R\r\n }\r\n
    \r\n
    \r\n \r\n @if (labelVisible()) {\r\n G\r\n }\r\n
    \r\n
    \r\n \r\n @if (labelVisible()) {\r\n B\r\n }\r\n
    \r\n@if (isAlphaVisible()) {\r\n
    \r\n \r\n @if (labelVisible()) {\r\n A\r\n }\r\n
    \r\n}", styles: [":host,:host ::ng-deep *{padding:0;margin:0;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}\n", ":host{display:table;width:100%;text-align:center;color:#b4b4b4;font-size:11px}.column{display:table-cell;padding:0 2px}input{width:100%;border:1px solid rgb(218,218,218);color:#272727;text-align:center;font-size:12px;-webkit-appearance:none;border-radius:0;margin:0 0 6px;height:26px;outline:none}\n"] }] ++ args: [{ selector: `rgba-input-component`, changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, imports: [ColorPickerInputDirective], template: "
    \r\n \r\n @if (labelVisible()) {\r\n R\r\n }\r\n
    \r\n
    \r\n \r\n @if (labelVisible()) {\r\n G\r\n }\r\n
    \r\n
    \r\n \r\n @if (labelVisible()) {\r\n B\r\n }\r\n
    \r\n@if (isAlphaVisible()) {\r\n
    \r\n \r\n @if (labelVisible()) {\r\n A\r\n }\r\n
    \r\n}", styles: [":host,:host ::ng-deep *{padding:0;margin:0;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}\n", ":host{display:table;width:100%;text-align:center;color:#b4b4b4;font-size:11px}.column{display:table-cell;padding:0 2px}input{width:100%;border:1px solid rgb(218,218,218);color:#272727;text-align:center;font-size:12px;-webkit-appearance:none;border-radius:0;margin:0 0 6px;height:26px;outline:none}\n"] }] + }] }); + + class HslaComponent { +@@ -1149,11 +1149,11 @@ class HslaComponent { + this.color.set(newColor); + } + static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.0.0", ngImport: i0, type: HslaComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); } +- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.0.0", type: HslaComponent, isStandalone: true, selector: "hsla-input-component", inputs: { color: { classPropertyName: "color", publicName: "color", isSignal: true, isRequired: true, transformFunction: null }, labelVisible: { classPropertyName: "labelVisible", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, isAlphaVisible: { classPropertyName: "isAlphaVisible", publicName: "alpha", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { color: "colorChange" }, ngImport: i0, template: "
    \r\n \r\n @if (labelVisible()) {\r\n H\r\n }\r\n
    \r\n
    \r\n \r\n @if (labelVisible()) {\r\n S\r\n }\r\n
    \r\n
    \r\n \r\n @if (labelVisible()) {\r\n L\r\n }\r\n
    \r\n@if (isAlphaVisible()) {\r\n
    \r\n \r\n @if (labelVisible()) {\r\n A\r\n }\r\n
    \r\n}", styles: [":host,:host ::ng-deep *{padding:0;margin:0;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}\n", ":host{display:table;width:100%;text-align:center;color:#b4b4b4;font-size:11px}.column{display:table-cell;padding:0 2px}input{width:100%;border:1px solid rgb(218,218,218);color:#272727;text-align:center;font-size:12px;-webkit-appearance:none;border-radius:0;margin:0 0 6px;height:26px;outline:none}\n", ""], dependencies: [{ kind: "directive", type: ColorPickerInputDirective, selector: "[inputChange]", inputs: ["min", "max"], outputs: ["inputChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); } ++ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.0.0", type: HslaComponent, isStandalone: true, selector: "hsla-input-component", inputs: { color: { classPropertyName: "color", publicName: "color", isSignal: true, isRequired: true, transformFunction: null }, labelVisible: { classPropertyName: "labelVisible", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, isAlphaVisible: { classPropertyName: "isAlphaVisible", publicName: "alpha", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { color: "colorChange" }, ngImport: i0, template: "
    \r\n \r\n @if (labelVisible()) {\r\n H\r\n }\r\n
    \r\n
    \r\n \r\n @if (labelVisible()) {\r\n S\r\n }\r\n
    \r\n
    \r\n \r\n @if (labelVisible()) {\r\n L\r\n }\r\n
    \r\n@if (isAlphaVisible()) {\r\n
    \r\n \r\n @if (labelVisible()) {\r\n A\r\n }\r\n
    \r\n}", styles: [":host,:host ::ng-deep *{padding:0;margin:0;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}\n", ":host{display:table;width:100%;text-align:center;color:#b4b4b4;font-size:11px}.column{display:table-cell;padding:0 2px}input{width:100%;border:1px solid rgb(218,218,218);color:#272727;text-align:center;font-size:12px;-webkit-appearance:none;border-radius:0;margin:0 0 6px;height:26px;outline:none}\n", ""], dependencies: [{ kind: "directive", type: ColorPickerInputDirective, selector: "[inputChange]", inputs: ["min", "max"], outputs: ["inputChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); } + } + i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.0.0", ngImport: i0, type: HslaComponent, decorators: [{ + type: Component, +- args: [{ selector: `hsla-input-component`, changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, imports: [ColorPickerInputDirective], template: "
    \r\n \r\n @if (labelVisible()) {\r\n H\r\n }\r\n
    \r\n
    \r\n \r\n @if (labelVisible()) {\r\n S\r\n }\r\n
    \r\n
    \r\n \r\n @if (labelVisible()) {\r\n L\r\n }\r\n
    \r\n@if (isAlphaVisible()) {\r\n
    \r\n \r\n @if (labelVisible()) {\r\n A\r\n }\r\n
    \r\n}", styles: [":host,:host ::ng-deep *{padding:0;margin:0;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}\n", ":host{display:table;width:100%;text-align:center;color:#b4b4b4;font-size:11px}.column{display:table-cell;padding:0 2px}input{width:100%;border:1px solid rgb(218,218,218);color:#272727;text-align:center;font-size:12px;-webkit-appearance:none;border-radius:0;margin:0 0 6px;height:26px;outline:none}\n"] }] ++ args: [{ selector: `hsla-input-component`, changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, imports: [ColorPickerInputDirective], template: "
    \r\n \r\n @if (labelVisible()) {\r\n H\r\n }\r\n
    \r\n
    \r\n \r\n @if (labelVisible()) {\r\n S\r\n }\r\n
    \r\n
    \r\n \r\n @if (labelVisible()) {\r\n L\r\n }\r\n
    \r\n@if (isAlphaVisible()) {\r\n
    \r\n \r\n @if (labelVisible()) {\r\n A\r\n }\r\n
    \r\n}", styles: [":host,:host ::ng-deep *{padding:0;margin:0;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}\n", ":host{display:table;width:100%;text-align:center;color:#b4b4b4;font-size:11px}.column{display:table-cell;padding:0 2px}input{width:100%;border:1px solid rgb(218,218,218);color:#272727;text-align:center;font-size:12px;-webkit-appearance:none;border-radius:0;margin:0 0 6px;height:26px;outline:none}\n"] }] + }] }); + + class HexComponent { +@@ -1184,11 +1184,11 @@ class HexComponent { + } + } + static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.0.0", ngImport: i0, type: HexComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); } +- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.0.0", type: HexComponent, isStandalone: true, selector: "hex-input-component", inputs: { color: { classPropertyName: "color", publicName: "color", isSignal: true, isRequired: true, transformFunction: null }, labelVisible: { classPropertyName: "labelVisible", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, prefixValue: { classPropertyName: "prefixValue", publicName: "prefix", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { color: "colorChange" }, ngImport: i0, template: "
    \r\n \r\n @if (labelVisible()) {\r\n HEX\r\n }\r\n
    ", styles: [":host,:host ::ng-deep *{padding:0;margin:0;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}\n", ":host{display:table;width:100%;text-align:center;color:#b4b4b4;font-size:11px}.column{display:table-cell;padding:0 2px}input{width:100%;border:1px solid rgb(218,218,218);color:#272727;text-align:center;font-size:12px;-webkit-appearance:none;border-radius:0;margin:0 0 6px;height:26px;outline:none}\n", ""], changeDetection: i0.ChangeDetectionStrategy.OnPush }); } ++ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.0.0", type: HexComponent, isStandalone: true, selector: "hex-input-component", inputs: { color: { classPropertyName: "color", publicName: "color", isSignal: true, isRequired: true, transformFunction: null }, labelVisible: { classPropertyName: "labelVisible", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, prefixValue: { classPropertyName: "prefixValue", publicName: "prefix", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { color: "colorChange" }, ngImport: i0, template: "
    \r\n \r\n @if (labelVisible()) {\r\n HEX\r\n }\r\n
    ", styles: [":host,:host ::ng-deep *{padding:0;margin:0;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}\n", ":host{display:table;width:100%;text-align:center;color:#b4b4b4;font-size:11px}.column{display:table-cell;padding:0 2px}input{width:100%;border:1px solid rgb(218,218,218);color:#272727;text-align:center;font-size:12px;-webkit-appearance:none;border-radius:0;margin:0 0 6px;height:26px;outline:none}\n", ""], changeDetection: i0.ChangeDetectionStrategy.OnPush }); } + } + i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.0.0", ngImport: i0, type: HexComponent, decorators: [{ + type: Component, +- args: [{ selector: `hex-input-component`, changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, template: "
    \r\n \r\n @if (labelVisible()) {\r\n HEX\r\n }\r\n
    ", styles: [":host,:host ::ng-deep *{padding:0;margin:0;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}\n", ":host{display:table;width:100%;text-align:center;color:#b4b4b4;font-size:11px}.column{display:table-cell;padding:0 2px}input{width:100%;border:1px solid rgb(218,218,218);color:#272727;text-align:center;font-size:12px;-webkit-appearance:none;border-radius:0;margin:0 0 6px;height:26px;outline:none}\n"] }] ++ args: [{ selector: `hex-input-component`, changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, template: "
    \r\n \r\n @if (labelVisible()) {\r\n HEX\r\n }\r\n
    ", styles: [":host,:host ::ng-deep *{padding:0;margin:0;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}\n", ":host{display:table;width:100%;text-align:center;color:#b4b4b4;font-size:11px}.column{display:table-cell;padding:0 2px}input{width:100%;border:1px solid rgb(218,218,218);color:#272727;text-align:center;font-size:12px;-webkit-appearance:none;border-radius:0;margin:0 0 6px;height:26px;outline:none}\n"] }] + }] }); + + const OpacityAnimation = trigger('opacityAnimation', [ diff --git a/ui-ngx/pom.xml b/ui-ngx/pom.xml index f2aadd3887..56fc4ed628 100644 --- a/ui-ngx/pom.xml +++ b/ui-ngx/pom.xml @@ -56,7 +56,7 @@ install-node-and-yarn - v20.18.0 + v22.18.0 v1.22.22 diff --git a/ui-ngx/src/app/core/auth/auth.models.ts b/ui-ngx/src/app/core/auth/auth.models.ts index 142d845cf4..21759fbca0 100644 --- a/ui-ngx/src/app/core/auth/auth.models.ts +++ b/ui-ngx/src/app/core/auth/auth.models.ts @@ -31,6 +31,10 @@ export interface SysParamsState { maxDebugModeDurationMinutes: number; maxDataPointsPerRollingArg: number; maxArgumentsPerCF: number; + minAllowedDeduplicationIntervalInSecForCF: number; + minAllowedAggregationIntervalInSecForCF: number; + minAllowedScheduledUpdateIntervalInSecForCF: number; + maxRelationLevelPerCfArgument: number; ruleChainDebugPerTenantLimitsConfiguration?: string; calculatedFieldDebugPerTenantLimitsConfiguration?: string; trendzSettings: TrendzSettings; diff --git a/ui-ngx/src/app/core/auth/auth.reducer.ts b/ui-ngx/src/app/core/auth/auth.reducer.ts index 785f40ce3c..af040a6d53 100644 --- a/ui-ngx/src/app/core/auth/auth.reducer.ts +++ b/ui-ngx/src/app/core/auth/auth.reducer.ts @@ -33,6 +33,10 @@ const emptyUserAuthState: AuthPayload = { mobileQrEnabled: false, maxResourceSize: 0, maxArgumentsPerCF: 0, + minAllowedDeduplicationIntervalInSecForCF: 0, + minAllowedAggregationIntervalInSecForCF: 0, + minAllowedScheduledUpdateIntervalInSecForCF: 0, + maxRelationLevelPerCfArgument: 0, maxDataPointsPerRollingArg: 0, maxDebugModeDurationMinutes: 0, userSettings: initialUserSettings, diff --git a/ui-ngx/src/app/core/auth/auth.service.ts b/ui-ngx/src/app/core/auth/auth.service.ts index f8ffd5e8b6..3b6a15688c 100644 --- a/ui-ngx/src/app/core/auth/auth.service.ts +++ b/ui-ngx/src/app/core/auth/auth.service.ts @@ -68,6 +68,7 @@ export class AuthService { redirectUrl: string; oauth2Clients: Array = null; twoFactorAuthProviders: Array = null; + forceTwoFactorAuthProviders: Array = null; private refreshTokenSubject: ReplaySubject = null; private jwtHelper = new JwtHelperService(); @@ -117,6 +118,9 @@ export class AuthService { if (loginResponse.scope === Authority.PRE_VERIFICATION_TOKEN) { this.router.navigateByUrl(`login/mfa`); } + if (loginResponse.scope === Authority.MFA_CONFIGURATION_TOKEN) { + this.router.navigateByUrl(`login/force-mfa`); + } } )); } @@ -239,6 +243,15 @@ export class AuthService { ); } + public getAvailableTwoFaProviders(): Observable> { + return this.http.get>(`/api/2fa/providers`, defaultHttpOptions()).pipe( + catchError(() => of([])), + tap((providers) => { + this.forceTwoFactorAuthProviders = providers; + }) + ); + } + public forceDefaultPlace(authState?: AuthState, path?: string, params?: any): boolean { if (authState && authState.authUser) { if (authState.authUser.authority === Authority.TENANT_ADMIN || authState.authUser.authority === Authority.CUSTOMER_USER) { @@ -266,6 +279,8 @@ export class AuthService { if (isAuthenticated) { if (authState.authUser.authority === Authority.PRE_VERIFICATION_TOKEN) { result = this.router.parseUrl('login/mfa'); + } else if (authState.authUser.authority === Authority.MFA_CONFIGURATION_TOKEN) { + result = this.router.parseUrl('login/force-mfa'); } else if (!path || path === 'login' || this.forceDefaultPlace(authState, path, params)) { if (this.redirectUrl) { const redirectUrl = this.redirectUrl; @@ -399,7 +414,7 @@ export class AuthService { loadUserSubject.error(err); } ); - } else if (authPayload.authUser?.authority === Authority.PRE_VERIFICATION_TOKEN) { + } else if (authPayload.authUser?.authority === Authority.PRE_VERIFICATION_TOKEN || authPayload.authUser?.authority === Authority.MFA_CONFIGURATION_TOKEN) { loadUserSubject.next(authPayload); loadUserSubject.complete(); } else if (authPayload.authUser?.userId) { diff --git a/ui-ngx/src/app/core/guards/auth.guard.ts b/ui-ngx/src/app/core/guards/auth.guard.ts index f6d8753882..5090af97ec 100644 --- a/ui-ngx/src/app/core/guards/auth.guard.ts +++ b/ui-ngx/src/app/core/guards/auth.guard.ts @@ -104,6 +104,16 @@ export class AuthGuard { } this.authService.logout(); return of(this.authService.defaultUrl(false)); + } else if (path === 'login.force-mfa') { + if (authState.authUser?.authority === Authority.MFA_CONFIGURATION_TOKEN) { + return this.authService.getAvailableTwoFaProviders().pipe( + map(() => { + return true; + }) + ); + } + this.authService.logout(); + return of(this.authService.defaultUrl(false)); } else { return of(true); } @@ -121,7 +131,7 @@ export class AuthGuard { } } if (this.mobileService.isMobileApp() && !path.startsWith('dashboard.')) { - this.mobileService.handleMobileNavigation(path, params); + this.mobileService.handleMobileNavigation(path, params, lastChild.queryParams); return of(false); } if (authState.authUser.authority === Authority.PRE_VERIFICATION_TOKEN) { diff --git a/ui-ngx/src/app/core/http/admin.service.ts b/ui-ngx/src/app/core/http/admin.service.ts index 55eaa16b90..36bb931f93 100644 --- a/ui-ngx/src/app/core/http/admin.service.ts +++ b/ui-ngx/src/app/core/http/admin.service.ts @@ -21,9 +21,9 @@ import { HttpClient } from '@angular/common/http'; import { AdminSettings, AutoCommitSettings, - MailConfigTemplate, FeaturesInfo, JwtSettings, + MailConfigTemplate, MailServerSettings, RepositorySettings, RepositorySettingsInfo, diff --git a/ui-ngx/src/app/core/http/asset.service.ts b/ui-ngx/src/app/core/http/asset.service.ts index a24be23593..0efeec3eef 100644 --- a/ui-ngx/src/app/core/http/asset.service.ts +++ b/ui-ngx/src/app/core/http/asset.service.ts @@ -15,7 +15,7 @@ /// import { Injectable } from '@angular/core'; -import { defaultHttpOptionsFromConfig, RequestConfig } from './http-utils'; +import { createDefaultHttpOptions, defaultHttpOptionsFromConfig, RequestConfig } from './http-utils'; import { Observable } from 'rxjs'; import { HttpClient } from '@angular/common/http'; import { PageLink } from '@shared/models/page/page-link'; @@ -23,6 +23,7 @@ import { PageData } from '@shared/models/page/page-data'; import { EntitySubtype } from '@shared/models/entity-type.models'; import { Asset, AssetInfo, AssetSearchQuery } from '@shared/models/asset.models'; import { BulkImportRequest, BulkImportResult } from '@shared/import-export/import-export.models'; +import { SaveEntityParams } from '@shared/models/entity.models'; @Injectable({ providedIn: 'root' @@ -69,8 +70,10 @@ export class AssetService { return this.http.get(`/api/asset/info/${assetId}`, defaultHttpOptionsFromConfig(config)); } - public saveAsset(asset: Asset, config?: RequestConfig): Observable { - return this.http.post('/api/asset', asset, defaultHttpOptionsFromConfig(config)); + public saveAsset(asset: Asset, config?: RequestConfig): Observable; + public saveAsset(asset: Asset, saveParams: SaveEntityParams, config?: RequestConfig): Observable; + public saveAsset(asset: Asset, saveParamsOrConfig?: SaveEntityParams | RequestConfig, config?: RequestConfig): Observable { + return this.http.post('/api/asset', asset, createDefaultHttpOptions(saveParamsOrConfig, config)); } public deleteAsset(assetId: string, config?: RequestConfig) { diff --git a/ui-ngx/src/app/core/http/attribute.service.ts b/ui-ngx/src/app/core/http/attribute.service.ts index 8687d3ec0a..fc87702628 100644 --- a/ui-ngx/src/app/core/http/attribute.service.ts +++ b/ui-ngx/src/app/core/http/attribute.service.ts @@ -15,7 +15,7 @@ /// import { Injectable } from '@angular/core'; -import { defaultHttpOptionsFromConfig, RequestConfig } from './http-utils'; +import { defaultHttpOptionsFromConfig, defaultHttpOptionsFromParams, RequestConfig } from './http-utils'; import { forkJoin, Observable, of } from 'rxjs'; import { HttpClient } from '@angular/common/http'; import { EntityId } from '@shared/models/id/entity-id'; @@ -35,47 +35,37 @@ export class AttributeService { public getEntityAttributes(entityId: EntityId, attributeScope?: AttributeScope, keys?: Array, config?: RequestConfig): Observable> { let url = `/api/plugins/telemetry/${entityId.entityType}/${entityId.id}/values/attributes`; - + let queryParams: object = null; if (attributeScope) { url += `/${attributeScope}`; } if (keys && keys.length) { - url += `?keys=${keys.join(',')}`; + queryParams = {key: keys}; } - return this.http.get>(url, defaultHttpOptionsFromConfig(config)); + return this.http.get>(url, defaultHttpOptionsFromParams(queryParams, config)); } public deleteEntityAttributes(entityId: EntityId, attributeScope: AttributeScope, attributes: Array, config?: RequestConfig): Observable { - const keys = attributes.map(attribute => encodeURIComponent(attribute.key)).join(','); - return this.http.delete(`/api/plugins/telemetry/${entityId.entityType}/${entityId.id}/${attributeScope}` + - `?keys=${keys}`, - defaultHttpOptionsFromConfig(config)); + return this.http.delete(`/api/plugins/telemetry/${entityId.entityType}/${entityId.id}/${attributeScope}`, + defaultHttpOptionsFromParams({key: attributes.map(attribute => attribute.key)}, config)); } public deleteEntityTimeseries(entityId: EntityId, timeseries: Array, deleteAllDataForKeys = false, startTs?: number, endTs?: number, rewriteLatestIfDeleted = false, deleteLatest = true, config?: RequestConfig): Observable { - const keys = timeseries.map(attribute => encodeURIComponent(attribute.key)).join(','); - let url = `/api/plugins/telemetry/${entityId.entityType}/${entityId.id}/timeseries/delete?keys=${keys}`; - if (isDefinedAndNotNull(deleteAllDataForKeys)) { - url += `&deleteAllDataForKeys=${deleteAllDataForKeys}`; - } - if (isDefinedAndNotNull(rewriteLatestIfDeleted)) { - url += `&rewriteLatestIfDeleted=${rewriteLatestIfDeleted}`; - } - if (isDefinedAndNotNull(deleteLatest)) { - url += `&deleteLatest=${deleteLatest}`; - } - if (isDefinedAndNotNull(startTs)) { - url += `&startTs=${startTs}`; - } - if (isDefinedAndNotNull(endTs)) { - url += `&endTs=${endTs}`; - } - return this.http.delete(url, defaultHttpOptionsFromConfig(config)); + const queryParams = { + key: timeseries.map(key => key.key), + deleteAllDataForKeys: deleteAllDataForKeys, + rewriteLatestIfDeleted: rewriteLatestIfDeleted, + deleteLatest: deleteLatest, + startTs: startTs, + endTs: endTs + }; + return this.http.delete(`/api/plugins/telemetry/${entityId.entityType}/${entityId.id}/timeseries/delete`, + defaultHttpOptionsFromParams(queryParams, config)); } public saveEntityAttributes(entityId: EntityId, attributeScope: AttributeScope, attributes: Array, @@ -138,32 +128,29 @@ export class AttributeService { limit: number = 100, agg: AggregationType = AggregationType.NONE, interval?: number, orderBy: DataSortOrder = DataSortOrder.DESC, useStrictDataTypes: boolean = false, config?: RequestConfig): Observable { - let url = `/api/plugins/telemetry/${entityId.entityType}/${entityId.id}/values/timeseries?keys=${keys.join(',')}&startTs=${startTs}&endTs=${endTs}`; - if (isDefinedAndNotNull(limit)) { - url += `&limit=${limit}`; - } - if (isDefinedAndNotNull(agg)) { - url += `&agg=${agg}`; - } - if (isDefinedAndNotNull(interval)) { - url += `&interval=${interval}`; - } - if (isDefinedAndNotNull(orderBy)) { - url += `&orderBy=${orderBy}`; - } - if (isDefinedAndNotNull(useStrictDataTypes)) { - url += `&useStrictDataTypes=${useStrictDataTypes}`; - } - - return this.http.get(url, defaultHttpOptionsFromConfig(config)); + const queryParams = { + key: keys, + startTs: startTs, + endTs: endTs, + limit: limit, + agg: agg, + interval: interval, + orderBy: orderBy, + useStrictDataTypes: useStrictDataTypes + } + return this.http.get(`/api/plugins/telemetry/${entityId.entityType}/${entityId.id}/values/timeseries`, + defaultHttpOptionsFromParams(queryParams, config)); } public getEntityTimeseriesLatest(entityId: EntityId, keys?: Array, useStrictDataTypes = false, config?: RequestConfig): Observable { - let url = `/api/plugins/telemetry/${entityId.entityType}/${entityId.id}/values/timeseries?useStrictDataTypes=${useStrictDataTypes}`; + const queryParams: Record = { + useStrictDataTypes: useStrictDataTypes + } if (isDefinedAndNotNull(keys) && keys.length) { - url += `&keys=${keys.join(',')}`; + queryParams.key = keys; } - return this.http.get(url, defaultHttpOptionsFromConfig(config)); + return this.http.get(`/api/plugins/telemetry/${entityId.entityType}/${entityId.id}/values/timeseries`, + defaultHttpOptionsFromParams(queryParams, config)); } } diff --git a/ui-ngx/src/app/core/http/calculated-fields.service.ts b/ui-ngx/src/app/core/http/calculated-fields.service.ts index 66c0cb609e..631550ae9e 100644 --- a/ui-ngx/src/app/core/http/calculated-fields.service.ts +++ b/ui-ngx/src/app/core/http/calculated-fields.service.ts @@ -15,11 +15,15 @@ /// import { Injectable } from '@angular/core'; -import { defaultHttpOptionsFromConfig, RequestConfig } from './http-utils'; +import { createDefaultHttpOptions, defaultHttpOptionsFromConfig, RequestConfig } from './http-utils'; import { Observable } from 'rxjs'; import { HttpClient } from '@angular/common/http'; import { PageData } from '@shared/models/page/page-data'; -import { CalculatedField, CalculatedFieldTestScriptInputParams } from '@shared/models/calculated-field.models'; +import { + CalculatedField, + CalculatedFieldTestScriptInputParams, + CalculatedFieldType +} from '@shared/models/calculated-field.models'; import { PageLink } from '@shared/models/page/page-link'; import { EntityId } from '@shared/models/id/entity-id'; import { EntityTestScriptResult } from '@shared/models/entity.models'; @@ -46,9 +50,8 @@ export class CalculatedFieldsService { return this.http.delete(`/api/calculatedField/${calculatedFieldId}`, defaultHttpOptionsFromConfig(config)); } - public getCalculatedFields({ entityType, id }: EntityId, pageLink: PageLink, config?: RequestConfig): Observable> { - return this.http.get>(`/api/${entityType}/${id}/calculatedFields${pageLink.toQuery()}`, - defaultHttpOptionsFromConfig(config)); + public getCalculatedFields({ entityType, id }: EntityId, pageLink: PageLink, type?: CalculatedFieldType, config?: RequestConfig): Observable> { + return this.http.get>(`/api/${entityType}/${id}/calculatedFields${pageLink.toQuery()}`, createDefaultHttpOptions(type ? {type} : null, config)); } public testScript(inputParams: CalculatedFieldTestScriptInputParams, config?: RequestConfig): Observable { diff --git a/ui-ngx/src/app/core/http/customer.service.ts b/ui-ngx/src/app/core/http/customer.service.ts index faf78f6f75..ec8955ca4b 100644 --- a/ui-ngx/src/app/core/http/customer.service.ts +++ b/ui-ngx/src/app/core/http/customer.service.ts @@ -15,12 +15,13 @@ /// import { Injectable } from '@angular/core'; -import { defaultHttpOptionsFromConfig, RequestConfig } from './http-utils'; +import { createDefaultHttpOptions, defaultHttpOptionsFromConfig, RequestConfig } from './http-utils'; import { Observable } from 'rxjs'; import { HttpClient } from '@angular/common/http'; import { PageLink } from '@shared/models/page/page-link'; import { PageData } from '@shared/models/page/page-data'; import { Customer } from '@shared/models/customer.model'; +import { SaveEntityParams } from '@shared/models/entity.models'; @Injectable({ providedIn: 'root' @@ -40,8 +41,10 @@ export class CustomerService { return this.http.get(`/api/customer/${customerId}`, defaultHttpOptionsFromConfig(config)); } - public saveCustomer(customer: Customer, config?: RequestConfig): Observable { - return this.http.post('/api/customer', customer, defaultHttpOptionsFromConfig(config)); + public saveCustomer(customer: Customer, config?: RequestConfig): Observable; + public saveCustomer(customer: Customer, saveParams: SaveEntityParams, config?: RequestConfig): Observable; + public saveCustomer(customer: Customer, saveParamsOrConfig?: SaveEntityParams | RequestConfig, config?: RequestConfig): Observable { + return this.http.post('/api/customer', customer, createDefaultHttpOptions(saveParamsOrConfig, config)); } public deleteCustomer(customerId: string, config?: RequestConfig) { diff --git a/ui-ngx/src/app/core/http/device.service.ts b/ui-ngx/src/app/core/http/device.service.ts index 1e3a304774..c93c658d8c 100644 --- a/ui-ngx/src/app/core/http/device.service.ts +++ b/ui-ngx/src/app/core/http/device.service.ts @@ -15,8 +15,9 @@ /// import { Injectable } from '@angular/core'; -import { defaultHttpOptionsFromConfig, RequestConfig } from './http-utils'; -import { Observable, ReplaySubject } from 'rxjs'; +import { createDefaultHttpOptions, defaultHttpOptionsFromConfig, RequestConfig } from './http-utils'; +import { catchError, Observable, of, ReplaySubject, throwError, timeout } from 'rxjs'; +import { map, switchMap } from "rxjs/operators"; import { HttpClient } from '@angular/common/http'; import { PageLink } from '@shared/models/page/page-link'; import { PageData } from '@shared/models/page/page-data'; @@ -28,13 +29,15 @@ import { DeviceInfo, DeviceInfoQuery, DeviceSearchQuery, - PublishTelemetryCommand + PublishTelemetryCommand, + SaveDeviceParams } from '@shared/models/device.models'; import { EntitySubtype } from '@shared/models/entity-type.models'; import { AuthService } from '@core/auth/auth.service'; import { BulkImportRequest, BulkImportResult } from '@shared/import-export/import-export.models'; import { PersistentRpc, RpcStatus } from '@shared/models/rpc.models'; import { ResourcesService } from '@core/services/resources.service'; +import { SaveEntityParams } from '@shared/models/entity.models'; @Injectable({ providedIn: 'root' @@ -87,15 +90,19 @@ export class DeviceService { return this.http.get(`/api/device/info/${deviceId}`, defaultHttpOptionsFromConfig(config)); } - public saveDevice(device: Device, config?: RequestConfig): Observable { - return this.http.post('/api/device', device, defaultHttpOptionsFromConfig(config)); + public saveDevice(device: Device, config?: RequestConfig): Observable; + public saveDevice(device: Device, saveParams?: SaveDeviceParams, config?: RequestConfig): Observable; + public saveDevice(device: Device, saveParamsOrConfig?: SaveDeviceParams | RequestConfig, config?: RequestConfig): Observable { + return this.http.post('/api/device', device, createDefaultHttpOptions(saveParamsOrConfig, config)); } - public saveDeviceWithCredentials(device: Device, credentials: DeviceCredentials, config?: RequestConfig): Observable { + public saveDeviceWithCredentials(device: Device, credentials: DeviceCredentials, config?: RequestConfig): Observable; + public saveDeviceWithCredentials(device: Device, credentials: DeviceCredentials, saveParams: SaveEntityParams, config?: RequestConfig): Observable; + public saveDeviceWithCredentials(device: Device, credentials: DeviceCredentials, saveParamsOrConfig?: SaveEntityParams | RequestConfig, config?: RequestConfig): Observable { return this.http.post('/api/device-with-credentials', { device, credentials - }, defaultHttpOptionsFromConfig(config)); + }, createDefaultHttpOptions(saveParamsOrConfig, config)); } public deleteDevice(deviceId: string, config?: RequestConfig) { @@ -219,4 +226,71 @@ export class DeviceService { public downloadGatewayDockerComposeFile(deviceId: string): Observable { return this.resourcesService.downloadResource(`/api/device-connectivity/gateway-launch/${deviceId}/docker-compose/download`); } + + public rebootDevice(deviceId: string, isBootstrapServer: boolean, config?: RequestConfig): Observable<{ + result: string, + msg: string + }> { + const rebootName = isBootstrapServer ? 'Bootstrap-Request Trigger' : 'Registration Update Trigger'; + return this.sendTwoWayRpcCommand(deviceId, {method: 'DiscoverAll'}, config).pipe( + timeout(10000), + switchMap((response: any) => { + if (response.result && response.result.toUpperCase() === 'CONTENT') { + const resourceId = isBootstrapServer ? 9 : 8; + const resourcePath = `/1/0/${resourceId}`; + return this.rebootTrigger(deviceId, resourcePath, config).pipe( + map((responseReboot: any) => { + if (responseReboot.result === 'CHANGED') { + return { + result: 'SUCCESS', + msg: `"${rebootName}" - Started Successfully.` + }; + } else { + return { + result: 'ERROR', + msg: `"${rebootName}" failed:
    ${JSON.stringify(responseReboot, null, 2)}
    ` + } + } + }), + catchError(err => + of({ + result: 'ERROR', + msg: `"${rebootName}" failed.
    Error: ${err.message || err}` + }) + ) + ); + } else { + return of({ + result: 'ERROR', + msg: `"${rebootName}" failed.
    Bad registration device with id = ${deviceId}.
    "DiscoverAll" - RPC result is not "CONTENT"` + }); + } + }), + catchError(err => + of({ + result: 'ERROR', + msg: `"${rebootName}" failed.
    Bad registration device with id = ${deviceId}.
    Error: ${err.message || err}` + }) + ) + ); + } + + private rebootTrigger(deviceId: string, resourcePath: string, config?: RequestConfig): Observable<{ result: string, msg?: string }> { + return this.sendTwoWayRpcCommand(deviceId, {method: 'Execute', params: {id: resourcePath}}, config).pipe( + timeout(10000), + map(res => { + if (res?.result?.toUpperCase() === 'CHANGED') { + return {result: 'CHANGED'}; + } else { + return { + result: `${res?.result}`, + msg: `${res?.error}` + } + } + }), + catchError(err => { + return throwError(() => err); + }) + ); + } } diff --git a/ui-ngx/src/app/core/http/entity-view.service.ts b/ui-ngx/src/app/core/http/entity-view.service.ts index ffd17d9a49..7285c5420f 100644 --- a/ui-ngx/src/app/core/http/entity-view.service.ts +++ b/ui-ngx/src/app/core/http/entity-view.service.ts @@ -15,13 +15,14 @@ /// import { Injectable } from '@angular/core'; -import { defaultHttpOptionsFromConfig, RequestConfig } from './http-utils'; +import { createDefaultHttpOptions, defaultHttpOptionsFromConfig, RequestConfig } from './http-utils'; import { Observable } from 'rxjs'; import { HttpClient } from '@angular/common/http'; import { PageLink } from '@shared/models/page/page-link'; import { PageData } from '@shared/models/page/page-data'; import { EntitySubtype } from '@app/shared/models/entity-type.models'; import { EntityView, EntityViewInfo, EntityViewSearchQuery } from '@app/shared/models/entity-view.models'; +import { SaveEntityParams } from '@shared/models/entity.models'; @Injectable({ providedIn: 'root' @@ -51,8 +52,10 @@ export class EntityViewService { return this.http.get(`/api/entityView/info/${entityViewId}`, defaultHttpOptionsFromConfig(config)); } - public saveEntityView(entityView: EntityView, config?: RequestConfig): Observable { - return this.http.post('/api/entityView', entityView, defaultHttpOptionsFromConfig(config)); + public saveEntityView(entityView: EntityView, config?: RequestConfig): Observable; + public saveEntityView(entityView: EntityView, saveParams: SaveEntityParams, config?: RequestConfig): Observable; + public saveEntityView(entityView: EntityView, saveParamsOrConfig?: SaveEntityParams | RequestConfig, config?: RequestConfig): Observable { + return this.http.post('/api/entityView', entityView, createDefaultHttpOptions(saveParamsOrConfig, config)); } public deleteEntityView(entityViewId: string, config?: RequestConfig) { diff --git a/ui-ngx/src/app/core/http/entity.service.ts b/ui-ngx/src/app/core/http/entity.service.ts index 572d6cc473..5cd4167afa 100644 --- a/ui-ngx/src/app/core/http/entity.service.ts +++ b/ui-ngx/src/app/core/http/entity.service.ts @@ -100,6 +100,7 @@ import { OAuth2Service } from '@core/http/oauth2.service'; import { MobileAppService } from '@core/http/mobile-app.service'; import { PlatformType } from '@shared/models/oauth2.models'; import { AiModelService } from '@core/http/ai-model.service'; +import { ResourceType } from "@shared/models/resource.models"; @Injectable({ providedIn: 'root' @@ -297,6 +298,9 @@ export class EntityService { (id) => this.ruleChainService.getRuleChain(id, config), entityIds); break; + case EntityType.TB_RESOURCE: + observable = this.resourceService.getResourcesByIds(entityIds, config); + break; } return observable; } @@ -472,7 +476,7 @@ export class EntityService { break; case EntityType.TB_RESOURCE: pageLink.sortOrder.property = 'title'; - entitiesObservable = this.resourceService.getTenantResources(pageLink, config); + entitiesObservable = this.resourceService.getResources(pageLink, subType as ResourceType, null, config); break; case EntityType.QUEUE_STATS: pageLink.sortOrder.property = 'createdTime'; @@ -813,6 +817,7 @@ export class EntityService { switch (entityType) { case EntityType.USER: entityFieldKeys.push(entityFields.name.keyName); + entityFieldKeys.push(entityFields.displayName.keyName); entityFieldKeys.push(entityFields.email.keyName); entityFieldKeys.push(entityFields.firstName.keyName); entityFieldKeys.push(entityFields.lastName.keyName); @@ -842,6 +847,7 @@ export class EntityService { case EntityType.EDGE: case EntityType.ASSET: entityFieldKeys.push(entityFields.name.keyName); + entityFieldKeys.push(entityFields.displayName.keyName); entityFieldKeys.push(entityFields.type.keyName); entityFieldKeys.push(entityFields.label.keyName); entityFieldKeys.push(entityFields.ownerName.keyName); diff --git a/ui-ngx/src/app/core/http/git-hub.service.ts b/ui-ngx/src/app/core/http/git-hub.service.ts new file mode 100644 index 0000000000..b6a927dab3 --- /dev/null +++ b/ui-ngx/src/app/core/http/git-hub.service.ts @@ -0,0 +1,38 @@ +/// +/// Copyright © 2016-2025 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Injectable } from '@angular/core'; +import { defaultHttpOptionsFromConfig, RequestConfig } from './http-utils'; +import { Observable, of } from 'rxjs'; +import { HttpClient } from '@angular/common/http'; +import { catchError, map } from 'rxjs/operators'; + +@Injectable({ + providedIn: 'root' +}) +export class GitHubService { + + constructor( + private http: HttpClient + ) { } + + public getGitHubStar(config?: RequestConfig): Observable { + return this.http.get('https://api.github.com/repos/thingsboard/thingsboard', defaultHttpOptionsFromConfig(config)).pipe( + catchError(() => of({})), + map((res: any) => res?.stargazers_count ?? 0) + ) + } +} diff --git a/ui-ngx/src/app/core/http/http-utils.ts b/ui-ngx/src/app/core/http/http-utils.ts index 787abcfbf3..3d28f6ddf4 100644 --- a/ui-ngx/src/app/core/http/http-utils.ts +++ b/ui-ngx/src/app/core/http/http-utils.ts @@ -18,32 +18,81 @@ import { InterceptorHttpParams } from '../interceptors/interceptor-http-params'; import { HttpHeaders } from '@angular/common/http'; import { InterceptorConfig } from '../interceptors/interceptor-config'; +export type QueryParams = { [param:string]: any }; + export interface RequestConfig { ignoreLoading?: boolean; ignoreErrors?: boolean; resendRequest?: boolean; + queryParams?: QueryParams; +} + +export function hasRequestConfig(config?: any): boolean { + if (!config) { + return false; + } + return config.hasOwnProperty('ignoreLoading') || config.hasOwnProperty('ignoreErrors') || config.hasOwnProperty('resendRequest') || config.hasOwnProperty('queryParams'); +} + +export function createDefaultHttpOptions(queryParamsOrConfig?: QueryParams | RequestConfig, config?: RequestConfig) { + if (hasRequestConfig(queryParamsOrConfig)) { + return defaultHttpOptionsFromConfig(queryParamsOrConfig as RequestConfig); + } + return defaultHttpOptionsFromParams(queryParamsOrConfig as QueryParams, config); +} + +export function defaultHttpOptionsFromParams(queryParams?: QueryParams, config?: RequestConfig) { + const finalConfig = { + ...config, + ...(queryParams && { queryParams }), + }; + return defaultHttpOptionsFromConfig(finalConfig); } export function defaultHttpOptionsFromConfig(config?: RequestConfig) { if (!config) { config = {}; } - return defaultHttpOptions(config.ignoreLoading, config.ignoreErrors, config.resendRequest); + return defaultHttpOptions(config.ignoreLoading, config.ignoreErrors, config.resendRequest, config.queryParams); } export function defaultHttpOptions(ignoreLoading: boolean = false, ignoreErrors: boolean = false, - resendRequest: boolean = false) { + resendRequest: boolean = false, + queryParams?: QueryParams) { + const cleanedParams = cleanQueryParams(queryParams); + return { headers: new HttpHeaders({'Content-Type': 'application/json'}), - params: new InterceptorHttpParams(new InterceptorConfig(ignoreLoading, ignoreErrors, resendRequest)) + params: new InterceptorHttpParams(new InterceptorConfig(ignoreLoading, ignoreErrors, resendRequest), cleanedParams) }; } export function defaultHttpUploadOptions(ignoreLoading: boolean = false, ignoreErrors: boolean = false, - resendRequest: boolean = false) { + resendRequest: boolean = false, + queryParams?: QueryParams) { + const cleanedParams = cleanQueryParams(queryParams); + return { - params: new InterceptorHttpParams(new InterceptorConfig(ignoreLoading, ignoreErrors, resendRequest)) + params: new InterceptorHttpParams(new InterceptorConfig(ignoreLoading, ignoreErrors, resendRequest), cleanedParams) }; } + +function cleanQueryParams(params?: QueryParams): QueryParams | undefined { + if (!params) { + return undefined; + } + + const entries = Object.entries(params); + + const cleanedEntries = entries.filter( + ([_, value]) => value !== null && value !== undefined + ); + + if (!cleanedEntries.length) { + return undefined; + } + + return Object.fromEntries(cleanedEntries); +} diff --git a/ui-ngx/src/app/core/http/public-api.ts b/ui-ngx/src/app/core/http/public-api.ts index 63cc393ce6..43354f6b60 100644 --- a/ui-ngx/src/app/core/http/public-api.ts +++ b/ui-ngx/src/app/core/http/public-api.ts @@ -21,11 +21,13 @@ export * from './asset.service'; export * from './asset-profile.service'; export * from './attribute.service'; export * from './audit-log.service'; +export * from './calculated-fields.service'; export * from './component-descriptor.service'; export * from './customer.service'; export * from './dashboard.service'; export * from './device.service'; export * from './device-profile.service'; +export * from './domain.service'; export * from './entities-version-control.service'; export * from './entity.service'; export * from './edge.service'; @@ -34,6 +36,8 @@ export * from './entity-view.service'; export * from './event.service'; export * from './http-utils'; export * from './image.service'; +export * from './mobile-app.service'; +export * from './mobile-application.service'; export * from './notification.service'; export * from './oauth2.service'; export * from './ota-package.service'; @@ -42,6 +46,7 @@ export * from './resource.service'; export * from './rule-chain.service'; export * from './tenant.service'; export * from './tenant-profile.service'; +export * from './two-factor-authentication.service'; export * from './ui-settings.service'; export * from './user.service'; export * from './user-settings.service'; diff --git a/ui-ngx/src/app/core/http/queue.service.ts b/ui-ngx/src/app/core/http/queue.service.ts index d9bc19ef62..651d26c9dc 100644 --- a/ui-ngx/src/app/core/http/queue.service.ts +++ b/ui-ngx/src/app/core/http/queue.service.ts @@ -75,7 +75,7 @@ export class QueueService { } public getQueueStatisticsByIds(queueStatIds: Array, config?: RequestConfig): Observable> { - return this.http.get>(`/api/queueStats?QueueStatsIds=${queueStatIds.join(',')}`, + return this.http.get>(`/api/queueStats?queueStatsIds=${queueStatIds.join(',')}`, defaultHttpOptionsFromConfig(config)).pipe( map(queueStats => queueStats.map(queueStat => this.parseQueueStatName(queueStat)) ) diff --git a/ui-ngx/src/app/core/http/resource.service.ts b/ui-ngx/src/app/core/http/resource.service.ts index 615b721b97..168c63b3b1 100644 --- a/ui-ngx/src/app/core/http/resource.service.ts +++ b/ui-ngx/src/app/core/http/resource.service.ts @@ -48,7 +48,7 @@ export class ResourceService { } public getTenantResources(pageLink: PageLink, config?: RequestConfig): Observable> { - return this.http.get>(`/api/resource/tenant${pageLink.toQuery()}`, defaultHttpOptionsFromConfig(config)); + return this.http.get>(`/api/resource/tenant${pageLink.toQuery()}`, defaultHttpOptionsFromConfig(config)) } public getResource(resourceId: string, config?: RequestConfig): Observable { @@ -94,4 +94,9 @@ export class ResourceService { return this.http.delete(`/api/resource/${resourceId}?force=${force}`, defaultHttpOptionsFromConfig(config)); } + public getResourcesByIds(ids: string[], config?: RequestConfig): Observable> { + return this.http.get>(`/api/resource?resourceIds=${ids.join(',')}`, + defaultHttpOptionsFromConfig(config)); + } + } diff --git a/ui-ngx/src/app/core/interceptors/global-http-interceptor.ts b/ui-ngx/src/app/core/interceptors/global-http-interceptor.ts index 12d3c78817..dccda985b4 100644 --- a/ui-ngx/src/app/core/interceptors/global-http-interceptor.ts +++ b/ui-ngx/src/app/core/interceptors/global-http-interceptor.ts @@ -30,6 +30,7 @@ import { DialogService } from '@core/services/dialog.service'; import { TranslateService } from '@ngx-translate/core'; import { parseHttpErrorMessage } from '@core/utils'; import { getInterceptorConfig } from './interceptor.util'; +import { DomSanitizer } from '@angular/platform-browser'; const tmpHeaders = {}; @@ -46,6 +47,7 @@ export class GlobalHttpInterceptor implements HttpInterceptor { private dialogService: DialogService, private translate: TranslateService, private authService: AuthService, + private sanitizer: DomSanitizer ) {} intercept(req: HttpRequest, next: HttpHandler): Observable> { @@ -129,7 +131,7 @@ export class GlobalHttpInterceptor implements HttpInterceptor { } if (unhandled && !ignoreErrors) { - const errorMessageWithTimeout = parseHttpErrorMessage(errorResponse, this.translate, req.responseType); + const errorMessageWithTimeout = parseHttpErrorMessage(errorResponse, this.translate, req.responseType, this.sanitizer); this.showError(errorMessageWithTimeout.message, errorMessageWithTimeout.timeout); } return throwError(() => errorResponse); diff --git a/ui-ngx/src/app/core/interceptors/interceptor-http-params.ts b/ui-ngx/src/app/core/interceptors/interceptor-http-params.ts index 1d9cbdddaa..ab75102464 100644 --- a/ui-ngx/src/app/core/interceptors/interceptor-http-params.ts +++ b/ui-ngx/src/app/core/interceptors/interceptor-http-params.ts @@ -20,7 +20,7 @@ import { InterceptorConfig } from './interceptor-config'; export class InterceptorHttpParams extends HttpParams { constructor( public interceptorConfig: InterceptorConfig, - params?: { [param: string]: string | string[] } + params?: { [param: string]: string | number | boolean | ReadonlyArray; } ) { super({ fromObject: params }); } diff --git a/ui-ngx/src/app/core/services/dashboard-utils.service.ts b/ui-ngx/src/app/core/services/dashboard-utils.service.ts index 1511840c57..9c7c5d28d6 100644 --- a/ui-ngx/src/app/core/services/dashboard-utils.service.ts +++ b/ui-ngx/src/app/core/services/dashboard-utils.service.ts @@ -38,6 +38,7 @@ import { import { deepClone, isDefined, isDefinedAndNotNull, isNotEmptyStr, isString, isUndefined } from '@core/utils'; import { Datasource, + datasourcesHasAggregation, datasourcesHasOnlyComparisonAggregation, DatasourceType, defaultLegendConfig, @@ -49,7 +50,8 @@ import { WidgetConfigMode, WidgetSize, widgetType, - WidgetTypeDescriptor + WidgetTypeDescriptor, + widgetTypeHasTimewindow } from '@app/shared/models/widget.models'; import { EntityType } from '@shared/models/entity-type.models'; import { AliasFilterType, EntityAlias, EntityAliasFilter } from '@app/shared/models/alias.models'; @@ -234,6 +236,7 @@ export class DashboardUtilsService { public validateAndUpdateWidget(widget: Widget): Widget { widget.config = this.validateAndUpdateWidgetConfig(widget.config, widget.type); widget = this.validateAndUpdateWidgetTypeFqn(widget); + this.removeTimewindowConfigIfUnused(widget); if (isDefined((widget as any).title)) { delete (widget as any).title; } @@ -294,8 +297,11 @@ export class DashboardUtilsService { } widgetConfig.datasources = this.validateAndUpdateDatasources(widgetConfig.datasources); if (type === widgetType.latest) { - const onlyHistoryTimewindow = datasourcesHasOnlyComparisonAggregation(widgetConfig.datasources); - widgetConfig.timewindow = initModelFromDefaultTimewindow(widgetConfig.timewindow, true, onlyHistoryTimewindow, this.timeService); + if (datasourcesHasAggregation(widgetConfig.datasources)) { + const onlyHistoryTimewindow = datasourcesHasOnlyComparisonAggregation(widgetConfig.datasources); + widgetConfig.timewindow = initModelFromDefaultTimewindow(widgetConfig.timewindow, true, + onlyHistoryTimewindow, this.timeService, false); + } } else if (type === widgetType.rpc) { if (widgetConfig.targetDeviceAliasIds && widgetConfig.targetDeviceAliasIds.length) { widgetConfig.targetDevice = { @@ -348,6 +354,33 @@ export class DashboardUtilsService { return widgetConfig; } + private removeTimewindowConfigIfUnused(widget: Widget) { + const widgetHasTimewindow = this.widgetHasTimewindow(widget); + if (!widgetHasTimewindow || widget.config.useDashboardTimewindow) { + delete widget.config.displayTimewindow; + delete widget.config.timewindow; + delete widget.config.timewindowStyle; + + if (!widgetHasTimewindow) { + delete widget.config.useDashboardTimewindow; + } + } + } + + private widgetHasTimewindow(widget: Widget): boolean { + const widgetDefinition = findWidgetModelDefinition(widget); + if (widgetDefinition) { + return widgetDefinition.hasTimewindow(widget); + } + return widgetTypeHasTimewindow(widget.type) + || (widget.type === widgetType.latest && datasourcesHasAggregation(widget.config.datasources)); + } + + public prepareWidgetForSaving(widget: Widget): Widget { + this.removeTimewindowConfigIfUnused(widget); + return widget; + } + public prepareWidgetForScadaLayout(widget: Widget, isScada: boolean): Widget { const config = widget.config; config.showTitle = false; diff --git a/ui-ngx/src/app/core/services/mobile.service.ts b/ui-ngx/src/app/core/services/mobile.service.ts index 6fcb7c3602..aceb564ec2 100644 --- a/ui-ngx/src/app/core/services/mobile.service.ts +++ b/ui-ngx/src/app/core/services/mobile.service.ts @@ -106,9 +106,9 @@ export class MobileService { } } - public handleMobileNavigation(path?: string, params?: Params) { + public handleMobileNavigation(path?: string, params?: Params, queryParams?: Params) { if (this.mobileApp) { - this.mobileChannel.callHandler(navigationHandler, path, params); + this.mobileChannel.callHandler(navigationHandler, path, params, queryParams); } } diff --git a/ui-ngx/src/app/core/utils.ts b/ui-ngx/src/app/core/utils.ts index 353727e006..c68f26d56f 100644 --- a/ui-ngx/src/app/core/utils.ts +++ b/ui-ngx/src/app/core/utils.ts @@ -27,12 +27,17 @@ import { serverErrorCodesTranslations } from '@shared/models/constants'; import { SubscriptionEntityInfo } from '@core/api/widget-api.models'; import { CompiledTbFunction, - compileTbFunction, GenericFunction, + compileTbFunction, + GenericFunction, isNotEmptyTbFunction, TbFunction } from '@shared/models/js-function.models'; +import { DomSanitizer } from '@angular/platform-browser'; +import { SecurityContext } from '@angular/core'; +import { AbstractControl, ValidationErrors, Validators } from '@angular/forms'; const varsRegex = /\${([^}]*)}/g; +const emailRegex = /^[A-Z0-9_!#$%&'*+/=?`{|}~^.-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i; export function onParentScrollOrWindowResize(el: Node): Observable { const scrollSubject = new Subject(); @@ -196,6 +201,23 @@ export function deleteNullProperties(obj: any) { }); } +export function deleteFalseProperties(obj: Record): void { + if (isUndefinedOrNull(obj)) { + return; + } + Object.keys(obj).forEach((propName) => { + if (obj[propName] === false || isUndefinedOrNull(obj[propName])) { + delete obj[propName]; + } else if (isObject(obj[propName])) { + deleteFalseProperties(obj[propName]); + } else if (Array.isArray(obj[propName])) { + (obj[propName] as any[]).forEach((elem) => { + deleteFalseProperties(elem); + }); + } + }); +} + export function objToBase64(obj: any): string { const json = JSON.stringify(obj); return btoa(encodeURIComponent(json).replace(/%([0-9A-F]{2})/g, @@ -773,6 +795,33 @@ export function deepTrim(obj: T): T { }, (Array.isArray(obj) ? [] : {}) as T); } +export function deepClean | any[]>(obj: T, { + cleanKeys = [] +} = {}): T { + return _.transform(obj, (result, value, key) => { + if (cleanKeys.includes(key)) { + return; + } + if (Array.isArray(value) || isLiteralObject(value)) { + value = deepClean(value, {cleanKeys}); + } + if(isLiteralObject(value) && isEmpty(value)) { + return; + } + if (Array.isArray(value) && !value.length) { + return; + } + if (value === undefined || value === null || value === '' || Number.isNaN(value)) { + return; + } + + if (Array.isArray(result)) { + return result.push(value); + } + result[key] = value; + }); +} + export function generateSecret(length?: number): string { if (isUndefined(length) || length == null) { length = 1; @@ -809,7 +858,7 @@ export function getEntityDetailsPageURL(id: string, entityType: EntityType): str } export function parseHttpErrorMessage(errorResponse: HttpErrorResponse, - translate: TranslateService, responseType?: string): {message: string; timeout: number} { + translate: TranslateService, responseType?: string, sanitizer?:DomSanitizer): {message: string; timeout: number} { let error = null; let errorMessage: string; let timeout = 0; @@ -837,6 +886,9 @@ export function parseHttpErrorMessage(errorResponse: HttpErrorResponse, errorText += errorKey ? translate.instant(errorKey) : errorResponse.statusText; errorMessage = errorText; } + if(sanitizer) { + errorMessage = sanitizer.sanitize(SecurityContext.HTML,errorMessage); + } return {message: errorMessage, timeout}; } @@ -958,3 +1010,36 @@ export const unwrapModule = (module: any) : any => { return module; } }; + +export const trimDefaultValues = (input: Record, defaults: Record): Record => { + const result: Record = {}; + + for (const key in input) { + if (!(key in defaults)) { + result[key] = input[key]; + } else if (typeof defaults[key] === 'object' && defaults[key] !== null && typeof input[key] === 'object' && input[key] !== null) { + const subPatch = trimDefaultValues(input[key], defaults[key]); + if (Object.keys(subPatch).length > 0) { + result[key] = subPatch; + } + } else if (defaults[key] !== input[key]) { + result[key] = input[key]; + } + } + + for (const key in defaults) { + if (!(key in input)) { + delete result[key]; + } + } + + return result; +} + +export const validateEmail = (control: AbstractControl): ValidationErrors | null => { + if (isUndefinedOrNull(control.value) || (typeof control.value === 'string' && control.value.length === 0)) { + return null; + } + return emailRegex.test(control.value) ? null : {email: true}; +}; + diff --git a/ui-ngx/src/app/modules/home/components/ai-model/ai-model-dialog.component.html b/ui-ngx/src/app/modules/home/components/ai-model/ai-model-dialog.component.html index 860e615410..9d192a0d14 100644 --- a/ui-ngx/src/app/modules/home/components/ai-model/ai-model-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/ai-model/ai-model-dialog.component.html @@ -55,31 +55,34 @@ -
    +
    @if (providerFieldsList.includes('personalAccessToken')) { ai-models.personal-access-token - + - + {{ 'ai-models.personal-access-token-required' | translate }} } @if (providerFieldsList.includes('projectId')) { - + ai-models.project-id - + {{ 'ai-models.project-id-required' | translate }} } @if (providerFieldsList.includes('location')) { - + ai-models.location - + {{ 'ai-models.location-required' | translate }} @@ -98,44 +101,58 @@ } @if (providerFieldsList.includes('endpoint')) { - + ai-models.endpoint - + {{ 'ai-models.endpoint-required' | translate }} } @if (providerFieldsList.includes('serviceVersion')) { - + ai-models.service-version } + @if (providerFieldsList.includes('baseUrl')) { + + ai-models.baseurl + + + {{ 'ai-models.baseurl-required' | translate }} + + + } @if (providerFieldsList.includes('apiKey')) { ai-models.api-key - + - - {{ 'ai-models.api-key-required' | translate }} + + {{ ( provider === aiProvider.OPENAI ? 'ai-models.api-key-open-ai-required' : 'ai-models.api-key-required') | translate }} } @if (providerFieldsList.includes('region')) { - + ai-models.region - + {{ 'ai-models.region-required' | translate }} } @if (providerFieldsList.includes('accessKeyId')) { - + ai-models.access-key-id - + {{ 'ai-models.access-key-id-required' | translate }} @@ -143,13 +160,60 @@ @if (providerFieldsList.includes('secretAccessKey')) { ai-models.secret-access-key - + - + {{ 'ai-models.secret-access-key-required' | translate }} } + @if (provider === aiProvider.OLLAMA) { +
    +
    +
    + {{ 'ai-models.authentication' | translate }} +
    + + {{ 'ai-models.authentication-type.none' | translate }} + {{ 'ai-models.authentication-type.basic' | translate }} + {{ 'ai-models.authentication-type.token' | translate }} + +
    +
    + @if (aiModelForms.get('configuration.providerConfig.auth.type').value === AuthenticationType.BASIC) { + + ai-models.username + + + {{ 'ai-models.username-required' | translate }} + + + + ai-models.password + + + + {{ 'ai-models.password-required' | translate }} + + + } + @if (aiModelForms.get('configuration.providerConfig.auth.type').value === AuthenticationType.TOKEN) { + + ai-models.token + + + + {{ 'ai-models.token-required' | translate }} + + + } +
    +
    + }
    @@ -161,6 +225,7 @@ additionalClass="tb-suffix-show-on-hover" appearance="outline" panelWidth="" + showInlineError required [label]="(provider === aiProvider.AZURE_OPENAI ? 'ai-models.deployment-name': 'ai-models.model-id') | translate" [errorText]="(provider === aiProvider.AZURE_OPENAI ? 'ai-models.deployment-name-required': 'ai-models.model-id-required') | translate" @@ -255,15 +320,18 @@
    - - warning - + type="number" step="1" placeholder="{{ 'ai-models.set' | translate }}"> + + + } + @if (modelFieldsList.includes('contextLength')) { +
    +
    + {{ 'ai-models.context-length' | translate }} +
    + +
    } diff --git a/ui-ngx/src/app/modules/home/components/ai-model/ai-model-dialog.component.ts b/ui-ngx/src/app/modules/home/components/ai-model/ai-model-dialog.component.ts index db5d1d7e23..5511160193 100644 --- a/ui-ngx/src/app/modules/home/components/ai-model/ai-model-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/ai-model/ai-model-dialog.component.ts @@ -30,16 +30,20 @@ import { AiModelMap, AiProvider, AiProviderTranslations, + AuthenticationType, ModelType, ProviderFieldsAllList } from '@shared/models/ai-model.models'; import { AiModelService } from '@core/http/ai-model.service'; import { CheckConnectivityDialogComponent } from '@home/components/ai-model/check-connectivity-dialog.component'; import { map } from 'rxjs/operators'; +import { deepTrim } from '@core/utils'; +import { TranslateService } from '@ngx-translate/core'; export interface AIModelDialogData { AIModel?: AiModel; isAdd?: boolean; + name?: string; } @Component({ @@ -61,18 +65,26 @@ export class AIModelDialogComponent extends DialogComponent, protected router: Router, protected dialogRef: MatDialogRef, @Inject(MAT_DIALOG_DATA) public data: AIModelDialogData, private fb: FormBuilder, private aiModelService: AiModelService, + private translate: TranslateService, private dialog: MatDialog) { super(store, router, dialogRef); @@ -88,17 +100,24 @@ export class AIModelDialogComponent extends DialogComponent { @@ -117,7 +141,35 @@ export class AIModelDialogComponent extends DialogComponent { + if (this.provider === AiProvider.OPENAI) { + this.updateApiKeyValidatorForOpenAIProvider(url); + } + }); + + this.aiModelForms.get('configuration.providerConfig.auth.type').valueChanges.pipe( + takeUntilDestroyed() + ).subscribe((type: AuthenticationType) => { + this.getAuthenticationHint(type); + this.aiModelForms.get('configuration.providerConfig.auth.username').disable(); + this.aiModelForms.get('configuration.providerConfig.auth.password').disable(); + this.aiModelForms.get('configuration.providerConfig.auth.token').disable(); + if (type === AuthenticationType.BASIC) { + this.aiModelForms.get('configuration.providerConfig.auth.username').enable(); + this.aiModelForms.get('configuration.providerConfig.auth.password').enable(); + } + if (type === AuthenticationType.TOKEN) { + this.aiModelForms.get('configuration.providerConfig.auth.token').enable(); + } + }); this.updateValidation(this.provider); } @@ -129,6 +181,27 @@ export class AIModelDialogComponent extends DialogComponent { if (AiModelMap.get(provider).providerFieldsList.includes(key)) { @@ -136,7 +209,13 @@ export class AIModelDialogComponent extends DialogComponent this.dialogRef.close(aiModel)); + this.aiModelService.saveAiModel(deepTrim(aiModel)).subscribe(aiModel => this.dialogRef.close(aiModel)); } } diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-details-dialog.component.html b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-details-dialog.component.html new file mode 100644 index 0000000000..9e5036f6ff --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-details-dialog.component.html @@ -0,0 +1,49 @@ + + + +

    {{ 'alarm-rule.edit-alarm-rule-additional-info' | translate }}

    + +
    +
    +
    + + + + +
    +
    +
    + + @if (!data.readonly) { + + } +
    + diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-details-dialog.component.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-details-dialog.component.ts new file mode 100644 index 0000000000..8a55f52678 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-details-dialog.component.ts @@ -0,0 +1,60 @@ +/// +/// Copyright © 2016-2025 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, Inject } from '@angular/core'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { FormBuilder, FormControl } from '@angular/forms'; +import { Router } from '@angular/router'; +import { DialogComponent } from '@app/shared/components/dialog.component'; + +export interface AlarmRuleDetailsDialogData { + alarmDetails: string; + readonly: boolean; +} + +@Component({ + selector: 'tb-edit-alarm-details-dialog', + templateUrl: './alarm-rule-details-dialog.component.html', + providers: [], + styleUrls: ['./cf-alarm-rules-dialog.component.scss'], +}) +export class AlarmRuleDetailsDialogComponent extends DialogComponent { + + alarmDetailsControl: FormControl; + + constructor(protected store: Store, + protected router: Router, + @Inject(MAT_DIALOG_DATA) public data: AlarmRuleDetailsDialogData, + public dialogRef: MatDialogRef, + private fb: FormBuilder) { + super(store, router, dialogRef); + + this.alarmDetailsControl = this.fb.control(this.data.alarmDetails); + if (this.data.readonly) { + this.alarmDetailsControl.disable(); + } + } + + cancel(): void { + this.dialogRef.close(null); + } + + save(): void { + this.dialogRef.close(this.alarmDetailsControl.value); + } +} diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.html b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.html new file mode 100644 index 0000000000..e0f1a052d5 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.html @@ -0,0 +1,163 @@ + +
    + +

    {{ 'alarm-rule.alarm-rule' | translate}}

    + + +
    +
    +
    +
    +
    {{ 'common.general' | translate }}
    +
    + + {{ 'alarm-rule.alarm-type' | translate }} + + @if (fieldFormGroup.get('name').errors && fieldFormGroup.get('name').touched) { + + @if (fieldFormGroup.get('name').hasError('required')) { + {{ 'alarm-rule.alarm-type-required' | translate }} + } @else if (fieldFormGroup.get('name').hasError('pattern')) { + {{ 'alarm-rule.alarm-type-pattern' | translate }} + } @else if (fieldFormGroup.get('name').hasError('maxlength')) { + {{ 'alarm-rule.alarm-type-max-length' | translate }} + } + + } + + +
    +
    + +
    +
    {{ 'calculated-fields.arguments' | translate }}
    + +
    +
    +
    {{ 'alarm-rule.create-alarm-rules' | translate }}
    +
    + + +
    +
    +
    +
    {{ 'alarm-rule.clear-alarm-rule' | translate }}
    +
    +
    + + +
    + +
    +
    + alarm-rule.no-clear-alarm-rule +
    +
    + +
    +
    +
    + + + {{ 'alarm-rule.advanced-settings' | translate }} + + +
    + + {{ 'alarm-rule.propagate-alarm' | translate }} + +
    + @if (configFormGroup.get('propagate').value) { + + alarm-rule.alarm-rule-relation-types-list + + + {{key}} + close + + + + + + } +
    + + {{ 'alarm-rule.propagate-alarm-to-owner' | translate }} + +
    +
    + + {{ 'alarm-rule.propagate-alarm-to-tenant' | translate }} + +
    +
    +
    +
    +
    +
    +
    +
    + + +
    +
    diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.scss b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.scss new file mode 100644 index 0000000000..04e46fd3d0 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.scss @@ -0,0 +1,34 @@ +/** + * Copyright © 2016-2025 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. + */ + +.calculated-field-dialog-container { + width: 869px; + max-width: 100%; +} + +.clear-alarm-rule { + border: 1px solid rgba(0, 0, 0, 0.12); + border-left-width: 4px; + border-left-color: green; + border-radius: 4px; + padding: 8px; + min-width: 0; +} + +.button-icon { + color: rgba(0, 0, 0, 0.38); + min-width: 40px; +} diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.ts new file mode 100644 index 0000000000..0ea3bd036e --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.ts @@ -0,0 +1,179 @@ +/// +/// Copyright © 2016-2025 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, DestroyRef, Inject, ViewEncapsulation } from '@angular/core'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { Router } from '@angular/router'; +import { DialogComponent } from '@shared/components/dialog.component'; +import { CalculatedField, CalculatedFieldArgument, CalculatedFieldType } from '@shared/models/calculated-field.models'; +import { oneSpaceInsideRegex } from '@shared/models/regex.constants'; +import { EntityType } from '@shared/models/entity-type.models'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { ScriptLanguage } from '@shared/models/rule-node.models'; +import { CalculatedFieldsService } from '@core/http/calculated-fields.service'; +import { EntityId } from '@shared/models/id/entity-id'; +import { AdditionalDebugActionConfig } from '@home/components/entity/debug/entity-debug-settings.model'; +import { COMMA, ENTER, SEMICOLON } from "@angular/cdk/keycodes"; +import { MatChipInputEvent } from "@angular/material/chips"; +import { AlarmRule, AlarmRuleConditionType, AlarmRuleExpressionType } from "@shared/models/alarm-rule.models"; +import { deepTrim } from "@core/utils"; + +export interface AlarmRuleDialogData { + value?: CalculatedField; + buttonTitle: string; + entityId: EntityId; + tenantId: string; + entityName?: string; + ownerId: EntityId; + additionalDebugActionConfig: AdditionalDebugActionConfig<(calculatedField: CalculatedField) => void>; + isDirty?: boolean; +} + +@Component({ + selector: 'tb-alarm-rule-dialog', + templateUrl: './alarm-rule-dialog.component.html', + styleUrls: ['./alarm-rule-dialog.component.scss'], + encapsulation: ViewEncapsulation.None +}) +export class AlarmRuleDialogComponent extends DialogComponent { + + fieldFormGroup = this.fb.group({ + name: ['', [Validators.required, Validators.pattern(oneSpaceInsideRegex), Validators.maxLength(255)]], + type: [CalculatedFieldType.ALARM], + debugSettings: [], + configuration: this.fb.group({ + arguments: this.fb.control({}), + propagate: [false], + propagateToOwner: [false], + propagateToTenant: [false], + propagateRelationTypes: [null], + createRules: [null], + clearRule: [null], + }), + }); + + additionalDebugActionConfig = this.data.value?.id ? { + ...this.data.additionalDebugActionConfig, + action: () => this.data.additionalDebugActionConfig.action({ id: this.data.value.id, ...this.fromGroupValue }), + } : null; + + readonly EntityType = EntityType; + readonly CalculatedFieldType = CalculatedFieldType; + readonly ScriptLanguage = ScriptLanguage; + + separatorKeysCodes = [ENTER, COMMA, SEMICOLON]; + + constructor(protected store: Store, + protected router: Router, + @Inject(MAT_DIALOG_DATA) public data: AlarmRuleDialogData, + protected dialogRef: MatDialogRef, + private calculatedFieldsService: CalculatedFieldsService, + private destroyRef: DestroyRef, + private fb: FormBuilder) { + super(store, router, dialogRef); + this.observeIsLoading(); + this.applyDialogData(); + } + + get configFormGroup(): FormGroup { + return this.fieldFormGroup.get('configuration') as FormGroup; + } + + get arguments(): Record { + return this.fieldFormGroup.get('configuration.arguments').value; + } + + public removeClearAlarmRule() { + this.configFormGroup.patchValue({clearRule: null}); + this.fieldFormGroup.markAsDirty(); + } + + public addClearAlarmRule() { + const clearAlarmRule: AlarmRule = { + condition: { + type: AlarmRuleConditionType.SIMPLE, + expression: { + type: AlarmRuleExpressionType.SIMPLE + } + } + }; + this.configFormGroup.patchValue({clearRule: clearAlarmRule}); + } + + removeRelationType(key: string): void { + const keys: string[] = this.configFormGroup.get('propagateRelationTypes').value; + const index = keys.indexOf(key); + if (index >= 0) { + keys.splice(index, 1); + this.configFormGroup.get('propagateRelationTypes').setValue(keys); + } + } + + addRelationType(event: MatChipInputEvent): void { + const input = event.chipInput.inputElement; + let value = (event.value ?? '').trim(); + if (value) { + let keys: string[] = this.configFormGroup.get('propagateRelationTypes').value ?? []; + if (keys.indexOf(value) === -1) { + keys.push(value); + this.configFormGroup.get('propagateRelationTypes').setValue(keys); + } + } + if (input) { + input.value = ''; + } + } + + get fromGroupValue(): CalculatedField { + return deepTrim(this.fieldFormGroup.value as CalculatedField); + } + + cancel(): void { + this.dialogRef.close(null); + } + + add(): void { + if (this.fieldFormGroup.valid) { + const alarmRule = { entityId: this.data.entityId, ...(this.data.value ?? {}), ...this.fromGroupValue}; + alarmRule.configuration.type = CalculatedFieldType.ALARM; + + this.calculatedFieldsService.saveCalculatedField(alarmRule) + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(calculatedField => this.dialogRef.close(calculatedField)); + } + } + + private applyDialogData(): void { + const { configuration = {}, type = CalculatedFieldType.ALARM, debugSettings = { failuresEnabled: true, allEnabled: true }, ...value } = this.data.value ?? {}; + this.fieldFormGroup.patchValue({ configuration, type, debugSettings, ...value }, {emitEvent: false}); + } + + private observeIsLoading(): void { + this.isLoading$.pipe(takeUntilDestroyed()).subscribe(loading => { + if (loading) { + this.fieldFormGroup.disable({emitEvent: false}); + } else { + this.fieldFormGroup.enable({emitEvent: false}); + if (this.data.isDirty) { + this.fieldFormGroup.markAsDirty(); + } + } + }); + } +} diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule.module.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule.module.ts new file mode 100644 index 0000000000..7acfca7b0d --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule.module.ts @@ -0,0 +1,80 @@ +/// +/// Copyright © 2016-2025 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { SharedModule } from '@shared/shared.module'; +import { AlarmRuleDialogComponent } from "@home/components/alarm-rules/alarm-rule-dialog.component"; +import { CreateCfAlarmRulesComponent } from "@home/components/alarm-rules/create-cf-alarm-rules.component"; +import { CfAlarmRuleComponent } from "@home/components/alarm-rules/cf-alarm-rule.component"; +import { CfAlarmRuleConditionComponent } from "@home/components/alarm-rules/cf-alarm-rule-condition.component"; +import { + CfAlarmRuleConditionDialogComponent +} from "@home/components/alarm-rules/cf-alarm-rule-condition-dialog.component"; +import { CfAlarmScheduleComponent } from "@home/components/alarm-rules/cf-alarm-schedule.component"; +import { CfAlarmScheduleDialogComponent } from "@home/components/alarm-rules/cf-alarm-schedule-dialog.component"; +import { + EntityDebugSettingsButtonComponent +} from "@home/components/entity/debug/entity-debug-settings-button.component"; +import { AlarmRuleFilterTextComponent } from "@home/components/alarm-rules/filter/alarm-rule-filter-text.component"; +import { + CalculatedFieldArgumentsTableModule +} from "@home/components/calculated-fields/components/calculated-field-arguments/calculated-field-arguments-table.module"; +import { + AlarmRuleFilterPredicateListComponent +} from "@home/components/alarm-rules/filter/alarm-rule-filter-predicate-list.component"; +import { + AlarmRuleFilterPredicateComponent +} from "@home/components/alarm-rules/filter/alarm-rule-filter-predicate.component"; +import { + AlarmRuleFilterPredicateValueComponent +} from "@home/components/alarm-rules/filter/alarm-rule-filter-predicate-value.component"; +import { + AlarmRuleComplexFilterPredicateDialogComponent +} from "@home/components/alarm-rules/filter/alarm-rule-complex-filter-predicate-dialog.component"; +import { AlarmRuleFilterListComponent } from "@home/components/alarm-rules/filter/alarm-rule-filter-list.component"; +import { AlarmRuleFilterDialogComponent } from "@home/components/alarm-rules/filter/alarm-rule-filter-dialog.component"; +import { AlarmRuleDetailsDialogComponent } from "@home/components/alarm-rules/alarm-rule-details-dialog.component"; + +@NgModule({ + declarations: [ + AlarmRuleDialogComponent, + CreateCfAlarmRulesComponent, + CfAlarmRuleComponent, + CfAlarmRuleConditionComponent, + CfAlarmRuleConditionDialogComponent, + CfAlarmScheduleComponent, + CfAlarmScheduleDialogComponent, + AlarmRuleFilterTextComponent, + AlarmRuleFilterListComponent, + AlarmRuleFilterDialogComponent, + AlarmRuleFilterPredicateListComponent, + AlarmRuleFilterPredicateComponent, + AlarmRuleFilterPredicateValueComponent, + AlarmRuleComplexFilterPredicateDialogComponent, + AlarmRuleDetailsDialogComponent, + ], + imports: [ + CommonModule, + SharedModule, + EntityDebugSettingsButtonComponent, + CalculatedFieldArgumentsTableModule + ], + exports: [ + AlarmRuleDialogComponent, + ] +}) +export class AlarmRuleModule { } diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table-config.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table-config.ts new file mode 100644 index 0000000000..5167e32853 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table-config.ts @@ -0,0 +1,281 @@ +/// +/// Copyright © 2016-2025 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { + checkBoxCell, + DateEntityTableColumn, + EntityTableColumn, + EntityTableConfig +} from '@home/models/entity/entities-table-config.models'; +import { EntityType } from '@shared/models/entity-type.models'; +import { TranslateService } from '@ngx-translate/core'; +import { Direction } from '@shared/models/page/sort-order'; +import { MatDialog } from '@angular/material/dialog'; +import { PageLink } from '@shared/models/page/page-link'; +import { Observable, of } from 'rxjs'; +import { PageData } from '@shared/models/page/page-data'; +import { EntityId } from '@shared/models/id/entity-id'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { getCurrentAuthUser } from '@core/auth/auth.selectors'; +import { DestroyRef, Renderer2 } from '@angular/core'; +import { EntityDebugSettings } from '@shared/models/entity.models'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { CalculatedFieldsService } from '@core/http/calculated-fields.service'; +import { catchError, filter, switchMap } from 'rxjs/operators'; +import { + ArgumentEntityType, + CalculatedField, + CalculatedFieldAlarmRule, + CalculatedFieldType, +} from '@shared/models/calculated-field.models'; +import { ImportExportService } from '@shared/import-export/import-export.service'; +import { EntityDebugSettingsService } from '@home/components/entity/debug/entity-debug-settings.service'; +import { DatePipe } from '@angular/common'; +import { + AlarmRuleDialogComponent, + AlarmRuleDialogData +} from "@home/components/alarm-rules/alarm-rule-dialog.component"; +import { + CalculatedFieldDebugDialogComponent, + CalculatedFieldDebugDialogData +} from "@home/components/calculated-fields/components/debug-dialog/calculated-field-debug-dialog.component"; +import { AlarmSeverity, alarmSeverityTranslations } from "@shared/models/alarm.models"; +import { UtilsService } from "@core/services/utils.service"; + +export class AlarmRulesTableConfig extends EntityTableConfig { + + readonly tenantId = getCurrentAuthUser(this.store).tenantId; + additionalDebugActionConfig = { + title: this.translate.instant('calculated-fields.see-debug-events'), + action: (calculatedField: CalculatedField) => this.openDebugEventsDialog.call(this, calculatedField), + }; + + constructor(private calculatedFieldsService: CalculatedFieldsService, + private translate: TranslateService, + private dialog: MatDialog, + private datePipe: DatePipe, + public entityId: EntityId = null, + private store: Store, + private destroyRef: DestroyRef, + private renderer: Renderer2, + public entityName: string, + private ownerId: EntityId = null, + private importExportService: ImportExportService, + private entityDebugSettingsService: EntityDebugSettingsService, + private utilsService: UtilsService, + ) { + super(); + this.tableTitle = this.translate.instant('alarm-rule.alarm-rules'); + this.detailsPanelEnabled = false; + this.pageMode = false; + this.entityType = EntityType.CALCULATED_FIELD; + this.entityTranslations = { + type: 'alarm-rule.alarm-rule', + typePlural: 'alarm-rule.alarm-rules', + list: 'alarm-rule.list', + add: 'action.add', + noEntities: 'alarm-rule.no-found', + search: 'action.search', + selectedEntities: 'alarm-rule.selected-fields' + }; + + this.entitiesFetchFunction = (pageLink: PageLink) => this.fetchCalculatedFields(pageLink); + this.addEntity = this.getCalculatedAlarmDialog.bind(this); + this.deleteEntityTitle = (field: CalculatedField) => this.translate.instant('alarm-rule.delete-title', {title: field.name}); + this.deleteEntityContent = () => this.translate.instant('alarm-rule.delete-text'); + this.deleteEntitiesTitle = count => this.translate.instant('alarm-rule.delete-multiple-title', {count}); + this.deleteEntitiesContent = () => this.translate.instant('alarm-rule.delete-multiple-text'); + this.deleteEntity = id => this.calculatedFieldsService.deleteCalculatedField(id.id); + this.addActionDescriptors = [ + { + name: this.translate.instant('alarm-rule.create'), + icon: 'insert_drive_file', + isEnabled: () => true, + onAction: ($event) => this.getTable().addEntity($event) + }, + { + name: this.translate.instant('alarm-rule.import'), + icon: 'file_upload', + isEnabled: () => true, + onAction: () => this.importCalculatedField() + } + ]; + + this.defaultSortOrder = {property: 'createdTime', direction: Direction.DESC}; + this.columns.push(new DateEntityTableColumn('createdTime', 'common.created-time', this.datePipe, '150px')); + this.columns.push(new EntityTableColumn('name', 'alarm-rule.alarm-type', '33%', + entity => this.utilsService.customTranslation(entity.name, entity.name))); + this.columns.push(new EntityTableColumn('createRule', 'alarm-rule.severities', '67%', + entity => Object.keys(entity.configuration.createRules).map((severity) => this.translate.instant(alarmSeverityTranslations.get(severity as AlarmSeverity))).join(', '), + () => ({}), false)); + this.columns.push(new EntityTableColumn('clearRule', 'alarm-rule.cleared', '70px', + entity => checkBoxCell(!!entity.configuration.clearRule), ()=> { return {padding: 0, textAlign: 'center'}}, false)); + + this.cellActionDescriptors.push( + { + name: this.translate.instant('action.export'), + icon: 'file_download', + isEnabled: () => true, + onAction: (event$, entity) => this.exportAlarmRule(event$, entity), + }, + { + name: this.translate.instant('entity-view.events'), + icon: 'mdi:clipboard-text-clock', + isEnabled: () => true, + onAction: (_, entity) => this.openDebugEventsDialog(entity), + }, + { + name: '', + nameFunction: entity => this.entityDebugSettingsService.getDebugConfigLabel(entity?.debugSettings), + icon: 'mdi:bug', + isEnabled: () => true, + iconFunction: ({ debugSettings }) => this.entityDebugSettingsService.isDebugActive(debugSettings?.allEnabledUntil) || debugSettings?.failuresEnabled ? 'mdi:bug' : 'mdi:bug-outline', + onAction: ($event, entity) => this.onOpenDebugConfig($event, entity), + }, + { + name: this.translate.instant('action.edit'), + icon: 'edit', + isEnabled: () => true, + onAction: (_, entity) => this.editCalculatedField(entity), + } + ); + } + + fetchCalculatedFields(pageLink: PageLink): Observable> { + return this.calculatedFieldsService.getCalculatedFields(this.entityId, pageLink, CalculatedFieldType.ALARM); + } + + onOpenDebugConfig($event: Event, calculatedField: CalculatedField): void { + const { debugSettings = {}, id } = calculatedField; + const additionalActionConfig = { + ...this.additionalDebugActionConfig, + action: () => this.openDebugEventsDialog(calculatedField) + }; + if ($event) { + $event.stopPropagation(); + } + + const { viewContainerRef, renderer } = this.entityDebugSettingsService; + if (!viewContainerRef || !renderer) { + this.entityDebugSettingsService.viewContainerRef = this.getTable().viewContainerRef; + this.entityDebugSettingsService.renderer = this.renderer; + } + + this.entityDebugSettingsService.openDebugStrategyPanel({ + debugSettings, + debugConfig: { + entityType: EntityType.CALCULATED_FIELD, + entityLabel: 'alarm-rule.alarm-rule', + additionalActionConfig, + }, + onSettingsAppliedFn: settings => this.onDebugConfigChanged(id.id, settings) + }, $event.target as Element); + } + + private editCalculatedField(calculatedField: CalculatedField, isDirty = false): void { + this.getCalculatedAlarmDialog(calculatedField, 'action.apply', isDirty) + .subscribe((res) => { + if (res) { + this.updateData(); + } + }); + } + + private getCalculatedAlarmDialog(value?: CalculatedField, buttonTitle = 'action.add', isDirty = false): Observable { + return this.dialog.open(AlarmRuleDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + value, + buttonTitle, + entityId: this.entityId, + tenantId: this.tenantId, + entityName: this.entityName, + ownerId: this.ownerId, + additionalDebugActionConfig: this.additionalDebugActionConfig, + isDirty, + }, + enterAnimationDuration: isDirty ? 0 : null, + }) + .afterClosed() + .pipe(filter(Boolean)); + } + + private openDebugEventsDialog(calculatedField: CalculatedField): void { + this.dialog.open(CalculatedFieldDebugDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + tenantId: this.tenantId, + value: calculatedField, + getTestScriptDialogFn: null, + } + }) + .afterClosed() + .subscribe(); + } + + private exportAlarmRule($event: Event, calculatedField: CalculatedField): void { + if ($event) { + $event.stopPropagation(); + } + this.importExportService.exportCalculatedField(calculatedField.id.id); + } + + private importCalculatedField(): void { + this.importExportService.openCalculatedFieldImportDialog() + .pipe( + filter(Boolean), + switchMap(calculatedField => this.getCalculatedAlarmDialog(this.updateImportedCalculatedField(calculatedField), 'action.add', true)), + filter(Boolean), + switchMap(calculatedField => this.calculatedFieldsService.saveCalculatedField(calculatedField)), + filter(Boolean), + takeUntilDestroyed(this.destroyRef) + ) + .subscribe(() => this.updateData()); + } + + private updateImportedCalculatedField(calculatedField: CalculatedField): CalculatedField { + if (calculatedField.type === CalculatedFieldType.GEOFENCING) { + calculatedField.configuration.zoneGroups = Object.keys(calculatedField.configuration.zoneGroups).reduce((acc, key) => { + const arg = calculatedField.configuration.zoneGroups[key]; + acc[key] = arg.refEntityId?.entityType === ArgumentEntityType.Tenant + ? { ...arg, refEntityId: { id: this.tenantId, entityType: ArgumentEntityType.Tenant } } + : arg; + return acc; + }, {}); + } else { + calculatedField.configuration.arguments = Object.keys(calculatedField.configuration.arguments).reduce((acc, key) => { + const arg = calculatedField.configuration.arguments[key]; + acc[key] = arg.refEntityId?.entityType === ArgumentEntityType.Tenant + ? { ...arg, refEntityId: { id: this.tenantId, entityType: ArgumentEntityType.Tenant } } + : arg; + return acc; + }, {}); + } + + return calculatedField; + } + + private onDebugConfigChanged(id: string, debugSettings: EntityDebugSettings): void { + this.calculatedFieldsService.getCalculatedFieldById(id).pipe( + switchMap(field => this.calculatedFieldsService.saveCalculatedField({ ...field, debugSettings })), + catchError(() => of(null)), + takeUntilDestroyed(this.destroyRef), + ).subscribe(() => this.updateData()); + } +} diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table.component.html b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table.component.html new file mode 100644 index 0000000000..e9eebb226c --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table.component.html @@ -0,0 +1,20 @@ + +@if (alarmRulesTableConfig) { + +} diff --git a/application/src/main/java/org/thingsboard/server/service/cf/CalculatedFieldInitService.java b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table.component.scss similarity index 86% rename from application/src/main/java/org/thingsboard/server/service/cf/CalculatedFieldInitService.java rename to ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table.component.scss index 6505dae581..0e0dacc392 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/CalculatedFieldInitService.java +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table.component.scss @@ -13,7 +13,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.service.cf; - -public interface CalculatedFieldInitService { +:host ::ng-deep { + tb-entities-table { + --mat-sidenav-content-background-color: white; + } } diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table.component.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table.component.ts new file mode 100644 index 0000000000..4ade1c5461 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table.component.ts @@ -0,0 +1,91 @@ +/// +/// Copyright © 2016-2025 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + DestroyRef, + effect, + input, + Renderer2, + ViewChild, +} from '@angular/core'; +import { EntityId } from '@shared/models/id/entity-id'; +import { EntitiesTableComponent } from '@home/components/entity/entities-table.component'; +import { TranslateService } from '@ngx-translate/core'; +import { MatDialog } from '@angular/material/dialog'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { CalculatedFieldsService } from '@core/http/calculated-fields.service'; +import { ImportExportService } from '@shared/import-export/import-export.service'; +import { EntityDebugSettingsService } from '@home/components/entity/debug/entity-debug-settings.service'; +import { DatePipe } from '@angular/common'; +import { AlarmRulesTableConfig } from "@home/components/alarm-rules/alarm-rules-table-config"; +import { UtilsService } from "@core/services/utils.service"; + +@Component({ + selector: 'tb-alarm-rules-table', + templateUrl: './alarm-rules-table.component.html', + styleUrls: ['./alarm-rules-table.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, + providers: [EntityDebugSettingsService] +}) +export class AlarmRulesTableComponent { + + @ViewChild(EntitiesTableComponent, {static: true}) entitiesTable: EntitiesTableComponent; + + active = input(); + entityId = input(); + entityName = input(); + ownerId = input(); + + alarmRulesTableConfig: AlarmRulesTableConfig; + + constructor(private calculatedFieldsService: CalculatedFieldsService, + private translate: TranslateService, + private dialog: MatDialog, + private store: Store, + private datePipe: DatePipe, + private cd: ChangeDetectorRef, + private renderer: Renderer2, + private importExportService: ImportExportService, + private entityDebugSettingsService: EntityDebugSettingsService, + private utilsService: UtilsService, + private destroyRef: DestroyRef) { + + effect(() => { + if (this.active()) { + this.alarmRulesTableConfig = new AlarmRulesTableConfig( + this.calculatedFieldsService, + this.translate, + this.dialog, + this.datePipe, + this.entityId(), + this.store, + this.destroyRef, + this.renderer, + this.entityName(), + this.ownerId(), + this.importExportService, + this.entityDebugSettingsService, + this.utilsService, + ); + this.cd.markForCheck(); + } + }); + } +} diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition-dialog.component.html b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition-dialog.component.html new file mode 100644 index 0000000000..d2f98ecd85 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition-dialog.component.html @@ -0,0 +1,188 @@ + +
    + +

    {{ (readonly ? 'alarm-rule.alarm-rule-condition' : 'alarm-rule.edit-alarm-rule-condition') | translate }}

    + +
    + + {{ 'alarm-rule.expression-type.simple' | translate }} + {{ 'alarm-rule.expression-type.tbel' | translate }} + +
    + +
    +
    +
    +
    + @if (conditionFormGroup.get('expression.type').value === AlarmRuleExpressionType.SIMPLE) { +
    +
    +
    {{ 'alarm-rule.argument-filters' | translate }}
    + + {{ complexOperationTranslationMap.get(ComplexOperation.AND) | translate }} + {{ complexOperationTranslationMap.get(ComplexOperation.OR) | translate }} + +
    + + +
    + } @else { +
    +
    + {{ 'alarm-rule.script' | translate }} +
    + +
    {{ 'alarm-rule.expression-type.tbel' | translate }} +
    +
    +
    + } +
    +
    +
    {{ 'alarm-rule.condition-settings' | translate }}
    + + alarm-rule.condition-type + + + {{ alarmConditionTypeTranslation.get(alarmConditionType) | translate }} + + + + @if (conditionFormGroup.get('type').value == AlarmConditionType.DURATION) { +
    +
    +
    {{ 'alarm-rule.value' | translate }}
    + + {{ 'alarm-rule.static' | translate }} + {{ 'alarm-rule.dynamic' | translate }} + +
    +
    +
    + +
    +
    + +
    +
    + + + + {{ timeUnitTranslations.get(timeUnit) | translate }} + + + + {{ 'alarm-rule.condition-duration-time-unit-required' | translate }} + + +
    +
    +
    + } @else if (conditionFormGroup.get('type').value == AlarmConditionType.REPEATING) { +
    +
    +
    {{ 'alarm-rule.value' | translate }}
    + + {{ 'alarm-rule.static' | translate }} + {{ 'alarm-rule.dynamic' | translate }} + +
    +
    +
    + +
    +
    + +
    +
    +
    + } +
    +
    +
    +
    + + @if (!readonly) { + + } +
    + + +
    + + + {{ defaultValuePlaceholder | translate }} + @if (conditionFormGroup.get(groupName).get('staticValue').hasError('required')) { + {{ defaultValueRequiredError | translate }} + } @else if (conditionFormGroup.get(groupName).get('staticValue').hasError('min')) { + {{ defaultValueRangeError | translate }} + } @else if (conditionFormGroup.get(groupName).get('staticValue').hasError('max')) { + {{ defaultValueRangeError | translate }} + } @else if (conditionFormGroup.get(groupName).get('staticValue').hasError('pattern')) { + {{ defaultValuePatternError | translate }} + } + +
    +
    + + + + alarm-rule.value-argument + + @for (argument of argumentsList; track argument) { + {{ argument }} + } + + + {{ 'calculated-fields.hint.argument-name-required' | translate }} + + + + +
    + diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition-dialog.component.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition-dialog.component.ts new file mode 100644 index 0000000000..48b290b32c --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition-dialog.component.ts @@ -0,0 +1,226 @@ +/// +/// Copyright © 2016-2025 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, Inject } from '@angular/core'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { FormBuilder, Validators } from '@angular/forms'; +import { Router } from '@angular/router'; +import { DialogComponent } from '@app/shared/components/dialog.component'; +import { TimeUnit, timeUnitTranslationMap } from '@shared/models/time/time.models'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { ScriptLanguage } from "@shared/models/rule-node.models"; +import { + AlarmRuleCondition, + AlarmRuleConditionType, + AlarmRuleConditionTypeTranslationMap, + AlarmRuleExpressionType +} from "@shared/models/alarm-rule.models"; +import { + CalculatedFieldArgument, + getCalculatedFieldArgumentsEditorCompleter, + getCalculatedFieldArgumentsHighlights +} from "@shared/models/calculated-field.models"; +import { TbEditorCompleter } from "@shared/models/ace/completion.models"; +import { AceHighlightRules } from "@shared/models/ace/ace.models"; +import { ComplexOperation, complexOperationTranslationMap } from "@shared/models/query/query.models"; + +export interface CfAlarmRuleConditionDialogData { + readonly: boolean; + condition: AlarmRuleCondition; + arguments?: Record; +} + +@Component({ + selector: 'tb-cf-alarm-rule-condition-dialog', + templateUrl: './cf-alarm-rule-condition-dialog.component.html', + providers: [], + styleUrls: ['./cf-alarm-rules-dialog.component.scss'], +}) +export class CfAlarmRuleConditionDialogComponent extends DialogComponent { + + AlarmRuleExpressionType = AlarmRuleExpressionType; + + timeUnits = Object.values(TimeUnit); + timeUnitTranslations = timeUnitTranslationMap; + alarmConditionTypes = Object.values(AlarmRuleConditionType); + AlarmConditionType = AlarmRuleConditionType; + alarmConditionTypeTranslation = AlarmRuleConditionTypeTranslationMap; + readonly = this.data.readonly; + condition = this.data.condition; + + conditionFormGroup = this.fb.group({ + expression: this.fb.group({ + type: [AlarmRuleExpressionType.SIMPLE], + expression: ['', [Validators.required]], + operation: [ComplexOperation.AND], + filters: [null], + }), + type: this.fb.control(AlarmRuleConditionType.SIMPLE, Validators.required), + unit: this.fb.control(TimeUnit.SECONDS, Validators.required), + value: this.fb.group({ + staticValue: this.fb.control(null, [Validators.required, Validators.min(1), Validators.max(2147483647), Validators.pattern('[0-9]*')]), + dynamicValueArgument: this.fb.control('', Validators.required), + }), + count: this.fb.group({ + staticValue: this.fb.control(null, [Validators.required, Validators.min(1), Validators.max(2147483647), Validators.pattern('[0-9]*')]), + dynamicValueArgument: this.fb.control('', Validators.required), + }), + }); + + readonly scriptLanguage = ScriptLanguage; + + defaultValuePlaceholder = ''; + defaultValueRequiredError = ''; + defaultValueRangeError = ''; + defaultValuePatternError = ''; + + durationDynamicModeControl = this.fb.control(false); + repeatingDynamicModeControl = this.fb.control(false); + + ComplexOperation = ComplexOperation; + complexOperationTranslationMap = complexOperationTranslationMap; + + functionArgs: Array; + argumentsEditorCompleter: TbEditorCompleter; + argumentsHighlightRules: AceHighlightRules; + + arguments = this.data.arguments; + argumentsList: Array; + + constructor(protected store: Store, + protected router: Router, + @Inject(MAT_DIALOG_DATA) public data: CfAlarmRuleConditionDialogData, + public dialogRef: MatDialogRef, + private fb: FormBuilder) { + super(store, router, dialogRef); + + this.functionArgs = ['ctx', ...Object.keys(this.data.arguments)]; + this.argumentsEditorCompleter = getCalculatedFieldArgumentsEditorCompleter(this.data.arguments); + this.argumentsHighlightRules = getCalculatedFieldArgumentsHighlights(this.data.arguments); + this.argumentsList = this.arguments ? Object.keys(this.arguments): []; + + this.conditionFormGroup.patchValue({ + expression: { + type: this.condition?.expression?.type ?? AlarmRuleExpressionType.SIMPLE, + expression: this.condition?.expression?.expression ?? null, + filters: this.condition?.expression?.filters ?? [], + operation: this.condition?.expression?.operation ?? ComplexOperation.AND + }, + type: this.condition?.type ?? AlarmRuleConditionType.SIMPLE, + unit: this.condition?.unit ?? TimeUnit.SECONDS, + value: { + staticValue: this.condition?.value?.staticValue, + dynamicValueArgument: this.condition?.value?.dynamicValueArgument, + }, + count: { + staticValue: this.condition?.count?.staticValue ?? null, + dynamicValueArgument: this.condition?.count?.dynamicValueArgument + } + }, {emitEvent: false}); + + this.durationDynamicModeControl.patchValue(!!this.condition?.value?.dynamicValueArgument, {emitEvent: false}); + this.repeatingDynamicModeControl.patchValue(!!this.condition?.count?.dynamicValueArgument, {emitEvent: false}); + + this.conditionFormGroup.get('type').valueChanges.pipe( + takeUntilDestroyed() + ).subscribe((type) => { + this.updateValidators(type, true); + }); + + this.conditionFormGroup.get('expression.type').valueChanges.pipe( + takeUntilDestroyed() + ).subscribe((type) => { + this.updateExpressionTypeValidator(type); + this.updateValidators(this.conditionFormGroup.get('type').value ?? AlarmRuleConditionType.SIMPLE); + }); + + this.durationDynamicModeControl.valueChanges.pipe( + takeUntilDestroyed() + ).subscribe((mode) => { + this.updateStaticValueValidator(AlarmRuleConditionType.DURATION, mode); + }); + this.repeatingDynamicModeControl.valueChanges.pipe( + takeUntilDestroyed() + ).subscribe((mode) => { + this.updateStaticValueValidator(AlarmRuleConditionType.REPEATING, mode); + }); + + this.updateValidators(this.conditionFormGroup.get('type').value ?? AlarmRuleConditionType.SIMPLE); + this.updateExpressionTypeValidator(this.condition?.expression?.type ?? 'SIMPLE'); + } + + updateStaticValueValidator(type: AlarmRuleConditionType, dynamicValue: boolean) { + const control = this.conditionFormGroup.get(type === AlarmRuleConditionType.DURATION ? 'value' : 'count'); + if (dynamicValue) { + control.get('staticValue').disable({emitEvent: false}); + control.get('dynamicValueArgument').enable({emitEvent: false}); + } else { + control.get('staticValue').enable({emitEvent: false}); + control.get('dynamicValueArgument').disable({emitEvent: false}); + } + } + + updateExpressionTypeValidator(type: 'SIMPLE' | 'TBEL') { + if (type === 'SIMPLE') { + this.conditionFormGroup.get(`expression.expression`).disable({emitEvent: false}); + this.conditionFormGroup.get(`expression.filters`).enable({emitEvent: false}); + } else { + this.conditionFormGroup.get(`expression.expression`).enable({emitEvent: false}); + this.conditionFormGroup.get(`expression.filters`).disable({emitEvent: false}); + } + } + + private updateValidators(type: AlarmRuleConditionType, emitEvent = false) { + switch (type) { + case AlarmRuleConditionType.DURATION: + this.conditionFormGroup.get('unit').enable({emitEvent: false}); + this.conditionFormGroup.get('value').enable({emitEvent: false}); + this.conditionFormGroup.get('count').disable({emitEvent: false}); + this.updateStaticValueValidator(type, this.durationDynamicModeControl.value); + this.defaultValuePlaceholder = 'alarm-rule.condition-duration-value'; + this.defaultValueRequiredError = 'alarm-rule.condition-duration-value-required'; + this.defaultValueRangeError = 'alarm-rule.condition-duration-value-range'; + this.defaultValuePatternError = 'alarm-rule.condition-duration-value-pattern'; + break; + case AlarmRuleConditionType.REPEATING: + this.conditionFormGroup.get('count').enable({emitEvent: false}); + this.conditionFormGroup.get('value').disable({emitEvent: false}); + this.conditionFormGroup.get('unit').disable({emitEvent: false}); + this.updateStaticValueValidator(type, this.repeatingDynamicModeControl.value); + this.defaultValuePlaceholder = 'alarm-rule.condition-repeating-value'; + this.defaultValueRequiredError = 'alarm-rule.condition-repeating-value-required'; + this.defaultValueRangeError = 'alarm-rule.condition-repeating-value-range'; + this.defaultValuePatternError = 'alarm-rule.condition-repeating-value-pattern'; + break; + case AlarmRuleConditionType.SIMPLE: + this.conditionFormGroup.get('value').disable({emitEvent: false}); + this.conditionFormGroup.get('count').disable({emitEvent: false}); + this.conditionFormGroup.get('unit').disable({emitEvent: false}); + break; + } + } + + cancel(): void { + this.dialogRef.close(null); + } + + save(): void { + this.dialogRef.close(this.conditionFormGroup.value as AlarmRuleCondition); + } + +} diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition.component.html b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition.component.html new file mode 100644 index 0000000000..e17d8c8261 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition.component.html @@ -0,0 +1,52 @@ + +
    +
    +
    {{ 'alarm-rule.condition' | translate }}
    + +
    +
    +
    {{ 'alarm-rule.schedule-title' | translate }}
    + +
    +
    diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition.component.scss b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition.component.scss new file mode 100644 index 0000000000..ac77089857 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition.component.scss @@ -0,0 +1,37 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host { + display: flex; + flex: 1; + + .tb-alarm-rule-condition { + display: flex; + flex: 1; + &-button { + --mat-outlined-button-horizontal-padding: 3px 0px 12px; + display: block; + width: 100%; + } + &-label { + display: block; + text-align: start; + width: 100%; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition.component.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition.component.ts new file mode 100644 index 0000000000..17ed11d892 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition.component.ts @@ -0,0 +1,281 @@ +/// +/// Copyright © 2016-2025 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { ChangeDetectorRef, Component, forwardRef, Input } from '@angular/core'; +import { + ControlValueAccessor, + FormBuilder, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + UntypedFormControl, + Validator, + Validators +} from '@angular/forms'; +import { MatDialog } from '@angular/material/dialog'; +import { deepClone, isDefinedAndNotNull } from '@core/utils'; +import { TranslateService } from '@ngx-translate/core'; +import { + dayOfWeekTranslations, + getAlarmScheduleRangeText, + utcTimestampToTimeOfDay +} from '@shared/models/device.models'; +import { TimeUnit } from '@shared/models/time/time.models'; +import { + CfAlarmRuleConditionDialogComponent, + CfAlarmRuleConditionDialogData +} from "@home/components/alarm-rules/cf-alarm-rule-condition-dialog.component"; +import { + AlarmRuleCondition, + AlarmRuleConditionType, + AlarmRuleExpressionType, + AlarmRuleSchedule, + AlarmRuleScheduleType +} from "@shared/models/alarm-rule.models"; +import { CalculatedFieldArgument } from "@shared/models/calculated-field.models"; +import { + AlarmRuleScheduleDialogData, + CfAlarmScheduleDialogComponent +} from "@home/components/alarm-rules/cf-alarm-schedule-dialog.component"; +import { coerceBoolean } from "@shared/decorators/coercion"; + +@Component({ + selector: 'tb-cf-alarm-rule-condition', + templateUrl: './cf-alarm-rule-condition.component.html', + styleUrls: ['./cf-alarm-rule-condition.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => CfAlarmRuleConditionComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => CfAlarmRuleConditionComponent), + multi: true, + } + ] +}) +export class CfAlarmRuleConditionComponent implements ControlValueAccessor, Validator { + + @Input() + @coerceBoolean() + disabled: boolean; + + @Input() + arguments: Record; + + alarmRuleConditionFormGroup = this.fb.group({ + type: ['SIMPLE'], + expression: [{type: AlarmRuleExpressionType.SIMPLE}, Validators.required], + schedule: [null], + }); + + specText = ''; + + scheduleText = ''; + + private modelValue: AlarmRuleCondition; + + private propagateChange = (v: any) => { }; + + constructor(private dialog: MatDialog, + private fb: FormBuilder, + private cd: ChangeDetectorRef, + private translate: TranslateService) { + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (this.disabled) { + this.alarmRuleConditionFormGroup.disable({emitEvent: false}); + } else { + this.alarmRuleConditionFormGroup.enable({emitEvent: false}); + } + } + + writeValue(value: AlarmRuleCondition): void { + this.modelValue = value; + this.updateConditionInfo(); + } + + public conditionSet() { + return this.modelValue && (this.modelValue.expression?.expression || this.modelValue.expression?.filters); + } + + public validate(c: UntypedFormControl) { + return this.conditionSet() ? null : { + alarmRuleCondition: { + valid: false, + }, + }; + } + + public openFilterDialog($event: Event) { + if ($event) { + $event.stopPropagation(); + } + this.dialog.open(CfAlarmRuleConditionDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + readonly: this.disabled, + condition: this.disabled ? this.modelValue : deepClone(this.modelValue), + arguments: this.arguments + } + }).afterClosed().subscribe((result) => { + if (result) { + this.modelValue = {...this.modelValue, ...result}; + this.updateModel(); + this.cd.detectChanges(); + } + }); + } + + private updateConditionInfo() { + this.alarmRuleConditionFormGroup.patchValue( + { + type: this.modelValue?.type, + expression: this.modelValue?.expression, + schedule: this.modelValue?.schedule, + }, {emitEvent: false} + ); + this.updateScheduleText(); + this.updateSpecText(); + } + + private updateSpecText() { + this.specText = ''; + if (this.modelValue && this.modelValue.type) { + const type = this.modelValue.type; + switch (type) { + case AlarmRuleConditionType.SIMPLE: + break; + case AlarmRuleConditionType.DURATION: + let duringText = ''; + switch (this.modelValue.unit) { + case TimeUnit.SECONDS: + duringText = this.translate.instant('timewindow.seconds', {seconds: this.modelValue.value.staticValue}); + break; + case TimeUnit.MINUTES: + duringText = this.translate.instant('timewindow.minutes', {minutes: this.modelValue.value.staticValue}); + break; + case TimeUnit.HOURS: + duringText = this.translate.instant('timewindow.hours', {hours: this.modelValue.value.staticValue}); + break; + case TimeUnit.DAYS: + duringText = this.translate.instant('timewindow.days', {days: this.modelValue.value.staticValue}); + break; + } + if (this.modelValue.value.dynamicValueArgument) { + this.specText = this.translate.instant('alarm-rule.condition-during-dynamic', { + attribute: `${this.modelValue.value.dynamicValueArgument}` + }); + } else { + this.specText = this.translate.instant('alarm-rule.condition-during', { + during: duringText + }); + } + break; + case AlarmRuleConditionType.REPEATING: + if (this.modelValue.count.dynamicValueArgument) { + this.specText = this.translate.instant('alarm-rule.condition-repeat-times-dynamic', { + attribute: `${this.modelValue.count.dynamicValueArgument}` + }); + } else { + this.specText = this.translate.instant('alarm-rule.condition-repeat-times', + {count: this.modelValue.count.staticValue}); + } + break; + } + } + } + + private updateModel() { + this.updateConditionInfo(); + this.propagateChange(this.modelValue); + } + + public openScheduleDialog($event: Event) { + if ($event) { + $event.stopPropagation(); + } + this.dialog.open(CfAlarmScheduleDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + readonly: this.disabled, + alarmSchedule: this.disabled ? this.modelValue?.schedule : deepClone(this.modelValue?.schedule), + arguments: this.arguments + } + }).afterClosed().subscribe((result) => { + if (result) { + this.modelValue.schedule = result; + this.updateModel(); + this.cd.detectChanges(); + } + }); + } + + private updateScheduleText() { + let schedule = this.modelValue?.schedule; + this.scheduleText = ''; + if (isDefinedAndNotNull(schedule)) { + if (schedule.dynamicValueArgument) { + this.scheduleText = this.translate.instant('alarm-rule.value-argument') + ': ' + schedule?.dynamicValueArgument + } else { + switch (schedule.staticValue.type) { + case AlarmRuleScheduleType.ANY_TIME: + this.scheduleText = this.translate.instant('alarm-rule.schedule.any-time'); + break; + case AlarmRuleScheduleType.SPECIFIC_TIME: + for (const day of schedule.staticValue.daysOfWeek) { + if (this.scheduleText.length) { + this.scheduleText += ', '; + } + this.scheduleText += this.translate.instant(dayOfWeekTranslations[day - 1]); + } + this.scheduleText += ' ' + getAlarmScheduleRangeText(utcTimestampToTimeOfDay(schedule.staticValue.startsOn), + utcTimestampToTimeOfDay(schedule.staticValue.endsOn)) + ''; + break; + case AlarmRuleScheduleType.CUSTOM: + for (const item of schedule.staticValue.items) { + if (item.enabled) { + if (this.scheduleText.length) { + this.scheduleText += ', '; + } + this.scheduleText += this.translate.instant(dayOfWeekTranslations[item.dayOfWeek - 1]); + this.scheduleText += ' ' + getAlarmScheduleRangeText(utcTimestampToTimeOfDay(item.startsOn), + utcTimestampToTimeOfDay(item.endsOn)) + ''; + } + } + break; + } + } + } + if (!this.scheduleText.length) { + this.scheduleText = this.translate.instant('alarm-rule.schedule.any-time'); + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule.component.html b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule.component.html new file mode 100644 index 0000000000..cd6b2c3796 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule.component.html @@ -0,0 +1,50 @@ + +
    + + + @if (!disabled || alarmRuleFormGroup.get('alarmDetails').value) { +
    +
    + alarm-rule.alarm-rule-additional-info +
    + + + + +
    + } + @if (!disabled || alarmRuleFormGroup.get('dashboardId').value) { +
    +
    + alarm-rule.alarm-rule-mobile-dashboard +
    + + +
    + } +
    diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule.component.scss b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule.component.scss new file mode 100644 index 0000000000..21b8a67c24 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule.component.scss @@ -0,0 +1,31 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host { + .tb-alarm-rule-details, .tb-alarm-rule-dashboard { + padding: 4px; + &.title { + opacity: 0.7; + overflow: visible; + } + } + .tb-alarm-rule-details { + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + cursor: pointer; + } +} + diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule.component.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule.component.ts new file mode 100644 index 0000000000..88ae1dd79d --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule.component.ts @@ -0,0 +1,149 @@ +/// +/// Copyright © 2016-2025 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, DestroyRef, forwardRef, Input, OnInit } from '@angular/core'; +import { + ControlValueAccessor, + FormBuilder, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + UntypedFormControl, + Validator, + Validators +} from '@angular/forms'; +import { MatDialog } from '@angular/material/dialog'; +import { isDefinedAndNotNull } from '@core/utils'; +import { DashboardId } from '@shared/models/id/dashboard-id'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { AlarmRule } from "@shared/models/alarm-rule.models"; +import { CalculatedFieldArgument } from "@shared/models/calculated-field.models"; +import { + AlarmRuleDetailsDialogComponent, + AlarmRuleDetailsDialogData +} from "@home/components/alarm-rules/alarm-rule-details-dialog.component"; +import { coerceBoolean } from "@shared/decorators/coercion"; + +@Component({ + selector: 'tb-cf-alarm-rule', + templateUrl: './cf-alarm-rule.component.html', + styleUrls: ['./cf-alarm-rule.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => CfAlarmRuleComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => CfAlarmRuleComponent), + multi: true, + } + ] +}) +export class CfAlarmRuleComponent implements ControlValueAccessor, OnInit, Validator { + + @Input() + @coerceBoolean() + disabled: boolean; + + @Input() + @coerceBoolean() + required: boolean; + + @Input() + arguments: Record; + + private modelValue: AlarmRule; + + alarmRuleFormGroup = this.fb.group({ + condition: this.fb.control({}, Validators.required), + alarmDetails: [null], + dashboardId: [null] + }); + + private propagateChange = (v: any) => { }; + + constructor(private dialog: MatDialog, + private fb: FormBuilder, + private destroyRef: DestroyRef) { + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + ngOnInit() { + this.alarmRuleFormGroup.valueChanges.pipe( + takeUntilDestroyed(this.destroyRef) + ).subscribe(() => { + this.updateModel(); + }); + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (this.disabled) { + this.alarmRuleFormGroup.disable({emitEvent: false}); + } else { + this.alarmRuleFormGroup.enable({emitEvent: false}); + } + } + + writeValue(value: AlarmRule): void { + this.modelValue = value; + const model = this.modelValue ? { + ...this.modelValue, + dashboardId: this.modelValue.dashboardId?.id + } : null; + this.alarmRuleFormGroup.patchValue(model, {emitEvent: false}); + } + + public openEditDetailsDialog($event: Event) { + if ($event) { + $event.stopPropagation(); + } + this.dialog.open(AlarmRuleDetailsDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + alarmDetails: this.alarmRuleFormGroup.get('alarmDetails').value, + readonly: this.disabled + } + }).afterClosed().subscribe((alarmDetails) => { + if (isDefinedAndNotNull(alarmDetails)) { + this.alarmRuleFormGroup.patchValue({alarmDetails}); + } + }); + } + + public validate(c: UntypedFormControl) { + return (!this.required && !this.modelValue || this.alarmRuleFormGroup.valid) ? null : { + alarmRule: { + valid: false, + }, + }; + } + + private updateModel() { + const value = this.alarmRuleFormGroup.value; + this.modelValue = {...value, dashboardId: value.dashboardId ? new DashboardId(value.dashboardId) : null} as AlarmRule; + this.propagateChange(this.modelValue); + } +} diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rules-dialog.component.scss b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rules-dialog.component.scss new file mode 100644 index 0000000000..14df9a3972 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rules-dialog.component.scss @@ -0,0 +1,60 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host { + form { + width: 900px; + max-width: 100%; + display: grid; + grid-template-rows: min-content minmax(auto, 1fr) min-content; + } + + .tbel-script-lang-chip { + line-height: 20px; + font-size: 14px; + font-weight: 500; + color: white; + border-radius: 100px; + width: 70px; + min-width: 70px; + display: flex; + justify-content: center; + margin-top: 2px; + margin-right: 4px; + } + + .tb-js-func { + .ace_tb { + &.ace_calculated-field { + &-ctx { + color: #C52F00; + } + &-args { + color: #185F2A; + } + &-key { + color: #c24c1a; + } + &-time-window, &-values, &-func, &-value, &-ts, &-latestTs { + color: #7214D0; + } + &-start-ts, &-end-ts { + color: #2CAA00; + } + } + } + } +} + diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-schedule-dialog.component.html b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-schedule-dialog.component.html new file mode 100644 index 0000000000..2d12355f48 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-schedule-dialog.component.html @@ -0,0 +1,53 @@ + +
    + +

    {{ (readonly ? 'alarm-rule.schedule-title' : 'alarm-rule.edit-schedule') | translate }}

    + + + {{ 'alarm-rule.static-schedule' | translate }} + {{ 'alarm-rule.dynamic-schedule' | translate }} + + +
    +
    + + +
    +
    + + @if (!readonly) { + + } +
    +
    diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-schedule-dialog.component.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-schedule-dialog.component.ts new file mode 100644 index 0000000000..e132dd499e --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-schedule-dialog.component.ts @@ -0,0 +1,70 @@ +/// +/// Copyright © 2016-2025 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, Inject } from '@angular/core'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { FormBuilder } from '@angular/forms'; +import { Router } from '@angular/router'; +import { DialogComponent } from '@app/shared/components/dialog.component'; +import { AlarmRuleSchedule } from "@shared/models/alarm-rule.models"; +import { CalculatedFieldArgument } from "@shared/models/calculated-field.models"; + +export interface AlarmRuleScheduleDialogData { + readonly: boolean; + alarmSchedule: AlarmRuleSchedule; + arguments: Record; +} + +@Component({ + selector: 'tb-cf-alarm-schedule-dialog', + templateUrl: './cf-alarm-schedule-dialog.component.html', + providers: [], + styleUrls: ['./cf-alarm-rules-dialog.component.scss'], +}) +export class CfAlarmScheduleDialogComponent extends DialogComponent{ + + readonly = this.data.readonly; + alarmSchedule = this.data.alarmSchedule; + arguments = this.data.arguments; + + alarmScheduleControl = this.fb.control(null); + + dynamicModeControl = this.fb.control(false); + + constructor(protected store: Store, + protected router: Router, + @Inject(MAT_DIALOG_DATA) public data: AlarmRuleScheduleDialogData, + public dialogRef: MatDialogRef, + private fb: FormBuilder) { + super(store, router, dialogRef); + this.alarmScheduleControl.patchValue(this.alarmSchedule, {emitEvent: false}); + this.dynamicModeControl.patchValue(!!this.alarmSchedule?.dynamicValueArgument, {emitEvent: false}); + if (this.readonly) { + this.alarmScheduleControl.disable({emitEvent: false}); + this.dynamicModeControl.disable({emitEvent: false}); + } + } + + cancel(): void { + this.dialogRef.close(null); + } + + save(): void { + this.dialogRef.close(this.alarmScheduleControl.value); + } +} diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-schedule.component.html b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-schedule.component.html new file mode 100644 index 0000000000..7fe09d7850 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-schedule.component.html @@ -0,0 +1,125 @@ + +
    + @if (!dynamicMode) { + + + + @for (scheduleType of alarmScheduleTypes; track scheduleType) { + {{ alarmScheduleTypeTranslate.get(scheduleType) | translate }} + } + + @if (alarmScheduleForm.get('staticValue.type').hasError('required')) { + {{ 'alarm-rule.schedule-type-required' | translate }} + } + + + @if (alarmScheduleForm.get('staticValue.type').value !== alarmScheduleType.ANY_TIME) { +
    +
    + + + @if (alarmScheduleForm.get('staticValue.type').value === alarmScheduleType.SPECIFIC_TIME) { +
    + + + {{ dayOfWeekTranslationsArray[day] | translate }} + + + +
    +
    + + alarm-rule.schedule-time-from + + + + + + alarm-rule.schedule-time-to + + + + +
    +
    +
    +
    +
    +
    + } + @if (alarmScheduleForm.get('staticValue.type').value === alarmScheduleType.CUSTOM) { +
    +
    +
    + + {{ dayOfWeekTranslationsArray[day] | translate }} + +
    + + alarm-rule.schedule-time-from + + + + + + alarm-rule.schedule-time-to + + + + +
    +
    +
    +
    +
    + +
    + } +
    +
    + } + } @else { +
    + + alarm-rule.value-argument + + @for (argument of argumentsList; track argument) { + {{ argument }} + } + + @if (alarmScheduleForm.get('dynamicValueArgument').hasError('required')) { + {{ 'calculated-fields.hint.argument-name-required' | translate }} + } + +
    +
    +
    + } +
    diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-schedule.component.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-schedule.component.ts new file mode 100644 index 0000000000..b1c7a4da7f --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-schedule.component.ts @@ -0,0 +1,322 @@ +/// +/// Copyright © 2016-2025 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, DestroyRef, forwardRef, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core'; +import { + AbstractControl, + ControlValueAccessor, + FormArray, + FormBuilder, + FormGroup, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + ValidationErrors, + Validator, + Validators +} from '@angular/forms'; +import { + CustomTimeSchedulerItem, + dayOfWeekTranslations, + getAlarmScheduleRangeText, + timeOfDayToUTCTimestamp, + utcTimestampToTimeOfDay +} from '@shared/models/device.models'; +import { isDefined } from '@core/utils'; +import { getDefaultTimezone } from '@shared/models/time/time.models'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { + AlarmRuleSchedule, + AlarmRuleScheduleType, + AlarmRuleScheduleTypeTranslationMap +} from "@shared/models/alarm-rule.models"; +import { CalculatedFieldArgument } from "@shared/models/calculated-field.models"; +import { MatChipSelectionChange } from "@angular/material/chips"; +import { coerceBoolean } from "@shared/decorators/coercion"; + +@Component({ + selector: 'tb-cf-alarm-schedule', + templateUrl: './cf-alarm-schedule.component.html', + styleUrls: [], + providers: [{ + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => CfAlarmScheduleComponent), + multi: true + }, { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => CfAlarmScheduleComponent), + multi: true + }] +}) +export class CfAlarmScheduleComponent implements ControlValueAccessor, Validator, OnInit, OnChanges { + + @Input() + @coerceBoolean() + disabled: boolean; + + @Input() + arguments: Record; + + @Input() + @coerceBoolean() + dynamicMode: boolean; + + alarmScheduleForm = this.fb.group({ + staticValue: this.fb.group({ + type: [AlarmRuleScheduleType.ANY_TIME, Validators.required], + timezone: ['', Validators.required], + daysOfWeek: this.fb.control(null, Validators.required), + startsOn: this.fb.control(0, Validators.required), + endsOn: this.fb.control(0, Validators.required), + items: this.fb.array(Array.from({length: 7}, (value, i) => this.defaultItemsScheduler(i)), this.validateItems), + }), + dynamicValueArgument: ['', Validators.required] + }); + + alarmScheduleTypes = Object.keys(AlarmRuleScheduleType) as Array; + alarmScheduleType = AlarmRuleScheduleType; + alarmScheduleTypeTranslate = AlarmRuleScheduleTypeTranslationMap; + dayOfWeekTranslationsArray = dayOfWeekTranslations; + + argumentsList: Array; + + allDays = Array(7).fill(0).map((x, i) => i); + + private modelValue: AlarmRuleSchedule; + + private defaultItems = Array.from({length: 7}, (value, i) => ({ + enabled: true, + dayOfWeek: i + 1 + })) as CustomTimeSchedulerItem[]; + + private propagateChange = (v: any) => { }; + + constructor(private fb: FormBuilder, + private destroyRef: DestroyRef) { + } + + ngOnInit(): void { + this.argumentsList = this.arguments ? Object.keys(this.arguments): []; + this.alarmScheduleForm.get('staticValue.type').valueChanges.pipe( + takeUntilDestroyed(this.destroyRef) + ).subscribe((type) => { + const defaultTimezone = getDefaultTimezone(); + const staticValue = {...this.alarmScheduleForm.get('staticValue').value, type, items: this.defaultItems, timezone: defaultTimezone}; + this.alarmScheduleForm.get('staticValue').patchValue(staticValue, {emitEvent: false}); + this.alarmScheduleForm.get('dynamicValueArgument').patchValue(null, {emitEvent: false}); + this.updateValidators(type); + }); + this.alarmScheduleForm.valueChanges.pipe( + takeUntilDestroyed(this.destroyRef) + ).subscribe(() => { + this.updateModel(); + }); + + this.alarmScheduleForm.get('staticValue.items').valueChanges.pipe( + takeUntilDestroyed(this.destroyRef) + ).subscribe((items) => { + items.forEach((item, index) => this.disabledSelectedTime(item.enabled, index, false)) + }); + } + + ngOnChanges(changes: SimpleChanges) { + if (changes.dynamicMode) { + const dynamicModeChanges = changes.dynamicMode; + if (!dynamicModeChanges.firstChange && dynamicModeChanges.currentValue !== dynamicModeChanges.previousValue) { + this.updateModeValidators(dynamicModeChanges.currentValue); + this.updateModel(); + } + } + } + + validateItems(control: AbstractControl): ValidationErrors | null { + const items: any[] = control.value; + if (!items || !items.length || !items.find(v => v.enabled === true)) { + return { + dayOfWeeks: true + }; + } + return null; + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (this.disabled) { + this.alarmScheduleForm.disable({emitEvent: false}); + } else { + this.updateModeValidators(this.dynamicMode); + } + } + + writeValue(value: AlarmRuleSchedule): void { + if (value) { + this.modelValue = value; + if (this.modelValue.dynamicValueArgument) { + this.alarmScheduleForm.get('dynamicValueArgument').patchValue(this.modelValue.dynamicValueArgument, {emitEvent: false}); + } else { + switch (this.modelValue.staticValue.type) { + case AlarmRuleScheduleType.SPECIFIC_TIME: + this.alarmScheduleForm.patchValue({ + staticValue: { + type: this.modelValue.staticValue.type, + timezone: this.modelValue.staticValue.timezone, + daysOfWeek: this.modelValue.staticValue.daysOfWeek, + startsOn: utcTimestampToTimeOfDay(this.modelValue.staticValue.startsOn), + endsOn: utcTimestampToTimeOfDay(this.modelValue.staticValue.endsOn), + }, + }, {emitEvent: false}); + break; + case AlarmRuleScheduleType.CUSTOM: + if (this.modelValue?.dynamicValueArgument) { + this.alarmScheduleForm.patchValue({ + staticValue: { + type: this.modelValue.staticValue.type, + }, + }, {emitEvent: false}); + } else if (this.modelValue.staticValue?.items) { + const alarmDays = []; + this.modelValue.staticValue.items + .sort((a, b) => a.dayOfWeek - b.dayOfWeek) + .forEach((item, index) => { + this.disabledSelectedTime(item.enabled, index); + alarmDays.push({ + enabled: item.enabled, + startsOn: utcTimestampToTimeOfDay(item.startsOn), + endsOn: utcTimestampToTimeOfDay(item.endsOn) + }); + }); + this.alarmScheduleForm.patchValue({ + staticValue: { + type: this.modelValue.staticValue.type, + timezone: this.modelValue.staticValue.timezone, + items: alarmDays, + }, + }, {emitEvent: false}); + } + break; + default: + this.alarmScheduleForm.patchValue(this.modelValue || undefined, {emitEvent: false}); + } + this.updateValidators(this.modelValue.staticValue.type); + } + this.updateModeValidators(this.dynamicMode); + } + } + + validate(control: FormGroup): ValidationErrors | null { + return this.alarmScheduleForm.valid ? null : { + alarmScheduler: { + valid: false + } + }; + } + + private updateModeValidators(mode: boolean) { + if (mode) { + this.alarmScheduleForm.get('staticValue').disable({emitEvent: false}); + this.alarmScheduleForm.get('dynamicValueArgument').enable({emitEvent: false}); + } else { + this.alarmScheduleForm.get('staticValue').enable({emitEvent: false}); + this.alarmScheduleForm.get('dynamicValueArgument').disable({emitEvent: false}); + this.updateValidators(this.alarmScheduleForm.get('staticValue.type').value); + } + } + + private updateValidators(type: AlarmRuleScheduleType){ + switch (type){ + case AlarmRuleScheduleType.ANY_TIME: + this.alarmScheduleForm.get('staticValue.timezone').disable({emitEvent: false}); + this.alarmScheduleForm.get('staticValue.daysOfWeek').disable({emitEvent: false}); + this.alarmScheduleForm.get('staticValue.startsOn').disable({emitEvent: false}); + this.alarmScheduleForm.get('staticValue.endsOn').disable({emitEvent: false}); + this.alarmScheduleForm.get('staticValue.items').disable({emitEvent: false}); + break; + case AlarmRuleScheduleType.SPECIFIC_TIME: + this.alarmScheduleForm.get('staticValue.timezone').enable({emitEvent: false}); + this.alarmScheduleForm.get('staticValue.daysOfWeek').enable({emitEvent: false}); + this.alarmScheduleForm.get('staticValue.startsOn').enable({emitEvent: false}); + this.alarmScheduleForm.get('staticValue.endsOn').enable({emitEvent: false}); + this.alarmScheduleForm.get('staticValue.items').disable({emitEvent: false}); + break; + case AlarmRuleScheduleType.CUSTOM: + this.alarmScheduleForm.get('staticValue.timezone').enable({emitEvent: false}); + this.alarmScheduleForm.get('staticValue.daysOfWeek').disable({emitEvent: false}); + this.alarmScheduleForm.get('staticValue.startsOn').disable({emitEvent: false}); + this.alarmScheduleForm.get('staticValue.endsOn').disable({emitEvent: false}); + this.alarmScheduleForm.get('staticValue.items').enable({emitEvent: false}); + break; + } + } + + private updateModel() { + const value = this.alarmScheduleForm.value as AlarmRuleSchedule; + if (!this.dynamicMode) { + if (isDefined(value.staticValue.startsOn) && value.staticValue.startsOn !== 0) { + value.staticValue.startsOn = timeOfDayToUTCTimestamp(value.staticValue.startsOn); + } + if (isDefined(value.staticValue.endsOn) && value.staticValue.endsOn !== 0) { + value.staticValue.endsOn = timeOfDayToUTCTimestamp(value.staticValue.endsOn); + } + if (isDefined(value.staticValue.items)){ + value.staticValue.items = this.alarmScheduleForm.getRawValue().staticValue.items as CustomTimeSchedulerItem[]; + value.staticValue.items = value.staticValue.items.map((item) => { + return { ...item, startsOn: timeOfDayToUTCTimestamp(item.startsOn), endsOn: timeOfDayToUTCTimestamp(item.endsOn)}; + }); + } + } + this.modelValue = value; + this.propagateChange(this.modelValue); + } + + + private defaultItemsScheduler(index: number): FormGroup { + return this.fb.group({ + enabled: [true], + dayOfWeek: [index + 1], + startsOn: [0, Validators.required], + endsOn: [0, Validators.required] + }); + } + + changeCustomScheduler($event: MatChipSelectionChange, index: number) { + const value = $event.selected; + this.disabledSelectedTime(value, index, true); + } + + private disabledSelectedTime(enable: boolean, index: number, emitEvent = false) { + if (enable) { + this.itemsSchedulerForm.at(index).get('startsOn').enable({emitEvent: false}); + this.itemsSchedulerForm.at(index).get('endsOn').enable({emitEvent}); + } else { + this.itemsSchedulerForm.at(index).get('startsOn').disable({emitEvent: false}); + this.itemsSchedulerForm.at(index).get('endsOn').disable({emitEvent}); + } + } + + getSchedulerRangeText(control: FormGroup | AbstractControl): string { + return getAlarmScheduleRangeText(control.get('startsOn').value, control.get('endsOn').value); + } + + get itemsSchedulerForm(): FormArray { + return this.alarmScheduleForm.get('staticValue.items') as FormArray; + } +} diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/create-cf-alarm-rules.component.html b/ui-ngx/src/app/modules/home/components/alarm-rules/create-cf-alarm-rules.component.html new file mode 100644 index 0000000000..820418a6b0 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/create-cf-alarm-rules.component.html @@ -0,0 +1,65 @@ + + +
    + @for (createAlarmRuleControl of createAlarmRulesFormArray().controls; track createAlarmRuleControl; let index = $index) { +
    +
    +
    +
    alarm.severity
    + + + + {{ alarmSeverityTranslationMap.get(alarmSeverityEnum[alarmSeverity]) | translate }} + + + +
    + + +
    + +
    + } +
    + + alarm-rule.add-create-alarm-rule-prompt + +
    +
    + +
    +
    diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/create-cf-alarm-rules.component.scss b/ui-ngx/src/app/modules/home/components/alarm-rules/create-cf-alarm-rules.component.scss new file mode 100644 index 0000000000..c00cf6af9c --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/create-cf-alarm-rules.component.scss @@ -0,0 +1,36 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host { + .create-alarm-rule { + border: 1px solid rgba(0, 0, 0, .12); + border-left-width: 4px; + border-radius: 4px; + padding: 8px; + min-width: 0; + } +} + +:host ::ng-deep { + .mat-mdc-form-field.severity { + .mat-mdc-form-field-infix { + width: 160px; + } + } + .button-icon { + color: rgba(0, 0, 0, 0.38); + min-width: 40px; + } +} diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/create-cf-alarm-rules.component.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/create-cf-alarm-rules.component.ts new file mode 100644 index 0000000000..1ffc52bb34 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/create-cf-alarm-rules.component.ts @@ -0,0 +1,187 @@ +/// +/// Copyright © 2016-2025 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, DestroyRef, forwardRef, Input } from '@angular/core'; +import { + AbstractControl, + ControlValueAccessor, + FormArray, + FormBuilder, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + UntypedFormArray, + UntypedFormControl, + Validator, + Validators +} from '@angular/forms'; +import { AlarmSeverity, alarmSeverityTranslations } from '@shared/models/alarm.models'; +import { AlarmRule } from "@shared/models/alarm-rule.models"; +import { CalculatedFieldArgument } from "@shared/models/calculated-field.models"; +import { AlarmSeverityNotificationColors } from "@shared/models/notification.models"; +import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; +import { coerceBoolean } from "@shared/decorators/coercion"; + +@Component({ + selector: 'tb-create-cf-alarm-rules', + templateUrl: './create-cf-alarm-rules.component.html', + styleUrls: ['./create-cf-alarm-rules.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => CreateCfAlarmRulesComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => CreateCfAlarmRulesComponent), + multi: true, + } + ] +}) +export class CreateCfAlarmRulesComponent implements ControlValueAccessor, Validator { + + @Input() + @coerceBoolean() + disabled: boolean; + + @Input() + arguments: Record; + + + alarmSeverities = Object.keys(AlarmSeverity); + alarmSeverityEnum = AlarmSeverity; + alarmSeverityTranslationMap = alarmSeverityTranslations; + + AlarmSeverityNotificationColors = AlarmSeverityNotificationColors; + + createAlarmRulesFormGroup = this.fb.group({ + createAlarmRules: this.fb.array<{severity: AlarmSeverity, alarmRule: AlarmRule}>([]) + }); + + private usedSeverities: AlarmSeverity[] = []; + + private propagateChange = (v: any) => { }; + + constructor(private fb: FormBuilder, + private destroyRef: DestroyRef) { + this.createAlarmRulesFormGroup.valueChanges.pipe( + takeUntilDestroyed(this.destroyRef) + ).subscribe(() => this.updateModel()); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + createAlarmRulesFormArray(): UntypedFormArray { + return this.createAlarmRulesFormGroup.get('createAlarmRules') as UntypedFormArray; + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (this.disabled) { + this.createAlarmRulesFormGroup.disable({emitEvent: false}); + } else { + this.createAlarmRulesFormGroup.enable({emitEvent: false}); + } + } + + writeValue(createAlarmRules: Record): void { + const createAlarmRulesControls: Array = []; + if (createAlarmRules) { + Object.keys(createAlarmRules).forEach((severity) => { + const createAlarmRule = createAlarmRules[severity]; + if (severity === 'empty') { + severity = null; + } + createAlarmRulesControls.push(this.fb.group({ + severity: [severity, Validators.required], + alarmRule: [createAlarmRule, Validators.required] + })); + }); + } + const formArray = this.createAlarmRulesFormGroup.get('createAlarmRules') as FormArray; + formArray.clear(); + createAlarmRulesControls.forEach(c => formArray.push(c)); + if (this.disabled) { + this.createAlarmRulesFormGroup.disable({emitEvent: false}); + } else { + this.createAlarmRulesFormGroup.enable({emitEvent: false}); + } + this.updateUsedSeverities(); + if (!this.disabled && !this.createAlarmRulesFormGroup.valid) { + this.updateModel(); + } + } + + public removeCreateAlarmRule(index: number) { + (this.createAlarmRulesFormGroup.get('createAlarmRules') as FormArray).removeAt(index); + } + + public addCreateAlarmRule() { + const createAlarmRulesArray = this.createAlarmRulesFormGroup.get('createAlarmRules') as FormArray; + createAlarmRulesArray.push(this.fb.group({ + severity: [this.getFirstUnusedSeverity(), Validators.required], + alarmRule: [null, Validators.required] + })); + this.createAlarmRulesFormGroup.updateValueAndValidity(); + if (!this.createAlarmRulesFormGroup.valid) { + this.updateModel(); + } + } + + private getFirstUnusedSeverity(): AlarmSeverity { + for (const severityKey of Object.keys(AlarmSeverity)) { + const severity = AlarmSeverity[severityKey]; + if (this.usedSeverities.indexOf(severity) === -1) { + return severity; + } + } + return null; + } + + public validate(c: UntypedFormControl) { + return (this.createAlarmRulesFormGroup.valid) ? null : { + createAlarmRules: { + valid: false, + }, + }; + } + + public isDisabledSeverity(severity: AlarmSeverity, index: number): boolean { + const usedIndex = this.usedSeverities.indexOf(severity); + return usedIndex > -1 && usedIndex !== index; + } + + private updateUsedSeverities() { + this.usedSeverities = []; + const value = this.createAlarmRulesFormGroup.get('createAlarmRules').value; + value.forEach((rule, index) => { + this.usedSeverities[index] = AlarmSeverity[rule.severity]; + }); + } + + private updateModel() { + const value = this.createAlarmRulesFormGroup.get('createAlarmRules').value; + const createAlarmRules = {} as Record; + value.forEach(v => createAlarmRules[v.severity] = v.alarmRule); + this.updateUsedSeverities(); + this.propagateChange(createAlarmRules); + } +} diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-complex-filter-predicate-dialog.component.html b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-complex-filter-predicate-dialog.component.html new file mode 100644 index 0000000000..75bce37a80 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-complex-filter-predicate-dialog.component.html @@ -0,0 +1,56 @@ + +
    + +

    filter.complex-filter

    + +
    +
    + + filter.operation.operation + + + {{complexOperationTranslations.get(complexOperationEnum[operation]) | translate}} + + + + + +
    +
    + + +
    +
    diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-complex-filter-predicate-dialog.component.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-complex-filter-predicate-dialog.component.ts new file mode 100644 index 0000000000..941ce326e1 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-complex-filter-predicate-dialog.component.ts @@ -0,0 +1,87 @@ +/// +/// Copyright © 2016-2025 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, Inject } from '@angular/core'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { FormBuilder, Validators } from '@angular/forms'; +import { Router } from '@angular/router'; +import { DialogComponent } from '@app/shared/components/dialog.component'; +import { + ComplexOperation, + complexOperationTranslationMap, + EntityKeyValueType, + FilterPredicateType +} from '@shared/models/query/query.models'; +import { AlarmRuleFilterPredicate, ComplexAlarmRuleFilterPredicate } from "@shared/models/alarm-rule.models"; +import { CalculatedFieldArgument } from "@shared/models/calculated-field.models"; + +export interface AlarmRuleComplexFilterPredicateDialogData { + complexPredicate: ComplexAlarmRuleFilterPredicate; + isAdd: boolean; + valueType: EntityKeyValueType; + arguments: Record; + argumentInUse: string; +} + +@Component({ + selector: 'tb-alarm-rule-complex-filter-predicate-dialog', + templateUrl: './alarm-rule-complex-filter-predicate-dialog.component.html', + providers: [], + styleUrls: [] +}) + +export class AlarmRuleComplexFilterPredicateDialogComponent extends + DialogComponent { + + complexFilterFormGroup = this.fb.group( + { + operation: [ComplexOperation.AND, [Validators.required]], + predicates: this.fb.control(null, Validators.required) + } + ); + + complexOperations = Object.keys(ComplexOperation); + complexOperationEnum = ComplexOperation; + complexOperationTranslations = complexOperationTranslationMap; + + isAdd: boolean; + + arguments = this.data.arguments; + + constructor(protected store: Store, + protected router: Router, + @Inject(MAT_DIALOG_DATA) public data: AlarmRuleComplexFilterPredicateDialogData, + public dialogRef: MatDialogRef, + private fb: FormBuilder) { + super(store, router, dialogRef); + + this.isAdd = this.data.isAdd; + + this.complexFilterFormGroup.patchValue(this.data.complexPredicate, {emitEvent: false}); + } + + cancel(): void { + this.dialogRef.close(null); + } + + save(): void { + const predicate = this.complexFilterFormGroup.value as ComplexAlarmRuleFilterPredicate; + predicate.type = FilterPredicateType.COMPLEX; + this.dialogRef.close(predicate); + } +} diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-dialog.component.html b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-dialog.component.html new file mode 100644 index 0000000000..abc1dd463d --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-dialog.component.html @@ -0,0 +1,99 @@ + +
    + +

    {{(data.isAdd ? 'alarm-rule.add-filter' : 'alarm-rule.edit-filter') | translate}}

    + +
    +
    +
    +
    +
    {{ 'alarm-rule.general' | translate }}
    +
    + + alarm-rule.value-argument + + @for (argument of argumentsList; track argument) { + {{ argument }} + } + + @if (filterFormGroup.get('argument').touched && filterFormGroup.get('argument').hasError('required')) { + + warning + + } + + + filter.value-type.value-type + + + + {{ entityKeyValueTypes.get(filterFormGroup.get('valueType').value)?.name | translate }} + + + + {{ entityKeyValueTypes.get(entityKeyValueTypeEnum[valueType]).name | translate }} + + + + {{ 'filter.value-type-required' | translate }} + + +
    +
    + +
    +
    +
    {{ 'alarm-rule.filter' | translate }}
    + + {{ complexOperationTranslationMap.get(ComplexOperation.AND) | translate }} + {{ complexOperationTranslationMap.get(ComplexOperation.OR) | translate }} + +
    + + +
    +
    +
    +
    + + +
    +
    diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-dialog.component.scss b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-dialog.component.scss new file mode 100644 index 0000000000..0ec7194f02 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-dialog.component.scss @@ -0,0 +1,28 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host ::ng-deep { + .mat-mdc-form-field.tb-value-type { + mat-select-trigger { + .mat-icon { + vertical-align: middle; + margin-right: 8px; + svg { + vertical-align: initial; + } + } + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-dialog.component.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-dialog.component.ts new file mode 100644 index 0000000000..6961524bdd --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-dialog.component.ts @@ -0,0 +1,120 @@ + /// +/// Copyright © 2016-2025 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + + import { Component, DestroyRef, Inject } from '@angular/core'; + import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; + import { Store } from '@ngrx/store'; + import { AppState } from '@core/core.state'; + import { FormBuilder, FormGroup, Validators } from '@angular/forms'; + import { Router } from '@angular/router'; + import { DialogComponent } from '@app/shared/components/dialog.component'; + import { + ComplexOperation, + complexOperationTranslationMap, + EntityKeyValueType, + entityKeyValueTypesMap + } from '@shared/models/query/query.models'; + import { DialogService } from '@core/services/dialog.service'; + import { TranslateService } from '@ngx-translate/core'; + import { AlarmRuleFilter, AlarmRuleFilterPredicate } from "@shared/models/alarm-rule.models"; + import { CalculatedFieldArgument } from "@shared/models/calculated-field.models"; + import { FormControlsFrom } from "@shared/models/tenant.model"; + import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; + + export interface AlarmRuleFilterDialogData { + filter: AlarmRuleFilter; + isAdd: boolean; + arguments: Record; + usedArguments: Array; +} + +@Component({ + selector: 'tb-alarm-rule-filter-dialog', + templateUrl: './alarm-rule-filter-dialog.component.html', + providers: [], + styleUrls: ['./alarm-rule-filter-dialog.component.scss'] +}) +export class AlarmRuleFilterDialogComponent extends DialogComponent { + + filterFormGroup: FormGroup>; + + entityKeyValueTypesKeys = Object.keys(EntityKeyValueType); + + entityKeyValueTypeEnum = EntityKeyValueType; + + entityKeyValueTypes = entityKeyValueTypesMap; + + complexOperationTranslationMap = complexOperationTranslationMap; + + ComplexOperation = ComplexOperation; + + arguments = this.data.arguments; + argumentsList: Array; + + constructor(protected store: Store, + protected router: Router, + @Inject(MAT_DIALOG_DATA) public data: AlarmRuleFilterDialogData, + public dialogRef: MatDialogRef, + private dialogs: DialogService, + private translate: TranslateService, + private fb: FormBuilder, + private destroyRef: DestroyRef) { + super(store, router, dialogRef); + + this.argumentsList = this.arguments ? Object.keys(this.arguments) : []; + + this.filterFormGroup = this.fb.group( + { + argument: [this.data.filter.argument, [Validators.required]], + valueType: [this.data.filter.valueType ?? EntityKeyValueType.STRING, [Validators.required]], + predicates: [this.data.filter.predicates, [Validators.required]], + operation: [this.data.filter.operation ?? ComplexOperation.AND] + } + ); + this.filterFormGroup.get('valueType').valueChanges.pipe( + takeUntilDestroyed(this.destroyRef) + ).subscribe((valueType: EntityKeyValueType) => { + const prevValueType: EntityKeyValueType = this.filterFormGroup.value.valueType; + const predicates: AlarmRuleFilterPredicate[] = this.filterFormGroup.get('predicates').value; + if (prevValueType && prevValueType !== valueType) { + if (predicates && predicates.length) { + this.dialogs.confirm(this.translate.instant('filter.key-value-type-change-title'), + this.translate.instant('filter.key-value-type-change-message')).subscribe( + (result) => { + if (result) { + this.filterFormGroup.get('predicates').setValue([]); + } else { + this.filterFormGroup.get('valueType').setValue(prevValueType, {emitEvent: false}); + } + } + ); + } + } + }); + } + + argumentInUse(argument: string): boolean { + return this.data.usedArguments.includes(argument); + } + + cancel(): void { + this.dialogRef.close(null); + } + + save(): void { + this.dialogRef.close(this.filterFormGroup.value as AlarmRuleFilter); + } +} diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-list.component.html b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-list.component.html new file mode 100644 index 0000000000..3f588a3d70 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-list.component.html @@ -0,0 +1,79 @@ + +
    +
    + +
    + + +   +
    +
    + +
    + @for (filterControl of filtersFormArray.controls; track filterControl; let index = $index) { +
    +
    + @if ($index) { +
    + {{ complexOperationTranslationMap.get(operation) | translate }} +
    + } +
    +
    +
    +
    {{ filterControl.value?.argument }}
    +
    {{ FilterPredicateTypeTranslationMap.get(filterControl.value?.predicates[0]?.type) | translate }}
    + + +
    +
    + @if (index) { + + } +
    + } + + filter.no-key-filters + +
    +
    + + diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-list.component.scss b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-list.component.scss new file mode 100644 index 0000000000..8e74f373f5 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-list.component.scss @@ -0,0 +1,50 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +:host { + .filter-title { + padding: 12px 0; + font-size: 14px; + font-weight: 500; + } + .filter-list { + overflow: auto; + max-height: 300px; + .no-data-found { + height: 50px; + } + + &-divider { + border-top: 1px solid rgba(0, 0, 0, 0.12); + } + } + .filters-operation { + display: flex; + justify-content: center; + margin-top: -14px; + &-container { + background-color: white; + } + &-label { + font-weight: 500; + color: #00695C; + padding: 0 8px; + border-radius: 4px; + border: 1px solid rgba(#00695C, 0.32); + background-color: rgba(#00695C, 0.04); + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-list.component.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-list.component.ts new file mode 100644 index 0000000000..11e2728659 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-list.component.ts @@ -0,0 +1,180 @@ +/// +/// Copyright © 2016-2025 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, DestroyRef, forwardRef, Input } from '@angular/core'; +import { + AbstractControl, + ControlValueAccessor, + FormArray, + FormBuilder, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + ValidationErrors, + Validator, + Validators +} from '@angular/forms'; +import { Observable } from 'rxjs'; +import { + ComplexOperation, + complexOperationTranslationMap, + EntityKeyValueType +} from '@shared/models/query/query.models'; +import { MatDialog } from '@angular/material/dialog'; +import { deepClone } from '@core/utils'; +import { + AlarmRuleFilterDialogComponent, + AlarmRuleFilterDialogData +} from "@home/components/alarm-rules/filter/alarm-rule-filter-dialog.component"; +import { AlarmRuleFilter, FilterPredicateTypeTranslationMap } from "@shared/models/alarm-rule.models"; +import { CalculatedFieldArgument } from "@shared/models/calculated-field.models"; +import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; + +@Component({ + selector: 'tb-alarm-rule-filter-list', + templateUrl: './alarm-rule-filter-list.component.html', + styleUrls: ['./alarm-rule-filter-list.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => AlarmRuleFilterListComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => AlarmRuleFilterListComponent), + multi: true + } + ] +}) +export class AlarmRuleFilterListComponent implements ControlValueAccessor, Validator { + + @Input() + arguments: Record; + + @Input() + operation: ComplexOperation = ComplexOperation.AND; + + filterListFormGroup = this.fb.group({ + filters: this.fb.array([]) + }); + + complexOperationTranslationMap = complexOperationTranslationMap; + FilterPredicateTypeTranslationMap = FilterPredicateTypeTranslationMap + + private propagateChange = (v: any) => { }; + + constructor(private fb: FormBuilder, + private dialog: MatDialog, + private destroyRef: DestroyRef) { + this.filterListFormGroup.valueChanges.pipe( + takeUntilDestroyed(this.destroyRef) + ).subscribe(() => this.updateModel()); + } + + + get filtersFormArray(): FormArray { + return this.filterListFormGroup.get('filters') as FormArray; + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + validate(): ValidationErrors | null { + return this.filterListFormGroup.valid && this.filterListFormGroup.get('filters').value?.length ? null : { + filterList: {valid: false} + }; + } + + setDisabledState(isDisabled: boolean): void { + if (isDisabled) { + this.filterListFormGroup.disable({emitEvent: false}); + } else { + this.filterListFormGroup.enable({emitEvent: false}); + } + } + + writeValue(filters: Array): void { + const keyFilterControls: Array = []; + if (filters) { + for (const filter of filters) { + keyFilterControls.push(this.fb.control(filter, [Validators.required])); + } + } + this.filtersFormArray.clear(); + keyFilterControls.forEach(c => this.filtersFormArray.push(c)); + } + + public removeFilter(index: number) { + (this.filterListFormGroup.get('filters') as FormArray).removeAt(index); + } + + public addFilter() { + const filtersFormArray = this.filterListFormGroup.get('filters') as FormArray; + this.openFilterDialog(null).subscribe(result => { + if (result) { + filtersFormArray.push(this.fb.control(result, [Validators.required])); + } + }); + } + + public editFilter(index: number) { + const filter: AlarmRuleFilter = + (this.filterListFormGroup.get('filters') as FormArray).at(index).value; + this.openFilterDialog(filter).subscribe(result => { + if (result) { + (this.filterListFormGroup.get('filters') as FormArray).at(index).patchValue(result); + } + }); + } + + private openFilterDialog(filter?: AlarmRuleFilter): Observable { + const isAdd = !filter; + if (isAdd) { + filter = { + argument: null, + valueType: EntityKeyValueType.STRING, + operation: ComplexOperation.AND, + predicates: [] + }; + } + return this.dialog.open(AlarmRuleFilterDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + filter: filter ? deepClone(filter) : null, + isAdd, + arguments: this.arguments, + usedArguments: this.getUsedArguments + } + }).afterClosed(); + } + + get getUsedArguments(): Array { + const filters = this.filterListFormGroup.get('filters').value as Array; + return filters.length ? filters.map((filter: AlarmRuleFilter) => filter.argument) : []; + } + + private updateModel() { + const filters = this.filterListFormGroup.value.filters as Array; + this.propagateChange(filters); + } + +} diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-list.component.html b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-list.component.html new file mode 100644 index 0000000000..b7450262f6 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-list.component.html @@ -0,0 +1,86 @@ + +
    +
    +
    +
    +
    + + + +
    +   +
    +
    + +
    + @for (predicateControl of predicatesFormArray.controls; track predicateControl; let index = $index) { +
    + @if (index) { +
    + {{ complexOperationTranslations.get(operation) | translate }} +
    + } +
    +
    + + + +
    +
    +
    + } + + filter.no-filters + +
    +
    +
    + + +
    +
    diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-list.component.scss b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-list.component.scss new file mode 100644 index 0000000000..054d0e5281 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-list.component.scss @@ -0,0 +1,50 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host { + .filter-title { + padding: 12px 8px; + font-size: 14px; + font-weight: 500; + } + .predicate-list { + .no-data-found { + height: 50px; + } + } + + .key-filter-list-divider { + border-top: 1px solid rgba(0, 0, 0, 0.12); + } + .filters-operation { + display: flex; + justify-content: center; + margin-top: -14px; + &-container { + position: absolute; + top: -12px; + left: 10px; + background-color: white; + } + &-label { + font-weight: 500; + color: #00695C; + padding: 0 8px; + border-radius: 4px; + border: 1px solid rgba(#00695C, 0.32); + background-color: rgba(#00695C, 0.04); + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-list.component.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-list.component.ts new file mode 100644 index 0000000000..ecb52944b2 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-list.component.ts @@ -0,0 +1,210 @@ +/// +/// Copyright © 2016-2025 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, DestroyRef, forwardRef, Input } from '@angular/core'; +import { + AbstractControl, + ControlValueAccessor, + FormArray, + FormBuilder, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + ValidationErrors, + Validator, + Validators +} from '@angular/forms'; +import { Observable, of } from 'rxjs'; +import { + BooleanOperation, + ComplexOperation, + complexOperationTranslationMap, + EntityKeyValueType, + entityKeyValueTypeToFilterPredicateType, + FilterPredicateType, + NumericOperation, + StringOperation +} from '@shared/models/query/query.models'; +import { MatDialog } from '@angular/material/dialog'; +import { map } from 'rxjs/operators'; +import { + AlarmRuleComplexFilterPredicateDialogComponent, + AlarmRuleComplexFilterPredicateDialogData +} from "@home/components/alarm-rules/filter/alarm-rule-complex-filter-predicate-dialog.component"; +import { + AlarmRuleFilterPredicate, + AlarmRulePredicateInfo, + ComplexAlarmRuleFilterPredicate +} from "@shared/models/alarm-rule.models"; +import { CalculatedFieldArgument } from "@shared/models/calculated-field.models"; +import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; + +@Component({ + selector: 'tb-alarm-rule-filter-predicate-list', + templateUrl: './alarm-rule-filter-predicate-list.component.html', + styleUrls: ['./alarm-rule-filter-predicate-list.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => AlarmRuleFilterPredicateListComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => AlarmRuleFilterPredicateListComponent), + multi: true + } + ] +}) +export class AlarmRuleFilterPredicateListComponent implements ControlValueAccessor, Validator { + + @Input() disabled: boolean; + + @Input() valueType: EntityKeyValueType; + + @Input() operation: ComplexOperation = ComplexOperation.AND; + + @Input() arguments: Record; + + @Input() argumentInUse: string; + + filterListFormGroup = this.fb.group({ + predicates: this.fb.array([]) + }); + + valueTypeEnum = EntityKeyValueType; + + complexOperationTranslations = complexOperationTranslationMap; + + private propagateChange= (v: any) => { }; + + constructor(private fb: FormBuilder, + private dialog: MatDialog, + private destroyRef: DestroyRef) { + this.filterListFormGroup.valueChanges.pipe( + takeUntilDestroyed(this.destroyRef) + ).subscribe(() => this.updateModel()); + } + + get predicatesFormArray(): FormArray { + return this.filterListFormGroup.get('predicates') as FormArray; + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (this.disabled) { + this.filterListFormGroup.disable({emitEvent: false}); + } else { + this.filterListFormGroup.enable({emitEvent: false}); + } + } + + validate(control: AbstractControl): ValidationErrors | null { + return this.filterListFormGroup.valid ? null : { + filterList: {valid: false} + }; + } + + writeValue(predicates: Array): void { + const predicateControls: Array = []; + if (predicates) { + for (const predicate of predicates) { + predicateControls.push(this.fb.control(predicate, [Validators.required])); + } + } + this.predicatesFormArray.clear(); + predicateControls.forEach(predicate => this.predicatesFormArray.push(predicate)); + } + + public removePredicate(index: number) { + this.predicatesFormArray.removeAt(index); + } + + public addPredicate(complex: boolean) { + const predicatesFormArray = this.filterListFormGroup.get('predicates') as FormArray; + const predicate = this.createDefaultFilterPredicate(this.valueType, complex); + let observable: Observable; + if (complex) { + observable = this.openComplexFilterDialog(predicate as ComplexAlarmRuleFilterPredicate); + } else { + observable = of(predicate); + } + observable.subscribe((result) => { + if (result) { + predicatesFormArray.push(this.fb.control(result, [Validators.required])); + } + }); + } + + private createDefaultFilterPredicate(valueType: EntityKeyValueType, complex: boolean): AlarmRuleFilterPredicate { + const predicate = { + type: complex ? FilterPredicateType.COMPLEX : entityKeyValueTypeToFilterPredicateType(valueType) + } as AlarmRuleFilterPredicate; + switch (predicate.type) { + case FilterPredicateType.STRING: + predicate.operation = StringOperation.STARTS_WITH; + predicate.value = { + staticValue: '' + }; + predicate.ignoreCase = false; + break; + case FilterPredicateType.NUMERIC: + predicate.operation = NumericOperation.EQUAL; + predicate.value = { + staticValue: valueType === EntityKeyValueType.DATE_TIME ? Date.now() : 0 + }; + break; + case FilterPredicateType.BOOLEAN: + predicate.operation = BooleanOperation.EQUAL; + predicate.value = { + staticValue: false + }; + break; + case FilterPredicateType.COMPLEX: + predicate.operation = ComplexOperation.AND; + predicate.predicates = []; + break; + } + return predicate; + } + + private openComplexFilterDialog(predicate: ComplexAlarmRuleFilterPredicate): Observable { + return this.dialog.open(AlarmRuleComplexFilterPredicateDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + complexPredicate: predicate as ComplexAlarmRuleFilterPredicate, + valueType: this.valueType, + isAdd: true, + arguments: this.arguments, + argumentInUse: this.argumentInUse + } + }).afterClosed().pipe( + map(result => result) + ); + } + + private updateModel() { + this.propagateChange(this.filterListFormGroup.get('predicates').value); + } +} diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-value.component.html b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-value.component.html new file mode 100644 index 0000000000..10294e851a --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-value.component.html @@ -0,0 +1,73 @@ + +
    +
    + + + {{'alarm-rule.static' | translate}} + {{'alarm-rule.dynamic' | translate}} + + + @if (!dynamicModeControl.value) { + @switch (valueType) { + @case (valueTypeEnum.STRING) { + + + + } + @case (valueTypeEnum.NUMERIC) { + + + + } + @case (valueTypeEnum.BOOLEAN) { + + {{ (filterPredicateValueFormGroup.get('staticValue').value ? 'value.true' : 'value.false') | translate }} + + } + @case (valueTypeEnum.DATE_TIME) { + + } + } + } @else { + + + @for (argument of argumentsList; track argument) { + {{ argument }} + } + + @if (filterPredicateValueFormGroup.get('dynamicValueArgument').touched && filterPredicateValueFormGroup.get('dynamicValueArgument').hasError('required')) { + + warning + + } + + } +
    +
    diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-value.component.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-value.component.ts new file mode 100644 index 0000000000..036083c826 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-value.component.ts @@ -0,0 +1,155 @@ +/// +/// Copyright © 2016-2025 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, DestroyRef, forwardRef, Input, OnInit } from '@angular/core'; +import { + ControlValueAccessor, + FormBuilder, + FormGroup, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + ValidationErrors, + Validator, + ValidatorFn, + Validators +} from '@angular/forms'; +import { EntityKeyValueType } from '@shared/models/query/query.models'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { CalculatedFieldArgument } from "@shared/models/calculated-field.models"; +import { AlarmRuleValue } from "@shared/models/alarm-rule.models"; +import { FormControlsFrom } from "@shared/models/tenant.model"; + +@Component({ + selector: 'tb-alarm-rule-filter-predicate-value', + templateUrl: './alarm-rule-filter-predicate-value.component.html', + styleUrls: [], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => AlarmRuleFilterPredicateValueComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => AlarmRuleFilterPredicateValueComponent), + multi: true + } + ] +}) +export class AlarmRuleFilterPredicateValueComponent implements ControlValueAccessor, Validator, OnInit { + + @Input() + arguments: Record; + + @Input() + valueType: EntityKeyValueType; + + @Input() + argumentInUse: string; + + valueTypeEnum = EntityKeyValueType; + + filterPredicateValueFormGroup: FormGroup>>; + + dynamicModeControl = this.fb.control(false); + + argumentsList: Array; + + private propagateChange= (v: any) => { }; + + constructor(private fb: FormBuilder, + private destroyRef: DestroyRef) { + } + + ngOnInit(): void { + this.argumentsList = this.arguments ? Object.keys(this.arguments): []; + let defaultValue: string | number | boolean; + let defaultValueValidators: ValidatorFn[]; + switch (this.valueType) { + case EntityKeyValueType.STRING: + defaultValue = ''; + defaultValueValidators = []; + break; + case EntityKeyValueType.NUMERIC: + defaultValue = 0; + defaultValueValidators = [Validators.required]; + break; + case EntityKeyValueType.BOOLEAN: + defaultValue = false; + defaultValueValidators = []; + break; + case EntityKeyValueType.DATE_TIME: + defaultValue = Date.now(); + defaultValueValidators = [Validators.required]; + break; + } + this.filterPredicateValueFormGroup = this.fb.group({ + staticValue: [defaultValue, defaultValueValidators], + dynamicValueArgument: ['', Validators.required] + }); + this.filterPredicateValueFormGroup.valueChanges.pipe( + takeUntilDestroyed(this.destroyRef) + ).subscribe(() => { + this.updateModel(); + }); + this.dynamicModeControl.valueChanges.pipe( + takeUntilDestroyed(this.destroyRef) + ).subscribe(value => this.updateValueModeValidators(value)); + } + + setDisabledState(isDisabled: boolean): void { + if (isDisabled) { + this.filterPredicateValueFormGroup.disable({emitEvent: false}); + this.dynamicModeControl.disable({emitEvent: false}); + } else { + this.filterPredicateValueFormGroup.enable({emitEvent: false}); + this.dynamicModeControl.enable({emitEvent: false}); + this.updateValueModeValidators(this.dynamicModeControl.value); + } + } + + private updateValueModeValidators(isDynamicMode: boolean): void { + if (isDynamicMode) { + this.filterPredicateValueFormGroup.get('staticValue').disable({emitEvent: false}); + this.filterPredicateValueFormGroup.get('dynamicValueArgument').enable(); + } else { + this.filterPredicateValueFormGroup.get('dynamicValueArgument').disable({emitEvent: false}); + this.filterPredicateValueFormGroup.get('staticValue').enable(); + } + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + validate(): ValidationErrors | null { + return this.filterPredicateValueFormGroup.valid ? null : { + filterPredicateValue: {valid: false} + }; + } + + writeValue(predicateValue: AlarmRuleValue): void { + this.filterPredicateValueFormGroup.patchValue(predicateValue, {emitEvent: false}); + this.dynamicModeControl.patchValue(!!predicateValue.dynamicValueArgument?.length, {emitEvent: false}); + } + + private updateModel() { + this.propagateChange(this.filterPredicateValueFormGroup.value); + } +} diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate.component.html b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate.component.html new file mode 100644 index 0000000000..d73c222f9d --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate.component.html @@ -0,0 +1,80 @@ + +
    +
    + @switch (type) { + @case (filterPredicateType.STRING) { +
    + + + + {{stringOperationTranslationMap.get(stringOperation[operation]) | translate}} + + + + + {{ 'alarm-rule.ignore-case' | translate }} + +
    + } + @case (filterPredicateType.NUMERIC) { +
    + + + + {{numericOperationTranslations.get(numericOperationEnum[operation]) | translate}} + + + +
    + } + @case (filterPredicateType.BOOLEAN) { +
    + + + + {{booleanOperationTranslations.get(booleanOperationEnum[operation]) | translate}} + + + +
    + } + @case (filterPredicateType.COMPLEX) { +
    + +
    + } + } + @if (type !== filterPredicateType.COMPLEX) { + + + } +
    +
    diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate.component.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate.component.ts new file mode 100644 index 0000000000..b508be0c79 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate.component.ts @@ -0,0 +1,162 @@ +/// +/// Copyright © 2016-2025 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { booleanAttribute, Component, DestroyRef, forwardRef, Input } from '@angular/core'; +import { + ControlValueAccessor, + FormBuilder, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + ValidationErrors, + Validator +} from '@angular/forms'; +import { + BooleanOperation, + booleanOperationTranslationMap, + EntityKeyValueType, + FilterPredicateType, + NumericOperation, + numericOperationTranslationMap, + StringOperation, + stringOperationTranslationMap +} from '@shared/models/query/query.models'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { AlarmRuleFilterPredicate, ComplexAlarmRuleFilterPredicate } from "@shared/models/alarm-rule.models"; +import { MatDialog } from "@angular/material/dialog"; +import { + AlarmRuleComplexFilterPredicateDialogComponent, + AlarmRuleComplexFilterPredicateDialogData +} from "@home/components/alarm-rules/filter/alarm-rule-complex-filter-predicate-dialog.component"; +import { CalculatedFieldArgument } from "@shared/models/calculated-field.models"; + +@Component({ + selector: 'tb-alarm-rule-filter-predicate', + templateUrl: './alarm-rule-filter-predicate.component.html', + styleUrls: [], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => AlarmRuleFilterPredicateComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => AlarmRuleFilterPredicateComponent), + multi: true + } + ] +}) +export class AlarmRuleFilterPredicateComponent implements ControlValueAccessor, Validator { + + @Input({ transform: booleanAttribute }) + disabled: boolean; + + @Input() + valueType: EntityKeyValueType; + + @Input() + arguments: Record; + + @Input() + argumentInUse: string; + + filterPredicateFormGroup = this.fb.group({ + operation: [], + ignoreCase: false, + predicates: [], + value: [] + }); + + type: FilterPredicateType; + + filterPredicateType = FilterPredicateType; + + stringOperations = Object.keys(StringOperation); + stringOperation = StringOperation; + stringOperationTranslationMap = stringOperationTranslationMap; + + numericOperations = Object.keys(NumericOperation); + numericOperationEnum = NumericOperation; + numericOperationTranslations = numericOperationTranslationMap; + + booleanOperations = Object.keys(BooleanOperation); + booleanOperationEnum = BooleanOperation; + booleanOperationTranslations = booleanOperationTranslationMap; + + private propagateChange= (v: any) => { }; + + constructor(private fb: FormBuilder, + private dialog: MatDialog, + private destroyRef: DestroyRef) { + this.filterPredicateFormGroup.valueChanges.pipe( + takeUntilDestroyed(this.destroyRef) + ).subscribe(() => { + this.updateModel(); + }); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + validate(): ValidationErrors | null { + return this.filterPredicateFormGroup.valid ? null : { + filterPredicate: {valid: false} + }; + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (isDisabled) { + this.filterPredicateFormGroup.disable({emitEvent: false}); + } else { + this.filterPredicateFormGroup.enable({emitEvent: false}); + } + } + + writeValue(predicate: AlarmRuleFilterPredicate): void { + this.type = predicate.type; + this.filterPredicateFormGroup.patchValue(predicate, {emitEvent: false}); + } + + private updateModel() { + this.propagateChange({type: this.type, ...this.filterPredicateFormGroup.value}); + } + + public openComplexFilterDialog() { + this.dialog.open(AlarmRuleComplexFilterPredicateDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + complexPredicate: this.filterPredicateFormGroup.value as ComplexAlarmRuleFilterPredicate, + valueType: this.valueType, + isAdd: false, + arguments: this.arguments, + argumentInUse: this.argumentInUse, + } + }).afterClosed().subscribe( + (result) => { + if (result) { + this.filterPredicateFormGroup.patchValue(result); + } + } + ); + } +} diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-text.component.html b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-text.component.html new file mode 100644 index 0000000000..cb390b6c0b --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-text.component.html @@ -0,0 +1,23 @@ + +
    +
    diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-text.component.scss b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-text.component.scss new file mode 100644 index 0000000000..a059bb4564 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-text.component.scss @@ -0,0 +1,85 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host { + text-overflow: ellipsis; + overflow: hidden; + .tb-filter-text { + overflow-y: auto; + text-align: start; + &.required { + color: #f44336; + padding: 0; + } + &.nowrap { + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; + } + } +} + +:host ::ng-deep { + .tb-filter-text { + line-height: 1.8em; + span { + display: inline-block; + vertical-align: middle; + line-height: 1.4em; + } + .tb-filter-predicate { + padding-right: 4px; + padding-left: 4px; + } + .tb-filter-entity-key, .tb-filter-value, .tb-filter-dynamic-source { + font-weight: bold; + border: 1px groove rgba(0, 0, 0, .25); + border-radius: 4px; + padding-left: 4px; + padding-right: 4px; + } + .tb-filter-entity-key, .tb-filter-value { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + max-width: 150px; + } + .tb-filter-dynamic-source { + } + .tb-filter-entity-key { + color: #305680; + } + .tb-filter-value { + color: #ff5722; + } + .tb-filter-simple-operation { + font-size: 0.9em; + } + .tb-filter-complex-operation { + font-weight: 400; + font-style: italic; + } + .tb-filter-dynamic-value { + .tb-filter-dynamic-source, .tb-filter-value { + color: #0c959c; + } + } + .tb-filter-bracket { + .tb-left-bracket, .tb-right-bracket { + font-size: 1.2em; + } + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-text.component.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-text.component.ts new file mode 100644 index 0000000000..498b2a0a81 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-text.component.ts @@ -0,0 +1,192 @@ +/// +/// Copyright © 2016-2025 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, Input } from '@angular/core'; +import { + booleanOperationTranslationMap, + ComplexOperation, + complexOperationTranslationMap, + EntityKeyValueType, + FilterPredicateType, + numericOperationTranslationMap, + stringOperationTranslationMap +} from '@shared/models/query/query.models'; +import { TranslateService } from '@ngx-translate/core'; +import { DatePipe } from '@angular/common'; +import { + AlarmRuleExpression, + AlarmRuleExpressionType, + AlarmRuleFilter, + AlarmRuleFilterPredicate, + ComplexAlarmRuleFilterPredicate +} from "@shared/models/alarm-rule.models"; +import { CalculatedFieldArgument } from "@shared/models/calculated-field.models"; +import { coerceBoolean } from "@shared/decorators/coercion"; + +@Component({ + selector: 'tb-alarm-rule-filter-text', + templateUrl: './alarm-rule-filter-text.component.html', + styleUrls: ['./alarm-rule-filter-text.component.scss'], + providers: [] +}) +export class AlarmRuleFilterTextComponent { + + @Input() + @coerceBoolean() + required = false; + + @Input() + noFilterText = this.translate.instant('filter.no-filter-text'); + + @Input() + addFilterPrompt = this.translate.instant('filter.add-filter-prompt'); + + @Input() + @coerceBoolean() + nowrap = false; + + @Input() + arguments: Record; + + private alarmRuleExpressionValue: AlarmRuleExpression; + get alarmRuleExpression(): AlarmRuleExpression { + return this.alarmRuleExpressionValue; + } + + @Input() + set alarmRuleExpression(value: AlarmRuleExpression) { + if (value !== this.alarmRuleExpressionValue) { + this.alarmRuleExpressionValue = value; + this.updateFilterText(value); + } + }; + + private specTextValue: string; + get specText(): string { + return this.specTextValue; + } + @Input() + set specText(value: string) { + if (value !== this.specTextValue) { + this.specTextValue = value; + this.updateFilterText(this.alarmRuleExpression); + } + } + + isRequired = false; + + public filterText: string; + + constructor(private translate: TranslateService, + private datePipe: DatePipe) { + } + + private updateFilterText(value: AlarmRuleExpression) { + this.isRequired = false; + if (value && (value.expression || value.filters)) { + if (value.type === AlarmRuleExpressionType.SIMPLE) { + this.filterText = this.keyFiltersToText(this.translate, this.datePipe, value.filters, value.operation); + } else { + this.filterText = 'function expression(ctx, ' + (this.arguments ? Object.keys(this.arguments).join(', ') : '' ) + ')'; + } + if (this.specText?.length) { + this.filterText = this.specText + ': ' + this.filterText; + } + } else { + if (this.required) { + this.filterText = this.addFilterPrompt; + this.isRequired = true; + } else { + this.filterText = this.noFilterText; + } + } + } + + private keyFiltersToText(translate: TranslateService, datePipe: DatePipe, keyFilters: Array, operation: ComplexOperation): string { + const filtersText = keyFilters.map(keyFilter => + this.filterPredicateToText(translate, datePipe, keyFilter, keyFilter.predicates)); + let result: string; + if (filtersText.length > 1) { + const operationText = translate.instant(complexOperationTranslationMap.get(operation)); + result = filtersText.join(' ' + operationText + ' '); + } else { + result = filtersText[0]; + } + return result; + } + + private filterPredicateToText(translate: TranslateService, + datePipe: DatePipe, + keyFilter: AlarmRuleFilter, + keyFilterPredicates: AlarmRuleFilterPredicate[], + complexOperation?: ComplexOperation): string { + const key = keyFilter.argument; + const filterOperation: ComplexOperation = complexOperation ? complexOperation : (keyFilter.operation ?? ComplexOperation.AND); + + const predicates = keyFilterPredicates.map((keyFilterPredicate: AlarmRuleFilterPredicate) => { + if (keyFilterPredicate.type === FilterPredicateType.COMPLEX) { + const complexPredicate = keyFilterPredicate as ComplexAlarmRuleFilterPredicate; + const complexOperation = complexPredicate.operation ?? ComplexOperation.AND; + return this.filterPredicateToText(translate, datePipe, keyFilter, complexPredicate.predicates, complexOperation); + } else { + let operation: string; + let value: string; + const val = keyFilterPredicate.value; + const dynamicValue = val?.dynamicValueArgument?.length; + if (dynamicValue) { + value = '' + val?.dynamicValueArgument + ''; + } + switch (keyFilterPredicate.type) { + case FilterPredicateType.STRING: + operation = translate.instant(stringOperationTranslationMap.get(keyFilterPredicate.operation)); + if (keyFilterPredicate.ignoreCase) { + operation += ' ' + translate.instant('filter.ignore-case'); + } + if (!dynamicValue) { + value = `'${keyFilterPredicate.value.staticValue}'`; + } + break; + case FilterPredicateType.NUMERIC: + operation = translate.instant(numericOperationTranslationMap.get(keyFilterPredicate.operation)); + if (!dynamicValue) { + if (keyFilter.valueType === EntityKeyValueType.DATE_TIME) { + value = datePipe.transform(keyFilterPredicate.value.staticValue, 'yyyy-MM-dd HH:mm'); + } else { + value = keyFilterPredicate.value.staticValue + ''; + } + } + break; + case FilterPredicateType.BOOLEAN: + operation = translate.instant(booleanOperationTranslationMap.get(keyFilterPredicate.operation)); + if (!dynamicValue) { + value = translate.instant(keyFilterPredicate.value.staticValue ? 'value.true' : 'value.false'); + } + break; + } + if (!dynamicValue) { + value = `${value}`; + } + return `${key} ${operation} ${value}` + } + }); + if (predicates.length > 1) { + return '(' + predicates.join(` ${translate.instant(complexOperationTranslationMap.get(filterOperation))} `)+ ')'; + } else { + return predicates.toString(); + } + } + +} diff --git a/ui-ngx/src/app/modules/home/components/alarm/alarm-filter-config.component.scss b/ui-ngx/src/app/modules/home/components/alarm/alarm-filter-config.component.scss index 2ee3c0089d..c9e5eee944 100644 --- a/ui-ngx/src/app/modules/home/components/alarm/alarm-filter-config.component.scss +++ b/ui-ngx/src/app/modules/home/components/alarm/alarm-filter-config.component.scss @@ -13,6 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + +@import '../scss/constants'; + :host { display: flex; max-width: 100%; @@ -39,16 +42,12 @@ tb-entity-subtype-list { flex: 1; - width: 180px; + @media #{$mat-gt-xs} { + width: 180px; + } .mdc-evolution-chip-set__chips { width: 100%; } } - - .mat-mdc-chip { - .mdc-evolution-chip__cell, .mat-mdc-chip-action, .mat-mdc-chip-action-label { - overflow: hidden; - } - } } } diff --git a/ui-ngx/src/app/modules/home/components/attribute/add-attribute-dialog.component.html b/ui-ngx/src/app/modules/home/components/attribute/add-attribute-dialog.component.html index 453f9f6217..cd87bde8f2 100644 --- a/ui-ngx/src/app/modules/home/components/attribute/add-attribute-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/attribute/add-attribute-dialog.component.html @@ -29,8 +29,8 @@
    -
    - +
    + attribute.key diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-field.module.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-field.module.ts new file mode 100644 index 0000000000..e9bf146bfd --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-field.module.ts @@ -0,0 +1,69 @@ +/// +/// Copyright © 2016-2025 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { SharedModule } from '@shared/shared.module'; +import { + CalculatedFieldDialogComponent +} from '@home/components/calculated-fields/components/dialog/calculated-field-dialog.component'; +import { + CalculatedFieldScriptTestDialogComponent +} from '@home/components/calculated-fields/components/test-dialog/calculated-field-script-test-dialog.component'; +import { + CalculatedFieldTestArgumentsComponent +} from '@home/components/calculated-fields/components/test-arguments/calculated-field-test-arguments.component'; +import { + EntityDebugSettingsButtonComponent +} from '@home/components/entity/debug/entity-debug-settings-button.component'; +import { + GeofencingConfigurationModule +} from '@home/components/calculated-fields/components/geofencing-configuration/geofencing-configuration.module'; +import { + SimpleConfigurationModule +} from '@home/components/calculated-fields/components/simple-configuration/simple-configuration.module'; +import { + PropagationConfigurationModule +} from '@home/components/calculated-fields/components/propagation-configuration/propagation-configuration.module'; +import { + RelatedEntitiesAggregationComponentModule +} from '@home/components/calculated-fields/components/related-entities-aggregation-configuration/related-entities-aggregation-component.module'; +import { + EntityAggregationComponentModule +} from '@home/components/calculated-fields/components/entity-aggregation-configuration/entity-aggregation-component.module'; + +@NgModule({ + declarations: [ + CalculatedFieldDialogComponent, + CalculatedFieldScriptTestDialogComponent, + CalculatedFieldTestArgumentsComponent, + ], + imports: [ + CommonModule, + SharedModule, + GeofencingConfigurationModule, + EntityDebugSettingsButtonComponent, + SimpleConfigurationModule, + PropagationConfigurationModule, + RelatedEntitiesAggregationComponentModule, + EntityAggregationComponentModule, + ], + exports: [ + CalculatedFieldDialogComponent, + CalculatedFieldScriptTestDialogComponent, + ] +}) +export class CalculatedFieldsModule {} diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table-config.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table-config.ts index 07f8f0293c..0ac64b428f 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table-config.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table-config.ts @@ -40,10 +40,12 @@ import { ArgumentType, CalculatedField, CalculatedFieldEventArguments, + CalculatedFieldScriptConfiguration, CalculatedFieldType, CalculatedFieldTypeTranslations, getCalculatedFieldArgumentsEditorCompleter, getCalculatedFieldArgumentsHighlights, + PropagationWithExpression, } from '@shared/models/calculated-field.models'; import { CalculatedFieldDebugDialogComponent, @@ -57,6 +59,7 @@ import { ImportExportService } from '@shared/import-export/import-export.service import { isObject } from '@core/utils'; import { EntityDebugSettingsService } from '@home/components/entity/debug/entity-debug-settings.service'; import { DatePipe } from '@angular/common'; +import { UtilsService } from "@core/services/utils.service"; export class CalculatedFieldsTableConfig extends EntityTableConfig { @@ -75,8 +78,10 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig('expression', 'calculated-fields.expression', '300px'); + const expressionColumn = new EntityTableColumn('expression', 'calculated-fields.expression', '250px'); expressionColumn.sortable = false; expressionColumn.cellContentFunction = entity => { const expressionLabel = this.getExpressionLabel(entity); - return expressionLabel.length < 45 ? expressionLabel : `${expressionLabel.substring(0, 44)}…`; + return expressionLabel?.length < 45 ? expressionLabel : `${expressionLabel.substring(0, 44)}…`; } expressionColumn.cellTooltipFunction = entity => { const expressionLabel = this.getExpressionLabel(entity); - return expressionLabel.length < 45 ? null : expressionLabel + return expressionLabel?.length < 45 ? null : expressionLabel }; this.columns.push(new DateEntityTableColumn('createdTime', 'common.created-time', this.datePipe, '150px')); - this.columns.push(new EntityTableColumn('name', 'common.name', '33%')); - this.columns.push(new EntityTableColumn('type', 'common.type', '50px', entity => this.translate.instant(CalculatedFieldTypeTranslations.get(entity.type)))); + this.columns.push(new EntityTableColumn('name', 'common.name', '33%', + entity => this.utilsService.customTranslation(entity.name, entity.name))); + this.columns.push(new EntityTableColumn('type', 'common.type', '170px', entity => this.translate.instant(CalculatedFieldTypeTranslations.get(entity.type).name), () => ({whiteSpace: 'nowrap' }))); this.columns.push(expressionColumn); this.cellActionDescriptors.push( @@ -156,11 +162,13 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig> { @@ -212,6 +220,7 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig { - const arg = calculatedField.configuration.arguments[key]; - acc[key] = arg.refEntityId?.entityType === ArgumentEntityType.Tenant - ? { ...arg, refEntityId: { id: this.tenantId, entityType: ArgumentEntityType.Tenant } } - : arg; - return acc; - }, {}); + if (calculatedField.type === CalculatedFieldType.GEOFENCING) { + calculatedField.configuration.zoneGroups = Object.keys(calculatedField.configuration.zoneGroups).reduce((acc, key) => { + const arg = calculatedField.configuration.zoneGroups[key]; + acc[key] = arg.refEntityId?.entityType === ArgumentEntityType.Tenant + ? { ...arg, refEntityId: { id: this.tenantId, entityType: ArgumentEntityType.Tenant } } + : arg; + return acc; + }, {}); + } else { + calculatedField.configuration.arguments = Object.keys(calculatedField.configuration.arguments).reduce((acc, key) => { + const arg = calculatedField.configuration.arguments[key]; + acc[key] = arg.refEntityId?.entityType === ArgumentEntityType.Tenant + ? { ...arg, refEntityId: { id: this.tenantId, entityType: ArgumentEntityType.Tenant } } + : arg; + return acc; + }, {}); + } return calculatedField; } @@ -277,32 +296,42 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig { - const resultArguments = Object.keys(calculatedField.configuration.arguments).reduce((acc, key) => { - const type = calculatedField.configuration.arguments[key].refEntityKey.type; - acc[key] = isObject(argumentsObj) && argumentsObj.hasOwnProperty(key) - ? { ...argumentsObj[key], type } - : type === ArgumentType.Rolling ? { values: [], type } : { value: '', type, ts: new Date().getTime() }; - return acc; - }, {}); - return this.dialog.open(CalculatedFieldScriptTestDialogComponent, - { - disableClose: true, - panelClass: ['tb-dialog', 'tb-fullscreen-dialog', 'tb-fullscreen-dialog-gt-xs'], - data: { - arguments: resultArguments, - expression: calculatedField.configuration.expression, - argumentsEditorCompleter: getCalculatedFieldArgumentsEditorCompleter(calculatedField.configuration.arguments), - argumentsHighlightRules: getCalculatedFieldArgumentsHighlights(calculatedField.configuration.arguments), - openCalculatedFieldEdit - } - }).afterClosed() - .pipe( - filter(Boolean), - tap(expression => { - if (openCalculatedFieldEdit) { - this.editCalculatedField({ entityId: this.entityId, ...calculatedField, configuration: {...calculatedField.configuration, expression } }, true) + if ( + calculatedField.type === CalculatedFieldType.SCRIPT || + (calculatedField.type === CalculatedFieldType.PROPAGATION && calculatedField.configuration.applyExpressionToResolvedArguments === true) + ) { + const resultArguments = Object.keys(calculatedField.configuration.arguments).reduce((acc, key) => { + const type = calculatedField.configuration.arguments[key].refEntityKey.type; + acc[key] = isObject(argumentsObj) && argumentsObj.hasOwnProperty(key) + ? {...argumentsObj[key], type} + : type === ArgumentType.Rolling ? {values: [], type} : {value: '', type, ts: new Date().getTime()}; + return acc; + }, {}); + return this.dialog.open(CalculatedFieldScriptTestDialogComponent, + { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog', 'tb-fullscreen-dialog-gt-xs'], + data: { + arguments: resultArguments, + expression: (calculatedField.configuration as CalculatedFieldScriptConfiguration | PropagationWithExpression).expression, + argumentsEditorCompleter: getCalculatedFieldArgumentsEditorCompleter(calculatedField.configuration.arguments), + argumentsHighlightRules: getCalculatedFieldArgumentsHighlights(calculatedField.configuration.arguments), + openCalculatedFieldEdit } - }), - ); + }).afterClosed() + .pipe( + filter(Boolean), + tap(expression => { + if (openCalculatedFieldEdit) { + this.editCalculatedField({ + entityId: this.entityId, ...calculatedField, + configuration: {...calculatedField.configuration, expression} as any + }, true) + } + }), + ); + } else { + return of(null); + } } } diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table.component.ts index e10c4b301e..ee3d013907 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table.component.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table.component.ts @@ -35,6 +35,7 @@ import { CalculatedFieldsService } from '@core/http/calculated-fields.service'; import { ImportExportService } from '@shared/import-export/import-export.service'; import { EntityDebugSettingsService } from '@home/components/entity/debug/entity-debug-settings.service'; import { DatePipe } from '@angular/common'; +import { UtilsService } from "@core/services/utils.service"; @Component({ selector: 'tb-calculated-fields-table', @@ -50,6 +51,7 @@ export class CalculatedFieldsTableComponent { active = input(); entityId = input(); entityName = input(); + ownerId = input(); calculatedFieldsTableConfig: CalculatedFieldsTableConfig; @@ -62,6 +64,7 @@ export class CalculatedFieldsTableComponent { private renderer: Renderer2, private importExportService: ImportExportService, private entityDebugSettingsService: EntityDebugSettingsService, + private utilsService: UtilsService, private destroyRef: DestroyRef) { effect(() => { @@ -76,8 +79,10 @@ export class CalculatedFieldsTableComponent { this.destroyRef, this.renderer, this.entityName(), + this.ownerId(), this.importExportService, this.entityDebugSettingsService, + this.utilsService, ); this.cd.markForCheck(); } diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-argument-panel.component.html b/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-argument-panel.component.html new file mode 100644 index 0000000000..9277bae767 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-argument-panel.component.html @@ -0,0 +1,263 @@ + +
    +
    {{ 'calculated-fields.argument-settings' | translate }}
    +
    + @if (hint) { +
    + {{ hint | translate }} +
    + } +
    + @if (!isOutputKey) { + + } + + @if (!hiddenEntityTypes) { +
    +
    {{ 'entity.entity-type' | translate }}
    + + + @for (type of argumentEntityTypes; track type) { + {{ ArgumentEntityTypeTranslations.get(type) | translate }} + } + + @if (argumentType.touched && argumentType.hasError('required')) { + + warning + + } + +
    + } + @if (ArgumentEntityTypeParamsMap.has(entityType)) { +
    +
    {{ ArgumentEntityTypeParamsMap.get(entityType).title | translate }}
    + +
    + } +
    + + @if (!hiddenEntityKeyTypes) { +
    +
    {{ 'calculated-fields.argument-type' | translate }}
    + + + @for (type of argumentTypes; track type) { + {{ ArgumentTypeTranslations.get(type) | translate }} + } + + @if (refEntityKeyFormGroup.get('type').hasError('required') && refEntityKeyFormGroup.get('type').touched) { + + warning + + } + +
    + } + @if (entityFilter.singleEntity?.id || entityType === ArgumentEntityType.Current || entityType === ArgumentEntityType.Tenant) { + @if (refEntityKeyFormGroup.get('type').value !== ArgumentType.Attribute) { +
    +
    {{ 'calculated-fields.timeseries-key' | translate }}
    + @if (refEntityKeyFormGroup.get('type').value === ArgumentType.LatestTelemetry) { + + } @else { + + } + + + +
    + } @else { + @if (enableAttributeScopeSelection) { +
    +
    {{ 'calculated-fields.attribute-scope' | translate }}
    + + + + {{ 'calculated-fields.server-attributes' | translate }} + + + {{ 'calculated-fields.client-attributes' | translate }} + + + {{ 'calculated-fields.shared-attributes' | translate }} + + + +
    + } +
    +
    {{ 'calculated-fields.attribute-key' | translate }}
    + +
    + } + } +
    + @if (isOutputKey) { + + } + @if (refEntityKeyFormGroup.get('type').value !== ArgumentType.Rolling) { + @if (!hiddenDefaultValue) { +
    +
    {{ 'calculated-fields.default-value' | translate }}
    + + + @if (argumentFormGroup.get('defaultValue').touched && argumentFormGroup.get('defaultValue').hasError('required')) { + + warning + + } + +
    + } + } @else { +
    +
    {{ 'calculated-fields.time-window' | translate }}
    + +
    + @if (maxDataPointsPerRollingArg) { +
    +
    {{ 'calculated-fields.limit' | translate }}
    +
    + + + + + + +
    +
    + } + } +
    +
    +
    + + +
    +
    + + +
    +
    {{ label | translate }}
    + + + @if (argumentFormGroup.get('argumentName').touched && argumentFormGroup.get('argumentName').hasError('required')) { + + warning + + } @else if (argumentFormGroup.get('argumentName').touched && argumentFormGroup.get('argumentName').hasError('duplicateName')) { + + warning + + } @else if (argumentFormGroup.get('argumentName').touched && argumentFormGroup.get('argumentName').hasError('pattern')) { + + warning + + } @else if (argumentFormGroup.get('argumentName').touched && argumentFormGroup.get('argumentName').hasError('maxlength')) { + + warning + + } @else if (argumentFormGroup.get('argumentName').touched && argumentFormGroup.get('argumentName').hasError('forbiddenName')) { + + warning + + } + +
    +
    diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-argument-panel.component.scss b/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-argument-panel.component.scss similarity index 80% rename from ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-argument-panel.component.scss rename to ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-argument-panel.component.scss index 773489ee60..f3e13b1f19 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-argument-panel.component.scss +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-argument-panel.component.scss @@ -15,22 +15,9 @@ */ @use '../../../../../../../scss/constants' as constants; -$panel-width: 520px; - :host { - display: flex; - width: $panel-width; - max-width: 100%; - max-height: 100vh; - - .fixed-title-width { - @media #{constants.$mat-xs} { - min-width: 120px; - } - } - .limit-field-row { - @media screen and (max-width: $panel-width) { + @media screen and (max-width: 520px) { display: flex; flex-direction: column; @@ -40,6 +27,10 @@ $panel-width: 520px; } } } + + .tb-primary-fill { + overflow: visible; + } } :host ::ng-deep { diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-argument-panel.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-argument-panel.component.ts similarity index 63% rename from ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-argument-panel.component.ts rename to ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-argument-panel.component.ts index 6f46e47af9..ad97e7b06e 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-argument-panel.component.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-argument-panel.component.ts @@ -14,9 +14,18 @@ /// limitations under the License. /// -import { AfterViewInit, ChangeDetectorRef, Component, Input, OnInit, output, ViewChild } from '@angular/core'; +import { + AfterViewInit, + ChangeDetectorRef, + Component, + DestroyRef, + Input, + OnInit, + output, + ViewChild +} from '@angular/core'; import { TbPopoverComponent } from '@shared/components/popover.component'; -import { FormBuilder, FormControl, FormGroup, ValidatorFn, Validators } from '@angular/forms'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { charsWithNumRegex, oneSpaceInsideRegex } from '@shared/models/regex.constants'; import { ArgumentEntityType, @@ -25,10 +34,12 @@ import { ArgumentType, ArgumentTypeTranslations, CalculatedFieldArgumentValue, - CalculatedFieldType, - getCalculatedFieldCurrentEntityFilter + FORBIDDEN_NAMES, + forbiddenNamesValidator, + getCalculatedFieldCurrentEntityFilter, + uniqueNameValidator } from '@shared/models/calculated-field.models'; -import { debounceTime, delay, distinctUntilChanged, filter } from 'rxjs/operators'; +import { debounceTime, distinctUntilChanged, filter } from 'rxjs/operators'; import { EntityType } from '@shared/models/entity-type.models'; import { AttributeScope, DataKeyType } from '@shared/models/telemetry/telemetry.models'; import { DatasourceType } from '@shared/models/widget.models'; @@ -43,11 +54,12 @@ import { AppState } from '@core/core.state'; import { Store } from '@ngrx/store'; import { EntityAutocompleteComponent } from '@shared/components/entity/entity-autocomplete.component'; import { NULL_UUID } from '@shared/models/id/has-uuid'; +import { TenantId } from '@shared/models/id/tenant-id'; @Component({ selector: 'tb-calculated-field-argument-panel', templateUrl: './calculated-field-argument-panel.component.html', - styleUrls: ['./calculated-field-argument-panel.component.scss'] + styleUrls: ['../common/calculated-field-panel.scss', './calculated-field-argument-panel.component.scss'] }) export class CalculatedFieldArgumentPanelComponent implements OnInit, AfterViewInit { @@ -56,22 +68,31 @@ export class CalculatedFieldArgumentPanelComponent implements OnInit, AfterViewI @Input() entityId: EntityId; @Input() tenantId: string; @Input() entityName: string; - @Input() calculatedFieldType: CalculatedFieldType; + @Input() ownerId: EntityId; + @Input() isScript: boolean; @Input() usedArgumentNames: string[]; + @Input() isOutputKey = false; + @Input() hiddenEntityTypes = false; + @Input() hiddenEntityKeyTypes = false; + @Input() hiddenDefaultValue = false; + @Input() defaultValueRequired = false; + @Input() hint: string; + @Input() predefinedEntityFilter: EntityFilter; + @Input() forbiddenNames = FORBIDDEN_NAMES; + @Input() argumentEntityTypes = Object.values(ArgumentEntityType).filter(value => value !== ArgumentEntityType.RelationQuery) as ArgumentEntityType[]; @ViewChild('entityAutocomplete') entityAutocomplete: EntityAutocompleteComponent; argumentsDataApplied = output(); + argumentType = this.fb.control(ArgumentEntityType.Current, Validators.required); + readonly maxDataPointsPerRollingArg = getCurrentAuthState(this.store).maxDataPointsPerRollingArg; readonly defaultLimit = Math.floor(this.maxDataPointsPerRollingArg / 10); argumentFormGroup = this.fb.group({ - argumentName: ['', [Validators.required, this.uniqNameRequired(), this.forbiddenArgumentNameValidator(), Validators.pattern(charsWithNumRegex), Validators.maxLength(255)]], - refEntityId: this.fb.group({ - entityType: [ArgumentEntityType.Current], - id: [''] - }), + argumentName: ['', [Validators.required, Validators.pattern(charsWithNumRegex), Validators.maxLength(255)]], + refEntityId: [null], refEntityKey: this.fb.group({ type: [ArgumentType.LatestTelemetry, [Validators.required]], key: ['', [Validators.pattern(oneSpaceInsideRegex)]], @@ -86,7 +107,6 @@ export class CalculatedFieldArgumentPanelComponent implements OnInit, AfterViewI entityFilter: EntityFilter; entityNameSubject = new BehaviorSubject(null); - readonly argumentEntityTypes = Object.values(ArgumentEntityType) as ArgumentEntityType[]; readonly ArgumentEntityTypeTranslations = ArgumentEntityTypeTranslations; readonly ArgumentType = ArgumentType; readonly DataKeyType = DataKeyType; @@ -103,20 +123,16 @@ export class CalculatedFieldArgumentPanelComponent implements OnInit, AfterViewI private fb: FormBuilder, private cd: ChangeDetectorRef, private popover: TbPopoverComponent, - private store: Store + private store: Store, + private destroyRef: DestroyRef ) { this.observeEntityFilterChanges(); - this.observeEntityTypeChanges(); + this.observeArgumentTypeChanges(); this.observeEntityKeyChanges(); - this.observeUpdatePosition(); } get entityType(): ArgumentEntityType { - return this.argumentFormGroup.get('refEntityId').get('entityType').value; - } - - get refEntityIdFormGroup(): FormGroup { - return this.argumentFormGroup.get('refEntityId') as FormGroup; + return this.argumentType.value; } get refEntityKeyFormGroup(): FormGroup { @@ -130,14 +146,24 @@ export class CalculatedFieldArgumentPanelComponent implements OnInit, AfterViewI } ngOnInit(): void { + this.updatedFormValidators(); + this.updatedArgumentType(); this.argumentFormGroup.patchValue(this.argument, {emitEvent: false}); this.currentEntityFilter = getCalculatedFieldCurrentEntityFilter(this.entityName, this.entityId); - this.updateEntityFilter(this.argument.refEntityId?.entityType, true); + this.updateEntityFilter(this.entityType, true); + this.updatedRefEntityIdState(this.entityType, false); this.toggleByEntityKeyType(this.argument.refEntityKey?.type); this.setInitialEntityKeyType(); + this.setInitialEntityType(); + this.setWatchKeyChange(); + + if (this.defaultValueRequired) { + this.argumentFormGroup.get('defaultValue').addValidators(Validators.required); + this.argumentFormGroup.get('defaultValue').updateValueAndValidity({onlySelf: true}); + } this.argumentTypes = Object.values(ArgumentType) - .filter(type => type !== ArgumentType.Rolling || this.calculatedFieldType === CalculatedFieldType.SCRIPT); + .filter(type => type !== ArgumentType.Rolling || this.isScript); } ngAfterViewInit(): void { @@ -147,12 +173,13 @@ export class CalculatedFieldArgumentPanelComponent implements OnInit, AfterViewI } saveArgument(): void { - const { refEntityId, ...restConfig } = this.argumentFormGroup.value; - const value = (refEntityId.entityType === ArgumentEntityType.Current ? restConfig : { refEntityId, ...restConfig }) as CalculatedFieldArgumentValue; - if (refEntityId.entityType === ArgumentEntityType.Tenant) { - refEntityId.id = this.tenantId; + const value = this.argumentFormGroup.value as CalculatedFieldArgumentValue; + if (this.entityType === ArgumentEntityType.Owner) { + value.refDynamicSourceConfiguration = {type: ArgumentEntityType.Owner}; + } else if (this.entityType === ArgumentEntityType.Tenant) { + value.refEntityId = new TenantId(this.tenantId) as any; } - if (refEntityId.entityType !== ArgumentEntityType.Current && refEntityId.entityType !== ArgumentEntityType.Tenant) { + if (this.entityType !== ArgumentEntityType.Current && this.entityType !== ArgumentEntityType.Tenant) { value.entityName = this.entityNameSubject.value; } if (value.defaultValue) { @@ -166,6 +193,22 @@ export class CalculatedFieldArgumentPanelComponent implements OnInit, AfterViewI this.popover.hide(); } + private updatedFormValidators(): void { + this.argumentFormGroup.get('argumentName').addValidators( + [uniqueNameValidator(this.usedArgumentNames), forbiddenNamesValidator(this.forbiddenNames)]); + this.argumentFormGroup.get('argumentName').updateValueAndValidity({emitEvent: false}); + } + + private updatedArgumentType(): void { + let argumentType = ArgumentEntityType.Current; + if (this.argument.refDynamicSourceConfiguration?.type === ArgumentEntityType.Owner) { + argumentType = ArgumentEntityType.Owner; + } else if (this.argument.refEntityId?.entityType) { + argumentType = this.argument.refEntityId.entityType; + } + this.argumentType.setValue(argumentType, {emitEvent: false}); + } + private toggleByEntityKeyType(type: ArgumentType): void { const isAttribute = type === ArgumentType.Attribute; const isRolling = type === ArgumentType.Rolling; @@ -181,6 +224,12 @@ export class CalculatedFieldArgumentPanelComponent implements OnInit, AfterViewI case ArgumentEntityType.Current: entityFilter = this.currentEntityFilter; break; + case ArgumentEntityType.Owner: + entityFilter = { + type: AliasFilterType.singleEntity, + singleEntity: this.ownerId + }; + break; case ArgumentEntityType.Tenant: entityFilter = { type: AliasFilterType.singleEntity, @@ -198,6 +247,8 @@ export class CalculatedFieldArgumentPanelComponent implements OnInit, AfterViewI } if (!onInit) { this.argumentFormGroup.get('refEntityKey').get('key').setValue(''); + } else if (this.predefinedEntityFilter) { + entityFilter = this.predefinedEntityFilter; } this.entityFilter = entityFilter; this.cd.markForCheck(); @@ -205,41 +256,27 @@ export class CalculatedFieldArgumentPanelComponent implements OnInit, AfterViewI private observeEntityFilterChanges(): void { merge( - this.refEntityIdFormGroup.get('entityType').valueChanges, + this.argumentType.valueChanges, this.refEntityKeyFormGroup.get('type').valueChanges, - this.refEntityIdFormGroup.get('id').valueChanges.pipe(filter(Boolean)), + this.argumentFormGroup.get('refEntityId').valueChanges.pipe(filter(Boolean)), this.refEntityKeyFormGroup.get('scope').valueChanges, ) .pipe(debounceTime(50), takeUntilDestroyed()) .subscribe(() => this.updateEntityFilter(this.entityType)); } - private observeEntityTypeChanges(): void { - this.refEntityIdFormGroup.get('entityType').valueChanges + private observeArgumentTypeChanges(): void { + this.argumentType.valueChanges .pipe(distinctUntilChanged(), takeUntilDestroyed()) .subscribe(type => { - this.argumentFormGroup.get('refEntityId').get('id').setValue(''); - const isEntityWithId = type !== ArgumentEntityType.Tenant && type !== ArgumentEntityType.Current; - this.argumentFormGroup.get('refEntityId') - .get('id')[isEntityWithId ? 'enable' : 'disable'](); - if (!isEntityWithId) { - this.entityNameSubject.next(null); - } + this.argumentFormGroup.get('refEntityId').setValue(null); + this.updatedRefEntityIdState(type); if (!this.enableAttributeScopeSelection) { this.refEntityKeyFormGroup.get('scope').setValue(AttributeScope.SERVER_SCOPE); } }); } - private uniqNameRequired(): ValidatorFn { - return (control: FormControl) => { - const newName = control.value.trim().toLowerCase(); - const isDuplicate = this.usedArgumentNames?.some(name => name.toLowerCase() === newName); - - return isDuplicate ? { duplicateName: true } : null; - }; - } - private observeEntityKeyChanges(): void { this.argumentFormGroup.get('refEntityKey').get('type').valueChanges .pipe(takeUntilDestroyed()) @@ -247,29 +284,37 @@ export class CalculatedFieldArgumentPanelComponent implements OnInit, AfterViewI } private setInitialEntityKeyType(): void { - if (this.calculatedFieldType === CalculatedFieldType.SIMPLE && this.argument.refEntityKey?.type === ArgumentType.Rolling) { + if (!this.isScript && this.argument.refEntityKey?.type === ArgumentType.Rolling) { const typeControl = this.argumentFormGroup.get('refEntityKey').get('type'); typeControl.setValue(null); typeControl.markAsTouched(); } } - private forbiddenArgumentNameValidator(): ValidatorFn { - return (control: FormControl) => { - const trimmedValue = control.value.trim().toLowerCase(); - const forbiddenArgumentNames = ['ctx', 'e', 'pi']; - return forbiddenArgumentNames.includes(trimmedValue) ? { forbiddenName: true } : null; - }; + private setInitialEntityType() { + if (!this.argumentEntityTypes.includes(this.entityType)) { + this.argumentType.setValue(null); + this.argumentType.markAsTouched(); + } } - private observeUpdatePosition(): void { - merge( - this.refEntityIdFormGroup.get('entityType').valueChanges, - this.refEntityKeyFormGroup.get('type').valueChanges, - this.argumentFormGroup.get('timeWindow').valueChanges, - this.refEntityIdFormGroup.get('id').valueChanges.pipe(filter(Boolean)), - ) - .pipe(delay(50), takeUntilDestroyed()) - .subscribe(() => this.popover.updatePosition()); + private setWatchKeyChange(): void { + if (this.isOutputKey) { + this.refEntityKeyFormGroup.get('key').valueChanges.pipe( + takeUntilDestroyed(this.destroyRef) + ).subscribe((key) => { + if (this.argumentFormGroup.get('argumentName').pristine) { + this.argumentFormGroup.get('argumentName').setValue(key); + } + }); + } + } + + private updatedRefEntityIdState(type: ArgumentEntityType, emitEvent = true): void { + const isEntityWithId = !!type && ![ArgumentEntityType.Tenant, ArgumentEntityType.Current, ArgumentEntityType.Owner].includes(type); + this.argumentFormGroup.get('refEntityId')[isEntityWithId ? 'enable' : 'disable']({emitEvent}); + if (!isEntityWithId) { + this.entityNameSubject.next(null); + } } } diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/arguments-table/calculated-field-arguments-table.component.html b/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-arguments-table.component.html similarity index 87% rename from ui-ngx/src/app/modules/home/components/calculated-fields/components/arguments-table/calculated-field-arguments-table.component.html rename to ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-arguments-table.component.html index b4b5a939e1..15d2f4b09b 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/arguments-table/calculated-field-arguments-table.component.html +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-arguments-table.component.html @@ -21,7 +21,7 @@ [matSortActive]="sortOrder.property" [matSortDirection]="sortOrder.direction" matSortDisableClear> -
    {{ 'common.name' | translate }}
    +
    {{ argumentNameColumn | translate }}
    @@ -29,7 +29,7 @@ @@ -37,13 +37,15 @@ - + {{ 'entity.entity-type' | translate }}
    @if (argument.refEntityId?.entityType === ArgumentEntityType.Tenant) { {{ 'calculated-fields.argument-current-tenant' | translate }} + } @else if (argument.refDynamicSourceConfiguration?.type === ArgumentEntityType.Owner) { + {{ 'calculated-fields.argument-owner' | translate }} } @else if (argument.refEntityId?.id) { {{ entityTypeTranslations.get(argument.refEntityId.entityType).type | translate }} } @else { @@ -59,7 +61,7 @@
    @if (argument.refEntityId?.id && argument.refEntityId?.entityType !== ArgumentEntityType.Tenant) { - {{ entityNameMap.get(argument.refEntityId.id) ?? '' }} @@ -80,7 +82,7 @@ {{ 'entity.key' | translate }} - +
    {{ argument.refEntityKey.key }}
    @@ -88,7 +90,7 @@ -
    +
    - -
    -
    {{ 'calculated-fields.arguments' | translate }}
    - -
    -
    -
    - {{ (fieldFormGroup.get('type').value === CalculatedFieldType.SIMPLE ? 'calculated-fields.expression' : 'calculated-fields.type.script' ) | translate }} -
    - - -
    -
    - @if (configFormGroup.get('expressionSIMPLE').errors && configFormGroup.get('expressionSIMPLE').touched) { - - @if (configFormGroup.get('expressionSIMPLE').hasError('required')) { - {{ 'calculated-fields.hint.expression-required' | translate }} - } @else if (configFormGroup.get('expressionSIMPLE').hasError('pattern')) { - {{ 'calculated-fields.hint.expression-invalid' | translate }} - } @else if (configFormGroup.get('expressionSIMPLE').hasError('maxLength')) { - {{ 'calculated-fields.hint.expression-max-length' | translate }} - } - - } @else { - {{ 'calculated-fields.hint.expression' | translate }} - } -
    -
    - -
    {{ 'api-usage.tbel' | translate }}
    - -
    -
    - -
    -
    -
    -
    -
    {{ 'calculated-fields.output' | translate }}
    -
    - - {{ 'calculated-fields.output-type' | translate }} - - @for (type of outputTypes; track type) { - {{ OutputTypeTranslations.get(type) | translate}} - } - - - @if (outputFormGroup.get('type').value === OutputType.Attribute - && (data.entityId.entityType === EntityType.DEVICE || data.entityId.entityType === EntityType.DEVICE_PROFILE)) { - - {{ 'calculated-fields.attribute-scope' | translate }} - - - {{ 'calculated-fields.server-attributes' | translate }} - - - {{ 'calculated-fields.shared-attributes' | translate }} - - - - } -
    - @if (fieldFormGroup.get('type').value === CalculatedFieldType.SIMPLE) { -
    - - - {{ (outputFormGroup.get('type').value === OutputType.Timeseries - ? 'calculated-fields.timeseries-key' - : 'calculated-fields.attribute-key') - | translate }} - - - @if (outputFormGroup.get('name').errors && outputFormGroup.get('name').touched) { - - @if (outputFormGroup.get('name').hasError('required')) { - {{ 'common.hint.key-required' | translate }} - } @else if (outputFormGroup.get('name').hasError('pattern')) { - {{ 'common.hint.key-pattern' | translate }} - } @else if (outputFormGroup.get('name').hasError('maxlength')) { - {{ 'common.hint.key-max-length' | translate }} - } - - } - - - {{ 'calculated-fields.decimals-by-default' | translate }} - - @if (outputFormGroup.get('decimalsByDefault').errors && outputFormGroup.get('decimalsByDefault').touched) { - {{ 'calculated-fields.hint.decimals-range' | translate }} - } - -
    -
    - -
    - calculated-fields.use-latest-timestamp -
    -
    -
    - } -
    -
    + @switch (fieldFormGroup.get('type').value) { + @case (CalculatedFieldType.GEOFENCING) { + + } + @case (CalculatedFieldType.PROPAGATION) { + + } + @case (CalculatedFieldType.RELATED_ENTITIES_AGGREGATION) { + + } + @case (CalculatedFieldType.ENTITY_AGGREGATION) { + + } + @default { + + } + }
    diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.scss b/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.scss index e192e3ccc0..5e83c1821c 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.scss +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.scss @@ -17,6 +17,11 @@ .calculated-field-dialog-container { width: 869px; max-width: 100%; + display: grid; + grid-template-rows: min-content minmax(auto, 1fr) min-content; + --mdc-outlined-text-field-outline-color: rgba(0,0,0,0.12); + --mdc-outlined-text-field-container-shape: 6px; + --mat-form-field-trailing-icon-color: rgba(0, 0, 0, 0.56); } .tbel-script-lang-chip { diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.ts index 975744b2c6..17c550dcdf 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.ts @@ -18,31 +18,25 @@ import { Component, DestroyRef, Inject, ViewEncapsulation } from '@angular/core' import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; -import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { FormBuilder, Validators } from '@angular/forms'; import { Router } from '@angular/router'; import { DialogComponent } from '@shared/components/dialog.component'; import { CalculatedField, CalculatedFieldConfiguration, - calculatedFieldDefaultScript, CalculatedFieldTestScriptFn, CalculatedFieldType, - CalculatedFieldTypeTranslations, - getCalculatedFieldArgumentsEditorCompleter, - getCalculatedFieldArgumentsHighlights, - OutputType, - OutputTypeTranslations + CalculatedFieldTypeTranslations } from '@shared/models/calculated-field.models'; -import { digitsRegex, oneSpaceInsideRegex } from '@shared/models/regex.constants'; -import { AttributeScope } from '@shared/models/telemetry/telemetry.models'; +import { oneSpaceInsideRegex } from '@shared/models/regex.constants'; import { EntityType } from '@shared/models/entity-type.models'; -import { map, startWith, switchMap } from 'rxjs/operators'; +import { pairwise, switchMap } from 'rxjs/operators'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; -import { ScriptLanguage } from '@shared/models/rule-node.models'; import { CalculatedFieldsService } from '@core/http/calculated-fields.service'; import { Observable } from 'rxjs'; import { EntityId } from '@shared/models/id/entity-id'; import { AdditionalDebugActionConfig } from '@home/components/entity/debug/entity-debug-settings.model'; +import { deepTrim } from '@core/utils'; export interface CalculatedFieldDialogData { value?: CalculatedField; @@ -50,6 +44,7 @@ export interface CalculatedFieldDialogData { entityId: EntityId; tenantId: string; entityName?: string; + ownerId: EntityId; additionalDebugActionConfig: AdditionalDebugActionConfig<(calculatedField: CalculatedField) => void>; getTestScriptDialogFn: CalculatedFieldTestScriptFn; isDirty?: boolean; @@ -67,51 +62,17 @@ export class CalculatedFieldDialogComponent extends DialogComponent({} as CalculatedFieldConfiguration), }); - functionArgs$ = this.configFormGroup.get('arguments').valueChanges - .pipe( - startWith(this.data.value?.configuration?.arguments ?? {}), - map(argumentsObj => ['ctx', ...Object.keys(argumentsObj)]) - ); - - argumentsEditorCompleter$ = this.configFormGroup.get('arguments').valueChanges - .pipe( - startWith(this.data.value?.configuration?.arguments ?? {}), - map(argumentsObj => getCalculatedFieldArgumentsEditorCompleter(argumentsObj)) - ); - - argumentsHighlightRules$ = this.configFormGroup.get('arguments').valueChanges - .pipe( - startWith(this.data.value?.configuration?.arguments ?? {}), - map(argumentsObj => getCalculatedFieldArgumentsHighlights(argumentsObj)) - ); - additionalDebugActionConfig = this.data.value?.id ? { ...this.data.additionalDebugActionConfig, action: () => this.data.additionalDebugActionConfig.action({ id: this.data.value.id, ...this.fromGroupValue }), } : null; - readonly OutputTypeTranslations = OutputTypeTranslations; - readonly OutputType = OutputType; - readonly AttributeScope = AttributeScope; readonly EntityType = EntityType; readonly CalculatedFieldType = CalculatedFieldType; - readonly ScriptLanguage = ScriptLanguage; - readonly fieldTypes = Object.values(CalculatedFieldType) as CalculatedFieldType[]; - readonly outputTypes = Object.values(OutputType) as OutputType[]; + readonly fieldTypes = Object.values(CalculatedFieldType).filter(type => type !== CalculatedFieldType.ALARM) as CalculatedFieldType[]; readonly CalculatedFieldTypeTranslations = CalculatedFieldTypeTranslations; constructor(protected store: Store, @@ -123,31 +84,12 @@ export class CalculatedFieldDialogComponent extends DialogComponent { const calculatedFieldId = this.data.value?.id?.id; - let testScriptDialogResult$: Observable; - if (calculatedFieldId) { - testScriptDialogResult$ = this.calculatedFieldsService.getLatestCalculatedFieldDebugEvent(calculatedFieldId) + return this.calculatedFieldsService.getLatestCalculatedFieldDebugEvent(calculatedFieldId, {ignoreLoading: true}) .pipe( switchMap(event => { const args = event?.arguments ? JSON.parse(event.arguments) : null; @@ -175,68 +115,14 @@ export class CalculatedFieldDialogComponent extends DialogComponent { - this.configFormGroup.get('expressionSCRIPT').setValue(expression); - this.configFormGroup.get('expressionSCRIPT').markAsDirty(); - }); + return this.data.getTestScriptDialogFn(this.fromGroupValue, null, false); } private applyDialogData(): void { - const { configuration = {}, type = CalculatedFieldType.SIMPLE, debugSettings = { failuresEnabled: true, allEnabled: true }, ...value } = this.data.value ?? {}; - const { expression, ...restConfig } = configuration as CalculatedFieldConfiguration; - const updatedConfig = { ...restConfig , ['expression'+type]: expression }; - this.fieldFormGroup.patchValue({ configuration: updatedConfig, type, debugSettings, ...value }, {emitEvent: false}); - } - - private observeTypeChanges(): void { - this.toggleKeyByCalculatedFieldType(this.fieldFormGroup.get('type').value); - this.toggleScopeByOutputType(this.outputFormGroup.get('type').value); - - this.outputFormGroup.get('type').valueChanges - .pipe(takeUntilDestroyed()) - .subscribe(type => this.toggleScopeByOutputType(type)); - this.fieldFormGroup.get('type').valueChanges - .pipe(takeUntilDestroyed()) - .subscribe(type => this.toggleKeyByCalculatedFieldType(type)); - } - - private toggleScopeByOutputType(type: OutputType): void { - if (type === OutputType.Attribute) { - this.outputFormGroup.get('scope').enable({emitEvent: false}); - } else { - this.outputFormGroup.get('scope').disable({emitEvent: false}); - } - if (this.fieldFormGroup.get('type').value === CalculatedFieldType.SIMPLE) { - if (type === OutputType.Attribute) { - this.configFormGroup.get('useLatestTs').disable({emitEvent: false}); - } else { - this.configFormGroup.get('useLatestTs').enable({emitEvent: false}); - } - } else { - this.configFormGroup.get('useLatestTs').disable({emitEvent: false}); - } - } - - private toggleKeyByCalculatedFieldType(type: CalculatedFieldType): void { - if (type === CalculatedFieldType.SIMPLE) { - this.outputFormGroup.get('name').enable({emitEvent: false}); - this.configFormGroup.get('expressionSIMPLE').enable({emitEvent: false}); - this.configFormGroup.get('expressionSCRIPT').disable({emitEvent: false}); - if (this.outputFormGroup.get('type').value === OutputType.Attribute) { - this.configFormGroup.get('useLatestTs').disable({emitEvent: false}); - } else { - this.configFormGroup.get('useLatestTs').enable({emitEvent: false}); - } - } else { - this.outputFormGroup.get('name').disable({emitEvent: false}); - this.configFormGroup.get('useLatestTs').disable({emitEvent: false}); - this.configFormGroup.get('expressionSIMPLE').disable({emitEvent: false}); - this.configFormGroup.get('expressionSCRIPT').enable({emitEvent: false}); - } + const { configuration = {} as CalculatedFieldConfiguration, type = CalculatedFieldType.SIMPLE, debugSettings = { failuresEnabled: true, allEnabled: true }, ...value } = this.data.value ?? {}; + this.fieldFormGroup.patchValue({ configuration, type, debugSettings, ...value }, {emitEvent: false}); + setTimeout(() => this.fieldFormGroup.get('type').updateValueAndValidity({onlySelf: true})); } private observeIsLoading(): void { @@ -245,12 +131,22 @@ export class CalculatedFieldDialogComponent extends DialogComponent { + if (![CalculatedFieldType.SIMPLE, CalculatedFieldType.SCRIPT].includes(prevType) || + ![CalculatedFieldType.SIMPLE, CalculatedFieldType.SCRIPT].includes(nextType)) { + this.fieldFormGroup.get('configuration').setValue(({} as CalculatedFieldConfiguration), {emitEvent: false}); + } + }); + } } diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/entity-aggregation-configuration/entity-aggregation-component.component.html b/ui-ngx/src/app/modules/home/components/calculated-fields/components/entity-aggregation-configuration/entity-aggregation-component.component.html new file mode 100644 index 0000000000..e4a40c3cdd --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/entity-aggregation-configuration/entity-aggregation-component.component.html @@ -0,0 +1,133 @@ + +
    +
    +
    + {{ 'calculated-fields.arguments' | translate }} +
    +
    + {{ 'calculated-fields.entity-aggregation.argument-hint' | translate }} +
    + +
    +
    +
    + {{ 'calculated-fields.metrics.metrics' | translate }} +
    + +
    +
    +
    + {{ 'calculated-fields.entity-aggregation.aggregation-interval' | translate }} +
    + +
    + + calculated-fields.aggregate-interval-type + + + {{ AggIntervalTypeTranslations.get(type) | translate }} + + + + + +
    + @if (entityAggregationConfiguration.get('interval.type').value === AggIntervalType.CUSTOM) { + + + } +
    + +
    + {{ 'calculated-fields.entity-aggregation.apply-offset' | translate }} +
    +
    + @if (entityAggregationConfiguration.get('interval.allowOffsetSec').value) { + + +
    + {{ hint }} +
    + } +
    +
    +
    + +
    + {{ 'calculated-fields.entity-aggregation.wait-delay' | translate }} +
    +
    + @if (entityAggregationConfiguration.get('allowWatermark').value) { + + + + + } +
    +
    + +
    diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/entity-aggregation-configuration/entity-aggregation-component.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/entity-aggregation-configuration/entity-aggregation-component.component.ts new file mode 100644 index 0000000000..5d612704a0 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/entity-aggregation-configuration/entity-aggregation-component.component.ts @@ -0,0 +1,411 @@ +/// +/// Copyright © 2016-2025 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, forwardRef, Input } from '@angular/core'; +import { + ControlValueAccessor, + FormBuilder, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + ValidationErrors, + Validator, + Validators +} from '@angular/forms'; +import { EntityId } from '@shared/models/id/entity-id'; +import { + AggInterval, + AggIntervalType, + AggIntervalTypeTranslations, + CalculatedFieldEntityAggregationConfiguration, + CalculatedFieldOutput, + CalculatedFieldType, + notEmptyObjectValidator, + OutputType +} from '@shared/models/calculated-field.models'; +import { filter, map } from 'rxjs/operators'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { AVG_MONTH, AVG_QUARTER, DAY, HOUR, MINUTE, SECOND, YEAR } from '@shared/models/time/time.models'; +import { deepClone, isDefinedAndNotNull } from '@core/utils'; +import { getCurrentAuthState } from '@core/auth/auth.selectors'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { merge } from 'rxjs'; +import { TranslateService } from '@ngx-translate/core'; +import _moment from 'moment'; + +interface CalculatedFieldEntityAggregationConfigurationValue extends CalculatedFieldEntityAggregationConfiguration { + interval: AggInterval & {allowOffsetSec?: boolean}; + allowWatermark: boolean; +} + +enum TimeCategory { + SECONDS = 'SECONDS', + MINUTES = 'MINUTES', + HOURS = 'HOURS', + DAYS = 'DAYS' +} + +@Component({ + selector: 'tb-entity-aggregation-component', + templateUrl: './entity-aggregation-component.component.html', + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => EntityAggregationComponentComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => EntityAggregationComponentComponent), + multi: true + } + ], +}) +export class EntityAggregationComponentComponent implements ControlValueAccessor, Validator { + + @Input({required: true}) + entityId: EntityId; + + @Input({required: true}) + tenantId: string; + + @Input({required: true}) + entityName: string; + + readonly minAllowedAggregationIntervalInSecForCF = getCurrentAuthState(this.store).minAllowedAggregationIntervalInSecForCF; + readonly DayInSec = DAY / SECOND; + + entityAggregationConfiguration = this.fb.group({ + arguments: this.fb.control({}, notEmptyObjectValidator()), + metrics: this.fb.control({}, notEmptyObjectValidator()), + interval: this.fb.group({ + type: [AggIntervalType.HOUR], + tz: ['', Validators.required], + durationSec: [this.minAllowedAggregationIntervalInSecForCF, Validators.required], + allowOffsetSec: [false], + offsetSec: [this.minAllowedAggregationIntervalInSecForCF > 60 ? MINUTE / SECOND : 1, Validators.required], + }), + allowWatermark: [false], + watermark: this.fb.group({ + duration: [HOUR/SECOND, Validators.required], + }), + output: this.fb.control({ + type: OutputType.Timeseries, + }), + }); + + arguments$ = this.entityAggregationConfiguration.get('arguments').valueChanges.pipe( + map(argumentsObj => Object.keys(argumentsObj)) + ); + + AggIntervalType = AggIntervalType; + AggIntervalTypes = Object.values(AggIntervalType) as AggIntervalType[]; + AggIntervalTypeTranslations = AggIntervalTypeTranslations; + + hint: string; + + private propagateChange: (config: CalculatedFieldEntityAggregationConfiguration) => void = () => { }; + + constructor(private fb: FormBuilder, + private store: Store, + private translate: TranslateService,) { + + this.entityAggregationConfiguration.get('interval.type').valueChanges.pipe( + takeUntilDestroyed() + ).subscribe((type: AggIntervalType) => { + this.checkAggIntervalType(type); + }); + + this.entityAggregationConfiguration.get('interval.allowOffsetSec').valueChanges.pipe( + takeUntilDestroyed() + ).subscribe((allow: boolean) => { + this.checkIntervalDuration(allow); + }); + + this.entityAggregationConfiguration.get('allowWatermark').valueChanges.pipe( + takeUntilDestroyed() + ).subscribe((allow: boolean) => { + this.checkWatermark(allow); + }); + + merge( + this.entityAggregationConfiguration.get('interval.type').valueChanges, + this.entityAggregationConfiguration.get('interval.durationSec').valueChanges, + this.entityAggregationConfiguration.get('interval.offsetSec').valueChanges, + this.entityAggregationConfiguration.get('interval.allowOffsetSec').valueChanges, + ).pipe( + filter(() => this.entityAggregationConfiguration.get('interval.allowOffsetSec').value), + takeUntilDestroyed() + ).subscribe(() => { + this.updatedOffsetHint(); + }); + + this.entityAggregationConfiguration.valueChanges.pipe( + takeUntilDestroyed() + ).subscribe((value: CalculatedFieldEntityAggregationConfigurationValue) => { + this.updatedModel(deepClone(value)); + }); + } + + validate(): ValidationErrors | null { + return this.entityAggregationConfiguration.valid || this.entityAggregationConfiguration.disabled ? null : {invalidPropagateConfig: false}; + } + + writeValue(value: CalculatedFieldEntityAggregationConfiguration): void { + const data: CalculatedFieldEntityAggregationConfigurationValue = { + ...value, + allowWatermark: isDefinedAndNotNull(value.watermark), + interval: {...value.interval, allowOffsetSec: isDefinedAndNotNull(value?.interval?.offsetSec)} + } + this.entityAggregationConfiguration.patchValue(data, {emitEvent: false}); + this.checkAggIntervalType(this.entityAggregationConfiguration.get('interval.type').value); + this.checkIntervalDuration(this.entityAggregationConfiguration.get('interval.allowOffsetSec').value); + this.checkWatermark(this.entityAggregationConfiguration.get('allowWatermark').value); + this.updatedOffsetHint(); + setTimeout(() => { + this.entityAggregationConfiguration.get('arguments').updateValueAndValidity({onlySelf: true}); + }); + } + + registerOnChange(fn: (config: CalculatedFieldEntityAggregationConfiguration) => void): void { + this.propagateChange = fn; + } + + registerOnTouched(_: any): void { } + + setDisabledState(isDisabled: boolean): void { + if (isDisabled) { + this.entityAggregationConfiguration.disable({emitEvent: false}); + } else { + this.entityAggregationConfiguration.enable({emitEvent: false}); + this.checkAggIntervalType(this.entityAggregationConfiguration.get('interval.type').value); + this.checkIntervalDuration(this.entityAggregationConfiguration.get('interval.allowOffsetSec').value); + this.checkWatermark(this.entityAggregationConfiguration.get('allowWatermark').value); + } + } + + get maxOffsetTime(): number { + switch (this.entityAggregationConfiguration.get('interval.type').value as AggIntervalType) { + case AggIntervalType.HOUR: + return HOUR / SECOND - 1; + case AggIntervalType.DAY: + return DAY / SECOND - 1; + case AggIntervalType.WEEK: + case AggIntervalType.WEEK_SUN_SAT: + return 7 * DAY / SECOND - 1; + case AggIntervalType.MONTH: + return AVG_MONTH / SECOND; + case AggIntervalType.QUARTER: + return AVG_QUARTER / SECOND - 1; + case AggIntervalType.YEAR: + return YEAR / SECOND - 1; + case AggIntervalType.CUSTOM: + return this.entityAggregationConfiguration.get('interval.durationSec').value - 1; + } + } + + private updatedModel(value: CalculatedFieldEntityAggregationConfigurationValue): void { + value.type = CalculatedFieldType.ENTITY_AGGREGATION; + if (!value.interval.allowOffsetSec) { + delete value.interval.offsetSec; + } + delete value.interval.allowOffsetSec; + if (!value.allowWatermark) { + delete value.watermark; + } + delete value.allowWatermark; + this.propagateChange(value); + } + + private checkAggIntervalType(type: AggIntervalType) { + if (type === AggIntervalType.CUSTOM) { + this.entityAggregationConfiguration.get('interval.durationSec').enable({emitEvent: false}); + } else { + this.entityAggregationConfiguration.get('interval.durationSec').disable({emitEvent: false}); + } + } + + private checkIntervalDuration(allow: boolean) { + if (allow) { + this.entityAggregationConfiguration.get('interval.offsetSec').enable({emitEvent: false}); + } else { + this.entityAggregationConfiguration.get('interval.offsetSec').disable({emitEvent: false}); + this.hint = ''; + } + } + + private checkWatermark(allow: boolean) { + if (allow) { + this.entityAggregationConfiguration.get('watermark').enable({emitEvent: false}); + } else { + this.entityAggregationConfiguration.get('watermark').disable({emitEvent: false}); + } + } + + private updatedOffsetHint(): void { + const offset = this.entityAggregationConfiguration.get('interval.offsetSec').value; + const intervalType = this.entityAggregationConfiguration.get('interval.type').value as AggIntervalType; + const durationSec = this.entityAggregationConfiguration.get('interval.durationSec').value; + const offsetCategory = this.getTimeCategory(offset); + const now = _moment.utc(); + let interval: string = ''; + if (intervalType === AggIntervalType.CUSTOM) { + const durationSecCategory = this.getTimeCategory(durationSec); + const formatString = this.getCustomFormatString(offsetCategory, durationSecCategory); + const intervals: string[] = []; + let allInterval = durationSec >= HOUR*6/SECOND && durationSec < DAY/SECOND; + now.startOf('year').add(offset, 'seconds'); + + let repeat = 2; + if (allInterval) { + repeat = Math.floor(DAY/SECOND/durationSec); + if (repeat > 4) { + repeat = 2; + allInterval = false; + } + } + + for (let i = 0; i < repeat; i++) { + const s1 = now.clone().add(i * durationSec, 'seconds').format(formatString); + const s2 = now.clone().add((i + 1) * durationSec, 'seconds').format(formatString); + intervals.push(`${s1} - ${s2}`); + } + interval = intervals.join('; '); + + if (allInterval) { + this.hint = this.translate.instant('calculated-fields.aggregate-period-hint-offset', {interval}); + } else { + interval += '…' + this.hint = this.translate.instant('calculated-fields.aggregate-period-hint-offset-and-so-on', {interval}); + } + } else { + interval = this.buildStandardIntervalString(now, intervalType, offset, offsetCategory); + this.hint = this.translate.instant('calculated-fields.aggregate-period-hint-offset-and-so-on', { interval }); + } + } + + private getTimeCategory(seconds: number): TimeCategory { + if (seconds % (DAY / SECOND) === 0) { + return TimeCategory.DAYS; + } + if (seconds % (HOUR / SECOND) === 0) { + return TimeCategory.HOURS; + } + if (seconds % (MINUTE / SECOND) === 0) { + return TimeCategory.MINUTES; + } + return TimeCategory.SECONDS; + } + + private getCustomFormatString(offsetCat: TimeCategory, durationCat: TimeCategory): string { + if (durationCat === TimeCategory.DAYS) { + if (offsetCat === TimeCategory.SECONDS) { + return '[Day] D, HH:mm:ss'; + } + if (offsetCat === TimeCategory.MINUTES || offsetCat === TimeCategory.HOURS) { + return '[Day] D, HH:mm'; + } + return '[Day] D'; + } else { + if (offsetCat === TimeCategory.SECONDS) { + return 'HH:mm:ss'; + } + return 'HH:mm'; + } + } + + private formatAdditiveInterval(now: _moment.Moment, addUnit: 'hour' | 'day' | 'month' | 'quarter', offsetCat: TimeCategory, + formats: { [key in TimeCategory]?: { s1: string, s2: string, s3: string } }): string { + const formatTs = formats[offsetCat] || formats[TimeCategory.SECONDS]; + + if (!formatTs) { + return ''; + } + + const s1 = now.format(formatTs.s1); + const s2 = now.clone().add(1, addUnit).format(formatTs.s2); + const s3 = now.clone().add(2, addUnit).format(formatTs.s3); + + return `${s1} - ${s2}; ${s2} - ${s3}…`; + } + + private formatNextInterval(now: _moment.Moment, offsetCat: TimeCategory, secFmt: string, minHourFmt: string, dayFmt: string): string { + let s1: string; + if (offsetCat === TimeCategory.SECONDS) { + s1 = now.format(secFmt); + } else if (offsetCat === TimeCategory.MINUTES || offsetCat === TimeCategory.HOURS) { + s1 = now.format(minHourFmt); + } else { + s1 = now.format(dayFmt); + } + + const s2 = `Next ${s1}`; + const s3 = `Following ${s1}`; + return `${s1} - ${s2}; ${s2} - ${s3}… `; + } + + private buildStandardIntervalString(now: _moment.Moment, type: AggIntervalType, offset: number, offsetCat: TimeCategory): string { + switch (type) { + case AggIntervalType.HOUR: + now.startOf('day').add(offset, 'seconds'); + return this.formatAdditiveInterval(now, 'hour', offsetCat, { + [TimeCategory.SECONDS]: { s1: 'HH:mm:ss', s2: 'HH:mm:ss', s3: 'HH:mm:ss' }, + [TimeCategory.MINUTES]: { s1: 'HH:mm:ss', s2: 'HH:mm', s3: 'HH:mm' } + }); + + case AggIntervalType.DAY: + now.startOf('month').add(offset, 'seconds'); + return this.formatAdditiveInterval(now, 'day', offsetCat, { + [TimeCategory.SECONDS]: { s1: '[Day] D, HH:mm:ss', s2: '[Day] D, HH:mm:ss', s3: '[Day] D, HH:mm:ss' }, + [TimeCategory.MINUTES]: { s1: '[Day] D, HH:mm:ss', s2: '[Day] D, HH:mm', s3: '[Day] D, HH:mm' }, + [TimeCategory.HOURS]: { s1: 'HH:mm:ss', s2: '[Day] D, HH:mm', s3: '[Day] D, HH:mm' } // Note: Original logic, s1 format is different + }); + + case AggIntervalType.WEEK: + now.isoWeekday(1).startOf('isoWeek').add(offset, 'seconds'); + return this.formatNextInterval(now, offsetCat, 'ddd, HH:mm:ss', 'ddd, HH:mm', 'ddd'); + + case AggIntervalType.WEEK_SUN_SAT: + now.startOf('week').add(offset, 'seconds'); + return this.formatNextInterval(now, offsetCat, 'ddd, HH:mm:ss', 'ddd, HH:mm', 'ddd'); + + case AggIntervalType.MONTH: + now.startOf('year').add(offset, 'seconds'); + return this.formatAdditiveInterval(now, 'month', offsetCat, { + [TimeCategory.SECONDS]: { s1: 'Do [of month], HH:mm:ss', s2: '[Next] Do, HH:mm:ss', s3: '[Following] Do, HH:mm:ss' }, + [TimeCategory.MINUTES]: { s1: 'Do [of month], HH:mm', s2: '[Next] Do, HH:mm', s3: '[Following] Do, HH:mm' }, + [TimeCategory.HOURS]: { s1: 'Do [of month], HH:mm', s2: '[Next] Do, HH:mm', s3: '[Following] Do, HH:mm' }, + [TimeCategory.DAYS]: { s1: 'Do [of month]', s2: '[Next] Do', s3: '[Following] Do' } + }); + + case AggIntervalType.QUARTER: + now.startOf('year').add(offset, 'seconds'); + return this.formatAdditiveInterval(now, 'quarter', offsetCat, { + [TimeCategory.SECONDS]: { s1: 'MMM Do, HH:mm:ss', s2: 'MMM Do, HH:mm:ss', s3: 'MMM Do, HH:mm:ss' }, + [TimeCategory.MINUTES]: { s1: 'MMM Do, HH:mm', s2: 'MMM Do, HH:mm', s3: 'MMM Do, HH:mm' }, + [TimeCategory.HOURS]: { s1: 'MMM Do, HH:mm', s2: 'MMM Do, HH:mm', s3: 'MMM Do, HH:mm' }, + [TimeCategory.DAYS]: { s1: 'MMM Do', s2: 'MMM Do', s3: 'MMM Do' } + }); + + case AggIntervalType.YEAR: + now.startOf('year').add(offset, 'seconds'); + return this.formatNextInterval(now, offsetCat, 'MMM Do, HH:mm:ss', 'MMM Do, HH:mm', 'MMM Do'); + + default: + return ''; + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/entity-aggregation-configuration/entity-aggregation-component.module.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/entity-aggregation-configuration/entity-aggregation-component.module.ts new file mode 100644 index 0000000000..44bbbc7237 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/entity-aggregation-configuration/entity-aggregation-component.module.ts @@ -0,0 +1,49 @@ +/// +/// Copyright © 2016-2025 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { SharedModule } from '@shared/shared.module'; +import { + CalculatedFieldOutputModule +} from '@home/components/calculated-fields/components/output/calculated-field-output.module'; +import { + CalculatedFieldArgumentsTableModule +} from '@home/components/calculated-fields/components/calculated-field-arguments/calculated-field-arguments-table.module'; +import { + EntityAggregationComponentComponent +} from '@home/components/calculated-fields/components/entity-aggregation-configuration/entity-aggregation-component.component'; +import { + CalculatedFieldMetricsTableModule +} from '@home/components/calculated-fields/components/metrics/calculated-field-metrics-table.module'; + +@NgModule({ + imports: [ + CommonModule, + SharedModule, + CalculatedFieldOutputModule, + CalculatedFieldArgumentsTableModule, + CalculatedFieldMetricsTableModule, + ], + declarations: [ + EntityAggregationComponentComponent, + ], + exports: [ + EntityAggregationComponentComponent, + ] +}) +export class EntityAggregationComponentModule { +} diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-panel.component.html b/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-panel.component.html new file mode 100644 index 0000000000..1f5444f8b3 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-panel.component.html @@ -0,0 +1,268 @@ + +
    +
    {{ 'calculated-fields.geofencing-zone-groups-settings' | translate }}
    +
    +
    +
    {{ 'calculated-fields.name' | translate }}
    + + + @if (geofencingFormGroup.get('name').touched && geofencingFormGroup.get('name').hasError('required')) { + + warning + + } @else if (geofencingFormGroup.get('name').touched && geofencingFormGroup.get('name').hasError('duplicateName')) { + + warning + + } @else if (geofencingFormGroup.get('name').touched && geofencingFormGroup.get('name').hasError('pattern')) { + + warning + + } @else if (geofencingFormGroup.get('name').touched && geofencingFormGroup.get('name').hasError('maxlength')) { + + warning + + } @else if (geofencingFormGroup.get('name').touched && geofencingFormGroup.get('name').hasError('forbiddenName')) { + + warning + + } + +
    + +
    +
    {{ 'entity.entity-type' | translate }}
    + + + @for (type of argumentEntityTypes; track type) { + {{ ArgumentEntityTypeTranslations.get(type) | translate }} + } + + +
    + @if (ArgumentEntityTypeParamsMap.has(entityType)) { +
    +
    {{ ArgumentEntityTypeParamsMap.get(entityType).title | translate }}
    + +
    + } +
    + +
    + + {{ 'calculated-fields.entity-zone-relationship' | translate }} +
    +
    +
    +
    calculated-fields.level
    +
    calculated-fields.direction-level
    +
    calculated-fields.relation-type
    +
    +
    + @if (levelsFormArray()?.controls?.length) { +
    + @for (keyControl of levelsFormArray().controls; track trackByKey; ) { +
    +
    + +
    +
    +
    {{ $index + 1 }}
    + + + @for (direction of GeofencingDirectionList; track direction) { + {{ GeofencingDirectionLevelTranslations.get(direction) | translate }} + } + + + + +
    +
    + +
    +
    + } +
    + } @else { + {{ 'calculated-fields.no-level' | translate }} + } + @if (levelsFormArray().errors) { + + } +
    +
    + @if (maxRelationLevelPerCfArgument && levelsFormArray().length >= maxRelationLevelPerCfArgument) { +
    + warning + {{ 'calculated-fields.max-allowed-levels-error' | translate }} +
    + } @else { + + } +
    +
    +
    +
    + + @if (entityFilter.singleEntity?.id || entityType === ArgumentEntityType.RelationQuery || entityType === ArgumentEntityType.Current) { +
    +
    + {{ 'calculated-fields.perimeter-attribute-key' | translate }} +
    + @if (entityType === ArgumentEntityType.RelationQuery) { + + + @if (geofencingFormGroup.get('perimeterKeyName').touched && geofencingFormGroup.get('perimeterKeyName').hasError('required')) { + + warning + + } @else if (geofencingFormGroup.get('perimeterKeyName').touched && geofencingFormGroup.get('perimeterKeyName').hasError('pattern')) { + + warning + + } + + } @else { + + } +
    + } +
    +
    {{ 'calculated-fields.report-strategy' | translate }}
    + + + @for (strategy of GeofencingReportStrategyList; track strategy) { + {{ GeofencingReportStrategyTranslations.get(strategy) | translate }} + } + + +
    +
    +
    + +
    + {{ 'calculated-fields.create-relation-with-matched-zones' | translate }} +
    +
    +
    +
    {{ 'calculated-fields.direction' | translate }}
    + + + @for (direction of GeofencingDirectionList; track direction) { + {{ GeofencingDirectionTranslations.get(direction) | translate }} + } + + +
    +
    +
    {{ 'calculated-fields.relation-type' | translate }}
    + + +
    +
    +
    +
    + + +
    +
    diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-panel.component.scss b/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-panel.component.scss new file mode 100644 index 0000000000..4963e67a2b --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-panel.component.scss @@ -0,0 +1,72 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@import '../../../../../../../scss/constants'; + +:host { + .level-text { + display: flex; + justify-content: center; + width: 25px; + color: rgba(0, 0, 0, 0.54); + } + + .tb-form-table { + .tb-form-row { + gap: 12px; + } + + .tb-form-table-body { + gap: unset; + } + + .tb-form-table-header { + padding: 0; + } + + .tb-form-table-header-cell { + &.tb-actions-header { + width: 40px; + min-width: 40px; + } + } + } + + .max-args-warning { + .mat-icon { + color: #FAA405; + } + } + + .limit-field-row { + @media screen and (max-width: 520px) { + display: flex; + flex-direction: column; + + .fixed-title-width { + align-self: flex-start; + padding-top: 8px; + } + } + } +} + +:host ::ng-deep { + tb-entity-autocomplete { + .mat-mdc-form-field-has-icon-suffix .mat-mdc-text-field-wrapper { + padding-right: 0 !important; + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-panel.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-panel.component.ts new file mode 100644 index 0000000000..5f3e00a0cf --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-panel.component.ts @@ -0,0 +1,322 @@ +/// +/// Copyright © 2016-2025 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { AfterViewInit, ChangeDetectorRef, Component, Input, OnInit, output, ViewChild } from '@angular/core'; +import { TbPopoverComponent } from '@shared/components/popover.component'; +import { + AbstractControl, + FormBuilder, + FormControl, + FormGroup, + UntypedFormArray, + ValidatorFn, + Validators +} from '@angular/forms'; +import { charsWithNumRegex, oneSpaceInsideRegex } from '@shared/models/regex.constants'; +import { + ArgumentEntityType, + ArgumentEntityTypeParamsMap, + ArgumentEntityTypeTranslations, + CalculatedFieldGeofencing, + CalculatedFieldGeofencingValue, + FORBIDDEN_NAMES, + forbiddenNamesValidator, + GeofencingDirectionLevelTranslations, + GeofencingDirectionTranslations, + GeofencingReportStrategy, + GeofencingReportStrategyTranslations, + getCalculatedFieldCurrentEntityFilter, + uniqueNameValidator +} from '@shared/models/calculated-field.models'; +import { debounceTime, distinctUntilChanged, map } from 'rxjs/operators'; +import { EntityType } from '@shared/models/entity-type.models'; +import { AttributeScope, DataKeyType } from '@shared/models/telemetry/telemetry.models'; +import { EntityId } from '@shared/models/id/entity-id'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { EntityFilter } from '@shared/models/query/query.models'; +import { AliasFilterType } from '@shared/models/alias.models'; +import { BehaviorSubject, merge, Observable, of } from 'rxjs'; +import { getCurrentAuthState } from '@core/auth/auth.selectors'; +import { AppState } from '@core/core.state'; +import { Store } from '@ngrx/store'; +import { EntityAutocompleteComponent } from '@shared/components/entity/entity-autocomplete.component'; +import { NULL_UUID } from '@shared/models/id/has-uuid'; +import { EntitySearchDirection } from '@shared/models/relation.models'; +import { CdkDragDrop } from "@angular/cdk/drag-drop"; + +@Component({ + selector: 'tb-calculated-field-geofencing-zone-groups-panel', + templateUrl: './calculated-field-geofencing-zone-groups-panel.component.html', + styleUrls: ['../common/calculated-field-panel.scss', './calculated-field-geofencing-zone-groups-panel.component.scss'] +}) +export class CalculatedFieldGeofencingZoneGroupsPanelComponent implements OnInit, AfterViewInit { + + @Input() buttonTitle: string; + @Input() zone: CalculatedFieldGeofencing; + @Input() entityId: EntityId; + @Input() tenantId: string; + @Input() entityName: string; + @Input() ownerId: EntityId; + @Input() usedNames: string[]; + + @ViewChild('entityAutocomplete') entityAutocomplete: EntityAutocompleteComponent; + + geofencingDataApplied = output(); + + readonly maxRelationLevelPerCfArgument = getCurrentAuthState(this.store).maxRelationLevelPerCfArgument; + + geofencingFormGroup = this.fb.group({ + name: ['', [Validators.required, forbiddenNamesValidator(FORBIDDEN_NAMES), Validators.pattern(charsWithNumRegex), Validators.maxLength(255)]], + refEntityId: this.fb.group({ + entityType: [ArgumentEntityType.Current], + id: [''] + }), + refDynamicSourceConfiguration: this.fb.group({ + levels: this.fb.array([], [this.levelsRequired()]) + }), + perimeterKeyName: ['', [Validators.required, Validators.pattern(oneSpaceInsideRegex)]], + reportStrategy: [GeofencingReportStrategy.REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS], + createRelationsWithMatchedZones: [false], + direction: [EntitySearchDirection.TO], + relationType: ['', [Validators.required]] + }); + + entityFilter: EntityFilter; + entityNameSubject = new BehaviorSubject(null); + + readonly ArgumentEntityType = ArgumentEntityType; + readonly argumentEntityTypes = Object.values(ArgumentEntityType) as ArgumentEntityType[]; + readonly ArgumentEntityTypeTranslations = ArgumentEntityTypeTranslations; + readonly DataKeyType = DataKeyType; + readonly ArgumentEntityTypeParamsMap = ArgumentEntityTypeParamsMap; + readonly GeofencingReportStrategyList = Object.values(GeofencingReportStrategy) as Array; + readonly GeofencingReportStrategyTranslations = GeofencingReportStrategyTranslations; + readonly GeofencingDirectionList = Object.values(EntitySearchDirection) as Array; + readonly GeofencingDirectionTranslations = GeofencingDirectionTranslations; + readonly GeofencingDirectionLevelTranslations = GeofencingDirectionLevelTranslations; + readonly AttributeScope = AttributeScope; + + private currentEntityFilter: EntityFilter; + + constructor( + private fb: FormBuilder, + private cd: ChangeDetectorRef, + private popover: TbPopoverComponent, + private store: Store + ) { + + this.observeEntityFilterChanges(); + this.observeEntityTypeChanges(); + this.observeCreateRelationZonesChanges(); + } + + get entityType(): ArgumentEntityType { + return this.geofencingFormGroup.get('refEntityId').get('entityType').value; + } + + get refEntityIdFormGroup(): FormGroup { + return this.geofencingFormGroup.get('refEntityId') as FormGroup; + } + + get refDynamicSourceFormGroup(): FormGroup { + return this.geofencingFormGroup.get('refDynamicSourceConfiguration') as FormGroup; + } + + ngOnInit(): void { + this.updatedFormValidators(); + this.geofencingFormGroup.patchValue(this.zone, {emitEvent: false}); + if (this.zone.refDynamicSourceConfiguration?.type) { + this.refEntityIdFormGroup.get('entityType').setValue(this.zone.refDynamicSourceConfiguration.type, {emitEvent: false}); + } + if (this.zone?.refDynamicSourceConfiguration?.levels?.length > 0) { + this.zone.refDynamicSourceConfiguration.levels.forEach(level => { + this.levelsFormArray().push(this.fb.group({ + direction: [level.direction], + relationType: [level.relationType, [Validators.required]] + })); + }) + } else { + this.addKey(); + } + this.validateDirectionAndRelationType(this.zone?.createRelationsWithMatchedZones); + this.validateRefDynamicSourceConfiguration(this.zone?.refEntityId?.entityType || this.zone?.refDynamicSourceConfiguration?.type); + + this.currentEntityFilter = getCalculatedFieldCurrentEntityFilter(this.entityName, this.entityId); + this.updateEntityFilter(this.zone.refEntityId?.entityType); + } + + fetchOptions(searchText: string): Observable> { + const search = searchText ? searchText?.toLowerCase() : ''; + return of(['Contains', 'Manages']).pipe(map(name => name?.filter(option => option.toLowerCase().includes(search)))); + } + + private updatedFormValidators(): void { + this.geofencingFormGroup.get('name').addValidators(uniqueNameValidator(this.usedNames)); + this.geofencingFormGroup.get('name').updateValueAndValidity({emitEvent: false}); + } + + private observeCreateRelationZonesChanges(): void { + this.geofencingFormGroup.get('createRelationsWithMatchedZones').valueChanges + .pipe(takeUntilDestroyed()) + .subscribe(value => this.validateDirectionAndRelationType(value)); + } + + private validateDirectionAndRelationType(createRelation = false): void { + if (createRelation) { + this.geofencingFormGroup.get('direction').enable({emitEvent: false}); + this.geofencingFormGroup.get('relationType').enable({emitEvent: false}); + } else { + this.geofencingFormGroup.get('direction').disable({emitEvent: false}); + this.geofencingFormGroup.get('relationType').disable({emitEvent: false}); + } + } + + private validateRefDynamicSourceConfiguration(type: ArgumentEntityType = ArgumentEntityType.Current): void { + if (type === ArgumentEntityType.RelationQuery) { + this.refDynamicSourceFormGroup.enable({emitEvent: false}); + } else { + this.refDynamicSourceFormGroup.disable({emitEvent: false}); + } + } + + ngAfterViewInit(): void { + if (this.zone.refEntityId?.id === NULL_UUID) { + this.entityAutocomplete.selectEntityFormGroup.get('entity').markAsTouched(); + } + } + + saveZone(): void { + const value = this.geofencingFormGroup.value as CalculatedFieldGeofencingValue; + const argumentType = value.refEntityId.entityType; + switch (argumentType) { + case ArgumentEntityType.Current: + delete value.refEntityId; + break; + case ArgumentEntityType.Owner: + delete value.refEntityId; + value.refDynamicSourceConfiguration = {type: ArgumentEntityType.Owner}; + break; + case ArgumentEntityType.RelationQuery: + delete value.refEntityId; + value.refDynamicSourceConfiguration.type = ArgumentEntityType.RelationQuery; + break; + case ArgumentEntityType.Tenant: + value.refEntityId.id = this.tenantId; + break + default: + value.entityName = this.entityNameSubject.value; + } + this.geofencingDataApplied.emit(value); + } + + cancel(): void { + this.popover.hide(); + } + + private updateEntityFilter(entityType: ArgumentEntityType = ArgumentEntityType.Current): void { + let entityFilter: EntityFilter; + switch (entityType) { + case ArgumentEntityType.Current: + case ArgumentEntityType.RelationQuery: + entityFilter = this.currentEntityFilter; + break; + case ArgumentEntityType.Owner: + entityFilter = { + type: AliasFilterType.singleEntity, + singleEntity: this.ownerId + }; + break; + case ArgumentEntityType.Tenant: + entityFilter = { + type: AliasFilterType.singleEntity, + singleEntity: { + id: this.tenantId, + entityType: EntityType.TENANT + }, + }; + break; + default: + entityFilter = { + type: AliasFilterType.singleEntity, + singleEntity: this.geofencingFormGroup.get('refEntityId').value as unknown as EntityId, + }; + } + this.entityFilter = entityFilter; + this.cd.markForCheck(); + } + + private observeEntityFilterChanges(): void { + merge( + this.refEntityIdFormGroup.get('entityType').valueChanges, + this.refEntityIdFormGroup.get('id').valueChanges, + ) + .pipe(debounceTime(50), takeUntilDestroyed()) + .subscribe(() => this.updateEntityFilter(this.entityType)); + + this.refEntityIdFormGroup.get('id').valueChanges.pipe(distinctUntilChanged(), takeUntilDestroyed()).subscribe(() => this.geofencingFormGroup.get('perimeterKeyName').reset('')); + } + + private observeEntityTypeChanges(): void { + this.refEntityIdFormGroup.get('entityType').valueChanges + .pipe(distinctUntilChanged(), takeUntilDestroyed()) + .subscribe(type => { + this.geofencingFormGroup.get('refEntityId').get('id').setValue(null); + const isEntityWithId = type !== ArgumentEntityType.Tenant && type !== ArgumentEntityType.Current && type !== ArgumentEntityType.RelationQuery; + this.geofencingFormGroup.get('refEntityId') + .get('id')[isEntityWithId ? 'enable' : 'disable'](); + if (!isEntityWithId) { + this.entityNameSubject.next(null); + } + this.validateRefDynamicSourceConfiguration(type); + }); + } + + private levelsRequired(): ValidatorFn { + return (control: FormControl) => { + return control.value.length ? null : { levelsRequired: true }; + }; + } + + levelsFormArray(): UntypedFormArray { + return this.refDynamicSourceFormGroup.get('levels') as UntypedFormArray; + } + + trackByKey(_index: number, keyControl: AbstractControl): any { + return keyControl; + } + + removeKey(index: number) { + this.levelsFormArray().removeAt(index); + } + + addKey() { + this.levelsFormArray().push(this.fb.group({ + direction: [EntitySearchDirection.TO], + relationType: ['', [Validators.required]] + })); + } + + keyDrop(event: CdkDragDrop) { + const keysArray = this.levelsFormArray(); + const key = keysArray.at(event.previousIndex); + keysArray.removeAt(event.previousIndex); + keysArray.insert(event.currentIndex, key); + } + + get dragEnabled(): boolean { + return this.levelsFormArray().controls.length > 1; + } +} diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-table.component.html b/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-table.component.html new file mode 100644 index 0000000000..dcfd37796d --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-table.component.html @@ -0,0 +1,148 @@ + +
    +
    + + + +
    {{ 'common.name' | translate }}
    +
    + +
    +
    {{ geofenceZone.name }}
    + +
    +
    +
    + + + {{ 'entity.entity-type' | translate }} + + +
    + @if (geofenceZone.refEntityId?.entityType === ArgumentEntityType.Tenant) { + {{ 'calculated-fields.argument-current-tenant' | translate }} + } @else if (geofenceZone.refDynamicSourceConfiguration?.type === ArgumentEntityType.Owner) { + {{ 'calculated-fields.argument-owner' | translate }} + } @else if (geofenceZone.refDynamicSourceConfiguration?.type === ArgumentEntityType.RelationQuery) { + {{ 'calculated-fields.argument-relation-query' | translate }} + } @else if (geofenceZone.refEntityId?.id) { + {{ entityTypeTranslations.get(geofenceZone.refEntityId.entityType).type | translate }} + } @else { + {{ 'calculated-fields.argument-current' | translate }} + } +
    +
    +
    + + + {{ 'calculated-fields.target-zone' | translate }} + + +
    + @if (geofenceZone.refEntityId?.id && geofenceZone.refEntityId?.entityType !== ArgumentEntityType.Tenant) { + + {{ entityNameMap.get(geofenceZone.refEntityId.id) ?? '' }} + + } +
    +
    +
    + + + + {{ 'calculated-fields.perimeter-key' | translate }} + + + +
    {{ geofenceZone.perimeterKeyName }}
    +
    +
    +
    + + + + {{ 'calculated-fields.report-strategy' | translate }} + + +
    {{ GeofencingReportStrategyTranslations.get(geofenceZone.reportStrategy) | translate }}
    +
    +
    + + + + +
    + + +
    +
    +
    + + +
    +
    + {{ 'calculated-fields.no-zone-configured' | translate }} +
    + @if (errorText) { + + } +
    +
    + + @if (maxArgumentsPerCF && zoneGroupsFormArray.length >= maxArgumentsPerCF) { +
    + warning + {{ 'calculated-fields.hint.max-geofencing-zone' | translate }} +
    + } +
    +
    diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-table.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-table.component.ts new file mode 100644 index 0000000000..9dc2e99465 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-table.component.ts @@ -0,0 +1,307 @@ +/// +/// Copyright © 2016-2025 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { + AfterViewInit, + ChangeDetectorRef, + Component, + DestroyRef, + forwardRef, + Input, + Renderer2, + ViewChild, + ViewContainerRef, +} from '@angular/core'; +import { + ControlValueAccessor, + FormBuilder, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + ValidationErrors, + Validator, +} from '@angular/forms'; +import { + ArgumentEntityType, + CalculatedFieldGeofencing, + CalculatedFieldGeofencingValue, + GeofencingReportStrategyTranslations, +} from '@shared/models/calculated-field.models'; +import { MatButton } from '@angular/material/button'; +import { TbPopoverService } from '@shared/components/popover.service'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { EntityId } from '@shared/models/id/entity-id'; +import { EntityType, entityTypeTranslations } from '@shared/models/entity-type.models'; +import { getEntityDetailsPageURL, isEqual } from '@core/utils'; +import { TbPopoverComponent } from '@shared/components/popover.component'; +import { TbTableDatasource } from '@shared/components/table/table-datasource.abstract'; +import { EntityService } from '@core/http/entity.service'; +import { MatSort } from '@angular/material/sort'; +import { getCurrentAuthState } from '@core/auth/auth.selectors'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { forkJoin, Observable } from 'rxjs'; +import { NULL_UUID } from '@shared/models/id/has-uuid'; +import { BaseData } from '@shared/models/base-data'; +import { + CalculatedFieldGeofencingZoneGroupsPanelComponent +} from '@home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-panel.component'; + +@Component({ + selector: 'tb-calculated-field-geofencing-zone-groups-table', + templateUrl: './calculated-field-geofencing-zone-groups-table.component.html', + styleUrls: [`../calculated-field-arguments/calculated-field-arguments-table.component.scss`], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => CalculatedFieldGeofencingZoneGroupsTableComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => CalculatedFieldGeofencingZoneGroupsTableComponent), + multi: true + } + ], +}) +export class CalculatedFieldGeofencingZoneGroupsTableComponent implements ControlValueAccessor, Validator, AfterViewInit { + + @Input({required: true}) entityId: EntityId; + @Input({required: true}) tenantId: string; + @Input({required: true}) entityName: string; + @Input({required: true}) ownerId: EntityId; + + @ViewChild(MatSort, { static: true }) sort: MatSort; + + errorText = ''; + zoneGroupsFormArray = this.fb.array([]); + entityNameMap = new Map(); + sortOrder = { direction: 'asc', property: '' }; + dataSource = new CalculatedFieldZoneDatasource(); + + readonly GeofencingReportStrategyTranslations = GeofencingReportStrategyTranslations; + readonly entityTypeTranslations = entityTypeTranslations; + readonly ArgumentEntityType = ArgumentEntityType; + readonly maxArgumentsPerCF = getCurrentAuthState(this.store).maxArgumentsPerCF - 2; + readonly NULL_UUID = NULL_UUID; + + private popoverComponent: TbPopoverComponent; + private propagateChange: (zonesObj: Record) => void = () => {}; + + constructor( + private fb: FormBuilder, + private popoverService: TbPopoverService, + private viewContainerRef: ViewContainerRef, + private cd: ChangeDetectorRef, + private renderer: Renderer2, + private entityService: EntityService, + private destroyRef: DestroyRef, + private store: Store + ) { + this.zoneGroupsFormArray.valueChanges.pipe(takeUntilDestroyed()).subscribe(value => { + this.updateDataSource(value); + this.propagateChange(this.getZonesObject(value)); + }); + } + + ngAfterViewInit(): void { + this.sort.sortChange.asObservable().pipe(takeUntilDestroyed(this.destroyRef)).subscribe(() => { + this.sortOrder.property = this.sort.active; + this.sortOrder.direction = this.sort.direction; + this.updateDataSource(this.zoneGroupsFormArray.value); + }); + } + + registerOnChange(fn: (zonesObj: Record) => void): void { + this.propagateChange = fn; + } + + registerOnTouched(_): void {} + + validate(): ValidationErrors | null { + this.updateErrorText(); + return this.errorText ? { zonesFormArray: false } : null; + } + + onDelete($event: Event, zone: CalculatedFieldGeofencingValue): void { + $event.stopPropagation(); + const index = this.zoneGroupsFormArray.controls.findIndex(control => isEqual(control.value, zone)); + this.zoneGroupsFormArray.removeAt(index); + this.zoneGroupsFormArray.markAsDirty(); + } + + manageZone($event: Event, matButton: MatButton, zone = {} as CalculatedFieldGeofencingValue): void { + $event?.stopPropagation(); + if (this.popoverComponent && !this.popoverComponent.tbHidden) { + this.popoverComponent.hide(); + } + const trigger = matButton._elementRef.nativeElement; + if (this.popoverService.hasPopover(trigger)) { + this.popoverService.hidePopover(trigger); + } else { + const index = this.zoneGroupsFormArray.controls.findIndex(control => isEqual(control.value, zone)); + const isExists = index !== -1; + const ctx = { + index, + zone, + entityId: this.entityId, + buttonTitle: isExists ? 'action.apply' : 'action.add', + tenantId: this.tenantId, + entityName: this.entityName, + ownerId: this.ownerId, + usedNames: this.zoneGroupsFormArray.value.map(({ name }) => name).filter(name => name !== zone.name), + }; + this.popoverComponent = this.popoverService.displayPopover({ + trigger, + renderer: this.renderer, + componentType: CalculatedFieldGeofencingZoneGroupsPanelComponent, + hostView: this.viewContainerRef, + preferredPlacement: isExists ? ['leftOnly', 'leftTopOnly', 'leftBottomOnly'] : ['rightOnly', 'rightTopOnly', 'rightBottomOnly'], + context: ctx, + isModal: true + }); + this.popoverComponent.tbComponentRef.instance.geofencingDataApplied.subscribe(({ entityName, ...value }) => { + this.popoverComponent.hide(); + if (entityName) { + this.entityNameMap.set(value.refEntityId.id, entityName); + } + if (isExists) { + this.zoneGroupsFormArray.at(index).setValue(value); + } else { + this.zoneGroupsFormArray.push(this.fb.control(value)); + } + this.cd.markForCheck(); + }); + } + } + + private updateDataSource(value: CalculatedFieldGeofencingValue[]): void { + const sortedValue = this.sortData(value); + this.dataSource.loadData(sortedValue); + } + + private updateErrorText(): void { + if (this.zoneGroupsFormArray.controls.some(control => control.value.refEntityId?.id === NULL_UUID)) { + this.errorText = 'calculated-fields.hint.geofencing-entity-not-found'; + } else if (!this.zoneGroupsFormArray.controls.length) { + this.errorText = 'calculated-fields.hint.geofencing-empty'; + } else { + this.errorText = ''; + } + } + + private getZonesObject(value: CalculatedFieldGeofencingValue[]): Record { + return value.reduce((acc, zoneValue) => { + const { name, ...zone } = zoneValue as CalculatedFieldGeofencingValue; + acc[name] = zone; + return acc; + }, {} as Record); + } + + writeValue(zonesObj: Record): void { + this.zoneGroupsFormArray.clear(); + this.populateZonesFormArray(zonesObj); + this.updateEntityNameMap(this.zoneGroupsFormArray.value); + } + + getEntityDetailsPageURL(id: string, type: EntityType): string { + return getEntityDetailsPageURL(id, type); + } + + private populateZonesFormArray(zonesObj: Record): void { + Object.keys(zonesObj).forEach(key => { + const value: CalculatedFieldGeofencingValue = { + ...zonesObj[key], + name: key + }; + this.zoneGroupsFormArray.push(this.fb.control(value), { emitEvent: false }); + }); + this.zoneGroupsFormArray.updateValueAndValidity(); + } + + private updateEntityNameMap(values: CalculatedFieldGeofencingValue[]): void { + const entitiesByType = values.reduce((acc, { refEntityId = {}}) => { + if (refEntityId.id && refEntityId.entityType !== ArgumentEntityType.Tenant) { + const { id, entityType } = refEntityId as EntityId; + acc[entityType] = acc[entityType] ?? []; + acc[entityType].push(id); + } + return acc; + }, {} as Record); + const tasks = Object.entries(entitiesByType).map(([entityType, ids]) => + this.entityService.getEntities(entityType as EntityType, ids) + ); + if (!tasks.length) { + return; + } + this.fetchEntityNames(tasks, values); + } + + private fetchEntityNames(tasks: Observable[]>[], values: CalculatedFieldGeofencingValue[]): void { + forkJoin(tasks as Observable[]>[]) + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe((result: Array>[]) => { + result.forEach((entities: BaseData[]) => entities.forEach((entity: BaseData) => this.entityNameMap.set(entity.id.id, entity.name))); + let updateTable = false; + values.forEach(({ refEntityId }) => { + if (refEntityId?.id && !this.entityNameMap.has(refEntityId.id) && refEntityId.entityType !== ArgumentEntityType.Tenant) { + updateTable = true; + const control = this.zoneGroupsFormArray.controls.find(control => control.value.refEntityId?.id === refEntityId.id); + const value = control.value; + value.refEntityId.id = NULL_UUID; + control.setValue(value, { emitEvent: false }); + } + }); + if (updateTable) { + this.zoneGroupsFormArray.updateValueAndValidity(); + } + }); + } + + private getSortValue(zone: CalculatedFieldGeofencingValue, column: string): string { + switch (column) { + case 'entityType': + if (zone.refEntityId?.entityType === ArgumentEntityType.Tenant) { + return 'calculated-fields.argument-current-tenant'; + } else if (zone.refDynamicSourceConfiguration.type === ArgumentEntityType.RelationQuery) { + return 'calculated-fields.argument-relation-query'; + } else if (zone.refEntityId?.id) { + return entityTypeTranslations.get((zone.refEntityId)?.entityType as unknown as EntityType).type; + } else { + return 'calculated-fields.argument-current'; + } + case 'key': + return zone.perimeterKeyName; + case 'reportStrategy': + return GeofencingReportStrategyTranslations.get(zone.reportStrategy); + default: + return zone.name; + } + } + + private sortData(data: CalculatedFieldGeofencingValue[]): CalculatedFieldGeofencingValue[] { + return data.sort((a, b) => { + const valA = this.getSortValue(a, this.sortOrder.property) ?? ''; + const valB = this.getSortValue(b, this.sortOrder.property) ?? ''; + return (this.sortOrder.direction === 'asc' ? 1 : -1) * valA.localeCompare(valB); + }); + } +} + +class CalculatedFieldZoneDatasource extends TbTableDatasource { + constructor() { + super(); + } +} diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/geofencing-configuration.component.html b/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/geofencing-configuration.component.html new file mode 100644 index 0000000000..e0580be7f1 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/geofencing-configuration.component.html @@ -0,0 +1,69 @@ + +
    +
    +
    + {{ 'calculated-fields.entity-coordinates' | translate }} +
    +
    + + +
    +
    + +
    +
    + {{ 'calculated-fields.geofencing-zone-groups' | translate }} +
    + +
    + +
    + {{ 'calculated-fields.zone-group-refresh-interval' | translate }} +
    +
    +
    + + +
    +
    +
    + + +
    diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/geofencing-configuration.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/geofencing-configuration.component.ts new file mode 100644 index 0000000000..846b063042 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/geofencing-configuration.component.ts @@ -0,0 +1,161 @@ +/// +/// Copyright © 2016-2025 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, forwardRef, Input, OnInit } from '@angular/core'; +import { + ControlValueAccessor, + FormBuilder, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + ValidationErrors, + Validator, + Validators +} from '@angular/forms'; +import { + ArgumentEntityType, + CalculatedFieldGeofencing, + CalculatedFieldGeofencingConfiguration, + CalculatedFieldOutput, + CalculatedFieldType, + getCalculatedFieldCurrentEntityFilter, + notEmptyObjectValidator, + OutputType +} from '@shared/models/calculated-field.models'; +import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; +import { getCurrentAuthState } from '@core/auth/auth.selectors'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { EntityFilter } from '@shared/models/query/query.models'; +import { EntityId } from '@shared/models/id/entity-id'; + +@Component({ + selector: 'tb-geofencing-configuration', + templateUrl: './geofencing-configuration.component.html', + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => GeofencingConfigurationComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => GeofencingConfigurationComponent), + multi: true + } + ], +}) +export class GeofencingConfigurationComponent implements ControlValueAccessor, Validator, OnInit { + + @Input({required: true}) + entityId: EntityId; + + @Input({required: true}) + tenantId: string; + + @Input({required: true}) + entityName: string; + + @Input({required: true}) + ownerId: EntityId; + + readonly minAllowedScheduledUpdateIntervalInSecForCF = getCurrentAuthState(this.store).minAllowedScheduledUpdateIntervalInSecForCF; + readonly DataKeyType = DataKeyType; + + geofencingConfiguration = this.fb.group({ + entityCoordinates: this.fb.group({ + latitudeKeyName: [null, [Validators.required]], + longitudeKeyName: [null, [Validators.required]], + }), + zoneGroups: this.fb.control>({}, notEmptyObjectValidator()), + scheduledUpdateEnabled: [true], + scheduledUpdateInterval: [this.minAllowedScheduledUpdateIntervalInSecForCF], + output: this.fb.control({type: OutputType.Timeseries}) + }); + + currentEntityFilter: EntityFilter; + isRelatedEntity: boolean; + + private propagateChange: (config: CalculatedFieldGeofencingConfiguration) => void = () => { }; + + constructor(private fb: FormBuilder, + private store: Store) { + + this.geofencingConfiguration.get('zoneGroups').valueChanges + .pipe(takeUntilDestroyed()) + .subscribe((zoneGroups: Record) => + this.checkRelatedEntity(zoneGroups) + ); + + this.geofencingConfiguration.get('scheduledUpdateEnabled').valueChanges + .pipe(takeUntilDestroyed()) + .subscribe((value: boolean) => + this.checkScheduledUpdateEnabled(value) + ); + + this.geofencingConfiguration.valueChanges.pipe( + takeUntilDestroyed() + ).subscribe(() => { + this.updatedModel(this.geofencingConfiguration.getRawValue() as any); + }) + } + + ngOnInit() { + this.currentEntityFilter = getCalculatedFieldCurrentEntityFilter(this.entityName, this.entityId); + } + + validate(): ValidationErrors | null { + return this.geofencingConfiguration.valid || this.geofencingConfiguration.disabled ? null : { geofencingConfigError: false }; + } + + writeValue(config: CalculatedFieldGeofencingConfiguration): void { + this.geofencingConfiguration.patchValue(config, {emitEvent: false}); + this.checkRelatedEntity(this.geofencingConfiguration.get('zoneGroups').value); + this.checkScheduledUpdateEnabled(this.geofencingConfiguration.get('scheduledUpdateEnabled').value); + } + + registerOnChange(fn: (config: CalculatedFieldGeofencingConfiguration) => void): void { + this.propagateChange = fn; + } + + registerOnTouched(_: any): void { } + + setDisabledState(isDisabled: boolean): void { + if (isDisabled) { + this.geofencingConfiguration.disable({emitEvent: false}); + } else { + this.geofencingConfiguration.enable({emitEvent: false}); + this.checkScheduledUpdateEnabled(this.geofencingConfiguration.get('scheduledUpdateEnabled').value); + } + } + + private updatedModel(value: CalculatedFieldGeofencingConfiguration) { + value.type = CalculatedFieldType.GEOFENCING; + this.propagateChange(value) + } + + private checkScheduledUpdateEnabled(value: boolean) { + if (value) { + this.geofencingConfiguration.get('scheduledUpdateInterval').enable({emitEvent: false}); + } else { + this.geofencingConfiguration.get('scheduledUpdateInterval').disable({emitEvent: false}); + } + } + + private checkRelatedEntity(zoneGroups: Record) { + this.isRelatedEntity = Object.values(zoneGroups).some(zone => zone.refDynamicSourceConfiguration?.type === ArgumentEntityType.RelationQuery); + } +} diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/geofencing-configuration.module.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/geofencing-configuration.module.ts new file mode 100644 index 0000000000..e270dd29cb --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/geofencing-configuration.module.ts @@ -0,0 +1,52 @@ +/// +/// Copyright © 2016-2025 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { + CalculatedFieldGeofencingZoneGroupsTableComponent +} from '@home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-table.component'; +import { + CalculatedFieldGeofencingZoneGroupsPanelComponent +} from '@home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-panel.component'; +import { SharedModule } from '@shared/shared.module'; +import { + GeofencingConfigurationComponent +} from '@home/components/calculated-fields/components/geofencing-configuration/geofencing-configuration.component'; +import { + CalculatedFieldOutputModule +} from '@home/components/calculated-fields/components/output/calculated-field-output.module'; + +@NgModule({ + imports: [ + CommonModule, + SharedModule, + CalculatedFieldOutputModule + ], + declarations: [ + CalculatedFieldGeofencingZoneGroupsTableComponent, + CalculatedFieldGeofencingZoneGroupsPanelComponent, + GeofencingConfigurationComponent + ], + exports: [ + CalculatedFieldGeofencingZoneGroupsTableComponent, + CalculatedFieldGeofencingZoneGroupsPanelComponent, + GeofencingConfigurationComponent + ] +}) +export class GeofencingConfigurationModule { + +} diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/metrics/calculated-field-metrics-panel.component.html b/ui-ngx/src/app/modules/home/components/calculated-fields/components/metrics/calculated-field-metrics-panel.component.html new file mode 100644 index 0000000000..eda8a779ad --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/metrics/calculated-field-metrics-panel.component.html @@ -0,0 +1,187 @@ + +
    +
    {{ 'calculated-fields.metrics.metric-settings' | translate }}
    +
    +
    +
    {{ 'calculated-fields.metrics.metric-name' | translate }}
    + + + @if (metricForm.get('name').touched && metricForm.get('name').hasError('required')) { + + warning + + } @else if (metricForm.get('name').touched && metricForm.get('name').hasError('duplicateName')) { + + warning + + } @else if (metricForm.get('name').touched && metricForm.get('name').hasError('pattern')) { + + warning + + } @else if (metricForm.get('name').touched && metricForm.get('name').hasError('maxlength')) { + + warning + + } @else if (metricForm.get('name').touched && metricForm.get('name').hasError('forbiddenName')) { + + warning + + } + +
    +
    +
    {{ 'calculated-fields.metrics.aggregation' | translate }}
    + + + @for (aggFunction of AggFunctions; track aggFunction) { + {{ AggFunctionTranslations.get(aggFunction) | translate }} + } + + +
    + + @if (!simpleMode) { +
    + + + + +
    + {{ 'calculated-fields.metrics.filter' | translate }} +
    +
    +
    +
    + + +
    {{ 'api-usage.tbel' | translate }} +
    +
    +
    +
    +
    + } + + @if (!simpleMode) { +
    +
    {{ 'calculated-fields.metrics.value-source' | translate }}
    + + + @for (inputType of AggInputTypes; track inputType) { + {{ AggInputTypeTranslations.get(inputType) | translate }} + } + + +
    + } + @if (this.metricForm.get('input.type').value === AggInputType.key) { +
    +
    {{ 'calculated-fields.argument-name' | translate }}
    + + + @for (argument of arguments; track argument) { + {{ argument }} + } + + @if (metricForm.get('input.key').touched && metricForm.get('input.key').hasError('required')) { + + warning + + } + +
    + } @else { + +
    {{ 'api-usage.tbel' | translate }} +
    +
    + } +
    + @if (simpleMode) { +
    +
    {{ 'calculated-fields.default-value' | translate }}
    + + + +
    + } +
    +
    + + +
    +
    diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/metrics/calculated-field-metrics-panel.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/metrics/calculated-field-metrics-panel.component.ts new file mode 100644 index 0000000000..4a843208c5 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/metrics/calculated-field-metrics-panel.component.ts @@ -0,0 +1,166 @@ +/// +/// Copyright © 2016-2025 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, Input, OnInit, output } from '@angular/core'; +import { TbPopoverComponent } from '@shared/components/popover.component'; +import { FormBuilder, Validators } from '@angular/forms'; +import { charsWithNumRegex } from '@shared/models/regex.constants'; +import { + AggFunction, + AggFunctionTranslations, + AggInputType, + AggInputTypeTranslations, + CalculatedFieldAggMetricValue, + FORBIDDEN_NAMES, + forbiddenNamesValidator, + uniqueNameValidator +} from '@shared/models/calculated-field.models'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { EntityFilter } from '@shared/models/query/query.models'; +import { ScriptLanguage } from '@shared/models/rule-node.models'; +import { TbEditorCompleter } from '@shared/models/ace/completion.models'; +import { AceHighlightRules } from '@shared/models/ace/ace.models'; + +interface CalculatedFieldAggMetricValuePanel extends CalculatedFieldAggMetricValue { + allowFilter: boolean; +} + +@Component({ + selector: 'tb-calculated-field-metrics-panel', + templateUrl: './calculated-field-metrics-panel.component.html', + styleUrl: '../common/calculated-field-panel.scss', +}) +export class CalculatedFieldMetricsPanelComponent implements OnInit { + + @Input() buttonTitle: string; + @Input() metric: CalculatedFieldAggMetricValue; + @Input() usedNames: string[]; + @Input() arguments: Array; + @Input() simpleMode: boolean; + @Input() editorCompleter: TbEditorCompleter; + @Input() highlightRules: AceHighlightRules; + + metricDataApplied = output(); + filterExpanded = false; + functionArgs: Array + + metricForm = this.fb.group({ + name: ['', [Validators.required, forbiddenNamesValidator(FORBIDDEN_NAMES), Validators.pattern(charsWithNumRegex), Validators.maxLength(255)]], + function: [AggFunction.AVG], + allowFilter: [false], + filter: ['', Validators.required], + input: this.fb.group({ + type: [AggInputType.key], + key: ['', Validators.required], + function: ['', Validators.required], + }), + defaultValue: [null] + }); + + entityFilter: EntityFilter; + + readonly AggFunctions = Object.values(AggFunction) as AggFunction[]; + readonly AggFunctionTranslations = AggFunctionTranslations; + readonly ScriptLanguage = ScriptLanguage; + readonly AggInputType = AggInputType; + readonly AggInputTypes = Object.values(AggInputType) as AggInputType[]; + readonly AggInputTypeTranslations = AggInputTypeTranslations; + + constructor( + private fb: FormBuilder, + private popover: TbPopoverComponent + ) { + this.observeFilterAllowChange(); + this.observeInputTypeChange(); + } + + ngOnInit(): void { + this.updatedForm(); + + const data: CalculatedFieldAggMetricValuePanel = { + ...this.metric, + allowFilter: !!this.metric.filter, + } + this.metricForm.patchValue(data, {emitEvent: false}); + + this.validateFilter(data.allowFilter); + this.validateInputTypeFilter(data.input?.type ?? AggInputType.key); + this.validateInputKey(); + + this.functionArgs = ['ctx', ...this.arguments]; + } + + saveMetric(): void { + const value = this.metricForm.value as CalculatedFieldAggMetricValuePanel; + if (!value.allowFilter) { + delete value.filter; + } + delete value.allowFilter; + this.metricDataApplied.emit(value); + } + + cancel(): void { + this.popover.hide(); + } + + private updatedForm(): void { + this.metricForm.get('name').addValidators(uniqueNameValidator(this.usedNames)); + this.metricForm.get('name').updateValueAndValidity({emitEvent: false}); + + if (!this.simpleMode) { + this.metricForm.removeControl('defaultValue', {emitEvent: false}); + } + } + + private observeFilterAllowChange(): void { + this.metricForm.get('allowFilter').valueChanges + .pipe(takeUntilDestroyed()) + .subscribe(value => this.validateFilter(value)); + } + + private observeInputTypeChange(): void { + this.metricForm.get('input.type').valueChanges + .pipe(takeUntilDestroyed()) + .subscribe(value => this.validateInputTypeFilter(value)); + } + + private validateFilter(allowFilter = false): void { + if (allowFilter) { + this.metricForm.get('filter').enable({emitEvent: false}); + } else { + this.metricForm.get('filter').disable({emitEvent: false}); + } + this.filterExpanded = allowFilter; + } + + private validateInputTypeFilter(value: AggInputType): void { + const inputForm = this.metricForm.get('input'); + if (value === AggInputType.key) { + inputForm.get('key').enable({emitEvent: false}); + inputForm.get('function').disable({emitEvent: false}); + } else { + inputForm.get('key').disable({emitEvent: false}); + inputForm.get('function').enable({emitEvent: false}); + } + } + + private validateInputKey() { + if (this.metric.input?.type === AggInputType.key && !this.arguments.includes(this.metric.input.key)) { + this.metricForm.get('input.key').setValue(null); + this.metricForm.get('input.key').markAsTouched(); + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/metrics/calculated-field-metrics-table.component.html b/ui-ngx/src/app/modules/home/components/calculated-fields/components/metrics/calculated-field-metrics-table.component.html new file mode 100644 index 0000000000..67cfdfc122 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/metrics/calculated-field-metrics-table.component.html @@ -0,0 +1,125 @@ + +
    +
    + + + +
    {{ 'calculated-fields.metrics.metric-name' | translate }}
    +
    + +
    +
    {{ metric.name }}
    + +
    +
    +
    + + + {{ 'calculated-fields.metrics.aggregation' | translate }} + + +
    {{ AggFunctionTranslations.get(metric.function) | translate }}
    +
    +
    + + + {{ 'calculated-fields.metrics.filtered' | translate }} + + +
    + {{ metric.filter ? 'check_box' : 'check_box_outline_blank' }} +
    +
    +
    + + + {{ 'calculated-fields.metrics.value-source' | translate }} + + +
    {{ AggInputTypeTranslations.get(metric.input.type) | translate }}
    +
    +
    + + + + +
    + + +
    +
    +
    + + +
    +
    + {{ 'calculated-fields.metrics.no-metrics-configured' | translate }} +
    + @if (errorText) { + + } +
    +
    + + @if (maxArgumentsPerCF && metricsFormArray.length >= maxArgumentsPerCF) { +
    + warning + {{ 'calculated-fields.metrics.max-metrics' | translate }} +
    + } +
    +
    diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/metrics/calculated-field-metrics-table.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/metrics/calculated-field-metrics-table.component.ts new file mode 100644 index 0000000000..22adf9a801 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/metrics/calculated-field-metrics-table.component.ts @@ -0,0 +1,254 @@ +/// +/// Copyright © 2016-2025 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { + AfterViewInit, + booleanAttribute, + ChangeDetectorRef, + Component, + DestroyRef, + forwardRef, + Input, + OnInit, + Renderer2, + ViewChild, + ViewContainerRef, +} from '@angular/core'; +import { + ControlValueAccessor, + FormBuilder, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + ValidationErrors, + Validator, +} from '@angular/forms'; +import { + AggFunctionTranslations, + AggInputTypeTranslations, + CalculatedFieldAggMetric, + CalculatedFieldAggMetricValue, +} from '@shared/models/calculated-field.models'; +import { MatButton } from '@angular/material/button'; +import { TbPopoverService } from '@shared/components/popover.service'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { isDefinedAndNotNull, isEqual } from '@core/utils'; +import { TbPopoverComponent } from '@shared/components/popover.component'; +import { TbTableDatasource } from '@shared/components/table/table-datasource.abstract'; +import { MatSort, SortDirection } from '@angular/material/sort'; +import { getCurrentAuthState } from '@core/auth/auth.selectors'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { + CalculatedFieldMetricsPanelComponent +} from '@home/components/calculated-fields/components/metrics/calculated-field-metrics-panel.component'; +import { TbEditorCompleter } from '@shared/models/ace/completion.models'; +import { AceHighlightRules } from '@shared/models/ace/ace.models'; + +@Component({ + selector: 'tb-calculated-field-metrics-table', + templateUrl: './calculated-field-metrics-table.component.html', + styleUrls: [`../calculated-field-arguments/calculated-field-arguments-table.component.scss`], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => CalculatedFieldMetricsTableComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => CalculatedFieldMetricsTableComponent), + multi: true + } + ], +}) +export class CalculatedFieldMetricsTableComponent implements OnInit, ControlValueAccessor, Validator, AfterViewInit { + + @Input() arguments: Array; + @Input() editorCompleter: TbEditorCompleter; + @Input() highlightRules: AceHighlightRules; + @Input({transform: booleanAttribute}) simpleMode: boolean = false; + + @ViewChild(MatSort, { static: true }) sort: MatSort; + + errorText = ''; + metricsFormArray = this.fb.array([]); + sortOrder = { direction: 'asc' as SortDirection, property: '' }; + dataSource = new CalculatedFieldMetricsDatasource(); + + displayColumns = ['name', 'function', 'filter', 'valueSource', 'actions']; + + readonly AggFunctionTranslations = AggFunctionTranslations; + readonly AggInputTypeTranslations = AggInputTypeTranslations; + readonly maxArgumentsPerCF = getCurrentAuthState(this.store).maxArgumentsPerCF - 2; + + private popoverComponent: TbPopoverComponent; + private propagateChange: (zonesObj: Record) => void = () => {}; + + constructor( + private fb: FormBuilder, + private popoverService: TbPopoverService, + private viewContainerRef: ViewContainerRef, + private cd: ChangeDetectorRef, + private renderer: Renderer2, + private destroyRef: DestroyRef, + private store: Store + ) { + this.metricsFormArray.valueChanges.pipe(takeUntilDestroyed()).subscribe(value => { + this.updateDataSource(value); + this.propagateChange(this.getMetricsObject(value)); + }); + } + + ngOnInit() { + if (this.simpleMode) { + this.displayColumns = ['name', 'function', 'actions']; + } + } + + ngAfterViewInit(): void { + this.sort.sortChange.asObservable().pipe( + takeUntilDestroyed(this.destroyRef) + ).subscribe(() => { + this.sortOrder.property = this.sort.active; + this.sortOrder.direction = this.sort.direction; + this.updateDataSource(this.metricsFormArray.value); + }); + } + + registerOnChange(fn: (zonesObj: Record) => void): void { + this.propagateChange = fn; + } + + registerOnTouched(_fn: any): void {} + + validate(): ValidationErrors | null { + this.updateErrorText(); + return this.errorText ? { metricsFormArray: false } : null; + } + + onDelete($event: Event, metric: CalculatedFieldAggMetricValue): void { + $event.stopPropagation(); + const index = this.metricsFormArray.controls.findIndex(control => isEqual(control.value, metric)); + this.metricsFormArray.removeAt(index); + this.metricsFormArray.markAsDirty(); + } + + manageMetrics($event: Event, matButton: MatButton, metric = {} as CalculatedFieldAggMetricValue): void { + $event?.stopPropagation(); + if (this.popoverComponent && !this.popoverComponent.tbHidden) { + this.popoverComponent.hide(); + } + const trigger = matButton._elementRef.nativeElement; + if (this.popoverService.hasPopover(trigger)) { + this.popoverService.hidePopover(trigger); + } else { + const index = this.metricsFormArray.controls.findIndex(control => isEqual(control.value, metric)); + const isExists = index !== -1; + const ctx = { + index, + metric, + buttonTitle: isExists ? 'action.apply' : 'action.add', + usedNames: this.metricsFormArray.value.map(({ name }) => name).filter(name => name !== metric.name), + arguments: this.arguments, + editorCompleter: this.editorCompleter, + highlightRules: this.highlightRules, + simpleMode: this.simpleMode, + }; + this.popoverComponent = this.popoverService.displayPopover({ + trigger, + renderer: this.renderer, + componentType: CalculatedFieldMetricsPanelComponent, + hostView: this.viewContainerRef, + preferredPlacement: isExists ? ['leftOnly', 'leftTopOnly', 'leftBottomOnly'] : ['rightOnly', 'rightTopOnly', 'rightBottomOnly'], + context: ctx, + isModal: true + }); + this.popoverComponent.tbComponentRef.instance.metricDataApplied.subscribe((value) => { + this.popoverComponent.hide(); + if (isExists) { + this.metricsFormArray.at(index).setValue(value); + } else { + this.metricsFormArray.push(this.fb.control(value)); + } + this.cd.markForCheck(); + }); + } + } + + private updateDataSource(value: CalculatedFieldAggMetricValue[]): void { + const sortedValue = this.sortData(value); + this.dataSource.loadData(sortedValue); + } + + private updateErrorText(): void { + if (!this.metricsFormArray.controls.length) { + this.errorText = 'calculated-fields.metrics.metrics-empty'; + } else { + this.errorText = ''; + } + } + + private getMetricsObject(value: CalculatedFieldAggMetricValue[]): Record { + return value.reduce((acc, metricValue) => { + const { name, ...metric } = metricValue; + acc[name] = metric; + return acc; + }, {} as Record); + } + + writeValue(metrics: Record): void { + this.metricsFormArray.clear(); + this.populateZonesFormArray(metrics); + } + + private populateZonesFormArray(metrics: Record): void { + Object.keys(metrics).forEach(key => { + const value: CalculatedFieldAggMetricValue = { + ...metrics[key], + name: key + }; + this.metricsFormArray.push(this.fb.control(value), { emitEvent: false }); + }); + this.metricsFormArray.updateValueAndValidity(); + } + + private getSortValue(metric: CalculatedFieldAggMetricValue, column: string): string { + switch (column) { + case 'function': + return metric.function; + case 'valueSource': + return metric.input?.type; + case 'filter': + return isDefinedAndNotNull(metric.filter).toString(); + default: + return metric.name; + } + } + + private sortData(data: CalculatedFieldAggMetricValue[]): CalculatedFieldAggMetricValue[] { + return data.sort((a, b) => { + const valA = this.getSortValue(a, this.sortOrder.property) ?? ''; + const valB = this.getSortValue(b, this.sortOrder.property) ?? ''; + return (this.sortOrder.direction === 'asc' ? 1 : -1) * valA.localeCompare(valB); + }); + } +} + +class CalculatedFieldMetricsDatasource extends TbTableDatasource { + constructor() { + super(); + } +} diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/metrics/calculated-field-metrics-table.module.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/metrics/calculated-field-metrics-table.module.ts new file mode 100644 index 0000000000..e7e45fd445 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/metrics/calculated-field-metrics-table.module.ts @@ -0,0 +1,42 @@ +/// +/// Copyright © 2016-2025 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { SharedModule } from '@shared/shared.module'; +import { + CalculatedFieldMetricsTableComponent +} from '@home/components/calculated-fields/components/metrics/calculated-field-metrics-table.component'; +import { + CalculatedFieldMetricsPanelComponent +} from '@home/components/calculated-fields/components/metrics/calculated-field-metrics-panel.component'; + +@NgModule({ + imports: [ + CommonModule, + SharedModule, + ], + declarations: [ + CalculatedFieldMetricsTableComponent, + CalculatedFieldMetricsPanelComponent + ], + exports: [ + CalculatedFieldMetricsTableComponent + ] +}) +export class CalculatedFieldMetricsTableModule { + +} diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/output/calculated-field-output.component.html b/ui-ngx/src/app/modules/home/components/calculated-fields/components/output/calculated-field-output.component.html new file mode 100644 index 0000000000..c33d033f7a --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/output/calculated-field-output.component.html @@ -0,0 +1,90 @@ + +
    +
    {{ 'calculated-fields.output' | translate }}
    +
    +
    + + {{ 'calculated-fields.output-type' | translate }} + + @for (type of outputTypes; track type) { + {{ OutputTypeTranslations.get(type) | translate }} + } + + + @if (outputForm.get('type').value === OutputType.Attribute + && (entityId.entityType === EntityType.DEVICE || entityId.entityType === EntityType.DEVICE_PROFILE)) { + + {{ 'calculated-fields.attribute-scope' | translate }} + + + {{ 'calculated-fields.server-attributes' | translate }} + + + {{ 'calculated-fields.shared-attributes' | translate }} + + + + } +
    + @if (simpleMode) { + @if (hiddenName) { +
    + + +
    + } @else { +
    + + + {{ + (outputForm.get('type').value === OutputType.Timeseries + ? 'calculated-fields.timeseries-key' + : 'calculated-fields.attribute-key') + | translate + }} + + + @if (outputForm.get('name').errors && outputForm.get('name').touched) { + + @if (outputForm.get('name').hasError('required')) { + {{ 'common.hint.key-required' | translate }} + } @else if (outputForm.get('name').hasError('pattern')) { + {{ 'common.hint.key-pattern' | translate }} + } @else if (outputForm.get('name').hasError('maxlength')) { + {{ 'common.hint.key-max-length' | translate }} + } + + } + + +
    + + } + } +
    +
    + + + {{ 'calculated-fields.decimals-by-default' | translate }} + + @if (outputForm.get('decimalsByDefault').errors && outputForm.get('decimalsByDefault').touched) { + {{ 'calculated-fields.hint.decimals-range' | translate }} + } + + diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/output/calculated-field-output.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/output/calculated-field-output.component.ts new file mode 100644 index 0000000000..94224006a8 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/output/calculated-field-output.component.ts @@ -0,0 +1,180 @@ +/// +/// Copyright © 2016-2025 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, DestroyRef, forwardRef, inject, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core'; +import { + ControlValueAccessor, + FormBuilder, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + ValidationErrors, + Validator, + Validators +} from '@angular/forms'; +import { AttributeScope } from '@shared/models/telemetry/telemetry.models'; +import { + CalculatedFieldOutput, + CalculatedFieldSimpleOutput, + OutputType, + OutputTypeTranslations +} from '@shared/models/calculated-field.models'; +import { digitsRegex, oneSpaceInsideRegex } from '@shared/models/regex.constants'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { EntityId } from '@shared/models/id/entity-id'; +import { EntityType } from '@shared/models/entity-type.models'; +import { coerceBoolean } from '@shared/decorators/coercion'; + +@Component({ + selector: 'tb-calculate-field-output', + templateUrl: './calculated-field-output.component.html', + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => CalculatedFieldOutputComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => CalculatedFieldOutputComponent), + multi: true + } + ], +}) +export class CalculatedFieldOutputComponent implements ControlValueAccessor, Validator, OnInit, OnChanges { + + @Input() + @coerceBoolean() + simpleMode = false; + + @Input() + @coerceBoolean() + hiddenName = false; + + @Input() + @coerceBoolean() + disableType = false; + + @Input() + containerInputClass: string | string[] | Record = 'flex flex-col gap-3'; + + @Input({required: true}) + entityId: EntityId; + + readonly outputTypes = Object.values(OutputType) as OutputType[]; + readonly OutputType = OutputType; + readonly AttributeScope = AttributeScope; + readonly OutputTypeTranslations = OutputTypeTranslations; + readonly EntityType = EntityType; + + private fb = inject(FormBuilder); + private destroyRef = inject(DestroyRef); + + outputForm = this.fb.group({ + name: ['', [Validators.required, Validators.pattern(oneSpaceInsideRegex), Validators.maxLength(255)]], + scope: [{value: AttributeScope.SERVER_SCOPE, disabled: true}], + type: [OutputType.Timeseries], + decimalsByDefault: [null as number, [Validators.min(0), Validators.max(15), Validators.pattern(digitsRegex)]], + }); + + private propagateChange: (config: CalculatedFieldOutput | CalculatedFieldSimpleOutput) => void = () => { }; + + ngOnInit() { + this.outputForm.get('type').valueChanges + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(type => this.toggleScopeByOutputType(type)); + + this.updatedFormWithMode(); + + this.outputForm.valueChanges.pipe( + takeUntilDestroyed(this.destroyRef) + ).subscribe((value: CalculatedFieldOutput | CalculatedFieldSimpleOutput) => { + this.updatedModel(value) + }) + } + + ngOnChanges(changes: SimpleChanges): void { + for (const propName of Object.keys(changes)) { + const change = changes[propName]; + if (change.currentValue !== change.previousValue) { + if (propName === 'simpleMode') { + this.updatedFormWithMode(); + if (!change.firstChange) { + this.outputForm.updateValueAndValidity(); + } + } + } + } + } + + validate(): ValidationErrors | null { + return this.outputForm.valid || this.outputForm.disabled ? null : {outputConfig: false}; + } + + writeValue(value: CalculatedFieldOutput | CalculatedFieldSimpleOutput): void { + this.outputForm.patchValue(value, {emitEvent: false}); + this.outputForm.get('type').updateValueAndValidity({onlySelf: true}); + } + + registerOnChange(fn: (config: CalculatedFieldOutput | CalculatedFieldSimpleOutput) => void): void { + this.propagateChange = fn; + } + + registerOnTouched(_: any): void { } + + setDisabledState(isDisabled: boolean): void { + if (isDisabled) { + this.outputForm.disable({emitEvent: false}); + } else { + this.outputForm.enable({emitEvent: false}); + this.updatedFormWithMode(); + this.toggleScopeByOutputType(this.outputForm.get('type').value); + } + } + + private updatedModel(value: CalculatedFieldOutput | CalculatedFieldSimpleOutput) { + if (this.simpleMode && 'name' in value) { + value.name = value.name?.trim() ?? ''; + } + if (this.disableType) { + value.type = this.outputForm.get('type').value; + } + this.propagateChange(value); + } + + private toggleScopeByOutputType(type: OutputType): void { + if (type === OutputType.Attribute) { + this.outputForm.get('scope').enable({emitEvent: false}); + } else { + this.outputForm.get('scope').disable({emitEvent: false}); + } + } + + private updatedFormWithMode(): void { + if (this.simpleMode && !this.hiddenName) { + this.outputForm.get('name').enable({emitEvent: false}); + } else { + this.outputForm.get('name').disable({emitEvent: false}); + } + if (this.simpleMode) { + this.outputForm.get('decimalsByDefault').enable({emitEvent: false}); + } else { + this.outputForm.get('decimalsByDefault').disable({emitEvent: false}); + } + if (this.disableType) { + this.outputForm.get('type').disable({emitEvent: false}); + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/output/calculated-field-output.module.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/output/calculated-field-output.module.ts new file mode 100644 index 0000000000..83b3970d9b --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/output/calculated-field-output.module.ts @@ -0,0 +1,36 @@ +/// +/// Copyright © 2016-2025 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { SharedModule } from '@shared/shared.module'; +import { + CalculatedFieldOutputComponent +} from '@home/components/calculated-fields/components/output/calculated-field-output.component'; + +@NgModule({ + imports: [ + CommonModule, + SharedModule, + ], + declarations: [ + CalculatedFieldOutputComponent + ], + exports: [ + CalculatedFieldOutputComponent + ] +}) +export class CalculatedFieldOutputModule { } diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-argument-panel.component.html b/ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-argument-panel.component.html deleted file mode 100644 index 92855d882f..0000000000 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-argument-panel.component.html +++ /dev/null @@ -1,209 +0,0 @@ - -
    -
    -
    {{ 'calculated-fields.argument-settings' | translate }}
    -
    -
    -
    {{ 'calculated-fields.argument-name' | translate }}
    - - - @if (argumentFormGroup.get('argumentName').touched && argumentFormGroup.get('argumentName').hasError('required')) { - - warning - - } @else if (argumentFormGroup.get('argumentName').touched && argumentFormGroup.get('argumentName').hasError('duplicateName')) { - - warning - - } @else if (argumentFormGroup.get('argumentName').touched && argumentFormGroup.get('argumentName').hasError('pattern')) { - - warning - - } @else if (argumentFormGroup.get('argumentName').touched && argumentFormGroup.get('argumentName').hasError('maxlength')) { - - warning - - } @else if (argumentFormGroup.get('argumentName').touched && argumentFormGroup.get('argumentName').hasError('forbiddenName')) { - - warning - - } - -
    - -
    -
    {{ 'entity.entity-type' | translate }}
    - - - @for (type of argumentEntityTypes; track type) { - {{ ArgumentEntityTypeTranslations.get(type) | translate }} - } - - -
    - @if (ArgumentEntityTypeParamsMap.has(entityType)) { -
    -
    {{ ArgumentEntityTypeParamsMap.get(entityType).title | translate }}
    - -
    - } -
    - -
    -
    {{ 'calculated-fields.argument-type' | translate }}
    - - - @for (type of argumentTypes; track type) { - {{ ArgumentTypeTranslations.get(type) | translate }} - } - - @if (refEntityKeyFormGroup.get('type').hasError('required') && refEntityKeyFormGroup.get('type').touched) { - - warning - - } - -
    - @if (entityFilter.singleEntity?.id || entityType === ArgumentEntityType.Current || entityType === ArgumentEntityType.Tenant) { - @if (refEntityKeyFormGroup.get('type').value !== ArgumentType.Attribute) { -
    -
    {{ 'calculated-fields.timeseries-key' | translate }}
    - @if (refEntityKeyFormGroup.get('type').value === ArgumentType.LatestTelemetry) { - - } @else { - - } - - - -
    - } @else { - @if (enableAttributeScopeSelection) { -
    -
    {{ 'calculated-fields.attribute-scope' | translate }}
    - - - - {{ 'calculated-fields.server-attributes' | translate }} - - - {{ 'calculated-fields.client-attributes' | translate }} - - - {{ 'calculated-fields.shared-attributes' | translate }} - - - -
    - } -
    -
    {{ 'calculated-fields.attribute-key' | translate }}
    - -
    - } - } -
    - @if (refEntityKeyFormGroup.get('type').value !== ArgumentType.Rolling) { -
    -
    {{ 'calculated-fields.default-value' | translate }}
    - - - -
    - } @else { -
    -
    {{ 'calculated-fields.time-window' | translate }}
    - -
    - @if (maxDataPointsPerRollingArg) { -
    -
    {{ 'calculated-fields.limit' | translate }}
    -
    - - - - - - -
    -
    - } - } -
    -
    -
    - - -
    -
    diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/propagation-configuration/propagation-configuration.component.html b/ui-ngx/src/app/modules/home/components/calculated-fields/components/propagation-configuration/propagation-configuration.component.html new file mode 100644 index 0000000000..cc8cf7dc4f --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/propagation-configuration/propagation-configuration.component.html @@ -0,0 +1,100 @@ + +
    +
    +
    + {{ 'calculated-fields.propagation-path-related-entities' | translate }} +
    +
    + + {{ 'calculated-fields.direction' | translate }} + + @for (direction of Directions; track direction) { + {{ PropagationDirectionTranslations.get(direction) | translate }} + } + + + + +
    +
    +
    +
    +
    + {{ 'calculated-fields.data-propagate' | translate }} +
    + + {{ 'calculated-fields.propagate-type.arguments-only' | translate }} + {{ 'calculated-fields.propagate-type.expression-result' | translate }} + +
    + +
    +
    +
    + {{ 'calculated-fields.expression' | translate }} +
    +
    + +
    {{ 'api-usage.tbel' | translate }} +
    + +
    +
    + +
    +
    +
    + + +
    diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/propagation-configuration/propagation-configuration.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/propagation-configuration/propagation-configuration.component.ts new file mode 100644 index 0000000000..4485ef2032 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/propagation-configuration/propagation-configuration.component.ts @@ -0,0 +1,178 @@ +/// +/// Copyright © 2016-2025 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, forwardRef, Input } from '@angular/core'; +import { + ControlValueAccessor, + FormBuilder, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + ValidationErrors, + Validator, + Validators +} from '@angular/forms'; +import { EntityId } from '@shared/models/id/entity-id'; +import { Observable, of } from 'rxjs'; +import { + calculatedFieldDefaultScript, + CalculatedFieldOutput, + CalculatedFieldPropagationConfiguration, + CalculatedFieldType, + getCalculatedFieldArgumentsEditorCompleter, + getCalculatedFieldArgumentsHighlights, + notEmptyObjectValidator, + OutputType, + PropagationDirectionTranslations, + PropagationWithExpression +} from '@shared/models/calculated-field.models'; +import { map } from 'rxjs/operators'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { ScriptLanguage } from '@app/shared/models/rule-node.models'; +import { EntitySearchDirection } from '@shared/models/relation.models'; + +@Component({ + selector: 'tb-propagation-configuration', + templateUrl: './propagation-configuration.component.html', + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => PropagationConfigurationComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => PropagationConfigurationComponent), + multi: true + } + ], +}) +export class PropagationConfigurationComponent implements ControlValueAccessor, Validator { + + @Input({required: true}) + entityId: EntityId; + + @Input({required: true}) + tenantId: string; + + @Input({required: true}) + entityName: string; + + @Input({required: true}) + ownerId: EntityId; + + @Input({required: true}) + testScript: () => Observable; + + propagateConfiguration = this.fb.group({ + arguments: this.fb.control({}, notEmptyObjectValidator()), + applyExpressionToResolvedArguments: [false], + relation: this.fb.group({ + direction: [EntitySearchDirection.TO, Validators.required], + relationType: ['Contains', Validators.required], + }), + expression: [calculatedFieldDefaultScript], + output: this.fb.control({ + type: OutputType.Timeseries, + }), + }); + + readonly ScriptLanguage = ScriptLanguage; + readonly CalculatedFieldType = CalculatedFieldType; + readonly OutputType = OutputType; + readonly Directions = Object.values(EntitySearchDirection) as Array; + readonly PropagationDirectionTranslations = PropagationDirectionTranslations; + + functionArgs$ = this.propagateConfiguration.get('arguments').valueChanges.pipe( + map(argumentsObj => ['ctx', ...Object.keys(argumentsObj)]) + ); + + argumentsEditorCompleter$ = this.propagateConfiguration.get('arguments').valueChanges.pipe( + map(argumentsObj => getCalculatedFieldArgumentsEditorCompleter(argumentsObj ?? {})) + ); + + argumentsHighlightRules$ = this.propagateConfiguration.get('arguments').valueChanges.pipe( + map(argumentsObj => getCalculatedFieldArgumentsHighlights(argumentsObj)) + ); + + private propagateChange: (config: CalculatedFieldPropagationConfiguration) => void = () => { }; + + constructor(private fb: FormBuilder) { + this.propagateConfiguration.get('applyExpressionToResolvedArguments').valueChanges.pipe( + takeUntilDestroyed() + ).subscribe(() => { + this.updatedFormWithScript(); + }) + + this.propagateConfiguration.valueChanges.pipe( + takeUntilDestroyed() + ).subscribe((value: CalculatedFieldPropagationConfiguration) => { + this.updatedModel(value); + }) + } + + validate(): ValidationErrors | null { + return this.propagateConfiguration.valid || this.propagateConfiguration.disabled ? null : {invalidPropagateConfig: false}; + } + + writeValue(value: PropagationWithExpression): void { + value.expression = value.expression ?? calculatedFieldDefaultScript; + this.propagateConfiguration.patchValue(value, {emitEvent: false}); + this.updatedFormWithScript(); + setTimeout(() => { + this.propagateConfiguration.get('arguments').updateValueAndValidity({onlySelf: true}); + }); + } + + registerOnChange(fn: (config: CalculatedFieldPropagationConfiguration) => void): void { + this.propagateChange = fn; + } + + registerOnTouched(_: any): void { } + + setDisabledState(isDisabled: boolean): void { + if (isDisabled) { + this.propagateConfiguration.disable({emitEvent: false}); + } else { + this.propagateConfiguration.enable({emitEvent: false}); + this.updatedFormWithScript(); + } + } + + onTestScript() { + this.testScript().subscribe((expression) => { + this.propagateConfiguration.get('expression').setValue(expression); + this.propagateConfiguration.get('expression').markAsDirty(); + }) + } + + fetchOptions(searchText: string): Observable> { + const search = searchText ? searchText?.toLowerCase() : ''; + return of(['Contains', 'Manages']).pipe(map(name => name?.filter(option => option.toLowerCase().includes(search)))); + } + + private updatedModel(value: CalculatedFieldPropagationConfiguration): void { + value.type = CalculatedFieldType.PROPAGATION; + this.propagateChange(value); + } + + private updatedFormWithScript() { + if (this.propagateConfiguration.get('applyExpressionToResolvedArguments').value) { + this.propagateConfiguration.get('expression').enable({emitEvent: false}); + } else { + this.propagateConfiguration.get('expression').disable({emitEvent: false}); + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/propagation-configuration/propagation-configuration.module.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/propagation-configuration/propagation-configuration.module.ts new file mode 100644 index 0000000000..83dc4badf9 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/propagation-configuration/propagation-configuration.module.ts @@ -0,0 +1,44 @@ +/// +/// Copyright © 2016-2025 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { SharedModule } from '@shared/shared.module'; +import { + CalculatedFieldOutputModule +} from '@home/components/calculated-fields/components/output/calculated-field-output.module'; +import { + CalculatedFieldArgumentsTableModule +} from '@home/components/calculated-fields/components/calculated-field-arguments/calculated-field-arguments-table.module'; +import { + PropagationConfigurationComponent +} from '@home/components/calculated-fields/components/propagation-configuration/propagation-configuration.component'; + +@NgModule({ + imports: [ + CommonModule, + SharedModule, + CalculatedFieldOutputModule, + CalculatedFieldArgumentsTableModule, + ], + declarations: [ + PropagationConfigurationComponent, + ], + exports: [ + PropagationConfigurationComponent, + ] +}) +export class PropagationConfigurationModule { } diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/public-api.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/public-api.ts index 9e3c52bc4f..d4d4f9d1da 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/public-api.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/public-api.ts @@ -15,7 +15,5 @@ /// export * from './dialog/calculated-field-dialog.component'; -export * from './arguments-table/calculated-field-arguments-table.component'; -export * from './panel/calculated-field-argument-panel.component'; export * from './debug-dialog/calculated-field-debug-dialog.component'; export * from './test-dialog/calculated-field-script-test-dialog.component'; diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/related-entities-aggregation-configuration/related-entities-aggregation-component.component.html b/ui-ngx/src/app/modules/home/components/calculated-fields/components/related-entities-aggregation-configuration/related-entities-aggregation-component.component.html new file mode 100644 index 0000000000..8a0361ee58 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/related-entities-aggregation-configuration/related-entities-aggregation-component.component.html @@ -0,0 +1,82 @@ + +
    +
    +
    + {{ 'calculated-fields.aggregation-path-related-entities' | translate }} +
    +
    + + {{ 'calculated-fields.direction' | translate }} + + @for (direction of Directions; track direction) { + {{ PropagationDirectionTranslations.get(direction) | translate }} + } + + + + +
    +
    +
    +
    + {{ 'calculated-fields.arguments' | translate }} +
    + +
    +
    +
    + {{ 'calculated-fields.metrics.metrics' | translate }} +
    + + + +
    + +
    + +
    +
    calculated-fields.use-latest-timestamp
    +
    +
    +
    +
    +
    diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/related-entities-aggregation-configuration/related-entities-aggregation-component.component.scss b/ui-ngx/src/app/modules/home/components/calculated-fields/components/related-entities-aggregation-configuration/related-entities-aggregation-component.component.scss new file mode 100644 index 0000000000..6a8faea40f --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/related-entities-aggregation-configuration/related-entities-aggregation-component.component.scss @@ -0,0 +1,32 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host ::ng-deep { + .simpleMode { + min-width: 0; + + .mat-slide { + overflow: hidden; + + .mdc-form-field { + width: 100%; + + .mdc-label { + min-width: 0; + } + } + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/related-entities-aggregation-configuration/related-entities-aggregation-component.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/related-entities-aggregation-configuration/related-entities-aggregation-component.component.ts new file mode 100644 index 0000000000..c53f400916 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/related-entities-aggregation-configuration/related-entities-aggregation-component.component.ts @@ -0,0 +1,156 @@ +/// +/// Copyright © 2016-2025 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, forwardRef, Input } from '@angular/core'; +import { + ControlValueAccessor, + FormBuilder, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + ValidationErrors, + Validator, + Validators +} from '@angular/forms'; +import { EntityId } from '@shared/models/id/entity-id'; +import { Observable, of } from 'rxjs'; +import { + CalculatedFieldOutput, + CalculatedFieldRelatedAggregationConfiguration, + CalculatedFieldType, + getCalculatedFieldArgumentsEditorCompleter, + getCalculatedFieldArgumentsHighlights, + notEmptyObjectValidator, + OutputType, + PropagationDirectionTranslations +} from '@shared/models/calculated-field.models'; +import { map } from 'rxjs/operators'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { ScriptLanguage } from '@app/shared/models/rule-node.models'; +import { EntitySearchDirection } from '@shared/models/relation.models'; +import { getCurrentAuthState } from '@core/auth/auth.selectors'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; + +@Component({ + selector: 'tb-related-entities-aggregation-component', + templateUrl: './related-entities-aggregation-component.component.html', + styleUrl: './related-entities-aggregation-component.component.scss', + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => RelatedEntitiesAggregationComponentComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => RelatedEntitiesAggregationComponentComponent), + multi: true + } + ], +}) +export class RelatedEntitiesAggregationComponentComponent implements ControlValueAccessor, Validator { + + @Input({required: true}) + entityId: EntityId; + + @Input({required: true}) + tenantId: string; + + @Input({required: true}) + entityName: string; + + readonly ScriptLanguage = ScriptLanguage; + readonly CalculatedFieldType = CalculatedFieldType; + readonly OutputType = OutputType; + readonly Directions = Object.values(EntitySearchDirection) as Array; + readonly PropagationDirectionTranslations = PropagationDirectionTranslations; + readonly minAllowedDeduplicationIntervalInSecForCF = getCurrentAuthState(this.store).minAllowedDeduplicationIntervalInSecForCF; + + relatedAggregationConfiguration = this.fb.group({ + relation: this.fb.group({ + direction: [EntitySearchDirection.FROM, Validators.required], + relationType: ['Contains', Validators.required], + }), + arguments: this.fb.control({}, notEmptyObjectValidator()), + metrics: this.fb.control({}, notEmptyObjectValidator()), + deduplicationIntervalInSec: [this.minAllowedDeduplicationIntervalInSecForCF], + output: this.fb.control({ + type: OutputType.Timeseries, + }), + useLatestTs: [false] + }); + + arguments$ = this.relatedAggregationConfiguration.get('arguments').valueChanges.pipe( + map(argumentsObj => Object.keys(argumentsObj)) + ); + + argumentsEditorCompleter$ = this.relatedAggregationConfiguration.get('arguments').valueChanges.pipe( + map(argumentsObj => getCalculatedFieldArgumentsEditorCompleter(argumentsObj ?? {})) + ); + + argumentsHighlightRules$ = this.relatedAggregationConfiguration.get('arguments').valueChanges.pipe( + map(argumentsObj => getCalculatedFieldArgumentsHighlights(argumentsObj)) + ); + + private readonly minAllowedScheduledUpdateIntervalInSecForCF = getCurrentAuthState(this.store).minAllowedScheduledUpdateIntervalInSecForCF; + private propagateChange: (config: CalculatedFieldRelatedAggregationConfiguration) => void = () => { }; + + constructor(private fb: FormBuilder, + private store: Store) { + + this.relatedAggregationConfiguration.valueChanges.pipe( + takeUntilDestroyed() + ).subscribe((value: CalculatedFieldRelatedAggregationConfiguration) => { + this.updatedModel(value); + }) + } + + validate(): ValidationErrors | null { + return this.relatedAggregationConfiguration.valid || this.relatedAggregationConfiguration.disabled ? null : {invalidPropagateConfig: false}; + } + + writeValue(value: CalculatedFieldRelatedAggregationConfiguration): void { + this.relatedAggregationConfiguration.patchValue(value, {emitEvent: false}); + setTimeout(() => { + this.relatedAggregationConfiguration.get('arguments').updateValueAndValidity({onlySelf: true}); + }); + } + + registerOnChange(fn: (config: CalculatedFieldRelatedAggregationConfiguration) => void): void { + this.propagateChange = fn; + } + + registerOnTouched(_: any): void { } + + setDisabledState(isDisabled: boolean): void { + if (isDisabled) { + this.relatedAggregationConfiguration.disable({emitEvent: false}); + } else { + this.relatedAggregationConfiguration.enable({emitEvent: false}); + } + } + + fetchOptions(searchText: string): Observable> { + const search = searchText ? searchText?.toLowerCase() : ''; + return of(['Contains', 'Manages']).pipe(map(name => name?.filter(option => option.toLowerCase().includes(search)))); + } + + private updatedModel(value: CalculatedFieldRelatedAggregationConfiguration): void { + value.type = CalculatedFieldType.RELATED_ENTITIES_AGGREGATION; + value.scheduledUpdateInterval = this.minAllowedScheduledUpdateIntervalInSecForCF; + this.propagateChange(value); + } +} diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/related-entities-aggregation-configuration/related-entities-aggregation-component.module.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/related-entities-aggregation-configuration/related-entities-aggregation-component.module.ts new file mode 100644 index 0000000000..abc64568ec --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/related-entities-aggregation-configuration/related-entities-aggregation-component.module.ts @@ -0,0 +1,49 @@ +/// +/// Copyright © 2016-2025 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { SharedModule } from '@shared/shared.module'; +import { + CalculatedFieldOutputModule +} from '@home/components/calculated-fields/components/output/calculated-field-output.module'; +import { + CalculatedFieldArgumentsTableModule +} from '@home/components/calculated-fields/components/calculated-field-arguments/calculated-field-arguments-table.module'; +import { + RelatedEntitiesAggregationComponentComponent +} from '@home/components/calculated-fields/components/related-entities-aggregation-configuration/related-entities-aggregation-component.component'; +import { + CalculatedFieldMetricsTableModule +} from '@home/components/calculated-fields/components/metrics/calculated-field-metrics-table.module'; + +@NgModule({ + imports: [ + CommonModule, + SharedModule, + CalculatedFieldOutputModule, + CalculatedFieldArgumentsTableModule, + CalculatedFieldMetricsTableModule, + ], + declarations: [ + RelatedEntitiesAggregationComponentComponent, + ], + exports: [ + RelatedEntitiesAggregationComponentComponent, + ] +}) +export class RelatedEntitiesAggregationComponentModule { +} diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/simple-configuration/simple-configuration.component.html b/ui-ngx/src/app/modules/home/components/calculated-fields/components/simple-configuration/simple-configuration.component.html new file mode 100644 index 0000000000..cfab9d9def --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/simple-configuration/simple-configuration.component.html @@ -0,0 +1,99 @@ + +
    +
    +
    {{ 'calculated-fields.arguments' | translate }}
    + +
    +
    +
    + {{ (isScript ? 'calculated-fields.type.script' : 'calculated-fields.expression') | translate }} +
    + + +
    +
    + @if (simpleConfiguration.get('expressionSIMPLE').errors && simpleConfiguration.get('expressionSIMPLE').touched) { + + @if (simpleConfiguration.get('expressionSIMPLE').hasError('required')) { + {{ 'calculated-fields.hint.expression-required' | translate }} + } @else if (simpleConfiguration.get('expressionSIMPLE').hasError('pattern')) { + {{ 'calculated-fields.hint.expression-invalid' | translate }} + } @else if (simpleConfiguration.get('expressionSIMPLE').hasError('maxLength')) { + {{ 'calculated-fields.hint.expression-max-length' | translate }} + } + + } @else { + {{ 'calculated-fields.hint.expression' | translate }} + } +
    +
    + +
    {{ 'api-usage.tbel' | translate }} +
    + +
    +
    + +
    +
    +
    + +
    + +
    + calculated-fields.use-latest-timestamp +
    +
    +
    +
    +
    diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/simple-configuration/simple-configuration.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/simple-configuration/simple-configuration.component.ts new file mode 100644 index 0000000000..065556081c --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/simple-configuration/simple-configuration.component.ts @@ -0,0 +1,207 @@ +/// +/// Copyright © 2016-2025 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, forwardRef, Input, OnChanges, SimpleChanges } from '@angular/core'; +import { + ControlValueAccessor, + FormBuilder, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + ValidationErrors, + Validator, + Validators +} from '@angular/forms'; +import { oneSpaceInsideRegex } from '@shared/models/regex.constants'; +import { + calculatedFieldDefaultScript, + CalculatedFieldScriptConfiguration, + CalculatedFieldSimpleConfiguration, + CalculatedFieldSimpleOutput, + CalculatedFieldType, + getCalculatedFieldArgumentsEditorCompleter, + getCalculatedFieldArgumentsHighlights, + OutputType +} from '@shared/models/calculated-field.models'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { deepClone } from '@core/utils'; +import { EntityId } from '@shared/models/id/entity-id'; +import { Observable } from 'rxjs'; +import { ScriptLanguage } from '@shared/models/rule-node.models'; +import { map } from 'rxjs/operators'; + +type SimpeConfiguration = CalculatedFieldSimpleConfiguration | CalculatedFieldScriptConfiguration; + +@Component({ + selector: 'tb-simple-configuration', + templateUrl: './simple-configuration.component.html', + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => SimpleConfigurationComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => SimpleConfigurationComponent), + multi: true + } + ], +}) +export class SimpleConfigurationComponent implements ControlValueAccessor, Validator, OnChanges { + + @Input() + isScript: boolean; + + @Input({required: true}) + entityId: EntityId; + + @Input({required: true}) + tenantId: string; + + @Input({required: true}) + entityName: string; + + @Input({required: true}) + ownerId: EntityId; + + @Input({required: true}) + testScript: () => Observable; + + simpleConfiguration = this.fb.group({ + arguments: this.fb.control({}), + expressionSIMPLE: ['', [Validators.required, Validators.pattern(oneSpaceInsideRegex), Validators.maxLength(255)]], + expressionSCRIPT: [calculatedFieldDefaultScript], + output: this.fb.control({ + name: '', + type: OutputType.Timeseries, + decimalsByDefault: null + }), + useLatestTs: [false] + }); + + readonly ScriptLanguage = ScriptLanguage; + readonly OutputType = OutputType; + + functionArgs$ = this.simpleConfiguration.get('arguments').valueChanges.pipe( + map(argumentsObj => ['ctx', ...Object.keys(argumentsObj)]) + ); + + argumentsEditorCompleter$ = this.simpleConfiguration.get('arguments').valueChanges.pipe( + map(argumentsObj => getCalculatedFieldArgumentsEditorCompleter(argumentsObj ?? {})) + ); + + argumentsHighlightRules$ = this.simpleConfiguration.get('arguments').valueChanges.pipe( + map(argumentsObj => getCalculatedFieldArgumentsHighlights(argumentsObj)) + ); + + private propagateChange: (config: SimpeConfiguration) => void = () => { }; + + constructor(private fb: FormBuilder) { + this.simpleConfiguration.get('output').valueChanges.pipe( + takeUntilDestroyed(), + ).subscribe(() => { + this.toggleScopeByOutputType(); + }); + + this.simpleConfiguration.valueChanges.pipe( + takeUntilDestroyed() + ).subscribe((value) => { + const { expressionSIMPLE, expressionSCRIPT, ...config } = value; + const cfConfig = config as SimpeConfiguration; + cfConfig.expression = this.isScript ? expressionSCRIPT : expressionSIMPLE; + this.updatedModel(cfConfig); + }) + } + + ngOnChanges(changes: SimpleChanges): void { + for (const propName of Object.keys(changes)) { + const change = changes[propName]; + if (change.currentValue !== change.previousValue) { + if (propName === 'isScript') { + this.updatedFormWithScript(); + if (!change.firstChange) { + this.simpleConfiguration.updateValueAndValidity(); + } + } + } + } + } + + validate(): ValidationErrors | null { + return this.simpleConfiguration.valid || this.simpleConfiguration.disabled ? null : {invalidSimpleConfig: false}; + } + + writeValue(value: SimpeConfiguration): void { + const formValue: any = deepClone(value); + if (this.isScript) { + formValue.expressionSCRIPT = formValue.expression ?? calculatedFieldDefaultScript; + } else { + formValue.expressionSIMPLE = formValue.expression; + } + this.simpleConfiguration.patchValue(formValue, {emitEvent: false}); + this.updatedFormWithScript(); + setTimeout(() => { + this.simpleConfiguration.get('arguments').updateValueAndValidity({onlySelf: true}); + }); + } + + registerOnChange(fn: (config: SimpeConfiguration) => void): void { + this.propagateChange = fn; + } + + registerOnTouched(_: any): void { + } + + setDisabledState(isDisabled: boolean): void { + if (isDisabled) { + this.simpleConfiguration.disable({emitEvent: false}); + } else { + this.simpleConfiguration.enable({emitEvent: false}); + this.updatedFormWithScript(); + } + } + + onTestScript() { + this.testScript().subscribe((expression) => { + this.simpleConfiguration.get('expressionSCRIPT').setValue(expression); + this.simpleConfiguration.get('expressionSCRIPT').markAsDirty(); + }) + } + + private updatedModel(value: SimpeConfiguration): void { + value.type = this.isScript ? CalculatedFieldType.SCRIPT : CalculatedFieldType.SIMPLE; + this.propagateChange(value); + } + + private updatedFormWithScript() { + if (this.isScript) { + this.simpleConfiguration.get('expressionSIMPLE').disable({emitEvent: false}); + this.simpleConfiguration.get('expressionSCRIPT').enable({emitEvent: false}); + } else { + this.simpleConfiguration.get('expressionSIMPLE').enable({emitEvent: false}); + this.simpleConfiguration.get('expressionSCRIPT').disable({emitEvent: false}); + } + this.toggleScopeByOutputType(); + } + + private toggleScopeByOutputType(): void { + if (this.isScript || this.simpleConfiguration.get('output').value.type === OutputType.Attribute) { + this.simpleConfiguration.get('useLatestTs').disable({emitEvent: false}); + } else { + this.simpleConfiguration.get('useLatestTs').enable({emitEvent: false}); + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/simple-configuration/simple-configuration.module.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/simple-configuration/simple-configuration.module.ts new file mode 100644 index 0000000000..2e5e14426e --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/simple-configuration/simple-configuration.module.ts @@ -0,0 +1,44 @@ +/// +/// Copyright © 2016-2025 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { SharedModule } from '@shared/shared.module'; +import { + SimpleConfigurationComponent +} from '@home/components/calculated-fields/components/simple-configuration/simple-configuration.component'; +import { + CalculatedFieldOutputModule +} from '@home/components/calculated-fields/components/output/calculated-field-output.module'; +import { + CalculatedFieldArgumentsTableModule +} from '@home/components/calculated-fields/components/calculated-field-arguments/calculated-field-arguments-table.module'; + +@NgModule({ + imports: [ + CommonModule, + SharedModule, + CalculatedFieldOutputModule, + CalculatedFieldArgumentsTableModule, + ], + declarations: [ + SimpleConfigurationComponent, + ], + exports: [ + SimpleConfigurationComponent + ] +}) +export class SimpleConfigurationModule {} diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts index 60d05d4c86..44d1e2efc3 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts @@ -58,7 +58,7 @@ import { } from '@app/shared/models/dashboard.models'; import { WINDOW } from '@core/services/window.service'; import { WindowMessage } from '@shared/models/window-message.model'; -import { deepClone, guid, isDefined, isDefinedAndNotNull, isEqual, isNotEmptyStr } from '@app/core/utils'; +import { deepClone, guid, isDefined, isDefinedAndNotNull, isNotEmptyStr } from '@app/core/utils'; import { DashboardContext, DashboardPageInitData, @@ -119,7 +119,8 @@ import { } from '@home/components/dashboard-page/dashboard-settings-dialog.component'; import { ManageDashboardStatesDialogComponent, - ManageDashboardStatesDialogData + ManageDashboardStatesDialogData, + ManageDashboardStatesDialogResult } from '@home/components/dashboard-page/states/manage-dashboard-states-dialog.component'; import { ImportExportService } from '@shared/import-export/import-export.service'; import { AuthState } from '@app/core/auth/auth.models'; @@ -648,9 +649,9 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC } private hideToolbarSetting(): boolean { - if (this.dashboard.configuration.settings && - isDefined(this.dashboard.configuration.settings.hideToolbar)) { - return this.dashboard.configuration.settings.hideToolbar; + if (isDefined(this.dashboard.configuration?.settings?.hideToolbar)) { + const canApplyHideSetting = !this.forceFullscreen || this.isMobileApp; + return this.dashboard.configuration.settings.hideToolbar && canApplyHideSetting; } else { return false; } @@ -964,17 +965,17 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC $event.stopPropagation(); } this.dialog.open(ManageDashboardStatesDialogComponent, { + ManageDashboardStatesDialogResult>(ManageDashboardStatesDialogComponent, { disableClose: true, panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], data: { states: deepClone(this.dashboard.configuration.states), - widgets: deepClone(this.dashboard.configuration.widgets) as {[id: string]: Widget} + widgets: this.dashboard.configuration.widgets as {[id: string]: Widget} } }).afterClosed().subscribe((result) => { if (result) { - if (!isEqual(result.widgets, this.dashboard.configuration.widgets)) { - this.dashboard.configuration.widgets = result.widgets; + if (result.addWidgets) { + Object.assign(this.dashboard.configuration.widgets, result.addWidgets); } if (result.states) { this.updateStates(result.states); @@ -1322,6 +1323,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC } private addWidgetToDashboard(widget: Widget) { + this.dashboardUtils.prepareWidgetForSaving(widget); if (this.addingLayoutCtx) { this.addWidgetToLayout(widget, this.addingLayoutCtx.id); this.addingLayoutCtx = null; @@ -1409,7 +1411,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC saveWidget() { this.editWidgetComponent.widgetFormGroup.markAsPristine(); - const widget = deepClone(this.editingWidget); + const widget = this.dashboardUtils.prepareWidgetForSaving(deepClone(this.editingWidget)); const widgetLayout = deepClone(this.editingWidgetLayout); const id = this.editingWidgetOriginal.id; this.dashboardConfiguration.widgets[id] = widget; diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/states/manage-dashboard-states-dialog.component.html b/ui-ngx/src/app/modules/home/components/dashboard-page/states/manage-dashboard-states-dialog.component.html index a2b0a4c0f3..b91a62c8e7 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/states/manage-dashboard-states-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/states/manage-dashboard-states-dialog.component.html @@ -15,143 +15,134 @@ limitations under the License. --> -
    - -

    dashboard.manage-states

    - - -
    - - -
    -
    -
    -
    -
    - -
    - dashboard.states - - - -
    -
    - -
    - - -   - - - -
    -
    -
    - - - {{ 'dashboard.state-name' | translate }} - - {{ state.name }} - - - - {{ 'dashboard.state-id' | translate }} - - {{ state.id }} - - - - {{ 'dashboard.is-root-state' | translate }} - - {{state.root ? 'check_box' : 'check_box_outline_blank'}} - - - - - - -
    - - - -
    -
    -
    - - -
    - {{ 'dashboard.no-states-text' }} -
    - - -
    -
    + +

    dashboard.manage-states

    + + +
    +
    +
    + +
    + dashboard.states + + +
    -
    -
    -
    - - + + +
    + + +   + + + +
    +
    +
    + + + {{ 'dashboard.state-name' | translate }} + + {{ state.name }} + + + + {{ 'dashboard.state-id' | translate }} + + {{ state.id }} + + + + {{ 'dashboard.is-root-state' | translate }} + + {{state.root ? 'check_box' : 'check_box_outline_blank'}} + + + + + + +
    + + + +
    +
    +
    + + +
    + {{ 'dashboard.no-states-text' }} +
    + +
    -
    +
    +
    + + +
    diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/states/manage-dashboard-states-dialog.component.scss b/ui-ngx/src/app/modules/home/components/dashboard-page/states/manage-dashboard-states-dialog.component.scss index 862737c22b..98d48861bd 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/states/manage-dashboard-states-dialog.component.scss +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/states/manage-dashboard-states-dialog.component.scss @@ -13,27 +13,39 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +@import "../scss/constants"; + :host { + height: 100%; + display: grid; + grid-template-rows: min-content auto min-content; + .manage-dashboard-states { - .tb-entity-table { - .tb-entity-table-content { - width: 100%; - height: 100%; - background: #fff; + .tb-entity-table-content { + width: 100%; + height: 100%; + background: #fff; - .tb-entity-table-title { - padding-right: 20px; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - } + .tb-entity-table-title { + padding-right: 20px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } - .table-container { - overflow: auto; - } + .table-container { + overflow: auto; } } } + + @media #{$mat-sm} { + min-width: 470px; + } + + @media #{$mat-gt-sm} { + min-width: 750px; + } } :host ::ng-deep { diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/states/manage-dashboard-states-dialog.component.ts b/ui-ngx/src/app/modules/home/components/dashboard-page/states/manage-dashboard-states-dialog.component.ts index 7dddc18dbd..6e74ee9b14 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/states/manage-dashboard-states-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/states/manage-dashboard-states-dialog.component.ts @@ -14,21 +14,10 @@ /// limitations under the License. /// -import { - AfterViewInit, - Component, - ElementRef, - Inject, - OnInit, - SecurityContext, - SkipSelf, - ViewChild -} from '@angular/core'; -import { ErrorStateMatcher } from '@angular/material/core'; +import { AfterViewInit, Component, ElementRef, Inject, OnInit, SecurityContext, ViewChild } from '@angular/core'; import { MAT_DIALOG_DATA, MatDialog, MatDialogRef } from '@angular/material/dialog'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; -import { FormGroupDirective, NgForm, UntypedFormBuilder, UntypedFormControl, UntypedFormGroup } from '@angular/forms'; import { Router } from '@angular/router'; import { DialogComponent } from '@app/shared/components/dialog.component'; import { DashboardState } from '@app/shared/models/dashboard.models'; @@ -44,7 +33,7 @@ import { fromEvent, merge } from 'rxjs'; import { debounceTime, distinctUntilChanged, tap } from 'rxjs/operators'; import { TranslateService } from '@ngx-translate/core'; import { DialogService } from '@core/services/dialog.service'; -import { deepClone, isDefined } from '@core/utils'; +import { deepClone, isDefined, isEqual } from '@core/utils'; import { DashboardStateDialogComponent, DashboardStateDialogData @@ -58,42 +47,42 @@ export interface ManageDashboardStatesDialogData { widgets: {[id: string]: Widget }; } +export interface ManageDashboardStatesDialogResult { + states: {[id: string]: DashboardState }; + addWidgets?: {[id: string]: Widget }; +} + @Component({ selector: 'tb-manage-dashboard-states-dialog', templateUrl: './manage-dashboard-states-dialog.component.html', - providers: [{provide: ErrorStateMatcher, useExisting: ManageDashboardStatesDialogComponent}], styleUrls: ['./manage-dashboard-states-dialog.component.scss'] }) export class ManageDashboardStatesDialogComponent - extends DialogComponent - implements OnInit, ErrorStateMatcher, AfterViewInit { + extends DialogComponent + implements OnInit, AfterViewInit { - statesFormGroup: UntypedFormGroup; + @ViewChild('searchInput', {static: false}) searchInputField: ElementRef; - states: {[id: string]: DashboardState }; - widgets: {[id: string]: Widget}; + @ViewChild(MatPaginator, {static: false}) paginator: MatPaginator; + @ViewChild(MatSort, {static: false}) sort: MatSort; + + isDirty = false; displayedColumns: string[]; pageLink: PageLink; textSearchMode = false; dataSource: DashboardStatesDatasource; - submitted = false; + private states: {[id: string]: DashboardState }; + private widgets: {[id: string]: Widget}; - stateNames: Set = new Set(); - - @ViewChild('searchInput') searchInputField: ElementRef; - - @ViewChild(MatPaginator) paginator: MatPaginator; - @ViewChild(MatSort) sort: MatSort; + private stateNames: Set = new Set(); + private addWidgets: {[id: string]: Widget} = {}; constructor(protected store: Store, protected router: Router, @Inject(MAT_DIALOG_DATA) public data: ManageDashboardStatesDialogData, - @SkipSelf() private errorStateMatcher: ErrorStateMatcher, - public dialogRef: MatDialogRef, - private fb: UntypedFormBuilder, + public dialogRef: MatDialogRef, private translate: TranslateService, private dialogs: DialogService, private utils: UtilsService, @@ -103,7 +92,6 @@ export class ManageDashboardStatesDialogComponent this.states = this.data.states; this.widgets = this.data.widgets; - this.statesFormGroup = this.fb.group({}); Object.values(this.states).forEach(value => this.stateNames.add(value.name)); const sortOrder: SortOrder = { property: 'name', direction: Direction.ASC }; @@ -258,77 +246,65 @@ export class ManageDashboardStatesDialogComponent } duplicateState($event: Event, state: DashboardStateInfo) { - const originalState = state; - const newStateName = this.getNextDuplicatedName(state.name); - if (newStateName) { - const newStateId = newStateName.toLowerCase().replace(/\W/g, '_'); - if (this.states[newStateId]) { - this.stateNames.add(newStateName); - this.duplicateState(null, state); + const suffix = ` - ${this.translate.instant('action.copy')} `; + let counter = 0; + const maxAttempts = 1000; + + while (counter++ < maxAttempts) { + const candidateName = `${state.name}${suffix}${counter}`; + if (this.stateNames.has(candidateName)) continue; + + const candidateId = candidateName.toLowerCase().replace(/\W/g, '_'); + if (this.states[candidateId]) { + continue; } - const duplicatedStates = deepClone(originalState); - const duplicatedWidgets = deepClone(this.widgets); + + const duplicatedState = deepClone(state); const mainWidgets = {}; const rightWidgets = {}; - duplicatedStates.id = newStateId; - duplicatedStates.name = newStateName; - duplicatedStates.root = false; - this.stateNames.add(duplicatedStates.name); + duplicatedState.id = candidateId; + duplicatedState.name = candidateName; + duplicatedState.root = false; + this.stateNames.add(duplicatedState.name); - for (const [key, value] of Object.entries(duplicatedStates.layouts.main.widgets)) { + for (const [key, value] of Object.entries(duplicatedState.layouts.main.widgets)) { const guid = this.utils.guid(); mainWidgets[guid] = value; - duplicatedWidgets[guid] = this.widgets[key]; - duplicatedWidgets[guid].id = guid; + this.addWidgets[guid] = deepClone(this.widgets[key] ?? this.addWidgets[key]); + this.addWidgets[guid].id = guid; } - duplicatedStates.layouts.main.widgets = mainWidgets; + duplicatedState.layouts.main.widgets = mainWidgets; - if (isDefined(duplicatedStates.layouts?.right)) { - for (const [key, value] of Object.entries(duplicatedStates.layouts.right.widgets)) { + if (isDefined(duplicatedState.layouts?.right)) { + for (const [key, value] of Object.entries(duplicatedState.layouts.right.widgets)) { const guid = this.utils.guid(); rightWidgets[guid] = value; - duplicatedWidgets[guid] = this.widgets[key]; - duplicatedWidgets[guid].id = guid; + this.addWidgets[guid] = deepClone(this.widgets[key] ?? this.addWidgets[key]); + this.addWidgets[guid].id = guid; } - duplicatedStates.layouts.right.widgets = rightWidgets; + duplicatedState.layouts.right.widgets = rightWidgets; } - this.states[duplicatedStates.id] = duplicatedStates; - this.widgets = duplicatedWidgets; + this.states[duplicatedState.id] = duplicatedState; this.onStatesUpdated(); + return; } } - private getNextDuplicatedName(stateName: string): string { - const suffix = ` - ${this.translate.instant('action.copy')} `; - let counter = 0; - while (++counter < Number.MAX_SAFE_INTEGER) { - const newName = `${stateName}${suffix}${counter}`; - if (!this.stateNames.has(newName)) { - return newName; - } - } - - return null; - } - private onStatesUpdated() { - this.statesFormGroup.markAsDirty(); + this.isDirty = true; this.updateData(true); } - isErrorState(control: UntypedFormControl | null, form: FormGroupDirective | NgForm | null): boolean { - const originalErrorState = this.errorStateMatcher.isErrorState(control, form); - const customErrorState = !!(control && control.invalid && this.submitted); - return originalErrorState || customErrorState; - } - cancel(): void { this.dialogRef.close(null); } save(): void { - this.submitted = true; - this.dialogRef.close({ states: this.states, widgets: this.widgets }); + const result: ManageDashboardStatesDialogResult = {states: this.states}; + if (!isEqual(this.addWidgets, {})) { + result.addWidgets = this.addWidgets; + } + this.dialogRef.close(result); } } diff --git a/ui-ngx/src/app/modules/home/components/device/device-credentials-lwm2m.component.html b/ui-ngx/src/app/modules/home/components/device/device-credentials-lwm2m.component.html index 56ec0c6561..7fa3da0cae 100644 --- a/ui-ngx/src/app/modules/home/components/device/device-credentials-lwm2m.component.html +++ b/ui-ngx/src/app/modules/home/components/device/device-credentials-lwm2m.component.html @@ -15,7 +15,7 @@ limitations under the License. --> - + @@ -72,6 +72,12 @@ device.lwm2m-security-config.client-public-key-hint + @@ -103,5 +109,11 @@
    + diff --git a/ui-ngx/src/app/modules/home/components/device/device-credentials-lwm2m.component.ts b/ui-ngx/src/app/modules/home/components/device/device-credentials-lwm2m.component.ts index ca91c14682..ceb0b0da78 100644 --- a/ui-ngx/src/app/modules/home/components/device/device-credentials-lwm2m.component.ts +++ b/ui-ngx/src/app/modules/home/components/device/device-credentials-lwm2m.component.ts @@ -14,20 +14,21 @@ /// limitations under the License. /// -import { Component, forwardRef, OnDestroy } from '@angular/core'; +import { Component, forwardRef, Input, OnDestroy } from '@angular/core'; import { ControlValueAccessor, - UntypedFormBuilder, - UntypedFormGroup, NG_VALIDATORS, NG_VALUE_ACCESSOR, + UntypedFormBuilder, + UntypedFormGroup, ValidationErrors, Validator, Validators } from '@angular/forms'; import { getDefaultClientSecurityConfig, - getDefaultServerSecurityConfig, Lwm2mClientKeyTooltipTranslationsMap, + getDefaultServerSecurityConfig, + Lwm2mClientKeyTooltipTranslationsMap, Lwm2mSecurityConfigModels, Lwm2mSecurityType, Lwm2mSecurityTypeTranslationMap @@ -35,6 +36,11 @@ import { import { Subject } from 'rxjs'; import { takeUntil } from 'rxjs/operators'; import { isDefinedAndNotNull } from '@core/utils'; +import { DeviceId } from "@shared/models/id/device-id"; +import { DeviceService } from "@core/http/device.service"; +import { ActionNotificationShow } from "@core/notification/notification.actions"; +import { Store } from "@ngrx/store"; +import { AppState } from "@core/core.state"; @Component({ selector: 'tb-device-credentials-lwm2m', @@ -65,7 +71,12 @@ export class DeviceCredentialsLwm2mComponent implements ControlValueAccessor, Va private destroy$ = new Subject(); private propagateChange = (v: any) => {}; - constructor(private fb: UntypedFormBuilder) { + @Input() + deviceId: DeviceId; + + constructor(protected store: Store, + private fb: UntypedFormBuilder, + private deviceService: DeviceService) { this.lwm2mConfigFormGroup = this.initLwm2mConfigForm(); } @@ -101,6 +112,41 @@ export class DeviceCredentialsLwm2mComponent implements ControlValueAccessor, Va this.destroy$.complete(); } + /** + * AbstractRpcController -> rpcController + * - API + * "/api/plugins/rpc/twoway/${this.deviceId.id}" + * - DiscoveryAll + * requestBody = "{\"method\":\"DiscoverAll\"}"; + * - "Registration Update Trigger", + * requestBody = "{\"method\": \"Execute\", \"params\": {\"id\": \"/1/0/8\"}} + * - "Bootstrap-Request Trigger" + * requestBody = "{\"method\": \"Execute\", \"params\": {\"id\": \"/1/0/9\"}} + */ + + public rebootDevice(isBootstrapServer: boolean): void { + this.deviceService.rebootDevice(this.deviceId.id, isBootstrapServer).subscribe(responseReboot => { + if (responseReboot.result === 'SUCCESS') { + this.store.dispatch(new ActionNotificationShow( + { + message: responseReboot.msg, + type: 'success', + duration: 1500, + verticalPosition: 'top', + horizontalPosition: 'left' + })); + } else { + this.store.dispatch(new ActionNotificationShow( + { + message: responseReboot.msg, + type: 'error', + verticalPosition: 'top', + horizontalPosition: 'left' + })); + } + }); + } + private initClientSecurityConfig(config: Lwm2mSecurityConfigModels): void { this.lwm2mConfigFormGroup.patchValue(config, {emitEvent: false}); this.securityConfigClientUpdateValidators(config.client.securityConfigClientMode); diff --git a/ui-ngx/src/app/modules/home/components/device/device-credentials.component.html b/ui-ngx/src/app/modules/home/components/device/device-credentials.component.html index 34c446f759..144f2059c3 100644 --- a/ui-ngx/src/app/modules/home/components/device/device-credentials.component.html +++ b/ui-ngx/src/app/modules/home/components/device/device-credentials.component.html @@ -81,7 +81,8 @@ - +
    diff --git a/ui-ngx/src/app/modules/home/components/device/device-credentials.component.ts b/ui-ngx/src/app/modules/home/components/device/device-credentials.component.ts index 012bdf9c9c..6c56d2da84 100644 --- a/ui-ngx/src/app/modules/home/components/device/device-credentials.component.ts +++ b/ui-ngx/src/app/modules/home/components/device/device-credentials.component.ts @@ -36,6 +36,7 @@ import { Subject } from 'rxjs'; import { takeUntil } from 'rxjs/operators'; import { generateSecret, isDefinedAndNotNull } from '@core/utils'; import { coerceBoolean } from '@shared/decorators/coercion'; +import { DeviceId } from "@shared/models/id/device-id"; @Component({ selector: 'tb-device-credentials', @@ -88,6 +89,8 @@ export class DeviceCredentialsComponent implements ControlValueAccessor, OnInit, credentialTypeNamesMap = credentialTypeNames; + deviceId: DeviceId; + private propagateChange = null; private propagateChangePending = false; @@ -126,6 +129,7 @@ export class DeviceCredentialsComponent implements ControlValueAccessor, OnInit, writeValue(value: DeviceCredentials | null): void { if (isDefinedAndNotNull(value)) { + this.deviceId = value.deviceId; const credentialsType = this.credentialsTypes.includes(value.credentialsType) ? value.credentialsType : this.credentialsTypes[0]; this.deviceCredentialsFormGroup.patchValue({ credentialsType, diff --git a/ui-ngx/src/app/modules/home/components/entity/contact-based.component.ts b/ui-ngx/src/app/modules/home/components/entity/contact-based.component.ts index ff052313ff..2db5dd3c97 100644 --- a/ui-ngx/src/app/modules/home/components/entity/contact-based.component.ts +++ b/ui-ngx/src/app/modules/home/components/entity/contact-based.component.ts @@ -24,6 +24,7 @@ import { EntityComponent } from './entity.component'; import { EntityTableConfig } from '@home/models/entity/entities-table-config.models'; import { CountryData } from '@shared/models/country.models'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { validateEmail } from '@app/core/utils'; @Directive() export abstract class ContactBasedComponent> extends EntityComponent implements AfterViewInit { @@ -50,7 +51,7 @@ export abstract class ContactBasedComponent> exten entityForm.addControl('address', this.fb.control(entity ? entity.address : '', [])); entityForm.addControl('address2', this.fb.control(entity ? entity.address2 : '', [])); entityForm.addControl('phone', this.fb.control(entity ? entity.phone : '', [Validators.maxLength(255)])); - entityForm.addControl('email', this.fb.control(entity ? entity.email : '', [Validators.email])); + entityForm.addControl('email', this.fb.control(entity ? entity.email : '', [validateEmail])); return entityForm; } diff --git a/ui-ngx/src/app/modules/home/components/entity/debug/entity-debug-settings-panel.component.ts b/ui-ngx/src/app/modules/home/components/entity/debug/entity-debug-settings-panel.component.ts index e19407a687..c3083d2180 100644 --- a/ui-ngx/src/app/modules/home/components/entity/debug/entity-debug-settings-panel.component.ts +++ b/ui-ngx/src/app/modules/home/components/entity/debug/entity-debug-settings-panel.component.ts @@ -53,6 +53,7 @@ export class EntityDebugSettingsPanelComponent extends PageComponent implements @Input({ transform: booleanAttribute }) failuresEnabled = false; @Input({ transform: booleanAttribute }) allEnabled = false; + @Input() entityLabel = ''; @Input() entityType: EntityType; @Input() allEnabledUntil = 0; @Input() maxDebugModeDuration = getCurrentAuthState(this.store).maxDebugModeDurationMinutes * MINUTE; @@ -64,7 +65,6 @@ export class EntityDebugSettingsPanelComponent extends PageComponent implements maxMessagesCount: string; maxTimeFrameDuration: number; initialAllEnabled: boolean; - entityLabel: string; isDebugAllActive$ = this.debugAllControl.valueChanges.pipe( startWith(this.debugAllControl.value), @@ -109,7 +109,9 @@ export class EntityDebugSettingsPanelComponent extends PageComponent implements this.onFailuresControl.patchValue(this.failuresEnabled); this.debugAllControl.patchValue(this.allEnabled); this.initialAllEnabled = this.allEnabled || this.allEnabledUntil > new Date().getTime(); - this.entityLabel = entityTypeTranslations.has(this.entityType) ? entityTypeTranslations.get(this.entityType).type : 'debug-settings.entity'; + if (!this.entityLabel) { + this.entityLabel = entityTypeTranslations.has(this.entityType) ? entityTypeTranslations.get(this.entityType).type : 'debug-settings.entity'; + } } onCancel(): void { diff --git a/ui-ngx/src/app/modules/home/components/entity/debug/entity-debug-settings.model.ts b/ui-ngx/src/app/modules/home/components/entity/debug/entity-debug-settings.model.ts index 0af9da52f2..f3b19d3737 100644 --- a/ui-ngx/src/app/modules/home/components/entity/debug/entity-debug-settings.model.ts +++ b/ui-ngx/src/app/modules/home/components/entity/debug/entity-debug-settings.model.ts @@ -28,6 +28,7 @@ export interface EntityDebugSettingPanelConfig { maxDebugModeDuration?: number; additionalActionConfig?: AdditionalDebugActionConfig; entityType: EntityType; + entityLabel?: string; } onSettingsAppliedFn: (settings: EntityDebugSettings) => void; } diff --git a/ui-ngx/src/app/modules/home/components/entity/entity-filter-view.component.ts b/ui-ngx/src/app/modules/home/components/entity/entity-filter-view.component.ts index ac7333079d..7eae04452d 100644 --- a/ui-ngx/src/app/modules/home/components/entity/entity-filter-view.component.ts +++ b/ui-ngx/src/app/modules/home/components/entity/entity-filter-view.component.ts @@ -128,6 +128,9 @@ export class EntityFilterViewComponent implements ControlValueAccessor { {edgeTypes}); } break; + case AliasFilterType.apiUsageState: + this.filterDisplayValue = this.translate.instant('alias.filter-type-apiUsageState'); + break; case AliasFilterType.entityViewType: const entityViewTypesQuoted = []; this.filter.entityViewTypes.forEach((entityViewType) => { diff --git a/ui-ngx/src/app/modules/home/components/entity/entity-filter.component.html b/ui-ngx/src/app/modules/home/components/entity/entity-filter.component.html index 25bb1d9700..736e716bec 100644 --- a/ui-ngx/src/app/modules/home/components/entity/entity-filter.component.html +++ b/ui-ngx/src/app/modules/home/components/entity/entity-filter.component.html @@ -45,6 +45,7 @@ diff --git a/ui-ngx/src/app/modules/home/components/filter/user-filter-dialog.component.html b/ui-ngx/src/app/modules/home/components/filter/user-filter-dialog.component.html index 4c350beee7..6ec45f2d16 100644 --- a/ui-ngx/src/app/modules/home/components/filter/user-filter-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/filter/user-filter-dialog.component.html @@ -17,7 +17,7 @@ -->
    -

    {{ filter.filter }}

    +

    {{ filter.filter | customTranslate }}

    @@ -326,10 +398,10 @@ - + {{ 'tenant-profile.max-d-p-storage-days-required' | translate}} - + {{ 'tenant-profile.max-d-p-storage-days-range' | translate}} @@ -339,10 +411,10 @@ - + {{ 'tenant-profile.alarms-ttl-days-required' | translate}} - + {{ 'tenant-profile.alarms-ttl-days-days-range' | translate}} @@ -354,10 +426,10 @@ - + {{ 'tenant-profile.default-storage-ttl-days-required' | translate}} - + {{ 'tenant-profile.default-storage-ttl-days-range' | translate}} @@ -367,10 +439,10 @@ - + {{ 'tenant-profile.rpc-ttl-days-required' | translate}} - + {{ 'tenant-profile.rpc-ttl-days-days-range' | translate}} @@ -382,10 +454,10 @@ - + {{ 'tenant-profile.queue-stats-ttl-days-required' | translate}} - + {{ 'tenant-profile.queue-stats-ttl-days-range' | translate}} @@ -395,10 +467,10 @@ - + {{ 'tenant-profile.rule-engine-exceptions-ttl-days-required' | translate}} - + {{ 'tenant-profile.rule-engine-exceptions-ttl-days-range' | translate}} @@ -413,16 +485,16 @@ {{ 'tenant-profile.sms-enabled' | translate }} - tenant-profile.max-sms - + {{ 'tenant-profile.max-sms-required' | translate}} - + {{ 'tenant-profile.max-sms-range' | translate}} @@ -432,10 +504,10 @@ - + {{ 'tenant-profile.max-emails-required' | translate}} - + {{ 'tenant-profile.max-emails-range' | translate}} @@ -445,10 +517,10 @@ - + {{ 'tenant-profile.max-created-alarms-required' | translate}} - + {{ 'tenant-profile.max-created-alarms-range' | translate}} @@ -466,7 +538,7 @@ - + {{ 'tenant-profile.maximum-debug-duration-min-range' | translate }} @@ -485,10 +557,10 @@ - + {{ 'tenant-profile.maximum-resources-sum-data-size-required' | translate}} - + {{ 'tenant-profile.maximum-resources-sum-data-size-range' | translate}} @@ -498,10 +570,10 @@ - + {{ 'tenant-profile.maximum-ota-package-sum-data-size-required' | translate}} - + {{ 'tenant-profile.maximum-ota-package-sum-data-size-range' | translate}} @@ -513,10 +585,10 @@ - + {{ 'tenant-profile.maximum-resource-size-required' | translate}} - + {{ 'tenant-profile.maximum-resource-size-range' | translate}} @@ -533,14 +605,14 @@ tenant-profile.ws-limit-max-sessions-per-tenant - + {{ 'tenant-profile.too-small-value-zero' | translate}} tenant-profile.ws-limit-max-subscriptions-per-tenant - + {{ 'tenant-profile.too-small-value-zero' | translate}} @@ -549,14 +621,14 @@ tenant-profile.ws-limit-max-sessions-per-customer - + {{ 'tenant-profile.too-small-value-zero' | translate}} tenant-profile.ws-limit-max-subscriptions-per-customer - + {{ 'tenant-profile.too-small-value-zero' | translate}} @@ -572,14 +644,14 @@ tenant-profile.ws-limit-max-sessions-per-public-user - + {{ 'tenant-profile.too-small-value-zero' | translate}} tenant-profile.ws-limit-max-subscriptions-per-public-user - + {{ 'tenant-profile.too-small-value-zero' | translate}} @@ -588,14 +660,14 @@ tenant-profile.ws-limit-max-sessions-per-regular-user - + {{ 'tenant-profile.too-small-value-zero' | translate}} tenant-profile.ws-limit-max-subscriptions-per-regular-user - + {{ 'tenant-profile.too-small-value-zero' | translate}} @@ -604,7 +676,7 @@ tenant-profile.ws-limit-queue-per-session - + {{ 'tenant-profile.too-small-value-one' | translate}} diff --git a/ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.ts b/ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.ts index c6efd9dee3..a61a1aa1f8 100644 --- a/ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.ts @@ -14,16 +14,13 @@ /// limitations under the License. /// -import { Component, forwardRef, Input, OnDestroy, OnInit } from '@angular/core'; -import { ControlValueAccessor, UntypedFormBuilder, UntypedFormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms'; -import { Store } from '@ngrx/store'; -import { AppState } from '@app/core/core.state'; -import { coerceBooleanProperty } from '@angular/cdk/coercion'; -import { DefaultTenantProfileConfiguration, TenantProfileConfiguration } from '@shared/models/tenant.model'; +import { Component, forwardRef, Input } from '@angular/core'; +import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms'; +import { DefaultTenantProfileConfiguration, FormControlsFrom } from '@shared/models/tenant.model'; import { isDefinedAndNotNull } from '@core/utils'; import { RateLimitsType } from './rate-limits/rate-limits.models'; -import { takeUntil } from 'rxjs/operators'; -import { Subject } from 'rxjs'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { coerceBoolean } from '@shared/decorators/coercion'; @Component({ selector: 'tb-default-tenant-profile-configuration', @@ -35,110 +32,109 @@ import { Subject } from 'rxjs'; multi: true }] }) -export class DefaultTenantProfileConfigurationComponent implements ControlValueAccessor, OnInit, OnDestroy { +export class DefaultTenantProfileConfigurationComponent implements ControlValueAccessor { - defaultTenantProfileConfigurationFormGroup: UntypedFormGroup; + tenantProfileConfigurationForm: FormGroup>; - private requiredValue: boolean; - private destroy$ = new Subject(); - get required(): boolean { - return this.requiredValue; - } @Input() - set required(value: boolean) { - this.requiredValue = coerceBooleanProperty(value); - } + @coerceBoolean() + required: boolean; @Input() + @coerceBoolean() disabled: boolean; rateLimitsType = RateLimitsType; - private propagateChange = (v: any) => { }; - - constructor(private store: Store, - private fb: UntypedFormBuilder) { - this.defaultTenantProfileConfigurationFormGroup = this.fb.group({ - maxDevices: [null, [Validators.required, Validators.min(0)]], - maxAssets: [null, [Validators.required, Validators.min(0)]], - maxCustomers: [null, [Validators.required, Validators.min(0)]], - maxUsers: [null, [Validators.required, Validators.min(0)]], - maxDashboards: [null, [Validators.required, Validators.min(0)]], - maxRuleChains: [null, [Validators.required, Validators.min(0)]], - maxEdges: [null, [Validators.required, Validators.min(0)]], - maxResourcesInBytes: [null, [Validators.required, Validators.min(0)]], - maxOtaPackagesInBytes: [null, [Validators.required, Validators.min(0)]], - maxResourceSize: [null, [Validators.required, Validators.min(0)]], - transportTenantMsgRateLimit: [null, []], - transportTenantTelemetryMsgRateLimit: [null, []], - transportTenantTelemetryDataPointsRateLimit: [null, []], - transportDeviceMsgRateLimit: [null, []], - transportDeviceTelemetryMsgRateLimit: [null, []], - transportDeviceTelemetryDataPointsRateLimit: [null, []], - transportGatewayMsgRateLimit: [null, []], - transportGatewayTelemetryMsgRateLimit: [null, []], - transportGatewayTelemetryDataPointsRateLimit: [null, []], - transportGatewayDeviceMsgRateLimit: [null, []], - transportGatewayDeviceTelemetryMsgRateLimit: [null, []], - transportGatewayDeviceTelemetryDataPointsRateLimit: [null, []], - tenantEntityExportRateLimit: [null, []], - tenantEntityImportRateLimit: [null, []], - tenantNotificationRequestsRateLimit: [null, []], - tenantNotificationRequestsPerRuleRateLimit: [null, []], - maxTransportMessages: [null, [Validators.required, Validators.min(0)]], - maxTransportDataPoints: [null, [Validators.required, Validators.min(0)]], - maxREExecutions: [null, [Validators.required, Validators.min(0)]], - maxJSExecutions: [null, [Validators.required, Validators.min(0)]], - maxTbelExecutions: [null, [Validators.required, Validators.min(0)]], - maxDPStorageDays: [null, [Validators.required, Validators.min(0)]], - maxRuleNodeExecutionsPerMessage: [null, [Validators.required, Validators.min(0)]], - maxEmails: [null, [Validators.required, Validators.min(0)]], - maxSms: [null, []], - smsEnabled: [null, []], - maxCreatedAlarms: [null, [Validators.required, Validators.min(0)]], - maxDebugModeDurationMinutes: [null, [Validators.min(0)]], - defaultStorageTtlDays: [null, [Validators.required, Validators.min(0)]], - alarmsTtlDays: [null, [Validators.required, Validators.min(0)]], - rpcTtlDays: [null, [Validators.required, Validators.min(0)]], - queueStatsTtlDays: [null, [Validators.required, Validators.min(0)]], - ruleEngineExceptionsTtlDays: [null, [Validators.required, Validators.min(0)]], - tenantServerRestLimitsConfiguration: [null, []], - customerServerRestLimitsConfiguration: [null, []], - maxWsSessionsPerTenant: [null, [Validators.min(0)]], - maxWsSessionsPerCustomer: [null, [Validators.min(0)]], - maxWsSessionsPerRegularUser: [null, [Validators.min(0)]], - maxWsSessionsPerPublicUser: [null, [Validators.min(0)]], - wsMsgQueueLimitPerSession: [null, [Validators.min(0)]], - maxWsSubscriptionsPerTenant: [null, [Validators.min(0)]], - maxWsSubscriptionsPerCustomer: [null, [Validators.min(0)]], - maxWsSubscriptionsPerRegularUser: [null, [Validators.min(0)]], - maxWsSubscriptionsPerPublicUser: [null, [Validators.min(0)]], - wsUpdatesPerSessionRateLimit: [null, []], - cassandraWriteQueryTenantCoreRateLimits: [null, []], - cassandraReadQueryTenantCoreRateLimits: [null, []], - cassandraWriteQueryTenantRuleEngineRateLimits: [null, []], - cassandraReadQueryTenantRuleEngineRateLimits: [null, []], - edgeEventRateLimits: [null, []], - edgeEventRateLimitsPerEdge: [null, []], - edgeUplinkMessagesRateLimits: [null, []], - edgeUplinkMessagesRateLimitsPerEdge: [null, []], - maxCalculatedFieldsPerEntity: [null, [Validators.required, Validators.min(0)]], - maxArgumentsPerCF: [null, [Validators.required, Validators.min(0)]], - maxDataPointsPerRollingArg: [null, [Validators.required, Validators.min(0)]], - maxStateSizeInKBytes: [null, [Validators.required, Validators.min(0)]], - calculatedFieldDebugEventsRateLimit: [null, []], - maxSingleValueArgumentSizeInKBytes: [null, [Validators.required, Validators.min(0)]], + private propagateChange = (_v: any) => { }; + + constructor(private fb: FormBuilder) { + this.tenantProfileConfigurationForm = this.fb.group({ + maxDevices: [0, [Validators.required, Validators.min(0)]], + maxAssets: [0, [Validators.required, Validators.min(0)]], + maxCustomers: [0, [Validators.required, Validators.min(0)]], + maxUsers: [0, [Validators.required, Validators.min(0)]], + maxDashboards: [0, [Validators.required, Validators.min(0)]], + maxRuleChains: [0, [Validators.required, Validators.min(0)]], + maxEdges: [0, [Validators.required, Validators.min(0)]], + maxResourcesInBytes: [0, [Validators.required, Validators.min(0)]], + maxOtaPackagesInBytes: [0, [Validators.required, Validators.min(0)]], + maxResourceSize: [0, [Validators.required, Validators.min(0)]], + transportTenantMsgRateLimit: [''], + transportTenantTelemetryMsgRateLimit: [''], + transportTenantTelemetryDataPointsRateLimit: [''], + transportDeviceMsgRateLimit: [''], + transportDeviceTelemetryMsgRateLimit: [''], + transportDeviceTelemetryDataPointsRateLimit: [''], + transportGatewayMsgRateLimit: [''], + transportGatewayTelemetryMsgRateLimit: [''], + transportGatewayTelemetryDataPointsRateLimit: [''], + transportGatewayDeviceMsgRateLimit: [''], + transportGatewayDeviceTelemetryMsgRateLimit: [''], + transportGatewayDeviceTelemetryDataPointsRateLimit: [''], + tenantEntityExportRateLimit: [''], + tenantEntityImportRateLimit: [''], + tenantNotificationRequestsRateLimit: [''], + tenantNotificationRequestsPerRuleRateLimit: [''], + maxTransportMessages: [0, [Validators.required, Validators.min(0)]], + maxTransportDataPoints: [0, [Validators.required, Validators.min(0)]], + maxREExecutions: [0, [Validators.required, Validators.min(0)]], + maxJSExecutions: [0, [Validators.required, Validators.min(0)]], + maxTbelExecutions: [0, [Validators.required, Validators.min(0)]], + maxDPStorageDays: [0, [Validators.required, Validators.min(0)]], + maxRuleNodeExecutionsPerMessage: [0, [Validators.required, Validators.min(0)]], + maxEmails: [0, [Validators.required, Validators.min(0)]], + maxSms: [0], + smsEnabled: [false], + maxCreatedAlarms: [0, [Validators.required, Validators.min(0)]], + maxDebugModeDurationMinutes: [0, [Validators.min(0)]], + defaultStorageTtlDays: [0, [Validators.required, Validators.min(0)]], + alarmsTtlDays: [0, [Validators.required, Validators.min(0)]], + rpcTtlDays: [0, [Validators.required, Validators.min(0)]], + queueStatsTtlDays: [0, [Validators.required, Validators.min(0)]], + ruleEngineExceptionsTtlDays: [0, [Validators.required, Validators.min(0)]], + tenantServerRestLimitsConfiguration: [''], + customerServerRestLimitsConfiguration: [''], + maxWsSessionsPerTenant: [0, [Validators.min(0)]], + maxWsSessionsPerCustomer: [0, [Validators.min(0)]], + maxWsSessionsPerRegularUser: [0, [Validators.min(0)]], + maxWsSessionsPerPublicUser: [0, [Validators.min(0)]], + wsMsgQueueLimitPerSession: [0, [Validators.min(0)]], + maxWsSubscriptionsPerTenant: [0, [Validators.min(0)]], + maxWsSubscriptionsPerCustomer: [0, [Validators.min(0)]], + maxWsSubscriptionsPerRegularUser: [0, [Validators.min(0)]], + maxWsSubscriptionsPerPublicUser: [0, [Validators.min(0)]], + wsUpdatesPerSessionRateLimit: [''], + cassandraWriteQueryTenantCoreRateLimits: [''], + cassandraReadQueryTenantCoreRateLimits: [''], + cassandraWriteQueryTenantRuleEngineRateLimits: [''], + cassandraReadQueryTenantRuleEngineRateLimits: [''], + edgeEventRateLimits: [''], + edgeEventRateLimitsPerEdge: [''], + edgeUplinkMessagesRateLimits: [''], + edgeUplinkMessagesRateLimitsPerEdge: [''], + maxCalculatedFieldsPerEntity: [0, [Validators.required, Validators.min(0)]], + maxArgumentsPerCF: [0, [Validators.required, Validators.min(0)]], + maxRelationLevelPerCfArgument: [1, [Validators.required, Validators.min(1)]], + minAllowedDeduplicationIntervalInSecForCF: [0, [Validators.required, Validators.min(0)]], + minAllowedAggregationIntervalInSecForCF: [0, [Validators.required, Validators.min(0)]], + maxRelatedEntitiesToReturnPerCfArgument: [1, [Validators.required, Validators.min(1)]], + minAllowedScheduledUpdateIntervalInSecForCF: [0, [Validators.required, Validators.min(0)]], + maxDataPointsPerRollingArg: [0, [Validators.required, Validators.min(0)]], + maxStateSizeInKBytes: [0, [Validators.required, Validators.min(0)]], + calculatedFieldDebugEventsRateLimit: [''], + maxSingleValueArgumentSizeInKBytes: [0, [Validators.required, Validators.min(0)]], }); - this.defaultTenantProfileConfigurationFormGroup.get('smsEnabled').valueChanges.pipe( - takeUntil(this.destroy$) + this.tenantProfileConfigurationForm.get('smsEnabled').valueChanges.pipe( + takeUntilDestroyed() ).subscribe((value: boolean) => { this.maxSmsValidation(value); } ); - this.defaultTenantProfileConfigurationFormGroup.valueChanges.pipe( - takeUntil(this.destroy$) + this.tenantProfileConfigurationForm.valueChanges.pipe( + takeUntilDestroyed() ).subscribe(() => { this.updateModel(); }); @@ -146,48 +142,40 @@ export class DefaultTenantProfileConfigurationComponent implements ControlValueA private maxSmsValidation(smsEnabled: boolean) { if (smsEnabled) { - this.defaultTenantProfileConfigurationFormGroup.get('maxSms').addValidators([Validators.required, Validators.min(0)]); + this.tenantProfileConfigurationForm.get('maxSms').addValidators([Validators.required, Validators.min(0)]); } else { - this.defaultTenantProfileConfigurationFormGroup.get('maxSms').clearValidators(); + this.tenantProfileConfigurationForm.get('maxSms').clearValidators(); } - this.defaultTenantProfileConfigurationFormGroup.get('maxSms').updateValueAndValidity({emitEvent: false}); - } - - ngOnDestroy(): void { - this.destroy$.next(); - this.destroy$.complete(); + this.tenantProfileConfigurationForm.get('maxSms').updateValueAndValidity({emitEvent: false}); } registerOnChange(fn: any): void { this.propagateChange = fn; } - registerOnTouched(fn: any): void { - } - - ngOnInit() { + registerOnTouched(_fn: any): void { } setDisabledState(isDisabled: boolean): void { this.disabled = isDisabled; if (this.disabled) { - this.defaultTenantProfileConfigurationFormGroup.disable({emitEvent: false}); + this.tenantProfileConfigurationForm.disable({emitEvent: false}); } else { - this.defaultTenantProfileConfigurationFormGroup.enable({emitEvent: false}); + this.tenantProfileConfigurationForm.enable({emitEvent: false}); } } writeValue(value: DefaultTenantProfileConfiguration | null): void { if (isDefinedAndNotNull(value)) { this.maxSmsValidation(value.smsEnabled); - this.defaultTenantProfileConfigurationFormGroup.patchValue(value, {emitEvent: false}); + this.tenantProfileConfigurationForm.patchValue(value, {emitEvent: false}); } } private updateModel() { - let configuration: TenantProfileConfiguration = null; - if (this.defaultTenantProfileConfigurationFormGroup.valid) { - configuration = this.defaultTenantProfileConfigurationFormGroup.getRawValue(); + let configuration: DefaultTenantProfileConfiguration = null; + if (this.tenantProfileConfigurationForm.valid) { + configuration = this.tenantProfileConfigurationForm.getRawValue(); } this.propagateChange(configuration); } diff --git a/ui-ngx/src/app/modules/home/components/relation/relation-filters.component.ts b/ui-ngx/src/app/modules/home/components/relation/relation-filters.component.ts index 92cd675ee8..e2d19b9e6c 100644 --- a/ui-ngx/src/app/modules/home/components/relation/relation-filters.component.ts +++ b/ui-ngx/src/app/modules/home/components/relation/relation-filters.component.ts @@ -128,7 +128,7 @@ export class RelationFiltersComponent extends PageComponent implements ControlVa entityTypes: [filter ? filter.entityTypes : []] }); if (this.enableNotOption) { - formGroup.addControl('negate', this.fb.control({value: filter ? filter.negate : false, disabled: true})); + formGroup.addControl('negate', this.fb.control({value: filter?.negate ?? false, disabled: !filter?.relationType})); formGroup.get('relationType').valueChanges.pipe( takeUntil(this.destroy$) ).subscribe(value => { diff --git a/ui-ngx/src/app/modules/home/components/relation/relation-table.component.html b/ui-ngx/src/app/modules/home/components/relation/relation-table.component.html index 9afaa889b4..7e75a00067 100644 --- a/ui-ngx/src/app/modules/home/components/relation/relation-table.component.html +++ b/ui-ngx/src/app/modules/home/components/relation/relation-table.component.html @@ -117,9 +117,13 @@ {{ 'relation.to-entity-name' | translate }} - - {{ relation.toName | customTranslate }} - + @if(relation.entityURL){ + + {{ relation.toName | customTranslate }} + + } @else { + {{ relation.toName | customTranslate }} + } diff --git a/ui-ngx/src/app/modules/home/components/resources/resources-dialog.component.html b/ui-ngx/src/app/modules/home/components/resources/resources-dialog.component.html new file mode 100644 index 0000000000..0062cfa9ac --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/resources/resources-dialog.component.html @@ -0,0 +1,54 @@ + + + +

    {{ 'resource.add' | translate }}

    + + +
    + + +
    +
    + + +
    +
    + + +
    + diff --git a/ui-ngx/src/app/shared/components/string-items-list.component.scss b/ui-ngx/src/app/modules/home/components/resources/resources-dialog.component.scss similarity index 80% rename from ui-ngx/src/app/shared/components/string-items-list.component.scss rename to ui-ngx/src/app/modules/home/components/resources/resources-dialog.component.scss index 726344037c..b32c3933c5 100644 --- a/ui-ngx/src/app/shared/components/string-items-list.component.scss +++ b/ui-ngx/src/app/modules/home/components/resources/resources-dialog.component.scss @@ -13,10 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -.tb-string-items-list { - .mat-mdc-standard-chip { - .mdc-evolution-chip__cell--primary, .mat-mdc-chip-action-label { - overflow: hidden; - } + +:host ::ng-deep { + .mat-mdc-dialog-content { + display: flex; + flex-direction: column; + height: 100%; + padding: 0 !important; } } diff --git a/ui-ngx/src/app/modules/home/components/resources/resources-dialog.component.ts b/ui-ngx/src/app/modules/home/components/resources/resources-dialog.component.ts new file mode 100644 index 0000000000..6216f087b1 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/resources/resources-dialog.component.ts @@ -0,0 +1,116 @@ +/// +/// Copyright © 2016-2025 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { AfterViewInit, Component, Inject, SkipSelf, ViewChild } from '@angular/core'; +import { DialogComponent } from '@shared/components/dialog.component'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { Router } from '@angular/router'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { FormGroupDirective, NgForm, UntypedFormControl } from '@angular/forms'; +import { EntityType } from '@shared/models/entity-type.models'; +import { map } from 'rxjs/operators'; +import { ResourcesLibraryComponent } from "@home/components/resources/resources-library.component"; +import { ErrorStateMatcher } from "@angular/material/core"; +import { Resource, ResourceType } from "@shared/models/resource.models"; +import { ResourceService } from "@core/http/resource.service"; + +export interface ResourcesDialogData { + resources?: Resource; + isAdd?: boolean; +} + +@Component({ + selector: 'tb-resources-dialog', + templateUrl: './resources-dialog.component.html', + providers: [{provide: ErrorStateMatcher, useExisting: ResourcesDialogComponent}], + styleUrls: ['./resources-dialog.component.scss'] +}) +export class ResourcesDialogComponent extends DialogComponent implements ErrorStateMatcher, AfterViewInit { + + readonly entityType = EntityType; + + ResourceType = ResourceType; + + isAdd = false; + + submitted = false; + + resources: Resource; + + @ViewChild('resourcesComponent', {static: true}) resourcesComponent: ResourcesLibraryComponent; + + constructor(protected store: Store, + protected router: Router, + protected dialogRef: MatDialogRef, + @Inject(MAT_DIALOG_DATA) public data: ResourcesDialogData, + @SkipSelf() private errorStateMatcher: ErrorStateMatcher, + private resourceService: ResourceService) { + super(store, router, dialogRef); + + if (this.data.isAdd) { + this.isAdd = true; + } + + if (this.data.resources) { + this.resources = this.data.resources; + } + } + + ngAfterViewInit(): void { + if (this.isAdd) { + setTimeout(() => { + this.resourcesComponent.entityForm.markAsDirty(); + }, 0); + } + } + + isErrorState(control: UntypedFormControl | null, form: FormGroupDirective | NgForm | null): boolean { + const originalErrorState = this.errorStateMatcher.isErrorState(control, form); + const customErrorState = !!(control && control.invalid && this.submitted); + return originalErrorState || customErrorState; + } + + cancel(): void { + this.dialogRef.close(null); + } + + save(): void { + this.submitted = true; + if (this.resourcesComponent.entityForm.valid) { + const resource = {...this.resourcesComponent.entityFormValue()}; + if (Array.isArray(resource.data)) { + const resources = []; + resource.data.forEach((data, index) => { + resources.push({ + resourceType: resource.resourceType, + data, + fileName: resource.fileName[index], + title: resource.title + }); + }); + this.resourceService.saveResources(resources, {resendRequest: true}).pipe( + map((response) => response[0]) + ).subscribe(result => this.dialogRef.close(result)); + } else { + if (resource.resourceType !== ResourceType.GENERAL) { + delete resource.descriptor; + } + this.resourceService.saveResource(resource).subscribe(result => this.dialogRef.close(result)); + } + } + } +} diff --git a/ui-ngx/src/app/modules/home/pages/admin/resource/resources-library.component.html b/ui-ngx/src/app/modules/home/components/resources/resources-library.component.html similarity index 64% rename from ui-ngx/src/app/modules/home/pages/admin/resource/resources-library.component.html rename to ui-ngx/src/app/modules/home/components/resources/resources-library.component.html index ebb946ccbc..819753e105 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/resource/resources-library.component.html +++ b/ui-ngx/src/app/modules/home/components/resources/resources-library.component.html @@ -15,7 +15,7 @@ limitations under the License. --> -
    +
    @@ -66,26 +66,29 @@ {{ 'resource.title-max-length' | translate }} - - -
    - - resource.file-name - - -
    + @if (isAdd || ((isAdd || isEdit) && entityForm.get('resourceType').value === resourceType.GENERAL)) { + + + } @else { +
    + + resource.file-name + + +
    + }
    diff --git a/ui-ngx/src/app/modules/home/pages/admin/resource/resources-library.component.ts b/ui-ngx/src/app/modules/home/components/resources/resources-library.component.ts similarity index 81% rename from ui-ngx/src/app/modules/home/pages/admin/resource/resources-library.component.ts rename to ui-ngx/src/app/modules/home/components/resources/resources-library.component.ts index 971961ff68..977eb6873a 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/resource/resources-library.component.ts +++ b/ui-ngx/src/app/modules/home/components/resources/resources-library.component.ts @@ -14,7 +14,7 @@ /// limitations under the License. /// -import { ChangeDetectorRef, Component, Inject, OnDestroy, OnInit } from '@angular/core'; +import { ChangeDetectorRef, Component, Inject, Input, OnDestroy, OnInit, Optional } from '@angular/core'; import { Subject } from 'rxjs'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; @@ -40,8 +40,16 @@ import { getCurrentAuthState } from '@core/auth/auth.selectors'; }) export class ResourcesLibraryComponent extends EntityComponent implements OnInit, OnDestroy { + @Input() + standalone = false; + + @Input() + resourceTypes = [ResourceType.LWM2M_MODEL, ResourceType.PKCS_12, ResourceType.JKS, ResourceType.GENERAL]; + + @Input() + defaultResourceType = ResourceType.LWM2M_MODEL; + readonly resourceType = ResourceType; - readonly resourceTypes = [ResourceType.LWM2M_MODEL, ResourceType.PKCS_12, ResourceType.JKS]; readonly resourceTypesTranslationMap = ResourceTypeTranslationMap; readonly maxResourceSize = getCurrentAuthState(this.store).maxResourceSize; @@ -49,8 +57,8 @@ export class ResourcesLibraryComponent extends EntityComponent impleme constructor(protected store: Store, protected translate: TranslateService, - @Inject('entity') protected entityValue: Resource, - @Inject('entitiesTableConfig') protected entitiesTableConfigValue: EntityTableConfig, + @Optional() @Inject('entity') protected entityValue: Resource, + @Optional() @Inject('entitiesTableConfig') protected entitiesTableConfigValue: EntityTableConfig, public fb: FormBuilder, protected cd: ChangeDetectorRef) { super(store, fb, entityValue, entitiesTableConfigValue, cd); @@ -82,10 +90,19 @@ export class ResourcesLibraryComponent extends EntityComponent impleme title: [entity ? entity.title : '', [Validators.required, Validators.maxLength(255)]], resourceType: [entity?.resourceType ? entity.resourceType : ResourceType.LWM2M_MODEL, Validators.required], fileName: [entity ? entity.fileName : null, Validators.required], - data: [entity ? entity.data : null, this.isAdd ? [Validators.required] : []] + data: [entity ? entity.data : null, this.isAdd ? [Validators.required] : []], + descriptor: this.fb.group({ + mediaType: [''] + }) }); } + mediaTypeChange(mediaType: string): void { + if (this.entityForm.get('resourceType').value === ResourceType.GENERAL) { + this.entityForm.get('descriptor').get('mediaType').patchValue(mediaType); + } + } + updateForm(entity: Resource): void { this.entityForm.patchValue(entity); } @@ -95,6 +112,10 @@ export class ResourcesLibraryComponent extends EntityComponent impleme if (this.isEdit && this.entityForm && !this.isAdd) { this.entityForm.get('resourceType').disable({ emitEvent: false }); this.entityForm.get('fileName').disable({ emitEvent: false }); + this.entityForm.get('data').disable({ emitEvent: false }); + } + if (this.isAdd && this.resourceTypes.length === 1) { + this.entityForm.get('resourceType').disable({ emitEvent: false }); } } @@ -138,7 +159,7 @@ export class ResourcesLibraryComponent extends EntityComponent impleme private observeResourceTypeChange(): void { this.entityForm.get('resourceType').valueChanges.pipe( - startWith(ResourceType.LWM2M_MODEL), + startWith(this.defaultResourceType || ResourceType.LWM2M_MODEL), takeUntil(this.destroy$) ).subscribe((type: ResourceType) => this.onResourceTypeChange(type)); } diff --git a/ui-ngx/src/app/modules/home/components/rule-node/action/advanced-processing-setting.component.html b/ui-ngx/src/app/modules/home/components/rule-node/action/advanced-processing-setting.component.html index edce8b12a9..a542d3e07d 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/action/advanced-processing-setting.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/action/advanced-processing-setting.component.html @@ -39,6 +39,6 @@ > diff --git a/ui-ngx/src/app/modules/home/components/rule-node/common/common-rule-node-config.module.ts b/ui-ngx/src/app/modules/home/components/rule-node/common/common-rule-node-config.module.ts index c7d8ce2723..049e489fde 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/common/common-rule-node-config.module.ts +++ b/ui-ngx/src/app/modules/home/components/rule-node/common/common-rule-node-config.module.ts @@ -33,7 +33,6 @@ import { RelationsQueryConfigOldComponent } from './relations-query-config-old.c import { SelectAttributesComponent } from './select-attributes.component'; import { AlarmStatusSelectComponent } from './alarm-status-select.component'; import { ExampleHintComponent } from './example-hint.component'; -import { TimeUnitInputComponent } from './time-unit-input.component'; @NgModule({ declarations: [ @@ -52,7 +51,6 @@ import { TimeUnitInputComponent } from './time-unit-input.component'; SelectAttributesComponent, AlarmStatusSelectComponent, ExampleHintComponent, - TimeUnitInputComponent ], imports: [ CommonModule, @@ -75,7 +73,6 @@ import { TimeUnitInputComponent } from './time-unit-input.component'; SelectAttributesComponent, AlarmStatusSelectComponent, ExampleHintComponent, - TimeUnitInputComponent ] }) diff --git a/ui-ngx/src/app/modules/home/components/rule-node/common/credentials-config.component.html b/ui-ngx/src/app/modules/home/components/rule-node/common/credentials-config.component.html index 7f7d50db3e..59bfa7ce9f 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/common/credentials-config.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/common/credentials-config.component.html @@ -49,7 +49,7 @@ rule-node-config.password - + {{ 'rule-node-config.password-required' | translate }} @@ -85,7 +85,7 @@ rule-node-config.private-key-password - + diff --git a/ui-ngx/src/app/modules/home/components/rule-node/external/ai-config.component.html b/ui-ngx/src/app/modules/home/components/rule-node/external/ai-config.component.html index 80519cea28..73b24d4274 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/external/ai-config.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/external/ai-config.component.html @@ -29,7 +29,7 @@ labelText="rule-node-config.ai.model" (entityChanged)="onEntityChange($event)" [entityType]="entityType.AI_MODEL" - (createNew)="createModelAi('modelId')" + (createNew)="createModelAi($event, 'modelId')" formControlName="modelId"> @@ -70,6 +70,17 @@ {{ 'rule-node-config.ai.user-prompt-blank' | translate }} + + @@ -79,25 +90,24 @@
    {{ 'rule-node-config.ai.response-format' | translate }}
    - + {{ 'rule-node-config.ai.response-text' | translate }} {{ 'rule-node-config.ai.response-json' | translate }} {{ 'rule-node-config.ai.response-json-schema' | translate }} - @if (aiConfigForm.get('responseFormat.type').value === responseFormat.JSON_SCHEMA) { - - - - } + + +
    diff --git a/ui-ngx/src/app/modules/home/components/rule-node/external/ai-config.component.ts b/ui-ngx/src/app/modules/home/components/rule-node/external/ai-config.component.ts index 47313da81d..dc50051a8a 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/external/ai-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/rule-node/external/ai-config.component.ts @@ -23,6 +23,9 @@ import { AIModelDialogComponent, AIModelDialogData } from '@home/components/ai-m import { AiModel, AiRuleNodeResponseFormatTypeOnlyText, ResponseFormat } from '@shared/models/ai-model.models'; import { deepTrim } from '@core/utils'; import { TranslateService } from '@ngx-translate/core'; +import { jsonRequired } from '@shared/components/json-object-edit.component'; +import { Resource, ResourceType } from "@shared/models/resource.models"; +import { ResourcesDialogComponent, ResourcesDialogData } from "@home/components/resources/resources-dialog.component"; @Component({ selector: 'tb-external-node-ai-config', @@ -37,7 +40,8 @@ export class AiConfigComponent extends RuleNodeConfigurationComponent { responseFormat = ResponseFormat; - disabledResponseFormatType: boolean; + EntityType = EntityType; + ResourceType = ResourceType; constructor(private fb: UntypedFormBuilder, private translate: TranslateService, @@ -52,11 +56,12 @@ export class AiConfigComponent extends RuleNodeConfigurationComponent { protected onConfigurationSet(configuration: RuleNodeConfiguration) { this.aiConfigForm = this.fb.group({ modelId: [configuration?.modelId ?? null, [Validators.required]], - systemPrompt: [configuration?.systemPrompt ?? '', [Validators.maxLength(10000), Validators.pattern(/.*\S.*/)]], - userPrompt: [configuration?.userPrompt ?? '', [Validators.required, Validators.maxLength(10000), Validators.pattern(/.*\S.*/)]], + systemPrompt: [configuration?.systemPrompt ?? '', [Validators.maxLength(500_000), Validators.pattern(/.*\S.*/)]], + userPrompt: [configuration?.userPrompt ?? '', [Validators.required, Validators.maxLength(500_000), Validators.pattern(/.*\S.*/)]], + resourceIds: [configuration?.resourceIds ?? []], responseFormat: this.fb.group({ type: [configuration?.responseFormat?.type ?? ResponseFormat.JSON, []], - schema: [configuration?.responseFormat?.schema ?? null, [Validators.required]], + schema: [configuration?.responseFormat?.schema ?? null, [jsonRequired]], }), timeoutSeconds: [configuration?.timeoutSeconds ?? 60, []], forceAck: [configuration?.forceAck ?? true, []] @@ -75,11 +80,15 @@ export class AiConfigComponent extends RuleNodeConfigurationComponent { } } - protected prepareOutputConfig(configuration: RuleNodeConfiguration): RuleNodeConfiguration { + protected prepareOutputConfig(): RuleNodeConfiguration { + const config = this.configForm().getRawValue(); if (!this.aiConfigForm.get('systemPrompt').value) { - delete configuration.systemPrompt; + delete config.systemPrompt; } - return deepTrim(configuration); + if (this.aiConfigForm.get('responseFormat.type').value !== ResponseFormat.JSON_SCHEMA) { + delete config.responseFormat.schema; + } + return deepTrim(config); } onEntityChange($event: AiModel) { @@ -88,10 +97,10 @@ export class AiConfigComponent extends RuleNodeConfigurationComponent { if (this.aiConfigForm.get('responseFormat.type').value !== ResponseFormat.TEXT) { this.aiConfigForm.get('responseFormat.type').patchValue(ResponseFormat.TEXT, {emitEvent: true}); } - this.disabledResponseFormatType = true; + this.aiConfigForm.get('responseFormat.type').disable({emitEvent: false}); } } else { - this.disabledResponseFormatType = false; + this.aiConfigForm.get('responseFormat.type').enable({emitEvent: false}); } } @@ -99,12 +108,13 @@ export class AiConfigComponent extends RuleNodeConfigurationComponent { return this.translate.instant(`rule-node-config.ai.response-format-hint-${this.aiConfigForm.get('responseFormat.type').value}`); } - createModelAi(formControl: string) { + createModelAi(name: string, formControl: string) { this.dialog.open(AIModelDialogComponent, { disableClose: true, panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], data: { - isAdd: true + isAdd: true, + name } }).afterClosed() .subscribe((model) => { @@ -113,5 +123,23 @@ export class AiConfigComponent extends RuleNodeConfigurationComponent { this.aiConfigForm.get(formControl).markAsDirty(); } }); + }; + + createAiResources(name: string, formControl: string) { + this.dialog.open(ResourcesDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + resources: {title: name, resourceType: ResourceType.GENERAL}, + isAdd: true + } + }).afterClosed() + .subscribe((resource) => { + if (resource) { + const resourceIds = [...(this.aiConfigForm.get(formControl).value || []), resource.id.id]; + this.aiConfigForm.get(formControl).patchValue(resourceIds); + this.aiConfigForm.get(formControl).markAsDirty(); + } + }); } } diff --git a/ui-ngx/src/app/modules/home/components/rule-node/external/rabbit-mq-config.component.html b/ui-ngx/src/app/modules/home/components/rule-node/external/rabbit-mq-config.component.html index 0676af17f9..6adf2e6a4e 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/external/rabbit-mq-config.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/external/rabbit-mq-config.component.html @@ -64,7 +64,7 @@ rule-node-config.password - + diff --git a/ui-ngx/src/app/modules/home/components/rule-node/external/send-email-config.component.html b/ui-ngx/src/app/modules/home/components/rule-node/external/send-email-config.component.html index 0e9db748bf..545f9ba985 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/external/send-email-config.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/external/send-email-config.component.html @@ -109,7 +109,7 @@ rule-node-config.password - + diff --git a/ui-ngx/src/app/modules/home/components/sms/aws-sns-provider-configuration.component.html b/ui-ngx/src/app/modules/home/components/sms/aws-sns-provider-configuration.component.html index 14d4a227ff..afd272e1cd 100644 --- a/ui-ngx/src/app/modules/home/components/sms/aws-sns-provider-configuration.component.html +++ b/ui-ngx/src/app/modules/home/components/sms/aws-sns-provider-configuration.component.html @@ -25,7 +25,7 @@ admin.aws-secret-access-key - + {{ 'admin.aws-secret-access-key-required' | translate }} diff --git a/ui-ngx/src/app/modules/home/components/vc/auto-commit-settings.component.html b/ui-ngx/src/app/modules/home/components/vc/auto-commit-settings.component.html index d80892664d..2bf50da468 100644 --- a/ui-ngx/src/app/modules/home/components/vc/auto-commit-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/vc/auto-commit-settings.component.html @@ -38,7 +38,7 @@
    - +
    @@ -60,6 +60,7 @@
    -
    {{ 'version-control.export-credentials' | translate }} diff --git a/ui-ngx/src/app/modules/home/components/vc/auto-commit-settings.component.scss b/ui-ngx/src/app/modules/home/components/vc/auto-commit-settings.component.scss index a72bd2ca77..e925c79b0d 100644 --- a/ui-ngx/src/app/modules/home/components/vc/auto-commit-settings.component.scss +++ b/ui-ngx/src/app/modules/home/components/vc/auto-commit-settings.component.scss @@ -52,7 +52,6 @@ padding: 0 8px 8px; tb-branch-autocomplete { min-width: 200px; - max-width: 200px; display: block; } } diff --git a/ui-ngx/src/app/modules/home/components/vc/complex-version-create.component.html b/ui-ngx/src/app/modules/home/components/vc/complex-version-create.component.html index e97e02190a..e6d9ec34bc 100644 --- a/ui-ngx/src/app/modules/home/components/vc/complex-version-create.component.html +++ b/ui-ngx/src/app/modules/home/components/vc/complex-version-create.component.html @@ -15,76 +15,79 @@ limitations under the License. --> -
    - -

    {{ 'version-control.create-entities-version' | translate }}

    - -
    - - -
    -
    - - - - version-control.version-name - - - {{ 'version-control.version-name-required' | translate }} - +@if (!versionCreateResult$) { +
    + +

    {{ 'version-control.create-entities-version' | translate }}

    + +
    + + + +
    + + + + version-control.version-name + + + {{ 'version-control.version-name-required' | translate }} + + +
    + + version-control.default-sync-strategy + + @for (strategy of syncStrategies; track strategy) { + + {{syncStrategyTranslations.get(strategy) | translate}} + + } + + + + + +
    + +
    - - version-control.default-sync-strategy - - - {{syncStrategyTranslations.get(strategy) | translate}} - - - - - - - -
    - - -
    -
    -
    -
    -
    -
    - -
    - +} @else { +
    + @if ((versionCreateResult$ | async)?.done || hasError) { +
    + +
    + } @else {
    version-control.creating-version
    -
    -
    + } +} diff --git a/ui-ngx/src/app/modules/home/components/vc/complex-version-load.component.html b/ui-ngx/src/app/modules/home/components/vc/complex-version-load.component.html index 17e6663a03..885058c9c9 100644 --- a/ui-ngx/src/app/modules/home/components/vc/complex-version-load.component.html +++ b/ui-ngx/src/app/modules/home/components/vc/complex-version-load.component.html @@ -15,59 +15,63 @@ limitations under the License. --> -
    - -

    {{ 'version-control.restore-entities-from-version' | translate: {versionName} }}

    - -
    - - -
    - - -
    -
    - - {{ 'version-control.rollback-on-error' | translate }} - -
    -
    - - -
    -
    -
    -
    +@if (!versionLoadResult$) { +
    + +

    {{ 'version-control.restore-entities-from-version' | translate: {versionName} }}

    + +
    + + +
    + + +
    +
    + + {{ 'version-control.rollback-on-error' | translate }} + +
    +
    + + +
    +
    +} @else { +
    {{ 'version-control.no-entities-restored' | translate }}
    -
    -
    {{ entityTypeLoadResultMessage(entityTypeLoadResult) }}
    -
    - -
    - +
    + @for (entityTypeLoadResult of entityTypeLoadResults; track entityTypeLoadResult.entityType) { +
    {{ entityTypeLoadResultMessage(entityTypeLoadResult) }}
    + } + @if ((versionLoadResult$ | async)?.done || hasError) { +
    + +
    + } @else {
    version-control.restoring-entities-from-version
    -
    -
    + } +} diff --git a/ui-ngx/src/app/modules/home/components/vc/entity-types-version-create.component.html b/ui-ngx/src/app/modules/home/components/vc/entity-types-version-create.component.html index 8fdf22f6cd..b2160e8ab8 100644 --- a/ui-ngx/src/app/modules/home/components/vc/entity-types-version-create.component.html +++ b/ui-ngx/src/app/modules/home/components/vc/entity-types-version-create.component.html @@ -18,86 +18,90 @@
    version-control.entities-to-export
    -
    - - -
    - -
    -
    {{ entityTypeText(entityTypeFormGroup) }}
    -
    -
    - - -
    -
    -
    - -
    - - -
    - - version-control.sync-strategy - - - {{ 'version-control.default' | translate }} - - - {{syncStrategyTranslations.get(strategy) | translate}} - - - -
    - - {{ 'version-control.export-credentials' | translate }} - - - {{ 'version-control.export-attributes' | translate }} - - - {{ 'version-control.export-relations' | translate }} - - - {{ 'version-control.export-calculated-fields' | translate }} - + @for (entityTypeFormGroup of entityTypesFormGroupArray(); track entityTypeFormGroup; let index = $index, isLast = $last) { +
    + + +
    + +
    +
    {{ entityTypeText(entityTypeFormGroup) }}
    +
    +
    + + +
    +
    +
    + +
    + + +
    + + version-control.sync-strategy + + + {{ 'version-control.default' | translate }} + + @for (strategy of syncStrategies; track strategy) { + + {{syncStrategyTranslations.get(strategy) | translate}} + + } + + +
    + + {{ 'version-control.export-credentials' | translate }} + + + {{ 'version-control.export-attributes' | translate }} + + + {{ 'version-control.export-relations' | translate }} + + + {{ 'version-control.export-calculated-fields' | translate }} + +
    +
    + + {{ 'version-control.all-entities' | translate }} + + @if (!entityTypeFormGroup.get('config').get('allEntities').value) { + + + } +
    -
    - - {{ 'version-control.all-entities' | translate }} - - - -
    -
    - -
    -
    - version-control.no-entities-to-export-prompt -
    + +
    + } @empty { + version-control.no-entities-to-export-prompt + }
    -
    - -
    - -
    - - -
    -
    - - {{ 'version-control.remove-other-entities' | translate }} - - - {{ 'version-control.find-existing-entity-by-name' | translate }} - -
    -
    - - {{ 'version-control.load-credentials' | translate }} - - - {{ 'version-control.load-attributes' | translate }} - - - {{ 'version-control.load-relations' | translate }} - - - {{ 'version-control.load-calculated-fields' | translate }} - + @for (entityTypeFormGroup of entityTypesFormGroupArray(); track entityTypeFormGroup; let index = $index, isLast = $last) { +
    + + +
    + +
    +
    {{ entityTypeText(entityTypeFormGroup) }}
    +
    +
    + + +
    +
    +
    + +
    + + +
    +
    + + {{ 'version-control.remove-other-entities' | translate }} + + + {{ 'version-control.find-existing-entity-by-name' | translate }} + +
    +
    + + {{ 'version-control.load-credentials' | translate }} + + + {{ 'version-control.load-attributes' | translate }} + + + {{ 'version-control.load-relations' | translate }} + + + {{ 'version-control.load-calculated-fields' | translate }} + +
    -
    - -
    -
    - version-control.no-entities-to-restore-prompt -
    + +
    + } @empty { + version-control.no-entities-to-restore-prompt + }
    -
    -
    -
    -
    +} @else { + @if ((versionCreateResult$ | async)?.done || resultMessage) { +
    {{ resultMessage }}
    -
    - + } @else {
    version-control.creating-version
    -
    -
    + } +} diff --git a/ui-ngx/src/app/modules/home/components/vc/entity-version-restore.component.html b/ui-ngx/src/app/modules/home/components/vc/entity-version-restore.component.html index 1183223850..e4312e0c98 100644 --- a/ui-ngx/src/app/modules/home/components/vc/entity-version-restore.component.html +++ b/ui-ngx/src/app/modules/home/components/vc/entity-version-restore.component.html @@ -15,51 +15,53 @@ limitations under the License. --> -
    - -

    {{ 'version-control.restore-entity-from-version' | translate: {versionName} }}

    - -
    - - - - -
    -
    - - {{ 'version-control.load-credentials' | translate }} - - - {{ 'version-control.load-attributes' | translate }} - - - {{ 'version-control.load-relations' | translate }} - - - {{ 'version-control.load-calculated-fields' | translate }} - -
    -
    - -
    - - -
    -
    -
    -
    -
    +@if (!versionLoadResult$) { + @if (entityDataInfo) { + +

    {{ 'version-control.restore-entity-from-version' | translate: {versionName} }}

    + +
    + + +
    +
    +
    + + {{ 'version-control.load-credentials' | translate }} + + + {{ 'version-control.load-attributes' | translate }} + + + {{ 'version-control.load-relations' | translate }} + + + {{ 'version-control.load-calculated-fields' | translate }} + +
    +
    +
    +
    + + +
    + } @else { + + } +} @else { + @if ((versionLoadResult$ | async)?.done || errorMessage) { +
    -
    - + } @else {
    version-control.restoring-entity-version
    -
    -
    + } +} diff --git a/ui-ngx/src/app/modules/home/components/vc/remove-other-entities-confirm.component.html b/ui-ngx/src/app/modules/home/components/vc/remove-other-entities-confirm.component.html index 71796247dc..6bd46bb9c8 100644 --- a/ui-ngx/src/app/modules/home/components/vc/remove-other-entities-confirm.component.html +++ b/ui-ngx/src/app/modules/home/components/vc/remove-other-entities-confirm.component.html @@ -19,7 +19,7 @@
    - +
    diff --git a/ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions.component.ts b/ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions.component.ts index a9b9d207d0..01404f7c6e 100644 --- a/ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions.component.ts @@ -24,6 +24,7 @@ import { NgZone, OnDestroy, OnInit, + SecurityContext, ViewChild } from '@angular/core'; import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; @@ -53,6 +54,7 @@ import { import { deepClone } from '@core/utils'; import { hidePageSizePixelValue } from '@shared/models/constants'; import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop'; +import { DomSanitizer } from '@angular/platform-browser'; @Component({ selector: 'tb-manage-widget-actions', @@ -106,7 +108,8 @@ export class ManageWidgetActionsComponent extends PageComponent implements OnIni private dialogs: DialogService, private cd: ChangeDetectorRef, private elementRef: ElementRef, - private zone: NgZone) { + private zone: NgZone, + private sanitizer: DomSanitizer) { super(); const sortOrder: SortOrder = { property: 'actionSourceName', direction: Direction.ASC }; this.pageLink = new PageLink(10, 0, null, sortOrder); @@ -289,7 +292,8 @@ export class ManageWidgetActionsComponent extends PageComponent implements OnIni } const title = this.translate.instant('widget-config.delete-action-title'); const content = this.translate.instant('widget-config.delete-action-text', {actionName: action.name}); - this.dialogs.confirm(title, content, + const safeContent = this.sanitizer.sanitize(SecurityContext.HTML, content); + this.dialogs.confirm(title, safeContent, this.translate.instant('action.no'), this.translate.instant('action.yes'), true).subscribe( (res) => { diff --git a/ui-ngx/src/app/modules/home/components/widget/action/widget-action-dialog.component.html b/ui-ngx/src/app/modules/home/components/widget/action/widget-action-dialog.component.html index e5d006076e..48a1e17044 100644 --- a/ui-ngx/src/app/modules/home/components/widget/action/widget-action-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/action/widget-action-dialog.component.html @@ -57,7 +57,7 @@ - {{ getCellClickColumnInfo($index, column) }} + {{ getCellClickColumnInfo($index, column) | customTranslate }}
    +
    + + {{ 'legend.show-total' | translate }} + +
    diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/latest-chart-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/latest-chart-basic-config.component.ts index 4a105c9cdd..7f86a5aebe 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/latest-chart-basic-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/latest-chart-basic-config.component.ts @@ -179,6 +179,7 @@ export abstract class LatestChartBasicConfigComponent +
    + + {{ 'tooltip.show-stack-total' | translate }} + +
    {{ 'tooltip.background-color' | translate }}
    - +
    + + diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/api-usage-data-key-row.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/api-usage-data-key-row.component.scss new file mode 100644 index 0000000000..0d690d5877 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/api-usage-data-key-row.component.scss @@ -0,0 +1,56 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@import '../../../../../../../../scss/constants'; + +.tb-form-table-row.tb-api-usage-data-key-row { + + .tb-source-field { + flex: 1 1 25%; + display: flex; + gap: 12px; + .tb-label-field { + flex: 1; + } + } + + .tb-data-key-fields { + display: flex; + gap: 12px; + min-width: 0; + flex: 1 1 50%; + } + + .tb-data-key-field { + flex: 1; + min-width: 0; + } + + .tb-remove-button { + width: 40px; + min-width: 40px; + } + + @media #{$mat-lt-lg} { + .tb-source-field { + flex-direction: column; + flex: 1 1 50%; + } + .tb-data-key-fields{ + flex-direction: column; + flex: 1 1 50%; + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/api-usage-data-key-row.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/api-usage-data-key-row.component.ts new file mode 100644 index 0000000000..df3e02f04b --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/api-usage-data-key-row.component.ts @@ -0,0 +1,156 @@ +/// +/// Copyright © 2016-2025 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { + ChangeDetectorRef, + Component, + DestroyRef, + EventEmitter, + forwardRef, + Input, + OnInit, + Output, + ViewEncapsulation +} from '@angular/core'; +import { + ControlValueAccessor, + NG_VALUE_ACCESSOR, + UntypedFormBuilder, + UntypedFormGroup, + Validators +} from '@angular/forms'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { DataKey, DatasourceType, widgetType } from '@shared/models/widget.models'; +import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; +import { + ApiUsageDataKeysSettings, + ApiUsageSettingsContext +} from "@home/components/widget/lib/settings/cards/api-usage-settings.component.models"; +import { Observable, of } from "rxjs"; + +@Component({ + selector: 'tb-api-usage-data-key-row', + templateUrl: './api-usage-data-key-row.component.html', + styleUrls: ['./api-usage-data-key-row.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => ApiUsageDataKeyRowComponent), + multi: true + } + ], + encapsulation: ViewEncapsulation.None +}) +export class ApiUsageDataKeyRowComponent implements ControlValueAccessor, OnInit { + + DatasourceType = DatasourceType; + DataKeyType = DataKeyType; + + widgetType = widgetType; + + @Input() + disabled: boolean; + + @Input() + dsEntityAliasId: string; + + @Input() + context: ApiUsageSettingsContext; + + @Output() + dataKeyRemoved = new EventEmitter(); + + dataKeyFormGroup: UntypedFormGroup; + + modelValue: ApiUsageDataKeysSettings; + + private propagateChange = (_val: any) => {}; + + constructor(private fb: UntypedFormBuilder, + private cd: ChangeDetectorRef, + private destroyRef: DestroyRef) { + } + + ngOnInit() { + this.dataKeyFormGroup = this.fb.group({ + label: [null, [Validators.required]], + state: [null, []], + status: [null, [Validators.required]], + maxLimit: [null, [Validators.required]], + current: [null, [Validators.required]] + }); + this.dataKeyFormGroup.valueChanges.pipe( + takeUntilDestroyed(this.destroyRef) + ).subscribe( + () => this.updateModel() + ); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(_fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (isDisabled) { + this.dataKeyFormGroup.disable({emitEvent: false}); + } else { + this.dataKeyFormGroup.enable({emitEvent: false}); + this.updateValidators(); + } + } + + writeValue(value: ApiUsageDataKeysSettings): void { + this.modelValue = value; + this.dataKeyFormGroup.patchValue( + { + label: value?.label, + state: value?.state, + status: value?.status, + maxLimit: value?.maxLimit, + current: value?.current + }, {emitEvent: false} + ); + this.updateValidators(); + this.cd.markForCheck(); + } + + editKey(keyType: 'status' | 'maxLimit' | 'current') { + const targetDataKey: DataKey = this.dataKeyFormGroup.get(keyType).value; + this.context.editKey(targetDataKey, this.dsEntityAliasId).subscribe( + (updatedDataKey) => { + if (updatedDataKey) { + this.dataKeyFormGroup.get(keyType).patchValue(updatedDataKey); + } + } + ); + } + + private updateValidators() { + } + + private updateModel() { + this.modelValue = {...this.modelValue, ...this.dataKeyFormGroup.value}; + this.propagateChange(this.modelValue); + } + + fetchDashboardStates(searchText?: string): Observable> { + return of(this.context.callbacks.fetchDashboardStates(searchText)); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/api-usage-settings.component.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/api-usage-settings.component.models.ts new file mode 100644 index 0000000000..7f4312cef9 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/api-usage-settings.component.models.ts @@ -0,0 +1,118 @@ +/// +/// Copyright © 2016-2025 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { IAliasController } from '@core/api/widget-api.models'; +import { WidgetConfigCallbacks } from '@home/components/widget/config/widget-config.component.models'; +import { DataKey, Widget, widgetType } from '@shared/models/widget.models'; +import { Observable } from 'rxjs'; +import { BackgroundSettings, BackgroundType } from '@shared/models/widget-settings.models'; +import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; +import { materialColors } from '@shared/models/material.models'; + +export interface ApiUsageSettingsContext { + aliasController: IAliasController; + callbacks: WidgetConfigCallbacks; + widget: Widget; + editKey: (key: DataKey, entityAliasId: string, WidgetType?: widgetType) => Observable; + generateDataKey: (key: DataKey) => DataKey; +} + + +export interface ApiUsageWidgetSettings { + dsEntityAliasId: string; + apiUsageDataKeys: ApiUsageDataKeysSettings[]; + targetDashboardState: string; + background: BackgroundSettings; + padding: string; +} + +export interface ApiUsageDataKeysSettings { + label: string; + state: string; + status: DataKey; + maxLimit: DataKey; + current: DataKey; +} + +const generateDataKey = (label: string, status: string, maxLimit: string, current: string) => { + return { + label, + state: '', + status: { + name: status, + label: status, + type: DataKeyType.timeseries, + funcBody: undefined, + settings: {}, + color: materialColors[0].value + }, + maxLimit: { + name: maxLimit, + label: maxLimit, + type: DataKeyType.timeseries, + funcBody: undefined, + settings: {}, + color: materialColors[0].value + }, + current: { + name: current, + label: current, + type: DataKeyType.timeseries, + funcBody: undefined, + settings: {}, + color: materialColors[0].value + } + } +} + +export const apiUsageDefaultSettings: ApiUsageWidgetSettings = { + dsEntityAliasId: '', + apiUsageDataKeys: [ + generateDataKey('{i18n:api-usage.transport-messages}', 'transportApiState', 'transportMsgLimit', 'transportMsgCount'), + generateDataKey('{i18n:api-usage.transport-data-points}', 'transportApiState', 'transportDataPointsLimit', 'transportDataPointsCount'), + generateDataKey('{i18n:api-usage.rule-engine-executions}', 'ruleEngineApiState', 'ruleEngineExecutionLimit', 'ruleEngineExecutionCount'), + generateDataKey('{i18n:api-usage.javascript-function-executions}', 'jsExecutionApiState', 'jsExecutionLimit', 'jsExecutionCount'), + generateDataKey('{i18n:api-usage.tbel-function-executions}', 'tbelExecutionApiState', 'tbelExecutionLimit', 'tbelExecutionCount'), + generateDataKey('{i18n:api-usage.data-points-storage-days}', 'dbApiState', 'storageDataPointsLimit', 'storageDataPointsCount'), + generateDataKey('{i18n:api-usage.alarms-created}', 'alarmApiState', 'createdAlarmsLimit', 'createdAlarmsCount'), + generateDataKey('{i18n:api-usage.emails}', 'emailApiState', 'emailLimit', 'emailCount'), + generateDataKey('{i18n:api-usage.sms}', 'notificationApiState', 'smsLimit', 'smsCount'), + ], + targetDashboardState: 'default', + background: { + type: BackgroundType.color, + color: '#fff', + overlay: { + enabled: false, + color: 'rgba(255,255,255,0.72)', + blur: 3 + } + }, + padding: '0' +}; + +export const getUniqueDataKeys = (data: ApiUsageDataKeysSettings[]): DataKey[] => { + const seenNames = new Set(); + return data + .flatMap(item => [item.status, item.maxLimit, item.current]) + .filter(key => { + if (seenNames.has(key.name)) { + return false; + } + seenNames.add(key.name); + return true; + }); +}; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/api-usage-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/api-usage-widget-settings.component.html new file mode 100644 index 0000000000..e96a654c44 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/api-usage-widget-settings.component.html @@ -0,0 +1,99 @@ + + +
    +
    widget-config.datasource
    + + + +
    +
    +
    +
    widgets.api-usage.label
    +
    widgets.api-usage.state-name
    +
    widgets.api-usage.status
    +
    widgets.api-usage.limit
    +
    widgets.api-usage.current-number
    +
    +
    +
    +
    +
    + + +
    + +
    +
    +
    +
    +
    +
    + +
    +
    + + {{ 'widgets.api-usage.no-key' | translate }} + + + +
    + +
    +
    widget-config.card-appearance
    +
    +
    {{ 'widgets.background.background' | translate }}
    + + +
    +
    +
    {{ 'widget-config.card-padding' | translate }}
    + + + +
    +
    +
    diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/api-usage-widget-settings.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/api-usage-widget-settings.component.scss new file mode 100644 index 0000000000..9543abb44b --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/api-usage-widget-settings.component.scss @@ -0,0 +1,62 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@import '../../../../../../../../scss/constants'; + +.tb-map-data-layers { + .tb-form-table-header-cell { + &.tb-source-header { + flex: 1 1 50%; + } + &.tb-x-pos-header { + flex: 1 1 25%; + } + &.tb-y-pos-header { + flex: 1 1 25%; + } + &.tb-key-header { + flex: 1 1 50%; + } + &.tb-actions-header { + width: 80px; + min-width: 80px; + } + @media #{$mat-lt-lg} { + &.tb-source-header { + flex: 1 1 30%; + } + &.tb-x-pos-header, &.tb-y-pos-header { + flex: 1 1 35%; + } + &.tb-key-header { + flex: 1 1 70%; + } + } + @media #{$mat-xs} { + &.tb-x-pos-header, &.tb-y-pos-header { + display: none; + } + &.tb-key-header { + display: none; + } + } + } + + .tb-form-table-body { + tb-api-usage-data-key-row { + overflow: hidden; + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/api-usage-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/api-usage-widget-settings.component.ts new file mode 100644 index 0000000000..3e909b0901 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/api-usage-widget-settings.component.ts @@ -0,0 +1,211 @@ +/// +/// Copyright © 2016-2025 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, forwardRef } from '@angular/core'; +import { + DataKey, + DataKeyConfigMode, + WidgetSettings, + WidgetSettingsComponent, + widgetType +} from '@shared/models/widget.models'; +import { + AbstractControl, + NG_VALUE_ACCESSOR, + UntypedFormArray, + UntypedFormBuilder, + UntypedFormGroup, + ValidationErrors, + ValidatorFn, Validators +} from '@angular/forms'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { + ApiUsageDataKeysSettings, + apiUsageDefaultSettings, + ApiUsageSettingsContext +} from '@home/components/widget/lib/settings/cards/api-usage-settings.component.models'; +import { deepClone } from '@core/utils'; +import { Observable, of } from 'rxjs'; +import { + DataKeyConfigDialogComponent, + DataKeyConfigDialogData +} from '@home/components/widget/lib/settings/common/key/data-key-config-dialog.component'; +import { MatDialog } from '@angular/material/dialog'; +import { CdkDragDrop } from '@angular/cdk/drag-drop'; + +@Component({ + selector: 'tb-api-usage-widget-settings', + templateUrl: './api-usage-widget-settings.component.html', + styleUrls: ['./../widget-settings.scss', 'api-usage-widget-settings.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => ApiUsageWidgetSettingsComponent), + multi: true + } + ], +}) +export class ApiUsageWidgetSettingsComponent extends WidgetSettingsComponent { + + apiUsageWidgetSettingsForm: UntypedFormGroup; + + context: ApiUsageSettingsContext; + + constructor(protected store: Store, + private dialog: MatDialog, + private fb: UntypedFormBuilder) { + super(store); + } + + ngOnInit() { + this.context = { + aliasController: this.aliasController, + callbacks: this.callbacks, + widget: this.widget, + editKey: this.editKey.bind(this), + generateDataKey: this.generateDataKey.bind(this) + }; + } + + protected doUpdateSettings(settingsForm: UntypedFormGroup, settings: WidgetSettings) { + settingsForm.setControl('apiUsageDataKeys', this.prepareDataKeysFormArray(settings?.apiUsageDataKeys), {emitEvent: false}); + } + + dataKeysFormArray(): UntypedFormArray { + return this.apiUsageWidgetSettingsForm.get('apiUsageDataKeys') as UntypedFormArray; + } + + trackByDataKey(index: number): any { + return index; + } + + get dragEnabled(): boolean { + return this.dataKeysFormArray().controls.length > 1; + } + + layerDrop(event: CdkDragDrop) { + const layer = this.dataKeysFormArray().at(event.previousIndex); + this.dataKeysFormArray().removeAt(event.previousIndex); + this.dataKeysFormArray().insert(event.currentIndex, layer); + } + + removeDataKey(index: number) { + (this.apiUsageWidgetSettingsForm.get('apiUsageDataKeys') as UntypedFormArray).removeAt(index); + } + + addDataKey() { + const dataKey = { + label: '', + state: '', + status: null, + maxLimit: null, + current: null + }; + const dataKeysArray = this.apiUsageWidgetSettingsForm.get('apiUsageDataKeys') as UntypedFormArray; + const dataKeyControl = this.fb.control(dataKey, [this.apiUsageDataKeyValidator()]); + dataKeysArray.push(dataKeyControl); + } + + protected settingsForm(): UntypedFormGroup { + return this.apiUsageWidgetSettingsForm; + } + + protected defaultSettings(): WidgetSettings { + return apiUsageDefaultSettings; + } + + protected prepareInputSettings(settings: WidgetSettings): WidgetSettings { + return { + dsEntityAliasId: settings?.dsEntityAliasId, + apiUsageDataKeys: settings?.apiUsageDataKeys, + targetDashboardState: settings?.targetDashboardState, + background: settings?.background, + padding: settings.padding + }; + } + + protected onSettingsSet(settings: WidgetSettings) { + this.apiUsageWidgetSettingsForm = this.fb.group({ + dsEntityAliasId: [settings?.dsEntityAliasId, Validators.required], + apiUsageDataKeys: this.prepareDataKeysFormArray(settings?.apiUsageDataKeys), + targetDashboardState: [settings?.targetDashboardState], + background: [settings?.background, []], + padding: [settings.padding, []] + }); + } + + private prepareDataKeysFormArray(dataKeys: ApiUsageDataKeysSettings[]): UntypedFormArray { + const dataKeysControls: Array = []; + if (dataKeys) { + dataKeys.forEach((dataLayer) => { + dataKeysControls.push(this.fb.control(dataLayer, [this.apiUsageDataKeyValidator()])); + }); + } + return this.fb.array(dataKeysControls); + } + + protected validatorTriggers(): string[] { + return []; + } + + protected updateValidators() { + } + + apiUsageDataKeyValidator = (): ValidatorFn => { + return (control: AbstractControl): ValidationErrors | null => { + const value: ApiUsageDataKeysSettings = control.value; + if (!value?.label || !value?.current || !value?.maxLimit || !value?.status) { + return { + dataKey: true + } + } + return null; + }; + }; + + private editKey(key: DataKey, entityAliasId: string, _widgetType = widgetType.latest): Observable { + return this.dialog.open(DataKeyConfigDialogComponent, + { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + dataKey: deepClone(key), + dataKeyConfigMode: DataKeyConfigMode.general, + aliasController: this.aliasController, + widgetType: _widgetType, + entityAliasId, + showPostProcessing: true, + callbacks: this.callbacks, + hideDataKeyColor: true, + hideDataKeyDecimals: true, + hideDataKeyUnits: true, + widget: this.widget, + dashboard: null, + dataKeySettingsForm: null, + dataKeySettingsDirective: null + } + }).afterClosed(); + } + + private generateDataKey(key: DataKey): DataKey { + return this.callbacks.generateDataKey(key.name, key.type, null, false, null); + } + + fetchDashboardStates(searchText?: string): Observable> { + return of(this.callbacks.fetchDashboardStates(searchText)); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-widget-settings.component.html index 52dbf0ba79..f236dd1acd 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-widget-settings.component.html @@ -115,6 +115,16 @@ {{ 'widgets.table.use-entity-label-tab-name' | translate }} +
    +
    widgets.table.sort-by
    + + + {{ 'widgets.table.sort-timestamp-option' | translate }} + {{ 'widgets.table.sort-asc' | translate }} + {{ 'widgets.table.sort-desc' | translate }} + + +
    widgets.table.rows
    diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-widget-settings.component.ts index 05d49aca26..ccbb582679 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-widget-settings.component.ts @@ -20,6 +20,7 @@ import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { buildPageStepSizeValues } from '@home/components/widget/lib/table-widget.models'; +import { TabSortKey } from '@app/modules/home/components/widget/lib/timeseries-table-widget.component' @Component({ selector: 'tb-timeseries-table-widget-settings', @@ -28,6 +29,8 @@ import { buildPageStepSizeValues } from '@home/components/widget/lib/table-widge }) export class TimeseriesTableWidgetSettingsComponent extends WidgetSettingsComponent { + TabSortKey = TabSortKey; + timeseriesTableWidgetSettingsForm: UntypedFormGroup; pageStepSizeValues = []; @@ -58,12 +61,14 @@ export class TimeseriesTableWidgetSettingsComponent extends WidgetSettingsCompon hideEmptyLines: false, disableStickyHeader: false, useRowStyleFunction: false, - rowStyleFunction: '' + rowStyleFunction: '', + tabSortKey: TabSortKey.TIMESTAMP }; } protected prepareInputSettings(settings: WidgetSettings): WidgetSettings { settings.pageStepIncrement = settings.pageStepIncrement ?? settings.defaultPageSize; + settings.tabSortKey = settings.tabSortKey ?? TabSortKey.TIMESTAMP; this.pageStepSizeValues = buildPageStepSizeValues(settings.pageStepCount, settings.pageStepIncrement); return settings; } @@ -93,7 +98,8 @@ export class TimeseriesTableWidgetSettingsComponent extends WidgetSettingsCompon hideEmptyLines: [settings.hideEmptyLines, []], disableStickyHeader: [settings.disableStickyHeader, []], useRowStyleFunction: [settings.useRowStyleFunction, []], - rowStyleFunction: [settings.rowStyleFunction, [Validators.required]] + rowStyleFunction: [settings.rowStyleFunction, [Validators.required]], + tabSortKey: [settings.tabSortKey, []], }); } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/doughnut-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/doughnut-widget-settings.component.ts index 8db598b919..f1854a0d00 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/doughnut-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/doughnut-widget-settings.component.ts @@ -69,9 +69,11 @@ export class DoughnutWidgetSettingsComponent extends LatestChartWidgetSettingsCo if (totalEnabled) { latestChartWidgetSettingsForm.get('totalValueFont').enable(); latestChartWidgetSettingsForm.get('totalValueColor').enable(); + latestChartWidgetSettingsForm.get('legendShowTotal').disable(); } else { latestChartWidgetSettingsForm.get('totalValueFont').disable(); latestChartWidgetSettingsForm.get('totalValueColor').disable(); + latestChartWidgetSettingsForm.get('legendShowTotal').enable(); } } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/latest-chart-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/latest-chart-widget-settings.component.html index fd5403984d..66d0234914 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/latest-chart-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/latest-chart-widget-settings.component.html @@ -61,6 +61,11 @@
    +
    + + {{ 'legend.show-total' | translate }} + +
    diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/latest-chart-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/latest-chart-widget-settings.component.ts index fc5940406c..ff7641bf1a 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/latest-chart-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/latest-chart-widget-settings.component.ts @@ -135,6 +135,7 @@ export abstract class LatestChartWidgetSettingsComponent +
    + + {{ 'tooltip.show-stack-total' | translate }} + +
    {{ 'tooltip.background-color' | translate }}
    } + @case (MapItemType.polyline) { +
    +
    widget-action.map-item-tooltip.start-draw-polyline
    + + + +
    +
    +
    widget-action.map-item-tooltip.finish-draw-polyline
    + + + +
    + } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/mobile-action-editor.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/mobile-action-editor.component.html index c3194676cf..5f1954adab 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/mobile-action-editor.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/mobile-action-editor.component.html @@ -35,117 +35,73 @@ - - - - - - - - - - - - - - - - - - - - - - + + @if (mobileActionFormGroup.get('type').value === mobileActionType.takePhoto || + mobileActionFormGroup.get('type').value === mobileActionType.takePictureFromGallery || + mobileActionFormGroup.get('type').value === mobileActionType.takeScreenshot) { +
    + + {{ 'widget-action.mobile.save-to-gallery' | translate }} + +
    + } + @if (mobileActionFormGroup.get('type').value === mobileActionType.deviceProvision) { +
    +
    {{ 'widget-action.mobile.provision-type' | translate }}*
    + + + + {{ provisionTypeTranslationMap.get(type) | translate }} + + + +
    + } + + @for (config of actionConfig; track config.formControlName) { +
    + + + + {{ config.title | translate }} + + + + +
    + }
    - - + + @if(mobileActionFormGroup.get('type').value) { + @for (config of commonActionConfig; track config.formControlName) { +
    + + + + {{ config.title | translate }} + + + + +
    + } + } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/mobile-action-editor.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/mobile-action-editor.component.ts index 52064fdf91..c53272d75a 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/mobile-action-editor.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/mobile-action-editor.component.ts @@ -24,6 +24,9 @@ import { } from '@angular/forms'; import { coerceBooleanProperty } from '@angular/cdk/coercion'; import { + ActionConfig, + ProvisionType, + provisionTypeTranslationMap, WidgetActionType, WidgetMobileActionDescriptor, WidgetMobileActionType, @@ -35,6 +38,7 @@ import { getDefaultGetPhoneNumberFunction, getDefaultHandleEmptyResultFunction, getDefaultHandleErrorFunction, + getDefaultHandleNonMobileFallBackFunction, getDefaultProcessImageFunction, getDefaultProcessLaunchResultFunction, getDefaultProcessLocationFunction, @@ -68,6 +72,12 @@ export class MobileActionEditorComponent implements ControlValueAccessor, OnInit functionScopeVariables: string[]; + actionConfig: ActionConfig[]; + commonActionConfig: ActionConfig[]; + + provisionTypes: string[] = Object.keys(ProvisionType); + provisionTypeTranslationMap = provisionTypeTranslationMap; + private requiredValue: boolean; get required(): boolean { return this.requiredValue; @@ -99,8 +109,10 @@ export class MobileActionEditorComponent implements ControlValueAccessor, OnInit this.mobileActionFormGroup = this.fb.group({ type: [null, Validators.required], handleEmptyResultFunction: [null], - handleErrorFunction: [null] + handleErrorFunction: [null], + handleNonMobileFallbackFunction: [null] }); + this.getCommonActionConfigs(); this.mobileActionFormGroup.get('type').valueChanges.pipe( takeUntilDestroyed(this.destroyRef) ).subscribe((type: WidgetMobileActionType) => { @@ -109,6 +121,7 @@ export class MobileActionEditorComponent implements ControlValueAccessor, OnInit action = {...action, ...this.mobileActionTypeFormGroup.value}; } this.updateMobileActionType(type, action); + this.getActionConfigs(); }); this.mobileActionFormGroup.valueChanges.pipe( takeUntilDestroyed(this.destroyRef) @@ -133,10 +146,14 @@ export class MobileActionEditorComponent implements ControlValueAccessor, OnInit } writeValue(value: WidgetMobileActionDescriptor | null): void { - this.mobileActionFormGroup.patchValue({type: value?.type, - handleEmptyResultFunction: value?.handleEmptyResultFunction, - handleErrorFunction: value?.handleErrorFunction}, {emitEvent: false}); + this.mobileActionFormGroup.patchValue({ + type: value?.type, + handleEmptyResultFunction: value?.handleEmptyResultFunction, + handleErrorFunction: value?.handleErrorFunction, + handleNonMobileFallbackFunction: value?.handleNonMobileFallbackFunction + }, {emitEvent: false}); this.updateMobileActionType(value?.type, value); + this.getActionConfigs(); } private updateModel() { @@ -164,6 +181,12 @@ export class MobileActionEditorComponent implements ControlValueAccessor, OnInit handleErrorFunction = getDefaultHandleErrorFunction(type); this.mobileActionFormGroup.patchValue({handleErrorFunction}, {emitEvent: false}); } + let handleNonMobileFallbackFunction = action?.handleNonMobileFallbackFunction; + const defaultHandleNonMobileFallbackFunction = getDefaultHandleNonMobileFallBackFunction(); + if (defaultHandleNonMobileFallbackFunction !== handleNonMobileFallbackFunction) { + handleNonMobileFallbackFunction = getDefaultHandleNonMobileFallBackFunction(); + this.mobileActionFormGroup.patchValue({handleNonMobileFallbackFunction}, {emitEvent: false}); + } } this.mobileActionTypeFormGroup = this.fb.group({}); if (type) { @@ -183,6 +206,10 @@ export class MobileActionEditorComponent implements ControlValueAccessor, OnInit 'processImageFunction', this.fb.control(processImageFunction, []) ); + this.mobileActionTypeFormGroup.addControl( + 'saveToGallery', + this.fb.control(action?.saveToGallery || false, []) + ); break; case WidgetMobileActionType.mapDirection: case WidgetMobileActionType.mapLocation: @@ -267,6 +294,10 @@ export class MobileActionEditorComponent implements ControlValueAccessor, OnInit 'handleProvisionSuccessFunction', this.fb.control(handleProvisionSuccessFunction, [Validators.required]) ); + this.mobileActionTypeFormGroup.addControl( + 'provisionType', + this.fb.control(action?.provisionType || ProvisionType.auto, []) + ); } } this.mobileActionTypeFormGroup.valueChanges.pipe( @@ -276,5 +307,108 @@ export class MobileActionEditorComponent implements ControlValueAccessor, OnInit }); } + getActionConfigs() { + const type = this.mobileActionFormGroup.get('type').value; + this.actionConfig = []; + switch (type) { + case this.mobileActionType.deviceProvision: + this.actionConfig.push({ + title: 'widget-action.mobile.handle-provision-success-function', + formControlName: 'handleProvisionSuccessFunction', + functionName: 'handleProvisionSuccess', + functionArgs: ['deviceName', '$event', 'widgetContext', 'entityId', 'entityName', 'additionalParams', 'entityLabel'] + }); + break; + case this.mobileActionType.mapDirection: + case this.mobileActionType.mapLocation: + this.actionConfig.push({ + title: 'widget-action.mobile.get-location-function', + formControlName: 'getLocationFunction', + functionName: 'getLocation', + functionArgs: ['$event', 'widgetContext', 'entityId', 'entityName', 'additionalParams', 'entityLabel'], + helpId: 'widget/action/mobile_get_location_fn' + }); + this.actionConfig.push({ + title: 'widget-action.mobile.process-launch-result-function', + formControlName: 'processLaunchResultFunction', + functionName: 'processLaunchResult', + functionArgs: ['launched', '$event', 'widgetContext', 'entityId', 'entityName', 'additionalParams', 'entityLabel'], + helpId: 'widget/action/mobile_process_launch_result_fn' + }); + break; + case this.mobileActionType.makePhoneCall: + this.actionConfig.push({ + title: 'widget-action.mobile.get-phone-number-function', + formControlName: 'getPhoneNumberFunction', + functionName: 'getPhoneNumber', + functionArgs: ['$event', 'widgetContext', 'entityId', 'entityName', 'additionalParams', 'entityLabel'], + helpId: 'widget/action/mobile_get_phone_number_fn' + }); + this.actionConfig.push({ + title: 'widget-action.mobile.process-launch-result-function', + formControlName: 'processLaunchResultFunction', + functionName: 'processLaunchResult', + functionArgs: ['launched', '$event', 'widgetContext', 'entityId', 'entityName', 'additionalParams', 'entityLabel'], + helpId: 'widget/action/mobile_process_launch_result_fn' + }); + break; + case this.mobileActionType.takePhoto: + case this.mobileActionType.takePictureFromGallery: + case this.mobileActionType.takeScreenshot: + this.actionConfig.push({ + title: 'widget-action.mobile.process-image-function', + formControlName: 'processImageFunction', + functionName: 'processImage', + functionArgs: ['imageUrl', '$event', 'widgetContext', 'entityId', 'entityName', 'additionalParams', 'entityLabel'], + helpId: 'widget/action/mobile_process_image_fn' + }); + break; + case this.mobileActionType.scanQrCode: + this.actionConfig.push({ + title: 'widget-action.mobile.process-qr-code-function', + formControlName: 'processQrCodeFunction', + functionName: 'processQrCode', + functionArgs: ['code', 'format', '$event', 'widgetContext', 'entityId', 'entityName', 'additionalParams', 'entityLabel'], + helpId: 'widget/action/mobile_process_qr_code_fn' + }); + break; + case this.mobileActionType.getLocation: + this.actionConfig.push({ + title: 'widget-action.mobile.process-location-function', + formControlName: 'processLocationFunction', + functionName: 'processLocation', + functionArgs: ['latitude', 'longitude', '$event', 'widgetContext', 'entityId', 'entityName', 'additionalParams', 'entityLabel'], + helpId: 'widget/action/mobile_process_location_fn' + }); + break; + } + } + + getCommonActionConfigs() { + this.commonActionConfig = [ + { + title: 'widget-action.mobile.handle-empty-result-function', + formControlName: 'handleEmptyResultFunction', + functionName: 'handleEmptyResult', + functionArgs: ['$event', 'widgetContext', 'entityId', 'entityName', 'additionalParams', 'entityLabel'], + helpId: 'widget/action/mobile_handle_empty_result_fn' + }, + { + title: 'widget-action.mobile.handle-error-function', + formControlName: 'handleErrorFunction', + functionName: 'handleError', + functionArgs: ['error', '$event', 'widgetContext', 'entityId', 'entityName', 'additionalParams', 'entityLabel'], + helpId: 'widget/action/mobile_handle_error_fn' + }, + { + title: 'widget-action.mobile.handle-non-mobile-fallback-function', + formControlName: 'handleNonMobileFallbackFunction', + functionName: 'handleNonMobileFallback', + functionArgs: ['$event', 'widgetContext'], + helpId: 'widget/action/mobile_handle_non_mobile_fallback_fn' + } + ]; + } + protected readonly WidgetActionType = WidgetActionType; } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/mobile-action-editor.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/mobile-action-editor.models.ts index 0d4c46c589..68a97bf779 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/mobile-action-editor.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/mobile-action-editor.models.ts @@ -172,6 +172,14 @@ const handleErrorFunctionTemplate: TbFunction = ' }, 100);\n' + '}\n'; +const handleNonMobileFallbackFunctionTemplate: TbFunction = + '// Optional function body to handle non-mobile fallback \n' + + 'showFallbackToast();\n' + + '\n' + + 'function showFallbackToast(title, error) {\n' + + ' widgetContext.showWarnToast(\'This action is only available in the mobile application.\');\n' + + '}\n'; + const getLocationFunctionTemplate: TbFunction = '// Function body that should return location as array of two numbers (latitude, longitude) for further processing by mobile action.\n' + '// Usually location can be obtained from entity attributes/telemetry. \n\n' + @@ -326,3 +334,5 @@ export const getDefaultHandleErrorFunction = (type: WidgetMobileActionType): TbF } return handleErrorFunctionTemplate.replace('--TITLE--', title); }; + +export const getDefaultHandleNonMobileFallBackFunction = () => handleNonMobileFallbackFunctionTemplate; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/place-map-item-sample-js.raw b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/place-map-item-sample-js.raw index cb8c23faee..05e06542a5 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/place-map-item-sample-js.raw +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/place-map-item-sample-js.raw @@ -79,7 +79,7 @@ function AddEntityDialogController(instance) { const mapType = widgetContext.mapInstance.type(); attributes.push({key: mapType === 'image' ? 'xPos' : 'latitude', value: additionalParams.coordinates.x}); attributes.push({key: mapType === 'image' ? 'yPos' : 'longitude', value: additionalParams.coordinates.y}); - } else if (mapItemType === 'Rectangle' || mapItemType === 'Polygon') { + } else if (mapItemType === 'Rectangle' || mapItemType === 'Polygon' || mapItemType === 'Line' ) { attributes.push({key: 'perimeter', value: additionalParams.coordinates}); } else if (mapItemType === 'Circle') { attributes.push({key: 'circle', value: additionalParams.coordinates}); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/alias/entity-alias-select.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/alias/entity-alias-select.component.ts index 1af25bd01e..d65d4067e5 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/alias/entity-alias-select.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/alias/entity-alias-select.component.ts @@ -251,7 +251,6 @@ export class EntityAliasSelectComponent implements ControlValueAccessor, OnInit, }, 0); } } else { - this.entityAliasList.push(newAlias); this.modelValue = newAlias.id; this.selectEntityAliasFormGroup.get('entityAlias').patchValue(newAlias, {emitEvent: true}); this.propagateChange(this.modelValue); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-property-panel.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-property-panel.component.html index 5dbeb8b646..5307a18a7b 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-property-panel.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-property-panel.component.html @@ -116,7 +116,7 @@
    dynamic-form.property.disable-on-property
    - + {{ 'dynamic-form.property.disable-on-property-none' | translate }} {{ prop }} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form.component.ts index 5f6682ea90..26c89ca4cb 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form.component.ts @@ -37,7 +37,7 @@ import { } from '@angular/forms'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; -import { isDefinedAndNotNull, mergeDeep } from '@core/utils'; +import { isDefinedAndNotNull, mergeDeep, trimDefaultValues } from '@core/utils'; import { defaultFormProperties, FormProperty, @@ -106,10 +106,16 @@ export class DynamicFormComponent implements OnInit, OnChanges, ControlValueAcce @coerceBoolean() noBorder = false; + @Input() + @coerceBoolean() + trimDefaults = false; + private modelValue: {[id: string]: any}; private propagateChange = null; + private defaults: {[id: string]: any}; + private validatorTriggers: string[]; public propertiesFormGroup: UntypedFormGroup; @@ -180,11 +186,13 @@ export class DynamicFormComponent implements OnInit, OnChanges, ControlValueAcce private loadMetadata() { this.validatorTriggers = []; this.propertyGroups = []; + this.defaults = {}; for (const control of Object.keys(this.propertiesFormGroup.controls)) { this.propertiesFormGroup.removeControl(control, {emitEvent: false}); } if (this.properties) { + this.defaults = defaultFormProperties(this.properties); for (let property of this.properties) { property.disabled = false; property.visible = true; @@ -282,8 +290,7 @@ export class DynamicFormComponent implements OnInit, OnChanges, ControlValueAcce private setupValue() { if (this.properties) { - const defaults = defaultFormProperties(this.properties); - this.modelValue = mergeDeep<{[id: string]: any}>(defaults, this.modelValue); + this.modelValue = mergeDeep<{[id: string]: any}>({}, this.defaults, this.modelValue); this.propertiesFormGroup.patchValue( this.modelValue, {emitEvent: false} ); @@ -295,7 +302,11 @@ export class DynamicFormComponent implements OnInit, OnChanges, ControlValueAcce private updateModel() { this.modelValue = this.propertiesFormGroup.getRawValue(); this.calculateControlsState(true); - this.propagateChange(this.modelValue); + let result = this.modelValue; + if (this.trimDefaults) { + result = trimDefaultValues(this.modelValue, this.defaults); + } + this.propagateChange(result); } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.html index 0ca912146c..178c948f43 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.html @@ -163,6 +163,26 @@ (keyEdit)="editKey('circleKey')" formControlName="circleKey"> + + +
    +
    widgets.maps.data-layer.shape.stroke
    @@ -455,7 +477,9 @@ [dsType]="dataLayerFormGroup.get('dsType').value" [dsEntityAliasId]="dataLayerFormGroup.get('dsEntityAliasId').value" [dsDeviceId]="dataLayerFormGroup.get('dsDeviceId').value" - helpId="{{ dataLayerType === 'polygons' ? 'widget/lib/map/polygon_stroke_color_fn' : 'widget/lib/map/circle_stroke_color_fn' }}" formControlName="strokeColor"> + helpId="{{ dataLayerType === 'polygons' ? 'widget/lib/map/polygon_stroke_color_fn' : + (dataLayerType === 'circles' ? 'widget/lib/map/polygon_stroke_color_fn' : 'widget/lib/map/polyline_stroke_color_fn') }}" + formControlName="strokeColor">
    diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.ts index 48854adc44..af653308da 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.ts @@ -29,7 +29,7 @@ import { MarkerType, pathDecoratorSymbols, pathDecoratorSymbolTranslationMap, - PolygonsDataLayerSettings, + PolygonsDataLayerSettings, PolylinesDataLayerSettings, ShapeDataLayerSettings, ShapeFillType, TripsDataLayerSettings, updateDataKeyToNewDsType @@ -113,6 +113,8 @@ export class MapDataLayerDialogComponent extends DialogComponent this.updateValidators() @@ -291,6 +298,15 @@ export class MapDataLayerDialogComponent extends DialogComponent + + +
    -
    -
    +
    +
    +
    + +
    diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layers.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layers.component.ts index 747ef21681..06e34e4eea 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layers.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layers.component.ts @@ -38,6 +38,7 @@ import { } from '@shared/models/widget/maps/map.models'; import { MapSettingsComponent } from '@home/components/widget/lib/settings/common/map/map-settings.component'; import { MapSettingsContext } from '@home/components/widget/lib/settings/common/map/map-settings.component.models'; +import { CdkDragDrop } from '@angular/cdk/drag-drop'; @Component({ selector: 'tb-map-data-layers', @@ -79,6 +80,10 @@ export class MapDataLayersComponent implements ControlValueAccessor, OnInit, Val noDataLayersText: string; + get dragEnabled(): boolean { + return this.dataLayersFormArray().controls.length > 1; + } + private propagateChange = (_val: any) => {}; constructor(private mapSettingsComponent: MapSettingsComponent, @@ -104,6 +109,10 @@ export class MapDataLayersComponent implements ControlValueAccessor, OnInit, Val this.addDataLayerText = 'widgets.maps.data-layer.circle.add-circle'; this.noDataLayersText = 'widgets.maps.data-layer.circle.no-circles'; break; + case 'polylines': + this.addDataLayerText = 'widgets.maps.data-layer.polyline.add-polylines'; + this.noDataLayersText = 'widgets.maps.data-layer.polyline.no-polylines'; + break; } this.dataLayersFormGroup = this.fb.group({ dataLayers: [this.fb.array([]), []] @@ -162,6 +171,13 @@ export class MapDataLayersComponent implements ControlValueAccessor, OnInit, Val removeDataLayer(index: number) { (this.dataLayersFormGroup.get('dataLayers') as UntypedFormArray).removeAt(index); } + + layerDrop(event: CdkDragDrop) { + const layersArray = this.dataLayersFormArray(); + const layer = layersArray.at(event.previousIndex); + layersArray.removeAt(event.previousIndex); + layersArray.insert(event.currentIndex, layer); + } addDataLayer() { const dataLayer = mergeDeep({} as MapDataLayerSettings, diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-settings.component.html index fe2afa3b19..c32242cc32 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-settings.component.html @@ -45,6 +45,8 @@ {{ 'widgets.maps.overlays.markers' | translate }} {{ 'widgets.maps.overlays.polygons' | translate }} {{ 'widgets.maps.overlays.circles' | translate }} + //todo translation + Polylines
    +
    diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-settings.component.ts index 5b949bae75..ab32155420 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-settings.component.ts @@ -145,6 +145,7 @@ export class MapSettingsComponent implements OnInit, ControlValueAccessor, Valid markers: [null, []], polygons: [null, []], circles: [null, []], + polylines: [null, []], additionalDataSources: [null, []], controlsPosition: [null, []], zoomActions: [null, []], @@ -180,7 +181,8 @@ export class MapSettingsComponent implements OnInit, ControlValueAccessor, Valid }); merge(this.mapSettingsFormGroup.get('markers').valueChanges, this.mapSettingsFormGroup.get('polygons').valueChanges, - this.mapSettingsFormGroup.get('circles').valueChanges + this.mapSettingsFormGroup.get('circles').valueChanges, + this.mapSettingsFormGroup.get('polylines').valueChanges ).pipe( takeUntilDestroyed(this.destroyRef) ).subscribe(() => { @@ -281,6 +283,10 @@ export class MapSettingsComponent implements OnInit, ControlValueAccessor, Valid const polygons: MapDataLayerSettings[] = this.mapSettingsFormGroup.get('polygons').value; dragModeButtonSettingsEnabled = polygons.some(d => d.edit && d.edit.enabledActions && d.edit.enabledActions.includes(DataLayerEditAction.move)); } + if (!dragModeButtonSettingsEnabled) { + const polylines: MapDataLayerSettings[] = this.mapSettingsFormGroup.get('polylines').value; + dragModeButtonSettingsEnabled = polylines.some(d => d.edit && d.edit.enabledActions && d.edit.enabledActions.includes(DataLayerEditAction.move)); + } if (!dragModeButtonSettingsEnabled) { const circles: MapDataLayerSettings[] = this.mapSettingsFormGroup.get('circles').value; dragModeButtonSettingsEnabled = circles.some(d => d.edit && d.edit.enabledActions && d.edit.enabledActions.includes(DataLayerEditAction.move)); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/photo-camera-input-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/photo-camera-input-widget-settings.component.html index 2225e22a44..cabd91d4e7 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/photo-camera-input-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/photo-camera-input-widget-settings.component.html @@ -15,19 +15,32 @@ limitations under the License. --> -
    -
    - widgets.input-widgets.general-settings - - widgets.input-widgets.widget-title - - -
    -
    - widgets.input-widgets.image-settings -
    - - widgets.input-widgets.image-format +
    +
    +
    widgets.input-widgets.general-settings
    +
    +
    widgets.input-widgets.widget-title
    + + + +
    +
    +
    +
    widgets.input-widgets.save-image
    + + {{ 'widgets.input-widgets.save-to-gallery' | translate }} + + @if (photoCameraInputWidgetSettingsForm.get('saveToGallery').value) { + + {{ 'widgets.input-widgets.public-image' | translate }} + + } +
    +
    +
    widgets.input-widgets.image-settings
    +
    +
    widgets.input-widgets.image-format
    + {{ 'widgets.input-widgets.image-format-jpeg' | translate }} @@ -40,20 +53,33 @@ - - widgets.input-widgets.image-quality - - -
    -
    - - widgets.input-widgets.max-image-width - - - - widgets.input-widgets.max-image-height - - -
    -
    +
    + + @if (photoCameraInputWidgetSettingsForm.get('imageFormat').value !== 'image/png') { +
    +
    widgets.input-widgets.image-quality
    + + + % + +
    + } + +
    +
    Size
    +
    +
    widgets.input-widgets.max-image-width
    + + + px + + +
    widgets.input-widgets.max-image-height
    + + + px + +
    +
    +
    diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/photo-camera-input-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/photo-camera-input-widget-settings.component.ts index 30c3e39833..4e7d552f44 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/photo-camera-input-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/input/photo-camera-input-widget-settings.component.ts @@ -15,10 +15,10 @@ /// import { Component } from '@angular/core'; -import { WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.models'; import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; -import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; +import { Store } from '@ngrx/store'; +import { WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.models'; @Component({ selector: 'tb-photo-camera-input-widget-settings', @@ -42,6 +42,8 @@ export class PhotoCameraInputWidgetSettingsComponent extends WidgetSettingsCompo return { widgetTitle: '', + saveToGallery: false, + usePublicGalleryLink: true, imageFormat: 'image/png', imageQuality: 0.92, maxWidth: 640, @@ -57,11 +59,28 @@ export class PhotoCameraInputWidgetSettingsComponent extends WidgetSettingsCompo widgetTitle: [settings.widgetTitle, []], // Image settings - + saveToGallery: [settings.saveToGallery], + usePublicGalleryLink: [settings.usePublicGalleryLink], imageFormat: [settings.imageFormat, []], - imageQuality: [settings.imageQuality, [Validators.min(0), Validators.max(1)]], + imageQuality: [settings.imageQuality, [Validators.min(0), Validators.max(100)]], maxWidth: [settings.maxWidth, [Validators.min(1)]], maxHeight: [settings.maxHeight, [Validators.min(1)]] }); } + + protected prepareInputSettings(settings: WidgetSettings): WidgetSettings { + return { + ...settings, + saveToGallery: settings.saveToGallery ?? false, + usePublicGalleryLink: settings.usePublicGalleryLink ?? false, + imageQuality: settings.imageQuality * 100 + } + } + + protected prepareOutputSettings(settings: WidgetSettings): WidgetSettings { + return { + ...settings, + imageQuality: settings.imageQuality / 100 + } + } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts index fbf9b2eec2..9e4de41841 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts @@ -375,6 +375,12 @@ import { ValueStepperWidgetSettingsComponent } from '@home/components/widget/lib/settings/control/value-stepper-widget-settings.component'; import { MapWidgetSettingsComponent } from '@home/components/widget/lib/settings/map/map-widget-settings.component'; +import { + ApiUsageWidgetSettingsComponent +} from "@home/components/widget/lib/settings/cards/api-usage-widget-settings.component"; +import { + ApiUsageDataKeyRowComponent +} from "@home/components/widget/lib/settings/cards/api-usage-data-key-row.component"; @NgModule({ declarations: [ @@ -508,7 +514,9 @@ import { MapWidgetSettingsComponent } from '@home/components/widget/lib/settings LabelValueCardWidgetSettingsComponent, UnreadNotificationWidgetSettingsComponent, ScadaSymbolWidgetSettingsComponent, - MapWidgetSettingsComponent + MapWidgetSettingsComponent, + ApiUsageWidgetSettingsComponent, + ApiUsageDataKeyRowComponent ], imports: [ CommonModule, @@ -647,7 +655,8 @@ import { MapWidgetSettingsComponent } from '@home/components/widget/lib/settings LabelValueCardWidgetSettingsComponent, UnreadNotificationWidgetSettingsComponent, ScadaSymbolWidgetSettingsComponent, - MapWidgetSettingsComponent + MapWidgetSettingsComponent, + ApiUsageWidgetSettingsComponent ] }) export class WidgetSettingsModule { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/table-widget.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/table-widget.models.ts index 7e91a2f043..8f36242d95 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/table-widget.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/table-widget.models.ts @@ -451,107 +451,36 @@ export function constructTableCssString(widgetConfig: WidgetConfig): string { const mdDarkDisabled = defaultColor.setAlpha(0.26).toRgbString(); const mdDarkDisabled2 = defaultColor.setAlpha(0.38).toRgbString(); const mdDarkDivider = defaultColor.setAlpha(0.12).toRgbString(); - - const cssString = - '.mat-mdc-input-element::placeholder {\n' + - ' color: ' + mdDarkSecondary + ';\n' + - '}\n' + - '.mat-mdc-input-element::-moz-placeholder {\n' + - ' color: ' + mdDarkSecondary + ';\n' + - '}\n' + - '.mat-mdc-input-element::-webkit-input-placeholder {\n' + - ' color: ' + mdDarkSecondary + ';\n' + - '}\n' + - '.mat-mdc-input-element:-ms-input-placeholder {\n' + - ' color: ' + mdDarkSecondary + ';\n' + - '}\n' + - 'mat-toolbar.mat-mdc-table-toolbar {\n' + - 'color: ' + mdDark + ';\n' + - '}\n' + - 'mat-toolbar.mat-mdc-table-toolbar:not([color="primary"]) button.mat-mdc-icon-button mat-icon {\n' + - 'color: ' + mdDarkSecondary + ';\n' + - '}\n' + - '.mat-mdc-tab .mdc-tab__text-label {\n' + - 'color: ' + mdDark + ';\n' + - '}\n' + - '.mat-mdc-tab-header-pagination-chevron {\n' + - 'border-color: ' + mdDark + ';\n' + - '}\n' + - '.mat-mdc-tab-header-pagination-disabled .mat-mdc-tab-header-pagination-chevron {\n' + - 'border-color: ' + mdDarkDisabled2 + ';\n' + - '}\n' + - '.mat-mdc-table .mat-mdc-header-row {\n' + - 'background-color: ' + origBackgroundColor + ';\n' + - '}\n' + - '.mat-mdc-table .mat-mdc-header-cell {\n' + - 'color: ' + mdDarkSecondary + ';\n' + - '}\n' + - '.mat-mdc-table .mat-mdc-cell, .mat-mdc-table .mat-mdc-header-cell {\n' + - 'border-bottom-color: ' + mdDarkDivider + ';\n' + - '}\n' + - '.mat-mdc-table .mat-mdc-cell .mat-mdc-checkbox ' + - '.mdc-checkbox__native-control:focus:enabled:not(:checked):not(:indeterminate):not([data-indeterminate=true])'+ - '~.mdc-checkbox__background, ' + - '.mat-table .mat-header-cell .mat-mdc-checkbox ' + - '.mdc-checkbox__native-control:focus:enabled:not(:checked):not(:indeterminate):not([data-indeterminate=true])'+ - '~.mdc-checkbox__background {\n' + - 'border-color: ' + mdDarkSecondary + ';\n' + - '}\n' + - '.mat-mdc-table .mat-mdc-row .mat-mdc-cell.mat-mdc-table-sticky {\n' + - 'transition: background-color .2s;\n' + - '}\n' + - '.mat-mdc-table .mat-mdc-row.tb-current-entity {\n' + - 'background-color: ' + currentEntityColor + ';\n' + - '}\n' + - '.mat-mdc-table .mat-mdc-row.tb-current-entity .mat-mdc-cell.mat-mdc-table-sticky {\n' + - 'background-color: ' + currentEntityStickyColor + ';\n' + - '}\n' + - '.mat-mdc-table .mat-mdc-row:hover:not(.tb-current-entity) {\n' + - 'background-color: ' + hoverColor + ';\n' + - '}\n' + - '.mat-mdc-table .mat-mdc-row:hover:not(.tb-current-entity) .mat-mdc-cell.mat-mdc-table-sticky {\n' + - 'background-color: ' + hoverStickyColor + ';\n' + - '}\n' + - '.mat-mdc-table .mat-mdc-row.mat-row-select.mat-selected:not(.tb-current-entity) {\n' + - 'background-color: ' + selectedColor + ';\n' + - '}\n' + - '.mat-mdc-table .mat-mdc-row.mat-row-select.mat-selected:not(.tb-current-entity) .mat-mdc-cell.mat-mdc-table-sticky {\n' + - 'background-color: ' + selectedStickyColor + ';\n' + - '}\n' + - '.mat-mdc-table .mat-mdc-row .mat-mdc-cell.mat-mdc-table-sticky, .mat-mdc-table .mat-mdc-header-cell.mat-mdc-table-sticky {\n' + - 'background-color: ' + origBackgroundColor + ';\n' + - '}\n' + - '.mat-mdc-table .mat-mdc-row {\n' + - 'color: ' + mdDark + ';\n' + - 'background-color: rgba(0, 0, 0, 0);\n' + - '}\n' + - '.mat-mdc-table .mat-mdc-cell button.mat-mdc-icon-button mat-icon {\n' + - 'color: ' + mdDarkSecondary + ';\n' + - '}\n' + - '.mat-mdc-table .mat-mdc-cell button.mat-mdc-icon-button[disabled][disabled] mat-icon {\n' + - 'color: ' + mdDarkDisabled + ';\n' + - '}\n' + - '.mat-mdc-table .mat-mdc-cell button.mat-mdc-icon-button tb-icon {\n' + - 'color: ' + mdDarkSecondary + ';\n' + - '}\n' + - '.mat-mdc-table .mat-mdc-cell button.mat-mdc-icon-button[disabled][disabled] tb-icon {\n' + - 'color: ' + mdDarkDisabled + ';\n' + - '}\n' + - '.mat-divider {\n' + - 'border-top-color: ' + mdDarkDivider + ';\n' + - '}\n' + - '.mat-mdc-paginator {\n' + - 'color: ' + mdDarkSecondary + ';\n' + - '}\n' + - '.mat-mdc-paginator button.mat-mdc-icon-button {\n' + - 'color: ' + mdDarkSecondary + ';\n' + - '}\n' + - '.mat-mdc-paginator button.mat-mdc-icon-button[disabled][disabled] {\n' + - 'color: ' + mdDarkDisabled + ';\n' + - '}\n' + - '.mat-mdc-paginator .mat-mdc-select-value {\n' + - 'color: ' + mdDarkSecondary + ';\n' + - '}'; + + const cssString = ` { + --mat-toolbar-container-text-color: ${mdDark}; + --mat-tab-header-active-label-text-color: ${mdDark}; + --mat-tab-header-inactive-label-text-color: ${mdDark}; + --mat-tab-header-pagination-icon-color: ${mdDark}; + --mat-tab-header-pagination-disabled-icon-color: ${mdDarkDisabled2}; + --mat-table-header-headline-color: ${mdDarkSecondary}; + --mat-table-row-item-label-text-color: ${mdDark}; + --mat-icon-color: ${mdDarkSecondary}; + --mdc-icon-button-disabled-icon-color: ${mdDarkDisabled}; + --mat-divider-color: ${mdDarkDivider}; + --mat-paginator-container-text-color: ${mdDarkSecondary}; + --mdc-icon-button-icon-color: ${mdDarkSecondary}; + --mat-paginator-enabled-icon-color: ${mdDarkSecondary}; + --mat-paginator-disabled-icon-color: ${mdDarkDisabled}; + --mat-select-enabled-trigger-text-color: ${mdDarkSecondary}; + --mat-select-disabled-trigger-text-color: ${mdDarkDisabled}; + --mat-table-row-item-outline-color: ${mdDarkDivider}; + --mdc-checkbox-unselected-focus-icon-color: ${mdDarkSecondary}; + + --tb-orig-background-color: ${origBackgroundColor}; + --tb-current-entity-color: ${currentEntityColor}; + --tb-current-entity-sticky-color: ${currentEntityStickyColor}; + --tb-hover-color: ${hoverColor}; + --tb-hover-sticky-color: ${hoverStickyColor}; + --tb-selected-color: ${selectedColor}; + --tb-selected-sticky-color: ${selectedStickyColor}; + } + `; return cssString; } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/table-widget.scss b/ui-ngx/src/app/modules/home/components/widget/lib/table-widget.scss index 3df2c7d0a2..dd8a026db4 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/table-widget.scss +++ b/ui-ngx/src/app/modules/home/components/widget/lib/table-widget.scss @@ -44,7 +44,53 @@ padding: 0 5px; } } + + .mat-mdc-tab-header-pagination-disabled .mat-mdc-tab-header-pagination-chevron { + border-color: var(--mat-tab-header-pagination-disabled-icon-color); + } + } + + .mat-mdc-input-element::placeholder { + color: var(--mat-table-header-headline-color); } + + .mat-mdc-table { + .mat-mdc-header-row { + background-color: var(--tb-orig-background-color); + } + + .mat-mdc-cell, .mat-mdc-header-cell { + &.mat-mdc-table-sticky { + background-color:var(--tb-orig-background-color); + } + } + + .mat-mdc-row { + background-color: rgba(0,0,0,0); + &.tb-current-entity { + background-color: var(--tb-current-entity-color); + .mat-mdc-cell.mat-mdc-table-sticky { + background-color: var(--tb-current-entity-sticky-color); + } + } + &:hover:not(.tb-current-entity){ + background-color: var(--tb-hover-color); + .mat-mdc-cell.mat-mdc-table-sticky { + background-color: var(--tb-hover-sticky-color); + } + } + &.mat-row-select.mat-selected:not(.tb-current-entity){ + background-color: var(--tb-selected-color); + .mat-mdc-cell.mat-mdc-table-sticky { + background-color: var(--tb-selected-sticky-color); + } + } + &, .mat-mdc-cell.mat-mdc-table-sticky { + transition: background-color .2s; + background-color:var(--tb-orig-background-color); + } + } + } } :host-context(.tb-has-timewindow) { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.html index 99ef5f7481..73ab2e679c 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.html @@ -41,13 +41,13 @@ class="flex-1" mat-stretch-tabs="false" [(selectedIndex)]="sourceIndex" (selectedIndexChange)="onSourceIndexChanged()"> - +
    - Timestamp + {{ 'widgets.table.timestamp-column-name' | translate }} @@ -78,7 +78,7 @@ + + + + +
    +
    + + +
    + +
    + + +
    + diff --git a/ui-ngx/src/app/modules/home/pages/dashboard/import-dashboard-file-dialog.component.ts b/ui-ngx/src/app/modules/home/pages/dashboard/import-dashboard-file-dialog.component.ts new file mode 100644 index 0000000000..01fca78bf9 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/dashboard/import-dashboard-file-dialog.component.ts @@ -0,0 +1,89 @@ +/// +/// Copyright © 2016-2025 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, Inject, OnInit } from '@angular/core'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { FormBuilder, FormGroup } from '@angular/forms'; +import { DashboardService } from '@core/http/dashboard.service'; +import { Dashboard } from '@app/shared/models/dashboard.models'; +import { ActionNotificationShow } from '@core/notification/notification.actions'; +import { DialogComponent } from '@shared/components/dialog.component'; +import { Router } from '@angular/router'; + +export interface DashboardInfoDialogData { + dashboard: Dashboard; +} + +@Component({ + selector: 'tb-import-dashboard-file-dialog', + templateUrl: './import-dashboard-file-dialog.component.html', + styleUrls: [] +}) +export class ImportDashboardFileDialogComponent extends DialogComponent implements OnInit { + + private dashboard: Dashboard; + currentFileName: string = ''; + uploadFileFormGroup: FormGroup; + + constructor(protected store: Store, + protected router: Router, + @Inject(MAT_DIALOG_DATA) public data: DashboardInfoDialogData, + private dashboardService: DashboardService, + protected dialogRef: MatDialogRef, + private fb: FormBuilder) { + super(store, router, dialogRef); + this.dashboard = data.dashboard; + } + + ngOnInit(): void { + this.uploadFileFormGroup = this.fb.group({ + file: [null] + }); + } + + cancel(): void { + this.dialogRef.close(); + } + + save() { + const fileControl = this.uploadFileFormGroup.get('file'); + if (!fileControl || !fileControl.value) { + return; + } + + const dashboardContent = { + ...fileControl.value, + description: this.dashboard.configuration.description + }; + this.dashboard.configuration = dashboardContent; + + this.dashboardService.saveDashboard(this.dashboard).subscribe(() => { + this.dialogRef.close(true); + }) + } + + loadDataFromJsonContent(content: string): any { + try { + const importData = JSON.parse(content); + return importData ? importData['configuration'] : importData; + } catch (err) { + this.store.dispatch(new ActionNotificationShow({message: err.message, type: 'error'})); + return null; + } + } +} diff --git a/ui-ngx/src/app/modules/home/pages/device-profile/device-profile-tabs.component.html b/ui-ngx/src/app/modules/home/pages/device-profile/device-profile-tabs.component.html index b9003c4c1c..b9b0292043 100644 --- a/ui-ngx/src/app/modules/home/pages/device-profile/device-profile-tabs.component.html +++ b/ui-ngx/src/app/modules/home/pages/device-profile/device-profile-tabs.component.html @@ -15,54 +15,92 @@ limitations under the License. --> - -
    - - device-profile.transport-type - - - {{deviceTransportTypeTranslations.get(type) | translate}} - - - - {{deviceTransportTypeHints.get(detailsForm.get('transportType').value) | translate}} - - - {{ 'device-profile.transport-type-required' | translate }} - - -
    - - -
    -
    -
    - - - - -
    -
    - +@if (entity) { + +
    + + device-profile.transport-type + + + {{deviceTransportTypeTranslations.get(type) | translate}} + + + + {{deviceTransportTypeHints.get(detailsForm.get('transportType').value) | translate}} + + + {{ 'device-profile.transport-type-required' | translate }} + + +
    + + +
    -
    - - -
    -
    - - + + @if (authUser.authority === authorities.TENANT_ADMIN && !isEdit) { + + + + } + + @if (hasOldRules || authUser.authority === authorities.TENANT_ADMIN && !isEdit) { + +
    + @if (hasOldRules && !isEdit) { +
    + + {{ 'alarm-rule.alarm-rules-actual' | translate }} + {{ 'alarm-rule.alarm-rules-old' | translate }} + +
    + } + @if (alarmRulesOldVersion || isEdit) { +
    +
    + +
    +
    + } @else { +
    + + +
    + } +
    +
    + } + + +
    +
    + + +
    -
    - + + @if (!isEdit) { + + + + } + @if (authUser.authority === authorities.TENANT_ADMIN && !isEdit) { + + + + + } +}
    @@ -73,13 +111,3 @@
    - - - - - - diff --git a/ui-ngx/src/app/modules/home/pages/device-profile/device-profile-tabs.component.ts b/ui-ngx/src/app/modules/home/pages/device-profile/device-profile-tabs.component.ts index 1e4b735ff7..629c9eeeef 100644 --- a/ui-ngx/src/app/modules/home/pages/device-profile/device-profile-tabs.component.ts +++ b/ui-ngx/src/app/modules/home/pages/device-profile/device-profile-tabs.component.ts @@ -41,6 +41,9 @@ export class DeviceProfileTabsComponent extends EntityTabsComponent, private destroyRef: DestroyRef) { super(store); @@ -57,6 +60,8 @@ export class DeviceProfileTabsComponent extends EntityTabsComponent - - - - - - - - - - - - - - - - - - - - - - - - - - +@if (entity) { + + + + + + + + + @if (authUser.authority === authorities.TENANT_ADMIN) { + + + + + + + } + + + + + + + + + + + + + @if (authUser.authority === authorities.TENANT_ADMIN) { + + + + } +} diff --git a/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app-dialog.component.ts b/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app-dialog.component.ts index f5a9dc4a5a..423979b4bc 100644 --- a/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app-dialog.component.ts @@ -29,6 +29,7 @@ import { MobileAppService } from '@core/http/mobile-app.service'; export interface MobileAppDialogData { platformType: PlatformType; + name?: string } @Component({ @@ -55,6 +56,9 @@ export class MobileAppDialogComponent extends DialogComponent { this.mobileAppComponent.entityForm.markAsDirty(); + if (this.data.name) { + this.mobileAppComponent.entityForm.get('title').patchValue(this.data.name, {emitEvent: false}); + } this.mobileAppComponent.entityForm.patchValue({platformType: this.data.platformType}); this.mobileAppComponent.entityForm.get('platformType').disable({emitEvent: false}); this.mobileAppComponent.isEdit = true; diff --git a/ui-ngx/src/app/modules/home/pages/mobile/bundes/mobile-bundle-dialog.component.html b/ui-ngx/src/app/modules/home/pages/mobile/bundes/mobile-bundle-dialog.component.html index 6d4bd01d1b..87f2481565 100644 --- a/ui-ngx/src/app/modules/home/pages/mobile/bundes/mobile-bundle-dialog.component.html +++ b/ui-ngx/src/app/modules/home/pages/mobile/bundes/mobile-bundle-dialog.component.html @@ -58,7 +58,7 @@ labelText="mobile.android-application" [entityType]="entityType.MOBILE_APP" [entitySubtype]="platformType.ANDROID" - (createNew)="createApplication('androidAppId', platformType.ANDROID)" + (createNew)="createApplication($event, 'androidAppId', platformType.ANDROID)" formControlName="androidAppId"> diff --git a/ui-ngx/src/app/modules/home/pages/mobile/bundes/mobile-bundle-dialog.component.ts b/ui-ngx/src/app/modules/home/pages/mobile/bundes/mobile-bundle-dialog.component.ts index 3e31401703..a866685282 100644 --- a/ui-ngx/src/app/modules/home/pages/mobile/bundes/mobile-bundle-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/pages/mobile/bundes/mobile-bundle-dialog.component.ts @@ -148,12 +148,13 @@ export class MobileBundleDialogComponent extends DialogComponent(MobileAppDialogComponent, { disableClose: true, panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], data: { - platformType + platformType, + name } }).afterClosed() .subscribe((app) => { diff --git a/ui-ngx/src/app/modules/home/pages/mobile/qr-code-widget/mobile-qr-code-widget-settings.component.html b/ui-ngx/src/app/modules/home/pages/mobile/qr-code-widget/mobile-qr-code-widget-settings.component.html index f88244add1..05f872ab25 100644 --- a/ui-ngx/src/app/modules/home/pages/mobile/qr-code-widget/mobile-qr-code-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/pages/mobile/qr-code-widget/mobile-qr-code-widget-settings.component.html @@ -39,6 +39,7 @@
    {{ 'mobile.bundle' | translate }}
    - - + + {{ recipientTitle }} - - + +
    @@ -233,7 +233,7 @@
    {{ preview.processedTemplates.MICROSOFT_TEAMS.subject }}
    {{ preview.processedTemplates.MICROSOFT_TEAMS.body }} - +
    diff --git a/ui-ngx/src/app/modules/home/pages/notification/template/template-notification-dialog.component.ts b/ui-ngx/src/app/modules/home/pages/notification/template/template-notification-dialog.component.ts index 98595de798..0f13409a3e 100644 --- a/ui-ngx/src/app/modules/home/pages/notification/template/template-notification-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/pages/notification/template/template-notification-dialog.component.ts @@ -40,6 +40,7 @@ export interface TemplateNotificationDialogData { predefinedType?: NotificationType; isAdd?: boolean; isCopy?: boolean; + name?: string; } @Component({ @@ -85,6 +86,9 @@ export class TemplateNotificationDialogComponent this.hideSelectType = true; this.templateNotificationForm.get('notificationType').setValue(this.data.predefinedType, {emitEvent: false}); } + if (isDefinedAndNotNull(this.data?.name)) { + this.templateNotificationForm.get('name').setValue(this.data.name, {emitEvent: false}); + } if (data.isAdd || data.isCopy) { this.dialogTitle = 'notification.add-notification-template'; diff --git a/ui-ngx/src/app/modules/home/pages/profile/profile.component.html b/ui-ngx/src/app/modules/home/pages/profile/profile.component.html index 44eea5865e..c3cf5ea0f0 100644 --- a/ui-ngx/src/app/modules/home/pages/profile/profile.component.html +++ b/ui-ngx/src/app/modules/home/pages/profile/profile.component.html @@ -37,7 +37,7 @@
    user.email - + {{ 'user.email-required' | translate }} @@ -58,12 +58,14 @@ [enableFlagsSelect]="true" formControlName="phone"> - + language.language - - - {{ lang ? ('language.locales.' + lang | translate) : ''}} - + + {{ 'language.auto' | translate }} + @for(lang of languageList; track lang) { + {{ 'language.locales.' + lang | translate }} + } diff --git a/ui-ngx/src/app/modules/home/pages/profile/profile.component.ts b/ui-ngx/src/app/modules/home/pages/profile/profile.component.ts index 7d1aa2aa4e..32d0c6aba8 100644 --- a/ui-ngx/src/app/modules/home/pages/profile/profile.component.ts +++ b/ui-ngx/src/app/modules/home/pages/profile/profile.component.ts @@ -25,10 +25,9 @@ import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms import { HasConfirmForm } from '@core/guards/confirm-on-exit.guard'; import { ActionAuthUpdateUserDetails } from '@core/auth/auth.actions'; import { environment as env } from '@env/environment'; -import { TranslateService } from '@ngx-translate/core'; import { ActionSettingsChangeLanguage } from '@core/settings/settings.actions'; import { ActivatedRoute } from '@angular/router'; -import { isDefinedAndNotNull, isNotEmptyStr } from '@core/utils'; +import { isDefinedAndNotNull, isNotEmptyStr, validateEmail } from '@core/utils'; import { getCurrentAuthUser } from '@core/auth/auth.selectors'; import { AuthService } from '@core/auth/auth.service'; import { UnitSystem, UnitSystems } from '@shared/models/unit.models'; @@ -52,7 +51,6 @@ export class ProfileComponent extends PageComponent implements OnInit, HasConfir private route: ActivatedRoute, private userService: UserService, private authService: AuthService, - private translate: TranslateService, private unitService: UnitService, private fb: UntypedFormBuilder) { super(store); @@ -66,7 +64,7 @@ export class ProfileComponent extends PageComponent implements OnInit, HasConfir private buildProfileForm() { this.profile = this.fb.group({ - email: ['', [Validators.required, Validators.email]], + email: ['', [Validators.required, validateEmail]], firstName: [''], lastName: [''], phone: [''], @@ -82,9 +80,13 @@ export class ProfileComponent extends PageComponent implements OnInit, HasConfir if (!this.user.additionalInfo) { this.user.additionalInfo = {}; } - this.user.additionalInfo.lang = this.profile.get('language').value; this.user.additionalInfo.homeDashboardId = this.profile.get('homeDashboardId').value; this.user.additionalInfo.homeDashboardHideToolbar = this.profile.get('homeDashboardHideToolbar').value; + if (isNotEmptyStr(this.profile.get('language').value)) { + this.user.additionalInfo.lang = this.profile.get('language').value; + } else { + delete this.user.additionalInfo.lang; + } if (isNotEmptyStr(this.profile.get('unitSystem').value)) { this.user.additionalInfo.unitSystem = this.profile.get('unitSystem').value; } else { @@ -105,7 +107,7 @@ export class ProfileComponent extends PageComponent implements OnInit, HasConfir id: user.id, lastName: user.lastName, } })); - this.store.dispatch(new ActionSettingsChangeLanguage({ userLang: user.additionalInfo.lang })); + this.store.dispatch(new ActionSettingsChangeLanguage({ userLang: user.additionalInfo.lang || env.defaultLang })); this.unitService.setUnitSystem(this.user.additionalInfo.unitSystem); this.authService.refreshJwtToken(false); } @@ -115,7 +117,7 @@ export class ProfileComponent extends PageComponent implements OnInit, HasConfir private userLoaded(user: User) { this.user = user; this.profile.reset(user); - let lang; + let lang: string = null; let homeDashboardId; let homeDashboardHideToolbar = true; let unitSystem: UnitSystem = null; @@ -131,9 +133,6 @@ export class ProfileComponent extends PageComponent implements OnInit, HasConfir unitSystem = user.additionalInfo.unitSystem; } } - if (!lang) { - lang = this.translate.currentLang; - } this.profile.get('language').setValue(lang); this.profile.get('unitSystem').setValue(unitSystem); this.profile.get('homeDashboardId').setValue(homeDashboardId); diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.ts b/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.ts index 2ac9b806e6..f7ead66646 100644 --- a/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.ts +++ b/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.ts @@ -26,6 +26,7 @@ import { OnInit, QueryList, Renderer2, + SecurityContext, SkipSelf, ViewChild, ViewChildren, @@ -97,6 +98,7 @@ import { HttpStatusCode } from '@angular/common/http'; import { TbContextMenuEvent } from '@shared/models/jquery-event.models'; import { EntityDebugSettings } from '@shared/models/entity.models'; import Timeout = NodeJS.Timeout; +import { DomSanitizer } from '@angular/platform-browser'; @Component({ selector: 'tb-rulechain-page', @@ -273,6 +275,7 @@ export class RuleChainPageComponent extends PageComponent private renderer: Renderer2, private viewContainerRef: ViewContainerRef, private changeDetector: ChangeDetectorRef, + private sanitizer:DomSanitizer, public dialog: MatDialog, public dialogService: DialogService, public fb: FormBuilder) { @@ -1360,9 +1363,13 @@ export class RuleChainPageComponent extends PageComponent name = node.name; desc = this.translate.instant(ruleNodeTypeDescriptors.get(node.component.type).name) + ' - ' + node.component.name; if (node.additionalInfo) { - details = node.additionalInfo.description; + details = this.sanitizer.sanitize(SecurityContext.HTML, node.additionalInfo.description); } } + + name = this.sanitizer.sanitize(SecurityContext.HTML, name); + desc = this.sanitizer.sanitize(SecurityContext.HTML, desc); + let tooltipContent = '
    ' + '
    ' + '
    ' + name + '
    ' + diff --git a/ui-ngx/src/app/modules/home/pages/security/authentication-dialog/email-auth-dialog.component.html b/ui-ngx/src/app/modules/home/pages/security/authentication-dialog/email-auth-dialog.component.html index 0175c85f56..6eba3f8cf7 100644 --- a/ui-ngx/src/app/modules/home/pages/security/authentication-dialog/email-auth-dialog.component.html +++ b/ui-ngx/src/app/modules/home/pages/security/authentication-dialog/email-auth-dialog.component.html @@ -39,7 +39,7 @@
    {{ 'user.email-required' | translate }} diff --git a/ui-ngx/src/app/modules/home/pages/security/authentication-dialog/email-auth-dialog.component.ts b/ui-ngx/src/app/modules/home/pages/security/authentication-dialog/email-auth-dialog.component.ts index 601c2e9277..6ed9099522 100644 --- a/ui-ngx/src/app/modules/home/pages/security/authentication-dialog/email-auth-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/pages/security/authentication-dialog/email-auth-dialog.component.ts @@ -28,6 +28,7 @@ import { TwoFactorAuthProviderType } from '@shared/models/two-factor-auth.models'; import { MatStepper } from '@angular/material/stepper'; +import { validateEmail } from '@app/core/utils'; export interface EmailAuthDialogData { email: string; @@ -57,7 +58,7 @@ export class EmailAuthDialogComponent extends DialogComponent

    security.2fa.dialog.scan-qr-code

    +

    login.enter-key-manually

    +
    + {{ totpAuthURLSecret }} + + +

    security.2fa.dialog.enter-verification-code

    ; @@ -55,6 +56,7 @@ export class TotpAuthDialogComponent extends DialogComponent { this.authAccountConfig = accountConfig as TotpTwoFactorAuthAccountConfig; this.totpAuthURL = this.authAccountConfig.authUrl; + this.totpAuthURLSecret = new URL(this.totpAuthURL).searchParams.get('secret'); this.authAccountConfig.useByDefault = true; import('qrcode').then((QRCode) => { unwrapModule(QRCode).toCanvas(this.canvasRef.nativeElement, this.totpAuthURL); diff --git a/ui-ngx/src/app/modules/home/pages/user/add-user-dialog.component.html b/ui-ngx/src/app/modules/home/pages/user/add-user-dialog.component.html index 8f4a8a4a6a..c9a83c1531 100644 --- a/ui-ngx/src/app/modules/home/pages/user/add-user-dialog.component.html +++ b/ui-ngx/src/app/modules/home/pages/user/add-user-dialog.component.html @@ -31,7 +31,7 @@
    - + user.activation-method diff --git a/ui-ngx/src/app/modules/home/pages/user/add-user-dialog.component.ts b/ui-ngx/src/app/modules/home/pages/user/add-user-dialog.component.ts index 7470deb3d6..11468f64fb 100644 --- a/ui-ngx/src/app/modules/home/pages/user/add-user-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/pages/user/add-user-dialog.component.ts @@ -84,6 +84,12 @@ export class AddUserDialogComponent extends DialogComponent { diff --git a/ui-ngx/src/app/modules/home/pages/user/user.component.html b/ui-ngx/src/app/modules/home/pages/user/user.component.html index 658b361255..6e654cf845 100644 --- a/ui-ngx/src/app/modules/home/pages/user/user.component.html +++ b/ui-ngx/src/app/modules/home/pages/user/user.component.html @@ -74,9 +74,9 @@
    - + user.email - + {{ 'user.invalid-email-format' | translate }} @@ -84,30 +84,52 @@ {{ 'user.email-required' | translate }} - + user.first-name - + user.last-name
    - + + language.language + + {{ 'language.auto' | translate }} + @for(lang of languageList; track lang) { + {{ 'language.locales.' + lang | translate }} + } + + + + unit.unit-system + + {{ 'unit.unit-system-type.AUTO' | translate }} + @for(unit of UnitSystems; track unit) { + {{ 'unit.unit-system-type.' + unit | translate }} + } + + + user.description
    -
    +
    -
    +
    { +export class UserComponent extends EntityComponent{ authority = Authority; + languageList = env.supportedLangs; + UnitSystems = UnitSystems; loginAsUserEnabled$ = this.store.pipe( select(selectAuth), @@ -70,13 +74,15 @@ export class UserComponent extends EntityComponent { buildForm(entity: User): UntypedFormGroup { return this.fb.group( { - email: [entity ? entity.email : '', [Validators.required, Validators.email]], + email: [entity ? entity.email : '', [Validators.required, validateEmail]], firstName: [entity ? entity.firstName : ''], lastName: [entity ? entity.lastName : ''], phone: [entity ? entity.phone : ''], additionalInfo: this.fb.group( { description: [entity && entity.additionalInfo ? entity.additionalInfo.description : ''], + lang: [entity && entity.additionalInfo ? entity.additionalInfo.lang : null], + unitSystem: [entity && entity.additionalInfo ? entity.additionalInfo.unitSystem : null], defaultDashboardId: [entity && entity.additionalInfo ? entity.additionalInfo.defaultDashboardId : null], defaultDashboardFullscreen: [entity && entity.additionalInfo ? entity.additionalInfo.defaultDashboardFullscreen : false], homeDashboardId: [entity && entity.additionalInfo ? entity.additionalInfo.homeDashboardId : null], @@ -94,6 +100,10 @@ export class UserComponent extends EntityComponent { this.entityForm.patchValue({lastName: entity.lastName}); this.entityForm.patchValue({phone: entity.phone}); this.entityForm.patchValue({additionalInfo: {description: entity.additionalInfo ? entity.additionalInfo.description : ''}}); + this.entityForm.patchValue({additionalInfo: + {lang: entity.additionalInfo ? entity.additionalInfo.lang : null}}); + this.entityForm.patchValue({additionalInfo: + {unitSystem: entity.additionalInfo ? entity.additionalInfo.unitSystem : null}}); this.entityForm.patchValue({additionalInfo: {defaultDashboardId: entity.additionalInfo ? entity.additionalInfo.defaultDashboardId : null}}); this.entityForm.patchValue({additionalInfo: diff --git a/ui-ngx/src/app/modules/home/pages/user/users-table-config.resolver.ts b/ui-ngx/src/app/modules/home/pages/user/users-table-config.resolver.ts index 2e056119d7..651da8d99f 100644 --- a/ui-ngx/src/app/modules/home/pages/user/users-table-config.resolver.ts +++ b/ui-ngx/src/app/modules/home/pages/user/users-table-config.resolver.ts @@ -158,6 +158,12 @@ export class UsersTableConfigResolver { user.tenantId = new TenantId(this.tenantId); user.customerId = new CustomerId(this.customerId); user.authority = this.authority; + if (!user.additionalInfo.lang) { + delete user.additionalInfo.lang; + } + if (!user.additionalInfo.unitSystem) { + delete user.additionalInfo.unitSystem; + } return this.userService.saveUser(user); } diff --git a/ui-ngx/src/app/modules/home/pages/widget/widget-type-autocomplete.component.scss b/ui-ngx/src/app/modules/home/pages/widget/widget-type-autocomplete.component.scss index e28d8ec14a..c8f678d679 100644 --- a/ui-ngx/src/app/modules/home/pages/widget/widget-type-autocomplete.component.scss +++ b/ui-ngx/src/app/modules/home/pages/widget/widget-type-autocomplete.component.scss @@ -24,7 +24,8 @@ } .tb-widget-type-option-image-preview { width: 36px; - max-height: 100%; + height: 100%; + max-height: 36px; object-fit: contain; border-radius: 6px; } diff --git a/ui-ngx/src/app/modules/login/login-routing.module.ts b/ui-ngx/src/app/modules/login/login-routing.module.ts index 3000d81fb8..a6e852958c 100644 --- a/ui-ngx/src/app/modules/login/login-routing.module.ts +++ b/ui-ngx/src/app/modules/login/login-routing.module.ts @@ -25,6 +25,7 @@ import { CreatePasswordComponent } from '@modules/login/pages/login/create-passw import { TwoFactorAuthLoginComponent } from '@modules/login/pages/login/two-factor-auth-login.component'; import { Authority } from '@shared/models/authority.enum'; import { LinkExpiredComponent } from '@modules/login/pages/login/link-expired.component'; +import { ForceTwoFactorAuthLoginComponent } from '@modules/login/pages/login/force-two-factor-auth-login.component'; const routes: Routes = [ { @@ -83,6 +84,16 @@ const routes: Routes = [ }, canActivate: [AuthGuard] }, + { + path: 'login/force-mfa', + component: ForceTwoFactorAuthLoginComponent, + data: { + title: 'login.two-factor-authentication', + auth: [Authority.MFA_CONFIGURATION_TOKEN], + module: 'public' + }, + canActivate: [AuthGuard] + }, { path: 'activationLinkExpired', component: LinkExpiredComponent, diff --git a/ui-ngx/src/app/modules/login/login.module.ts b/ui-ngx/src/app/modules/login/login.module.ts index 35dbfad7e2..9b5d1d7711 100644 --- a/ui-ngx/src/app/modules/login/login.module.ts +++ b/ui-ngx/src/app/modules/login/login.module.ts @@ -25,6 +25,7 @@ import { ResetPasswordComponent } from '@modules/login/pages/login/reset-passwor import { CreatePasswordComponent } from '@modules/login/pages/login/create-password.component'; import { TwoFactorAuthLoginComponent } from '@modules/login/pages/login/two-factor-auth-login.component'; import { LinkExpiredComponent } from '@modules/login/pages/login/link-expired.component'; +import { ForceTwoFactorAuthLoginComponent } from '@modules/login/pages/login/force-two-factor-auth-login.component'; @NgModule({ declarations: [ @@ -33,7 +34,8 @@ import { LinkExpiredComponent } from '@modules/login/pages/login/link-expired.co ResetPasswordComponent, CreatePasswordComponent, TwoFactorAuthLoginComponent, - LinkExpiredComponent + LinkExpiredComponent, + ForceTwoFactorAuthLoginComponent, ], imports: [ CommonModule, diff --git a/ui-ngx/src/app/modules/login/pages/login/force-two-factor-auth-login.component.html b/ui-ngx/src/app/modules/login/pages/login/force-two-factor-auth-login.component.html new file mode 100644 index 0000000000..39236ef8e2 --- /dev/null +++ b/ui-ngx/src/app/modules/login/pages/login/force-two-factor-auth-login.component.html @@ -0,0 +1,304 @@ + + + + + + + + + diff --git a/ui-ngx/src/app/modules/login/pages/login/force-two-factor-auth-login.component.scss b/ui-ngx/src/app/modules/login/pages/login/force-two-factor-auth-login.component.scss new file mode 100644 index 0000000000..d62d7f1628 --- /dev/null +++ b/ui-ngx/src/app/modules/login/pages/login/force-two-factor-auth-login.component.scss @@ -0,0 +1,110 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@import '../../../../../scss/constants'; + +:host { + display: flex; + flex: 1 1 0; + width: 100%; + height: 100%; + + .tb-two-factor-auth-login-content { + background-color: #eee; + + .tb-two-factor-auth-login-card { + max-height: 100vh; + overflow: auto; + padding: 48px 48px 48px 16px; + + @media #{$mat-xs} { + height: 100%; + } + + @media #{$mat-gt-xs} { + width: 450px !important; + } + + .mat-mdc-card-title { + font: 400 28px / 36px Roboto, "Helvetica Neue", sans-serif; + } + + .mat-mdc-card-header { + padding: 0; + } + + .mat-mdc-card-content { + margin-top: 34px; + margin-left: 40px; + padding: 0; + } + + .mat-body { + letter-spacing: 0.25px; + line-height: 16px; + } + + .backup-code { + p { + text-align: justify; + } + + .container { + border: 1px solid; + border-radius: 4px; + gap: 16px; + display: grid; + grid-template-columns: 1fr 1fr; + justify-items: center; + padding: 16px 0; + margin-bottom: 16px; + + .code { + letter-spacing: 0.25px; + font-family: Roboto Mono, "Helvetica Neue", monospace; + } + } + + .action-buttons { + margin-bottom: 40px; + } + } + } + } + + ::ng-deep { + .tb-two-factor-auth-login-content { + .tb-two-factor-auth-login-card { + button.mat-mdc-icon-button { + .mat-icon { + color: rgba(255, 255, 255, 0.8); + } + } + } + .mat-mdc-form-field .mat-mdc-form-field-hint-wrapper { + color: rgba(255, 255, 255, 0.8); + } + } + + button.provider, button.navigation { + text-align: start; + font-weight: 400; + color: rgba(255, 255, 255, 0.8); + &:not([disabled][disabled]) { + border-color: rgba(255, 255, 255, .8); + } + } + } +} diff --git a/ui-ngx/src/app/modules/login/pages/login/force-two-factor-auth-login.component.ts b/ui-ngx/src/app/modules/login/pages/login/force-two-factor-auth-login.component.ts new file mode 100644 index 0000000000..ef1917ba24 --- /dev/null +++ b/ui-ngx/src/app/modules/login/pages/login/force-two-factor-auth-login.component.ts @@ -0,0 +1,300 @@ +/// +/// Copyright © 2016-2025 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, ElementRef, OnDestroy, OnInit, signal, ViewChild } from '@angular/core'; +import { AuthService } from '@core/auth/auth.service'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { PageComponent } from '@shared/components/page.component'; +import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; +import { TwoFactorAuthenticationService } from '@core/http/two-factor-authentication.service'; +import { + AccountTwoFaSettings, + BackupCodeTwoFactorAuthAccountConfig, + TotpTwoFactorAuthAccountConfig, + TwoFactorAuthAccountConfig, + twoFactorAuthProvidersEnterCodeCardTranslate, + twoFactorAuthProvidersLoginData, + twoFactorAuthProvidersSuccessCardTranslate, + TwoFactorAuthProviderType +} from '@shared/models/two-factor-auth.models'; +import { phoneNumberPattern } from '@shared/models/settings.models'; +import { deepClone, isDefinedAndNotNull, unwrapModule } from '@core/utils'; +import { MatDialog } from '@angular/material/dialog'; +import { DialogService } from '@core/services/dialog.service'; +import { getCurrentAuthUser } from '@core/auth/auth.selectors'; +import printTemplate from '@home/pages/security/authentication-dialog/backup-code-print-template.raw'; +import { ImportExportService } from '@shared/import-export/import-export.service'; +import { mergeMap, tap } from 'rxjs/operators'; + +enum ForceTwoFAState { + SETUP = 'setup', + AUTHENTICATOR_APP = 'authenticatorApp', + SMS = 'sms', + EMAIL = 'email', + BACKUP_CODE = 'backupCode', +} + +enum ProvidersState { + INPUT = 'INPUT', + ENTER_CODE = 'ENTER_CODE', + SUCCESS = 'SUCCESS', +} + +enum BackupCodeState { + CODE = 'CODE', + SUCCESS = 'SUCCESS', +} + +@Component({ + selector: 'tb-force-two-factor-auth-login', + templateUrl: './force-two-factor-auth-login.component.html', + styleUrls: ['./force-two-factor-auth-login.component.scss'] +}) +export class ForceTwoFactorAuthLoginComponent extends PageComponent implements OnInit, OnDestroy { + + TwoFactorAuthProviderType = TwoFactorAuthProviderType; + providersData = twoFactorAuthProvidersLoginData; + allowProviders: TwoFactorAuthProviderType[] = []; + config: AccountTwoFaSettings; + + twoFactorAuthProvidersEnterCodeCardTranslate = twoFactorAuthProvidersEnterCodeCardTranslate; + twoFactorAuthProvidersSuccessCardTranslate = twoFactorAuthProvidersSuccessCardTranslate; + + ForceTwoFAState = ForceTwoFAState; + ProvidersState = ProvidersState; + BackupCodeState = BackupCodeState + + state = signal(ForceTwoFAState.SETUP); + appState = signal(ProvidersState.INPUT); + smsState = signal(ProvidersState.INPUT); + emailState = signal(ProvidersState.INPUT); + backupCodeState = signal(BackupCodeState.CODE); + + totpAuthURL: string; + totpAuthURLSecret: string; + backupCode: BackupCodeTwoFactorAuthAccountConfig; + + configForm: UntypedFormGroup; + smsConfigForm: UntypedFormGroup; + emailConfigForm: UntypedFormGroup; + + private providersInfo: TwoFactorAuthProviderType[]; + private authAccountConfig: TwoFactorAuthAccountConfig; + private useByDefault: boolean = true; + + @ViewChild('canvas', {static: false}) canvasRef: ElementRef; + + constructor(protected store: Store, + private authService: AuthService, + private twoFaService: TwoFactorAuthenticationService, + private importExportService: ImportExportService, + public dialog: MatDialog, + public dialogService: DialogService, + private fb: UntypedFormBuilder) { + super(store); + } + + ngOnInit() { + this.providersInfo = this.authService.forceTwoFactorAuthProviders; + this.allowedProviders(); + this.configForm = this.fb.group({ + verificationCode: ['', [ + Validators.required, + Validators.minLength(6), + Validators.maxLength(6), + Validators.pattern(/^\d*$/) + ]] + }); + + this.smsConfigForm = this.fb.group({ + phone: ['', [Validators.required, Validators.pattern(phoneNumberPattern)]] + }); + + this.emailConfigForm = this.fb.group({ + email: [getCurrentAuthUser(this.store).sub, [Validators.required, Validators.email]] + }); + + this.twoFaService.getAccountTwoFaSettings().subscribe(accountConfig => { + if (accountConfig) { + this.config = accountConfig; + this.useByDefault = false; + } + }); + } + + goBackByType(type: TwoFactorAuthProviderType) { + switch (type) { + case TwoFactorAuthProviderType.TOTP: + this.appState.set(ProvidersState.INPUT); + this.updateQRCode(); + break; + case TwoFactorAuthProviderType.SMS: + this.smsState.set(ProvidersState.INPUT); + break; + case TwoFactorAuthProviderType.EMAIL: + this.emailState.set(ProvidersState.INPUT); + break; + } + } + + get isAnyProviderAvailable() { + return this.config?.configs ? Object.keys(this.config?.configs)?.length < this.allowProviders?.length : true; + } + + private allowedProviders() { + if (isDefinedAndNotNull(this.config)) { + this.allowProviders = this.providersInfo; + } else { + this.allowProviders = this.providersInfo.filter(provider => provider !== TwoFactorAuthProviderType.BACKUP_CODE); + } + } + + updateState(type: TwoFactorAuthProviderType) { + switch (type) { + case TwoFactorAuthProviderType.TOTP: + this.state.set(ForceTwoFAState.AUTHENTICATOR_APP); + this.twoFaService.generateTwoFaAccountConfig(TwoFactorAuthProviderType.TOTP).subscribe(accountConfig => { + this.authAccountConfig = accountConfig as TotpTwoFactorAuthAccountConfig; + this.totpAuthURL = this.authAccountConfig.authUrl; + this.totpAuthURLSecret = new URL(this.totpAuthURL).searchParams.get('secret'); + this.authAccountConfig.useByDefault = this.useByDefault; + this.useByDefault = false; + this.updateQRCode(); + }); + break; + case TwoFactorAuthProviderType.SMS: + this.state.set(ForceTwoFAState.SMS); + break; + case TwoFactorAuthProviderType.EMAIL: + this.state.set(ForceTwoFAState.EMAIL); + break; + case TwoFactorAuthProviderType.BACKUP_CODE: + this.state.set(ForceTwoFAState.BACKUP_CODE); + this.twoFaService.generateTwoFaAccountConfig(TwoFactorAuthProviderType.BACKUP_CODE).pipe( + tap((data: BackupCodeTwoFactorAuthAccountConfig) => this.backupCode = data), + mergeMap(data => this.twoFaService.verifyAndSaveTwoFaAccountConfig(data, null, {ignoreLoading: true})) + ).subscribe((config) => { + this.config = config; + }); + break; + } + } + + sendSmsCode() { + if (this.smsConfigForm.valid) { + this.authAccountConfig = { + providerType: TwoFactorAuthProviderType.SMS, + useByDefault: this.useByDefault, + phoneNumber: this.smsConfigForm.get('phone').value as string + }; + this.useByDefault = false; + this.twoFaService.submitTwoFaAccountConfig(this.authAccountConfig).subscribe(() => this.smsState.set(ProvidersState.ENTER_CODE)); + } + } + + sendEmailCode() { + if (this.emailConfigForm.valid) { + this.authAccountConfig = { + providerType: TwoFactorAuthProviderType.EMAIL, + useByDefault: this.useByDefault, + email: this.emailConfigForm.get('email').value as string + }; + this.useByDefault = false; + this.twoFaService.submitTwoFaAccountConfig(this.authAccountConfig).subscribe(() => this.emailState.set(ProvidersState.ENTER_CODE)); + } + } + + tryAnotherWay(type: TwoFactorAuthProviderType) { + this.state.set(ForceTwoFAState.SETUP); + this.configForm.reset(); + switch (type) { + case TwoFactorAuthProviderType.TOTP: + this.appState.set(ProvidersState.INPUT); + break; + case TwoFactorAuthProviderType.SMS: + this.smsState.set(ProvidersState.INPUT); + this.smsConfigForm.reset(); + break; + case TwoFactorAuthProviderType.EMAIL: + this.emailState.set(ProvidersState.INPUT) + this.emailConfigForm.get('email').reset(getCurrentAuthUser(this.store).sub); + break; + } + } + + saveConfig(type: TwoFactorAuthProviderType) { + if (this.configForm.valid) { + this.twoFaService.verifyAndSaveTwoFaAccountConfig(this.authAccountConfig, + this.configForm.get('verificationCode').value).subscribe((config) => { + switch (type) { + case TwoFactorAuthProviderType.TOTP: + this.appState.set(ProvidersState.SUCCESS); + break; + case TwoFactorAuthProviderType.SMS: + this.smsState.set(ProvidersState.SUCCESS); + break; + case TwoFactorAuthProviderType.EMAIL: + this.emailState.set(ProvidersState.SUCCESS); + break; + } + this.config = config; + this.authAccountConfig = null; + this.allowedProviders(); + }); + } + } + + private updateQRCode() { + import('qrcode').then((QRCode) => { + unwrapModule(QRCode).toCanvas(this.canvasRef.nativeElement, this.totpAuthURL); + this.canvasRef.nativeElement.style.width = 'auto'; + this.canvasRef.nativeElement.style.height = 'auto'; + }); + } + + ngOnDestroy() { + super.ngOnDestroy(); + } + + cancelLogin() { + this.authService.logout(); + } + + downloadFile() { + this.importExportService.exportText(this.backupCode.codes, 'backup-codes'); + } + + printCode() { + const codeTemplate = deepClone(this.backupCode.codes) + .map(code => `
    ${code}
    `).join(''); + const printPage = printTemplate.replace('${codesBlock}', codeTemplate); + const newWindow = window.open('', 'Print backup code'); + + newWindow.document.open(); + newWindow.document.write(printPage); + + setTimeout(() => { + newWindow.print(); + + newWindow.document.close(); + + setTimeout(() => { + newWindow.close(); + }, 10); + }, 0); + } +} diff --git a/ui-ngx/src/app/modules/login/pages/login/login.component.html b/ui-ngx/src/app/modules/login/pages/login/login.component.html index 65b82023fe..c1a4b17a3c 100644 --- a/ui-ngx/src/app/modules/login/pages/login/login.component.html +++ b/ui-ngx/src/app/modules/login/pages/login/login.component.html @@ -43,7 +43,7 @@
    login.username - + email {{ 'login.invalid-email-format' | translate }} diff --git a/ui-ngx/src/app/modules/login/pages/login/login.component.ts b/ui-ngx/src/app/modules/login/pages/login/login.component.ts index aa7642b654..a4bd2853b9 100644 --- a/ui-ngx/src/app/modules/login/pages/login/login.component.ts +++ b/ui-ngx/src/app/modules/login/pages/login/login.component.ts @@ -19,11 +19,12 @@ import { AuthService } from '@core/auth/auth.service'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { PageComponent } from '@shared/components/page.component'; -import { UntypedFormBuilder } from '@angular/forms'; +import { UntypedFormBuilder, Validators } from '@angular/forms'; import { HttpErrorResponse } from '@angular/common/http'; import { Constants } from '@shared/models/constants'; import { Router } from '@angular/router'; import { OAuth2ClientLoginInfo } from '@shared/models/oauth2.models'; +import { validateEmail } from '@app/core/utils'; @Component({ selector: 'tb-login', @@ -35,8 +36,8 @@ export class LoginComponent extends PageComponent implements OnInit { passwordViolation = false; loginFormGroup = this.fb.group({ - username: '', - password: '' + username: ['', [Validators.required, validateEmail]], + password: [''] }); oauth2Clients: Array = null; diff --git a/ui-ngx/src/app/modules/login/pages/login/reset-password-request.component.html b/ui-ngx/src/app/modules/login/pages/login/reset-password-request.component.html index 96b85f2e47..abf2e6c61c 100644 --- a/ui-ngx/src/app/modules/login/pages/login/reset-password-request.component.html +++ b/ui-ngx/src/app/modules/login/pages/login/reset-password-request.component.html @@ -33,7 +33,7 @@ login.email - + email {{ 'user.invalid-email-format' | translate }} diff --git a/ui-ngx/src/app/modules/login/pages/login/reset-password-request.component.ts b/ui-ngx/src/app/modules/login/pages/login/reset-password-request.component.ts index 98b3d97398..3daeaa693d 100644 --- a/ui-ngx/src/app/modules/login/pages/login/reset-password-request.component.ts +++ b/ui-ngx/src/app/modules/login/pages/login/reset-password-request.component.ts @@ -22,6 +22,7 @@ import { PageComponent } from '@shared/components/page.component'; import { UntypedFormBuilder, Validators } from '@angular/forms'; import { ActionNotificationShow } from '@core/notification/notification.actions'; import { TranslateService } from '@ngx-translate/core'; +import { validateEmail } from '@app/core/utils'; @Component({ selector: 'tb-reset-password-request', @@ -33,7 +34,7 @@ export class ResetPasswordRequestComponent extends PageComponent implements OnIn clicked: boolean = false; requestPasswordRequest = this.fb.group({ - email: ['', [Validators.email, Validators.required]] + email: ['', [Validators.required, validateEmail]], }, {updateOn: 'submit'}); constructor(protected store: Store, diff --git a/ui-ngx/src/app/modules/login/pages/login/two-factor-auth-login.component.scss b/ui-ngx/src/app/modules/login/pages/login/two-factor-auth-login.component.scss index 68a4dcb4a4..09e3c29c43 100644 --- a/ui-ngx/src/app/modules/login/pages/login/two-factor-auth-login.component.scss +++ b/ui-ngx/src/app/modules/login/pages/login/two-factor-auth-login.component.scss @@ -72,9 +72,19 @@ } ::ng-deep { + .tb-two-factor-auth-login-content { + .tb-two-factor-auth-login-card { + button.mat-mdc-icon-button { + .mat-icon { + color: rgba(255, 255, 255, 0.8); + } + } + } + } button.provider { text-align: start; font-weight: 400; + color: rgba(255, 255, 255, 0.8); &:not([disabled][disabled]) { border-color: rgba(255, 255, 255, .8); } diff --git a/ui-ngx/src/app/shared/components/color-picker/hex-input.component.html b/ui-ngx/src/app/shared/components/color-picker/hex-input.component.html index 5209bc6b24..0d83545c63 100644 --- a/ui-ngx/src/app/shared/components/color-picker/hex-input.component.html +++ b/ui-ngx/src/app/shared/components/color-picker/hex-input.component.html @@ -30,7 +30,7 @@ - + %
    diff --git a/ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.html b/ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.html index 92a316efe9..4e03ca09de 100644 --- a/ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.html +++ b/ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.html @@ -18,7 +18,6 @@ - +
    @@ -71,6 +70,11 @@ {{ noEntitiesMatchingText | translate: {entity: searchText} }} + @if (allowCreateNew) { + + entity.create-new-key + + }
    diff --git a/ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.ts b/ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.ts index 8cb785c6f1..6f8b39c171 100644 --- a/ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.ts +++ b/ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.ts @@ -22,7 +22,7 @@ import { catchError, debounceTime, map, share, switchMap, tap } from 'rxjs/opera import { Store } from '@ngrx/store'; import { AppState } from '@app/core/core.state'; import { AliasEntityType, EntityType } from '@shared/models/entity-type.models'; -import { BaseData } from '@shared/models/base-data'; +import { BaseData, getEntityDisplayName } from '@shared/models/base-data'; import { EntityId } from '@shared/models/id/entity-id'; import { EntityService } from '@core/http/entity.service'; import { getCurrentAuthUser } from '@core/auth/auth.selectors'; @@ -138,11 +138,15 @@ export class EntityAutocompleteComponent implements ControlValueAccessor, OnInit @coerceArray() additionalClasses: Array; + @Input() + @coerceBoolean() + useEntityDisplayName = false; + @Output() entityChanged = new EventEmitter>(); @Output() - createNew = new EventEmitter(); + createNew = new EventEmitter(); @ViewChild('entityInput', {static: true}) entityInput: ElementRef; @@ -395,7 +399,7 @@ export class EntityAutocompleteComponent implements ControlValueAccessor, OnInit } displayEntityFn(entity?: BaseData): string | undefined { - return entity ? entity.name : undefined; + return entity ? (this.useEntityDisplayName ? getEntityDisplayName(entity) : entity.name) : undefined; } private fetchEntities(searchText?: string): Observable>> { @@ -451,9 +455,9 @@ export class EntityAutocompleteComponent implements ControlValueAccessor, OnInit return entityType; } - createNewEntity($event: Event) { + createNewEntity($event: Event, searchText?: string) { $event.stopPropagation(); - this.createNew.emit(); + this.createNew.emit(searchText); } get showEntityLink(): boolean { diff --git a/ui-ngx/src/app/shared/components/entity/entity-key-autocomplete.component.html b/ui-ngx/src/app/shared/components/entity/entity-key-autocomplete.component.html index 839a2e00cd..c08355462f 100644 --- a/ui-ngx/src/app/shared/components/entity/entity-key-autocomplete.component.html +++ b/ui-ngx/src/app/shared/components/entity/entity-key-autocomplete.component.html @@ -16,23 +16,23 @@ --> - - @if (keyControl.value) { + @if (keyControl.value && keyControl.enabled) { - } @else if (keyControl.hasError('required') && keyControl.touched) { + } @else if (keyControl.hasError('required') && keyControl.touched && keyControl.enabled) { warning diff --git a/ui-ngx/src/app/shared/components/entity/entity-key-autocomplete.component.ts b/ui-ngx/src/app/shared/components/entity/entity-key-autocomplete.component.ts index 84d723d114..8727c17f77 100644 --- a/ui-ngx/src/app/shared/components/entity/entity-key-autocomplete.component.ts +++ b/ui-ngx/src/app/shared/components/entity/entity-key-autocomplete.component.ts @@ -14,7 +14,17 @@ /// limitations under the License. /// -import { Component, effect, ElementRef, forwardRef, input, OnChanges, SimpleChanges, ViewChild, } from '@angular/core'; +import { + Component, + effect, + ElementRef, + forwardRef, + Input, + input, + OnChanges, + SimpleChanges, + ViewChild, +} from '@angular/core'; import { ControlValueAccessor, FormBuilder, @@ -32,6 +42,7 @@ import { AttributeScope, DataKeyType } from '@shared/models/telemetry/telemetry. import { EntitiesKeysByQuery } from '@shared/models/entity.models'; import { EntityFilter } from '@shared/models/query/query.models'; import { isEqual } from '@core/utils'; +import { TranslateService } from '@ngx-translate/core'; @Component({ selector: 'tb-entity-key-autocomplete', @@ -53,6 +64,9 @@ export class EntityKeyAutocompleteComponent implements ControlValueAccessor, Val @ViewChild('keyInput', {static: true}) keyInput: ElementRef; + @Input() placeholder = this.translate.instant('action.set'); + @Input() requiredText = this.translate.instant('common.hint.key-required'); + entityFilter = input.required(); dataKeyType = input.required(); keyScopeType = input(); @@ -70,7 +84,7 @@ export class EntityKeyAutocompleteComponent implements ControlValueAccessor, Val return this.cachedResult ? of(this.cachedResult) : this.entityService.findEntityKeysByQuery({ pageLink: { page: 0, pageSize: 100 }, entityFilter: this.entityFilter(), - }, this.dataKeyType() === DataKeyType.attribute, this.dataKeyType() === DataKeyType.timeseries, this.keyScopeType()); + }, this.dataKeyType() === DataKeyType.attribute, this.dataKeyType() === DataKeyType.timeseries, this.keyScopeType(), {ignoreLoading: true}); }), map(result => { this.cachedResult = result; @@ -96,6 +110,7 @@ export class EntityKeyAutocompleteComponent implements ControlValueAccessor, Val constructor( private fb: FormBuilder, private entityService: EntityService, + private translate: TranslateService, ) { this.keyControl.valueChanges .pipe(takeUntilDestroyed()) @@ -118,6 +133,7 @@ export class EntityKeyAutocompleteComponent implements ControlValueAccessor, Val if (filterChanged || keyScopeChanged || keyTypeChanged) { this.keyControl.setValue('', {emitEvent: false}); + this.cachedResult = null; } } @@ -136,10 +152,18 @@ export class EntityKeyAutocompleteComponent implements ControlValueAccessor, Val registerOnTouched(_): void {} validate(): ValidationErrors | null { - return this.keyControl.valid ? null : { keyControl: false }; + return this.keyControl.valid || this.keyControl.disabled ? null : { keyControl: false }; } writeValue(value: string): void { this.keyControl.patchValue(value, {emitEvent: false}); } + + setDisabledState(isDisabled: boolean): void { + if (isDisabled) { + this.keyControl.disable({emitEvent: false}); + } else { + this.keyControl.enable({emitEvent: false}); + } + } } diff --git a/ui-ngx/src/app/shared/components/entity/entity-list-select.component.html b/ui-ngx/src/app/shared/components/entity/entity-list-select.component.html index f3983111bf..0ae793a1ac 100644 --- a/ui-ngx/src/app/shared/components/entity/entity-list-select.component.html +++ b/ui-ngx/src/app/shared/components/entity/entity-list-select.component.html @@ -36,6 +36,7 @@ *ngIf="modelValue.entityType" [required]="required" [entityType]="modelValue.entityType" + [useEntityDisplayName]="useEntityDisplayName" formControlName="entityIds">
    diff --git a/ui-ngx/src/app/shared/components/entity/entity-list-select.component.ts b/ui-ngx/src/app/shared/components/entity/entity-list-select.component.ts index e4bef51d15..7c2c4e7f2a 100644 --- a/ui-ngx/src/app/shared/components/entity/entity-list-select.component.ts +++ b/ui-ngx/src/app/shared/components/entity/entity-list-select.component.ts @@ -68,6 +68,9 @@ export class EntityListSelectComponent implements ControlValueAccessor, OnInit { @Input() additionEntityTypes: {[key in string]: string} = {}; + @Input({transform: booleanAttribute}) + useEntityDisplayName = false; + displayEntityTypeSelect: boolean; private defaultEntityType: EntityType | AliasEntityType = null; diff --git a/ui-ngx/src/app/shared/components/entity/entity-list.component.html b/ui-ngx/src/app/shared/components/entity/entity-list.component.html index 9510f2a952..33c684285e 100644 --- a/ui-ngx/src/app/shared/components/entity/entity-list.component.html +++ b/ui-ngx/src/app/shared/components/entity/entity-list.component.html @@ -25,9 +25,10 @@ - {{entity.name}} + {{ displayEntityFn(entity) }} close + - +
    @@ -55,6 +62,11 @@ {{ 'entity.no-entities-matching' | translate: {entity: searchText} }} + @if (allowCreateNew) { + + entity.create-new-key + + }
    diff --git a/ui-ngx/src/app/shared/components/entity/entity-list.component.ts b/ui-ngx/src/app/shared/components/entity/entity-list.component.ts index 552c4f1f71..f93172c9b0 100644 --- a/ui-ngx/src/app/shared/components/entity/entity-list.component.ts +++ b/ui-ngx/src/app/shared/components/entity/entity-list.component.ts @@ -14,7 +14,18 @@ /// limitations under the License. /// -import { Component, ElementRef, forwardRef, Input, OnChanges, OnInit, SimpleChanges, ViewChild } from '@angular/core'; +import { + Component, + ElementRef, + EventEmitter, + forwardRef, + Input, + OnChanges, + OnInit, + Output, + SimpleChanges, + ViewChild +} from '@angular/core'; import { ControlValueAccessor, NG_VALIDATORS, @@ -28,7 +39,7 @@ import { Observable } from 'rxjs'; import { filter, map, mergeMap, share, tap } from 'rxjs/operators'; import { TranslateService } from '@ngx-translate/core'; import { EntityType } from '@shared/models/entity-type.models'; -import { BaseData } from '@shared/models/base-data'; +import { BaseData, getEntityDisplayName } from '@shared/models/base-data'; import { EntityId } from '@shared/models/id/entity-id'; import { EntityService } from '@core/http/entity.service'; import { MatAutocomplete } from '@angular/material/autocomplete'; @@ -93,6 +104,7 @@ export class EntityListComponent implements ControlValueAccessor, OnInit, OnChan } @Input() + @coerceBoolean() disabled: boolean; @Input() @@ -109,6 +121,17 @@ export class EntityListComponent implements ControlValueAccessor, OnInit, OnChan @coerceBoolean() inlineField: boolean; + @Input() + @coerceBoolean() + allowCreateNew: boolean; + + @Input() + @coerceBoolean() + useEntityDisplayName = false; + + @Output() + createNew = new EventEmitter(); + @ViewChild('entityInput') entityInput: ElementRef; @ViewChild('entityAutocomplete') matAutocomplete: MatAutocomplete; @ViewChild('chipList', {static: true}) chipList: MatChipGrid; @@ -136,6 +159,11 @@ export class EntityListComponent implements ControlValueAccessor, OnInit, OnChan this.entityListFormGroup.get('entities').updateValueAndValidity(); } + createNewEntity($event: Event, searchText?: string) { + $event.stopPropagation(); + this.createNew.emit(searchText); + } + registerOnChange(fn: any): void { this.propagateChange = fn; } @@ -201,6 +229,9 @@ export class EntityListComponent implements ControlValueAccessor, OnInit, OnChan this.modelValue = null; } this.dirty = true; + if (this.entityInput) { + this.entityInput.nativeElement.value = ''; + } } validate(): ValidationErrors | null { @@ -250,7 +281,7 @@ export class EntityListComponent implements ControlValueAccessor, OnInit, OnChan } public displayEntityFn(entity?: BaseData): string | undefined { - return entity ? entity.name : undefined; + return entity ? (this.useEntityDisplayName ? getEntityDisplayName(entity) : entity.name) : undefined; } private fetchEntities(searchText?: string): Observable>> { diff --git a/ui-ngx/src/app/shared/components/entity/entity-select.component.html b/ui-ngx/src/app/shared/components/entity/entity-select.component.html index 2b07af9e08..c0b4f1aafb 100644 --- a/ui-ngx/src/app/shared/components/entity/entity-select.component.html +++ b/ui-ngx/src/app/shared/components/entity/entity-select.component.html @@ -33,6 +33,7 @@ [appearance]="appearance" [required]="required" [entityType]="modelValue.entityType" + [useEntityDisplayName]="useEntityDisplayName" formControlName="entityId">
    diff --git a/ui-ngx/src/app/shared/components/entity/entity-select.component.ts b/ui-ngx/src/app/shared/components/entity/entity-select.component.ts index 01b1f03388..5e75426952 100644 --- a/ui-ngx/src/app/shared/components/entity/entity-select.component.ts +++ b/ui-ngx/src/app/shared/components/entity/entity-select.component.ts @@ -62,6 +62,10 @@ export class EntitySelectComponent implements ControlValueAccessor, OnInit, Afte @Input() appearance: MatFormFieldAppearance = 'fill'; + @Input() + @coerceBoolean() + useEntityDisplayName = false; + displayEntityTypeSelect: boolean; AliasEntityType = AliasEntityType; diff --git a/ui-ngx/src/app/shared/components/entity/entity-subtype-list.component.html b/ui-ngx/src/app/shared/components/entity/entity-subtype-list.component.html index 595010fb19..fb2888c636 100644 --- a/ui-ngx/src/app/shared/components/entity/entity-subtype-list.component.html +++ b/ui-ngx/src/app/shared/components/entity/entity-subtype-list.component.html @@ -22,6 +22,7 @@ {{customTranslate(entitySubtype)}} @@ -46,7 +47,7 @@ (optionSelected)="selected($event)" [displayWith]="displayEntitySubtypeFn"> - +
    diff --git a/ui-ngx/src/app/shared/components/file-input.component.html b/ui-ngx/src/app/shared/components/file-input.component.html index 105257dd24..1f46ea1e34 100644 --- a/ui-ngx/src/app/shared/components/file-input.component.html +++ b/ui-ngx/src/app/shared/components/file-input.component.html @@ -39,11 +39,13 @@ [flow]="flow.flowJs">
    cloud_upload - {{ dropLabel }} - +
    + {{ dropLabel }} + +
    diff --git a/ui-ngx/src/app/shared/components/file-input.component.ts b/ui-ngx/src/app/shared/components/file-input.component.ts index bbe68bb9c6..6960db73dd 100644 --- a/ui-ngx/src/app/shared/components/file-input.component.ts +++ b/ui-ngx/src/app/shared/components/file-input.component.ts @@ -129,10 +129,15 @@ export class FileInputComponent extends PageComponent implements AfterViewInit, @Output() fileNameChanged = new EventEmitter(); + @Output() + mediaTypeChanged = new EventEmitter(); + fileName: string | string[]; fileContent: any; files: File[]; + mediaType: string; + @ViewChild('flow', {static: true}) flow: FlowDirective; @@ -180,6 +185,7 @@ export class FileInputComponent extends PageComponent implements AfterViewInit, this.fileContent = files[0].fileContent; this.fileName = files[0].fileName; this.files = files[0].files; + this.mediaType = files[0].mediaType; this.updateModel(); } else if (files.length > 1) { this.fileContent = files.map(content => content.fileContent); @@ -203,6 +209,7 @@ export class FileInputComponent extends PageComponent implements AfterViewInit, let fileName = null; let fileContent = null; let files = null; + let mediaType = null; if (reader.readyState === reader.DONE) { if (!this.workFromFileObj) { fileContent = reader.result; @@ -211,16 +218,18 @@ export class FileInputComponent extends PageComponent implements AfterViewInit, fileContent = this.contentConvertFunction(fileContent); } fileName = fileContent ? file.name : null; + mediaType = file?.file?.type || null; } } else if (file.name || file.file){ files = file.file; fileName = file.name; + mediaType = file.file.type || null; } } - resolve({fileContent, fileName, files}); + resolve({fileContent, fileName, files, mediaType}); }; reader.onerror = () => { - resolve({fileContent: null, fileName: null, files: null}); + resolve({fileContent: null, fileName: null, files: null, mediaType: null}); }; if (this.readAsBinary) { reader.readAsBinaryString(file.file); @@ -283,6 +292,7 @@ export class FileInputComponent extends PageComponent implements AfterViewInit, this.propagateChange(this.files); } else { this.propagateChange(this.fileContent); + this.mediaTypeChanged.emit(this.mediaType); this.fileNameChanged.emit(this.fileName); } } diff --git a/ui-ngx/src/app/shared/components/icon.component.ts b/ui-ngx/src/app/shared/components/icon.component.ts index 2b6170b71c..20ee9c4a72 100644 --- a/ui-ngx/src/app/shared/components/icon.component.ts +++ b/ui-ngx/src/app/shared/components/icon.component.ts @@ -33,6 +33,9 @@ import { Subscription } from 'rxjs'; import { take } from 'rxjs/operators'; import { isSvgIcon, splitIconName } from '@shared/models/icon.models'; import { ContentObserver } from '@angular/cdk/observers'; +import { isTbImage } from '@shared/models/resource.models'; +import { ImagePipe } from '@shared/pipe/image.pipe'; +import { DomSanitizer } from '@angular/platform-browser'; const _TbIconBase = mixinColor( class { @@ -70,7 +73,7 @@ const funcIriPattern = /^url\(['"]?#(.*?)['"]?\)$/; host: { role: 'img', class: 'mat-icon notranslate', - '[attr.data-mat-icon-type]': '!_useSvgIcon ? "font" : "svg"', + '[attr.data-mat-icon-type]': '_useSvgIcon ? "svg" : (_useImageIcon ? null : "font")', '[attr.data-mat-icon-name]': '_svgName', '[attr.data-mat-icon-namespace]': '_svgNamespace', '[class.mat-icon-no-color]': 'color !== "primary" && color !== "accent" && color !== "warn"', @@ -99,6 +102,9 @@ export class TbIconComponent extends _TbIconBase private _textElement = null; + _useImageIcon = false; + private _imageElement = null; + private _previousPath?: string; private _elementsWithExternalReferences?: Map; @@ -109,6 +115,8 @@ export class TbIconComponent extends _TbIconBase private contentObserver: ContentObserver, private renderer: Renderer2, private _iconRegistry: MatIconRegistry, + private imagePipe: ImagePipe, + private sanitizer: DomSanitizer, @Inject(MAT_ICON_LOCATION) private _location: MatIconLocation, private readonly _errorHandler: ErrorHandler) { super(elementRef); @@ -148,16 +156,29 @@ export class TbIconComponent extends _TbIconBase private _updateIcon() { const useSvgIcon = isSvgIcon(this.icon); + const useImageIcon = isTbImage(this.icon); if (this._useSvgIcon !== useSvgIcon) { this._useSvgIcon = useSvgIcon; if (!this._useSvgIcon) { this._updateSvgIcon(undefined); } else { this._updateFontIcon(undefined); + this._updateImageIcon(undefined); + } + } + if (this._useImageIcon !== useImageIcon) { + this._useImageIcon = useImageIcon; + if (!this._useImageIcon) { + this._updateImageIcon(undefined); + } else { + this._updateFontIcon(undefined); + this._updateSvgIcon(undefined); } } if (this._useSvgIcon) { this._updateSvgIcon(this.icon); + } else if (this._useImageIcon) { + this._updateImageIcon(this.icon); } else { this._updateFontIcon(this.icon); } @@ -278,4 +299,49 @@ export class TbIconComponent extends _TbIconBase } } + private _updateImageIcon(rawName: string | undefined) { + if (rawName) { + this._clearImageIcon(); + this.imagePipe.transform(rawName, { asString: true, ignoreLoadingImage: true }).subscribe( + imageUrl => { + const urlStr = imageUrl as string; + const isSvg = urlStr?.startsWith('data:image/svg+xml') || urlStr?.endsWith('.svg'); + if (isSvg) { + const safeUrl = this.sanitizer.bypassSecurityTrustResourceUrl(urlStr); + this._iconRegistry + .getSvgIconFromUrl(safeUrl) + .pipe(take(1)) + .subscribe({ + next: (svg) => { + this.renderer.insertBefore(this._elementRef.nativeElement, svg, this._iconNameContent.nativeElement); + this._imageElement = svg; + }, + error: () => this._setImageElement(urlStr) + }); + } else { + this._setImageElement(urlStr); + } + } + ); + } else { + this._clearImageIcon(); + } + } + + private _setImageElement(urlStr: string) { + const imgElement = this.renderer.createElement('img'); + this.renderer.addClass(imgElement, 'mat-icon'); + this.renderer.setAttribute(imgElement, 'alt', 'Image icon'); + this.renderer.setAttribute(imgElement, 'src', urlStr); + this.renderer.insertBefore(this._elementRef.nativeElement, imgElement, this._iconNameContent.nativeElement); + this._imageElement = imgElement; + } + + private _clearImageIcon() { + const elem: HTMLElement = this._elementRef.nativeElement; + if (this._imageElement !== null) { + this.renderer.removeChild(elem, this._imageElement); + this._imageElement = null; + } + } } diff --git a/ui-ngx/src/app/shared/components/material-icon-select.component.ts b/ui-ngx/src/app/shared/components/material-icon-select.component.ts index 9432b3c96e..2bbc5b7528 100644 --- a/ui-ngx/src/app/shared/components/material-icon-select.component.ts +++ b/ui-ngx/src/app/shared/components/material-icon-select.component.ts @@ -71,6 +71,10 @@ export class MaterialIconSelectComponent extends PageComponent implements OnInit @coerceBoolean() iconClearButton = false; + @Input() + @coerceBoolean() + allowedCustomIcon = false; + private requiredValue: boolean; get required(): boolean { return this.requiredValue; @@ -169,7 +173,8 @@ export class MaterialIconSelectComponent extends PageComponent implements OnInit this.viewContainerRef, MaterialIconsComponent, 'left', true, null, { selectedIcon: this.materialIconFormGroup.get('icon').value, - iconClearButton: this.iconClearButton + iconClearButton: this.iconClearButton, + allowedCustomIcon: this.allowedCustomIcon, }, {}, {}, {}, true); diff --git a/ui-ngx/src/app/shared/components/material-icons.component.html b/ui-ngx/src/app/shared/components/material-icons.component.html index d4d9fe93a6..b6880e4ffe 100644 --- a/ui-ngx/src/app/shared/components/material-icons.component.html +++ b/ui-ngx/src/app/shared/components/material-icons.component.html @@ -16,63 +16,86 @@ -->
    -
    icon.icons
    - - search - - - - -
    - - - - +
    + icon.icons + @if (allowedCustomIcon) { + + {{ 'resource.system' | translate }} + {{ 'icon.custom' | translate }} + + + } +
    + @if (!isCustomIcon) { + + search + + + + +
    + + + + +
    +
    + +
    +
    +
    {{ 'icon.no-icons-found' | translate:{iconSearch: searchIconControl.value} }}
    +
    +
    +
    + + +
    - - -
    -
    -
    {{ 'icon.no-icons-found' | translate:{iconSearch: searchIconControl.value} }}
    + } @else { + + +
    + +
    - -
    - - - -
    + }
    diff --git a/ui-ngx/src/app/shared/components/material-icons.component.ts b/ui-ngx/src/app/shared/components/material-icons.component.ts index ece1a99108..d144fd45e6 100644 --- a/ui-ngx/src/app/shared/components/material-icons.component.ts +++ b/ui-ngx/src/app/shared/components/material-icons.component.ts @@ -37,6 +37,7 @@ import { TbPopoverComponent } from '@shared/components/popover.component'; import { BreakpointObserver } from '@angular/cdk/layout'; import { MediaBreakpoints } from '@shared/models/constants'; import { coerceBoolean } from '@shared/decorators/coercion'; +import { isTbImage } from '@shared/models/resource.models'; @Component({ selector: 'tb-material-icons', @@ -61,6 +62,10 @@ export class MaterialIconsComponent extends PageComponent implements OnInit { @coerceBoolean() showTitle = true; + @Input() + @coerceBoolean() + allowedCustomIcon = false; + @Input() popover: TbPopoverComponent; @@ -71,6 +76,8 @@ export class MaterialIconsComponent extends PageComponent implements OnInit { showAllSubject = new BehaviorSubject(false); searchIconControl: UntypedFormControl; + isCustomIcon = false; + iconsRowHeight = 48; iconsPanelHeight: string; @@ -122,14 +129,15 @@ export class MaterialIconsComponent extends PageComponent implements OnInit { map((data) => data.iconRows), share() ); + this.isCustomIcon = isTbImage(this.selectedIcon) } clearSearch() { this.searchIconControl.patchValue('', {emitEvent: true}); } - selectIcon(icon: MaterialIcon) { - this.iconSelected.emit(icon.name); + selectIcon(icon: string) { + this.iconSelected.emit(icon); } clearIcon() { diff --git a/ui-ngx/src/app/shared/components/notification/template-autocomplete.component.html b/ui-ngx/src/app/shared/components/notification/template-autocomplete.component.html index d5e67a1f60..d712f11114 100644 --- a/ui-ngx/src/app/shared/components/notification/template-autocomplete.component.html +++ b/ui-ngx/src/app/shared/components/notification/template-autocomplete.component.html @@ -29,7 +29,7 @@ (click)="clear()"> close -
    diff --git a/ui-ngx/src/app/shared/components/notification/template-autocomplete.component.ts b/ui-ngx/src/app/shared/components/notification/template-autocomplete.component.ts index 97c2a56bf5..3e703e5d6a 100644 --- a/ui-ngx/src/app/shared/components/notification/template-autocomplete.component.ts +++ b/ui-ngx/src/app/shared/components/notification/template-autocomplete.component.ts @@ -212,13 +212,17 @@ export class TemplateAutocompleteComponent implements ControlValueAccessor, OnIn } createTemplate($event: Event, button: MatButton) { - if ($event) { - $event.stopPropagation(); - } + $event?.stopPropagation(); button._elementRef.nativeElement.blur(); + this.createTemplateByName($event); + } + + createTemplateByName($event: Event, name?: string) { + $event?.stopPropagation(); this.openNotificationTemplateDialog({ isAdd: true, - predefinedType: this.notificationTypes + predefinedType: this.notificationTypes, + name }); } diff --git a/ui-ngx/src/app/shared/components/phone-input.component.html b/ui-ngx/src/app/shared/components/phone-input.component.html index 6bf61ae4b8..ff5d5f4853 100644 --- a/ui-ngx/src/app/shared/components/phone-input.component.html +++ b/ui-ngx/src/app/shared/components/phone-input.component.html @@ -27,7 +27,7 @@
    - + {{ label }} - + - {{ 'phone-input.phone-input-required' | translate }} + {{ requiredErrorText }} - {{ 'phone-input.phone-input-validation' | translate }} + {{ validationErrorText }}
    diff --git a/ui-ngx/src/app/shared/components/phone-input.component.ts b/ui-ngx/src/app/shared/components/phone-input.component.ts index bd1124f97a..5d6d0b21a7 100644 --- a/ui-ngx/src/app/shared/components/phone-input.component.ts +++ b/ui-ngx/src/app/shared/components/phone-input.component.ts @@ -77,6 +77,15 @@ export class PhoneInputComponent implements OnInit, ControlValueAccessor, Valida @Input() label = this.translate.instant('phone-input.phone-input-label'); + @Input() + hint = 'phone-input.phone-input-hint'; + + @Input() + requiredErrorText = this.translate.instant('phone-input.phone-input-required'); + + @Input() + validationErrorText = this.translate.instant('phone-input.phone-input-validation'); + get showFlagSelect(): boolean { return this.enableFlagsSelect && !this.isLegacy; } diff --git a/ui-ngx/src/app/shared/components/string-autocomplete.component.html b/ui-ngx/src/app/shared/components/string-autocomplete.component.html index 8eb22707f9..31966b932d 100644 --- a/ui-ngx/src/app/shared/components/string-autocomplete.component.html +++ b/ui-ngx/src/app/shared/components/string-autocomplete.component.html @@ -24,12 +24,12 @@ [matAutocomplete]="optionsAutocomplete"> - warning - + {{errorText}} this.updateView(value)), @@ -152,7 +156,7 @@ export class StringAutocompleteComponent implements ControlValueAccessor, OnInit updateView(value: string) { this.searchText = value ? value : ''; if (this.modelValue !== value) { - this.modelValue = value; + this.modelValue = value?.trim(); this.propagateChange(this.modelValue); } } diff --git a/ui-ngx/src/app/shared/components/string-items-list.component.html b/ui-ngx/src/app/shared/components/string-items-list.component.html index d467fbe5f0..0469be2589 100644 --- a/ui-ngx/src/app/shared/components/string-items-list.component.html +++ b/ui-ngx/src/app/shared/components/string-items-list.component.html @@ -23,6 +23,7 @@ {{ label }} diff --git a/ui-ngx/src/app/shared/components/string-items-list.component.ts b/ui-ngx/src/app/shared/components/string-items-list.component.ts index 5fe6f44609..1c210fdf1f 100644 --- a/ui-ngx/src/app/shared/components/string-items-list.component.ts +++ b/ui-ngx/src/app/shared/components/string-items-list.component.ts @@ -39,7 +39,7 @@ export interface StringItemsOption { @Component({ selector: 'tb-string-items-list', templateUrl: './string-items-list.component.html', - styleUrls: ['./string-items-list.component.scss'], + styleUrls: [], providers: [ { provide: NG_VALUE_ACCESSOR, diff --git a/ui-ngx/src/app/modules/home/components/rule-node/common/time-unit-input.component.html b/ui-ngx/src/app/shared/components/time-unit-input.component.html similarity index 68% rename from ui-ngx/src/app/modules/home/components/rule-node/common/time-unit-input.component.html rename to ui-ngx/src/app/shared/components/time-unit-input.component.html index 0da1cf4023..c49ce517b3 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/common/time-unit-input.component.html +++ b/ui-ngx/src/app/shared/components/time-unit-input.component.html @@ -15,10 +15,10 @@ limitations under the License. --> -
    - + + subscriptSizing="dynamic"> @if (labelText && !inlineField) { {{ labelText }} } @@ -28,21 +28,22 @@
    @if (inlineField) { - warning - - } @else { - - - {{ hasError }} - + matTooltipPosition="above" + matTooltipClass="tb-error-tooltip" + [matTooltip]="hasError" + *ngIf="hasError" + class="tb-error"> + warning + } + {{ hintText }} + + {{ hasError }} + - @if (!inlineField) { diff --git a/ui-ngx/src/app/modules/home/components/rule-node/common/time-unit-input.component.ts b/ui-ngx/src/app/shared/components/time-unit-input.component.ts similarity index 61% rename from ui-ngx/src/app/modules/home/components/rule-node/common/time-unit-input.component.ts rename to ui-ngx/src/app/shared/components/time-unit-input.component.ts index e31d9abf9e..ac2211a50d 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/common/time-unit-input.component.ts +++ b/ui-ngx/src/app/shared/components/time-unit-input.component.ts @@ -14,7 +14,7 @@ /// limitations under the License. /// -import { Component, DestroyRef, forwardRef, Input, OnInit } from '@angular/core'; +import { Component, DestroyRef, forwardRef, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core'; import { AbstractControl, ControlValueAccessor, @@ -23,9 +23,10 @@ import { NG_VALUE_ACCESSOR, ValidationErrors, Validator, + ValidatorFn, Validators } from '@angular/forms'; -import { TimeUnit, timeUnitTranslations } from '../rule-node-config.models'; +import { TimeUnit, timeUnitTranslations } from '@home/components/rule-node/rule-node-config.models'; import { isDefinedAndNotNull, isNumeric } from '@core/utils'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { coerceBoolean, coerceNumber } from '@shared/decorators/coercion'; @@ -50,11 +51,14 @@ interface TimeUnitInputModel { multi: true }] }) -export class TimeUnitInputComponent implements ControlValueAccessor, Validator, OnInit { +export class TimeUnitInputComponent implements ControlValueAccessor, Validator, OnInit, OnChanges { @Input() labelText: string; + @Input() + hintText: string; + @Input() @coerceBoolean() required: boolean; @@ -76,6 +80,13 @@ export class TimeUnitInputComponent implements ControlValueAccessor, Validator, @Input() maxErrorText: string; + @Input() + @coerceNumber() + stepMultipleOf: number; + + @Input() + stepMultipleOfErrorText: string; + @Input() subscriptSizing: SubscriptSizing = 'fixed'; @@ -86,6 +97,13 @@ export class TimeUnitInputComponent implements ControlValueAccessor, Validator, @coerceBoolean() inlineField: boolean; + @Input() + @coerceBoolean() + sameWidthInputs: boolean = false; + + @Input() + containerClass: string | string[] | Record = "flex gap-4"; + timeUnits = Object.values(TimeUnit).filter(item => item !== TimeUnit.MILLISECONDS) as TimeUnit[]; timeUnitTranslations = timeUnitTranslations; @@ -111,17 +129,10 @@ export class TimeUnitInputComponent implements ControlValueAccessor, Validator, } ngOnInit() { - if (this.maxTime) { - const maxTimeMs = this.maxTime * SECOND; - if (maxTimeMs < MINUTE) { - this.timeUnits = this.timeUnits.filter(item => item !== TimeUnit.MINUTES && item !== TimeUnit.HOURS && item !== TimeUnit.DAYS); - } else if (maxTimeMs < HOUR) { - this.timeUnits = this.timeUnits.filter(item => item !== TimeUnit.HOURS && item !== TimeUnit.DAYS); - } else if (maxTimeMs < DAY) { - this.timeUnits = this.timeUnits.filter(item => item !== TimeUnit.DAYS); - } + if (isDefinedAndNotNull(this.maxTime)) { + this.updatedAllowTimeUnitInterval(this.maxTime); } - if(this.required || this.maxTime) { + if (this.required || this.maxTime || isDefinedAndNotNull(this.minTime) || this.stepMultipleOf) { const timeControl = this.timeInputForm.get('time'); const validators = [Validators.pattern(/^\d*$/)]; if (this.required) { @@ -133,7 +144,13 @@ export class TimeUnitInputComponent implements ControlValueAccessor, Validator, ); } if (isDefinedAndNotNull(this.minTime)) { - validators.push(Validators.min(this.minTime)); + validators.push((control: AbstractControl) => + Validators.min(Math.ceil(this.minTime / this.timeIntervalsInSec.get(this.timeInputForm.get('timeUnit').value)))(control) + ); + } + + if (isDefinedAndNotNull(this.stepMultipleOf) && this.stepMultipleOf > 0) { + validators.push(this.createStepMultipleOfValidator()); } timeControl.setValidators(validators); @@ -161,6 +178,23 @@ export class TimeUnitInputComponent implements ControlValueAccessor, Validator, return this.minErrorText; } else if (this.timeInputForm.get('time').hasError('max') && this.maxErrorText) { return this.maxErrorText; + } else if (this.timeInputForm.get('time').hasError('stepMultipleOf') && this.stepMultipleOfErrorText) { + return this.stepMultipleOfErrorText; + } + } + + ngOnChanges(changes: SimpleChanges): void { + for (const propName of Object.keys(changes)) { + const change = changes[propName]; + if (!change.firstChange && change.currentValue !== change.previousValue) { + if (propName === 'maxTime') { + if (isDefinedAndNotNull(this.maxTime)) { + this.timeUnits = Object.values(TimeUnit).filter(item => item !== TimeUnit.MILLISECONDS) as TimeUnit[]; + this.updatedAllowTimeUnitInterval(this.maxTime); + this.timeInputForm.get('time').updateValueAndValidity({emitEvent: false}); + } + } + } } } @@ -176,7 +210,7 @@ export class TimeUnitInputComponent implements ControlValueAccessor, Validator, this.timeInputForm.disable({emitEvent: false}); } else { this.timeInputForm.enable({emitEvent: false}); - if(this.timeInputForm.invalid) { + if(!this.timeInputForm.valid) { setTimeout(() => this.updatedModel(this.timeInputForm.value, true)) } } @@ -198,7 +232,7 @@ export class TimeUnitInputComponent implements ControlValueAccessor, Validator, } validate(): ValidationErrors | null { - return this.timeInputForm.valid ? null : { + return this.timeInputForm.disabled || this.timeInputForm.valid ? null : { timeInput: false }; } @@ -223,4 +257,46 @@ export class TimeUnitInputComponent implements ControlValueAccessor, Validator, } } + private createStepMultipleOfValidator(): ValidatorFn { + return (control: AbstractControl): ValidationErrors | null => { + const time = control.value; + if (!isDefinedAndNotNull(time) || !isNumeric(time)) { + return null; + } + const numericTime = Number(time); + if (numericTime === 0) { + return null; + } + + const timeUnit = control.parent?.get('timeUnit')?.value as TimeUnit; + if (!timeUnit) { + return null; + } + + const unitInSec = this.timeIntervalsInSec.get(timeUnit); + const totalTimeInSec = numericTime * unitInSec; + const multipleOfVal = this.stepMultipleOf; + + let isValid: boolean; + if (totalTimeInSec < multipleOfVal) { + isValid = (multipleOfVal % totalTimeInSec === 0); + } else { + isValid = (totalTimeInSec % multipleOfVal === 0); + } + return isValid ? null : { stepMultipleOf: true }; + }; + } + + private updatedAllowTimeUnitInterval(maxTime: number) { + const maxTimeMs = maxTime * SECOND; + this.timeUnits = Object.values(TimeUnit).filter(item => item !== TimeUnit.MILLISECONDS) as TimeUnit[]; + if (maxTimeMs < MINUTE) { + this.timeUnits = this.timeUnits.filter(item => item !== TimeUnit.MINUTES && item !== TimeUnit.HOURS && item !== TimeUnit.DAYS); + } else if (maxTimeMs < HOUR) { + this.timeUnits = this.timeUnits.filter(item => item !== TimeUnit.HOURS && item !== TimeUnit.DAYS); + } else if (maxTimeMs < DAY) { + this.timeUnits = this.timeUnits.filter(item => item !== TimeUnit.DAYS); + } + } + } diff --git a/ui-ngx/src/app/shared/components/time/datapoints-limit.component.ts b/ui-ngx/src/app/shared/components/time/datapoints-limit.component.ts index 02470cc949..e2799bd0fc 100644 --- a/ui-ngx/src/app/shared/components/time/datapoints-limit.component.ts +++ b/ui-ngx/src/app/shared/components/time/datapoints-limit.component.ts @@ -29,6 +29,7 @@ import { coerceBooleanProperty } from '@angular/cdk/coercion'; import { TimeService } from '@core/services/time.service'; import { takeUntil } from 'rxjs/operators'; import { Subject } from 'rxjs'; +import { isDefined } from '@core/utils'; @Component({ selector: 'tb-datapoints-limit', @@ -69,7 +70,11 @@ export class DatapointsLimitComponent implements ControlValueAccessor, Validator @Input() disabled: boolean; - private propagateChange = (v: any) => { }; + private propagateChangeValue: any; + + private propagateChange = (v: any) => { + this.propagateChangeValue = v; + }; private destroy$ = new Subject(); @@ -79,6 +84,9 @@ export class DatapointsLimitComponent implements ControlValueAccessor, Validator registerOnChange(fn: any): void { this.propagateChange = fn; + if (isDefined(this.propagateChangeValue)) { + this.propagateChange(this.propagateChangeValue); + } } registerOnTouched(fn: any): void { @@ -115,19 +123,20 @@ export class DatapointsLimitComponent implements ControlValueAccessor, Validator } } - private checkLimit(limit?: number): number { - if (!limit || limit < this.minDatapointsLimit()) { - return this.minDatapointsLimit(); + writeValue(value: number | null): void { + this.modelValue = value; + let limit = this.modelValue; + if (!limit) { + limit = Math.ceil(this.maxDatapointsLimit() / 2); + } else if (limit < this.minDatapointsLimit()) { + limit = this.minDatapointsLimit(); } else if (limit > this.maxDatapointsLimit()) { - return this.maxDatapointsLimit(); + limit = this.maxDatapointsLimit(); } - return limit; - } - writeValue(value: number | null): void { - this.modelValue = this.checkLimit(value); + this.updateView(limit); this.datapointsLimitFormGroup.patchValue( - { limit: this.modelValue }, {emitEvent: false} + { limit: limit }, {emitEvent: false} ); } diff --git a/ui-ngx/src/app/shared/components/time/timewindow-config-dialog.component.ts b/ui-ngx/src/app/shared/components/time/timewindow-config-dialog.component.ts index 1e1f831c4f..af3d51de69 100644 --- a/ui-ngx/src/app/shared/components/time/timewindow-config-dialog.component.ts +++ b/ui-ngx/src/app/shared/components/time/timewindow-config-dialog.component.ts @@ -36,7 +36,15 @@ import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { TimeService } from '@core/services/time.service'; -import { deepClone, isDefined, isDefinedAndNotNull, isObject, mergeDeep } from '@core/utils'; +import { + deepClean, + deepClone, + deleteFalseProperties, + isDefined, + isDefinedAndNotNull, + isEmpty, + mergeDeepIgnoreArray +} from '@core/utils'; import { ToggleHeaderOption } from '@shared/components/toggle-header.component'; import { TranslateService } from '@ngx-translate/core'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; @@ -152,73 +160,73 @@ export class TimewindowConfigDialogComponent extends PageComponent implements On this.timewindowForm = this.fb.group({ selectedTab: [isDefined(this.timewindow.selectedTab) ? this.timewindow.selectedTab : TimewindowType.REALTIME], realtime: this.fb.group({ - realtimeType: [ isDefined(realtime?.realtimeType) ? this.timewindow.realtime.realtimeType : RealtimeWindowType.LAST_INTERVAL ], - timewindowMs: [ isDefined(realtime?.timewindowMs) ? this.timewindow.realtime.timewindowMs : null ], - interval: [ isDefined(realtime?.interval) ? this.timewindow.realtime.interval : null ], - quickInterval: [ isDefined(realtime?.quickInterval) ? this.timewindow.realtime.quickInterval : null ], - disableCustomInterval: [ isDefinedAndNotNull(this.timewindow.realtime?.disableCustomInterval) - ? this.timewindow.realtime?.disableCustomInterval : false ], - disableCustomGroupInterval: [ isDefinedAndNotNull(this.timewindow.realtime?.disableCustomGroupInterval) - ? this.timewindow.realtime?.disableCustomGroupInterval : false ], - hideInterval: [ isDefinedAndNotNull(this.timewindow.realtime.hideInterval) - ? this.timewindow.realtime.hideInterval : false ], + realtimeType: [ isDefined(realtime?.realtimeType) ? realtime.realtimeType : RealtimeWindowType.LAST_INTERVAL ], + timewindowMs: [ isDefined(realtime?.timewindowMs) ? realtime.timewindowMs : null ], + interval: [ isDefined(realtime?.interval) ? realtime.interval : null ], + quickInterval: [ isDefined(realtime?.quickInterval) ? realtime.quickInterval : null ], + disableCustomInterval: [ isDefinedAndNotNull(realtime?.disableCustomInterval) + ? realtime.disableCustomInterval : false ], + disableCustomGroupInterval: [ isDefinedAndNotNull(realtime?.disableCustomGroupInterval) + ? realtime.disableCustomGroupInterval : false ], + hideInterval: [ isDefinedAndNotNull(realtime?.hideInterval) + ? realtime.hideInterval : false ], hideLastInterval: [{ - value: isDefinedAndNotNull(this.timewindow.realtime.hideLastInterval) - ? this.timewindow.realtime.hideLastInterval : false, - disabled: this.timewindow.realtime.hideInterval + value: isDefinedAndNotNull(realtime?.hideLastInterval) + ? realtime.hideLastInterval : false, + disabled: realtime?.hideInterval }], hideQuickInterval: [{ - value: isDefinedAndNotNull(this.timewindow.realtime.hideQuickInterval) - ? this.timewindow.realtime.hideQuickInterval : false, - disabled: this.timewindow.realtime.hideInterval + value: isDefinedAndNotNull(realtime?.hideQuickInterval) + ? realtime.hideQuickInterval : false, + disabled: realtime?.hideInterval }], advancedParams: this.fb.group({ - allowedLastIntervals: [ isDefinedAndNotNull(this.timewindow.realtime?.advancedParams?.allowedLastIntervals) - ? this.timewindow.realtime.advancedParams.allowedLastIntervals : null ], - allowedQuickIntervals: [ isDefinedAndNotNull(this.timewindow.realtime?.advancedParams?.allowedQuickIntervals) - ? this.timewindow.realtime.advancedParams.allowedQuickIntervals : null ], - lastAggIntervalsConfig: [ isDefinedAndNotNull(this.timewindow.realtime?.advancedParams?.lastAggIntervalsConfig) - ? this.timewindow.realtime.advancedParams.lastAggIntervalsConfig : null ], - quickAggIntervalsConfig: [ isDefinedAndNotNull(this.timewindow.realtime?.advancedParams?.quickAggIntervalsConfig) - ? this.timewindow.realtime.advancedParams.quickAggIntervalsConfig : null ] + allowedLastIntervals: [ isDefinedAndNotNull(realtime?.advancedParams?.allowedLastIntervals) + ? realtime.advancedParams.allowedLastIntervals : null ], + allowedQuickIntervals: [ isDefinedAndNotNull(realtime?.advancedParams?.allowedQuickIntervals) + ? realtime.advancedParams.allowedQuickIntervals : null ], + lastAggIntervalsConfig: [ isDefinedAndNotNull(realtime?.advancedParams?.lastAggIntervalsConfig) + ? realtime.advancedParams.lastAggIntervalsConfig : null ], + quickAggIntervalsConfig: [ isDefinedAndNotNull(realtime?.advancedParams?.quickAggIntervalsConfig) + ? realtime.advancedParams.quickAggIntervalsConfig : null ] }) }), history: this.fb.group({ - historyType: [ isDefined(history?.historyType) ? this.timewindow.history.historyType : HistoryWindowType.LAST_INTERVAL ], - timewindowMs: [ isDefined(history?.timewindowMs) ? this.timewindow.history.timewindowMs : null ], - interval: [ isDefined(history?.interval) ? this.timewindow.history.interval : null ], - fixedTimewindow: [ isDefined(history?.fixedTimewindow) ? this.timewindow.history.fixedTimewindow : null ], - quickInterval: [ isDefined(history?.quickInterval) ? this.timewindow.history.quickInterval : null ], - disableCustomInterval: [ isDefinedAndNotNull(this.timewindow.history?.disableCustomInterval) - ? this.timewindow.history?.disableCustomInterval : false ], - disableCustomGroupInterval: [ isDefinedAndNotNull(this.timewindow.history?.disableCustomGroupInterval) - ? this.timewindow.history?.disableCustomGroupInterval : false ], - hideInterval: [ isDefinedAndNotNull(this.timewindow.history.hideInterval) - ? this.timewindow.history.hideInterval : false ], + historyType: [ isDefined(history?.historyType) ? history.historyType : HistoryWindowType.LAST_INTERVAL ], + timewindowMs: [ isDefined(history?.timewindowMs) ? history.timewindowMs : null ], + interval: [ isDefined(history?.interval) ? history.interval : null ], + fixedTimewindow: [ isDefined(history?.fixedTimewindow) ? history.fixedTimewindow : null ], + quickInterval: [ isDefined(history?.quickInterval) ? history.quickInterval : null ], + disableCustomInterval: [ isDefinedAndNotNull(history?.disableCustomInterval) + ? history.disableCustomInterval : false ], + disableCustomGroupInterval: [ isDefinedAndNotNull(history?.disableCustomGroupInterval) + ? history.disableCustomGroupInterval : false ], + hideInterval: [ isDefinedAndNotNull(history?.hideInterval) + ? history.hideInterval : false ], hideLastInterval: [{ - value: isDefinedAndNotNull(this.timewindow.history.hideLastInterval) - ? this.timewindow.history.hideLastInterval : false, - disabled: this.timewindow.history.hideInterval + value: isDefinedAndNotNull(history?.hideLastInterval) + ? history.hideLastInterval : false, + disabled: history?.hideInterval }], hideQuickInterval: [{ - value: isDefinedAndNotNull(this.timewindow.history.hideQuickInterval) - ? this.timewindow.history.hideQuickInterval : false, - disabled: this.timewindow.history.hideInterval + value: isDefinedAndNotNull(history?.hideQuickInterval) + ? history.hideQuickInterval : false, + disabled: history?.hideInterval }], hideFixedInterval: [{ - value: isDefinedAndNotNull(this.timewindow.history.hideFixedInterval) - ? this.timewindow.history.hideFixedInterval : false, - disabled: this.timewindow.history.hideInterval + value: isDefinedAndNotNull(history?.hideFixedInterval) + ? history.hideFixedInterval : false, + disabled: history?.hideInterval }], advancedParams: this.fb.group({ - allowedLastIntervals: [ isDefinedAndNotNull(this.timewindow.history?.advancedParams?.allowedLastIntervals) - ? this.timewindow.history.advancedParams.allowedLastIntervals : null ], - allowedQuickIntervals: [ isDefinedAndNotNull(this.timewindow.history?.advancedParams?.allowedQuickIntervals) - ? this.timewindow.history.advancedParams.allowedQuickIntervals : null ], - lastAggIntervalsConfig: [ isDefinedAndNotNull(this.timewindow.history?.advancedParams?.lastAggIntervalsConfig) - ? this.timewindow.history.advancedParams.lastAggIntervalsConfig : null ], - quickAggIntervalsConfig: [ isDefinedAndNotNull(this.timewindow.history?.advancedParams?.quickAggIntervalsConfig) - ? this.timewindow.history.advancedParams.quickAggIntervalsConfig : null ] + allowedLastIntervals: [ isDefinedAndNotNull(history?.advancedParams?.allowedLastIntervals) + ? history.advancedParams.allowedLastIntervals : null ], + allowedQuickIntervals: [ isDefinedAndNotNull(history?.advancedParams?.allowedQuickIntervals) + ? history.advancedParams.allowedQuickIntervals : null ], + lastAggIntervalsConfig: [ isDefinedAndNotNull(history?.advancedParams?.lastAggIntervalsConfig) + ? history.advancedParams.lastAggIntervalsConfig : null ], + quickAggIntervalsConfig: [ isDefinedAndNotNull(history?.advancedParams?.quickAggIntervalsConfig) + ? history.advancedParams.quickAggIntervalsConfig : null ] }) }), aggregation: this.fb.group({ @@ -411,9 +419,10 @@ export class TimewindowConfigDialogComponent extends PageComponent implements On const timewindowFormValue = this.timewindowForm.getRawValue(); const realtimeDisableCustomInterval = timewindowFormValue.realtime.disableCustomInterval; const historyDisableCustomInterval = timewindowFormValue.history.disableCustomInterval; - updateFormValuesOnTimewindowTypeChange(selectedTab, this.quickIntervalOnly, this.timewindowForm, + updateFormValuesOnTimewindowTypeChange(selectedTab, this.timewindowForm, realtimeDisableCustomInterval, historyDisableCustomInterval, - timewindowFormValue.realtime.advancedParams, timewindowFormValue.history.advancedParams); + timewindowFormValue.realtime.advancedParams, timewindowFormValue.history.advancedParams, + this.realtimeTimewindowOptions, this.historyTimewindowOptions); this.timewindowForm.patchValue({ hideAggregation: timewindowFormValue.hideAggregation, hideAggInterval: timewindowFormValue.hideAggInterval, @@ -423,7 +432,7 @@ export class TimewindowConfigDialogComponent extends PageComponent implements On update() { const timewindowFormValue = this.timewindowForm.getRawValue(); - this.timewindow = mergeDeep(this.timewindow, timewindowFormValue); + this.timewindow = mergeDeepIgnoreArray(this.timewindow, timewindowFormValue); const realtimeConfigurableLastIntervalsAvailable = !(timewindowFormValue.hideAggInterval && (timewindowFormValue.realtime.hideInterval || timewindowFormValue.realtime.hideLastInterval)); @@ -434,82 +443,50 @@ export class TimewindowConfigDialogComponent extends PageComponent implements On const historyConfigurableQuickIntervalsAvailable = !(timewindowFormValue.hideAggInterval && (timewindowFormValue.history.hideInterval || timewindowFormValue.history.hideQuickInterval)); - if (realtimeConfigurableLastIntervalsAvailable && timewindowFormValue.realtime.advancedParams.allowedLastIntervals?.length) { - this.timewindow.realtime.advancedParams.allowedLastIntervals = timewindowFormValue.realtime.advancedParams.allowedLastIntervals; - } else { + if (!realtimeConfigurableLastIntervalsAvailable) { delete this.timewindow.realtime.advancedParams.allowedLastIntervals; } - if (realtimeConfigurableQuickIntervalsAvailable && timewindowFormValue.realtime.advancedParams.allowedQuickIntervals?.length) { - this.timewindow.realtime.advancedParams.allowedQuickIntervals = timewindowFormValue.realtime.advancedParams.allowedQuickIntervals; - } else { + if (!realtimeConfigurableQuickIntervalsAvailable) { delete this.timewindow.realtime.advancedParams.allowedQuickIntervals; } - if (realtimeConfigurableLastIntervalsAvailable && isObject(timewindowFormValue.realtime.advancedParams.lastAggIntervalsConfig) && - Object.keys(timewindowFormValue.realtime.advancedParams.lastAggIntervalsConfig).length) { + if (realtimeConfigurableLastIntervalsAvailable && !isEmpty(timewindowFormValue.realtime.advancedParams.lastAggIntervalsConfig)) { this.timewindow.realtime.advancedParams.lastAggIntervalsConfig = timewindowFormValue.realtime.advancedParams.lastAggIntervalsConfig; } else { delete this.timewindow.realtime.advancedParams.lastAggIntervalsConfig; } - if (realtimeConfigurableQuickIntervalsAvailable && isObject(timewindowFormValue.realtime.advancedParams.quickAggIntervalsConfig) && - Object.keys(timewindowFormValue.realtime.advancedParams.quickAggIntervalsConfig).length) { + if (realtimeConfigurableQuickIntervalsAvailable && !isEmpty(timewindowFormValue.realtime.advancedParams.quickAggIntervalsConfig)) { this.timewindow.realtime.advancedParams.quickAggIntervalsConfig = timewindowFormValue.realtime.advancedParams.quickAggIntervalsConfig; } else { delete this.timewindow.realtime.advancedParams.quickAggIntervalsConfig; } - if (historyConfigurableLastIntervalsAvailable && timewindowFormValue.history.advancedParams.allowedLastIntervals?.length) { - this.timewindow.history.advancedParams.allowedLastIntervals = timewindowFormValue.history.advancedParams.allowedLastIntervals; - } else { + if (!historyConfigurableLastIntervalsAvailable) { delete this.timewindow.history.advancedParams.allowedLastIntervals; } - if (historyConfigurableQuickIntervalsAvailable && timewindowFormValue.history.advancedParams.allowedQuickIntervals?.length) { - this.timewindow.history.advancedParams.allowedQuickIntervals = timewindowFormValue.history.advancedParams.allowedQuickIntervals; - } else { + if (!historyConfigurableQuickIntervalsAvailable) { delete this.timewindow.history.advancedParams.allowedQuickIntervals; } - if (historyConfigurableLastIntervalsAvailable && isObject(timewindowFormValue.history.advancedParams.lastAggIntervalsConfig) && - Object.keys(timewindowFormValue.history.advancedParams.lastAggIntervalsConfig).length) { + if (historyConfigurableLastIntervalsAvailable && !isEmpty(timewindowFormValue.history.advancedParams.lastAggIntervalsConfig)) { this.timewindow.history.advancedParams.lastAggIntervalsConfig = timewindowFormValue.history.advancedParams.lastAggIntervalsConfig; } else { delete this.timewindow.history.advancedParams.lastAggIntervalsConfig; } - if (historyConfigurableQuickIntervalsAvailable && isObject(timewindowFormValue.history.advancedParams.quickAggIntervalsConfig) && - Object.keys(timewindowFormValue.history.advancedParams.quickAggIntervalsConfig).length) { + if (historyConfigurableQuickIntervalsAvailable && !isEmpty(timewindowFormValue.history.advancedParams.quickAggIntervalsConfig)) { this.timewindow.history.advancedParams.quickAggIntervalsConfig = timewindowFormValue.history.advancedParams.quickAggIntervalsConfig; } else { delete this.timewindow.history.advancedParams.quickAggIntervalsConfig; } - if (!Object.keys(this.timewindow.realtime.advancedParams).length) { - delete this.timewindow.realtime.advancedParams; - } - if (!Object.keys(this.timewindow.history.advancedParams).length) { - delete this.timewindow.history.advancedParams; - } - - if (timewindowFormValue.allowedAggTypes?.length && !timewindowFormValue.hideAggregation) { - this.timewindow.allowedAggTypes = timewindowFormValue.allowedAggTypes; - } else { + if (timewindowFormValue.hideAggregation) { delete this.timewindow.allowedAggTypes; } - if (!this.timewindow.realtime.disableCustomInterval) { - delete this.timewindow.realtime.disableCustomInterval; - } - if (!this.timewindow.realtime.disableCustomGroupInterval) { - delete this.timewindow.realtime.disableCustomGroupInterval; - } - if (!this.timewindow.history.disableCustomInterval) { - delete this.timewindow.history.disableCustomInterval; - } - if (!this.timewindow.history.disableCustomGroupInterval) { - delete this.timewindow.history.disableCustomGroupInterval; - } - if (!this.aggregation) { delete this.timewindow.aggregation; } - this.dialogRef.close(this.timewindow); + + deleteFalseProperties(this.timewindow); + this.dialogRef.close(deepClean(this.timewindow)); } cancel() { diff --git a/ui-ngx/src/app/shared/components/time/timewindow-panel.component.ts b/ui-ngx/src/app/shared/components/time/timewindow-panel.component.ts index d7a0d44013..c306ce4a49 100644 --- a/ui-ngx/src/app/shared/components/time/timewindow-panel.component.ts +++ b/ui-ngx/src/app/shared/components/time/timewindow-panel.component.ts @@ -27,12 +27,14 @@ import { } from '@angular/core'; import { AggregationType, + clearTimewindowConfig, currentHistoryTimewindow, currentRealtimeTimewindow, historyAllowedAggIntervals, HistoryWindowType, historyWindowTypeTranslations, Interval, + MINUTE, QuickTimeInterval, realtimeAllowedAggIntervals, RealtimeWindowType, @@ -167,14 +169,14 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit, O }); } - if ((this.isEdit || !this.timewindow.realtime.hideLastInterval) && !this.quickIntervalOnly) { + if ((this.isEdit || !this.timewindow.realtime?.hideLastInterval) && !this.quickIntervalOnly) { this.realtimeTimewindowOptions.push({ name: this.translate.instant(realtimeWindowTypeTranslations.get(RealtimeWindowType.LAST_INTERVAL)), value: this.realtimeTypes.LAST_INTERVAL }); } - if (this.isEdit || !this.timewindow.realtime.hideQuickInterval || this.quickIntervalOnly) { + if (this.isEdit || !this.timewindow.realtime?.hideQuickInterval || this.quickIntervalOnly) { this.realtimeTimewindowOptions.push({ name: this.translate.instant(realtimeWindowTypeTranslations.get(RealtimeWindowType.INTERVAL)), value: this.realtimeTypes.INTERVAL @@ -188,21 +190,21 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit, O }); } - if (this.isEdit || !this.timewindow.history.hideLastInterval) { + if (this.isEdit || !this.timewindow.history?.hideLastInterval) { this.historyTimewindowOptions.push({ name: this.translate.instant(historyWindowTypeTranslations.get(HistoryWindowType.LAST_INTERVAL)), value: this.historyTypes.LAST_INTERVAL }); } - if (this.isEdit || !this.timewindow.history.hideFixedInterval) { + if (this.isEdit || !this.timewindow.history?.hideFixedInterval) { this.historyTimewindowOptions.push({ name: this.translate.instant(historyWindowTypeTranslations.get(HistoryWindowType.FIXED)), value: this.historyTypes.FIXED }); } - if (this.isEdit || !this.timewindow.history.hideQuickInterval) { + if (this.isEdit || !this.timewindow.history?.hideQuickInterval) { this.historyTimewindowOptions.push({ name: this.translate.instant(historyWindowTypeTranslations.get(HistoryWindowType.INTERVAL)), value: this.historyTypes.INTERVAL @@ -211,10 +213,10 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit, O this.realtimeTypeSelectionAvailable = this.realtimeTimewindowOptions.length > 1; this.historyTypeSelectionAvailable = this.historyTimewindowOptions.length > 1; - this.realtimeIntervalSelectionAvailable = this.isEdit || !(this.timewindow.realtime.hideInterval || - (this.timewindow.realtime.hideLastInterval && this.timewindow.realtime.hideQuickInterval)); - this.historyIntervalSelectionAvailable = this.isEdit || !(this.timewindow.history.hideInterval || - (this.timewindow.history.hideLastInterval && this.timewindow.history.hideQuickInterval && this.timewindow.history.hideFixedInterval)); + this.realtimeIntervalSelectionAvailable = this.isEdit || !(this.timewindow.realtime?.hideInterval || + (this.timewindow.realtime?.hideLastInterval && this.timewindow.realtime?.hideQuickInterval)); + this.historyIntervalSelectionAvailable = this.isEdit || !(this.timewindow.history?.hideInterval || + (this.timewindow.history?.hideLastInterval && this.timewindow.history?.hideQuickInterval && this.timewindow.history?.hideFixedInterval)); this.aggregationOptionsAvailable = this.aggregation && (this.isEdit || !(this.timewindow.hideAggregation && this.timewindow.hideAggInterval)); @@ -230,28 +232,28 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit, O const aggregation = this.timewindow.aggregation; if (!this.isEdit) { - if (realtime.hideLastInterval && !realtime.hideQuickInterval) { + if (realtime?.hideLastInterval && !realtime?.hideQuickInterval) { realtime.realtimeType = RealtimeWindowType.INTERVAL; } - if (realtime.hideQuickInterval && !realtime.hideLastInterval) { + if (realtime?.hideQuickInterval && !realtime?.hideLastInterval) { realtime.realtimeType = RealtimeWindowType.LAST_INTERVAL; } - if (history.hideLastInterval) { + if (history?.hideLastInterval) { if (!history.hideFixedInterval) { history.historyType = HistoryWindowType.FIXED; } else if (!history.hideQuickInterval) { history.historyType = HistoryWindowType.INTERVAL; } } - if (history.hideFixedInterval) { + if (history?.hideFixedInterval) { if (!history.hideLastInterval) { history.historyType = HistoryWindowType.LAST_INTERVAL; } else if (!history.hideQuickInterval) { history.historyType = HistoryWindowType.INTERVAL; } } - if (history.hideQuickInterval) { + if (history?.hideQuickInterval) { if (!history.hideLastInterval) { history.historyType = HistoryWindowType.LAST_INTERVAL; } else if (!history.hideFixedInterval) { @@ -265,29 +267,29 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit, O realtime: this.fb.group({ realtimeType: [{ value: isDefined(realtime?.realtimeType) ? realtime.realtimeType : RealtimeWindowType.LAST_INTERVAL, - disabled: realtime.hideInterval + disabled: realtime?.hideInterval }], timewindowMs: [{ - value: isDefined(realtime?.timewindowMs) ? realtime.timewindowMs : null, - disabled: realtime.hideInterval || realtime.hideLastInterval + value: isDefined(realtime?.timewindowMs) ? realtime.timewindowMs : MINUTE, + disabled: realtime?.hideInterval || realtime?.hideLastInterval }], interval: [{ value:isDefined(realtime?.interval) ? realtime.interval : null, disabled: hideAggInterval }], quickInterval: [{ - value: isDefined(realtime?.quickInterval) ? realtime.quickInterval : null, - disabled: realtime.hideInterval || realtime.hideQuickInterval + value: isDefined(realtime?.quickInterval) ? realtime.quickInterval : QuickTimeInterval.CURRENT_DAY, + disabled: realtime?.hideInterval || realtime?.hideQuickInterval }] }), history: this.fb.group({ historyType: [{ value: isDefined(history?.historyType) ? history.historyType : HistoryWindowType.LAST_INTERVAL, - disabled: history.hideInterval + disabled: history?.hideInterval }], timewindowMs: [{ - value: isDefined(history?.timewindowMs) ? history.timewindowMs : null, - disabled: history.hideInterval || history.hideLastInterval + value: isDefined(history?.timewindowMs) ? history.timewindowMs : MINUTE, + disabled: history?.hideInterval || history?.hideLastInterval }], interval: [{ value:isDefined(history?.interval) ? history.interval : null, @@ -296,11 +298,11 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit, O fixedTimewindow: [{ value: isDefined(history?.fixedTimewindow) && this.timewindow.selectedTab === TimewindowType.HISTORY && history.historyType === HistoryWindowType.FIXED ? history.fixedTimewindow : null, - disabled: history.hideInterval || history.hideFixedInterval + disabled: history?.hideInterval || history?.hideFixedInterval }], quickInterval: [{ - value: isDefined(history?.quickInterval) ? history.quickInterval : null, - disabled: history.hideInterval || history.hideQuickInterval + value: isDefined(history?.quickInterval) ? history.quickInterval : QuickTimeInterval.CURRENT_DAY, + disabled: history?.hideInterval || history?.hideQuickInterval }] }), aggregation: this.fb.group({ @@ -379,8 +381,7 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit, O this.timewindowForm.valueChanges.pipe( takeUntil(this.destroy$) ).subscribe(() => { - this.prepareTimewindowConfig(); - this.changeTimewindow.emit(this.timewindow); + this.changeTimewindow.emit(this.prepareTimewindowConfig()); }); } } @@ -400,33 +401,36 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit, O } private onTimewindowTypeChange(selectedTab: TimewindowType) { - updateFormValuesOnTimewindowTypeChange(selectedTab, this.quickIntervalOnly, this.timewindowForm, + updateFormValuesOnTimewindowTypeChange(selectedTab, this.timewindowForm, this.realtimeDisableCustomInterval, this.historyDisableCustomInterval, - this.realtimeAdvancedParams, this.historyAdvancedParams); + this.realtimeAdvancedParams, this.historyAdvancedParams, + this.realtimeTimewindowOptions, this.historyTimewindowOptions); } update() { - this.prepareTimewindowConfig(); - this.result = this.timewindow; + this.result = this.prepareTimewindowConfig(); this.overlayRef?.dispose(); } - private prepareTimewindowConfig() { + private prepareTimewindowConfig(clearConfig = true): Timewindow { const timewindowFormValue = this.timewindowForm.getRawValue(); this.timewindow.selectedTab = timewindowFormValue.selectedTab; - this.timewindow.realtime = {...this.timewindow.realtime, ...{ - realtimeType: timewindowFormValue.realtime.realtimeType, - timewindowMs: timewindowFormValue.realtime.timewindowMs, - quickInterval: timewindowFormValue.realtime.quickInterval, - interval: timewindowFormValue.realtime.interval - }}; - this.timewindow.history = {...this.timewindow.history, ...{ - historyType: timewindowFormValue.history.historyType, - timewindowMs: timewindowFormValue.history.timewindowMs, - interval: timewindowFormValue.history.interval, - fixedTimewindow: timewindowFormValue.history.fixedTimewindow, - quickInterval: timewindowFormValue.history.quickInterval, - }}; + if (this.timewindow.selectedTab === TimewindowType.REALTIME) { + this.timewindow.realtime = {...this.timewindow.realtime, ...{ + realtimeType: timewindowFormValue.realtime.realtimeType, + timewindowMs: timewindowFormValue.realtime.timewindowMs, + quickInterval: timewindowFormValue.realtime.quickInterval, + interval: timewindowFormValue.realtime.interval, + }}; + } else { + this.timewindow.history = {...this.timewindow.history, ...{ + historyType: timewindowFormValue.history.historyType, + timewindowMs: timewindowFormValue.history.timewindowMs, + fixedTimewindow: timewindowFormValue.history.fixedTimewindow, + quickInterval: timewindowFormValue.history.quickInterval, + interval: timewindowFormValue.history.interval, + }}; + } if (this.aggregation) { this.timewindow.aggregation = { @@ -437,6 +441,12 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit, O if (this.timezone) { this.timewindow.timezone = timewindowFormValue.timezone; } + + if (clearConfig) { + return clearTimewindowConfig(this.timewindow, this.quickIntervalOnly, this.historyOnly, this.aggregation, this.timezone); + } else { + return deepClone(this.timewindow); + } } private updateTimewindowForm() { @@ -546,7 +556,6 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit, O } openTimewindowConfig() { - this.prepareTimewindowConfig(); this.dialog.open( TimewindowConfigDialogComponent, { autoFocus: false, @@ -555,7 +564,7 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit, O data: { quickIntervalOnly: this.quickIntervalOnly, aggregation: this.aggregation, - timewindow: deepClone(this.timewindow) + timewindow: this.prepareTimewindowConfig(false) } }).afterClosed() .subscribe((res) => { @@ -568,12 +577,12 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit, O } private updateTimewindowAdvancedParams() { - this.realtimeDisableCustomInterval = this.timewindow.realtime.disableCustomInterval; - this.realtimeDisableCustomGroupInterval = this.timewindow.realtime.disableCustomGroupInterval; - this.historyDisableCustomInterval = this.timewindow.history.disableCustomInterval; - this.historyDisableCustomGroupInterval = this.timewindow.history.disableCustomGroupInterval; + this.realtimeDisableCustomInterval = this.timewindow.realtime?.disableCustomInterval; + this.realtimeDisableCustomGroupInterval = this.timewindow.realtime?.disableCustomGroupInterval; + this.historyDisableCustomInterval = this.timewindow.history?.disableCustomInterval; + this.historyDisableCustomGroupInterval = this.timewindow.history?.disableCustomGroupInterval; - if (this.timewindow.realtime.advancedParams) { + if (this.timewindow.realtime?.advancedParams) { this.realtimeAdvancedParams = this.timewindow.realtime.advancedParams; this.realtimeAllowedLastIntervals = this.timewindow.realtime.advancedParams.allowedLastIntervals; this.realtimeAllowedQuickIntervals = this.timewindow.realtime.advancedParams.allowedQuickIntervals; @@ -582,7 +591,7 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit, O this.realtimeAllowedLastIntervals = null; this.realtimeAllowedQuickIntervals = null; } - if (this.timewindow.history.advancedParams) { + if (this.timewindow.history?.advancedParams) { this.historyAdvancedParams = this.timewindow.history.advancedParams; this.historyAllowedLastIntervals = this.timewindow.history.advancedParams.allowedLastIntervals; this.historyAllowedQuickIntervals = this.timewindow.history.advancedParams.allowedQuickIntervals; diff --git a/ui-ngx/src/app/shared/components/time/timewindow.component.ts b/ui-ngx/src/app/shared/components/time/timewindow.component.ts index a52e3eb091..07f90db66e 100644 --- a/ui-ngx/src/app/shared/components/time/timewindow.component.ts +++ b/ui-ngx/src/app/shared/components/time/timewindow.component.ts @@ -319,7 +319,8 @@ export class TimewindowComponent implements ControlValueAccessor, OnInit, OnChan } writeValue(obj: Timewindow): void { - this.innerValue = initModelFromDefaultTimewindow(obj, this.quickIntervalOnly, this.historyOnly, this.timeService); + this.innerValue = initModelFromDefaultTimewindow(obj, this.quickIntervalOnly, this.historyOnly, this.timeService, + this.aggregation); this.timewindowDisabled = this.isTimewindowDisabled(); if (this.onHistoryOnlyChanged()) { setTimeout(() => { diff --git a/ui-ngx/src/app/shared/components/vc/branch-autocomplete.component.html b/ui-ngx/src/app/shared/components/vc/branch-autocomplete.component.html index 4515797a85..665b4d63ad 100644 --- a/ui-ngx/src/app/shared/components/vc/branch-autocomplete.component.html +++ b/ui-ngx/src/app/shared/components/vc/branch-autocomplete.component.html @@ -15,7 +15,10 @@ limitations under the License. --> - + {{ 'version-control.branch' | translate }} , private translate: TranslateService, private dashboardService: DashboardService, @@ -177,9 +175,7 @@ export class ImportExportService { public exportCalculatedField(calculatedFieldId: string): void { this.calculatedFieldsService.getCalculatedFieldById(calculatedFieldId).subscribe({ next: (calculatedField) => { - let name = calculatedField.name; - name = name.toLowerCase().replace(/\W/g, '_'); - this.exportToPc(this.prepareCalculatedFieldExport(calculatedField), name); + this.exportToPc(this.prepareCalculatedFieldExport(calculatedField), calculatedField.name, true); }, error: (e) => { this.handleExportError(e, 'calculated-fields.export-failed-error'); @@ -200,9 +196,7 @@ export class ImportExportService { this.updateUserSettingsIncludeResourcesIfNeeded(includeResources, result.include, 'includeResourcesInExportDashboard'); this.dashboardService.exportDashboard(dashboardId, result.include).subscribe({ next: (dashboard) => { - let name = dashboard.title; - name = name.toLowerCase().replace(/\W/g, '_'); - this.exportToPc(this.prepareDashboardExport(dashboard), name); + this.exportToPc(this.prepareDashboardExport(dashboard), dashboard.title, true); }, error: (e) => { this.handleExportError(e, 'dashboard.export-failed-error'); @@ -261,9 +255,8 @@ export class ImportExportService { widgetTitle: string, breakpoint: BreakpointId) { const widgetItem = this.itembuffer.prepareWidgetItem(dashboard, sourceState, sourceLayout, widget, breakpoint); const widgetDefaultName = this.widgetService.getWidgetInfoFromCache(widget.typeFullFqn).widgetName; - let fileName = widgetDefaultName + (isNotEmptyStr(widgetTitle) ? `_${widgetTitle}` : ''); - fileName = fileName.toLowerCase().replace(/\W/g, '_'); - this.exportToPc(this.prepareExport(widgetItem), fileName); + const fileName = widgetDefaultName + (isNotEmptyStr(widgetTitle) ? `_${widgetTitle}` : ''); + this.exportToPc(this.prepareExport(widgetItem), fileName, true); } public importWidget(dashboard: Dashboard, targetState: string, @@ -360,9 +353,7 @@ export class ImportExportService { this.updateUserSettingsIncludeResourcesIfNeeded(includeResources, result.include, 'includeResourcesInExportWidgetTypes'); this.widgetService.exportWidgetType(widgetTypeId, result.include).subscribe({ next: (widgetTypeDetails) => { - let name = widgetTypeDetails.name; - name = name.toLowerCase().replace(/\W/g, '_'); - this.exportToPc(this.prepareExport(widgetTypeDetails), name); + this.exportToPc(this.prepareExport(widgetTypeDetails), widgetTypeDetails.name, true); }, error: (e) => { this.handleExportError(e, 'widget-type.export-failed-error'); @@ -440,7 +431,7 @@ export class ImportExportService { public exportEntity(entityData: VersionedEntity): void { const id = (entityData as EntityInfoData).id ?? (entityData as RuleChainMetaData).ruleChainId; let fileName = (entityData as EntityInfoData).name; - let preparedData; + let preparedData: any; switch (id.entityType) { case EntityType.DEVICE_PROFILE: case EntityType.ASSET_PROFILE: @@ -511,9 +502,7 @@ export class ImportExportService { for (const widgetTypeDetails of widgetTypesDetails) { widgetsBundleItem.widgetTypes.push(this.prepareExport(widgetTypeDetails)); } - let name = widgetsBundle.title; - name = name.toLowerCase().replace(/\W/g, '_'); - this.exportToPc(widgetsBundleItem, name); + this.exportToPc(widgetsBundleItem, widgetsBundle.title, true); }, error: (e) => { this.handleExportError(e, 'widgets-bundle.export-failed-error'); @@ -528,9 +517,7 @@ export class ImportExportService { widgetsBundle: this.prepareExport(widgetsBundle), widgetTypeFqns }; - let name = widgetsBundle.title; - name = name.toLowerCase().replace(/\W/g, '_'); - this.exportToPc(widgetsBundleItem, name); + this.exportToPc(widgetsBundleItem, widgetsBundle.title, true); }, error: (e) => { this.handleExportError(e, 'widgets-bundle.export-failed-error'); @@ -662,11 +649,9 @@ export class ImportExportService { private onRuleChainExported() { return { next: (ruleChainExport: RuleChainImport) => { - let name = ruleChainExport.ruleChain.name; - name = name.toLowerCase().replace(/\W/g, '_'); - this.exportToPc(ruleChainExport, name); + this.exportToPc(ruleChainExport, ruleChainExport.ruleChain.name, true); }, - error: (e) => { + error: (e: any) => { this.handleExportError(e, 'rulechain.export-failed-error'); } }; @@ -747,9 +732,7 @@ export class ImportExportService { public exportDeviceProfile(deviceProfileId: string) { this.deviceProfileService.exportDeviceProfile(deviceProfileId).subscribe({ next: (deviceProfile) => { - let name = deviceProfile.name; - name = name.toLowerCase().replace(/\W/g, '_'); - this.exportToPc(this.prepareProfileExport(deviceProfile), name); + this.exportToPc(this.prepareProfileExport(deviceProfile), deviceProfile.name, true); }, error: (e) => { this.handleExportError(e, 'device-profile.export-failed-error'); @@ -776,9 +759,7 @@ export class ImportExportService { public exportAssetProfile(assetProfileId: string) { this.assetProfileService.exportAssetProfile(assetProfileId).subscribe({ next: (assetProfile) => { - let name = assetProfile.name; - name = name.toLowerCase().replace(/\W/g, '_'); - this.exportToPc(this.prepareProfileExport(assetProfile), name); + this.exportToPc(this.prepareProfileExport(assetProfile), assetProfile.name, true); }, error: (e) => { this.handleExportError(e, 'asset-profile.export-failed-error'); @@ -805,9 +786,7 @@ export class ImportExportService { public exportTenantProfile(tenantProfileId: string) { this.tenantProfileService.getTenantProfile(tenantProfileId).subscribe({ next: (tenantProfile) => { - let name = tenantProfile.name; - name = name.toLowerCase().replace(/\W/g, '_'); - this.exportToPc(this.prepareProfileExport(tenantProfile), name); + this.exportToPc(this.prepareProfileExport(tenantProfile), tenantProfile.name, true); }, error: (e) => { this.handleExportError(e, 'tenant-profile.export-failed-error'); @@ -842,7 +821,7 @@ export class ImportExportService { return cellData; } - public exportCsv(data: {[key: string]: any}[], filename: string) { + public exportCsv(data: {[key: string]: any}[], filename: string, normalizeFileName = false) { let colsHead: string; let colsData: string; if (data && data.length) { @@ -857,18 +836,18 @@ export class ImportExportService { colsData = ''; } const csvData = `${colsHead}\n${colsData}`; - this.downloadFile(csvData, filename, CSV_TYPE); + this.downloadFile(csvData, filename, CSV_TYPE, normalizeFileName); } - public exportText(data: string | Array, filename: string) { + public exportText(data: string | Array, filename: string, normalizeFileName = false) { let content = data; if (Array.isArray(data)) { content = data.join('\n'); } - this.downloadFile(content, filename, TEXT_TYPE); + this.downloadFile(content, filename, TEXT_TYPE, normalizeFileName); } - public exportJSZip(data: object, filename: string): Observable { + public exportJSZip(data: object, filename: string, normalizeFileName = false): Observable { const exportJsSubjectSubject = new Subject(); import('jszip').then((JSZip) => { try { @@ -880,9 +859,9 @@ export class ImportExportService { } } jsZip.generateAsync({type: 'blob'}).then(content => { - this.downloadFile(content, filename, ZIP_TYPE); + this.downloadFile(content, filename, ZIP_TYPE, normalizeFileName); exportJsSubjectSubject.next(null); - }).catch(e => { + }).catch((e: any) => { exportJsSubjectSubject.error(e); }); } catch (e) { @@ -1180,42 +1159,40 @@ export class ImportExportService { )); } - private exportToPc(data: any, filename: string) { + private exportToPc(data: any, filename: string, normalizeFileName = false) { if (!data) { console.error('No data'); return; } - this.exportJson(data, filename); + this.exportJson(data, filename, normalizeFileName); } - public exportJson(data: any, filename: string) { + public exportJson(data: any, filename: string, normalizeFileName = false) { if (isObject(data)) { data = JSON.stringify(data, null, 2); } - this.downloadFile(data, filename, JSON_TYPE); + this.downloadFile(data, filename, JSON_TYPE, normalizeFileName); } - private downloadFile(data: any, filename: string, fileType: FileType) { - if (!filename) { - filename = 'download'; + private prepareFilename(filename: string, extension: string, normalizeFileName = false): string { + if (normalizeFileName) { + filename = filename.toLowerCase().replace(/\s/g, '_'); } - filename += '.' + fileType.extension; + filename = filename.replace(/[\\/<>:"|?*\s]/g, '_'); + return `${filename}.${extension}`; + } + + private downloadFile(data: any, filename = 'download', fileType: FileType, normalizeFileName: boolean) { + filename = this.prepareFilename(filename, fileType.extension, normalizeFileName); const blob = new Blob([data], {type: fileType.mimeType}); - // @ts-ignore - if (this.window.navigator && this.window.navigator.msSaveOrOpenBlob) { - // @ts-ignore - this.window.navigator.msSaveOrOpenBlob(blob, filename); - } else { - const e = this.document.createEvent('MouseEvents'); - const a = this.document.createElement('a'); - a.download = filename; - a.href = URL.createObjectURL(blob); - a.dataset.downloadurl = [fileType.mimeType, a.download, a.href].join(':'); - // @ts-ignore - e.initEvent('click', true, false, this.window, - 0, 0, 0, 0, 0, false, false, false, false, 0, null); - a.dispatchEvent(e); - } + const url = URL.createObjectURL(blob); + + const a = this.document.createElement('a'); + a.href = url; + a.download = filename; + a.dataset.downloadurl = [fileType.mimeType, a.download, a.href].join(':'); + a.click(); + setTimeout(() => URL.revokeObjectURL(url), 0); } private prepareDashboardExport(dashboard: Dashboard): Dashboard { diff --git a/ui-ngx/src/app/shared/models/ai-model.models.ts b/ui-ngx/src/app/shared/models/ai-model.models.ts index f3161263b7..d3b70f9a74 100644 --- a/ui-ngx/src/app/shared/models/ai-model.models.ts +++ b/ui-ngx/src/app/shared/models/ai-model.models.ts @@ -34,6 +34,13 @@ export interface AiModel extends Omit, 'label'>, HasTenantId region?: string; accessKeyId?: string; secretAccessKey?: string; + baseUrl?: string; + auth?: { + type: AuthenticationType; + username?: string; + password?: string; + token?: string + } }; modelId: string; temperature?: number; @@ -42,6 +49,7 @@ export interface AiModel extends Omit, 'label'>, HasTenantId frequencyPenalty?: number; presencePenalty?: number; maxOutputTokens?: number; + contextLength?: number; } } @@ -57,7 +65,8 @@ export enum AiProvider { MISTRAL_AI = 'MISTRAL_AI', ANTHROPIC = 'ANTHROPIC', AMAZON_BEDROCK = 'AMAZON_BEDROCK', - GITHUB_MODELS = 'GITHUB_MODELS' + GITHUB_MODELS = 'GITHUB_MODELS', + OLLAMA = 'OLLAMA' } export const AiProviderTranslations = new Map( @@ -69,7 +78,8 @@ export const AiProviderTranslations = new Map( [AiProvider.MISTRAL_AI , 'ai-models.ai-providers.mistral-ai'], [AiProvider.ANTHROPIC , 'ai-models.ai-providers.anthropic'], [AiProvider.AMAZON_BEDROCK , 'ai-models.ai-providers.amazon-bedrock'], - [AiProvider.GITHUB_MODELS , 'ai-models.ai-providers.github-models'] + [AiProvider.GITHUB_MODELS , 'ai-models.ai-providers.github-models'], + [AiProvider.OLLAMA , 'ai-models.ai-providers.ollama'] ] ); @@ -84,10 +94,11 @@ export const ProviderFieldsAllList = [ 'serviceVersion', 'region', 'accessKeyId', - 'secretAccessKey' + 'secretAccessKey', + 'baseUrl' ]; -export const ModelFieldsAllList = ['temperature', 'topP', 'topK', 'frequencyPenalty', 'presencePenalty', 'maxOutputTokens']; +export const ModelFieldsAllList = ['temperature', 'topP', 'topK', 'frequencyPenalty', 'presencePenalty', 'maxOutputTokens', 'contextLength']; export const AiModelMap = new Map([ [ @@ -99,13 +110,16 @@ export const AiModelMap = new Map( + [ + [AlarmRuleScheduleType.ANY_TIME, 'alarm-rule.schedule.any-time'], + [AlarmRuleScheduleType.SPECIFIC_TIME, 'alarm-rule.schedule.specific-time'], + [AlarmRuleScheduleType.CUSTOM, 'alarm-rule.schedule.custom'] + ] +); + +export enum AlarmRuleConditionType { + SIMPLE = 'SIMPLE', + DURATION = 'DURATION', + REPEATING = 'REPEATING' +} + +export const AlarmRuleConditionTypeTranslationMap = new Map( + [ + [AlarmRuleConditionType.SIMPLE, 'alarm-rule.conditions.simple'], + [AlarmRuleConditionType.DURATION, 'alarm-rule.conditions.duration'], + [AlarmRuleConditionType.REPEATING, 'alarm-rule.conditions.repeating'] + ] +); + +export enum AlarmRuleExpressionType { + SIMPLE = 'SIMPLE', + TBEL = 'TBEL', +} + +export const FilterPredicateTypeTranslationMap = new Map( + [ + [FilterPredicateType.STRING, 'alarm-rule.filter-predicate-type.string'], + [FilterPredicateType.NUMERIC, 'alarm-rule.filter-predicate-type.numeric'], + [FilterPredicateType.BOOLEAN, 'alarm-rule.filter-predicate-type.boolean'], + [FilterPredicateType.COMPLEX, 'alarm-rule.filter-predicate-type.complex'] + ] +); + +export interface AlarmRule { + condition: AlarmRuleCondition; + alarmDetails?: string; + dashboardId?: DashboardId; +} + +export interface AlarmRuleCondition { + type: AlarmRuleConditionType; + expression: AlarmRuleExpression; + schedule?: AlarmRuleSchedule; + unit?: TimeUnit; + value?: AlarmRuleValue; + count?: AlarmRuleValue; +} + +export interface AlarmRuleExpression { + type: AlarmRuleExpressionType; + expression?: string; + filters?: Array; + operation?: ComplexOperation; +} + +export interface AlarmRuleSchedule { + staticValue?: { + type?: AlarmRuleScheduleType; + timezone?: string; + daysOfWeek?: number[]; + startsOn?: number; + endsOn?: number; + items?: CustomTimeSchedulerItem[]; + }; + dynamicValueArgument?: string; +} + +export interface AlarmRuleFilter { + argument: string; + valueType: EntityKeyValueType; + operation: ComplexOperation; + predicates: AlarmRuleFilterPredicate[]; +} + +export interface AlarmRulePredicateInfo { + keyFilterPredicate: AlarmRuleFilterPredicate; +} + +export type AlarmRuleFilterPredicate = StringAlarmRuleFilterPredicate | + NumericAlarmRuleFilterPredicate | + BooleanAlarmRuleFilterPredicate | + ComplexAlarmRuleFilterPredicate; + +export interface AlarmRuleValue { + dynamicValueArgument?: string; + staticValue?: T +} + +export interface StringAlarmRuleFilterPredicate { + type: FilterPredicateType.STRING; + operation: StringOperation; + value: AlarmRuleValue; + ignoreCase: boolean; +} + +export interface NumericAlarmRuleFilterPredicate { + type: FilterPredicateType.NUMERIC; + operation: NumericOperation; + value: AlarmRuleValue; +} + +export interface BooleanAlarmRuleFilterPredicate { + type: FilterPredicateType.BOOLEAN; + operation: BooleanOperation; + value: AlarmRuleValue; +} + +export interface BaseComplexFilterPredicate { + type: FilterPredicateType.COMPLEX; + operation: ComplexOperation; + predicates: Array; +} + +export type ComplexAlarmRuleFilterPredicate = BaseComplexFilterPredicate; diff --git a/ui-ngx/src/app/shared/models/alarm.models.ts b/ui-ngx/src/app/shared/models/alarm.models.ts index 61407e1248..590e866465 100644 --- a/ui-ngx/src/app/shared/models/alarm.models.ts +++ b/ui-ngx/src/app/shared/models/alarm.models.ts @@ -140,6 +140,7 @@ export interface AlarmCommentInfo extends AlarmComment { export interface AlarmInfo extends Alarm { originatorName: string; originatorLabel: string; + originatorDisplayName?: string; assignee: AlarmAssignee; } @@ -172,6 +173,7 @@ export const simulatedAlarm: AlarmInfo = { clearTs: 0, assignTs: 0, originatorName: 'Simulated', + originatorDisplayName: 'Simulated', originatorLabel: 'Simulated', assignee: { firstName: '', @@ -242,6 +244,11 @@ export const alarmFields: {[fieldName: string]: AlarmField} = { value: 'originatorName', name: 'alarm.originator' }, + originatorDisplayName: { + keyName: 'originatorDisplayName', + value: 'originatorDisplayName', + name: 'alarm.originator' + }, originatorLabel: { keyName: 'originatorLabel', value: 'originatorLabel', @@ -271,6 +278,11 @@ export const alarmFields: {[fieldName: string]: AlarmField} = { keyName: 'assignee', value: 'assignee', name: 'alarm.assignee' + }, + details: { + keyName: 'details', + value: 'details', + name: 'alarm.details' } }; diff --git a/ui-ngx/src/app/shared/models/authority.enum.ts b/ui-ngx/src/app/shared/models/authority.enum.ts index 8b18beb887..d91ecf777a 100644 --- a/ui-ngx/src/app/shared/models/authority.enum.ts +++ b/ui-ngx/src/app/shared/models/authority.enum.ts @@ -20,5 +20,6 @@ export enum Authority { CUSTOMER_USER = 'CUSTOMER_USER', REFRESH_TOKEN = 'REFRESH_TOKEN', ANONYMOUS = 'ANONYMOUS', - PRE_VERIFICATION_TOKEN = 'PRE_VERIFICATION_TOKEN' + PRE_VERIFICATION_TOKEN = 'PRE_VERIFICATION_TOKEN', + MFA_CONFIGURATION_TOKEN = 'MFA_CONFIGURATION_TOKEN' } diff --git a/ui-ngx/src/app/shared/models/base-data.ts b/ui-ngx/src/app/shared/models/base-data.ts index 972fa0b5a0..d9dc4b070d 100644 --- a/ui-ngx/src/app/shared/models/base-data.ts +++ b/ui-ngx/src/app/shared/models/base-data.ts @@ -16,7 +16,9 @@ import { EntityId } from '@shared/models/id/entity-id'; import { HasUUID } from '@shared/models/id/has-uuid'; -import { isDefinedAndNotNull } from '@core/utils'; +import { isDefinedAndNotNull, isNotEmptyStr } from '@core/utils'; +import { EntityType } from '@shared/models/entity-type.models'; +import { User } from '@shared/models/user.model'; export declare type HasId = EntityId | HasUUID; @@ -49,3 +51,12 @@ export function hasIdEquals(id1: HasId, id2: HasId): boolean { return id1 === id2; } } + +export function getEntityDisplayName(entity: BaseData): string { + if (entity?.id?.entityType === EntityType.USER) { + const user = entity as User; + const userName = (user?.firstName ?? '') + " " + (user?.lastName ?? ''); + return isNotEmptyStr(userName) ? userName.trim() : entity?.name; + } + return isNotEmptyStr(entity?.label) ? entity.label : entity?.name; +} diff --git a/ui-ngx/src/app/shared/models/calculated-field.models.ts b/ui-ngx/src/app/shared/models/calculated-field.models.ts index 76b775b2fd..d667d48f0b 100644 --- a/ui-ngx/src/app/shared/models/calculated-field.models.ts +++ b/ui-ngx/src/app/shared/models/calculated-field.models.ts @@ -14,11 +14,7 @@ /// limitations under the License. /// -import { - HasEntityDebugSettings, - HasTenantId, - HasVersion -} from '@shared/models/entity.models'; +import { HasEntityDebugSettings, HasTenantId, HasVersion } from '@shared/models/entity.models'; import { BaseData, ExportableEntity } from '@shared/models/base-data'; import { CalculatedFieldId } from '@shared/models/id/calculated-field-id'; import { EntityId } from '@shared/models/id/entity-id'; @@ -33,36 +29,191 @@ import { dotOperatorHighlightRule, endGroupHighlightRule } from '@shared/models/ace/ace.models'; +import { EntitySearchDirection } from '@shared/models/relation.models'; +import { AbstractControl, FormControl, ValidationErrors, ValidatorFn } from '@angular/forms'; +import { AlarmRule } from "@shared/models/alarm-rule.models"; +import { AlarmSeverity } from "@shared/models/alarm.models"; + +export const FORBIDDEN_NAMES = ['ctx', 'e', 'pi']; -export interface CalculatedField extends Omit, 'label'>, HasVersion, HasEntityDebugSettings, HasTenantId, ExportableEntity { - configuration: CalculatedFieldConfiguration; - type: CalculatedFieldType; +interface BaseCalculatedField extends Omit, 'label'>, HasVersion, HasEntityDebugSettings, HasTenantId, ExportableEntity { entityId: EntityId; } +export interface CalculatedFieldSimple extends BaseCalculatedField { + type: CalculatedFieldType.SIMPLE; + configuration: CalculatedFieldSimpleConfiguration; +} + +export interface CalculatedFieldScript extends BaseCalculatedField { + type: CalculatedFieldType.SCRIPT; + configuration: CalculatedFieldScriptConfiguration; +} + +export interface CalculatedFieldGeofencing extends BaseCalculatedField { + type: CalculatedFieldType.GEOFENCING; + configuration: CalculatedFieldGeofencingConfiguration; +} + +export interface CalculatedFieldPropagation extends BaseCalculatedField { + type: CalculatedFieldType.PROPAGATION; + configuration: CalculatedFieldPropagationConfiguration; +} + +export interface CalculatedFieldAlarmRule extends BaseCalculatedField { + type: CalculatedFieldType.ALARM; + configuration: CalculatedFieldAlarmRuleConfiguration; +} + +export type CalculatedField = + | CalculatedFieldSimple + | CalculatedFieldScript + | CalculatedFieldGeofencing + | CalculatedFieldPropagation + | CalculatedFieldAlarmRule; + export enum CalculatedFieldType { SIMPLE = 'SIMPLE', SCRIPT = 'SCRIPT', + GEOFENCING = 'GEOFENCING', + PROPAGATION = 'PROPAGATION', + RELATED_ENTITIES_AGGREGATION = 'RELATED_ENTITIES_AGGREGATION', + ENTITY_AGGREGATION = 'ENTITY_AGGREGATION', + ALARM = 'ALARM', +} + +interface CalculatedFieldTypeTranslate { + name: string; + hint?: string; } -export const CalculatedFieldTypeTranslations = new Map( +export const CalculatedFieldTypeTranslations = new Map( [ - [CalculatedFieldType.SIMPLE, 'calculated-fields.type.simple'], - [CalculatedFieldType.SCRIPT, 'calculated-fields.type.script'], + [CalculatedFieldType.SIMPLE, { + name: 'calculated-fields.type.simple' + }], + [CalculatedFieldType.SCRIPT, { + name: 'calculated-fields.type.script' + }], + [CalculatedFieldType.GEOFENCING, { + name: 'calculated-fields.type.geofencing' + }], + [CalculatedFieldType.PROPAGATION, { + name: 'calculated-fields.type.propagation' + }], + [CalculatedFieldType.RELATED_ENTITIES_AGGREGATION, { + name: 'calculated-fields.type.related-entities-aggregation', + hint: 'calculated-fields.type.related-entities-aggregation-hint' + }], + [CalculatedFieldType.ENTITY_AGGREGATION, { + name: 'calculated-fields.type.time-series-data-aggregation', + hint: 'calculated-fields.type.time-series-data-aggregation-hint', + }], ] ) -export interface CalculatedFieldConfiguration { - type: CalculatedFieldType; +export type CalculatedFieldConfiguration = + | CalculatedFieldSimpleConfiguration + | CalculatedFieldScriptConfiguration + | CalculatedFieldGeofencingConfiguration + | CalculatedFieldPropagationConfiguration + | CalculatedFieldRelatedAggregationConfiguration + | CalculatedFieldEntityAggregationConfiguration + | CalculatedFieldAlarmRuleConfiguration; + +export interface CalculatedFieldSimpleConfiguration { + type: CalculatedFieldType.SIMPLE; + expression: string; + arguments: Record; + useLatestTs: boolean; + output: CalculatedFieldSimpleOutput; +} + +export interface CalculatedFieldScriptConfiguration { + type: CalculatedFieldType.SCRIPT; expression: string; arguments: Record; output: CalculatedFieldOutput; } -export interface CalculatedFieldOutput { - type: OutputType; +export interface CalculatedFieldGeofencingConfiguration { + type: CalculatedFieldType.GEOFENCING; + zoneGroups: Record; + scheduledUpdateEnabled: boolean; + scheduledUpdateInterval?: number; + output: CalculatedFieldOutput; +} + +export interface CalculatedFieldRelatedAggregationConfiguration { + type: CalculatedFieldType.RELATED_ENTITIES_AGGREGATION; + relation: RelationPathLevel; + arguments: Record; + metrics: Record; + deduplicationIntervalInSec: number; + scheduledUpdateInterval?: number; + useLatestTs: boolean; + output: CalculatedFieldOutput & { decimalsByDefault?: number; }; +} + +export interface CalculatedFieldEntityAggregationConfiguration { + type: CalculatedFieldType.ENTITY_AGGREGATION; + arguments: Record; + metrics: Record; + interval: AggInterval; + watermark?: WatermarkConfig; + output: CalculatedFieldOutput & { decimalsByDefault?: number; }; +} + +export interface WatermarkConfig { + duration: number; +} + +interface BasePropagationConfiguration { + type: CalculatedFieldType.PROPAGATION; + relation: RelationPathLevel; + arguments: Record; + output: CalculatedFieldOutput; +} + +interface CalculatedFieldAlarmRuleConfiguration { + type: CalculatedFieldType.ALARM; + arguments: Record; + createRules: Record; + clearRule?: AlarmRule; + propagate: boolean; + propagateToOwner: boolean; + propagateToTenant: boolean; + propagateRelationTypes?: Array; +} + +export interface PropagationWithNoExpression extends BasePropagationConfiguration { + applyExpressionToResolvedArguments: false; +} + +export interface PropagationWithExpression extends BasePropagationConfiguration { + applyExpressionToResolvedArguments: true; + expression: string; +} + +export type CalculatedFieldPropagationConfiguration = + | PropagationWithNoExpression + | PropagationWithExpression; + +export type CalculatedFieldOutput = + | CalculatedFieldOutputAttribute + | CalculatedFieldOutputTimeSeries; + +export interface CalculatedFieldOutputAttribute { + type: OutputType.Attribute, + scope: AttributeScope; +} + +export interface CalculatedFieldOutputTimeSeries { + type: OutputType.Timeseries; +} + +export type CalculatedFieldSimpleOutput = CalculatedFieldOutput & { name: string; - scope?: AttributeScope; decimalsByDefault?: number; } @@ -72,6 +223,8 @@ export enum ArgumentEntityType { Asset = 'ASSET', Customer = 'CUSTOMER', Tenant = 'TENANT', + Owner = 'CURRENT_OWNER', + RelationQuery = 'RELATION_PATH_QUERY', } export const ArgumentEntityTypeTranslations = new Map( @@ -81,6 +234,43 @@ export const ArgumentEntityTypeTranslations = new Map( + [ + [GeofencingReportStrategy.REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS, 'calculated-fields.report-transition-event-and-presence'], + [GeofencingReportStrategy.REPORT_TRANSITION_EVENTS_ONLY, 'calculated-fields.report-transition-event-only'], + [GeofencingReportStrategy.REPORT_PRESENCE_STATUS_ONLY, 'calculated-fields.report-presence-status-only'] + ] +) + +export const GeofencingDirectionTranslations = new Map( + [ + [EntitySearchDirection.FROM, 'calculated-fields.direction-from'], + [EntitySearchDirection.TO, 'calculated-fields.direction-to'], + ] +) + +export const GeofencingDirectionLevelTranslations = new Map( + [ + [EntitySearchDirection.FROM, 'calculated-fields.direction-down'], + [EntitySearchDirection.TO, 'calculated-fields.direction-up'], + ] +) + +export const PropagationDirectionTranslations = new Map( + [ + [EntitySearchDirection.FROM, 'calculated-fields.direction-down-child'], + [EntitySearchDirection.TO, 'calculated-fields.direction-up-parent'], ] ) @@ -127,10 +317,115 @@ export interface CalculatedFieldArgument { refEntityKey: RefEntityKey; defaultValue?: string; refEntityId?: RefEntityId; + refDynamicSourceConfiguration?: RefDynamicSourceConfiguration; limit?: number; timeWindow?: number; } +export interface RefDynamicSourceConfiguration { + type: ArgumentEntityType.Owner; +} + +export enum AggFunction { + AVG='AVG', + MIN='MIN', + MAX='MAX', + SUM='SUM', + COUNT='COUNT', + COUNT_UNIQUE='COUNT_UNIQUE' +} + +export const AggFunctionTranslations = new Map([ + [AggFunction.AVG, 'calculated-fields.metrics.aggregation-type.avg'], + [AggFunction.MIN, 'calculated-fields.metrics.aggregation-type.min'], + [AggFunction.MAX, 'calculated-fields.metrics.aggregation-type.max'], + [AggFunction.SUM, 'calculated-fields.metrics.aggregation-type.sum'], + [AggFunction.COUNT, 'calculated-fields.metrics.aggregation-type.count'], + [AggFunction.COUNT_UNIQUE, 'calculated-fields.metrics.aggregation-type.count-unique'], +]) + +export enum AggIntervalType { + HOUR = 'HOUR', + DAY = 'DAY', + WEEK = 'WEEK', + WEEK_SUN_SAT = 'WEEK_SUN_SAT', + MONTH = 'MONTH', + QUARTER = 'QUARTER', + YEAR = 'YEAR', + CUSTOM = 'CUSTOM' +} + +export const AggIntervalTypeTranslations = new Map( + [ + [AggIntervalType.HOUR, 'calculated-fields.aggregate-period.hour'], + [AggIntervalType.DAY, 'calculated-fields.aggregate-period.day'], + [AggIntervalType.WEEK, 'calculated-fields.aggregate-period.week'], + [AggIntervalType.WEEK_SUN_SAT, 'calculated-fields.aggregate-period.week-sun-sat'], + [AggIntervalType.MONTH, 'calculated-fields.aggregate-period.month'], + [AggIntervalType.QUARTER, 'calculated-fields.aggregate-period.quarter'], + [AggIntervalType.YEAR, 'calculated-fields.aggregate-period.year'], + [AggIntervalType.CUSTOM, 'calculated-fields.aggregate-period.custom'] + ] +); + +export interface AggInterval { + type: AggIntervalType; + tz: string; + offsetSec?: number + durationSec?: number +} + +export interface CalculatedFieldAggMetric { + function: AggFunction; + filter?: string; + input: AggKeyInput | AggFunctionInput; + defaultValue?: number; +} + +export interface CalculatedFieldAggMetricValue extends CalculatedFieldAggMetric { + name: string; +} + +export enum AggInputType { + key = 'key', + function = 'function' +} + +export const AggInputTypeTranslations = new Map([ + [AggInputType.key, 'calculated-fields.metrics.value-source-type.key'], + [AggInputType.function, 'calculated-fields.metrics.value-source-type.function'], +]) + +export interface AggKeyInput { + type: AggInputType.key; + key: string; +} + +export interface AggFunctionInput { + type: AggInputType.function; + function: string; +} + +export interface CalculatedFieldGeofencing { + perimeterKeyName: string; + reportStrategy: GeofencingReportStrategy; + refEntityId?: RefEntityId; + refDynamicSourceConfiguration: RefDynamicSourceGeofencingConfiguration; + createRelationsWithMatchedZones: boolean; + relationType: string; + direction: EntitySearchDirection; +} + +export interface RefDynamicSourceGeofencingConfiguration { + type: ArgumentEntityType.RelationQuery | ArgumentEntityType.Owner; + levels?: Array; +} + +export interface CalculatedFieldGeofencingValue extends CalculatedFieldGeofencing { + name: string; + entityName?: string; +} + export interface RefEntityKey { key: string; type: ArgumentType; @@ -190,6 +485,11 @@ export interface CalculatedFieldArgumentValueBase { type: ArgumentType; } +export interface RelationPathLevel { + direction: EntitySearchDirection; + relationType: string; +} + export interface CalculatedFieldAttributeArgumentValue extends CalculatedFieldArgumentValueBase { ts: number; value: ValueType; @@ -654,3 +954,36 @@ export const calculatedFieldDefaultScript = 'return {\n' + ' "temperatureC": (temperatureF - 32) / 1.8\n' + '};' + +export function notEmptyObjectValidator(): ValidatorFn { + return (control: AbstractControl): ValidationErrors | null => { + const value = control.value; + if (typeof value === 'object' && value !== null && Object.keys(value).length === 0) { + return {emptyObject: true}; + } + return null; + }; +} + +export function forbiddenNamesValidator(forbiddenNames: string[]): ValidatorFn { + const forbiddenNameSet = new Set(forbiddenNames); + + return (control: FormControl) => { + const trimmedValue = (control.value || '').trim(); + return forbiddenNameSet.has(trimmedValue) ? { forbiddenName: true } : null; + }; +} + +export function uniqueNameValidator(existingNames: string[]): ValidatorFn { + const namesSet = new Set((existingNames || []).map(name => name.toLowerCase())); + + return (control: FormControl) => { + const newName = (control.value || '').trim().toLowerCase(); + + if (!newName) { + return null; + } + + return namesSet.has(newName) ? { duplicateName: true } : null; + }; +} diff --git a/ui-ngx/src/app/shared/models/constants.ts b/ui-ngx/src/app/shared/models/constants.ts index 1617e46b58..f935828989 100644 --- a/ui-ngx/src/app/shared/models/constants.ts +++ b/ui-ngx/src/app/shared/models/constants.ts @@ -94,76 +94,83 @@ export const HelpLinks = { oauth2Apple: 'https://developer.apple.com/sign-in-with-apple/get-started/', oauth2Facebook: 'https://developers.facebook.com/docs/facebook-login/web#logindialog', oauth2Github: 'https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/creating-an-oauth-app', - oauth2Google: 'https://developers.google.com/google-ads/api/docs/start', + oauth2Google: 'https://developers.google.com/identity/protocols/oauth2', ruleEngine: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/overview/`, - ruleNodeCheckRelation: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/filter-nodes/#check-relation-filter-node`, - ruleNodeCheckExistenceFields: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/filter-nodes/#check-existence-fields-node`, - ruleNodeGpsGeofencingFilter: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/filter-nodes/#gps-geofencing-filter-node`, - ruleNodeJsFilter: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/filter-nodes/#script-filter-node`, - ruleNodeJsSwitch: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/filter-nodes/#switch-node`, - ruleNodeAssetProfileSwitch: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/filter-nodes/#asset-profile-switch`, - ruleNodeDeviceProfileSwitch: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/filter-nodes/#device-profile-switch`, - ruleNodeCheckAlarmStatus: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/filter-nodes/#check-alarm-status`, - ruleNodeMessageTypeFilter: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/filter-nodes/#message-type-filter-node`, - ruleNodeMessageTypeSwitch: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/filter-nodes/#message-type-switch-node`, - ruleNodeOriginatorTypeFilter: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/filter-nodes/#originator-type-filter-node`, - ruleNodeOriginatorTypeSwitch: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/filter-nodes/#originator-type-switch-node`, - ruleNodeOriginatorAttributes: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/enrichment-nodes/#originator-attributes`, - ruleNodeOriginatorFields: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/enrichment-nodes/#originator-fields`, - ruleNodeOriginatorTelemetry: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/enrichment-nodes/#originator-telemetry`, - ruleNodeCustomerAttributes: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/enrichment-nodes/#customer-attributes`, - ruleNodeCustomerDetails: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/enrichment-nodes/#customer-details`, - ruleNodeFetchDeviceCredentials: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/enrichment-nodes/#fetch-device-credentials`, - ruleNodeDeviceAttributes: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/enrichment-nodes/#device-attributes`, - ruleNodeRelatedAttributes: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/enrichment-nodes/#related-attributes`, - ruleNodeTenantAttributes: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/enrichment-nodes/#tenant-attributes`, - ruleNodeTenantDetails: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/enrichment-nodes/#tenant-details`, - ruleNodeChangeOriginator: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/transformation-nodes/#change-originator`, - ruleNodeTransformMsg: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/transformation-nodes/#script-transformation-node`, - ruleNodeMsgToEmail: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/transformation-nodes/#to-email-node`, - ruleNodeAssignToCustomer: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/action-nodes/#assign-to-customer-node`, - ruleNodeUnassignFromCustomer: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/action-nodes/#unassign-from-customer-node`, - ruleNodeCalculatedFields: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/action-nodes/#calculated-fields-node`, - ruleNodeClearAlarm: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/action-nodes/#clear-alarm-node`, - ruleNodeCreateAlarm: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/action-nodes/#create-alarm-node`, - ruleNodeCopyToView: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/action-nodes/#copy-to-view-node`, - ruleNodeCreateRelation: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/action-nodes/#create-relation-node`, - ruleNodeDeleteRelation: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/action-nodes/#delete-relation-node`, - ruleNodeDeviceState: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/action-nodes/#device-state-node`, - ruleNodeMessageCount: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/action-nodes/#message-count-node`, - ruleNodeMsgDelay: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/action-nodes/#delay-node-deprecated`, - ruleNodeMsgGenerator: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/action-nodes/#generator-node`, - ruleNodeGpsGeofencingEvents: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/action-nodes/#gps-geofencing-events-node`, - ruleNodeLog: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/action-nodes/#log-node`, - ruleNodeRpcCallReply: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/action-nodes/#rpc-call-reply-node`, - ruleNodeRpcCallRequest: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/action-nodes/#rpc-call-request-node`, - ruleNodeSaveAttributes: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/action-nodes/#save-attributes-node`, - ruleNodeDeleteAttributes: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/action-nodes/#delete-attributes-node`, - ruleNodeSaveTimeseries: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/action-nodes/#save-timeseries-node`, - ruleNodeSaveToCustomTable: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/action-nodes/#save-to-custom-table-node`, - ruleNodeRuleChain: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/flow-nodes/#rule-chain-node`, - ruleNodeOutputNode: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/flow-nodes/#output-node`, - ruleNodeAiRequest: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/external-nodes/#ai-request-node`, - ruleNodeAwsLambda: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/external-nodes/#aws-lambda-node`, - ruleNodeAwsSns: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/external-nodes/#aws-sns-node`, - ruleNodeAwsSqs: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/external-nodes/#aws-sqs-node`, - ruleNodeKafka: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/external-nodes/#kafka-node`, - ruleNodeMqtt: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/external-nodes/#mqtt-node`, - ruleNodeAzureIotHub: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/external-nodes/#azure-iot-hub-node`, - ruleNodeRabbitMq: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/external-nodes/#rabbitmq-node`, - ruleNodeRestApiCall: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/external-nodes/#rest-api-call-node`, - ruleNodeSendEmail: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/external-nodes/#send-email-node`, - ruleNodeSendSms: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/external-nodes/#send-sms-node`, - ruleNodeMath: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/action-nodes/#math-function-node`, - ruleNodeCalculateDelta: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/enrichment-nodes/#calculate-delta`, - ruleNodeRestCallReply: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/action-nodes/#rest-call-reply-node`, - ruleNodePushToCloud: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/action-nodes/#push-to-cloud`, - ruleNodePushToEdge: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/action-nodes/#push-to-edge`, - ruleNodeDeviceProfile: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/action-nodes/#device-profile-node`, - ruleNodeAcknowledge: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/flow-nodes/#acknowledge-node`, - ruleNodeCheckpoint: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/flow-nodes/#checkpoint-node`, - ruleNodeSendNotification: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/external-nodes/#send-notification-node`, - ruleNodeSendSlack: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/external-nodes/#send-to-slack-node`, + ruleNodeCheckRelation: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/filter/check-relation-presence/`, + ruleNodeCheckExistenceFields: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/filter/check-fields-presence/`, + ruleNodeGpsGeofencingFilter: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/filter/gps-geofencing-filter/`, + ruleNodeJsFilter: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/filter/script/`, + ruleNodeJsSwitch: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/filter/switch/`, + ruleNodeAssetProfileSwitch: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/filter/asset-profile-switch/`, + ruleNodeDeviceProfileSwitch: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/filter/device-profile-switch/`, + ruleNodeCheckAlarmStatus: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/filter/alarm-status-filter/`, + ruleNodeMessageTypeFilter: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/filter/message-type-filter/`, + ruleNodeMessageTypeSwitch: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/filter/message-type-switch/`, + ruleNodeOriginatorTypeFilter: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/filter/entity-type-filter/`, + ruleNodeOriginatorTypeSwitch: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/filter/entity-type-switch/`, + ruleNodeOriginatorAttributes: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/enrichment/originator-attributes/`, + ruleNodeOriginatorFields: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/enrichment/originator-fields/`, + ruleNodeOriginatorTelemetry: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/enrichment/originator-telemetry/`, + ruleNodeCustomerAttributes: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/enrichment/customer-attributes/`, + ruleNodeCustomerDetails: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/enrichment/customer-details/`, + ruleNodeFetchDeviceCredentials: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/enrichment/fetch-device-credentials/`, + ruleNodeDeviceAttributes: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/enrichment/related-device-attributes/`, + ruleNodeRelatedAttributes: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/enrichment/related-entity-data/`, + ruleNodeTenantAttributes: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/enrichment/tenant-attributes/`, + ruleNodeTenantDetails: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/enrichment/tenant-details/`, + ruleNodeChangeOriginator: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/transformation/change-originator/`, + ruleNodeCopyKeyValuePairs: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/transformation/copy-key-value-pairs/`, + ruleNodeDeduplication: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/transformation/deduplication/`, + ruleNodeDeleteKeyValuePairs: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/transformation/delete-key-value-pairs/`, + ruleNodeJsonPath: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/transformation/json-path/`, + ruleNodeRenameKeys: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/transformation/rename-keys/`, + ruleNodeTransformMsg: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/transformation/script/`, + ruleNodeSplitArrayMsg: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/transformation/split-array-msg/`, + ruleNodeMsgToEmail: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/transformation/to-email/`, + ruleNodeAssignToCustomer: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/action/assign-to-customer/`, + ruleNodeUnassignFromCustomer: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/action/unassign-from-customer/`, + ruleNodeCalculatedFields: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/action/calculated-fields/`, + ruleNodeClearAlarm: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/action/clear-alarm/`, + ruleNodeCreateAlarm: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/action/create-alarm/`, + ruleNodeCopyToView: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/action/copy-to-view/`, + ruleNodeCreateRelation: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/action/create-relation/`, + ruleNodeDeleteRelation: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/action/delete-relation/`, + ruleNodeDeviceState: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/action/device-state/`, + ruleNodeMessageCount: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/action/message-count/`, + ruleNodeMsgDelay: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/action/delay/`, + ruleNodeMsgGenerator: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/action/generator/`, + ruleNodeGpsGeofencingEvents: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/action/gps-geofencing-events/`, + ruleNodeLog: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/action/log/`, + ruleNodeRpcCallReply: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/action/rpc-call-reply/`, + ruleNodeRpcCallRequest: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/action/rpc-call-request/`, + ruleNodeSaveAttributes: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/action/save-attributes/`, + ruleNodeDeleteAttributes: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/action/delete-attributes/`, + ruleNodeSaveTimeseries: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/action/save-timeseries/`, + ruleNodeSaveToCustomTable: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/action/save-to-custom-table/`, + ruleNodeRuleChain: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/flow/rule-chain/`, + ruleNodeOutputNode: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/flow/output/`, + ruleNodeAiRequest: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/external/ai-request/`, + ruleNodeAwsLambda: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/external/aws-lambda/`, + ruleNodeAwsSns: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/external/aws-sns/`, + ruleNodeAwsSqs: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/external/aws-sqs/`, + ruleNodeKafka: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/external/kafka/`, + ruleNodeMqtt: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/external/mqtt/`, + ruleNodeAzureIotHub: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/external/azure-iot-hub/`, + ruleNodeGcpPubSub: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/external/gcp-pubsub/`, + ruleNodeRabbitMq: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/external/rabbitmq/`, + ruleNodeRestApiCall: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/external/rest-api-call/`, + ruleNodeSendEmail: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/external/send-email/`, + ruleNodeSendSms: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/external/send-sms/`, + ruleNodeMath: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/action/math-function/`, + ruleNodeCalculateDelta: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/enrichment/calculate-delta/`, + ruleNodeRestCallReply: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/action/rest-call-reply/`, + ruleNodePushToCloud: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/action/push-to-cloud/`, + ruleNodePushToEdge: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/action/push-to-edge/`, + ruleNodeDeviceProfile: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/action/device-profile/`, + ruleNodeAcknowledge: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/flow/acknowledge/`, + ruleNodeCheckpoint: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/flow/checkpoint/`, + ruleNodeSendNotification: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/external/send-notification/`, + ruleNodeSendSlack: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/external/send-to-slack/`, tenants: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/ui/tenants`, tenantProfiles: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/ui/tenant-profiles`, customers: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/ui/customers`, @@ -177,6 +184,7 @@ export const HelpLinks = { entitiesImport: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/bulk-provisioning`, rulechains: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/ui/rule-chains`, lwm2mResourceLibrary: `${helpBaseUrl}/docs${docPlatformPrefix}/reference/lwm2m-api`, + jsExtension: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/contribution/ui/advanced-development`, dashboards: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/ui/dashboards`, otaUpdates: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/ota-updates`, widgetTypes: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/ui/widget-library/#widget-types`, @@ -206,7 +214,7 @@ export const HelpLinks = { mobileBundle: `${helpBaseUrl}/docs${docPlatformPrefix}/mobile-center/mobile-center/`, mobileQrCode: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/ui/mobile-qr-code/`, calculatedField: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/calculated-fields/`, - aiModels: `${helpBaseUrl}/docs${docPlatformPrefix}/ai-models`, + aiModels: `${helpBaseUrl}/docs${docPlatformPrefix}/samples/analytics/ai-models/`, timewindowSettings: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/dashboards/#time-window`, trendzSettings: `${helpBaseUrl}/docs/trendz/` } diff --git a/ui-ngx/src/app/shared/models/device.models.ts b/ui-ngx/src/app/shared/models/device.models.ts index 8298d3a1fe..c2b95037a4 100644 --- a/ui-ngx/src/app/shared/models/device.models.ts +++ b/ui-ngx/src/app/shared/models/device.models.ts @@ -22,7 +22,7 @@ import { DeviceCredentialsId } from '@shared/models/id/device-credentials-id'; import { EntitySearchQuery } from '@shared/models/relation.models'; import { DeviceProfileId } from '@shared/models/id/device-profile-id'; import { RuleChainId } from '@shared/models/id/rule-chain-id'; -import { EntityInfoData, HasTenantId, HasVersion } from '@shared/models/entity.models'; +import { EntityInfoData, HasTenantId, HasVersion, SaveEntityParams } from '@shared/models/entity.models'; import { FilterPredicateValue, KeyFilter } from '@shared/models/query/query.models'; import { TimeUnit } from '@shared/models/time/time.models'; import _moment from 'moment'; @@ -739,6 +739,10 @@ export interface DeviceInfoFilter { active?: boolean; } +export interface SaveDeviceParams extends SaveEntityParams { + accessToken?: string; +} + export class DeviceInfoQuery { pageLink: PageLink; diff --git a/ui-ngx/src/app/shared/models/entity.models.ts b/ui-ngx/src/app/shared/models/entity.models.ts index 5aa526b583..a0e6b89c32 100644 --- a/ui-ngx/src/app/shared/models/entity.models.ts +++ b/ui-ngx/src/app/shared/models/entity.models.ts @@ -163,6 +163,11 @@ export const entityFields: {[fieldName: string]: EntityField} = { name: 'entity-field.label', value: 'label' }, + displayName: { + keyName: 'displayName', + name: 'entity-field.name', + value: 'name' + }, queueName: { keyName: 'queueName', name: 'entity-field.queue-name', @@ -209,3 +214,19 @@ export interface EntityTestScriptResult { } export type VersionedEntity = EntityInfoData & HasVersion | RuleChainMetaData; + +export enum NameConflictPolicy { + FAIL = 'FAIL', + UNIQUIFY = 'UNIQUIFY', +} + +export enum UniquifyStrategy { + RANDOM = 'RANDOM', + INCREMENTAL = 'INCREMENTAL' +} + +export interface SaveEntityParams { + nameConflictPolicy?: NameConflictPolicy; + uniquifyStrategy?: UniquifyStrategy; + uniquifySeparator?: string; +} diff --git a/ui-ngx/src/app/shared/models/public-api.ts b/ui-ngx/src/app/shared/models/public-api.ts index 91e1a2b033..bd7fe18262 100644 --- a/ui-ngx/src/app/shared/models/public-api.ts +++ b/ui-ngx/src/app/shared/models/public-api.ts @@ -18,6 +18,8 @@ export * from './id/public-api'; export * from './page/public-api'; export * from './telemetry/telemetry.models'; export * from './time/time.models'; +export * from './widget/public-api'; +export * from './action-widget-settings.models'; export * from './alarm.models'; export * from './alias.models'; export * from './api-usage.models'; @@ -25,18 +27,22 @@ export * from './asset.models'; export * from './audit-log.models'; export * from './authority.enum'; export * from './base-data'; +export * from './calculated-field.models'; export * from './component-descriptor.models'; export * from './constants'; export * from './contact-based.model'; +export * from './country.models'; export * from './customer.model'; export * from './dashboard.models'; export * from './device.models'; +export * from './dynamic-form.models'; export * from './edge.models'; export * from './entity.models'; export * from './entity-type.models'; export * from './entity-view.models'; export * from './error.models'; export * from './event.models'; +export * from './js-function.models'; export * from './limited-api.models'; export * from './login.models'; export * from './material.models'; diff --git a/ui-ngx/src/app/shared/models/resource.models.ts b/ui-ngx/src/app/shared/models/resource.models.ts index 0419e40a7c..e19f1c82a0 100644 --- a/ui-ngx/src/app/shared/models/resource.models.ts +++ b/ui-ngx/src/app/shared/models/resource.models.ts @@ -24,7 +24,8 @@ export enum ResourceType { LWM2M_MODEL = 'LWM2M_MODEL', PKCS_12 = 'PKCS_12', JKS = 'JKS', - JS_MODULE = 'JS_MODULE' + JS_MODULE = 'JS_MODULE', + GENERAL = 'GENERAL', } export enum ResourceSubType { @@ -57,7 +58,8 @@ export const ResourceTypeTranslationMap = new Map( [ResourceType.LWM2M_MODEL, 'resource.type.lwm2m-model'], [ResourceType.PKCS_12, 'resource.type.pkcs-12'], [ResourceType.JKS, 'resource.type.jks'], - [ResourceType.JS_MODULE, 'resource.type.js-module'] + [ResourceType.JS_MODULE, 'resource.type.js-module'], + [ResourceType.GENERAL, 'resource.type.general'], ] ); @@ -76,8 +78,8 @@ export interface TbResourceInfo extends Omit, 'name' | title?: string; resourceType: ResourceType; resourceSubType?: ResourceSubType; - fileName: string; - public: boolean; + fileName?: string; + public?: boolean; publicResourceKey?: string; readonly link?: string; readonly publicLink?: string; @@ -87,7 +89,7 @@ export interface TbResourceInfo extends Omit, 'name' | export type ResourceInfo = TbResourceInfo; export interface Resource extends ResourceInfo { - data: string; + data?: string; name?: string; } @@ -188,6 +190,7 @@ export const isImageResourceUrl = (url: string): boolean => url && IMAGES_URL_RE export const isJSResourceUrl = (url: string): boolean => url && RESOURCES_URL_REGEXP.test(url); export const isJSResource = (url: string): boolean => url?.startsWith(TB_RESOURCE_PREFIX); +export const isTbImage = (url: string): boolean => url?.startsWith(TB_IMAGE_PREFIX); export const extractParamsFromImageResourceUrl = (url: string): {type: ImageResourceType; key: string} => { const res = url.match(IMAGES_URL_REGEXP); diff --git a/ui-ngx/src/app/shared/models/rule-node.models.ts b/ui-ngx/src/app/shared/models/rule-node.models.ts index 8225fbf18f..786fd72448 100644 --- a/ui-ngx/src/app/shared/models/rule-node.models.ts +++ b/ui-ngx/src/app/shared/models/rule-node.models.ts @@ -476,7 +476,13 @@ const ruleNodeClazzHelpLinkMap = { 'org.thingsboard.rule.engine.metadata.TbGetTenantDetailsNode': 'ruleNodeTenantDetails', 'org.thingsboard.rule.engine.metadata.CalculateDeltaNode': 'ruleNodeCalculateDelta', 'org.thingsboard.rule.engine.transform.TbChangeOriginatorNode': 'ruleNodeChangeOriginator', + 'org.thingsboard.rule.engine.transform.TbCopyKeysNode': 'ruleNodeCopyKeyValuePairs', + 'org.thingsboard.rule.engine.deduplication.TbMsgDeduplicationNode': 'ruleNodeDeduplication', + 'org.thingsboard.rule.engine.transform.TbDeleteKeysNode': 'ruleNodeDeleteKeyValuePairs', + 'org.thingsboard.rule.engine.transform.TbJsonPathNode': 'ruleNodeJsonPath', + 'org.thingsboard.rule.engine.transform.TbRenameKeysNode': 'ruleNodeRenameKeys', 'org.thingsboard.rule.engine.transform.TbTransformMsgNode': 'ruleNodeTransformMsg', + 'org.thingsboard.rule.engine.transform.TbSplitArrayMsgNode': 'ruleNodeSplitArrayMsg', 'org.thingsboard.rule.engine.mail.TbMsgToEmailNode': 'ruleNodeMsgToEmail', 'org.thingsboard.rule.engine.action.TbAssignToCustomerNode': 'ruleNodeAssignToCustomer', 'org.thingsboard.rule.engine.action.TbUnassignFromCustomerNode': 'ruleNodeUnassignFromCustomer', @@ -505,6 +511,7 @@ const ruleNodeClazzHelpLinkMap = { 'org.thingsboard.rule.engine.kafka.TbKafkaNode': 'ruleNodeKafka', 'org.thingsboard.rule.engine.mqtt.TbMqttNode': 'ruleNodeMqtt', 'org.thingsboard.rule.engine.mqtt.azure.TbAzureIotHubNode': 'ruleNodeAzureIotHub', + 'org.thingsboard.rule.engine.gcp.pubsub.TbPubSubNode': 'ruleNodeGcpPubSub', 'org.thingsboard.rule.engine.rabbitmq.TbRabbitMqNode': 'ruleNodeRabbitMq', 'org.thingsboard.rule.engine.rest.TbRestApiCallNode': 'ruleNodeRestApiCall', 'org.thingsboard.rule.engine.mail.TbSendEmailNode': 'ruleNodeSendEmail', diff --git a/ui-ngx/src/app/shared/models/tenant.model.ts b/ui-ngx/src/app/shared/models/tenant.model.ts index 059fe39ada..0cfa8df888 100644 --- a/ui-ngx/src/app/shared/models/tenant.model.ts +++ b/ui-ngx/src/app/shared/models/tenant.model.ts @@ -19,6 +19,11 @@ import { TenantId } from './id/tenant-id'; import { TenantProfileId } from '@shared/models/id/tenant-profile-id'; import { BaseData, ExportableEntity } from '@shared/models/base-data'; import { QueueInfo } from '@shared/models/queue.models'; +import { FormControl } from '@angular/forms'; + +export type FormControlsFrom = { + [K in keyof T]-?: FormControl; +}; export enum TenantProfileType { DEFAULT = 'DEFAULT' @@ -101,6 +106,11 @@ export interface DefaultTenantProfileConfiguration { maxCalculatedFieldsPerEntity: number; maxArgumentsPerCF: number; + maxRelationLevelPerCfArgument: number; + minAllowedDeduplicationIntervalInSecForCF: number; + minAllowedAggregationIntervalInSecForCF: number; + maxRelatedEntitiesToReturnPerCfArgument: number; + minAllowedScheduledUpdateIntervalInSecForCF: number; maxDataPointsPerRollingArg: number; maxStateSizeInKBytes: number; maxSingleValueArgumentSizeInKBytes: number; @@ -165,6 +175,11 @@ export function createTenantProfileConfiguration(type: TenantProfileType): Tenan maxCalculatedFieldsPerEntity: 5, maxArgumentsPerCF: 10, maxDataPointsPerRollingArg: 1000, + maxRelationLevelPerCfArgument: 10, + minAllowedDeduplicationIntervalInSecForCF: 60, + minAllowedAggregationIntervalInSecForCF: 60, + maxRelatedEntitiesToReturnPerCfArgument: 100, + minAllowedScheduledUpdateIntervalInSecForCF: 0, maxStateSizeInKBytes: 32, maxSingleValueArgumentSizeInKBytes: 2, calculatedFieldDebugEventsRateLimit: '' diff --git a/ui-ngx/src/app/shared/models/time/time.models.ts b/ui-ngx/src/app/shared/models/time/time.models.ts index c204cb6867..f84a883235 100644 --- a/ui-ngx/src/app/shared/models/time/time.models.ts +++ b/ui-ngx/src/app/shared/models/time/time.models.ts @@ -15,11 +15,12 @@ /// import { TimeService } from '@core/services/time.service'; -import { deepClone, isDefined, isDefinedAndNotNull, isNumeric, isUndefined } from '@app/core/utils'; +import { deepClean, deepClone, isDefined, isDefinedAndNotNull, isNumeric, isUndefined } from '@app/core/utils'; import moment_ from 'moment'; import * as momentTz from 'moment-timezone'; import { IntervalType } from '@shared/models/telemetry/telemetry.models'; import { FormGroup } from '@angular/forms'; +import { ToggleHeaderOption } from '@shared/components/toggle-header.component'; const moment = moment_; @@ -281,19 +282,12 @@ export const historyInterval = (timewindowMs: number): Timewindow => ({ export const defaultTimewindow = (timeService: TimeService): Timewindow => { const currentTime = moment().valueOf(); return { - displayValue: '', - hideAggregation: false, - hideAggInterval: false, - hideTimezone: false, selectedTab: TimewindowType.REALTIME, realtime: { realtimeType: RealtimeWindowType.LAST_INTERVAL, interval: SECOND, timewindowMs: MINUTE, quickInterval: QuickTimeInterval.CURRENT_DAY, - hideInterval: false, - hideLastInterval: false, - hideQuickInterval: false }, history: { historyType: HistoryWindowType.LAST_INTERVAL, @@ -304,10 +298,6 @@ export const defaultTimewindow = (timeService: TimeService): Timewindow => { endTimeMs: currentTime }, quickInterval: QuickTimeInterval.CURRENT_DAY, - hideInterval: false, - hideLastInterval: false, - hideFixedInterval: false, - hideQuickInterval: false }, aggregation: { type: AggregationType.AVG, @@ -325,40 +315,47 @@ const getTimewindowType = (timewindow: Timewindow): TimewindowType => { }; export const initModelFromDefaultTimewindow = (value: Timewindow, quickIntervalOnly: boolean, - historyOnly: boolean, timeService: TimeService): Timewindow => { + historyOnly: boolean, timeService: TimeService, hasAggregation: boolean): Timewindow => { const model = defaultTimewindow(timeService); if (value) { if (value.allowedAggTypes?.length) { model.allowedAggTypes = value.allowedAggTypes; } - model.hideAggregation = value.hideAggregation; - model.hideAggInterval = value.hideAggInterval; - model.hideTimezone = value.hideTimezone; + if (value.hideAggregation) { + model.hideAggregation = value.hideAggregation; + } + if (value.hideAggInterval) { + model.hideAggInterval = value.hideAggInterval; + } + if (value.hideTimezone) { + model.hideTimezone = value.hideTimezone; + } + model.selectedTab = getTimewindowType(value); // for backward compatibility - if (isDefinedAndNotNull((value as any).hideInterval)) { + if ((value as any).hideInterval) { model.realtime.hideInterval = (value as any).hideInterval; model.history.hideInterval = (value as any).hideInterval; delete (value as any).hideInterval; } - if (isDefinedAndNotNull((value as any).hideLastInterval)) { + if ((value as any).hideLastInterval) { model.realtime.hideLastInterval = (value as any).hideLastInterval; delete (value as any).hideLastInterval; } - if (isDefinedAndNotNull((value as any).hideQuickInterval)) { + if ((value as any).hideQuickInterval) { model.realtime.hideQuickInterval = (value as any).hideQuickInterval; delete (value as any).hideQuickInterval; } if (isDefined(value.realtime)) { - if (isDefinedAndNotNull(value.realtime.hideInterval)) { + if (value.realtime.hideInterval) { model.realtime.hideInterval = value.realtime.hideInterval; } - if (isDefinedAndNotNull(value.realtime.hideLastInterval)) { + if (value.realtime.hideLastInterval) { model.realtime.hideLastInterval = value.realtime.hideLastInterval; } - if (isDefinedAndNotNull(value.realtime.hideQuickInterval)) { + if (value.realtime.hideQuickInterval) { model.realtime.hideQuickInterval = value.realtime.hideQuickInterval; } if (value.realtime.disableCustomInterval) { @@ -392,16 +389,16 @@ export const initModelFromDefaultTimewindow = (value: Timewindow, quickIntervalO } } if (isDefined(value.history)) { - if (isDefinedAndNotNull(value.history.hideInterval)) { + if (value.history.hideInterval) { model.history.hideInterval = value.history.hideInterval; } - if (isDefinedAndNotNull(value.history.hideLastInterval)) { + if (value.history.hideLastInterval) { model.history.hideLastInterval = value.history.hideLastInterval; } - if (isDefinedAndNotNull(value.history.hideFixedInterval)) { + if (value.history.hideFixedInterval) { model.history.hideFixedInterval = value.history.hideFixedInterval; } - if (isDefinedAndNotNull(value.history.hideQuickInterval)) { + if (value.history.hideQuickInterval) { model.history.hideQuickInterval = value.history.hideQuickInterval; } if (value.history.disableCustomInterval) { @@ -450,7 +447,9 @@ export const initModelFromDefaultTimewindow = (value: Timewindow, quickIntervalO } model.aggregation.limit = value.aggregation.limit || Math.floor(timeService.getMaxDatapointsLimit() / 2); } - model.timezone = value.timezone; + if (value.timezone) { + model.timezone = value.timezone; + } } if (quickIntervalOnly) { model.realtime.realtimeType = RealtimeWindowType.INTERVAL; @@ -458,7 +457,7 @@ export const initModelFromDefaultTimewindow = (value: Timewindow, quickIntervalO if (historyOnly) { model.selectedTab = TimewindowType.HISTORY; } - return model; + return clearTimewindowConfig(model, quickIntervalOnly, historyOnly, hasAggregation); }; export const toHistoryTimewindow = (timewindow: Timewindow, startTimeMs: number, endTimeMs: number, @@ -526,17 +525,20 @@ export const timewindowTypeChanged = (newTimewindow: Timewindow, oldTimewindow: }; export const updateFormValuesOnTimewindowTypeChange = (selectedTab: TimewindowType, - quickIntervalOnly: boolean, timewindowForm: FormGroup, + timewindowForm: FormGroup, realtimeDisableCustomInterval: boolean, historyDisableCustomInterval: boolean, - realtimeAdvancedParams?: TimewindowAdvancedParams, - historyAdvancedParams?: TimewindowAdvancedParams) => { + realtimeAdvancedParams: TimewindowAdvancedParams, + historyAdvancedParams: TimewindowAdvancedParams, + realtimeTimewindowOptions: ToggleHeaderOption[], + historyTimewindowOptions: ToggleHeaderOption[]) => { const timewindowFormValue = timewindowForm.getRawValue(); if (selectedTab === TimewindowType.REALTIME) { - if (timewindowFormValue.history.historyType !== HistoryWindowType.FIXED - && !(quickIntervalOnly && timewindowFormValue.history.historyType === HistoryWindowType.LAST_INTERVAL)) { - if (Object.keys(RealtimeWindowType).includes(HistoryWindowType[timewindowFormValue.history.historyType])) { - timewindowForm.get('realtime.realtimeType').patchValue(RealtimeWindowType[HistoryWindowType[timewindowFormValue.history.historyType]]); - } + const sameWindowTypeOptionAvailable = realtimeTimewindowOptions.some( + option => { + return option.value === RealtimeWindowType[HistoryWindowType[timewindowFormValue.history.historyType]] + }); + if (sameWindowTypeOptionAvailable) { + timewindowForm.get('realtime.realtimeType').patchValue(RealtimeWindowType[HistoryWindowType[timewindowFormValue.history.historyType]]); if (!realtimeDisableCustomInterval || !realtimeAdvancedParams?.allowedLastIntervals?.length || realtimeAdvancedParams.allowedLastIntervals.includes(timewindowFormValue.history.timewindowMs)) { timewindowForm.get('realtime.timewindowMs').patchValue(timewindowFormValue.history.timewindowMs); @@ -554,20 +556,26 @@ export const updateFormValuesOnTimewindowTypeChange = (selectedTab: TimewindowTy } } } else { - timewindowForm.get('history.historyType').patchValue(HistoryWindowType[RealtimeWindowType[timewindowFormValue.realtime.realtimeType]]); - if (!historyDisableCustomInterval || + const sameWindowTypeOptionAvailable = historyTimewindowOptions.some( + option => { + return option.value === HistoryWindowType[RealtimeWindowType[timewindowFormValue.realtime.realtimeType]] + }); + if (sameWindowTypeOptionAvailable) { + timewindowForm.get('history.historyType').patchValue(HistoryWindowType[RealtimeWindowType[timewindowFormValue.realtime.realtimeType]]); + if (!historyDisableCustomInterval || !historyAdvancedParams?.allowedLastIntervals?.length || historyAdvancedParams.allowedLastIntervals?.includes(timewindowFormValue.realtime.timewindowMs)) { - timewindowForm.get('history.timewindowMs').patchValue(timewindowFormValue.realtime.timewindowMs); - } - if (!historyAdvancedParams?.allowedQuickIntervals?.length || historyAdvancedParams.allowedQuickIntervals?.includes(timewindowFormValue.realtime.quickInterval)) { - timewindowForm.get('history.quickInterval').patchValue(timewindowFormValue.realtime.quickInterval); - } - const defaultAggInterval = historyDefaultAggInterval(timewindowForm.getRawValue(), historyAdvancedParams); - const allowedAggIntervals = historyAllowedAggIntervals(timewindowForm.getRawValue(), historyAdvancedParams); - if (defaultAggInterval || !allowedAggIntervals.length || allowedAggIntervals.includes(timewindowFormValue.realtime.interval)) { - setTimeout(() => timewindowForm.get('history.interval').patchValue( - defaultAggInterval ?? timewindowFormValue.realtime.interval - )); + timewindowForm.get('history.timewindowMs').patchValue(timewindowFormValue.realtime.timewindowMs); + } + if (!historyAdvancedParams?.allowedQuickIntervals?.length || historyAdvancedParams.allowedQuickIntervals?.includes(timewindowFormValue.realtime.quickInterval)) { + timewindowForm.get('history.quickInterval').patchValue(timewindowFormValue.realtime.quickInterval); + } + const defaultAggInterval = historyDefaultAggInterval(timewindowForm.getRawValue(), historyAdvancedParams); + const allowedAggIntervals = historyAllowedAggIntervals(timewindowForm.getRawValue(), historyAdvancedParams); + if (defaultAggInterval || !allowedAggIntervals.length || allowedAggIntervals.includes(timewindowFormValue.realtime.interval)) { + setTimeout(() => timewindowForm.get('history.interval').patchValue( + defaultAggInterval ?? timewindowFormValue.realtime.interval + )); + } } } timewindowForm.patchValue({ @@ -1098,19 +1106,94 @@ export const cloneSelectedTimewindow = (timewindow: Timewindow): Timewindow => { if (timewindow.allowedAggTypes?.length) { cloned.allowedAggTypes = timewindow.allowedAggTypes; } - cloned.hideAggregation = timewindow.hideAggregation || false; - cloned.hideAggInterval = timewindow.hideAggInterval || false; - cloned.hideTimezone = timewindow.hideTimezone || false; + if (timewindow.hideAggregation) { + cloned.hideAggregation = timewindow.hideAggregation; + } + if (timewindow.hideAggInterval) { + cloned.hideAggInterval = timewindow.hideAggInterval; + } + if (timewindow.hideTimezone) { + cloned.hideTimezone = timewindow.hideTimezone; + } if (isDefined(timewindow.selectedTab)) { cloned.selectedTab = timewindow.selectedTab; } - cloned.realtime = deepClone(timewindow.realtime); - cloned.history = deepClone(timewindow.history); - cloned.aggregation = deepClone(timewindow.aggregation); - cloned.timezone = timewindow.timezone; + if (isDefined(timewindow.realtime)) { + cloned.realtime = deepClone(timewindow.realtime); + } + if (isDefined(timewindow.history)) { + cloned.history = deepClone(timewindow.history); + } + if (isDefined(timewindow.aggregation)) { + cloned.aggregation = deepClone(timewindow.aggregation); + } + if (timewindow.timezone) { + cloned.timezone = timewindow.timezone; + } return cloned; }; +export const clearTimewindowConfig = (timewindow: Timewindow, quickIntervalOnly: boolean, + historyOnly: boolean, hasAggregation: boolean, hasTimezone = true): Timewindow => { + const noneAggregation = hasAggregation && timewindow.aggregation?.type === AggregationType.NONE; + if (timewindow.selectedTab === TimewindowType.REALTIME) { + if (quickIntervalOnly || timewindow.realtime.realtimeType === RealtimeWindowType.INTERVAL) { + delete timewindow.realtime.timewindowMs; + } else { + delete timewindow.realtime.quickInterval; + } + + delete timewindow.history?.historyType; + delete timewindow.history?.timewindowMs; + delete timewindow.history?.fixedTimewindow; + delete timewindow.history?.quickInterval; + + delete timewindow.history?.interval; + if (!hasAggregation || noneAggregation) { + delete timewindow.realtime.interval; + } + } else { + if (timewindow.history.historyType === HistoryWindowType.LAST_INTERVAL) { + delete timewindow.history.fixedTimewindow; + delete timewindow.history.quickInterval; + } else if (timewindow.history.historyType === HistoryWindowType.FIXED) { + delete timewindow.history.timewindowMs; + delete timewindow.history.quickInterval; + } else if (timewindow.history.historyType === HistoryWindowType.INTERVAL) { + delete timewindow.history.timewindowMs; + delete timewindow.history.fixedTimewindow; + } else { + delete timewindow.history.timewindowMs; + delete timewindow.history.fixedTimewindow; + delete timewindow.history.quickInterval; + } + + delete timewindow.realtime?.realtimeType; + delete timewindow.realtime?.timewindowMs; + delete timewindow.realtime?.quickInterval; + + delete timewindow.realtime?.interval; + if (!hasAggregation || noneAggregation) { + delete timewindow.history.interval; + } + } + + if (!hasAggregation) { + delete timewindow.aggregation; + } else if (!noneAggregation) { + delete timewindow.aggregation.limit; + } + + if (historyOnly) { + delete timewindow.realtime; + } + + if (!hasTimezone) { + delete timewindow.timezone; + } + return deepClean(timewindow); +}; + export interface TimeInterval { name: string; translateParams: {[key: string]: any}; diff --git a/ui-ngx/src/app/shared/models/two-factor-auth.models.ts b/ui-ngx/src/app/shared/models/two-factor-auth.models.ts index f2f392bd04..96a8d8186e 100644 --- a/ui-ngx/src/app/shared/models/two-factor-auth.models.ts +++ b/ui-ngx/src/app/shared/models/two-factor-auth.models.ts @@ -14,7 +14,11 @@ /// limitations under the License. /// +import { UsersFilter } from '@shared/models/notification.models'; + export interface TwoFactorAuthSettings { + enforceTwoFa: boolean; + enforcedUsersFilter: UsersFilter; maxVerificationFailuresBeforeUserLockout: number; providers: Array; totalAllowedTimeForVerification: number; @@ -24,12 +28,18 @@ export interface TwoFactorAuthSettings { } export interface TwoFactorAuthSettingsForm extends TwoFactorAuthSettings{ + enforceTwoFa: boolean; + enforcedUsersFilter: UsersFilterWithFilterByTenant; providers: Array; verificationCodeCheckRateLimitEnable: boolean; verificationCodeCheckRateLimitNumber: number; verificationCodeCheckRateLimitTime: number; } +export interface UsersFilterWithFilterByTenant extends UsersFilter{ + filterByTenants?: boolean; +} + export type TwoFactorAuthProviderConfig = Partial; @@ -183,3 +193,61 @@ export const twoFactorAuthProvidersLoginData = new Map>( + [ + [ + TwoFactorAuthProviderType.TOTP, { + name: 'login.enable-authenticator-app', + description: 'login.enable-authenticator-app-description' + } + ], + [ + TwoFactorAuthProviderType.SMS, { + name: 'login.enable-authenticator-sms', + description: 'login.enable-authenticator-sms-description' + } + ], + [ + TwoFactorAuthProviderType.EMAIL, { + name: 'login.enable-authenticator-email', + description: 'login.enable-authenticator-email-description' + } + ], + [ + TwoFactorAuthProviderType.BACKUP_CODE, { + name: 'security.2fa.provider.backup_code', + description: 'login.backup-code-auth-description' + } + ] + ] +); + +export const twoFactorAuthProvidersSuccessCardTranslate = new Map>( + [ + [ + TwoFactorAuthProviderType.TOTP, { + name: 'login.authenticator-app-success', + description: 'login.authenticator-app-success-description' + } + ], + [ + TwoFactorAuthProviderType.SMS, { + name: 'login.authenticator-sms-success', + description: 'login.authenticator-sms-success-description' + } + ], + [ + TwoFactorAuthProviderType.EMAIL, { + name: 'login.authenticator-email-success', + description: 'login.authenticator-email-success-description' + } + ], + [ + TwoFactorAuthProviderType.BACKUP_CODE, { + name: 'login.authenticator-backup-code-success', + description: 'login.authenticator-backup-code-success-description' + } + ] + ] +); diff --git a/ui-ngx/src/app/shared/models/widget.models.ts b/ui-ngx/src/app/shared/models/widget.models.ts index 00b62cfb72..f612dd1503 100644 --- a/ui-ngx/src/app/shared/models/widget.models.ts +++ b/ui-ngx/src/app/shared/models/widget.models.ts @@ -33,7 +33,7 @@ import { PageComponent } from '@shared/components/page.component'; import { AfterViewInit, DestroyRef, Directive, EventEmitter, inject, Inject, OnInit, Type } from '@angular/core'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; -import { AbstractControl, UntypedFormGroup } from '@angular/forms'; +import { AbstractControl, UntypedFormGroup, ValidatorFn } from '@angular/forms'; import { Observable } from 'rxjs'; import { Dashboard } from '@shared/models/dashboard.models'; import { IAliasController } from '@core/api/widget-api.models'; @@ -51,6 +51,7 @@ import { TbFunction } from '@shared/models/js-function.models'; import { FormProperty, jsonFormSchemaToFormProperties } from '@shared/models/dynamic-form.models'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { TbUnit } from '@shared/models/unit.models'; +import { ImageResourceInfo } from '@shared/models/resource.models'; export enum widgetType { timeseries = 'timeseries', @@ -489,6 +490,14 @@ export const targetDeviceValid = (targetDevice?: TargetDevice): boolean => ((targetDevice.type === TargetDeviceType.device && !!targetDevice.deviceId) || (targetDevice.type === TargetDeviceType.entity && !!targetDevice.entityAliasId)); +export const widgetTypeHasTimewindow = (type: widgetType): boolean => { + return type === widgetType.timeseries || type === widgetType.alarm; +} + +export const widgetTypeCanHaveTimewindow = (type: widgetType): boolean => { + return widgetTypeHasTimewindow(type) || type === widgetType.latest; +} + export const datasourcesHasAggregation = (datasources?: Array): boolean => { if (datasources) { const foundDatasource = datasources.find(datasource => { @@ -622,11 +631,36 @@ export enum WidgetMobileActionType { deviceProvision = 'deviceProvision', } +export interface ActionConfig { + title: string, + formControlName: string, + functionName: string, + functionArgs: string[], + helpId?: string +} + +export enum ProvisionType { + auto = 'auto', + wiFi = 'wiFi', + ble = 'ble', + softAp = 'softAp' +} + +export const provisionTypeTranslationMap = new Map( + [ + [ ProvisionType.auto, 'widget-action.mobile.auto' ], + [ ProvisionType.wiFi, 'widget-action.mobile.wi-fi' ], + [ ProvisionType.ble, 'widget-action.mobile.ble' ], + [ ProvisionType.softAp, 'widget-action.mobile.soft-ap' ], + ] +); + export enum MapItemType { marker = 'marker', polygon = 'polygon', rectangle = 'rectangle', - circle = 'circle' + circle = 'circle', + polyline = 'polyline' } export const widgetActionTypes = Object.keys(WidgetActionType) @@ -666,6 +700,7 @@ export const mapItemTypeTranslationMap = new Map( [ MapItemType.polygon, 'widget-action.map-item.polygon' ], [ MapItemType.rectangle, 'widget-action.map-item.rectangle' ], [ MapItemType.circle, 'widget-action.map-item.circle' ], + [ MapItemType.polyline, 'widget-action.map-item.polyline' ] ] ) @@ -675,6 +710,7 @@ export interface MobileLaunchResult { export interface MobileImageResult { imageUrl: string; + imageInfo?: ImageResourceInfo; } export interface MobileQrCodeResult { @@ -706,10 +742,12 @@ export interface WidgetMobileActionResult { export interface ProvisionSuccessDescriptor { handleProvisionSuccessFunction: TbFunction; + provisionType?: string; } export interface ProcessImageDescriptor { processImageFunction: TbFunction; + saveToGallery?: boolean; } export interface ProcessLaunchResultDescriptor { @@ -743,6 +781,7 @@ export interface WidgetMobileActionDescriptor extends WidgetMobileActionDescript type: WidgetMobileActionType; handleErrorFunction?: TbFunction; handleEmptyResultFunction?: TbFunction; + handleNonMobileFallbackFunction?: TbFunction; } export interface CustomActionDescriptor { @@ -789,6 +828,8 @@ export interface MapItemTooltips { finishRect?: string; startCircle?: string; finishCircle?: string; + startPolyline?: string; + finishPolyline?: string; } export const mapItemTooltipsTranslation: Required = Object.freeze({ @@ -799,7 +840,9 @@ export const mapItemTooltipsTranslation: Required = Object.free startRect: 'widgets.maps.data-layer.polygon.rectangle-place-first-point-hint', finishRect: 'widgets.maps.data-layer.polygon.finish-rectangle-hint', startCircle: 'widgets.maps.data-layer.circle.place-circle-center-hint', - finishCircle: 'widgets.maps.data-layer.circle.finish-circle-hint' + finishCircle: 'widgets.maps.data-layer.circle.finish-circle-hint', + startPolyline: 'widgets.maps.data-layer.polyline.polyline-place-first-point-hint', + finishPolyline: 'widgets.maps.data-layer.polyline.finish-polyline-hint' }) export interface WidgetActionDescriptor extends WidgetAction { @@ -1108,5 +1151,4 @@ export abstract class WidgetSettingsComponent extends PageComponent implements protected onWidgetConfigSet(widgetConfig: WidgetConfigComponentData) { } - } diff --git a/ui-ngx/src/app/shared/models/widget/home-widgets/api-usage-model.definition.ts b/ui-ngx/src/app/shared/models/widget/home-widgets/api-usage-model.definition.ts new file mode 100644 index 0000000000..5c92b84cbe --- /dev/null +++ b/ui-ngx/src/app/shared/models/widget/home-widgets/api-usage-model.definition.ts @@ -0,0 +1,88 @@ +/// +/// Copyright © 2016-2025 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { EntityAliases, EntityAliasInfo, getEntityAliasId } from '@shared/models/alias.models'; +import { FilterInfo, Filters } from '@shared/models/query/query.models'; +import { Dashboard } from '@shared/models/dashboard.models'; +import { Datasource, DatasourceType, Widget } from '@shared/models/widget.models'; +import { WidgetModelDefinition } from '@shared/models/widget/widget-model.definition'; +import { + ApiUsageWidgetSettings, + getUniqueDataKeys +} from '@home/components/widget/lib/settings/cards/api-usage-settings.component.models'; + +interface AliasFilterPair { + alias?: EntityAliasInfo; + filter?: FilterInfo; +} + +interface ApiUsageDatasourcesInfo { + ds?: AliasFilterPair; +} + +export const ApiUsageModelDefinition: WidgetModelDefinition = { + testWidget(widget: Widget): boolean { + if (widget?.config?.settings) { + const settings = widget.config.settings; + if (settings.apiUsageDataKeys && Array.isArray(settings.apiUsageDataKeys)) { + return true; + } + } + return false; + }, + prepareExportInfo(dashboard: Dashboard, widget: Widget): ApiUsageDatasourcesInfo { + const settings: ApiUsageWidgetSettings = widget.config.settings as ApiUsageWidgetSettings; + const info: ApiUsageDatasourcesInfo = {}; + if (settings.dsEntityAliasId) { + info.ds = prepareExportDataSourcesInfo(dashboard, settings.dsEntityAliasId); + } + return info; + }, + updateFromExportInfo(widget: Widget, entityAliases: EntityAliases, filters: Filters, info: ApiUsageDatasourcesInfo): void { + const settings: ApiUsageWidgetSettings = widget.config.settings as ApiUsageWidgetSettings; + if (info?.ds?.alias) { + settings.dsEntityAliasId = getEntityAliasId(entityAliases, info.ds.alias); + } + }, + datasources(widget: Widget): Datasource[] { + const settings: ApiUsageWidgetSettings = widget.config.settings as ApiUsageWidgetSettings; + const datasources: Datasource[] = []; + if (settings.apiUsageDataKeys?.length && settings.dsEntityAliasId) { + datasources.push({ + type: DatasourceType.entity, + name: '', + entityAliasId: settings.dsEntityAliasId, + dataKeys: getUniqueDataKeys(settings.apiUsageDataKeys) + }); + } + return datasources; + }, + hasTimewindow(): boolean { + return false; + } +}; + +const prepareExportDataSourcesInfo = (dashboard: Dashboard, settings: string): AliasFilterPair => { + const aliasAndFilter: AliasFilterPair = {}; + const entityAlias = dashboard.configuration.entityAliases[settings]; + if (entityAlias) { + aliasAndFilter.alias = { + alias: entityAlias.alias, + filter: entityAlias.filter + }; + } + return aliasAndFilter; +} diff --git a/ui-ngx/src/app/shared/models/widget/maps/map-model.definition.ts b/ui-ngx/src/app/shared/models/widget/maps/map-model.definition.ts index 1dcd6890d8..a5dc5e3ff4 100644 --- a/ui-ngx/src/app/shared/models/widget/maps/map-model.definition.ts +++ b/ui-ngx/src/app/shared/models/widget/maps/map-model.definition.ts @@ -17,10 +17,12 @@ import { EntityAliases, EntityAliasInfo, getEntityAliasId } from '@shared/models/alias.models'; import { FilterInfo, Filters, getFilterId } from '@shared/models/query/query.models'; import { Dashboard } from '@shared/models/dashboard.models'; -import { Datasource, DatasourceType, Widget } from '@shared/models/widget.models'; +import { Datasource, datasourcesHasAggregation, DatasourceType, Widget } from '@shared/models/widget.models'; import { + additionalMapDataSourcesToDatasources, BaseMapSettings, MapDataLayerSettings, + MapDataLayerType, MapDataSourceSettings, mapDataSourceSettingsToDatasource, MapType @@ -121,9 +123,30 @@ export const MapModelDefinition: WidgetModelDefinition = { datasources.push(...getMapDataLayersDatasources(settings.circles)); } if (settings.additionalDataSources?.length) { - datasources.push(...getMapDataLayersDatasources(settings.additionalDataSources)); + datasources.push(...additionalMapDataSourcesToDatasources(settings.additionalDataSources)); } return datasources; + }, + hasTimewindow(widget: Widget): boolean { + const settings: BaseMapSettings = widget.config.settings as BaseMapSettings; + if (settings.trips?.length) { + return true; + } else { + const datasources: Datasource[] = []; + if (settings.markers?.length) { + datasources.push(...getMapDataLayersDatasources(settings.markers, true, 'markers')); + } + if (settings.polygons?.length) { + datasources.push(...getMapDataLayersDatasources(settings.polygons, true, 'polygons')); + } + if (settings.circles?.length) { + datasources.push(...getMapDataLayersDatasources(settings.circles, true, 'circles')); + } + if (settings.additionalDataSources?.length) { + datasources.push(...additionalMapDataSourcesToDatasources(settings.additionalDataSources)); + } + return datasourcesHasAggregation(datasources); + } } }; @@ -211,13 +234,19 @@ const prepareAliasAndFilterPair = (dashboard: Dashboard, settings: MapDataSource } } -const getMapDataLayersDatasources = (settings: MapDataLayerSettings[] | MapDataSourceSettings[]): Datasource[] => { +const getMapDataLayersDatasources = (settings: MapDataLayerSettings[], + includeDataKeys = false, dataLayerType?: MapDataLayerType): Datasource[] => { const datasources: Datasource[] = []; settings.forEach((dsSettings) => { - datasources.push(mapDataSourceSettingsToDatasource(dsSettings)); - if ((dsSettings as MapDataLayerSettings).additionalDataSources?.length) { - (dsSettings as MapDataLayerSettings).additionalDataSources.forEach((ds) => { - datasources.push(mapDataSourceSettingsToDatasource(ds)); + const datasource: Datasource = mapDataSourceSettingsToDatasource(dsSettings, null, includeDataKeys, dataLayerType); + datasources.push(datasource); + if (dsSettings.additionalDataSources?.length) { + dsSettings.additionalDataSources.forEach((ds) => { + const additionalDatasource: Datasource = mapDataSourceSettingsToDatasource(ds); + if (includeDataKeys) { + additionalDatasource.dataKeys.push(...datasource.dataKeys); + } + datasources.push(additionalDatasource); }); } }); diff --git a/ui-ngx/src/app/shared/models/widget/maps/map.models.ts b/ui-ngx/src/app/shared/models/widget/maps/map.models.ts index 0468fe1fab..d3be99541f 100644 --- a/ui-ngx/src/app/shared/models/widget/maps/map.models.ts +++ b/ui-ngx/src/app/shared/models/widget/maps/map.models.ts @@ -24,6 +24,7 @@ import { } from '@shared/models/widget.models'; import { AttributeScope, DataKeyType } from '@shared/models/telemetry/telemetry.models'; import { + deepClone, guid, hashCode, isDefinedAndNotNull, @@ -61,19 +62,47 @@ export interface TbMapDatasource extends Datasource { mapDataIds: string[]; } -export const mapDataSourceSettingsToDatasource = (settings: MapDataSourceSettings, id = guid()): TbMapDatasource => { +export const mapDataSourceSettingsToDatasource = (settings: MapDataSourceSettings | MapDataLayerSettings, + id = guid(), + includeDataKeys = false, dataLayerType?: MapDataLayerType): TbMapDatasource => { + const dataKeys = includeDataKeys ? mapDataLayerDatasourceDataKeys((settings as MapDataLayerSettings), dataLayerType) : []; return { type: settings.dsType, name: settings.dsLabel, deviceId: settings.dsDeviceId, entityAliasId: settings.dsEntityAliasId, filterId: settings.dsFilterId, - dataKeys: [], + dataKeys: dataKeys, latestDataKeys: [], mapDataIds: [id] }; }; +const mapDataLayerDatasourceDataKeys = (settings: MapDataLayerSettings, + dataLayerType: MapDataLayerType): DataKey[] => { + const dataKeys = settings.additionalDataKeys?.length ? deepClone(settings.additionalDataKeys) : []; + switch (dataLayerType) { + case 'trips': + const tripsSettings = settings as TripsDataLayerSettings; + dataKeys.push(tripsSettings.xKey, tripsSettings.yKey); + break; + case 'markers': + const markersSettings = settings as MarkersDataLayerSettings; + dataKeys.push(markersSettings.xKey, markersSettings.yKey); + break; + case 'polygons': + dataKeys.push((settings as PolygonsDataLayerSettings).polygonKey); + break; + case 'circles': + dataKeys.push((settings as CirclesDataLayerSettings).circleKey); + break; + case 'polylines': + dataKeys.push((settings as PolylinesDataLayerSettings).polylineKey); + break; + } + return dataKeys; +}; + export enum DataLayerPatternType { pattern = 'pattern', @@ -133,6 +162,7 @@ export interface DataLayerEditSettings { snappable: boolean; } + export interface MapDataLayerSettings extends MapDataSourceSettings { additionalDataSources?: MapDataSourceSettings[]; additionalDataKeys?: DataKey[]; @@ -140,7 +170,7 @@ export interface MapDataLayerSettings extends MapDataSourceSettings { tooltip: DataLayerTooltipSettings; click: WidgetAction; groups?: string[]; - edit: DataLayerEditSettings; + edit: DataLayerEditSettings; } export const defaultBaseDataLayerSettings = (mapType: MapType): Partial => ({ @@ -156,7 +186,7 @@ export const defaultBaseDataLayerSettings = (mapType: MapType): Partial${entityName}

    Latitude: ${latitude:7}
    Longitude: ${longitude:7}
    Temperature: ${temperature} °C
    See tooltip settings for details' - : '${entityName}

    X Pos: ${xPos:2}
    Y Pos: ${yPos:2}
    Temperature: ${temperature} °C
    See tooltip settings for details', + : '${entityName}

    X Pos: ${xPos:2}
    Y Pos: ${yPos:2}
    Temperature: ${temperature} °C
    See tooltip settings for details', offsetX: 0, offsetY: -1 }, @@ -170,9 +200,9 @@ export const defaultBaseDataLayerSettings = (mapType: MapType): Partial { if (!dataLayer.dsType || ![DatasourceType.function, DatasourceType.device, DatasourceType.entity].includes(dataLayer.dsType)) { @@ -196,7 +226,7 @@ export const mapDataLayerValid = (dataLayer: MapDataLayerSettings, type: MapData case 'markers': const markersDataLayer = dataLayer as MarkersDataLayerSettings; if (!markersDataLayer.xKey?.type || !markersDataLayer.xKey?.name || - !markersDataLayer.yKey?.type || !markersDataLayer.xKey?.name) { + !markersDataLayer.yKey?.type || !markersDataLayer.xKey?.name) { return false; } break; @@ -206,6 +236,12 @@ export const mapDataLayerValid = (dataLayer: MapDataLayerSettings, type: MapData return false; } break; + case 'polylines': + const polylinesDataLayer = dataLayer as PolylinesDataLayerSettings; + if (!polylinesDataLayer.polylineKey?.type || !polylinesDataLayer.polylineKey?.name) { + return false; + } + break; case 'circles': const circlesDataLayer = dataLayer as CirclesDataLayerSettings; if (!circlesDataLayer.circleKey?.type || !circlesDataLayer.circleKey?.name) { @@ -274,6 +310,7 @@ export interface MarkerIconSettings extends BaseMarkerShapeSettings { iconContainer?: MarkerIconContainer; icon: string; } + export interface MarkerClusteringSettings { enable: boolean; zoomOnClick: boolean; @@ -574,8 +611,11 @@ export const defaultBasePolygonsDataLayerSettings = (mapType: MapType): Partial< color: '#3388ff', }, strokeWeight: 3 -} as Partial, defaultBaseDataLayerSettings(mapType), - {label: {show: false}, tooltip: {show: false, pattern: '${entityName}

    TimeStamp: ${ts:7}'}} as Partial) + } as Partial, defaultBaseDataLayerSettings(mapType), + { + label: {show: false}, + tooltip: {show: false, pattern: '${entityName}

    TimeStamp: ${ts:7}'} + } as Partial) export interface CirclesDataLayerSettings extends ShapeDataLayerSettings { circleKey: DataKey; @@ -625,8 +665,45 @@ export const defaultBaseCirclesDataLayerSettings = (mapType: MapType): Partial, defaultBaseDataLayerSettings(mapType), - {label: {show: false}, tooltip: {show: false, pattern: '${entityName}

    TimeStamp: ${ts:7}'}} as Partial) + } as Partial, defaultBaseDataLayerSettings(mapType), + { + label: {show: false}, + tooltip: {show: false, pattern: '${entityName}

    TimeStamp: ${ts:7}'} + } as Partial) + +export interface PolylinesDataLayerSettings extends ShapeDataLayerSettings { + polylineKey: DataKey; +} + +export const defaultPolylinesDataLayerSettings = (mapType: MapType, functionsOnly = false): PolylinesDataLayerSettings => mergeDeep({ + dsType: functionsOnly ? DatasourceType.function : DatasourceType.entity, + dsLabel: functionsOnly ? 'First polyline' : '', + polylineKey: { + name: functionsOnly ? 'f(x)' : 'perimeter', + label: 'perimeter', + type: functionsOnly ? DataKeyType.function : DataKeyType.attribute, + settings: {}, + color: materialColors[0].value + } +} as PolylinesDataLayerSettings, defaultBasePolylinesDataLayerSettings(mapType) as PolylinesDataLayerSettings); + +export const defaultBasePolylinesDataLayerSettings = (mapType: MapType): Partial => mergeDeep({ + fillType: ShapeFillType.color, + fillColor: { + type: DataLayerColorType.constant, + color: 'rgba(51,136,255,0.2)', + }, + strokeColor: { + type: DataLayerColorType.constant, + color: '#3388ff', + }, + strokeWeight: 3 + } as Partial, defaultBaseDataLayerSettings(mapType), + { + label: {show: false}, + tooltip: {show: false, pattern: '${entityName}

    TimeStamp: ${ts:7}'} + } as Partial) + export const defaultMapDataLayerSettings = (mapType: MapType, dataLayerType: MapDataLayerType, functionsOnly = false): MapDataLayerSettings => { switch (dataLayerType) { @@ -638,6 +715,8 @@ export const defaultMapDataLayerSettings = (mapType: MapType, dataLayerType: Map return defaultPolygonsDataLayerSettings(mapType, functionsOnly); case 'circles': return defaultCirclesDataLayerSettings(mapType, functionsOnly); + case 'polylines': + return defaultPolylinesDataLayerSettings(mapType, functionsOnly); } }; @@ -651,6 +730,8 @@ export const defaultBaseMapDataLayerSettings = ( return defaultBasePolygonsDataLayerSettings(mapType) as T; case 'circles': return defaultBaseCirclesDataLayerSettings(mapType) as T; + case 'polylines': + return defaultBasePolylinesDataLayerSettings(mapType) as T; } } @@ -658,10 +739,11 @@ export interface AdditionalMapDataSourceSettings extends MapDataSourceSettings { dataKeys: DataKey[]; } -export const additionalMapDataSourcesToDatasources = (additionalMapDataSources: AdditionalMapDataSourceSettings[]): TbMapDatasource[] => { +export const additionalMapDataSourcesToDatasources = (additionalMapDataSources: AdditionalMapDataSourceSettings[], + includeDataKeys = true): TbMapDatasource[] => { return additionalMapDataSources.map(addDs => { const res = mapDataSourceSettingsToDatasource(addDs); - res.dataKeys = addDs.dataKeys; + res.dataKeys = includeDataKeys ? addDs.dataKeys : []; return res; }); }; @@ -700,13 +782,13 @@ export const additionalMapDataSourceValid = (dataSource: AdditionalMapDataSource }; export const additionalMapDataSourceValidator: ValidatorFn = (control: AbstractControl): ValidationErrors | null => { - const dataSource: AdditionalMapDataSourceSettings = control.value; - if (!additionalMapDataSourceValid(dataSource)) { - return { - dataSource: true - }; - } - return null; + const dataSource: AdditionalMapDataSourceSettings = control.value; + if (!additionalMapDataSourceValid(dataSource)) { + return { + dataSource: true + }; + } + return null; }; export const defaultAdditionalMapDataSourceSettings = (functionsOnly = false): AdditionalMapDataSourceSettings => { @@ -788,6 +870,7 @@ export interface BaseMapSettings { markers: MarkersDataLayerSettings[]; polygons: PolygonsDataLayerSettings[]; circles: CirclesDataLayerSettings[]; + polylines: PolylinesDataLayerSettings[]; additionalDataSources: AdditionalMapDataSourceSettings[]; controlsPosition: MapControlsPosition; zoomActions: MapZoomAction[]; @@ -812,6 +895,7 @@ export const defaultBaseMapSettings: BaseMapSettings = { markers: [], polygons: [], circles: [], + polylines: [], additionalDataSources: [], controlsPosition: MapControlsPosition.topleft, zoomActions: [MapZoomAction.scroll, MapZoomAction.doubleClick, MapZoomAction.controlButtons], @@ -1218,6 +1302,12 @@ export type TbPolyData = L.LatLngTuple[] | L.LatLngTuple[][] | L.LatLngTuple[][] export type TbPolygonCoordinate = L.LatLng | L.LatLng[] | L.LatLng[][]; export type TbPolygonCoordinates = TbPolygonCoordinate[]; +export type TbPolylineRawCoordinate = L.LatLngTuple | L.LatLngTuple[] | L.LatLngTuple[][]; +export type TbPolylineRawCoordinates = TbPolylineRawCoordinate[]; +export type TbPolylineData = L.LatLngTuple[] | L.LatLngTuple[][] | L.LatLngTuple[][][]; +export type TbPolylineCoordinate = L.LatLng | L.LatLng[] | L.LatLng[][]; +export type TbPolylineCoordinates = TbPolylineCoordinate[]; + export interface TbCircleData { latitude: number; longitude: number; diff --git a/ui-ngx/src/app/shared/models/widget/public-api.ts b/ui-ngx/src/app/shared/models/widget/public-api.ts new file mode 100644 index 0000000000..e9240e526a --- /dev/null +++ b/ui-ngx/src/app/shared/models/widget/public-api.ts @@ -0,0 +1,21 @@ +/// +/// Copyright © 2016-2025 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. +/// + +export * from './widget-model.definition'; +export * from './maps/map.models'; +export * from './maps/map-model.definition'; +export * from './maps/marker-shape.models'; +export * from './rpc/knob.component.models'; diff --git a/ui-ngx/src/app/shared/models/widget/widget-model.definition.ts b/ui-ngx/src/app/shared/models/widget/widget-model.definition.ts index 1b54240c09..ccec658766 100644 --- a/ui-ngx/src/app/shared/models/widget/widget-model.definition.ts +++ b/ui-ngx/src/app/shared/models/widget/widget-model.definition.ts @@ -19,16 +19,19 @@ import { Dashboard } from '@shared/models/dashboard.models'; import { EntityAliases } from '@shared/models/alias.models'; import { Filters } from '@shared/models/query/query.models'; import { MapModelDefinition } from '@shared/models/widget/maps/map-model.definition'; +import { ApiUsageModelDefinition } from '@shared/models/widget/home-widgets/api-usage-model.definition'; export interface WidgetModelDefinition { testWidget(widget: Widget): boolean; prepareExportInfo(dashboard: Dashboard, widget: Widget): T; updateFromExportInfo(widget: Widget, entityAliases: EntityAliases, filters: Filters, info: T): void; datasources(widget: Widget): Datasource[]; + hasTimewindow(widget: Widget): boolean; } const widgetModelRegistry: WidgetModelDefinition[] = [ - MapModelDefinition + MapModelDefinition, + ApiUsageModelDefinition ]; export const findWidgetModelDefinition = (widget: Widget): WidgetModelDefinition => { diff --git a/ui-ngx/src/app/shared/pipe/public-api.ts b/ui-ngx/src/app/shared/pipe/public-api.ts index 3d30e2f5e7..5deaff7e1b 100644 --- a/ui-ngx/src/app/shared/pipe/public-api.ts +++ b/ui-ngx/src/app/shared/pipe/public-api.ts @@ -14,6 +14,7 @@ /// limitations under the License. /// +export * from './custom-translate.pipe'; export * from './date-ago.pipe'; export * from './enum-to-array.pipe'; export * from './highlight.pipe'; diff --git a/ui-ngx/src/app/shared/shared.module.ts b/ui-ngx/src/app/shared/shared.module.ts index 9fd6e4a71e..b94e72d76f 100644 --- a/ui-ngx/src/app/shared/shared.module.ts +++ b/ui-ngx/src/app/shared/shared.module.ts @@ -228,6 +228,7 @@ import { JsFuncModuleRowComponent } from '@shared/components/js-func-module-row. import { EntityKeyAutocompleteComponent } from '@shared/components/entity/entity-key-autocomplete.component'; import { DurationLeftPipe } from '@shared/pipe/duration-left.pipe'; import { MqttVersionSelectComponent } from '@shared/components/mqtt-version-select.component'; +import { TimeUnitInputComponent } from '@shared/components/time-unit-input.component'; export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) { return markedOptionsService; @@ -236,6 +237,7 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) @NgModule({ providers: [ DatePipe, + SelectableColumnsPipe, MillisecondsToTimeStringPipe, EnumToArrayPipe, HighlightPipe, @@ -443,6 +445,7 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) ScadaSymbolInputComponent, EntityKeyAutocompleteComponent, MqttVersionSelectComponent, + TimeUnitInputComponent, ], imports: [ CommonModule, @@ -707,6 +710,7 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) ScadaSymbolInputComponent, EntityKeyAutocompleteComponent, MqttVersionSelectComponent, + TimeUnitInputComponent, ] }) export class SharedModule { } diff --git a/ui-ngx/src/assets/dashboard/api_usage.json b/ui-ngx/src/assets/dashboard/api_usage.json index 9738e9ac0f..994b431ade 100644 --- a/ui-ngx/src/assets/dashboard/api_usage.json +++ b/ui-ngx/src/assets/dashboard/api_usage.json @@ -79,7 +79,6 @@ } ], "timewindow": { - "hideInterval": false, "hideAggregation": false, "hideAggInterval": false, "selectedTab": 0, @@ -99,7 +98,8 @@ "settings": { "showTimestamp": true, "displayPagination": true, - "defaultPageSize": 10 + "defaultPageSize": 10, + "tabSortKey": "NAME_ASC" }, "title": "{i18n:api-usage.exceptions}", "dropShadow": true, @@ -121,779 +121,2158 @@ "widgetCss": "", "pageSize": 1024, "noDataDisplayMessage": "", - "configMode": "basic" + "configMode": "basic", + "borderRadius": "4px" }, "id": "a669cf86-e715-efa4-dd9a-b839abf499e9", "typeFullFqn": "system.cards.timeseries_table" }, - "aab68ab5-8e40-8694-c55c-8eb1c89b88fb": { - "typeFullFqn": "system.cards.markdown_card", - "type": "latest", - "sizeX": 5, - "sizeY": 3.5, + "fa938580-33db-f1b3-fafc-bc3e3784ad57": { + "typeFullFqn": "system.time_series_chart", + "type": "timeseries", + "sizeX": 8, + "sizeY": 5, "config": { "datasources": [ { "type": "entity", - "name": null, - "entityAliasId": "40193437-33ac-3172-eefd-0b08eb849062", - "filterId": null, + "entityAliasId": "2e4c97b0-257a-a1b9-690c-141d9bf2ec6f", "dataKeys": [ { - "name": "transportMsgLimit", + "name": "successfulMsgs", "type": "timeseries", - "label": "limit", + "label": "{i18n:api-usage.successful}", "color": "#4caf50", - "settings": {}, - "_hash": 0.5463603803546802, - "units": null, - "decimals": null, - "funcBody": null, - "usePostProcessing": true, - "postFuncBody": "return value !== '' ? parseInt(value, 10) : 0;", - "aggregationType": "NONE" - }, - { - "name": "transportMsgCount", - "type": "timeseries", - "label": "count", - "color": "#f44336", - "settings": {}, - "_hash": 0.5564241862015964, + "settings": { + "yAxisId": "default", + "showInLegend": true, + "dataHiddenByDefault": false, + "type": "line", + "lineSettings": { + "showLine": true, + "step": false, + "stepType": "start", + "smooth": false, + "lineType": "solid", + "lineWidth": 2.5, + "showPoints": false, + "showPointLabel": false, + "pointLabelPosition": "top", + "pointLabelFont": { + "family": "Roboto", + "size": 11, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "pointLabelColor": "rgba(0, 0, 0, 0.76)", + "pointShape": "circle", + "pointSize": 12, + "fillAreaSettings": { + "type": "none", + "opacity": 0.4, + "gradient": { + "start": 100, + "end": 0 + } + } + }, + "barSettings": { + "showBorder": false, + "borderWidth": 2, + "borderRadius": 0, + "showLabel": false, + "labelPosition": "top", + "labelFont": { + "family": "Roboto", + "size": 11, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "labelColor": "rgba(0, 0, 0, 0.76)", + "backgroundSettings": { + "type": "none", + "opacity": 0.4, + "gradient": { + "start": 100, + "end": 0 + } + } + } + }, + "_hash": 0.15490750967648736, + "aggregationType": null, "units": null, "decimals": null, "funcBody": null, - "usePostProcessing": true, - "postFuncBody": "return value !== '' ? parseInt(value, 10) : 0;\n", - "aggregationType": "NONE" + "usePostProcessing": null, + "postFuncBody": null }, { - "name": "transportDataPointsLimit", + "name": "failedMsgs", "type": "timeseries", - "label": "pointsLimit", - "color": "#9c27b0", - "settings": {}, - "_hash": 0.22082255831864894, + "label": "{i18n:api-usage.permanent-failures}", + "color": "#ef5350", + "settings": { + "yAxisId": "default", + "showInLegend": true, + "dataHiddenByDefault": false, + "type": "line", + "lineSettings": { + "showLine": true, + "step": false, + "stepType": "start", + "smooth": false, + "lineType": "solid", + "lineWidth": 2.5, + "showPoints": false, + "showPointLabel": false, + "pointLabelPosition": "top", + "pointLabelFont": { + "family": "Roboto", + "size": 11, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "pointLabelColor": "rgba(0, 0, 0, 0.76)", + "pointShape": "circle", + "pointSize": 12, + "fillAreaSettings": { + "type": "none", + "opacity": 0.4, + "gradient": { + "start": 100, + "end": 0 + } + } + }, + "barSettings": { + "showBorder": false, + "borderWidth": 2, + "borderRadius": 0, + "showLabel": false, + "labelPosition": "top", + "labelFont": { + "family": "Roboto", + "size": 11, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "labelColor": "rgba(0, 0, 0, 0.76)", + "backgroundSettings": { + "type": "none", + "opacity": 0.4, + "gradient": { + "start": 100, + "end": 0 + } + } + } + }, + "_hash": 0.4186621166514697, + "aggregationType": null, "units": null, "decimals": null, "funcBody": null, - "usePostProcessing": true, - "postFuncBody": "return value !== '' ? parseInt(value, 10) : 0;", - "aggregationType": "NONE" + "usePostProcessing": null, + "postFuncBody": null }, { - "name": "transportDataPointsCount", + "name": "tmpFailed", "type": "timeseries", - "label": "pointsCount", - "color": "#8bc34a", - "settings": {}, - "_hash": 0.6340356364819146, + "label": "{i18n:api-usage.processing-failures}", + "color": "#ffc107", + "settings": { + "yAxisId": "default", + "showInLegend": true, + "dataHiddenByDefault": false, + "type": "line", + "lineSettings": { + "showLine": true, + "step": false, + "stepType": "start", + "smooth": false, + "lineType": "solid", + "lineWidth": 2.5, + "showPoints": false, + "showPointLabel": false, + "pointLabelPosition": "top", + "pointLabelFont": { + "family": "Roboto", + "size": 11, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "pointLabelColor": "rgba(0, 0, 0, 0.76)", + "pointShape": "circle", + "pointSize": 12, + "fillAreaSettings": { + "type": "none", + "opacity": 0.4, + "gradient": { + "start": 100, + "end": 0 + } + } + }, + "barSettings": { + "showBorder": false, + "borderWidth": 2, + "borderRadius": 0, + "showLabel": false, + "labelPosition": "top", + "labelFont": { + "family": "Roboto", + "size": 11, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "labelColor": "rgba(0, 0, 0, 0.76)", + "backgroundSettings": { + "type": "none", + "opacity": 0.4, + "gradient": { + "start": 100, + "end": 0 + } + } + } + }, + "_hash": 0.49891007198715376, + "aggregationType": null, "units": null, "decimals": null, "funcBody": null, - "usePostProcessing": true, - "postFuncBody": "return value !== '' ? parseInt(value, 10) : 0;", - "aggregationType": "NONE" - }, + "usePostProcessing": null, + "postFuncBody": null + } + ], + "alarmFilterConfig": { + "statusList": [ + "ACTIVE" + ] + }, + "latestDataKeys": [ { - "name": "transportApiState", - "type": "timeseries", - "label": "title", - "color": "#3f51b5", + "name": "queueName", + "type": "entityField", + "label": "Queue name", + "color": "#ffc107", "settings": {}, - "_hash": 0.6894070537030252, - "units": null, - "decimals": null, - "funcBody": null, - "usePostProcessing": true, - "postFuncBody": "return \"{i18n:api-usage.transport}\";" + "_hash": 0.7021721434431745 }, { - "name": "transportApiState", - "type": "timeseries", - "label": "apiStatus", - "color": "#3f51b5", + "name": "serviceId", + "type": "entityField", + "label": "Service Id", + "color": "#607d8b", "settings": {}, - "_hash": 0.430957831457494, - "aggregationType": "NONE", - "units": null, - "decimals": null, - "funcBody": null, - "usePostProcessing": true, - "postFuncBody": "return value ? value.toLowerCase() : 'enabled';" - }, - { - "name": "transportApiState", - "type": "timeseries", - "label": "unit", - "color": "#8bc34a", - "settings": {}, - "_hash": 0.662147926074595, - "aggregationType": "NONE", - "units": null, - "decimals": null, - "funcBody": null, - "usePostProcessing": true, - "postFuncBody": "return '{i18n:api-usage.messages}';" - }, - { - "name": "transportApiState", - "type": "timeseries", - "label": "pointsUnit", - "color": "#3f51b5", - "settings": {}, - "_hash": 0.44620898738917947, - "aggregationType": "NONE", - "units": null, - "decimals": null, - "funcBody": null, - "usePostProcessing": true, - "postFuncBody": "return '{i18n:api-usage.data-points}';" + "_hash": 0.5924381120750077 } - ], - "alarmFilterConfig": { - "statusList": [ - "ACTIVE" - ] - } + ] } ], "timewindow": { - "displayValue": "", + "hideAggregation": false, + "hideAggInterval": false, "selectedTab": 0, "realtime": { - "realtimeType": 1, - "interval": 1000, - "timewindowMs": 60000, - "quickInterval": "CURRENT_DAY" - }, - "history": { - "historyType": 0, - "interval": 1000, - "timewindowMs": 60000, - "fixedTimewindow": { - "startTimeMs": 1708518962586, - "endTimeMs": 1708605362586 - }, - "quickInterval": "CURRENT_DAY" + "timewindowMs": 3600000, + "interval": 1000 }, "aggregation": { - "type": "AVG", - "limit": 25000 + "type": "NONE", + "limit": 10000 } }, - "showTitle": false, - "backgroundColor": "#fff", + "showTitle": true, + "backgroundColor": "#FFFFFF", "color": "rgba(0, 0, 0, 0.87)", "padding": "0px", "settings": { - "useMarkdownTextFunction": true, - "markdownTextPattern": "", - "markdownTextFunction": "function toShortNumber(number) {\n const rounder = Math.pow(10, 1);\n const powers = [\n {key: 'Q', value: Math.pow(10, 15)},\n {key: 'T', value: Math.pow(10, 12)},\n {key: 'B', value: Math.pow(10, 9)},\n {key: 'M', value: Math.pow(10, 6)},\n {key: 'K', value: 1000}\n ];\n let key = '';\n for (const power of powers) {\n const reduced = number / power.value;\n for (const power of powers) {\n let reduced = number / power.value;\n reduced = Math.round(reduced * rounder) / rounder;\n if (reduced >= 1) {\n number = reduced;\n key = power.key;\n break;\n }\n }\n }\n \n return number + key;\n}\n\nfunction calculateBarValues(count, limit) {\n let apiUsageBar = '0%';\n let apiUsagePercent = '';\n let apiUsageValue = `${toShortNumber(count)} / ∞`;\n if (Number.isFinite(limit) && limit > 0) {\n var percent = Math.min(100, ((count / limit) * 100));\n apiUsageBar = `${percent}%`\n apiUsagePercent = `${percent.toFixed(2)}%`;\n apiUsageValue = `${toShortNumber(count)} / ${toShortNumber(limit)}`;\n }\n \n return [apiUsageBar, apiUsagePercent, apiUsageValue]\n}\n\nconst [apiUsageBar, apiUsagePercent, apiUsageValue] = calculateBarValues(data[0].count, data[0].limit);\nconst [apiUsageBar2, apiUsagePercent2, apiUsageValue2] = calculateBarValues(data[0].pointsCount, data[0].pointsLimit);\n\n\nreturn `
    ` +\n '
    ' +\n '
    ' +\n '
    ' +\n '
    ' +\n `${data[0].title}` +\n '
    ' +\n `
    ${data[0].apiStatus.toUpperCase()}
    ` +\n '
    ' +\n '
    ' +\n '
    ' +\n '
    ' +\n `
    ${data[0].unit}
    ` +\n '
    ' +\n `
    ` +\n '
    ' +\n '
    ' +\n `
    ${apiUsagePercent}
    ` +\n '
    ' +\n `
    ${apiUsageValue}
    ` +\n '
    ' +\n '
    ' +\n '
    ' +\n '
    ' +\n '
    ' +\n `
    ${data[0].pointsUnit}
    ` +\n '
    ' +\n `
    ` +\n '
    ' +\n '
    ' +\n `
    ${apiUsagePercent2}
    ` +\n '
    ' +\n `
    ${apiUsageValue2}
    ` +\n '
    ' +\n '
    ' + \n '
    ' +\n '
    ' +\n '
    ' +\n '
    ' +\n '' +\n '
    '+\n '' +\n '
    ' +\n '
    '\n", - "applyDefaultMarkdownStyle": false, - "markdownCss": "\n" - }, - "title": "Transport", - "showTitleIcon": false, - "iconColor": "rgba(0, 0, 0, 0.87)", - "iconSize": "24px", - "titleTooltip": "", - "dropShadow": true, - "enableFullscreen": false, - "widgetStyle": {}, - "titleStyle": { - "fontSize": "16px", - "fontWeight": 400 - }, - "showLegend": false, - "useDashboardTimewindow": true, - "displayTimewindow": true, - "widgetCss": "", - "pageSize": 1024, - "noDataDisplayMessage": "", - "actions": { - "elementClick": [ - { - "name": "transport_details", - "icon": "insert_chart", - "useShowWidgetActionFunction": null, - "showWidgetActionFunction": "return true;", - "type": "openDashboardState", - "targetDashboardStateId": "transport", - "setEntityId": false, - "stateEntityParamName": null, - "openRightLayout": false, - "openInSeparateDialog": false, - "openInPopover": false, - "id": "a60e09be-1bea-dfc3-6abb-f87e73256899" - } - ] - } - }, - "row": 0, - "col": 0, - "id": "aab68ab5-8e40-8694-c55c-8eb1c89b88fb" - }, - "a84fa70a-ddfa-3b24-9aa4-cf9ce91f919a": { - "typeFullFqn": "system.cards.markdown_card", - "type": "latest", - "sizeX": 5, - "sizeY": 3.5, - "config": { - "datasources": [ - { - "type": "entity", - "name": null, - "entityAliasId": "40193437-33ac-3172-eefd-0b08eb849062", - "filterId": null, - "dataKeys": [ - { - "name": "ruleEngineApiState", - "type": "timeseries", - "label": "apiStatus", - "color": "#2196f3", - "settings": {}, - "_hash": 0.8830669138660703, - "units": null, - "decimals": null, - "funcBody": null, - "usePostProcessing": true, - "postFuncBody": "return value ? value : 'enabled';", - "aggregationType": "NONE" - }, - { - "name": "ruleEngineExecutionLimit", - "type": "timeseries", - "label": "limit", - "color": "#4caf50", - "settings": {}, - "_hash": 0.5463603803546802, - "units": null, - "decimals": null, - "funcBody": null, - "usePostProcessing": true, - "postFuncBody": "return value !== '' ? parseInt(value, 10) : 0;", - "aggregationType": "NONE" - }, - { - "name": "ruleEngineExecutionCount", - "type": "timeseries", - "label": "count", - "color": "#f44336", - "settings": {}, - "_hash": 0.5564241862015964, - "units": null, - "decimals": null, - "funcBody": null, - "usePostProcessing": true, - "postFuncBody": "return value !== '' ? parseInt(value, 10) : 0;", - "aggregationType": "NONE" + "yAxes": { + "default": { + "units": null, + "decimals": 0, + "show": true, + "label": "", + "labelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "600", + "lineHeight": "1" }, - { - "name": "ruleEngineApiState", - "type": "timeseries", - "label": "title", - "color": "#9c27b0", - "settings": {}, - "_hash": 0.3551317421302518, - "units": null, - "decimals": null, - "funcBody": null, - "usePostProcessing": true, - "postFuncBody": "return \"{i18n:api-usage.rule-engine}\";" + "labelColor": "rgba(0, 0, 0, 0.54)", + "position": "left", + "showTickLabels": true, + "tickLabelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" }, - { - "name": "ruleEngineApiState", - "type": "timeseries", - "label": "unit", - "color": "#8bc34a", - "settings": {}, - "_hash": 0.5100381746798048, - "units": null, - "decimals": null, - "funcBody": null, - "usePostProcessing": true, - "postFuncBody": "return \"{i18n:api-usage.executions}\";" - } - ], - "alarmFilterConfig": { - "statusList": [ - "ACTIVE" - ] + "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormatter": "var rounder = Math.pow(10, 1);\nvar powers = [\n {key: 'Q', value: Math.pow(10, 15)},\n {key: 'T', value: Math.pow(10, 12)},\n {key: 'B', value: Math.pow(10, 9)},\n {key: 'M', value: Math.pow(10, 6)},\n {key: 'K', value: 1000}\n];\n\nvar key = '';\n\nfor (var i = 0; i < powers.length; i++) {\n var reduced = value / powers[i].value;\n reduced = Math.round(reduced * rounder) / rounder;\n if (reduced >= 1) {\n value = reduced;\n key = powers[i].key;\n break;\n }\n}\nreturn value + key;", + "showTicks": true, + "ticksColor": "rgba(0, 0, 0, 0.54)", + "showLine": true, + "lineColor": "rgba(0, 0, 0, 0.54)", + "showSplitLines": true, + "splitLinesColor": "rgba(0, 0, 0, 0.12)", + "id": "default", + "order": 0, + "min": null, + "max": null } - } - ], - "timewindow": { - "displayValue": "", - "selectedTab": 0, - "realtime": { - "realtimeType": 1, - "interval": 1000, - "timewindowMs": 60000, - "quickInterval": "CURRENT_DAY" }, - "history": { - "historyType": 0, - "interval": 1000, - "timewindowMs": 60000, - "fixedTimewindow": { - "startTimeMs": 1708518962586, - "endTimeMs": 1708605362586 + "thresholds": [], + "dataZoom": false, + "stack": false, + "xAxis": { + "show": true, + "label": "", + "labelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "600", + "lineHeight": "1" }, - "quickInterval": "CURRENT_DAY" + "labelColor": "rgba(0, 0, 0, 0.54)", + "position": "bottom", + "showTickLabels": true, + "tickLabelFont": { + "family": "Roboto", + "size": 10, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, + "showTicks": true, + "ticksColor": "rgba(0, 0, 0, 0.54)", + "showLine": true, + "lineColor": "rgba(0, 0, 0, 0.54)", + "showSplitLines": true, + "splitLinesColor": "rgba(0, 0, 0, 0.12)" }, - "aggregation": { - "type": "AVG", - "limit": 25000 - } - }, - "showTitle": false, - "backgroundColor": "#fff", - "color": "rgba(0, 0, 0, 0.87)", - "padding": "0px", - "settings": { - "useMarkdownTextFunction": true, - "markdownTextPattern": "", - "markdownTextFunction": "function toShortNumber(number) {\n const rounder = Math.pow(10, 1);\n const powers = [\n {key: 'Q', value: Math.pow(10, 15)},\n {key: 'T', value: Math.pow(10, 12)},\n {key: 'B', value: Math.pow(10, 9)},\n {key: 'M', value: Math.pow(10, 6)},\n {key: 'K', value: 1000}\n ];\n let key = '';\n for (const power of powers) {\n let reduced = number / power.value;\n reduced = Math.round(reduced * rounder) / rounder;\n if (reduced >= 1) {\n number = reduced;\n key = power.key;\n break;\n }\n }\n \n return number + key;\n}\n\nfunction calculateBarValues(count, limit) {\n let apiUsageBar = '0%';\n let apiUsagePercent = '';\n let apiUsageValue = `${toShortNumber(count)} / ∞`;\n if (Number.isFinite(limit) && limit > 0) {\n var percent = Math.min(100, ((count / limit) * 100));\n apiUsageBar = `${percent}%`\n apiUsagePercent = `${percent.toFixed(2)}%`;\n apiUsageValue = `${toShortNumber(count)} / ${toShortNumber(limit)}`;\n }\n \n return [apiUsageBar, apiUsagePercent, apiUsageValue]\n}\n\nconst [apiUsageBar, apiUsagePercent, apiUsageValue] = calculateBarValues(data[0].count, data[0].limit);\n\n\nreturn `
    ` +\n '
    ' +\n '
    ' +\n '
    ' +\n '
    ${title}
    ' +\n `
    ${data[0].apiStatus.toUpperCase()}
    ` +\n '
    ' +\n '
    ' +\n `
    ${data[0].unit}
    ` +\n '
    ' +\n `
    ` +\n '
    ' +\n '
    ' +\n `
    ${apiUsagePercent}
    ` +\n '
    ' +\n `
    ${apiUsageValue}
    ` +\n '
    ' +\n '
    ' +\n '
    ' +\n '
    ' +\n '' +\n '
    '+\n '' +\n '' +\n '
    ' +\n '
    '\n", - "applyDefaultMarkdownStyle": false, - "markdownCss": "\n" - }, - "title": "Rule Engine execution", - "showTitleIcon": false, - "iconColor": "rgba(0, 0, 0, 0.87)", - "iconSize": "24px", - "titleTooltip": "", - "dropShadow": true, - "enableFullscreen": false, - "widgetStyle": {}, - "titleStyle": { - "fontSize": "16px", - "fontWeight": 400 - }, - "showLegend": false, - "useDashboardTimewindow": true, - "displayTimewindow": true, - "widgetCss": "", - "pageSize": 1024, - "noDataDisplayMessage": "", - "actions": { - "elementClick": [ - { - "name": "rule_engine_execution_details", - "icon": "insert_chart", - "type": "openDashboardState", - "targetDashboardStateId": "rule_engine_execution", - "setEntityId": false, - "stateEntityParamName": null, - "openRightLayout": false, - "id": "3c30248f-0cd8-fb97-a917-bc1e09984a79" + "noAggregationBarWidthSettings": { + "strategy": "group", + "groupWidth": { + "relative": false, + "relativeWidth": 2, + "absoluteWidth": 1800000 }, - { - "name": "rule_engine_statistics_details", - "icon": "show_chart", - "type": "openDashboardState", - "targetDashboardStateId": "rule_engine_statistics", - "setEntityId": false, - "stateEntityParamName": null, - "openRightLayout": false, - "id": "04e4565a-9e24-23df-f376-f2ec70a8165f" + "barWidth": { + "relative": true, + "relativeWidth": 2, + "absoluteWidth": 1000 } - ] - } + }, + "showLegend": true, + "legendLabelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "legendLabelColor": "rgba(0, 0, 0, 0.76)", + "legendConfig": { + "direction": "column", + "position": "bottom", + "sortDataKeys": false, + "showMin": true, + "showMax": true, + "showAvg": false, + "showTotal": true, + "showLatest": false, + "valueFormat": null + }, + "showTooltip": true, + "tooltipTrigger": "axis", + "tooltipValueFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "500", + "lineHeight": "16px" + }, + "tooltipValueColor": "rgba(0, 0, 0, 0.76)", + "tooltipShowDate": true, + "tooltipDateFormat": { + "format": "yyyy-MM-dd HH:mm:ss", + "lastUpdateAgo": false, + "custom": false, + "auto": true, + "autoDateFormatSettings": {} + }, + "tooltipDateFont": { + "family": "Roboto", + "size": 11, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "tooltipDateColor": "rgba(0, 0, 0, 0.76)", + "tooltipDateInterval": true, + "tooltipHideZeroValues": true, + "tooltipBackgroundColor": "rgba(255, 255, 255, 0.76)", + "tooltipBackgroundBlur": 4, + "animation": { + "animation": true, + "animationThreshold": 2000, + "animationDuration": 300, + "animationEasing": "cubicOut", + "animationDelay": 0, + "animationDurationUpdate": 300, + "animationEasingUpdate": "cubicOut", + "animationDelayUpdate": 0 + }, + "background": { + "type": "color", + "color": "#fff", + "overlay": { + "enabled": false, + "color": "rgba(255,255,255,0.72)", + "blur": 3 + } + }, + "padding": "12px", + "comparisonEnabled": false, + "timeForComparison": "previousInterval", + "comparisonCustomIntervalValue": 7200000, + "comparisonXAxis": { + "show": true, + "label": "", + "labelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "600", + "lineHeight": "1" + }, + "labelColor": "rgba(0, 0, 0, 0.54)", + "position": "top", + "showTickLabels": true, + "tickLabelFont": { + "family": "Roboto", + "size": 10, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, + "showTicks": true, + "ticksColor": "rgba(0, 0, 0, 0.54)", + "showLine": true, + "lineColor": "rgba(0, 0, 0, 0.54)", + "showSplitLines": true, + "splitLinesColor": "rgba(0, 0, 0, 0.12)" + }, + "grid": { + "show": false, + "backgroundColor": null, + "borderWidth": 1, + "borderColor": "#ccc" + }, + "legendColumnTitleFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "legendColumnTitleColor": "rgba(0, 0, 0, 0.38)", + "legendValueFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "500", + "lineHeight": "16px" + }, + "legendValueColor": "rgba(0, 0, 0, 0.87)", + "tooltipLabelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "tooltipLabelColor": "rgba(0, 0, 0, 0.76)" + }, + "title": "{i18n:api-usage.queue-stats}", + "dropShadow": true, + "enableFullscreen": true, + "titleStyle": null, + "configMode": "basic", + "actions": {}, + "showTitleIcon": false, + "titleIcon": "thermostat", + "iconColor": "#1F6BDD", + "useDashboardTimewindow": false, + "displayTimewindow": true, + "titleFont": { + "size": 16, + "sizeUnit": "px", + "family": "Roboto", + "weight": "500", + "style": "normal", + "lineHeight": "24px" + }, + "titleColor": "rgba(0, 0, 0, 0.87)", + "titleTooltip": "", + "widgetStyle": {}, + "widgetCss": "", + "pageSize": 1024, + "units": "", + "decimals": null, + "noDataDisplayMessage": "", + "timewindowStyle": { + "showIcon": false, + "iconSize": "24px", + "icon": null, + "iconPosition": "left", + "font": { + "size": 12, + "sizeUnit": "px", + "family": "Roboto", + "weight": "400", + "style": "normal", + "lineHeight": "16px" + }, + "color": "rgba(0, 0, 0, 0.38)", + "displayTypePrefix": true + }, + "margin": "0px", + "borderRadius": "4px", + "iconSize": "0px" }, "row": 0, "col": 0, - "id": "a84fa70a-ddfa-3b24-9aa4-cf9ce91f919a" + "id": "fa938580-33db-f1b3-fafc-bc3e3784ad57" }, - "d70d26d4-e22d-4ca9-9ea7-f9c87c093321": { - "typeFullFqn": "system.cards.markdown_card", - "type": "latest", - "sizeX": 5, - "sizeY": 3.5, + "2ee89893-4e38-5331-95b7-3fd4f310c5a7": { + "typeFullFqn": "system.time_series_chart", + "type": "timeseries", + "sizeX": 8, + "sizeY": 5, "config": { "datasources": [ { "type": "entity", - "name": null, - "entityAliasId": "40193437-33ac-3172-eefd-0b08eb849062", - "filterId": null, + "entityAliasId": "2e4c97b0-257a-a1b9-690c-141d9bf2ec6f", "dataKeys": [ { - "name": "jsExecutionApiState", + "name": "timeoutMsgs", "type": "timeseries", - "label": "jsApiState", - "color": "#2196f3", - "settings": {}, - "_hash": 0.8830669138660703, + "label": "{i18n:api-usage.permanent-timeouts}", + "color": "#4caf50", + "settings": { + "yAxisId": "default", + "showInLegend": true, + "dataHiddenByDefault": false, + "type": "line", + "lineSettings": { + "showLine": true, + "step": false, + "stepType": "start", + "smooth": false, + "lineType": "solid", + "lineWidth": 2.5, + "showPoints": false, + "showPointLabel": false, + "pointLabelPosition": "top", + "pointLabelFont": { + "family": "Roboto", + "size": 11, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "pointLabelColor": "rgba(0, 0, 0, 0.76)", + "pointShape": "circle", + "pointSize": 12, + "fillAreaSettings": { + "type": "none", + "opacity": 0.4, + "gradient": { + "start": 100, + "end": 0 + } + } + }, + "barSettings": { + "showBorder": false, + "borderWidth": 2, + "borderRadius": 0, + "showLabel": false, + "labelPosition": "top", + "labelFont": { + "family": "Roboto", + "size": 11, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "labelColor": "rgba(0, 0, 0, 0.76)", + "backgroundSettings": { + "type": "none", + "opacity": 0.4, + "gradient": { + "start": 100, + "end": 0 + } + } + } + }, + "_hash": 0.565222981550328, + "aggregationType": null, "units": null, "decimals": null, "funcBody": null, - "usePostProcessing": true, - "postFuncBody": "return value ? value : 'ENABLED';", - "aggregationType": "NONE" + "usePostProcessing": null, + "postFuncBody": null }, { - "name": "jsExecutionLimit", + "name": "tmpTimeout", "type": "timeseries", - "label": "jsLimit", - "color": "#4caf50", - "settings": {}, - "_hash": 0.5463603803546802, - "units": null, - "decimals": null, - "funcBody": null, - "usePostProcessing": true, - "postFuncBody": "return value !== '' ? parseInt(value, 10) : 0;", - "aggregationType": "NONE" - }, - { - "name": "jsExecutionCount", - "type": "timeseries", - "label": "jsCount", - "color": "#f44336", - "settings": {}, - "_hash": 0.5564241862015964, - "units": null, - "decimals": null, - "funcBody": null, - "usePostProcessing": true, - "postFuncBody": "return value !== '' ? parseInt(value, 10) : 0;", - "aggregationType": "NONE" - }, - { - "name": "jsExecutionApiState", - "type": "timeseries", - "label": "title", + "label": "{i18n:api-usage.processing-timeouts}", "color": "#9c27b0", - "settings": {}, - "_hash": 0.7673280949238444, - "units": null, - "decimals": null, - "funcBody": null, - "usePostProcessing": true, - "postFuncBody": "return \"{i18n:api-usage.scripts}\";", - "aggregationType": "NONE" - }, - { - "name": "jsExecutionApiState", - "type": "timeseries", - "label": "jsUnit", - "color": "#8bc34a", - "settings": {}, - "_hash": 0.7926918686567068, - "units": null, - "decimals": null, - "funcBody": null, - "usePostProcessing": true, - "postFuncBody": "return \"{i18n:api-usage.javascript}\";", - "aggregationType": "NONE" - }, - { - "name": "tbelExecutionApiState", - "type": "timeseries", - "label": "tbelApiState", - "color": "#3f51b5", - "settings": {}, - "_hash": 0.2002981454581909, - "aggregationType": "NONE", - "units": null, - "decimals": null, - "funcBody": null, - "usePostProcessing": true, - "postFuncBody": "return value ? value : 'ENABLED';" - }, - { - "name": "tbelExecutionLimit", - "type": "timeseries", - "label": "tbelLimit", - "color": "#ffeb3b", - "settings": {}, - "_hash": 0.5039854873031677, - "aggregationType": "NONE", - "units": null, - "decimals": null, - "funcBody": null, - "usePostProcessing": true, - "postFuncBody": "return value !== '' ? parseInt(value, 10) : 0;" - }, - { - "name": "tbelExecutionCount", - "type": "timeseries", - "label": "tbelCount", - "color": "#e91e63", - "settings": {}, - "_hash": 0.9506731992087107, - "aggregationType": "NONE", - "units": null, - "decimals": null, - "funcBody": null, - "usePostProcessing": true, - "postFuncBody": "return value !== '' ? parseInt(value, 10) : 0;" - }, - { - "name": "tbelExecutionApiState", - "type": "timeseries", - "label": "tbelUnit", - "color": "#ffeb3b", - "settings": {}, - "_hash": 0.3673530683177082, - "aggregationType": "NONE", + "settings": { + "yAxisId": "default", + "showInLegend": true, + "dataHiddenByDefault": false, + "type": "line", + "lineSettings": { + "showLine": true, + "step": false, + "stepType": "start", + "smooth": false, + "lineType": "solid", + "lineWidth": 2.5, + "showPoints": false, + "showPointLabel": false, + "pointLabelPosition": "top", + "pointLabelFont": { + "family": "Roboto", + "size": 11, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "pointLabelColor": "rgba(0, 0, 0, 0.76)", + "pointShape": "circle", + "pointSize": 12, + "fillAreaSettings": { + "type": "none", + "opacity": 0.4, + "gradient": { + "start": 100, + "end": 0 + } + } + }, + "barSettings": { + "showBorder": false, + "borderWidth": 2, + "borderRadius": 0, + "showLabel": false, + "labelPosition": "top", + "labelFont": { + "family": "Roboto", + "size": 11, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "labelColor": "rgba(0, 0, 0, 0.76)", + "backgroundSettings": { + "type": "none", + "opacity": 0.4, + "gradient": { + "start": 100, + "end": 0 + } + } + } + }, + "_hash": 0.2679547062508352, + "aggregationType": null, "units": null, "decimals": null, "funcBody": null, - "usePostProcessing": true, - "postFuncBody": "return \"{i18n:api-usage.tbel}\";" + "usePostProcessing": null, + "postFuncBody": null } ], "alarmFilterConfig": { "statusList": [ "ACTIVE" ] - } - } - ], - "timewindow": { - "displayValue": "", - "selectedTab": 0, - "realtime": { - "realtimeType": 1, - "interval": 1000, - "timewindowMs": 60000, - "quickInterval": "CURRENT_DAY" - }, - "history": { - "historyType": 0, - "interval": 1000, - "timewindowMs": 60000, - "fixedTimewindow": { - "startTimeMs": 1708518962586, - "endTimeMs": 1708605362586 }, - "quickInterval": "CURRENT_DAY" - }, - "aggregation": { - "type": "AVG", - "limit": 25000 - } - }, - "showTitle": false, - "backgroundColor": "#fff", - "color": "rgba(0, 0, 0, 0.87)", - "padding": "0px", - "settings": { - "useMarkdownTextFunction": true, - "markdownTextPattern": "", - "markdownTextFunction": "function toShortNumber(number) {\n const rounder = Math.pow(10, 1);\n const powers = [\n {key: 'Q', value: Math.pow(10, 15)},\n {key: 'T', value: Math.pow(10, 12)},\n {key: 'B', value: Math.pow(10, 9)},\n {key: 'M', value: Math.pow(10, 6)},\n {key: 'K', value: 1000}\n ];\n let key = '';\n for (const power of powers) {\n const reduced = number / power.value;\n for (const power of powers) {\n let reduced = number / power.value;\n reduced = Math.round(reduced * rounder) / rounder;\n if (reduced >= 1) {\n number = reduced;\n key = power.key;\n break;\n }\n }\n }\n \n return number + key;\n}\n\nfunction calculateBarValues(count, limit) {\n let apiUsageBar = '0%';\n let apiUsagePercent = '';\n let apiUsageValue = `${toShortNumber(count)} / ∞`;\n if (Number.isFinite(limit) && limit > 0) {\n var percent = Math.min(100, ((count / limit) * 100));\n apiUsageBar = `${percent}%`\n apiUsagePercent = `${percent.toFixed(2)}%`;\n apiUsageValue = `${toShortNumber(count)} / ${toShortNumber(limit)}`;\n }\n \n return [apiUsageBar, apiUsagePercent, apiUsageValue]\n}\n\nconst [jsUsageBar, jsUsagePercent, jsUsageValue] = calculateBarValues(data[0].jsCount, data[0].jsLimit);\nconst [tbelUsageBar, tbelUsagePercent, tbelUsageValue] = calculateBarValues(data[0].tbelCount, data[0].tbelLimit);\n\nconst jsApiState = data[0].jsApiState;\nconst tbelApiState = data[0].tbelApiState;\nlet currentState;\nif (jsApiState === 'DISABLED' || tbelApiState === 'DISABLED') {\n currentState = 'DISABLED';\n} else if (jsApiState === 'WARNING' || tbelApiState === 'WARNING') {\n currentState = 'WARNING';\n} else {\n currentState = 'ENABLED';\n}\nconst cardClass = currentState.toLowerCase()\n\nreturn `
    ` +\n '
    ' +\n '
    ' +\n '
    ' +\n '
    ' +\n `${data[0].title}` +\n '
    ' +\n `
    ${currentState}
    ` +\n '
    ' +\n '
    ' +\n '
    ' +\n '
    ' +\n `
    ${data[0].jsUnit}
    ` +\n '
    ' +\n `
    ` +\n '
    ' +\n '
    ' +\n `
    ${jsUsagePercent}
    ` +\n '
    ' +\n `
    ${jsUsageValue}
    ` +\n '
    ' +\n '
    ' +\n '
    ' +\n '
    ' +\n '
    ' +\n `
    ${data[0].tbelUnit}
    ` +\n '
    ' +\n `
    ` +\n '
    ' +\n '
    ' +\n `
    ${tbelUsagePercent}
    ` +\n '
    ' +\n `
    ${tbelUsageValue}
    ` +\n '
    ' +\n '
    ' + \n '
    ' +\n '
    ' +\n '
    ' +\n '
    ' +\n '' +\n '
    '+\n '' +\n '
    ' +\n '
    '\n", - "applyDefaultMarkdownStyle": false, - "markdownCss": "\n" - }, - "title": "JavaScript functions", - "showTitleIcon": false, - "iconColor": "rgba(0, 0, 0, 0.87)", - "iconSize": "24px", - "titleTooltip": "", - "dropShadow": true, - "enableFullscreen": false, - "widgetStyle": {}, - "titleStyle": { - "fontSize": "16px", - "fontWeight": 400 - }, - "showLegend": false, - "useDashboardTimewindow": true, - "displayTimewindow": true, - "widgetCss": "", - "pageSize": 1024, - "noDataDisplayMessage": "", - "actions": { - "elementClick": [ - { - "name": "script_functions_details", - "icon": "insert_chart", - "useShowWidgetActionFunction": null, - "showWidgetActionFunction": "return true;", - "type": "openDashboardState", - "targetDashboardStateId": "script_functions", - "setEntityId": false, - "stateEntityParamName": null, - "openRightLayout": false, - "openInSeparateDialog": false, - "openInPopover": false, - "id": "d4961bea-84de-e1af-e50f-666b98d34cd5" - } - ] - } - }, - "row": 0, - "col": 0, - "id": "d70d26d4-e22d-4ca9-9ea7-f9c87c093321" - }, - "4d3ea95c-3188-9872-1817-2f989c7729e0": { - "typeFullFqn": "system.cards.markdown_card", - "type": "latest", - "sizeX": 5, - "sizeY": 3.5, - "config": { - "datasources": [ - { - "type": "entity", - "name": null, - "entityAliasId": "40193437-33ac-3172-eefd-0b08eb849062", - "filterId": null, - "dataKeys": [ - { - "name": "storageDataPointsLimit", - "type": "timeseries", - "label": "limit", - "color": "#4caf50", - "settings": {}, - "_hash": 0.5463603803546802, - "units": null, - "decimals": null, - "funcBody": null, - "usePostProcessing": true, - "postFuncBody": "return value !== '' ? parseInt(value, 10) : 0;", - "aggregationType": "NONE" - }, + "latestDataKeys": [ { - "name": "storageDataPointsCount", - "type": "timeseries", - "label": "count", + "name": "queueName", + "type": "entityField", + "label": "Queue name", "color": "#f44336", "settings": {}, - "_hash": 0.5564241862015964, - "units": null, - "decimals": null, - "funcBody": null, - "usePostProcessing": true, - "postFuncBody": "return value !== '' ? parseInt(value, 10) : 0;", - "aggregationType": "NONE" + "_hash": 0.7066844328378095 }, { - "name": "dbApiState", - "type": "timeseries", - "label": "apiStatus", + "name": "serviceId", + "type": "entityField", + "label": "Service Id", "color": "#ffc107", "settings": {}, - "_hash": 0.8737107059960671, - "units": null, - "decimals": null, - "funcBody": null, - "usePostProcessing": true, - "postFuncBody": "return value ? value : 'enabled';", - "aggregationType": "NONE" - }, - { - "name": "dbApiState", - "type": "timeseries", - "label": "title", - "color": "#9c27b0", - "settings": {}, - "_hash": 0.6301889725474652, - "units": null, - "decimals": null, - "funcBody": null, - "usePostProcessing": true, - "postFuncBody": "return \"{i18n:api-usage.telemetry}\";" - }, - { - "name": "dbApiState", - "type": "timeseries", - "label": "unit", - "color": "#8bc34a", - "settings": {}, - "_hash": 0.0027742924142306613, - "units": null, - "decimals": null, - "funcBody": null, - "usePostProcessing": true, - "postFuncBody": "return \"{i18n:api-usage.data-points-storage-days}\";" + "_hash": 0.1371570237026627 } - ], - "alarmFilterConfig": { - "statusList": [ - "ACTIVE" - ] - } + ] } ], "timewindow": { - "displayValue": "", + "hideAggregation": false, + "hideAggInterval": false, "selectedTab": 0, "realtime": { - "realtimeType": 1, - "interval": 1000, - "timewindowMs": 60000, - "quickInterval": "CURRENT_DAY" + "timewindowMs": 3600000, + "interval": 1000 }, - "history": { - "historyType": 0, - "interval": 1000, - "timewindowMs": 60000, - "fixedTimewindow": { - "startTimeMs": 1708518962586, - "endTimeMs": 1708605362586 - }, - "quickInterval": "CURRENT_DAY" + "aggregation": { + "type": "NONE", + "limit": 10000 + } + }, + "showTitle": true, + "backgroundColor": "#FFFFFF", + "color": "rgba(0, 0, 0, 0.87)", + "padding": "0px", + "settings": { + "yAxes": { + "default": { + "units": null, + "decimals": 0, + "show": true, + "label": "", + "labelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "600", + "lineHeight": "1" + }, + "labelColor": "rgba(0, 0, 0, 0.54)", + "position": "left", + "showTickLabels": true, + "tickLabelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormatter": "var rounder = Math.pow(10, 1);\nvar powers = [\n {key: 'Q', value: Math.pow(10, 15)},\n {key: 'T', value: Math.pow(10, 12)},\n {key: 'B', value: Math.pow(10, 9)},\n {key: 'M', value: Math.pow(10, 6)},\n {key: 'K', value: 1000}\n];\n\nvar key = '';\n\nfor (var i = 0; i < powers.length; i++) {\n var reduced = value / powers[i].value;\n reduced = Math.round(reduced * rounder) / rounder;\n if (reduced >= 1) {\n value = reduced;\n key = powers[i].key;\n break;\n }\n}\nreturn value + key;", + "showTicks": true, + "ticksColor": "rgba(0, 0, 0, 0.54)", + "showLine": true, + "lineColor": "rgba(0, 0, 0, 0.54)", + "showSplitLines": true, + "splitLinesColor": "rgba(0, 0, 0, 0.12)", + "id": "default", + "order": 0, + "min": null, + "max": null + } + }, + "thresholds": [], + "dataZoom": false, + "stack": false, + "xAxis": { + "show": true, + "label": "", + "labelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "600", + "lineHeight": "1" + }, + "labelColor": "rgba(0, 0, 0, 0.54)", + "position": "bottom", + "showTickLabels": true, + "tickLabelFont": { + "family": "Roboto", + "size": 10, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, + "showTicks": true, + "ticksColor": "rgba(0, 0, 0, 0.54)", + "showLine": true, + "lineColor": "rgba(0, 0, 0, 0.54)", + "showSplitLines": true, + "splitLinesColor": "rgba(0, 0, 0, 0.12)" + }, + "noAggregationBarWidthSettings": { + "strategy": "group", + "groupWidth": { + "relative": false, + "relativeWidth": 2, + "absoluteWidth": 1800000 + }, + "barWidth": { + "relative": true, + "relativeWidth": 2, + "absoluteWidth": 1000 + } + }, + "showLegend": true, + "legendLabelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "legendLabelColor": "rgba(0, 0, 0, 0.76)", + "legendConfig": { + "direction": "column", + "position": "bottom", + "sortDataKeys": false, + "showMin": true, + "showMax": true, + "showAvg": false, + "showTotal": true, + "showLatest": false, + "valueFormat": null + }, + "showTooltip": true, + "tooltipTrigger": "axis", + "tooltipValueFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "500", + "lineHeight": "16px" + }, + "tooltipValueColor": "rgba(0, 0, 0, 0.76)", + "tooltipShowDate": true, + "tooltipDateFormat": { + "format": "yyyy-MM-dd HH:mm:ss", + "lastUpdateAgo": false, + "custom": false, + "auto": true, + "autoDateFormatSettings": {} + }, + "tooltipDateFont": { + "family": "Roboto", + "size": 11, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "tooltipDateColor": "rgba(0, 0, 0, 0.76)", + "tooltipDateInterval": true, + "tooltipHideZeroValues": true, + "tooltipBackgroundColor": "rgba(255, 255, 255, 0.76)", + "tooltipBackgroundBlur": 4, + "animation": { + "animation": true, + "animationThreshold": 2000, + "animationDuration": 300, + "animationEasing": "cubicOut", + "animationDelay": 0, + "animationDurationUpdate": 300, + "animationEasingUpdate": "cubicOut", + "animationDelayUpdate": 0 + }, + "background": { + "type": "color", + "color": "#fff", + "overlay": { + "enabled": false, + "color": "rgba(255,255,255,0.72)", + "blur": 3 + } + }, + "padding": "12px", + "comparisonEnabled": false, + "timeForComparison": "previousInterval", + "comparisonCustomIntervalValue": 7200000, + "comparisonXAxis": { + "show": true, + "label": "", + "labelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "600", + "lineHeight": "1" + }, + "labelColor": "rgba(0, 0, 0, 0.54)", + "position": "top", + "showTickLabels": true, + "tickLabelFont": { + "family": "Roboto", + "size": 10, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, + "showTicks": true, + "ticksColor": "rgba(0, 0, 0, 0.54)", + "showLine": true, + "lineColor": "rgba(0, 0, 0, 0.54)", + "showSplitLines": true, + "splitLinesColor": "rgba(0, 0, 0, 0.12)" + }, + "grid": { + "show": false, + "backgroundColor": null, + "borderWidth": 1, + "borderColor": "#ccc" + }, + "legendColumnTitleFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "legendColumnTitleColor": "rgba(0, 0, 0, 0.38)", + "legendValueFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "500", + "lineHeight": "16px" + }, + "legendValueColor": "rgba(0, 0, 0, 0.87)", + "tooltipLabelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "tooltipLabelColor": "rgba(0, 0, 0, 0.76)" + }, + "title": "{i18n:api-usage.processing-failures-and-timeouts}", + "dropShadow": true, + "enableFullscreen": true, + "titleStyle": null, + "configMode": "basic", + "actions": {}, + "showTitleIcon": false, + "titleIcon": "thermostat", + "iconColor": "#1F6BDD", + "useDashboardTimewindow": false, + "displayTimewindow": true, + "titleFont": { + "size": 16, + "sizeUnit": "px", + "family": "Roboto", + "weight": "500", + "style": "normal", + "lineHeight": "24px" + }, + "titleColor": "rgba(0, 0, 0, 0.87)", + "titleTooltip": "", + "widgetStyle": {}, + "widgetCss": "", + "pageSize": 1024, + "units": "", + "decimals": null, + "noDataDisplayMessage": "", + "timewindowStyle": { + "showIcon": false, + "iconSize": "24px", + "icon": null, + "iconPosition": "left", + "font": { + "size": 12, + "sizeUnit": "px", + "family": "Roboto", + "weight": "400", + "style": "normal", + "lineHeight": "16px" + }, + "color": "rgba(0, 0, 0, 0.38)", + "displayTypePrefix": true + }, + "margin": "0px", + "borderRadius": "4px", + "iconSize": "0px" + }, + "row": 0, + "col": 0, + "id": "2ee89893-4e38-5331-95b7-3fd4f310c5a7" + }, + "85240e8c-7af7-90a9-ad0a-726013c479a6": { + "typeFullFqn": "system.time_series_chart", + "type": "timeseries", + "sizeX": 8, + "sizeY": 5, + "config": { + "datasources": [ + { + "type": "entity", + "name": null, + "entityAliasId": "40193437-33ac-3172-eefd-0b08eb849062", + "filterId": null, + "dataKeys": [ + { + "name": "transportMsgCountHourly", + "type": "timeseries", + "label": "{i18n:api-usage.transport-messages}", + "color": "#2196f3", + "settings": { + "excludeFromStacking": false, + "hideDataByDefault": false, + "disableDataHiding": false, + "removeFromLegend": false, + "showLines": false, + "fillLines": false, + "showPoints": false, + "showPointShape": "circle", + "pointShapeFormatter": "", + "showPointsLineWidth": 5, + "showPointsRadius": 3, + "showSeparateAxis": false, + "axisPosition": "left", + "thresholds": [ + { + "thresholdValueSource": "predefinedValue" + } + ], + "comparisonSettings": { + "showValuesForComparison": true + }, + "type": "bar" + }, + "_hash": 0.0661644137210089, + "units": null, + "decimals": null, + "funcBody": null, + "usePostProcessing": false, + "postFuncBody": null, + "aggregationType": null + } + ], + "alarmFilterConfig": { + "statusList": [ + "ACTIVE" + ] + } + } + ], + "timewindow": { + "selectedTab": 0, + "realtime": { + "realtimeType": 0, + "interval": 3600000, + "timewindowMs": 86400000 + }, + "aggregation": { + "type": "NONE", + "limit": 50000 + } + }, + "showTitle": true, + "backgroundColor": "#FFFFFF", + "color": "rgba(0, 0, 0, 0.87)", + "padding": "0px", + "settings": { + "yAxes": { + "default": { + "units": null, + "decimals": 0, + "show": true, + "label": "", + "labelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "600", + "lineHeight": "1" + }, + "labelColor": "rgba(0, 0, 0, 0.54)", + "position": "left", + "showTickLabels": true, + "tickLabelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormatter": "var rounder = Math.pow(10, 1);\nvar powers = [\n {key: 'Q', value: Math.pow(10, 15)},\n {key: 'T', value: Math.pow(10, 12)},\n {key: 'B', value: Math.pow(10, 9)},\n {key: 'M', value: Math.pow(10, 6)},\n {key: 'K', value: 1000}\n];\n\nvar key = '';\n\nfor (var i = 0; i < powers.length; i++) {\n var reduced = value / powers[i].value;\n reduced = Math.round(reduced * rounder) / rounder;\n if (reduced >= 1) {\n value = reduced;\n key = powers[i].key;\n break;\n }\n}\nreturn value + key;", + "showTicks": true, + "ticksColor": "rgba(0, 0, 0, 0.54)", + "showLine": true, + "lineColor": "rgba(0, 0, 0, 0.54)", + "showSplitLines": true, + "splitLinesColor": "rgba(0, 0, 0, 0.12)", + "id": "default", + "order": 0, + "min": null, + "max": null + } + }, + "thresholds": [], + "dataZoom": false, + "stack": false, + "xAxis": { + "show": true, + "label": "", + "labelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "600", + "lineHeight": "1" + }, + "labelColor": "rgba(0, 0, 0, 0.54)", + "position": "bottom", + "showTickLabels": true, + "tickLabelFont": { + "family": "Roboto", + "size": 10, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "tickLabelColor": null, + "ticksFormat": {}, + "showTicks": true, + "ticksColor": "rgba(0, 0, 0, 0.54)", + "showLine": true, + "lineColor": "rgba(0, 0, 0, 0.54)", + "showSplitLines": true, + "splitLinesColor": "rgba(0, 0, 0, 0.12)" + }, + "noAggregationBarWidthSettings": { + "strategy": "group", + "groupWidth": { + "relative": true, + "relativeWidth": 6, + "absoluteWidth": 3600000 + }, + "barWidth": { + "relative": true, + "relativeWidth": 2, + "absoluteWidth": 1000 + } + }, + "showLegend": true, + "legendLabelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "legendLabelColor": "rgba(0, 0, 0, 0.76)", + "legendConfig": { + "direction": "column", + "position": "bottom", + "sortDataKeys": false, + "showMin": false, + "showMax": false, + "showAvg": false, + "showTotal": true, + "showLatest": false, + "valueFormat": null + }, + "showTooltip": true, + "tooltipTrigger": "axis", + "tooltipValueFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "500", + "lineHeight": "16px" + }, + "tooltipValueColor": "rgba(0, 0, 0, 0.76)", + "tooltipShowDate": true, + "tooltipDateFormat": { + "format": "yyyy-MM-dd HH:mm:ss", + "lastUpdateAgo": false, + "custom": false, + "auto": true, + "autoDateFormatSettings": {} + }, + "tooltipDateFont": { + "family": "Roboto", + "size": 11, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "tooltipDateColor": "rgba(0, 0, 0, 0.76)", + "tooltipDateInterval": true, + "tooltipBackgroundColor": "rgba(255, 255, 255, 0.76)", + "tooltipBackgroundBlur": 4, + "animation": { + "animation": true, + "animationThreshold": 2000, + "animationDuration": 1000, + "animationEasing": "cubicOut", + "animationDelay": 0, + "animationDurationUpdate": 300, + "animationEasingUpdate": "cubicOut", + "animationDelayUpdate": 0 + }, + "background": { + "type": "color", + "color": "#fff", + "overlay": { + "enabled": false, + "color": "rgba(255,255,255,0.72)", + "blur": 3 + } + }, + "comparisonEnabled": false, + "timeForComparison": "previousInterval", + "comparisonCustomIntervalValue": 7200000, + "comparisonXAxis": { + "show": true, + "label": "", + "labelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "600", + "lineHeight": "1" + }, + "labelColor": "rgba(0, 0, 0, 0.54)", + "position": "top", + "showTickLabels": true, + "tickLabelFont": { + "family": "Roboto", + "size": 10, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, + "showTicks": true, + "ticksColor": "rgba(0, 0, 0, 0.54)", + "showLine": true, + "lineColor": "rgba(0, 0, 0, 0.54)", + "showSplitLines": true, + "splitLinesColor": "rgba(0, 0, 0, 0.12)" + }, + "grid": { + "show": false, + "backgroundColor": null, + "borderWidth": 1, + "borderColor": "#ccc" + }, + "legendColumnTitleFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "legendColumnTitleColor": "rgba(0, 0, 0, 0.38)", + "legendValueFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "500", + "lineHeight": "16px" + }, + "legendValueColor": "rgba(0, 0, 0, 0.87)", + "tooltipLabelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "tooltipLabelColor": "rgba(0, 0, 0, 0.76)", + "tooltipHideZeroValues": null, + "padding": "12px" + }, + "title": "{i18n:api-usage.transport-messages-hourly-activity}", + "dropShadow": true, + "enableFullscreen": true, + "titleStyle": null, + "configMode": "basic", + "actions": { + "headerButton": [] + }, + "showTitleIcon": false, + "titleIcon": "thermostat", + "iconColor": "#1F6BDD", + "useDashboardTimewindow": false, + "displayTimewindow": true, + "titleFont": { + "size": 16, + "sizeUnit": "px", + "family": "Roboto", + "weight": "500", + "style": "normal", + "lineHeight": "24px" + }, + "titleColor": "rgba(0, 0, 0, 0.87)", + "titleTooltip": "", + "widgetStyle": {}, + "widgetCss": "", + "pageSize": 1024, + "units": "", + "decimals": null, + "noDataDisplayMessage": "", + "timewindowStyle": { + "showIcon": false, + "iconSize": "24px", + "icon": null, + "iconPosition": "left", + "font": { + "size": 12, + "sizeUnit": "px", + "family": "Roboto", + "weight": "400", + "style": "normal", + "lineHeight": "16px" + }, + "color": "rgba(0, 0, 0, 0.38)", + "displayTypePrefix": true + }, + "margin": "0px", + "borderRadius": "4px", + "iconSize": "0px" + }, + "row": 0, + "col": 0, + "id": "85240e8c-7af7-90a9-ad0a-726013c479a6" + }, + "d0a10a8f-8f48-f9d6-8306-d12af9b49690": { + "typeFullFqn": "system.time_series_chart", + "type": "timeseries", + "sizeX": 8, + "sizeY": 5, + "config": { + "datasources": [ + { + "type": "entity", + "name": null, + "entityAliasId": "40193437-33ac-3172-eefd-0b08eb849062", + "filterId": null, + "dataKeys": [ + { + "name": "transportDataPointsCountHourly", + "type": "timeseries", + "label": "{i18n:api-usage.transport-data-points}", + "color": "#4caf50", + "settings": { + "excludeFromStacking": false, + "hideDataByDefault": false, + "disableDataHiding": false, + "removeFromLegend": false, + "showLines": false, + "fillLines": false, + "showPoints": false, + "showPointShape": "circle", + "pointShapeFormatter": "", + "showPointsLineWidth": 5, + "showPointsRadius": 3, + "showSeparateAxis": false, + "axisPosition": "left", + "thresholds": [ + { + "thresholdValueSource": "predefinedValue" + } + ], + "comparisonSettings": { + "showValuesForComparison": true + }, + "type": "bar" + }, + "_hash": 0.46849996721308895, + "units": null, + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null + } + ], + "alarmFilterConfig": { + "statusList": [ + "ACTIVE" + ] + } + } + ], + "timewindow": { + "selectedTab": 0, + "realtime": { + "realtimeType": 0, + "interval": 3600000, + "timewindowMs": 86400000 + }, + "aggregation": { + "type": "NONE", + "limit": 50000 + } + }, + "showTitle": true, + "backgroundColor": "#FFFFFF", + "color": "rgba(0, 0, 0, 0.87)", + "padding": "0px", + "settings": { + "yAxes": { + "default": { + "units": null, + "decimals": 0, + "show": true, + "label": "", + "labelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "600", + "lineHeight": "1" + }, + "labelColor": "rgba(0, 0, 0, 0.54)", + "position": "left", + "showTickLabels": true, + "tickLabelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormatter": "var rounder = Math.pow(10, 1);\nvar powers = [\n {key: 'Q', value: Math.pow(10, 15)},\n {key: 'T', value: Math.pow(10, 12)},\n {key: 'B', value: Math.pow(10, 9)},\n {key: 'M', value: Math.pow(10, 6)},\n {key: 'K', value: 1000}\n];\n\nvar key = '';\n\nfor (var i = 0; i < powers.length; i++) {\n var reduced = value / powers[i].value;\n reduced = Math.round(reduced * rounder) / rounder;\n if (reduced >= 1) {\n value = reduced;\n key = powers[i].key;\n break;\n }\n}\nreturn value + key;", + "showTicks": true, + "ticksColor": "rgba(0, 0, 0, 0.54)", + "showLine": true, + "lineColor": "rgba(0, 0, 0, 0.54)", + "showSplitLines": true, + "splitLinesColor": "rgba(0, 0, 0, 0.12)", + "id": "default", + "order": 0, + "min": null, + "max": null + } + }, + "thresholds": [], + "dataZoom": false, + "stack": false, + "xAxis": { + "show": true, + "label": "", + "labelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "600", + "lineHeight": "1" + }, + "labelColor": "rgba(0, 0, 0, 0.54)", + "position": "bottom", + "showTickLabels": true, + "tickLabelFont": { + "family": "Roboto", + "size": 10, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "tickLabelColor": null, + "ticksFormat": {}, + "showTicks": true, + "ticksColor": "rgba(0, 0, 0, 0.54)", + "showLine": true, + "lineColor": "rgba(0, 0, 0, 0.54)", + "showSplitLines": true, + "splitLinesColor": "rgba(0, 0, 0, 0.12)" + }, + "noAggregationBarWidthSettings": { + "strategy": "group", + "groupWidth": { + "relative": true, + "relativeWidth": 6, + "absoluteWidth": 3600000 + }, + "barWidth": { + "relative": true, + "relativeWidth": 2, + "absoluteWidth": 1000 + } + }, + "showLegend": true, + "legendLabelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "legendLabelColor": "rgba(0, 0, 0, 0.76)", + "legendConfig": { + "direction": "column", + "position": "bottom", + "sortDataKeys": false, + "showMin": false, + "showMax": false, + "showAvg": false, + "showTotal": true, + "showLatest": false, + "valueFormat": null + }, + "showTooltip": true, + "tooltipTrigger": "axis", + "tooltipValueFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "500", + "lineHeight": "16px" + }, + "tooltipValueColor": "rgba(0, 0, 0, 0.76)", + "tooltipShowDate": true, + "tooltipDateFormat": { + "format": "yyyy-MM-dd HH:mm:ss", + "lastUpdateAgo": false, + "custom": false, + "auto": true, + "autoDateFormatSettings": {} + }, + "tooltipDateFont": { + "family": "Roboto", + "size": 11, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "tooltipDateColor": "rgba(0, 0, 0, 0.76)", + "tooltipDateInterval": true, + "tooltipBackgroundColor": "rgba(255, 255, 255, 0.76)", + "tooltipBackgroundBlur": 4, + "animation": { + "animation": true, + "animationThreshold": 2000, + "animationDuration": 1000, + "animationEasing": "cubicOut", + "animationDelay": 0, + "animationDurationUpdate": 300, + "animationEasingUpdate": "cubicOut", + "animationDelayUpdate": 0 + }, + "background": { + "type": "color", + "color": "#fff", + "overlay": { + "enabled": false, + "color": "rgba(255,255,255,0.72)", + "blur": 3 + } + }, + "comparisonEnabled": false, + "timeForComparison": "previousInterval", + "comparisonCustomIntervalValue": 7200000, + "comparisonXAxis": { + "show": true, + "label": "", + "labelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "600", + "lineHeight": "1" + }, + "labelColor": "rgba(0, 0, 0, 0.54)", + "position": "top", + "showTickLabels": true, + "tickLabelFont": { + "family": "Roboto", + "size": 10, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, + "showTicks": true, + "ticksColor": "rgba(0, 0, 0, 0.54)", + "showLine": true, + "lineColor": "rgba(0, 0, 0, 0.54)", + "showSplitLines": true, + "splitLinesColor": "rgba(0, 0, 0, 0.12)" + }, + "grid": { + "show": false, + "backgroundColor": null, + "borderWidth": 1, + "borderColor": "#ccc" + }, + "legendColumnTitleFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "legendColumnTitleColor": "rgba(0, 0, 0, 0.38)", + "legendValueFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "500", + "lineHeight": "16px" + }, + "legendValueColor": "rgba(0, 0, 0, 0.87)", + "tooltipLabelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "tooltipLabelColor": "rgba(0, 0, 0, 0.76)", + "tooltipHideZeroValues": null, + "padding": "12px" + }, + "title": "{i18n:api-usage.transport-data-point-hourly-activity}", + "dropShadow": true, + "enableFullscreen": true, + "titleStyle": null, + "configMode": "basic", + "actions": { + "headerButton": [] + }, + "showTitleIcon": false, + "titleIcon": "thermostat", + "iconColor": "#1F6BDD", + "useDashboardTimewindow": false, + "displayTimewindow": true, + "titleFont": { + "size": 16, + "sizeUnit": "px", + "family": "Roboto", + "weight": "500", + "style": "normal", + "lineHeight": "24px" + }, + "titleColor": "rgba(0, 0, 0, 0.87)", + "titleTooltip": "", + "widgetStyle": {}, + "widgetCss": "", + "pageSize": 1024, + "units": "", + "decimals": null, + "noDataDisplayMessage": "", + "timewindowStyle": { + "showIcon": false, + "iconSize": "24px", + "icon": null, + "iconPosition": "left", + "font": { + "size": 12, + "sizeUnit": "px", + "family": "Roboto", + "weight": "400", + "style": "normal", + "lineHeight": "16px" + }, + "color": "rgba(0, 0, 0, 0.38)", + "displayTypePrefix": true + }, + "margin": "0px", + "borderRadius": "4px", + "iconSize": "0px" + }, + "row": 0, + "col": 0, + "id": "d0a10a8f-8f48-f9d6-8306-d12af9b49690" + }, + "4544080d-9b6f-b592-9cd4-0e0335d33857": { + "typeFullFqn": "system.time_series_chart", + "type": "timeseries", + "sizeX": 8, + "sizeY": 5, + "config": { + "datasources": [ + { + "type": "entity", + "name": null, + "entityAliasId": "40193437-33ac-3172-eefd-0b08eb849062", + "filterId": null, + "dataKeys": [ + { + "name": "ruleEngineExecutionCountHourly", + "type": "timeseries", + "label": "{i18n:api-usage.rule-engine-executions}", + "color": "#ab00ff", + "settings": { + "showInLegend": true, + "dataHiddenByDefault": false, + "type": "bar", + "lineSettings": { + "showLine": true, + "step": false, + "stepType": "start", + "smooth": false, + "lineType": "solid", + "lineWidth": 2, + "showPoints": false, + "showPointLabel": false, + "pointLabelPosition": "top", + "pointLabelFont": { + "family": "Roboto", + "size": 11, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "pointLabelColor": "rgba(0, 0, 0, 0.76)", + "pointShape": "emptyCircle", + "pointSize": 4, + "fillAreaSettings": { + "type": "none", + "opacity": 0.4, + "gradient": { + "start": 100, + "end": 0 + } + } + }, + "barSettings": { + "showBorder": false, + "borderWidth": 2, + "borderRadius": 0, + "showLabel": false, + "labelPosition": "top", + "labelFont": { + "family": "Roboto", + "size": 11, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "labelColor": "rgba(0, 0, 0, 0.76)", + "backgroundSettings": { + "type": "none", + "opacity": 0.4, + "gradient": { + "start": 100, + "end": 0 + } + } + } + }, + "_hash": 0.0661644137210089, + "units": null, + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null, + "aggregationType": null + } + ], + "alarmFilterConfig": { + "statusList": [ + "ACTIVE" + ] + } + } + ], + "timewindow": { + "selectedTab": 0, + "realtime": { + "realtimeType": 0, + "interval": 3600000, + "timewindowMs": 86400000 }, "aggregation": { - "type": "AVG", - "limit": 25000 + "type": "NONE", + "limit": 50000 } }, - "showTitle": false, - "backgroundColor": "#fff", + "showTitle": true, + "backgroundColor": "#FFFFFF", "color": "rgba(0, 0, 0, 0.87)", "padding": "0px", "settings": { - "useMarkdownTextFunction": true, - "markdownTextPattern": "", - "markdownTextFunction": "function toShortNumber(number) {\n const rounder = Math.pow(10, 1);\n const powers = [\n {key: 'Q', value: Math.pow(10, 15)},\n {key: 'T', value: Math.pow(10, 12)},\n {key: 'B', value: Math.pow(10, 9)},\n {key: 'M', value: Math.pow(10, 6)},\n {key: 'K', value: 1000}\n ];\n let key = '';\n for (const power of powers) {\n let reduced = number / power.value;\n reduced = Math.round(reduced * rounder) / rounder;\n if (reduced >= 1) {\n number = reduced;\n key = power.key;\n break;\n }\n }\n \n return number + key;\n}\n\nfunction calculateBarValues(count, limit) {\n let apiUsageBar = '0%';\n let apiUsagePercent = '';\n let apiUsageValue = `${toShortNumber(count)} / ∞`;\n if (Number.isFinite(limit) && limit > 0) {\n var percent = Math.min(100, ((count / limit) * 100));\n apiUsageBar = `${percent}%`\n apiUsagePercent = `${percent.toFixed(2)}%`;\n apiUsageValue = `${toShortNumber(count)} / ${toShortNumber(limit)}`;\n }\n \n return [apiUsageBar, apiUsagePercent, apiUsageValue]\n}\n\nconst [apiUsageBar, apiUsagePercent, apiUsageValue] = calculateBarValues(data[0].count, data[0].limit);\n\n\nreturn `
    ` +\n '
    ' +\n '
    ' +\n '
    ' +\n '
    ${title}
    ' +\n `
    ${data[0].apiStatus.toUpperCase()}
    ` +\n '
    ' +\n '
    ' +\n `
    ${data[0].unit}
    ` +\n '
    ' +\n `
    ` +\n '
    ' +\n '
    ' +\n `
    ${apiUsagePercent}
    ` +\n '
    ' +\n `
    ${apiUsageValue}
    ` +\n '
    ' +\n '
    ' +\n '
    ' +\n '
    ' +\n '' +\n '
    '+\n '' +\n '
    ' +\n '
    '\n", - "applyDefaultMarkdownStyle": false, - "markdownCss": "\n" + "yAxes": { + "default": { + "units": null, + "decimals": 0, + "show": true, + "label": "", + "labelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "600", + "lineHeight": "1" + }, + "labelColor": "rgba(0, 0, 0, 0.54)", + "position": "left", + "showTickLabels": true, + "tickLabelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormatter": "var rounder = Math.pow(10, 1);\nvar powers = [\n {key: 'Q', value: Math.pow(10, 15)},\n {key: 'T', value: Math.pow(10, 12)},\n {key: 'B', value: Math.pow(10, 9)},\n {key: 'M', value: Math.pow(10, 6)},\n {key: 'K', value: 1000}\n];\n\nvar key = '';\n\nfor (var i = 0; i < powers.length; i++) {\n var reduced = value / powers[i].value;\n reduced = Math.round(reduced * rounder) / rounder;\n if (reduced >= 1) {\n value = reduced;\n key = powers[i].key;\n break;\n }\n}\nreturn value + key;", + "showTicks": true, + "ticksColor": "rgba(0, 0, 0, 0.54)", + "showLine": true, + "lineColor": "rgba(0, 0, 0, 0.54)", + "showSplitLines": true, + "splitLinesColor": "rgba(0, 0, 0, 0.12)", + "id": "default", + "order": 0, + "min": null, + "max": null + } + }, + "thresholds": [], + "dataZoom": false, + "stack": false, + "xAxis": { + "show": true, + "label": "", + "labelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "600", + "lineHeight": "1" + }, + "labelColor": "rgba(0, 0, 0, 0.54)", + "position": "bottom", + "showTickLabels": true, + "tickLabelFont": { + "family": "Roboto", + "size": 10, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, + "showTicks": true, + "ticksColor": "rgba(0, 0, 0, 0.54)", + "showLine": true, + "lineColor": "rgba(0, 0, 0, 0.54)", + "showSplitLines": true, + "splitLinesColor": "rgba(0, 0, 0, 0.12)" + }, + "noAggregationBarWidthSettings": { + "strategy": "group", + "groupWidth": { + "relative": true, + "relativeWidth": 6, + "absoluteWidth": 3600000 + }, + "barWidth": { + "relative": true, + "relativeWidth": 2, + "absoluteWidth": 1000 + } + }, + "showLegend": true, + "legendLabelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "legendLabelColor": "rgba(0, 0, 0, 0.76)", + "legendConfig": { + "direction": "column", + "position": "bottom", + "sortDataKeys": false, + "showMin": false, + "showMax": false, + "showAvg": false, + "showTotal": true, + "showLatest": false, + "valueFormat": null + }, + "showTooltip": true, + "tooltipTrigger": "axis", + "tooltipValueFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "500", + "lineHeight": "16px" + }, + "tooltipValueColor": "rgba(0, 0, 0, 0.76)", + "tooltipShowDate": true, + "tooltipDateFormat": { + "format": "yyyy-MM-dd HH:mm:ss", + "lastUpdateAgo": false, + "custom": false, + "auto": true, + "autoDateFormatSettings": {} + }, + "tooltipDateFont": { + "family": "Roboto", + "size": 11, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "tooltipDateColor": "rgba(0, 0, 0, 0.76)", + "tooltipDateInterval": true, + "tooltipBackgroundColor": "rgba(255, 255, 255, 0.76)", + "tooltipBackgroundBlur": 4, + "animation": { + "animation": true, + "animationThreshold": 2000, + "animationDuration": 1000, + "animationEasing": "cubicOut", + "animationDelay": 0, + "animationDurationUpdate": 300, + "animationEasingUpdate": "cubicOut", + "animationDelayUpdate": 0 + }, + "background": { + "type": "color", + "color": "#fff", + "overlay": { + "enabled": false, + "color": "rgba(255,255,255,0.72)", + "blur": 3 + } + }, + "comparisonEnabled": false, + "timeForComparison": "previousInterval", + "comparisonCustomIntervalValue": 7200000, + "comparisonXAxis": { + "show": true, + "label": "", + "labelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "600", + "lineHeight": "1" + }, + "labelColor": "rgba(0, 0, 0, 0.54)", + "position": "top", + "showTickLabels": true, + "tickLabelFont": { + "family": "Roboto", + "size": 10, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, + "showTicks": true, + "ticksColor": "rgba(0, 0, 0, 0.54)", + "showLine": true, + "lineColor": "rgba(0, 0, 0, 0.54)", + "showSplitLines": true, + "splitLinesColor": "rgba(0, 0, 0, 0.12)" + }, + "grid": { + "show": false, + "backgroundColor": null, + "borderWidth": 1, + "borderColor": "#ccc" + }, + "legendColumnTitleFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "legendColumnTitleColor": "rgba(0, 0, 0, 0.38)", + "legendValueFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "500", + "lineHeight": "16px" + }, + "legendValueColor": "rgba(0, 0, 0, 0.87)", + "tooltipLabelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "tooltipLabelColor": "rgba(0, 0, 0, 0.76)", + "tooltipHideZeroValues": null, + "padding": "12px" }, - "title": "Telemetry persistence", - "showTitleIcon": false, - "iconColor": "rgba(0, 0, 0, 0.87)", - "iconSize": "24px", - "titleTooltip": "", + "title": "{i18n:api-usage.rule-engine-hourly-activity}", "dropShadow": true, - "enableFullscreen": false, - "widgetStyle": {}, - "titleStyle": { - "fontSize": "16px", - "fontWeight": 400 - }, - "showLegend": false, - "useDashboardTimewindow": true, - "displayTimewindow": true, - "widgetCss": "", - "pageSize": 1024, - "noDataDisplayMessage": "", + "enableFullscreen": true, + "titleStyle": null, + "configMode": "basic", "actions": { - "elementClick": [ + "headerButton": [ { - "name": "telemetry_persistence_details", - "icon": "insert_chart", + "name": "{i18n:api-usage.view-statistics}", + "icon": "show_chart", "type": "openDashboardState", - "targetDashboardStateId": "telemetry_persistence", + "targetDashboardStateId": "rule_engine_statistics", "setEntityId": false, "stateEntityParamName": null, "openRightLayout": false, - "id": "6248831c-5b3f-8879-8548-afcf43f10610" + "id": "f9f08190-9ed9-d802-5b7a-c57ff84b5648" } ] - } + }, + "showTitleIcon": false, + "titleIcon": "thermostat", + "iconColor": "#1F6BDD", + "useDashboardTimewindow": false, + "displayTimewindow": true, + "titleFont": { + "size": 16, + "sizeUnit": "px", + "family": "Roboto", + "weight": "500", + "style": "normal", + "lineHeight": "24px" + }, + "titleColor": "rgba(0, 0, 0, 0.87)", + "titleTooltip": "", + "widgetStyle": {}, + "widgetCss": "", + "pageSize": 1024, + "units": "", + "decimals": null, + "noDataDisplayMessage": "", + "timewindowStyle": { + "showIcon": false, + "iconSize": "24px", + "icon": null, + "iconPosition": "left", + "font": { + "size": 12, + "sizeUnit": "px", + "family": "Roboto", + "weight": "400", + "style": "normal", + "lineHeight": "16px" + }, + "color": "rgba(0, 0, 0, 0.38)", + "displayTypePrefix": true + }, + "margin": "0px", + "borderRadius": "4px", + "iconSize": "0px" }, "row": 0, "col": 0, - "id": "4d3ea95c-3188-9872-1817-2f989c7729e0" + "id": "4544080d-9b6f-b592-9cd4-0e0335d33857" }, - "2d0d6ff6-cd59-51d4-b916-38e22cdd0702": { - "typeFullFqn": "system.cards.markdown_card", - "type": "latest", - "sizeX": 5, - "sizeY": 3.5, + "5d0f2f57-499d-1324-8e1b-cfbc0b3149d2": { + "typeFullFqn": "system.time_series_chart", + "type": "timeseries", + "sizeX": 8, + "sizeY": 5, "config": { "datasources": [ { @@ -903,72 +2282,76 @@ "filterId": null, "dataKeys": [ { - "name": "createdAlarmsLimit", - "type": "timeseries", - "label": "limit", - "color": "#4caf50", - "settings": {}, - "_hash": 0.5463603803546802, - "units": null, - "decimals": null, - "funcBody": null, - "usePostProcessing": true, - "postFuncBody": "return value !== '' ? parseInt(value, 10) : 0;", - "aggregationType": "NONE" - }, - { - "name": "createdAlarmsCount", - "type": "timeseries", - "label": "count", - "color": "#f44336", - "settings": {}, - "_hash": 0.5564241862015964, - "units": null, - "decimals": null, - "funcBody": null, - "usePostProcessing": true, - "postFuncBody": "return value !== '' ? parseInt(value, 10) : 0;", - "aggregationType": "NONE" - }, - { - "name": "alarmApiState", - "type": "timeseries", - "label": "apiStatus", - "color": "#ffc107", - "settings": {}, - "_hash": 0.8737107059960671, - "units": null, - "decimals": null, - "funcBody": null, - "usePostProcessing": true, - "postFuncBody": "return value ? value : 'enabled';", - "aggregationType": "NONE" - }, - { - "name": "alarmApiState", - "type": "timeseries", - "label": "title", - "color": "#9c27b0", - "settings": {}, - "_hash": 0.43439375716502227, - "units": null, - "decimals": null, - "funcBody": null, - "usePostProcessing": true, - "postFuncBody": "return \"{i18n:api-usage.alarm}\";" - }, - { - "name": "alarmApiState", + "name": "storageDataPointsCountHourly", "type": "timeseries", - "label": "unit", - "color": "#8bc34a", - "settings": {}, - "_hash": 0.9964061963495883, + "label": "{i18n:api-usage.data-points-storage-days}", + "color": "#1039ee", + "settings": { + "showInLegend": true, + "dataHiddenByDefault": false, + "type": "bar", + "lineSettings": { + "showLine": true, + "step": false, + "stepType": "start", + "smooth": false, + "lineType": "solid", + "lineWidth": 2, + "showPoints": false, + "showPointLabel": false, + "pointLabelPosition": "top", + "pointLabelFont": { + "family": "Roboto", + "size": 11, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "pointLabelColor": "rgba(0, 0, 0, 0.76)", + "pointShape": "emptyCircle", + "pointSize": 4, + "fillAreaSettings": { + "type": "none", + "opacity": 0.4, + "gradient": { + "start": 100, + "end": 0 + } + } + }, + "barSettings": { + "showBorder": false, + "borderWidth": 2, + "borderRadius": 0, + "showLabel": false, + "labelPosition": "top", + "labelFont": { + "family": "Roboto", + "size": 11, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "labelColor": "rgba(0, 0, 0, 0.76)", + "backgroundSettings": { + "type": "none", + "opacity": 0.4, + "gradient": { + "start": 100, + "end": 0 + } + } + } + }, + "_hash": 0.0661644137210089, "units": null, "decimals": null, "funcBody": null, - "usePostProcessing": true, - "postFuncBody": "return \"{i18n:api-usage.alarms-created}\";" + "usePostProcessing": null, + "postFuncBody": null, + "aggregationType": null } ], "alarmFilterConfig": { @@ -976,221 +2359,353 @@ "ACTIVE" ] } - } - ], - "timewindow": { - "displayValue": "", - "selectedTab": 0, - "realtime": { - "realtimeType": 1, - "interval": 1000, - "timewindowMs": 60000, - "quickInterval": "CURRENT_DAY" + } + ], + "timewindow": { + "selectedTab": 0, + "realtime": { + "realtimeType": 0, + "interval": 3600000, + "timewindowMs": 86400000 + }, + "aggregation": { + "type": "NONE", + "limit": 50000 + } + }, + "showTitle": true, + "backgroundColor": "#FFFFFF", + "color": "rgba(0, 0, 0, 0.87)", + "padding": "0px", + "settings": { + "yAxes": { + "default": { + "units": null, + "decimals": 0, + "show": true, + "label": "", + "labelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "600", + "lineHeight": "1" + }, + "labelColor": "rgba(0, 0, 0, 0.54)", + "position": "left", + "showTickLabels": true, + "tickLabelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormatter": "var rounder = Math.pow(10, 1);\nvar powers = [\n {key: 'Q', value: Math.pow(10, 15)},\n {key: 'T', value: Math.pow(10, 12)},\n {key: 'B', value: Math.pow(10, 9)},\n {key: 'M', value: Math.pow(10, 6)},\n {key: 'K', value: 1000}\n];\n\nvar key = '';\n\nfor (var i = 0; i < powers.length; i++) {\n var reduced = value / powers[i].value;\n reduced = Math.round(reduced * rounder) / rounder;\n if (reduced >= 1) {\n value = reduced;\n key = powers[i].key;\n break;\n }\n}\nreturn value + key;", + "showTicks": true, + "ticksColor": "rgba(0, 0, 0, 0.54)", + "showLine": true, + "lineColor": "rgba(0, 0, 0, 0.54)", + "showSplitLines": true, + "splitLinesColor": "rgba(0, 0, 0, 0.12)", + "id": "default", + "order": 0, + "min": null, + "max": null + } + }, + "thresholds": [], + "dataZoom": false, + "stack": false, + "xAxis": { + "show": true, + "label": "", + "labelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "600", + "lineHeight": "1" + }, + "labelColor": "rgba(0, 0, 0, 0.54)", + "position": "bottom", + "showTickLabels": true, + "tickLabelFont": { + "family": "Roboto", + "size": 10, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, + "showTicks": true, + "ticksColor": "rgba(0, 0, 0, 0.54)", + "showLine": true, + "lineColor": "rgba(0, 0, 0, 0.54)", + "showSplitLines": true, + "splitLinesColor": "rgba(0, 0, 0, 0.12)" + }, + "noAggregationBarWidthSettings": { + "strategy": "group", + "groupWidth": { + "relative": true, + "relativeWidth": 6, + "absoluteWidth": 3600000 + }, + "barWidth": { + "relative": true, + "relativeWidth": 2, + "absoluteWidth": 1000 + } }, - "history": { - "historyType": 0, - "interval": 1000, - "timewindowMs": 60000, - "fixedTimewindow": { - "startTimeMs": 1708518962586, - "endTimeMs": 1708605362586 + "showLegend": true, + "legendLabelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "legendLabelColor": "rgba(0, 0, 0, 0.76)", + "legendConfig": { + "direction": "column", + "position": "bottom", + "sortDataKeys": false, + "showMin": false, + "showMax": false, + "showAvg": false, + "showTotal": true, + "showLatest": false, + "valueFormat": null + }, + "showTooltip": true, + "tooltipTrigger": "axis", + "tooltipValueFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "500", + "lineHeight": "16px" + }, + "tooltipValueColor": "rgba(0, 0, 0, 0.76)", + "tooltipShowDate": true, + "tooltipDateFormat": { + "format": "yyyy-MM-dd HH:mm:ss", + "lastUpdateAgo": false, + "custom": false, + "auto": true, + "autoDateFormatSettings": {} + }, + "tooltipDateFont": { + "family": "Roboto", + "size": 11, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "tooltipDateColor": "rgba(0, 0, 0, 0.76)", + "tooltipDateInterval": true, + "tooltipBackgroundColor": "rgba(255, 255, 255, 0.76)", + "tooltipBackgroundBlur": 4, + "animation": { + "animation": true, + "animationThreshold": 2000, + "animationDuration": 1000, + "animationEasing": "cubicOut", + "animationDelay": 0, + "animationDurationUpdate": 300, + "animationEasingUpdate": "cubicOut", + "animationDelayUpdate": 0 + }, + "background": { + "type": "color", + "color": "#fff", + "overlay": { + "enabled": false, + "color": "rgba(255,255,255,0.72)", + "blur": 3 + } + }, + "comparisonEnabled": false, + "timeForComparison": "previousInterval", + "comparisonCustomIntervalValue": 7200000, + "comparisonXAxis": { + "show": true, + "label": "", + "labelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "600", + "lineHeight": "1" }, - "quickInterval": "CURRENT_DAY" + "labelColor": "rgba(0, 0, 0, 0.54)", + "position": "top", + "showTickLabels": true, + "tickLabelFont": { + "family": "Roboto", + "size": 10, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, + "showTicks": true, + "ticksColor": "rgba(0, 0, 0, 0.54)", + "showLine": true, + "lineColor": "rgba(0, 0, 0, 0.54)", + "showSplitLines": true, + "splitLinesColor": "rgba(0, 0, 0, 0.12)" }, - "aggregation": { - "type": "AVG", - "limit": 25000 - } + "grid": { + "show": false, + "backgroundColor": null, + "borderWidth": 1, + "borderColor": "#ccc" + }, + "legendColumnTitleFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "legendColumnTitleColor": "rgba(0, 0, 0, 0.38)", + "legendValueFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "500", + "lineHeight": "16px" + }, + "legendValueColor": "rgba(0, 0, 0, 0.87)", + "tooltipLabelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "tooltipLabelColor": "rgba(0, 0, 0, 0.76)", + "tooltipHideZeroValues": null, + "padding": "12px" }, - "showTitle": false, - "backgroundColor": "#fff", - "color": "rgba(0, 0, 0, 0.87)", - "padding": "0px", - "settings": { - "useMarkdownTextFunction": true, - "markdownTextPattern": "", - "markdownTextFunction": "function toShortNumber(number) {\n const rounder = Math.pow(10, 1);\n const powers = [\n {key: 'Q', value: Math.pow(10, 15)},\n {key: 'T', value: Math.pow(10, 12)},\n {key: 'B', value: Math.pow(10, 9)},\n {key: 'M', value: Math.pow(10, 6)},\n {key: 'K', value: 1000}\n ];\n let key = '';\n for (const power of powers) {\n let reduced = number / power.value;\n reduced = Math.round(reduced * rounder) / rounder;\n if (reduced >= 1) {\n number = reduced;\n key = power.key;\n break;\n }\n }\n \n return number + key;\n}\n\nfunction calculateBarValues(count, limit) {\n let apiUsageBar = '0%';\n let apiUsagePercent = '';\n let apiUsageValue = `${toShortNumber(count)} / ∞`;\n if (Number.isFinite(limit) && limit > 0) {\n var percent = Math.min(100, ((count / limit) * 100));\n apiUsageBar = `${percent}%`\n apiUsagePercent = `${percent.toFixed(2)}%`;\n apiUsageValue = `${toShortNumber(count)} / ${toShortNumber(limit)}`;\n }\n \n return [apiUsageBar, apiUsagePercent, apiUsageValue]\n}\n\nconst [apiUsageBar, apiUsagePercent, apiUsageValue] = calculateBarValues(data[0].count, data[0].limit);\n\n\nreturn `
    ` +\n '
    ' +\n '
    ' +\n '
    ' +\n '
    ${title}
    ' +\n `
    ${data[0].apiStatus.toUpperCase()}
    ` +\n '
    ' +\n '
    ' +\n `
    ${data[0].unit}
    ` +\n '
    ' +\n `
    ` +\n '
    ' +\n '
    ' +\n `
    ${apiUsagePercent}
    ` +\n '
    ' +\n `
    ${apiUsageValue}
    ` +\n '
    ' +\n '
    ' +\n '
    ' +\n '
    ' +\n '' +\n '
    '+\n '' +\n '
    ' +\n '
    '\n", - "applyDefaultMarkdownStyle": false, - "markdownCss": "\n" + "title": "{i18n:api-usage.data-points-storage-days-hourly-activity}", + "dropShadow": true, + "enableFullscreen": true, + "titleStyle": null, + "configMode": "basic", + "actions": { + "headerButton": [] }, - "title": "Alarm created", "showTitleIcon": false, - "iconColor": "rgba(0, 0, 0, 0.87)", - "iconSize": "24px", + "titleIcon": "thermostat", + "iconColor": "#1F6BDD", + "useDashboardTimewindow": false, + "displayTimewindow": true, + "titleFont": { + "size": 16, + "sizeUnit": "px", + "family": "Roboto", + "weight": "500", + "style": "normal", + "lineHeight": "24px" + }, + "titleColor": "rgba(0, 0, 0, 0.87)", "titleTooltip": "", - "dropShadow": true, - "enableFullscreen": false, "widgetStyle": {}, - "titleStyle": { - "fontSize": "16px", - "fontWeight": 400 - }, - "showLegend": false, - "useDashboardTimewindow": true, - "displayTimewindow": true, "widgetCss": "", "pageSize": 1024, + "units": "", + "decimals": null, "noDataDisplayMessage": "", - "actions": { - "elementClick": [ - { - "name": "email_messages_details", - "icon": "insert_chart", - "type": "openDashboardState", - "targetDashboardStateId": "alarms_created", - "setEntityId": false, - "stateEntityParamName": null, - "openInSeparateDialog": null, - "dialogTitle": null, - "dialogHideDashboardToolbar": true, - "dialogWidth": null, - "dialogHeight": null, - "openRightLayout": false, - "id": "946ba769-84ac-1507-6baa-94701de8967b" - } - ] - } + "timewindowStyle": { + "showIcon": false, + "iconSize": "24px", + "icon": null, + "iconPosition": "left", + "font": { + "size": 12, + "sizeUnit": "px", + "family": "Roboto", + "weight": "400", + "style": "normal", + "lineHeight": "16px" + }, + "color": "rgba(0, 0, 0, 0.38)", + "displayTypePrefix": true + }, + "margin": "0px", + "borderRadius": "4px", + "iconSize": "0px" }, "row": 0, "col": 0, - "id": "2d0d6ff6-cd59-51d4-b916-38e22cdd0702" - }, - "120573cc-e246-eb49-7d80-68e5d3b3c0cc": { - "typeFullFqn": "system.cards.markdown_card", - "type": "latest", - "sizeX": 5, - "sizeY": 3.5, - "config": { - "datasources": [ - { - "type": "entity", - "name": null, - "entityAliasId": "40193437-33ac-3172-eefd-0b08eb849062", - "filterId": null, - "dataKeys": [ - { - "name": "emailApiState", - "type": "timeseries", - "label": "apiState", - "color": "#2196f3", - "settings": {}, - "_hash": 0.8830669138660703, - "units": null, - "decimals": null, - "funcBody": null, - "usePostProcessing": null, - "postFuncBody": null - }, - { - "name": "emailLimit", - "type": "timeseries", - "label": "limit", - "color": "#4caf50", - "settings": {}, - "_hash": 0.5463603803546802, - "units": null, - "decimals": null, - "funcBody": null, - "usePostProcessing": true, - "postFuncBody": "return value !== '' ? parseInt(value, 10) : 0;", - "aggregationType": "NONE" - }, - { - "name": "emailCount", - "type": "timeseries", - "label": "count", - "color": "#f44336", - "settings": {}, - "_hash": 0.5564241862015964, - "units": null, - "decimals": null, - "funcBody": null, - "usePostProcessing": true, - "postFuncBody": "return value !== '' ? parseInt(value, 10) : 0;", - "aggregationType": "NONE" - }, + "id": "5d0f2f57-499d-1324-8e1b-cfbc0b3149d2" + }, + "fb155957-1af4-233e-e2fb-09e648e75d6e": { + "typeFullFqn": "system.time_series_chart", + "type": "timeseries", + "sizeX": 8, + "sizeY": 5, + "config": { + "datasources": [ + { + "type": "entity", + "name": null, + "entityAliasId": "40193437-33ac-3172-eefd-0b08eb849062", + "filterId": null, + "dataKeys": [ { - "name": "smsApiState", + "name": "transportMsgCountHourly", "type": "timeseries", - "label": "apiStatePoint", - "color": "#e91e63", - "settings": {}, - "_hash": 0.2969682764607864, + "label": "{i18n:api-usage.transport-messages}", + "color": "#2196f3", + "settings": { + "excludeFromStacking": false, + "hideDataByDefault": false, + "disableDataHiding": false, + "removeFromLegend": false, + "showLines": false, + "fillLines": false, + "showPoints": false, + "showPointShape": "circle", + "pointShapeFormatter": "", + "showPointsLineWidth": 5, + "showPointsRadius": 3, + "showSeparateAxis": false, + "axisPosition": "left", + "thresholds": [ + { + "thresholdValueSource": "predefinedValue" + } + ], + "comparisonSettings": { + "showValuesForComparison": true + }, + "type": "bar" + }, + "_hash": 0.0661644137210089, "units": null, "decimals": null, "funcBody": null, "usePostProcessing": null, "postFuncBody": null - }, - { - "name": "smsLimit", - "type": "timeseries", - "label": "pointsLimit", - "color": "#9c27b0", - "settings": {}, - "_hash": 0.22082255831864894, - "units": null, - "decimals": null, - "funcBody": null, - "usePostProcessing": true, - "postFuncBody": "return value !== '' ? parseInt(value, 10) : 0;", - "aggregationType": "NONE" - }, - { - "name": "smsCount", - "type": "timeseries", - "label": "pointsCount", - "color": "#8bc34a", - "settings": {}, - "_hash": 0.6340356364819146, - "units": null, - "decimals": null, - "funcBody": null, - "usePostProcessing": true, - "postFuncBody": "return value !== '' ? parseInt(value, 10) : 0;", - "aggregationType": "NONE" - }, - { - "name": "notificationApiState", - "type": "timeseries", - "label": "title", - "color": "#3f51b5", - "settings": {}, - "_hash": 0.6894070537030252, - "units": null, - "decimals": null, - "funcBody": null, - "usePostProcessing": true, - "postFuncBody": "return \"{i18n:api-usage.notifications}\";", - "aggregationType": "NONE" - }, - { - "name": "notificationApiState", - "type": "timeseries", - "label": "unit", - "color": "#3f51b5", - "settings": {}, - "_hash": 0.0005447336528170421, - "aggregationType": "NONE", - "units": null, - "decimals": null, - "funcBody": null, - "usePostProcessing": true, - "postFuncBody": "return '{i18n:api-usage.email}';" - }, - { - "name": "notificationApiState", - "type": "timeseries", - "label": "pointsUnit", - "color": "#e91e63", - "settings": {}, - "_hash": 0.12117146988088967, - "aggregationType": "NONE", - "units": null, - "decimals": null, - "funcBody": null, - "usePostProcessing": true, - "postFuncBody": "return '{i18n:api-usage.sms}';" } ], "alarmFilterConfig": { @@ -1201,83 +2716,310 @@ } ], "timewindow": { - "displayValue": "", - "selectedTab": 0, - "realtime": { - "realtimeType": 1, - "interval": 1000, - "timewindowMs": 60000, + "hideAggregation": false, + "hideAggInterval": false, + "hideTimezone": false, + "selectedTab": 1, + "history": { + "historyType": 0, + "timewindowMs": 2592000000, + "interval": 86400000, + "fixedTimewindow": { + "startTimeMs": 1709729389667, + "endTimeMs": 1709815789667 + }, "quickInterval": "CURRENT_DAY" }, - "history": { - "historyType": 0, - "interval": 1000, - "timewindowMs": 60000, - "fixedTimewindow": { - "startTimeMs": 1708518962586, - "endTimeMs": 1708605362586 + "aggregation": { + "type": "SUM", + "limit": 25000 + }, + "timezone": null + }, + "showTitle": true, + "backgroundColor": "#FFFFFF", + "color": "rgba(0, 0, 0, 0.87)", + "padding": "0px", + "settings": { + "yAxes": { + "default": { + "units": null, + "decimals": 0, + "show": true, + "label": "", + "labelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "600", + "lineHeight": "1" + }, + "labelColor": "rgba(0, 0, 0, 0.54)", + "position": "left", + "showTickLabels": true, + "tickLabelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormatter": "var rounder = Math.pow(10, 1);\nvar powers = [\n {key: 'Q', value: Math.pow(10, 15)},\n {key: 'T', value: Math.pow(10, 12)},\n {key: 'B', value: Math.pow(10, 9)},\n {key: 'M', value: Math.pow(10, 6)},\n {key: 'K', value: 1000}\n];\n\nvar key = '';\n\nfor (var i = 0; i < powers.length; i++) {\n var reduced = value / powers[i].value;\n reduced = Math.round(reduced * rounder) / rounder;\n if (reduced >= 1) {\n value = reduced;\n key = powers[i].key;\n break;\n }\n}\nreturn value + key;", + "showTicks": true, + "ticksColor": "rgba(0, 0, 0, 0.54)", + "showLine": true, + "lineColor": "rgba(0, 0, 0, 0.54)", + "showSplitLines": true, + "splitLinesColor": "rgba(0, 0, 0, 0.12)", + "id": "default", + "order": 0, + "min": null, + "max": null + } + }, + "thresholds": [], + "dataZoom": false, + "stack": false, + "xAxis": { + "show": true, + "label": "", + "labelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "600", + "lineHeight": "1" + }, + "labelColor": "rgba(0, 0, 0, 0.54)", + "position": "bottom", + "showTickLabels": true, + "tickLabelFont": { + "family": "Roboto", + "size": 10, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, + "showTicks": true, + "ticksColor": "rgba(0, 0, 0, 0.54)", + "showLine": true, + "lineColor": "rgba(0, 0, 0, 0.54)", + "showSplitLines": true, + "splitLinesColor": "rgba(0, 0, 0, 0.12)" + }, + "noAggregationBarWidthSettings": { + "strategy": "group", + "groupWidth": { + "relative": true, + "relativeWidth": 6, + "absoluteWidth": 1800000 + }, + "barWidth": { + "relative": true, + "relativeWidth": 2, + "absoluteWidth": 1000 + } + }, + "showLegend": true, + "legendLabelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "legendLabelColor": "rgba(0, 0, 0, 0.76)", + "legendConfig": { + "direction": "column", + "position": "bottom", + "sortDataKeys": false, + "showMin": false, + "showMax": false, + "showAvg": false, + "showTotal": true, + "showLatest": false, + "valueFormat": null + }, + "showTooltip": true, + "tooltipTrigger": "axis", + "tooltipValueFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "500", + "lineHeight": "16px" + }, + "tooltipValueColor": "rgba(0, 0, 0, 0.76)", + "tooltipShowDate": true, + "tooltipDateFormat": { + "format": "yyyy-MM-dd HH:mm:ss", + "lastUpdateAgo": false, + "custom": false, + "auto": true, + "autoDateFormatSettings": {} + }, + "tooltipDateFont": { + "family": "Roboto", + "size": 11, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "tooltipDateColor": "rgba(0, 0, 0, 0.76)", + "tooltipDateInterval": true, + "tooltipBackgroundColor": "rgba(255, 255, 255, 0.76)", + "tooltipBackgroundBlur": 4, + "animation": { + "animation": true, + "animationThreshold": 2000, + "animationDuration": 1000, + "animationEasing": "cubicOut", + "animationDelay": 0, + "animationDurationUpdate": 300, + "animationEasingUpdate": "cubicOut", + "animationDelayUpdate": 0 + }, + "background": { + "type": "color", + "color": "#fff", + "overlay": { + "enabled": false, + "color": "rgba(255,255,255,0.72)", + "blur": 3 + } + }, + "comparisonEnabled": false, + "timeForComparison": "previousInterval", + "comparisonCustomIntervalValue": 7200000, + "comparisonXAxis": { + "show": true, + "label": "", + "labelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "600", + "lineHeight": "1" }, - "quickInterval": "CURRENT_DAY" + "labelColor": "rgba(0, 0, 0, 0.54)", + "position": "top", + "showTickLabels": true, + "tickLabelFont": { + "family": "Roboto", + "size": 10, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, + "showTicks": true, + "ticksColor": "rgba(0, 0, 0, 0.54)", + "showLine": true, + "lineColor": "rgba(0, 0, 0, 0.54)", + "showSplitLines": true, + "splitLinesColor": "rgba(0, 0, 0, 0.12)" }, - "aggregation": { - "type": "AVG", - "limit": 25000 - } - }, - "showTitle": false, - "backgroundColor": "#fff", - "color": "rgba(0, 0, 0, 0.87)", - "padding": "0px", - "settings": { - "useMarkdownTextFunction": true, - "markdownTextPattern": "", - "markdownTextFunction": "function toShortNumber(number) {\n const rounder = Math.pow(10, 1);\n const powers = [\n {key: 'Q', value: Math.pow(10, 15)},\n {key: 'T', value: Math.pow(10, 12)},\n {key: 'B', value: Math.pow(10, 9)},\n {key: 'M', value: Math.pow(10, 6)},\n {key: 'K', value: 1000}\n ];\n let key = '';\n for (const power of powers) {\n const reduced = number / power.value;\n for (const power of powers) {\n let reduced = number / power.value;\n reduced = Math.round(reduced * rounder) / rounder;\n if (reduced >= 1) {\n number = reduced;\n key = power.key;\n break;\n }\n }\n }\n \n return number + key;\n}\n\nfunction calculateBarValues(count, limit) {\n let apiUsageBar = '0%';\n let apiUsagePercent = '';\n let apiUsageValue = `${toShortNumber(count)} / ∞`;\n if (Number.isFinite(limit) && limit > 0) {\n var percent = Math.min(100, ((count / limit) * 100));\n apiUsageBar = `${percent}%`\n apiUsagePercent = `${percent.toFixed(2)}%`;\n apiUsageValue = `${toShortNumber(count)} / ${toShortNumber(limit)}`;\n }\n \n return [apiUsageBar, apiUsagePercent, apiUsageValue]\n}\n\nconst [apiUsageBar, apiUsagePercent, apiUsageValue] = calculateBarValues(data[0].count, data[0].limit);\nconst [apiUsageBar2, apiUsagePercent2, apiUsageValue2] = calculateBarValues(data[0].pointsCount, data[0].pointsLimit);\n\nconst apiState = data[0].apiState;\nconst apiStatePoint = data[0].apiStatePoint;\nlet currentState;\nif (apiState === 'DISABLED' || apiStatePoint === 'DISABLED') {\n currentState = 'DISABLED';\n} else if (apiState === 'WARNING' || apiStatePoint === 'WARNING') {\n currentState = 'WARNING';\n} else {\n currentState = 'ENABLED';\n}\n\n\n\nreturn `
    ` +\n '
    ' +\n '
    ' +\n '
    ' +\n '
    ' +\n `${data[0].title}` +\n '
    ' +\n `
    ${currentState}
    ` +\n '
    ' +\n '
    ' +\n '
    ' +\n '
    ' +\n `
    ${data[0].unit}
    ` +\n '
    ' +\n `
    ` +\n '
    ' +\n '
    ' +\n `
    ${apiUsagePercent}
    ` +\n '
    ' +\n `
    ${apiUsageValue}
    ` +\n '
    ' +\n '
    ' +\n '
    ' +\n '
    ' +\n '
    ' +\n `
    ${data[0].pointsUnit}
    ` +\n '
    ' +\n `
    ` +\n '
    ' +\n '
    ' +\n `
    ${apiUsagePercent2}
    ` +\n '
    ' +\n `
    ${apiUsageValue2}
    ` +\n '
    ' +\n '
    ' + \n '
    ' +\n '
    ' +\n '
    ' +\n '
    ' +\n '' +\n '
    '+\n '' +\n '
    ' +\n '
    '\n", - "applyDefaultMarkdownStyle": false, - "markdownCss": "\n" + "grid": { + "show": false, + "backgroundColor": null, + "borderWidth": 1, + "borderColor": "#ccc" + }, + "legendColumnTitleFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "legendColumnTitleColor": "rgba(0, 0, 0, 0.38)", + "legendValueFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "500", + "lineHeight": "16px" + }, + "legendValueColor": "rgba(0, 0, 0, 0.87)", + "tooltipLabelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "tooltipLabelColor": "rgba(0, 0, 0, 0.76)", + "tooltipHideZeroValues": null, + "padding": "12px" }, - "title": "Notifications (Email/SMS)", + "title": "{i18n:api-usage.transport-msg-daily-activity}", + "dropShadow": true, + "enableFullscreen": true, + "titleStyle": null, + "configMode": "basic", + "actions": {}, "showTitleIcon": false, - "iconColor": "rgba(0, 0, 0, 0.87)", - "iconSize": "24px", + "titleIcon": "thermostat", + "iconColor": "#1F6BDD", + "useDashboardTimewindow": false, + "displayTimewindow": true, + "titleFont": { + "size": 16, + "sizeUnit": "px", + "family": "Roboto", + "weight": "500", + "style": "normal", + "lineHeight": "24px" + }, + "titleColor": "rgba(0, 0, 0, 0.87)", "titleTooltip": "", - "dropShadow": true, - "enableFullscreen": false, "widgetStyle": {}, - "titleStyle": { - "fontSize": "16px", - "fontWeight": 400 - }, - "showLegend": false, - "useDashboardTimewindow": true, - "displayTimewindow": true, "widgetCss": "", "pageSize": 1024, + "units": "", + "decimals": null, "noDataDisplayMessage": "", - "actions": { - "elementClick": [ - { - "name": "transport_details", - "icon": "insert_chart", - "type": "openDashboardState", - "targetDashboardStateId": "notifications", - "setEntityId": false, - "stateEntityParamName": null, - "openInSeparateDialog": null, - "dialogTitle": null, - "dialogHideDashboardToolbar": true, - "dialogWidth": null, - "dialogHeight": null, - "openRightLayout": false, - "id": "46b7cefe-e1f2-67c1-4055-3a214520f869" - } - ] - } + "timewindowStyle": { + "showIcon": false, + "iconSize": "24px", + "icon": null, + "iconPosition": "left", + "font": { + "size": 12, + "sizeUnit": "px", + "family": "Roboto", + "weight": "400", + "style": "normal", + "lineHeight": "16px" + }, + "color": "rgba(0, 0, 0, 0.38)", + "displayTypePrefix": true + }, + "margin": "0px", + "borderRadius": "4px", + "iconSize": "0px" }, "row": 0, "col": 0, - "id": "120573cc-e246-eb49-7d80-68e5d3b3c0cc" + "id": "fb155957-1af4-233e-e2fb-09e648e75d6e" }, - "63f99d90-23ab-f8c2-3290-1e693ded5a2e": { + "4817e33b-87be-5be3-eaca-ca68a2eb4e0c": { "typeFullFqn": "system.time_series_chart", "type": "timeseries", "sizeX": 8, @@ -1323,71 +3065,40 @@ "units": null, "decimals": null, "funcBody": null, - "usePostProcessing": false, - "postFuncBody": null, - "aggregationType": null - }, - { - "name": "transportDataPointsCountHourly", - "type": "timeseries", - "label": "{i18n:api-usage.transport-data-points}", - "color": "#4caf50", - "settings": { - "excludeFromStacking": false, - "hideDataByDefault": false, - "disableDataHiding": false, - "removeFromLegend": false, - "showLines": false, - "fillLines": false, - "showPoints": false, - "showPointShape": "circle", - "pointShapeFormatter": "", - "showPointsLineWidth": 5, - "showPointsRadius": 3, - "showSeparateAxis": false, - "axisPosition": "left", - "thresholds": [ - { - "thresholdValueSource": "predefinedValue" - } - ], - "comparisonSettings": { - "showValuesForComparison": true - }, - "type": "bar" - }, - "_hash": 0.46849996721308895, - "units": null, - "decimals": null, - "funcBody": null, "usePostProcessing": null, "postFuncBody": null } - ], - "alarmFilterConfig": { - "statusList": [ - "ACTIVE" - ] - } + ] } ], "timewindow": { - "hideInterval": false, - "hideLastInterval": false, - "hideQuickInterval": false, "hideAggregation": false, "hideAggInterval": false, "hideTimezone": false, - "selectedTab": 0, + "selectedTab": 1, "realtime": { "realtimeType": 0, - "timewindowMs": 86400000, + "interval": 1000, + "timewindowMs": 60000, + "quickInterval": "CURRENT_DAY", + "hideInterval": false, + "hideLastInterval": false, + "hideQuickInterval": false + }, + "history": { + "historyType": 0, + "interval": 2592000000, + "timewindowMs": 31536000000, + "fixedTimewindow": null, "quickInterval": "CURRENT_DAY", - "interval": 300000 + "hideInterval": false, + "hideLastInterval": false, + "hideFixedInterval": false, + "hideQuickInterval": false }, "aggregation": { "type": "NONE", - "limit": 50000 + "limit": 25000 }, "timezone": null }, @@ -1460,7 +3171,8 @@ "weight": "400", "lineHeight": "1" }, - "tickLabelColor": null, + "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, "showTicks": true, "ticksColor": "rgba(0, 0, 0, 0.54)", "showLine": true, @@ -1471,9 +3183,9 @@ "noAggregationBarWidthSettings": { "strategy": "group", "groupWidth": { - "relative": false, - "relativeWidth": 2, - "absoluteWidth": 3600000 + "relative": true, + "relativeWidth": 6, + "absoluteWidth": 1800000 }, "barWidth": { "relative": true, @@ -1499,7 +3211,8 @@ "showMax": false, "showAvg": false, "showTotal": true, - "showLatest": false + "showLatest": false, + "valueFormat": null }, "showTooltip": true, "tooltipTrigger": "axis", @@ -1516,7 +3229,9 @@ "tooltipDateFormat": { "format": "yyyy-MM-dd HH:mm:ss", "lastUpdateAgo": false, - "custom": false + "custom": false, + "auto": true, + "autoDateFormatSettings": {} }, "tooltipDateFont": { "family": "Roboto", @@ -1548,27 +3263,83 @@ "color": "rgba(255,255,255,0.72)", "blur": 3 } - } + }, + "comparisonEnabled": false, + "timeForComparison": "previousInterval", + "comparisonCustomIntervalValue": 7200000, + "comparisonXAxis": { + "show": true, + "label": "", + "labelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "600", + "lineHeight": "1" + }, + "labelColor": "rgba(0, 0, 0, 0.54)", + "position": "top", + "showTickLabels": true, + "tickLabelFont": { + "family": "Roboto", + "size": 10, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, + "showTicks": true, + "ticksColor": "rgba(0, 0, 0, 0.54)", + "showLine": true, + "lineColor": "rgba(0, 0, 0, 0.54)", + "showSplitLines": true, + "splitLinesColor": "rgba(0, 0, 0, 0.12)" + }, + "grid": { + "show": false, + "backgroundColor": null, + "borderWidth": 1, + "borderColor": "#ccc" + }, + "legendColumnTitleFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "legendColumnTitleColor": "rgba(0, 0, 0, 0.38)", + "legendValueFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "500", + "lineHeight": "16px" + }, + "legendValueColor": "rgba(0, 0, 0, 0.87)", + "tooltipLabelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "tooltipLabelColor": "rgba(0, 0, 0, 0.76)", + "tooltipHideZeroValues": null, + "padding": "12px" }, - "title": "{i18n:api-usage.transport-hourly-activity}", + "title": "{i18n:api-usage.transport-msg-monthly-activity}", "dropShadow": true, "enableFullscreen": true, "titleStyle": null, "configMode": "basic", - "actions": { - "headerButton": [ - { - "name": "{i18n:api-usage.view-details}", - "icon": "insert_chart", - "type": "openDashboardState", - "targetDashboardStateId": "transport", - "setEntityId": false, - "stateEntityParamName": null, - "openRightLayout": false, - "id": "6ef12f6a-0266-25cf-6ca5-5dcb772252c6" - } - ] - }, + "actions": {}, "showTitleIcon": false, "titleIcon": "thermostat", "iconColor": "#1F6BDD", @@ -1607,14 +3378,14 @@ "displayTypePrefix": true }, "margin": "0px", - "borderRadius": "0px", + "borderRadius": "4px", "iconSize": "0px" }, "row": 0, "col": 0, - "id": "63f99d90-23ab-f8c2-3290-1e693ded5a2e" + "id": "4817e33b-87be-5be3-eaca-ca68a2eb4e0c" }, - "a2b7e906-2d8a-41a8-99a6-409531bfa743": { + "79056202-c92b-1dae-ce49-318ec52e2d3b": { "typeFullFqn": "system.time_series_chart", "type": "timeseries", "sizeX": 8, @@ -1628,68 +3399,34 @@ "filterId": null, "dataKeys": [ { - "name": "ruleEngineExecutionCountHourly", + "name": "transportDataPointsCountHourly", "type": "timeseries", - "label": "{i18n:api-usage.rule-engine-executions}", - "color": "#ab00ff", + "label": "{i18n:api-usage.transport-data-points}", + "color": "#4CAF50", "settings": { - "showInLegend": true, - "dataHiddenByDefault": false, - "type": "bar", - "lineSettings": { - "showLine": true, - "step": false, - "stepType": "start", - "smooth": false, - "lineType": "solid", - "lineWidth": 2, - "showPoints": false, - "showPointLabel": false, - "pointLabelPosition": "top", - "pointLabelFont": { - "family": "Roboto", - "size": 11, - "sizeUnit": "px", - "style": "normal", - "weight": "400", - "lineHeight": "1" - }, - "pointLabelColor": "rgba(0, 0, 0, 0.76)", - "pointShape": "emptyCircle", - "pointSize": 4, - "fillAreaSettings": { - "type": "none", - "opacity": 0.4, - "gradient": { - "start": 100, - "end": 0 - } + "excludeFromStacking": false, + "hideDataByDefault": false, + "disableDataHiding": false, + "removeFromLegend": false, + "showLines": false, + "fillLines": false, + "showPoints": false, + "showPointShape": "circle", + "pointShapeFormatter": "", + "showPointsLineWidth": 5, + "showPointsRadius": 3, + "showSeparateAxis": false, + "axisPosition": "left", + "thresholds": [ + { + "thresholdValueSource": "predefinedValue" } + ], + "comparisonSettings": { + "showValuesForComparison": true }, - "barSettings": { - "showBorder": false, - "borderWidth": 2, - "borderRadius": 0, - "showLabel": false, - "labelPosition": "top", - "labelFont": { - "family": "Roboto", - "size": 11, - "sizeUnit": "px", - "style": "normal", - "weight": "400", - "lineHeight": "1" - }, - "labelColor": "rgba(0, 0, 0, 0.76)", - "backgroundSettings": { - "type": "none", - "opacity": 0.4, - "gradient": { - "start": 100, - "end": 0 - } - } - } + "type": "bar", + "yAxisId": "default" }, "_hash": 0.0661644137210089, "units": null, @@ -1708,22 +3445,23 @@ } ], "timewindow": { - "hideInterval": false, - "hideLastInterval": false, - "hideQuickInterval": false, "hideAggregation": false, "hideAggInterval": false, "hideTimezone": false, - "selectedTab": 0, - "realtime": { - "realtimeType": 0, - "timewindowMs": 86400000, - "quickInterval": "CURRENT_YEAR", - "interval": 7200000 + "selectedTab": 1, + "history": { + "historyType": 0, + "timewindowMs": 2592000000, + "interval": 86400000, + "fixedTimewindow": { + "startTimeMs": 1709729389667, + "endTimeMs": 1709815789667 + }, + "quickInterval": "CURRENT_DAY" }, "aggregation": { - "type": "NONE", - "limit": 50000 + "type": "SUM", + "limit": 25000 }, "timezone": null }, @@ -1797,6 +3535,7 @@ "lineHeight": "1" }, "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, "showTicks": true, "ticksColor": "rgba(0, 0, 0, 0.54)", "showLine": true, @@ -1807,9 +3546,9 @@ "noAggregationBarWidthSettings": { "strategy": "group", "groupWidth": { - "relative": false, - "relativeWidth": 2, - "absoluteWidth": 3600000 + "relative": true, + "relativeWidth": 6, + "absoluteWidth": 1800000 }, "barWidth": { "relative": true, @@ -1835,7 +3574,8 @@ "showMax": false, "showAvg": false, "showTotal": true, - "showLatest": false + "showLatest": false, + "valueFormat": null }, "showTooltip": true, "tooltipTrigger": "axis", @@ -1852,7 +3592,9 @@ "tooltipDateFormat": { "format": "yyyy-MM-dd HH:mm:ss", "lastUpdateAgo": false, - "custom": false + "custom": false, + "auto": true, + "autoDateFormatSettings": {} }, "tooltipDateFont": { "family": "Roboto", @@ -1884,37 +3626,83 @@ "color": "rgba(255,255,255,0.72)", "blur": 3 } - } + }, + "comparisonEnabled": false, + "timeForComparison": "previousInterval", + "comparisonCustomIntervalValue": 7200000, + "comparisonXAxis": { + "show": true, + "label": "", + "labelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "600", + "lineHeight": "1" + }, + "labelColor": "rgba(0, 0, 0, 0.54)", + "position": "top", + "showTickLabels": true, + "tickLabelFont": { + "family": "Roboto", + "size": 10, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, + "showTicks": true, + "ticksColor": "rgba(0, 0, 0, 0.54)", + "showLine": true, + "lineColor": "rgba(0, 0, 0, 0.54)", + "showSplitLines": true, + "splitLinesColor": "rgba(0, 0, 0, 0.12)" + }, + "grid": { + "show": false, + "backgroundColor": null, + "borderWidth": 1, + "borderColor": "#ccc" + }, + "legendColumnTitleFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "legendColumnTitleColor": "rgba(0, 0, 0, 0.38)", + "legendValueFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "500", + "lineHeight": "16px" + }, + "legendValueColor": "rgba(0, 0, 0, 0.87)", + "tooltipLabelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "tooltipLabelColor": "rgba(0, 0, 0, 0.76)", + "tooltipHideZeroValues": null, + "padding": "12px" }, - "title": "{i18n:api-usage.rule-engine-hourly-activity}", + "title": "{i18n:api-usage.transport-data-points-daily-activity}", "dropShadow": true, "enableFullscreen": true, "titleStyle": null, "configMode": "basic", - "actions": { - "headerButton": [ - { - "name": "{i18n:api-usage.view-statistics}", - "icon": "show_chart", - "type": "openDashboardState", - "targetDashboardStateId": "rule_engine_statistics", - "setEntityId": false, - "stateEntityParamName": null, - "openRightLayout": false, - "id": "f9f08190-9ed9-d802-5b7a-c57ff84b5648" - }, - { - "name": "{i18n:api-usage.view-details}", - "icon": "insert_chart", - "type": "openDashboardState", - "targetDashboardStateId": "rule_engine_execution", - "setEntityId": false, - "stateEntityParamName": null, - "openRightLayout": false, - "id": "1aec196b-44ba-ddf4-c4dc-c3f60c1eb6fc" - } - ] - }, + "actions": {}, "showTitleIcon": false, "titleIcon": "thermostat", "iconColor": "#1F6BDD", @@ -1953,14 +3741,14 @@ "displayTypePrefix": true }, "margin": "0px", - "borderRadius": "0px", + "borderRadius": "4px", "iconSize": "0px" }, "row": 0, "col": 0, - "id": "a2b7e906-2d8a-41a8-99a6-409531bfa743" + "id": "79056202-c92b-1dae-ce49-318ec52e2d3b" }, - "ca996b66-ab7e-f977-152c-98e4ebf2a901": { + "966ffee7-ba0d-8e54-f903-e8d015ca8cd2": { "typeFullFqn": "system.time_series_chart", "type": "timeseries", "sizeX": 8, @@ -1974,11 +3762,12 @@ "filterId": null, "dataKeys": [ { - "name": "jsExecutionCountHourly", + "name": "transportDataPointsCountHourly", "type": "timeseries", - "label": "{i18n:api-usage.javascript-executions}", - "color": "#ff9900", + "label": "{i18n:api-usage.transport-data-points}", + "color": "#4CAF50", "settings": { + "yAxisId": "default", "showInLegend": true, "dataHiddenByDefault": false, "type": "bar", @@ -2001,6 +3790,8 @@ "lineHeight": "1" }, "pointLabelColor": "rgba(0, 0, 0, 0.76)", + "enablePointLabelBackground": false, + "pointLabelBackground": "rgba(255,255,255,0.56)", "pointShape": "emptyCircle", "pointSize": 4, "fillAreaSettings": { @@ -2027,6 +3818,8 @@ "lineHeight": "1" }, "labelColor": "rgba(0, 0, 0, 0.76)", + "enableLabelBackground": false, + "labelBackground": "rgba(255,255,255,0.56)", "backgroundSettings": { "type": "none", "opacity": 0.4, @@ -2035,81 +3828,14 @@ "end": 0 } } - } - }, - "_hash": 0.0661644137210089, - "units": null, - "decimals": null, - "funcBody": null, - "usePostProcessing": null, - "postFuncBody": null, - "aggregationType": null - }, - { - "name": "tbelExecutionCountHourly", - "type": "timeseries", - "label": "{i18n:api-usage.tbel-executions}", - "color": "#4caf50", - "settings": { - "showInLegend": true, - "dataHiddenByDefault": false, - "type": "bar", - "lineSettings": { - "showLine": true, - "step": false, - "stepType": "start", - "smooth": false, - "lineType": "solid", - "lineWidth": 2, - "showPoints": false, - "showPointLabel": false, - "pointLabelPosition": "top", - "pointLabelFont": { - "family": "Roboto", - "size": 11, - "sizeUnit": "px", - "style": "normal", - "weight": "400", - "lineHeight": "1" - }, - "pointLabelColor": "rgba(0, 0, 0, 0.76)", - "pointShape": "emptyCircle", - "pointSize": 4, - "fillAreaSettings": { - "type": "none", - "opacity": 0.4, - "gradient": { - "start": 100, - "end": 0 - } - } }, - "barSettings": { - "showBorder": false, - "borderWidth": 2, - "borderRadius": 0, - "showLabel": false, - "labelPosition": "top", - "labelFont": { - "family": "Roboto", - "size": 11, - "sizeUnit": "px", - "style": "normal", - "weight": "400", - "lineHeight": "1" - }, - "labelColor": "rgba(0, 0, 0, 0.76)", - "backgroundSettings": { - "type": "none", - "opacity": 0.4, - "gradient": { - "start": 100, - "end": 0 - } - } + "comparisonSettings": { + "showValuesForComparison": false, + "comparisonValuesLabel": "", + "color": "" } }, - "_hash": 0.6818645685001823, + "_hash": 0.12814821361119078, "aggregationType": null, "units": null, "decimals": null, @@ -2126,22 +3852,33 @@ } ], "timewindow": { - "hideInterval": false, - "hideLastInterval": false, - "hideQuickInterval": false, "hideAggregation": false, "hideAggInterval": false, "hideTimezone": false, - "selectedTab": 0, + "selectedTab": 1, "realtime": { "realtimeType": 0, - "timewindowMs": 86400000, + "interval": 1000, + "timewindowMs": 60000, + "quickInterval": "CURRENT_DAY", + "hideInterval": false, + "hideLastInterval": false, + "hideQuickInterval": false + }, + "history": { + "historyType": 0, + "interval": 2592000000, + "timewindowMs": 31536000000, + "fixedTimewindow": null, "quickInterval": "CURRENT_DAY", - "interval": 3600000 + "hideInterval": false, + "hideLastInterval": false, + "hideFixedInterval": false, + "hideQuickInterval": false }, "aggregation": { "type": "NONE", - "limit": 50000 + "limit": 25000 }, "timezone": null }, @@ -2215,6 +3952,7 @@ "lineHeight": "1" }, "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, "showTicks": true, "ticksColor": "rgba(0, 0, 0, 0.54)", "showLine": true, @@ -2225,9 +3963,9 @@ "noAggregationBarWidthSettings": { "strategy": "group", "groupWidth": { - "relative": false, - "relativeWidth": 2, - "absoluteWidth": 3600000 + "relative": true, + "relativeWidth": 6, + "absoluteWidth": 1800000 }, "barWidth": { "relative": true, @@ -2253,7 +3991,8 @@ "showMax": false, "showAvg": false, "showTotal": true, - "showLatest": false + "showLatest": false, + "valueFormat": null }, "showTooltip": true, "tooltipTrigger": "axis", @@ -2270,7 +4009,9 @@ "tooltipDateFormat": { "format": "yyyy-MM-dd HH:mm:ss", "lastUpdateAgo": false, - "custom": false + "custom": false, + "auto": true, + "autoDateFormatSettings": {} }, "tooltipDateFont": { "family": "Roboto", @@ -2302,31 +4043,83 @@ "color": "rgba(255,255,255,0.72)", "blur": 3 } - } + }, + "comparisonEnabled": false, + "timeForComparison": "previousInterval", + "comparisonCustomIntervalValue": 7200000, + "comparisonXAxis": { + "show": true, + "label": "", + "labelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "600", + "lineHeight": "1" + }, + "labelColor": "rgba(0, 0, 0, 0.54)", + "position": "top", + "showTickLabels": true, + "tickLabelFont": { + "family": "Roboto", + "size": 10, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, + "showTicks": true, + "ticksColor": "rgba(0, 0, 0, 0.54)", + "showLine": true, + "lineColor": "rgba(0, 0, 0, 0.54)", + "showSplitLines": true, + "splitLinesColor": "rgba(0, 0, 0, 0.12)" + }, + "grid": { + "show": false, + "backgroundColor": null, + "borderWidth": 1, + "borderColor": "#ccc" + }, + "legendColumnTitleFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "legendColumnTitleColor": "rgba(0, 0, 0, 0.38)", + "legendValueFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "500", + "lineHeight": "16px" + }, + "legendValueColor": "rgba(0, 0, 0, 0.87)", + "tooltipLabelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "tooltipLabelColor": "rgba(0, 0, 0, 0.76)", + "tooltipHideZeroValues": null, + "padding": "12px" }, - "title": "{i18n:api-usage.scripts-hourly-activity}", + "title": "{i18n:api-usage.transport-data-points-monthly-activity}", "dropShadow": true, "enableFullscreen": true, "titleStyle": null, "configMode": "basic", - "actions": { - "headerButton": [ - { - "name": "{i18n:api-usage.view-details}", - "icon": "insert_chart", - "useShowWidgetActionFunction": null, - "showWidgetActionFunction": "return true;", - "type": "openDashboardState", - "targetDashboardStateId": "script_functions", - "setEntityId": false, - "stateEntityParamName": null, - "openRightLayout": false, - "openInSeparateDialog": false, - "openInPopover": false, - "id": "4687d3f6-8800-a3b6-26e5-0d33f3b828a9" - } - ] - }, + "actions": {}, "showTitleIcon": false, "titleIcon": "thermostat", "iconColor": "#1F6BDD", @@ -2365,14 +4158,14 @@ "displayTypePrefix": true }, "margin": "0px", - "borderRadius": "0px", + "borderRadius": "4px", "iconSize": "0px" }, "row": 0, "col": 0, - "id": "ca996b66-ab7e-f977-152c-98e4ebf2a901" + "id": "966ffee7-ba0d-8e54-f903-e8d015ca8cd2" }, - "a3c2f1bb-7d3a-f11c-7b3d-28cd84fdfe34": { + "84fbe63a-bcb6-7bc1-8af0-46b3b1ee5adc": { "typeFullFqn": "system.time_series_chart", "type": "timeseries", "sizeX": 8, @@ -2386,11 +4179,12 @@ "filterId": null, "dataKeys": [ { - "name": "storageDataPointsCountHourly", + "name": "ruleEngineExecutionCountHourly", "type": "timeseries", - "label": "{i18n:api-usage.data-points-storage-days}", - "color": "#1039ee", + "label": "{i18n:api-usage.rule-engine-executions}", + "color": "#AB00FF", "settings": { + "yAxisId": "default", "showInLegend": true, "dataHiddenByDefault": false, "type": "bar", @@ -2413,6 +4207,8 @@ "lineHeight": "1" }, "pointLabelColor": "rgba(0, 0, 0, 0.76)", + "enablePointLabelBackground": false, + "pointLabelBackground": "rgba(255,255,255,0.56)", "pointShape": "emptyCircle", "pointSize": 4, "fillAreaSettings": { @@ -2439,6 +4235,8 @@ "lineHeight": "1" }, "labelColor": "rgba(0, 0, 0, 0.76)", + "enableLabelBackground": false, + "labelBackground": "rgba(255,255,255,0.56)", "backgroundSettings": { "type": "none", "opacity": 0.4, @@ -2447,15 +4245,20 @@ "end": 0 } } + }, + "comparisonSettings": { + "showValuesForComparison": false, + "comparisonValuesLabel": "", + "color": "" } }, - "_hash": 0.0661644137210089, + "_hash": 0.01948850513940492, + "aggregationType": null, "units": null, "decimals": null, "funcBody": null, "usePostProcessing": null, - "postFuncBody": null, - "aggregationType": null + "postFuncBody": null } ], "alarmFilterConfig": { @@ -2466,22 +4269,23 @@ } ], "timewindow": { - "hideInterval": false, - "hideLastInterval": false, - "hideQuickInterval": false, "hideAggregation": false, "hideAggInterval": false, "hideTimezone": false, - "selectedTab": 0, - "realtime": { - "realtimeType": 0, - "timewindowMs": 86400000, - "quickInterval": "CURRENT_DAY", - "interval": 300000 + "selectedTab": 1, + "history": { + "historyType": 0, + "timewindowMs": 2592000000, + "interval": 86400000, + "fixedTimewindow": { + "startTimeMs": 1709729389667, + "endTimeMs": 1709815789667 + }, + "quickInterval": "CURRENT_DAY" }, "aggregation": { - "type": "NONE", - "limit": 50000 + "type": "SUM", + "limit": 25000 }, "timezone": null }, @@ -2555,6 +4359,7 @@ "lineHeight": "1" }, "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, "showTicks": true, "ticksColor": "rgba(0, 0, 0, 0.54)", "showLine": true, @@ -2565,9 +4370,9 @@ "noAggregationBarWidthSettings": { "strategy": "group", "groupWidth": { - "relative": false, - "relativeWidth": 2, - "absoluteWidth": 3600000 + "relative": true, + "relativeWidth": 6, + "absoluteWidth": 1800000 }, "barWidth": { "relative": true, @@ -2593,7 +4398,8 @@ "showMax": false, "showAvg": false, "showTotal": true, - "showLatest": false + "showLatest": false, + "valueFormat": null }, "showTooltip": true, "tooltipTrigger": "axis", @@ -2610,7 +4416,9 @@ "tooltipDateFormat": { "format": "yyyy-MM-dd HH:mm:ss", "lastUpdateAgo": false, - "custom": false + "custom": false, + "auto": true, + "autoDateFormatSettings": {} }, "tooltipDateFont": { "family": "Roboto", @@ -2642,9 +4450,78 @@ "color": "rgba(255,255,255,0.72)", "blur": 3 } - } + }, + "comparisonEnabled": false, + "timeForComparison": "previousInterval", + "comparisonCustomIntervalValue": 7200000, + "comparisonXAxis": { + "show": true, + "label": "", + "labelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "600", + "lineHeight": "1" + }, + "labelColor": "rgba(0, 0, 0, 0.54)", + "position": "top", + "showTickLabels": true, + "tickLabelFont": { + "family": "Roboto", + "size": 10, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, + "showTicks": true, + "ticksColor": "rgba(0, 0, 0, 0.54)", + "showLine": true, + "lineColor": "rgba(0, 0, 0, 0.54)", + "showSplitLines": true, + "splitLinesColor": "rgba(0, 0, 0, 0.12)" + }, + "grid": { + "show": false, + "backgroundColor": null, + "borderWidth": 1, + "borderColor": "#ccc" + }, + "legendColumnTitleFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "legendColumnTitleColor": "rgba(0, 0, 0, 0.38)", + "legendValueFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "500", + "lineHeight": "16px" + }, + "legendValueColor": "rgba(0, 0, 0, 0.87)", + "tooltipLabelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "tooltipLabelColor": "rgba(0, 0, 0, 0.76)", + "tooltipHideZeroValues": null, + "padding": "12px" }, - "title": "{i18n:api-usage.telemetry-persistence-hourly-activity}", + "title": "{i18n:api-usage.rule-engine-daily-activity}", "dropShadow": true, "enableFullscreen": true, "titleStyle": null, @@ -2652,14 +4529,21 @@ "actions": { "headerButton": [ { - "name": "{i18n:api-usage.view-details}", - "icon": "insert_chart", + "name": "{i18n:api-usage.view-statistics}", + "buttonType": "icon", + "icon": "show_chart", + "buttonColor": "rgba(0, 0, 0, 0.87)", + "customButtonStyle": {}, + "useShowWidgetActionFunction": null, + "showWidgetActionFunction": "return true;", "type": "openDashboardState", - "targetDashboardStateId": "telemetry_persistence", - "setEntityId": false, + "targetDashboardStateId": "rule_engine_statistics", + "setEntityId": true, "stateEntityParamName": null, "openRightLayout": false, - "id": "16707efb-e572-bd02-c219-55fc1b0f672a" + "openInSeparateDialog": false, + "openInPopover": false, + "id": "2592147a-3f62-987a-78c0-cdb775fb4233" } ] }, @@ -2701,14 +4585,14 @@ "displayTypePrefix": true }, "margin": "0px", - "borderRadius": "0px", + "borderRadius": "4px", "iconSize": "0px" }, "row": 0, "col": 0, - "id": "a3c2f1bb-7d3a-f11c-7b3d-28cd84fdfe34" + "id": "84fbe63a-bcb6-7bc1-8af0-46b3b1ee5adc" }, - "5cebd4f1-ff6e-62f9-025c-8e7583c3d66a": { + "43a2b982-6c02-d9bd-71ee-34e8e6cf8893": { "typeFullFqn": "system.time_series_chart", "type": "timeseries", "sizeX": 8, @@ -2722,11 +4606,12 @@ "filterId": null, "dataKeys": [ { - "name": "createdAlarmsCountHourly", + "name": "ruleEngineExecutionCount", "type": "timeseries", - "label": "{i18n:api-usage.alarms-created}", - "color": "#d35a00", + "label": "{i18n:api-usage.rule-engine-executions}", + "color": "#AB00FF", "settings": { + "yAxisId": "default", "showInLegend": true, "dataHiddenByDefault": false, "type": "bar", @@ -2749,6 +4634,8 @@ "lineHeight": "1" }, "pointLabelColor": "rgba(0, 0, 0, 0.76)", + "enablePointLabelBackground": false, + "pointLabelBackground": "rgba(255,255,255,0.56)", "pointShape": "emptyCircle", "pointSize": 4, "fillAreaSettings": { @@ -2775,6 +4662,8 @@ "lineHeight": "1" }, "labelColor": "rgba(0, 0, 0, 0.76)", + "enableLabelBackground": false, + "labelBackground": "rgba(255,255,255,0.56)", "backgroundSettings": { "type": "none", "opacity": 0.4, @@ -2783,15 +4672,20 @@ "end": 0 } } + }, + "comparisonSettings": { + "showValuesForComparison": false, + "comparisonValuesLabel": "", + "color": "" } }, - "_hash": 0.0661644137210089, + "_hash": 0.5125470598651091, + "aggregationType": null, "units": null, "decimals": null, "funcBody": null, "usePostProcessing": null, - "postFuncBody": null, - "aggregationType": null + "postFuncBody": null } ], "alarmFilterConfig": { @@ -2802,22 +4696,33 @@ } ], "timewindow": { - "hideInterval": false, - "hideLastInterval": false, - "hideQuickInterval": false, "hideAggregation": false, "hideAggInterval": false, "hideTimezone": false, - "selectedTab": 0, + "selectedTab": 1, "realtime": { "realtimeType": 0, - "timewindowMs": 86400000, + "interval": 1000, + "timewindowMs": 60000, + "quickInterval": "CURRENT_DAY", + "hideInterval": false, + "hideLastInterval": false, + "hideQuickInterval": false + }, + "history": { + "historyType": 0, + "interval": 2592000000, + "timewindowMs": 31536000000, + "fixedTimewindow": null, "quickInterval": "CURRENT_DAY", - "interval": 300000 + "hideInterval": false, + "hideLastInterval": false, + "hideFixedInterval": false, + "hideQuickInterval": false }, "aggregation": { "type": "NONE", - "limit": 50000 + "limit": 25000 }, "timezone": null }, @@ -2891,6 +4796,7 @@ "lineHeight": "1" }, "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, "showTicks": true, "ticksColor": "rgba(0, 0, 0, 0.54)", "showLine": true, @@ -2901,9 +4807,9 @@ "noAggregationBarWidthSettings": { "strategy": "group", "groupWidth": { - "relative": false, - "relativeWidth": 2, - "absoluteWidth": 3600000 + "relative": true, + "relativeWidth": 6, + "absoluteWidth": 1800000 }, "barWidth": { "relative": true, @@ -2929,7 +4835,8 @@ "showMax": false, "showAvg": false, "showTotal": true, - "showLatest": false + "showLatest": false, + "valueFormat": null }, "showTooltip": true, "tooltipTrigger": "axis", @@ -2946,7 +4853,9 @@ "tooltipDateFormat": { "format": "yyyy-MM-dd HH:mm:ss", "lastUpdateAgo": false, - "custom": false + "custom": false, + "auto": true, + "autoDateFormatSettings": {} }, "tooltipDateFont": { "family": "Roboto", @@ -2970,17 +4879,86 @@ "animationEasingUpdate": "cubicOut", "animationDelayUpdate": 0 }, - "background": { - "type": "color", - "color": "#fff", - "overlay": { - "enabled": false, - "color": "rgba(255,255,255,0.72)", - "blur": 3 - } - } + "background": { + "type": "color", + "color": "#fff", + "overlay": { + "enabled": false, + "color": "rgba(255,255,255,0.72)", + "blur": 3 + } + }, + "comparisonEnabled": false, + "timeForComparison": "previousInterval", + "comparisonCustomIntervalValue": 7200000, + "comparisonXAxis": { + "show": true, + "label": "", + "labelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "600", + "lineHeight": "1" + }, + "labelColor": "rgba(0, 0, 0, 0.54)", + "position": "top", + "showTickLabels": true, + "tickLabelFont": { + "family": "Roboto", + "size": 10, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, + "showTicks": true, + "ticksColor": "rgba(0, 0, 0, 0.54)", + "showLine": true, + "lineColor": "rgba(0, 0, 0, 0.54)", + "showSplitLines": true, + "splitLinesColor": "rgba(0, 0, 0, 0.12)" + }, + "grid": { + "show": false, + "backgroundColor": null, + "borderWidth": 1, + "borderColor": "#ccc" + }, + "legendColumnTitleFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "legendColumnTitleColor": "rgba(0, 0, 0, 0.38)", + "legendValueFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "500", + "lineHeight": "16px" + }, + "legendValueColor": "rgba(0, 0, 0, 0.87)", + "tooltipLabelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "tooltipLabelColor": "rgba(0, 0, 0, 0.76)", + "tooltipHideZeroValues": null, + "padding": "12px" }, - "title": "{i18n:api-usage.alarms-created-hourly-activity}", + "title": "{i18n:api-usage.rule-engine-monthly-activity}", "dropShadow": true, "enableFullscreen": true, "titleStyle": null, @@ -2988,19 +4966,21 @@ "actions": { "headerButton": [ { - "name": "{i18n:api-usage.view-details}", - "icon": "insert_chart", + "name": "{i18n:api-usage.view-statistics}", + "buttonType": "icon", + "icon": "show_chart", + "buttonColor": "rgba(0, 0, 0, 0.87)", + "customButtonStyle": {}, + "useShowWidgetActionFunction": null, + "showWidgetActionFunction": "return true;", "type": "openDashboardState", - "targetDashboardStateId": "alarms_created", - "setEntityId": false, + "targetDashboardStateId": "rule_engine_statistics", + "setEntityId": true, "stateEntityParamName": null, - "openInSeparateDialog": null, - "dialogTitle": null, - "dialogHideDashboardToolbar": true, - "dialogWidth": null, - "dialogHeight": null, "openRightLayout": false, - "id": "371882f9-ea23-3abc-fca8-9449c5dfdd6b" + "openInSeparateDialog": false, + "openInPopover": false, + "id": "b6ba96cf-48b8-f40f-f010-10b95e7dc819" } ] }, @@ -3042,14 +5022,14 @@ "displayTypePrefix": true }, "margin": "0px", - "borderRadius": "0px", + "borderRadius": "4px", "iconSize": "0px" }, "row": 0, "col": 0, - "id": "5cebd4f1-ff6e-62f9-025c-8e7583c3d66a" + "id": "43a2b982-6c02-d9bd-71ee-34e8e6cf8893" }, - "bc0c8840-a9b5-5583-de7b-9e9450f5d8fe": { + "76fe83c9-c30f-00a5-6299-40c759ca6705": { "typeFullFqn": "system.time_series_chart", "type": "timeseries", "sizeX": 8, @@ -3063,140 +5043,34 @@ "filterId": null, "dataKeys": [ { - "name": "emailCountHourly", + "name": "jsExecutionCountHourly", "type": "timeseries", - "label": "{i18n:api-usage.email-messages}", - "color": "#4caf50", + "label": "{i18n:api-usage.javascript-function-executions}", + "color": "#FF9900", "settings": { - "showInLegend": true, - "dataHiddenByDefault": false, - "type": "bar", - "lineSettings": { - "showLine": true, - "step": false, - "stepType": "start", - "smooth": false, - "lineType": "solid", - "lineWidth": 2, - "showPoints": false, - "showPointLabel": false, - "pointLabelPosition": "top", - "pointLabelFont": { - "family": "Roboto", - "size": 11, - "sizeUnit": "px", - "style": "normal", - "weight": "400", - "lineHeight": "1" - }, - "pointLabelColor": "rgba(0, 0, 0, 0.76)", - "pointShape": "emptyCircle", - "pointSize": 4, - "fillAreaSettings": { - "type": "none", - "opacity": 0.4, - "gradient": { - "start": 100, - "end": 0 - } + "excludeFromStacking": false, + "hideDataByDefault": false, + "disableDataHiding": false, + "removeFromLegend": false, + "showLines": false, + "fillLines": false, + "showPoints": false, + "showPointShape": "circle", + "pointShapeFormatter": "", + "showPointsLineWidth": 5, + "showPointsRadius": 3, + "showSeparateAxis": false, + "axisPosition": "left", + "thresholds": [ + { + "thresholdValueSource": "predefinedValue" } + ], + "comparisonSettings": { + "showValuesForComparison": true }, - "barSettings": { - "showBorder": false, - "borderWidth": 2, - "borderRadius": 0, - "showLabel": false, - "labelPosition": "top", - "labelFont": { - "family": "Roboto", - "size": 11, - "sizeUnit": "px", - "style": "normal", - "weight": "400", - "lineHeight": "1" - }, - "labelColor": "rgba(0, 0, 0, 0.76)", - "backgroundSettings": { - "type": "none", - "opacity": 0.4, - "gradient": { - "start": 100, - "end": 0 - } - } - } - }, - "_hash": 0.1348755140779876, - "units": null, - "decimals": null, - "funcBody": null, - "usePostProcessing": null, - "postFuncBody": null, - "aggregationType": null - }, - { - "name": "smsCountHourly", - "type": "timeseries", - "label": "{i18n:api-usage.sms-messages}", - "color": "#f36021", - "settings": { - "showInLegend": true, - "dataHiddenByDefault": false, "type": "bar", - "lineSettings": { - "showLine": true, - "step": false, - "stepType": "start", - "smooth": false, - "lineType": "solid", - "lineWidth": 2, - "showPoints": false, - "showPointLabel": false, - "pointLabelPosition": "top", - "pointLabelFont": { - "family": "Roboto", - "size": 11, - "sizeUnit": "px", - "style": "normal", - "weight": "400", - "lineHeight": "1" - }, - "pointLabelColor": "rgba(0, 0, 0, 0.76)", - "pointShape": "emptyCircle", - "pointSize": 4, - "fillAreaSettings": { - "type": "none", - "opacity": 0.4, - "gradient": { - "start": 100, - "end": 0 - } - } - }, - "barSettings": { - "showBorder": false, - "borderWidth": 2, - "borderRadius": 0, - "showLabel": false, - "labelPosition": "top", - "labelFont": { - "family": "Roboto", - "size": 11, - "sizeUnit": "px", - "style": "normal", - "weight": "400", - "lineHeight": "1" - }, - "labelColor": "rgba(0, 0, 0, 0.76)", - "backgroundSettings": { - "type": "none", - "opacity": 0.4, - "gradient": { - "start": 100, - "end": 0 - } - } - } + "yAxisId": "default" }, "_hash": 0.0661644137210089, "units": null, @@ -3215,24 +5089,16 @@ } ], "timewindow": { - "hideInterval": false, - "hideLastInterval": false, - "hideQuickInterval": false, - "hideAggregation": false, - "hideAggInterval": false, - "hideTimezone": false, "selectedTab": 0, "realtime": { "realtimeType": 0, - "timewindowMs": 86400000, - "quickInterval": "CURRENT_DAY", - "interval": 300000 + "interval": 3600000, + "timewindowMs": 86400000 }, "aggregation": { "type": "NONE", "limit": 50000 - }, - "timezone": null + } }, "showTitle": true, "backgroundColor": "#FFFFFF", @@ -3304,6 +5170,7 @@ "lineHeight": "1" }, "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, "showTicks": true, "ticksColor": "rgba(0, 0, 0, 0.54)", "showLine": true, @@ -3314,9 +5181,9 @@ "noAggregationBarWidthSettings": { "strategy": "group", "groupWidth": { - "relative": false, - "relativeWidth": 2, - "absoluteWidth": 3600000 + "relative": true, + "relativeWidth": 6, + "absoluteWidth": 1800000 }, "barWidth": { "relative": true, @@ -3342,7 +5209,8 @@ "showMax": false, "showAvg": false, "showTotal": true, - "showLatest": false + "showLatest": false, + "valueFormat": null }, "showTooltip": true, "tooltipTrigger": "axis", @@ -3359,7 +5227,9 @@ "tooltipDateFormat": { "format": "yyyy-MM-dd HH:mm:ss", "lastUpdateAgo": false, - "custom": false + "custom": false, + "auto": true, + "autoDateFormatSettings": {} }, "tooltipDateFont": { "family": "Roboto", @@ -3391,32 +5261,83 @@ "color": "rgba(255,255,255,0.72)", "blur": 3 } - } - }, - "title": "{i18n:api-usage.notifications-hourly-activity}", - "dropShadow": true, - "enableFullscreen": true, - "titleStyle": null, - "configMode": "basic", - "actions": { - "headerButton": [ - { - "name": "{i18n:api-usage.view-details}", - "icon": "insert_chart", - "type": "openDashboardState", - "targetDashboardStateId": "notifications", - "setEntityId": false, - "stateEntityParamName": null, - "openInSeparateDialog": null, - "dialogTitle": null, - "dialogHideDashboardToolbar": true, - "dialogWidth": null, - "dialogHeight": null, - "openRightLayout": false, - "id": "49aefac0-ec5e-d6f3-f39c-8744759f4b19" - } - ] + }, + "comparisonEnabled": false, + "timeForComparison": "previousInterval", + "comparisonCustomIntervalValue": 7200000, + "comparisonXAxis": { + "show": true, + "label": "", + "labelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "600", + "lineHeight": "1" + }, + "labelColor": "rgba(0, 0, 0, 0.54)", + "position": "top", + "showTickLabels": true, + "tickLabelFont": { + "family": "Roboto", + "size": 10, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, + "showTicks": true, + "ticksColor": "rgba(0, 0, 0, 0.54)", + "showLine": true, + "lineColor": "rgba(0, 0, 0, 0.54)", + "showSplitLines": true, + "splitLinesColor": "rgba(0, 0, 0, 0.12)" + }, + "grid": { + "show": false, + "backgroundColor": null, + "borderWidth": 1, + "borderColor": "#ccc" + }, + "legendColumnTitleFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "legendColumnTitleColor": "rgba(0, 0, 0, 0.38)", + "legendValueFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "500", + "lineHeight": "16px" + }, + "legendValueColor": "rgba(0, 0, 0, 0.87)", + "tooltipLabelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "tooltipLabelColor": "rgba(0, 0, 0, 0.76)", + "tooltipHideZeroValues": null, + "padding": "12px" }, + "title": "{i18n:api-usage.javascript-function-executions-hourly-activity}", + "dropShadow": true, + "enableFullscreen": true, + "titleStyle": null, + "configMode": "basic", + "actions": {}, "showTitleIcon": false, "titleIcon": "thermostat", "iconColor": "#1F6BDD", @@ -3455,14 +5376,14 @@ "displayTypePrefix": true }, "margin": "0px", - "borderRadius": "0px", + "borderRadius": "4px", "iconSize": "0px" }, "row": 0, "col": 0, - "id": "bc0c8840-a9b5-5583-de7b-9e9450f5d8fe" + "id": "76fe83c9-c30f-00a5-6299-40c759ca6705" }, - "0b091dc3-eec3-847e-d0ad-fdf12d474e7a": { + "a43598d1-7bfd-f329-ee61-c343f34f069f": { "typeFullFqn": "system.time_series_chart", "type": "timeseries", "sizeX": 8, @@ -3476,10 +5397,10 @@ "filterId": null, "dataKeys": [ { - "name": "transportMsgCountHourly", + "name": "jsExecutionCountHourly", "type": "timeseries", - "label": "{i18n:api-usage.transport-messages}", - "color": "#2196f3", + "label": "{i18n:api-usage.javascript-function-executions}", + "color": "#FF9900", "settings": { "excludeFromStacking": false, "hideDataByDefault": false, @@ -3502,58 +5423,26 @@ "comparisonSettings": { "showValuesForComparison": true }, - "type": "bar" + "type": "bar", + "yAxisId": "default" }, "_hash": 0.0661644137210089, "units": null, "decimals": null, "funcBody": null, "usePostProcessing": null, - "postFuncBody": null - }, - { - "name": "transportDataPointsCountHourly", - "type": "timeseries", - "label": "{i18n:api-usage.transport-data-points}", - "color": "#4caf50", - "settings": { - "excludeFromStacking": false, - "hideDataByDefault": false, - "disableDataHiding": false, - "removeFromLegend": false, - "showLines": false, - "fillLines": false, - "showPoints": false, - "showPointShape": "circle", - "pointShapeFormatter": "", - "showPointsLineWidth": 5, - "showPointsRadius": 3, - "showSeparateAxis": false, - "axisPosition": "left", - "thresholds": [ - { - "thresholdValueSource": "predefinedValue" - } - ], - "comparisonSettings": { - "showValuesForComparison": true - }, - "type": "bar" - }, - "_hash": 0.46849996721308895, - "units": null, - "decimals": null, - "funcBody": null, - "usePostProcessing": null, - "postFuncBody": null + "postFuncBody": null, + "aggregationType": null } - ] + ], + "alarmFilterConfig": { + "statusList": [ + "ACTIVE" + ] + } } ], "timewindow": { - "hideInterval": false, - "hideLastInterval": false, - "hideQuickInterval": false, "hideAggregation": false, "hideAggInterval": false, "hideTimezone": false, @@ -3644,6 +5533,7 @@ "lineHeight": "1" }, "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, "showTicks": true, "ticksColor": "rgba(0, 0, 0, 0.54)", "showLine": true, @@ -3654,8 +5544,8 @@ "noAggregationBarWidthSettings": { "strategy": "group", "groupWidth": { - "relative": false, - "relativeWidth": 2, + "relative": true, + "relativeWidth": 6, "absoluteWidth": 1800000 }, "barWidth": { @@ -3682,7 +5572,8 @@ "showMax": false, "showAvg": false, "showTotal": true, - "showLatest": false + "showLatest": false, + "valueFormat": null }, "showTooltip": true, "tooltipTrigger": "axis", @@ -3699,7 +5590,9 @@ "tooltipDateFormat": { "format": "yyyy-MM-dd HH:mm:ss", "lastUpdateAgo": false, - "custom": false + "custom": false, + "auto": true, + "autoDateFormatSettings": {} }, "tooltipDateFont": { "family": "Roboto", @@ -3731,9 +5624,78 @@ "color": "rgba(255,255,255,0.72)", "blur": 3 } - } + }, + "comparisonEnabled": false, + "timeForComparison": "previousInterval", + "comparisonCustomIntervalValue": 7200000, + "comparisonXAxis": { + "show": true, + "label": "", + "labelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "600", + "lineHeight": "1" + }, + "labelColor": "rgba(0, 0, 0, 0.54)", + "position": "top", + "showTickLabels": true, + "tickLabelFont": { + "family": "Roboto", + "size": 10, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, + "showTicks": true, + "ticksColor": "rgba(0, 0, 0, 0.54)", + "showLine": true, + "lineColor": "rgba(0, 0, 0, 0.54)", + "showSplitLines": true, + "splitLinesColor": "rgba(0, 0, 0, 0.12)" + }, + "grid": { + "show": false, + "backgroundColor": null, + "borderWidth": 1, + "borderColor": "#ccc" + }, + "legendColumnTitleFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "legendColumnTitleColor": "rgba(0, 0, 0, 0.38)", + "legendValueFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "500", + "lineHeight": "16px" + }, + "legendValueColor": "rgba(0, 0, 0, 0.87)", + "tooltipLabelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "tooltipLabelColor": "rgba(0, 0, 0, 0.76)", + "tooltipHideZeroValues": null, + "padding": "12px" }, - "title": "{i18n:api-usage.transport-daily-activity}", + "title": "{i18n:api-usage.javascript-function-executions-daily-activity}", "dropShadow": true, "enableFullscreen": true, "titleStyle": null, @@ -3777,14 +5739,14 @@ "displayTypePrefix": true }, "margin": "0px", - "borderRadius": "0px", + "borderRadius": "4px", "iconSize": "0px" }, "row": 0, "col": 0, - "id": "0b091dc3-eec3-847e-d0ad-fdf12d474e7a" + "id": "a43598d1-7bfd-f329-ee61-c343f34f069f" }, - "536d7104-49f8-fde6-5827-61b8419f15ec": { + "3ebd62a8-dcb7-c96b-8571-e61084248f5b": { "typeFullFqn": "system.time_series_chart", "type": "timeseries", "sizeX": 8, @@ -3798,10 +5760,10 @@ "filterId": null, "dataKeys": [ { - "name": "transportMsgCount", + "name": "jsExecutionCount", "type": "timeseries", - "label": "{i18n:api-usage.transport-messages}", - "color": "#2196f3", + "label": "{i18n:api-usage.javascript-function-executions}", + "color": "#FF9900", "settings": { "excludeFromStacking": false, "hideDataByDefault": false, @@ -3824,7 +5786,8 @@ "comparisonSettings": { "showValuesForComparison": true }, - "type": "bar" + "type": "bar", + "yAxisId": "default" }, "_hash": 0.0661644137210089, "units": null, @@ -3833,73 +5796,38 @@ "usePostProcessing": null, "postFuncBody": null, "aggregationType": null - }, - { - "name": "transportDataPointsCount", - "type": "timeseries", - "label": "{i18n:api-usage.transport-data-points}", - "color": "#4caf50", - "settings": { - "excludeFromStacking": false, - "hideDataByDefault": false, - "disableDataHiding": false, - "removeFromLegend": false, - "showLines": false, - "fillLines": false, - "showPoints": false, - "showPointShape": "circle", - "pointShapeFormatter": "", - "showPointsLineWidth": 5, - "showPointsRadius": 3, - "showSeparateAxis": false, - "axisPosition": "left", - "thresholds": [ - { - "thresholdValueSource": "predefinedValue" - } - ], - "comparisonSettings": { - "showValuesForComparison": true - }, - "type": "bar" - }, - "_hash": 0.46849996721308895, - "units": null, - "decimals": null, - "funcBody": null, - "usePostProcessing": null, - "postFuncBody": null, - "aggregationType": null } - ], - "alarmFilterConfig": { - "statusList": [ - "ACTIVE" - ] - } + ] } ], "timewindow": { - "hideInterval": false, - "hideLastInterval": false, - "hideQuickInterval": false, "hideAggregation": false, "hideAggInterval": false, "hideTimezone": false, "selectedTab": 1, + "realtime": { + "realtimeType": 0, + "interval": 1000, + "timewindowMs": 60000, + "quickInterval": "CURRENT_DAY", + "hideInterval": false, + "hideLastInterval": false, + "hideQuickInterval": false + }, "history": { "historyType": 0, + "interval": 2592000000, "timewindowMs": 31536000000, - "interval": 86400000, - "fixedTimewindow": { - "startTimeMs": 1709729389667, - "endTimeMs": 1709815789667 - }, - "quickInterval": "CURRENT_DAY" + "fixedTimewindow": null, + "quickInterval": "CURRENT_DAY", + "hideInterval": false, + "hideLastInterval": false, + "hideFixedInterval": false, + "hideQuickInterval": false }, "aggregation": { "type": "NONE", - "limit": 1000 + "limit": 25000 }, "timezone": null }, @@ -3973,6 +5901,7 @@ "lineHeight": "1" }, "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, "showTicks": true, "ticksColor": "rgba(0, 0, 0, 0.54)", "showLine": true, @@ -3985,7 +5914,7 @@ "groupWidth": { "relative": true, "relativeWidth": 6, - "absoluteWidth": 900000000 + "absoluteWidth": 1800000 }, "barWidth": { "relative": true, @@ -4011,58 +5940,130 @@ "showMax": false, "showAvg": false, "showTotal": true, - "showLatest": false + "showLatest": false, + "valueFormat": null + }, + "showTooltip": true, + "tooltipTrigger": "axis", + "tooltipValueFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "500", + "lineHeight": "16px" + }, + "tooltipValueColor": "rgba(0, 0, 0, 0.76)", + "tooltipShowDate": true, + "tooltipDateFormat": { + "format": "yyyy-MM-dd HH:mm:ss", + "lastUpdateAgo": false, + "custom": false, + "auto": true, + "autoDateFormatSettings": {} + }, + "tooltipDateFont": { + "family": "Roboto", + "size": 11, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "tooltipDateColor": "rgba(0, 0, 0, 0.76)", + "tooltipDateInterval": true, + "tooltipBackgroundColor": "rgba(255, 255, 255, 0.76)", + "tooltipBackgroundBlur": 4, + "animation": { + "animation": true, + "animationThreshold": 2000, + "animationDuration": 1000, + "animationEasing": "cubicOut", + "animationDelay": 0, + "animationDurationUpdate": 300, + "animationEasingUpdate": "cubicOut", + "animationDelayUpdate": 0 + }, + "background": { + "type": "color", + "color": "#fff", + "overlay": { + "enabled": false, + "color": "rgba(255,255,255,0.72)", + "blur": 3 + } + }, + "comparisonEnabled": false, + "timeForComparison": "previousInterval", + "comparisonCustomIntervalValue": 7200000, + "comparisonXAxis": { + "show": true, + "label": "", + "labelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "600", + "lineHeight": "1" + }, + "labelColor": "rgba(0, 0, 0, 0.54)", + "position": "top", + "showTickLabels": true, + "tickLabelFont": { + "family": "Roboto", + "size": 10, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, + "showTicks": true, + "ticksColor": "rgba(0, 0, 0, 0.54)", + "showLine": true, + "lineColor": "rgba(0, 0, 0, 0.54)", + "showSplitLines": true, + "splitLinesColor": "rgba(0, 0, 0, 0.12)" + }, + "grid": { + "show": false, + "backgroundColor": null, + "borderWidth": 1, + "borderColor": "#ccc" }, - "showTooltip": true, - "tooltipTrigger": "axis", - "tooltipValueFont": { + "legendColumnTitleFont": { "family": "Roboto", "size": 12, "sizeUnit": "px", "style": "normal", - "weight": "500", + "weight": "400", "lineHeight": "16px" }, - "tooltipValueColor": "rgba(0, 0, 0, 0.76)", - "tooltipShowDate": true, - "tooltipDateFormat": { - "format": "yyyy-MM-dd HH:mm:ss", - "lastUpdateAgo": false, - "custom": false + "legendColumnTitleColor": "rgba(0, 0, 0, 0.38)", + "legendValueFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "500", + "lineHeight": "16px" }, - "tooltipDateFont": { + "legendValueColor": "rgba(0, 0, 0, 0.87)", + "tooltipLabelFont": { "family": "Roboto", - "size": 11, + "size": 12, "sizeUnit": "px", "style": "normal", "weight": "400", "lineHeight": "16px" }, - "tooltipDateColor": "rgba(0, 0, 0, 0.76)", - "tooltipDateInterval": true, - "tooltipBackgroundColor": "rgba(255, 255, 255, 0.76)", - "tooltipBackgroundBlur": 4, - "animation": { - "animation": true, - "animationThreshold": 2000, - "animationDuration": 1000, - "animationEasing": "cubicOut", - "animationDelay": 0, - "animationDurationUpdate": 300, - "animationEasingUpdate": "cubicOut", - "animationDelayUpdate": 0 - }, - "background": { - "type": "color", - "color": "#fff", - "overlay": { - "enabled": false, - "color": "rgba(255,255,255,0.72)", - "blur": 3 - } - } + "tooltipLabelColor": "rgba(0, 0, 0, 0.76)", + "tooltipHideZeroValues": null, + "padding": "12px" }, - "title": "{i18n:api-usage.transport-daily-activity}", + "title": "{i18n:api-usage.javascript-function-executions-monthly-activity}", "dropShadow": true, "enableFullscreen": true, "titleStyle": null, @@ -4106,14 +6107,14 @@ "displayTypePrefix": true }, "margin": "0px", - "borderRadius": "0px", + "borderRadius": "4px", "iconSize": "0px" }, "row": 0, "col": 0, - "id": "536d7104-49f8-fde6-5827-61b8419f15ec" + "id": "3ebd62a8-dcb7-c96b-8571-e61084248f5b" }, - "c77e417c-ad9d-8e23-3ea1-c75edd653bc0": { + "88e25971-e5cb-eebb-3c7c-1ce33a8a38f4": { "typeFullFqn": "system.time_series_chart", "type": "timeseries", "sizeX": 8, @@ -4127,10 +6128,10 @@ "filterId": null, "dataKeys": [ { - "name": "ruleEngineExecutionCountHourly", + "name": "tbelExecutionCountHourly", "type": "timeseries", - "label": "{i18n:api-usage.rule-engine-executions}", - "color": "#ab00ff", + "label": "{i18n:api-usage.tbel-function-executions}", + "color": "#4CAF50", "settings": { "excludeFromStacking": false, "hideDataByDefault": false, @@ -4153,41 +6154,36 @@ "comparisonSettings": { "showValuesForComparison": true }, - "type": "bar" + "type": "bar", + "yAxisId": "default" }, "_hash": 0.0661644137210089, "units": null, "decimals": null, "funcBody": null, "usePostProcessing": null, - "postFuncBody": null + "postFuncBody": null, + "aggregationType": null } - ] + ], + "alarmFilterConfig": { + "statusList": [ + "ACTIVE" + ] + } } ], "timewindow": { - "hideInterval": false, - "hideLastInterval": false, - "hideQuickInterval": false, - "hideAggregation": false, - "hideAggInterval": false, - "hideTimezone": false, - "selectedTab": 1, - "history": { - "historyType": 0, - "timewindowMs": 2592000000, - "interval": 86400000, - "fixedTimewindow": { - "startTimeMs": 1709729900300, - "endTimeMs": 1709816300300 - }, - "quickInterval": "CURRENT_DAY" + "selectedTab": 0, + "realtime": { + "realtimeType": 0, + "interval": 3600000, + "timewindowMs": 86400000 }, "aggregation": { - "type": "SUM", - "limit": 25000 - }, - "timezone": null + "type": "NONE", + "limit": 50000 + } }, "showTitle": true, "backgroundColor": "#FFFFFF", @@ -4259,6 +6255,7 @@ "lineHeight": "1" }, "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, "showTicks": true, "ticksColor": "rgba(0, 0, 0, 0.54)", "showLine": true, @@ -4269,8 +6266,8 @@ "noAggregationBarWidthSettings": { "strategy": "group", "groupWidth": { - "relative": false, - "relativeWidth": 2, + "relative": true, + "relativeWidth": 6, "absoluteWidth": 1800000 }, "barWidth": { @@ -4297,7 +6294,8 @@ "showMax": false, "showAvg": false, "showTotal": true, - "showLatest": false + "showLatest": false, + "valueFormat": null }, "showTooltip": true, "tooltipTrigger": "axis", @@ -4314,7 +6312,9 @@ "tooltipDateFormat": { "format": "yyyy-MM-dd HH:mm:ss", "lastUpdateAgo": false, - "custom": false + "custom": false, + "auto": true, + "autoDateFormatSettings": {} }, "tooltipDateFont": { "family": "Roboto", @@ -4346,9 +6346,78 @@ "color": "rgba(255,255,255,0.72)", "blur": 3 } - } + }, + "comparisonEnabled": false, + "timeForComparison": "previousInterval", + "comparisonCustomIntervalValue": 7200000, + "comparisonXAxis": { + "show": true, + "label": "", + "labelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "600", + "lineHeight": "1" + }, + "labelColor": "rgba(0, 0, 0, 0.54)", + "position": "top", + "showTickLabels": true, + "tickLabelFont": { + "family": "Roboto", + "size": 10, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, + "showTicks": true, + "ticksColor": "rgba(0, 0, 0, 0.54)", + "showLine": true, + "lineColor": "rgba(0, 0, 0, 0.54)", + "showSplitLines": true, + "splitLinesColor": "rgba(0, 0, 0, 0.12)" + }, + "grid": { + "show": false, + "backgroundColor": null, + "borderWidth": 1, + "borderColor": "#ccc" + }, + "legendColumnTitleFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "legendColumnTitleColor": "rgba(0, 0, 0, 0.38)", + "legendValueFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "500", + "lineHeight": "16px" + }, + "legendValueColor": "rgba(0, 0, 0, 0.87)", + "tooltipLabelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "tooltipLabelColor": "rgba(0, 0, 0, 0.76)", + "tooltipHideZeroValues": null, + "padding": "12px" }, - "title": "{i18n:api-usage.rule-engine-daily-activity}", + "title": "{i18n:api-usage.tbel-function-executions-hourly-activity}", "dropShadow": true, "enableFullscreen": true, "titleStyle": null, @@ -4392,14 +6461,14 @@ "displayTypePrefix": true }, "margin": "0px", - "borderRadius": "0px", + "borderRadius": "4px", "iconSize": "0px" }, "row": 0, "col": 0, - "id": "c77e417c-ad9d-8e23-3ea1-c75edd653bc0" + "id": "88e25971-e5cb-eebb-3c7c-1ce33a8a38f4" }, - "870904d2-d2e1-a1b9-ce56-b03fd47259b5": { + "a1b5731c-e3b3-8cfb-7c50-3abcdce891d2": { "typeFullFqn": "system.time_series_chart", "type": "timeseries", "sizeX": 8, @@ -4413,10 +6482,10 @@ "filterId": null, "dataKeys": [ { - "name": "ruleEngineExecutionCount", + "name": "tbelExecutionCountHourly", "type": "timeseries", - "label": "{i18n:api-usage.rule-engine-executions}", - "color": "#ab00ff", + "label": "{i18n:api-usage.tbel-function-executions}", + "color": "#4CAF50", "settings": { "excludeFromStacking": false, "hideDataByDefault": false, @@ -4439,32 +6508,45 @@ "comparisonSettings": { "showValuesForComparison": true }, - "type": "bar" + "type": "bar", + "yAxisId": "default" }, "_hash": 0.0661644137210089, "units": null, "decimals": null, "funcBody": null, "usePostProcessing": null, - "postFuncBody": null + "postFuncBody": null, + "aggregationType": null } - ] + ], + "alarmFilterConfig": { + "statusList": [ + "ACTIVE" + ] + } } ], "timewindow": { - "hideInterval": false, "hideAggregation": false, "hideAggInterval": false, + "hideTimezone": false, "selectedTab": 1, "history": { "historyType": 0, - "timewindowMs": 31536000000, - "interval": 1000 + "timewindowMs": 2592000000, + "interval": 86400000, + "fixedTimewindow": { + "startTimeMs": 1709729389667, + "endTimeMs": 1709815789667 + }, + "quickInterval": "CURRENT_DAY" }, "aggregation": { - "type": "NONE", - "limit": 1000 - } + "type": "SUM", + "limit": 25000 + }, + "timezone": null }, "showTitle": true, "backgroundColor": "#FFFFFF", @@ -4536,6 +6618,7 @@ "lineHeight": "1" }, "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, "showTicks": true, "ticksColor": "rgba(0, 0, 0, 0.54)", "showLine": true, @@ -4548,7 +6631,7 @@ "groupWidth": { "relative": true, "relativeWidth": 6, - "absoluteWidth": 900000000 + "absoluteWidth": 1800000 }, "barWidth": { "relative": true, @@ -4574,7 +6657,8 @@ "showMax": false, "showAvg": false, "showTotal": true, - "showLatest": false + "showLatest": false, + "valueFormat": null }, "showTooltip": true, "tooltipTrigger": "axis", @@ -4591,7 +6675,9 @@ "tooltipDateFormat": { "format": "yyyy-MM-dd HH:mm:ss", "lastUpdateAgo": false, - "custom": false + "custom": false, + "auto": true, + "autoDateFormatSettings": {} }, "tooltipDateFont": { "family": "Roboto", @@ -4623,9 +6709,78 @@ "color": "rgba(255,255,255,0.72)", "blur": 3 } - } + }, + "comparisonEnabled": false, + "timeForComparison": "previousInterval", + "comparisonCustomIntervalValue": 7200000, + "comparisonXAxis": { + "show": true, + "label": "", + "labelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "600", + "lineHeight": "1" + }, + "labelColor": "rgba(0, 0, 0, 0.54)", + "position": "top", + "showTickLabels": true, + "tickLabelFont": { + "family": "Roboto", + "size": 10, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, + "showTicks": true, + "ticksColor": "rgba(0, 0, 0, 0.54)", + "showLine": true, + "lineColor": "rgba(0, 0, 0, 0.54)", + "showSplitLines": true, + "splitLinesColor": "rgba(0, 0, 0, 0.12)" + }, + "grid": { + "show": false, + "backgroundColor": null, + "borderWidth": 1, + "borderColor": "#ccc" + }, + "legendColumnTitleFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "legendColumnTitleColor": "rgba(0, 0, 0, 0.38)", + "legendValueFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "500", + "lineHeight": "16px" + }, + "legendValueColor": "rgba(0, 0, 0, 0.87)", + "tooltipLabelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "tooltipLabelColor": "rgba(0, 0, 0, 0.76)", + "tooltipHideZeroValues": null, + "padding": "12px" }, - "title": "{i18n:api-usage.rule-engine-monthly-activity}", + "title": "{i18n:api-usage.tbel-function-executions-daily-activity}", "dropShadow": true, "enableFullscreen": true, "titleStyle": null, @@ -4669,14 +6824,14 @@ "displayTypePrefix": true }, "margin": "0px", - "borderRadius": "0px", + "borderRadius": "4px", "iconSize": "0px" }, "row": 0, "col": 0, - "id": "870904d2-d2e1-a1b9-ce56-b03fd47259b5" + "id": "a1b5731c-e3b3-8cfb-7c50-3abcdce891d2" }, - "c66e5060-57fd-11e7-6616-65b82c294ac2": { + "efc8d4e9-dee2-b677-c378-c1a666543bf4": { "typeFullFqn": "system.time_series_chart", "type": "timeseries", "sizeX": 8, @@ -4690,10 +6845,10 @@ "filterId": null, "dataKeys": [ { - "name": "jsExecutionCountHourly", + "name": "tbelExecutionCount", "type": "timeseries", - "label": "{i18n:api-usage.javascript-executions}", - "color": "#ff9900", + "label": "{i18n:api-usage.tbel-function-executions}", + "color": "#4CAF50", "settings": { "excludeFromStacking": false, "hideDataByDefault": false, @@ -4716,48 +6871,50 @@ "comparisonSettings": { "showValuesForComparison": true }, - "type": "bar" + "type": "bar", + "yAxisId": "default" }, "_hash": 0.0661644137210089, "units": null, "decimals": null, "funcBody": null, "usePostProcessing": null, - "postFuncBody": null - }, - { - "name": "tbelExecutionCountHourly", - "type": "timeseries", - "label": "{i18n:api-usage.tbel-executions}", - "color": "#4caf50", - "settings": { - "type": "bar" - }, - "_hash": 0.5212969314724616, - "aggregationType": null, - "units": null, - "decimals": null, - "funcBody": null, - "usePostProcessing": null, - "postFuncBody": null + "postFuncBody": null, + "aggregationType": null } ] } ], "timewindow": { - "hideInterval": false, "hideAggregation": false, "hideAggInterval": false, + "hideTimezone": false, "selectedTab": 1, + "realtime": { + "realtimeType": 0, + "interval": 1000, + "timewindowMs": 60000, + "quickInterval": "CURRENT_DAY", + "hideInterval": false, + "hideLastInterval": false, + "hideQuickInterval": false + }, "history": { "historyType": 0, - "timewindowMs": 2592000000, - "interval": 86400000 + "interval": 2592000000, + "timewindowMs": 31536000000, + "fixedTimewindow": null, + "quickInterval": "CURRENT_DAY", + "hideInterval": false, + "hideLastInterval": false, + "hideFixedInterval": false, + "hideQuickInterval": false }, "aggregation": { - "type": "SUM", - "limit": 1000 - } + "type": "NONE", + "limit": 25000 + }, + "timezone": null }, "showTitle": true, "backgroundColor": "#FFFFFF", @@ -4829,6 +6986,7 @@ "lineHeight": "1" }, "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, "showTicks": true, "ticksColor": "rgba(0, 0, 0, 0.54)", "showLine": true, @@ -4839,8 +6997,8 @@ "noAggregationBarWidthSettings": { "strategy": "group", "groupWidth": { - "relative": false, - "relativeWidth": 2, + "relative": true, + "relativeWidth": 6, "absoluteWidth": 1800000 }, "barWidth": { @@ -4867,7 +7025,8 @@ "showMax": false, "showAvg": false, "showTotal": true, - "showLatest": false + "showLatest": false, + "valueFormat": null }, "showTooltip": true, "tooltipTrigger": "axis", @@ -4884,7 +7043,9 @@ "tooltipDateFormat": { "format": "yyyy-MM-dd HH:mm:ss", "lastUpdateAgo": false, - "custom": false + "custom": false, + "auto": true, + "autoDateFormatSettings": {} }, "tooltipDateFont": { "family": "Roboto", @@ -4916,9 +7077,78 @@ "color": "rgba(255,255,255,0.72)", "blur": 3 } - } + }, + "comparisonEnabled": false, + "timeForComparison": "previousInterval", + "comparisonCustomIntervalValue": 7200000, + "comparisonXAxis": { + "show": true, + "label": "", + "labelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "600", + "lineHeight": "1" + }, + "labelColor": "rgba(0, 0, 0, 0.54)", + "position": "top", + "showTickLabels": true, + "tickLabelFont": { + "family": "Roboto", + "size": 10, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, + "showTicks": true, + "ticksColor": "rgba(0, 0, 0, 0.54)", + "showLine": true, + "lineColor": "rgba(0, 0, 0, 0.54)", + "showSplitLines": true, + "splitLinesColor": "rgba(0, 0, 0, 0.12)" + }, + "grid": { + "show": false, + "backgroundColor": null, + "borderWidth": 1, + "borderColor": "#ccc" + }, + "legendColumnTitleFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "legendColumnTitleColor": "rgba(0, 0, 0, 0.38)", + "legendValueFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "500", + "lineHeight": "16px" + }, + "legendValueColor": "rgba(0, 0, 0, 0.87)", + "tooltipLabelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "tooltipLabelColor": "rgba(0, 0, 0, 0.76)", + "tooltipHideZeroValues": null, + "padding": "12px" }, - "title": "{i18n:api-usage.scripts-daily-activity}", + "title": "{i18n:api-usage.tbel-function-executions-monthly-activity}", "dropShadow": true, "enableFullscreen": true, "titleStyle": null, @@ -4962,14 +7192,14 @@ "displayTypePrefix": true }, "margin": "0px", - "borderRadius": "0px", + "borderRadius": "4px", "iconSize": "0px" }, "row": 0, "col": 0, - "id": "c66e5060-57fd-11e7-6616-65b82c294ac2" + "id": "efc8d4e9-dee2-b677-c378-c1a666543bf4" }, - "d0e8603e-5d2e-9287-e2c6-8ccbe9c66806": { + "1249d3e2-6b3a-4e4a-65e9-6ed22959871e": { "typeFullFqn": "system.time_series_chart", "type": "timeseries", "sizeX": 8, @@ -4983,10 +7213,10 @@ "filterId": null, "dataKeys": [ { - "name": "jsExecutionCount", + "name": "storageDataPointsCountHourly", "type": "timeseries", - "label": "{i18n:api-usage.javascript-executions}", - "color": "#ff9900", + "label": "{i18n:api-usage.data-points-storage-days}", + "color": "#1039EE", "settings": { "excludeFromStacking": false, "hideDataByDefault": false, @@ -5009,48 +7239,45 @@ "comparisonSettings": { "showValuesForComparison": true }, - "type": "bar" + "type": "bar", + "yAxisId": "default" }, "_hash": 0.0661644137210089, "units": null, "decimals": null, "funcBody": null, "usePostProcessing": null, - "postFuncBody": null - }, - { - "name": "tbelExecutionCount", - "type": "timeseries", - "label": "{i18n:api-usage.tbel-executions}", - "color": "#4caf50", - "settings": { - "type": "bar" - }, - "_hash": 0.49748239768082403, - "aggregationType": null, - "units": null, - "decimals": null, - "funcBody": null, - "usePostProcessing": null, - "postFuncBody": null + "postFuncBody": null, + "aggregationType": null } - ] + ], + "alarmFilterConfig": { + "statusList": [ + "ACTIVE" + ] + } } ], "timewindow": { - "hideInterval": false, "hideAggregation": false, "hideAggInterval": false, + "hideTimezone": false, "selectedTab": 1, "history": { "historyType": 0, - "timewindowMs": 31536000000, - "interval": 1000 + "timewindowMs": 2592000000, + "interval": 86400000, + "fixedTimewindow": { + "startTimeMs": 1709729389667, + "endTimeMs": 1709815789667 + }, + "quickInterval": "CURRENT_DAY" }, "aggregation": { - "type": "NONE", - "limit": 1000 - } + "type": "SUM", + "limit": 25000 + }, + "timezone": null }, "showTitle": true, "backgroundColor": "#FFFFFF", @@ -5122,6 +7349,7 @@ "lineHeight": "1" }, "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, "showTicks": true, "ticksColor": "rgba(0, 0, 0, 0.54)", "showLine": true, @@ -5134,12 +7362,12 @@ "groupWidth": { "relative": true, "relativeWidth": 6, - "absoluteWidth": 900000000 + "absoluteWidth": 1800000 }, "barWidth": { - "relative": false, + "relative": true, "relativeWidth": 2, - "absoluteWidth": 900000000 + "absoluteWidth": 1000 } }, "showLegend": true, @@ -5160,7 +7388,8 @@ "showMax": false, "showAvg": false, "showTotal": true, - "showLatest": false + "showLatest": false, + "valueFormat": null }, "showTooltip": true, "tooltipTrigger": "axis", @@ -5177,7 +7406,9 @@ "tooltipDateFormat": { "format": "yyyy-MM-dd HH:mm:ss", "lastUpdateAgo": false, - "custom": false + "custom": false, + "auto": true, + "autoDateFormatSettings": {} }, "tooltipDateFont": { "family": "Roboto", @@ -5209,9 +7440,78 @@ "color": "rgba(255,255,255,0.72)", "blur": 3 } - } + }, + "comparisonEnabled": false, + "timeForComparison": "previousInterval", + "comparisonCustomIntervalValue": 7200000, + "comparisonXAxis": { + "show": true, + "label": "", + "labelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "600", + "lineHeight": "1" + }, + "labelColor": "rgba(0, 0, 0, 0.54)", + "position": "top", + "showTickLabels": true, + "tickLabelFont": { + "family": "Roboto", + "size": 10, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, + "showTicks": true, + "ticksColor": "rgba(0, 0, 0, 0.54)", + "showLine": true, + "lineColor": "rgba(0, 0, 0, 0.54)", + "showSplitLines": true, + "splitLinesColor": "rgba(0, 0, 0, 0.12)" + }, + "grid": { + "show": false, + "backgroundColor": null, + "borderWidth": 1, + "borderColor": "#ccc" + }, + "legendColumnTitleFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "legendColumnTitleColor": "rgba(0, 0, 0, 0.38)", + "legendValueFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "500", + "lineHeight": "16px" + }, + "legendValueColor": "rgba(0, 0, 0, 0.87)", + "tooltipLabelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "tooltipLabelColor": "rgba(0, 0, 0, 0.76)", + "tooltipHideZeroValues": null, + "padding": "12px" }, - "title": "{i18n:api-usage.scripts-monthly-activity}", + "title": "{i18n:api-usage.data-points-storage-days-daily-activity}", "dropShadow": true, "enableFullscreen": true, "titleStyle": null, @@ -5255,14 +7555,14 @@ "displayTypePrefix": true }, "margin": "0px", - "borderRadius": "0px", + "borderRadius": "4px", "iconSize": "0px" }, "row": 0, "col": 0, - "id": "d0e8603e-5d2e-9287-e2c6-8ccbe9c66806" + "id": "1249d3e2-6b3a-4e4a-65e9-6ed22959871e" }, - "7f4100d2-41be-4954-d353-1d45000dbbbb": { + "c2f2da29-741d-54f6-5f1d-6f6ae616ea02": { "typeFullFqn": "system.time_series_chart", "type": "timeseries", "sizeX": 8, @@ -5276,10 +7576,10 @@ "filterId": null, "dataKeys": [ { - "name": "storageDataPointsCountHourly", + "name": "storageDataPointsCount", "type": "timeseries", "label": "{i18n:api-usage.data-points-storage-days}", - "color": "#1039ee", + "color": "#1039EE", "settings": { "excludeFromStacking": false, "hideDataByDefault": false, @@ -5302,32 +7602,55 @@ "comparisonSettings": { "showValuesForComparison": true }, - "type": "bar" + "type": "bar", + "yAxisId": "default" }, "_hash": 0.0661644137210089, "units": null, "decimals": null, "funcBody": null, "usePostProcessing": null, - "postFuncBody": null + "postFuncBody": null, + "aggregationType": null } - ] + ], + "alarmFilterConfig": { + "statusList": [ + "ACTIVE" + ] + } } ], "timewindow": { - "hideInterval": false, "hideAggregation": false, "hideAggInterval": false, + "hideTimezone": false, "selectedTab": 1, + "realtime": { + "realtimeType": 0, + "interval": 1000, + "timewindowMs": 60000, + "quickInterval": "CURRENT_DAY", + "hideInterval": false, + "hideLastInterval": false, + "hideQuickInterval": false + }, "history": { "historyType": 0, - "timewindowMs": 2592000000, - "interval": 86400000 + "interval": 2592000000, + "timewindowMs": 31536000000, + "fixedTimewindow": null, + "quickInterval": "CURRENT_DAY", + "hideInterval": false, + "hideLastInterval": false, + "hideFixedInterval": false, + "hideQuickInterval": false }, "aggregation": { - "type": "SUM", - "limit": 1000 - } + "type": "NONE", + "limit": 25000 + }, + "timezone": null }, "showTitle": true, "backgroundColor": "#FFFFFF", @@ -5399,6 +7722,7 @@ "lineHeight": "1" }, "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, "showTicks": true, "ticksColor": "rgba(0, 0, 0, 0.54)", "showLine": true, @@ -5409,8 +7733,8 @@ "noAggregationBarWidthSettings": { "strategy": "group", "groupWidth": { - "relative": false, - "relativeWidth": 2, + "relative": true, + "relativeWidth": 6, "absoluteWidth": 1800000 }, "barWidth": { @@ -5437,7 +7761,8 @@ "showMax": false, "showAvg": false, "showTotal": true, - "showLatest": false + "showLatest": false, + "valueFormat": null }, "showTooltip": true, "tooltipTrigger": "axis", @@ -5454,7 +7779,9 @@ "tooltipDateFormat": { "format": "yyyy-MM-dd HH:mm:ss", "lastUpdateAgo": false, - "custom": false + "custom": false, + "auto": true, + "autoDateFormatSettings": {} }, "tooltipDateFont": { "family": "Roboto", @@ -5486,9 +7813,78 @@ "color": "rgba(255,255,255,0.72)", "blur": 3 } - } + }, + "comparisonEnabled": false, + "timeForComparison": "previousInterval", + "comparisonCustomIntervalValue": 7200000, + "comparisonXAxis": { + "show": true, + "label": "", + "labelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "600", + "lineHeight": "1" + }, + "labelColor": "rgba(0, 0, 0, 0.54)", + "position": "top", + "showTickLabels": true, + "tickLabelFont": { + "family": "Roboto", + "size": 10, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, + "showTicks": true, + "ticksColor": "rgba(0, 0, 0, 0.54)", + "showLine": true, + "lineColor": "rgba(0, 0, 0, 0.54)", + "showSplitLines": true, + "splitLinesColor": "rgba(0, 0, 0, 0.12)" + }, + "grid": { + "show": false, + "backgroundColor": null, + "borderWidth": 1, + "borderColor": "#ccc" + }, + "legendColumnTitleFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "legendColumnTitleColor": "rgba(0, 0, 0, 0.38)", + "legendValueFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "500", + "lineHeight": "16px" + }, + "legendValueColor": "rgba(0, 0, 0, 0.87)", + "tooltipLabelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "tooltipLabelColor": "rgba(0, 0, 0, 0.76)", + "tooltipHideZeroValues": null, + "padding": "12px" }, - "title": "{i18n:api-usage.telemetry-persistence-daily-activity}", + "title": "{i18n:api-usage.data-points-storage-days-monthly-activity}", "dropShadow": true, "enableFullscreen": true, "titleStyle": null, @@ -5532,14 +7928,14 @@ "displayTypePrefix": true }, "margin": "0px", - "borderRadius": "0px", + "borderRadius": "4px", "iconSize": "0px" }, "row": 0, "col": 0, - "id": "7f4100d2-41be-4954-d353-1d45000dbbbb" + "id": "c2f2da29-741d-54f6-5f1d-6f6ae616ea02" }, - "226ef8c9-8488-3664-21ac-0b6217336202": { + "8e07dbe5-aa7a-19c1-c470-5f055df948a7": { "typeFullFqn": "system.time_series_chart", "type": "timeseries", "sizeX": 8, @@ -5553,10 +7949,10 @@ "filterId": null, "dataKeys": [ { - "name": "storageDataPointsCount", + "name": "createdAlarmsCountHourly", "type": "timeseries", - "label": "{i18n:api-usage.data-points-storage-days}", - "color": "#1039ee", + "label": "{i18n:api-usage.alarms-created}", + "color": "#D35A00", "settings": { "excludeFromStacking": false, "hideDataByDefault": false, @@ -5579,31 +7975,35 @@ "comparisonSettings": { "showValuesForComparison": true }, - "type": "bar" + "type": "bar", + "yAxisId": "default" }, "_hash": 0.0661644137210089, "units": null, "decimals": null, "funcBody": null, "usePostProcessing": null, - "postFuncBody": null + "postFuncBody": null, + "aggregationType": null } - ] + ], + "alarmFilterConfig": { + "statusList": [ + "ACTIVE" + ] + } } ], "timewindow": { - "hideInterval": false, - "hideAggregation": false, - "hideAggInterval": false, - "selectedTab": 1, - "history": { - "historyType": 0, - "timewindowMs": 31536000000, - "interval": 1000 + "selectedTab": 0, + "realtime": { + "realtimeType": 0, + "interval": 3600000, + "timewindowMs": 86400000 }, "aggregation": { "type": "NONE", - "limit": 1000 + "limit": 50000 } }, "showTitle": true, @@ -5676,6 +8076,7 @@ "lineHeight": "1" }, "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, "showTicks": true, "ticksColor": "rgba(0, 0, 0, 0.54)", "showLine": true, @@ -5688,7 +8089,7 @@ "groupWidth": { "relative": true, "relativeWidth": 6, - "absoluteWidth": 900000000 + "absoluteWidth": 1800000 }, "barWidth": { "relative": true, @@ -5714,7 +8115,8 @@ "showMax": false, "showAvg": false, "showTotal": true, - "showLatest": false + "showLatest": false, + "valueFormat": null }, "showTooltip": true, "tooltipTrigger": "axis", @@ -5731,7 +8133,9 @@ "tooltipDateFormat": { "format": "yyyy-MM-dd HH:mm:ss", "lastUpdateAgo": false, - "custom": false + "custom": false, + "auto": true, + "autoDateFormatSettings": {} }, "tooltipDateFont": { "family": "Roboto", @@ -5763,9 +8167,78 @@ "color": "rgba(255,255,255,0.72)", "blur": 3 } - } + }, + "comparisonEnabled": false, + "timeForComparison": "previousInterval", + "comparisonCustomIntervalValue": 7200000, + "comparisonXAxis": { + "show": true, + "label": "", + "labelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "600", + "lineHeight": "1" + }, + "labelColor": "rgba(0, 0, 0, 0.54)", + "position": "top", + "showTickLabels": true, + "tickLabelFont": { + "family": "Roboto", + "size": 10, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, + "showTicks": true, + "ticksColor": "rgba(0, 0, 0, 0.54)", + "showLine": true, + "lineColor": "rgba(0, 0, 0, 0.54)", + "showSplitLines": true, + "splitLinesColor": "rgba(0, 0, 0, 0.12)" + }, + "grid": { + "show": false, + "backgroundColor": null, + "borderWidth": 1, + "borderColor": "#ccc" + }, + "legendColumnTitleFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "legendColumnTitleColor": "rgba(0, 0, 0, 0.38)", + "legendValueFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "500", + "lineHeight": "16px" + }, + "legendValueColor": "rgba(0, 0, 0, 0.87)", + "tooltipLabelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "tooltipLabelColor": "rgba(0, 0, 0, 0.76)", + "tooltipHideZeroValues": null, + "padding": "12px" }, - "title": "{i18n:api-usage.telemetry-persistence-monthly-activity}", + "title": "{i18n:api-usage.alarms-created-hourly-activity}", "dropShadow": true, "enableFullscreen": true, "titleStyle": null, @@ -5809,14 +8282,14 @@ "displayTypePrefix": true }, "margin": "0px", - "borderRadius": "0px", + "borderRadius": "4px", "iconSize": "0px" }, "row": 0, "col": 0, - "id": "226ef8c9-8488-3664-21ac-0b6217336202" + "id": "8e07dbe5-aa7a-19c1-c470-5f055df948a7" }, - "bef6c27b-9fe7-ee92-40d9-9696c501a1f9": { + "e0fe9887-d61c-7813-05a7-f60811e5c5bf": { "typeFullFqn": "system.time_series_chart", "type": "timeseries", "sizeX": 8, @@ -5833,7 +8306,7 @@ "name": "createdAlarmsCountHourly", "type": "timeseries", "label": "{i18n:api-usage.alarms-created}", - "color": "#d35a00", + "color": "#D35A00", "settings": { "excludeFromStacking": false, "hideDataByDefault": false, @@ -5843,7 +8316,7 @@ "fillLines": false, "showPoints": false, "showPointShape": "circle", - "pointShapeFormatter": "var size = radius * Math.sqrt(Math.PI) / 2;\nctx.moveTo(x - size, y - size);\nctx.lineTo(x + size, y + size);\nctx.moveTo(x - size, y + size);\nctx.lineTo(x + size, y - size);", + "pointShapeFormatter": "", "showPointsLineWidth": 5, "showPointsRadius": 3, "showSeparateAxis": false, @@ -5856,32 +8329,45 @@ "comparisonSettings": { "showValuesForComparison": true }, - "type": "bar" + "type": "bar", + "yAxisId": "default" }, "_hash": 0.0661644137210089, "units": null, "decimals": null, "funcBody": null, "usePostProcessing": null, - "postFuncBody": null + "postFuncBody": null, + "aggregationType": null } - ] + ], + "alarmFilterConfig": { + "statusList": [ + "ACTIVE" + ] + } } ], "timewindow": { - "hideInterval": false, "hideAggregation": false, "hideAggInterval": false, + "hideTimezone": false, "selectedTab": 1, "history": { "historyType": 0, "timewindowMs": 2592000000, - "interval": 86400000 + "interval": 86400000, + "fixedTimewindow": { + "startTimeMs": 1709729389667, + "endTimeMs": 1709815789667 + }, + "quickInterval": "CURRENT_DAY" }, "aggregation": { "type": "SUM", - "limit": 1000 - } + "limit": 25000 + }, + "timezone": null }, "showTitle": true, "backgroundColor": "#FFFFFF", @@ -5953,6 +8439,7 @@ "lineHeight": "1" }, "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, "showTicks": true, "ticksColor": "rgba(0, 0, 0, 0.54)", "showLine": true, @@ -5963,8 +8450,8 @@ "noAggregationBarWidthSettings": { "strategy": "group", "groupWidth": { - "relative": false, - "relativeWidth": 2, + "relative": true, + "relativeWidth": 6, "absoluteWidth": 1800000 }, "barWidth": { @@ -5991,7 +8478,8 @@ "showMax": false, "showAvg": false, "showTotal": true, - "showLatest": false + "showLatest": false, + "valueFormat": null }, "showTooltip": true, "tooltipTrigger": "axis", @@ -6008,7 +8496,9 @@ "tooltipDateFormat": { "format": "yyyy-MM-dd HH:mm:ss", "lastUpdateAgo": false, - "custom": false + "custom": false, + "auto": true, + "autoDateFormatSettings": {} }, "tooltipDateFont": { "family": "Roboto", @@ -6040,7 +8530,76 @@ "color": "rgba(255,255,255,0.72)", "blur": 3 } - } + }, + "comparisonEnabled": false, + "timeForComparison": "previousInterval", + "comparisonCustomIntervalValue": 7200000, + "comparisonXAxis": { + "show": true, + "label": "", + "labelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "600", + "lineHeight": "1" + }, + "labelColor": "rgba(0, 0, 0, 0.54)", + "position": "top", + "showTickLabels": true, + "tickLabelFont": { + "family": "Roboto", + "size": 10, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, + "showTicks": true, + "ticksColor": "rgba(0, 0, 0, 0.54)", + "showLine": true, + "lineColor": "rgba(0, 0, 0, 0.54)", + "showSplitLines": true, + "splitLinesColor": "rgba(0, 0, 0, 0.12)" + }, + "grid": { + "show": false, + "backgroundColor": null, + "borderWidth": 1, + "borderColor": "#ccc" + }, + "legendColumnTitleFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "legendColumnTitleColor": "rgba(0, 0, 0, 0.38)", + "legendValueFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "500", + "lineHeight": "16px" + }, + "legendValueColor": "rgba(0, 0, 0, 0.87)", + "tooltipLabelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "tooltipLabelColor": "rgba(0, 0, 0, 0.76)", + "tooltipHideZeroValues": null, + "padding": "12px" }, "title": "{i18n:api-usage.alarms-created-daily-activity}", "dropShadow": true, @@ -6086,14 +8645,14 @@ "displayTypePrefix": true }, "margin": "0px", - "borderRadius": "0px", + "borderRadius": "4px", "iconSize": "0px" }, "row": 0, "col": 0, - "id": "bef6c27b-9fe7-ee92-40d9-9696c501a1f9" + "id": "e0fe9887-d61c-7813-05a7-f60811e5c5bf" }, - "52305cf8-2258-5745-a0e7-41a171594bb3": { + "99a40c35-c232-16c5-c42f-3cc80ddb9243": { "typeFullFqn": "system.time_series_chart", "type": "timeseries", "sizeX": 8, @@ -6110,7 +8669,7 @@ "name": "createdAlarmsCount", "type": "timeseries", "label": "{i18n:api-usage.alarms-created}", - "color": "#d35a00", + "color": "#D35A00", "settings": { "excludeFromStacking": false, "hideDataByDefault": false, @@ -6120,7 +8679,7 @@ "fillLines": false, "showPoints": false, "showPointShape": "circle", - "pointShapeFormatter": "var size = radius * Math.sqrt(Math.PI) / 2;\nctx.moveTo(x - size, y - size);\nctx.lineTo(x + size, y + size);\nctx.moveTo(x - size, y + size);\nctx.lineTo(x + size, y - size);", + "pointShapeFormatter": "", "showPointsLineWidth": 5, "showPointsRadius": 3, "showSeparateAxis": false, @@ -6133,32 +8692,55 @@ "comparisonSettings": { "showValuesForComparison": true }, - "type": "bar" + "type": "bar", + "yAxisId": "default" }, "_hash": 0.0661644137210089, "units": null, "decimals": null, "funcBody": null, "usePostProcessing": null, - "postFuncBody": null + "postFuncBody": null, + "aggregationType": null } - ] + ], + "alarmFilterConfig": { + "statusList": [ + "ACTIVE" + ] + } } ], "timewindow": { - "hideInterval": false, "hideAggregation": false, "hideAggInterval": false, + "hideTimezone": false, "selectedTab": 1, + "realtime": { + "realtimeType": 0, + "interval": 1000, + "timewindowMs": 60000, + "quickInterval": "CURRENT_DAY", + "hideInterval": false, + "hideLastInterval": false, + "hideQuickInterval": false + }, "history": { "historyType": 0, + "interval": 2592000000, "timewindowMs": 31536000000, - "interval": 1000 + "fixedTimewindow": null, + "quickInterval": "CURRENT_DAY", + "hideInterval": false, + "hideLastInterval": false, + "hideFixedInterval": false, + "hideQuickInterval": false }, "aggregation": { "type": "NONE", - "limit": 1000 - } + "limit": 25000 + }, + "timezone": null }, "showTitle": true, "backgroundColor": "#FFFFFF", @@ -6230,6 +8812,7 @@ "lineHeight": "1" }, "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, "showTicks": true, "ticksColor": "rgba(0, 0, 0, 0.54)", "showLine": true, @@ -6242,7 +8825,7 @@ "groupWidth": { "relative": true, "relativeWidth": 6, - "absoluteWidth": 900000000 + "absoluteWidth": 1800000 }, "barWidth": { "relative": true, @@ -6268,7 +8851,8 @@ "showMax": false, "showAvg": false, "showTotal": true, - "showLatest": false + "showLatest": false, + "valueFormat": null }, "showTooltip": true, "tooltipTrigger": "axis", @@ -6285,7 +8869,9 @@ "tooltipDateFormat": { "format": "yyyy-MM-dd HH:mm:ss", "lastUpdateAgo": false, - "custom": false + "custom": false, + "auto": true, + "autoDateFormatSettings": {} }, "tooltipDateFont": { "family": "Roboto", @@ -6317,7 +8903,76 @@ "color": "rgba(255,255,255,0.72)", "blur": 3 } - } + }, + "comparisonEnabled": false, + "timeForComparison": "previousInterval", + "comparisonCustomIntervalValue": 7200000, + "comparisonXAxis": { + "show": true, + "label": "", + "labelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "600", + "lineHeight": "1" + }, + "labelColor": "rgba(0, 0, 0, 0.54)", + "position": "top", + "showTickLabels": true, + "tickLabelFont": { + "family": "Roboto", + "size": 10, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, + "showTicks": true, + "ticksColor": "rgba(0, 0, 0, 0.54)", + "showLine": true, + "lineColor": "rgba(0, 0, 0, 0.54)", + "showSplitLines": true, + "splitLinesColor": "rgba(0, 0, 0, 0.12)" + }, + "grid": { + "show": false, + "backgroundColor": null, + "borderWidth": 1, + "borderColor": "#ccc" + }, + "legendColumnTitleFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "legendColumnTitleColor": "rgba(0, 0, 0, 0.38)", + "legendValueFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "500", + "lineHeight": "16px" + }, + "legendValueColor": "rgba(0, 0, 0, 0.87)", + "tooltipLabelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "tooltipLabelColor": "rgba(0, 0, 0, 0.76)", + "tooltipHideZeroValues": null, + "padding": "12px" }, "title": "{i18n:api-usage.alarms-created-monthly-activity}", "dropShadow": true, @@ -6363,14 +9018,14 @@ "displayTypePrefix": true }, "margin": "0px", - "borderRadius": "0px", + "borderRadius": "4px", "iconSize": "0px" }, "row": 0, "col": 0, - "id": "52305cf8-2258-5745-a0e7-41a171594bb3" + "id": "99a40c35-c232-16c5-c42f-3cc80ddb9243" }, - "36fdf999-ca22-9a4c-269d-3f004d792792": { + "407f7630-406e-9c24-cb3d-b1cbdd190f15": { "typeFullFqn": "system.time_series_chart", "type": "timeseries", "sizeX": 8, @@ -6387,7 +9042,7 @@ "name": "emailCountHourly", "type": "timeseries", "label": "{i18n:api-usage.email-messages}", - "color": "#d35a00", + "color": "#F36021", "settings": { "excludeFromStacking": false, "hideDataByDefault": false, @@ -6410,31 +9065,35 @@ "comparisonSettings": { "showValuesForComparison": true }, - "type": "bar" + "type": "bar", + "yAxisId": "default" }, "_hash": 0.0661644137210089, "units": null, "decimals": null, "funcBody": null, "usePostProcessing": null, - "postFuncBody": null + "postFuncBody": null, + "aggregationType": null } - ] + ], + "alarmFilterConfig": { + "statusList": [ + "ACTIVE" + ] + } } ], "timewindow": { - "hideInterval": false, - "hideAggregation": false, - "hideAggInterval": false, - "selectedTab": 1, - "history": { - "historyType": 0, - "timewindowMs": 2592000000, - "interval": 86400000 + "selectedTab": 0, + "realtime": { + "realtimeType": 0, + "interval": 3600000, + "timewindowMs": 86400000 }, "aggregation": { - "type": "SUM", - "limit": 1000 + "type": "NONE", + "limit": 50000 } }, "showTitle": true, @@ -6507,6 +9166,7 @@ "lineHeight": "1" }, "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, "showTicks": true, "ticksColor": "rgba(0, 0, 0, 0.54)", "showLine": true, @@ -6517,8 +9177,8 @@ "noAggregationBarWidthSettings": { "strategy": "group", "groupWidth": { - "relative": false, - "relativeWidth": 2, + "relative": true, + "relativeWidth": 6, "absoluteWidth": 1800000 }, "barWidth": { @@ -6545,7 +9205,8 @@ "showMax": false, "showAvg": false, "showTotal": true, - "showLatest": false + "showLatest": false, + "valueFormat": null }, "showTooltip": true, "tooltipTrigger": "axis", @@ -6562,7 +9223,9 @@ "tooltipDateFormat": { "format": "yyyy-MM-dd HH:mm:ss", "lastUpdateAgo": false, - "custom": false + "custom": false, + "auto": true, + "autoDateFormatSettings": {} }, "tooltipDateFont": { "family": "Roboto", @@ -6594,9 +9257,78 @@ "color": "rgba(255,255,255,0.72)", "blur": 3 } - } + }, + "comparisonEnabled": false, + "timeForComparison": "previousInterval", + "comparisonCustomIntervalValue": 7200000, + "comparisonXAxis": { + "show": true, + "label": "", + "labelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "600", + "lineHeight": "1" + }, + "labelColor": "rgba(0, 0, 0, 0.54)", + "position": "top", + "showTickLabels": true, + "tickLabelFont": { + "family": "Roboto", + "size": 10, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, + "showTicks": true, + "ticksColor": "rgba(0, 0, 0, 0.54)", + "showLine": true, + "lineColor": "rgba(0, 0, 0, 0.54)", + "showSplitLines": true, + "splitLinesColor": "rgba(0, 0, 0, 0.12)" + }, + "grid": { + "show": false, + "backgroundColor": null, + "borderWidth": 1, + "borderColor": "#ccc" + }, + "legendColumnTitleFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "legendColumnTitleColor": "rgba(0, 0, 0, 0.38)", + "legendValueFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "500", + "lineHeight": "16px" + }, + "legendValueColor": "rgba(0, 0, 0, 0.87)", + "tooltipLabelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "tooltipLabelColor": "rgba(0, 0, 0, 0.76)", + "tooltipHideZeroValues": null, + "padding": "12px" }, - "title": "{i18n:api-usage.email-messages-daily-activity}", + "title": "{i18n:api-usage.emails-hourly-activity}", "dropShadow": true, "enableFullscreen": true, "titleStyle": null, @@ -6640,14 +9372,14 @@ "displayTypePrefix": true }, "margin": "0px", - "borderRadius": "0px", + "borderRadius": "4px", "iconSize": "0px" }, "row": 0, "col": 0, - "id": "36fdf999-ca22-9a4c-269d-3f004d792792" + "id": "407f7630-406e-9c24-cb3d-b1cbdd190f15" }, - "9a191755-499d-535e-86c5-061102729c02": { + "b12fb875-89fe-af4c-b344-bf4178de419f": { "typeFullFqn": "system.time_series_chart", "type": "timeseries", "sizeX": 8, @@ -6661,10 +9393,10 @@ "filterId": null, "dataKeys": [ { - "name": "smsCountHourly", + "name": "emailCountHourly", "type": "timeseries", - "label": "{i18n:api-usage.sms-messages}", - "color": "#f36021", + "label": "{i18n:api-usage.email-messages}", + "color": "#F36021", "settings": { "excludeFromStacking": false, "hideDataByDefault": false, @@ -6687,32 +9419,45 @@ "comparisonSettings": { "showValuesForComparison": true }, - "type": "bar" + "type": "bar", + "yAxisId": "default" }, "_hash": 0.0661644137210089, "units": null, "decimals": null, "funcBody": null, "usePostProcessing": null, - "postFuncBody": null + "postFuncBody": null, + "aggregationType": null } - ] + ], + "alarmFilterConfig": { + "statusList": [ + "ACTIVE" + ] + } } ], "timewindow": { - "hideInterval": false, "hideAggregation": false, "hideAggInterval": false, + "hideTimezone": false, "selectedTab": 1, "history": { "historyType": 0, "timewindowMs": 2592000000, - "interval": 86400000 + "interval": 86400000, + "fixedTimewindow": { + "startTimeMs": 1709729389667, + "endTimeMs": 1709815789667 + }, + "quickInterval": "CURRENT_DAY" }, "aggregation": { "type": "SUM", - "limit": 1000 - } + "limit": 25000 + }, + "timezone": null }, "showTitle": true, "backgroundColor": "#FFFFFF", @@ -6784,6 +9529,7 @@ "lineHeight": "1" }, "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, "showTicks": true, "ticksColor": "rgba(0, 0, 0, 0.54)", "showLine": true, @@ -6794,8 +9540,8 @@ "noAggregationBarWidthSettings": { "strategy": "group", "groupWidth": { - "relative": false, - "relativeWidth": 2, + "relative": true, + "relativeWidth": 6, "absoluteWidth": 1800000 }, "barWidth": { @@ -6822,7 +9568,8 @@ "showMax": false, "showAvg": false, "showTotal": true, - "showLatest": false + "showLatest": false, + "valueFormat": null }, "showTooltip": true, "tooltipTrigger": "axis", @@ -6839,7 +9586,9 @@ "tooltipDateFormat": { "format": "yyyy-MM-dd HH:mm:ss", "lastUpdateAgo": false, - "custom": false + "custom": false, + "auto": true, + "autoDateFormatSettings": {} }, "tooltipDateFont": { "family": "Roboto", @@ -6871,9 +9620,78 @@ "color": "rgba(255,255,255,0.72)", "blur": 3 } - } + }, + "comparisonEnabled": false, + "timeForComparison": "previousInterval", + "comparisonCustomIntervalValue": 7200000, + "comparisonXAxis": { + "show": true, + "label": "", + "labelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "600", + "lineHeight": "1" + }, + "labelColor": "rgba(0, 0, 0, 0.54)", + "position": "top", + "showTickLabels": true, + "tickLabelFont": { + "family": "Roboto", + "size": 10, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, + "showTicks": true, + "ticksColor": "rgba(0, 0, 0, 0.54)", + "showLine": true, + "lineColor": "rgba(0, 0, 0, 0.54)", + "showSplitLines": true, + "splitLinesColor": "rgba(0, 0, 0, 0.12)" + }, + "grid": { + "show": false, + "backgroundColor": null, + "borderWidth": 1, + "borderColor": "#ccc" + }, + "legendColumnTitleFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "legendColumnTitleColor": "rgba(0, 0, 0, 0.38)", + "legendValueFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "500", + "lineHeight": "16px" + }, + "legendValueColor": "rgba(0, 0, 0, 0.87)", + "tooltipLabelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "tooltipLabelColor": "rgba(0, 0, 0, 0.76)", + "tooltipHideZeroValues": null, + "padding": "12px" }, - "title": "{i18n:api-usage.sms-messages-daily-activity}", + "title": "{i18n:api-usage.emails-daily-activity}", "dropShadow": true, "enableFullscreen": true, "titleStyle": null, @@ -6917,14 +9735,14 @@ "displayTypePrefix": true }, "margin": "0px", - "borderRadius": "0px", + "borderRadius": "4px", "iconSize": "0px" }, "row": 0, "col": 0, - "id": "9a191755-499d-535e-86c5-061102729c02" + "id": "b12fb875-89fe-af4c-b344-bf4178de419f" }, - "4b266318-8357-33ef-ca5a-74cbf90e014f": { + "0b00099d-d131-3e8b-97ce-c4b8d7bcab1f": { "typeFullFqn": "system.time_series_chart", "type": "timeseries", "sizeX": 8, @@ -6941,7 +9759,7 @@ "name": "emailCount", "type": "timeseries", "label": "{i18n:api-usage.email-messages}", - "color": "#d35a00", + "color": "#F36021", "settings": { "excludeFromStacking": false, "hideDataByDefault": false, @@ -6964,32 +9782,50 @@ "comparisonSettings": { "showValuesForComparison": true }, - "type": "bar" + "type": "bar", + "yAxisId": "default" }, "_hash": 0.0661644137210089, "units": null, "decimals": null, "funcBody": null, "usePostProcessing": null, - "postFuncBody": null + "postFuncBody": null, + "aggregationType": null } ] } ], "timewindow": { - "hideInterval": false, "hideAggregation": false, "hideAggInterval": false, + "hideTimezone": false, "selectedTab": 1, + "realtime": { + "realtimeType": 0, + "interval": 1000, + "timewindowMs": 60000, + "quickInterval": "CURRENT_DAY", + "hideInterval": false, + "hideLastInterval": false, + "hideQuickInterval": false + }, "history": { "historyType": 0, + "interval": 2592000000, "timewindowMs": 31536000000, - "interval": 1000 + "fixedTimewindow": null, + "quickInterval": "CURRENT_DAY", + "hideInterval": false, + "hideLastInterval": false, + "hideFixedInterval": false, + "hideQuickInterval": false }, "aggregation": { "type": "NONE", - "limit": 1000 - } + "limit": 25000 + }, + "timezone": null }, "showTitle": true, "backgroundColor": "#FFFFFF", @@ -7061,6 +9897,7 @@ "lineHeight": "1" }, "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, "showTicks": true, "ticksColor": "rgba(0, 0, 0, 0.54)", "showLine": true, @@ -7073,7 +9910,7 @@ "groupWidth": { "relative": true, "relativeWidth": 6, - "absoluteWidth": 900000000 + "absoluteWidth": 1800000 }, "barWidth": { "relative": true, @@ -7099,7 +9936,8 @@ "showMax": false, "showAvg": false, "showTotal": true, - "showLatest": false + "showLatest": false, + "valueFormat": null }, "showTooltip": true, "tooltipTrigger": "axis", @@ -7116,7 +9954,9 @@ "tooltipDateFormat": { "format": "yyyy-MM-dd HH:mm:ss", "lastUpdateAgo": false, - "custom": false + "custom": false, + "auto": true, + "autoDateFormatSettings": {} }, "tooltipDateFont": { "family": "Roboto", @@ -7148,9 +9988,78 @@ "color": "rgba(255,255,255,0.72)", "blur": 3 } - } + }, + "comparisonEnabled": false, + "timeForComparison": "previousInterval", + "comparisonCustomIntervalValue": 7200000, + "comparisonXAxis": { + "show": true, + "label": "", + "labelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "600", + "lineHeight": "1" + }, + "labelColor": "rgba(0, 0, 0, 0.54)", + "position": "top", + "showTickLabels": true, + "tickLabelFont": { + "family": "Roboto", + "size": 10, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, + "showTicks": true, + "ticksColor": "rgba(0, 0, 0, 0.54)", + "showLine": true, + "lineColor": "rgba(0, 0, 0, 0.54)", + "showSplitLines": true, + "splitLinesColor": "rgba(0, 0, 0, 0.12)" + }, + "grid": { + "show": false, + "backgroundColor": null, + "borderWidth": 1, + "borderColor": "#ccc" + }, + "legendColumnTitleFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "legendColumnTitleColor": "rgba(0, 0, 0, 0.38)", + "legendValueFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "500", + "lineHeight": "16px" + }, + "legendValueColor": "rgba(0, 0, 0, 0.87)", + "tooltipLabelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "tooltipLabelColor": "rgba(0, 0, 0, 0.76)", + "tooltipHideZeroValues": null, + "padding": "12px" }, - "title": "{i18n:api-usage.email-messages-monthly-activity}", + "title": "{i18n:api-usage.emails-monthly-activity}", "dropShadow": true, "enableFullscreen": true, "titleStyle": null, @@ -7194,14 +10103,14 @@ "displayTypePrefix": true }, "margin": "0px", - "borderRadius": "0px", + "borderRadius": "4px", "iconSize": "0px" }, "row": 0, "col": 0, - "id": "4b266318-8357-33ef-ca5a-74cbf90e014f" + "id": "0b00099d-d131-3e8b-97ce-c4b8d7bcab1f" }, - "5aa33b0b-3bd5-7fe7-ee72-f564c2ca79d8": { + "5648a56e-5a33-3018-92bd-d8e3dbe8aeee": { "typeFullFqn": "system.time_series_chart", "type": "timeseries", "sizeX": 8, @@ -7215,10 +10124,10 @@ "filterId": null, "dataKeys": [ { - "name": "smsCount", + "name": "smsCountHourly", "type": "timeseries", "label": "{i18n:api-usage.sms-messages}", - "color": "#f36021", + "color": "#F36021", "settings": { "excludeFromStacking": false, "hideDataByDefault": false, @@ -7241,31 +10150,35 @@ "comparisonSettings": { "showValuesForComparison": true }, - "type": "bar" + "type": "bar", + "yAxisId": "default" }, "_hash": 0.0661644137210089, "units": null, "decimals": null, "funcBody": null, "usePostProcessing": null, - "postFuncBody": null + "postFuncBody": null, + "aggregationType": null } - ] + ], + "alarmFilterConfig": { + "statusList": [ + "ACTIVE" + ] + } } ], "timewindow": { - "hideInterval": false, - "hideAggregation": false, - "hideAggInterval": false, - "selectedTab": 1, - "history": { - "historyType": 0, - "timewindowMs": 31536000000, - "interval": 1000 + "selectedTab": 0, + "realtime": { + "realtimeType": 0, + "interval": 3600000, + "timewindowMs": 86400000 }, "aggregation": { "type": "NONE", - "limit": 1000 + "limit": 50000 } }, "showTitle": true, @@ -7338,6 +10251,7 @@ "lineHeight": "1" }, "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, "showTicks": true, "ticksColor": "rgba(0, 0, 0, 0.54)", "showLine": true, @@ -7350,7 +10264,7 @@ "groupWidth": { "relative": true, "relativeWidth": 6, - "absoluteWidth": 900000000 + "absoluteWidth": 1800000 }, "barWidth": { "relative": true, @@ -7376,7 +10290,8 @@ "showMax": false, "showAvg": false, "showTotal": true, - "showLatest": false + "showLatest": false, + "valueFormat": null }, "showTooltip": true, "tooltipTrigger": "axis", @@ -7393,7 +10308,9 @@ "tooltipDateFormat": { "format": "yyyy-MM-dd HH:mm:ss", "lastUpdateAgo": false, - "custom": false + "custom": false, + "auto": true, + "autoDateFormatSettings": {} }, "tooltipDateFont": { "family": "Roboto", @@ -7425,9 +10342,78 @@ "color": "rgba(255,255,255,0.72)", "blur": 3 } - } + }, + "comparisonEnabled": false, + "timeForComparison": "previousInterval", + "comparisonCustomIntervalValue": 7200000, + "comparisonXAxis": { + "show": true, + "label": "", + "labelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "600", + "lineHeight": "1" + }, + "labelColor": "rgba(0, 0, 0, 0.54)", + "position": "top", + "showTickLabels": true, + "tickLabelFont": { + "family": "Roboto", + "size": 10, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, + "showTicks": true, + "ticksColor": "rgba(0, 0, 0, 0.54)", + "showLine": true, + "lineColor": "rgba(0, 0, 0, 0.54)", + "showSplitLines": true, + "splitLinesColor": "rgba(0, 0, 0, 0.12)" + }, + "grid": { + "show": false, + "backgroundColor": null, + "borderWidth": 1, + "borderColor": "#ccc" + }, + "legendColumnTitleFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "legendColumnTitleColor": "rgba(0, 0, 0, 0.38)", + "legendValueFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "500", + "lineHeight": "16px" + }, + "legendValueColor": "rgba(0, 0, 0, 0.87)", + "tooltipLabelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "tooltipLabelColor": "rgba(0, 0, 0, 0.76)", + "tooltipHideZeroValues": null, + "padding": "12px" }, - "title": "{i18n:api-usage.sms-messages-monthly-activity}", + "title": "{i18n:api-usage.sms-messages-hourly-activity}", "dropShadow": true, "enableFullscreen": true, "titleStyle": null, @@ -7471,14 +10457,14 @@ "displayTypePrefix": true }, "margin": "0px", - "borderRadius": "0px", + "borderRadius": "4px", "iconSize": "0px" }, "row": 0, "col": 0, - "id": "5aa33b0b-3bd5-7fe7-ee72-f564c2ca79d8" + "id": "5648a56e-5a33-3018-92bd-d8e3dbe8aeee" }, - "fa938580-33db-f1b3-fafc-bc3e3784ad57": { + "ab5518c1-34d6-7e17-04b4-6520496d5fe1": { "typeFullFqn": "system.time_series_chart", "type": "timeseries", "sizeX": 8, @@ -7487,266 +10473,76 @@ "datasources": [ { "type": "entity", - "entityAliasId": "2e4c97b0-257a-a1b9-690c-141d9bf2ec6f", + "name": null, + "entityAliasId": "40193437-33ac-3172-eefd-0b08eb849062", + "filterId": null, "dataKeys": [ { - "name": "successfulMsgs", - "type": "timeseries", - "label": "{i18n:api-usage.successful}", - "color": "#4caf50", - "settings": { - "yAxisId": "default", - "showInLegend": true, - "dataHiddenByDefault": false, - "type": "line", - "lineSettings": { - "showLine": true, - "step": false, - "stepType": "start", - "smooth": false, - "lineType": "solid", - "lineWidth": 2.5, - "showPoints": false, - "showPointLabel": false, - "pointLabelPosition": "top", - "pointLabelFont": { - "family": "Roboto", - "size": 11, - "sizeUnit": "px", - "style": "normal", - "weight": "400", - "lineHeight": "1" - }, - "pointLabelColor": "rgba(0, 0, 0, 0.76)", - "pointShape": "circle", - "pointSize": 12, - "fillAreaSettings": { - "type": "none", - "opacity": 0.4, - "gradient": { - "start": 100, - "end": 0 - } - } - }, - "barSettings": { - "showBorder": false, - "borderWidth": 2, - "borderRadius": 0, - "showLabel": false, - "labelPosition": "top", - "labelFont": { - "family": "Roboto", - "size": 11, - "sizeUnit": "px", - "style": "normal", - "weight": "400", - "lineHeight": "1" - }, - "labelColor": "rgba(0, 0, 0, 0.76)", - "backgroundSettings": { - "type": "none", - "opacity": 0.4, - "gradient": { - "start": 100, - "end": 0 - } - } - } - }, - "_hash": 0.15490750967648736, - "aggregationType": null, - "units": null, - "decimals": null, - "funcBody": null, - "usePostProcessing": null, - "postFuncBody": null - }, - { - "name": "failedMsgs", - "type": "timeseries", - "label": "{i18n:api-usage.permanent-failures}", - "color": "#ef5350", - "settings": { - "yAxisId": "default", - "showInLegend": true, - "dataHiddenByDefault": false, - "type": "line", - "lineSettings": { - "showLine": true, - "step": false, - "stepType": "start", - "smooth": false, - "lineType": "solid", - "lineWidth": 2.5, - "showPoints": false, - "showPointLabel": false, - "pointLabelPosition": "top", - "pointLabelFont": { - "family": "Roboto", - "size": 11, - "sizeUnit": "px", - "style": "normal", - "weight": "400", - "lineHeight": "1" - }, - "pointLabelColor": "rgba(0, 0, 0, 0.76)", - "pointShape": "circle", - "pointSize": 12, - "fillAreaSettings": { - "type": "none", - "opacity": 0.4, - "gradient": { - "start": 100, - "end": 0 - } - } - }, - "barSettings": { - "showBorder": false, - "borderWidth": 2, - "borderRadius": 0, - "showLabel": false, - "labelPosition": "top", - "labelFont": { - "family": "Roboto", - "size": 11, - "sizeUnit": "px", - "style": "normal", - "weight": "400", - "lineHeight": "1" - }, - "labelColor": "rgba(0, 0, 0, 0.76)", - "backgroundSettings": { - "type": "none", - "opacity": 0.4, - "gradient": { - "start": 100, - "end": 0 - } - } - } - }, - "_hash": 0.4186621166514697, - "aggregationType": null, - "units": null, - "decimals": null, - "funcBody": null, - "usePostProcessing": null, - "postFuncBody": null - }, - { - "name": "tmpFailed", + "name": "smsCountHourly", "type": "timeseries", - "label": "{i18n:api-usage.processing-failures}", - "color": "#ffc107", + "label": "{i18n:api-usage.sms-messages}", + "color": "#F36021", "settings": { - "yAxisId": "default", - "showInLegend": true, - "dataHiddenByDefault": false, - "type": "line", - "lineSettings": { - "showLine": true, - "step": false, - "stepType": "start", - "smooth": false, - "lineType": "solid", - "lineWidth": 2.5, - "showPoints": false, - "showPointLabel": false, - "pointLabelPosition": "top", - "pointLabelFont": { - "family": "Roboto", - "size": 11, - "sizeUnit": "px", - "style": "normal", - "weight": "400", - "lineHeight": "1" - }, - "pointLabelColor": "rgba(0, 0, 0, 0.76)", - "pointShape": "circle", - "pointSize": 12, - "fillAreaSettings": { - "type": "none", - "opacity": 0.4, - "gradient": { - "start": 100, - "end": 0 - } + "excludeFromStacking": false, + "hideDataByDefault": false, + "disableDataHiding": false, + "removeFromLegend": false, + "showLines": false, + "fillLines": false, + "showPoints": false, + "showPointShape": "circle", + "pointShapeFormatter": "", + "showPointsLineWidth": 5, + "showPointsRadius": 3, + "showSeparateAxis": false, + "axisPosition": "left", + "thresholds": [ + { + "thresholdValueSource": "predefinedValue" } + ], + "comparisonSettings": { + "showValuesForComparison": true }, - "barSettings": { - "showBorder": false, - "borderWidth": 2, - "borderRadius": 0, - "showLabel": false, - "labelPosition": "top", - "labelFont": { - "family": "Roboto", - "size": 11, - "sizeUnit": "px", - "style": "normal", - "weight": "400", - "lineHeight": "1" - }, - "labelColor": "rgba(0, 0, 0, 0.76)", - "backgroundSettings": { - "type": "none", - "opacity": 0.4, - "gradient": { - "start": 100, - "end": 0 - } - } - } + "type": "bar", + "yAxisId": "default" }, - "_hash": 0.49891007198715376, - "aggregationType": null, + "_hash": 0.0661644137210089, "units": null, "decimals": null, "funcBody": null, "usePostProcessing": null, - "postFuncBody": null + "postFuncBody": null, + "aggregationType": null } ], "alarmFilterConfig": { "statusList": [ "ACTIVE" ] - }, - "latestDataKeys": [ - { - "name": "queueName", - "type": "entityField", - "label": "Queue name", - "color": "#ffc107", - "settings": {}, - "_hash": 0.7021721434431745 - }, - { - "name": "serviceId", - "type": "entityField", - "label": "Service Id", - "color": "#607d8b", - "settings": {}, - "_hash": 0.5924381120750077 - } - ] + } } ], "timewindow": { - "hideInterval": false, "hideAggregation": false, "hideAggInterval": false, - "selectedTab": 0, - "realtime": { - "timewindowMs": 3600000, - "interval": 1000 + "hideTimezone": false, + "selectedTab": 1, + "history": { + "historyType": 0, + "timewindowMs": 2592000000, + "interval": 86400000, + "fixedTimewindow": { + "startTimeMs": 1709729389667, + "endTimeMs": 1709815789667 + }, + "quickInterval": "CURRENT_DAY" }, "aggregation": { - "type": "NONE", - "limit": 10000 - } + "type": "SUM", + "limit": 25000 + }, + "timezone": null }, "showTitle": true, "backgroundColor": "#FFFFFF", @@ -7818,6 +10614,7 @@ "lineHeight": "1" }, "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, "showTicks": true, "ticksColor": "rgba(0, 0, 0, 0.54)", "showLine": true, @@ -7828,8 +10625,8 @@ "noAggregationBarWidthSettings": { "strategy": "group", "groupWidth": { - "relative": false, - "relativeWidth": 2, + "relative": true, + "relativeWidth": 6, "absoluteWidth": 1800000 }, "barWidth": { @@ -7852,11 +10649,12 @@ "direction": "column", "position": "bottom", "sortDataKeys": false, - "showMin": true, - "showMax": true, + "showMin": false, + "showMax": false, "showAvg": false, "showTotal": true, - "showLatest": false + "showLatest": false, + "valueFormat": null }, "showTooltip": true, "tooltipTrigger": "axis", @@ -7873,7 +10671,9 @@ "tooltipDateFormat": { "format": "yyyy-MM-dd HH:mm:ss", "lastUpdateAgo": false, - "custom": false + "custom": false, + "auto": true, + "autoDateFormatSettings": {} }, "tooltipDateFont": { "family": "Roboto", @@ -7885,13 +10685,12 @@ }, "tooltipDateColor": "rgba(0, 0, 0, 0.76)", "tooltipDateInterval": true, - "tooltipHideZeroValues": true, "tooltipBackgroundColor": "rgba(255, 255, 255, 0.76)", "tooltipBackgroundBlur": 4, "animation": { "animation": true, "animationThreshold": 2000, - "animationDuration": 300, + "animationDuration": 1000, "animationEasing": "cubicOut", "animationDelay": 0, "animationDurationUpdate": 300, @@ -7907,9 +10706,77 @@ "blur": 3 } }, + "comparisonEnabled": false, + "timeForComparison": "previousInterval", + "comparisonCustomIntervalValue": 7200000, + "comparisonXAxis": { + "show": true, + "label": "", + "labelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "600", + "lineHeight": "1" + }, + "labelColor": "rgba(0, 0, 0, 0.54)", + "position": "top", + "showTickLabels": true, + "tickLabelFont": { + "family": "Roboto", + "size": 10, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, + "showTicks": true, + "ticksColor": "rgba(0, 0, 0, 0.54)", + "showLine": true, + "lineColor": "rgba(0, 0, 0, 0.54)", + "showSplitLines": true, + "splitLinesColor": "rgba(0, 0, 0, 0.12)" + }, + "grid": { + "show": false, + "backgroundColor": null, + "borderWidth": 1, + "borderColor": "#ccc" + }, + "legendColumnTitleFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "legendColumnTitleColor": "rgba(0, 0, 0, 0.38)", + "legendValueFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "500", + "lineHeight": "16px" + }, + "legendValueColor": "rgba(0, 0, 0, 0.87)", + "tooltipLabelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "tooltipLabelColor": "rgba(0, 0, 0, 0.76)", + "tooltipHideZeroValues": null, "padding": "12px" }, - "title": "{i18n:api-usage.queue-stats}", + "title": "{i18n:api-usage.sms-messages-daily-activity}", "dropShadow": true, "enableFullscreen": true, "titleStyle": null, @@ -7932,230 +10799,118 @@ "titleTooltip": "", "widgetStyle": {}, "widgetCss": "", - "pageSize": 1024, - "units": "", - "decimals": null, - "noDataDisplayMessage": "", - "timewindowStyle": { - "showIcon": false, - "iconSize": "24px", - "icon": null, - "iconPosition": "left", - "font": { - "size": 12, - "sizeUnit": "px", - "family": "Roboto", - "weight": "400", - "style": "normal", - "lineHeight": "16px" - }, - "color": "rgba(0, 0, 0, 0.38)", - "displayTypePrefix": true - }, - "margin": "0px", - "borderRadius": "0px", - "iconSize": "0px" - }, - "row": 0, - "col": 0, - "id": "fa938580-33db-f1b3-fafc-bc3e3784ad57" - }, - "2ee89893-4e38-5331-95b7-3fd4f310c5a7": { - "typeFullFqn": "system.time_series_chart", - "type": "timeseries", - "sizeX": 8, - "sizeY": 5, - "config": { - "datasources": [ - { - "type": "entity", - "entityAliasId": "2e4c97b0-257a-a1b9-690c-141d9bf2ec6f", - "dataKeys": [ - { - "name": "timeoutMsgs", - "type": "timeseries", - "label": "{i18n:api-usage.permanent-timeouts}", - "color": "#4caf50", - "settings": { - "yAxisId": "default", - "showInLegend": true, - "dataHiddenByDefault": false, - "type": "line", - "lineSettings": { - "showLine": true, - "step": false, - "stepType": "start", - "smooth": false, - "lineType": "solid", - "lineWidth": 2.5, - "showPoints": false, - "showPointLabel": false, - "pointLabelPosition": "top", - "pointLabelFont": { - "family": "Roboto", - "size": 11, - "sizeUnit": "px", - "style": "normal", - "weight": "400", - "lineHeight": "1" - }, - "pointLabelColor": "rgba(0, 0, 0, 0.76)", - "pointShape": "circle", - "pointSize": 12, - "fillAreaSettings": { - "type": "none", - "opacity": 0.4, - "gradient": { - "start": 100, - "end": 0 - } - } - }, - "barSettings": { - "showBorder": false, - "borderWidth": 2, - "borderRadius": 0, - "showLabel": false, - "labelPosition": "top", - "labelFont": { - "family": "Roboto", - "size": 11, - "sizeUnit": "px", - "style": "normal", - "weight": "400", - "lineHeight": "1" - }, - "labelColor": "rgba(0, 0, 0, 0.76)", - "backgroundSettings": { - "type": "none", - "opacity": 0.4, - "gradient": { - "start": 100, - "end": 0 - } - } - } - }, - "_hash": 0.565222981550328, - "aggregationType": null, - "units": null, - "decimals": null, - "funcBody": null, - "usePostProcessing": null, - "postFuncBody": null - }, + "pageSize": 1024, + "units": "", + "decimals": null, + "noDataDisplayMessage": "", + "timewindowStyle": { + "showIcon": false, + "iconSize": "24px", + "icon": null, + "iconPosition": "left", + "font": { + "size": 12, + "sizeUnit": "px", + "family": "Roboto", + "weight": "400", + "style": "normal", + "lineHeight": "16px" + }, + "color": "rgba(0, 0, 0, 0.38)", + "displayTypePrefix": true + }, + "margin": "0px", + "borderRadius": "4px", + "iconSize": "0px" + }, + "row": 0, + "col": 0, + "id": "ab5518c1-34d6-7e17-04b4-6520496d5fe1" + }, + "2e7326ac-98d3-e68c-b7cf-948118a3f140": { + "typeFullFqn": "system.time_series_chart", + "type": "timeseries", + "sizeX": 8, + "sizeY": 5, + "config": { + "datasources": [ + { + "type": "entity", + "name": null, + "entityAliasId": "40193437-33ac-3172-eefd-0b08eb849062", + "filterId": null, + "dataKeys": [ { - "name": "tmpTimeout", + "name": "smsCount", "type": "timeseries", - "label": "{i18n:api-usage.processing-timeouts}", - "color": "#9c27b0", + "label": "{i18n:api-usage.sms-messages}", + "color": "#F36021", "settings": { - "yAxisId": "default", - "showInLegend": true, - "dataHiddenByDefault": false, - "type": "line", - "lineSettings": { - "showLine": true, - "step": false, - "stepType": "start", - "smooth": false, - "lineType": "solid", - "lineWidth": 2.5, - "showPoints": false, - "showPointLabel": false, - "pointLabelPosition": "top", - "pointLabelFont": { - "family": "Roboto", - "size": 11, - "sizeUnit": "px", - "style": "normal", - "weight": "400", - "lineHeight": "1" - }, - "pointLabelColor": "rgba(0, 0, 0, 0.76)", - "pointShape": "circle", - "pointSize": 12, - "fillAreaSettings": { - "type": "none", - "opacity": 0.4, - "gradient": { - "start": 100, - "end": 0 - } + "excludeFromStacking": false, + "hideDataByDefault": false, + "disableDataHiding": false, + "removeFromLegend": false, + "showLines": false, + "fillLines": false, + "showPoints": false, + "showPointShape": "circle", + "pointShapeFormatter": "", + "showPointsLineWidth": 5, + "showPointsRadius": 3, + "showSeparateAxis": false, + "axisPosition": "left", + "thresholds": [ + { + "thresholdValueSource": "predefinedValue" } + ], + "comparisonSettings": { + "showValuesForComparison": true }, - "barSettings": { - "showBorder": false, - "borderWidth": 2, - "borderRadius": 0, - "showLabel": false, - "labelPosition": "top", - "labelFont": { - "family": "Roboto", - "size": 11, - "sizeUnit": "px", - "style": "normal", - "weight": "400", - "lineHeight": "1" - }, - "labelColor": "rgba(0, 0, 0, 0.76)", - "backgroundSettings": { - "type": "none", - "opacity": 0.4, - "gradient": { - "start": 100, - "end": 0 - } - } - } + "type": "bar", + "yAxisId": "default" }, - "_hash": 0.2679547062508352, - "aggregationType": null, + "_hash": 0.0661644137210089, "units": null, "decimals": null, "funcBody": null, "usePostProcessing": null, - "postFuncBody": null - } - ], - "alarmFilterConfig": { - "statusList": [ - "ACTIVE" - ] - }, - "latestDataKeys": [ - { - "name": "queueName", - "type": "entityField", - "label": "Queue name", - "color": "#f44336", - "settings": {}, - "_hash": 0.7066844328378095 - }, - { - "name": "serviceId", - "type": "entityField", - "label": "Service Id", - "color": "#ffc107", - "settings": {}, - "_hash": 0.1371570237026627 + "postFuncBody": null, + "aggregationType": null } ] } ], "timewindow": { - "hideInterval": false, "hideAggregation": false, "hideAggInterval": false, - "selectedTab": 0, + "hideTimezone": false, + "selectedTab": 1, "realtime": { - "timewindowMs": 3600000, - "interval": 1000 + "realtimeType": 0, + "interval": 1000, + "timewindowMs": 60000, + "quickInterval": "CURRENT_DAY", + "hideInterval": false, + "hideLastInterval": false, + "hideQuickInterval": false + }, + "history": { + "historyType": 0, + "interval": 2592000000, + "timewindowMs": 31536000000, + "fixedTimewindow": null, + "quickInterval": "CURRENT_DAY", + "hideInterval": false, + "hideLastInterval": false, + "hideFixedInterval": false, + "hideQuickInterval": false }, "aggregation": { "type": "NONE", - "limit": 10000 - } + "limit": 25000 + }, + "timezone": null }, "showTitle": true, "backgroundColor": "#FFFFFF", @@ -8227,6 +10982,7 @@ "lineHeight": "1" }, "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, "showTicks": true, "ticksColor": "rgba(0, 0, 0, 0.54)", "showLine": true, @@ -8237,8 +10993,8 @@ "noAggregationBarWidthSettings": { "strategy": "group", "groupWidth": { - "relative": false, - "relativeWidth": 2, + "relative": true, + "relativeWidth": 6, "absoluteWidth": 1800000 }, "barWidth": { @@ -8261,15 +11017,113 @@ "direction": "column", "position": "bottom", "sortDataKeys": false, - "showMin": true, - "showMax": true, + "showMin": false, + "showMax": false, "showAvg": false, "showTotal": true, - "showLatest": false + "showLatest": false, + "valueFormat": null + }, + "showTooltip": true, + "tooltipTrigger": "axis", + "tooltipValueFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "500", + "lineHeight": "16px" + }, + "tooltipValueColor": "rgba(0, 0, 0, 0.76)", + "tooltipShowDate": true, + "tooltipDateFormat": { + "format": "yyyy-MM-dd HH:mm:ss", + "lastUpdateAgo": false, + "custom": false, + "auto": true, + "autoDateFormatSettings": {} + }, + "tooltipDateFont": { + "family": "Roboto", + "size": 11, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "tooltipDateColor": "rgba(0, 0, 0, 0.76)", + "tooltipDateInterval": true, + "tooltipBackgroundColor": "rgba(255, 255, 255, 0.76)", + "tooltipBackgroundBlur": 4, + "animation": { + "animation": true, + "animationThreshold": 2000, + "animationDuration": 1000, + "animationEasing": "cubicOut", + "animationDelay": 0, + "animationDurationUpdate": 300, + "animationEasingUpdate": "cubicOut", + "animationDelayUpdate": 0 + }, + "background": { + "type": "color", + "color": "#fff", + "overlay": { + "enabled": false, + "color": "rgba(255,255,255,0.72)", + "blur": 3 + } + }, + "comparisonEnabled": false, + "timeForComparison": "previousInterval", + "comparisonCustomIntervalValue": 7200000, + "comparisonXAxis": { + "show": true, + "label": "", + "labelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "600", + "lineHeight": "1" + }, + "labelColor": "rgba(0, 0, 0, 0.54)", + "position": "top", + "showTickLabels": true, + "tickLabelFont": { + "family": "Roboto", + "size": 10, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, + "showTicks": true, + "ticksColor": "rgba(0, 0, 0, 0.54)", + "showLine": true, + "lineColor": "rgba(0, 0, 0, 0.54)", + "showSplitLines": true, + "splitLinesColor": "rgba(0, 0, 0, 0.12)" + }, + "grid": { + "show": false, + "backgroundColor": null, + "borderWidth": 1, + "borderColor": "#ccc" + }, + "legendColumnTitleFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" }, - "showTooltip": true, - "tooltipTrigger": "axis", - "tooltipValueFont": { + "legendColumnTitleColor": "rgba(0, 0, 0, 0.38)", + "legendValueFont": { "family": "Roboto", "size": 12, "sizeUnit": "px", @@ -8277,48 +11131,20 @@ "weight": "500", "lineHeight": "16px" }, - "tooltipValueColor": "rgba(0, 0, 0, 0.76)", - "tooltipShowDate": true, - "tooltipDateFormat": { - "format": "yyyy-MM-dd HH:mm:ss", - "lastUpdateAgo": false, - "custom": false - }, - "tooltipDateFont": { + "legendValueColor": "rgba(0, 0, 0, 0.87)", + "tooltipLabelFont": { "family": "Roboto", - "size": 11, + "size": 12, "sizeUnit": "px", "style": "normal", "weight": "400", "lineHeight": "16px" }, - "tooltipDateColor": "rgba(0, 0, 0, 0.76)", - "tooltipDateInterval": true, - "tooltipHideZeroValues": true, - "tooltipBackgroundColor": "rgba(255, 255, 255, 0.76)", - "tooltipBackgroundBlur": 4, - "animation": { - "animation": true, - "animationThreshold": 2000, - "animationDuration": 300, - "animationEasing": "cubicOut", - "animationDelay": 0, - "animationDurationUpdate": 300, - "animationEasingUpdate": "cubicOut", - "animationDelayUpdate": 0 - }, - "background": { - "type": "color", - "color": "#fff", - "overlay": { - "enabled": false, - "color": "rgba(255,255,255,0.72)", - "blur": 3 - } - }, + "tooltipLabelColor": "rgba(0, 0, 0, 0.76)", + "tooltipHideZeroValues": null, "padding": "12px" }, - "title": "{i18n:api-usage.processing-failures-and-timeouts}", + "title": "{i18n:api-usage.sms-messages-monthly-activity}", "dropShadow": true, "enableFullscreen": true, "titleStyle": null, @@ -8362,12 +11188,317 @@ "displayTypePrefix": true }, "margin": "0px", - "borderRadius": "0px", + "borderRadius": "4px", "iconSize": "0px" }, "row": 0, "col": 0, - "id": "2ee89893-4e38-5331-95b7-3fd4f310c5a7" + "id": "2e7326ac-98d3-e68c-b7cf-948118a3f140" + }, + "07e3a570-c961-b72d-3371-5b29f3617b73": { + "typeFullFqn": "system.api_usage", + "type": "latest", + "sizeX": 7.5, + "sizeY": 3, + "config": { + "datasources": [ + { + "type": "entity", + "name": "", + "dataKeys": [] + } + ], + "showTitle": true, + "backgroundColor": "#fff", + "color": "rgba(0, 0, 0, 0.87)", + "padding": "0", + "settings": { + "dsEntityAliasId": "40193437-33ac-3172-eefd-0b08eb849062", + "apiUsageDataKeys": [ + { + "label": "{i18n:api-usage.transport-messages}", + "state": "transport_messages", + "status": { + "name": "transportApiState", + "label": "transportApiState", + "type": "timeseries", + "settings": {}, + "color": "#2196f3" + }, + "maxLimit": { + "name": "transportMsgLimit", + "label": "transportMsgLimit", + "type": "timeseries", + "settings": {}, + "color": "#2196f3" + }, + "current": { + "name": "transportMsgCount", + "label": "transportMsgCount", + "type": "timeseries", + "settings": {}, + "color": "#2196f3" + } + }, + { + "label": "{i18n:api-usage.transport-data-points}", + "state": "transport_data_points", + "status": { + "name": "transportApiState", + "label": "transportApiState", + "type": "timeseries", + "settings": {}, + "color": "#2196f3" + }, + "maxLimit": { + "name": "transportDataPointsLimit", + "label": "transportDataPointsLimit", + "type": "timeseries", + "settings": {}, + "color": "#2196f3" + }, + "current": { + "name": "transportDataPointsCount", + "label": "transportDataPointsCount", + "type": "timeseries", + "settings": {}, + "color": "#2196f3" + } + }, + { + "label": "{i18n:api-usage.rule-engine-executions}", + "state": "rule_engine_executions", + "status": { + "name": "ruleEngineApiState", + "label": "ruleEngineApiState", + "type": "timeseries", + "settings": {}, + "color": "#2196f3" + }, + "maxLimit": { + "name": "ruleEngineExecutionLimit", + "label": "ruleEngineExecutionLimit", + "type": "timeseries", + "settings": {}, + "color": "#2196f3" + }, + "current": { + "name": "ruleEngineExecutionCount", + "label": "ruleEngineExecutionCount", + "type": "timeseries", + "settings": {}, + "color": "#2196f3" + } + }, + { + "label": "{i18n:api-usage.javascript-function-executions}", + "state": "javascript_function_executions", + "status": { + "name": "jsExecutionApiState", + "label": "jsExecutionApiState", + "type": "timeseries", + "settings": {}, + "color": "#2196f3" + }, + "maxLimit": { + "name": "jsExecutionLimit", + "label": "jsExecutionLimit", + "type": "timeseries", + "settings": {}, + "color": "#2196f3" + }, + "current": { + "name": "jsExecutionCount", + "label": "jsExecutionCount", + "type": "timeseries", + "settings": {}, + "color": "#2196f3" + } + }, + { + "label": "{i18n:api-usage.tbel-function-executions}", + "state": "tbel_function_executions", + "status": { + "name": "tbelExecutionApiState", + "label": "tbelExecutionApiState", + "type": "timeseries", + "settings": {}, + "color": "#2196f3" + }, + "maxLimit": { + "name": "tbelExecutionLimit", + "label": "tbelExecutionLimit", + "type": "timeseries", + "settings": {}, + "color": "#2196f3" + }, + "current": { + "name": "tbelExecutionCount", + "label": "tbelExecutionCount", + "type": "timeseries", + "settings": {}, + "color": "#2196f3" + } + }, + { + "label": "{i18n:api-usage.data-points-storage-days}", + "state": "data_points_storage_days", + "status": { + "name": "dbApiState", + "label": "dbApiState", + "type": "timeseries", + "settings": {}, + "color": "#2196f3" + }, + "maxLimit": { + "name": "storageDataPointsLimit", + "label": "storageDataPointsLimit", + "type": "timeseries", + "settings": {}, + "color": "#2196f3" + }, + "current": { + "name": "storageDataPointsCount", + "label": "storageDataPointsCount", + "type": "timeseries", + "settings": {}, + "color": "#2196f3" + } + }, + { + "label": "{i18n:api-usage.alarms-created}", + "state": "alarms_created", + "status": { + "name": "alarmApiState", + "label": "alarmApiState", + "type": "timeseries", + "settings": {}, + "color": "#2196f3" + }, + "maxLimit": { + "name": "createdAlarmsLimit", + "label": "createdAlarmsLimit", + "type": "timeseries", + "settings": {}, + "color": "#2196f3" + }, + "current": { + "name": "createdAlarmsCount", + "label": "createdAlarmsCount", + "type": "timeseries", + "settings": {}, + "color": "#2196f3" + } + }, + { + "label": "{i18n:api-usage.emails}", + "state": "emails", + "status": { + "name": "emailApiState", + "label": "emailApiState", + "type": "timeseries", + "settings": {}, + "color": "#2196f3" + }, + "maxLimit": { + "name": "emailLimit", + "label": "emailLimit", + "type": "timeseries", + "settings": {}, + "color": "#2196f3" + }, + "current": { + "name": "emailCount", + "label": "emailCount", + "type": "timeseries", + "settings": {}, + "color": "#2196f3" + } + }, + { + "label": "{i18n:api-usage.sms}", + "state": "sms", + "status": { + "name": "notificationApiState", + "label": "notificationApiState", + "type": "timeseries", + "settings": {}, + "color": "#2196f3" + }, + "maxLimit": { + "name": "smsLimit", + "label": "smsLimit", + "type": "timeseries", + "settings": {}, + "color": "#2196f3" + }, + "current": { + "name": "smsCount", + "label": "smsCount", + "type": "timeseries", + "settings": {}, + "color": "#2196f3" + } + } + ], + "targetDashboardState": "default", + "background": { + "type": "color", + "color": "#fff", + "overlay": { + "enabled": false, + "color": "rgba(255,255,255,0.72)", + "blur": 3 + } + }, + "padding": "0" + }, + "title": "{i18n:api-usage.api-usage}", + "decimals": null, + "showTitleIcon": false, + "titleTooltip": "", + "dropShadow": true, + "enableFullscreen": false, + "widgetStyle": {}, + "widgetCss": ".tb-widget-header {\n height: 48px;\n align-items: center !important;\n padding: 5px 10px 0 10px;\n}", + "titleStyle": {}, + "pageSize": 1024, + "noDataDisplayMessage": "", + "actions": { + "headerButton": [ + { + "name": "{i18n:widgets.api-usage.go-to-main-state}", + "buttonType": "stroked", + "showIcon": false, + "icon": "undo", + "buttonColor": "#305680", + "buttonBorderColor": "#0000001F", + "customButtonStyle": { + "padding": "0 16px" + }, + "useShowWidgetActionFunction": true, + "showWidgetActionFunction": "return widgetContext.stateController.getStateId() !== widgetContext.settings.targetDashboardState && widgetContext.settings.targetDashboardState;", + "type": "custom", + "customFunction": "const state = widgetContext.settings.targetDashboardState?.length ? widgetContext.settings.targetDashboardState : 'default';\nwidgetContext.stateController.updateState(state, widgetContext.stateController.getStateParams(), false);", + "openInSeparateDialog": false, + "openInPopover": false, + "id": "1ea1cca6-47d1-3539-d051-9535129fb12b" + } + ] + }, + "titleFont": { + "size": 16, + "sizeUnit": "px", + "family": null, + "weight": "500", + "style": null, + "lineHeight": "21px" + }, + "borderRadius": "4px" + }, + "row": 0, + "col": 0, + "id": "07e3a570-c961-b72d-3371-5b29f3617b73" } }, "states": { @@ -8377,346 +11508,843 @@ "layouts": { "main": { "widgets": { - "aab68ab5-8e40-8694-c55c-8eb1c89b88fb": { - "sizeX": 4, - "sizeY": 2, + "07e3a570-c961-b72d-3371-5b29f3617b73": { + "sizeX": 24, + "sizeY": 39, "row": 0, "col": 0 - }, - "a84fa70a-ddfa-3b24-9aa4-cf9ce91f919a": { - "sizeX": 4, - "sizeY": 2, + } + }, + "gridSettings": { + "backgroundColor": "#eeeeee", + "color": "rgba(0,0,0,0.870588)", + "columns": 12, + "margin": 8, + "backgroundSizeMode": "100%", + "autoFillHeight": true, + "backgroundImageUrl": null, + "mobileAutoFillHeight": true, + "mobileRowHeight": 100, + "outerMargin": true, + "layoutType": "divider", + "minColumns": 12, + "viewFormat": "grid", + "rowHeight": 70, + "layoutDimension": { + "type": "percentage", + "leftWidthPercentage": 30 + } + } + }, + "right": { + "widgets": { + "85240e8c-7af7-90a9-ad0a-726013c479a6": { + "sizeX": 7, + "sizeY": 5, "row": 0, - "col": 4 + "col": 0, + "resizable": true, + "mobileHeight": 6 }, - "d70d26d4-e22d-4ca9-9ea7-f9c87c093321": { - "sizeX": 4, - "sizeY": 2, + "d0a10a8f-8f48-f9d6-8306-d12af9b49690": { + "sizeX": 7, + "sizeY": 5, "row": 0, - "col": 8 + "col": 7, + "resizable": true, + "mobileHeight": 6 + }, + "4544080d-9b6f-b592-9cd4-0e0335d33857": { + "sizeX": 7, + "sizeY": 5, + "row": 5, + "col": 0, + "resizable": true, + "mobileHeight": 6 + }, + "5d0f2f57-499d-1324-8e1b-cfbc0b3149d2": { + "sizeX": 7, + "sizeY": 5, + "row": 5, + "col": 7, + "resizable": true, + "mobileHeight": 6 + } + }, + "gridSettings": { + "layoutType": "divider", + "backgroundColor": "#eeeeee", + "columns": 12, + "margin": 8, + "outerMargin": true, + "backgroundSizeMode": "100%", + "minColumns": 12, + "viewFormat": "grid", + "autoFillHeight": true, + "rowHeight": 70, + "backgroundImageUrl": null, + "mobileAutoFillHeight": false, + "mobileRowHeight": 70, + "mobileDisplayLayoutFirst": false + } + } + } + }, + "rule_engine_statistics": { + "name": "{i18n:api-usage.rule-engine-statistics}", + "root": false, + "layouts": { + "main": { + "widgets": { + "a669cf86-e715-efa4-dd9a-b839abf499e9": { + "sizeX": 24, + "sizeY": 5, + "row": 7, + "col": 0, + "resizable": true, + "mobileHeight": 6 }, - "4d3ea95c-3188-9872-1817-2f989c7729e0": { - "sizeX": 4, - "sizeY": 2, + "fa938580-33db-f1b3-fafc-bc3e3784ad57": { + "sizeX": 12, + "sizeY": 7, "row": 0, - "col": 12 + "col": 0, + "resizable": true, + "mobileHeight": 6 }, - "2d0d6ff6-cd59-51d4-b916-38e22cdd0702": { - "sizeX": 4, - "sizeY": 2, + "2ee89893-4e38-5331-95b7-3fd4f310c5a7": { + "sizeX": 12, + "sizeY": 7, "row": 0, - "col": 16 - }, - "120573cc-e246-eb49-7d80-68e5d3b3c0cc": { - "sizeX": 4, - "sizeY": 2, + "col": 12, + "resizable": true, + "mobileHeight": 6 + } + }, + "gridSettings": { + "backgroundColor": "#eeeeee", + "color": "rgba(0,0,0,0.870588)", + "columns": 24, + "margin": 8, + "backgroundSizeMode": "100%", + "autoFillHeight": true, + "backgroundImageUrl": null, + "mobileAutoFillHeight": false, + "mobileRowHeight": 70, + "outerMargin": true, + "layoutType": "default", + "minColumns": 24, + "viewFormat": "grid", + "rowHeight": 70 + } + } + } + }, + "transport_messages": { + "name": "{i18n:api-usage.transport-messages}", + "root": false, + "layouts": { + "main": { + "widgets": { + "07e3a570-c961-b72d-3371-5b29f3617b73": { + "sizeX": 24, + "sizeY": 39, "row": 0, - "col": 20 - }, - "63f99d90-23ab-f8c2-3290-1e693ded5a2e": { - "sizeX": 8, - "sizeY": 4, - "row": 2, - "col": 0 - }, - "a2b7e906-2d8a-41a8-99a6-409531bfa743": { - "sizeX": 8, + "col": 0, + "resizable": true, + "mobileHeight": 4 + } + }, + "gridSettings": { + "layoutType": "divider", + "backgroundColor": "#eeeeee", + "columns": 12, + "margin": 8, + "outerMargin": true, + "backgroundSizeMode": "100%", + "minColumns": 12, + "viewFormat": "grid", + "autoFillHeight": true, + "rowHeight": 70, + "backgroundImageUrl": null, + "mobileAutoFillHeight": true, + "mobileRowHeight": 70, + "layoutDimension": { + "type": "percentage", + "leftWidthPercentage": 30 + } + } + }, + "right": { + "widgets": { + "fb155957-1af4-233e-e2fb-09e648e75d6e": { + "sizeX": 6, "sizeY": 4, - "row": 2, - "col": 8 + "row": 4, + "col": 0, + "resizable": true, + "mobileHeight": 6 }, - "ca996b66-ab7e-f977-152c-98e4ebf2a901": { - "sizeX": 8, + "4817e33b-87be-5be3-eaca-ca68a2eb4e0c": { + "sizeX": 6, "sizeY": 4, - "row": 2, - "col": 16 + "row": 4, + "col": 6, + "resizable": true, + "mobileHeight": 6 }, - "a3c2f1bb-7d3a-f11c-7b3d-28cd84fdfe34": { - "sizeX": 8, + "85240e8c-7af7-90a9-ad0a-726013c479a6": { + "sizeX": 12, "sizeY": 4, - "row": 6, + "resizable": true, + "row": 0, "col": 0 - }, - "5cebd4f1-ff6e-62f9-025c-8e7583c3d66a": { - "sizeX": 8, - "sizeY": 4, - "row": 6, - "col": 8 - }, - "bc0c8840-a9b5-5583-de7b-9e9450f5d8fe": { - "sizeX": 8, - "sizeY": 4, - "row": 6, - "col": 16 } }, "gridSettings": { + "layoutType": "divider", "backgroundColor": "#eeeeee", - "color": "rgba(0,0,0,0.870588)", - "columns": 24, - "margin": 5, + "columns": 12, + "margin": 8, + "outerMargin": true, "backgroundSizeMode": "100%", + "minColumns": 12, + "viewFormat": "grid", "autoFillHeight": true, + "rowHeight": 70, "backgroundImageUrl": null, "mobileAutoFillHeight": false, - "mobileRowHeight": 100, - "outerMargin": true + "mobileRowHeight": 70, + "mobileDisplayLayoutFirst": false } } } }, - "transport": { - "name": "{i18n:api-usage.transport}", + "transport_data_points": { + "name": "{i18n:api-usage.transport-data-points}", "root": false, "layouts": { "main": { "widgets": { - "0b091dc3-eec3-847e-d0ad-fdf12d474e7a": { + "07e3a570-c961-b72d-3371-5b29f3617b73": { "sizeX": 24, - "sizeY": 6, + "sizeY": 39, "row": 0, "col": 0 + } + }, + "gridSettings": { + "layoutType": "divider", + "backgroundColor": "#eeeeee", + "columns": 12, + "margin": 8, + "outerMargin": true, + "backgroundSizeMode": "100%", + "minColumns": 12, + "viewFormat": "grid", + "autoFillHeight": true, + "rowHeight": 70, + "backgroundImageUrl": null, + "mobileAutoFillHeight": true, + "mobileRowHeight": 70, + "layoutDimension": { + "type": "percentage", + "leftWidthPercentage": 30 + } + } + }, + "right": { + "widgets": { + "79056202-c92b-1dae-ce49-318ec52e2d3b": { + "sizeX": 6, + "sizeY": 4, + "row": 4, + "col": 0, + "resizable": true, + "mobileHeight": 6 }, - "536d7104-49f8-fde6-5827-61b8419f15ec": { - "sizeX": 24, - "sizeY": 6, - "row": 6, + "966ffee7-ba0d-8e54-f903-e8d015ca8cd2": { + "sizeX": 6, + "sizeY": 4, + "row": 4, + "col": 6, + "resizable": true, + "mobileHeight": 6 + }, + "d0a10a8f-8f48-f9d6-8306-d12af9b49690": { + "sizeX": 12, + "sizeY": 4, + "resizable": true, + "row": 0, "col": 0 } }, "gridSettings": { + "layoutType": "divider", "backgroundColor": "#eeeeee", - "color": "rgba(0,0,0,0.870588)", - "columns": 24, - "margin": 5, + "columns": 12, + "margin": 8, + "outerMargin": true, "backgroundSizeMode": "100%", + "minColumns": 12, + "viewFormat": "grid", "autoFillHeight": true, + "rowHeight": 70, "backgroundImageUrl": null, "mobileAutoFillHeight": false, "mobileRowHeight": 70, - "outerMargin": true + "mobileDisplayLayoutFirst": false } } } }, - "rule_engine_execution": { + "rule_engine_executions": { "name": "{i18n:api-usage.rule-engine-executions}", "root": false, "layouts": { "main": { "widgets": { - "c77e417c-ad9d-8e23-3ea1-c75edd653bc0": { + "07e3a570-c961-b72d-3371-5b29f3617b73": { "sizeX": 24, - "sizeY": 6, + "sizeY": 39, "row": 0, "col": 0 + } + }, + "gridSettings": { + "layoutType": "divider", + "backgroundColor": "#eeeeee", + "columns": 12, + "margin": 8, + "outerMargin": true, + "backgroundSizeMode": "100%", + "minColumns": 12, + "viewFormat": "grid", + "autoFillHeight": true, + "rowHeight": 70, + "backgroundImageUrl": null, + "mobileAutoFillHeight": true, + "mobileRowHeight": 70, + "layoutDimension": { + "type": "percentage", + "leftWidthPercentage": 30 + } + } + }, + "right": { + "widgets": { + "84fbe63a-bcb6-7bc1-8af0-46b3b1ee5adc": { + "sizeX": 6, + "sizeY": 4, + "row": 4, + "col": 0, + "resizable": true, + "mobileHeight": 6 }, - "870904d2-d2e1-a1b9-ce56-b03fd47259b5": { - "sizeX": 24, - "sizeY": 6, - "row": 6, + "43a2b982-6c02-d9bd-71ee-34e8e6cf8893": { + "sizeX": 6, + "sizeY": 4, + "row": 4, + "col": 6, + "resizable": true, + "mobileHeight": 6 + }, + "4544080d-9b6f-b592-9cd4-0e0335d33857": { + "sizeX": 12, + "sizeY": 4, + "resizable": true, + "row": 0, "col": 0 } }, "gridSettings": { + "layoutType": "divider", "backgroundColor": "#eeeeee", - "color": "rgba(0,0,0,0.870588)", - "columns": 24, - "margin": 5, + "columns": 12, + "margin": 8, + "outerMargin": true, "backgroundSizeMode": "100%", + "minColumns": 12, + "viewFormat": "grid", "autoFillHeight": true, + "rowHeight": 70, "backgroundImageUrl": null, "mobileAutoFillHeight": false, "mobileRowHeight": 70, - "outerMargin": true + "mobileDisplayLayoutFirst": false } } } }, - "telemetry_persistence": { - "name": "{i18n:api-usage.telemetry-persistence}", + "javascript_function_executions": { + "name": "{i18n:api-usage.javascript-function-executions}", "root": false, "layouts": { "main": { "widgets": { - "7f4100d2-41be-4954-d353-1d45000dbbbb": { + "07e3a570-c961-b72d-3371-5b29f3617b73": { "sizeX": 24, - "sizeY": 6, + "sizeY": 39, "row": 0, "col": 0 + } + }, + "gridSettings": { + "layoutType": "divider", + "backgroundColor": "#eeeeee", + "columns": 12, + "margin": 8, + "outerMargin": true, + "backgroundSizeMode": "100%", + "minColumns": 12, + "viewFormat": "grid", + "autoFillHeight": true, + "rowHeight": 70, + "backgroundImageUrl": null, + "mobileAutoFillHeight": true, + "mobileRowHeight": 70, + "layoutDimension": { + "type": "percentage", + "leftWidthPercentage": 30 + } + } + }, + "right": { + "widgets": { + "76fe83c9-c30f-00a5-6299-40c759ca6705": { + "sizeX": 12, + "sizeY": 4, + "row": 0, + "col": 0, + "resizable": true, + "mobileHeight": 6 }, - "226ef8c9-8488-3664-21ac-0b6217336202": { - "sizeX": 24, - "sizeY": 6, - "row": 6, - "col": 0 + "a43598d1-7bfd-f329-ee61-c343f34f069f": { + "sizeX": 6, + "sizeY": 4, + "row": 4, + "col": 0, + "resizable": true, + "mobileHeight": 6 + }, + "3ebd62a8-dcb7-c96b-8571-e61084248f5b": { + "sizeX": 6, + "sizeY": 4, + "row": 4, + "col": 6, + "resizable": true, + "mobileHeight": 6 } }, "gridSettings": { + "layoutType": "divider", "backgroundColor": "#eeeeee", - "color": "rgba(0,0,0,0.870588)", - "columns": 24, - "margin": 5, + "columns": 12, + "margin": 8, + "outerMargin": true, "backgroundSizeMode": "100%", + "minColumns": 12, + "viewFormat": "grid", "autoFillHeight": true, + "rowHeight": 70, "backgroundImageUrl": null, "mobileAutoFillHeight": false, "mobileRowHeight": 70, - "outerMargin": true + "mobileDisplayLayoutFirst": false } } } }, - "rule_engine_statistics": { - "name": "{i18n:api-usage.rule-engine-statistics}", + "tbel_function_executions": { + "name": "{i18n:api-usage.tbel-function-executions}", "root": false, "layouts": { "main": { "widgets": { - "a669cf86-e715-efa4-dd9a-b839abf499e9": { + "07e3a570-c961-b72d-3371-5b29f3617b73": { "sizeX": 24, - "sizeY": 5, - "row": 7, + "sizeY": 39, + "row": 0, "col": 0 - }, - "fa938580-33db-f1b3-fafc-bc3e3784ad57": { + } + }, + "gridSettings": { + "layoutType": "divider", + "backgroundColor": "#eeeeee", + "columns": 12, + "margin": 8, + "outerMargin": true, + "backgroundSizeMode": "100%", + "minColumns": 12, + "viewFormat": "grid", + "autoFillHeight": true, + "rowHeight": 70, + "backgroundImageUrl": null, + "mobileAutoFillHeight": true, + "mobileRowHeight": 70, + "layoutDimension": { + "type": "percentage", + "leftWidthPercentage": 30 + } + } + }, + "right": { + "widgets": { + "88e25971-e5cb-eebb-3c7c-1ce33a8a38f4": { "sizeX": 12, - "sizeY": 7, + "sizeY": 4, + "row": 0, + "col": 0, + "resizable": true, + "mobileHeight": 6 + }, + "a1b5731c-e3b3-8cfb-7c50-3abcdce891d2": { + "sizeX": 6, + "sizeY": 4, + "row": 4, + "col": 0, + "resizable": true, + "mobileHeight": 6 + }, + "efc8d4e9-dee2-b677-c378-c1a666543bf4": { + "sizeX": 6, + "sizeY": 4, + "row": 4, + "col": 6, + "resizable": true, + "mobileHeight": 6 + } + }, + "gridSettings": { + "layoutType": "divider", + "backgroundColor": "#eeeeee", + "columns": 12, + "margin": 8, + "outerMargin": true, + "backgroundSizeMode": "100%", + "minColumns": 12, + "viewFormat": "grid", + "autoFillHeight": true, + "rowHeight": 70, + "backgroundImageUrl": null, + "mobileAutoFillHeight": false, + "mobileRowHeight": 70, + "mobileDisplayLayoutFirst": false + } + } + } + }, + "data_points_storage_days": { + "name": "{i18n:api-usage.data-points-storage-days}", + "root": false, + "layouts": { + "main": { + "widgets": { + "07e3a570-c961-b72d-3371-5b29f3617b73": { + "sizeX": 24, + "sizeY": 39, "row": 0, "col": 0 + } + }, + "gridSettings": { + "layoutType": "divider", + "backgroundColor": "#eeeeee", + "columns": 12, + "margin": 8, + "outerMargin": true, + "backgroundSizeMode": "100%", + "minColumns": 12, + "viewFormat": "grid", + "autoFillHeight": true, + "rowHeight": 70, + "backgroundImageUrl": null, + "mobileAutoFillHeight": true, + "mobileRowHeight": 70, + "layoutDimension": { + "type": "percentage", + "leftWidthPercentage": 30 + } + } + }, + "right": { + "widgets": { + "1249d3e2-6b3a-4e4a-65e9-6ed22959871e": { + "sizeX": 6, + "sizeY": 4, + "row": 4, + "col": 0, + "resizable": true, + "mobileHeight": 6 }, - "2ee89893-4e38-5331-95b7-3fd4f310c5a7": { + "c2f2da29-741d-54f6-5f1d-6f6ae616ea02": { + "sizeX": 6, + "sizeY": 4, + "row": 4, + "col": 6, + "resizable": true, + "mobileHeight": 6 + }, + "5d0f2f57-499d-1324-8e1b-cfbc0b3149d2": { "sizeX": 12, - "sizeY": 7, + "sizeY": 4, + "resizable": true, "row": 0, - "col": 12 + "col": 0 } }, "gridSettings": { + "layoutType": "divider", "backgroundColor": "#eeeeee", - "color": "rgba(0,0,0,0.870588)", - "columns": 24, - "margin": 5, + "columns": 12, + "margin": 8, + "outerMargin": true, "backgroundSizeMode": "100%", + "minColumns": 12, + "viewFormat": "grid", "autoFillHeight": true, + "rowHeight": 70, "backgroundImageUrl": null, "mobileAutoFillHeight": false, "mobileRowHeight": 70, - "outerMargin": true + "mobileDisplayLayoutFirst": false } } } }, - "notifications": { - "name": "{i18n:api-usage.notifications-email-sms}", + "emails": { + "name": "{i18n:api-usage.emails}", "root": false, "layouts": { "main": { "widgets": { - "36fdf999-ca22-9a4c-269d-3f004d792792": { - "sizeX": 12, - "sizeY": 6, + "07e3a570-c961-b72d-3371-5b29f3617b73": { + "sizeX": 24, + "sizeY": 39, "row": 0, "col": 0 - }, - "9a191755-499d-535e-86c5-061102729c02": { + } + }, + "gridSettings": { + "layoutType": "divider", + "backgroundColor": "#eeeeee", + "columns": 12, + "margin": 8, + "outerMargin": true, + "backgroundSizeMode": "100%", + "minColumns": 12, + "viewFormat": "grid", + "autoFillHeight": true, + "rowHeight": 70, + "backgroundImageUrl": null, + "mobileAutoFillHeight": true, + "mobileRowHeight": 70, + "layoutDimension": { + "type": "percentage", + "leftWidthPercentage": 30 + } + } + }, + "right": { + "widgets": { + "407f7630-406e-9c24-cb3d-b1cbdd190f15": { "sizeX": 12, - "sizeY": 6, + "sizeY": 4, "row": 0, - "col": 12 + "col": 0, + "resizable": true, + "mobileHeight": 6 }, - "4b266318-8357-33ef-ca5a-74cbf90e014f": { - "sizeX": 12, - "sizeY": 6, - "row": 6, - "col": 0 + "b12fb875-89fe-af4c-b344-bf4178de419f": { + "sizeX": 6, + "sizeY": 4, + "row": 4, + "col": 0, + "resizable": true, + "mobileHeight": 6 }, - "5aa33b0b-3bd5-7fe7-ee72-f564c2ca79d8": { - "sizeX": 12, - "sizeY": 6, - "row": 6, - "col": 12 + "0b00099d-d131-3e8b-97ce-c4b8d7bcab1f": { + "sizeX": 6, + "sizeY": 4, + "row": 4, + "col": 6, + "resizable": true, + "mobileHeight": 6 } }, "gridSettings": { + "layoutType": "divider", "backgroundColor": "#eeeeee", - "color": "rgba(0,0,0,0.870588)", - "columns": 24, - "margin": 5, + "columns": 12, + "margin": 8, + "outerMargin": true, "backgroundSizeMode": "100%", + "minColumns": 12, + "viewFormat": "grid", "autoFillHeight": true, + "rowHeight": 70, "backgroundImageUrl": null, "mobileAutoFillHeight": false, "mobileRowHeight": 70, - "outerMargin": true + "mobileDisplayLayoutFirst": false } } } }, - "alarms_created": { - "name": "{i18n:api-usage.alarms-created}", + "sms": { + "name": "{i18n:api-usage.sms}", "root": false, "layouts": { "main": { "widgets": { - "bef6c27b-9fe7-ee92-40d9-9696c501a1f9": { + "07e3a570-c961-b72d-3371-5b29f3617b73": { "sizeX": 24, - "sizeY": 6, + "sizeY": 39, "row": 0, "col": 0 + } + }, + "gridSettings": { + "layoutType": "divider", + "backgroundColor": "#eeeeee", + "columns": 12, + "margin": 8, + "outerMargin": true, + "backgroundSizeMode": "100%", + "minColumns": 12, + "viewFormat": "grid", + "autoFillHeight": true, + "rowHeight": 70, + "backgroundImageUrl": null, + "mobileAutoFillHeight": true, + "mobileRowHeight": 70, + "layoutDimension": { + "type": "percentage", + "leftWidthPercentage": 30 + } + } + }, + "right": { + "widgets": { + "5648a56e-5a33-3018-92bd-d8e3dbe8aeee": { + "sizeX": 12, + "sizeY": 4, + "row": 0, + "col": 0, + "resizable": true, + "mobileHeight": 6 }, - "52305cf8-2258-5745-a0e7-41a171594bb3": { - "sizeX": 24, - "sizeY": 6, - "row": 6, - "col": 0 + "ab5518c1-34d6-7e17-04b4-6520496d5fe1": { + "sizeX": 6, + "sizeY": 4, + "row": 4, + "col": 0, + "resizable": true, + "mobileHeight": 6 + }, + "2e7326ac-98d3-e68c-b7cf-948118a3f140": { + "sizeX": 6, + "sizeY": 4, + "row": 4, + "col": 6, + "resizable": true, + "mobileHeight": 6 } }, "gridSettings": { + "layoutType": "divider", "backgroundColor": "#eeeeee", - "color": "rgba(0,0,0,0.870588)", - "columns": 24, - "margin": 5, + "columns": 12, + "margin": 8, + "outerMargin": true, "backgroundSizeMode": "100%", + "minColumns": 12, + "viewFormat": "grid", "autoFillHeight": true, + "rowHeight": 70, "backgroundImageUrl": null, "mobileAutoFillHeight": false, "mobileRowHeight": 70, - "outerMargin": true + "mobileDisplayLayoutFirst": false } } } }, - "script_functions": { - "name": "{i18n:api-usage.scripts}", + "alarms_created": { + "name": "{i18n:api-usage.alarms-created}", "root": false, "layouts": { "main": { "widgets": { - "c66e5060-57fd-11e7-6616-65b82c294ac2": { + "07e3a570-c961-b72d-3371-5b29f3617b73": { "sizeX": 24, - "sizeY": 6, + "sizeY": 39, "row": 0, "col": 0 + } + }, + "gridSettings": { + "layoutType": "divider", + "backgroundColor": "#eeeeee", + "columns": 12, + "margin": 8, + "outerMargin": true, + "backgroundSizeMode": "100%", + "minColumns": 12, + "viewFormat": "grid", + "autoFillHeight": true, + "rowHeight": 70, + "backgroundImageUrl": null, + "mobileAutoFillHeight": true, + "mobileRowHeight": 70, + "layoutDimension": { + "type": "percentage", + "leftWidthPercentage": 30 + } + } + }, + "right": { + "widgets": { + "8e07dbe5-aa7a-19c1-c470-5f055df948a7": { + "sizeX": 12, + "sizeY": 4, + "row": 0, + "col": 0, + "resizable": true, + "mobileHeight": 6 }, - "d0e8603e-5d2e-9287-e2c6-8ccbe9c66806": { - "sizeX": 24, - "sizeY": 6, - "row": 6, - "col": 0 + "e0fe9887-d61c-7813-05a7-f60811e5c5bf": { + "sizeX": 6, + "sizeY": 4, + "row": 4, + "col": 0, + "resizable": true, + "mobileHeight": 6 + }, + "99a40c35-c232-16c5-c42f-3cc80ddb9243": { + "sizeX": 6, + "sizeY": 4, + "row": 4, + "col": 6, + "resizable": true, + "mobileHeight": 6 } }, "gridSettings": { + "layoutType": "divider", "backgroundColor": "#eeeeee", - "color": "rgba(0,0,0,0.870588)", - "columns": 24, - "margin": 5, + "columns": 12, + "margin": 8, + "outerMargin": true, "backgroundSizeMode": "100%", + "minColumns": 12, + "viewFormat": "grid", "autoFillHeight": true, + "rowHeight": 70, "backgroundImageUrl": null, "mobileAutoFillHeight": false, "mobileRowHeight": 70, - "outerMargin": true + "mobileDisplayLayoutFirst": false } } } @@ -8743,9 +12371,6 @@ }, "filters": {}, "timewindow": { - "hideInterval": false, - "hideLastInterval": false, - "hideQuickInterval": false, "hideAggregation": false, "hideAggInterval": false, "hideTimezone": false, @@ -8776,7 +12401,7 @@ "dashboardLogoUrl": null, "hideToolbar": false, "showUpdateDashboardImage": false, - "dashboardCss": ".card .bars-row {\n flex: 1;\n display: flex;\n flex-direction: row;\n}\n\n.card .bar-column {\n flex: 1;\n display: flex;\n flex-direction: column;\n}\n\n\n.card {\n width: 100%;\n height: 100%;\n box-sizing: border-box;\n display: flex;\n flex-direction: column;\n}\n\n.card > img {\n height: 0;\n}\n\n.card .content {\n flex: 1; \n padding: 12px 12px 0;\n display: flex;\n box-sizing: border-box;\n}\n\n.card .content .column {\n display: flex;\n flex-direction: column; \n justify-content: space-around;\n flex: 1;\n}\n\n.card .content .title-row {\n display: flex;\n flex-direction: row;\n padding-bottom: 10px;\n}\n\n.card .title {\n flex: 1;\n font-size: 20px;\n font-weight: 400;\n color: #666666;\n}\n\n.card .state {\n text-transform: uppercase;\n font-size: 20px;\n font-weight: bold;\n}\n\n.card.enabled .state {\n color: #00B260;\n}\n\n.card.warning .state {\n color: #FFAD6F;\n}\n\n.card.disabled .state {\n color: #F73243;\n}\n\n.card .bar-container {\n flex: 1;\n display: flex;\n flex-direction: column;\n justify-content: center;\n}\n\n.card .bar {\n flex: 1;\n max-height: 30px;\n margin-top: 3.5px;\n margin-bottom: 4px;\n background-color: #F0F0F0;\n border: 1px solid #DADCDB;\n border-radius: 2px;\n box-shadow: inset 0 1px 3px rgba(0, 0, 0, .2);\n}\n\n.card.enabled .bar {\n border-color: #00B260;\n background-color: #F0FBF7;\n}\n\n.card.warning .bar {\n border-color: #FFAD6F;\n background-color: #FFFAF6;\n}\n\n.card.disabled .bar {\n border-color: #F73243;\n background-color: #FFF0F0;\n}\n\n.card .bar .bar-fill {\n background-color: #F0F0F0;\n border-radius: 2px;\n height: 100%;\n width: 0%;\n}\n\n.card.enabled .bar-fill {\n background-color: #00C46C;\n}\n\n.card.warning .bar-fill {\n background-color: #FFD099;\n}\n\n.card.disabled .bar-fill {\n background-color: #FF9494;\n}\n\n.card .bar-labels {\n height: 20px;\n font-size: 16px;\n color: #666;\n display: flex;\n flex-direction: row;\n}\n\n\n.card .mat-mdc-button-base {\n text-transform: uppercase;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n.card .mdc-button__label {\n pointer-events: none;\n}\n\n.action-row {\n display: flex;\n flex-direction: row;\n justify-content: flex-end;\n padding: 8px 0;\n}\n\n.card .unit {\n color: #666666;\n}\n\n@media screen and (min-width: 960px) and (max-width: 1279px) {\n .card .title {\n font-size: 12px;\n }\n .card .state {\n font-size: 12px;\n }\n .card .unit {\n font-size: 8px;\n }\n .card .bar-labels {\n font-size: 8px;\n }\n .card .mat-mdc-button-base {\n font-size: 8px;\n }\n .card .action-row {\n padding: 0;\n }\n}\n\n@media screen and (min-width: 1280px) and (max-width: 1599px) {\n .card .title {\n font-size: 14px;\n }\n .card .state {\n font-size: 14px;\n }\n .card .unit {\n font-size: 10px;\n }\n .card .bar-labels {\n font-size: 10px;\n }\n .card .mat-mdc-button-base {\n font-size: 10px;\n }\n .card .action-row {\n padding: 0;\n }\n}\n\n@media screen and (min-width: 1600px) and (max-width: 1919px) {\n .card .title {\n font-size: 16px;\n }\n .card .state {\n font-size: 16px;\n }\n .card .unit {\n font-size: 12px;\n }\n .card .bar-labels {\n font-size: 12px;\n }\n .card .mat-mdc-button-base {\n font-size: 12px;\n }\n .card .action-row {\n padding: 0;\n }\n} " + "dashboardCss": "" } }, "name": "Api Usage" diff --git a/ui-ngx/src/assets/help/en_US/alarm-rule/alarm_rule_schedule_format.md b/ui-ngx/src/assets/help/en_US/alarm-rule/alarm_rule_schedule_format.md new file mode 100644 index 0000000000..94ffabbc34 --- /dev/null +++ b/ui-ngx/src/assets/help/en_US/alarm-rule/alarm_rule_schedule_format.md @@ -0,0 +1,123 @@ +#### Active all time schedule format + +An attribute with a dynamic value for an active all-time schedule format must contain an empty JSON object or JSON in the following format: + +```javascript +{ + "type": "ANY_TIME" +} +``` + +#### Specific time schedule format + +An attribute with a dynamic value for a specific schedule format must have JSON in the following format: + +```javascript +{ + "type": "SPECIFIC_TIME", + "daysOfWeek": [ + 2, + 4 + ], + "endsOn": 0, + "startsOn": 0, + "timezone": "Europe/Kiev" +} +``` + +
      +
    • +timezone: this value is used to designate the timezone you are using. +
    • +
    • +daysOfWeek: this value is used to designate the days in numerical representation (Monday - 1, Tuesday 2, etc.) on which the schedule will be active. +
    • +
    • +startsOn: this value is used to designate the timestamp in milliseconds, from which the schedule will be active for the designated days. +
    • +
    • +endsOn: this value is used to designate the timestamp in milliseconds until which the schedule will be active for the specified days. +
    • +
    +When startsOn and endsOn equals 0 it's means that the schedule will be active the whole day. + +#### Custom time schedule format + +An attribute with a dynamic value for a custom schedule format must have JSON in the following format: + +```javascript +{ + "type": "CUSTOM" + "timezone": "Europe/Kiev", + "items": [ + { + "dayOfWeek": 1, + "enabled": true, + "endsOn": 0, + "startsOn": 0 + }, + { + "dayOfWeek": 2, + "enabled": true, + "endsOn": 0, + "startsOn": 0 + }, + { + "dayOfWeek": 3, + "enabled": true, + "endsOn": 0, + "startsOn": 0 + }, + { + "dayOfWeek": 4, + "enabled": true, + "endsOn": 0, + "startsOn": 0 + }, + { + "dayOfWeek": 5, + "enabled": true, + "endsOn": 0, + "startsOn": 0 + }, + { + "dayOfWeek": 6, + "enabled": true, + "endsOn": 0, + "startsOn": 0 + }, + { + "dayOfWeek": 7, + "enabled": true, + "endsOn": 0, + "startsOn": 0 + } + ] +} +``` + +
      +
    • +timezone: this value is used to designate the timezone you are using. +
    • +
    • +items: the array of values representing the days on which the schedule will be active. +
    • +
    + +One array item contains such fields: +
      +
    • +dayOfWeek: this value is used to designate the specified day in numerical representation (Monday - 1, Tuesday 2, etc.) on which the schedule will be active. +
    • +
    • +enabled: this boolean value, used to designate that the specified day in the schedule will be enabled. +
    • +
    • +startsOn: this value is used to designate the timestamp in milliseconds, from which the schedule will be active for the designated day. +
    • +
    • +endsOn: this value is used to designate the timestamp in milliseconds until which the schedule will be active for the specified day. +
    • +
    +When startsOn and endsOn equals 0 it's means that the schedule will be active the whole day. diff --git a/ui-ngx/src/assets/help/en_US/calculated-field/filter_expression_fn.md b/ui-ngx/src/assets/help/en_US/calculated-field/filter_expression_fn.md new file mode 100644 index 0000000000..ce8ca22f51 --- /dev/null +++ b/ui-ngx/src/assets/help/en_US/calculated-field/filter_expression_fn.md @@ -0,0 +1,10 @@ +## Calculated Field TBEL Filter Function + +The **filter()** function is a user-defined script that enables custom calculations using [TBEL](${siteBaseUrl}/docs${docPlatformPrefix}/user-guide/tbel/) on telemetry and attribute data. +It receives arguments configured in the calculated field setup, along with an additional `ctx` object that stores `latestTs` and provides access to all arguments. + +### Function Signature + +```javascript +function calculate(ctx, arg1, arg2, ...): boolean +``` diff --git a/ui-ngx/src/assets/help/en_US/notification/alarm.md b/ui-ngx/src/assets/help/en_US/notification/alarm.md index 00ba021e1b..e0e0dc3e8c 100644 --- a/ui-ngx/src/assets/help/en_US/notification/alarm.md +++ b/ui-ngx/src/assets/help/en_US/notification/alarm.md @@ -16,11 +16,13 @@ Available template parameters: * `alarmStatus` - the alarm status; * `alarmOriginatorEntityType` - the entity type of the alarm originator, e.g. 'Device'; * `alarmOriginatorName` - the name of the alarm originator, e.g. 'Sensor T1'; +* `alarmOriginatorLabel` - the label of the alarm originator, e.g. 'Sensor T1'; * `alarmOriginatorId` - the alarm originator entity id as uuid string; * `recipientTitle` - title of the recipient (first and last name if specified, email otherwise); * `recipientEmail` - email of the recipient; * `recipientFirstName` - first name of the recipient; * `recipientLastName` - last name of the recipient; +* `details.` - any key field from the alarm's details. Fox example, if details are `{"data": "Temperature is 25"}`, use `${details.data}` to access "Temperature is 25"; Parameter names must be wrapped using `${...}`. For example: `${action}`. You may also modify the value of the parameter with one of the suffixes: diff --git a/ui-ngx/src/assets/help/en_US/notification/alarm_assignment.md b/ui-ngx/src/assets/help/en_US/notification/alarm_assignment.md index aa80b13b35..53b2ade36d 100644 --- a/ui-ngx/src/assets/help/en_US/notification/alarm_assignment.md +++ b/ui-ngx/src/assets/help/en_US/notification/alarm_assignment.md @@ -15,6 +15,7 @@ Available template parameters: * `alarmStatus` - the alarm status; * `alarmOriginatorEntityType` - the entity type of the alarm originator, e.g. 'Device'; * `alarmOriginatorName` - the name of the alarm originator, e.g. 'Sensor T1'; +* `alarmOriginatorLabel` - the label of the alarm originator, e.g. 'Sensor T1'; * `alarmOriginatorId` - the alarm originator entity id as uuid string; * `assigneeTitle` - title of the assignee; * `assigneeEmail` - email of the assignee; diff --git a/ui-ngx/src/assets/help/en_US/notification/alarm_comment.md b/ui-ngx/src/assets/help/en_US/notification/alarm_comment.md index 150e76babc..41ada1fba2 100644 --- a/ui-ngx/src/assets/help/en_US/notification/alarm_comment.md +++ b/ui-ngx/src/assets/help/en_US/notification/alarm_comment.md @@ -15,6 +15,7 @@ Available template parameters: * `alarmStatus` - the alarm status; * `alarmOriginatorEntityType` - the entity type of the alarm originator, e.g. 'Device'; * `alarmOriginatorName` - the name of the alarm originator, e.g. 'Sensor T1'; +* `alarmOriginatorLabel` - the label of the alarm originator, e.g. 'Sensor T1'; * `alarmOriginatorId` - the alarm originator entity id as uuid string; * `comment` - text of the comment; * `action` - one of: 'added', 'updated'; diff --git a/ui-ngx/src/assets/help/en_US/notification/edge_communication_failure.md b/ui-ngx/src/assets/help/en_US/notification/edge_communication_failure.md index 712f2b45e7..0df4732ab4 100644 --- a/ui-ngx/src/assets/help/en_US/notification/edge_communication_failure.md +++ b/ui-ngx/src/assets/help/en_US/notification/edge_communication_failure.md @@ -12,6 +12,9 @@ Available template parameters: * `edgeId` - the edge id as uuid string; * `edgeName` - the name of the edge; * `failureMsg` - the string representation of the failure, occurred on the Edge; +* `recipientEmail` - email of the recipient; +* `recipientFirstName` - first name of the recipient; +* `recipientLastName` - last name of the recipient; Parameter names must be wrapped using `${...}`. For example: `${edgeName}`. You may also modify the value of the parameter with one of the suffixes: diff --git a/ui-ngx/src/assets/help/en_US/notification/edge_connection.md b/ui-ngx/src/assets/help/en_US/notification/edge_connection.md index 37f0ec7573..4c97f6ccde 100644 --- a/ui-ngx/src/assets/help/en_US/notification/edge_connection.md +++ b/ui-ngx/src/assets/help/en_US/notification/edge_connection.md @@ -12,6 +12,9 @@ Available template parameters: * `edgeId` - the edge id as uuid string; * `edgeName` - the name of the edge; * `eventType` - the string representation of the connectivity status: connected or disconnected; +* `recipientEmail` - email of the recipient; +* `recipientFirstName` - first name of the recipient; +* `recipientLastName` - last name of the recipient; Parameter names must be wrapped using `${...}`. For example: `${edgeName}`. You may also modify the value of the parameter with one of the suffixes: diff --git a/ui-ngx/src/assets/help/en_US/notification/resources_shortage.md b/ui-ngx/src/assets/help/en_US/notification/resources_shortage.md index 6ea03a514c..2c0634a5f0 100644 --- a/ui-ngx/src/assets/help/en_US/notification/resources_shortage.md +++ b/ui-ngx/src/assets/help/en_US/notification/resources_shortage.md @@ -13,6 +13,9 @@ Available template parameters: * `usage` - the current usage value of the resource; * `serviceId` - the service id (convenient in cluster setup); * `serviceType` - the service type (convenient in cluster setup); +* `recipientEmail` - email of the recipient; +* `recipientFirstName` - first name of the recipient; +* `recipientLastName` - last name of the recipient; Parameter names must be wrapped using `${...}`. For example: `${resource}`. You may also modify the value of the parameter with one of the suffixes: diff --git a/ui-ngx/src/assets/help/en_US/notification/task_processing_failure.md b/ui-ngx/src/assets/help/en_US/notification/task_processing_failure.md index 0bcf107298..da46dc11bd 100644 --- a/ui-ngx/src/assets/help/en_US/notification/task_processing_failure.md +++ b/ui-ngx/src/assets/help/en_US/notification/task_processing_failure.md @@ -16,6 +16,9 @@ Available template parameters: * `entityType` - the type of the entity to which the task is related; * `entityId` - the id of the entity to which the task is related; * `attempt` - the number of attempts processing the task +* `recipientEmail` - email of the recipient; +* `recipientFirstName` - first name of the recipient; +* `recipientLastName` - last name of the recipient; Parameter names must be wrapped using `${...}`. For example: `${entityType}`. You may also modify the value of the parameter with one of the suffixes: diff --git a/ui-ngx/src/assets/help/en_US/rulenode/ai_node_prompt_settings.md b/ui-ngx/src/assets/help/en_US/rulenode/ai_node_prompt_settings.md index e2577244c1..1c2e537efc 100644 --- a/ui-ngx/src/assets/help/en_US/rulenode/ai_node_prompt_settings.md +++ b/ui-ngx/src/assets/help/en_US/rulenode/ai_node_prompt_settings.md @@ -1,3 +1,174 @@ +#### Example Usage: AI-Powered Predictive Maintenance + +This example demonstrates how to use the AI request node to analyze telemetry from rotating equipment for a predictive maintenance use case. + +##### Scenario + +Assume you’re monitoring a centrifugal pump that streams vibration, temperature, and acoustic readings. +To catch problems early and avoid downtime, you can use AI to analyze the telemetry for signs of **Bearing Wear**, **Misalignment**, **Overheating**, or **Imbalance** and return an alarm object if found. Downstream nodes can use it to create a ThingsBoard alarm and notify the maintenance team. + +1. **Incoming message structure** + +First, we need to collect telemetry readings. This can be achieved either by configuring a Calculated Field with the “Time series rolling” arguments to gather recent samples, +or by running a periodic check using nodes like "generator" and "originator telemetry" to fetch the latest samples and assemble the payload. + +Message payload (shortened for brevity): + +```json +{ + "acousticDeviation": { + "timeWindow": { "startTs": 1755093373000, "endTs": 1755093414551 }, + "values": [ + { "ts": 1755093373000, "value": 5.0 }, + { "ts": 1755093373100, "value": 18.0 }, + { "ts": 1755093373200, "value": 17.0 }, + { "ts": 1755093414380, "value": 5.0 }, + { "ts": 1755093414551, "value": 17.0 } + ] + }, + "temperature": { + "timeWindow": { "startTs": 1755093373000, "endTs": 1755093414551 }, + "values": [ + { "ts": 1755093373000, "value": 70.0 }, + { "ts": 1755093373120, "value": 86.0 }, + { "ts": 1755093373200, "value": 84.0 }, + { "ts": 1755093414380, "value": 70.0 }, + { "ts": 1755093414551, "value": 84.0 } + ] + }, + "vibration": { + "timeWindow": { "startTs": 1755093373000, "endTs": 1755093414551 }, + "values": [ + { "ts": 1755093373000, "value": 4.2 }, + { "ts": 1755093373120, "value": 7.4 }, + { "ts": 1755093373182, "value": 8.0 }, + { "ts": 1755093414437, "value": 6.2 }, + { "ts": 1755093414551, "value": 7.2 } + ] + } +} +``` + +Message metadata: + +```json +{ + "deviceName": "Pump-103", + "deviceType": "CentrifugalPump" +} +``` + +2. **Prompt configuration** + +As a second step, we need to explain the task to AI model. Describe the context of your device and the desired response format (in this case, minimal ThingsBoard alarm JSON object) in the system prompt. We will also put the task description in the system prompt since it does not change depending on a message. In the user prompt, we will use templates to dynamically inject telemetry data produced by the device. + +**System prompt** + +``` +You are an AI predictive maintenance assistant that detects alarm conditions in telemetry data of industrial devices based on incident patterns. + +Output JSON only. If an alarm condition is detected, output: +{ + "type": "Bearing Wear | Misalignment | Overheating | Imbalance", + "severity": "CRITICAL | MAJOR | MINOR | WARNING", + "details": { "summary": "2–3 sentences in plain English of concise, plain-language summary for maintenance teams; include units when citing values." } +} +If no alarm condition is detected, output: {} + +Inputs: time-stamped vibration (mm/s), temperature (°C), acoustic spectrum deviation (%). + +Telemetry thresholds: +- Vibration (mm/s): ≤4.5 normal; 4.5–5.0 WARNING; 5.0–6.0 MINOR; 6.0–7.1 MAJOR; >7.1 CRITICAL +- Temperature (°C): ≤75 normal; 75–80 WARNING; 80–85 MAJOR; >85 CRITICAL +- Acoustic deviation (%): ≤15 normal; 15–25 WARNING; 25–40 MINOR; 40–60 MAJOR; >60 CRITICAL + +Incident patterns: +- Bearing Wear: gradual vibration rise + temperature spike. +- Misalignment: sudden vibration spike without temperature change. +- Imbalance: rising vibration + irregular acoustics; temperature near normal. +- Overheating: temperature >85 °C ≥10 min or Δtemp ≥10 °C/10 min with Δvib <1.0 mm/s; or 75–85 °C ≥30 min with normal vib/acoustic. + +Severity policy: +- Start with the max of per-signal severities; if ≥2 signals are abnormal, escalate one level (cap at CRITICAL). +- Ignore very brief blips (<2 samples just over a boundary) unless strongly pattern-matched. +- Be conservative; use input units. +``` + +**User prompt** + +``` +Analyze telemetry from a "${deviceType}" named "${deviceName}". + +Data: +$[*] +``` +3. **Response format** (optional) + +In the previous step, we described desired response format in the system prompt, but it is possible to enforce format with JSON Schema if model you are using supports it. We recommend using JSON Schema if possible. Here is the example of response schema you can use (if using JSON Schema, in the system prompt just say that model output should be in JSON format): + +```json +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "Alarm", + "type": "object", + "additionalProperties": false, + "required": ["type", "severity", "details"], + "properties": { + "type": { + "type": "string", + "description": "Incident type", + "enum": ["Bearing Wear", "Misalignment", "Imbalance", "Overheating"] + }, + "severity": { + "type": "string", + "description": "Severity level of the incident", + "enum": ["WARNING", "MINOR", "MAJOR", "CRITICAL"] + }, + "details": { + "type": "object", + "additionalProperties": false, + "required": ["summary"], + "properties": { + "summary": { + "type": "string", + "description": "2–3 sentences in plain English of concise, plain-language summary for maintenance teams; include units when citing values." + } + } + } + } +} +``` + +4. **How it works** + +When the message containing sample data from **Pump-103** is processed, the templates are substituted: + +* `${deviceName}` → `"Pump-103"` +* `${deviceType}` → `"CentrifugalPump"` +* `$[*]` → the entire message payload JSON (e.g. telemetry data we collected in step 1) + +> **Tip:** `${*}` can substitute the entire metadata JSON if needed. + +The final instruction sent to the model is the System prompt plus the substituted User prompt. AI response will be placed in outgoing message payload. + +5. **Expected AI output** + +Given the sample data from step 1, the AI will likely output something like this: + +```json +{ + "type": "Bearing Wear", + "severity": "CRITICAL", + "details": { + "summary": "Pump-103 showed a vibration spike from 4.2 to 8.0 mm/s with continued elevated levels (6.2–7.2 mm/s), along with a temperature spike to 86 °C and recurrent 84 °C. With acoustics only in the WARNING band (~17–18%), this pattern indicates bearing wear; inspect and replace the bearing promptly to prevent failure." + } +} +``` + +6. **Next steps** + +Check the AI response: if it’s a non-empty object, it’s ready-to-use alarm JSON. Route it directly to the "create alarm" node to create an alarm. If it is empty, just ignore the output as everything is normal. + #### Example Usage: AI-Powered Alarm Analysis This example demonstrates how to use the AI node to automatically analyze a new device alarm, generate a human-readable summary, and suggest troubleshooting steps. diff --git a/ui-ngx/src/assets/help/en_US/widget/action/place_map_item/create_dialog_js.md b/ui-ngx/src/assets/help/en_US/widget/action/place_map_item/create_dialog_js.md index bc8823c777..3bb410f10b 100644 --- a/ui-ngx/src/assets/help/en_US/widget/action/place_map_item/create_dialog_js.md +++ b/ui-ngx/src/assets/help/en_US/widget/action/place_map_item/create_dialog_js.md @@ -79,7 +79,7 @@ function AddEntityDialogController(instance) { const mapType = widgetContext.mapInstance.type(); attributes.push({key: mapType === 'image' ? 'xPos' : 'latitude', value: additionalParams.coordinates.x}); attributes.push({key: mapType === 'image' ? 'yPos' : 'longitude', value: additionalParams.coordinates.y}); - } else if (mapItemType === 'Rectangle' || mapItemType === 'Polygon') { + } else if (mapItemType === 'Rectangle' || mapItemType === 'Polygon' || mapItemType === 'Line') { attributes.push({key: 'perimeter', value: additionalParams.coordinates}); } else if (mapItemType === 'Circle') { attributes.push({key: 'circle', value: additionalParams.coordinates}); diff --git a/ui-ngx/src/assets/help/en_US/widget/action/place_map_item/place_map_item_action.md b/ui-ngx/src/assets/help/en_US/widget/action/place_map_item/place_map_item_action.md index 131a674099..b192042d2f 100644 --- a/ui-ngx/src/assets/help/en_US/widget/action/place_map_item/place_map_item_action.md +++ b/ui-ngx/src/assets/help/en_US/widget/action/place_map_item/place_map_item_action.md @@ -26,8 +26,9 @@ A JavaScript function triggered after a map item is placed. Optionally uses an H
  • coordinates: Coordinates - Represents geographical coordinates of the placed map item. The actual format of this parameter depends on the type of the selected map item:
    • Marker: {x: number; y: number}, where x represents latitude, and y represents longitude.
    • -
    • Polygon, Rectangle: TbPolygonRawCoordinates contains an array of points defining the shape boundaries.
    • -
    • Circle: TbCircleData contains center coordinates and radius information.
    • +
    • Polygon, Rectangle: TbPolygonRawCoordinates contains an array of points defining the shape boundaries.
    • +
    • Circle: TbCircleData contains center coordinates and radius information.
    • +
    • Polyline: TbPolylineRawCoordinates contains an array of points defining the polyline.
    Note: The coordinates will be automatically converted according to the selected map type.
  • diff --git a/ui-ngx/src/assets/help/en_US/widget/lib/map/path_point_color_fn.md b/ui-ngx/src/assets/help/en_US/widget/lib/map/path_point_color_fn.md index be95f136ad..9af015d033 100644 --- a/ui-ngx/src/assets/help/en_US/widget/lib/map/path_point_color_fn.md +++ b/ui-ngx/src/assets/help/en_US/widget/lib/map/path_point_color_fn.md @@ -5,7 +5,7 @@ *function (data, dsData): string* -A JavaScript function used to compute color of the trip path point. +A JavaScript function used to compute color of the path point. **Parameters:** @@ -15,7 +15,7 @@ A JavaScript function used to compute color of the trip path point. **Returns:** -Should return string value presenting color of the trip path point. +Should return string value presenting color of the path point. In case no data is returned, color value from **Color** settings field will be used. diff --git a/ui-ngx/src/assets/help/en_US/widget/lib/map/path_point_tooltip_fn.md b/ui-ngx/src/assets/help/en_US/widget/lib/map/path_point_tooltip_fn.md index 4ce996626a..e9e202ef85 100644 --- a/ui-ngx/src/assets/help/en_US/widget/lib/map/path_point_tooltip_fn.md +++ b/ui-ngx/src/assets/help/en_US/widget/lib/map/path_point_tooltip_fn.md @@ -5,7 +5,7 @@ *function (data, dsData): string* -A JavaScript function used to compute text or HTML code to be displayed in the trip path point tooltip. +A JavaScript function used to compute text or HTML code to be displayed in the path point tooltip. **Parameters:** diff --git a/ui-ngx/src/assets/help/en_US/widget/lib/map/polyline_label_fn.md b/ui-ngx/src/assets/help/en_US/widget/lib/map/polyline_label_fn.md new file mode 100644 index 0000000000..b2eafef8cb --- /dev/null +++ b/ui-ngx/src/assets/help/en_US/widget/lib/map/polyline_label_fn.md @@ -0,0 +1,22 @@ +#### Polyline label function + +
    +
    + +*function (data, dsData): string* + +A JavaScript function used to compute text or HTML code of the polyline label. + +**Parameters:** + +
      + {% include widget/lib/map/map_fn_args %} +
    + +**Returns:** + +Should return string value presenting text or HTML of the polyline label. + +
    + +{% include widget/lib/map/label_fn_examples %} diff --git a/ui-ngx/src/assets/help/en_US/widget/lib/map/polyline_stroke_color_fn.md b/ui-ngx/src/assets/help/en_US/widget/lib/map/polyline_stroke_color_fn.md new file mode 100644 index 0000000000..6b49763393 --- /dev/null +++ b/ui-ngx/src/assets/help/en_US/widget/lib/map/polyline_stroke_color_fn.md @@ -0,0 +1,24 @@ +#### Polyline stroke color function + +
    +
    + +*function (data, dsData): string* + +A JavaScript function used to compute stroke color of the polyline. + +**Parameters:** + +
      + {% include widget/lib/map/map_fn_args %} +
    + +**Returns:** + +Should return string value presenting stroke color of the polyline. + +In case no data is returned, color value from **Color** settings field will be used. + +
    + +{% include widget/lib/map/color_fn_examples %} diff --git a/ui-ngx/src/assets/help/en_US/widget/lib/map/polyline_tooltip_fn.md b/ui-ngx/src/assets/help/en_US/widget/lib/map/polyline_tooltip_fn.md new file mode 100644 index 0000000000..0de9b8ee7a --- /dev/null +++ b/ui-ngx/src/assets/help/en_US/widget/lib/map/polyline_tooltip_fn.md @@ -0,0 +1,22 @@ +#### Polyline tooltip function + +
    +
    + +*function (data, dsData): string* + +A JavaScript function used to compute text or HTML code to be displayed in the polyline tooltip. + +**Parameters:** + +
      + {% include widget/lib/map/map_fn_args %} +
    + +**Returns:** + +Should return string value presenting text or HTML for the polyline tooltip. + +
    + +{% include widget/lib/map/tooltip_fn_examples %} diff --git a/ui-ngx/src/assets/locale/locale.constant-da_DK.json b/ui-ngx/src/assets/locale/locale.constant-da_DK.json index 8bff960467..624053b088 100644 --- a/ui-ngx/src/assets/locale/locale.constant-da_DK.json +++ b/ui-ngx/src/assets/locale/locale.constant-da_DK.json @@ -82,7 +82,7 @@ "upload": "Upload", "delete-anyway": "Slet alligevel", "delete-selected": "Slet valgte", - "set": "Angiv" + "set": "Indstil" }, "aggregation": { "aggregation": "Aggregering", @@ -539,20 +539,26 @@ }, "resources": "Ressourcer", "notifications": "Notifikationer", - "notifications-settings": "Notifikationsindstillinger", - "slack-api-token": "Slack API-token", + "notifications-settings": "Indstillinger for notifikationer", + "slack-api-token": "Slack API-nøgle", "slack": "Slack", "slack-settings": "Slack-indstillinger", "mobile-settings": "Mobilindstillinger", - "firebase-service-account-file": "Firebase servicekonto legitimationsoplysninger (JSON-fil)", - "select-firebase-service-account-file": "Træk og slip din Firebase servicekonto-fil eller " + "firebase-service-account-file": "Firebase servicekonto-legitimationsoplysninger JSON-fil", + "select-firebase-service-account-file": "Træk og slip din Firebase servicekonto-legitimationsfil eller ", + "trendz": "Trendz", + "trendz-settings": "Trendz-indstillinger", + "trendz-url": "Trendz URL", + "trendz-url-required": "Trendz URL er påkrævet", + "trendz-api-key": "Trendz API-nøgle", + "trendz-enable": "Aktivér Trendz" }, "alarm": { "alarm": "Alarm", "alarms": "Alarmer", "all-alarms": "Alle alarmer", "select-alarm": "Vælg alarm", - "no-alarms-matching": "Ingen alarmer matcher '{{entity}}'.", + "no-alarms-matching": "Ingen alarmer, der matcher '{{entity}}', blev fundet.", "alarm-required": "Alarm er påkrævet", "alarm-filter": "Alarmfilter", "filter": "Filter", @@ -677,8 +683,8 @@ "filter-type-entity-list": "Enhedslisten", "filter-type-entity-name": "Enhedsnavn", "filter-type-entity-type": "Enhedstype", - "filter-type-state-entity": "Enhed fra dashboardtilstand", - "filter-type-state-entity-description": "Enhed hentet fra dashboardtilstandsparametre", + "filter-type-state-entity": "Enhed fra dashboard-tilstand", + "filter-type-state-entity-description": "Enhed hentet fra dashboard-tilstandsparametre", "filter-type-asset-type": "Assettype", "filter-type-asset-type-description": "Assets af typen '{{assetTypes}}'", "filter-type-asset-type-and-name-description": "Assets af typen '{{assetTypes}}' og med navn der begynder med '{{prefix}}'", @@ -709,15 +715,16 @@ "filter-type-required": "Filtertype er påkrævet.", "entity-filter-no-entity-matched": "Ingen enheder matchede det angivne filter.", "no-entity-filter-specified": "Intet enhedsfilter angivet", - "root-state-entity": "Brug dashboardtilstandens enhed som rod", - "last-level-relation": "Hent kun sidste niveau relation", + "root-state-entity": "Brug dashboard-tilstandsenhed som rod", + "last-level-relation": "Hent kun relation på sidste niveau", "root-entity": "Rodenhed", - "state-entity-parameter-name": "Navn på tilstandsparameter", + "state-entity-parameter-name": "Parameter for tilstandsenhed", "default-state-entity": "Standard tilstandsenhed", "default-entity-parameter-name": "Som standard", - "max-relation-level": "Maks. relationsniveau", + "query-options": "Forespørgselsmuligheder", + "max-relation-level": "Maksimalt relationsniveau", "unlimited-level": "Ubegrænset niveau", - "state-entity": "Dashboardtilstandsenhed", + "state-entity": "Dashboard-tilstandsenhed", "all-entities": "Alle enheder", "any-relation": "enhver" }, @@ -859,14 +866,14 @@ "alarm": "Alarm", "alarms-created": "Oprettede alarmer", "queue-stats": "Køstatistik", - "processing-failures-and-timeouts": "Behandlingsfejl og timeouts", + "processing-failures-and-timeouts": "Behandlingsfejl og timeout", "exceptions": "Undtagelser", "alarms-created-daily-activity": "Daglig aktivitet for oprettede alarmer", - "alarms-created-hourly-activity": "Timebaseret aktivitet for oprettede alarmer", + "alarms-created-hourly-activity": "Timeaktivitet for oprettede alarmer", "alarms-created-monthly-activity": "Månedlig aktivitet for oprettede alarmer", "data-points": "Datapunkter", "data-points-storage-days": "Opbevaringsdage for datapunkter", - "device-api": "Device API", + "device-api": "Enheds-API", "email": "E-mail", "email-messages": "E-mailbeskeder", "email-messages-daily-activity": "Daglig aktivitet for e-mailbeskeder", @@ -917,7 +924,12 @@ "view-statistics": "Vis statistik" }, "api-limit": { - "cassandra-queries": "Cassandra-forespørgsler", + "cassandra-write-queries-core": "REST API Cassandra-skriveforespørgsler", + "cassandra-read-queries-core": "REST API og WS-telemetri Cassandra-læseforespørgsler", + "cassandra-write-queries-rule-engine": "Rule Engine-telemetri Cassandra-skriveforespørgsler", + "cassandra-read-queries-rule-engine": "Rule Engine-telemetri Cassandra-læseforespørgsler", + "cassandra-write-queries-monolith": "Monolitisk telemetri Cassandra-skriveforespørgsler", + "cassandra-read-queries-monolith": "Monolitisk telemetri Cassandra-læseforespørgsler", "entity-version-creation": "Oprettelse af enhedsversion", "entity-version-load": "Indlæsning af enhedsversion", "notification-requests": "Notifikationsanmodninger", @@ -925,14 +937,14 @@ "rest-api-requests": "REST API-anmodninger", "rest-api-requests-per-customer": "REST API-anmodninger pr. kunde", "transport-messages": "Transportbeskeder", - "transport-messages-per-device": "Transportbeskeder pr. device", + "transport-messages-per-device": "Transportbeskeder pr. enhed", "transport-messages-per-gateway": "Transportbeskeder pr. gateway", "transport-messages-per-gateway-device": "Transportbeskeder pr. gateway-enhed", "ws-updates-per-session": "WS-opdateringer pr. session", "edge-events": "Edge-hændelser", "edge-events-per-edge": "Edge-hændelser pr. edge", - "edge-uplink-messages": "Edge-uplinkbeskeder", - "edge-uplink-messages-per-edge": "Edge-uplinkbeskeder pr. edge" + "edge-uplink-messages": "Edge-oplinkbeskeder", + "edge-uplink-messages-per-edge": "Edge-oplinkbeskeder pr. edge" }, "audit-log": { "audit": "Revision", @@ -1018,13 +1030,13 @@ "add-argument": "Tilføj argument", "test-script-function": "Test scriptfunktion", "no-arguments": "Ingen argumenter konfigureret", - "argument-settings": "Argumentindstillinger", - "argument-current": "Nuværende enhed", - "argument-current-tenant": "Nuværende lejer", + "argument-settings": "Indstillinger for argument", + "argument-current": "Aktuel enhed", + "argument-current-tenant": "Aktuel lejer", "argument-device": "Enhed", "argument-asset": "Aktiv", "argument-customer": "Kunde", - "argument-tenant": "Nuværende lejer", + "argument-tenant": "Aktuel lejer", "argument-type": "Argumenttype", "see-debug-events": "Se fejlsøgningshændelser", "attribute": "Attribut", @@ -1057,6 +1069,7 @@ "delete-multiple-title": "Er du sikker på, at du vil slette { count, plural, =1 {1 beregnet felt} other {# beregnede felter} }?", "delete-multiple-text": "Vær forsigtig, efter bekræftelse vil alle valgte beregnede felter blive fjernet og alle relaterede data ikke kunne gendannes.", "test-with-this-message": "Test med denne meddelelse", + "use-latest-timestamp": "Brug seneste tidsstempel", "hint": { "arguments-simple-with-rolling": "Beregnet felt af typen simpel må ikke indeholde nøgler med tidsserieglidningstype.", "arguments-empty": "Argumenter må ikke være tomme.", @@ -1072,9 +1085,87 @@ "max-args": "Maksimalt antal argumenter er nået.", "decimals-range": "Standard decimaler skal være et tal mellem 0 og 15.", "expression": "Standardudtryk demonstrerer, hvordan man omregner temperatur fra Fahrenheit til Celsius.", - "arguments-entity-not-found": "Målentitet for argument ikke fundet." + "arguments-entity-not-found": "Målentitet for argument ikke fundet.", + "use-latest-timestamp": "Hvis aktiveret, vil den beregnede værdi blive gemt med det nyeste tidsstempel fra argumenternes telemetri i stedet for serverens tid." } }, + "ai-models": { + "ai-models": "AI-modeller", + "ai-model": "AI-model", + "model": "Model", + "name": "Navn", + "ai-provider": "AI-udbyder", + "no-found": "Ingen AI-modeller fundet", + "list": "{ count, plural, =1 {Én model} other {Liste med # modeller} }", + "selected-fields": "{ count, plural, =1 {1 model} other {# modeller} } valgt", + "add": "Tilføj model", + "delete-model-title": "Er du sikker på, at du vil slette modellen '{{modelName}}'?", + "delete-model-text": "Vær forsigtig, efter bekræftelse vil modellen og alle relaterede data ikke kunne gendannes.", + "delete-models-title": "Er du sikker på, at du vil slette { count, plural, =1 {1 model} other {# modeller} }?", + "delete-models-text": "Vær forsigtig, efter bekræftelse vil alle valgte modeller blive fjernet og deres relaterede data vil ikke kunne gendannes.", + "ai-providers": { + "openai": "OpenAI", + "azure-openai": "Azure OpenAI", + "google-ai-gemini": "Google AI Gemini", + "google-vertex-ai-gemini": "Google Vertex AI Gemini", + "mistral-ai": "Mistral AI", + "anthropic": "Anthropic", + "amazon-bedrock": "Amazon Bedrock", + "github-models": "GitHub-modeller" + }, + "name-required": "Navn er påkrævet.", + "name-max-length": "Navn må højst være 255 tegn.", + "provider": "Udbyder", + "api-key": "API-nøgle", + "api-key-required": "API-nøgle er påkrævet.", + "project-id": "Projekt-ID", + "project-id-required": "Projekt-ID er påkrævet", + "location": "Placering", + "location-required": "Placering er påkrævet.", + "service-account-key-file": "Servicekontonøglefil", + "service-account-key-file-required": "Servicekontonøglefil er påkrævet.", + "no-file": "Ingen fil valgt.", + "drop-file": "Slip en fil eller klik for at vælge en fil til upload.", + "personal-access-token": "Personlig adgangstoken", + "personal-access-token-required": "Personlig adgangstoken er påkrævet.", + "configuration": "Konfiguration", + "model-id": "Model-ID", + "model-id-required": "Model-ID er påkrævet.", + "deployment-name": "Deploymentsnavn", + "deployment-name-required": "Deploymentsnavn er påkrævet", + "set": "Angiv", + "region": "Region", + "region-required": "Region er påkrævet.", + "access-key-id": "Adgangsnøgle-ID", + "access-key-id-required": "Adgangsnøgle-ID er påkrævet.", + "secret-access-key": "Hemmelig adgangsnøgle", + "secret-access-key-required": "Hemmelig adgangsnøgle er påkrævet.", + "temperature": "Temperature", + "temperature-hint": "Justerer graden af tilfældighed i modellens output. Højere værdier øger tilfældigheden, mens lavere værdier reducerer den.", + "temperature-min": "Skal være 0 eller højere.", + "top-p": "Top P", + "top-p-hint": "Opretter en pulje af de mest sandsynlige tokens for modellen at vælge imellem. Højere værdier skaber en større og mere varieret pulje, mens lavere værdier skaber en mindre.", + "top-p-min-max": "Skal være større end 0 og op til 1.", + "top-k": "Top K", + "top-k-hint": "Begrænser modellens valg til et fast sæt af de \"K\" mest sandsynlige tokens.", + "top-k-min": "Skal være 0 eller højere.", + "presence-penalty": "Tilstedeværelsesstraf", + "presence-penalty-hint": "Påfører en fast straf på sandsynligheden for et token, hvis det allerede er optrådt i teksten.", + "frequency-penalty": "Frekvensstraf", + "frequency-penalty-hint": "Påfører en straf på et tokens sandsynlighed, som stiger baseret på dets hyppighed i teksten.", + "max-output-tokens": "Maksimalt outputtokens", + "max-output-tokens-min": "Skal være større end 0.", + "max-output-tokens-hint": "Angiver det maksimale antal tokens, som modellen kan generere i ét svar.", + "endpoint": "Endpoint", + "endpoint-required": "Endpoint er påkrævet.", + "service-version": "Serviceversion", + "check-connectivity": "Tjek forbindelsen", + "check-connectivity-success": "Testanmodningen var vellykket", + "check-connectivity-failed": "Testanmodningen mislykkedes", + "no-model-matching": "Ingen modeller, der matcher '{{entity}}', blev fundet.", + "model-required": "Model er påkrævet.", + "no-model-text": "Ingen modeller fundet." + }, "confirm-on-exit": { "message": "Du har ikke gemte ændringer. Er du sikker på, at du vil forlade denne side?", "html-message": "Du har ikke gemte ændringer.
    Er du sikker på, at du vil forlade denne side?", @@ -1722,10 +1813,10 @@ "type-password": "Adgangskode", "type-textarea": "Tekstområde", "type-number": "Tal", - "type-switch": "Kontakt", + "type-switch": "Skifte", "type-select": "Vælg", "type-radios": "Radioknapper", - "type-datetime": "Dato/tid", + "type-datetime": "Dato/Tid", "type-image": "Billede", "type-javascript": "JavaScript", "type-json": "JSON", @@ -1737,7 +1828,7 @@ "type-font": "Skrifttype", "type-units": "Enheder", "type-icon": "Ikon", - "type-fieldset": "Feltelement", + "type-fieldset": "Feltgruppe", "type-array": "Array", "type-html-section": "HTML-sektion", "group-title": "Gruppetitel", @@ -1754,6 +1845,7 @@ "selected-options-limit": "Valgte valgmuligheder grænse", "advanced-ui-settings": "Avancerede UI-indstillinger", "disable-on-property": "Deaktiver ved egenskab", + "disable-on-property-none": "Ingen (feltet er altid aktiveret)", "display-condition-function": "Visningsbetingelsesfunktion", "sub-label": "Undertekst", "vertical-divider-after": "Lodret skillelinje efter", @@ -1787,7 +1879,8 @@ "array-item": "Array-element", "item-type": "Elementtype", "item-name": "Elementnavn", - "no-items": "Ingen elementer" + "no-items": "Ingen elementer", + "support-unit-conversion": "Understøt enhedskonvertering" }, "clear-form": "Ryd formular", "clear-form-prompt": "Er du sikker på, at du vil fjerne alle formularens egenskaber?", @@ -1910,11 +2003,12 @@ "mqtt-use-json-format-for-default-downlink-topics": "Brug JSON-format til standard downlink-topics", "mqtt-use-json-format-for-default-downlink-topics-hint": "Ved aktivering bruges JSON-payload til push af attributter og RPC via standard topics. Har ingen effekt på nye (v2) topics.", "mqtt-send-ack-on-validation-exception": "Send PUBACK ved valideringsfejl af PUBLISH-besked", - "mqtt-send-ack-on-validation-exception-hint": "Som standard afsluttes MQTT-session ved valideringsfejl. Ved aktivering sendes bekræftelse i stedet.", + "mqtt-send-ack-on-validation-exception-hint": "Som standard vil platformen lukke MQTT-sessionen ved valideringsfejl. Når aktiveret, sender platformen en bekræftelse i stedet for at lukke sessionen.", + "mqtt-protocol-version": "Protokolversion", "snmp-add-mapping": "Tilføj SNMP-kortlægning", - "snmp-mapping-not-configured": "Ingen kortlægning fra OID til tidsserier/telemetri konfigureret", - "snmp-timseries-or-attribute-name": "Tidsserie-/attributnavn til kortlægning", - "snmp-timseries-or-attribute-type": "Tidsserie-/attributtype til kortlægning", + "snmp-mapping-not-configured": "Ingen kortlægning for OID til tidsserie/telemetri er konfigureret", + "snmp-timseries-or-attribute-name": "Navn på tidsserie/attribut til kortlægning", + "snmp-timseries-or-attribute-type": "Type af tidsserie/attribut til kortlægning", "snmp-method-pdu-type-get-request": "GetRequest", "snmp-method-pdu-type-get-next-request": "GetNextRequest", "snmp-oid": "OID", @@ -2171,12 +2265,15 @@ "add-lwm2m-server-config": "Tilføj LwM2M-server", "no-config-servers": "Ingen servere konfigureret", "others-tab": "Andre indstillinger", - "client-strategy": "Klientstrategi ved tilslutning", + "ota-update": "OTA-opdatering", + "use-object-19-for-ota-update": "Brug objekt 19 til OTA-filmetadata (checksum, størrelse, version, navn)", + "use-object-19-for-ota-update-hint": "Brug Resource ObjectId = 19 til OTA-opdateringer: FirmWare → InstanceId = 65534, SoftWare → InstanceId = 65535. Dataformatet er JSON indlejret i Base64. Denne JSON indeholder metadata for OTA-filen (filinformation): \"Checksum\" (SHA256). Yderligere felter: \"Title\" (OTA-navn), \"Version\" (OTA-version), \"File Name\" (filnavn til lagring af OTA på klienten), \"File Size\" (OTA-størrelse i bytes).", + "client-strategy": "Klientstrategi ved opkobling", "client-strategy-label": "Strategi", - "client-strategy-only-observe": "Send kun Observe-anmodning til klienten efter første forbindelse", - "client-strategy-read-all": "Læs alle ressourcer og send Observe-anmodning til klienten efter registrering", + "client-strategy-only-observe": "Kun Observe Request til klienten efter den indledende forbindelse", + "client-strategy-read-all": "Læs alle ressourcer & Observe Request til klienten efter registrering", "fw-update": "Firmwareopdatering", - "fw-update-strategy": "Firmwareopdateringsstrategi", + "fw-update-strategy": "Strategi for firmwareopdatering", "fw-update-strategy-data": "Push firmware som binær fil via Object 19 og Resource 0 (Data)", "fw-update-strategy-package": "Push firmware som binær fil via Object 5 og Resource 0 (Package)", "fw-update-strategy-package-uri": "Generér automatisk unik CoAP-URL og push firmware via Object 5 og Resource 1 (Package URI)", @@ -2201,7 +2298,17 @@ "default-object-id": "Standardobjektversion (Attribut)", "default-object-id-ver": { "v1-0": "1.0", - "v1-1": "1.1" + "v1-1": "1.1", + "v1-2": "1.2" + }, + "observe-strategy": { + "observe-strategy": "Observe-strategi", + "single": "Enkelt", + "single-description": "Én Observe-anmodning pr. ressource (højere præcision, mere netværkstrafik)", + "composite-all": "Sammensat – alle", + "composite-all-description": "Alle ressourcer observeres med en enkelt sammensat Observe-anmodning (mere effektivt, mindre fleksibelt)", + "composite-by-object": "Sammensat efter objekt", + "composite-by-object-description": "Ressourcer grupperes efter objekttype og observeres via separate sammensatte Observe-anmodninger (balanceret tilgang)" } }, "snmp": { @@ -2524,6 +2631,8 @@ "type-current-user-owner": "Aktuel bruger-ejer", "type-calculated-field": "Beregnet felt", "type-calculated-fields": "Beregnet felter", + "type-ai-model": "AI-model", + "type-ai-models": "AI-modeller", "type-widgets-bundle": "Widgetpakke", "type-widgets-bundles": "Widgetpakker", "list-of-widgets-bundles": "{ count, plural, =1 {Én widgetpakke} other {Liste over # widgetpakker} }", @@ -2553,6 +2662,8 @@ "type-tb-resources": "Ressourcer", "list-of-tb-resources": "{ count, plural, =1 {Én ressource} other {Liste over # ressourcer} }", "type-ota-package": "OTA-pakke", + "type-ota-packages": "OTA-pakker", + "list-of-ota-packages": "{ count, plural, =1 {Én OTA-pakke} other {Liste med # OTA-pakker} }", "type-rpc": "RPC", "type-queue": "Kø", "type-queue-stats": "Køstatistik", @@ -2938,6 +3049,7 @@ "missing-key-filters-error": "Nøglefiltre mangler for filteret '{{filter}}'.", "filter": "Filter", "editable": "Redigerbar", + "editable-hint": "Tillad brugeren at ændre filterværdien i dashboards.", "no-filters-found": "Ingen filtre fundet.", "no-filter-text": "Intet filter angivet", "add-filter-prompt": "Tilføj venligst et filter", @@ -2976,12 +3088,14 @@ "edit-filter-user-params": "Rediger brugerparametre for filterbetingelse", "filter-user-params": "Filterbetingelsens brugerparametre", "user-parameters": "Brugerparametre", - "display-label": "Etiket til visning", - "order-priority": "Prioritet for feltorden", - "key-filter": "Nøglefilter", - "key-filters": "Nøglefiltre", - "key-name": "Nøglenavn", - "key-name-required": "Nøglenavn er påkrævet.", + "display-label": "Etiket der vises", + "custom-label": "Brugerdefineret etiket", + "custom-label-hint": "Aktivér for at angive din egen etiket for filteret. Når deaktiveret, genereres en etiket automatisk.", + "order-priority": "Visningsrækkefølge", + "key-filter": "Nøgelfilter", + "key-filters": "Nøgelfiltre", + "key-name": "Nøglens navn", + "key-name-required": "Nøglens navn er påkrævet.", "key-type": { "key-type": "Nøgletype", "attribute": "Attribut", @@ -3021,7 +3135,8 @@ "switch-to-dynamic-value": "Skift til dynamisk værdi", "switch-to-default-value": "Skift til standardværdi", "inherit-owner": "Arv fra ejer", - "source-attribute-not-set": "Hvis kildeattribut ikke er angivet" + "source-attribute-not-set": "Hvis kildeattribut ikke er angivet", + "unit": "Enhed" }, "fullscreen": { "expand": "Udvid til fuld skærm", @@ -3400,12 +3515,13 @@ "run": "Kør", "run-hint": "Handling udføres, når brugeren klikker for at starte komponenten.", "stop": "Stop", - "stop-hint": "Handling udføres, når brugeren klikker for at stoppe komponenten.", + "stop-hint": "Handling der udføres, når brugeren klikker for at stoppe komponenten.", "temperature-step": "Temperaturtrin", - "heat-pump-color": "Varme pumpe farve", + "heat-pump-color": "Varmepumpefarve", "power-button-background": "Baggrund for tænd/sluk-knap", "value-box-background": "Baggrund for værdiboks", - "value-units": "Værdi enheder", + "value-units": "Værdienheder", + "enable-units-scale": "Aktivér enheder på skala", "filtration-mode": "Filtreringstilstand", "filtration-mode-hint": "Heltalsværdi, der angiver den aktuelle filtreringstilstand.", "filtration-mode-update": "Opdater filtreringstilstand", @@ -3720,8 +3836,10 @@ "mobile-center": "Mobilcenter", "mobile-package": "Applikationspakke", "mobile-package-max-length": "Applikationspakken skal være mindre end 256 tegn", - "mobile-package-required": "Applikationspakken er påkrævet.", + "mobile-package-required": "Applikationspakke er påkrævet.", "mobile-package-pattern": "Ugyldigt format for applikationspakke", + "mobile-package-title": "Applikationstitel", + "mobile-package-title-max-length": "Applikationstitlen skal være mindre end 256 tegn", "no-application": "Ingen applikationer fundet", "no-bundles": "Ingen pakker fundet", "platform-type": "Platformstype", @@ -3802,20 +3920,16 @@ "configuration-app": "Konfigurationsapp", "configuration-step": { "prepare-environment-title": "Forbered udviklingsmiljø", - "prepare-environment-text": "Flutter ThingsBoard Mobile Application kræver Flutter SDK. Følg vejledningen for at konfigurere Flutter SDK.", - "get-source-code-title": "Hent appens kildekode", - "get-source-code-text": "Du kan hente Flutter ThingsBoard Mobile Application kildekode ved at klone den fra GitHub-repositoriet:", - "configure-api-title": "Konfigurer ThingsBoard API-endepunkt", - "configure-api-text": "Åbn projektet flutter_thingsboard_pe_app i din editor/IDE. Rediger:", - "configure-api-hint": "Angiv værdien af konstanten thingsBoardApiEndpoint, så den matcher API-endepunktet på din ThingsBoard-serverinstans. Brug ikke \"localhost\" eller \"127.0.0.1\" som værter.", + "prepare-environment-text": "Flutter ThingsBoard Mobile Application kræver Flutter SDK. Følg instruktionerne for at opsætte Flutter SDK.", + "get-source-code-title": "Hent kildekode til appen", + "get-source-code-text": "Du kan hente kildekoden til Flutter ThingsBoard Mobile Application ved at klone den fra GitHub-repositoriet:", + "configure-app-settings-title": "Konfigurér appindstillinger", + "configure-app-settings-text": "Download konfigurationsfilen og placer den i rodkataloget for det projekt, du klonede i forrige trin.", + "download-file": "Download fil", "run-app-title": "Kør appen", - "run-app-text": "Kør appen som beskrevet i din IDE.\nHvis du bruger terminalen, kan du køre appen med følgende kommando:", - "more-information": "Detaljeret information findes i vores dokumentation for Kom godt i gang.", - "getting-started": "Kom godt i gang", - "configure-package-title": "Konfigurer applikationspakke", - "configure-package-text": "Du kan manuelt ændre applikationspakken eller bruge et tredjeparts CLI-værktøj.", - "configure-package-text-install": "For at installere Rename CLI-værktøjet skal du køre følgende kommando:", - "configure-package-run-commands": "Kør disse kommandoer i projektets rodmappe:" + "run-app-text": "Kør appen som beskrevet i din IDE.\nHvis du bruger terminalen, så kør appen med følgende kommando:", + "more-information": "Detaljeret information findes i vores Getting Started-dokumentation.", + "getting-started": "Kom godt i gang" } }, "notification": { @@ -3839,6 +3953,7 @@ "new-platform-version-trigger-settings": "Indstillinger for udløser af ny platformsversion", "rate-limits-trigger-settings": "Indstillinger for udløser ved overskredne hastighedsgrænser", "task-processing-failure-trigger-settings": "Indstillinger for udløser ved fejl i opgavebehandling", + "resources-shortage-trigger-settings": "Indstillinger for trigger ved mangel på ressourcer", "at-least-one-should-be-selected": "Mindst én skal vælges", "basic-settings": "Grundindstillinger", "button-text": "Knaptekst", @@ -3853,6 +3968,7 @@ "create-new": "Opret ny", "created": "Oprettet", "customize-messages": "Tilpas beskeder", + "cpu-threshold": "CPU-grænseværdi", "delete-notification-text": "Vær forsigtig, efter bekræftelsen vil notifikationen ikke kunne gendannes.", "delete-notification-title": "Er du sikker på, at du vil slette notifikationen?", "delete-notifications-text": "Vær forsigtig, efter bekræftelsen vil notifikationerne ikke kunne gendannes.", @@ -3919,6 +4035,7 @@ "input-fields-support-templatization": "Inputfelter understøtter templatization.", "link": "Link", "link-required": "Link er påkrævet", + "link-max-length": "Linket skal være mindre end eller lig med {{ length }} tegn", "link-type": { "dashboard": "Åbn dashboard", "link": "Åbn URL-link" @@ -3945,6 +4062,7 @@ "no-severity-found": "Ingen alvorlighed fundet", "no-severity-matching": "'{{severity}}' ikke fundet.", "no-template-matching": "Ingen ressource matcher '{{template}}'.", + "create-new-template": "Opret en ny!", "not-found-slack-recipient": "Slack-modtager ikke fundet", "notification": "Notifikation", "notification-center": "Notifikationscenter", @@ -3968,6 +4086,7 @@ "only-rule-chain-lifecycle-failures": "Kun fejl i livscyklus for regelkæder", "only-rule-node-lifecycle-failures": "Kun fejl i livscyklus for regelnoder", "platform-users": "Platformbrugere", + "ram-threshold": "RAM-grænseværdi", "rate-limits": "Grænser for frekvens", "rate-limits-hint": "Hvis feltet er tomt, anvendes udløseren på alle grænser for frekvens", "recipient": "Modtager", @@ -4033,6 +4152,7 @@ "start-from-scratch": "Start fra bunden", "status": "Status", "stop-escalation-alarm-status-become": "Stop eskaleringen når alarmstatus bliver:", + "storage-threshold": "Lagringsgrænseværdi", "subject": "Emne", "subject-required": "Emne er påkrævet", "subject-max-length": "Emnet må højst være {{ length }} tegn", @@ -4046,15 +4166,16 @@ "api-usage-limit": "API-brugsgrænse", "device-activity": "Enhedsaktivitet", "entities-limit": "Enhedsgrænse", - "entity-action": "Entitetshandling", - "general": "Generel", - "rule-engine-lifecycle-event": "Livscyklus for regelmotor", - "rule-node": "Regelnode", + "entity-action": "Enhedshandling", + "general": "Generelt", + "rule-engine-lifecycle-event": "Livscyklushændelse for Rule Engine", + "rule-node": "Rule node", "new-platform-version": "Ny platformversion", - "rate-limits": "Overskredet grænse for frekvens", - "edge-communication-failure": "Edge-kommunikationsfejl", + "rate-limits": "Overskredne hastighedsgrænser", + "edge-communication-failure": "Kommunikationsfejl på edge", "edge-connection": "Edge-forbindelse", - "task-processing-failure": "Fejl i opgavebehandling" + "task-processing-failure": "Fejl i opgavebehandling", + "resources-shortage": "Mangel på ressourcer" }, "templates": "Skabeloner", "notification-templates": "Notifikationer / Skabeloner", @@ -4070,16 +4191,17 @@ "alarm-comment": "Alarmkommentar", "api-usage-limit": "API-brugsgrænse", "device-activity": "Enhedsaktivitet", - "entities-limit": "Entitetsgrænse", - "entity-action": "Entitetshandling", - "rule-engine-lifecycle-event": "Regelmotor livscyklusbegivenhed", + "entities-limit": "Enhedsgrænse", + "entity-action": "Enhedshandling", + "rule-engine-lifecycle-event": "Livscyklushændelse for Rule Engine", "new-platform-version": "Ny platformversion", - "rate-limits": "Overskredet frekvensgrænse", + "rate-limits": "Overskredne hastighedsgrænser", "edge-connection": "Edge-forbindelse", - "edge-communication-failure": "Edge-kommunikationsfejl", + "edge-communication-failure": "Kommunikationsfejl på edge", "task-processing-failure": "Fejl i opgavebehandling", - "trigger": "Udløser", - "trigger-required": "Udløser er påkrævet" + "resources-shortage": "Mangel på ressourcer", + "trigger": "Trigger", + "trigger-required": "Trigger er påkrævet" }, "type": "Type", "unread": "Ulæst", @@ -4116,9 +4238,10 @@ "checksum": "Checksum", "checksum-hint": "Hvis checksum er tom, vil den blive genereret automatisk", "checksum-algorithm": "Checksum-algoritme", - "checksum-copied-message": "Pakke-checksum er blevet kopieret til udklipsholderen", - "change-firmware": "Ændring af firmware kan medføre opdatering af { count, plural, =1 {1 enhed} other {# enheder} }.", - "change-software": "Ændring af software kan medføre opdatering af { count, plural, =1 {1 enhed} other {# enheder} }.", + "checksum-copied-message": "Pakkechecksummen er kopieret til udklipsholderen", + "change-firmware": "Ændring af firmwaren kan medføre opdatering af { count, plural, =1 {1 enhed} other {# enheder} }.", + "change-software": "Ændring af softwaren kan medføre opdatering af { count, plural, =1 {1 enhed} other {# enheder} }.", + "change-ota-setting-title": "Er du sikker på, at du vil ændre OTA-indstillingerne?", "chose-compatible-device-profile": "Den uploadede pakke vil kun være tilgængelig for enheder med den valgte profil.", "chose-firmware-distributed-device": "Vælg firmware, der skal distribueres til enhederne", "chose-software-distributed-device": "Vælg software, der skal distribueres til enhederne", @@ -4314,8 +4437,9 @@ "add-relation-filter": "Tilføj relationsfilter", "any-relation": "Enhver relation", "relation-filters": "Relationsfiltre", + "relation-filter": "Relationsfilter", "additional-info": "Yderligere info (JSON)", - "invalid-additional-info": "Kan ikke analysere yderligere info JSON.", + "invalid-additional-info": "Kunne ikke fortolke JSON for yderligere info.", "no-relations-text": "Ingen relationer fundet", "not": "Ikke" }, @@ -4814,7 +4938,7 @@ "max-parallel-requests-count": "Maksimalt antal parallelle forespørgsler", "max-parallel-requests-count-hint": "Værdien 0 betyder ingen begrænsning", "max-response-size": "Maks. svarstørrelse (i KB)", - "max-response-size-hint": "Maksimal hukommelse tildelt til buffering ved afkodning/enkodning af HTTP-meddelelser", + "max-response-size-hint": "Den maksimale mængde hukommelse, der er tildelt til buffering af data ved dekodning eller kodning af HTTP-meddelelser, såsom JSON- eller XML-payloads", "headers": "Headers", "headers-hint": "Brug ${metadataKey} for værdi fra metadata, $[messageKey] for værdi fra meddelelsesindhold i header/værdifelter", "header": "Header", @@ -4891,7 +5015,7 @@ "client-id": "Klient-ID", "client-id-hint": "Valgfri. Lad være tomt for automatisk generering. For at undgå konflikter i mikrotjenester, brug unikke ID’er.", "append-client-id-suffix": "Tilføj service-ID som suffix til klient-ID", - "client-id-suffix-hint": "Anvendes kun hvis klient-ID er angivet. Hjælper med at undgå konflikter i mikrotjenestetilstand.", + "client-id-suffix-hint": "Valgfrit. Anvendes når \"Client ID\" er angivet eksplicit. Hvis valgt, tilføjes Service ID som et suffiks til Client ID. Hjælper med at undgå fejl, når platformen kører i microservices-tilstand.", "device-id": "Enheds-ID", "device-id-required": "Enheds-ID er påkrævet.", "clean-session": "Ren session", @@ -5043,9 +5167,9 @@ "min-outside-duration-value-required": "Værdi er påkrævet", "min-outside-duration-time-unit": "Tidsenhed for minimal varighed udenfor", "tell-failure-if-absent": "Rapporter fejl ved fravær", - "tell-failure-if-absent-hint": "Hvis en valgt nøgle mangler, returneres fejl.", - "get-latest-value-with-ts": "Hent seneste værdi med tidsstempel", - "get-latest-value-with-ts-hint": "Returnerer JSON med både værdi og tidsstempel.", + "tell-failure-if-absent-hint": "Hvis mindst én af de valgte nøgler ikke findes, vil udgående besked rapportere \"Fejl\".", + "get-latest-value-with-ts": "Hent tidsstempel for de seneste telemetriværdier", + "get-latest-value-with-ts-hint": "Hvis valgt, vil de seneste telemetriværdier også inkludere tidsstempel, f.eks.: \"temp\": \"{\"ts\":1574329385897, \"value\":42}\"", "ignore-null-strings": "Ignorér tomme strenge", "ignore-null-strings-hint": "Ignorerer felter med tomme værdier.", "add-metadata-key-values-as-kafka-headers": "Tilføj nøgle-værdi par fra metadata til Kafka headers", @@ -5152,11 +5276,11 @@ "add-originator-attributes-to": "Tilføj afsenders attributter til", "originator-attributes": "Afsenders attributter", "fetch-latest-telemetry-with-timestamp": "Hent seneste telemetry med tidsstempel", - "fetch-latest-telemetry-with-timestamp-tooltip": "Inkluderer tidsstempel i metadata, f.eks.: \"{{latestTsKeyName}}\": \"{ts:1574329385897, value:42}\"", + "fetch-latest-telemetry-with-timestamp-tooltip": "Inkluderer tidsstempel i metadata, f.eks.: \"{{latestTsKeyName}}\": \"{\"ts\":1574329385897, \"value\":42}\"", "tell-failure": "Rapportér fejl hvis attribut mangler", "tell-failure-tooltip": "Rapporterer fejl hvis mindst én valgt nøgle mangler.", "created-time": "Oprettelsestid", - "chip-help": "Tryk 'Enter' for at afslutte {{inputName}}. 'Backspace' sletter. Flere værdier understøttet.", + "chip-help": "Tryk på 'Enter' for at fuldføre {{inputName}}-indtastning. \nTryk på 'Backspace' for at slette {{inputName}}. \nFlere værdier understøttes.", "detail": "detalje", "field-name": "feltnavn", "device-profile": "enhedsprofil", @@ -5172,14 +5296,14 @@ "fields": "Felter", "skip-empty-fields": "Spring tomme felter over", "skip-empty-fields-tooltip": "Tomme felter tilføjes ikke til uddata.", - "fetch-interval": "Hent interval", - "fetch-strategy": "Hentestrategi", - "fetch-timeseries-from-to": "Hent tidsserie fra {{startInterval}} {{startIntervalTimeUnit}} til {{endInterval}} {{endIntervalTimeUnit}} siden.", - "fetch-timeseries-from-to-invalid": "\"Start\" skal være mindre end \"Slut\".", - "use-metadata-dynamic-interval-tooltip": "Bruger dynamisk start og slut interval baseret på besked og metadata.", - "all-mode-hint": "Ved \"Alle\" hentes alle værdier i intervallet.", - "first-mode-hint": "Ved \"Første\" hentes nærmeste værdi ved start.", - "last-mode-hint": "Ved \"Sidste\" hentes nærmeste værdi ved slut.", + "fetch-interval": "Hentningsinterval", + "fetch-strategy": "Hentningsstrategi", + "fetch-timeseries-from-to": "Hent tidsserier fra {{startInterval}} {{startIntervalTimeUnit}} siden til {{endInterval}} {{endIntervalTimeUnit}} siden.", + "fetch-timeseries-from-to-invalid": "Ugyldig hentning af tidsserier (\"Intervalstart\" skal være mindre end \"Intervalslut\").", + "use-metadata-dynamic-interval-tooltip": "Hvis valgt, vil rule node bruge dynamisk intervalstart og -slut baseret på mønstre i beskeden og metadata.", + "all-mode-hint": "Hvis hentningstilstand \"Alle\" er valgt, vil rule node hente telemetri fra intervallet med konfigurerbare forespørgselsparametre.", + "first-mode-hint": "Hvis hentningstilstand \"Første\" er valgt, vil rule node hente den telemetri, der ligger tættest på intervallets start.", + "last-mode-hint": "Hvis hentningstilstand \"Sidste\" er valgt, vil rule node hente den telemetri, der ligger tættest på intervallets slutning.", "ascending": "Stigende", "descending": "Faldende", "min": "Min", @@ -5230,7 +5354,7 @@ "function-name": "Funktionsnavn", "function-name-required": "Funktionsnavn er påkrævet.", "qualifier": "Kvalifikator", - "qualifier-hint": "Hvis kvalifikator ikke er angivet, bruges standardværdien \"$LATEST\".", + "qualifier-hint": "Hvis kvalifikatoren ikke er angivet, vil standardkvalifikatoren \"$LATEST\" blive brugt.", "aws-credentials": "AWS-legitimationsoplysninger", "connection-timeout": "Forbindelsestimeout", "connection-timeout-required": "Forbindelsestimeout er påkrævet.", @@ -5299,11 +5423,41 @@ "plain-text": "Almindelig tekst", "html": "HTML", "dynamic": "Dynamisk", - "use-body-type-template": "Brug skabelon til brødtype", - "plain-text-description": "Simpel tekst uden formatering.", - "html-text-description": "Tillader HTML-tags til formatering, links og billeder.", - "dynamic-text-description": "Vælg dynamisk mellem almindelig tekst eller HTML.", - "after-template-evaluation-hint": "Efter evaluering: true = HTML, false = tekst." + "use-body-type-template": "Brug skabelon for brødteksttype", + "plain-text-description": "Simpel, uformateret tekst uden særlig formatering eller styling.", + "html-text-description": "Giver dig mulighed for at bruge HTML-tags til formatering, links og billeder i mailens brødtekst.", + "dynamic-text-description": "Gør det muligt at bruge almindelig tekst eller HTML som brødteksttype dynamisk baseret på skabelonfunktionalitet.", + "after-template-evaluation-hint": "Efter skabelonevaluering bør værdien være true for HTML og false for almindelig tekst." + }, + "ai": { + "ai-model": "AI-model", + "model": "Model", + "ai-model-hint": "Vælg den forudkonfigurerede AI-model til at behandle forespørgsler sendt af denne rule node, eller brug \"Opret ny\" for at konfigurere en ny.", + "prompt-settings": "Promptindstillinger", + "prompt-settings-hint": "Den valgfrie systemprompt angiver AI'ens generelle rolle og begrænsninger, mens brugerprompten definerer den specifikke opgave. Begge felter understøtter også skabelonfunktionalitet.", + "system-prompt": "Systemprompt", + "system-prompt-max-length": "Systemprompt må højst være 500000 tegn.", + "system-prompt-blank": "Systemprompt må ikke være tom.", + "user-prompt": "Brugerprompt", + "user-prompt-required": "Brugerprompt er påkrævet.", + "user-prompt-max-length": "Brugerprompt må højst være 500000 tegn.", + "user-prompt-blank": "Brugerprompt må ikke være tom.", + "response-format": "Svarformat", + "response-text": "Tekst", + "response-json": "JSON", + "response-json-schema": "JSON-skema", + "response-format-hint-TEXT": "Tillader modellen at generere vilkårlig tekst, som måske eller måske ikke er gyldig JSON. Hvis output ikke er gyldig JSON, bliver det automatisk pakket ind i en JSON med nøglen \"response\".", + "response-format-hint-JSON": "Modellen skal generere et svar, der er gyldig JSON. Hvis output ikke er gyldig JSON, bliver det automatisk pakket ind i en JSON med nøglen \"response\".", + "response-format-hint-JSON_SCHEMA": "Modellen skal generere en JSON, der matcher strukturen og datatyperne defineret i det angivne skema. Hvis output ikke er gyldig JSON, bliver det automatisk pakket ind i en JSON med nøglen \"response\".", + "response-json-schema-hint": "Selvom enhver gyldig JSON-skema kan indtastes, understøtter denne rule node kun et begrænset sæt funktioner. Se dokumentationen for detaljer.", + "response-json-schema-required": "JSON-skema er påkrævet", + "advanced-settings": "Avancerede indstillinger", + "timeout": "Timeout", + "timeout-hint": "Maksimalt tidsrum at vente på svar \nfra AI-modellen, før forespørgslen afsluttes.", + "timeout-required": "Timeout er påkrævet", + "timeout-validation": "Skal være mellem 1 sekund og 10 minutter.", + "force-acknowledgement": "Tving kvittering", + "force-acknowledgement-hint": "Hvis aktiveret, kvitteres der straks for den indgående besked. Modellens svar bliver derefter sat i kø som en separat, ny besked." } }, "timezone": { @@ -5625,7 +5779,10 @@ "too-small-value-zero": "Værdien skal være større end 0", "too-small-value-one": "Værdien skal være større end 1", "queue-size-is-limited-by-system-configuration": "Køens størrelse er også begrænset af systemkonfigurationen.", - "cassandra-tenant-limits-configuration": "Cassandra-forespørgsel for lejer", + "cassandra-write-tenant-core-limits-configuration": "REST API Cassandra-skriveforespørgsler", + "cassandra-read-tenant-core-limits-configuration": "REST API- og WS-telemetri Cassandra-læseforespørgsler", + "cassandra-write-tenant-rule-engine-limits-configuration": "Rule Engine-telemetri Cassandra-skriveforespørgsler", + "cassandra-read-tenant-rule-engine-limits-configuration": "Rule Engine-telemetri Cassandra-læseforespørgsler", "ws-limit-max-sessions-per-tenant": "Maksimalt antal sessioner pr. lejer", "ws-limit-max-sessions-per-customer": "Maksimalt antal sessioner pr. kunde", "ws-limit-max-sessions-per-regular-user": "Maksimalt antal sessioner pr. almindelig bruger", @@ -5638,6 +5795,7 @@ "ws-limit-updates-per-session": "WS-opdateringer pr. session", "rate-limits": { "add-limit": "Tilføj begrænsning", + "and-also-less-than": "og også mindre end", "advanced-settings": "Avancerede indstillinger", "edit-limit": "Rediger begrænsning", "calculated-field-debug-event-rate-limit": "Fejlfindingshændelser for beregnede felter", @@ -5657,7 +5815,10 @@ "edit-tenant-rest-limits-title": "Rediger REST-anmodninger for lejer", "edit-customer-rest-limits-title": "Rediger REST-anmodninger for kunde", "edit-ws-limit-updates-per-session-title": "Rediger grænse for WS-opdateringer pr. session", - "edit-cassandra-tenant-limits-configuration-title": "Rediger Cassandra-forespørgselsgrænse for lejer", + "edit-cassandra-write-tenant-core-limits-configuration": "Redigér REST API Cassandra skriveforespørgsler", + "edit-cassandra-read-tenant-core-limits-configuration": "Redigér REST API- og WS-telemetri Cassandra læseforespørgsler", + "edit-cassandra-write-tenant-rule-engine-limits-configuration": "Redigér Rule Engine-telemetri Cassandra skriveforespørgsler", + "edit-cassandra-read-tenant-rule-engine-limits-configuration": "Redigér Rule Engine-telemetri Cassandra læseforespørgsler", "edit-tenant-entity-export-rate-limit-title": "Rediger grænse for versionering af entitet", "edit-tenant-entity-import-rate-limit-title": "Rediger grænse for indlæsning af entitet", "edit-tenant-notification-request-rate-limit-title": "Rediger grænse for notifikationsanmodninger", @@ -5679,6 +5840,7 @@ "per-seconds": "Pr. sekunder", "per-seconds-required": "Tidsrate er påkrævet.", "per-seconds-min": "Minimumsværdi er 1.", + "per-seconds-duplicate": "Duplikeret tidsinterval. Hvert tidsinterval skal være unikt.", "rate-limits": "Grænser", "remove-limit": "Fjern begrænsning", "transport-tenant-msg": "Transportlejers beskeder", @@ -5825,14 +5987,126 @@ "label": "Etiket", "value": "Værdi", "date": "Dato", - "show-date-time-interval": "Vis dato og tidsinterval", - "show-date-time-interval-hint": "Vis dato og tidsinterval i henhold til dataaggregeringen.", + "show-date-time-interval": "Vis datotidsinterval", + "show-date-time-interval-hint": "Vis datotidsinterval i henhold til dataaggregering.", + "hide-zero-tooltip-values": "Skjul nulværdier", "background-color": "Baggrundsfarve", "background-blur": "Baggrundssløring" }, "unit": { + "set-unit-conversion": "Indstil enhedsomregning", + "unit-settings": { + "unit-settings": "Enhedsindstillinger", + "source-unit": "Kildenhed", + "source-unit-hint": "Dette er enheden for den gemte værdi. Den enhed, du konverterer fra. Indtast symbolet, som dine kildedata bruger (f.eks. m, km, ft, in).", + "target-metric-unit": "Målenhed (SI) som mål", + "target-metric-unit-hint": "Vælg hvilken måleenhed (SI) du ønsker, at kildeværdien konverteres til (f.eks. cm, mm, km).", + "target-imperial-unit": "Imperial enhed som mål", + "target-imperial-unit-hint": "Vælg hvilken imperial enhed du ønsker, at kildeværdien konverteres til (f.eks. in, ft, yd).", + "target-hybrid-unit": "Hybrid enhed som mål", + "target-hybrid-unit-hint": "Vælg hvilken hybrid enhed du ønsker, at kildeværdien konverteres til (f.eks. cm, in, km). Hybrid enheder kombinerer metriske og/eller imperial enheder.", + "enable-unit-conversion": "Aktivér enhedsomregning", + "enable-unit-conversion-hint": "Slå til for at aktivere omregning. Når slået fra, sendes kildeværdien videre uden ændringer. Deaktiveret hvis der kun er én enhed i den tilsvarende målegruppe (f.eks. Lysstrøm, AQI)." + }, + "unit-system": "Enhedssystem", + "unit-system-type": { + "AUTO": "Auto", + "METRIC": "Metrisk", + "IMPERIAL": "Imperial", + "HYBRID": "Hybrid" + }, + "measures": { + "absorbed-dose-rate": "Absorberet dosis pr. tidsenhed", + "acceleration": "Acceleration", + "acidity": "Surhedsgrad", + "air-quality-index": "Luftkvalitetsindeks", + "amount-of-substance": "Stofmængde", + "angle": "Vinkel", + "angular-acceleration": "Vinkelacceleration", + "area": "Areal", + "area-density": "Arealdensitet", + "capacitance": "Kapacitans", + "catalytic-activity": "Katalytisk aktivitet", + "catalytic-concentration": "Katalytisk koncentration", + "charge": "Elektrisk ladning", + "current-density": "Strømstyrketæthed", + "data-transfer-rate": "Dataoverførselshastighed", + "density": "Densitet", + "digital": "Digital", + "dimension-ratio": "Forhold mellem dimensioner", + "dynamic-viscosity": "Dynamisk viskositet", + "earthquake-magnitude": "Jordskælvsmagnitude", + "electric-charge-density": "Elektrisk ladningstæthed", + "electric-current": "Elektrisk strøm", + "electric-dipole-moment": "Elektrisk dipolmoment", + "electric-field-strength": "Elektrisk feltstyrke", + "electric-flux": "Elektrisk flux", + "electric-permittivity": "Elektrisk permittivitet", + "electric-polarizability": "Elektrisk polariserbarhed", + "electrical-conductance": "Elektrisk ledningsevne", + "electrical-conductivity": "Elektrisk konduktivitet", + "energy": "Energi", + "energy-density": "Energitæthed", + "force": "Kraft", + "frequency": "Frekvens", + "fuel-efficiency": "Brændstofeffektivitet", + "heat-capacity": "Varmekapacitet", + "illuminance": "Belysningsstyrke", + "inductance": "Induktans", + "kinematic-viscosity": "Kinematisk viskositet", + "length": "Længde", + "light-exposure": "Lysudsættelse", + "linear-charge-density": "Lineær ladningstæthed", + "logarithmic-ratio": "Logaritmisk forhold", + "luminous-efficacy": "Lysudbytte", + "luminous-flux": "Lysstrøm", + "luminous-intensity": "Lysintensitet", + "magnetic-field-gradient": "Magnetfeltgradient", + "magnetic-flux": "Magnetisk flux", + "magnetic-flux-density": "Magnetisk fluxdensitet", + "magnetic-moment": "Magnetisk moment", + "magnetic-permeability": "Magnetisk permeabilitet", + "mass": "Masse", + "mass-fraction": "Massefraktion", + "molar-concentration": "Molær koncentration", + "molar-energy": "Molær energi", + "molar-heat-capacity": "Molær varmekapacitet", + "molar-mass": "Molær masse", + "number-concentration": "Koncentration (antal)", + "parts-per-million": "Dele pr. million", + "power": "Effekt", + "power-density": "Effekttæthed", + "pressure": "Tryk", + "radiance": "Strålingsintensitet", + "radiant-intensity": "Strålingsstyrke", + "radiation-dose": "Strålingsdosis", + "radioactive-decay": "Radioaktivt henfald", + "radioactivity": "Radioaktivitet", + "radioactivity-concentration": "Koncentration af radioaktivitet", + "reciprocal-length": "Reciprok længde", + "resistance": "Modstand", + "reynolds-number": "Reynolds-tal", + "signal-level": "Signalkvalitet", + "solid-angle": "Rumvinkel", + "specific-energy": "Specifik energi", + "specific-heat-capacity": "Specifik varmekapacitet", + "specific-humidity": "Specifik fugtighed", + "specific-volume": "Specifikt volumen", + "speed": "Hastighed", + "surface-charge-density": "Overfladeladningstæthed", + "surface-tension": "Overfladespænding", + "temperature": "Temperatur", + "thermal-conductivity": "Termisk ledningsevne", + "time": "Tid", + "torque": "Moment", + "turbidity": "Turbiditet", + "voltage": "Spænding", + "volume": "Volumen", + "volume-flow": "Volumenstrøm" + }, "millimeter": "Millimeter", "centimeter": "Centimeter", + "decimeter": "Decimeter", "angstrom": "Ångström", "nanometer": "Nanometer", "micrometer": "Mikrometer", @@ -5840,6 +6114,7 @@ "kilometer": "Kilometer", "inch": "Tommer", "foot": "Fod", + "foot-us": "Fod (US opmåling)", "yard": "Yard", "mile": "Mil", "nautical-mile": "Sømil", @@ -5847,14 +6122,14 @@ "reciprocal-metre": "Reciprok meter", "meter-per-meter": "Meter pr. meter", "steradian": "Steradian", - "thou": "Tusindedel tomme", + "thou": "Tø", "barleycorn": "Bygkorn", "hand": "Hånd", "chain": "Kæde", "furlong": "Furlong", "league": "League", "fathom": "Favn", - "cable": "Kabellængde", + "cable": "Kabel", "link": "Led", "rod": "Stang", "nanogram": "Nanogram", @@ -5886,6 +6161,7 @@ "cubic-foot": "Kubikfod", "cubic-yard": "Kubikyard", "fluid-ounce": "Fluid ounce", + "fluid-ounce-per-second": "Fluid ounce pr. sekund", "pint": "Pint", "quart": "Quart", "gallon": "Gallon", @@ -5904,9 +6180,13 @@ "meter-per-second": "Meter pr. sekund", "kilometer-per-hour": "Kilometer i timen", "foot-per-second": "Fod pr. sekund", + "foot-per-minute": "Fod pr. minut", "mile-per-hour": "Mil i timen", "knot": "Knob", + "inch-per-second": "Tommer pr. sekund", + "inch-per-hour": "Tommer pr. time", "millimeters-per-minute": "Millimeter pr. minut", + "meter-per-minute": "Meter pr. minut", "kilometer-per-hour-squared": "Kilometer i timen i anden", "foot-per-second-squared": "Fod pr. sekund i anden", "pascal": "Pascal", @@ -5923,6 +6203,7 @@ "newton-per-meter": "Newton pr. meter", "atmospheres": "Atmosfærer", "pounds-per-square-inch": "Pund pr. kvadrattomme", + "kilopound-per-square-inch": "Kilopund pr. kvadrattomme", "torr": "Torr", "inches-of-mercury": "Tommer kviksølv", "pascal-per-square-meter": "Pascal pr. kvadratmeter", @@ -5940,10 +6221,16 @@ "megajoule": "Megajoule", "gigajoule": "Gigajoule", "watt-hour": "Watt-time", + "watt-minute": "Watt-minut", "kilowatt-hour": "Kilowatt-time", + "milliwatt-hour": "Milliwatt-time", + "megawatt-hour": "Megawatt-time", + "gigawatt-hour": "Gigawatt-time", "electron-volts": "Elektronvolt", "joules-per-coulomb": "Joule pr. coulomb", "british-thermal-unit": "Britiske termiske enheder", + "thousand-british-thermal-unit": "Tusinde britiske termiske enheder", + "million-british-thermal-unit": "Million britiske termiske enheder", "foot-pound": "Fodpund", "calorie": "Kalorie", "small-calorie": "Lille kalorie", @@ -5974,10 +6261,20 @@ "watt-per-square-inch": "Watt pr. kvadrattomme", "kilowatt-per-square-inch": "Kilowatt pr. kvadrattomme", "horsepower": "Hestekræfter", - "btu-per-hour": "Britiske termiske enheder/time", + "btu-per-hour": "Britiske termiske enheder pr. time", + "btu-per-second": "Britiske termiske enheder pr. sekund", + "btu-per-day": "Britiske termiske enheder pr. dag", + "mbtu-per-hour": "Tusinde BTU pr. time", + "mbtu-per-second": "Tusinde BTU pr. sekund", + "mbtu-per-day": "Tusinde BTU pr. dag", + "mmbtu-per-hour": "Million BTU pr. time", + "mmbtu-per-second": "Million BTU pr. sekund", + "mmbtu-per-day": "Million BTU pr. dag", + "foot-pound-per-second": "Fodpund pr. sekund", "coulomb": "Coulomb", "millicoulomb": "Millicoulomb", "microcoulomb": "Mikrocoulomb", + "nanocoulomb": "Nanocoulomb", "picocoulomb": "Picocoulomb", "coulomb-per-meter": "Coulomb pr. meter", "coulomb-per-cubic-meter": "Coulomb pr. kubikmeter", @@ -5987,7 +6284,7 @@ "square-meter": "Kvadratmeter", "hectare": "Hektar", "square-kilometer": "Kvadratkilometer", - "square-inch": "Kvadrattomme", + "square-inch": "Kvadrattommer", "square-foot": "Kvadratfod", "square-yard": "Kvadratyard", "acre": "Acre", @@ -5996,24 +6293,31 @@ "barn": "Barn", "circular-inch": "Cirkulær tomme", "milliampere-hour": "Milliampere-time", - "ampere-hours": "Amperetimer", - "kiloampere-hours": "Kiloamperetimer", + "ampere-hours": "Ampere-timer", + "kiloampere-hours": "Kiloampere-timer", "nanoampere": "Nanoampere", "picoampere": "Picoampere", "microampere": "Mikroampere", "milliampere": "Milliampere", "ampere": "Ampere", + "kiloampere": "Kiloampere", + "megaampere": "Megaampere", + "gigaampere": "Gigaampere", "microampere-per-square-centimeter": "Mikroampere pr. kvadratcentimeter", "ampere-per-square-meter": "Ampere pr. kvadratmeter", "ampere-per-meter": "Ampere pr. meter", "oersted": "Oersted", "bohr-magneton": "Bohr magneton", - "ampere-meter-squared": "Ampere-meter kvadreret", + "ampere-meter-squared": "Ampere meter i anden", "nanovolt": "Nanovolt", "picovolt": "Picovolt", + "millivolt": "Millivolt", + "microvolt": "Mikrovolt", "volt": "Volt", - "dbmV": "dBmV", - "dbm": "dBm", + "kilovolt": "Kilovolt", + "megavolt": "Megavolt", + "dbmV": "Decibel volt", + "dbm": "Decibel-milliwatt", "volt-meter": "Volt-meter", "kilovolt-meter": "Kilovolt-meter", "megavolt-meter": "Megavolt-meter", @@ -6023,13 +6327,15 @@ "ohm": "Ohm", "microohm": "Mikroohm", "milliohm": "Milliohm", - "kilohm": "Kiloohm", - "megohm": "Megaohm", - "gigohm": "Gigaohm", + "kilohm": "Kilohm", + "megohm": "Megohm", + "gigohm": "Gigohm", + "millihertz": "Millihertz", "hertz": "Hertz", "kilohertz": "Kilohertz", "megahertz": "Megahertz", "gigahertz": "Gigahertz", + "terahertz": "Terahertz", "rpm": "Omdrejninger pr. minut", "candela-per-square-meter": "Candela pr. kvadratmeter", "candela": "Candela", @@ -6037,8 +6343,8 @@ "lux": "Lux", "foot-candle": "Foot-candle", "lumen-per-square-meter": "Lumen pr. kvadratmeter", - "lux-second": "Lux sekund", - "lumen-second": "Lumen sekund", + "lux-second": "Lux-sekund", + "lumen-second": "Lumen-sekund", "lumens-per-watt": "Lumen pr. watt", "mole": "Mol", "nanomole": "Nanomol", @@ -6046,9 +6352,9 @@ "millimole": "Millimol", "kilomole": "Kilomol", "mole-per-cubic-meter": "Mol pr. kubikmeter", - "rssi": "RSSI", - "ppm": "Dele pr. million", - "ppb": "Dele pr. milliard", + "rssi": "Signalstyrkeindikator (RSSI)", + "ppm": "Dele pr. million (ppm)", + "ppb": "Dele pr. milliard (ppb)", "micrograms-per-cubic-meter": "Mikrogram pr. kubikmeter", "aqi": "Luftkvalitetsindeks (AQI)", "gram-per-cubic-meter": "Gram pr. kubikmeter", @@ -6115,6 +6421,9 @@ "millibars": "Millibar", "inch-of-mercury": "Tommer kviksølv", "richter-scale": "Richterskala", + "nanosecond": "Nanosekund", + "microsecond": "Mikrosekund", + "millisecond": "Millisekund", "second": "Sekund", "minute": "Minut", "hour": "Time", @@ -6127,9 +6436,10 @@ "cubic-meters-per-second": "Kubikmeter pr. sekund", "liter-per-second": "Liter pr. sekund", "liter-per-minute": "Liter pr. minut", - "gallons-per-minute": "Galloner pr. minut", + "gallons-per-minute": "Gallons pr. minut", "cubic-foot-per-second": "Kubikfod pr. sekund", "milliliters-per-minute": "Milliliter pr. minut", + "cubic-decimeter-per-second": "Kubikdecimeter pr. sekund", "bit": "Bit", "byte": "Byte", "kilobyte": "Kilobyte", @@ -6152,6 +6462,9 @@ "degree": "Grad", "radian": "Radian", "gradian": "Gradian", + "arcminute": "Bue minut", + "arcsecond": "Bue sekund", + "milliradian": "Milliradian", "revolution": "Omdrejning", "siemens": "Siemens", "millisiemens": "Millisiemens", @@ -6221,10 +6534,12 @@ "radian-per-second": "Radian pr. sekund", "radian-per-second-squared": "Radian pr. sekund i anden", "revolutions-per-minute-per-second": "Vinkelacceleration", - "deg-per-second": "grader/sekund", + "deg-per-second": "Grader pr. sekund", + "rotation-per-minute": "Rotation pr. minut", "degrees-brix": "Grader Brix", "katal": "Katal", - "katal-per-cubic-metre": "Katal pr. kubikmeter" + "katal-per-cubic-metre": "Katal pr. kubikmeter", + "paris-inch": "Paris-tomme" }, "user": { "user": "Bruger", @@ -6256,7 +6571,7 @@ "default-dashboard": "Standard dashboard", "always-fullscreen": "Altid fuldskærm", "select-user": "Vælg bruger", - "no-users-matching": "Ingen brugere matcher '{{entity}}'.", + "no-users-matching": "Ingen brugere, der matcher '{{entity}}', blev fundet.", "user-required": "Bruger er påkrævet", "activation-method": "Aktiveringsmetode", "display-activation-link": "Vis aktiveringslink", @@ -6362,7 +6677,7 @@ "updated": "{{updated}} opdateret", "deleted": "{{deleted}} slettet", "remove-other-entities-confirm-text": "Vær forsigtig! Dette vil permanent slette alle nuværende enheder
    som ikke er til stede i den version, du ønsker at gendanne.

    Indtast \"remove other entities\" for at bekræfte.", - "auto-commit-to-branch": "auto-commit til {{ branch }} gren", + "auto-commit-to-branch": "auto-commit til {{ branch }}-grenen", "default-create-entity-version-name": "{{entityName}} opdatering", "sync-strategy-merge-hint": "Opretter eller opdaterer valgte enheder i arkivet. Alle andre enheder i arkivet ændres ikke.", "sync-strategy-overwrite-hint": "Opretter eller opdaterer valgte enheder i arkivet. Alle andre enheder i arkivet bliver slettet.", @@ -6382,28 +6697,28 @@ "all-widgets": "Alle widgets", "widget": "Widget", "select-widget": "Vælg widget", - "no-widgets-matching": "Ingen widgets matcher '{{entity}}'.", + "no-widgets-matching": "Ingen widgets, der matcher '{{entity}}', blev fundet.", "no-widgets": "Ingen widgets endnu", "no-widgets-text": "Ingen widgets fundet", "management": "Widgetadministration", "editor": "Widget-editor", - "confirm-to-exit-editor-html": "Du har ikke gemte widgetindstillinger.
    Er du sikker på, at du vil forlade denne side?", - "widget-type-not-found": "Problem med indlæsning af widgetkonfiguration.
    Den tilknyttede widgettype er muligvis blevet fjernet.", - "widget-type-load-error": "Widget blev ikke indlæst pga. følgende fejl:", + "confirm-to-exit-editor-html": "Du har ikke gemt dine widgetindstillinger.
    Er du sikker på, at du vil forlade denne side?", + "widget-type-not-found": "Problem med at indlæse widgetkonfiguration.
    Den tilknyttede widgettype er sandsynligvis blevet fjernet.", + "widget-type-load-error": "Widget blev ikke indlæst på grund af følgende fejl:", "remove": "Fjern widget", "delete": "Slet widget", "edit": "Rediger widget", "remove-widget-title": "Er du sikker på, at du vil fjerne widgetten '{{widgetTitle}}'?", - "remove-widget-text": "Efter bekræftelse vil widgetten og alle relaterede data være uoprettelige.", + "remove-widget-text": "Efter bekræftelse vil widgetten og alle tilknyttede data ikke kunne gendannes.", "replace-reference-with-widget-copy": "Erstat reference med kopi af widget", - "timeseries": "Tidsserie", + "timeseries": "Tidsserier", "search-data": "Søg data", "no-data-found": "Ingen data fundet", "latest": "Seneste værdier", "rpc": "Kontrolwidget", "alarm": "Alarmwidget", "static": "Statisk widget", - "timeseries-short": "serie", + "timeseries-short": "serier", "latest-short": "seneste", "rpc-short": "kontrol", "alarm-short": "alarm", @@ -7774,6 +8089,18 @@ "fill-area-opacity": "Gennemsigtighed for områdeudfyldning", "range-chart-style": "Stil for område-diagram" }, + "knob": { + "behavior": "Adfærd", + "initial-value": "Startværdi", + "initial-value-hint": "Handling til at hente startværdien for knappen.", + "on-value-change": "Ved værdiskift", + "on-value-change-hint": "Handling udløst, når værdien på knappen ændres.", + "range": "Interval", + "min": "min", + "max": "maks", + "value": "Værdi", + "fallback-initial-value": "Fallback startværdi" + }, "rpc": { "value-settings": "Indstillinger for værdi", "initial-value": "Startværdi", @@ -7830,9 +8157,7 @@ "led-status-value-timeseries": "Enheds tidsserie der indeholder LED-statusværdi", "check-status-method": "RPC-metode til kontrol af enhedsstatus", "parse-led-status-value-function": "Fortolk LED-statusværdi-funktion", - "knob-title": "Titel for knap", - "min-value": "Minimumværdi", - "max-value": "Maksimumværdi" + "knob-title": "Titel for knap" }, "maps": { "map-type": { @@ -8676,18 +9001,22 @@ "pie-chart-card-style": "Kortstil for cirkeldiagram" }, "radar-chart": { - "radar-appearance": "Udseende for radardiagram", + "radar-appearance": "Radar-udseende", "shape": "Form", "shape-polygon": "Polygon", "shape-circle": "Cirkel", "color": "Farve", "line": "Linje", "points": "Punkter", - "points-label": "Etiket for punkter", + "points-label": "Punktetiket", "radar-axis": "Radarakse", "axis-label": "Aksesetiket", - "ticks-label": "Etiket for mærker", - "radar-chart-style": "Stil for radardiagram" + "ticks-label": "Taktetiket", + "radar-chart-style": "Radar-diagramstil", + "max-axes-scaling": "Maks. akseskalering", + "max-axes-scaling-hint": "Vælg om hver radarakse skal have sin egen maksimale værdi (Separat), eller om den deler den højeste værdi på tværs af alle akser baseret på widget-datasættet (Fælles).", + "separate": "Separat", + "common": "Fælles" }, "time-series-chart": { "chart": "Diagram", @@ -9191,6 +9520,6 @@ "items-per-page-separator": "af" }, "language": { - "language": "Language" + "language": "Sprog" } } diff --git a/ui-ngx/src/assets/locale/locale.constant-de_DE.json b/ui-ngx/src/assets/locale/locale.constant-de_DE.json index f48081bee3..8df5966d60 100644 --- a/ui-ngx/src/assets/locale/locale.constant-de_DE.json +++ b/ui-ngx/src/assets/locale/locale.constant-de_DE.json @@ -545,7 +545,13 @@ "slack-settings": "Slack-Einstellungen", "mobile-settings": "Mobile Einstellungen", "firebase-service-account-file": "Firebase-Service-Konto-Anmeldeinformationen (JSON-Datei)", - "select-firebase-service-account-file": "Ziehen Sie Ihre Firebase-Service-Konto-Datei hierher oder " + "select-firebase-service-account-file": "Ziehen Sie Ihre Firebase-Service-Konto-Datei hierher oder ", + "trendz": "Trendz", + "trendz-settings": "Trendz-Einstellungen", + "trendz-url": "Trendz-URL", + "trendz-url-required": "Trendz-URL ist erforderlich", + "trendz-api-key": "Trendz-API-Schlüssel", + "trendz-enable": "Trendz aktivieren" }, "alarm": { "alarm": "Alarm", @@ -677,8 +683,8 @@ "filter-type-entity-list": "Entitätsliste", "filter-type-entity-name": "Entitätsname", "filter-type-entity-type": "Entitätstyp", - "filter-type-state-entity": "Entität aus Dashboard-Zustand", - "filter-type-state-entity-description": "Entität aus Dashboard-Zustandsparametern", + "filter-type-state-entity": "Entität aus dem Dashboard-Zustand", + "filter-type-state-entity-description": "Entität, die aus den Dashboard-Zustandsparametern stammt", "filter-type-asset-type": "Asset-Typ", "filter-type-asset-type-description": "Assets vom Typ '{{assetTypes}}'", "filter-type-asset-type-and-name-description": "Assets vom Typ '{{assetTypes}}' mit Namen beginnend mit '{{prefix}}'", @@ -709,12 +715,13 @@ "filter-type-required": "Filtertyp ist erforderlich.", "entity-filter-no-entity-matched": "Keine Entitäten entsprechen dem angegebenen Filter.", "no-entity-filter-specified": "Kein Entitätsfilter angegeben", - "root-state-entity": "Dashboard-Zustandsentität als Root verwenden", + "root-state-entity": "Dashboard-Zustandsentität als Wurzel verwenden", "last-level-relation": "Nur letzte Beziehungsebene abrufen", - "root-entity": "Root-Entität", + "root-entity": "Wurzelentität", "state-entity-parameter-name": "Parametername der Zustandsentität", "default-state-entity": "Standard-Zustandsentität", "default-entity-parameter-name": "Standardmäßig", + "query-options": "Abfrageoptionen", "max-relation-level": "Maximale Beziehungsebene", "unlimited-level": "Unbegrenzte Ebene", "state-entity": "Dashboard-Zustandsentität", @@ -917,18 +924,23 @@ "view-statistics": "Statistiken anzeigen" }, "api-limit": { - "cassandra-queries": "Cassandra-Abfragen", + "cassandra-write-queries-core": "REST-API Cassandra-Schreibabfragen", + "cassandra-read-queries-core": "REST-API- und WS-Telemetrie Cassandra-Leseabfragen", + "cassandra-write-queries-rule-engine": "Rule Engine Telemetrie Cassandra-Schreibabfragen", + "cassandra-read-queries-rule-engine": "Rule Engine Telemetrie Cassandra-Leseabfragen", + "cassandra-write-queries-monolith": "Monolith Telemetrie Cassandra-Schreibabfragen", + "cassandra-read-queries-monolith": "Monolith Telemetrie Cassandra-Leseabfragen", "entity-version-creation": "Erstellung von Entitätsversionen", "entity-version-load": "Laden von Entitätsversionen", - "notification-requests": "Benachrichtigungsanforderungen", - "notification-requests-per-rule": "Benachrichtigungsanforderungen pro Regel", + "notification-requests": "Benachrichtigungsanfragen", + "notification-requests-per-rule": "Benachrichtigungsanfragen pro Regel", "rest-api-requests": "REST-API-Anfragen", "rest-api-requests-per-customer": "REST-API-Anfragen pro Kunde", "transport-messages": "Transportnachrichten", "transport-messages-per-device": "Transportnachrichten pro Gerät", "transport-messages-per-gateway": "Transportnachrichten pro Gateway", "transport-messages-per-gateway-device": "Transportnachrichten pro Gateway-Gerät", - "ws-updates-per-session": "WebSocket-Aktualisierungen pro Sitzung", + "ws-updates-per-session": "WS-Updates pro Sitzung", "edge-events": "Edge-Ereignisse", "edge-events-per-edge": "Edge-Ereignisse pro Edge", "edge-uplink-messages": "Edge-Uplink-Nachrichten", @@ -1057,6 +1069,7 @@ "delete-multiple-title": "Möchten Sie { count, plural, =1 {1 berechnetes Feld} other {# berechnete Felder} } wirklich löschen?", "delete-multiple-text": "Vorsicht, nach der Bestätigung werden alle ausgewählten berechneten Felder entfernt und alle zugehörigen Daten unwiederbringlich gelöscht.", "test-with-this-message": "Mit dieser Nachricht testen", + "use-latest-timestamp": "Letzten Zeitstempel verwenden", "hint": { "arguments-simple-with-rolling": "Einfacher Feldtyp darf keine Schlüssel mit Zeitreihen-Rollup-Typ enthalten.", "arguments-empty": "Argumente dürfen nicht leer sein.", @@ -1072,9 +1085,87 @@ "max-args": "Maximale Anzahl an Argumenten erreicht.", "decimals-range": "Standard-Dezimalstellen sollten eine Zahl zwischen 0 und 15 sein.", "expression": "Standardausdruck demonstriert, wie eine Temperatur von Fahrenheit in Celsius umgewandelt wird.", - "arguments-entity-not-found": "Zielentität des Arguments nicht gefunden." + "arguments-entity-not-found": "Zielentität des Arguments nicht gefunden.", + "use-latest-timestamp": "Wenn aktiviert, wird der berechnete Wert mit dem neuesten Zeitstempel aus der Telemetrie der Argumente gespeichert, anstatt mit der Serverzeit." } }, + "ai-models": { + "ai-models": "KI-Modelle", + "ai-model": "KI-Modell", + "model": "Modell", + "name": "Name", + "ai-provider": "KI-Anbieter", + "no-found": "Keine KI-Modelle gefunden", + "list": "{ count, plural, =1 {Ein Modell} other {Liste von # Modellen} }", + "selected-fields": "{ count, plural, =1 {1 Modell} other {# Modelle} } ausgewählt", + "add": "Modell hinzufügen", + "delete-model-title": "Sind Sie sicher, dass Sie das Modell '{{modelName}}' löschen möchten?", + "delete-model-text": "Achtung, nach der Bestätigung wird das Modell und alle zugehörigen Daten unwiederbringlich gelöscht.", + "delete-models-title": "Sind Sie sicher, dass Sie { count, plural, =1 {1 Modell} other {# Modelle} } löschen möchten?", + "delete-models-text": "Achtung, nach der Bestätigung werden alle ausgewählten Modelle entfernt und alle zugehörigen Daten unwiederbringlich gelöscht.", + "ai-providers": { + "openai": "OpenAI", + "azure-openai": "Azure OpenAI", + "google-ai-gemini": "Google AI Gemini", + "google-vertex-ai-gemini": "Google Vertex AI Gemini", + "mistral-ai": "Mistral AI", + "anthropic": "Anthropic", + "amazon-bedrock": "Amazon Bedrock", + "github-models": "GitHub-Modelle" + }, + "name-required": "Name ist erforderlich.", + "name-max-length": "Der Name darf höchstens 255 Zeichen lang sein.", + "provider": "Anbieter", + "api-key": "API-Schlüssel", + "api-key-required": "API-Schlüssel ist erforderlich.", + "project-id": "Projekt-ID", + "project-id-required": "Projekt-ID ist erforderlich.", + "location": "Standort", + "location-required": "Standort ist erforderlich.", + "service-account-key-file": "Dienstkontoschlüssel-Datei", + "service-account-key-file-required": "Dienstkontoschlüssel-Datei ist erforderlich.", + "no-file": "Keine Datei ausgewählt.", + "drop-file": "Datei ablegen oder klicken, um eine Datei auszuwählen.", + "personal-access-token": "Persönlicher Zugriffstoken", + "personal-access-token-required": "Persönlicher Zugriffstoken ist erforderlich.", + "configuration": "Konfiguration", + "model-id": "Modell-ID", + "model-id-required": "Modell-ID ist erforderlich.", + "deployment-name": "Bereitstellungsname", + "deployment-name-required": "Bereitstellungsname ist erforderlich.", + "set": "Festlegen", + "region": "Region", + "region-required": "Region ist erforderlich.", + "access-key-id": "Access Key ID", + "access-key-id-required": "Access Key ID ist erforderlich.", + "secret-access-key": "Secret Access Key", + "secret-access-key-required": "Secret Access Key ist erforderlich.", + "temperature": "Temperatur", + "temperature-hint": "Reguliert den Grad der Zufälligkeit im Modellausgang. Höhere Werte erhöhen die Zufälligkeit, niedrigere verringern sie.", + "temperature-min": "Muss 0 oder größer sein.", + "top-p": "Top P", + "top-p-hint": "Erstellt einen Pool der wahrscheinlichsten Tokens, aus denen das Modell auswählt. Höhere Werte erweitern den Pool, niedrigere verkleinern ihn.", + "top-p-min-max": "Muss größer als 0 und maximal 1 sein.", + "top-k": "Top K", + "top-k-hint": "Begrenzt die Auswahl des Modells auf die „K“ wahrscheinlichsten Tokens.", + "top-k-min": "Muss 0 oder größer sein.", + "presence-penalty": "Strafe für Anwesenheit", + "presence-penalty-hint": "Wendet eine feste Strafe auf die Wahrscheinlichkeit eines Tokens an, wenn es bereits im Text erschienen ist.", + "frequency-penalty": "Strafe für Häufigkeit", + "frequency-penalty-hint": "Verringert die Wahrscheinlichkeit eines Tokens basierend auf seiner Häufigkeit im Text.", + "max-output-tokens": "Maximale Ausgabetokens", + "max-output-tokens-min": "Muss größer als 0 sein.", + "max-output-tokens-hint": "Legt die maximale Anzahl an Tokens fest, die das Modell in einer Antwort generieren kann.", + "endpoint": "Endpunkt", + "endpoint-required": "Endpunkt ist erforderlich.", + "service-version": "Service-Version", + "check-connectivity": "Konnektivität prüfen", + "check-connectivity-success": "Testanfrage war erfolgreich", + "check-connectivity-failed": "Testanfrage fehlgeschlagen", + "no-model-matching": "Keine mit '{{entity}}' übereinstimmenden Modelle gefunden.", + "model-required": "Modell ist erforderlich.", + "no-model-text": "Keine Modelle gefunden." + }, "confirm-on-exit": { "message": "Sie haben ungespeicherte Änderungen. Möchten Sie diese Seite wirklich verlassen?", "html-message": "Sie haben ungespeicherte Änderungen.
    Möchten Sie diese Seite wirklich verlassen?", @@ -1754,6 +1845,7 @@ "selected-options-limit": "Auswahloptionen Limit", "advanced-ui-settings": "Erweiterte UI-Einstellungen", "disable-on-property": "Bei Eigenschaft deaktivieren", + "disable-on-property-none": "Keine (Feld immer aktiviert)", "display-condition-function": "Anzeigebedingungsfunktion", "sub-label": "Unterbezeichnung", "vertical-divider-after": "Vertikale Trennlinie danach", @@ -1787,7 +1879,8 @@ "array-item": "Array-Element", "item-type": "Elementtyp", "item-name": "Elementname", - "no-items": "Keine Elemente" + "no-items": "Keine Elemente", + "support-unit-conversion": "Unterstützt Einheitenumrechnung" }, "clear-form": "Formular löschen", "clear-form-prompt": "Möchten Sie wirklich alle Formulareigenschaften entfernen?", @@ -1911,6 +2004,7 @@ "mqtt-use-json-format-for-default-downlink-topics-hint": "Wenn aktiviert, wird das JSON-Format für die Standard-Downlink-Themen verwendet, z.B.: v1/devices/me/attributes/response/$request_id. Neue v2-Themen sind davon nicht betroffen.", "mqtt-send-ack-on-validation-exception": "PUBACK bei Validierungsfehler senden", "mqtt-send-ack-on-validation-exception-hint": "Standardmäßig wird die Sitzung bei einem Validierungsfehler geschlossen. Wenn aktiviert, wird stattdessen eine Bestätigung gesendet.", + "mqtt-protocol-version": "Protokollversion", "snmp-add-mapping": "SNMP-Zuordnung hinzufügen", "snmp-mapping-not-configured": "Keine Zuordnung für OID zu Zeitreihen/Telemetrie konfiguriert", "snmp-timseries-or-attribute-name": "Zeitreihe/Attributname für Zuordnung", @@ -2171,6 +2265,9 @@ "add-lwm2m-server-config": "LwM2M-Server hinzufügen", "no-config-servers": "Keine Server konfiguriert", "others-tab": "Weitere Einstellungen", + "ota-update": "OTA-Update", + "use-object-19-for-ota-update": "Objekt 19 für OTA-Dateimetadaten verwenden (Prüfsumme, Größe, Version, Name)", + "use-object-19-for-ota-update-hint": "Verwenden Sie Resource ObjectId = 19 für OTA-Updates: Firmware → InstanceId = 65534, Software → InstanceId = 65535. Das Datenformat ist JSON, codiert in Base64. Dieses JSON enthält OTA-Dateimetadaten (Dateiinformationen): „Checksum“ (SHA256). Zusätzliche Felder: „Title“ (OTA-Name), „Version“ (OTA-Version), „File Name“ (Dateiname zur Speicherung auf dem Client), „File Size“ (OTA-Größe in Bytes).", "client-strategy": "Client-Strategie beim Verbinden", "client-strategy-label": "Strategie", "client-strategy-only-observe": "Nur Beobachtungsanfragen nach der ersten Verbindung", @@ -2201,7 +2298,17 @@ "default-object-id": "Standardobjektversion (Attribut)", "default-object-id-ver": { "v1-0": "1.0", - "v1-1": "1.1" + "v1-1": "1.1", + "v1-2": "1.2" + }, + "observe-strategy": { + "observe-strategy": "Beobachtungsstrategie", + "single": "Einzeln", + "single-description": "Eine Beobachtungsanfrage pro Ressource (höhere Präzision, mehr Netzwerkverkehr)", + "composite-all": "Komplett zusammengefasst", + "composite-all-description": "Alle Ressourcen werden mit einer einzigen zusammengefassten Beobachtungsanfrage überwacht (effizienter, weniger flexibel)", + "composite-by-object": "Nach Objekten zusammengefasst", + "composite-by-object-description": "Ressourcen werden nach Objekttyp gruppiert und mit separaten zusammengefassten Beobachtungsanfragen überwacht (ausgewogener Ansatz)" } }, "snmp": { @@ -2524,6 +2631,8 @@ "type-current-user-owner": "Aktueller Benutzerinhaber", "type-calculated-field": "Berechnetes Feld", "type-calculated-fields": "Berechnete Felder", + "type-ai-model": "KI-Modell", + "type-ai-models": "KI-Modelle", "type-widgets-bundle": "Widget-Bündel", "type-widgets-bundles": "Widget-Bündel", "list-of-widgets-bundles": "{ count, plural, =1 {Ein Widget-Bündel} other {Liste von # Widget-Bündeln} }", @@ -2553,6 +2662,8 @@ "type-tb-resources": "Ressourcen", "list-of-tb-resources": "{ count, plural, =1 {Eine Ressource} other {Liste von # Ressourcen} }", "type-ota-package": "OTA-Paket", + "type-ota-packages": "OTA-Pakete", + "list-of-ota-packages": "{ count, plural, =1 {Ein OTA-Paket} other {Liste von # OTA-Paketen} }", "type-rpc": "RPC", "type-queue": "Warteschlange", "type-queue-stats": "Warteschlangenstatistiken", @@ -2933,11 +3044,12 @@ "duplicate-filter": "Ein Filter mit demselben Namen existiert bereits.", "filters": "Filter", "unable-delete-filter-title": "Filter kann nicht gelöscht werden", - "unable-delete-filter-text": "Filter '{{filter}}' kann nicht gelöscht werden, da er von folgendem/n Widget(s) verwendet wird:
    {{widgetsList}}", - "duplicate-filter-error": "Doppelter Filter gefunden '{{filter}}'.
    Filter müssen innerhalb des Dashboards eindeutig sein.", - "missing-key-filters-error": "Schlüsselfilter fehlen für Filter '{{filter}}'.", + "unable-delete-filter-text": "Der Filter '{{filter}}' kann nicht gelöscht werden, da er von folgenden Widget(s) verwendet wird:
    {{widgetsList}}", + "duplicate-filter-error": "Doppelter Filter gefunden '{{filter}}'.
    Filter müssen innerhalb des Dashboards eindeutig sein.", + "missing-key-filters-error": "Schlüsselfilter fehlt für Filter '{{filter}}'.", "filter": "Filter", "editable": "Bearbeitbar", + "editable-hint": "Benutzern erlauben, den Filterwert in Dashboards zu ändern.", "no-filters-found": "Keine Filter gefunden.", "no-filter-text": "Kein Filter angegeben", "add-filter-prompt": "Bitte Filter hinzufügen", @@ -2977,6 +3089,8 @@ "filter-user-params": "Filterprädikat-Benutzerparameter", "user-parameters": "Benutzerparameter", "display-label": "Anzeigebezeichnung", + "custom-label": "Benutzerdefiniertes Label", + "custom-label-hint": "Aktivieren Sie diese Option, um ein eigenes Label für den Filter festzulegen. Wenn deaktiviert, wird automatisch ein Label generiert.", "order-priority": "Priorität der Feldreihenfolge", "key-filter": "Schlüsselfilter", "key-filters": "Schlüsselfilter", @@ -3021,7 +3135,8 @@ "switch-to-dynamic-value": "Auf dynamischen Wert umschalten", "switch-to-default-value": "Auf Standardwert umschalten", "inherit-owner": "Vom Eigentümer übernehmen", - "source-attribute-not-set": "Falls Quellattribut nicht gesetzt ist" + "source-attribute-not-set": "Falls Quellattribut nicht gesetzt ist", + "unit": "Einheit" }, "fullscreen": { "expand": "Auf Vollbildmodus erweitern", @@ -3406,6 +3521,7 @@ "power-button-background": "Hintergrund des Netzschalters", "value-box-background": "Hintergrund der Wertbox", "value-units": "Werteinheiten", + "enable-units-scale": "Einheiten auf Skala aktivieren", "filtration-mode": "Filtrationsmodus", "filtration-mode-hint": "Ganzzahlige Angabe des aktuellen Filtrationsmodus.", "filtration-mode-update": "Filtrationsmodus-Aktualisierung", @@ -3722,6 +3838,8 @@ "mobile-package-max-length": "Anwendungspaket sollte weniger als 256 Zeichen enthalten", "mobile-package-required": "Anwendungspaket ist erforderlich.", "mobile-package-pattern": "Ungültiges Format des Anwendungspakets", + "mobile-package-title": "Anwendungstitel", + "mobile-package-title-max-length": "Der Anwendungstitel sollte weniger als 256 Zeichen umfassen", "no-application": "Keine Anwendungen gefunden", "no-bundles": "Keine Bundles gefunden", "platform-type": "Plattformtyp", @@ -3802,20 +3920,16 @@ "configuration-app": "Konfigurations-App", "configuration-step": { "prepare-environment-title": "Entwicklungsumgebung vorbereiten", - "prepare-environment-text": "Für die ThingsBoard Flutter Mobile App wird das Flutter SDK benötigt. Folgen Sie den Anweisungen zur Einrichtung des Flutter SDK.", - "get-source-code-title": "Quellcode der App erhalten", - "get-source-code-text": "Sie können den Quellcode der ThingsBoard Flutter Mobile App durch Klonen des GitHub-Repositories erhalten:", - "configure-api-title": "ThingsBoard API-Endpunkt konfigurieren", - "configure-api-text": "Öffnen Sie das Projekt 'flutter_thingsboard_pe_app' in Ihrem Editor/IDE. Bearbeiten Sie:", - "configure-api-hint": "Setzen Sie den Wert der Konstante 'thingsBoardApiEndpoint' entsprechend dem API-Endpunkt Ihrer ThingsBoard-Instanz. Verwenden Sie keine Hostnamen wie „localhost“ oder „127.0.0.1“.", + "prepare-environment-text": "Die Flutter ThingsBoard Mobile App erfordert das Flutter SDK. Befolgen Sie die Anweisungen zur Einrichtung des Flutter SDK.", + "get-source-code-title": "App-Quellcode beziehen", + "get-source-code-text": "Sie können den Quellcode der Flutter ThingsBoard Mobile App durch Klonen aus dem GitHub-Repository beziehen:", + "configure-app-settings-title": "App-Einstellungen konfigurieren", + "configure-app-settings-text": "Laden Sie die Konfigurationsdatei herunter und platzieren Sie sie im Stammverzeichnis des Projekts, das Sie im vorherigen Schritt geklont haben.", + "download-file": "Datei herunterladen", "run-app-title": "App ausführen", - "run-app-text": "Führen Sie die App wie in Ihrer IDE beschrieben aus.\nWenn Sie das Terminal verwenden, führen Sie folgenden Befehl aus:", - "more-information": "Detaillierte Informationen finden Sie in unserer Einstiegsdokumentation.", - "getting-started": "Erste Schritte", - "configure-package-title": "Anwendungspaket konfigurieren", - "configure-package-text": "Sie können das Anwendungspaket manuell ändern oder ein CLI-Tool eines Drittanbieters verwenden.", - "configure-package-text-install": "Um das Rename CLI Tool zu installieren, führen Sie folgenden Befehl aus:", - "configure-package-run-commands": "Führen Sie diese Befehle im Stammverzeichnis Ihres Projekts aus:" + "run-app-text": "Führen Sie die App wie in Ihrer IDE beschrieben aus.\nWenn Sie das Terminal verwenden, führen Sie die App mit folgendem Befehl aus:", + "more-information": "Detaillierte Informationen finden Sie in unserer Erste-Schritte-Dokumentation.", + "getting-started": "Erste Schritte" } }, "notification": { @@ -3839,6 +3953,7 @@ "new-platform-version-trigger-settings": "Neue Plattformversion Auslöser-Einstellungen", "rate-limits-trigger-settings": "Auslöser für überschrittene Ratenlimits", "task-processing-failure-trigger-settings": "Aufgabenverarbeitungsfehler Auslöser-Einstellungen", + "resources-shortage-trigger-settings": "Auslöseeinstellungen bei Ressourcenknappheit", "at-least-one-should-be-selected": "Mindestens eine Auswahl muss getroffen werden", "basic-settings": "Grundeinstellungen", "button-text": "Schaltflächentext", @@ -3853,6 +3968,7 @@ "create-new": "Neu erstellen", "created": "Erstellt", "customize-messages": "Nachrichten anpassen", + "cpu-threshold": "CPU-Schwellenwert", "delete-notification-text": "Seien Sie vorsichtig, nach der Bestätigung ist die Benachrichtigung nicht wiederherstellbar.", "delete-notification-title": "Sind Sie sicher, dass Sie die Benachrichtigung löschen möchten?", "delete-notifications-text": "Seien Sie vorsichtig, nach der Bestätigung sind die Benachrichtigungen nicht wiederherstellbar.", @@ -3919,6 +4035,7 @@ "input-fields-support-templatization": "Eingabefelder unterstützen Templatisierung.", "link": "Link", "link-required": "Link ist erforderlich", + "link-max-length": "Der Link darf maximal {{ length }} Zeichen lang sein", "link-type": { "dashboard": "Dashboard öffnen", "link": "URL-Link öffnen" @@ -3945,6 +4062,7 @@ "no-severity-found": "Keine Schwere gefunden", "no-severity-matching": "'{{severity}}' nicht gefunden.", "no-template-matching": "Keine Ressource passend zu '{{template}}' gefunden.", + "create-new-template": "Erstellen Sie eine neue!", "not-found-slack-recipient": "Slack-Empfänger nicht gefunden", "notification": "Benachrichtigung", "notification-center": "Benachrichtigungszentrale", @@ -3968,6 +4086,7 @@ "only-rule-chain-lifecycle-failures": "Nur Lebenszyklusfehler von Regelketten", "only-rule-node-lifecycle-failures": "Nur Lebenszyklusfehler von Regelknoten", "platform-users": "Plattformbenutzer", + "ram-threshold": "RAM-Schwellenwert", "rate-limits": "Ratenbegrenzungen", "rate-limits-hint": "Wenn das Feld leer ist, wird der Auslöser auf alle Ratenbegrenzungen angewendet", "recipient": "Empfänger", @@ -4033,6 +4152,7 @@ "start-from-scratch": "Von vorne beginnen", "status": "Status", "stop-escalation-alarm-status-become": "Eskalation beenden, wenn Alarmstatus wird zu:", + "storage-threshold": "Speicher-Schwellenwert", "subject": "Betreff", "subject-required": "Betreff ist erforderlich", "subject-max-length": "Betreff sollte höchstens {{ length }} Zeichen lang sein", @@ -4048,13 +4168,14 @@ "entities-limit": "Entitätenlimit", "entity-action": "Entitätsaktion", "general": "Allgemein", - "rule-engine-lifecycle-event": "Regelmaschinenlebenszyklusereignis", - "rule-node": "Regelknoten", + "rule-engine-lifecycle-event": "Lebenszyklusereignis der Rule Engine", + "rule-node": "Rule Node", "new-platform-version": "Neue Plattformversion", - "rate-limits": "Überschrittene Ratenlimits", + "rate-limits": "Rate-Limits überschritten", "edge-communication-failure": "Edge-Kommunikationsfehler", "edge-connection": "Edge-Verbindung", - "task-processing-failure": "Fehler bei der Aufgabenverarbeitung" + "task-processing-failure": "Fehler bei der Aufgabenverarbeitung", + "resources-shortage": "Ressourcenknappheit" }, "templates": "Vorlagen", "notification-templates": "Benachrichtigungen / Vorlagen", @@ -4072,12 +4193,13 @@ "device-activity": "Geräteaktivität", "entities-limit": "Entitätenlimit", "entity-action": "Entitätsaktion", - "rule-engine-lifecycle-event": "Regelmaschinenlebenszyklusereignis", + "rule-engine-lifecycle-event": "Lebenszyklusereignis der Rule Engine", "new-platform-version": "Neue Plattformversion", - "rate-limits": "Überschrittene Ratenlimits", + "rate-limits": "Rate-Limits überschritten", "edge-connection": "Edge-Verbindung", "edge-communication-failure": "Edge-Kommunikationsfehler", "task-processing-failure": "Fehler bei der Aufgabenverarbeitung", + "resources-shortage": "Ressourcenknappheit", "trigger": "Auslöser", "trigger-required": "Auslöser ist erforderlich" }, @@ -4119,6 +4241,7 @@ "checksum-copied-message": "Paket-Prüfsumme wurde in die Zwischenablage kopiert", "change-firmware": "Die Änderung der Firmware kann ein Update von { count, plural, =1 {1 Gerät} other {# Geräten} } verursachen.", "change-software": "Die Änderung der Software kann ein Update von { count, plural, =1 {1 Gerät} other {# Geräten} } verursachen.", + "change-ota-setting-title": "Sind Sie sicher, dass Sie die OTA-Einstellungen ändern möchten?", "chose-compatible-device-profile": "Das hochgeladene Paket ist nur für Geräte mit dem ausgewählten Profil verfügbar.", "chose-firmware-distributed-device": "Firmware auswählen, die auf die Geräte verteilt wird", "chose-software-distributed-device": "Software auswählen, die auf die Geräte verteilt wird", @@ -4314,6 +4437,7 @@ "add-relation-filter": "Beziehungsfilter hinzufügen", "any-relation": "Beliebige Beziehung", "relation-filters": "Beziehungsfilter", + "relation-filter": "Beziehungsfilter", "additional-info": "Zusätzliche Informationen (JSON)", "invalid-additional-info": "Zusätzliche Info-JSON kann nicht geparst werden.", "no-relations-text": "Keine Beziehungen gefunden", @@ -4635,7 +4759,7 @@ "copy-from": "Kopieren von", "data-to-metadata": "Daten zu Metadaten", "metadata-to-data": "Metadaten zu Daten", - "use-regular-expression-hint": "Verwenden Sie reguläre Ausdrücke zum Kopieren nach Muster.\n\nTipps:\nEnter = Eingabe abschließen\nBackspace = löschen\nMehrere Felder erlaubt.", + "use-regular-expression-hint": "Verwenden Sie reguläre Ausdrücke, um Schlüssel anhand eines Musters zu kopieren.\n\nTipps & Tricks:\nDrücken Sie 'Enter', um die Eingabe des Feldnamens abzuschließen.\nDrücken Sie 'Rücktaste', um den Feldnamen zu löschen. Mehrere Feldnamen werden unterstützt.", "interval": "Intervall", "interval-required": "Intervall ist erforderlich", "interval-hint": "Deduplizierungsintervall in Sekunden.", @@ -4889,9 +5013,9 @@ "connect-timeout-required": "Verbindungs-Timeout ist erforderlich.", "connect-timeout-range": "Verbindungs-Timeout muss im Bereich von 1 bis 200 liegen.", "client-id": "Client-ID", - "client-id-hint": "Optional. Leer lassen für automatisch generierte Client-ID. Vorsicht beim Festlegen der Client-ID: Die meisten MQTT-Broker erlauben keine mehrfachen Verbindungen mit derselben Client-ID. Um mit solchen Brokern zu verbinden, muss deine MQTT-Client-ID eindeutig sein. Wenn die Plattform im Microservice-Modus betrieben wird, wird eine Kopie des Regelknotens in jedem Microservice gestartet. Dies führt automatisch zu mehreren MQTT-Clients mit derselben ID und kann zu Fehlern führen. Um dies zu vermeiden, aktiviere die Option „Dienst-ID als Suffix zur Client-ID hinzufügen“ unten.", + "client-id-hint": "Optional. Leer lassen für automatisch generierte Client-ID. Vorsicht beim Festlegen der Client-ID: Die meisten MQTT-Broker erlauben keine mehrfachen Verbindungen mit derselben Client-ID. Um mit solchen Brokern zu verbinden, muss deine MQTT-Client-ID eindeutig sein. Wenn die Plattform im Microservice-Modus betrieben wird, wird eine Kopie des Regelknotens in jedem Microservice gestartet. Dies führt automatisch zu mehreren MQTT-Clients mit derselben ID und kann zu Fehlern führen. Um dies zu vermeiden, aktiviere die Option \"Dienst-ID als Suffix zur Client-ID hinzufügen\" unten.", "append-client-id-suffix": "Dienst-ID als Suffix zur Client-ID hinzufügen", - "client-id-suffix-hint": "Optional. Wird angewendet, wenn die „Client-ID“ explizit angegeben wurde. Falls ausgewählt, wird die Dienst-ID als Suffix zur Client-ID hinzugefügt. Hilft Fehler zu vermeiden, wenn die Plattform im Microservice-Modus läuft.", + "client-id-suffix-hint": "Optional. Wird angewendet, wenn die \"Client ID\" explizit angegeben ist. Falls ausgewählt, wird die Service-ID als Suffix zur Client-ID hinzugefügt. Dies hilft, Fehler zu vermeiden, wenn die Plattform im Microservices-Modus betrieben wird.", "device-id": "Geräte-ID", "device-id-required": "Geräte-ID ist erforderlich.", "clean-session": "Saubere Sitzung", @@ -5304,6 +5428,36 @@ "html-text-description": "Ermöglicht HTML-Tags zur Formatierung, für Links und Bilder im Nachrichtentext.", "dynamic-text-description": "Erlaubt die dynamische Verwendung von reinem Text oder HTML basierend auf der Templating-Funktion.", "after-template-evaluation-hint": "Nach der Template-Auswertung muss der Wert true für HTML und false für reinen Text sein." + }, + "ai": { + "ai-model": "KI-Modell", + "model": "Modell", + "ai-model-hint": "Wählen Sie das vorkonfigurierte KI-Modell zur Verarbeitung der von diesem Rule Node gesendeten Anfragen aus, oder verwenden Sie „Neu erstellen“, um ein neues zu konfigurieren.", + "prompt-settings": "Prompt-Einstellungen", + "prompt-settings-hint": "Der optionale System-Prompt definiert die allgemeine Rolle und Einschränkungen der KI, während der Benutzer-Prompt die spezifische Aufgabe beschreibt. Beide Felder unterstützen auch die Verwendung von Templates.", + "system-prompt": "System-Prompt", + "system-prompt-max-length": "Der System-Prompt darf maximal 500.000 Zeichen lang sein.", + "system-prompt-blank": "Der System-Prompt darf nicht leer sein.", + "user-prompt": "Benutzer-Prompt", + "user-prompt-required": "Benutzer-Prompt ist erforderlich.", + "user-prompt-max-length": "Der Benutzer-Prompt darf maximal 500.000 Zeichen lang sein.", + "user-prompt-blank": "Der Benutzer-Prompt darf nicht leer sein.", + "response-format": "Antwortformat", + "response-text": "Text", + "response-json": "JSON", + "response-json-schema": "JSON-Schema", + "response-format-hint-TEXT": "Erlaubt dem Modell, beliebigen Text zu generieren, der möglicherweise kein gültiges JSON-Objekt ist. Ist die Ausgabe kein gültiges JSON, wird sie automatisch in ein JSON-Objekt unter dem Schlüssel \"response\" eingebettet.", + "response-format-hint-JSON": "Das Modell muss eine gültige JSON-Antwort erzeugen. Ist die Ausgabe kein gültiges JSON-Objekt, wird sie automatisch unter dem Schlüssel \"response\" eingebettet.", + "response-format-hint-JSON_SCHEMA": "Das Modell muss ein JSON erzeugen, das der im bereitgestellten Schema definierten Struktur und den Datentypen entspricht. Ist das Ergebnis kein gültiges JSON-Objekt, wird es automatisch unter dem Schlüssel \"response\" eingebettet.", + "response-json-schema-hint": "Obwohl jedes gültige JSON-Schema eingegeben werden kann, unterstützt dieser Rule Node nur einen begrenzten Funktionsumfang. Details finden Sie in der Dokumentation des Nodes.", + "response-json-schema-required": "JSON-Schema ist erforderlich", + "advanced-settings": "Erweiterte Einstellungen", + "timeout": "Zeitüberschreitung", + "timeout-hint": "Maximale Zeit, die auf eine Antwort \nvom KI-Modell gewartet wird, bevor die Anfrage abgebrochen wird.", + "timeout-required": "Zeitüberschreitung ist erforderlich", + "timeout-validation": "Muss zwischen 1 Sekunde und 10 Minuten liegen.", + "force-acknowledgement": "Erzwinge Bestätigung", + "force-acknowledgement-hint": "Wenn aktiviert, wird die eingehende Nachricht sofort bestätigt. Die Antwort des Modells wird dann als separate, neue Nachricht eingereiht." } }, "timezone": { @@ -5625,7 +5779,10 @@ "too-small-value-zero": "Der Wert muss größer als 0 sein", "too-small-value-one": "Der Wert muss größer als 1 sein", "queue-size-is-limited-by-system-configuration": "Die Größe der Warteschlange ist auch durch die Systemkonfiguration begrenzt.", - "cassandra-tenant-limits-configuration": "Cassandra-Abfrage für Mieter", + "cassandra-write-tenant-core-limits-configuration": "REST-API Cassandra-Schreibabfragen", + "cassandra-read-tenant-core-limits-configuration": "REST-API- und WS-Telemetrie Cassandra-Leseabfragen", + "cassandra-write-tenant-rule-engine-limits-configuration": "Rule Engine Telemetrie Cassandra-Schreibabfragen", + "cassandra-read-tenant-rule-engine-limits-configuration": "Rule Engine-Telemetrie-Cassandra-Leseabfragen", "ws-limit-max-sessions-per-tenant": "Maximale Sitzungen pro Mieter", "ws-limit-max-sessions-per-customer": "Maximale Sitzungen pro Kunde", "ws-limit-max-sessions-per-regular-user": "Maximale Sitzungen pro normalem Benutzer", @@ -5638,6 +5795,7 @@ "ws-limit-updates-per-session": "WebSocket-Aktualisierungen pro Sitzung", "rate-limits": { "add-limit": "Limit hinzufügen", + "and-also-less-than": "und außerdem kleiner als", "advanced-settings": "Erweiterte Einstellungen", "edit-limit": "Limit bearbeiten", "calculated-field-debug-event-rate-limit": "Berechnete Feld-Debug-Ereignisse", @@ -5657,7 +5815,10 @@ "edit-tenant-rest-limits-title": "REST-Anfragelimits für Mieter bearbeiten", "edit-customer-rest-limits-title": "REST-Anfragelimits für Kunden bearbeiten", "edit-ws-limit-updates-per-session-title": "Limit für WebSocket-Aktualisierungen pro Sitzung bearbeiten", - "edit-cassandra-tenant-limits-configuration-title": "Cassandra-Abfrage-Limits für Mieter bearbeiten", + "edit-cassandra-write-tenant-core-limits-configuration": "REST-API Cassandra-Schreibabfragen bearbeiten", + "edit-cassandra-read-tenant-core-limits-configuration": "REST-API- und WS-Telemetrie Cassandra-Leseabfragen bearbeiten", + "edit-cassandra-write-tenant-rule-engine-limits-configuration": "Rule Engine Telemetrie Cassandra-Schreibabfragen bearbeiten", + "edit-cassandra-read-tenant-rule-engine-limits-configuration": "Rule Engine Telemetrie Cassandra-Leseabfragen bearbeiten", "edit-tenant-entity-export-rate-limit-title": "Limit für Entitätsversionserstellung bearbeiten", "edit-tenant-entity-import-rate-limit-title": "Limit für Entitätsversionsladevorgang bearbeiten", "edit-tenant-notification-request-rate-limit-title": "Limit für Benachrichtigungsanfragen bearbeiten", @@ -5679,6 +5840,7 @@ "per-seconds": "Pro Sekunde", "per-seconds-required": "Zeitintervall ist erforderlich.", "per-seconds-min": "Minimalwert ist 1.", + "per-seconds-duplicate": "Doppelte Zeitrate. Jeder Zeitintervall muss eindeutig sein.", "rate-limits": "Ratenlimits", "remove-limit": "Limit entfernen", "transport-tenant-msg": "Transport-Mieter-Nachrichten", @@ -5827,12 +5989,124 @@ "date": "Datum", "show-date-time-interval": "Datum/Zeit-Intervall anzeigen", "show-date-time-interval-hint": "Datum/Zeit-Intervall gemäß Datenaggregation anzeigen.", + "hide-zero-tooltip-values": "Nullwerte ausblenden", "background-color": "Hintergrundfarbe", "background-blur": "Hintergrundunschärfe" }, "unit": { + "set-unit-conversion": "Einheitenumrechnung festlegen", + "unit-settings": { + "unit-settings": "Einheitseinstellungen", + "source-unit": "Ausgangseinheit", + "source-unit-hint": "Dies ist die Einheit des gespeicherten Wertes. Die Einheit, aus der konvertiert wird. Geben Sie das Symbol ein, das Ihre Quelldaten verwenden (z. B. m, km, ft, in).", + "target-metric-unit": "Ziel-Metrikeinheit", + "target-metric-unit-hint": "Wählen Sie die Metrikeinheit (SI), in die Ihr Quellwert umgerechnet werden soll (z. B. cm, mm, km).", + "target-imperial-unit": "Ziel-Imperialeinheit", + "target-imperial-unit-hint": "Wählen Sie die Imperialeinheit, in die Ihr Quellwert umgerechnet werden soll (z. B. in, ft, yd).", + "target-hybrid-unit": "Ziel-Hybrideinheit", + "target-hybrid-unit-hint": "Wählen Sie die Hybrideinheit, in die Ihr Quellwert umgerechnet werden soll (z. B. cm, in, km). Hybride Einheiten kombinieren metrische oder imperiale Einheiten.", + "enable-unit-conversion": "Einheitenumrechnung aktivieren", + "enable-unit-conversion-hint": "Aktivieren Sie diese Option, um die Umrechnung zu aktivieren. Wenn deaktiviert, wird der Quellwert unverändert übernommen. Deaktiviert, wenn es in der entsprechenden Messeinheitsgruppe nur eine Einheit gibt (z. B. Lichtstrom, AQI)." + }, + "unit-system": "Einheitensystem", + "unit-system-type": { + "AUTO": "Automatisch", + "METRIC": "Metrisch", + "IMPERIAL": "Imperial", + "HYBRID": "Hybrid" + }, + "measures": { + "absorbed-dose-rate": "Absorptionsdosisrate", + "acceleration": "Beschleunigung", + "acidity": "Säuregrad", + "air-quality-index": "Luftqualitätsindex", + "amount-of-substance": "Stoffmenge", + "angle": "Winkel", + "angular-acceleration": "Winkelbeschleunigung", + "area": "Fläche", + "area-density": "Flächendichte", + "capacitance": "Kapazität", + "catalytic-activity": "Katalytische Aktivität", + "catalytic-concentration": "Katalytische Konzentration", + "charge": "Ladung", + "current-density": "Stromdichte", + "data-transfer-rate": "Datenübertragungsrate", + "density": "Dichte", + "digital": "Digital", + "dimension-ratio": "Maßverhältnis", + "dynamic-viscosity": "Dynamische Viskosität", + "earthquake-magnitude": "Erdbebenstärke", + "electric-charge-density": "Elektrische Ladungsdichte", + "electric-current": "Elektrischer Strom", + "electric-dipole-moment": "Elektrisches Dipolmoment", + "electric-field-strength": "Elektrische Feldstärke", + "electric-flux": "Elektrischer Fluss", + "electric-permittivity": "Elektrische Permittivität", + "electric-polarizability": "Elektrische Polarisierbarkeit", + "electrical-conductance": "Elektrische Leitfähigkeit", + "electrical-conductivity": "Elektrische Leitfähigkeit", + "energy": "Energie", + "energy-density": "Energiedichte", + "force": "Kraft", + "frequency": "Frequenz", + "fuel-efficiency": "Kraftstoffeffizienz", + "heat-capacity": "Wärmekapazität", + "illuminance": "Beleuchtungsstärke", + "inductance": "Induktivität", + "kinematic-viscosity": "Kinematische Viskosität", + "length": "Länge", + "light-exposure": "Lichtexposition", + "linear-charge-density": "Lineare Ladungsdichte", + "logarithmic-ratio": "Logarithmisches Verhältnis", + "luminous-efficacy": "Lichtausbeute", + "luminous-flux": "Lichtstrom", + "luminous-intensity": "Lichtstärke", + "magnetic-field-gradient": "Magnetfeldgradient", + "magnetic-flux": "Magnetischer Fluss", + "magnetic-flux-density": "Magnetische Flussdichte", + "magnetic-moment": "Magnetisches Moment", + "magnetic-permeability": "Magnetische Permeabilität", + "mass": "Masse", + "mass-fraction": "Massenanteil", + "molar-concentration": "Molare Konzentration", + "molar-energy": "Molare Energie", + "molar-heat-capacity": "Molare Wärmekapazität", + "molar-mass": "Molmasse", + "number-concentration": "Teilchenkonzentration", + "parts-per-million": "Teile pro Million", + "power": "Leistung", + "power-density": "Leistungsdichte", + "pressure": "Druck", + "radiance": "Strahldichte", + "radiant-intensity": "Strahlungsintensität", + "radiation-dose": "Strahlendosis", + "radioactive-decay": "Radioaktiver Zerfall", + "radioactivity": "Radioaktivität", + "radioactivity-concentration": "Radioaktivitätskonzentration", + "reciprocal-length": "Reziproke Länge", + "resistance": "Widerstand", + "reynolds-number": "Reynolds-Zahl", + "signal-level": "Signalpegel", + "solid-angle": "Raumwinkel", + "specific-energy": "Spezifische Energie", + "specific-heat-capacity": "Spezifische Wärmekapazität", + "specific-humidity": "Spezifische Luftfeuchtigkeit", + "specific-volume": "Spezifisches Volumen", + "speed": "Geschwindigkeit", + "surface-charge-density": "Oberflächenladungsdichte", + "surface-tension": "Oberflächenspannung", + "temperature": "Temperatur", + "thermal-conductivity": "Wärmeleitfähigkeit", + "time": "Zeit", + "torque": "Drehmoment", + "turbidity": "Trübung", + "voltage": "Spannung", + "volume": "Volumen", + "volume-flow": "Volumenstrom" + }, "millimeter": "Millimeter", "centimeter": "Zentimeter", + "decimeter": "Dezimeter", "angstrom": "Angström", "nanometer": "Nanometer", "micrometer": "Mikrometer", @@ -5840,6 +6114,7 @@ "kilometer": "Kilometer", "inch": "Zoll", "foot": "Fuß", + "foot-us": "Fuß (US-Vermessung)", "yard": "Yard", "mile": "Meile", "nautical-mile": "Seemeile", @@ -5849,14 +6124,14 @@ "steradian": "Steradiant", "thou": "Thou", "barleycorn": "Gerstenkorn", - "hand": "Handbreit", + "hand": "Handbreite", "chain": "Kette", "furlong": "Furlong", - "league": "Leuge", + "league": "Legua", "fathom": "Faden", "cable": "Kabellänge", "link": "Glied", - "rod": "Rute", + "rod": "Stange", "nanogram": "Nanogramm", "microgram": "Mikrogramm", "milligram": "Milligramm", @@ -5866,12 +6141,12 @@ "ounce": "Unze", "pound": "Pfund", "stone": "Stone", - "hundredweight-count": "Zentner", - "short-tons": "US-Tonnen", + "hundredweight-count": "Zentner (US)", + "short-tons": "Kurztonnen", "dalton": "Dalton", - "grain": "Grain", + "grain": "Korn", "drachm": "Drachme", - "quarter": "Quarter", + "quarter": "Viertelzentner", "slug": "Slug", "carat": "Karat", "cubic-millimeter": "Kubikmillimeter", @@ -5886,6 +6161,7 @@ "cubic-foot": "Kubikfuß", "cubic-yard": "Kubikyard", "fluid-ounce": "Flüssigunze", + "fluid-ounce-per-second": "Flüssigunze pro Sekunde", "pint": "Pint", "quart": "Quart", "gallon": "Gallone", @@ -5904,9 +6180,13 @@ "meter-per-second": "Meter pro Sekunde", "kilometer-per-hour": "Kilometer pro Stunde", "foot-per-second": "Fuß pro Sekunde", + "foot-per-minute": "Fuß pro Minute", "mile-per-hour": "Meile pro Stunde", "knot": "Knoten", + "inch-per-second": "Zoll pro Sekunde", + "inch-per-hour": "Zoll pro Stunde", "millimeters-per-minute": "Millimeter pro Minute", + "meter-per-minute": "Meter pro Minute", "kilometer-per-hour-squared": "Kilometer pro Stunde zum Quadrat", "foot-per-second-squared": "Fuß pro Sekunde zum Quadrat", "pascal": "Pascal", @@ -5918,11 +6198,12 @@ "kilobar": "Kilobar", "newton": "Newton", "newton-meter": "Newtonmeter", - "foot-pounds": "Fuß-Pfund", - "inch-pounds": "Zoll-Pfund", + "foot-pounds": "Fußpfund", + "inch-pounds": "Zollpfund", "newton-per-meter": "Newton pro Meter", "atmospheres": "Atmosphären", "pounds-per-square-inch": "Pfund pro Quadratzoll", + "kilopound-per-square-inch": "Kilopfund pro Quadratzoll", "torr": "Torr", "inches-of-mercury": "Zoll Quecksilbersäule", "pascal-per-square-meter": "Pascal pro Quadratmeter", @@ -5940,11 +6221,17 @@ "megajoule": "Megajoule", "gigajoule": "Gigajoule", "watt-hour": "Wattstunde", + "watt-minute": "Wattminute", "kilowatt-hour": "Kilowattstunde", + "milliwatt-hour": "Milliwattstunde", + "megawatt-hour": "Megawattstunde", + "gigawatt-hour": "Gigawattstunde", "electron-volts": "Elektronenvolt", "joules-per-coulomb": "Joule pro Coulomb", "british-thermal-unit": "Britische Wärmeeinheit (BTU)", - "foot-pound": "Fuß-Pfund", + "thousand-british-thermal-unit": "Tausend BTU", + "million-british-thermal-unit": "Million BTU", + "foot-pound": "Fußpfund", "calorie": "Kalorie", "small-calorie": "Kleine Kalorie", "kilocalorie": "Kilokalorie", @@ -5953,7 +6240,7 @@ "joule-per-kilogram": "Joule pro Kilogramm", "watt-per-meter-kelvin": "Watt pro Meter-Kelvin", "joule-per-cubic-meter": "Joule pro Kubikmeter", - "therm": "Therm", + "therm": "Therme", "electric-dipole-moment": "Elektrisches Dipolmoment", "magnetic-dipole-moment": "Magnetisches Dipolmoment", "debye": "Debye", @@ -5975,9 +6262,19 @@ "kilowatt-per-square-inch": "Kilowatt pro Quadratzoll", "horsepower": "Pferdestärke", "btu-per-hour": "BTU pro Stunde", + "btu-per-second": "BTU pro Sekunde", + "btu-per-day": "BTU pro Tag", + "mbtu-per-hour": "Tausend BTU pro Stunde", + "mbtu-per-second": "Tausend BTU pro Sekunde", + "mbtu-per-day": "Tausend BTU pro Tag", + "mmbtu-per-hour": "Million BTU pro Stunde", + "mmbtu-per-second": "Million BTU pro Sekunde", + "mmbtu-per-day": "Million BTU pro Tag", + "foot-pound-per-second": "Fußpfund pro Sekunde", "coulomb": "Coulomb", "millicoulomb": "Millicoulomb", "microcoulomb": "Mikrocoulomb", + "nanocoulomb": "Nanocoulomb", "picocoulomb": "Picocoulomb", "coulomb-per-meter": "Coulomb pro Meter", "coulomb-per-cubic-meter": "Coulomb pro Kubikmeter", @@ -5995,25 +6292,32 @@ "are": "Ar", "barn": "Barn", "circular-inch": "Kreiszoll", - "milliampere-hour": "Milliampere-Stunde", + "milliampere-hour": "Milliamperestunde", "ampere-hours": "Amperestunden", "kiloampere-hours": "Kiloamperestunden", "nanoampere": "Nanoampere", - "picoampere": "Picoampere", + "picoampere": "Pikoampere", "microampere": "Mikroampere", "milliampere": "Milliampere", "ampere": "Ampere", + "kiloampere": "Kiloampere", + "megaampere": "Megaampere", + "gigaampere": "Gigaampere", "microampere-per-square-centimeter": "Mikroampere pro Quadratzentimeter", "ampere-per-square-meter": "Ampere pro Quadratmeter", "ampere-per-meter": "Ampere pro Meter", "oersted": "Oersted", - "bohr-magneton": "Bohrsche Magneton", + "bohr-magneton": "Bohrsches Magneton", "ampere-meter-squared": "Ampere-Quadratmeter", "nanovolt": "Nanovolt", - "picovolt": "Picovolt", + "picovolt": "Pikovolt", + "millivolt": "Millivolt", + "microvolt": "Mikrovolt", "volt": "Volt", - "dbmV": "dBmV", - "dbm": "dBm", + "kilovolt": "Kilovolt", + "megavolt": "Megavolt", + "dbmV": "Dezibel-Volt", + "dbm": "Dezibel-Milliwatt", "volt-meter": "Volt-Meter", "kilovolt-meter": "Kilovolt-Meter", "megavolt-meter": "Megavolt-Meter", @@ -6026,10 +6330,12 @@ "kilohm": "Kiloohm", "megohm": "Megaohm", "gigohm": "Gigaohm", + "millihertz": "Millihertz", "hertz": "Hertz", "kilohertz": "Kilohertz", "megahertz": "Megahertz", "gigahertz": "Gigahertz", + "terahertz": "Terahertz", "rpm": "Umdrehungen pro Minute", "candela-per-square-meter": "Candela pro Quadratmeter", "candela": "Candela", @@ -6046,13 +6352,13 @@ "millimole": "Millimol", "kilomole": "Kilomol", "mole-per-cubic-meter": "Mol pro Kubikmeter", - "rssi": "RSSI", - "ppm": "Teile pro Million", - "ppb": "Teile pro Milliarde", + "rssi": "Signalstärkeindikator (RSSI)", + "ppm": "Teile pro Million (ppm)", + "ppb": "Teile pro Milliarde (ppb)", "micrograms-per-cubic-meter": "Mikrogramm pro Kubikmeter", "aqi": "Luftqualitätsindex (AQI)", "gram-per-cubic-meter": "Gramm pro Kubikmeter", - "gram-per-kilogram": "Spezifische Feuchtigkeit", + "gram-per-kilogram": "Spezifische Luftfeuchtigkeit", "millimeters-per-second": "Millimeter pro Sekunde", "neper": "Neper", "bel": "Bel", @@ -6103,18 +6409,21 @@ "g-force": "g-Kraft", "kilonewton": "Kilonewton", "kilogram-force": "Kilopond", - "pound-force": "Pfundkraft", - "kilopound-force": "Kilopfundkraft", + "pound-force": "Pfund-Kraft", + "kilopound-force": "Kilopfund-Kraft", "dyne": "Dyne", "poundal": "Poundal", "kip": "Kip", "gal": "Gal", - "gravity": "Schwerkraft", + "gravity": "Gravitation", "hectopascal": "Hektopascal", "atmosphere": "Atmosphäre", "millibars": "Millibar", - "inch-of-mercury": "Zoll Quecksilbersäule", + "inch-of-mercury": "Zoll Quecksilber", "richter-scale": "Richterskala", + "nanosecond": "Nanosekunde", + "microsecond": "Mikrosekunde", + "millisecond": "Millisekunde", "second": "Sekunde", "minute": "Minute", "hour": "Stunde", @@ -6130,6 +6439,7 @@ "gallons-per-minute": "Gallonen pro Minute", "cubic-foot-per-second": "Kubikfuß pro Sekunde", "milliliters-per-minute": "Milliliter pro Minute", + "cubic-decimeter-per-second": "Kubikdezimeter pro Sekunde", "bit": "Bit", "byte": "Byte", "kilobyte": "Kilobyte", @@ -6152,13 +6462,16 @@ "degree": "Grad", "radian": "Radiant", "gradian": "Gon", + "arcminute": "Bogenminute", + "arcsecond": "Bogensekunde", + "milliradian": "Milliradiant", "revolution": "Umdrehung", "siemens": "Siemens", "millisiemens": "Millisimens", - "microsiemens": "Mikrosiemens", - "kilosiemens": "Kilosiemens", - "megasiemens": "Megasiemens", - "gigasiemens": "Gigasiemens", + "microsiemens": "Mikrosimens", + "kilosiemens": "Kilosimens", + "megasiemens": "Megasimens", + "gigasiemens": "Gigasimens", "farad": "Farad", "millifarad": "Millifarad", "microfarad": "Mikrofarad", @@ -6182,17 +6495,17 @@ "lambda": "Lambda", "square-meter-per-second": "Quadratmeter pro Sekunde", "square-centimeter-per-second": "Quadratzentimeter pro Sekunde", - "stoke": "Stoke", + "stoke": "Stokes", "centistokes": "Zentistokes", "square-foot-per-second": "Quadratfuß pro Sekunde", "square-inch-per-second": "Quadratzoll pro Sekunde", "pascal-second": "Pascal-Sekunde", "centipoise": "Zentipoise", "poise": "Poise", - "reynolds": "Reynolds", + "reynolds": "Reynolds-Zahl", "pound-per-foot-hour": "Pfund pro Fuß-Stunde", - "newton-second-per-square-meter": "Newtonsekunde pro Quadratmeter", - "dyne-second-per-square-centimeter": "Dynesekunde pro Quadratzentimeter", + "newton-second-per-square-meter": "Newton-Sekunde pro Quadratmeter", + "dyne-second-per-square-centimeter": "Dyne-Sekunde pro Quadratzentimeter", "kilogram-per-meter-second": "Kilogramm pro Meter-Sekunde", "tesla-square-meters": "Tesla-Quadratmeter", "maxwell": "Maxwell", @@ -6220,11 +6533,13 @@ "kilovolts-per-meter": "Kilovolt pro Meter", "radian-per-second": "Radiant pro Sekunde", "radian-per-second-squared": "Radiant pro Sekunde zum Quadrat", - "revolutions-per-minute-per-second": "Drehbeschleunigung", + "revolutions-per-minute-per-second": "Winkelbeschleunigung", "deg-per-second": "Grad pro Sekunde", + "rotation-per-minute": "Umdrehungen pro Minute", "degrees-brix": "Grad Brix", "katal": "Katal", - "katal-per-cubic-metre": "Katal pro Kubikmeter" + "katal-per-cubic-metre": "Katal pro Kubikmeter", + "paris-inch": "Pariser Zoll" }, "user": { "user": "Benutzer", @@ -6707,9 +7022,9 @@ "advanced-settings": "Erweiterte Einstellungen", "data-settings": "Daten-Einstellungen", "limits": "Grenzwerte", - "no-data-display-message": "Alternative Nachricht bei fehlenden Daten", - "data-page-size": "Maximale Anzahl von Entitäten pro Datenquelle", - "settings-component-not-found": "Einstellungsformular-Komponente für Selector '{{selector}}' nicht gefunden", + "no-data-display-message": "Alternative Meldung für \"Keine Daten zur Anzeige\"", + "data-page-size": "Maximale Entitäten pro Datenquelle", + "settings-component-not-found": "Einstellungsformular-Komponente für Selektor '{{selector}}' nicht gefunden", "preview": "Vorschau", "set": "Festlegen", "set-message": "Nachricht festlegen", @@ -7774,6 +8089,18 @@ "fill-area-opacity": "Füllbereichsdeckkraft", "range-chart-style": "Stil des Bereichsdiagramms" }, + "knob": { + "behavior": "Verhalten", + "initial-value": "Anfangswert", + "initial-value-hint": "Aktion zum Abrufen des Anfangswerts des Reglers.", + "on-value-change": "Beim Wertwechsel", + "on-value-change-hint": "Aktion, die ausgelöst wird, wenn der Reglerwert geändert wird.", + "range": "Bereich", + "min": "min", + "max": "max", + "value": "Wert", + "fallback-initial-value": "Ausweich-Anfangswert" + }, "rpc": { "value-settings": "Werteinstellungen", "initial-value": "Anfangswert", @@ -7830,9 +8157,7 @@ "led-status-value-timeseries": "Zeitreihe des Geräts mit LED-Statuswert", "check-status-method": "RPC-Methode zur Geräteprüfung", "parse-led-status-value-function": "Funktion zum Parsen des LED-Statuswerts", - "knob-title": "Drehregler-Titel", - "min-value": "Minimalwert", - "max-value": "Maximalwert" + "knob-title": "Drehregler-Titel" }, "maps": { "map-type": { @@ -8676,18 +9001,22 @@ "pie-chart-card-style": "Kreisdiagramm-Kartenstil" }, "radar-chart": { - "radar-appearance": "Radar-Diagramm", + "radar-appearance": "Radar-Darstellung", "shape": "Form", "shape-polygon": "Polygon", "shape-circle": "Kreis", "color": "Farbe", "line": "Linie", "points": "Punkte", - "points-label": "Punktebezeichnung", + "points-label": "Punktebeschriftung", "radar-axis": "Radar-Achse", "axis-label": "Achsenbeschriftung", - "ticks-label": "Skalenbeschriftung", - "radar-chart-style": "Radar-Diagrammstil" + "ticks-label": "Teilstrichbeschriftung", + "radar-chart-style": "Radar-Diagramm-Stil", + "max-axes-scaling": "Maximale Achsenskalierung", + "max-axes-scaling-hint": "Wählen Sie, ob jede Radarachse ihren eigenen Maximalwert hat (Separat) oder ob alle Achsen den höchsten Wert aus dem Widget-Datensatz gemeinsam nutzen (Gemeinsam).", + "separate": "Separat", + "common": "Gemeinsam" }, "time-series-chart": { "chart": "Diagramm", diff --git a/ui-ngx/src/assets/locale/locale.constant-el_GR.json b/ui-ngx/src/assets/locale/locale.constant-el_GR.json index 151dbeb528..4af644ab8d 100644 --- a/ui-ngx/src/assets/locale/locale.constant-el_GR.json +++ b/ui-ngx/src/assets/locale/locale.constant-el_GR.json @@ -121,11 +121,11 @@ "mqtts": "MQTTs", "coap": "COAP", "coaps": "COAPs", - "hint": "Αν τα πεδία κεντρικού υπολογιστή ή θύρας είναι κενά, θα χρησιμοποιηθεί η προεπιλεγμένη τιμή πρωτοκόλλου.", - "host": "Κεντρικός υπολογιστής", - "port": "Θύρα", - "port-pattern": "Η θύρα πρέπει να είναι θετικός ακέραιος αριθμός.", - "port-range": "Η θύρα πρέπει να είναι στην περιοχή από 1 έως 65535." + "hint": "Αν τα πεδία του υπολογιστή ή της θύρας είναι κενά, θα χρησιμοποιηθεί η προεπιλεγμένη τιμή του πρωτοκόλλου.", + "host": "Υπολογιστής (Host)", + "port": "Θύρα (Port)", + "port-pattern": "Η θύρα πρέπει να είναι ένας θετικός ακέραιος αριθμός.", + "port-range": "Η θύρα πρέπει να είναι εντός του εύρους από 1 έως 65535." }, "mail-from": "Από διεύθυνση email", "mail-from-required": "Η από διεύθυνση email είναι υποχρεωτική.", @@ -540,12 +540,18 @@ "resources": "Πόροι", "notifications": "Ειδοποιήσεις", "notifications-settings": "Ρυθμίσεις ειδοποιήσεων", - "slack-api-token": "Slack API διακριτικό", + "slack-api-token": "Slack API token", "slack": "Slack", "slack-settings": "Ρυθμίσεις Slack", "mobile-settings": "Ρυθμίσεις κινητού", "firebase-service-account-file": "Αρχείο διαπιστευτηρίων λογαριασμού υπηρεσίας Firebase (JSON)", - "select-firebase-service-account-file": "Σύρετε και αποθέστε το αρχείο διαπιστευτηρίων Firebase ή " + "select-firebase-service-account-file": "Σύρετε και αποθέστε το αρχείο διαπιστευτηρίων λογαριασμού υπηρεσίας Firebase ή ", + "trendz": "Trendz", + "trendz-settings": "Ρυθμίσεις Trendz", + "trendz-url": "Διεύθυνση URL Trendz", + "trendz-url-required": "Απαιτείται η διεύθυνση URL του Trendz", + "trendz-api-key": "API κλειδί Trendz", + "trendz-enable": "Ενεργοποίηση Trendz" }, "alarm": { "alarm": "Συναγερμός", @@ -678,7 +684,7 @@ "filter-type-entity-name": "Όνομα οντότητας", "filter-type-entity-type": "Τύπος οντότητας", "filter-type-state-entity": "Οντότητα από την κατάσταση του πίνακα ελέγχου", - "filter-type-state-entity-description": "Οντότητα από παραμέτρους κατάστασης πίνακα ελέγχου", + "filter-type-state-entity-description": "Οντότητα που λαμβάνεται από τις παραμέτρους κατάστασης του πίνακα ελέγχου", "filter-type-asset-type": "Τύπος περιουσιακού στοιχείου", "filter-type-asset-type-description": "Περιουσιακά στοιχεία τύπου '{{assetTypes}}'", "filter-type-asset-type-and-name-description": "Περιουσιακά στοιχεία τύπου '{{assetTypes}}' και με όνομα που ξεκινά με '{{prefix}}'", @@ -709,12 +715,13 @@ "filter-type-required": "Ο τύπος φίλτρου είναι υποχρεωτικός.", "entity-filter-no-entity-matched": "Δεν βρέθηκαν οντότητες που να ταιριάζουν με το καθορισμένο φίλτρο.", "no-entity-filter-specified": "Δεν έχει καθοριστεί φίλτρο οντοτήτων", - "root-state-entity": "Χρήση οντότητας κατάστασης πίνακα ελέγχου ως ριζική", - "last-level-relation": "Ανάκτηση μόνο σχέσης τελευταίου επιπέδου", - "root-entity": "Ριζική οντότητα", + "root-state-entity": "Χρήση της οντότητας κατάστασης του πίνακα ελέγχου ως ρίζα", + "last-level-relation": "Ανάκτηση μόνο της τελευταίας σχέσης επιπέδου", + "root-entity": "Οντότητα ρίζας", "state-entity-parameter-name": "Όνομα παραμέτρου οντότητας κατάστασης", "default-state-entity": "Προεπιλεγμένη οντότητα κατάστασης", - "default-entity-parameter-name": "Προεπιλογή", + "default-entity-parameter-name": "Προεπιλεγμένο", + "query-options": "Επιλογές ερωτήματος", "max-relation-level": "Μέγιστο επίπεδο σχέσης", "unlimited-level": "Απεριόριστο επίπεδο", "state-entity": "Οντότητα κατάστασης πίνακα ελέγχου", @@ -917,22 +924,27 @@ "view-statistics": "Προβολή στατιστικών" }, "api-limit": { - "cassandra-queries": "Ερωτήματα Cassandra", + "cassandra-write-queries-core": "Ερωτήματα εγγραφής Cassandra από REST API", + "cassandra-read-queries-core": "Ερωτήματα ανάγνωσης Cassandra τηλεμετρίας από REST API και WS", + "cassandra-write-queries-rule-engine": "Ερωτήματα εγγραφής Cassandra τηλεμετρίας από Rule Engine", + "cassandra-read-queries-rule-engine": "Ερωτήματα ανάγνωσης Cassandra τηλεμετρίας από Rule Engine", + "cassandra-write-queries-monolith": "Ερωτήματα εγγραφής Cassandra τηλεμετρίας από το Monolith", + "cassandra-read-queries-monolith": "Ερωτήματα ανάγνωσης Cassandra τηλεμετρίας από το Monolith", "entity-version-creation": "Δημιουργία έκδοσης οντότητας", "entity-version-load": "Φόρτωση έκδοσης οντότητας", - "notification-requests": "Αιτήματα ειδοποίησης", - "notification-requests-per-rule": "Αιτήματα ειδοποίησης ανά κανόνα", + "notification-requests": "Αιτήματα ειδοποιήσεων", + "notification-requests-per-rule": "Αιτήματα ειδοποιήσεων ανά κανόνα", "rest-api-requests": "Αιτήματα REST API", "rest-api-requests-per-customer": "Αιτήματα REST API ανά πελάτη", "transport-messages": "Μηνύματα μεταφοράς", "transport-messages-per-device": "Μηνύματα μεταφοράς ανά συσκευή", - "transport-messages-per-gateway": "Μηνύματα μεταφοράς ανά πύλη", - "transport-messages-per-gateway-device": "Μηνύματα μεταφοράς ανά συσκευή πύλης", + "transport-messages-per-gateway": "Μηνύματα μεταφοράς ανά gateway", + "transport-messages-per-gateway-device": "Μηνύματα μεταφοράς ανά συσκευή σε gateway", "ws-updates-per-session": "Ενημερώσεις WS ανά συνεδρία", "edge-events": "Γεγονότα Edge", - "edge-events-per-edge": "Γεγονότα Edge ανά Edge", - "edge-uplink-messages": "Μηνύματα uplink Edge", - "edge-uplink-messages-per-edge": "Μηνύματα uplink ανά Edge" + "edge-events-per-edge": "Γεγονότα Edge ανά μονάδα Edge", + "edge-uplink-messages": "Μηνύματα uplink από Edge", + "edge-uplink-messages-per-edge": "Μηνύματα uplink από Edge ανά μονάδα Edge" }, "audit-log": { "audit": "Έλεγχος", @@ -1057,24 +1069,103 @@ "delete-multiple-title": "Είστε βέβαιοι ότι θέλετε να διαγράψετε { count, plural, =1 {1 υπολογιζόμενο πεδίο} other {# υπολογιζόμενα πεδία} };", "delete-multiple-text": "Προσοχή, μετά την επιβεβαίωση όλα τα επιλεγμένα υπολογιζόμενα πεδία θα διαγραφούν και όλα τα σχετικά δεδομένα δεν θα είναι ανακτήσιμα.", "test-with-this-message": "Δοκιμή με αυτό το μήνυμα", + "use-latest-timestamp": "Χρήση πιο πρόσφατης χρονικής σήμανσης", "hint": { - "arguments-simple-with-rolling": "Το απλού τύπου υπολογιζόμενο πεδίο δεν πρέπει να περιλαμβάνει κλειδιά με τύπο συσσώρευσης χρονοσειράς.", + "arguments-simple-with-rolling": "Τα υπολογιζόμενα πεδία τύπου Simple δεν πρέπει να περιέχουν κλειδιά τύπου time series rolling.", "arguments-empty": "Τα ορίσματα δεν πρέπει να είναι κενά.", - "expression-required": "Η έκφραση είναι υποχρεωτική.", - "expression-invalid": "Η έκφραση δεν είναι έγκυρη", + "expression-required": "Απαιτείται έκφραση.", + "expression-invalid": "Η έκφραση δεν είναι έγκυρη.", "expression-max-length": "Το μήκος της έκφρασης πρέπει να είναι μικρότερο από 255 χαρακτήρες.", - "argument-name-required": "Το όνομα ορίσματος είναι υποχρεωτικό.", - "argument-name-pattern": "Το όνομα ορίσματος δεν είναι έγκυρο.", + "argument-name-required": "Απαιτείται όνομα ορίσματος.", + "argument-name-pattern": "Το όνομα του ορίσματος δεν είναι έγκυρο.", "argument-name-duplicate": "Υπάρχει ήδη όρισμα με αυτό το όνομα.", - "argument-name-max-length": "Το όνομα ορίσματος πρέπει να είναι μικρότερο από 256 χαρακτήρες.", - "argument-name-forbidden": "Το όνομα ορίσματος είναι δεσμευμένο και δεν μπορεί να χρησιμοποιηθεί.", - "argument-type-required": "Ο τύπος ορίσματος είναι υποχρεωτικός.", - "max-args": "Έχει επιτευχθεί ο μέγιστος αριθμός ορισμάτων.", - "decimals-range": "Οι δεκαδικοί στην προεπιλογή πρέπει να είναι αριθμός από 0 έως 15.", - "expression": "Η προεπιλεγμένη έκφραση δείχνει πώς να μετατραπεί η θερμοκρασία από Φαρενάιτ σε Κελσίου.", - "arguments-entity-not-found": "Η οντότητα-στόχος του ορίσματος δεν βρέθηκε." + "argument-name-max-length": "Το όνομα του ορίσματος πρέπει να είναι μικρότερο από 256 χαρακτήρες.", + "argument-name-forbidden": "Το όνομα του ορίσματος είναι δεσμευμένο και δεν μπορεί να χρησιμοποιηθεί.", + "argument-type-required": "Απαιτείται τύπος ορίσματος.", + "max-args": "Έχει επιτευχθεί το μέγιστο πλήθος ορισμάτων.", + "decimals-range": "Ο αριθμός δεκαδικών πρέπει να είναι από 0 έως 15.", + "expression": "Η προεπιλεγμένη έκφραση δείχνει πώς να μετατρέψετε τη θερμοκρασία από Φαρενάιτ σε Κελσίου.", + "arguments-entity-not-found": "Η οντότητα στόχος του ορίσματος δεν βρέθηκε.", + "use-latest-timestamp": "Αν είναι ενεργοποιημένο, η τιμή θα αποθηκευτεί με τη πιο πρόσφατη χρονική σήμανση από τα τηλεμετρικά δεδομένα των ορισμάτων, αντί για την ώρα του διακομιστή." } }, + "ai-models": { + "ai-models": "Μοντέλα AI", + "ai-model": "Μοντέλο AI", + "model": "Μοντέλο", + "name": "Όνομα", + "ai-provider": "Πάροχος AI", + "no-found": "Δεν βρέθηκαν μοντέλα AI", + "list": "{ count, plural, =1 {Ένα μοντέλο} other {Λίστα με # μοντέλα} }", + "selected-fields": "{ count, plural, =1 {1 μοντέλο} other {# μοντέλα} } επιλεγμένα", + "add": "Προσθήκη μοντέλου", + "delete-model-title": "Είστε σίγουροι ότι θέλετε να διαγράψετε το μοντέλο '{{modelName}}';", + "delete-model-text": "Προσοχή, μετά την επιβεβαίωση το μοντέλο και όλα τα σχετικά δεδομένα δεν θα μπορούν να ανακτηθούν.", + "delete-models-title": "Είστε σίγουροι ότι θέλετε να διαγράψετε { count, plural, =1 {1 μοντέλο} other {# μοντέλα} };", + "delete-models-text": "Προσοχή, μετά την επιβεβαίωση όλα τα επιλεγμένα μοντέλα θα διαγραφούν και όλα τα σχετικά δεδομένα δεν θα μπορούν να ανακτηθούν.", + "ai-providers": { + "openai": "OpenAI", + "azure-openai": "Azure OpenAI", + "google-ai-gemini": "Google AI Gemini", + "google-vertex-ai-gemini": "Google Vertex AI Gemini", + "mistral-ai": "Mistral AI", + "anthropic": "Anthropic", + "amazon-bedrock": "Amazon Bedrock", + "github-models": "GitHub Models" + }, + "name-required": "Απαιτείται όνομα.", + "name-max-length": "Το όνομα πρέπει να έχει έως 255 χαρακτήρες.", + "provider": "Πάροχος", + "api-key": "Κλειδί API", + "api-key-required": "Απαιτείται κλειδί API.", + "project-id": "Αναγνωριστικό έργου", + "project-id-required": "Απαιτείται αναγνωριστικό έργου.", + "location": "Τοποθεσία", + "location-required": "Απαιτείται τοποθεσία.", + "service-account-key-file": "Αρχείο κλειδιού λογαριασμού υπηρεσίας", + "service-account-key-file-required": "Απαιτείται αρχείο κλειδιού λογαριασμού υπηρεσίας.", + "no-file": "Δεν επιλέχθηκε αρχείο.", + "drop-file": "Σύρετε ή κάντε κλικ για να επιλέξετε αρχείο προς μεταφόρτωση.", + "personal-access-token": "Προσωπικό διακριτικό πρόσβασης", + "personal-access-token-required": "Απαιτείται προσωπικό διακριτικό πρόσβασης.", + "configuration": "Ρύθμιση παραμέτρων", + "model-id": "Αναγνωριστικό μοντέλου", + "model-id-required": "Απαιτείται αναγνωριστικό μοντέλου.", + "deployment-name": "Όνομα ανάπτυξης", + "deployment-name-required": "Απαιτείται όνομα ανάπτυξης", + "set": "Ορισμός", + "region": "Περιοχή", + "region-required": "Απαιτείται περιοχή.", + "access-key-id": "Αναγνωριστικό κλειδιού πρόσβασης", + "access-key-id-required": "Απαιτείται αναγνωριστικό κλειδιού πρόσβασης.", + "secret-access-key": "Μυστικό κλειδί πρόσβασης", + "secret-access-key-required": "Απαιτείται μυστικό κλειδί πρόσβασης.", + "temperature": "Θερμοκρασία", + "temperature-hint": "Ρυθμίζει το επίπεδο τυχαιότητας στην έξοδο του μοντέλου. Υψηλότερες τιμές αυξάνουν την τυχαιότητα, ενώ χαμηλότερες τη μειώνουν.", + "temperature-min": "Πρέπει να είναι 0 ή μεγαλύτερο.", + "top-p": "Top P", + "top-p-hint": "Δημιουργεί μια ομάδα με τα πιο πιθανά tokens από τα οποία θα επιλέξει το μοντέλο. Υψηλότερες τιμές δημιουργούν μεγαλύτερη και πιο ποικίλη ομάδα.", + "top-p-min-max": "Πρέπει να είναι μεγαλύτερο από 0 και έως 1.", + "top-k": "Top K", + "top-k-hint": "Περιορίζει τις επιλογές του μοντέλου σε ένα καθορισμένο σύνολο με τα \"K\" πιο πιθανά tokens.", + "top-k-min": "Πρέπει να είναι 0 ή μεγαλύτερο.", + "presence-penalty": "Ποινή παρουσίας", + "presence-penalty-hint": "Εφαρμόζει σταθερή ποινή στην πιθανότητα ενός token αν έχει ήδη εμφανιστεί στο κείμενο.", + "frequency-penalty": "Ποινή συχνότητας", + "frequency-penalty-hint": "Εφαρμόζει ποινή στην πιθανότητα ενός token που αυξάνεται με βάση τη συχνότητα εμφάνισής του στο κείμενο.", + "max-output-tokens": "Μέγιστος αριθμός tokens εξόδου", + "max-output-tokens-min": "Πρέπει να είναι μεγαλύτερο από 0.", + "max-output-tokens-hint": "Ορίζει τον μέγιστο αριθμό tokens που μπορεί να δημιουργήσει το μοντέλο σε μία απάντηση.", + "endpoint": "Σημείο τερματισμού (Endpoint)", + "endpoint-required": "Απαιτείται σημείο τερματισμού.", + "service-version": "Έκδοση υπηρεσίας", + "check-connectivity": "Έλεγχος συνδεσιμότητας", + "check-connectivity-success": "Το δοκιμαστικό αίτημα ήταν επιτυχές", + "check-connectivity-failed": "Το δοκιμαστικό αίτημα απέτυχε", + "no-model-matching": "Δεν βρέθηκαν μοντέλα που να ταιριάζουν με '{{entity}}'.", + "model-required": "Απαιτείται μοντέλο.", + "no-model-text": "Δεν βρέθηκαν μοντέλα." + }, "confirm-on-exit": { "message": "Έχετε μη αποθηκευμένες αλλαγές. Είστε βέβαιοι ότι θέλετε να φύγετε από αυτήν τη σελίδα;", "html-message": "Έχετε μη αποθηκευμένες αλλαγές.
    Είστε βέβαιοι ότι θέλετε να φύγετε από αυτήν τη σελίδα;", @@ -1227,91 +1318,91 @@ "to": "Έως" }, "dashboard": { - "dashboard": "Dashboard", - "dashboards": "Dashboards", - "management": "Dashboard management", - "view-dashboards": "View Dashboards", - "add": "Add dashboard", - "assign-dashboard-to-customer": "Assign Dashboard(s) To Customer", - "assign-dashboard-to-customer-text": "Please select the dashboards to assign to the customer", - "assign-to-customer-text": "Please select the customer to assign the dashboard(s)", - "assign-to-customer": "Assign to customer", - "unassign-from-customer": "Unassign from customer", - "make-public": "Make dashboard public", - "make-private": "Make dashboard private", - "manage-assigned-customers": "Manage assigned customers", - "assigned-customers": "Assigned customers", - "assign-to-customers": "Assign Dashboard(s) To Customers", - "assign-to-customers-text": "Please select the customers to assign the dashboard(s)", - "unassign-from-customers": "Unassign Dashboard(s) From Customers", - "unassign-from-customers-text": "Please select the customers to unassign from the dashboard(s)", - "no-dashboards-text": "No dashboards found", - "no-widgets": "No widgets configured", - "add-widget": "Add new widget", - "add-widget-button-text": "Add widget", - "title": "Title", - "image": "Dashboard image", - "mobile-app-settings": "Mobile application settings", - "mobile-order": "Dashboard order in mobile application", - "mobile-hide": "Hide dashboard in mobile application", - "update-image": "Update dashboard image", - "take-screenshot": "Take screenshot", - "select-widget-title": "Select widget", - "select-widget-value": "{{title}}: select widget", - "select-widget-subtitle": "List of available widget types", - "delete": "Delete dashboard", - "title-required": "Title is required.", - "title-max-length": "Title should be less than 256", - "description": "Description", - "details": "Details", - "dashboard-details": "Dashboard details", - "add-dashboard-text": "Add new dashboard", - "assign-dashboards": "Assign dashboards", - "assign-new-dashboard": "Assign new dashboard", - "assign-dashboards-text": "Assign { count, plural, =1 {1 dashboard} other {# dashboards} } to customers", - "unassign-dashboards-action-text": "Unassign { count, plural, =1 {1 dashboard} other {# dashboards} } from customers", - "delete-dashboards": "Delete dashboards", - "unassign-dashboards": "Unassign dashboards", - "unassign-dashboards-action-title": "Unassign { count, plural, =1 {1 dashboard} other {# dashboards} } from customer", - "delete-dashboard-title": "Are you sure you want to delete the dashboard '{{dashboardTitle}}'?", - "delete-dashboard-text": "Be careful, after the confirmation the dashboard and all related data will become unrecoverable.", - "delete-dashboards-title": "Are you sure you want to delete { count, plural, =1 {1 dashboard} other {# dashboards} }?", - "delete-dashboards-action-title": "Delete { count, plural, =1 {1 dashboard} other {# dashboards} }", - "delete-dashboards-text": "Be careful, after the confirmation all selected dashboards will be removed and all related data will become unrecoverable.", - "unassign-dashboard-title": "Are you sure you want to unassign the dashboard '{{dashboardTitle}}'?", - "unassign-dashboard-text": "After the confirmation the dashboard will be unassigned and won't be accessible by the customer.", - "unassign-dashboard": "Unassign dashboard", - "unassign-dashboards-title": "Are you sure you want to unassign { count, plural, =1 {1 dashboard} other {# dashboards} }?", - "unassign-dashboards-text": "After the confirmation all selected dashboards will be unassigned and won't be accessible by the customer.", - "public-dashboard-title": "Dashboard is now public", - "public-dashboard-text": "Your dashboard {{dashboardTitle}} is now public and accessible via next public link:", - "public-dashboard-notice": "Note: Do not forget to make related devices public in order to access their data.", - "make-private-dashboard-title": "Are you sure you want to make the dashboard '{{dashboardTitle}}' private?", - "make-private-dashboard-text": "After the confirmation the dashboard will be made private and won't be accessible by others.", - "make-private-dashboard": "Make dashboard private", - "socialshare-text": "'{{dashboardTitle}}' powered by ThingsBoard", - "socialshare-title": "'{{dashboardTitle}}' powered by ThingsBoard", - "select-dashboard": "Select dashboard", - "no-dashboards-matching": "No dashboards matching '{{entity}}' were found.", - "dashboard-required": "Dashboard is required.", - "select-existing": "Select existing dashboard", - "create-new": "Create new dashboard", - "new-dashboard-title": "New dashboard title", - "open-dashboard": "Open dashboard", - "set-background": "Set background", - "background-color": "Background color", - "background-image": "Background image", - "background-size-mode": "Background size mode", - "no-image": "No image selected", - "empty-image": "No image", - "drop-image": "Drop an image or click to select a file to upload.", - "maximum-upload-file-size": "Maximum upload file size: {{ size }}", - "cannot-upload-file": "Cannot upload file", - "settings": "Settings", - "move-all-widgets": "Move all widgets", - "move-by": "Move by", - "cols": "cols", - "rows": "rows", + "dashboard": "Πίνακας ελέγχου", + "dashboards": "Πίνακες ελέγχου", + "management": "Διαχείριση πίνακα ελέγχου", + "view-dashboards": "Προβολή πινάκων ελέγχου", + "add": "Προσθήκη πίνακα ελέγχου", + "assign-dashboard-to-customer": "Ανάθεση πίνακα(ων) ελέγχου σε πελάτη", + "assign-dashboard-to-customer-text": "Επιλέξτε τους πίνακες ελέγχου που θα ανατεθούν στον πελάτη", + "assign-to-customer-text": "Επιλέξτε τον πελάτη για ανάθεση του πίνακα(ων) ελέγχου", + "assign-to-customer": "Ανάθεση σε πελάτη", + "unassign-from-customer": "Αφαίρεση ανάθεσης από πελάτη", + "make-public": "Δημοσίευση πίνακα ελέγχου", + "make-private": "Ιδιωτικοποίηση πίνακα ελέγχου", + "manage-assigned-customers": "Διαχείριση ανατεθειμένων πελατών", + "assigned-customers": "Ανατεθειμένοι πελάτες", + "assign-to-customers": "Ανάθεση πίνακα(ων) ελέγχου σε πελάτες", + "assign-to-customers-text": "Επιλέξτε τους πελάτες στους οποίους θα ανατεθούν οι πίνακες ελέγχου", + "unassign-from-customers": "Αφαίρεση ανάθεσης πίνακα(ων) ελέγχου από πελάτες", + "unassign-from-customers-text": "Επιλέξτε τους πελάτες από τους οποίους θα αφαιρεθεί η ανάθεση των πινάκων ελέγχου", + "no-dashboards-text": "Δεν βρέθηκαν πίνακες ελέγχου", + "no-widgets": "Δεν έχουν ρυθμιστεί widgets", + "add-widget": "Προσθήκη νέου widget", + "add-widget-button-text": "Προσθήκη widget", + "title": "Τίτλος", + "image": "Εικόνα πίνακα ελέγχου", + "mobile-app-settings": "Ρυθμίσεις εφαρμογής κινητού", + "mobile-order": "Σειρά πίνακα ελέγχου στην εφαρμογή κινητού", + "mobile-hide": "Απόκρυψη πίνακα ελέγχου στην εφαρμογή κινητού", + "update-image": "Ενημέρωση εικόνας πίνακα ελέγχου", + "take-screenshot": "Λήψη στιγμιότυπου", + "select-widget-title": "Επιλογή widget", + "select-widget-value": "{{title}}: επιλογή widget", + "select-widget-subtitle": "Λίστα διαθέσιμων τύπων widget", + "delete": "Διαγραφή πίνακα ελέγχου", + "title-required": "Απαιτείται τίτλος.", + "title-max-length": "Ο τίτλος πρέπει να είναι μικρότερος από 256 χαρακτήρες", + "description": "Περιγραφή", + "details": "Λεπτομέρειες", + "dashboard-details": "Λεπτομέρειες πίνακα ελέγχου", + "add-dashboard-text": "Προσθήκη νέου πίνακα ελέγχου", + "assign-dashboards": "Ανάθεση πινάκων ελέγχου", + "assign-new-dashboard": "Ανάθεση νέου πίνακα ελέγχου", + "assign-dashboards-text": "Ανάθεση { count, plural, =1 {1 πίνακα ελέγχου} other {# πινάκων ελέγχου} } σε πελάτες", + "unassign-dashboards-action-text": "Αφαίρεση ανάθεσης { count, plural, =1 {1 πίνακα ελέγχου} other {# πινάκων ελέγχου} } από πελάτες", + "delete-dashboards": "Διαγραφή πινάκων ελέγχου", + "unassign-dashboards": "Αφαίρεση ανάθεσης πινάκων ελέγχου", + "unassign-dashboards-action-title": "Αφαίρεση ανάθεσης { count, plural, =1 {1 πίνακα ελέγχου} other {# πινάκων ελέγχου} } από πελάτη", + "delete-dashboard-title": "Είστε σίγουροι ότι θέλετε να διαγράψετε τον πίνακα ελέγχου '{{dashboardTitle}}';", + "delete-dashboard-text": "Προσοχή, μετά την επιβεβαίωση ο πίνακας ελέγχου και όλα τα σχετικά δεδομένα δεν θα μπορούν να ανακτηθούν.", + "delete-dashboards-title": "Είστε σίγουροι ότι θέλετε να διαγράψετε { count, plural, =1 {1 πίνακα ελέγχου} other {# πινάκων ελέγχου} };", + "delete-dashboards-action-title": "Διαγραφή { count, plural, =1 {1 πίνακα ελέγχου} other {# πινάκων ελέγχου} }", + "delete-dashboards-text": "Προσοχή, μετά την επιβεβαίωση όλοι οι επιλεγμένοι πίνακες ελέγχου θα διαγραφούν και όλα τα σχετικά δεδομένα δεν θα μπορούν να ανακτηθούν.", + "unassign-dashboard-title": "Είστε σίγουροι ότι θέλετε να αφαιρέσετε την ανάθεση του πίνακα ελέγχου '{{dashboardTitle}}';", + "unassign-dashboard-text": "Μετά την επιβεβαίωση ο πίνακας ελέγχου θα αποσυνδεθεί και δεν θα είναι προσβάσιμος από τον πελάτη.", + "unassign-dashboard": "Αφαίρεση ανάθεσης πίνακα ελέγχου", + "unassign-dashboards-title": "Είστε σίγουροι ότι θέλετε να αφαιρέσετε την ανάθεση { count, plural, =1 {1 πίνακα ελέγχου} other {# πινάκων ελέγχου} };", + "unassign-dashboards-text": "Μετά την επιβεβαίωση όλοι οι επιλεγμένοι πίνακες ελέγχου θα αποσυνδεθούν και δεν θα είναι προσβάσιμοι από τον πελάτη.", + "public-dashboard-title": "Ο πίνακας ελέγχου είναι πλέον δημόσιος", + "public-dashboard-text": "Ο πίνακάς σας {{dashboardTitle}} είναι πλέον δημόσιος και προσβάσιμος από τον παρακάτω δημόσιο σύνδεσμο:", + "public-dashboard-notice": "Σημείωση: Μην ξεχάσετε να δημοσιοποιήσετε τις σχετικές συσκευές για να είναι προσβάσιμα τα δεδομένα τους.", + "make-private-dashboard-title": "Είστε σίγουροι ότι θέλετε να κάνετε ιδιωτικό τον πίνακα ελέγχου '{{dashboardTitle}}';", + "make-private-dashboard-text": "Μετά την επιβεβαίωση ο πίνακας ελέγχου θα γίνει ιδιωτικός και δεν θα είναι πλέον προσβάσιμος από άλλους.", + "make-private-dashboard": "Ιδιωτικοποίηση πίνακα ελέγχου", + "socialshare-text": "'{{dashboardTitle}}' με την υποστήριξη του ThingsBoard", + "socialshare-title": "'{{dashboardTitle}}' με την υποστήριξη του ThingsBoard", + "select-dashboard": "Επιλογή πίνακα ελέγχου", + "no-dashboards-matching": "Δεν βρέθηκαν πίνακες ελέγχου που να ταιριάζουν με '{{entity}}'.", + "dashboard-required": "Απαιτείται πίνακας ελέγχου.", + "select-existing": "Επιλογή υπάρχοντος πίνακα ελέγχου", + "create-new": "Δημιουργία νέου πίνακα ελέγχου", + "new-dashboard-title": "Τίτλος νέου πίνακα ελέγχου", + "open-dashboard": "Άνοιγμα πίνακα ελέγχου", + "set-background": "Ορισμός φόντου", + "background-color": "Χρώμα φόντου", + "background-image": "Εικόνα φόντου", + "background-size-mode": "Λειτουργία μεγέθους φόντου", + "no-image": "Δεν επιλέχθηκε εικόνα", + "empty-image": "Καμία εικόνα", + "drop-image": "Σύρετε μια εικόνα ή κάντε κλικ για επιλογή αρχείου προς μεταφόρτωση.", + "maximum-upload-file-size": "Μέγιστο μέγεθος αρχείου μεταφόρτωσης: {{ size }}", + "cannot-upload-file": "Αδυναμία μεταφόρτωσης αρχείου", + "settings": "Ρυθμίσεις", + "move-all-widgets": "Μετακίνηση όλων των widgets", + "move-by": "Μετακίνηση κατά", + "cols": "στήλες", + "rows": "γραμμές", "layout": "Διάταξη", "layout-type-default": "Προεπιλογή", "layout-type-scada": "SCADA", @@ -1753,7 +1844,8 @@ "step": "Βήμα", "selected-options-limit": "Όριο επιλεγμένων επιλογών", "advanced-ui-settings": "Προηγμένες ρυθμίσεις UI", - "disable-on-property": "Απενεργοποίηση με βάση ιδιότητα", + "disable-on-property": "Απενεργοποίηση βάσει ιδιότητας", + "disable-on-property-none": "Καμία (το πεδίο είναι πάντα ενεργό)", "display-condition-function": "Συνάρτηση συνθήκης εμφάνισης", "sub-label": "Υποετικέτα", "vertical-divider-after": "Κατακόρυφος διαχωριστής μετά", @@ -1787,7 +1879,8 @@ "array-item": "Στοιχείο πίνακα", "item-type": "Τύπος στοιχείου", "item-name": "Όνομα στοιχείου", - "no-items": "Δεν υπάρχουν στοιχεία" + "no-items": "Δεν υπάρχουν στοιχεία", + "support-unit-conversion": "Υποστήριξη μετατροπής μονάδων" }, "clear-form": "Εκκαθάριση φόρμας", "clear-form-prompt": "Είστε βέβαιοι ότι θέλετε να αφαιρέσετε όλες τις ιδιότητες της φόρμας;", @@ -1911,6 +2004,7 @@ "mqtt-use-json-format-for-default-downlink-topics-hint": "Όταν ενεργοποιηθεί, χρησιμοποιείται JSON payload για αποστολή attributes και RPC μέσω θεμάτων v1/devices/me/attributes/response/$request_id, v1/devices/me/attributes, v1/devices/me/rpc/request/$request_id, v1/devices/me/rpc/response/$request_id. Δεν επηρεάζει νέα θέματα (v2): v2/a/res/$request_id κ.λπ.", "mqtt-send-ack-on-validation-exception": "Αποστολή PUBACK σε αποτυχία επικύρωσης PUBLISH μηνύματος", "mqtt-send-ack-on-validation-exception-hint": "Αντί για τερματισμό της συνεδρίας, η πλατφόρμα θα στείλει PUBACK αν είναι ενεργοποιημένο.", + "mqtt-protocol-version": "Έκδοση πρωτοκόλλου", "snmp-add-mapping": "Προσθήκη SNMP αντιστοίχισης", "snmp-mapping-not-configured": "Δεν έχει διαμορφωθεί αντιστοίχιση OID σε χρονική σειρά/χαρακτηριστικό", "snmp-timseries-or-attribute-name": "Όνομα για αντιστοίχιση χρονικής σειράς/χαρακτηριστικού", @@ -2131,24 +2225,24 @@ "notification-storing": "Αποθήκευση ειδοποιήσεων όταν είναι απενεργοποιημένο ή εκτός σύνδεσης", "binding": "Δεσμευτικό", "binding-type": { - "u": "U: Το client είναι προσβάσιμο μέσω UDP ανά πάσα στιγμή.", - "m": "M: Το client είναι προσβάσιμο μέσω MQTT ανά πάσα στιγμή.", - "h": "H: Το client είναι προσβάσιμο μέσω HTTP ανά πάσα στιγμή.", - "t": "T: Το client είναι προσβάσιμο μέσω TCP ανά πάσα στιγμή.", - "s": "S: Το client είναι προσβάσιμο μέσω SMS ανά πάσα στιγμή.", - "n": "N: Το client πρέπει να απαντήσει μέσω Non-IP (υποστηρίζεται από LWM2M 1.1).", - "uq": "UQ: UDP σε queue mode (δεν υποστηρίζεται από LWM2M 1.1)", - "uqs": "UQS: UDP + SMS ενεργά· UDP σε queue mode, SMS σε standard (δεν υποστηρίζεται από LWM2M 1.1)", - "tq": "TQ: TCP σε queue mode (δεν υποστηρίζεται από LWM2M 1.1)", - "tqs": "TQS: TCP + SMS ενεργά· TCP σε queue mode, SMS σε standard (δεν υποστηρίζεται από LWM2M 1.1)", - "sq": "SQ: SMS σε queue mode (δεν υποστηρίζεται από LWM2M 1.1)" + "u": "U: Ο πελάτης είναι προσβάσιμος μέσω UDP ανά πάσα στιγμή.", + "m": "M: Ο πελάτης είναι προσβάσιμος μέσω MQTT ανά πάσα στιγμή.", + "h": "H: Ο πελάτης είναι προσβάσιμος μέσω HTTP ανά πάσα στιγμή.", + "t": "T: Ο πελάτης είναι προσβάσιμος μέσω TCP ανά πάσα στιγμή.", + "s": "S: Ο πελάτης είναι προσβάσιμος μέσω SMS ανά πάσα στιγμή.", + "n": "N: Ο πελάτης ΠΡΕΠΕΙ να στείλει την απάντηση σε αυτό το αίτημα μέσω σύνδεσης Non-IP (υποστηρίζεται από την έκδοση LWM2M 1.1).", + "uq": "UQ: Σύνδεση UDP σε λειτουργία ουράς (δεν υποστηρίζεται από την έκδοση LWM2M 1.1)", + "uqs": "UQS: Ενεργές τόσο οι συνδέσεις UDP όσο και SMS· UDP σε λειτουργία ουράς, SMS σε κανονική λειτουργία (δεν υποστηρίζεται από την έκδοση LWM2M 1.1)", + "tq": "TQ: Σύνδεση TCP σε λειτουργία ουράς (δεν υποστηρίζεται από την έκδοση LWM2M 1.1)", + "tqs": "TQS: Ενεργές τόσο οι συνδέσεις TCP όσο και SMS· TCP σε λειτουργία ουράς, SMS σε κανονική λειτουργία (δεν υποστηρίζεται από την έκδοση LWM2M 1.1)", + "sq": "SQ: Σύνδεση SMS σε λειτουργία ουράς (δεν υποστηρίζεται από την έκδοση LWM2M 1.1)" }, - "binding-tooltip": "Ορίζει τα υποστηριζόμενα binding modes για το LwM2M Client. Πρέπει να είναι ίδιο με το \"Supported Binding and Modes\" στο αντικείμενο Συσκευής. Υποστηρίζεται ένα binding ανά συνεδρία μεταφοράς.", - "bootstrap-server": "Bootstrap διακομιστής", - "lwm2m-server": "LwM2M διακομιστής", - "include-bootstrap-server": "Συμπερίληψη ενημερώσεων Bootstrap Server", - "bootstrap-update-title": "Ο Bootstrap Server έχει ήδη διαμορφωθεί. Θέλετε σίγουρα να εξαιρεθεί;", - "bootstrap-update-text": "Προσοχή, τα δεδομένα διαμόρφωσης θα χαθούν μετά την επιβεβαίωση.", + "binding-tooltip": "Αυτή είναι η λίστα στο πόρο \"binding\" του αντικειμένου διακομιστή LwM2M - /1/x/7.\nΥποδεικνύει τις υποστηριζόμενες λειτουργίες σύνδεσης στον LwM2M Client.\nΑυτή η τιμή ΠΡΕΠΕΙ να είναι ίδια με την τιμή στο πόρο \"Supported Binding and Modes\" στο Αντικείμενο Συσκευής (/3/0/16).\nΑν και υποστηρίζονται πολλαπλά μέσα μεταφοράς, μόνο ένα μπορεί να χρησιμοποιηθεί κατά τη διάρκεια ολόκληρης της περιόδου σύνδεσης μεταφοράς.\nΓια παράδειγμα, όταν υποστηρίζονται UDP και SMS, ο LwM2M Client και ο LwM2M Server μπορούν να επιλέξουν είτε UDP είτε SMS για όλη τη διάρκεια της μεταφοράς.", + "bootstrap-server": "Διακομιστής Bootstrap", + "lwm2m-server": "Διακομιστής LwM2M", + "include-bootstrap-server": "Συμπερίληψη ενημερώσεων διακομιστή Bootstrap", + "bootstrap-update-title": "Έχετε ήδη ρυθμίσει διακομιστή Bootstrap. Είστε σίγουροι ότι θέλετε να εξαιρέσετε τις ενημερώσεις;", + "bootstrap-update-text": "Προσοχή, μετά την επιβεβαίωση τα δεδομένα ρύθμισης του διακομιστή Bootstrap δεν θα μπορούν να ανακτηθούν.", "server-host": "Διεύθυνση υποδοχής", "server-host-required": "Απαιτείται διεύθυνση υποδοχής.", "server-port": "Θύρα", @@ -2171,6 +2265,9 @@ "add-lwm2m-server-config": "Προσθήκη διακομιστή LwM2M", "no-config-servers": "Δεν έχουν διαμορφωθεί διακομιστές", "others-tab": "Άλλες ρυθμίσεις", + "ota-update": "Ενημέρωση OTA", + "use-object-19-for-ota-update": "Χρήση Αντικειμένου 19 για μεταδεδομένα αρχείου OTA (άθροισμα ελέγχου, μέγεθος, έκδοση, όνομα)", + "use-object-19-for-ota-update-hint": "Χρησιμοποιεί το Resource ObjectId = 19 για ενημερώσεις OTA: FirmWare → InstanceId = 65534, SoftWare → InstanceId = 65535. Η μορφή δεδομένων είναι JSON κωδικοποιημένο σε Base64. Το JSON περιέχει μεταδεδομένα αρχείου OTA (πληροφορίες αρχείου): \"Checksum\" (SHA256). Πρόσθετα πεδία: \"Title\" (όνομα OTA), \"Version\" (έκδοση OTA), \"File Name\" (όνομα αρχείου για αποθήκευση OTA στον client), \"File Size\" (μέγεθος OTA σε bytes).", "client-strategy": "Στρατηγική πελάτη κατά τη σύνδεση", "client-strategy-label": "Στρατηγική", "client-strategy-only-observe": "Μόνο αίτημα παρακολούθησης μετά την αρχική σύνδεση", @@ -2201,7 +2298,17 @@ "default-object-id": "Προεπιλεγμένη έκδοση αντικειμένου (χαρακτηριστικό)", "default-object-id-ver": { "v1-0": "1.0", - "v1-1": "1.1" + "v1-1": "1.1", + "v1-2": "1.2" + }, + "observe-strategy": { + "observe-strategy": "Στρατηγική παρακολούθησης (Observe)", + "single": "Μονή", + "single-description": "Ένα αίτημα Observe ανά πόρο (υψηλότερη ακρίβεια, περισσότερη κυκλοφορία δικτύου)", + "composite-all": "Συνδυασμένο - όλα", + "composite-all-description": "Όλοι οι πόροι παρακολουθούνται με ένα ενιαίο αίτημα Composite Observe (πιο αποδοτικό, λιγότερο ευέλικτο)", + "composite-by-object": "Συνδυασμένο ανά αντικείμενο", + "composite-by-object-description": "Οι πόροι ομαδοποιούνται ανά τύπο αντικειμένου και παρακολουθούνται με ξεχωριστά αιτήματα Composite Observe (ισορροπημένη προσέγγιση)" } }, "snmp": { @@ -2524,6 +2631,8 @@ "type-current-user-owner": "Ιδιοκτήτης τρέχοντος χρήστη", "type-calculated-field": "Υπολογιζόμενο πεδίο", "type-calculated-fields": "Υπολογιζόμενα πεδία", + "type-ai-model": "Μοντέλο AI", + "type-ai-models": "Μοντέλα AI", "type-widgets-bundle": "Πακέτο γραφικών", "type-widgets-bundles": "Πακέτα γραφικών", "list-of-widgets-bundles": "{ count, plural, =1 {Ένα πακέτο γραφικών} other {Λίστα με # πακέτα γραφικών} }", @@ -2553,6 +2662,8 @@ "type-tb-resources": "Πόροι", "list-of-tb-resources": "{ count, plural, =1 {Ένας πόρος} other {Λίστα με # πόρους} }", "type-ota-package": "Πακέτο OTA", + "type-ota-packages": "Πακέτα OTA", + "list-of-ota-packages": "{ count, plural, =1 {Ένα πακέτο OTA} other {Λίστα με # πακέτα OTA} }", "type-rpc": "RPC", "type-queue": "Ουρά", "type-queue-stats": "Στατιστικά ουράς", @@ -2938,6 +3049,7 @@ "missing-key-filters-error": "Λείπει φίλτρο κλειδιού για το φίλτρο '{{filter}}'.", "filter": "Φίλτρο", "editable": "Επεξεργάσιμο", + "editable-hint": "Επιτρέπει στον χρήστη να αλλάξει την τιμή του φίλτρου στους πίνακες ελέγχου.", "no-filters-found": "Δεν βρέθηκαν φίλτρα.", "no-filter-text": "Δεν έχει καθοριστεί φίλτρο", "add-filter-prompt": "Παρακαλώ προσθέστε φίλτρο", @@ -2977,6 +3089,8 @@ "filter-user-params": "Παράμετροι χρήστη φίλτρου", "user-parameters": "Παράμετροι χρήστη", "display-label": "Ετικέτα εμφάνισης", + "custom-label": "Προσαρμοσμένη ετικέτα", + "custom-label-hint": "Ενεργοποιήστε για να ορίσετε δική σας ετικέτα για το φίλτρο. Όταν είναι απενεργοποιημένο, θα δημιουργηθεί αυτόματα μια ετικέτα.", "order-priority": "Προτεραιότητα πεδίου", "key-filter": "Φίλτρο κλειδιού", "key-filters": "Φίλτρα κλειδιών", @@ -3021,7 +3135,8 @@ "switch-to-dynamic-value": "Μετάβαση σε δυναμική τιμή", "switch-to-default-value": "Μετάβαση σε προεπιλεγμένη τιμή", "inherit-owner": "Κληρονόμηση από ιδιοκτήτη", - "source-attribute-not-set": "Εάν δεν έχει οριστεί χαρακτηριστικό πηγής" + "source-attribute-not-set": "Εάν δεν έχει οριστεί χαρακτηριστικό πηγής", + "unit": "Μονάδα" }, "fullscreen": { "expand": "Επέκταση σε πλήρη οθόνη", @@ -3406,6 +3521,7 @@ "power-button-background": "Φόντο κουμπιού ισχύος", "value-box-background": "Φόντο πλαισίου τιμής", "value-units": "Μονάδες τιμής", + "enable-units-scale": "Ενεργοποίηση μονάδων στην κλίμακα", "filtration-mode": "Λειτουργία φιλτραρίσματος", "filtration-mode-hint": "Ακέραια τιμή που δηλώνει την τρέχουσα λειτουργία.", "filtration-mode-update": "Κατάσταση ενημέρωσης λειτουργίας", @@ -3722,6 +3838,8 @@ "mobile-package-max-length": "Το πακέτο εφαρμογής πρέπει να είναι λιγότερο από 256 χαρακτήρες", "mobile-package-required": "Το πακέτο εφαρμογής είναι υποχρεωτικό.", "mobile-package-pattern": "Μη έγκυρη μορφή πακέτου εφαρμογής", + "mobile-package-title": "Τίτλος εφαρμογής", + "mobile-package-title-max-length": "Ο τίτλος της εφαρμογής πρέπει να είναι μικρότερος από 256 χαρακτήρες", "no-application": "Δεν βρέθηκαν εφαρμογές", "no-bundles": "Δεν βρέθηκαν πακέτα", "platform-type": "Τύπος πλατφόρμας", @@ -3802,20 +3920,16 @@ "configuration-app": "Εφαρμογή ρυθμίσεων", "configuration-step": { "prepare-environment-title": "Προετοιμασία περιβάλλοντος ανάπτυξης", - "prepare-environment-text": "Η εφαρμογή ThingsBoard Mobile απαιτεί το Flutter SDK. Ακολουθήστε τις οδηγίες για να ρυθμίσετε το Flutter SDK.", - "get-source-code-title": "Λήψη πηγαίου κώδικα", - "get-source-code-text": "Μπορείτε να λάβετε τον πηγαίο κώδικα της εφαρμογής ThingsBoard Mobile με cloning από το GitHub:", - "configure-api-title": "Διαμόρφωση ThingsBoard API endpoint", - "configure-api-text": "Ανοίξτε το έργο flutter_thingsboard_pe_app στον επεξεργαστή σας. Επεξεργαστείτε:", - "configure-api-hint": "Ορίστε την τιμή της σταθεράς thingsBoardApiEndpoint ώστε να ταιριάζει με το API endpoint του ThingsBoard server. Μην χρησιμοποιείτε 'localhost' ή '127.0.0.1'.", - "run-app-title": "Εκτέλεση εφαρμογής", - "run-app-text": "Εκτελέστε την εφαρμογή μέσω του IDE σας ή μέσω τερματικού με την ακόλουθη εντολή:", - "more-information": "Περισσότερες πληροφορίες βρίσκονται στην τεκμηρίωση έναρξης.", - "getting-started": "Ξεκινώντας", - "configure-package-title": "Διαμόρφωση πακέτου εφαρμογής", - "configure-package-text": "Μπορείτε να αλλάξετε χειροκίνητα το Πακέτο Εφαρμογής ή να χρησιμοποιήσετε εργαλείο CLI τρίτων.", - "configure-package-text-install": "Για να εγκαταστήσετε το εργαλείο Rename CLI, εκτελέστε την ακόλουθη εντολή:", - "configure-package-run-commands": "Εκτελέστε αυτές τις εντολές στον root φάκελο του έργου σας:" + "prepare-environment-text": "Η εφαρμογή Flutter ThingsBoard Mobile απαιτεί το Flutter SDK. Ακολουθήστε τις οδηγίες για την εγκατάσταση του Flutter SDK.", + "get-source-code-title": "Λήψη πηγαίου κώδικα της εφαρμογής", + "get-source-code-text": "Μπορείτε να αποκτήσετε τον πηγαίο κώδικα της εφαρμογής Flutter ThingsBoard Mobile κάνοντάς τον clone από το αποθετήριο GitHub:", + "configure-app-settings-title": "Ρύθμιση παραμέτρων εφαρμογής", + "configure-app-settings-text": "Κατεβάστε το αρχείο ρυθμίσεων και τοποθετήστε το στον ριζικό φάκελο του έργου που κάνατε clone στο προηγούμενο βήμα.", + "download-file": "Λήψη αρχείου", + "run-app-title": "Εκτέλεση της εφαρμογής", + "run-app-text": "Εκτελέστε την εφαρμογή όπως περιγράφεται στο IDE σας.\nΑν χρησιμοποιείτε το τερματικό, εκτελέστε την εφαρμογή με την παρακάτω εντολή:", + "more-information": "Αναλυτικές πληροφορίες μπορείτε να βρείτε στην τεκμηρίωση 'Ξεκινώντας'.", + "getting-started": "Ξεκινώντας" } }, "notification": { @@ -3839,6 +3953,7 @@ "new-platform-version-trigger-settings": "Ρυθμίσεις σκανδάλης νέας έκδοσης πλατφόρμας", "rate-limits-trigger-settings": "Ρυθμίσεις σκανδάλης υπέρβασης ορίων ρυθμού", "task-processing-failure-trigger-settings": "Ρυθμίσεις σκανδάλης αποτυχίας επεξεργασίας εργασίας", + "resources-shortage-trigger-settings": "Ρυθμίσεις ενεργοποίησης για έλλειψη πόρων", "at-least-one-should-be-selected": "Πρέπει να επιλεγεί τουλάχιστον ένα", "basic-settings": "Βασικές ρυθμίσεις", "button-text": "Κείμενο κουμπιού", @@ -3853,6 +3968,7 @@ "create-new": "Δημιουργία νέου", "created": "Δημιουργήθηκε", "customize-messages": "Προσαρμογή μηνυμάτων", + "cpu-threshold": "Όριο CPU", "delete-notification-text": "Προσοχή, μετά την επιβεβαίωση η ειδοποίηση δεν θα είναι ανακτήσιμη.", "delete-notification-title": "Είστε βέβαιοι ότι θέλετε να διαγράψετε την ειδοποίηση;", "delete-notifications-text": "Προσοχή, μετά την επιβεβαίωση οι ειδοποιήσεις δεν θα είναι ανακτήσιμες.", @@ -3918,7 +4034,8 @@ "input-field-support-templatization": "Το πεδίο εισαγωγής υποστηρίζει δυναμική μορφοποίηση.", "input-fields-support-templatization": "Τα πεδία εισαγωγής υποστηρίζουν δυναμική μορφοποίηση.", "link": "Σύνδεσμος", - "link-required": "Ο σύνδεσμος είναι υποχρεωτικός", + "link-required": "Απαιτείται σύνδεσμος", + "link-max-length": "Ο σύνδεσμος πρέπει να έχει μήκος μικρότερο ή ίσο με {{ length }} χαρακτήρες", "link-type": { "dashboard": "Άνοιγμα πίνακα ελέγχου", "link": "Άνοιγμα συνδέσμου URL" @@ -3945,6 +4062,7 @@ "no-severity-found": "Δεν βρέθηκε σοβαρότητα", "no-severity-matching": "'{{severity}}' δεν βρέθηκε.", "no-template-matching": "Δεν βρέθηκε πόρος που να ταιριάζει με '{{template}}'", + "create-new-template": "Δημιουργήστε ένα νέο!", "not-found-slack-recipient": "Δεν βρέθηκε παραλήπτης Slack", "notification": "Ειδοποίηση", "notification-center": "Κέντρο ειδοποιήσεων", @@ -3968,6 +4086,7 @@ "only-rule-chain-lifecycle-failures": "Μόνο αποτυχίες κύκλου ζωής αλυσίδας κανόνων", "only-rule-node-lifecycle-failures": "Μόνο αποτυχίες κύκλου ζωής κόμβου κανόνων", "platform-users": "Χρήστες πλατφόρμας", + "ram-threshold": "Όριο RAM", "rate-limits": "Όρια ρυθμού", "rate-limits-hint": "Αν το πεδίο είναι κενό, η σκανδάλη θα εφαρμοστεί σε όλα τα όρια ρυθμού", "recipient": "Παραλήπτης", @@ -4033,6 +4152,7 @@ "start-from-scratch": "Ξεκινήστε από την αρχή", "status": "Κατάσταση", "stop-escalation-alarm-status-become": "Διακοπή κλιμάκωσης όταν η κατάσταση του συναγερμού γίνει:", + "storage-threshold": "Όριο αποθήκευσης", "subject": "Θέμα", "subject-required": "Το θέμα είναι υποχρεωτικό", "subject-max-length": "Το θέμα πρέπει να είναι μικρότερο ή ίσο με {{ length }} χαρακτήρες", @@ -4054,7 +4174,8 @@ "rate-limits": "Υπέρβαση ορίων ρυθμού", "edge-communication-failure": "Αποτυχία επικοινωνίας Edge", "edge-connection": "Σύνδεση Edge", - "task-processing-failure": "Αποτυχία επεξεργασίας εργασίας" + "task-processing-failure": "Αποτυχία επεξεργασίας εργασίας", + "resources-shortage": "Έλλειψη πόρων" }, "templates": "Πρότυπα", "notification-templates": "Ειδοποιήσεις / Πρότυπα", @@ -4078,6 +4199,7 @@ "edge-connection": "Σύνδεση Edge", "edge-communication-failure": "Αποτυχία επικοινωνίας Edge", "task-processing-failure": "Αποτυχία επεξεργασίας εργασίας", + "resources-shortage": "Έλλειψη πόρων", "trigger": "Σκανδάλη", "trigger-required": "Η σκανδάλη είναι υποχρεωτική" }, @@ -4119,6 +4241,7 @@ "checksum-copied-message": "Το άθροισμα ελέγχου του πακέτου έχει αντιγραφεί στο πρόχειρο", "change-firmware": "Η αλλαγή του firmware μπορεί να προκαλέσει ενημέρωση σε { count, plural, =1 {1 συσκευή} other {# συσκευές} }.", "change-software": "Η αλλαγή του λογισμικού μπορεί να προκαλέσει ενημέρωση σε { count, plural, =1 {1 συσκευή} other {# συσκευές} }.", + "change-ota-setting-title": "Είστε σίγουροι ότι θέλετε να αλλάξετε τις ρυθμίσεις OTA;", "chose-compatible-device-profile": "Το ανεβασμένο πακέτο θα είναι διαθέσιμο μόνο για συσκευές με το επιλεγμένο προφίλ.", "chose-firmware-distributed-device": "Επιλέξτε το firmware που θα διανεμηθεί στις συσκευές", "chose-software-distributed-device": "Επιλέξτε το λογισμικό που θα διανεμηθεί στις συσκευές", @@ -4314,6 +4437,7 @@ "add-relation-filter": "Προσθήκη φίλτρου συσχέτισης", "any-relation": "Οποιαδήποτε συσχέτιση", "relation-filters": "Φίλτρα συσχετίσεων", + "relation-filter": "Φίλτρο σχέσεων", "additional-info": "Επιπλέον πληροφορίες (JSON)", "invalid-additional-info": "Δεν ήταν δυνατή η ανάλυση του JSON των επιπλέον πληροφοριών.", "no-relations-text": "Δεν βρέθηκαν συσχετίσεις", @@ -4554,7 +4678,7 @@ "user-name-pattern": "Email χρήστη", "edge-name-pattern": "Όνομα Edge", "entity-name-pattern-required": "Απαιτείται μοτίβο ονόματος", - "entity-name-pattern-hint": "Το πεδίο μοτίβου ονόματος υποστηρίζει templatization. Χρησιμοποιήστε $[messageKey] για να εξάγετε τιμή από το μήνυμα και ${metadataKey} από τα μεταδεδομένα.", + "entity-name-pattern-hint": "Το πεδίο μοτίβου ονόματος υποστηρίζει χρήση προτύπων. Χρησιμοποιήστε $[messageKey] για εξαγωγή τιμής από το μήνυμα και ${metadataKey} για εξαγωγή τιμής από τα μεταδεδομένα.", "copy-message-type": "Αντιγραφή τύπου μηνύματος", "entity-type-pattern": "Μοτίβο τύπου", "entity-type-pattern-required": "Απαιτείται μοτίβο τύπου", @@ -4813,8 +4937,8 @@ "read-timeout-hint": "Τιμή 0 σημαίνει άπειρο χρονικό όριο", "max-parallel-requests-count": "Μέγιστος αριθμός παραλλήλων αιτημάτων", "max-parallel-requests-count-hint": "Τιμή 0 σημαίνει χωρίς περιορισμό", - "max-response-size": "Μέγιστο μέγεθος απόκρισης (KB)", - "max-response-size-hint": "Μέγιστο μέγεθος μνήμης για αποκωδικοποίηση/κωδικοποίηση HTTP", + "max-response-size": "Μέγιστο μέγεθος απόκρισης (σε KB)", + "max-response-size-hint": "Η μέγιστη ποσότητα μνήμης που διατίθεται για προσωρινή αποθήκευση δεδομένων κατά την αποκωδικοποίηση ή κωδικοποίηση HTTP μηνυμάτων, όπως φορτία JSON ή XML", "headers": "Κεφαλίδες", "headers-hint": "Χρησιμοποιήστε ${metadataKey} για τιμή από τα μεταδεδομένα, $[messageKey] για τιμή από το σώμα του μηνύματος στα πεδία κεφαλίδας/τιμής", "header": "Κεφαλίδα", @@ -4823,7 +4947,7 @@ "value-required": "Απαιτείται τιμή", "topic-pattern": "Μοτίβο θέματος", "key-pattern": "Μοτίβο κλειδιού", - "key-pattern-hint": "Αν δοθεί partition number, θα χρησιμοποιηθεί. Διαφορετικά, θα χρησιμοποιηθεί το κλειδί.", + "key-pattern-hint": "Προαιρετικό. Αν έχει καθοριστεί έγκυρος αριθμός partition, θα χρησιμοποιηθεί κατά την αποστολή της εγγραφής. Αν δεν καθοριστεί partition, θα χρησιμοποιηθεί το key. Αν δεν καθοριστεί κανένα από τα δύο, θα εκχωρηθεί partition με κυκλικό τρόπο (round-robin).", "topic-pattern-required": "Απαιτείται μοτίβο θέματος", "topic": "Θέμα", "topic-required": "Απαιτείται θέμα", @@ -5230,7 +5354,7 @@ "function-name": "Όνομα συνάρτησης", "function-name-required": "Απαιτείται όνομα συνάρτησης.", "qualifier": "Καταληκτικό", - "qualifier-hint": "Αν δεν καθοριστεί, χρησιμοποιείται το \"$LATEST\".", + "qualifier-hint": "Αν δεν καθοριστεί qualifier, θα χρησιμοποιηθεί το προεπιλεγμένο qualifier \"$LATEST\".", "aws-credentials": "Διαπιστευτήρια AWS", "connection-timeout": "Χρονικό όριο σύνδεσης", "connection-timeout-required": "Απαιτείται χρονικό όριο σύνδεσης.", @@ -5304,6 +5428,36 @@ "html-text-description": "Υποστηρίζει ετικέτες HTML για μορφοποίηση, συνδέσμους και εικόνες.", "dynamic-text-description": "Υποστηρίζει δυναμική εναλλαγή μεταξύ Plain Text και HTML βάσει προτύπων.", "after-template-evaluation-hint": "Μετά την αξιολόγηση, αν η τιμή είναι true τότε είναι HTML, αλλιώς Plain text." + }, + "ai": { + "ai-model": "Μοντέλο AI", + "model": "Μοντέλο", + "ai-model-hint": "Επιλέξτε το προ-ρυθμισμένο μοντέλο AI για την επεξεργασία αιτημάτων που αποστέλλονται από αυτόν τον κόμβο κανόνων ή χρησιμοποιήστε το \"Δημιουργία νέου\" για να ρυθμίσετε ένα νέο μοντέλο.", + "prompt-settings": "Ρυθμίσεις prompt", + "prompt-settings-hint": "Το προαιρετικό system prompt ορίζει τον γενικό ρόλο και τους περιορισμούς του AI, ενώ το user prompt καθορίζει το συγκεκριμένο έργο προς εκτέλεση. Και τα δύο πεδία υποστηρίζουν χρήση προτύπων.", + "system-prompt": "System prompt", + "system-prompt-max-length": "Το system prompt πρέπει να είναι έως 500000 χαρακτήρες.", + "system-prompt-blank": "Το system prompt δεν πρέπει να είναι κενό.", + "user-prompt": "User prompt", + "user-prompt-required": "Απαιτείται user prompt.", + "user-prompt-max-length": "Το user prompt πρέπει να είναι έως 500000 χαρακτήρες.", + "user-prompt-blank": "Το user prompt δεν πρέπει να είναι κενό.", + "response-format": "Μορφή απόκρισης", + "response-text": "Κείμενο", + "response-json": "JSON", + "response-json-schema": "JSON Schema", + "response-format-hint-TEXT": "Επιτρέπει στο μοντέλο να δημιουργήσει αυθαίρετο κείμενο, το οποίο μπορεί να μην είναι έγκυρο αντικείμενο JSON. Αν δεν είναι έγκυρο, θα ενσωματωθεί αυτόματα σε ένα αντικείμενο JSON υπό το κλειδί \"response\".", + "response-format-hint-JSON": "Το μοντέλο πρέπει να παράγει απάντηση που να είναι έγκυρο αντικείμενο JSON. Αν δεν είναι έγκυρο, θα ενσωματωθεί αυτόματα υπό το κλειδί \"response\".", + "response-format-hint-JSON_SCHEMA": "Το μοντέλο πρέπει να δημιουργήσει JSON που να συμμορφώνεται με τη δομή και τους τύπους δεδομένων του παρεχόμενου schema. Αν δεν είναι έγκυρο, θα ενσωματωθεί αυτόματα υπό το κλειδί \"response\".", + "response-json-schema-hint": "Μπορεί να εισαχθεί οποιοδήποτε έγκυρο JSON Schema, ωστόσο αυτός ο κόμβος υποστηρίζει μόνο περιορισμένο υποσύνολο δυνατοτήτων του. Δείτε την τεκμηρίωση του κόμβου για λεπτομέρειες.", + "response-json-schema-required": "Απαιτείται JSON Schema", + "advanced-settings": "Προχωρημένες ρυθμίσεις", + "timeout": "Χρονικό όριο (Timeout)", + "timeout-hint": "Μέγιστος χρόνος αναμονής για απάντηση \nαπό το μοντέλο AI πριν τερματιστεί το αίτημα.", + "timeout-required": "Απαιτείται χρονικό όριο", + "timeout-validation": "Πρέπει να είναι από 1 δευτερόλεπτο έως 10 λεπτά.", + "force-acknowledgement": "Εξαναγκασμένη επιβεβαίωση", + "force-acknowledgement-hint": "Αν είναι ενεργοποιημένο, το εισερχόμενο μήνυμα επιβεβαιώνεται άμεσα. Η απάντηση του μοντέλου τοποθετείται στη συνέχεια στην ουρά ως ξεχωριστό νέο μήνυμα." } }, "timezone": { @@ -5625,7 +5779,10 @@ "too-small-value-zero": "Η τιμή πρέπει να είναι μεγαλύτερη από 0", "too-small-value-one": "Η τιμή πρέπει να είναι μεγαλύτερη από 1", "queue-size-is-limited-by-system-configuration": "Το μέγεθος της ουράς περιορίζεται επίσης από τη διαμόρφωση του συστήματος.", - "cassandra-tenant-limits-configuration": "Ερώτημα Cassandra για ενοικιαστή", + "cassandra-write-tenant-core-limits-configuration": "Ερωτήματα εγγραφής Cassandra μέσω Rest API", + "cassandra-read-tenant-core-limits-configuration": "Ερωτήματα ανάγνωσης Cassandra μέσω Rest API και WS τηλεμετρίας", + "cassandra-write-tenant-rule-engine-limits-configuration": "Ερωτήματα εγγραφής Cassandra τηλεμετρίας από Rule Engine", + "cassandra-read-tenant-rule-engine-limits-configuration": "Ερωτήματα ανάγνωσης Cassandra τηλεμετρίας από Rule Engine", "ws-limit-max-sessions-per-tenant": "Μέγιστος αριθμός συνεδριών ανά ενοικιαστή", "ws-limit-max-sessions-per-customer": "Μέγιστος αριθμός συνεδριών ανά πελάτη", "ws-limit-max-sessions-per-regular-user": "Μέγιστος αριθμός συνεδριών ανά χρήστη", @@ -5638,31 +5795,34 @@ "ws-limit-updates-per-session": "WS ενημερώσεις ανά συνεδρία", "rate-limits": { "add-limit": "Προσθήκη ορίου", - "advanced-settings": "Προχωρημένες ρυθμίσεις", + "and-also-less-than": "και επίσης μικρότερο από", + "advanced-settings": "Προηγμένες ρυθμίσεις", "edit-limit": "Επεξεργασία ορίου", - "but-less-than": "αλλά λιγότερο από", - "calculated-field-debug-event-rate-limit": "Σφάλματα πεδίου υπολογισμού", - "edit-calculated-field-debug-event-rate-limit": "Επεξεργασία ορίων σφαλμάτων πεδίου υπολογισμού", - "edit-transport-tenant-msg-title": "Επεξεργασία ορίων μηνυμάτων μεταφοράς ενοικιαστή", - "edit-transport-tenant-telemetry-msg-title": "Επεξεργασία ορίων τηλεμετρικών μηνυμάτων μεταφοράς ενοικιαστή", - "edit-transport-tenant-telemetry-data-points-title": "Επεξεργασία ορίων σημείων δεδομένων τηλεμετρίας μεταφοράς ενοικιαστή", - "edit-transport-device-msg-title": "Επεξεργασία ορίων μηνυμάτων μεταφοράς συσκευής", - "edit-transport-device-telemetry-msg-title": "Επεξεργασία ορίων τηλεμετρικών μηνυμάτων μεταφοράς συσκευής", - "edit-transport-device-telemetry-data-points-title": "Επεξεργασία ορίων σημείων δεδομένων τηλεμετρίας μεταφοράς συσκευής", - "edit-transport-gateway-msg-title": "Επεξεργασία ορίων μηνυμάτων μεταφοράς πύλης", - "edit-transport-gateway-telemetry-msg-title": "Επεξεργασία ορίων τηλεμετρικών μηνυμάτων μεταφοράς πύλης", - "edit-transport-gateway-telemetry-data-points-title": "Επεξεργασία ορίων σημείων δεδομένων τηλεμετρίας μεταφοράς πύλης", - "edit-transport-gateway-device-msg-title": "Επεξεργασία ορίων μηνυμάτων συσκευής πύλης", - "edit-transport-gateway-device-telemetry-msg-title": "Επεξεργασία ορίων τηλεμετρίας συσκευής πύλης", - "edit-transport-gateway-device-telemetry-data-points-title": "Επεξεργασία ορίων σημείων δεδομένων τηλεμετρίας συσκευής πύλης", - "edit-tenant-rest-limits-title": "Επεξεργασία ορίων αιτημάτων REST για τον ενοικιαστή", - "edit-customer-rest-limits-title": "Επεξεργασία ορίων αιτημάτων REST για πελάτη", - "edit-ws-limit-updates-per-session-title": "Επεξεργασία ορίων ενημερώσεων WS ανά συνεδρία", - "edit-cassandra-tenant-limits-configuration-title": "Επεξεργασία ορίων ερωτημάτων Cassandra για ενοικιαστή", - "edit-tenant-entity-export-rate-limit-title": "Επεξεργασία ορίων δημιουργίας εκδόσεων οντοτήτων", - "edit-tenant-entity-import-rate-limit-title": "Επεξεργασία ορίων φόρτωσης εκδόσεων οντοτήτων", - "edit-tenant-notification-request-rate-limit-title": "Επεξεργασία ορίων αιτημάτων ειδοποίησης", - "edit-tenant-notification-requests-per-rule-rate-limit-title": "Επεξεργασία ορίων αιτημάτων ειδοποίησης ανά κανόνα", + "calculated-field-debug-event-rate-limit": "Συμβάντα εντοπισμού σφαλμάτων υπολογιζόμενων πεδίων", + "edit-calculated-field-debug-event-rate-limit": "Επεξεργασία ορίων για συμβάντα εντοπισμού σφαλμάτων υπολογιζόμενων πεδίων", + "edit-transport-tenant-msg-title": "Επεξεργασία ορίων ταχύτητας για μηνύματα μεταφοράς ενοικιαστή", + "edit-transport-tenant-telemetry-msg-title": "Επεξεργασία ορίων ταχύτητας για τηλεμετρικά μηνύματα μεταφοράς ενοικιαστή", + "edit-transport-tenant-telemetry-data-points-title": "Επεξεργασία ορίων ταχύτητας για τηλεμετρικά δεδομένα μεταφοράς ενοικιαστή", + "edit-transport-device-msg-title": "Επεξεργασία ορίων ταχύτητας για μηνύματα μεταφοράς συσκευής", + "edit-transport-device-telemetry-msg-title": "Επεξεργασία ορίων ταχύτητας για τηλεμετρικά μηνύματα μεταφοράς συσκευής", + "edit-transport-device-telemetry-data-points-title": "Επεξεργασία ορίων ταχύτητας για τηλεμετρικά δεδομένα μεταφοράς συσκευής", + "edit-transport-gateway-msg-title": "Επεξεργασία ορίων ταχύτητας για μηνύματα μεταφοράς πύλης", + "edit-transport-gateway-telemetry-msg-title": "Επεξεργασία ορίων ταχύτητας για τηλεμετρικά μηνύματα μεταφοράς πύλης", + "edit-transport-gateway-telemetry-data-points-title": "Επεξεργασία ορίων ταχύτητας για τηλεμετρικά δεδομένα μεταφοράς πύλης", + "edit-transport-gateway-device-msg-title": "Επεξεργασία ορίων ταχύτητας για μηνύματα μεταφοράς συσκευών μέσω πύλης", + "edit-transport-gateway-device-telemetry-msg-title": "Επεξεργασία ορίων ταχύτητας για τηλεμετρικά μηνύματα συσκευών μέσω πύλης", + "edit-transport-gateway-device-telemetry-data-points-title": "Επεξεργασία ορίων ταχύτητας για τηλεμετρικά δεδομένα συσκευών μέσω πύλης", + "edit-tenant-rest-limits-title": "Επεξεργασία ορίων ταχύτητας αιτήσεων REST για τον ενοικιαστή", + "edit-customer-rest-limits-title": "Επεξεργασία ορίων ταχύτητας αιτήσεων REST για τον πελάτη", + "edit-ws-limit-updates-per-session-title": "Επεξεργασία ορίων WS ενημερώσεων ανά συνεδρία", + "edit-cassandra-write-tenant-core-limits-configuration": "Επεξεργασία ορίων ερωτημάτων εγγραφής Cassandra μέσω Rest API", + "edit-cassandra-read-tenant-core-limits-configuration": "Επεξεργασία ορίων ερωτημάτων ανάγνωσης Cassandra μέσω Rest API και WS τηλεμετρίας", + "edit-cassandra-write-tenant-rule-engine-limits-configuration": "Επεξεργασία ορίων εγγραφής τηλεμετρίας Cassandra από Rule Engine", + "edit-cassandra-read-tenant-rule-engine-limits-configuration": "Επεξεργασία ορίων ανάγνωσης τηλεμετρίας Cassandra από Rule Engine", + "edit-tenant-entity-export-rate-limit-title": "Επεξεργασία ορίων ταχύτητας για δημιουργία έκδοσης οντοτήτων", + "edit-tenant-entity-import-rate-limit-title": "Επεξεργασία ορίων ταχύτητας για φόρτωση έκδοσης οντοτήτων", + "edit-tenant-notification-request-rate-limit-title": "Επεξεργασία ορίων ταχύτητας αιτημάτων ειδοποιήσεων", + "edit-tenant-notification-requests-per-rule-rate-limit-title": "Επεξεργασία ορίων ταχύτητας αιτημάτων ειδοποιήσεων ανά κανόνα", "edit-edge-events-rate-limit": "Επεξεργασία ορίων συμβάντων άκρης", "edit-edge-events-per-edge-rate-limit": "Επεξεργασία ορίων συμβάντων ανά άκρη", "edge-events-rate-limit": "Συμβάντα άκρης", @@ -5680,21 +5840,22 @@ "per-seconds": "Ανά δευτερόλεπτα", "per-seconds-required": "Απαιτείται χρονικός ρυθμός.", "per-seconds-min": "Η ελάχιστη τιμή είναι 1.", - "rate-limits": "Όρια ρυθμού", + "per-seconds-duplicate": "Διπλότυπος χρονικός ρυθμός. Κάθε χρονικό διάστημα πρέπει να είναι μοναδικό.", + "rate-limits": "Όρια ταχύτητας", "remove-limit": "Αφαίρεση ορίου", "transport-tenant-msg": "Μηνύματα μεταφοράς ενοικιαστή", - "transport-tenant-telemetry-msg": "Τηλεμετρικά μηνύματα ενοικιαστή", - "transport-tenant-telemetry-data-points": "Σημεία τηλεμετρίας ενοικιαστή", + "transport-tenant-telemetry-msg": "Τηλεμετρικά μηνύματα μεταφοράς ενοικιαστή", + "transport-tenant-telemetry-data-points": "Τηλεμετρικά δεδομένα μεταφοράς ενοικιαστή", "transport-device-msg": "Μηνύματα μεταφοράς συσκευής", - "transport-device-telemetry-msg": "Τηλεμετρικά μηνύματα συσκευής", - "transport-device-telemetry-data-points": "Σημεία τηλεμετρίας συσκευής", + "transport-device-telemetry-msg": "Τηλεμετρικά μηνύματα μεταφοράς συσκευής", + "transport-device-telemetry-data-points": "Τηλεμετρικά δεδομένα μεταφοράς συσκευής", "transport-gateway-msg": "Μηνύματα μεταφοράς πύλης", - "transport-gateway-telemetry-msg": "Τηλεμετρικά μηνύματα πύλης", - "transport-gateway-telemetry-data-points": "Σημεία τηλεμετρίας πύλης", - "transport-gateway-device-msg": "Μηνύματα συσκευής πύλης", - "transport-gateway-device-telemetry-msg": "Τηλεμετρικά μηνύματα συσκευής πύλης", - "transport-gateway-device-telemetry-data-points": "Σημεία τηλεμετρίας συσκευής πύλης", - "sec": "δευτ." + "transport-gateway-telemetry-msg": "Τηλεμετρικά μηνύματα μεταφοράς πύλης", + "transport-gateway-telemetry-data-points": "Τηλεμετρικά δεδομένα μεταφοράς πύλης", + "transport-gateway-device-msg": "Μηνύματα συσκευών μεταφοράς μέσω πύλης", + "transport-gateway-device-telemetry-msg": "Τηλεμετρικά μηνύματα συσκευών μεταφοράς μέσω πύλης", + "transport-gateway-device-telemetry-data-points": "Τηλεμετρικά δεδομένα συσκευών μεταφοράς μέσω πύλης", + "sec": "δευτ" } }, "timeinterval": { @@ -5827,13 +5988,125 @@ "value": "Τιμή", "date": "Ημερομηνία", "show-date-time-interval": "Εμφάνιση χρονικού διαστήματος", - "show-date-time-interval-hint": "Εμφάνιση χρονικού διαστήματος σύμφωνα με την ομαδοποίηση δεδομένων.", + "show-date-time-interval-hint": "Εμφάνιση χρονικού διαστήματος σύμφωνα με τη συγχώνευση δεδομένων.", + "hide-zero-tooltip-values": "Απόκρυψη μηδενικών τιμών", "background-color": "Χρώμα φόντου", "background-blur": "Θόλωση φόντου" }, "unit": { + "set-unit-conversion": "Ορισμός μετατροπής μονάδων", + "unit-settings": { + "unit-settings": "Ρυθμίσεις μονάδων", + "source-unit": "Μονάδα προέλευσης", + "source-unit-hint": "Αυτή είναι η μονάδα της αποθηκευμένης τιμής. Η μονάδα από την οποία γίνεται η μετατροπή. Εισάγετε το σύμβολο που χρησιμοποιούν τα αρχικά σας δεδομένα (π.χ. m, km, ft, in).", + "target-metric-unit": "Στόχος σε μετρική μονάδα", + "target-metric-unit-hint": "Επιλέξτε σε ποια μετρική μονάδα (SI) θέλετε να μετατραπεί η αρχική σας τιμή (π.χ. cm, mm, km).", + "target-imperial-unit": "Στόχος σε αυτοκρατορική μονάδα", + "target-imperial-unit-hint": "Επιλέξτε σε ποια αυτοκρατορική μονάδα θέλετε να μετατραπεί η αρχική σας τιμή (π.χ. in, ft, yd).", + "target-hybrid-unit": "Στόχος σε υβριδική μονάδα", + "target-hybrid-unit-hint": "Επιλέξτε σε ποια υβριδική μονάδα θέλετε να μετατραπεί η αρχική σας τιμή (π.χ. cm, in, km). Οι υβριδικές μονάδες συνδυάζουν μετρικές ή αυτοκρατορικές μονάδες.", + "enable-unit-conversion": "Ενεργοποίηση μετατροπής μονάδων", + "enable-unit-conversion-hint": "Ενεργοποιήστε για να γίνει μετατροπή. Όταν είναι απενεργοποιημένο, η αρχική τιμή θα παραμείνει αμετάβλητη. Δεν είναι διαθέσιμο αν υπάρχει μόνο μία μονάδα στην αντίστοιχη ομάδα μέτρησης (π.χ. Φωτεινή ροή, AQI)." + }, + "unit-system": "Σύστημα μονάδων", + "unit-system-type": { + "AUTO": "Αυτόματο", + "METRIC": "Μετρικό", + "IMPERIAL": "Αυτοκρατορικό", + "HYBRID": "Υβριδικό" + }, + "measures": { + "absorbed-dose-rate": "Ρυθμός απορροφούμενης δόσης", + "acceleration": "Επιτάχυνση", + "acidity": "Οξύτητα", + "air-quality-index": "Δείκτης ποιότητας αέρα", + "amount-of-substance": "Ποσότητα ουσίας", + "angle": "Γωνία", + "angular-acceleration": "Γωνιακή επιτάχυνση", + "area": "Επιφάνεια", + "area-density": "Πυκνότητα επιφάνειας", + "capacitance": "Χωρητικότητα", + "catalytic-activity": "Καταλυτική δραστηριότητα", + "catalytic-concentration": "Καταλυτική συγκέντρωση", + "charge": "Φορτίο", + "current-density": "Πυκνότητα ρεύματος", + "data-transfer-rate": "Ρυθμός μεταφοράς δεδομένων", + "density": "Πυκνότητα", + "digital": "Ψηφιακό", + "dimension-ratio": "Αναλογία διαστάσεων", + "dynamic-viscosity": "Δυναμικό ιξώδες", + "earthquake-magnitude": "Μέγεθος σεισμού", + "electric-charge-density": "Πυκνότητα ηλεκτρικού φορτίου", + "electric-current": "Ηλεκτρικό ρεύμα", + "electric-dipole-moment": "Δίπολη ροπή", + "electric-field-strength": "Ένταση ηλεκτρικού πεδίου", + "electric-flux": "Ηλεκτρική ροή", + "electric-permittivity": "Ηλεκτρική επιτρεπτικότητα", + "electric-polarizability": "Ηλεκτρική πολωσιμότητα", + "electrical-conductance": "Ηλεκτρική αγωγιμότητα", + "electrical-conductivity": "Ηλεκτρική αγωγιμότητα (ειδική)", + "energy": "Ενέργεια", + "energy-density": "Πυκνότητα ενέργειας", + "force": "Δύναμη", + "frequency": "Συχνότητα", + "fuel-efficiency": "Απόδοση καυσίμου", + "heat-capacity": "Θερμοχωρητικότητα", + "illuminance": "Φωτισμός", + "inductance": "Επαγωγή", + "kinematic-viscosity": "Κινηματικό ιξώδες", + "length": "Μήκος", + "light-exposure": "Έκθεση στο φως", + "linear-charge-density": "Γραμμική πυκνότητα φορτίου", + "logarithmic-ratio": "Λογαριθμικός λόγος", + "luminous-efficacy": "Φωτεινή απόδοση", + "luminous-flux": "Φωτεινή ροή", + "luminous-intensity": "Φωτεινή ένταση", + "magnetic-field-gradient": "Κλίση μαγνητικού πεδίου", + "magnetic-flux": "Μαγνητική ροή", + "magnetic-flux-density": "Πυκνότητα μαγνητικής ροής", + "magnetic-moment": "Μαγνητική ροπή", + "magnetic-permeability": "Μαγνητική διαπερατότητα", + "mass": "Μάζα", + "mass-fraction": "Κλασματική μάζα", + "molar-concentration": "Μοριακή συγκέντρωση", + "molar-energy": "Μοριακή ενέργεια", + "molar-heat-capacity": "Μοριακή θερμοχωρητικότητα", + "molar-mass": "Μοριακή μάζα", + "number-concentration": "Αριθμητική συγκέντρωση", + "parts-per-million": "Μέρη ανά εκατομμύριο", + "power": "Ισχύς", + "power-density": "Πυκνότητα ισχύος", + "pressure": "Πίεση", + "radiance": "Ακτινοβολία", + "radiant-intensity": "Ακτινική ένταση", + "radiation-dose": "Δόση ακτινοβολίας", + "radioactive-decay": "Ραδιενεργή αποσύνθεση", + "radioactivity": "Ραδιενέργεια", + "radioactivity-concentration": "Συγκέντρωση ραδιενέργειας", + "reciprocal-length": "Αντίστροφο μήκος", + "resistance": "Αντίσταση", + "reynolds-number": "Αριθμός Reynolds", + "signal-level": "Επίπεδο σήματος", + "solid-angle": "Στερεά γωνία", + "specific-energy": "Ειδική ενέργεια", + "specific-heat-capacity": "Ειδική θερμοχωρητικότητα", + "specific-humidity": "Ειδική υγρασία", + "specific-volume": "Ειδικός όγκος", + "speed": "Ταχύτητα", + "surface-charge-density": "Επιφανειακή πυκνότητα φορτίου", + "surface-tension": "Επιφανειακή τάση", + "temperature": "Θερμοκρασία", + "thermal-conductivity": "Θερμική αγωγιμότητα", + "time": "Χρόνος", + "torque": "Ροπή", + "turbidity": "Θολότητα", + "voltage": "Τάση", + "volume": "Όγκος", + "volume-flow": "Ροή όγκου" + }, "millimeter": "Χιλιοστόμετρο", "centimeter": "Εκατοστόμετρο", + "decimeter": "Δεκατόμετρο", "angstrom": "Άνγκστρομ", "nanometer": "Νανομέτρο", "micrometer": "Μικρόμετρο", @@ -5841,6 +6114,7 @@ "kilometer": "Χιλιόμετρο", "inch": "Ίντσα", "foot": "Πόδι", + "foot-us": "Πόδι (μέτρηση ΗΠΑ)", "yard": "Γιάρδα", "mile": "Μίλι", "nautical-mile": "Ναυτικό μίλι", @@ -5887,6 +6161,7 @@ "cubic-foot": "Κυβικό πόδι", "cubic-yard": "Κυβική γιάρδα", "fluid-ounce": "Υγρή ουγγιά", + "fluid-ounce-per-second": "Υγρή ουγγιά ανά δευτερόλεπτο", "pint": "Πίντα", "quart": "Κουάρτο", "gallon": "Γαλόνι", @@ -5904,10 +6179,14 @@ "percent": "Ποσοστό", "meter-per-second": "Μέτρο ανά δευτερόλεπτο", "kilometer-per-hour": "Χιλιόμετρα ανά ώρα", - "foot-per-second": "Πόδια ανά δευτερόλεπτο", - "mile-per-hour": "Μίλια ανά ώρα", + "foot-per-second": "Πόδι ανά δευτερόλεπτο", + "foot-per-minute": "Πόδι ανά λεπτό", + "mile-per-hour": "Μίλι ανά ώρα", "knot": "Κόμβος", + "inch-per-second": "Ίντσα ανά δευτερόλεπτο", + "inch-per-hour": "Ίντσα ανά ώρα", "millimeters-per-minute": "Χιλιοστά ανά λεπτό", + "meter-per-minute": "Μέτρο ανά λεπτό", "kilometer-per-hour-squared": "Χιλιόμετρα ανά ώρα τετράγωνο", "foot-per-second-squared": "Πόδια ανά δευτερόλεπτο τετράγωνο", "pascal": "Πασκάλ", @@ -5924,6 +6203,7 @@ "newton-per-meter": "Νιούτον ανά μέτρο", "atmospheres": "Ατμόσφαιρες", "pounds-per-square-inch": "Λίβρες ανά τετραγωνική ίντσα", + "kilopound-per-square-inch": "Χιλιολίβρα ανά τετραγωνική ίντσα", "torr": "Τορρ", "inches-of-mercury": "Ίντσες υδραργύρου", "pascal-per-square-meter": "Πασκάλ ανά τετραγωνικό μέτρο", @@ -5940,11 +6220,17 @@ "kilojoule": "Κιλοτζάουλ", "megajoule": "Μεγατζάουλ", "gigajoule": "Γιγκατζάουλ", - "watt-hour": "Βατ-ώρα", + "watt-hour": "Βατώρα", + "watt-minute": "Βατλεπτό", "kilowatt-hour": "Κιλοβατώρα", + "milliwatt-hour": "Μιλιβατώρα", + "megawatt-hour": "Μεγαβατώρα", + "gigawatt-hour": "Γιγαβατώρα", "electron-volts": "Ηλεκτρονιοβόλτ", "joules-per-coulomb": "Τζάουλ ανά κουλόμπ", - "british-thermal-unit": "Βρετανικές θερμικές μονάδες", + "british-thermal-unit": "Βρετανική θερμική μονάδα", + "thousand-british-thermal-unit": "Χίλιες βρετανικές θερμικές μονάδες", + "million-british-thermal-unit": "Εκατομμύριο βρετανικές θερμικές μονάδες", "foot-pound": "Πόδι-λίβρα", "calorie": "Θερμίδα", "small-calorie": "Μικρή θερμίδα", @@ -5975,10 +6261,20 @@ "watt-per-square-inch": "Βατ ανά τετραγωνική ίντσα", "kilowatt-per-square-inch": "Κιλοβάτ ανά τετραγωνική ίντσα", "horsepower": "Ίππος", - "btu-per-hour": "BTU ανά ώρα", + "btu-per-hour": "Βρετανικές θερμικές μονάδες ανά ώρα", + "btu-per-second": "Βρετανικές θερμικές μονάδες ανά δευτερόλεπτο", + "btu-per-day": "Βρετανικές θερμικές μονάδες ανά ημέρα", + "mbtu-per-hour": "Χίλιες βρετανικές θερμικές μονάδες ανά ώρα", + "mbtu-per-second": "Χίλιες βρετανικές θερμικές μονάδες ανά δευτερόλεπτο", + "mbtu-per-day": "Χίλιες βρετανικές θερμικές μονάδες ανά ημέρα", + "mmbtu-per-hour": "Εκατομμύριο βρετανικές θερμικές μονάδες ανά ώρα", + "mmbtu-per-second": "Εκατομμύριο βρετανικές θερμικές μονάδες ανά δευτερόλεπτο", + "mmbtu-per-day": "Εκατομμύριο βρετανικές θερμικές μονάδες ανά ημέρα", + "foot-pound-per-second": "Πόδι-λίβρα ανά δευτερόλεπτο", "coulomb": "Κουλόμπ", "millicoulomb": "Μιλικουλόμπ", "microcoulomb": "Μικροκουλόμπ", + "nanocoulomb": "Νανοκουλόμπ", "picocoulomb": "Πικοκουλόμπ", "coulomb-per-meter": "Κουλόμπ ανά μέτρο", "coulomb-per-cubic-meter": "Κουλόμπ ανά κυβικό μέτρο", @@ -5995,7 +6291,7 @@ "square-mile": "Τετραγωνικό μίλι", "are": "Άρ", "barn": "Μπαρν", - "circular-inch": "Κυκλική Ίντσα", + "circular-inch": "Κυκλική ίντσα", "milliampere-hour": "Μιλιαμπερώρια", "ampere-hours": "Αμπερώρια", "kiloampere-hours": "Κιλοαμπερώρια", @@ -6004,17 +6300,24 @@ "microampere": "Μικροαμπέρ", "milliampere": "Μιλιαμπέρ", "ampere": "Αμπέρ", + "kiloampere": "Κιλοαμπέρ", + "megaampere": "Μεγααμπέρ", + "gigaampere": "Γιγααμπέρ", "microampere-per-square-centimeter": "Μικροαμπέρ ανά τετραγωνικό εκατοστό", "ampere-per-square-meter": "Αμπέρ ανά τετραγωνικό μέτρο", "ampere-per-meter": "Αμπέρ ανά μέτρο", - "oersted": "Όερστεντ", - "bohr-magneton": "Μπορ μαγνητόνιο", - "ampere-meter-squared": "Αμπέρ-μέτρο τετραγωνικό", + "oersted": "Οέρστεντ", + "bohr-magneton": "Μαγνητόνιο Bohr", + "ampere-meter-squared": "Αμπέρ-μέτρα τετράγωνα", "nanovolt": "Νανοβόλτ", "picovolt": "Πικοβόλτ", + "millivolt": "Μιλιβόλτ", + "microvolt": "Μικροβόλτ", "volt": "Βολτ", - "dbmV": "dBmV", - "dbm": "dBm", + "kilovolt": "Κιλοβόλτ", + "megavolt": "Μεγαβόλτ", + "dbmV": "Ντεσιμπέλ βολτ", + "dbm": "Ντεσιμπέλ μιλιβάτ", "volt-meter": "Βολτόμετρο", "kilovolt-meter": "Κιλοβολτόμετρο", "megavolt-meter": "Μεγαβολτόμετρο", @@ -6023,23 +6326,25 @@ "nanovolt-meter": "Νανοβολτόμετρο", "ohm": "Ωμ", "microohm": "Μικροωμ", - "milliohm": "Μιλιωμ", + "milliohm": "Μιλιοωμ", "kilohm": "Κιλοωμ", "megohm": "Μεγαωμ", "gigohm": "Γιγαωμ", - "hertz": "Χερτζ", + "millihertz": "Μιλιχέρτζ", + "hertz": "Χέρτζ", "kilohertz": "Κιλοχέρτζ", "megahertz": "Μεγαχέρτζ", "gigahertz": "Γιγαχέρτζ", + "terahertz": "Τεραχέρτζ", "rpm": "Στροφές ανά λεπτό", "candela-per-square-meter": "Καντέλα ανά τετραγωνικό μέτρο", "candela": "Καντέλα", "lumen": "Λούμεν", "lux": "Λουξ", - "foot-candle": "Πόδι-κερί", + "foot-candle": "Πόδι-καντέλα", "lumen-per-square-meter": "Λούμεν ανά τετραγωνικό μέτρο", - "lux-second": "Λουξ-δευτερόλεπτο", - "lumen-second": "Λούμεν-δευτερόλεπτο", + "lux-second": "Λουξ δευτερόλεπτο", + "lumen-second": "Λούμεν δευτερόλεπτο", "lumens-per-watt": "Λούμεν ανά βατ", "mole": "Μόλιο", "nanomole": "Νανομόλιο", @@ -6047,12 +6352,12 @@ "millimole": "Μιλιμόλιο", "kilomole": "Κιλομόλιο", "mole-per-cubic-meter": "Μόλιο ανά κυβικό μέτρο", - "rssi": "RSSI", + "rssi": "Δείκτης ισχύος ληφθέντος σήματος", "ppm": "Μέρη ανά εκατομμύριο", "ppb": "Μέρη ανά δισεκατομμύριο", "micrograms-per-cubic-meter": "Μικρογραμμάρια ανά κυβικό μέτρο", - "aqi": "Δείκτης Ποιότητας Αέρα (AQI)", - "gram-per-cubic-meter": "Γραμμάριο ανά κυβικό μέτρο", + "aqi": "Δείκτης ποιότητας αέρα (AQI)", + "gram-per-cubic-meter": "Γραμμάρια ανά κυβικό μέτρο", "gram-per-kilogram": "Ειδική υγρασία", "millimeters-per-second": "Χιλιοστά ανά δευτερόλεπτο", "neper": "Νέπερ", @@ -6075,38 +6380,38 @@ "becquerels-per-second": "Μπεκερέλ ανά δευτερόλεπτο", "curies-per-second": "Κιουρί ανά δευτερόλεπτο", "gy-per-second": "Γκρέι ανά δευτερόλεπτο", - "watt-per-steradian": "Βατ ανά στερακτίδιο", - "watt-per-square-metre-steradian": "Βατ ανά τετραγωνικό μέτρο-στερακτίδιο", + "watt-per-steradian": "Βατ ανά στερακτιάνιο", + "watt-per-square-metre-steradian": "Βατ ανά τετραγωνικό μέτρο-στερακτιάνιο", "ph-level": "Επίπεδο pH", "turbidity": "Θολότητα", "mg-per-liter": "Μιλιγραμμάρια ανά λίτρο", "microsiemens-per-centimeter": "Μικροσίμενς ανά εκατοστό", "millisiemens-per-meter": "Μιλισίμενς ανά μέτρο", "siemens-per-meter": "Σίμενς ανά μέτρο", - "kilogram-per-cubic-meter": "Κιλό ανά κυβικό μέτρο", - "gram-per-cubic-centimeter": "Γραμμάριο ανά κυβικό εκατοστό", - "kilogram-per-square-meter": "Κιλό ανά τετραγωνικό μέτρο", + "kilogram-per-cubic-meter": "Κιλά ανά κυβικό μέτρο", + "gram-per-cubic-centimeter": "Γραμμάρια ανά κυβικό εκατοστό", + "kilogram-per-square-meter": "Κιλά ανά τετραγωνικό μέτρο", "milligram-per-milliliter": "Μιλιγραμμάριο ανά χιλιοστόλιτρο", "milligram-per-cubic-meter": "Μιλιγραμμάριο ανά κυβικό μέτρο", "pound-per-cubic-foot": "Λίβρα ανά κυβικό πόδι", - "ounces-per-cubic-inch": "Ουγγιά ανά κυβική ίντσα", + "ounces-per-cubic-inch": "Ουγγιές ανά κυβική ίντσα", "tons-per-cubic-yard": "Τόνοι ανά κυβική γιάρδα", "particle-density": "Πυκνότητα σωματιδίων", "kilometers-per-liter": "Χιλιόμετρα ανά λίτρο", "miles-per-gallon": "Μίλια ανά γαλόνι", - "liters-per-100-km": "Λίτρα ανά 100 χιλιόμετρα", + "liters-per-100-km": "Λίτρα ανά 100 χλμ", "gallons-per-mile": "Γαλόνια ανά μίλι", "liters-per-hour": "Λίτρα ανά ώρα", "gallons-per-hour": "Γαλόνια ανά ώρα", - "beats-per-minute": "Κτύποι ανά λεπτό", + "beats-per-minute": "Χτύποι ανά λεπτό", "millimeters-of-mercury": "Χιλιοστά υδραργύρου", "milligrams-per-deciliter": "Μιλιγραμμάρια ανά δεκατόλιτρο", - "g-force": "Επιτάχυνση βαρύτητας (g-force)", + "g-force": "Δύναμη g", "kilonewton": "Κιλονιούτον", "kilogram-force": "Δύναμη κιλού", "pound-force": "Δύναμη λίβρας", - "kilopound-force": "Κιλολίβρα-δύναμη", - "dyne": "Ντίν", + "kilopound-force": "Δύναμη χιλίων λιβρών", + "dyne": "Ντάιν", "poundal": "Πάουνταλ", "kip": "Κιπ", "gal": "Γκαλ", @@ -6116,6 +6421,9 @@ "millibars": "Μιλιμπάρ", "inch-of-mercury": "Ίντσα υδραργύρου", "richter-scale": "Κλίμακα Ρίχτερ", + "nanosecond": "Νανοδευτερόλεπτο", + "microsecond": "Μικροδευτερόλεπτο", + "millisecond": "Χιλιοστό του δευτερολέπτου", "second": "Δευτερόλεπτο", "minute": "Λεπτό", "hour": "Ώρα", @@ -6130,29 +6438,33 @@ "liter-per-minute": "Λίτρο ανά λεπτό", "gallons-per-minute": "Γαλόνια ανά λεπτό", "cubic-foot-per-second": "Κυβικό πόδι ανά δευτερόλεπτο", - "milliliters-per-minute": "Χιλιοστόλιτρα ανά λεπτό", - "bit": "Bit", - "byte": "Byte", - "kilobyte": "Kilobyte", - "megabyte": "Megabyte", - "gigabyte": "Gigabyte", - "terabyte": "Terabyte", - "petabyte": "Petabyte", - "exabyte": "Exabyte", - "zettabyte": "Zettabyte", - "yottabyte": "Yottabyte", - "bit-per-second": "Bit ανά δευτερόλεπτο", - "kilobit-per-second": "Kilobit ανά δευτερόλεπτο", - "megabit-per-second": "Megabit ανά δευτερόλεπτο", - "gigabit-per-second": "Gigabit ανά δευτερόλεπτο", - "terabit-per-second": "Terabit ανά δευτερόλεπτο", - "byte-per-second": "Byte ανά δευτερόλεπτο", - "kilobyte-per-second": "Kilobyte ανά δευτερόλεπτο", - "megabyte-per-second": "Megabyte ανά δευτερόλεπτο", - "gigabyte-per-second": "Gigabyte ανά δευτερόλεπτο", + "milliliters-per-minute": "Μιλιλίτρα ανά λεπτό", + "cubic-decimeter-per-second": "Κυβικό δεκατόλιτρο ανά δευτερόλεπτο", + "bit": "Μπιτ", + "byte": "Μπάιτ", + "kilobyte": "Κιλομπάιτ", + "megabyte": "Μεγαμπάιτ", + "gigabyte": "Γιγκαμπάιτ", + "terabyte": "Τεραμπάιτ", + "petabyte": "Πεταμπάιτ", + "exabyte": "Εξαμπάιτ", + "zettabyte": "Ζεταμπάιτ", + "yottabyte": "Γιοταμπάιτ", + "bit-per-second": "Μπιτ ανά δευτερόλεπτο", + "kilobit-per-second": "Κιλομπιτ ανά δευτερόλεπτο", + "megabit-per-second": "Μεγαμπιτ ανά δευτερόλεπτο", + "gigabit-per-second": "Γιγκαμπιτ ανά δευτερόλεπτο", + "terabit-per-second": "Τεραμπιτ ανά δευτερόλεπτο", + "byte-per-second": "Μπάιτ ανά δευτερόλεπτο", + "kilobyte-per-second": "Κιλομπάιτ ανά δευτερόλεπτο", + "megabyte-per-second": "Μεγαμπάιτ ανά δευτερόλεπτο", + "gigabyte-per-second": "Γιγκαμπάιτ ανά δευτερόλεπτο", "degree": "Μοίρα", "radian": "Ακτίνιο", - "gradian": "Γκραντ", + "gradian": "Γκράντ", + "arcminute": "Γωνιακό λεπτό", + "arcsecond": "Γωνιακό δευτερόλεπτο", + "milliradian": "Μιλιράντιο", "revolution": "Περιστροφή", "siemens": "Σίμενς", "millisiemens": "Μιλισίμενς", @@ -6167,7 +6479,7 @@ "picofarad": "Πικοφαράντ", "kilofarad": "Κιλοφαράντ", "megafarad": "Μεγαφαράντ", - "gigafarad": "Γιγαφαράντ", + "gigafarad": "Γιγκαφαράντ", "terfarad": "Τεραφαράντ", "farad-per-meter": "Φαράντ ανά μέτρο", "tesla": "Τέσλα", @@ -6188,8 +6500,8 @@ "square-foot-per-second": "Τετραγωνικό πόδι ανά δευτερόλεπτο", "square-inch-per-second": "Τετραγωνική ίντσα ανά δευτερόλεπτο", "pascal-second": "Πασκάλ-δευτερόλεπτο", - "centipoise": "Σεντιπόιζ", - "poise": "Πόιζ", + "centipoise": "Σεντιπόις", + "poise": "Πόις", "reynolds": "Ρέινολντς", "pound-per-foot-hour": "Λίβρα ανά πόδι-ώρα", "newton-second-per-square-meter": "Νιούτον-δευτερόλεπτο ανά τετραγωνικό μέτρο", @@ -6202,30 +6514,32 @@ "weber": "Βέμπερ", "microweber": "Μικροβέμπερ", "milliweber": "Μιλιβέμπερ", - "gauss-square-centimeter": "Γκάους-τετραγωνικό εκατοστό", - "kilogauss-square-centimeter": "Κιλογκάους-τετραγωνικό εκατοστό", + "gauss-square-centimeter": "Γκάους τετραγωνικό εκατοστό", + "kilogauss-square-centimeter": "Κιλογκάους τετραγωνικό εκατοστό", "henry": "Χένρι", "millihenry": "Μιλιχένρι", "microhenry": "Μικροχένρι", "nanohenry": "Νανοχένρι", "henry-per-meter": "Χένρι ανά μέτρο", - "tesla-meter-per-ampere": "Τέσλα μέτρο ανά Αμπέρ", - "gauss-per-oersted": "Γκάους ανά Όερστεντ", - "kilogram-per-mole": "Κιλό ανά μολ", - "gram-per-mole": "Γραμμάριο ανά μολ", - "milligram-per-mole": "Μιλιγραμμάριο ανά μολ", - "joule-per-mole": "Τζάουλ ανά μολ", - "joule-per-mole-kelvin": "Τζάουλ ανά μολ-Κέλβιν", + "tesla-meter-per-ampere": "Τέσλα μέτρο ανά αμπέρ", + "gauss-per-oersted": "Γκάους ανά Έρστεντ", + "kilogram-per-mole": "Κιλά ανά μόλιο", + "gram-per-mole": "Γραμμάρια ανά μόλιο", + "milligram-per-mole": "Μιλιγραμμάρια ανά μόλιο", + "joule-per-mole": "Τζάουλ ανά μόλιο", + "joule-per-mole-kelvin": "Τζάουλ ανά μόλιο-Κέλβιν", "millivolts-per-meter": "Μιλιβόλτ ανά μέτρο", "volts-per-meter": "Βόλτ ανά μέτρο", "kilovolts-per-meter": "Κιλοβόλτ ανά μέτρο", "radian-per-second": "Ακτίνιο ανά δευτερόλεπτο", "radian-per-second-squared": "Ακτίνιο ανά δευτερόλεπτο τετράγωνο", "revolutions-per-minute-per-second": "Γωνιακή επιτάχυνση", - "deg-per-second": "μοίρες/δευτ.", + "deg-per-second": "Μοίρες ανά δευτερόλεπτο", + "rotation-per-minute": "Περιστροφές ανά λεπτό", "degrees-brix": "Μοίρες Brix", "katal": "Κατάλ", - "katal-per-cubic-metre": "Κατάλ ανά κυβικό μέτρο" + "katal-per-cubic-metre": "Κατάλ ανά κυβικό μέτρο", + "paris-inch": "Ίντσα Παρισιού" }, "user": { "user": "Χρήστης", @@ -7775,6 +8089,18 @@ "fill-area-opacity": "Διαφάνεια γεμίσματος περιοχής", "range-chart-style": "Στυλ διαγράμματος εύρους" }, + "knob": { + "behavior": "Συμπεριφορά", + "initial-value": "Αρχική τιμή", + "initial-value-hint": "Ενέργεια για λήψη της αρχικής τιμής του περιστροφικού ρυθμιστή.", + "on-value-change": "Κατά την αλλαγή τιμής", + "on-value-change-hint": "Ενέργεια που ενεργοποιείται όταν αλλάζει η τιμή του περιστροφικού ρυθμιστή.", + "range": "Εύρος", + "min": "ελάχ.", + "max": "μέγ.", + "value": "Τιμή", + "fallback-initial-value": "Εναλλακτική αρχική τιμή" + }, "rpc": { "value-settings": "Ρυθμίσεις τιμής", "initial-value": "Αρχική τιμή", @@ -7831,9 +8157,7 @@ "led-status-value-timeseries": "Χρονοσειρά συσκευής με τιμή κατάστασης LED", "check-status-method": "RPC μέθοδος ελέγχου κατάστασης συσκευής", "parse-led-status-value-function": "Συνάρτηση ανάλυσης τιμής κατάστασης LED", - "knob-title": "Τίτλος περιστροφικού ελεγκτή", - "min-value": "Ελάχιστη τιμή", - "max-value": "Μέγιστη τιμή" + "knob-title": "Τίτλος περιστροφικού ελεγκτή" }, "maps": { "map-type": { @@ -8688,7 +9012,11 @@ "radar-axis": "Άξονας ραντάρ", "axis-label": "Ετικέτα άξονα", "ticks-label": "Ετικέτα διαβαθμίσεων", - "radar-chart-style": "Στυλ διαγράμματος ραντάρ" + "radar-chart-style": "Στυλ διαγράμματος ραντάρ", + "max-axes-scaling": "Μέγιστη κλίμακα αξόνων", + "max-axes-scaling-hint": "Επιλέξτε αν κάθε άξονας του ραντάρ έχει τη δική του μέγιστη τιμή (Διαφορετική) ή αν μοιράζονται τη μέγιστη τιμή όλων των αξόνων με βάση το σύνολο δεδομένων του widget (Κοινή).", + "separate": "Διαφορετική", + "common": "Κοινή" }, "time-series-chart": { "chart": "Διάγραμμα", diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index a7396ad49e..f317e61529 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -496,24 +496,26 @@ "number-of-codes-pattern": "Number of codes must be a positive integer.", "number-of-codes-required": "Number of codes is required.", "provider": "Provider", - "retry-verification-code-period": "Retry verification code period (sec)", + "retry-verification-code-period": "Retry verification code period", "retry-verification-code-period-pattern": "Minimal period time is 5 sec", "retry-verification-code-period-required": "Retry verification code period is required.", - "total-allowed-time-for-verification": "Total allowed time for verification (sec)", + "total-allowed-time-for-verification": "Total allowed time for verification", "total-allowed-time-for-verification-pattern": "Minimal total allowed time is 60 sec", "total-allowed-time-for-verification-required": "Total allowed time is required.", "use-system-two-factor-auth-settings": "Use system two factor auth settings", "verification-code-check-rate-limit": "Verification code check rate limit", - "verification-code-lifetime": "Verification code lifetime (sec)", + "verification-code-lifetime": "Verification code lifetime", "verification-code-lifetime-pattern": "Verification code lifetime must be a positive integer.", "verification-code-lifetime-required": "Verification code lifetime is required.", "verification-message-template": "Verification message template", "verification-limitations": "Verification limitations", "verification-message-template-pattern": "Verification message need to contains pattern: ${code}", "verification-message-template-required": "Verification message template is required.", - "within-time": "Within time (sec)", + "within-time": "Within time", "within-time-pattern": "Time must be a positive integer.", - "within-time-required": "Time is required." + "within-time-required": "Time is required.", + "force-2fa": "Force two-factor authentication", + "enforce-for": "Enforce for" }, "jwt": { "security-settings": "JWT security settings", @@ -865,15 +867,18 @@ "api-features": "API features", "api-usage": "API usage", "alarm": "Alarm", - "alarms-created": "Alarms created", + "alarms-created": "Created alarms", "queue-stats": "Queue Stats", "processing-failures-and-timeouts": "Processing Failures and Timeouts", "exceptions": "Exceptions", - "alarms-created-daily-activity": "Alarms created daily activity", - "alarms-created-hourly-activity": "Alarms created hourly activity", - "alarms-created-monthly-activity": "Alarms created monthly activity", + "alarms-created-daily-activity": "Created alarms daily activity", + "alarms-created-hourly-activity": "Created alarms hourly activity", + "alarms-created-monthly-activity": "Created alarms monthly activity", "data-points": "Data points", "data-points-storage-days": "Data points storage days", + "data-points-storage-days-hourly-activity": "Data points storage days hourly activity", + "data-points-storage-days-daily-activity": "Data points storage days daily activity", + "data-points-storage-days-monthly-activity": "Data points storage days monthly activity", "device-api": "Device API", "email": "Email", "email-messages": "Email messages", @@ -899,14 +904,15 @@ "processing-timeouts": "${entityName} Processing Timeouts", "rule-chain": "Rule Chain", "rule-engine": "Rule Engine", - "rule-engine-daily-activity": "Rule Engine daily activity", "rule-engine-executions": "Rule Engine executions", "rule-engine-hourly-activity": "Rule Engine hourly activity", + "rule-engine-daily-activity": "Rule Engine daily activity", "rule-engine-monthly-activity": "Rule Engine monthly activity", "rule-engine-statistics": "Rule Engine Statistics", "rule-node": "Rule Node", "sms": "SMS", "sms-messages": "SMS messages", + "sms-messages-hourly-activity": "SMS messages hourly activity", "sms-messages-daily-activity": "SMS messages daily activity", "sms-messages-monthly-activity": "SMS messages monthly activity", "successful": "${entityName} Successful", @@ -916,13 +922,40 @@ "telemetry-persistence-hourly-activity": "Telemetry persistence hourly activity", "telemetry-persistence-monthly-activity": "Telemetry persistence monthly activity", "transport": "Transport", + "transport-msg-hourly-activity": "Transport messages hourly activity", + "transport-msg-daily-activity": "Transport messages daily activity", + "transport-msg-monthly-activity": "Transport messages monthly activity", "transport-daily-activity": "Transport daily activity", "transport-data-points": "Transport data points", - "transport-hourly-activity": "Transport hourly activity", - "transport-messages": "Transport messages", - "transport-monthly-activity": "Transport monthly activity", + "transport-data-points-hourly-activity": "Transport data points hourly activity", + "transport-data-points-daily-activity": "Transport data points daily activity", + "transport-data-points-monthly-activity": "Transport data points monthly activity", "view-details": "View details", - "view-statistics": "View statistics" + "view-statistics": "View statistics", + "transport-messages": "Transport messages", + "transport-messages-hourly-activity": "Transport messages hourly activity", + "transport-data-point-hourly-activity": "Transport data point hourly activity", + "javascript-function-executions": "JavaScript function executions", + "javascript-function-executions-hourly-activity": "JavaScript function executions hourly activity", + "javascript-function-executions-daily-activity": "JavaScript function executions daily activity", + "javascript-function-executions-monthly-activity": "JavaScript function executions monthly activity", + "tbel-function-executions": "TBEL function executions", + "tbel-function-executions-hourly-activity": "TBEL function executions hourly activity", + "tbel-function-executions-daily-activity": "TBEL function executions daily activity", + "tbel-function-executions-monthly-activity": "TBEL function executions monthly activity", + "created-reports": "Created reports", + "created-reports-hourly-activity": "Created reports hourly activity", + "created-reports-daily-activity": "Created reports daily activity", + "created-reports-monthly-activity": "Created reports monthly activity", + "emails": "Emails", + "emails-hourly-activity": "Emails hourly activity", + "emails-daily-activity": "Emails daily activity", + "emails-monthly-activity": "Emails monthly activity", + "status": { + "enabled": "Enabled", + "disabled": "Disabled", + "warning": "Warning" + } }, "api-limit": { "cassandra-write-queries-core": "Rest API Cassandra write queries", @@ -1021,15 +1054,23 @@ "selected-fields": "{ count, plural, =1 {1 calculated field} other {# calculated fields} } selected", "type": { "simple": "Simple", - "script": "Script" + "script": "Script", + "geofencing" : "Geofencing", + "propagation": "Propagation", + "related-entities-aggregation": "Related entities aggregation", + "related-entities-aggregation-hint": "Aggregation of data from related entities", + "time-series-data-aggregation": "Time series data aggregation", + "time-series-data-aggregation-hint": "Aggregation of historical data from a current entity" }, "arguments": "Arguments", "decimals-by-default": "Decimals by default", "debugging": "Calculated field debugging", "argument-name": "Argument name", + "name": "Name", "datasource": "Datasource", "add-argument": "Add argument", "test-script-function": "Test script function", + "test-expression-function": "Test expression function", "no-arguments": "No arguments configured", "argument-settings": "Argument settings", "argument-current": "Current entity", @@ -1038,6 +1079,8 @@ "argument-asset": "Asset", "argument-customer": "Customer", "argument-tenant": "Current tenant", + "argument-owner": "Current owner", + "argument-relation-query": "Related entities", "argument-type": "Argument type", "see-debug-events": "See debug events", "attribute": "Attribute", @@ -1052,6 +1095,7 @@ "shared-attributes": "Shared attributes", "attribute-key": "Attribute key", "default-value": "Default value", + "default-value-required": "Default value is required.", "limit": "Max values", "time-window": "Time window", "customer-name": "Customer name", @@ -1071,8 +1115,126 @@ "delete-multiple-text": "Be careful, after the confirmation all selected calculated fields will be removed and all related data will become unrecoverable.", "test-with-this-message": "Test with this message", "use-latest-timestamp": "Use latest timestamp", + "entity-coordinates": "Entity coordinates", + "latitude-time-series-key": "Latitude time series key", + "latitude-time-series-key-required": "Latitude time series key is required.", + "longitude-time-series-key": "Longitude time series key", + "longitude-time-series-key-required": "Longitude time series key is required.", + "geofencing-zone-groups": "Geofencing zone groups", + "geofencing-zone-groups-settings": "Geofencing zone group settings", + "target-zone": "Target zone", + "perimeter-key": "Perimeter key", + "report-strategy": "Report strategy", + "no-zone-configured": "No zone group configured", + "no-zone-configured-required": "At least one zone group must be configured.", + "add-zone-group": "Add zone group", + "report-transition-event-only": "Transition events only", + "report-presence-status-only": "Presence status only", + "report-transition-event-and-presence": "Presence status and transition events", + "perimeter-attribute-key": "Perimeter attribute key", + "perimeter-attribute-key-required": "Perimeter attribute key is required.", + "perimeter-attribute-key-pattern": "Perimeter attribute key is invalid.", + "entity-zone-relationship": "Path from Entity to Zones *", + "direction": "Relation direction", + "direction-from": "From entity to zone", + "direction-to": "From zone to entity", + "relation-type": "Relation type", + "create-relation-with-matched-zones": "Create relations for source entity with matched zones", + "relation-level": "Relation level", + "fetch-last-available-level": "Fetch last available level only", + "zone-group-refresh-interval": "Zone groups refresh interval", + "copy-zone-group-name": "Copy zone group name", + "open-details-page": "Open entity details page", + "level": "Level", + "direction-level": "Direction", + "direction-up": "Up", + "direction-up-parent": "Up to parent", + "direction-down": "Down", + "direction-down-child": "Down to child", + "add-level": "Add level", + "delete-level": "Delete level", + "no-level": "No level configured", + "levels-required": "At least one level must be configured.", + "max-allowed-levels-error": "Relation level exceeds the maximum allowed.", + "propagation-path-related-entities": "Propagation path to related entities", + "propagate-type": { + "arguments-only": "Arguments only", + "expression-result": "Expression result" + }, + "data-propagate": "Data to propagate", + "output-key": "Output key", + "copy-output-key": "Copy output key", + "aggregation-path-related-entities": "Aggregation path to related entities", + "deduplication-interval": "Deduplication interval", + "deduplication-interval-min": "Deduplication interval should be at least {{ sec }} second.", + "deduplication-interval-required": "Deduplication interval is required.", + "metrics": { + "metrics": "Metrics", + "metrics-empty": "At least one metric must be configured.", + "metric-name": "Metric name", + "copy-metric-name": "Copy metric name", + "aggregation": "Aggregation", + "aggregation-type": { + "avg": "Average", + "min": "Minimum", + "max": "Maximum", + "sum": "Sum", + "count": "Count", + "count-unique": "Count unique" + }, + "filtered": "Filtered", + "value-source": "Value source", + "value-source-type": { + "key": "Key", + "function": "Function" + }, + "no-metrics-configured": "No metrics configured", + "add-metric": "Add metric", + "max-metrics": "Maximum number of metrics reached.", + "metric-settings": "Metric settings", + "filter": "Filter", + "filter-hint": "Enables filtering of entities during aggregation. The filter function must return a boolean value and can use all configured arguments." + }, + "aggregate-interval-type": "Aggregate interval type", + "aggregate-interval-value": "Aggregate interval value", + "aggregate-interval-value-required": "Aggregate interval value is required", + "aggregate-interval-value-min": "Aggregate interval value should be at least { sec, plural, =0 {0 second} =1 {1 second} other {# seconds} }", + "aggregate-interval-value-step-multiple-of": "Aggregate interval value must be a divisor or multiple of 1 day", + "aggregate-period": { + "hour": "Hour", + "day": "Day", + "week": "Week (Mon - Sun)", + "week-sun-sat": "Week (Sun - Sat)", + "month": "Month", + "quarter": "Quarter", + "year": "Year", + "custom": "Custom" + }, + "aggregate-period-hint-offset": "Your aggregation interval will be: {{ interval }}", + "aggregate-period-hint-offset-and-so-on": "Your aggregation interval will be: {{ interval }} and so on", + "entity-aggregation": { + "argument-hint": "Data will be fetched from selected entity", + "argument-setting-hint": "Latest telemetry is the only available argument type for this calculated field", + "aggregation-interval": "Aggregation interval", + "aggregation-interval-hint": "Defines how often to perform aggregation. Example: every 1 hour aggregates data at 00:00, 01:00, 02:00, etc.", + "apply-offset": "Apply offset to aggregation interval", + "apply-offset-hint": "Defines how much to shift the start of each aggregation period (e.g., +10 minutes - 00:10, 01:10).", + "offset-value": "Offset value", + "offset-value-required": "Offset value is required", + "offset-value-min": "Offset value must be a positive integer", + "offset-value-max": "Offset value should be less than the aggregate interval value", + "wait-delay": "Wait for delayed telemetry", + "wait-delay-hint": "Waits for delayed telemetry after the interval ends", + "duration": "Duration", + "duration-required": "Duration is required", + "duration-min": "Duration should be at least 1 minute", + "duration-hint": "How long to wait for delayed data after the interval ends" + }, "hint": { "arguments-simple-with-rolling": "Simple type calculated field should not contain keys with time series rolling type.", + "arguments-propagate-arguments-with-rolling": "'Time series rolling' type is incompatible with 'Arguments only' propagation.", + "arguments-propagate-argument-entity-type": "Entity type is incompatible with 'Arguments only' propagation.", + "arguments-propagate-argument-must-current-entity": "At least one argument must be configured with the 'Current entity' source entity type.", "arguments-empty": "Arguments should not be empty.", "expression-required": "Expression is required.", "expression-invalid": "Expression is invalid", @@ -1082,14 +1244,178 @@ "argument-name-duplicate": "Argument with such name already exists.", "argument-name-max-length": "Argument name should be less than 256 characters.", "argument-name-forbidden": "Argument name is reserved and cannot be used.", + "output-key-required": "Output key is required.", + "output-key-pattern": "Output key is invalid.", + "output-key-duplicate": "Key with such name already exists.", + "output-key-max-length": "Output key should be less than 256 characters.", + "output-key-forbidden": "Output key is reserved and cannot be used.", + "entity-type-required": "Entity type is required", + "name-required": "Name is required.", + "name-pattern": "Name is invalid.", + "name-duplicate": "Name with such name already exists.", + "name-max-length": "Name should be less than 256 characters.", + "name-forbidden": "Name is reserved and cannot be used.", "argument-type-required": "Argument type is required.", "max-args": "Maximum number of arguments reached.", "decimals-range": "Decimals by default should be a number between 0 and 15.", "expression": "Default expression demonstrates how to transform a temperature from Fahrenheit to Celsius.", "arguments-entity-not-found": "Argument target entity not found.", - "use-latest-timestamp": "If enabled, the calculated value will be persisted using the most recent timestamp from the arguments telemetry, instead of the server time." + "use-latest-timestamp": "If enabled, the calculated value will be persisted using the most recent timestamp from the arguments telemetry, instead of the server time.", + "entity-coordinates": "Specify the time series keys that provide entity GPS coordinates (latitude and longitude).", + "geofencing-zone-groups": "Define one or more geofencing zones groups to check (e.g. 'allowedZones', 'restrictedZones'). Each group must have a unique name, which is used as a prefix for calculated field output telemetry keys.", + "perimeter-attribute-key": "Set the attribute key that contains the geofencing zone perimeter definition. The perimeter is always taken from server-side attributes of the zone entity.", + "report-strategy": "Presence status reports whether the entity is currently INSIDE or OUTSIDE the zone group. Transition events report when the entity ENTERED or LEFT the zone group.", + "create-relation-with-matched-zones": "Automatically create and maintain relations between the entity and the zones it is currently inside. Relations are removed when the entity leaves a zone and created when it enters a new one.", + "relation-type-required": "Relation type is required.", + "relation-level-required": "Relation level is required.", + "relation-level-min": "Minimum relation level value is 1.", + "relation-level-max": "Maximum relation level value is {{max}}.", + "geofencing-empty": "At least one zone group must be configured.", + "geofencing-entity-not-found": "Geofencing target entity not found.", + "max-geofencing-zone": "Maximum number of geofencing zones reached.", + "zone-group-refresh-interval": "Defines how often zone groups configured via related entities are refreshed.", + "zone-group-refresh-interval-required": "Zone groups refresh interval is required.", + "zone-group-refresh-interval-min": "Zone group refresh interval should be at least {{ min }} second.", + "propagation-path-related-entities": "Defines a direct, single-level path to a related entity based on the selected direction and relation type.", + "data-propagate": "Defines the data to be propagated from the arguments configured below. 'Arguments only' uses the retrieved data directly, while 'Expression result' calculates a new value from that data.", + "aggregation-path-related-entities": "Defines a single-level aggregation path via direct relations with parent or child entities based on direction and relation type. Only relations between device, asset, customer, and tenant entities are supported.", + "arguments-aggregation": "Defines input parameters used for filtering and aggregation.", + "setting-arguments-aggregation": "Data will be fetched from related entities configured in aggregation path.", + "metrics": "Defines metrics aggregated based on the configured arguments." } }, + "alarm-rule": { + "alarm-rules-tab": "Alarm rules", + "alarm-rule": "Alarm rule", + "alarm-rules": "Alarm rules", + "alarm-rules-old": "Old", + "alarm-rules-actual": "Actual", + "severities": "Severities", + "cleared": "Clear condition", + "delete-title": "Are you sure you want to delete the alarm rule '{{title}}'?", + "delete-text": "Be careful, after the confirmation the alarm rule and all related data will become unrecoverable.", + "delete-multiple-title": "Are you sure you want to delete { count, plural, =1 {1 alarm rule} other {# alarm rules} }?", + "delete-multiple-text": "Be careful, after the confirmation all selected alarm rules will be removed and all related data will become unrecoverable.", + "create": "Create new alarm rule", + "no-found": "No alarm rules found", + "list": "{ count, plural, =1 {One alarm rule} other {List of # alarm rules} }", + "selected-fields": "{ count, plural, =1 {1 alarm rule} other {# alarm rules} } selected", + "import": "Import alarm rule", + "export": "Export alarm rule", + "export-failed-error": "Unable to export alarm rule: {{error}}", + "alarm-type": "Alarm type", + "alarm-type-required": "Alarm type is required.", + "alarm-type-pattern": "Alarm type is invalid.", + "alarm-type-max-length": "Alarm type should be less than 256 characters.", + "clear-alarm": "Clear alarm", + "value-argument": "Argument", + "value-argument-required": "Argument is required.", + "static-settings": "Static settings", + "configuration": "Configuration", + "static-schedule": "Static", + "dynamic-schedule": "Dynamic", + "operation-and": "AND", + "operation-or": "OR", + "condition-during": "During {{during}}", + "condition-during-dynamic": "During \"{{ attribute }}\"", + "condition-repeat-times": "Repeats { count, plural, =1 {1 time} other {# times} }", + "condition-repeat-times-dynamic": "Repeats \"{ attribute }\"", + "filter-preview": "Filter preview", + "condition-settings": "Condition settings", + "static": "Static", + "dynamic": "Dynamic", + "argument-filters": "Argument filters", + "argument-name": "Argument name", + "value-type": "Value type", + "general": "General", + "filter": "Filter", + "operation": "Operation", + "value-source": "Value source", + "value": "Value", + "ignore-case": "Ignore case", + "condition": "Condition", + "script": "Script", + "add-filter": "Add filter", + "edit-filter": "Edit filter", + "conditions": { + "simple": "Simple", + "duration": "Duration", + "repeating": "Repeating" + }, + "schedule-title": "Schedule", + "edit-schedule": "Edit alarm schedule", + "schedule-type": "Scheduler type", + "schedule-type-required": "Scheduler type is required.", + "schedule": { + "any-time": "Active all the time", + "specific-time": "Active at a specific time", + "custom": "Custom" + }, + "schedule-day": { + "monday": "Monday", + "tuesday": "Tuesday", + "wednesday": "Wednesday", + "thursday": "Thursday", + "friday": "Friday", + "saturday": "Saturday", + "sunday": "Sunday" + }, + "schedule-days": "Days", + "schedule-time": "Time", + "schedule-time-from": "From", + "schedule-time-to": "To", + "schedule-days-of-week-required": "At least one day of week should be selected.", + "expression-type": { + "simple": "Simple", + "tbel": "TBEL" + }, + "operation-type": { + "and": "And", + "or": "Or" + }, + "filter-predicate-type": { + "string": "String", + "numeric": "Numeric", + "boolean": "Boolean", + "complex": "Complex" + }, + "alarm-rule-additional-info": "Additional info", + "edit-alarm-rule-additional-info": "Edit additional info", + "alarm-rule-additional-info-placeholder": "Please provide your comments and adjustments here to display them within Alarm details under Additional info", + "alarm-rule-additional-info-hint": "Hint: use ${Argument name} to substitute values of the arguments that are used in alarm rule condition.", + "alarm-rule-mobile-dashboard": "Mobile dashboard", + "alarm-rule-mobile-dashboard-hint": "Used by mobile application as an alarm details dashboard", + "alarm-rule-no-mobile-dashboard": "No dashboard selected", + "alarm-rule-condition": "Alarm rule condition", + "enter-alarm-rule-condition-prompt": "Please add alarm rule condition", + "edit-alarm-rule-condition": "Edit alarm rule condition", + "condition-type": "Condition type", + "select-alarm-severity": "Select alarm severity", + "add-create-alarm-rule-prompt": "Please add create alarm rule", + "add-create-alarm-rule": "Add create condition", + "add-clear-alarm-rule": "Add clear condition", + "condition-duration": "Condition duration", + "condition-duration-value": "Duration value", + "condition-duration-time-unit": "Time unit", + "condition-duration-value-range": "Duration value should be in a range from 1 to 2147483647.", + "condition-duration-value-pattern": "Duration value should be integers.", + "condition-duration-value-required": "Duration value is required.", + "condition-duration-time-unit-required": "Time unit is required.", + "condition-repeating-value": "Count of events", + "condition-repeating-value-range": "Count of events should be in a range from 1 to 2147483647.", + "condition-repeating-value-pattern": "Count of events should be integers.", + "condition-repeating-value-required": "Count of events is required.", + "create-alarm-rules": "Create alarm rules", + "clear-alarm-rule": "Clear alarm rule", + "no-clear-alarm-rule": "No clear condition configured", + "advanced-settings": "Advanced settings", + "propagate-alarm": "Propagate alarm to related entities", + "alarm-rule-relation-types-list": "Relation types", + "alarm-rule-relation-types-list-hint": "Defines relation types to filter the related entities. If not set, the alarm will be propagated to all related entities.", + "propagate-alarm-to-owner": "Propagate alarm to entity owner (Customer or Tenant)", + "propagate-alarm-to-tenant": "Propagate alarm to Tenant", + "debugging": "Alarm rule debugging" + }, "ai-models": { "ai-models": "AI models", "ai-model": "AI model", @@ -1112,13 +1438,15 @@ "mistral-ai": "Mistral AI", "anthropic": "Anthropic", "amazon-bedrock": "Amazon Bedrock", - "github-models": "GitHub Models" + "github-models": "GitHub Models", + "ollama": "Ollama" }, "name-required": "Name is required.", "name-max-length": "Name must be 255 characters or less.", "provider": "Provider", "api-key": "API key", "api-key-required": "API key is required.", + "api-key-open-ai-required": "API key is required when using the official OpenAI API.", "project-id": "Project ID", "project-id-required": "Project ID is required", "location": "Location", @@ -1155,17 +1483,34 @@ "frequency-penalty": "Frequency penalty", "frequency-penalty-hint": "Applies a penalty to a token's likelihood that increases based on its frequency in the text.", "max-output-tokens": "Maximum output tokens", - "max-output-tokens-min": "Must be greater than 0.", "max-output-tokens-hint": "Sets the maximum number of tokens that the \nmodel can generate in a single response.", + "context-length": "Context length", + "context-length-hint": "Defines the size of the context window in tokens. This value sets the total memory limit for the model, including both the user's input and the generated response.", "endpoint": "Endpoint", "endpoint-required": "Endpoint is required.", + "baseurl": "Base URL", + "baseurl-required": "Base URL is required.", "service-version": "Service version", "check-connectivity": "Check connectivity", "check-connectivity-success": "Test request was successful", "check-connectivity-failed": "Test request failed", "no-model-matching": "No models matching '{{entity}}' were found.", "model-required": "Model is required.", - "no-model-text": "No models found." + "no-model-text": "No models found.", + "authentication": "Authentication", + "authentication-basic-hint": "Uses standard HTTP Basic authentication. The username and password will be combined, Base64-encoded, and sent in an \"Authorization\" header with each request to the Ollama server.", + "authentication-token-hint": "Uses Bearer token authentication. The provided token will be sent directly in an \"Authorization\" eader with each request to the Ollama server.", + "authentication-type": { + "none": "None", + "basic": "Basic", + "token": "Token" + }, + "username": "Username", + "username-required": "Username is required.", + "password": "Password", + "password-required": "Password is required.", + "token": "Token", + "token-required": "Token is required." }, "confirm-on-exit": { "message": "You have unsaved changes. Are you sure you want to leave this page?", @@ -1347,6 +1692,8 @@ "mobile-order": "Dashboard order in mobile application", "mobile-hide": "Hide dashboard in mobile application", "update-image": "Update dashboard image", + "update-new-version": "Upload new version", + "upload-file-to-update": "Upload file to update", "take-screenshot": "Take screenshot", "select-widget-title": "Select widget", "select-widget-value": "{{title}}: select widget", @@ -1717,6 +2064,8 @@ "bootstrap-tab": "Bootstrap Client", "bootstrap-server": "Bootstrap Server", "lwm2m-server": "LwM2M Server", + "client-reboot": "Registration Update Trigger", + "bootstrap-reboot": "Bootstrap-Request Trigger", "client-publicKey-or-id": "Client Public Key or Id", "client-publicKey-or-id-required": "Client Public Key or Id is required.", "client-publicKey-or-id-tooltip-psk": "The PSK identifier is an arbitrary PSK identifier up to 128 bytes, as described in the standard [RFC7925].\nThe PSK identifier MUST first be converted to a character string and then encoded into octets using UTF-8.", @@ -1848,6 +2197,7 @@ "selected-options-limit": "Selected options limit", "advanced-ui-settings": "Advanced UI settings", "disable-on-property": "Disable on property", + "disable-on-property-none": "None (field always enabled)", "display-condition-function": "Display condition function", "sub-label": "Sub label", "vertical-divider-after": "Vertical divider after", @@ -2217,6 +2567,7 @@ "short-id-required": "Short server ID is required.", "short-id-range": "Short server ID should be in a range from {{ min }} to {{ max }}.", "short-id-pattern": "Short server ID must be a positive integer.", + "short-id-pattern-bs": "Short server ID must be only null", "lifetime": "Client registration lifetime", "lifetime-required": "Client registration lifetime is required.", "lifetime-pattern": "Client registration lifetime must be a positive integer.", @@ -2311,7 +2662,9 @@ "composite-all-description": "All resources are observed with a single Composite Observe request (more efficient, less flexible)", "composite-by-object": "Composite by objects", "composite-by-object-description": "Resources are grouped by object type and observed using separate Composite Observe requests (balanced approach)" - } + }, + "init-attr-tel-as-obs-strategy": "Initialize attributes and telemetry using Observe strategy", + "init-attr-tel-as-obs-strategy-hint": "If false - attributes and telemetry are initialized by reading their values one by one.\\nIf true - attributes and telemetry are initialized by subscribing to their values using the Observe strategy." }, "snmp": { "add-communication-config": "Add communication config", @@ -2648,6 +3001,7 @@ "details": "Entity details", "no-entities-prompt": "No entities found", "no-data": "No data to display", + "show-all-columns":"Show All", "columns-to-display": "Columns to Display", "type-api-usage-state": "API Usage State", "type-edge": "Edge", @@ -2811,7 +3165,7 @@ "target-entity": "Target entity", "attributes-propagation": "Attributes propagation", "attributes-propagation-hint": "Entity View will automatically copy specified attributes from Target Entity each time you save or update this entity view. For performance reasons target entity attributes are not propagated to entity view on each attribute change. You can enable automatic propagation by configuring \"copy to view\" rule node in your rule chain and linking \"Post attributes\" and \"Attributes Updated\" messages to the new rule node.", - "timeseries-data": "Times eries data", + "timeseries-data": "Time series data", "timeseries-data-hint": "Configure time series data keys of the target entity that will be accessible to the entity view. This time series data is read-only.", "search": "Search entity views", "selected-entity-views": "{ count, plural, =1 {1 entity view} other {# entity views} } selected", @@ -3801,7 +4155,49 @@ "activation-link-expired": "Activation link has expired", "activation-link-expired-message": "The link to activate your profile has expired. You can return to the login page to receive a new email.", "reset-password-link-expired": "Password reset link has expired", - "reset-password-link-expired-message": "The link to reset your password has expired. You can return to the login page to receive a new email." + "reset-password-link-expired-message": "The link to reset your password has expired. You can return to the login page to receive a new email.", + "two-fa": "Two-factor authentication", + "two-fa-required": "Two-factor authentication is required", + "set-up-verification-method": "Set up a verification method to continue", + "set-up-verification-method-login": "Set up a verification method or login", + "enable-authenticator-app": "Enable authenticator app", + "enable-authenticator-app-description": "Please enter the security code from your authenticator app", + "enable-authenticator-sms": "Enable SMS authenticator", + "enable-authenticator-sms-description": "Enter a 6-digit code we just sent to ", + "enable-authenticator-email": "Enable email authenticator", + "enable-authenticator-email-description": "A security code has been sent to your email address at ", + "enter-key-manually": "or enter this 32-digits key manually:", + "continue": "Continue", + "confirm": "Confirm", + "authenticator-app-success": "Authenticator app successfully enabled", + "authenticator-app-success-description": "The next time you log in, you will need to provide a two-factor authentication code", + "authenticator-sms-success": "SMS authenticator successfully enabled", + "authenticator-sms-success-description": "The next time you log in, you will be prompted to enter the security code that will be sent to the phone number", + "authenticator-email-success": "Email authenticator successfully enabled", + "authenticator-email-success-description": "The next time you log in, you will be prompted to enter the security code that will be sent to your email address", + "authenticator-backup-code-success": "Backup code successfully enabled", + "authenticator-backup-code-success-description": "The next time you log in, you will be prompted to enter the security code or use one of backup code.", + "add-verification-method": "Add verification method", + "get-backup-code": "Get backup code", + "copy-key": "Copy key", + "send-code": "Send code", + "email-label": "Email", + "email-description": "Enter an email to use as your authenticator.", + "sms-description": "Enter a phone number to use as your authenticator.", + "backup-code-description": "Print out the codes so you have them handy when you need to use them to log in to your account. You can use each backup code once.", + "backup-code-warn": "Once you leave this page, these codes cannot be shown again. Store them safely using the options below.", + "download-txt": "Download (txt)", + "print": "Print", + "verification-code": "6-digit code", + "verification-code-invalid": "Invalid verification code format", + "scan-qr-code": "Scan this QR code with your verification app", + "phone-input": { + "phone-input-label": "Phone number", + "phone-input-required": "Phone number is required", + "phone-input-validation": "Phone number is invalid or not possible", + "phone-input-pattern": "Invalid phone number. Should be in E.164 format, ex. {{phoneNumber}}", + "phone-input-hint": "Phone Number in E.164 format, ex. {{phoneNumber}}" + } }, "markdown": { "edit": "Edit", @@ -4071,6 +4467,7 @@ "no-severity-found": "No severity found", "no-severity-matching": "'{{severity}}' not found.", "no-template-matching": "No resource matching '{{template}}' were found.", + "create-new-template": "Create a new one!", "not-found-slack-recipient": "Slack recipient not found", "notification": "Notification", "notification-center": "Notification center", @@ -4486,7 +4883,8 @@ "jks": "JKS", "js-module": "JS module", "lwm2m-model": "LWM2M model", - "pkcs-12": "PKCS #12" + "pkcs-12": "PKCS #12", + "general": "General" }, "resource-sub-type": "Sub-type", "sub-type": { @@ -4494,7 +4892,12 @@ "scada-symbol": "Scada symbol", "extension": "Extension", "module": "Module" - } + }, + "resource-is-in-use": "Resource is used by other entities", + "resources-are-in-use": "Resources are used by other entities", + "resource-is-in-use-text": "The Resource '{{title}}' was not deleted because it is used by the following entities:", + "resources-are-in-use-text": "Not all Resources have been deleted because they are used by other entities.
    You can view referenced entities by clicking the References button in the corresponding resource row.
    If you still want to delete these resources, select them in the table below and click the Delete selected button.", + "delete-resource-in-use-text": "If you still want to delete the resource, click the Delete anyway button." }, "javascript": { "add": "Add JavaScript resource", @@ -5394,7 +5797,7 @@ "time-series": "Time series", "latest": "Latest values", "web-sockets": "WebSockets", - "calculated-fields": "Calculated fields" + "calculated-fields-and-alarm-rules": "Calculated fields and alarm rules" }, "save-attribute": { "processing-settings": "Processing settings", @@ -5444,11 +5847,11 @@ "prompt-settings": "Prompt settings", "prompt-settings-hint": "The optional system prompt sets the AI's general role and constraints, while the user prompt defines the specific task to perform. Both fields also support templatization.", "system-prompt": "System prompt", - "system-prompt-max-length": "System prompt must be 10000 characters or less.", + "system-prompt-max-length": "System prompt must be 500000 characters or less.", "system-prompt-blank": "System prompt must not be blank.", "user-prompt": "User prompt", "user-prompt-required": "User prompt is required.", - "user-prompt-max-length": "User prompt must be 10000 characters or less.", + "user-prompt-max-length": "User prompt must be 500000 characters or less.", "user-prompt-blank": "User prompt must not be blank.", "response-format": "Response format", "response-text": "Text", @@ -5465,7 +5868,8 @@ "timeout-required": "Timeout is required", "timeout-validation": "Must be from 1 second to 10 minutes.", "force-acknowledgement": "Force acknowledgement", - "force-acknowledgement-hint": "If enabled, the incoming message is acknowledged immediately. The model's response is then enqueued as a separate, new message." + "force-acknowledgement-hint": "If enabled, the incoming message is acknowledged immediately. The model's response is then enqueued as a separate, new message.", + "ai-resources": "AI resources" } }, "timezone": { @@ -5726,6 +6130,18 @@ "max-arguments-per-cf": "Arguments per calculated field max number", "max-arguments-per-cf-range": "Arguments per calculated field max number can't be negative", "max-arguments-per-cf-required": "Arguments per calculated field max number is required", + "max-related-level-per-argument": "Maximum relation level per 'Related entities' argument", + "max-related-level-per-argument-range": "Relation level per 'Related entities' argument max number can't be less than '1'", + "max-related-level-per-argument-required": "Relation level per 'Related entities' argument max number is required", + "min-allowed-scheduled-update-interval": "Min allowed update interval for 'Related entities' arguments (seconds)", + "min-allowed-scheduled-update-interval-range": "Min allowed update interval min number can't be negative", + "min-allowed-deduplication-interval": "Min allowed deduplication interval (seconds)", + "min-allowed-deduplication-interval-range": "Min allowed deduplication interval value can't be negative", + "min-allowed-deduplication-interval-required": "Min allowed deduplication interval is required", + "min-allowed-aggregation-interval": "Min allowed aggregation interval (seconds)", + "min-allowed-aggregation-interval-range": "Min allowed aggregation interval value can't be negative", + "min-allowed-aggregation-interval-required": "Min allowed aggregation interval is required", + "min-allowed-scheduled-update-interval-required": "Min allowed update interval min number is required", "max-state-size": "State maximum size in KB", "max-state-size-range": "State maximum size in KB can't be negative", "max-state-size-required": "State maximum size in KB is required", @@ -5801,6 +6217,10 @@ "ws-limit-max-subscriptions-per-regular-user": "Subscriptions per regular user maximum number", "ws-limit-max-subscriptions-per-public-user": "Subscriptions per public user maximum number", "ws-limit-updates-per-session": "WS updates per session", + "relation-search-entity-limit": "Relation search entity limit", + "relation-search-entity-limit-hint": "Limits the number of entities resolved at the last level of the relation path. Applies to 'Related entities' arguments and Propagation fields.", + "relation-search-entity-limit-required": "Relation search entity limit", + "relation-search-entity-limit-range": "Relation search entity limit can't be less than '1'", "rate-limits": { "add-limit": "Add limit", "and-also-less-than": "and also less than", @@ -5998,6 +6418,7 @@ "show-date-time-interval": "Show date time interval", "show-date-time-interval-hint": "Show date time interval according to the data aggregation.", "hide-zero-tooltip-values": "Hide zero values", + "show-stack-total": "Show total value in stack mode", "background-color": "Background color", "background-blur": "Background blur" }, @@ -6866,7 +7287,23 @@ "scan-qr-code": "Scan QR Code", "make-phone-call": "Make phone call", "get-location": "Get phone location", - "take-screenshot": "Take screenshot" + "take-screenshot": "Take screenshot", + "handle-provision-success-function": "Handle provision success function", + "get-location-function": "Get location function", + "process-launch-result-function": "Process launch result function", + "get-phone-number-function": "Get phone number function", + "process-image-function": "Process image function", + "process-qr-code-function": "Process QR code function", + "process-location-function": "Process location function", + "handle-empty-result-function": "Handle empty result function", + "handle-error-function": "Handle error function", + "handle-non-mobile-fallback-function": "Handle Non-Mobile fallback function", + "save-to-gallery": "Save to gallery", + "provision-type": "Provision type", + "auto": "Auto", + "wi-fi": "Wi-Fi", + "ble": "BLE", + "soft-ap": "Soft AP" }, "custom-action-function": "Custom action function", "custom-pretty-function": "Custom action (with HTML template) function", @@ -6875,7 +7312,8 @@ "marker": "Marker", "polygon": "Polygon", "rectangle": "Rectangle", - "circle": "Circle" + "circle": "Circle", + "polyline": "Polyline" }, "place-map-item": "Place map item", "map-item-tooltip": { @@ -6887,7 +7325,9 @@ "continue-draw-polygon": "Continue draw polygon", "finish-draw-polygon": "Finish draw polygon", "start-draw-circle": "Start draw circle", - "finish-draw-circle": "Finish draw circle" + "finish-draw-circle": "Finish draw circle", + "start-draw-polyline": "Start draw polyline", + "finish-draw-polyline": "Finish draw polyline" } }, "widgets-bundle": { @@ -7903,14 +8343,14 @@ "attribute-scope-server": "Server attribute", "attribute-scope-shared": "Shared attribute", "value-required": "Value required", - "image-settings": "Image settings", + "image-settings": "Image output settings", "image-format": "Image format", "image-format-jpeg": "JPEG", "image-format-png": "PNG", "image-format-webp": "WEBP", - "image-quality": "Image quality that use lossy compression such as jpeg and webp", - "max-image-width": "Maximum image width", - "max-image-height": "Maximum image height", + "image-quality": "Image quality", + "max-image-width": "Max width", + "max-image-height": "Max height", "action-buttons": "Action buttons", "show-action-buttons": "Show action buttons", "update-all-values": "Update all values, not only modified", @@ -7991,7 +8431,10 @@ "add-radio-option": "Add radio option", "radio-label-position": "Label position", "radio-label-position-before": "Before", - "radio-label-position-after": "After" + "radio-label-position-after": "After", + "save-image": "Save image", + "save-to-gallery": "Automatically store captured images in Image Gallery", + "public-image": "Makes image avaliable for any unauthorized user" }, "invalid-qr-code-text": "Invalid input text for QR code. Input should have a string type", "qr-code": { @@ -8286,7 +8729,8 @@ "trips": "Trips", "markers": "Markers", "polygons": "Polygons", - "circles": "Circles" + "circles": "Circles", + "polylines": "Polylines" }, "data-layer": { "source": "Source", @@ -8483,6 +8927,25 @@ "finish-circle-hint-with-entity": "Circle for '{{entityName}}': click to finish and save circle", "finish-circle-hint": "Circle: click to finish drawing" }, + "polyline": { + "polyline-key": "Polyline key", + "polyline-key-required": "Polyline key required", + "no-polylines": "No polylines configured", + "add-polylines": "Add polyline", + "polyline-configuration": "Polyline configuration", + "remove-polyline": "Remove polyline", + "edit": "Edit polyline", + "cut": "Cut polyline area", + "rotate": "Rotate polyline", + "remove-polyline-for": "Remove polyline for '{{entityName}}'", + "draw-polyline": "Draw polyline", + "polyline-place-first-point-hint-with-entity": "Polyline for '{{entityName}}': click to place first point", + "polyline-place-first-point-hint": "Polyline: click to place first point", + "finish-polyline-hint-with-entity": "Polyline for '{{entityName}}': click to finish drawing", + "finish-polyline-hint": "Polyline: click to finish drawing", + "polyline-place-first-point-cut-hint": "Click to place first point", + "finish-polyline-cut-hint": "Click first marker to finish and save" + }, "select-entity": "Select entity", "select-entity-hint": "Hint: after selection click at the map to set position" }, @@ -8924,6 +9387,7 @@ "show-empty-space-hidden-action": "Show empty space instead of hidden cell button action", "dont-reserve-space-hidden-action": "Don't reserve space for hidden action buttons", "display-timestamp": "Timestamp", + "timestamp-column-name":"Timestamp", "display-pagination": "Display pagination", "default-page-size": "Default page size", "page-step-settings": "Page step settings", @@ -8985,7 +9449,11 @@ "alarm-column-error": "At least one alarm column should be specified", "table-tabs": "Table tabs", "show-cell-actions-menu-mobile": "Show cell actions dropdown menu in mobile mode", - "disable-sorting": "Disable sorting" + "disable-sorting": "Disable sorting", + "sort-by": "Sort tabs by", + "sort-asc": "Name Ascending", + "sort-desc": "Name Descending", + "sort-timestamp-option": "Created time" }, "latest-chart": { "total": "Total", @@ -9481,6 +9949,22 @@ "how-to-create-customer-and-assign-dashboard": "How to create Customer and assign Dashboard" } } + }, + "api-usage": { + "api-usage": "API usage", + "label": "Label", + "state-name": "State name", + "status": "Status", + "status-required": "Status is required.", + "limit": "Max limit", + "limit-required": "Max limit is required.", + "current-number": "Current number", + "current-number-required": "Current number is required.", + "add-key": "Add key", + "no-key": "No key", + "delete-key": "Delete key", + "target-dashboard-state": "Target dashboard state", + "go-to-main-state": "Go to default view" } }, "color": { @@ -9489,6 +9973,7 @@ "icon": { "icon": "Icon", "icons": "Icons", + "custom": "Custom", "select-icon": "Select icon", "material-icons": "Material icons", "show-all": "Show all icons", @@ -9529,6 +10014,7 @@ "items-per-page-separator": "of" }, "language": { + "auto": "Auto", "language": "Language", "locales": { "ar_AE": "العربية (الإمارات العربية المتحدة)", diff --git a/ui-ngx/src/assets/locale/locale.constant-es_ES.json b/ui-ngx/src/assets/locale/locale.constant-es_ES.json index daab8822fd..3abb65bec6 100644 --- a/ui-ngx/src/assets/locale/locale.constant-es_ES.json +++ b/ui-ngx/src/assets/locale/locale.constant-es_ES.json @@ -261,8 +261,8 @@ "client-secret-required": "Se requiere el secreto del cliente.", "client-secret-max-length": "El secreto del cliente debe tener menos de 2049 caracteres", "custom-setting": "Configuraciones personalizadas", - "customer-name-pattern": "Patrón de nombre de cliente", - "customer-name-pattern-max-length": "El patrón de nombre de cliente debe tener menos de 256 caracteres", + "customer-name-pattern": "Patrón de nombre de customer", + "customer-name-pattern-max-length": "El patrón de nombre de customer debe tener menos de 256 caracteres", "default-dashboard-name": "Nombre del tablero predeterminado", "default-dashboard-name-max-length": "El nombre del tablero predeterminado debe tener menos de 256 caracteres", "delete-domain-text": "Ten cuidado, después de la confirmación el dominio y todos los datos del proveedor dejarán de estar disponibles.", @@ -294,10 +294,10 @@ "registration-id-unique": "El ID de registro debe ser único en el sistema.", "scope": "Alcance", "scope-required": "Se requiere el alcance.", - "tenant-name-pattern": "Patrón de nombre del inquilino", - "tenant-name-pattern-required": "Se requiere el patrón de nombre del inquilino.", - "tenant-name-pattern-max-length": "El patrón de nombre del inquilino debe tener menos de 256 caracteres", - "tenant-name-strategy": "Estrategia de nombre del inquilino", + "tenant-name-pattern": "Patrón de nombre del Tenant", + "tenant-name-pattern-required": "Se requiere el patrón de nombre del Tenant.", + "tenant-name-pattern-max-length": "El patrón de nombre del Tenant debe tener menos de 256 caracteres", + "tenant-name-strategy": "Estrategia de nombre del Tenant", "type": "Tipo de mapeador", "uri-pattern-error": "Formato de URI inválido.", "url": "URL", @@ -349,8 +349,8 @@ "domain-name": "Nombre de dominio", "domain-name-required": "Se requiere el nombre de dominio", "redirect-url-template": "Plantilla de URI de redirección", - "microsoft-tenant-id": "ID de directorio (inquilino)", - "microsoft-tenant-id-required": "Se requiere el ID de directorio (inquilino)", + "microsoft-tenant-id": "ID de directorio (tenant)", + "microsoft-tenant-id-required": "Se requiere el ID de directorio (tenant)", "token-uri": "URI del token", "token-uri-required": "Se requiere la URI del token", "redirect-uri": "URI de redirección", @@ -545,7 +545,13 @@ "slack-settings": "Configuración de Slack", "mobile-settings": "Configuración móvil", "firebase-service-account-file": "Archivo JSON de credenciales de cuenta de servicio de Firebase", - "select-firebase-service-account-file": "Arrastra y suelta tu archivo de credenciales de cuenta de servicio de Firebase o " + "select-firebase-service-account-file": "Arrastra y suelta tu archivo de credenciales de cuenta de servicio de Firebase o ", + "trendz": "Trendz", + "trendz-settings": "Configuración de Trendz", + "trendz-url": "URL de Trendz", + "trendz-url-required": "La URL de Trendz es obligatoria", + "trendz-api-key": "Clave API de Trendz", + "trendz-enable": "Habilitar Trendz" }, "alarm": { "alarm": "Alarma", @@ -678,7 +684,7 @@ "filter-type-entity-name": "Nombre de la entidad", "filter-type-entity-type": "Tipo de entidad", "filter-type-state-entity": "Entidad del estado del tablero", - "filter-type-state-entity-description": "Entidad tomada de los parámetros del estado del tablero", + "filter-type-state-entity-description": "Entidad obtenida de los parámetros del estado del tablero", "filter-type-asset-type": "Tipo de activo", "filter-type-asset-type-description": "Activos del tipo '{{assetTypes}}'", "filter-type-asset-type-and-name-description": "Activos del tipo '{{assetTypes}}' con nombre que comienza con '{{prefix}}'", @@ -709,12 +715,13 @@ "filter-type-required": "Se requiere el tipo de filtro.", "entity-filter-no-entity-matched": "No se encontraron entidades que coincidan con el filtro especificado.", "no-entity-filter-specified": "No se especificó ningún filtro de entidad", - "root-state-entity": "Usar entidad del estado del tablero como raíz", - "last-level-relation": "Obtener solo relaciones del último nivel", + "root-state-entity": "Usar la entidad del estado del tablero como raíz", + "last-level-relation": "Obtener solo la relación del último nivel", "root-entity": "Entidad raíz", - "state-entity-parameter-name": "Nombre del parámetro de entidad del estado", + "state-entity-parameter-name": "Nombre del parámetro de la entidad de estado", "default-state-entity": "Entidad de estado predeterminada", "default-entity-parameter-name": "Por defecto", + "query-options": "Opciones de consulta", "max-relation-level": "Nivel máximo de relación", "unlimited-level": "Nivel ilimitado", "state-entity": "Entidad del estado del tablero", @@ -728,16 +735,16 @@ "view-assets": "Ver activos", "add": "Agregar activo", "asset-type-max-length": "El tipo de activo debe tener menos de 256 caracteres", - "assign-to-customer": "Asignar a cliente", + "assign-to-customer": "Asignar a customer", "assign-asset-to-customer": "Asignar activo(s) al cliente", - "assign-asset-to-customer-text": "Selecciona los activos que deseas asignar al cliente", + "assign-asset-to-customer-text": "Selecciona los activos que deseas asignar al customer", "no-assets-text": "No se encontraron activos", - "assign-to-customer-text": "Selecciona el cliente al que deseas asignar el/los activo(s)", + "assign-to-customer-text": "Selecciona el customer al que deseas asignar el/los activo(s)", "public": "Público", - "assignedToCustomer": "Asignado a cliente", + "assignedToCustomer": "Asignado a customer", "make-public": "Hacer público el activo", "make-private": "Hacer privado el activo", - "unassign-from-customer": "Desasignar del cliente", + "unassign-from-customer": "Desasignar del customer", "delete": "Eliminar activo", "asset-public": "El activo es público", "asset-type": "Tipo de activo", @@ -760,12 +767,12 @@ "add-asset-text": "Agregar nuevo activo", "asset-details": "Detalles del activo", "assign-assets": "Asignar activos", - "assign-assets-text": "Asignar { count, plural, =1 {1 activo} other {# activos} } al cliente", + "assign-assets-text": "Asignar { count, plural, =1 {1 activo} other {# activos} } al customer", "assign-asset-to-edge-title": "Asignar activo(s) a Edge", "assign-asset-to-edge-text": "Selecciona los activos que deseas asignar a Edge", "delete-assets": "Eliminar activos", "unassign-assets": "Desasignar activos", - "unassign-assets-action-title": "Desasignar { count, plural, =1 {1 activo} other {# activos} } del cliente", + "unassign-assets-action-title": "Desasignar { count, plural, =1 {1 activo} other {# activos} } del customer", "assign-new-asset": "Asignar nuevo activo", "delete-asset-title": "¿Estás seguro de que deseas eliminar el activo '{{assetName}}'?", "delete-asset-text": "Ten cuidado, después de la confirmación el activo y todos los datos relacionados serán irrecuperables.", @@ -777,10 +784,10 @@ "make-private-asset-title": "¿Estás seguro de que deseas hacer privado el activo '{{assetName}}'?", "make-private-asset-text": "Después de la confirmación, el activo y todos sus datos serán privados y no serán accesibles por otros.", "unassign-asset-title": "¿Estás seguro de que deseas desasignar el activo '{{assetName}}'?", - "unassign-asset-text": "Después de la confirmación, el activo se desasignará y no será accesible por el cliente.", + "unassign-asset-text": "Después de la confirmación, el activo se desasignará y no será accesible por el customer.", "unassign-asset": "Desasignar activo", "unassign-assets-title": "¿Estás seguro de que deseas desasignar { count, plural, =1 {1 activo} other {# activos} }?", - "unassign-assets-text": "Después de la confirmación, todos los activos seleccionados se desasignarán y no serán accesibles por el cliente.", + "unassign-assets-text": "Después de la confirmación, todos los activos seleccionados se desasignarán y no serán accesibles por el customer.", "copyId": "Copiar ID del activo", "idCopiedMessage": "El ID del activo ha sido copiado al portapapeles", "select-asset": "Seleccionar activo", @@ -917,22 +924,27 @@ "view-statistics": "Ver estadísticas" }, "api-limit": { - "cassandra-queries": "Consultas Cassandra", - "entity-version-creation": "Creación de versión de entidad", - "entity-version-load": "Carga de versión de entidad", + "cassandra-write-queries-core": "Consultas de escritura en Cassandra desde la API REST", + "cassandra-read-queries-core": "Consultas de lectura en Cassandra desde la API REST y telemetría WS", + "cassandra-write-queries-rule-engine": "Consultas de escritura en Cassandra desde el motor de reglas (Rule Engine)", + "cassandra-read-queries-rule-engine": "Consultas de lectura en Cassandra desde el motor de reglas (Rule Engine)", + "cassandra-write-queries-monolith": "Consultas de escritura en Cassandra desde la telemetría monolítica", + "cassandra-read-queries-monolith": "Consultas de lectura en Cassandra desde la telemetría monolítica", + "entity-version-creation": "Creación de versiones de entidad", + "entity-version-load": "Carga de versiones de entidad", "notification-requests": "Solicitudes de notificación", "notification-requests-per-rule": "Solicitudes de notificación por regla", - "rest-api-requests": "Solicitudes de API REST", - "rest-api-requests-per-customer": "Solicitudes de API REST por cliente", + "rest-api-requests": "Solicitudes de la API REST", + "rest-api-requests-per-customer": "Solicitudes de la API REST por customer", "transport-messages": "Mensajes de transporte", "transport-messages-per-device": "Mensajes de transporte por dispositivo", "transport-messages-per-gateway": "Mensajes de transporte por gateway", "transport-messages-per-gateway-device": "Mensajes de transporte por dispositivo de gateway", "ws-updates-per-session": "Actualizaciones WS por sesión", - "edge-events": "Eventos de Edge", - "edge-events-per-edge": "Eventos de Edge por Edge", - "edge-uplink-messages": "Mensajes uplink de Edge", - "edge-uplink-messages-per-edge": "Mensajes uplink de Edge por Edge" + "edge-events": "Eventos de edge", + "edge-events-per-edge": "Eventos de edge por instancia de edge", + "edge-uplink-messages": "Mensajes uplink de edge", + "edge-uplink-messages-per-edge": "Mensajes uplink de edge por instancia de edge" }, "audit-log": { "audit": "Auditoría", @@ -951,8 +963,8 @@ "type-attributes-deleted": "Atributos eliminados", "type-rpc-call": "Llamada RPC", "type-credentials-updated": "Credenciales actualizadas", - "type-assigned-to-customer": "Asignado al cliente", - "type-unassigned-from-customer": "Desasignado del cliente", + "type-assigned-to-customer": "Asignado al customer", + "type-unassigned-from-customer": "Desasignado del customer", "type-assigned-to-edge": "Asignado a Edge", "type-unassigned-from-edge": "Desasignado de Edge", "type-activated": "Activado", @@ -981,8 +993,8 @@ "failure-details": "Detalles del fallo", "search": "Buscar registros de auditoría", "clear-search": "Borrar búsqueda", - "type-assigned-from-tenant": "Asignado desde inquilino", - "type-assigned-to-tenant": "Asignado a inquilino", + "type-assigned-from-tenant": "Asignado desde Tenant", + "type-assigned-to-tenant": "Asignado a Tenant", "type-provision-success": "Dispositivo aprovisionado", "type-provision-failure": "El aprovisionamiento del dispositivo falló", "type-timeseries-updated": "Telemetría actualizada", @@ -1018,13 +1030,13 @@ "add-argument": "Agregar argumento", "test-script-function": "Probar función de script", "no-arguments": "No hay argumentos configurados", - "argument-settings": "Configuración del argumento", + "argument-settings": "Configuración de argumentos", "argument-current": "Entidad actual", - "argument-current-tenant": "Inquilino actual", + "argument-current-tenant": "Tenant actual", "argument-device": "Dispositivo", "argument-asset": "Activo", - "argument-customer": "Cliente", - "argument-tenant": "Inquilino actual", + "argument-customer": "Customer", + "argument-tenant": "Tenant actual", "argument-type": "Tipo de argumento", "see-debug-events": "Ver eventos de depuración", "attribute": "Atributo", @@ -1041,7 +1053,7 @@ "default-value": "Valor por defecto", "limit": "Máx. valores", "time-window": "Ventana de tiempo", - "customer-name": "Nombre del cliente", + "customer-name": "Nombre del customer", "asset-name": "Nombre del activo", "timeseries": "Serie temporal", "output": "Salida", @@ -1057,24 +1069,103 @@ "delete-multiple-title": "¿Estás seguro de que deseas eliminar { count, plural, =1 {1 campo calculado} other {# campos calculados} }?", "delete-multiple-text": "Ten cuidado, después de la confirmación todos los campos calculados seleccionados serán eliminados y todos los datos relacionados serán irrecuperables.", "test-with-this-message": "Probar con este mensaje", + "use-latest-timestamp": "Usar la última marca de tiempo", "hint": { - "arguments-simple-with-rolling": "Un campo calculado de tipo simple no debe contener claves con tipo de serie temporal acumulativa.", + "arguments-simple-with-rolling": "Un campo calculado de tipo simple no debe contener claves con tipo de serie temporal con agregación.", "arguments-empty": "Los argumentos no deben estar vacíos.", "expression-required": "Se requiere una expresión.", - "expression-invalid": "La expresión es inválida", - "expression-max-length": "La longitud de la expresión debe ser menor a 255 caracteres.", + "expression-invalid": "La expresión no es válida.", + "expression-max-length": "La longitud de la expresión debe ser inferior a 255 caracteres.", "argument-name-required": "Se requiere el nombre del argumento.", - "argument-name-pattern": "El nombre del argumento es inválido.", + "argument-name-pattern": "El nombre del argumento no es válido.", "argument-name-duplicate": "Ya existe un argumento con ese nombre.", "argument-name-max-length": "El nombre del argumento debe tener menos de 256 caracteres.", - "argument-name-forbidden": "El nombre del argumento está reservado y no puede ser utilizado.", + "argument-name-forbidden": "El nombre del argumento está reservado y no puede utilizarse.", "argument-type-required": "Se requiere el tipo de argumento.", - "max-args": "Se alcanzó el número máximo de argumentos.", + "max-args": "Se ha alcanzado el número máximo de argumentos.", "decimals-range": "Los decimales por defecto deben ser un número entre 0 y 15.", "expression": "La expresión por defecto muestra cómo transformar una temperatura de Fahrenheit a Celsius.", - "arguments-entity-not-found": "No se encontró la entidad objetivo del argumento." + "arguments-entity-not-found": "No se encontró la entidad de destino del argumento.", + "use-latest-timestamp": "Si está habilitado, el valor calculado se almacenará utilizando la marca de tiempo más reciente de la telemetría de los argumentos, en lugar del tiempo del servidor." } }, + "ai-models": { + "ai-models": "Modelos de IA", + "ai-model": "Modelo de IA", + "model": "Modelo", + "name": "Nombre", + "ai-provider": "Proveedor de IA", + "no-found": "No se encontraron modelos de IA", + "list": "{ count, plural, =1 {Un modelo} other {Lista de # modelos} }", + "selected-fields": "{ count, plural, =1 {1 modelo} other {# modelos} } seleccionados", + "add": "Agregar modelo", + "delete-model-title": "¿Estás seguro de que deseas eliminar el modelo '{{modelName}}'?", + "delete-model-text": "Ten cuidado, después de la confirmación el modelo y todos los datos relacionados serán irrecuperables.", + "delete-models-title": "¿Estás seguro de que deseas eliminar { count, plural, =1 {1 modelo} other {# modelos} }?", + "delete-models-text": "Ten cuidado, después de la confirmación todos los modelos seleccionados serán eliminados y todos los datos relacionados serán irrecuperables.", + "ai-providers": { + "openai": "OpenAI", + "azure-openai": "Azure OpenAI", + "google-ai-gemini": "Google AI Gemini", + "google-vertex-ai-gemini": "Google Vertex AI Gemini", + "mistral-ai": "Mistral AI", + "anthropic": "Anthropic", + "amazon-bedrock": "Amazon Bedrock", + "github-models": "Modelos de GitHub" + }, + "name-required": "Se requiere un nombre.", + "name-max-length": "El nombre debe tener 255 caracteres o menos.", + "provider": "Proveedor", + "api-key": "Clave API", + "api-key-required": "Se requiere la clave API.", + "project-id": "ID del proyecto", + "project-id-required": "Se requiere el ID del proyecto", + "location": "Ubicación", + "location-required": "Se requiere la ubicación.", + "service-account-key-file": "Archivo de clave de cuenta de servicio", + "service-account-key-file-required": "Se requiere el archivo de clave de cuenta de servicio.", + "no-file": "Ningún archivo seleccionado.", + "drop-file": "Suelta un archivo o haz clic para seleccionar uno y subirlo.", + "personal-access-token": "Token de acceso personal", + "personal-access-token-required": "Se requiere el token de acceso personal.", + "configuration": "Configuración", + "model-id": "ID del modelo", + "model-id-required": "Se requiere el ID del modelo.", + "deployment-name": "Nombre del despliegue", + "deployment-name-required": "Se requiere el nombre del despliegue", + "set": "Establecer", + "region": "Región", + "region-required": "Se requiere la región.", + "access-key-id": "ID de clave de acceso", + "access-key-id-required": "Se requiere el ID de clave de acceso.", + "secret-access-key": "Clave de acceso secreta", + "secret-access-key-required": "Se requiere la clave de acceso secreta.", + "temperature": "Temperatura", + "temperature-hint": "Ajusta el nivel de aleatoriedad en la salida del modelo. Valores más altos aumentan la aleatoriedad, mientras que valores más bajos la reducen.", + "temperature-min": "Debe ser 0 o mayor.", + "top-p": "Top P", + "top-p-hint": "Crea un conjunto de los tokens más probables entre los cuales el modelo puede elegir. Valores más altos generan un conjunto más grande y diverso; valores más bajos, uno más reducido.", + "top-p-min-max": "Debe ser mayor que 0 y hasta 1.", + "top-k": "Top K", + "top-k-hint": "Restringe las opciones del modelo a un conjunto fijo de los \"K\" tokens más probables.", + "top-k-min": "Debe ser 0 o mayor.", + "presence-penalty": "Penalización por presencia", + "presence-penalty-hint": "Aplica una penalización fija a la probabilidad de un token si ya ha aparecido en el texto.", + "frequency-penalty": "Penalización por frecuencia", + "frequency-penalty-hint": "Aplica una penalización a la probabilidad de un token que aumenta en función de su frecuencia en el texto.", + "max-output-tokens": "Número máximo de tokens de salida", + "max-output-tokens-min": "Debe ser mayor que 0.", + "max-output-tokens-hint": "Define el número máximo de tokens que el modelo puede generar en una sola respuesta.", + "endpoint": "Endpoint", + "endpoint-required": "Se requiere el endpoint.", + "service-version": "Versión del servicio", + "check-connectivity": "Verificar conectividad", + "check-connectivity-success": "La solicitud de prueba fue exitosa", + "check-connectivity-failed": "La solicitud de prueba falló", + "no-model-matching": "No se encontraron modelos que coincidan con '{{entity}}'.", + "model-required": "Se requiere un modelo.", + "no-model-text": "No se encontraron modelos." + }, "confirm-on-exit": { "message": "Tienes cambios sin guardar. ¿Estás seguro de que deseas salir de esta página?", "html-message": "Tienes cambios sin guardar.
    ¿Estás seguro de que deseas salir de esta página?", @@ -1145,36 +1236,36 @@ "color": "Color" }, "customer": { - "customer": "Cliente", - "customers": "Clientes", - "management": "Gestión de clientes", - "dashboard": "Tablero del cliente", - "dashboards": "Tableros del cliente", - "devices": "Dispositivos del cliente", - "entity-views": "Vistas de entidad del cliente", - "assets": "Activos del cliente", + "customer": "Customer", + "customers": "Customers", + "management": "Gestión de customers", + "dashboard": "Tablero del customer", + "dashboards": "Tableros del customer", + "devices": "Dispositivos del customer", + "entity-views": "Vistas de entidad del customer", + "assets": "Activos del customer", "public-dashboards": "Tableros públicos", "public-devices": "Dispositivos públicos", "public-assets": "Activos públicos", "public-entity-views": "Vistas de entidad públicas", - "add": "Agregar cliente", - "delete": "Eliminar cliente", - "manage-customer-users": "Gestionar usuarios del cliente", - "manage-customer-devices": "Gestionar dispositivos del cliente", - "manage-customer-dashboards": "Gestionar tableros del cliente", + "add": "Agregar customer", + "delete": "Eliminar customer", + "manage-customer-users": "Gestionar usuarios del Customer", + "manage-customer-devices": "Gestionar dispositivos del Customer", + "manage-customer-dashboards": "Gestionar tableros del Customer", "manage-public-devices": "Gestionar dispositivos públicos", "manage-public-dashboards": "Gestionar tableros públicos", - "manage-customer-assets": "Gestionar activos del cliente", - "manage-customer-edges": "Gestionar edge del cliente", + "manage-customer-assets": "Gestionar activos del Customer", + "manage-customer-edges": "Gestionar edges del Customer", "manage-public-assets": "Gestionar activos públicos", - "add-customer-text": "Agregar nuevo cliente", - "no-customers-text": "No se encontraron clientes", - "customer-details": "Detalles del cliente", - "delete-customer-title": "¿Estás seguro de que deseas eliminar el cliente '{{customerTitle}}'?", - "delete-customer-text": "Ten cuidado, después de la confirmación el cliente y todos los datos relacionados serán irrecuperables.", - "delete-customers-title": "¿Estás seguro de que deseas eliminar { count, plural, =1 {1 cliente} other {# clientes} }?", - "delete-customers-action-title": "Eliminar { count, plural, =1 {1 cliente} other {# clientes} }", - "delete-customers-text": "Ten cuidado, después de la confirmación todos los clientes seleccionados serán eliminados y todos los datos relacionados serán irrecuperables.", + "add-customer-text": "Agregar nuevo customer", + "no-customers-text": "No se encontraron customers", + "customer-details": "Detalles del customer", + "delete-customer-title": "¿Estás seguro de que deseas eliminar el customer '{{customerTitle}}'?", + "delete-customer-text": "Ten cuidado, después de la confirmación el customer y todos los datos relacionados serán irrecuperables.", + "delete-customers-title": "¿Estás seguro de que deseas eliminar { count, plural, =1 {1 customer} other {# customers} }?", + "delete-customers-action-title": "Eliminar { count, plural, =1 {1 customer} other {# customers} }", + "delete-customers-text": "Ten cuidado, después de la confirmación todos los customers seleccionados serán eliminados y todos los datos relacionados serán irrecuperables.", "manage-users": "Gestionar usuarios", "manage-assets": "Gestionar activos", "manage-devices": "Gestionar dispositivos", @@ -1185,17 +1276,17 @@ "description": "Descripción", "details": "Detalles", "events": "Eventos", - "copyId": "Copiar ID del cliente", - "idCopiedMessage": "ID del cliente copiado al portapapeles", - "select-customer": "Seleccionar cliente", - "no-customers-matching": "No se encontraron clientes que coincidan con '{{entity}}'.", - "customer-required": "Se requiere un cliente", - "select-default-customer": "Seleccionar cliente por defecto", - "default-customer": "Cliente por defecto", - "default-customer-required": "Se requiere el cliente por defecto para depurar el tablero a nivel de inquilino", - "search": "Buscar clientes", - "selected-customers": "{ count, plural, =1 {1 cliente} other {# clientes} } seleccionado(s)", - "edges": "Instancias de edge del cliente", + "copyId": "Copiar ID del customer", + "idCopiedMessage": "ID del customer copiado al portapapeles", + "select-customer": "Seleccionar customer", + "no-customers-matching": "No se encontraron customers que coincidan con '{{entity}}'.", + "customer-required": "Se requiere un customer", + "select-default-customer": "Seleccionar customer por defecto", + "default-customer": "Customer por defecto", + "default-customer-required": "Se requiere el customer por defecto para depurar el tablero a nivel de Tenant", + "search": "Buscar customers", + "selected-customers": "{ count, plural, =1 {1 customer} other {# customers} } seleccionado(s)", + "edges": "Instancias de edge del customer", "manage-edges": "Gestionar edge" }, "css-size": { @@ -1232,19 +1323,19 @@ "management": "Gestión de tableros", "view-dashboards": "Ver tableros", "add": "Añadir tablero", - "assign-dashboard-to-customer": "Asignar tablero(s) al cliente", - "assign-dashboard-to-customer-text": "Por favor, seleccione los tableros para asignar al cliente", - "assign-to-customer-text": "Por favor, seleccione el cliente para asignar el/los tablero(s)", - "assign-to-customer": "Asignar al cliente", - "unassign-from-customer": "Desasignar del cliente", + "assign-dashboard-to-customer": "Asignar tablero(s) al customer", + "assign-dashboard-to-customer-text": "Por favor, seleccione los tableros para asignar al customer", + "assign-to-customer-text": "Por favor, seleccione el customer para asignar el/los tablero(s)", + "assign-to-customer": "Asignar al customer", + "unassign-from-customer": "Desasignar del customer", "make-public": "Hacer público el tablero", "make-private": "Hacer privado el tablero", - "manage-assigned-customers": "Gestionar clientes asignados", - "assigned-customers": "Clientes asignados", - "assign-to-customers": "Asignar tablero(s) a clientes", - "assign-to-customers-text": "Selecciona los clientes a los que deseas asignar el/los tablero(s)", - "unassign-from-customers": "Desasignar tablero(s) de clientes", - "unassign-from-customers-text": "Selecciona los clientes de los que deseas desasignar el/los tablero(s)", + "manage-assigned-customers": "Gestionar customers asignados", + "assigned-customers": "Customers asignados", + "assign-to-customers": "Asignar tablero(s) a customers", + "assign-to-customers-text": "Selecciona los customers a los que deseas asignar el/los tablero(s)", + "unassign-from-customers": "Desasignar tablero(s) de customers", + "unassign-from-customers-text": "Selecciona los customers de los que deseas desasignar el/los tablero(s)", "no-dashboards-text": "No se encontraron tableros", "no-widgets": "No hay widgets configurados", "add-widget": "Agregar nuevo widget", @@ -1268,21 +1359,21 @@ "add-dashboard-text": "Agregar nuevo tablero", "assign-dashboards": "Asignar tableros", "assign-new-dashboard": "Asignar nuevo tablero", - "assign-dashboards-text": "Asignar { count, plural, =1 {1 tablero} other {# tableros} } a clientes", - "unassign-dashboards-action-text": "Desasignar { count, plural, =1 {1 tablero} other {# tableros} } de clientes", + "assign-dashboards-text": "Asignar { count, plural, =1 {1 tablero} other {# tableros} } a customers", + "unassign-dashboards-action-text": "Desasignar { count, plural, =1 {1 tablero} other {# tableros} } de customers", "delete-dashboards": "Eliminar tableros", "unassign-dashboards": "Desasignar tableros", - "unassign-dashboards-action-title": "Desasignar { count, plural, =1 {1 tablero} other {# tableros} } del cliente", + "unassign-dashboards-action-title": "Desasignar { count, plural, =1 {1 tablero} other {# tableros} } del customer", "delete-dashboard-title": "¿Estás seguro de que deseas eliminar el tablero '{{dashboardTitle}}'?", "delete-dashboard-text": "Ten cuidado, después de la confirmación el tablero y todos los datos relacionados serán irrecuperables.", "delete-dashboards-title": "¿Estás seguro de que deseas eliminar { count, plural, =1 {1 tablero} other {# tableros} }?", "delete-dashboards-action-title": "Eliminar { count, plural, =1 {1 tablero} other {# tableros} }", "delete-dashboards-text": "Ten cuidado, después de la confirmación todos los tableros seleccionados serán eliminados y todos los datos relacionados serán irrecuperables.", "unassign-dashboard-title": "¿Estás seguro de que deseas desasignar el tablero '{{dashboardTitle}}'?", - "unassign-dashboard-text": "Después de la confirmación, el tablero será desasignado y no será accesible por el cliente.", + "unassign-dashboard-text": "Después de la confirmación, el tablero será desasignado y no será accesible por el customer.", "unassign-dashboard": "Desasignar tablero", "unassign-dashboards-title": "¿Estás seguro de que deseas desasignar { count, plural, =1 {1 tablero} other {# tableros} }?", - "unassign-dashboards-text": "Después de la confirmación, todos los tableros seleccionados serán desasignados y no serán accesibles por el cliente.", + "unassign-dashboards-text": "Después de la confirmación, todos los tableros seleccionados serán desasignados y no serán accesibles por el customer.", "public-dashboard-title": "El tablero ahora es público", "public-dashboard-text": "Tu tablero {{dashboardTitle}} ahora es público y accesible mediante el siguiente enlace:", "public-dashboard-notice": "Nota: No olvides hacer públicos los dispositivos relacionados para acceder a sus datos.", @@ -1383,8 +1474,8 @@ "alias-resolution-error-title": "Error de configuración de alias del tablero", "invalid-aliases-config": "No se pudieron encontrar dispositivos que coincidan con algunos de los filtros de alias.
    Por favor, contacta con tu administrador para resolver este problema.", "select-devices": "Seleccionar dispositivos", - "assignedToCustomer": "Asignado al cliente", - "assignedToCustomers": "Asignado a clientes", + "assignedToCustomer": "Asignado al customer", + "assignedToCustomers": "Asignado a customers", "public": "Público", "copyId": "Copiar ID del tablero", "idCopiedMessage": "El ID del tablero ha sido copiado al portapapeles", @@ -1550,24 +1641,24 @@ "device-name-filter-required": "Se requiere el filtro de nombre del dispositivo.", "device-name-filter-no-device-matched": "No se encontraron dispositivos que comiencen con '{{device}}'.", "add": "Agregar dispositivo", - "assign-to-customer": "Asignar al cliente", - "assign-device-to-customer": "Asignar dispositivo(s) al cliente", - "assign-device-to-customer-text": "Selecciona los dispositivos que deseas asignar al cliente", + "assign-to-customer": "Asignar al customer", + "assign-device-to-customer": "Asignar dispositivo(s) al customer", + "assign-device-to-customer-text": "Selecciona los dispositivos que deseas asignar al customer", "make-public": "Hacer público el dispositivo", "make-private": "Hacer privado el dispositivo", "no-devices-text": "No se encontraron dispositivos", - "assign-to-customer-text": "Selecciona el cliente al que deseas asignar el/los dispositivo(s)", + "assign-to-customer-text": "Selecciona el customer al que deseas asignar el/los dispositivo(s)", "device-details": "Detalles del dispositivo", "add-device-text": "Agregar nuevo dispositivo", "credentials": "Credenciales", "manage-credentials": "Gestionar credenciales", "delete": "Eliminar dispositivo", "assign-devices": "Asignar dispositivos", - "assign-devices-text": "Asignar { count, plural, =1 {1 dispositivo} other {# dispositivos} } al cliente", + "assign-devices-text": "Asignar { count, plural, =1 {1 dispositivo} other {# dispositivos} } al customer", "delete-devices": "Eliminar dispositivos", - "unassign-from-customer": "Desasignar del cliente", + "unassign-from-customer": "Desasignar del customer", "unassign-devices": "Desasignar dispositivos", - "unassign-devices-action-title": "Desasignar { count, plural, =1 {1 dispositivo} other {# dispositivos} } del cliente", + "unassign-devices-action-title": "Desasignar { count, plural, =1 {1 dispositivo} other {# dispositivos} } del customer", "unassign-device-from-edge-title": "¿Estás seguro de que deseas desasignar el dispositivo '{{deviceName}}'?", "unassign-device-from-edge-text": "Después de la confirmación, el dispositivo será desasignado y no será accesible por el edge.", "unassign-devices-from-edge": "Desasignar dispositivos del edge", @@ -1583,10 +1674,10 @@ "delete-devices-action-title": "Eliminar { count, plural, =1 {1 dispositivo} other {# dispositivos} }", "delete-devices-text": "Ten cuidado, después de la confirmación todos los dispositivos seleccionados serán eliminados y todos los datos relacionados serán irrecuperables.", "unassign-device-title": "¿Estás seguro de que deseas desasignar el dispositivo '{{deviceName}}'?", - "unassign-device-text": "Después de la confirmación, el dispositivo será desasignado y no será accesible por el cliente.", + "unassign-device-text": "Después de la confirmación, el dispositivo será desasignado y no será accesible por el customer.", "unassign-device": "Desasignar dispositivo", "unassign-devices-title": "¿Estás seguro de que deseas desasignar { count, plural, =1 {1 dispositivo} other {# dispositivos} }?", - "unassign-devices-text": "Después de la confirmación, todos los dispositivos seleccionados serán desasignados y no serán accesibles por el cliente.", + "unassign-devices-text": "Después de la confirmación, todos los dispositivos seleccionados serán desasignados y no serán accesibles por el customer.", "device-credentials": "Credenciales del dispositivo", "loading-device-credentials": "Cargando credenciales del dispositivo...", "credentials-type": "Tipo de credenciales", @@ -1738,7 +1829,7 @@ "type-units": "Unidades", "type-icon": "Ícono", "type-fieldset": "Conjunto de campos", - "type-array": "Arreglo", + "type-array": "Array", "type-html-section": "Sección HTML", "group-title": "Título del grupo", "no-properties": "No hay propiedades configuradas", @@ -1754,6 +1845,7 @@ "selected-options-limit": "Límite de opciones seleccionadas", "advanced-ui-settings": "Configuración avanzada de la interfaz", "disable-on-property": "Deshabilitar en propiedad", + "disable-on-property-none": "Ninguno (campo siempre habilitado)", "display-condition-function": "Función de condición de visualización", "sub-label": "Subetiqueta", "vertical-divider-after": "Divisor vertical después", @@ -1787,7 +1879,8 @@ "array-item": "Elemento del arreglo", "item-type": "Tipo de elemento", "item-name": "Nombre del elemento", - "no-items": "No hay elementos" + "no-items": "No hay elementos", + "support-unit-conversion": "Soportar conversión de unidades" }, "clear-form": "Limpiar formulario", "clear-form-prompt": "¿Estás seguro de que deseas eliminar todas las propiedades del formulario?", @@ -1897,24 +1990,25 @@ "no-device-profiles-found": "No se encontraron perfiles de dispositivos.", "create-new-device-profile": "¡Crear uno nuevo!", "mqtt-device-topic-filters": "Filtros de temas MQTT del dispositivo", - "mqtt-device-topic-filters-unique": "Los filtros de temas MQTT deben ser únicos.", - "mqtt-device-topic-filters-spark-plug": "Nodo EoN de Sparkplug B para MQTT.", - "mqtt-device-topic-filters-spark-plug-hint": "Permite conexiones de nodos EoN con formato de carga útil y tema de Sparkplug B.", - "mqtt-device-topic-filters-spark-plug-attribute-metric-names": "Métricas SparkPlug para almacenar como atributos.", - "mqtt-device-topic-filters-spark-plug-attribute-metric-names-hint": "Nombres de métricas SparkPlug que se almacenarán como atributos del dispositivo. Todas las demás métricas se almacenarán como telemetría.", + "mqtt-device-topic-filters-unique": "Los filtros de temas MQTT del dispositivo deben ser únicos.", + "mqtt-device-topic-filters-spark-plug": "Nodo Edge of Network (EoN) Sparkplug B MQTT.", + "mqtt-device-topic-filters-spark-plug-hint": "Permitir conexiones desde nodos EoN con formato de tema y carga útil Sparkplug B.", + "mqtt-device-topic-filters-spark-plug-attribute-metric-names": "Métricas Sparkplug a almacenar como atributos.", + "mqtt-device-topic-filters-spark-plug-attribute-metric-names-hint": "Nombres de métricas Sparkplug que se almacenarán como atributos del dispositivo. Todas las demás métricas se almacenarán como telemetría del dispositivo.", "mqtt-device-payload-type": "Carga útil del dispositivo MQTT", "mqtt-device-payload-type-json": "JSON", "mqtt-device-payload-type-proto": "Protobuf", "mqtt-enable-compatibility-with-json-payload-format": "Habilitar compatibilidad con otros formatos de carga útil.", - "mqtt-enable-compatibility-with-json-payload-format-hint": "Cuando está habilitado, la plataforma usará por defecto Protobuf. Si falla el análisis, intentará usar JSON. Útil para compatibilidad durante actualizaciones de firmware. Puede degradar el rendimiento, se recomienda deshabilitarlo cuando todos los dispositivos estén actualizados.", - "mqtt-use-json-format-for-default-downlink-topics": "Usar formato Json para temas de bajada predeterminados", - "mqtt-use-json-format-for-default-downlink-topics-hint": "Cuando está habilitado, se usa formato Json para enviar atributos y RPC en temas como: v1/devices/me/attributes/response/$request_id, etc. No afecta suscripciones en temas v2: v2/a/res/$request_id, etc.", - "mqtt-send-ack-on-validation-exception": "Enviar PUBACK en error de validación de mensaje PUBLISH", - "mqtt-send-ack-on-validation-exception-hint": "Por defecto se cierra la sesión MQTT ante errores. Con esta opción, se envía un ACK en su lugar.", + "mqtt-enable-compatibility-with-json-payload-format-hint": "Cuando está habilitado, la plataforma utilizará el formato de carga útil Protobuf por defecto. Si falla el análisis, intentará usar el formato JSON. Útil para compatibilidad con versiones anteriores durante actualizaciones de firmware. Por ejemplo, si la versión inicial del firmware usa JSON y la nueva usa Protobuf, durante la actualización será necesario soportar ambos formatos simultáneamente. Este modo introduce una ligera degradación en el rendimiento, por lo que se recomienda desactivarlo una vez que todos los dispositivos hayan sido actualizados.", + "mqtt-use-json-format-for-default-downlink-topics": "Usar formato JSON para los temas de bajada predeterminados", + "mqtt-use-json-format-for-default-downlink-topics-hint": "Cuando está habilitado, la plataforma usará formato JSON para enviar atributos y RPC a través de los siguientes temas: v1/devices/me/attributes/response/$request_id, v1/devices/me/attributes, v1/devices/me/rpc/request/$request_id, v1/devices/me/rpc/response/$request_id. Esta configuración no afecta las suscripciones a atributos y RPC usando los nuevos temas (v2): v2/a/res/$request_id, v2/a, v2/r/req/$request_id, v2/r/res/$request_id. Donde $request_id es un identificador de solicitud entero.", + "mqtt-send-ack-on-validation-exception": "Enviar PUBACK al fallar la validación del mensaje PUBLISH", + "mqtt-send-ack-on-validation-exception-hint": "Por defecto, la plataforma cerrará la sesión MQTT al fallar la validación del mensaje. Cuando está habilitado, enviará una confirmación de publicación en lugar de cerrar la sesión.", + "mqtt-protocol-version": "Versión del protocolo", "snmp-add-mapping": "Agregar mapeo SNMP", - "snmp-mapping-not-configured": "No hay mapeo de OID a serie temporal/telemetría configurado", - "snmp-timseries-or-attribute-name": "Nombre de serie temporal/atributo para mapeo", - "snmp-timseries-or-attribute-type": "Tipo de serie temporal/atributo para mapeo", + "snmp-mapping-not-configured": "No se ha configurado ningún mapeo de OID a serie temporal/telemetría", + "snmp-timseries-or-attribute-name": "Nombre de serie temporal/atributo para el mapeo", + "snmp-timseries-or-attribute-type": "Tipo de serie temporal/atributo para el mapeo", "snmp-method-pdu-type-get-request": "GetRequest", "snmp-method-pdu-type-get-next-request": "GetNextRequest", "snmp-oid": "OID", @@ -1986,8 +2080,8 @@ "propagate-alarm": "Propagar alarma a entidades relacionadas", "alarm-rule-relation-types-list": "Tipos de relación", "alarm-rule-relation-types-list-hint": "Define tipos de relación para filtrar las entidades relacionadas. Si no se define, la alarma se propagará a todas las entidades relacionadas.", - "propagate-alarm-to-owner": "Propagar alarma al propietario de la entidad (Cliente o Inquilino)", - "propagate-alarm-to-tenant": "Propagar alarma al inquilino", + "propagate-alarm-to-owner": "Propagar alarma al propietario de la entidad (Customer o Tenant)", + "propagate-alarm-to-tenant": "Propagar alarma al Tenant", "alarm-rule-condition": "Condición de la regla de alarma", "enter-alarm-rule-condition-prompt": "Por favor, agregue condición para la regla de alarma", "edit-alarm-rule-condition": "Editar condición de la regla de alarma", @@ -2171,10 +2265,13 @@ "add-lwm2m-server-config": "Agregar servidor LwM2M", "no-config-servers": "No hay servidores configurados", "others-tab": "Otras configuraciones", - "client-strategy": "Estrategia del cliente al conectarse", + "ota-update": "Actualización OTA", + "use-object-19-for-ota-update": "Usar el Objeto 19 para metadatos del archivo OTA (checksum, tamaño, versión, nombre)", + "use-object-19-for-ota-update-hint": "Usar el Objeto de Recursos con ObjectId = 19 para actualizaciones OTA: FirmWare → InstanceId = 65534, SoftWare → InstanceId = 65535. El formato de datos es JSON codificado en Base64. Este JSON contiene metadatos del archivo OTA (información del archivo): \"Checksum\" (SHA256). Campos adicionales: \"Title\" (nombre de la OTA), \"Version\" (versión de la OTA), \"File Name\" (nombre del archivo para almacenar la OTA en el cliente), \"File Size\" (tamaño de la OTA en bytes).", + "client-strategy": "Estrategia del cliente al conectar", "client-strategy-label": "Estrategia", - "client-strategy-only-observe": "Solo enviar solicitud Observe al cliente después de la conexión inicial", - "client-strategy-read-all": "Leer todos los recursos y enviar solicitud Observe al cliente después del registro", + "client-strategy-only-observe": "Solo solicitud Observe al cliente después de la conexión inicial", + "client-strategy-read-all": "Leer todos los recursos y solicitud Observe al cliente después del registro", "fw-update": "Actualización de firmware", "fw-update-strategy": "Estrategia de actualización de firmware", "fw-update-strategy-data": "Enviar actualización de firmware como archivo binario usando Objeto 19 y Recurso 0 (Data)", @@ -2201,7 +2298,17 @@ "default-object-id": "Versión de objeto predeterminado (Atributo)", "default-object-id-ver": { "v1-0": "1.0", - "v1-1": "1.1" + "v1-1": "1.1", + "v1-2": "1.2" + }, + "observe-strategy": { + "observe-strategy": "Estrategia de observación", + "single": "Individual", + "single-description": "Una solicitud Observe por recurso (mayor precisión, más tráfico de red)", + "composite-all": "Compuesta - todos", + "composite-all-description": "Todos los recursos se observan con una única solicitud Composite Observe (más eficiente, menos flexible)", + "composite-by-object": "Compuesta por objetos", + "composite-by-object-description": "Los recursos se agrupan por tipo de objeto y se observan usando solicitudes Composite Observe separadas (enfoque equilibrado)" } }, "snmp": { @@ -2296,18 +2403,18 @@ "event-action": "Acción del evento", "entity-id": "ID de entidad", "select-edge-type": "Seleccionar tipo de edge", - "assign-to-customer": "Asignar al cliente", - "assign-to-customer-text": "Por favor, seleccione el cliente para asignar el(los) edge(s)", - "assign-edge-to-customer": "Asignar edge(s) al cliente", - "assign-edge-to-customer-text": "Por favor, seleccione los edges para asignar al cliente", - "assignedToCustomer": "Asignado al cliente", + "assign-to-customer": "Asignar al customer", + "assign-to-customer-text": "Por favor, seleccione el customer para asignar el(los) edge(s)", + "assign-edge-to-customer": "Asignar edge(s) al customer", + "assign-edge-to-customer-text": "Por favor, seleccione los edges para asignar al customer", + "assignedToCustomer": "Asignado al customer", "edge-public": "El edge es público", "assigned-to-customer": "Asignado a: {{customerTitle}}", - "unassign-from-customer": "Desasignar del cliente", + "unassign-from-customer": "Desasignar del customer", "unassign-edge-title": "¿Está seguro de que desea desasignar el edge '{{edgeName}}'?", - "unassign-edge-text": "Después de la confirmación, el edge se desasignará y no será accesible por el cliente.", + "unassign-edge-text": "Después de la confirmación, el edge se desasignará y no será accesible por el customer.", "unassign-edges-title": "¿Está seguro de que desea desasignar { count, plural, =1 {1 edge} other {# edges} }?", - "unassign-edges-text": "Después de la confirmación, todos los edges seleccionados se desasignarán y no serán accesibles por el cliente.", + "unassign-edges-text": "Después de la confirmación, todos los edges seleccionados se desasignarán y no serán accesibles por el customer.", "make-public": "Hacer público el edge", "make-public-edge-title": "¿Está seguro de que desea hacer público el edge '{{edgeName}}'?", "make-public-edge-text": "Después de la confirmación, el edge y todos sus datos se harán públicos y serán accesibles por otros.", @@ -2373,9 +2480,9 @@ "type-rule-chain-metadata": "Metadatos de cadena de reglas", "type-edge": "Edge", "type-user": "Usuario", - "type-tenant": "Inquilino", - "type-tenant-profile": "Perfil de inquilino", - "type-customer": "Cliente", + "type-tenant": "Tenant", + "type-tenant-profile": "Perfil de Tenant", + "type-customer": "Customer", "type-relation": "Relación", "type-widgets-bundle": "Paquete de widgets", "type-widgets-type": "Tipo de widget", @@ -2390,8 +2497,8 @@ "action-type-attributes-deleted": "Atributos eliminados", "action-type-timeseries-updated": "Serie temporal actualizada", "action-type-credentials-updated": "Credenciales actualizadas", - "action-type-assigned-to-customer": "Asignado al cliente", - "action-type-unassigned-from-customer": "Desasignado del cliente", + "action-type-assigned-to-customer": "Asignado al Customer", + "action-type-unassigned-from-customer": "Desasignado del Customer", "action-type-relation-add-or-update": "Relación agregada o actualizada", "action-type-relation-deleted": "Relación eliminada", "action-type-rpc-call": "Llamada RPC", @@ -2486,18 +2593,18 @@ "type-plugins": "Plugins", "list-of-plugins": "{ count, plural, =1 {Un plugin} other {Lista de # plugins} }", "plugin-name-starts-with": "Plugins cuyos nombres comienzan con '{{prefix}}'", - "type-tenant": "Inquilino", - "type-tenants": "Inquilinos", - "list-of-tenants": "{ count, plural, =1 {Un inquilino} other {Lista de # inquilinos} }", - "tenant-name-starts-with": "Inquilinos cuyos nombres comienzan con '{{prefix}}'", - "type-tenant-profile": "Perfil de inquilino", - "type-tenant-profiles": "Perfiles de inquilino", - "list-of-tenant-profiles": "{ count, plural, =1 {Un perfil de inquilino} other {Lista de # perfiles de inquilino} }", - "tenant-profile-name-starts-with": "Perfiles de inquilino cuyos nombres comienzan con '{{prefix}}'", - "type-customer": "Cliente", - "type-customers": "Clientes", - "list-of-customers": "{ count, plural, =1 {Un cliente} other {Lista de # clientes} }", - "customer-name-starts-with": "Clientes cuyos nombres comienzan con '{{prefix}}'", + "type-tenant": "Tenant", + "type-tenants": "Tenants", + "list-of-tenants": "{ count, plural, =1 {Un tenant} other {Lista de # tenants} }", + "tenant-name-starts-with": "Tenants cuyos nombres comienzan con '{{prefix}}'", + "type-tenant-profile": "Perfil de tenant", + "type-tenant-profiles": "Perfiles de tenant", + "list-of-tenant-profiles": "{ count, plural, =1 {Un perfil de tenant} other {Lista de # perfiles de tenant} }", + "tenant-profile-name-starts-with": "Perfiles de tenant cuyos nombres comienzan con '{{prefix}}'", + "type-customer": "Customer", + "type-customers": "Customers", + "list-of-customers": "{ count, plural, =1 {Un customer} other {Lista de # customers} }", + "customer-name-starts-with": "Customers cuyos nombres comienzan con '{{prefix}}'", "type-user": "Usuario", "type-users": "Usuarios", "list-of-users": "{ count, plural, =1 {Un usuario} other {Lista de # usuarios} }", @@ -2518,12 +2625,14 @@ "type-rulenodes": "Nodos de regla", "list-of-rulenodes": "{ count, plural, =1 {Un nodo de regla} other {Lista de # nodos de regla} }", "rulenode-name-starts-with": "Nodos de regla cuyos nombres comienzan con '{{prefix}}'", - "type-current-customer": "Cliente actual", - "type-current-tenant": "Inquilino actual", + "type-current-customer": "Customer actual", + "type-current-tenant": "Tenant actual", "type-current-user": "Usuario actual", "type-current-user-owner": "Propietario del usuario actual", "type-calculated-field": "Campo calculado", "type-calculated-fields": "Campos calculados", + "type-ai-model": "Modelo de IA", + "type-ai-models": "Modelos de IA", "type-widgets-bundle": "Paquete de widgets", "type-widgets-bundles": "Paquetes de widgets", "list-of-widgets-bundles": "{ count, plural, =1 {Un paquete de widgets} other {Lista de # paquetes de widgets} }", @@ -2553,6 +2662,8 @@ "type-tb-resources": "Recursos", "list-of-tb-resources": "{ count, plural, =1 {Un recurso} other {Lista de # recursos} }", "type-ota-package": "Paquete OTA", + "type-ota-packages": "Paquetes OTA", + "list-of-ota-packages": "{ count, plural, =1 {Un paquete OTA} other {Lista de # paquetes OTA} }", "type-rpc": "RPC", "type-queue": "Cola", "type-queue-stats": "Estadísticas de cola", @@ -2643,13 +2754,13 @@ "add-entity-view-text": "Agregar nueva vista de entidad", "delete": "Eliminar vista de entidad", "assign-entity-views": "Asignar vistas de entidad", - "assign-entity-views-text": "Asignar { count, plural, =1 {1 vista de entidad} other {# vistas de entidad} } al cliente", + "assign-entity-views-text": "Asignar { count, plural, =1 {1 vista de entidad} other {# vistas de entidad} } al Customer", "delete-entity-views": "Eliminar vistas de entidad", "make-public": "Hacer pública la vista de entidad", "make-private": "Hacer privada la vista de entidad", - "unassign-from-customer": "Desasignar del cliente", + "unassign-from-customer": "Desasignar del customer", "unassign-entity-views": "Desasignar vistas de entidad", - "unassign-entity-views-action-title": "Desasignar { count, plural, =1 {1 vista de entidad} other {# vistas de entidad} } del cliente", + "unassign-entity-views-action-title": "Desasignar { count, plural, =1 {1 vista de entidad} other {# vistas de entidad} } del customer", "assign-new-entity-view": "Asignar nueva vista de entidad", "delete-entity-view-title": "¿Está seguro de que desea eliminar la vista de entidad '{{entityViewName}}'?", "delete-entity-view-text": "Tenga cuidado, después de la confirmación, la vista de entidad y todos los datos relacionados serán irrecuperables.", @@ -2661,10 +2772,10 @@ "make-private-entity-view-title": "¿Está seguro de que desea hacer privada la vista de entidad '{{entityViewName}}'?", "make-private-entity-view-text": "Después de la confirmación, la vista de entidad y todos sus datos serán privados y no estarán accesibles por otros.", "unassign-entity-view-title": "¿Está seguro de que desea desasignar la vista de entidad '{{entityViewName}}'?", - "unassign-entity-view-text": "Después de la confirmación, la vista de entidad será desasignada y no estará accesible por el cliente.", + "unassign-entity-view-text": "Después de la confirmación, la vista de entidad será desasignada y no estará accesible por el customer.", "unassign-entity-view": "Desasignar vista de entidad", "unassign-entity-views-title": "¿Está seguro de que desea desasignar { count, plural, =1 {1 vista de entidad} other {# vistas de entidad} }?", - "unassign-entity-views-text": "Después de la confirmación, todas las vistas de entidad seleccionadas serán desasignadas y no estarán accesibles por el cliente.", + "unassign-entity-views-text": "Después de la confirmación, todas las vistas de entidad seleccionadas serán desasignadas y no estarán accesibles por el customer.", "entity-view-type": "Tipo de vista de entidad", "entity-view-type-required": "El tipo de vista de entidad es obligatorio.", "select-entity-view-type": "Seleccionar tipo de vista de entidad", @@ -2683,7 +2794,7 @@ "details": "Detalles", "copyId": "Copiar ID de la vista de entidad", "idCopiedMessage": "El ID de la vista de entidad se ha copiado al portapapeles", - "assignedToCustomer": "Asignado al cliente", + "assignedToCustomer": "Asignado al customer", "unable-entity-view-device-alias-title": "No se puede eliminar el alias de la vista de entidad", "unable-entity-view-device-alias-text": "El alias del dispositivo '{{entityViewAlias}}' no puede eliminarse porque lo usan los siguientes widgets:
    {{widgetsList}}", "select-entity-view": "Seleccionar vista de entidad", @@ -2938,6 +3049,7 @@ "missing-key-filters-error": "Faltan filtros clave para el filtro '{{filter}}'.", "filter": "Filtro", "editable": "Editable", + "editable-hint": "Permitir que el usuario cambie el valor del filtro en los tableros.", "no-filters-found": "No se encontraron filtros.", "no-filter-text": "Ningún filtro especificado", "add-filter-prompt": "Por favor, agregue un filtro", @@ -2977,6 +3089,8 @@ "filter-user-params": "Parámetros del predicado del filtro", "user-parameters": "Parámetros del usuario", "display-label": "Etiqueta para mostrar", + "custom-label": "Etiqueta personalizada", + "custom-label-hint": "Habilita esta opción para establecer tu propia etiqueta para el filtro. Si está deshabilitada, se generará una etiqueta automáticamente.", "order-priority": "Prioridad de orden del campo", "key-filter": "Filtro de clave", "key-filters": "Filtros de clave", @@ -3009,7 +3123,7 @@ "date": "Fecha", "time": "Hora", "current-tenant": "Tenant actual", - "current-customer": "Cliente actual", + "current-customer": "Customer actual", "current-user": "Usuario actual", "current-device": "Dispositivo actual", "default-value": "Valor predeterminado", @@ -3021,7 +3135,8 @@ "switch-to-dynamic-value": "Cambiar a valor dinámico", "switch-to-default-value": "Cambiar a valor predeterminado", "inherit-owner": "Heredar del propietario", - "source-attribute-not-set": "Si el atributo de origen no está establecido" + "source-attribute-not-set": "Si el atributo de origen no está establecido", + "unit": "Unidad" }, "fullscreen": { "expand": "Expandir a pantalla completa", @@ -3406,6 +3521,7 @@ "power-button-background": "Fondo del botón de encendido", "value-box-background": "Fondo de la caja de valor", "value-units": "Unidades del valor", + "enable-units-scale": "Habilitar unidades en la escala", "filtration-mode": "Modo de filtración", "filtration-mode-hint": "Valor entero que indica el modo de filtración actual.", "filtration-mode-update": "Estado de actualización del modo de filtración", @@ -3722,6 +3838,8 @@ "mobile-package-max-length": "El paquete de la aplicación debe tener menos de 256 caracteres", "mobile-package-required": "Se requiere el paquete de la aplicación", "mobile-package-pattern": "Formato inválido del paquete de la aplicación", + "mobile-package-title": "Título de la aplicación", + "mobile-package-title-max-length": "El título de la aplicación debe tener menos de 256 caracteres", "no-application": "No se encontraron aplicaciones", "no-bundles": "No se encontraron paquetes", "platform-type": "Tipo de plataforma", @@ -3801,21 +3919,17 @@ "configuration-dialog": "Diálogo de configuración", "configuration-app": "Aplicación de configuración", "configuration-step": { - "prepare-environment-title": "Preparar entorno de desarrollo", - "prepare-environment-text": "La aplicación móvil ThingsBoard Flutter requiere el SDK de Flutter. Sigue las instrucciones para configurar el SDK.", - "get-source-code-title": "Obtener código fuente de la aplicación", - "get-source-code-text": "Puedes obtener el código fuente de la aplicación móvil ThingsBoard Flutter clonándolo desde el repositorio de GitHub:", - "configure-api-title": "Configurar el endpoint de la API de ThingsBoard", - "configure-api-text": "Abre el proyecto flutter_thingsboard_pe_app en tu editor/IDE. Edita:", - "configure-api-hint": "Establece el valor de la constante thingsBoardApiEndpoint para que coincida con el endpoint API de tu instancia de servidor ThingsBoard. No uses los nombres de host “localhost” o “127.0.0.1”.", + "prepare-environment-title": "Preparar el entorno de desarrollo", + "prepare-environment-text": "La aplicación móvil ThingsBoard basada en Flutter requiere Flutter SDK. Sigue las instrucciones para configurar el SDK de Flutter.", + "get-source-code-title": "Obtener el código fuente de la app", + "get-source-code-text": "Puedes obtener el código fuente de la aplicación móvil ThingsBoard basada en Flutter clonándolo desde el repositorio de GitHub:", + "configure-app-settings-title": "Configurar los ajustes de la app", + "configure-app-settings-text": "Descarga el archivo de configuración y colócalo en el directorio raíz del proyecto que clonaste en el paso anterior.", + "download-file": "Descargar archivo", "run-app-title": "Ejecutar la aplicación", - "run-app-text": "Ejecuta la aplicación según lo descrito en tu IDE.\nSi usas la terminal, ejecuta la aplicación con el siguiente comando:", - "more-information": "Información detallada se encuentra en nuestra documentación de introducción.", - "getting-started": "Guía de inicio", - "configure-package-title": "Configurar paquete de la aplicación", - "configure-package-text": "Puedes cambiar manualmente el paquete de la aplicación o usar una herramienta CLI de terceros.", - "configure-package-text-install": "Para instalar la herramienta Rename CLI, ejecuta el siguiente comando:", - "configure-package-run-commands": "Ejecuta estos comandos en el directorio raíz de tu proyecto:" + "run-app-text": "Ejecuta la aplicación como se indica en tu IDE.\nSi usas la terminal, ejecuta la aplicación con el siguiente comando:", + "more-information": "Puedes encontrar información detallada en nuestra documentación de introducción.", + "getting-started": "Introducción" } }, "notification": { @@ -3839,6 +3953,7 @@ "new-platform-version-trigger-settings": "Configuración del disparador de nueva versión de plataforma", "rate-limits-trigger-settings": "Configuración del disparador por límites de tasa excedidos", "task-processing-failure-trigger-settings": "Configuración del disparador por error en procesamiento de tareas", + "resources-shortage-trigger-settings": "Configuración del disparador por escasez de recursos", "at-least-one-should-be-selected": "Debe seleccionarse al menos uno", "basic-settings": "Configuraciones básicas", "button-text": "Texto del botón", @@ -3853,6 +3968,7 @@ "create-new": "Crear nuevo", "created": "Creado", "customize-messages": "Personalizar mensajes", + "cpu-threshold": "Umbral de CPU", "delete-notification-text": "Ten cuidado, después de la confirmación, la notificación será irrecuperable.", "delete-notification-title": "¿Estás seguro de que deseas eliminar la notificación?", "delete-notifications-text": "Ten cuidado, después de la confirmación, las notificaciones serán irrecuperables.", @@ -3919,6 +4035,7 @@ "input-fields-support-templatization": "Los campos de entrada admiten plantillas.", "link": "Enlace", "link-required": "El enlace es obligatorio", + "link-max-length": "El enlace debe tener {{ length }} caracteres o menos", "link-type": { "dashboard": "Abrir tablero", "link": "Abrir enlace URL" @@ -3945,6 +4062,7 @@ "no-severity-found": "No se encontró severidad", "no-severity-matching": "'{{severity}}' no encontrado.", "no-template-matching": "No se encontraron recursos que coincidan con '{{template}}'.", + "create-new-template": "¡Crear una nueva!", "not-found-slack-recipient": "Destinatario de Slack no encontrado", "notification": "Notificación", "notification-center": "Centro de notificaciones", @@ -3968,17 +4086,18 @@ "only-rule-chain-lifecycle-failures": "Solo fallos en el ciclo de vida de la cadena de reglas", "only-rule-node-lifecycle-failures": "Solo fallos en el ciclo de vida del nodo de regla", "platform-users": "Usuarios de la plataforma", + "ram-threshold": "Umbral de RAM", "rate-limits": "Límites de tasa", "rate-limits-hint": "Si el campo está vacío, el disparador se aplicará a todos los límites de tasa", "recipient": "Destinatario", "recipient-group": "Grupo de destinatarios", "recipient-type": { - "affected-tenant-administrators": "Administradores del inquilino afectados", + "affected-tenant-administrators": "Administradores del tenant afectados", "affected-user": "Usuario afectado", "all-users": "Todos los usuarios", - "customer-users": "Usuarios del cliente", + "customer-users": "Usuarios del customer", "system-administrators": "Administradores del sistema", - "tenant-administrators": "Administradores del inquilino", + "tenant-administrators": "Administradores del tenant", "user-filters": "Filtro de usuarios", "user-list": "Lista de usuarios", "users-entity-owner": "Usuarios del propietario de la entidad" @@ -4033,6 +4152,7 @@ "start-from-scratch": "Empezar desde cero", "status": "Estado", "stop-escalation-alarm-status-become": "Detener la escalada al cambiar el estado de la alarma a:", + "storage-threshold": "Umbral de almacenamiento", "subject": "Asunto", "subject-required": "El asunto es obligatorio", "subject-max-length": "El asunto debe tener menos o igual a {{ length }} caracteres", @@ -4054,12 +4174,13 @@ "rate-limits": "Límites de tasa excedidos", "edge-communication-failure": "Fallo de comunicación con el edge", "edge-connection": "Conexión con el edge", - "task-processing-failure": "Fallo de procesamiento de tarea" + "task-processing-failure": "Fallo de procesamiento de tarea", + "resources-shortage": "Escasez de recursos" }, "templates": "Plantillas", "notification-templates": "Notificaciones / Plantillas", - "tenant-profiles-list-rule-hint": "Si el campo está vacío, el disparador se aplicará a todos los perfiles de inquilino", - "tenants-list-rule-hint": "Si el campo está vacío, el disparador se aplicará a todos los inquilinos", + "tenant-profiles-list-rule-hint": "Si el campo está vacío, el disparador se aplicará a todos los perfiles de tenant", + "tenants-list-rule-hint": "Si el campo está vacío, el disparador se aplicará a todos los tenants", "threshold": "Umbral", "theme-color": "Color del tema", "time": "Hora", @@ -4068,18 +4189,19 @@ "alarm": "Alarma", "alarm-assignment": "Asignación de alarma", "alarm-comment": "Comentario de alarma", - "api-usage-limit": "Límite de uso de API", + "api-usage-limit": "Límite de uso de la API", "device-activity": "Actividad del dispositivo", "entities-limit": "Límite de entidades", - "entity-action": "Acción sobre entidad", + "entity-action": "Acción de entidad", "rule-engine-lifecycle-event": "Evento del ciclo de vida del motor de reglas", "new-platform-version": "Nueva versión de la plataforma", - "rate-limits": "Límites de tasa excedidos", - "edge-connection": "Conexión con el edge", - "edge-communication-failure": "Fallo de comunicación con el edge", - "task-processing-failure": "Fallo de procesamiento de tarea", + "rate-limits": "Límites de tasa superados", + "edge-connection": "Conexión de edge", + "edge-communication-failure": "Fallo de comunicación de edge", + "task-processing-failure": "Fallo en el procesamiento de tarea", + "resources-shortage": "Escasez de recursos", "trigger": "Disparador", - "trigger-required": "El disparador es obligatorio" + "trigger-required": "Se requiere un disparador" }, "type": "Tipo", "unread": "No leído", @@ -4119,6 +4241,7 @@ "checksum-copied-message": "El checksum del paquete se ha copiado al portapapeles", "change-firmware": "El cambio de firmware puede provocar la actualización de { count, plural, =1 {1 dispositivo} other {# dispositivos} }.", "change-software": "El cambio de software puede provocar la actualización de { count, plural, =1 {1 dispositivo} other {# dispositivos} }.", + "change-ota-setting-title": "¿Estás seguro de que deseas cambiar la configuración de OTA?", "chose-compatible-device-profile": "El paquete cargado solo estará disponible para dispositivos con el perfil seleccionado.", "chose-firmware-distributed-device": "Seleccione el firmware que se distribuirá a los dispositivos", "chose-software-distributed-device": "Seleccione el software que se distribuirá a los dispositivos", @@ -4314,6 +4437,7 @@ "add-relation-filter": "Agregar filtro de relación", "any-relation": "Cualquier relación", "relation-filters": "Filtros de relación", + "relation-filter": "Filtro de relación", "additional-info": "Información adicional (JSON)", "invalid-additional-info": "No se pudo analizar el JSON de información adicional.", "no-relations-text": "No se encontraron relaciones", @@ -4549,7 +4673,7 @@ "device-name-pattern": "Nombre del dispositivo", "asset-name-pattern": "Nombre del activo", "entity-view-name-pattern": "Nombre de vista de entidad", - "customer-title-pattern": "Título del cliente", + "customer-title-pattern": "Título del customer", "dashboard-name-pattern": "Título del tablero", "user-name-pattern": "Correo del usuario", "edge-name-pattern": "Nombre del Edge", @@ -4566,15 +4690,15 @@ "entity-cache-expiration-hint": "Especifica el intervalo de tiempo máximo permitido para almacenar registros de entidades encontrados. El valor 0 significa que los registros nunca expirarán.", "entity-cache-expiration-required": "El tiempo de expiración de la caché de entidades es obligatorio.", "entity-cache-expiration-range": "El tiempo de expiración debe ser mayor o igual a 0.", - "customer-name-pattern": "Título del cliente", - "customer-name-pattern-required": "El título del cliente es obligatorio", + "customer-name-pattern": "Título del customer", + "customer-name-pattern-required": "El título del customer es obligatorio", "customer-name-pattern-hint": "Usa $[messageKey] para extraer el valor del mensaje y ${metadataKey} para extraerlo de los metadatos.", - "create-customer-if-not-exists": "Crear nuevo cliente si no existe", - "unassign-from-customer": "Desasignar de cliente específico si el originador es un tablero", - "unassign-from-customer-tooltip": "Solo los tableros pueden asignarse a múltiples clientes al mismo tiempo.\nSi el originador del mensaje es un tablero, debes especificar explícitamente el título del cliente del que se desea desasignar.", - "customer-cache-expiration": "Tiempo de expiración de la caché de clientes (seg)", - "customer-cache-expiration-hint": "Especifica el intervalo de tiempo máximo permitido para almacenar registros de clientes encontrados. El valor 0 significa que los registros nunca expirarán.", - "customer-cache-expiration-required": "El tiempo de expiración de la caché de clientes es obligatorio.", + "create-customer-if-not-exists": "Crear nuevo customer si no existe", + "unassign-from-customer": "Desasignar de customer específico si el originador es un tablero", + "unassign-from-customer-tooltip": "Solo los tableros pueden asignarse a múltiples customers al mismo tiempo.\nSi el originador del mensaje es un tablero, debes especificar explícitamente el título del customer del que se desea desasignar.", + "customer-cache-expiration": "Tiempo de expiración de la caché de customers (seg)", + "customer-cache-expiration-hint": "Especifica el intervalo de tiempo máximo permitido para almacenar registros de customers encontrados. El valor 0 significa que los registros nunca expirarán.", + "customer-cache-expiration-required": "El tiempo de expiración de la caché de customers es obligatorio.", "customer-cache-expiration-range": "El tiempo de expiración debe ser mayor o igual a 0.", "interval-start": "Inicio del intervalo", "interval-end": "Fin del intervalo", @@ -4713,7 +4837,7 @@ "source-field-required": "El campo fuente es obligatorio.", "originator-source": "Fuente del originador", "new-originator": "Nuevo originador", - "originator-customer": "Cliente", + "originator-customer": "Customer", "originator-tenant": "Tenant", "originator-related": "Entidad relacionada", "originator-alarm-originator": "Originador de la alarma", @@ -4769,7 +4893,7 @@ "alarm-status-list-empty": "La lista de estados de alarma está vacía", "no-alarm-status-matching": "No se encontró ningún estado de alarma coincidente.", "propagate": "Propagar alarma a entidades relacionadas", - "propagate-to-owner": "Propagar alarma al propietario de la entidad (Cliente o Tenant)", + "propagate-to-owner": "Propagar alarma al propietario de la entidad (Customer o Tenant)", "propagate-to-tenant": "Propagar alarma al Tenant", "condition": "Condición", "details": "Detalles", @@ -4902,8 +5026,8 @@ "credentials-anonymous": "Anónimo", "credentials-basic": "Básico", "credentials-pem": "PEM", - "credentials-pem-hint": "Se requiere al menos el archivo del certificado CA del servidor o un par de archivos de certificado de cliente y clave privada del cliente", - "credentials-sas": "Firma de acceso compartido (SAS)", + "credentials-pem-hint": "Se requiere al menos el archivo del certificado CA del servidor o un par de archivos del certificado del cliente y la clave privada del cliente.", + "credentials-sas": "Firma de acceso compartido (Shared Access Signature)", "sas-key": "Clave SAS", "sas-key-required": "La clave SAS es obligatoria.", "hostname": "Nombre del host", @@ -5119,11 +5243,11 @@ "type-field-input": "Tipo", "type-field-input-required": "El tipo es obligatorio.", "key-field-input": "Clave", - "key-field-input-required": "La clave es obligatoria.", "add-entity-type": "Agregar tipo de entidad", "add-device-profile": "Agregar perfil de dispositivo", - "number-floating-point-field-input": "Dígitos después del punto decimal", - "number-floating-point-field-input-hint": "Usa 0 para convertir el resultado en entero", + "key-field-input-required": "Se requiere una clave.", + "number-floating-point-field-input": "Número de dígitos después del punto decimal", + "number-floating-point-field-input-hint": "Usa 0 para convertir el resultado a entero", "add-to-message-field-input": "Agregar al mensaje", "add-to-metadata-field-input": "Agregar a metadatos", "custom-expression-field-input": "Expresión matemática", @@ -5171,15 +5295,15 @@ "add-mapped-originator-fields-to": "Agregar campos mapeados del originador a", "fields": "Campos", "skip-empty-fields": "Omitir campos vacíos", - "skip-empty-fields-tooltip": "Los campos con valores vacíos no se agregarán al mensaje/metadatos de salida.", + "skip-empty-fields-tooltip": "Los campos con valores vacíos no se agregarán al mensaje de salida ni a los metadatos de salida.", "fetch-interval": "Intervalo de obtención", "fetch-strategy": "Estrategia de obtención", "fetch-timeseries-from-to": "Obtener serie temporal desde hace {{startInterval}} {{startIntervalTimeUnit}} hasta hace {{endInterval}} {{endIntervalTimeUnit}}.", - "fetch-timeseries-from-to-invalid": "Obtención de serie temporal no válida (\"Inicio del intervalo\" debe ser menor que \"Fin del intervalo\").", - "use-metadata-dynamic-interval-tooltip": "Si está seleccionado, el nodo de reglas usará intervalos dinámicos de inicio y fin basados en patrones de mensaje y metadatos.", - "all-mode-hint": "Si se selecciona el modo de obtención 'Todo', el nodo de reglas recuperará telemetría desde el intervalo de obtención con parámetros configurables.", - "first-mode-hint": "Si se selecciona el modo de obtención 'Primero', el nodo recuperará la telemetría más cercana al inicio del intervalo.", - "last-mode-hint": "Si se selecciona el modo de obtención 'Último', el nodo recuperará la telemetría más cercana al final del intervalo.", + "fetch-timeseries-from-to-invalid": "Intervalo de serie temporal no válido (\"Inicio del intervalo\" debe ser menor que \"Fin del intervalo\").", + "use-metadata-dynamic-interval-tooltip": "Si está seleccionado, el nodo de regla usará un intervalo dinámico de inicio y fin basado en los patrones del mensaje y los metadatos.", + "all-mode-hint": "Si se selecciona el modo de obtención \"Todos\", el nodo de regla recuperará la telemetría del intervalo con parámetros de consulta configurables.", + "first-mode-hint": "Si se selecciona el modo de obtención \"Primero\", el nodo de regla recuperará la telemetría más cercana al inicio del intervalo.", + "last-mode-hint": "Si se selecciona el modo de obtención \"Último\", el nodo de regla recuperará la telemetría más cercana al final del intervalo.", "ascending": "Ascendente", "descending": "Descendente", "min": "Mínimo", @@ -5191,13 +5315,13 @@ "last-level-relation-tooltip": "Si se selecciona, el nodo buscará entidades relacionadas solo en el nivel definido en el máximo nivel de relación.", "last-level-device-relation-tooltip": "Si se selecciona, el nodo buscará dispositivos relacionados solo en el nivel definido en el máximo nivel de relación.", "data-to-fetch": "Datos a obtener", - "mapping-of-customers": "Mapeo de clientes", + "mapping-of-customers": "Mapeo de customers", "map-fields-required": "Todos los campos de mapeo son obligatorios.", "attributes": "Atributos", "related-device-attributes": "Atributos de dispositivos relacionados", "add-selected-attributes-to": "Agregar atributos seleccionados a", "device-profiles": "Perfiles de dispositivo", - "mapping-of-tenant": "Mapeo de inquilino", + "mapping-of-tenant": "Mapeo de tenant", "add-attribute-key": "Agregar clave de atributo", "message-template": "Plantilla de mensaje", "message-template-required": "La plantilla de mensaje es obligatoria", @@ -5209,8 +5333,8 @@ "recipients": "Destinatarios", "message-subject-and-content": "Asunto y contenido del mensaje", "template-rules-hint": "Ambos campos de entrada admiten tematización. Usa $[messageKey] para extraer el valor del mensaje y ${metadataKey} para extraer el valor de los metadatos.", - "originator-customer-desc": "Usar el cliente del originador del mensaje entrante como nuevo originador.", - "originator-tenant-desc": "Usar el inquilino actual como nuevo originador.", + "originator-customer-desc": "Usar el customer del originador del mensaje entrante como nuevo originador.", + "originator-tenant-desc": "Usar el tenant actual como nuevo originador.", "originator-related-entity-desc": "Usar entidad relacionada como nuevo originador. La búsqueda se basa en el tipo y dirección de relación configurados.", "originator-alarm-originator-desc": "Usar el originador de la alarma como nuevo originador. Solo si el originador del mensaje entrante es una entidad de alarma.", "originator-entity-by-name-pattern-desc": "Usar entidad obtenida desde la base de datos como nuevo originador. La búsqueda se basa en el tipo de entidad y el patrón de nombre especificado.", @@ -5304,6 +5428,36 @@ "html-text-description": "Permite el uso de etiquetas HTML para formato, enlaces e imágenes en el cuerpo del correo.", "dynamic-text-description": "Permite usar texto plano o HTML dinámicamente según la función de tematización.", "after-template-evaluation-hint": "Después de la evaluación de la plantilla, el valor debe ser true para HTML y false para texto plano." + }, + "ai": { + "ai-model": "Modelo de IA", + "model": "Modelo", + "ai-model-hint": "Selecciona el modelo de IA preconfigurado para procesar las solicitudes enviadas por este nodo de regla, o usa \"Crear nuevo\" para configurar uno nuevo.", + "prompt-settings": "Configuración del prompt", + "prompt-settings-hint": "El prompt del sistema (opcional) define el rol general y las restricciones de la IA, mientras que el prompt del usuario define la tarea específica a realizar. Ambos campos admiten plantillas.", + "system-prompt": "Prompt del sistema", + "system-prompt-max-length": "El prompt del sistema debe tener 500.000 caracteres o menos.", + "system-prompt-blank": "El prompt del sistema no debe estar vacío.", + "user-prompt": "Prompt del usuario", + "user-prompt-required": "Se requiere el prompt del usuario.", + "user-prompt-max-length": "El prompt del usuario debe tener 500.000 caracteres o menos.", + "user-prompt-blank": "El prompt del usuario no debe estar vacío.", + "response-format": "Formato de respuesta", + "response-text": "Texto", + "response-json": "JSON", + "response-json-schema": "Esquema JSON", + "response-format-hint-TEXT": "Permite al modelo generar texto arbitrario, que puede o no ser un objeto JSON válido. Si la salida no es un JSON válido, se envolverá automáticamente en un objeto JSON bajo la clave \"response\".", + "response-format-hint-JSON": "Se requiere que el modelo genere una respuesta que sea un JSON válido. Si la salida no es un JSON válido, se envolverá automáticamente en un objeto JSON bajo la clave \"response\".", + "response-format-hint-JSON_SCHEMA": "Se requiere que el modelo genere un JSON que cumpla con la estructura y tipos de datos definidos en el esquema proporcionado. Si la salida no es un JSON válido, se envolverá automáticamente en un objeto JSON bajo la clave \"response\".", + "response-json-schema-hint": "Aunque se puede ingresar cualquier esquema JSON válido, este nodo de regla solo admite un subconjunto limitado de sus características. Consulta la documentación del nodo para más detalles.", + "response-json-schema-required": "Se requiere un esquema JSON", + "advanced-settings": "Configuración avanzada", + "timeout": "Tiempo de espera", + "timeout-hint": "Tiempo máximo de espera \npara recibir una respuesta del modelo de IA antes de finalizar la solicitud.", + "timeout-required": "Se requiere un tiempo de espera", + "timeout-validation": "Debe ser de entre 1 segundo y 10 minutos.", + "force-acknowledgement": "Forzar acuse de recibo", + "force-acknowledgement-hint": "Si está habilitado, el mensaje entrante se reconoce inmediatamente. La respuesta del modelo se coloca en cola como un mensaje nuevo y separado." } }, "timezone": { @@ -5384,8 +5538,8 @@ "strategies": { "sequential-by-originator-label": "Secuencial por originador", "sequential-by-originator-hint": "No se envía un nuevo mensaje para p.ej. dispositivo A hasta que se confirme el mensaje anterior para ese dispositivo", - "sequential-by-tenant-label": "Secuencial por inquilino", - "sequential-by-tenant-hint": "No se envía un nuevo mensaje para p.ej. inquilino A hasta que se confirme el mensaje anterior para ese inquilino", + "sequential-by-tenant-label": "Secuencial por tenant", + "sequential-by-tenant-hint": "No se envía un nuevo mensaje para p.ej. tenant A hasta que se confirme el mensaje anterior para ese tenant", "sequential-label": "Secuencial", "sequential-hint": "No se envía un nuevo mensaje hasta que el anterior sea confirmado", "burst-label": "Explosión (burst)", @@ -5419,7 +5573,7 @@ "general": "Error general del servidor", "authentication": "Error de autenticación", "jwt-token-expired": "Token JWT expirado", - "tenant-trial-expired": "Prueba del inquilino expirada", + "tenant-trial-expired": "Prueba del tenant expirada", "credentials-expired": "Credenciales expiradas", "permission-denied": "Permiso denegado", "invalid-arguments": "Argumentos inválidos", @@ -5429,75 +5583,75 @@ "too-many-updates": "Demasiadas actualizaciones" }, "tenant": { - "tenant": "Inquilino", - "tenants": "Inquilinos", - "management": "Gestión de inquilinos", - "add": "Agregar inquilino", + "tenant": "Tenant", + "tenants": "Tenants", + "management": "Gestión de tenant", + "add": "Agregar tenant", "admins": "Administradores", - "manage-tenant-admins": "Gestionar administradores del inquilino", - "delete": "Eliminar inquilino", - "add-tenant-text": "Agregar nuevo inquilino", - "no-tenants-text": "No se encontraron inquilinos", - "tenant-details": "Detalles del inquilino", + "manage-tenant-admins": "Gestionar administradores del tenant", + "delete": "Eliminar tenant", + "add-tenant-text": "Agregar nuevo tenant", + "no-tenants-text": "No se encontraron tenants", + "tenant-details": "Detalles del tenant", "title-max-length": "El título debe tener menos de 256 caracteres", - "delete-tenant-title": "¿Estás seguro de que deseas eliminar el inquilino '{{tenantTitle}}'?", - "delete-tenant-text": "Ten cuidado, después de la confirmación el inquilino y todos los datos relacionados serán irrecuperables.", - "delete-tenants-title": "¿Estás seguro de que deseas eliminar { count, plural, =1 {1 inquilino} other {# inquilinos} }?", - "delete-tenants-action-title": "Eliminar { count, plural, =1 {1 inquilino} other {# inquilinos} }", - "delete-tenants-text": "Ten cuidado, después de la confirmación todos los inquilinos seleccionados serán eliminados y todos los datos relacionados serán irrecuperables.", + "delete-tenant-title": "¿Estás seguro de que deseas eliminar el tenant '{{tenantTitle}}'?", + "delete-tenant-text": "Ten cuidado, después de la confirmación el tenant y todos los datos relacionados serán irrecuperables.", + "delete-tenants-title": "¿Estás seguro de que deseas eliminar { count, plural, =1 {1 tenant} other {# tenants} }?", + "delete-tenants-action-title": "Eliminar { count, plural, =1 {1 tenant} other {# tenants} }", + "delete-tenants-text": "Ten cuidado, después de la confirmación todos los tenants seleccionados serán eliminados y todos los datos relacionados serán irrecuperables.", "title": "Título", "title-required": "El título es obligatorio.", "description": "Descripción", "details": "Detalles", "events": "Eventos", - "copyId": "Copiar ID del inquilino", - "idCopiedMessage": "ID del inquilino copiado al portapapeles", - "select-tenant": "Seleccionar inquilino", - "no-tenants-matching": "No se encontraron inquilinos que coincidan con '{{entity}}'.", - "tenant-required": "El inquilino es obligatorio", - "search": "Buscar inquilinos", - "selected-tenants": "{ count, plural, =1 {1 inquilino} other {# inquilinos} } seleccionado(s)", + "copyId": "Copiar ID del tenant", + "idCopiedMessage": "ID del tenant copiado al portapapeles", + "select-tenant": "Seleccionar tenant", + "no-tenants-matching": "No se encontraron tenants que coincidan con '{{entity}}'.", + "tenant-required": "El tenant es obligatorio", + "search": "Buscar tenants", + "selected-tenants": "{ count, plural, =1 {1 tenant} other {# tenants} } seleccionado(s)", "isolated-tb-rule-engine": "Usar colas aisladas del motor de reglas de ThingsBoard", - "isolated-tb-rule-engine-details": "Cada inquilino tendrá colas del motor de reglas dedicadas" + "isolated-tb-rule-engine-details": "Cada tenant tendrá colas del motor de reglas dedicadas" }, "tenant-profile": { - "tenant-profile": "Perfil del inquilino", - "tenant-profiles": "Perfiles del inquilino", - "add": "Agregar perfil de inquilino", + "tenant-profile": "Perfil del tenant", + "tenant-profiles": "Perfiles del tenant", + "add": "Agregar perfil de tenant", "add-profile": "Agregar perfil", "debug": "Depurar", - "edit": "Editar perfil de inquilino", - "tenant-profile-details": "Detalles del perfil del inquilino", - "no-tenant-profiles-text": "No se encontraron perfiles de inquilino", + "edit": "Editar perfil de tenant", + "tenant-profile-details": "Detalles del perfil del tenant", + "no-tenant-profiles-text": "No se encontraron perfiles de tenant", "name-max-length": "El nombre debe tener menos de 256 caracteres", - "search": "Buscar perfiles de inquilino", - "selected-tenant-profiles": "{ count, plural, =1 {1 perfil de inquilino} other {# perfiles de inquilino} } seleccionado(s)", - "no-tenant-profiles-matching": "No se encontró ningún perfil de inquilino que coincida con '{{entity}}'.", - "tenant-profile-required": "El perfil de inquilino es obligatorio", - "idCopiedMessage": "El ID del perfil de inquilino ha sido copiado al portapapeles", - "set-default": "Hacer perfil de inquilino predeterminado", - "delete": "Eliminar perfil de inquilino", - "copyId": "Copiar ID del perfil de inquilino", + "search": "Buscar perfiles de tenant", + "selected-tenant-profiles": "{ count, plural, =1 {1 perfil de tenant} other {# perfiles de tenant} } seleccionado(s)", + "no-tenant-profiles-matching": "No se encontró ningún perfil de tenant que coincida con '{{entity}}'.", + "tenant-profile-required": "El perfil de tenant es obligatorio", + "idCopiedMessage": "El ID del perfil de tenant ha sido copiado al portapapeles", + "set-default": "Hacer perfil de tenant predeterminado", + "delete": "Eliminar perfil de tenant", + "copyId": "Copiar ID del perfil de tenant", "name": "Nombre", "name-required": "El nombre es obligatorio.", "data": "Datos del perfil", "profile-configuration": "Configuración del perfil", "description": "Descripción", "default": "Predeterminado", - "delete-tenant-profile-title": "¿Estás seguro de que deseas eliminar el perfil de inquilino '{{tenantProfileName}}'?", - "delete-tenant-profile-text": "Ten cuidado, después de la confirmación el perfil de inquilino y todos los datos relacionados serán irrecuperables.", - "delete-tenant-profiles-title": "¿Estás seguro de que deseas eliminar { count, plural, =1 {1 perfil de inquilino} other {# perfiles de inquilino} }?", - "delete-tenant-profiles-text": "Ten cuidado, después de la confirmación todos los perfiles de inquilino seleccionados serán eliminados y todos los datos relacionados serán irrecuperables.", - "set-default-tenant-profile-title": "¿Estás seguro de que deseas hacer predeterminado el perfil de inquilino '{{tenantProfileName}}'?", - "set-default-tenant-profile-text": "Después de la confirmación, el perfil de inquilino se marcará como predeterminado y se usará para nuevos inquilinos sin un perfil especificado.", - "no-tenant-profiles-found": "No se encontraron perfiles de inquilino.", + "delete-tenant-profile-title": "¿Estás seguro de que deseas eliminar el perfil de tenant '{{tenantProfileName}}'?", + "delete-tenant-profile-text": "Ten cuidado, después de la confirmación el perfil de tenant y todos los datos relacionados serán irrecuperables.", + "delete-tenant-profiles-title": "¿Estás seguro de que deseas eliminar { count, plural, =1 {1 perfil de tenant} other {# perfiles de tenant} }?", + "delete-tenant-profiles-text": "Ten cuidado, después de la confirmación todos los perfiles de tenant seleccionados serán eliminados y todos los datos relacionados serán irrecuperables.", + "set-default-tenant-profile-title": "¿Estás seguro de que deseas hacer predeterminado el perfil de tenant '{{tenantProfileName}}'?", + "set-default-tenant-profile-text": "Después de la confirmación, el perfil de tenant se marcará como predeterminado y se usará para nuevos tenants sin un perfil especificado.", + "no-tenant-profiles-found": "No se encontraron perfiles de tenant.", "create-new-tenant-profile": "¡Crear uno nuevo!", - "create-tenant-profile": "Crear nuevo perfil de inquilino", - "import": "Importar perfil de inquilino", - "export": "Exportar perfil de inquilino", - "export-failed-error": "No se pudo exportar el perfil de inquilino: {{error}}", - "tenant-profile-file": "Archivo del perfil de inquilino", - "invalid-tenant-profile-file-error": "No se pudo importar el perfil de inquilino: estructura de datos inválida.", + "create-tenant-profile": "Crear nuevo perfil de tenant", + "import": "Importar perfil de tenant", + "export": "Exportar perfil de tenant", + "export-failed-error": "No se pudo exportar el perfil de tenant: {{error}}", + "tenant-profile-file": "Archivo del perfil de tenant", + "invalid-tenant-profile-file-error": "No se pudo importar el perfil de tenant: estructura de datos inválida.", "advanced-settings": "Configuraciones avanzadas", "entities": "Entidades", "rule-engine": "Motor de reglas", @@ -5513,9 +5667,9 @@ "maximum-assets": "Máximo número de activos", "maximum-assets-required": "Se requiere el número máximo de activos.", "maximum-assets-range": "El número máximo de activos no puede ser negativo", - "maximum-customers": "Máximo número de clientes", - "maximum-customers-required": "Se requiere el número máximo de clientes.", - "maximum-customers-range": "El número máximo de clientes no puede ser negativo", + "maximum-customers": "Máximo número de customers", + "maximum-customers-required": "Se requiere el número máximo de customers.", + "maximum-customers-range": "El número máximo de customers no puede ser negativo", "maximum-users": "Máximo número de usuarios", "maximum-users-required": "Se requiere el número máximo de usuarios.", "maximum-users-range": "El número máximo de usuarios no puede ser negativo", @@ -5539,9 +5693,9 @@ "maximum-ota-package-sum-data-size-range": "El tamaño total máximo de archivos OTA no puede ser negativo", "maximum-debug-duration-min": "Duración máxima de depuración (min)", "maximum-debug-duration-min-range": "La duración máxima de depuración no puede ser negativa", - "rest-requests-for-tenant": "Solicitudes REST para el inquilino", - "transport-tenant-telemetry-msg-rate-limit": "Mensajes de telemetría del inquilino por transporte", - "transport-tenant-telemetry-data-points-rate-limit": "Puntos de datos de telemetría del inquilino por transporte", + "rest-requests-for-tenant": "Solicitudes REST para el tenant", + "transport-tenant-telemetry-msg-rate-limit": "Mensajes de telemetría del tenant por transporte", + "transport-tenant-telemetry-data-points-rate-limit": "Puntos de datos de telemetría del tenant por transporte", "transport-device-msg-rate-limit": "Mensajes del dispositivo por transporte", "transport-device-telemetry-msg-rate-limit": "Límite de mensajes de telemetría del dispositivo por transporte", "transport-device-telemetry-data-points-rate-limit": "Límite de puntos de datos de telemetría del dispositivo por transporte", @@ -5619,32 +5773,36 @@ "no-queue": "Ninguna cola configurada", "add-queue": "Agregar cola", "queues-with-count": "Colas ({{count}})", - "tenant-rest-limits": "Solicitudes REST para el inquilino", - "customer-rest-limits": "Solicitudes REST para el cliente", + "tenant-rest-limits": "Solicitudes REST para el tenant", + "customer-rest-limits": "Solicitudes REST para el customer", "incorrect-pattern-for-rate-limits": "El formato es una lista separada por comas de pares de capacidad y período (en segundos) con dos puntos entre ellos, por ejemplo: 100:1,2000:60", "too-small-value-zero": "El valor debe ser mayor que 0", "too-small-value-one": "El valor debe ser mayor que 1", "queue-size-is-limited-by-system-configuration": "El tamaño de la cola también está limitado por la configuración del sistema.", - "cassandra-tenant-limits-configuration": "Consulta Cassandra para el inquilino", - "ws-limit-max-sessions-per-tenant": "Número máximo de sesiones por inquilino", - "ws-limit-max-sessions-per-customer": "Número máximo de sesiones por cliente", + "cassandra-write-tenant-core-limits-configuration": "Consultas de escritura Cassandra vía REST API", + "cassandra-read-tenant-core-limits-configuration": "Consultas de lectura Cassandra vía REST API y telemetría WS", + "cassandra-write-tenant-rule-engine-limits-configuration": "Consultas de escritura Cassandra para telemetría del Rule Engine", + "cassandra-read-tenant-rule-engine-limits-configuration": "Consultas de lectura Cassandra para telemetría del Rule Engine", + "ws-limit-max-sessions-per-tenant": "Número máximo de sesiones por tenant", + "ws-limit-max-sessions-per-customer": "Número máximo de sesiones por customer", "ws-limit-max-sessions-per-regular-user": "Número máximo de sesiones por usuario regular", "ws-limit-max-sessions-per-public-user": "Número máximo de sesiones por usuario público", "ws-limit-queue-per-session": "Tamaño máximo de la cola de mensajes por sesión", - "ws-limit-max-subscriptions-per-tenant": "Número máximo de suscripciones por inquilino", - "ws-limit-max-subscriptions-per-customer": "Número máximo de suscripciones por cliente", + "ws-limit-max-subscriptions-per-tenant": "Número máximo de suscripciones por tenant", + "ws-limit-max-subscriptions-per-customer": "Número máximo de suscripciones por customer", "ws-limit-max-subscriptions-per-regular-user": "Número máximo de suscripciones por usuario regular", "ws-limit-max-subscriptions-per-public-user": "Número máximo de suscripciones por usuario público", "ws-limit-updates-per-session": "Actualizaciones WS por sesión", "rate-limits": { "add-limit": "Agregar límite", - "advanced-settings": "Configuraciones avanzadas", + "and-also-less-than": "y también menor que", + "advanced-settings": "Configuración avanzada", "edit-limit": "Editar límite", "calculated-field-debug-event-rate-limit": "Eventos de depuración de campo calculado", "edit-calculated-field-debug-event-rate-limit": "Editar límites de eventos de depuración de campo calculado", - "edit-transport-tenant-msg-title": "Editar límites de velocidad de mensajes de transporte del inquilino", - "edit-transport-tenant-telemetry-msg-title": "Editar límites de velocidad de mensajes de telemetría del inquilino", - "edit-transport-tenant-telemetry-data-points-title": "Editar límites de velocidad de puntos de datos de telemetría del inquilino", + "edit-transport-tenant-msg-title": "Editar límites de velocidad de mensajes de transporte del tenant", + "edit-transport-tenant-telemetry-msg-title": "Editar límites de velocidad de mensajes de telemetría del tenant", + "edit-transport-tenant-telemetry-data-points-title": "Editar límites de velocidad de puntos de datos de telemetría del tenant", "edit-transport-device-msg-title": "Editar límites de velocidad de mensajes de transporte del dispositivo", "edit-transport-device-telemetry-msg-title": "Editar límites de velocidad de mensajes de telemetría del dispositivo", "edit-transport-device-telemetry-data-points-title": "Editar límites de velocidad de puntos de datos de telemetría del dispositivo", @@ -5654,22 +5812,25 @@ "edit-transport-gateway-device-msg-title": "Editar límites de velocidad de mensajes del dispositivo del gateway", "edit-transport-gateway-device-telemetry-msg-title": "Editar límites de velocidad de mensajes de telemetría del dispositivo del gateway", "edit-transport-gateway-device-telemetry-data-points-title": "Editar límites de velocidad de puntos de datos de telemetría del dispositivo del gateway", - "edit-tenant-rest-limits-title": "Editar límites de solicitudes REST para el inquilino", - "edit-customer-rest-limits-title": "Editar límites de solicitudes REST para el cliente", - "edit-ws-limit-updates-per-session-title": "Editar límites de actualizaciones WS por sesión", - "edit-cassandra-tenant-limits-configuration-title": "Editar límites de consulta de Cassandra para el inquilino", - "edit-tenant-entity-export-rate-limit-title": "Editar límites de velocidad de creación de versión de entidad", - "edit-tenant-entity-import-rate-limit-title": "Editar límites de velocidad de carga de versión de entidad", - "edit-tenant-notification-request-rate-limit-title": "Editar límites de velocidad de solicitudes de notificación", - "edit-tenant-notification-requests-per-rule-rate-limit-title": "Editar límites de velocidad de solicitudes por regla de notificación", - "edit-edge-events-rate-limit": "Editar límites de velocidad de eventos del edge", - "edit-edge-events-per-edge-rate-limit": "Editar límites de eventos por edge", - "edge-events-rate-limit": "Eventos del edge", + "edit-tenant-rest-limits-title": "Editar límites de solicitudes REST para el tenant", + "edit-customer-rest-limits-title": "Editar límites de solicitudes REST para el customer", + "edit-ws-limit-updates-per-session-title": "Editar límites de tasa de actualizaciones WS por sesión", + "edit-cassandra-write-tenant-core-limits-configuration": "Editar consultas de escritura Cassandra vía REST API", + "edit-cassandra-read-tenant-core-limits-configuration": "Editar consultas de lectura Cassandra vía REST API y telemetría WS", + "edit-cassandra-write-tenant-rule-engine-limits-configuration": "Editar consultas de escritura Cassandra para telemetría del Rule Engine", + "edit-cassandra-read-tenant-rule-engine-limits-configuration": "Editar consultas de lectura Cassandra para telemetría del Rule Engine", + "edit-tenant-entity-export-rate-limit-title": "Editar límites de tasa para la creación de versiones de entidad", + "edit-tenant-entity-import-rate-limit-title": "Editar límites de tasa para la carga de versiones de entidad", + "edit-tenant-notification-request-rate-limit-title": "Editar límites de tasa para solicitudes de notificación", + "edit-tenant-notification-requests-per-rule-rate-limit-title": "Editar límites de tasa para solicitudes de notificación por regla de notificación", + "edit-edge-events-rate-limit": "Editar límites de tasa para eventos de edge", + "edit-edge-events-per-edge-rate-limit": "Editar límites de tasa para eventos por edge", + "edge-events-rate-limit": "Eventos de edge", "edge-events-per-edge-rate-limit": "Eventos por edge", - "edit-edge-uplink-messages-rate-limit": "Editar límites de velocidad de mensajes de subida del edge", - "edit-edge-uplink-messages-per-edge-rate-limit": "Editar límites de mensajes de subida por edge", - "edge-uplink-messages-rate-limit": "Mensajes de subida del edge", - "edge-uplink-messages-per-edge-rate-limit": "Mensajes de subida por edge", + "edit-edge-uplink-messages-rate-limit": "Editar límites de tasa para mensajes ascendentes de edge", + "edit-edge-uplink-messages-per-edge-rate-limit": "Editar límites de tasa para mensajes ascendentes por edge", + "edge-uplink-messages-rate-limit": "Mensajes ascendentes de edge", + "edge-uplink-messages-per-edge-rate-limit": "Mensajes ascendentes por edge", "messages-per": "mensajes por", "not-set": "No establecido", "number-of-messages": "Número de mensajes", @@ -5677,13 +5838,14 @@ "number-of-messages-min": "El valor mínimo es 1.", "preview": "Vista previa", "per-seconds": "Por segundos", - "per-seconds-required": "La tasa de tiempo es obligatoria.", + "per-seconds-required": "Se requiere tasa de tiempo.", "per-seconds-min": "El valor mínimo es 1.", - "rate-limits": "Límites de velocidad", + "per-seconds-duplicate": "Tasa de tiempo duplicada. Cada intervalo de tiempo debe ser único.", + "rate-limits": "Límites de tasa", "remove-limit": "Eliminar límite", - "transport-tenant-msg": "Mensajes de transporte del inquilino", - "transport-tenant-telemetry-msg": "Mensajes de telemetría del inquilino", - "transport-tenant-telemetry-data-points": "Puntos de datos de telemetría del inquilino", + "transport-tenant-msg": "Mensajes de transporte del tenant", + "transport-tenant-telemetry-msg": "Mensajes de telemetría del tenant", + "transport-tenant-telemetry-data-points": "Puntos de datos de telemetría del tenant", "transport-device-msg": "Mensajes de transporte del dispositivo", "transport-device-telemetry-msg": "Mensajes de telemetría del dispositivo", "transport-device-telemetry-data-points": "Puntos de datos de telemetría del dispositivo", @@ -5826,13 +5988,125 @@ "value": "Valor", "date": "Fecha", "show-date-time-interval": "Mostrar intervalo de fecha y hora", - "show-date-time-interval-hint": "Mostrar intervalo de fecha y hora de acuerdo con la agregación de datos.", + "show-date-time-interval-hint": "Mostrar intervalo de fecha y hora según la agregación de datos.", + "hide-zero-tooltip-values": "Ocultar valores cero", "background-color": "Color de fondo", "background-blur": "Desenfoque de fondo" }, "unit": { + "set-unit-conversion": "Establecer conversión de unidades", + "unit-settings": { + "unit-settings": "Configuración de unidades", + "source-unit": "Unidad de origen", + "source-unit-hint": "Esta es la unidad del valor almacenado. La unidad desde la cual estás convirtiendo. Ingresa el símbolo que usa tu dato de origen (ej. m, km, ft, in).", + "target-metric-unit": "Unidad métrica de destino", + "target-metric-unit-hint": "Elige a qué unidad métrica (SI) quieres convertir tu valor de origen (ej. cm, mm, km).", + "target-imperial-unit": "Unidad imperial de destino", + "target-imperial-unit-hint": "Elige a qué unidad imperial quieres convertir tu valor de origen (ej. in, ft, yd).", + "target-hybrid-unit": "Unidad híbrida de destino", + "target-hybrid-unit-hint": "Elige a qué unidad híbrida quieres convertir tu valor de origen (ej. cm, in, km). Las unidades híbridas combinan unidades métricas o imperiales.", + "enable-unit-conversion": "Habilitar conversión de unidades", + "enable-unit-conversion-hint": "Activa para habilitar la conversión. Cuando está desactivado, tu valor de origen pasará sin cambios. Se desactiva si hay solo una unidad en el grupo de medición correspondiente (ej. Flujo luminoso, AQI)." + }, + "unit-system": "Sistema de unidades", + "unit-system-type": { + "AUTO": "Automático", + "METRIC": "Métrico", + "IMPERIAL": "Imperial", + "HYBRID": "Híbrido" + }, + "measures": { + "absorbed-dose-rate": "Tasa de dosis absorbida", + "acceleration": "Aceleración", + "acidity": "Acidez", + "air-quality-index": "Índice de calidad del aire", + "amount-of-substance": "Cantidad de sustancia", + "angle": "Ángulo", + "angular-acceleration": "Aceleración angular", + "area": "Área", + "area-density": "Densidad superficial", + "capacitance": "Capacitancia", + "catalytic-activity": "Actividad catalítica", + "catalytic-concentration": "Concentración catalítica", + "charge": "Carga", + "current-density": "Densidad de corriente", + "data-transfer-rate": "Velocidad de transferencia de datos", + "density": "Densidad", + "digital": "Digital", + "dimension-ratio": "Relación dimensional", + "dynamic-viscosity": "Viscosidad dinámica", + "earthquake-magnitude": "Magnitud del terremoto", + "electric-charge-density": "Densidad de carga eléctrica", + "electric-current": "Corriente eléctrica", + "electric-dipole-moment": "Momento dipolar eléctrico", + "electric-field-strength": "Intensidad del campo eléctrico", + "electric-flux": "Flujo eléctrico", + "electric-permittivity": "Permitividad eléctrica", + "electric-polarizability": "Polarizabilidad eléctrica", + "electrical-conductance": "Conductancia eléctrica", + "electrical-conductivity": "Conductividad eléctrica", + "energy": "Energía", + "energy-density": "Densidad energética", + "force": "Fuerza", + "frequency": "Frecuencia", + "fuel-efficiency": "Eficiencia de combustible", + "heat-capacity": "Capacidad calorífica", + "illuminance": "Iluminancia", + "inductance": "Inductancia", + "kinematic-viscosity": "Viscosidad cinemática", + "length": "Longitud", + "light-exposure": "Exposición a la luz", + "linear-charge-density": "Densidad lineal de carga", + "logarithmic-ratio": "Relación logarítmica", + "luminous-efficacy": "Eficacia luminosa", + "luminous-flux": "Flujo luminoso", + "luminous-intensity": "Intensidad luminosa", + "magnetic-field-gradient": "Gradiente del campo magnético", + "magnetic-flux": "Flujo magnético", + "magnetic-flux-density": "Densidad de flujo magnético", + "magnetic-moment": "Momento magnético", + "magnetic-permeability": "Permeabilidad magnética", + "mass": "Masa", + "mass-fraction": "Fracción de masa", + "molar-concentration": "Concentración molar", + "molar-energy": "Energía molar", + "molar-heat-capacity": "Capacidad calorífica molar", + "molar-mass": "Masa molar", + "number-concentration": "Concentración numérica", + "parts-per-million": "Partes por millón", + "power": "Potencia", + "power-density": "Densidad de potencia", + "pressure": "Presión", + "radiance": "Radiancia", + "radiant-intensity": "Intensidad radiante", + "radiation-dose": "Dosis de radiación", + "radioactive-decay": "Desintegración radiactiva", + "radioactivity": "Radiactividad", + "radioactivity-concentration": "Concentración de radiactividad", + "reciprocal-length": "Longitud recíproca", + "resistance": "Resistencia", + "reynolds-number": "Número de Reynolds", + "signal-level": "Nivel de señal", + "solid-angle": "Ángulo sólido", + "specific-energy": "Energía específica", + "specific-heat-capacity": "Capacidad calorífica específica", + "specific-humidity": "Humedad específica", + "specific-volume": "Volumen específico", + "speed": "Velocidad", + "surface-charge-density": "Densidad de carga superficial", + "surface-tension": "Tensión superficial", + "temperature": "Temperatura", + "thermal-conductivity": "Conductividad térmica", + "time": "Tiempo", + "torque": "Par (torque)", + "turbidity": "Turbidez", + "voltage": "Voltaje", + "volume": "Volumen", + "volume-flow": "Flujo volumétrico" + }, "millimeter": "Milímetro", "centimeter": "Centímetro", + "decimeter": "Decímetro", "angstrom": "Ångström", "nanometer": "Nanómetro", "micrometer": "Micrómetro", @@ -5840,6 +6114,7 @@ "kilometer": "Kilómetro", "inch": "Pulgada", "foot": "Pie", + "foot-us": "Pie (encuesta de EE.UU.)", "yard": "Yarda", "mile": "Milla", "nautical-mile": "Milla náutica", @@ -5886,12 +6161,13 @@ "cubic-foot": "Pie cúbico", "cubic-yard": "Yarda cúbica", "fluid-ounce": "Onza líquida", + "fluid-ounce-per-second": "Onza líquida por segundo", "pint": "Pinta", "quart": "Cuarto", "gallon": "Galón", "oil-barrels": "Barril de petróleo", "cubic-meter-per-kilogram": "Metro cúbico por kilogramo", - "gill": "Gill", + "gill": "Jill (gill)", "hogshead": "Hogshead", "teaspoon": "Cucharadita", "tablespoon": "Cucharada", @@ -5904,11 +6180,15 @@ "meter-per-second": "Metro por segundo", "kilometer-per-hour": "Kilómetro por hora", "foot-per-second": "Pie por segundo", + "foot-per-minute": "Pie por minuto", "mile-per-hour": "Milla por hora", "knot": "Nudo", + "inch-per-second": "Pulgada por segundo", + "inch-per-hour": "Pulgada por hora", "millimeters-per-minute": "Milímetros por minuto", - "kilometer-per-hour-squared": "Kilómetro por hora al cuadrado", - "foot-per-second-squared": "Pie por segundo al cuadrado", + "meter-per-minute": "Metro por minuto", + "kilometer-per-hour-squared": "Kilómetro por hora cuadrado", + "foot-per-second-squared": "Pie por segundo cuadrado", "pascal": "Pascal", "kilopascal": "Kilopascal", "megapascal": "Megapascal", @@ -5923,6 +6203,7 @@ "newton-per-meter": "Newton por metro", "atmospheres": "Atmósferas", "pounds-per-square-inch": "Libras por pulgada cuadrada", + "kilopound-per-square-inch": "Kilolibras por pulgada cuadrada", "torr": "Torr", "inches-of-mercury": "Pulgadas de mercurio", "pascal-per-square-meter": "Pascal por metro cuadrado", @@ -5940,10 +6221,16 @@ "megajoule": "Megajulio", "gigajoule": "Gigajulio", "watt-hour": "Vatio-hora", + "watt-minute": "Vatio-minuto", "kilowatt-hour": "Kilovatio-hora", + "milliwatt-hour": "Milivatio-hora", + "megawatt-hour": "Megavatio-hora", + "gigawatt-hour": "Gigavatio-hora", "electron-volts": "Electrón-voltios", "joules-per-coulomb": "Julios por culombio", "british-thermal-unit": "Unidad térmica británica", + "thousand-british-thermal-unit": "Mil unidades térmicas británicas", + "million-british-thermal-unit": "Millón de unidades térmicas británicas", "foot-pound": "Pie-libra", "calorie": "Caloría", "small-calorie": "Caloría pequeña", @@ -5975,9 +6262,19 @@ "kilowatt-per-square-inch": "Kilovatios por pulgada cuadrada", "horsepower": "Caballo de fuerza", "btu-per-hour": "Unidades térmicas británicas por hora", + "btu-per-second": "Unidades térmicas británicas por segundo", + "btu-per-day": "Unidades térmicas británicas por día", + "mbtu-per-hour": "Mil unidades térmicas británicas por hora", + "mbtu-per-second": "Mil unidades térmicas británicas por segundo", + "mbtu-per-day": "Mil unidades térmicas británicas por día", + "mmbtu-per-hour": "Millón de unidades térmicas británicas por hora", + "mmbtu-per-second": "Millón de unidades térmicas británicas por segundo", + "mmbtu-per-day": "Millón de unidades térmicas británicas por día", + "foot-pound-per-second": "Pie-libra por segundo", "coulomb": "Culombio", "millicoulomb": "Miliculombios", "microcoulomb": "Microculombio", + "nanocoulomb": "Nanoculombio", "picocoulomb": "Picoculombio", "coulomb-per-meter": "Culombio por metro", "coulomb-per-cubic-meter": "Culombio por metro cúbico", @@ -6003,6 +6300,9 @@ "microampere": "Microamperio", "milliampere": "Miliamperio", "ampere": "Amperio", + "kiloampere": "Kiloamperio", + "megaampere": "Megaamperio", + "gigaampere": "Gigaamperio", "microampere-per-square-centimeter": "Microamperio por centímetro cuadrado", "ampere-per-square-meter": "Amperio por metro cuadrado", "ampere-per-meter": "Amperio por metro", @@ -6011,25 +6311,31 @@ "ampere-meter-squared": "Amperio-metro cuadrado", "nanovolt": "Nanovoltio", "picovolt": "Picovoltio", + "millivolt": "Milivoltios", + "microvolt": "Microvoltios", "volt": "Voltio", - "dbmV": "dBmV", - "dbm": "dBm", - "volt-meter": "Voltímetro", - "kilovolt-meter": "Kilovoltímetro", - "megavolt-meter": "Megavoltímetro", - "microvolt-meter": "Microvoltímetro", - "millivolt-meter": "Milivoltímetro", - "nanovolt-meter": "Nanovoltímetro", + "kilovolt": "Kilovoltio", + "megavolt": "Megavoltio", + "dbmV": "Decibelio-voltio", + "dbm": "Decibelio-miliwatt", + "volt-meter": "Voltio-metro", + "kilovolt-meter": "Kilovoltio-metro", + "megavolt-meter": "Megavoltio-metro", + "microvolt-meter": "Microvoltio-metro", + "millivolt-meter": "Milivoltio-metro", + "nanovolt-meter": "Nanovoltio-metro", "ohm": "Ohmio", "microohm": "Microohmio", "milliohm": "Miliohmio", "kilohm": "Kiloohmio", "megohm": "Megaohmio", "gigohm": "Gigaohmio", - "hertz": "Hercio", + "millihertz": "Milihertz", + "hertz": "Hertz", "kilohertz": "Kilohertz", "megahertz": "Megahertz", "gigahertz": "Gigahertz", + "terahertz": "Terahertz", "rpm": "Revoluciones por minuto", "candela-per-square-meter": "Candela por metro cuadrado", "candela": "Candela", @@ -6046,7 +6352,7 @@ "millimole": "Milimol", "kilomole": "Kilomol", "mole-per-cubic-meter": "Mol por metro cúbico", - "rssi": "RSSI", + "rssi": "Indicador de intensidad de señal recibida", "ppm": "Partes por millón", "ppb": "Partes por mil millones", "micrograms-per-cubic-meter": "Microgramos por metro cúbico", @@ -6057,7 +6363,7 @@ "neper": "Neper", "bel": "Bel", "decibel": "Decibelio", - "meters-per-second-squared": "Metros por segundo al cuadrado", + "meters-per-second-squared": "Metros por segundo cuadrado", "becquerel": "Becquerel", "curie": "Curie", "gray": "Gray", @@ -6097,7 +6403,7 @@ "gallons-per-mile": "Galones por milla", "liters-per-hour": "Litros por hora", "gallons-per-hour": "Galones por hora", - "beats-per-minute": "Pulsaciones por minuto", + "beats-per-minute": "Latidos por minuto", "millimeters-of-mercury": "Milímetros de mercurio", "milligrams-per-deciliter": "Miligramos por decilitro", "g-force": "Fuerza G", @@ -6115,6 +6421,9 @@ "millibars": "Milibares", "inch-of-mercury": "Pulgada de mercurio", "richter-scale": "Escala de Richter", + "nanosecond": "Nanosegundo", + "microsecond": "Microsegundo", + "millisecond": "Milisegundo", "second": "Segundo", "minute": "Minuto", "hour": "Hora", @@ -6130,6 +6439,7 @@ "gallons-per-minute": "Galones por minuto", "cubic-foot-per-second": "Pie cúbico por segundo", "milliliters-per-minute": "Mililitros por minuto", + "cubic-decimeter-per-second": "Decímetro cúbico por segundo", "bit": "Bit", "byte": "Byte", "kilobyte": "Kilobyte", @@ -6152,6 +6462,9 @@ "degree": "Grado", "radian": "Radián", "gradian": "Gradián", + "arcminute": "Minuto de arco", + "arcsecond": "Segundo de arco", + "milliradian": "Miliradián", "revolution": "Revolución", "siemens": "Siemens", "millisiemens": "Milisiemens", @@ -6177,7 +6490,7 @@ "nanotesla": "Nanotesla", "kilotesla": "Kilotesla", "megatesla": "Megatesla", - "millitesla-square-meters": "Militesla por metro cuadrado", + "millitesla-square-meters": "Militesla metro cuadrado", "gamma": "Gamma", "lambda": "Lambda", "square-meter-per-second": "Metro cuadrado por segundo", @@ -6191,25 +6504,25 @@ "poise": "Poise", "reynolds": "Reynolds", "pound-per-foot-hour": "Libra por pie-hora", - "newton-second-per-square-meter": "Newton segundo por metro cuadrado", - "dyne-second-per-square-centimeter": "Dina segundo por centímetro cuadrado", + "newton-second-per-square-meter": "Newton-segundo por metro cuadrado", + "dyne-second-per-square-centimeter": "Dina-segundo por centímetro cuadrado", "kilogram-per-meter-second": "Kilogramo por metro-segundo", - "tesla-square-meters": "Tesla por metro cuadrado", + "tesla-square-meters": "Tesla metro cuadrado", "maxwell": "Maxwell", "tesla-per-meter": "Tesla por metro", "gauss-per-centimeter": "Gauss por centímetro", "weber": "Weber", "microweber": "Microweber", "milliweber": "Milliweber", - "gauss-square-centimeter": "Gauss por centímetro cuadrado", - "kilogauss-square-centimeter": "Kilogauss por centímetro cuadrado", + "gauss-square-centimeter": "Gauss centímetro cuadrado", + "kilogauss-square-centimeter": "Kilogauss centímetro cuadrado", "henry": "Henry", - "millihenry": "Milihenry", - "microhenry": "Microhenry", - "nanohenry": "Nanohenry", - "henry-per-meter": "Henry por metro", + "millihenry": "Milihenrio", + "microhenry": "Microhenrio", + "nanohenry": "Nanohenrio", + "henry-per-meter": "Henrio por metro", "tesla-meter-per-ampere": "Tesla metro por amperio", - "gauss-per-oersted": "Gauss por Oersted", + "gauss-per-oersted": "Gauss por oersted", "kilogram-per-mole": "Kilogramo por mol", "gram-per-mole": "Gramo por mol", "milligram-per-mole": "Miligramo por mol", @@ -6219,21 +6532,23 @@ "volts-per-meter": "Voltios por metro", "kilovolts-per-meter": "Kilovoltios por metro", "radian-per-second": "Radián por segundo", - "radian-per-second-squared": "Radián por segundo cuadrado", + "radian-per-second-squared": "Radián por segundo al cuadrado", "revolutions-per-minute-per-second": "Aceleración angular", - "deg-per-second": "grados/segundo", + "deg-per-second": "Grados por segundo", + "rotation-per-minute": "Rotaciones por minuto", "degrees-brix": "Grados Brix", "katal": "Katal", - "katal-per-cubic-metre": "Katal por metro cúbico" + "katal-per-cubic-metre": "Katal por metro cúbico", + "paris-inch": "Pulgada de París" }, "user": { "user": "Usuario", "users": "Usuarios", - "customer-users": "Usuarios del cliente", - "tenant-admins": "Administradores del inquilino", + "customer-users": "Usuarios del customer", + "tenant-admins": "Administradores del tenant", "sys-admin": "Administrador del sistema", - "tenant-admin": "Administrador del inquilino", - "customer": "Cliente", + "tenant-admin": "Administrador del tenant", + "customer": "Customer", "anonymous": "Anónimo", "add": "Agregar usuario", "delete": "Eliminar usuario", @@ -6266,8 +6581,8 @@ "copy-activation-link": "Copiar enlace de activación", "activation-link-copied-message": "El enlace de activación del usuario ha sido copiado al portapapeles", "details": "Detalles", - "login-as-tenant-admin": "Iniciar sesión como administrador del inquilino", - "login-as-customer-user": "Iniciar sesión como usuario del cliente", + "login-as-tenant-admin": "Iniciar sesión como administrador del tenant", + "login-as-customer-user": "Iniciar sesión como usuario del Customer", "search": "Buscar usuarios", "selected-users": "{ count, plural, =1 {1 usuario} other {# usuarios} } seleccionados", "disable-account": "Desactivar cuenta de usuario", @@ -7774,6 +8089,18 @@ "fill-area-opacity": "Opacidad del área rellena", "range-chart-style": "Estilo del gráfico de rango" }, + "knob": { + "behavior": "Comportamiento", + "initial-value": "Valor inicial", + "initial-value-hint": "Acción para obtener el valor inicial del control giratorio.", + "on-value-change": "Al cambiar el valor", + "on-value-change-hint": "Acción que se activa cuando se cambia el valor del control giratorio.", + "range": "Rango", + "min": "mín", + "max": "máx", + "value": "Valor", + "fallback-initial-value": "Valor inicial alternativo" + }, "rpc": { "value-settings": "Configuración de valor", "initial-value": "Valor inicial", @@ -7830,9 +8157,7 @@ "led-status-value-timeseries": "Serie temporal del dispositivo que contiene el estado del LED", "check-status-method": "Método RPC para verificar estado del dispositivo", "parse-led-status-value-function": "Función para analizar el valor del estado del LED", - "knob-title": "Título del control giratorio", - "min-value": "Valor mínimo", - "max-value": "Valor máximo" + "knob-title": "Título del control giratorio" }, "maps": { "map-type": { @@ -8676,7 +9001,7 @@ "pie-chart-card-style": "Estilo de tarjeta de gráfico circular" }, "radar-chart": { - "radar-appearance": "Apariencia del gráfico de radar", + "radar-appearance": "Apariencia del radar", "shape": "Forma", "shape-polygon": "Polígono", "shape-circle": "Círculo", @@ -8684,10 +9009,14 @@ "line": "Línea", "points": "Puntos", "points-label": "Etiqueta de puntos", - "radar-axis": "Eje de radar", + "radar-axis": "Eje del radar", "axis-label": "Etiqueta del eje", "ticks-label": "Etiqueta de marcas", - "radar-chart-style": "Estilo de gráfico de radar" + "radar-chart-style": "Estilo del gráfico de radar", + "max-axes-scaling": "Escalado máximo de ejes", + "max-axes-scaling-hint": "Elige si cada eje del radar tiene su propio valor máximo (Separado) o comparte el valor más alto entre todos los ejes según el conjunto de datos del widget (Común).", + "separate": "Separado", + "common": "Común" }, "time-series-chart": { "chart": "Gráfico", @@ -9047,8 +9376,8 @@ "advanced-features": "Funciones avanzadas", "notification-center": "Centro de notificaciones", "api-usage": "Uso de API", - "customers": "Clientes", - "customers-hierarchy": "Jerarquía de clientes", + "customers": "Customers", + "customers-hierarchy": "Jerarquía de customers", "roles-and-permissions": "Roles y permisos", "groups": "Grupos", "integrations": "Integraciones", @@ -9075,7 +9404,7 @@ "sys-admin": { "step1": { "title": "Crear Tenant y Administrador del Tenant", - "content": "

    Un tenant es una persona u organización que posee o produce dispositivos y activos. El tenant puede tener múltiples usuarios administradores, clientes, dispositivos y activos.

    El Administrador del Tenant puede crear y gestionar dispositivos, activos, clientes y tableros dentro de la cuenta del tenant.

    Sigue la documentación para saber cómo hacerlo:

    ", + "content": "

    Un tenant es una persona u organización que posee o produce dispositivos y activos. El tenant puede tener múltiples usuarios administradores, customers, dispositivos y activos.

    El Administrador del Tenant puede crear y gestionar dispositivos, activos, customers y tableros dentro de la cuenta del tenant.

    Sigue la documentación para saber cómo hacerlo:

    ", "how-to-create-tenant": "Cómo crear Tenant y Administrador del Tenant" }, "step2": { @@ -9085,7 +9414,7 @@ }, "step3": { "title": "Configurar función: Proveedor de SMS", - "content": "

    Configura proveedores de SMS para notificar a los clientes sobre las alarmas vía SMS.

    Sigue la documentación para saber cómo hacerlo:

    ", + "content": "

    Configura proveedores de SMS para notificar a los customers sobre las alarmas vía SMS.

    Sigue la documentación para saber cómo hacerlo:

    ", "how-to-configure-sms-provider": "Cómo configurar el proveedor de SMS" }, "step4": { @@ -9098,7 +9427,7 @@ }, "step6": { "title": "Configurar función: OAuth 2", - "content": "

    Simplifica el inicio de sesión para los usuarios de tenant y clientes con inicio de sesión único a través de OAuth 2.0.

    Sigue la documentación para saber cómo hacerlo:

    " + "content": "

    Simplifica el inicio de sesión para los usuarios de tenant y customer con inicio de sesión único a través de OAuth 2.0.

    Sigue la documentación para saber cómo hacerlo:

    " } }, "tenant-admin": { @@ -9142,8 +9471,8 @@ "how-to-create-alarm": "Cómo crear una alarma" }, "step6": { - "title": "Crear cliente y compartir tablero", - "content": "

    Al crear tableros para el usuario final, un usuario cliente solo podrá ver sus propios dispositivos y los datos de otro cliente estarán ocultos.

    Sigue la documentación para saber cómo hacerlo:

    " + "title": "Crear customer y compartir tablero", + "content": "

    Al crear tableros para el usuario final, un usuario customer solo podrá ver sus propios dispositivos y los datos de otro customer estarán ocultos.

    Sigue la documentación para saber cómo hacerlo:

    " } } } diff --git a/ui-ngx/src/assets/locale/locale.constant-fr_FR.json b/ui-ngx/src/assets/locale/locale.constant-fr_FR.json index d715c60187..e278f5f8a9 100644 --- a/ui-ngx/src/assets/locale/locale.constant-fr_FR.json +++ b/ui-ngx/src/assets/locale/locale.constant-fr_FR.json @@ -545,7 +545,13 @@ "slack-settings": "Paramètres Slack", "mobile-settings": "Paramètres mobiles", "firebase-service-account-file": "Fichier JSON des identifiants du compte de service Firebase", - "select-firebase-service-account-file": "Glissez-déposez votre fichier d'identifiants de compte de service Firebase ou " + "select-firebase-service-account-file": "Glissez-déposez votre fichier d'identifiants de compte de service Firebase ou ", + "trendz": "Trendz", + "trendz-settings": "Paramètres Trendz", + "trendz-url": "URL Trendz", + "trendz-url-required": "L'URL Trendz est requise", + "trendz-api-key": "Clé API Trendz", + "trendz-enable": "Activer Trendz" }, "alarm": { "alarm": "Alarme", @@ -677,8 +683,8 @@ "filter-type-entity-list": "Liste d'entités", "filter-type-entity-name": "Nom de l'entité", "filter-type-entity-type": "Type d'entité", - "filter-type-state-entity": "Entité à partir de l'état du tableau de bord", - "filter-type-state-entity-description": "Entité extraite des paramètres d'état du tableau de bord", + "filter-type-state-entity": "Entité de l'état du tableau de bord", + "filter-type-state-entity-description": "Entité extraite des paramètres de l'état du tableau de bord", "filter-type-asset-type": "Type d'actif", "filter-type-asset-type-description": "Actifs de type '{{assetTypes}}'", "filter-type-asset-type-and-name-description": "Actifs de type '{{assetTypes}}' dont le nom commence par '{{prefix}}'", @@ -709,17 +715,18 @@ "filter-type-required": "Le type de filtre est requis.", "entity-filter-no-entity-matched": "Aucune entité ne correspond au filtre spécifié.", "no-entity-filter-specified": "Aucun filtre d'entité spécifié", - "root-state-entity": "Utiliser l'entité d'état du tableau de bord comme racine", + "root-state-entity": "Utiliser l'entité de l'état du tableau de bord comme racine", "last-level-relation": "Ne récupérer que le dernier niveau de relation", "root-entity": "Entité racine", "state-entity-parameter-name": "Nom du paramètre d'entité d'état", "default-state-entity": "Entité d'état par défaut", "default-entity-parameter-name": "Par défaut", - "max-relation-level": "Niveau maximum de relation", + "query-options": "Options de requête", + "max-relation-level": "Niveau de relation maximal", "unlimited-level": "Niveau illimité", "state-entity": "Entité d'état du tableau de bord", "all-entities": "Toutes les entités", - "any-relation": "n'importe laquelle" + "any-relation": "n'importe quelle" }, "asset": { "asset": "Actif", @@ -917,22 +924,27 @@ "view-statistics": "Voir les statistiques" }, "api-limit": { - "cassandra-queries": "Requêtes Cassandra", + "cassandra-write-queries-core": "Requêtes d'écriture Cassandra via l'API REST", + "cassandra-read-queries-core": "Requêtes de lecture Cassandra via l'API REST et WS (télémétrie)", + "cassandra-write-queries-rule-engine": "Requêtes d'écriture Cassandra du moteur de règles (télémétrie)", + "cassandra-read-queries-rule-engine": "Requêtes de lecture Cassandra du moteur de règles (télémétrie)", + "cassandra-write-queries-monolith": "Requêtes d'écriture Cassandra monolithiques (télémétrie)", + "cassandra-read-queries-monolith": "Requêtes de lecture Cassandra monolithiques (télémétrie)", "entity-version-creation": "Création de version d'entité", "entity-version-load": "Chargement de version d'entité", "notification-requests": "Requêtes de notification", "notification-requests-per-rule": "Requêtes de notification par règle", - "rest-api-requests": "Requêtes REST API", - "rest-api-requests-per-customer": "Requêtes REST API par client", + "rest-api-requests": "Requêtes API REST", + "rest-api-requests-per-customer": "Requêtes API REST par client", "transport-messages": "Messages de transport", "transport-messages-per-device": "Messages de transport par appareil", "transport-messages-per-gateway": "Messages de transport par passerelle", "transport-messages-per-gateway-device": "Messages de transport par appareil de passerelle", "ws-updates-per-session": "Mises à jour WS par session", "edge-events": "Événements Edge", - "edge-events-per-edge": "Événements Edge par instance", + "edge-events-per-edge": "Événements Edge par instance Edge", "edge-uplink-messages": "Messages montants Edge", - "edge-uplink-messages-per-edge": "Messages montants Edge par instance" + "edge-uplink-messages-per-edge": "Messages montants Edge par instance Edge" }, "audit-log": { "audit": "Audit", @@ -996,9 +1008,9 @@ "failures": "Échecs", "entity": "entité", "hint": { - "main-limited": "Pas plus de {{msg}} messages de débogage de {{entity}} par {{time}} ne seront enregistrés.", - "on-failure": "Enregistrer uniquement les messages d'erreur.", - "all-messages": "Enregistrer tous les messages de débogage." + "main-limited": "Pas plus de {{msg}} messages de débogage pour {{entity}} toutes les {{time}} seront enregistrés.", + "on-failure": "Journaliser uniquement les messages d'erreur.", + "all-messages": "Journaliser tous les messages de débogage." } }, "calculated-fields": { @@ -1018,13 +1030,13 @@ "add-argument": "Ajouter un argument", "test-script-function": "Tester la fonction script", "no-arguments": "Aucun argument configuré", - "argument-settings": "Paramètres de l'argument", + "argument-settings": "Paramètres des arguments", "argument-current": "Entité actuelle", - "argument-current-tenant": "Tenant actuel", + "argument-current-tenant": "Locataire actuel", "argument-device": "Appareil", "argument-asset": "Actif", "argument-customer": "Client", - "argument-tenant": "Tenant actuel", + "argument-tenant": "Locataire actuel", "argument-type": "Type d'argument", "see-debug-events": "Voir les événements de débogage", "attribute": "Attribut", @@ -1057,24 +1069,103 @@ "delete-multiple-title": "Êtes-vous sûr de vouloir supprimer { count, plural, =1 {1 champ calculé} other {# champs calculés} } ?", "delete-multiple-text": "Attention, après confirmation tous les champs calculés sélectionnés seront supprimés et toutes les données associées seront irrécupérables.", "test-with-this-message": "Tester avec ce message", + "use-latest-timestamp": "Utiliser l'horodatage le plus récent", "hint": { - "arguments-simple-with-rolling": "Le type simple de champ calculé ne doit pas contenir de clés avec un rolling de séries temporelles.", + "arguments-simple-with-rolling": "Un champ calculé de type simple ne doit pas contenir de clés avec type de séries temporelles roulantes.", "arguments-empty": "Les arguments ne doivent pas être vides.", - "expression-required": "L'expression est requise.", + "expression-required": "Une expression est requise.", "expression-invalid": "L'expression est invalide", "expression-max-length": "La longueur de l'expression doit être inférieure à 255 caractères.", "argument-name-required": "Le nom de l'argument est requis.", "argument-name-pattern": "Le nom de l'argument est invalide.", "argument-name-duplicate": "Un argument portant ce nom existe déjà.", - "argument-name-max-length": "Le nom de l'argument doit contenir moins de 256 caractères.", - "argument-name-forbidden": "Le nom de l'argument est réservé et ne peut pas être utilisé.", + "argument-name-max-length": "Le nom de l'argument doit comporter moins de 256 caractères.", + "argument-name-forbidden": "Ce nom d'argument est réservé et ne peut pas être utilisé.", "argument-type-required": "Le type d'argument est requis.", - "max-args": "Nombre maximum d'arguments atteint.", + "max-args": "Nombre maximal d'arguments atteint.", "decimals-range": "Les décimales par défaut doivent être un nombre entre 0 et 15.", - "expression": "L'expression par défaut montre comment transformer une température de Fahrenheit en Celsius.", - "arguments-entity-not-found": "L'entité cible de l'argument est introuvable." + "expression": "L'expression par défaut montre comment convertir une température de Fahrenheit en Celsius.", + "arguments-entity-not-found": "Entité cible de l'argument introuvable.", + "use-latest-timestamp": "Si activé, la valeur calculée sera enregistrée avec l'horodatage le plus récent des télémétries des arguments, au lieu de l'heure du serveur." } }, + "ai-models": { + "ai-models": "Modèles IA", + "ai-model": "Modèle IA", + "model": "Modèle", + "name": "Nom", + "ai-provider": "Fournisseur IA", + "no-found": "Aucun modèle IA trouvé", + "list": "{ count, plural, =1 {Un modèle} other {Liste de # modèles} }", + "selected-fields": "{ count, plural, =1 {1 modèle} other {# modèles} } sélectionné(s)", + "add": "Ajouter un modèle", + "delete-model-title": "Êtes-vous sûr de vouloir supprimer le modèle '{{modelName}}' ?", + "delete-model-text": "Attention, après confirmation, le modèle et toutes les données associées seront irrécupérables.", + "delete-models-title": "Êtes-vous sûr de vouloir supprimer { count, plural, =1 {1 modèle} other {# modèles} } ?", + "delete-models-text": "Attention, après confirmation, tous les modèles sélectionnés seront supprimés et toutes les données associées deviendront irrécupérables.", + "ai-providers": { + "openai": "OpenAI", + "azure-openai": "Azure OpenAI", + "google-ai-gemini": "Google AI Gemini", + "google-vertex-ai-gemini": "Google Vertex AI Gemini", + "mistral-ai": "Mistral AI", + "anthropic": "Anthropic", + "amazon-bedrock": "Amazon Bedrock", + "github-models": "Modèles GitHub" + }, + "name-required": "Le nom est requis.", + "name-max-length": "Le nom doit comporter 255 caractères ou moins.", + "provider": "Fournisseur", + "api-key": "Clé API", + "api-key-required": "La clé API est requise.", + "project-id": "ID de projet", + "project-id-required": "L'ID de projet est requis.", + "location": "Emplacement", + "location-required": "L'emplacement est requis.", + "service-account-key-file": "Fichier de clé du compte de service", + "service-account-key-file-required": "Le fichier de clé du compte de service est requis.", + "no-file": "Aucun fichier sélectionné.", + "drop-file": "Déposez un fichier ou cliquez pour en sélectionner un à téléverser.", + "personal-access-token": "Jeton d'accès personnel", + "personal-access-token-required": "Le jeton d'accès personnel est requis.", + "configuration": "Configuration", + "model-id": "ID du modèle", + "model-id-required": "L'ID du modèle est requis.", + "deployment-name": "Nom du déploiement", + "deployment-name-required": "Le nom du déploiement est requis.", + "set": "Définir", + "region": "Région", + "region-required": "La région est requise.", + "access-key-id": "ID de la clé d'accès", + "access-key-id-required": "L'ID de la clé d'accès est requis.", + "secret-access-key": "Clé d'accès secrète", + "secret-access-key-required": "La clé d'accès secrète est requise.", + "temperature": "Température", + "temperature-hint": "Ajuste le niveau d'aléatoire dans la sortie du modèle. Des valeurs plus élevées augmentent l'aléatoire, tandis que des valeurs plus faibles la réduisent.", + "temperature-min": "Doit être supérieur ou égal à 0.", + "top-p": "Top P", + "top-p-hint": "Crée un ensemble des jetons les plus probables pour que le modèle puisse choisir. Des valeurs plus élevées créent un ensemble plus large et diversifié, tandis que des valeurs plus faibles le réduisent.", + "top-p-min-max": "Doit être supérieur à 0 et inférieur ou égal à 1.", + "top-k": "Top K", + "top-k-hint": "Limite les choix du modèle à un ensemble fixe des \"K\" jetons les plus probables.", + "top-k-min": "Doit être supérieur ou égal à 0.", + "presence-penalty": "Pénalité de présence", + "presence-penalty-hint": "Applique une pénalité fixe à la probabilité d’un jeton s’il est déjà apparu dans le texte.", + "frequency-penalty": "Pénalité de fréquence", + "frequency-penalty-hint": "Applique une pénalité à la probabilité d’un jeton qui augmente avec sa fréquence dans le texte.", + "max-output-tokens": "Nombre maximum de jetons en sortie", + "max-output-tokens-min": "Doit être supérieur à 0.", + "max-output-tokens-hint": "Définit le nombre maximal de jetons que le modèle peut générer en une seule réponse.", + "endpoint": "Point de terminaison", + "endpoint-required": "Le point de terminaison est requis.", + "service-version": "Version du service", + "check-connectivity": "Vérifier la connectivité", + "check-connectivity-success": "La requête de test a réussi", + "check-connectivity-failed": "La requête de test a échoué", + "no-model-matching": "Aucun modèle correspondant à '{{entity}}' trouvé.", + "model-required": "Le modèle est requis.", + "no-model-text": "Aucun modèle trouvé." + }, "confirm-on-exit": { "message": "Vous avez des modifications non enregistrées. Êtes-vous sûr de vouloir quitter cette page ?", "html-message": "Vous avez des modifications non enregistrées.
    Êtes-vous sûr de vouloir quitter cette page ?", @@ -1753,7 +1844,8 @@ "step": "Pas", "selected-options-limit": "Limite d'options sélectionnées", "advanced-ui-settings": "Paramètres UI avancés", - "disable-on-property": "Désactiver en fonction de la propriété", + "disable-on-property": "Désactiver selon la propriété", + "disable-on-property-none": "Aucune (champ toujours activé)", "display-condition-function": "Fonction de condition d'affichage", "sub-label": "Sous-étiquette", "vertical-divider-after": "Séparateur vertical après", @@ -1787,7 +1879,8 @@ "array-item": "Élément du tableau", "item-type": "Type d'élément", "item-name": "Nom de l'élément", - "no-items": "Aucun élément" + "no-items": "Aucun élément", + "support-unit-conversion": "Prise en charge de la conversion d'unités" }, "clear-form": "Effacer le formulaire", "clear-form-prompt": "Êtes-vous sûr de vouloir supprimer toutes les propriétés du formulaire ?", @@ -1911,6 +2004,7 @@ "mqtt-use-json-format-for-default-downlink-topics-hint": "Utilise JSON pour les sujets : v1/devices/me/attributes/response/$request_id, etc. Ne s'applique pas aux sujets v2.", "mqtt-send-ack-on-validation-exception": "Envoyer PUBACK en cas d'échec de validation", "mqtt-send-ack-on-validation-exception-hint": "Par défaut, la session MQTT est fermée sur erreur. Si activé, envoie un accusé de réception PUBACK à la place.", + "mqtt-protocol-version": "Version du protocole", "snmp-add-mapping": "Ajouter un mappage SNMP", "snmp-mapping-not-configured": "Aucun mappage OID vers série temporelle ou attribut configuré", "snmp-timseries-or-attribute-name": "Nom de série temporelle/attribut pour le mappage", @@ -2171,6 +2265,9 @@ "add-lwm2m-server-config": "Ajouter un serveur LwM2M", "no-config-servers": "Aucun serveur configuré", "others-tab": "Autres paramètres", + "ota-update": "Mise à jour OTA", + "use-object-19-for-ota-update": "Utiliser l'objet 19 pour les métadonnées de fichier OTA (checksum, taille, version, nom)", + "use-object-19-for-ota-update-hint": "Utiliser l'objet ressource avec ObjectId = 19 pour les mises à jour OTA : FirmWare → InstanceId = 65534, SoftWare → InstanceId = 65535. Le format des données est du JSON encodé en Base64. Ce JSON contient les métadonnées du fichier OTA : \"Checksum\" (SHA256). Champs supplémentaires : \"Title\" (nom de la mise à jour OTA), \"Version\" (version OTA), \"File Name\" (nom du fichier pour le stockage OTA côté client), \"File Size\" (taille OTA en octets).", "client-strategy": "Stratégie client lors de la connexion", "client-strategy-label": "Stratégie", "client-strategy-only-observe": "Envoyer uniquement la requête d’observation après connexion initiale", @@ -2201,7 +2298,17 @@ "default-object-id": "Version par défaut de l'objet (Attribut)", "default-object-id-ver": { "v1-0": "1.0", - "v1-1": "1.1" + "v1-1": "1.1", + "v1-2": "1.2" + }, + "observe-strategy": { + "observe-strategy": "Stratégie d'observation", + "single": "Unique", + "single-description": "Une requête Observe par ressource (plus grande précision, plus de trafic réseau)", + "composite-all": "Composé - tout", + "composite-all-description": "Toutes les ressources sont observées avec une seule requête Composite Observe (plus efficace, moins flexible)", + "composite-by-object": "Composé par objets", + "composite-by-object-description": "Les ressources sont regroupées par type d'objet et observées via des requêtes Composite Observe distinctes (approche équilibrée)" } }, "snmp": { @@ -2524,6 +2631,8 @@ "type-current-user-owner": "Propriétaire utilisateur actuel", "type-calculated-field": "Champ calculé", "type-calculated-fields": "Champs calculés", + "type-ai-model": "Modèle IA", + "type-ai-models": "Modèles IA", "type-widgets-bundle": "Pack de widgets", "type-widgets-bundles": "Packs de widgets", "list-of-widgets-bundles": "{ count, plural, =1 {Un pack de widgets} other {Liste de # packs de widgets} }", @@ -2553,6 +2662,8 @@ "type-tb-resources": "Ressources", "list-of-tb-resources": "{ count, plural, =1 {Une ressource} other {Liste de # ressources} }", "type-ota-package": "Paquet OTA", + "type-ota-packages": "Packages OTA", + "list-of-ota-packages": "{ count, plural, =1 {Un package OTA} other {Liste de # packages OTA} }", "type-rpc": "RPC", "type-queue": "File d'attente", "type-queue-stats": "Statistiques de file d'attente", @@ -2938,6 +3049,7 @@ "missing-key-filters-error": "Les filtres clés sont manquants pour le filtre '{{filter}}'.", "filter": "Filtre", "editable": "Éditable", + "editable-hint": "Autoriser l'utilisateur à modifier la valeur du filtre dans les tableaux de bord.", "no-filters-found": "Aucun filtre trouvé.", "no-filter-text": "Aucun filtre spécifié", "add-filter-prompt": "Veuillez ajouter un filtre", @@ -2977,6 +3089,8 @@ "filter-user-params": "Paramètres utilisateur du prédicat du filtre", "user-parameters": "Paramètres utilisateur", "display-label": "Libellé à afficher", + "custom-label": "Libellé personnalisé", + "custom-label-hint": "Activer pour définir votre propre étiquette pour le filtre. Si désactivé, une étiquette sera générée automatiquement.", "order-priority": "Priorité d'ordre des champs", "key-filter": "Filtre par clé", "key-filters": "Filtres par clé", @@ -3021,7 +3135,8 @@ "switch-to-dynamic-value": "Basculer vers la valeur dynamique", "switch-to-default-value": "Basculer vers la valeur par défaut", "inherit-owner": "Hériter du propriétaire", - "source-attribute-not-set": "Si l'attribut source n'est pas défini" + "source-attribute-not-set": "Si l'attribut source n'est pas défini", + "unit": "Unité" }, "fullscreen": { "expand": "Agrandir en plein écran", @@ -3406,6 +3521,7 @@ "power-button-background": "Arrière-plan du bouton de mise sous tension", "value-box-background": "Arrière-plan de la boîte de valeur", "value-units": "Unités de valeur", + "enable-units-scale": "Activer les unités sur l'échelle", "filtration-mode": "Mode de filtration", "filtration-mode-hint": "Valeur entière indiquant le mode de filtration actuel.", "filtration-mode-update": "État de mise à jour du mode de filtration", @@ -3718,10 +3834,12 @@ "min-version": "Version minimale", "invalid-version-pattern": "Format de version invalide. Veuillez utiliser le format : majeur.mineur.correctif (ex. : 1.0.0).", "mobile-center": "Centre mobile", - "mobile-package": "Paquet de l'application", - "mobile-package-max-length": "Le paquet de l'application doit contenir moins de 256 caractères", - "mobile-package-required": "Le paquet de l'application est requis.", - "mobile-package-pattern": "Format du paquet de l'application invalide", + "mobile-package": "Package de l'application", + "mobile-package-max-length": "Le package de l'application doit contenir moins de 256 caractères", + "mobile-package-required": "Le package de l'application est requis.", + "mobile-package-pattern": "Format du package de l'application invalide", + "mobile-package-title": "Titre de l'application", + "mobile-package-title-max-length": "Le titre de l'application doit contenir moins de 256 caractères", "no-application": "Aucune application trouvée", "no-bundles": "Aucun bundle trouvé", "platform-type": "Type de plateforme", @@ -3805,17 +3923,13 @@ "prepare-environment-text": "L'application mobile Flutter ThingsBoard nécessite le SDK Flutter. Suivez les instructions pour configurer le SDK Flutter.", "get-source-code-title": "Obtenir le code source de l'application", "get-source-code-text": "Vous pouvez obtenir le code source de l'application mobile Flutter ThingsBoard en le clonant depuis le dépôt GitHub :", - "configure-api-title": "Configurer le point de terminaison de l'API ThingsBoard", - "configure-api-text": "Ouvrez le projet flutter_thingsboard_pe_app dans votre éditeur/IDE. Modifiez :", - "configure-api-hint": "Définissez la valeur de la constante thingsBoardApiEndpoint pour correspondre au point de terminaison API de votre instance ThingsBoard. N'utilisez pas les noms d'hôte “localhost” ou “127.0.0.1”.", - "run-app-title": "Lancer l'application", - "run-app-text": "Lancez l'application comme décrit dans votre IDE.\nSi vous utilisez le terminal, exécutez l'application avec la commande suivante :", + "configure-app-settings-title": "Configurer les paramètres de l'application", + "configure-app-settings-text": "Téléchargez le fichier de configuration et placez-le dans le répertoire racine du projet que vous avez cloné à l'étape précédente.", + "download-file": "Télécharger le fichier", + "run-app-title": "Exécuter l'application", + "run-app-text": "Exécutez l'application comme décrit dans votre IDE.\nSi vous utilisez le terminal, exécutez l'application avec la commande suivante :", "more-information": "Des informations détaillées sont disponibles dans notre documentation de démarrage.", - "getting-started": "Commencer", - "configure-package-title": "Configurer le paquet de l'application", - "configure-package-text": "Vous pouvez modifier manuellement le paquet de l'application ou utiliser un outil CLI tiers.", - "configure-package-text-install": "Pour installer l'outil Rename CLI, exécutez la commande suivante :", - "configure-package-run-commands": "Exécutez ces commandes dans le répertoire racine de votre projet :" + "getting-started": "Démarrage" } }, "notification": { @@ -3839,6 +3953,7 @@ "new-platform-version-trigger-settings": "Paramètres du déclencheur de nouvelle version de la plateforme", "rate-limits-trigger-settings": "Paramètres du déclencheur de dépassement de limites", "task-processing-failure-trigger-settings": "Paramètres du déclencheur d'échec du traitement de tâche", + "resources-shortage-trigger-settings": "Paramètres de déclenchement de pénurie de ressources", "at-least-one-should-be-selected": "Au moins un élément doit être sélectionné", "basic-settings": "Paramètres de base", "button-text": "Texte du bouton", @@ -3853,6 +3968,7 @@ "create-new": "Créer nouveau", "created": "Créé", "customize-messages": "Personnaliser les messages", + "cpu-threshold": "Seuil CPU", "delete-notification-text": "Attention, après confirmation, la notification sera irrécupérable.", "delete-notification-title": "Êtes-vous sûr de vouloir supprimer la notification ?", "delete-notifications-text": "Attention, après confirmation, les notifications seront irrécupérables.", @@ -3919,6 +4035,7 @@ "input-fields-support-templatization": "Les champs de saisie prennent en charge la templatisation.", "link": "Lien", "link-required": "Le lien est requis", + "link-max-length": "Le lien doit comporter au maximum {{ length }} caractères", "link-type": { "dashboard": "Ouvrir le tableau de bord", "link": "Ouvrir un lien URL" @@ -3945,6 +4062,7 @@ "no-severity-found": "Aucune sévérité trouvée", "no-severity-matching": "'{{severity}}' introuvable.", "no-template-matching": "Aucune ressource correspondant à '{{template}}' trouvée.", + "create-new-template": "Créer un nouveau modèle !", "not-found-slack-recipient": "Destinataire Slack introuvable", "notification": "Notification", "notification-center": "Centre de notifications", @@ -3968,6 +4086,7 @@ "only-rule-chain-lifecycle-failures": "Uniquement les échecs de cycle de vie de chaîne de règles", "only-rule-node-lifecycle-failures": "Uniquement les échecs de cycle de vie de nœud de règles", "platform-users": "Utilisateurs de la plateforme", + "ram-threshold": "Seuil RAM", "rate-limits": "Limites de taux", "rate-limits-hint": "Si le champ est vide, le déclencheur s’appliquera à toutes les limites de taux", "recipient": "Destinataire", @@ -4033,6 +4152,7 @@ "start-from-scratch": "Commencer de zéro", "status": "Statut", "stop-escalation-alarm-status-become": "Arrêter l'escalade lorsque le statut de l'alarme devient :", + "storage-threshold": "Seuil de stockage", "subject": "Sujet", "subject-required": "Le sujet est requis", "subject-max-length": "Le sujet doit contenir au maximum {{ length }} caractères", @@ -4051,10 +4171,11 @@ "rule-engine-lifecycle-event": "Événement du cycle de vie du moteur de règles", "rule-node": "Nœud de règle", "new-platform-version": "Nouvelle version de la plateforme", - "rate-limits": "Limites de taux dépassées", - "edge-communication-failure": "Échec de communication avec l'Edge", + "rate-limits": "Limites de débit dépassées", + "edge-communication-failure": "Échec de communication Edge", "edge-connection": "Connexion Edge", - "task-processing-failure": "Échec de traitement de tâche" + "task-processing-failure": "Échec du traitement de la tâche", + "resources-shortage": "Pénurie de ressources" }, "templates": "Modèles", "notification-templates": "Notifications / Modèles", @@ -4074,10 +4195,11 @@ "entity-action": "Action sur l'entité", "rule-engine-lifecycle-event": "Événement du cycle de vie du moteur de règles", "new-platform-version": "Nouvelle version de la plateforme", - "rate-limits": "Limites de taux dépassées", + "rate-limits": "Limites de débit dépassées", "edge-connection": "Connexion Edge", - "edge-communication-failure": "Échec de communication avec l'Edge", - "task-processing-failure": "Échec de traitement de tâche", + "edge-communication-failure": "Échec de communication Edge", + "task-processing-failure": "Échec du traitement de la tâche", + "resources-shortage": "Pénurie de ressources", "trigger": "Déclencheur", "trigger-required": "Le déclencheur est requis" }, @@ -4119,6 +4241,7 @@ "checksum-copied-message": "La somme de contrôle du paquet a été copiée dans le presse-papiers", "change-firmware": "Changer le micrologiciel peut entraîner la mise à jour de { count, plural, =1 {1 appareil} other {# appareils} }.", "change-software": "Changer le logiciel peut entraîner la mise à jour de { count, plural, =1 {1 appareil} other {# appareils} }.", + "change-ota-setting-title": "Êtes-vous sûr de vouloir modifier les paramètres OTA ?", "chose-compatible-device-profile": "Le paquet téléchargé ne sera disponible que pour les appareils ayant le profil choisi.", "chose-firmware-distributed-device": "Choisissez le micrologiciel à distribuer aux appareils", "chose-software-distributed-device": "Choisissez le logiciel à distribuer aux appareils", @@ -4314,6 +4437,7 @@ "add-relation-filter": "Ajouter un filtre de relation", "any-relation": "Toute relation", "relation-filters": "Filtres de relation", + "relation-filter": "Filtre de relation", "additional-info": "Infos supplémentaires (JSON)", "invalid-additional-info": "Impossible d'analyser le JSON des infos supplémentaires.", "no-relations-text": "Aucune relation trouvée", @@ -5174,12 +5298,12 @@ "skip-empty-fields-tooltip": "Les champs vides ne seront pas ajoutés au message de sortie ou aux métadonnées de sortie.", "fetch-interval": "Intervalle de récupération", "fetch-strategy": "Stratégie de récupération", - "fetch-timeseries-from-to": "Récupérer les séries temporelles de {{startInterval}} {{startIntervalTimeUnit}} à {{endInterval}} {{endIntervalTimeUnit}}.", - "fetch-timeseries-from-to-invalid": "Récupération des séries invalide (« Début de l'intervalle » doit être inférieur à « Fin de l'intervalle »).", - "use-metadata-dynamic-interval-tooltip": "Utilise un intervalle dynamique basé sur le message et les métadonnées si activé.", - "all-mode-hint": "En mode « Tout », récupère la télémétrie selon l'intervalle et les paramètres définis.", - "first-mode-hint": "Récupère la télémétrie la plus proche du début de l'intervalle.", - "last-mode-hint": "Récupère la télémétrie la plus proche de la fin de l'intervalle.", + "fetch-timeseries-from-to": "Récupérer les séries temporelles de {{startInterval}} {{startIntervalTimeUnit}} en arrière jusqu'à {{endInterval}} {{endIntervalTimeUnit}} en arrière.", + "fetch-timeseries-from-to-invalid": "Récupération des séries temporelles invalide (\"Début de l'intervalle\" doit être inférieur à \"Fin de l'intervalle\").", + "use-metadata-dynamic-interval-tooltip": "Si cette option est sélectionnée, le nœud de règle utilisera un intervalle dynamique basé sur le message et les modèles de métadonnées.", + "all-mode-hint": "Si le mode de récupération \"Tout\" est sélectionné, le nœud de règle récupérera la télémétrie de l'intervalle de récupération avec des paramètres de requête configurables.", + "first-mode-hint": "Si le mode de récupération \"Premier\" est sélectionné, le nœud de règle récupérera la télémétrie la plus proche du début de l'intervalle.", + "last-mode-hint": "Si le mode de récupération \"Dernier\" est sélectionné, le nœud de règle récupérera la télémétrie la plus proche de la fin de l'intervalle.", "ascending": "Croissant", "descending": "Décroissant", "min": "Minimum", @@ -5304,6 +5428,36 @@ "html-text-description": "Permet d'utiliser des balises HTML pour la mise en forme, les liens et les images dans le corps du message.", "dynamic-text-description": "Permet d'utiliser dynamiquement le texte brut ou HTML selon le modèle.", "after-template-evaluation-hint": "Après évaluation du modèle, la valeur doit être true pour HTML, et false pour Texte brut." + }, + "ai": { + "ai-model": "Modèle IA", + "model": "Modèle", + "ai-model-hint": "Sélectionnez le modèle IA préconfiguré pour traiter les requêtes envoyées par ce nœud de règle, ou utilisez \"Créer un nouveau\" pour en configurer un nouveau.", + "prompt-settings": "Paramètres de prompt", + "prompt-settings-hint": "Le prompt système optionnel définit le rôle général et les contraintes de l'IA, tandis que le prompt utilisateur précise la tâche à exécuter. Les deux champs prennent en charge la modélisation par modèle (templatization).", + "system-prompt": "Prompt système", + "system-prompt-max-length": "Le prompt système doit comporter 500 000 caractères ou moins.", + "system-prompt-blank": "Le prompt système ne doit pas être vide.", + "user-prompt": "Prompt utilisateur", + "user-prompt-required": "Le prompt utilisateur est requis.", + "user-prompt-max-length": "Le prompt utilisateur doit comporter 500 000 caractères ou moins.", + "user-prompt-blank": "Le prompt utilisateur ne doit pas être vide.", + "response-format": "Format de réponse", + "response-text": "Texte", + "response-json": "JSON", + "response-json-schema": "Schéma JSON", + "response-format-hint-TEXT": "Permet au modèle de générer un texte libre, qui peut ou non être un objet JSON valide. Si la sortie n’est pas un objet JSON valide, elle sera automatiquement encapsulée dans un objet JSON sous la clé \"response\".", + "response-format-hint-JSON": "Le modèle doit générer une réponse sous forme de JSON valide. Si la sortie n’est pas un objet JSON valide, elle sera automatiquement encapsulée dans un objet JSON sous la clé \"response\".", + "response-format-hint-JSON_SCHEMA": "Le modèle doit générer un JSON conforme à la structure et aux types de données définis dans le schéma fourni. Si la sortie n’est pas un objet JSON valide, elle sera automatiquement encapsulée dans un objet JSON sous la clé \"response\".", + "response-json-schema-hint": "Bien que tout schéma JSON valide puisse être saisi, ce nœud de règle ne prend en charge qu’un sous-ensemble limité de ses fonctionnalités. Consultez la documentation du nœud pour plus de détails.", + "response-json-schema-required": "Le schéma JSON est requis", + "advanced-settings": "Paramètres avancés", + "timeout": "Délai d'attente", + "timeout-hint": "Temps maximal d’attente d’une réponse \nde la part du modèle IA avant l’arrêt de la requête.", + "timeout-required": "Le délai d'attente est requis", + "timeout-validation": "Doit être compris entre 1 seconde et 10 minutes.", + "force-acknowledgement": "Forcer l’accusé de réception", + "force-acknowledgement-hint": "Si activé, le message entrant est accusé immédiatement. La réponse du modèle est ensuite mise en file d’attente comme un nouveau message distinct." } }, "timezone": { @@ -5625,7 +5779,10 @@ "too-small-value-zero": "La valeur doit être supérieure à 0", "too-small-value-one": "La valeur doit être supérieure à 1", "queue-size-is-limited-by-system-configuration": "La taille de la file d’attente est également limitée par la configuration système.", - "cassandra-tenant-limits-configuration": "Requête Cassandra pour le locataire", + "cassandra-write-tenant-core-limits-configuration": "Requêtes d’écriture Cassandra via l’API REST", + "cassandra-read-tenant-core-limits-configuration": "Requêtes de lecture Cassandra via l’API REST et WS (télémétrie)", + "cassandra-write-tenant-rule-engine-limits-configuration": "Requêtes d’écriture Cassandra du moteur de règles (télémétrie)", + "cassandra-read-tenant-rule-engine-limits-configuration": "Requêtes de lecture Cassandra du moteur de règles (télémétrie)", "ws-limit-max-sessions-per-tenant": "Nombre maximal de sessions par locataire", "ws-limit-max-sessions-per-customer": "Nombre maximal de sessions par client", "ws-limit-max-sessions-per-regular-user": "Nombre maximal de sessions par utilisateur régulier", @@ -5638,26 +5795,30 @@ "ws-limit-updates-per-session": "Mises à jour WS par session", "rate-limits": { "add-limit": "Ajouter une limite", + "and-also-less-than": "et aussi inférieur à", "advanced-settings": "Paramètres avancés", "edit-limit": "Modifier la limite", "calculated-field-debug-event-rate-limit": "Événements de débogage des champs calculés", - "edit-calculated-field-debug-event-rate-limit": "Modifier la limite des événements de débogage de champ calculé", - "edit-transport-tenant-msg-title": "Modifier les limites de messages de transport du locataire", - "edit-transport-tenant-telemetry-msg-title": "Modifier les limites des messages de télémétrie du locataire", - "edit-transport-tenant-telemetry-data-points-title": "Modifier les limites des points de données de télémétrie du locataire", - "edit-transport-device-msg-title": "Modifier les limites des messages de l’appareil", - "edit-transport-device-telemetry-msg-title": "Modifier les limites des messages de télémétrie de l’appareil", - "edit-transport-device-telemetry-data-points-title": "Modifier les limites des points de données de télémétrie de l’appareil", - "edit-transport-gateway-msg-title": "Modifier les limites des messages de la passerelle", - "edit-transport-gateway-telemetry-msg-title": "Modifier les limites des messages de télémétrie de la passerelle", - "edit-transport-gateway-telemetry-data-points-title": "Modifier les limites des points de données de télémétrie de la passerelle", - "edit-transport-gateway-device-msg-title": "Modifier les limites des messages des appareils de la passerelle", - "edit-transport-gateway-device-telemetry-msg-title": "Modifier les limites des messages de télémétrie des appareils de la passerelle", - "edit-transport-gateway-device-telemetry-data-points-title": "Modifier les limites des points de données de télémétrie des appareils de la passerelle", - "edit-tenant-rest-limits-title": "Modifier les limites des requêtes REST du locataire", - "edit-customer-rest-limits-title": "Modifier les limites des requêtes REST du client", - "edit-ws-limit-updates-per-session-title": "Modifier les limites des mises à jour WS par session", - "edit-cassandra-tenant-limits-configuration-title": "Modifier les limites des requêtes Cassandra du locataire", + "edit-calculated-field-debug-event-rate-limit": "Modifier les limites de débit des événements de débogage des champs calculés", + "edit-transport-tenant-msg-title": "Modifier les limites de débit des messages transport du locataire", + "edit-transport-tenant-telemetry-msg-title": "Modifier les limites de débit des messages de télémétrie transport du locataire", + "edit-transport-tenant-telemetry-data-points-title": "Modifier les limites de débit des points de données de télémétrie transport du locataire", + "edit-transport-device-msg-title": "Modifier les limites de débit des messages transport de l'appareil", + "edit-transport-device-telemetry-msg-title": "Modifier les limites de débit des messages de télémétrie transport de l'appareil", + "edit-transport-device-telemetry-data-points-title": "Modifier les limites de débit des points de données de télémétrie transport de l'appareil", + "edit-transport-gateway-msg-title": "Modifier les limites de débit des messages transport de la passerelle", + "edit-transport-gateway-telemetry-msg-title": "Modifier les limites de débit des messages de télémétrie transport de la passerelle", + "edit-transport-gateway-telemetry-data-points-title": "Modifier les limites de débit des points de données de télémétrie transport de la passerelle", + "edit-transport-gateway-device-msg-title": "Modifier les limites de débit des messages transport des appareils de la passerelle", + "edit-transport-gateway-device-telemetry-msg-title": "Modifier les limites de débit des messages de télémétrie transport des appareils de la passerelle", + "edit-transport-gateway-device-telemetry-data-points-title": "Modifier les limites de débit des points de données de télémétrie transport des appareils de la passerelle", + "edit-tenant-rest-limits-title": "Modifier les limites de débit des requêtes REST du locataire", + "edit-customer-rest-limits-title": "Modifier les limites de débit des requêtes REST du client", + "edit-ws-limit-updates-per-session-title": "Modifier les limites de débit des mises à jour WS par session", + "edit-cassandra-write-tenant-core-limits-configuration": "Modifier les requêtes d’écriture Cassandra via l’API REST", + "edit-cassandra-read-tenant-core-limits-configuration": "Modifier les requêtes de lecture Cassandra via l’API REST et WS (télémétrie)", + "edit-cassandra-write-tenant-rule-engine-limits-configuration": "Modifier les requêtes d’écriture Cassandra du moteur de règles (télémétrie)", + "edit-cassandra-read-tenant-rule-engine-limits-configuration": "Modifier les requêtes de lecture Cassandra du moteur de règles (télémétrie)", "edit-tenant-entity-export-rate-limit-title": "Modifier les limites de création de version d’entité", "edit-tenant-entity-import-rate-limit-title": "Modifier les limites de chargement de version d’entité", "edit-tenant-notification-request-rate-limit-title": "Modifier les limites des requêtes de notification", @@ -5679,6 +5840,7 @@ "per-seconds": "Par secondes", "per-seconds-required": "La durée est requise.", "per-seconds-min": "La valeur minimale est 1.", + "per-seconds-duplicate": "Taux de temps en double. Chaque intervalle de temps doit être unique.", "rate-limits": "Limites de débit", "remove-limit": "Supprimer la limite", "transport-tenant-msg": "Messages de transport du locataire", @@ -5768,11 +5930,11 @@ "sec": "{{ sec }} sec", "sec-short": "{{ sec }}s", "short": { - "years": "{ years, plural, =1 {1 année } other {# années } }", + "years": "{ years, plural, =1 {1 an } other {# ans } }", "days": "{ days, plural, =1 {1 jour } other {# jours } }", "hours": "{ hours, plural, =1 {1 heure } other {# heures } }", - "minutes": "{{minutes}} min", - "seconds": "{{seconds}} sec" + "minutes": "{{minutes}} min ", + "seconds": "{{seconds}} sec " }, "realtime": "Temps réel", "history": "Historique", @@ -5826,13 +5988,125 @@ "value": "Valeur", "date": "Date", "show-date-time-interval": "Afficher l’intervalle date/heure", - "show-date-time-interval-hint": "Afficher l’intervalle date/heure selon l’agrégation des données.", + "show-date-time-interval-hint": "Afficher l’intervalle date/heure en fonction de l’agrégation des données.", + "hide-zero-tooltip-values": "Masquer les valeurs nulles", "background-color": "Couleur d’arrière-plan", "background-blur": "Flou d’arrière-plan" }, "unit": { + "set-unit-conversion": "Définir la conversion d’unités", + "unit-settings": { + "unit-settings": "Paramètres d’unité", + "source-unit": "Unité source", + "source-unit-hint": "Il s’agit de l’unité de la valeur stockée. L’unité à partir de laquelle vous effectuez la conversion. Entrez le symbole utilisé par vos données sources (ex. : m, km, ft, in).", + "target-metric-unit": "Unité métrique cible", + "target-metric-unit-hint": "Choisissez l’unité métrique (SI) vers laquelle vous souhaitez convertir votre valeur source (ex. : cm, mm, km).", + "target-imperial-unit": "Unité impériale cible", + "target-imperial-unit-hint": "Choisissez l’unité impériale vers laquelle vous souhaitez convertir votre valeur source (ex. : in, ft, yd).", + "target-hybrid-unit": "Unité hybride cible", + "target-hybrid-unit-hint": "Choisissez l’unité hybride vers laquelle vous souhaitez convertir votre valeur source (ex. : cm, in, km). Les unités hybrides combinent des unités métriques ou impériales.", + "enable-unit-conversion": "Activer la conversion d’unités", + "enable-unit-conversion-hint": "Activez cette option pour appliquer la conversion. Si désactivée, votre valeur source sera transmise sans modification. Désactivée s’il n’existe qu’une seule unité dans le groupe de mesure correspondant (ex. : flux lumineux, AQI)." + }, + "unit-system": "Système d’unités", + "unit-system-type": { + "AUTO": "Auto", + "METRIC": "Métrique", + "IMPERIAL": "Impérial", + "HYBRID": "Hybride" + }, + "measures": { + "absorbed-dose-rate": "Débit de dose absorbée", + "acceleration": "Accélération", + "acidity": "Acidité", + "air-quality-index": "Indice de qualité de l’air", + "amount-of-substance": "Quantité de matière", + "angle": "Angle", + "angular-acceleration": "Accélération angulaire", + "area": "Surface", + "area-density": "Densité surfacique", + "capacitance": "Capacité électrique", + "catalytic-activity": "Activité catalytique", + "catalytic-concentration": "Concentration catalytique", + "charge": "Charge", + "current-density": "Densité de courant", + "data-transfer-rate": "Débit de transfert de données", + "density": "Densité", + "digital": "Numérique", + "dimension-ratio": "Rapport de dimensions", + "dynamic-viscosity": "Viscosité dynamique", + "earthquake-magnitude": "Magnitude sismique", + "electric-charge-density": "Densité de charge électrique", + "electric-current": "Courant électrique", + "electric-dipole-moment": "Moment dipolaire électrique", + "electric-field-strength": "Intensité du champ électrique", + "electric-flux": "Flux électrique", + "electric-permittivity": "Permittivité électrique", + "electric-polarizability": "Polarisabilité électrique", + "electrical-conductance": "Conductance électrique", + "electrical-conductivity": "Conductivité électrique", + "energy": "Énergie", + "energy-density": "Densité d’énergie", + "force": "Force", + "frequency": "Fréquence", + "fuel-efficiency": "Rendement énergétique", + "heat-capacity": "Capacité thermique", + "illuminance": "Éclairement lumineux", + "inductance": "Inductance", + "kinematic-viscosity": "Viscosité cinématique", + "length": "Longueur", + "light-exposure": "Exposition lumineuse", + "linear-charge-density": "Densité linéique de charge", + "logarithmic-ratio": "Rapport logarithmique", + "luminous-efficacy": "Efficacité lumineuse", + "luminous-flux": "Flux lumineux", + "luminous-intensity": "Intensité lumineuse", + "magnetic-field-gradient": "Gradient de champ magnétique", + "magnetic-flux": "Flux magnétique", + "magnetic-flux-density": "Induction magnétique", + "magnetic-moment": "Moment magnétique", + "magnetic-permeability": "Perméabilité magnétique", + "mass": "Masse", + "mass-fraction": "Fraction massique", + "molar-concentration": "Concentration molaire", + "molar-energy": "Énergie molaire", + "molar-heat-capacity": "Capacité thermique molaire", + "molar-mass": "Masse molaire", + "number-concentration": "Concentration numérique", + "parts-per-million": "Parties par million", + "power": "Puissance", + "power-density": "Densité de puissance", + "pressure": "Pression", + "radiance": "Radiance", + "radiant-intensity": "Intensité de rayonnement", + "radiation-dose": "Dose de rayonnement", + "radioactive-decay": "Désintégration radioactive", + "radioactivity": "Radioactivité", + "radioactivity-concentration": "Concentration radioactive", + "reciprocal-length": "Longueur réciproque", + "resistance": "Résistance", + "reynolds-number": "Nombre de Reynolds", + "signal-level": "Niveau du signal", + "solid-angle": "Angle solide", + "specific-energy": "Énergie spécifique", + "specific-heat-capacity": "Capacité thermique spécifique", + "specific-humidity": "Humidité spécifique", + "specific-volume": "Volume spécifique", + "speed": "Vitesse", + "surface-charge-density": "Densité de charge surfacique", + "surface-tension": "Tension superficielle", + "temperature": "Température", + "thermal-conductivity": "Conductivité thermique", + "time": "Temps", + "torque": "Couple", + "turbidity": "Turbidité", + "voltage": "Tension", + "volume": "Volume", + "volume-flow": "Débit volumique" + }, "millimeter": "Millimètre", "centimeter": "Centimètre", + "decimeter": "Décimètre", "angstrom": "Angström", "nanometer": "Nanomètre", "micrometer": "Micromètre", @@ -5840,6 +6114,7 @@ "kilometer": "Kilomètre", "inch": "Pouce", "foot": "Pied", + "foot-us": "Pied (US survey)", "yard": "Yard", "mile": "Mille", "nautical-mile": "Mille nautique", @@ -5886,6 +6161,7 @@ "cubic-foot": "Pied cube", "cubic-yard": "Yard cube", "fluid-ounce": "Once liquide", + "fluid-ounce-per-second": "Once liquide par seconde", "pint": "Pinte", "quart": "Quart", "gallon": "Gallon", @@ -5904,9 +6180,13 @@ "meter-per-second": "Mètre par seconde", "kilometer-per-hour": "Kilomètre par heure", "foot-per-second": "Pied par seconde", + "foot-per-minute": "Pied par minute", "mile-per-hour": "Mille par heure", "knot": "Nœud", + "inch-per-second": "Pouce par seconde", + "inch-per-hour": "Pouce par heure", "millimeters-per-minute": "Millimètres par minute", + "meter-per-minute": "Mètre par minute", "kilometer-per-hour-squared": "Kilomètre par heure carrée", "foot-per-second-squared": "Pied par seconde carrée", "pascal": "Pascal", @@ -5923,6 +6203,7 @@ "newton-per-meter": "Newton par mètre", "atmospheres": "Atmosphères", "pounds-per-square-inch": "Livres par pouce carré", + "kilopound-per-square-inch": "Kilolivre par pouce carré", "torr": "Torr", "inches-of-mercury": "Pouces de mercure", "pascal-per-square-meter": "Pascal par mètre carré", @@ -5940,10 +6221,16 @@ "megajoule": "Mégajoule", "gigajoule": "Gigajoule", "watt-hour": "Watt-heure", + "watt-minute": "Watt-minute", "kilowatt-hour": "Kilowatt-heure", - "electron-volts": "Électronvolt", + "milliwatt-hour": "Milliwatt-heure", + "megawatt-hour": "Mégawatt-heure", + "gigawatt-hour": "Gigawatt-heure", + "electron-volts": "Électron-volt", "joules-per-coulomb": "Joules par coulomb", - "british-thermal-unit": "British Thermal Unit", + "british-thermal-unit": "Unité thermique britannique", + "thousand-british-thermal-unit": "Mille unités thermiques britanniques", + "million-british-thermal-unit": "Million d’unités thermiques britanniques", "foot-pound": "Pied-livre", "calorie": "Calorie", "small-calorie": "Petite calorie", @@ -5974,10 +6261,20 @@ "watt-per-square-inch": "Watt par pouce carré", "kilowatt-per-square-inch": "Kilowatt par pouce carré", "horsepower": "Cheval-vapeur", - "btu-per-hour": "BTU/heure", + "btu-per-hour": "Unités thermiques britanniques par heure", + "btu-per-second": "Unités thermiques britanniques par seconde", + "btu-per-day": "Unités thermiques britanniques par jour", + "mbtu-per-hour": "Mille unités thermiques britanniques par heure", + "mbtu-per-second": "Mille unités thermiques britanniques par seconde", + "mbtu-per-day": "Mille unités thermiques britanniques par jour", + "mmbtu-per-hour": "Million d’unités thermiques britanniques par heure", + "mmbtu-per-second": "Million d’unités thermiques britanniques par seconde", + "mmbtu-per-day": "Million d’unités thermiques britanniques par jour", + "foot-pound-per-second": "Pied-livre par seconde", "coulomb": "Coulomb", "millicoulomb": "Millicoulomb", "microcoulomb": "Microcoulomb", + "nanocoulomb": "Nanocoulomb", "picocoulomb": "Picocoulomb", "coulomb-per-meter": "Coulomb par mètre", "coulomb-per-cubic-meter": "Coulomb par mètre cube", @@ -5996,25 +6293,32 @@ "barn": "Barn", "circular-inch": "Pouce circulaire", "milliampere-hour": "Milliampère-heure", - "ampere-hours": "Ampères-heures", - "kiloampere-hours": "Kiloampères-heures", + "ampere-hours": "Ampère-heure", + "kiloampere-hours": "Kiloampère-heure", "nanoampere": "Nanoampère", "picoampere": "Picoampère", "microampere": "Microampère", "milliampere": "Milliampère", "ampere": "Ampère", + "kiloampere": "Kiloampère", + "megaampere": "Mégaampère", + "gigaampere": "Gigaampère", "microampere-per-square-centimeter": "Microampère par centimètre carré", "ampere-per-square-meter": "Ampère par mètre carré", "ampere-per-meter": "Ampère par mètre", "oersted": "Oersted", - "bohr-magneton": "Magnéton de Bohr", + "bohr-magneton": "Magneton de Bohr", "ampere-meter-squared": "Ampère-mètre carré", "nanovolt": "Nanovolt", "picovolt": "Picovolt", + "millivolt": "Millivolt", + "microvolt": "Microvolt", "volt": "Volt", - "dbmV": "dBmV", - "dbm": "dBm", - "volt-meter": "Voltmètre", + "kilovolt": "Kilovolt", + "megavolt": "Mégavolt", + "dbmV": "Décibel-volt", + "dbm": "Décibel-milliwatt", + "volt-meter": "Volt-mètre", "kilovolt-meter": "Kilovolt-mètre", "megavolt-meter": "Mégavolt-mètre", "microvolt-meter": "Microvolt-mètre", @@ -6024,12 +6328,14 @@ "microohm": "Microohm", "milliohm": "Milliohm", "kilohm": "Kilohm", - "megohm": "Mégohm", - "gigohm": "Gigohm", + "megohm": "Mégaohm", + "gigohm": "Gigaohm", + "millihertz": "Millihertz", "hertz": "Hertz", "kilohertz": "Kilohertz", "megahertz": "Mégahertz", "gigahertz": "Gigahertz", + "terahertz": "Térrahertz", "rpm": "Tours par minute", "candela-per-square-meter": "Candela par mètre carré", "candela": "Candela", @@ -6046,12 +6352,12 @@ "millimole": "Millimole", "kilomole": "Kilomole", "mole-per-cubic-meter": "Mole par mètre cube", - "rssi": "RSSI", + "rssi": "Indicateur de puissance du signal reçu (RSSI)", "ppm": "Parties par million", "ppb": "Parties par milliard", "micrograms-per-cubic-meter": "Microgrammes par mètre cube", - "aqi": "Indice de qualité de l’air (AQI)", - "gram-per-cubic-meter": "Grammes par mètre cube", + "aqi": "Indice de qualité de l'air (AQI)", + "gram-per-cubic-meter": "Gramme par mètre cube", "gram-per-kilogram": "Humidité spécifique", "millimeters-per-second": "Millimètres par seconde", "neper": "Néper", @@ -6090,11 +6396,11 @@ "pound-per-cubic-foot": "Livre par pied cube", "ounces-per-cubic-inch": "Onces par pouce cube", "tons-per-cubic-yard": "Tonnes par yard cube", - "particle-density": "Densité des particules", + "particle-density": "Densité de particules", "kilometers-per-liter": "Kilomètres par litre", "miles-per-gallon": "Miles par gallon", "liters-per-100-km": "Litres par 100 km", - "gallons-per-mile": "Gallons par mile", + "gallons-per-mile": "Gallons par mille", "liters-per-hour": "Litres par heure", "gallons-per-hour": "Gallons par heure", "beats-per-minute": "Battements par minute", @@ -6115,6 +6421,9 @@ "millibars": "Millibars", "inch-of-mercury": "Pouce de mercure", "richter-scale": "Échelle de Richter", + "nanosecond": "Nanoseconde", + "microsecond": "Microseconde", + "millisecond": "Milliseconde", "second": "Seconde", "minute": "Minute", "hour": "Heure", @@ -6130,6 +6439,7 @@ "gallons-per-minute": "Gallons par minute", "cubic-foot-per-second": "Pied cube par seconde", "milliliters-per-minute": "Millilitres par minute", + "cubic-decimeter-per-second": "Décimètre cube par seconde", "bit": "Bit", "byte": "Octet", "kilobyte": "Kilooctet", @@ -6146,12 +6456,15 @@ "gigabit-per-second": "Gigabit par seconde", "terabit-per-second": "Térabit par seconde", "byte-per-second": "Octet par seconde", - "kilobyte-per-second": "Kilooctets par seconde", - "megabyte-per-second": "Mégaoctets par seconde", - "gigabyte-per-second": "Gigaoctets par seconde", + "kilobyte-per-second": "Kilooctet par seconde", + "megabyte-per-second": "Mégaoctet par seconde", + "gigabyte-per-second": "Gigaoctet par seconde", "degree": "Degré", "radian": "Radian", - "gradian": "Gradian", + "gradian": "Grade", + "arcminute": "Minute d’arc", + "arcsecond": "Seconde d’arc", + "milliradian": "Milliradian", "revolution": "Révolution", "siemens": "Siemens", "millisiemens": "Millisiemens", @@ -6209,7 +6522,7 @@ "nanohenry": "Nanohenry", "henry-per-meter": "Henry par mètre", "tesla-meter-per-ampere": "Tesla mètre par ampère", - "gauss-per-oersted": "Gauss par Oersted", + "gauss-per-oersted": "Gauss par oersted", "kilogram-per-mole": "Kilogramme par mole", "gram-per-mole": "Gramme par mole", "milligram-per-mole": "Milligramme par mole", @@ -6221,10 +6534,12 @@ "radian-per-second": "Radian par seconde", "radian-per-second-squared": "Radian par seconde carrée", "revolutions-per-minute-per-second": "Accélération angulaire", - "deg-per-second": "Degrés/seconde", + "deg-per-second": "Degrés par seconde", + "rotation-per-minute": "Rotation par minute", "degrees-brix": "Degrés Brix", "katal": "Katal", - "katal-per-cubic-metre": "Katal par mètre cube" + "katal-per-cubic-metre": "Katal par mètre cube", + "paris-inch": "Pouce de Paris" }, "user": { "user": "Utilisateur", @@ -6387,14 +6702,14 @@ "no-widgets-text": "Aucun widget trouvé", "management": "Gestion des widgets", "editor": "Éditeur de widgets", - "confirm-to-exit-editor-html": "Vous avez des paramètres de widget non enregistrés.
    Êtes-vous sûr de vouloir quitter cette page ?", - "widget-type-not-found": "Problème de chargement de la configuration du widget.
    Le type de widget associé a probablement été supprimé.", - "widget-type-load-error": "Le widget n’a pas été chargé en raison des erreurs suivantes :", + "confirm-to-exit-editor-html": "Vous avez des paramètres de widget non enregistrés.
    Êtes-vous sûr de vouloir quitter cette page ?", + "widget-type-not-found": "Problème lors du chargement de la configuration du widget.
    Le type de widget associé a probablement été supprimé.", + "widget-type-load-error": "Le widget n’a pas pu être chargé en raison des erreurs suivantes :", "remove": "Supprimer le widget", "delete": "Supprimer le widget", - "edit": "Modifier le widget", - "remove-widget-title": "Êtes-vous sûr de vouloir supprimer le widget '{{widgetTitle}}' ?", - "remove-widget-text": "Après confirmation, le widget et toutes les données associées seront irrécupérables.", + "edit": "Éditer le widget", + "remove-widget-title": "Êtes-vous sûr de vouloir supprimer le widget '{{widgetTitle}}' ?", + "remove-widget-text": "Après confirmation, le widget et toutes les données associées seront définitivement perdus.", "replace-reference-with-widget-copy": "Remplacer la référence par une copie du widget", "timeseries": "Séries temporelles", "search-data": "Rechercher des données", @@ -6508,7 +6823,7 @@ "dialog-hide-dashboard-toolbar": "Masquer la barre d'outils du tableau de bord dans la boîte de dialogue", "dialog-width": "Largeur de la boîte de dialogue en pourcentage de la largeur de l'écran", "dialog-height": "Hauteur de la boîte de dialogue en pourcentage de la hauteur de l'écran", - "dialog-size-range-error": "La taille de la boîte de dialogue doit être comprise entre 1 et 100 %.", + "dialog-size-range-error": "La taille de la boîte de dialogue doit être comprise entre 1 et 100.", "popover-preferred-placement": "Placement préféré de l'infobulle", "popover-placement-top": "Haut", "popover-placement-topLeft": "Haut gauche", @@ -7774,6 +8089,18 @@ "fill-area-opacity": "Opacité de la zone remplie", "range-chart-style": "Style du graphique à plages" }, + "knob": { + "behavior": "Comportement", + "initial-value": "Valeur initiale", + "initial-value-hint": "Action permettant d’obtenir la valeur initiale du bouton rotatif.", + "on-value-change": "Lors du changement de valeur", + "on-value-change-hint": "Action déclenchée lorsque la valeur du bouton rotatif est modifiée.", + "range": "Plage", + "min": "min", + "max": "max", + "value": "Valeur", + "fallback-initial-value": "Valeur initiale de secours" + }, "rpc": { "value-settings": "Paramètres de la valeur", "initial-value": "Valeur initiale", @@ -7830,9 +8157,7 @@ "led-status-value-timeseries": "Série temporelle contenant l’état LED", "check-status-method": "Méthode RPC de vérification de l’état de l’appareil", "parse-led-status-value-function": "Fonction d’analyse de l’état LED", - "knob-title": "Titre du bouton rotatif", - "min-value": "Valeur minimale", - "max-value": "Valeur maximale" + "knob-title": "Titre du bouton rotatif" }, "maps": { "map-type": { @@ -8683,11 +9008,15 @@ "color": "Couleur", "line": "Ligne", "points": "Points", - "points-label": "Libellé des points", + "points-label": "Étiquette des points", "radar-axis": "Axe radar", - "axis-label": "Libellé de l'axe", - "ticks-label": "Libellé des graduations", - "radar-chart-style": "Style du graphique radar" + "axis-label": "Étiquette des axes", + "ticks-label": "Étiquette des graduations", + "radar-chart-style": "Style du graphique radar", + "max-axes-scaling": "Échelle maximale des axes", + "max-axes-scaling-hint": "Choisissez si chaque axe radar a sa propre valeur maximale (Séparée) ou partage la valeur maximale parmi toutes les axes en fonction des données du widget (Commune).", + "separate": "Séparée", + "common": "Commune" }, "time-series-chart": { "chart": "Graphique", diff --git a/ui-ngx/src/assets/locale/locale.constant-lt_LT.json b/ui-ngx/src/assets/locale/locale.constant-lt_LT.json index 935ae65eb4..c3ddb5af74 100644 --- a/ui-ngx/src/assets/locale/locale.constant-lt_LT.json +++ b/ui-ngx/src/assets/locale/locale.constant-lt_LT.json @@ -1,7708 +1,9524 @@ -{ - "access": { - "unauthorized": "Neautorizuotas", - "unauthorized-access": "Neautorizuota prieiga", - "unauthorized-access-text": "Prieiga prie šio funkcionalumo leidžiama tik prisijungusiam vartotojui!", - "access-forbidden": "Prieiga uždrausta", - "access-forbidden-text": "Neturite teisių prieigai!
    Bandykite prisijungti kitu vartotoju.", - "refresh-token-expired": "Sesijos galiojimas baigėsi", - "refresh-token-failed": "Sesijos atnaujinti nepavyksta", - "permission-denied": "Leidimas nesuteiktas", - "permission-denied-text": "Šio veiksmo atlikti negalite!" - }, - "account": { - "account": "Account", - "notification-settings": "Notification settings" - }, - "action": { - "activate": "Aktyvuoti", - "suspend": "Sustabdyti", - "save": "Išsaugoti", - "saveAs": "Išsaugoti kaip", - "move": "Perkelti", - "cancel": "Atmesti", - "ok": "OK", - "delete": "Panaikinti", - "add": "Pridėti", - "yes": "Taip", - "no": "Ne", - "update": "Atnaujinti", - "remove": "Panaikinti", - "search": "Paieška", - "clear-search": "Išvalyti", - "assign": "Priskirti", - "unassign": "Atsieti", - "share": "Pasidalinti", - "make-private": "Riboti matomumą", - "make-public": "Neriboti matomumo", - "apply": "Taikyti", - "apply-changes": "Pritaikyti pakeitimus", - "edit-mode": "Redagavimo režimas", - "enter-edit-mode": "Redagavimo režimas", - "decline-changes": "Atšaukti pakeitimus", - "open": "Atverti", - "decline": "Atšaukti", - "close": "Uždaryti", - "back": "Atgal", - "run": "Paleisti", - "sign-in": "Prisijunkite!", - "edit": "Redaguoti", - "view": "Peržiūrėti", - "create": "Kurti", - "drag": "Vilkti", - "refresh": "Atnaujinti", - "undo": "Atšaukti", - "copy": "Kopijuoti", - "paste": "Įklijuoti", - "copy-reference": "Kopijuoti nuorodą", - "paste-reference": "Įklijuoti nuorodą", - "import": "Importuoti", - "export": "Eksportuoti", - "share-via": "Pasidalinti per {{provider}}", - "select": "Pasirinkti", - "continue": "Tęsti", - "discard-changes": "Atsisakyti pakeitimų", - "download": "Parsisiųsti", - "next": "Sekantis", - "next-with-label": "Sekantis: {{label}}", - "read-more": "Skaityti daugiau", - "hide": "Paslėpti", - "done": "Atlikta", - "print": "Spausdinti", - "restore": "Atkurti", - "confirm": "Patvirtinti", - "more": "Daugiau", - "less": "Mažiau", - "skip": "Praleisti", - "send": "Siųsti", - "reset": "Nustatyti iš naujo", - "show-more": "Rodyti daugiau", - "dont-show-again": "Daugiau neberodyti", - "see-documentation": "Žiūrėti dokumentacijoje", - "clear": "Išvalyti" - }, - "aggregation": { - "aggregation": "Agregavimas", - "function": "Duomenų aggregavimo funkcija", - "limit": "Maksimali reikšmė", - "group-interval": "Grupavimo intervalas", - "min": "Min", - "max": "Max", - "avg": "Vidurkis", - "sum": "Sum", - "count": "Kiekis", - "none": "Nėra" - }, - "admin": { - "settings": "Settings", - "general": "General", - "general-settings": "General Settings", - "home-settings": "Home Settings", - "home": "Home", - "outgoing-mail": "Mail Server", - "outgoing-mail-settings": "Outgoing Mail Server Settings", - "system-settings": "System Settings", - "test-mail-sent": "Test mail was successfully sent!", - "base-url": "Base URL", - "base-url-required": "Base URL is required.", - "prohibit-different-url": "Prohibit to use hostname from the client request headers", - "prohibit-different-url-hint": "This setting should be enabled for production environments. May cause security issues when disabled", - "mail-from": "Mail From", - "mail-from-required": "Mail From is required.", - "smtp-protocol": "SMTP protocol", - "smtp-host": "SMTP host", - "smtp-host-required": "SMTP host is required.", - "smtp-port": "SMTP port", - "smtp-port-required": "You must supply a smtp port.", - "smtp-port-invalid": "That doesn't look like a valid smtp port.", - "timeout-msec": "Timeout (msec)", - "timeout-required": "Timeout is required.", - "timeout-invalid": "That doesn't look like a valid timeout.", - "enable-tls": "Enable TLS", - "tls-version": "TLS version", - "enable-proxy": "Enable proxy", - "proxy-host": "Proxy host", - "proxy-host-required": "Proxy host is required.", - "proxy-port": "Proxy port", - "proxy-port-required": "Proxy port is required.", - "proxy-port-range": "Proxy port should be in a range from 1 to 65535.", - "proxy-user": "Proxy user", - "proxy-password": "Proxy password", - "change-password": "Change password", - "send-test-mail": "Send test mail", - "use-system-mail-settings": "Use System Mail Server Settings", - "mail-templates": "Mail Templates", - "mail-template-settings": "Mail Templates Settings", - "use-system-mail-template-settings": "Use System Mail Templates", - "mail-template": { - "mail-template": "Mail template", - "test": "Test email message", - "activation": "Account activation message", - "account-activated": "Account activated message", - "account-lockout": "Account lockout message", - "reset-password": "Reset password message", - "password-was-reset": "Password was reset message", - "user-activated": "User activated message", - "user-registered": "User registered message", - "api-usage-state-enabled": "Api usage state enabled", - "api-usage-state-warning": "Api usage state warning", - "api-usage-state-disabled": "Api usage state disabled", - "two-fa-verification": "2FA verification message" - }, - "mail-subject": "Mail Subject", - "mail-body": "Mail body", - "sms-provider": "SMS provider", - "sms-provider-settings": "SMS provider settings", - "use-system-sms-settings": "Use System SMS provider settings", - "sms-provider-type": "SMS provider type", - "sms-provider-type-required": "SMS provider type is required.", - "sms-provider-type-aws-sns": "Amazon SNS", - "sms-provider-type-twilio": "Twilio", - "sms-provider-type-smpp": "SMPP", - "aws-access-key-id": "AWS Access Key ID", - "aws-access-key-id-required": "AWS Access Key ID is required", - "aws-secret-access-key": "AWS Secret Access Key", - "aws-secret-access-key-required": "AWS Secret Access Key is required", - "aws-region": "AWS Region", - "aws-region-required": "AWS Region is required", - "number-from": "Phone Number From", - "number-from-required": "Phone Number From is required.", - "number-to": "Phone Number To", - "number-to-required": "Phone Number To is required.", - "phone-number-hint": "Phone Number in E.164 format, ex. +19995550123", - "phone-number-hint-twilio": "Phone Number in E.164 format/Phone Number's SID/Messaging Service SID, ex. +19995550123/PNXXX/MGXXX", - "phone-number-pattern": "Invalid phone number. Should be in E.164 format, ex. +19995550123.", - "phone-number-pattern-twilio": "Invalid phone number. Should be in E.164 format/Phone Number's SID/Messaging Service SID, ex. +19995550123/PNXXX/MGXXX.", - "sms-message": "SMS message", - "sms-message-required": "SMS message is required.", - "sms-message-max-length": "SMS message can't be longer 1600 characters", - "twilio-account-sid": "Twilio Account SID", - "twilio-account-sid-required": "Twilio Account SID is required", - "twilio-account-token": "Twilio Account Token", - "twilio-account-token-required": "Twilio Account Token is required", - "send-test-sms": "Send test SMS", - "test-sms-sent": "Test SMS was successfully sent!", - "security-settings": "Security settings", - "password-policy": "Password policy", - "minimum-password-length": "Minimum password length", - "minimum-password-length-required": "Minimum password length is required", - "minimum-password-length-range": "Minimum password length should be in a range from 5 to 50", - "minimum-uppercase-letters": "Minimum number of uppercase letters", - "minimum-uppercase-letters-range": "Minimum number of uppercase letters can't be negative", - "minimum-lowercase-letters": "Minimum number of lowercase letters", - "minimum-lowercase-letters-range": "Minimum number of lowercase letters can't be negative", - "minimum-digits": "Minimum number of digits", - "minimum-digits-range": "Minimum number of digits can't be negative", - "minimum-special-characters": "Minimum number of special characters", - "minimum-special-characters-range": "Minimum number of special characters can't be negative", - "password-expiration-period-days": "Password expiration period in days", - "password-expiration-period-days-range": "Password expiration period in days can't be negative", - "password-reuse-frequency-days": "Password reuse frequency in days", - "password-reuse-frequency-days-range": "Password reuse frequency in days can't be negative", - "allow-whitespace": "Allow whitespace", - "general-policy": "General policy", - "max-failed-login-attempts": "Maximum number of failed login attempts, before account is locked", - "minimum-max-failed-login-attempts-range": "Maximum number of failed login attempts can't be negative", - "user-lockout-notification-email": "In case user account lockout, send notification to email", - "domain-name": "Domain name", - "domain-name-unique": "Domain name and protocol need to unique.", - "domain-name-max-length": "Domain name should be less than 256", - "error-verification-url": "A domain name shouldn't contain symbols '/' and ':'. Example: thingsboard.io", - "connection-settings": "Connection settings", - "oauth2": { - "access-token-uri": "Access token URI", - "access-token-uri-required": "Access token URI is required.", - "activate-user": "Activate user", - "add-domain": "Add domain", - "delete-domain": "Delete domain", - "add-provider": "Add provider", - "delete-provider": "Delete provider", - "allow-user-creation": "Allow user creation", - "always-fullscreen": "Always fullscreen", - "authorization-uri": "Authorization URI", - "authorization-uri-required": "Authorization URI is required.", - "client-authentication-method": "Client authentication method", - "client-id": "Client ID", - "client-id-required": "Client ID is required.", - "client-id-max-length": "Client ID should be less than 256", - "client-secret": "Client secret", - "client-secret-required": "Client secret is required.", - "client-secret-max-length": "Client secret should be less than 2049", - "custom-setting": "Custom settings", - "customer-name-pattern": "Customer name pattern", - "customer-name-pattern-max-length": "Customer name pattern should be less than 256", - "parent-customer-name-pattern": "Parent customer name pattern", - "user-groups-name-pattern": "User groups name pattern", - "default-dashboard-name": "Default dashboard name", - "default-dashboard-name-max-length": "Default dashboard name should be less than 256", - "delete-domain-text": "Be careful, after the confirmation a domain and all provider data will be unavailable.", - "delete-domain-title": "Are you sure you want to delete settings the domain '{{domainName}}'?", - "delete-registration-text": "Be careful, after the confirmation a provider data will be unavailable.", - "delete-registration-title": "Are you sure you want to delete the provider '{{name}}'?", - "email-attribute-key": "Email attribute key", - "email-attribute-key-required": "Email attribute key is required.", - "email-attribute-key-max-length": "Email attribute key should be less than 32", - "first-name-attribute-key": "First name attribute key", - "first-name-attribute-key-max-length": "First name attribute key should be less than 32", - "general": "General", - "jwk-set-uri": "JSON Web Key URI", - "last-name-attribute-key": "Last name attribute key", - "last-name-attribute-key-max-length": "Last name attribute key should be less than 32", - "login-button-icon": "Login button icon", - "login-button-label": "Provider label", - "login-button-label-placeholder": "Login with $(Provider label)", - "login-button-label-required": "Label is required.", - "login-provider": "Login provider", - "mapper": "Mapper", - "new-domain": "New domain", - "oauth2": "OAuth2", - "password-max-length": "Password should be less than 256", - "redirect-uri-template": "Redirect URI template", - "copy-redirect-uri": "Copy redirect URI", - "registration-id": "Registration ID", - "registration-id-required": "Registration ID is required.", - "registration-id-unique": "Registration ID need to unique for the system.", - "scope": "Scope", - "scope-required": "Scope is required.", - "tenant-name-pattern": "Tenant name pattern", - "tenant-name-pattern-required": "Tenant name pattern is required.", - "tenant-name-pattern-max-length": "Tenant name pattern ishould be less than 256", - "tenant-name-strategy": "Tenant name strategy", - "type": "Mapper type", - "uri-pattern-error": "Invalid URI format.", - "url": "URL", - "url-pattern": "Invalid URL format.", - "url-required": "URL is required.", - "url-max-length": "URL should be less than 256", - "user-info-uri": "User info URI", - "user-info-uri-required": "User info URI is required.", - "username-max-length": "User name should be less than 256", - "user-name-attribute-name": "User name attribute key", - "user-name-attribute-name-required": "User name attribute key is required", - "protocol": "Protocol", - "domain-schema-http": "HTTP", - "domain-schema-https": "HTTPS", - "domain-schema-mixed": "HTTP+HTTPS", - "enable": "Enable OAuth2 settings", - "domains": "Domains", - "mobile-apps": "Mobile applications", - "no-mobile-apps": "No applications configured", - "mobile-package": "Application package", - "mobile-package-placeholder": "Ex.: my.example.app", - "mobile-package-hint": "For Android: your own unique Application ID. For iOS: Product bundle identifier.", - "mobile-package-unique": "Application package must be unique.", - "mobile-app-secret": "Application secret", - "invalid-mobile-app-secret": "Application secret must contain only alphanumeric characters and must be between 16 and 2048 characters long.", - "copy-mobile-app-secret": "Copy application secret", - "add-mobile-app": "Add application", - "delete-mobile-app": "Delete application info", - "providers": "Providers", - "platform-web": "Web", - "platform-android": "Android", - "platform-ios": "iOS", - "all-platforms": "All platforms", - "smtp-provider": "SMTP provider", - "allowed-platforms": "Allowed platforms", - "authentication": "Authentication", - "basic": "Basic", - "provider": "Provider", - "redirect-url": "Redirect URI", - "domain-name": "Domain name", - "redirect-url-template": "Redirect URI template", - "microsoft-tenant-id": "Directory (tenant) Id", - "microsoft-tenant-id-required": "Directory (tenant) Id is required", - "token-uri": "Token URI", - "token-uri-required": "Token URI is required", - "redirect-uri": "Redirect URI", - "google-provider": "Google", - "microsoft-provider": "Office 365", - "sendgrid-provider": "Sendgrid", - "custom-provider": "Custom", - "generate-access-token": "Generate access token", - "update-access-token": "Update access token", - "access-token-status": "Access token status:", - "token-status-generated": "generated", - "token-status-not-generated": "not generated" - }, - "smpp-provider": { - "smpp-version": "SMPP version", - "smpp-host": "SMPP host", - "smpp-host-required": "SMPP host is required", - "smpp-port": "SMPP port", - "smpp-port-required": "SMPP port is required", - "system-id": "System ID", - "system-id-required": "System ID is required", - "password": "Password", - "password-required": "Password is required", - "type-settings": "Type settings", - "source-settings": "Source settings", - "destination-settings": "Destination settings", - "additional-settings": "Additional settings", - "system-type": "System type", - "bind-type": "Bind type", - "service-type": "Service type", - "source-address": "Source address", - "source-ton": "Source TON", - "source-npi": "Source NPI", - "destination-ton": "Destination TON (Type of Number)", - "destination-npi": "Destination NPI (Numbering Plan Identification)", - "address-range": "Address range", - "coding-scheme": "Coding scheme", - "bind-type-tx": "Transmitter", - "bind-type-rx": "Receiver", - "bind-type-trx": "Transciever", - "ton-unknown": "Unknown", - "ton-international": "International", - "ton-national": "National", - "ton-network-specific": "Network Specific", - "ton-subscriber-number": "Subscriber Number", - "ton-alphanumeric": "Alphanumeric", - "ton-abbreviated": "Abbreviated", - "npi-unknown": "0 - Unknown", - "npi-isdn": "1 - ISDN/telephone numbering plan (E163/E164)", - "npi-data-numbering-plan": "3 - Data numbering plan (X.121)", - "npi-telex-numbering-plan": "4 - Telex numbering plan (F.69)", - "npi-land-mobile": "6 - Land Mobile (E.212)", - "npi-national-numbering-plan": "8 - National numbering plan", - "npi-private-numbering-plan": "9 - Private numbering plan", - "npi-ermes-numbering-plan": "10 - ERMES numbering plan (ETSI DE/PS 3 01-3)", - "npi-internet": "13 - Internet (IP)", - "npi-wap-client-id": "18 - WAP Client Id (to be defined by WAP Forum)", - "scheme-smsc": "0 - SMSC Default Alphabet (ASCII for short and long code and to GSM for toll-free)", - "scheme-ia5": "1 - IA5 (ASCII for short and long code, Latin 9 for toll-free (ISO-8859-9))", - "scheme-octet-unspecified-2": "2 - Octet Unspecified (8-bit binary)", - "scheme-latin-1": "3 - Latin 1 (ISO-8859-1)", - "scheme-octet-unspecified-4": "4 - Octet Unspecified (8-bit binary)", - "scheme-jis": "5 - JIS (X 0208-1990)", - "scheme-cyrillic": "6 - Cyrillic (ISO-8859-5)", - "scheme-latin-hebrew": "7 - Latin/Hebrew (ISO-8859-8)", - "scheme-ucs-utf": "8 - UCS2/UTF-16 (ISO/IEC-10646)", - "scheme-pictogram-encoding": "9 - Pictogram Encoding", - "scheme-music-codes": "10 - Music Codes (ISO-2022-JP)", - "scheme-extended-kanji-jis": "13 - Extended Kanji JIS (X 0212-1990)", - "scheme-korean-graphic-character-set": "14 - Korean Graphic Character Set (KS C 5601/KS X 1001)" - }, - "queue-select-name": "Select queue name", - "queue-name": "Name", - "queue-name-required": "Queue name is required!", - "queues": "Queues", - "queue-partitions": "Partitions", - "queue-submit-strategy": "Submit strategy", - "queue-processing-strategy": "Processing strategy", - "queue-configuration": "Queue configuration", - "repository-settings": "Repository settings", - "repository": "Repository", - "repository-url": "Repository URL", - "repository-url-required": "Repository URL is required.", - "default-branch": "Default branch name", - "repository-read-only": "Read-only", - "show-merge-commits": "Show merge commits", - "authentication-settings": "Authentication settings", - "auth-method": "Authentication method", - "auth-method-username-password": "Password / access token", - "auth-method-username-password-hint": "GitHub users must use access tokens with write permissions to the repository.", - "auth-method-private-key": "Private key", - "password-access-token": "Password / access token", - "change-password-access-token": "Change password / access token", - "private-key": "Private key", - "drop-private-key-file-or": "Drag and drop a private key file or", - "passphrase": "Passphrase", - "enter-passphrase": "Enter passphrase", - "change-passphrase": "Change passphrase", - "check-access": "Check access", - "check-repository-access-success": "Repository access successfully verified!", - "delete-repository-settings-title": "Are you sure you want to delete repository settings?", - "delete-repository-settings-text": "Be careful, after the confirmation the repository settings will be removed and version control feature will be unavailable.", - "auto-commit-settings": "Auto-commit settings", - "auto-commit": "Auto-commit", - "auto-commit-entities": "Auto-commit entities", - "no-auto-commit-entities-prompt": "No entities configured for auto-commit", - "delete-auto-commit-settings-title": "Are you sure you want to delete auto-commit settings?", - "delete-auto-commit-settings-text": "Be careful, after the confirmation the auto-commit settings will be removed and auto-commit will be disabled for all entities.", - "2fa": { - "2fa": "Two-factor authentication", - "available-providers": "Available providers", - "issuer-name": "Issuer name", - "issuer-name-required": "Issuer name is required.", - "max-verification-failures-before-user-lockout": "Max verification failures before user lockout", - "max-verification-failures-before-user-lockout-pattern": "Max verification failures must be a positive integer.", - "number-of-checking-attempts": "Number of checking attempts", - "number-of-checking-attempts-pattern": "Number of checking attempts must be a positive integer.", - "number-of-checking-attempts-required": "Number of checking attempts is required.", - "number-of-codes": "Number of codes", - "number-of-codes-pattern": "Number of codes must be a positive integer.", - "number-of-codes-required": "Number of codes is required.", - "provider": "Provider", - "retry-verification-code-period": "Retry verification code period (sec)", - "retry-verification-code-period-pattern": "Minimal period time is 5 sec", - "retry-verification-code-period-required": "Retry verification code period is required.", - "total-allowed-time-for-verification": "Total allowed time for verification (sec)", - "total-allowed-time-for-verification-pattern": "Minimal total allowed time is 60 sec", - "total-allowed-time-for-verification-required": "Total allowed time is required.", - "use-system-two-factor-auth-settings": "Use system two factor auth settings", - "verification-code-check-rate-limit": "Verification code check rate limit", - "verification-code-lifetime": "Verification code lifetime (sec)", - "verification-code-lifetime-pattern": "Verification code lifetime must be a positive integer.", - "verification-code-lifetime-required": "Verification code lifetime is required.", - "verification-message-template": "Verification message template", - "verification-limitations": "Verification limitations", - "verification-message-template-pattern": "Verification message need to contains pattern: ${code}", - "verification-message-template-required": "Verification message template is required.", - "within-time": "Within time (sec)", - "within-time-pattern": "Time must be a positive integer.", - "within-time-required": "Time is required." - }, - "jwt": { - "security-settings": "JWT security settings", - "issuer-name": "Issuer name", - "issuer-name-required": "Issuer name is required.", - "signings-key": "Signing key", - "signings-key-hint": "Base64 encoded string representing at least 256 bits of data.", - "signings-key-required": "Signing key is required.", - "signings-key-min-length": "Signing key must be at least 256 bits of data.", - "signings-key-base64": "Signing key must be base64 format.", - "expiration-time": "Token expiration time (sec)", - "expiration-time-required": "Token expiration time is required.", - "expiration-time-min": "Minimum time is 60 seconds (1 minute).", - "refresh-expiration-time": "Refresh token expiration time (sec)", - "refresh-expiration-time-required": "Refresh token expiration time is required.", - "refresh-expiration-time-min": "Minimum time is 900 seconds (15 minute).", - "refresh-expiration-time-less-token": "Refresh token time must be greater token time.", - "generate-key": "Generate key", - "info-header": "All users will be to re-logined", - "info-message": "Change of the JWT Signing Key will cause all issued tokens to be invalid. All users will need to re-login. This will also affect scripts that use Rest API/Websockets." - }, - "resources": "Resources", - "notifications": "Notifications", - "notifications-settings": "Notifications settings", - "slack-api-token": "Slack API token", - "slack": "Slack", - "slack-settings": "Slack settings" - }, - "alarm": { - "alarm": "Įspėjimas", - "alarms": "Įspėjimai", - "all-alarms": "Visi Įspėjimai", - "select-alarm": "Pasirinkti įspėjimą", - "no-alarms-matching": "Su '{{entity}}' susijusių įspėjimų nėra.", - "alarm-required": "Įspėjimas yra privalomas", - "alarm-filter": "Įspėjimų filtras", - "filter": "Filtras", - "alarm-status": "Įspėjimo statusas", - "alarm-status-list": "Įspėjimo statusų sąrašas", - "any-status": "Visi statusai", - "search-status": { - "ANY": "Visi", - "ACTIVE": "Aktyvūs", - "CLEARED": "Atmesti", - "ACK": "Pripažinti", - "UNACK": "Nepatvirtinti" - }, - "display-status": { - "ACTIVE_UNACK": "Aktyvūs nepatvirtinti", - "ACTIVE_ACK": "Aktyvūs pripažinti", - "CLEARED_UNACK": "Atmesti nepatvirtinti", - "CLEARED_ACK": "Atmesti pripažinti" - }, - "no-alarms-prompt": "Įspėjimų nėra", - "created-time": "Sukūrimo laikas", - "type": "Tipas", - "severity": "Lygis", - "originator": "Iniciatorius", - "originator-type": "Iniciatoriaus tipas", - "details": "Pastabos", - "originator-label": "Iniciatoriaus pavadinimas", - "assign": "Priskirti", - "assignments": "Priskyrimai", - "assignee": "Atsakingas", - "assignee-id": "Atsakingo asmens ID", - "assignee-first-name": "Atsakingo asmens vardas", - "assignee-last-name": "Atsakingo asmens pavardė", - "assignee-email": "Atsakingo asmens e-paštas", - "unassigned": "Panaikinti priskyrimą", - "assignee-not-set": "Visi", - "status": "Statusas", - "alarm-details": "Įspėjimo pastabos", - "start-time": "Pradžia", - "assign-time": "Priskyrimo laikas", - "end-time": "Pabaiga", - "ack-time": "Pripažinimo laikas", - "clear-time": "Atmetimo laikas", - "duration": "Trukmė", - "alarm-severity-list": "Įspėjimų lygiai", - "any-severity": "Visi lygiai", - "severity-critical": "Kritinis", - "severity-major": "Svarbus", - "severity-minor": "Antraeilis", - "severity-warning": "Įspėjimas", - "severity-indeterminate": "Nenustatytas", - "acknowledge": "Patvirtinti", - "clear": "Atmesti", - "delete": "Panaikinti", - "search": "Įspėjimo paieška", - "selected-alarms": "Pasirinkta { count, plural, =1 {1 įspėjimas} other {# įspėjimai} }", - "no-data": "Nėra duomenų", - "polling-interval": "Įspėjimų tikrinimo intervalas (sek)", - "polling-interval-required": "Reikalingas įspėjimų tikrinimo intervalas", - "min-polling-interval-message": "Mažiausias įspėjimų tikrinimo intervalas yra 1 sekundė.", - "aknowledge-alarms-title": "Pripažinti { count, plural, =1 {1 įspėjimą} other {# įspėjimus} }", - "aknowledge-alarms-text": "Pripažinti { count, plural, =1 {1 įspėjimą} other {# įspėjimus} }?", - "aknowledge-alarm-title": "Pripažinti įspėjimą", - "aknowledge-alarm-text": "Pripažinti įspėjimą?", - "selected-alarms-are-acknowledged": "Pasirinkti įspėjimai pripažinti", - "clear-alarms-title": "Atmesti { count, plural, =1 {1 įspėjimą} other {# įspėjimus} }", - "clear-alarms-text": "Atmesti { count, plural, =1 {1 įspėjimą} other {# įspėjimus} }?", - "clear-alarm-title": "Atmesti įspėjimą", - "clear-alarm-text": "Atmesti įspėjimą?", - "delete-alarms-title": "Panaikinti { count, plural, =1 {1 įspėjimą} other {# įspėjimus} }", - "delete-alarms-text": "Panaikinti { count, plural, =1 {1 įspėjimą} other {# įspėjimus} }?", - "selected-alarms-are-cleared": "Pasirinkti įspėjimai atmesti", - "alarm-status-filter": "Įspėjimų statusai", - "alarm-filter-title": "Įspėjimų filtras", - "assigned": "Priskirta", - "filter-title": "Filtras", - "max-count-load": "Didžiausias rodomų įspėjimų skaičius (0 – neribotas)", - "max-count-load-required": "Reikalingas didžiausias rodomų įspėjimų skaičius (0 – neribotas).", - "max-count-load-error-min": "Mažiausia reikšmė yra 0.", - "fetch-size": "Fetch size", - "fetch-size-required": "Fetch size is required.", - "fetch-size-error-min": "Mažiausia rekšmė yra 10.", - "alarm-type-list": "Įspėjimų tipai", - "any-type": "Visi tipai", - "assigned-to-current-user": "Priskirta prisijungusiam vartotojui", - "assigned-to-me": "Priskirta man", - "search-propagated-alarms": "Ieškoti išplatintų įspėjimų", - "comments": "Įspėjimo pastabos", - "show-more": "Rodyti daugiau", - "additional-info": "Papildoma informacija", - "alarm-type": "Įspėjimo tipas", - "enter-alarm-type": "Įveskite įspėjimo tipą", - "no-alarm-types-matching": "Nėra įspėjimų tipų, kurie atitiktų '{{entitySubtype}}'.", - "alarm-type-list-empty": "Nepasirinktas įspėjimų tipas" - }, - "alarm-activity": { - "add": "Pridėti komentarą...", - "alarm-comment": "Įspėjimo komentaras", - "comments": "Komentarai", - "delete-alarm-comment": "Ištrinti komentarą?", - "refresh": "Atnaujinti", - "oldest-first": "Seniausi viršuje", - "newest-first": "Naujausi viršuje", - "activity": "Veikla", - "export": "Eksportuoti į CSV", - "author": "Sukūrė", - "created-date": "Sukūrimo dara", - "edited-date": "Redagavimo data", - "text": "Tekstas", - "system": "Sistema" - }, - "alias": { - "add": "Pridėti pseudonimą", - "edit": "Redaguoti pseudonimą", - "name": "Pseudonimas", - "name-required": "Reikalingas pseudonimas", - "duplicate-alias": "Toks pseudonimas sistemoje jau yra.", - "filter-type-single-entity": "Vienas subjektas", - "filter-type-entity-group": "Subjektų grupė", - "filter-type-entity-list": "Subjektų sąrašas", - "filter-type-entity-name": "Subjekto pavadinimas", - "filter-type-entity-type": "Subjekto tipas", - "filter-type-entity-group-list": "Subjektų grupių sąrašas", - "filter-type-entity-group-name": "Subjektų grupės pavadinimas", - "filter-type-entities-by-group-name": "Subjektai pagal grupės pavadinimą", - "filter-type-state-entity": "Subjektas iš skydelio būsenos", - "filter-type-state-entity-description": "Subjektas iš skydelio būsenos parametrų", - "filter-type-state-entity-owner": "Skydelio būsenos subjekto savininkas", - "filter-type-state-entity-owner-description": "Subjekto, iš skydelio būsenos parametrų, savininkas", - "filter-type-asset-type": "Turto tipas", - "filter-type-asset-type-description": "Turtas, kurio tipas '{{assetTypes}}'", - "filter-type-asset-type-and-name-description": "Turtas, kurio tipas '{{assetTypes}}' ir kurio pavadinimas prasideda '{{prefix}}'", - "filter-type-device-type": "Įrenginio tipas", - "filter-type-device-type-description": "Įrenginiai, kurių tipas '{{deviceTypes}}'", - "filter-type-device-type-and-name-description": "Įrenginiai, kurių tipas '{{deviceTypes}}' ir kurių pavadinimai '{{prefix}}'", - "filter-type-entity-view-type": "Subjekto rodinio tipas", - "filter-type-entity-view-type-description": "Subjektų rodiniai, kurių tipas '{{entityViewTypes}}'", - "filter-type-entity-view-type-and-name-description": "Subjektų rodiniai, kurių tipas '{{entityViewTypes}}' ir kurių pavadinimai prasideda '{{prefix}}'", - "filter-type-edge-type": "Edge type", - "filter-type-edge-type-description": "Edges of type '{{edgeTypes}}'", - "filter-type-edge-type-and-name-description": "Edges of type '{{edgeTypes}}' and with name starting with '{{prefix}}'", - "filter-type-relations-query": "Ryšių užklausa", - "filter-type-relations-query-description": "{{entities}} kurie turi {{relationType}} ryšį {{direction}} {{rootEntity}}", - "filter-type-edge-search-query": "Edge search query", - "filter-type-edge-search-query-description": "Edges with types {{edgeTypes}} that have {{relationType}} relation {{direction}} {{rootEntity}}", - "filter-type-scheduler-event": "Scheduler events", - "filter-type-scheduler-event-type-description": "Scheduler events with type '{{eventType}}'", - "filter-type-scheduler-event-originator-description": "Scheduler events of specific originator", - "filter-type-scheduler-event-type-originator-description": "Scheduler events of specific originator with '{{eventType}}'", - "filter-type-asset-search-query": "Turto paieškos užklausa", - "filter-type-asset-search-query-description": "{{assetTypes}} tipo turtas, turintis {{relationType}} tipo ryšį {{direction}} {{rootEntity}}", - "filter-type-device-search-query": "Įrenginio paieškos užklausa", - "filter-type-device-search-query-description": "{{deviceTypes}} tipo įrenginiai, turintys {{relationType}} tipo ryšį {{direction}} {{rootEntity}}", - "filter-type-entity-view-search-query": "Subjektų rodinio paieškos užklausa", - "filter-type-entity-view-search-query-description": "{{entityViewTypes}} tipo subjektai, turintys {{relationType}} tipo ryšį {{direction}} {{rootEntity}}", - "filter-type-apiUsageState": "Api Usage State", - "entity-filter": "Subjektų filtras", - "resolve-multiple": "Traktuoti kaip kelis subjektus", - "filter-type": "Filtro tipas", - "filter-type-required": "Filtro tipas būtinas.", - "entity-filter-no-entity-matched": "Subjektų, atitinkančių filtro kriterijus, nerasta.", - "no-entity-filter-specified": "Subjektų filtras nenustatytas", - "root-state-entity": "Subjektą, iš skydelio būsenos, naudoti kaip pagrindinį", - "group-state-entity": "Skydelio būsenos subjektą naudoti kaip subjektų grupę", - "group-state-entity-owner": "Skydelio būsenos subjektą naudoti kaip subjektų grupės sąvininką", - "last-level-relation": "Gauti tik paskutinio lygio ryšį", - "root-entity": "Pagridinis subjektas", - "state-entity-parameter-name": "Būsenos subjekto parametro pavadinimas", - "default-state-entity": "Numatytasis būsenos subjektas", - "default-state-entity-group": "Numatytoji būsenos subjektų grupė", - "default-entity-parameter-name": "Pagal nutylėjimą", - "max-relation-level": "Maksimalus ryšių lygis", - "unlimited-level": "Neribojamas lygis", - "state-entity": "Skydelio būsenos subjektas", - "entities-of-group-state-entity": "Subjektai iš skydelio būsenos subjektų grupės", - "all-entities": "Visi subjektai", - "any-relation": "visi", - "originator": "Iniciatorius", - "originator-state-entity": "Skydelio būsenos subjektą naudoti kaip iniciatorių", - "scheduler-event-type": "Scheduler event type" - }, - "asset": { - "all": "Visas", - "all-assets": "Visas turtas", - "groups": "Grupės", - "shared": "Bendrinamas", - "asset": "Turtas", - "assets": "Turtas", - "management": "Turto valdymas", - "view-assets": "Peržiūrėti turtą", - "add": "Pridėti turtą", - "asset-type-max-length": "Turto tipas turi būti trumpesnis nei 256 simboliai", - "assign-to-customer": "Priskirti klientui", - "assign-asset-to-customer": "Turtą priskirti klientui", - "assign-asset-to-customer-text": "Pasirinkite turtą, kurį norite priskiri klientui", - "no-assets-text": "Turto nerasta", - "assign-to-customer-text": "Pasirinkite klientą, kuriam priskiriamas turtas", - "public": "Viešas", - "assignedToCustomer": "Priskirti klientui", - "make-public": "Neriboti turto matomumo", - "make-private": "Riboti turto matomumą", - "unassign-from-customer": "Atsieti nuo kliento", - "delete": "Panaikinti turtą", - "asset-public": "Turtas yra viešas", - "asset-type": "Turto tipas", - "asset-type-required": "Turto tipas būtinas.", - "select-asset-type": "Pasirinkite turto tipą", - "enter-asset-type": "Įveskite turto tipą", - "any-asset": "Visas turtas", - "no-asset-types-matching": "Turto tipas, atitinkantis '{{entitySubtype}}', nerastas.", - "asset-type-list-empty": "Nepasirinkti turto tipai.", - "asset-types": "Turto tipai", - "name": "Pavadinimas", - "name-required": "Pavadinimas yra privalomas.", - "name-max-length": "Pavadinimas turi būti trumpesnis nei 256 simboliai", - "label-max-length": "Vardas turi būti trumpesnis nei 256 simboliai", - "description": "Aprašymas", - "type": "Tipas", - "type-required": "Tipas yra prvalomas.", - "details": "Pastabos", - "events": "Įvykiai", - "add-asset-text": "Pridėti turtą", - "asset-details": "Pastabos apie turtą", - "assign-assets": "Priskirti turtą", - "assign-assets-text": "Priskirti { count, plural, =1 {1 turtą} other {# turtus} } to customer", - "assign-asset-to-edge-title": "Assign Asset(s) To Edge", - "assign-asset-to-edge-text": "Please select the assets to assign to the edge", - "delete-assets": "Pašalinti turtą", - "unassign-assets": "Atsieti turtą", - "unassign-assets-action-title": "Atsieti { count, plural, =1 {1 turtą} other {# turtus} } nuo kliento?", - "assign-new-asset": "Prisirti naują turtą", - "delete-asset-title": "Pašalinti '{{assetName}}' turtą?", - "delete-asset-text": "Būkite dėmesingi, po patvirtinimo, turtas ir su juo susijusi informacija, bus pašalinta.", - "delete-assets-title": "Pašalinti { count, plural, =1 {1 turtą} other {# turtus} }?", - "delete-assets-action-title": "Pašalinti { count, plural, =1 {1 turtą} other {# turtus} }", - "delete-assets-text": "Būkite dėmesingi, po patvirtinimo, visi turto įrašai ir su jais susijusi informacija, bus pašalinta.", - "make-public-asset-title": "Ar tikrai norite neriboti turto '{{assetName}}' matomumo?", - "make-public-asset-text": "Po patvirtinimo turtas ir visa su juo susijusi informacija bus prieinama visiems.", - "make-private-asset-title": "Ar tikrai norite riboti turto '{{assetName}}' matomumą?", - "make-private-asset-text": "Po patvirtinimo, turtas ir visa su juo susijusi informacija nebus prieinama kitiems.", - "unassign-asset-title": "Ar tikrai norite atsieti turtą '{{assetName}}'?", - "unassign-asset-text": "Po patvirtinimo turtas bus atsietas ir klientas neteks prieigos prie jo.", - "unassign-asset": "Atsieti turtą", - "unassign-assets-title": "Ar tikrai norite atsieti { count, plural, =1 {1 turtą} other {# turtus} }?", - "unassign-assets-text": "Po patvirtinimo visas turtas bus atsietas ir klientas neteks prieigos prie jo.", - "copyId": "Kopijuoti turto Id", - "idCopiedMessage": "Turto Id nukopijuotas į iškarpinę", - "select-asset": "Pasirinkti turtą", - "no-assets-matching": "Turtas atitinkantis '{{entity}}' nerastas.", - "asset-required": "Turtas būtinas", - "name-starts-with": "Turto pavadinimas prasideda", - "help-text": "naudokite simbolį '%' pagal tai, kaip norite ieškoti: '%turto_pavadinimo_fragmentas%', '%turto_pavadinimo_pabaiga', 'turto_pavadinimo_pradžia%'.", - "search": "Turto paieška", - "select-group-to-add": "Pasirinkti grupę, į kurią turtą pridėti", - "select-group-to-move": "Pasirinkite grupę, į kurią turtą perkelti", - "remove-assets-from-group": "Ar tikrai norite { count, plural, =1 {1 turtą} other {# turtus} } pašalinti iš grupės '{{entityGroup}}'?", - "group": "Turto grupė", - "list-of-groups": "{ count, plural, =1 {Viena turto grupė} other { # Turto grupių sąrašas} }", - "group-name-starts-with": "Turto grupės, kurių pavadinimai prasideda '{{prefix}}'", - "import": "Importuoti turtą", - "asset-file": "Turto failas", - "label": "Etiketė", - "assign-asset-to-edge": "Assign Asset(s) To Edge", - "unassign-asset-from-edge": "Unassign asset", - "unassign-asset-from-edge-title": "Are you sure you want to unassign the asset '{{assetName}}'?", - "unassign-asset-from-edge-text": "After the confirmation the asset will be unassigned and won't be accessible by the edge.", - "unassign-assets-from-edge-title": "Are you sure you want to unassign { count, plural, =1 {1 asset} other {# assets} }?", - "unassign-assets-from-edge-text": "After the confirmation all selected assets will be unassigned and won't be accessible by the edge.", - "selected-assets": "Pasirinkta { count, plural, =1 {1 turtas} other {# turtai} }" - }, - "attribute": { - "attributes": "Atributai", - "latest-telemetry": "Paskutinė telemetrija", - "no-latest-telemetry": "Telemetrijos nėra", - "attributes-scope": "Subjekto atributų kontekstas", - "scope-telemetry": "Telemetrija", - "scope-latest-telemetry": "Paskutinė telemetrija", - "scope-client": "Kliento atributai", - "scope-server": "Serverio atributai", - "scope-shared": "Bendrinami atributai", - "add": "Pridėti atributą", - "add-attribute-prompt": "Pridėkite atributą", - "key": "Raktas", - "key-max-length": "Raktas negali viršyti 256 simbolių", - "last-update-time": "Atnaujinimo laikas", - "key-required": "Atributo raktas būtinas.", - "value": "Reikšmė", - "value-required": "Atributo reikšmė būtina.", - "telemetry-key-required": "Telemetrijos raktas būtinas", - "telemetry-value-required": "Telemetrijos reikšmė būtina", - "delete-attributes-title": "Ar tikrai norite pašalinti { count, plural, =1 {1 atributą} other {# atributus} }?", - "delete-attributes-text": "Būkite dėmesingi, po patvirtinimo pasirinkti atributai bus pašalinti.", - "delete-attributes": "Pašalinti atributus", - "enter-attribute-value": "Įveskite atributo reikšmę", - "show-on-widget": "Rodyti valdiklyje", - "widget-mode": "Valdiklio režimas", - "next-widget": "Sekantis valdiklis", - "prev-widget": "Ankstesnis valdiklis", - "add-to-dashboard": "Pridėti į skydelį", - "add-widget-to-dashboard": "Pridėti valdiklį į skydelį", - "selected-attributes": "Pasirinkta { count, plural, =1 {1 atributas} other {# atributai} }", - "selected-telemetry": "Pasirinkta { count, plural, =1 {1 telemetrijos blokas} other {# telemetrijos blokai} }", - "no-attributes-text": "Atributų nėra", - "no-telemetry-text": "Telemetrijos nėra", - "copy-key": "Kopijuoti raktą", - "add-telemetry": "Pridėti telemetriją", - "copy-value": "Kopijuoti reikšmę", - "delete-timeseries": { - "start-time": "Pradžia", - "ends-on": "Pabaiga", - "strategy": "Strategija", - "delete-strategy": "Šalinimo startegija", - "all-data": "Pašalinti visus duomenis", - "all-data-except-latest-value": "Pašalinti visus duomenis išskyrus naujausią reikšmę", - "latest-value": "Pašalinti naujausią reikšmę", - "all-data-for-time-period": "Pašalinti visus duomenis periodui", - "rewrite-latest-value": "Perrašyti naujausią reikšmę" - } +{ + "access": { + "unauthorized": "Neautorizuotas", + "unauthorized-access": "Neautorizuota prieiga", + "unauthorized-access-text": "Prieiga prie šio funkcionalumo leidžiama tik prisijungusiam vartotojui!", + "access-forbidden": "Prieiga uždrausta", + "access-forbidden-text": "Neturite teisių prieigai!
    Bandykite prisijungti kitu vartotoju.", + "refresh-token-expired": "Sesijos galiojimas baigėsi", + "refresh-token-failed": "Sesijos atnaujinti nepavyksta", + "permission-denied": "Leidimas nesuteiktas", + "permission-denied-text": "Šio veiksmo atlikti negalite!" + }, + "account": { + "account": "Paskyra", + "notification-settings": "Pranešimų nustatymai" + }, + "action": { + "activate": "Aktyvuoti", + "suspend": "Sustabdyti", + "save": "Išsaugoti", + "saveAs": "Išsaugoti kaip", + "move": "Perkelti", + "cancel": "Atmesti", + "ok": "OK", + "delete": "Panaikinti", + "add": "Pridėti", + "yes": "Taip", + "no": "Ne", + "update": "Atnaujinti", + "remove": "Panaikinti", + "search": "Paieška", + "clear-search": "Išvalyti", + "assign": "Priskirti", + "unassign": "Atsieti", + "share": "Pasidalinti", + "make-private": "Riboti matomumą", + "apply": "Taikyti", + "apply-changes": "Pritaikyti pakeitimus", + "edit-mode": "Redagavimo režimas", + "enter-edit-mode": "Redagavimo režimas", + "decline-changes": "Atšaukti pakeitimus", + "decline": "Atšaukti", + "close": "Uždaryti", + "back": "Atgal", + "run": "Paleisti", + "sign-in": "Prisijunkite!", + "edit": "Redaguoti", + "view": "Peržiūrėti", + "create": "Kurti", + "drag": "Vilkti", + "refresh": "Atnaujinti", + "undo": "Atšaukti", + "copy": "Kopijuoti", + "paste": "Įklijuoti", + "copy-reference": "Kopijuoti nuorodą", + "paste-reference": "Įklijuoti nuorodą", + "import": "Importuoti", + "export": "Eksportuoti", + "share-via": "Pasidalinti per {{provider}}", + "select": "Pasirinkti", + "continue": "Tęsti", + "discard-changes": "Atsisakyti pakeitimų", + "download": "Parsisiųsti", + "next": "Sekantis", + "next-with-label": "Sekantis: {{label}}", + "read-more": "Skaityti daugiau", + "hide": "Paslėpti", + "test": "Test", + "done": "Atlikta", + "print": "Spausdinti", + "restore": "Atkurti", + "confirm": "Patvirtinti", + "more": "Daugiau", + "less": "Mažiau", + "skip": "Praleisti", + "send": "Siųsti", + "reset": "Nustatyti iš naujo", + "show-more": "Rodyti daugiau", + "dont-show-again": "Daugiau neberodyti", + "see-documentation": "Žiūrėti dokumentacijoje", + "clear": "Išvalyti", + "upload": "Įkelti", + "delete-anyway": "Trinti vistike", + "delete-selected": "Trinti pasirinktą", + "set": "Nustatyti" + }, + "aggregation": { + "aggregation": "Agregavimas", + "function": "Duomenų aggregavimo funkcija", + "limit": "Maksimali reikšmė", + "group-interval": "Grupavimo intervalas", + "min": "Min", + "max": "Max", + "avg": "Vidurkis", + "sum": "Sum", + "count": "Kiekis", + "none": "Nėra" + }, + "admin": { + "settings": "Nustatymai", + "general": "Bendrieji", + "general-settings": "Bendrieji nustatymai", + "home-settings": "Pagrindinio puslapio nustatymai", + "home": "Pagrindinis puslapis", + "outgoing-mail": "Pašto serveris", + "outgoing-mail-settings": "Išeinančio pašto serverio nustatymai", + "system-settings": "Sistemos nustatymai", + "test-mail-sent": "Bandomasis laiškas sėkmingai išsiųstas!", + "base-url": "Pagrindinis URL", + "base-url-required": "Pagrindinis URL yra privalomas.", + "prohibit-different-url": "Drausti naudoti kliento užklausų antraštėse nurodytą pagrindinio kompiuterio pavadinimą", + "prohibit-different-url-hint": "Šis nustatymas turėtų būti įjungtas gamybinėje aplinkoje. Jo išjungimas gali sukelti saugumo problemų", + "device-connectivity": { + "device-connectivity": "Įrenginių jungiamumas", + "http-s": "HTTP(s)", + "mqtt-s": "MQTT(s)", + "coap-s": "COAP(s)", + "http": "HTTP", + "https": "HTTPs", + "mqtt": "MQTT", + "mqtts": "MQTTs", + "coap": "COAP", + "coaps": "COAPs", + "hint": "Jei laukeliai 'host' arba 'port' yra tušti, bus naudojamos numatytosios protokolo reikšmės.", + "host": "Host", + "port": "Port", + "port-pattern": "Port turi būti teigiamas sveikasis skaičius.", + "port-range": "Port turi būti nuo 1 iki 65535." }, - "api-usage": { - "api-features": "API features", - "api-usage": "Api Usage", - "alarm": "Alarm", - "alarms-created": "Alarms created", - "alarms-created-daily-activity": "Alarms created daily activity", - "alarms-created-hourly-activity": "Alarms created hourly activity", - "alarms-created-monthly-activity": "Alarms created monthly activity", - "data-points": "Data points", - "data-points-storage-days": "Data points storage days", - "device-api": "Device API", - "email": "Email", - "email-messages": "Email messages", - "email-messages-daily-activity": "Email messages daily activity", - "email-messages-monthly-activity": "Email messages monthly activity", - "exceptions": "Exceptions", - "executions": "Executions", - "javascript": "JavaScript", - "javascript-executions": "JavaScript executions", - "javascript-functions": "JavaScript functions", - "javascript-functions-daily-activity": "JavaScript functions daily activity", - "javascript-functions-hourly-activity": "JavaScript functions hourly activity", - "javascript-functions-monthly-activity": "JavaScript functions monthly activity", - "latest-error": "Latest Error", - "messages": "Messages", - "notifications": "Notifications", - "notifications-email-sms": "Notifications (Email/SMS)", - "notifications-hourly-activity": "Notifications hourly activity", - "permanent-failures": "${entityName} Permanent Failures", - "permanent-timeouts": "${entityName} Permanent Timeouts", - "processing-failures": "${entityName} Processing Failures", - "processing-failures-and-timeouts": "Processing Failures and Timeouts", - "processing-timeouts": "${entityName} Processing Timeouts", - "queue-stats": "Queue Stats", - "rule-chain": "Rule Chain", - "rule-engine": "Rule Engine", - "rule-engine-daily-activity": "Rule Engine daily activity", - "rule-engine-executions": "Rule Engine executions", - "rule-engine-hourly-activity": "Rule Engine hourly activity", - "rule-engine-monthly-activity": "Rule Engine monthly activity", - "rule-engine-statistics": "Rule Engine Statistics", - "rule-node": "Rule Node", - "sms": "SMS", - "sms-messages": "SMS messages", - "sms-messages-daily-activity": "SMS messages daily activity", - "sms-messages-monthly-activity": "SMS messages monthly activity", - "successful": "${entityName} Successful", - "telemetry": "Telemetry", - "telemetry-persistence": "Telemetry persistence", - "telemetry-persistence-daily-activity": "Telemetry persistence daily activity", - "telemetry-persistence-hourly-activity": "Telemetry persistence hourly activity", - "telemetry-persistence-monthly-activity": "Telemetry persistence monthly activity", - "transport": "Transport", - "transport-daily-activity": "Transport daily activity", - "transport-data-points": "Transport data points", - "transport-hourly-activity": "Transport hourly activity", - "transport-messages": "Transport messages", - "transport-monthly-activity": "Transport monthly activity", - "view-details": "View details", - "view-statistics": "View statistics" - }, - "api-limit": { - "cassandra-queries": "Cassandra queries", - "entity-version-creation": "Entity version creation", - "entity-version-load": "Entity version load", - "integration-messages": "Integration messages", - "integration-messages-per-device": "Integration messages per device", - "notification-requests": "Notification requests", - "notification-requests-per-rule": "Notification requests per rule", - "reports-generation": "Reports generation", - "rest-api-requests": "REST API requests", - "rest-api-requests-per-customer": "REST API requests per customer", - "transport-messages": "Transport messages", - "transport-messages-per-device": "Transport messages per device", - "ws-updates-per-session": "WS updates per session" - }, - "audit-log": { - "audit": "Auditas", - "audit-logs": "Audito žurnalas", - "timestamp": "Laiko žyma", - "entity-type": "Subjektas", - "entity-name": "Pavadinimas", - "user": "Vartotojas", - "type": "Veiksmas", - "status": "Statusas", - "details": "Informacija", - "type-added": "Pridėta", - "type-deleted": "Pašalinta", - "type-updated": "Atnaujinta", - "type-attributes-updated": "Attributai atnaujinti", - "type-attributes-deleted": "Atributai pašalinti", - "type-rpc-call": "RPC iškvietimas", - "type-credentials-updated": "Įgaliojimai atnaujinti", - "type-assigned-to-customer": "Priskirta klientui", - "type-unassigned-from-customer": "Atsieta nuo kliento", - "type-assigned-to-edge": "Assigned to Edge", - "type-unassigned-from-edge": "Unassigned from Edge", - "type-activated": "Aktyvuota", - "type-suspended": "Sustabdyta", - "type-credentials-read": "Įgaliojimų skaitymas", - "type-attributes-read": "Atributų skaitymas", - "type-added-to-entity-group": "Įtraukta į grupę", - "type-removed-from-entity-group": "Pašalinta iš grupės", - "type-relation-add-or-update": "ryšys atnaujintas", - "type-relation-delete": "Ryšys panaikintas", - "type-relations-delete": "Visi ryšiai panaikinti", - "type-alarm-ack": "Pripažinta", - "type-alarm-clear": "Išvalyta", - "type-alarm-assign": "Priskitas", - "type-alarm-unassign": "Atsietas", - "type-added-comment": "Pridėtas komentaras", - "type-updated-comment": "Atnaujintas komentaras", - "type-deleted-comment": "Pašalintas komentaras", - "type-rest-api-rule-engine-call": "Rule engine REST API call", - "type-made-public": "Padaryti viešu", - "type-made-private": "Padaryti privačiu", - "type-login": "Prisijungti", - "type-logout": "Atsijungti", - "type-lockout": "Užblokuoti", - "status-success": "Sėkmė", - "status-failure": "Nesėkmė", - "audit-log-details": "Informacija apie autido žurnalą", - "no-audit-logs-prompt": "Žurnalo įrašų nėra", - "action-data": "Informacija apie veiksmus", - "failure-details": "Informacija apie nesėkmę", - "search": "Žurnalo įrašų paieška", - "clear-search": "Išvalyti paiešką", - "type-assigned-from-tenant": "Priskirta valdytojo", - "type-assigned-to-tenant": "Priskirta valdytojui", - "type-provision-success": "Device provisioned", - "type-provision-failure": "Device provisioning was failed", - "type-timeseries-updated": "Telemetrija atnaujinta", - "type-timeseries-deleted": "Telemetrija pašalinta", - "type-owner-changed": "Savininkas pakeistas", - "type-sms-sent": "SMS sent" - }, - "confirm-on-exit": { - "message": "Turite neišsaugotų pakeitimų. Ar tikrai norite palikti šį puslapį?", - "html-message": "Turite neišsaugotų pakeitimų.
    Ar tikrai norite palikti šį puslapį?", - "title": "Neišsaugoti pakeitimai" - }, - "contact": { - "country": "Šalis", - "city": "Miestas", - "state": "Rajonas", - "postal-code": "Pašto kodas", - "postal-code-invalid": "Neteisingas pašto kodo formatas.", - "address": "Adresas", - "address2": "Adresas 2", - "phone": "Telefonas", + "mail-from": "Laiškas nuo", + "mail-from-required": "Laukas „Laiškas nuo“ yra privalomas.", + "smtp-protocol": "SMTP protokolas", + "smtp-host": "SMTP serveris", + "smtp-host-required": "SMTP serveris yra privalomas.", + "smtp-port": "SMTP prievadas", + "smtp-port-required": "Turite nurodyti SMTP prievadą.", + "smtp-port-invalid": "Tai neatrodo kaip tinkamas SMTP prievadas.", + "timeout-msec": "Laiko limitas (msek)", + "timeout-required": "Laiko limitas yra privalomas.", + "timeout-invalid": "Tai neatrodo kaip tinkamas laiko limitas.", + "enable-tls": "Įjungti TLS", + "tls-version": "TLS versija", + "enable-proxy": "Įjungti proxy", + "proxy-host": "Proxy serveris", + "proxy-host-required": "Proxy serveris yra privalomas.", + "proxy-port": "Proxy prievadas", + "proxy-port-required": "Proxy prievadas yra privalomas.", + "proxy-port-range": "Proxy prievadas turi būti nuo 1 iki 65535.", + "proxy-user": "Proxy naudotojas", + "proxy-password": "Proxy slaptažodis", + "change-password": "Pakeisti slaptažodį", + "send-test-mail": "Siųsti bandomąjį laišką", + "sms-provider": "SMS tiekėjas", + "sms-provider-settings": "SMS tiekėjo nustatymai", + "sms-provider-type": "SMS tiekėjo tipas", + "sms-provider-type-required": "SMS tiekėjo tipas yra privalomas.", + "sms-provider-type-aws-sns": "Amazon SNS", + "sms-provider-type-twilio": "Twilio", + "sms-provider-type-smpp": "SMPP", + "aws-access-key-id": "AWS prieigos raktas (Access Key ID)", + "aws-access-key-id-required": "AWS prieigos raktas (Access Key ID) yra privalomas", + "aws-secret-access-key": "AWS slaptas prieigos raktas (Secret Access Key)", + "aws-secret-access-key-required": "AWS slaptas prieigos raktas (Secret Access Key) yra privalomas", + "aws-region": "AWS regionas", + "aws-region-required": "AWS regionas yra privalomas", + "number-from": "Telefono numeris nuo", + "number-from-required": "Telefono numeris nuo yra privalomas.", + "number-to": "Telefono numeris kam", + "number-to-required": "Telefono numeris kam yra privalomas.", + "phone-number-hint": "Telefono numeris E.164 formatu, pvz. +19995550123", + "phone-number-hint-twilio": "Telefono numeris E.164 formatu / Telefono numerio SID / Žinučių paslaugos SID, pvz. +19995550123/PNXXX/MGXXX", + "phone-number-pattern": "Neteisingas telefono numeris. Turi būti E.164 formatu, pvz. +19995550123.", + "phone-number-pattern-twilio": "Neteisingas telefono numeris. Turi būti E.164 formatu / Telefono numerio SID / Žinučių paslaugos SID, pvz. +19995550123/PNXXX/MGXXX.", + "sms-message": "SMS žinutė", + "sms-message-required": "SMS žinutė yra privaloma.", + "sms-message-max-length": "SMS žinutė negali būti ilgesnė nei 1600 simbolių", + "twilio-account-sid": "Twilio paskyros SID", + "twilio-account-sid-required": "Twilio paskyros SID yra privalomas", + "twilio-account-token": "Twilio paskyros prieigos raktas (Token)", + "twilio-account-token-required": "Twilio paskyros prieigos raktas (Token) yra privalomas", + "send-test-sms": "Siųsti bandomąją SMS", + "test-sms-sent": "Bandomoji SMS sėkmingai išsiųsta!", + "security-settings": "Saugumo nustatymai", + "password-policy": "Slaptažodžių politika", + "minimum-password-length": "Minimalus slaptažodžio ilgis", + "minimum-password-length-required": "Minimalus slaptažodžio ilgis yra privalomas", + "minimum-password-length-range": "Minimalus slaptažodžio ilgis turi būti nuo 5 iki 50 simbolių", + "maximum-password-length": "Maksimalus slaptažodžio ilgis", + "maximum-password-length-min": "Maksimalus slaptažodžio ilgis turi būti bent 6 simboliai", + "maximum-password-length-less-min": "Maksimalus slaptažodžio ilgis turi būti didesnis nei minimalus", + "minimum-uppercase-letters": "Minimalus didžiųjų raidžių kiekis", + "minimum-uppercase-letters-range": "Minimalus didžiųjų raidžių kiekis negali būti neigiamas", + "minimum-lowercase-letters": "Minimalus mažųjų raidžių kiekis", + "minimum-lowercase-letters-range": "Minimalus mažųjų raidžių kiekis negali būti neigiamas", + "minimum-digits": "Minimalus skaitmenų kiekis", + "minimum-digits-range": "Minimalus skaitmenų kiekis negali būti neigiamas", + "minimum-special-characters": "Minimalus specialiųjų simbolių kiekis", + "minimum-special-characters-range": "Minimalus specialiųjų simbolių kiekis negali būti neigiamas", + "password-expiration-period-days": "Slaptažodžio galiojimo laikotarpis dienomis", + "password-expiration-period-days-range": "Slaptažodžio galiojimo laikotarpis dienomis negali būti neigiamas", + "password-reuse-frequency-days": "Slaptažodžio pakartotinio naudojimo dažnis dienomis", + "password-reuse-frequency-days-range": "Slaptažodžio pakartotinio naudojimo dažnis dienomis negali būti neigiamas", + "allow-whitespace": "Leisti tarpus", + "force-reset-password-if-no-valid": "Priverstinai atstatyti slaptažodį, jei jis negalioja", + "force-reset-password-if-no-valid-hint": "Būkite atsargūs įjungdami šią funkciją: naudotojai, kurių slaptažodžiai netinkami, turės juos atstatyti per el. paštą.", + "general-policy": "Bendroji politika", + "max-failed-login-attempts": "Maksimalus neteisingų prisijungimų skaičius prieš paskyros užrakinimą", + "minimum-max-failed-login-attempts-range": "Maksimalus neteisingų prisijungimų skaičius negali būti neigiamas", + "user-lockout-notification-email": "Naudotojo paskyros užrakinimo atveju siųsti pranešimą el. paštu", + "user-activation-token-ttl": "Naudotojo aktyvavimo nuorodos TTL valandomis", + "user-activation-token-ttl-range": "Naudotojo aktyvavimo nuorodos TTL turi būti nuo 1 iki 24 valandų", + "password-reset-token-ttl": "Slaptažodžio atstatymo nuorodos TTL valandomis", + "password-reset-token-ttl-range": "Slaptažodžio atstatymo nuorodos TTL turi būti nuo 1 iki 24 valandų", + "mobile-secret-key-length": "Mobiliojo slaptosios rakto ilgis", + "mobile-secret-key-length-range": "Mobiliojo slaptosios rakto ilgis turi būti teigiamas", + "domain-name": "Domeno pavadinimas", + "domain-name-unique": "Domeno pavadinimas ir protokolas turi būti unikalūs.", + "domain-name-max-length": "Domeno pavadinimas turi būti trumpesnis nei 256 simboliai", + "error-verification-url": "Domeno pavadinime neturi būti simbolių '/' ir ':'. Pavyzdys: thingsboard.io", + "connection-settings": "Ryšio nustatymai", + "oauth2": { + "access-token-uri": "Prieigos rakto URI", + "access-token-uri-required": "Prieigos rakto URI yra privalomas.", + "activate-user": "Aktyvuoti naudotoją", + "add-domain": "Pridėti domeną", + "delete-domain": "Ištrinti domeną", + "add-provider": "Pridėti tiekėją", + "delete-provider": "Ištrinti tiekėją", + "allow-user-creation": "Leisti naudotojų kūrimą", + "always-fullscreen": "Visada viso ekrano režimu", + "authorization-uri": "Autorizacijos URI", + "authorization-uri-required": "Autorizacijos URI yra privalomas.", + "add-client": "Pridėti OAuth 2.0 klientą", + "client-details": "OAuth 2.0 kliento informacija", + "client": "OAuth 2.0 klientas", + "clients": "OAuth 2.0 klientai", + "no-oauth2-clients": "OAuth 2.0 klientų nerasta", + "search-oauth2-clients": "Ieškoti OAuth 2.0 klientų", + "delete-client-title": "Ar tikrai norite ištrinti OAuth 2.0 klientą '{{clientName}}'?", + "delete-client-text": "Būkite atsargūs, po patvirtinimo klientas ir visi susiję duomenys bus negrįžtamai pašalinti.", + "delete-mobile-app-title": "Ar tikrai norite ištrinti mobiliojoje programėlėje '{{applicationName}}'?", + "delete-mobile-app-text": "Būkite atsargūs, po patvirtinimo mobilioji programėlė ir visi susiję duomenys bus negrįžtamai pašalinti.", + "title": "Pavadinimas.", + "client-title-required": "Pavadinimas yra privalomas.", + "client-title-max-length": "Pavadinimas turi būti mažiau nei 100", + "advanced-settings": "Išplėstiniai nustatymai", + "domain-details": "Domeno informacija", + "no-domains": "Domenų nerasta.", + "search-domains": "Ieškoti domenų.", + "mobile-app-details": "Mobiliosios programėlės informacija", + "add-mobile-app": "Pridėti mobiliąją programėlę", + "no-mobile-apps": "No applications configured", + "search-mobile-apps": "Ieškoti mobiliųjų programėlių.", + "send-token": "Siųsti prieigos raktą.", + "create-new": "Sukurti naują", + "client-authentication-method": "Kliento autentifikavimo metodas", + "client-id": "Kliento ID", + "client-id-required": "Kliento ID yra privalomas.", + "client-id-max-length": "Kliento ID turi būti trumpesnis nei 256 simboliai", + "client-secret": "Kliento slaptas raktas", + "client-secret-required": "Kliento slaptas raktas yra privalomas.", + "client-secret-max-length": "Kliento slaptis turi būti trumpesnė nei 2049 simboliai", + "custom-setting": "Pasirinktiniai nustatymai.", + "customer-name-pattern": "Kliento vardo šablonas.", + "customer-name-pattern-max-length": "Kliento vardo šablonas turi būti trumpesnis nei 256 simboliai.", + "default-dashboard-name": "Numatytas prietaisų skydelio pavadinimas", + "default-dashboard-name-max-length": "Numatytojo prietaisų skydelio pavadinimas turi būti trumpesnis nei 256 simboliai", + "delete-domain-text": "Būkite atsargūs — po patvirtinimo domenas ir visi tiekėjo duomenys taps nepasiekiami.", + "delete-domain-title": "Ar tikrai norite ištrinti domeną „{{domainName}}“?", + "delete-registration-text": "Būkite atsargūs, po patvirtinimo tiekėjo duomenys taps nepasiekiami.", + "delete-registration-title": "Ar tikrai norite ištrinti tiekėją „{{name}}“?", + "email-attribute-key": "Email attribute key", + "email-attribute-key-required": "Email attribute key is required.", + "email-attribute-key-max-length": "Email attribute key should be less than 32", + "first-name-attribute-key": "First name attribute key", + "first-name-attribute-key-max-length": "First name attribute key should be less than 32", + "general": "General", + "jwk-set-uri": "JSON Web Key URI", + "last-name-attribute-key": "Last name attribute key", + "last-name-attribute-key-max-length": "Last name attribute key should be less than 32", + "login-button-icon": "Login button icon", + "login-button-label": "Provider label", + "login-button-label-placeholder": "Login with $(Provider label)", + "login-button-label-required": "Label is required.", + "login-provider": "Login provider", + "mapper": "Mapper", + "new-domain": "New domain", + "oauth2": "OAuth2", + "password-max-length": "Password should be less than 256", + "redirect-uri-template": "Redirect URI template", + "copy-redirect-uri": "Nukopijuoti nukreipimo URI.", + "registration-id": "Registration ID", + "registration-id-required": "Registration ID is required.", + "registration-id-unique": "Registration ID need to unique for the system.", + "scope": "Scope", + "scope-required": "Scope is required.", + "tenant-name-pattern": "Tenant name pattern", + "tenant-name-pattern-required": "Tenant name pattern is required.", + "tenant-name-pattern-max-length": "Tenant name pattern ishould be less than 256", + "tenant-name-strategy": "Tenant name strategy", + "type": "Mapper type", + "uri-pattern-error": "Invalid URI format.", + "url": "URL", + "url-pattern": "Invalid URL format.", + "url-required": "URL is required.", + "url-max-length": "URL should be less than 256", + "user-info-uri": "User info URI", + "user-info-uri-required": "User info URI is required.", + "username-max-length": "User name should be less than 256", + "user-name-attribute-name": "User name attribute key", + "user-name-attribute-name-required": "User name attribute key is required", + "protocol": "Protocol", + "domain-schema-http": "HTTP", + "domain-schema-https": "HTTPS", + "domain-schema-mixed": "HTTP+HTTPS", + "enable": "Enable OAuth2 settings", + "disable": "Išjungti OAuth 2.0 nustatymus", + "edge": "Propaguoti į Edge", + "edge-enable": "Įjungti propagavimą į „Edge“", + "edge-disable": "Išjungti propagavimą į „Edge“", + "domains": "Domains", + "mobile-apps": "Mobile applications", + "mobile-package": "Application package", + "mobile-package-placeholder": "Ex.: my.example.app", + "mobile-package-hint": "For Android: your own unique Application ID. For iOS: Product bundle identifier.", + "mobile-package-unique": "Application package must be unique.", + "mobile-package-required": "Programėlės paketo pavadinimas yra privalomas.", + "mobile-package-max-length": "Programėlės paketo pavadinimas turi būti trumpesnis nei 256 simboliai.", + "mobile-package-spaces": "Programėlės paketo pavadinimas negali turėti tarpų.", + "mobile-app-secret": "Application secret", + "mobile-app-secret-hint": "Base64 koduota eilutė, vaizduojanti bent 512 bitų duomenų.", + "mobile-app-secret-required": "Programėlės slaptasis raktas yra privalomas.", + "mobile-app-secret-min-length": "Programėlės slaptasis raktas turi būti bent 512 bitų duomenų.", + "mobile-app-secret-base64": "Programos paslaptis turi būti „base64“ formato.", + "invalid-mobile-app-secret": "Application secret must contain only alphanumeric characters and must be between 16 and 2048 characters long.", + "copy-mobile-app-secret": "Nukopijuoti programos slaptą raktą", + "delete-mobile-app": "Ištrinti programos informaciją", + "providers": "Providers", + "platform-web": "Web", + "platform-android": "Android", + "platform-ios": "iOS", + "all-platforms": "Visos platformos", + "smtp-provider": "SMTP provider", + "allowed-platforms": "Leistinos platforms", + "authentication": "Autentifikacija", + "basic": "Bazinis", + "provider": "Provider", + "redirect-url": "Redirect URI", + "domain-name": "Domeno pavadinimas", + "domain-name-required": "Domeno pavadinimas yra privalomas", + "redirect-url-template": "Redirect URI template", + "microsoft-tenant-id": "Directory (tenant) Id", + "microsoft-tenant-id-required": "Directory (tenant) Id is required", + "token-uri": "Token URI", + "token-uri-required": "Token URI is required", + "redirect-uri": "Redirect URI", + "google-provider": "Google", + "microsoft-provider": "Office 365", + "sendgrid-provider": "Sendgrid", + "custom-provider": "Pasirinktinis", + "generate-access-token": "Generate access token", + "update-access-token": "Update access token", + "access-token-status": "Prieigos rakto būsena:", + "token-status-generated": "generated", + "token-status-not-generated": "not generated" + }, + "smpp-provider": { + "smpp-version": "SMPP versija", + "smpp-host": "SMPP serveris", + "smpp-host-required": "SMPP serveris yra privalomas", + "smpp-port": "SMPP prievadas", + "smpp-port-required": "SMPP prievadas yra privalomas", + "system-id": "Sistemos ID", + "system-id-required": "Sistemos ID yra privalomas", + "password": "Slaptažodis", + "password-required": "Slaptažodis yra privalomas", + "type-settings": "Tipo nustatymai", + "source-settings": "Šaltinio nustatymai", + "destination-settings": "Paskirties nustatymai", + "additional-settings": "Papildomi nustatymai", + "system-type": "Sistemos tipas", + "bind-type": "Susiejimo tipas", + "service-type": "Paslaugos tipas", + "source-address": "Šaltinio adresas", + "source-ton": "Šaltinio TON", + "source-npi": "Šaltinio NPI", + "destination-ton": "Paskirties TON (Numerio tipas)", + "destination-npi": "Paskirties NPI (Numeracijos plano identifikacija)", + "address-range": "Adreso diapazonas", + "coding-scheme": "Koduotės schema", + "bind-type-tx": "Siuntėjas", + "bind-type-rx": "Gavėjas", + "bind-type-trx": "Siuntėjas/Gavėjas (Transceiver)", + "ton-unknown": "Nežinomas", + "ton-international": "Tarptautinis", + "ton-national": "Nacionalinis", + "ton-network-specific": "Tinklo specifinis", + "ton-subscriber-number": "Abonento numeris", + "ton-alphanumeric": "Raidinis-skaitmeninis", + "ton-abbreviated": "Sutrumpintas", + "npi-unknown": "0 - Nežinomas", + "npi-isdn": "1 - ISDN/telefono numeracijos planas (E163/E164)", + "npi-data-numbering-plan": "3 - Duomenų numeracijos planas (X.121)", + "npi-telex-numbering-plan": "4 - Telex numeracijos planas (F.69)", + "npi-land-mobile": "6 - Mobilusis (E.212)", + "npi-national-numbering-plan": "8 - Nacionalinis numeracijos planas", + "npi-private-numbering-plan": "9 - Privatus numeracijos planas", + "npi-ermes-numbering-plan": "10 - ERMES numeracijos planas (ETSI DE/PS 3 01-3)", + "npi-internet": "13 - Internetas (IP)", + "npi-wap-client-id": "18 - WAP kliento ID (nustatomas WAP forumo)", + "scheme-smsc": "0 - SMSC numatytoji abėcėlė (ASCII trumpiems ir ilgiems kodams bei GSM be mokesčio numeriams)", + "scheme-ia5": "1 - IA5 (ASCII trumpiems ir ilgiems kodams, Latin 9 be mokesčio numeriams (ISO-8859-9))", + "scheme-octet-unspecified-2": "2 - Neapibrėžtas aštuonetas (8 bitų dvejetainis)", + "scheme-latin-1": "3 - Lotynų 1 (ISO-8859-1)", + "scheme-octet-unspecified-4": "4 - Neapibrėžtas aštuonetas (8 bitų dvejetainis)", + "scheme-jis": "5 - JIS (X 0208-1990)", + "scheme-cyrillic": "6 - Kirilica (ISO-8859-5)", + "scheme-latin-hebrew": "7 - Lotynų/Hebrajų (ISO-8859-8)", + "scheme-ucs-utf": "8 - UCS2/UTF-16 (ISO/IEC-10646)", + "scheme-pictogram-encoding": "9 - Piktogramų koduotė", + "scheme-music-codes": "10 - Muzikos kodai (ISO-2022-JP)", + "scheme-extended-kanji-jis": "13 - Išplėstinė Kanji JIS (X 0212-1990)", + "scheme-korean-graphic-character-set": "14 - Korėjiečių grafinis simbolių rinkinys (KS C 5601/KS X 1001)" + }, + "queue-select-name": "Pasirinkite eilės pavadinimą", + "queue-name": "Pavadinimas", + "queue-name-required": "Eilės pavadinimas yra privalomas!", + "queues": "Eilės", + "queue-partitions": "Skirsniai", + "queue-submit-strategy": "Pateikimo strategija", + "queue-processing-strategy": "Apdorojimo strategija", + "queue-configuration": "Eilės konfigūracija", + "repository-settings": "Repozitorijos nustatymai", + "repository": "Repozitorija", + "repository-url": "Repozitorijos URL", + "repository-url-required": "Repozitorijos URL yra privalomas.", + "default-branch": "Numatytasis šakos pavadinimas", + "repository-read-only": "Tik skaitymui", + "show-merge-commits": "Rodyti sujungimo (merge) įrašus", + "authentication-settings": "Autentifikavimo nustatymai", + "auth-method": "Autentifikavimo metodas", + "auth-method-username-password": "Slaptažodis / prieigos raktas", + "auth-method-username-password-hint": "GitHub naudotojai privalo naudoti prieigos raktus su rašymo teisėmis į repozitoriją.", + "auth-method-private-key": "Privatus raktas", + "password-access-token": "Slaptažodis / prieigos raktas", + "change-password-access-token": "Pakeisti slaptažodį / prieigos raktą", + "private-key": "Privatus raktas", + "drop-private-key-file-or": "Vilkite ir numeskite privataus rakto failą arba", + "passphrase": "Slaptažodžio frazė", + "enter-passphrase": "Įveskite slaptažodžio frazę", + "change-passphrase": "Pakeisti slaptažodžio frazę", + "check-access": "Patikrinti prieigą", + "check-repository-access-success": "Repozitorijos prieiga sėkmingai patvirtinta!", + "delete-repository-settings-title": "Ar tikrai norite ištrinti repozitorijos nustatymus?", + "delete-repository-settings-text": "Būkite atsargūs, po patvirtinimo repozitorijos nustatymai bus pašalinti ir versijų valdymo funkcija taps nepasiekiama.", + "auto-commit-settings": "Automatinio įrašo nustatymai", + "auto-commit": "Automatinis įrašas (commit)", + "auto-commit-entities": "Automatinio įrašo objektai", + "no-auto-commit-entities-prompt": "Nėra sukonfigūruotų objektų automatiniam įrašui", + "delete-auto-commit-settings-title": "Ar tikrai norite ištrinti automatinio įrašo nustatymus?", + "delete-auto-commit-settings-text": "Būkite atsargūs, po patvirtinimo automatinio įrašo nustatymai bus pašalinti ir funkcija bus išjungta visiems objektams.", + "mobile-app": { + "mobile-app": "Mobilioji programėlė", + "mobile-app-qr-code-widget-settings": "Mobiliosios programėlės QR kodo valdiklio nustatymai", + "applications": "Programėlės", + "default": "Numatytasis", + "custom": "Pasirinktinis", + "android": "Android", + "ios": "iOS", + "appearance": "Išvaizda", + "appearance-on-home-page": "Išvaizda pagrindiniame puslapyje", + "enabled": "Įjungta", + "disabled": "Išjungta", + "badges": "Ženkleliai", + "label": "Etiketė", + "label-required": "Etiketė yra privaloma", + "label-max-length": "Etiketė turi būti ne ilgesnė kaip 50 simbolių", + "right": "Dešinėje", + "left": "Kairėje", + "set": "Nustatyti", + "preview": "Peržiūra", + "connect-mobile-app": "Prijungti mobiliąją programėlę", + "use-system-settings": "Naudoti sistemos nustatymus" + }, + "2fa": { + "2fa": "Dvigubas autentifikavimas", + "available-providers": "Galimi tiekėjai", + "issuer-name": "Leidėjo pavadinimas", + "issuer-name-required": "Leidėjo pavadinimas yra privalomas.", + "max-verification-failures-before-user-lockout": "Maksimalus neteisingų patvirtinimų skaičius prieš naudotojo užrakinimą", + "max-verification-failures-before-user-lockout-pattern": "Maksimalus patvirtinimų skaičius turi būti teigiamas sveikasis skaičius.", + "number-of-checking-attempts": "Tikrinimo bandymų skaičius", + "number-of-checking-attempts-pattern": "Tikrinimo bandymų skaičius turi būti teigiamas sveikasis skaičius.", + "number-of-checking-attempts-required": "Tikrinimo bandymų skaičius yra privalomas.", + "number-of-codes": "Kodų skaičius", + "number-of-codes-pattern": "Kodų skaičius turi būti teigiamas sveikasis skaičius.", + "number-of-codes-required": "Kodų skaičius yra privalomas.", + "provider": "Tiekėjas", + "retry-verification-code-period": "Pakartotinio patvirtinimo kodo laikotarpis (sek)", + "retry-verification-code-period-pattern": "Minimalus laikotarpis yra 5 sek.", + "retry-verification-code-period-required": "Pakartotinio patvirtinimo kodo laikotarpis yra privalomas.", + "total-allowed-time-for-verification": "Bendras leidžiamas patvirtinimo laikas (sek)", + "total-allowed-time-for-verification-pattern": "Minimalus bendras leidžiamas laikas yra 60 sek.", + "total-allowed-time-for-verification-required": "Bendras leidžiamas laikas yra privalomas.", + "use-system-two-factor-auth-settings": "Naudoti sistemos dviejų veiksnių autentifikavimo nustatymus", + "verification-code-check-rate-limit": "Patvirtinimo kodo tikrinimo dažnio limitas", + "verification-code-lifetime": "Patvirtinimo kodo galiojimo laikas (sek)", + "verification-code-lifetime-pattern": "Patvirtinimo kodo galiojimo laikas turi būti teigiamas sveikasis skaičius.", + "verification-code-lifetime-required": "Patvirtinimo kodo galiojimo laikas yra privalomas.", + "verification-message-template": "Patvirtinimo pranešimo šablonas", + "verification-limitations": "Patvirtinimo apribojimai", + "verification-message-template-pattern": "Patvirtinimo pranešime turi būti šablonas: ${code}", + "verification-message-template-required": "Patvirtinimo pranešimo šablonas yra privalomas.", + "within-time": "Per laiką (sek)", + "within-time-pattern": "Laikas turi būti teigiamas sveikasis skaičius.", + "within-time-required": "Laikas yra privalomas." + }, + "jwt": { + "security-settings": "JWT saugumo nustatymai", + "issuer-name": "Leidėjo pavadinimas", + "issuer-name-required": "Leidėjo pavadinimas yra privalomas.", + "signings-key": "Pasirašymo raktas", + "signings-key-hint": "Base64 koduota eilutė, atitinkanti bent 256 bitų duomenis.", + "signings-key-required": "Pasirašymo raktas yra privalomas.", + "signings-key-min-length": "Pasirašymo raktas turi būti bent 256 bitų duomenų ilgio.", + "signings-key-base64": "Pasirašymo raktas turi būti Base64 formatu.", + "expiration-time": "Žetono galiojimo laikas (sek)", + "expiration-time-required": "Žetono galiojimo laikas yra privalomas.", + "expiration-time-max": "Didžiausias leidžiamas laikas – 2147483647 sekundės (68 metai).", + "expiration-time-min": "Mažiausias laikas – 60 sekundžių (1 minutė).", + "refresh-expiration-time": "Atnaujinimo žetono galiojimo laikas (sek)", + "refresh-expiration-time-required": "Atnaujinimo žetono galiojimo laikas yra privalomas.", + "refresh-expiration-time-max": "Didžiausias leidžiamas laikas – 2147483647 sekundės (68 metai).", + "refresh-expiration-time-min": "Mažiausias laikas – 900 sekundžių (15 minučių).", + "refresh-expiration-time-less-token": "Atnaujinimo žetono laikas turi būti ilgesnis už pagrindinio žetono laiką.", + "generate-key": "Generuoti raktą", + "info-header": "Visi naudotojai turės prisijungti iš naujo", + "info-message": "JWT pasirašymo rakto pakeitimas panaikins visų išduotų žetonų galiojimą. Visi naudotojai turės prisijungti iš naujo. Tai taip pat paveiks scenarijus, naudojančius Rest API / Websockets." + }, + "resources": "Ištekliai", + "notifications": "Pranešimai", + "notifications-settings": "Pranešimų nustatymai", + "slack-api-token": "Slack API prieigos raktas", + "slack": "Slack", + "slack-settings": "Slack nustatymai", + "mobile-settings": "Mobiliosios programos nustatymai", + "firebase-service-account-file": "Firebase paslaugos paskyros kredencialų JSON failas", + "select-firebase-service-account-file": "Vilkite ir numeskite savo Firebase paslaugos paskyros kredencialų failą arba ", + "trendz": "PredAna", + "trendz-settings": "PredAna nustatymai", + "trendz-url": "PredAna URL", + "trendz-url-required": "PredAna URL yra privalomas", + "trendz-api-key": "PredAna API raktas", + "trendz-enable": "Įjungti PredAna" + }, + "alarm": { + "alarm": "Įspėjimas", + "alarms": "Įspėjimai", + "all-alarms": "Visi įspėjimai", + "select-alarm": "Pasirinkite įspėjimą", + "no-alarms-matching": "Su '{{entity}}' susijusių įspėjimų nerasta.", + "alarm-required": "Įspėjimas yra privalomas", + "alarm-filter": "Įspėjimų filtras", + "filter": "Filtras", + "alarm-status": "Įspėjimo būsena", + "alarm-status-list": "Įspėjimo būsenų sąrašas", + "any-status": "Visos būsenos", + "search-status": { + "ANY": "Visi", + "ACTIVE": "Aktyvūs", + "CLEARED": "Išvalyti", + "ACK": "Patvirtinti", + "UNACK": "Nepatvirtinti" + }, + "display-status": { + "ACTIVE_UNACK": "Aktyvūs nepatvirtinti", + "ACTIVE_ACK": "Aktyvūs pripažinti", + "CLEARED_UNACK": "Atmesti nepatvirtinti", + "CLEARED_ACK": "Atmesti pripažinti" + }, + "no-alarms-prompt": "Įspėjimų nėra", + "created-time": "Sukūrimo laikas", + "type": "Tipas", + "severity": "Lygis", + "originator": "Iniciatorius", + "originator-type": "Iniciatoriaus tipas", + "details": "Pastabos", + "originator-label": "Iniciatoriaus pavadinimas", + "assign": "Priskirti", + "assignments": "Priskyrimai", + "assignee": "Atsakingas", + "assignee-id": "Atsakingo asmens ID", + "assignee-first-name": "Atsakingo asmens vardas", + "assignee-last-name": "Atsakingo asmens pavardė", + "assignee-email": "Atsakingo asmens e-paštas", + "unassigned": "Panaikinti priskyrimą", + "user-deleted": "Vartotojas ištrintas", + "assignee-not-set": "Visi", + "status": "Statusas", + "alarm-details": "Įspėjimo pastabos", + "start-time": "Pradžia", + "assign-time": "Priskyrimo laikas", + "end-time": "Pabaiga", + "ack-time": "Pripažinimo laikas", + "clear-time": "Atmetimo laikas", + "duration": "Trukmė", + "alarm-severity": "Alarm severity", + "alarm-severity-list": "Įspėjimų lygiai", + "any-severity": "Visi lygiai", + "severity-critical": "Kritinis", + "severity-major": "Svarbus", + "severity-minor": "Antraeilis", + "severity-warning": "Įspėjimas", + "severity-indeterminate": "Nenustatytas", + "acknowledge": "Patvirtinti", + "clear": "Atmesti", + "delete": "Panaikinti", + "search": "Įspėjimo paieška", + "selected-alarms": "Pasirinkta { count, plural, =1 {1 įspėjimas} other {# įspėjimai} }", + "no-data": "Nėra duomenų", + "polling-interval": "Įspėjimų tikrinimo intervalas (sek)", + "polling-interval-required": "Reikalingas įspėjimų tikrinimo intervalas", + "min-polling-interval-message": "Mažiausias įspėjimų tikrinimo intervalas yra 1 sekundė.", + "aknowledge-alarms-title": "Pripažinti { count, plural, =1 {1 įspėjimą} other {# įspėjimus} }", + "aknowledge-alarms-text": "Pripažinti { count, plural, =1 {1 įspėjimą} other {# įspėjimus} }?", + "aknowledge-alarm-title": "Pripažinti įspėjimą", + "aknowledge-alarm-text": "Pripažinti įspėjimą?", + "selected-alarms-are-acknowledged": "Pasirinkti įspėjimai pripažinti", + "clear-alarms-title": "Atmesti { count, plural, =1 {1 įspėjimą} other {# įspėjimus} }", + "clear-alarms-text": "Atmesti { count, plural, =1 {1 įspėjimą} other {# įspėjimus} }?", + "clear-alarm-title": "Atmesti įspėjimą", + "clear-alarm-text": "Atmesti įspėjimą?", + "delete-alarms-title": "Panaikinti { count, plural, =1 {1 įspėjimą} other {# įspėjimus} }", + "delete-alarms-text": "Panaikinti { count, plural, =1 {1 įspėjimą} other {# įspėjimus} }?", + "selected-alarms-are-cleared": "Pasirinkti įspėjimai atmesti", + "alarm-status-filter": "Įspėjimų statusai", + "alarm-filter-title": "Įspėjimų filtras", + "assigned": "Priskirta", + "filter-title": "Filtras", + "max-count-load": "Didžiausias rodomų įspėjimų skaičius (0 – neribotas)", + "max-count-load-required": "Reikalingas didžiausias rodomų įspėjimų skaičius (0 – neribotas).", + "max-count-load-error-min": "Mažiausia reikšmė yra 0.", + "fetch-size": "Fetch size", + "fetch-size-required": "Fetch size is required.", + "fetch-size-error-min": "Mažiausia rekšmė yra 10.", + "alarm-types": "Įspėjimų tipai", + "alarm-type-list": "Įspėjimų tipų sąrašas", + "any-type": "Visi tipai", + "assigned-to-current-user": "Priskirta prisijungusiam vartotojui", + "assigned-to-me": "Priskirta man", + "search-propagated-alarms": "Ieškoti išplatintų įspėjimų", + "comments": "Įspėjimo pastabos", + "show-more": "Rodyti daugiau", + "additional-info": "Papildoma informacija", + "alarm-type": "Įspėjimo tipas", + "enter-alarm-type": "Įveskite įspėjimo tipą", + "no-alarm-types-matching": "Nėra įspėjimų tipų, kurie atitiktų '{{entitySubtype}}'.", + "alarm-type-list-empty": "Nepasirinktas įspėjimų tipas" + }, + "alarm-activity": { + "add": "Pridėti komentarą...", + "alarm-comment": "Įspėjimo komentaras", + "comments": "Komentarai", + "delete-alarm-comment": "Ištrinti komentarą?", + "refresh": "Atnaujinti", + "oldest-first": "Seniausi viršuje", + "newest-first": "Naujausi viršuje", + "activity": "Veikla", + "export": "Eksportuoti į CSV", + "author": "Sukūrė", + "created-date": "Sukūrimo dara", + "edited-date": "Redagavimo data", + "text": "Tekstas", + "system": "Sistema" + }, + "alias": { + "add": "Pridėti pseudonimą", + "edit": "Redaguoti pseudonimą", + "name": "Pseudonimas", + "name-required": "Pseudonimas yra privalomas", + "duplicate-alias": "Toks pseudonimas sistemoje jau egzistuoja.", + "filter-type-single-entity": "Vienas subjektas", + "filter-type-entity-list": "Subjektų sąrašas", + "filter-type-entity-name": "Subjekto pavadinimas", + "filter-type-entity-type": "Subjekto tipas", + "filter-type-state-entity": "Subjektas iš skydelio būsenos", + "filter-type-state-entity-description": "Subjektas iš skydelio būsenos parametrų", + "filter-type-asset-type": "Turto tipas", + "filter-type-asset-type-description": "Turtas, kurio tipas '{{assetTypes}}'", + "filter-type-asset-type-and-name-description": "Turtas, kurio tipas '{{assetTypes}}' ir pavadinimas prasideda '{{prefix}}'", + "filter-type-device-type": "Įrenginio tipas", + "filter-type-device-type-description": "Įrenginiai, kurių tipas '{{deviceTypes}}'", + "filter-type-device-type-and-name-description": "Įrenginiai, kurių tipas '{{deviceTypes}}' ir kurių pavadinimas prasideda '{{prefix}}'", + "filter-type-entity-view-type": "Subjekto rodinio tipas", + "filter-type-entity-view-type-description": "Subjektų rodiniai, kurių tipas '{{entityViewTypes}}'", + "filter-type-entity-view-type-and-name-description": "Subjektų rodiniai, kurių tipas '{{entityViewTypes}}' ir kurių pavadinimas prasideda '{{prefix}}'", + "filter-type-edge-type": "Krašto (Edge) tipas", + "filter-type-edge-type-description": "Kraštai (Edges), kurių tipas '{{edgeTypes}}'", + "filter-type-edge-type-and-name-description": "Kraštai, kurių tipas '{{edgeTypes}}' ir pavadinimas prasideda '{{prefix}}'", + "filter-type-relations-query": "Ryšių užklausa", + "filter-type-relations-query-description": "{{entities}} turintys {{relationType}} ryšį {{direction}} {{rootEntity}}", + "filter-type-edge-search-query": "Kraštų (Edge) paieškos užklausa", + "filter-type-edge-search-query-description": "Kraštai, kurių tipai {{edgeTypes}}, turintys {{relationType}} ryšį {{direction}} {{rootEntity}}", + "filter-type-asset-search-query": "Turto paieškos užklausa", + "filter-type-asset-search-query-description": "{{assetTypes}} tipo turtas, turintis {{relationType}} tipo ryšį {{direction}} {{rootEntity}}", + "filter-type-device-search-query": "Įrenginio paieškos užklausa", + "filter-type-device-search-query-description": "{{deviceTypes}} tipo įrenginiai, turintys {{relationType}} tipo ryšį {{direction}} {{rootEntity}}", + "filter-type-entity-view-search-query": "Subjektų rodinio paieškos užklausa", + "filter-type-entity-view-search-query-description": "{{entityViewTypes}} tipo subjektai, turintys {{relationType}} tipo ryšį {{direction}} {{rootEntity}}", + "filter-type-apiUsageState": "API naudojimo būsena", + "entity-filter": "Subjektų filtras", + "resolve-multiple": "Apdoroti kelis subjektus vienu metu", + "resolve-multiple-hint": "Įjunkite, jei norite vienu metu atvaizduoti duomenis iš visų filtruotų subjektų.\nJei išjungta, valdiklis rodys duomenis tik iš pasirinkto subjekto.", + "filter-type": "Filtro tipas", + "filter-type-required": "Filtro tipas yra privalomas.", + "entity-filter-no-entity-matched": "Subjektų, atitinkančių filtro kriterijus, nerasta.", + "no-entity-filter-specified": "Subjektų filtras nenustatytas", + "root-state-entity": "Naudoti subjektą iš skydelio būsenos kaip pagrindinį", + "last-level-relation": "Gauti tik paskutinio lygio ryšį", + "root-entity": "Pagrindinis subjektas", + "state-entity-parameter-name": "Būsenos subjekto parametro pavadinimas", + "default-state-entity": "Numatytasis būsenos subjektas", + "default-entity-parameter-name": "Numatytasis parametro pavadinimas", + "query-options": "Užklausos parinktys", + "max-relation-level": "Maksimalus ryšių lygis", + "unlimited-level": "Neribotas lygis", + "state-entity": "Skydelio būsenos subjektas", + "all-entities": "Visi subjektai", + "any-relation": "Visi ryšiai" + }, + "asset": { + "asset": "Turtas", + "assets": "Turtas", + "management": "Turto valdymas", + "view-assets": "Peržiūrėti turtą", + "add": "Pridėti turtą", + "asset-type-max-length": "Turto tipas turi būti trumpesnis nei 256 simboliai", + "assign-to-customer": "Priskirti klientui", + "assign-asset-to-customer": "Priskirti turtą klientui", + "assign-asset-to-customer-text": "Pasirinkite turtą, kurį norite priskirti klientui", + "no-assets-text": "Turtas nerastas", + "assign-to-customer-text": "Pasirinkite klientą, kuriam priskiriamas turtas", + "public": "Viešas", + "assignedToCustomer": "Priskirtas klientui", + "make-public": "Neriboti turto matomumo", + "make-private": "Riboti turto matomumą", + "unassign-from-customer": "Atsieti nuo kliento", + "delete": "Pašalinti turtą", + "asset-public": "Turtas yra viešas", + "asset-type": "Turto tipas", + "asset-type-required": "Turto tipas yra privalomas.", + "select-asset-type": "Pasirinkite turto tipą", + "enter-asset-type": "Įveskite turto tipą", + "any-asset": "Bet koks turtas", + "no-asset-types-matching": "Turto tipas, atitinkantis '{{entitySubtype}}', nerastas.", + "asset-type-list-empty": "Nepasirinkti turto tipai.", + "asset-types": "Turto tipai", + "name": "Pavadinimas", + "name-required": "Pavadinimas yra privalomas.", + "name-max-length": "Pavadinimas turi būti trumpesnis nei 256 simboliai", + "label-max-length": "Etiketė turi būti trumpesnė nei 256 simboliai", + "description": "Aprašymas", + "type": "Tipas", + "type-required": "Tipas yra privalomas.", + "details": "Pastabos", + "events": "Įvykiai", + "add-asset-text": "Pridėti turtą", + "asset-details": "Turto informacija", + "assign-assets": "Priskirti turtą", + "assign-assets-text": "Priskirti { count, plural, =1 {1 turtą} other {# turtus} } klientui", + "assign-asset-to-edge-title": "Priskirti turtą (-us) Edge įrenginiui", + "assign-asset-to-edge-text": "Pasirinkite turtą (-us), kuriuos norite priskirti Edge įrenginiui", + "delete-assets": "Pašalinti turtą", + "unassign-assets": "Atsieti turtą", + "unassign-assets-action-title": "Atsieti { count, plural, =1 {1 turtą} other {# turtus} } nuo kliento?", + "assign-new-asset": "Priskirti naują turtą", + "delete-asset-title": "Pašalinti turtą '{{assetName}}'?", + "delete-asset-text": "Būkite atsargūs, po patvirtinimo turtas ir visa su juo susijusi informacija bus pašalinta.", + "delete-assets-title": "Pašalinti { count, plural, =1 {1 turtą} other {# turtus} }?", + "delete-assets-action-title": "Pašalinti { count, plural, =1 {1 turtą} other {# turtus} }", + "delete-assets-text": "Būkite atsargūs, po patvirtinimo visas turtas ir su juo susijusi informacija bus pašalinta.", + "make-public-asset-title": "Ar tikrai norite neriboti turto '{{assetName}}' matomumo?", + "make-public-asset-text": "Po patvirtinimo turtas ir visa su juo susijusi informacija bus prieinama visiems.", + "make-private-asset-title": "Ar tikrai norite apriboti turto '{{assetName}}' matomumą?", + "make-private-asset-text": "Po patvirtinimo turtas ir visa su juo susijusi informacija nebebus prieinama kitiems.", + "unassign-asset-title": "Ar tikrai norite atsieti turtą '{{assetName}}'?", + "unassign-asset-text": "Po patvirtinimo turtas bus atsietas, o klientas neteks prieigos prie jo.", + "unassign-asset": "Atsieti turtą", + "unassign-assets-title": "Ar tikrai norite atsieti { count, plural, =1 {1 turtą} other {# turtus} }?", + "unassign-assets-text": "Po patvirtinimo visas pasirinktas turtas bus atsietas, o klientas neteks prieigos prie jo.", + "copyId": "Kopijuoti turto ID", + "idCopiedMessage": "Turto ID nukopijuotas į iškarpinę", + "select-asset": "Pasirinkite turtą", + "no-assets-matching": "Turtas, atitinkantis '{{entity}}', nerastas.", + "asset-required": "Turtas yra privalomas", + "name-starts-with": "Turto pavadinimas prasideda nuo", + "help-text": "Naudokite simbolį '%' pagal paieškos poreikį: '%turto_fragmentas%', '%turto_pabaiga', 'turto_pradžia%'.", + "search": "Turto paieška", + "import": "Importuoti turtą", + "asset-file": "Turto failas", + "label": "Etiketė", + "assign-asset-to-edge": "Priskirti turtą (-us) Edge įrenginiui", + "unassign-asset-from-edge": "Atsieti turtą nuo Edge", + "unassign-asset-from-edge-title": "Ar tikrai norite atsieti turtą '{{assetName}}' nuo Edge?", + "unassign-asset-from-edge-text": "Po patvirtinimo turtas bus atsietas ir nebebus pasiekiamas Edge įrenginyje.", + "unassign-assets-from-edge-title": "Ar tikrai norite atsieti { count, plural, =1 {1 turtą} other {# turtus} } nuo Edge?", + "unassign-assets-from-edge-text": "Po patvirtinimo visi pasirinkti turtai bus atsieti ir nebebus prieinami Edge įrenginiuose.", + "selected-assets": "Pasirinkta { count, plural, =1 {1 turtas} other {# turtai} }" + }, + "attribute": { + "attributes": "Atributai", + "latest-telemetry": "Naujausia telemetrija", + "no-latest-telemetry": "Telemetrijos duomenų nėra", + "attributes-scope": "Subjekto atributų sritis", + "scope-telemetry": "Telemetrija", + "scope-latest-telemetry": "Naujausia telemetrija", + "scope-client": "Kliento atributai", + "scope-server": "Serverio atributai", + "scope-shared": "Bendrinami atributai", + "scope-client-short": "Client", + "scope-server-short": "Server", + "scope-shared-short": "Shared", + "scope-latest-short": "Latest", + "scope-any": "Bet kurie", + "add": "Pridėti atributą", + "key": "Raktas", + "key-max-length": "Raktas negali viršyti 256 simbolių", + "last-update-time": "Paskutinio atnaujinimo laikas", + "key-required": "Atributo raktas yra privalomas.", + "value": "Reikšmė", + "value-required": "Atributo reikšmė yra privaloma.", + "telemetry-key-required": "Telemetrijos raktas yra privalomas.", + "telemetry-value-required": "Telemetrijos reikšmė yra privaloma.", + "delete-attributes-title": "Ar tikrai norite pašalinti { count, plural, =1 {1 atributą} other {# atributus} }?", + "delete-attributes-text": "Būkite atsargūs — po patvirtinimo pasirinkti atributai bus pašalinti.", + "delete-attributes": "Pašalinti atributus", + "enter-attribute-value": "Įveskite atributo reikšmę", + "show-on-widget": "Rodyti valdiklyje", + "widget-mode": "Valdiklio režimas", + "next-widget": "Kitas valdiklis", + "prev-widget": "Ankstesnis valdiklis", + "add-to-dashboard": "Pridėti į skydelį", + "add-widget-to-dashboard": "Pridėti valdiklį į skydelį", + "selected-attributes": "Pasirinkta { count, plural, =1 {1 atributas} other {# atributai} }", + "selected-telemetry": "Pasirinkta { count, plural, =1 {1 telemetrijos įrašas} other {# telemetrijos įrašai} }", + "no-attributes-text": "Atributų nėra", + "no-telemetry-text": "Telemetrijos duomenų nėra", + "copy-key": "Kopijuoti raktą", + "add-telemetry": "Pridėti telemetriją", + "copy-value": "Kopijuoti reikšmę", + "delete-timeseries": { + "start-time": "Pradžios laikas", + "ends-on": "Pabaigos laikas", + "strategy": "Strategija", + "delete-strategy": "Šalinimo strategija", + "all-data": "Pašalinti visus duomenis", + "all-data-except-latest-value": "Pašalinti visus duomenis, išskyrus naujausią reikšmę", + "latest-value": "Pašalinti naujausią reikšmę", + "all-data-for-time-period": "Pašalinti visus duomenis laikotarpiui", + "rewrite-latest-value": "Perrašyti naujausią reikšmę" + } + }, + "api-usage": { + "api-features": "API funkcijos", + "api-usage": "API naudojimas", + "alarm": "Įspėjimas", + "alarms-created": "Sukurti įspėjimai", + "queue-stats": "Eilės statistika", + "processing-failures-and-timeouts": "Apdorojimo klaidos ir laiko limitai", + "exceptions": "Išimtys", + "alarms-created-daily-activity": "Įspėjimų dienos veikla", + "alarms-created-hourly-activity": "Įspėjimų valandinė veikla", + "alarms-created-monthly-activity": "Įspėjimų mėnesinė veikla", + "data-points": "Duomenų taškai", + "data-points-storage-days": "Duomenų taškų saugojimo dienos", + "device-api": "Įrenginio API", + "email": "El. paštas", + "email-messages": "El. laiškai", + "email-messages-daily-activity": "El. laiškų dienos veikla", + "email-messages-monthly-activity": "El. laiškų mėnesinė veikla", + "executions": "Vykdymai", + "scripts": "Skriptai", + "scripts-hourly-activity": "Skriptų valandinė veikla", + "scripts-daily-activity": "Skriptų dienos veikla", + "scripts-monthly-activity": "Skriptų mėnesinė veikla", + "javascript": "JavaScript", + "javascript-executions": "JavaScript vykdymai", + "tbel": "TBEL", + "tbel-executions": "TBEL vykdymai", + "latest-error": "Naujausia klaida", + "messages": "Pranešimai", + "notifications": "Pranešimai", + "notifications-email-sms": "Pranešimai (El. paštas / SMS)", + "notifications-hourly-activity": "Pranešimų valandinė veikla", + "permanent-failures": "${entityName} nuolatinės klaidos", + "permanent-timeouts": "${entityName} nuolatiniai laiko limitai", + "processing-failures": "${entityName} apdorojimo klaidos", + "processing-timeouts": "${entityName} apdorojimo laiko limitai", + "rule-chain": "Taisyklių grandinė (Rule Chain)", + "rule-engine": "Taisyklių variklis (Rule Engine)", + "rule-engine-daily-activity": "Taisyklių variklio dienos veikla", + "rule-engine-executions": "Taisyklių variklio vykdymai", + "rule-engine-hourly-activity": "Taisyklių variklio valandinė veikla", + "rule-engine-monthly-activity": "Taisyklių variklio mėnesinė veikla", + "rule-engine-statistics": "Taisyklių variklio statistika", + "rule-node": "Taisyklių mazgas (Rule Node)", + "sms": "SMS", + "sms-messages": "SMS žinutės", + "sms-messages-daily-activity": "SMS žinučių dienos veikla", + "sms-messages-monthly-activity": "SMS žinučių mėnesinė veikla", + "successful": "${entityName} sėkmingi vykdymai", + "telemetry": "Telemetrija", + "telemetry-persistence": "Telemetrijos įrašymas", + "telemetry-persistence-daily-activity": "Telemetrijos įrašymo dienos veikla", + "telemetry-persistence-hourly-activity": "Telemetrijos įrašymo valandinė veikla", + "telemetry-persistence-monthly-activity": "Telemetrijos įrašymo mėnesinė veikla", + "transport": "Transportas", + "transport-daily-activity": "Transporto dienos veikla", + "transport-data-points": "Transporto duomenų taškai", + "transport-hourly-activity": "Transporto valandinė veikla", + "transport-messages": "Transporto pranešimai", + "transport-monthly-activity": "Transporto mėnesinė veikla", + "view-details": "Peržiūrėti detales", + "view-statistics": "Peržiūrėti statistiką" + }, + "api-limit": { + "cassandra-write-queries-core": "REST API Cassandra rašymo užklausos", + "cassandra-read-queries-core": "REST API ir WS telemetrijos Cassandra skaitymo užklausos", + "cassandra-write-queries-rule-engine": "Taisyklių variklio (Rule Engine) telemetrijos Cassandra rašymo užklausos", + "cassandra-read-queries-rule-engine": "Taisyklių variklio (Rule Engine) telemetrijos Cassandra skaitymo užklausos", + "cassandra-write-queries-monolith": "Monolitinės telemetrijos Cassandra rašymo užklausos", + "cassandra-read-queries-monolith": "Monolitinės telemetrijos Cassandra skaitymo užklausos", + "entity-version-creation": "Subjekto versijos kūrimas", + "entity-version-load": "Subjekto versijos įkėlimas", + "notification-requests": "Pranešimų užklausos", + "notification-requests-per-rule": "Pranešimų užklausos pagal taisyklę", + "rest-api-requests": "REST API užklausos", + "rest-api-requests-per-customer": "REST API užklausos pagal klientą", + "transport-messages": "Transporto pranešimai", + "transport-messages-per-device": "Transporto pranešimai pagal įrenginį", + "transport-messages-per-gateway": "Transporto pranešimai pagal šliuzą (gateway)", + "transport-messages-per-gateway-device": "Transporto pranešimai pagal šliuzo įrenginį", + "ws-updates-per-session": "WS atnaujinimai per sesiją", + "edge-events": "Edge įvykiai", + "edge-events-per-edge": "Edge įvykiai pagal Edge", + "edge-uplink-messages": "Edge uplink pranešimai", + "edge-uplink-messages-per-edge": "Edge uplink pranešimai pagal Edge" + }, + "audit-log": { + "audit": "Auditas", + "audit-logs": "Audito žurnalas", + "timestamp": "Laiko žyma", + "entity-type": "Subjektas", + "entity-name": "Pavadinimas", + "user": "Vartotojas", + "type": "Veiksmas", + "status": "Statusas", + "details": "Informacija", + "type-added": "Pridėta", + "type-deleted": "Pašalinta", + "type-updated": "Atnaujinta", + "type-attributes-updated": "Atributai atnaujinti", + "type-attributes-deleted": "Atributai pašalinti", + "type-rpc-call": "RPC iškvietimas", + "type-credentials-updated": "Įgaliojimai atnaujinti", + "type-assigned-to-customer": "Priskirta klientui", + "type-unassigned-from-customer": "Atsieta nuo kliento", + "type-assigned-to-edge": "Priskirta Edge įrenginiui", + "type-unassigned-from-edge": "Atsieta nuo Edge įrenginio", + "type-activated": "Aktyvuota", + "type-suspended": "Sustabdyta", + "type-credentials-read": "Įgaliojimų skaitymas", + "type-attributes-read": "Atributų skaitymas", + "type-relation-add-or-update": "Ryšys pridėtas arba atnaujintas", + "type-relation-delete": "Ryšys panaikintas", + "type-relations-delete": "Visi ryšiai panaikinti", + "type-alarm-ack": "Įspėjimas patvirtintas", + "type-alarm-clear": "Įspėjimas išvalytas", + "type-alarm-delete": "Įspėjimas pašalintas", + "type-alarm-assign": "Įspėjimas priskirtas", + "type-alarm-unassign": "Įspėjimas atsietas", + "type-added-comment": "Pridėtas komentaras", + "type-updated-comment": "Atnaujintas komentaras", + "type-deleted-comment": "Pašalintas komentaras", + "type-login": "Prisijungimas", + "type-logout": "Atsijungimas", + "type-lockout": "Užblokuotas", + "status-success": "Sėkminga", + "status-failure": "Nesėkminga", + "audit-log-details": "Audito žurnalo informacija", + "no-audit-logs-prompt": "Audito žurnalo įrašų nėra", + "action-data": "Veiksmų informacija", + "failure-details": "Nesėkmės informacija", + "search": "Žurnalo įrašų paieška", + "clear-search": "Išvalyti paiešką", + "type-assigned-from-tenant": "Priskirta iš nuomotojo", + "type-assigned-to-tenant": "Priskirta nuomotojui", + "type-provision-success": "Įrenginys sėkmingai įtrauktas (provisioned)", + "type-provision-failure": "Įrenginio įtraukimas nepavyko", + "type-timeseries-updated": "Telemetrija atnaujinta", + "type-timeseries-deleted": "Telemetrija pašalinta", + "type-sms-sent": "SMS išsiųsta" + }, + "debug-settings": { + "label": "Derinimo konfigūracija", + "on-failure": "Tik klaidos (24/7)", + "all-messages": "Visos žinutės ({{time}})", + "failures": "Klaidos", + "entity": "objektas", + "hint": { + "main-limited": "Per {{time}} bus įrašyta ne daugiau kaip {{msg}} derinimo žinučių iš {{entity}}.", + "on-failure": "Registruoti tik klaidų žinutes.", + "all-messages": "Registruoti visas derinimo žinutes." + } + }, + "calculated-fields": { + "expression": "Išraiška", + "no-found": "Apskaičiuotų laukų nerasta", + "list": "{ count, plural, =1 {Vienas apskaičiuotas laukas} other {# apskaičiuotų laukų sąrašas} }", + "selected-fields": "{ count, plural, =1 {1 apskaičiuotas laukas} other {# apskaičiuoti laukai} } pasirinkta", + "type": { + "simple": "Paprastas", + "script": "Skriptas" + }, + "arguments": "Argumentai", + "decimals-by-default": "Numatytasis dešimtainių skaičius", + "debugging": "Apskaičiuoto lauko derinimas", + "argument-name": "Argumento pavadinimas", + "datasource": "Duomenų šaltinis", + "add-argument": "Pridėti argumentą", + "test-script-function": "Išbandyti skripto funkciją", + "no-arguments": "Argumentai nesukonfigūruoti", + "argument-settings": "Argumento nustatymai", + "argument-current": "Dabartinis subjektas", + "argument-current-tenant": "Dabartinis valdytojas", + "argument-device": "Įrenginys", + "argument-asset": "Turtas", + "argument-customer": "Klientas", + "argument-tenant": "Dabartinis valdytojas", + "argument-type": "Argumento tipas", + "see-debug-events": "Peržiūrėti derinimo įvykius", + "attribute": "Atributas", + "copy-argument-name": "Kopijuoti argumento pavadinimą", + "timeseries-key": "Laiko sekos raktas", + "device-name": "Įrenginio pavadinimas", + "latest-telemetry": "Naujausia telemetrija", + "rolling": "Laiko sekos kaupimas", + "attribute-scope": "Atributo sritis", + "server-attributes": "Serverio atributai", + "client-attributes": "Kliento atributai", + "shared-attributes": "Bendrinami atributai", + "attribute-key": "Atributo raktas", + "default-value": "Numatytoji reikšmė", + "limit": "Maksimalių reikšmių skaičius", + "time-window": "Laiko intervalas", + "customer-name": "Kliento pavadinimas", + "asset-name": "Turto pavadinimas", + "timeseries": "Laiko seka", + "output": "Rezultatas", + "create": "Sukurti naują apskaičiuotą lauką", + "file": "Apskaičiuoto lauko failas", + "invalid-file-error": "Netinkamas failo formatas. Įsitikinkite, kad failas yra tinkamas JSON formatas.", + "import": "Importuoti apskaičiuotą lauką", + "export": "Eksportuoti apskaičiuotą lauką", + "export-failed-error": "Nepavyko eksportuoti apskaičiuoto lauko: {{error}}", + "output-type": "Rezultato tipas", + "delete-title": "Ar tikrai norite pašalinti apskaičiuotą lauką '{{title}}'?", + "delete-text": "Būkite atsargūs — po patvirtinimo apskaičiuotas laukas ir visa susijusi informacija bus negrįžtamai pašalinta.", + "delete-multiple-title": "Ar tikrai norite pašalinti { count, plural, =1 {1 apskaičiuotą lauką} other {# apskaičiuotus laukus} }?", + "delete-multiple-text": "Būkite atsargūs — po patvirtinimo visi pasirinkti apskaičiuoti laukai bus pašalinti kartu su visa susijusia informacija.", + "test-with-this-message": "Išbandyti su šia žinute", + "use-latest-timestamp": "Naudoti naujausią laiko žymą", + "hint": { + "arguments-simple-with-rolling": "Paprasto tipo apskaičiuotas laukas neturėtų turėti laiko sekos kaupimo tipo raktų.", + "arguments-empty": "Argumentai negali būti tušti.", + "expression-required": "Reikalinga išraiška.", + "expression-invalid": "Išraiška netinkama.", + "expression-max-length": "Išraiškos ilgis turi būti trumpesnis nei 255 simboliai.", + "argument-name-required": "Argumento pavadinimas yra būtinas.", + "argument-name-pattern": "Argumento pavadinimas netinkamas.", + "argument-name-duplicate": "Toks argumento pavadinimas jau egzistuoja.", + "argument-name-max-length": "Argumento pavadinimas turi būti trumpesnis nei 256 simboliai.", + "argument-name-forbidden": "Argumento pavadinimas yra rezervuotas ir negali būti naudojamas.", + "argument-type-required": "Reikalingas argumento tipas.", + "max-args": "Pasiektas didžiausias leidžiamų argumentų skaičius.", + "decimals-range": "Numatytasis dešimtainių skaičius turi būti tarp 0 ir 15.", + "expression": "Numatytoji išraiška parodo, kaip paversti temperatūrą iš Farenheito į Celsijų.", + "arguments-entity-not-found": "Argumento paskirties subjektas nerastas.", + "use-latest-timestamp": "Jei įjungta, apskaičiuota reikšmė bus išsaugota naudojant naujausią argumentų telemetrijos laiko žymą, o ne serverio laiką." + } + }, + "ai-models": { + "ai-models": "AI modeliai", + "ai-model": "AI modelis", + "model": "Modelis", + "name": "Pavadinimas", + "ai-provider": "AI tiekėjas", + "no-found": "AI modelių nerasta", + "list": "{ count, plural, =1 {Vienas modelis} other {# modelių sąrašas} }", + "selected-fields": "{ count, plural, =1 {1 modelis} other {# modeliai} } pasirinkta", + "add": "Pridėti modelį", + "delete-model-title": "Ar tikrai norite pašalinti modelį '{{modelName}}'?", + "delete-model-text": "Būkite atsargūs — po patvirtinimo modelis ir visa su juo susijusi informacija bus negrįžtamai pašalinta.", + "delete-models-title": "Ar tikrai norite pašalinti { count, plural, =1 {1 modelį} other {# modelius} }?", + "delete-models-text": "Būkite atsargūs — po patvirtinimo visi pasirinkti modeliai bus pašalinti kartu su visa susijusia informacija.", + "ai-providers": { + "openai": "OpenAI", + "azure-openai": "Azure OpenAI", + "google-ai-gemini": "Google AI Gemini", + "google-vertex-ai-gemini": "Google Vertex AI Gemini", + "mistral-ai": "Mistral AI", + "anthropic": "Anthropic", + "amazon-bedrock": "Amazon Bedrock", + "github-models": "GitHub Modeliai" + }, + "name-required": "Pavadinimas yra privalomas.", + "name-max-length": "Pavadinimas turi būti ne ilgesnis nei 255 simboliai.", + "provider": "Tiekėjas", + "api-key": "API raktas", + "api-key-required": "API raktas yra privalomas.", + "project-id": "Projekto ID", + "project-id-required": "Projekto ID yra privalomas.", + "location": "Vieta", + "location-required": "Vieta yra privaloma.", + "service-account-key-file": "Paslaugos paskyros rakto failas", + "service-account-key-file-required": "Paslaugos paskyros rakto failas yra privalomas.", + "no-file": "Failas nepasirinktas.", + "drop-file": "Vilkite failą arba spustelėkite, kad pasirinktumėte įkelti failą.", + "personal-access-token": "Asmeninis prieigos raktas", + "personal-access-token-required": "Asmeninis prieigos raktas yra privalomas.", + "configuration": "Konfigūracija", + "model-id": "Modelio ID", + "model-id-required": "Modelio ID yra privalomas.", + "deployment-name": "Diegimo pavadinimas", + "deployment-name-required": "Diegimo pavadinimas yra privalomas.", + "set": "Nustatyti", + "region": "Regionas", + "region-required": "Regionas yra privalomas.", + "access-key-id": "Prieigos rakto ID", + "access-key-id-required": "Prieigos rakto ID yra privalomas.", + "secret-access-key": "Slaptas prieigos raktas", + "secret-access-key-required": "Slaptas prieigos raktas yra privalomas.", + "temperature": "Temperatūra", + "temperature-hint": "Nustato modelio atsakymo atsitiktinumo lygį. Didesnės reikšmės didina atsitiktinumą, mažesnės – mažina.", + "temperature-min": "Turi būti 0 arba daugiau.", + "top-p": "Top P", + "top-p-hint": "Sukuria baseiną tikėtiniausių žetonų, iš kurių modelis gali rinktis. Didesnės reikšmės sukuria didesnį ir įvairesnį baseiną, mažesnės – siauresnį.", + "top-p-min-max": "Turi būti didesnis nei 0 ir neviršyti 1.", + "top-k": "Top K", + "top-k-hint": "Apriboja modelio pasirinkimus iki fiksuoto „K“ tikėtiniausių žetonų rinkinio.", + "top-k-min": "Turi būti 0 arba daugiau.", + "presence-penalty": "Buvo pasirodymo bauda", + "presence-penalty-hint": "Taikoma fiksuota bauda žetonui, jei jis jau buvo panaudotas tekste.", + "frequency-penalty": "Dažnio bauda", + "frequency-penalty-hint": "Taikoma bauda žetono tikimybei, kuri didėja priklausomai nuo jo pasikartojimo dažnio tekste.", + "max-output-tokens": "Maksimalus išvesties žetonų skaičius", + "max-output-tokens-hint": "Nustato didžiausią žetonų skaičių, kurį modelis gali sugeneruoti viename atsakyme.", + "endpoint": "Pabaigos taškas (Endpoint)", + "endpoint-required": "Pabaigos taškas yra privalomas.", + "service-version": "Paslaugos versija", + "check-connectivity": "Tikrinti ryšį", + "check-connectivity-success": "Bandomasis užklausimas sėkmingas", + "check-connectivity-failed": "Bandomasis užklausimas nepavyko", + "no-model-matching": "Modelių, atitinkančių '{{entity}}', nerasta.", + "model-required": "Modelis yra privalomas.", + "no-model-text": "Modelių nerasta." + }, + "confirm-on-exit": { + "message": "Turite neišsaugotų pakeitimų. Ar tikrai norite palikti šį puslapį?", + "html-message": "Turite neišsaugotų pakeitimų.
    Ar tikrai norite palikti šį puslapį?", + "title": "Neišsaugoti pakeitimai" + }, + "contact": { + "country": "Šalis", + "country-required": "Country is required.", + "city": "Miestas", + "state": "Rajonas", + "postal-code": "Pašto kodas", + "postal-code-invalid": "Neteisingas pašto kodo formatas.", + "address": "Adresas", + "address2": "Adresas 2", + "phone": "Telefonas", + "email": "El. paštas", + "no-address": "Adresas nenurodytas", + "no-country-found": "No countries found.", + "no-country-matching": "No country matching '{{country}}' were found.", + "state-max-length": "Rajono pavadinimas negali viršyti 256 simbolių", + "phone-max-length": "Telefono numeris negali viršyti 256 simbolių", + "city-max-length": "Miesto pavadinimas negali viršyti 256 simbolių" + }, + "common": { + "name": "Pavadinimas", + "type": "Tipas", + "general": "Bendra informacija", + "username": "Vartotojo vardas", + "password": "Slaptažodis", + "data": "Duomenys", + "timestamp": "Laiko žyma", + "enter-username": "Įveskite vartotojo vardą", + "enter-password": "Įveskite slaptažodį", + "enter-search": "Įveskite paieškos kriterijus", + "created-time": "Sukūrimo laikas", + "disabled": "Išjungta", + "loading": "Įkeliama...", + "proceed": "Tęsti", + "open-details-page": "Atidaryti informacijos puslapį", + "not-found": "Nerasta", + "value": "Reikšmė", + "documentation": "Dokumentacija", + "time-left": "Likęs laikas: {{time}}", + "output": "Išvestis", + "suffix": { + "s": "s", + "ms": "ms" + }, + "hint": { + "name-required": "Pavadinimas yra privalomas.", + "name-pattern": "Pavadinimas netinkamas.", + "name-max-length": "Pavadinimas turi būti trumpesnis nei 256 simboliai.", + "title-required": "Antraštė yra privaloma.", + "title-pattern": "Antraštė netinkama.", + "title-max-length": "Antraštė turi būti trumpesnė nei 256 simboliai.", + "key-required": "Raktas yra privalomas.", + "key-pattern": "Raktas netinkamas.", + "key-max-length": "Raktas turi būti trumpesnis nei 256 simboliai." + }, + "required-fields": "Trūksta privalomų laukų" + }, + "content-type": { + "json": "Json", + "text": "Tekstas", + "binary": "Binarinis (Base64)" + }, + "color": { + "color": "Spalva" + }, + "customer": { + "customer": "Klientas", + "customers": "Klientai", + "management": "Klientų valdymas", + "dashboard": "Kliento skydelis", + "dashboards": "Klientų skydeliai", + "devices": "Kliento įrenginiai", + "entity-views": "Kliento subjektų rodiniai", + "assets": "Kliento turtas", + "public-dashboards": "Vieši skydeliai", + "public-devices": "Vieši įrenginiai", + "public-assets": "Viešas turtas", + "public-entity-views": "Vieši subjektų rodiniai", + "add": "Pridėti klientą", + "delete": "Pašalinti klientą", + "manage-customer-users": "Kliento vartotojai", + "manage-customer-devices": "Kliento įrenginiai", + "manage-customer-dashboards": "Kliento skydeliai", + "manage-public-devices": "Vieši įrenginiai", + "manage-public-dashboards": "Vieši skydeliai", + "manage-customer-assets": "Kliento turtas", + "manage-customer-edges": "Kliento Edge įrenginiai", + "manage-public-assets": "Viešas turtas", + "add-customer-text": "Pridėti naują klientą", + "no-customers-text": "Klientų nėra", + "customer-details": "Informacija apie klientą", + "delete-customer-title": "Ar tikrai norite pašalinti klientą '{{customerTitle}}'?", + "delete-customer-text": "Būkite dėmesingi – po patvirtinimo kliento ir su juo susijusios informacijos atkurti nebegalėsite.", + "delete-customers-title": "Ar tikrai norite pašalinti { count, plural, =1 {1 klientą} other {# klientus} }?", + "delete-customers-action-title": "Pašalinti { count, plural, =1 {1 klientą} other {# klientus} }", + "delete-customers-text": "Būkite dėmesingi – po patvirtinimo klientų ir su jais susijusios informacijos atkurti nebegalėsite.", + "manage-users": "Vartotojai", + "manage-assets": "Turtas", + "manage-devices": "Įrenginiai", + "manage-dashboards": "Skydeliai", + "title": "Pavadinimas", + "title-required": "Pavadinimas yra privalomas.", + "title-max-length": "Pavadinimas negali viršyti 256 simbolių.", + "description": "Aprašymas", + "details": "Informacija", + "events": "Įvykiai", + "copyId": "Kopijuoti kliento ID", + "idCopiedMessage": "Kliento ID nukopijuotas į iškarpinę", + "select-customer": "Pasirinkti klientą", + "no-customers-matching": "Klientų, atitinkančių '{{entity}}', nerasta.", + "customer-required": "Klientas yra privalomas", + "select-default-customer": "Pasirinkite numatytąjį klientą", + "default-customer": "Numatytasis klientas", + "default-customer-required": "Numatytasis klientas būtinas, norint suderinti prietaisų skydelį valdytojo lygiu", + "search": "Klientų paieška", + "selected-customers": "Pasirinkta { count, plural, =1 {1 klientas} other {# klientai} }", + "edges": "Kliento Edge įrenginiai", + "manage-edges": "Valdyti Edge įrenginius" + }, + "css-size": { + "size-value-required": "Dydžio reikšmė yra privaloma", + "invalid-size-value": "Neteisinga dydžio reikšmė" + }, + "date": { + "last-update-n-ago": "Paskutinis atnaujinimas prieš N", + "last-update-n-ago-text": "Paskutinis atnaujinimas prieš {{ agoText }}", + "custom-date": "Pasirinkta data", + "format": "Formatas", + "preview": "Peržiūra", + "auto": "Automatinis", + "time-granularity-formats": "Laiko granuliacijos formatai", + "unit-year": "Metai", + "unit-month": "Mėnesiai", + "unit-day": "Dienos", + "unit-hour": "Valandos", + "unit-minute": "Minutės", + "unit-second": "Sekundės", + "unit-millisecond": "Milisekundės" + }, + "datetime": { + "date-from": "Data nuo", + "time-from": "Laikas nuo", + "date-to": "Data iki", + "time-to": "Laikas iki", + "from": "Nuo", + "to": "Iki" + }, + "dashboard": { + "dashboard": "Skydelis", + "dashboards": "Skydeliai", + "management": "Skydelių valdymas", + "view-dashboards": "Peržiūrėti skydelius", + "add": "Pridėti skydelį", + "assign-dashboard-to-customer": "Skydelį (-ius) priskirti klientui", + "assign-dashboard-to-customer-text": "Pasirinkite skydelius, kuriuos norite priskirti klientui", + "assign-to-customer-text": "Pasirinkite klientą, kuriam priskirti skydelį (-ius)", + "assign-to-customer": "Priskirti klientui", + "unassign-from-customer": "Atsieti nuo kliento", + "make-public": "Skydelį padaryti viešą", + "make-private": "Skydelį padaryti privatų", + "manage-assigned-customers": "Valdyti priskirtus klientus", + "assigned-customers": "Priskirti klientai", + "assign-to-customers": "Skydelį (-ius) priskirti klientams", + "assign-to-customers-text": "Pasirinkite klientus, kuriems priskirti skydelį (-ius)", + "unassign-from-customers": "Skydelį (-ius) atsieti nuo klientų", + "unassign-from-customers-text": "Pasirinkite klientus, kuriems atsieti skydelį (-ius)", + "no-dashboards-text": "Skydelių nėra", + "no-widgets": "Valdikliai nesukonfigūruoti", + "add-widget": "Pridėti naują valdiklį", + "add-widget-button-text": "Pridėti valdiklį", + "title": "Pavadinimas", + "image": "Skydelio paveikslėlis", + "mobile-app-settings": "Mobiliosios programėlės nustatymai", + "mobile-order": "Skydelio eiliškumas mobiliojoje programėlėje", + "mobile-hide": "Skydelį slėpti mobiliojoje programėlėje", + "update-image": "Atnaujinti skydelio paveikslėlį", + "take-screenshot": "Padaryti ekrano nuotrauką", + "select-widget-title": "Pasirinkite valdiklį", + "select-widget-value": "{{title}}: pasirinkite valdiklį", + "select-widget-subtitle": "Galimų valdiklių sąrašas", + "delete": "Pašalinti skydelį", + "title-required": "Pavadinimas būtinas.", + "title-max-length": "Pavadinimas negali viršyti 256 simbolių", + "description": "Aprašymas", + "details": "Informacija", + "dashboard-details": "Skydelio informacija", + "add-dashboard-text": "Pridėti naują skydelį", + "assign-dashboards": "Priskirti skydelius", + "assign-new-dashboard": "Priskirti naują skydelį", + "assign-dashboards-text": "Priskirti { count, plural, =1 {1 skydelį} other {# skydelius} } klientams", + "unassign-dashboards-action-text": "Atsieti { count, plural, =1 {1 skydelį} other {# skydelius} } nuo klientų", + "delete-dashboards": "Panaikinti skydelius", + "unassign-dashboards": "Atsieti skydelius", + "unassign-dashboards-action-title": "Atsieti { count, plural, =1 {1 skydelį} other {# skydelius} } nuo kliento", + "delete-dashboard-title": "Ar tikrai norite pašalinti skydelį '{{dashboardTitle}}'?", + "delete-dashboard-text": "Būkite dėmesingi, po patvirtinimo skydelio ir su juo susijusios informacijos atkurti nebegalėsite.", + "delete-dashboards-title": "Ar tikrai norite pašalinti { count, plural, =1 {1 skydelį} other {# skydelius} }?", + "delete-dashboards-action-title": "Pašalinti { count, plural, =1 {1 skydelį} other {# skydelius} }", + "delete-dashboards-text": "Būkite dėmesingi, po patvirtinimo pasirinkti skydeliai ir su jais susijusi informacija bus pašalinti ir jų atkurti nebegalėsite.", + "unassign-dashboard-title": "Ar tikrai norite atsieti skydelį '{{dashboardTitle}}'?", + "unassign-dashboard-text": "Po patvirtinimo skydelis bus atsietas ir klientas jo nebematys.", + "unassign-dashboard": "Atsieti skydelį", + "unassign-dashboards-title": "Ar tikrai norite atsieti { count, plural, =1 {1 skydelį} other {# skydelius} }?", + "unassign-dashboards-text": "Po patvirtinimo visi skydeliai bus atsieti ir klientas jų nebematys.", + "public-dashboard-title": "Skydelis dabar yra viešas", + "public-dashboard-text": "Skydelis {{dashboardTitle}} dabar yra viešas ir pasiekiamas per šią nuorodą:", + "public-dashboard-notice": "Pastaba: Įrenginiai turi būti vieši, jei norite matyti jų duomenis.", + "make-private-dashboard-title": "Ar tikrai norite skydelį '{{dashboardTitle}}' padaryti privačiu?", + "make-private-dashboard-text": "Po patvirtinimo skydelis taps privačiu ir jo nematys kiti vartotojai.", + "make-private-dashboard": "Skydelį padaryti privačiu", + "socialshare-text": "'{{dashboardTitle}}' sukurta naudojant ThingsBoard", + "socialshare-title": "'{{dashboardTitle}}' sukurta naudojant ThingsBoard", + "select-dashboard": "Pasirinkti skydelį", + "no-dashboards-matching": "Skydelio, atitinkančio '{{entity}}', nėra.", + "dashboard-required": "Skydelis būtinas.", + "select-existing": "Pasirinkti esamą skydelį", + "create-new": "Sukurti naują skydelį", + "new-dashboard-title": "Naujo skydelio pavadinimas", + "open-dashboard": "Atidaryti skydelį", + "set-background": "Nustatyti foną", + "background-color": "Fono spalva", + "background-image": "Fono paveikslėlis", + "background-size-mode": "Fono dydis", + "no-image": "Paveikslėlis nepasirinktas", + "empty-image": "Nėra paveikslėlio", + "drop-image": "Užvilkite paveikslėlį arba spustelėkite ir pasirinkite failą.", + "maximum-upload-file-size": "Maksimalus įkeliamo failo dydis: {{ size }}", + "cannot-upload-file": "Failo įkelti nepavyko", + "settings": "Nustatymai", + "move-all-widgets": "Perkelti visus valdiklius", + "move-by": "Perkelti per", + "cols": "Stulpeliai", + "rows": "Eilutės", + "layout": "Išdėstymas", + "layout-type-default": "Numatytasis", + "layout-type-scada": "SCADA", + "layout-type-divider": "Skyriklis", + "layout-settings-type": "Išdėstymo nustatymai: {{ type }} lūžio taškas", + "columns-count": "Stulpelių skaičius", + "columns-count-required": "Stulpelių skaičius būtinas.", + "min-columns-count-message": "Minimalus stulpelių skaičius - 10.", + "max-columns-count-message": "Maksimalus stulpelių skaičius - 1000.", + "min-layout-width": "Minimalus išdėstymo plotis", + "columns-suffix": "stulpeliai", + "widgets-margins": "Paraštės tarp valdiklių dydis", + "margin-required": "Paraštės dydžio reikšmė būtina.", + "min-margin-message": "Minimali paraštės dydžio reikšmė - 0.", + "max-margin-message": "Maksimali paraštės dydžio reikšmė - 50.", + "horizontal-margin": "Horizontalios paraštės dydis", + "horizontal-margin-required": "Horizontalios paraštės dydžio reikšmė būtina.", + "min-horizontal-margin-message": "Minimali horizontalios paraštės dydžio reikšmė - 0", + "max-horizontal-margin-message": "Maksimali horizontalios paraštės dydžio reikšmė - 50.", + "vertical-margin": "Vertikalios paraštės dydis", + "vertical-margin-required": "Vertikalios paraštės dydžio reikšmė būtina.", + "min-vertical-margin-message": "Minimali vertikalios paraštės dydžio reikšmė - 0.", + "max-vertical-margin-message": "Maksimali vertikalios paraštės dydžio reikšmė - 50.", + "apply-outer-margin": "Taikyti paraštes maketo šonuose", + "autofill-height": "Automatiškai pritaikyti aukštį", + "mobile-layout": "Mobiliojo režimo išdėstymo nustatymai", + "mobile-row-height": "Mobiliojo režimo eilutės aukštis pikseliais", + "mobile-row-height-required": "Mobiliojo režimo eilutės aukštis būtinas.", + "min-mobile-row-height-message": "Minimalus mobiliojo režimo eilutės aukštis yra 5 pikseliai.", + "max-mobile-row-height-message": "Maksimalus mobiliojo režimo eilutės aukštis yra 200 pikselių.", + "row-height": "Eilutės aukštis", + "row-height-required": "Eilutės aukščio reikšmė yra privaloma.", + "min-row-height-message": "Mažiausias eilutės aukštis – 5 pikseliai.", + "max-row-height-message": "Didžiausias eilutės aukštis – 200 pikselių.", + "display-first-in-mobile-view": "Rodyti pirmą mobiliajame rodinyje", + "title-settings": "Pavadinimo nustatymai", + "display-title": "Rodyti skydelio pavadinimą", + "title-color": "Pavadinimo spalva", + "toolbar-settings": "Įrankių juostos nustatymai", + "hide-toolbar": "Paslėpti įrankių juostą", + "toolbar-always-open": "Įrankių juostą laikyti atidarytą", + "display-dashboards-selection": "Rodyti skydelių pasirinkimą", + "display-entities-selection": "Rodyti subjektų pasirinkimą", + "display-filters": "Rodyti filtrus", + "display-dashboard-timewindow": "Rodyti laiko langą", + "display-dashboard-export": "Rodyti eksportą", + "display-update-dashboard-image": "Rodyti skydelio paveikslėlio atnaujinimą", + "dashboard-logo-settings": "Skydelio logotipo nustatymai", + "display-dashboard-logo": "Rodyti logotipą skydelio viso ekrano režime", + "dashboard-logo-image": "Skydelio logotipo paveikslėlis", + "advanced-settings": "Išplėstiniai nustatymai", + "dashboard-css": "Skydelio CSS", + "import": "Importuoti skydelį", + "export": "Eksportuoti skydelį", + "export-failed-error": "Skydelio eksportuoti nepavyko: {{error}}", + "export-prompt": "Įterpti skydelio paveikslėlius ir išteklius", + "create-new-dashboard": "Sukurti naują skydelį", + "dashboard-file": "Skydelio failas", + "invalid-dashboard-file-error": "Skydelio importuoti nepavyko: neteisinga duomenų struktūra.", + "dashboard-import-missing-aliases-title": "Konfigūruoti importuoto skydelio naudojamus pseudonimus", + "create-new-widget": "Sukurti naują valdiklį", + "import-widget": "Importuoti valdiklį", + "widget-file": "Valdiklio failas", + "invalid-widget-file-error": "Valdiklio importuoti nepavyko: neteisinga duomenų struktūra.", + "widget-import-missing-aliases-title": "Konfigūruoti importuoto valdiklio naudojamus pseudonimus", + "open-toolbar": "Atidaryti skydelio įrankių juostą", + "close-toolbar": "Uždaryti įrankių juostą", + "configuration-error": "Konfigūracijos klaida", + "alias-resolution-error-title": "Skydelio pseudonimų konfigūracijos klaida", + "invalid-aliases-config": "Įrenginių, atitinkančių pseudonimų filtrus, nėra.
    Susisiekite su sistemos administratoriumi.", + "select-devices": "Pasirinkite įrenginius", + "assignedToCustomer": "Priskirta klientui", + "assignedToCustomers": "Priskirta klientams", + "public": "Viešas", + "copyId": "Kopijuoti skydelio Id", + "idCopiedMessage": "Skydelio Id nukopijuotas į iškarpinę", + "public-link": "Vieša nuoroda", + "copy-public-link": "Kopijuoti viešą nuorodą", + "public-link-copied-message": "Skydelio vieša nuoroda nukopijuota į iškarpinę", + "manage-states": "Valdyti skydelio būsenas", + "states": "Skydelio būsenos", + "states-short": "Būsenos", + "search-states": "Skydelio būsenų paieška", + "selected-states": "Pasirinkta skydelio { count, plural, =1 {1 būsena} other {# būsenos} }", + "edit-state": "Redaguoti skydelio būseną", + "delete-state": "Panaikinti skydelio būseną", + "add-state": "Pridėti skydelio būseną", + "no-states-text": "Būsenų nėra", + "state": "Skydelio būsena", + "state-name": "Pavadinimas", + "state-name-required": "Skydelio pavadinimas būtinas.", + "state-id": "Būsenos Id", + "state-id-required": "Būsenos Id būtinas.", + "state-id-exists": "Skydelio būsena su tokiu pačiu Id jau yra.", + "is-root-state": "Pagrindinė būsena", + "delete-state-title": "Panaikinti skydelio būseną", + "delete-state-text": "Ar tikrai norite panaikinti skydelio būseną '{{stateName}}'?", + "show-details": "Rodyti detales", + "hide-details": "Slėpti detales", + "select-state": "Pasirinkite būseną", + "state-controller": "Būsenų valdiklis", + "state-controller-default": "static (pasenęs)", + "search": "Skydelių paieška", + "selected-dashboards": "Pasirinkta { count, plural, =1 {1 skydelis} other {# skydeliai} }", + "home-dashboard": "Pagrindinis skydelis", + "home-dashboard-hide-toolbar": "Slėpti pagrindinio skydelio įrankių juostą", + "unassign-dashboard-from-edge-text": "Po patvirtinimo skydelis bus atsietas ir nebus pasiekiamas Edge įrenginio.", + "unassign-dashboards-from-edge-title": "Ar tikrai norite atsieti { count, plural, =1 {1 skydelį} other {# skydelius} }?", + "unassign-dashboards-from-edge-text": "Po patvirtinimo visi pasirinkti skydeliai bus atsieti ir nebus pasiekiami Edge įrenginio.", + "assign-dashboard-to-edge": "Priskirti skydelį (-ius) Edge įrenginiui", + "assign-dashboard-to-edge-text": "Pasirinkite skydelius, kuriuos priskirti Edge įrenginiui", + "non-existent-dashboard-state-error": "Skydelio būsena su id „{{ stateId }}“ nerasta", + "edit-mode": "Redagavimo režimas", + "duplicate-state-action": "Dubliuoti būseną", + "breakpoint-value": "Lūžio taškas ({{ value }})", + "breakpoints-id": { + "default": "Numatytasis", + "xs": "Mobilusis (xs)", + "sm": "Planšetė (sm)", + "md": "Nešiojamas (md)", + "lg": "Stalinis (lg)", + "xl": "Stalinis (xl)" + }, + "view-format-type-grid": "Tinklelis", + "view-format-type-list": "Sąrašas", + "view-format": "Rodinio formatas" + }, + "datakey": { + "settings": "Nustatymai", + "general": "Bendra", + "advanced": "Pažangūs nustatymai", + "key": "Raktas", + "keys": "Raktai", + "label": "Etiketė", + "color": "Spalva", + "units": "Vienetai (simboliai, rodomi šalia reikšmės)", + "decimals": "Skaitmenų po kablelio kiekis", + "data-generation-func": "Duomenų generavimo funkcija", + "use-data-post-processing-func": "Naudoti papildomo duomenų apdorojimo funkciją", + "configuration": "Duomenų raktų konfigūracija", + "timeseries": "Telemetrija", + "attributes": "Atributai", + "entity-field": "Subjekto laukas", + "alarm": "Įspėjimo laukai", + "timeseries-required": "Subjekto telemetrija būtina.", + "timeseries-or-attributes-required": "Subjekto telemetrija arba atributai būtini.", + "alarm-fields-timeseries-or-attributes-required": "Įspėjimo laukai arba subjekto telemetrija/atributai būtini.", + "maximum-timeseries-or-attributes": "Leidžiama daugiausiai { count, plural, =1 {1 telemetrija/atributas} other {# telemetrijų/atributų} }.", + "alarm-fields-required": "Įspėjimo laukai būtini.", + "function-types": "Funkcijų tipai", + "function-type": "Funkcijos tipas", + "function-types-required": "Funkcijos tipas būtinas.", + "data-keys": "Duomenų raktai", + "data-key": "Duomenų raktas", + "data-keys-required": "Duomenų raktai būtini.", + "data-key-required": "Duomenų raktas būtinas.", + "alarm-keys": "Įspėjimo duomenų raktai", + "alarm-key": "Įspėjimo duomenų raktas", + "alarm-key-functions": "Įspėjimo rakto funkcijos", + "alarm-key-function": "Įspėjimo rakto funkcija", + "latest-keys": "Naujausi duomenų raktai", + "latest-key": "Naujausias duomenų raktas", + "latest-key-functions": "Naujausio rakto funkcijos", + "latest-key-function": "Naujausio rakto funkcija", + "timeseries-keys": "Telemetrijos duomenų raktai", + "timeseries-key": "Telemetrijos duomenų raktas", + "timeseries-key-functions": "Telemetrijos rakto funkcijos", + "timeseries-key-function": "Telemetrijos rakto funkcija", + "maximum-function-types": "Daugiausia leidžiama { count, plural, =1 {1 funkcijos tipas} other {# funkcijos tipai} }.", + "time-description": "Dabartinės vertės laiko žyma;", + "value-description": "Dabartinė vertė;", + "prev-value-description": "Ankstesnio funkcijos kvietimo rezultatas;", + "time-prev-description": "Ankstesnės vertės laiko žyma;", + "prev-orig-value-description": "Pradinė ankstesnė vertė;", + "aggregation": "Agregavimas", + "aggregation-type-hint-common": "Dėl našumo priežasčių agreguotų reikšmių skaičiavimas galimas tik fiksuotiems laiko intervalams, pvz., 'dabartinė diena', 'dabartinis mėnuo' ir pan. Tai netaikoma slankiesiems intervalams, kaip 'paskutinės 30 min' ar 'paskutinės 24 val'.", + "aggregation-type-none-hint": "Naudoti naujausią reikšmę.", + "aggregation-type-min-hint": "Rasti mažiausią reikšmę tarp duomenų taškų pasirinktoje laiko atkarpoje.", + "aggregation-type-max-hint": "Rasti didžiausią reikšmę tarp duomenų taškų pasirinktoje laiko atkarpoje.", + "aggregation-type-avg-hint": "Apskaičiuoti vidurkį tarp duomenų taškų pasirinktoje laiko atkarpoje.", + "aggregation-type-sum-hint": "Suskaičiuoti visų duomenų taškų reikšmių sumą pasirinktoje laiko atkarpoje.", + "aggregation-type-count-hint": "Duomenų taškų skaičius pasirinktoje laiko atkarpoje.", + "delta-calculation": "Delta skaičiavimas", + "enable-delta-calculation": "Įjungti delta skaičiavimą", + "enable-delta-calculation-hint": "Kai įjungta, rakto reikšmė skaičiuojama pagal agreguotas reikšmes pasirinktam laikotarpiui ir lyginimo periodui. Dėl našumo delta skaičiavimas galimas tik istoriniams laikotarpiams, o ne realaus laiko reikšmėms. Pvz., galima apskaičiuoti skirtumą tarp vakarykščio ir užvakar dienos energijos suvartojimo.", + "delta-calculation-result": "Delta skaičiavimo rezultatas", + "delta-calculation-result-previous-value": "Ankstesnė reikšmė", + "delta-calculation-result-delta-absolute": "Delta (absoliuti)", + "delta-calculation-result-delta-percent": "Delta (procentinė)", + "source": "Šaltinis", + "latest": "Naujausia", + "latest-value": "Naujausia reikšmė", + "delta": "delta", + "percent": "procentai", + "absolute": "absoliuti" + }, + "datasource": { + "type": "Duomenų šaltinio tipas", + "name": "Pavadinimas", + "label": "Etiketė", + "add-datasource-prompt": "Pridėkite duomenų šaltinį" + }, + "details": { + "details": "Informacija", + "edit-mode": "Redagavimo režimas", + "edit-json": "Redaguoti JSON", + "toggle-edit-mode": "Redagavimo režimas" + }, + "device": { + "device": "Įrenginys", + "device-required": "Įrenginys būtinas.", + "devices": "Įrenginiai", + "management": "Įrenginių valdymas", + "view-devices": "Peržiūrėti įrenginius", + "device-alias": "Įrenginio pseudonimas", + "device-type-max-length": "Įrenginio tipas negali viršyti 256 simbolių", + "aliases": "Įrenginių pseudonimai aliases", + "no-alias-matching": "'{{alias}}' nerasta.", + "no-aliases-found": "Pseudonimų nėra.", + "no-key-matching": "'{{key}}' nerasta.", + "no-keys-found": "Raktų nėra.", + "create-new-alias": "Sukurti naują!", + "create-new-key": "Sukurti naują!", + "duplicate-alias-error": "'{{alias}}' dubliuojasi.
    Įrenginio pseudonimai skydelyje turi būti unikalūs.", + "configure-alias": "Sukonfigūruoti '{{alias}}' pseudonimą", + "no-devices-matching": "Įrenginių, attinkančių '{{entity}}' nėra.", + "alias": "Pseudonimas", + "alias-required": "Įrenginio pseudonimas būtinas.", + "remove-alias": "Panaikinti įrenginio pseudonimą", + "add-alias": "Pridėti įrenginio pseudonimą", + "name-starts-with": "Įrenginio pavadinimas prasideda", + "help-text": "Naudokite '%' simbolį pagal tai, kaip norite ieškoti: '%įrenginio_pavadinimo_fragmentas%', '%įrenginio_pavadinimo_pabaiga', 'įrenginio_pavadinimo_pradžia%'.", + "device-list": "Įrenginių sąrašas", + "use-device-name-filter": "Naudoti filtrą", + "device-list-empty": "Įrenginiai nepasirinkti.", + "device-name-filter-required": "Įrenginio pavadinimo filtras turi būti nustatytas.", + "device-name-filter-no-device-matched": "Įrenginių, kurių pavadinimas prasideda '{{device}}' nėra.", + "add": "Pridėti įrenginį", + "assign-to-customer": "Priskirti kientui", + "assign-device-to-customer": "Įrenginį (-ius) priskirti klientui", + "assign-device-to-customer-text": "Pasirinkite įrenginius, kuriuos norite priskirti klientui", + "make-public": "Įrenginį padaryti viešu", + "make-private": "Įrenginį padaryti privačiu", + "no-devices-text": "Įrenginių nėra", + "assign-to-customer-text": "Pasirinkite klientą, kuriam norite priskirti įrenginį (-ius)", + "device-details": "Įrenginio informacija", + "add-device-text": "Pridėti naują įrenginį", + "credentials": "Įgaliojimai", + "manage-credentials": "Valdyti įgaliojimus", + "delete": "Panaikinti įrenginį", + "assign-devices": "priskirti įrenginius", + "assign-devices-text": "Priskirti { count, plural, =1 {1 įrenginį} other {# įrenginius} } klientui", + "delete-devices": "Panaikinti įrenginius", + "unassign-from-customer": "Atseti nuo kliento", + "unassign-devices": "Atsieti įrenginius", + "unassign-devices-action-title": "Atsieti { count, plural, =1 {1 įrenginį} other {# įrenginius} } nuo kliento", + "unassign-device-from-edge-title": "Are you sure you want to unassign the device '{{deviceName}}'?", + "unassign-device-from-edge-text": "After the confirmation the device will be unassigned and won't be accessible by the edge.", + "unassign-devices-from-edge": "Unassign devices from edge", + "assign-new-device": "Priskirti naują įrenginį", + "make-public-device-title": "Ar tikrai norite įrenginį '{{deviceName}}' padaryti viešu?", + "make-public-device-text": "Po patvirtinimo įrenginys ir visi jo duomenys bus vieši ir prieinami kitiems vartotojams.", + "make-private-device-title": "Ar tikrai norite įrenginį '{{deviceName}}' padaryti privačiu?", + "make-private-device-text": "Po patvirtinimo įrenginys ir visi jo duomenys taps privatūs ir nebus prieinami kitiems vartotojams.", + "view-credentials": "Peržiūrėti įgaliojimus", + "delete-device-title": "Ar tikrai norite panaikinti įrenginį '{{deviceName}}'?", + "delete-device-text": "Būkite dėmesingi, po patvirtinimo įrenginys ir visa su juo susijusi informacija bus panaikinta ir jų atkurti nebegalėsite", + "delete-devices-title": "Ar tikrai norite panaikinti { count, plural, =1 {1 įrenginį} other {# įrenginius} }?", + "delete-devices-action-title": "Panaikinti { count, plural, =1 {1 įrenginį} other {# įrenginius} }", + "delete-devices-text": "Būkite dėmesingi, po patvirtinimo, visi pasirinkti įrenginiai ir su jais susijusi informacija bus panaikinta ir jų atkurti nebegalėsite.", + "unassign-device-title": "Ar tikrai norite atsieti įrenginį '{{deviceName}}'?", + "unassign-device-text": "Po patvirtinimo įrenginys bus atsietas ir klientas jo nebematys.", + "unassign-device": "Atsieti įrenginį", + "unassign-devices-title": "Ar tikrai norite atsieti { count, plural, =1 {1 įrenginį} other {# įrenginius} }?", + "unassign-devices-text": "Po patvirtinimo visi pasirinkti įrenginiai bus atsieti nuo kliento.", + "device-credentials": "įrenginio įgaliojimai", + "loading-device-credentials": "Įkeliami įrenginio įgaliojimai...", + "credentials-type": "Įgaliojimų tipas", + "access-token": "Prieigos raktas", + "access-token-required": "Prieigos raktas būtinas.", + "access-token-invalid": "Prieigos rakto ilgis turi būti nuo 1 iki 32 simbolių.", + "certificate-pem-format": "Sertifikatas PEM formate", + "certificate-pem-format-required": "Certificate is required.", + "copy-access-token": "Kopijuoti prieigos raktą", + "copy-certificate": "Kopijuoti sertifikatą", + "copy-client-id": "Kopijuoti kliento ID", + "copy-user-name": "Kopijuoti vartotojo vardą", + "copy-password": "Kopijuoti slaptažodį", + "generate-client-id": "Generuoti kliento ID", + "generate-user-name": "Generuoti vartotojo vardą", + "generate-password": "Generuoti slaptažodį", + "generate-access-token": "Generuoti prieigos raktą", + "lwm2m-security-config": { + "identity": "Kliento identifikatorius", + "identity-required": "Kliento identifikatorius būtinas.", + "identity-tooltip": "PSK identifikatorius yra savavališkai parinktas iki 128 baitų identifikatorius, kaip aprašyta standarte [RFC7925].\nPSK identifikatorius pirmiausia turi būti konvertuotas į simbolių eilutę ir tada užkoduotas į baitus naudojant UTF-8.", + "client-key": "Kliento raktas", + "client-key-required": "Kliento raktas būtinas.", + "client-key-tooltip-prk": "RPK viešasis raktas arba ID turi atitikti standartą [RFC7250] ir būti užkoduotas Base64 formatu!", + "client-key-tooltip-psk": "PSK raktas turi atitikti standartą [RFC4279] ir būti HexDec formatu: 32, 64 arba 128 simboliai!", + "endpoint": "Kliento galinio taško pavadinimas (Endpoint)", + "endpoint-required": "Kliento galinio taško pavadinimas būtinas.", + "client-public-key": "Kliento viešasis raktas", + "client-public-key-hint": "Jei viešasis raktas nenurodytas, bus naudojamas patikimas sertifikatas.", + "client-public-key-tooltip": "X509 viešasis raktas turi būti DER koduotas X509v3 formatu, naudoti tik EC algoritmą ir būti užkoduotas Base64 formatu!", + "mode": "Saugumo konfigūracijos režimas", + "client-tab": "Kliento saugumo konfigūracija", + "client-certificate": "Kliento sertifikatas", + "bootstrap-tab": "Bootstrap klientas", + "bootstrap-server": "Bootstrap serveris", + "lwm2m-server": "LwM2M serveris", + "client-publicKey-or-id": "Kliento viešasis raktas arba ID", + "client-publicKey-or-id-required": "Kliento viešasis raktas arba ID būtinas.", + "client-publicKey-or-id-tooltip-psk": "PSK identifikatorius yra savavališkai parinktas iki 128 baitų identifikatorius, kaip aprašyta standarte [RFC7925].\nPSK identifikatorius pirmiausia turi būti konvertuotas į simbolių eilutę ir tada užkoduotas į baitus naudojant UTF-8.", + "client-publicKey-or-id-tooltip-rpk": "RPK viešasis raktas arba ID turi atitikti standartą [RFC7250] ir būti užkoduotas Base64 formatu!", + "client-publicKey-or-id-tooltip-x509": "X509 viešasis raktas turi būti DER koduotas X509v3 formatu, naudoti tik EC algoritmą ir būti užkoduotas Base64 formatu!", + "client-secret-key": "Kliento slaptasis raktas", + "client-secret-key-required": "Kliento slaptasis raktas būtinas.", + "client-secret-key-tooltip-psk": "PSK raktas turi atitikti standartą [RFC4279] ir būti HexDec formatu: 32, 64 arba 128 simboliai!", + "client-secret-key-tooltip-prk": "RPK slaptasis raktas turi būti PKCS_8 formatu (DER kodavimas, standartas [RFC5958]) ir būti užkoduotas Base64 formatu!", + "client-secret-key-tooltip-x509": "X509 slaptasis raktas turi būti PKCS_8 formatu (DER kodavimas, standartas [RFC5958]) ir būti užkoduotas Base64 formatu!" + }, + "client-id": "Kliento ID", + "client-id-pattern": "Yra netinkamas simbolis.", + "user-name": "Vartotojo vardas", + "user-name-required": "Vartotojo vardas būtinas.", + "client-id-or-user-name-necessary": "Kliento ID ir/arba vartotojo vardas būtini.", + "password": "Slaptažodis", + "secret": "Slaptas raktas", + "secret-required": "Slaptas raktas būtinas.", + "device-type": "Įrenginio tipas", + "device-type-required": "Įrenginio tipas būtinas.", + "select-device-type": "Pasirinkite įrenginio tipą", + "enter-device-type": "Įveskite įrenginio tipą", + "any-device": "Bet kuris įrenginys", + "no-device-types-matching": "Įrenginio tipų, atitinkančių '{{entitySubtype}}', nėra.", + "device-type-list-empty": "Nepasirinktas įrenginio tipas!", + "device-profile-type-list-empty": "Turi būti pasirinktas bent vienas įrenginio profilis.", + "device-types": "Įrenginių tipai", + "name": "Pavadinimas", + "name-required": "Pavadinimas būtinas.", + "name-max-length": "Pavadinimas negali viršyti 256 simbolių.", + "label-max-length": "Etiketė negali viršyti 256 simbolių.", + "description": "Aprašymas", + "label": "Etiketė", + "events": "Įvykiai", + "details": "Informacija", + "copyId": "Kopijuoti įrenginio ID", + "copyAccessToken": "Kopijuoti prieigos raktą", + "copy-mqtt-authentication": "Kopijuoti MQTT autentifikacijos duomenis", + "idCopiedMessage": "Įrenginio ID nukopijuotas į iškarpinę.", + "accessTokenCopiedMessage": "Įrenginio prieigos raktas nukopijuotas į iškarpinę.", + "mqtt-authentication-copied-message": "Įrenginio MQTT autentifikacijos duomenys nukopijuoti į iškarpinę.", + "assignedToCustomer": "Priskirtas klientui", + "unable-delete-device-alias-title": "Nepavyko panaikinti įrenginio pseudonimo", + "unable-delete-device-alias-text": "Įrenginio pseudonimo '{{deviceAlias}}' panaikinti nepavyko, nes jis naudojamas šiuose valdikliuose:
    {{widgetsList}}", + "is-gateway": "Yra šliuzas (Gateway)", + "overwrite-activity-time": "Perrašyti aktyvumo laiką prijungtam įrenginiui", + "device-filter": "Įrenginių filtras", + "device-filter-title": "Įrenginių filtras", + "filter-title": "Filtras", + "device-state": "Įrenginio būsena", + "state": "Būsena", + "any": "Visos", + "active": "Aktyvus", + "inactive": "Neaktyvus", + "public": "Viešas", + "device-public": "Įrenginys yra viešas", + "select-device": "Pasirinkite įrenginį", + "import": "Importuoti įrenginį", + "device-file": "Įrenginio failas", + "search": "Įrenginių paieška", + "selected-devices": "Pasirinkta { count, plural, =1 {1 įrenginys} other {# įrenginiai} }", + "device-configuration": "Įrenginio konfigūracija", + "transport-configuration": "Transporto konfigūracija", + "wizard": { + "device-details": "Informacija apie įrenginį" + }, + "unassign-devices-from-edge-title": "Ar tikrai norite atsieti { count, plural, =1 {1 įrenginį} other {# įrenginius} } nuo Edge?", + "unassign-devices-from-edge-text": "Po patvirtinimo visi pasirinkti įrenginiai bus atsieti ir nebebus pasiekiami per Edge.", + "time": "Laikas", + "connectivity": { + "check-connectivity": "Patikrinti ryšį", + "device-created-check-connectivity": "Įrenginys sukurtas. Patikrinkime ryšį!", + "loading-check-connectivity-command": "Įkeliamos ryšio tikrinimo komandos...", + "use-following-instructions": "Naudokite šias instrukcijas, kad išsiųstumėte telemetriją įrenginio vardu naudodami komandų eilutę (shell).", + "execute-following-command": "Vykdykite šią komandą", + "install-curl-windows": "Nuo Windows 10 versijos 17063, cURL įrankis prieinamas pagal numatymą.", + "install-curl-macos": "Nuo Mac OS X 10.2 (Jaguar) versijos, cURL įrankis prieinamas pagal numatymą.", + "install-mqtt-windows": "Naudokite instrukcijas, kad atsisiųstumėte, įdiegtumėte, sukonfigūruotumėte ir paleistumėte „mosquitto_pub“ įrankį.", + "install-coap-client": "Naudokite instrukcijas, kad atsisiųstumėte, įdiegtumėte, sukonfigūruotumėte ir paleistumėte „coap-client“ įrankį.", + "install-necessary-client-tools": "Įdiekite būtinus kliento įrankius", + "mqtts-x509-command": "Naudokite šią dokumentaciją, kad prijungtumėte įrenginį per MQTT su X.509 autorizacija.", + "coaps-x509-command": "Naudokite šią dokumentaciją, kad prijungtumėte įrenginį per CoAP (DTLS) su X.509 autorizacija.", + "snmp-command": "Naudokite šią dokumentaciją, kad prijungtumėte įrenginį per SNMP protokolą.", + "sparkplug-command": "Naudokite šią dokumentaciją, kad prijungtumėte įrenginį per MQTT Sparkplug protokolą.", + "lwm2m-command": "Naudokite šią dokumentaciją, kad prijungtumėte įrenginį per LwM2M protokolą." + } + }, + "dynamic-form": { + "property": { + "properties": "Savybės", + "property": "Savybė", + "id": "Id", + "name": "Pavadinimas", + "type": "Tipas", + "type-text": "Tekstas", + "type-password": "Slaptažodis", + "type-textarea": "Teksto sritis", + "type-number": "Skaičius", + "type-switch": "Perjungiklis", + "type-select": "Pasirinkimas", + "type-radios": "Žymimieji mygtukai", + "type-datetime": "Data / Laikas", + "type-image": "Paveikslėlis", + "type-javascript": "JavaScript", + "type-json": "JSON", + "type-html": "HTML", + "type-css": "CSS", + "type-markdown": "Markdown", + "type-color": "Spalva", + "type-color-settings": "Spalvų nustatymai", + "type-font": "Šriftas", + "type-units": "Vienetai", + "type-icon": "Piktograma", + "type-fieldset": "Lauko rinkinys (fieldset)", + "type-array": "Masyvas", + "type-html-section": "HTML sekcija", + "group-title": "Grupės pavadinimas", + "no-properties": "Savybės nesukonfigūruotos", + "add-property": "Pridėti savybę", + "property-settings": "Savybės nustatymai", + "remove-property": "Pašalinti savybę", + "default-value": "Numatytoji reikšmė", + "value-required": "Reikšmė būtina", + "number-settings": "Skaičiaus nustatymai", + "min": "Min", + "max": "Maks", + "step": "Žingsnis", + "selected-options-limit": "Pasirinktų parinkčių limitas", + "advanced-ui-settings": "Išplėstiniai UI nustatymai", + "disable-on-property": "Išjungti pagal savybę", + "disable-on-property-none": "Nėra (laukas visada įjungtas)", + "display-condition-function": "Rodymo sąlygos funkcija", + "sub-label": "Papildoma etiketė", + "vertical-divider-after": "Vertikalus skirtukas po elemento", + "input-field-suffix": "Įvesties lauko sufiksas", + "property-row-classes": "Savybės eilutės CSS klasės", + "property-field-classes": "Savybės lauko CSS klasės", + "not-unique-property-ids-error": "Savybių ID turi būti unikalūs!", + "enable-multiple-select": "Įjungti daugybinį pasirinkimą", + "allow-empty-select-option": "Leisti tuščią pasirinkimo reikšmę", + "select-options": "Pasirinkimo parinktys", + "not-unique-select-option-value-error": "Pasirinkimo reikšmės turi būti unikalios!", + "value": "Reikšmė", + "label": "Etiketė", + "add-option": "Pridėti parinktį", + "no-options": "Parinkčių nėra", + "remove-option": "Pašalinti parinktį", + "textarea-rows": "Teksto srities eilučių skaičius", + "help-id": "Pagalbos ID", + "buttons-direction": "Mygtukų išdėstymo kryptis", + "direction-row": "Eilutėje", + "direction-column": "Stulpelyje", + "radio-button-options": "Žymimųjų mygtukų parinktys", + "datetime-type": "Datos/laiko lauko tipas", + "datetime-type-date": "Data", + "datetime-type-time": "Laikas", + "datetime-type-datetime": "Data ir laikas", + "enable-clear-button": "Įjungti išvalymo mygtuką", + "html-section-settings": "HTML sekcijos nustatymai", + "html-section-classes": "HTML sekcijos CSS klasės", + "html-section-content": "HTML sekcijos turinys", + "array-item": "Masyvo elementas", + "item-type": "Elemento tipas", + "item-name": "Elemento pavadinimas", + "no-items": "Elementų nėra", + "support-unit-conversion": "Palaikyti vienetų konvertavimą" + }, + "clear-form": "Išvalyti formą", + "clear-form-prompt": "Ar tikrai norite pašalinti visas formos savybes?", + "import-form": "Importuoti formą iš JSON", + "export-form": "Eksportuoti formą į JSON", + "json-file": "JSON failas", + "json-content": "JSON turinys", + "invalid-form-json-file-error": "Nepavyko importuoti formos iš JSON: neteisinga formos JSON duomenų struktūra." + }, + "asset-profile": { + "asset-profile": "Turto profilis", + "asset-profiles": "Turto profiliai", + "all-asset-profiles": "Visi turto profiliai", + "add": "Pridėti turto profilį", + "edit": "Redaguoti turto profilį", + "asset-profile-details": "Turto profilio informacija", + "no-asset-profiles-text": "Turto profilių nerasta", + "search": "Ieškoti turto profilių", + "selected-asset-profiles": "{ count, plural, =1 {1 turto profilis} other {# turto profiliai} } pasirinkta", + "no-asset-profiles-matching": "Turto profilio, atitinkančio '{{entity}}', nerasta.", + "asset-profile-required": "Turto profilis būtinas.", + "idCopiedMessage": "Turto profilio ID nukopijuotas į iškarpinę", + "set-default": "Nustatyti kaip numatytąjį turto profilį", + "delete": "Pašalinti turto profilį", + "copyId": "Kopijuoti turto profilio ID", + "name-max-length": "Pavadinimas turi būti trumpesnis nei 256 simboliai.", + "new-device-profile-name": "Turto profilio pavadinimas", + "new-device-profile-name-required": "Turto profilio pavadinimas būtinas.", + "name": "Pavadinimas", + "name-required": "Pavadinimas būtinas.", + "image": "Turto profilio paveikslėlis", + "description": "Aprašymas", + "default": "Numatytasis", + "default-rule-chain": "Numatytoji taisyklių grandinė", + "default-edge-rule-chain": "Numatytoji Edge taisyklių grandinė", + "default-edge-rule-chain-hint": "Naudojama Edge aplinkoje apdoroti gaunamus duomenis turtui, kuris priklauso šiam profiliui.", + "mobile-dashboard": "Mobilusis skydelis", + "mobile-dashboard-hint": "Naudojamas mobiliojoje programėlėje kaip turto informacijos skydelis.", + "select-queue-hint": "Pasirinkite iš išskleidžiamojo sąrašo.", + "delete-asset-profile-title": "Ar tikrai norite pašalinti turto profilį '{{assetProfileName}}'?", + "delete-asset-profile-text": "Būkite atsargūs, po patvirtinimo turto profilis ir visi su juo susiję duomenys bus negrįžtamai pašalinti.", + "delete-asset-profiles-title": "Ar tikrai norite pašalinti { count, plural, =1 {1 turto profilį} other {# turto profilius} }?", + "delete-asset-profiles-text": "Būkite atsargūs, po patvirtinimo visi pasirinkti turto profiliai ir susiję duomenys bus negrįžtamai pašalinti.", + "set-default-asset-profile-title": "Ar tikrai norite nustatyti turto profilį '{{assetProfileName}}' kaip numatytąjį?", + "set-default-asset-profile-text": "Po patvirtinimo šis turto profilis bus pažymėtas kaip numatytasis ir bus naudojamas naujiems turtams, kuriems nenurodytas profilis.", + "no-asset-profiles-found": "Turto profilių nerasta.", + "create-new-asset-profile": "Sukurti naują!", + "create-asset-profile": "Sukurti naują turto profilį", + "import": "Importuoti turto profilį", + "export": "Eksportuoti turto profilį", + "export-failed-error": "Nepavyko eksportuoti turto profilio: {{error}}", + "asset-profile-file": "Turto profilio failas", + "invalid-asset-profile-file-error": "Nepavyko importuoti turto profilio: neteisinga duomenų struktūra." + }, + "device-profile": { + "device-profile": "Įrenginio profilis", + "device-profiles": "Įrenginių profiliai", + "all-device-profiles": "Visi įrenginių profiliai", + "add": "Pridėti įrenginio profilį", + "edit": "Redaguoti įrenginio profilį", + "device-profile-details": "Įrenginio profilio informacija", + "no-device-profiles-text": "Įrenginių profilių nerasta", + "search": "Ieškoti įrenginių profilių", + "selected-device-profiles": "{ count, plural, =1 {1 įrenginio profilis} other {# įrenginių profiliai} } pasirinkta", + "no-device-profiles-matching": "Įrenginio profilio, atitinkančio '{{entity}}', nerasta.", + "device-profile-required": "Įrenginio profilis būtinas.", + "idCopiedMessage": "Įrenginio profilio ID nukopijuotas į iškarpinę", + "set-default": "Nustatyti kaip numatytąjį įrenginio profilį", + "delete": "Pašalinti įrenginio profilį", + "copyId": "Kopijuoti įrenginio profilio ID", + "name-max-length": "Pavadinimas turi būti trumpesnis nei 256 simboliai.", + "name": "Pavadinimas", + "name-required": "Pavadinimas būtinas.", + "type": "Profilio tipas", + "type-required": "Profilio tipas būtinas.", + "type-default": "Numatytasis", + "image": "Įrenginio profilio paveikslėlis", + "transport-type": "Perdavimo tipas", + "transport-type-required": "Perdavimo tipas būtinas.", + "transport-type-default": "Numatytasis", + "transport-type-default-hint": "Palaiko pagrindinius MQTT, HTTP ir CoAP perdavimo tipus.", + "transport-type-mqtt": "MQTT", + "transport-type-mqtt-hint": "Įjungia išplėstinius MQTT perdavimo nustatymus.", + "transport-type-coap": "CoAP", + "transport-type-coap-hint": "Įjungia išplėstinius CoAP perdavimo nustatymus.", + "transport-type-lwm2m": "LWM2M", + "transport-type-lwm2m-hint": "LWM2M perdavimo tipas.", + "transport-type-snmp": "SNMP", + "transport-type-snmp-hint": "Nustatykite SNMP perdavimo konfigūraciją.", + "transport-type-http": "HTTP", + "description": "Aprašymas", + "default": "Numatytasis", + "profile-configuration": "Profilio konfigūracija", + "transport-configuration": "Perdavimo konfigūracija", + "default-rule-chain": "Numatytoji taisyklių grandinė", + "default-edge-rule-chain": "Numatytoji Edge taisyklių grandinė", + "default-edge-rule-chain-hint": "Naudojama Edge aplinkoje apdoroti duomenis, gaunamus iš įrenginių, naudojančių šį profilį.", + "mobile-dashboard": "Mobilusis skydelis", + "mobile-dashboard-hint": "Naudojamas mobiliojoje programėlėje kaip įrenginio informacijos skydelis.", + "select-queue-hint": "Pasirinkite iš išskleidžiamojo sąrašo.", + "delete-device-profile-title": "Ar tikrai norite pašalinti įrenginio profilį '{{deviceProfileName}}'?", + "delete-device-profile-text": "Būkite atsargūs, po patvirtinimo įrenginio profilis ir visi susiję duomenys, įskaitant OTA atnaujinimus, bus negrįžtamai pašalinti.", + "delete-device-profiles-title": "Ar tikrai norite pašalinti { count, plural, =1 {1 įrenginio profilį} other {# įrenginių profilius} }?", + "delete-device-profiles-text": "Būkite atsargūs, po patvirtinimo visi pasirinkti įrenginių profiliai ir susiję duomenys, įskaitant OTA atnaujinimus, bus negrįžtamai pašalinti.", + "set-default-device-profile-title": "Ar tikrai norite nustatyti įrenginio profilį '{{deviceProfileName}}' kaip numatytąjį?", + "set-default-device-profile-text": "Po patvirtinimo šis įrenginio profilis bus pažymėtas kaip numatytasis ir bus naudojamas naujiems įrenginiams, kuriems profilis nenurodytas.", + "no-device-profiles-found": "Įrenginių profilių nerasta.", + "create-new-device-profile": "Sukurti naują!", + "mqtt-device-topic-filters": "MQTT įrenginių temų filtrai", + "mqtt-device-topic-filters-unique": "MQTT įrenginių temų filtrai turi būti unikalūs.", + "mqtt-device-topic-filters-spark-plug": "MQTT Sparkplug B Edge of Network (EoN) mazgas.", + "mqtt-device-topic-filters-spark-plug-hint": "Leidžia prisijungimus iš EoN mazgų, naudojančių Sparkplug B formatą ir temų struktūrą.", + "mqtt-device-topic-filters-spark-plug-attribute-metric-names": "SparkPlug metrikos, saugomos kaip atributai.", + "mqtt-device-topic-filters-spark-plug-attribute-metric-names-hint": "SparkPlug metrikų pavadinimai, kurie bus saugomi kaip įrenginio atributai. Visos kitos metrikos bus saugomos kaip telemetrijos duomenys.", + "mqtt-device-payload-type": "MQTT įrenginio duomenų formatas", + "mqtt-device-payload-type-json": "JSON", + "mqtt-device-payload-type-proto": "Protobuf", + "mqtt-enable-compatibility-with-json-payload-format": "Įjungti suderinamumą su kitais duomenų formatais.", + "mqtt-enable-compatibility-with-json-payload-format-hint": "Įjungus šią parinktį, platforma pagal nutylėjimą naudos Protobuf formatą. Jei analizė nepavyks, bus naudojamas JSON. Naudinga pereinant nuo JSON prie Protobuf atnaujinant programinę įrangą. Rekomenduojama išjungti, kai visi įrenginiai bus atnaujinti.", + "mqtt-use-json-format-for-default-downlink-topics": "Naudoti JSON formatą numatytosioms atsiuntimo temoms", + "mqtt-use-json-format-for-default-downlink-topics-hint": "Kai įjungta, platforma naudos JSON formatą siųsdama atributus ir RPC per temas v1/devices/me/.... Tai neturi įtakos naujoms (v2) temoms.", + "mqtt-send-ack-on-validation-exception": "Siųsti PUBACK, kai PUBLISH žinutės validacija nesėkminga", + "mqtt-send-ack-on-validation-exception-hint": "Pagal numatymą platforma nutraukia MQTT sesiją, kai žinutės validacija nesėkminga. Įjungus šią parinktį, vietoj to bus siunčiamas patvirtinimas.", + "mqtt-protocol-version": "Protokolo versija", + "snmp-add-mapping": "Pridėti SNMP atvaizdavimą", + "snmp-mapping-not-configured": "OID atvaizdavimas į telemetriją/atributus nesukonfigūruotas", + "snmp-timseries-or-attribute-name": "Telemetrijos/atributo pavadinimas atvaizdavimui", + "snmp-timseries-or-attribute-type": "Telemetrijos/atributo tipas atvaizdavimui", + "snmp-method-pdu-type-get-request": "GetRequest", + "snmp-method-pdu-type-get-next-request": "GetNextRequest", + "snmp-oid": "OID", + "transport-device-payload-type-json": "JSON", + "transport-device-payload-type-proto": "Protobuf", + "mqtt-payload-type-required": "Duomenų formatas būtinas.", + "coap-device-type": "CoAP įrenginio tipas", + "coap-device-payload-type": "CoAP įrenginio duomenų formatas", + "coap-device-type-required": "CoAP įrenginio tipas būtinas.", + "coap-device-type-default": "Numatytasis", + "coap-device-type-efento": "Efento NB-IoT", + "support-level-wildcards": "Palaikomi vieno lygmens [+] ir kelių lygių [#] pakaitos simboliai.", + "telemetry-topic-filter": "Telemetrijos temų filtras", + "telemetry-topic-filter-required": "Telemetrijos temų filtras būtinas.", + "attributes-topic-filter": "Atributų publikavimo temų filtras", + "attributes-subscribe-topic-filter": "Atributų prenumeratos temų filtras", + "attributes-topic-filter-required": "Atributų publikavimo temų filtras būtinas.", + "attributes-subscribe-topic-filter-required": "Atributų prenumeratos tema būtina.", + "telemetry-proto-schema": "Telemetrijos Protobuf schema", + "telemetry-proto-schema-required": "Telemetrijos Protobuf schema būtina.", + "attributes-proto-schema": "Atributų Protobuf schema", + "attributes-proto-schema-required": "Atributų Protobuf schema būtina.", + "rpc-response-proto-schema": "RPC atsakymo Protobuf schema", + "rpc-response-proto-schema-required": "RPC atsakymo Protobuf schema būtina.", + "rpc-response-topic-filter": "RPC atsakymo temų filtras", + "rpc-response-topic-filter-required": "RPC atsakymo temų filtras būtinas.", + "rpc-request-proto-schema": "RPC užklausos Protobuf schema", + "rpc-request-proto-schema-required": "RPC užklausos Protobuf schema būtina.", + "rpc-request-proto-schema-hint": "RPC užklausos žinutėje visada turi būti laukeliai: string method = 1; int32 requestId = 2; ir params = 3 (bet kokio tipo).", + "not-valid-pattern-topic-filter": "Netinkamas temų filtro šablonas.", + "not-valid-single-character": "Netinkamas vieno lygmens pakaitos simbolio naudojimas.", + "not-valid-multi-character": "Netinkamas kelių lygių pakaitos simbolio naudojimas.", + "single-level-wildcards-hint": "[+] galima naudoti bet kuriame temų filtro lygyje, pvz. v1/devices/+/telemetry.", + "multi-level-wildcards-hint": "[#] turi būti paskutinis simbolis, pvz. # arba v1/devices/me/#.", + "alarm-rules": "Įspėjimų taisyklės", + "alarm-rules-with-count": "Įspėjimų taisyklės ({{count}})", + "no-alarm-rules": "Įspėjimų taisyklių nesukonfigūruota.", + "add-alarm-rule": "Pridėti įspėjimo taisyklę", + "edit-alarm-rule": "Redaguoti įspėjimo taisyklę", + "alarm-type": "Įspėjimo tipas", + "alarm-type-required": "Įspėjimo tipas būtinas.", + "alarm-type-unique": "Įspėjimo tipas turi būti unikalus šiame profilyje.", + "alarm-type-max-length": "Įspėjimo tipo pavadinimas turi būti trumpesnis nei 256 simboliai.", + "create-alarm-pattern": "Sukurti {{alarmType}} įspėjimą", + "create-alarm-rules": "Sukurti įspėjimo taisykles", + "no-create-alarm-rules": "Sukūrimo sąlygų nėra.", + "add-create-alarm-rule-prompt": "Pridėkite įspėjimo sukūrimo sąlygą", + "clear-alarm-rule": "Išvalyti įspėjimo taisyklę", + "no-clear-alarm-rule": "Nėra išvalymo sąlygų", + "add-create-alarm-rule": "Pridėti sukūrimo sąlygą", + "add-clear-alarm-rule": "Pridėti išvalymo sąlygą", + "select-alarm-severity": "Pasirinkite įspėjimo rimtumą", + "alarm-severity-required": "Įspėjimo rimtumas būtinas.", + "condition-duration": "Sąlygos trukmė", + "condition-duration-value": "Trukmės reikšmė", + "condition-duration-time-unit": "Laiko vienetas", + "condition-duration-value-range": "Trukmės reikšmė turi būti nuo 1 iki 2147483647.", + "condition-duration-value-pattern": "Trukmės reikšmė turi būti sveikasis skaičius.", + "condition-duration-value-required": "Trukmės reikšmė būtina.", + "condition-duration-time-unit-required": "Laiko vienetas būtinas.", + "advanced-settings": "Išplėstiniai nustatymai", + "alarm-rule-additional-info": "Papildoma informacija", + "edit-alarm-rule-additional-info": "Redaguoti papildomą informaciją", + "alarm-rule-additional-info-placeholder": "Pateikite komentarus ar pastabas, kurios bus rodomos įspėjimo informacijos skiltyje „Papildoma informacija“.", + "alarm-rule-additional-info-hint": "Patarimas: naudokite ${keyName} norėdami įtraukti atributų ar telemetrijos reikšmes.", + "alarm-rule-mobile-dashboard": "Mobilusis įspėjimų skydelis", + "alarm-rule-mobile-dashboard-hint": "Naudojamas mobiliojoje programėlėje kaip įspėjimo detalių skydelis.", + "alarm-rule-no-mobile-dashboard": "Skydelis nepasirinktas.", + "propagate-alarm": "Platinti įspėjimą susijusiems subjektams", + "alarm-rule-relation-types-list": "Ryšių tipai, kuriems platinti įspėjimą", + "alarm-rule-relation-types-list-hint": "Jei nenurodyta, įspėjimai bus platinami be filtravimo pagal ryšių tipą.", + "propagate-alarm-to-owner": "Platinti įspėjimą subjekto savininkui (klientui ar nuomininkui)", + "propagate-alarm-to-tenant": "Platinti įspėjimą nuomininkui", + "alarm-rule-condition": "Įspėjimo sąlyga", + "enter-alarm-rule-condition-prompt": "Pridėkite įspėjimo sąlygą", + "edit-alarm-rule-condition": "Redaguoti įspėjimo sąlygą", + "device-provisioning": "Įrenginių paruošimas (provisioning)", + "provision-strategy": "Paruošimo strategija", + "provision-strategy-required": "Paruošimo strategija būtina.", + "provision-strategy-disabled": "Išjungta", + "provision-strategy-created-new": "Leisti kurti naujus įrenginius", + "provision-strategy-check-pre-provisioned": "Tikrinti iš anksto paruoštus įrenginius", + "provision-device-key": "Įrenginio paruošimo raktas", + "provision-device-key-required": "Įrenginio paruošimo raktas būtinas.", + "copy-provision-key": "Kopijuoti paruošimo raktą", + "provision-key-copied-message": "Paruošimo raktas nukopijuotas į iškarpinę.", + "provision-device-secret": "Įrenginio paruošimo slaptas kodas", + "provision-device-secret-required": "Įrenginio paruošimo slaptas kodas būtinas.", + "copy-provision-secret": "Kopijuoti slaptą kodą", + "provision-secret-copied-message": "Slaptas kodas nukopijuotas į iškarpinę.", + "provision-strategy-x509": { + "certificate-chain": "X509 sertifikatų grandinė", + "certificate-chain-hint": "X.509 strategija naudojama įrenginiams paruošti naudojant dvipusį TLS ryšį ir kliento sertifikatus.", + "allow-create-new-devices": "Leisti kurti naujus įrenginius", + "allow-create-new-devices-hint": "Jei pažymėta, nauji įrenginiai bus kuriami, o kliento sertifikatas naudojamas kaip įrenginio įgaliojimai.", + "certificate-value": "Sertifikatas PEM formatu", + "certificate-value-required": "Sertifikatas PEM formatu būtinas.", + "cn-regex-variable": "CN reguliariosios išraiškos kintamasis", + "cn-regex-variable-required": "CN reguliariosios išraiškos kintamasis būtinas.", + "cn-regex-variable-hint": "Naudojamas įrenginio pavadinimui gauti iš X509 sertifikato 'Common Name' lauko." + }, + "condition": "Sąlyga", + "condition-type": "Sąlygos tipas", + "condition-type-simple": "Paprasta", + "condition-type-duration": "Trukmės", + "condition-during": "Per {{during}}", + "condition-during-dynamic": "Per „{{attribute}}“ ({{during}})", + "condition-type-repeating": "Pasikartojanti", + "condition-type-required": "Sąlygos tipas būtinas.", + "condition-repeating-value": "Įvykių skaičius", + "condition-repeating-value-range": "Įvykių skaičius turi būti nuo 1 iki 2147483647.", + "condition-repeating-value-pattern": "Įvykių skaičius turi būti sveikasis skaičius.", + "condition-repeating-value-required": "Įvykių skaičius būtinas.", + "condition-repeat-times": "Kartojasi { count, plural, =1 {1 kartą} other {# kartus} }", + "condition-repeat-times-dynamic": "Kartojasi „{attribute}“ ({ count, plural, =1 {1 kartą} other {# kartus} })", + "schedule-type": "Tvarkaraščio tipas", + "schedule-type-required": "Tvarkaraščio tipas būtinas.", + "schedule": "Tvarkaraštis", + "edit-schedule": "Redaguoti įspėjimo tvarkaraštį", + "schedule-any-time": "Aktyvus visą laiką", + "schedule-specific-time": "Aktyvus tam tikru laiku", + "schedule-custom": "Pasirinktinis", + "schedule-day": { + "monday": "Pirmadienis", + "tuesday": "Antradienis", + "wednesday": "Trečiadienis", + "thursday": "Ketvirtadienis", + "friday": "Penktadienis", + "saturday": "Šeštadienis", + "sunday": "Sekmadienis" + }, + "schedule-days": "Dienos", + "schedule-time": "Laikas", + "schedule-time-from": "Nuo", + "schedule-time-to": "Iki", + "schedule-days-of-week-required": "Reikia pasirinkti bent vieną savaitės dieną.", + "create-device-profile": "Sukurti naują įrenginio profilį", + "import": "Importuoti įrenginio profilį", + "export": "Eksportuoti įrenginio profilį", + "export-failed-error": "Nepavyko eksportuoti įrenginio profilio: {{error}}", + "device-profile-file": "Įrenginio profilio failas", + "invalid-device-profile-file-error": "Nepavyko importuoti įrenginio profilio: neteisinga profilio duomenų struktūra.", + "power-saving-mode": "Energijos taupymo režimas", + "power-saving-mode-type": { + "default": "Naudoti įrenginio profilio energijos taupymo režimą", + "psm": "Power Saving Mode (PSM)", + "drx": "Discontinuous Reception (DRX)", + "edrx": "Extended Discontinuous Reception (eDRX)" + }, + "edrx-cycle": "eDRX ciklas", + "edrx-cycle-required": "eDRX ciklas būtinas.", + "edrx-cycle-pattern": "eDRX ciklas turi būti teigiamas sveikasis skaičius.", + "edrx-cycle-min": "Minimalus eDRX ciklo skaičius – {{ min }} sekundžių.", + "paging-transmission-window": "Puslapiavimo perdavimo langas", + "paging-transmission-window-required": "Puslapiavimo perdavimo langas būtinas.", + "paging-transmission-window-pattern": "Puslapiavimo perdavimo langas turi būti teigiamas sveikasis skaičius.", + "paging-transmission-window-min": "Minimalus puslapiavimo perdavimo langas – {{ min }} sekundžių.", + "psm-activity-timer": "PSM aktyvumo laikmatis", + "psm-activity-timer-required": "PSM aktyvumo laikmatis būtinas.", + "psm-activity-timer-pattern": "PSM aktyvumo laikmatis turi būti teigiamas sveikasis skaičius.", + "psm-activity-timer-min": "Minimalus PSM aktyvumo laikmatis – {{ min }} sekundžių.", + "lwm2m": { + "object-list": "Objektų sąrašas", + "object-list-empty": "Objektai nepasirinkti.", + "no-objects-found": "Objektų nerasta.", + "no-objects-matching": "Objektų, atitinkančių '{{object}}', nerasta.", + "model-tab": "LWM2M modelis", + "add-new-instances": "Pridėti naujus egzempliorius", + "instances-list": "Egzempliorių sąrašas", + "instances-list-required": "Egzempliorių sąrašas būtinas.", + "instance-id-pattern": "Egzemplioriaus ID turi būti teigiamas sveikasis skaičius.", + "instance-id-max": "Didžiausia egzemplioriaus ID reikšmė – {{max}}.", + "instance": "Egzempliorius", + "resource-label": "#ID Išteklius (Resource name)", + "observe-label": "Stebėjimas", + "attribute-label": "Atributas", + "telemetry-label": "Telemetrija", + "edit-observe-select": "Norėdami redaguoti stebėjimą, pasirinkite telemetriją arba atributą", + "edit-attributes-select": "Norėdami redaguoti atributus, pasirinkite telemetriją arba atributą", + "no-attributes-set": "Atributai nenustatyti", + "key-name": "Rakto pavadinimas", + "key-name-required": "Rakto pavadinimas būtinas.", + "attribute-name": "Atributo pavadinimas", + "attribute-name-required": "Atributo pavadinimas būtinas.", + "attribute-value": "Atributo reikšmė", + "attribute-value-required": "Atributo reikšmė būtina.", + "attribute-value-pattern": "Atributo reikšmė turi būti teigiamas sveikasis skaičius.", + "edit-attributes": "Redaguoti atributus: {{ name }}", + "view-attributes": "Peržiūrėti atributus: {{ name }}", + "add-attribute": "Pridėti atributą", + "edit-attribute": "Redaguoti atributą", + "view-attribute": "Peržiūrėti atributą", + "remove-attribute": "Pašalinti atributą", + "delete-server-text": "Būkite atsargūs – po patvirtinimo serverio konfigūracija bus negrįžtamai prarasta.", + "delete-server-title": "Ar tikrai norite pašalinti serverį?", + "mode": "Saugumo konfigūracijos režimas", + "bootstrap-tab": "Bootstrap", + "bootstrap-server-legend": "Bootstrap Server (Trumpasis ID...)", + "lwm2m-server-legend": "LwM2M Server (Trumpasis ID...)", + "server": "Serveris", + "short-id": "Trumpasis serverio ID", + "short-id-tooltip": "Trumpasis serverio ID naudojamas LwM2M serverio egzemplioriaus susiejimui.\nŠis identifikatorius unikalus kiekvienam LwM2M klientui.\nReikšmė privaloma, kai Bootstrap-Server parametras yra 'false'.\nVertės ID:0 ir ID:65535 negali būti naudojamos LwM2M serverio identifikavimui.", + "short-id-tooltip-bootstrap": "Trumpasis serverio ID naudojamas LwM2M serverio egzemplioriaus susiejimui.\nPrivaloma reikšmė, kai Bootstrap-Server parametras yra 'false'.", + "short-id-required": "Trumpasis serverio ID būtinas.", + "short-id-range": "Trumpasis serverio ID turi būti tarp {{ min }} ir {{ max }}.", + "short-id-pattern": "Trumpasis serverio ID turi būti teigiamas sveikasis skaičius.", + "lifetime": "Kliento registracijos trukmė", + "lifetime-required": "Kliento registracijos trukmė būtina.", + "lifetime-pattern": "Kliento registracijos trukmė turi būti teigiamas sveikasis skaičius.", + "default-min-period": "Minimalus laikotarpis tarp dviejų pranešimų (s)", + "default-min-period-tooltip": "Numatytoji vertė, kurią LwM2M klientas turėtų naudoti minimaliam stebėjimo laikotarpiui, jei parametras nenurodytas.", + "default-min-period-required": "Minimalus laikotarpis būtinas.", + "default-min-period-pattern": "Minimalus laikotarpis turi būti teigiamas sveikasis skaičius.", + "notification-storing": "Pranešimų saugojimas, kai įrenginys išjungtas arba neprisijungęs", + "binding": "Susiejimas (Binding)", + "binding-type": { + "u": "U: Klientas pasiekiamas per UDP ryšį bet kuriuo metu.", + "m": "M: Klientas pasiekiamas per MQTT ryšį bet kuriuo metu.", + "h": "H: Klientas pasiekiamas per HTTP ryšį bet kuriuo metu.", + "t": "T: Klientas pasiekiamas per TCP ryšį bet kuriuo metu.", + "s": "S: Klientas pasiekiamas per SMS ryšį bet kuriuo metu.", + "n": "N: Klientas turi siųsti atsakymą naudodamas Non-IP ryšį (palaikoma nuo LWM2M 1.1).", + "uq": "UQ: UDP ryšys eilės režimu (nepalaikoma nuo LWM2M 1.1).", + "uqs": "UQS: aktyvus tiek UDP, tiek SMS ryšys; UDP eilės režimu, SMS standartiniu režimu (nepalaikoma nuo LWM2M 1.1).", + "tq": "TQ: TCP ryšys eilės režimu (nepalaikoma nuo LWM2M 1.1).", + "tqs": "TQS: aktyvus tiek TCP, tiek SMS ryšys; TCP eilės režimu, SMS standartiniu režimu (nepalaikoma nuo LWM2M 1.1).", + "sq": "SQ: SMS ryšys eilės režimu (nepalaikoma nuo LWM2M 1.1)." + }, + "binding-tooltip": "Tai yra sąrašas iš LwM2M serverio objekto „binding“ ištekliaus - /1/x/7.\nNurodo palaikomus ryšio (binding) režimus LwM2M kliente.\nŠi reikšmė TURĖTŲ sutapti su „Supported Binding and Modes“ ištekliaus reikšme įrenginio objekte (/3/0/16).\nNors palaikomi keli transportavimo protokolai, visos sesijos metu galima naudoti tik vieną ryšio būdą.\nPavyzdžiui, kai palaikomi UDP ir SMS, LwM2M klientas ir serveris gali naudoti tik vieną — arba UDP, arba SMS — visos sesijos metu.", + "bootstrap-server": "Bootstrap serveris", + "lwm2m-server": "LwM2M serveris", + "include-bootstrap-server": "Įtraukti Bootstrap serverio atnaujinimus", + "bootstrap-update-title": "Bootstrap serveris jau sukonfigūruotas. Ar tikrai norite pašalinti atnaujinimus?", + "bootstrap-update-text": "Būkite atsargūs — po patvirtinimo Bootstrap serverio konfigūracija bus negrįžtamai prarasta.", + "server-host": "Serverio adresas (Host)", + "server-host-required": "Serverio adresas būtinas.", + "server-port": "Prievadas (Port)", + "server-port-required": "Prievadas būtinas.", + "server-port-pattern": "Prievadas turi būti teigiamas sveikasis skaičius.", + "server-port-range": "Prievadas turi būti tarp 1 ir 65535.", + "server-public-key": "Serverio viešasis raktas", + "server-public-key-required": "Serverio viešasis raktas būtinas.", + "client-hold-off-time": "Kliento atidėjimo laikas (Hold Off Time)", + "client-hold-off-time-required": "Kliento atidėjimo laikas būtinas.", + "client-hold-off-time-pattern": "Kliento atidėjimo laikas turi būti teigiamas sveikasis skaičius.", + "client-hold-off-time-tooltip": "Naudojamas tik su Bootstrap serveriu – nurodo laukimo laiką prieš registraciją.", + "account-after-timeout": "Sąskaita po pasibaigusio laiko (Account after timeout)", + "account-after-timeout-required": "„Account after timeout“ reikšmė būtina.", + "account-after-timeout-pattern": "„Account after timeout“ turi būti teigiamas sveikasis skaičius.", + "account-after-timeout-tooltip": "Bootstrap serverio „Account after timeout“ reikšmė, apibrėžianti veiksmą po nurodyto laiko.", + "server-type": "Serverio tipas", + "add-new-server-title": "Pridėti naują serverio konfigūraciją", + "add-server-config": "Pridėti serverio konfigūraciją", + "add-lwm2m-server-config": "Pridėti LwM2M serverį", + "no-config-servers": "Serverių konfigūracijų nėra", + "others-tab": "Kiti nustatymai", + "ota-update": "OTA atnaujinimas", + "use-object-19-for-ota-update": "Naudoti objektą 19 OTA failo metaduomenims (kontrolinė suma, dydis, versija, pavadinimas)", + "use-object-19-for-ota-update-hint": "Naudokite išteklių ObjectId = 19 OTA atnaujinimams: Firmware → InstanceId = 65534, Software → InstanceId = 65535. Duomenų formatas – JSON, užkoduotas Base64 formatu. JSON faile pateikiama OTA metainformacija: „Checksum“ (SHA256). Papildomi laukai: „Title“ (pavadinimas), „Version“ (versija), „File Name“ (failo pavadinimas), „File Size“ (dydis baitais).", + "client-strategy": "Kliento strategija jungiantis", + "client-strategy-label": "Strategija", + "client-strategy-only-observe": "Tik „Observe“ užklausos klientui po pradinio prisijungimo", + "client-strategy-read-all": "Perskaityti visus išteklius ir siųsti „Observe“ užklausas po registracijos", + "fw-update": "Programinės aparatinės įrangos (Firmware) atnaujinimas", + "fw-update-strategy": "Firmware atnaujinimo strategija", + "fw-update-strategy-data": "Įkelti firmware kaip dvejetainį failą naudojant objektą 19 ir išteklių 0 (Data)", + "fw-update-strategy-package": "Įkelti firmware kaip dvejetainį failą naudojant objektą 5 ir išteklių 0 (Package)", + "fw-update-strategy-package-uri": "Automatiškai sugeneruoti unikalų CoAP URL firmware parsisiuntimui ir naudoti objektą 5 bei išteklių 1 (Package URI)", + "sw-update": "Programinės įrangos (Software) atnaujinimas", + "sw-update-strategy": "Software atnaujinimo strategija", + "sw-update-strategy-package": "Įkelti dvejetainį failą naudojant objektą 9 ir išteklių 2 (Package)", + "sw-update-strategy-package-uri": "Automatiškai sugeneruoti unikalų CoAP URL programos parsisiuntimui ir naudoti objektą 9 bei išteklių 3 (Package URI)", + "fw-update-resource": "Firmware atnaujinimo CoAP išteklius", + "fw-update-resource-required": "Firmware atnaujinimo CoAP išteklius būtinas.", + "sw-update-resource": "Software atnaujinimo CoAP išteklius", + "sw-update-resource-required": "Software atnaujinimo CoAP išteklius būtinas.", + "config-json-tab": "JSON konfigūracijos profilis", + "attributes-name": { + "min-period": "Minimalus periodas", + "max-period": "Maksimalus periodas", + "greater-than": "Didesnis nei", + "less-than": "Mažesnis nei", + "step": "Žingsnis", + "min-evaluation-period": "Minimalus vertinimo periodas", + "max-evaluation-period": "Maksimalus vertinimo periodas" + }, + "default-object-id": "Numatytoji Objekto Versija (Atributas)", + "default-object-id-ver": { + "v1-0": "1.0", + "v1-1": "1.1", + "v1-2": "1.2" + }, + "observe-strategy": { + "observe-strategy": "Stebėjimo strategija", + "single": "Atskiras", + "single-description": "Vienas stebėjimo užklausimas vienam ištekliui (didesnis tikslumas, daugiau tinklo srauto)", + "composite-all": "Bendras (visi ištekliai)", + "composite-all-description": "Visi ištekliai stebimi naudojant vieną bendrą stebėjimo užklausą (efektyviau, bet mažiau lankstu)", + "composite-by-object": "Pagal objektus", + "composite-by-object-description": "Ištekliai grupuojami pagal objektų tipus ir stebimi naudojant atskiras bendras stebėjimo užklausas (subalansuotas metodas)" + } + }, + "snmp": { + "add-communication-config": "Pridėti ryšio konfigūraciją", + "add-mapping": "Pridėti susiejimą", + "authentication-passphrase": "Autentifikacijos slaptažodis", + "authentication-passphrase-required": "Autentifikacijos slaptažodis būtinas.", + "authentication-protocol": "Autentifikacijos protokolas", + "authentication-protocol-required": "Autentifikacijos protokolas būtinas.", + "communication-configs": "Ryšio konfigūracijos", + "community": "Bendruomenės eilutė (Community string)", + "community-required": "Bendruomenės eilutė būtina.", + "context-name": "Konteksto pavadinimas", + "data-key": "Duomenų raktas", + "data-key-required": "Duomenų raktas būtinas.", + "data-type": "Duomenų tipas", + "data-type-required": "Duomenų tipas būtinas.", + "engine-id": "Variklio ID (Engine ID)", + "host": "Serveris / įrenginys (Host)", + "host-required": "Serverio / įrenginio pavadinimas būtinas.", + "oid": "OID (Objekto identifikatorius)", + "oid-pattern": "Neteisingas OID formatas.", + "oid-required": "OID būtinas.", + "please-add-communication-config": "Pridėkite ryšio konfigūraciją.", + "please-add-mapping-config": "Pridėkite susiejimo konfigūraciją.", + "port": "Prievadas (Port)", + "port-format": "Neteisingas prievado formatas.", + "port-required": "Prievadas būtinas.", + "privacy-passphrase": "Privatumo slaptažodis", + "privacy-passphrase-required": "Privatumo slaptažodis būtinas.", + "privacy-protocol": "Privatumo protokolas", + "privacy-protocol-required": "Privatumo protokolas būtinas.", + "protocol-version": "Protokolo versija", + "protocol-version-required": "Protokolo versija būtina.", + "querying-frequency": "Užklausų dažnis, ms", + "querying-frequency-invalid-format": "Užklausų dažnis turi būti teigiamas sveikasis skaičius.", + "querying-frequency-required": "Užklausų dažnis būtinas.", + "retries": "Bandymų skaičius (Retries)", + "retries-invalid-format": "Bandymų skaičius turi būti teigiamas sveikasis skaičius.", + "retries-required": "Bandymų skaičius būtinas.", + "scope": "Apimtis (Scope)", + "scope-required": "Apimtis būtina.", + "security-name": "Saugos pavadinimas", + "security-name-required": "Saugos pavadinimas būtinas.", + "timeout-ms": "Laiko limitas, ms", + "timeout-ms-invalid-format": "Laiko limitas turi būti teigiamas sveikasis skaičius.", + "timeout-ms-required": "Laiko limitas būtinas.", + "user-name": "Vartotojo vardas", + "user-name-required": "Vartotojo vardas būtinas." + } + }, + "dialog": { + "close": "Uždaryti dialogo langą", + "error-message-title": "Klaidos pranešimas:", + "error-details-title": "Informacija apie klaidą" + }, + "direction": { + "column": "Stulpelis", + "row": "Eilutė" + }, + "edge": { + "edge": "Edge įrenginys", + "edge-instances": "Edge instancijos", + "instances": "Instancijos", + "edge-file": "Edge failas", + "name-max-length": "Pavadinimas negali viršyti 256 simbolių.", + "label-max-length": "Etiketė negali viršyti 256 simbolių.", + "type-max-length": "Tipas negali viršyti 256 simbolių.", + "management": "Edge valdymas", + "no-edges-matching": "Edge įrenginių, atitinkančių '{{entity}}', nerasta.", + "add": "Pridėti Edge įrenginį", + "no-edges-text": "Edge įrenginių nerasta.", + "edge-details": "Edge įrenginio informacija", + "add-edge-text": "Pridėti naują Edge įrenginį", + "delete": "Panaikinti Edge įrenginį", + "delete-edge-title": "Ar tikrai norite panaikinti Edge įrenginį '{{edgeName}}'?", + "delete-edge-text": "Po patvirtinimo Edge įrenginys ir visa su juo susijusi informacija bus negrįžtamai panaikinta.", + "delete-edges-title": "Ar tikrai norite panaikinti { count, plural, =1 {1 Edge įrenginį} other {# Edge įrenginius} }?", + "delete-edges-text": "Po patvirtinimo visi pasirinkti Edge įrenginiai ir jų duomenys bus negrįžtamai panaikinti.", + "name": "Pavadinimas", + "name-starts-with": "Edge įrenginio pavadinimas prasideda", + "name-required": "Pavadinimas būtinas.", + "description": "Aprašymas", + "details": "Išsami informacija", + "events": "Įvykiai", + "copy-id": "Kopijuoti Edge ID", + "id-copied-message": "Edge ID nukopijuotas į iškarpinę", + "sync": "Sinchronizuoti Edge", + "edge-required": "Edge būtinas.", + "edge-type": "Edge tipas", + "edge-type-required": "Edge tipas būtinas.", + "event-action": "Įvykio veiksmas", + "entity-id": "Subjekto ID", + "select-edge-type": "Pasirinkite Edge tipą", + "assign-to-customer": "Priskirti klientui", + "assign-to-customer-text": "Pasirinkite klientą, kuriam norite priskirti Edge įrenginį (-ius)", + "assign-edge-to-customer": "Priskirti Edge įrenginį (-ius) klientui", + "assign-edge-to-customer-text": "Pasirinkite Edge įrenginius, kuriuos norite priskirti klientui", + "assignedToCustomer": "Priskirta klientui", + "edge-public": "Edge yra viešas", + "assigned-to-customer": "Priskirta: {{customerTitle}}", + "unassign-from-customer": "Atsieti nuo kliento", + "unassign-edge-title": "Ar tikrai norite atsieti Edge '{{edgeName}}'?", + "unassign-edge-text": "Po patvirtinimo Edge įrenginys bus atsietas ir nepasiekiamas klientui.", + "unassign-edges-title": "Ar tikrai norite atsieti { count, plural, =1 {1 Edge įrenginį} other {# Edge įrenginius} }?", + "unassign-edges-text": "Po patvirtinimo visi pasirinkti Edge įrenginiai bus atsieti nuo kliento.", + "make-public": "Padaryti Edge viešu", + "make-public-edge-title": "Ar tikrai norite Edge '{{edgeName}}' padaryti viešu?", + "make-public-edge-text": "Po patvirtinimo Edge ir visi jo duomenys bus prieinami kitiems vartotojams.", + "make-private": "Padaryti Edge privačiu", + "public": "Viešas", + "make-private-edge-title": "Ar tikrai norite Edge '{{edgeName}}' padaryti privačiu?", + "make-private-edge-text": "Po patvirtinimo Edge ir jo duomenys taps privatūs ir nepasiekiami kitiems vartotojams.", + "import": "Importuoti Edge", + "install-connect-instructions": "Diegimo ir prijungimo instrukcijos", + "install-connect-instructions-edge-created": "Edge sukurtas! Peržiūrėkite diegimo ir prijungimo instrukcijas.", + "loading-edge-instructions": "Įkeliamos Edge instrukcijos...", + "label": "Etiketė", + "load-entity-error": "Nepavyko įkelti duomenų. Subjektas buvo panaikintas.", + "assign-new-edge": "Priskirti naują Edge", + "unassign-from-edge": "Atsieti nuo Edge", + "edge-key": "Edge raktas", + "copy-edge-key": "Kopijuoti Edge raktą", + "edge-key-copied-message": "Edge raktas nukopijuotas į iškarpinę", + "edge-secret": "Edge slaptas raktas", + "copy-edge-secret": "Kopijuoti Edge slaptą raktą", + "edge-secret-copied-message": "Edge slaptas raktas nukopijuotas į iškarpinę", + "manage-assets": "Valdyti turtą (assets)", + "manage-devices": "Valdyti įrenginius", + "manage-entity-views": "Valdyti subjekto rodinius", + "manage-dashboards": "Valdyti skydelius", + "manage-rulechains": "Valdyti taisyklių grandines (rule chains)", + "assets": "Edge turtas", + "devices": "Edge įrenginiai", + "entity-views": "Edge subjekto rodiniai", + "dashboard": "Edge skydelis", + "dashboards": "Edge skydeliai", + "rulechain-templates": "Taisyklių grandinių šablonai", + "edge-rulechain-templates": "Edge taisyklių grandinių šablonai", + "rulechains": "Edge taisyklių grandinės", + "search": "Ieškoti Edge įrenginių", + "selected-edges": "Pasirinkta { count, plural, =1 {1 Edge įrenginys} other {# Edge įrenginiai} }", + "any-edge": "Bet kuris Edge įrenginys", + "no-edge-types-matching": "Edge tipų, atitinkančių '{{entitySubtype}}', nerasta.", + "edge-type-list-empty": "Edge tipai nepasirinkti.", + "edge-types": "Edge tipai", + "enter-edge-type": "Įveskite Edge tipą", + "deployed": "Įdiegtas", + "pending": "Laukia", + "downlinks": "Downlink ryšiai", + "no-downlinks-prompt": "Downlink ryšių nerasta.", + "sync-process-started-successfully": "Sinchronizacija sėkmingai pradėta!", + "missing-related-rule-chains-title": "Edge trūksta susijusių taisyklių grandinių", + "missing-related-rule-chains-text": "Edge priskirtos taisyklių grandinės naudoja mazgus, nukreipiančius į kitų grandinių žinutes, kurios nėra priskirtos šiam Edge.

    Trūkstamų grandinių sąrašas:
    {{missingRuleChains}}", + "widget-datasource-error": "Šis valdiklis palaiko tik EDGE subjekto duomenų šaltinius", + "upgrade-instructions": "Atnaujinimo instrukcijos.", + "connected": "Prisijungęs", + "disconnected": "Atsijungęs" + }, + "edge-event": { + "type-dashboard": "Skydelis", + "type-asset": "Turtas (Asset)", + "type-device": "Įrenginys", + "type-device-profile": "Įrenginio profilis", + "type-asset-profile": "Turto profilis", + "type-entity-view": "Subjekto rodinys", + "type-alarm": "Signalizacija (Alarm)", + "type-rule-chain": "Taisyklių grandinė (Rule Chain)", + "type-rule-chain-metadata": "Taisyklių grandinės metaduomenys", + "type-edge": "Edge įrenginys", + "type-user": "Vartotojas", + "type-tenant": "Nuomininkas (Tenant)", + "type-tenant-profile": "Nuomininko profilis", + "type-customer": "Klientas", + "type-relation": "Ryšys (Relation)", + "type-widgets-bundle": "Valdiklių paketas (Widgets Bundle)", + "type-widgets-type": "Valdiklio tipas (Widget Type)", + "type-admin-settings": "Administratoriaus nustatymai", + "type-ota-package": "OTA paketas (Over-The-Air)", + "type-queue": "Eilė (Queue)", + "action-type-added": "Pridėta", + "action-type-deleted": "Panaikinta", + "action-type-updated": "Atnaujinta", + "action-type-post-attributes": "Atributai įkelti", + "action-type-attributes-updated": "Atributai atnaujinti", + "action-type-attributes-deleted": "Atributai ištrinti", + "action-type-timeseries-updated": "Telemetrija atnaujinta", + "action-type-credentials-updated": "Įgaliojimai atnaujinti", + "action-type-assigned-to-customer": "Priskirta klientui", + "action-type-unassigned-from-customer": "Atsieta nuo kliento", + "action-type-relation-add-or-update": "Ryšys pridėtas arba atnaujintas", + "action-type-relation-deleted": "Ryšys panaikintas", + "action-type-rpc-call": "RPC iškvietimas", + "action-type-alarm-ack": "Signalizacija patvirtinta", + "action-type-alarm-clear": "Signalizacija išvalyta", + "action-type-alarm-assigned": "Signalizacija priskirta", + "action-type-alarm-unassigned": "Signalizacija atsieta", + "action-type-assigned-to-edge": "Priskirta Edge įrenginiui", + "action-type-unassigned-from-edge": "Atsieta nuo Edge įrenginio", + "action-type-credentials-request": "Įgaliojimų užklausa", + "action-type-entity-merge-request": "Subjektų sujungimo užklausa" + }, + "error": { + "unable-to-connect": "Nepavyksta prisijungti prie serverio! Patikrinkite interneto ryšį.", + "unhandled-error-code": "Neapdorotos klaidos kodas: {{errorCode}}", + "unknown-error": "Nežinoma klaida" + }, + "entity": { + "entity": "Subjektas", + "entities": "Subjektai", + "entities-count": "Subjektų kiekis", + "alarms-count": "Įspėjimų kiekis", + "aliases": "Subjektų pseudonimai", + "aliases-short": "Pseudonimai", + "entity-alias": "Subjekto pseudonimas", + "unable-delete-entity-alias-title": "Subjekto pseudonimo panaikinti nepavyko", + "unable-delete-entity-alias-text": "Subjekto pseudonimas '{{entityAlias}}' negali būti panaikintas, nes jis naudojamas šiuose valdikliuose:
    {{widgetsList}}", + "duplicate-alias-error": "Pseudonimas '{{alias}}' dubliuojasi.
    Subjektų pseudonimai skydelyje negali kartotis.", + "missing-entity-filter-error": "Trūksta pseudonimo '{{alias}}' filtro.", + "configure-alias": "Konfigūruoti pseudonimą '{{alias}}'", + "alias": "Pseudonimas", + "alias-required": "Subjekto pseudonimas būtinas.", + "remove-alias": "Pašalinti subjekto pseudonimą", + "add-alias": "Pridėti subjekto pseudonimą", + "edit-alias": "Redaguoti subjekto pseudonimą", + "entity-list": "Subjektų sąrašas", + "entity-type": "Subjekto tipas", + "entity-types": "Subjektų tipai", + "entity-type-list": "Subjektų tipų sąrašas", + "any-entity": "Bet kuris subjektas", + "add-entity-type": "Pridėti subjekto tipą", + "enter-entity-type": "Įveskite subjekto tipą", + "no-entities-matching": "Subjektų, atitinkančių '{{entity}}', nėra.", + "no-entities-text": "Subjektų nerasta", + "no-entity-types-matching": "Subjektų tipų, atitinkančių '{{entityType}}', nėra.", + "name-starts-with": "Pavadinimas prasideda", + "help-text": "Naudokite simbolį '%' pagal paieškos poreikį: '%subjekto_pavadinimo_fragmentas%', '%subjekto_pavadinimo_pabaiga', 'subjekto_pavadinimo_pradžia%'.", + "use-entity-name-filter": "Naudoti filtrą", + "entity-list-empty": "Subjektai nepasirinkti.", + "entity-type-list-required": "Bent vienas subjekto tipas turi būti pasirinktas.", + "entity-name-filter-required": "Subjekto pavadinimo filtras būtinas.", + "entity-name-filter-no-entity-matched": "Subjektų, kurių pavadinimas prasideda '{{entity}}', nėra.", + "all-subtypes": "Visi", + "select-entities": "Pasirinkite subjektus", + "no-aliases-found": "Pseudonimų nėra.", + "no-alias-matching": "Pseudonimo '{{alias}}' nėra.", + "create-new-alias": "Sukurti naują!", + "create-new": "Sukurti naują", + "key": "Raktas", + "key-name": "Rakto pavadinimas", + "no-keys-found": "Raktų nėra.", + "no-key-matching": "Rakto '{{key}}' nėra.", + "create-new-key": "Sukurti naują!", + "type": "Tipas", + "type-required": "Subjekto tipas būtinas.", + "type-device": "Įrenginys", + "type-devices": "Įrenginiai", + "list-of-devices": "{ count, plural, =1 {Vienas įrenginys} other {# įrenginių sąrašas} }", + "device-name-starts-with": "Įrenginiai, kurių pavadinimas prasideda '{{prefix}}'", + "type-device-profile": "Įrenginio profilis", + "type-device-profiles": "Įrenginių profiliai", + "clear-selected-profiles": "Išvalyti pasirinktus profilius", + "list-of-device-profiles": "{ count, plural, =1 {Vienas įrenginio profilis} other {# įrenginių profilių sąrašas} }", + "device-profile-name-starts-with": "Įrenginių profiliai, kurių pavadinimai prasideda '{{prefix}}'", + "type-asset-profile": "Turto profilis", + "type-asset-profiles": "Turto profiliai", + "list-of-asset-profiles": "{ count, plural, =1 {Vienas turto profilis} other {# turto profilių sąrašas} }", + "asset-profile-name-starts-with": "Turto profiliai, kurių pavadinimai prasideda '{{prefix}}'", + "type-asset": "Turtas", + "type-assets": "Turtai", + "list-of-assets": "{ count, plural, =1 {Vienas turtas} other {# turto sąrašas} }", + "asset-name-starts-with": "Turtas, kurio pavadinimas prasideda '{{prefix}}'", + "type-entity-view": "Subjekto rodinys", + "type-entity-views": "Subjektų rodiniai", + "list-of-entity-views": "{ count, plural, =1 {Vienas subjekto rodinys} other {# subjektų rodinių sąrašas} }", + "entity-view-name-starts-with": "Subjektų rodiniai, kurių pavadinimai prasideda '{{prefix}}'", + "type-rule": "Taisyklė", + "type-rules": "Taisyklės", + "list-of-rules": "{ count, plural, =1 {Viena taisyklė} other {# taisyklių sąrašas} }", + "rule-name-starts-with": "Taisyklės, kurių pavadinimai prasideda '{{prefix}}'", + "type-plugin": "Papildinys", + "type-plugins": "Papildiniai", + "list-of-plugins": "{ count, plural, =1 {Vienas papildinys} other {# papildinių sąrašas} }", + "plugin-name-starts-with": "Papildiniai, kurių pavadinimai prasideda '{{prefix}}'", + "type-tenant": "Valdytojas", + "type-tenants": "Valdytojai", + "list-of-tenants": "{ count, plural, =1 {Vienas valdytojas} other {# valdytojų sąrašas} }", + "tenant-name-starts-with": "Valdytojai, kurių pavadinimai prasideda '{{prefix}}'", + "type-tenant-profile": "Valdytojo profilis", + "type-tenant-profiles": "Valdytojo profiliai", + "list-of-tenant-profiles": "{ count, plural, =1 {Vienas valdytojo profilis} other {# valdytojo profilių sąrašas} }", + "tenant-profile-name-starts-with": "Valdytojo profiliai, kurių pavadinimai prasideda '{{prefix}}'", + "type-customer": "Klientas", + "type-customers": "Klientai", + "list-of-customers": "{ count, plural, =1 {Vienas klientas} other {# klientų sąrašas} }", + "customer-name-starts-with": "Klientai, kurių pavadinimai prasideda '{{prefix}}'", + "type-user": "Vartotojas", + "type-users": "Vartotojai", + "list-of-users": "{ count, plural, =1 {Vienas vartotojas} other {# vartotojų sąrašas} }", + "user-name-starts-with": "Vartotojai, kurių pavadinimai prasideda '{{prefix}}'", + "type-dashboard": "Skydelis", + "type-dashboards": "Skydeliai", + "list-of-dashboards": "{ count, plural, =1 {Vienas skydelis} other {# skydelių sąrašas} }", + "dashboard-name-starts-with": "Skydeliai, kurių pavadinimai prasideda '{{prefix}}'", + "type-alarm": "Įspėjimas", + "type-alarms": "Įspėjimai", + "list-of-alarms": "{ count, plural, =1 {Vienas įspėjimas} other {# įspėjimų sąrašas} }", + "alarm-name-starts-with": "Įspėjimai, kurių pavadinimai prasideda '{{prefix}}'", + "type-rulechain": "Taisyklių grandinė", + "type-rulechains": "Taisyklių grandinės", + "list-of-rulechains": "{ count, plural, =1 {Viena taisyklių grandinė} other {# taisyklių grandinių sąrašas} }", + "rulechain-name-starts-with": "Taisyklių grandinės, kurių pavadinimai prasideda '{{prefix}}'", + "type-rulenode": "Taisyklės mazgas", + "type-rulenodes": "Taisyklių mazgai", + "list-of-rulenodes": "{ count, plural, =1 {Vienas taisyklės mazgas} other {# taisyklių mazgų sąrašas} }", + "rulenode-name-starts-with": "Taisyklių mazgai, kurių pavadinimai prasideda '{{prefix}}'", + "type-current-customer": "Dabartinis klientas", + "type-current-tenant": "Dabartinis valdytojas", + "type-current-user": "Dabartinis vartotojas", + "type-current-user-owner": "Dabartinio vartotojo savininkas", + "type-calculated-field": "Apskaičiuotas laukas", + "type-calculated-fields": "Apskaičiuoti laukai", + "type-ai-model": "DI modelis", + "type-ai-models": "DI modeliai", + "type-widgets-bundle": "Valdiklių rinkinys", + "type-widgets-bundles": "Valdiklių rinkiniai", + "list-of-widgets-bundles": "{ count, plural, =1 {Vienas valdiklių rinkinys} other {# valdiklių rinkinių sąrašas} }", + "type-widget": "Valdiklis", + "type-widgets": "Valdikliai", + "list-of-widgets": "{ count, plural, =1 {Vienas valdiklis} other {# valdiklių sąrašas} }", + "search": "Subjektų paieška", + "selected-entities": "Pasirinkta { count, plural, =1 {1 subjektas} other {# subjektai} }", + "entity-name": "Subjekto pavadinimas", + "entity-label": "Subjekto etiketė", + "details": "Informacija apie subjektą", + "no-entities-prompt": "Subjektų nėra", + "no-data": "Nėra duomenų atvaizdavimui", + "columns-to-display": "Rodomi stulpeliai", + "type-api-usage-state": "API naudojimo būsena", + "type-edge": "Edge", + "type-edges": "Edge įrenginiai", + "list-of-edges": "{ count, plural, =1 {Vienas Edge} other {# Edge sąrašas} }", + "edge-name-starts-with": "Edge, kurių pavadinimai prasideda '{{prefix}}'", + "version-conflict": { + "message": "Ar norite perrašyti esamą versiją, ar atmesti pakeitimus ir įkelti naujausią versiją?", + "link": "Savo {{entityType}} versiją galite atsisiųsti naudojantis šia", + "overwrite": "Perrašyti versiją", + "discard": "Atmesti pakeitimus" + }, + "type-tb-resource": "Išteklius", + "type-tb-resources": "Ištekliai", + "list-of-tb-resources": "{ count, plural, =1 {Vienas išteklius} other {# išteklių sąrašas} }", + "type-ota-package": "OTA paketas", + "type-ota-packages": "OTA paketai", + "list-of-ota-packages": "{ count, plural, =1 {Vienas OTA paketas} other {# OTA paketų sąrašas} }", + "type-rpc": "RPC", + "type-queue": "Eilė", + "type-queue-stats": "Eilės statistika", + "type-queues-stats": "Eilių statistika", + "type-notification": "Pranešimas", + "type-notification-rule": "Pranešimo taisyklė", + "type-notification-rules": "Pranešimų taisyklės", + "list-of-notification-rules": "{ count, plural, =1 {Viena pranešimo taisyklė} other {# pranešimų taisyklių sąrašas} }", + "type-notification-target": "Pranešimo gavėjas", + "type-notification-targets": "Pranešimų gavėjai", + "list-of-notification-targets": "{ count, plural, =1 {Vienas pranešimo gavėjas} other {# pranešimų gavėjų sąrašas} }", + "type-notification-request": "Pranešimo užklausa", + "type-notification-template": "Pranešimo šablonas", + "type-notification-templates": "Pranešimų šablonai", + "list-of-notification-templates": "{ count, plural, =1 {Vienas pranešimo šablonas} other {# pranešimų šablonų sąrašas} }", + "link": "nuoroda", + "type-oauth2-client": "OAuth 2.0 klientas", + "type-oauth2-clients": "OAuth 2.0 klientai", + "list-of-oauth2-clients": "{ count, plural, =1 {Vienas OAuth 2.0 klientas} other {# OAuth 2.0 klientų sąrašas} }", + "type-domain": "Domenas", + "type-domains": "Domenai", + "list-of-domains": "{ count, plural, =1 {Vienas domenas} other {# domenų sąrašas} }", + "type-mobile-app": "Mobilioji programa", + "type-mobile-apps": "Mobiliosios programos", + "list-of-mobile-apps": "{ count, plural, =1 {Viena mobilioji programa} other {# mobiliųjų programų sąrašas} }", + "type-mobile-app-bundle": "Mobiliųjų programų rinkinys", + "type-mobile-app-bundles": "Mobiliųjų programų rinkiniai", + "list-of-mobile-app-bundles": "{ count, plural, =1 {Vienas mobiliųjų programų rinkinys} other {# mobiliųjų programų rinkinių sąrašas} }" + }, + "entity-field": { + "created-time": "Sukūrimo laikas", + "name": "Pavadinimas", + "type": "Tipas", + "first-name": "Vardas", + "last-name": "Pavardė", + "email": "El. paštas", + "title": "Pavadinimas", + "country": "Šalis", + "state": "Rajonas", + "city": "Miestas", + "address": "Adresas", + "address2": "Adresas 2", + "zip": "Pašto kodas", + "phone": "Telefonas", + "label": "Etiketė", + "queue-name": "Eilės pavadinimas", + "service-id": "Paslaugos ID", + "owner-name": "Savininko vardas", + "owner-type": "Savininko tipas" + }, + "entity-view": { + "entity-view": "Subjektų rodiniai", + "entity-view-required": "Subjeto rodinys būtinas.", + "entity-views": "Subjektų rodiniai", + "management": "Subjektų rodinių valdymas", + "view-entity-views": "Peržiūrėti subjektų rodinius", + "entity-view-alias": "Subjektų rodinio pseudonimas", + "aliases": "Subjektų rodinio pseudonimai", + "no-alias-matching": "'{{alias}}' nerastas.", + "no-aliases-found": "Pseudonimų nėra.", + "no-key-matching": "'{{key}}' nerastas.", + "no-keys-found": "Raktų nėra.", + "create-new-alias": "Sukurti naują!", + "create-new-key": "Sukurti naują!", + "duplicate-alias-error": "'{{alias}}' dubliuojasi.
    Įrenginio pseudonimai skydelyje turi būti unikalūs.", + "configure-alias": "Konfigūruoti '{{alias}}' pseudonimą", + "no-entity-views-matching": "Subjektų rodinių, atitinkančių '{{entity}}' nėra.", + "public": "Viešas", + "alias": "Pseudonimas", + "alias-required": "Subjekto rodinio pseudonimas būtinas.", + "remove-alias": "Panaikinti subjekto rodinio pseudonimą", + "add-alias": "Pridėti subjekto rodinio pseudonimą", + "name-starts-with": "Subjekto rodinio pavadinimas prasideda", + "help-text": "Naudokite '%' simbolį pagal tai, kaip norite ieškoti: '%subjekto_rodinio_pavadinimo_fragmentas%', '%subjekto_rodinio_pavadinimo_pabaiga', 'subjekto_rodinio_pavadinimo_pradžia%'.", + "entity-view-list": "Subjektų rodinių sąrašas", + "use-entity-view-name-filter": "Naudoti filtrą", + "entity-view-list-empty": "Subjektai nepasirinkti.", + "entity-view-name-filter-required": "Subjekto rodinio pavadinimo filtras būtinas.", + "entity-view-name-filter-no-entity-view-matched": "Subjektų rodinių, kurių pavadinimai prasideda '{{entityView}}' nėra.", + "add": "Pridėti subjektų rodinį", + "entity-view-public": "Subjektų rodnys yra viešas", + "assign-to-customer": "Priskirti klientui", + "assign-entity-view-to-customer": "Subjektų rodinį (-ius) priskirti klientui", + "assign-entity-view-to-customer-text": "Pasirinkite subjektų rodinius, kuriuos norite priskirti klientui", + "no-entity-views-text": "Subjektų rodinių nėra", + "assign-to-customer-text": "Pasirinkite klientą, kuriam priskirti subjektų rodinį (-ius)", + "entity-view-details": "Informacija apie subjektų rodinį", + "add-entity-view-text": "Pridėti naują subjektų rodinį", + "delete": "Panikinti subjektų rodinį", + "assign-entity-views": "Priskirti subjektų rodinius", + "assign-entity-views-text": "Priskirti { count, plural, =1 {1 subjektų rodinį} other {# subjektų rodinius} } klientui", + "delete-entity-views": "Panaikinti subjektų rodinius", + "make-public": "Subjektų rodinį padaryti viešu", + "make-private": "Subjektų rodinį padaryti privačiu", + "unassign-from-customer": "Atsieti nuo kliento", + "unassign-entity-views": "Atsieti subjektų rodinius", + "unassign-entity-views-action-title": "Atsieti { count, plural, =1 {1 subjektų rodinį} other {# subjektų rodinius} } nuo kliento", + "assign-new-entity-view": "Priskirti naują subjektų rodinį", + "delete-entity-view-title": "Ar tikrai norite panaikinti subjektų rodinį '{{entityViewName}}'?", + "delete-entity-view-text": "Būkite dėmesingi, po patvirtinimo, subjektų rodinys ir visa su juo susijusi informacija bus panaikinta ir jų atkurti nebegalėsite.", + "delete-entity-views-title": "Ar tikrai norite panaikinti { count, plural, =1 {1 subketų rodinį} other {# subjektų rodinius} }?", + "delete-entity-views-action-title": "Panaikinti { count, plural, =1 {1 subjektų rodinį} other {# subjektų rodinius} }", + "delete-entity-views-text": "Būkite dėmesingi, po patvirtinimo, visi pasirinkti subjektų rodiniai ir visa su jais susijusi informacija bus panaikinta ir jų atkurti nebegalėsite.", + "make-public-entity-view-title": "Ar tikrai norite subjektų rodinį '{{entityViewName}}' padaryti viešu?", + "make-public-entity-view-text": "Po patvirtinimo subjektų rodinys ir visa su juo susijusi informacija taps vieša ir matoma kitiems vartotojams.", + "make-private-entity-view-title": "Ar tikrai norite subjektų rodinį '{{entityViewName}}' padaryti privačiu?", + "make-private-entity-view-text": "Po patvirtinimo subjektų rodinys ir visa su juo susijusi informacija taps privati ir kiti vartotojai jų nebematys.", + "unassign-entity-view-title": "Ar tikrai norite atsieti subjektų rodinį '{{entityViewName}}'?", + "unassign-entity-view-text": "Po patvirtinimo subjektų rodinys bus atsietas ir klientas jo nebematys.", + "unassign-entity-view": "Atsieti subjektų rodinį", + "unassign-entity-views-title": "Ar tikrai norite atsieti { count, plural, =1 {1 subjektų rodinį} other {# subjektų rodinius} }?", + "unassign-entity-views-text": "Po patvirtinimo visi pasirinkti subjektų rodiniai bus atsieti ir klientas jų nebematys.", + "entity-view-type": "Subjektų rodinio tipas", + "entity-view-type-required": "Subjektų rodinio tipas būtinas.", + "select-entity-view-type": "Pasirinkite subjektų rodinio tipą", + "enter-entity-view-type": "Įveskite subjektų rodinio tipą", + "any-entity-view": "Bet kuris subjektų tipas", + "no-entity-view-types-matching": "Subjektų rodinių, atitinkančių '{{entitySubtype}}' nėra.", + "entity-view-type-list-empty": "Subjektų tipai nepasirinkti.", + "entity-view-types": "Subjektų rodinio tipai", + "created-time": "Sukūrimo laikas", + "name": "Pavadinimas", + "name-required": "Pavadinimas būtinas.", + "name-max-length": "Pavadinimas negali viršyti 256 simbolių", + "type-max-length": "Rodinio tipas negali viršyti 256 simbolių", + "description": "Aprašymas", + "events": "Įvykiai", + "details": "Informacija", + "copyId": "Kopijuoti subjekto rodinio Id", + "idCopiedMessage": "Subjektų rodinio Id nukopjuotas į iškarpinę", + "assignedToCustomer": "Priskirtas klientui", + "unable-entity-view-device-alias-title": "Subjektų rodinio pseudonimo ištrinti nepavyko", + "unable-entity-view-device-alias-text": "Įrenginio pseudonimas '{{entityViewAlias}}' Negali būti ištrintas, nes jis naudojamas valdikliuose:
    {{widgetsList}}", + "select-entity-view": "Pasirinkti subjektų rodinį", + "start-ts": "Pradžios laikas", + "end-ts": "Pabaigos laikas", + "date-limits": "Datos limitas", + "client-attributes": "Kliento atributai", + "shared-attributes": "Bendrinami atributai", + "server-attributes": "Serverio atributai", + "timeseries": "Telemetrija", + "client-attributes-placeholder": "Kliento atributai", + "shared-attributes-placeholder": "Bendrinami atributai", + "server-attributes-placeholder": "Serverio atributai", + "timeseries-placeholder": "Telemetrija", + "target-entity": "Tikslinis subjektas", + "attributes-propagation": "Atributų paplitimas", + "attributes-propagation-hint": "Subjektų rodinys automatikškai kopijuos nurodytus atributus iš tikslinio subjekto kiekvieną kartą, kai išsaugosite ar atnaujinsite šį rodinį. Norint išsaugoti sistemos spartą, tikslinio subjekto atributai neperkeliami į subjektų rodinį kiekvieną kartą kai jie pasikeičia. Tačiau jūs galite įjungti automatinį atributų reikšmių perdavimą, taisyklių grandinėje sukonfigūravę \"kopijuoti į vaizdą\" taisyklę ir \"Pateikti atributus\" bei \"Atributai atnaujinti\" žinutes nukreipę į naują taisyklę.", + "timeseries-data": "Temetrijos duomenys", + "timeseries-data-hint": "Sukonfigūruokite tikslinio objekto telemetrijos duomenų raktus, kurie bus pasiekiami objekto rodinyje. Šios telemetrijos duomenys yra tik skaitomi.", + "search": "Subjektų rodinių paieška", + "selected-entity-views": "Pasirinkta { count, plural, =1 {1 subjektų rodinys} other {# subjektų rodiniai} }", + "assign-entity-view-to-edge": "Priskirti subjekto rodinį (-ius) „Edge“ sistemai", + "assign-entity-view-to-edge-text": "Pasirinkite subjekto rodinius, kuriuos norite priskirti „Edge“ sistemai", + "unassign-entity-view-from-edge-title": "Ar tikrai norite atsieti subjekto rodinį '{{entityViewName}}' nuo „Edge“?", + "unassign-entity-view-from-edge-text": "Po patvirtinimo subjekto rodinys bus atsietas ir nebebus pasiekiamas per „Edge“.", + "unassign-entity-views-from-edge-action-title": "Atsieti { count, plural, =1 {1 subjekto rodinį} other {# subjekto rodinius} } nuo „Edge“", + "unassign-entity-view-from-edge": "Atsieti subjekto rodinį", + "unassign-entity-views-from-edge-title": "Ar tikrai norite atsieti { count, plural, =1 {1 subjekto rodinį} other {# subjekto rodinius} }?", + "unassign-entity-views-from-edge-text": "Po patvirtinimo visi pasirinkti subjekto rodiniai bus atsieti ir nebebus pasiekiami per „Edge“." + }, + "event": { + "event-type": "Įvykio tipas", + "events-filter": "Įvykių filtras", + "clean-events": "Išvalyti įvykius", + "type-error": "Klaida", + "type-lc-event": "Gyvavimo ciklo įvykis", + "type-stats": "Statistika", + "type-debug-rule-node": "Derinimas (Rule Node)", + "type-debug-rule-chain": "Derinimas (Rule Chain)", + "type-debug-calculated-field": "Derinimas (Calculated Field)", + "arguments": "Argumentai", + "result": "Rezultatas", + "no-events-prompt": "Įvykių nėra", + "error": "Klaida", + "alarm": "Įspėjimas", + "event-time": "Įvykio laikas", + "server": "Serveris", + "body": "Turinys", + "method": "Metodas", + "type": "Tipas", + "metadata": "Metaduomenys", + "message": "Pranešimas", + "message-id": "Pranešimo ID", + "copy-message-id": "Kopijuoti pranešimo ID", + "message-type": "Pranešimo tipas", + "data-type": "Duomenų tipas", + "relation-type": "Ryšio tipas", + "data": "Duomenys", + "event": "Įvykis", + "status": "Būsena", + "success": "Sėkmingas", + "failed": "Nepavyko", + "messages-processed": "Apdoroti pranešimai", + "max-messages-processed": "Maks. apdorotų pranešimų", + "min-messages-processed": "Min. apdorotų pranešimų", + "errors-occurred": "Įvyko klaidų", + "max-errors-occurred": "Maks. klaidų", + "min-errors-occurred": "Min. klaidų", + "min-value": "Mažiausia leistina reikšmė yra 0.", + "all-events": "Visi", + "has-error": "Turi klaidą", + "entity-id": "Subjekto ID", + "copy-entity-id": "Kopijuoti subjekto ID", + "entity-type": "Subjekto tipas", + "clear-filter": "Išvalyti filtrą", + "clear-request-title": "Išvalyti visus įvykius", + "clear-request-text": "Ar tikrai norite išvalyti visus įvykius?", + "started": "Pradėta", + "updated": "Atnaujinta", + "stopped": "Sustabdyta" + }, + "extension": { + "extensions": "Plėtiniai", + "selected-extensions": "Pasirinkta { count, plural, =1 {1 plėtinys} other {# plėtiniai} }", + "type": "Tipas", + "key": "Raktas", + "value": "Reikšmė", + "id": "Id", + "extension-id": "Plėtinio ID", + "extension-type": "Plėtinio tipas", + "transformer-json": "JSON *", + "unique-id-required": "Dabartinis plėtinio ID jau egzistuoja.", + "delete": "Pašalinti plėtinį", + "add": "Pridėti plėtinį", + "edit": "Redaguoti plėtinį", + "delete-extension-title": "Ar tikrai norite pašalinti plėtinį '{{extensionId}}'?", + "delete-extension-text": "Būkite atsargūs — po patvirtinimo šis plėtinys ir visi susiję duomenys bus negrįžtamai pašalinti.", + "delete-extensions-title": "Ar tikrai norite pašalinti { count, plural, =1 {1 plėtinį} other {# plėtinius} }?", + "delete-extensions-text": "Būkite atsargūs — po patvirtinimo visi pasirinkti plėtiniai bus pašalinti.", + "converters": "Keitikliai", + "converter-id": "Keitiklio ID", + "configuration": "Konfigūracija", + "converter-configurations": "Keitiklio konfigūracijos", + "token": "Saugos raktas", + "add-converter": "Pridėti keitiklį", + "add-config": "Pridėti keitiklio konfigūraciją", + "device-name-expression": "Įrenginio pavadinimo išraiška", + "device-type-expression": "Įrenginio tipo išraiška", + "custom": "Tinkintas", + "to-double": "Konvertuoti į Double", + "transformer": "Transformatorius", + "json-required": "Reikalingas transformatoriaus JSON.", + "json-parse": "Nepavyko apdoroti transformatoriaus JSON.", + "attributes": "Atributai", + "add-attribute": "Pridėti atributą", + "add-map": "Pridėti susiejimo elementą", + "timeseries": "Telemetrija", + "add-timeseries": "Pridėti telemetriją", + "field-required": "Laukas būtinas.", + "brokers": "Tarpiniai serveriai", + "add-broker": "Pridėti tarpinį serverį", + "host": "Serveris", + "port": "Prievadas", + "port-range": "Prievadas turi būti tarp 1 ir 65535.", + "ssl": "SSL", + "credentials": "Įgaliojimai", + "username": "Vartotojo vardas", + "password": "Slaptažodis", + "retry-interval": "Kartojimo intervalas (ms)", + "anonymous": "Anoniminis", + "basic": "Paprastas (Basic)", + "pem": "PEM", + "ca-cert": "CA sertifikato failas *", + "private-key": "Privataus rakto failas *", + "cert": "Sertifikato failas *", + "no-file": "Failas nepasirinktas.", + "drop-file": "Įmeskite failą arba spustelėkite, kad pasirinktumėte įkėlimui.", + "mapping": "Susiejimas", + "topic-filter": "Temos filtras", + "converter-type": "Keitiklio tipas", + "converter-json": "JSON", + "json-name-expression": "Įrenginio pavadinimo JSON išraiška", + "topic-name-expression": "Įrenginio pavadinimo temos išraiška", + "json-type-expression": "Įrenginio tipo JSON išraiška", + "topic-type-expression": "Įrenginio tipo temos išraiška", + "attribute-key-expression": "Atributo rakto išraiška", + "attr-json-key-expression": "Atributo rakto JSON išraiška", + "attr-topic-key-expression": "Atributo rakto temos išraiška", + "request-id-expression": "Užklausos ID išraiška", + "request-id-json-expression": "Užklausos ID JSON išraiška", + "request-id-topic-expression": "Užklausos ID temos išraiška", + "response-topic-expression": "Atsakymo temos išraiška", + "value-expression": "Reikšmės išraiška", + "topic": "Tema", + "timeout": "Laiko limitas (ms)", + "converter-json-required": "Keitiklio JSON yra būtinas.", + "converter-json-parse": "Nepavyko apdoroti keitiklio JSON.", + "filter-expression": "Filtro išraiška", + "connect-requests": "Prisijungimo užklausos", + "add-connect-request": "Pridėti prisijungimo užklausą", + "disconnect-requests": "Atsijungimo užklausos", + "add-disconnect-request": "Pridėti atsijungimo užklausą", + "attribute-requests": "Atributų užklausos", + "add-attribute-request": "Pridėti atributo užklausą", + "attribute-updates": "Atributų atnaujinimai", + "add-attribute-update": "Pridėti atributo atnaujinimą", + "server-side-rpc": "Serverio pusės RPC", + "add-server-side-rpc-request": "Pridėti serverio pusės RPC užklausą", + "device-name-filter": "Įrenginio pavadinimo filtras", + "attribute-filter": "Atributų filtras", + "method-filter": "Metodo filtras", + "request-topic-expression": "Užklausos temos išraiška", + "response-timeout": "Atsakymo laukimo limitas (ms)", + "topic-expression": "Temos išraiška", + "client-scope": "Kliento sritis", + "add-device": "Pridėti įrenginį", + "opc-server": "Serveriai", + "opc-add-server": "Pridėti serverį", + "opc-add-server-prompt": "Prašome pridėti serverį", + "opc-application-name": "Programos pavadinimas", + "opc-application-uri": "Programos URI", + "opc-scan-period-in-seconds": "Skenavimo periodas (s)", + "opc-security": "Saugumas", + "opc-identity": "Tapatybė", + "opc-keystore": "Raktinė (Keystore)", + "opc-type": "Tipas", + "opc-keystore-type": "Raktinės tipas", + "opc-keystore-location": "Vieta *", + "opc-keystore-password": "Slaptažodis", + "opc-keystore-alias": "Aliasas", + "opc-keystore-key-password": "Rakto slaptažodis", + "opc-device-node-pattern": "Įrenginio mazgo šablonas", + "opc-device-name-pattern": "Įrenginio pavadinimo šablonas", + "modbus-server": "Serveriai / vergai", + "modbus-add-server": "Pridėti serverį / vergą", + "modbus-add-server-prompt": "Prašome pridėti serverį / vergą", + "modbus-transport": "Transportas", + "modbus-tcp-reconnect": "Automatiškai prisijungti iš naujo", + "modbus-rtu-over-tcp": "RTU per TCP", + "modbus-port-name": "Serijinio prievado pavadinimas", + "modbus-encoding": "Kodavimas", + "modbus-parity": "Paritetas", + "modbus-baudrate": "Perdavimo greitis (baud)", + "modbus-databits": "Duomenų bitai", + "modbus-stopbits": "Stop bitai", + "modbus-databits-range": "Duomenų bitai turi būti nuo 7 iki 8.", + "modbus-stopbits-range": "Stop bitai turi būti nuo 1 iki 2.", + "modbus-unit-id": "Vieneto ID", + "modbus-unit-id-range": "Vieneto ID turi būti nuo 1 iki 247.", + "modbus-device-name": "Įrenginio pavadinimas", + "modbus-poll-period": "Užklausos periodas (ms)", + "modbus-attributes-poll-period": "Atributų užklausos periodas (ms)", + "modbus-timeseries-poll-period": "Telemetrijos užklausos periodas (ms)", + "modbus-poll-period-range": "Užklausos periodas turi būti teigiama reikšmė.", + "modbus-tag": "Žyma", + "modbus-function": "Funkcija", + "modbus-register-address": "Registrų adresas", + "modbus-register-address-range": "Registrų adresas turi būti nuo 0 iki 65535.", + "modbus-register-bit-index": "Bito indeksas", + "modbus-register-bit-index-range": "Bito indeksas turi būti nuo 0 iki 15.", + "modbus-register-count": "Registrų skaičius", + "modbus-register-count-range": "Registrų skaičius turi būti teigiama reikšmė.", + "modbus-byte-order": "Baitų tvarka", + "sync": { + "status": "Būsena", + "sync": "Sinchronizuota", + "not-sync": "Nesinchronizuota", + "last-sync-time": "Paskutinio sinchronizavimo laikas", + "not-available": "Nėra duomenų" + }, + "export-extensions-configuration": "Eksportuoti plėtinių konfigūraciją", + "import-extensions-configuration": "Importuoti plėtinių konfigūraciją", + "import-extensions": "Importuoti plėtinius", + "import-extension": "Importuoti plėtinį", + "export-extension": "Eksportuoti plėtinį", + "file": "Plėtinių failas", + "invalid-file-error": "Neteisingas plėtinio failas" + }, + "feature": { + "advanced-features": "Išplėstinės funkcijos" + }, + "filter": { + "add": "Pridėti filtrą", + "edit": "Redaguoti filtrą", + "name": "Filtro pavadinimas", + "name-required": "Filtro pavadinimas būtinas.", + "duplicate-filter": "Toks filtro pavadinimas jau egzistuoja.", + "filters": "Filtrai", + "unable-delete-filter-title": "Nepavyko ištrinti filtro", + "unable-delete-filter-text": "Filtras '{{filter}}' negali būti ištrintas, nes jis naudojamas šiuose valdikliuose:
    {{widgetsList}}", + "duplicate-filter-error": "Rastas pasikartojantis filtras '{{filter}}'.
    Filtrų pavadinimai skydelyje turi būti unikalūs.", + "missing-key-filters-error": "Trūksta pagrindinio filtro '{{filter}}'.", + "filter": "Filtras", + "editable": "Redaguojamas", + "editable-hint": "Leisti vartotojui pakeisti filtro reikšmę skydelyje.", + "no-filters-found": "Filtrų nerasta.", + "no-filter-text": "Filtras nenurodytas", + "add-filter-prompt": "Pridėkite filtrą", + "no-filter-matching": "Filtras '{{filter}}' nerastas.", + "create-new-filter": "Sukurti naują!", + "create-new": "Sukurti naują", + "filter-required": "Filtras būtinas.", + "operation": { + "operation": "Operacija", + "equal": "lygu", + "not-equal": "nelygu", + "starts-with": "prasideda nuo", + "ends-with": "baigiasi", + "contains": "turi reikšmę", + "not-contains": "neturi reikšmės", + "greater": "didesnis nei", + "less": "mažesnis nei", + "greater-or-equal": "didesnis arba lygus", + "less-or-equal": "mažesnis arba lygus", + "and": "ir", + "or": "arba", + "in": "yra sąraše", + "not-in": "nėra sąraše" + }, + "ignore-case": "Nepaisyti raidžių dydžio", + "value": "Reikšmė", + "remove-filter": "Pašalinti filtrą", + "duplicate-filter-action": "Dubliuoti filtrą", + "preview": "Filtro peržiūra", + "no-filters": "Filtrai nesukonfigūruoti", + "add-filter": "Pridėti filtrą", + "add-complex-filter": "Pridėti sudėtingą filtrą", + "add-complex": "Pridėti sudėtingą", + "complex-filter": "Sudėtingas filtras", + "edit-complex-filter": "Redaguoti sudėtingą filtrą", + "edit-filter-user-params": "Redaguoti filtro naudotojo parametrus", + "filter-user-params": "Filtro naudotojo parametrai", + "user-parameters": "Naudotojo parametrai", + "display-label": "Rodoma etiketė", + "custom-label": "Pasirinktinė etiketė", + "custom-label-hint": "Įjunkite, jei norite nustatyti savo etiketę filtrui. Išjungus, etiketė bus sugeneruota automatiškai.", + "order-priority": "Lauko tvarkos prioritetas", + "key-filter": "Raktinis filtras", + "key-filters": "Raktiniai filtrai", + "key-name": "Rakto pavadinimas", + "key-name-required": "Rakto pavadinimas būtinas.", + "key-type": { + "key-type": "Rakto tipas", + "attribute": "Atributas", + "timeseries": "Telemetrija", + "entity-field": "Subjekto laukas", + "constant": "Konstanta", + "client-attribute": "Kliento atributas", + "server-attribute": "Serverio atributas", + "shared-attribute": "Bendrinamas atributas" + }, + "value-type": { + "value-type": "Reikšmės tipas", + "string": "Tekstas", + "numeric": "Skaitinė", + "boolean": "Loginė (taip/ne)", + "date-time": "Data/laikas" + }, + "value-type-required": "Rakto reikšmės tipas būtinas.", + "key-value-type-change-title": "Ar tikrai norite pakeisti rakto reikšmės tipą?", + "key-value-type-change-message": "Patvirtinus naują reikšmės tipą, visi įvesti rakto filtrai bus pašalinti.", + "no-key-filters": "Rakto filtrai nesukonfigūruoti", + "add-key-filter": "Pridėti rakto filtrą", + "remove-key-filter": "Pašalinti rakto filtrą", + "edit-key-filter": "Redaguoti rakto filtrą", + "date": "Data", + "time": "Laikas", + "current-tenant": "Dabartinis valdytojas", + "current-customer": "Dabartinis klientas", + "current-user": "Dabartinis vartotojas", + "current-device": "Dabartinis įrenginys", + "default-value": "Numatytoji reikšmė", + "default-comma-separated-values": "Numatytosios reikšmės, atskirtos kableliais", + "dynamic-source-type": "Dinaminio šaltinio tipas", + "dynamic-value": "Dinaminė reikšmė", + "no-dynamic-value": "Nėra dinaminės reikšmės", + "source-attribute": "Šaltinio atributas", + "switch-to-dynamic-value": "Perjungti į dinaminę reikšmę", + "switch-to-default-value": "Perjungti į numatytąją reikšmę", + "inherit-owner": "Paveldėti iš savininko", + "source-attribute-not-set": "Jei šaltinio atributas nenustatytas", + "unit": "Vienetas" + }, + "fullscreen": { + "expand": "Rodyti per visą ekraną", + "exit": "Išjungti rodymą per visą ekraną", + "toggle": "Rodyti per visą ekraną", + "fullscreen": "Rodyti per visą ekraną" + }, + "function": { + "function": "Funkcija" + }, + "gateway": { + "gateway-name": "Šliuzo pavadinimas", + "gateway-name-required": "Šliuzo pavadinimas būtinas.", + "gateways": "Šliuzai", + "create-new-gateway": "Sukurti naują šliuzą", + "create-new-gateway-text": "Ar tikrai norite sukurti naują šliuzą pavadinimu: '{{gatewayName}}'?", + "launch-command": "Paleidimo komanda", + "no-gateway-found": "Šliuzų nerasta.", + "no-gateway-matching": "'{{item}}' nerasta." + }, + "grid": { + "delete-item-title": "Ar tikrai norite pašalinti šį elementą?", + "delete-item-text": "Būkite dėmesingi, po patvirtinimo šio elemento ir visos su juo susijusios informacijos atkurti nebegalėsite.", + "delete-items-title": "Ar tikrai norite pašalinti { count, plural, =1 {1 elementą} other {# elementus} }?", + "delete-items-action-title": "Panaikinti { count, plural, =1 {1 elementą} other {# elementus} }", + "delete-items-text": "Būkite dėmesingi, po patvirtinimo visi pasirinkti elementai ir su jais susijusi informacija bus pašalinti ir jų atkurti nebegalėsite.", + "add-item-text": "Pridėti naują elementą", + "no-items-text": "Elementų nėra", + "item-details": "Informacija apie elementą", + "delete-item": "Panaikinti elementą", + "delete-items": "Panaikinti elmentus", + "scroll-to-top": "Slinkti į viršų" + }, + "help": { + "goto-help-page": "Pagalba", + "show-help": "Pagalba" + }, + "home": { + "home": "Pagrindinis", + "profile": "Profilis", + "logout": "Atsijungti", + "menu": "Meniu", + "avatar": "Paveiksliukas", + "open-user-menu": "Atverti vartoto meniu" + }, + "file-input": { + "browse-file": "Pasirinkti failą", + "browse-files": "Pasirinkti failus" + }, + "image": { + "gallery": "Paveikslėlių galerija", + "search": "Ieškoti paveikslėlio", + "selected-images": "{ count, plural, =1 {1 paveikslėlis} other {# paveikslėliai} } pasirinkta", + "created-time": "Sukūrimo laikas", + "name": "Pavadinimas", + "name-required": "Pavadinimas būtinas.", + "resolution": "Raiška", + "size": "Dydis", + "system": "Sistema", + "download-image": "Atsisiųsti paveikslėlį", + "export-image": "Eksportuoti paveikslėlį į JSON", + "import-image": "Importuoti paveikslėlį iš JSON", + "upload-image": "Įkelti paveikslėlį", + "edit-image": "Redaguoti paveikslėlį", + "image-details": "Paveikslėlio informacija", + "no-images": "Paveikslėlių nerasta", + "delete-image": "Pašalinti paveikslėlį", + "delete-image-title": "Ar tikrai norite pašalinti paveikslėlį '{{imageTitle}}'?", + "delete-image-text": "Būkite dėmesingi — po patvirtinimo paveikslėlio atkurti nebus įmanoma.", + "delete-images-title": "Ar tikrai norite pašalinti { count, plural, =1 {1 paveikslėlį} other {# paveikslėlius} }?", + "delete-images-text": "Būkite dėmesingi — po patvirtinimo visi pasirinkti paveikslėliai bus pašalinti, o susiję duomenys taps negrąžinami.", + "list-mode": "Sąrašo rodinys", + "grid-mode": "Tinklelio rodinys", + "image-preview": "Paveikslėlio peržiūra", + "update-image": "Atnaujinti paveikslėlį", + "export-failed-error": "Nepavyko eksportuoti paveikslėlio: {{error}}", + "image-json-file": "Paveikslėlio JSON failas", + "invalid-image-json-file-error": "Nepavyko importuoti paveikslėlio iš JSON: neteisinga JSON duomenų struktūra.", + "image-is-in-use": "Paveikslėlis naudojamas kituose subjektuose", + "images-are-in-use": "Paveikslėliai naudojami kituose subjektuose", + "image-is-in-use-text": "Paveikslėlis '{{title}}' nebuvo ištrintas, nes jis naudojamas šiuose subjektuose:", + "images-are-in-use-text": "Ne visi paveikslėliai buvo ištrinti, nes jie naudojami kituose subjektuose.
    Galite peržiūrėti susijusius subjektus spustelėję Nuorodos mygtuką atitinkamoje eilutėje.
    Jei vis tiek norite ištrinti šiuos paveikslėlius, pasirinkite juos ir spustelėkite Ištrinti pasirinktus.", + "delete-image-in-use-text": "Jei vis tiek norite ištrinti paveikslėlį, spustelėkite Ištrinti vis tiek.", + "system-entities": "Sistemos subjektai:", + "entities": "Subjektai:", + "references": "Nuorodos", + "include-system-images": "Įtraukti sistemos paveikslėlius", + "clear-image": "Išvalyti paveikslėlį", + "no-image": "Nėra paveikslėlio", + "no-image-selected": "Nepasirinktas paveikslėlis", + "browse-from-gallery": "Pasirinkti iš galerijos", + "set-link": "Nustatyti nuorodą", + "image-link": "Paveikslėlio nuoroda", + "link": "Nuoroda", + "copy-image-link": "Kopijuoti paveikslėlio nuorodą", + "embed-image": "Įterpti paveikslėlį", + "embed-to-html": "Įterpti į HTML", + "embed-to-html-hint": "Ši funkcija leis nuorodą pasiekti neautorizuotiems naudotojams.", + "embed-to-html-text": "Naudodami šį kodo fragmentą galite įterpti paveikslėlį į HTML pagrįstus komponentus.
    Tokie komponentai apima HTML kortelių valdiklius, langelių turinio funkcijas ir pan.", + "embed-to-angular-template": "Įterpti į Angular HTML šabloną", + "embed-to-angular-template-text": "Naudodami šį kodo fragmentą galite įterpti paveikslėlį į Angular HTML šabloną, naudojamą komponentams.
    Tokie komponentai apima Markdown valdiklį, HTML sekciją valdiklio redaktoriuje, pasirinktus veiksmus ir pan." + }, + "image-input": { + "drop-images-or": "Užvilkite paveikslėlį arba", + "drag-and-drop": "Užvilkite", + "or": "arba", + "browse": "Pasirinkite", + "no-images": "Paveikslėlis nepasirinktas", + "images": "paveikslėliai" + }, + "import": { + "no-file": "Nepasirinktas failas", + "drop-file": "Nuvilkite JSON failą arba spustelėkite, kad pasirinktumėte failą, kurį norite įkelti.", + "drop-json-file-or": "Nuvilkite JSON failą arba", + "drop-file-csv": "Nuvilkite CSV failą arba spustelėkite, kad pasirinktumėte failą, kurį norite įkelti.", + "drop-file-csv-or": "Nuvilkite CSV failą arba", + "column-value": "Reikšmė", + "column-title": "Pavadinimas", + "column-example": "Duomenų pavyzdys", + "column-key": "Atributo/telemetrijos raktas", + "credentials": "Įgaliojimai", + "csv-delimiter": "CSV skyriklis", + "csv-first-line-header": "Pirmoje eilutėje stulpelių pavadinimai", + "csv-update-data": "Atnaujinti artibutą/telemetriją", + "details": "Informacija", + "import-csv-number-columns-error": "Faile turi būti bent du stulpeliai", + "import-csv-invalid-format-error": "Neteisingas failo formatas. Elutė: '{{line}}'", + "column-type": { + "name": "Pavadinimas", + "type": "Tipas", + "label": "Etiketė", + "column-type": "Stulpelio tipas", + "client-attribute": "Kliento atributas", + "shared-attribute": "Bendrinamas atributas", + "server-attribute": "Serverio atributas", + "timeseries": "Telemetrija", + "entity-field": "Subjekto laukas", + "access-token": "Prieigos raktas", + "x509": "X.509", + "mqtt": { + "client-id": "MQTT kliento ID", + "user-name": "MQTT vartotojo vardas", + "password": "MQTT slaptažodis" + }, + "lwm2m": { + "client-endpoint": "LwM2M kliento galinio taško pavadinimas", + "security-config-mode": "LwM2M saugumo konfigūracijos režimas", + "client-identity": "LwM2M kliento identifikatorius", + "client-key": "LwM2M kliento raktas", + "client-cert": "LwM2M kliento viešasis raktas", + "bootstrap-server-security-mode": "LwM2M pradinio serverio (bootstrap) saugumo režimas", + "bootstrap-server-secret-key": "LwM2M pradinio serverio slaptasis raktas", + "bootstrap-server-public-key-id": "LwM2M pradinio serverio viešasis raktas arba ID", + "lwm2m-server-security-mode": "LwM2M serverio saugumo režimas", + "lwm2m-server-secret-key": "LwM2M serverio slaptasis raktas", + "lwm2m-server-public-key-id": "LwM2M serverio viešasis raktas arba ID" + }, + "snmp": { + "host": "SNMP serveris (hostas)", + "port": "SNMP prievadas (portas)", + "version": "SNMP versija (v1, v2c arba v3)", + "community-string": "SNMP bendruomenės eilutė" + }, + "isgateway": "Yra šliuzas", + "activity-time-from-gateway-device": "Aktyvumo laikas iš šliuzo įrenginio", + "description": "Aprašymas", + "routing-key": "Šliuzo raktas", + "secret": "Šliuzo slaptas raktas" + }, + "stepper-text": { + "select-file": "Pasirinkite failą", + "configuration": "Importo konfigūracija", + "column-type": "Pasirinkite stulpelių tipus", + "creat-entities": "Sukurti naujus subjektus" + }, + "message": { + "create-entities": "Sukurti {{count}} nauji subjektai.", + "update-entities": "Atnaujinti {{count}} subjektai.", + "error-entities": "Kuriant {{count}} subjektų įvyko klaidų." + } + }, + "scada": { + "symbols": "SCADA simboliai", + "search": "Ieškoti simbolio", + "selected-symbols": "{ count, plural, =1 {1 simbolis} other {# simboliai} } pasirinkta", + "download-symbol": "Atsisiųsti SCADA simbolį", + "export-symbol": "Eksportuoti SCADA simbolį į JSON", + "import-symbol": "Importuoti SCADA simbolį iš JSON", + "upload-symbol": "Įkelti SCADA simbolį", + "update-symbol": "Atnaujinti SCADA simbolį", + "edit-symbol": "Redaguoti SCADA simbolį", + "symbol-details": "SCADA simbolio informacija", + "mode-svg": "SVG", + "mode-xml": "XML", + "no-symbols": "Simbolių nerasta", + "show-hidden-elements": "Rodyti paslėptus elementus", + "hide-hidden-elements": "Slėpti paslėptus elementus", + "delete-symbol": "Ištrinti SCADA simbolį", + "delete-symbol-title": "Ar tikrai norite ištrinti SCADA simbolį '{{imageTitle}}'?", + "delete-symbol-text": "Būkite atsargūs — po patvirtinimo SCADA simbolis bus negrįžtamai pašalintas.", + "delete-symbols-title": "Ar tikrai norite ištrinti { count, plural, =1 {1 SCADA simbolį} other {# SCADA simbolius} }?", + "delete-symbols-text": "Būkite atsargūs — po patvirtinimo visi pasirinkti SCADA simboliai ir susiję duomenys bus negrįžtamai pašalinti.", + "include-system-symbols": "Įtraukti sistemos simbolius", + "symbol-preview": "Simbolio peržiūra", + "general": "Bendra informacija", + "tags": "Žymos", + "properties": "Savybės", + "title": "Pavadinimas", + "description": "Aprašymas", + "search-tags": "Ieškoti žymų", + "widget-size": "Valdiklio dydis", + "cols": "stulpeliai", + "rows": "eilutės", + "state-render-function": "Būsenos atvaizdavimo funkcija", + "preview": "Peržiūra", + "preview-widget-action-text": "Valdiklio veiksmas '{{type}}' sėkmingai įvykdytas!", + "no-symbol": "Nėra SCADA simbolio", + "no-symbol-selected": "Nepasirinktas SCADA simbolis", + "clear-symbol": "Išvalyti SCADA simbolį", + "browse-symbol-from-gallery": "Pasirinkti SCADA simbolį iš galerijos", + "zoom-in": "Priartinti", + "zoom-out": "Atitolinti", + "create-widget": "Sukurti valdiklį", + "create-widget-from-symbol": "Sukurti valdiklį iš SCADA simbolio", + "hidden": "paslėpta", + "tag": { + "tag": "Žyma", + "on-click-action": "Veiksmas paspaudus", + "no-tags": "Žymos nesukonfigūruotos", + "delete-tag-text": "Ar tikrai norite pašalinti žymą
    {{tag}}{{elementType}} elemento?", + "update-tag": "Atnaujinti žymą", + "enter-tag": "Įveskite žymą", + "tag-settings": "Žymos nustatymai", + "remove-tag": "Pašalinti žymą", + "add-tag": "Pridėti žymą" + }, + "behavior": { + "behavior": "Elgsena", + "id": "ID", + "name": "Pavadinimas", + "type": "Tipas", + "no-behaviors": "Elgsenos nesukonfigūruotos", + "add-behavior": "Pridėti elgseną", + "type-action": "Veiksmas", + "type-value": "Reikšmė", + "type-widget-action": "Valdiklio veiksmas", + "behavior-settings": "Elgsenos nustatymai", + "remove-behavior": "Pašalinti elgseną", + "hint": "Patarimas", + "group-title": "Grupės pavadinimas", + "value-type": "Reikšmės tipas", + "default-value": "Numatytoji reikšmė", + "true-label": "Tiesa etiketė", + "false-label": "Netiesa etiketė", + "state-label": "Būsenos etiketė", + "default-payload": "Numatytasis krovinys", + "not-unique-behavior-ids-error": "Elgsenos ID turi būti unikalūs!", + "default-settings": "Numatytieji nustatymai" + }, + "symbol": { + "symbol": "SCADA simbolis", + "fluid-presence": "Skysčio buvimas", + "fluid-presence-hint": "Nurodo, ar vamzdyje yra skysčio.", + "fluid-present": "Skystis yra", + "present": "Yra", + "absent": "Nėra", + "flow-presence": "Srauto buvimas", + "flow-presence-hint": "Nurodo, ar vamzdyje teka skystis.", + "flow-present": "Srautas yra", + "flow-direction": "Srauto kryptis", + "flow-direction-hint": "Nurodo skysčio tekėjimo kryptį.", + "forward": "Pirmyn", + "reverse": "Atgal", + "flow-animation-speed": "Srauto animacijos greitis", + "flow-animation-speed-hint": "Reikšmė, nurodanti animacijos greitį. 1 – normalus greitis, 0 – be animacijos, < 1 – lėtesnė, > 1 – greitesnė animacija.", + "leak": "Nuotėkis", + "leak-hint": "Nurodo, ar yra nuotėkis vamzdyje.", + "leak-present": "Nuotėkis yra", + "fluid-color": "Skysčio spalva", + "pipe-color": "Vamzdžio spalva", + "horizontal-pipe": "Horizontalus vamzdis", + "vertical-pipe": "Vertikalus vamzdis", + "horizontal-fluid-color": "Horizontalaus skysčio spalva", + "vertical-fluid-color": "Vertikalaus skysčio spalva", + "left-pipe": "Kairysis vamzdis", + "right-pipe": "Dešinysis vamzdis", + "top-pipe": "Viršutinis vamzdis", + "bottom-pipe": "Apatinis vamzdis", + "left-fluid-color": "Kairiojo skysčio spalva", + "right-fluid-color": "Dešiniojo skysčio spalva", + "top-fluid-color": "Viršutinio skysčio spalva", + "bottom-fluid-color": "Apatinio skysčio spalva", + "display": "Ekranas", + "display-format": "Ekrano formatas", + "value": "Reikšmė", + "decimals": "Skaitmenys po kablelio", + "units": "Vienetai", + "flow-meter-value-hint": "Reikšmė rodoma srauto matuoklyje", + "value-hint": "Reikšmė, nurodanti dabartinį dydį", + "running": "Veikia", + "running-hint": "Nurodo, ar komponentas veikia.", + "warning-state": "Įspėjimo būsena", + "warning": "Įspėjimas", + "warning-click": "Įspėjimo paspaudimas", + "warning-state-hint": "Nurodo, ar komponentas yra įspėjimo būsenoje.", + "critical-state": "Kritinė būsena", + "critical": "Kritinis", + "critical-click": "Kritinio paspaudimas", + "critical-state-hint": "Nurodo, ar komponentas yra kritinėje būsenoje.", + "critical-state-animation": "Kritinės būsenos animacija", + "critical-state-animation-hint": "Įjungia mirksėjimo animaciją, kai komponentas yra kritinėje būsenoje.", + "warning-critical-state-animation": "Įspėjimo/kritinės būsenos animacija", + "warning-critical-state-animation-hint": "Įjungia mirksėjimo animaciją, kai komponentas yra įspėjimo ar kritinėje būsenoje.", + "animation": "Animacija", + "broken": "Sugedęs", + "broken-hint": "Nurodo, ar komponentas sugedęs.", + "on-display-click": "Paspaudus ekraną", + "on-display-click-hint": "Veiksmas, atliekamas paspaudus ekraną.", + "pipe": "Vamzdis", + "default-border-color": "Numatytoji rėmelio spalva", + "active-border-color": "Aktyvaus rėmelio spalva", + "warning-border-color": "Įspėjimo rėmelio spalva", + "critical-border-color": "Kritinio rėmelio spalva", + "background-color": "Fono spalva", + "rotation-animation-speed": "Sukimosi animacijos greitis", + "rotation-animation-speed-hint": "Reikšmė, nurodanti sukimosi animacijos greitį. 1 – normalus, 0 – be animacijos, < 1 – lėtesnė, > 1 – greitesnė animacija.", + "on-click": "Paspaudus", + "on-click-hint": "Veiksmas, atliekamas paspaudus komponentą.", + "connectors-positions": "Jungčių pozicijos", + "right-connector": "Dešinė jungtis", + "right-top-connector": "Dešinysis viršutinis jungtuvas.", + "right-bottom-connector": "Dešinysis apatinis jungtuvas.", + "left-connector": "Kairė jungtis", + "left-top-connector": "Kairysis viršutinis jungtuvas.", + "left-bottom-connector": "Kairysis apatinis jungtuvas.", + "top-left-connector": "Kairysis viršutinis jungtuvas.", + "top-right-connector": "Dešinysis viršutinis jungtuvas.", + "top-connector": "Viršutinė jungtis", + "bottom-connector": "Apatinė jungtis", + "running-color": "Veikimo spalva", + "stopped-color": "Sustojimo spalva", + "stopped": "Sustojęs", + "warning-color": "Įspėjimo spalva", + "critical-color": "Kritinė spalva", + "opened": "Atidaryta", + "opened-hint": "Nurodo, ar komponentas atidarytas.", + "open": "Atidaryti", + "open-hint": "Veiksmas, atliekamas vartotojui spustelėjus, kad atidarytų komponentą.", + "close": "Uždaryti", + "close-hint": "Veiksmas, atliekamas vartotojui spustelėjus komponento uždarymą.", + "close-state-animation": "Uždarytos būsenos animacija.", + "close-state-animation-hint": "Ar įjungti mirksinčią animaciją, kai komponentas yra uždarytos būsenos.", + "opened-color": "Atidarytos būsenos spalva.", + "closed-color": "Uždarytos būsenos spalva.", + "opened-rotation-angle": "Atidarytos būsenos pasukimo kampas.", + "closed-rotation-angle": "Uždarytos būsenos pasukimo kampas.", + "tank-capacity": "Talpyklos talpa", + "tank-capacity-hint": "Slankiojo kablelio reikšmė, nurodanti bendrą bako talpą.", + "current-volume": "Dabartinis tūris", + "current-volume-hint": "Slankiojo kablelio reikšmė, nurodanti šiuo metu užimtą tūrį.", + "tank-color": "Talpyklos spalva", + "value-box": "Reikšmės langelis.", + "value-text": "Reikšmės tekstas.", + "scale": "Skalė", + "transparent-mode": "Permatomas režimas.", + "major-ticks": "Pagrindiniai žymekliai.", + "intervals": "Intervalai.", + "major-ticks-color": "Pagrindinių žymeklių spalva.", + "normal": "Normalu", + "minor-ticks": "Smulkieji žymekliai.", + "minor-ticks-color": "Smulkiųjų žymeklių spalva.", + "temperature": "Temperatūra", + "temperature-hint": "Slankiojo kablelio reikšmė, nurodanti dabartinę temperatūrą.", + "update-temperature": "Atnaujinti temperatūrą.", + "update-temperature-hint": "Veiksmas, atliekamas vartotojui spustelėjus, kad pakeistų dabartinę temperatūrą.", + "run": "Paleisti", + "run-hint": "Veiksmas, atliekamas vartotojui spustelėjus, kad paleistų komponentą.", + "stop": "Sustabdyti", + "stop-hint": "Veiksmas, atliekamas vartotojui spustelėjus, kad sustabdytų komponentą.", + "temperature-step": "Temperatūros žingsnio padidinimas.", + "heat-pump-color": "Šilumos siurblio spalva.", + "power-button-background": "Maitinimo mygtuko fonas.", + "value-box-background": "Value box background", + "value-units": "Reikšmės vienetai.", + "enable-units-scale": "Įjungti vienetus skalėje.", + "filtration-mode": "Filtravimo režimas.", + "filtration-mode-hint": "Sveikasis skaičius, nurodantis dabartinį filtravimo režimą.", + "filtration-mode-update": "Filtravimo režimo atnaujinimo būsena.", + "filtration-mode-update-hint": "Veiksmas, atliekamas vartotojui spustelėjus, kad pakeistų dabartinį filtravimo režimą.", + "filter-mode": "Filtras.", + "waste-mode": "Atliekos.", + "backwash-mode": "Atbulinis plovimas.", + "recirculate-mode": "Recirkuliacija.", + "rinse-mode": "Skalavimas.", + "closed-mode": "Uždaryta.", + "sand-filter-color": "Smėlio filtro spalva.", + "mode-box-background": "Režimo langelio fonas.", + "border-color": "Kraštinės spalva.", + "label-color": "Etiketės spalva", + "water-leak-hint": "Nurodo, ar yra nutekėjimas.", + "default-color": "Numatytoji spalva.", + "leak-color": "Nutekėjimo spalva.", + "full-value": "Pilna reikšmė.", + "full-value-hint": "Slankiojo kablelio reikšmė, nurodanti pilną reikšmę.", + "label": "Etiketė", + "icon": "Ikona", + "button-color": "Mygtuko spalva.", + "on-label": "„Įjungta“ žymės tekstas.", + "off-label": "„Išjungta“ žymės tekstas.", + "arrow-presence": "Rodyklės buvimas.", + "arrow-presence-hint": "Nurodo, ar jungtyje yra rodyklė.", + "arrow-present": "Rodyklė yra.", + "arrow-direction": "Srauto kryptis.", + "arrow-direction-hint": "Nurodo srauto kryptį.", + "flow-animation": "Srauto buvimas.", + "flow-animation-hint": "Nurodo, ar jungtyje teka skystis.", + "flow": "Srautas.", + "flow-line": "Linija.", + "flow-line-style": "Linijos stilius.", + "flow-style-hint": "Nustatykite brūkšnio ir tarpo reikšmes taip, kad jų suma būtų daloma iš 100 be liekanos, kad animacija būtų idealiai sinchronizuota.", + "flow-dash-cap": "Brūkšnio galas.", + "dash-cap-butt": "Galinė briauna.", + "dash-cap-round": "Apvalus.", + "dash-cap-square": "Kvadratinis.", + "dash": "Brūkšnys.", + "gap": "Tarpas.", + "main-line": "Pagrindinė linija.", + "line": "Linija", + "line-color": "Linijos spalva", + "arrow-color": "Rodyklės spalva.", + "target-value": "Tikslo reikšmė.", + "target-value-hint": "Nurodo tikslo tašką skalėje.", + "min-max-value": "Minimali ir maksimali reikšmė.", + "min-value": "Min.", + "max-value": "Maks.", + "progress-bar": "Eigos juosta.", + "progress-arrow": "Eigos rodyklė.", + "warning-scale-color": "Įspėjimo skalės spalva.", + "critical-scale-color": "Kritinės skalės spalva.", + "scale-color": "Skalės spalva.", + "target": "Tikslas.", + "high-warning-state": "Aukšto įspėjimo būsena.", + "show-high-warning-scale": "Rodyti aukšto įspėjimo skalės reikšmę.", + "high-warning-scale": "Aukšto įspėjimo skalė.", + "high-warning-state-hint": "Slankiojo kablelio reikšmė, nurodanti aukšto įspėjimo diapazoną iki aukštos kritinės arba didžiausios reikšmės.", + "low-warning-state": "Žemo įspėjimo būsena.", + "show-low-warning-scale": "Rodyti žemo įspėjimo skalės reikšmę.", + "low-warning-scale": "Žemo įspėjimo skalė.", + "low-warning-state-hint": "Slankiojo kablelio reikšmė, nurodanti žemo įspėjimo diapazoną iki žemos kritinės arba minimalios reikšmės.", + "high-critical-state": "Aukšta kritinė būsena.", + "show-high-critical-scale": "Rodyti aukštos kritinės skalės reikšmę.", + "high-critical-scale": "Aukšta kritinė skalė.", + "high-critical-state-hint": "Slankiojo kablelio reikšmė, nurodanti aukštą kritinį diapazoną iki didžiausios skalės reikšmės.", + "low-critical-state": "Žemos kritinės būsenos reikšmė.", + "show-low-critical-scale": "Rodyti žemos kritinės būsenos reikšmę.", + "low-critical-scale": "Žema kritinė būsena.", + "low-critical-state-hint": "Slankiojo kablelio reikšmė, nurodanti žemos kritinės reikšmės diapazoną iki minimalios skalės reikšmės.", + "filter-color": "Filtro spalva", + "colors": "Spalvos", + "indicator-colors": "Indikatorių spalvos", + "enabled": "Įjungta", + "disabled": "Išjungta", + "on": "Įjungta", + "off": "Išjungta", + "on-off-state": "Įjungimo/Išjungimo būsena.", + "on-off-state-hint": "Nurodo, ar komponentas yra įjungtos, ar išjungtos būsenos.", + "on-update-state": "Įjungimo atnaujinimo būsena.", + "on-update-state-hint": "Veiksmas, atliekamas vartotojui spustelėjus, kad atnaujintų būseną į „Įjungta“.", + "off-update-state": "Išjungimo atnaujinimo būsena.", + "off-update-state-hint": "Veiksmas, atliekamas vartotojui spustelėjus, kad atnaujintų būseną į „Išjungta“.", + "voltage": "Įtampa", + "input-voltage": "Įėjimo įtampa.", + "input-voltage-hint": "Slankiojo kablelio reikšmė, nurodanti įėjimo įtampos dydį.", + "output-voltage": "Išėjimo įtampa.", + "output-voltage-hint": "Slankiojo kablelio reikšmė, nurodanti išėjimo įtampos dydį.", + "first-phase-voltage": "Pirmosios fazės įtampa.", + "second-phase-voltage": "Antrosios fazės įtampa.", + "third-phase-voltage": "Trečiosios fazės įtampa.", + "phase-voltage-hint": "Slankiojo kablelio reikšmė, nurodanti dabartinės fazės įtampos dydį.", + "voltage-hint": "Slankiojo kablelio reikšmė, nurodanti dabartinę įtampą.", + "current-voltage-color": "Dabartinės įtampos spalva.", + "phase-indicator-color": "Fazės indikatoriaus spalva.", + "measured": "Išmatuota.", + "measured-hint": "Slankiojo kablelio reikšmė, nurodanti energijos suvartojimą kilovatvalandėmis.", + "day-rate": "Dienos tarifas.", + "night-rate": "Naktinis tarifas.", + "off-peak-rate": "Ne piko tarifas.", + "peak-rate": "Piko tarifas.", + "export-rate": "Eksporto tarifas.", + "operating-mode": "Veikimo režimas.", + "bypass-mode": "Apeiti.", + "operating-mode-hint": "Sveikasis skaičius, nurodantis dabartinį veikimo režimą (0 – IŠJUNGTA, 1 – ĮJUNGTA, 2 – APEINIMAS).", + "connected": "Prijungta", + "connected-hint": "Nurodo, ar komponentas yra prijungtos būsenos.", + "disconnected": "Atjungta", + "indicator": "Indikatorius", + "operation-mode": "Veikimo režimas", + "operation-mode-hint": "Nurodo, ar keitiklis veikia tinklo (Mains), ar inverterio (Inverter) režimu.", + "operation-mode-indicators-color": "Veikimo režimo indikatorių spalva.", + "mains-on-mode": "Tinklas įjungtas.", + "inverter-on-mode": "Keitiklis įjungtas.", + "charging-mode": "Įkrovimo režimas", + "charging-mode-hint": "Sveikasis skaičius, nurodantis dabartinį įkrovimo režimą (1 – Greitas įkrovimas, 2 – Sugėrimas, 3 – Palaikomasis įkrovimas).", + "charging-mode-indicators-color": "Įkrovimo režimo indikatorių spalva.", + "inverter-faults": "Gedimai.", + "inverter-fault-indicators-color": "Gedimo indikatorių spalva.", + "overload-fault": "Perkrova", + "overload-fault-hint": "Nurodo, ar keitiklis yra perkrovos būsenoje.", + "low-battery-fault": "Silpna baterija", + "low-battery-fault-hint": "Nurodo, ar akumuliatorius yra per daug išsikrovęs.", + "temperature-fault": "Temperatūra", + "temperature-fault-hint": "Nurodo, ar keitiklyje yra aukšta temperatūra.", + "triangle": "Trikampis.", + "socket": "Lizdas.", + "left-button": "Kairysis mygtukas.", + "right-button": "Dešinysis mygtukas.", + "alarm-colors": "Aliarmo spalvos", + "hook-color": "Kabliuko spalva" + } + }, + "item": { + "selected": "Pasirinkta" + }, + "js-func": { + "no-return-error": "Funkcija privalo grąžinti reikšmę!", + "return-type-mismatch": "Funkcijos grąžinama reikšmė privalo būti '{{type}}' tipo!", + "tidy": "Sutvarkyti", + "mini": "Sumažinti", + "modules": "Moduliai", + "remove-module": "Pašalinti modulį", + "no-modules": "Modulių nesukonfigūruota", + "add-module": "Pridėti modulį", + "module-alias": "Pseudonimas", + "invalid-module-alias-name": "Neteisingas pseudonimo pavadinimas", + "module-resource": "JS modulio šaltinis", + "not-unique-module-aliases-error": "Modulių pseudonimai turi būti unikalūs!", + "show-module-info": "Rodyti modulio informaciją", + "show-module-source-code": "Rodyti modulio pirminį kodą", + "module-members": "Modulio nariai", + "module-no-members": "Modulis neturi eksportuotų narių", + "module-load-error": "Modulio įkėlimo klaida", + "source-code": "Pirminis kodas", + "source-code-load-error": "Pirminio kodo įkėlimo klaida", + "no-js-module-text": "JS modulių nerasta", + "no-js-module-matching": "Nerasta JS modulių, atitinkančių '{{module}}'." + }, + "key-val": { + "key": "Raktas", + "value": "Reikšmė", + "remove-entry": "Panaiknti įrašą", + "add-entry": "Pridėti įrašą", + "no-data": "Įrašų nėra" + }, + "layout": { + "layout": "Maketas", + "layouts": "Maketai", + "manage": "Valdyti maketus", + "settings": "Maketų nustatymai", + "color": "Spalva", + "main": "Pagrindinis", + "right": "Į dešinę", + "left": "Į kairę", + "select": "Pasirinkti maketą", + "percentage-width": "Procentinis plotis (%)", + "fixed-width": "Fiksuotas plotis (px)", + "left-width": "Kairysis stulpelis (%)", + "right-width": "Dešinysis stulpelis (%)", + "pick-fixed-side": "Fiksuota pusė: ", + "layout-fixed-width": "Fiksuotas plotis (px)", + "value-min-error": "Reikšmė turi būti dedesnė nei {{min}}{{unit}}", + "value-max-error": "Reikšmė turi būti mažesnė nei {{max}}{{unit}}", + "layout-fixed-width-required": "Fiksuotas plotis būtinas", + "right-width-percentage-required": "Dešinės pusės procentas būtinas", + "left-width-percentage-required": "kairės pusės procentas būtinas", + "divider": "Daliklis", + "right-side": "Dešinės pusės maketas", + "left-side": "Kairės pusės maketas", + "add-new-breakpoint": "Pridėti naują pertraukos tašką", + "breakpoint": "Pertraukos taškas", + "breakpoints": "Pertraukos taškai", + "copy-from": "Kopijuoti iš", + "size": "Dydis", + "delete-breakpoint-title": "Ar tikrai norite ištrinti pertraukos tašką '{{name}}'?", + "delete-breakpoint-text": "Atkreipkite dėmesį, kad po patvirtinimo pertraukos taškas bus negrįžtamai pašalintas, o nustatymai bus atkurti pagal numatytąjį pertraukos tašką." + }, + "legend": { + "direction": "Elementų išdėstymo kryptis", + "position": "Legendos pozicija", + "show-values": "Rodyti reikšmes", + "min-option": "Min", + "max-option": "Max", + "average-option": "Vidurkis", + "total-option": "Viso", + "latest-option": "Naujausi", + "sort-legend": "Legendoje rodyti duomenų raktus", + "show-max": "Rodyti didžiausią reišmę", + "show-min": "Rodyti mažiausią reikšmę", + "show-avg": "Rodyti vidutinę reikšmę", + "show-total": "Rodyti suminę rekšmę", + "show-latest": "Rodyti naujausią reikšmę", + "settings": "Legendos nustatymai", + "min": "Min", + "max": "Max", + "avg": "Vidurkis", + "total": "Viso", + "latest": "Naujausia", + "Min": "Min", + "Max": "Max", + "Avg": "Vid.", + "Total": "Viso", + "Latest": "Vėliausia", + "comparison-time-ago": { + "previousInterval": "(ankstesnis intervalas)", + "customInterval": "(pasirinktinis intervalas)", + "days": "(prieš dieną)", + "weeks": "(prieš savaitę)", + "months": "(prieš mėnesį)", + "years": "(prieš metus)" + }, + "column-title": "Stulpelio pavadinimas", + "label": "Etiketė", + "value": "Reikšmė" + }, + "login": { + "login": "Prisijungti", + "request-password-reset": "Prašyti slaptažodžio atstatymo", + "reset-password": "Atstatyti slaptažodį", + "create-password": "Sukurti slaptažodį", + "two-factor-authentication": "Dviejų veiksnių autentifikavimas", + "passwords-mismatch-error": "Įvesti slaptažodžiai turi sutapti!", + "password-again": "Pakartokite slaptažodį", + "sign-in": "Prisijungti", + "username": "Vartotojo vardas (el. paštas)", + "remember-me": "Prisiminti mane", + "forgot-password": "Pamiršote slaptažodį?", + "password-reset": "Slaptažodis atstatytas", + "expired-password-reset-message": "Jūsų slaptažodžio galiojimas baigėsi! Nustatykite naują slaptažodį.", + "new-password": "Naujas slaptažodis", + "new-password-again": "Pakartokite naują slaptažodį", + "password-link-sent-message": "Atstatymo nuoroda išsiųsta", + "email": "El. paštas", + "invalid-email-format": "Neteisingas el. pašto formatas.", + "login-with": "Prisijungti kaip {{name}}", + "or": "arba", + "error": "Prisijungimo klaida", + "verify-your-identity": "Patvirtinkite savo tapatybę", + "select-way-to-verify": "Pasirinkite tapatybės patvirtinimo būdą", + "resend-code": "Siųsti kodą iš naujo", + "resend-code-wait": "Galima siųsti iš naujo po { time, plural, =1 {1 sekundės} other {# sekundžių} }", + "try-another-way": "Bandykite kitu būdu", + "totp-auth-description": "Įveskite saugos kodą iš autentifikavimo programėlės.", + "totp-auth-placeholder": "Kodas", + "sms-auth-description": "Saugos kodas išsiųstas į jūsų telefoną adresu {{contact}}.", + "sms-auth-placeholder": "SMS kodas", + "email-auth-description": "Saugos kodas išsiųstas į jūsų el. paštą adresu {{contact}}.", + "email-auth-placeholder": "El. pašto kodas", + "backup-code-auth-description": "Įveskite vieną iš atsarginių kodų.", + "backup-code-auth-placeholder": "Atsarginis kodas", + "activation-link-expired": "Aktyvavimo nuorodos galiojimas baigėsi", + "activation-link-expired-message": "Profilio aktyvavimo nuoroda nebegalioja. Galite grįžti į prisijungimo puslapį ir gauti naują el. laišką.", + "reset-password-link-expired": "Slaptažodžio atstatymo nuoroda nebegalioja", + "reset-password-link-expired-message": "Slaptažodžio atstatymo nuoroda nebegalioja. Galite grįžti į prisijungimo puslapį ir gauti naują el. laišką." + }, + "mobile": { + "add-application": "Pridėti programėlę", + "app-id": "Programėlės svetainės asociacijos ID", + "app-id-required": "Programėlės svetainės asociacijos ID yra būtinas", + "app-id-pattern": "Neteisingas programėlės svetainės asociacijos ID formatas", + "app-store-link": "App Store nuoroda", + "app-store-link-required": "App Store nuoroda yra būtina", + "application-details": "Programėlės informacija", + "application-package": "Programėlės paketas", + "application-secret": "Programėlės slaptas raktas", + "application-secret-required": "Programėlės slaptas raktas yra būtinas", + "application": "Programėlė", + "applications": "Programėlės", + "copy-app-id": "Kopijuoti programėlės ID", + "copy-app-store-link": "Kopijuoti App Store nuorodą", + "copy-application-package": "Kopijuoti programėlės paketą", + "copy-application-secret": "Kopijuoti programėlės slaptą raktą", + "copy-google-play-link": "Kopijuoti Google Play nuorodą", + "copy-sha256-certificate-fingerprints": "Kopijuoti SHA256 sertifikato atspaudus", + "delete-application": "Ištrinti programėlę", + "delete-application-button-text": "Suprantu pasekmes, ištrinti programėlę", + "delete-application-text": "Šio veiksmo atšaukti nebus galima. Tai visam laikui ištrins jūsų programėlę.
    Jei nenorite jos ištrinti visam laikui, galite laikinai ją sustabdyti.
    Norėdami vis tiek ištrinti programėlę, įveskite patvirtinimo frazę \"{{phrase}}\".", + "delete-application-title-short": "Ar tikrai norite ištrinti programėlę '{{name}}'?", + "delete-application-text-short": "Būkite atsargūs – po patvirtinimo programėlė ir visi susiję duomenys bus negrįžtamai pašalinti.", + "delete-application-phrase": "ištrinti programėlę", + "delete-applications-bundle-text": "Būkite atsargūs – po patvirtinimo mobilusis paketas ir visi susiję duomenys bus negrįžtamai pašalinti.", + "delete-applications-bundle-title": "Ar tikrai norite ištrinti mobilųjį paketą '{{bundleName}}'?", + "generate-application-secret": "Generuoti programėlės slaptą raktą", + "google-play-link": "Google Play nuoroda", + "google-play-link-required": "Google Play nuoroda yra būtina", + "latest-version": "Naujausia versija", + "min-version": "Minimali versija", + "invalid-version-pattern": "Neteisingas versijos formatas. Naudokite formatą: major.minor.patch (pvz., 1.0.0).", + "mobile-center": "Mobilusis centras", + "mobile-package": "Programėlės paketas", + "mobile-package-max-length": "Programėlės pavadinimas turi būti trumpesnis nei 256 simboliai", + "mobile-package-required": "Programėlės paketas yra būtinas", + "mobile-package-pattern": "Neteisingas programėlės paketo formatas", + "mobile-package-title": "Programėlės pavadinimas", + "mobile-package-title-max-length": "Programėlės pavadinimas turi būti trumpesnis nei 256 simboliai", + "no-application": "Programėlių nerasta", + "no-bundles": "Paketų nerasta", + "platform-type": "Platformos tipas", + "search-application": "Ieškoti programėlių", + "search-bundles": "Ieškoti paketų", + "set": "Nustatyti", + "sha256-certificate-fingerprints": "SHA256 sertifikato atspaudai", + "sha256-certificate-fingerprints-required": "SHA256 sertifikato atspaudai yra būtini", + "sha256-certificate-fingerprints-pattern": "Neteisingas SHA256 sertifikato atspaudo formatas", + "show-hidden-pages": "Rodyti paslėptus puslapius", + "status": "Būsena", + "status-type": { + "deprecated": "Nebenaudojama", + "draft": "Juodraštis", + "published": "Paskelbta", + "suspended": "Sustabdyta" + }, + "store-information": "Parduotuvės informacija", + "version-information": "Versijos informacija", + "min-version-release-notes": "Minimalios versijos išleidimo pastabos", + "latest-version-release-notes": "Naujausios versijos išleidimo pastabos", + "bundle": "Paketas", + "bundles": "Paketai", + "add-bundle": "Pridėti paketą", + "title": "Pavadinimas", + "title-required": "Pavadinimas būtinas", + "title-cannot-contain-only-spaces": "Pavadinimas negali būti tik iš tarpų", + "title-max-length": "Pavadinimas turi būti trumpesnis nei 256 simboliai", + "oauth-clients": "OAuth 2.0 klientai", + "android-app": "Android programa", + "android-application": "Android aplikacija", + "ios-app": "iOS programa", + "ios-application": "iOS aplikacija", + "invalid-store-link": "Neteisinga parduotuvės nuoroda", + "enable-oauth": "Įjungti OAuth 2.0", + "enable-self-registration": "Įjungti savarankišką registraciją", + "edit-bundle": "Redaguoti paketą", + "description": "Aprašymas", + "basic-settings": "Pagrindiniai nustatymai", + "no-application-matching": "Nerasta programų, atitinkančių '{{entity}}'.", + "no-bundle-matching": "Nerasta paketų, atitinkančių '{{entity}}'.", + "application-required": "Programa būtina.", + "bundle-required": "Paketas būtinas.", + "no-application-text": "Programų nerasta", + "no-bundle-text": "Paketų nerasta", + "layout": "Išdėstymas", + "pages": "Puslapiai", + "hide-all-pages": "Slėpti visus puslapius", + "reset-to-default-pages": "Atstatyti numatytuosius puslapius", + "add-specific-page": "Pridėti konkretų puslapį", + "visible": "Matomas", + "hidden": "Paslėptas", + "reset-to-page-default": "Atstatyti puslapį į numatytąją būseną", + "mobile-599": "Mobilus (maks. 599px)", + "tablet-959": "Planšetė (maks. 959px)", + "max-element-number": "Maksimalus elementų skaičius", + "page-name": "Puslapio pavadinimas", + "page-name-required": "Puslapio pavadinimas būtinas.", + "page-name-cannot-contain-only-spaces": "Puslapio pavadinimas negali būti tik iš tarpų.", + "page-name-max-length": "Puslapio pavadinimas turi būti trumpesnis nei 256 simboliai", + "page-type": "Puslapio tipas", + "pages-types": { + "dashboard": "Valdymo skydas", + "web-view": "Žiniatinklio peržiūra", + "custom": "Pasirinktinis" + }, + "url": "URL", + "invalid-url-format": "Neteisingas URL formatas", + "path": "Kelias", + "invalid-path-format": "Neteisingas kelio formatas", + "custom-page": "Pasirinktinis puslapis", + "edit-page": "Redaguoti puslapį", + "edit-custom-page": "Redaguoti pasirinktą puslapį", + "delete-page": "Ištrinti puslapį", + "qr-code-widget": "QR kodo valdiklis", + "type-here": "Įveskite čia", + "configuration-dialog": "Konfigūracijos dialogas", + "configuration-app": "Konfigūracijos programa", + "configuration-step": { + "prepare-environment-title": "Paruošti kūrimo aplinką", + "prepare-environment-text": "Flutter ThingsBoard mobiliajai programai reikalingas Flutter SDK. Vadovaukitės instrukcijomis, kad nustatytumėte Flutter SDK.", + "get-source-code-title": "Gauti programos išeities kodą", + "get-source-code-text": "Galite gauti Flutter ThingsBoard mobiliosios programos išeities kodą, klonuodami jį iš GitHub saugyklos:", + "configure-app-settings-title": "Sukonfigūruoti programos nustatymus", + "configure-app-settings-text": "Atsisiųskite konfigūracijos failą ir įdėkite jį į projekto, kurį klonavote ankstesniame žingsnyje, šakninį katalogą.", + "download-file": "Atsisiųsti failą", + "run-app-title": "Paleisti programą", + "run-app-text": "Paleiskite programą kaip aprašyta jūsų IDE.\nJei naudojate terminalą, paleiskite programą naudodami šią komandą:", + "more-information": "Išsami informacija pateikta mūsų „Pradžios vadove“.", + "getting-started": "Pradžios vadovas" + } + }, + "notification": { + "action-button": "Veiksmo mygtukas", + "action-type": "Veiksmo tipas", + "active": "Aktyvus", + "add-notification-recipients-group": "Pridėti pranešimų gavėjų grupę", + "add-notification-template": "Pridėti pranešimo šabloną", + "add-recipient": "Pridėti gavėją", + "add-recipients": "Pridėti gavėjus", + "add-rule": "Pridėti taisyklę", + "add-stage": "Pridėti etapą", + "add-template": "Pridėti šabloną", + "after": "Po", + "alarm-assignment-trigger-settings": "Aliarmo priskyrimo trigerio nustatymai", + "alarm-comment-trigger-settings": "Aliarmo komentaro trigerio nustatymai", + "alarm-trigger-settings": "Aliarmo trigerio nustatymai", + "all": "Visi", + "api-feature-hint": "Jei laukas tuščias, trigeris bus taikomas visoms API funkcijoms", + "api-usage-trigger-settings": "API naudojimo trigerio nustatymai", + "new-platform-version-trigger-settings": "Naujos platformos versijos trigerio nustatymai", + "rate-limits-trigger-settings": "Viršytų užklausų ribų trigerio nustatymai", + "task-processing-failure-trigger-settings": "Užduočių apdorojimo klaidų trigerio nustatymai", + "resources-shortage-trigger-settings": "Išteklių trūkumo trigerio nustatymai", + "at-least-one-should-be-selected": "Reikia pasirinkti bent vieną", + "basic-settings": "Pagrindiniai nustatymai", + "button-text": "Mygtuko tekstas", + "button-text-required": "Mygtuko tekstas yra privalomas", + "button-text-max-length": "Mygtuko tekstas turi būti trumpesnis nei {{ length }} simbolių", + "compose": "Sukurti", + "conversation": "Pokalbis", + "conversation-required": "Pokalbis yra privalomas", + "copy-notification-template": "Kopijuoti pranešimo šabloną", + "copy-rule": "Kopijuoti taisyklę", + "copy-template": "Kopijuoti šabloną", + "create-new": "Sukurti naują", + "created": "Sukurta", + "customize-messages": "Tinkinti pranešimus", + "cpu-threshold": "CPU riba", + "delete-notification-text": "Būkite atsargūs – po patvirtinimo pranešimas taps neatstatomas.", + "delete-notification-title": "Ar tikrai norite ištrinti pranešimą?", + "delete-notifications-text": "Būkite atsargūs – po patvirtinimo pranešimai taps neatstatomi.", + "delete-notifications-title": "Ar tikrai norite ištrinti { count, plural, =1 {1 pranešimą} other {# pranešimus} }?", + "delete-recipient-text": "Būkite atsargūs – po patvirtinimo gavėjas taps neatstatomas.", + "delete-recipient-title": "Ar tikrai norite ištrinti gavėją '{{recipientName}}'?", + "delete-recipients-text": "Būkite atsargūs – po patvirtinimo gavėjai taps neatstatomi.", + "delete-recipients-title": "Ar tikrai norite ištrinti { count, plural, =1 {1 gavėją} other {# gavėjus} }?", + "delete-request-text": "Būkite atsargūs – po patvirtinimo užklausa taps neatstatoma.", + "delete-request-title": "Ar tikrai norite ištrinti užklausą?", + "delete-requests-text": "Būkite atsargūs – po patvirtinimo užklausos taps neatstatomos.", + "delete-requests-title": "Ar tikrai norite ištrinti { count, plural, =1 {1 užklausą} other {# užklausas} }?", + "delete-rule-text": "Būkite atsargūs – po patvirtinimo taisyklė taps neatstatoma.", + "delete-rule-title": "Ar tikrai norite ištrinti taisyklę '{{ruleName}}'?", + "delete-rules-text": "Būkite atsargūs – po patvirtinimo taisyklės taps neatstatomos.", + "delete-rules-title": "Ar tikrai norite ištrinti { count, plural, =1 {1 taisyklę} other {# taisykles} }?", + "delete-template-text": "Būkite atsargūs – po patvirtinimo šablonas taps neatstatomas.", + "delete-template-title": "Ar tikrai norite ištrinti šabloną '{{templateName}}'?", + "delete-templates-text": "Būkite atsargūs – po patvirtinimo šablonai taps neatstatomi.", + "delete-templates-title": "Ar tikrai norite ištrinti { count, plural, =1 {1 šabloną} other {# šablonus} }?", + "deleted": "Ištrinta", + "delivery-method": { + "delivery-method": "Pristatymo būdas", + "email": "El. paštas", + "email-preview": "El. pašto pranešimo peržiūra", + "slack": "Slack", + "slack-preview": "Slack pranešimo peržiūra", + "microsoft-teams": "Microsoft Teams", + "microsoft-teams-preview": "Microsoft Teams pranešimo peržiūra", + "sms": "SMS", + "sms-preview": "SMS pranešimo peržiūra", + "web": "Žiniatinklis", + "web-preview": "Žiniatinklio pranešimo peržiūra", + "mobile-app": "Mobilioji programėlė", + "mobile-app-preview": "Mobiliosios programėlės pranešimo peržiūra" + }, + "delivery-method-not-configure-click": "Pristatymo būdas nesukonfigūruotas. Spustelėkite, kad nustatytumėte.", + "delivery-method-not-configure-contact": "Pristatymo būdas nesukonfigūruotas. Susisiekite su sistemos administratoriumi.", + "delivery-methods": "Pristatymo būdai", + "description": "Aprašymas", + "device-activity-trigger-settings": "Įrenginio aktyvumo trigerio nustatymai", + "device-list-rule-hint": "Jei laukas tuščias, trigeris bus taikomas visiems įrenginiams", + "device-profiles-list-rule-hint": "Jei laukas tuščias, trigeris bus taikomas visiems įrenginių profiliams", + "disabled": "Išjungta", + "edge-trigger-settings": "Edge trigerio nustatymai", + "edge-list-rule-hint": "Jei laukas tuščias, trigeris bus taikomas visoms Edge instancijoms", + "edit-notification-recipients-group": "Redaguoti pranešimų gavėjų grupę", + "edit-notification-template": "Redaguoti pranešimo šabloną", + "edit-rule": "Redaguoti taisyklę", + "edit-template": "Redaguoti šabloną", + "enabled": "Įjungta", + "entities-limit-trigger-settings": "Objektų limito trigerio nustatymai", + "entity-action-trigger-settings": "Objekto veiksmo trigerio nustatymai", + "entity-type": "Objekto tipas", + "escalation-chain": "Eskalacijos grandinė", + "failed-send": "Siuntimo klaidos", + "fails": "{ count, plural, =1 {1 klaida} other {# klaidos} }", + "filter": "Filtras", + "first-recipient": "Pirmasis gavėjas", + "inactive": "Neaktyvus", + "inbox": "Gautieji", + "notification-inbox": "Pranešimai / Gautieji", + "input-field-support-templatization": "Įvesties laukas palaiko šablonizaciją.", + "input-fields-support-templatization": "Įvesties laukai palaiko šablonizaciją.", + "link": "Nuoroda", + "link-required": "Nuoroda yra privaloma", + "link-max-length": "Nuoroda turi būti ne ilgesnė kaip {{ length }} simbolių", + "link-type": { + "dashboard": "Atidaryti valdymo skydą", + "link": "Atidaryti URL nuorodą" + }, + "loading-notifications": "Įkeliami pranešimai...", + "management": "Pranešimų valdymas", + "mark-all-as-read": "Pažymėti visus kaip skaitytus", + "mark-as-read": "Pažymėti kaip skaitytą", + "message": "Žinutė", + "message-required": "Žinutė yra privaloma", + "message-max-length": "Žinutė turi būti ne ilgesnė kaip {{ length }} simbolių", + "name": "Pavadinimas", + "name-required": "Pavadinimas yra privalomas", + "new-notification": "Naujas pranešimas", + "no-inbox-notification": "Pranešimų nerasta", + "no-notification-request": "Pranešimo užklausų nėra", + "no-notification-templates": "Pranešimų šablonų nerasta", + "no-notifications-yet": "Kol kas pranešimų nėra", + "no-recipients-notification": "Nėra pranešimo gavėjų", + "no-recipients-matching": "Gavėjų atitinkančių '{{entity}}' nerasta.", + "no-recipients-text": "Gavėjų nerasta", + "no-rule": "Nėra sukonfigūruotos taisyklės", + "no-rules-notification": "Nėra pranešimų taisyklių", + "no-severity-found": "Prioritetų nerasta", + "no-severity-matching": "Prioritetas '{{severity}}' nerastas.", + "no-template-matching": "Resurso, atitinkančio '{{template}}', nerasta.", + "create-new-template": "Sukurti naują!", + "not-found-slack-recipient": "Slack gavėjas nerastas", + "notification": "Pranešimas", + "notification-center": "Pranešimų centras", + "notification-tap-action": "Veiksmas paspaudus pranešimą", + "notification-tap-action-hint": "Jei neįjungta, bus naudojamas numatytasis aliarmo prietaisų skydas", + "notify": "Pranešti", + "notify-again": "Pranešti dar kartą", + "notify-alarm-action": { + "acknowledged": "Aliarmas patvirtintas", + "assigned": "Aliarmas priskirtas", + "cleared": "Aliarmas išvalytas", + "created": "Aliarmas sukurtas", + "severity-changed": "Aliarmo prioritetas pakeistas", + "unassigned": "Aliarmas nepriskirtas" + }, + "notify-on": "Pranešti apie", + "notify-on-comment-update": "Pranešti apie komentaro atnaujinimą", + "notify-on-required": "„Pranešti apie“ yra privalomas", + "notify-on-unassign": "Pranešti apie nepriskyrimą", + "notify-only-user-comments": "Pranešti tik apie vartotojų komentarus", + "only-rule-chain-lifecycle-failures": "Tik taisyklių grandinės ciklo klaidos", + "only-rule-node-lifecycle-failures": "Tik taisyklių mazgų ciklo klaidos", + "platform-users": "Platformos vartotojai", + "ram-threshold": "RAM riba", + "rate-limits": "Naudojimo apribojimai", + "rate-limits-hint": "Jei laukas tuščias, trigeris bus taikomas visiems naudojimo apribojimams", + "recipient": "Gavėjas", + "recipient-group": "Gavėjų grupė", + "recipient-type": { + "affected-tenant-administrators": "Paveikti nuomininko administratoriai", + "affected-user": "Paveiktas vartotojas", + "all-users": "Visi vartotojai", + "customer-users": "Klientų vartotojai", + "system-administrators": "Sistemos administratoriai", + "tenant-administrators": "Nuomininko administratoriai", + "user-filters": "Vartotojų filtras", + "user-list": "Vartotojų sąrašas", + "users-entity-owner": "Subjekto savininko vartotojai" + }, + "recipients": "Gavėjai", + "notification-recipient": "Pranešimo gavėjas", + "notification-recipient-required": "Pranešimo gavėjas yra privalomas.", + "notification-recipients": "Pranešimai / Gavėjai", + "recipients-count": "{ count, plural, =1 {1 gavėjas} other {# gavėjai} }", + "recipients-required": "Gavėjai yra privalomi", + "refresh-allow-delivery-method": "Atnaujinti leidžiamą pristatymo metodą", + "request-search": "Užklausos paieška", + "request-status": { + "processing": "Vykdoma", + "scheduled": "Suplanuota", + "sent": "Išsiųsta" + }, + "review": "Peržiūra", + "rule": "Taisyklė", + "rule-chain-list-rule-hint": "Jei laukas tuščias, trigeris bus taikomas visoms taisyklių grandinėms", + "rule-engine-events-trigger-settings": "Taisyklių variklio įvykių trigerio nustatymai", + "rule-engine-filter": "Taisyklių variklio filtras", + "rule-name": "Taisyklės pavadinimas", + "rule-name-required": "Pavadinimas yra privalomas", + "rule-disable": "Išjungti pranešimų taisyklę", + "rule-enable": "Įjungti pranešimų taisyklę", + "rule-node-filter": "Taisyklių mazgo filtras", + "rules": "Taisyklės", + "notification-rules": "Pranešimai / Taisyklės", + "scheduler-later": "Suplanuoti vėlesniam laikui", + "search-notification": "Ieškoti pranešimų", + "search-recipients": "Ieškoti gavėjų", + "search-rules": "Ieškoti taisyklių", + "search-templates": "Ieškoti šablonų", + "see-documentation": "Peržiūrėti dokumentaciją", + "selected-notifications": "{ count, plural, =1 {1 pranešimas} other {# pranešimai} } pasirinkta", + "selected-recipients": "{ count, plural, =1 {1 gavėjas} other {# gavėjai} } pasirinkta", + "selected-requests": "{ count, plural, =1 {1 užklausa} other {# užklausos} } pasirinkta", + "selected-rules": "{ count, plural, =1 {1 taisyklė} other {# taisyklės} } pasirinkta", + "selected-template": "{ count, plural, =1 {1 šablonas} other {# šablonai} } pasirinkta", + "send-notification": "Siųsti pranešimą", + "sent": "Išsiųsta", + "setup": "Nustatymai", + "notification-sent": "Pranešimai / Išsiųsti", + "set-entity-from-notification": "Nustatyti objektą iš pranešimo į valdymo suvestinę", + "slack-chanel-type": "Slack kanalo tipas", + "slack-chanel-types": { + "direct": "Tiesioginė žinutė", + "private-channel": "Privatus kanalas", + "public-channel": "Viešas kanalas" + }, + "start-from-scratch": "Pradėti nuo pradžių", + "status": "Būsena", + "stop-escalation-alarm-status-become": "Sustabdyti eskalaciją, kai aliarmo būsena tampa:", + "storage-threshold": "Saugyklos riba", + "subject": "Tema", + "subject-required": "Tema yra privaloma", + "subject-max-length": "Temos ilgis turi būti ne didesnis nei {{ length }} simbolių", + "template": "Šablonas", + "template-name": "Šablono pavadinimas", + "template-required": "Šablonas yra privalomas", + "template-type": { + "alarm": "Aliarmas", + "alarm-assignment": "Aliarmo priskyrimas", + "alarm-comment": "Aliarmo komentaras", + "api-usage-limit": "API naudojimo limitas", + "device-activity": "Įrenginio aktyvumas", + "entities-limit": "Objektų limitas", + "entity-action": "Objekto veiksmas", + "general": "Bendras", + "rule-engine-lifecycle-event": "Taisyklių variklio gyvavimo ciklo įvykis", + "rule-node": "Taisyklių mazgas", + "new-platform-version": "Nauja platformos versija", + "rate-limits": "Viršytos normos ribos", + "edge-communication-failure": "Edge ryšio klaida", + "edge-connection": "Edge prisijungimas", + "task-processing-failure": "Užduoties apdorojimo klaida", + "resources-shortage": "Išteklių trūkumas" + }, + "templates": "Šablonai", + "notification-templates": "Pranešimai / Šablonai", + "tenant-profiles-list-rule-hint": "Jei laukas tuščias, trigeris bus taikomas visiems nuomininkų profiliams", + "tenants-list-rule-hint": "Jei laukas tuščias, trigeris bus taikomas visiems nuomininkams", + "threshold": "Riba", + "theme-color": "Temos spalva", + "time": "Laikas", + "track-rule-node-events": "Stebėti taisyklių mazgų įvykius", + "trigger": { + "alarm": "Aliarmas", + "alarm-assignment": "Aliarmo priskyrimas", + "alarm-comment": "Aliarmo komentaras", + "api-usage-limit": "API naudojimo limitas", + "device-activity": "Įrenginio aktyvumas", + "entities-limit": "Objektų limitas", + "entity-action": "Objekto veiksmas", + "rule-engine-lifecycle-event": "Taisyklių variklio gyvavimo ciklo įvykis", + "new-platform-version": "Nauja platformos versija", + "rate-limits": "Viršytos normos ribos", + "edge-connection": "Edge prisijungimas", + "edge-communication-failure": "Edge ryšio klaida", + "task-processing-failure": "Užduoties apdorojimo klaida", + "resources-shortage": "Išteklių trūkumas", + "trigger": "Trigeris", + "trigger-required": "Trigeris yra privalomas" + }, + "type": "Tipas", + "unread": "Neskaitytas", + "updated": "Atnaujintas", + "use-deprecated-webhook-connectors": "Naudoti pasenusius Webhook jungiklius", + "use-old-api": "Naudoti seną API", + "use-template": "Naudoti šabloną", + "view-all": "Peržiūrėti visus", + "warning": "Įspėjimas", + "webhook-url": "Webhook URL", + "webhook-url-required": "Webhook URL yra privalomas", + "workflow-url": "Darbo eigos URL", + "workflow-url-required": "Darbo eigos URL yra privalomas", + "channel-name": "Kanalas", + "channel-name-required": "Kanalas yra privalomas", + "settings": { + "notification-settings": "Pranešimų nustatymai", + "reset-all": "Atstatyti visus nustatymus", + "reset-all-title": "Ar tikrai norite atstatyti formą?", + "reset-all-text": "Po patvirtinimo nustatymų forma bus atstatyta į numatytąsias reikšmes ir išsaugota.", + "type": "Tipas", + "enable-all": "Įjungti visus", + "disable-all": "Išjungti visus", + "delivery-not-configured": "Pristatymo metodas nesukonfigūruotas" + } + }, + "ota-update": { + "add": "Pridėti paketą", + "assign-firmware": "Priskirta programinė aparatinė įranga", + "assign-firmware-required": "Priskirta programinė aparatinė įranga yra privaloma", + "assign-software": "Priskirta programinė įranga", + "assign-software-required": "Priskirta programinė įranga yra privaloma", + "auto-generate-checksum": "Automatiškai generuoti kontrolinę sumą", + "checksum": "Kontrolinė suma", + "checksum-hint": "Jei kontrolinė suma tuščia, ji bus sugeneruota automatiškai", + "checksum-algorithm": "Kontrolinės sumos algoritmas", + "checksum-copied-message": "Paketo kontrolinė suma nukopijuota į iškarpinę", + "change-firmware": "Programinės aparatinės įrangos pakeitimas gali sukelti { count, plural, =1 {1 įrenginio} other {# įrenginių} } atnaujinimą.", + "change-software": "Programinės įrangos pakeitimas gali sukelti { count, plural, =1 {1 įrenginio} other {# įrenginių} } atnaujinimą.", + "change-ota-setting-title": "Ar tikrai norite pakeisti OTA nustatymus?", + "chose-compatible-device-profile": "Įkeltas paketas bus pasiekiamas tik įrenginiams su pasirinktu profiliu.", + "chose-firmware-distributed-device": "Pasirinkite programinę aparatinę įrangą, kuri bus paskirstyta įrenginiams", + "chose-software-distributed-device": "Pasirinkite programinę įrangą, kuri bus paskirstyta įrenginiams", + "content-type": "Turinio tipas", + "copy-checksum": "Kopijuoti kontrolinę sumą", + "copy-direct-url": "Kopijuoti tiesioginį URL", + "copyId": "Kopijuoti paketo ID", + "copied": "Nukopijuota!", + "delete": "Ištrinti paketą", + "delete-ota-update-text": "Būkite atsargūs – po patvirtinimo OTA atnaujinimas bus negrįžtamai pašalintas.", + "delete-ota-update-title": "Ar tikrai norite ištrinti OTA atnaujinimą '{{title}}'?", + "delete-ota-updates-text": "Būkite atsargūs – po patvirtinimo visi pasirinkti OTA atnaujinimai bus pašalinti.", + "delete-ota-updates-title": "Ar tikrai norite ištrinti { count, plural, =1 {1 OTA atnaujinimą} other {# OTA atnaujinimus} }?", + "description": "Aprašymas", + "direct-url": "Tiesioginis URL", + "direct-url-copied-message": "Paketo tiesioginis URL nukopijuotas į iškarpinę", + "direct-url-required": "Tiesioginis URL yra privalomas", + "download": "Atsisiųsti paketą", + "drop-file": "Įmeskite paketo failą arba spustelėkite norėdami įkelti.", + "drop-package-file-or": "Nutempkite paketo failą arba", + "file-name": "Failo pavadinimas", + "file-size": "Failo dydis", + "file-size-bytes": "Failo dydis baitais", + "idCopiedMessage": "Paketo ID nukopijuotas į iškarpinę", + "no-firmware-matching": "Nerasta jokių suderinamų programinės aparatinės įrangos OTA atnaujinimo paketų, atitinkančių '{{entity}}'.", + "no-firmware-text": "Nėra suderinamų programinės aparatinės įrangos OTA atnaujinimo paketų.", + "no-packages-text": "Paketų nerasta", + "no-software-matching": "Nerasta jokių suderinamų programinės įrangos OTA atnaujinimo paketų, atitinkančių '{{entity}}'.", + "no-software-text": "Nėra suderinamų programinės įrangos OTA atnaujinimo paketų.", + "ota-update": "OTA atnaujinimas", + "ota-update-details": "OTA atnaujinimo informacija", + "ota-updates": "OTA atnaujinimai", + "package-file": "Paketo failas", + "package-type": "Paketo tipas", + "packages-repository": "Paketų saugykla", + "search": "Ieškoti paketų", + "selected-package": "{ count, plural, =1 {1 paketas} other {# paketai} } pasirinkta", + "title": "Pavadinimas", + "title-required": "Pavadinimas yra privalomas.", + "title-max-length": "Pavadinimas turi būti trumpesnis nei 256 simboliai", + "types": { + "firmware": "Programinė aparatinė įranga", + "software": "Programinė įranga" + }, + "upload-binary-file": "Įkelti dvejetainį failą", + "use-external-url": "Naudoti išorinį URL", + "version": "Versija", + "version-required": "Versija yra privaloma.", + "version-tag": "Versijos žyma", + "version-tag-hint": "Pasirinktinė žyma turi atitikti paketo versiją, kurią praneša jūsų įrenginys.", + "version-max-length": "Versija turi būti trumpesnė nei 256 simboliai", + "warning-after-save-no-edit": "Įkėlus paketą, pavadinimo, versijos, įrenginio profilio ir paketo tipo keisti nebegalėsite." + }, + "position": { + "top": "Viršus", + "bottom": "Apačia", + "left": "Kairioji pusė", + "right": "Dešinioji pusė" + }, + "profile": { + "profile": "Profilis", + "last-login-time": "Paskutinio prisijungimo laikas", + "change-password": "Pakeisti slaptažodį", + "current-password": "Dabartinis slaptažodis", + "copy-jwt-token": "Kopijuoti JWT raktą", + "jwt-token": "JWT raktas", + "token-valid-till": "Raktas galioja iki", + "tokenCopiedSuccessMessage": "JWT raktas nukopijuotas į iškarpinę", + "tokenCopiedWarnMessage": "JWT raktas nebegalioja! Atnaujinkite puslapį." + }, + "profiles": { + "profiles": "Profiliai" + }, + "security": { + "security": "Saugumas", + "general-settings": "Bendrieji saugumo nustatymai", + "access-token": "Prieigos raktas", + "access-token-required": "Prieigos raktas yra privalomas", + "clientId": "Kliento ID", + "clientId-required": "Kliento ID yra privalomas", + "username": "Vartotojo vardas", + "username-required": "Vartotojo vardas yra privalomas", + "ca-cert": "CA sertifikatas", + "2fa": { + "2fa": "Dviejų veiksnių autentifikavimas", + "2fa-description": "Dviejų veiksnių autentifikavimas apsaugo jūsų paskyrą nuo neteisėtos prieigos. Prisijungdami turėsite įvesti saugos kodą.", + "authenticate-with": "Galite autentifikuotis naudodami:", + "disable-2fa-provider-text": "Išjungus {{name}}, jūsų paskyra taps mažiau saugi.", + "disable-2fa-provider-title": "Ar tikrai norite išjungti {{name}}?", + "get-new-code": "Gauti naują kodą", + "main-2fa-method": "Naudoti kaip pagrindinį dviejų veiksnių autentifikavimo būdą", + "dialog": { + "activation-step-description-email": "Kitą kartą prisijungiant bus prašoma įvesti saugos kodą, kuris bus išsiųstas jūsų el. pašto adresu.", + "activation-step-description-sms": "Kitą kartą prisijungiant bus prašoma įvesti saugos kodą, kuris bus išsiųstas jūsų telefono numeriu.", + "activation-step-description-totp": "Kitą kartą prisijungiant turėsite pateikti dviejų veiksnių autentifikavimo kodą.", + "activation-step-label": "Aktyvacija", + "backup-code-description": "Atsispausdinkite atsarginius kodus, kad turėtumėte juos po ranka, kai reikės prisijungti prie paskyros. Kiekvienas atsarginis kodas gali būti naudojamas tik vieną kartą.", + "backup-code-warn": "Palikus šį puslapį, šie kodai nebebus rodomi. Išsaugokite juos saugiai.", + "download-txt": "Atsisiųsti (txt)", + "email-step-description": "Įveskite el. pašto adresą, kuris bus naudojamas kaip autentifikatorius.", + "email-step-label": "El. paštas", + "enable-email-title": "Įjungti el. pašto autentifikatorių", + "enable-sms-title": "Įjungti SMS autentifikatorių", + "enable-totp-title": "Įjungti autentifikavimo programėlę", + "enter-verification-code": "Įveskite 6 skaitmenų kodą", + "get-backup-code-title": "Gauti atsarginį kodą", + "next": "Toliau", + "scan-qr-code": "Nuskaitykite šį QR kodą naudodami autentifikavimo programėlę", + "send-code": "Siųsti kodą", + "sms-step-description": "Įveskite telefono numerį, kuris bus naudojamas kaip autentifikatorius.", + "sms-step-label": "Telefono numeris", + "success": "Sėkmingai!", + "totp-step-description-install": "Galite įdiegti tokias programėles kaip Google Authenticator, Authy arba Duo.", + "totp-step-description-open": "Atidarykite autentifikavimo programėlę savo telefone.", + "totp-step-label": "Gauti programėlę", + "verification-code": "6 skaitmenų kodas", + "verification-code-invalid": "Neteisingas kodo formatas", + "verification-code-incorrect": "Neteisingas patvirtinimo kodas", + "verification-code-many-request": "Per daug bandymų patikrinti kodą", + "verification-step-description": "Įveskite 6 skaitmenų kodą, kurį ką tik išsiuntėme adresu {{address}}", + "verification-step-label": "Patvirtinimas" + }, + "provider": { "email": "El. paštas", - "no-address": "Adresas nenurodytas", - "state-max-length": "Rajono pavadinimas negali viršyti 256 simbolių", - "phone-max-length": "Telefono numeris negali viršyti 256 simbolių", - "city-max-length": "Miesto pavadinimas negali viršyti 256 simbolių" - }, - "common": { - "username": "Vartoto vardas", - "password": "Slaptažodis", - "enter-username": "Įveskite vartotojo vardą", - "enter-password": "Įveskite slaptažodį", - "enter-search": "Įveskite paieškos kriterijus", - "created-time": "Sukūrimo laikas", - "loading": "Įkeliama...", - "proceed": "Tęsti", - "open-details-page": "Informacija", - "not-found": "Nerasta", - "documentation": "Dokumentacija" - }, - "converter": { - "converter": "Data converter", - "converters": "Data converters", - "select-converter": "Select data converter", - "no-converters-matching": "No data converters matching '{{entity}}' were found.", - "no-converters-found": "No data converters found.", - "converter-required": "Data converter is required", - "delete": "Delete converter", - "management": "Data converters management", - "add-converter-text": "Add new data converter", - "no-converters-text": "No data converters found", - "selected-converters": "{ count, plural, =1 {1 data converter} other {# data converters} } selected", - "delete-converter-title": "Are you sure you want to delete the data converter '{{converterName}}'?", - "delete-converter-text": "Be careful, after the confirmation the data converter and all related data will become unrecoverable.", - "delete-converters-title": "Are you sure you want to delete { count, plural, =1 {1 data converter} other {# data converters} }?", - "delete-converters-action-title": "Delete { count, plural, =1 {1 data converter} other {# data converters} }", - "delete-converters-text": "Be careful, after the confirmation all selected data converters will be removed and all related data will become unrecoverable.", - "events": "Events", - "add": "Add Data Converter", - "search": "Search data converters", - "converter-details": "Data converter details", - "details": "Details", - "copyId": "Copy converter Id", - "idCopiedMessage": "Converter Id has been copied to clipboard", - "debug-mode": "Debug mode", - "created-time": "Created time", - "name": "Name", - "name-required": "Name is required.", - "name-max-length": "Name should be less than 256", - "description": "Description", - "decoder": "Decoder", - "encoder": "Encoder", - "test-decoder-fuction": "Test decoder function", - "test-encoder-fuction": "Test encoder function", - "test-with-this-message": "{{test}} with this message", - "decoder-input-params": "Decoder input parameters", - "encoder-input-params": "Encoder input parameters", - "payload": "Payload", - "payload-content-type": "Payload content type", - "payload-content": "Payload content", - "message": "Message", - "message-type": "Message type", - "message-type-required": "Message type is required", - "test": "Test", - "metadata": "Metadata", - "metadata-required": "Metadata entries can't be empty.", - "integration-metadata": "Integration metadata", - "integration-metadata-required": "Integration metadata entries can't be empty.", - "output": "Output", - "import": "Import converter", - "export": "Export converter", - "export-failed-error": "Unable to export converter: {{error}}", - "create-new-converter": "Create new converter", - "converter-file": "Converter file", - "invalid-converter-file-error": "Unable to import converter: Invalid converter data structure.", - "type": "Type", - "type-required": "Type is required.", - "type-uplink": "Uplink", - "type-downlink": "Downlink", - "update-only-keys-list": "Update only keys list", - "update-only-keys-list-hint": "The values associated with the provided keys will be saved to the database only if they are different from the corresponding values in the previous converted message. This functionality applies to both attributes and telemetry in the converter output.", - "add-key": "Add key" - }, - "content-type": { - "json": "Json", - "text": "Text", - "binary": "Binary (Base64)" + "email-description": "Naudokite saugos kodą, išsiųstą į jūsų el. pašto adresą, kad autentifikuotumėtės.", + "email-hint": "Autentifikavimo kodai siunčiami el. paštu adresu {{ info }}", + "sms": "SMS", + "sms-description": "Naudokite savo telefoną autentifikacijai. Prisijungiant išsiųsime jums saugos kodą SMS žinute.", + "sms-hint": "Autentifikavimo kodai siunčiami SMS žinute numeriu {{ info }}", + "totp": "Autentifikavimo programėlė", + "totp-description": "Naudokite programėles, tokias kaip Google Authenticator, Authy ar Duo, savo telefone autentifikacijai. Jos generuos prisijungimo kodą.", + "totp-hint": "Autentifikavimo programėlė sukonfigūruota jūsų paskyrai", + "backup_code": "Atsarginis kodas", + "backup-code-description": "Šie vienkartiniai spausdinami kodai leidžia prisijungti, kai neturite prieigos prie telefono, pavyzdžiui, keliaujant.", + "backup-code-hint": "Šiuo metu aktyvūs {{ info }} vienkartinio naudojimo kodai" + } }, - "color": { - "color": "Color", - "color-picker": "Color picker", - "primary-colors": "Primary colors", - "accent-colors": "Accent colors", - "no-color-selected": "No color selected" - }, - "customer": { - "all": "Visi", - "all-customers": "Visi klientai", - "groups": "Grupės", - "shared": "Bendrinami", - "hierarchy": "Hierarchija", - "customer": "Klientas", - "customers": "Klientai", - "management": "Klientų valdymas", - "dashboard": "Klientų skydelis", - "dashboards": "Kliientų skydeliai", - "devices": "Kliento įrenginiai", - "entity-views": "Kliento subjektų rodiniai", - "assets": "Kliento turtas", - "public-dashboards": "Vieši skydeliai", - "public-devices": "Vieši įrenginiai", - "public-assets": "Viešas turtas", - "public-entity-views": "Vieši subjektų rodiniai", - "add": "Pridėti klientą", - "delete": "Pašalinti klientą", - "manage-customer-user-groups": "Kliento vartotojų grupės", - "manage-customer-groups": "Klientų grupės", - "manage-customer-device-groups": "Kliento įrenginių grupės", - "manage-customer-asset-groups": "Kliento turto grupės", - "manage-customer-entity-view-groups": "Kliento subjektų rodinių grupės", - "manage-customer-edge-groups": "Manage customer edge groups", - "manage-customer-dashboard-groups": "Kliento skydelių grupės", - "manage-customer-users": "Kliento vartotojai", - "manage-customers": "Klientai", - "manage-customer-devices": "Kliento įrenginiai", - "manage-customer-entity-views": "Kliento subjektų rodiniai", - "manage-customer-dashboards": "Kliento skydeliai", - "manage-public-devices": "Vieši įrenginiai", - "manage-public-dashboards": "Vieši skydeliai", - "manage-customer-assets": "Kliento turtas", - "manage-customer-edges": "Manage customer edges", - "manage-public-assets": "Viešas turtas", - "add-customer-text": "Pridėti naują klientą", - "no-customers-text": "Klientų nėra", - "customer-details": "Informacija apie klientą", - "delete-customer-title": "Ar tikrai norite pašalinti klientą '{{customerTitle}}'?", - "delete-customer-text": "Būkite dėmesingi, po patvirtinimo, kliento ir su juo susijusios informacijos atkurti nebegalėsite.", - "delete-customers-title": "Ar tikrai norite pašalinti { count, plural, =1 {1 klientą} other {# klientus} }?", - "delete-customers-action-title": "Pašalinti { count, plural, =1 {1 klientą} other {# klientus} }", - "delete-customers-text": "Būkite dėmesingi, po patvirtinimo, klientų ir su jais susijusios informacijos atkurti nebegalėsite.", - "manage-user-groups": "Vartotjų grupės", - "manage-asset-groups": "turto grupės", - "manage-device-groups": "Įrenginių grupės", - "manage-dashboard-groups": "Skydelių grupės", - "manage-entity-view-groups": "Subjektų rodinių grupės", - "manage-edge-groups": "Manage edge groups", - "manage-users": "Vartotojai", - "manage-assets": "Turtas", - "manage-devices": "Įrenginiai", - "manage-dashboards": "Skydeliai", - "manage-entity-views": "Subjektų rodiniai", - "title": "Pavadinimas", - "title-required": "Pavadinimas būtinas.", - "title-max-length": "Pavadinimas negali viršyti 256 simbolių", - "description": "Aprašymas", - "details": "Informacija", - "events": "Įvykiai", - "copyId": "Kopijuoti kliento Id", - "idCopiedMessage": "Kliento Id nukopijuotas į iškarpinę", - "select-customer": "Pasirinkti klientą", - "no-customers-matching": "Klientų, atitinkančių '{{entity}}' nėra.", - "customer-required": "Klientas būtinas", - "select-group-to-add": "Pasirinkite grupę, į kurią pridėti pažymėtus klientus", - "select-group-to-move": "Pasirinkite grupę, į kurią perkelti pažymėtus klientus", - "remove-customers-from-group": "Ar tikrai { count, plural, =1 {1 klientą} other {# klientus} } norite pašalinti iš grupės '{{entityGroup}}'?", - "group": "Klientų grupė", - "list-of-groups": "{ count, plural, =1 {Viena kliento grupė} other {Sąrašas # klientų grupių} }", - "group-name-starts-with": "Klientų grupės, kurios prasideda '{{prefix}}'", - "select-default-customer": "Pasirinkite numatytąjį klientą", - "default-customer": "Numatytasis klientas", - "default-customer-required": "Numatytasis klientas būtinas, norint suderinti prietaisų skydelį valdytojo lygiu", - "allow-white-labeling": "Leisti tinkinti aplikaciją", - "search": "Klientų paieška", - "selected-customers": "Pasirinkta { count, plural, =1 {1 klientas} other {# kientai} }", - "edges": "Customer edge instances", - "manage-edges": "Manage edges" - }, - "customers-hierarchy": { - "customers-hierarchy": "Klientų hierarchija", - "open-nav-tree": "Atverti navigacijos medį", - "return-to-top-level": "Grįžti į aukščiausią lygį" - }, - "custom-menu": { - "custom-menu": "Vartotojo meniu", - "custom-menu-hint": "Aprašykite pasirinktinį meniu JSON formoje. Šiame JSON nurodomas vartotojo meniu elementų sąrašas." - }, - "custom-translation": { - "custom-translation": "Vartotojo vertimai", - "translation-map": "Vertimų žemėlapis", - "key": "Vertimo raktas", - "import": "Importuoti vertimą", - "export": "Eksportuoti vertimą", - "export-data": "Eksportuoti vertimo duomenis", - "import-data": "Importuoti vertimo duomenis", - "translation-file": "Vertimų failas", - "invalid-translation-file-error": "Nepavyksta importuoti vertimo failo: Neteisinga duomenų struktūra.", - "custom-translation-hint": "Aprašykite vartotojo vertimą JSON formatu žemiau. Šis JSON perrašys standartinį vertimą. Paspaudę 'Parsisiųsti lokalės failą', gausite standartinio vertimo failą, kuriame galite surasti reikalingas raktų-reikšmių poros ir jas pakeisti savo JSON.", - "download-locale-file": "Parsisųsti lokalės failą" - }, - "date": { - "last-update-n-ago": "Last update N ago", - "last-update-n-ago-text": "Last update {{ agoText }}", - "custom-date": "Custom date", - "format": "Format", - "preview": "Preview" - }, - "datetime": { - "date-from": "Data nuo", - "time-from": "Laikas nuo", - "date-to": "Data iki", - "time-to": "Laikas iki" - }, - "dashboard": { - "all": "Visi", - "all-dashboards": "Visi skydeliai", - "groups": "Grupės", - "shared": "Bendrinami", - "dashboard": "Skydelis", - "dashboards": "Skydeliai", - "management": "Skydelių valdymas", - "view-dashboards": "Paržiūrėti skydelius", - "add": "Pridėti skydelį", - "assign-dashboard-to-customer": "Skydelį (-ius) priskirti klientui", - "assign-dashboard-to-customer-text": "Pasirinkite skydelius, kuriuos norite priskirti klientui", - "assign-to-customer-text": "Pasirinkite klientą, kuriam priskirti skydelį (-ius)", - "assign-to-customer": "Priskirti kientui", - "unassign-from-customer": "Atsieti nuo kliento", - "make-public": "Skydelį padaryti viešą", - "make-private": "Skydelį padaryti privatų", - "manage-assigned-customers": "Valdyti priskirtus klientus", - "assigned-customers": "Priskirti klientai", - "assign-to-customers": "Skydelį (-ius) priskirti klientams", - "assign-to-customers-text": "Pasirinkite klientus, kuriems priskirti skydelį (-ius)", - "unassign-from-customers": "Skydelį (-ius) atsieti nuo klientų", - "unassign-from-customers-text": "Pasirinkite klientus, kuriems atsieti skydelį (-ius)", - "no-dashboards-text": "Skydelių nėra", - "no-widgets": "Valdikliai nesukonfigūruoti", - "add-widget": "Pridėti naują valdiklį", - "add-widget-button-text": "Pridėt valdiklį", - "title": "Pavadinimas", - "image": "Skydelio paveiksliukas", - "mobile-app-settings": "Mobiliosios aplikacijos nustatymai", - "mobile-order": "Skydelio eilės tvarka mobiliojoje aplikacijoje", - "mobile-hide": "Mobiliojoje aplikacijoje skydelį paslėpti", - "update-image": "Atnaujinti skydelio paveiksliuką", - "take-screenshot": "Padaryti ekrano nuotrauką", - "select-widget-title": "Pasirinkite valdiklį", - "select-widget-value": "{{title}}: pasirinkite valdiklį", - "select-widget-subtitle": "Galimų valdiklių sąrašas", - "delete": "Pašalinti skydelį", - "title-required": "Pavadinimas būtinas.", - "title-max-length": "Pavadinimas negali viršyti 256 simbolių", - "description": "Aprašymas", - "details": "Informacija", - "dashboard-details": "Informacija", - "add-dashboard-text": "Pridėti naują skydelį", - "assign-dashboards": "Priskirti skydelius", - "assign-new-dashboard": "Priskirti naują skydelį", - "assign-dashboards-text": "Priskirti { count, plural, =1 {1 skydelį} other {# skydelius} } klientams", - "unassign-dashboards-action-text": "Atsieti { count, plural, =1 {1 skydelį} other {# skydelius} } nuo klientų", - "delete-dashboards": "Panaikinti skydelius", - "unassign-dashboards": "Atsieti skydelius", - "unassign-dashboards-action-title": "Atsieti { count, plural, =1 {1 skydelį} other {# skydelius} } nuo kliento", - "delete-dashboard-title": "Ar tikrai norite pašalinti skydelį '{{dashboardTitle}}'?", - "delete-dashboard-text": "Būkite dėmesingi, po patvirtinimo, skydelio ir su juo susijusios informacijos atkurti nebegalėsite.", - "delete-dashboards-title": "Ar tikrai norite pašalinti { count, plural, =1 {1 skydelį} other {# skydelius} }?", - "delete-dashboards-action-title": "Pašalinti { count, plural, =1 {1 skydelį} other {# skydelius} }", - "delete-dashboards-text": "Būkite dėmesingi, po patvirtinimo, pasirinkti skydeliai ir su jais susijusi informacija bus pašalinti ir jų atkurti nebegalėsite.", - "unassign-dashboard-title": "Ar tikrai norite atsieti skydelį '{{dashboardTitle}}'?", - "unassign-dashboard-text": "Po patvrtinimo skydelis bus atsietas ir klientas jo nebematys.", - "unassign-dashboard": "Atsieti skydelį", - "unassign-dashboards-title": "Ar tikrai norite atsieti { count, plural, =1 {1 skydelį} other {# skydelius} }?", - "unassign-dashboards-text": "Po patvirtinimo visi skydeliai bus atsieti ir klientas jų nebematys.", - "public-dashboard-title": "Skydelis dabar yra viešas", - "public-dashboard-text": "Skydelis {{dashboardTitle}} dabar yra viešas ir pasiekiamas per šią nuorodą:", - "public-dashboard-notice": "Pastaba: Įrenginiai turi būti vieši, jei norite matyti jų duomenis.", - "public-dashboard-link": "Vieša skydelio nuoroda", - "public-dashboard-link-text": "Viešą sydelį {{dashboardTitle}} galima atidaryti per šią nuorodą:", - "public-dashboard-link-notice": "Pastaba: Įrenginiai, turtas ir subjektai turi būti vieši, jei norite matyti jų duomenis.", - "make-private-dashboard-title": "Ar tikrai norite skydelį '{{dashboardTitle}}' padaryti privačiu?", - "make-private-dashboard-text": "Po patvirtinimo, skydelis taps privačiu ir jo nematys kiti vartotojai.", - "make-private-dashboard": "Skydelį padaryti privačiu", - "socialshare-text": "'{{dashboardTitle}}' powered by ThingsBoard", - "socialshare-title": "'{{dashboardTitle}}' powered by ThingsBoard", - "select-dashboard": "Pasirinkite skydelį", - "no-dashboards-matching": "Skydelio, atitinkančio '{{entity}}' nėra.", - "dashboard-required": "Skydelis būtinas.", - "select-existing": "Pasirinkite egzistuojantį skydelį", - "create-new": "Sukurti naują skydelį", - "new-dashboard-title": "Naujas skydelio pavadinimas", - "open-dashboard": "Atidaryti skydelį", - "set-background": "Nustatyti foną", - "background-color": "Fono spalva", - "background-image": "Fono paveikslėlis", - "background-size-mode": "Fono dydis", - "no-image": "Paveikslėlis nepasirinktas", - "empty-image": "Nėra paveikslėlio", - "drop-image": "Užvilkite paveikslėlį arba spustelėkite ir pasirinkite failą.", - "maximum-upload-file-size": "Maksimalus įkeliamo failo dydis: {{ size }}", - "cannot-upload-file": "Failo įkelti nepavyko", - "settings": "Nustatymai", - "columns-count": "Stulpelių skaičius", - "columns-count-required": "Stulpelių skaičius būtinas.", - "min-columns-count-message": "Minimalus stulpelių skaičius - 10.", - "max-columns-count-message": "Maksimalus stulpelių skaičius - 1000.", - "widgets-margins": "Paraštės tarp valdiklių dysis", - "margin-required": "Paraštės dydžio reikšmė būtina.", - "min-margin-message": "Minimali paraštės dydžio reikšmė - 0.", - "max-margin-message": "Maksimali paraštės dydžio reikšmė - 50.", - "horizontal-margin": "Horizontalios paraštės dydis", - "horizontal-margin-required": "Horizontalios paraštės dydžio reikšmė būtina.", - "min-horizontal-margin-message": "Minimali horizontalios paraštės dydžio reikšmė - 0", - "max-horizontal-margin-message": "Maksimali horizontalios paraštės dydžio reikšmė - 50.", - "vertical-margin": "Vertikalios paraštės dydis", - "vertical-margin-required": "Vertikalios paraštės dydžio reikšmė būtina.", - "min-vertical-margin-message": "Minimali vertikalios paraštės dydžio reikšmė - 0.", - "max-vertical-margin-message": "Maksimali vertikalios paraštės dydžio reikšmė - 50.", - "apply-outer-margin": "Taikyti paraštes maketo šonuose", - "autofill-height": "Automatiškai pritaikyti aukštį", - "mobile-layout": "Mobiliojo režimo išdėstymo nustatymai", - "mobile-row-height": "Mobiliojo režimo eilutės aukštis pikseliais", - "mobile-row-height-required": "Mobiliojo režimo eilutės aukštis būtinas.", - "min-mobile-row-height-message": "Minimalus mobiliojo režimo eilutės aukštis yra 5 pikseliai.", - "max-mobile-row-height-message": "Maksimalus mobiliojo režimo eilutės aukštis yra 200 pikselių.", - "title-settings": "Pavadinimo nustatymai", - "display-title": "Rodyt skydelio pavadinimą", - "title-color": "Pavadinimo spalva", - "toolbar-settings": "Įrankių juostos nustatymai", - "hide-toolbar": "Paslėpti įrankių juostą", - "toolbar-always-open": "Įrankių juostą laikyti atdarytą", - "display-dashboards-selection": "Rodyti skydelių pasirinkimą", - "display-entities-selection": "Rodyti subjektų pasirinkimą", - "display-filters": "Rodyti filtrus", - "display-dashboard-timewindow": "Rodyti laiko langą", - "display-dashboard-export": "Rodyti eksportą", - "display-update-dashboard-image": "Display update dashboard image", - "dashboard-logo-settings": "Dashboard logo settings", - "display-dashboard-logo": "Display logo in dashboard fullscreen mode", - "dashboard-logo-image": "Dashboard logo image", - "advanced-settings": "Advanced settings", - "dashboard-css": "Dashboard CSS", - "import": "Importuoti skydelį", - "export": "Eksportuoti skydelį", - "export-failed-error": "Skydelio importuoti nepavyko: {{error}}", - "export-pdf": "Eksportuoti kaip PDF", - "export-png": "Eksportuoti kaip PNG", - "export-jpg": "Eksportuoti kaip JPEG", - "export-json-config": "Eksportuoti JSON konfigūraciją", - "download-dashboard-progress": "Generuojamas skydelis {{reportType}} ...", - "create-new-dashboard": "Sukurti naują skydelį", - "dashboard-file": "Skydelio failas", - "invalid-dashboard-file-error": "Skydelio importuoti nepavyko: neteisinga duomenų struktūra.", - "dashboard-import-missing-aliases-title": "Konfigūruoti importuoto skydelio naudojamus pseudonimus", - "create-new-widget": "Sukurti naują valdiklį", - "import-widget": "importuoti valdiklį", - "widget-file": "valdiklio failas", - "invalid-widget-file-error": "Valdiklio importuoti nepavyko: neteisinga duomenų struktūra.", - "widget-import-missing-aliases-title": "Konfigūruoti importuoto valdiklio naudojamus pseudonimus", - "open-toolbar": "Atidaryti skydelio įrankių juostą", - "close-toolbar": "Uždaryti įrankių juostą", - "configuration-error": "Konfigūracijos klaida", - "alias-resolution-error-title": "Skydelio pseudonimų konfigūracijos klaida", - "invalid-aliases-config": "Įrenginių, atitinkančių pseudonimų filtrus, nėra.
    Susisiekite su sistemos administratoriumi.", - "select-devices": "Pasirinkite įrenginius", - "assignedToCustomer": "Priskirta klientui", - "assignedToCustomers": "Priskirta klientams", - "public": "Viešas", - "copyId": "Kopijuoti skydelio Id", - "idCopiedMessage": "Skydelio Id nukopijuotas į iškarpinę", - "public-link": "Vieša nuoroda", - "copy-public-link": "Kopijuoti viešą nuorodą", - "public-link-copied-message": "Skydelio vieša nuoroda nukopijuota į iškarpinę", - "manage-states": "Valdyti skydelio būsenas", - "states": "Skydelio būsenos", - "states-short": "Būsenos", - "search-states": "Skydelio būsenų paieška", - "selected-states": "Pasirinkta skydelio { count, plural, =1 {1 būsena} other {# būsenos} }", - "edit-state": "Redaguoti skydelio būseną", - "delete-state": "Panaikinti skydelio būseną", - "add-state": "Pridėti skydelio būseną", - "no-states-text": "Būsenų nėra", - "state": "Skydelio būsena", - "state-name": "Pavadinimas", - "state-name-required": "Skydelio pavadinimas būtinas.", - "state-id": "Būsenos Id", - "state-id-required": "Būsenos Id būtinas.", - "state-id-exists": "Skydelio būsena su tokiu pačiu Id jau yra.", - "is-root-state": "Pagrindinė būsena", - "delete-state-title": "Panaikinti skydelio būseną", - "delete-state-text": "Ar tikrai norite panaikinti skydelio būseną '{{stateName}}'?", - "show-details": "Rodyti detales", - "hide-details": "Slėpti detales", - "select-state": "Pasirinkite būseną", - "state-controller": "Būsenų valdiklis", - "search": "Skydelių paieška", - "selected-dashboards": "Pasirinkta { count, plural, =1 {1 skydelis} other {# skydeliai} }", - "home-dashboard": "Pagrindinis skydelis", - "home-dashboard-hide-toolbar": "Slėpti pagrindinio skydelio įrankių juostą", - "select-group-to-add": "Pasirinkite grupę, prie kurios pridėti pažymėtus skydelius", - "select-group-to-move": "Pasirinkite grupę, į kurią perkelti pasirinktus skydelius", - "remove-dashboards-from-group": "Ar tikrai pašalinti { count, plural, =1 {1 skydelį} other {# skydelius} } iš grupės '{{entityGroup}}'?", - "group": "Skydelių grupė", - "list-of-groups": "{ count, plural, =1 {Viena skydelių grupė} other {Sąrašas # skydelių grupių} }", - "group-name-starts-with": "Skydelių grupės, kurių pavadinimai prasideda '{{prefix}}'", - "unassign-dashboard-from-edge-text": "After the confirmation the dashboard will be unassigned and won't be accessible by the edge.", - "unassign-dashboards-from-edge-title": "Are you sure you want to unassign { count, plural, =1 {1 dashboard} other {# dashboards} }?", - "unassign-dashboards-from-edge-text": "After the confirmation all selected dashboards will be unassigned and won't be accessible by the edge.", - "assign-dashboard-to-edge": "Assign Dashboard(s) To Edge", - "assign-dashboard-to-edge-text": "Please select the dashboards to assign to the edge", - "non-existent-dashboard-state-error": "Dashboard state with id \"{{ stateId }}\" is not found", - "edit-mode": "Edit mode" - }, - "datakey": { - "settings": "Nustatymai", - "general": "Bendra", - "advanced": "Patyrusiam vartotojui", - "key": "Raktas", - "label": "Etiketė", - "color": "Spalva", - "units": "Specialieji simboliai, rodomi šalia reikšmės", - "decimals": "Skaičių kiekis po kablelio", - "data-generation-func": "Duomenų generavimo funkcija", - "use-data-post-processing-func": "Papildomo duomenų apdorojimo funkcija", - "configuration": "Duomenų raktų konfigūracija", - "timeseries": "Telemetrija", - "attributes": "Atributai", - "entity-field": "Subjekto laukas", - "alarm": "Įspėjimo laukai", - "timeseries-required": "Subjekto telemetrija būtina.", - "timeseries-or-attributes-required": "Subjekto telemetrija/atributai būtini.", - "alarm-fields-timeseries-or-attributes-required": "Įspėjimo laukai ar subjekto telemetrijos/atributai būtini.", - "maximum-timeseries-or-attributes": "Leidžiama daugiausiai { count, plural, =1 {1 viena telemetrija/atributas.} other {# telemetrijos/atributai} }", - "alarm-fields-required": "Įspėjimo laukai būtini.", - "function-types": "Funkcijų tipai", - "function-type": "Funkcijos tipas", - "function-types-required": "Funkcijos tipai būtini.", - "data-keys": "Duomenų raktai", - "data-key": "Duomenų raktas", - "data-keys-required": "Duomenų raktai būtini.", - "data-key-required": "Duomenų raktas būtinas.", - "alarm-keys": "Įspėjimo duomenų raktai", - "alarm-key": "Įspėjimo duomenų raktas", - "alarm-key-functions": "Alarm key functions", - "alarm-key-function": "Alarm key function", - "latest-keys": "Latest data keys", - "latest-key": "Latest data key", - "latest-key-functions": "Latest key functions", - "latest-key-function": "Latest key function", - "timeseries-keys": "Timeseries data keys", - "timeseries-key": "Timeseries data key", - "timeseries-key-functions": "Timeseries key functions", - "timeseries-key-function": "Timeseries key function", - "maximum-function-types": "Daugiausiai leidžiama { count, plural, =1 {1 funkcijos tipas.} other {# funkcijos tipai} }", - "time-description": "Dabartinės vertės laiko žyma;", - "value-description": "Dabartinė vertė;", - "prev-value-description": "Ankstesnio funkcijos kvietimo rezultatas;", - "time-prev-description": "Ankstesnės vertės laiko žyma;", - "prev-orig-value-description": "Pradinė ankstesnė vertė;", - "aggregation": "Agregavimas", - "aggregation-type-hint-common": "For performance reasons, the aggregated values calculation is available only for fixed time intervals like \"current day\", \"current month\", etc, and is not available for sliding window intervals like 'last 30 minutes' or 'last 24 hours'.", - "aggregation-type-none-hint": "Take latest value.", - "aggregation-type-min-hint": "Find minimum value among data points within a selected time window.", - "aggregation-type-max-hint": "Find maximum value among data points within a selected time window.", - "aggregation-type-avg-hint": "Calculate an average value among data points within a selected time window.", - "aggregation-type-sum-hint": "Sum all values of the data points within a selected time window.", - "aggregation-type-count-hint": "Total number of the data points within a selected time window.", - "delta-calculation": "Delta calculation", - "enable-delta-calculation": "Enable delta calculation", - "enable-delta-calculation-hint": "When enabled, the data key value is calculated based on the aggregated values for a selected time window and a specified comparison period. For performance reasons, the delta calculation is available only for history time windows and not for real-time values. For example, you may calculate the delta between the energy consumption for yesterday compared to the energy consumption for the day before yesterday.", - "delta-calculation-result": "Delta calculation result", - "delta-calculation-result-previous-value": "Previous value", - "delta-calculation-result-delta-absolute": "Delta (absolute)", - "delta-calculation-result-delta-percent": "Delta (percent)", - "source": "Source", - "latest": "Latest", - "latest-value": "Latest value", - "delta": "delta", - "percent": "percent", - "absolute": "absolute" - }, - "datasource": { - "type": "Duomenų šaltinio tipas", - "name": "Pavadinimas", - "label": "Etiketė", - "add-datasource-prompt": "Pridėkite duomenų šaltinį" + "password-requirement": { + "at-least": "Bent:", + "character": "{ count, plural, =1 {1 simbolis} other {# simboliai(-ų)} }", + "digit": "{ count, plural, =1 {1 skaitmuo} other {# skaitmenys(-ų)} }", + "incorrect-password-try-again": "Neteisingas slaptažodis. Bandykite dar kartą", + "lowercase-letter": "{ count, plural, =1 {1 mažoji raidė} other {# mažosios raidės(-ių)} }", + "new-passwords-not-match": "Nauji slaptažodžiai nesutampa", + "password-should-not-contain-spaces": "Slaptažodyje negali būti tarpų", + "password-not-meet-requirements": "Slaptažodis neatitinka reikalavimų", + "password-requirements": "Slaptažodžio reikalavimai", + "password-should-difference": "Naujas slaptažodis turi skirtis nuo dabartinio", + "special-character": "{ count, plural, =1 {1 specialus simbolis} other {# specialūs simboliai(-ių)} }", + "uppercase-letter": "{ count, plural, =1 {1 didžioji raidė} other {# didžiosios raidės(-ių)} }", + "at-most": "Daugiausia:" + } + }, + "relation": { + "relations": "Ryšiai", + "direction": "Kryptis", + "clear-relation-type": "Aiškus ryšio tipas", + "search-direction": { + "FROM": "Iš", + "TO": "Į" }, - "details": { - "details": "Informacija", - "edit-mode": "Redagavimo režimas", - "edit-json": "Redaguoti JSON", - "toggle-edit-mode": "Redagavimo režimas" + "direction-type": { + "FROM": "iš", + "TO": "į" }, - "device": { - "all": "Visi", - "all-devices": "Visi įrenginiai", - "groups": "Grupės", - "shared": "Bendrinami", - "device": "Įrenginys", - "device-required": "Įrenginys būtinas.", - "devices": "Įrenginiai", - "management": "Įrenginių valdymas", - "view-devices": "Peržiūrėti įrenginius", - "device-alias": "Įrenginio pseudonimas", - "device-type-max-length": "Įrenginio tipas negali viršyti 256 simbolių", - "aliases": "Įrenginių pseudonimai aliases", - "no-alias-matching": "'{{alias}}' nerasta.", - "no-aliases-found": "Pseudonimų nėra.", - "no-key-matching": "'{{key}}' nerasta.", - "no-keys-found": "Raktų nėra.", - "create-new-alias": "Sukurti naują!", - "create-new-key": "Sukurti naują!", - "duplicate-alias-error": "'{{alias}}' dubliuojasi.
    Įrenginio pseudonimai skydelyje turi būti unikalūs.", - "configure-alias": "Sukonfigūruoti '{{alias}}' pseudonimą", - "no-devices-matching": "Įrenginių, attinkančių '{{entity}}' nėra.", - "alias": "Pseudonimas", - "alias-required": "Įrenginio pseudonimas būtinas.", - "remove-alias": "Panaikinti įrenginio pseudonimą", - "add-alias": "Pridėti įrenginio pseudonimą", - "name-starts-with": "Įrenginio pavadinimas prasideda", - "help-text": "Naudokite '%' simbolį pagal tai, kaip norite ieškoti: '%įrenginio_pavadinimo_fragmentas%', '%įrenginio_pavadinimo_pabaiga', 'įrenginio_pavadinimo_pradžia%'.", - "device-list": "Įrenginių sąrašas", - "use-device-name-filter": "Naudoti filtrą", - "device-list-empty": "Įrenginiai nepasirinkti.", - "device-name-filter-required": "Įrenginio pavadinimo filtras turi būti nustatytas.", - "device-name-filter-no-device-matched": "Įrenginių, kurių pavadinimas prasideda '{{device}}' nėra.", - "add": "Pridėti įrenginį", - "assign-to-customer": "Priskirti kientui", - "assign-device-to-customer": "Įrenginį (-ius) priskirti klientui", - "assign-device-to-customer-text": "Pasirinkite įrenginius, kuriuos norite priskirti klientui", - "make-public": "Įrenginį padaryti viešu", - "make-private": "Įrenginį padaryti privačiu", - "no-devices-text": "Įrenginių nėra", - "assign-to-customer-text": "Pasirinkite klientą, kuriam norite priskirti įrenginį (-ius)", - "device-details": "Įrenginio informacija", - "add-device-text": "Pridėti naują įrenginį", - "credentials": "Įgaliojimai", - "manage-credentials": "Valdyti įgaliojimus", - "delete": "Panaikinti įrenginį", - "assign-devices": "priskirti įrenginius", - "assign-devices-text": "Priskirti { count, plural, =1 {1 įrenginį} other {# įrenginius} } klientui", - "delete-devices": "Panaikinti įrenginius", - "unassign-from-customer": "Atseti nuo kliento", - "unassign-devices": "Atsieti įrenginius", - "unassign-devices-action-title": "Atsieti { count, plural, =1 {1 įrenginį} other {# įrenginius} } nuo kliento", - "unassign-device-from-edge-title": "Are you sure you want to unassign the device '{{deviceName}}'?", - "unassign-device-from-edge-text": "After the confirmation the device will be unassigned and won't be accessible by the edge.", - "unassign-devices-from-edge": "Unassign devices from edge", - "assign-new-device": "Priskirti naują įrenginį", - "make-public-device-title": "Ar tikrai norite įrenginį '{{deviceName}}' padaryti viešu?", - "make-public-device-text": "Po patvirtinimo įrenginys ir visi jo duomenys bus vieši ir prieinami kitiems vartotojams.", - "make-private-device-title": "Ar tikrai norite įrenginį '{{deviceName}}' padaryti privačiu?", - "make-private-device-text": "Po patvirtinimo įrenginys ir visi jo duomenys taps privatūs ir nebus prieinami kitiems vartotojams.", - "view-credentials": "Peržiūrėti įgaliojimus", - "delete-device-title": "Ar tikrai norite panaikinti įrenginį '{{deviceName}}'?", - "delete-device-text": "Būkite dėmesingi, po patvirtinimo įrenginys ir visa su juo susijusi informacija bus panaikinta ir jų atkurti nebegalėsite", - "delete-devices-title": "Ar tikrai norite panaikinti { count, plural, =1 {1 įrenginį} other {# įrenginius} }?", - "delete-devices-action-title": "Panaikinti { count, plural, =1 {1 įrenginį} other {# įrenginius} }", - "delete-devices-text": "Būkite dėmesingi, po patvirtinimo, visi pasirinkti įrenginiai ir su jais susijusi informacija bus panaikinta ir jų atkurti nebegalėsite.", - "unassign-device-title": "Ar tikrai norite atsieti įrenginį '{{deviceName}}'?", - "unassign-device-text": "Po patvirtinimo įrenginys bus atsietas ir klientas jo nebematys.", - "unassign-device": "Atsieti įrenginį", - "unassign-devices-title": "Ar tikrai norite atsieti { count, plural, =1 {1 įrenginį} other {# įrenginius} }?", - "unassign-devices-text": "Po patvirtinimo visi pasirinkti įrenginiai bus atsieti nuo kliento.", - "device-credentials": "įrenginio įgaliojimai", - "loading-device-credentials": "Įkeliami įrenginio įgaliojimai...", - "credentials-type": "Įgaliojimų tipas", - "access-token": "Prieigos raktas", - "access-token-required": "Prieigos raktas būtinas.", - "access-token-invalid": "Prieigos rakto ilgis turi būti nuo 1 iki 32 simbolių.", - "certificate-pem-format": "Certificate in PEM format", - "certificate-pem-format-required": "Certificate is required.", - "copy-access-token": "Copy Access token", - "copy-certificate": "Copy Certificate", - "copy-client-id": "Copy Client ID", - "copy-user-name": "Copy User name", - "copy-password": "Copy Password", - "generate-client-id": "Generate Client ID", - "generate-user-name": "Generate User name", - "generate-password": "Generate Password", - "generate-access-token": "Generate Access Token", - "lwm2m-security-config": { - "identity": "Client Identity", - "identity-required": "Client Identity is required.", - "identity-tooltip": "The PSK identifier is an arbitrary PSK identifier up to 128 bytes, as described in the standard [RFC7925].\nThe PSK identifier MUST first be converted to a character string and then encoded into octets using UTF-8.", - "client-key": "Client Key", - "client-key-required": "Client Key is required.", - "client-key-tooltip-prk": "RPK public key or id must be in the standard [RFC7250] and encoded to Base64 format!", - "client-key-tooltip-psk": "PSK key must be in the standard [RFC4279] and HexDec format: 32, 64, 128 characters!", - "endpoint": "Endpoint Client Name", - "endpoint-required": "Endpoint Client Name is required.", - "client-public-key": "Client public key", - "client-public-key-hint": "If client public key is empty, the trusted certificate will be used", - "client-public-key-tooltip": "X509 public key must be in DER-encoded X509v3 format and support exclusively EC algorithm and then encoded to Base64 format!", - "mode": "Security config mode", - "client-tab": "Client Security Config", - "client-certificate": "Client certificate", - "bootstrap-tab": "Bootstrap Client", - "bootstrap-server": "Bootstrap Server", - "lwm2m-server": "LwM2M Server", - "client-publicKey-or-id": "Client Public Key or Id", - "client-publicKey-or-id-required": "Client Public Key or Id is required.", - "client-publicKey-or-id-tooltip-psk": "The PSK identifier is an arbitrary PSK identifier up to 128 bytes, as described in the standard [RFC7925].\nThe PSK identifier MUST first be converted to a character string and then encoded into octets using UTF-8.", - "client-publicKey-or-id-tooltip-rpk": "RPK public key or id must be in the standard [RFC7250] and encoded to Base64 format!", - "client-publicKey-or-id-tooltip-x509": "X509 public key must be in DER-encoded X509v3 format and support exclusively EC algorithm and then encoded to Base64 format", - "client-secret-key": "Client Secret Key", - "client-secret-key-required": "Client Secret Key is required.", - "client-secret-key-tooltip-psk": "PSK key must be in the standard [RFC4279] and HexDec format: 32, 64, 128 characters!", - "client-secret-key-tooltip-prk": "RPK secret key must be in PKCS_8 format (DER encoding, standard [RFC5958]) and then encoded to Base64 format!", - "client-secret-key-tooltip-x509": "X509 secret key must be in PKCS_8 format (DER encoding, standard [RFC5958]) and then encoded to Base64 format!" - }, - "client-id": "Client ID", - "client-id-pattern": "Contains invalid character.", - "user-name": "User Name", - "user-name-required": "User Name is required.", - "client-id-or-user-name-necessary": "Client ID and/or User Name are necessary", - "password": "Password", - "secret": "Secret", - "secret-required": "Secret is required.", - "device-type": "Įrenginio tipas", - "device-type-required": "Įrenginio tipas būtinas.", - "select-device-type": "Pasirinkite įrenginio tipą", - "enter-device-type": "Įveskite įrenginio tipą", - "any-device": "Bet kuris įrenginys", - "no-device-types-matching": "Įrenginio tipų, atitinkančių '{{entitySubtype}}', nėra.", - "device-type-list-empty": "Nepasirinktas įrenginio tipas!", - "device-types": "Įrenginių tipai", - "name": "Pavadinimas", - "name-required": "Pavadinimas būtinas.", - "name-max-length": "Pavadinimas negali viršyti 256 simbolių", - "label-max-length": "Etiketė negali viršyti 256 simbolių", - "description": "Aprašymas", + "from-relations": "Išeinantys ryšiai", + "to-relations": "įeinantys ryšiai", + "selected-relations": "Pasirinkta { count, plural, =1 {1 ryšys} other {# ryšiai} } selected", + "type": "Tipas", + "to-entity-type": "Į subjekto tipą", + "to-entity-name": "Į subjektą", + "from-entity-type": "iš subjektų tipo", + "from-entity-name": "Iš subjekto", + "to-entity": "į subjektą", + "from-entity": "Iš subjekto", + "delete": "panaikinti ryšį", + "relation-type": "Ryšio tipas", + "relation-type-required": "Ryšio tipas būtinas.", + "relation-type-max-length": "Ryšio tipas negali viršyti 256 simbolių", + "any-relation-type": "Bet kuris tipas", + "add": "pridėti ryšį", + "edit": "Redaguoti ryšį", + "delete-to-relation-title": "Ar tikrai norite panaikinti ryšį su '{{entityName}}' subjektu?", + "delete-to-relation-text": "Būkite dėmesingi, po patvirtinimo '{{entityName}}' subjektas neteks ryšio su dabartiniu subjektu.", + "delete-to-relations-title": "Ar tikrai norite panaikinti { count, plural, =1 {1 ryšį} other {# ryšius} }?", + "delete-to-relations-text": "Būkite dėmesingi, po patvirtinimo visi pasirinkti ryšiai bus panaikinti ir atitinkami subjektai neteks ryšių su dabartiniu subjektu.", + "delete-from-relation-title": "Ar tikrai norite panaikinti ryšį iš '{{entityName}}' subjekto?", + "delete-from-relation-text": "Būkite dėmesingi, po patvirtinimo, dabartinis subjektas neteks ryšio su '{{entityName}}' subjektu.", + "delete-from-relations-title": "Ar tikrai norite panaikinti { count, plural, =1 {1 ryšį} other {# ryšius} }?", + "delete-from-relations-text": "Būkite dėmesingi, po patvirtinimo, visi pasirinkti ryšiai bus panaikinti ir dabartinis subjektas neteks ryšių su atitinkamais subjektais.", + "remove-relation-filter": "Panaikinti ryšių filtrą", + "remove-filter": "Panakinti filtrą", + "add-relation-filter": "Pridėti ryšių filtrą", + "any-relation": "Bet kuris ryšys", + "relation-filters": "Ryšių filtrai", + "relation-filter": "Relation filter", + "additional-info": "Papildoma informacija (JSON)", + "invalid-additional-info": "Papildomos JSON informacijos išanalizuoti nepavyko.", + "no-relations-text": "Ryšių nėra", + "not": "Ne" + }, + "resource": { + "add": "Pridėti išteklių", + "all-types": "Visi", + "copyId": "Kopijuoti ištekliaus ID", + "delete": "Ištrinti išteklių", + "delete-resource-text": "Būkite atsargūs, po patvirtinimo šis išteklius bus negrįžtamai pašalintas.", + "delete-resource-title": "Ar tikrai norite ištrinti išteklių '{{resourceTitle}}'?", + "delete-resources-action-title": "Ištrinti { count, plural, =1 {1 išteklių} other {# išteklius(-ių)} }", + "delete-resources-text": "Atkreipkite dėmesį, kad pasirinkti ištekliai bus ištrinti, net jei jie naudojami įrenginių profiliuose.", + "delete-resources-title": "Ar tikrai norite ištrinti { count, plural, =1 {1 išteklių} other {# išteklius(-ių)} }?", + "download": "Atsisiųsti išteklių", + "drop-file": "Įkelkite išteklių failą arba spustelėkite norėdami pasirinkti failą.", + "drop-resource-file-or": "Vilkdami įkelkite išteklių failą arba", + "empty": "Išteklius tuščias", + "file-name": "Failo pavadinimas", + "idCopiedMessage": "Išteklius ID nukopijuotas į iškarpinę", + "no-resource-matching": "Nerasta ištekliaus, atitinkančio '{{widgetsBundle}}'.", + "no-resource-text": "Išteklių nerasta", + "open-widgets-bundle": "Atidaryti valdiklių paketą", + "resource": "Išteklius", + "resource-file": "Išteklių failas", + "resource-files": "Išteklių failai", + "resource-library-details": "Išteklių informacija", + "resource-type": "Išteklių tipas", + "resources-library": "Išteklių biblioteka", + "search": "Ieškoti išteklių", + "selected-resources": "{ count, plural, =1 {1 išteklius} other {# ištekliai(-ių)} } pasirinkta", + "system": "Sistema", + "title": "Pavadinimas", + "title-required": "Pavadinimas yra privalomas.", + "title-max-length": "Pavadinimas turi būti trumpesnis nei 256 simboliai", + "type": { + "jks": "JKS", + "js-module": "JS modulis", + "lwm2m-model": "LWM2M modelis", + "pkcs-12": "PKCS #12" + }, + "resource-sub-type": "Potipio", + "sub-type": { + "image": "Paveikslėlis", + "scada-symbol": "SCADA simbolis", + "extension": "Plėtinys", + "module": "Modulis" + } + }, + "javascript": { + "add": "Pridėti JavaScript išteklių", + "delete": "Ištrinti JavaScript išteklių", + "delete-javascript-resource-text": "Būkite atsargūs – po patvirtinimo šis JavaScript išteklius bus nebeatkuriamas.", + "delete-javascript-resource-title": "Ar tikrai norite ištrinti JavaScript išteklių '{{resourceTitle}}'?", + "delete-javascript-resources-action-title": "Ištrinti JavaScript { count, plural, =1 {1 išteklių} other {# išteklius} }", + "delete-javascript-resources-text": "Atkreipkite dėmesį, kad pasirinkti JavaScript ištekliai, net jei jie naudojami JavaScript funkcijose, bus ištrinti.", + "delete-javascript-resources-title": "Ar tikrai norite ištrinti JavaScript { count, plural, =1 {1 išteklių} other {# išteklius} }?", + "delete-javascript-resource-in-use-text": "Jei vis tiek norite ištrinti šį JavaScript išteklių, spustelėkite mygtuką Ištrinti vis tiek.", + "download": "Atsisiųsti JavaScript išteklių", + "upload-from-file": "Įkelti JavaScript iš failo", + "resource-file": "JavaScript ištekliaus failas", + "drop-file": "Įmeskite JavaScript failą arba spustelėkite, kad pasirinktumėte failą įkėlimui.", + "drop-resource-file-or": "Vilkite ir numeskite JavaScript failą arba", + "javascript-library": "JavaScript biblioteka", + "javascript-type": "JavaScript tipas", + "javascript-resource-details": "JavaScript ištekliaus detalės", + "javascript-resource-is-in-use": "JavaScript išteklius naudojamas kituose objektuose", + "javascript-resources-are-in-use": "JavaScript ištekliai naudojami kituose objektuose", + "javascript-resource-is-in-use-text": "JavaScript išteklius '{{title}}' nebuvo ištrintas, nes jį naudoja šie objektai:", + "javascript-resources-are-in-use-text": "Ne visi JavaScript ištekliai buvo ištrinti, nes jie naudojami kituose objektuose.
    Galite peržiūrėti susijusius objektus spustelėdami mygtuką Nuorodos atitinkamoje eilutėje.
    Jei vis tiek norite ištrinti šiuos JavaScript išteklius, pasirinkite juos lentelėje ir spustelėkite mygtuką Ištrinti pasirinktus.", + "search": "Ieškoti JavaScript išteklių", + "selected-javascript-resources": "{ count, plural, =1 {1 JavaScript išteklius} other {# JavaScript ištekliai} } pasirinkta", + "no-javascript-resource-text": "JavaScript išteklių nerasta", + "all-types": "Visi", + "module-script": "Modulio scenarijus" + }, + "rpc": { + "error": { + "target-device-is-not-set": "Tikslinis įrenginys nenustatytas!", + "invalid-target-entity": "RPC komandos nepalaikomos {{entityType}} objektui.", + "failed-to-resolve-target-device": "Nepavyko nustatyti tikslo įrenginio!", + "request-timeout": "Užklausos laikas baigėsi", + "rpc-http-error": "Klaida: {{status}} - {{statusText}}" + } + }, + "rulechain": { + "rulechain": "Taisyklių grandinė", + "rulechain-events": "Taisyklių grandinės įvykiai", + "rulechains": "Taisyklių grandinės", + "root": "Pagrindinė", + "delete": "Ištrinti taisyklių grandinę", + "name": "Pavadinimas", + "name-required": "Pavadinimas yra privalomas.", + "name-max-length": "Pavadinimas turi būti trumpesnis nei 256 simboliai", + "description": "Aprašymas", + "add": "Pridėti taisyklių grandinę", + "set-root": "Padaryti pagrindine", + "set-root-rulechain-title": "Ar tikrai norite taisyklių grandinę '{{ruleChainName}}' padaryti pagrindine?", + "set-root-rulechain-text": "Patvirtinus ši taisyklių grandinė taps pagrindine ir tvarkys visus įeinančius transporto pranešimus.", + "delete-rulechain-title": "Ar tikrai norite ištrinti taisyklių grandinę '{{ruleChainName}}'?", + "delete-rulechain-text": "Atsargiai, po patvirtinimo ši taisyklių grandinė ir visi susiję duomenys bus negrįžtamai pašalinti.", + "delete-rulechains-title": "Ar tikrai norite ištrinti { count, plural, =1 {1 taisyklių grandinę} other {# taisyklių grandines} }?", + "delete-rulechains-action-title": "Ištrinti { count, plural, =1 {1 taisyklių grandinę} other {# taisyklių grandines} }", + "delete-rulechains-text": "Atsargiai, po patvirtinimo visos pažymėtos taisyklių grandinės bus pašalintos ir visi susiję duomenys bus prarasti.", + "add-rulechain-text": "Pridėti naują taisyklių grandinę", + "no-rulechains-text": "Taisyklių grandinių nerasta", + "rulechain-details": "Taisyklių grandinės detalės", + "details": "Detalės", + "events": "Įvykiai", + "system": "Sistema", + "import": "Importuoti taisyklių grandinę", + "export": "Eksportuoti taisyklių grandinę", + "export-failed-error": "Nepavyko eksportuoti taisyklių grandinės: {{error}}", + "create-new-rulechain": "Sukurti naują taisyklių grandinę", + "rulechain-file": "Taisyklių grandinės failas", + "invalid-rulechain-file-error": "Nepavyko importuoti taisyklių grandinės: netinkama duomenų struktūra.", + "copyId": "Kopijuoti taisyklių grandinės ID", + "idCopiedMessage": "Taisyklių grandinės ID nukopijuotas į iškarpinę", + "select-rulechain": "Pasirinkti taisyklių grandinę", + "no-rulechains-matching": "Taisyklių grandinių, atitinkančių '{{entity}}', nerasta.", + "rulechain-required": "Taisyklių grandinė yra privaloma", + "management": "Taisyklių valdymas", + "debug-mode": "Derinimo režimas", + "search": "Ieškoti taisyklių grandinių", + "selected-rulechains": "{ count, plural, =1 {1 taisyklių grandinė} other {# taisyklių grandinės} } pasirinkta", + "open-rulechain": "Atidaryti taisyklių grandinę", + "edge-template-root": "Edge šablono pagrindas", + "assign-to-edge": "Priskirti Edge", + "edge-rulechain": "Edge taisyklių grandinė", + "unassign-rulechain-from-edge-text": "Patvirtinus, taisyklių grandinė bus atšaukta ir nebebus pasiekiama Edge įrenginiui.", + "unassign-rulechains-from-edge-title": "Ar tikrai norite atšaukti { count, plural, =1 {1 taisyklių grandinę} other {# taisyklių grandines} }?", + "unassign-rulechains-from-edge-text": "Patvirtinus visos pažymėtos taisyklių grandinės bus atšauktos ir nebebus prieinamos Edge įrenginiui.", + "assign-rulechain-to-edge-title": "Priskirti taisyklių grandinę (-es) Edge įrenginiui", + "assign-rulechain-to-edge-text": "Pasirinkite taisyklių grandines, kurias norite priskirti Edge įrenginiui", + "set-edge-template-root-rulechain": "Padaryti taisyklių grandinę Edge šablono pagrindine", + "set-edge-template-root-rulechain-title": "Ar tikrai norite taisyklių grandinę '{{ruleChainName}}' padaryti Edge šablono pagrindine?", + "set-edge-template-root-rulechain-text": "Patvirtinus ši taisyklių grandinė taps pagrindine naujai kuriamiems Edge įrenginiams.", + "invalid-rulechain-type-error": "Nepavyko importuoti taisyklių grandinės: netinkamas tipas. Tikimasi tipo {{expectedRuleChainType}}.", + "set-auto-assign-to-edge": "Automatiškai priskirti taisyklių grandinę Edge įrenginiams kūrimo metu", + "set-auto-assign-to-edge-title": "Ar tikrai norite automatiškai priskirti Edge taisyklių grandinę '{{ruleChainName}}' naujai kuriamiems Edge įrenginiams?", + "set-auto-assign-to-edge-text": "Patvirtinus ši taisyklių grandinė bus automatiškai priskiriama Edge įrenginiams jų kūrimo metu.", + "unset-auto-assign-to-edge": "Nepriskirti taisyklių grandinės Edge įrenginiams kūrimo metu", + "unset-auto-assign-to-edge-title": "Ar tikrai nenorite automatiškai priskirti Edge taisyklių grandinės '{{ruleChainName}}' naujai kuriamiems Edge įrenginiams?", + "unset-auto-assign-to-edge-text": "Patvirtinus ši taisyklių grandinė nebus automatiškai priskiriama Edge įrenginiams.", + "unassign-rulechain-title": "Ar tikrai norite atšaukti taisyklių grandinę '{{ruleChainName}}'?", + "unassign-rulechains": "Atšaukti taisyklių grandines" + }, + "rulenode": { + "rule-node-events": "Taisyklių mazgų įvykiai", + "details": "Detalės", + "events": "Įvykiai", + "search": "Ieškoti mazgų", + "open-node-library": "Atidaryti mazgų biblioteką", + "close-node-library": "Uždaryti mazgų biblioteką", + "add": "Pridėti taisyklės mazgą", + "name": "Pavadinimas", + "name-required": "Pavadinimas yra privalomas.", + "name-max-length": "Pavadinimas turi būti trumpesnis nei 256 simboliai", + "type": "Tipas", + "rule-node-description": "Taisyklių mazgo aprašymas", + "delete": "Ištrinti taisyklės mazgą", + "select-all-objects": "Pažymėti visus mazgus ir jungtis", + "deselect-all-objects": "Atžymėti visus mazgus ir jungtis", + "delete-selected-objects": "Ištrinti pažymėtus mazgus ir jungtis", + "delete-selected": "Ištrinti pažymėtus", + "create-nested-rulechain": "Sukurti įdėtą taisyklių grandinę", + "select-all": "Pažymėti visus", + "copy-selected": "Kopijuoti pažymėtus", + "deselect-all": "Atžymėti visus", + "rulenode-details": "Taisyklių mazgo detalės", + "debug-mode": "Derinimo režimas", + "singleton": "Vienetinis (Singleton)", + "configuration": "Konfigūracija", + "link": "Jungtis", + "link-details": "Taisyklių mazgo jungties detalės", + "add-link": "Pridėti jungtį", + "link-label": "Jungties pavadinimas", + "link-label-required": "Jungties pavadinimas yra privalomas.", + "custom-link-label": "Tinkintas jungties pavadinimas", + "custom-link-label-required": "Tinkintas jungties pavadinimas yra privalomas.", + "link-labels": "Jungčių pavadinimai", + "link-labels-required": "Jungčių pavadinimai yra privalomi.", + "no-link-labels-found": "Jungčių pavadinimų nerasta", + "no-link-label-matching": "Jungtis '{{label}}' nerasta.", + "create-new-link-label": "Sukurti naują!", + "type-filter": "Filtras", + "type-filter-details": "Filtruoti įeinančias žinutes pagal konfigūruotas sąlygas", + "type-enrichment": "Praturtinimas", + "type-enrichment-details": "Papildyti žinutės metaduomenis papildoma informacija", + "type-transformation": "Transformacija", + "type-transformation-details": "Keisti žinutės turinį arba metaduomenis", + "type-action": "Veiksmas", + "type-action-details": "Atlikti specialų veiksmą", + "type-external": "Išorinis", + "type-external-details": "Sąveikauja su išorine sistema", + "type-rule-chain": "Taisyklių grandinė", + "type-rule-chain-details": "Perduoda įeinančias žinutes nurodytai taisyklių grandinei", + "type-flow": "Srautas", + "type-flow-details": "Organizuoja žinučių srautą", + "type-input": "Įvestis", + "type-input-details": "Taisyklių grandinės loginė įvestis, perduodanti žinutes tolesniam susijusiam mazgui", + "type-unknown": "Nežinomas", + "type-unknown-details": "Nesprendžiamas taisyklių mazgas", + "directive-is-not-loaded": "Nurodyta konfigūracijos direktyva '{{directiveName}}' nėra prieinama.", + "ui-resources-load-error": "Nepavyko įkelti konfigūracijos UI išteklių.", + "invalid-target-rulechain": "Nepavyko nustatyti paskirties taisyklių grandinės!", + "test-script-function": "Testuoti skripto funkciją", + "script-lang-java-script": "JavaScript", + "script-lang-tbel": "TBEL", + "message": "Žinutė", + "message-type": "Žinutės tipas", + "select-message-type": "Pasirinkti žinutės tipą", + "message-type-required": "Žinutės tipas yra privalomas", + "metadata": "Metaduomenys", + "metadata-required": "Metaduomenų įrašai negali būti tušti.", + "output": "Išvestis", + "test": "Testuoti", + "help": "Pagalba", + "reset-debug-settings": "Atstatyti derinimo nustatymus visuose mazguose", + "test-with-this-message": "{{test}} su šia žinute", + "queue-hint": "Pasirinkite eilę, į kurią bus persiunčiamos žinutės. Numatytoji eilė – 'Main'.", + "queue-singleton-hint": "Pasirinkite eilę žinučių perdavimui kelių egzempliorių aplinkoje. Numatytoji eilė – 'Pagrindinis'." + }, + "rule-node-config": { + "id": "Id", + "additional-info": "Papildoma informacija", + "advanced-settings": "Išplėstiniai nustatymai", + "create-entity-if-not-exists": "Sukurti naują objektą, jei jis neegzistuoja", + "create-entity-if-not-exists-hint": "Jei įjungta, naujas objektas su nurodytais parametrais bus sukurtas, nebent toks jau egzistuoja. Esami objektai bus naudojami tokie, kokie yra, ryšio užmezgimui.", + "select-device-connectivity-event": "Pasirinkite įrenginio ryšio įvykį", + "entity-name-pattern": "Pavadinimo šablonas", + "device-name-pattern": "Įrenginio pavadinimas", + "asset-name-pattern": "Turto pavadinimas", + "entity-view-name-pattern": "Objekto peržiūros pavadinimas", + "customer-title-pattern": "Kliento pavadinimas", + "dashboard-name-pattern": "Valdymo skydelio pavadinimas", + "user-name-pattern": "Naudotojo el. paštas", + "edge-name-pattern": "Edge pavadinimas", + "entity-name-pattern-required": "Pavadinimo šablonas yra privalomas", + "entity-name-pattern-hint": "Pavadinimo šablono laukas palaiko šablonizaciją. Naudokite $[messageKey], kad gautumėte reikšmę iš pranešimo, ir ${metadataKey}, kad gautumėte reikšmę iš metaduomenų.", + "copy-message-type": "Kopijuoti pranešimo tipą", + "entity-type-pattern": "Tipo šablonas", + "entity-type-pattern-required": "Tipo šablonas yra privalomas", + "message-type-value": "Pranešimo tipo reikšmė", + "message-type-value-required": "Pranešimo tipo reikšmė yra privaloma", + "message-type-value-max-length": "Pranešimo tipo reikšmė turi būti trumpesnė nei 256 simboliai", + "output-message-type": "Išvesties pranešimo tipas", + "entity-cache-expiration": "Objektų talpyklos galiojimo laikas (sek.)", + "entity-cache-expiration-hint": "Nurodo maksimalų laiką, kiek galima saugoti rastų objektų įrašus. Reikšmė 0 reiškia, kad įrašai niekada nesibaigs.", + "entity-cache-expiration-required": "Objektų talpyklos galiojimo laikas yra privalomas.", + "entity-cache-expiration-range": "Objektų talpyklos galiojimo laikas turi būti didesnis arba lygus 0.", + "customer-name-pattern": "Kliento pavadinimas", + "customer-name-pattern-required": "Kliento pavadinimas yra privalomas", + "customer-name-pattern-hint": "Naudokite $[messageKey], kad gautumėte reikšmę iš pranešimo, ir ${metadataKey}, kad gautumėte reikšmę iš metaduomenų.", + "create-customer-if-not-exists": "Sukurti naują klientą, jei jis neegzistuoja", + "unassign-from-customer": "Atšaukti priskyrimą klientui, jei siuntėjas yra valdymo skydelis", + "unassign-from-customer-tooltip": "Tik valdymo skydeliai gali būti priskirti keliems klientams vienu metu.\nJei pranešimo siuntėjas yra valdymo skydelis, turite aiškiai nurodyti kliento pavadinimą, kad jį atšauktumėte.", + "customer-cache-expiration": "Klientų talpyklos galiojimo laikas (sek.)", + "customer-cache-expiration-hint": "Nurodo maksimalų laiką, kiek galima saugoti rastų klientų įrašus. Reikšmė 0 reiškia, kad įrašai niekada nesibaigs.", + "customer-cache-expiration-required": "Klientų talpyklos galiojimo laikas yra privalomas.", + "customer-cache-expiration-range": "Klientų talpyklos galiojimo laikas turi būti didesnis arba lygus 0.", + "interval-start": "Intervalo pradžia", + "interval-end": "Intervalo pabaiga", + "time-unit": "Laiko vienetas", + "fetch-mode": "Gavimo režimas", + "order-by-timestamp": "Rikiuoti pagal laiko žymą", + "limit": "Riba", + "limit-hint": "Mažiausia riba – 2, didžiausia – 1000. Jei norite gauti vieną įrašą, pasirinkite gavimo režimą 'Pirmas' arba 'Paskutinis'.", + "limit-required": "Riba yra privaloma.", + "limit-range": "Riba turi būti nuo 2 iki 1000.", + "time-unit-milliseconds": "Milisekundės", + "time-unit-seconds": "Sekundės", + "time-unit-minutes": "Minutės", + "time-unit-hours": "Valandos", + "time-unit-days": "Dienos", + "time-value-range": "Leidžiamas diapazonas nuo 1 iki 2147483647.", + "start-interval-value-required": "Intervalo pradžia yra privaloma.", + "end-interval-value-required": "Intervalo pabaiga yra privaloma.", + "filter": "Filtras", + "switch": "Perjungiklis", + "math-templatization-tooltip": "Šis laukas palaiko šablonizaciją. Naudokite $[messageKey], kad gautumėte reikšmę iš pranešimo, ir ${metadataKey}, kad gautumėte reikšmę iš metaduomenų.", + "add-message-type": "Pridėti pranešimo tipą", + "select-message-types-required": "Turi būti pasirinktas bent vienas pranešimo tipas.", + "select-message-types": "Pasirinkti pranešimo tipus", + "no-message-types-found": "Pranešimo tipų nerasta", + "no-message-type-matching": "'{{messageType}}' nerasta.", + "create-new-message-type": "Sukurti naują.", + "message-types-required": "Pranešimo tipai yra privalomi.", + "client-attributes": "Kliento atributai", + "shared-attributes": "Bendri atributai", + "server-attributes": "Serverio atributai", + "attributes-keys": "Atributų raktai", + "attributes-keys-required": "Atributų raktai yra privalomi", + "attributes-scope": "Atributų sritis", + "attributes-scope-value": "Atributų srities reikšmė", + "attributes-scope-value-copy": "Kopijuoti atributų srities reikšmę", + "attributes-scope-hint": "Naudokite metaduomenų raktą 'scope', kad dinamiškai nustatytumėte atributų sritį kiekvienam pranešimui. Jei pateikta, tai perrašo konfigūracijoje nustatytą sritį.", + "notify-device": "Priverstinai pranešti įrenginiui", + "send-attributes-updated-notification": "Siųsti atnaujintų atributų pranešimą", + "send-attributes-updated-notification-hint": "Siųsti pranešimą apie atnaujintus atributus kaip atskirą žinutę į taisyklių variklio eilę.", + "send-attributes-deleted-notification": "Siųsti ištrintų atributų pranešimą", + "send-attributes-deleted-notification-hint": "Siųsti pranešimą apie ištrintus atributus kaip atskirą žinutę į taisyklių variklio eilę.", + "update-attributes-only-on-value-change": "Išsaugoti atributus tik jei pasikeitė reikšmė", + "update-attributes-only-on-value-change-hint": "Atnaujina atributus su kiekvienu gaunamu pranešimu, neatsižvelgiant į tai, ar reikšmė pasikeitė. Tai padidina API apkrovą ir sumažina našumą.", + "update-attributes-only-on-value-change-hint-enabled": "Atnaujina atributus tik jei jų reikšmė pasikeitė. Jei reikšmė nepasikeitė, atributų laiko žyma ir pranešimas apie pakeitimą nebus siunčiami.", + "fetch-credentials-to-metadata": "Įtraukti prisijungimo duomenis į metaduomenis", + "notify-device-on-update-hint": "Jei įjungta, priverstinai praneša įrenginiui apie bendrų atributų atnaujinimą. Jei išjungta, pranešimo elgesį valdo 'notifyDevice' parametras iš gaunamo pranešimo metaduomenų. Norėdami išjungti pranešimą, pranešimo metaduomenyse turi būti parametras 'notifyDevice' nustatytas į 'false'. Kitais atvejais pranešimas bus siunčiamas įrenginiui.", + "notify-device-on-delete-hint": "Jei įjungta, priverstinai praneša įrenginiui apie bendrų atributų pašalinimą. Jei išjungta, pranešimo elgesį valdo 'notifyDevice' parametras iš gaunamo pranešimo metaduomenų. Norėdami įjungti pranešimą, pranešimo metaduomenyse turi būti parametras 'notifyDevice' nustatytas į 'true'. Kitais atvejais pranešimas įrenginiui nebus siunčiamas.", + "latest-timeseries": "Naujausių laiko eilučių duomenų raktai", + "timeseries-keys": "Laiko eilučių raktai", + "timeseries-keys-required": "Turi būti pasirinktas bent vienas laiko eilučių raktas.", + "add-timeseries-key": "Pridėti laiko eilučių raktą", + "add-message-field": "Pridėti pranešimo lauką", + "relation-search-parameters": "Ryšių paieškos parametrai", + "relation-parameters": "Ryšių parametrai", + "add-metadata-field": "Pridėti metaduomenų lauką", + "data-keys": "Pranešimo laukų pavadinimai", + "copy-from": "Kopijuoti iš", + "data-to-metadata": "Duomenis į metaduomenis", + "metadata-to-data": "Metaduomenis į duomenis", + "use-regular-expression-hint": "Naudokite reguliariąją išraišką raktų kopijavimui pagal šabloną.\n\nPatarimai:\nPaspauskite 'Enter', kad užbaigtumėte lauko pavadinimo įvedimą.\nPaspauskite 'Backspace', kad ištrintumėte lauko pavadinimą. Palaikomi keli pavadinimai.", + "interval": "Intervalas", + "interval-required": "Intervalas yra privalomas", + "interval-hint": "Dublikavimo intervalas sekundėmis.", + "interval-min-error": "Mažiausia leidžiama reikšmė yra 1", + "max-pending-msgs": "Maks. laukiančių pranešimų", + "max-pending-msgs-hint": "Maksimalus pranešimų skaičius, saugomas atmintyje kiekvienam unikaliam dublikavimo ID.", + "max-pending-msgs-required": "Maks. laukiančių pranešimų reikšmė yra privaloma", + "max-pending-msgs-max-error": "Didžiausia leidžiama reikšmė yra 1000", + "max-pending-msgs-min-error": "Mažiausia leidžiama reikšmė yra 1", + "max-retries": "Maksimalus bandymų skaičius", + "max-retries-required": "Maksimalus bandymų skaičius yra privalomas", + "max-retries-hint": "Didžiausias bandymų skaičius išsiųsti dublikuotus pranešimus į eilę. Tarp bandymų taikomas 10 sekundžių delsimas.", + "max-retries-max-error": "Didžiausia leidžiama reikšmė yra 100", + "max-retries-min-error": "Mažiausia leidžiama reikšmė yra 0", + "strategy": "Strategija", + "strategy-required": "Strategija yra privaloma", + "strategy-all-hint": "Grąžina visus pranešimus, gautus dublikavimo laikotarpiu, kaip vieną JSON masyvą, kur kiekvienas elementas turi objektą su 'msg' ir 'metadata' savybėmis.", + "strategy-first-hint": "Grąžina pirmą pranešimą, gautą dublikavimo laikotarpiu.", + "strategy-last-hint": "Grąžina paskutinį pranešimą, gautą dublikavimo laikotarpiu.", + "first": "Pirmas", + "last": "Paskutinis", + "all": "Visi", + "output-msg-type-hint": "Dublikavimo rezultato pranešimo tipas.", + "queue-name-hint": "Eilės pavadinimas, į kurią bus publikuojamas dublikavimo rezultatas.", + "keys": "Raktai", + "keys-required": "Raktai yra privalomi", + "rename-keys-in": "Pervadinti raktus", + "data": "Duomenys", + "message": "Pranešimas", + "metadata": "Metaduomenys", + "current-key-name": "Dabartinis rakto pavadinimas", + "key-name-required": "Rakto pavadinimas yra privalomas", + "new-key-name": "Naujas rakto pavadinimas", + "new-key-name-required": "Naujas rakto pavadinimas yra privalomas", + "metadata-keys": "Metaduomenų laukų pavadinimai", + "json-path-expression": "JSON kelio išraiška", + "json-path-expression-required": "JSON kelio išraiška yra privaloma", + "json-path-expression-hint": "JSONPath nurodo kelią į elementą arba elementų rinkinį JSON struktūroje. '$' reiškia šakninį objektą arba masyvą.", + "relations-query": "Ryšių užklausa", + "device-relations-query": "Įrenginio ryšių užklausa", + "max-relation-level": "Didžiausias ryšių lygis", + "max-relation-level-error": "Reikšmė turi būti didesnė nei 0 arba nenustatyta.", + "max-relation-level-invalid": "Reikšmė turi būti sveikasis skaičius.", + "relation-type": "Ryšio tipas", + "relation-type-pattern": "Ryšio tipo šablonas", + "relation-type-pattern-required": "Ryšio tipo šablonas yra privalomas", + "relation-types-list": "Ryšių tipai, kuriuos reikia perduoti", + "relation-types-list-hint": "Jei nepasirinkti ryšių tipai, signalai (alarmai) bus perduodami be filtravimo pagal ryšio tipą.", + "unlimited-level": "Neribotas lygis", + "latest-telemetry": "Naujausia telemetrija", + "add-telemetry-key": "Pridėti telemetrijos raktą", + "delete-from": "Ištrinti iš", + "use-regular-expression-delete-hint": "Naudokite reguliariąją išraišką raktų ištrynimui pagal šabloną.\n\nPatarimai:\nPaspauskite 'Enter', kad užbaigtumėte lauko pavadinimo įvedimą.\nPaspauskite 'Backspace', kad ištrintumėte lauko pavadinimą.\nPalaikomi keli pavadinimai.", + "fetch-into": "Užkrauti į", + "attr-mapping": "Atributų susiejimas:", + "source-attribute": "Pirminio atributo raktas", + "source-attribute-required": "Pirminio atributo raktas yra privalomas.", + "source-telemetry": "Pirminės telemetrijos raktas", + "source-telemetry-required": "Pirminės telemetrijos raktas yra privalomas.", + "target-key": "Tikslinis raktas", + "target-key-required": "Tikslinis raktas yra privalomas.", + "attr-mapping-required": "Turi būti nurodyta bent viena susiejimo eilutė.", + "fields-mapping": "Laukų susiejimas", + "fields-mapping-hint": "Jei pranešimo laukas nustatytas į $entityId, pranešimo kilmės (originatoriaus) ID bus įrašytas į nurodytą lentelės stulpelį.", + "relations-query-config-direction-suffix": "kilmės objektas", + "profile-name": "Profilio pavadinimas", + "fetch-circle-parameter-info-from-metadata-hint": "Metaduomenų laukas '{{perimeterKeyName}}' turi būti pateiktas šiuo formatu: {\"latitude\":48.196, \"longitude\":24.6532, \"radius\":100.0, \"radiusUnit\":\"METER\"}", + "fetch-poligon-parameter-info-from-metadata-hint": "Metaduomenų laukas '{{perimeterKeyName}}' turi būti pateiktas šiuo formatu: [[48.19736,24.65235],[48.19800,24.65060],...,[48.19849,24.65420]]", + "short-templatization-tooltip": "Naudokite $[messageKey], kad gautumėte reikšmę iš pranešimo, ir ${metadataKey}, kad gautumėte reikšmę iš metaduomenų.", + "fields-mapping-required": "Turi būti nurodytas bent vienas laukų susiejimas.", + "at-least-one-field-required": "Bent vienas įvesties laukas turi turėti reikšmę (-es).", + "originator-fields-sv-map-hint": "Tikslinių raktų laukai palaiko šablonizaciją. Naudokite $[messageKey], kad gautumėte reikšmę iš pranešimo, ir ${metadataKey}, kad gautumėte reikšmę iš metaduomenų.", + "sv-map-hint": "Tik tikslinių raktų laukai palaiko šablonizaciją. Naudokite $[messageKey], kad gautumėte reikšmę iš pranešimo, ir ${metadataKey}, kad gautumėte reikšmę iš metaduomenų.", + "source-field": "Pirminis laukas", + "source-field-required": "Pirminis laukas yra privalomas.", + "originator-source": "Kilmės šaltinis", + "new-originator": "Naujas kilmės objektas", + "originator-customer": "Klientas", + "originator-tenant": "Nuomininkas", + "originator-related": "Susijęs objektas", + "originator-alarm-originator": "Signalo kilmės objektas", + "originator-entity": "Objektas pagal pavadinimo šabloną", + "clone-message": "Klonuoti pranešimą", + "transform": "Transformuoti", + "default-ttl": "Numatytasis TTL", + "default-ttl-required": "Numatytasis TTL yra privalomas.", + "default-ttl-hint": "Taisyklės mazgas (Rule Node) paims Time-to-Live (TTL) reikšmę iš pranešimo metaduomenų. Jei reikšmės nėra, bus naudojamas konfigūracijoje nurodytas TTL. Jei reikšmė lygi 0, bus taikomas TTL iš nuomininko (Tenant) profilio konfigūracijos.", + "default-ttl-zero-hint": "TTL nebus taikomas, jei jo reikšmė nustatyta į 0.", + "min-default-ttl-message": "Leidžiama tik minimali 0 TTL reikšmė.", + "generation-parameters": "Generavimo parametrai", + "message-count": "Generuojamų pranešimų limitas (0 - neribota)", + "message-count-required": "Generuojamų pranešimų limitas yra privalomas.", + "min-message-count-message": "Leidžiama tik minimali 0 pranešimų reikšmė.", + "period-seconds": "Periodas sekundėmis", + "period-seconds-required": "Periodas yra privalomas.", + "generation-frequency-seconds": "Generavimo dažnis sekundėmis", + "generation-frequency-required": "Generavimo dažnis yra privalomas.", + "min-generation-frequency-message": "Leidžiamas mažiausias 60 sekundžių intervalas.", + "script-lang-tbel": "TBEL", + "script-lang-js": "JS", + "use-metadata-period-in-seconds-patterns": "Naudoti periodo sekundėmis šabloną", + "use-metadata-period-in-seconds-patterns-hint": "Jei pasirinkta, taisyklės mazgas naudos periodo sekundėmis šabloną iš pranešimo metaduomenų arba duomenų, laikydamas, kad intervalai pateikti sekundėmis.", + "period-in-seconds-pattern": "Periodo sekundėmis šablonas", + "period-in-seconds-pattern-required": "Periodo sekundėmis šablonas yra privalomas", + "min-period-seconds-message": "Leidžiamas mažiausias 60 sekundžių periodas.", + "originator": "Kilmės objektas", + "message-body": "Pranešimo turinys", + "message-metadata": "Pranešimo metaduomenys", + "generate": "Generuoti", + "current-rule-node": "Dabartinis taisyklės mazgas", + "current-tenant": "Dabartinis nuomininkas", + "generator-function": "Generatoriaus funkcija", + "test-generator-function": "Testuoti generatoriaus funkciją", + "generator": "Generatorius", + "test-filter-function": "Testuoti filtravimo funkciją", + "test-switch-function": "Testuoti perjungimo funkciją", + "test-transformer-function": "Testuoti transformavimo funkciją", + "transformer": "Transformatorius", + "alarm-create-condition": "Signalo (aliarmo) sukūrimo sąlyga", + "test-condition-function": "Testuoti sąlygos funkciją", + "alarm-clear-condition": "Signalo (aliarmo) išvalymo sąlyga", + "alarm-details-builder": "Signalo (aliarmo) informacijos kūrėjas", + "test-details-function": "Testuoti informacijos kūrimo funkciją", + "alarm-type": "Signalo tipas", + "select-entity-types": "Pasirinkti objektų tipus", + "alarm-type-required": "Signalo tipas yra privalomas.", + "alarm-severity": "Signalo svarba", + "alarm-severity-required": "Signalo svarba yra privaloma", + "alarm-severity-pattern": "Signalo svarbos šablonas", + "alarm-status-filter": "Signalo būsenos filtras", + "alarm-status-list-empty": "Signalo būsenų sąrašas tuščias", + "no-alarm-status-matching": "Nerasta atitinkančių signalo būsenų.", + "propagate": "Perduoti signalą susijusiems objektams", + "propagate-to-owner": "Perduoti signalą objekto savininkui (klientui arba nuomininkui)", + "propagate-to-tenant": "Perduoti signalą nuomininkui", + "condition": "Sąlyga", + "details": "Išsami informacija", + "to-string": "Į eilutę (string)", + "test-to-string-function": "Testuoti konvertavimo į eilutę funkciją", + "from-template": "Iš", + "from-template-required": "Laukas „Iš“ yra privalomas", + "message-to-metadata": "Pranešimą į metaduomenis", + "metadata-to-message": "Metaduomenis į pranešimą", + "from-message": "Iš pranešimo", + "from-metadata": "Iš metaduomenų", + "to-template": "Į", + "to-template-required": "Laukas „Į šabloną“ yra privalomas", + "mail-address-list-template-hint": "Adreso sąrašas atskiriamas kableliais. Naudokite ${metadataKey} reikšmėms iš metaduomenų ir $[messageKey] reikšmėms iš pranešimo turinio gauti.", + "cc-template": "Kopija (Cc)", + "bcc-template": "Slapta kopija (Bcc)", + "subject-template": "Tema", + "subject-template-required": "Temos šablonas yra privalomas", + "body-template": "Turinys", + "body-template-required": "Turinio šablonas yra privalomas", + "dynamic-mail-body-type": "Dinaminio el. laiško turinio tipas", + "mail-body-type": "El. laiško turinio tipas", + "body-type-template": "Turinio tipo šablonas", + "reply-routing-configuration": "Atsakymų nukreipimo konfigūracija", + "rpc-reply-routing-configuration-hint": "Šie konfigūracijos parametrai nurodo metaduomenų raktų pavadinimus, naudojamus paslaugos, sesijos ir užklausos identifikavimui siunčiant atsakymą atgal.", + "reply-routing-configuration-hint": "Šie konfigūracijos parametrai nurodo metaduomenų raktų pavadinimus, naudojamus paslaugos ir užklausos identifikavimui siunčiant atsakymą atgal.", + "request-id-metadata-attribute": "Užklausos ID", + "service-id-metadata-attribute": "Paslaugos ID", + "session-id-metadata-attribute": "Sesijos ID", + "timeout-sec": "Laiko limitas (sekundėmis)", + "timeout-required": "Laiko limitas yra privalomas", + "min-timeout-message": "Leidžiama tik minimali 0 laiko limito reikšmė.", + "endpoint-url-pattern": "Galutinio taško (endpoint) URL šablonas", + "endpoint-url-pattern-required": "Galutinio taško (endpoint) URL šablonas yra privalomas", + "request-method": "Užklausos metodas", + "use-simple-client-http-factory": "Naudoti paprastą HTTP kliento kūrimo būdą", + "ignore-request-body": "Be užklausos turinio", + "parse-to-plain-text": "Konvertuoti į gryną tekstą", + "parse-to-plain-text-hint": "Jei pasirinkta, užklausos turinys bus konvertuotas iš JSON eilutės į paprastą tekstą, pvz., msg = \"Hello,\\t\"world\"\" bus konvertuota į Hello, \"world\"", + "read-timeout": "Skaitymo laiko limitas (milisekundėmis)", + "read-timeout-hint": "Reikšmė 0 reiškia neribotą laiko limitą", + "max-parallel-requests-count": "Maksimalus lygiagrečių užklausų skaičius", + "max-parallel-requests-count-hint": "Reikšmė 0 nurodo, kad lygiagretus apdorojimas nėra ribojamas", + "max-response-size": "Maksimalus atsako dydis (KB)", + "max-response-size-hint": "Didžiausias atminties kiekis, skirtas duomenų buferiavimui dekoduojant ar koduojant HTTP pranešimus, pvz., JSON ar XML turinius", + "headers": "Antraštės (Headers)", + "headers-hint": "Naudokite ${metadataKey} reikšmėms iš metaduomenų ir $[messageKey] reikšmėms iš pranešimo turinio antraščių/vertės laukuose", + "header": "Antraštė", + "header-required": "Antraštė yra privaloma", + "value": "Reikšmė", + "value-required": "Reikšmė yra privaloma", + "topic-pattern": "Temos šablonas", + "key-pattern": "Rakto šablonas", + "key-pattern-hint": "Nebūtina. Jei nurodytas galiojantis skaidinio (partition) numeris, jis bus naudojamas siunčiant įrašą. Jei skaidinys nenurodytas, bus naudojamas raktas. Jei abu nėra nurodyti – bus priskiriama apvaliuoju būdu (round-robin).", + "topic-pattern-required": "Temos šablonas yra privalomas", + "topic": "Tema", + "topic-required": "Tema yra privaloma", + "bootstrap-servers": "Bootstrap serveriai", + "bootstrap-servers-required": "Bootstrap serverių reikšmė yra privaloma", + "other-properties": "Kitos savybės", + "key": "Raktas", + "key-required": "Raktas yra privalomas", + "retries": "Automatinio bandymo kartai, jei nepavyksta", + "min-retries-message": "Leidžiama tik minimali 0 pakartojimų reikšmė.", + "batch-size-bytes": "Siunčiamo paketo dydis baitais", + "min-batch-size-bytes-message": "Leidžiama tik minimali 0 paketo dydžio reikšmė.", + "linger-ms": "Buferiavimo laikas vietoje (ms)", + "min-linger-ms-message": "Leidžiama tik minimali 0 ms reikšmė.", + "buffer-memory-bytes": "Maksimalus kliento buferio dydis baitais", + "min-buffer-memory-message": "Leidžiama tik minimali 0 buferio dydžio reikšmė.", + "memory-buffer-size-range": "Atminties buferio dydis turi būti nuo 0 iki {{max}} KB", + "acks": "Patvirtinimų skaičius (ACKs)", + "topic-arn-pattern": "Temos ARN šablonas", + "topic-arn-pattern-required": "Temos ARN šablonas yra privalomas", + "aws-access-key-id": "AWS prieigos rakto ID (Access Key ID)", + "aws-access-key-id-required": "AWS prieigos rakto ID yra privalomas", + "aws-secret-access-key": "AWS slaptasis prieigos raktas (Secret Access Key)", + "aws-secret-access-key-required": "AWS slaptasis prieigos raktas yra privalomas", + "aws-region": "AWS regionas", + "aws-region-required": "AWS regionas yra privalomas", + "exchange-name-pattern": "Mainų (Exchange) pavadinimo šablonas", + "routing-key-pattern": "Maršruto rakto (Routing key) šablonas", + "message-properties": "Pranešimo savybės", + "host": "Serveris (Host)", + "host-required": "Serverio adresas yra privalomas", + "port": "Prievadas (Port)", + "port-required": "Prievadas yra privalomas", + "port-range": "Prievadas turi būti nuo 1 iki 65535.", + "virtual-host": "Virtualus serveris (Virtual host)", + "username": "Vartotojo vardas", + "password": "Slaptažodis", + "automatic-recovery": "Automatinis atkūrimas", + "connection-timeout-ms": "Prisijungimo laiko limitas (ms)", + "min-connection-timeout-ms-message": "Leidžiama tik minimali 0 ms reikšmė.", + "handshake-timeout-ms": "Rankos paspaudimo (Handshake) laiko limitas (ms)", + "min-handshake-timeout-ms-message": "Leidžiama tik minimali 0 ms reikšmė.", + "client-properties": "Kliento savybės", + "queue-url-pattern": "Eilės (Queue) URL šablonas", + "queue-url-pattern-required": "Eilės URL šablonas yra privalomas", + "delay-seconds": "Vėlinimas (sekundėmis)", + "min-delay-seconds-message": "Leidžiama tik minimali 0 sekundžių reikšmė.", + "max-delay-seconds-message": "Leidžiama tik maksimali 900 sekundžių reikšmė.", + "name": "Pavadinimas", + "name-required": "Pavadinimas yra privalomas", + "queue-type": "Eilės tipas", + "sqs-queue-standard": "Standartinė", + "sqs-queue-fifo": "FIFO (First-In-First-Out)", + "gcp-project-id": "GCP projekto ID", + "gcp-project-id-required": "GCP projekto ID yra privalomas", + "gcp-service-account-key": "GCP paslaugos paskyros raktas (key file)", + "gcp-service-account-key-required": "GCP paslaugos paskyros raktas yra privalomas", + "pubsub-topic-name": "Temos pavadinimas", + "pubsub-topic-name-required": "Temos pavadinimas yra privalomas", + "message-attributes": "Pranešimo atributai", + "message-attributes-hint": "Naudokite ${metadataKey} reikšmėms iš metaduomenų ir $[messageKey] reikšmėms iš pranešimo turinio nurodyti vardų/vertės laukuose", + "connect-timeout": "Prisijungimo laiko limitas (sekundėmis)", + "connect-timeout-required": "Prisijungimo laiko limitas yra privalomas.", + "connect-timeout-range": "Prisijungimo laiko limitas turi būti nuo 1 iki 200.", + "client-id": "Kliento ID", + "client-id-hint": "Nebūtina. Palikite tuščią, jei ID generuojamas automatiškai. Nurodydami konkretų Client ID būkite atsargūs — dauguma MQTT brokerių neleidžia kelių jungčių su tuo pačiu ID. Jei platforma veikia mikroservisų režimu, kiekvienas servisas paleidžia atskirą šio mazgo (rule node) egzempliorių, todėl dubliuoti ID gali sukelti klaidų. Kad to išvengtumėte, įjunkite „Pridėti paslaugos ID kaip priesagą prie kliento ID“.", + "append-client-id-suffix": "Pridėti paslaugos ID kaip priesagą prie kliento ID", + "client-id-suffix-hint": "Nebūtina. Naudojama, kai „Client ID“ nurodytas rankiniu būdu. Pridėjus paslaugos ID kaip priesagą, išvengiama klaidų mikroservisų architektūroje.", + "device-id": "Įrenginio ID", + "device-id-required": "Įrenginio ID yra privalomas.", + "clean-session": "Švari sesija (Clean session)", + "enable-ssl": "Įjungti SSL", + "credentials": "Autentifikacijos duomenys", + "credentials-type": "Autentifikacijos tipas", + "credentials-type-required": "Autentifikacijos tipas yra privalomas.", + "credentials-anonymous": "Anoniminis", + "credentials-basic": "Paprastas (Basic)", + "credentials-pem": "PEM sertifikatai", + "credentials-pem-hint": "Reikalingas bent Serverio CA sertifikatas arba Kliento sertifikato ir privačiojo rakto pora", + "credentials-sas": "Bendro prieigos parašo (Shared Access Signature)", + "sas-key": "SAS raktas", + "sas-key-required": "SAS raktas yra privalomas.", + "hostname": "Serverio pavadinimas (Hostname)", + "hostname-required": "Serverio pavadinimas yra privalomas.", + "azure-ca-cert": "CA sertifikato failas", + "username-required": "Vartotojo vardas yra privalomas.", + "password-required": "Slaptažodis yra privalomas.", + "ca-cert": "Serverio CA sertifikato failas", + "private-key": "Kliento privataus rakto failas", + "cert": "Kliento sertifikato failas", + "no-file": "Failas nepasirinktas.", + "drop-file": "Nutempkite failą arba spustelėkite, kad pasirinktumėte failą įkėlimui.", + "private-key-password": "Privataus rakto slaptažodis", + "use-system-smtp-settings": "Naudoti sistemos SMTP nustatymus", + "use-metadata-dynamic-interval": "Naudoti dinaminį intervalą", + "metadata-dynamic-interval-hint": "Intervalo pradžios ir pabaigos laukai palaiko šablonizaciją (templatization). Pakeistos reikšmės turi būti milisekundėmis. Naudokite $[messageKey] reikšmei iš pranešimo ir ${metadataKey} reikšmei iš metaduomenų.", + "use-metadata-interval-patterns-hint": "Jei pasirinkta, mazgas (rule node) naudos pradžios ir pabaigos intervalo šablonus iš pranešimo duomenų arba metaduomenų, manydamas, kad intervalai yra milisekundėmis.", + "use-message-alarm-data": "Naudoti pranešimo pavojaus (alarm) duomenis", + "overwrite-alarm-details": "Perrašyti pavojaus detales", + "use-alarm-severity-pattern": "Naudoti pavojaus svarbos (severity) šabloną", + "check-all-keys": "Patikrinti, ar visi nurodyti laukai egzistuoja", + "check-all-keys-hint": "Jei pasirinkta, tikrina, ar visi nurodyti raktai yra pranešimo duomenyse ir metaduomenyse.", + "check-relation-to-specific-entity": "Tikrinti ryšį su konkrečiu objektu", + "check-relation-to-specific-entity-tooltip": "Jei įjungta, tikrinamas ryšio buvimas su konkrečiu objektu, kitu atveju – su bet kuriuo objektu. Abiem atvejais ryšys nustatomas pagal kryptį ir tipą.", + "check-relation-hint": "Tikrina ryšio buvimą su konkrečiu ar bet kuriuo objektu, atsižvelgiant į kryptį ir ryšio tipą.", + "delete-relation-with-specific-entity": "Pašalinti ryšį su konkrečiu objektu", + "delete-relation-with-specific-entity-hint": "Jei įjungta, bus pašalintas tik ryšys su vienu konkrečiu objektu. Kitu atveju, ryšys bus pašalintas su visais atitinkančiais objektais.", + "delete-relation-hint": "Pašalina ryšį tarp pranešimo kilmės (originator) ir nurodyto objekto (-ų), remiantis kryptimi ir tipu.", + "remove-current-relations": "Pašalinti esamus ryšius", + "remove-current-relations-hint": "Pašalina esamus ryšius iš pranešimo kilmės (originator) pagal kryptį ir tipą.", + "change-originator-to-related-entity": "Pakeisti pranešimo kilmę į susijusį objektą", + "change-originator-to-related-entity-hint": "Naudojama, kad pranešimas būtų apdorojamas kaip gautas iš kito objekto.", + "start-interval": "Intervalo pradžia", + "end-interval": "Intervalo pabaiga", + "start-interval-required": "Intervalo pradžia yra privaloma.", + "end-interval-required": "Intervalo pabaiga yra privaloma.", + "smtp-protocol": "Protokolas", + "smtp-host": "SMTP serveris", + "smtp-host-required": "SMTP serveris yra privalomas.", + "smtp-port": "SMTP prievadas", + "smtp-port-required": "Reikia nurodyti SMTP prievadą.", + "smtp-port-range": "SMTP prievadas turi būti nuo 1 iki 65535.", + "timeout-msec": "Laiko limitas (ms)", + "min-timeout-msec-message": "Leidžiama tik minimali 0 ms reikšmė.", + "enter-username": "Įveskite vartotojo vardą", + "enter-password": "Įveskite slaptažodį", + "enable-tls": "Įjungti TLS", + "tls-version": "TLS versija", + "enable-proxy": "Įjungti tarpiklį (Proxy)", + "use-system-proxy-properties": "Naudoti sistemos tarpiklio nustatymus", + "proxy-host": "Tarpiklio serveris (Proxy host)", + "proxy-host-required": "Tarpiklio serveris yra privalomas.", + "proxy-port": "Tarpiklio prievadas (Proxy port)", + "proxy-port-required": "Tarpiklio prievadas yra privalomas.", + "proxy-port-range": "Tarpiklio prievadas turi būti nuo 1 iki 65535.", + "proxy-user": "Tarpiklio vartotojas", + "proxy-password": "Tarpiklio slaptažodis", + "proxy-scheme": "Tarpiklio schema (Proxy scheme)", + "numbers-to-template": "Telefono numerių šablonas", + "numbers-to-template-required": "Telefono numerių šablonas yra privalomas", + "numbers-to-template-hint": "Telefono numerius atskirkite kableliais. Naudokite ${metadataKey} reikšmėms iš metaduomenų ir $[messageKey] reikšmėms iš pranešimo turinio.", + "sms-message-template": "SMS žinutės šablonas", + "sms-message-template-required": "SMS žinutės šablonas yra privalomas", + "use-system-sms-settings": "Naudoti sistemos SMS paslaugų teikėjo nustatymus", + "min-period-0-seconds-message": "Leidžiamas tik minimalus 0 sekundžių periodas.", + "max-pending-messages": "Maksimalus laukiančių pranešimų skaičius", + "max-pending-messages-required": "Maksimalus laukiančių pranešimų skaičius yra privalomas.", + "max-pending-messages-range": "Maksimalus laukiančių pranešimų skaičius turi būti nuo 1 iki 100000.", + "originator-types-filter": "Siuntėjų (originatorių) tipų filtras", + "interval-seconds": "Intervalas sekundėmis", + "interval-seconds-required": "Intervalas yra privalomas.", + "int-range": "Reikšmė neturi viršyti maksimalios sveiko skaičiaus ribos (2147483648)", + "min-interval-seconds-message": "Leidžiamas tik minimalus 1 sekundės intervalas.", + "output-timeseries-key-prefix": "Išvesties laiko eilučių rakto priešdėlis", + "output-timeseries-key-prefix-required": "Išvesties laiko eilučių rakto priešdėlis yra privalomas.", + "separator-hint": "Paspauskite „Enter“, kad užbaigtumėte lauko įvedimą.", + "select-details": "Pasirinkti detales", + "entity-details-id": "Id", + "entity-details-title": "Pavadinimas", + "entity-details-country": "Šalis", + "entity-details-state": "Valstija / regionas", + "entity-details-city": "Miestas", + "entity-details-zip": "Pašto kodas", + "entity-details-address": "Adresas", + "entity-details-address2": "Adresas 2", + "entity-details-additional_info": "Papildoma informacija", + "entity-details-phone": "Telefonas", + "entity-details-email": "El. paštas", + "email-sender": "El. pašto siuntėjas", + "fields-to-check": "Laukai, kuriuos reikia patikrinti", + "add-detail": "Pridėti detalę", + "check-all-keys-tooltip": "Jei įjungta, tikrina, ar visi pranešime ir metaduomenyse nurodyti laukai yra pranešime.", + "fields-to-check-hint": "Paspauskite „Enter“, kad užbaigtumėte lauko įvedimą. Palaikomi keli laukų pavadinimai.", + "entity-details-list-empty": "Reikia pasirinkti bent vieną detalę.", + "alarm-status": "Pavojaus būsena", + "alarm-required": "Turi būti pasirinkta bent viena pavojaus būsena.", + "no-entity-details-matching": "Nerasta atitinkančių objektų detalių.", + "custom-table-name": "Individualios lentelės pavadinimas", + "custom-table-name-required": "Lentelės pavadinimas yra privalomas", + "custom-table-hint": "Lentelė turi būti sukurta jūsų Cassandra klasteryje, jos pavadinimas turi prasidėti prefiksu „cs_tb_“, kad duomenys nebūtų įrašyti į bendras TB lenteles. Įveskite lentelės pavadinimą be „cs_tb_“ prefikso.", + "message-field": "Pranešimo laukas", + "message-field-required": "Pranešimo laukas yra privalomas.", + "table-col": "Lentelės stulpelis", + "table-col-required": "Lentelės stulpelis yra privalomas.", + "latitude-field-name": "Platumos lauko pavadinimas", + "longitude-field-name": "Ilgumos lauko pavadinimas", + "latitude-field-name-required": "Platumos lauko pavadinimas yra privalomas.", + "longitude-field-name-required": "Ilgumos lauko pavadinimas yra privalomas.", + "fetch-perimeter-info-from-metadata": "Gauti perimetro informaciją iš metaduomenų", + "fetch-perimeter-info-from-metadata-tooltip": "Jei perimetro tipas nustatytas kaip „Poligonas“, metaduomenų lauko '{{perimeterKeyName}}' reikšmė bus naudojama kaip perimetro apibrėžimas be papildomo reikšmės apdorojimo. Priešingu atveju, jei perimetro tipas nustatytas kaip „Apskritimas“, '{{perimeterKeyName}}' metaduomenų lauko reikšmė bus išanalizuota, kad būtų išgauti 'latitude', 'longitude', 'radius', 'radiusUnit' laukai, naudojami apskritimo perimetro apibrėžimui.", + "perimeter-key-name": "Perimetro rakto pavadinimas", + "perimeter-key-name-hint": "Metaduomenų lauko pavadinimas, kuriame yra perimetro informacija.", + "perimeter-key-name-required": "Perimetro rakto pavadinimas yra privalomas.", + "perimeter-circle": "Apskritimas", + "perimeter-polygon": "Poligonas", + "perimeter-type": "Perimetro tipas", + "circle-center-latitude": "Centro platuma", + "circle-center-latitude-required": "Centro platuma yra privaloma.", + "circle-center-longitude": "Centro ilguma", + "circle-center-longitude-required": "Centro ilguma yra privaloma.", + "range-unit-meter": "Metrai", + "range-unit-kilometer": "Kilometrai", + "range-unit-foot": "Pėdos", + "range-unit-mile": "Mylios", + "range-unit-nautical-mile": "Jūrmylės", + "range-units": "Atstumo vienetai", + "range-units-required": "Atstumo vienetai yra privalomi.", + "range": "Atstumas", + "range-required": "Atstumas yra privalomas.", + "polygon-definition": "Poligono apibrėžimas", + "polygon-definition-required": "Poligono apibrėžimas yra privalomas.", + "polygon-definition-hint": "Naudokite šį formatą rankiniam poligono apibrėžimui: [[lat1,lon1],[lat2,lon2], ... ,[latN,lonN]].", + "min-inside-duration": "Minimalus buvimo viduje laikas", + "min-inside-duration-value-required": "Minimalus buvimo viduje laikas yra privalomas", + "min-inside-duration-time-unit": "Minimalus buvimo viduje laiko vienetas", + "min-outside-duration": "Minimalus buvimo išorėje laikas", + "min-outside-duration-value-required": "Minimalus buvimo išorėje laikas yra privalomas", + "min-outside-duration-time-unit": "Minimalus buvimo išorėje laiko vienetas", + "tell-failure-if-absent": "Pranešti apie klaidą", + "tell-failure-if-absent-hint": "Jei bent vienas pasirinktas raktas neegzistuoja, siunčiama žinutė praneš apie „Klaida“.", + "get-latest-value-with-ts": "Gauti naujausias telemetrijos reikšmes su laiko žyma", + "get-latest-value-with-ts-hint": "Jei pasirinkta, naujausios telemetrijos reikšmės taip pat turės laiko žymą, pvz.: \"temp\": \"{\"ts\":1574329385897, \"value\":42}\"", + "ignore-null-strings": "Ignoruoti tuščias eilutes", + "ignore-null-strings-hint": "Jei pasirinkta, taisyklės mazgas ignoruos objektų laukus su tuščia reikšme.", + "add-metadata-key-values-as-kafka-headers": "Pridėti metaduomenų raktų-reikšmių poras į Kafka antraštes", + "add-metadata-key-values-as-kafka-headers-hint": "Jei pasirinkta, raktų-reikšmių poros iš metaduomenų bus pridėtos prie siunčiamų įrašų antraščių kaip baitų masyvai su iš anksto nustatytu kodavimu.", + "charset-encoding": "Koduotė", + "charset-encoding-required": "Koduotė yra privaloma.", + "charset-us-ascii": "US-ASCII", + "charset-iso-8859-1": "ISO-8859-1", + "charset-utf-8": "UTF-8", + "charset-utf-16be": "UTF-16BE", + "charset-utf-16le": "UTF-16LE", + "charset-utf-16": "UTF-16", + "select-queue-hint": "Eilės pavadinimą galima pasirinkti iš sąrašo arba įvesti rankiniu būdu.", + "device-profile-node-hint": "Naudinga, jei turite trukmės ar pasikartojančias sąlygas, kad būtų užtikrintas aliarmo būsenos vertinimo tęstinumas.", + "persist-alarm-rules": "Išsaugoti aliarmo taisyklių būseną", + "persist-alarm-rules-hint": "Jei įjungta, taisyklių mazgas išsaugos apdorojimo būseną duomenų bazėje.", + "fetch-alarm-rules": "Atkurti aliarmo taisyklių būseną", + "fetch-alarm-rules-hint": "Jei įjungta, taisyklių mazgas atkūrimo metu atkurs apdorojimo būseną ir užtikrins, kad aliarmai būtų aktyvuojami net po serverio perkrovimo. Priešingu atveju būsena bus atkurta, kai atvyks pirmoji žinutė iš įrenginio.", + "input-value-key": "Įvesties reikšmės raktas", + "input-value-key-required": "Įvesties reikšmės raktas yra privalomas.", + "output-value-key": "Išvesties reikšmės raktas", + "output-value-key-required": "Išvesties reikšmės raktas yra privalomas.", + "number-of-digits-after-floating-point": "Skaičių kiekis po kablelio", + "number-of-digits-after-floating-point-range": "Skaičių kiekis po kablelio turi būti nuo 0 iki 15.", + "failure-if-delta-negative": "Pranešti apie klaidą, jei delta yra neigiama", + "failure-if-delta-negative-tooltip": "Taisyklių mazgas privers pranešimą žymėti kaip klaidą, jei delta reikšmė neigiama.", + "use-caching": "Naudoti kešavimą", + "use-caching-tooltip": "Taisyklių mazgas kaups \"{{inputValueKey}}\" reikšmę iš gaunamos žinutės, kad pagerintų našumą. Atkreipkite dėmesį, kad kešas nebus atnaujintas, jei reikšmė bus pakeista kitur.", + "add-time-difference-between-readings": "Pridėti laiko skirtumą tarp \"{{inputValueKey}}\" nuskaitymų", + "add-time-difference-between-readings-tooltip": "Jei įjungta, taisyklių mazgas pridės \"{{periodValueKey}}\" prie siunčiamos žinutės.", + "period-value-key": "Laikotarpio reikšmės raktas", + "period-value-key-required": "Laikotarpio reikšmės raktas yra privalomas.", + "general-pattern-hint": "Naudokite ${metadataKey} reikšmei iš metaduomenų, $[messageKey] reikšmei iš pranešimo kūno.", + "alarm-severity-pattern-hint": "Naudokite ${metadataKey} reikšmei iš metaduomenų, $[messageKey] reikšmei iš pranešimo kūno. Aliarmo rimtumas turi būti sistemos (CRITICAL, MAJOR ir t. t.)", + "output-node-name-hint": "Taisyklių mazgo pavadinimas atitinka pranešimo ryšio tipą ir naudojamas pranešimams perduoti kitiems taisyklių mazgams toje pačioje taisyklių grandinėje.", + "use-server-ts": "Naudoti serverio laiko žymą", + "use-server-ts-hint": "Naudoti serverio dabartinę laiko žymą laiko eilučių duomenims, kurie neturi aiškios laiko žymos. Tai padeda išlaikyti teisingą eiliškumą apdorojant žinutes iš kelių šaltinių arba kai jos atvyksta ne iš eilės.", + "kv-map-pattern-hint": "Visi įvesties laukai palaiko šablonizaciją. Naudokite $[messageKey], kad gautumėte reikšmę iš pranešimo, ir ${metadataKey}, kad gautumėte reikšmę iš metaduomenų.", + "kv-map-single-pattern-hint": "Įvesties laukas palaiko šablonizaciją. Naudokite $[messageKey], kad gautumėte reikšmę iš pranešimo, ir ${metadataKey}, kad gautumėte reikšmę iš metaduomenų.", + "shared-scope": "Bendras taikymo sritis", + "server-scope": "Serverio taikymo sritis", + "client-scope": "Kliento taikymo sritis", + "attribute-type": "Atributas", + "attribute-type-description": "Gauti atributo reikšmę iš duomenų bazės", + "attribute-type-result-description": "Išsaugoti rezultatą kaip subjekto atributą duomenų bazėje", + "constant-type": "Konstanta", + "constant-type-description": "Nustatyti pastovią reikšmę", + "time-series-type": "Laiko eilutė", + "time-series-type-description": "Gauti naujausią laiko eilučių reikšmę iš duomenų bazės", + "time-series-type-result-description": "Išsaugoti rezultatą kaip subjekto laiko eilutę duomenų bazėje", + "message-body-type": "Pranešimas", + "message-body-type-description": "Gauti argumento reikšmę iš gaunamo pranešimo", + "message-body-type-result-description": "Pridėti rezultatą prie siunčiamo pranešimo", + "message-metadata-type": "Metaduomenys", + "message-metadata-type-description": "Gauti argumento reikšmę iš gaunamo pranešimo metaduomenų", + "message-metadata-result-description": "Pridėti rezultatą prie siunčiamo pranešimo metaduomenų", + "argument-tile": "Argumentai", + "no-arguments-prompt": "Argumentai nesukonfigūruoti", + "result-title": "Rezultatas", + "functions-field-input": "Funkcijos", + "no-option-found": "Parinkčių nerasta", + "argument-source-field-input": "Šaltinis", + "argument-source-field-input-required": "Argumento šaltinis yra privalomas.", + "argument-key-field-input": "Raktas", + "argument-key-field-input-required": "Argumento raktas yra privalomas.", + "constant-value-field-input": "Pastovioji reikšmė", + "constant-value-field-input-required": "Pastovioji reikšmė yra privaloma.", + "attribute-scope-field-input": "Atributo taikymo sritis", + "attribute-scope-field-input-required": "Atributo taikymo sritis yra privaloma.", + "default-value-field-input": "Numatytoji reikšmė", + "type-field-input": "Tipas", + "type-field-input-required": "Tipas yra privalomas.", + "key-field-input": "Raktas", + "add-entity-type": "Pridėti subjekto tipą", + "add-device-profile": "Pridėti įrenginio profilį", + "key-field-input-required": "Raktas yra privalomas.", + "number-floating-point-field-input": "Skaičių kiekis po kablelio", + "number-floating-point-field-input-hint": "Naudokite 0, kad konvertuotumėte rezultatą į sveiką skaičių", + "add-to-message-field-input": "Pridėti prie pranešimo", + "add-to-metadata-field-input": "Pridėti prie metaduomenų", + "custom-expression-field-input": "Matematinė išraiška", + "custom-expression-field-input-required": "Matematinė išraiška yra privaloma", + "custom-expression-field-input-hint": "Nurodykite matematinę išraišką įvertinimui. Numatytoji išraiška parodo, kaip konvertuoti Fahrenheit į Celsijų", + "retained-message": "Išsaugotas", + "attributes-mapping": "Atributų susiejimas", + "latest-telemetry-mapping": "Naujausios telemetrijos susiejimas", + "add-mapped-attribute-to": "Pridėti susietus atributus į", + "add-mapped-latest-telemetry-to": "Pridėti susietą naujausią telemetriją į", + "add-mapped-fields-to": "Pridėti susietus laukus į", + "add-selected-details-to": "Pridėti pasirinktą informaciją į", + "clear-selected-types": "Išvalyti pasirinktus tipus", + "clear-selected-details": "Išvalyti pasirinktą informaciją", + "clear-selected-fields": "Išvalyti pasirinktus laukus", + "clear-selected-keys": "Išvalyti pasirinktus raktus", + "geofence-configuration": "Geozonos konfigūracija", + "coordinate-field-names": "Koordinačių laukų pavadinimai", + "coordinate-field-hint": "Taisyklių mazgas bando gauti nurodytus laukus iš pranešimo. Jei jų nėra, ieškos metaduomenyse.", + "presence-monitoring-strategy": "Buvimo stebėjimo strategija", + "presence-monitoring-strategy-on-first-message": "Pirmoje žinutėje", + "presence-monitoring-strategy-on-each-message": "Kiekvienoje žinutėje", + "presence-monitoring-strategy-on-first-message-hint": "Praneša buvimo būseną „Viduje“ arba „Išorėje“ pirmoje žinutėje, kai nuo ankstesnio būsenos „Įėjo“ arba „Išėjo“ atnaujinimo praėjo nustatytas minimalus laikas.", + "presence-monitoring-strategy-on-each-message-hint": "Praneša buvimo būseną „Viduje“ arba „Išorėje“ kiekvienoje žinutėje po būsenos „Įėjo“ arba „Išėjo“ atnaujinimo.", + "fetch-credentials-to": "Gauti kredencialus į", + "add-originator-attributes-to": "Pridėti siuntėjo atributus į", + "originator-attributes": "Siuntėjo atributai", + "fetch-latest-telemetry-with-timestamp": "Gauti naujausią telemetriją su laiko žyma", + "fetch-latest-telemetry-with-timestamp-tooltip": "Jei pasirinkta, naujausios telemetrijos reikšmės bus pridėtos prie siunčiamų metaduomenų su laiko žyma, pvz.: \"{{latestTsKeyName}}\": \"{\"ts\":1574329385897, \"value\":42}\"", + "tell-failure": "Pranešti apie klaidą, jei trūksta bent vieno atributo", + "tell-failure-tooltip": "Jei bent vienas pasirinktas raktas neegzistuoja, siunčiama žinutė praneš apie „Klaida“.", + "created-time": "Sukūrimo laikas", + "chip-help": "Paspauskite 'Enter', kad užbaigtumėte {{inputName}} įvedimą.\nPaspauskite 'Backspace', kad ištrintumėte {{inputName}}.\nPalaikomos kelios reikšmės.", + "detail": "informacija", + "field-name": "lauko pavadinimas", + "device-profile": "įrenginio profilis", + "entity-type": "subjekto tipas", + "message-type": "pranešimo tipas", + "timeseries-key": "laiko eilučių raktas", + "type": "Tipas", + "first-name": "Vardas", + "last-name": "Pavardė", + "label": "Etiketė", + "originator-fields-mapping": "Siuntėjo laukų susiejimas", + "add-mapped-originator-fields-to": "Pridėti susietus siuntėjo laukus į", + "fields": "Laukai", + "skip-empty-fields": "Praleisti tuščius laukus", + "skip-empty-fields-tooltip": "Laukai su tuščiomis reikšmėmis nebus pridėti prie išvesties pranešimo ar metaduomenų.", + "fetch-interval": "Gavimo intervalas", + "fetch-strategy": "Gavimo strategija", + "fetch-timeseries-from-to": "Gauti laiko eilučių duomenis nuo {{startInterval}} {{startIntervalTimeUnit}} iki {{endInterval}} {{endIntervalTimeUnit}} atgal.", + "fetch-timeseries-from-to-invalid": "Laiko eilučių gavimo klaida („Intervalo pradžia“ turi būti mažesnė už „Intervalo pabaigą“).", + "use-metadata-dynamic-interval-tooltip": "Jei pasirinkta, taisyklių mazgas naudos dinaminį intervalo pradžios ir pabaigos laiką pagal pranešimo bei metaduomenų šablonus.", + "all-mode-hint": "Jei pasirinktas gavimo režimas „Visi“, taisyklių mazgas gaus telemetriją iš intervalo pagal konfigūruojamus užklausos parametrus.", + "first-mode-hint": "Jei pasirinktas režimas „Pirmas“, taisyklių mazgas gaus artimiausią telemetriją intervalo pradžiai.", + "last-mode-hint": "Jei pasirinktas režimas „Paskutinis“, taisyklių mazgas gaus artimiausią telemetriją intervalo pabaigai.", + "ascending": "Didėjančia tvarka", + "descending": "Mažėjančia tvarka", + "min": "Min.", + "max": "Maks.", + "average": "Vidurkis", + "sum": "Suma", + "count": "Kiekis", + "none": "Nėra", + "last-level-relation-tooltip": "Jei pasirinkta, taisyklių mazgas ieškos susijusių subjektų tik maksimaliame nustatytame ryšio lygyje.", + "last-level-device-relation-tooltip": "Jei pasirinkta, taisyklių mazgas ieškos susijusių įrenginių tik maksimaliame nustatytame ryšio lygyje.", + "data-to-fetch": "Duomenys, kuriuos reikia gauti", + "mapping-of-customers": "Klientų susiejimas", + "map-fields-required": "Visi susiejimo laukai yra privalomi.", + "attributes": "Atributai", + "related-device-attributes": "Susijusio įrenginio atributai", + "add-selected-attributes-to": "Pridėti pasirinktus atributus į", + "device-profiles": "Įrenginių profiliai", + "mapping-of-tenant": "Nuomininko susiejimas", + "add-attribute-key": "Pridėti atributo raktą", + "message-template": "Pranešimo šablonas", + "message-template-required": "Pranešimo šablonas yra privalomas", + "use-system-slack-settings": "Naudoti sistemos Slack nustatymus", + "slack-api-token": "Slack API raktas", + "slack-api-token-required": "Slack API raktas yra privalomas", + "keys-mapping": "Raktų susiejimas", + "add-key": "Pridėti raktą", + "recipients": "Gavėjai", + "message-subject-and-content": "Pranešimo tema ir turinys", + "template-rules-hint": "Abu įvesties laukai palaiko šablonizaciją. Naudokite $[messageKey] reikšmei iš pranešimo ir ${metadataKey} reikšmei iš metaduomenų.", + "originator-customer-desc": "Naudoti gaunamo pranešimo siuntėjo klientą kaip naują siuntėją.", + "originator-tenant-desc": "Naudoti dabartinį nuomininką kaip naują siuntėją.", + "originator-related-entity-desc": "Naudoti susijusį subjektą kaip naują siuntėją. Paieška atliekama pagal konfigūruotą ryšio tipą ir kryptį.", + "originator-alarm-originator-desc": "Naudoti aliarmo siuntėją kaip naują siuntėją. Tik jei gaunamo pranešimo siuntėjas yra aliarmo subjektas.", + "originator-entity-by-name-pattern-desc": "Naudoti iš duomenų bazės gautą subjektą kaip naują siuntėją. Paieška atliekama pagal subjekto tipą ir nurodytą pavadinimo šabloną.", + "email-from-template-hint": "Naudokite $[messageKey] reikšmei iš pranešimo ir ${metadataKey} reikšmei iš metaduomenų.", + "recipients-block-main-hint": "Adresų sąrašas, atskirtas kableliais. Visi įvesties laukai palaiko šablonizaciją. Naudokite $[messageKey] reikšmei iš pranešimo ir ${metadataKey} reikšmei iš metaduomenų.", + "forward-msg-default-rule-chain": "Persiųsti pranešimą į siuntėjo numatytą taisyklių grandinę", + "forward-msg-default-rule-chain-tooltip": "Jei įjungta, pranešimas bus persiųstas į siuntėjo numatytą taisyklių grandinę arba į konfigūracijoje nurodytą grandinę, jei siuntėjas neturi numatytos grandinės subjekto profilyje.", + "exclude-zero-deltas": "Pašalinti nulinės delta reikšmes iš siunčiamo pranešimo", + "exclude-zero-deltas-hint": "Jei įjungta, \"{{outputValueKey}}\" bus pridėtas prie siunčiamo pranešimo tik jei reikšmė nėra lygi nuliui.", + "exclude-zero-deltas-time-difference-hint": "Jei įjungta, \"{{outputValueKey}}\" ir \"{{periodValueKey}}\" bus pridėti prie siunčiamo pranešimo tik jei \"{{outputValueKey}}\" reikšmė nėra nulinė.", + "search-direction-from": "Nuo siuntėjo iki tikslo", + "search-direction-to": "Nuo tikslo iki siuntėjo", + "del-relation-direction-from": "Nuo siuntėjo", + "del-relation-direction-to": "Į siuntėją", + "target-entity": "Tikslinis subjektas", + "function-configuration": "Funkcijos konfigūracija", + "function-name": "Funkcijos pavadinimas", + "function-name-required": "Funkcijos pavadinimas yra privalomas.", + "qualifier": "Kvalifikatorius", + "qualifier-hint": "Jei kvalifikatorius nenurodytas, bus naudojamas numatytasis \"$LATEST\".", + "aws-credentials": "AWS kredencialai", + "connection-timeout": "Ryšio laiko limitas", + "connection-timeout-required": "Ryšio laiko limitas yra privalomas.", + "connection-timeout-min": "Minimalus ryšio laikas yra 0.", + "connection-timeout-hint": "Laikas sekundėmis, kurį sistema laukia užmezgant ryšį prieš nutraukdama bandymą. Reikšmė 0 reiškia begalybę ir nerekomenduojama.", + "request-timeout": "Užklausos laiko limitas", + "request-timeout-required": "Užklausos laiko limitas yra privalomas.", + "request-timeout-min": "Minimalus užklausos laikas yra 0.", + "request-timeout-hint": "Laikas sekundėmis, kiek sistema laukia, kol užklausa bus įvykdyta. Reikšmė 0 reiškia begalybę ir nerekomenduojama.", + "units": "Vienetai", + "tell-failure-aws-lambda": "Pranešti apie klaidą, jei AWS Lambda funkcijos vykdymas sukelia išimtį", + "tell-failure-aws-lambda-hint": "Taisyklių mazgas privers pranešimą žymėti kaip klaidą, jei AWS Lambda funkcijos vykdymas sukelia išimtį.", + "basic-mode": "Paprastas režimas", + "advanced-mode": "Išplėstinis režimas", + "save-time-series": { + "processing-settings": "Apdorojimo nustatymai", + "processing-settings-hint": "Nustatykite, kaip apdorojamos gaunamos žinutės. Paprasti apdorojimo nustatymai leidžia pasirinkti iš anksto sukonfigūruotas strategijas, o išplėstiniai – individualiai pritaikyti strategijas kiekvienam veiksmui.", + "advanced-settings-hint": "Būkite atsargūs konfigūruodami apdorojimo strategijas. Kai kurie deriniai gali sukelti netikėtą elgesį.", + "strategy": "Strategija", + "deduplication-interval": "Dublikavimo šalinimo intervalas", + "deduplication-interval-required": "Dublikavimo šalinimo intervalas yra privalomas.", + "deduplication-interval-min-max-range": "Dublikavimo šalinimo intervalas turi būti ne trumpesnis nei 1 sekundė ir ne ilgesnis nei 1 diena.", + "strategy-type": { + "every-message": "Kiekvienai žinutei", + "skip": "Praleisti", + "deduplicate": "Pašalinti dublikatus", + "web-sockets-only": "Tik WebSockets" + }, + "time-series": "Laiko eilutės", + "latest": "Naujausios reikšmės", + "web-sockets": "WebSockets", + "calculated-fields": "Apskaičiuoti laukai" + }, + "save-attribute": { + "processing-settings": "Apdorojimo nustatymai", + "processing-settings-hint": "Nustatykite, kaip bus apdorojamos gaunamos žinutės. Paprasti apdorojimo nustatymai leidžia pasirinkti iš anksto sukonfigūruotas strategijas, o išplėstiniai – pasirinkti individualias apdorojimo strategijas kiekvienam veiksmui.", + "advanced-settings-hint": "Būkite atsargūs konfigūruodami apdorojimo strategijas. Kai kurie deriniai gali sukelti netikėtą elgesį.", + "strategy": "Strategija", + "deduplication-interval": "Dublikavimo šalinimo intervalas", + "deduplication-interval-required": "Dublikavimo šalinimo intervalas yra privalomas.", + "deduplication-interval-min-max-range": "Dublikavimo šalinimo intervalas turi būti ne trumpesnis nei 1 sekundė ir ne ilgesnis nei 1 diena.", + "scope": "Taikymo sritis", + "strategy-type": { + "every-message": "Kiekvienai žinutei", + "skip": "Praleisti", + "deduplicate": "Pašalinti dublikatus", + "web-sockets-only": "Tik WebSockets" + }, + "attributes": "Atributai" + }, + "key-val": { + "key": "Raktas", + "value": "Reikšmė", + "see-examples": "Žiūrėti pavyzdžius.", + "remove-entry": "Pašalinti įrašą", + "remove-mapping-entry": "Pašalinti susiejimo įrašą", + "add-mapping-entry": "Pridėti susiejimą", + "add-entry": "Pridėti įrašą", + "copy-key-values-from": "Kopijuoti raktų-reikšmių poras iš", + "delete-key-values": "Ištrinti raktų-reikšmių poras", + "delete-key-values-from": "Ištrinti raktų-reikšmių poras iš", + "at-least-one-key-error": "Turi būti pasirinktas bent vienas raktas.", + "unique-key-value-pair-error": "'{{keyText}}' turi skirtis nuo '{{valText}}'!" + }, + "mail-body-types": { + "plain-text": "Grynasis tekstas", + "html": "HTML", + "dynamic": "Dinaminis", + "use-body-type-template": "Naudoti turinio tipo šabloną", + "plain-text-description": "Paprastas, neformatuotas tekstas be specialaus stiliaus ar formatavimo.", + "html-text-description": "Leidžia naudoti HTML žymes formatavimui, nuorodoms ir vaizdams el. laiško turinyje.", + "dynamic-text-description": "Leidžia dinamiškai naudoti Grynąjį tekstą arba HTML turinio tipą pagal šablonizacijos funkciją.", + "after-template-evaluation-hint": "Po šablono įvertinimo reikšmė turi būti „true“ HTML turiniui ir „false“ grynajam tekstui." + }, + "ai": { + "ai-model": "DI modelis", + "model": "Modelis", + "ai-model-hint": "Pasirinkite iš anksto sukonfigūruotą DI modelį užklausų, siunčiamų per šį taisyklių mazgą, apdorojimui arba naudokite „Sukurti naują“, kad sukonfigūruotumėte naują modelį.", + "prompt-settings": "Užklausos nustatymai", + "prompt-settings-hint": "Neprivalomas sistemos užklausos tekstas nustato DI bendrą vaidmenį ir apribojimus, o vartotojo užklausa apibrėžia konkrečią vykdomą užduotį. Abu laukai palaiko šablonizaciją.", + "system-prompt": "Sistemos užklausa", + "system-prompt-max-length": "Sistemos užklausa turi būti ne ilgesnė nei 500000 simbolių.", + "system-prompt-blank": "Sistemos užklausa negali būti tuščia.", + "user-prompt": "Vartotojo užklausa", + "user-prompt-required": "Vartotojo užklausa yra privaloma.", + "user-prompt-max-length": "Vartotojo užklausa turi būti ne ilgesnė nei 500000 simbolių.", + "user-prompt-blank": "Vartotojo užklausa negali būti tuščia.", + "response-format": "Atsakymo formatas", + "response-text": "Tekstas", + "response-json": "JSON", + "response-json-schema": "JSON Schema", + "response-format-hint-TEXT": "Leidžia modeliui generuoti laisvo formato tekstą, kuris gali būti arba nebūti tinkamas JSON objektas. Jei išvestis nėra tinkamas JSON objektas, ji bus automatiškai įvyniota į JSON objektą po raktu „response“.", + "response-format-hint-JSON": "Modelis privalo sugeneruoti atsakymą, kuris yra teisingas JSON objektas. Jei išvestis nėra tinkamas JSON objektas, ji bus automatiškai įvyniota į JSON objektą po raktu „response“.", + "response-format-hint-JSON_SCHEMA": "Modelis privalo sugeneruoti JSON, atitinkantį nurodytą struktūrą ir duomenų tipus pagal pateiktą schemą. Jei išvestis nėra tinkamas JSON objektas, ji bus automatiškai įvyniota į JSON objektą po raktu „response“.", + "response-json-schema-hint": "Galima įvesti bet kokią tinkamą JSON schemą, tačiau šis taisyklių mazgas palaiko tik ribotą jos funkcionalumo rinkinį. Išsamesnę informaciją rasite mazgo dokumentacijoje.", + "response-json-schema-required": "JSON schema yra privaloma.", + "advanced-settings": "Išplėstiniai nustatymai", + "timeout": "Laiko limitas", + "timeout-hint": "Didžiausias laukimo laikas, kol bus gautas atsakymas iš DI modelio prieš nutraukiant užklausą.", + "timeout-required": "Laiko limitas yra privalomas.", + "timeout-validation": "Turi būti nuo 1 sekundės iki 10 minučių.", + "force-acknowledgement": "Priverstinis patvirtinimas", + "force-acknowledgement-hint": "Jei įjungta, gaunamas pranešimas patvirtinamas iš karto. Modelio atsakymas tada įtraukiamas į eilę kaip atskiras naujas pranešimas." + } + }, + "timezone": { + "timezone": "Laiko juosta", + "select-timezone": "Pasirinkti laiko juostą", + "no-timezones-matching": "Laiko juostų, atitinkančių '{{timezone}}' nėra.", + "timezone-required": "Laiko juosta būtina.", + "browser-time": "Naršyklės laikas" + }, + "queue": { + "queue-name": "Eilė", + "no-queues-found": "Eilių nerasta.", + "no-queues-matching": "Eilių, atitinkančių '{{queue}}', nerasta.", + "select-name": "Pasirinkite eilės pavadinimą", + "name": "Pavadinimas", + "name-required": "Eilės pavadinimas yra privalomas!", + "name-unique": "Eilės pavadinimas nėra unikalus!", + "name-pattern": "Eilės pavadinimas turi simbolį, kuris nėra ASCII raidinis-skaitmeninis, '.', '_' ar '-'!", + "queue-required": "Eilė yra privaloma!", + "topic-required": "Eilės tema yra privaloma!", + "poll-interval-required": "Apklausos intervalas yra privalomas!", + "poll-interval-min-value": "Apklausos intervalo reikšmė negali būti mažesnė nei 1.", + "partitions-required": "Padaliniai yra privalomi!", + "partitions-min-value": "Padalinių reikšmė negali būti mažesnė nei 1.", + "pack-processing-timeout-required": "Apdorojimo laiko limitas yra privalomas.", + "pack-processing-timeout-min-value": "Apdorojimo laiko limito reikšmė negali būti mažesnė nei 1.", + "batch-size-required": "Paketo dydis yra privalomas!", + "batch-size-min-value": "Paketo dydžio reikšmė negali būti mažesnė nei 1.", + "retries-required": "Bandymai yra privalomi!", + "retries-min-value": "Bandymai negali būti neigiami.", + "failure-percentage-required": "Klaidos procentas yra privalomas!", + "failure-percentage-min-value": "Klaidos procentas negali būti mažesnis nei 0.", + "failure-percentage-max-value": "Klaidos procentas negali būti didesnis nei 100.", + "pause-between-retries-required": "Pauzė tarp bandymų yra privaloma!", + "pause-between-retries-min-value": "Pauzės tarp bandymų reikšmė negali būti mažesnė nei 1.", + "max-pause-between-retries-required": "Maksimali pauzė tarp bandymų yra privaloma!", + "max-pause-between-retries-min-value": "Maksimalios pauzės tarp bandymų reikšmė negali būti mažesnė nei 1.", + "submit-strategy-type-required": "Pateikimo strategijos tipas yra privalomas!", + "processing-strategy-type-required": "Apdorojimo strategijos tipas yra privalomas!", + "queues": "Eilės", + "selected-queues": "Pasirinkta { count, plural, =1 {1 eilė} other {# eilės} }", + "delete-queue-title": "Ar tikrai norite ištrinti eilę '{{queueName}}'?", + "delete-queues-title": "Ar tikrai norite ištrinti { count, plural, =1 {1 eilę} other {# eiles} }?", + "delete-queue-text": "Būkite atsargūs — po patvirtinimo eilė ir visi su ja susiję duomenys bus negrįžtamai pašalinti.", + "delete-queues-text": "Po patvirtinimo visos pasirinktos eilės bus ištrintos ir taps nepasiekiamos.", + "search": "Ieškoti eilės", + "add": "Pridėti eilę", + "details": "Eilės informacija", + "topic": "Tema", + "submit-settings": "Pateikimo nustatymai", + "submit-strategy": "Strategijos tipas *", + "grouping-parameter": "Grupavimo parametras", + "processing-settings": "Bandyminių apdorojimų nustatymai", + "processing-strategy": "Apdorojimo tipas *", + "retries-settings": "Bandyminių bandymų nustatymai", + "polling-settings": "Apklausos nustatymai", + "batch-processing": "Paketo apdorojimas", + "poll-interval": "Apklausos intervalas", + "partitions": "Padaliniai", + "immediate-processing": "Tiesioginis apdorojimas", + "consumer-per-partition": "Siųsti žinučių apklausą kiekvienam vartotojui", + "consumer-per-partition-hint": "Įjungti atskirą vartotoją (-us) kiekvienam padaliniui", + "duplicate-msg-to-all-partitions": "Dubliuoti žinutę į visus padalinius", + "processing-timeout": "Apdorojimo trukmė, ms", + "batch-size": "Paketo dydis", + "retries": "Bandymai (0 – neribota)", + "failure-percentage": "Klaidingų žinučių procentas, praleidžiant bandymus", + "pause-between-retries": "Pakartoti po, sek.", + "max-pause-between-retries": "Papildomas pakartojimas po, sek.", + "delete": "Ištrinti eilę", + "copyId": "Kopijuoti eilės ID", + "idCopiedMessage": "Eilės ID nukopijuotas į iškarpinę", + "description": "Aprašymas", + "description-hint": "Šis tekstas bus rodomas eilės aprašyme vietoj pasirinktos strategijos.", + "alt-description": "Pateikimo strategija: {{submitStrategy}}, Apdorojimo strategija: {{processingStrategy}}", + "custom-properties": "Pasirinktinės savybės", + "custom-properties-hint": "Pasirinktinės eilės (temos) kūrimo savybės, pvz.: 'retention.ms:604800000;retention.bytes:1048576000'", + "strategies": { + "sequential-by-originator-label": "Sekvencinis pagal siuntėją", + "sequential-by-originator-hint": "Nauja žinutė, pvz., iš įrenginio A, nepateikiama, kol nepatvirtinama ankstesnė žinutė iš to paties įrenginio.", + "sequential-by-tenant-label": "Sekvencinis pagal nuomininką", + "sequential-by-tenant-hint": "Nauja žinutė, pvz., iš nuomininko A, nepateikiama, kol nepatvirtinama ankstesnė žinutė iš to paties nuomininko.", + "sequential-label": "Sekvencinis", + "sequential-hint": "Nauja žinutė nepateikiama, kol nepatvirtinama ankstesnė žinutė.", + "burst-label": "Srautinis (Burst)", + "burst-hint": "Visos žinutės pateikiamos taisyklių grandinėms tokia tvarka, kokia jos gaunamos.", + "batch-label": "Paketas", + "batch-hint": "Naujas paketas nepateikiamas, kol ankstesnis nepatvirtintas.", + "skip-all-failures-label": "Praleisti visas klaidas", + "skip-all-failures-hint": "Ignoruoti visas klaidas.", + "skip-all-failures-and-timeouts-label": "Praleisti visas klaidas ir laiko limitus", + "skip-all-failures-and-timeouts-hint": "Ignoruoti visas klaidas ir laiko viršijimus.", + "retry-all-label": "Kartoti visas", + "retry-all-hint": "Kartoti visas žinutes iš apdorojimo paketo.", + "retry-failed-label": "Kartoti nesėkmingas", + "retry-failed-hint": "Kartoti visas nesėkmingai apdorotas žinutes iš paketo.", + "retry-timeout-label": "Kartoti laiko viršijusias", + "retry-timeout-hint": "Kartoti visas laiko limitą viršijusias žinutes iš apdorojimo paketo.", + "retry-failed-and-timeout-label": "Kartoti nesėkmingas ir laiko viršijusias", + "retry-failed-and-timeout-hint": "Kartoti visas nesėkmingas ir laiko limitą viršijusias žinutes iš apdorojimo paketo." + } + }, + "queue-statistics": { + "queue-statistics": "Eilių statistika", + "no-queue-statistics-matching": "Eilių statistikos, atitinkančios '{{entity}}', nerasta.", + "queue-statistics-required": "Eilių statistika yra privaloma.", + "list-of-queue-statistics": "{ count, plural, =1 {Viena eilės statistika} other {# eilių statistikos sąrašas} }", + "selected-queue-statistics": "Pasirinkta { count, plural, =1 {1 eilės statistika} other {# eilių statistikos} }", + "no-queue-statistics-text": "Eilių statistikos nerasta", + "queue-statistics-starts-with": "Eilių statistika, kurios pavadinimai prasideda '{{prefix}}'" + }, + "server-error": { + "general": "Bendra serverio klaida", + "authentication": "Autentifikavimo klaida", + "jwt-token-expired": "JWT prieigos raktas pasibaigęs", + "tenant-trial-expired": "Nuomininko bandomasis laikotarpis pasibaigė", + "credentials-expired": "Prisijungimo duomenys pasibaigę", + "permission-denied": "Prieiga uždrausta", + "invalid-arguments": "Neteisingi argumentai", + "bad-request-params": "Netinkami užklausos parametrai", + "item-not-found": "Elementas nerastas", + "too-many-requests": "Per daug užklausų", + "too-many-updates": "Per daug atnaujinimų" + }, + "tenant": { + "tenant": "Valdytojas", + "tenants": "Valdytojai", + "management": "Valdytojų valdymas", + "add": "Pridėti valdytoją", + "admins": "Administratoriai", + "manage-tenant-admins": "Valdyti valdytojo administratorius", + "delete": "Ištrinti valdytoją", + "add-tenant-text": "Pridėti naują valdytoją", + "no-tenants-text": "Valdytojų nėra", + "tenant-details": "Informacija apie valdytoją", + "title-max-length": "Pavadinimas negali viršyti 256 simbolių", + "delete-tenant-title": "Ar tikrai norite ištrinti valdytoją '{{tenantTitle}}'?", + "delete-tenant-text": "Būkite dėmesingi, po patvirtinimo valdytojas ir visa su juo susijusi informacija bus pašalinta.", + "delete-tenants-title": "Ar tikrai norite pašalinti { count, plural, =1 {1 valdytoją} other {# valdytojus} }?", + "delete-tenants-action-title": "Pašalinti { count, plural, =1 {1 valdytoją} other {# valdytojus} }", + "delete-tenants-text": "Būkite dėmesingi, po patvirtinimo, visi pasirinkti valdytojai ir su jais susijusi informacija bus pašalinta.", + "title": "Pavadinimas", + "title-required": "Pavadinimas būtinas.", + "description": "Aprašymas", + "details": "Informacija", + "events": "Įvykiai", + "copyId": "Kopijuoti valdytojo Id", + "idCopiedMessage": "Valdytojo Id nukopijuotas į iškarpinę", + "select-tenant": "Pasirinkti valdytoją", + "no-tenants-matching": "Valdytojų, atitinkančių '{{entity}}' nėra.", + "tenant-required": "Valdytojas būtinas", + "search": "Valdytojų paieška", + "selected-tenants": "Pasirinkta { count, plural, =1 {1 valdytojas} other {# valdytojai} }", + "isolated-tb-rule-engine": "Naudoti atskiras ThingsBoard taisyklių variklio eiles", + "isolated-tb-rule-engine-details": "Kiekvienas nuomininkas turės atskiras taisyklių variklio eiles" + }, + "tenant-profile": { + "tenant-profile": "Nuomininko profilis", + "tenant-profiles": "Nuomininkų profiliai", + "add": "Pridėti nuomininko profilį", + "add-profile": "Pridėti profilį", + "debug": "Derinimas", + "edit": "Redaguoti nuomininko profilį", + "tenant-profile-details": "Informacija apie nuomininko profilį", + "no-tenant-profiles-text": "Nuomininkų profilių nėra", + "name-max-length": "Pavadinimas negali viršyti 256 simbolių", + "search": "Nuomininkų profilių paieška", + "selected-tenant-profiles": "Pasirinkta { count, plural, =1 {1 nuomininko profilis} other {# nuomininkų profiliai} }", + "no-tenant-profiles-matching": "Nuomininkų profilių, atitinkančių '{{entity}}', nėra.", + "tenant-profile-required": "Nuomininko profilis yra privalomas", + "idCopiedMessage": "Nuomininko profilio ID nukopijuotas į iškarpinę", + "set-default": "Nustatyti kaip pagrindinį nuomininko profilį", + "delete": "Pašalinti nuomininko profilį", + "copyId": "Kopijuoti nuomininko profilio ID", + "name": "Pavadinimas", + "name-required": "Pavadinimas yra privalomas.", + "data": "Profilio duomenys", + "profile-configuration": "Profilio konfigūracija", + "description": "Aprašymas", + "default": "Pagrindinis", + "delete-tenant-profile-title": "Ar tikrai norite pašalinti nuomininko profilį '{{tenantProfileName}}'?", + "delete-tenant-profile-text": "Būkite atsargūs — po patvirtinimo nuomininko profilis ir visa su juo susijusi informacija bus negrįžtamai pašalinta.", + "delete-tenant-profiles-title": "Ar tikrai norite pašalinti { count, plural, =1 {1 nuomininko profilį} other {# nuomininkų profilius} }?", + "delete-tenant-profiles-text": "Būkite atsargūs — po patvirtinimo visi pasirinkti nuomininkų profiliai ir su jais susijusi informacija bus negrįžtamai pašalinti.", + "set-default-tenant-profile-title": "Ar tikrai norite nuomininko profilį '{{tenantProfileName}}' nustatyti kaip pagrindinį?", + "set-default-tenant-profile-text": "Po patvirtinimo šis nuomininko profilis bus nustatytas kaip pagrindinis ir bus automatiškai priskiriamas naujiems nuomininkams, kuriems profilis nenurodytas.", + "no-tenant-profiles-found": "Nuomininkų profilių nerasta.", + "create-new-tenant-profile": "Sukurti naują!", + "create-tenant-profile": "Sukurti naują nuomininko profilį", + "import": "Importuoti nuomininko profilį", + "export": "Eksportuoti nuomininko profilį", + "export-failed-error": "Nepavyko eksportuoti nuomininko profilio: {{error}}", + "tenant-profile-file": "Nuomininko profilio failas", + "invalid-tenant-profile-file-error": "Nepavyko importuoti nuomininko profilio: neteisinga duomenų struktūra.", + "advanced-settings": "Pažangūs nustatymai", + "entities": "Subjektai", + "rule-engine": "Taisyklių variklis", + "time-to-live": "Gyvavimo trukmė (Time-to-Live)", + "calculated-fields": "Apskaičiuojami laukai", + "alarms-and-notifications": "Pavojaus signalai ir pranešimai", + "ota-files-in-bytes": "OTA failai baitais", + "ws-title": "WS", + "unlimited": "(0 – neribota)", + "maximum-devices": "Didžiausias įrenginių skaičius", + "maximum-devices-required": "Didžiausias įrenginių skaičius yra privalomas.", + "maximum-devices-range": "Didžiausias įrenginių skaičius negali būti neigiamas", + "maximum-assets": "Didžiausias išteklių (asset) skaičius", + "maximum-assets-required": "Didžiausias išteklių (asset) skaičius yra privalomas.", + "maximum-assets-range": "Didžiausias išteklių (asset) skaičius negali būti neigiamas", + "maximum-customers": "Didžiausias klientų skaičius", + "maximum-customers-required": "Didžiausias klientų skaičius yra privalomas.", + "maximum-customers-range": "Didžiausias klientų skaičius negali būti neigiamas", + "maximum-users": "Didžiausias vartotojų skaičius", + "maximum-users-required": "Didžiausias vartotojų skaičius yra privalomas.", + "maximum-users-range": "Didžiausias vartotojų skaičius negali būti neigiamas", + "maximum-dashboards": "Didžiausias skydelių (dashboard) skaičius", + "maximum-dashboards-required": "Didžiausias skydelių (dashboard) skaičius yra privalomas.", + "maximum-dashboards-range": "Didžiausias skydelių (dashboard) skaičius negali būti neigiamas", + "maximum-edges": "Didžiausias Edge įrenginių skaičius", + "maximum-edges-required": "Didžiausias Edge įrenginių skaičius yra privalomas.", + "maximum-edges-range": "Didžiausias Edge įrenginių skaičius negali būti neigiamas", + "maximum-rule-chains": "Didžiausias taisyklių grandinių (rule chains) skaičius", + "maximum-rule-chains-required": "Didžiausias taisyklių grandinių (rule chains) skaičius yra privalomas.", + "maximum-rule-chains-range": "Didžiausias taisyklių grandinių (rule chains) skaičius negali būti neigiamas", + "maximum-resources-sum-data-size": "Išteklių failų bendras dydis", + "maximum-resources-sum-data-size-required": "Išteklių failų bendras dydis yra privalomas.", + "maximum-resources-sum-data-size-range": "Išteklių failų bendras dydis negali būti neigiamas", + "maximum-resource-size": "Didžiausias vieno ištekliaus failo dydis (baitais)", + "maximum-resource-size-required": "Didžiausias ištekliaus failo dydis yra privalomas.", + "maximum-resource-size-range": "Didžiausias ištekliaus failo dydis negali būti neigiamas", + "maximum-ota-packages-sum-data-size": "OTA paketų failų bendras dydis", + "maximum-ota-package-sum-data-size-required": "OTA paketų failų bendras dydis yra privalomas.", + "maximum-ota-package-sum-data-size-range": "OTA paketų failų bendras dydis negali būti neigiamas", + "maximum-debug-duration-min": "Didžiausia derinimo trukmė (minutėmis)", + "maximum-debug-duration-min-range": "Didžiausia derinimo trukmė negali būti neigiama", + "rest-requests-for-tenant": "REST užklausos nuomininkui", + "transport-tenant-telemetry-msg-rate-limit": "Nuomininko telemetrijos pranešimų limitas", + "transport-tenant-telemetry-data-points-rate-limit": "Nuomininko telemetrijos duomenų taškų limitas", + "transport-device-msg-rate-limit": "Įrenginių pranešimų limitas", + "transport-device-telemetry-msg-rate-limit": "Įrenginių telemetrijos pranešimų limitas", + "transport-device-telemetry-data-points-rate-limit": "Įrenginių telemetrijos duomenų taškų limitas", + "transport-gateway-msg-rate-limit": "Šliuzo pranešimų limitas", + "transport-gateway-telemetry-msg-rate-limit": "Šliuzo telemetrijos pranešimų limitas", + "transport-gateway-telemetry-data-points-rate-limit": "Šliuzo telemetrijos duomenų taškų limitas", + "transport-gateway-device-msg-rate-limit": "Šliuzo įrenginių pranešimų limitas", + "transport-gateway-device-telemetry-msg-rate-limit": "Šliuzo įrenginių telemetrijos pranešimų limitas", + "transport-gateway-device-telemetry-data-points-rate-limit": "Šliuzo įrenginių telemetrijos duomenų taškų limitas", + "tenant-entity-export-rate-limit": "Objektų versijų kūrimo limitas", + "tenant-entity-import-rate-limit": "Objektų versijų įkėlimo limitas", + "tenant-notification-request-rate-limit": "Pranešimų užklausų limitas", + "tenant-notification-requests-per-rule-rate-limit": "Pranešimų užklausų limitas pagal taisyklę", + "max-calculated-fields": "Didžiausias apskaičiuojamų laukų skaičius vienam subjektui", + "max-calculated-fields-range": "Apskaičiuojamų laukų skaičius negali būti neigiamas", + "max-calculated-fields-required": "Didžiausias apskaičiuojamų laukų skaičius vienam subjektui yra privalomas", + "max-data-points-per-rolling-arg": "Didžiausias duomenų taškų skaičius slenkančiuose argumentuose", + "max-data-points-per-rolling-arg-range": "Didžiausias duomenų taškų skaičius slenkančiuose argumentuose negali būti neigiamas", + "max-data-points-per-rolling-arg-required": "Didžiausias duomenų taškų skaičius slenkančiuose argumentuose yra privalomas", + "max-arguments-per-cf": "Didžiausias argumentų skaičius viename apskaičiuojamame lauke", + "max-arguments-per-cf-range": "Didžiausias argumentų skaičius viename apskaičiuojamame lauke negali būti neigiamas", + "max-arguments-per-cf-required": "Didžiausias argumentų skaičius viename apskaičiuojamame lauke yra privalomas", + "max-state-size": "Didžiausias būsenos dydis (KB)", + "max-state-size-range": "Didžiausias būsenos dydis (KB) negali būti neigiamas", + "max-state-size-required": "Didžiausias būsenos dydis (KB) yra privalomas", + "max-value-argument-size": "Didžiausias vieno argumento reikšmės dydis (KB)", + "max-value-argument-size-range": "Didžiausias vieno argumento reikšmės dydis (KB) negali būti neigiamas", + "max-value-argument-size-required": "Didžiausias vieno argumento reikšmės dydis (KB) yra privalomas", + "max-transport-messages": "Didžiausias transporto pranešimų skaičius", + "max-transport-messages-required": "Didžiausias transporto pranešimų skaičius yra privalomas.", + "max-transport-messages-range": "Didžiausias transporto pranešimų skaičius negali būti neigiamas", + "max-transport-data-points": "Didžiausias transporto duomenų taškų skaičius", + "max-transport-data-points-required": "Didžiausias transporto duomenų taškų skaičius yra privalomas.", + "max-transport-data-points-range": "Didžiausias transporto duomenų taškų skaičius negali būti neigiamas", + "max-r-e-executions": "Didžiausias taisyklių variklio vykdymų skaičius", + "max-r-e-executions-required": "Didžiausias taisyklių variklio vykdymų skaičius yra privalomas.", + "max-r-e-executions-range": "Didžiausias taisyklių variklio vykdymų skaičius negali būti neigiamas", + "max-j-s-executions": "Didžiausias JavaScript vykdymų skaičius", + "max-j-s-executions-required": "Didžiausias JavaScript vykdymų skaičius yra privalomas.", + "max-j-s-executions-range": "Didžiausias JavaScript vykdymų skaičius negali būti neigiamas", + "max-tbel-executions": "Didžiausias TBEL vykdymų skaičius", + "max-tbel-executions-required": "Didžiausias TBEL vykdymų skaičius yra privalomas.", + "max-tbel-executions-range": "Didžiausias TBEL vykdymų skaičius negali būti neigiamas", + "max-d-p-storage-days": "Didžiausias duomenų taškų saugojimo dienų skaičius", + "max-d-p-storage-days-required": "Didžiausias duomenų taškų saugojimo dienų skaičius yra privalomas.", + "max-d-p-storage-days-range": "Didžiausias duomenų taškų saugojimo dienų skaičius negali būti neigiamas", + "default-storage-ttl-days": "Numatytasis duomenų saugojimo laikotarpis (TTL dienomis)", + "default-storage-ttl-days-required": "Numatytasis duomenų saugojimo laikotarpis (TTL) yra privalomas.", + "default-storage-ttl-days-range": "Numatytasis duomenų saugojimo laikotarpis (TTL) negali būti neigiamas", + "alarms-ttl-days": "Pavojaus signalų TTL (dienomis)", + "alarms-ttl-days-required": "Pavojaus signalų TTL (dienomis) yra privalomas", + "alarms-ttl-days-days-range": "Pavojaus signalų TTL (dienomis) negali būti neigiamas", + "rpc-ttl-days": "RPC TTL (dienomis)", + "rpc-ttl-days-required": "RPC TTL (dienomis) yra privalomas", + "rpc-ttl-days-days-range": "RPC TTL (dienomis) negali būti neigiamas", + "queue-stats-ttl-days": "Eilių statistikos TTL (dienomis)", + "queue-stats-ttl-days-required": "Eilių statistikos TTL (dienomis) yra privalomas", + "queue-stats-ttl-days-range": "Eilių statistikos TTL (dienomis) negali būti neigiamas", + "rule-engine-exceptions-ttl-days": "Taisyklių variklio išimčių TTL (dienomis)", + "rule-engine-exceptions-ttl-days-required": "Taisyklių variklio išimčių TTL (dienomis) yra privalomas", + "rule-engine-exceptions-ttl-days-range": "Taisyklių variklio išimčių TTL (dienomis) negali būti neigiamas", + "max-rule-node-executions-per-message": "Didžiausias taisyklių mazgo vykdymų skaičius vienai žinutei", + "max-rule-node-executions-per-message-required": "Didžiausias taisyklių mazgo vykdymų skaičius vienai žinutei yra privalomas.", + "max-rule-node-executions-per-message-range": "Didžiausias taisyklių mazgo vykdymų skaičius vienai žinutei negali būti neigiamas", + "max-emails": "Didžiausias išsiųstų el. laiškų skaičius", + "max-emails-required": "Didžiausias išsiųstų el. laiškų skaičius yra privalomas.", + "max-emails-range": "Didžiausias išsiųstų el. laiškų skaičius negali būti neigiamas", + "sms-enabled": "SMS siuntimas įjungtas", + "max-sms": "Didžiausias išsiųstų SMS skaičius", + "max-sms-required": "Didžiausias išsiųstų SMS skaičius yra privalomas.", + "max-sms-range": "Didžiausias išsiųstų SMS skaičius negali būti neigiamas", + "max-created-alarms": "Didžiausias sukurtų pavojaus signalų skaičius", + "max-created-alarms-required": "Didžiausias sukurtų pavojaus signalų skaičius yra privalomas.", + "max-created-alarms-range": "Didžiausias sukurtų pavojaus signalų skaičius negali būti neigiamas", + "no-queue": "Eilė nesukonfigūruota", + "add-queue": "Pridėti eilę", + "queues-with-count": "Eilės ({{count}})", + "tenant-rest-limits": "REST užklausų limitas nuomininkui", + "customer-rest-limits": "REST užklausų limitas klientui", + "incorrect-pattern-for-rate-limits": "Formatas turi būti kableliais atskirtos poros su talpa ir laikotarpiu (sekundėmis), pvz.: 100:1,2000:60", + "too-small-value-zero": "Reikšmė turi būti didesnė nei 0", + "too-small-value-one": "Reikšmė turi būti didesnė nei 1", + "queue-size-is-limited-by-system-configuration": "Eilės dydis taip pat yra ribojamas sistemos konfigūracijos.", + "cassandra-write-tenant-core-limits-configuration": "REST API Cassandra rašymo užklausos", + "cassandra-read-tenant-core-limits-configuration": "REST API ir WS telemetrijos Cassandra skaitymo užklausos", + "cassandra-write-tenant-rule-engine-limits-configuration": "Taisyklių variklio telemetrijos Cassandra rašymo užklausos", + "cassandra-read-tenant-rule-engine-limits-configuration": "Taisyklių variklio telemetrijos Cassandra skaitymo užklausos", + "ws-limit-max-sessions-per-tenant": "Didžiausias sesijų skaičius vienam nuomininkui", + "ws-limit-max-sessions-per-customer": "Didžiausias sesijų skaičius vienam klientui", + "ws-limit-max-sessions-per-regular-user": "Didžiausias sesijų skaičius vienam vartotojui", + "ws-limit-max-sessions-per-public-user": "Didžiausias sesijų skaičius viešam vartotojui", + "ws-limit-queue-per-session": "Didžiausias pranešimų eilės dydis vienai sesijai", + "ws-limit-max-subscriptions-per-tenant": "Didžiausias prenumeratų skaičius vienam nuomininkui", + "ws-limit-max-subscriptions-per-customer": "Didžiausias prenumeratų skaičius vienam klientui", + "ws-limit-max-subscriptions-per-regular-user": "Didžiausias prenumeratų skaičius vienam vartotojui", + "ws-limit-max-subscriptions-per-public-user": "Didžiausias prenumeratų skaičius viešam vartotojui", + "ws-limit-updates-per-session": "WS atnaujinimų skaičius vienai sesijai", + "rate-limits": { + "add-limit": "Pridėti limitą", + "and-also-less-than": "ir taip pat mažesnis nei", + "advanced-settings": "Pažangūs nustatymai", + "edit-limit": "Redaguoti limitą", + "calculated-field-debug-event-rate-limit": "Apskaičiuojamų laukų derinimo įvykių limitas", + "edit-calculated-field-debug-event-rate-limit": "Redaguoti apskaičiuojamų laukų derinimo įvykių limitus", + "edit-transport-tenant-msg-title": "Redaguoti nuomininko transporto pranešimų limitus", + "edit-transport-tenant-telemetry-msg-title": "Redaguoti nuomininko telemetrijos pranešimų limitus", + "edit-transport-tenant-telemetry-data-points-title": "Redaguoti nuomininko telemetrijos duomenų taškų limitus", + "edit-transport-device-msg-title": "Redaguoti įrenginių transporto pranešimų limitus", + "edit-transport-device-telemetry-msg-title": "Redaguoti įrenginių telemetrijos pranešimų limitus", + "edit-transport-device-telemetry-data-points-title": "Redaguoti įrenginių telemetrijos duomenų taškų limitus", + "edit-transport-gateway-msg-title": "Redaguoti šliuzo pranešimų limitus", + "edit-transport-gateway-telemetry-msg-title": "Redaguoti šliuzo telemetrijos pranešimų limitus", + "edit-transport-gateway-telemetry-data-points-title": "Redaguoti šliuzo telemetrijos duomenų taškų limitus", + "edit-transport-gateway-device-msg-title": "Redaguoti šliuzo įrenginių pranešimų limitus", + "edit-transport-gateway-device-telemetry-msg-title": "Redaguoti šliuzo įrenginių telemetrijos pranešimų limitus", + "edit-transport-gateway-device-telemetry-data-points-title": "Redaguoti šliuzo įrenginių telemetrijos duomenų taškų limitus", + "edit-tenant-rest-limits-title": "Redaguoti REST užklausų limitus nuomininkui", + "edit-customer-rest-limits-title": "Redaguoti REST užklausų limitus klientui", + "edit-ws-limit-updates-per-session-title": "Redaguoti WS atnaujinimų per sesiją limitus", + "edit-cassandra-write-tenant-core-limits-configuration": "Redaguoti REST API Cassandra rašymo užklausų limitus", + "edit-cassandra-read-tenant-core-limits-configuration": "Redaguoti REST API ir WS telemetrijos Cassandra skaitymo užklausų limitus", + "edit-cassandra-write-tenant-rule-engine-limits-configuration": "Redaguoti taisyklių variklio telemetrijos Cassandra rašymo užklausų limitus", + "edit-cassandra-read-tenant-rule-engine-limits-configuration": "Redaguoti taisyklių variklio telemetrijos Cassandra skaitymo užklausų limitus", + "edit-tenant-entity-export-rate-limit-title": "Redaguoti objektų versijų kūrimo limitus", + "edit-tenant-entity-import-rate-limit-title": "Redaguoti objektų versijų įkėlimo limitus", + "edit-tenant-notification-request-rate-limit-title": "Redaguoti pranešimų užklausų limitus", + "edit-tenant-notification-requests-per-rule-rate-limit-title": "Redaguoti pranešimų užklausų limitus pagal taisyklę", + "edit-edge-events-rate-limit": "Redaguoti Edge įvykių limitus", + "edit-edge-events-per-edge-rate-limit": "Redaguoti Edge įvykių limitus pagal Edge įrenginį", + "edge-events-rate-limit": "Edge įvykiai", + "edge-events-per-edge-rate-limit": "Edge įvykiai pagal Edge įrenginį", + "edit-edge-uplink-messages-rate-limit": "Redaguoti Edge į viršų siunčiamų pranešimų (uplink) limitus", + "edit-edge-uplink-messages-per-edge-rate-limit": "Redaguoti Edge į viršų siunčiamų pranešimų (uplink) limitus pagal Edge įrenginį", + "edge-uplink-messages-rate-limit": "Edge uplink pranešimai", + "edge-uplink-messages-per-edge-rate-limit": "Edge uplink pranešimai pagal Edge įrenginį", + "messages-per": "pranešimų per", + "not-set": "Nenustatyta", + "number-of-messages": "Pranešimų skaičius", + "number-of-messages-required": "Pranešimų skaičius yra privalomas.", + "number-of-messages-min": "Mažiausia reikšmė yra 1.", + "preview": "Peržiūra", + "per-seconds": "Per sekundes", + "per-seconds-required": "Laiko norma yra privaloma.", + "per-seconds-min": "Mažiausia reikšmė yra 1.", + "per-seconds-duplicate": "Pasikartojanti laiko norma. Kiekvienas laiko intervalas turi būti unikalus.", + "rate-limits": "Greitaveikos limitai", + "remove-limit": "Pašalinti limitą", + "transport-tenant-msg": "Nuomininko transporto pranešimai", + "transport-tenant-telemetry-msg": "Nuomininko telemetrijos pranešimai", + "transport-tenant-telemetry-data-points": "Nuomininko telemetrijos duomenų taškai", + "transport-device-msg": "Įrenginių transporto pranešimai", + "transport-device-telemetry-msg": "Įrenginių telemetrijos pranešimai", + "transport-device-telemetry-data-points": "Įrenginių telemetrijos duomenų taškai", + "transport-gateway-msg": "Šliuzo pranešimai", + "transport-gateway-telemetry-msg": "Šliuzo telemetrijos pranešimai", + "transport-gateway-telemetry-data-points": "Šliuzo telemetrijos duomenų taškai", + "transport-gateway-device-msg": "Šliuzo įrenginių pranešimai", + "transport-gateway-device-telemetry-msg": "Šliuzo įrenginių telemetrijos pranešimai", + "transport-gateway-device-telemetry-data-points": "Šliuzo įrenginių telemetrijos duomenų taškai", + "sec": "sek." + } + }, + "timeinterval": { + "seconds-interval": "{ seconds, plural, =1 {1 sekundė} other {# sekundės} }", + "minutes-interval": "{ minutes, plural, =1 {1 minutė} other {# minutės} }", + "hours-interval": "{ hours, plural, =1 {1 valanda} other {# valandos} }", + "days-interval": "{ days, plural, =1 {1 diena} other {# dienos} }", + "days": "Dienos", + "hours": "Valandos", + "minutes": "Minutės", + "seconds": "Sekundės", + "advanced": "Išplėstiniai nustatymai", + "custom": "Pasirinktinis", + "predefined": { + "yesterday": "Vakar", + "day-before-yesterday": "Užvakar", + "this-day-last-week": "Ši diena prieš savaitę", + "previous-week": "Praėjusi savaitė (Sek – Šeš)", + "previous-week-iso": "Praėjusi savaitė (Pir – Sek)", + "previous-month": "Praėjęs mėnuo", + "previous-quarter": "Praėjęs ketvirtis", + "previous-half-year": "Praėjęs pusmetis", + "previous-year": "Praėję metai", + "current-hour": "Dabartinė valanda", + "current-day": "Dabartinė diena", + "current-day-so-far": "Dabartinė diena iki dabar", + "current-week": "Dabartinė savaitė (Sek – Šeš)", + "current-week-iso": "Dabartinė savaitė (Pir – Sek)", + "current-week-so-far": "Dabartinė savaitė iki dabar (Sek – Šeš)", + "current-week-iso-so-far": "Dabartinė savaitė iki dabar (Pir – Sek)", + "current-month": "Dabartinis mėnuo", + "current-month-so-far": "Dabartinis mėnuo iki dabar", + "current-quarter": "Dabartinis ketvirtis", + "current-quarter-so-far": "Dabartinis ketvirtis iki dabar", + "current-half-year": "Dabartinis pusmetis", + "current-half-year-so-far": "Dabartinis pusmetis iki dabar", + "current-year": "Dabartiniai metai", + "current-year-so-far": "Dabartiniai metai iki dabar" + }, + "type": { + "week": "Savaitė (Sek – Šeš)", + "week-iso": "Savaitė (Pir – Sek)", + "month": "Mėnuo", + "quarter": "Ketvirtis" + } + }, + "timeunit": { + "milliseconds": "Milisekundės", + "seconds": "Sekundės", + "minutes": "Minutės", + "hours": "Valandos", + "days": "Dienos" + }, + "timewindow": { + "timewindow": "Laiko langas", + "timewindow-settings": "Laiko lango nustatymai", + "years": "{ years, plural, =1 { metai } other {# metai } }", + "years-short": "{{ years }}m", + "months": "{ months, plural, =1 { mėnuo } other {# mėnesiai } }", + "months-short": "{{ months }}mėn", + "weeks": "{ weeks, plural, =1 { savaitė } other {# savaitės } }", + "weeks-short": "{{ weeks }}sav", + "days": "{ days, plural, =1 { diena } other {# dienos } }", + "days-short": "{{ days }}d", + "hours": "{ hours, plural, =0 { valandų } =1 {1 valanda } other {# valandos } }", + "hr": "{{ hr }} val", + "hr-short": "{{ hr }}val", + "minutes": "{ minutes, plural, =0 { minučių } =1 {1 minutė } other {# minutės } }", + "min": "{{ min }} min", + "min-short": "{{ min }}min", + "seconds": "{ seconds, plural, =0 { sekundžių } =1 {1 sekundė } other {# sekundės } }", + "sec": "{{ sec }} sek", + "sec-short": "{{ sec }}s", + "short": { + "years": "{ years, plural, =1 {1 metai } other {# metai } }", + "days": "{ days, plural, =1 {1 diena } other {# dienos } }", + "hours": "{ hours, plural, =1 {1 valanda } other {# valandos } }", + "minutes": "{{ minutes }} min", + "seconds": "{{ seconds }} sek" + }, + "realtime": "Realiu laiku", + "history": "Istorija", + "last-prefix": "Paskutinė", + "period": "nuo {{ startTime }} iki {{ endTime }}", + "edit": "Redaguoti laiko langą", + "date-range": "Datų intervalas", + "for-all-time": "Visas laikotarpis", + "last": "Paskutinė", + "time-period": "Laikotarpis", + "hide": "Slėpti", + "interval": "Intervalas", + "just-now": "Ką tik", + "just-now-lower": "ką tik", + "ago": "prieš", + "style": "Laiko lango stilius", + "icon": "Piktograma", + "icon-position": "Piktogramos padėtis", + "icon-position-left": "Kairėje", + "icon-position-right": "Dešinėje", + "font": "Šriftas", + "color": "Spalva", + "displayTypePrefix": "Rodyti „Realiu laiku“ / „Istorija“ priešdėlį", + "preview": "Peržiūra", + "relative": "Santykinis", + "range": "Intervalas", + "hide-timewindow-section": "Slėpti laiko lango sekciją nuo naudotojų", + "hide-last-interval": "Slėpti paskutinį intervalą nuo naudotojų", + "hide-relative-interval": "Slėpti santykinį intervalą nuo naudotojų", + "hide-fixed-interval": "Slėpti fiksuotą intervalą nuo naudotojų", + "hide-aggregation": "Slėpti agregaciją nuo naudotojų", + "hide-group-interval": "Slėpti grupavimo intervalą nuo naudotojų", + "hide-max-values": "Slėpti maksimalias reikšmes nuo naudotojų", + "hide-timezone": "Slėpti laiko juostą nuo naudotojų", + "disable-custom-interval": "Išjungti pasirinktinių intervalų pasirinkimą", + "edit-aggregation-functions-list": "Redaguoti agregavimo funkcijų sąrašą", + "edit-aggregation-functions-list-hint": "Galima nurodyti galimų funkcijų sąrašą.", + "allowed-aggregation-functions": "Leidžiamos agregavimo funkcijos", + "edit-intervals-list": "Redaguoti intervalų sąrašą", + "allowed-agg-intervals": "Grupavimo intervalai", + "default-agg-interval": "Numatytasis grupavimo intervalas", + "edit-intervals-list-hint": "Galima nurodyti galimų intervalų parinkčių sąrašą.", + "edit-grouping-intervals-list-hint": "Galima sukonfigūruoti grupavimo intervalų sąrašą ir numatytąjį grupavimo intervalą.", + "all": "Visi" + }, + "tooltip": { + "trigger": "Trigeris", + "trigger-point": "Taškas", + "trigger-axis": "Ašis", + "label": "Etiketė", + "value": "Reikšmė", + "date": "Data", + "show-date-time-interval": "Rodyti datos ir laiko intervalą", + "show-date-time-interval-hint": "Rodyti datos ir laiko intervalą pagal duomenų agregaciją.", + "hide-zero-tooltip-values": "Slėpti nulines reikšmes", + "background-color": "Fono spalva", + "background-blur": "Fono suliejimas" + }, + "unit": { + "set-unit-conversion": "Nustatyti matavimo vienetų konvertavimą", + "unit-settings": { + "unit-settings": "Vienetų nustatymai", + "source-unit": "Pirminis vienetas", + "source-unit-hint": "Tai saugomos reikšmės vienetas. Vienetas, iš kurio konvertuojama. Įveskite simbolį, naudojamą jūsų šaltinio duomenyse (pvz., m, km, ft, in).", + "target-metric-unit": "Tikslinis metrinis vienetas", + "target-metric-unit-hint": "Pasirinkite, į kurį metrinį (SI) vienetą konvertuoti šaltinio reikšmę (pvz., cm, mm, km).", + "target-imperial-unit": "Tikslinis imperinis vienetas", + "target-imperial-unit-hint": "Pasirinkite, į kurį imperinį vienetą konvertuoti šaltinio reikšmę (pvz., in, ft, yd).", + "target-hybrid-unit": "Tikslinis mišrus vienetas", + "target-hybrid-unit-hint": "Pasirinkite, į kurį mišrų vienetą konvertuoti šaltinio reikšmę (pvz., cm, in, km). Mišrūs vienetai jungia metrinius ir/arba imperinius vienetus.", + "enable-unit-conversion": "Įjungti vienetų konvertavimą", + "enable-unit-conversion-hint": "Įjunkite, kad suaktyvintumėte konvertavimą. Kai išjungta – šaltinio reikšmė bus perduodama nekeičiama. Išjungta, jei atitinkamoje matavimo grupėje yra tik vienas vienetas (pvz., šviesos srautas, OKI)." + }, + "unit-system": "Matavimo sistema", + "unit-system-type": { + "AUTO": "Automatinė", + "METRIC": "Metrinė", + "IMPERIAL": "Imperinė", + "HYBRID": "Mišri" + }, + "measures": { + "absorbed-dose-rate": "Sugertos dozės greitis", + "acceleration": "Pagreitis", + "acidity": "Rūgštingumas", + "air-quality-index": "Oro kokybės indeksas", + "amount-of-substance": "Medžiagos kiekis", + "angle": "Kampas", + "angular-acceleration": "Kampinis pagreitis", + "area": "Plotas", + "area-density": "Paviršiaus tankis", + "capacitance": "Talpa", + "catalytic-activity": "Katalizinė veikla", + "catalytic-concentration": "Katalizinė koncentracija", + "charge": "Krovinys", + "current-density": "Srovės tankis", + "data-transfer-rate": "Duomenų perdavimo greitis", + "density": "Tankis", + "digital": "Skaitmeninis", + "dimension-ratio": "Matmenų santykis", + "dynamic-viscosity": "Dinaminė klampa", + "earthquake-magnitude": "Žemės drebėjimo stiprumas", + "electric-charge-density": "Elektros krūvio tankis", + "electric-current": "Elektros srovė", + "electric-dipole-moment": "Elektrinis dipolio momentas", + "electric-field-strength": "Elektrinio lauko stipris", + "electric-flux": "Elektrinis srautas", + "electric-permittivity": "Elektrinė skvarba", + "electric-polarizability": "Elektrinė poliariškumas", + "electrical-conductance": "Elektrinė laidumo varža", + "electrical-conductivity": "Elektrinis laidumas", + "energy": "Energija", + "energy-density": "Energijos tankis", + "force": "Jėga", + "frequency": "Dažnis", + "fuel-efficiency": "Kuro sąnaudos / efektyvumas", + "heat-capacity": "Šiluminė talpa", + "illuminance": "Apšvietis", + "inductance": "Induktyvumas", + "kinematic-viscosity": "Kinematinė klampa", + "length": "Ilgis", + "light-exposure": "Šviesos ekspozicija", + "linear-charge-density": "Linijinis krūvio tankis", + "logarithmic-ratio": "Logaritminis santykis", + "luminous-efficacy": "Šviesos efektyvumas", + "luminous-flux": "Šviesos srautas", + "luminous-intensity": "Šviesos stipris", + "magnetic-field-gradient": "Magnetinio lauko gradientas", + "magnetic-flux": "Magnetinis srautas", + "magnetic-flux-density": "Magnetinio srauto tankis", + "magnetic-moment": "Magnetinis momentas", + "magnetic-permeability": "Magnetinė skvarba", + "mass": "Masa", + "mass-fraction": "Masės dalis", + "molar-concentration": "Molinė koncentracija", + "molar-energy": "Molinė energija", + "molar-heat-capacity": "Molinė šiluminė talpa", + "molar-mass": "Molinė masė", + "number-concentration": "Dalelių koncentracija", + "parts-per-million": "Dalinės dalys milijone (ppm)", + "power": "Galia", + "power-density": "Galios tankis", + "pressure": "Slėgis", + "radiance": "Spinduliuotė", + "radiant-intensity": "Spinduliuotės intensyvumas", + "radiation-dose": "Radiacijos dozė", + "radioactive-decay": "Radioaktyvus skilimas", + "radioactivity": "Radioaktyvumas", + "radioactivity-concentration": "Radioaktyvumo koncentracija", + "reciprocal-length": "Atvirkštinis ilgis", + "resistance": "Varža", + "reynolds-number": "Reinoldso skaičius", + "signal-level": "Signalo lygis", + "solid-angle": "Erdvinis kampas", + "specific-energy": "Specifinė energija", + "specific-heat-capacity": "Specifinė šiluminė talpa", + "specific-humidity": "Specifinė drėgmė", + "specific-volume": "Specifinis tūris", + "speed": "Greičio", + "surface-charge-density": "Paviršiaus krūvio tankis", + "surface-tension": "Paviršiaus įtempimas", + "temperature": "Temperatūra", + "thermal-conductivity": "Šilumos laidumas", + "time": "Laikas", + "torque": "Sukimo momentas", + "turbidity": "Drumstumas", + "voltage": "Įtampa", + "volume": "Tūris", + "volume-flow": "Tūrio srautas" + }, + "millimeter": "Milimetras", + "centimeter": "Centimetras", + "decimeter": "Decimetras", + "angstrom": "Angstromas", + "nanometer": "Nanometras", + "micrometer": "Mikrometras", + "meter": "Metras", + "kilometer": "Kilometras", + "inch": "Colis", + "foot": "Pėda", + "foot-us": "Pėda (JAV matavimas)", + "yard": "Jardas", + "mile": "Mylia", + "nautical-mile": "Jūrmylė", + "astronomical-unit": "Astronominis vienetas", + "reciprocal-metre": "Atvirkštinis metras", + "meter-per-meter": "Metrai vienam metrui", + "steradian": "Steradianas", + "thou": "Tausas", + "barleycorn": "Miežio grūdas", + "hand": "Rankos ilgis", + "chain": "Grandinė", + "furlong": "Furlongas", + "league": "Lygos vienetas", + "fathom": "Sazenas", + "cable": "Kabelis", + "link": "Nuoroda", + "rod": "Lazda", + "nanogram": "Nanogramas", + "microgram": "Mikrogramas", + "milligram": "Miligramas", + "gram": "Gramas", + "kilogram": "Kilogramas", + "tonne": "Tona", + "ounce": "Uncija", + "pound": "Svaras", + "stone": "Akmuo (matavimo vienetas)", + "hundredweight-count": "Svorinė šimtinė", + "short-tons": "Trumpa tona", + "dalton": "Daltonas", + "grain": "Grūdas", + "drachm": "Drachma", + "quarter": "Ketvirtis", + "slug": "Slugas", + "carat": "Karat", + "cubic-millimeter": "Kubinis milimetras", + "cubic-centimeter": "Kubinis centimetras", + "cubic-meter": "Kubinis metras", + "cubic-kilometer": "Kubinis kilometras", + "microliter": "Mikrolitras", + "milliliter": "Mililitras", + "liter": "Litras", + "hectoliter": "Hektolitras", + "cubic-inch": "Kubinis colis", + "cubic-foot": "Kubinė pėda", + "cubic-yard": "Kubinis jardas", + "fluid-ounce": "Skysčio uncija", + "fluid-ounce-per-second": "Skysčio uncija per sekundę", + "pint": "Pinta", + "quart": "Kvorta", + "gallon": "Galonas", + "oil-barrels": "Naftos statinė", + "cubic-meter-per-kilogram": "Kubinis metras kilogramui", + "gill": "Gilas", + "hogshead": "Statinė (Hogshead)", + "teaspoon": "Arbatinis šaukštelis", + "tablespoon": "Valgomasis šaukštas", + "cup": "Puodelis", + "celsius": "Celsijus", + "kelvin": "Kelvinas", + "rankine": "Rankinas", + "fahrenheit": "Farenheitas", + "percent": "Procentas", + "meter-per-second": "Metrai per sekundę", + "kilometer-per-hour": "Kilometrai per valandą", + "foot-per-second": "Pėdos per sekundę", + "foot-per-minute": "Pėdos per minutę", + "mile-per-hour": "Mylios per valandą", + "knot": "Mazgas", + "inch-per-second": "Coliai per sekundę", + "inch-per-hour": "Coliai per valandą", + "millimeters-per-minute": "Milimetrai per minutę", + "meter-per-minute": "Metrai per minutę", + "kilometer-per-hour-squared": "Kilometrai per valandą kvadratu", + "foot-per-second-squared": "Pėdos per sekundę kvadratu", + "pascal": "Paskalis", + "kilopascal": "Kilopaskalis", + "megapascal": "Megapaskalis", + "gigapascal": "Gigapaskalis", + "millibar": "Milibaras", + "bar": "Baras", + "kilobar": "Kilobaras", + "newton": "Niutonas", + "newton-meter": "Niutono metras", + "foot-pounds": "Pėdos-svarai", + "inch-pounds": "Colio-svarai", + "newton-per-meter": "Niutonas metrui", + "atmospheres": "Atmosferos", + "pounds-per-square-inch": "Svarai kvadratiniam coliui", + "kilopound-per-square-inch": "Kilosvarai kvadratiniam coliui", + "torr": "Torras", + "inches-of-mercury": "Gyvsidabrio coliai", + "pascal-per-square-meter": "Paskalis kvadratiniam metrui", + "pound-per-square-inch": "Svoris kvadratiniam coliui", + "newton-per-square-meter": "Niutonas kvadratiniam metrui", + "kilogram-force-per-square-meter": "Kilogramo jėga kvadratiniam metrui", + "pascal-per-square-centimeter": "Paskalis kvadratiniam centimetrui", + "ton-force-per-square-inch": "Tonos jėga kvadratiniam coliui", + "kilonewton-per-square-meter": "Kiloniutonas kvadratiniam metrui", + "newton-per-square-millimeter": "Niutonas kvadratiniam milimetrui", + "microjoule": "Mikrodžiaulis", + "millijoule": "Milidžiaulis", + "joule": "Džiaulis", + "kilojoule": "Kilodžiaulis", + "megajoule": "Megadžiaulis", + "gigajoule": "Gigadžiaulis", + "watt-hour": "Vatvalandė", + "watt-minute": "Vatminutė", + "kilowatt-hour": "Kilovatvalandė", + "milliwatt-hour": "Milivatvalandė", + "megawatt-hour": "Megavatvalandė", + "gigawatt-hour": "Gigavatvalandė", + "electron-volts": "Elektronvoltai", + "joules-per-coulomb": "Džiauliai kulonui", + "british-thermal-unit": "Britų šiluminis vienetas (BTU)", + "thousand-british-thermal-unit": "Tūkstantis britų šiluminių vienetų", + "million-british-thermal-unit": "Milijonas britų šiluminių vienetų", + "foot-pound": "Pėdos-svaras", + "calorie": "Kalorija", + "small-calorie": "Mažoji kalorija", + "kilocalorie": "Kilokalorija", + "joule-per-kelvin": "Džiaulis kelvinui", + "joule-per-kilogram-kelvin": "Džiaulis kilogramui-kelvinui", + "joule-per-kilogram": "Džiaulis kilogramui", + "watt-per-meter-kelvin": "Vatas metrui-kelvinui", + "joule-per-cubic-meter": "Džiaulis kubiniam metrui", + "therm": "Termas", + "electric-dipole-moment": "Elektrinis dipolio momentas", + "magnetic-dipole-moment": "Magnetinis dipolio momentas", + "debye": "Debajus", + "coulomb-per-square-meter-per-volt": "Kulonas kvadratiniam metrui voltui", + "milliwatt": "Miliwatas", + "microwatt": "Mikrovatas", + "watt": "Vatas", + "kilowatt": "Kilovatas", + "megawatt": "Megavatas", + "gigawatt": "Gigavatas", + "metric-horsepower": "Metinė arklio galia", + "milliwatt-per-square-centimeter": "Milivatai kvadratiniam centimetrui", + "watt-per-square-centimeter": "Vatai kvadratiniam centimetrui", + "kilowatt-per-square-centimeter": "Kilovatai kvadratiniam centimetrui", + "milliwatt-per-square-meter": "Milivatai kvadratiniam metrui", + "watt-per-square-meter": "Vatai kvadratiniam metrui", + "kilowatt-per-square-meter": "Kilovatai kvadratiniam metrui", + "watt-per-square-inch": "Vatai kvadratiniam coliui", + "kilowatt-per-square-inch": "Kilovatai kvadratiniam coliui", + "horsepower": "Arklio galia", + "btu-per-hour": "Britų šiluminiai vienetai per valandą", + "btu-per-second": "Britų šiluminiai vienetai per sekundę", + "btu-per-day": "Britų šiluminiai vienetai per dieną", + "mbtu-per-hour": "Tūkstantis britų šiluminių vienetų per valandą", + "mbtu-per-second": "Tūkstantis britų šiluminių vienetų per sekundę", + "mbtu-per-day": "Tūkstantis britų šiluminių vienetų per dieną", + "mmbtu-per-hour": "Milijonas britų šiluminių vienetų per valandą", + "mmbtu-per-second": "Milijonas britų šiluminių vienetų per sekundę", + "mmbtu-per-day": "Milijonas britų šiluminių vienetų per dieną", + "foot-pound-per-second": "Pėdos-svarai per sekundę", + "coulomb": "Kulonas", + "millicoulomb": "Milikulonas", + "microcoulomb": "Mikrokulonas", + "nanocoulomb": "Nanokulonas", + "picocoulomb": "Pikokulonas", + "coulomb-per-meter": "Kulonai metrui", + "coulomb-per-cubic-meter": "Kulonai kubiniam metrui", + "coulomb-per-square-meter": "Kulonai kvadratiniam metrui", + "square-millimeter": "Kvadratinis milimetras", + "square-centimeter": "Kvadratinis centimetras", + "square-meter": "Kvadratinis metras", + "hectare": "Hektaras", + "square-kilometer": "Kvadratinis kilometras", + "square-inch": "Kvadratinis colis", + "square-foot": "Kvadratinė pėda", + "square-yard": "Kvadratinis jardas", + "acre": "Akras", + "square-mile": "Kvadratinė mylia", + "are": "Aras", + "barn": "Barnas", + "circular-inch": "Apskritiminis colis", + "milliampere-hour": "Miliampervalandė", + "ampere-hours": "Ampervalandės", + "kiloampere-hours": "Kiloampervalandės", + "nanoampere": "Nanoamperas", + "picoampere": "Pikoamperas", + "microampere": "Mikroamperas", + "milliampere": "Miliamperas", + "ampere": "Amperas", + "kiloampere": "Kiloamperas", + "megaampere": "Megaamperas", + "gigaampere": "Gigaamperas", + "microampere-per-square-centimeter": "Mikroamperai kvadratiniam centimetrui", + "ampere-per-square-meter": "Amperai kvadratiniam metrui", + "ampere-per-meter": "Amperai metrui", + "oersted": "Erstedas", + "bohr-magneton": "Boro magnetonas", + "ampere-meter-squared": "Ampermetrai kvadratu", + "nanovolt": "Nanovoltas", + "picovolt": "Pikovoltas", + "millivolt": "Milivoltas", + "microvolt": "Mikrovoltas", + "volt": "Voltas", + "kilovolt": "Kilovoltas", + "megavolt": "Megavoltas", + "dbmV": "dBmV", + "dbm": "Decibelmilivatai", + "volt-meter": "Voltmetras", + "kilovolt-meter": "Kilovoltmetras", + "megavolt-meter": "Megavoltmetras", + "microvolt-meter": "Mikrovoltmetras", + "millivolt-meter": "Milivoltmetras", + "nanovolt-meter": "Nanovoltmetras", + "ohm": "Omas", + "microohm": "Mikroomas", + "milliohm": "Milioomas", + "kilohm": "Kiloomas", + "megohm": "Megaomas", + "gigohm": "Gigaomas", + "millihertz": "Milihercas", + "hertz": "Hercas", + "kilohertz": "Kilohercas", + "megahertz": "Megahercas", + "gigahertz": "Gigahercas", + "terahertz": "Terahercas", + "rpm": "Apsisukimai per minutę", + "candela-per-square-meter": "Kandela kvadratiniam metrui", + "candela": "Kandela", + "lumen": "Liumenas", + "lux": "Liuksas", + "foot-candle": "Pėdos žvakė", + "lumen-per-square-meter": "Liumenai kvadratiniam metrui", + "lux-second": "Liuksas sekundei", + "lumen-second": "Liumenas sekundei", + "lumens-per-watt": "Liumenai vatui", + "mole": "Molis", + "nanomole": "Nanomolis", + "micromole": "Mikromolis", + "millimole": "Milimolis", + "kilomole": "Kilomolis", + "mole-per-cubic-meter": "Moliai kubiniam metrui", + "rssi": "RSSI (signalo stiprumas)", + "ppm": "Dalis milijone (ppm)", + "ppb": "Dalis milijarde (ppb)", + "micrograms-per-cubic-meter": "Mikrogramai kubiniam metrui", + "aqi": "Oro kokybės indeksas (OKI)", + "gram-per-cubic-meter": "Gramai kubiniam metrui", + "gram-per-kilogram": "Specifinis drėgnis (g/kg)", + "millimeters-per-second": "Milimetrai per sekundę", + "neper": "Neperas", + "bel": "Belas", + "decibel": "Decibelas", + "meters-per-second-squared": "Metrai per sekundę kvadratu", + "becquerel": "Bekerelis", + "curie": "Kiuris", + "gray": "Grėjus", + "sievert": "Zyvertas", + "roentgen": "Rentgenas", + "cps": "Impulsai per sekundę", + "rad": "Radas", + "rem": "Remas", + "dps": "Skaidymasis per sekundę", + "rutherford": "Ruterfordas", + "coulombs-per-kilogram": "Kulonai kilogramui", + "becquerels-per-cubic-meter": "Bekereliai kubiniam metrui", + "curies-per-liter": "Kiuriai litrui", + "becquerels-per-second": "Bekereliai per sekundę", + "curies-per-second": "Kiuriai per sekundę", + "gy-per-second": "Grėjai per sekundę", + "watt-per-steradian": "Vatai steradianui", + "watt-per-square-metre-steradian": "Vatai kvadratiniam metrui-steradianui", + "ph-level": "pH lygis", + "turbidity": "Drumstumas", + "mg-per-liter": "Miligramai litrui", + "microsiemens-per-centimeter": "Mikrosiemensai centimetrui", + "millisiemens-per-meter": "Milisiemensai metrui", + "siemens-per-meter": "Siemensai metrui", + "kilogram-per-cubic-meter": "Kilogramai kubiniam metrui", + "gram-per-cubic-centimeter": "Gramai kubiniam centimetrui", + "kilogram-per-square-meter": "Kilogramai kvadratiniam metrui", + "milligram-per-milliliter": "Miligramai mililitrui", + "milligram-per-cubic-meter": "Miligramai kubiniam metrui", + "pound-per-cubic-foot": "Svarai kubinei pėdai", + "ounces-per-cubic-inch": "Uncijos kubiniam coliui", + "tons-per-cubic-yard": "Tonos kubiniam jardui", + "particle-density": "Dalinė tankio vertė", + "kilometers-per-liter": "Kilometrai litrui", + "miles-per-gallon": "Mylios galonui", + "liters-per-100-km": "Litrai 100 kilometrų", + "gallons-per-mile": "Galonai myliai", + "liters-per-hour": "Litrai per valandą", + "gallons-per-hour": "Galonai per valandą", + "beats-per-minute": "Dūžiai per minutę", + "millimeters-of-mercury": "Gyvsidabrio milimetrai", + "milligrams-per-deciliter": "Miligramai decilitrui", + "g-force": "G-jėga", + "kilonewton": "Kiloniutonas", + "kilogram-force": "Kilogramo jėga", + "pound-force": "Svarinė jėga", + "kilopound-force": "Kilosvarinė jėga", + "dyne": "Dina", + "poundal": "Paundalas", + "kip": "Kipas", + "gal": "Galas", + "gravity": "Gravitacija", + "hectopascal": "Hektopaskalis", + "atmosphere": "Atmosfera", + "millibars": "Milibarai", + "inch-of-mercury": "Gyvsidabrio colis", + "richter-scale": "Richtėro skalė", + "nanosecond": "Nanosekundė", + "microsecond": "Mikrosekundė", + "millisecond": "Milisekundė", + "second": "Sekundė", + "minute": "Minutė", + "hour": "Valanda", + "day": "Diena", + "week": "Savaitė", + "month": "Mėnuo", + "year": "Metai", + "cubic-foot-per-minute": "Kubinės pėdos per minutę", + "cubic-meters-per-hour": "Kubiniai metrai per valandą", + "cubic-meters-per-second": "Kubiniai metrai per sekundę", + "liter-per-second": "Litrai per sekundę", + "liter-per-minute": "Litrai per minutę", + "gallons-per-minute": "Galonai per minutę", + "cubic-foot-per-second": "Kubinė pėda per sekundę", + "milliliters-per-minute": "Mililitrai per minutę", + "cubic-decimeter-per-second": "Kubinis decimetras per sekundę", + "bit": "Bitas", + "byte": "Baitas", + "kilobyte": "Kilobaitas", + "megabyte": "Megabaitas", + "gigabyte": "Gigabaitas", + "terabyte": "Terabaitas", + "petabyte": "Petabaitas", + "exabyte": "Eksabaitas", + "zettabyte": "Zetabaitas", + "yottabyte": "Jotabaitas", + "bit-per-second": "Bitai per sekundę", + "kilobit-per-second": "Kilobitai per sekundę", + "megabit-per-second": "Megabitai per sekundę", + "gigabit-per-second": "Gigabitai per sekundę", + "terabit-per-second": "Terabitai per sekundę", + "byte-per-second": "Baitai per sekundę", + "kilobyte-per-second": "Kilobaitai per sekundę", + "megabyte-per-second": "Megabaitai per sekundę", + "gigabyte-per-second": "Gigabaitai per sekundę", + "degree": "Laipsnis", + "radian": "Radianas", + "gradian": "Gradianas", + "arcminute": "Kampinė minutė", + "arcsecond": "Kampinė sekundė", + "milliradian": "Miliradianas", + "revolution": "Apsisukimas", + "siemens": "Siemensas", + "millisiemens": "Milisiemensas", + "microsiemens": "Mikrosiemensas", + "kilosiemens": "Kilosiemensas", + "megasiemens": "Megasiemensas", + "gigasiemens": "Gigasiemensas", + "farad": "Faradas", + "millifarad": "Milifaradas", + "microfarad": "Mikrofaradas", + "nanofarad": "Nanofaradas", + "picofarad": "Pikofaradas", + "kilofarad": "Kilofaradas", + "megafarad": "Megafaradas", + "gigafarad": "Gigafaradas", + "terfarad": "Terafaradas", + "farad-per-meter": "Faradai metrui", + "tesla": "Tesla", + "gauss": "Gausas", + "kilogauss": "Kilogausas", + "millitesla": "Militesla", + "microtesla": "Mikrotesla", + "nanotesla": "Nanotesla", + "kilotesla": "Kilotesla", + "megatesla": "Megatesla", + "millitesla-square-meters": "Militeslos kvadratiniai metrai", + "gamma": "Gama", + "lambda": "Lambda", + "square-meter-per-second": "Kvadratiniai metrai per sekundę", + "square-centimeter-per-second": "Kvadratiniai centimetrai per sekundę", + "stoke": "Stokas", + "centistokes": "Centistokai", + "square-foot-per-second": "Kvadratinės pėdos per sekundę", + "square-inch-per-second": "Kvadratiniai coliai per sekundę", + "pascal-second": "Paskalų sekundė", + "centipoise": "CentiPoisas", + "poise": "Poisas", + "reynolds": "Reinoldso skaičius", + "pound-per-foot-hour": "Svarai pėdai per valandą", + "newton-second-per-square-meter": "Niutono sekundė kvadratiniam metrui", + "dyne-second-per-square-centimeter": "Dinos sekundė kvadratiniam centimetrui", + "kilogram-per-meter-second": "Kilogramai metrui per sekundę", + "tesla-square-meters": "Teslos kvadratiniai metrai", + "maxwell": "Maksvelas", + "tesla-per-meter": "Teslos metrui", + "gauss-per-centimeter": "Gausai centimetrui", + "weber": "Veberis", + "microweber": "Mikroveberis", + "milliweber": "Miliveberis", + "gauss-square-centimeter": "Gausai kvadratiniam centimetrui", + "kilogauss-square-centimeter": "Kilogausai kvadratiniam centimetrui", + "henry": "Henris", + "millihenry": "Milihenris", + "microhenry": "Mikrohenris", + "nanohenry": "Nanohenris", + "henry-per-meter": "Henriai metrui", + "tesla-meter-per-ampere": "Teslos metras amperui", + "gauss-per-oersted": "Gausai erstedui", + "kilogram-per-mole": "Kilogramai moliui", + "gram-per-mole": "Gramai moliui", + "milligram-per-mole": "Miligramai moliui", + "joule-per-mole": "Džiauliai moliui", + "joule-per-mole-kelvin": "Džiauliai moliui-kelvinui", + "millivolts-per-meter": "Milivoltai metrui", + "volts-per-meter": "Voltai metrui", + "kilovolts-per-meter": "Kilovoltai metrui", + "radian-per-second": "Radianai per sekundę", + "radian-per-second-squared": "Radianai per sekundę kvadratu", + "revolutions-per-minute-per-second": "Apsisukimai per minutę per sekundę", + "deg-per-second": "laipsn./s", + "rotation-per-minute": "Apsisukimai per minutę", + "degrees-brix": "Brix laipsniai", + "katal": "Katalis", + "katal-per-cubic-metre": "Kataliai kubiniam metrui", + "paris-inch": "Paryžiaus colis" + }, + "user": { + "user": "Vartotojas", + "users": "Vartotojai", + "customer-users": "Kliento vartotojai", + "tenant-admins": "Valdytojų administratoriai", + "sys-admin": "Sistemos administratoriai", + "tenant-admin": "Valdytojo administratoriai", + "customer": "Klientas", + "anonymous": "Anonimas", + "add": "Pridėti vartotoją", + "delete": "Panaikinti vartotoją", + "add-user-text": "Pridėti naują vartotoją", + "no-users-text": "Vartotojų nėra", + "user-details": "Informacija apie vartotoją", + "delete-user-title": "Ar tikrai norite panaikinti vartotoją '{{userEmail}}'?", + "delete-user-text": "Būkite dėmesingi – po patvirtinimo vartotojas ir visa su juo susijusi informacija bus panaikinta.", + "delete-users-title": "Ar tikrai norite panaikinti { count, plural, =1 {1 vartotoją} other {# vartotojus} }?", + "delete-users-action-title": "Panaikinti { count, plural, =1 {1 vartotoją} other {# vartotojus} }", + "delete-users-text": "Būkite dėmesingi – po patvirtinimo visi pasirinkti vartotojai ir su jais susijusi informacija bus panaikinta.", + "activation-email-sent-message": "Aktyvinimo el. laiškas sėkmingai išsiųstas!", + "resend-activation": "Iš naujo siųsti aktyvavimo laišką", + "email": "El. pašto adresas", + "email-required": "El. pašto adresas būtinas.", + "invalid-email-format": "Neteisingas el. pašto adreso formatas.", + "first-name": "Vardas", + "last-name": "Pavardė", + "description": "Aprašymas", + "default-dashboard": "Pagrindinis skydelis", + "always-fullscreen": "Visada per visą ekraną", + "select-user": "Pasirinkti vartotoją", + "no-users-matching": "Vartotojų, atitinkančių '{{entity}}', nėra.", + "user-required": "Vartotojas būtinas", + "activation-method": "Aktyvavimo būdas", + "display-activation-link": "Rodyti aktyvavimo nuorodą", + "send-activation-mail": "Siųsti aktyvavimo el. laišką", + "activation-link": "Vartotojo aktyvavimo nuoroda", + "activation-link-text": "Norėdami aktyvuoti vartotoją, paspauskite nuorodą:", + "copy-activation-link": "Kopijuoti aktyvavimo nuorodą", + "activation-link-copied-message": "Vartotojo aktyvavimo nuoroda nukopijuota į iškarpinę", + "details": "Informacija", + "login-as-tenant-admin": "Prisijungti kaip valdytojo administratorius", + "login-as-customer-user": "Prisijungti kaip kliento vartotojas", + "search": "Vartotojų paieška", + "selected-users": "Pasirinkta { count, plural, =1 {1 vartotojas} other {# vartotojai} }", + "disable-account": "Išjungti vartotojo paskyrą", + "enable-account": "Įjungti vartotojo paskyrą", + "enable-account-message": "Vartotojo paskyra sėkmingai įjungta!", + "disable-account-message": "Vartotojo paskyra sėkmingai išjungta!", + "copyId": "Kopijuoti vartotojo ID", + "idCopiedMessage": "Vartotojo ID nukopijuotas į iškarpinę", + "user-list": "Vartotojų sąrašas", + "user-list-required": "Vartotojų sąrašas būtinas" + }, + "value": { + "type": "Reikšmės tipas", + "string": "Tekstas", + "string-value": "Tekstinė informacija", + "string-value-required": "Tekstinė informacija būtina", + "integer": "Sveikasis skaičius", + "integer-value": "Sveikojo skaičiaus reikšmė", + "integer-value-required": "Sveikojo skaičiaus reikšmė būtina", + "invalid-integer-value": "Sveikojo skaičiaus reikšmė neteisinga", + "double": "Realusis skaičius", + "double-value": "Realiojo skaičiaus reikšmė", + "double-value-required": "Realiojo skaičiaus reikšmė būtina", + "boolean": "Loginis", + "boolean-value": "Loginė reikšmė", + "false": "Netiesa", + "true": "Tiesa", + "long": "Sveikas skaičius", + "json": "JSON", + "json-value": "JSON reikšmė", + "json-value-invalid": "Neteisingas JSON reikšmės formatas", + "json-value-required": "JSON reikšmė būtina." + }, + "version-control": { + "version-control": "Versijų valdymas", + "management": "Versijų valdymo administravimas", + "search": "Ieškoti versijų", + "branch": "Šaka", + "default": "Numatytoji", + "select-branch": "Pasirinkite šaką", + "branch-required": "Šaka būtina", + "create-entity-version": "Sukurti subjekto versiją", + "version-name": "Versijos pavadinimas", + "version-name-required": "Versijos pavadinimas būtinas", + "author": "Autorius", + "export-relations": "Eksportuoti ryšius", + "export-attributes": "Eksportuoti atributus", + "export-credentials": "Eksportuoti įgaliojimus", + "export-calculated-fields": "Eksportuoti skaičiuojamus laukus", + "entity-versions": "Subjektų versijos", + "versions": "Versijos", + "created-time": "Sukūrimo laikas", + "version-id": "Versijos ID", + "no-entity-versions-text": "Subjektų versijų nerasta", + "no-versions-text": "Versijų nerasta", + "copy-full-version-id": "Kopijuoti pilną versijos ID", + "create-version": "Sukurti versiją", + "creating-version": "Kuriama versija... Prašome palaukti", + "nothing-to-commit": "Nėra pakeitimų, kuriuos galima įkelti", + "restore-version": "Atkurti versiją", + "restore-entity-from-version": "Atkurti subjektą iš versijos '{{versionName}}'", + "restoring-entity-version": "Atkuriama subjekto versija... Prašome palaukti", + "load-relations": "Įkelti ryšius", + "load-attributes": "Įkelti atributus", + "load-credentials": "Įkelti įgaliojimus", + "load-calculated-fields": "Įkelti skaičiuojamus laukus", + "compare-with-current": "Palyginti su dabartine", + "diff-entity-with-version": "Palyginti su subjekto versija '{{versionName}}'", + "previous-difference": "Ankstesnis skirtumas", + "next-difference": "Kitas skirtumas", + "current": "Dabartinė", + "differences": "{ count, plural, =1 {1 skirtumas} other {# skirtumai} }", + "create-entities-version": "Sukurti subjektų versiją", + "default-sync-strategy": "Numatytoji sinchronizavimo strategija", + "sync-strategy-merge": "Sujungti (Merge)", + "sync-strategy-overwrite": "Perrašyti (Overwrite)", + "entities-to-export": "Subjektai eksportui", + "entities-to-restore": "Subjektai atkūrimui", + "sync-strategy": "Sinchronizavimo strategija", + "all-entities": "Visi subjektai", + "no-entities-to-export-prompt": "Nurodykite subjektus, kuriuos norite eksportuoti", + "no-entities-to-restore-prompt": "Nurodykite subjektus, kuriuos norite atkurti", + "add-entity-type": "Pridėti subjekto tipą", + "remove-all": "Pašalinti visus", + "version-create-result": "{ added, plural, =0 {Subjektų nepridėta} =1 {1 subjektas pridėtas} other {# subjektai pridėti} }.
    { modified, plural, =0 {Neatnaujinta nė vieno subjekto} =1 {1 subjektas atnaujintas} other {# subjektai atnaujinti} }.
    { removed, plural, =0 {Neištrinta nė vieno subjekto} =1 {1 subjektas pašalintas} other {# subjektai pašalinti} }.", + "remove-other-entities": "Pašalinti kitus subjektus", + "find-existing-entity-by-name": "Rasti esamą subjektą pagal pavadinimą", + "restore-entities-from-version": "Atkurti subjektus iš versijos '{{versionName}}'", + "restoring-entities-from-version": "Atkuriami subjektai... Prašome palaukti", + "no-entities-restored": "Neatkurtas nė vienas subjektas", + "created": "{{created}} sukurta", + "updated": "{{updated}} atnaujinta", + "deleted": "{{deleted}} ištrinta", + "remove-other-entities-confirm-text": "Būkite atsargūs! Tai negrįžtamai ištrins visus dabartinius subjektus,
    kurių nėra atkuriamoje versijoje.

    Įveskite „remove other entities“, kad patvirtintumėte.", + "auto-commit-to-branch": "automatiškai įkelti į šaką {{ branch }}", + "default-create-entity-version-name": "{{entityName}} atnaujinimas", + "sync-strategy-merge-hint": "Sukuria arba atnaujina pasirinktus subjektus saugykloje. Kiti saugyklos subjektai neliečiami.", + "sync-strategy-overwrite-hint": "Sukuria arba atnaujina pasirinktus subjektus saugykloje. Visi kiti saugyklos subjektai bus ištrinti.", + "device-credentials-conflict": "Nepavyko įkelti įrenginio su išoriniu ID {{entityId}}
    nes tie patys įgaliojimai jau egzistuoja kitam įrenginiui duomenų bazėje.
    Apsvarstykite galimybę išjungti įkelti įgaliojimus nustatymą atkūrimo formoje.", + "missing-referenced-entity": "Nepavyko įkelti {{sourceEntityTypeName}} su išoriniu ID {{sourceEntityId}}
    nes jis nurodo neegzistuojantį {{targetEntityTypeName}} su ID {{targetEntityId}}.", + "runtime-failed": "Klaida: {{message}}", + "auto-commit-settings-read-only-hint": "Automatinio įkėlimo funkcija neveikia, kai saugykla nustatyta tik skaitymui.", + "rollback-on-error": "Atšaukti pakeitimus klaidos atveju", + "rollback-on-error-hint": "Jei atkuriate didelį subjektų kiekį, galite išjungti šią parinktį našumui padidinti.\nAtminkite: jei atkūrimo metu įvyks klaida, jau išsaugoti subjektai (su ryšiais, atributais ir kt.) liks nepakitę." + }, + "widget": { + "widget-library": "Valdiklių galerija", + "widget-bundle": "Valdiklių rinkinys", + "all-bundles": "Visi rinkiniai", + "select-widgets-bundle": "Pasirinkti valdiklių rinkinį", + "widgets": "Valdikliai", + "all-widgets": "Visi valdikliai", + "widget": "Valdiklis", + "select-widget": "Pasirinkti valdiklį", + "no-widgets-matching": "Valdiklių atitinkančių '{{entity}}' nėra.", + "no-widgets": "Valdiklių dar nėra", + "no-widgets-text": "Valdiklių nėra", + "management": "Valdiklių valdymas", + "editor": "Valdiklių redaktorius", + "confirm-to-exit-editor-html": "Turite neišsaugotų valdiklio nustatymų.
    Ar tikrai norite palikti šį puslapį?", + "widget-type-not-found": "Įkeliant valdiklio konfigūraciją įvyko klaida.
    Gali būti, kad šis valdiklio tipas buvo pašalintas.", + "widget-type-load-error": "Valdiklis neįkeltas dėl klaidų:", + "remove": "Panaikinti valdiklį", + "delete": "Panaikinti valdiklį", + "edit": "Redaguoti valdiklį", + "remove-widget-title": "Ar tikrai norite panaikinti valdiklį '{{widgetTitle}}'?", + "remove-widget-text": "Būkite dėmesingi — po patvirtinimo valdiklis ir visa su juo susijusi informacija bus pašalinta.", + "replace-reference-with-widget-copy": "Pakeisti nuorodą valdiklio kopija", + "timeseries": "Telemetrija", + "search-data": "Duomenų paieška", + "no-data-found": "Duomenų nerasta", + "latest": "Naujausios reikšmės", + "rpc": "Valdymo valdiklis", + "alarm": "Įspėjimų valdiklis", + "static": "Statinis valdiklis", + "timeseries-short": "telemetrija", + "latest-short": "naujausios", + "rpc-short": "valdymas", + "alarm-short": "įspėjimas", + "static-short": "statinis", + "select-widget-type": "Pasirinkite valdiklio tipą", + "missing-widget-title-error": "Valdiklio pavadinimas būtinas!", + "widget-saved": "Valdiklis išsaugotas", + "unable-to-save-widget-error": "Valdiklio išsaugoti nepavyko! Yra klaidų konfigūracijoje.", + "save": "Išsaugoti valdiklį", + "saveAs": "Išsaugoti valdiklį kaip", + "move": "Perkelti valdiklį", + "save-widget-as": "Išsaugoti valdiklį kaip", + "save-widget-as-text": "Įveskite naują valdiklio pavadinimą", + "toggle-fullscreen": "Perjungti į viso ekrano režimą", + "run": "Paleisti valdiklį", + "widget-title": "Valdiklio pavadinimas", + "title": "Pavadinimas", + "title-required": "Valdiklio pavadinimas būtinas.", + "title-max-length": "Pavadinimas negali viršyti 256 simbolių.", + "system": "Sisteminis", + "type": "Tipas", + "resources": "Resursai", + "resource-url": "JavaScript/CSS URL", + "resource-is-extension": "Yra plėtinys", + "remove-resource": "Pašalinti resursą", + "add-resource": "Pridėti resursą", + "html": "HTML", + "tidy": "Tvarkyti (Tidy)", + "css": "CSS", + "settings-form": "Nustatymų forma", + "data-key-settings-form": "Duomenų rakto nustatymų forma", + "latest-data-key-settings-form": "Naujausių duomenų rakto nustatymų forma", + "widget-settings": "Valdiklio nustatymai", + "description": "Aprašymas", + "tags": "Žymos", + "image-preview": "Paveiksliuko peržiūra", + "settings-form-selector": "Nustatymų formos pasirinkiklis", + "data-key-settings-form-selector": "Duomenų rakto nustatymų formos pasirinkiklis", + "latest-data-key-settings-form-selector": "Naujausių duomenų rakto nustatymų formos pasirinkiklis", + "all": "Visi", + "actual": "Esamas", + "scada": "SCADA simbolis", + "deprecated": "Pasenęs", + "has-basic-mode": "Turi bazinį režimą", + "basic-mode-form-selector": "Bazinio režimo formos pasirinkiklis", + "basic-mode": "Bazinė versija", + "advanced-mode": "Pažangi versija", + "javascript": "JavaScript", + "js": "JS", + "delete-widget-title": "Ar tikrai norite panaikinti valdiklį '{{widgetName}}'?", + "delete-widget-text": "Po patvirtinimo valdiklis ir visa susijusi informacija bus negrįžtamai pašalinti.", + "delete-widgets-title": "Ar tikrai norite panaikinti { count, plural, =1 {1 valdiklį} other {# valdiklius} }?", + "delete-widgets-text": "Būkite dėmesingi — po patvirtinimo visi pasirinkti valdikliai ir susijusi informacija bus negrįžtamai pašalinta.", + "delete-widget": "Panaikinti valdiklį", + "widget-template-load-failed-error": "Nepavyko įkelti valdiklio šablono!", + "details": "Informacija", + "widget-details": "Informacija apie valdiklį", + "add": "Pridėti valdiklį", + "add-existing-widget": "Pridėti esamą valdiklį", + "add-new-widget": "Pridėti naują valdiklį", + "search-widgets": "Valdiklių paieška", + "selected-widgets": "Pasirinkta { count, plural, =1 {1 valdiklis} other {# valdikliai} }", + "undo": "Atšaukti valdiklio pakeitimus", + "export": "Eksportuoti valdiklį", + "export-prompt": "Įtraukti valdiklio paveikslėlius ir resursus", + "export-widgets": "Eksportuoti valdiklius", + "export-widgets-prompt": "Įtraukti valdiklių paveikslėlius ir resursus", + "import": "Importuoti valdiklį", + "no-data": "Nėra duomenų atvaizdavimui", + "data-overflow": "Valdiklyje rodoma {{count}} iš {{total}} įrašų", + "alarm-data-overflow": "Valdiklyje rodomi {{allowedEntities}} (maksimalus leistinas skaičius) įspėjimų įrašai iš {{totalEntities}}", + "search": "Valdiklių paieška", + "filter": "Valdiklių filtro tipas", + "loading-widgets": "Įkeliami valdikliai...", + "widget-template-error": "Netinkamas valdiklio HTML šablonas.", + "reference": "Nuoroda" + }, + "widget-action": { + "header-button": "Valdiklio antraštės mygtukas", + "do-nothing": "Nedaryti nieko", + "open-dashboard-state": "Eiti į naują skydelio būseną", + "update-dashboard-state": "Atnaujinti dabartinę skydelio būseną", + "open-dashboard": "Eiti į kitą skydelį", + "custom": "Aprašomas veiksmas", + "custom-pretty": "Aprašomas veiksmas (su HTML šablonu)", + "custom-pretty-error-title": "Klaida vykdant pasirinktą dialogą", + "custom-pretty-template-error": "Netinkamas pasirinktinio dialogo šablonas.", + "custom-pretty-controller-error": "Įvyko klaida vykdant pasirinktinio dialogo funkciją.", + "mobile-action": "Mobilusis veiksmas", + "target-dashboard-state": "Tikslinė skydelio būsena", + "target-dashboard-state-required": "Tikslnė skydelio būsena būtina", + "set-entity-from-widget": "Nustatyti subjektą iš valdiklio", + "target-dashboard": "Tikslinis skydelis", + "select-target-dashboard": "Pasirinkite tikslinį skydelį", + "target-dashboard-required": "Tikslinis skydelis būtinas.", + "open-right-layout": "Atidaryti tinkamą skydelio išdėstymą (mobilusis vaizdas)", + "state-display-type": "Skydelio būsenos rodymo būdas", + "open-normal": "Normalus vaizdas", + "open-in-separate-dialog": "Atidaryti atskirame dialoge", + "open-in-popover": "Atidaryti iškylančiame lange", + "dialog-title": "Dialogo pavadinimas", + "dialog-hide-dashboard-toolbar": "Slėpti skydelio įrankių juostą dialoge", + "dialog-width": "Dialogo plotis (procentais nuo lango pločio)", + "dialog-height": "Dialogo aukštis (procentais nuo lango aukščio)", + "dialog-size-range-error": "Dialogo dydžio reikšmė turi būti nuo 1 iki 100 procentų.", + "popover-preferred-placement": "Pageidaujama iškylančio lango padėtis", + "popover-placement-top": "Viršuje", + "popover-placement-topLeft": "Viršuje kairėje", + "popover-placement-topRight": "Viršuje dešinėje", + "popover-placement-right": "Dešinėje", + "popover-placement-rightTop": "Dešinėje viršuje", + "popover-placement-rightBottom": "Dešinėje apačioje", + "popover-placement-bottom": "Apačioje", + "popover-placement-bottomLeft": "Apačioje kairėje", + "popover-placement-bottomRight": "Apačioje dešinėje", + "popover-placement-left": "Kairėje", + "popover-placement-leftTop": "Kairėje viršuje", + "popover-placement-leftBottom": "Kairėje apačioje", + "popover-hide-on-click-outside": "Slėpti iškylantį langą spustelėjus už ribų", + "popover-hide-dashboard-toolbar": "Slėpti skydelio įrankių juostą iškylančiame lange", + "popover-width": "Iškylančio lango plotis naršyklės vienetais (pvz. 100px, 25vw)", + "popover-height": "Iškylančio lango aukštis naršyklės vienetais (pvz. 100px, 25vh)", + "popover-style": "Iškylančio lango stilius", + "open-new-browser-tab": "Atidaryti naujame naršyklės skirtuke", + "open-URL": "Atidaryti URL", + "URL": "URL", + "url-required": "URL būtinas.", + "mobile": { + "device-provision": "Įrenginio paruošimas", + "action-type": "Mobiliojo veiksmo tipas", + "select-action-type": "Pasirinkite mobiliojo veiksmo tipą", + "action-type-required": "Mobiliojo veiksmo tipas būtinas.", + "take-picture-from-gallery": "Pasirinkti nuotrauką iš galerijos", + "take-photo": "Padaryti nuotrauką", + "map-direction": "Atidaryti maršrutą žemėlapyje", + "map-location": "Atidaryti vietą žemėlapyje", + "scan-qr-code": "Skenuoti QR kodą", + "make-phone-call": "Skambinti telefonu", + "get-location": "Gauti telefono vietą", + "take-screenshot": "Padaryti ekrano nuotrauką" + }, + "custom-action-function": "Pasirinktinio veiksmo funkcija", + "custom-pretty-function": "Pasirinktinio veiksmo (su HTML šablonu) funkcija", + "map-item-type": "Žemėlapio elemento tipas", + "map-item": { + "marker": "Žymeklis", + "polygon": "Daugiakampis", + "rectangle": "Stačiakampis", + "circle": "Apskritimas" + }, + "place-map-item": "Įdėti žemėlapio elementą", + "map-item-tooltip": { + "customize-map-item-tooltips": "Tinkinti žemėlapio elementų paaiškinimus (tooltips)", + "place-marker": "Padėti žymeklį", + "start-draw-rectangle": "Pradėti piešti stačiakampį", + "finish-draw-rectangle": "Baigti piešti stačiakampį", + "start-draw-polygon": "Pradėti piešti daugiakampį", + "continue-draw-polygon": "Tęsti daugiakampio piešimą", + "finish-draw-polygon": "Baigti piešti daugiakampį", + "start-draw-circle": "Pradėti piešti apskritimą", + "finish-draw-circle": "Baigti piešti apskritimą" + } + }, + "widgets-bundle": { + "current": "Dabartinis rinkinys", + "widgets-bundles": "Valdiklių rinkiniai", + "widgets-bundle-widgets": "Valdiklių rinkinio valdikliai", + "add": "Pridėti valdiklių rinkinį", + "delete": "Panaikinti valdiklių rinkinį", + "title": "Pavadinimas", + "title-required": "Pavadinimas būtinas.", + "title-max-length": "Pavadinimas negali viršyti 256 simbolių.", + "description": "Aprašymas", + "image-preview": "Paveiksliuko peržiūra", + "scada": "SCADA valdiklių rinkinys", + "order": "Eiliškumas", + "add-widgets-bundle-text": "Pridėti naują valdiklių rinkinį", + "no-widgets-bundles-text": "Valdiklių rinkinių nėra", + "empty": "Valdiklių rinkinys tuščias", + "details": "Informacija", + "widgets-bundle-details": "Informacija apie valdiklių rinkinį", + "delete-widgets-bundle-title": "Ar tikrai norite panaikinti valdiklių rinkinį '{{widgetsBundleTitle}}'?", + "delete-widgets-bundle-text": "Būkite dėmesingi — po patvirtinimo valdiklių rinkinys ir visa su juo susijusi informacija bus pašalinta.", + "delete-widgets-bundles-title": "Ar tikrai norite panaikinti { count, plural, =1 {1 valdiklių rinkinį} other {# valdiklių rinkinius} }?", + "delete-widgets-bundles-action-title": "Panaikinti { count, plural, =1 {1 valdiklių rinkinį} other {# valdiklių rinkinius} }", + "delete-widgets-bundles-text": "Būkite dėmesingi — po patvirtinimo visi pasirinkti valdiklių rinkiniai ir su jais susijusi informacija bus pašalinti.", + "no-widgets-bundles-matching": "Valdiklių rinkinių, atitinkančių '{{widgetsBundle}}', nėra.", + "widgets-bundle-required": "Valdiklių rinkinys būtinas.", + "system": "Sisteminis", + "import": "Importuoti valdiklių rinkinį", + "export": "Eksportuoti valdiklių rinkinį", + "export-widgets-bundle-widgets-prompt": "Įtraukti valdiklių tipus į eksportuotus duomenis (kitu atveju bus eksportuojami tik nurodytų valdiklių FQN identifikatoriai)", + "export-failed-error": "Valdiklių rinkinio eksportuoti nepavyko: {{error}}", + "create-new-widgets-bundle": "Sukurti naują valdiklių rinkinį", + "widgets-bundle-file": "Valdiklių rinkinio failas", + "invalid-widgets-bundle-file-error": "Valdiklių rinkinio importuoti nepavyko: neteisinga duomenų struktūra.", + "search": "Valdiklių rinkinių paieška", + "selected-widgets-bundles": "Pasirinkta { count, plural, =1 {1 valdiklių rinkinys} other {# valdiklių rinkiniai} }", + "open-widgets-bundle": "Atverti valdiklių rinkinį", + "loading-widgets-bundles": "Įkeliamas valdiklių rinkinys...", + "create-new": "Sukurti naują valdiklių rinkinį" + }, + "widget-config": { + "data": "Duomenys", + "settings": "Nustatymai", + "advanced": "Išplėstiniai nustatymai", + "appearance": "Išvaizda", + "widget-card": "Valdiklio kortelė", + "mobile": "Mobilusis vaizdas", + "title": "Pavadinimas", + "title-tooltip": "Pavadinimo paaiškinimas", + "general-settings": "Pagrindiniai nustatymai", + "display-title": "Rodyti valdiklio pavadinimą", + "card-title": "Kortelės pavadinimas", + "drop-shadow": "Šešėlis", + "enable-fullscreen": "Įjungti rodymą per visą ekraną", + "background-color": "Fono spalva", + "text-color": "Teksto spalva", + "border-radius": "Rėmelio apvalinimas", + "padding": "Vidinis atstumas (padding)", + "margin": "Išorinis atstumas (margin)", + "widget-style": "Valdiklio stilius", + "widget-css": "Valdiklio CSS stilius", + "title-style": "Antraštės stilius", + "mobile-mode-settings": "Mobiliojo režimo nustatymai", + "order": "Rikiavimo tvarka", + "height": "Aukštis", + "mobile-hide": "Slėpti valdiklį mobiliajame režime", + "desktop-hide": "Slėpti valdiklį darbalaukio režime", + "units": "Papildomas simbolis šalia reikšmės", + "units-by-default": "Numatytieji matavimo vienetai", + "decimals": "Skaitmenys po kablelio", + "decimals-by-default": "Numatytieji dešimtainiai skaitmenys", + "default-data-key-parameter-hint": "Šis parametras taikomas visoms valdiklio reikšmėms, nebent jis perrašomas konkretaus duomenų rakto konfigūracijoje", + "units-short": "Vienetai", + "decimals-short": "Dešimtainiai", + "decimals-suffix": "dešimtainiai skaitmenys", + "digits-suffix": "skaitmenys", + "timewindow": "Laikotarpio langas", + "use-dashboard-timewindow": "Naudoti skydelio laikotarpio langą", + "use-widget-timewindow": "Naudoti valdiklio laikotarpio langą", + "display-timewindow": "Rodyti laikotarpio langą", + "legend": "Legenda", + "display-legend": "Rodyti legendą", + "datasources": "Duomenų šaltiniai", + "datasource": "Duomenų šaltinis", + "maximum-datasources": "Daugiausiai leidžiama { count, plural, =1 {1 duomenų šaltinis.} other {# duomenų šaltiniai} }", + "timeseries-key-error": "Reikia nurodyti bent vieną telemetrijos rodiklį", + "datasource-type": "Tipas", + "datasource-parameters": "Parametrai", + "remove-datasource": "Pašalinti duomenų šaltinį", + "add-datasource": "Pridėti duomenų šaltinį", + "target-device": "Tikslinis įrenginys", + "alarm-source": "Įspėjimo šaltinis", + "actions": "Veiksmai", + "action": "Veiksmas", + "add-action": "Pridėti veiksmą", + "search-actions": "Veiksmų paieška", + "no-actions-text": "Veiksmų nėra", + "action-source": "Veiksmo šaltinis", + "select-action-source": "Pasirinkite veiksmo šaltinį", + "action-source-required": "Veiksmo šaltinis būtinas.", + "column-index": "Stulpelio indeksas", + "select-column-index": "Pasirinkite stulpelio indeksą", + "column-index-required": "Stulpelio indeksas būtinas.", + "not-set": "Nenurodyta", + "action-name": "Veiksmo pavadinimas", + "action-name-required": "Veiksmo pavadinimas būtinas.", + "action-name-not-unique": "Veiksmas su tokiu pavadinimu jau egzistuoja.
    To paties šaltinio veiksmų pavadinimai negali kartotis.", + "action-icon": "Piktograma", + "header-button": { + "button-settings": "Mygtuko nustatymai", + "button-type": "Mygtuko tipas", + "button-type-basic": "Paprastas", + "button-type-raised": "Iškeltas (Raised)", + "button-type-stroked": "Kontūrinis (Stroked)", + "button-type-flat": "Plokščias (Flat)", + "button-type-icon": "Piktogramos mygtukas", + "button-type-mini-fab": "Mini FAB (Floating Action Button)", + "colors": "Spalvos", + "color": "Spalva", + "background": "Fonas", + "border": "Rėmelis", + "advanced-button-style": "Išplėstinis mygtuko stilius", + "button-style": "Mygtuko stilius" + }, + "show-hide-action-using-function": "Rodyti / slėpti veiksmą pagal funkciją", + "show-action-function": "Veiksmo rodymo funkcija", + "action-type": "Veiksmo tipas", + "action-type-required": "Veiksmo tipas būtinas.", + "edit-action": "Redaguoti veiksmą", + "delete-action": "Panaikinti veiksmą", + "delete-action-title": "Panaikinti valdiklio veiksmą", + "delete-action-text": "Ar tikrai norite panaikinti valdiklio veiksmą '{{actionName}}'?", + "title-icon": "Antraštės piktograma", + "display-icon": "Rodyti antraštės piktogramą", + "card-icon": "Kortelės piktograma", + "icon": "Piktograma", + "icon-color": "Piktogramos spalva", + "icon-size": "Piktogramos dydis", + "advanced-settings": "Išplėstiniai nustatymai", + "data-settings": "Duomenų nustatymai", + "limits": "Ribos", + "no-data-display-message": "Alternatyvus tekstas, kai nėra duomenų", + "data-page-size": "Maksimalus subjektų skaičius viename duomenų šaltinyje", + "settings-component-not-found": "Nerasta nustatymų formos komponento su selektoriumi '{{selector}}'", + "preview": "Peržiūra", + "set": "Nustatyti", + "set-message": "Nustatyti pranešimą", + "advanced-title-style": "Išplėstinis antraštės stilius", + "card-style": "Kortelės stilius", + "text": "Tekstas", + "background": "Fonas", + "advanced-widget-style": "Išplėstinis valdiklio stilius", + "card-buttons": "Kortelės mygtukai", + "show-card-buttons": "Rodyti kortelės mygtukus", + "card-border-radius": "Kortelės kampų apvalinimas", + "card-padding": "Kortelės vidinis atstumas (padding)", + "card-appearance": "Kortelės išvaizda", + "color": "Spalva", + "tooltip": "Paaiškinimas (tooltip)", + "units-required": "Matavimo vienetas būtinas.", + "list-layout": "Sąrašo išdėstymas", + "layout": "Išdėstymas", + "resize-options": "Dydžio keitimo parinktys", + "resizable": "Keičiamo dydžio", + "preserve-aspect-ratio": "Išlaikyti proporcijas" + }, + "widget-type": { + "import": "Importuoti valdiklį", + "export": "Eksportuoti valdiklį", + "export-failed-error": "Valdiklio eksportuoti nepavyko: {{error}}", + "widget-file": "Valdiklio failas", + "invalid-widget-file-error": "Nepavyko importuoti valdiklio: neteisinga valdiklio duomenų struktūra." + }, + "markdown": { + "edit": "Redaguoti", + "preview": "Peržiūra", + "copy-code": "Spustelėkite, kad nukopijuotumėte", + "copied": "Nukopijuota!" + }, + "widgets": { + "mobile-app-qr-code": { + "configuration-hint": "Konfigūracija priklauso nuo mobiliosios aplikacijos QR kodo valdiklio pagrindiniuose platformos nustatymuose", + "get-it-on-google-play": "Atsisiųsti iš Google Play", + "download-on-the-app-store": "Atsisiųsti iš App Store" + }, + "action-button": { + "behavior": "Elgsena", + "on-click": "Paspaudus", + "on-click-hint": "Veiksmas, kuris įvykdomas paspaudus mygtuką", + "first-button-click": "Pirmas mygtuko paspaudimas", + "first-button-click-hint": "Veiksmas, vykdomas pirmo mygtuko paspaudimo metu.", + "second-button-click": "Antras mygtuko paspaudimas", + "second-button-click-hint": "Veiksmas, vykdomas antro mygtuko paspaudimo metu.", + "button-click-hint": "Veiksmas, vykdomas paspaudus valdiklį." + }, + "command-button": { + "behavior": "Elgsena", + "on-click": "Paspaudus", + "on-click-hint": "Veiksmas, atliekamas paspaudus mygtuką." + }, + "power-button": { + "behavior": "Elgsena", + "power-on": "Įjungti", + "power-on-hint": "Veiksmas, atliekamas įjungiant komponentą.", + "power-off": "Išjungti", + "power-off-hint": "Veiksmas, atliekamas išjungiant komponentą.", + "on-label": "Įjungta", + "off-label": "Išjungta", + "layout": "Išdėstymas", + "layout-default": "Numatytasis", + "layout-simplified": "Supaprastintas", + "layout-outlined": "Aptvertas", + "layout-default-volume": "Numatytasis.Garsas", + "layout-simplified-volume": "Supaprastintas.Garsas", + "layout-outlined-volume": "Aptvertas.Garsas", + "layout-default-icon": "Numatytasis.Piktograma", + "layout-simplified-icon": "Supaprastintas.Piktograma", + "layout-outlined-icon": "Aptvertas.Piktograma", + "main": "Pagrindinis", + "background": "Fonas", + "button-icon-on": "Mygtuko piktograma 'Įjungta'", + "button-icon-off": "Mygtuko piktograma 'Išjungta'", + "power-on-colors": "Įjungimo spalvos", + "power-off-colors": "Išjungimo spalvos", + "disabled-colors": "Išjungtos spalvos", + "button": "Mygtukas" + }, + "toggle-button": { + "behavior": "Elgsena", + "checked": "Pažymėta", + "unchecked": "Nepažymėta", + "check": "Pažymėti", + "check-hint": "Veiksmas, atliekamas pažymint komponentą.", + "uncheck": "Nuimti žymėjimą", + "uncheck-hint": "Veiksmas, atliekamas nuimant žymėjimą nuo komponento.", + "auto-scale": "Automatinis mastelis", + "horizontal-fill": "Horizontalus užpildymas", + "vertical-fill": "Vertikalus užpildymas", + "button-appearance": "Mygtuko išvaizda" + }, + "segmented-button": { + "layout": "Išdėstymas", + "layout-squared": "Kampuotas", + "layout-rounded": "Apvalintas", + "card-border": "Kortelės rėmelis", + "button-appearance": "Mygtuko išvaizda", + "first": "Pirmas", + "second": "Antras", + "color-styles": "Spalvų stiliai", + "selected": "Pasirinkta", + "unselected": "Nepasirinkta" + }, + "button": { + "layout": "Išdėstymas", + "outlined": "Aptvertas", + "filled": "Užpildytas", + "underlined": "Pabrauktas", + "basic": "Paprastas", + "auto-scale": "Automatinis mastelis", + "label": "Etiketė", + "icon": "Piktograma", + "border-radius": "Kampų apvalinimas", + "color-palette": "Spalvų paletė", + "main": "Pagrindinis", + "background": "Fonas", + "border": "Rėmelis", + "custom-styles": "Tinkinti stiliai", + "clear-style": "Išvalyti stilių", + "shadow": "Šešėlis", + "enabled": "Įjungta", + "disabled": "Išjungta", + "preview": "Peržiūra", + "copy-style-from": "Kopijuoti stilių iš" + }, + "value-stepper": { + "behavior": "Elgsena", + "simplified": "Supaprastintas", + "filled": "Užpildytas", + "outlined": "Aptvertas", + "volume": "Garsas", + "initial-state": "Pradinė būsena", + "initial-state-hint": "Veiksmas, skirtas gauti pradinę reikšmę.", + "disabled-state": "Išjungta būsena", + "disabled-state-hint": "Nustatykite sąlygą, kada komponentas yra išjungiamas.", + "right-button-click": "Dešinio mygtuko paspaudimas", + "right-button-click-hint": "Veiksmas paspaudus dešinį mygtuką.", + "left-button-click": "Kairio mygtuko paspaudimas", + "left-button-click-hint": "Veiksmas paspaudus kairį mygtuką.", + "auto-scale": "Automatinis mastelis", + "value-range": "Reikšmių intervalas", + "min-range": "Minimali reikšmė", + "max-range": "Maksimali reikšmė", + "value-increment-decrement-step": "Reikšmės didinimo/mažinimo žingsnis", + "value": "Reikšmė", + "value-box-background": "Reikšmės laukelio fonas", + "border": "Rėmelis", + "button-appearance": "Mygtuko išvaizda", + "left": "Kairė", + "right": "Dešinė", + "left-button": "Kairysis mygtukas", + "right-button": "Dešinysis mygtukas", + "icon": "Piktograma", + "color-palette": "Spalvų paletė", + "main": "Pagrindinis", + "background": "Fonas", + "button-icon-on": "Mygtuko piktograma 'Įjungta'", + "button-on-colors": "Įjungimo spalvos", + "disabled-colors": "Išjungtos spalvos" + }, + "button-state": { + "activated-state": "Aktyvuota būsena", + "activated-state-hint": "Nustatykite sąlygą, kada mygtukas yra aktyvus.", + "disabled-state": "Išjungta būsena", + "disabled-state-hint": "Nustatykite sąlygą, kada mygtukas yra išjungiamas.", + "selected-state": "Pasirinkta būsena", + "selected-state-hint": "Nustatykite sąlygą, kada mygtukas yra pasirinktas.", + "enabled": "Įjungta", + "hovered": "Užvestas", + "pressed": "Paspaustas", + "activated": "Aktyvuotas", + "disabled": "Išjungtas", + "initial": "Pirmasis mygtukas", + "first": "Pirmas", + "second": "Antras" + }, + "background": { + "background": "Fonas", + "background-settings": "Fono nustatymai", + "background-type-image": "Įkelti paveikslėlį", + "background-type-color": "Vientisa spalva", + "image-url": "Paveikslėlio URL", + "overlay": "Perdanga", + "enable-overlay": "Įjungti perdangą", + "blur": "Suliejimas", + "preview": "Peržiūra" + }, + "bar-chart": { + "bar-appearance": "Stulpelio išvaizda", + "label-on-bar": "Etiketė ant stulpelio", + "value-on-bar": "Reikšmė ant stulpelio", + "bar-chart-style": "Stulpelinės diagramos stilius", + "bar-axis": "Stulpelio ašis" + }, + "polar-area-chart": { + "polar-axis": "Polinė ašis", + "start-angle": "Pradinis kampas", + "polar-area-chart-style": "Polinės diagramos stilius" + }, + "battery-level": { + "layout": "Išdėstymas", + "layout-vertical-solid": "Vertikalus. Vientisas", + "layout-horizontal-solid": "Horizontalus. Vientisas", + "layout-vertical-divided": "Vertikalus. Padalintas", + "layout-horizontal-divided": "Horizontalus. Padalintas", + "icon": "Piktograma", + "value": "Reikšmė", + "auto-scale": "Automatinis mastelis", + "battery-level-color": "Baterijos lygio spalva", + "battery-shape-color": "Baterijos formos spalva", + "battery-level-card-style": "Baterijos lygio kortelės stilius", + "sections-count": "Sekcijų skaičius" + }, + "signal-strength": { + "value": "Reikšmė", + "last-update": "Paskutinis atnaujinimas", + "no-signal": "Nėra signalo", + "layout": "Išdėstymas", + "layout-wifi": "Wi-Fi", + "layout-cellular-bar": "Mobiliojo ryšio stulpeliai", + "icon": "Piktograma", + "date": "Data", + "active-bars-color": "Aktyvių signalo stulpelių spalva", + "inactive-bars-color": "Neaktyvių signalo stulpelių spalva", + "signal-strength-card-style": "Signalo stiprumo kortelės stilius", + "no-signal-rssi-value": "\"Nėra signalo\" RSSI reikšmė" + }, + "status-widget": { + "behavior": "Elgsena", + "layout": "Išdėstymas", + "layout-default": "Numatytasis", + "layout-center": "Centruotas", + "layout-icon": "Piktograma", + "on": "Įjungta", + "off": "Išjungta", + "label": "Etiketė", + "status": "Būsena", + "icon": "Piktograma", + "color-palette": "Spalvų paletė", + "disabled-color-palette": "Išjungta spalvų paletė", + "primary": "Pagrindinė", + "primary-color-hint": "Piktogramos ir etiketės spalva", + "secondary": "Antrinė", + "secondary-color-hint": "Būsenos spalva", + "background": "Fonas" + }, + "chart": { + "common-settings": "Bendrieji nustatymai", + "enable-stacking-mode": "Įjungti kaupimo režimą", + "selection": "Laiko intervalo pasirinkimas", + "enable-selection-mode": "Įjungti pasirinkimo režimą", + "line-shadow-size": "Linijos šešėlio dydis", + "display-smooth-lines": "Rodyti glotnias (lenktas) linijas", + "default-bar-width": "Numatytasis stulpelio plotis neagreguotiems duomenims (milisekundėmis)", + "bar-alignment": "Stulpelių lygiavimas", + "bar-alignment-left": "Kairėje", + "bar-alignment-right": "Dešinėje", + "bar-alignment-center": "Centre", + "default-font": "Numatytasis šriftas", + "default-font-size": "Numatytasis šrifto dydis", + "default-font-color": "Numatytoji šrifto spalva", + "thresholds-line-width": "Numatytasis ribų linijos plotis", + "tooltip-settings": "Užuominos nustatymai", + "tooltip": "Užuomina", + "show-tooltip": "Rodyti užuominą", + "hover-individual-points": "Užvesti virš atskirų taškų", + "show-cumulative-values": "Rodyti sumines reikšmes kaupimo režime", + "hide-zero-false-values": "Slėpti nulinės/neteisingos reikšmės užuominose", + "tooltip-value-format-function": "Užuominos reikšmės formatavimo funkcija", + "grid-settings": "Tinklelio nustatymai", + "show-vertical-lines": "Rodyti vertikalias linijas", + "show-horizontal-lines": "Rodyti horizontalias linijas", + "grid-outline-border-width": "Tinklelio kontūro/rėmelio plotis (px)", + "primary-color": "Pagrindinė spalva", + "background-color": "Fono spalva", + "ticks-color": "Atskirų padalų spalva", + "xaxis-settings": "X ašies nustatymai", + "axis-title": "Ašies pavadinimas", + "xaxis-tick-labels-settings": "X ašies padalų etikečių nustatymai", + "show-tick-labels": "Rodyti ašies padalų etiketes", + "yaxis-settings": "Y ašies nustatymai", + "min-scale-value": "Minimalioji skalės reikšmė", + "max-scale-value": "Maksimalioji skalės reikšmė", + "yaxis-tick-labels-settings": "Y ašies padalų etikečių nustatymai", + "tick-step-size": "Padalų žingsnio dydis", + "number-of-decimals": "Rodyti dešimtainių skaičių kiekis", + "ticks-formatter-function": "Padalų formatavimo funkcija", + "comparison-settings": "Palyginimo nustatymai", + "enable-comparison": "Įjungti palyginimą", + "time-for-comparison": "Palyginimo laikotarpis", + "time-for-comparison-previous-interval": "Ankstesnis intervalas (numatytasis)", + "time-for-comparison-days": "Prieš dieną", + "time-for-comparison-weeks": "Prieš savaitę", + "time-for-comparison-months": "Prieš mėnesį", + "time-for-comparison-years": "Prieš metus", + "time-for-comparison-custom-interval": "Pasirinktinis intervalas", + "custom-interval-value": "Pasirinktinis intervalo dydis (ms)", + "comparison-x-axis-settings": "Palyginimo X ašies nustatymai", + "axis-position": "Ašies padėtis", + "axis-position-top": "Viršuje (numatyta)", + "axis-position-bottom": "Apačioje", + "custom-legend-settings": "Tinkintos legendos nustatymai", + "enable-custom-legend": "Įjungti tinkintą legendą (leidžia naudoti atributų/laikinių eilučių reikšmes etiketėse)", + "key-name": "Raktinio parametro pavadinimas", + "key-name-required": "Būtina nurodyti raktinio parametro pavadinimą", + "key-type": "Rakto tipas", + "key-type-attribute": "Atributas", + "key-type-timeseries": "Laikinė eilutė", + "label-keys-list": "Etiketėse naudojamų raktų sąrašas", + "no-label-keys": "Nesukonfigūruoti jokie raktai", + "add-label-key": "Pridėti naują raktą", + "line-width": "Linijos plotis", + "color": "Spalva", + "data-is-hidden-by-default": "Duomenys pagal numatymą paslėpti", + "disable-data-hiding": "Išjungti duomenų slėpimą", + "remove-from-legend": "Pašalinti duomenų raktą iš legendos", + "exclude-from-stacking": "Pašalinti iš kaupimo (prieinama tik „Kaupimo“ režime)", + "line-settings": "Linijos nustatymai", + "show-line": "Rodyti liniją", + "fill-line": "Užpildyti liniją", + "fill-line-opacity": "Užpildymo nepermatomumas", + "points-settings": "Taškų nustatymai", + "show-points": "Rodyti taškus", + "points-line-width": "Taškų kontūro plotis", + "points-radius": "Taškų spindulys", + "point-shape": "Taško forma", + "point-shape-circle": "Apskritimas", + "point-shape-cross": "Kryžius", + "point-shape-diamond": "Deimantas", + "point-shape-square": "Kvadratas", + "point-shape-triangle": "Trikampis", + "point-shape-custom": "Tinkinta funkcija", + "point-shape-draw-function": "Taško piešimo funkcija", + "show-separate-axis": "Rodyti atskirą ašį", + "axis-position-left": "Kairėje", + "axis-position-right": "Dešinėje", + "thresholds": "Ribos", + "no-thresholds": "Ribos nesukonfigūruotos", + "add-threshold": "Pridėti ribą", + "show-values-for-comparison": "Rodyti istorines reikšmes palyginimui", + "comparison-values-label": "Istorinių reikšmių etiketė", + "comparison-line-color": "Palyginimo linijos spalva", + "threshold-settings": "Ribų nustatymai", + "use-as-threshold": "Naudoti rakto reikšmę kaip ribą", + "threshold-line-width": "Ribos linijos plotis", + "threshold-color": "Ribos spalva", + "common-pie-settings": "Bendrieji skritulinės diagramos nustatymai", + "radius": "Spindulys", + "inner-radius": "Vidinis spindulys", + "tilt": "Pasvirimas", + "common-pie-settings-range-error": "Reikšmė turi būti nuo 0 iki 1", + "stroke-settings": "Kontūro nustatymai", + "width-pixels": "Plotis (pikseliais)", + "show-labels": "Rodyti etiketes", + "animation-settings": "Animacijos nustatymai", + "animated-pie": "Įjungti skritulinės diagramos animaciją (eksperimentinė funkcija)", + "border-settings": "Rėmelio nustatymai", + "border-width": "Rėmelio plotis", + "border-color": "Rėmelio spalva", + "legend-settings": "Legendos nustatymai", + "display-legend": "Rodyti legendą", + "labels-font-color": "Etikečių šrifto spalva", + "series": "Serijos", + "add-series": "Pridėti seriją", + "series-settings": "Serijos nustatymai", + "remove-series": "Pašalinti seriją", + "no-series": "Nesukonfigūruota jokia serija", + "no-series-error": "Būtina nurodyti bent vieną seriją", + "chart-appearance": "Diagramos išvaizda", + "vertical-grid-lines": "Vertikalios tinklelio linijos", + "horizontal-grid-lines": "Horizontalios tinklelio linijos", + "chart-background": "Diagramos fonas", + "grid-lines-color": "Tinklelio linijų spalva", + "border": "Rėmelis", + "axis": "Ašis", + "vertical-axis": "Vertikali ašis", + "ticks": "Padalos", + "horizontal-axis": "Horizontali ašis", + "shape-empty-circle": "Tuščias apskritimas", + "shape-circle": "Apskritimas", + "shape-rect": "Stačiakampis", + "shape-round-rect": "Apvalintų kampų stačiakampis", + "shape-triangle": "Trikampis", + "shape-diamond": "Deimantas", + "shape-pin": "Žymeklis", + "shape-arrow": "Rodyklė", + "shape-none": "Nėra", + "line-type-solid": "Vientisa", + "line-type-dashed": "Brūkšniuota", + "line-type-dotted": "Taškuota", + "label-position-top": "Viršuje", + "label-position-bottom": "Apačioje", + "label-position-outside": "Išorėje", + "label-position-inside": "Viduje", + "fill": "Užpildas", + "fill-type-none": "Nėra", + "fill-type-solid": "Vientisas", + "fill-type-opacity": "Permatomumas", + "fill-type-gradient": "Gradientas", + "background": "Fonas", + "opacity": "Permatomumas", + "gradient-stops": "Gradiento taškai", + "gradient-start": "pradžia", + "gradient-end": "pabaiga", + "animation": { + "animation": "Animacija", + "animation-threshold": "Animacijos riba", + "animation-duration": "Animacijos trukmė", + "animation-easing": "Animacijos švelninimas", + "animation-delay": "Animacijos delsimas", + "update-animation-duration": "Atnaujinimo animacijos trukmė", + "update-animation-easing": "Atnaujinimo animacijos švelninimas", + "update-animation-delay": "Atnaujinimo animacijos delsimas" + }, + "chart-axis": { + "scale": "Skalė", + "scale-min": "min", + "scale-max": "maks", + "scale-auto": "Automatinė" + }, + "bar": { + "show-border": "Rodyti rėmelį", + "border-width": "Rėmelio plotis", + "border-radius": "Kampų apvalinimas", + "bar-width": "Stulpelio plotis", "label": "Etiketė", - "events": "Įvykiai", - "details": "Informacija", - "copyId": "Kopijuoti įrenginio Id", - "copyAccessToken": "Kopijuoti prieigos raktą", - "copy-mqtt-authentication": "Copy MQTT credentials", - "idCopiedMessage": "Įrenginio Id nukopijuotas į iškarpinę", - "accessTokenCopiedMessage": "Įrenginio prieigos raktas nukopijuotas į iškarpinę", - "mqtt-authentication-copied-message": "Device MQTT authentication has been copied to clipboard", - "assignedToCustomer": "Priskirtas klientui", - "unable-delete-device-alias-title": "Įrenginio pseudonimo panaikinti nepavyko", - "unable-delete-device-alias-text": "Įrenginio pseudonimas '{{deviceAlias}}' nes jis naudojamas šiuose valdikliuose:
    {{widgetsList}}", - "is-gateway": "Is gateway", - "overwrite-activity-time": "Overwrite activity time for connected device", - "device-filter": "Įrenginių filtras", - "device-filter-title": "Įrenginių filtras", - "filter-title": "Filtras", - "device-state": "Įrenginio būsena", - "state": "Būsena", - "any": "Visos", - "active": "Aktyvus", - "inactive": "Neaktyvus", - "public": "Viešas", - "device-public": "Įrenginys yra viešas", - "select-device": "Pasirinkite įrenginį", - "select-group-to-add": "Pasirinkite grupę, į kurią norite pridėti pažymėtus įrenginius", - "select-group-to-move": "Pasirinkite grupę, į kurią norite perkelti pažymėtus įrenginius", - "remove-devices-from-group": "Ar tikrai norite iš grupės '{{entityGroup}}' pašalinti { count, plural, =1 {1 įrenginį} other {# įrenginius} }?", - "group": "Įrenginių grupė", - "list-of-groups": "{ count, plural, =1 {Viena įrenginio grupė} other {# įrenginių grupių sąrašas} }", - "group-name-starts-with": "Įrenginių grupės, kurių pavadinimai prasideda '{{prefix}}'", - "import": "Importuoti įrenginį", - "device-file": "Įrenginio failas", - "search": "Įrenginių paieška", - "selected-devices": "Pasirinkta { count, plural, =1 {1 įrenginys} other {# įrenginiai} }", - "device-configuration": "Įrenginių konfigūracija", - "transport-configuration": "Transport configuration", - "wizard": { - "device-details": "Informacija apie įrenginį" - }, - "unassign-devices-from-edge-title": "Are you sure you want to unassign { count, plural, =1 {1 device} other {# devices} }?", - "unassign-devices-from-edge-text": "After the confirmation all selected devices will be unassigned and won't be accessible by the edge.", - "time": "Time", - "connectivity": { - "check-connectivity": "Check connectivity", - "device-created-check-connectivity": "Device created. Let's check connectivity!", - "loading-check-connectivity-command": "Loading check connectivity commands...", - "use-following-instructions": "Use the following instructions for sending telemetry on behalf of the device using shell", - "execute-following-command": "Execute the following command", - "install-curl-windows": "Starting Windows 10 b17063, cURL is available by default", - "install-curl-macos": "Starting Mac OS X 10.2 6C115 (Jaguar), cURL is available by default", - "install-mqtt-windows": "Use the instructions to download, install, setup and run mosquitto_pub", - "install-coap-client": "Use the instructions to download, install, setup and run coap-client", - "install-necessary-client-tools": "Install necessary client tools", - "mqtts-x509-command": "Use the following documentation to connect the device via MQTT with authorization X509", - "coaps-x509-command": "Use the following documentation to connect the device via CoAP over DTLS with authorization X509", - "snmp-command": "Use the following documentation to connect the device through the SNMP.", - "sparkplug-command": "Use the following documentation to connect the device through the MQTT Sparkplug.", - "lwm2m-command": "Use the following documentation to connect the device through the LWM2M." - } + "label-hint": "Rodyti etiketę virš stulpelio.", + "series-label-hint": "Rodyti etiketę su reikšme virš stulpelio.", + "label-background": "Etiketės fonas" + } }, - "asset-profile": { - "asset-profile": "Profilis", - "asset-profiles": "Asset profiles", - "all-asset-profiles": "Visi", - "add": "Add asset profile", - "edit": "Edit asset profile", - "asset-profile-details": "Asset profile details", - "no-asset-profiles-text": "No asset profiles found", - "search": "Search asset profiles", - "selected-asset-profiles": "{ count, plural, =1 {1 asset profile} other {# asset profiles} } selected", - "no-asset-profiles-matching": "No asset profile matching '{{entity}}' were found.", - "asset-profile-required": "Asset profile is required", - "idCopiedMessage": "Asset profile Id has been copied to clipboard", - "set-default": "Make asset profile default", - "delete": "Delete asset profile", - "copyId": "Copy asset profile Id", - "name-max-length": "Name should be less than 256", - "new-device-profile-name": "Asset profile name", - "new-device-profile-name-required": "Asset profile name is required.", - "name": "Name", - "name-required": "Name is required.", - "image": "Asset profile image", - "description": "Description", - "default": "Default", - "default-rule-chain": "Default rule chain", - "default-edge-rule-chain": "Default edge rule chain", - "default-edge-rule-chain-hint": "Used on edge as rule chain to process incoming data for assets of this asset profile", - "mobile-dashboard": "Mobile dashboard", - "mobile-dashboard-hint": "Used by mobile application as a asset details dashboard", - "select-queue-hint": "Select from a drop-down list.", - "delete-asset-profile-title": "Are you sure you want to delete the asset profile '{{assetProfileName}}'?", - "delete-asset-profile-text": "Be careful, after the confirmation the asset profile and all related data will become unrecoverable.", - "delete-asset-profiles-title": "Are you sure you want to delete { count, plural, =1 {1 asset profile} other {# asset profiles} }?", - "delete-asset-profiles-text": "Be careful, after the confirmation all selected asset profiles will be removed and all related data will become unrecoverable.", - "set-default-asset-profile-title": "Are you sure you want to make the asset profile '{{assetProfileName}}' default?", - "set-default-asset-profile-text": "After the confirmation the asset profile will be marked as default and will be used for new assets with no profile specified.", - "no-asset-profiles-found": "No asset profiles found.", - "create-new-asset-profile": "Create a new one!", - "create-asset-profile": "Create new asset profile", - "import": "Import asset profile", - "export": "Export asset profile", - "export-failed-error": "Unable to export asset profile: {{error}}", - "asset-profile-file": "Asset profile file", - "invalid-asset-profile-file-error": "Unable to import asset profile: Invalid asset profile data structure." - }, - "device-profile": { - "device-profile": "Profilis", - "device-profiles": "Device profiles", - "all-device-profiles": "Visi", - "add": "Add device profile", - "edit": "Edit device profile", - "device-profile-details": "Device profile details", - "no-device-profiles-text": "No device profiles found", - "search": "Search device profiles", - "selected-device-profiles": "{ count, plural, =1 {1 device profile} other {# device profiles} } selected", - "no-device-profiles-matching": "No device profile matching '{{entity}}' were found.", - "device-profile-required": "Device profile is required", - "idCopiedMessage": "Device profile Id has been copied to clipboard", - "set-default": "Make device profile default", - "delete": "Delete device profile", - "copyId": "Copy device profile Id", - "name-max-length": "Name should be less than 256", - "name": "Name", - "name-required": "Name is required.", - "type": "Profile type", - "type-required": "Profile type is required.", - "type-default": "Default", - "image": "Device profile image", - "transport-type": "Transport type", - "transport-type-required": "Transport type is required.", - "transport-type-default": "Default", - "transport-type-default-hint": "Supports basic MQTT, HTTP and CoAP transport", - "transport-type-mqtt": "MQTT", - "transport-type-mqtt-hint": "Enables advanced MQTT transport settings", - "transport-type-coap": "CoAP", - "transport-type-coap-hint": "Enables advanced CoAP transport settings", - "transport-type-lwm2m": "LWM2M", - "transport-type-lwm2m-hint": "LWM2M transport type", - "transport-type-snmp": "SNMP", - "transport-type-snmp-hint": "Specify SNMP transport configuration", - "transport-type-http": "HTTP", - "description": "Description", - "default": "Default", - "profile-configuration": "Profile configuration", - "transport-configuration": "Transport configuration", - "default-rule-chain": "Default rule chain", - "default-edge-rule-chain": "Default edge rule chain", - "default-edge-rule-chain-hint": "Used on edge as rule chain to process incoming data for devices of this device profile", - "mobile-dashboard": "Mobile dashboard", - "mobile-dashboard-hint": "Used by mobile application as a device details dashboard", - "select-queue-hint": "Select from a drop-down list.", - "delete-device-profile-title": "Are you sure you want to delete the device profile '{{deviceProfileName}}'?", - "delete-device-profile-text": "Be careful, after the confirmation the device profile and all related data including associated OTA updates will become unrecoverable.", - "delete-device-profiles-title": "Are you sure you want to delete { count, plural, =1 {1 device profile} other {# device profiles} }?", - "delete-device-profiles-text": "Be careful, after the confirmation all selected device profiles will be removed and all related data including associated OTA updates will become unrecoverable.", - "set-default-device-profile-title": "Are you sure you want to make the device profile '{{deviceProfileName}}' default?", - "set-default-device-profile-text": "After the confirmation the device profile will be marked as default and will be used for new devices with no profile specified.", - "no-device-profiles-found": "No device profiles found.", - "create-new-device-profile": "Create a new one!", - "mqtt-device-topic-filters": "MQTT device topic filters", - "mqtt-device-topic-filters-unique": "MQTT device topic filters need to be unique.", - "mqtt-device-topic-filters-spark-plug": "MQTT Sparkplug B Edge of Network (EoN) node.", - "mqtt-device-topic-filters-spark-plug-hint": "Allow connections from EoN nodes with Sparkplug B payload and topic format.", - "mqtt-device-topic-filters-spark-plug-attribute-metric-names": "SparkPlug metrics to store as attributes.", - "mqtt-device-topic-filters-spark-plug-attribute-metric-names-hint": "Names of SparkPlug metrics that will be stored as device attributes. All other metrics will be stored as device telemetry.", - "mqtt-device-payload-type": "MQTT device payload", - "mqtt-device-payload-type-json": "JSON", - "mqtt-device-payload-type-proto": "Protobuf", - "mqtt-enable-compatibility-with-json-payload-format": "Enable compatibility with other payload formats.", - "mqtt-enable-compatibility-with-json-payload-format-hint": "When enabled, the platform will use a Protobuf payload format by default. If parsing fails, the platform will attempt to use JSON payload format. Useful for backward compatibility during firmware updates. For example, the initial release of the firmware uses Json, while the new release uses Protobuf. During the process of firmware update for the fleet of devices, it is required to support both Protobuf and JSON simultaneously. The compatibility mode introduces slight performance degradation, so it is recommended to disable this mode once all devices are updated.", - "mqtt-use-json-format-for-default-downlink-topics": "Use Json format for default downlink topics", - "mqtt-use-json-format-for-default-downlink-topics-hint": "When enabled, the platform will use Json payload format to push attributes and RPC via the following topics: v1/devices/me/attributes/response/$request_id, v1/devices/me/attributes, v1/devices/me/rpc/request/$request_id, v1/devices/me/rpc/response/$request_id. This setting does not impact attribute and rpc subscriptions sent using new (v2) topics: v2/a/res/$request_id, v2/a, v2/r/req/$request_id, v2/r/res/$request_id. Where $request_id is an integer request identifier.", - "mqtt-send-ack-on-validation-exception": "Send PUBACK on PUBLISH message validation failure", - "mqtt-send-ack-on-validation-exception-hint": "By default, the platform will close the MQTT session on message validation failure. When enabled, the platform will send publish acknowledgment instead of closing the session.", - "snmp-add-mapping": "Add SNMP mapping", - "snmp-mapping-not-configured": "No mapping for OID to timeseries/telemetry configured", - "snmp-timseries-or-attribute-name": "Timeseries/attribute name for mapping", - "snmp-timseries-or-attribute-type": "Timeseries/attribute type for mapping", - "snmp-method-pdu-type-get-request": "GetRequest", - "snmp-method-pdu-type-get-next-request": "GetNextRequest", - "snmp-oid": "OID", - "transport-device-payload-type-json": "JSON", - "transport-device-payload-type-proto": "Protobuf", - "mqtt-payload-type-required": "Payload type is required.", - "coap-device-type": "CoAP device type", - "coap-device-payload-type": "CoAP device payload", - "coap-device-type-required": "CoAP device type is required.", - "coap-device-type-default": "Default", - "coap-device-type-efento": "Efento NB-IoT", - "support-level-wildcards": "Single [+] and multi-level [#] wildcards supported.", - "telemetry-topic-filter": "Telemetry topic filter", - "telemetry-topic-filter-required": "Telemetry topic filter is required.", - "attributes-topic-filter": "Attributes publish topic filter", - "attributes-subscribe-topic-filter": "Attributes subscribe topic filter", - "attributes-topic-filter-required": "Attributes publish topic filter is required.", - "attributes-subscribe-topic-filter-required": "Attributes subscribe topic is required", - "telemetry-proto-schema": "Telemetry proto schema", - "telemetry-proto-schema-required": "Telemetry proto schema is required.", - "attributes-proto-schema": "Attributes proto schema", - "attributes-proto-schema-required": "Attributes proto schema is required.", - "rpc-response-proto-schema": "RPC response proto schema", - "rpc-response-proto-schema-required": "RPC response proto schema is required.", - "rpc-response-topic-filter": "RPC response topic filter", - "rpc-response-topic-filter-required": "RPC response topic filter is required.", - "rpc-request-proto-schema": "RPC request proto schema", - "rpc-request-proto-schema-required": "RPC request proto schema is required.", - "rpc-request-proto-schema-hint": "RPC request message should always have fields: string method = 1; int32 requestId = 2; and params = 3 of any data type.", - "not-valid-pattern-topic-filter": "Not valid pattern topic filter", - "not-valid-single-character": "Invalid use of a single-level wildcard character", - "not-valid-multi-character": "Invalid use of a multi-level wildcard character", - "single-level-wildcards-hint": "[+] is suitable for any topic filter level. Ex.: v1/devices/+/telemetry or +/devices/+/attributes.", - "multi-level-wildcards-hint": "[#] can replace the topic filter itself and must be the last symbol of the topic. Ex.: # or v1/devices/me/#.", - "alarm-rules": "Alarm rules", - "alarm-rules-with-count": "Alarm rules ({{count}})", - "no-alarm-rules": "No alarm rules configured", - "add-alarm-rule": "Add alarm rule", - "edit-alarm-rule": "Edit alarm rule", - "alarm-type": "Alarm type", - "alarm-type-required": "Alarm type is required.", - "alarm-type-unique": "Alarm type must be unique within the device profile alarm rules.", - "alarm-type-max-length": "Alarm type should be less than 256", - "create-alarm-pattern": "Create {{alarmType}} alarm", - "create-alarm-rules": "Create alarm rules", - "no-create-alarm-rules": "No create conditions configured", - "add-create-alarm-rule-prompt": "Please add create alarm rule", - "clear-alarm-rule": "Clear alarm rule", - "no-clear-alarm-rule": "No clear condition configured", - "add-create-alarm-rule": "Add create condition", - "add-clear-alarm-rule": "Add clear condition", - "select-alarm-severity": "Select alarm severity", - "alarm-severity-required": "Alarm severity is required.", - "condition-duration": "Condition duration", - "condition-duration-value": "Duration value", - "condition-duration-time-unit": "Time unit", - "condition-duration-value-range": "Duration value should be in a range from 1 to 2147483647.", - "condition-duration-value-pattern": "Duration value should be integers.", - "condition-duration-value-required": "Duration value is required.", - "condition-duration-time-unit-required": "Time unit is required.", - "advanced-settings": "Advanced settings", - "alarm-rule-additional-info": "Papildoma informacija", - "edit-alarm-rule-additional-info": "Redaguoti papildomą informaciją", - "alarm-rule-additional-info-placeholder": "Pateikite savo komentarus ir patikslinimus čia, kad jie būtų rodomi pavojaus signalo detalių skiltyje „Papildoma informacija“.", - "alarm-rule-additional-info-hint": "Hint: use ${keyName} to substitute values of the attribute or telemetry keys that are used in alarm rule condition.", - "alarm-rule-mobile-dashboard": "Mobile dashboard", - "alarm-rule-mobile-dashboard-hint": "Used by mobile application as an alarm details dashboard", - "alarm-rule-no-mobile-dashboard": "No dashboard selected", - "propagate-alarm": "Propagate alarm to related entities", - "alarm-rule-relation-types-list": "Relation types to propagate", - "alarm-rule-relation-types-list-hint": "If Propagate relation types are not selected, alarms will be propagated without filtering by relation type.", - "propagate-alarm-to-owner": "Propagate alarm to entity owner (Customer or Tenant)", - "propagate-alarm-to-owner-hierarchy": "Propagate alarm to entity owners hierarchy", - "propagate-alarm-to-tenant": "Propagate alarm to Tenant", - "alarm-rule-condition": "Alarm rule condition", - "enter-alarm-rule-condition-prompt": "Please add alarm rule condition", - "edit-alarm-rule-condition": "Edit alarm rule condition", - "device-provisioning": "Device provisioning", - "provision-strategy": "Provision strategy", - "provision-strategy-required": "Provision strategy is required.", - "provision-strategy-disabled": "Disabled", - "provision-strategy-created-new": "Allow to create new devices", - "provision-strategy-check-pre-provisioned": "Check for pre-provisioned devices", - "provision-device-key": "Provision device key", - "provision-device-key-required": "Provision device key is required.", - "copy-provision-key": "Copy provision key", - "provision-key-copied-message": "Provision key has been copied to clipboard", - "provision-device-secret": "Provision device secret", - "provision-device-secret-required": "Provision device secret is required.", - "copy-provision-secret": "Copy provision secret", - "provision-secret-copied-message": "Provision secret has been copied to clipboard", - "provision-strategy-x509": { - "certificate-chain": "X509 Certificates Chain", - "certificate-chain-hint": "X.509 certificates strategy is used to provision devices by client certificates in two-way TLS communication.", - "allow-create-new-devices": "Create new devices", - "allow-create-new-devices-hint": "If selected new devices will be created and client certificate will be used as device credentials.", - "certificate-value": "Certificate in PEM format", - "certificate-value-required": "Certificate in PEM format is required", - "cn-regex-variable": "CN Regular Expression variable", - "cn-regex-variable-required": "CN Regular Expression variable is required", - "cn-regex-variable-hint": "Required to fetch device name from device's X509 certificate's common name." - }, - "condition": "Condition", - "condition-type": "Condition type", - "condition-type-simple": "Simple", - "condition-type-duration": "Duration", - "condition-during": "During {{during}}", - "condition-during-dynamic": "During \"{{ attribute }}\" ({{during}})", - "condition-type-repeating": "Repeating", - "condition-type-required": "Condition type is required.", - "condition-repeating-value": "Count of events", - "condition-repeating-value-range": "Count of events should be in a range from 1 to 2147483647.", - "condition-repeating-value-pattern": "Count of events should be integers.", - "condition-repeating-value-required": "Count of events is required.", - "condition-repeat-times": "Repeats { count, plural, =1 {1 time} other {# times} }", - "condition-repeat-times-dynamic": "Repeats \"{ attribute }\" ({ count, plural, =1 {1 time} other {# times} })", - "schedule-type": "Scheduler type", - "schedule-type-required": "Scheduler type is required.", - "schedule": "Schedule", - "edit-schedule": "Edit alarm schedule", - "schedule-any-time": "Active all the time", - "schedule-specific-time": "Active at a specific time", - "schedule-custom": "Custom", - "schedule-day": { - "monday": "Monday", - "tuesday": "Tuesday", - "wednesday": "Wednesday", - "thursday": "Thursday", - "friday": "Friday", - "saturday": "Saturday", - "sunday": "Sunday" - }, - "schedule-days": "Days", - "schedule-time": "Time", - "schedule-time-from": "From", - "schedule-time-to": "To", - "schedule-days-of-week-required": "At least one day of week should be selected.", - "create-device-profile": "Create new device profile", - "import": "Import device profile", - "export": "Export device profile", - "export-failed-error": "Unable to export device profile: {{error}}", - "device-profile-file": "Device profile file", - "invalid-device-profile-file-error": "Unable to import device profile: Invalid device profile data structure.", - "power-saving-mode": "Power Saving Mode", - "power-saving-mode-type": { - "default": "Use device profile power saving mode", - "psm": "Power Saving Mode (PSM)", - "drx": "Discontinuous Reception (DRX)", - "edrx": "Extended Discontinuous Reception (eDRX)" - }, - "edrx-cycle": "eDRX cycle", - "edrx-cycle-required": "eDRX cycle is required.", - "edrx-cycle-pattern": "eDRX cycle must be a positive integer.", - "edrx-cycle-min": "Minimum number of eDRX cycle is {{ min }} seconds.", - "paging-transmission-window": "Paging Transmission Window", - "paging-transmission-window-required": "Paging transmission window is required.", - "paging-transmission-window-pattern": "Paging transmission window must be a positive integer.", - "paging-transmission-window-min": "Minimum number ofpPaging transmission window is {{ min }} seconds.", - "psm-activity-timer": "PSM Activity Timer", - "psm-activity-timer-required": "PSM activity timer is required.", - "psm-activity-timer-pattern": "PSM activity timer must be a positive integer.", - "psm-activity-timer-min": "Minimum number of PSM activity timer is {{ min }} seconds.", - "lwm2m": { - "object-list": "Object list", - "object-list-empty": "No objects selected.", - "no-objects-found": "No objects found.", - "no-objects-matching": "No objects matching '{{object}}' were found.", - "model-tab": "LWM2M Model", - "add-new-instances": "Add new instances", - "instances-list": "Instances list", - "instances-list-required": "Instances list is required.", - "instance-id-pattern": "Instance id must be a positive integer.", - "instance-id-max": "Maximum instance id value {{max}}.", - "instance": "Instance", - "resource-label": "#ID Resource name", - "observe-label": "Observe", - "attribute-label": "Attribute", - "telemetry-label": "Telemetry", - "edit-observe-select": "To edit observe select telemetry or attribute", - "edit-attributes-select": "To edit attributes select telemetry or attribute", - "no-attributes-set": "No attributes set", - "key-name": "Key name", - "key-name-required": "Key name is required", - "attribute-name": "Name attribute", - "attribute-name-required": "Name attribute is required.", - "attribute-value": "Attribute value", - "attribute-value-required": "Attribute value is required.", - "attribute-value-pattern": "Attribute value must be a positive integer.", - "edit-attributes": "Edit attributes: {{ name }}", - "view-attributes": "View attributes: {{ name }}", - "add-attribute": "Add attribute", - "edit-attribute": "Edit attribute", - "view-attribute": "View attribute", - "remove-attribute": "Remove attribute", - "delete-server-text": "Be careful, after the confirmation the server configuration will become unrecoverable.", - "delete-server-title": "Are you sure you want to delete the server?", - "mode": "Security config mode", - "bootstrap-tab": "Bootstrap", - "bootstrap-server-legend": "Bootstrap Server (ShortId...)", - "lwm2m-server-legend": "LwM2M Server (ShortId...)", - "server": "Server", - "short-id": "Short server ID", - "short-id-tooltip": "Server short Id. Used as link to associate server Object Instance.\nThis identifier uniquely identifies each LwM2M Server configured for the LwM2M Client.\nResource MUST be set when the Bootstrap-Server Resource has a value of 'false'.\nThe values ID:0 and ID:65535 values MUST NOT be used for identifying the LwM2M Server.", - "short-id-required": "Short server ID is required.", - "short-id-range": "Short server ID should be in a range from {{ min }} to {{ max }}.", - "short-id-pattern": "Short server ID must be a positive integer.", - "lifetime": "Client registration lifetime", - "lifetime-required": "Client registration lifetime is required.", - "lifetime-pattern": "Client registration lifetime must be a positive integer.", - "default-min-period": "Min period between two notifications (s)", - "default-min-period-tooltip": "The default value the LwM2M Client should use for the Minimum Period of an Observation in the absence of this parameter being included in an Observation.", - "default-min-period-required": "Minimum period is required.", - "default-min-period-pattern": "Minimum period must be a positive integer.", - "notification-storing": "Notification storing when disabled or offline", - "binding": "Binding", - "binding-type": { - "u": "U: Client is reachable via the UDP binding at any time.", - "m": "M: Client is reachable via the MQTT binding at any time.", - "h": "H: Client is reachable via the HTTP binding at any time.", - "t": "T: Client is reachable via the TCP binding at any time.", - "s": "S: Client is reachable via the SMS binding at any time.", - "n": "N: Client MUST send the response to such a request over the Non-IP binding (is supported since LWM2M 1.1).", - "uq": "UQ: UDP connection in queue mode (is not supported since LWM2M 1.1)", - "uqs": "UQS: both UDP and SMS connections active; UDP in queue mode, SMS in standard mode (is not supported since LWM2M 1.1)", - "tq": "TQ: TCP connection in queue mode (is not supported since LWM2M 1.1)", - "tqs": "TQS: both TCP and SMS connections active; TCP in queue mode, SMS in standard mode (is not supported since LWM2M 1.1)", - "sq": "SQ: SMS connection in queue mode (is not supported since LWM2M 1.1)" - }, - "binding-tooltip": "This is the list in the\"binding\" resource of the LwM2M server object - /1/x/7.\nIndicates the supported binding modes in the LwM2M Client.\nThis value SHOULD be the same as the value in the “Supported Binding and Modes” resource in the Device Object (/3/0/16).\nWhile multiple transports are supported, only one transport binding can be used during the entire Transport Session.\nAs an example, when UDP and SMS are both supported, the LwM2M Client and the LwM2M Server can choose to communicate either over UDP or SMS during the entire Transport Session.", - "bootstrap-server": "Bootstrap Server", - "lwm2m-server": "LwM2M Server", - "include-bootstrap-server": "Include Bootstrap Server updates", - "bootstrap-update-title": "You already have configured Bootstrap Server. Are you sure you want to exclude the updates?", - "bootstrap-update-text": "Be careful, after the confirmation the Bootstrap Server configuration data will become unrecoverable.", - "server-host": "Host", - "server-host-required": "Host is required.", - "server-port": "Port", - "server-port-required": "Port is required.", - "server-port-pattern": "Port must be a positive integer.", - "server-port-range": "Port should be in a range from 1 to 65535.", - "server-public-key": "Server Public Key", - "server-public-key-required": "Server Public Key is required.", - "client-hold-off-time": "Hold Off Time", - "client-hold-off-time-required": "Hold Off Time is required.", - "client-hold-off-time-pattern": "Hold Off Time must be a positive integer.", - "client-hold-off-time-tooltip": "Client Hold Off Time for use with a Bootstrap-Server only", - "account-after-timeout": "Account after the timeout", - "account-after-timeout-required": "Account after the timeout is required.", - "account-after-timeout-pattern": "Account after the timeout must be a positive integer.", - "account-after-timeout-tooltip": "Bootstrap-Server Account after the timeout value given by this resource.", - "server-type": "Server type", - "add-new-server-title": "Add new server config", - "add-server-config": "Add server config", - "add-lwm2m-server-config": "Add LwM2M server", - "no-config-servers": "No servers configured", - "others-tab": "Other settings", - "client-strategy": "Client strategy when connecting", - "client-strategy-label": "Strategy", - "client-strategy-only-observe": "Only Observe Request to the client after the initial connection", - "client-strategy-read-all": "Read All Resources & Observe Request to the client after registration", - "fw-update": "Firmware update", - "fw-update-strategy": "Firmware update strategy", - "fw-update-strategy-data": "Push firmware update as binary file using Object 19 and Resource 0 (Data)", - "fw-update-strategy-package": "Push firmware update as binary file using Object 5 and Resource 0 (Package)", - "fw-update-strategy-package-uri": "Auto-generate unique CoAP URL to download the package and push firmware update as Object 5 and Resource 1 (Package URI)", - "sw-update": "Software update", - "sw-update-strategy": "Software update strategy", - "sw-update-strategy-package": "Push binary file using Object 9 and Resource 2 (Package)", - "sw-update-strategy-package-uri": "Auto-generate unique CoAP URL to download the package and push software update using Object 9 and Resource 3 (Package URI)", - "fw-update-resource": "Firmware update CoAP resource", - "fw-update-resource-required": "Firmware update CoAP resource is required.", - "sw-update-resource": "Software update CoAP resource", - "sw-update-resource-required": "Software update CoAP resource is required.", - "config-json-tab": "Json Config Profile Device", - "attributes-name": { - "min-period": "Minimum period", - "max-period": "Maximum period", - "greater-than": "Greater than", - "less-than": "Less than", - "step": "Step", - "min-evaluation-period": "Minimum evaluation period", - "max-evaluation-period": "Maximum evaluation period" - }, - "composite-operations-support": "Supports composite Read/Write/Observe operations" - }, - "snmp": { - "add-communication-config": "Add communication config", - "add-mapping": "Add mapping", - "authentication-passphrase": "Authentication passphrase", - "authentication-passphrase-required": "Authentication passphrase is required.", - "authentication-protocol": "Authentication protocol", - "authentication-protocol-required": "Authentication protocol is required.", - "communication-configs": "Communication configs", - "community": "Community string", - "community-required": "Community string is required.", - "context-name": "Context name", - "data-key": "Data key", - "data-key-required": "Data key is required.", - "data-type": "Data type", - "data-type-required": "Data type is required.", - "engine-id": "Engine ID", - "host": "Host", - "host-required": "Host is required.", - "oid": "OID", - "oid-pattern": "Invalid OID format.", - "oid-required": "OID is required.", - "please-add-communication-config": "Please add communication config", - "please-add-mapping-config": "Please add mapping config", - "port": "Port", - "port-format": "Invalid port format.", - "port-required": "Port is required.", - "privacy-passphrase": "Privacy passphrase", - "privacy-passphrase-required": "Privacy passphrase is required.", - "privacy-protocol": "Privacy protocol", - "privacy-protocol-required": "Privacy protocol is required.", - "protocol-version": "Protocol version", - "protocol-version-required": "Protocol version is required.", - "querying-frequency": "Querying frequency, ms", - "querying-frequency-invalid-format": "Querying frequency must be a positive integer.", - "querying-frequency-required": "Querying frequency is required.", - "retries": "Retries", - "retries-invalid-format": "Retries must be a positive integer.", - "retries-required": "Retries is required.", - "scope": "Scope", - "scope-required": "Scope is required.", - "security-name": "Security name", - "security-name-required": "Security name is required.", - "timeout-ms": "Timeout, ms", - "timeout-ms-invalid-format": "Timeout must be a positive integer.", - "timeout-ms-required": "Timeout is required.", - "user-name": "User name", - "user-name-required": "User name is required." - } + "color": { + "color-settings": "Spalvų nustatymai", + "color-type-constant": "Pastovi", + "color-type-gradient": "Gradientas", + "color-type-range": "Intervalas", + "color-type-function": "Funkcija", + "color": "Spalva", + "value-range": "Reikšmių intervalas", + "from": "Nuo", + "to": "Iki", + "color-function": "Spalvos funkcija", + "copy-color-settings-from": "Kopijuoti spalvų nustatymus iš", + "copy-from": "Kopijuoti iš", + "settings-type": "Nustatymų tipas", + "basic-mode": "Paprastas", + "advanced-mode": "Išplėstinis", + "entity-alias": "Objekto pseudonimas", + "entity-attribute": "Objekto atributas", + "gradient-color": "Gradiento spalva", + "gradient-color-min": "Spalva", + "gradient-start": "Gradiento pradžios spalva", + "gradient-start-min": "Pradžia", + "gradient-end": "Gradiento pabaigos spalva", + "gradient-end-min": "Pabaiga", + "start-value": "Pradinė reikšmė", + "end-value": "Galutinė reikšmė", + "gradient-type": "Gradiento tipas" + }, + "dashboard-state": { + "dashboard-state-settings": "Valdymo skydelio būsenos nustatymai", + "dashboard-state": "Valdymo skydelio būsenos ID", + "autofill-state-layout": "Automatiškai užpildyti būsenos išdėstymo aukštį pagal numatymą", + "default-margin": "Numatytasis valdiklių tarpas", + "default-background-color": "Numatytoji fono spalva", + "sync-parent-state-params": "Sinchronizuoti būsenos parametrus su pagrindiniu valdymo skydeliu" + }, + "date-range-navigator": { + "date-range-picker-settings": "Datų intervalo parinkiklio nustatymai", + "hide-date-range-picker": "Slėpti datų intervalo parinkiklį", + "picker-one-panel": "Datų intervalo parinkiklis viename skydelyje", + "picker-auto-confirm": "Automatinis datų intervalo patvirtinimas", + "picker-show-template": "Rodyti datų intervalo šabloną", + "first-day-of-week": "Pirmoji savaitės diena", + "interval-settings": "Intervalo nustatymai", + "hide-interval": "Slėpti intervalą", + "initial-interval": "Pradinis intervalas", + "interval-hour": "Valanda", + "interval-day": "Diena", + "interval-week": "Savaitė", + "interval-two-weeks": "2 savaitės", + "interval-month": "Mėnuo", + "interval-three-months": "3 mėnesiai", + "interval-six-months": "6 mėnesiai", + "step-settings": "Žingsnio nustatymai", + "hide-step-size": "Slėpti žingsnio dydį", + "initial-step-size": "Pradinis žingsnio dydis", + "hide-labels": "Slėpti etiketes", + "use-session-storage": "Naudoti sesijos saugyklą", + "localizationMap": { + "Sun": "Sek", + "Mon": "Pir", + "Tue": "Ant", + "Wed": "Tre", + "Thu": "Ket", + "Fri": "Pen", + "Sat": "Šeš", + "Jan": "Sau", + "Feb": "Vas", + "Mar": "Kov", + "Apr": "Bal", + "May": "Geg", + "Jun": "Bir", + "Jul": "Lie", + "Aug": "Rgp", + "Sep": "Rgs", + "Oct": "Spa", + "Nov": "Lap", + "Dec": "Grd", + "January": "Sausis", + "February": "Vasaris", + "March": "Kovas", + "April": "Balandis", + "June": "Birželis", + "July": "Liepa", + "August": "Rugpjūtis", + "September": "Rugsėjis", + "October": "Spalis", + "November": "Lapkritis", + "December": "Gruodis", + "Custom Date Range": "Pasirinktinis datų intervalas", + "Date Range Template": "Datų intervalo šablonas", + "Today": "Šiandien", + "Yesterday": "Vakar", + "This Week": "Šią savaitę", + "Last Week": "Praėjusią savaitę", + "This Month": "Šį mėnesį", + "Last Month": "Praėjusį mėnesį", + "Year": "Metai", + "This Year": "Šiais metais", + "Last Year": "Praėjusiais metais", + "Date picker": "Datų parinkiklis", + "Hour": "Valanda", + "Day": "Diena", + "Week": "Savaitė", + "2 weeks": "2 savaitės", + "Month": "Mėnuo", + "3 months": "3 mėnesiai", + "6 months": "6 mėnesiai", + "Custom interval": "Pasirinktinis intervalas", + "Interval": "Intervalas", + "Step size": "Žingsnio dydis", + "Ok": "Gerai" + } }, - "dialog": { - "close": "Uždaryti dialogo langą", - "error-message-title": "Klaidos pranešimas:", - "error-details-title": "Informacija apie klaidą" + "doughnut": { + "doughnut-appearance": "Žiedo diagramos išvaizda", + "layout": "Išdėstymas", + "layout-default": "Numatytasis", + "layout-with-total": "Su suma", + "central-total-value": "Centrinė bendra reikšmė", + "doughnut-card-style": "Žiedo diagramos kortelės stilius" }, - "direction": { - "column": "Stulpelis", - "row": "Eilutė" + "entities-hierarchy": { + "hierarchy-data-settings": "Hierarchijos duomenų nustatymai", + "relations-query-function": "Mazgo ryšių užklausos funkcija", + "has-children-function": "Funkcija, tikrinanti, ar mazgas turi vaikų", + "node-state-settings": "Mazgo būsenos nustatymai", + "node-opened-function": "Numatytoji mazgo atidarymo funkcija", + "node-disabled-function": "Mazgo išjungimo funkcija", + "display-settings": "Atvaizdavimo nustatymai", + "node-icon-function": "Mazgo piktogramos funkcija", + "node-text-function": "Mazgo teksto funkcija", + "sort-settings": "Rikiavimo nustatymai", + "nodes-sort-function": "Mazgų rikiavimo funkcija" }, "edge": { - "all": "All", - "all-edges": "All edges", - "groups": "Groups", - "shared": "Shared", - "edge": "Edge", - "edge-instances": "Edge instances", - "instances": "Instances", - "edge-file": "Edge file", - "name-max-length": "Name should be less than 256", - "label-max-length": "Label should be less than 256", - "type-max-length": "Type should be less than 256", - "management": "Edge management", - "no-edges-matching": "No edges matching '{{entity}}' were found.", - "add": "Add Edge", - "no-edges-text": "No edges found", - "edge-details": "Edge details", - "add-edge-text": "Add new edge", - "delete": "Delete edge", - "delete-edge-title": "Are you sure you want to delete the edge '{{edgeName}}'?", - "delete-edge-text": "Be careful, after the confirmation the edge and all related data will become unrecoverable.", - "delete-edges-title": "Are you sure you want to edge { count, plural, =1 {1 edge} other {# edges} }?", - "delete-edges-text": "Be careful, after the confirmation all selected edges will be removed and all related data will become unrecoverable.", - "name": "Name", - "name-starts-with": "Edge name starts with", - "name-required": "Name is required.", - "edge-license-key": "Edge License Key", - "edge-license-key-required": "Edge License Key is required.", - "edge-license-key-max-length": "Edge License Key should be less than 31", - "edge-license-key-hint": "IMPORTANT NOTE: default edge license key provided only for evaluation purposes.
    This key is active only for 30 days after the activation. White-labeling feature is disabled.
    To obtain your license key please navigate to the pricing page and select the best license option for your case.", - "cloud-endpoint": "Cloud Endpoint", - "cloud-endpoint-required": "Cloud Endpoint is required.", - "cloud-endpoint-max-length": "Cloud Endpoint should be less than 256", - "cloud-endpoint-hint": "Edge requires HTTP(s) access to Cloud (ThingsBoard PE) to verify the license key. Please specify Cloud URL that Edge is able to connect to.", - "description": "Description", - "details": "Details", - "events": "Events", - "copy-id": "Copy Edge Id", - "id-copied-message": "Edge Id has been copied to clipboard", - "sync": "Sync Edge", - "edge-required": "Edge required", - "edge-type": "Edge type", - "edge-type-required": "Edge type is required.", - "event-action": "Event action", - "entity-id": "Entity ID", - "select-edge-type": "Select edge type", - "assign-to-customer": "Assign to customer", - "assign-to-customer-text": "Please select the customer to assign the edge(s)", - "assign-edge-to-customer": "Assign Edge(s) To Customer", - "assign-edge-to-customer-text": "Please select the edges to assign to the customer", - "assignedToCustomer": "Assigned to customer", - "edge-public": "Edge is public", - "assigned-to-customer": "Assigned to: {{customerTitle}}", - "unassign-from-customer": "Unassign from customer", - "unassign-edge-title": "Are you sure you want to unassign the edge '{{edgeName}}'?", - "unassign-edge-text": "After the confirmation the edge will be unassigned and won't be accessible by the customer.", - "unassign-edges-title": "Are you sure you want to unassign { count, plural, =1 {1 edge} other {# edges} }?", - "unassign-edges-text": "After the confirmation all selected edges will be unassigned and won't be accessible by the customer.", - "make-public": "Make edge public", - "make-public-edge-title": "Are you sure you want to make the edge '{{edgeName}}' public?", - "make-public-edge-text": "After the confirmation the edge and all its data will be made public and accessible by others.", - "make-private": "Make edge private", - "public": "Public", - "make-private-edge-title": "Are you sure you want to make the edge '{{edgeName}}' private?", - "make-private-edge-text": "After the confirmation the edge and all its data will be made private and won't be accessible by others.", - "import": "Import edge", - "install-connect-instructions": "Install & Connect Instructions", - "install-connect-instructions-edge-created": "Edge created! Check Install & Connect Instructions", - "loading-edge-instructions": "Loading edge instructions...", - "label": "Label", - "load-entity-error": "Failed to load data. Entity has been deleted.", - "assign-new-edge": "Assign new edge", - "unassign-from-edge": "Unassign from edge", - "edge-key": "Edge key", - "copy-edge-key": "Copy Edge key", - "edge-key-copied-message": "Edge key has been copied to clipboard", - "edge-secret": "Edge secret", - "copy-edge-secret": "Copy Edge secret", - "edge-secret-copied-message": "Edge secret has been copied to clipboard", - "manage-assets": "Manage assets", - "manage-devices": "Manage devices", - "manage-entity-views": "Manage entity views", - "manage-dashboards": "Manage dashboards", - "manage-rulechains": "Manage rule chains", - "assets": "Edge assets", - "devices": "Edge devices", - "entity-views": "Edge entity views", - "dashboard": "Edge dashboard", - "dashboards": "Edge Dashboards", - "rulechain-templates": "Rule chain templates", - "edge-rulechain-templates": "Edge rule chain templates", - "converter-templates": "Converter templates", - "edge-converter-templates": "Edge converter templates", - "integration-templates": "Integration templates", - "edge-integration-templates": "Edge integration templates", - "rulechains": "Edge rule chains", - "integrations": "Integrations", - "search": "Search edges", - "selected-edges": "{ count, plural, =1 {1 edge} other {# edges} } selected", - "any-edge": "Any edge", - "no-edge-types-matching": "No edge types matching '{{entitySubtype}}' were found.", - "edge-type-list-empty": "No edge types selected.", - "edge-types": "Edge types", - "enter-edge-type": "Enter edge type", - "deployed": "Deployed", - "pending": "Pending", - "downlinks": "Downlinks", - "no-downlinks-prompt": "No downlinks found", - "sync-process-started-successfully": "Sync process started successfully!", - "missing-related-rule-chains-title": "Edge has missing related rule chain(s)", - "missing-related-rule-chains-text": "Assigned to edge rule chain(s) use rule nodes that forward message(s) to rule chain(s) that are not assigned to this edge.

    List of missing rule chain(s):
    {{missingRuleChains}}", - "widget-datasource-error": "This widget supports only EDGE entity datasource", - "assign-to-edge": "Assign to edge", - "assign-to-edge-title": "Assign Entity Group(s) to Edge", - "manage-edge-user-groups": "Manage edge user groups", - "manage-edge-asset-groups": "Manage edge asset groups", - "manage-edge-device-groups": "Manage edge device groups", - "manage-edge-entity-view-groups": "Manage edge entity view groups", - "manage-edge-dashboard-groups": "Manage edge dashboard groups", - "manage-edge-rule-chains": "Manage edge rule chains", - "manage-edge-integrations": "Manage edge integrations", - "manage-edge-scheduler-events": "Manage edge scheduler events", - "select-group-to-add": "Select target group to add selected edges", - "select-group-to-move": "Select target group to move selected edges", - "remove-edges-from-group": "Are you sure you want to remove { count, plural, =1 {1 edge} other {# edges} } from group '{entityGroup}'?", - "group": "Group of edges", - "list-of-groups": "{ count, plural, =1 {One edge group} other {List of # edge groups} }", - "group-name-starts-with": "Edge groups whose names start with '{{prefix}}'", - "unassign-entity-group-from-edge-title": "Are you sure you want to unassign the entity group '{{ entityGroupName }}'?", - "unassign-entity-group-from-edge-text": "After the confirmation the entity group will be unassigned and won't be accessible by the edge.", - "unassign-entity-group-from-edge": "Unassign entity group from edge", - "unassign-entity-groups-from-edge-title": "Are you sure you want to unassign {{count}} entity groups?", - "unassign-entity-groups-from-edge-text": "After the confirmation entity groups will be unassigned and won't be accessible by the edge.", - "unassign-entity-groups-from-edge": "Unassign entity groups from edge", - "unassign-scheduler-events-from-edge": "Unassign scheduler events from edge", - "unassign-scheduler-event-from-edge-title": "Are you sure you want to unassign scheduler event '{{schedulerEventName}}'?", - "unassign-scheduler-event-from-edge-text": "After the confirmation the scheduler event will be unassigned and won't be accessible by the edge.", - "unassign-scheduler-events-from-edge-title": "Are you sure you want to unassign { count, plural, =1 {1 scheduler event} other {# scheduler events} }?", - "unassign-scheduler-events-from-edge-text": "After the confirmation all selected scheduler events will be unassigned and won't be accessible by the edge.", - "manage-user-groups": "Manage user groups", - "manage-asset-groups": "Manage asset groups", - "manage-device-groups": "Manage device groups", - "manage-dashboard-groups": "Manage dashboard groups", - "manage-entity-view-groups": "Manage entity view groups", - "manage-scheduler-events": "Manage scheduler events", - "manage-integrations": "Manage integrations", - "assign-scheduler-event-to-edge-title": "Assign Scheduler Events To Edge", - "assign-scheduler-event-to-edge-text": "Please select the scheduler events to assign to the edge", - "assign-entity-groups-to-edge-text": "Please select the entity groups to assign to the edge", - "assign-integration-to-edge-title": "Assign Integrations To Edge", - "assign-integration-to-edge-text": "Please select the integrations to assign to the edge", - "missing-attributes-title": "Edge has missing attribute(s)", - "missing-attributes-text": "Integration(s), assigned to edge, have attribute placeholders that are not available on this edge.

    List of missing edge attributes(s):
    {{missingEdgeAttributes}}", - "missing-all-related-attributes-title": "Edge(s) have missing attribute(s)", - "missing-all-related-attributes-text": "Edge(s), related to integration, don't have attributes that are present inside integration configuration as attribute placeholders.

    List of missing edge attributes(s):
    {{missingEdgeAttributes}}", - "quick-overview-widget-header": "{{edgeName}} Quick Overview" - }, - "edge-event": { - "type-dashboard": "Dashboard", - "type-asset": "Asset", - "type-device": "Device", - "type-device-profile": "Device Profile", - "type-asset-profile": "Asset Profile", - "type-entity-view": "Entity View", - "type-alarm": "Alarm", - "type-entity-group": "Entity Group", - "type-rule-chain": "Rule Chain", - "type-rule-chain-metadata": "Rule Chain Metadata", - "type-edge": "Edge", - "type-user": "User", - "type-tenant": "Tenant", - "type-tenant-profile": "Tenant Profile", - "type-customer": "Customer", - "type-relation": "Relation", - "type-widgets-bundle": "Widgets Bundle", - "type-widgets-type": "Widgets Type", - "type-admin-settings": "Admin Settings", - "type-ota-package": "Ota Package", - "type-queue": "Queue", - "type-scheduler-event": "Scheduler Event", - "type-white-labeling": "White Labeling", - "type-login-white-labeling": "Login White Labeling", - "type-custom-translation": "Custom Translation", - "type-role": "Role", - "type-group-permission": "Group Permission", - "type-integration": "Integration", - "type-converter": "Converter", - "action-type-added": "Added", - "action-type-deleted": "Deleted", - "action-type-updated": "Updated", - "action-type-post-attributes": "Post Attributes", - "action-type-attributes-updated": "Attributes Updated", - "action-type-attributes-deleted": "Attributes Deleted", - "action-type-timeseries-updated": "Timeseries Updated", - "action-type-credentials-updated": "Credentials Updated", - "action-type-assigned-to-customer": "Assigned to Customer", - "action-type-unassigned-from-customer": "Unassigned from Customer", - "action-type-relation-add-or-update": "Relation Add or Update", - "action-type-relation-deleted": "Relation Deleted", - "action-type-rpc-call": "RPC Call", - "action-type-alarm-ack": "Alarm Ack", - "action-type-alarm-clear": "Alarm Clear", - "action-type-alarm-assigned": "Alarm Assigned", - "action-type-alarm-unassigned": "Alarm Unassigned", - "action-type-assigned-to-edge": "Assigned to Edge", - "action-type-unassigned-from-edge": "Unassigned from Edge", - "action-type-credentials-request": "Credentials Request", - "action-type-entity-merge-request": "Entity Merge Request", - "action-type-added-to-entity-group": "Added to Entity Group", - "action-type-removed-from-entity-group": "Removed from Entity Group", - "action-type-change-owner": "Change owner" + "display-default-title": "Rodyti numatytąjį pavadinimą" }, - "error": { - "unable-to-connect": "Nepavyksta prisijungti prie serverio! Patikrinkite interneto ryšį.", - "unhandled-error-code": "Neapdorotos klaidos kodas: {{errorCode}}", - "unknown-error": "Nežinoma klaida" - }, - "entity": { - "entity": "Subjektas", - "entities": "Subjektai", - "entities-count": "Subjektų kiekis", - "alarms-count": "Įspėjimų kiekis", - "aliases": "Subjektų pseudonimai", - "aliases-short": "Pseudonimai", - "entity-alias": "Subjekto pseudonimas", - "unable-delete-entity-alias-title": "Subjekto pseudonimo panaikinti nepavyko", - "unable-delete-entity-alias-text": "Subjekto pseudonimas '{{entityAlias}}' negali būti panaikintas, nes jis naudojamas valdikliuose:
    {{widgetsList}}", - "duplicate-alias-error": "Pseudonimas '{{alias}}' dubliuojasi.
    Subjektų pseudonimai skydelyje negali kartotis.", - "missing-entity-filter-error": "Trūksta pseudonimo '{{alias}}' filtro.", - "configure-alias": "Konfigūruoti '{{alias}}' pseudonimą", - "alias": "Pseudonimas", - "alias-required": "Subjekto pseudonimas būtinas.", - "remove-alias": "Panaikinti subjekto pseudonimą", - "add-alias": "Pridėti subjekto pseudonimą", - "entity-list": "Subjektų sąrašas", - "entity-type": "Subjekto tipas", - "entity-types": "Subjektų tipai", - "entity-type-list": "Subjektų tipų sąrašas", - "any-entity": "Bet kuris subjektas", - "add-entity-type": "Pridėti subjekto tipą", - "enter-entity-type": "Įveskitesubjekto tipą", - "no-entities-matching": "Subjektų, atitinkančių '{{entity}}' nėra.", - "no-entity-types-matching": "Subjektų tipų, atitinkančių '{{entityType}}' nėra.", - "name-starts-with": "Pavadinimas prasideda", - "help-text": "naudokite simbolį '%' pagal tai, kaip norite ieškoti: '%subjekto_pavadinimo_fragmentas%', '%subjekto_pavadinimo_pabaiga', 'subjekto_pavadinimo_pradžia%'.", - "use-entity-name-filter": "Naudoti filtrą", - "entity-list-empty": "Subjektai nepasirinkti.", - "entity-type-list-required": "Nors vienas subjekto tipas turi būti pasirinktas.", - "entity-name-filter-required": "Subjekto vardo filtras būtinas.", - "entity-name-filter-no-entity-matched": "Subjektų, kurių pavadinimas prasideda '{{entity}}' nėra.", - "all-subtypes": "Visi", - "select-entities": "Pasirinkte subjektus", - "no-aliases-found": "Pseudonimų nėra.", - "no-alias-matching": "'{{alias}}' nėra.", - "create-new-alias": "Sukurti naują!", - "create-new": "Sukurti naują", - "key": "Raktas", - "key-name": "Rakto pavadinimas", - "no-keys-found": "Rakto nėra.", - "no-key-matching": "'{{key}}' nėra.", - "create-new-key": "Sukurti naują!", - "type": "Tipas", - "type-required": "Subjekto tipas būtinas.", - "type-device": "Įrenginys", - "type-devices": "Įrenginiai", - "list-of-devices": "{ count, plural, =1 {Vienas įrenginys} other {# įrenginių sąrašas} }", - "device-name-starts-with": "Įrenginiai, kurių pavadinimas prasideda '{{prefix}}'", - "type-device-profile": "Įrenginio profilis", - "type-device-profiles": "Device profiles", - "clear-selected-profiles": "Clear selected profiles", - "list-of-device-profiles": "{ count, plural, =1 {One device profile} other {List of # device profiles} }", - "device-profile-name-starts-with": "Device profiles whose names start with '{{prefix}}'", - "type-asset-profile": "Asset profile", - "type-asset-profiles": "Asset profiles", - "list-of-asset-profiles": "{ count, plural, =1 {One asset profile} other {List of # asset profiles} }", - "asset-profile-name-starts-with": "Asset profiles whose names start with '{{prefix}}'", - "type-asset": "Turtas", - "type-assets": "Turtas", - "list-of-assets": "{ count, plural, =1 {Turtas} other {# Turto sąrašas} }", - "asset-name-starts-with": "Turtas, kurio pavadinimas prasideda '{{prefix}}'", - "type-entity-view": "Subjekto rodinys", - "type-entity-views": "Subjektų rodinys", - "list-of-entity-views": "{ count, plural, =1 {Vienas subjekto rodinys} other {# Subjektų rodinių sąrašas} }", - "entity-view-name-starts-with": "Subjektų rodiniai, kurių pavadinimai prasideda '{{prefix}}'", - "type-rule": "Taisyklė", - "type-rules": "Taisyklės", - "list-of-rules": "{ count, plural, =1 {Viena taisyklė} other {# Taisyklių sąrašas} }", - "rule-name-starts-with": "Taisyklės, kurių pavadinimai prasideda '{{prefix}}'", - "type-plugin": "Papildinys", - "type-plugins": "Papildiniai", - "list-of-plugins": "{ count, plural, =1 {Vienas papildinys} other {# Papildinių sąrašas} }", - "plugin-name-starts-with": "Papildiniai, kurių pavadinimai prasideda '{{prefix}}'", - "type-tenant": "Valdytojas", - "type-tenants": "valdytojai", - "list-of-tenants": "{ count, plural, =1 {Vienas valdytojas} other {# Valdytojų sąrašas} }", - "tenant-name-starts-with": "Valdytojai, kurių pavadinimai prasideda '{{prefix}}'", - "type-tenant-profile": "Tenant profile", - "type-tenant-profiles": "Tenant profiles", - "list-of-tenant-profiles": "{ count, plural, =1 {One tenant profile} other {List of # tenant profiles} }", - "tenant-profile-name-starts-with": "Tenant profiles whose names start with '{{prefix}}'", - "type-customer": "Klientas", - "type-customers": "Klientai", - "list-of-customers": "{ count, plural, =1 {Vienas klientas} other {# Klientų sąrašas} }", - "customer-name-starts-with": "Klientai, kurių pavadinimai prasideda '{{prefix}}'", - "type-user": "Vartotojas", - "type-users": "Vartotojai", - "list-of-users": "{ count, plural, =1 {Vienas vartotojas} other {# Vartotojų sąrašas} }", - "user-name-starts-with": "Vartotojai, kurių pavadinimai prasideda '{{prefix}}'", - "type-dashboard": "Skydelis", - "type-dashboards": "Skydeliai", - "list-of-dashboards": "{ count, plural, =1 {Vienas skydelis} other {# Skydelių sąrašas} }", - "dashboard-name-starts-with": "Skydeliai, kurių pavadinimai prasideda '{{prefix}}'", - "type-alarm": "Įspėjimas", - "type-alarms": "Įspėjimai", - "list-of-alarms": "{ count, plural, =1 {Vienas įspėjimas} other {# Įspėjimų sąrašas} }", - "alarm-name-starts-with": "Įspėjimai, kurių pavadinimai prasideda '{{prefix}}'", - "type-rulechain": "Taisyklių grandinė", - "type-rulechains": "Taisyklių grandinės", - "list-of-rulechains": "{ count, plural, =1 {Viena taisyklių grandinė} other {# Taisyklių grandinių sąrašas} }", - "rulechain-name-starts-with": "Taisyklių grandinės, kurų pavadinimai prasideda '{{prefix}}'", - "type-scheduler-event": "Tvarkaraščio įvykis", - "type-scheduler-events": "Tvarkaraščio įvykiai", - "list-of-scheduler-events": "{ count, plural, =1 {Vienas tvarkaraščio įvykis} other {# Tvarkaraščio įvykių sąrašas} }", - "scheduler-event-name-starts-with": "Tvarkaraščio įvykiai, kurių pavadinimai prasideda '{{prefix}}'", - "type-blob-entity": "Blob entity", - "type-blob-entities": "Blob entities", - "list-of-blob-entities": "{ count, plural, =1 {One blob entity} other {List of # blob entities} }", - "blob-entity-name-starts-with": "Blob entities whose names start with '{{prefix}}'", - "type-rulenode": "Tasyklė", - "type-rulenodes": "Taisyklės", - "list-of-rulenodes": "{ count, plural, =1 {Viena taisyklė} other {# Taisyklių sąrašas} }", - "rulenode-name-starts-with": "Taisyklės, kurių pavadinimai prasideda '{{prefix}}'", - "type-current-customer": "Dabartinis klientas", - "type-current-tenant": "Dabartinis valdytojas", - "type-current-user": "Dabartinis vartotojas", - "type-current-user-owner": "Dabartinio vartotojo savininkas", - "type-widgets-bundle": "Valdiklių rinkinys", - "type-widgets-bundles": "Valdiklių rinkiniai", - "list-of-widgets-bundles": "{ count, plural, =1 {Vienas valdiklių rinkinys} other {# Valdiklių rinkinių sąrašas} }", - "type-widget-type": "Valdiklio tipas", - "type-widget-types": "Valdiklių tipai", - "list-of-widget-types": "{ count, plural, =1 {Vienas valdiklio tipas} other {# Valdiklių tipų sąrašas} }", - "search": "Subjektų paieška", - "selected-entities": "Pasirinkta { count, plural, =1 {1 subjektas} other {# subjektai} }", - "entity-name": "Subjekto pavadinimas", - "entity-label": "Subjekto etikelė", - "details": "Informacija apie subjektą", - "no-entities-prompt": "Subjektų nėra", - "no-data": "Duomenų atvaizdavimui nėra", - "columns-to-display": "Rodomi stulpeliai", - "type-api-usage-state": "Api Usage State", - "type-entity-group": "Subjektų grupė", - "type-converter": "Duomenų keitiklis", - "type-converters": "Duomenų keitikliai", - "list-of-converters": "{ count, plural, =1 {Vienas duomenų keitiklis} other {# Duomenų keitiklių sąrašas} }", - "converter-name-starts-with": "Duomenų keitikliai, kurių pavadinimai prasideda '{{prefix}}'", - "type-integration": "Integracija", - "type-integrations": "Integracijos", - "list-of-integrations": "{ count, plural, =1 {Viena integracija} other {# Integracijų sąrašas} }", - "integration-name-starts-with": "Integracijos, kurių pavadinimai prasideda '{{prefix}}'", - "type-role": "Rolė", - "type-roles": "Rolės", - "list-of-roles": "{ count, plural, =1 {Viena rolė} other {# rolių sąrašas} }", - "role-name-starts-with": "Rolės, kurių pavadinimai prasideda '{{prefix}}'", - "type-group-permission": "Grupės leidimai", - "type-edge": "Edge", - "type-edges": "Edges", - "list-of-edges": "{ count, plural, =1 {One edge} other {List of # edges} }", - "edge-name-starts-with": "Edges whose names start with '{{prefix}}'", - "type-tb-resource": "Resource", - "type-ota-package": "OTA package", - "type-rpc": "RPC", - "type-queue": "Queue", - "type-notification": "Notification", - "type-notification-rule": "Notification rule", - "type-notification-rules": "Notification rules", - "list-of-notification-rules": "{ count, plural, =1 {One notification rule} other {List of # notification rules} }", - "type-notification-target": "Notification recipient", - "type-notification-targets": "Notification recipients", - "list-of-notification-targets": "{ count, plural, =1 {One notification recipient} other {List of # notification recipients} }", - "type-notification-request": "Notification request", - "type-notification-template": "Notification template", - "type-notification-templates": "Notification templates", - "list-of-notification-templates": "{ count, plural, =1 {One notification template} other {List of # notification templates} }", - "customer-name": "Klientas", - "sub-customer-name": "Priklausantis klientas", - "parent-customer-name": "Pirminis klientas", - "parent-sub-customer-name": "Pirminis klientas", - "include-customer-entities": "Rodyti klientui priklausančius klientus", - "include-sub-customer-entities": "Rodyti priklausančių klientų subjektus", - "groups": "Grupė" - }, - "entity-group": { - "entity-group": "Subjektų grupė", - "details": "Informacija", - "columns": "Stulpeliai", - "add-column": "Pridėti stulpelį", - "column-value": "Reikšmė", - "column-value-required": "Stulpelio reikšmė būtina.", - "column-title": "Pavadinimas", - "default-sort-order": "Numatytoji rūšiavimo tvarka", - "default-sort-order-required": "Numatytoji rūšiavimo tvarka būtina.", - "hide-in-mobile-view": "Nerodyti mobiliąjame režime", - "use-cell-style-function": "Taikyti langelio stiliaus funkciją", - "use-cell-content-function": "Taikyti langelio turinio funkciją", - "edit-column": "Redaguoti stulpelį", - "column-details": "Stulpelio informacija", - "actions": "Veiksmai", - "settings": "Nustatymai", - "search": "Subjektų grupių paieška", - "delete": "Panaikinti subjektų grupę", - "name": "Pavadinimas", - "name-required": "Pavadinimas būtinas.", - "name-max-length": "Pavadinimas turi būti trumpesnis nei 256 simboliai", - "description": "Aprašymas", - "add": "Pridėti subjektų grupę", - "open-entity-group": "Atverti subjektų grupę", - "add-entity-group-text": "Pridėti naują subjektų grupę", - "no-entity-groups-text": "Subjektų grupių nėra", - "entity-group-details": "Informacija apie subjektų grupę", - "selected-entity-groups": "Pasirinkta { count, plural, =1 {1 subjektų grupė} other {# subjektų grupės} }", - "delete-entity-groups": "Panaikinti subjektų grupes", - "delete-entity-group-title": "Ar tikrai norite panaikinti subjektų grupę '{{entityGroupName}}'?", - "delete-entity-group-text": "Būkite dėmesingi, po patvirtinimo, subjektų grupė ir visa su ja susijusi informacija bus pašalinta ir jų atkurti nebegalėsite.", - "delete-entity-groups-title": "Ar tikrai norite panaikinti { count, plural, =1 {1 subjektų grupę} other {# subjektų grupes} }?", - "delete-entity-groups-action-title": "Panaikinti { count, plural, =1 {1 subjektų grupę} other {# subjektų grupes} }", - "delete-entity-groups-text": "Būkite dėmesingi, po patvirtinimo visos subjektų grupės ir su jomis susijusi informacija bus panaikintos ir jų atkurti nebegalėsite.", - "device-groups": "Įrenginių grupės", - "shared-device-groups": "Bendrinamos įrenginių grupės", - "asset-groups": "Turto grupės", - "shared-asset-groups": "Bendrinamos turto grupės", - "customer-groups": "Klientų grupės", - "shared-customer-groups": "Bendrinamos klientų grupės", - "device-group": "Įrenginių grupė", - "asset-group": "Turto grupė", - "user-group": "Vartotojų grupė", - "user-groups": "Vartotojų grupės", - "customer-group": "Klientų grupė", - "entity-view-groups": "Subjektų rodinių grupė", - "shared-entity-view-groups": "Bendrinamos subjektų rodinių grupės", - "entity-view-group": "Subjektų rodinių grupė", - "edge-groups": "Edge groups", - "shared-edge-groups": "Shared edge groups", - "edge-group": "Edge group", - "dashboard-groups": "Skydelių grupės", - "shared-dashboard-groups": "Bendrinamos skydelių grupės", - "dashboard-group": "Skydelių grupė", - "fetch-more": "Gauti daugiau", - "column-type": { - "column-type": "Stulpelio tipas", - "client-attribute": "Kliento atributas", - "shared-attribute": "Bendrinamas atributas", - "server-attribute": "Serverio atributas", - "timeseries": "Telemetrija", - "entity-field": "Subjekto laukas" + "gateway": { + "general-settings": "Bendrieji nustatymai", + "widget-title": "Valdiklio pavadinimas", + "default-archive-file-name": "Numatytasis archyvo failo pavadinimas", + "device-type-for-new-gateway": "Įrenginio tipas naujam tinklo šliuzui", + "messages-settings": "Pranešimų nustatymai", + "save-config-success-message": "Tekstas, rodomas sėkmingai išsaugojus šliuzo konfigūraciją", + "device-name-exists-message": "Tekstas, rodomas kai įrenginys su įvestu pavadinimu jau egzistuoja", + "gateway-title": "Šliuzo forma", + "read-only": "Tik skaitymui", + "events-title": "Šliuzo įvykių formos pavadinimas", + "events-filter": "Įvykių filtras", + "event-key-contains": "Įvykio rakte yra...", + "show-connector": "Rodyti jungtį", + "connector-state-param-key": "Jungties būsenos parametro raktas", + "message": "Pranešimas", + "level": "Lygis", + "created-time": "Sukūrimo laikas" + }, + "gauge": { + "default-color": "Numatytoji spalva", + "radial-gauge-settings": "Radialinio matuoklio nustatymai", + "ticks-settings": "Padalų nustatymai", + "min-value": "Minimali reikšmė", + "max-value": "Maksimali reikšmė", + "min-value-short": "min", + "max-value-short": "maks", + "start-ticks-angle": "Pradinis padalų kampas", + "ticks-angle": "Padalų kampas", + "major-ticks": "Pagrindinės padalos", + "major-ticks-count": "Pagrindinių padalų skaičius", + "major-ticks-color": "Pagrindinių padalų spalva", + "minor-ticks": "Antrinės padalos", + "minor-ticks-count": "Antrinių padalų skaičius", + "minor-ticks-color": "Antrinių padalų spalva", + "tick-numbers-font": "Padalų numerių šriftas", + "unit-title-settings": "Matavimo vieneto pavadinimo nustatymai", + "show-unit-title": "Rodyti vieneto pavadinimą", + "unit-title": "Vieneto pavadinimas", + "title-font": "Pavadinimo teksto šriftas", + "units-settings": "Vienetų nustatymai", + "units-font": "Vienetų teksto šriftas", + "value-box-settings": "Reikšmės laukelio nustatymai", + "show-value-box": "Rodyti reikšmės laukelį", + "value-box": "Reikšmės laukelis", + "value-int": "Skaitmenų skaičius sveikai reikšmės daliai", + "value-text": "Reikšmės tekstas", + "value-text-shadow": "Reikšmės teksto šešėlis", + "value-font": "Reikšmės teksto šriftas", + "rect-stroke-color-start": "Stačiakampio rėmelio spalva – gradiento pradžia", + "rect-stroke-color-end": "Stačiakampio rėmelio spalva – gradiento pabaiga", + "background-color": "Fono spalva", + "shadow-color": "Šešėlio spalva", + "value-box-rect-stroke-color": "Reikšmės laukelio rėmelio spalva", + "value-box-rect-stroke-color-end": "Reikšmės laukelio rėmelio spalva – gradiento pabaiga", + "value-box-background-color": "Reikšmės laukelio fono spalva", + "value-box-shadow-color": "Reikšmės laukelio šešėlio spalva", + "plate-settings": "Plokštės nustatymai", + "show-plate-border": "Rodyti plokštės rėmelį", + "plate-color": "Plokštės spalva", + "needle-settings": "Rodyklės nustatymai", + "needle-circle-size": "Rodyklės apskritimo dydis", + "needle-color": "Rodyklės spalva", + "needle-color-start": "Rodyklės spalva – gradiento pradžia", + "needle-color-end": "Rodyklės spalva – gradiento pabaiga", + "needle-color-shadow-up": "Viršutinės rodyklės dalies šešėlio spalva", + "needle-color-shadow-down": "Rodyklės metamo šešėlio spalva", + "highlights-settings": "Paryškinimų nustatymai", + "highlights-width": "Paryškinimų plotis", + "highlights": "Paryškinimai", + "highlight-from": "Nuo", + "highlight-to": "Iki", + "highlight-color": "Spalva", + "no-highlights": "Paryškinimai nesukonfigūruoti", + "add-highlight": "Pridėti paryškinimą", + "animation-settings": "Animacijos nustatymai", + "enable-animation": "Įjungti animaciją", + "animation-duration-rule": "Animacijos trukmės ir taisyklių nustatymas", + "animation-duration": "Animacijos trukmė", + "animation-rule": "Animacijos taisyklė", + "animation-linear": "Tiesinė", + "animation-quad": "Kvadratinė", + "animation-quint": "Penktos eilės", + "animation-cycle": "Ciklinė", + "animation-bounce": "Šokčiojanti", + "animation-elastic": "Elastinga", + "animation-dequad": "Atvirkštinė kvadratinė", + "animation-dequint": "Atvirkštinė penktos eilės", + "animation-decycle": "Atvirkštinis ciklas", + "animation-debounce": "Atšokimo mažinimas", + "animation-delastic": "Atvirkštinė elastinga", + "linear-gauge-settings": "Tiesinio matuoklio nustatymai", + "bar-stroke": "Juostos kontūras", + "bar-stroke-width": "Juostos kontūro plotis", + "bar-stroke-color": "Juostos kontūro spalva", + "bar-background-color": "Matuoklio juostos fono spalva", + "bar-background-color-end": "Juostos fono spalva – gradiento pabaiga", + "progress-bar-color": "Progreso juostos spalva", + "progress-bar": "Progreso juosta", + "progress-bar-color-start": "Progreso juostos spalva – gradiento pradžia", + "progress-bar-color-end": "Progreso juostos spalva – gradiento pabaiga", + "major-ticks-names": "Pagrindinių padalų pavadinimai", + "show-stroke-ticks": "Rodyti padalų kontūrus", + "major-ticks-font": "Pagrindinių padalų šriftas", + "border-color": "Rėmelio spalva", + "border-width": "Rėmelio plotis", + "needle-circle": "Rodyklės apskritimas", + "needle-circle-color": "Rodyklės apskritimo spalva", + "animation-target": "Animacijos objektas", + "animation-target-needle": "Rodyklė", + "animation-target-plate": "Plokštė", + "common-settings": "Bendrieji matuoklio nustatymai", + "gauge-type": "Matuoklio tipas", + "gauge-type-arc": "Lankas", + "gauge-type-donut": "Žiedas", + "gauge-type-horizontal-bar": "Horizontali juosta", + "gauge-type-vertical-bar": "Vertikali juosta", + "donut-start-angle": "Pradinis kampas", + "bar-settings": "Matuoklio juostos nustatymai", + "relative-bar-width": "Santykinis juostos plotis", + "neon-glow-brightness": "Neoninio švytėjimo ryškumas (0–100), 0 – išjungta", + "neon-glow-brightness-hint": "0 – išjungia efektą", + "stripes-thickness": "Juostelių storis, 0 – be juostelių", + "stripes-thickness-hint": "0 – be juostelių", + "rounded-line-cap": "Rodyti apvalintą linijos galą", + "bar-color-settings": "Juostos spalvų nustatymai", + "use-precise-level-color-values": "Naudoti tikslias spalvų reikšmes", + "bar-colors": "Juostos spalvos nuo žemiausios iki aukščiausios", + "color": "Spalva", + "no-bar-colors": "Juostos spalvos nesukonfigūruotos", + "add-bar-color": "Pridėti juostos spalvą", + "from": "Nuo", + "to": "Iki", + "fixed-level-colors": "Juostos spalvos pagal ribines reikšmes", + "gauge-title-settings": "Matuoklio pavadinimo nustatymai", + "show-gauge-title": "Rodyti matuoklio pavadinimą", + "gauge-title": "Matuoklio pavadinimas", + "gauge-title-font": "Matuoklio pavadinimo šriftas", + "unit-title-and-timestamp-settings": "Vieneto pavadinimo ir laiko žymos nustatymai", + "show-timestamp": "Rodyti reikšmės laiko žymą", + "timestamp-format": "Laiko žymos formatas", + "label-font": "Etiketės šriftas po reikšme", + "value-settings": "Reikšmės nustatymai", + "show-value": "Rodyti reikšmės tekstą", + "min-max-settings": "Minimalių/maksimalių reikšmių etikečių nustatymai", + "show-min-max": "Rodyti minimalias ir maksimalias reikšmes", + "min-max-font": "Minimalių ir maksimalių reikšmių šriftas", + "show-ticks": "Rodyti padalas", + "tick-width": "Padalos plotis", + "tick-color": "Padalos spalva", + "tick-values": "Padalų reikšmės", + "no-tick-values": "Padalų reikšmės nesukonfigūruotos", + "add-tick-value": "Pridėti padalos reikšmę", + "gauge-appearance": "Matuoklio išvaizda", + "units-title": "Vienetų pavadinimas", + "value": "Reikšmė", + "ticks": "Padalos", + "arrow-and-scale-color": "Rodyklės ir skalės numatytoji spalva", + "scale-settings": "Skalės nustatymai", + "scale": "Skalė", + "scale-color": "Skalės spalvos", + "compass-appearance": "Kompaso išvaizda", + "label": "Etiketė", + "labels": "Etiketės", + "label-style": "Etiketės stilius", + "simple-gauge-type": "Tipas", + "gauge-bar-background": "Matuoklio juostos fonas", + "bar-color": "Juostos spalva", + "min-and-max-value": "Minimalios ir maksimalios reikšmės", + "min-and-max-label": "Minimalios ir maksimalios etiketės", + "font": "Šriftas", + "tick-width-and-color": "Padalų plotis ir spalva", + "min-max-validation-text": "Maksimali reikšmė turi būti didesnė nei minimali" + }, + "gpio": { + "pin": "Kaištis", + "label": "Etiketė", + "row": "Eilutė", + "column": "Stulpelis", + "color": "Spalva", + "panel-settings": "Skydelio nustatymai", + "background-color": "Fono spalva", + "gpio-switches": "GPIO jungikliai", + "no-gpio-switches": "GPIO jungikliai nesukonfigūruoti", + "add-gpio-switch": "Pridėti GPIO jungiklį", + "gpio-status-request": "GPIO būsenos užklausa", + "method-name": "Metodo pavadinimas", + "method-body": "Metodo turinys", + "gpio-status-change-request": "GPIO būsenos keitimo užklausa", + "parse-gpio-status-function": "GPIO būsenos analizės funkcija", + "gpio-leds": "GPIO šviesos diodai", + "no-gpio-leds": "GPIO šviesos diodai nesukonfigūruoti", + "add-gpio-led": "Pridėti GPIO šviesos diodą" + }, + "html-card": { + "html": "HTML", + "css": "CSS" + }, + "input-widgets": { + "attribute-not-allowed": "Atributo parametras negali būti naudojamas šiame valdiklyje", + "blocked-location": "Geolokacija yra užblokuota jūsų naršyklėje", + "claim-device": "Priskirti įrenginį", + "claim-failed": "Nepavyko priskirti įrenginio!", + "claim-not-found": "Įrenginys nerastas!", + "claim-successful": "Įrenginys sėkmingai priskirtas!", + "date": "Data", + "device-name": "Įrenginio pavadinimas", + "device-name-required": "Įrenginio pavadinimas yra privalomas", + "discard-changes": "Atmesti pakeitimus", + "entity-attribute-required": "Objekto atributas yra privalomas", + "entity-coordinate-required": "Reikalingi abu laukai – platuma ir ilguma", + "entity-timeseries-required": "Objekto laiko seka yra privaloma", + "get-location": "Gauti dabartinę vietą", + "invalid-date": "Neteisinga data", + "latitude": "Platuma", + "longitude": "Ilguma", + "min-value-error": "Minimali reikšmė yra {{value}}", + "max-value-error": "Maksimali reikšmė yra {{value}}", + "not-allowed-entity": "Pasirinktas objektas negali turėti bendrų atributų", + "no-attribute-selected": "Nepasirinktas joks atributas", + "no-datakey-selected": "Nepasirinktas joks duomenų raktas", + "no-coordinate-specified": "Nenurodytas duomenų raktas platumai/ilgumai", + "no-entity-selected": "Nepasirinktas joks objektas", + "no-image": "Nėra paveikslėlio", + "no-support-geolocation": "Jūsų naršyklė nepalaiko geolokacijos", + "no-support-web-camera": "Jūsų naršyklė nepalaiko kamerų", + "enable-https-use-widget": "Įjunkite HTTPS, kad galėtumėte naudoti šį valdiklį", + "no-found-your-camera": "Kamera nerasta", + "no-permission-camera": "Naudotojas atmetė prieigą / svetainei nesuteikta leidimo naudoti kamerą", + "no-timeseries-selected": "Nepasirinkta jokia laiko seka", + "secret-key": "Slaptasis raktas", + "secret-key-required": "Slaptasis raktas yra privalomas", + "switch-attribute-value": "Perjungti objekto atributo reikšmę", + "switch-camera": "Perjungti kamerą", + "switch-timeseries-value": "Perjungti objekto laiko sekos reikšmę", + "take-photo": "Padaryti nuotrauką", + "time": "Laikas", + "timeseries-not-allowed": "Laiko sekos parametras negali būti naudojamas šiame valdiklyje", + "update-failed": "Atnaujinimas nepavyko", + "update-successful": "Atnaujinimas sėkmingas", + "update-attribute": "Atnaujinti atributą", + "update-timeseries": "Atnaujinti laiko seką", + "value": "Reikšmė", + "general-settings": "Bendrieji nustatymai", + "widget-title": "Valdiklio pavadinimas", + "claim-button-label": "Priskyrimo mygtuko etiketė", + "show-secret-key-field": "Rodyti laukelį „Slaptasis raktas“", + "labels-settings": "Etikečių nustatymai", + "show-labels": "Rodyti etiketes", + "device-name-label": "Etiketė įrenginio pavadinimo laukui", + "secret-key-label": "Etiketė slaptojo rakto laukui", + "messages-settings": "Pranešimų nustatymai", + "claim-device-success-message": "Tekstas, rodomas sėkmingai priskyrus įrenginį", + "claim-device-not-found-message": "Tekstas, rodomas, kai įrenginys nerastas", + "claim-device-failed-message": "Tekstas, rodomas nepavykus priskirti įrenginio", + "claim-device-name-required-message": "Klaidos pranešimas „Įrenginio pavadinimas privalomas“", + "claim-device-secret-key-required-message": "Klaidos pranešimas „Slaptasis raktas privalomas“", + "show-label": "Rodyti etiketę", + "label": "Etiketė", + "required": "Privaloma", + "required-error-message": "Klaidos pranešimas „Privaloma“", + "show-result-message": "Rodyti rezultatų pranešimą", + "integer-field-settings": "Sveikojo skaičiaus lauko nustatymai", + "min-value": "Minimali reikšmė", + "max-value": "Maksimali reikšmė", + "double-field-settings": "Skaičiaus su kableliu lauko nustatymai", + "text-field-settings": "Teksto lauko nustatymai", + "min-length": "Minimalus ilgis", + "max-length": "Maksimalus ilgis", + "checkbox-settings": "Žymimojo langelio nustatymai", + "true-label": "Pažymėto langelio etiketė", + "false-label": "Nepažymėto langelio etiketė", + "image-input-settings": "Paveikslėlio įvesties nustatymai", + "display-preview": "Rodyti peržiūrą", + "display-clear-button": "Rodyti išvalymo mygtuką", + "display-apply-button": "Rodyti taikymo mygtuką", + "display-discard-button": "Rodyti atmetimo mygtuką", + "datetime-field-settings": "Datos/laiko lauko nustatymai", + "display-time-input": "Rodyti laiko įvedimo lauką", + "latitude-key-name": "Platumos rakto pavadinimas", + "longitude-key-name": "Ilgumos rakto pavadinimas", + "show-get-location-button": "Rodyti mygtuką „Gauti dabartinę vietą“", + "use-high-accuracy": "Naudoti didelį tikslumą", + "location-fields-settings": "Vietos laukų nustatymai", + "latitude-label": "Etiketė platumai", + "longitude-label": "Etiketė ilgumai", + "input-fields-alignment": "Įvesties laukų lygiavimas", + "input-fields-alignment-column": "Stulpeliu (numatytasis)", + "input-fields-alignment-row": "Eilute", + "layout": "Išdėstymas", + "row-gap": "Tarpas tarp eilučių (pikseliais)", + "column-gap": "Tarpas tarp stulpelių (pikseliais)", + "latitude-field-required": "Platumos laukas yra privalomas", + "longitude-field-required": "Ilgumos laukas yra privalomas", + "attribute-settings": "Atributų nustatymai", + "widget-mode": "Valdiklio režimas", + "widget-mode-update-attribute": "Atnaujinti atributą", + "widget-mode-update-timeseries": "Atnaujinti laiko seką", + "attribute-scope": "Atributo sritis", + "attribute-scope-server": "Serverio atributas", + "attribute-scope-shared": "Bendras atributas", + "value-required": "Reikšmė privaloma", + "image-settings": "Paveikslėlio nustatymai", + "image-format": "Paveikslėlio formatas", + "image-format-jpeg": "JPEG", + "image-format-png": "PNG", + "image-format-webp": "WEBP", + "image-quality": "Paveikslėlio kokybė (nuostolinis glaudinimas, pvz., JPEG ar WEBP)", + "max-image-width": "Maksimalus paveikslėlio plotis", + "max-image-height": "Maksimalus paveikslėlio aukštis", + "action-buttons": "Veiksmo mygtukai", + "show-action-buttons": "Rodyti veiksmo mygtukus", + "update-all-values": "Atnaujinti visas reikšmes, o ne tik pakeistas", + "save-button-label": "Mygtuko „IŠSAUGOTI“ etiketė", + "reset-button-label": "Mygtuko „ATŠAUKTI“ etiketė", + "group-settings": "Grupės nustatymai", + "show-group-title": "Rodyti pavadinimą laukų grupei, susijusiai su skirtingais objektais", + "group-title": "Grupės pavadinimas", + "fields-alignment": "Laukų lygiavimas", + "fields-alignment-row": "Eilute (numatytasis)", + "fields-alignment-column": "Stulpeliu", + "fields-in-row": "Laukų skaičius eilutėje", + "option-value": "Reikšmė („null“ – tuščiam pasirinkimui)", + "option-label": "Etiketė", + "hide-input-field": "Slėpti įvesties lauką", + "datakey-type": "Duomenų rakto tipas", + "datakey-type-server": "Serverio atributas (numatytasis)", + "datakey-type-shared": "Bendras atributas", + "datakey-type-timeseries": "Laiko seka", + "datakey-value-type": "Duomenų rakto reikšmės tipas", + "datakey-value-type-string": "Tekstas", + "datakey-value-type-double": "Skaičius su kableliu", + "datakey-value-type-integer": "Sveikasis skaičius", + "datakey-value-type-json": "JSON", + "datakey-value-type-boolean-checkbox": "Loginis (žymimasis langelis)", + "datakey-value-type-boolean-switch": "Loginis (jungiklis)", + "datakey-value-type-date-time": "Data ir laikas", + "datakey-value-type-date": "Data", + "datakey-value-type-time": "Laikas", + "datakey-value-type-select": "Pasirinkimas", + "datakey-value-type-radio": "Pasirinkimo mygtukas", + "datakey-value-type-color": "Spalva", + "value-is-required": "Reikšmė yra privaloma", + "ability-to-edit-attribute": "Atributo redagavimo galimybė", + "ability-to-edit-attribute-editable": "Redaguojamas (numatytasis)", + "ability-to-edit-attribute-disabled": "Išjungtas", + "ability-to-edit-attribute-readonly": "Tik skaitymui", + "disable-on-datakey-name": "Išjungti, jei kito duomenų rakto reikšmė yra neteisinga (nurodykite rakto pavadinimą)", + "field-appearance": "Lauko išvaizda", + "appearance-fill": "Užpildytas", + "appearance-outline": "Kontūrinis", + "subscript-sizing": "Indekso dydis", + "subscript-sizing-fixed": "Fiksuotas", + "subscript-sizing-dynamic": "Dinaminis", + "slide-toggle-settings": "Perjungimo slankiklio nustatymai", + "slide-toggle-label-position": "Perjungimo etiketės padėtis", + "slide-toggle-label-position-after": "Po", + "slide-toggle-label-position-before": "Prieš", + "select-options": "Pasirinkimo parinktys", + "no-select-options": "Pasirinkimo parinktys nesukonfigūruotos", + "add-select-option": "Pridėti pasirinkimo parinktį", + "numeric-field-settings": "Skaitinio lauko nustatymai", + "step-interval": "Reikšmių žingsnio intervalas", + "error-messages": "Klaidų pranešimai", + "min-value-error-message": "Klaidos pranešimas „Minimali reikšmė“", + "max-value-error-message": "Klaidos pranešimas „Maksimali reikšmė“", + "invalid-date-error-message": "Klaidos pranešimas „Neteisinga data“", + "invalid-JSON-error-message": "Klaidos pranešimas „Neteisingas JSON formatas“", + "icon-settings": "Piktogramos nustatymai", + "dialog-editor-settings": "Dialogo redaktoriaus nustatymai", + "use-custom-icon": "Naudoti tinkintą piktogramą", + "input-cell-icon": "Piktograma prieš įvesties laukelį", + "value-conversion-settings": "Reikšmės konvertavimo nustatymai", + "get-value-settings": "Reikšmės gavimo nustatymai", + "use-get-value-function": "Naudoti getValue funkciją", + "get-value-function": "getValue funkcija", + "set-value-settings": "Reikšmės nustatymo nustatymai", + "use-set-value-function": "Naudoti setValue funkciją", + "set-value-function": "setValue funkcija", + "json-invalid": "JSON reikšmė turi neteisingą formatą", + "title": "Pavadinimas", + "cancel-button-label": "Mygtuko „Atšaukti“ etiketė", + "radio-button-settings": "Pasirinkimo mygtuko nustatymai", + "color": "Spalva", + "columns": "Stulpeliai", + "radio-options": "Pasirinkimo parinktys", + "no-radio-options": "Pasirinkimo parinktys nesukonfigūruotos", + "add-radio-option": "Pridėti pasirinkimo parinktį", + "radio-label-position": "Etiketės padėtis", + "radio-label-position-before": "Prieš", + "radio-label-position-after": "Po" + }, + "invalid-qr-code-text": "Neteisingas įvestas tekstas QR kodui. Įvestis turi būti teksto tipo", + "qr-code": { + "use-qr-code-text-function": "Naudoti QR kodo teksto funkciją", + "qr-code-text-pattern": "QR kodo teksto šablonas (pvz. '${entityName} | ${keyName} - papildomas tekstas.')", + "qr-code-text-pattern-hint": "QR kodo teksto šablonas naudoja pirmo rasto rakto reikšmę iš objektų, esančių objekto pseudonime.", + "qr-code-text-pattern-required": "QR kodo teksto šablonas yra privalomas.", + "qr-code-text-function": "QR kodo teksto funkcija" + }, + "label-widget": { + "label-pattern": "Šablonas", + "label-pattern-hint": "Patarimas: pvz. 'Tekstas ${keyName} vienetai.' arba ${#<rakto indeksas>} vienetai'", + "label-pattern-required": "Šablonas yra privalomas", + "label-position": "Padėtis (procentais fone)", + "x-pos": "X", + "y-pos": "Y", + "background-color": "Fono spalva", + "font-settings": "Šrifto nustatymai", + "background-image": "Fono paveikslėlis", + "labels": "Etiketės", + "no-labels": "Etiketės nesukonfigūruotos", + "add-label": "Pridėti etiketę" + }, + "navigation": { + "title": "Pavadinimas", + "navigation-path": "Naršymo kelias", + "filter-type": "Filtro tipas", + "filter-type-all": "Visi elementai", + "filter-type-include": "Įtraukti elementus", + "filter-type-exclude": "Išskirti elementus", + "items": "Elementai", + "enter-urls-to-filter": "Įveskite URL filtravimui..." + }, + "persistent-table": { + "rpc-id": "RPC ID", + "message-type": "Pranešimo tipas", + "method": "Metodas", + "params": "Parametrai", + "created-time": "Sukūrimo laikas", + "expiration-time": "Galiojimo pabaigos laikas", + "retries": "Bandymų skaičius", + "status": "Būsena", + "filter": "Filtras", + "refresh": "Atnaujinti", + "add": "Pridėti RPC užklausą", + "details": "Išsamiau", + "delete": "Ištrinti", + "delete-request-title": "Ištrinti nuolatinę RPC užklausą", + "delete-request-text": "Ar tikrai norite ištrinti šią užklausą?", + "details-title": "Išsami informacija RPC ID: ", + "additional-info": "Papildoma informacija", + "response": "Atsakas", + "any-status": "Bet kuri būsena", + "rpc-status-list": "RPC būsenų sąrašas", + "no-request-prompt": "Nėra rodomų užklausų", + "send-request": "Siųsti užklausą", + "add-title": "Sukurti nuolatinę RPC užklausą", + "method-error": "Metodas yra privalomas.", + "timeout-error": "Minimalus laukimo laikas yra 5000 (5 sekundės).", + "white-space-error": "Tarpai neleidžiami.", + "rpc-status": { + "QUEUED": "EILĖJE", + "SENT": "IŠSIŲSTA", + "DELIVERED": "PRISTATYTA", + "SUCCESSFUL": "SĖKMINGA", + "TIMEOUT": "PASIBAIGĖ LAIKAS", + "EXPIRED": "NEBEGALIOJA", + "FAILED": "NEPAVYKO" + }, + "rpc-search-status-all": "VISOS", + "message-types": { + "false": "Dvipusis", + "true": "Vienpusis" + }, + "general-settings": "Bendrieji nustatymai", + "enable-filter": "Įjungti filtrą", + "enable-sticky-header": "Rodyti antraštę slenkant", + "enable-sticky-action": "Rodyti veiksmų stulpelį slenkant", + "display-request-details": "Rodyti užklausos detales", + "allow-send-request": "Leisti siųsti RPC užklausą", + "allow-delete-request": "Leisti ištrinti RPC užklausą", + "columns-settings": "Stulpelių nustatymai", + "display-columns": "Rodytini stulpeliai", + "column": "Stulpelis", + "no-columns-found": "Nerasta jokių stulpelių", + "no-columns-matching": "„{{column}}“ nerasta." + }, + "range-chart": { + "chart": "Diagrama", + "data-zoom": "Duomenų mastelio keitimas", + "range-chart-appearance": "Intervalinės diagramos išvaizda", + "range-colors": "Intervalo spalvos", + "out-of-range-color": "Už intervalo ribų spalva", + "show-range-thresholds": "Rodyti intervalo ribas", + "range-thresholds-settings": "Intervalo ribų nustatymai", + "fill-area": "Užpildymo sritis", + "fill-area-opacity": "Užpildymo srities nepermatomumas", + "range-chart-style": "Intervalinės diagramos stilius" + }, + "knob": { + "behavior": "Elgsena", + "initial-value": "Pradinė reikšmė", + "initial-value-hint": "Veiksmas, skirtas gauti pradinę rankenėlės reikšmę.", + "on-value-change": "Pasikeitus reikšmei", + "on-value-change-hint": "Veiksmas, vykdomas, kai rankenėlės reikšmė pasikeičia.", + "range": "Intervalas", + "min": "min", + "max": "maks", + "value": "Reikšmė", + "fallback-initial-value": "Atsarginė pradinė reikšmė" + }, + "rpc": { + "value-settings": "Reikšmės nustatymai", + "initial-value": "Pradinė reikšmė", + "retrieve-value-settings": "Įjungimo/išjungimo reikšmės gavimo nustatymai", + "retrieve-value-method": "Reikšmės gavimo metodas", + "retrieve-value-method-none": "Negauti reikšmės", + "retrieve-value-method-rpc": "Gauti reikšmę naudojant RPC metodą", + "retrieve-value-method-attribute": "Prenumeruoti atributą", + "retrieve-value-method-timeseries": "Prenumeruoti laiko seką", + "attribute-value-key": "Atributo raktas", + "timeseries-value-key": "Laiko sekos raktas", + "get-value-method": "RPC reikšmės gavimo metodas", + "parse-value-function": "Reikšmės analizės funkcija", + "update-value-settings": "Reikšmės atnaujinimo nustatymai", + "set-value-method": "RPC reikšmės nustatymo metodas", + "convert-value-function": "Reikšmės konvertavimo funkcija", + "rpc-settings": "RPC nustatymai", + "request-timeout": "RPC užklausos laukimo laikas (ms)", + "persistent-rpc-settings": "Nuolatinės RPC nustatymai", + "request-persistent": "Nuolatinė RPC užklausa", + "persistent-polling-interval": "Apklausos intervalas (ms) nuolatinei RPC komandai gauti", + "common-settings": "Bendrieji nustatymai", + "switch-title": "Jungiklio pavadinimas", + "show-on-off-labels": "Rodyti įjungta/išjungta etiketes", + "slide-toggle-label": "Perjungimo etiketė", + "label-position": "Etiketės padėtis", + "label-position-before": "Prieš", + "label-position-after": "Po", + "slider-color": "Slankiklio spalva", + "slider-color-primary": "Pagrindinė", + "slider-color-accent": "Akcentinė", + "slider-color-warn": "Įspėjamoji", + "button-style": "Mygtuko stilius", + "button-raised": "Iškilęs mygtukas", + "button-primary": "Pagrindinė spalva", + "button-background-color": "Mygtuko fono spalva", + "button-text-color": "Mygtuko teksto spalva", + "widget-title": "Valdiklio pavadinimas", + "button-label": "Mygtuko etiketė", + "device-attribute-scope": "Įrenginio atributo sritis", + "server-attribute": "Serverio atributas", + "shared-attribute": "Bendras atributas", + "device-attribute-parameters": "Įrenginio atributo parametrai", + "is-one-way-command": "Vienpusė komanda", + "rpc-method": "RPC metodas", + "rpc-method-params": "RPC metodo parametrai", + "show-rpc-error": "Rodyti RPC komandos vykdymo klaidą", + "led-title": "LED pavadinimas", + "led-color": "LED spalva", + "check-status-settings": "Būsenos tikrinimo nustatymai", + "perform-rpc-status-check": "Vykdyti RPC įrenginio būsenos patikrinimą", + "retrieve-led-status-value-method": "LED būsenos gavimo metodas", + "led-status-value-attribute": "Įrenginio atributas, kuriame saugoma LED būsena", + "led-status-value-timeseries": "Įrenginio laiko seka, kurioje saugoma LED būsena", + "check-status-method": "RPC įrenginio būsenos tikrinimo metodas", + "parse-led-status-value-function": "LED būsenos reikšmės analizės funkcija", + "knob-title": "Rankenėlės pavadinimas" + }, + "maps": { + "map-type": { + "type": "Žemėlapio tipas", + "map": "Žemėlapis", + "image": "Paveikslėlis" + }, + "image": { + "image-source": "Paveikslėlio šaltinis", + "image-source-image": "Paveikslėlis", + "image-source-entity-key": "Objekto raktas", + "source-entity-alias": "Šaltinio objekto pseudonimas", + "image-url-key": "Paveikslėlio URL raktas", + "image-url-key-required": "Paveikslėlio URL raktas yra privalomas" + }, + "control": { + "map-controls": "Žemėlapio valdikliai", + "position": "Padėtis", + "position-topleft": "Viršuje kairėje", + "position-topright": "Viršuje dešinėje", + "position-bottomleft": "Apačioje kairėje", + "position-bottomright": "Apačioje dešinėje", + "zoom-actions": "Mastelio keitimo veiksmai", + "zoom-scroll": "Slinkimas", + "zoom-double-click": "Dvigubas paspaudimas", + "zoom-control-buttons": "Valdymo mygtukai", + "scale": "Mastelis", + "scale-metric": "Metrinis", + "scale-imperial": "Imperinis", + "switch-to-drag-mode-using-button": "Perjungti į tempimo režimą naudojant mygtuką" + }, + "timeline": { + "control-panel": "Laiko juostos valdymo skydelis", + "time-step": "Laiko žingsnis", + "speed-options": "Greičio parinktys", + "timestamp": "Laiko žyma", + "snap-to-real-location": "Pririšti prie tikros vietos", + "location-snap-filter-function": "Vietos pririšimo filtravimo funkcija", + "no-trips-data-available": "Kelionių duomenų nėra" + }, + "map-action": { + "map-action-buttons": "Žemėlapio veiksmų mygtukai", + "label": "Etiketė", + "icon": "Piktograma", + "color": "Spalva", + "action": "Veiksmas", + "add-button": "Pridėti mygtuką", + "no-action-buttons-configured": "Veiksmų mygtukai nesukonfigūruoti", + "remove-action-button": "Pašalinti veiksmų mygtuką", + "map-action-button": "Žemėlapio veiksmų mygtukas", + "button-requires": "Mygtukui reikalinga etiketė arba piktograma" + }, + "common": { + "common-map-settings": "Bendrieji žemėlapio nustatymai", + "fit-map-bounds": "Pritaikyti žemėlapio ribas, kad apimtų visus žymeklius", + "default-map-center-position": "Numatytoji žemėlapio centro padėtis", + "default-map-zoom-level": "Numatytasis žemėlapio mastelio lygis", + "entities-limit": "Įkeltinų objektų riba" + }, + "layer": { + "label": "Etiketė", + "layer": "Sluoksnis", + "layers": "Sluoksniai", + "map-layers": "Žemėlapio sluoksniai", + "add-layer": "Pridėti sluoksnį", + "layer-settings": "Sluoksnio nustatymai", + "remove-layer": "Pašalinti sluoksnį", + "no-layers": "Sluoksniai nesukonfigūruoti", + "roadmap": "Kelio žemėlapis", + "satellite": "Palydovinis", + "hybrid": "Hibridinis", + "reference": { + "reference-layer": "Etaloninis sluoksnis", + "no-layer": "Nėra sluoksnio", + "openstreetmap-hybrid": "OpenStreetMap hibridinis", + "world-edition-hybrid": "World Edition hibridinis", + "enhanced-contrast-hybrid": "Padidinto kontrasto hibridinis" }, - "column-type-required": "Stulpelio tipas būtinas.", - "entity-field": { - "created-time": "Sukūrimo laikas", - "name": "Pavadinimas", - "type": "Tipas", - "device_profile": "Device profile", - "asset_profile": "Asset profile", - "assigned_customer": "Priskirtas klientas", - "authority": "Authority", - "first_name": "Vardas", - "last_name": "Pavardė", - "email": "El. paštas", - "title": "Pavadinimas", - "country": "Šalis", - "state": "Apskritis/rajonas", - "city": "Miestas", - "address": "Adresas", - "address2": "Adresas 2", - "zip": "Pašto kodas", - "phone": "Tel, numeris", - "label": "Etiketė" + "provider": { + "provider": "Tiekėjas", + "openstreet": { + "title": "OpenStreet", + "mapnik": "Mapnik", + "hot": "HOT", + "esri-street": "WorldStreetMap", + "esri-topo": "WorldTopoMap", + "esri-imagery": "WorldImagery", + "cartodb-positron": "Positron", + "cartodb-dark-matter": "DarkMatter" + }, + "google": { + "title": "Google", + "roadmap": "Kelio žemėlapis", + "satellite": "Palydovinis", + "hybrid": "Hibridinis", + "terrain": "Reljefas" + }, + "here": { + "title": "HERE", + "normal-day": "Įprasta diena", + "normal-night": "Įprasta naktis", + "hybrid-day": "Hibridinė diena", + "terrain-day": "Reljefinė diena" + }, + "tencent": { + "title": "Tencent", + "normal": "Įprastas", + "satellite": "Palydovinis", + "terrain": "Reljefinis" + }, + "custom": { + "title": "Tinkintas", + "tile-url": "Plytelės URL" + } }, - "sort-order": { - "asc": "Didėjanti", - "desc": "Mažėjanti", - "none": "Nėra" + "credentials": { + "credentials": "Prisijungimo duomenys", + "api-key": "API raktas" + } + }, + "overlays": { + "overlays": "Perdangos", + "overlays-hint": "Sukonfigūruokite duomenų šaltinius, išvaizdą, elgseną, redagavimo parinktis ir objektų grupavimą žemėlapyje", + "trips": "Kelionės", + "markers": "Žymekliai", + "polygons": "Daugiakampiai", + "circles": "Apskritimai" + }, + "data-layer": { + "source": "Šaltinis", + "filter": "Filtras", + "additional-data-keys": "Papildomi duomenų raktai", + "additional-datasources": "Papildomi duomenų šaltiniai", + "additional-datasources-hint": "Duomenų šaltinis, skirtas pasiekti atributus ar telemetriją iš objektų, nerodomų žemėlapyje; gali būti naudojamas žemėlapio perdangų funkcijose.", + "more-datasources": "Daugiau duomenų šaltinių", + "data-keys": "Duomenų raktai", + "add-datasource": "Pridėti duomenų šaltinį", + "no-datasources": "Duomenų šaltiniai nesukonfigūruoti", + "remove-datasource": "Pašalinti duomenų šaltinį", + "behavior": "Elgsena", + "on-click": "Paspaudus", + "on-click-hint": "Veiksmas, vykdomas, kai naudotojas paspaudžia ant žemėlapio elemento.", + "groups": "Grupės", + "groups-hint": "Grupių pavadinimų sąrašas, priskirtas perdangai, naudojamas jos matomumui žemėlapyje keisti.", + "color": "Spalva", + "color-settings": "Spalvos nustatymai", + "color-type-constant": "Pastovi", + "color-type-range": "Intervalas", + "color-type-function": "Funkcija", + "color-range-source-key": "Spalvų intervalo šaltinio raktas", + "color-range-source-key-required": "Spalvų intervalo šaltinio raktas yra privalomas", + "color-range": "Spalvų intervalas", + "color-function": "Spalvos funkcija", + "label": "Etiketė", + "tooltip": "Patarimas", + "pattern-type-pattern": "Šablonas", + "pattern-type-function": "Funkcija", + "label-pattern": "Etiketė (šablonų pavyzdžiai: '${entityName}', '${entityName}: (Tekstas ${keyName} vienetai.)')", + "label-function": "Etiketės funkcija", + "tooltip-pattern": "Patarimas (pvz. 'Tekstas ${keyName} vienetai.' arba Nuorodos tekstas)", + "tooltip-function": "Patarimo funkcija", + "tooltip-trigger": "Patarimo aktyvatorius", + "tooltip-trigger-click": "Rodyti patarimą paspaudus", + "tooltip-trigger-hover": "Rodyti patarimą užvedus žymeklį", + "auto-close-tooltips": "Automatiškai uždaryti patarimus", + "tooltip-offset": "Patarimo poslinkis", + "tooltip-offset-horizontal": "Horizontalus", + "tooltip-offset-vertical": "Vertikalus", + "tooltip-tag-actions": "Žymų veiksmai", + "add-tooltip-tag-action": "Pridėti žymos veiksmą", + "edit-tooltip-tag-action": "Redaguoti žymos veiksmą", + "remove-tooltip-tag-action": "Pašalinti žymos veiksmą", + "action-add": "Pridėti", + "action-edit": "Redaguoti", + "action-move": "Perkelti", + "action-remove": "Pašalinti", + "edit-instruments": "Įrankiai", + "persist-location-attribute-scope": "Atributo sritis vietos išsaugojimui", + "enable-snapping": "Įjungti pritraukimą prie kitų viršūnių tiksliam braižymui", + "enable-snapping-hint": "Automatiškai sulygiuoja naujus taškus su esamomis formomis, kad braižymas būtų tikslesnis.", + "drag-drop-mode": "Vilkimo režimas", + "trip": { + "no-trips": "Kelionės nesukonfigūruotos", + "add-trip": "Pridėti kelionę", + "trip-configuration": "Kelionės konfigūracija", + "remove-trip": "Pašalinti kelionę" }, - "details-mode": { - "on-row-click": "Spustelėjus eilutėje", - "on-action-button-click": "Paspaudus informacijos mygtuką", - "disabled": "Išjungta" + "marker": { + "marker": "Žymeklis", + "latitude-key": "Platumos raktas", + "longitude-key": "Ilgumos raktas", + "x-pos-key": "X padėties raktas", + "y-pos-key": "Y padėties raktas", + "latitude-key-required": "Platumos raktas yra privalomas", + "longitude-key-required": "Ilgumos raktas yra privalomas", + "x-pos-key-required": "X padėties raktas yra privalomas", + "y-pos-key-required": "Y padėties raktas yra privalomas", + "no-markers": "Žymekliai nesukonfigūruoti", + "add-marker": "Pridėti žymeklį", + "marker-configuration": "Žymeklio konfigūracija", + "remove-marker": "Pašalinti žymeklį", + "marker-type": "Žymeklio tipas", + "marker-type-shape": "Forma", + "marker-type-icon": "Piktograma", + "marker-type-image": "Paveikslėlis", + "shape": "Forma", + "icon": "Piktograma", + "image": "Paveikslėlis", + "marker-shapes": "Žymeklio formos", + "marker-icon": "Žymeklio piktograma", + "marker-appearance": "Žymeklio išvaizda", + "marker-image": "Žymeklio paveikslėlis", + "marker-image-type-image": "Paveikslėlis", + "marker-image-type-function": "Funkcija", + "custom-marker-image-size": "Tinkintas žymeklio paveikslėlio dydis", + "marker-image-function": "Žymeklio paveikslėlio funkcija", + "marker-images": "Žymeklio paveikslėliai", + "marker-offset": "Žymeklio poslinkis", + "offset-horizontal": "Horizontalus", + "offset-vertical": "Vertikalus", + "rotate-marker": "Pasukti žymeklį", + "offset-angle": "Poslinkio kampas", + "position-conversion": "Padėties konversija", + "position-conversion-function": "Padėties konversijos funkcija, turi grąžinti x,y koordinates (dvigubo tipo) nuo 0 iki 1", + "clustering": { + "use-map-markers-clustering": "Naudoti žemėlapio žymeklių grupavimą", + "zoom-on-cluster-click": "Priartinti paspaudus ant grupės", + "max-zoom": "Didžiausias mastelio lygis, kai žymeklis gali priklausyti grupei (0–18)", + "max-radius": "Didžiausias spindulys, kurį apima grupė", + "zoom-animation": "Žymeklių animacija keičiat mastelį", + "bounds-on-cluster-mouse-over": "Rodyti žymeklių ribas užvedus pelę ant grupės", + "spiderfy-max-zoom-level": "Išskleisti grupės žymeklius esant didžiausiam masteliui (kad matytųsi visi)", + "load-optimization": "Įkėlimo optimizavimas", + "chunked-load": "Naudoti dalinį įkėlimą, kad puslapis neužstrigtų", + "lazy-load": "Naudoti uždelstą įkėlimą žymekliams pridėti", + "use-cluster-marker-color-function": "Naudoti grupės žymeklių spalvos funkciją", + "marker-color-function": "Žymeklio spalvos funkcija" + }, + "edit": "Redaguoti žymeklį", + "remove-marker-for": "Pašalinti žymeklį objektui '{{entityName}}'", + "place-marker": "Padėti žymeklį", + "place-marker-hint": "Spustelėkite, kad padėtumėte žymeklį", + "place-marker-hint-with-entity": "Spustelėkite, kad padėtumėte objektą '{{entityName}}'" }, - "change-owner": "Pakeisti savininką", - "select-target-owner": "Pasirinkite savininką", - "no-owners-matching": "Savinikų atitinkančių '{{owner}}' nėra.", - "target-owner-required": "Savininkas būtinas.", - "confirm-change-owner-title": "Ar tikrai norite pakeisti savininką { count, plural, =1 {1 pasirinktam subjektui} other {# pasirinktiems subjektams} }?", - "confirm-change-owner-text": "Būkite dėmesingi, po patvirtinimo, visi pasirinkti subjektai iš esamo savininko bus perkelti į naujo savininko grupę 'Visi'.", - "add-to-group": "Pridėti į grupę", - "move-to-group": "Perkelti į grupę", - "select-entity-group": "Pasirinkite subjektų grupę", - "no-entity-groups-matching": "Subjektų grupių, atitinkančių '{{entityGroup}}' nėra.", - "create-new-entity-group": "Sukurti naują!", - "target-entity-group-required": "Subjektų grupė būtina.", - "select-user-group": "Pasirinkite vartotojų grupę", - "no-user-groups-matching": "Vartotojų grupių, atitinkančių '{{entityGroup}}' nėra.", - "target-user-group-required": "Vartotojų grupė būtina.", - "remove-from-group": "Pašalinti iš grupės", - "group-table-title": "Grupių lentelės pavadinimas", - "enable-search": "Leisti subjektų paiešką", - "enable-add": "Leisti pridėti subjektus", - "enable-delete": "Leisti panaikinti subjektus", - "enable-selection": "Leisti subjektų pasirinkimą", - "enable-group-transfer": "Leisti perkėlimus tarp grupių", - "display-pagination": "Rodyti dalinimą puslapiais", - "default-page-size": "Numatytasis puslapio dydis", - "enable-assignment-actions": "Leisti priskirti", - "enable-credentials-management": "Leisti valdyti įgaliojimus", - "enable-login-as-user": "Leisti prisijungti kaip vartotojui", - "enable-users-management": "Leisti valdyti vartotojus", - "enable-customers-management": "Leisti valdyti klientus", - "enable-assets-management": "Leisti valdyti turtą", - "enable-devices-management": "Leisti valdyti įrenginius", - "enable-entity-views-management": "Leisti subjektų rodinių valdymą", - "enable-edges-management": "Enable edges management", - "enable-dashboards-management": "Leisti skydelių valdymą", - "enable-scheduler-events-management": "Leisti tvarkaraščio įvykių valdymą", - "open-details-on": "Atverti subjekto informaciją", - "select-existing": "Pasirinkti egzistuojančią subjektų grupę", - "create-new": "Sukurti naują subjektų grupę", - "new-entity-group-name": "Naujos subjektų grupės pavadinimas", - "entity-group-list": "Subjektų grupių sąrašas", - "entity-group-list-empty": "Nepasirinktos subjektų grupės.", - "name-starts-with": "Subjektų grupės pavadinimas prasideda", - "entity-group-name-filter-required": "Subjektų grupės filtras būtinas.", - "roles": "Rolės", - "permissions": "Leidimai", - "public": "Vieša", - "entity-group-public": "Subjektų grupė yra vieša", - "make-public": "Subjektų grupę padaryti vieša", - "make-private": "Subjektų grupę padaryti privačia", - "make-public-entity-group-title": "Ar tikrai norite subjektų grupę '{{entityGroupName}}' padaryti vieša?", - "make-public-entity-group-text": "Po patvirtinimo subjektų grupė ir visi jai priklausantys subjektai taps vieši ir matomi kitiems vartotojams.", - "make-private-entity-group-title": "Ar tikrai norite subjektų grupę '{{entityGroupName}}' padaryti privačia?", - "make-private-entity-group-text": "Po patvirtinimo, subjektų grupė ir visi jai priklausantys subjektai taps privatūs ir nematomi kitiems vartotojams.", - "share": "Pasdalinti grupe", - "copyId": "Kopijuoti subjektų grupės Id", - "idCopiedMessage": "Subjektų grupės Id nukopijuotas į škarpinę", - "entity-group-name": "Subjektų grupės pavadinimas", - "all-users": "Visi vartotojai", - "owner": "Savininkas", - "owner-required": "Savininkas būtinas.", - "manage-owner-and-groups": "Savininkas ir grupės", - "owner-and-groups": "Savininkas ir grupės" - }, - "entity-field": { - "created-time": "Sukūrimo laikas", - "name": "Pavadinimas", - "type": "Tipas", - "first-name": "Vardas", - "last-name": "Pavardė", - "email": "El. paštas", - "title": "Pavadinimas", - "country": "Šalis", - "state": "Rajonas", - "city": "Miestas", - "address": "Addresas", - "address2": "Addresas 2", - "zip": "Pašto kodas", - "phone": "Telefonas", - "label": "Etikelė", - "configuration": "Konfigūracija", - "schedule": "Grafikas", - "originatorId": "Iniciatoriaus Id", - "originatorType": "Iniciatoriaus tipas" - }, - "entity-view": { - "all": "Visi", - "all-entity-views": "Visi subjektų rodiniai", - "groups": "Grupės", - "shared": "bendrinami", - "entity-view": "Subjektų rodiniai", - "entity-view-required": "Subjeto rodinys būtinas.", - "entity-views": "Subjektų rodiniai", - "management": "Subjektų rodinių valdymas", - "view-entity-views": "Peržiūrėti subjektų rodinius", - "entity-view-alias": "Subjektų rodinio pseudonimas", - "aliases": "Subjektų rodinio pseudonimai", - "no-alias-matching": "'{{alias}}' nerastas.", - "no-aliases-found": "Pseudonimų nėra.", - "no-key-matching": "'{{key}}' nerastas.", - "no-keys-found": "Raktų nėra.", - "create-new-alias": "Sukurti naują!", - "create-new-key": "Sukurti naują!", - "duplicate-alias-error": "'{{alias}}' dubliuojasi.
    Įrenginio pseudonimai skydelyje turi būti unikalūs.", - "configure-alias": "Konfigūruoti '{{alias}}' pseudonimą", - "no-entity-views-matching": "Subjektų rodinių, atitinkančių '{{entity}}' nėra.", - "public": "Viešas", - "alias": "Pseudonimas", - "alias-required": "Subjekto rodinio pseudonimas būtinas.", - "remove-alias": "Panaikinti subjekto rodinio pseudonimą", - "add-alias": "Pridėti subjekto rodinio pseudonimą", - "name-starts-with": "Subjekto rodinio pavadinimas prasideda", - "help-text": "Naudokite '%' simbolį pagal tai, kaip norite ieškoti: '%subjekto_rodinio_pavadinimo_fragmentas%', '%subjekto_rodinio_pavadinimo_pabaiga', 'subjekto_rodinio_pavadinimo_pradžia%'.", - "entity-view-list": "Subjektų rodinių sąrašas", - "use-entity-view-name-filter": "Naudoti filtrą", - "entity-view-list-empty": "Subjektai nepasirinkti.", - "entity-view-name-filter-required": "Subjekto rodinio pavadinimo filtras būtinas.", - "entity-view-name-filter-no-entity-view-matched": "Subjektų rodinių, kurių pavadinimai prasideda '{{entityView}}' nėra.", - "add": "Pridėti subjektų rodinį", - "entity-view-public": "Subjektų rodnys yra viešas", - "assign-to-customer": "Priskirti klientui", - "assign-entity-view-to-customer": "Subjektų rodinį (-ius) priskirti klientui", - "assign-entity-view-to-customer-text": "Pasirinkite subjektų rodinius, kuriuos norite priskirti klientui", - "no-entity-views-text": "Subjektų rodinių nėra", - "assign-to-customer-text": "Pasirinkite klientą, kuriam priskirti subjektų rodinį (-ius)", - "entity-view-details": "Informacija apie subjektų rodinį", - "add-entity-view-text": "Pridėti naują subjektų rodinį", - "delete": "Panikinti subjektų rodinį", - "assign-entity-views": "Priskirti subjektų rodinius", - "assign-entity-views-text": "Priskirti { count, plural, =1 {1 subjektų rodinį} other {# subjektų rodinius} } klientui", - "delete-entity-views": "Panaikinti subjektų rodinius", - "make-public": "Subjektų rodinį padaryti viešu", - "make-private": "Subjektų rodinį padaryti privačiu", - "unassign-from-customer": "Atsieti nuo kliento", - "unassign-entity-views": "Atsieti subjektų rodinius", - "unassign-entity-views-action-title": "Atsieti { count, plural, =1 {1 subjektų rodinį} other {# subjektų rodinius} } nuo kliento", - "assign-new-entity-view": "Priskirti naują subjektų rodinį", - "delete-entity-view-title": "Ar tikrai norite panaikinti subjektų rodinį '{{entityViewName}}'?", - "delete-entity-view-text": "Būkite dėmesingi, po patvirtinimo, subjektų rodinys ir visa su juo susijusi informacija bus panaikinta ir jų atkurti nebegalėsite.", - "delete-entity-views-title": "Ar tikrai norite panaikinti { count, plural, =1 {1 subketų rodinį} other {# subjektų rodinius} }?", - "delete-entity-views-action-title": "Panaikinti { count, plural, =1 {1 subjektų rodinį} other {# subjektų rodinius} }", - "delete-entity-views-text": "Būkite dėmesingi, po patvirtinimo, visi pasirinkti subjektų rodiniai ir visa su jais susijusi informacija bus panaikinta ir jų atkurti nebegalėsite.", - "make-public-entity-view-title": "Ar tikrai norite subjektų rodinį '{{entityViewName}}' padaryti viešu?", - "make-public-entity-view-text": "Po patvirtinimo subjektų rodinys ir visa su juo susijusi informacija taps vieša ir matoma kitiems vartotojams.", - "make-private-entity-view-title": "Ar tikrai norite subjektų rodinį '{{entityViewName}}' padaryti privačiu?", - "make-private-entity-view-text": "Po patvirtinimo subjektų rodinys ir visa su juo susijusi informacija taps privati ir kiti vartotojai jų nebematys.", - "unassign-entity-view-title": "Ar tikrai norite atsieti subjektų rodinį '{{entityViewName}}'?", - "unassign-entity-view-text": "Po patvirtinimo subjektų rodinys bus atsietas ir klientas jo nebematys.", - "unassign-entity-view": "Atsieti subjektų rodinį", - "unassign-entity-views-title": "Ar tikrai norite atsieti { count, plural, =1 {1 subjektų rodinį} other {# subjektų rodinius} }?", - "unassign-entity-views-text": "Po patvirtinimo visi pasirinkti subjektų rodiniai bus atsieti ir klientas jų nebematys.", - "entity-view-type": "Subjektų rodinio tipas", - "entity-view-type-required": "Subjektų rodinio tipas būtinas.", - "select-entity-view-type": "Pasirinkite subjektų rodinio tipą", - "enter-entity-view-type": "Įveskite subjektų rodinio tipą", - "any-entity-view": "Bet kuris subjektų tipas", - "no-entity-view-types-matching": "Subjektų rodinių, atitinkančių '{{entitySubtype}}' nėra.", - "entity-view-type-list-empty": "Subjektų tipai nepasirinkti.", - "entity-view-types": "Subjektų rodinio tipai", - "created-time": "Sukūrimo laikas", - "name": "Pavadinimas", - "name-required": "Pavadinimas būtinas.", - "name-max-length": "Pavadinimas negali viršyti 256 simbolių", - "type-max-length": "Rodinio tipas negali viršyti 256 simbolių", - "description": "Aprašymas", - "events": "Įvykiai", - "details": "Informacija", - "copyId": "Kopijuoti subjekto rodinio Id", - "idCopiedMessage": "Subjektų rodinio Id nukopjuotas į iškarpinę", - "assignedToCustomer": "Priskirtas klientui", - "unable-entity-view-device-alias-title": "Subjektų rodinio pseudonimo ištrinti nepavyko", - "unable-entity-view-device-alias-text": "Įrenginio pseudonimas '{{entityViewAlias}}' Negali būti ištrintas, nes jis naudojamas valdikliuose:
    {{widgetsList}}", - "select-entity-view": "Pasirinkti subjektų rodinį", - "start-date": "Pradžios data", - "start-ts": "Pradžios laikas", - "end-date": "Pabaigos data", - "end-ts": "Pabaigos laikas", - "date-limits": "Datos limitas", - "client-attributes": "Kliento atributai", - "shared-attributes": "Bendrinami atributai", - "server-attributes": "Serverio atributai", - "timeseries": "Telemetrija", - "client-attributes-placeholder": "Kliento atributai", - "shared-attributes-placeholder": "Bendrinami atributai", - "server-attributes-placeholder": "Serverio atributai", - "timeseries-placeholder": "Telemetrija", - "target-entity": "Tikslinis subjektas", - "attributes-propagation": "Atributų paplitimas", - "attributes-propagation-hint": "Subjektų rodinys automatikškai kopijuos nurodytus atributus iš tikslinio subjekto kiekvieną kartą, kai išsaugosite ar atnaujinsite šį rodinį. Norint išsaugoti sistemos spartą, tikslinio subjekto atributai neperkeliami į subjektų rodinį kiekvieną kartą kai jie pasikeičia. Tačiau jūs galite įjungti automatinį atributų reikšmių perdavimą, taisyklių grandinėje sukonfigūravę \"copy to view\" taisyklę ir \"Post attributes\" bei \"Attributes Updated\" žinutes nukreipę į naują taisyklę.", - "timeseries-data": "Temetrijos duomenys", - "timeseries-data-hint": "Sukonfigūruokite tikslinio objekto telemetrijos duomenų raktus, kurie bus pasiekiami objekto rodinyje. Šios telemetrijos duomenys yra tik skaitomi.", - "select-group-to-add": "Pasirinkite grupę į kurią pridėti pažymėtus subjektų rodinius", - "select-group-to-move": "Pasirinkite grupę į kurią perkelti pažymėtus subjektų rodinius", - "remove-entity-views-from-group": "Ar tikrai norite iš '{{entityGroup}}' grupės pašalinti { count, plural, =1 {1 subjektų rodinį} other {# subjektų rodinius} }?", - "group": "Subjektų rodinių grupė", - "list-of-groups": "{ count, plural, =1 {Viena subjektų rodinių grupė} other {# subjektų rodinių grupių sąrašas} }", - "group-name-starts-with": "Subjektų rodinių grupės pavadinimas prasideda '{{prefix}}'", - "search": "Subjektų rodinių paieška", - "selected-entity-views": "Pasirinkta { count, plural, =1 {1 subjektų rodinys} other {# subjektų rodiniai} }", - "assign-entity-view-to-edge": "Assign Entity View(s) To Edge", - "assign-entity-view-to-edge-text": "Please select the entity views to assign to the edge", - "unassign-entity-view-from-edge-title": "Are you sure you want to unassign the entity view '{{entityViewName}}'?", - "unassign-entity-view-from-edge-text": "After the confirmation the entity view will be unassigned and won't be accessible by the edge.", - "unassign-entity-views-from-edge-action-title": "Unassign { count, plural, =1 {1 entity view} other {# entity views} } from edge", - "unassign-entity-view-from-edge": "Unassign entity view", - "unassign-entity-views-from-edge-title": "Are you sure you want to unassign { count, plural, =1 {1 entity view} other {# entity views} }?", - "unassign-entity-views-from-edge-text": "After the confirmation all selected entity views will be unassigned and won't be accessible by the edge." - }, - "event": { - "events": "Įvykiai", - "event-type": "Įvykio tipas", - "events-filter": "Įvykių filtras", - "clean-events": "Įšvalyti įvykius", - "type-error": "Klaida", - "type-lc-event": "Gyvavimo ciklo įvykis", - "type-stats": "Statistika", - "type-debug-converter": "Debug", - "type-debug-integration": "Debug", - "type-debug-rule-node": "Debug", - "type-debug-rule-chain": "Debug", - "no-events-prompt": "Įvykių nėra", - "error": "Klaida", - "alarm": "Įspėjimas", - "event-time": "Įvykio laikas", - "server": "Serveris", - "body": "Body", - "method": "Metodas", - "type": "Tipas", - "in": "Į", - "out": "Iš", - "metadata": "Metadata", - "message": "Message", - "entity": "Entity", - "message-id": "Message Id", - "copy-message-id": "Copy message Id", - "message-type": "Message Type", - "data-type": "Data Type", - "relation-type": "Relation Type", - "data": "Data", - "event": "Event", - "status": "Status", - "success": "Success", - "failed": "Failed", - "messages-processed": "Messages processed", - "max-messages-processed": "Maximum messages processed", - "min-messages-processed": "Minimum messages processed", - "errors-occurred": "Errors occurred", - "max-errors-occurred": "Maximum errors occurred", - "min-errors-occurred": "Minimum errors occurred", - "min-value": "Minimum value is 0.", - "all-events": "All", - "has-error": "Has error", - "entity-id": "Entity Id", - "copy-entity-id": "Copy entity Id", - "entity-type": "Entity type", - "clear-filter": "Clear Filter", - "clear-request-title": "Clear all events", - "clear-request-text": "Are you sure you want to clear all events?", - "started": "Started", - "updated": "Updated", - "stopped": "Stopped" - }, - "extension": { - "extensions": "Extensions", - "selected-extensions": "{ count, plural, =1 {1 extension} other {# extensions} } selected", - "type": "Type", - "key": "Key", - "value": "Value", - "id": "Id", - "extension-id": "Extension id", - "extension-type": "Extension type", - "transformer-json": "JSON *", - "unique-id-required": "Current extension id already exists.", - "delete": "Delete extension", - "add": "Add extension", - "edit": "Edit extension", - "view": "View extension", - "delete-extension-title": "Are you sure you want to delete the extension '{{extensionId}}'?", - "delete-extension-text": "Be careful, after the confirmation the extension and all related data will become unrecoverable.", - "delete-extensions-title": "Are you sure you want to delete { count, plural, =1 {1 extension} other {# extensions} }?", - "delete-extensions-text": "Be careful, after the confirmation all selected extensions will be removed.", - "converters": "Converters", - "converter-id": "Converter id", - "configuration": "Configuration", - "converter-configurations": "Converter configurations", - "token": "Security token", - "add-converter": "Add converter", - "add-config": "Add converter configuration", - "device-name-expression": "Device name expression", - "device-type-expression": "Device type expression", - "custom": "Custom", - "to-double": "To Double", - "transformer": "Transformer", - "json-required": "Transformer json is required.", - "json-parse": "Unable to parse transformer json.", - "attributes": "Attributes", - "add-attribute": "Add attribute", - "add-map": "Add mapping element", - "timeseries": "Timeseries", - "add-timeseries": "Add timeseries", - "field-required": "Field is required", - "brokers": "Brokers", - "add-broker": "Add broker", - "host": "Host", - "port": "Port", - "port-range": "Port should be in a range from 1 to 65535.", - "ssl": "Ssl", - "credentials": "Credentials", - "username": "Username", - "password": "Password", - "retry-interval": "Retry interval in milliseconds", - "sas": "Shared Access Signature", - "anonymous": "Anonymous", - "basic": "Basic", - "pem": "PEM", - "ca-cert": "CA certificate file *", - "private-key": "Private key file *", - "cert": "Certificate file *", - "no-file": "No file selected.", - "drop-file": "Drop a file or click to select a file to upload.", - "mapping": "Mapping", - "topic-filter": "Topic filter", - "converter-type": "Converter type", - "converter-json": "Json", - "json-name-expression": "Device name json expression", - "topic-name-expression": "Device name topic expression", - "json-type-expression": "Device type json expression", - "topic-type-expression": "Device type topic expression", - "attribute-key-expression": "Attribute key expression", - "attr-json-key-expression": "Attribute key json expression", - "attr-topic-key-expression": "Attribute key topic expression", - "request-id-expression": "Request id expression", - "request-id-json-expression": "Request id json expression", - "request-id-topic-expression": "Request id topic expression", - "response-topic-expression": "Response topic expression", - "value-expression": "Value expression", - "topic": "Topic", - "timeout": "Timeout in milliseconds", - "converter-json-required": "Converter json is required.", - "converter-json-parse": "Unable to parse converter json.", - "filter-expression": "Filter expression", - "connect-requests": "Connect requests", - "add-connect-request": "Add connect request", - "disconnect-requests": "Disconnect requests", - "add-disconnect-request": "Add disconnect request", - "attribute-requests": "Attribute requests", - "add-attribute-request": "Add attribute request", - "attribute-updates": "Attribute updates", - "add-attribute-update": "Add attribute update", - "server-side-rpc": "Server side RPC", - "add-server-side-rpc-request": "Add server-side RPC request", - "device-name-filter": "Device name filter", - "attribute-filter": "Attribute filter", - "method-filter": "Method filter", - "request-topic-expression": "Request topic expression", - "response-timeout": "Response timeout in milliseconds", - "topic-expression": "Topic expression", - "client-scope": "Client scope", - "add-device": "Add device", - "opc-server": "Servers", - "opc-add-server": "Add server", - "opc-add-server-prompt": "Please add server", - "opc-application-name": "Application name", - "opc-application-uri": "Application uri", - "opc-scan-period-in-seconds": "Scan period in seconds", - "opc-security": "Security", - "opc-identity": "Identity", - "opc-keystore": "Keystore", - "opc-type": "Type", - "opc-keystore-type": "Type", - "opc-keystore-location": "Location *", - "opc-keystore-password": "Password", - "opc-keystore-alias": "Alias", - "opc-keystore-key-password": "Key password", - "opc-device-node-pattern": "Device node pattern", - "opc-device-name-pattern": "Device name pattern", - "modbus-server": "Servers/slaves", - "modbus-add-server": "Add server/slave", - "modbus-add-server-prompt": "Please add server/slave", - "modbus-transport": "Transport", - "modbus-tcp-reconnect": "Automatically reconnect", - "modbus-rtu-over-tcp": "RTU over TCP", - "modbus-port-name": "Serial port name", - "modbus-encoding": "Encoding", - "modbus-parity": "Parity", - "modbus-baudrate": "Baud rate", - "modbus-databits": "Data bits", - "modbus-stopbits": "Stop bits", - "modbus-databits-range": "Data bits should be in a range from 7 to 8.", - "modbus-stopbits-range": "Stop bits should be in a range from 1 to 2.", - "modbus-unit-id": "Unit ID", - "modbus-unit-id-range": "Unit ID should be in a range from 1 to 247.", - "modbus-device-name": "Device name", - "modbus-poll-period": "Poll period (ms)", - "modbus-attributes-poll-period": "Attributes poll period (ms)", - "modbus-timeseries-poll-period": "Timeseries poll period (ms)", - "modbus-poll-period-range": "Poll period should be positive value.", - "modbus-tag": "Tag", - "modbus-function": "Function", - "modbus-register-address": "Register address", - "modbus-register-address-range": "Register address should be in a range from 0 to 65535.", - "modbus-register-bit-index": "Bit index", - "modbus-register-bit-index-range": "Bit index should be in a range from 0 to 15.", - "modbus-register-count": "Register count", - "modbus-register-count-range": "Register count should be a positive value.", - "modbus-byte-order": "Byte order", - "sync": { - "status": "Status", - "sync": "Sync", - "not-sync": "Not sync", - "last-sync-time": "Last sync time", - "not-available": "Not available" + "path": { + "path": "Kelias", + "path-decorator": "Kelio dekoratorius", + "decorator-symbol": "Dekoratoriaus simbolis", + "decorator-symbol-arrow-head": "Rodyklė", + "decorator-symbol-dash": "Brūkšnys", + "decorator-arrangement": "Dekoratoriaus išdėstymas", + "decorator-offset": "Pradžia", + "decorator-end-offset": "Pabaiga", + "decorator-repeat": "Kartojimas" + }, + "points": { + "points": "Taškai", + "point-tooltip": "Taško patarimas" }, - "export-extensions-configuration": "Export extensions configuration", - "import-extensions-configuration": "Import extensions configuration", - "import-extensions": "Import extensions", - "import-extension": "Import extension", - "export-extension": "Export extension", - "file": "Extensions file", - "invalid-file-error": "Invalid extension file", - "text": "TEXT", - "json": "JSON", - "binary": "BINARY", - "hex": "HEX" - }, - "feature": { - "advanced-features": "Advanced features" - }, - "filter": { - "add": "Add filter", - "edit": "Edit filter", - "name": "Filter name", - "name-required": "Filter name is required.", - "duplicate-filter": "Filter with same name is already exists.", - "filters": "Filters", - "unable-delete-filter-title": "Unable to delete filter", - "unable-delete-filter-text": "Filter '{{filter}}' can't be deleted as it used by the following widget(s):
    {{widgetsList}}", - "duplicate-filter-error": "Duplicate filter found '{{filter}}'.
    Filters must be unique within the dashboard.", - "missing-key-filters-error": "Key filters is missing for filter '{{filter}}'.", - "filter": "Filter", - "editable": "Editable", - "no-filters-found": "No filters found.", - "no-filter-text": "No filter specified", - "add-filter-prompt": "Please add filter", - "no-filter-matching": "'{{filter}}' not found.", - "create-new-filter": "Create a new one!", - "create-new": "Create new", - "filter-required": "Filter is required.", - "operation": { - "operation": "Operation", - "equal": "equal", - "not-equal": "not equal", - "starts-with": "starts with", - "ends-with": "ends with", - "contains": "contains", - "not-contains": "not contains", - "greater": "greater than", - "less": "less than", - "greater-or-equal": "greater or equal", - "less-or-equal": "less or equal", - "and": "and", - "or": "or", - "in": "in", - "not-in": "not in" + "shape": { + "fill": "Užpildymas", + "fill-type-color": "Spalva", + "fill-type-stripe": "Dryžis", + "fill-type-image": "Paveikslėlis", + "color": "Spalva", + "stripe": "Dryžis", + "image": "Paveikslėlis", + "stroke": "Kontūras", + "fill-image": "Užpildo paveikslėlis", + "fill-image-type-image": "Paveikslėlis", + "fill-image-type-function": "Funkcija", + "preserve-aspect-ratio": "Išlaikyti proporcijas", + "opacity": "Nepermatomumas", + "angle": "Pasukimo kampas", + "scale": "Mastelis", + "fill-image-function": "Formos užpildo paveikslėlio funkcija", + "fill-images": "Formos užpildo paveikslėliai", + "stripe-pattern": "Dryžių raštas", + "first-stripe": "Pirmas dryžis", + "second-stripe": "Antras dryžis" }, - "ignore-case": "ignore case", - "value": "Value", - "remove-filter": "Remove filter", - "duplicate-filter-action": "Duplicate filter", - "preview": "Filter preview", - "no-filters": "No filters configured", - "add-filter": "Add filter", - "add-complex-filter": "Add complex filter", - "add-complex": "Add complex", - "complex-filter": "Complex filter", - "edit-complex-filter": "Edit complex filter", - "edit-filter-user-params": "Edit filter predicate user parameters", - "filter-user-params": "Filter predicate user parameters", - "user-parameters": "User parameters", - "display-label": "Label to display", - "order-priority": "Field order priority", - "key-filter": "Key filter", - "key-filters": "Key filters", - "key-name": "Key name", - "key-name-required": "Key name is required.", - "key-type": { - "key-type": "Key type", - "attribute": "Attribute", - "timeseries": "Timeseries", - "entity-field": "Entity field", - "constant": "Constant" + "polygon": { + "polygon-key": "Daugiakampio raktas", + "polygon-key-required": "Daugiakampio raktas yra privalomas", + "no-polygons": "Daugiakampiai nesukonfigūruoti", + "add-polygon": "Pridėti daugiakampį", + "polygon-configuration": "Daugiakampio konfigūracija", + "remove-polygon": "Pašalinti daugiakampį", + "edit": "Redaguoti daugiakampį", + "remove-polygon-for": "Pašalinti daugiakampį objektui '{{entityName}}'", + "cut": "Iškirpti daugiakampio sritį", + "rotate": "Pasukti daugiakampį", + "draw-rectangle": "Nubrėžti stačiakampį", + "draw-polygon": "Nubrėžti daugiakampį", + "polygon-place-first-point-cut-hint": "Spustelėkite, kad padėtumėte pirmą tašką", + "continue-polygon-cut-hint": "Spustelėkite, kad tęstumėte braižymą", + "finish-polygon-cut-hint": "Spustelėkite pirmą žymeklį, kad baigtumėte ir išsaugotumėte", + "polygon-place-first-point-hint": "Daugiakampis: spustelėkite, kad padėtumėte pirmą tašką", + "polygon-place-first-point-hint-with-entity": "Daugiakampis objektui '{{entityName}}': spustelėkite, kad padėtumėte pirmą tašką", + "continue-polygon-hint": "Daugiakampis: spustelėkite, kad tęstumėte braižymą", + "continue-polygon-hint-with-entity": "Daugiakampis objektui '{{entityName}}': spustelėkite, kad tęstumėte braižymą", + "finish-polygon-hint": "Daugiakampis: spustelėkite pirmą žymeklį, kad baigtumėte braižymą", + "finish-polygon-hint-with-entity": "Daugiakampis objektui '{{entityName}}': spustelėkite pirmą žymeklį, kad baigtumėte ir išsaugotumėte", + "rectangle-place-first-point-hint": "Stačiakampis: spustelėkite, kad padėtumėte pirmą tašką", + "rectangle-place-first-point-hint-with-entity": "Stačiakampis objektui '{{entityName}}': spustelėkite, kad padėtumėte pirmą tašką", + "finish-rectangle-hint": "Stačiakampis: spustelėkite, kad baigtumėte braižymą", + "finish-rectangle-hint-with-entity": "Stačiakampis objektui '{{entityName}}': spustelėkite, kad baigtumėte ir išsaugotumėte" }, - "value-type": { - "value-type": "Value type", - "string": "String", - "numeric": "Numeric", - "boolean": "Boolean", - "date-time": "Datetime" + "circle": { + "circle-key": "Apskritimo raktas", + "circle-key-required": "Apskritimo raktas yra privalomas", + "no-circles": "Apskritimai nesukonfigūruoti", + "add-circle": "Pridėti apskritimą", + "circle-configuration": "Apskritimo konfigūracija", + "remove-circle": "Pašalinti apskritimą", + "edit": "Redaguoti apskritimą", + "remove-circle-for": "Pašalinti apskritimą objektui '{{entityName}}'", + "draw-circle": "Nubrėžti apskritimą", + "place-circle-center-hint-with-entity": "Apskritimas objektui '{{entityName}}': spustelėkite, kad padėtumėte apskritimo centrą", + "place-circle-center-hint": "Apskritimas: spustelėkite, kad padėtumėte apskritimo centrą", + "finish-circle-hint-with-entity": "Apskritimas objektui '{{entityName}}': spustelėkite, kad baigtumėte ir išsaugotumėte", + "finish-circle-hint": "Apskritimas: spustelėkite, kad baigtumėte braižymą" }, - "value-type-required": "Key value type is required.", - "key-value-type-change-title": "Are you sure you want to change key value type?", - "key-value-type-change-message": "If you confirm new value type all entered key filters will be removed.", - "no-key-filters": "No key filters configured", - "add-key-filter": "Add key filter", - "remove-key-filter": "Remove key filter", - "edit-key-filter": "Edit key filter", - "date": "Date", - "time": "Time", - "current-tenant": "Current tenant", - "current-customer": "Current customer", - "current-user": "Current user", - "current-device": "Current device", - "default-value": "Default value", - "dynamic-source-type": "Dynamic source type", - "dynamic-value": "Dynamic value", - "no-dynamic-value": "No dynamic value", - "source-attribute": "Source attribute", - "switch-to-dynamic-value": "Switch to dynamic value", - "switch-to-default-value": "Switch to default value", - "inherit-owner": "Inherit from owner", - "source-attribute-not-set": "If source attribute isn't set" - }, - "fullscreen": { - "expand": "Rodyti per visą ekraną", - "exit": "Išjungti rodymą per visą ekraną", - "toggle": "Rodyti per visą ekraną", - "fullscreen": "Rodyti per visą ekraną" - }, - "function": { - "function": "Funkcija" + "select-entity": "Pasirinkti objektą", + "select-entity-hint": "Patarimas: po pasirinkimo spustelėkite žemėlapyje, kad nustatytumėte padėtį" + }, + "select-entity": "Pasirinkti objektą", + "select-entity-hint": "Patarimas: po pasirinkimo spustelėkite žemėlapyje, kad nustatytumėte padėtį", + "tooltips": { + "placeMarker": "Spustelėkite, kad padėtumėte objektą '{{entityName}}'", + "firstVertex": "Daugiakampis objektui '{{entityName}}': spustelėkite, kad padėtumėte pirmą tašką", + "firstVertex-cut": "Spustelėkite, kad padėtumėte pirmą tašką", + "continueLine": "Daugiakampis objektui '{{entityName}}': spustelėkite, kad tęstumėte braižymą", + "continueLine-cut": "Spustelėkite, kad tęstumėte braižymą", + "finishLine": "Spustelėkite bet kurį esamą žymeklį, kad baigtumėte", + "finishPoly": "Daugiakampis objektui '{{entityName}}': spustelėkite pirmą žymeklį, kad baigtumėte ir išsaugotumėte", + "finishPoly-cut": "Spustelėkite pirmą žymeklį, kad baigtumėte ir išsaugotumėte", + "finishRect": "Daugiakampis objektui '{{entityName}}': spustelėkite, kad baigtumėte ir išsaugotumėte", + "startCircle": "Apskritimas objektui '{{entityName}}': spustelėkite, kad padėtumėte apskritimo centrą", + "finishCircle": "Apskritimas objektui '{{entityName}}': spustelėkite, kad baigtumėte apskritimą", + "placeCircleMarker": "Spustelėkite, kad padėtumėte apskritimo žymeklį" + }, + "actions": { + "finish": "Baigti", + "cancel": "Atšaukti", + "removeLastVertex": "Pašalinti paskutinį tašką" + }, + "buttonTitles": { + "drawMarkerButton": "Padėti objektą", + "drawPolyButton": "Sukurti daugiakampį", + "drawLineButton": "Sukurti liniją", + "drawCircleButton": "Sukurti apskritimą", + "drawRectButton": "Sukurti stačiakampį", + "editButton": "Redagavimo režimas", + "dragButton": "Vilkimo režimas", + "cutButton": "Iškirpti daugiakampio sritį", + "deleteButton": "Pašalinti", + "drawCircleMarkerButton": "Sukurti apskritimo žymeklį", + "rotateButton": "Pasukti daugiakampį" + }, + "map-provider-settings": "Žemėlapio tiekėjo nustatymai", + "map-provider": "Žemėlapio tiekėjas", + "map-provider-google": "Google žemėlapiai", + "map-provider-openstreet": "OpenStreet žemėlapiai", + "map-provider-here": "HERE žemėlapiai", + "map-provider-image": "Paveikslėlio žemėlapis", + "map-provider-tencent": "Tencent žemėlapiai", + "openstreet-provider": "OpenStreet žemėlapių tiekėjas", + "openstreet-provider-mapnik": "OpenStreetMap.Mapnik (numatytasis)", + "openstreet-provider-hot": "OpenStreetMap.HOT", + "openstreet-provider-esri-street": "Esri.WorldStreetMap", + "openstreet-provider-esri-topo": "Esri.WorldTopoMap", + "openstreet-provider-esri-imagery": "Esri.WorldImagery", + "openstreet-provider-cartodb-positron": "CartoDB.Positron", + "openstreet-provider-cartodb-dark-matter": "CartoDB.DarkMatter", + "use-custom-provider": "Naudoti pasirinktinį tiekėją", + "custom-provider-tile-url": "Pasirinktinio tiekėjo plytelių URL", + "google-maps-api-key": "Google Maps API raktas", + "default-map-type": "Numatytasis žemėlapio tipas", + "google-map-type-roadmap": "Keliai", + "google-map-type-satelite": "Palydovas", + "google-map-type-hybrid": "Hibridinis", + "google-map-type-terrain": "Reljefas", + "map-layer": "Žemėlapio sluoksnis", + "here-map-normal-day": "HERE.normalDay (numatytasis)", + "here-map-normal-night": "HERE.normalNight", + "here-map-hybrid-day": "HERE.hybridDay", + "here-map-terrain-day": "HERE.terrainDay", + "credentials": "Prisijungimo duomenys", + "here-app-id": "HERE programos ID", + "here-app-code": "HERE programos kodas", + "here-api-key": "HERE API raktas", + "here-use-new-version-api-3": "Naudoti API 3 versiją", + "tencent-maps-api-key": "Tencent Maps API raktas", + "tencent-map-type-roadmap": "Keliai", + "tencent-map-type-satelite": "Palydovas", + "tencent-map-type-hybrid": "Hibridinis", + "image-map-background": "Paveikslėlio žemėlapio fonas", + "image-map-background-from-entity-attribute": "Naudoti paveikslėlį iš objekto atributo kaip žemėlapio foną", + "image-url-source-entity-alias": "Paveikslėlio URL šaltinio objekto pseudonimas", + "image-url-source-entity-attribute": "Paveikslėlio URL šaltinio objekto atributas", + "common-map-settings": "Bendrieji žemėlapio nustatymai", + "x-pos-key-name": "X padėties rakto pavadinimas", + "y-pos-key-name": "Y padėties rakto pavadinimas", + "latitude-key-name": "Platumos rakto pavadinimas", + "longitude-key-name": "Ilgumos rakto pavadinimas", + "default-map-zoom-level": "Numatytasis priartinimo lygis (0–20)", + "default-map-center-position": "Numatytoji žemėlapio centro padėtis (0,0)", + "disable-scroll-zooming": "Išjungti priartinimą slenkant", + "disable-double-click-zooming": "Išjungti priartinimą dukart spustelėjus", + "disable-zoom-control-buttons": "Išjungti priartinimo valdymo mygtukus", + "fit-map-bounds": "Pritaikyti žemėlapio ribas, kad apimtų visus žymeklius", + "use-default-map-center-position": "Naudoti numatytąją žemėlapio centro padėtį", + "entities-limit": "Objektų įkėlimo riba", + "markers-settings": "Žymeklių nustatymai", + "marker-offset-x": "Žymeklio X poslinkis, lyginant su padėtimi (daugiklis žymeklio pločiui)", + "marker-offset-y": "Žymeklio Y poslinkis, lyginant su padėtimi (daugiklis žymeklio aukščiui)", + "position-function": "Padėties konversijos funkcija, grąžinanti x,y koordinates nuo 0 iki 1", + "draggable-marker": "Vilkiojamas žymeklis", + "label": "Etiketė", + "show-label": "Rodyti etiketę", + "use-label-function": "Naudoti etiketės funkciją", + "label-pattern": "Etiketė (pavyzdžiai: '${entityName}', '${entityName}: (Tekstas ${keyName} vienetai.)')", + "label-function": "Etiketės funkcija", + "tooltip": "Patarimas", + "show-tooltip": "Rodyti patarimą", + "show-tooltip-action": "Veiksmas, kai rodomas patarimas", + "show-tooltip-action-click": "Rodyti spustelėjus (numatytasis)", + "show-tooltip-action-hover": "Rodyti užvedus žymeklį", + "auto-close-tooltips": "Automatiškai uždaryti patarimus", + "use-tooltip-function": "Naudoti patarimo funkciją", + "tooltip-pattern": "Patarimas (pvz.: 'Tekstas ${keyName} vienetai.' arba Nuorodos tekstas)", + "tooltip-function": "Patarimo funkcija", + "tooltip-offset-x": "Patarimo X poslinkis (pagal žymeklio inkarą, dauginamas iš pločio)", + "tooltip-offset-y": "Patarimo Y poslinkis (pagal žymeklio inkarą, dauginamas iš aukščio)", + "color": "Spalva", + "use-color-function": "Naudoti spalvos funkciją", + "color-function": "Spalvos funkcija", + "marker-image": "Žymeklio paveikslėlis", + "use-marker-image-function": "Naudoti žymeklio paveikslėlio funkciją", + "custom-marker-image": "Pasirinktinis žymeklio paveikslėlis", + "custom-marker-image-size": "Pasirinktinio žymeklio paveikslėlio dydis (px)", + "marker-image-function": "Žymeklio paveikslėlio funkcija", + "marker-images": "Žymeklio paveikslėliai", + "polygon-settings": "Daugiakampio nustatymai", + "show-polygon": "Rodyti daugiakampį", + "polygon-key-name": "Daugiakampio rakto pavadinimas", + "enable-polygon-edit": "Įjungti daugiakampio redagavimą", + "polygon-label": "Daugiakampio etiketė", + "show-polygon-label": "Rodyti daugiakampio etiketę", + "use-polygon-label-function": "Naudoti daugiakampio etiketės funkciją", + "polygon-label-pattern": "Daugiakampio etiketė (pavyzdžiai: '${entityName}', '${entityName}: (Tekstas ${keyName} vienetai.)')", + "polygon-label-function": "Daugiakampio etiketės funkcija", + "polygon-tooltip": "Daugiakampio patarimas", + "show-polygon-tooltip": "Rodyti daugiakampio patarimą", + "auto-close-polygon-tooltips": "Automatiškai uždaryti daugiakampio patarimus", + "use-polygon-tooltip-function": "Naudoti daugiakampio patarimo funkciją", + "polygon-tooltip-pattern": "Patarimas (pvz.: 'Tekstas ${keyName} vienetai.' arba Nuorodos tekstas)", + "polygon-tooltip-function": "Daugiakampio patarimo funkcija", + "polygon-color": "Daugiakampio spalva", + "polygon-opacity": "Daugiakampio permatomumas", + "use-polygon-color-function": "Naudoti daugiakampio spalvos funkciją", + "polygon-color-function": "Daugiakampio spalvos funkcija", + "polygon-stroke": "Daugiakampio brūkšnys", + "stroke-color": "Brūkšnio spalva", + "stroke-opacity": "Brūkšnio permatomumas", + "stroke-weight": "Brūkšnio storis", + "use-polygon-stroke-color-function": "Naudoti daugiakampio brūkšnio spalvos funkciją", + "polygon-stroke-color-function": "Daugiakampio brūkšnio spalvos funkcija", + "circle-settings": "Apskritimo nustatymai", + "show-circle": "Rodyti apskritimą", + "circle-key-name": "Apskritimo rakto pavadinimas", + "enable-circle-edit": "Įjungti apskritimo redagavimą", + "circle-label": "Apskritimo etiketė", + "show-circle-label": "Rodyti apskritimo etiketę", + "use-circle-label-function": "Naudoti apskritimo etiketės funkciją", + "circle-label-pattern": "Apskritimo etiketė (pavyzdžiai: '${entityName}', '${entityName}: (Tekstas ${keyName} vienetai.)')", + "circle-label-function": "Apskritimo etiketės funkcija", + "circle-tooltip": "Apskritimo patarimas", + "show-circle-tooltip": "Rodyti apskritimo patarimą", + "auto-close-circle-tooltips": "Automatiškai uždaryti apskritimo patarimus", + "use-circle-tooltip-function": "Naudoti apskritimo patarimo funkciją", + "circle-tooltip-pattern": "Patarimas (pvz.: 'Tekstas ${keyName} vienetai.' arba Nuorodos tekstas)", + "circle-tooltip-function": "Apskritimo patarimo funkcija", + "circle-fill-color": "Apskritimo užpildo spalva", + "circle-fill-color-opacity": "Apskritimo užpildo permatomumas", + "use-circle-fill-color-function": "Naudoti apskritimo užpildo spalvos funkciją", + "circle-fill-color-function": "Apskritimo užpildo spalvos funkcija", + "circle-stroke": "Apskritimo brūkšnys", + "use-circle-stroke-color-function": "Naudoti apskritimo brūkšnio spalvos funkciją", + "circle-stroke-color-function": "Apskritimo brūkšnio spalvos funkcija", + "markers-clustering-settings": "Žymeklių grupavimo nustatymai", + "use-map-markers-clustering": "Naudoti žymeklių grupavimą", + "zoom-on-cluster-click": "Priartinti paspaudus ant grupės", + "max-cluster-zoom": "Didžiausias priartinimo lygis, kuriame žymeklis gali priklausyti grupei (0–18)", + "max-cluster-radius-pixels": "Didžiausias grupės spindulys (pikseliais)", + "cluster-zoom-animation": "Rodyti animaciją žymekliams priartinant", + "show-markers-bounds-on-cluster-mouse-over": "Rodyti žymeklių ribas užvedus pelę ant grupės", + "spiderfy-max-zoom-level": "Išskleisti (spiderfy) didžiausiame priartinimo lygyje, kad matytųsi visi žymekliai", + "load-optimization": "Įkėlimo optimizavimas", + "cluster-chunked-loading": "Naudoti dalinį žymeklių įkėlimą, kad puslapis neužstrigtų", + "cluster-markers-lazy-load": "Naudoti „lazy load“ žymeklių įkėlimui", + "editor-settings": "Redaktoriaus nustatymai", + "enable-snapping": "Įjungti pritraukimą prie kitų viršūnių tikslumui", + "init-draggable-mode": "Paleisti žemėlapį vilkiojimo režimu", + "hide-all-edit-buttons": "Slėpti visus redagavimo valdiklius", + "hide-draw-buttons": "Slėpti braižymo mygtukus", + "hide-edit-buttons": "Slėpti redagavimo mygtukus", + "hide-remove-button": "Slėpti šalinimo mygtuką", + "route-map-settings": "Maršruto žemėlapio nustatymai", + "trip-animation-settings": "Kelionės animacijos nustatymai", + "normalization-step": "Normalizavimo žingsnis (ms)", + "tooltip-background-color": "Patarimo fono spalva", + "tooltip-font-color": "Patarimo teksto spalva", + "tooltip-opacity": "Patarimo permatomumas (0–1)", + "auto-close-tooltip": "Automatiškai uždaryti patarimą", + "rotation-angle": "Papildomas žymeklio pasukimo kampas (laipsniais)", + "path-settings": "Kelio nustatymai", + "path-color": "Kelio spalva", + "use-path-color-function": "Naudoti kelio spalvos funkciją", + "path-color-function": "Kelio spalvos funkcija", + "path-decorator": "Kelio dekoratorius", + "use-path-decorator": "Naudoti kelio dekoratorių", + "decorator-symbol": "Dekoratoriaus simbolis", + "decorator-symbol-arrow-head": "Rodyklė", + "decorator-symbol-dash": "Brūkšnys", + "decorator-symbol-size": "Dekoratoriaus simbolio dydis (px)", + "use-path-decorator-custom-color": "Naudoti pasirinktinę dekoratoriaus spalvą", + "decorator-custom-color": "Dekoratoriaus pasirinktinė spalva", + "decorator-offset": "Dekoratoriaus poslinkis", + "end-decorator-offset": "Dekoratoriaus pabaigos poslinkis", + "decorator-repeat": "Dekoratoriaus kartojimas", + "points-settings": "Taškų nustatymai", + "show-points": "Rodyti taškus", + "point-color": "Taško spalva", + "point-size": "Taško dydis (px)", + "use-point-color-function": "Naudoti taško spalvos funkciją", + "point-color-function": "Taško spalvos funkcija", + "use-point-as-anchor": "Naudoti tašką kaip inkarą", + "point-as-anchor-function": "Taško kaip inkaro funkcija", + "independent-point-tooltip": "Nepriklausomas taško patarimas", + "clustering-markers": "Žymeklių grupavimas", + "use-icon-create-function": "Naudoti žymeklių spalvos funkciją", + "marker-color-function": "Žymeklio spalvos funkcija" }, - "gateway": { - "gateway-exists": "Device with same name is already exists.", - "gateway-name": "Gateway name", - "gateway-name-required": "Gateway name is required.", - "gateway-saved": "Gateway configuration successfully saved.", - "gateway": "Gateway", - "create-new-gateway": "Create a new gateway", - "create-new-gateway-text": "Are you sure you want create a new gateway with name: '{{gatewayName}}'?", - "no-gateway-found": "No gateway found.", - "no-gateway-matching": " '{{item}}' not found." - }, - "grid": { - "delete-item-title": "Ar tikrai norite pašalinti šį elementą?", - "delete-item-text": "Būkite dėmesingi, po patvirtinimo šio elemento ir visos su juo susijusios informacijos atkurti nebegalėsite.", - "delete-items-title": "Ar tikrai norite pašalinti { count, plural, =1 {1 elementą} other {# elementus} }?", - "delete-items-action-title": "Panaikinti { count, plural, =1 {1 elementą} other {# elementus} }", - "delete-items-text": "Būkite dėmesingi, po patvirtinimo visi pasirinkti elementai ir su jais susijusi informacija bus pašalinti ir jų atkurti nebegalėsite.", - "add-item-text": "Pridėti naują elementą", - "no-items-text": "Elementų nėra", - "item-details": "Informacija apie elementą", - "delete-item": "Panaikinti elementą", - "delete-items": "Panaikinti elmentus", - "scroll-to-top": "Slinkti į viršų" - }, - "help": { - "goto-help-page": "Pagalba", - "show-help": "Pagalba" + "markdown": { + "use-markdown-text-function": "Naudoti markdown/HTML reikšmės funkciją", + "markdown-text-function": "Markdown/HTML reikšmės funkcija", + "markdown-text-pattern": "Markdown/HTML šablonas (markdown arba HTML su kintamaisiais, pvz.: '${entityName} arba ${keyName} - kažkoks tekstas.')", + "apply-default-markdown-style": "Taikyti numatytąjį markdown stilių", + "markdown-css": "Markdown/HTML CSS" }, - "home": { - "home": "Pagrindinis", - "profile": "Profilis", - "logout": "Atsijungti", - "menu": "Meniu", - "avatar": "Paveiksliukas", - "open-user-menu": "Atverti vartoto meniu" - }, - "file-input": { - "browse-file": "Pasirinkti failą", - "browse-files": "Pasirinkti failus" - }, - "image-input": { - "drop-images-or": "Užvilkite paveikslėlį arba", - "drag-and-drop": "Užvilkite", - "or": "arba", - "browse": "Pasirinkite", - "no-images": "Paveikslėlis nepasirinktas", - "images": "paveikslėliai" - }, - "import": { - "no-file": "Nepasirinktas failas", - "drop-file": "Nuvilkite JSON failą arba spustelėkite, kad pasirinktumėte failą, kurį norite įkelti.", - "drop-csv-file": "Nuvilkite CSV failą arba spustelėkite, kad pasirinktumėte failą, kurį norite įkelti.", - "drop-json-file-or": "Nuvilkite JSON failą arba", - "drop-file-csv": "Nuvilkite CSV failą arba spustelėkite, kad pasirinktumėte failą, kurį norite įkelti.", - "drop-file-csv-or": "Nuvilkite CSV failą arba", - "column-value": "Reikšmė", - "column-title": "Pavadinimas", - "column-example": "Duomenų pavyzdys", - "column-key": "Atributo/telemetrijos raktas", - "credentials": "Įgaliojimai", - "csv-delimiter": "CSV skyriklis", - "csv-first-line-header": "Pirmoje eilutėje stulpelių pavadinimai", - "csv-update-data": "Atnaujinti artibutą/telemetriją", - "details": "Informacija", - "import-csv-number-columns-error": "Faile turi būti bent du stulpeliai", - "import-csv-invalid-format-error": "Neteisingas failo formatas. Elutė: '{{line}}'", - "column-type": { - "name": "Pavadinimas", - "type": "Tipas", - "label": "Etiketė", - "column-type": "Stulpelio tipas", - "client-attribute": "Kliento atributas", - "shared-attribute": "Bendrinamas atributas", - "server-attribute": "Serverio atributas", - "timeseries": "Telemetrija", - "entity-field": "Subjekto laukas", - "access-token": "Prieigos raktas", - "x509": "X.509", - "mqtt": { - "client-id": "MQTT client ID", - "user-name": "MQTT user name", - "password": "MQTT password" - }, - "lwm2m": { - "client-endpoint": "LwM2M endpoint client name", - "security-config-mode": "LwM2M security config mode", - "client-identity": "LwM2M client identity", - "client-key": "LwM2M client key", - "client-cert": "LwM2M client public key", - "bootstrap-server-security-mode": "LwM2M bootstrap server security mode", - "bootstrap-server-secret-key": "LwM2M bootstrap server secret key", - "bootstrap-server-public-key-id": "LwM2M bootstrap server public key or id", - "lwm2m-server-security-mode": "LwM2M server security mode", - "lwm2m-server-secret-key": "LwM2M server secret key", - "lwm2m-server-public-key-id": "LwM2M server public key or id" - }, - "snmp": { - "host": "SNMP host", - "port": "SNMP port", - "version": "SNMP version (v1, v2c or v3)", - "community-string": "SNMP community string" - }, - "isgateway": "Is Gateway", - "activity-time-from-gateway-device": "Activity time from gateway device", - "description": "Description", - "edge-license-key": "License Key", - "cloud-endpoint": "Cloud Endpoint", - "routing-key": "Edge key", - "secret": "Edge secret" - }, - "stepper-text": { - "select-file": "Pasirinkite failą", - "configuration": "Importo konfigūracija", - "column-type": "Pasirinkite stulpelių tipus", - "creat-entities": "Sukurti naujus subjektus" - }, - "message": { - "create-entities": "Sukurti {{count}} nauji subjektai.", - "update-entities": "Atnaujinti {{count}} subjektai.", - "error-entities": "Kuriant {{count}} subjektų įvyko klaidų." - } + "simple-card": { + "label": "Etiketė", + "label-position": "Etiketės padėtis", + "label-position-left": "Kairėje", + "label-position-top": "Viršuje" }, - "integration": { - "integration": "Integration", - "integrations": "Integrations", - "integrations-center": "Integrations center", - "select-integration": "Select integration", - "no-integrations-matching": "No integrations matching '{{entity}}' were found.", - "integration-required": "Integration is required", - "delete": "Delete integration", - "management": "Integrations management", - "add-integration-text": "Add new integration", - "no-integrations-text": "No integrations found", - "data-convertor-name": "{{convertorType}} data converter for {{integrationName}}", - "integration-name": "{{integrationType}} integration", - "selected-integrations": "{ count, plural, =1 {1 integration} other {# integrations} } selected", - "delete-integration-title": "Are you sure you want to delete the integration '{{integrationName}}'?", - "delete-integration-text": "Be careful, after the confirmation the integration and all related data will become unrecoverable.", - "delete-integrations-title": "Are you sure you want to delete { count, plural, =1 {1 integration} other {# integrations} }?", - "delete-integrations-action-title": "Delete { count, plural, =1 {1 integration} other {# integrations} }", - "delete-integrations-text": "Be careful, after the confirmation all selected integrations will be removed and all related data will become unrecoverable.", - "events": "Events", - "enabled": "Enable integration", - "allow-create-devices-or-assets": "Allow create devices or assets", - "all-types": "All integration types", - "add": "Add Integration", - "search": "Search integrations", - "integration-details": "Integration details", - "details": "Details", - "copyId": "Copy integration Id", - "idCopiedMessage": "Integration Id has been copied to clipboard", - "debug-mode": "Debug mode", - "enable-security": "Enable security", - "enable-security-new": "Enable security for automatic token updates", - "headers-filter": "Headers filter", - "header": "Header", - "no-headers-filter": "No headers filter", - "downlink-url": "Downlink URL", - "downlink-url-required": "Downlink URL is required", - "create-loriot-output": "Create Loriot Application output", - "send-downlink": "Send downlink", - "allow-downlink": "Allow downlink", - "server": "Server", - "server-required": "Server is required", - "domain": "Domain", - "app-id": "Application ID", - "app-id-required": "Application ID is required", - "app-token": "Application Access Token", - "app-token-required": "Application Access Token is required", - "email": "Email", - "email-required": "Email is required", - "application-uri": "Application URI", - "as-id": "AS ID", - "as-id-required": "AS ID is required.", - "as-key": "AS Key", - "as-key-required": "AS Key is required.", - "client-id-new": "Client Id", - "client-id-new-required": "Client Id (login) is required (login).", - "client-secret": "Client Secret", - "client-secret-required": "Client Secret (password) is required (password).", - "max-time-diff-in-seconds": "Maximum time difference (seconds)", - "max-time-diff-in-seconds-required": "Maximum time difference is required.", - "created-time": "Created time", - "name": "Name", - "name-required": "Name is required.", - "name-max-length": "Name should be less than 256", - "description": "Description", - "base-url": "Base URL", - "base-url-required": "Base URL is required", - "security-key": "Security key", - "http-endpoint": "HTTP endpoint URL", - "replace-no-content-to-ok": "Replace response status from 'No-Content' to 'OK'", - "copy-http-endpoint-url": "Copy HTTP endpoint URL", - "http-endpoint-url-copied-message": "HTTP endpoint URL has been copied to clipboard", - "host": "Host", - "host-required": "Host is required.", - "host-private": "Host must be in the public network.", - "url-private": "URL must be in the public network.", - "host-type": "Host type", - "host-type-required": "Host type is required.", - "api-version": "Use API v3", - "custom-host": "Custom host", - "custom-host-required": "Custom host is required.", - "port": "Port", - "port-required": "Port is required.", - "port-range": "Port should be in a range from 1 to 65535.", - "connect-timeout": "Connection timeout (sec)", - "connect-timeout-required": "Connection timeout is required.", - "connect-timeout-range": "Connection timeout should be in a range from 1 to 200.", - "client-id": "Client ID", - "client-id-required": "Client ID is required.", - "client-id-hint": "Hint: Optional. Leave empty for auto-generated client ID. Be careful when specifying the Client ID. Majority of the MQTT brokers will not allow multiple connections with the same client ID. To connect to such brokers, your MQTT client ID must be unique.", - "max-bytes-in-message": "Max bytes in message", - "max-bytes-in-message-range": "Max bytes in message should be in a range from 1 to 256000000.", - "device-id": "Device ID", - "device-id-required": "Device ID is required.", - "device-id-range": "Device ID should be in a range from 1 to 65535 characters.", - "device-id-pattern": "Device ID should consist of numbers, upper and lower case letters. [MQTT-3.1.3-5]", - "group-id": "Group ID", - "group-id-required": "Group ID is required.", - "topics": "Topics", - "topics-required": "Topic is required.", - "routing-keys": "Routing keys", - "routing-keys-required": "Routing key is required.", - "queues": "Queues", - "queues-required": "Queue name is required.", - "durable": "Durable", - "exclusive": "Exclusive", - "autoDelete": "Auto delete", - "exchange-name": "Exchange name", - "exchange-name-required": "Exchange name is required.", - "downlink-topic": "Downlink topic", - "connection-timeout": "Connection timeout, ms", - "connection-timeout-min": "Invalid connection timeout value.", - "handshake-timeout": "Handshake timeout, ms", - "handshake-timeout-min": "Invalid handshake timeout value.", - "virtual-host": "Virtual host", - "rabbit-mq-poll-interval": "Poll interval, ms", - "rabbit-mq-poll-interval-min": "Invalid poll interval value.", - "application-server-url": "Application server URL", - "application-server-url-required": "Application server URL is required.", - "application-server-api-token": "Application server API Token", - "application-server-api-token-required": "Application server API Token is required.", - "use-api-four-plus": "Use API for ChirpStack 4+", - "bootstrap-servers": "Bootstrap servers", - "bootstrap-servers-required": "Bootstrap servers is required.", - "poll-interval": "Poll interval", - "poll-interval-required": "Poll interval is required.", - "poll-interval-min-value": "Poll interval value can't be less then 1", - "auto-create-topics": "Auto create topics", - "clean-session": "Clean session", - "enable-ssl": "Enable SSL", - "credentials": "Credentials", - "credentials-type": "Credentials type", - "credentials-type-required": "Credentials type is required.", - "username": "Username", - "username-required": "Username is required.", - "password": "Password", - "password-required": "Password is required.", - "azure-ca-cert": "CA certificate file", - "ca-cert": "CA certificate file", - "private-key": "Private key file", - "private-key-password": "Private key password", - "cert": "Certificate file", - "no-file": "No file selected.", - "drop-file": "Drop a file or click to select a file to upload.", - "drop-file-or": "Drag and drop a file or", - "check-connection": "Check connection", - "check-success": "Connection established.", - "topic-filters": "Topic filters", - "remove-topic-filter": "Remove topic filter", - "add-topic-filter": "Add topic filter", - "add-topic-filter-prompt": "Please add topic filter", - "topic": "Topic", - "mqtt-qos": "QoS", - "mqtt-qos-required": "QoS is required", - "mqtt-qos-range": "QoS values range is from 0 to 2", - "mqtt-qos-at-most-once": "At most once", - "mqtt-qos-at-least-once": "At least once", - "mqtt-qos-exactly-once": "Exactly once", - "downlink-topic-pattern": "Downlink topic pattern", - "downlink-topic-pattern-required": "Downlink topic pattern is required.", - "retained-message": "Retained", - "aws-access-key-id": "AWS access key Id", - "aws-secret-access-key": "AWS secret access key", - "aws-region": "Region", - "aws-iot-endpoint": "AWS IoT Endpoint", - "aws-iot-endpoint-required": "AWS IoT Endpoint is required.", - "aws-iot-credentials": "AWS IoT Credentials", - "aws-sqs-polling-period-in-seconds": "Polling period in seconds", - "aws-sqs-queue-url": "SQS Queue URL", - "aws-sqs-queue-url-required": "SQS Queue URL is required", - "aws-sqs-access-key-id-required": "Access Key Id is required", - "aws-sqs-secret-access-key-required": "Secret Access Key is required", - "application-credentials": "Application Credentials", - "api-key": "API Key", - "api-key-required": "API Key is required.", - "api-key-format": "Invalid API Key format.", - "auth-token": "Authentication Token", - "auth-token-required": "Authentication Token is required.", - "region": "Region", - "region-required": "Region is required.", - "application-id": "Application ID", - "application-id-required": "Application ID is required.", - "access-id": "Access Id", - "access-id-required": "Access Id is required.", - "access-key": "Access Key", - "access-key-required": "Access Key is required.", - "access-key-min-length": "Access Key is too short.", - "access-key-max-length": "Access Key is too long.", - "connection-parameters": "Connection parameters", - "service-bus-namespace-name": "Service Bus Namespace Name", - "service-bus-namespace-name-required": "Service Bus Namespace Name is required.", - "connection-string": "Connection string", - "consumer-group": "Consumer group", - "connection-string-required": "Connection string required!", - "event-hub-name": "Event Hub Name", - "event-hub-name-required": "Event Hub Name is required.", - "event-iot-hub-name-required": "Iot Hub Name is required for downlink", - "sas-key-name": "SAS Key Name", - "sas-key-name-required": "SAS Key Name is required.", - "sas-key": "SAS Key", - "sas-key-required": "SAS Key is required.", - "iot-hub-name": "IoT Hub Name (required for downlink)", - "hostname": "Hostname", - "hostname-required": "Hostname is required", - "integration-clazz": "Integration class", - "integration-clazz-required": "Integration class is required", - "integration-configuration": "Integration JSON configuration", - "metadata": "Metadata", - "type": "Integration type", - "select-integration-type": "Select integration type", - "type-required": "Integration type is required.", - "type-not-found": "Integration type not found", - "uplink-converter": "Uplink data converter", - "uplink-converter-required": "Uplink data converter is required.", - "downlink-converter": "Downlink data converter", - "type-http": "HTTP", - "type-http-description": "Transferring text or multimedia files protocol.", - "type-ocean-connect": "OceanConnect", - "type-ocean-connect-description": "Integration of devices with different capabilities with the IoT platform.", - "type-sigfox": "SigFox", - "type-sigfox-description": "Lightweight protocol to handle small data transmissions.", - "type-thingpark": "ThingPark", - "type-thingpark-description": "Message-oriented transport layer protocol.", - "type-loriot": "Loriot", - "type-loriot-description": "Distributed LoRaWAN infrastructure.", - "type-thingpark-enterprise": "ThingParkEnterprise", - "type-thingpark-enterprise-description": "Messaging platform for the LPWA network.", - "type-tmobile-iot-cdp": "iotcreators.com (T-Mobile – IoT CDP)", - "type-tmobile-iot-cdp-description": "Mobile IoT connectivity service.", - "type-mqtt": "MQTT", - "type-mqtt-description": "Machine to machine network protocol.", - "type-aws-iot": "AWS IoT", - "type-aws-iot-description": "Management, storage, and analytics service.", - "type-aws-sqs": "AWS SQS", - "type-aws-sqs-description": "Fully managed message queuing service.", - "type-aws-kinesis": "AWS Kinesis", - "type-aws-kinesis-description": "Service for real-time video and data streams.", - "type-ibm-watson-iot": "IBM Watson IoT", - "type-ibm-watson-iot-description": "Fully managed, cloud-hosted service.", - "type-ttn": "The Things Stack Community", - "type-ttn-description": "Open and decentralized LoRaWAN network.", - "type-tti": "The Things Stack Industries", - "type-tti-description": "Software to operate private LoRaWAN networks at scale.", - "type-chirpstack": "ChirpStack", - "type-chirpstack-description": "LoRaWAN network configuration server.", - "type-particle": "Particle", - "type-particle-description": "IoT enablement platform-as-a-service.", - "type-azure-event-hub": "Azure Event Hub", - "type-azure-event-hub-description": "Big data streaming platform and event ingestion service.", - "type-azure-iot-hub": "Azure IoT Hub", - "type-azure-iot-hub-description": "Cloud-hosted messaging service.", - "type-opc-ua": "OPC-UA", - "type-opc-ua-description": "Cross-platform for data exchange from sensors to cloud apps.", - "type-custom": "Custom", - "type-coap": "CoAP", - "type-coap-description": "Machine to machine network protocol for constrained devices.", - "type-udp": "UDP", - "type-udp-description": "Message-oriented transport layer protocol.", - "type-tcp": "TCP", - "type-tcp-description": "Connection-oriented protocol.", - "type-kafka": "Kafka", - "type-kafka-description": "Streaming analytics, data integration streaming platform.", - "type-rabbitmq": "RabbitMQ", - "type-rabbitmq-description": "Message broker that supports multiple messaging protocols.", - "type-pubsub": "Pub/Sub", - "type-pubsub-description": "Service for asynchronous messaging between apps.", - "type-apache-pulsar": "Apache Pulsar", - "type-apache-pulsar-description": "Distributed pub-sub messaging and streaming platform.", - "type-tuya": "Tuya", - "type-tuya-description": "Cloud-hosted messaging. device platform.", - "type-azure-service-bus": "Azure Service Bus", - "type-azure-service-bus-description": "Fully managed enterprise message broker with message queues and publish-subscribe topics.", - "opc-ua-application-name": "Application name", - "opc-ua-application-uri": "Application uri", - "opc-ua-scan-period-in-seconds": "Scan period in seconds", - "opc-ua-scan-period-in-seconds-required": "Scan period is required", - "opc-ua-timeout": "Timeout in milliseconds", - "opc-ua-timeout-required": "Timeout is required", - "opc-ua-security": "Security", - "opc-ua-security-required": "Security is required", - "opc-ua-identity": "Identity", - "opc-ua-identity-required": "Identity is required", - "opc-ua-keystore": "Keystore", - "add-opc-ua-keystore-prompt": "Please add keystore file", - "opc-ua-keystore-required": "Keystore is required", - "opc-ua-type": "Type", - "opc-ua-keystore-type": "Type", - "opc-ua-keystore-type-required": "Type is required", - "opc-ua-keystore-location": "Location *", - "opc-ua-keystore-password": "Password", - "opc-ua-keystore-password-required": "Password is required", - "opc-ua-keystore-alias": "Alias", - "opc-ua-keystore-alias-required": "Alias is required", - "opc-ua-keystore-key-password": "Key password", - "opc-ua-keystore-key-password-required": "Key password is required", - "opc-ua-mapping": "Mapping", - "add-opc-ua-mapping-prompt": "Please add mapping", - "opc-ua-mapping-type": "Mapping type", - "opc-ua-mapping-type-required": "Mapping type is required", - "opc-ua-device-node-pattern": "Device Node Pattern", - "opc-ua-device-node-pattern-required": "Device Node Pattern is required", - "opc-ua-namespace": "Namespace", - "opc-ua-add-map": "Add mapping element", - "kinesis-stream-name": "Stream name", - "kinesis-stream-name-required": "Stream name is required", - "kinesis-region": "Region", - "kinesis-region-required": "Region is required", - "kinesis-access-key-id": "Access Key Id", - "kinesis-access-key-id-required": "Access Key Id is required", - "kinesis-secret-access-key": "Secret Access Key", - "kinesis-secret-access-key-required": "Secret Access Key is required", - "kinesis-use-consumers-with-enhanced-fan-out": "Use Consumers with Enhanced Fan-Out", - "kinesis-use-credentials-from-instance-metadata": "Use credentials from the Amazon EC2 Instance Metadata Service", - "kinesis-application-name": "Application name (by default equals Stream name)", - "kinesis-initial-position-in-stream": "Initial position in stream", - "kinesis-initial-position-in-stream-required": "Initial position in stream required", - "kinesis-max-records": "Max records", - "kinesis-max-records-required": "Max records is required", - "kinesis-max-records-length-range": "Max records length should be in a range from 1 to 10000", - "kinesis-request-timeout": "Request timeout in seconds", - "kinesis-request-timeout-required": "Request timeout is required", - "other-properties": "Other properties", - "subscription-tags": "Subscription tags", - "remove-subscription-tag": "Remove subscription tag", - "add-subscription-tag": "Add subscription tag", - "add-subscription-tag-prompt": "Please add subscription tag", - "key": "Key", - "path": "Path", - "required": "Required", - "integration-key": "Integration key", - "copy-integration-key": "Copy integration key", - "integration-key-copied-message": "Integration key has been copied to clipboard", - "integration-secret": "Integration secret", - "copy-integration-secret": "Copy integration secret", - "integration-secret-copied-message": "Integration secret has been copied to clipboard", - "execute-remotely": "Execute remotely", - "remote": "Remote", - "handler-configuration": "Handler Configuration", - "handler-configuration-type": "Handler Type", - "so-broadcast": "Enable broadcast - integration will accepts broadcast address packets", - "so-keepalive-option": "Enable sending of keep-alive messages on connection-oriented sockets", - "so-reuse-addr": "Bind the process to a port", - "tcp-no-delay": "Forces a socket to send the data without buffering (disable Nagle's buffering algorithm)", - "fail-fast": "Thrown exception soon as the decoder notices the length of the frame will exceed max size", - "strip-delimiter": "Strip Delimiter", - "length-field-offset": "Length Field Offset", - "length-field-offset-required": "Length Field Offset is required.", - "length-field-offset-range": "Length Field Offset should be in a range from 0 to 8.", - "length-field-length": "Length Field Length", - "length-field-length-required": "Length Field Length is required.", - "length-field-length-range": "Length Field Length should be in a range from 0 to 8.", - "length-adjustment": "Length Adjustment (the compensation value to add to the value of the length field)", - "length-adjustment-required": "Length Adjustment is required.", - "length-adjustment-range": "Length Adjustment should be in a range from 0 to 8.", - "byte-order": "Byte Order of the length field", - "initial-bytes-to-strip": "Number of first bytes to strip out from the decoded frame", - "initial-bytes-to-strip-required": "Number of first bytes to strip out from the decoded frame is required.", - "initial-bytes-to-strip-range": "Number of first bytes to strip out from the decoded frame should be in a range from 0 to 8.", - "so-backlog-option": "Max number of pending connects on the socket", - "so-backlog-option-required": "The max number of pending connects on the socket is required.", - "so-backlog-option-range": "The max number of pending connects on the socket should be in a range from 1 to 65535.", - "so-rcv-buf": "Size of the buffer for inbound socket (in KB)", - "so-rcv-buf-required": "Size of the buffer for inbound socket (in KB) is required.", - "so-rcv-buf-range": "Size of the buffer for inbound socket (in KB) should be in a range from 1 to 65535.", - "so-snd-buf": "Size of the buffer for outbound socket (in KB)", - "so-snd-buf-required": "Size of the buffer for outbound socket (in KB) is required.", - "so-snd-buf-range": "Size of the buffer for outbound socket (in KB) should be in a range from 1 to 65535.", - "charset-name": "Charset Name", - "charset-name-required": "Charset Name is required.", - "message-separator": "Message Separator", - "message-separator-required": "Message Separator is required.", - "character-sequence": "Character Sequence", - "character-sequence-required": "Character Sequence is required.", - "max-frame-length": "Max Frame Length (in bytes)", - "max-frame-length-required": "Max Frame Length (in bytes) is required.", - "max-frame-length-range": "Max Frame Length (in bytes) should be in a range from 1 to 65535.", - "handler-type": "Handler Type", - "message-size": "Message Size", - "message-size-required": "Message Size is required.", - "service-url": "Service URL", - "service-url-required": "Service URL is required.", - "subscription-name": "Subscription name", - "subscription-name-required": "Subscription name is required.", - "max-num-messages": "Max number of messages", - "max-num-messages-required": "Max number of messages is required.", - "max-num-bytes": "Max number of bytes", - "max-num-bytes-required": "Max number of bytes required.", - "timeout-in-ms": "Timeout in milliseconds", - "timeout-in-ms-required": "Timeout in milliseconds is required.", - "user-id": "User ID", - "user-id-required": "User ID is required.", - "token": "Token", - "token-required": "Token is required.", - "project-id": "Project ID", - "project-id-required": "Project ID is required.", - "subscription-id": "Subscription ID", - "subscription-id-required": "Subscription ID is required.", - "service-account-key": "Service Account key file", - "service-account-key-required": "Service Account key file is required.", - "tcp": { - "system-line-separator": "System Line Separator", - "nul-delimiter": "Nul Delimiter", - "byte-order-little-endian": "Little Endian", - "byte-order-big-endian": "Big Endian" - }, - "cache-size": "Cache Size", - "cache-time-to-live": "Cache time to live in minutes", - "min-cache-size": "Cache size can't be lower 0", - "min-cache-time-to-live": "Cache time to live can't be lower 0", - "max-cache-time-to-live": "Invalid time of cache live, select between 0 and 525600", - "coap-security-mode": "Security mode", - "coap-security-mode-required": "CoAP security mode is required", - "coap-security-mode-no-secure": "NO SECURE", - "coap-security-mode-dtls": "DTLS", - "coap-security-mode-mixed": "MIXED", - "coap-endpoint": "CoAP endpoint URL", - "coap-endpoint-url-copied-message": "CoAP endpoint URL has been copied to clipboard", - "copy-coap-endpoint-url": "Copy CoAP endpoint URL", - "copy-coap-dtls-endpoint-url": "Copy CoAP DTLS endpoint URL", - "coap-dtls-base-url": "DTLS Base URL", - "coap-dtls-base-url-required": "DTLS Base URL is required", - "coap-dtls-endpoint": "CoAP DTLS endpoint URL", - "coap-dtls-endpoint-url-copied-message": "CoAP DTLS endpoint URL has been copied to clipboard", - "unassign-integration-title": "Are you sure you want to unassign the integration '{{integrationName}}'?", - "unassign-integration-from-edge-text": "After the confirmation the integration will be unassigned and won't be accessible by the edge.", - "unassign-integrations-from-edge-title": "Are you sure you want to unassign { count, plural, =1 {1 integration} other {# integrations} }?", - "unassign-integrations-from-edge-text": "After the confirmation all selected integrations will be unassigned and won't be accessible by the edge.", - "unassign-integrations": "Unassign integrations", - "edge-placeholder-hint": "You can use placeholder ${{ATTRIBUTE_KEY}} to substitute integration field with attribute value from specific Edge entity. For example, 'Edge A' has attribute 'baseUrl' that equals to 'http://localhost:9999'. You can set ${{baseUrl}} as one of the integration fields, and it is going to be replaced to 'http://localhost:9999' during the assignment of this integration to 'Edge A'. Additionally, if 'Edge A' attribute 'baseUrl' is going to be updated - integration with new updated value is going to be automatically provisioned to 'Edge A'.", - "status": { - "status": "Status", - "active": "Active", - "disabled": "Disabled", - "failed": "Failed", - "pending": "Pending" - }, - "daily-activity": "Daily activity", - "enable-debug-mode": "Enable debug mode", - "disable-debug-mode": "Disable debug mode", - "advanced-settings": "Advanced settings", - "basic-settings": "Basic settings", - "existing-converter": "Select existing", - "new-converter": "Create new", - "connection": "Connection", - "connected": "Connected", - "not-connected": "Not connected", - "environment": "Environment", - "environment-required": "Environment is required.", - "region-cn": "China", - "region-us": "United States", - "region-eu": "Europe", - "region-in": "India", - "topic-name": "Topic name", - "topic-name-required": "Topic name is required", - "sub-name": "Subscription name", - "sub-name-required": "Subscription name is required", - "downlink-connection-string": "Downlink connection string", - "downlink-connection-string-required": "Downlink connection string is required", - "downlink-topic-name": "Downlink topic name", - "downlink-topic-name-required": "Downlink topic name is required" - }, - "item": { - "selected": "Pasirinkta" - }, - "js-func": { - "no-return-error": "Funkcija privalo grąžinti reikšmę!", - "return-type-mismatch": "Funkcijos grąžinama reikšmė privalo būti '{{type}}' tipo!", - "tidy": "Tidy", - "mini": "Mini" + "single-switch": { + "behavior": "Elgsena", + "layout": "Išdėstymas", + "layout-right": "Dešinėje", + "layout-left": "Kairėje", + "layout-centered": "Centruota", + "auto-scale": "Automatinis mastelis", + "label": "Etiketė", + "icon": "Piktograma", + "switch-color": "Jungiklio spalva", + "on": "Įjungta", + "off": "Išjungta", + "disabled": "Išjungta būsena", + "tumbler-color": "Perjungiklio spalva", + "on-label": "Įjungta etiketė", + "off-label": "Išjungta etiketė", + "switch": "Jungiklis" }, - "key-val": { - "key": "Raktas", - "value": "Reikšmė", - "remove-entry": "Panaiknti įrašą", - "add-entry": "Pridėti įrašą", - "no-data": "Įrašų nėra" - }, - "layout": { - "layout": "Maketas", - "layouts": "Maketai", - "manage": "Valdyti maketus", - "settings": "Maketų nustatymai", - "color": "Spalva", - "main": "Pagrindinis", - "right": "Į dešinę", - "left": "Į kairę", - "select": "Pasirinkti maketą", - "percentage-width": "Procentinis plotis (%)", - "fixed-width": "Fiksuotas plotis (px)", - "left-width": "Kairysis stulpelis (%)", - "right-width": "Dešinysis stulpelis (%)", - "pick-fixed-side": "Fiksuota pusė: ", - "layout-fixed-width": "Fiksuotas plotis (px)", - "value-min-error": "Reikšmė turi būti dedesnė nei {{min}}{{unit}}", - "value-max-error": "Reikšmė turi būti mažesnė nei {{max}}{{unit}}", - "layout-fixed-width-required": "Fiksuotas plotis būtinas", - "right-width-percentage-required": "Dešinės pusės procentas būtinas", - "left-width-percentage-required": "kairės pusės procentas būtinas", - "divider": "Daliklis", - "right-side": "Dešinės pusės maketas", - "left-side": "Kairės pusės maketas" - }, - "legend": { - "direction": "Elementų išdėstymo kryptis", - "position": "Legendos pozicija", - "show-values": "Rodyti reikšmes", - "min-option": "Min", - "max-option": "Max", - "average-option": "Vidurkis", - "total-option": "Viso", - "latest-option": "Naujausi", - "sort-legend": "Legendoje rodyti duomenų raktus", - "show-max": "Rodyti didžiausią reišmę", - "show-min": "Rodyti mažiausią reikšmę", - "show-avg": "Rodyti vidutinę reikšmę", - "show-total": "Rodyti suminę rekšmę", - "show-latest": "Rodyti naujausią reikšmę", - "settings": "Legendos nustatymai", - "min": "Min", - "max": "Max", - "avg": "Vidurkis", - "total": "Viso", - "latest": "Naujausia", - "comparison-time-ago": { - "previousInterval": "(previous interval)", - "customInterval": "(custom interval)", - "days": "(day ago)", - "weeks": "(week ago)", - "months": "(month ago)", - "years": "(year ago)" - } + "slider": { + "behavior": "Elgsena", + "initial-value": "Pradinė reikšmė", + "initial-value-hint": "Veiksmas, skirtas gauti pradinę slankiklio reikšmę.", + "on-value-change": "Keičiantis reikšmei", + "on-value-change-hint": "Veiksmas, paleidžiamas, kai pasikeičia slankiklio reikšmė.", + "layout": "Išdėstymas", + "layout-default": "Numatytasis", + "layout-extended": "Išplėstinis", + "layout-simplified": "Supaprastintas", + "auto-scale": "Automatinis mastelis", + "icon": "Piktograma", + "value": "Reikšmė", + "range": "Intervalas", + "min": "min", + "max": "maks", + "range-ticks": "Intervalo žymės", + "tick-marks": "Padalos", + "colors": "Spalvos", + "main": "Pagrindinė", + "background": "Fonas", + "left-icon": "Kairioji piktograma", + "right-icon": "Dešinioji piktograma", + "slider": "Slankiklis" }, - "login": { - "login": "Prisijungti", - "request-password-reset": "Prašyti atstatyti slaptažodį", - "reset-password": "Atstatyti slaptažodį", - "create-password": "Nustatyti slaptažodį", - "two-factor-authentication": "Dviejų veiksnių autentifikavimas", - "passwords-mismatch-error": "Įvesti slaptažodžiai turi sutapti!", - "password-again": "Slaptažodį įveskite dar kartą", - "sign-in": "Prisijunkite", - "username": "Vartoto vardas (el. paštas)", - "remember-me": "Prisiminti mane", - "forgot-password": "Pamiršote slaptažodį?", - "password-reset": "Slaptažodis atstatytas", - "expired-password-reset-message": "Jūsų slaptažodio galiojimas baigėsi! Nustatykite naują slaptažodį.", - "new-password": "Naujas slaptažodis", - "new-password-again": "Pakartokite naują slaptažodį", - "password-link-sent-message": "Atstatymo nuoroda išsiųsta", - "email": "El. paštas", - "no-account": "Neturi paskyros?", - "create-account": "Sukurti paskyrą", - "login-with": "Prisijungti kaip {{name}}", - "or": "arba", - "error": "Login error", - "verify-your-identity": "Verify your identity", - "select-way-to-verify": "Select a way to verify", - "resend-code": "Resend code", - "resend-code-wait": "Resend code in { time, plural, =1 {1 second} other {# seconds} }", - "try-another-way": "Try another way", - "totp-auth-description": "Please enter the security code from your authenticator app.", - "totp-auth-placeholder": "Code", - "sms-auth-description": "A security code has been sent to your phone at {{contact}}.", - "sms-auth-placeholder": "SMS code", - "email-auth-description": "A security code has been sent to your email address at {{contact}}.", - "email-auth-placeholder": "Email code", - "backup-code-auth-description": "Please enter one of your backup codes.", - "backup-code-auth-placeholder": "Backup code" - }, - "signup": { - "firstname": "Vardas", - "lastname": "Pavardė", - "email": "El. paštas", - "signup": "Registracija", - "create-password": "nustatyti slaptažodį", - "repeat-password": "Pakartokite slaptažodį", - "have-account": "Jau turite paskyrą?", - "signin": "Prisijungti", - "no-captcha-message": "Turite patvirtinti, jog nesate robotas", - "password-length-message": "Jūsų slaptažodis turi būti ne nei 6 simboliai", - "email-verification": "El. pašto adreso patvirtinimas", - "email-verification-message": "El. laiškas su patvirtinimo informacija išsiųstas nurodytu el. pašto adresu.
    Sekite laiške pateiktas intrukcijas ir atlikite prisijungimui reikalingus veiksmus.
    Pastaba: Jei nematote el. laiško, patikrinkite 'Šlamšto' katalogą arba bandykite dar kartą siųsti laišką paspausdami 'Siųsti dar kartą' mygtuką", - "account-activation-title": "Paskyros aktyvavimas", - "account-activated": "Paskyra aktyvuota!", - "account-activated-text": "Sveikiname!
    Jūsų paskyra aktyvuota.", - "resend": "Siųsti dar kartą", - "inactive-user-exists-title": "Neaktyvuotas vartotojas sistemoje jau yra", - "inactive-user-exists-text": "Vartotojas su tokiu el. pašto adresu jau užregistruotas, tačiau šis adresas nepatvirtintas.
    Paspauskite 'Siųsti dar kartą' mygtuką jei norite išsiųsti patvirtinimo laišką.", - "activating-account": "Paskyra aktyvuojama...", - "activating-account-text": "Jūsų paskyra aktyvuojama. Palaukite...", - "accept-privacy-policy": "Sutikti su privatumo politika", - "accept": "Sutikti", - "privacy-policy": "Privatumo politika", - "accept-privacy-policy-message": "You must accept our Privacy Policy", - "recaptcha-title": "reCAPTCHA", - "terms-of-use": "Terms of Use", - "accept-terms-of-use-message": "You must accept our Terms Of Use" + "value-card": { + "layout": "Išdėstymas", + "layout-square": "Kvadratinis", + "layout-vertical": "Vertikalus", + "layout-centered": "Centruotas", + "layout-simplified": "Supaprastintas", + "layout-horizontal": "Horizontalus", + "layout-horizontal-reversed": "Apverstas horizontalus", + "label": "Etiketė", + "icon": "Piktograma", + "value": "Reikšmė", + "date": "Data", + "value-card-style": "Reikšmės kortelės stilius", + "auto-scale": "Automatinis mastelis" + }, + "label-card": { + "auto-scale": "Automatinis mastelis", + "label": "Etiketė", + "icon": "Piktograma", + "label-card-style": "Etiketės kortelės stilius" + }, + "label-value-card": { + "value": "Reikšmė", + "label-value-card-style": "Etiketės ir reikšmės kortelės stilius" + }, + "liquid-level-card": { + "layout-simple": "Paprastas", + "layout-percentage": "Procentinis", + "layout-absolute": "Absoliutus", + "layout": "Išdėstymas", + "background-overlay": "Reikšmės fono perdanga", + "total-volume": "Bendras tūris", + "total-volume-units": "Bendro tūrio vienetai", + "tank": "Talpa", + "shape": "Forma", + "datasource-units": "Šaltinio vienetai", + "widget-units": "Valdiklio vienetai", + "decimals": "Dešimtainės", + "liquid": "Skystis", + "liquid-color": "Skysčio spalva", + "value": "Reikšmė", + "value-font": "Reikšmės šriftas", + "level": "Lygis", + "last-update": "Paskutinis atnaujinimas", + "shape-by-attribute": "Nustatyti talpos formą pagal atributo pavadinimą", + "tooltip-background": "Fono spalva", + "background-blur": "Fono suliejimas", + "tank-color": "Talpos spalva", + "static": "Statinis", + "see-examples": "Peržiūrėti pavyzdžius", + "attribute": "Atributas", + "shape-type": "Tipas", + "v-oval": "Vertikali ovalo forma", + "v-cylinder": "Vertikalus cilindras", + "v-capsule": "Vertikali kapsulė", + "rectangle": "Stačiakampis", + "h-oval": "Horizontalus ovalas", + "h-ellipse": "Horizontali elipsė", + "h-dish-ends": "Horizontalūs išgaubti galai", + "h-cylinder": "Horizontalus cilindras", + "h-capsule": "Horizontali kapsulė", + "h-elliptical_2_1": "Horizontalus 2:1 elipsinis", + "icon": "Kortelės piktograma", + "title": "Kortelės pavadinimas", + "units": "Vienetai", + "color-and-font": "Spalva ir šriftas", + "shape-attribute-name": "Atributo pavadinimas", + "total-volume-required": "Bendras tūris yra privalomas.", + "attribute-name-required": "Atributo pavadinimas yra privalomas.", + "attribute-key-not-set": "Atributo '{{attributeName}}' raktas nenustatytas", + "attribute-key-invalid": "Atributo '{{attributeName}}' raktas neteisingas" + }, + "aggregated-value-card": { + "subtitle": "Paantraštė", + "chart": "Diagrama", + "values": "Reikšmės", + "value-appearance": "Reikšmės išvaizda", + "position": "Padėtis", + "position-center": "Centre", + "position-right-top": "Dešinėje viršuje", + "position-right-bottom": "Dešinėje apačioje", + "position-left-top": "Kairėje viršuje", + "position-left-bottom": "Kairėje apačioje", + "font": "Šriftas", + "color": "Spalva", + "arrow": "Rodyklė", + "display-up-down-arrow": "Rodyti aukštyn/žemyn rodyklę", + "add-value": "Pridėti reikšmę", + "remove-value": "Pašalinti reikšmę", + "no-values": "Reikšmės nesukonfigūruotos", + "aggregation": "Agregavimas", + "aggregated-value-card-style": "Agreguotų reikšmių kortelės stilius", + "auto-scale": "Automatinis mastelis" + }, + "value-chart-card": { + "layout": "Išdėstymas", + "layout-left": "Kairėje", + "layout-right": "Dešinėje", + "auto-scale": "Automatinis mastelis", + "icon": "Piktograma", + "value": "Reikšmė", + "chart": "Diagrama", + "value-chart-card-style": "Reikšmės ir diagramos kortelės stilius" + }, + "progress-bar": { + "layout": "Išdėstymas", + "layout-default": "Numatytasis", + "layout-simplified": "Supaprastintas", + "auto-scale": "Automatinis mastelis", + "icon": "Piktograma", + "value": "Reikšmė", + "range": "Intervalas", + "min": "min", + "max": "maks", + "range-ticks": "Intervalo žymės", + "bar": "Juosta", + "bar-color": "Juostos spalva", + "bar-background": "Juostos fonas", + "progress-bar-card-style": "Progreso juostos kortelės stilius" }, "notification": { - "action-button": "Action button", - "action-type": "Action type", - "active": "Active", - "add-notification-recipients-group": "Add notification recipients group", - "add-notification-template": "Add notification template", - "add-recipient": "Add recipient", - "add-recipients": "Add recipients", - "add-rule": "Add rule", - "add-stage": "Add stage", - "add-template": "Add template", - "after": "After", - "alarm-assignment-trigger-settings": "Alarm assignment trigger settings", - "alarm-comment-trigger-settings": "Alarm comment trigger settings", - "alarm-trigger-settings": "Alarm trigger settings", - "all": "All", - "api-feature-hint": "If the field is empty, the trigger will be applied to all api features", - "api-usage-trigger-settings": "API usage trigger settings", - "new-platform-version-trigger-settings": "New platform version trigger settings", - "rate-limits-trigger-settings": "Exceeded rate limits trigger settings", - "at-least-one-should-be-selected": "At least one should be selected", - "basic-settings": "Basic settings", - "button-text": "Button text", - "button-text-required": "Button text is required", - "button-text-max-length": "Button text should be less than or equal to {{ length }} characters", - "compose": "Compose", - "conversation": "Conversation", - "conversation-required": "Conversation is required", - "copy-notification-template": "Copy notification template", - "copy-rule": "Copy rule", - "copy-template": "Copy template", - "create-new": "Create new", - "created": "Created", - "delete-notification-text": "Be careful, after the confirmation the notification will become unrecoverable.", - "delete-notification-title": "Are you sure you want to delete the notification?", - "delete-notifications-text": "Be careful, after the confirmation notifications will become unrecoverable.", - "delete-notifications-title": "Are you sure you want to delete { count, plural, =1 {1 notification} other {# notifications} }?", - "delete-recipient-text": "Be careful, after the confirmation the recipient will become unrecoverable.", - "delete-recipient-title": "Are you sure you want to delete recipient '{{recipientName}}'?", - "delete-recipients-text": "Be careful, after the confirmation recipients will become unrecoverable.", - "delete-recipients-title": "Are you sure you want to delete { count, plural, =1 {1 recipient} other {# recipients} }?", - "delete-request-text": "Be careful, after the confirmation request will become unrecoverable.", - "delete-request-title": "Are you sure you want to delete request?", - "delete-requests-text": "Be careful, after the confirmation requests will become unrecoverable.", - "delete-requests-title": "Are you sure you want to delete { count, plural, =1 {1 request} other {# requests} }?", - "delete-rule-text": "Be careful, after the confirmation rule will become unrecoverable.", - "delete-rule-title": "Are you sure you want to delete rule '{{ruleName}}'?", - "delete-rules-text": "Be careful, after the confirmation rules will become unrecoverable.", - "delete-rules-title": "Are you sure you want to delete { count, plural, =1 {1 rule} other {# rules} }?", - "delete-template-text": "Be careful, after the confirmation template will become unrecoverable.", - "delete-template-title": "Are you sure you want to delete template '{{templateName}}'?", - "delete-templates-text": "Be careful, after the confirmation templates will become unrecoverable.", - "delete-templates-title": "Are you sure you want to delete { count, plural, =1 {1 template} other {# templates} }?", - "deleted": "Deleted", - "delivery-method": { - "delivery-method": "Delivery method", - "email": "Email", - "email-preview": "Email notification preview", - "slack": "Slack", - "slack-preview": "Slack notification preview", - "microsoft-teams": "Microsoft Teams", - "microsoft-teams-preview": "Microsoft Teams notification preview", - "sms": "SMS", - "sms-preview": "SMS notification preview", - "web": "Web", - "web-preview": "Web notification preview" - }, - "delivery-method-not-configure-click": "Delivery method is not configured. Click to setup.", - "delivery-method-not-configure-contact": "Delivery method is not configured. Contact your system administrator.", - "delivery-methods": "Delivery methods", - "description": "Description", - "device-activity-trigger-settings": "Device active trigger settings", - "device-list-rule-hint": "If the field is empty, the trigger will be applied to all devices", - "device-profiles-list-rule-hint": "If the field is empty, the trigger will be applied to all device profiles", - "disabled": "Disabled", - "edit-notification-recipients-group": "Edit notification recipients group", - "edit-notification-template": "Edit notification template", - "edit-rule": "Edit rule", - "edit-template": "Edit template", - "enabled": "Enabled", - "entities-limit-trigger-settings": "Entities limit trigger settings", - "entity-action-trigger-settings": "Entity action trigger settings", - "entity-type": "Entity type", - "escalation-chain": "Escalation chain", - "failed-send": "Sending failures", - "fails": "{ count, plural, =1 {1 fail} other {# fails} }", - "filter": "Filter", - "first-recipient": "First recipient", - "inactive": "Inactive", - "inbox": "Inbox", - "notification-inbox": "Notifications / Inbox", - "input-field-support-templatization": "Input field support templatization.", - "input-fields-support-templatization": "Input fields support templatization.", - "integration-action-trigger-settings": "Integration events trigger settings", - "integration-list-rule-hint": "If the field is empty, the trigger will be applied to all integrations", - "link": "Link", - "link-required": "Link is required", - "link-type": { - "dashboard": "Open dashboard", - "link": "Open URL link" - }, - "loading-notifications": "Loading notifications...", - "management": "Notification management", - "mark-all-as-read": "Mark all as read", - "mark-as-read": "Mark as read", - "message": "Message", - "message-required": "Message is required", - "message-max-length": "Message should be less than or equal to {{ length }} characters", - "name": "Name", - "name-required": "Name is required", - "new-notification": "New notification", - "no-inbox-notification": "No notification found", - "no-notification-request": "No notification request", - "no-notification-templates": "No notification templates found", - "no-notifications-yet": "No notifications yet", - "no-recipients-notification": "No recipients notification", - "no-rule": "No rule configured", - "no-rules-notification": "No rules notification", - "no-severity-found": "No severity found", - "no-severity-matching": "'{{severity}}' not found.", - "no-template-matching": "No resource matching '{{template}}' were found.", - "not-found-slack-recipient": "Slack recipient not found", - "notification": "Notification", - "notification-center": "Notification center", - "notify": "notify", - "notify-again": "Notify again", - "notify-alarm-action": { - "acknowledged": "Alarm acknowledged", - "assigned": "Alarm assigned", - "cleared": "Alarm cleared", - "created": "Alarm created", - "severity-changed": "Alarm severity changed", - "unassigned": "Alarm unassigned" - }, - "notify-on": "Notify on", - "notify-on-comment-update": "Notify on comment update", - "notify-on-required": "Notify on is required", - "notify-on-unassign": "Notify on unassign", - "notify-only-integrations-errors": "Only notify on error", - "notify-only-user-comments": "Notify only user comments", - "only-rule-chain-lifecycle-failures": "Only rule chain lifecycle failures", - "only-rule-node-lifecycle-failures": "Only rule node lifecycle failures", - "platform-users": "Platform users", - "rate-limits": "Rate limits", - "rate-limits-hint": "If the field is empty, the trigger will be applied to all rate limits", - "recipient": "Recipient", - "recipient-group": "Recipient group", - "recipient-type": { - "affected-tenant-administrators": "Affected tenant administrators", - "affected-user": "Affected user", - "affected-user-hint": "Affected user hint", - "all-users": "All users", - "customer-users": "Customer users", - "system-administrators": "System administrators", - "tenant-administrators": "Tenant administrators", - "user-filters": "User filter", - "user-group-list": "User group list", - "user-list": "User list", - "user-role": "User role", - "users-entity-owner": "Users of the entity owner", - "users-entity-owner-hint": "Users of the entity owner hint" - }, - "recipients": "Recipients", - "notification-recipients": "Notifications / Recipients", - "recipients-count": "{ count, plural, =1 {1 recipient} other {# recipients} }", - "recipients-required": "Recipients are required", - "refresh-allow-delivery-method": "Refresh allow delivery method", - "request-search": "Request search", - "request-status": { - "processing": "Processing", - "scheduled": "Scheduled", - "sent": "Sent" - }, - "review": "Review", - "rule": "Rule", - "rule-chain-list-rule-hint": "If the field is empty, the trigger will be applied to all rule chains", - "rule-engine-events-trigger-settings": "Rule engine events trigger settings", - "rule-engine-filter": "Rule engine filter", - "rule-name": "Rule name", - "rule-name-required": "Name is required", - "rule-disable": "Disable notification rule", - "rule-enable": "Enable notification rule", - "rule-node-filter": "Rule node filter", - "rules": "Rules", - "notification-rules": "Notifications / Rules", - "scheduler-later": "Schedule for later", - "search-notification": "Search notifications", - "search-recipients": "Search recipients", - "search-rules": "Search rules", - "search-templates": "Search templates", - "see-documentation": "See documentation", - "selected-notifications": "{ count, plural, =1 {1 notification} other {# notifications} } selected", - "selected-recipients": "{ count, plural, =1 {1 recipient} other {# recipients} } selected", - "selected-requests": "{ count, plural, =1 {1 request} other {# requests} } selected", - "selected-rules": "{ count, plural, =1 {1 rule} other {# rules} } selected", - "selected-template": "{ count, plural, =1 {1 template} other {# templates} } selected", - "send-notification": "Send notification", - "sent": "Sent", - "notification-sent": "Notifications / Sent", - "set-entity-from-notification": "Set entity from notification to dashboard state", - "slack-chanel-type": "Slack channel type", - "slack-chanel-types": { - "direct": "Direct message", - "private-channel": "Private channel", - "public-channel": "Public channel" - }, - "start-from-scratch": "Start from scratch", - "status": "Status", - "stop-escalation-alarm-status-become": "Stop the escalation on the alarm status become:", - "subject": "Subject", - "subject-required": "Subject is required", - "template": "Template", - "template-name": "Template name", - "template-required": "Template is required", - "template-type": { - "alarm": "Alarm", - "alarm-assignment": "Alarm assignment", - "alarm-comment": "Alarm comment", - "api-usage-limit": "API usage limit", - "device-activity": "Device activity", - "entities-limit": "Entities limit", - "entity-action": "Entity action", - "general": "General", - "integration-lifecycle-event": "Integration lifecycle event", - "rule-engine-lifecycle-event": "Rule engine lifecycle event", - "rule-node": "Rule node", - "new-platform-version": "New platform version", - "rate-limits": "Exceeded rate limits" - }, - "templates": "Templates", - "notification-templates": "Notifications / Templates", - "tenant-profiles-list-rule-hint": "If the field is empty, the trigger will be applied to all tenant profiles", - "tenants-list-rule-hint": "If the field is empty, the trigger will be applied to all tenants", - "threshold": "Threshold", - "theme-color": "Theme color", - "time": "Time", - "track-rule-node-events": "Track rule node events", - "trigger": { - "alarm": "Alarm", - "alarm-assignment": "Alarm assignment", - "alarm-comment": "Alarm comment", - "api-usage-limit": "API usage limit", - "device-activity": "Device activity", - "entities-limit": "Entities limit", - "entity-action": "Entity action", - "integration-lifecycle-event": "Integration lifecycle event", - "rule-engine-lifecycle-event": "Rule engine lifecycle event", - "new-platform-version": "New platform version", - "rate-limits": "Exceeded rate limits", - "trigger": "Trigger", - "trigger-required": "Trigger is required" - }, - "type": "Type", - "unread": "Unread", - "updated": "Updated", - "use-template": "Use template", - "view-all": "View all", - "view-notification-recipients-group": "View notification recipients group", - "view-notification-template": "View notification template", - "view-rule": "View rule", - "warning": "Warning", - "webhook-url": "Webhook URL", - "webhook-url-required": "Webhook URL is required", - "channel-name": "Channel name", - "channel-name-required": "Channel name is required", - "settings": { - "notification-settings": "Notification settings", - "reset-all": "Reset all settings", - "reset-all-title": "Are you sure you want to reset form?", - "reset-all-text": "After the confirmation, the settings form will reset to the default value and save.", - "type": "Type", - "enable-all": "Enable all", - "disable-all": "Disable all", - "delivery-not-configured": "Delivery method is not configured" - } + "max-notification-display": "Maksimalus rodomų pranešimų skaičius", + "counter": "Skaitiklis", + "counter-hint": "Skaitiklis bus rodomas, jei įjungtas „Valdiklio pavadinimas“", + "icon": "Piktograma", + "counter-value": "Reikšmė", + "counter-color": "Spalva", + "notification-button": "Pranešimų mygtukai", + "button-view-all": "Peržiūrėti visus", + "button-filter": "Filtruoti", + "type-filter": "Tipų filtras", + "button-mark-read": "Pažymėti visus kaip skaitytus", + "notification-types": "Pranešimų tipai", + "notification-type": "Pranešimo tipas", + "search-type": "Paieškos tipas", + "any-type": "Bet koks tipas" }, - "ota-update": { - "add": "Add package", - "assign-firmware": "Assigned firmware", - "assign-firmware-required": "Assigned firmware is required", - "assign-software": "Assigned software", - "assign-software-required": "Assigned software is required", - "auto-generate-checksum": "Auto-generate checksum", - "cant-applied-group-all": "Can't be applied for group All", - "checksum": "Checksum", - "checksum-hint": "If checksum is empty, it will be generated automatically", - "checksum-algorithm": "Checksum algorithm", - "checksum-copied-message": "Package checksum has been copied to clipboard", - "change-firmware": "Change of the firmware may cause update of { count, plural, =1 {1 device} other {# devices} }.", - "change-software": "Change of the software may cause update of { count, plural, =1 {1 device} other {# devices} }.", - "chose-compatible-device-profile": "The uploaded package will be available only for devices with the chosen profile.", - "chose-firmware-distributed-device": "Choose firmware that will be distributed to the devices", - "chose-software-distributed-device": "Choose software that will be distributed to the devices", - "content-type": "Content type", - "copy-checksum": "Copy checksum", - "copy-direct-url": "Copy direct URL", - "copyId": "Copy package Id", - "copied": "Copied!", - "delete": "Delete package", - "delete-ota-update-text": "Be careful, after the confirmation the OTA update will become unrecoverable.", - "delete-ota-update-title": "Are you sure you want to delete the OTA update '{{title}}'?", - "delete-ota-updates-text": "Be careful, after the confirmation all selected OTA updates will be removed.", - "delete-ota-updates-title": "Are you sure you want to delete { count, plural, =1 {1 OTA update} other {# OTA updates} }?", - "description": "Description", - "direct-url": "Direct URL", - "direct-url-copied-message": "Package direct URL has been copied to clipboard", - "direct-url-required": "Direct URL is required", - "download": "Download package", - "drop-file": "Drop a package file or click to select a file to upload.", - "drop-package-file-or": "Drag and drop a package file or", - "file-name": "File name", - "file-size": "File size", - "file-size-bytes": "File size in bytes", - "idCopiedMessage": "Package Id has been copied to clipboard", - "no-firmware-matching": "No compatible Firmware OTA Update packages matching '{{entity}}' were found.", - "no-firmware-text": "No compatible Firmware OTA Update packages provisioned.", - "no-packages-text": "No packages found", - "no-software-matching": "No compatible Software OTA Update packages matching '{{entity}}' were found.", - "no-software-text": "No compatible Software OTA Update packages provisioned.", - "ota-update": "OTA update", - "ota-update-details": "OTA update details", - "ota-updates": "OTA updates", - "package-type": "Package type", - "packages-repository": "Packages repository", - "search": "Search packages", - "selected-package": "{ count, plural, =1 {1 package} other {# packages} } selected", - "title": "Title", - "title-required": "Title is required.", - "title-max-length": "Title should be less than 256", - "types": { - "firmware": "Firmware", - "software": "Software" - }, - "upload-binary-file": "Upload binary file", - "use-external-url": "Use external URL", - "version": "Version", - "version-required": "Version is required.", - "version-tag": "Version Tag", - "version-tag-hint": "Custom tag should match the package version reported by your device.", - "version-max-length": "Version should be less than 256", - "warning-after-save-no-edit": "Once the package is uploaded, you will not be able to modify title, version, device profile and package type." - }, - "position": { - "top": "Viršus", - "bottom": "Apačia", - "left": "Kairioji pusė", - "right": "Dešinioji pusė" - }, - "profile": { - "profile": "Profilis", - "last-login-time": "Paskutinio prisijungimo laikas", - "change-password": "Pakeisti slaptažodį", - "current-password": "Dabartinis slaptažodis", - "copy-jwt-token": "Copy JWT token", - "jwt-token": "JWT token", - "token-valid-till": "Token is valid till", - "tokenCopiedSuccessMessage": "JWT token has been copied to clipboard", - "tokenCopiedWarnMessage": "JWT token is expired! Please, refresh the page." - }, - "profiles": { - "profiles": "Profiles" - }, - "security": { - "security": "Security", - "general-settings": "General security settings", - "access-token": "Access token", - "access-token-required": "Access token is required", - "clientId": "Client ID", - "clientId-required": "Client ID is required", - "username": "Username", - "username-required": "Username is required", - "ca-cert": "CA certificate", - "2fa": { - "2fa": "Two-factor authentication", - "2fa-description": "Two-factor authentication protects your account from unauthorized access. All you have to do is enter a security code when you log in.", - "authenticate-with": "You can authenticate with:", - "disable-2fa-provider-text": "Disabling {{name}} will make your account less secure", - "disable-2fa-provider-title": "Are you sure you want to disable {{name}}?", - "get-new-code": "Get new code", - "main-2fa-method": "Use as main two-factor authentication method", - "dialog": { - "activation-step-description-email": "The next time you login in, you will be prompted to enter the security code that will be sent to your email address.", - "activation-step-description-sms": "The next time you login in, you will be prompted to enter the security code that will be sent to the phone number.", - "activation-step-description-totp": "The next time you login in, you will need to provide a two-factor authentication code.", - "activation-step-label": "Activation", - "backup-code-description": "Print out the codes so you have them handy when you need to use them to log in to your account. You can use each backup code once.", - "backup-code-warn": "Once you leave this page, these codes cannot be shown again. Store them safely using the options below.", - "download-txt": "Download (txt)", - "email-step-description": "Enter an email to use as your authenticator.", - "email-step-label": "Email", - "enable-email-title": "Enable email authenticator", - "enable-sms-title": "Enable SMS authenticator", - "enable-totp-title": "Enable authenticator app", - "enter-verification-code": "Enter the 6-digit code here", - "get-backup-code-title": "Get backup code", - "next": "Next", - "scan-qr-code": "Scan this QR code with your verification app", - "send-code": "Send code", - "sms-step-description": "Enter a phone number to use as your authenticator.", - "sms-step-label": "Phone Number", - "success": "Success!", - "totp-step-description-install": "You can install apps like Google Authenticator, Authy, or Duo.", - "totp-step-description-open": "Open the authenticator app on your mobile phone.", - "totp-step-label": "Get app", - "verification-code": "6-digit code", - "verification-code-invalid": "Invalid verification code format", - "verification-code-incorrect": "Verification code is incorrect", - "verification-code-many-request": "Too many requests check verification code", - "verification-step-description": "Enter a 6-digit code we just sent to {{address}}", - "verification-step-label": "Verification" - }, - "provider": { - "email": "Email", - "email-description": "Use a security code sent to your email address to authenticate.", - "email-hint": "Authentication codes are sent via email to {{ info }}", - "sms": "SMS", - "sms-description": "Use your phone to authenticate. We'll send you a security code via SMS message when you log in.", - "sms-hint": "Authentication codes are sent by text message to {{ info }}", - "totp": "Authenticator app", - "totp-description": "Use apps like Google Authenticator, Authy, or Duo on your phone to authenticate. It will generate a security code for logging in.", - "totp-hint": "Authenticator app is set up for your account", - "backup_code": "Backup code", - "backup-code-description": "These printable one-time passcodes allow you to sign in when away from your phone, like when you’re traveling.", - "backup-code-hint": "{{ info }} single-use codes are active at this time" - } - }, - "password-requirement": { - "at-least": "At least:", - "character": "{ count, plural, =1 {1 character} other {# characters} }", - "digit": "{ count, plural, =1 {1 digit} other {# digits} }", - "incorrect-password-try-again": "Incorrect password. Try again", - "lowercase-letter": "{ count, plural, =1 {1 lowercase letter} other {# lowercase letters} }", - "new-passwords-not-match": "New password didn't match", - "password-should-not-contain-spaces": "Your password should not contain spaces", - "password-not-meet-requirements": "Password didn't meet requirements", - "password-requirements": "Password requirements", - "password-should-difference": "New password should be different from current", - "special-character": "{ count, plural, =1 {1 special character} other {# special characters} }", - "uppercase-letter": "{ count, plural, =1 {1 uppercase letter} other {# uppercase letters} }" - } + "alarm-count": { + "alarm-count-card-style": "Signalizacijų skaičiaus kortelės stilius" }, - "relation": { - "relations": "Ryšiai", - "direction": "Direction", - "clear-relation-type": "Clear relation type", - "search-direction": { - "FROM": "Iš", - "TO": "Į" - }, - "direction-type": { - "FROM": "iš", - "TO": "į" - }, - "from-relations": "Išeinantys ryšiai", - "to-relations": "įeinantys ryšiai", - "selected-relations": "Pasirinkta { count, plural, =1 {1 ryšys} other {# ryšiai} } selected", - "type": "Tipas", - "to-entity-type": "Į subjekto tipą", - "to-entity-name": "Į subjektą", - "from-entity-type": "iš subjektų tipo", - "from-entity-name": "Iš subjekto", - "to-entity": "į subjektą", - "from-entity": "Iš subjekto", - "delete": "panaikinti ryšį", - "relation-type": "Ryšio tipas", - "relation-type-required": "Ryšio tipas būtinas.", - "relation-type-max-length": "Ryšio tipas negali viršyti 256 simbolių", - "any-relation-type": "Bet kuris tipas", - "add": "pridėti ryšį", - "edit": "Redaguoti ryšį", - "view": "peržiūrėti ryšį", - "delete-to-relation-title": "Ar tikrai norite panaikinti ryšį su '{{entityName}}' subjektu?", - "delete-to-relation-text": "Būkite dėmesingi, po patvirtinimo '{{entityName}}' subjektas neteks ryšio su dabartiniu subjektu.", - "delete-to-relations-title": "Ar tikrai norite panaikinti { count, plural, =1 {1 ryšį} other {# ryšius} }?", - "delete-to-relations-text": "Būkite dėmesingi, po patvirtinimo visi pasirinkti ryšiai bus panaikinti ir atitinkami subjektai neteks ryšių su dabartiniu subjektu.", - "delete-from-relation-title": "Ar tikrai norite panaikinti ryšį iš '{{entityName}}' subjekto?", - "delete-from-relation-text": "Būkite dėmesingi, po patvirtinimo, dabartinis subjektas neteks ryšio su '{{entityName}}' subjektu.", - "delete-from-relations-title": "Ar tikrai norite panaikinti { count, plural, =1 {1 ryšį} other {# ryšius} }?", - "delete-from-relations-text": "Būkite dėmesingi, po patvirtinimo, visi pasirinkti ryšiai bus panaikinti ir dabartinis subjektas neteks ryšių su atitinkamais subjektais.", - "remove-relation-filter": "Panaikinti ryšių filtrą", - "remove-filter": "Panakinti filtrą", - "add-relation-filter": "Pridėti ryšių filtrą", - "any-relation": "Bet kuris ryšys", - "relation-filters": "Ryšių filtrai", - "additional-info": "Papildoma informacija (JSON)", - "invalid-additional-info": "Papildomos JSON informacijos išanalizuoti nepavyko.", - "no-relations-text": "Ryšių nėra" - }, - "resource": { - "add": "Add Resource", - "all-types": "All", - "copyId": "Copy resource Id", - "delete": "Delete resource", - "delete-resource-text": "Be careful, after the confirmation the resource will become unrecoverable.", - "delete-resource-title": "Are you sure you want to delete the resource '{{resourceTitle}}'?", - "delete-resources-action-title": "Delete { count, plural, =1 {1 resource} other {# resources} }", - "delete-resources-text": "Please note that the selected resources, even if they are used in device profiles, will be deleted.", - "delete-resources-title": "Are you sure you want to delete { count, plural, =1 {1 resource} other {# resources} }?", - "download": "Download resource", - "drop-file": "Drop a resource file or click to select a file to upload.", - "drop-resource-file-or": "Drag and drop a resource file or", - "empty": "Resource is empty", - "file-name": "File name", - "idCopiedMessage": "Resource Id has been copied to clipboard", - "no-resource-matching": "No resource matching '{{widgetsBundle}}' were found.", - "no-resource-text": "No resources found", - "open-widgets-bundle": "Open widgets bundle", - "resource": "Resource", - "resource-library-details": "Resource details", - "resource-type": "Resource type", - "resources-library": "Resources library", - "search": "Search resources", - "selected-resources": "{ count, plural, =1 {1 resource} other {# resources} } selected", - "system": "System", - "title": "Title", - "title-required": "Title is required.", - "title-max-length": "Title should be less than 256", - "type": { - "jks": "JKS", - "js-module": "JS module", - "lwm2m-model": "LWM2M model", - "pkcs-12": "PKCS #12" - } + "entity-count": { + "entity-count-card-style": "Objektų skaičiaus kortelės stilius" }, - "rulechain": { - "rulechain": "Rule chain", - "rulechain-events": "Rule chain events", - "rulechains": "Rule chains", - "root": "Root", - "delete": "Delete rule chain", - "name": "Name", - "name-required": "Name is required.", - "name-max-length": "Name should be less than 256", - "description": "Description", - "add": "Add Rule Chain", - "set-root": "Make rule chain root", - "set-root-rulechain-title": "Are you sure you want to make the rule chain '{{ruleChainName}}' root?", - "set-root-rulechain-text": "After the confirmation the rule chain will become root and will handle all incoming transport messages.", - "delete-rulechain-title": "Are you sure you want to delete the rule chain '{{ruleChainName}}'?", - "delete-rulechain-text": "Be careful, after the confirmation the rule chain and all related data will become unrecoverable.", - "delete-rulechains-title": "Are you sure you want to delete { count, plural, =1 {1 rule chain} other {# rule chains} }?", - "delete-rulechains-action-title": "Delete { count, plural, =1 {1 rule chain} other {# rule chains} }", - "delete-rulechains-text": "Be careful, after the confirmation all selected rule chains will be removed and all related data will become unrecoverable.", - "add-rulechain-text": "Add new rule chain", - "no-rulechains-text": "No rule chains found", - "rulechain-details": "Rule chain details", - "details": "Details", - "events": "Events", - "system": "System", - "import": "Import rule chain", - "export": "Export rule chain", - "export-failed-error": "Unable to export rule chain: {{error}}", - "create-new-rulechain": "Create new rule chain", - "rulechain-file": "Rule chain file", - "invalid-rulechain-file-error": "Unable to import rule chain: Invalid rule chain data structure.", - "copyId": "Copy rule chain Id", - "idCopiedMessage": "Rule chain Id has been copied to clipboard", - "select-rulechain": "Select rule chain", - "no-rulechains-matching": "No rule chains matching '{{entity}}' were found.", - "rulechain-required": "Rule chain is required", - "management": "Rules management", - "debug-mode": "Debug mode", - "search": "Search rule chains", - "selected-rulechains": "{ count, plural, =1 {1 rule chain} other {# rule chains} } selected", - "open-rulechain": "Open rule chain", - "edge-template-root": "Template Root", - "assign-to-edge": "Assign to Edge", - "edge-rulechain": "Edge rule chain", - "unassign-rulechain-from-edge-text": "After the confirmation the rulechain will be unassigned and won't be accessible by the edge.", - "unassign-rulechains-from-edge-title": "Are you sure you want to unassign { count, plural, =1 {1 rulechain} other {# rulechains} }?", - "unassign-rulechains-from-edge-text": "After the confirmation all selected rulechains will be unassigned and won't be accessible by the edge.", - "assign-rulechain-to-edge-title": "Assign Rule Chain(s) To Edge", - "assign-rulechain-to-edge-text": "Please select the rulechains to assign to the edge", - "set-edge-template-root-rulechain": "Make rule chain as edge template root", - "set-edge-tscheduler-eventemplate-root-rulechain": "Make rule chain as edge template root", - "set-edge-template-root-rulechain-title": "Are you sure you want to make the rule chain '{{ruleChainName}}' edge template root?", - "set-edge-template-root-rulechain-text": "After the confirmation the rule chain will become edge template root and will be root rule chain for a newly created edges.", - "invalid-rulechain-type-error": "Unable to import rule chain: Invalid rule chain type. Expected type is {{expectedRuleChainType}}.", - "set-auto-assign-to-edge": "Assign rule chain to edge(s) on creation", - "set-auto-assign-to-edge-title": "Are you sure you want to assign the edge rule chain '{{ruleChainName}}' to edge(s) on creation?", - "set-auto-assign-to-edge-text": "After the confirmation the edge rule chain will be automatically assigned to edge(s) on creation.", - "unset-auto-assign-to-edge": "Do not assign rule chain to edge(s) on creation", - "unset-auto-assign-to-edge-title": "Are you sure you do not want to assign the edge rule chain '{{ruleChainName}}' to edge(s) on creation?", - "unset-auto-assign-to-edge-text": "After the confirmation the edge rule chain will no longer be automatically assigned to edge(s) on creation.", - "unassign-rulechain-title": "Are you sure you want to unassign the rulechain '{{ruleChainName}}'?", - "unassign-rulechains": "Unassign rulechains" - }, - "rulenode": { - "rule-node-events": "Rule node events", - "details": "Details", - "events": "Events", - "search": "Search nodes", - "open-node-library": "Open node library", - "close-node-library": "Close node library", - "add": "Add rule node", - "name": "Name", - "name-required": "Name is required.", - "name-max-length": "Name should be less than 256", - "type": "Type", - "rule-node-description": "Rule node description", - "delete": "Delete rule node", - "select-all-objects": "Select all nodes and connections", - "deselect-all-objects": "Deselect all nodes and connections", - "delete-selected-objects": "Delete selected nodes and connections", - "delete-selected": "Delete selected", - "create-nested-rulechain": "Create nested rule chain", - "select-all": "Select all", - "copy-selected": "Copy selected", - "deselect-all": "Deselect all", - "rulenode-details": "Rule node details", - "debug-mode": "Debug mode", - "configuration": "Configuration", - "link": "Link", - "link-details": "Rule node link details", - "add-link": "Add link", - "link-label": "Link label", - "link-label-required": "Link label is required.", - "custom-link-label": "Custom link label", - "custom-link-label-required": "Custom link label is required.", - "link-labels": "Link labels", - "link-labels-required": "Link labels is required.", - "no-link-labels-found": "No link labels found", - "no-link-label-matching": "'{{label}}' not found.", - "create-new-link-label": "Create a new one!", - "type-filter": "Filter", - "type-filter-details": "Filter incoming messages with configured conditions", - "type-enrichment": "Enrichment", - "type-enrichment-details": "Add additional information into Message Metadata", - "type-transformation": "Transformation", - "type-transformation-details": "Change Message payload and Metadata", - "type-action": "Action", - "type-action-details": "Perform special action", - "type-analytics": "Analytics", - "type-analytics-details": "Perform analysis of streamed or persisted data", - "type-external": "External", - "type-external-details": "Interacts with external system", - "type-rule-chain": "Rule Chain", - "type-rule-chain-details": "Forwards incoming messages to specified Rule Chain", - "type-flow": "Flow", - "type-flow-details": "Organizes message flow", - "type-input": "Input", - "type-input-details": "Logical input of Rule Chain, forwards incoming messages to next related Rule Node", - "type-unknown": "Unknown", - "type-unknown-details": "Unresolved Rule Node", - "directive-is-not-loaded": "Defined configuration directive '{{directiveName}}' is not available.", - "ui-resources-load-error": "Failed to load configuration ui resources.", - "invalid-target-rulechain": "Unable to resolve target rule chain!", - "test-script-function": "Test script function", - "script-lang-java-script": "Java Script", - "script-lang-tbel": "TBEL", - "message": "Message", - "message-type": "Message type", - "select-message-type": "Select message type", - "message-type-required": "Message type is required", - "metadata": "Metadata", - "metadata-required": "Metadata entries can't be empty.", - "output": "Output", - "test": "Test", - "help": "Help", - "test-with-this-message": "{{test}} with this message" - }, - "role": { - "role": "Rolė", - "roles": "Rolės", - "management": "Rolių valdymas", - "view-roles": "Peržiūrėti roles", - "no-roles-matching": "Rolių, atitinkančių '{{entity}}' nėra.", - "role-list": "Rolių sąrašas", - "role-list-required": "Rolių sąrašas būtinas", - "add": "Pridėti rolę", - "view": "Peržiūrėti rolę", - "search": "Rolių paieška", - "selected-roles": "Pasirinkta { count, plural, =1 {1 rolė} other {# rolės} }", - "no-roles-text": "Rolių nėra", - "role-details": "Informacija apie rolę", - "add-role-text": "Pridėti naują rolę", - "delete": "Panaikinti rolę", - "delete-roles": "Panaikinti roles", - "delete-role-title": "Ar tikrai norite panaikinti '{{roleName}}' rolę?", - "delete-role-text": "Būkite dėmesingi, po patvirtinimo, rolė ir visa su ja susijusi informacija bus pašalinta.", - "delete-roles-title": "Ar tikrai norite panaikinti { count, plural, =1 {1 rolę} other {# roles} }?", - "delete-roles-action-title": "Panaikinti { count, plural, =1 {1 rolę} other {# roles} }", - "delete-roles-text": "Būkite dėmesingi, po patvirtinimo, visos pasirinktos rolės ir su jomis susijusi informacija bus pašalintos.", - "role-type": "Tipas", - "role-type-required": "Rolės tipas būtinas.", - "select-role-type": "Pasirinkite rolės tipą", - "enter-role-type": "Įveskite rolės tipą", - "any-role": "Bet kuri rolė", - "no-role-types-matching": "Rolių tipų, atitinkančių '{{entitySubtype}}' nėra.", - "role-type-list-empty": "Nepasirinktas rolės tipas.", - "role-types": "Rolių tipais", - "created-time": "Sukųrimo laikas", - "name": "Pavadinimas", - "name-required": "Pavadinimas būtinas.", - "name-max-length": "Pavadinimas negali viršyti 256 simbolių", - "description": "Aprašymas", - "events": "Įvykiai", - "details": "Informacija", - "copyId": "Kopijuoti rolės Id", - "idCopiedMessage": "Rolės Id nukopijuotas į iškarpinę", - "permissions": "Leidimai", - "role-required": "Rolė būtina", - "roles-required": "Rolės būtinos", - "display-type": { - "GENERIC": "Bendroji", - "GROUP": "Grupinė" - } + "count": { + "layout": "Išdėstymas", + "layout-column": "Stulpelis", + "layout-row": "Eilutė", + "label": "Etiketė", + "icon": "Piktograma", + "icon-background": "Piktogramos fonas", + "value": "Reikšmė", + "chevron": "Rodyklė", + "auto-scale": "Automatinis mastelis" }, - "group-permission": { - "user-group-roles": "Vartotojų grupės rolės", - "entity-group-permissions": "Subjektų grupės leidimai", - "role-type": "Rolės tipas", - "role-name": "Rolės pavadinimas", - "group-type": "Grupės tipas", - "group-name": "Grupės pavadinimas", - "group-owner": "Grupės savininkas", - "user-group-name": "Vartotojų grupės pavadinimas", - "user-group-owner": "Vartotojų grupės savininkas", - "edit": "Redaguoti leidimus", - "delete": "Naikinti leidimus", - "selected-group-permissions": "Pasirinkta { count, plural, =1 {1 grupinis leidimas} other {# grupiniai leidimai} }", - "delete-group-permission-title": "Ar tikrai norite panaikinti '{{roleName}}' grupinį leidimą?", - "delete-group-permission-text": "Būkite dėmesingi, po patvirtinimo, grupinis leidimas ir visa su juo sisijusi informacija bus pašalinta.", - "delete-group-permission": "Panaikinti grupinį leidima", - "delete-group-permissions-title": "Ar tikrai norite panaikinti { count, plural, =1 {1 grupinį leidimą} other {# grupinius leidimus} }?", - "delete-group-permissions-text": "Būkite dėmesingi, po patvirtinimo, pasirinkti grupiniai leidimai ir visa su jais susijusi informacija bus panaikinta ir atitinkami vartotojai neteks prieigos prie tam tikrų resursų.", - "delete-group-permissions": "Panaikinti grupinius leidimus", - "add-group-permission": "Pridėti grupinį leidimą", - "edit-group-permission": "Redaguoti grupinį leidimą", - "entity-group": "Subjektų grupė", - "user-group": "Vartotojų grupė", - "no-owners-matching": "Savininkų, atitinkančių '{{owner}}' nėra.", - "target-owner-required": "Subjektų grupės savininkas būtinas.", - "target-user-group-owner-required": "Vartotojų grupės savininkas būtinas.", - "no-group-permissions-text": "Grupinių leidimų nėra" - }, - "permission": { - "permissions-required": "Turi būti nurodytas nors vienas leidimas.", - "remove-permission": "Panaikinti leidimą", - "add-permission": "Pridėti leidimą", - "other": "Kita", - "resource": { - "resource": "Resursas", - "select-resource": "Pasirinkite resursą", - "resource-required": "Resursas būtinas", - "no-resources-matching": "Resursų, atitinkančių '{{resource}}' nėra.", - "display-type": { - "ALL": "Visi", - "PROFILE": "Profilis", - "ADMIN_SETTINGS": "Administratoriaus nustatymai", - "ALARM": "Įspėjimai", - "DEVICE": "Įrenginys", - "DEVICE_PROFILE": "Įrenginio profilis", - "ASSET": "Turtas", - "CUSTOMER": "Klientas", - "DASHBOARD": "Skydelis", - "ENTITY_VIEW": "Subjekto rodinys", - "EDGE": "Edge", - "TENANT": "Valdytojas", - "TENANT_PROFILE": "Valdytojo profilis", - "RULE_CHAIN": "Taisyklių grandinė", - "USER": "Vartotojas", - "WIDGETS_BUNDLE": "Valdiklių rinkinys", - "WIDGET_TYPE": "Valdiklio tipas", - "CONVERTER": "Keitiklis", - "INTEGRATION": "Integracija", - "SCHEDULER_EVENT": "Tvarkaraščio įvykis", - "BLOB_ENTITY": "Blob subjektas", - "CUSTOMER_GROUP": "Klientų grupė", - "DEVICE_GROUP": "Įrenginių grupė", - "ASSET_GROUP": "Turto grupė", - "USER_GROUP": "Vartotojų grupė", - "ENTITY_VIEW_GROUP": "Subjektų rodinių grupė", - "DASHBOARD_GROUP": "Skydelių grupė", - "ROLE": "Rolė", - "GROUP_PERMISSION": "Grupinis leidimas", - "WHITE_LABELING": "Tinkinimas", - "AUDIT_LOG": "Audito žurnalas", - "API_USAGE_STATE": "API Usage State", - "TB_RESOURCE": "Resource", - "EDGE_GROUP": "Edge Group", - "OTA_PACKAGE": "Ota package", - "QUEUE": "Queue", - "VERSION_CONTROL": "Version control", - "ASSET_PROFILE": "Asset Profile", - "NOTIFICATION": "Notification" - } - }, - "operation": { - "operation": "Operacija", - "operations": "Operacijos", - "operations-required": "Turi būti nurodyta nors viena operacija.", - "enter-operation": "Įveskite operaciją", - "no-operations-matching": "Operacijų, atitinkančių '{{operation}}' nėra.", - "display-type": { - "ALL": "Visi", - "CREATE": "Sukurti", - "READ": "Skaityti", - "WRITE": "Rašyti", - "DELETE": "Panaikinti", - "ASSIGN_TO_CUSTOMER": "Priskirti klientui", - "UNASSIGN_FROM_CUSTOMER": "Atsieti nuo kliento", - "RPC_CALL": "RPC kvietimas", - "READ_CREDENTIALS": "Skaityti įgaliojimus", - "WRITE_CREDENTIALS": "Rašyti įgaliojimus", - "READ_ATTRIBUTES": "Skaityti atributus", - "WRITE_ATTRIBUTES": "Rašyti atributus", - "READ_TELEMETRY": "Skaityti telemetriją", - "WRITE_TELEMETRY": "Rašyti telemetriją", - "CLAIM_DEVICES": "Patvirtinti įrenginius", - "IMPERSONATE": "Įeiti kaip vartotojui", - "CHANGE_OWNER": "Pakeisti savininką", - "ADD_TO_GROUP": "Įtraukti į grupę", - "REMOVE_FROM_GROUP": "Pašalinti iš grupės", - "SHARE_GROUP": "Bendrinti grupę", - "ASSIGN_TO_TENANT": "Priskirti valdytojui" - } - } + "table": { + "common-table-settings": "Bendrieji lentelės nustatymai", + "enable-search": "Įjungti paiešką", + "enable-sticky-header": "Visada rodyti antraštę", + "enable-sticky-action": "Visada rodyti veiksmų stulpelį", + "hidden-cell-button-display-mode": "Paslėpto mygtuko veiksmų rodymo režimas", + "show-empty-space-hidden-action": "Rodyti tuščią vietą vietoje paslėpto langelio mygtuko veiksmo", + "dont-reserve-space-hidden-action": "Nerezervuoti vietos paslėptiems veiksmų mygtukams", + "display-timestamp": "Rodyti laiko žymos stulpelį", + "display-pagination": "Rodyti puslapiavimą", + "default-page-size": "Numatytasis puslapio dydis", + "page-step-settings": "Puslapio žingsnių nustatymai", + "page-step-count": "Žingsnių skaičius", + "page-step-increment": "Žingsnio didinimas", + "page-step-count-format-message": "Turi būti sveikasis skaičius nuo 1 iki 100.", + "page-step-increment-format-message": "Turi būti sveikasis skaičius, ne mažesnis kaip 1.", + "use-entity-label-tab-name": "Naudoti objekto etiketę skirtuko pavadinime", + "hide-empty-lines": "Slėpti tuščias eilutes", + "row-style": "Eilutės stilius", + "use-row-style-function": "Naudoti eilutės stiliaus funkciją", + "row-style-function": "Eilutės stiliaus funkcija", + "cell-style": "Langelio stilius", + "use-cell-style-function": "Naudoti langelio stiliaus funkciją", + "cell-style-function": "Langelio stiliaus funkcija", + "cell-content": "Langelio turinys", + "use-cell-content-function": "Naudoti langelio turinio funkciją", + "cell-content-function": "Langelio turinio funkcija", + "show-latest-data-column": "Rodyti naujausių duomenų stulpelį", + "latest-data-column-order": "Naujausių duomenų stulpelio tvarka", + "entities-table-title": "Objektų lentelės pavadinimas", + "enable-select-column-display": "Įjungti pasirinktų stulpelių rodymą", + "display-entity-name": "Rodyti objekto pavadinimo stulpelį", + "entity-name-column-title": "Objekto pavadinimo stulpelio pavadinimas", + "display-entity-label": "Rodyti objekto etiketės stulpelį", + "entity-label-column-title": "Objekto etiketės stulpelio pavadinimas", + "display-entity-type": "Rodyti objekto tipo stulpelį", + "default-sort-order": "Numatytoji rūšiavimo tvarka", + "custom-title": "Tinkintas antraštės pavadinimas", + "column-width": "Stulpelio plotis (px arba %)", + "default-column-visibility": "Numatytasis stulpelio matomumas", + "column-visibility-visible": "Matomas", + "column-visibility-hidden": "Paslėptas", + "column-visibility-hidden-mobile": "Paslėptas mobiliuoju režimu", + "column-selection-to-display": "Stulpelių pasirinkimas rodymui", + "column-selection-to-display-enabled": "Įjungta", + "column-selection-to-display-disabled": "Išjungta", + "alarms-table-title": "Pavojaus signalų lentelės pavadinimas", + "enable-alarms-selection": "Įjungti signalų pasirinkimą", + "enable-alarms-search": "Įjungti signalų paiešką", + "enable-alarm-filter": "Įjungti signalų filtrą", + "display-alarm-details": "Rodyti signalų detales", + "allow-alarms-ack": "Leisti patvirtinti signalus", + "allow-alarms-clear": "Leisti išvalyti signalus", + "display-alarm-activity": "Rodyti signalų veiklą", + "allow-alarms-assign": "Leisti priskirti signalus", + "columns": "Stulpeliai", + "column-settings": "Stulpelių nustatymai", + "remove-column": "Pašalinti stulpelį", + "add-column": "Pridėti stulpelį", + "no-columns": "Stulpeliai nesukonfigūruoti", + "columns-to-display": "Stulpeliai, kuriuos rodyti", + "table-header": "Lentelės antraštė", + "header-buttons": "Antraštės mygtukai", + "table-buttons": "Lentelės mygtukai", + "pagination": "Puslapiavimas", + "rows": "Eilutės", + "timeseries-column-error": "Turi būti nurodytas bent vienas laiko sekos stulpelis", + "alarm-column-error": "Turi būti nurodytas bent vienas signalų stulpelis", + "table-tabs": "Lentelės skirtukai", + "show-cell-actions-menu-mobile": "Rodyti langelio veiksmų meniu mobiliajame režime", + "disable-sorting": "Išjungti rūšiavimą" }, - "scheduler": { - "scheduler": "Tvarkaraštis", - "scheduler-event": "Tvarkaraščio įvykis", - "select-scheduler-event": "Pasirinkite tvarkaraščio įvykį", - "no-scheduler-events-matching": "Tvarkaraščio įvykių, atitinkančių '{{entity}}' nėra.", - "scheduler-event-required": "Tvarkaraščio įvykis būtinas", - "management": "Tvarkaraščio valdymas", - "scheduler-events": "Tvarkaraščio įvykiai", - "add-scheduler-event": "Pridėti tvarkaraščio įvykį", - "search-scheduler-events": "Tvarkaraščio įvykių paieška", - "created-time": "Sukūrimo laikas", - "name": "Pavadinimas", - "name-required": "Pavadinimas būtinas", - "name-max-length": "Pavadinimas negali viršyti 256 simbolių", - "type": "Tipas", - "created_customer": "Sukūrė klientas", - "edit-scheduler-event": "Redaguoti tvarkaraščio įvykį", - "view-scheduler-event": "Peržiūrėti tvarkaraščio įvykį", - "delete-scheduler-event": "Panaikinti tvarkaraščio įvykį", - "no-scheduler-events": "Tvarkaraščio įvykių nėra", - "selected-scheduler-events": "Pasirinkta { count, plural, =1 {1 tvarkaraščio įvykis} other {# tvarkaraščio įvykiai} }", - "delete-scheduler-event-title": "Ar tikrai norite panaikinti tvarkaraščio įvykį '{{schedulerEventName}}'?", - "delete-scheduler-event-text": "`", - "delete-scheduler-events-title": "Ar tikrai norite panaikinti { count, plural, =1 {1 tvarkaraščio įvykį} other {# tvarkaraščio įvykius} }?", - "delete-scheduler-events-text": "Būkite dėmesingi, po patvirtinimo, pasirinkti tvarkaraščio įvykiai ir visa su jais susijusi informacija bus panaikinta.", - "create": "Sukurti tvarkaraščio įvykį", - "edit": "Redaguoti tvarkaraščio įvykį", - "view": "Peržiūrėti tvarkaraščio įvykį", - "configuration": "Konfigūracija", - "schedule": "Tvarkaraštis", - "start-time": "Pradžios laikas", - "repeat": "Pakartoti", - "repeats": "Pakartojimai", - "daily": "Kasdieną", - "every-n-days": "Kas N dienas", - "every-n-days-text": "Every { days, plural, =1 {day} other {# days} }", - "weekly": "Kas savaitę", - "every-n-weeks": "Kas N savaites", - "every-n-weeks-text": "Every { weeks, plural, =1 {week} other {# weeks} }", - "monthly": "Kas mėnesį", - "yearly": "Kas metus", - "timer": "Nurodytu laiku", - "repeats-required": "Pakartojimai būtini.", - "repeat-on": "Kartoti", - "repeat-every": "Kartoti kiekvieną", - "repeat-every-n-days": "Kartoti kas N dienas", - "repeat-days-required": "Pakartojim dienų skaičius būtinas.", - "invalid-repeat-days-value": "Pakartojimo dienų skaičius turi būti sveikas teigiamas skaičius.", - "repeat-every-n-weeks": "Kartoti kas N savaites", - "repeat-weeks-required": "Pakartojimo savaičių skaičius būtinas.", - "invalid-repeat-weeks-value": "Pakartojimo savaičių skaičius turi būti sveikas teigiamas skaičius.", - "ends-on": "Pabaiga", - "sunday-label": "Sk", - "monday-label": "Pr", - "tuesday-label": "An", - "wednesday-label": "Tr", - "thursday-label": "Kt", - "friday-label": "Pn", - "saturday-label": "Št", - "repeat-on-sunday": "Kartoti sekmadieniais", - "repeat-on-monday": "Kartoti pirmadieniais", - "repeat-on-tuesday": "Kartoti antradieniais", - "repeat-on-wednesday": "Kartoti trečiadieniais", - "repeat-on-thursday": "Kartoti ketvirtadieniais", - "repeat-on-friday": "Kartoti penktadieniais", - "repeat-on-saturday": "Kartoti šeštadieniais", - "event-type": "Įvykio tipas", - "select-event-type": "Pasirinkite įvykio tiipą", - "event-type-required": "Įvykio tipas būtinas.", - "event-type-max-length": "Įvykio tipas negali viršyti 256 simbolių.", - "list-mode": "Sąrašas", - "calendar-mode": "Kalendorius", - "calendar-view-type": "Kalendoriaus peržiūros tipas", - "month": "Mėnuo", - "week": "Savaitė", - "day": "Diena", - "agenda-week": "Savaitinis tvarkaraštis", - "agenda-day": "Dienų tvarkaraštis", - "list-year": "Sąrašas metams", - "list-month": "Sąrašas mėnesiui", - "list-week": "Sąrašas savaitei", - "list-day": "Sąrašas dienai", - "today": "Šiandien", - "navigate-before": "Ankstesnis", - "navigate-next": "Sekantis", - "starting-from": "Pradėti nuo", - "until": "iki", - "on": "į", - "sunday": "Sekmadienis", - "monday": "Pirmadienis", - "tuesday": "Antradienis", - "wednesday": "Trečiadienis", - "thursday": "Ketvirtadienis", - "friday": "Penktadienis", - "saturday": "Šesštadienis", - "originator": "Iniciatorius", - "single-entity": "Vienas subjektas", - "group-of-entities": "Subjektų grupė", - "entities-group-owner": "Subjektų grupės savininkas", - "single-device": "Vienas įrenginys", - "group-of-devices": "Įrenginių grupė", - "devices-group-owner": "Įrenginių grupės savininkas", - "message-body": "Pranešimo tekstas", - "target": "Tikslas", - "rpc-method": "Method", - "rpc-method-required": "Method is required", - "rpc-method-white-space": "White space is not allowed.", - "rpc-params": "Params", - "select-dashboard-state": "Pasirinkite skydelio būseną", - "hours": "Valandos", - "minutes": "Minutės", - "seconds": "Sekundės", - "time-interval-required": "Laiko intervalas būtinas", - "time-unit-required": "Laiko vienetas būtinas", - "every-hour": "kas { count, plural, =1 {valandą} other {# valandas} }", - "every-minute": "kas { count, plural, =1 {minutę} other {# minutes} }", - "every-second": "kas { count, plural, =1 {sekundę} other {# sekundes} }", - "invalid-time": "Neteisingas laikas" - }, - "report": { - "report-config": "Ataskaitos konfigūracija", - "email-config": "Elektroninio pašto konfigūracija", - "dashboard-state-param": "Skydelio būsenos parametro reikšmė", - "base-url": "Pagrindinis URL", - "base-url-required": "Pagrindinis URL būtinas.", - "use-dashboard-timewindow": "Naudoti skydelio laiko langą", - "timewindow": "Laiko langas", - "name-pattern": "Ataskaitos pavadinimo šablonas", - "name-pattern-required": "Ataskaitos pavadinimo šablonas būtinas", - "type": "Ataskaitos tipas", - "use-current-user-credentials": "Naudoti dabartinio vartotojo įgaliojimus", - "customer-user-credentials": "Kliento vartotojo įgaliojimai", - "customer-user-credentials-required": "Kliento vartotojo įgaliojimai būtini", - "generate-test-report": "Generuoti bandomąją ataskaitą", - "send-email": "Siųsti laišką", + "latest-chart": { + "total": "Bendra suma", + "auto-scale": "Automatinis mastelis", + "clockwise-layout": "Išdėstymas pagal laikrodžio rodyklę", + "sort-series": "Rūšiuoti serijas pagal etiketę", + "tooltip-value-type-absolute": "Absoliuti reikšmė", + "tooltip-value-type-percentage": "Procentinė reikšmė" + }, + "pie-chart": { + "pie-chart-appearance": "Skritulinės diagramos išvaizda", + "label": "Etiketė", + "border": "Kraštinė", + "radius": "Spindulys", + "pie-chart-card-style": "Skritulinės diagramos kortelės stilius" + }, + "radar-chart": { + "radar-appearance": "Radaro diagramos išvaizda", + "shape": "Forma", + "shape-polygon": "Daugiakampis", + "shape-circle": "Apskritimas", + "color": "Spalva", + "line": "Linija", + "points": "Taškai", + "points-label": "Taškų etiketė", + "radar-axis": "Radaro ašis", + "axis-label": "Ašies etiketė", + "ticks-label": "Padalų etiketė", + "radar-chart-style": "Radaro diagramos stilius", + "max-axes-scaling": "Maksimalus ašių mastelis", + "max-axes-scaling-hint": "Pasirinkite, ar kiekviena radaro ašis turi savo maksimalų dydį (Atskiras), ar dalijasi didžiausia reikšme tarp visų ašių pagal valdiklio duomenis (Bendras).", + "separate": "Atskiras", + "common": "Bendras" + }, + "time-series-chart": { + "chart": "Diagrama", + "chart-style": "Diagramos stilius", + "data-zoom": "Duomenų mastelio keitimas", + "stack-mode": "Sluoksniavimo režimas", + "stack-mode-hint": "Sluoksniuoja serijas diagramoje. Serijos su tomis pačiomis vienetais bus dedamos viena ant kitos.", + "axes": "Ašys", + "y-axes": "Y ašys", + "line-type": "Linijos tipas", + "line-width": "Linijos storis", + "type-line": "Linija", + "type-bar": "Stulpelis", + "type-point": "Taškas", + "no-aggregation-bar-width-strategy": "Stulpelių pločio strategija neagreguotiems duomenims", + "no-aggregation-bar-width-strategy-group": "Grupuoti", + "no-aggregation-bar-width-strategy-separate": "Atskiri", + "bar-group-width": "Stulpelių grupės plotis", + "bar-width": "Stulpelio plotis", + "bar-width-relative": "Procentinė laiko lango dalis", + "bar-width-absolute": "Absoliutus (ms)", + "comparison": { + "comparison": "Palyginimas", + "comparison-hint": "Palyginimas veikia tik su istorinių duomenų!", + "show": "Rodyti", + "settings": "Palyginimo nustatymai", + "show-values-for-comparison": "Rodyti istorinius duomenis palyginimui", + "comparison-values-label": "Palyginimo rakto etiketė", + "comparison-values-label-auto": "Automatinė", + "comparison-data-color": "Palyginimo duomenų spalva" + }, + "threshold": { + "thresholds": "Slenksčiai", + "source": "Šaltinis", + "key-value": "Raktas / Reikšmė", + "no-thresholds": "Slenksčiai nesukonfigūruoti", + "add-threshold": "Pridėti slenkstį", + "type-constant": "Pastovus", + "type-latest-key": "Raktas", + "type-entity": "Objektas", + "threshold-settings": "Slenksčio nustatymai", + "remove-threshold": "Pašalinti slenkstį", + "threshold-value-required": "Reikalinga slenksčio reikšmė.", + "key-required": "Reikalingas raktas.", + "entity-key-required": "Reikalingas objekto raktas.", + "line-appearance": "Linijos išvaizda", + "line-color": "Linijos spalva", + "start-symbol": "Pradžios simbolis", + "end-symbol": "Pabaigos simbolis", + "symbol-size": "Dydis", + "label": "Etiketė", + "label-position-start": "Pradžia", + "label-position-middle": "Vidurys", + "label-position-end": "Pabaiga", + "label-position-inside-start": "Viduje pradžioje", + "label-position-inside-start-top": "Viduje viršuje pradžioje", + "label-position-inside-start-bottom": "Viduje apačioje pradžioje", + "label-position-inside-middle": "Viduje per vidurį", + "label-position-inside-middle-top": "Viduje per vidurį viršuje", + "label-position-inside-middle-bottom": "Viduje per vidurį apačioje", + "label-position-inside-end": "Viduje pabaigoje", + "label-position-inside-end-top": "Viduje pabaigoje viršuje", + "label-position-inside-end-bottom": "Viduje pabaigoje apačioje", + "label-background": "Etiketės fonas" + }, + "state": { + "states": "Būsenos", + "label": "Etiketė", + "ticks-value": "Padalų reikšmė", + "source": "Šaltinis", + "value-range": "Reikšmė / Diapazonas", + "no-states": "Būsenos nesukonfigūruotos", + "add-state": "Pridėti būseną", + "type-constant": "Pastovi", + "type-range": "Diapazonas", "from": "Nuo", - "from-required": "Būtina nurodyti nuo ko siunčiamas laiškas.", - "to": "Kam", - "to-required": "Būtina nurodyti kam siunčiamas laiškas.", - "cc": "Cc", - "bcc": "Bcc", - "subject": "Tema", - "subject-required": "Būtina nurodyti laiško temą.", - "body": "Tekstas", - "body-required": "Būtina įrašyti laiško tekstą." - }, - "blob-entity": { - "blob-entity": "Blob subjektas", - "select-blob-entity": "Pasirinkite blob subjektą", - "no-blob-entities-matching": "Blob subjektų, atitinkančių '{{entity}}' nėra.", - "blob-entity-required": "Blob subjektas būtinas", - "files": "Failai", - "search": "Failų paieška", - "clear-search": "Išvalyti paiešką", - "no-blob-entities-prompt": "Failų nėra", - "report": "Ataskaita", - "created-time": "Sukūrimo laikas", - "name": "Pavadinimas", + "to": "Iki", + "remove-state": "Pašalinti būseną" + }, + "grid": { + "grid": "Tinklelis", + "background-color": "Fono spalva", + "border": "Rėmelis" + }, + "axis": { + "axes": "Ašys", + "x-axis": "X ašis", + "y-axis": "Y ašis", + "y-axis-settings": "Y ašies nustatymai", + "comparison-x-axis-settings": "Palyginimo X ašies nustatymai", + "remove-y-axis": "Pašalinti Y ašį", + "id": "Id", + "label": "Etiketė", + "position": "Pozicija", + "position-left": "Kairėje", + "position-right": "Dešinėje", + "position-top": "Viršuje", + "position-bottom": "Apačioje", + "tick-labels": "Padalų etiketės", + "ticks-formatter-function": "Padalų formatavimo funkcija", + "ticks-generator-function": "Padalų generavimo funkcija", + "show-ticks": "Rodyti padalas", + "show-line": "Rodyti liniją", + "show-split-lines": "Rodyti padalų linijas", + "show-split-lines-x-axis-hint": "Jei įjungta, bus rodomos vertikalios diagramos linijos.", + "show-split-lines-y-axis-hint": "Jei įjungta, bus rodomos horizontalios diagramos linijos.", + "ticks-interval": "Padalų intervalas", + "ticks-interval-hint": "Priverstinai nustato ašies segmentacijos intervalą.", + "split-number": "Padalų skaičius", + "split-number-hint": "Padalų, į kurias suskirstyta ašis, skaičius.", + "min": "Min.", + "max": "Maks.", + "show": "Rodyti", + "add-y-axis": "Pridėti Y ašį" + }, + "series": { + "legend-settings": "Legenda nustatymai", + "show-in-legend": "Rodyti legendoje", + "show-in-legend-hint": "Rodyti serijos pavadinimą ir duomenis legendoje.", + "hidden-by-default": "Numatytai paslėpta", + "hidden-by-default-hint": "Padaryti seriją paslėptą legendoje pagal numatytuosius nustatymus.", + "series-type": "Serijos tipas", "type": "Tipas", - "created_customer": "Skūrė klientas", - "selected-blob-entities": "Pasirinkta{ count, plural, =1 {1 failas} other {# failai} }", - "download-blob-entity": "Atsisiųsti failą", - "delete-blob-entity": "Ištrinti failą", - "delete-blob-entity-title": "Ar tikrai norite ištrinti '{{blobEntityName}}' failą?", - "delete-blob-entity-text": "Būkite dėmesingi, po patvirtinimo failas bus ištrintas.", - "delete-blob-entities-title": "Artikrai norite ištrinti { count, plural, =1 {1 failą} other {# failus} }?", - "delete-blob-entities-text": "Būkite dėmesingi, po patvirtinimo visi pasirinkti failai ir su jais susijusi informacija bus ištrinta." - }, - "timezone": { - "timezone": "Laiko juosta", - "select-timezone": "Pasirinkti laiko juostą", - "no-timezones-matching": "Laiko juostų, atitinkančių '{{timezone}}' nėra.", - "timezone-required": "Laiko juosta būtina.", - "browser-time": "Browser Time" - }, - "queue": { - "queue-name": "Queue", - "no-queues-found": "No queues found.", - "no-queues-matching": "No queues matching '{{queue}}' were found.", - "select-name": "Select queue name", - "name": "Name", - "name-required": "Queue name is required!", - "name-unique": "Queue name is not unique!", - "name-pattern": "Queue name contains a character other than ASCII alphanumerics, '.', '_' and '-'!", - "queue-required": "Queue is required!", - "topic-required": "Queue topic is required!", - "poll-interval-required": "Poll interval is required!", - "poll-interval-min-value": "Poll interval value can't be less then 1", - "partitions-required": "Partitions is required!", - "partitions-min-value": "Partitions value can't be less then 1", - "pack-processing-timeout-required": "Processing timeout is required", - "pack-processing-timeout-min-value": "Processing timeout value can't be less then 1", - "batch-size-required": "Batch size is required!", - "batch-size-min-value": "Batch size value can't be less then 1", - "retries-required": "Retries is required!", - "retries-min-value": "Retries value can't be negative", - "failure-percentage-required": "Failure percentage is required!", - "failure-percentage-min-value": "Failure percentage value can't be less then 0", - "failure-percentage-max-value": "Failure percentage value can't be more then 100", - "pause-between-retries-required": "Pause between retries is required!", - "pause-between-retries-min-value": "Pause between retries value can't be less then 1", - "max-pause-between-retries-required": "Max pause between retries is required!", - "max-pause-between-retries-min-value": "Max pause between retries value can't be less then 1", - "submit-strategy-type-required": "Submit strategy type is required!", - "processing-strategy-type-required": "Processing strategy type is required!", - "queues": "Queues", - "selected-queues": "{ count, plural, =1 {1 queue} other {# queues} } selected", - "delete-queue-title": "Are you sure you want to delete the queue '{{queueName}}'?", - "delete-queues-title": "Are you sure you want to delete { count, plural, =1 {1 queue} other {# queues} }?", - "delete-queue-text": "Be careful, after the confirmation the queue and all related data will become unrecoverable.", - "delete-queues-text": "After the confirmation all selected queues will be deleted and won't be accessible.", - "search": "Search queue", - "add": "Add queue", - "details": "Queue details", - "topic": "Topic", - "submit-settings": "Submit settings", - "submit-strategy": "Strategy type *", - "grouping-parameter": "Grouping parameter", - "processing-settings": "Retries processing settings", - "processing-strategy": "Processing type *", - "retries-settings": "Retries settings", - "polling-settings": "Polling settings", - "batch-processing": "Batch processing", - "poll-interval": "Poll interval", - "partitions": "Partitions", - "immediate-processing": "Immediate processing", - "consumer-per-partition": "Send message poll for each consumer", - "consumer-per-partition-hint": "Enable separate consumer(s) per each partition", - "processing-timeout": "Processing within, ms", - "batch-size": "Batch size", - "retries": "Number of retries (0 – unlimited)", - "failure-percentage": "Percentage of failure messages for skipping retries", - "pause-between-retries": "Retry within, sec", - "max-pause-between-retries": "Additional retry within, sec", - "delete": "Delete queue", - "copyId": "Copy queue Id", - "idCopiedMessage": "Queue Id has been copied to clipboard", - "description": "Description", - "description-hint": "This text will be displayed in the Queue description instead of the selected strategy", - "alt-description": "Submit Strategy: {{submitStrategy}}, Processing Strategy: {{processingStrategy}}", - "custom-properties": "Custom properties", - "custom-properties-hint": "Custom queue (topic) creation properties, e.g. 'retention.ms:604800000;retention.bytes:1048576000'", - "strategies": { - "sequential-by-originator-label": "Sequential by originator", - "sequential-by-originator-hint": "New message for e.g. device A is not submitted until previous message for device A is acknowledged", - "sequential-by-tenant-label": "Sequential by tenant", - "sequential-by-tenant-hint": "New message for e.g tenant A is not submitted until previous message for tenant A is acknowledged", - "sequential-label": "Sequential", - "sequential-hint": "New message is not submitted until previous message is acknowledged", - "burst-label": "Burst", - "burst-hint": "All messages are submitted to the rule chains in the order they arrive", - "batch-label": "Batch", - "batch-hint": "New batch is not submitted until previous batch is acknowledged", - "skip-all-failures-label": "Skip all failures", - "skip-all-failures-hint": "Ignore all failures", - "skip-all-failures-and-timeouts-label": "Skip all failures and timeouts", - "skip-all-failures-and-timeouts-hint": "Ignore all failures and timeouts", - "retry-all-label": "Retry all", - "retry-all-hint": "Retry all messages from processing pack", - "retry-failed-label": "Retry failed", - "retry-failed-hint": "Retry all failed messages from processing pack", - "retry-timeout-label": "Retry timeout", - "retry-timeout-hint": "Retry all timed-out messages from processing pack", - "retry-failed-and-timeout-label": "Retry failed and timeout", - "retry-failed-and-timeout-hint": "Retry all failed and timed-out messages from processing pack" + "type-line": "Linija", + "type-bar": "Stulpelis", + "line": { + "line": "Linija", + "show-line": "Rodyti liniją", + "step-line": "Pakopinė linija", + "step-type-start": "Pradžia", + "step-type-middle": "Vidurys", + "step-type-end": "Pabaiga", + "smooth-line": "Glotni linija" + }, + "point": { + "points": "Taškai", + "show-points": "Rodyti taškus", + "point-label": "Taško etiketė", + "point-label-hint": "Rodyti etiketę su reikšme virš serijos taško.", + "point-label-background": "Taško etiketės fonas", + "point-shape": "Taško forma", + "point-size": "Taško dydis" } + } }, - "server-error": { - "general": "General server error", - "authentication": "Authentication error", - "jwt-token-expired": "JWT token expired", - "tenant-trial-expired": "Tenant trial expired", - "credentials-expired": "Credentials expired", - "permission-denied": "Permission denied", - "invalid-arguments": "Invalid arguments", - "bad-request-params": "Bad request params", - "item-not-found": "Item not found", - "too-many-requests": "Too many requests", - "too-many-updates": "Too many updates" - }, - "tenant": { - "tenant": "Valdytojas", - "tenants": "Valdytojai", - "management": "Valdytojų valdymas", - "add": "Pridėti valdytoją", - "admins": "Administratoriai", - "manage-tenant-admins": "Valdyti valdytojo administratorius", - "delete": "Ištrinti valdytoją", - "add-tenant-text": "Pridėti naują valdytoją", - "no-tenants-text": "Valdytojų nėra", - "tenant-details": "Informacija apie valdytoją", - "title-max-length": "Pavadinimas negali viršyti 256 simbolių", - "delete-tenant-title": "Ar tikrai norite ištrinti valdytoją '{{tenantTitle}}'?", - "delete-tenant-text": "Būkite dėmesingi, po patvirtinimo valdytojas ir visa su juo susijusi informacija bus pašalinta.", - "delete-tenants-title": "Ar tikrai norite pašalinti { count, plural, =1 {1 valdytoją} other {# valdytojus} }?", - "delete-tenants-action-title": "Pašalinti { count, plural, =1 {1 valdytoją} other {# valdytojus} }", - "delete-tenants-text": "Būkite dėmesingi, po patvirtinimo, visi pasirinkti valdytojai ir su jais susijusi informacija bus pašalinta.", - "title": "Pavadinimas", - "title-required": "Pavadinimas būtinas.", - "description": "Aprašymas", - "details": "Informacija", - "events": "Įvykiai", - "copyId": "Kopijuoti valdytojo Id", - "idCopiedMessage": "Valdytojo Id nukopijuotas į iškarpinę", - "select-tenant": "Pasirinkti valdytoją", - "no-tenants-matching": "Valdytojų, atitinkančių '{{entity}}' nėra.", - "tenant-required": "Valdytojas būtinas", - "allow-white-labeling": "Leisti personalizavimą", - "allow-customer-white-labeling": "Leisti kliento personalizavimą", - "search": "Valdytojų paieška", - "selected-tenants": "Pasirinkta { count, plural, =1 {1 valdytojas} other {# valdutojai} }", - "isolated-tb-rule-engine": "Use isolated ThingsBoard Rule Engine queues", - "isolated-tb-rule-engine-details": "Each tenant will have dedicated Rule Engine queues" - }, - "tenant-profile": { - "tenant-profile": "Valdytojų profilis", - "tenant-profiles": "Valdytojų profiliai", - "add": "Pridėti valdytojų profilį", - "add-profile": "Pridėti profilį", - "edit": "Redaguoti valdytojų profilį", - "tenant-profile-details": "Informacija apie valdytojų profilį", - "no-tenant-profiles-text": "Valdytojų profilių nėra", - "name-max-length": "Pavadinimas negali viršyti 256 simbolių", - "search": "Valdytojų profilių paieška", - "selected-tenant-profiles": "Pasirinkta { count, plural, =1 {1 valdytojų profilis} other {# valdytojų profiliai} }", - "no-tenant-profiles-matching": "Valdytojų profilių, atitinkančių '{{entity}}' nėra.", - "tenant-profile-required": "Valdytojų profilis būtinas", - "idCopiedMessage": "Valdytojų profilio Id nukopijuotas į iškarpinę", - "set-default": "Valdytojų profilį nustatyti kaip pagrndinį", - "delete": "Panaikinti valdytojų profilį", - "copyId": "Kopijuoti valdytojų profilio Id", - "name": "Pavadinimas", - "name-required": "Pavadinimas būtinas.", - "data": "Profilio duomenys", - "profile-configuration": "Profilio konfigūracija", - "description": "Aprašymas", - "default": "Pagrindinis", - "delete-tenant-profile-title": "Ar tikrai norite panaikinti valdytojų profilį '{{tenantProfileName}}'?", - "delete-tenant-profile-text": "Būkite dėmesingi, po patvirtinimo, valdytojų profilis ir visa su juo susijusi informacija bus pašalinta.", - "delete-tenant-profiles-title": "Ar tikrai norite pašalinti { count, plural, =1 {1 valdytojų profilį} other {# valdytojų profilius} }?", - "delete-tenant-profiles-text": "Būkite dėmesingi, po patvirtinimo, visi pasirinkti valdytojų profiliai ir su jais susijusi informacija bus pašalinta.", - "set-default-tenant-profile-title": "Ar tikrai norite valdytojų profilį '{{tenantProfileName}}' nustatyti kaip pagrindinį?", - "set-default-tenant-profile-text": "Po patvirtinimo, valdytojų profilis bus nustatytas kaip pagrinfinis ir bus priskiriamas naujiems valdytojams, kuriems valdytojų profilis nenurodytas.", - "no-tenant-profiles-found": "Valdytojų profilių nėra.", - "create-new-tenant-profile": "Sukurti naują!", - "create-tenant-profile": "Sukuri naują valdytojo profilį", - "import": "Importuoti valdytojo profilį", - "export": "Eksportuoti valdytojo profilį", - "export-failed-error": "Valdytojo profilio importuoti nepavyko: {{error}}", - "tenant-profile-file": "Valdytojo profilio failas", - "invalid-tenant-profile-file-error": "Valdytojo profilio importuoti nepavyko: Neteisinga valdytojo profilio duomenų struktūra.", - "advanced-settings": "Pažangūs nustatymai", - "entities": "Subjektai", - "rule-engine": "Rule Engine", - "time-to-live": "Time-to-live", - "alarms-and-notifications": "Alarms and notifications", - "ota-files-in-bytes": "OTA files in bytes", - "ws-title": "WS", - "unlimited": "(0 - unlimited)", - "maximum-devices": "Devices maximum number", - "maximum-devices-required": "Devices maximum number is required.", - "maximum-devices-range": "Devices maximum number can't be negative", - "maximum-assets": "Assets maximum number", - "maximum-assets-required": "Assets maximum number is required.", - "maximum-assets-range": "Assets maximum number can't be negative", - "maximum-customers": "Customers maximum number", - "maximum-customers-required": "Customers maximum number is required.", - "maximum-customers-range": "Customers maximum number can't be negative", - "maximum-users": "Users maximum number", - "maximum-users-required": "Users maximum number is required.", - "maximum-users-range": "Users maximum number can't be negative", - "maximum-dashboards": "Dashboards maximum number", - "maximum-dashboards-required": "Dashboards maximum number is required.", - "maximum-dashboards-range": "Dashboards maximum number can't be negative", - "maximum-edges": "Edges maximum number", - "maximum-edges-required": "Edges maximum number is required.", - "maximum-edges-range": "Edges maximum number can't be negative", - "maximum-rule-chains": "Rule chains maximum number", - "maximum-rule-chains-required": "Rule chains maximum number is required.", - "maximum-rule-chains-range": "Rule chains maximum number can't be negative", - "maximum-integrations": "Integrations maximum number", - "maximum-integrations-required": "Integrations maximum number is required.", - "maximum-integrations-range": "Integrations maximum number can't be negative", - "maximum-converters": "Converters maximum number", - "maximum-converters-required": "Converters maximum number is required.", - "maximum-converters-range": "Converters maximum number can't be negative", - "maximum-scheduler-events": "Scheduler events maximum number", - "maximum-scheduler-events-required": "Scheduler events maximum number is required.", - "maximum-scheduler-events-range": "Scheduler events maximum number can't be negative", - "maximum-resources-sum-data-size": "Resource files sum size", - "maximum-resources-sum-data-size-required": "Resource files sum size is required.", - "maximum-resources-sum-data-size-range": "Resource files sum size can`t be negative", - "maximum-ota-packages-sum-data-size": "OTA package files sum size", - "maximum-ota-package-sum-data-size-required": "OTA package files sum size is required.", - "maximum-ota-package-sum-data-size-range": "OTA package files sum size can`t be negative", - "rest-requests-for-tenant": "REST requests for tenant", - "transport-tenant-telemetry-msg-rate-limit": "Transport tenant telemetry messages", - "transport-tenant-telemetry-data-points-rate-limit": "Transport tenant telemetry data points", - "transport-device-msg-rate-limit": "Transport device messages", - "transport-device-telemetry-msg-rate-limit": "Transport device telemetry messages", - "transport-device-telemetry-data-points-rate-limit": "Transport device telemetry data points", - "tenant-entity-export-rate-limit": "Entity version creation", - "tenant-entity-import-rate-limit": "Entity version load", - "tenant-notification-request-rate-limit": "Notification requests", - "tenant-notification-requests-per-rule-rate-limit": "Notification requests per notification rule", - "integration-msgs-per-tenant-rate-limit": "Tenant integration messages", - "integration-msgs-per-device-rate-limit": "Device integration messages", - "max-transport-messages": "Transport messages maximum number", - "max-transport-messages-required": "Transport messages maximum number is required.", - "max-transport-messages-range": "Transport messages maximum number can't be negative", - "max-transport-data-points": "Transport data points maximum number ", - "max-transport-data-points-required": "Transport data points maximum number is required.", - "max-transport-data-points-range": "Transport data points maximum number can't be negative", - "max-r-e-executions": "Rule Engine executions maximum number", - "max-r-e-executions-required": "Rule Engine executions maximum number is required.", - "max-r-e-executions-range": "Rule Engine executions maximum number can't be negative", - "max-j-s-executions": "JavaScript executions maximum number ", - "max-j-s-executions-required": "JavaScript executions maximum number is required.", - "max-j-s-executions-range": "JavaScript executions maximum number can't be negative", - "max-d-p-storage-days": "Data points storage days maximum number", - "max-d-p-storage-days-required": "Data points storage days maximum number is required.", - "max-d-p-storage-days-range": "Data points storage days maximum number can't be negative", - "default-storage-ttl-days": "Storage TTL days by default", - "default-storage-ttl-days-required": "Storage TTL days by default is required.", - "default-storage-ttl-days-range": "Storage TTL days by default can't be negative", - "alarms-ttl-days": "Alarms TTL days", - "alarms-ttl-days-required": "Alarms TTL days required", - "alarms-ttl-days-days-range": "Alarms TTL days can't be negative", - "rpc-ttl-days": "RPC TTL days", - "rpc-ttl-days-required": "RPC TTL days required", - "rpc-ttl-days-days-range": "RPC TTL days can't be negative", - "max-rule-node-executions-per-message": "Rule node per message executions maximum number", - "max-rule-node-executions-per-message-required": "MRule node per message executions maximum number is required.", - "max-rule-node-executions-per-message-range": "Rule node per message executions maximum number can't be negative", - "max-emails": "Emails sent maximum number", - "max-emails-required": "Emails sent maximum number is required.", - "max-emails-range": "Emails sent maximum number can't be negative", - "sms-enabled": "SMS enabled", - "max-sms": "SMS sent maximum number", - "max-sms-required": "SMS sent maximum number is required.", - "max-sms-range": "SMS sent maximum number can't be negative", - "max-created-alarms": "Alarms created maximum number", - "max-created-alarms-required": "Alarms created maximum number is required.", - "max-created-alarms-range": "Alarms created maximum number be negative", - "no-queue": "No Queue configured", - "add-queue": "Add Queue", - "queues-with-count": "Queues ({{count}})", - "tenant-rest-limits": "REST requests for tenant", - "customer-rest-limits": "REST requests for customer", - "incorrect-pattern-for-rate-limits": "The format is comma separated pairs of capacity and period (in seconds) with a colon between, e.g. 100:1,2000:60", - "too-small-value-zero": "The value must be bigger than 0", - "too-small-value-one": "The value must be bigger than 1", - "queue-size-is-limited-by-system-configuration": "The size of the queue is also limited by the system configuration.", - "cassandra-tenant-limits-configuration": "Cassandra query for tenant", - "ws-limit-max-sessions-per-tenant": "Sessions per tenant maximum number", - "ws-limit-max-sessions-per-customer": "Sessions per customer maximum number", - "ws-limit-max-sessions-per-regular-user": "Sessions per regular user maximum number", - "ws-limit-max-sessions-per-public-user": "Sessions per public user maximum number", - "ws-limit-queue-per-session": "Message queue per session maximum size", - "ws-limit-max-subscriptions-per-tenant": "Subscriptions per tenant maximum number", - "ws-limit-max-subscriptions-per-customer": "Subscriptions per customer maximum number", - "ws-limit-max-subscriptions-per-regular-user": "Subscriptions per regular user maximum number", - "ws-limit-max-subscriptions-per-public-user": "Subscriptions per public user maximum number", - "ws-limit-updates-per-session": "WS updates per session", - "rate-limits": { - "add-limit": "Add limit", - "advanced-settings": "Advanced settings", - "edit-limit": "Edit limit", - "edit-transport-tenant-msg-title": "Edit transport tenant messages rate limits", - "edit-transport-tenant-telemetry-msg-title": "Edit transport tenant telemetry messages rate limits", - "edit-transport-tenant-telemetry-data-points-title": "Edit transport tenant telemetry data points rate limits", - "edit-transport-device-msg-title": "Edit transport device messages rate limits", - "edit-transport-device-telemetry-msg-title": "Edit transport device telemetry messages rate limits", - "edit-transport-device-telemetry-data-points-title": "Edit transport device telemetry data points rate limits", - "edit-tenant-rest-limits-title": "Edit REST requests for tenant rate limits", - "edit-customer-rest-limits-title": "Edit REST requests for customer rate limits", - "edit-ws-limit-updates-per-session-title": "Edit WS updates per session rate limits", - "edit-cassandra-tenant-limits-configuration-title": "Edit Cassandra query for tenant rate limits", - "edit-tenant-entity-export-rate-limit-title": "Edit entity version creation rate limits", - "edit-tenant-entity-import-rate-limit-title": "Edit entity version load rate limits", - "edit-tenant-notification-request-rate-limit-title": "Edit notification requests rate limits", - "edit-tenant-notification-requests-per-rule-rate-limit-title": "Edit notification requests per notification rule rate limits", - "edit-integration-msgs-per-tenant-rate-limit-title": "Edit integration messages per tenant rate limits", - "edit-integration-msgs-per-device-rate-limit-title": "Edit integration messages per device rate limits", - "messages-per": "messages per", - "not-set": "Not set", - "number-of-messages": "Number of messages", - "number-of-messages-required": "Number of messages is required.", - "number-of-messages-min": "Minimum value is 1.", - "preview": "Preview", - "per-seconds": "Per seconds", - "per-seconds-required": "Time rate is required.", - "per-seconds-min": "Minimum value is 1.", - "rate-limits": "Rate limits", - "remove-limit": "Remove limit", - "transport-tenant-msg": "Transport tenant messages", - "transport-tenant-telemetry-msg": "Transport tenant telemetry messages", - "transport-tenant-telemetry-data-points": "Transport tenant telemetry data points", - "transport-device-msg": "Transport device messages", - "transport-device-telemetry-msg": "Transport device telemetry messages", - "transport-device-telemetry-data-points": "Transport device telemetry data points", - "sec": "sec" - } + "wind-speed-direction": { + "layout": "Išdėstymas", + "layout-default": "Numatytasis", + "layout-advanced": "Išplėstinis", + "layout-simplified": "Supaprastintas", + "values": "Reikšmės", + "wind-direction": "Vėjo kryptis", + "center-value": "Centrinė reikšmė", + "icon": "Piktograma", + "arrow": "Rodyklė", + "ticks": "Padalos", + "labels-type": "Etikečių tipas", + "directional-names": "Krypties pavadinimai", + "degrees": "Laipsniai", + "major-ticks": "Pagrindinės padalos", + "minor-ticks": "Smulkios padalos", + "wind-speed-direction-card-style": "Vėjo greičio ir krypties kortelės stilius", + "ticks-color": "Padalų spalva", + "ticks-labels-type": "Padalų etikečių tipas", + "arrow-color": "Rodyklės spalva" }, - "timeinterval": { - "seconds-interval": "{ seconds, plural, =1 {1 sekundė} other {# sekundės} }", - "minutes-interval": "{ minutes, plural, =1 {1 minutė} other {# minutės} }", - "hours-interval": "{ hours, plural, =1 {1 valanda} other {# valandos} }", - "days-interval": "{ days, plural, =1 {1 diena} other {# dienos} }", - "days": "Dienos", - "hours": "Valandos", - "minutes": "Minutės", - "seconds": "Sekundės", - "advanced": "Patyręs vartotojas", - "predefined": { - "yesterday": "Yesterday", - "day-before-yesterday": "Day before yesterday", - "this-day-last-week": "This day last week", - "previous-week": "Previous week (Sun - Sat)", - "previous-week-iso": "Previous week (Mon - Sun)", - "previous-month": "Previous month", - "previous-quarter": "Previous quarter", - "previous-half-year": "Previous half year", - "previous-year": "Previous year", - "current-hour": "Current hour", - "current-day": "Current day", - "current-day-so-far": "Current day so far", - "current-week": "Current week (Sun - Sat)", - "current-week-iso": "Current week (Mon - Sun)", - "current-week-so-far": "Current week so far (Sun - Sat)", - "current-week-iso-so-far": "Current week so far (Mon - Sun)", - "current-month": "Current month", - "current-month-so-far": "Current month so far", - "current-quarter": "Current quarter", - "current-quarter-so-far": "Current quarter so far", - "current-half-year": "Current half year", - "current-half-year-so-far": "Current half year so far", - "current-year": "Current year", - "current-year-so-far": "Current year so far" - } + "value-source": { + "value-source": "Reikšmės šaltinis", + "predefined-value": "Iš anksto nustatyta reikšmė", + "entity-attribute": "Objekto atributas", + "value": "Reikšmė", + "value-required": "Reikšmė yra privaloma.", + "key-required": "Raktas yra privalomas.", + "entity-key-required": "Objekto raktas yra privalomas.", + "source-entity-alias": "Šaltinio objekto pseudonimas", + "source-entity-attribute": "Šaltinio objekto atributas", + "type-constant": "Pastovus", + "type-latest-key": "Raktas", + "type-entity": "Objektas" }, - "timeunit": { - "milliseconds": "Milisekundės", - "seconds": "Sekundės", - "minutes": "Minutės", - "hours": "Valandos", - "days": "Dienos" - }, - "timewindow": { - "timewindow": "Laikotarpio vaizdas", - "years": "{ years, plural, =1 { metai } other {# metai } }", - "years-short": "{{ years }}m", - "months": "{ months, plural, =1 { mėnuo } other {# mėnesiai } }", - "months-short": "{{ months }}mėn", - "weeks": "{ weeks, plural, =1 { savaitė } other {# savaitės } }", - "weeks-short": "{{ weeks }}sav", - "days": "{ days, plural, =1 { diena } other {# dienos } }", - "days-short": "{{ days }}d", - "hours": "{ hours, plural, =0 { valandų } =1 {1 valanda } other {# valandos } }", - "hr": "{{ hr }} val", - "hr-short": "{{ hr }}val", - "minutes": "{ minutes, plural, =0 { minučių } =1 {1 minutė } other {# minutės } }", - "min": "{{ min }} min", - "min-short": "{{ min }}min", - "seconds": "{ seconds, plural, =0 { sekundžių } =1 {1 sekundė } other {# sekundės } }", - "sec": "{{ sec }} sek", - "sec-short": "{{ sec }}s", - "short": { - "days": "{ days, plural, =1 {1 diena } other {# dienos } }", - "hours": "{ hours, plural, =1 {1 valanda } other {# valandos } }", - "minutes": "{{minutes}} min ", - "seconds": "{{seconds}} sek " - }, - "realtime": "Realiu laiku", - "history": "istorija", - "last-prefix": "Paskutinės", - "period": "nuo {{ startTime }} iki {{ endTime }}", - "edit": "Redaguoti laikotarpio vaizdą", - "date-range": "Datų intervalas", - "for-all-time": "Viskas", - "last": "Paskutinės", - "time-period": "Laikotarpis", - "hide": "Slėpti", - "interval": "Interval", - "just-now": "Just now", - "just-now-lower": "just now", - "ago": "ago", - "style": "Timewindow style", - "icon": "Icon", - "icon-position": "Icon position", - "icon-position-left": "Left", - "icon-position-right": "Right", - "font": "Font", - "color": "Color", - "displayTypePrefix": "Display Realtime/History prefix", - "preview": "Preview" - }, - "unit": { - "millimeter": "Millimeter", - "centimeter": "Centimeter", - "angstrom": "Angstrom", - "nanometer": "Nanometer", - "micrometer": "Micrometer", - "meter": "Meter", - "kilometer": "Kilometer", - "inch": "Inch", - "foot": "Foot", - "yard": "Yard", - "mile": "Mile", - "nautical-mile": "Nautical Mile", - "astronomical-unit": "Astronomical Unit", - "reciprocal-metre": "Reciprocal Metre", - "meter-per-meter": "Meter per meter", - "steradian": "Steradian", - "thou": "Thou", - "barleycorn": "Barleycorn", - "hand": "Hand", - "chain": "Chain", - "furlong": "Furlong", - "league": "League", - "fathom": "Fathom", - "cable": "Cable", - "link": "Link", - "rod": "Rod", - "nanogram": "Nanogram", - "microgram": "Microgram", - "milligram": "Milligram", - "gram": "Gram", - "kilogram": "Kilogram", - "tonne": "Tonne", - "ounce": "Ounce", - "pound": "Pound", - "stone": "Stone", - "hundredweight-count": "Hundredweight count", - "short-tons": "Short tons", - "dalton": "Dalton", - "grain": "Grain", - "drachm": "Drachm", - "quarter": "Quarter", - "slug": "Slug", - "carat": "Carat", - "cubic-millimeter": "Cubic Millimeter", - "cubic-centimeter": "Cubic Centimeter", - "cubic-meter": "Cubic Meter/s", - "cubic-kilometer": "Cubic Kilometers", - "microliter": "Microliter", - "milliliter": "Milliliter", - "liter": "Liter", - "hectoliter": "Hectolitre", - "cubic-inch": "Cubic Inch", - "cubic-foot": "Cubic Foot", - "cubic-yard": "Cubic Yards", - "fluid-ounce": "Fluid Ounce", - "pint": "Pint", - "quart": "Quart", - "gallon": "Gallon", - "oil-barrels": "Oil Barrels", - "cubic-meter-per-kilogram": "Cubic Meter per Kilogram", - "gill": "Gill", - "hogshead": "Hogshead", - "teaspoon": "Teaspoon", - "tablespoon": "Tablespoon", - "cup": "Cup", - "celsius": "Celsius", - "kelvin": "Kelvin", - "rankine": "Rankine", - "fahrenheit": "Fahrenheit", - "percent": "Percent", - "meter-per-second": "Meter per Second", - "kilometer-per-hour": "Kilometer per Hour", - "foot-per-second": "Foot per Second", - "mile-per-hour": "Mile per Hour", - "knot": "Knot", - "millimeters-per-minute": "Millimeters per minute", - "kilometer-per-hour-squared": "Kilometer per hour squared", - "foot-per-second-squared": "Foot per second squared", - "pascal": "Pascal", - "kilopascal": "Kilopascal", - "megapascal": "Megapascal", - "gigapascal": "Gigapascal", - "millibar": "Millibar", - "bar": "Bar", - "kilobar": "Kilobar", - "newton": "Newton", - "newton-meter": "Newton meter", - "foot-pounds": "Foot-pounds", - "inch-pounds": "Inch-pounds", - "newton-per-meter": "Newton per meter", - "atmospheres": "Atmospheres", - "pounds-per-square-inch": "Pounds per Square Inch", - "torr": "Torr", - "inches-of-mercury": "Inches of Mercury", - "pascal-per-square-meter": "Pascal per Square Meter", - "pound-per-square-inch": "Pound per Square Inch", - "newton-per-square-meter": "Newton per Square Meter", - "kilogram-force-per-square-meter": "Kilogram-force per Square Meter", - "pascal-per-square-centimeter": "Pascal per Square Centimeter", - "ton-force-per-square-inch": "Ton-force per Square Inch", - "kilonewton-per-square-meter": "Kilonewton per Square Meter", - "newton-per-square-millimeter": "Newton per Square Millimeter", - "microjoule": "Microjoule", - "millijoule": "Millijoule", - "joule": "Joule", - "kilojoule": "Kilojoule", - "megajoule": "Megajoule", - "gigajoule": "Gigajoule", - "watt-hour": "Watt-hour", - "kilowatt-hour": "Kilowatt-hour", - "electron-volts": "Electron volts", - "joules-per-coulomb": "Joules per Coulomb", - "british-thermal-unit": "British Thermal Units", - "foot-pound": "Foot-pound", - "calorie": "Calorie", - "small-calorie": "Small Calorie", - "kilocalorie": "Kilocalorie", - "joule-per-kelvin": "Joule per Kelvin", - "joule-per-kilogram-kelvin": "Joule per Kilogram-Kelvin", - "joule-per-kilogram": "Joule per Kilogram", - "watt-per-meter-kelvin": "Watt per Meter-Kelvin", - "joule-per-cubic-meter": "Joule per Cubic Meter", - "therm": "Therm", - "electric-dipole-moment": "Electric Dipole Moment", - "magnetic-dipole-moment": "Magnetic Dipole Moment", - "debye": "Debye", - "coulomb-per-square-meter-per-volt": "Coulomb per Square Meter per Volt", - "milliwatt": "Milliwatt", - "microwatt": "Microwatt", - "watt": "Watt", - "kilowatt": "Kilowatt", - "megawatt": "Megawatt", - "gigawatt": "Gigawatt", - "metric-horsepower": "Metric Horsepower", - "milliwatt-per-square-centimeter": "Milliwatts per square centimeter", - "watt-per-square-centimeter": "Watts per square centimeter", - "kilowatt-per-square-centimeter": "Kilowatts per square centimeter", - "milliwatt-per-square-meter": "Milliwatts per square meter", - "watt-per-square-meter": "Watts per square meter", - "kilowatt-per-square-meter": "Kilowatts per square meter", - "watt-per-square-inch": "Watts per square inch", - "kilowatt-per-square-inch": "Kilowatts per square inch", - "horsepower": "Horsepower", - "btu-per-hour": "British thermal units/hour", - "coulomb": "Coulomb", - "millicoulomb": "Millicoulombs", - "microcoulomb": "Microcoulomb", - "picocoulomb": "Picocoulomb", - "coulomb-per-meter": "Coulomb per meter", - "coulomb-per-cubic-meter": "Coulomb per Cubic Meter", - "coulomb-per-square-meter": "Coulomb per Square Meter", - "square-millimeter": "Square Millimeter", - "square-centimeter": "Square Centimeter", - "square-meter": "Square Meter", - "hectare": "Hectare", - "square-kilometer": "Square Kilometer", - "square-inch": "Square Inch", - "square-foot": "Square Foot", - "square-yard": "Square Yard", - "acre": "Acre", - "square-mile": "Square Mile", - "are": "Are", - "barn": "Barn", - "circular-inch": "Circular Inch", - "milliampere-hour": "Milliampere-hour", - "milliampere-hour-tags": "electric current, current flow, electric charge, current capacity, flow of electricity, electrical flow, milliampere-hour, milliampere-hours, mAh", - "ampere-hours": "Ampere-hours", - "ampere-hours-tags": "electric current, current flow, electric charge, current capacity, flow of electricity, electrical flow, ampere, ampere-hours, Ah", - "kiloampere-hours": "Kiloampere-hours", - "kiloampere-hours-tags": "electric current, current flow, electric charge, current capacity, flow of electricity, electrical flow, kiloampere-hours, kiloampere-hour, kAh", - "nanoampere": "Nanoampere", - "nanoampere-tags": "current, amperes, nanoampere, nA", - "picoampere": "Picoampere", - "picoampere-tags": "current, amperes, picoampere, pA", - "microampere": "Microampere", - "microampere-tags": "electric current, microampere, microamperes, μA", - "milliampere": "Milliampere", - "milliampere-tags": "electric current, milliampere, milliamperes, mA", - "ampere": "Ampere", - "ampere-tags": "electric current, current flow, flow of electricity, electrical flow, ampere, amperes, amperage, A", - "kiloamperes": "Kiloamperes", - "kiloamperes-tags": "electric current, current flow, kiloamperes, kA", - "microampere-per-square-centimeter": "Microampere per square centimeter", - "microampere-per-square-centimeter-tags": "Current density, microampere per square centimeter, µA/cm²", - "ampere-per-square-meter": "Ampere per Square Meter", - "ampere-per-square-meter-tags": "current density, current per unit area, ampere per square meter, A/m²", - "ampere-per-meter": "Ampere per Meter", - "ampere-per-meter-tags": "magnetic field strength, magnetic field intensity, ampere per meter, A/m", - "oersted": "Oersted", - "oersted-tags": "magnetic field, oersted, Oe", - "bohr-magneton": "Bohr Magneton", - "bohr-magneton-tags": "atomic physics, magnetic moment, bohr magneton, μB", - "ampere-meter-squared": "Ampere-Meter Squared", - "ampere-meter-squared-tags": "magnetic moment, dipole moment, ampere-meter squared, A·m²", - "ampere-meter": "Ampere-Meter", - "ampere-meter-tags": "magnetic field, current loop, ampere-meter, A·m", - "nanovolt": "Nanovolt", - "picovolt": "Picovolt", - "millivolts": "Millivolts", - "microvolts": "Microvolts", - "volt": "Volt", - "kilovolts": "Kilovolts", - "dbmV": "dBmV", - "volt-meter": "Volt-Meter", - "kilovolt-meter": "Kilovolt-Meter", - "megavolt-meter": "Megavolt-Meter", - "microvolt-meter": "Microvolt-Meter", - "millivolt-meter": "Millivolt-Meter", - "nanovolt-meter": "Nanovolt-Meter", - "ohm": "Ohm", - "microohm": "Microohm", - "milliohm": "Milliohm", - "kilohm": "Kilohm", - "megohm": "Megohm", - "gigohm": "Gigohm", - "hertz": "Hertz", - "kilohertz": "Kilohertz", - "megahertz": "Megahertz", - "gigahertz": "Gigahertz", - "rpm": "Revolutions Per Minute", - "candela-per-square-meter": "Candela per square meter", - "candela": "Candela", - "lumen": "Lumen", - "lux": "Lux", - "foot-candle": "Foot-candle", - "lumen-per-square-meter": "Lumen per square meter", - "lux-second": "Lux second", - "lumen-second": "Lumen second", - "lumens-per-watt": "Lumens per watt", - "absorbance": "Absorbance", - "mole": "Mole", - "nanomole": "Nanomole", - "micromole": "MicroMole", - "millimole": "Millimole", - "kilomole": "Kilomole", - "mole-per-cubic-meter": "Mole per Cubic Meter", - "rssi": "RSSI", - "ppm": "Parts Per Million", - "ppb": "Parts Per Billion", - "micrograms-per-cubic-meter": "Micrograms per Cubic Meter", - "aqi": "AQI", - "gram-per-cubic-meter": "Gram per cubic meter", - "gram-per-kilogram": "Specific Humidity", - "millimeters-per-second": "Millimeters per second", - "neper": "Neper", - "bel": "Bel", - "decibel": "Decibel", - "meters-per-second-squared": "Meters per second squared", - "becquerel": "Becquerel", - "curie": "Curie", - "gray": "Gray", - "sievert": "Sievert", - "roentgen": "Roentgen", - "cps": "Counts per Second", - "rad": "Rad", - "rem": "Rem", - "dps": "Disintegrations per second", - "rutherford": "Rutherford", - "coulombs-per-kilogram": "Coulombs per kilogram", - "becquerels-per-cubic-meter": "Becquerels per cubic meter", - "curies-per-liter": "Curies per liter", - "becquerels-per-second": "Becquerels per second", - "curies-per-second": "Curies per second", - "gy-per-second": "Gray per Second", - "watt-per-steradian": "Watt per Steradian", - "watt-per-square-metre-steradian": "Watt per Square Metre-Steradian", - "ph-level": "pH Level", - "turbidity": "Turbidity", - "mg-per-liter": "Milligrams per liter", - "microsiemens-per-centimeter": "Microsiemens per centimeter", - "millisiemens-per-meter": "Millisiemens per meter", - "siemens-per-meter": "Siemens per meter", - "kilogram-per-cubic-meter": "Kilogram per cubic meter", - "gram-per-cubic-centimeter": "Gram per cubic centimeter", - "kilogram-per-square-meter": "Kilogram per square metre", - "milligram-per-milliliter": "Milligram per milliliter", - "pound-per-cubic-foot": "Pound per cubic foot", - "ounces-per-cubic-inch": "Ounces per cubic inch", - "tons-per-cubic-yard": "Tons per cubic yard", - "particle-density": "Particle density", - "kilometers-per-liter": "Kilometers per liter", - "miles-per-gallon": "Miles per gallon", - "liters-per-100-km": "Liters per 100 km", - "gallons-per-mile": "Gallons per mile", - "liters-per-hour": "Liters per hour", - "gallons-per-hour": "Gallons per hour", - "beats-per-minute": "Beats per minute", - "millimeters-of-mercury": "Millimeters of mercury", - "milligrams-per-deciliter": "Milligrams per deciliter", - "g-force": "G-force", - "kilonewton": "Kilonewton", - "kilogram-force": "Kilogram-Force", - "pound-force": "Pound-Force", - "kilopound-force": "Kilopound-Force", - "dyne": "Dyne", - "poundal": "Poundal", - "kip": "Kip", - "gal": "Gal", - "gravity": "Gravity", - "hectopascal": "Hectopascal", - "atmosphere": "Atmosphere", - "millibars": "Millibars", - "inch-of-mercury": "One inch of mercury", - "richter-scale": "Richter Scale", - "second": "Second", - "minute": "Minute", - "hour": "Hour", - "day": "Day", - "week": "Week", - "month": "Month", - "year": "Year", - "cubic-foot-per-minute": "Cubic Foot Per Minute", - "cubic-meters-per-hour": "Cubic Meters Per Hour", - "cubic-meters-per-second": "Cubic Meters Per Second", - "liter-per-second": "Liter Per Second", - "liter-per-minute": "Liter Per Minute", - "gallons-per-minute": "Gallons Per Minute", - "cubic-foot-per-second": "Cubic foot per second", - "milliliters-per-minute": "Milliliters per minute", - "bit": "Bit", - "byte": "Byte", - "kilobyte": "Kilobyte", - "megabyte": "Megabyte", - "gigabyte": "Gigabyte", - "terabyte": "Terabyte", - "petabyte": "Petabyte", - "exabyte": "Exabyte", - "zettabyte": "Zettabyte", - "yottabyte": "Yottabyte", - "bit-per-second": "Bit per second", - "kilobit-per-second": "Kilobit per second", - "megabit-per-second": "Megabit per second", - "gigabit-per-second": "Gigabit per second", - "terabit-per-second": "Terabit per second", - "byte-per-second": "Byte per second", - "kilobyte-per-second": "Kilobyte per second", - "megabyte-per-second": "Megabyte per second", - "gigabyte-per-second": "Gigabyte per second", - "degree": "Degree", - "radian": "Radian", - "gradian": "Gradian", - "mil": "Mil", - "revolution": "Revolution", - "siemens": "Siemens", - "millisiemens": "Millisiemens", - "microsiemens": "Microsiemens", - "kilosiemens": "Kilosiemens", - "megasiemens": "Megasiemens", - "gigasiemens": "Gigasiemens", - "farad": "Farad", - "millifarad": "Millifarad", - "microfarad": "Microfarad", - "nanofarad": "Nanofarad", - "picofarad": "Picofarad", - "kilofarad": "Kilofarad", - "megafarad": "Megafarad", - "gigafarad": "Gigafarad", - "terfarad": "Terfarad", - "farad-per-meter": "Farad per Meter", - "tesla": "Tesla", - "gauss": "Gauss", - "kilogauss": "Kilogauss", - "millitesla": "Millitesla", - "microtesla": "Microtesla", - "nanotesla": "Nanotesla", - "kilotesla": "Kilotesla", - "megatesla": "Megatesla", - "millitesla-square-meters": "millitesla square meters", - "gamma": "Gamma", - "lambda": "Lambda", - "square-meter-per-second": "Square meter per second", - "square-centimeter-per-second": "Square centimeter per second", - "stoke": "Stoke", - "centistokes": "Centistokes", - "square-foot-per-second": "Square foot per second", - "square-inch-per-second": "Square inch per second", - "pascal-second": "Pascal-second", - "centipoise": "Centipoise", - "poise": "Poise", - "reynolds": "Reynolds", - "pound-per-foot-hour": "Pound per foot-hour", - "newton-second-per-square-meter": "Newton second per square meter", - "dyne-second-per-square-centimeter": "Dyne second per square centimeter", - "kilogram-per-meter-second": "Kilogram per meter-second", - "tesla-square-meters": "Tesla square meters", - "maxwell": "Maxwell", - "tesla-per-meter": "Tesla per Meter", - "gauss-per-centimeter": "Gauss per Centimeter", - "weber": "Weber", - "microweber": "Microweber", - "milliweber": "Milliweber", - "gauss-square-centimeter": "Gauss-Square Centimeter", - "kilogauss-square-centimeter": "Kilogauss-Square Centimeter", - "henry": "Henry", - "millihenry": "Millihenry", - "microhenry": "Microhenry", - "nanohenry": "Nanohenry", - "henry-per-meter": "Henry per Meter", - "tesla-meter-per-ampere": "Tesla Meter per Ampere", - "gauss-per-oersted": "Gauss per Oersted", - "kilogram-per-mole": "Kilogram per mole", - "gram-per-mole": "Gram per mole", - "milligram-per-mole": "Milligram per mole", - "joule-per-mole": "Joule per Mole", - "joule-per-mole-kelvin": "Joule per Mole-Kelvin", - "millivolts-per-meter": "Millivolts per meter", - "volts-per-meter": "Volts per meter", - "kilovolts-per-meter": "Kilovolts per meter", - "radian-per-second": "Radian per second", - "radian-per-second-squared": "Radian per second squared", - "revolutions-per-minute-per-second": "Angular acceleration", - "revolutions-per-minute-per-second-squared": "Angular Acceleration", - "deg-per-second": "deg/s", - "degrees-brix": "Degrees Brix", - "katal": "Katal", - "katal-per-cubic-metre": "Katal per Cubic Metre" - }, - "user": { - "all": "Visi", - "all-users": "Visi vartotojai", - "groups": "Grupės", - "user": "Vartotojas", - "users": "Vartotojai", - "management": "Vartotojų valdymas", - "customer-users": "Kliento vartotojai", - "tenant-admins": "Valdytojų administratoriai", - "sys-admin": "Sistemos administratoriai", - "tenant-admin": "Valdytojo administratoriai", - "customer": "Klientas", - "anonymous": "Anonimas", - "add": "Pridėti vartotoją", - "delete": "Panaikinti vartotoją", - "add-user-text": "Pridėti naują vartotoją", - "no-users-text": "Vartotojų nėra", - "user-details": "Informacija apie vartotoją", - "delete-users": "Panaikinti vartotojus", - "delete-user-title": "Ar tikrai norite panaikinti vartotoją '{{userEmail}}'?", - "delete-user-text": "Būkite dėmesingi, po patvirtinimo vartotojas ir visa su juo susijusi informacija bus panaikinta.", - "delete-users-title": "Ar tikrai norite panaikinti { count, plural, =1 {1 vatotoją} other {# vartotojus} }?", - "delete-users-action-title": "Panaikinti { count, plural, =1 {1 vartotoją} other {# vartotojus} }", - "delete-users-text": "Būkite dėmesingi, po patvirtinimo visi pasirinkti vartotojai ir su jis susijusi informacija bus panaikinta.", - "activation-email-sent-message": "Aktyvinimo el. laiškas sėkmingai išsiųstas!", - "resend-activation": "Iš naujo siųsti aktyvinimą", - "email": "El. pašto adresas", - "email-required": "El. pašto adresas būtinas.", - "invalid-email-format": "Neteisingas el. pašto adreso formatas.", - "first-name": "Vardas", - "last-name": "Pavardė", - "description": "Aprašymas", - "default-dashboard": "Pagrindinis skydelis", - "always-fullscreen": "Visada per visą ekraną", - "select-user": "Pasirinkti vartotoją", - "no-users-matching": "Vartotojų, atitinkančių'{{entity}}' nėra.", - "user-required": "Vartotojas būtinas", - "activation-method": "Aktyvavimo būdas", - "display-activation-link": "Rodyti aktyvavimo nuorodą", - "send-activation-mail": "Siųsti aktyvavimo el. laišką", - "activation-link": "Vartotojo aktyvavimo nuoroda", - "activation-link-text": "Vartotojui aktyvuoti, paspauskite nuorodą :", - "copy-activation-link": "Kopijuoti aktyvavimo nuorodą", - "activation-link-copied-message": "Vartotojo aktyvavimo nuoroda nukopijuota į iškarpinę", - "details": "Informacija", - "login-as-tenant-admin": "Prisijunkti kaip valdytojo administratorius", - "login-as-customer-user": "Prisijunkti kaip kliento vartotojas", - "select-group-to-add": "Pasirinkite grupę, kuriai priskirti pažymėtus vartotojus", - "select-group-to-move": "Pasirinkite grupę, į kurią perkelti pažymėtus vartotojus", - "remove-users-from-group": "Ar tikrai norite pašalinti { count, plural, =1 {1 vartotoją} other {# vartotojus} } iš grupės '{{entityGroup}}'?", - "group": "Vartotojų grupė", - "list-of-groups": "{ count, plural, =1 {Viena vartotojų grupė} other {# Vartotojų grupių sąrašas} }", - "group-name-starts-with": "Vartotojų grupės, kurios prasideda '{{prefix}}'", - "search": "Vartotojų paieška", - "selected-users": "Pasirinkta { count, plural, =1 {1 vartotojas} other {# vartotojai} }", - "disable-account": "Išjungti vartotojo paskyrą", - "enable-account": "Įjungti vartotojo paskyrą", - "enable-account-message": "Vartotojo paskyra sėkmingai įjungta!", - "disable-account-message": "Vartotojo paskyra sėkmingai išjungta!", - "copyId": "Kopijuoti vartotojo Id", - "idCopiedMessage": "Vartotojo Id nukopiuotas į iškarpinę", - "user-list": "Vartotojų sąrašas", - "user-list-required": "Vartotojų sąrašas būtinas" - }, - "value": { - "type": "Reikšmės tipas", - "string": "Tekstas", - "string-value": "Tekstinė informacija", - "string-value-required": "Tekstinė informacija būtina", - "integer": "Sveikasis skaičius", - "integer-value": "Sveikojo skaičiaus reikšmė", - "integer-value-required": "Sveikojo skaičiaus reikšmė būtina", - "invalid-integer-value": "Sveikojo skaičiaus reikšmė neteisinga", - "double": "Realusis skaičius", - "double-value": "Realiojo skaičiaus reikšmė", - "double-value-required": "Realiojo skaičiaus reikšmė būtina", - "boolean": "Loginis", - "boolean-value": "Loginė reikšmė", - "false": "Netiesa", - "true": "Tiesa", - "long": "Sveikas skaičius", - "json": "JSON", - "json-value": "JSON reikšmė", - "json-value-invalid": "Neteisingas JSON reikšmės formatas", - "json-value-required": "JSON reikšmė būtina." - }, - "version-control": { - "version-control": "Version control", - "management": "Version control management", - "search": "Search versions", - "branch": "Branch", - "default": "Default", - "select-branch": "Select branch", - "branch-required": "Branch is required", - "create-entity-version": "Create entity version", - "version-name": "Version name", - "version-name-required": "Version name is required", - "author": "Author", - "export-relations": "Export relations", - "export-attributes": "Export attributes", - "export-credentials": "Export credentials", - "export-group-entities": "Export group entities", - "export-roles": "Export roles", - "entity-versions": "Entity versions", - "versions": "Versions", - "created-time": "Created time", - "version-id": "Version ID", - "no-entity-versions-text": "No entity versions found", - "no-versions-text": "No versions found", - "copy-full-version-id": "Copy full version id", - "create-version": "Create version", - "creating-version": "Creating version... Please wait", - "nothing-to-commit": "No changes to commit", - "restore-version": "Restore version", - "restore-entity-from-version": "Restore entity from version '{{versionName}}'", - "restoring-entity-version": "Restoring entity version... Please wait", - "load-relations": "Load relations", - "load-attributes": "Load attributes", - "load-credentials": "Load credentials", - "load-group-entities": "Load group entities", - "load-roles": "Load roles", - "compare-with-current": "Compare with current", - "diff-entity-with-version": "Diff with entity version '{{versionName}}'", - "previous-difference": "Previous Difference", - "next-difference": "Next Difference", - "current": "Current", - "differences": "{ count, plural, =1 {1 difference} other {# differences} }", - "create-entities-version": "Create entities version", - "default-sync-strategy": "Default sync strategy", - "sync-strategy-merge": "Merge", - "sync-strategy-overwrite": "Overwrite", - "entities-to-export": "Entities to export", - "entities-to-restore": "Entities to restore", - "sync-strategy": "Sync strategy", - "all-entities": "All entities", - "no-entities-to-export-prompt": "Please specify entities to export", - "no-entities-to-restore-prompt": "Please specify entities to restore", - "add-entity-type": "Add entity type", - "remove-all": "Remove all", - "version-create-result": "{ added, plural, =0 {No entities} =1 {1 entity} other {# entities} } added.
    { modified, plural, =0 {No entities} =1 {1 entity} other {# entities} } modified.
    { removed, plural, =0 {No entities} =1 {1 entity} other {# entities} } removed.", - "remove-other-entities": "Remove other entities", - "find-existing-entity-by-name": "Find existing entity by name", - "restore-entities-from-version": "Restore entities from version '{{versionName}}'", - "restoring-entities-from-version": "Restoring entities... Please wait", - "no-entities-restored": "No entities restored", - "created": "{{created}} created", - "updated": "{{updated}} updated", - "deleted": "{{deleted}} deleted", - "groups-created": "{ created, plural, =1 {1 group} other {# groups} } created", - "groups-updated": "{ updated, plural, =1 {1 group} other {# groups} } updated", - "groups-deleted": "{ deleted, plural, =1 {1 group} other {# groups} } deleted", - "remove-other-entities-confirm-text": "Be careful! This will permanently delete all current entities
    not present in the version you want to restore.

    Please type \"remove other entities\" to confirm.", - "auto-commit-to-branch": "auto-commit to {{ branch }} branch", - "default-create-entity-version-name": "{{entityName}} update", - "sync-strategy-merge-hint": "Creates or updates selected entities in the repository. All other repository entities are not modified.", - "sync-strategy-overwrite-hint": "Creates or updates selected entities in the repository. All other repository entities are deleted.", - "device-credentials-conflict": "Failed to load the device with external id {{entityId}}
    due to the same credentials are already present in the database for another device.
    Please consider disabling the load credentials setting in the restore form.", - "missing-referenced-entity": "Failed to load the {{sourceEntityTypeName}} with external id {{sourceEntityId}}
    because it references missing {{targetEntityTypeName}} with id {{targetEntityId}}.", - "add-entity-groups": "Add entity groups", - "entity-groups": "Entity groups", - "integration-routing-key-conflict": "Failed to load the integration with external id {{entityId}}
    due to the same integration key is already present in the database for another integration.
    Please consider enabling the auto-generate integration key setting in the restore form.", - "auto-generate-integration-key": "Auto-generate integration key", - "runtime-failed": "Failed: {{message}}", - "auto-commit-settings-read-only-hint": "Auto-commit feature doesn't work with enabled read-only option in Repository settings." - }, - "widget": { - "widget-library": "Valdiklių galerija", - "widget-bundle": "Valdiklių rinkinys", - "all-bundles": "Visi rinkiniai", - "select-widgets-bundle": "Pasirinkti valdiklių rinkinį", - "widgets": "Valdikliai", - "all-widgets": "Visi valdikliai", - "widget": "Valdiklis", - "select-widget": "Pasirinkti valdiklį", - "no-widgets-matching": "Valdiklių atitinkančių '{{entity}}' nėra.", - "no-widgets": "Valdiklių dar nėra", - "no-widgets-text": "Valdiklių nėra", - "management": "Valdiklių valdymas", - "editor": "Valdiklių redaktorius", - "widget-type-not-found": "Įkeliant valdiklio konfigūraciją kilo klaidų.
    Gali būti, kad\n toks valdiklio tipas panaikintas.", - "widget-type-load-error": "Valdiklis neįkeltas, nes iškilo klaidų:", - "remove": "Panaikinti valdiklį", - "delete": "Panaikinti valdiklį", - "edit": "Redaguoti valdiklį", - "edit-widget-type": "Redaguoti valdikliį", - "remove-widget-title": "Ar tikrai norite panaikinti valdiklį '{{widgetTitle}}'?", - "remove-widget-text": "Būkite dėmesingi, po patvirtinimo valdiklis ir visa su juo susijusi informacija bus pašalinta.", - "timeseries": "Telemetrija", - "search-data": "Duomenų paieška", - "no-data-found": "Duomenų nėra", - "latest": "Naujausios reikšmės", - "rpc": "Valdymo valdiklis", - "alarm": "Įspėjimų valdiklis", - "static": "Statinis valdiklis", - "select-widget-type": "Pasirinkite valdiklio tipą", - "missing-widget-title-error": "Valdiklio pavadinimas būtinas!", - "widget-saved": "Valdiklis išsaugotas", - "unable-to-save-widget-error": "Valdiklio išsaugoti negalima! Valdiklyje yra klaidų!", - "save": "Valdiklis išsaugotas", - "saveAs": "valdiklį išsaugoti kaip", - "move": "Perkelti valdiklį", - "save-widget-type-as": "Valdiklį išsaugoti kaip", - "save-widget-type-as-text": "Įrašykite naują valdiklio pavadinimą", - "toggle-fullscreen": "Perjungti į viso ekrano režimą", - "run": "Paleisti valdiklį", - "widget-title": "Pavadinimas", - "title": "Pavadinimas", - "title-required": "Valdiklio pavadinimas būtinas.", - "title-max-length": "Pavadinimas negali viršyti 256 simbolių", - "system": "Sisteminis", - "type": "Tipas", - "resources": "Resursai", - "resource-url": "JavaScript/CSS URL", - "remove-resource": "Panaikinti resursą", - "add-resource": "Pridėti resursą", - "html": "HTML", - "tidy": "Tidy", - "css": "CSS", - "settings-schema": "Nustatymų schema", - "datakey-settings-schema": "Duomenų raktų nustatymų schema", - "latest-datakey-settings-schema": "Latest data key settings schema", - "widget-settings": "Valdiklio nustatymai", - "description": "Aprašymas", - "image-preview": "Paveiksliuko peržiūra", - "settings-form-selector": "Settings form selector", - "data-key-settings-form-selector": "Data key settings form selector", - "latest-data-key-settings-form-selector": "Latest data key settings form selector", - "all": "All", - "actual": "Actual", - "deprecated": "Pasenęs", - "has-basic-mode": "Has basic mode", - "basic-mode-form-selector": "Basic mode form selector", - "basic-mode": "Basic", - "advanced-mode": "Advanced", - "javascript": "Javascript", - "js": "JS", - "remove-widget-type-title": "Ar tikrai norite pašalinti valdiklį '{{widgetName}}'?", - "remove-widget-type-text": "Būkite dėmesingi, po patvirtinimo valdiklis ir visa su juo susijusi informacija bus pašalinta.", - "remove-widget-type": "Panaikinti valdiklį", - "widget-types": "Valdikliai", - "delete-widget-type-title": "Ar tikrai norite panaikinti valdiklį '{{widgetTypeName}}'?", - "delete-widget-type-text": "Būkite dėmesingi, po patvirtinimo valdiklis ir visa su juo susijusi informacija bus pašalinta.", - "delete-widget-types-title": "Ar tikrai norite panaikinti { count, plural, =1 {1 valdiklį} other {# valdiklius} }?", - "delete-widget-types-text": "Būkite dėmesingi, po patvirtinimo visi pasirinkti valdikliai ir su jais susijusi informacija bus pašalinta.", - "delete-widget-type": "Panaikinti valdiklį", - "add-widget-type": "Pridėti naują valdiklį", - "widget-type-load-failed-error": "Valdiklio įkelti nepavyko!", - "widget-template-load-failed-error": "Valdiklio šablono įkelti nepavyko!", - "details": "Informacija", - "widget-type-details": "Informacija apie valdiklį", - "add": "Pridėti valdiklį", - "no-widget-types-text": "Valdiklių nėra", - "search-widget-types": "Valdiklių paieška", - "selected-widget-types": "Pasirinkta { count, plural, =1 {1 valdiklis} other {# valdikliai} }", - "undo": "Atšaukti valdiklio pakeitimus", - "export": "Eksportuoti valdiklį", - "export-data": "Eksportuoti valdiklio duomenis", - "export-to-csv": "Eksportuoti valdiklio duomenis į CSV...", - "export-to-excel": "Eksportuoti valdiklio duomenis į XLS...", - "export-to-excel-xlsx": "Eksportuoti valdiklio duomenis į XLSX...", - "no-data": "Duomenų atvaizdavimui nėra", - "data-overflow": "Valdiklyje rodoma {{count}} iš {{total}} įrašų", - "alarm-data-overflow": "Valdiklyje rodomi {{allowedEntities}} (didižiausias leistinas skaičius) įspėjimų įrašai iš {{totalEntities}}", - "search": "Valdiklių paieška", - "filter": "Valdiklių filtro tipas", - "loading-widgets": "Valdikliai įkeliami...", - "widget-template-error": "Netinkamas valdiklio HTML šablonas." + "rpc-state": { + "initial-state": "Pradinė būsena", + "initial-state-hint": "Veiksmas, skirtas gauti pradinę komponento būseną (Įjungta / Išjungta).", + "disabled-state": "Išjungta būsena", + "disabled-state-hint": "Sąlyga, pagal kurią komponentas yra išjungiamas.", + "turn-on": "Įjungti", + "turn-on-hint": "Veiksmas, paleidžiamas, kai jungiklis perjungiamas į 'Įjungta'.", + "turn-off": "Išjungti", + "turn-off-hint": "Veiksmas, paleidžiamas, kai jungiklis perjungiamas į 'Išjungta'.", + "on": "Įjungta", + "off": "Išjungta", + "disabled": "Išjungta" }, - "widget-action": { - "header-button": "Valdiklio antraštės mygtukas", - "open-dashboard-state": "Eiti į naują skydelio būseną", - "update-dashboard-state": "Atnaujinti dabartinę skydelio būseną", - "open-dashboard": "Eiti į kitą skydelio būseną", - "custom": "Aprašomas veiksmas", - "custom-pretty": "Aprašomas veiksmas (su HTML šablonu)", - "custom-pretty-error-title": "Custom dialog error", - "custom-pretty-template-error": "Invalid custom dialog template.", - "custom-pretty-controller-error": "Error occurred while evaluating custom dialog function.", - "mobile-action": "Mobile action", - "target-dashboard-state": "Tikslinė skydelio būsena", - "target-dashboard-state-required": "Tikslnė skydelio būsena būtina", - "set-entity-from-widget": "Nustatyti subjektą iš valdiklio", - "target-dashboard": "Tikslinis skydelis", - "open-right-layout": "Atidarykiti tinkamą skydelio išdėstymą (vaizdas mobiliesiems)", - "state-display-type": "Skydelio būsenos rodymo parinktis", - "open-normal": "Normal", - "open-in-separate-dialog": "Open in separate dialog", - "open-in-popover": "Open in popover", - "dialog-title": "Dialog title", - "dialog-hide-dashboard-toolbar": "Hide dashboard toolbar in dialog", - "dialog-width": "Dialog width in percents relative to viewport width", - "dialog-height": "Dialog height in percents relative to viewport height", - "dialog-size-range-error": "Dialog size percent value should be in a range from 1 to 100.", - "popover-preferred-placement": "Preferred popover placement", - "popover-placement-top": "Top", - "popover-placement-topLeft": "Top left", - "popover-placement-topRight": "Top right", - "popover-placement-right": "Right", - "popover-placement-rightTop": "Right top", - "popover-placement-rightBottom": "Right bottom", - "popover-placement-bottom": "Bottom", - "popover-placement-bottomLeft": "Bottom left", - "popover-placement-bottomRight": "Bottom right", - "popover-placement-left": "Left", - "popover-placement-leftTop": "Left top", - "popover-placement-leftBottom": "Left bottom", - "popover-hide-on-click-outside": "Hide popover on outside click", - "popover-hide-dashboard-toolbar": "Hide dashboard toolbar in popover", - "popover-width": "Popover width in browser units (ex. 100px, 25vw)", - "popover-height": "Popover height in browser units (ex. 100px, 25vh)", - "popover-style": "Popover style", - "open-new-browser-tab": "Open in a new browser tab", - "mobile": { - "action-type": "Mobile action type", - "action-type-required": "Mobile action type is required", - "take-picture-from-gallery": "Take picture from gallery", - "take-photo": "Take photo", - "map-direction": "Open map directions", - "map-location": "Open map location", - "scan-qr-code": "Scan QR Code", - "make-phone-call": "Make phone call", - "get-location": "Get phone location", - "take-screenshot": "Take screenshot" - } + "value-action": { + "do-nothing": "Nedaryti nieko", + "execute-rpc": "Vykdyti RPC komandą", + "get-attribute": "Gauti atributą", + "set-attribute": "Nustatyti atributą", + "get-time-series": "Gauti laiko eilutę", + "get-alarm-status": "Gauti aliarmo būseną", + "get-dashboard-state": "Gauti skydelio būsenos ID", + "get-dashboard-state-object": "Gauti skydelio būsenos objektą", + "add-time-series": "Pridėti laiko eilutę", + "execute-rpc-text": "Vykdyti RPC metodą '{{methodName}}'", + "get-time-series-text": "Naudoti laiko eilutę '{{key}}'", + "get-attribute-text": "Naudoti atributą '{{key}}'", + "get-alarm-status-text": "Naudoti aliarmo būseną", + "get-dashboard-state-text": "Naudoti skydelio būseną", + "get-dashboard-state-object-text": "Naudoti skydelio būsenos objektą", + "when-dashboard-state-is-text": "Kai skydelio būsena yra '{{state}}'", + "when-dashboard-state-function-is-text": "Kai f(skydelio būsena) = '{{state}}'", + "when-dashboard-state-object-function-is-text": "Kai f(skydelio būsenos objektas) = '{{state}}'", + "set-attribute-to-value-text": "Nustatyti '{{key}}' atributo reikšmę į: {{value}}", + "add-time-series-value-text": "Pridėti '{{key}}' laiko eilutės reikšmę: {{value}}", + "set-attribute-text": "Nustatyti '{{key}}' atributą", + "add-time-series-text": "Pridėti '{{key}}' laiko eilutę", + "action": "Veiksmas", + "value": "Reikšmė", + "init-value-hint": "Reikšmė, kuri bus naudojama iki kol įrenginys atsiųs duomenis.", + "method": "Metodas", + "method-name-required": "Metodo pavadinimas yra privalomas.", + "request-timeout-ms": "RPC užklausos laiko limitas (ms)", + "request-timeout-required": "Reikalingas užklausos laiko limitas.", + "min-request-timeout-error": "Užklausos laiko limitas turi būti ne mažesnis nei 5000 ms (5 sekundės).", + "request-persistent": "Išliekanti RPC užklausa", + "persistent-polling-interval": "Išliekantis apklausos intervalas (ms)", + "persistent-polling-interval-hint": "Apklausos intervalas (ms) išliekantiems RPC komandų atsakymams gauti", + "persistent-polling-interval-required": "Išliekantis apklausos intervalas yra privalomas.", + "min-persistent-polling-interval-error": "Apklausos intervalas turi būti ne mažesnis nei 1000 ms (1 sekundė).", + "attribute-scope": "Atributo taikymo sritis", + "attribute-key": "Atributo raktas", + "attribute-key-required": "Atributo raktas yra privalomas.", + "time-series-key": "Laiko eilutės raktas", + "time-series-key-required": "Laiko eilutės raktas yra privalomas.", + "action-result-converter": "Veiksmo rezultatų konverteris", + "converter-none": "Nėra", + "converter-function": "Funkcija", + "converter-constant": "Pastovi reikšmė", + "converter-value": "Reikšmė", + "parse-value-function": "Reikšmės apdorojimo funkcija", + "state-when-result-is": "'{{state}}' kai rezultatas yra", + "parameters": "Parametrai", + "convert-value-function": "Reikšmės konvertavimo funkcija", + "error": { + "target-entity-is-not-set": "Tikslinis objektas nenustatytas!", + "failed-to-perform-action": "Nepavyko atlikti veiksmo {{ actionLabel }}.", + "invalid-attribute-scope": "{{scope}} atributų taikymo sritis nepalaikoma {{entityType}} objekte." + } }, - "widgets-bundle": { - "current": "Dabartinis rinkinys", - "widgets-bundles": "Valdiklių rinkiniai", - "widgets-bundle-widgets": "Valdiklių rinkinio valdikliai", - "add": "Pridėti valdiklių rinkinį", - "delete": "Panaikinti valdiklių rinkinį", - "title": "Pavadinimas", - "title-required": "Pavadinimas būtinas.", - "title-max-length": "Pavadinimas negali viršyti 256 simbolių", - "description": "Aprašymas", - "image-preview": "Paveiksliuko peržiūra", - "add-widgets-bundle-text": "Pridėti naują valdiklių rinkinį", - "no-widgets-bundles-text": "Valdiklių rinkinių nėra", - "empty": "Valdiklių rinkinys tuščias", - "details": "Informacija", - "widgets-bundle-details": "Informacija apie valdiklių rinkinį", - "delete-widgets-bundle-title": "Ar tikrai norite panaikinti valdiklių rinkinį '{{widgetsBundleTitle}}'?", - "delete-widgets-bundle-text": "Būkite dėmesingi, po patvirtinimo valdiklių rinkinys ir visa su juo susijusi informacija bus panaikinta.", - "delete-widgets-bundles-title": "Ar tikrai norite panaikinti { count, plural, =1 {1 valdiklių rinkinį} other {# valdiklių rinkinius} }?", - "delete-widgets-bundles-action-title": "Panaikinti { count, plural, =1 {1 valdiklių rinkinį} other {# valdiklių rinknius} }", - "delete-widgets-bundles-text": "Būkite dėmesingi, po patvirtinimo visi pasirinkti valdiklių rinkiniai ir su jais susijusi informacija bus panaikinta.", - "no-widgets-bundles-matching": "Valdiklių rinkinių, atitinkančių '{{widgetsBundle}}' nėra.", - "widgets-bundle-required": "Valdiklių rinkinys būtinas.", - "system": "Sisteminis", - "import": "Importuoti valdiklių rinknį", - "export": "Eksportuoti valdiklių rinkinį", - "export-widgets-bundle-widgets-prompt": "Valdiklių rinkinių tipus įtraukti į eksportuotus duomenis (kitu atveju bus eksportuojami tik nurodytų valdiklių tipų FQN)", - "export-failed-error": "Valdiklių rinkinio eksportuoti nepavyko: {{error}}", - "create-new-widgets-bundle": "Sukurti naują valdiklių rinkinį", - "widgets-bundle-file": "Valdiklių rinkinio failas", - "invalid-widgets-bundle-file-error": "Valdiklių rinkinio importuoti nepavyko: neteisinga duomenš struktūra.", - "search": "Valdiklių rinkinių paieška", - "selected-widgets-bundles": "Pasirinkta { count, plural, =1 {1 valdiklių rinkinys} other {# valdiklių rinkiniai} }", - "open-widgets-bundle": "Atverti valdiklių rinkinį", - "loading-widgets-bundles": "Įkeliamas valdiklių rinkinys..." - }, - "widget-config": { - "data": "Duomenys", - "settings": "Nustatymai", - "advanced": "Išplėstiniai nustatymai", - "appearance": "Išvaizda", - "widget-card": "Valdiklio kortelė", - "mobile": "Mobilusis vaizdas", - "title": "Pavadinimas", - "title-tooltip": "Pavadinimo paaiškinimas", - "general-settings": "Pagrindiniai nustatymai", - "display-title": "Rodyti valdiklio pavadinimą", - "card-title": "Kortelės pavadinimas", - "drop-shadow": "Šešėlis", - "enable-fullscreen": "Įjungti rodymą per visą ekraną", - "enable-data-export": "Įjungti duomenų eksportą", - "data-export": "Duomenų eksportas", - "background-color": "Fono spalva", - "text-color": "Teksto spalva", - "border-radius": "Rėmelio storis", - "padding": "Padding", - "margin": "Margin", - "widget-style": "Valdiklio stilius", - "widget-css": "Valdiklio CSS", - "title-style": "Antraštės stilius", - "mobile-mode-settings": "Mobiliojo vaizdo režimas", - "order": "Rikiavimo tvarka", - "height": "Aukštis", - "mobile-hide": "Valdiklį paslėpti mobiliojo vaizdo režime", - "desktop-hide": "Slėpti valdiklį darbalaukio režime", - "units": "Papildomas simbolis šalia reikšmės", - "units-by-default": "Numatytieji matavimo vienetai", - "decimals": "Skaimenys po kablelio", - "decimals-by-default": "Dešimtainiai skaičiai pagal numatytuosius nustatymus", - "default-data-key-parameter-hint": "Šis parametras taikomas visoms valdiklių reikšmėms, nebent jis nustatomas duomenų rakto konfigūracijoje", - "units-short": "Vienetai", - "decimals-short": "Dešimtainiai skaičiai", - "decimals-suffix": "dešimtainiai skaičiai", - "timewindow": "Laikotarpio langas", - "use-dashboard-timewindow": "Naudoti skydelio laikotarpio langą", - "use-widget-timewindow": "Naudoti valdiklio laikotarpio langą", - "display-timewindow": "Rodyti laikotarpio langą", - "legend": "Legenda", - "display-legend": "Rodyti legendą", - "datasources": "Duomenų šaltiniai", - "datasource": "Duomenų šaltinis", - "maximum-datasources": "Daugiausiai leidžiama { count, plural, =1 {1 vienas duomenų šaltinis.} other {# duomenų šaltiniai} }", - "timeseries-key-error": "Reikia nurodyti nors vieną telemetrijos rodiklį", - "datasource-type": "Tipas", - "datasource-parameters": "Parametrai", - "remove-datasource": "Pašalinti duomenų šaltinį", - "add-datasource": "Pridėti duomenų šaltinį", - "target-device": "Tikslinis įrenginys", - "alarm-source": "Įspėjimo šaltinis", - "actions": "Veiksmai", - "action": "Veiksmas", - "add-action": "Pridėti veiksmą", - "search-actions": "Veiksmų paieška", - "no-actions-text": "Veiksmų nėra", - "action-source": "Veiksmo šaltinis", - "action-source-required": "Veiksmo šaltinis būtinas.", - "action-name": "Pavadinimas", - "action-name-required": "Veiksmo pavadinimas būtinas.", - "action-name-not-unique": "Veiksmas su tokiu pavadinimu jau yra.
    Šaltinyje veiksmų pavadinimai kartotis negali.", - "action-icon": "Piktograma", - "show-hide-action-using-function": "Rodyti/slėpti veiksmą funkcijos pagalba", - "action-type": "Tipas", - "action-type-required": "Veiksmo tipas būtinas.", - "edit-action": "Redaguoti veiksmą", - "delete-action": "Panaikinti veiksmą", - "delete-action-title": "Panaikinti valdiklio veiksmą", - "delete-action-text": "Ar tikrai norite panaikinti valdiklio veiksmą '{{actionName}}'?", - "title-icon": "Antraštės piktograma", - "display-icon": "Rodyti antraštės piktoramą", - "card-icon": "Kortelės piktograma", - "icon-color": "Piktogramos spalva", - "icon-size": "Piktogramos dydis", - "advanced-settings": "Išplėstiniai nustatymai", - "data-settings": "Duomenų nustatymai", - "limits": "Ribos", - "no-data-display-message": "\"Nėra duomenų\" alternatyvus tekstas", - "data-page-size": "Maximum entities per datasource", - "settings-component-not-found": "Settings form component not found for selector '{{selector}}'", - "preview": "Preview", - "set": "Set", - "set-message": "Set message", - "advanced-title-style": "Advanced title style", - "card-style": "Card style", - "text": "Text", - "background": "Background", - "advanced-widget-style": "Advanced widget style", - "card-buttons": "Card buttons", - "show-card-buttons": "Show card buttons", - "card-border-radius": "Card border radius", - "card-appearance": "Card appearance", - "color": "Color" - }, - "widget-type": { - "import": "Importuoti valdiklį", - "export": "Eksportuoti valdiklį", - "export-widget-types": "Eksportuoti valdiklius", - "export-failed-error": "Valdiklio eksportuoti nepavyko: {{error}}", - "create-new-widget-type": "Sukurt naują valdiklį", - "widget-type-file": "Valdiklių failas", - "invalid-widget-type-file-error": "Nepavyko importuoti valdiklio: neteisinga valdiklio duomenų struktūra." - }, - "self-registration": { - "self-registration": "Self Registration", - "self-registration-url": "Self Registration URL", - "captcha-version": "reCAPTCHA version", - "captcha-action": "reCAPTCHA log action name", - "captcha-site-key": "reCAPTCHA site key", - "captcha-secret-key": "reCAPTCHA secret key", - "notification-email": "El. pašto adresas pranešimų siuntimui", - "notification-email-invalid": "Neteisingas el. pašto adreso formatas.", - "notification-email-required": "El. pašto adresas pranešimų siuntimui būtinas.", - "privacy-policy-text": "Privatumo politikos tekstas", - "terms-of-use-text": "Terms of Use text", - "text-message-page": "Tekstinė žinutė registracijos puslapiui", - "enable-mobile-self-registration": "Enable self registration from mobile application", - "mobile-self-registration-title": "Mobile application self registration settings", - "mobile-package": "Application package", - "mobile-package-placeholder": "Ex.: my.example.app", - "mobile-package-hint": "For Android: your own unique Application ID. For iOS: Product bundle identifier.", - "mobile-package-required": "Application package is required", - "mobile-app-secret": "Application secret", - "invalid-mobile-app-secret": "Application secret must contain only alphanumeric characters and must be between 16 and 2048 characters long.", - "copy-mobile-app-secret": "Copy application secret", - "mobile-app-scheme": "Application URL scheme", - "mobile-app-scheme-placeholder": "Ex.: customscheme", - "mobile-app-scheme-required": "Application URL scheme is required", - "mobile-app-host": "Application URL hostname", - "mobile-app-host-placeholder": "Ex.: my.app.host", - "mobile-app-host-required": "Application URL hostname is required", - "show-privacy-policy": "Show Privacy Policy", - "show-terms-of-use": "Show Terms of Use", - "domain-settings": "Domain settings", - "general-settings": "General settings" - }, - "solution-template": { - "solution-template": "Solution template", - "solution-templates": "Solution templates", - "management": "Manage solution templates", - "details": "Details", - "install": "Install", - "level": "Level", - "install-title": "Solution template successfully installed", - "install-failed-title": "Solution template installation failed", - "instructions": "Instructions", - "goto-main-dashboard": "Goto main dashboard", - "delete": "Delete", - "delete-solution-title": "Are you sure you want to delete the solution '{{solutionTitle}}'?", - "delete-solution-text": "Be careful, after the confirmation the solution and all related data will become unrecoverable.", - "installing": "Installing solution template..." + "widget-font": { + "font-settings": "Šrifto nustatymai", + "font-family": "Šrifto šeima", + "size": "Dydis", + "relative-font-size": "Santykinis šrifto dydis (procentais)", + "font-style": "Stilius", + "font-style-normal": "Normalus", + "font-style-italic": "Kursyvas", + "font-style-oblique": "Pasviręs", + "font-weight": "Storis", + "font-weight-normal": "Normalus", + "font-weight-bold": "Paryškintas", + "font-weight-bolder": "Labiau paryškintas", + "font-weight-lighter": "Plonesnis", + "color": "Spalva", + "shadow-color": "Šešėlio spalva", + "preview": "Peržiūra", + "line-height": "Eilučių aukštis", + "auto": "Automatinis" }, - "markdown": { - "edit": "Edit", - "preview": "Preview", - "copy-code": "Click to copy", - "copied": "Copied!" - }, - "white-labeling": { - "white-labeling": "Tinkinimas", - "white-labeling-general": "General White Labeling", - "login-white-labeling": "Prisijungimo puslapio tinkinimas", - "general": "General", - "login": "Prisijungti", - "preview": "Peržiūra", - "app-title": "Aplikaciijios pavadinimas", - "favicon": "Aplikacijos piktograma", - "favicon-description": "*.ico, *.gif ar *.png paveiksliukas, kurio maksimalus dydis {{kbSize}} kilobaitai.", - "favicon-size-error": "Paveiksliukas per didelis. Maksimalus leistinas dydis yra {{kbSize}} kilobaitai.", - "favicon-type-error": "Netinkamas paveiksliuko formatas. Leidžiami tik ICO, GIF ar PNG formatai.", - "drop-favicon-image": "Užvilkite svetainės piktogramos paveiksliuką arba spustelėkite, kad pasirinktumėte failą, kurį norite įkelti.", - "drop-favicon-image-or": "Drag and drop a website icon image or", - "no-favicon-image": "Piktograma nepasirinkta", - "logo": "Logo", - "logo-description": "Bet koks paveiksliukas, kurio dydis neviršija {{kbSize}} kilobaitų.", - "logo-size-error": "Logotipas per didelis. Maksimalus leistinas dydis yra {{kbSize}} kilobaitai.", - "logo-type-error": "Netinkamas logotipo failo. Only images are accepted.", - "drop-logo-image": "Užvilkite svetainės logotipo paveiksliuką arba spustelėkite, kad pasirinktumėte failą, kurį norite įkelti.", - "drop-logo-image-or": "Drag and drop a logo image or", - "no-logo-image": "Logotipas nepasirinktas", - "logo-height": "Logotipo aukštis, px", - "primary-palette": "Pirminė paletė", - "accent-palette": "Akcentų paletė", - "customize-palette": "Pritaikyti", - "advanced-css": "Advanced CSS", - "edit-palette": "Redaguoti paletę", - "save-palette": "Išsaugoti paletę", - "primary-background": "Pirminis fonas", - "secondary-background": "Antrinis fonas", - "hue1": "HUE 1", - "hue2": "HUE 2", - "hue3": "HUE 3", - "page-background-color": "Puslapio fono spalva", - "dark-foreground": "Tamsi tema", - "domain-name": "Domeno pavadinimas", - "base-url": "Bazinis URL", - "prohibit-different-url": "Prohibit to use hostname from the client request headers", - "prohibit-different-url-hint": "This setting should be enabled for production environments. May cause security issues when disabled", - "help-link-base-url": "Bazinis pagalbos puslapio url", - "ui-help-base-url": "UI help base url", - "enable-help-links": "Įjungti pagalbos nuorodas", - "error-verification-url": "Domeno pavadinime negali būti '/' ir ':' simbolių. Pavyzdžiui: thingsboard.io", - "show-platform-name-version": "Rodyti platformos pavadinimą ir versiją", - "platform-name": "Platformas pavadinimas", - "platform-version": "Platformos versija", - "version-mask": "{{name}} v.{{version}}", - "position": { - "label": "Platformos pavadinimo ir versijos išdėstymas", - "under-logo": "Po logotipu", - "bottom": "Prisijungimo formos apačioje" - } + "home": { + "no-data-available": "Nėra prieinamų duomenų" }, - "widgets": { - "background": { - "background": "Background", - "background-settings": "Background settings", - "background-type-image": "Upload image", - "background-type-image-url": "Image URL", - "background-type-color": "Solid color", - "image-url": "Image URL", - "overlay": "Overlay", - "enable-overlay": "Enable overlay", - "blur": "Blur", - "preview": "Preview" - }, - "battery-level": { - "layout": "Layout", - "layout-vertical-solid": "Vertical. Solid", - "layout-horizontal-solid": "Horizontal. Solid", - "layout-vertical-divided": "Vertical. Divided", - "layout-horizontal-divided": "Horizontal. Divided", - "icon": "Icon", - "value": "Value", - "auto-scale": "Auto scale", - "battery-level-color": "Battery level color", - "battery-shape-color": "Battery shape color", - "battery-level-card-style": "Battery level card style" - }, - "chart": { - "common-settings": "Common settings", - "enable-stacking-mode": "Enable stacking mode", - "selection": "Time range selection", - "enable-selection-mode": "Enable selection mode", - "line-shadow-size": "Line shadow size", - "display-smooth-lines": "Display smooth (curved) lines", - "default-bar-width": "Default bar width for non-aggregated data (milliseconds)", - "bar-alignment": "Bar alignment", - "bar-alignment-left": "Left", - "bar-alignment-right": "Right", - "bar-alignment-center": "Center", - "default-font": "Default font", - "default-font-size": "Default font size", - "default-font-color": "Default font color", - "thresholds-line-width": "Default line width for all thresholds", - "tooltip-settings": "Tooltip settings", - "tooltip": "Tooltip", - "show-tooltip": "Show tooltip", - "hover-individual-points": "Hover individual points", - "show-cumulative-values": "Show cumulative values in stacking mode", - "hide-zero-false-values": "Hide zero/false values from tooltip", - "tooltip-value-format-function": "Tooltip value format function", - "grid-settings": "Grid settings", - "show-vertical-lines": "Show vertical lines", - "show-horizontal-lines": "Show horizontal lines", - "grid-outline-border-width": "Grid outline/border width (px)", - "primary-color": "Primary color", - "background-color": "Background color", - "ticks-color": "Ticks color", - "xaxis-settings": "X axis settings", - "axis-title": "Axis title", - "xaxis-tick-labels-settings": "X axis tick labels settings", - "show-tick-labels": "Show axis tick labels", - "yaxis-settings": "Y axis settings", - "min-scale-value": "Minimum value on the scale", - "max-scale-value": "Maximum value on the scale", - "yaxis-tick-labels-settings": "Y axis tick labels settings", - "tick-step-size": "Step size between ticks", - "number-of-decimals": "The number of decimals to display", - "ticks-formatter-function": "Ticks formatter function", - "comparison-settings": "Comparison settings", - "enable-comparison": "Enable comparison", - "time-for-comparison": "Comparison period", - "time-for-comparison-previous-interval": "Previous interval (default)", - "time-for-comparison-days": "Day ago", - "time-for-comparison-weeks": "Week ago", - "time-for-comparison-months": "Month ago", - "time-for-comparison-years": "Year ago", - "time-for-comparison-custom-interval": "Custom interval", - "custom-interval-value": "Custom interval value (ms)", - "comparison-x-axis-settings": "Comparison X axis settings", - "axis-position": "Axis position", - "axis-position-top": "Top (default)", - "axis-position-bottom": "Bottom", - "custom-legend-settings": "Custom legend settings", - "enable-custom-legend": "Enable custom legend (this will allow you to use attribute/timeseries values in key labels)", - "key-name": "Key name", - "key-name-required": "Key name is required", - "key-type": "Key type", - "key-type-attribute": "Attribute", - "key-type-timeseries": "Timeseries", - "label-keys-list": "Keys list to use in labels", - "no-label-keys": "No keys configured", - "add-label-key": "Add new key", - "line-width": "Line width", - "color": "Color", - "data-is-hidden-by-default": "Data is hidden by default", - "disable-data-hiding": "Disable data hiding", - "remove-from-legend": "Remove datakey from legend", - "exclude-from-stacking": "Exclude from stacking(available in \"Stacking\" mode)", - "line-settings": "Line settings", - "show-line": "Show line", - "fill-line": "Fill line", - "fill-line-opacity": "Fill opacity", - "points-settings": "Points settings", - "show-points": "Show points", - "points-line-width": "Line width of points", - "points-radius": "Radius of points", - "point-shape": "Point shape", - "point-shape-circle": "Circle", - "point-shape-cross": "Cross", - "point-shape-diamond": "Diamond", - "point-shape-square": "Square", - "point-shape-triangle": "Triangle", - "point-shape-custom": "Custom function", - "point-shape-draw-function": "Point shape draw function", - "show-separate-axis": "Show separate axis", - "axis-position-left": "Left", - "axis-position-right": "Right", - "thresholds": "Thresholds", - "no-thresholds": "No thresholds configured", - "add-threshold": "Add threshold", - "show-values-for-comparison": "Show historical values for comparison", - "comparison-values-label": "Historical values label", - "comparison-line-color": "Comparison line color", - "threshold-settings": "Threshold settings", - "use-as-threshold": "Use key value as threshold", - "threshold-line-width": "Threshold line width", - "threshold-color": "Threshold color", - "common-pie-settings": "Common pie settings", - "radius": "Radius", - "inner-radius": "Inner radius", - "tilt": "Tilt", - "common-pie-settings-range-error": "Value should be in range from 0 to 1", - "stroke-settings": "Stroke settings", - "width-pixels": "Width (pixels)", - "show-labels": "Show labels", - "animation-settings": "Animation settings", - "animated-pie": "Enable pie animation (experimental)", - "border-settings": "Border settings", - "border-width": "Border width", - "border-color": "Border color", - "legend-settings": "Legend settings", - "display-legend": "Display legend", - "labels-font-color": "Labels font color", - "series": "Series", - "add-series": "Add series", - "series-settings": "Series settings", - "remove-series": "Remove series", - "no-series": "No series configured", - "no-series-error": "At least one series should be specified", - "chart-appearance": "Chart appearance", - "vertical-grid-lines": "Vertical grid lines", - "horizontal-grid-lines": "Horizontal grid lines", - "chart-background": "Chart background", - "grid-lines-color": "Grid lines color", - "border": "Border", - "axis": "Axis", - "vertical-axis": "Vertical axis", - "ticks": "Ticks", - "horizontal-axis": "Horizontal axis" - }, - "color": { - "color-settings": "Color settings", - "color-type-constant": "Constant", - "color-type-range": "Range", - "color-type-function": "Function", - "color": "Color", - "value-range": "Value range", - "from": "From", - "to": "To", - "color-function": "Color function" - }, - "dashboard-state": { - "dashboard-state-settings": "Dashboard state settings", - "dashboard-state": "Dashboard state id", - "autofill-state-layout": "Autofill state layout height by default", - "default-margin": "Default widgets margin", - "default-background-color": "Default background color", - "sync-parent-state-params": "Sync state params with parent dashboard" - }, - "date-range-navigator": { - "date-range-picker-settings": "Date range picker settings", - "hide-date-range-picker": "Hide date range picker", - "picker-one-panel": "Date range picker one panel", - "picker-auto-confirm": "Date range picker auto confirm", - "picker-show-template": "Date range picker show template", - "first-day-of-week": "First day of the week", - "interval-settings": "Interval settings", - "hide-interval": "Hide interval", - "initial-interval": "Initial interval", - "interval-hour": "Hour", - "interval-day": "Day", - "interval-week": "Week", - "interval-two-weeks": "2 weeks", - "interval-month": "Month", - "interval-three-months": "3 months", - "interval-six-months": "6 months", - "step-settings": "Step settings", - "hide-step-size": "Hide step size", - "initial-step-size": "Initial step size", - "hide-labels": "Hide labels", - "use-session-storage": "Use session storage", - "localizationMap": { - "Sun": "Sun", - "Mon": "Mon", - "Tue": "Tue", - "Wed": "Wed", - "Thu": "Thu", - "Fri": "Fri", - "Sat": "Sat", - "Jan": "Jan", - "Feb": "Feb", - "Mar": "Mar", - "Apr": "Apr", - "May": "May", - "Jun": "Jun", - "Jul": "Jul", - "Aug": "Aug", - "Sep": "Sep", - "Oct": "Oct", - "Nov": "Nov", - "Dec": "Dec", - "January": "January", - "February": "February", - "March": "March", - "April": "April", - "June": "June", - "July": "July", - "August": "August", - "September": "September", - "October": "October", - "November": "November", - "December": "December", - "Custom Date Range": "Custom Date Range", - "Date Range Template": "Date Range Template", - "Today": "Today", - "Yesterday": "Yesterday", - "This Week": "This Week", - "Last Week": "Last Week", - "This Month": "This Month", - "Last Month": "Last Month", - "Year": "Year", - "This Year": "This Year", - "Last Year": "Last Year", - "Date picker": "Date picker", - "Hour": "Hour", - "Day": "Day", - "Week": "Week", - "2 weeks": "2 Weeks", - "Month": "Month", - "3 months": "3 Months", - "6 months": "6 Months", - "Custom interval": "Custom interval", - "Interval": "Interval", - "Step size": "Step size", - "Ok": "Ok" - } - }, - "entities-hierarchy": { - "hierarchy-data-settings": "Hierarchy data settings", - "relations-query-function": "Node relations query function", - "has-children-function": "Node has children function", - "node-state-settings": "Node state settings", - "node-opened-function": "Default node opened function", - "node-disabled-function": "Node disabled function", - "display-settings": "Display settings", - "node-icon-function": "Node icon function", - "node-text-function": "Node text function", - "sort-settings": "Sort settings", - "nodes-sort-function": "Nodes sort function" - }, - "edge": { - "display-default-title": "Display default title" - }, - "gateway": { - "general-settings": "General settings", - "widget-title": "Widget title", - "default-archive-file-name": "Default archive file name", - "device-type-for-new-gateway": "Device type for new gateway", - "messages-settings": "Messages settings", - "save-config-success-message": "Text message about successfully saved gateway configuration", - "device-name-exists-message": "Text message when device with entered name is already exists", - "gateway-title": "Gateway form", - "read-only": "Read only", - "events-title": "Gateway events form title", - "events-filter": "Events filter", - "event-key-contains": "Event key contains...", - "show-connector": "Show for the connector", - "connector-state-param-key": "Connector state parameter key", - "message": "Message", - "created-time": "Created time" - }, - "gauge": { - "default-color": "Default color", - "radial-gauge-settings": "Radial gauge settings", - "ticks-settings": "Ticks settings", - "min-value": "Minimum value", - "max-value": "Maximum value", - "start-ticks-angle": "Start ticks angle", - "ticks-angle": "Ticks angle", - "major-ticks-count": "Major ticks count", - "major-ticks-color": "Major ticks color", - "minor-ticks-count": "Minor ticks count", - "minor-ticks-color": "Minor ticks color", - "tick-numbers-font": "Tick numbers font", - "unit-title-settings": "Unit title settings", - "show-unit-title": "Show unit title", - "unit-title": "Unit title", - "title-font": "Title text font", - "units-settings": "Units settings", - "units-font": "Units text font", - "value-box-settings": "Value box settings", - "show-value-box": "Show value box", - "value-int": "Digits count for integer part of value", - "value-font": "Value text font", - "value-box-rect-stroke-color": "Value box rectangle stroke color", - "value-box-rect-stroke-color-end": "Value box rectangle stroke color - end gradient", - "value-box-background-color": "Value box background color", - "value-box-shadow-color": "Value box shadow color", - "plate-settings": "Plate settings", - "show-plate-border": "Show plate border", - "plate-color": "Plate color", - "needle-settings": "Needle settings", - "needle-circle-size": "Needle circle size", - "needle-color": "Needle color", - "needle-color-end": "Needle color - end gradient", - "needle-color-shadow-up": "Upper half of the needle shadow color", - "needle-color-shadow-down": "Drop shadow needle color", - "highlights-settings": "Highlights settings", - "highlights-width": "Highlights width", - "highlights": "Highlights", - "highlight-from": "From", - "highlight-to": "To", - "highlight-color": "Color", - "no-highlights": "No highlights configured", - "add-highlight": "Add highlight", - "animation-settings": "Animation settings", - "enable-animation": "Enable animation", - "animation-duration": "Animation duration", - "animation-rule": "Animation rule", - "animation-linear": "Linear", - "animation-quad": "Quad", - "animation-quint": "Quint", - "animation-cycle": "Cycle", - "animation-bounce": "Bounce", - "animation-elastic": "Elastic", - "animation-dequad": "Dequad", - "animation-dequint": "Dequint", - "animation-decycle": "Decycle", - "animation-debounce": "Debounce", - "animation-delastic": "Delastic", - "linear-gauge-settings": "Linear gauge settings", - "bar-stroke-width": "Bar stroke width", - "bar-stroke-color": "Bar stroke color", - "bar-background-color": "Gauge bar background color", - "bar-background-color-end": "Bar background color - end gradient", - "progress-bar-color": "Progress bar color", - "progress-bar-color-end": "Progress bar color - end gradient", - "major-ticks-names": "Major ticks names", - "show-stroke-ticks": "Show ticks stroke", - "major-ticks-font": "Major ticks font", - "border-color": "Border color", - "border-width": "Border width", - "needle-circle-color": "Needle circle color", - "animation-target": "Animation target", - "animation-target-needle": "Needle", - "animation-target-plate": "Plate", - "common-settings": "Common gauge settings", - "gauge-type": "Gauge type", - "gauge-type-arc": "Arc", - "gauge-type-donut": "Donut", - "gauge-type-horizontal-bar": "Horizontal bar", - "gauge-type-vertical-bar": "Vertical bar", - "donut-start-angle": "Angle to start from", - "bar-settings": "Gauge bar settings", - "relative-bar-width": "Relative bar width", - "neon-glow-brightness": "Neon glow effect brightness, (0-100), 0 - disable effect", - "stripes-thickness": "Thickness of the stripes, 0 - no stripes", - "rounded-line-cap": "Display rounded line cap", - "bar-color-settings": "Bar color settings", - "use-precise-level-color-values": "Use precise color levels", - "bar-colors": "Bar colors, from lower to upper", - "color": "Color", - "no-bar-colors": "No bar colors configured", - "add-bar-color": "Add bar color", - "from": "From", - "to": "To", - "fixed-level-colors": "Bar colors using boundary values", - "gauge-title-settings": "Gauge title settings", - "show-gauge-title": "Show gauge title", - "gauge-title": "Gauge title", - "gauge-title-font": "Gauge title font", - "unit-title-and-timestamp-settings": "Unit title and timestamp settings", - "show-timestamp": "Show value timestamp", - "timestamp-format": "Timestamp format", - "label-font": "Font of label showing under value", - "value-settings": "Value settings", - "show-value": "Show value text", - "min-max-settings": "Minimum/maximum labels settings", - "show-min-max": "Show min and max values", - "min-max-font": "Font of minimum and maximum labels", - "show-ticks": "Show ticks", - "tick-width": "Tick width", - "tick-color": "Tick color", - "tick-values": "Tick values", - "no-tick-values": "No tick values configured", - "add-tick-value": "Add tick value" - }, - "gpio": { - "pin": "Pin", - "label": "Label", - "row": "Row", - "column": "Column", - "color": "Color", - "panel-settings": "Panel settings", - "background-color": "Background color", - "gpio-switches": "GPIO switches", - "no-gpio-switches": "No GPIO switches configured", - "add-gpio-switch": "Add GPIO switch", - "gpio-status-request": "GPIO status request", - "method-name": "Method name", - "method-body": "Method body", - "gpio-status-change-request": "GPIO status change request", - "parse-gpio-status-function": "Parse gpio status function", - "gpio-leds": "GPIO leds", - "no-gpio-leds": "No GPIO leds configured", - "add-gpio-led": "Add GPIO led" - }, - "html-card": { - "html": "HTML", - "css": "CSS" - }, - "input-widgets": { - "attribute-not-allowed": "Attribute parameter cannot be used in this widget", - "blocked-location": "Geolocation is blocked in your browser", - "claim-device": "Claim device", - "claim-failed": "Failed to claim the device!", - "claim-not-found": "Device not found!", - "claim-successful": "Device was successfully claimed!", - "date": "Date", - "device-name": "Device name", - "device-name-required": "Device name is required", - "discard-changes": "Discard changes", - "entity-attribute-required": "Entity attribute is required", - "entity-coordinate-required": "Both fields, latitude and longitude, are required", - "entity-timeseries-required": "Entity timeseries is required", - "get-location": "Get current location", - "invalid-date": "Invalid Date", - "latitude": "Latitude", - "longitude": "Longitude", - "min-value-error": "Min value is {{value}}", - "max-value-error": "Max value is {{value}}", - "not-allowed-entity": "Selected entity cannot have shared attributes", - "no-attribute-selected": "No attribute is selected", - "no-datakey-selected": "No datakey is selected", - "no-coordinate-specified": "Datakey for latitude/longitude doesn't specified", - "no-entity-selected": "No entity selected", - "no-image": "No image", - "no-support-geolocation": "Your browser doesn't support geolocation", - "no-support-web-camera": "Your browser does not support cameras", - "enable-https-use-widget": "Please enable HTTPS to use this widget", - "no-found-your-camera": "Can't find your camera", - "no-permission-camera": "Permission was denied by the user / This site doesn't have permission to use the camera", - "no-timeseries-selected": "No timeseries selected", - "secret-key": "Secret key", - "secret-key-required": "Secret key is required", - "switch-attribute-value": "Switch entity attribute value", - "switch-camera": "Switch camera", - "switch-timeseries-value": "Switch entity timeseries value", - "take-photo": "Take photo", - "time": "Time", - "timeseries-not-allowed": "Timeseries parameter cannot be used in this widget", - "update-failed": "Update failed", - "update-successful": "Update successful", - "update-attribute": "Update attribute", - "update-timeseries": "Update timeseries", - "value": "Value", - "general-settings": "General settings", - "widget-title": "Widget title", - "claim-button-label": "Claiming button label", - "show-secret-key-field": "Show 'Secret key' input field", - "labels-settings": "Labels settings", - "show-labels": "Show labels", - "device-name-label": "Label for device name input field", - "secret-key-label": "Label for secret key input field", - "messages-settings": "Messages settings", - "claim-device-success-message": "Text message of successful device claiming", - "claim-device-not-found-message": "Text message when device not found", - "claim-device-failed-message": "Text message of failed device claiming", - "claim-device-name-required-message": "'Device name required' error message", - "claim-device-secret-key-required-message": "'Secret key required' error message", - "relations-settings": "Relations settings", - "relate-device-to-state-entity": "Relate device to current state entity", - "relate-direction": "Relate direction", - "relate-direction-from": "From", - "relate-direction-to": "To", - "relation-type": "Relation type", - "show-label": "Show label", - "label": "Label", - "required": "Required", - "required-error-message": "'Required' error message", - "show-result-message": "Show result message", - "integer-field-settings": "Integer field settings", - "min-value": "Min value", - "max-value": "Max value", - "double-field-settings": "Double field settings", - "text-field-settings": "Text field settings", - "min-length": "Min length", - "max-length": "Max length", - "checkbox-settings": "Checkbox settings", - "true-label": "Checked label", - "false-label": "Unchecked label", - "image-input-settings": "Image input settings", - "display-preview": "Display preview", - "display-clear-button": "Display clear button", - "display-apply-button": "Display apply button", - "display-discard-button": "Display discard button", - "datetime-field-settings": "Date/time field settings", - "display-time-input": "Display time input", - "latitude-key-name": "Latitude key name", - "longitude-key-name": "Longitude key name", - "show-get-location-button": "Show button 'Get current location'", - "use-high-accuracy": "Use high accuracy", - "location-fields-settings": "Location fields settings", - "latitude-label": "Label for latitude", - "longitude-label": "Label for longitude", - "input-fields-alignment": "Input fields alignment", - "input-fields-alignment-column": "Column (default)", - "input-fields-alignment-row": "Row", - "layout": "Layout", - "row-gap": "Gap between rows in pixels", - "column-gap": "Gap between columns in pixels", - "latitude-field-required": "Latitude field required", - "longitude-field-required": "Longitude field required", - "attribute-settings": "Attribute settings", - "widget-mode": "Widget mode", - "widget-mode-update-attribute": "Update attribute", - "widget-mode-update-timeseries": "Update timeseries", - "attribute-scope": "Attribute scope", - "attribute-scope-server": "Server attribute", - "attribute-scope-shared": "Shared attribute", - "value-required": "Value required", - "image-settings": "Image settings", - "image-format": "Image format", - "image-format-jpeg": "JPEG", - "image-format-png": "PNG", - "image-format-webp": "WEBP", - "image-quality": "Image quality that use lossy compression such as jpeg and webp", - "max-image-width": "Maximum image width", - "max-image-height": "Maximum image height", - "action-buttons": "Action buttons", - "show-action-buttons": "Show action buttons", - "update-all-values": "Update all values, not only modified", - "save-button-label": "'SAVE' button label", - "reset-button-label": "'UNDO' button label", - "group-settings": "Group settings", - "show-group-title": "Show title for group of fields, related to different entities", - "group-title": "Group title", - "fields-alignment": "Fields alignment", - "fields-alignment-row": "Row (default)", - "fields-alignment-column": "Column", - "fields-in-row": "Number of fields in the row", - "option-value": "Value (write 'null' for create empty option)", - "option-label": "Label", - "hide-input-field": "Hide input field", - "datakey-type": "Datakey type", - "datakey-type-server": "Server attribute (default)", - "datakey-type-shared": "Shared attribute", - "datakey-type-timeseries": "Timeseries", - "datakey-value-type": "Datakey value type", - "datakey-value-type-string": "String", - "datakey-value-type-double": "Double", - "datakey-value-type-integer": "Integer", - "datakey-value-type-json": "JSON", - "datakey-value-type-boolean-checkbox": "Boolean (Checkbox)", - "datakey-value-type-boolean-switch": "Boolean (Switch)", - "datakey-value-type-date-time": "Date & Time", - "datakey-value-type-date": "Date", - "datakey-value-type-time": "Time", - "datakey-value-type-select": "Select", - "datakey-value-type-color": "Color", - "value-is-required": "Value is required", - "ability-to-edit-attribute": "Ability to edit attribute", - "ability-to-edit-attribute-editable": "Editable (default)", - "ability-to-edit-attribute-disabled": "Disabled", - "ability-to-edit-attribute-readonly": "Read-only", - "disable-on-datakey-name": "Disable on false value of another datakey (specify datakey name)", - "field-appearance": "Field appearance", - "appearance-fill": "Fill", - "appearance-outline": "Outline", - "subscript-sizing": "Subscript sizing", - "subscript-sizing-fixed": "Fixed", - "subscript-sizing-dynamic": "Dynamic", - "slide-toggle-settings": "Slide toggle settings", - "slide-toggle-label-position": "Slide toggle label position", - "slide-toggle-label-position-after": "After", - "slide-toggle-label-position-before": "Before", - "select-options": "Select options", - "no-select-options": "No select options configured", - "add-select-option": "Add select option", - "numeric-field-settings": "Numeric field settings", - "step-interval": "Step interval between values", - "error-messages": "Error messages", - "min-value-error-message": "'Min value' error message", - "max-value-error-message": "'Max value' error message", - "invalid-date-error-message": "'Invalid date' error message", - "invalid-JSON-error-message": "'Invalid JSON' error message", - "icon-settings": "Icon settings", - "dialog-editor-settings": "Dialog editor settings", - "use-custom-icon": "Use custom icon", - "input-cell-icon": "Icon to show before input cell", - "value-conversion-settings": "Value conversion settings", - "get-value-settings": "Get value settings", - "use-get-value-function": "Use getValue function", - "get-value-function": "getValue function", - "set-value-settings": "Set value settings", - "use-set-value-function": "Use setValue function", - "set-value-function": "setValue function", - "json-invalid": "JSON value has an invalid format", - "title": "Title", - "cancel-button-label": "'Cancel' button label" - }, - "invalid-qr-code-text": "Invalid input text for QR code. Input should have a string type", - "qr-code": { - "use-qr-code-text-function": "Use QR code text function", - "qr-code-text-pattern": "QR code text pattern (for ex. '${entityName} | ${keyName} - some text.')", - "qr-code-text-pattern-hint": "QR code text pattern use the value of the first found key in the entities in the entity alias.", - "qr-code-text-pattern-required": "QR code text pattern is required.", - "qr-code-text-function": "QR code text function" - }, - "label-widget": { - "label-pattern": "Pattern", - "label-pattern-hint": "Hint: for ex. 'Text ${keyName} units.' or ${#<key index>} units'", - "label-pattern-required": "Pattern is required", - "label-position": "Position (Percentage relative to background)", - "x-pos": "X", - "y-pos": "Y", - "background-color": "Background color", - "font-settings": "Font settings", - "background-image": "Background image", - "labels": "Labels", - "no-labels": "No labels configured", - "add-label": "Add label" - }, - "navigation": { - "title": "Title", - "navigation-path": "Navigation path", - "filter-type": "Filter type", - "filter-type-all": "All items", - "filter-type-include": "Include items", - "filter-type-exclude": "Exclude items", - "items": "Items", - "enter-urls-to-filter": "Enter urls to filter..." - }, - "persistent-table": { - "rpc-id": "RPC ID", - "message-type": "Message type", - "method": "Method", - "params": "Params", - "created-time": "Created time", - "expiration-time": "Expiration time", - "retries": "Retries", - "status": "Status", - "filter": "Filter", - "refresh": "Refresh", - "add": "Add RPC request", - "details": "Details", - "delete": "Delete", - "delete-request-title": "Delete Persistent RPC request", - "delete-request-text": "Are you sure you want to delete request?", - "details-title": "Details RPC ID: ", - "additional-info": "Additional info", - "response": "Response", - "any-status": "Any status", - "rpc-status-list": "RPC status list", - "no-request-prompt": "No request to display", - "send-request": "Send request", - "add-title": "Create Persistent RPC request", - "method-error": "Method is required.", - "timeout-error": "Min timeout value is 5000 (5 seconds).", - "white-space-error": "White space is not allowed.", - "rpc-status": { - "QUEUED": "QUEUED", - "SENT": "SENT", - "DELIVERED": "DELIVERED", - "SUCCESSFUL": "SUCCESSFUL", - "TIMEOUT": "TIMEOUT", - "EXPIRED": "EXPIRED", - "FAILED": "FAILED" - }, - "rpc-search-status-all": "ALL", - "message-types": { - "false": "Two-way", - "true": "One-way" - }, - "general-settings": "General settings", - "enable-filter": "Enable filter", - "enable-sticky-header": "Display header while scrolling", - "enable-sticky-action": "Display actions column while scrolling", - "display-request-details": "Display request details", - "allow-send-request": "Allow send RPC request", - "allow-delete-request": "Allow delete request", - "columns-settings": "Columns settings", - "display-columns": "Columns to display", - "column": "Column", - "no-columns-found": "No columns found", - "no-columns-matching": "'{{column}}' not found." - }, - "rpc": { - "value-settings": "Value settings", - "initial-value": "Initial value", - "retrieve-value-settings": "Retrieve on/off value settings", - "retrieve-value-method": "Retrieve value using method", - "retrieve-value-method-none": "Don't retrieve", - "retrieve-value-method-rpc": "Call RPC get value method", - "retrieve-value-method-attribute": "Subscribe for attribute", - "retrieve-value-method-timeseries": "Subscribe for timeseries", - "attribute-value-key": "Attribute key", - "timeseries-value-key": "Timeseries key", - "get-value-method": "RPC get value method", - "parse-value-function": "Parse value function", - "update-value-settings": "Update value settings", - "set-value-method": "RPC set value method", - "convert-value-function": "Convert value function", - "rpc-settings": "RPC settings", - "request-timeout": "RPC request timeout (ms)", - "persistent-rpc-settings": "Persistent RPC settings", - "request-persistent": "RPC request persistent", - "persistent-polling-interval": "Polling interval (ms) to get persistent RPC command response", - "common-settings": "Common settings", - "switch-title": "Switch title", - "show-on-off-labels": "Show on/off labels", - "slide-toggle-label": "Slide toggle label", - "label-position": "Label position", - "label-position-before": "Before", - "label-position-after": "After", - "slider-color": "Slider color", - "slider-color-primary": "Primary", - "slider-color-accent": "Accent", - "slider-color-warn": "Warn", - "button-style": "Button style", - "button-raised": "Raised button", - "button-primary": "Primary color", - "button-background-color": "Button background color", - "button-text-color": "Button text color", - "widget-title": "Widget title", - "button-label": "Button label", - "device-attribute-scope": "Device attribute scope", - "server-attribute": "Server attribute", - "shared-attribute": "Shared attribute", - "device-attribute-parameters": "Device attribute parameters", - "is-one-way-command": "Is one way command", - "rpc-method": "RPC method", - "rpc-method-params": "RPC method params", - "show-rpc-error": "Show RPC command execution error", - "led-title": "LED title", - "led-color": "LED color", - "check-status-settings": "Check status settings", - "perform-rpc-status-check": "Perform RPC device status check", - "retrieve-led-status-value-method": "Retrieve led status value using method", - "led-status-value-attribute": "Device attribute containing led status value", - "led-status-value-timeseries": "Device timeseries containing led status value", - "check-status-method": "RPC check device status method", - "parse-led-status-value-function": "Parse led status value function", - "knob-title": "Knob title", - "min-value": "Minimum value", - "max-value": "Maximum value" - }, - "maps": { - "select-entity": "Select entity", - "select-entity-hint": "Hint: after selection click at the map to set position", - "tooltips": { - "placeMarker": "Click to place '{{entityName}}' entity", - "firstVertex": "Polygon for '{{entityName}}': click to place first point", - "firstVertex-cut": "Click to place first point", - "continueLine": "Polygon for '{{entityName}}': click to continue drawing", - "continueLine-cut": "Click to continue drawing", - "finishLine": "Click any existing marker to finish", - "finishPoly": "Polygon for '{{entityName}}': click first marker to finish and save", - "finishPoly-cut": "Click first marker to finish and save", - "finishRect": "Polygon for '{{entityName}}': click to finish and save", - "startCircle": "Circle for '{{entityName}}': click to place circle center", - "finishCircle": "Circle for '{{entityName}}': click to finish circle", - "placeCircleMarker": "Click to place circle marker" - }, - "actions": { - "finish": "Finish", - "cancel": "Cancel", - "removeLastVertex": "Remove last point" - }, - "buttonTitles": { - "drawMarkerButton": "Place entity", - "drawPolyButton": "Create polygon", - "drawLineButton": "Create polyline", - "drawCircleButton": "Create circle", - "drawRectButton": "Create rectangle", - "editButton": "Edit mode", - "dragButton": "Drag-drop mode", - "cutButton": "Cut polygon area", - "deleteButton": "Remove", - "drawCircleMarkerButton": "Create circle marker", - "rotateButton": "Rotate polygon" - }, - "map-provider-settings": "Map provider settings", - "map-provider": "Map provider", - "map-provider-google": "Google maps", - "map-provider-openstreet": "OpenStreet maps", - "map-provider-here": "HERE maps", - "map-provider-image": "Image map", - "map-provider-tencent": "Tencent maps", - "openstreet-provider": "OpenStreet map provider", - "openstreet-provider-mapnik": "OpenStreetMap.Mapnik (Default)", - "openstreet-provider-hot": "OpenStreetMap.HOT", - "openstreet-provider-esri-street": "Esri.WorldStreetMap", - "openstreet-provider-esri-topo": "Esri.WorldTopoMap", - "openstreet-provider-esri-imagery": "Esri.WorldImagery", - "openstreet-provider-cartodb-positron": "CartoDB.Positron", - "openstreet-provider-cartodb-dark-matter": "CartoDB.DarkMatter", - "use-custom-provider": "Use custom provider", - "custom-provider-tile-url": "Custom provider tile URL", - "google-maps-api-key": "Google Maps API Key", - "default-map-type": "Default map type", - "google-map-type-roadmap": "Roadmap", - "google-map-type-satelite": "Satellite", - "google-map-type-hybrid": "Hybrid", - "google-map-type-terrain": "Terrain", - "map-layer": "Map layer", - "here-map-normal-day": "HERE.normalDay (Default)", - "here-map-normal-night": "HERE.normalNight", - "here-map-hybrid-day": "HERE.hybridDay", - "here-map-terrain-day": "HERE.terrainDay", - "credentials": "Credentials", - "here-app-id": "HERE app id", - "here-app-code": "HERE app code", - "here-api-key": "HERE API key", - "here-use-new-version-api-3": "Use API version 3", - "tencent-maps-api-key": "Tencent Maps API Key", - "tencent-map-type-roadmap": "Roadmap", - "tencent-map-type-satelite": "Satellite", - "tencent-map-type-hybrid": "Hybrid", - "image-map-background": "Image map background", - "image-map-background-from-entity-attribute": "Take image map background from entity attribute", - "image-url-source-entity-alias": "Image URL source entity alias", - "image-url-source-entity-attribute": "Image URL source entity attribute", - "common-map-settings": "Common map settings", - "x-pos-key-name": "X position key name", - "y-pos-key-name": "Y position key name", - "latitude-key-name": "Latitude key name", - "longitude-key-name": "Longitude key name", - "default-map-zoom-level": "Default map zoom level (0 - 20)", - "default-map-center-position": "Default map center position (0,0)", - "disable-scroll-zooming": "Disable scroll zooming", - "disable-double-click-zooming": "Disable double click zooming", - "disable-zoom-control-buttons": "Disable zoom control buttons", - "fit-map-bounds": "Fit map bounds to cover all markers", - "use-default-map-center-position": "Use default map center position", - "entities-limit": "Limit of entities to load", - "markers-settings": "Markers settings", - "marker-offset-x": "Marker X offset relative to position multiplied by marker width", - "marker-offset-y": "Marker Y offset relative to position multiplied by marker height", - "position-function": "Position conversion function, should return x,y coordinates as double from 0 to 1 each", - "draggable-marker": "Draggable marker", - "label": "Label", - "show-label": "Show label", - "use-label-function": "Use label function", - "label-pattern": "Label (pattern examples: '${entityName}', '${entityName}: (Text ${keyName} units.)' )", - "label-function": "Label function", - "tooltip": "Tooltip", - "show-tooltip": "Show tooltip", - "show-tooltip-action": "Action for displaying the tooltip", - "show-tooltip-action-click": "Show tooltip on click (Default)", - "show-tooltip-action-hover": "Show tooltip on hover", - "auto-close-tooltips": "Auto-close tooltips", - "use-tooltip-function": "Use tooltip function", - "tooltip-pattern": "Tooltip (for ex. 'Text ${keyName} units.' or Link text')", - "tooltip-function": "Tooltip function", - "tooltip-offset-x": "Tooltip X offset relative to marker anchor multiplied by marker width", - "tooltip-offset-y": "Tooltip Y offset relative to marker anchor multiplied by marker height", - "color": "Color", - "use-color-function": "Use color function", - "color-function": "Color function", - "marker-image": "Marker image", - "use-marker-image-function": "Use marker image function", - "custom-marker-image": "Custom marker image", - "custom-marker-image-size": "Custom marker image size (px)", - "marker-image-function": "Marker image function", - "marker-images": "Marker images", - "polygon-settings": "Polygon settings", - "show-polygon": "Show polygon", - "polygon-key-name": "Polygon key name", - "enable-polygon-edit": "Enable polygon edit", - "polygon-label": "Polygon label", - "show-polygon-label": "Show polygon label", - "use-polygon-label-function": "Use polygon label function", - "polygon-label-pattern": "Polygon label (pattern examples: '${entityName}', '${entityName}: (Text ${keyName} units.)' )", - "polygon-label-function": "Polygon label function", - "polygon-tooltip": "Polygon tooltip", - "show-polygon-tooltip": "Show polygon tooltip", - "auto-close-polygon-tooltips": "Auto-close polygon tooltips", - "use-polygon-tooltip-function": "Use polygon tooltip function", - "polygon-tooltip-pattern": "Tooltip (for ex. 'Text ${keyName} units.' or Link text')", - "polygon-tooltip-function": "Polygon tooltip function", - "polygon-color": "Polygon color", - "polygon-opacity": "Polygon opacity", - "use-polygon-color-function": "Use polygon color function", - "polygon-color-function": "Polygon color function", - "polygon-stroke": "Polygon stroke", - "stroke-color": "Stroke color", - "stroke-opacity": "Stroke opacity", - "stroke-weight": "Stroke weight", - "use-polygon-stroke-color-function": "Use polygon stroke color function", - "polygon-stroke-color-function": "Polygon stroke color function", - "circle-settings": "Circle settings", - "show-circle": "Show circle", - "circle-key-name": "Circle key name", - "enable-circle-edit": "Enable circle edit", - "circle-label": "Circle label", - "show-circle-label": "Show circle label", - "use-circle-label-function": "Use circle label function", - "circle-label-pattern": "Circle label (pattern examples: '${entityName}', '${entityName}: (Text ${keyName} units.)' )", - "circle-label-function": "Circle label function", - "circle-tooltip": "Circle tooltip", - "show-circle-tooltip": "Show circle tooltip", - "auto-close-circle-tooltips": "Auto-close circle tooltips", - "use-circle-tooltip-function": "Use circle tooltip function", - "circle-tooltip-pattern": "Tooltip (for ex. 'Text ${keyName} units.' or Link text')", - "circle-tooltip-function": "Circle tooltip function", - "circle-fill-color": "Circle fill color", - "circle-fill-color-opacity": "Circle fill color opacity", - "use-circle-fill-color-function": "Use circle fill color function", - "circle-fill-color-function": "Circle fill color function", - "circle-stroke": "Circle stroke", - "use-circle-stroke-color-function": "Use circle stroke color function", - "circle-stroke-color-function": "Circle stroke color function", - "markers-clustering-settings": "Markers clustering settings", - "use-map-markers-clustering": "Use map markers clustering", - "zoom-on-cluster-click": "Zoom when clicking on a cluster", - "max-cluster-zoom": "The maximum zoom level when a marker can be part of a cluster (0 - 18)", - "max-cluster-radius-pixels": "Maximum radius that a cluster will cover in pixels", - "cluster-zoom-animation": "Show animation on markers when zooming", - "show-markers-bounds-on-cluster-mouse-over": "Show the bounds of markers when mouse over a cluster", - "spiderfy-max-zoom-level": "Spiderfy at the max zoom level (to see all cluster markers)", - "load-optimization": "Load optimization", - "cluster-chunked-loading": "Use chunks for adding markers so that the page does not freeze", - "cluster-markers-lazy-load": "Use lazy load for adding markers", - "editor-settings": "Editor settings", - "enable-snapping": "Enable snapping to other vertices for precision drawing", - "init-draggable-mode": "Initialize map in draggable mode", - "hide-all-edit-buttons": "Hide all edit control buttons", - "hide-draw-buttons": "Hide draw buttons", - "hide-edit-buttons": "Hide edit buttons", - "hide-remove-button": "Hide remove button", - "route-map-settings": "Route map settings", - "trip-animation-settings": "Trip animation settings", - "normalization-step": "Normalization data step (ms)", - "tooltip-background-color": "Tooltip background color", - "tooltip-font-color": "Tooltip font color", - "tooltip-opacity": "Tooltip opacity (0-1)", - "auto-close-tooltip": "Auto-close tooltip", - "rotation-angle": "Set additional rotation angle for marker (deg)", - "path-settings": "Path settings", - "path-color": "Path color", - "use-path-color-function": "Use path color function", - "path-color-function": "Path color function", - "path-decorator": "Path decorator", - "use-path-decorator": "Use path decorator", - "decorator-symbol": "Decorator symbol", - "decorator-symbol-arrow-head": "Arrow", - "decorator-symbol-dash": "Dash", - "decorator-symbol-size": "Decorator symbol size (px)", - "use-path-decorator-custom-color": "Use path decorator custom color", - "decorator-custom-color": "Decorator custom color", - "decorator-offset": "Decorator offset", - "end-decorator-offset": "End decorator offset", - "decorator-repeat": "Decorator repeat", - "points-settings": "Points settings", - "show-points": "Show points", - "point-color": "Point color", - "point-size": "Point size (px)", - "use-point-color-function": "Use point color function", - "point-color-function": "Point color function", - "use-point-as-anchor": "Use point as anchor", - "point-as-anchor-function": "Point as anchor function", - "independent-point-tooltip": "Independent point tooltip", - "clustering-markers": "Clustering markers", - "use-icon-create-function": "Use markers colour function", - "marker-color-function": "Marker color function" - }, - "markdown": { - "use-markdown-text-function": "Use markdown/HTML value function", - "markdown-text-function": "Markdown/HTML value function", - "markdown-text-pattern": "Markdown/HTML pattern (markdown or HTML with variables, for ex. '${entityName} or ${keyName} - some text.')", - "apply-default-markdown-style": "Apply default markdown style", - "markdown-css": "Markdown/HTML CSS" - }, - "simple-card": { - "label": "Label", - "label-position": "Label position", - "label-position-left": "Left", - "label-position-top": "Top" - }, - "value-card": { - "layout": "Layout", - "layout-square": "Square", - "layout-vertical": "Vertical", - "layout-centered": "Centered", - "layout-simplified": "Simplified", - "layout-horizontal": "Horizontal", - "layout-horizontal-reversed": "Horizontal reversed", - "label": "Label", - "icon": "Icon", - "value": "Value", - "date": "Date", - "value-card-style": "Value card style" - }, - "aggregated-value-card": { - "subtitle": "Subtitle", - "chart": "Chart", - "values": "Values", - "value-appearance": "Value appearance", - "position": "Position", - "position-center": "Center", - "position-right-top": "Right top", - "position-right-bottom": "Right bottom", - "position-left-top": "Left top", - "position-left-bottom": "Left bottom", - "font": "Font", - "color": "Color", - "arrow": "Arrow", - "display-up-down-arrow": "Display Up/Down arrow", - "add-value": "Add value", - "remove-value": "Remove value", - "no-values": "No values configured", - "aggregation": "Aggregation", - "aggregated-value-card-style": "Aggregated value card style" - }, - "alarm-count": { - "alarm-count-card-style": "Alarm count card style" - }, - "entity-count": { - "entity-count-card-style": "Entity count card style" - }, - "count": { - "layout": "Layout", - "layout-column": "Column", - "layout-row": "Row", - "label": "Label", - "icon": "Icon", - "icon-background": "Icon background", - "value": "Value", - "chevron": "Chevron" - }, - "table": { - "common-table-settings": "Common Table Settings", - "enable-search": "Enable search", - "enable-sticky-header": "Always display header", - "enable-sticky-action": "Always display actions column", - "hidden-cell-button-display-mode": "Hidden cell button actions display mode", - "show-empty-space-hidden-action": "Show empty space instead of hidden cell button action", - "dont-reserve-space-hidden-action": "Don't reserve space for hidden action buttons", - "display-timestamp": "Display timestamp column", - "display-milliseconds": "Display timestamp milliseconds", - "display-pagination": "Display pagination", - "default-page-size": "Default page size", - "use-entity-label-tab-name": "Use entity label in tab name", - "hide-empty-lines": "Hide empty lines", - "row-style": "Row style", - "use-row-style-function": "Use row style function", - "row-style-function": "Row style function", - "cell-style": "Cell style", - "use-cell-style-function": "Use cell style function", - "cell-style-function": "Cell style function", - "cell-content": "Cell content", - "use-cell-content-function": "Use cell content function", - "cell-content-function": "Cell content function", - "show-latest-data-column": "Show latest data column", - "latest-data-column-order": "Latest data column order", - "entities-table-title": "Entities table title", - "enable-select-column-display": "Enable select columns to display", - "display-entity-name": "Display entity name column", - "entity-name-column-title": "Entity name column title", - "display-entity-label": "Display entity label column", - "entity-label-column-title": "Entity label column title", - "display-entity-type": "Display entity type column", - "default-sort-order": "Default sort order", - "custom-title": "Custom header title", - "column-width": "Column width (px or %)", - "default-column-visibility": "Default column visibility", - "column-visibility-visible": "Visible", - "column-visibility-hidden": "Hidden", - "column-visibility-hidden-mobile": "Hidden in mobile mode", - "column-selection-to-display": "Column selection in 'Columns to Display'", - "column-selection-to-display-enabled": "Enabled", - "column-selection-to-display-disabled": "Disabled", - "column-export-option": "Include column in export", - "column-export-option-always": "Always", - "column-export-option-only-visible": "Only if column visible", - "column-export-option-never": "Never", - "alarms-table-title": "Alarms table title", - "enable-alarms-selection": "Enable alarms selection", - "enable-alarms-search": "Enable alarms search", - "enable-alarm-filter": "Enable alarm filter", - "display-alarm-details": "Display alarm details", - "allow-alarms-ack": "Allow alarms acknowledgment", - "allow-alarms-clear": "Allow alarms clear", - "display-alarm-activity": "Display alarm activity", - "allow-alarms-assign": "Allow alarms assignment", - "blob-entities-table-title": "Blob entities table title", - "display-created-time": "Display created time column", - "display-type": "Display type column", - "display-customer": "Display customer column", - "no-data-display-message": "\"No data to display\" alternative message", - "force-default-blob-entity-type": "Force default blob entity type", - "scheduler-events-table-title": "Scheduler events table title", - "force-default-event-type": "Force default event type", - "columns": "Columns", - "column-settings": "Column settings", - "remove-column": "Remove column", - "add-column": "Add column", - "no-columns": "No columns configured", - "columns-to-display": "Columns to display", - "table-header": "Table header", - "header-buttons": "Header buttons", - "table-buttons": "Table buttons", - "pagination": "Pagination", - "rows": "Rows", - "timeseries-column-error": "At least one timeseries column should be specified", - "table-tabs": "Table tabs", - "show-cell-actions-menu-mobile": "Show cell actions dropdown menu in mobile mode" - }, - "scheduler": { - "enabled-scheduler-event-views": "Enabled scheduler events views", - "enabled-scheduler-event-views-both": "Both", - "enabled-scheduler-event-views-list": "List View Only", - "enabled-scheduler-event-views-calendar": "Calendar View Only", - "display-name": "Display name", - "type-name": "Type name", - "display-originator-select": "Display originator entity select", - "display-message-type-select": "Display message type select", - "display-metadata-table": "Display message metadata table", - "configuration-html-template": "Configuration HTML template", - "custom-event-types": "Custom event types", - "no-custom-event-types": "No custom event types configured", - "add-custom-event-type": "Add custom event type" - }, - "value-source": { - "value-source": "Value source", - "predefined-value": "Predefined value", - "entity-attribute": "Entity attribute", - "value": "Value", - "source-entity-alias": "Source entity alias", - "source-entity-attribute": "Source entity attribute" - }, - "widget-font": { - "font-settings": "Font settings", - "font-family": "Font family", - "size": "Size", - "relative-font-size": "Relative font size (percents)", - "font-style": "Style", - "font-style-normal": "Normal", - "font-style-italic": "Italic", - "font-style-oblique": "Oblique", - "font-weight": "Weight", - "font-weight-normal": "Normal", - "font-weight-bold": "Bold", - "font-weight-bolder": "Bolder", - "font-weight-lighter": "Lighter", - "color": "Color", - "shadow-color": "Shadow color", - "preview": "Preview", - "line-height": "Line height", - "auto": "Auto" - }, - "home": { - "no-data-available": "No data available" - }, - "system-info": { - "cpu": "CPU", - "ram": "RAM", - "disk": "Disk", - "cpu-warning-text": "Running high on CPU usage. To avoid system failure, optimize system performance.", - "cpu-critical-text": "Critically high CPU usage. To avoid system failure, optimize system performance.", - "ram-warning-text": "Running low on reserve of RAM. To avoid system failure, optimize system performance or increase the size of RAM.", - "ram-critical-text": "Critically low reserve of RAM. To avoid system failure, optimize system performance or increase the size of RAM.", - "disk-warning-text": "Running low on disk space. To avoid data loss, free up or expand the disk space.", - "disk-critical-text": "Critically low disk space. To avoid data loss, free up or expand the disk space." - }, - "cluster-info": { - "service-id": "Service id", - "service-type": "Service type", - "no-data": "No data" - }, - "transport-messages": { - "title": "Transport messages", - "info": "All the messages that came from devices" - }, - "activity": { - "title": "Activity" - }, - "documentation": { - "title": "Documentation", - "add-link": "Add link", - "add-link-title": "Add documentation link", - "name": "Name", - "name-required": "Name is required.", - "link": "Link", - "link-required": "Link is required.", - "columns": "Columns" - }, - "quick-links": { - "title": "Quick links", - "add-link": "Add link", - "add-link-title": "Add quick link", - "quick-link": "Quick link", - "quick-link-required": "Quick link is required.", - "no-links-matching": "No links matching '{{name}}' were found.", - "columns": "Columns" - }, - "recent-dashboards": { - "title": "Dashboards", - "last": "Last viewed", - "starred": "Starred", - "name": "Name", - "last-viewed": "Last viewed", - "no-last-viewed-dashboards": "No last viewed dashboards yet" + "system-info": { + "cpu": "CPU", + "ram": "RAM", + "disk": "Diskas", + "cpu-warning-text": "Didelis procesoriaus apkrovimas. Siekiant išvengti sistemos gedimo, optimizuokite našumą.", + "cpu-critical-text": "Kritiškai didelis procesoriaus apkrovimas. Nedelsiant optimizuokite sistemos našumą.", + "ram-warning-text": "Mažas RAM rezervas. Padidinkite RAM arba optimizuokite sistemos veikimą.", + "ram-critical-text": "Kritiškai mažas RAM rezervas. Būtina optimizacija arba RAM padidinimas.", + "disk-warning-text": "Mažai vietos diske. Išvalykite arba išplėskite disko talpą.", + "disk-critical-text": "Kritiškai mažai vietos diske. Skubiai atlaisvinkite arba išplėskite talpą." + }, + "cluster-info": { + "service-id": "Paslaugos ID", + "service-type": "Paslaugos tipas", + "no-data": "Nėra duomenų" + }, + "transport-messages": { + "title": "Transporto pranešimai", + "info": "Visi pranešimai, gauti iš įrenginių" + }, + "activity": { + "title": "Aktyvumas" + }, + "documentation": { + "title": "Dokumentacija", + "add-link": "Pridėti nuorodą", + "add-link-title": "Pridėti dokumentacijos nuorodą", + "name": "Pavadinimas", + "name-required": "Pavadinimas yra privalomas.", + "link": "Nuoroda", + "link-required": "Nuoroda yra privaloma.", + "columns": "Stulpeliai" + }, + "quick-links": { + "title": "Greitosios nuorodos", + "add-link": "Pridėti nuorodą", + "add-link-title": "Pridėti greitąją nuorodą", + "quick-link": "Greitoji nuoroda", + "quick-link-required": "Greitoji nuoroda yra privaloma.", + "no-links-matching": "Nuorodų, atitinkančių '{{name}}', nerasta.", + "columns": "Stulpeliai" + }, + "recent-dashboards": { + "title": "Valdymo skydeliai", + "last": "Paskutiniai peržiūrėti", + "starred": "Pažymėti žvaigždute", + "name": "Pavadinimas", + "last-viewed": "Paskutinį kartą peržiūrėtas", + "no-last-viewed-dashboards": "Dar nėra peržiūrėtų skydelių" + }, + "configured-features": { + "title": "Sukonfigūruotos funkcijos", + "info": "Funkcijų, kurioms reikia konfigūracijos, būsena", + "email-feature": "El. paštas", + "sms-feature": "SMS", + "slack-feature": "Slack", + "oauth2-feature": "OAuth 2", + "2fa-feature": "Dvigubas autentifikavimas (2FA)", + "feature-configured": "Funkcija sukonfigūruota.\nSpustelėkite, kad nustatytumėte", + "feature-not-configured": "Funkcija nesukonfigūruota.\nSpustelėkite, kad nustatytumėte" + }, + "version-info": { + "title": "Versija", + "contact-us": "Susisiekite su mumis", + "current-version": "Dabartinė versija", + "current": "Dabartinė", + "available-version": "Prieinama versija", + "available": "Prieinama", + "upgrade": "Atnaujinti", + "version-is-up-to-date": "Versija yra naujausia" + }, + "usage-info": { + "title": "Naudojimas", + "entities": "Objektai", + "api-calls": "API užklausos" + }, + "functions": { + "title": "Funkcijos", + "pe-feature-tooltip": "Galima tik ThingsBoard\nProfessional Edition versijoje", + "switch-to-pe": "Pereiti į PE versiją", + "alarms": "Aliarmai", + "dashboards": "Valdymo skydeliai", + "entities-and-relations": "Objektai ir ryšiai", + "profiles": "Profiliai", + "advanced-features": "Išplėstinės funkcijos", + "notification-center": "Pranešimų centras", + "api-usage": "API naudojimas", + "customers": "Klientai", + "customers-hierarchy": "Klientų hierarchija", + "roles-and-permissions": "Vaidmenys ir leidimai", + "groups": "Grupės", + "integrations": "Integracijos", + "solution-templates": "Sprendimų šablonai", + "scheduler": "Tvarkaraštis", + "white-labeling": "Prekinio ženklo pritaikymas (White-labeling)" + }, + "devices": { + "view-docs": "Peržiūrėti dokumentaciją", + "inactive": "Neaktyvūs", + "active": "Aktyvūs", + "total": "Iš viso" + }, + "alarms": { + "critical": "Kritiniai", + "assigned-to-me": "Priskirti man", + "total": "Iš viso" + }, + "getting-started": { + "get-started": "Pradėti", + "finish": "Baigti", + "done-welcome-title": "Sveiki atvykę!", + "done-welcome-text": "Puikiai susitvarkėte!", + "sys-admin": { + "step1": { + "title": "Sukurkite nuomininką ir jo administratorių", + "content": "

    Nuomininkas – tai fizinis ar juridinis asmuo, turintis arba valdantis įrenginius ir išteklius. Nuomininkas gali turėti kelis administratoriaus vartotojus, klientus, įrenginius ir išteklius.

    Nuomininko administratorius gali kurti ir valdyti įrenginius, išteklius, klientus bei valdymo skydelius nuomininko paskyroje.

    Sekite dokumentaciją, kaip tai atlikti:

    ", + "how-to-create-tenant": "Kaip sukurti nuomininką ir jo administratorių" }, - "configured-features": { - "title": "Configured features", - "info": "Status of features that require configuration", - "white-labeling-feature": "White labeling", - "email-feature": "Email", - "sms-feature": "SMS", - "slack-feature": "Slack", - "oauth2-feature": "OAuth 2", - "2fa-feature": "2FA", - "feature-configured": "Feature is configured.\nClick to setup", - "feature-not-configured": "Feature is not configured.\nClick to setup" + "step2": { + "title": "Sukonfigūruokite funkciją: Pašto serveris", + "content": "

    Pašto serverio konfigūracija būtina vartotojų aktyvavimui, slaptažodžio atkūrimui ir aliarmų pranešimų siuntimui.

    Sekite dokumentaciją, kaip tai atlikti:

    ", + "how-to-configure-mail-server": "Kaip sukonfigūruoti pašto serverį" }, - "version-info": { - "title": "Version", - "contact-us": "Contact us", - "current-version": "Current version", - "current": "Current", - "available-version": "Available version", - "available": "Available", - "upgrade": "Upgrade", - "version-is-up-to-date": "Version is up to date" + "step3": { + "title": "Sukonfigūruokite funkciją: SMS tiekėjas", + "content": "

    Sukonfigūruokite SMS tiekėjus, kad klientai gautų aliarmų pranešimus SMS žinutėmis.

    Sekite dokumentaciją, kaip tai atlikti:

    ", + "how-to-configure-sms-provider": "Kaip sukonfigūruoti SMS tiekėją" }, - "license-info": { - "title": "License info", - "view-all": "View all", - "license-portal": "License portal", - "devices-info": "{count} / { max, plural, =0 {∞} other {#} } Devices", - "assets-info": "{count} / { max, plural, =0 {∞} other {#} } Assets", - "dashboards-info": "{count} / { max, plural, =0 {∞} other {#} } Dashboards", - "integrations-info": "{count} / { max, plural, =0 {∞} other {#} } Integrations", - "community-support": "Community support", - "white-labeling": "White labeling", - "unlimited-datapoints-and-messages": "Unlimited datapoints and messages", - "unlimited-api-calls": "Unlimited API calls" + "step4": { + "title": "Sukonfigūruokite funkciją: Prekinio ženklo pritaikymas", + "content": "

    Lengvai pritaikykite savo įmonės ar produkto logotipą ir spalvų schemą be programavimo ir paslaugos paleidimo iš naujo.

    Sekite dokumentaciją, kaip tai atlikti:

    " }, - "usage-info": { - "title": "Usage", - "entities": "Entities", - "api-calls": "API calls" + "step5": { + "title": "Sukonfigūruokite funkciją: Dvigubas autentifikavimas (2FA)", + "content": "

    Pagerinkite paskyrų saugumą naudodami dviejų faktorių autentifikavimą.

    Sekite dokumentaciją, kaip tai atlikti:

    " }, - "functions": { - "title": "Functions", - "pe-feature-tooltip": "Only on ThingsBoard\nProfessional Edition", - "switch-to-pe": "Switch to PE", - "alarms": "Alarms", - "dashboards": "Dashboards", - "entities-and-relations": "Entities & Relations", - "profiles": "Profiles", - "advanced-features": "Advanced features", - "notification-center": "Notification center", - "api-usage": "API usage", - "customers": "Customers", - "customers-hierarchy": "Customers hierarchy", - "roles-and-permissions": "Roles & Permissions", - "groups": "Groups", - "integrations": "Integrations", - "solution-templates": "Solution templates", - "scheduler": "Scheduler", - "white-labeling": "White-labeling" + "step6": { + "title": "Sukonfigūruokite funkciją: OAuth 2", + "content": "

    Supaprastinkite nuomininko ir kliento vartotojų prisijungimą naudodami vieningo prisijungimo (Single Sign-On) funkciją per OAuth 2.0.

    Sekite dokumentaciją, kaip tai atlikti:

    " + } + }, + "tenant-admin": { + "step1": { + "title": "Sukurti įrenginį", + "content": "

    Sukurkite savo pirmąjį įrenginį platformoje per naudotojo sąsają (UI). Sekite dokumentaciją, kaip tai atlikti:

    ", + "how-to-create-device": "Kaip sukurti įrenginį" }, - "devices": { - "view-docs": "View docs", - "inactive": "Inactive", - "active": "Active", - "total": "Total" + "step2": { + "title": "Prijungti įrenginį", + "content-before": "

    Norint prijungti įrenginį, reikia gauti jo prisijungimo duomenis. Šiame vadove rekomenduojame naudoti numatytuosius automatiškai sugeneruotus prisijungimo duomenis – prieigos raktą (Access Token).

    • Eikite į įrenginių lentelę
    • Spustelėkite įrenginio eilutę, kad atidarytumėte jo informaciją
    • Paspauskite mygtuką „Kopijuoti prieigos raktą“

    Naudokite paprastas HTTP komandas duomenų publikavimui. Nepamirškite pakeisti $ACCESS_TOKEN savo įrenginio prieigos raktu:

    ", + "ubuntu": { + "install-curl": "Įdiekite cURL Ubuntu sistemoje:" + }, + "macos": { + "install-curl": "Įdiekite cURL MacOS sistemoje:" + }, + "windows": { + "install-curl": "Nuo Windows 10 b17063 versijos cURL yra įdiegtas pagal nutylėjimą." + }, + "replace-access-token": "Pakeiskite $ACCESS_TOKEN savo įrenginio prieigos raktu:", + "content-after": "

    Taip pat galite naudoti kitus protokolus, tokius kaip MQTT, CoAP ir kt.

    Sekite dokumentaciją, kaip tai atlikti:

    ", + "how-to-connect-device": "Kaip prijungti įrenginį" }, - "alarms": { - "critical": "Critical", - "assigned-to-me": "Assigned to me", - "total": "Total" + "step3": { + "title": "Sukurti valdymo skydelį", + "content": "

    Sukurkite valdymo skydelį duomenų vizualizavimui iš objektų, tokių kaip įrenginiai ar ištekliai.

    Sekite dokumentaciją, kaip tai atlikti:

    ", + "how-to-create-dashboard": "Kaip sukurti valdymo skydelį" }, - "solution-templates": { - "title": "Solution templates", - "prototype-plan": "Prototype plan", - "startup-plan": "Startup plan" + "step4": { + "title": "Sukonfigūruoti aliarmo taisykles", + "alarm-rules": "Aliarmo taisyklės", + "content": "

    Sukurkime aliarmą, kuris suveiks, kai temperatūra pasieks 25°C. Sekite dokumentaciją, kaip tai atlikti:

    ", + "how-to-configure-alarm-rules": "Kaip sukonfigūruoti aliarmo taisykles" }, - "getting-started": { - "get-started": "Get started", - "finish": "Finish", - "done-welcome-title": "Welcome on board", - "done-welcome-text": "You did great with it!", - "sys-admin": { - "step1": { - "title": "Create Tenant & Tenant Administrator", - "content": "

    A tenant is an individual or an organization that owns or produces devices and assets. The tenant may have multiple tenant administrator users, customers, devices, and assets.

    The Tenant Administrator can create and manage devices, assets, customers, and dashboards within the tenant account.

    Follow the documentation on how to do it:

    ", - "how-to-create-tenant": "How to create Tenant & Tenant Administrator" - }, - "step2": { - "title": "Configure feature: Mail server", - "content": "

    Mail server configuration is essential for user activation, password recovery, and alarm notification delivery.

    Follow the documentation on how to do it:

    ", - "how-to-configure-mail-server": "How to configure Mail server" - }, - "step3": { - "title": "Configure feature: SMS provider", - "content": "

    Configure SMS providers to notify the customers about the alarms via SMS.

    Follow the documentation on how to do it:

    ", - "how-to-configure-sms-provider": "How to configure SMS provider" - }, - "step4": { - "title": "Configure feature: White-labeling", - "content": "

    Easily customize your company's or product's logo and color scheme without coding and without restarting the service.

    Follow the documentation on how to do it:

    ", - "how-to-configure-white-labeling": "How to configure White-labeling" - }, - "step5": { - "title": "Configure feature: 2FA", - "content": "

    Improve security of the platform accounts with two-factor authentication.

    Follow the documentation on how to do it:

    ", - "how-to-configure-2fa": "How to configure 2FA" - }, - "step6": { - "title": "Configure feature: OAuth 2", - "content": "

    Simplify login for the tenant and customer users with Single Sign-On functionality via OAuth 2.0.

    Follow the documentation on how to do it:

    ", - "how-to-configure-oauth2": "How to configure OAuth 2" - }, - "step7": { - "title": "Configure feature: Slack", - "content": "

    Distribute notifications to the tenant and customer users via Slack according to the notification rules you set.

    Follow the documentation on how to do it:

    ", - "how-to-configure-notifications": "How to configure Slack" - } - }, - "tenant-admin": { - "step1": { - "title": "Create device", - "content": "

    Let's provision your first device to the platform via UI. Follow the documentation on how to do it:

    ", - "how-to-create-device": "How to create Device" - }, - "step2": { - "title": "Connect device", - "content-before": "

    To connect the device you need to get the device credentials. We recommend using default auto-generated credentials which is access token for this guide.

    • Go to device table
    • Click on the device row to open device details
    • Press the button \"Copy access token\"

    Use simple commands to publish data over HTTP. Don't forget to replace $ACCESS_TOKEN with your device access token:

    ", - "ubuntu": { - "install-curl": "Install cURL for Ubuntu:" - }, - "macos": { - "install-curl": "Install cURL for MacOS:" - }, - "windows": { - "install-curl": "Starting Windows 10 b17063, cURL is available by default." - }, - "replace-access-token": "Replace $ACCESS_TOKEN with your device's token:", - "content-after": "

    You can also use other protocols such as MQTT, CoAP, etc.

    Follow the documentation on how to do it:

    ", - "how-to-connect-device": "How to connect Device" - }, - "step3": { - "title": "Create dashboard", - "content": "

    Create a dashboard to visualize data from entities such as assets, devices, etc.

    Follow the documentation on how to do it:

    ", - "how-to-create-dashboard": "How to create Dashboard" - }, - "step4": { - "title": "Configure alarm rules", - "alarm-rules": "Alarm rules", - "content": "

    Let's raise an alarm when the temperature reaches 25°C. Follow the documentation on how to do it:

    ", - "how-to-configure-alarm-rules": "How to configure Alarm rules" - }, - "step5": { - "title": "Create alarm", - "content-before": "

    To trigger the alarm, send a new telemetry value of 26°C or higher.

    ", - "replace-access-token": "Replace $ACCESS_TOKEN with your device's token:", - "content-after": "

    Follow the documentation on how to do it:

    ", - "how-to-create-alarm": "How to create Alarm" - }, - "step6": { - "title": "Create customer and share dashboard", - "content": "

    By creating end-user dashboards, a customer user can only see his own devices, and data from another customer will be hidden.

    Follow the documentation on how to do it:

    ", - "how-to-create-customer-and-share-dashboard": "How to create Customer and share Dashboard" - } - } - } - }, - "icon": { - "icon": "Icon", - "icons": "Icons", - "select-icon": "Select icon", - "material-icons": "Material icons", - "show-all": "Show all icons", - "search-icon": "Search icon", - "no-icons-found": "No icons found for '{{iconSearch}}'" - }, - "subscription": { - "entity-limit-text": "However you can upgrade your subscription plan in order to increase your limits.", - "upgrade-your-plan": "Upgrade subscription plan", - "white-labeling-feature": "White labeling feature", - "white-labeling-text-full": "Rebrand ThingsBoard platform web interface with your company or product logo and color scheme in 2 minutes.

    Remove “Powered By” on the dashboards footer.
    No coding or service restart required. Allow your customers to white-label their interface as well.", - "enable-white-labeling": "Enable white-labeling feature now by upgrading your subscription plan!", - "read-more": "Read more", - "white-labeling-video-text": "See video tutorial below to see how this feature works!" - }, - "subscription-error": { - "title": "Subscription violation", - "warning-title": "Subscription warning", - "upgrade-subscription-plan": "Please upgrade your subscription plan", - "upgrade-subscription-plan-to-install-solution-template": "In order to install {{solutionTemplateName}} solution you must upgrade your subscription at least to the {{planName}} plan!", - "limit-reached": { - "device-count": "You have reached maximum devices ({{value}}) allowed by your subscription plan!", - "asset-count": "You have reached maximum assets ({{value}}) allowed by your subscription plan!" + "step5": { + "title": "Sukurti aliarmą", + "content-before": "

    Norėdami suaktyvinti aliarmą, siųskite naują telemetrijos reikšmę, didesnę arba lygią 26°C.

    ", + "replace-access-token": "Pakeiskite $ACCESS_TOKEN savo įrenginio prieigos raktu:", + "content-after": "

    Sekite dokumentaciją, kaip tai atlikti:

    ", + "how-to-create-alarm": "Kaip sukurti aliarmą" }, - "feature-disabled": { - "white-labeling": "White Labeling feature is not allowed by your subscription plan!" - } - }, - "phone-input": { - "phone-input-label": "Phone number", - "phone-input-required": "Phone number is required", - "phone-input-validation": "Phone number is invalid or not possible", - "phone-input-pattern": "Invalid phone number. Should be in E.164 format, ex. {{phoneNumber}}", - "phone-input-hint": "Telefono numeris tarptautiniu formatu , pvz. +37060123456" - }, - "custom": { - "widget-action": { - "action-cell-button": "Action cell button", - "row-click": "On row click", - "polygon-click": "On polygon click", - "marker-click": "On marker click", - "circle-click": "On circle click", - "tooltip-tag-action": "Tooltip tag action", - "node-selected": "On node selected", - "element-click": "On HTML element click", - "pie-slice-click": "On slice click", - "row-double-click": "On row double click", - "card-click": "On card click" + "step6": { + "title": "Sukurti klientą ir pasidalyti valdymo skydeliu", + "content": "

    Sukūrę galutinio naudotojo valdymo skydelius, kliento vartotojas matys tik savo įrenginius, o kitų klientų duomenys bus paslėpti.

    Sekite dokumentaciją, kaip tai atlikti:

    " } - }, - "paginator": { - "items-per-page": "Eilučių skaičius puslapyje:", - "first-page-label": "Pirmas puslapis", - "last-page-label": "Paskutinis puslapis", - "next-page-label": "Kitas puslapis", - "previous-page-label": "Ankstesnis puslapis", - "items-per-page-separator": "iš" + } + } + }, + "icon": { + "icon": "Piktograma", + "icons": "Piktogramos", + "select-icon": "Pasirinkti piktogramą", + "material-icons": "Material piktogramos", + "show-all": "Rodyti visas piktogramas", + "search-icon": "Ieškoti piktogramos", + "no-icons-found": "Nerasta piktogramų pagal '{{iconSearch}}'" + }, + "phone-input": { + "phone-input-label": "Telefono numeris", + "phone-input-required": "Telefono numeris yra privalomas", + "phone-input-validation": "Telefono numeris yra neteisingas arba neįmanomas", + "phone-input-pattern": "Neteisingas telefono numeris. Formatą turi atitikti E.164, pvz. {{phoneNumber}}", + "phone-input-hint": "Telefono numeris tarptautiniu formatu, pvz. +37060123456" + }, + "custom": { + "widget-action": { + "action-cell-button": "Veiksmo langelio mygtukas", + "row-click": "Paspaudus eilutę", + "cell-click": "Paspaudus langelį", + "polygon-click": "Paspaudus daugiakampį", + "marker-click": "Paspaudus žymeklį", + "circle-click": "Paspaudus apskritimą", + "tooltip-tag-action": "Patarimo žymos veiksmas", + "node-selected": "Pasirinkus mazgą", + "element-click": "Paspaudus HTML elementą", + "pie-slice-click": "Paspaudus diagramos dalį", + "row-double-click": "Dukart paspaudus eilutę", + "cell-double-click": "Dukart paspaudus langelį", + "card-click": "Paspaudus kortelę", + "click": "Paspaudus" } + }, + "paginator": { + "items-per-page": "Eilučių skaičius puslapyje:", + "first-page-label": "Pirmas puslapis", + "last-page-label": "Paskutinis puslapis", + "next-page-label": "Kitas puslapis", + "previous-page-label": "Ankstesnis puslapis", + "items-per-page-separator": "iš" + }, + "language": { + "language": "Kalba" + } } - diff --git a/ui-ngx/src/assets/locale/locale.constant-tr_TR.json b/ui-ngx/src/assets/locale/locale.constant-tr_TR.json index 1af97591a4..74bd56e2f5 100644 --- a/ui-ngx/src/assets/locale/locale.constant-tr_TR.json +++ b/ui-ngx/src/assets/locale/locale.constant-tr_TR.json @@ -2,19 +2,24 @@ "access": { "unauthorized": "Yetkisiz", "unauthorized-access": "Yetkisiz Erişim", - "unauthorized-access-text": "Bu kaynağa erişmek için giriş yapmalısınız!", + "unauthorized-access-text": "Bu kaynağa erişim için giriş yapmalısınız!", "access-forbidden": "Erişim Yasaklandı", - "access-forbidden-text": "Bu konuma erişim haklarınız yok!
    Bu yere hala erişmek istiyorsanız farklı kullanıcılarla oturum açmayı deneyin.", + "access-forbidden-text": "Bu konuma erişim hakkınız yok!
    Bu konuma erişim sağlamak istiyorsanız farklı bir kullanıcı ile giriş yapmayı deneyin.", "refresh-token-expired": "Oturum süresi doldu", - "refresh-token-failed": "Oturum yenilenemiyor", + "refresh-token-failed": "Oturum yenileme başarısız oldu", "permission-denied": "İzin Reddedildi", - "permission-denied-text": "Bu işlemi gerçekleştirme izniniz yok!" + "permission-denied-text": "Bu işlemi gerçekleştirmek için izniniz yok!" + }, + "account": { + "account": "Hesap", + "notification-settings": "Bildirim ayarları" }, "action": { "activate": "Etkinleştir", - "suspend": "Askıya al", + "suspend": "Askıya Al", "save": "Kaydet", "saveAs": "Farklı Kaydet", + "move": "Taşı", "cancel": "İptal", "ok": "Tamam", "delete": "Sil", @@ -23,18 +28,18 @@ "no": "Hayır", "update": "Güncelle", "remove": "Kaldır", - "select": "Seç", "search": "Ara", - "clear-search": "Aramayı Temizle", + "clear-search": "Aramayı temizle", "assign": "Ata", "unassign": "Atamayı kaldır", "share": "Paylaş", "make-private": "Özel yap", "apply": "Uygula", - "apply-changes": "Değişiklikleri Uygula", - "edit-mode": "Düzenleme Modu", + "apply-changes": "Değişiklikleri uygula", + "edit-mode": "Düzenleme modu", "enter-edit-mode": "Düzenleme moduna gir", "decline-changes": "Değişiklikleri reddet", + "decline": "Reddet", "close": "Kapat", "back": "Geri", "run": "Çalıştır", @@ -49,19 +54,38 @@ "paste": "Yapıştır", "copy-reference": "Referansı kopyala", "paste-reference": "Referansı yapıştır", - "import": "İçe aktar", - "export": "Dışa aktar", + "import": "İçe Aktar", + "export": "Dışa Aktar", "share-via": "{{provider}} ile paylaş", - "continue": "Devam", - "discard-changes": "Değişikliklerden Vazgeç", + "select": "Seç", + "continue": "Devam et", + "discard-changes": "Değişiklikleri At", "download": "İndir", - "next-with-label": "Sonraki: {{label}}", - "read-more": "Devamını Oku", + "next": "İleri", + "next-with-label": "İleri: {{label}}", + "read-more": "Daha fazla oku", "hide": "Gizle", - "done": "Tamamlandı" + "test": "Test Et", + "done": "Tamamlandı", + "print": "Yazdır", + "restore": "Geri Yükle", + "confirm": "Onayla", + "more": "Daha fazla", + "less": "Daha az", + "skip": "Atla", + "send": "Gönder", + "reset": "Sıfırla", + "show-more": "Daha fazla göster", + "dont-show-again": "Bir daha gösterme", + "see-documentation": "Dokümantasyonu görüntüle", + "clear": "Temizle", + "upload": "Yükle", + "delete-anyway": "Yine de sil", + "delete-selected": "Seçilenleri sil", + "set": "Ayarla" }, "aggregation": { - "aggregation": "Aggregation", + "aggregation": "Toplama", "function": "Veri toplama fonksiyonu", "limit": "Maksimum değerler", "group-interval": "Gruplama aralığı", @@ -73,74 +97,97 @@ "none": "Yok" }, "admin": { + "settings": "Ayarlar", "general": "Genel", - "general-settings": "Genel Ayarlar", - "home-settings": "Ana Sayfa Ayarları", - "outgoing-mail": "Giden Posta Sunucusu", + "general-settings": "Genel ayarlar", + "home-settings": "Ana sayfa ayarları", + "home": "Ana sayfa", + "outgoing-mail": "Posta sunucusu", "outgoing-mail-settings": "Giden Posta Sunucusu Ayarları", "system-settings": "Sistem Ayarları", "test-mail-sent": "Test e-postası başarıyla gönderildi!", - "base-url": "Taban URL", - "base-url-required": "Taban URL gerekli.", - "prohibit-different-url": "İstemci istek başlıklarından ana bilgisayar adını kullanmayı yasakla", - "prohibit-different-url-hint": "Bu ayar, üretim ortamları için etkinleştirilmelidir. Devre dışı bırakıldığında güvenlik sorunlarına neden olabilir", - "mail-from": "Gönderen Kişi", - "mail-from-required": "Gönderen Kişi gerekli.", + "base-url": "Temel URL", + "base-url-required": "Temel URL gerekli.", + "prohibit-different-url": "İstemci isteği başlıklarından gelen ana makine adının kullanımını yasakla", + "prohibit-different-url-hint": "Bu ayar üretim ortamları için etkinleştirilmelidir. Devre dışı bırakıldığında güvenlik sorunlarına neden olabilir", + "device-connectivity": { + "device-connectivity": "Cihaz bağlantısı", + "http-s": "HTTP(s)", + "mqtt-s": "MQTT(s)", + "coap-s": "COAP(s)", + "http": "HTTP", + "https": "HTTPs", + "mqtt": "MQTT", + "mqtts": "MQTTs", + "coap": "COAP", + "coaps": "COAPs", + "hint": "Ev sahibi veya port alanları boşsa, varsayılan protokol değeri kullanılacaktır.", + "host": "Sunucu", + "port": "Port", + "port-pattern": "Port pozitif bir tamsayı olmalıdır.", + "port-range": "Port değeri 1 ile 65535 arasında olmalıdır." + }, + "mail-from": "Gönderen E-posta", + "mail-from-required": "Gönderen E-posta gereklidir.", "smtp-protocol": "SMTP protokolü", "smtp-host": "SMTP sunucusu", - "smtp-host-required": "SMTP sunucusu gerekli.", + "smtp-host-required": "SMTP sunucusu gereklidir.", "smtp-port": "SMTP portu", - "smtp-port-required": "Bir SMTP portu gerekli.", - "smtp-port-invalid": "Bu geçerli bir smtp portu gibi görünmüyor.", + "smtp-port-required": "SMTP portu sağlamalısınız.", + "smtp-port-invalid": "Geçerli bir SMTP portu gibi görünmüyor.", "timeout-msec": "Zaman aşımı (milisaniye)", - "timeout-required": "Zaman aşımı değeri gerekli.", - "timeout-invalid": "Bu geçerli bir zaman aşımı gibi görünmüyor.", + "timeout-required": "Zaman aşımı değeri gereklidir.", + "timeout-invalid": "Geçerli bir zaman aşımı değeri gibi görünmüyor.", "enable-tls": "TLS'i etkinleştir", "tls-version": "TLS sürümü", - "enable-proxy": "Proxy etkinleştir", + "enable-proxy": "Proxy'i etkinleştir", "proxy-host": "Proxy sunucusu", "proxy-host-required": "Proxy sunucusu gereklidir.", "proxy-port": "Proxy portu", "proxy-port-required": "Proxy portu gereklidir.", - "proxy-port-range": "Proxy portu 1 ile 65535 aralığında olmalıdır.", - "proxy-user": "Proxy kullanıcı adı", + "proxy-port-range": "Proxy portu 1 ile 65535 arasında olmalıdır.", + "proxy-user": "Proxy kullanıcısı", "proxy-password": "Proxy şifresi", - "change-password": "Şifre değiştir", - "send-test-mail": "Test postası gönder", + "change-password": "Şifreyi değiştir", + "send-test-mail": "Test e-postası gönder", "sms-provider": "SMS sağlayıcı", "sms-provider-settings": "SMS sağlayıcı ayarları", "sms-provider-type": "SMS sağlayıcı türü", "sms-provider-type-required": "SMS sağlayıcı türü gereklidir.", "sms-provider-type-aws-sns": "Amazon SNS", "sms-provider-type-twilio": "Twilio", - "aws-access-key-id": "AWS Erişim Anahtarı Kimliği", - "aws-access-key-id-required": "AWS Erişim Anahtarı Kimliği gereklidir", - "aws-secret-access-key": "AWS Gizli Erişim Anahtarı", - "aws-secret-access-key-required": "AWS Gizli Erişim Anahtarı gereklidir", + "sms-provider-type-smpp": "SMPP", + "aws-access-key-id": "AWS Access Key ID", + "aws-access-key-id-required": "AWS Access Key ID gereklidir", + "aws-secret-access-key": "AWS Secret Access Key", + "aws-secret-access-key-required": "AWS Secret Access Key gereklidir", "aws-region": "AWS Bölgesi", "aws-region-required": "AWS Bölgesi gereklidir", "number-from": "Gönderen Telefon Numarası", "number-from-required": "Gönderen Telefon Numarası gereklidir.", - "number-to": "Gönderilen Telefon Numarası", - "number-to-required": "Gönderilen Telefon Numarası gereklidir.", - "phone-number-hint": "Telefon Numarası (E.164 formatında, ör: +905555555555)", - "phone-number-hint-twilio": "Telefon Numarası E.164 formatında/Telefon Numarasının SID'si/Mesajlaşma Hizmeti SID'si, ör: +905555555555/PNXXX/MGXXX", - "phone-number-pattern": "Geçersiz telefon numarası. E.164 formatında olmalıdır, ör: +905555555555.", - "phone-number-pattern-twilio": "Geçersiz telefon numarası. E.164 formatı/Telefon Numarasının SID'si/Mesaj Hizmeti SID'si olmalıdır, ör: +905555555555/PNXXX/MGXXX", + "number-to": "Alıcı Telefon Numarası", + "number-to-required": "Alıcı Telefon Numarası gereklidir.", + "phone-number-hint": "Telefon numarası E.164 formatında olmalı, örn. +19995550123", + "phone-number-hint-twilio": "Telefon numarası E.164 formatında/Telefon Numarası SID/Mesaj Servisi SID, örn. +19995550123/PNXXX/MGXXX", + "phone-number-pattern": "Geçersiz telefon numarası. E.164 formatında olmalı, örn. +19995550123.", + "phone-number-pattern-twilio": "Geçersiz telefon numarası. E.164 formatında/Telefon Numarası SID/Mesaj Servisi SID olmalı, örn. +19995550123/PNXXX/MGXXX.", "sms-message": "SMS mesajı", "sms-message-required": "SMS mesajı gereklidir.", "sms-message-max-length": "SMS mesajı 1600 karakterden uzun olamaz", - "twilio-account-sid": "Twilio Hesabı SID'si", - "twilio-account-sid-required": "Twilio Hesabı SID'si gereklidir", - "twilio-account-token": "Twilio Hesabı Token", - "twilio-account-token-required": "Twilio Hesabı Token gereklidir", - "send-test-sms": "Test SMS'i gönder", - "test-sms-sent": "Test SMS'i başarıyla gönderildi!", - "security-settings": "Güvenlik Ayarları", + "twilio-account-sid": "Twilio Hesap SID", + "twilio-account-sid-required": "Twilio Hesap SID gereklidir", + "twilio-account-token": "Twilio Hesap Token", + "twilio-account-token-required": "Twilio Hesap Token gereklidir", + "send-test-sms": "Test SMS gönder", + "test-sms-sent": "Test SMS başarıyla gönderildi!", + "security-settings": "Güvenlik ayarları", "password-policy": "Şifre politikası", "minimum-password-length": "Minimum şifre uzunluğu", - "minimum-password-length-required": "Minimum şifre uzunluğu zorunludur", - "minimum-password-length-range": "Minimum şifre uzunluğu 5 ile 50 arasında olmalıdır", + "minimum-password-length-required": "Minimum şifre uzunluğu gereklidir", + "minimum-password-length-range": "Minimum şifre uzunluğu 6 ile 50 arasında olmalıdır", + "maximum-password-length": "Maksimum şifre uzunluğu", + "maximum-password-length-min": "Maksimum şifre uzunluğu en az 6 olmalıdır", + "maximum-password-length-less-min": "Maksimum şifre uzunluğu minimum değerden büyük olmalıdır", "minimum-uppercase-letters": "Minimum büyük harf sayısı", "minimum-uppercase-letters-range": "Minimum büyük harf sayısı negatif olamaz", "minimum-lowercase-letters": "Minimum küçük harf sayısı", @@ -149,389 +196,762 @@ "minimum-digits-range": "Minimum rakam sayısı negatif olamaz", "minimum-special-characters": "Minimum özel karakter sayısı", "minimum-special-characters-range": "Minimum özel karakter sayısı negatif olamaz", - "password-expiration-period-days": "Gün bazlı şifre son kullanma peryodu", - "password-expiration-period-days-range": "Gün bazlı şifre son kullanma peryodu negatif olamaz", - "password-reuse-frequency-days": "Gün bazlı şifre yeniden kullanım sıklığı", - "password-reuse-frequency-days-range": "Gün bazlı şifre yeniden kullanım sıklığı negatif olamaz", + "password-expiration-period-days": "Şifre geçerlilik süresi (gün)", + "password-expiration-period-days-range": "Şifre geçerlilik süresi negatif olamaz", + "password-reuse-frequency-days": "Şifre tekrar kullanma sıklığı (gün)", + "password-reuse-frequency-days-range": "Şifre tekrar kullanma süresi negatif olamaz", + "allow-whitespace": "Boşluk karakterine izin ver", + "force-reset-password-if-no-valid": "Geçerli değilse şifre sıfırlamayı zorla", + "force-reset-password-if-no-valid-hint": "Bu özelliği etkinleştirirken dikkatli olun: Geçersiz şifreye sahip kullanıcıların e-posta ile şifre sıfırlaması gerekecektir.", "general-policy": "Genel politika", - "max-failed-login-attempts": "Hesap kilitlenmesi için gerekli maksimum hatalı giriş deneme sayısı", - "minimum-max-failed-login-attempts-range": "Hesap kilitlenmesi için gerekli maksimum hatalı giriş deneme sayısı negatif olamaz", - "user-lockout-notification-email": "Hesap kilidi kaldırıldığında bilgilendirme maili gönder", + "max-failed-login-attempts": "Hesap kilitlenmeden önce izin verilen maksimum başarısız giriş denemesi", + "minimum-max-failed-login-attempts-range": "Maksimum başarısız giriş denemesi negatif olamaz", + "user-lockout-notification-email": "Kullanıcı hesabı kilitlenirse e-posta bildirimi gönder", + "user-activation-token-ttl": "Kullanıcı etkinleştirme bağlantısı TTL (saat)", + "user-activation-token-ttl-range": "Kullanıcı etkinleştirme bağlantısı süresi 1 ile 24 saat arasında olmalıdır", + "password-reset-token-ttl": "Şifre sıfırlama bağlantısı TTL (saat)", + "password-reset-token-ttl-range": "Şifre sıfırlama bağlantısı süresi 1 ile 24 saat arasında olmalıdır", + "mobile-secret-key-length": "Mobil gizli anahtar uzunluğu", + "mobile-secret-key-length-range": "Mobil gizli anahtar uzunluğu pozitif olmalıdır", "domain-name": "Alan adı", - "domain-name-unique": "Alan adı ve protokolün benzersiz olması gerekir.", - "error-verification-url": "Bir alan adı '/' ve ':' sembollerini içermemelidir. Örnek: thingsboard.io", + "domain-name-unique": "Alan adı ve protokol benzersiz olmalıdır.", + "domain-name-max-length": "Alan adı 256 karakterden kısa olmalıdır", + "error-verification-url": "Alan adı '/' ve ':' karakterlerini içermemelidir. Örnek: thingsboard.io", + "connection-settings": "Bağlantı ayarları", "oauth2": { "access-token-uri": "Erişim belirteci URI'si", - "access-token-uri-required": "Erişim belirteci URI'si gerekli.", + "access-token-uri-required": "Erişim belirteci URI'si gereklidir.", "activate-user": "Kullanıcıyı etkinleştir", - "add-domain": "Alan ekle", - "delete-domain": "Alanı sil", + "add-domain": "Alan adı ekle", + "delete-domain": "Alan adını sil", "add-provider": "Sağlayıcı ekle", "delete-provider": "Sağlayıcıyı sil", - "allow-user-creation": "Kullanıcı oluşturmaya izin ver", + "allow-user-creation": "Kullanıcı oluşturulmasına izin ver", "always-fullscreen": "Her zaman tam ekran", "authorization-uri": "Yetkilendirme URI'si", - "authorization-uri-required": "Yetkilendirme URI'si gerekli.", + "authorization-uri-required": "Yetkilendirme URI'si gereklidir.", + "add-client": "OAuth 2.0 istemcisi ekle", + "client-details": "OAuth 2.0 istemci detayları", + "client": "OAuth 2.0 istemci", + "clients": "OAuth 2.0 istemcileri", + "no-oauth2-clients": "OAuth 2.0 istemcisi bulunamadı", + "search-oauth2-clients": "OAuth 2.0 istemcileri ara", + "delete-client-title": "OAuth 2.0 istemcisi '{{clientName}}' silinsin mi?", + "delete-client-text": "Dikkatli olun, onaydan sonra istemci ve tüm ilişkili veriler geri alınamaz hale gelecektir.", + "delete-mobile-app-title": "Mobil uygulama '{{applicationName}}' silinsin mi?", + "delete-mobile-app-text": "Dikkatli olun, onaydan sonra mobil uygulama ve tüm ilişkili veriler geri alınamaz hale gelecektir.", + "title": "Başlık", + "client-title-required": "Başlık gereklidir", + "client-title-max-length": "Başlık 100 karakterden kısa olmalıdır", + "advanced-settings": "Gelişmiş ayarlar", + "domain-details": "Alan adı detayları", + "no-domains": "Alan adı bulunamadı", + "search-domains": "Alan adlarını ara", + "mobile-app-details": "Mobil uygulama detayları", + "add-mobile-app": "Mobil uygulama ekle", + "no-mobile-apps": "Mobil uygulama bulunamadı", + "search-mobile-apps": "Mobil uygulama ara", + "send-token": "Belirteç gönder", + "create-new": "Yeni oluştur", "client-authentication-method": "İstemci kimlik doğrulama yöntemi", - "client-id": "Kullanıcı Grubu Kimliği", - "client-id-required": "Kullanıcı Grubu Kimliği gereklidir.", - "client-secret": "Kullanıcı Grubu Özel Anahtarı", - "client-secret-required": "Kullanıcı Grubu Özel Anahtarı gereklidir.", - "custom-setting": "Özel ayarlar", - "customer-name-pattern": "Kullanıcı Grubu adı kalıbı", - "default-dashboard-name": "Varsayılan pano adı", - "delete-domain-text": "Dikkatli olun, onaydan sonra bir alan adı ve tüm sağlayıcı verileri kullanılamayacak.", - "delete-domain-title": "'{{domainName}}' alan adının ayarlarını silmek istediğinizden emin misiniz?", - "delete-registration-text": "Dikkatli olun, onaydan sonra sağlayıcı verileri kullanılamayacak.", - "delete-registration-title": "'{{name}}' sağlayıcısını silmek istediğinizden emin misiniz?", + "client-id": "İstemci ID", + "client-id-required": "İstemci ID gereklidir.", + "client-id-max-length": "İstemci ID 256 karakterden kısa olmalıdır", + "client-secret": "İstemci gizli anahtarı", + "client-secret-required": "İstemci gizli anahtarı gereklidir.", + "client-secret-max-length": "Gizli anahtar 2049 karakterden kısa olmalıdır", + "custom-setting": "Özel ayar", + "customer-name-pattern": "Müşteri adı desen", + "customer-name-pattern-max-length": "Müşteri adı deseni 256 karakterden kısa olmalıdır", + "default-dashboard-name": "Varsayılan kontrol paneli adı", + "default-dashboard-name-max-length": "Varsayılan kontrol paneli adı 256 karakterden kısa olmalıdır", + "delete-domain-text": "Dikkatli olun, onaydan sonra alan adı ve tüm sağlayıcı verileri kullanılamaz hale gelecektir.", + "delete-domain-title": "Alan adı '{{domainName}}' silinsin mi?", + "delete-registration-text": "Dikkatli olun, onaydan sonra sağlayıcı verisi kullanılamaz hale gelecektir.", + "delete-registration-title": "Sağlayıcı '{{name}}' silinsin mi?", "email-attribute-key": "E-posta öznitelik anahtarı", - "email-attribute-key-required": "E-posta öznitelik anahtarı gerekli.", + "email-attribute-key-required": "E-posta öznitelik anahtarı gereklidir.", + "email-attribute-key-max-length": "E-posta öznitelik anahtarı 32 karakterden kısa olmalıdır", "first-name-attribute-key": "Ad öznitelik anahtarı", + "first-name-attribute-key-max-length": "Ad öznitelik anahtarı 32 karakterden kısa olmalıdır", "general": "Genel", - "jwk-set-uri": "JSON Web Anahtarı URI'sı", - "last-name-attribute-key": "Soyadı öznitelik anahtarı", + "jwk-set-uri": "JSON Web Key URI", + "last-name-attribute-key": "Soyad öznitelik anahtarı", + "last-name-attribute-key-max-length": "Soyad öznitelik anahtarı 32 karakterden kısa olmalıdır", "login-button-icon": "Giriş düğmesi simgesi", "login-button-label": "Sağlayıcı etiketi", - "login-button-label-placeholder": "$(Provider label) ile giriş yapın", - "login-button-label-required": "Etiket gerekli.", - "login-provider": "Giriş sağlayıcı", + "login-button-label-placeholder": "$(Provider label) ile giriş yap", + "login-button-label-required": "Etiket gereklidir.", + "login-provider": "Giriş sağlayıcısı", "mapper": "Eşleyici", - "new-domain": "Yeni alan", - "oauth2": "OAuth2", + "new-domain": "Yeni alan adı", + "oauth2": "OAuth 2.0", + "password-max-length": "Şifre 256 karakterden kısa olmalıdır", "redirect-uri-template": "Yönlendirme URI şablonu", "copy-redirect-uri": "Yönlendirme URI'sini kopyala", - "registration-id": "Kayıt Kimliği", - "registration-id-required": "Kayıt kimliği gerekli.", - "registration-id-unique": "Kayıt kimliğinin sistem için benzersiz olması gerekir.", + "registration-id": "Kayıt ID'si", + "registration-id-required": "Kayıt ID'si gereklidir.", + "registration-id-unique": "Kayıt ID'si sistem için benzersiz olmalıdır.", "scope": "Kapsam", - "scope-required": "Kapsam gerekli.", - "tenant-name-pattern": "Tenant isim modeli", - "tenant-name-pattern-required": "Tenant isim modeli gerekli.", - "tenant-name-strategy": "Tenant isim stratejisi", + "scope-required": "Kapsam gereklidir.", + "tenant-name-pattern": "Kiracı adı deseni", + "tenant-name-pattern-required": "Kiracı adı deseni gereklidir.", + "tenant-name-pattern-max-length": "Kiracı adı deseni 256 karakterden kısa olmalıdır", + "tenant-name-strategy": "Kiracı adı stratejisi", "type": "Eşleyici türü", - "uri-pattern-error": "Geçersiz URI biçimi.", + "uri-pattern-error": "Geçersiz URI formatı.", "url": "URL", - "url-pattern": "Geçersiz URL biçimi.", - "url-required": "URL gerekli.", + "url-pattern": "Geçersiz URL formatı.", + "url-required": "URL gereklidir.", + "url-max-length": "URL 256 karakterden kısa olmalıdır", "user-info-uri": "Kullanıcı bilgisi URI'si", - "user-info-uri-required": "Kullanıcı bilgisi URI'si gerekli.", + "user-info-uri-required": "Kullanıcı bilgisi URI'si gereklidir.", + "username-max-length": "Kullanıcı adı 256 karakterden kısa olmalıdır", "user-name-attribute-name": "Kullanıcı adı öznitelik anahtarı", - "user-name-attribute-name-required": "Kullanıcı adı öznitelik anahtarı gerekli", + "user-name-attribute-name-required": "Kullanıcı adı öznitelik anahtarı gereklidir", "protocol": "Protokol", "domain-schema-http": "HTTP", "domain-schema-https": "HTTPS", "domain-schema-mixed": "HTTP+HTTPS", - "enable": "OAuth2 ayarlarını etkinleştir", - "domains": "Etki Alanları", + "enable": "OAuth 2.0 ayarlarını etkinleştir", + "disable": "OAuth 2.0 ayarlarını devre dışı bırak", + "edge": "Edge'e yay", + "edge-enable": "Edge'e yaymayı etkinleştir", + "edge-disable": "Edge'e yaymayı devre dışı bırak", + "domains": "Alan adları", "mobile-apps": "Mobil uygulamalar", - "no-mobile-apps": "Yapılandırılan uygulama yok", "mobile-package": "Uygulama paketi", - "mobile-package-placeholder": "Ör.: benim.example.app", - "mobile-package-hint": "Android için: kendi benzersiz Uygulama Kimliğiniz. iOS için: Ürün paketi tanımlayıcısı.", + "mobile-package-placeholder": "Örn.: my.example.app", + "mobile-package-hint": "Android için: kendinize ait benzersiz Uygulama ID'si. iOS için: Ürün paket tanımlayıcısı.", "mobile-package-unique": "Uygulama paketi benzersiz olmalıdır.", - "mobile-app-secret": "Uygulama Özel Anahtarı", - "invalid-mobile-app-secret": "Uygulama Özel Anahtarı yalnızca alfasayısal karakterler içermeli ve 16 ila 2048 karakter uzunluğunda olmalıdır.", - "copy-mobile-app-secret": "Uygulama Özel Anahtarını Kopyala", - "add-mobile-app": "Uygulama ekle", - "delete-mobile-app": "Uygulama bilgilerini sil", + "mobile-package-required": "Uygulama paketi gereklidir.", + "mobile-package-max-length": "Uygulama paketi 256 karakterden kısa olmalıdır", + "mobile-package-spaces": "Uygulama paketi boşluk içeremez", + "mobile-app-secret": "Uygulama gizli anahtarı", + "mobile-app-secret-hint": "En az 512 bit veri temsil eden Base64 kodlu string.", + "mobile-app-secret-required": "Uygulama gizli anahtarı gereklidir.", + "mobile-app-secret-min-length": "Uygulama gizli anahtarı en az 512 bit veri içermelidir.", + "mobile-app-secret-base64": "Uygulama gizli anahtarı base64 formatında olmalıdır.", + "invalid-mobile-app-secret": "Uygulama gizli anahtarı yalnızca alfasayısal karakterler içermeli ve 16 ile 2048 karakter uzunluğunda olmalıdır.", + "copy-mobile-app-secret": "Uygulama gizli anahtarını kopyala", + "delete-mobile-app": "Uygulama bilgisini sil", "providers": "Sağlayıcılar", "platform-web": "Web", "platform-android": "Android", "platform-ios": "iOS", "all-platforms": "Tüm platformlar", - "allowed-platforms": "İzin verilen platformlar" - } + "smtp-provider": "SMTP sağlayıcı", + "allowed-platforms": "İzin verilen platformlar", + "authentication": "Kimlik doğrulama", + "basic": "Temel", + "provider": "Sağlayıcı", + "redirect-url": "Yönlendirme URI'si", + "domain-name": "Alan adı", + "domain-name-required": "Alan adı gereklidir", + "redirect-url-template": "Yönlendirme URI şablonu", + "microsoft-tenant-id": "Dizin (kiracı) ID", + "microsoft-tenant-id-required": "Dizin (kiracı) ID gereklidir", + "token-uri": "Belirteç URI'si", + "token-uri-required": "Belirteç URI'si gereklidir", + "redirect-uri": "Yönlendirme URI'si", + "google-provider": "Google", + "microsoft-provider": "Office 365", + "sendgrid-provider": "Sendgrid", + "custom-provider": "Özel", + "generate-access-token": "Erişim belirteci oluştur", + "update-access-token": "Erişim belirtecini güncelle", + "access-token-status": "Erişim belirteci durumu:", + "token-status-generated": "oluşturuldu", + "token-status-not-generated": "oluşturulmadı" + }, + "smpp-provider": { + "smpp-version": "SMPP sürümü", + "smpp-host": "SMPP sunucusu", + "smpp-host-required": "SMPP sunucusu gereklidir", + "smpp-port": "SMPP portu", + "smpp-port-required": "SMPP portu gereklidir", + "system-id": "Sistem ID", + "system-id-required": "Sistem ID gereklidir", + "password": "Şifre", + "password-required": "Şifre gereklidir", + "type-settings": "Tür ayarları", + "source-settings": "Kaynak ayarları", + "destination-settings": "Hedef ayarları", + "additional-settings": "Ek ayarlar", + "system-type": "Sistem türü", + "bind-type": "Bağlantı türü", + "service-type": "Hizmet türü", + "source-address": "Kaynak adresi", + "source-ton": "Kaynak TON", + "source-npi": "Kaynak NPI", + "destination-ton": "Hedef TON (Numara Türü)", + "destination-npi": "Hedef NPI (Numaralandırma Planı Tanımlayıcı)", + "address-range": "Adres aralığı", + "coding-scheme": "Kodlama şeması", + "bind-type-tx": "Gönderici", + "bind-type-rx": "Alıcı", + "bind-type-trx": "Gönderici/Alıcı", + "ton-unknown": "Bilinmiyor", + "ton-international": "Uluslararası", + "ton-national": "Ulusal", + "ton-network-specific": "Ağa Özel", + "ton-subscriber-number": "Abone Numarası", + "ton-alphanumeric": "Alfasayısal", + "ton-abbreviated": "Kısaltılmış", + "npi-unknown": "0 - Bilinmiyor", + "npi-isdn": "1 - ISDN/telefon numaralandırma planı (E163/E164)", + "npi-data-numbering-plan": "3 - Veri numaralandırma planı (X.121)", + "npi-telex-numbering-plan": "4 - Telex numaralandırma planı (F.69)", + "npi-land-mobile": "6 - Kara Mobil (E.212)", + "npi-national-numbering-plan": "8 - Ulusal numaralandırma planı", + "npi-private-numbering-plan": "9 - Özel numaralandırma planı", + "npi-ermes-numbering-plan": "10 - ERMES numaralandırma planı (ETSI DE/PS 3 01-3)", + "npi-internet": "13 - İnternet (IP)", + "npi-wap-client-id": "18 - WAP İstemci ID (WAP Forumu tarafından tanımlanacak)", + "scheme-smsc": "0 - SMSC Varsayılan Alfabe (Kısa ve uzun kod için ASCII, ücretsiz için GSM)", + "scheme-ia5": "1 - IA5 (Kısa ve uzun kod için ASCII, ücretsiz için Latin 9 (ISO-8859-9))", + "scheme-octet-unspecified-2": "2 - Oktet Belirsiz (8-bit ikili)", + "scheme-latin-1": "3 - Latin 1 (ISO-8859-1)", + "scheme-octet-unspecified-4": "4 - Oktet Belirsiz (8-bit ikili)", + "scheme-jis": "5 - JIS (X 0208-1990)", + "scheme-cyrillic": "6 - Kiril (ISO-8859-5)", + "scheme-latin-hebrew": "7 - Latin/İbranice (ISO-8859-8)", + "scheme-ucs-utf": "8 - UCS2/UTF-16 (ISO/IEC-10646)", + "scheme-pictogram-encoding": "9 - Piktogram Kodlaması", + "scheme-music-codes": "10 - Müzik Kodları (ISO-2022-JP)", + "scheme-extended-kanji-jis": "13 - Genişletilmiş Kanji JIS (X 0212-1990)", + "scheme-korean-graphic-character-set": "14 - Kore Grafik Karakter Kümesi (KS C 5601/KS X 1001)" + }, + "queue-select-name": "Kuyruk adını seçin", + "queue-name": "Ad", + "queue-name-required": "Kuyruk adı gereklidir!", + "queues": "Kuyruklar", + "queue-partitions": "Bölümler", + "queue-submit-strategy": "Gönderme stratejisi", + "queue-processing-strategy": "İşleme stratejisi", + "queue-configuration": "Kuyruk yapılandırması", + "repository-settings": "Depo ayarları", + "repository": "Depo", + "repository-url": "Depo URL'si", + "repository-url-required": "Depo URL'si gereklidir.", + "default-branch": "Varsayılan dal adı", + "repository-read-only": "Salt okunur", + "show-merge-commits": "Birleştirme commit'lerini göster", + "authentication-settings": "Kimlik doğrulama ayarları", + "auth-method": "Kimlik doğrulama yöntemi", + "auth-method-username-password": "Şifre / erişim belirteci", + "auth-method-username-password-hint": "GitHub kullanıcıları mutlaka depo üzerinde yazma yetkisi olan erişim belirteçlerini kullanmalıdır.", + "auth-method-private-key": "Özel anahtar", + "password-access-token": "Şifre / erişim belirteci", + "change-password-access-token": "Şifre / erişim belirtecini değiştir", + "private-key": "Özel anahtar", + "drop-private-key-file-or": "Bir özel anahtar dosyası sürükleyip bırakın veya", + "passphrase": "Parola", + "enter-passphrase": "Parola girin", + "change-passphrase": "Parolayı değiştir", + "check-access": "Erişimi kontrol et", + "check-repository-access-success": "Depo erişimi başarıyla doğrulandı!", + "delete-repository-settings-title": "Depo ayarlarını silmek istediğinizden emin misiniz?", + "delete-repository-settings-text": "Dikkatli olun, onaydan sonra depo ayarları silinecek ve sürüm kontrolü özelliği kullanılamaz hale gelecektir.", + "auto-commit-settings": "Otomatik işlem ayarları", + "auto-commit": "Otomatik işlem", + "auto-commit-entities": "Otomatik işlem varlıkları", + "no-auto-commit-entities-prompt": "Otomatik işlem için yapılandırılmış varlık yok", + "delete-auto-commit-settings-title": "Otomatik işlem ayarlarını silmek istediğinizden emin misiniz?", + "delete-auto-commit-settings-text": "Dikkatli olun, onaydan sonra otomatik işlem ayarları silinecek ve tüm varlıklar için otomatik işlem devre dışı kalacaktır.", + "mobile-app": { + "mobile-app": "Mobil uygulama", + "mobile-app-qr-code-widget-settings": "Mobil uygulama QR kod bileşeni ayarları", + "applications": "Uygulamalar", + "default": "Varsayılan", + "custom": "Özel", + "android": "Android", + "ios": "iOS", + "appearance": "Görünüm", + "appearance-on-home-page": "Ana sayfadaki görünüm", + "enabled": "Etkin", + "disabled": "Devre dışı", + "badges": "Rozetler", + "label": "Etiket", + "label-required": "Etiket gereklidir", + "label-max-length": "Etiket en fazla 50 karakter uzunluğunda olmalıdır", + "right": "Sağ", + "left": "Sol", + "set": "Ayarla", + "preview": "Önizleme", + "connect-mobile-app": "Mobil uygulamayı bağla", + "use-system-settings": "Sistem ayarlarını kullan" + }, + "2fa": { + "2fa": "İki faktörlü kimlik doğrulama", + "available-providers": "Mevcut sağlayıcılar", + "issuer-name": "Yayımlayıcı adı", + "issuer-name-required": "Yayımlayıcı adı gereklidir.", + "max-verification-failures-before-user-lockout": "Kullanıcı kilitlenmeden önceki maksimum doğrulama başarısızlığı", + "max-verification-failures-before-user-lockout-pattern": "Maksimum doğrulama başarısızlığı pozitif bir tamsayı olmalıdır.", + "number-of-checking-attempts": "Kontrol denemesi sayısı", + "number-of-checking-attempts-pattern": "Kontrol denemesi sayısı pozitif bir tamsayı olmalıdır.", + "number-of-checking-attempts-required": "Kontrol denemesi sayısı gereklidir.", + "number-of-codes": "Kod sayısı", + "number-of-codes-pattern": "Kod sayısı pozitif bir tamsayı olmalıdır.", + "number-of-codes-required": "Kod sayısı gereklidir.", + "provider": "Sağlayıcı", + "retry-verification-code-period": "Doğrulama kodu tekrar deneme süresi (sn)", + "retry-verification-code-period-pattern": "Minimum süre 5 saniyedir", + "retry-verification-code-period-required": "Doğrulama kodu tekrar deneme süresi gereklidir.", + "total-allowed-time-for-verification": "Toplam izin verilen doğrulama süresi (sn)", + "total-allowed-time-for-verification-pattern": "Minimum toplam süre 60 saniyedir", + "total-allowed-time-for-verification-required": "Toplam izin verilen süre gereklidir.", + "use-system-two-factor-auth-settings": "Sistem iki faktörlü doğrulama ayarlarını kullan", + "verification-code-check-rate-limit": "Doğrulama kodu kontrol sınırı", + "verification-code-lifetime": "Doğrulama kodu geçerlilik süresi (sn)", + "verification-code-lifetime-pattern": "Doğrulama kodu geçerlilik süresi pozitif bir tamsayı olmalıdır.", + "verification-code-lifetime-required": "Doğrulama kodu geçerlilik süresi gereklidir.", + "verification-message-template": "Doğrulama mesajı şablonu", + "verification-limitations": "Doğrulama sınırlamaları", + "verification-message-template-pattern": "Doğrulama mesajı şu deseni içermelidir: ${code}", + "verification-message-template-required": "Doğrulama mesajı şablonu gereklidir.", + "within-time": "Belirli süre içinde (sn)", + "within-time-pattern": "Süre pozitif bir tamsayı olmalıdır.", + "within-time-required": "Süre gereklidir." + }, + "jwt": { + "security-settings": "JWT güvenlik ayarları", + "issuer-name": "Yayımlayıcı adı", + "issuer-name-required": "Yayımlayıcı adı gereklidir.", + "signings-key": "İmzalama anahtarı", + "signings-key-hint": "En az 512 bit veri temsil eden Base64 kodlu string.", + "signings-key-required": "İmzalama anahtarı gereklidir.", + "signings-key-min-length": "İmzalama anahtarı en az 512 bit veri içermelidir.", + "signings-key-base64": "İmzalama anahtarı base64 formatında olmalıdır.", + "expiration-time": "Belirteç geçerlilik süresi (sn)", + "expiration-time-required": "Belirteç geçerlilik süresi gereklidir.", + "expiration-time-max": "Maksimum izin verilen süre 2147483647 saniyedir (68 yıl).", + "expiration-time-min": "Minimum süre 60 saniyedir (1 dakika).", + "refresh-expiration-time": "Yenileme belirteci geçerlilik süresi (sn)", + "refresh-expiration-time-required": "Yenileme belirteci geçerlilik süresi gereklidir.", + "refresh-expiration-time-max": "Maksimum izin verilen süre 2147483647 saniyedir (68 yıl).", + "refresh-expiration-time-min": "Minimum süre 900 saniyedir (15 dakika).", + "refresh-expiration-time-less-token": "Yenileme belirteci süresi, belirteç süresinden uzun olmalıdır.", + "generate-key": "Anahtar oluştur", + "info-header": "Tüm kullanıcılar yeniden oturum açmak zorunda kalacak", + "info-message": "JWT İmzalama Anahtarının değiştirilmesi tüm belirteçlerin geçersiz hale gelmesine neden olur. Tüm kullanıcılar yeniden oturum açmalıdır. Bu, Rest API/Websocket kullanan betikleri de etkiler." + }, + "resources": "Kaynaklar", + "notifications": "Bildirimler", + "notifications-settings": "Bildirim ayarları", + "slack-api-token": "Slack API anahtarı", + "slack": "Slack", + "slack-settings": "Slack ayarları", + "mobile-settings": "Mobil ayarlar", + "firebase-service-account-file": "Firebase servis hesabı kimlik bilgileri JSON dosyası", + "select-firebase-service-account-file": "Firebase servis hesabı kimlik bilgileri dosyanızı sürükleyip bırakın veya ", + "trendz": "Trendz", + "trendz-settings": "Trendz ayarları", + "trendz-url": "Trendz URL'si", + "trendz-url-required": "Trendz URL'si gereklidir", + "trendz-api-key": "Trendz API anahtarı", + "trendz-enable": "Trendz'i etkinleştir" }, "alarm": { "alarm": "Alarm", "alarms": "Alarmlar", - "select-alarm": "Alarm seç", + "all-alarms": "Tüm alarmlar", + "select-alarm": "Alarm seçin", "no-alarms-matching": "'{{entity}}' ile eşleşen alarm bulunamadı.", - "alarm-required": "Alarm gerekli", + "alarm-required": "Alarm gereklidir", + "alarm-filter": "Alarm filtresi", + "filter": "Filtre", "alarm-status": "Alarm durumu", - "alarm-status-list": "Alarm durum listesi", + "alarm-status-list": "Alarm durumu listesi", "any-status": "Herhangi bir durum", "search-status": { - "ANY": "Herhangi biri", + "ANY": "Herhangi", "ACTIVE": "Aktif", "CLEARED": "Temizlendi", "ACK": "Onaylandı", "UNACK": "Onaylanmadı" }, "display-status": { - "ACTIVE_UNACK": "Aktif Onaylanmadı", - "ACTIVE_ACK": "Aktif Onaylandı", - "CLEARED_UNACK": "Temizlendi Onaylanmadı", - "CLEARED_ACK": "Temizlendi Onaylandı" + "ACTIVE_UNACK": "Aktif Onaylanmamış", + "ACTIVE_ACK": "Aktif Onaylanmış", + "CLEARED_UNACK": "Temizlendi Onaylanmamış", + "CLEARED_ACK": "Temizlendi Onaylanmış" }, "no-alarms-prompt": "Alarm bulunamadı", - "created-time": "Oluşma zamanı", - "type": "Tip", - "severity": "Şiddet", - "originator": "Kaynak", - "originator-type": "Kaynak tipi", + "created-time": "Oluşturulma zamanı", + "type": "Tür", + "severity": "Önem derecesi", + "originator": "Başlatan", + "originator-type": "Başlatan türü", "details": "Detaylar", + "originator-label": "Başlatan etiketi", + "assign": "Ata", + "assignments": "Atamalar", + "assignee": "Atanan kişi", + "assignee-id": "Atanan ID", + "assignee-first-name": "Atanan ad", + "assignee-last-name": "Atanan soyad", + "assignee-email": "Atanan e-posta", + "unassigned": "Atanmamış", + "user-deleted": "Kullanıcı silindi", + "assignee-not-set": "Tümü", "status": "Durum", "alarm-details": "Alarm detayları", - "start-time": "Başlangıç zamanı", + "start-time": "Başlama zamanı", + "assign-time": "Atanma zamanı", "end-time": "Bitiş zamanı", - "ack-time": "Onaylanma zamanı", + "ack-time": "Onay zamanı", "clear-time": "Temizlenme zamanı", - "alarm-severity-list": "Alarm önem listesi", + "duration": "Süre", + "alarm-severity": "Alarm önem derecesi", + "alarm-severity-list": "Alarm önem derecesi listesi", "any-severity": "Herhangi bir önem derecesi", "severity-critical": "Kritik", - "severity-major": "Birincil", - "severity-minor": "İkincil", + "severity-major": "Büyük", + "severity-minor": "Küçük", "severity-warning": "Uyarı", "severity-indeterminate": "Belirsiz", "acknowledge": "Onayla", "clear": "Temizle", - "search": "Alarm ara", + "delete": "Sil", + "search": "Alarmlarda ara", "selected-alarms": "{ count, plural, =1 {1 alarm} other {# alarm} } seçildi", - "no-data": "Görüntülenecek veri bulunmuyor", - "polling-interval": "Alarm yoklama aralığı (saniye)", - "polling-interval-required": "Alarm yoklama aralığı gerekli.", - "min-polling-interval-message": "Alarm yoklama aralığı en az 1 saniye olmalıdır.", - "aknowledge-alarms-title": "{ count, plural, =1 {1 alarmı} other {# alarmı} } onayla", - "aknowledge-alarms-text": "{ count, plural, =1 {1 alarmı} other {# alarmı} } onaylamak istediğinize emin misiniz?", + "no-data": "Görüntülenecek veri yok", + "polling-interval": "Alarm sorgulama aralığı (sn)", + "polling-interval-required": "Alarm sorgulama aralığı gereklidir.", + "min-polling-interval-message": "En az 1 saniye sorgulama aralığına izin verilir.", + "aknowledge-alarms-title": "{ count, plural, =1 {1 alarm} other {# alarm} } Onayla", + "aknowledge-alarms-text": "{ count, plural, =1 {1 alarm} other {# alarm} } onaylamak istediğinizden emin misiniz?", "aknowledge-alarm-title": "Alarmı Onayla", "aknowledge-alarm-text": "Alarmı onaylamak istediğinizden emin misiniz?", - "clear-alarms-title": "{ count, plural, =1 {1 alarmı} other {# alarmı} } temizle", - "clear-alarms-text": "{ count, plural, =1 {1 alarmı} other {# alarmı} } temizlemek istediğinize emin misiniz?", + "selected-alarms-are-acknowledged": "Seçilen alarmlar zaten onaylanmış", + "clear-alarms-title": "{ count, plural, =1 {1 alarm} other {# alarm} } Temizle", + "clear-alarms-text": "{ count, plural, =1 {1 alarm} other {# alarm} } temizlemek istediğinizden emin misiniz?", "clear-alarm-title": "Alarmı Temizle", - "clear-alarm-text": "Alarmı silmek istediğinizden emin misiniz?", + "clear-alarm-text": "Alarmı temizlemek istediğinizden emin misiniz?", + "delete-alarms-title": "{ count, plural, =1 {1 alarm} other {# alarm} } Sil", + "delete-alarms-text": "{ count, plural, =1 {1 alarm} other {# alarm} } silmek istediğinizden emin misiniz?", + "selected-alarms-are-cleared": "Seçilen alarmlar zaten temizlenmiş", "alarm-status-filter": "Alarm Durum Filtresi", - "alarm-filter": "Alarm Filtresi", + "alarm-filter-title": "Alarm Filtresi", + "assigned": "Atanmış", + "filter-title": "Filtre", "max-count-load": "Yüklenecek maksimum alarm sayısı (0 - sınırsız)", - "max-count-load-required": "Yüklenecek maksimum alarm sayısı gerekli.", - "max-count-load-error-min": "Minimum değer 0'dır.", - "fetch-size": "İstek boyutu", - "fetch-size-required": "İstek boyutu gereklidir.", - "fetch-size-error-min": "Minimum değer 10'dur.", - "alarm-type-list": "Alarm tipi listesi", - "any-type": "Her hangi bir tür", - "search-propagated-alarms": "Yayılan alarmları ara" + "max-count-load-required": "Yüklenecek maksimum alarm sayısı gereklidir.", + "max-count-load-error-min": "Minimum değer 0 olmalıdır.", + "fetch-size": "Veri getirme boyutu", + "fetch-size-required": "Veri getirme boyutu gereklidir.", + "fetch-size-error-min": "Minimum değer 10 olmalıdır.", + "alarm-types": "Alarm türleri", + "alarm-type-list": "Alarm türü listesi", + "any-type": "Herhangi bir tür", + "assigned-to-current-user": "Geçerli kullanıcıya atanmış", + "assigned-to-me": "Bana atanmış", + "search-propagated-alarms": "Yayılan alarmlarda ara", + "comments": "Alarm yorumları", + "show-more": "Daha fazla göster", + "additional-info": "Ek bilgi", + "alarm-type": "Alarm türü", + "enter-alarm-type": "Alarm türünü girin", + "no-alarm-types-matching": "'{{entitySubtype}}' ile eşleşen alarm türü bulunamadı.", + "alarm-type-list-empty": "Seçilmiş alarm türü yok." + }, + "alarm-activity": { + "add": "Yorum ekle...", + "alarm-comment": "Alarm yorumu", + "comments": "Yorumlar", + "delete-alarm-comment": "Bu yorumu silmek istiyor musunuz?", + "refresh": "Yenile", + "oldest-first": "En eskiden", + "newest-first": "En yeniden", + "activity": "Aktivite", + "export": "CSV'ye dışa aktar", + "author": "Yazar", + "created-date": "Oluşturulma tarihi", + "edited-date": "Düzenlenme tarihi", + "text": "Metin", + "system": "Sistem" }, "alias": { - "add": "Kısa ad ekle", - "edit": "Kısa ad düzenle", - "name": "Kısa ad", - "name-required": "Kısa ad gerekli", - "duplicate-alias": "Aynı kısa ad daha önce kullanılmış.", - "filter-type-single-entity": "Tek öğe", - "filter-type-entity-list": "Öğe listesi", - "filter-type-entity-name": "Öğe adı", - "filter-type-state-entity": "Gösterge panelinden öğe", - "filter-type-state-entity-description": "Gösterge paneli durum parametrelerinden alınan öğeler", + "add": "Takma ad ekle", + "edit": "Takma adı düzenle", + "name": "Takma ad adı", + "name-required": "Takma ad adı gerekli", + "duplicate-alias": "Aynı adda bir takma ad zaten mevcut.", + "filter-type-single-entity": "Tek varlık", + "filter-type-entity-list": "Varlık listesi", + "filter-type-entity-name": "Varlık adı", + "filter-type-entity-type": "Varlık türü", + "filter-type-state-entity": "Pano durumundan varlık", + "filter-type-state-entity-description": "Pano durumu parametrelerinden alınan varlık", "filter-type-asset-type": "Varlık türü", - "filter-type-asset-type-description": "'{{assetTypes}}' türünde varlıklar", - "filter-type-asset-type-and-name-description": "Adı '{{prefix}}' ile başlayan '{{assetTypes}}' türünde varlıklar", + "filter-type-asset-type-description": "'{{assetTypes}}' türündeki varlıklar", + "filter-type-asset-type-and-name-description": "'{{assetTypes}}' türünde ve adı '{{prefix}}' ile başlayan varlıklar", "filter-type-device-type": "Cihaz türü", - "filter-type-device-type-description": "'{{deviceTypes}}' türünde cihazlar", - "filter-type-device-type-and-name-description": "Adı '{{prefix}}' ile başlayan'{{deviceTypes}}' türünde cihazlar", - "filter-type-entity-view-type": "Öğe Görünümü türü", - "filter-type-entity-view-type-description": "'{{entityViewTypes}}' türünde Öğe Görünümleri", - "filter-type-entity-view-type-and-name-description": "'{{entityViewTypes}}' türünde ve adı '{{prefix}}' ile başlayan Öğe Görünümleri", - "filter-type-edge-type": "Uç tipi", - "filter-type-edge-type-description": "'{{edgeTypes}}' türünün uçları", - "filter-type-edge-type-and-name-description": "'{{edgeTypes}}' türü ve adı '{{prefix}}' ile başlayan kenarlar", - "filter-type-relations-query": "İlişkiler sorgusu", - "filter-type-relations-query-description": "{{relationType}} türünde ilişkili olan öğeler: {{entities}}. {{direction}}: {{rootEntity}}", + "filter-type-device-type-description": "'{{deviceTypes}}' türündeki cihazlar", + "filter-type-device-type-and-name-description": "'{{deviceTypes}}' türünde ve adı '{{prefix}}' ile başlayan cihazlar", + "filter-type-entity-view-type": "Varlık Görünümü türü", + "filter-type-entity-view-type-description": "'{{entityViewTypes}}' türündeki Varlık Görünümleri", + "filter-type-entity-view-type-and-name-description": "'{{entityViewTypes}}' türünde ve adı '{{prefix}}' ile başlayan Varlık Görünümleri", + "filter-type-edge-type": "Edge türü", + "filter-type-edge-type-description": "'{{edgeTypes}}' türündeki Edge'ler", + "filter-type-edge-type-and-name-description": "'{{edgeTypes}}' türünde ve adı '{{prefix}}' ile başlayan Edge'ler", + "filter-type-relations-query": "İlişki sorgusu", + "filter-type-relations-query-description": "{{entities}}, {{relationType}} ilişkisine sahip {{direction}} {{rootEntity}}", + "filter-type-edge-search-query": "Edge arama sorgusu", + "filter-type-edge-search-query-description": "{{edgeTypes}} türündeki ve {{relationType}} ilişkisine sahip {{direction}} {{rootEntity}} Edge'ler", "filter-type-asset-search-query": "Varlık arama sorgusu", - "filter-type-asset-search-query-description": "{{relationType}} türünde ilişkisi olan varlıklar {{assetTypes}}. {{direction}}: {{rootEntity}}", + "filter-type-asset-search-query-description": "{{assetTypes}} türünde ve {{relationType}} ilişkisine sahip {{direction}} {{rootEntity}} varlıklar", "filter-type-device-search-query": "Cihaz arama sorgusu", - "filter-type-device-search-query-description": "{{relationType}} türünde ilişkisi olan cihaz tipleri {{deviceTypes}}. {{direction}}: {{rootEntity}}", - "filter-type-entity-view-search-query": "Öğe görünümü arama sorgusu", - "filter-type-entity-view-search-query-description": "{{relationType}} {{direction}} {{rootEntity}} ilişkisine sahip {{entityViewTypes}} türlerine sahip öğe görünümleri", + "filter-type-device-search-query-description": "{{deviceTypes}} türünde ve {{relationType}} ilişkisine sahip {{direction}} {{rootEntity}} cihazlar", + "filter-type-entity-view-search-query": "Varlık görünümü arama sorgusu", + "filter-type-entity-view-search-query-description": "{{entityViewTypes}} türünde ve {{relationType}} ilişkisine sahip {{direction}} {{rootEntity}} varlık görünümleri", "filter-type-apiUsageState": "API Kullanım Durumu", - "filter-type-edge-search-query": "Uç arama sorgusu", - "filter-type-edge-search-query-description": "{{relationType}} {{direction}} {{rootEntity}} ilişkisine sahip {{edgeType}} türlerine sahip uçlar", - "entity-filter": "Öğe filtresi", - "resolve-multiple": "Çoklu öğe olarak çözümle", - "filter-type": "Filtre tipi", - "filter-type-required": "Filtre tipi gerekli.", - "entity-filter-no-entity-matched": "Belirlenen filtre ile eşleşen bir öğe bulunamadı.", - "no-entity-filter-specified": "Hiçbir öğe filtresi belirtilmedi", - "root-state-entity": "Gösterge panelini kök olarak kullan", - "last-level-relation": "Yalnızca son düzey ilişkiyi getir", - "root-entity": "Kök öğe", + "entity-filter": "Varlık filtresi", + "resolve-multiple": "Birden fazla varlık olarak çöz", + "resolve-multiple-hint": "Filtrelenen tüm varlıklardan verileri aynı anda göstermek için etkinleştirin. \nDevre dışı bırakıldığında, bileşen yalnızca seçilen varlıktan veri gösterir.", + "filter-type": "Filtre türü", + "filter-type-required": "Filtre türü gerekli.", + "entity-filter-no-entity-matched": "Belirtilen filtreyle eşleşen varlık bulunamadı.", + "no-entity-filter-specified": "Herhangi bir varlık filtresi belirtilmedi", + "root-state-entity": "Kök olarak pano durumu varlığını kullan", + "last-level-relation": "Yalnızca son düzey ilişkiyi al", + "root-entity": "Kök varlık", "state-entity-parameter-name": "Durum varlığı parametre adı", - "default-state-entity": "Varsayılan durum öğesi", - "default-entity-parameter-name": "Varsayılan", + "default-state-entity": "Varsayılan pano durumu varlığı", + "default-entity-parameter-name": "Varsayılan olarak", + "query-options": "Sorgu seçenekleri", "max-relation-level": "Maksimum ilişki düzeyi", - "unlimited-level": "Sınırsız seviye", - "state-entity": "Gösterge paneli öğesi", - "all-entities": "Tüm öğeler", - "any-relation": "Herhangi biri" + "unlimited-level": "Sınırsız düzey", + "state-entity": "Pano durumu varlığı", + "all-entities": "Tüm varlıklar", + "any-relation": "herhangi" }, "asset": { "asset": "Varlık", "assets": "Varlıklar", - "management": "Varlık Yönetimi", + "management": "Varlık yönetimi", "view-assets": "Varlıkları Görüntüle", "add": "Varlık ekle", - "assign-to-customer": "Kullanıcı grubuna ata", - "assign-asset-to-customer": "Varlıkları Kullanıcı Grubuna Ata", - "assign-asset-to-customer-text": "Lütfen kullanıcı grubuna atanacak varlıkları seçin", + "asset-type-max-length": "Varlık türü 256 karakterden kısa olmalıdır", + "assign-to-customer": "Müşteriye ata", + "assign-asset-to-customer": "Varlık(ları) Müşteriye Ata", + "assign-asset-to-customer-text": "Lütfen müşteriye atanacak varlıkları seçin", "no-assets-text": "Varlık bulunamadı", - "assign-to-customer-text": "Lütfen varlıkları atamak için kullanıcı grubu seçin", - "public": "Açık", - "assignedToCustomer": "Kullanıcı grubuna atandı", - "make-public": "Varlığı açık hale getir", - "make-private": "Varlığı özel hale getir", - "unassign-from-customer": "Kullanıcı grubundan atamayı kaldır", + "assign-to-customer-text": "Lütfen varlık(ları) atayacağınız müşteriyi seçin", + "public": "Genel", + "assignedToCustomer": "Müşteriye atanmış", + "make-public": "Varlığı genel yap", + "make-private": "Varlığı özel yap", + "unassign-from-customer": "Müşteriden atamayı kaldır", "delete": "Varlığı sil", - "asset-public": "Varlık açık halde", + "asset-public": "Varlık geneldir", "asset-type": "Varlık türü", - "asset-type-required": "Varlık türü gerekli.", - "select-asset-type": "Varlık türü seçin", - "enter-asset-type": "Varlık türü girin", + "asset-type-required": "Varlık türü gereklidir.", + "select-asset-type": "Varlık türü seç", + "enter-asset-type": "Varlık profilini girin", "any-asset": "Herhangi bir varlık", - "no-asset-types-matching": "'{{entitySubtype}}' ile eşleşen varlık bulunamadı.", - "asset-type-list-empty": "Herhangi bir varlık türü bulunamadı.", + "no-asset-types-matching": "'{{entitySubtype}}' ile eşleşen varlık türü bulunamadı.", + "asset-type-list-empty": "Seçilmiş varlık türü yok.", "asset-types": "Varlık türleri", - "name": "İsim", - "name-required": "İsim gerekli.", + "name": "Ad", + "name-required": "Ad gereklidir.", + "name-max-length": "Ad 256 karakterden kısa olmalıdır", + "label-max-length": "Etiket 256 karakterden kısa olmalıdır", "description": "Açıklama", "type": "Tür", - "type-required": "Tür gerekli.", + "type-required": "Tür gereklidir.", "details": "Detaylar", - "events": "Etkinlikler", + "events": "Olaylar", "add-asset-text": "Yeni varlık ekle", "asset-details": "Varlık detayları", "assign-assets": "Varlıkları ata", - "assign-assets-text": "{ count, plural, =1 {1 varlığı} other {# varlığı} } kullanıcı grubuna ata", - "assign-asset-to-edge-title": "Varlıkları Uç'a Ata", - "assign-asset-to-edge-text": "Lütfen uca atanacak varlıkları seçin", + "assign-assets-text": "{ count, plural, =1 {1 varlık} other {# varlık} } müşteriye ata", + "assign-asset-to-edge-title": "Varlık(ları) Edge'e Ata", + "assign-asset-to-edge-text": "Lütfen Edge'e atanacak varlıkları seçin", "delete-assets": "Varlıkları sil", - "unassign-assets": "Varlıkların atamalarını kaldır", - "unassign-assets-action-title": "{ count, plural, =1 {1 varlığın} other {# varlığın} } atamalarını kullanıcı grubundan kaldır", + "unassign-assets": "Varlıkların atamasını kaldır", + "unassign-assets-action-title": "{ count, plural, =1 {1 varlık} other {# varlık} } müşteriden atamasını kaldır", "assign-new-asset": "Yeni varlık ata", - "delete-asset-title": "'{{assetName}}' isimli varlığı silmek istediğinize emin misiniz?", - "delete-asset-text": "UYARI: Onaylandıktan sonra varlık ve ilgili tüm veriler geri yüklenemeyecek şekilde silinecek.", - "delete-assets-title": "{ count, plural, =1 {1 varlığı} other {# varlığı} } silmek istediğinize emin misiniz?", - "delete-assets-action-title": "{ count, plural, =1 {1 varlığı} other {# varlığı} } sil", - "delete-assets-text": "UYARI: Onaylandıktan sonra tüm seçili varlıklar ver ilgili tüm veriler geri yüklenemyeck şekilde silinecek.", - "make-public-asset-title": "'{{assetName}}' isimli varlığı açık hale getirmek istediğinize emin misiniz?", - "make-public-asset-text": "Onaylandıktan sonra varlık ve ilgili veriler açık hale gelecek ve başkaları tarafından erişilebilir olacaktır.", - "make-private-asset-title": "'{{assetName}}' isimli varlığı özel hale getirmek istediğinize emin misiniz?", - "make-private-asset-text": "Onaylandıktan sonra varlık ve ilgili veriler özel hale gelecek ve başkaları tarafından erişilemez olacaktır.", - "unassign-asset-title": "'{{assetName}}' isimli varlığın atamasını kaldırmak istediğinize emin misiniz?", - "unassign-asset-text": "Onaylandıktan sonra varlığın ataması kaldırılacak ve kullanıcı grubu tarafından erişilemez olacaktır.", - "unassign-asset": "Varlık atamasını kaldır", - "unassign-assets-title": " { count, plural, =1 {1 varlık} other {# varlık} } atamasını kaldırmak istediğinize emin misiniz?", - "unassign-assets-text": "Onaylandıktan sonra tüm seçili varlıkların ataması kaldırılacak ve kullanıcı grubu tarafından erişilemez olacaktır.", - "unassign-assets-from-edge": "Uçtan varlıkların atamasını kaldır", - "copyId": "Varlık kimliğini kopyala", - "idCopiedMessage": "Varlık kimliği panoya kopyalandı", - "select-asset": "Varlık seç", - "no-assets-matching": "'{{entity}}' isimli varlık bulunamadı.", - "asset-required": "Varlık gerekli", - "name-starts-with": "... ile başlayan varlık adı", - "help-text": "İhtiyaca göre '%' kullanın: '%asset_name_contains%', '%asset_name_ends', 'asset_starts_with'.", + "delete-asset-title": "'{{assetName}}' adlı varlığı silmek istediğinizden emin misiniz?", + "delete-asset-text": "Dikkat! Onaydan sonra varlık ve tüm ilgili veriler geri alınamaz şekilde silinecektir.", + "delete-assets-title": "{ count, plural, =1 {1 varlık} other {# varlık} } silmek istediğinizden emin misiniz?", + "delete-assets-action-title": "{ count, plural, =1 {1 varlık} other {# varlık} } sil", + "delete-assets-text": "Dikkat! Onaydan sonra tüm seçilen varlıklar silinecek ve ilgili tüm veriler geri alınamaz olacaktır.", + "make-public-asset-title": "'{{assetName}}' adlı varlığı genel yapmak istediğinizden emin misiniz?", + "make-public-asset-text": "Onaydan sonra varlık ve tüm verileri genel hale gelecek ve başkaları tarafından erişilebilir olacaktır.", + "make-private-asset-title": "'{{assetName}}' adlı varlığı özel yapmak istediğinizden emin misiniz?", + "make-private-asset-text": "Onaydan sonra varlık ve tüm verileri özel hale gelecek ve başkaları tarafından erişilemez olacaktır.", + "unassign-asset-title": "'{{assetName}}' adlı varlığın atamasını kaldırmak istediğinizden emin misiniz?", + "unassign-asset-text": "Onaydan sonra varlık atanmamış olacak ve müşteri tarafından erişilemeyecektir.", + "unassign-asset": "Varlığın atamasını kaldır", + "unassign-assets-title": "{ count, plural, =1 {1 varlık} other {# varlık} } atamasını kaldırmak istediğinizden emin misiniz?", + "unassign-assets-text": "Onaydan sonra seçilen tüm varlıklar atanmamış olacak ve müşteri tarafından erişilemeyecektir.", + "copyId": "Varlık ID'sini kopyala", + "idCopiedMessage": "Varlık ID'si panoya kopyalandı", + "select-asset": "Varlık seçin", + "no-assets-matching": "'{{entity}}' ile eşleşen varlık bulunamadı.", + "asset-required": "Varlık gereklidir", + "name-starts-with": "Varlık adı ifadesi", + "help-text": "İhtiyaca göre '%' kullanın: '%varlık_adı_içerir%', '%varlık_adı_biter', 'varlık_adı_başlar'.", + "search": "Varlıklarda ara", "import": "Varlıkları içe aktar", "asset-file": "Varlık dosyası", "label": "Etiket", - "search": "Varlık ara", - "assign-asset-to-edge": "Varlıkları Uç'a Ata", - "unassign-asset-from-edge": "Öğe atamasını kaldır", - "unassign-asset-from-edge-title": "'{{assetName}}' öğesinin atamasını kaldırmak istediğinizden emin misiniz?", - "unassign-asset-from-edge-text": "Onaydan sonra varlığın ataması kaldırılacak ve uç tarafından erişilebilir olmayacaktır.", + "assign-asset-to-edge": "Varlık(ları) Edge'e Ata", + "unassign-asset-from-edge": "Varlığın atamasını kaldır", + "unassign-asset-from-edge-title": "'{{assetName}}' adlı varlığın Edge atamasını kaldırmak istediğinizden emin misiniz?", + "unassign-asset-from-edge-text": "Onaydan sonra varlık atanmamış olacak ve Edge tarafından erişilemeyecektir.", "unassign-assets-from-edge-title": "{ count, plural, =1 {1 varlık} other {# varlık} } atamasını kaldırmak istediğinizden emin misiniz?", - "unassign-assets-from-edge-text": "Onaydan sonra seçilen tüm varlıkların ataması kaldırılacak ve uç tarafından erişilebilir olmayacaktır.", + "unassign-assets-from-edge-text": "Onaydan sonra tüm seçilen varlıklar atanmamış olacak ve Edge tarafından erişilemeyecektir.", "selected-assets": "{ count, plural, =1 {1 varlık} other {# varlık} } seçildi" }, "attribute": { "attributes": "Öznitelikler", - "latest-telemetry": "Son telemetri", + "latest-telemetry": "En son telemetri", + "no-latest-telemetry": "En son telemetri bulunamadı", "attributes-scope": "Varlık öznitelik kapsamı", - "scope-telemetry": "telemetri", - "scope-latest-telemetry": "Son telemetri", - "scope-client": "İstemci öznitelikler", - "scope-server": "Sunucu öznitelikler", + "scope-telemetry": "Telemetri", + "scope-latest-telemetry": "En son telemetri", + "scope-client": "İstemci öznitelikleri", + "scope-server": "Sunucu öznitelikleri", "scope-shared": "Paylaşılan öznitelikler", + "scope-client-short": "İstemci", + "scope-server-short": "Sunucu", + "scope-shared-short": "Paylaşılan", + "scope-latest-short": "En son", + "scope-any": "Herhangi", "add": "Öznitelik ekle", "key": "Anahtar", + "key-max-length": "Anahtar 256 karakterden kısa olmalıdır", "last-update-time": "Son güncelleme zamanı", - "key-required": "Öznitelik anahtarı gerekli.", + "key-required": "Öznitelik anahtarı gereklidir.", "value": "Değer", - "value-required": "Öznitelik değeri gerekli.", - "delete-attributes-title": "Silmek istediğinize emin misiniz { count, plural, =1 {1 öznitelik} other {# öznitelik} }?", - "delete-attributes-text": "UYARI: Onaylandıktan sonra tüm seçili öznitelikler kaldırılacak.", + "value-required": "Öznitelik değeri gereklidir.", + "telemetry-key-required": "Telemetri anahtarı gereklidir", + "telemetry-value-required": "Telemetri değeri gereklidir", + "delete-attributes-title": "{ count, plural, =1 {1 özniteliği} other {# özniteliği} } silmek istediğinizden emin misiniz?", + "delete-attributes-text": "Dikkat, onaydan sonra tüm seçilen öznitelikler silinecektir.", "delete-attributes": "Öznitelikleri sil", - "enter-attribute-value": "Öznitelik değeri gir", - "show-on-widget": "Göstergede göster", - "widget-mode": "Gösterge modu", - "next-widget": "Sonraki gösterge", - "prev-widget": "Önceki gösterge", + "enter-attribute-value": "Öznitelik değeri girin", + "show-on-widget": "Bileşende göster", + "widget-mode": "Bileşen modu", + "next-widget": "Sonraki bileşen", + "prev-widget": "Önceki bileşen", "add-to-dashboard": "Gösterge paneline ekle", - "add-widget-to-dashboard": "Göstergeyi, gösterge paneline ekle", + "add-widget-to-dashboard": "Bileşeni gösterge paneline ekle", "selected-attributes": "{ count, plural, =1 {1 öznitelik} other {# öznitelik} } seçildi", "selected-telemetry": "{ count, plural, =1 {1 telemetri birimi} other {# telemetri birimi} } seçildi", "no-attributes-text": "Öznitelik bulunamadı", - "no-telemetry-text": "Telemetri bulunamadı" + "no-telemetry-text": "Telemetri bulunamadı", + "copy-key": "Anahtarı kopyala", + "add-telemetry": "Telemetri ekle", + "copy-value": "Değeri kopyala", + "delete-timeseries": { + "start-time": "Başlangıç zamanı", + "ends-on": "Bitiş zamanı", + "strategy": "Strateji", + "delete-strategy": "Silme stratejisi", + "all-data": "Tüm veriyi sil", + "all-data-except-latest-value": "En son değer hariç tüm veriyi sil", + "latest-value": "En son değeri sil", + "all-data-for-time-period": "Belirli zaman dilimi için tüm veriyi sil", + "rewrite-latest-value": "En son değeri yeniden yaz" + } }, "api-usage": { - "api-usage": "API Kullanımı", + "api-features": "API özellikleri", + "api-usage": "API kullanımı", "alarm": "Alarm", - "alarms-created": "Oluşturulan Alarmlar", + "alarms-created": "Oluşturulan alarmlar", + "queue-stats": "Kuyruk İstatistikleri", + "processing-failures-and-timeouts": "İşleme Hataları ve Zaman Aşımı", + "exceptions": "İstisnalar", "alarms-created-daily-activity": "Günlük oluşturulan alarmlar", "alarms-created-hourly-activity": "Saatlik oluşturulan alarmlar", "alarms-created-monthly-activity": "Aylık oluşturulan alarmlar", "data-points": "Veri noktaları", - "data-points-storage-days": "Veri noktaları depolama günleri", + "data-points-storage-days": "Veri noktası saklama süresi (gün)", + "device-api": "Cihaz API'si", "email": "E-posta", "email-messages": "E-posta mesajları", - "email-messages-daily-activity": "Günlük E-posta mesajları", - "email-messages-monthly-activity": "Aylık E-posta mesajları", - "exceptions": "Sıradışı Durumlar", + "email-messages-daily-activity": "Günlük e-posta mesajları", + "email-messages-monthly-activity": "Aylık e-posta mesajları", "executions": "Çalıştırmalar", + "scripts": "Komut dosyaları", + "scripts-hourly-activity": "Saatlik komut dosyası etkinliği", + "scripts-daily-activity": "Günlük komut dosyası etkinliği", + "scripts-monthly-activity": "Aylık komut dosyası etkinliği", "javascript": "JavaScript", "javascript-executions": "JavaScript çalıştırmaları", - "latest-error": "Son Hata", + "tbel": "TBEL", + "tbel-executions": "TBEL çalıştırmaları", + "latest-error": "Son hata", "messages": "Mesajlar", "notifications": "Bildirimler", "notifications-email-sms": "Bildirimler (E-posta/SMS)", - "notifications-hourly-activity": "Saatlik Bildirimler", + "notifications-hourly-activity": "Saatlik bildirim etkinliği", "permanent-failures": "${entityName} Kalıcı Hatalar", "permanent-timeouts": "${entityName} Kalıcı Zaman Aşımları", "processing-failures": "${entityName} İşleme Hataları", - "processing-failures-and-timeouts": "İşleme Hataları ve Zaman Aşımları", "processing-timeouts": "${entityName} İşleme Zaman Aşımları", - "queue-stats": "Sıra İstatistikleri", "rule-chain": "Kural Zinciri", "rule-engine": "Kural Motoru", - "rule-engine-daily-activity": "Günlük Rule Engine etkinliği", - "rule-engine-executions": "Kural Motoru yürütmeleri", - "rule-engine-hourly-activity": "Saatlik Rule Engine etkinliği", - "rule-engine-monthly-activity": "Aylık Rule Engine etkinliği", + "rule-engine-daily-activity": "Kural Motoru günlük etkinliği", + "rule-engine-executions": "Kural Motoru çalıştırmaları", + "rule-engine-hourly-activity": "Kural Motoru saatlik etkinliği", + "rule-engine-monthly-activity": "Kural Motoru aylık etkinliği", "rule-engine-statistics": "Kural Motoru İstatistikleri", "rule-node": "Kural Düğümü", "sms": "SMS", "sms-messages": "SMS mesajları", - "sms-messages-daily-activity": "Günlük SMS mesajları etkinliği", - "sms-messages-monthly-activity": "Aylık SMS mesajları etkinliği", + "sms-messages-daily-activity": "Günlük SMS mesajları", + "sms-messages-monthly-activity": "Aylık SMS mesajları", "successful": "${entityName} Başarılı", "telemetry": "Telemetri", "telemetry-persistence": "Telemetri kalıcılığı", - "telemetry-persistence-daily-activity": "Günlük Telemetri kalıcılığı", - "telemetry-persistence-hourly-activity": "Saatlik Telemetri kalıcılığı", - "telemetry-persistence-monthly-activity": "Aylık Telemetri kalıcılığı", - "transport": "Aktarım", - "transport-daily-activity": "Günlük Aktarım etkinliği", - "transport-data-points": "Aktarım veri noktaları", - "transport-hourly-activity": "Saatlik Aktarım etkinliği", - "transport-messages": "Aktarım mesajları", - "transport-monthly-activity": "Aylık Aktarım etkinliği", - "view-details": "Detayları göster", + "telemetry-persistence-daily-activity": "Telemetri kalıcılığı günlük etkinliği", + "telemetry-persistence-hourly-activity": "Telemetri kalıcılığı saatlik etkinliği", + "telemetry-persistence-monthly-activity": "Telemetri kalıcılığı aylık etkinliği", + "transport": "İletim", + "transport-daily-activity": "İletim günlük etkinliği", + "transport-data-points": "İletim veri noktaları", + "transport-hourly-activity": "İletim saatlik etkinliği", + "transport-messages": "İletim mesajları", + "transport-monthly-activity": "İletim aylık etkinliği", + "view-details": "Detayları görüntüle", "view-statistics": "İstatistikleri görüntüle" }, + "api-limit": { + "cassandra-write-queries-core": "Rest API Cassandra yazma sorguları", + "cassandra-read-queries-core": "Rest API ve WS telemetri Cassandra okuma sorguları", + "cassandra-write-queries-rule-engine": "Kural Motoru telemetri Cassandra yazma sorguları", + "cassandra-read-queries-rule-engine": "Kural Motoru telemetri Cassandra okuma sorguları", + "cassandra-write-queries-monolith": "Monolith telemetri Cassandra yazma sorguları", + "cassandra-read-queries-monolith": "Monolith telemetri Cassandra okuma sorguları", + "entity-version-creation": "Varlık sürümü oluşturma", + "entity-version-load": "Varlık sürümü yükleme", + "notification-requests": "Bildirim istekleri", + "notification-requests-per-rule": "Kural başına bildirim istekleri", + "rest-api-requests": "REST API istekleri", + "rest-api-requests-per-customer": "Müşteri başına REST API istekleri", + "transport-messages": "İletim mesajları", + "transport-messages-per-device": "Cihaz başına iletim mesajları", + "transport-messages-per-gateway": "Ağ geçidi başına iletim mesajları", + "transport-messages-per-gateway-device": "Ağ geçidi cihazı başına iletim mesajları", + "ws-updates-per-session": "Oturum başına WS güncellemeleri", + "edge-events": "Edge olayları", + "edge-events-per-edge": "Edge başına olaylar", + "edge-uplink-messages": "Edge yukarı bağlantı mesajları", + "edge-uplink-messages-per-edge": "Edge başına yukarı bağlantı mesajları" + }, "audit-log": { - "audit": "Log ve Hata Yönetimi", - "audit-logs": "Loglar ve Hatalar", - "timestamp": "Zaman", - "entity-type": "Öğe Türü", - "entity-name": "Öğe İsmi", + "audit": "Denetim", + "audit-logs": "Denetim günlükleri", + "timestamp": "Zaman damgası", + "entity-type": "Varlık türü", + "entity-name": "Varlık adı", "user": "Kullanıcı", "type": "Tür", "status": "Durum", @@ -541,486 +961,979 @@ "type-updated": "Güncellendi", "type-attributes-updated": "Öznitelikler güncellendi", "type-attributes-deleted": "Öznitelikler silindi", - "type-rpc-call": "Uzaktan işlem çağrısı", + "type-rpc-call": "RPC çağrısı", "type-credentials-updated": "Kimlik bilgileri güncellendi", - "type-assigned-to-customer": "Kullanıcı grubuna atandı", - "type-unassigned-from-customer": "Kullanıcı grubundan atama kaldırıldı", - "type-assigned-to-edge": "Uç'a Atandı", - "type-unassigned-from-edge": "Uç'tan Kaldırıldı", + "type-assigned-to-customer": "Müşteriye atandı", + "type-unassigned-from-customer": "Müşteriden ataması kaldırıldı", + "type-assigned-to-edge": "Edge'e atandı", + "type-unassigned-from-edge": "Edge'den ataması kaldırıldı", "type-activated": "Etkinleştirildi", "type-suspended": "Askıya alındı", "type-credentials-read": "Kimlik bilgileri okundu", "type-attributes-read": "Öznitelikler okundu", "type-relation-add-or-update": "İlişki güncellendi", "type-relation-delete": "İlişki silindi", - "type-relations-delete": "Tüm ilişki silindi", - "type-alarm-ack": "Kabul edilen", - "type-alarm-clear": "Temizlendi", + "type-relations-delete": "Tüm ilişkiler silindi", + "type-alarm-ack": "Alarm onaylandı", + "type-alarm-clear": "Alarm temizlendi", + "type-alarm-delete": "Alarm silindi", + "type-alarm-assign": "Alarm atandı", + "type-alarm-unassign": "Alarm ataması kaldırıldı", + "type-added-comment": "Yorum eklendi", + "type-updated-comment": "Yorum güncellendi", + "type-deleted-comment": "Yorum silindi", "type-login": "Giriş", "type-logout": "Çıkış", "type-lockout": "Kilitleme", "status-success": "Başarılı", "status-failure": "Başarısız", - "audit-log-details": "Log ve hata detayları", - "no-audit-logs-prompt": "Log ve hata bulunamadı", - "action-data": "Eylem verisi", - "failure-details": "Başarısız işlem detayları", - "search": "Hata ve Log Geçmişinde Ara", + "audit-log-details": "Denetim günlüğü detayları", + "no-audit-logs-prompt": "Günlük bulunamadı", + "action-data": "İşlem verisi", + "failure-details": "Hata detayları", + "search": "Denetim günlüklerinde ara", "clear-search": "Aramayı temizle", - "type-assigned-from-tenant": "Tenant'tan atandı", - "type-assigned-to-tenant": "Tenant'a atandı", - "type-provision-success": "Cihaz sağlandı", - "type-provision-failure": "Cihaz temel hazırlığı başarısız oldu", + "type-assigned-from-tenant": "Kiracıdan atandı", + "type-assigned-to-tenant": "Kiracıya atandı", + "type-provision-success": "Cihaz tedarik edildi", + "type-provision-failure": "Cihaz tedarik işlemi başarısız oldu", "type-timeseries-updated": "Telemetri güncellendi", - "type-timeseries-deleted": "Telemetri silindi" + "type-timeseries-deleted": "Telemetri silindi", + "type-sms-sent": "SMS gönderildi" + }, + "debug-settings": { + "label": "Hata Ayıklama Yapılandırması", + "on-failure": "Sadece hatalar (7/24)", + "all-messages": "Tüm mesajlar ({{time}})", + "failures": "Hatalar", + "entity": "varlık", + "hint": { + "main-limited": "Her {{time}} en fazla {{msg}} {{entity}} hata ayıklama mesajı kaydedilecektir.", + "on-failure": "Yalnızca hata mesajlarını günlüğe kaydet.", + "all-messages": "Tüm hata ayıklama mesajlarını günlüğe kaydet." + } + }, + "calculated-fields": { + "expression": "İfade", + "no-found": "Hesaplanmış alan bulunamadı", + "list": "{ count, plural, =1 {Bir hesaplanmış alan} other {# hesaplanmış alan listesi} }", + "selected-fields": "{ count, plural, =1 {1 hesaplanmış alan} other {# hesaplanmış alan} } seçildi", + "type": { + "simple": "Basit", + "script": "Komut dosyası" + }, + "arguments": "Argümanlar", + "decimals-by-default": "Varsayılan ondalık", + "debugging": "Hesaplanmış alan hata ayıklama", + "argument-name": "Argüman adı", + "datasource": "Veri kaynağı", + "add-argument": "Argüman ekle", + "test-script-function": "Komut dosyası işlevini test et", + "no-arguments": "Yapılandırılmış argüman yok", + "argument-settings": "Argüman ayarları", + "argument-current": "Geçerli varlık", + "argument-current-tenant": "Geçerli kiracı", + "argument-device": "Cihaz", + "argument-asset": "Varlık", + "argument-customer": "Müşteri", + "argument-tenant": "Geçerli kiracı", + "argument-type": "Argüman türü", + "see-debug-events": "Hata ayıklama olaylarını görüntüle", + "attribute": "Öznitelik", + "copy-argument-name": "Argüman adını kopyala", + "timeseries-key": "Zaman serisi anahtarı", + "device-name": "Cihaz adı", + "latest-telemetry": "En son telemetri", + "rolling": "Zaman serisi kaydırma", + "attribute-scope": "Öznitelik kapsamı", + "server-attributes": "Sunucu öznitelikleri", + "client-attributes": "İstemci öznitelikleri", + "shared-attributes": "Paylaşılan öznitelikler", + "attribute-key": "Öznitelik anahtarı", + "default-value": "Varsayılan değer", + "limit": "Maksimum değer", + "time-window": "Zaman aralığı", + "customer-name": "Müşteri adı", + "asset-name": "Varlık adı", + "timeseries": "Zaman serisi", + "output": "Çıktı", + "create": "Yeni hesaplanmış alan oluştur", + "file": "Hesaplanmış alan dosyası", + "invalid-file-error": "Geçersiz dosya biçimi. Lütfen dosyanın geçerli bir JSON dosyası olduğundan emin olun.", + "import": "Hesaplanmış alan içe aktar", + "export": "Hesaplanmış alan dışa aktar", + "export-failed-error": "Hesaplanmış alan dışa aktarılamadı: {{error}}", + "output-type": "Çıktı türü", + "delete-title": "Hesaplanmış alan '{{title}}' silinsin mi?", + "delete-text": "Dikkat, onaydan sonra hesaplanmış alan ve ilgili tüm veriler geri alınamaz hale gelecektir.", + "delete-multiple-title": "{ count, plural, =1 {1 hesaplanmış alanı} other {# hesaplanmış alanı} } silmek istediğinizden emin misiniz?", + "delete-multiple-text": "Dikkat, onaydan sonra seçilen tüm hesaplanmış alanlar ve ilgili tüm veriler geri alınamaz hale gelecektir.", + "test-with-this-message": "Bu mesaj ile test et", + "use-latest-timestamp": "En son zaman damgasını kullan", + "hint": { + "arguments-simple-with-rolling": "Basit türde hesaplanmış alan zaman serisi kaydırma tipi anahtar içermemelidir.", + "arguments-empty": "Argümanlar boş olmamalıdır.", + "expression-required": "İfade gereklidir.", + "expression-invalid": "İfade geçersiz", + "expression-max-length": "İfade uzunluğu 255 karakterden az olmalıdır.", + "argument-name-required": "Argüman adı gereklidir.", + "argument-name-pattern": "Argüman adı geçersiz.", + "argument-name-duplicate": "Bu adda bir argüman zaten mevcut.", + "argument-name-max-length": "Argüman adı 256 karakterden kısa olmalıdır.", + "argument-name-forbidden": "Bu argüman adı rezerve edilmiştir ve kullanılamaz.", + "argument-type-required": "Argüman türü gereklidir.", + "max-args": "Maksimum argüman sayısına ulaşıldı.", + "decimals-range": "Varsayılan ondalık sayısı 0 ile 15 arasında olmalıdır.", + "expression": "Varsayılan ifade, sıcaklığı Fahrenheit'tan Celsius'a dönüştürmeyi gösterir.", + "arguments-entity-not-found": "Argüman hedef varlığı bulunamadı.", + "use-latest-timestamp": "Etkinleştirilirse, hesaplanan değer sunucu zamanı yerine argümanlardan gelen telemetri için en son zaman damgası ile kaydedilir." + } + }, + "ai-models": { + "ai-models": "Yapay Zeka Modelleri", + "ai-model": "Yapay Zeka Modeli", + "model": "Model", + "name": "Ad", + "ai-provider": "Yapay Zeka Sağlayıcısı", + "no-found": "Yapay zeka modeli bulunamadı", + "list": "{ count, plural, =1 {Bir model} other {# model listesi} }", + "selected-fields": "{ count, plural, =1 {1 model} other {# model seçildi} }", + "add": "Model Ekle", + "delete-model-title": "'{{modelName}}' modelini silmek istediğinizden emin misiniz?", + "delete-model-text": "Dikkatli olun, onaydan sonra model ve tüm ilişkili veriler geri alınamaz hale gelecektir.", + "delete-models-title": "{ count, plural, =1 {1 modeli} other {# modeli} } silmek istediğinizden emin misiniz?", + "delete-models-text": "Dikkatli olun, onaydan sonra tüm seçili modeller silinecek ve tüm ilişkili veriler geri alınamaz hale gelecektir.", + "ai-providers": { + "openai": "OpenAI", + "azure-openai": "Azure OpenAI", + "google-ai-gemini": "Google AI Gemini", + "google-vertex-ai-gemini": "Google Vertex AI Gemini", + "mistral-ai": "Mistral AI", + "anthropic": "Anthropic", + "amazon-bedrock": "Amazon Bedrock", + "github-models": "GitHub Modelleri" + }, + "name-required": "Ad gerekli.", + "name-max-length": "Ad en fazla 255 karakter olmalıdır.", + "provider": "Sağlayıcı", + "api-key": "API anahtarı", + "api-key-required": "API anahtarı gerekli.", + "project-id": "Proje Kimliği", + "project-id-required": "Proje kimliği gerekli.", + "location": "Konum", + "location-required": "Konum gerekli.", + "service-account-key-file": "Hizmet hesabı anahtar dosyası", + "service-account-key-file-required": "Hizmet hesabı anahtar dosyası gereklidir.", + "no-file": "Dosya seçilmedi.", + "drop-file": "Bir dosya bırakın veya yüklemek için tıklayın.", + "personal-access-token": "Kişisel erişim belirteci", + "personal-access-token-required": "Kişisel erişim belirteci gereklidir.", + "configuration": "Yapılandırma", + "model-id": "Model Kimliği", + "model-id-required": "Model Kimliği gereklidir.", + "deployment-name": "Dağıtım adı", + "deployment-name-required": "Dağıtım adı gereklidir", + "set": "Ayarla", + "region": "Bölge", + "region-required": "Bölge gereklidir.", + "access-key-id": "Erişim anahtarı kimliği", + "access-key-id-required": "Erişim anahtarı kimliği gereklidir.", + "secret-access-key": "Gizli erişim anahtarı", + "secret-access-key-required": "Gizli erişim anahtarı gereklidir.", + "temperature": "Sıcaklık", + "temperature-hint": "Modelin çıktısındaki rastgelelik düzeyini ayarlar. Yüksek değerler rastgeleliği artırır, düşük değerler azaltır.", + "temperature-min": "0 veya daha büyük olmalıdır.", + "top-p": "Top P", + "top-p-hint": "Modelin seçim yapabileceği en olası belirteçlerden oluşan bir havuz oluşturur. Yüksek değerler daha geniş ve çeşitli bir havuz yaratır, düşük değerler daha küçük bir havuz oluşturur.", + "top-p-min-max": "0'dan büyük ve 1'e kadar olmalıdır.", + "top-k": "Top K", + "top-k-hint": "Modelin seçeneklerini en olası \"K\" belirteçle sınırlar.", + "top-k-min": "0 veya daha büyük olmalıdır.", + "presence-penalty": "Varlık cezası", + "presence-penalty-hint": "Bir belirteç metinde zaten bulunuyorsa, olasılığına sabit bir ceza uygular.", + "frequency-penalty": "Frekans cezası", + "frequency-penalty-hint": "Bir belirtecin metinde geçme sıklığına göre olasılığına ceza uygular.", + "max-output-tokens": "Maksimum çıktı belirteçleri", + "max-output-tokens-min": "0'dan büyük olmalıdır.", + "max-output-tokens-hint": "Modelin tek bir yanıtta üretebileceği maksimum belirteç sayısını ayarlar.", + "endpoint": "Uç nokta", + "endpoint-required": "Uç nokta gereklidir.", + "service-version": "Servis versiyonu", + "check-connectivity": "Bağlantıyı kontrol et", + "check-connectivity-success": "Test isteği başarılı oldu", + "check-connectivity-failed": "Test isteği başarısız oldu", + "no-model-matching": "'{{entity}}' ile eşleşen model bulunamadı.", + "model-required": "Model gereklidir.", + "no-model-text": "Model bulunamadı." }, "confirm-on-exit": { - "message": "Kaydedilmemiş değişiklikler var. Sayfadan ayrılmak istediğinize emin misiniz?", - "html-message": "Kaydedilmemiş değişiklikler var.
    Sayfadan ayrılmak istediğinize emin misiniz?", - "title": "Kaydedilmemiş Değişiklikler" + "message": "Kaydedilmemiş değişiklikleriniz var. Bu sayfadan ayrılmak istediğinizden emin misiniz?", + "html-message": "Kaydedilmemiş değişiklikleriniz var.
    Bu sayfadan ayrılmak istediğinizden emin misiniz?", + "title": "Kaydedilmemiş değişiklikler" }, "contact": { "country": "Ülke", + "country-required": "Ülke gereklidir.", "city": "Şehir", "state": "Eyalet / İl", "postal-code": "Posta Kodu", - "postal-code-invalid": "Geçersiz Posta Kodu.", + "postal-code-invalid": "Geçersiz posta kodu biçimi.", "address": "Adres", "address2": "Adres 2", "phone": "Telefon", "email": "E-posta", - "no-address": "Adres yok" + "no-address": "Adres yok", + "no-country-found": "Ülke bulunamadı.", + "no-country-matching": "'{{country}}' ile eşleşen ülke bulunamadı.", + "state-max-length": "Eyalet uzunluğu 256 karakterden kısa olmalıdır", + "phone-max-length": "Telefon numarası 256 karakterden kısa olmalıdır", + "city-max-length": "Belirtilen şehir 256 karakterden kısa olmalıdır" }, "common": { + "name": "Ad", + "type": "Tür", + "general": "Genel", "username": "Kullanıcı adı", "password": "Parola", - "enter-username": "Kullanıcı adı gir", - "enter-password": "Parola gir", - "enter-search": "Arama gir", - "created-time": "Oluşma zamanı", + "data": "Veri", + "timestamp": "Zaman damgası", + "enter-username": "Kullanıcı adı girin", + "enter-password": "Parola girin", + "enter-search": "Arama yapın", + "created-time": "Oluşturulma zamanı", + "disabled": "Devre dışı", "loading": "Yükleniyor...", - "proceed": "İlerle" + "proceed": "Devam et", + "open-details-page": "Detay sayfasını aç", + "not-found": "Bulunamadı", + "value": "Değer", + "documentation": "Dokümantasyon", + "time-left": "{{time}} kaldı", + "output": "Çıktı", + "suffix": { + "s": "sn", + "ms": "ms" + }, + "hint": { + "name-required": "Ad gereklidir.", + "name-pattern": "Ad geçersiz.", + "name-max-length": "Ad 256 karakterden kısa olmalıdır.", + "title-required": "Başlık gereklidir.", + "title-pattern": "Başlık geçersiz.", + "title-max-length": "Başlık 256 karakterden kısa olmalıdır.", + "key-required": "Anahtar gereklidir.", + "key-pattern": "Anahtar geçersiz.", + "key-max-length": "Anahtar 256 karakterden kısa olmalıdır." + }, + "required-fields": "Zorunlu alanlar eksik" }, "content-type": { "json": "Json", "text": "Metin", "binary": "İkili (Base64)" }, + "color": { + "color": "Renk" + }, "customer": { - "customer": "Kullanıcı Grubu", - "customers": "Kullanıcı Grupları", - "management": "Kullanıcı Grubu Yönetimi", - "dashboard": "Kullanıcı Grubu Gösterge Paneli", - "dashboards": "Kullanıcı Grubu Gösterge Panellleri", - "devices": "Kullanıcı Grubu Cihazları", - "entity-views": "Kullanıcı Grubu Öğe Görüntüleme Sayısı", - "assets": "Kullanıcı Grubu Varlıkları", - "public-dashboards": "Açık Gösterge Panelleri", - "public-devices": "Açık Cihazlar", - "public-assets": "Açık Varlıklar", - "public-edges": "Açık Uçlar", - "public-entity-views": "Açık Öğe Görünümleri", - "add": "Kullanıcı grubu ekle", - "delete": "Kullanıcı grubunu sil", - "manage-customer-users": "Kullanıcı grubu kullanıcılarını yönet", - "manage-customer-devices": "Kullanıcı grubu cihazlarını yönet", - "manage-customer-dashboards": "Kullanıcı grubu Gösterge panellerini yönet", - "manage-public-devices": "Açık cihazları yönet", - "manage-public-dashboards": "Açık Gösterge panellerini yönet", - "manage-customer-assets": "Kullanıcı Grubu varlıklarını yönet", - "manage-public-assets": "Açık varlıkları yönet", - "manage-customer-edges": "Kullanıcı Grubu uçlarını yönetin", - "manage-public-edges": "Açık Uçları yönetin", - "add-customer-text": "Yeni Kullanıcı Grubu ekle", - "no-customers-text": "Kullanıcı Grubu bulunamadı", - "customer-details": "Kullanıcı Grubu detayları", - "delete-customer-title": "'{{customerTitle}}' Kullanıcı Grubunu silmek istediğinizden emin misiniz?", - "delete-customer-text": "Dikkatli olun, onaydan sonra kullanıcı grubu ve ilgili tüm veriler kurtarılamaz hale gelecektir.", - "delete-customers-title": "{ count, plural, =1 {1 kullanıcı grubunu} other {# kullanıcı grubunu} } silmek istediğinize emin misiniz?", - "delete-customers-action-title": "{ count, plural, =1 {1 kullanıcı grubunu} other {# kullanıcı grubunu} } sil", - "delete-customers-text": "YARI: Onaylandıktan sonra tüm seçili kullanıcı grupları ve ilişkili veriler geri yüklenemez şekilde silinecek.", + "customer": "Müşteri", + "customers": "Müşteriler", + "management": "Müşteri yönetimi", + "dashboard": "Müşteri Panosu", + "dashboards": "Müşteri Panoları", + "devices": "Müşteri Cihazları", + "entity-views": "Müşteri Varlık Görünümleri", + "assets": "Müşteri Varlıkları", + "public-dashboards": "Genel Panolar", + "public-devices": "Genel Cihazlar", + "public-assets": "Genel Varlıklar", + "public-entity-views": "Genel Varlık Görünümleri", + "add": "Müşteri ekle", + "delete": "Müşteriyi sil", + "manage-customer-users": "Müşteri kullanıcılarını yönet", + "manage-customer-devices": "Müşteri cihazlarını yönet", + "manage-customer-dashboards": "Müşteri panolarını yönet", + "manage-public-devices": "Genel cihazları yönet", + "manage-public-dashboards": "Genel panoları yönet", + "manage-customer-assets": "Müşteri varlıklarını yönet", + "manage-customer-edges": "Müşteri uç birimlerini yönet", + "manage-public-assets": "Genel varlıkları yönet", + "add-customer-text": "Yeni müşteri ekle", + "no-customers-text": "Hiç müşteri bulunamadı", + "customer-details": "Müşteri detayları", + "delete-customer-title": "‘{{customerTitle}}’ adlı müşteriyi silmek istediğinizden emin misiniz?", + "delete-customer-text": "Dikkatli olun, onaydan sonra müşteri ve tüm ilgili veriler geri alınamaz şekilde silinecek.", + "delete-customers-title": "{ count, plural, =1 {1 müşteriyi} other {# müşteriyi} } silmek istediğinizden emin misiniz?", + "delete-customers-action-title": "{ count, plural, =1 {1 müşteriyi} other {# müşteriyi} } sil", + "delete-customers-text": "Dikkatli olun, onaydan sonra tüm seçili müşteriler ve ilgili veriler geri alınamaz şekilde silinecek.", "manage-users": "Kullanıcıları yönet", "manage-assets": "Varlıkları yönet", "manage-devices": "Cihazları yönet", - "manage-dashboards": "Gösterge panellerini yönet", + "manage-dashboards": "Panoları yönet", "title": "Başlık", "title-required": "Başlık gerekli.", + "title-max-length": "Başlık 256 karakterden kısa olmalıdır", "description": "Açıklama", "details": "Detaylar", - "events": "Etkinlikler", - "copyId": "Kullanıcı kimliğini kopyala", - "idCopiedMessage": "Kullanıcı kimliği panoya kopyalandı", - "select-customer": "Kullanıcı grubunu seç", - "no-customers-matching": "'{{entity}}' ile eşleşen kullanıcı grubu bulunamadı.", - "customer-required": "Kullanıcı grubu gerekli", - "select-default-customer": "Varsayılan kullanıcı grubunu seç", - "default-customer": "Varsayılan kullanıcı grubu", - "default-customer-required": "Tenant düzeyinde gösterge tablosunda hata ayıklamak için varsayılan kullanıcı grubu gerekiyor", - "search": "Kullanıcı grubu ara", - "selected-customers": "{ count, plural, =1 {1 kullanıcı grubu} other {# kullanıcı grubu} } seçildi", - "edges": "Kullanıcı Grubu uç örnekleri", - "manage-edges": "Uçları yönet" + "events": "Olaylar", + "copyId": "Müşteri Kimliğini kopyala", + "idCopiedMessage": "Müşteri Kimliği panoya kopyalandı", + "select-customer": "Müşteri seç", + "no-customers-matching": "‘{{entity}}’ ile eşleşen müşteri bulunamadı.", + "customer-required": "Müşteri gerekli", + "select-default-customer": "Varsayılan müşteri seç", + "default-customer": "Varsayılan müşteri", + "default-customer-required": "Tenant seviyesinde pano hata ayıklaması için varsayılan müşteri gereklidir", + "search": "Müşteri ara", + "selected-customers": "{ count, plural, =1 {1 müşteri} other {# müşteri} } seçildi", + "edges": "Müşteri uç birimleri", + "manage-edges": "Uç birimleri yönet" + }, + "css-size": { + "size-value-required": "Boyut değeri gereklidir", + "invalid-size-value": "Geçersiz boyut değeri" + }, + "date": { + "last-update-n-ago": "Son güncelleme N önce", + "last-update-n-ago-text": "Son güncelleme {{ agoText }}", + "custom-date": "Özel tarih", + "format": "Format", + "preview": "Önizleme", + "auto": "Otomatik", + "time-granularity-formats": "Zaman ayrıntı formatları", + "unit-year": "Yıllar", + "unit-month": "Aylar", + "unit-day": "Günler", + "unit-hour": "Saatler", + "unit-minute": "Dakikalar", + "unit-second": "Saniyeler", + "unit-millisecond": "Milisaniyeler" }, "datetime": { - "date-from": "Tarihinden", - "time-from": "Saatinden", - "date-to": "Tarihine", - "time-to": "Saatine" + "date-from": "Başlangıç tarihi", + "time-from": "Başlangıç saati", + "date-to": "Bitiş tarihi", + "time-to": "Bitiş saati", + "from": "Başlangıç", + "to": "Bitiş" }, "dashboard": { - "dashboard": "Gösterge Paneli", - "dashboards": "Gösterge Panelleri", - "management": "Gösterge Paneli Yönetimi", - "view-dashboards": "Gösterge Panellerini Görüntüle", - "add": "Gösterge Paneli Ekle", - "assign-dashboard-to-customer": "Kullanıcı Grubuna Gösterge Panel(ler)i Ata", - "assign-dashboard-to-customer-text": "Lütfen kullanıcı grubuna atanacak gösterge panellerini seçin", - "assign-dashboard-to-edge-title": "Gösterge panellerini Uç'a Ata", - "assign-to-customer-text": "Lütfen gösterge panellerini atayacak kullanıcı grubu seçin", - "assign-to-customer": "Kullanıcı grubuna ata", - "unassign-from-customer": "Kullanıcı grubundan atamayı kaldır", - "make-public": "Gösterge panelini açık hale getir", - "make-private": "Gösterge panelini özel hale getir", - "manage-assigned-customers": "Atanan kullanıcı gruplarını yönet", - "assigned-customers": "Atanan kullanıcı grupları", - "assign-to-customers": "Kullanıcı gruplarına gösterge paneli ata", - "assign-to-customers-text": "Lütfen gösterge panosunu atamak için kullanıcı gruplarını seçin", - "unassign-from-customers": "Kullanıcı gruplarından gösterge panelini kaldır", - "unassign-from-customers-text": "Lütfen gösterge tablosundan atamak için kullanıcı gruplarını seçin", - "no-dashboards-text": "Gösterge paneli bulunamadı", - "no-widgets": "Hiçbir gösterge yapılandırılmadı", - "add-widget": "Yeni gösterge ekle", + "dashboard": "Pano", + "dashboards": "Panolar", + "management": "Pano yönetimi", + "view-dashboards": "Panoları görüntüle", + "add": "Pano ekle", + "assign-dashboard-to-customer": "Pano(ları) müşteriye ata", + "assign-dashboard-to-customer-text": "Lütfen müşteriye atamak için panoları seçin", + "assign-to-customer-text": "Lütfen pano(ları) atamak için müşteriyi seçin", + "assign-to-customer": "Müşteriye ata", + "unassign-from-customer": "Müşteriden kaldır", + "make-public": "Panoyu herkese açık yap", + "make-private": "Panoyu gizli yap", + "manage-assigned-customers": "Atanmış müşterileri yönet", + "assigned-customers": "Atanmış müşteriler", + "assign-to-customers": "Pano(ları) müşterilere ata", + "assign-to-customers-text": "Lütfen pano(ları) atamak için müşterileri seçin", + "unassign-from-customers": "Pano(ları) müşterilerden kaldır", + "unassign-from-customers-text": "Lütfen pano(ları) kaldırmak için müşterileri seçin", + "no-dashboards-text": "Pano bulunamadı", + "no-widgets": "Yapılandırılmış widget yok", + "add-widget": "Yeni widget ekle", + "add-widget-button-text": "Widget ekle", "title": "Başlık", - "image": "Gösterge Paneli resmi", + "image": "Pano görseli", "mobile-app-settings": "Mobil uygulama ayarları", - "mobile-order": "Mobil uygulamada gösterge paneli sırası", - "mobile-hide": "Mobil uygulamada gösterge panelini gizle", - "update-image": "Gösterge paneli resmini güncelle", + "mobile-order": "Mobil uygulamadaki pano sırası", + "mobile-hide": "Panoyu mobil uygulamada gizle", + "update-image": "Pano görselini güncelle", "take-screenshot": "Ekran görüntüsü al", - "select-widget-title": "Gösterge seç", - "select-widget-value": "{{title}}: gösterge seç", - "select-widget-subtitle": "Kullanılabilir gösterge türleri listesi", - "delete": "Gösterge paneli sil", - "title-required": "Başlık gerekli.", + "select-widget-title": "Widget seç", + "select-widget-value": "{{title}}: widget seç", + "select-widget-subtitle": "Mevcut widget türlerinin listesi", + "delete": "Panoyu sil", + "title-required": "Başlık gereklidir.", + "title-max-length": "Başlık 256 karakterden kısa olmalıdır", "description": "Açıklama", "details": "Detaylar", - "dashboard-details": "Gösterge paneli detayları", - "add-dashboard-text": "Yeni Gösterge paneli ekle", - "assign-dashboards": "Gösterge panelleri ata", - "assign-new-dashboard": "Yeni gösterge paneli ata", - "assign-dashboards-text": "{ count, plural, =1 {1 gösterge panelini} other {# gösterge panelini} } kullanıcı grubuna ata", - "unassign-dashboards-action-text": "Kullanıcı Gruplarından atama { count, plural, =1 {1 gösterge tablosu} other {# panolar} }", - "delete-dashboards": "Gösterge panellerini sil", - "unassign-dashboards": "Gösterge panellerinden atamayı kaldır", - "unassign-dashboards-action-title": "{ count, plural, =1 {1 gösterge panelinin} other {# gösterge panelinin} } atamaları kullanıcı grubundan kaldır", - "delete-dashboard-title": "'{{dashboardTitle}}' isimli gösterge panelini silmek istediğinize emin misiniz?", - "delete-dashboard-text": "UYARI: Onaylandıktan sonra gösterge paneli ve ilişkili verileri geri yüklenemez şekilde silinecek.", - "delete-dashboards-title": "{ count, plural, =1 {1 gösterge panelini} other {# gösterge panelini} } silmek istediğinize emin misiniz?", - "delete-dashboards-action-title": "{ count, plural, =1 {1 gösterge panelini} other {# gösterge panelini} } sil", - "delete-dashboards-text": "UYARI: Onaylandıktan sonra tüm seçili gösterge panelleri ve ilişkili verileri geri yüklenemez şekilde silinecek.", - "unassign-dashboard-title": "'{{dashboardTitle}}' isimli gösterge panelindeki atamayı kaldırmak istediğinize emin misiniz?", - "unassign-dashboard-text": "Onaylandıktan sonra gösterge panelinin ataması kaldırılacak ve kullanıcı grubu tarafından erişilemez hale gelecektir.", - "unassign-dashboard": "Gösterge panelinin ataması kaldır", - "unassign-dashboards-title": "{count, plural, =1 {1 gösterge panelindeki} other {# gösterge panelindeki} } atamayı kaldırmak istediğinize emin misiniz?", - "unassign-dashboards-text": "Onaylandıktan {{dashboardTitle}} açık hale getirildi ve bu bağlantıdan erişilebilir durumda", - "public-dashboard-notice": "Not: Gösterge panelinden tüm verilere erişebilmek adına ilişkili cihazları da açık hale getirmeniz gerekmektedir.", - "make-private-dashboard-title": "'{{dashboardTitle}}' isimli gösterge panelini özel hale getirmek istediğinize emin misiniz?", - "make-private-dashboard-text": "Onaylandıktan sonra gösterge paneli özel hale getirilecek ve başkaları tarafından erişilemez olacak.", - "make-private-dashboard": "Gösterge panelini özel hale getir", - "socialshare-text": "'{{dashboardTitle}}'", - "socialshare-title": "'{{dashboardTitle}}'", - "select-dashboard": "Gösterge paneli seç", - "no-dashboards-matching": "'{{entity}}' ile eşleşen gösterge paneli bulunamadı.", - "dashboard-required": "Gösterge paneli gerekli.", - "select-existing": "Var olan bir gösterge paneli seç", - "create-new": "Yeni bir gösterge paneli oluştur", - "new-dashboard-title": "Yeni gösterge paneli başlığı", - "open-dashboard": "Gösterge panelini aç", - "set-background": "Arka plan belirle", + "dashboard-details": "Pano detayları", + "add-dashboard-text": "Yeni pano ekle", + "assign-dashboards": "Panoları ata", + "assign-new-dashboard": "Yeni pano ata", + "assign-dashboards-text": "{ count, plural, =1 {1 pano} other {# pano} } müşterilere ata", + "unassign-dashboards-action-text": "{ count, plural, =1 {1 pano} other {# pano} } müşterilerden kaldır", + "delete-dashboards": "Panoları sil", + "unassign-dashboards": "Panoları kaldır", + "unassign-dashboards-action-title": "{ count, plural, =1 {1 pano} other {# pano} } müşteriden kaldır", + "delete-dashboard-title": "'{{dashboardTitle}}' panosunu silmek istediğinize emin misiniz?", + "delete-dashboard-text": "Dikkatli olun, onaydan sonra pano ve tüm ilgili veriler geri alınamaz olacak.", + "delete-dashboards-title": "{ count, plural, =1 {1 pano} other {# pano} } silmek istediğinize emin misiniz?", + "delete-dashboards-action-title": "{ count, plural, =1 {1 pano} other {# pano} } sil", + "delete-dashboards-text": "Dikkatli olun, onaydan sonra tüm seçili panolar silinecek ve tüm ilgili veriler geri alınamaz olacak.", + "unassign-dashboard-title": "'{{dashboardTitle}}' panosunu kaldırmak istediğinize emin misiniz?", + "unassign-dashboard-text": "Onaydan sonra pano kaldırılacak ve müşteri tarafından erişilemeyecek.", + "unassign-dashboard": "Panoyu kaldır", + "unassign-dashboards-title": "{ count, plural, =1 {1 pano} other {# pano} } kaldırmak istediğinize emin misiniz?", + "unassign-dashboards-text": "Onaydan sonra seçili tüm panolar kaldırılacak ve müşteri tarafından erişilemeyecek.", + "public-dashboard-title": "Pano artık herkese açık", + "public-dashboard-text": "{{dashboardTitle}} panonuz artık herkese açık ve aşağıdaki bağlantı üzerinden erişilebilir:", + "public-dashboard-notice": "Not: Verilerine erişebilmek için ilgili cihazları herkese açık yapmayı unutmayın.", + "make-private-dashboard-title": "'{{dashboardTitle}}' panosunu gizli yapmak istediğinize emin misiniz?", + "make-private-dashboard-text": "Onaydan sonra pano gizli olacak ve başkaları tarafından erişilemeyecek.", + "make-private-dashboard": "Panoyu gizli yap", + "socialshare-text": "'{{dashboardTitle}}' ThingsBoard tarafından desteklenmektedir", + "socialshare-title": "'{{dashboardTitle}}' ThingsBoard tarafından desteklenmektedir", + "select-dashboard": "Pano seç", + "no-dashboards-matching": "'{{entity}}' ile eşleşen pano bulunamadı.", + "dashboard-required": "Pano gereklidir.", + "select-existing": "Mevcut panoyu seç", + "create-new": "Yeni pano oluştur", + "new-dashboard-title": "Yeni pano başlığı", + "open-dashboard": "Panoyu aç", + "set-background": "Arka planı ayarla", "background-color": "Arka plan rengi", "background-image": "Arka plan resmi", - "background-size-mode": "Arka plan boyutu modu", - "no-image": "Hiçbir resim seçilmedi", - "empty-image": "Resim yok", - "drop-image": "Bir resim bırakın veya yüklenecek dosyayı seçmek için tıklayın.", - "maximum-upload-file-size": "Maksimum yükleme dosyası boyutu: {{ size }}", + "background-size-mode": "Arka plan boyut modu", + "no-image": "Seçili görsel yok", + "empty-image": "Görsel yok", + "drop-image": "Bir görsel bırakın veya yüklemek için tıklayın.", + "maximum-upload-file-size": "Maksimum dosya boyutu: {{ size }}", "cannot-upload-file": "Dosya yüklenemiyor", "settings": "Ayarlar", + "move-all-widgets": "Tüm widget'ları taşı", + "move-by": "Şu kadar taşı", + "cols": "sütun", + "rows": "satır", + "layout": "Yerleşim", + "layout-type-default": "Varsayılan", + "layout-type-scada": "SCADA", + "layout-type-divider": "Bölücü", + "layout-settings-type": "Yerleşim ayarları: {{ type }} kırılım noktası", "columns-count": "Sütun sayısı", - "columns-count-required": "Sütun sayısı gerekli.", - "min-columns-count-message": "Yalnızca 10 minimum sütun sayısına izin verilir.", - "max-columns-count-message": "Yalnızca 1000 maksimum sütun sayısına izin verilir.", - "widgets-margins": "Göstergeler arasındaki boşluk", - "margin-required": "Boşluk değeri gerekli.", - "min-margin-message": "Minimum boşluk değeri olarak yalnızca 0'a izin verilir.", - "max-margin-message": "Maksimum boşluk değeri olarak yalnızca 50'ye izin verilir.", - "horizontal-margin": "Yatay kenar boşluğu", - "horizontal-margin-required": "Yatay kenar boşluğu değeri gerekli.", - "min-horizontal-margin-message": "Minimum yatay kenar boşluğu değeri olarak yalnızca 0'a izin verilir.", - "max-horizontal-margin-message": "Maksimum yatay kenar boşluğu değeri olarak yalnızca 50'ye izin verilir.", - "vertical-margin": "Dikey kenar boşluğu", - "vertical-margin-required": "Dikey kenar boşluğu değeri gerekli.", - "min-vertical-margin-message": "Minimum dikey kenar boşluğu değeri olarak yalnızca 0'a izin verilir.", - "max-vertical-margin-message": "Maksimum dikey kenar boşluğu değeri olarak yalnızca 50'ye izin verilir.", - "autofill-height": "Otomatik doldurma görünüm yüksekliği", - "mobile-layout": "Mobil görünüm ayarları", - "mobile-row-height": "Mobil satır yüksekliği, px", - "mobile-row-height-required": "Mobil satır yükseklik değeri gerekli.", - "min-mobile-row-height-message": "Minimum mobil satır yüksekliği değeri olarak yalnızca 5 piksele izin verilir.", - "max-mobile-row-height-message": "Maksimum mobil satır yüksekliği değeri olarak yalnızca 200 piksele izin verilir.", + "columns-count-required": "Sütun sayısı gereklidir.", + "min-columns-count-message": "En az 10 sütun değeri girilebilir.", + "max-columns-count-message": "En fazla 1000 sütun değeri girilebilir.", + "min-layout-width": "Minimum yerleşim genişliği", + "columns-suffix": "sütun", + "widgets-margins": "Widget'lar arası boşluk", + "margin-required": "Boşluk değeri gereklidir.", + "min-margin-message": "Minimum boşluk değeri yalnızca 0 olabilir.", + "max-margin-message": "Maksimum boşluk değeri yalnızca 50 olabilir.", + "horizontal-margin": "Yatay boşluk", + "horizontal-margin-required": "Yatay boşluk değeri gereklidir.", + "min-horizontal-margin-message": "Minimum yatay boşluk değeri yalnızca 0 olabilir.", + "max-horizontal-margin-message": "Maksimum yatay boşluk değeri yalnızca 50 olabilir.", + "vertical-margin": "Dikey boşluk", + "vertical-margin-required": "Dikey boşluk değeri gereklidir.", + "min-vertical-margin-message": "Minimum dikey boşluk değeri yalnızca 0 olabilir.", + "max-vertical-margin-message": "Maksimum dikey boşluk değeri yalnızca 50 olabilir.", + "apply-outer-margin": "Yerleşim kenarlarına boşluk uygula", + "autofill-height": "Yerleşim yüksekliğini otomatik doldur", + "mobile-layout": "Mobil yerleşim ayarları", + "mobile-row-height": "Mobil satır yüksekliği", + "mobile-row-height-required": "Mobil satır yüksekliği gereklidir.", + "min-mobile-row-height-message": "Minimum mobil satır yüksekliği değeri yalnızca 5 piksel olabilir.", + "max-mobile-row-height-message": "Maksimum mobil satır yüksekliği değeri yalnızca 200 piksel olabilir.", + "row-height": "Satır yüksekliği", + "row-height-required": "Satır yüksekliği değeri gereklidir.", + "min-row-height-message": "Minimum satır yüksekliği değeri yalnızca 5 piksel olabilir.", + "max-row-height-message": "Maksimum satır yüksekliği değeri yalnızca 200 piksel olabilir.", + "display-first-in-mobile-view": "Mobil görünümde ilk olarak göster", "title-settings": "Başlık ayarları", - "display-title": "Gösterge paneli başlığını görüntüle", + "display-title": "Pano başlığını göster", "title-color": "Başlık rengi", "toolbar-settings": "Araç çubuğu ayarları", "hide-toolbar": "Araç çubuğunu gizle", - "toolbar-always-open": "Araç çubuğunu açık tut", - "display-dashboards-selection": "Gösterge paneli seçimini görüntüle", - "display-entities-selection": "Öğe seçimini görüntüle", - "display-filters": "Görüntü filtreleri", - "display-dashboard-timewindow": "Zaman penceresini göster", - "display-dashboard-export": "Dışa aktarmayı görüntüle", - "display-update-dashboard-image": "Gösterge paneli resmini güncellemeyi görüntüle", - "dashboard-logo-settings": "Gösterge paneli logosu ayarları", - "display-dashboard-logo": "Gösterge paneli tam ekran modunda logoyu görüntüle", - "dashboard-logo-image": "Gösterge paneli logosu resmi", - "import": "Gösterge panelini içe aktar", - "export": "Gösterge panelini dışa aktar", - "export-failed-error": "Gösterge paneli dışa aktarılamıyor: {{hata}}", - "create-new-dashboard": "Yeni gösterge paneli oluştur", - "dashboard-file": "Gösterge paneli dosyası", - "invalid-dashboard-file-error": "Gösterge paneli içe aktarılamıyor: Geçersiz Gösterge paneli veri yapısı.", - "dashboard-import-missing-aliases-title": "İçe aktarılan gösterge paneli tarafından kullanılan kısa adları yapılandırın", - "create-new-widget": "Yeni gösterge oluştur", - "import-widget": "Göstergeyi içe aktar", - "widget-file": "Gösterge dosyası", - "invalid-widget-file-error": "Gösterge içe aktarılamıyor: Geçersiz gösterge veri yapısı.", - "widget-import-missing-aliases-title": "İçe aktarılan gösterge tarafından kullanılan kısa adları yapılandırın", - "open-toolbar": "Gösterge paneli araç çubuğunu aç", + "toolbar-always-open": "Araç çubuğunu her zaman açık tut", + "display-dashboards-selection": "Pano seçimini göster", + "display-entities-selection": "Varlık seçimini göster", + "display-filters": "Filtreleri göster", + "display-dashboard-timewindow": "Zaman aralığını göster", + "display-dashboard-export": "Dışa aktarımı göster", + "display-update-dashboard-image": "Pano görselini güncelle seçeneğini göster", + "dashboard-logo-settings": "Pano logosu ayarları", + "display-dashboard-logo": "Tam ekran modunda logoyu göster", + "dashboard-logo-image": "Pano logosu görseli", + "advanced-settings": "Gelişmiş ayarlar", + "dashboard-css": "Pano CSS", + "import": "Panoyu içe aktar", + "export": "Panoyu dışa aktar", + "export-failed-error": "Panoyu dışa aktarma başarısız: {{error}}", + "export-prompt": "Pano görselleri ve kaynaklarını göm", + "create-new-dashboard": "Yeni pano oluştur", + "dashboard-file": "Pano dosyası", + "invalid-dashboard-file-error": "Panoyu içe aktarma başarısız: Geçersiz pano veri yapısı.", + "dashboard-import-missing-aliases-title": "İçe aktarılan panoda kullanılan takma adları yapılandır", + "create-new-widget": "Yeni widget oluştur", + "import-widget": "Widget içe aktar", + "widget-file": "Widget dosyası", + "invalid-widget-file-error": "Widget içe aktarılamadı: Geçersiz widget veri yapısı.", + "widget-import-missing-aliases-title": "İçe aktarılan widget'ta kullanılan takma adları yapılandır", + "open-toolbar": "Pano araç çubuğunu aç", "close-toolbar": "Araç çubuğunu kapat", "configuration-error": "Yapılandırma hatası", - "alias-resolution-error-title": "Gösterge paneli kısa adları yapılandırma hatası", - "invalid-aliases-config": "Kısa ad filtresinin bazılarıyla eşleşen herhangi bir cihaz bulunamadı.
    Bu sorunu çözmek için lütfen yöneticinizle iletişime geçin.", - "select-devices": "Cihaz seçin", - "assignedToCustomer": "Kullanıcı grubuna atandı", - "assignedToCustomers": "Kullanıcılara atandı", - "public": "Açık", - "public-link": "Açık bağlantı", - "copy-public-link": "Açık bağlantıyı kopyala", - "public-link-copied-message": "Gösterge paneli açık bağlantısı panoya kopyalandı", - "manage-states": "Gösterge paneli durumlarını yönet", - "states": "Gösterge paneli durumları", - "search-states": "Gösterge paneli durumlarını ara", - "selected-states": "{ count, plural, =1 {1 gösterge paneli durumu} other {# gösterge paneli durumları} } seçildi", - "edit-state": "Gösterge paneli durumunu düzenle", - "delete-state": "Gösterge paneli durumunu sil", - "add-state": "Gösterge paneli durumu ekle", + "alias-resolution-error-title": "Pano takma ad yapılandırma hatası", + "invalid-aliases-config": "Bazı takma ad filtrelerine uyan cihazlar bulunamadı.
    Bu sorunu çözmek için yöneticinize başvurun.", + "select-devices": "Cihazları seç", + "assignedToCustomer": "Müşteriye atanmış", + "assignedToCustomers": "Müşterilere atanmış", + "public": "Genel", + "copyId": "Pano kimliğini kopyala", + "idCopiedMessage": "Pano kimliği panoya kopyalandı", + "public-link": "Genel bağlantı", + "copy-public-link": "Genel bağlantıyı kopyala", + "public-link-copied-message": "Pano genel bağlantısı panoya kopyalandı", + "manage-states": "Pano durumlarını yönet", + "states": "Pano durumları", + "states-short": "Durumlar", + "search-states": "Pano durumlarını ara", + "selected-states": "{ count, plural, =1 {1 pano durumu} other {# pano durumu} } seçildi", + "edit-state": "Pano durumunu düzenle", + "delete-state": "Pano durumunu sil", + "add-state": "Pano durumu ekle", "no-states-text": "Durum bulunamadı", - "state": "Gösterge paneli durumu", - "state-name": "İsim", - "state-name-required": "Gösterge paneli durumu ismi gerekli.", - "state-id": "Durum Kimliği", - "state-id-required": "Durum Kimliği gerekli.", - "state-id-exists": "Aynı kimliğe sahip gösterge paneli durumu zaten var.", + "state": "Pano durumu", + "state-name": "Ad", + "state-name-required": "Pano durumu adı gereklidir.", + "state-id": "Durum kimliği", + "state-id-required": "Pano durumu kimliği gereklidir.", + "state-id-exists": "Aynı kimliğe sahip bir pano durumu zaten mevcut.", "is-root-state": "Kök durum", - "delete-state-title": "Gösterge paneli durumunu sil", - "delete-state-text": "'{{stateName}}' adlı gösterge paneli durumunu silmek istediğinizden emin misiniz?", - "show-details": "Detayları göster", - "hide-details": "Detayları gizle", - "select-state": "Hedef durumu seçin", + "delete-state-title": "Pano durumunu sil", + "delete-state-text": "'{{stateName}}' adlı pano durumunu silmek istediğinizden emin misiniz?", + "show-details": "Ayrıntıları göster", + "hide-details": "Ayrıntıları gizle", + "select-state": "Hedef durumu seç", "state-controller": "Durum denetleyicisi", - "search": "Gösterge panellerini ara", - "selected-dashboards": "{ count, plural, =1 {1 gösterge paneli} other {# gösterge panelleri} } seçildi", - "home-dashboard": "Ana sayfa gösterge paneli", - "home-dashboard-hide-toolbar": "Ana sayfa gösterge paneli araç çubuğunu gizle", - "unassign-dashboard-from-edge-text": "Onaydan sonra gösterge panelinin ataması kaldırılacak ve uç tarafından erişilebilir olmayacaktır.", - "unassign-dashboards-from-edge-title": "{ count, plural, =1 {1 gösterge paneli} other {# gösterge panelleri} } atamasını kaldırmak istediğinizden emin misiniz?", - "unassign-dashboards-from-edge-text": "Onaydan sonra, seçilen tüm gösterge panellerinin ataması kaldırılacak ve uç tarafından erişilemeyecek.", - "assign-dashboard-to-edge": "Gösterge panellerini uca ata", - "assign-dashboard-to-edge-text": "Lütfen uca atanacak gösterge panellerini seçin" + "state-controller-default": "statik (kullanımdan kaldırıldı)", + "search": "Panoları ara", + "selected-dashboards": "{ count, plural, =1 {1 pano} other {# pano} } seçildi", + "home-dashboard": "Ana pano", + "home-dashboard-hide-toolbar": "Ana pano araç çubuğunu gizle", + "unassign-dashboard-from-edge-text": "Onaydan sonra pano kenardan çıkarılacak ve kenar tarafından erişilemeyecek.", + "unassign-dashboards-from-edge-title": "{ count, plural, =1 {1 pano} other {# pano} } kenardan çıkarılsın mı?", + "unassign-dashboards-from-edge-text": "Onaydan sonra seçilen tüm panolar kenardan çıkarılacak ve erişilemeyecek.", + "assign-dashboard-to-edge": "Pano(lar)ı Kenara Ata", + "assign-dashboard-to-edge-text": "Kenara atanacak panoları seçin", + "non-existent-dashboard-state-error": "\"{{ stateId }}\" kimliğine sahip pano durumu bulunamadı", + "edit-mode": "Düzenleme modu", + "duplicate-state-action": "Durumu çoğalt", + "breakpoint-value": "Kırılım noktası ({{ value }})", + "breakpoints-id": { + "default": "Varsayılan", + "xs": "Mobil (xs)", + "sm": "Tablet (sm)", + "md": "Dizüstü (md)", + "lg": "Masaüstü (lg)", + "xl": "Masaüstü (xl)" + }, + "view-format-type-grid": "Izgara", + "view-format-type-list": "Liste", + "view-format": "Görünüm formatı" }, "datakey": { "settings": "Ayarlar", - "advanced": "İleri düzey", + "general": "Genel", + "advanced": "Gelişmiş", + "key": "Anahtar", + "keys": "Anahtarlar", "label": "Etiket", "color": "Renk", - "units": "Değerin yanında göstermek için özel simge", - "decimals": "Noktadan sonraki basamak sayısı", - "data-generation-func": "Veri oluşturma fonksiyonu", - "use-data-post-processing-func": "Veri işleme sonrası fonksiyonunu kullanın", + "units": "Değerin yanına gösterilecek özel sembol", + "decimals": "Ondalık basamak sayısı", + "data-generation-func": "Veri üretim fonksiyonu", + "use-data-post-processing-func": "Veri son işlem fonksiyonunu kullan", "configuration": "Veri anahtarı yapılandırması", "timeseries": "Zaman serisi", "attributes": "Öznitelikler", - "entity-field": "Öğe alanı", + "entity-field": "Varlık alanı", "alarm": "Alarm alanları", - "timeseries-required": "Zaman serisi öğesi gerekli.", - "timeseries-or-attributes-required": "Zaman serisi/öznitelikler öğesi gerekli.", - "alarm-fields-timeseries-or-attributes-required": "Alarm alanları veya Zaman serisi/öznitelikler öğesi gerekli.", - "maximum-timeseries-or-attributes": "Maksimum { count, plural, =1 {1 zamanserisi/öznitelik kabul edilir.} other {# zamanserisi/öznitelik kabul edilir} }", - "alarm-fields-required": "Alarm alanları gerekli.", + "timeseries-required": "Varlık zaman serisi gereklidir.", + "timeseries-or-attributes-required": "Varlık zaman serisi/öznitelikleri gereklidir.", + "alarm-fields-timeseries-or-attributes-required": "Alarm alanları veya varlık zaman serisi/öznitelikleri gereklidir.", + "maximum-timeseries-or-attributes": "En fazla { count, plural, =1 {1 zaman serisi/öznitelik izin verilir.} other {# zaman serisi/öznitelik izin verilir} }", + "alarm-fields-required": "Alarm alanları gereklidir.", "function-types": "Fonksiyon türleri", - "function-types-required": "Fonksiyon türleri gerekli.", - "maximum-function-types": "Maksimum { count, plural, =1 {1 fonksiyon türü kabul edilir.} other {# fonksiyon türü kabul edilir} }", + "function-type": "Fonksiyon türü", + "function-types-required": "Fonksiyon türleri gereklidir.", + "data-keys": "Veri anahtarları", + "data-key": "Veri anahtarı", + "data-keys-required": "Veri anahtarları gereklidir.", + "data-key-required": "Veri anahtarı gereklidir.", + "alarm-keys": "Alarm veri anahtarları", + "alarm-key": "Alarm veri anahtarı", + "alarm-key-functions": "Alarm anahtarı fonksiyonları", + "alarm-key-function": "Alarm anahtarı fonksiyonu", + "latest-keys": "En son veri anahtarları", + "latest-key": "En son veri anahtarı", + "latest-key-functions": "En son anahtar fonksiyonları", + "latest-key-function": "En son anahtar fonksiyonu", + "timeseries-keys": "Zaman serisi veri anahtarları", + "timeseries-key": "Zaman serisi veri anahtarı", + "timeseries-key-functions": "Zaman serisi anahtar fonksiyonları", + "timeseries-key-function": "Zaman serisi anahtar fonksiyonu", + "maximum-function-types": "En fazla { count, plural, =1 {1 fonksiyon türüne izin verilir.} other {# fonksiyon türüne izin verilir} }", "time-description": "geçerli değerin zaman damgası;", "value-description": "geçerli değer;", "prev-value-description": "önceki fonksiyon çağrısının sonucu;", "time-prev-description": "önceki değerin zaman damgası;", - "prev-orig-value-description": "orijinal önceki değer;" + "prev-orig-value-description": "önceki orijinal değer;", + "aggregation": "Birleştirme", + "aggregation-type-hint-common": "Performans nedenleriyle, birleştirilmiş değer hesaplamaları yalnızca 'mevcut gün', 'mevcut ay' gibi sabit zaman aralıkları için geçerlidir; 'son 30 dakika' veya 'son 24 saat' gibi kayan pencereler için geçerli değildir.", + "aggregation-type-none-hint": "En son değeri al.", + "aggregation-type-min-hint": "Seçilen zaman aralığında en küçük değeri bul.", + "aggregation-type-max-hint": "Seçilen zaman aralığında en büyük değeri bul.", + "aggregation-type-avg-hint": "Seçilen zaman aralığında ortalama değeri hesapla.", + "aggregation-type-sum-hint": "Seçilen zaman aralığında tüm değerleri topla.", + "aggregation-type-count-hint": "Seçilen zaman aralığındaki veri noktalarının toplam sayısı.", + "delta-calculation": "Delta hesaplama", + "enable-delta-calculation": "Delta hesaplamayı etkinleştir", + "enable-delta-calculation-hint": "Etkinleştirildiğinde, veri anahtarı değeri, seçilen zaman aralığına ve belirtilen karşılaştırma dönemine göre birleştirilmiş değerler üzerinden hesaplanır. Performans nedenleriyle delta hesaplama yalnızca geçmiş zaman aralıklarında geçerlidir. Örneğin, dün ile önceki gün arasındaki enerji tüketimi farkını hesaplayabilirsiniz.", + "delta-calculation-result": "Delta hesaplama sonucu", + "delta-calculation-result-previous-value": "Önceki değer", + "delta-calculation-result-delta-absolute": "Delta (mutlak)", + "delta-calculation-result-delta-percent": "Delta (yüzde)", + "source": "Kaynak", + "latest": "En son", + "latest-value": "En son değer", + "delta": "delta", + "percent": "yüzde", + "absolute": "mutlak" }, "datasource": { "type": "Veri kaynağı türü", - "name": "İsim", + "name": "Ad", "label": "Etiket", "add-datasource-prompt": "Lütfen veri kaynağı ekleyin" }, "details": { - "details": "Detaylar", + "details": "Ayrıntılar", "edit-mode": "Düzenleme modu", - "edit-json": "JSON Düzenle", - "toggle-edit-mode": "Düzenleme modunu aç/kapat" + "edit-json": "JSON'u düzenle", + "toggle-edit-mode": "Düzenleme modunu değiştir" }, "device": { "device": "Cihaz", "device-required": "Cihaz gerekli.", "devices": "Cihazlar", - "management": "Cihaz Yönetimi", - "view-devices": "Cihazları görüntüle", - "device-alias": "Cihaz kısa adı", - "aliases": "Cihaz kısa adları", + "management": "Cihaz yönetimi", + "view-devices": "Cihazları Görüntüle", + "device-alias": "Cihaz takma adı", + "device-type-max-length": "Cihaz türü 256 karakterden kısa olmalıdır", + "aliases": "Cihaz takma adları", "no-alias-matching": "'{{alias}}' bulunamadı.", - "no-aliases-found": "Hiçbir kısa ad bulunamadı.", + "no-aliases-found": "Hiçbir takma ad bulunamadı.", "no-key-matching": "'{{key}}' bulunamadı.", "no-keys-found": "Hiçbir anahtar bulunamadı.", "create-new-alias": "Yeni bir tane oluştur!", "create-new-key": "Yeni bir tane oluştur!", - "duplicate-alias-error": "'{{alias}}' daha önce kaydedilmiş.
    Cihaz kısa adları kontrol paneli özelinde emsalsiz olmalıdır.", - "configure-alias": "'{{alias}}' kısa adını yapılandırın", + "duplicate-alias-error": "Yinelenen takma ad bulundu '{{alias}}'.
    Dashboard içinde cihaz takma adları benzersiz olmalıdır.", + "configure-alias": "'{{alias}}' takma adını yapılandır", "no-devices-matching": "'{{entity}}' ile eşleşen cihaz bulunamadı.", - "alias": "Kısa ad", - "alias-required": "Cihaz kısa adı gerekli.", - "remove-alias": "Cihaz kısa adını kaldır", - "add-alias": "Cihaz kısa adı ekle", - "name-starts-with": "... ile başlayan cihaz adı", + "alias": "Takma ad", + "alias-required": "Cihaz takma adı gerekli.", + "remove-alias": "Cihaz takma adını kaldır", + "add-alias": "Cihaz takma adı ekle", + "name-starts-with": "Cihaz adı ifadesi", "help-text": "İhtiyaca göre '%' kullanın: '%device_name_contains%', '%device_name_ends', 'device_starts_with'.", "device-list": "Cihaz listesi", "use-device-name-filter": "Filtre kullan", "device-list-empty": "Hiçbir cihaz seçilmedi.", "device-name-filter-required": "Cihaz adı filtresi gerekli.", - "device-name-filter-no-device-matched": "'{{device}}' ile başlayan herhangi bir cihaz bulunamadı.", + "device-name-filter-no-device-matched": "'{{device}}' ile başlayan cihaz bulunamadı.", "add": "Cihaz ekle", - "assign-to-customer": "Kullanıcı grubuna ata", - "assign-device-to-customer": "Cihazları Kullanıcı Grubuna Ata", - "assign-device-to-customer-text": "Lütfen kullanıcı grubuna atanacak cihazları seçin", - "assign-device-to-edge-title": "Cihazları uca ata", - "assign-device-to-edge-text": "Lütfen uca atanacak cihazları seçin", - "make-public": "Cihazı açık hale getir", - "make-private": "Cihazı gizli hale getir", + "assign-to-customer": "Müşteriye ata", + "assign-device-to-customer": "Cihaz(lar)ı Müşteriye Ata", + "assign-device-to-customer-text": "Lütfen müşteriye atanacak cihazları seçin", + "make-public": "Cihazı herkese açık yap", + "make-private": "Cihazı özel yap", "no-devices-text": "Hiçbir cihaz bulunamadı", - "assign-to-customer-text": "Lütfen cihaz(lar)ı atayacak kullanıcı grubu seçin", - "device-details": "Cihaz detayları", + "assign-to-customer-text": "Lütfen cihaz(lar)ı atamak için müşteri seçin", + "device-details": "Cihaz ayrıntıları", "add-device-text": "Yeni cihaz ekle", "credentials": "Kimlik bilgileri", "manage-credentials": "Kimlik bilgilerini yönet", - "delete": "Cihaz sil", - "assign-devices": "Cihaz ata", - "assign-devices-text": "{ count, plural, =1 {1 cihazı} other {# cihazı} } kullanıcı grubuna ata", + "delete": "Cihazı sil", + "assign-devices": "Cihazları ata", + "assign-devices-text": "{ count, plural, =1 {1 cihaz} other {# cihaz} } müşteriye ata", "delete-devices": "Cihazları sil", - "unassign-from-customer": "Kullanıcı Grubundan atamayı kaldır", - "unassign-devices": "Cihazlardan atamayı kaldır", - "unassign-devices-action-title": "{ count, plural, =1 {1 cihazın} other {# cihazın} } atamasını kullanıcı grubundan kaldır", - "unassign-device-from-edge-title": "'{{deviceName}}' cihazının atamasını kaldırmak istediğinizden emin misiniz?", - "unassign-device-from-edge-text": "Onaydan sonra cihazın ataması kaldırılacak ve cihaza uç tarafından erişilemeyecek.", - "unassign-devices-from-edge": "Cihazların atamasını uçtan kaldır", + "unassign-from-customer": "Müşteriden çıkar", + "unassign-devices": "Cihazların atamasını kaldır", + "unassign-devices-action-title": "{ count, plural, =1 {1 cihaz} other {# cihaz} } müşteriden çıkar", + "unassign-device-from-edge-title": "'{{deviceName}}' cihazının edge'den çıkarılmasını istiyor musunuz?", + "unassign-device-from-edge-text": "Onaydan sonra cihazın edge erişimi olmayacaktır.", + "unassign-devices-from-edge": "Cihazları edge'den çıkar", "assign-new-device": "Yeni cihaz ata", - "make-public-device-title": "'{{deviceName}}' isimli cihazı açık hale getirmek istediğinizden emin misiniz?", - "make-public-device-text": "Onaylandıktan sonra cihaz ve verileri açık hale getirilecek ve diğerleri tarafından erişilebilir olacak.", - "make-private-device-title": "'{{deviceName}}' isimli cihazı gizli hale getirmek istediğinizden emin misiniz?", - "make-private-device-text": "Onaylandıktan sonra cihaz ve verileri gizli hale getirilecek ve diğerleri tarafından erişilemez olacak.", + "make-public-device-title": "'{{deviceName}}' cihazını herkese açık yapmak istediğinize emin misiniz?", + "make-public-device-text": "Onaydan sonra cihaz ve tüm verileri herkese açık olacak.", + "make-private-device-title": "'{{deviceName}}' cihazını özel yapmak istediğinize emin misiniz?", + "make-private-device-text": "Onaydan sonra cihaz ve tüm verileri özel olacak ve başkaları tarafından erişilemeyecek.", "view-credentials": "Kimlik bilgilerini görüntüle", - "delete-device-title": "'{{deviceName}}' isimli cihazı silmek istediğinize emin misiniz?", - "delete-device-text": "UYARI: Onaylandıktan sonra cihaz ve ilişkili verileri geri yüklenemez şekilde silinecek.", - "delete-devices-title": "{ count, plural, =1 {1 cihazı} other {# cihazı} } silmek istediğinize emin misiniz?", - "delete-devices-action-title": "{ count, plural, =1 {1 cihazı} other {# cihazı} } sil", - "delete-devices-text": "UYARI: Onaylandıktan sonra tüm seçili cihazlar ve ilişkili verileri geri yüklenemez şekilde silinecek.", - "unassign-device-title": "'{{deviceName}}' isimli cihazın atamasını kaldırmak istediğinize emin misiniz?", - "unassign-device-text": "Onaylandıktan sonra cihazın ataması kaldırılacak ve kullanıcı grubu tarafından erişilemez olacak.", - "unassign-device": "Cihaz atamasını kaldır", - "unassign-devices-title": "{ count, plural, =1 {1 cihazın} other {# cihazın} } atamasını kaldırmak istediğinize emin misiniz?", - "unassign-devices-text": "Onaylandıktan sonra seçili cihazların atamaları kaldırılacak ve kullanıcı grubu tarafından erişilemez olacak.", + "delete-device-title": "'{{deviceName}}' cihazını silmek istediğinize emin misiniz?", + "delete-device-text": "Dikkatli olun, onaydan sonra cihaz ve tüm ilişkili veriler geri alınamaz hale gelecek.", + "delete-devices-title": "{ count, plural, =1 {1 cihaz} other {# cihaz} } silmek istediğinize emin misiniz?", + "delete-devices-action-title": "{ count, plural, =1 {1 cihaz} other {# cihaz} } sil", + "delete-devices-text": "Onaydan sonra seçilen tüm cihazlar ve ilgili veriler kalıcı olarak silinecektir.", + "unassign-device-title": "'{{deviceName}}' cihazının atamasını kaldırmak istediğinize emin misiniz?", + "unassign-device-text": "Onaydan sonra cihazın müşteri erişimi olmayacaktır.", + "unassign-device": "Cihazın atamasını kaldır", + "unassign-devices-title": "{ count, plural, =1 {1 cihaz} other {# cihaz} } atamasını kaldırmak istediğinize emin misiniz?", + "unassign-devices-text": "Onaydan sonra tüm seçili cihazların müşteri erişimi kaldırılacaktır.", "device-credentials": "Cihaz Kimlik Bilgileri", "loading-device-credentials": "Cihaz kimlik bilgileri yükleniyor...", - "credentials-type": "Kimlik Bilgi Türü", - "access-token": "Erişim şifresi", - "access-token-required": "Erişim şifresi gerekli.", - "access-token-invalid": "Erişim şifresi uzunluğu 1 ile 32 karakter arasında olmalıdır.", + "credentials-type": "Kimlik bilgisi türü", + "access-token": "Erişim anahtarı", + "access-token-required": "Erişim anahtarı gerekli.", + "access-token-invalid": "Erişim anahtarı uzunluğu 1 ile 32 karakter arasında olmalıdır.", + "certificate-pem-format": "PEM formatında sertifika", + "certificate-pem-format-required": "Sertifika gerekli.", + "copy-access-token": "Erişim anahtarını kopyala", + "copy-certificate": "Sertifikayı kopyala", + "copy-client-id": "İstemci Kimliğini kopyala", + "copy-user-name": "Kullanıcı adını kopyala", + "copy-password": "Şifreyi kopyala", + "generate-client-id": "İstemci Kimliği Oluştur", + "generate-user-name": "Kullanıcı Adı Oluştur", + "generate-password": "Şifre Oluştur", + "generate-access-token": "Erişim Anahtarı Oluştur", "lwm2m-security-config": { "identity": "İstemci Kimliği", - "identity-required": "İstemci Kimliği gerekli.", + "identity-required": "İstemci Kimliği gereklidir.", + "identity-tooltip": "PSK tanımlayıcısı, [RFC7925] standardında tanımlandığı gibi en fazla 128 bayt uzunluğunda rastgele bir tanımlayıcıdır.\nTanımlayıcı önce karakter dizisine dönüştürülmeli ve ardından UTF-8 ile baytlara kodlanmalıdır.", "client-key": "İstemci Anahtarı", - "client-key-required": "İstemci Anahtarı gerekli.", + "client-key-required": "İstemci Anahtarı gereklidir.", + "client-key-tooltip-prk": "RPK genel anahtarı veya kimliği [RFC7250] standardında olmalı ve Base64 formatında kodlanmalıdır!", + "client-key-tooltip-psk": "PSK anahtarı [RFC4279] standardında ve HexDec formatında olmalıdır: 32, 64, 128 karakter!", "endpoint": "Uç Nokta İstemci Adı", - "endpoint-required": "Uç Nokta İstemci Adı gerekli.", + "endpoint-required": "Uç Nokta İstemci Adı gereklidir.", + "client-public-key": "İstemci genel anahtarı", + "client-public-key-hint": "Eğer istemci genel anahtarı boşsa, güvenilen sertifika kullanılacaktır", + "client-public-key-tooltip": "X509 genel anahtarı, yalnızca EC algoritmasını destekleyen DER kodlu X509v3 formatında olmalı ve Base64 formatında kodlanmalıdır!", "mode": "Güvenlik yapılandırma modu", "client-tab": "İstemci Güvenlik Yapılandırması", "client-certificate": "İstemci sertifikası", - "bootstrap-tab": "Önyükleme İstemcisi", - "bootstrap-server": "Önyükleme Sunucusu", + "bootstrap-tab": "Başlatma İstemcisi", + "bootstrap-server": "Başlatma Sunucusu", "lwm2m-server": "LwM2M Sunucusu", "client-publicKey-or-id": "İstemci Genel Anahtarı veya Kimliği", - "client-publicKey-or-id-required": "İstemci Genel Anahtarı veya Kimliği gerekli.", + "client-publicKey-or-id-required": "İstemci Genel Anahtarı veya Kimliği gereklidir.", + "client-publicKey-or-id-tooltip-psk": "[RFC7925] standardına göre PSK tanımlayıcısı en fazla 128 baytlık rastgele bir tanımlayıcıdır.\nTanımlayıcı önce karakter dizisine çevrilmeli ve ardından UTF-8 ile kodlanmalıdır.", + "client-publicKey-or-id-tooltip-rpk": "RPK genel anahtarı veya kimliği [RFC7250] standardında olmalı ve Base64 formatında kodlanmalıdır!", + "client-publicKey-or-id-tooltip-x509": "X509 genel anahtarı, yalnızca EC algoritmasını destekleyen DER kodlu X509v3 formatında olmalı ve Base64 formatında kodlanmalıdır", "client-secret-key": "İstemci Gizli Anahtarı", - "client-secret-key-required": "İstemci Gizli Anahtarı gerekli.", - "client-public-key": "İstemci açık anahtarı", - "client-public-key-hint": "İstemci açık anahtarı boşsa, güvenilen sertifika kullanılacaktır." + "client-secret-key-required": "İstemci Gizli Anahtarı gereklidir.", + "client-secret-key-tooltip-psk": "PSK anahtarı [RFC4279] standardında ve HexDec formatında olmalıdır: 32, 64, 128 karakter!", + "client-secret-key-tooltip-prk": "RPK gizli anahtarı, PKCS_8 formatında olmalı (DER kodlaması, [RFC5958] standardı) ve ardından Base64 formatında kodlanmalıdır!", + "client-secret-key-tooltip-x509": "X509 gizli anahtarı, PKCS_8 formatında olmalı (DER kodlaması, [RFC5958] standardı) ve ardından Base64 formatında kodlanmalıdır!" }, - "client-id": "İstemci ID", + "client-id": "İstemci Kimliği", "client-id-pattern": "Geçersiz karakter içeriyor.", "user-name": "Kullanıcı Adı", - "user-name-required": "Kullanıcı Adı gerekli.", - "client-id-or-user-name-necessary": "İstemci ID veya Kullanıcı Adı gerekli", + "user-name-required": "Kullanıcı Adı gereklidir.", + "client-id-or-user-name-necessary": "İstemci Kimliği ve/veya Kullanıcı Adı gereklidir", "password": "Şifre", - "secret": "Gizli Anahtar", - "secret-required": "Gizli Anahtar is required.", - "device-type": "Cihaz türü", - "device-type-required": "Cihaz türü gerekli.", - "select-device-type": "Cihaz türü seç", - "enter-device-type": "Cihaz türünü girin", + "secret": "Gizli", + "secret-required": "Gizli alan gereklidir.", + "device-type": "Cihaz profili", + "device-type-required": "Cihaz türü gereklidir.", + "select-device-type": "Cihaz türünü seç", + "enter-device-type": "Cihaz profili girin", "any-device": "Herhangi bir cihaz", - "no-device-types-matching": "'{{entitySubtype}}' ile eşleşen cihaz türü bulunamadı.", - "device-type-list-empty": "Hiçbir cihaz türü seçilmedi.", - "device-types": "Cihaz Türleri", - "name": "İsim", - "name-required": "İsim gerekli.", + "no-device-types-matching": "'{{entitySubtype}}' ile eşleşen cihaz profili bulunamadı.", + "device-type-list-empty": "Hiçbir cihaz profili seçilmedi!", + "device-profile-type-list-empty": "En az bir cihaz profili seçilmelidir.", + "device-types": "Cihaz türleri", + "name": "Ad", + "name-required": "Ad gereklidir.", + "name-max-length": "Ad 256 karakterden az olmalıdır", + "label-max-length": "Etiket 256 karakterden az olmalıdır", "description": "Açıklama", "label": "Etiket", - "events": "Etkinlikler", - "details": "Detaylar", - "copyId": "Cihaz kimliğini kopyala", - "copyAccessToken": "Erişim şifresini kopyala", + "events": "Olaylar", + "details": "Ayrıntılar", + "copyId": "Cihaz Id kopyala", + "copyAccessToken": "Erişim anahtarını kopyala", "copy-mqtt-authentication": "MQTT kimlik bilgilerini kopyala", - "idCopiedMessage": "Cihaz Kimliği panoya kopyalandı", - "accessTokenCopiedMessage": "Cihaz erişim şifresi panoya kopyalandı", - "mqtt-authentication-copied-message": "Cihaz MQTT kimlik doğrulaması panoya kopyalandı", - "assignedToCustomer": "Kullanıcı grubuna atandı", - "unable-delete-device-alias-title": "Cihaz kısa adı silinemiyor", - "unable-delete-device-alias-text": "Cihaz kısa adı('{{deviceAlias}}'), şu göstergeler tarafından kullanıldığı için silinemedi:
    {{widgetsList}}", - "is-gateway": "Ağ geçidi mi?", - "overwrite-activity-time": "Bağlı cihaz için etkinlik süresini üstüne yaz", - "public": "Açık", - "device-public": "Cihaz açık", + "idCopiedMessage": "Cihaz Id panoya kopyalandı", + "accessTokenCopiedMessage": "Cihaz erişim anahtarı panoya kopyalandı", + "mqtt-authentication-copied-message": "Cihaz MQTT kimlik doğrulama bilgileri panoya kopyalandı", + "assignedToCustomer": "Müşteriye atanmış", + "unable-delete-device-alias-title": "Cihaz takma adı silinemiyor", + "unable-delete-device-alias-text": "'{{deviceAlias}}' cihaz takma adı aşağıdaki widget(lar) tarafından kullanıldığı için silinemiyor:
    {{widgetsList}}", + "is-gateway": "Ağ geçidi mi", + "overwrite-activity-time": "Bağlı cihaz için etkinlik zamanını üzerine yaz", + "device-filter": "Cihaz filtresi", + "device-filter-title": "Cihaz Filtresi", + "filter-title": "Filtre", + "device-state": "Cihaz durumu", + "state": "Durum", + "any": "Herhangi", + "active": "Aktif", + "inactive": "Pasif", + "public": "Herkese açık", + "device-public": "Cihaz herkese açık", "select-device": "Cihaz seç", - "import": "Cihazı içe aktar", + "import": "Cihaz içe aktar", "device-file": "Cihaz dosyası", - "search": "Cihaz ara", + "search": "Cihazları ara", "selected-devices": "{ count, plural, =1 {1 cihaz} other {# cihaz} } seçildi", "device-configuration": "Cihaz yapılandırması", - "transport-configuration": "Aktarım yapılandırması", + "transport-configuration": "Taşıma yapılandırması", "wizard": { "device-details": "Cihaz ayrıntıları" }, - "unassign-devices-from-edge-title": "{ count, plural, =1 {1 cihazın} other {# cihazın} } atamasını kaldırmak istediğinizden emin misiniz?", - "unassign-devices-from-edge-text": "Onaydan sonra, seçilen tüm cihazların ataması kaldırılacak ve uç tarafından erişilemeyecek." + "unassign-devices-from-edge-title": "{ count, plural, =1 {1 cihazı} other {# cihazı} } kaldırmak istediğinize emin misiniz?", + "unassign-devices-from-edge-text": "Onaydan sonra, seçilen tüm cihazlar kaldırılacak ve edge tarafından erişilemeyecektir.", + "time": "Zaman", + "connectivity": { + "check-connectivity": "Bağlantıyı kontrol et", + "device-created-check-connectivity": "Cihaz oluşturuldu. Hadi bağlantıyı kontrol edelim!", + "loading-check-connectivity-command": "Bağlantı kontrol komutları yükleniyor...", + "use-following-instructions": "Cihaz adına telemetri göndermek için aşağıdaki komutları kullanın", + "execute-following-command": "Aşağıdaki komutu çalıştırın", + "install-curl-windows": "Windows 10 b17063 itibarıyla cURL varsayılan olarak mevcuttur", + "install-curl-macos": "Mac OS X 10.2 6C115 (Jaguar) itibarıyla cURL varsayılan olarak mevcuttur", + "install-mqtt-windows": "mosquitto_pub'u indirip kurmak ve çalıştırmak için talimatları kullanın", + "install-coap-client": "coap-client'i indirip kurmak ve çalıştırmak için talimatları kullanın", + "install-necessary-client-tools": "Gerekli istemci araçlarını yükleyin", + "mqtts-x509-command": "MQTT üzerinden X509 yetkilendirmesiyle cihazı bağlamak için bu dokümantasyonu kullanın", + "coaps-x509-command": "DTLS üzerinden CoAP ile X509 yetkilendirmesiyle cihazı bağlamak için bu dokümantasyonu kullanın", + "snmp-command": "Cihazı SNMP aracılığıyla bağlamak için bu dokümantasyonu kullanın.", + "sparkplug-command": "Cihazı MQTT Sparkplug ile bağlamak için bu dokümantasyonu kullanın.", + "lwm2m-command": "Cihazı LWM2M ile bağlamak için bu dokümantasyonu kullanın." + } + }, + "dynamic-form": { + "property": { + "properties": "Özellikler", + "property": "Özellik", + "id": "Id", + "name": "Ad", + "type": "Tür", + "type-text": "Metin", + "type-password": "Şifre", + "type-textarea": "Metin alanı", + "type-number": "Sayı", + "type-switch": "Anahtar", + "type-select": "Seçim", + "type-radios": "Radyo düğmeleri", + "type-datetime": "Tarih/Saat", + "type-image": "Resim", + "type-javascript": "JavaScript", + "type-json": "JSON", + "type-html": "HTML", + "type-css": "CSS", + "type-markdown": "Markdown", + "type-color": "Renk", + "type-color-settings": "Renk ayarları", + "type-font": "Yazı tipi", + "type-units": "Birimler", + "type-icon": "Simge", + "type-fieldset": "Alan kümesi", + "type-array": "Dizi", + "type-html-section": "HTML bölümü", + "group-title": "Grup başlığı", + "no-properties": "Tanımlı özellik yok", + "add-property": "Özellik ekle", + "property-settings": "Özellik ayarları", + "remove-property": "Özelliği kaldır", + "default-value": "Varsayılan değer", + "value-required": "Değer gerekli", + "number-settings": "Sayı ayarları", + "min": "Min", + "max": "Maks", + "step": "Adım", + "selected-options-limit": "Seçilen seçenek sınırı", + "advanced-ui-settings": "Gelişmiş arayüz ayarları", + "disable-on-property": "Özelliğe göre devre dışı bırak", + "disable-on-property-none": "Hiçbiri (alan her zaman etkin)", + "display-condition-function": "Görüntüleme koşulu fonksiyonu", + "sub-label": "Alt etiket", + "vertical-divider-after": "Dikey ayırıcı sonrasında", + "input-field-suffix": "Girdi alanı soneki", + "property-row-classes": "Özellik satır sınıfları", + "property-field-classes": "Özellik alanı sınıfları", + "not-unique-property-ids-error": "Özellik Id'leri benzersiz olmalıdır!", + "enable-multiple-select": "Çoklu seçim etkinleştir", + "allow-empty-select-option": "Boş seçenek izin ver", + "select-options": "Seçenekleri seç", + "not-unique-select-option-value-error": "Seçenek değerleri benzersiz olmalıdır!", + "value": "Değer", + "label": "Etiket", + "add-option": "Seçenek ekle", + "no-options": "Tanımlı seçenek yok", + "remove-option": "Seçeneği kaldır", + "textarea-rows": "Metin alanı satırları", + "help-id": "Yardım kimliği", + "buttons-direction": "Düğme yönü", + "direction-row": "Satır", + "direction-column": "Sütun", + "radio-button-options": "Radyo düğmesi seçenekleri", + "datetime-type": "Tarih/Saat alan türü", + "datetime-type-date": "Tarih", + "datetime-type-time": "Saat", + "datetime-type-datetime": "Tarih/Saat", + "enable-clear-button": "Temizleme düğmesini etkinleştir", + "html-section-settings": "HTML bölümü ayarları", + "html-section-classes": "HTML bölüm sınıfları", + "html-section-content": "HTML bölüm içeriği", + "array-item": "Dizi öğesi", + "item-type": "Öğe türü", + "item-name": "Öğe adı", + "no-items": "Öğe yok", + "support-unit-conversion": "Birim dönüşümünü destekle" + }, + "clear-form": "Formu temizle", + "clear-form-prompt": "Tüm form özelliklerini kaldırmak istediğinize emin misiniz?", + "import-form": "JSON'dan form içe aktar", + "export-form": "JSON'a form dışa aktar", + "json-file": "JSON dosyası", + "json-content": "JSON içeriği", + "invalid-form-json-file-error": "Form JSON'dan içe aktarılamıyor: Geçersiz form JSON veri yapısı." + }, + "asset-profile": { + "asset-profile": "Varlık profili", + "asset-profiles": "Varlık profilleri", + "all-asset-profiles": "Tümü", + "add": "Varlık profili ekle", + "edit": "Varlık profilini düzenle", + "asset-profile-details": "Varlık profili ayrıntıları", + "no-asset-profiles-text": "Hiçbir varlık profili bulunamadı", + "search": "Varlık profili ara", + "selected-asset-profiles": "{ count, plural, =1 {1 varlık profili} other {# varlık profili} } seçildi", + "no-asset-profiles-matching": "'{{entity}}' ile eşleşen varlık profili bulunamadı.", + "asset-profile-required": "Varlık profili gereklidir", + "idCopiedMessage": "Varlık profili kimliği panoya kopyalandı", + "set-default": "Varlık profilini varsayılan yap", + "delete": "Varlık profilini sil", + "copyId": "Varlık profili kimliğini kopyala", + "name-max-length": "Ad 256 karakterden kısa olmalıdır", + "new-device-profile-name": "Varlık profili adı", + "new-device-profile-name-required": "Varlık profili adı gereklidir.", + "name": "Ad", + "name-required": "Ad gereklidir.", + "image": "Varlık profili resmi", + "description": "Açıklama", + "default": "Varsayılan", + "default-rule-chain": "Varsayılan kural zinciri", + "default-edge-rule-chain": "Varsayılan edge kural zinciri", + "default-edge-rule-chain-hint": "Bu varlık profiline sahip varlıklar için gelen verileri işlemek üzere edge üzerinde kullanılan kural zinciri", + "mobile-dashboard": "Mobil kontrol paneli", + "mobile-dashboard-hint": "Mobil uygulama tarafından varlık detay kontrol paneli olarak kullanılır", + "select-queue-hint": "Açılır listeden seçin.", + "delete-asset-profile-title": "'{{assetProfileName}}' varlık profilini silmek istediğinizden emin misiniz?", + "delete-asset-profile-text": "Dikkatli olun, onaydan sonra varlık profili ve ilgili tüm veriler geri alınamaz şekilde silinecektir.", + "delete-asset-profiles-title": "{ count, plural, =1 {1 varlık profili} other {# varlık profili} } silmek istediğinizden emin misiniz?", + "delete-asset-profiles-text": "Dikkatli olun, onaydan sonra seçilen tüm varlık profilleri ve ilgili veriler geri alınamaz şekilde silinecektir.", + "set-default-asset-profile-title": "'{{assetProfileName}}' varlık profilini varsayılan yapmak istediğinizden emin misiniz?", + "set-default-asset-profile-text": "Onaylandıktan sonra bu varlık profili varsayılan olarak işaretlenecek ve profili belirtilmemiş yeni varlıklar için kullanılacaktır.", + "no-asset-profiles-found": "Hiçbir varlık profili bulunamadı.", + "create-new-asset-profile": "Yeni bir tane oluştur!", + "create-asset-profile": "Yeni varlık profili oluştur", + "import": "Varlık profili içe aktar", + "export": "Varlık profili dışa aktar", + "export-failed-error": "Varlık profili dışa aktarılamıyor: {{error}}", + "asset-profile-file": "Varlık profili dosyası", + "invalid-asset-profile-file-error": "Varlık profili içe aktarılamıyor: Geçersiz varlık profili veri yapısı." }, "device-profile": { "device-profile": "Cihaz profili", @@ -1029,161 +1942,194 @@ "add": "Cihaz profili ekle", "edit": "Cihaz profilini düzenle", "device-profile-details": "Cihaz profili ayrıntıları", - "no-device-profiles-text": "Cihaz profili bulunamadı", - "search": "Cihaz profillerini ara", + "no-device-profiles-text": "Hiçbir cihaz profili bulunamadı", + "search": "Cihaz profili ara", "selected-device-profiles": "{ count, plural, =1 {1 cihaz profili} other {# cihaz profili} } seçildi", "no-device-profiles-matching": "'{{entity}}' ile eşleşen cihaz profili bulunamadı.", - "device-profile-required": "Cihaz profili gerekli", + "device-profile-required": "Cihaz profili gereklidir", "idCopiedMessage": "Cihaz profili kimliği panoya kopyalandı", "set-default": "Cihaz profilini varsayılan yap", "delete": "Cihaz profilini sil", "copyId": "Cihaz profili kimliğini kopyala", - "name": "İsim", - "name-required": "İsim gerekli.", + "name-max-length": "Ad 256 karakterden kısa olmalıdır", + "name": "Ad", + "name-required": "Ad gereklidir.", "type": "Profil türü", - "type-required": "Profil türü gerekli.", + "type-required": "Profil türü gereklidir.", "type-default": "Varsayılan", - "image": "Cihaz profil resmi", - "transport-type": "Aktarım türü", - "transport-type-required": "Aktarım türü gerekli.", + "image": "Cihaz profili resmi", + "transport-type": "İletim türü", + "transport-type-required": "İletim türü gereklidir.", "transport-type-default": "Varsayılan", - "transport-type-default-hint": "Temel MQTT, HTTP ve CoAP aktarımını destekler", + "transport-type-default-hint": "Temel MQTT, HTTP ve CoAP iletimi desteklenir", "transport-type-mqtt": "MQTT", - "transport-type-mqtt-hint": "Gelişmiş MQTT aktarım ayarlarını etkinleştirir", + "transport-type-mqtt-hint": "Gelişmiş MQTT iletim ayarlarını etkinleştirir", "transport-type-coap": "CoAP", - "transport-type-coap-hint": "Gelişmiş CoAP aktarım ayarlarını etkinleştirir", + "transport-type-coap-hint": "Gelişmiş CoAP iletim ayarlarını etkinleştirir", "transport-type-lwm2m": "LWM2M", - "transport-type-lwm2m-hint": "LWM2M aktarım türü", + "transport-type-lwm2m-hint": "LWM2M iletim türü", "transport-type-snmp": "SNMP", - "transport-type-snmp-hint": "SNMP aktarım yapılandırmasını belirtin", + "transport-type-snmp-hint": "SNMP iletim yapılandırmasını belirtin", + "transport-type-http": "HTTP", "description": "Açıklama", "default": "Varsayılan", "profile-configuration": "Profil yapılandırması", - "transport-configuration": "Aktarım yapılandırması", + "transport-configuration": "İletim yapılandırması", "default-rule-chain": "Varsayılan kural zinciri", - "mobile-dashboard": "Mobil gösterge paneli", - "mobile-dashboard-hint": "Mobil uygulama tarafından cihaz ayrıntıları gösterge paneli olarak kullanılır", - "select-queue-hint": "Açılır listeden seçin veya özel bir ad ekleyin.", + "default-edge-rule-chain": "Varsayılan edge kural zinciri", + "default-edge-rule-chain-hint": "Bu cihaz profiline sahip cihazlar için edge üzerinde gelen verileri işlemek üzere kullanılır", + "mobile-dashboard": "Mobil kontrol paneli", + "mobile-dashboard-hint": "Mobil uygulama tarafından cihaz detay kontrol paneli olarak kullanılır", + "select-queue-hint": "Açılır listeden seçin.", "delete-device-profile-title": "'{{deviceProfileName}}' cihaz profilini silmek istediğinizden emin misiniz?", - "delete-device-profile-text": "Dikkatli olun, onaydan sonra cihaz profili ve ilgili tüm veriler kurtarılamaz hale gelecektir.", - "delete-device-profiles-title": "{ count, plural, =1 {1 cihaz profilini} other {# cihaz profilini} } silmek istediğinizden emin misiniz?", - "delete-device-profiles-text": "Dikkatli olun, onaydan sonra seçilen tüm cihaz profilleri kaldırılacak ve ilgili tüm veriler kurtarılamaz hale gelecektir.", + "delete-device-profile-text": "Dikkatli olun, onaydan sonra cihaz profili ve ilişkili OTA güncellemeleri dahil tüm veriler geri alınamaz şekilde silinecektir.", + "delete-device-profiles-title": "{ count, plural, =1 {1 cihaz profili} other {# cihaz profili} } silmek istediğinizden emin misiniz?", + "delete-device-profiles-text": "Dikkatli olun, onaydan sonra seçilen tüm cihaz profilleri ve ilişkili veriler (OTA güncellemeleri dahil) geri alınamaz şekilde silinecektir.", "set-default-device-profile-title": "'{{deviceProfileName}}' cihaz profilini varsayılan yapmak istediğinizden emin misiniz?", - "set-default-device-profile-text": "Onaydan sonra cihaz profili varsayılan olarak işaretlenecek ve profil belirtilmemiş yeni cihazlar için kullanılacaktır.", - "no-device-profiles-found": "Cihaz profili bulunamadı.", + "set-default-device-profile-text": "Onaylandıktan sonra bu cihaz profili varsayılan olarak işaretlenecek ve profili belirtilmemiş yeni cihazlar için kullanılacaktır.", + "no-device-profiles-found": "Hiçbir cihaz profili bulunamadı.", "create-new-device-profile": "Yeni bir tane oluştur!", "mqtt-device-topic-filters": "MQTT cihaz konu filtreleri", - "mqtt-device-topic-filters-unique": "MQTT cihaz konu filtrelerinin benzersiz olması gerekir.", + "mqtt-device-topic-filters-unique": "MQTT cihaz konu filtreleri benzersiz olmalıdır.", + "mqtt-device-topic-filters-spark-plug": "MQTT Sparkplug B Edge of Network (EoN) düğümü.", + "mqtt-device-topic-filters-spark-plug-hint": "Sparkplug B yükü ve konu formatı ile EoN düğümlerinden bağlantılara izin ver.", + "mqtt-device-topic-filters-spark-plug-attribute-metric-names": "SparkPlug metriklerini özellik olarak kaydet.", + "mqtt-device-topic-filters-spark-plug-attribute-metric-names-hint": "Cihaz özellikleri olarak saklanacak SparkPlug metriklerinin adları. Diğer tüm metrikler cihaz telemetrisi olarak saklanacaktır.", "mqtt-device-payload-type": "MQTT cihaz yükü", "mqtt-device-payload-type-json": "JSON", "mqtt-device-payload-type-proto": "Protobuf", + "mqtt-enable-compatibility-with-json-payload-format": "Diğer yük formatlarıyla uyumluluğu etkinleştir", + "mqtt-enable-compatibility-with-json-payload-format-hint": "Etkinleştirildiğinde, platform varsayılan olarak Protobuf yük formatını kullanır. Ayrıştırma başarısız olursa JSON yük formatı denenir. Eski ve yeni ürün yazılımları için geçici olarak kullanılabilir. Tüm cihazlar güncellendikten sonra devre dışı bırakılması önerilir.", + "mqtt-use-json-format-for-default-downlink-topics": "Varsayılan downlink konuları için JSON formatını kullan", + "mqtt-use-json-format-for-default-downlink-topics-hint": "Etkinleştirildiğinde platform, belirli konular üzerinden özellik ve RPC iletiminde JSON formatı kullanır. Yeni (v2) konular etkilenmez.", + "mqtt-send-ack-on-validation-exception": "Yayın doğrulama hatasında PUBACK gönder", + "mqtt-send-ack-on-validation-exception-hint": "Varsayılan olarak platform, doğrulama hatasında MQTT oturumunu kapatır. Etkinleştirildiğinde oturumu kapatmak yerine yayın onayı gönderilir.", + "mqtt-protocol-version": "Protokol sürümü", "snmp-add-mapping": "SNMP eşlemesi ekle", - "snmp-mapping-not-configured": "OID için yapılandırılmış zaman serisi/telemetri eşlemesi yok", - "snmp-timseries-or-attribute-name": "Eşleme için zaman serisi/öznitelik adı", - "snmp-timseries-or-attribute-type": "Eşleme için zaman serisi/öznitelik türü", + "snmp-mapping-not-configured": "OID ile zaman serisi/telemetri için eşleme yapılandırılmamış", + "snmp-timseries-or-attribute-name": "Zaman serisi/özellik adı", + "snmp-timseries-or-attribute-type": "Zaman serisi/özellik türü", "snmp-method-pdu-type-get-request": "GetRequest", "snmp-method-pdu-type-get-next-request": "GetNextRequest", "snmp-oid": "OID", "transport-device-payload-type-json": "JSON", "transport-device-payload-type-proto": "Protobuf", - "mqtt-payload-type-required": "Yük türü gerekli.", - "coap-device-type": "CoAP cihaz tipi", + "mqtt-payload-type-required": "Yük türü gereklidir.", + "coap-device-type": "CoAP cihaz türü", "coap-device-payload-type": "CoAP cihaz yükü", - "coap-device-type-required": "CoAP cihaz türü gerekli.", + "coap-device-type-required": "CoAP cihaz türü gereklidir.", "coap-device-type-default": "Varsayılan", "coap-device-type-efento": "Efento NB-IoT", - "support-level-wildcards": "Tekli [+] ve çoklu [#] joker karakter destekler.", + "support-level-wildcards": "Tek seviyeli [+] ve çok seviyeli [#] joker karakterleri desteklenir.", "telemetry-topic-filter": "Telemetri konu filtresi", - "telemetry-topic-filter-required": "Telemetri konu filtresi gerekli.", - "attributes-topic-filter": "Attributes publish topic filter", - "attributes-subscribe-topic-filter": "Attributes subscribe topic filter", - "attributes-topic-filter-required": "Attributes publish topic filter is required.", - "attributes-subscribe-topic-filter-required": "Attributes subscribe topic is required", + "telemetry-topic-filter-required": "Telemetri konu filtresi gereklidir.", + "attributes-topic-filter": "Öznitelik yayın konu filtresi", + "attributes-subscribe-topic-filter": "Öznitelik abone konu filtresi", + "attributes-topic-filter-required": "Öznitelik yayın konu filtresi gereklidir.", + "attributes-subscribe-topic-filter-required": "Öznitelik abone konusu gereklidir", "telemetry-proto-schema": "Telemetri proto şeması", - "telemetry-proto-schema-required": "Telemetri proto şeması gerekli.", - "attributes-proto-schema": "Öznitelikler proto şeması", - "attributes-proto-schema-required": "Öznitelikler proto şeması gerekli.", - "rpc-response-proto-schema": "RPC yanıt protokolü şeması", - "rpc-response-proto-schema-required": "RPC yanıt protokolü şeması gerekli.", - "rpc-response-topic-filter": "RPC yanıtı konu filtresi", - "rpc-response-topic-filter-required": "RPC yanıtı konu filtresi gerekli.", + "telemetry-proto-schema-required": "Telemetri proto şeması gereklidir.", + "attributes-proto-schema": "Öznitelik proto şeması", + "attributes-proto-schema-required": "Öznitelik proto şeması gereklidir.", + "rpc-response-proto-schema": "RPC yanıt proto şeması", + "rpc-response-proto-schema-required": "RPC yanıt proto şeması gereklidir.", + "rpc-response-topic-filter": "RPC yanıt konu filtresi", + "rpc-response-topic-filter-required": "RPC yanıt konu filtresi gereklidir.", "rpc-request-proto-schema": "RPC istek proto şeması", - "rpc-request-proto-schema-required": "RPC istek proto şeması gerekli.", - "rpc-request-proto-schema-hint": "RPC istek mesajında her zaman bu alanlar olmalıdır: string method = 1; int32 requestId = 2; ve any params = 3.", - "not-valid-pattern-topic-filter": "Geçersiz konu filtresi modeli", - "not-valid-single-character": "Tek düzeyli joker karakterin geçersiz kullanımı", - "not-valid-multi-character": "Çok seviyeli joker karakterin geçersiz kullanımı", - "single-level-wildcards-hint": "[+] herhangi bir konu filtresi seviyesi için uygundur. Ör: v1/devices/+/telemetry veya +/devices/+/attributes.", - "multi-level-wildcards-hint": "[#] konu filtresinin yerini alabilir ve konunun son sembolü olmalıdır. Ör: # or v1/devices/me/#.", + "rpc-request-proto-schema-required": "RPC istek proto şeması gereklidir.", + "rpc-request-proto-schema-hint": "RPC istek mesajı her zaman şu alanları içermelidir: string method = 1; int32 requestId = 2; ve params = 3 herhangi bir veri tipi olabilir.", + "not-valid-pattern-topic-filter": "Geçerli olmayan konu filtresi deseni", + "not-valid-single-character": "Tek seviyeli joker karakterin hatalı kullanımı", + "not-valid-multi-character": "Çok seviyeli joker karakterin hatalı kullanımı", + "single-level-wildcards-hint": "[+] her konu seviyesi için uygundur. Örn.: v1/devices/+/telemetry veya +/devices/+/attributes.", + "multi-level-wildcards-hint": "[#] konu filtresinin tamamını değiştirebilir ve son karakter olmalıdır. Örn.: # veya v1/devices/me/#.", "alarm-rules": "Alarm kuralları", "alarm-rules-with-count": "Alarm kuralları ({{count}})", - "no-alarm-rules": "Yapılandırılmış alarm kuralı yok", + "no-alarm-rules": "Tanımlı alarm kuralı yok", "add-alarm-rule": "Alarm kuralı ekle", "edit-alarm-rule": "Alarm kuralını düzenle", - "alarm-type": "Alarm tipi", - "alarm-type-required": "Alarm türü gerekli.", - "alarm-type-unique": "Alarm türü, cihaz profili alarm kuralları dahilinde benzersiz olmalıdır.", - "create-alarm-pattern": "{{alarmType}} alarmı oluşturun", - "create-alarm-rules": "Alarm kuralları oluşturun", - "no-create-alarm-rules": "Yapılandırılmış koşul oluşturma yok", + "alarm-type": "Alarm türü", + "alarm-type-required": "Alarm türü gereklidir.", + "alarm-type-unique": "Alarm türü cihaz profili içinde benzersiz olmalıdır.", + "alarm-type-max-length": "Alarm türü 256 karakterden kısa olmalıdır", + "create-alarm-pattern": "{{alarmType}} alarmı oluştur", + "create-alarm-rules": "Alarm kuralları oluştur", + "no-create-alarm-rules": "Tanımlı oluşturma koşulu yok", "add-create-alarm-rule-prompt": "Lütfen alarm oluşturma kuralı ekleyin", "clear-alarm-rule": "Alarm kuralını temizle", - "no-clear-alarm-rule": "Alarm temizleme kuralı yapılandırılmamış", + "no-clear-alarm-rule": "Tanımlı temizleme koşulu yok", "add-create-alarm-rule": "Oluşturma koşulu ekle", - "add-clear-alarm-rule": "Alarm temizleme koşulu ekle", - "select-alarm-severity": "Alarm şiddetini seçin", - "alarm-severity-required": "Alarm şiddeti gerekli.", + "add-clear-alarm-rule": "Temizleme koşulu ekle", + "select-alarm-severity": "Alarm şiddetini seç", + "alarm-severity-required": "Alarm şiddeti gereklidir.", "condition-duration": "Koşul süresi", "condition-duration-value": "Süre değeri", "condition-duration-time-unit": "Zaman birimi", "condition-duration-value-range": "Süre değeri 1 ile 2147483647 arasında olmalıdır.", - "condition-duration-value-pattern": "Süre değeri tamsayı olmalıdır.", - "condition-duration-value-required": "Süre değeri gerekli.", - "condition-duration-time-unit-required": "Zaman birimi gerekli.", + "condition-duration-value-pattern": "Süre değeri tam sayı olmalıdır.", + "condition-duration-value-required": "Süre değeri gereklidir.", + "condition-duration-time-unit-required": "Zaman birimi gereklidir.", "advanced-settings": "Gelişmiş ayarlar", - "alarm-rule-mobile-dashboard": "Mobil gösterge paneli", - "alarm-rule-mobile-dashboard-hint": "Mobil uygulama tarafından alarm ayrıntıları gösterge paneli olarak kullanılır", - "alarm-rule-no-mobile-dashboard": "Gösterge paneli seçilmedi", - "propagate-alarm": "Alarmı yay", - "alarm-rule-relation-types-list": "Yayılacak ilişki türleri", - "alarm-rule-relation-types-list-hint": "Yayma ilişki türleri seçilmezse, alarmlar ilişki türüne göre filtreleme yapılmadan yayılır.", - "alarm-rule-condition": "Alarm kuralı koşulu", + "alarm-rule-additional-info": "Ek bilgi", + "edit-alarm-rule-additional-info": "Ek bilgiyi düzenle", + "alarm-rule-additional-info-placeholder": "Alarm ayrıntılarında Ek bilgi sekmesinde görüntülenecek yorum ve ayarları buraya girin", + "alarm-rule-additional-info-hint": "İpucu: Alarm kural koşulunda kullanılan öznitelik veya telemetri anahtarlarının değerlerini yerleştirmek için ${keyName} kullanın.", + "alarm-rule-mobile-dashboard": "Mobil kontrol paneli", + "alarm-rule-mobile-dashboard-hint": "Mobil uygulama tarafından alarm detay paneli olarak kullanılır", + "alarm-rule-no-mobile-dashboard": "Seçilen kontrol paneli yok", + "propagate-alarm": "Alarmı ilişkili varlıklara yay", + "alarm-rule-relation-types-list": "İlişki türleri", + "alarm-rule-relation-types-list-hint": "İlişkili varlıkları filtrelemek için ilişki türlerini tanımlar. Belirtilmezse alarm tüm ilişkili varlıklara yayılır.", + "propagate-alarm-to-owner": "Alarmı varlık sahibine (Müşteri veya Kiracı) yay", + "propagate-alarm-to-tenant": "Alarmı Kiracı'ya yay", + "alarm-rule-condition": "Alarm kural koşulu", "enter-alarm-rule-condition-prompt": "Lütfen alarm kuralı koşulu ekleyin", "edit-alarm-rule-condition": "Alarm kuralı koşulunu düzenle", - "device-provisioning": "Cihaz tedarik", - "provision-strategy": "Tedarik stratejisi", - "provision-strategy-required": "Tedarik stratejisi gerekli.", + "device-provisioning": "Cihaz sağlama", + "provision-strategy": "Sağlama stratejisi", + "provision-strategy-required": "Sağlama stratejisi gereklidir.", "provision-strategy-disabled": "Devre dışı", - "provision-strategy-created-new": "Yeni cihazlar oluşturmaya izin ver", - "provision-strategy-check-pre-provisioned": "Önceden hazırlanmış cihazları kontrol edin", - "provision-device-key": "Cihaz Sağlama Anahtarı", - "provision-device-key-required": "Cihaz Sağlama Anahtarı gerekli.", - "copy-provision-key": "Cihaz Sağlama Anahtarını kopyala", - "provision-key-copied-message": "Cihaz Sağlama Anahtarı panoya kopyalandı", - "provision-device-secret": "Cihaz Sağlama Özel Anahtarı", - "provision-device-secret-required": "Cihaz Sağlama Özel Anahtarı gerekli.", - "copy-provision-secret": "Cihaz Sağlama Özel Anahtarını kopyala", - "provision-secret-copied-message": "Cihaz Sağlama Özel Anahtarı panoya kopyalandı", + "provision-strategy-created-new": "Yeni cihazların oluşturulmasına izin ver", + "provision-strategy-check-pre-provisioned": "Önceden sağlanmış cihazları kontrol et", + "provision-device-key": "Cihaz sağlama anahtarı", + "provision-device-key-required": "Cihaz sağlama anahtarı gereklidir.", + "copy-provision-key": "Sağlama anahtarını kopyala", + "provision-key-copied-message": "Sağlama anahtarı panoya kopyalandı", + "provision-device-secret": "Cihaz sağlama gizli anahtarı", + "provision-device-secret-required": "Cihaz sağlama gizli anahtarı gereklidir.", + "copy-provision-secret": "Gizli sağlama anahtarını kopyala", + "provision-secret-copied-message": "Gizli sağlama anahtarı panoya kopyalandı", + "provision-strategy-x509": { + "certificate-chain": "X509 Sertifika Zinciri", + "certificate-chain-hint": "X.509 sertifikaları stratejisi, iki yönlü TLS iletişiminde istemci sertifikalarıyla cihaz sağlamak için kullanılır.", + "allow-create-new-devices": "Yeni cihazlar oluştur", + "allow-create-new-devices-hint": "Seçildiğinde yeni cihazlar oluşturulacak ve istemci sertifikası cihaz kimlik bilgileri olarak kullanılacaktır.", + "certificate-value": "PEM formatında sertifika", + "certificate-value-required": "PEM formatında sertifika gereklidir", + "cn-regex-variable": "CN Düzenli İfade değişkeni", + "cn-regex-variable-required": "CN Düzenli İfade değişkeni gereklidir", + "cn-regex-variable-hint": "Cihaz adını cihazın X509 sertifikasının common name (CN) alanından almak için gereklidir." + }, "condition": "Koşul", "condition-type": "Koşul türü", "condition-type-simple": "Basit", "condition-type-duration": "Süre", - "condition-during": "{{during}} sırasında", - "condition-during-dynamic": "\"{{ attribute }}\" sırasında ({{during}})", + "condition-during": "{{during}} süresince", + "condition-during-dynamic": "\"{{ attribute }}\" ({{during}}) süresince", "condition-type-repeating": "Tekrarlayan", - "condition-type-required": "Koşul türü gerekli.", - "condition-repeating-value": "Etkinlik sayısı sayısı", - "condition-repeating-value-range": "Etkinlik sayısı 1 ile 2147483647 arasında olmalıdır.", - "condition-repeating-value-pattern": "Etkinlik sayısı tamsayı olmalıdır.", - "condition-repeating-value-required": "Etkinlik sayısı gerekli.", - "condition-repeat-times": "{ count, plural, =1 {1 kere} other {# kere} } tekrar eder", - "condition-repeat-times-dynamic": "\"{ attribute }\" ({ count, plural, =1 {1 kere} other {# kere} } tekrar eder)", - "schedule-type": "Plan türü", - "schedule-type-required": "Plan türü gerekli.", - "schedule": "Plan", - "edit-schedule": "Alarm planını düzenle", - "schedule-any-time": "Her zaman aktif", - "schedule-specific-time": "Belirli bir zamanda aktif", + "condition-type-required": "Koşul türü gereklidir.", + "condition-repeating-value": "Olay sayısı", + "condition-repeating-value-range": "Olay sayısı 1 ile 2147483647 arasında olmalıdır.", + "condition-repeating-value-pattern": "Olay sayısı tamsayı olmalıdır.", + "condition-repeating-value-required": "Olay sayısı gereklidir.", + "condition-repeat-times": "{ count, plural, =1 {1 kez} other {# kez} } tekrarlar", + "condition-repeat-times-dynamic": "\"{ attribute }\" ({ count, plural, =1 {1 kez} other {# kez} }) tekrarlar", + "schedule-type": "Zamanlayıcı türü", + "schedule-type-required": "Zamanlayıcı türü gereklidir.", + "schedule": "Zamanlama", + "edit-schedule": "Alarm zamanlamasını düzenle", + "schedule-any-time": "Her zaman etkin", + "schedule-specific-time": "Belirli zamanda etkin", "schedule-custom": "Özel", "schedule-day": { "monday": "Pazartesi", @@ -1194,118 +2140,151 @@ "saturday": "Cumartesi", "sunday": "Pazar" }, - "schedule-days": "Gün", - "schedule-time": "Saat", + "schedule-days": "Günler", + "schedule-time": "Zaman", "schedule-time-from": "Başlangıç", "schedule-time-to": "Bitiş", - "schedule-days-of-week-required": "Haftanın en az bir günü seçilmelidir.", + "schedule-days-of-week-required": "En az bir gün seçilmelidir.", "create-device-profile": "Yeni cihaz profili oluştur", - "import": "Cihaz profilini içe aktar", - "export": "Cihaz profilini dışa aktar", - "export-failed-error": "Cihaz profili dışa aktarılamıyor: {{error}}", + "import": "Cihaz profili içe aktar", + "export": "Cihaz profili dışa aktar", + "export-failed-error": "Cihaz profili dışa aktarılamadı: {{error}}", "device-profile-file": "Cihaz profili dosyası", - "invalid-device-profile-file-error": "Cihaz profili içe aktarılamıyor: Geçersiz cihaz profili veri yapısı.", - "power-saving-mode": "Güç tasarrufu modu", + "invalid-device-profile-file-error": "Cihaz profili içe aktarılamadı: Geçersiz cihaz profili veri yapısı.", + "power-saving-mode": "Güç Tasarruf Modu", "power-saving-mode-type": { - "default": "Cihaz profili güç tasarrufu modunu kullan", - "psm": "Güç tasarrufu modu (PSM)", - "drx": "Discontinuous Reception (DRX)", - "edrx": "Extended Discontinuous Reception (eDRX)" - }, - "edrx-cycle": "eDRX çevrim", - "edrx-cycle-required": "eDRX çevrim gerekli.", - "edrx-cycle-pattern": "eDRX çevrimi pozitif bir tam sayı olmalıdır.", - "edrx-cycle-min": "Minimum eDRX çevrim sayısı {{ min }} saniyedir.", - "paging-transmission-window": "Paging Transmission Window", - "paging-transmission-window-required": "Paging Transmission Window gerekli.", - "paging-transmission-window-pattern": "Paging Transmission Window pozitif bir tam sayı olmalıdır.", - "paging-transmission-window-min": "Minimum Paging Transmission Window sayısı {{ min }} saniyedir.", - "psm-activity-timer": "PSM Etkinlik Zamanlayıcısı", - "psm-activity-timer-required": "PSM Etkinlik Zamanlayıcısı gerekli.", - "psm-activity-timer-pattern": "PSM etkinlik zamanlayıcısı pozitif bir tam sayı olmalıdır.", - "psm-activity-timer-min": "Minimum PSM etkinlik zamanlayıcı sayısı {{ min }} saniyedir.", + "default": "Cihaz profili güç tasarruf modunu kullan", + "psm": "Güç Tasarruf Modu (PSM)", + "drx": "Kesintili Alım (DRX)", + "edrx": "Genişletilmiş Kesintili Alım (eDRX)" + }, + "edrx-cycle": "eDRX döngüsü", + "edrx-cycle-required": "eDRX döngüsü gereklidir.", + "edrx-cycle-pattern": "eDRX döngüsü pozitif tamsayı olmalıdır.", + "edrx-cycle-min": "Minimum eDRX döngüsü {{ min }} saniyedir.", + "paging-transmission-window": "Sayfalama Aktarım Penceresi", + "paging-transmission-window-required": "Sayfalama aktarım penceresi gereklidir.", + "paging-transmission-window-pattern": "Sayfalama aktarım penceresi pozitif bir tamsayı olmalıdır.", + "paging-transmission-window-min": "Minimum sayfalama aktarım penceresi değeri {{ min }} saniyedir.", + "psm-activity-timer": "PSM Aktivite Zamanlayıcısı", + "psm-activity-timer-required": "PSM aktivite zamanlayıcısı gereklidir.", + "psm-activity-timer-pattern": "PSM aktivite zamanlayıcısı pozitif bir tamsayı olmalıdır.", + "psm-activity-timer-min": "Minimum PSM aktivite zamanlayıcısı {{ min }} saniyedir.", "lwm2m": { "object-list": "Nesne listesi", - "object-list-empty": "Hiçbir nesne seçilmedi.", - "no-objects-found": "Hiçbir nesne bulunamadı.", + "object-list-empty": "Seçili nesne yok.", + "no-objects-found": "Nesne bulunamadı.", "no-objects-matching": "'{{object}}' ile eşleşen nesne bulunamadı.", "model-tab": "LWM2M Modeli", - "add-new-instances": "Yeni nesne ekle", - "instances-list": "Nesne listesi", - "instances-list-required": "Nesne listesi gerekli.", - "instance-id-pattern": "Nesne ID pozitif bir tam sayı olmalıdır.", - "instance-id-max": "Maksimum nesne kimliği değeri {{max}}.", - "instance": "Nesne", + "add-new-instances": "Yeni örnekler ekle", + "instances-list": "Örnekler listesi", + "instances-list-required": "Örnekler listesi gereklidir.", + "instance-id-pattern": "Örnek kimliği pozitif bir tamsayı olmalıdır.", + "instance-id-max": "Maksimum örnek kimliği değeri {{max}}.", + "instance": "Örnek", "resource-label": "#ID Kaynak adı", - "observe-label": "Gözlem", - "attribute-label": "Öznitelik", + "observe-label": "Gözlemle", + "attribute-label": "Özellik", "telemetry-label": "Telemetri", - "edit-observe-select": "Gözlemi düzenlemek için telemetri veya özniteliği seçin", - "edit-attributes-select": "Öznitelikleri düzenlemek için telemetri veya öznitelik seçin", - "no-attributes-set": "Öznitelik ayarlanmadı", + "edit-observe-select": "Gözlemi düzenlemek için telemetri veya özellik seçin", + "edit-attributes-select": "Özellikleri düzenlemek için telemetri veya özellik seçin", + "no-attributes-set": "Tanımlı özellik yok", "key-name": "Anahtar adı", - "key-name-required": "Anahtar adı gerekli", - "attribute-name": "Ad özniteliği", - "attribute-name-required": "Ad özniteliği gerekli.", - "attribute-value": "Öznitelik değeri", - "attribute-value-required": "Öznitelik değeri gerekli.", - "attribute-value-pattern": "Öznitelik değeri pozitif bir tam sayı olmalıdır.", - "edit-attributes": "Öznitelikleri düzenle: {{ name }}", - "view-attributes": "Öznitelikleri görüntüle: {{ name }}", - "add-attribute": "Öznitelik ekle", - "edit-attribute": "Öznitelik düzenle", - "view-attribute": "Öznitelik görüntüle", - "remove-attribute": "Öznitelik kaldır", + "key-name-required": "Anahtar adı gereklidir", + "attribute-name": "Özellik adı", + "attribute-name-required": "Özellik adı gereklidir.", + "attribute-value": "Özellik değeri", + "attribute-value-required": "Özellik değeri gereklidir.", + "attribute-value-pattern": "Özellik değeri pozitif bir tamsayı olmalıdır.", + "edit-attributes": "Özellikleri düzenle: {{ name }}", + "view-attributes": "Özellikleri görüntüle: {{ name }}", + "add-attribute": "Özellik ekle", + "edit-attribute": "Özelliği düzenle", + "view-attribute": "Özelliği görüntüle", + "remove-attribute": "Özelliği kaldır", + "delete-server-text": "Dikkatli olun, onaydan sonra sunucu yapılandırması geri alınamaz hale gelecektir.", + "delete-server-title": "Sunucuyu silmek istediğinizden emin misiniz?", "mode": "Güvenlik yapılandırma modu", - "short-id": "Kısa ID", - "short-id-required": "Kısa ID gerekli.", - "short-id-range": "Kısa ID {{ min }} ile {{ max }} aralığında olmalıdır.", - "short-id-pattern": "Kısa ID pozitif bir tam sayı olmalıdır.", - "lifetime": "İstemci kayıt ömrü", - "lifetime-required": "İstemci kayıt ömrü gerekli.", - "lifetime-pattern": "İstemci kayıt ömrü, pozitif bir tam sayı olmalıdır.", - "default-min-period": "İki bildirim(ler) arasındaki minimum süre", - "default-min-period-required": "Minimum süre gerekli.", - "default-min-period-pattern": "Minimum süre pozitif bir tam sayı olmalıdır.", - "notification-storing": "Devre dışı bırakıldığında veya çevrimdışı olduğunda bildirim depolama", + "bootstrap-tab": "Başlatma", + "bootstrap-server-legend": "Başlatma Sunucusu (ShortId...)", + "lwm2m-server-legend": "LwM2M Sunucusu (ShortId...)", + "server": "Sunucu", + "short-id": "Kısa sunucu kimliği", + "short-id-tooltip": "Sunucu kısa kimliği. Sunucu Nesne Örneğiyle ilişkilendirme bağlantısı olarak kullanılır.\nBu kimlik, LwM2M İstemcisi için yapılandırılmış her LwM2M Sunucusunu benzersiz olarak tanımlar.\nBootstrap-Server kaynağının değeri 'false' olduğunda bu kaynak AYARLANMALIDIR.\nID:0 ve ID:65535 değerleri LwM2M Sunucusunu tanımlamak için KULLANILMAMALIDIR.", + "short-id-tooltip-bootstrap": "Sunucu kısa kimliği. Sunucu Nesne Örneğiyle ilişkilendirme bağlantısı olarak kullanılır.\nBu kimlik, LwM2M İstemcisi için yapılandırılmış her LwM2M Sunucusunu benzersiz olarak tanımlar.\nBootstrap-Server kaynağının değeri 'false' olduğunda bu kaynak AYARLANMALIDIR.", + "short-id-required": "Kısa sunucu kimliği gereklidir.", + "short-id-range": "Kısa sunucu kimliği {{ min }} ile {{ max }} arasında olmalıdır.", + "short-id-pattern": "Kısa sunucu kimliği pozitif bir tamsayı olmalıdır.", + "lifetime": "İstemci kayıt süresi", + "lifetime-required": "İstemci kayıt süresi gereklidir.", + "lifetime-pattern": "İstemci kayıt süresi pozitif bir tamsayı olmalıdır.", + "default-min-period": "İki bildirim arasındaki minimum süre (sn)", + "default-min-period-tooltip": "LwM2M İstemcisinin, bir Gözlemde bu parametre dahil edilmediğinde, Gözlemin Minimum Süresi için kullanması gereken varsayılan değer.", + "default-min-period-required": "Minimum süre gereklidir.", + "default-min-period-pattern": "Minimum süre pozitif bir tamsayı olmalıdır.", + "notification-storing": "Bildirimler devre dışıyken veya çevrimdışıyken saklansın", "binding": "Bağlama", - "bootstrap-tab": "Bootstrap", - "bootstrap-server": "Bootstrap Sunucusu", + "binding-type": { + "u": "U: İstemci her zaman UDP bağlantısı üzerinden erişilebilir.", + "m": "M: İstemci her zaman MQTT bağlantısı üzerinden erişilebilir.", + "h": "H: İstemci her zaman HTTP bağlantısı üzerinden erişilebilir.", + "t": "T: İstemci her zaman TCP bağlantısı üzerinden erişilebilir.", + "s": "S: İstemci her zaman SMS bağlantısı üzerinden erişilebilir.", + "n": "N: İstemci, bu tür bir isteğe yanıtı Non-IP bağlantısı üzerinden göndermelidir (LWM2M 1.1 itibariyle desteklenmektedir).", + "uq": "UQ: Kuyruk modunda UDP bağlantısı (LWM2M 1.1 itibariyle desteklenmemektedir)", + "uqs": "UQS: UDP ve SMS bağlantıları aktif; UDP kuyruk modunda, SMS standart modda (LWM2M 1.1 itibariyle desteklenmemektedir)", + "tq": "TQ: Kuyruk modunda TCP bağlantısı (LWM2M 1.1 itibariyle desteklenmemektedir)", + "tqs": "TQS: TCP ve SMS bağlantıları aktif; TCP kuyruk modunda, SMS standart modda (LWM2M 1.1 itibariyle desteklenmemektedir)", + "sq": "SQ: Kuyruk modunda SMS bağlantısı (LWM2M 1.1 itibariyle desteklenmemektedir)" + }, + "binding-tooltip": "\"binding\" kaynağındaki (LwM2M sunucu nesnesi - /1/x/7) listedir.\nLwM2M İstemcisi tarafından desteklenen bağlanma modlarını belirtir.\nBu değer, Aygıt Nesnesindeki \"Supported Binding and Modes\" kaynağındaki (/3/0/16) değerle aynı OLMALIDIR.\nBirden fazla taşıma protokolü desteklenmesine rağmen, tüm Taşıma Oturumu süresince yalnızca bir bağlantı türü kullanılabilir.\nÖrneğin, UDP ve SMS desteklendiğinde, LwM2M İstemcisi ve Sunucusu tüm oturum boyunca ya UDP ya da SMS üzerinden iletişim kurmalıdır.", + "bootstrap-server": "Başlatma Sunucusu", "lwm2m-server": "LwM2M Sunucusu", - "server-host": "Host", - "server-host-required": "Host gerekli.", + "include-bootstrap-server": "Başlatma Sunucusu güncellemelerini dahil et", + "bootstrap-update-title": "Zaten yapılandırılmış bir Başlatma Sunucunuz var. Güncellemeleri hariç tutmak istediğinizden emin misiniz?", + "bootstrap-update-text": "Dikkatli olun, onaydan sonra Başlatma Sunucusu yapılandırma verileri geri alınamaz hale gelecektir.", + "server-host": "Sunucu", + "server-host-required": "Sunucu gereklidir.", "server-port": "Port", - "server-port-required": "Port gerekli.", - "server-port-pattern": "Port pozitif bir tam sayı olmalıdır.", - "server-port-range": "Port 1 ila 65535 aralığında olmalıdır.", - "server-public-key": "Sunucu Açık Anahtarı", - "server-public-key-required": "Sunucu Açık Anahtarı gerekli.", + "server-port-required": "Port gereklidir.", + "server-port-pattern": "Port pozitif bir tamsayı olmalıdır.", + "server-port-range": "Port değeri 1 ile 65535 arasında olmalıdır.", + "server-public-key": "Sunucu Genel Anahtarı", + "server-public-key-required": "Sunucu Genel Anahtarı gereklidir.", "client-hold-off-time": "Bekleme Süresi", - "client-hold-off-time-required": "Bekleme Süresi gerekli.", - "client-hold-off-time-pattern": "Bekleme Süresi pozitif bir tam sayı olmalıdır.", - "client-hold-off-time-tooltip": "Yalnızca Bootstrap Sunucusu ile kullanım için İstemci Bekleme Süresi", - "account-after-timeout": "Zaman aşımından sonra hesap", - "account-after-timeout-required": "Zaman aşımından sonraki hesap gerekli.", - "account-after-timeout-pattern": "Zaman aşımından sonraki hesap pozitif bir tam sayı olmalıdır.", - "account-after-timeout-tooltip": "Bu kaynak tarafından verilen zaman aşımı değerinden sonra Bootstrap Sunucu Hesabı.", + "client-hold-off-time-required": "Bekleme süresi gereklidir.", + "client-hold-off-time-pattern": "Bekleme süresi pozitif bir tamsayı olmalıdır.", + "client-hold-off-time-tooltip": "Yalnızca Başlatma Sunucusu ile kullanılmak üzere istemci bekleme süresi.", + "account-after-timeout": "Zaman aşımından sonra hesapla", + "account-after-timeout-required": "Zaman aşımından sonra hesaplama gereklidir.", + "account-after-timeout-pattern": "Zaman aşımından sonra hesaplama pozitif bir tamsayı olmalıdır.", + "account-after-timeout-tooltip": "Başlatma Sunucusu tarafından verilen zaman aşımı değeri sonrası hesaplama.", + "server-type": "Sunucu türü", + "add-new-server-title": "Yeni sunucu yapılandırması ekle", + "add-server-config": "Sunucu yapılandırması ekle", + "add-lwm2m-server-config": "LwM2M sunucusu ekle", + "no-config-servers": "Yapılandırılmış sunucu yok", "others-tab": "Diğer ayarlar", + "ota-update": "OTA güncellemesi", + "use-object-19-for-ota-update": "OTA dosya meta verileri için Nesne 19'u kullan", + "use-object-19-for-ota-update-hint": "Nesne ID=19 şu şekilde kullanılır: Firmware → InstanceId=65534, Software → InstanceId=65535. Veri formatı: Base64 içinde JSON. JSON içinde: \"Checksum\" (SHA256), \"Title\", \"Version\", \"File Name\", \"File Size\" alanları yer alır.", "client-strategy": "Bağlanırken istemci stratejisi", "client-strategy-label": "Strateji", - "client-strategy-only-observe": "Yalnızca ilk bağlantıdan sonra istemciye yapılan isteği gözlemleyin", - "client-strategy-read-all": "Tüm Kaynakları Okuyun ve Kayıttan Sonra İstemciye Yapılan Talebi Gözlemleyin", - "fw-update": "Donanım yazılımı güncellemesi", - "fw-update-strategy": "Donanım yazılımı güncelleme stratejisi", - "fw-update-strategy-data": "Nesne 19 ve Kaynak 0 (Veri) kullanarak Donanım yazılımı güncellemesini binary dosya olarak gönderin", - "fw-update-strategy-package": "Nesne 5 ve Kaynak 0 (Paket) kullanarak Donanım yazılımı güncellemesini binary dosya olarak gönderin", - "fw-update-strategy-package-uri": "Paketi indirmek ve ürün Donanım yazılımı güncellemesini Nesne 5 ve Kaynak 1 (Paket URI'si) olarak göndermek için otomatik olarak benzersiz CoAP URL'si oluşturun", - "sw-update": "Software güncellemesi", - "sw-update-strategy": "Software güncelleme stratejisi", - "sw-update-strategy-package": "Nesne 9 ve Kaynak 2 (Paket) kullanarak binary dosya gönderin", - "sw-update-strategy-package-uri": "Paketi indirmek ve Nesne 9 ve Kaynak 3'ü (Paket URI) kullanarak yazılım güncellemesini göndermek için otomatik olarak benzersiz CoAP URL'si oluşturun", - "fw-update-resource": "Donanım yazılımı güncellemesi CoAP kaynağı", - "fw-update-resource-required": "Donanım yazılımı güncellemesi CoAP kaynağı gerekli.", - "sw-update-resource": "Yazılım güncellemesi CoAP kaynağı", - "sw-update-resource-required": "Yazılım güncellemesi CoAP kaynağı gerekli.", + "client-strategy-only-observe": "İlk bağlantı sonrası sadece Gözlem isteği", + "client-strategy-read-all": "Kayıttan sonra tüm kaynakları oku ve Gözlem isteği gönder", + "fw-update": "Yazılım güncellemesi (Firmware)", + "fw-update-strategy": "Yazılım güncelleme stratejisi", + "fw-update-strategy-data": "Nesne 19 ve Kaynak 0 (Data) kullanarak ikili dosya gönder", + "fw-update-strategy-package": "Nesne 5 ve Kaynak 0 (Package) kullanarak ikili dosya gönder", + "fw-update-strategy-package-uri": "Benzersiz CoAP URL oluştur ve Nesne 5 ile Kaynak 1 (Package URI) üzerinden gönder", + "sw-update": "Yazılım güncellemesi (Software)", + "sw-update-strategy": "Yazılım güncelleme stratejisi", + "sw-update-strategy-package": "Nesne 9 ve Kaynak 2 (Package) ile ikili dosya gönder", + "sw-update-strategy-package-uri": "Benzersiz CoAP URL oluştur ve Nesne 9 ile Kaynak 3 (Package URI) üzerinden yazılım gönder", + "fw-update-resource": "Firmware güncelleme CoAP kaynağı", + "fw-update-resource-required": "Firmware güncelleme CoAP kaynağı gereklidir.", + "sw-update-resource": "Yazılım güncelleme CoAP kaynağı", + "sw-update-resource-required": "Yazılım güncelleme CoAP kaynağı gereklidir.", "config-json-tab": "Json Yapılandırma Profil Cihazı", "attributes-name": { "min-period": "Minimum süre", @@ -1315,472 +2294,587 @@ "step": "Adım", "min-evaluation-period": "Minimum değerlendirme süresi", "max-evaluation-period": "Maksimum değerlendirme süresi" + }, + "default-object-id": "Varsayılan Nesne Sürümü (Özellik)", + "default-object-id-ver": { + "v1-0": "1.0", + "v1-1": "1.1", + "v1-2": "1.2" + }, + "observe-strategy": { + "observe-strategy": "Gözlem stratejisi", + "single": "Tekil", + "single-description": "Her kaynak için tek Gözlem isteği (daha hassas, daha fazla ağ trafiği)", + "composite-all": "Tümünü birleştir", + "composite-all-description": "Tüm kaynaklar tek Composite Observe isteğiyle gözlemlenir (daha verimli, daha az esnek)", + "composite-by-object": "Nesnelere göre birleştir", + "composite-by-object-description": "Kaynaklar nesne türüne göre gruplanır ve ayrı Composite Observe istekleri ile gözlemlenir (dengeli yaklaşım)" } }, "snmp": { "add-communication-config": "İletişim yapılandırması ekle", "add-mapping": "Eşleme ekle", "authentication-passphrase": "Kimlik doğrulama parolası", - "authentication-passphrase-required": "Kimlik doğrulama parolası gerekli.", + "authentication-passphrase-required": "Kimlik doğrulama parolası gereklidir.", "authentication-protocol": "Kimlik doğrulama protokolü", - "authentication-protocol-required": "Kimlik doğrulama protokolü gerekli.", + "authentication-protocol-required": "Kimlik doğrulama protokolü gereklidir.", "communication-configs": "İletişim yapılandırmaları", - "community": "Topluluk dizisi", - "community-required": "Topluluk dizesi gerekli.", - "context-name": "İçerik adı", + "community": "Topluluk dizesi", + "community-required": "Topluluk dizesi gereklidir.", + "context-name": "Bağlam adı", "data-key": "Veri anahtarı", - "data-key-required": "Veri anahtarı gerekli.", + "data-key-required": "Veri anahtarı gereklidir.", "data-type": "Veri türü", - "data-type-required": "Veri türü gerekli.", - "engine-id": "Engine ID", - "host": "Host", - "host-required": "Host gerekli.", + "data-type-required": "Veri türü gereklidir.", + "engine-id": "Motor Kimliği", + "host": "Sunucu", + "host-required": "Sunucu gereklidir.", "oid": "OID", "oid-pattern": "Geçersiz OID biçimi.", - "oid-required": "OID gerekli.", - "please-add-communication-config": "Lütfen iletişim yapılandırmasını ekleyin", - "please-add-mapping-config": "Lütfen eşleme yapılandırmasını ekleyin", + "oid-required": "OID gereklidir.", + "please-add-communication-config": "Lütfen iletişim yapılandırması ekleyin", + "please-add-mapping-config": "Lütfen eşleme yapılandırması ekleyin", "port": "Port", "port-format": "Geçersiz port biçimi.", - "port-required": "Port gerekli.", + "port-required": "Port gereklidir.", "privacy-passphrase": "Gizlilik parolası", - "privacy-passphrase-required": "Gizlilik parolası gerekli.", + "privacy-passphrase-required": "Gizlilik parolası gereklidir.", "privacy-protocol": "Gizlilik protokolü", - "privacy-protocol-required": "Gizlilik protokolü gerekli.", + "privacy-protocol-required": "Gizlilik protokolü gereklidir.", "protocol-version": "Protokol sürümü", - "protocol-version-required": "Protokol sürümü gerekli.", + "protocol-version-required": "Protokol sürümü gereklidir.", "querying-frequency": "Sorgulama sıklığı, ms", - "querying-frequency-invalid-format": "Sorgulama sıklığı pozitif bir tam sayı olmalıdır.", - "querying-frequency-required": "Sorgulama sıklığı gerekli.", - "retries": "Deneme sayısı", - "retries-invalid-format": "Deneme sayısı pozitif bir tam sayı olmalıdır.", - "retries-required": "Deneme sayısı gerekli.", + "querying-frequency-invalid-format": "Sorgulama sıklığı pozitif bir tamsayı olmalıdır.", + "querying-frequency-required": "Sorgulama sıklığı gereklidir.", + "retries": "Yeniden deneme sayısı", + "retries-invalid-format": "Yeniden deneme sayısı pozitif bir tamsayı olmalıdır.", + "retries-required": "Yeniden deneme sayısı gereklidir.", "scope": "Kapsam", - "scope-required": "Kapsam gerekli.", + "scope-required": "Kapsam gereklidir.", "security-name": "Güvenlik adı", - "security-name-required": "Güvenlik adı gerekli.", + "security-name-required": "Güvenlik adı gereklidir.", "timeout-ms": "Zaman aşımı, ms", - "timeout-ms-invalid-format": "Zaman aşımı pozitif bir tam sayı olmalıdır.", - "timeout-ms-required": "Zaman aşımı gerekli.", + "timeout-ms-invalid-format": "Zaman aşımı pozitif bir tamsayı olmalıdır.", + "timeout-ms-required": "Zaman aşımı gereklidir.", "user-name": "Kullanıcı adı", - "user-name-required": "Kullanıcı adı gerekli." + "user-name-required": "Kullanıcı adı gereklidir." } }, "dialog": { - "close": "Kapat" + "close": "Diyaloğu kapat", + "error-message-title": "Hata mesajı:", + "error-details-title": "Hata ayrıntıları" }, "direction": { - "column": "Kolon", + "column": "Sütun", "row": "Satır" }, "edge": { - "edge": "Edge", - "edge-instances": "Edge instances", - "edge-file": "Edge file", - "management": "Edge management", - "no-edges-matching": "No edges matching '{{entity}}' were found.", - "add": "Add Edge", - "no-edges-text": "No edges found", - "edge-details": "Edge details", - "add-edge-text": "Add new edge", - "delete": "Delete edge", - "delete-edge-title": "Are you sure you want to delete the edge '{{edgeName}}'?", - "delete-edge-text": "Be careful, after the confirmation the edge and all related data will become unrecoverable.", - "delete-edges-title": "Are you sure you want to edge { count, plural, =1 {1 edge} other {# edges} }?", - "delete-edges-text": "Be careful, after the confirmation all selected edges will be removed and all related data will become unrecoverable.", - "name": "Name", - "name-starts-with": "Edge name starts with", - "name-required": "Name is required.", - "description": "Description", - "details": "Details", - "events": "Events", - "copy-id": "Copy Edge Id", - "id-copied-message": "Edge Id has been copied to clipboard", - "sync": "Sync Edge", - "edge-required": "Edge required", - "edge-type": "Edge type", - "edge-type-required": "Edge type is required.", - "event-action": "Event action", - "entity-id": "Entity ID", - "select-edge-type": "Select edge type", - "assign-to-customer": "Assign to customer", - "assign-to-customer-text": "Please select the customer to assign the edge(s)", - "assign-edge-to-customer": "Assign Edge(s) To Customer", - "assign-edge-to-customer-text": "Please select the edges to assign to the customer", - "assignedToCustomer": "Assigned to customer", - "edge-public": "Edge is public", - "assigned-to-customer": "Assigned to: {{customerTitle}}", - "unassign-from-customer": "Unassign from customer", - "unassign-edge-title": "Are you sure you want to unassign the edge '{{edgeName}}'?", - "unassign-edge-text": "After the confirmation the edge will be unassigned and won't be accessible by the customer.", - "unassign-edges-title": "Are you sure you want to unassign { count, plural, =1 {1 edge} other {# edges} }?", - "unassign-edges-text": "After the confirmation all selected edges will be unassigned and won't be accessible by the customer.", - "make-public": "Make edge public", - "make-public-edge-title": "Are you sure you want to make the edge '{{edgeName}}' public?", - "make-public-edge-text": "After the confirmation the edge and all its data will be made public and accessible by others.", - "make-private": "Make edge private", - "public": "Public", - "make-private-edge-title": "Are you sure you want to make the edge '{{edgeName}}' private?", - "make-private-edge-text": "After the confirmation the edge and all its data will be made private and won't be accessible by others.", - "import": "Import edge", - "label": "Label", - "load-entity-error": "Failed to load data. Entity has been deleted.", - "assign-new-edge": "Assign new edge", - "unassign-from-edge": "Unassign from edge", - "edge-key": "Edge key", - "copy-edge-key": "Copy Edge key", - "edge-key-copied-message": "Edge key has been copied to clipboard", - "edge-secret": "Edge secret", - "copy-edge-secret": "Copy Edge secret", - "edge-secret-copied-message": "Edge secret has been copied to clipboard", - "edge-assets": "Edge assets", - "edge-devices": "Edge devices", - "edge-entity-views": "Edge entity views", - "edge-dashboards": "Edge dashboards", - "edge-rulechains": "Edge rule chains", - "assets": "Edge assets", - "devices": "Edge devices", - "entity-views": "Edge entity views", - "dashboard": "Edge dashboard", - "dashboards": "Edge Dashboards", - "rulechain-templates": "Rule chain templates", - "rulechains": "Rule chains", - "search": "Search edges", - "selected-edges": "{ count, plural, =1 {1 edge} other {# edges} } selected", - "any-edge": "Any edge", - "no-edge-types-matching": "No edge types matching '{{entitySubtype}}' were found.", - "edge-type-list-empty": "No edge types selected.", - "edge-types": "Edge types", - "enter-edge-type": "Enter edge type", - "deployed": "Deployed", - "pending": "Pending", - "downlinks": "Downlinks", - "no-downlinks-prompt": "No downlinks found", - "sync-process-started-successfully": "Sync process started successfully!", - "missing-related-rule-chains-title": "Edge has missing related rule chain(s)", - "missing-related-rule-chains-text": "Assigned to edge rule chain(s) use rule nodes that forward message(s) to rule chain(s) that are not assigned to this edge.

    List of missing rule chain(s):
    {{missingRuleChains}}", - "widget-datasource-error": "This widget supports only EDGE entity datasource" + "edge": "Kenar", + "edge-instances": "Kenar örnekleri", + "instances": "Örnekler", + "edge-file": "Kenar dosyası", + "name-max-length": "Ad 256 karakterden kısa olmalıdır", + "label-max-length": "Etiket 256 karakterden kısa olmalıdır", + "type-max-length": "Tür 256 karakterden kısa olmalıdır", + "management": "Kenar yönetimi", + "no-edges-matching": "'{{entity}}' ile eşleşen kenar bulunamadı.", + "add": "Kenar ekle", + "no-edges-text": "Kenar bulunamadı", + "edge-details": "Kenar ayrıntıları", + "add-edge-text": "Yeni kenar ekle", + "delete": "Kenarı sil", + "delete-edge-title": "'{{edgeName}}' kenarını silmek istediğinizden emin misiniz?", + "delete-edge-text": "Dikkatli olun, onaydan sonra kenar ve ilişkili tüm veriler geri alınamaz hale gelecektir.", + "delete-edges-title": "{ count, plural, =1 {1 kenar} other {# kenar} } silmek istediğinizden emin misiniz?", + "delete-edges-text": "Dikkatli olun, onaydan sonra tüm seçili kenarlar ve ilişkili tüm veriler geri alınamaz hale gelecektir.", + "name": "Ad", + "name-starts-with": "Kenar adı ile başlayan", + "name-required": "Ad gereklidir.", + "description": "Açıklama", + "details": "Ayrıntılar", + "events": "Olaylar", + "copy-id": "Kenar Kimliğini Kopyala", + "id-copied-message": "Kenar Kimliği panoya kopyalandı", + "sync": "Kenarı Eşitle", + "edge-required": "Kenar gereklidir", + "edge-type": "Kenar türü", + "edge-type-required": "Kenar türü gereklidir.", + "event-action": "Olay eylemi", + "entity-id": "Varlık Kimliği", + "select-edge-type": "Kenar türü seç", + "assign-to-customer": "Müşteriye ata", + "assign-to-customer-text": "Kenarı atamak istediğiniz müşteriyi seçin", + "assign-edge-to-customer": "Kenar(lar)ı müşteriye ata", + "assign-edge-to-customer-text": "Müşteriye atanacak kenarları seçin", + "assignedToCustomer": "Müşteriye atandı", + "edge-public": "Kenar herkese açık", + "assigned-to-customer": "Atandığı müşteri: {{customerTitle}}", + "unassign-from-customer": "Müşteriden çıkar", + "unassign-edge-title": "'{{edgeName}}' kenarını müşteriden çıkarmak istediğinizden emin misiniz?", + "unassign-edge-text": "Onaydan sonra kenar müşteriden çıkarılacak ve erişilemeyecek.", + "unassign-edges-title": "{ count, plural, =1 {1 kenar} other {# kenar} } müşteriden çıkarmak istediğinizden emin misiniz?", + "unassign-edges-text": "Onaydan sonra tüm seçilen kenarlar müşteriden çıkarılacak ve erişilemeyecek.", + "make-public": "Kenarı herkese açık yap", + "make-public-edge-title": "'{{edgeName}}' kenarını herkese açık yapmak istediğinizden emin misiniz?", + "make-public-edge-text": "Onaydan sonra kenar ve tüm verileri herkese açık hale gelecektir.", + "make-private": "Kenarı gizli yap", + "public": "Herkese açık", + "make-private-edge-title": "'{{edgeName}}' kenarını gizli yapmak istediğinizden emin misiniz?", + "make-private-edge-text": "Onaydan sonra kenar ve tüm verileri gizli hale gelecek ve erişilemeyecek.", + "import": "Kenar içe aktar", + "install-connect-instructions": "Kurulum ve Bağlantı Talimatları", + "install-connect-instructions-edge-created": "Kenar oluşturuldu! Kurulum ve Bağlantı Talimatlarını kontrol edin", + "loading-edge-instructions": "Kenar talimatları yükleniyor...", + "label": "Etiket", + "load-entity-error": "Veri yüklenemedi. Varlık silinmiş.", + "assign-new-edge": "Yeni kenar ata", + "unassign-from-edge": "Kenardan çıkar", + "edge-key": "Kenar anahtarı", + "copy-edge-key": "Kenar anahtarını kopyala", + "edge-key-copied-message": "Kenar anahtarı panoya kopyalandı", + "edge-secret": "Kenar gizli anahtarı", + "copy-edge-secret": "Kenar gizli anahtarını kopyala", + "edge-secret-copied-message": "Kenar gizli anahtarı panoya kopyalandı", + "manage-assets": "Varlıkları yönet", + "manage-devices": "Cihazları yönet", + "manage-entity-views": "Varlık görünümlerini yönet", + "manage-dashboards": "Gösterge panellerini yönet", + "manage-rulechains": "Kural zincirlerini yönet", + "assets": "Kenar varlıkları", + "devices": "Kenar cihazları", + "entity-views": "Kenar varlık görünümleri", + "dashboard": "Kenar gösterge paneli", + "dashboards": "Kenar gösterge panelleri", + "rulechain-templates": "Kural zinciri şablonları", + "edge-rulechain-templates": "Kenar kural zinciri şablonları", + "rulechains": "Kenar kural zincirleri", + "search": "Kenarları ara", + "selected-edges": "{ count, plural, =1 {1 kenar} other {# kenar} } seçildi", + "any-edge": "Herhangi bir kenar", + "no-edge-types-matching": "'{{entitySubtype}}' ile eşleşen kenar türü bulunamadı.", + "edge-type-list-empty": "Seçili kenar türü yok.", + "edge-types": "Kenar türleri", + "enter-edge-type": "Kenar türü girin", + "deployed": "Yayında", + "pending": "Beklemede", + "downlinks": "Aşağı bağlantılar", + "no-downlinks-prompt": "Aşağı bağlantı bulunamadı", + "sync-process-started-successfully": "Eşitleme işlemi başarıyla başlatıldı!", + "missing-related-rule-chains-title": "Kenarda eksik ilişkili kural zinciri(leri) var", + "missing-related-rule-chains-text": "Kenara atanan kural zinciri(leri), başka kural zincirlerine mesaj yönlendiren düğümler içeriyor ancak bu kural zincirleri bu kenara atanmamış.

    Eksik kural zincirleri listesi:
    {{missingRuleChains}}", + "widget-datasource-error": "Bu widget yalnızca EDGE varlık veri kaynağını destekler", + "upgrade-instructions": "Yükseltme Talimatları", + "connected": "Bağlandı", + "disconnected": "Bağlantı kesildi" }, "edge-event": { - "type-dashboard": "Dashboard", - "type-asset": "Asset", - "type-device": "Device", - "type-device-profile": "Device Profile", - "type-entity-view": "Entity View", + "type-dashboard": "Gösterge Paneli", + "type-asset": "Varlık", + "type-device": "Cihaz", + "type-device-profile": "Cihaz Profili", + "type-asset-profile": "Varlık Profili", + "type-entity-view": "Varlık Görünümü", "type-alarm": "Alarm", - "type-rule-chain": "Rule Chain", - "type-rule-chain-metadata": "Rule Chain Metadata", - "type-edge": "Edge", - "type-user": "User", - "type-customer": "Customer", - "type-relation": "Relation", - "type-widgets-bundle": "Widgets Bundle", - "type-widgets-type": "Widgets Type", - "type-admin-settings": "Admin Settings", - "action-type-added": "Added", - "action-type-deleted": "Deleted", - "action-type-updated": "Updated", - "action-type-post-attributes": "Post Attributes", - "action-type-attributes-updated": "Attributes Updated", - "action-type-attributes-deleted": "Attributes Deleted", - "action-type-timeseries-updated": "Timeseries Updated", - "action-type-credentials-updated": "Credentials Updated", - "action-type-assigned-to-customer": "Assigned to Customer", - "action-type-unassigned-from-customer": "Unassigned from Customer", - "action-type-relation-add-or-update": "Relation Add or Update", - "action-type-relation-deleted": "Relation Deleted", - "action-type-rpc-call": "RPC Call", - "action-type-alarm-ack": "Alarm Ack", - "action-type-alarm-clear": "Alarm Clear", - "action-type-assigned-to-edge": "Assigned to Edge", - "action-type-unassigned-from-edge": "Unassigned from Edge", - "action-type-credentials-request": "Credentials Request", - "action-type-entity-merge-request": "Entity Merge Request" + "type-rule-chain": "Kural Zinciri", + "type-rule-chain-metadata": "Kural Zinciri Metaverisi", + "type-edge": "Kenar", + "type-user": "Kullanıcı", + "type-tenant": "Kiracı", + "type-tenant-profile": "Kiracı Profili", + "type-customer": "Müşteri", + "type-relation": "İlişki", + "type-widgets-bundle": "Bileşen Paketi", + "type-widgets-type": "Bileşen Türü", + "type-admin-settings": "Yönetici Ayarları", + "type-ota-package": "OTA Paketi", + "type-queue": "Kuyruk", + "action-type-added": "Eklendi", + "action-type-deleted": "Silindi", + "action-type-updated": "Güncellendi", + "action-type-post-attributes": "Öznitelikler Gönderildi", + "action-type-attributes-updated": "Öznitelikler Güncellendi", + "action-type-attributes-deleted": "Öznitelikler Silindi", + "action-type-timeseries-updated": "Zaman Serisi Güncellendi", + "action-type-credentials-updated": "Kimlik Bilgileri Güncellendi", + "action-type-assigned-to-customer": "Müşteriye Atandı", + "action-type-unassigned-from-customer": "Müşteriden Kaldırıldı", + "action-type-relation-add-or-update": "İlişki Ekle veya Güncelle", + "action-type-relation-deleted": "İlişki Silindi", + "action-type-rpc-call": "RPC Çağrısı", + "action-type-alarm-ack": "Alarm Onaylandı", + "action-type-alarm-clear": "Alarm Temizlendi", + "action-type-alarm-assigned": "Alarm Atandı", + "action-type-alarm-unassigned": "Alarm Kaldırıldı", + "action-type-assigned-to-edge": "Kenara Atandı", + "action-type-unassigned-from-edge": "Kenardan Kaldırıldı", + "action-type-credentials-request": "Kimlik Bilgileri İsteği", + "action-type-entity-merge-request": "Varlık Birleştirme İsteği" }, "error": { - "unable-to-connect": "Sunucuya bağlanamadı! Lütfen internet bağlantınızı kontrol edin.", - "unhandled-error-code": "İşlenmeyen hata koud: {{errorCode}}", + "unable-to-connect": "Sunucuya bağlanılamıyor! Lütfen internet bağlantınızı kontrol edin.", + "unhandled-error-code": "İşlenmeyen hata kodu: {{errorCode}}", "unknown-error": "Bilinmeyen hata" }, "entity": { - "entity": "Öğe", - "entities": "Öğeler", - "entities-count": "Öğe sayısı", - "aliases": "Öğe kısa adları", - "entity-alias": "Öğe kısa adı", - "unable-delete-entity-alias-title": "Öğe kısa adı silinemedi", - "unable-delete-entity-alias-text": "Öğe kısa adı('{{entityAlias}}'), şu göstergeler tarafından kullanıldığı için silinemiyor:
    {{widgetsList}}", - "duplicate-alias-error": "'{{alias}}' daha önce kaydedilmiş.
    Öğe kısa adları kontrol paneli özelinde emsalsiz olmalı.", - "missing-entity-filter-error": "'{{alias}}' için filtre bulunmuyor.", - "configure-alias": "'{{alias}}' kısa adını yapılandır", - "alias": "Kısa ad", - "alias-required": "Öğe kısa adı gerekli.", - "remove-alias": "Öğe kısa adını kaldır", - "add-alias": "Öğe kısa adı ekle", - "entity-list": "Öğe listesi", - "entity-type": "Öğe türü", - "entity-types": "Öğe türleri", - "entity-type-list": "Öğe türü listesi", - "any-entity": "Herhangi bir öğe", - "enter-entity-type": "Öğe türü girin", - "no-entities-matching": "'{{entity}}' ile eşleşen öğe bulunamadı.", - "no-entity-types-matching": "'{{entityType}}' ile eşleşen öğe türü bulunamadı.", - "name-starts-with": "... ile başlayan isim", + "entity": "Varlık", + "entities": "Varlıklar", + "entities-count": "Varlık sayısı", + "alarms-count": "Alarm sayısı", + "aliases": "Varlık takma adları", + "aliases-short": "Takma adlar", + "entity-alias": "Varlık takma adı", + "unable-delete-entity-alias-title": "Varlık takma adı silinemiyor", + "unable-delete-entity-alias-text": "'{{entityAlias}}' takma adı silinemiyor çünkü şu bileşen(ler) tarafından kullanılıyor:
    {{widgetsList}}", + "duplicate-alias-error": "Yinelenen takma ad bulundu '{{alias}}'.
    Gösterge panelinde takma adlar benzersiz olmalıdır.", + "missing-entity-filter-error": "'{{alias}}' takma adı için filtre eksik.", + "configure-alias": "'{{alias}}' takma adını yapılandır", + "alias": "Takma ad", + "alias-required": "Varlık takma adı gereklidir.", + "remove-alias": "Varlık takma adını kaldır", + "add-alias": "Varlık takma adı ekle", + "edit-alias": "Varlık takma adını düzenle", + "entity-list": "Varlık listesi", + "entity-type": "Varlık türü", + "entity-types": "Varlık türleri", + "entity-type-list": "Varlık türü listesi", + "any-entity": "Herhangi bir varlık", + "add-entity-type": "Varlık türü ekle", + "enter-entity-type": "Varlık türü girin", + "no-entities-matching": "'{{entity}}' ile eşleşen varlık bulunamadı.", + "no-entities-text": "Varlık bulunamadı", + "no-entity-types-matching": "'{{entityType}}' ile eşleşen varlık türü bulunamadı.", + "name-starts-with": "İsim ifadesi", "help-text": "İhtiyaca göre '%' kullanın: '%entity_name_contains%', '%entity_name_ends', 'entity_starts_with'.", "use-entity-name-filter": "Filtre kullan", - "entity-list-empty": "Hiçbir öğe seçilmedi.", - "entity-name-filter-required": "Öğe ismi filtresi gerekli.", - "entity-name-filter-no-entity-matched": "'{{entity}}' ile başlayan hiçbir öğe bulunamadı.", + "entity-list-empty": "Seçilen varlık yok.", + "entity-type-list-required": "En az bir varlık türü seçilmelidir.", + "entity-name-filter-required": "Varlık adı filtresi gereklidir.", + "entity-name-filter-no-entity-matched": "'{{entity}}' ile başlayan varlık bulunamadı.", "all-subtypes": "Tümü", - "select-entities": "Öğeleri seç", - "no-aliases-found": "Hiçbir kısa ad bulunamadı.", + "select-entities": "Varlık seç", + "no-aliases-found": "Takma ad bulunamadı.", "no-alias-matching": "'{{alias}}' bulunamadı.", "create-new-alias": "Yeni bir tane oluştur!", + "create-new": "Yeni oluştur", "key": "Anahtar", "key-name": "Anahtar adı", - "no-keys-found": "Hiçbir anahtar bulunamadı.", + "no-keys-found": "Anahtar bulunamadı.", "no-key-matching": "'{{key}}' bulunamadı.", "create-new-key": "Yeni bir tane oluştur!", "type": "Tür", - "type-required": "Öğe türü gerekli.", + "type-required": "Varlık türü gereklidir.", "type-device": "Cihaz", "type-devices": "Cihazlar", - "list-of-devices": "{ count, plural, =1 {Bir cihaz} other {# cihazın listesi} }", - "device-name-starts-with": "İsimleri '{{prefix}}' ile başlayan cihazlar", + "list-of-devices": "{ count, plural, =1 {Bir cihaz} other {# cihaz listesi} }", + "device-name-starts-with": "'{{prefix}}' ile başlayan cihazlar", "type-device-profile": "Cihaz profili", "type-device-profiles": "Cihaz profilleri", - "list-of-device-profiles": "{ count, plural, =1 {Bir cihaz profili} other {# cihaz profilinin listesi} }", - "device-profile-name-starts-with": "Adları '{{prefix}}' ile başlayan cihaz profilleri", + "clear-selected-profiles": "Seçilen profilleri temizle", + "list-of-device-profiles": "{ count, plural, =1 {Bir cihaz profili} other {# cihaz profili listesi} }", + "device-profile-name-starts-with": "'{{prefix}}' ile başlayan cihaz profilleri", + "type-asset-profile": "Varlık profili", + "type-asset-profiles": "Varlık profilleri", + "list-of-asset-profiles": "{ count, plural, =1 {Bir varlık profili} other {# varlık profili listesi} }", + "asset-profile-name-starts-with": "'{{prefix}}' ile başlayan varlık profilleri", "type-asset": "Varlık", "type-assets": "Varlıklar", - "list-of-assets": "{ count, plural, =1 {Bir varlık} other {# Varlığın Listesi} }", - "asset-name-starts-with": "İsmi '{{prefix}}' ile başlayan varlıklar", + "list-of-assets": "{ count, plural, =1 {Bir varlık} other {# varlık listesi} }", + "asset-name-starts-with": "'{{prefix}}' ile başlayan varlıklar", "type-entity-view": "Varlık Görünümü", "type-entity-views": "Varlık Görünümleri", - "list-of-entity-views": "{ count, plural, =1 {Bir varlık görünümü} other {# varlık görüntüleme} } listesi", - "entity-view-name-starts-with": "İsmi {{prefix}} ile başlayan varlık görünümleri", + "list-of-entity-views": "{ count, plural, =1 {Bir varlık görünümü} other {# varlık görünümü listesi} }", + "entity-view-name-starts-with": "'{{prefix}}' ile başlayan varlık görünümleri", "type-rule": "Kural", "type-rules": "Kurallar", - "list-of-rules": "{ count, plural, =1 {Bir kural} other {# Kuralın Listesi} }", - "rule-name-starts-with": "İsmi '{{prefix}}' ile başlayan kurallar", + "list-of-rules": "{ count, plural, =1 {Bir kural} other {# kural listesi} }", + "rule-name-starts-with": "'{{prefix}}' ile başlayan kurallar", "type-plugin": "Eklenti", "type-plugins": "Eklentiler", - "list-of-plugins": "{ count, plural, =1 {Bir eklenti} other {# Eklentinin Listesi} }", - "plugin-name-starts-with": "İsmi '{{prefix}}' ile başlayan eklentiler", - "type-tenant": "Tenant", - "type-tenants": "Tenantlar", - "list-of-tenants": "{ count, plural, =1 {Bir tenant} other {# Tenantın Listesi} }", - "tenant-name-starts-with": "İsmi '{{prefix}}' ile başlayan tenantlar", - "type-tenant-profile": "Tenant profili", - "type-tenant-profiles": "Tenant profilleri", - "list-of-tenant-profiles": "{ count, plural, =1 {Bir tenant profili} other {# tenant profili listesi} }", - "tenant-profile-name-starts-with": "İsmi '{{prefix}}' ile başlayan tenant profilleri", - "type-customer": "Kullanıcı Grubu", - "type-customers": "Kullanıcı Grupları", - "list-of-customers": "{ count, plural, =1 {Bir kullanıcı grubu} other {# kullanıcı grupları listesi} }", - "customer-name-starts-with": "İsmi '{{prefix}}' ile başlayan kullanıcı grupları", + "list-of-plugins": "{ count, plural, =1 {Bir eklenti} other {# eklenti listesi} }", + "plugin-name-starts-with": "'{{prefix}}' ile başlayan eklentiler", + "type-tenant": "Kiracı", + "type-tenants": "Kiracılar", + "list-of-tenants": "{ count, plural, =1 {Bir kiracı} other {# kiracı listesi} }", + "tenant-name-starts-with": "'{{prefix}}' ile başlayan kiracılar", + "type-tenant-profile": "Kiracı profili", + "type-tenant-profiles": "Kiracı profilleri", + "list-of-tenant-profiles": "{ count, plural, =1 {Bir kiracı profili} other {# kiracı profili listesi} }", + "tenant-profile-name-starts-with": "'{{prefix}}' ile başlayan kiracı profilleri", + "type-customer": "Müşteri", + "type-customers": "Müşteriler", + "list-of-customers": "{ count, plural, =1 {Bir müşteri} other {# müşteri listesi} }", + "customer-name-starts-with": "'{{prefix}}' ile başlayan müşteriler", "type-user": "Kullanıcı", "type-users": "Kullanıcılar", "list-of-users": "{ count, plural, =1 {Bir kullanıcı} other {# kullanıcı listesi} }", - "user-name-starts-with": "İsmi '{{prefix}}' ile başlayan kullanıcılar", + "user-name-starts-with": "'{{prefix}}' ile başlayan kullanıcılar", "type-dashboard": "Gösterge Paneli", "type-dashboards": "Gösterge Panelleri", "list-of-dashboards": "{ count, plural, =1 {Bir gösterge paneli} other {# gösterge paneli listesi} }", - "dashboard-name-starts-with": "İsmi '{{prefix}}' ile başlayan gösterge panelleri", + "dashboard-name-starts-with": "'{{prefix}}' ile başlayan gösterge panelleri", "type-alarm": "Alarm", "type-alarms": "Alarmlar", "list-of-alarms": "{ count, plural, =1 {Bir alarm} other {# alarm listesi} }", - "alarm-name-starts-with": "İsmi '{{prefix}}' ile başlayan alarmlar", - "type-rulechain": "Kural zinciri", - "type-rulechains": "Kural zincirleri", + "alarm-name-starts-with": "'{{prefix}}' ile başlayan alarmlar", + "type-rulechain": "Kural Zinciri", + "type-rulechains": "Kural Zincirleri", "list-of-rulechains": "{ count, plural, =1 {Bir kural zinciri} other {# kural zinciri listesi} }", - "rulechain-name-starts-with": "İsmi '{{prefix}}' ile başlayan kural zincirleri", + "rulechain-name-starts-with": "'{{prefix}}' ile başlayan kural zincirleri", "type-rulenode": "Kural düğümü", "type-rulenodes": "Kural düğümleri", - "list-of-rulenodes": "{ count, plural, =1 {One rule node} other {List of # rule nodes} }", - "rulenode-name-starts-with": "İsmi '{{prefix}}' ile başlayan kural düğümleri", - "type-current-customer": "Aktif Kullanıcı Grubu", - "type-current-tenant": "Aktif Tenant", - "type-current-user": "Aktif Kullanıcı", - "type-current-user-owner": "Aktif Kullanıcı Sahibi", - "search": "Öğeleri ara", - "selected-entities": "{ count, plural, =1 {1 öğe} other {# öğe} } seçildi", - "entity-name": "Öğe adı", - "entity-label": "Öğe etiketi", - "details": "Öğe detayları", - "no-entities-prompt": "Öğe bulunamadı", - "no-data": "Gösterilecek veri yok", - "columns-to-display": "Görüntülenecek Sütunlar", + "list-of-rulenodes": "{ count, plural, =1 {Bir kural düğümü} other {# kural düğümü listesi} }", + "rulenode-name-starts-with": "'{{prefix}}' ile başlayan kural düğümleri", + "type-current-customer": "Mevcut Müşteri", + "type-current-tenant": "Mevcut Kiracı", + "type-current-user": "Mevcut Kullanıcı", + "type-current-user-owner": "Mevcut Kullanıcı Sahibi", + "type-calculated-field": "Hesaplanmış alan", + "type-calculated-fields": "Hesaplanmış alanlar", + "type-ai-model": "Yapay zeka modeli", + "type-ai-models": "Yapay zeka modelleri", + "type-widgets-bundle": "Bileşen paketi", + "type-widgets-bundles": "Bileşen paketleri", + "list-of-widgets-bundles": "{ count, plural, =1 {Bir bileşen paketi} other {# bileşen paketi listesi} }", + "type-widget": "Bileşen", + "type-widgets": "Bileşenler", + "list-of-widgets": "{ count, plural, =1 {Bir bileşen} other {# bileşen listesi} }", + "search": "Varlıkları ara", + "selected-entities": "{ count, plural, =1 {1 varlık} other {# varlık} } seçildi", + "entity-name": "Varlık adı", + "entity-label": "Varlık etiketi", + "details": "Varlık ayrıntıları", + "no-entities-prompt": "Varlık bulunamadı", + "no-data": "Görüntülenecek veri yok", + "columns-to-display": "Görüntülenecek sütunlar", "type-api-usage-state": "API Kullanım Durumu", "type-edge": "Uç", "type-edges": "Uçlar", "list-of-edges": "{ count, plural, =1 {Bir uç} other {# uç listesi} }", - "edge-name-starts-with": "İsmi '{{prefix}}' ile başlayan uçlar", + "edge-name-starts-with": "'{{prefix}}' ile başlayan uçlar", + "version-conflict": { + "message": "Mevcut sürümü üzerine yazmak mı yoksa değişiklikleri iptal edip en son sürümü yüklemek mi istiyorsunuz?", + "link": "{{entityType}} varlığının kendi sürümünü indirmek için bu bağlantıyı kullanabilirsiniz", + "overwrite": "Sürümün üzerine yaz", + "discard": "Değişiklikleri iptal et" + }, "type-tb-resource": "Kaynak", - "type-ota-package": "OTA paketi" + "type-tb-resources": "Kaynaklar", + "list-of-tb-resources": "{ count, plural, =1 {Bir kaynak} other {# kaynak listesi} }", + "type-ota-package": "OTA paketi", + "type-ota-packages": "OTA paketleri", + "list-of-ota-packages": "{ count, plural, =1 {Bir OTA paketi} other {# OTA paketi listesi} }", + "type-rpc": "RPC", + "type-queue": "Kuyruk", + "type-queue-stats": "Kuyruk istatistikleri", + "type-queues-stats": "Kuyruklar istatistikleri", + "type-notification": "Bildirim", + "type-notification-rule": "Bildirim kuralı", + "type-notification-rules": "Bildirim kuralları", + "list-of-notification-rules": "{ count, plural, =1 {Bir bildirim kuralı} other {# bildirim kuralı listesi} }", + "type-notification-target": "Bildirim alıcısı", + "type-notification-targets": "Bildirim alıcıları", + "list-of-notification-targets": "{ count, plural, =1 {Bir bildirim alıcısı} other {# bildirim alıcısı listesi} }", + "type-notification-request": "Bildirim isteği", + "type-notification-template": "Bildirim şablonu", + "type-notification-templates": "Bildirim şablonları", + "list-of-notification-templates": "{ count, plural, =1 {Bir bildirim şablonu} other {# bildirim şablonu listesi} }", + "link": "bağlantı", + "type-oauth2-client": "OAuth 2.0 istemcisi", + "type-oauth2-clients": "OAuth 2.0 istemcileri", + "list-of-oauth2-clients": "{ count, plural, =1 {Bir OAuth 2.0 istemcisi} other {# OAuth 2.0 istemcisi listesi} }", + "type-domain": "Alan", + "type-domains": "Alanlar", + "list-of-domains": "{ count, plural, =1 {Bir alan} other {# alan listesi} }", + "type-mobile-app": "Mobil uygulama", + "type-mobile-apps": "Mobil uygulamalar", + "list-of-mobile-apps": "{ count, plural, =1 {Bir mobil uygulama} other {# mobil uygulama listesi} }", + "type-mobile-app-bundle": "Mobil paket", + "type-mobile-app-bundles": "Mobil paketler", + "list-of-mobile-app-bundles": "{ count, plural, =1 {Bir mobil paket} other {# mobil paket listesi} }" }, "entity-field": { "created-time": "Oluşturulma zamanı", - "name": "İsim", + "name": "Ad", "type": "Tür", "first-name": "Ad", "last-name": "Soyad", "email": "E-posta", "title": "Başlık", "country": "Ülke", - "state": "Eyalet", + "state": "Eyalet/İl", "city": "Şehir", "address": "Adres", "address2": "Adres 2", "zip": "Posta kodu", "phone": "Telefon", - "label": "Etiket" + "label": "Etiket", + "queue-name": "Kuyruk adı", + "service-id": "Servis Kimliği", + "owner-name": "Sahip adı", + "owner-type": "Sahip türü" }, "entity-view": { - "entity-view": "Öğe Görünümü", - "entity-view-required": "Öğe Görünümü gerekli.", - "entity-views": "Öğe Görünümleri", - "management": "Öğe Görünümü yönetimi", - "view-entity-views": "Öğe Görünümlerini Görüntüle", - "entity-view-alias": "Öğe Görünümü kısa adı", - "aliases": "Öğe Görünümü kısa adları", + "entity-view": "Varlık görünümü", + "entity-view-required": "Varlık görünümü gereklidir.", + "entity-views": "Varlık görünümleri", + "management": "Varlık Görünümü yönetimi", + "view-entity-views": "Varlık Görünümlerini Görüntüle", + "entity-view-alias": "Varlık Görünümü takma adı", + "aliases": "Varlık Görünümü takma adları", "no-alias-matching": "'{{alias}}' bulunamadı.", - "no-aliases-found": "Kısa ad bulunamadı.", - "no-key-matching": "'{{key}}' anahtar bulunamadı.", - "no-keys-found": "Anahtar bulunamadı.", + "no-aliases-found": "Hiçbir takma ad bulunamadı.", + "no-key-matching": "'{{key}}' bulunamadı.", + "no-keys-found": "Hiçbir anahtar bulunamadı.", "create-new-alias": "Yeni bir tane oluştur!", "create-new-key": "Yeni bir tane oluştur!", - "duplicate-alias-error": "Yinelenen kısa ad bulundu '{{alias}}'.
    öğe Görünümü kısa adları gösterge panelinde benzersiz olmalıdır.", - "configure-alias": "'{{alias}}' kısa adını yapılandırın", - "no-entity-views-matching": "'{{entity}}' ile eşleşen öğe görünümü bulunamadı.", - "public": "Açık", - "alias": "Kısa ad", - "alias-required": "Öğe Görünümü kısa adı gerekli.", - "remove-alias": "Öğe görünümü kısa adını kaldır", - "add-alias": "Öğe görünümü kısa adı ekle", - "name-starts-with": "Öğe Görünümü adı ifadesi", + "duplicate-alias-error": "Yinelenen takma ad bulundu '{{alias}}'.
    Varlık Görünümü takma adları gösterge paneli içinde benzersiz olmalıdır.", + "configure-alias": "'{{alias}}' takma adını yapılandır", + "no-entity-views-matching": "'{{entity}}' ile eşleşen hiçbir varlık görünümü bulunamadı.", + "public": "Genel", + "alias": "Takma ad", + "alias-required": "Varlık Görünümü takma adı gereklidir.", + "remove-alias": "Varlık görünümü takma adını kaldır", + "add-alias": "Varlık görünümü takma adı ekle", + "name-starts-with": "Varlık Görünümü ad ifadesi", "help-text": "İhtiyaca göre '%' kullanın: '%entity-view_name_contains%', '%entity-view_name_ends', 'entity-view_starts_with'.", - "entity-view-list": "Öğe Görünümü listesi", + "entity-view-list": "Varlık Görünümü listesi", "use-entity-view-name-filter": "Filtre kullan", - "entity-view-list-empty": "Hiçbir öğe görünümü seçilmedi.", - "entity-view-name-filter-required": "Öğe görünümü adı filtresi gerekli.", - "entity-view-name-filter-no-entity-view-matched": "'{{entityView}}' ile başlayan öğe görünümü bulunamadı.", - "add": "Öğe Görünümü Ekle", - "entity-view-public": "Öğe görünümü herkese açık", - "assign-to-customer": "Kullanıcı grubuna ata", - "assign-entity-view-to-customer": "Öğe görünümlerini kullanıcı grubuna ata", - "assign-entity-view-to-customer-text": "Lütfen kullanıcı grubuna atanacak öğe görünümlerini seçin", - "assign-entity-view-to-edge-title": "Öğe görünümlerini uca ata", - "no-entity-views-text": "Öğe görünümü bulunamadı", - "assign-to-customer-text": "Lütfen öğe görünümlerini atamak için müşteriyi seçin", - "entity-view-details": "Öğe görünümü ayrıntıları", - "add-entity-view-text": "Yeni öğe görünümü ekle", - "delete": "Öğe görünümünü sil", - "assign-entity-views": "Öğe görünümlerini ata", - "assign-entity-views-text": "{ count, plural, =1 {1 öğe görünümünü} other {# öğe görünümünü} } kullanıcı grubuna ata", - "delete-entity-views": "Öğe görünümlerini sil", - "unassign-from-customer": "Kullanıcı grubundan atamayı kaldır", - "unassign-entity-views": "Öğe görünümlerinin atamasını kaldır", - "unassign-entity-views-action-title": "{ count, plural, =1 {1 öğe görünümünü} other {# öğe görünümünü} } kullanıcı grubundan kaldır", - "assign-new-entity-view": "Yeni öğe görünümü ata", - "delete-entity-view-title": "'{{entityViewName}}' öğe görünümünü silmek istediğinizden emin misiniz?", - "delete-entity-view-text": "Dikkatli olun, onaydan sonra öğe görünümü ve ilgili tüm veriler kurtarılamaz hale gelecektir.", - "delete-entity-views-title": "{ count, plural, =1 {1 öğe görünümünü} other {# öğe görünümünü} } silmek istediğinizden emin misiniz?", - "delete-entity-views-action-title": "{ count, plural, =1 {1 öğe görünümünü} other {# öğe görünümünü} } sil", - "delete-entity-views-text": "Dikkatli olun, onaydan sonra seçilen tüm öğe görünümleri kaldırılacak ve ilgili tüm veriler kurtarılamaz hale gelecektir.", - "unassign-entity-view-title": "'{{entityViewName}}' öğe görünümünün atamasını kaldırmak istediğinizden emin misiniz?", - "unassign-entity-view-text": "Onaydan sonra öğe görünümünün ataması kaldırılacak ve müşteri tarafından erişilebilir olmayacaktır.", - "unassign-entity-view": "Öğe görünümünün atamasını kaldır", - "unassign-entity-views-title": "{ count, plural, =1 {1 öğe görünümünün} other {# öğe görünümünün} } atamasını kaldırmak istediğinizden emin misiniz?", - "unassign-entity-views-text": "Onaydan sonra, seçilen tüm öğe görünümlerinin ataması kaldırılacak ve kullanıcı grubu tarafından erişilebilir olmayacaktır.", - "entity-view-type": "Öğe Görünümü türü", - "entity-view-type-required": "Öğe Görünümü türü gerekli.", - "select-entity-view-type": "Öğe Görünümü türü seç", - "enter-entity-view-type": "Öğe görünümü türünü girin", - "any-entity-view": "Herhangi bir öğe görünümü", - "no-entity-view-types-matching": "'{{entitySubtype}}' ile eşleşen öğe görünümü türü bulunamadı.", - "entity-view-type-list-empty": "Hiçbir öğe görünümü türü seçilmedi.", - "entity-view-types": "Öğe Görünümü türleri", - "created-time": "Oluşturulan zaman", - "name": "İsim", - "name-required": "İsim zorunlu.", + "entity-view-list-empty": "Seçili varlık görünümü yok.", + "entity-view-name-filter-required": "Varlık görünümü adı filtresi gereklidir.", + "entity-view-name-filter-no-entity-view-matched": "'{{entityView}}' ile başlayan varlık görünümü bulunamadı.", + "add": "Varlık görünümü ekle", + "entity-view-public": "Varlık görünümü herkese açıktır", + "assign-to-customer": "Müşteriye ata", + "assign-entity-view-to-customer": "Varlık Görünümü(leri)ni Müşteriye Ata", + "assign-entity-view-to-customer-text": "Lütfen müşteriye atanacak varlık görünümlerini seçin", + "no-entity-views-text": "Hiçbir varlık görünümü bulunamadı", + "assign-to-customer-text": "Lütfen varlık görünümü(lerini) atamak için müşteri seçin", + "entity-view-details": "Varlık görünümü detayları", + "add-entity-view-text": "Yeni varlık görünümü ekle", + "delete": "Varlık görünümünü sil", + "assign-entity-views": "Varlık görünümlerini ata", + "assign-entity-views-text": "{ count, plural, =1 {1 varlık görünümü} other {# varlık görünümleri} } müşteriye ata", + "delete-entity-views": "Varlık görünümlerini sil", + "make-public": "Varlık görünümünü herkese açık yap", + "make-private": "Varlık görünümünü özel yap", + "unassign-from-customer": "Müşteriden kaldır", + "unassign-entity-views": "Varlık görünümlerini kaldır", + "unassign-entity-views-action-title": "{ count, plural, =1 {1 varlık görünümünü} other {# varlık görünümünü} } müşteriden kaldır", + "assign-new-entity-view": "Yeni varlık görünümü ata", + "delete-entity-view-title": "Varlık görünümünü '{{entityViewName}}' silmek istediğinizden emin misiniz?", + "delete-entity-view-text": "Dikkatli olun, onaydan sonra varlık görünümü ve tüm ilişkili veriler geri alınamaz hale gelecektir.", + "delete-entity-views-title": "{ count, plural, =1 {1 varlık görünümünü} other {# varlık görünümünü} } silmek istediğinizden emin misiniz?", + "delete-entity-views-action-title": "{ count, plural, =1 {1 varlık görünümünü} other {# varlık görünümünü} } sil", + "delete-entity-views-text": "Dikkatli olun, onaydan sonra seçili tüm varlık görünümleri ve ilişkili tüm veriler geri alınamaz hale gelecektir.", + "make-public-entity-view-title": "Varlık görünümünü '{{entityViewName}}' herkese açık yapmak istediğinizden emin misiniz?", + "make-public-entity-view-text": "Onaydan sonra varlık görünümü ve tüm verileri herkese açık olacak ve başkaları tarafından erişilebilir hale gelecektir.", + "make-private-entity-view-title": "Varlık görünümünü '{{entityViewName}}' özel yapmak istediğinizden emin misiniz?", + "make-private-entity-view-text": "Onaydan sonra varlık görünümü ve tüm verileri özel olacak ve başkaları tarafından erişilemeyecektir.", + "unassign-entity-view-title": "Varlık görünümünü '{{entityViewName}}' kaldırmak istediğinizden emin misiniz?", + "unassign-entity-view-text": "Onaydan sonra varlık görünümü müşteriyle ilişkilendirilmemiş olacak ve müşteri tarafından erişilemeyecektir.", + "unassign-entity-view": "Varlık görünümünü kaldır", + "unassign-entity-views-title": "{ count, plural, =1 {1 varlık görünümünü} other {# varlık görünümünü} } kaldırmak istediğinizden emin misiniz?", + "unassign-entity-views-text": "Onaydan sonra seçilen tüm varlık görünümleri kaldırılacak ve müşteri tarafından erişilemeyecektir.", + "entity-view-type": "Varlık görünüm tipi", + "entity-view-type-required": "Varlık görünüm tipi gereklidir.", + "select-entity-view-type": "Varlık görünüm türünü seçin", + "enter-entity-view-type": "Varlık görünüm türünü girin", + "any-entity-view": "Herhangi bir varlık görünümü", + "no-entity-view-types-matching": "'{{entitySubtype}}' ile eşleşen varlık görünüm türü bulunamadı.", + "entity-view-type-list-empty": "Seçili varlık görünüm türü yok.", + "entity-view-types": "Varlık görünüm türleri", + "created-time": "Oluşturulma zamanı", + "name": "Ad", + "name-required": "Ad gereklidir.", + "name-max-length": "Ad 256 karakterden kısa olmalıdır", + "type-max-length": "Varlık görünüm türü 256 karakterden kısa olmalıdır", "description": "Açıklama", - "events": "Etkinlikler", + "events": "Olaylar", "details": "Detaylar", - "copyId": "Öğe görünümü kimliğini kopyala", - "idCopiedMessage": "Öğe görünümü kimliği panoya kopyalandı", - "assignedToCustomer": "Kullanıcı grubuna atandı", - "unable-entity-view-device-alias-title": "Öğe görünümü kısa adı silinemiyor", - "unable-entity-view-device-alias-text": "Cihaz kısa adı '{{entityViewAlias}}', aşağıdaki gösterge(ler) tarafından kullanıldığı için silinemez:
    {{widgetsList}}", - "select-entity-view": "Öğe görünümü seç", - "make-public": "Öğe görünümünü herkese açık yap", - "make-private": "Öğe görünümünü gizli yap", - "start-date": "Başlangıç tarihi", - "start-ts": "Başlangıç saati", - "end-date": "Bitiş tarihi", - "end-ts": "Bitiş saati", - "date-limits": "Tarih limitleri", - "client-attributes": "İstemci öznitelikler", - "shared-attributes": "Paylaşılan öznitelikler", - "server-attributes": "Sunucu öznitelikler", + "copyId": "Varlık görünüm Kimliğini kopyala", + "idCopiedMessage": "Varlık görünüm Kimliği panoya kopyalandı", + "assignedToCustomer": "Müşteriye atanmış", + "unable-entity-view-device-alias-title": "Varlık görünüm takma adı silinemiyor", + "unable-entity-view-device-alias-text": "Aygıt takma adı '{{entityViewAlias}}' aşağıdaki bileşen(ler) tarafından kullanıldığı için silinemez:
    {{widgetsList}}", + "select-entity-view": "Varlık görünümünü seçin", + "start-ts": "Başlangıç zamanı", + "end-ts": "Bitiş zamanı", + "date-limits": "Tarih sınırları", + "client-attributes": "İstemci özellikleri", + "shared-attributes": "Paylaşılan özellikler", + "server-attributes": "Sunucu özellikleri", "timeseries": "Zaman serisi", - "client-attributes-placeholder": "İstemci öznitelikler", - "shared-attributes-placeholder": "Paylaşılan öznitelikler", - "server-attributes-placeholder": "Sunucu öznitelikler", + "client-attributes-placeholder": "İstemci özellikleri", + "shared-attributes-placeholder": "Paylaşılan özellikler", + "server-attributes-placeholder": "Sunucu özellikleri", "timeseries-placeholder": "Zaman serisi", - "target-entity": "Hedef öğe", - "attributes-propagation": "Öznitelik işlenmesi", - "attributes-propagation-hint": "Öğe Görünümü, bu öğe görünümünü her kaydettiğinizde veya güncellediğinizde hedef öğeden belirtilen öznitelikleri otomatik olarak kopyalayacaktır. Performans nedenleriyle, hedef öğe öznitelikleri, her bir öznitelik değişikliğinde öğe görünümüne işlenmez. Kural zincirinizde \"copy to view\" kural düğümünü yapılandırarak ve \"Post attributes\" ve \"Attributes Updated\" iletilerini yeni kural düğümüne bağlayarak otomatik işlenmesini etkinleştirebilirsiniz.", - "timeseries-data": "Zaman serisi verileri", - "timeseries-data-hint": "Öğe görünümü tarafından erişilebilir olacak hedef varlığın zaman serisi veri anahtarlarını yapılandırın. Bu zaman serisi verileri salt okunurdur.", - "make-public-entity-view-title": "'{{entityViewName}}' öğe görünümünü herkese açık hale getirmek istediğinizden emin misiniz?", - "make-public-entity-view-text": "Onaydan sonra öğe görünümü ve tüm verileri herkese açık hale getirilecek ve başkaları tarafından erişilebilir hale getirilecektir.", - "make-private-entity-view-title": "'{{entityViewName}}' öğe görünümünü gizli yapmak istediğinizden emin misiniz?", - "make-private-entity-view-text": "Onaydan sonra öğe görünümü ve tüm verileri gizli hale getirilecek ve başkaları tarafından erişilemeyecek.", - "assign-entity-view-to-edge": "Öğe Görünümlerini Uca Ata", - "assign-entity-view-to-edge-text": "Lütfen uca atanacak öğe görünümlerini seçin", - "unassign-entity-view-from-edge-title": "'{{entityViewName}}' öğe görünümünün atamasını kaldırmak istediğinizden emin misiniz?", - "unassign-entity-view-from-edge-text": "Onaydan sonra öğe görünümünün ataması kaldırılacak ve uç tarafından erişilebilir olmayacaktır.", - "unassign-entity-views-from-edge-action-title": "{ count, plural, =1 {1 öğe görünümünü} other {# öğe görünümünü} } uçtan kaldır", - "unassign-entity-view-from-edge": "Öğe görünümünün atamasını kaldır", - "unassign-entity-views-from-edge-title": "{ count, plural, =1 {1 öğe görünümünün} other {# öğe görünümünün} } atamasını kaldırmak istediğinizden emin misiniz?", - "unassign-entity-views-from-edge-text": "Onaydan sonra, seçilen tüm öğe görünümlerinin ataması kaldırılacak ve uç tarafından erişilebilir olmayacaktır." + "target-entity": "Hedef varlık", + "attributes-propagation": "Özellik yayılımı", + "attributes-propagation-hint": "Varlık görünümü, bu varlık görünümünü her kaydettiğinizde veya güncellediğinizde, hedef varlıktan belirtilen özellikleri otomatik olarak kopyalar. Performans nedenleriyle, hedef varlık özellikleri her değiştiğinde görünümde otomatik olarak güncellenmez. Otomatik yayılımı etkinleştirmek için kural zincirinizde 'copy to view' kural düğümünü yapılandırıp, 'Post attributes' ve 'Attributes Updated' mesajlarını bu düğüme yönlendirin.", + "timeseries-data": "Zaman serisi verisi", + "timeseries-data-hint": "Varlık görünümünde erişilebilir olacak hedef varlığın zaman serisi veri anahtarlarını yapılandırın. Bu zaman serisi verisi yalnızca okunabilir durumdadır.", + "search": "Varlık görünümlerinde ara", + "selected-entity-views": "{ count, plural, =1 {1 varlık görünümü} other {# varlık görünümü} } seçildi", + "assign-entity-view-to-edge": "Varlık görünüm(lerini) Uç Cihaza Ata", + "assign-entity-view-to-edge-text": "Uç cihaza atamak için varlık görünümlerini seçin", + "unassign-entity-view-from-edge-title": "'{{entityViewName}}' varlık görünümünün uç cihazdan bağlantısını kaldırmak istediğinizden emin misiniz?", + "unassign-entity-view-from-edge-text": "Onaydan sonra, varlık görünümünün uç cihaza erişimi kaldırılacak.", + "unassign-entity-views-from-edge-action-title": "{ count, plural, =1 {1 varlık görünümünü} other {# varlık görünümünü} } uç cihazdan kaldır", + "unassign-entity-view-from-edge": "Varlık görünümünün bağlantısını kaldır", + "unassign-entity-views-from-edge-title": "{ count, plural, =1 {1 varlık görünümünün} other {# varlık görünümünün} } bağlantısını kaldırmak istediğinizden emin misiniz?", + "unassign-entity-views-from-edge-text": "Onaydan sonra tüm seçili varlık görünümlerinin uç cihaz bağlantıları kaldırılacak ve erişilemeyecekler." }, "event": { - "event-type": "Etkinlik türü", - "events-filter": "Etkinlik Filtresi", + "event-type": "Olay türü", + "events-filter": "Olay Filtresi", + "clean-events": "Olayları Temizle", "type-error": "Hata", - "type-lc-event": "Yaşam Döngüsü Etkinliği", + "type-lc-event": "Yaşam döngüsü olayı", "type-stats": "İstatistikler", - "type-debug-rule-node": "Hata Ayıklama", - "type-debug-rule-chain": "Hata Ayıklama", - "no-events-prompt": "Hiçbir etkinlik bulunamadı", + "type-debug-rule-node": "Hata ayıklama", + "type-debug-rule-chain": "Hata ayıklama", + "type-debug-calculated-field": "Hata ayıklama", + "arguments": "Argümanlar", + "result": "Sonuç", + "no-events-prompt": "Hiç olay bulunamadı", "error": "Hata", "alarm": "Alarm", - "event-time": "Etkinlik zamanı", + "event-time": "Olay zamanı", "server": "Sunucu", - "body": "Body", - "method": "Metod", + "body": "Gövde", + "method": "Yöntem", "type": "Tür", + "metadata": "Meta veriler", + "message": "Mesaj", "message-id": "Mesaj Kimliği", + "copy-message-id": "Mesaj Kimliğini kopyala", "message-type": "Mesaj Türü", "data-type": "Veri Türü", "relation-type": "İlişki Türü", - "metadata": "Meta veri", "data": "Veri", - "event": "Etkinlik", + "event": "Olay", "status": "Durum", "success": "Başarılı", "failed": "Başarısız", "messages-processed": "İşlenen mesajlar", - "min-messages-processed": "İşlenen minimum mesaj sayısı", + "max-messages-processed": "Maksimum işlenen mesaj", + "min-messages-processed": "Minimum işlenen mesaj", "errors-occurred": "Hatalar oluştu", - "min-errors-occurred": "Minimum hata oluştu", + "max-errors-occurred": "Maksimum hata", + "min-errors-occurred": "Minimum hata", "min-value": "Minimum değer 0'dır.", "all-events": "Tümü", "has-error": "Hata var", - "entity-id": "Öğe Kimliği", - "entity-type": "Öğe Türü" + "entity-id": "Varlık Kimliği", + "copy-entity-id": "Varlık Kimliğini kopyala", + "entity-type": "Varlık türü", + "clear-filter": "Filtreyi Temizle", + "clear-request-title": "Tüm olayları temizle", + "clear-request-text": "Tüm olayları temizlemek istediğinizden emin misiniz?", + "started": "Başlatıldı", + "updated": "Güncellendi", + "stopped": "Durduruldu" }, "extension": { "extensions": "Uzantılar", @@ -1788,460 +2882,1418 @@ "type": "Tür", "key": "Anahtar", "value": "Değer", - "id": "ID", - "extension-id": "Uzantı Kimliği", - "extension-type": "Uzantı Türü", + "id": "Kimlik", + "extension-id": "Uzantı kimliği", + "extension-type": "Uzantı türü", "transformer-json": "JSON *", - "unique-id-required": "Mevcut uzantı kimliği zaten var.", + "unique-id-required": "Geçerli uzantı kimliği zaten mevcut.", "delete": "Uzantıyı sil", "add": "Uzantı ekle", "edit": "Uzantıyı düzenle", - "delete-extension-title": "'{{extensionId}}' uzantısını silmek istediğinizden emin misiniz?", - "delete-extension-text": "Dikkatli olun, onaydan sonra uzantı ve ilgili tüm veriler kurtarılamaz hale gelecektir.", - "delete-extensions-title": "{ count, plural, =1 {1 uzantıyı} other {# uzantıyı} } silmek istediğinizden emin misiniz?", + "delete-extension-title": "Uzantı '{{extensionId}}' silinsin mi?", + "delete-extension-text": "Dikkatli olun, onaydan sonra uzantı ve tüm ilgili veriler geri alınamaz hale gelecektir.", + "delete-extensions-title": "{ count, plural, =1 {1 uzantı} other {# uzantı} } silinsin mi?", "delete-extensions-text": "Dikkatli olun, onaydan sonra seçilen tüm uzantılar kaldırılacaktır.", - "converters": "Çeviriciler", - "converter-id": "Çevirici ID", + "converters": "Dönüştürücüler", + "converter-id": "Dönüştürücü kimliği", "configuration": "Yapılandırma", - "converter-configurations": "Çevirici Yapılandırmaları", - "token": "Güvenlik tokeni", - "add-converter": "Çevirici ekle", - "add-config": "Çevirici yapılandırması ekle", - "device-name-expression": "Cihaz adı modeli", - "device-type-expression": "Cihaz türü modeli", + "converter-configurations": "Dönüştürücü yapılandırmaları", + "token": "Güvenlik belirteci", + "add-converter": "Dönüştürücü ekle", + "add-config": "Dönüştürücü yapılandırması ekle", + "device-name-expression": "Cihaz adı ifadesi", + "device-type-expression": "Cihaz türü ifadesi", "custom": "Özel", - "to-double": "Double'a çevir", + "to-double": "Double'a dönüştür", "transformer": "Dönüştürücü", - "json-required": "Dönüştürücü json gerekli.", - "json-parse": "Dönüştürücü json'u ayrıştırılamıyor.", + "json-required": "Dönüştürücü JSON gereklidir.", + "json-parse": "Dönüştürücü JSON ayrıştırılamıyor.", "attributes": "Öznitelikler", "add-attribute": "Öznitelik ekle", - "add-map": "Eşleme elemanı ekle", + "add-map": "Eşleme öğesi ekle", "timeseries": "Zaman serisi", "add-timeseries": "Zaman serisi ekle", - "field-required": "Alan gerekli", - "brokers": "Brokerlar", + "field-required": "Alan gereklidir", + "brokers": "Broker'lar", "add-broker": "Broker ekle", - "host": "Host", + "host": "Sunucu", "port": "Port", - "port-range": "Port 1 ila 65535 aralığında olmalıdır.", + "port-range": "Port 1 ile 65535 arasında olmalıdır.", "ssl": "SSL", - "credentials": "Kimlik Bilgileri", - "username": "Kullanıcı Adı", + "credentials": "Kimlik bilgileri", + "username": "Kullanıcı adı", "password": "Şifre", - "retry-interval": "Milisaniye cinsinden yeniden deneme aralığı", + "retry-interval": "Yeniden deneme aralığı (ms)", "anonymous": "Anonim", - "basic": "Basic", + "basic": "Temel", "pem": "PEM", "ca-cert": "CA sertifika dosyası *", "private-key": "Özel anahtar dosyası *", "cert": "Sertifika dosyası *", "no-file": "Dosya seçilmedi.", - "drop-file": "Bir dosya bırakın veya yüklenecek dosyayı seçmek için tıklayın.", + "drop-file": "Bir dosya bırakın veya yüklemek için tıklayın.", "mapping": "Eşleme", "topic-filter": "Konu filtresi", - "converter-type": "Çevirici Türü", + "converter-type": "Dönüştürücü türü", "converter-json": "Json", - "json-name-expression": "Device name json expression", - "topic-name-expression": "Device name topic expression", - "json-type-expression": "Device type json expression", - "topic-type-expression": "Device type topic expression", - "attribute-key-expression": "Attribute key expression", - "attr-json-key-expression": "Attribute key json expression", - "attr-topic-key-expression": "Attribute key topic expression", - "request-id-expression": "Request id expression", - "request-id-json-expression": "Request id json expression", - "request-id-topic-expression": "Request id topic expression", - "response-topic-expression": "Response topic expression", - "value-expression": "Value expression", - "topic": "Topic", - "timeout": "Timeout in milliseconds", - "converter-json-required": "Converter json is required.", - "converter-json-parse": "Unable to parse converter json.", - "filter-expression": "Filter expression", - "connect-requests": "Connect requests", - "add-connect-request": "Add connect request", - "disconnect-requests": "Disconnect requests", - "add-disconnect-request": "Add disconnect request", - "attribute-requests": "Attribute requests", - "add-attribute-request": "Add attribute request", - "attribute-updates": "Attribute updates", - "add-attribute-update": "Add attribute update", - "server-side-rpc": "Server side RPC", - "add-server-side-rpc-request": "Add server-side RPC request", - "device-name-filter": "Device name filter", - "attribute-filter": "Attribute filter", - "method-filter": "Method filter", - "request-topic-expression": "Request topic expression", - "response-timeout": "Response timeout in milliseconds", - "topic-expression": "Topic expression", - "client-scope": "Client scope", - "add-device": "Add device", - "opc-server": "Servers", - "opc-add-server": "Add server", - "opc-add-server-prompt": "Please add server", - "opc-application-name": "Application name", - "opc-application-uri": "Application uri", - "opc-scan-period-in-seconds": "Scan period in seconds", - "opc-security": "Security", - "opc-identity": "Identity", - "opc-keystore": "Keystore", - "opc-type": "Type", - "opc-keystore-type": "Type", - "opc-keystore-location": "Location *", - "opc-keystore-password": "Password", - "opc-keystore-alias": "Alias", - "opc-keystore-key-password": "Key password", - "opc-device-node-pattern": "Device node pattern", - "opc-device-name-pattern": "Device name pattern", - "modbus-server": "Servers/slaves", - "modbus-add-server": "Add server/slave", - "modbus-add-server-prompt": "Please add server/slave", - "modbus-transport": "Transport", - "modbus-tcp-reconnect": "Automatically reconnect", - "modbus-rtu-over-tcp": "RTU over TCP", - "modbus-port-name": "Serial port name", - "modbus-encoding": "Encoding", - "modbus-parity": "Parity", - "modbus-baudrate": "Baud rate", - "modbus-databits": "Data bits", - "modbus-stopbits": "Stop bits", - "modbus-databits-range": "Data bits should be in a range from 7 to 8.", - "modbus-stopbits-range": "Stop bits should be in a range from 1 to 2.", - "modbus-unit-id": "Unit ID", - "modbus-unit-id-range": "Unit ID should be in a range from 1 to 247.", - "modbus-device-name": "Device name", - "modbus-poll-period": "Poll period (ms)", - "modbus-attributes-poll-period": "Attributes poll period (ms)", - "modbus-timeseries-poll-period": "Timeseries poll period (ms)", - "modbus-poll-period-range": "Poll period should be positive value.", - "modbus-tag": "Tag", - "modbus-function": "Function", - "modbus-register-address": "Register address", - "modbus-register-address-range": "Register address should be in a range from 0 to 65535.", - "modbus-register-bit-index": "Bit index", - "modbus-register-bit-index-range": "Bit index should be in a range from 0 to 15.", - "modbus-register-count": "Register count", - "modbus-register-count-range": "Register count should be a positive value.", - "modbus-byte-order": "Byte order", + "json-name-expression": "Cihaz adı json ifadesi", + "topic-name-expression": "Cihaz adı konu ifadesi", + "json-type-expression": "Cihaz türü json ifadesi", + "topic-type-expression": "Cihaz türü konu ifadesi", + "attribute-key-expression": "Öznitelik anahtar ifadesi", + "attr-json-key-expression": "Öznitelik anahtar json ifadesi", + "attr-topic-key-expression": "Öznitelik anahtar konu ifadesi", + "request-id-expression": "İstek ID ifadesi", + "request-id-json-expression": "İstek ID json ifadesi", + "request-id-topic-expression": "İstek ID konu ifadesi", + "response-topic-expression": "Yanıt konu ifadesi", + "value-expression": "Değer ifadesi", + "topic": "Konu", + "timeout": "Zaman aşımı (milisaniye cinsinden)", + "converter-json-required": "Dönüştürücü JSON gereklidir.", + "converter-json-parse": "Dönüştürücü JSON ayrıştırılamıyor.", + "filter-expression": "Filtre ifadesi", + "connect-requests": "Bağlantı istekleri", + "add-connect-request": "Bağlantı isteği ekle", + "disconnect-requests": "Bağlantı kesme istekleri", + "add-disconnect-request": "Bağlantı kesme isteği ekle", + "attribute-requests": "Öznitelik istekleri", + "add-attribute-request": "Öznitelik isteği ekle", + "attribute-updates": "Öznitelik güncellemeleri", + "add-attribute-update": "Öznitelik güncellemesi ekle", + "server-side-rpc": "Sunucu tarafı RPC", + "add-server-side-rpc-request": "Sunucu tarafı RPC isteği ekle", + "device-name-filter": "Cihaz adı filtresi", + "attribute-filter": "Öznitelik filtresi", + "method-filter": "Yöntem filtresi", + "request-topic-expression": "İstek konu ifadesi", + "response-timeout": "Yanıt zaman aşımı (milisaniye cinsinden)", + "topic-expression": "Konu ifadesi", + "client-scope": "İstemci kapsamı", + "add-device": "Cihaz ekle", + "opc-server": "Sunucular", + "opc-add-server": "Sunucu ekle", + "opc-add-server-prompt": "Lütfen sunucu ekleyin", + "opc-application-name": "Uygulama adı", + "opc-application-uri": "Uygulama URI'si", + "opc-scan-period-in-seconds": "Taramasüresi (saniye)", + "opc-security": "Güvenlik", + "opc-identity": "Kimlik", + "opc-keystore": "Anahtar deposu", + "opc-type": "Tür", + "opc-keystore-type": "Tür", + "opc-keystore-location": "Konum *", + "opc-keystore-password": "Parola", + "opc-keystore-alias": "Takma ad", + "opc-keystore-key-password": "Anahtar parolası", + "opc-device-node-pattern": "Cihaz düğüm deseni", + "opc-device-name-pattern": "Cihaz adı deseni", + "modbus-server": "Sunucular/köleler", + "modbus-add-server": "Sunucu/köle ekle", + "modbus-add-server-prompt": "Lütfen sunucu/köle ekleyin", + "modbus-transport": "Taşıma", + "modbus-tcp-reconnect": "Otomatik yeniden bağlan", + "modbus-rtu-over-tcp": "TCP üzerinden RTU", + "modbus-port-name": "Seri port adı", + "modbus-encoding": "Kodlama", + "modbus-parity": "Parite", + "modbus-baudrate": "Baud hızı", + "modbus-databits": "Veri bitleri", + "modbus-stopbits": "Dur bitleri", + "modbus-databits-range": "Veri bitleri 7 ile 8 arasında olmalıdır.", + "modbus-stopbits-range": "Dur bitleri 1 ile 2 arasında olmalıdır.", + "modbus-unit-id": "Birim kimliği", + "modbus-unit-id-range": "Birim kimliği 1 ile 247 arasında olmalıdır.", + "modbus-device-name": "Cihaz adı", + "modbus-poll-period": "Sorgu periyodu (ms)", + "modbus-attributes-poll-period": "Öznitelik sorgu periyodu (ms)", + "modbus-timeseries-poll-period": "Zaman serisi sorgu periyodu (ms)", + "modbus-poll-period-range": "Sorgu periyodu pozitif bir değer olmalıdır.", + "modbus-tag": "Etiket", + "modbus-function": "Fonksiyon", + "modbus-register-address": "Kayıt adresi", + "modbus-register-address-range": "Kayıt adresi 0 ile 65535 arasında olmalıdır.", + "modbus-register-bit-index": "Bit indeksi", + "modbus-register-bit-index-range": "Bit indeksi 0 ile 15 arasında olmalıdır.", + "modbus-register-count": "Kayıt sayısı", + "modbus-register-count-range": "Kayıt sayısı pozitif bir değer olmalıdır.", + "modbus-byte-order": "Bayt sıralaması", "sync": { - "status": "Status", - "sync": "Sync", - "not-sync": "Not sync", - "last-sync-time": "Last sync time", - "not-available": "Not available" - }, - "export-extensions-configuration": "Export extensions configuration", - "import-extensions-configuration": "Import extensions configuration", - "import-extensions": "Import extensions", - "import-extension": "Import extension", - "export-extension": "Export extension", - "file": "Extensions file", - "invalid-file-error": "Invalid extension file" + "status": "Durum", + "sync": "Senkronize", + "not-sync": "Senkronize değil", + "last-sync-time": "Son senkronizasyon zamanı", + "not-available": "Mevcut değil" + }, + "export-extensions-configuration": "Uzantı yapılandırmasını dışa aktar", + "import-extensions-configuration": "Uzantı yapılandırmasını içe aktar", + "import-extensions": "Uzantıları içe aktar", + "import-extension": "Uzantı içe aktar", + "export-extension": "Uzantı dışa aktar", + "file": "Uzantı dosyası", + "invalid-file-error": "Geçersiz uzantı dosyası" + }, + "feature": { + "advanced-features": "Gelişmiş özellikler" }, "filter": { "add": "Filtre ekle", - "edit": "Filtre düzenle", - "name": "Filtre ismi", - "name-required": "Filtre ismi gerekli.", - "duplicate-filter": "Aynı ada sahip filtre zaten mevcut.", + "edit": "Filtreyi düzenle", + "name": "Filtre adı", + "name-required": "Filtre adı gereklidir.", + "duplicate-filter": "Aynı ada sahip bir filtre zaten mevcut.", "filters": "Filtreler", "unable-delete-filter-title": "Filtre silinemiyor", - "unable-delete-filter-text": "'{{filter}}' filtresi, şu gösterge(ler) tarafından kullanıldığı için silinemez:
    {{widgetsList}}", - "duplicate-filter-error": "Yinelenen filtre \"{{filter}}\" bulundu.
    Filtreler, gösterge panelinde benzersiz olmalıdır.", - "missing-key-filters-error": "\"{{filter}}\" filtresi için anahtar filtreler eksik.", + "unable-delete-filter-text": "'{{filter}}' filtresi aşağıdaki widget(lar) tarafından kullanıldığı için silinemiyor:
    {{widgetsList}}", + "duplicate-filter-error": "Yinelenen filtre bulundu '{{filter}}'.
    Filtreler panoda benzersiz olmalıdır.", + "missing-key-filters-error": "'{{filter}}' filtresi için anahtar filtreler eksik.", "filter": "Filtre", "editable": "Düzenlenebilir", - "no-filters-found": "Filtre bulunamadı.", - "no-filter-text": "Filtre belirtilmedi", - "add-filter-prompt": "Lütfen filtre ekleyin", + "editable-hint": "Kullanıcının panolarda filtre değerini değiştirmesine izin ver.", + "no-filters-found": "Hiç filtre bulunamadı.", + "no-filter-text": "Herhangi bir filtre belirtilmedi", + "add-filter-prompt": "Lütfen bir filtre ekleyin", "no-filter-matching": "'{{filter}}' bulunamadı.", "create-new-filter": "Yeni bir tane oluştur!", - "filter-required": "Filtre gerekli.", + "create-new": "Yeni oluştur", + "filter-required": "Filtre gereklidir.", "operation": { - "operation": "Operasyon", - "equal": "equal", - "not-equal": "not equal", - "starts-with": "starts with", - "ends-with": "ends with", - "contains": "contains", - "not-contains": "not contains", - "greater": "greater than", - "less": "less than", - "greater-or-equal": "greater or equal", - "less-or-equal": "less or equal", - "and": "and", - "or": "or" - }, - "ignore-case": "ignore case", + "operation": "İşlem", + "equal": "eşittir", + "not-equal": "eşit değildir", + "starts-with": "ile başlar", + "ends-with": "ile biter", + "contains": "içerir", + "not-contains": "içermez", + "greater": "büyüktür", + "less": "küçüktür", + "greater-or-equal": "büyük veya eşit", + "less-or-equal": "küçük veya eşit", + "and": "ve", + "or": "veya", + "in": "içinde", + "not-in": "içinde değil" + }, + "ignore-case": "büyük/küçük harf yok say", "value": "Değer", "remove-filter": "Filtreyi kaldır", - "preview": "Filtre önizlemesi", - "no-filters": "Yapılandırılmış filtre yok", + "duplicate-filter-action": "Filtreyi kopyala", + "preview": "Filtre önizleme", + "no-filters": "Hiç filtre yapılandırılmadı", "add-filter": "Filtre ekle", "add-complex-filter": "Karmaşık filtre ekle", - "add-complex": "Kompleks ekle", + "add-complex": "Karmaşık ekle", "complex-filter": "Karmaşık filtre", "edit-complex-filter": "Karmaşık filtreyi düzenle", - "edit-filter-user-params": "Filtre belirteci kullanıcı parametrelerini düzenle", - "filter-user-params": "Filtre belirteci kullanıcı parametreleri", + "edit-filter-user-params": "Filtre ölçütü kullanıcı parametrelerini düzenle", + "filter-user-params": "Filtre ölçütü kullanıcı parametreleri", "user-parameters": "Kullanıcı parametreleri", "display-label": "Görüntülenecek etiket", - "order-priority": "Alan sırası önceliği", + "custom-label": "Özel etiket", + "custom-label-hint": "Filtre için kendi etiketinizi belirlemenizi sağlar. Devre dışı bırakıldığında, etiket otomatik olarak oluşturulacaktır.", + "order-priority": "Görüntüleme sırası", "key-filter": "Anahtar filtresi", "key-filters": "Anahtar filtreleri", "key-name": "Anahtar adı", - "key-name-required": "Anahtar adı gerekli.", + "key-name-required": "Anahtar adı gereklidir.", "key-type": { "key-type": "Anahtar türü", "attribute": "Öznitelik", "timeseries": "Zaman serisi", - "entity-field": "Öğe alanı", - "constant": "Sabit" + "entity-field": "Varlık alanı", + "constant": "Sabit", + "client-attribute": "İstemci özniteliği", + "server-attribute": "Sunucu özniteliği", + "shared-attribute": "Paylaşılan öznitelik" }, "value-type": { "value-type": "Değer türü", - "string": "String", - "numeric": "Numeric", - "boolean": "Boolean", - "date-time": "Datetime" + "string": "Metin", + "numeric": "Sayısal", + "boolean": "Mantıksal", + "date-time": "Tarih-saat" }, - "value-type-required": "Anahtar değer türü gerekli.", + "value-type-required": "Anahtar değer türü gereklidir.", "key-value-type-change-title": "Anahtar değer türünü değiştirmek istediğinizden emin misiniz?", - "key-value-type-change-message": "Yeni değer türünü onaylarsanız, girilen tüm anahtar filtreler kaldırılacaktır.", - "no-key-filters": "Yapılandırılmış anahtar filtre yok", - "add-key-filter": "Anahtar filtre ekle", - "remove-key-filter": "Anahtar filtreyi kaldır", + "key-value-type-change-message": "Yeni değer türünü onaylarsanız, girilen tüm anahtar filtreleri silinecektir.", + "no-key-filters": "Hiç anahtar filtresi yapılandırılmadı", + "add-key-filter": "Anahtar filtresi ekle", + "remove-key-filter": "Anahtar filtresini kaldır", "edit-key-filter": "Anahtar filtresini düzenle", "date": "Tarih", - "time": "Saat", - "current-tenant": "Aktif tenant", - "current-customer": "Aktif kullanıcı grubu", - "current-user": "Aktif kullanıcı", - "current-device": "Aktif cihaz", + "time": "Zaman", + "current-tenant": "Geçerli kiracı", + "current-customer": "Geçerli müşteri", + "current-user": "Geçerli kullanıcı", + "current-device": "Geçerli cihaz", "default-value": "Varsayılan değer", + "default-comma-separated-values": "Varsayılan virgülle ayrılmış değerler", "dynamic-source-type": "Dinamik kaynak türü", + "dynamic-value": "Dinamik değer", "no-dynamic-value": "Dinamik değer yok", - "source-attribute": "Kaynak özniteliği", + "source-attribute": "Kaynak öznitelik", "switch-to-dynamic-value": "Dinamik değere geç", "switch-to-default-value": "Varsayılan değere geç", - "inherit-owner": "Sahibinden devral", - "source-attribute-not-set": "Kaynak özniteliği ayarlanmamışsa" + "inherit-owner": "Sahipten devral", + "source-attribute-not-set": "Kaynak öznitelik ayarlanmazsa", + "unit": "Birim" }, "fullscreen": { - "expand": "Tam ekran yap", + "expand": "Tam ekrana genişlet", "exit": "Tam ekrandan çık", - "toggle": "Tam ekran modu aç/kapat", + "toggle": "Tam ekran modunu değiştir", "fullscreen": "Tam ekran" }, "function": { "function": "Fonksiyon" }, "gateway": { - "gateway-exists": "Aynı ada sahip cihaz zaten var.", "gateway-name": "Ağ geçidi adı", - "gateway-name-required": "Ağ geçidi adı gerekli.", - "gateway-saved": "Ağ geçidi yapılandırması başarıyla kaydedildi.", - "gateway": "Ağ geçidi", - "create-new-gateway": "Yeni bir ağ geçidi oluştur", - "create-new-gateway-text": "'{{gatewayName}}' adında yeni bir ağ geçidi oluşturmak istediğinizden emin misiniz?", - "no-gateway-found": "Ağ geçidi bulunamadı.", - "no-gateway-matching": " '{{item}}' bulunamadı." + "gateway-name-required": "Ağ geçidi adı gereklidir.", + "gateways": "Ağ geçitleri", + "create-new-gateway": "Yeni ağ geçidi oluştur", + "create-new-gateway-text": "‘{{gatewayName}}’ adında yeni bir ağ geçidi oluşturmak istediğinizden emin misiniz?", + "launch-command": "Başlatma komutu", + "no-gateway-found": "Hiç ağ geçidi bulunamadı.", + "no-gateway-matching": "'{{item}}' bulunamadı." }, "grid": { "delete-item-title": "Bu öğeyi silmek istediğinizden emin misiniz?", - "delete-item-text": "Dikkatli olun, onaylandıktan sonra bu öğe ve ilgili tüm veriler kurtarılamaz hale gelecektir.", - "delete-items-title": "{ count, plural, =1 {1 öğeyi} other {# öğeyi} } silmek istediğinizden emin misiniz??", + "delete-item-text": "Dikkatli olun, onaydan sonra bu öğe ve tüm ilişkili veriler geri alınamaz şekilde silinecektir.", + "delete-items-title": "{ count, plural, =1 {1 öğeyi} other {# öğeyi} } silmek istediğinizden emin misiniz?", "delete-items-action-title": "{ count, plural, =1 {1 öğeyi} other {# öğeyi} } sil", - "delete-items-text": "Dikkatli olun, onaydan sonra seçilen tüm öğeler kaldırılacak ve ilgili tüm veriler kurtarılamaz hale gelecektir.", + "delete-items-text": "Dikkatli olun, onaydan sonra seçilen tüm öğeler ve ilgili veriler geri alınamaz şekilde silinecektir.", "add-item-text": "Yeni öğe ekle", - "no-items-text": "Hiç bir öğe bulunamadı", - "item-details": "Ürün ayrıntıları", + "no-items-text": "Hiç öğe bulunamadı", + "item-details": "Öğe detayları", "delete-item": "Öğeyi sil", "delete-items": "Öğeleri sil", "scroll-to-top": "Yukarı kaydır" }, "help": { - "goto-help-page": "Yardım sayfasına git" + "goto-help-page": "Yardım sayfasına git", + "show-help": "Yardımı göster" }, "home": { "home": "Ana sayfa", "profile": "Profil", - "logout": "Çıkış", + "logout": "Çıkış yap", "menu": "Menü", "avatar": "Avatar", "open-user-menu": "Kullanıcı menüsünü aç" }, + "file-input": { + "browse-file": "Dosya gözat", + "browse-files": "Dosyalara gözat" + }, + "image": { + "gallery": "Görsel galerisi", + "search": "Görsel ara", + "selected-images": "{ count, plural, =1 {1 görsel} other {# görsel} } seçildi", + "created-time": "Oluşturulma zamanı", + "name": "Ad", + "name-required": "Ad gereklidir.", + "resolution": "Çözünürlük", + "size": "Boyut", + "system": "Sistem", + "download-image": "Görseli indir", + "export-image": "Görseli JSON olarak dışa aktar", + "import-image": "Görseli JSON'dan içe aktar", + "upload-image": "Görsel yükle", + "edit-image": "Görseli düzenle", + "image-details": "Görsel detayları", + "no-images": "Görsel bulunamadı", + "delete-image": "Görseli sil", + "delete-image-title": "‘{{imageTitle}}’ adlı görseli silmek istediğinizden emin misiniz?", + "delete-image-text": "Dikkatli olun, onaydan sonra görsel geri alınamaz hale gelecektir.", + "delete-images-title": "{ count, plural, =1 {1 görseli} other {# görseli} } silmek istediğinizden emin misiniz?", + "delete-images-text": "Dikkatli olun, onaydan sonra seçilen tüm görseller ve ilişkili veriler geri alınamaz şekilde silinecektir.", + "list-mode": "Liste görünümü", + "grid-mode": "Karo görünümü", + "image-preview": "Görsel önizleme", + "update-image": "Görseli güncelle", + "export-failed-error": "Görsel dışa aktarılamadı: {{error}}", + "image-json-file": "Görsel JSON dosyası", + "invalid-image-json-file-error": "Görsel JSON'dan içe aktarılamadı: Geçersiz görsel JSON veri yapısı.", + "image-is-in-use": "Görsel başka varlıklar tarafından kullanılıyor", + "images-are-in-use": "Görseller başka varlıklar tarafından kullanılıyor", + "image-is-in-use-text": "‘{{title}}’ görseli aşağıdaki varlıklar tarafından kullanıldığı için silinmedi:", + "images-are-in-use-text": "Tüm görseller silinemedi çünkü bazıları başka varlıklar tarafından kullanılmakta.
    İlgili görselleri görmek için ilgili satırdaki Referanslar düğmesine tıklayabilirsiniz.
    Yine de bu görselleri silmek istiyorsanız, aşağıdaki tabloda seçin ve Seçilenleri sil düğmesine tıklayın.", + "delete-image-in-use-text": "Görseli silmekte kararlıysanız Yine de sil düğmesine tıklayın.", + "system-entities": "Sistem varlıkları:", + "entities": "varlıklar:", + "references": "Referanslar", + "include-system-images": "Sistem görsellerini dahil et", + "clear-image": "Görseli temizle", + "no-image": "Görsel yok", + "no-image-selected": "Görsel seçilmedi", + "browse-from-gallery": "Galeriden gözat", + "set-link": "Bağlantı ayarla", + "image-link": "Görsel bağlantısı", + "link": "Bağlantı", + "copy-image-link": "Görsel bağlantısını kopyala", + "embed-image": "Görseli göm", + "embed-to-html": "HTML'ye göm", + "embed-to-html-hint": "Bu özellik bağlantıyı yetkisiz tüm kullanıcılar için erişilebilir yapacaktır.", + "embed-to-html-text": "Aşağıdaki kod parçacığını kullanarak görseli düz HTML tabanlı bileşenlere gömebilirsiniz.
    Bu bileşenler HTML kart widget'ları, hücre içerik fonksiyonları vb. içerir.", + "embed-to-angular-template": "Angular HTML şablonuna göm", + "embed-to-angular-template-text": "Aşağıdaki kod parçacığını kullanarak görseli Angular HTML şablonuna gömebilirsiniz.
    Bu bileşenler arasında Markdown widget'ı, widget düzenleyicisindeki HTML bölümü, özel eylemler vb. bulunur." + }, + "image-input": { + "drop-images-or": "Bir veya birden fazla görseli sürükleyip bırakın ya da", + "drag-and-drop": "Sürükle & Bırak", + "or": "veya", + "browse": "Gözat", + "no-images": "Görsel seçilmedi", + "images": "görseller" + }, "import": { - "no-file": "Hiçbir dosya seçilmedi", - "drop-file": "Bir JSON dosyası bırakın veya yüklenecek bir dosyayı seçmek için tıklayın.", - "drop-file-csv": "Bir CSV dosyası bırakın veya yüklenecek dosyayı seçmek için tıklayın.", + "no-file": "Dosya seçilmedi", + "drop-file": "Bir JSON dosyasını bırakın veya yüklemek için tıklayın.", + "drop-json-file-or": "Bir JSON dosyasını sürükleyip bırakın ya da", + "drop-file-csv": "Bir CSV dosyasını bırakın veya yüklemek için tıklayın.", + "drop-file-csv-or": "Bir CSV dosyasını sürükleyip bırakın ya da", "column-value": "Değer", "column-title": "Başlık", - "column-example": "Örnek değer verileri", - "column-key": "Öznitelik/telemetri anahtarı", - "csv-delimiter": "CSV sınırlayıcı", + "column-example": "Örnek değer verisi", + "column-key": "Özellik/zaman serisi anahtarı", + "credentials": "Kimlik bilgileri", + "csv-delimiter": "CSV ayırıcı", "csv-first-line-header": "İlk satır sütun adlarını içerir", - "csv-update-data": "Öznitelikleri/telemetriyi güncelle", - "import-csv-number-columns-error": "Bir dosya en az iki sütun içermelidir", + "csv-update-data": "Özellikleri/zaman serilerini güncelle", + "details": "Detaylar", + "import-csv-number-columns-error": "Dosya en az iki sütun içermelidir", "import-csv-invalid-format-error": "Geçersiz dosya formatı. Satır: '{{line}}'", "column-type": { - "name": "İsim", + "name": "Ad", "type": "Tür", "label": "Etiket", "column-type": "Sütun türü", - "client-attribute": "İstemci öznitelik", - "shared-attribute": "Paylaşılan öznitelik", - "server-attribute": "Sunucu öznitelik", + "client-attribute": "İstemci özelliği", + "shared-attribute": "Paylaşılan özellik", + "server-attribute": "Sunucu özelliği", "timeseries": "Zaman serisi", - "entity-field": "Öğe alanı", - "access-token": "Access token", - "isgateway": "Ağ Geçidi", - "activity-time-from-gateway-device": "Ağ geçidi cihazından etkinlik süresi", + "entity-field": "Varlık alanı", + "access-token": "Erişim anahtarı", + "x509": "X.509", + "mqtt": { + "client-id": "MQTT istemci kimliği", + "user-name": "MQTT kullanıcı adı", + "password": "MQTT şifresi" + }, + "lwm2m": { + "client-endpoint": "LwM2M uç nokta istemci adı", + "security-config-mode": "LwM2M güvenlik yapılandırma modu", + "client-identity": "LwM2M istemci kimliği", + "client-key": "LwM2M istemci anahtarı", + "client-cert": "LwM2M istemci genel anahtarı", + "bootstrap-server-security-mode": "LwM2M bootstrap sunucusu güvenlik modu", + "bootstrap-server-secret-key": "LwM2M bootstrap sunucusu gizli anahtarı", + "bootstrap-server-public-key-id": "LwM2M bootstrap sunucusu genel anahtarı veya kimliği", + "lwm2m-server-security-mode": "LwM2M sunucusu güvenlik modu", + "lwm2m-server-secret-key": "LwM2M sunucusu gizli anahtarı", + "lwm2m-server-public-key-id": "LwM2M sunucusu genel anahtarı veya kimliği" + }, + "snmp": { + "host": "SNMP sunucusu", + "port": "SNMP portu", + "version": "SNMP sürümü (v1, v2c veya v3)", + "community-string": "SNMP topluluk dizesi" + }, + "isgateway": "Ağ Geçidi mi", + "activity-time-from-gateway-device": "Ağ geçidi cihazından etkinlik zamanı", "description": "Açıklama", - "routing-key": "Uç Anahtarı", - "secret": "Uç Secret" + "routing-key": "Edge anahtarı", + "secret": "Edge gizli anahtarı" }, "stepper-text": { "select-file": "Bir dosya seçin", - "configuration": "Yapılandırmayı içe aktar", - "column-type": "Sütun türünü seçin", - "creat-entities": "Yeni varlıklar oluşturma" + "configuration": "İçe aktarma yapılandırması", + "column-type": "Sütun türlerini seçin", + "creat-entities": "Yeni varlıklar oluşturuluyor" }, "message": { - "create-entities": "{{count}} yeni öğe başarıyla oluşturuldu.", - "update-entities": "{{count}} öğe başarıyla güncellendi.", - "error-entities": "{{count}} öğe oluşturulurken bir hata oluştu." + "create-entities": "{{count}} yeni varlık başarıyla oluşturuldu.", + "update-entities": "{{count}} varlık başarıyla güncellendi.", + "error-entities": "{{count}} varlık oluşturulurken hata oluştu." + } + }, + "scada": { + "symbols": "SCADA sembolleri", + "search": "Sembol ara", + "selected-symbols": "{ count, plural, =1 {1 sembol} other {# sembol} } seçildi", + "download-symbol": "SCADA sembolünü indir", + "export-symbol": "SCADA sembolünü JSON olarak dışa aktar", + "import-symbol": "SCADA sembolünü JSON'dan içe aktar", + "upload-symbol": "SCADA sembolü yükle", + "update-symbol": "SCADA sembolünü güncelle", + "edit-symbol": "SCADA sembolünü düzenle", + "symbol-details": "SCADA sembol detayları", + "mode-svg": "SVG", + "mode-xml": "XML", + "no-symbols": "Sembol bulunamadı", + "show-hidden-elements": "Gizli öğeleri göster", + "hide-hidden-elements": "Gizli öğeleri gizle", + "delete-symbol": "SCADA sembolünü sil", + "delete-symbol-title": "SCADA sembolü '{{imageTitle}}' silinsin mi?", + "delete-symbol-text": "Dikkatli olun, onaydan sonra SCADA sembolü kurtarılamaz hale gelecek.", + "delete-symbols-title": "{ count, plural, =1 {1 SCADA sembolü} other {# SCADA sembolü} } silinsin mi?", + "delete-symbols-text": "Dikkatli olun, onaydan sonra seçilen tüm SCADA sembolleri ve ilgili veriler geri alınamaz şekilde silinecek.", + "include-system-symbols": "Sistem sembollerini dahil et", + "symbol-preview": "Sembol önizleme", + "general": "Genel", + "tags": "Etiketler", + "properties": "Özellikler", + "title": "Başlık", + "description": "Açıklama", + "search-tags": "Etiket ara", + "widget-size": "Bileşen boyutu", + "cols": "sütun", + "rows": "satır", + "state-render-function": "Durum render fonksiyonu", + "preview": "Önizleme", + "preview-widget-action-text": "Bileşen eylemi '{{type}}' başarıyla çalıştırıldı!", + "no-symbol": "SCADA sembolü yok", + "no-symbol-selected": "SCADA sembolü seçilmedi", + "clear-symbol": "SCADA sembolünü temizle", + "browse-symbol-from-gallery": "Galeri üzerinden SCADA sembolü seç", + "zoom-in": "Yakınlaştır", + "zoom-out": "Uzaklaştır", + "create-widget": "Bileşen oluştur", + "create-widget-from-symbol": "SCADA sembolünden bileşen oluştur", + "hidden": "gizli", + "tag": { + "tag": "Etiket", + "on-click-action": "Tıklama eylemi", + "no-tags": "Etiket yapılandırılmadı", + "delete-tag-text": "{{elementType}} öğesinden
    {{tag}} etiketini silmek istediğinizden emin misiniz?", + "update-tag": "Etiketi güncelle", + "enter-tag": "Etiket gir", + "tag-settings": "Etiket ayarları", + "remove-tag": "Etiketi kaldır", + "add-tag": "Etiket ekle" + }, + "behavior": { + "behavior": "Davranış", + "id": "Id", + "name": "Ad", + "type": "Tür", + "no-behaviors": "Tanımlı davranış yok", + "add-behavior": "Davranış ekle", + "type-action": "Eylem", + "type-value": "Değer", + "type-widget-action": "Bileşen eylemi", + "behavior-settings": "Davranış ayarları", + "remove-behavior": "Davranışı kaldır", + "hint": "İpucu", + "group-title": "Grup başlığı", + "value-type": "Değer türü", + "default-value": "Varsayılan değer", + "true-label": "Doğru etiketi", + "false-label": "Yanlış etiketi", + "state-label": "Durum etiketi", + "default-payload": "Varsayılan yük", + "not-unique-behavior-ids-error": "Davranış Kimlikleri benzersiz olmalıdır!", + "default-settings": "Varsayılan ayarlar" + }, + "symbol": { + "symbol": "SCADA sembolü", + "fluid-presence": "Akışkan varlığı", + "fluid-presence-hint": "Boru içinde akışkan olup olmadığını belirtir.", + "fluid-present": "Akışkan var", + "present": "Var", + "absent": "Yok", + "flow-presence": "Akış varlığı", + "flow-presence-hint": "Boru içinde akışkan akışı olup olmadığını belirtir.", + "flow-present": "Akış mevcut", + "flow-direction": "Akış yönü", + "flow-direction-hint": "Akışkanın akış yönünü belirtir.", + "forward": "İleri", + "reverse": "Geri", + "flow-animation-speed": "Akış animasyon hızı", + "flow-animation-speed-hint": "Akış animasyon hızını gösteren ondalık değer. 1 - normal hız, 0 - animasyon yok, < 1 - daha yavaş animasyon, > 1 - daha hızlı animasyon.", + "leak": "Sızıntı", + "leak-hint": "Boru içinde sızıntı olup olmadığını belirtir.", + "leak-present": "Sızıntı var", + "fluid-color": "Akışkan rengi", + "pipe-color": "Boru rengi", + "horizontal-pipe": "Yatay boru", + "vertical-pipe": "Dikey boru", + "horizontal-fluid-color": "Yatay akışkan rengi", + "vertical-fluid-color": "Dikey akışkan rengi", + "left-pipe": "Sol boru", + "right-pipe": "Sağ boru", + "top-pipe": "Üst boru", + "bottom-pipe": "Alt boru", + "left-fluid-color": "Sol akışkan rengi", + "right-fluid-color": "Sağ akışkan rengi", + "top-fluid-color": "Üst akışkan rengi", + "bottom-fluid-color": "Alt akışkan rengi", + "display": "Görüntüle", + "display-format": "Görüntü formatı", + "value": "Değer", + "decimals": "Ondalık basamak", + "units": "Birimler", + "flow-meter-value-hint": "Debimetre ekranında gösterilen ondalık değer", + "value-hint": "Geçerli değeri gösteren ondalık değer", + "running": "Çalışıyor", + "running-hint": "Bileşenin çalışır durumda olup olmadığını belirtir.", + "warning-state": "Uyarı durumu", + "warning": "Uyarı", + "warning-click": "Uyarı tıklaması", + "warning-state-hint": "Bileşenin uyarı durumunda olup olmadığını belirtir.", + "critical-state": "Kritik durum", + "critical": "Kritik", + "critical-click": "Kritik tıklama", + "critical-state-hint": "Bileşenin kritik durumda olup olmadığını belirtir.", + "critical-state-animation": "Kritik durum animasyonu", + "critical-state-animation-hint": "Bileşen kritik durumda olduğunda yanıp sönme animasyonunun etkinleştirilip etkinleştirilmeyeceği.", + "warning-critical-state-animation": "Uyarı/Kritik durum animasyonu", + "warning-critical-state-animation-hint": "Bileşen uyarı veya kritik durumdayken yanıp sönme animasyonunun etkinleştirilip etkinleştirilmeyeceği.", + "animation": "Animasyon", + "broken": "Bozuk", + "broken-hint": "Bileşenin bozuk olup olmadığını belirtir.", + "on-display-click": "Ekran tıklaması", + "on-display-click-hint": "Kullanıcı ekranı tıkladığında tetiklenen eylem.", + "pipe": "Boru", + "default-border-color": "Varsayılan kenarlık rengi", + "active-border-color": "Aktif kenarlık rengi", + "warning-border-color": "Uyarı kenarlık rengi", + "critical-border-color": "Kritik kenarlık rengi", + "background-color": "Arka plan rengi", + "rotation-animation-speed": "Dönme animasyon hızı", + "rotation-animation-speed-hint": "Dönme animasyon hızını gösteren ondalık değer. 1 - normal hız, 0 - animasyon yok, < 1 - daha yavaş, > 1 - daha hızlı.", + "on-click": "Tıklama", + "on-click-hint": "Kullanıcı bileşeni tıkladığında tetiklenen eylem.", + "connectors-positions": "Bağlantı konumları", + "right-connector": "Sağ bağlantı", + "right-top-connector": "Sağ üst bağlantı", + "right-bottom-connector": "Sağ alt bağlantı", + "left-connector": "Sol bağlantı", + "left-top-connector": "Sol üst bağlantı", + "left-bottom-connector": "Sol alt bağlantı", + "top-left-connector": "Üst sol bağlantı", + "top-right-connector": "Üst sağ bağlantı", + "top-connector": "Üst bağlantı", + "bottom-connector": "Alt bağlantı", + "running-color": "Çalışma rengi", + "stopped-color": "Durma rengi", + "stopped": "Durdu", + "warning-color": "Uyarı rengi", + "critical-color": "Kritik rengi", + "opened": "Açık", + "opened-hint": "Bileşenin açık durumda olup olmadığını belirtir.", + "open": "Aç", + "open-hint": "Kullanıcı bileşeni açmak için tıkladığında tetiklenen eylem.", + "close": "Kapat", + "close-hint": "Kullanıcı bileşeni kapatmak için tıkladığında tetiklenen eylem.", + "close-state-animation": "Kapalı durum animasyonu", + "close-state-animation-hint": "Bileşen kapalı durumdayken yanıp sönme animasyonunun etkinleştirilip etkinleştirilmeyeceği.", + "opened-color": "Açık renk", + "closed-color": "Kapalı renk", + "opened-rotation-angle": "Açık konum dönme açısı", + "closed-rotation-angle": "Kapalı konum dönme açısı", + "tank-capacity": "Tank kapasitesi", + "tank-capacity-hint": "Toplam tank kapasitesini gösteren ondalık değer.", + "current-volume": "Mevcut hacim", + "current-volume-hint": "Mevcut doluluk hacmini gösteren ondalık değer.", + "tank-color": "Tank rengi", + "value-box": "Değer kutusu", + "value-text": "Değer metni", + "scale": "Ölçek", + "transparent-mode": "Şeffaf mod", + "major-ticks": "Büyük işaretler", + "intervals": "Aralıklar", + "major-ticks-color": "Büyük işaret rengi", + "normal": "Normal", + "minor-ticks": "Küçük işaretler", + "minor-ticks-color": "Küçük işaret rengi", + "temperature": "Sıcaklık", + "temperature-hint": "Geçerli sıcaklığı gösteren ondalık değer.", + "update-temperature": "Sıcaklığı güncelle", + "update-temperature-hint": "Kullanıcı sıcaklığı değiştirmek için tıkladığında tetiklenen eylem.", + "run": "Çalıştır", + "run-hint": "Kullanıcı bileşeni çalıştırmak için tıkladığında tetiklenen eylem.", + "stop": "Durdur", + "stop-hint": "Kullanıcı bileşeni durdurmak için tıkladığında tetiklenen eylem.", + "temperature-step": "Sıcaklık adım artışı", + "heat-pump-color": "Isı pompası rengi", + "power-button-background": "Güç düğmesi arka planı", + "value-box-background": "Değer kutusu arka planı", + "value-units": "Değer birimleri", + "enable-units-scale": "Ölçekte birimleri etkinleştir", + "filtration-mode": "Filtrasyon modu", + "filtration-mode-hint": "Geçerli filtrasyon modunu gösteren tam sayı değeri.", + "filtration-mode-update": "Filtrasyon modu güncelleme durumu", + "filtration-mode-update-hint": "Kullanıcı geçerli filtrasyon modunu değiştirmek için tıkladığında tetiklenen eylem.", + "filter-mode": "Filtre", + "waste-mode": "Atık", + "backwash-mode": "Ters yıkama", + "recirculate-mode": "Dolaşım", + "rinse-mode": "Durulama", + "closed-mode": "Kapalı", + "sand-filter-color": "Kum filtresi rengi", + "mode-box-background": "Mod kutusu arka planı", + "border-color": "Kenarlık rengi", + "label-color": "Etiket rengi", + "water-leak-hint": "Sızıntı olup olmadığını belirtir.", + "default-color": "Varsayılan renk", + "leak-color": "Sızıntı rengi", + "full-value": "Tam değer", + "full-value-hint": "Tam değeri gösteren ondalık sayı.", + "label": "Etiket", + "icon": "Simge", + "button-color": "Düğme rengi", + "on-label": "'Açık' etiketi metni", + "off-label": "'Kapalı' etiketi metni", + "arrow-presence": "Ok varlığı", + "arrow-presence-hint": "Bağlayıcıda okun olup olmadığını belirtir.", + "arrow-present": "Ok mevcut", + "arrow-direction": "Akış yönü", + "arrow-direction-hint": "Akış yönünü belirtir.", + "flow-animation": "Akış varlığı", + "flow-animation-hint": "Bağlayıcıda sıvı akışının olup olmadığını belirtir.", + "flow": "Akış", + "flow-line": "Çizgi", + "flow-line-style": "Çizgi stili", + "flow-style-hint": "Animasyonun senkronizasyonu için Dash ve Gap değerlerinin toplamı 100'e tam bölünebilir olmalıdır.", + "flow-dash-cap": "Çizgi ucu", + "dash-cap-butt": "Düz", + "dash-cap-round": "Yuvarlak", + "dash-cap-square": "Kare", + "dash": "Çizgi", + "gap": "Boşluk", + "main-line": "Ana çizgi", + "line": "Çizgi", + "line-color": "Çizgi rengi", + "arrow-color": "Ok rengi", + "target-value": "Hedef değer", + "target-value-hint": "Ölçekteki hedef noktayı belirtir.", + "min-max-value": "Min ve maks değer", + "min-value": "Min", + "max-value": "Maks", + "progress-bar": "İlerleme çubuğu", + "progress-arrow": "İlerleme oku", + "warning-scale-color": "Uyarı ölçek rengi", + "critical-scale-color": "Kritik ölçek rengi", + "scale-color": "Ölçek rengi", + "target": "Hedef", + "high-warning-state": "Yüksek uyarı durumu", + "show-high-warning-scale": "Yüksek uyarı ölçeğini göster", + "high-warning-scale": "Yüksek uyarı ölçeği", + "high-warning-state-hint": "Ondalık değer, yüksek uyarı aralığını yüksek kritik veya maks değere kadar belirtir.", + "low-warning-state": "Düşük uyarı durumu", + "show-low-warning-scale": "Düşük uyarı ölçeğini göster", + "low-warning-scale": "Düşük uyarı ölçeği", + "low-warning-state-hint": "Ondalık değer, düşük uyarı aralığını düşük kritik veya min değere kadar belirtir.", + "high-critical-state": "Yüksek kritik durumu", + "show-high-critical-scale": "Yüksek kritik ölçeğini göster", + "high-critical-scale": "Yüksek kritik ölçeği", + "high-critical-state-hint": "Ondalık değer, yüksek kritik aralığını maks ölçek değerine kadar belirtir.", + "low-critical-state": "Düşük kritik durumu", + "show-low-critical-scale": "Düşük kritik ölçeği göster", + "low-critical-scale": "Düşük kritik ölçek", + "low-critical-state-hint": "Ondalık değer, düşük kritik aralığını min ölçek değerine kadar belirtir.", + "filter-color": "Filtre rengi", + "colors": "Renkler", + "indicator-colors": "Gösterge renkleri", + "enabled": "Etkin", + "disabled": "Devre dışı", + "on": "AÇIK", + "off": "KAPALI", + "on-off-state": "Açık/Kapalı durumu", + "on-off-state-hint": "Bileşenin Açık veya Kapalı durumda olup olmadığını belirtir.", + "on-update-state": "Durumu Açık olarak güncelle", + "on-update-state-hint": "Kullanıcı durumu Açık olarak güncellemek için tıkladığında tetiklenen eylem.", + "off-update-state": "Durumu Kapalı olarak güncelle", + "off-update-state-hint": "Kullanıcı durumu Kapalı olarak güncellemek için tıkladığında tetiklenen eylem.", + "voltage": "Voltaj", + "input-voltage": "Giriş voltajı", + "input-voltage-hint": "Ondalık değer giriş voltajını belirtir.", + "output-voltage": "Çıkış voltajı", + "output-voltage-hint": "Ondalık değer çıkış voltajını belirtir.", + "first-phase-voltage": "Birinci faz voltajı", + "second-phase-voltage": "İkinci faz voltajı", + "third-phase-voltage": "Üçüncü faz voltajı", + "phase-voltage-hint": "Ondalık değer, mevcut faz için voltajı belirtir.", + "voltage-hint": "Ondalık değer mevcut voltajı belirtir.", + "current-voltage-color": "Mevcut voltaj rengi", + "phase-indicator-color": "Faz gösterge rengi", + "measured": "Ölçülen", + "measured-hint": "Ondalık değer kilovat-saat cinsinden enerji kullanımını belirtir.", + "day-rate": "Gündüz tarifesi", + "night-rate": "Gece tarifesi", + "off-peak-rate": "Yoğun olmayan zaman tarifesi", + "peak-rate": "Yoğun zaman tarifesi", + "export-rate": "İhracat tarifesi", + "operating-mode": "Çalışma modu", + "bypass-mode": "Baypas", + "operating-mode-hint": "Tamsayı değeri geçerli çalışma modunu belirtir (0 - KAPALI, 1 - AÇIK, 2 - BAYPAS)", + "connected": "Bağlı", + "connected-hint": "Bileşenin bağlı durumda olup olmadığını belirtir.", + "disconnected": "Bağlı değil", + "indicator": "Gösterge", + "operation-mode": "Çalışma modu", + "operation-mode-hint": "İnvertörün Şebeke veya İnvertör modunda olup olmadığını belirtir.", + "operation-mode-indicators-color": "Çalışma modu gösterge rengi", + "mains-on-mode": "Şebeke açık", + "inverter-on-mode": "İnvertör açık", + "charging-mode": "Şarj modu", + "charging-mode-hint": "Tamsayı değeri mevcut şarj modunu belirtir (1 - Hızlı, 2 - Emme, 3 - Yüzen)", + "charging-mode-indicators-color": "Şarj modu gösterge rengi", + "inverter-faults": "Hatalar", + "inverter-fault-indicators-color": "Hata gösterge rengi", + "overload-fault": "Aşırı yük", + "overload-fault-hint": "İnvertör aşırı yük durumundaysa belirtir.", + "low-battery-fault": "Düşük pil", + "low-battery-fault-hint": "Pilin aşırı şekilde boşalmış olup olmadığını belirtir.", + "temperature-fault": "Sıcaklık", + "temperature-fault-hint": "İnvertörde yüksek sıcaklık olup olmadığını belirtir.", + "triangle": "Üçgen", + "socket": "Priz", + "left-button": "Sol düğme", + "right-button": "Sağ düğme", + "alarm-colors": "Alarm renkleri", + "hook-color": "Kanca rengi" } }, "item": { - "selected": "Seçildi" + "selected": "Seçili" }, "js-func": { "no-return-error": "Fonksiyon bir değer döndürmelidir!", - "return-type-mismatch": "Fonksiyon, '{{type}}' türünde bir değer döndürmelidir!", - "tidy": "Tidy", - "mini": "Mini" + "return-type-mismatch": "Fonksiyon '{{type}}' türünde bir değer döndürmelidir!", + "tidy": "Düzenle", + "mini": "Mini", + "modules": "Modüller", + "remove-module": "Modülü kaldır", + "no-modules": "Hiçbir modül yapılandırılmadı", + "add-module": "Modül ekle", + "module-alias": "Takma ad", + "invalid-module-alias-name": "Geçersiz takma ad adı", + "module-resource": "JS modül kaynağı", + "not-unique-module-aliases-error": "Modül takma adları benzersiz olmalıdır!", + "show-module-info": "Modül bilgisini göster", + "show-module-source-code": "Modül kaynak kodunu göster", + "module-members": "Modül üyeleri", + "module-no-members": "Modülün dışa aktarılmış üyesi yok", + "module-load-error": "Modül yükleme hatası", + "source-code": "Kaynak kodu", + "source-code-load-error": "Kaynak kodu yüklenemedi", + "no-js-module-text": "Hiçbir JS modülü bulunamadı", + "no-js-module-matching": "'{{module}}' ile eşleşen JS modülü bulunamadı." }, "key-val": { "key": "Anahtar", "value": "Değer", - "remove-entry": "Kaydı kaldır", - "add-entry": "Kayıt ekle", - "no-data": "Kayıt yok" + "remove-entry": "Girdiyi kaldır", + "add-entry": "Girdi ekle", + "no-data": "Girdi yok" }, "layout": { - "layout": "Arayüz Düzeni", - "manage": "Arayüz düzenini yönet", - "settings": "Arayüz düzeni ayarları", + "layout": "Yerleşim", + "layouts": "Yerleşimler", + "manage": "Yerleşimleri yönet", + "settings": "Yerleşim ayarları", "color": "Renk", "main": "Ana", "right": "Sağ", - "select": "Hedef düzen seç" + "left": "Sol", + "select": "Hedef yerleşimi seç", + "percentage-width": "Yüzdelik genişlik (%)", + "fixed-width": "Sabit genişlik (px)", + "left-width": "Sol sütun (%)", + "right-width": "Sağ sütun (%)", + "pick-fixed-side": "Sabit taraf: ", + "layout-fixed-width": "Sabit genişlik (px)", + "value-min-error": "Değer {{min}}{{unit}} değerinden büyük olmalıdır", + "value-max-error": "Değer {{max}}{{unit}} değerinden küçük olmalıdır", + "layout-fixed-width-required": "Sabit genişlik gereklidir", + "right-width-percentage-required": "Sağ yüzdelik oran gereklidir", + "left-width-percentage-required": "Sol yüzdelik oran gereklidir", + "divider": "Bölücü", + "right-side": "Sağ taraf yerleşimi", + "left-side": "Sol taraf yerleşimi", + "add-new-breakpoint": "Yeni kırılma noktası ekle", + "breakpoint": "Kırılma noktası", + "breakpoints": "Kırılma noktaları", + "copy-from": "Kopyala", + "size": "Boyut", + "delete-breakpoint-title": "Kırılma noktası '{{name}}' silinsin mi?", + "delete-breakpoint-text": "Lütfen unutmayın, onaydan sonra kırılma noktası geri alınamaz hale gelecektir ve ayarlar varsayılan kırılma noktasına dönecektir." }, "legend": { - "direction": "Lejant yönü", - "position": "Lejant konumu", - "sort-legend": "Veri anahtarlarını lejantta sıralayın", + "direction": "Yön", + "position": "Konum", + "show-values": "Değerleri göster", + "min-option": "Min", + "max-option": "Maks", + "average-option": "Ortalama", + "total-option": "Toplam", + "latest-option": "Son", + "sort-legend": "Açıklamada veri anahtarlarını sırala", "show-max": "Maksimum değeri göster", "show-min": "Minimum değeri göster", "show-avg": "Ortalama değeri göster", "show-total": "Toplam değeri göster", - "settings": "Lejant ayarları", + "show-latest": "Son değeri göster", + "settings": "Açıklama ayarları", "min": "min", - "max": "max", + "max": "maks", "avg": "ort", "total": "toplam", + "latest": "son", + "Min": "Min", + "Max": "Maks", + "Avg": "Ort", + "Total": "Toplam", + "Latest": "Son", "comparison-time-ago": { "previousInterval": "(önceki aralık)", + "customInterval": "(özel aralık)", "days": "(gün önce)", "weeks": "(hafta önce)", "months": "(ay önce)", "years": "(yıl önce)" - } + }, + "column-title": "Sütun başlığı", + "label": "Etiket", + "value": "Değer" }, "login": { - "login": "Giriş Yap", - "request-password-reset": "Parola Sıfırlama İsteği Gönder", - "reset-password": "Parola Sıfırla", - "create-password": "Parola Oluştur", - "passwords-mismatch-error": "Girilen parolalar eşleşmeli!", - "password-again": "Parola tekrarı", - "sign-in": "Lütfen girişi yapın", + "login": "Giriş", + "request-password-reset": "Şifre Sıfırlama Talebi", + "reset-password": "Şifreyi Sıfırla", + "create-password": "Şifre Oluştur", + "two-factor-authentication": "İki adımlı doğrulama", + "passwords-mismatch-error": "Girilen şifreler aynı olmalıdır!", + "password-again": "Şifre tekrar", + "sign-in": "Lütfen giriş yapın", "username": "Kullanıcı adı (e-posta)", "remember-me": "Beni hatırla", - "forgot-password": "Parolamı unuttum", - "password-reset": "Parola sıfırla", - "expired-password-reset-message": "Kimlik bilgilerinizin süresi doldu! Lütfen yeni şifre oluşturun.", - "new-password": "Yeni parola", - "new-password-again": "Yeni parola tekrarı", - "password-link-sent-message": "Parola sıfırlama e-postası başarıyla gönderildi!", + "forgot-password": "Şifrenizi mi unuttunuz?", + "password-reset": "Şifre sıfırlama", + "expired-password-reset-message": "Kimlik bilgilerinizin süresi doldu! Lütfen yeni bir şifre oluşturun.", + "new-password": "Yeni şifre", + "new-password-again": "Yeni şifreyi onayla", + "password-link-sent-message": "Sıfırlama bağlantısı gönderildi", "email": "E-posta", - "login-with": "{{name}} ile Giriş Yap", - "or": "ya da", - "error": "Giriş hatası" + "invalid-email-format": "Geçersiz e-posta formatı.", + "login-with": "{{name}} ile giriş yap", + "or": "veya", + "error": "Giriş hatası", + "verify-your-identity": "Kimliğinizi doğrulayın", + "select-way-to-verify": "Doğrulama yöntemi seçin", + "resend-code": "Kodu yeniden gönder", + "resend-code-wait": "Kodu yeniden gönderme süresi: { time, plural, =1 {1 saniye} other {# saniye} }", + "try-another-way": "Başka bir yol deneyin", + "totp-auth-description": "Lütfen kimlik doğrulayıcı uygulamanızdaki güvenlik kodunu girin.", + "totp-auth-placeholder": "Kod", + "sms-auth-description": "Telefonunuza {{contact}} numarasına bir güvenlik kodu gönderildi.", + "sms-auth-placeholder": "SMS kodu", + "email-auth-description": "E-posta adresinize {{contact}} adresine bir güvenlik kodu gönderildi.", + "email-auth-placeholder": "E-posta kodu", + "backup-code-auth-description": "Lütfen yedek kodlarınızdan birini girin.", + "backup-code-auth-placeholder": "Yedek kod", + "activation-link-expired": "Aktivasyon bağlantısının süresi doldu", + "activation-link-expired-message": "Profilinizi aktifleştirmek için gönderilen bağlantının süresi doldu. Yeni bir e-posta almak için giriş sayfasına dönebilirsiniz.", + "reset-password-link-expired": "Şifre sıfırlama bağlantısının süresi doldu", + "reset-password-link-expired-message": "Şifre sıfırlama bağlantısının süresi doldu. Yeni bir e-posta almak için giriş sayfasına dönebilirsiniz." + }, + "mobile": { + "add-application": "Uygulama ekle", + "app-id": "Uygulama Kimliği", + "app-id-required": "Uygulama Kimliği gereklidir", + "app-id-pattern": "Geçersiz Uygulama Kimliği formatı", + "app-store-link": "App Store bağlantısı", + "app-store-link-required": "App Store bağlantısı gereklidir", + "application-details": "Uygulama detayları", + "application-package": "Uygulama paketi", + "application-secret": "Uygulama gizli anahtarı", + "application-secret-required": "Uygulama gizli anahtarı gereklidir", + "application": "Uygulama", + "applications": "Uygulamalar", + "copy-app-id": "Uygulama Kimliğini kopyala", + "copy-app-store-link": "App Store bağlantısını kopyala", + "copy-application-package": "Uygulama paketini kopyala", + "copy-application-secret": "Uygulama gizli anahtarını kopyala", + "copy-google-play-link": "Google Play bağlantısını kopyala", + "copy-sha256-certificate-fingerprints": "SHA256 sertifika parmak izini kopyala", + "delete-application": "Uygulamayı sil", + "delete-application-button-text": "Sonuçlarını anlıyorum, uygulamayı sil", + "delete-application-text": "Bu işlem geri alınamaz. Bu, uygulamanızı kalıcı olarak silecektir.
    Eğer kalıcı olarak silmek istemiyorsanız uygulamayı geçici olarak askıya alabilirsiniz.
    Silmek için lütfen onaylamak için \"{{phrase}}\" yazın.", + "delete-application-title-short": "‘{{name}}’ adlı uygulamayı silmek istediğinize emin misiniz?", + "delete-application-text-short": "Dikkatli olun, onaydan sonra uygulama ve tüm ilgili veriler geri alınamaz hale gelecektir.", + "delete-application-phrase": "uygulamayı sil", + "delete-applications-bundle-text": "Dikkatli olun, onaydan sonra mobil paket ve tüm ilgili veriler geri alınamaz hale gelecektir.", + "delete-applications-bundle-title": "‘{{bundleName}}’ adlı mobil paketi silmek istediğinize emin misiniz?", + "generate-application-secret": "Uygulama gizli anahtarı oluştur", + "google-play-link": "Google Play bağlantısı", + "google-play-link-required": "Google Play bağlantısı gereklidir", + "latest-version": "En son sürüm", + "min-version": "Minimum sürüm", + "invalid-version-pattern": "Geçersiz sürüm formatı. Lütfen şu formatı kullanın: major.minor.patch (örn., 1.0.0).", + "mobile-center": "Mobil merkezi", + "mobile-package": "Uygulama paketi", + "mobile-package-max-length": "Uygulama paketi 256 karakterden kısa olmalıdır", + "mobile-package-required": "Uygulama paketi gereklidir.", + "mobile-package-pattern": "Geçersiz uygulama paketi formatı", + "mobile-package-title": "Uygulama başlığı", + "mobile-package-title-max-length": "Uygulama başlığı 256 karakterden kısa olmalıdır", + "no-application": "Hiç uygulama bulunamadı", + "no-bundles": "Hiç paket bulunamadı", + "platform-type": "Platform türü", + "search-application": "Uygulama ara", + "search-bundles": "Paketleri ara", + "set": "Ayarla", + "sha256-certificate-fingerprints": "SHA256 sertifika parmak izi", + "sha256-certificate-fingerprints-required": "SHA256 sertifika parmak izi gereklidir", + "sha256-certificate-fingerprints-pattern": "Geçersiz SHA256 sertifika parmak izi formatı", + "show-hidden-pages": "Gizli sayfaları göster", + "status": "Durum", + "status-type": { + "deprecated": "Kullanımdan kaldırıldı", + "draft": "Taslak", + "published": "Yayınlandı", + "suspended": "Askıya alındı" + }, + "store-information": "Mağaza bilgisi", + "version-information": "Sürüm bilgisi", + "min-version-release-notes": "Minimum sürüm sürüm notları", + "latest-version-release-notes": "En son sürüm sürüm notları", + "bundle": "Paket", + "bundles": "Paketler", + "add-bundle": "Paket ekle", + "title": "Başlık", + "title-required": "Başlık gereklidir", + "title-cannot-contain-only-spaces": "Başlık yalnızca boşluk içeremez", + "title-max-length": "Başlık 256 karakterden az olmalıdır", + "oauth-clients": "OAuth 2.0 istemcileri", + "android-app": "Android Uygulaması", + "android-application": "Android Uygulaması", + "ios-app": "iOS Uygulaması", + "ios-application": "iOS Uygulaması", + "invalid-store-link": "Geçersiz mağaza bağlantısı", + "enable-oauth": "OAuth 2.0'ı etkinleştir", + "enable-self-registration": "Kendi kendine kaydı etkinleştir", + "edit-bundle": "Paketi düzenle", + "description": "Açıklama", + "basic-settings": "Temel ayarlar", + "no-application-matching": "'{{entity}}' ile eşleşen uygulama bulunamadı.", + "no-bundle-matching": "'{{entity}}' ile eşleşen paket bulunamadı.", + "application-required": "Uygulama gereklidir.", + "bundle-required": "Paket gereklidir.", + "no-application-text": "Hiç uygulama bulunamadı", + "no-bundle-text": "Hiç paket bulunamadı", + "layout": "Yerleşim", + "pages": "Sayfalar", + "hide-all-pages": "Tüm sayfaları gizle", + "reset-to-default-pages": "Sayfaları varsayılana sıfırla", + "add-specific-page": "Belirli bir sayfa ekle", + "visible": "Görünür", + "hidden": "Gizli", + "reset-to-page-default": "Sayfayı varsayılan haline sıfırla", + "mobile-599": "Mobil (maks. 599px)", + "tablet-959": "Tablet (maks. 959px)", + "max-element-number": "Maksimum öğe sayısı", + "page-name": "Sayfa adı", + "page-name-required": "Sayfa adı gereklidir.", + "page-name-cannot-contain-only-spaces": "Sayfa adı yalnızca boşluk içeremez.", + "page-name-max-length": "Sayfa adı 256 karakterden kısa olmalıdır", + "page-type": "Sayfa türü", + "pages-types": { + "dashboard": "Kontrol paneli", + "web-view": "Web görünümü", + "custom": "Özel" + }, + "url": "URL", + "invalid-url-format": "Geçersiz URL formatı", + "path": "Yol", + "invalid-path-format": "Geçersiz yol formatı", + "custom-page": "Özel sayfa", + "edit-page": "Sayfayı düzenle", + "edit-custom-page": "Özel sayfayı düzenle", + "delete-page": "Sayfayı sil", + "qr-code-widget": "QR kod bileşeni", + "type-here": "Buraya yazın", + "configuration-dialog": "Yapılandırma penceresi", + "configuration-app": "Yapılandırma uygulaması", + "configuration-step": { + "prepare-environment-title": "Geliştirme ortamını hazırla", + "prepare-environment-text": "Flutter ThingsBoard Mobil Uygulaması, Flutter SDK gerektirir. Flutter SDK'yı kurmak için talimatları izleyin.", + "get-source-code-title": "Uygulama kaynak kodunu edinin", + "get-source-code-text": "Flutter ThingsBoard Mobil Uygulaması'nın kaynak kodunu GitHub deposundan klonlayarak edinebilirsiniz:", + "configure-app-settings-title": "Uygulama ayarlarını yapılandır", + "configure-app-settings-text": "Yapılandırma dosyasını indirip, bir önceki adımda klonladığınız projenin kök dizinine yerleştirin.", + "download-file": "Dosyayı indir", + "run-app-title": "Uygulamayı çalıştır", + "run-app-text": "IDE'nizde açıklandığı şekilde uygulamayı çalıştırın.\nTerminal kullanıyorsanız, aşağıdaki komutla uygulamayı çalıştırın:", + "more-information": "Detaylı bilgiye Başlarken dokümantasyonumuzdan ulaşabilirsiniz.", + "getting-started": "Başlarken" + } + }, + "notification": { + "action-button": "Eylem düğmesi", + "action-type": "Eylem türü", + "active": "Aktif", + "add-notification-recipients-group": "Bildirim alıcıları grubunu ekle", + "add-notification-template": "Bildirim şablonu ekle", + "add-recipient": "Alıcı ekle", + "add-recipients": "Alıcılar ekle", + "add-rule": "Kural ekle", + "add-stage": "Aşama ekle", + "add-template": "Şablon ekle", + "after": "Sonra", + "alarm-assignment-trigger-settings": "Alarm atama tetikleyici ayarları", + "alarm-comment-trigger-settings": "Alarm yorumu tetikleyici ayarları", + "alarm-trigger-settings": "Alarm tetikleyici ayarları", + "all": "Tümü", + "api-feature-hint": "Alan boşsa, tetikleyici tüm API özelliklerine uygulanır", + "api-usage-trigger-settings": "API kullanım tetikleyici ayarları", + "new-platform-version-trigger-settings": "Yeni platform sürümü tetikleyici ayarları", + "rate-limits-trigger-settings": "Aşılmış hız sınırı tetikleyici ayarları", + "task-processing-failure-trigger-settings": "Görev işleme hatası tetikleyici ayarları", + "resources-shortage-trigger-settings": "Kaynak yetersizliği tetikleyici ayarları", + "at-least-one-should-be-selected": "En az bir tane seçilmelidir", + "basic-settings": "Temel ayarlar", + "button-text": "Düğme metni", + "button-text-required": "Düğme metni gereklidir", + "button-text-max-length": "Düğme metni en fazla {{ length }} karakter olmalıdır", + "compose": "Oluştur", + "conversation": "Konuşma", + "conversation-required": "Konuşma gereklidir", + "copy-notification-template": "Bildirim şablonunu kopyala", + "copy-rule": "Kuralı kopyala", + "copy-template": "Şablonu kopyala", + "create-new": "Yeni oluştur", + "created": "Oluşturuldu", + "customize-messages": "Mesajları özelleştir", + "cpu-threshold": "CPU eşiği", + "delete-notification-text": "Dikkatli olun, onaydan sonra bildirim geri alınamaz hale gelecektir.", + "delete-notification-title": "Bu bildirimi silmek istediğinizden emin misiniz?", + "delete-notifications-text": "Dikkatli olun, onaydan sonra bildirimler geri alınamaz hale gelecektir.", + "delete-notifications-title": "{ count, plural, =1 {1 bildirim} other {# bildirim} } silmek istediğinizden emin misiniz?", + "delete-recipient-text": "Dikkatli olun, onaydan sonra alıcı geri alınamaz hale gelecektir.", + "delete-recipient-title": "'{{recipientName}}' alıcısını silmek istediğinizden emin misiniz?", + "delete-recipients-text": "Dikkatli olun, onaydan sonra alıcılar geri alınamaz hale gelecektir.", + "delete-recipients-title": "{ count, plural, =1 {1 alıcı} other {# alıcı} } silmek istediğinizden emin misiniz?", + "delete-request-text": "Dikkatli olun, onaydan sonra istek geri alınamaz hale gelecektir.", + "delete-request-title": "Bu isteği silmek istediğinizden emin misiniz?", + "delete-requests-text": "Dikkatli olun, onaydan sonra istekler geri alınamaz hale gelecektir.", + "delete-requests-title": "{ count, plural, =1 {1 istek} other {# istek} } silmek istediğinizden emin misiniz?", + "delete-rule-text": "Dikkatli olun, onaydan sonra kural geri alınamaz hale gelecektir.", + "delete-rule-title": "'{{ruleName}}' kuralını silmek istediğinizden emin misiniz?", + "delete-rules-text": "Dikkatli olun, onaydan sonra kurallar geri alınamaz hale gelecektir.", + "delete-rules-title": "{ count, plural, =1 {1 kural} other {# kural} } silmek istediğinizden emin misiniz?", + "delete-template-text": "Dikkatli olun, onaydan sonra şablon geri alınamaz hale gelecektir.", + "delete-template-title": "'{{templateName}}' şablonunu silmek istediğinizden emin misiniz?", + "delete-templates-text": "Dikkatli olun, onaydan sonra şablonlar geri alınamaz hale gelecektir.", + "delete-templates-title": "{ count, plural, =1 {1 şablon} other {# şablon} } silmek istediğinizden emin misiniz?", + "deleted": "Silindi", + "delivery-method": { + "delivery-method": "Teslimat yöntemi", + "email": "E-posta", + "email-preview": "E-posta bildirimi önizlemesi", + "slack": "Slack", + "slack-preview": "Slack bildirimi önizlemesi", + "microsoft-teams": "Microsoft Teams", + "microsoft-teams-preview": "Microsoft Teams bildirimi önizlemesi", + "sms": "SMS", + "sms-preview": "SMS bildirimi önizlemesi", + "web": "Web", + "web-preview": "Web bildirimi önizlemesi", + "mobile-app": "Mobil uygulama", + "mobile-app-preview": "Mobil uygulama bildirimi önizlemesi" + }, + "delivery-method-not-configure-click": "Teslimat yöntemi yapılandırılmamış. Yapılandırmak için tıklayın.", + "delivery-method-not-configure-contact": "Teslimat yöntemi yapılandırılmamış. Sistem yöneticinizle iletişime geçin.", + "delivery-methods": "Teslimat yöntemleri", + "description": "Açıklama", + "device-activity-trigger-settings": "Cihaz etkinliği tetikleyici ayarları", + "device-list-rule-hint": "Alan boşsa, tetikleyici tüm cihazlara uygulanır", + "device-profiles-list-rule-hint": "Alan boşsa, tetikleyici tüm cihaz profillerine uygulanır", + "disabled": "Devre dışı", + "edge-trigger-settings": "Edge tetikleyici ayarları", + "edge-list-rule-hint": "Alan boşsa, tetikleyici tüm Edge örneklerine uygulanır", + "edit-notification-recipients-group": "Bildirim alıcıları grubunu düzenle", + "edit-notification-template": "Bildirim şablonunu düzenle", + "edit-rule": "Kuralı düzenle", + "edit-template": "Şablonu düzenle", + "enabled": "Etkin", + "entities-limit-trigger-settings": "Varlık sınırı tetikleyici ayarları", + "entity-action-trigger-settings": "Varlık eylemi tetikleyici ayarları", + "entity-type": "Varlık tipi", + "escalation-chain": "Yükseltme zinciri", + "failed-send": "Gönderim hataları", + "fails": "{ count, plural, =1 {1 hata} other {# hata} }", + "filter": "Filtre", + "first-recipient": "İlk alıcı", + "inactive": "Pasif", + "inbox": "Gelen kutusu", + "notification-inbox": "Bildirimler / Gelen kutusu", + "input-field-support-templatization": "Giriş alanı şablonlamayı destekler.", + "input-fields-support-templatization": "Giriş alanları şablonlamayı destekler.", + "link": "Bağlantı", + "link-required": "Bağlantı gereklidir", + "link-max-length": "Bağlantı uzunluğu en fazla {{ length }} karakter olmalıdır", + "link-type": { + "dashboard": "Panoyu aç", + "link": "URL bağlantısını aç" + }, + "loading-notifications": "Bildirimler yükleniyor...", + "management": "Bildirim yönetimi", + "mark-all-as-read": "Tümünü okundu olarak işaretle", + "mark-as-read": "Okundu olarak işaretle", + "message": "Mesaj", + "message-required": "Mesaj gereklidir", + "message-max-length": "Mesaj en fazla {{ length }} karakter olmalıdır", + "name": "İsim", + "name-required": "İsim gereklidir", + "new-notification": "Yeni bildirim", + "no-inbox-notification": "Bildirim bulunamadı", + "no-notification-request": "Bildirim isteği yok", + "no-notification-templates": "Bildirim şablonu bulunamadı", + "no-notifications-yet": "Henüz bildirim yok", + "no-recipients-notification": "Alıcı bulunamadı bildirimi", + "no-recipients-matching": "'{{entity}}' ile eşleşen alıcı bulunamadı.", + "no-recipients-text": "Alıcı bulunamadı", + "no-rule": "Kural yapılandırılmamış", + "no-rules-notification": "Kural bildirimi yok", + "no-severity-found": "Önem seviyesi bulunamadı", + "no-severity-matching": "'{{severity}}' bulunamadı.", + "no-template-matching": "'{{template}}' ile eşleşen kaynak bulunamadı.", + "create-new-template": "Yeni bir tane oluştur!", + "not-found-slack-recipient": "Slack alıcısı bulunamadı", + "notification": "Bildirim", + "notification-center": "Bildirim merkezi", + "notification-tap-action": "Bildirim dokunma eylemi", + "notification-tap-action-hint": "Etkin değilse, varsayılan alarm panosu kullanılacaktır", + "notify": "bildir", + "notify-again": "Yeniden bildir", + "notify-alarm-action": { + "acknowledged": "Alarm onaylandı", + "assigned": "Alarm atandı", + "cleared": "Alarm temizlendi", + "created": "Alarm oluşturuldu", + "severity-changed": "Alarm şiddeti değişti", + "unassigned": "Alarm ataması kaldırıldı" + }, + "notify-on": "Bildirim tetikleyicisi", + "notify-on-comment-update": "Yorum güncellemesinde bildir", + "notify-on-required": "Bildirim tetikleyicisi gereklidir", + "notify-on-unassign": "Atama kaldırıldığında bildir", + "notify-only-user-comments": "Sadece kullanıcı yorumlarında bildir", + "only-rule-chain-lifecycle-failures": "Sadece kural zinciri yaşam döngüsü hataları", + "only-rule-node-lifecycle-failures": "Sadece kural düğümü yaşam döngüsü hataları", + "platform-users": "Platform kullanıcıları", + "ram-threshold": "RAM eşiği", + "rate-limits": "Hız sınırları", + "rate-limits-hint": "Alan boşsa, tetikleyici tüm hız sınırlarına uygulanır", + "recipient": "Alıcı", + "recipient-group": "Alıcı grubu", + "recipient-type": { + "affected-tenant-administrators": "Etkilenen kiracı yöneticileri", + "affected-user": "Etkilenen kullanıcı", + "all-users": "Tüm kullanıcılar", + "customer-users": "Müşteri kullanıcıları", + "system-administrators": "Sistem yöneticileri", + "tenant-administrators": "Kiracı yöneticileri", + "user-filters": "Kullanıcı filtresi", + "user-list": "Kullanıcı listesi", + "users-entity-owner": "Varlık sahibinin kullanıcıları" + }, + "recipients": "Alıcılar", + "notification-recipient": "Bildirim alıcısı", + "notification-recipient-required": "Bildirim alıcısı gereklidir.", + "notification-recipients": "Bildirimler / Alıcılar", + "recipients-count": "{ count, plural, =1 {1 alıcı} other {# alıcı} }", + "recipients-required": "Alıcılar gereklidir", + "refresh-allow-delivery-method": "İzin verilen iletim yöntemini yenile", + "request-search": "İstek araması", + "request-status": { + "processing": "İşleniyor", + "scheduled": "Zamanlandı", + "sent": "Gönderildi" + }, + "review": "İnceleme", + "rule": "Kural", + "rule-chain-list-rule-hint": "Alan boşsa, tetikleyici tüm kural zincirlerine uygulanır", + "rule-engine-events-trigger-settings": "Kural motoru olay tetikleyici ayarları", + "rule-engine-filter": "Kural motoru filtresi", + "rule-name": "Kural adı", + "rule-name-required": "Ad gereklidir", + "rule-disable": "Bildirim kuralını devre dışı bırak", + "rule-enable": "Bildirim kuralını etkinleştir", + "rule-node-filter": "Kural düğümü filtresi", + "rules": "Kurallar", + "notification-rules": "Bildirimler / Kurallar", + "scheduler-later": "Daha sonra zamanla", + "search-notification": "Bildirimlerde ara", + "search-recipients": "Alıcılarda ara", + "search-rules": "Kurallarda ara", + "search-templates": "Şablonlarda ara", + "see-documentation": "Belgeleri gör", + "selected-notifications": "{ count, plural, =1 {1 bildirim} other {# bildirim} } seçildi", + "selected-recipients": "{ count, plural, =1 {1 alıcı} other {# alıcı} } seçildi", + "selected-requests": "{ count, plural, =1 {1 istek} other {# istek} } seçildi", + "selected-rules": "{ count, plural, =1 {1 kural} other {# kural} } seçildi", + "selected-template": "{ count, plural, =1 {1 şablon} other {# şablon} } seçildi", + "send-notification": "Bildirim gönder", + "sent": "Gönderildi", + "setup": "Kurulum", + "notification-sent": "Bildirimler / Gönderilen", + "set-entity-from-notification": "Bildirimden varlık ayarla ve pano durumuna geçir", + "slack-chanel-type": "Slack kanal türü", + "slack-chanel-types": { + "direct": "Doğrudan mesaj", + "private-channel": "Özel kanal", + "public-channel": "Genel kanal" + }, + "start-from-scratch": "Sıfırdan başla", + "status": "Durum", + "stop-escalation-alarm-status-become": "Alarm durumu olduğunda artan bildirimleri durdur:", + "storage-threshold": "Depolama eşiği", + "subject": "Konu", + "subject-required": "Konu gereklidir", + "subject-max-length": "Konu en fazla {{ length }} karakter olmalıdır", + "template": "Şablon", + "template-name": "Şablon adı", + "template-required": "Şablon gereklidir", + "template-type": { + "alarm": "Alarm", + "alarm-assignment": "Alarm ataması", + "alarm-comment": "Alarm yorumu", + "api-usage-limit": "API kullanım limiti", + "device-activity": "Cihaz etkinliği", + "entities-limit": "Varlık sınırı", + "entity-action": "Varlık işlemi", + "general": "Genel", + "rule-engine-lifecycle-event": "Kural motoru yaşam döngüsü olayı", + "rule-node": "Kural düğümü", + "new-platform-version": "Yeni platform sürümü", + "rate-limits": "Aşılan hız sınırları", + "edge-communication-failure": "Edge iletişim hatası", + "edge-connection": "Edge bağlantısı", + "task-processing-failure": "Görev işleme hatası", + "resources-shortage": "Kaynak yetersizliği" + }, + "templates": "Şablonlar", + "notification-templates": "Bildirimler / Şablonlar", + "tenant-profiles-list-rule-hint": "Alan boş bırakılırsa, tetikleyici tüm kiracı profillerine uygulanacaktır", + "tenants-list-rule-hint": "Alan boş bırakılırsa, tetikleyici tüm kiracılara uygulanacaktır", + "threshold": "Eşik", + "theme-color": "Tema rengi", + "time": "Zaman", + "track-rule-node-events": "Kural düğümü olaylarını takip et", + "trigger": { + "alarm": "Alarm", + "alarm-assignment": "Alarm ataması", + "alarm-comment": "Alarm yorumu", + "api-usage-limit": "API kullanım limiti", + "device-activity": "Cihaz etkinliği", + "entities-limit": "Varlık sınırı", + "entity-action": "Varlık işlemi", + "rule-engine-lifecycle-event": "Kural motoru yaşam döngüsü olayı", + "new-platform-version": "Yeni platform sürümü", + "rate-limits": "Aşılan hız sınırları", + "edge-connection": "Edge bağlantısı", + "edge-communication-failure": "Edge iletişim hatası", + "task-processing-failure": "Görev işleme hatası", + "resources-shortage": "Kaynak yetersizliği", + "trigger": "Tetikleyici", + "trigger-required": "Tetikleyici gereklidir" + }, + "type": "Tür", + "unread": "Okunmamış", + "updated": "Güncellendi", + "use-deprecated-webhook-connectors": "Eski Webhook bağlayıcılarını kullan", + "use-old-api": "Eski API'yi kullan", + "use-template": "Şablon kullan", + "view-all": "Tümünü görüntüle", + "warning": "Uyarı", + "webhook-url": "Webhook URL", + "webhook-url-required": "Webhook URL gereklidir", + "workflow-url": "İş akışı URL", + "workflow-url-required": "İş akışı URL gereklidir", + "channel-name": "Kanal adı", + "channel-name-required": "Kanal adı gereklidir", + "settings": { + "notification-settings": "Bildirim ayarları", + "reset-all": "Tüm ayarları sıfırla", + "reset-all-title": "Formu sıfırlamak istediğinizden emin misiniz?", + "reset-all-text": "Onaydan sonra, ayarlar formu varsayılan değerlere sıfırlanacak ve kaydedilecektir.", + "type": "Tür", + "enable-all": "Tümünü etkinleştir", + "disable-all": "Tümünü devre dışı bırak", + "delivery-not-configured": "Teslimat yöntemi yapılandırılmamış" + } }, "ota-update": { "add": "Paket ekle", - "assign-firmware": "Atanan donanım yazılımı (Firmware)", - "assign-firmware-required": "Atanan donanım yazılımı gerekli", + "assign-firmware": "Atanan ürün yazılımı", + "assign-firmware-required": "Atanan ürün yazılımı gereklidir", "assign-software": "Atanan yazılım", - "assign-software-required": "Atanan yazılım gerekli (Software)", - "auto-generate-checksum": "Otomatik checksum oluştur", - "checksum": "Checksum", - "checksum-hint": "Checksum boşsa, otomatik olarak oluşturulur", - "checksum-algorithm": "Checksum algoritması", - "checksum-copied-message": "Paket checksum panoya kopyalandı", - "change-firmware": "Firmware değişikliği { count, plural, =1 {1 cihazın} other {# cihazın} } güncellenmesine neden olabilir.", - "change-software": "Software değişikliği { count, plural, =1 {1 cihazın} other {# cihazın} }.", + "assign-software-required": "Atanan yazılım gereklidir", + "auto-generate-checksum": "Otomatik sağlama toplamı oluştur", + "checksum": "Sağlama toplamı", + "checksum-hint": "Sağlama toplamı boşsa otomatik olarak oluşturulacaktır", + "checksum-algorithm": "Sağlama algoritması", + "checksum-copied-message": "Paketin sağlama toplamı panoya kopyalandı", + "change-firmware": "Ürün yazılımının değiştirilmesi { count, plural, =1 {1 cihazı} other {# cihazı} } güncelleyebilir.", + "change-software": "Yazılımın değiştirilmesi { count, plural, =1 {1 cihazı} other {# cihazı} } güncelleyebilir.", + "change-ota-setting-title": "OTA ayarlarını değiştirmek istediğinizden emin misiniz?", "chose-compatible-device-profile": "Yüklenen paket yalnızca seçilen profile sahip cihazlar için geçerli olacaktır.", - "chose-firmware-distributed-device": "Cihazlara dağıtılacak firmware'i seçin", - "chose-software-distributed-device": "Cihazlara dağıtılacak software'i seçin", + "chose-firmware-distributed-device": "Cihazlara dağıtılacak ürün yazılımını seçin", + "chose-software-distributed-device": "Cihazlara dağıtılacak yazılımı seçin", "content-type": "İçerik türü", - "copy-checksum": "Checksum kopyala", - "copy-direct-url": "Açık URL'yi kopyala", + "copy-checksum": "Sağlama toplamını kopyala", + "copy-direct-url": "Doğrudan URL’yi kopyala", "copyId": "Paket kimliğini kopyala", "copied": "Kopyalandı!", "delete": "Paketi sil", - "delete-ota-update-text": "Dikkatli olun, onaydan sonra OTA güncellemesi kurtarılamaz hale gelecektir.", - "delete-ota-update-title": "'{{title}}' OTA güncellemesini silmek istediğinizden emin misiniz?", - "delete-ota-updates-text": "Dikkatli olun, onaydan sonra seçilen tüm OTA güncellemeleri kaldırılacaktır.", - "delete-ota-updates-title": "{ count, plural, =1 {1 OTA güncellemesini} other {# OTA güncellemesini} } silmek istediğinizden emin misiniz?", + "delete-ota-update-text": "Dikkatli olun, onaydan sonra OTA güncellemesi geri alınamaz hale gelecektir.", + "delete-ota-update-title": "OTA güncellemesi '{{title}}' silinsin mi?", + "delete-ota-updates-text": "Dikkatli olun, onaydan sonra seçilen tüm OTA güncellemeleri silinecektir.", + "delete-ota-updates-title": "{ count, plural, =1 {1 OTA güncellemesi} other {# OTA güncellemesi} } silinsin mi?", "description": "Açıklama", - "direct-url": "Açık URL", - "direct-url-copied-message": "Paket açık URL'si panoya kopyalandı", - "direct-url-required": "Açık URL gerekli", + "direct-url": "Doğrudan URL", + "direct-url-copied-message": "Paketin doğrudan URL'si panoya kopyalandı", + "direct-url-required": "Doğrudan URL gereklidir", "download": "Paketi indir", - "drop-file": "Bir paket dosyası bırakın veya yüklenecek dosyayı seçmek için tıklayın.", + "drop-file": "Bir paket dosyasını bırakın veya yüklemek için tıklayın.", + "drop-package-file-or": "Bir paket dosyasını sürükleyip bırakın veya", "file-name": "Dosya adı", "file-size": "Dosya boyutu", - "file-size-bytes": "Bayt (byte) cinsinden dosya boyutu", + "file-size-bytes": "Bayt cinsinden dosya boyutu", "idCopiedMessage": "Paket kimliği panoya kopyalandı", - "no-firmware-matching": "'{{entity}}' ile eşleşen uyumlu Ürün Yazılımı OTA Güncelleme paketi bulunamadı.", - "no-firmware-text": "Uyumlu Donanım Yazılımı OTA Güncelleme paketi sağlanmadı.", + "no-firmware-matching": "Uygun Firmware OTA güncelleme paketi '{{entity}}' için bulunamadı.", + "no-firmware-text": "Uygun Firmware OTA güncelleme paketi yok.", "no-packages-text": "Paket bulunamadı", - "no-software-matching": "'{{entity}}' ile eşleşen uyumlu Yazılım OTA Güncelleme paketi bulunamadı.", - "no-software-text": "Uyumlu Yazılım OTA Güncelleme paketi sağlanmadı.", + "no-software-matching": "Uygun Yazılım OTA güncelleme paketi '{{entity}}' için bulunamadı.", + "no-software-text": "Uygun Yazılım OTA güncelleme paketi yok.", "ota-update": "OTA güncellemesi", "ota-update-details": "OTA güncelleme ayrıntıları", "ota-updates": "OTA güncellemeleri", - "package-type": "Paket Tipi", + "package-file": "Paket dosyası", + "package-type": "Paket türü", "packages-repository": "Paket deposu", - "search": "Paketleri ara", + "search": "Paket ara", "selected-package": "{ count, plural, =1 {1 paket} other {# paket} } seçildi", "title": "Başlık", - "title-required": "Başlık gerekli.", + "title-required": "Başlık gereklidir.", + "title-max-length": "Başlık 256 karakterden kısa olmalıdır", "types": { - "firmware": "Firmware", - "software": "Software" + "firmware": "Ürün yazılımı", + "software": "Yazılım" }, - "upload-binary-file": "Binary dosya yükle", + "upload-binary-file": "İkili dosya yükle", "use-external-url": "Harici URL kullan", "version": "Sürüm", - "version-required": "Sürüm gerekli.", - "version-tag": "Sürüm Etiketi", - "version-tag-hint": "Özel etiket, cihazınız tarafından bildirilen paket sürümüyle eşleşmelidir.", - "warning-after-save-no-edit": "Paket yüklendikten sonra başlığı, sürümü, cihaz profilini ve paket türünü değiştiremezsiniz.." + "version-required": "Sürüm gereklidir.", + "version-tag": "Sürüm etiketi", + "version-tag-hint": "Özel etiket, cihazınızın bildirdiği paket sürümüyle eşleşmelidir.", + "version-max-length": "Sürüm 256 karakterden kısa olmalıdır", + "warning-after-save-no-edit": "Paket yüklendikten sonra başlık, sürüm, cihaz profili ve paket türü değiştirilemez." }, "position": { "top": "Üst", @@ -2251,16 +4303,105 @@ }, "profile": { "profile": "Profil", - "last-login-time": "Son giriş tarihi", - "change-password": "Şifre değiştir", - "current-password": "Şimdiki şifre" + "last-login-time": "Son Giriş", + "change-password": "Şifreyi Değiştir", + "current-password": "Mevcut şifre", + "copy-jwt-token": "JWT jetonunu kopyala", + "jwt-token": "JWT jetonu", + "token-valid-till": "Jeton geçerlilik süresi", + "tokenCopiedSuccessMessage": "JWT jetonu panoya kopyalandı", + "tokenCopiedWarnMessage": "JWT jetonu süresi dolmuş! Lütfen sayfayı yenileyin." + }, + "profiles": { + "profiles": "Profiller" + }, + "security": { + "security": "Güvenlik", + "general-settings": "Genel güvenlik ayarları", + "access-token": "Erişim belirteci", + "access-token-required": "Erişim belirteci gereklidir", + "clientId": "İstemci Kimliği", + "clientId-required": "İstemci Kimliği gereklidir", + "username": "Kullanıcı adı", + "username-required": "Kullanıcı adı gereklidir", + "ca-cert": "CA sertifikası", + "2fa": { + "2fa": "İki faktörlü kimlik doğrulama", + "2fa-description": "İki faktörlü kimlik doğrulama, hesabınızı yetkisiz erişime karşı korur. Tek yapmanız gereken giriş yaparken bir güvenlik kodu girmektir.", + "authenticate-with": "Şununla kimlik doğrulaması yapabilirsiniz:", + "disable-2fa-provider-text": "{{name}} devre dışı bırakıldığında hesabınız daha az güvenli olur", + "disable-2fa-provider-title": "{{name}} sağlayıcısını devre dışı bırakmak istediğinizden emin misiniz?", + "get-new-code": "Yeni kod al", + "main-2fa-method": "Ana iki faktörlü kimlik doğrulama yöntemi olarak kullan", + "dialog": { + "activation-step-description-email": "Bir dahaki girişinizde, e-posta adresinize gönderilecek güvenlik kodunu girmeniz istenecek.", + "activation-step-description-sms": "Bir dahaki girişinizde, telefon numaranıza gönderilecek güvenlik kodunu girmeniz istenecek.", + "activation-step-description-totp": "Bir dahaki girişinizde, iki faktörlü kimlik doğrulama kodu sağlamanız gerekecek.", + "activation-step-label": "Etkinleştirme", + "backup-code-description": "Yedek kodları yazdırın, böylece hesabınıza giriş yapmak için ihtiyaç duyduğunuzda elinizin altında olur. Her bir yedek kod yalnızca bir kez kullanılabilir.", + "backup-code-warn": "Bu sayfadan ayrıldığınızda, bu kodlar tekrar gösterilemez. Aşağıdaki seçenekleri kullanarak güvenli bir şekilde saklayın.", + "download-txt": "İndir (txt)", + "email-step-description": "Kimlik doğrulayıcı olarak kullanılacak bir e-posta girin.", + "email-step-label": "E-posta", + "enable-email-title": "E-posta kimlik doğrulayıcıyı etkinleştir", + "enable-sms-title": "SMS kimlik doğrulayıcıyı etkinleştir", + "enable-totp-title": "Kimlik doğrulama uygulamasını etkinleştir", + "enter-verification-code": "6 haneli kodu buraya girin", + "get-backup-code-title": "Yedek kod al", + "next": "İleri", + "scan-qr-code": "Doğrulama uygulamanızla bu QR kodunu tarayın", + "send-code": "Kodu gönder", + "sms-step-description": "Kimlik doğrulayıcı olarak kullanılacak bir telefon numarası girin.", + "sms-step-label": "Telefon Numarası", + "success": "Başarılı!", + "totp-step-description-install": "Google Authenticator, Authy veya Duo gibi uygulamaları yükleyebilirsiniz.", + "totp-step-description-open": "Mobil telefonunuzda kimlik doğrulayıcı uygulamasını açın.", + "totp-step-label": "Uygulama edin", + "verification-code": "6 haneli kod", + "verification-code-invalid": "Geçersiz doğrulama kodu biçimi", + "verification-code-incorrect": "Doğrulama kodu yanlış", + "verification-code-many-request": "Çok fazla istek gönderildi, doğrulama kodunu kontrol edin", + "verification-step-description": "{{address}} adresine yeni gönderdiğimiz 6 haneli kodu girin", + "verification-step-label": "Doğrulama" + }, + "provider": { + "email": "E-posta", + "email-description": "Kimlik doğrulamak için e-posta adresinize gönderilen bir güvenlik kodunu kullanın.", + "email-hint": "Kimlik doğrulama kodları {{ info }} adresine e-posta ile gönderilir", + "sms": "SMS", + "sms-description": "Kimlik doğrulamak için telefonunuzu kullanın. Giriş yaptığınızda size SMS ile bir güvenlik kodu göndereceğiz.", + "sms-hint": "Kimlik doğrulama kodları {{ info }} numarasına SMS ile gönderilir", + "totp": "Kimlik doğrulayıcı uygulama", + "totp-description": "Google Authenticator, Authy veya Duo gibi uygulamaları telefonunuzda kullanarak kimlik doğrulama yapın. Giriş yapmak için bir güvenlik kodu üretir.", + "totp-hint": "Hesabınız için kimlik doğrulayıcı uygulama ayarlandı", + "backup_code": "Yedek kod", + "backup-code-description": "Bu yazdırılabilir tek kullanımlık kodlar, seyahatteyken veya telefonunuza erişiminiz olmadığında oturum açmanıza olanak tanır.", + "backup-code-hint": "Şu anda {{ info }} adet tek kullanımlık kod aktif" + } + }, + "password-requirement": { + "at-least": "En az:", + "character": "{ count, plural, =1 {1 karakter} other {# karakter} }", + "digit": "{ count, plural, =1 {1 rakam} other {# rakam} }", + "incorrect-password-try-again": "Hatalı şifre. Lütfen tekrar deneyin", + "lowercase-letter": "{ count, plural, =1 {1 küçük harf} other {# küçük harf} }", + "new-passwords-not-match": "Yeni şifreler eşleşmedi", + "password-should-not-contain-spaces": "Şifreniz boşluk karakteri içermemelidir", + "password-not-meet-requirements": "Şifre gereksinimlerini karşılamıyor", + "password-requirements": "Şifre gereksinimleri", + "password-should-difference": "Yeni şifre mevcut şifreden farklı olmalıdır", + "special-character": "{ count, plural, =1 {1 özel karakter} other {# özel karakter} }", + "uppercase-letter": "{ count, plural, =1 {1 büyük harf} other {# büyük harf} }", + "at-most": "En fazla:" + } }, "relation": { "relations": "İlişkiler", - "direction": "Yönelim", + "direction": "Yön", + "clear-relation-type": "İlişki türünü temizle", "search-direction": { - "FROM": "KAYNAK", - "TO": "HEDEF" + "FROM": "Kaynak", + "TO": "Hedef" }, "direction-type": { "FROM": "kaynak", @@ -2270,330 +4411,1452 @@ "to-relations": "Gelen ilişkiler", "selected-relations": "{ count, plural, =1 {1 ilişki} other {# ilişki} } seçildi", "type": "Tür", - "to-entity-type": "Hedef Öğe Türü", - "to-entity-name": "Hedef Öğe Adı", - "from-entity-type": "Kaynak Öğe Türü", - "from-entity-name": "Kaynak Öğe Adı", - "to-entity": "Hedef Öğe", - "from-entity": "Kaynak Öğe", + "to-entity-type": "Hedef varlık türü", + "to-entity-name": "Hedef varlık adı", + "from-entity-type": "Kaynak varlık türü", + "from-entity-name": "Kaynak varlık adı", + "to-entity": "Hedef varlık", + "from-entity": "Kaynak varlık", "delete": "İlişkiyi sil", "relation-type": "İlişki türü", - "relation-type-required": "İlişki türü gerekli.", - "any-relation-type": "Her hangi bir tür", + "relation-type-required": "İlişki türü gereklidir.", + "relation-type-max-length": "İlişki türü 256 karakterden kısa olmalıdır", + "any-relation-type": "Herhangi bir tür", "add": "İlişki ekle", - "edit": "İlişki düzenle", - "delete-to-relation-title": "'{{entityName}}' öğesine olan ilişkiyi silmek istediğinize emin misiniz?", - "delete-to-relation-text": "UYARI: Onaylandıktan sonra '{{entityName}}' öğesinin şimdiki öğeyle olan ilişkisi sona erecektir.", - "delete-to-relations-title": "{ count, plural, =1 {1 ilişkiyi} other {# ilişkiyi} } silmek istediğinize emin misiniz?", - "delete-to-relations-text": "UYARI: Onaylandıktan sonra tüm seçili ilişkiler kaldırılacaktır ve ilgili öğelerin şimdiki öğeyle ilişkisi sona erecektir.", - "delete-from-relation-title": "'{{entityName}}' öğesinden ilişkiyi silmek istediğinize emin misiniz?", - "delete-from-relation-text": "UYARI: Onaylandıktan sonra şimdiki öğenin '{{entityName}}' öğesiyle ilişkisi sonlandırılacaktır.", - "delete-from-relations-title": "{ count, plural, =1 {1 ilişkiyi} other {# ilişkiyi} } silmek istediğinize emin misiniz?", - "delete-from-relations-text": "UYARI: Onaylandıktan sonra tüm seçili ilişkiler kaldırılacak ve şimdiki öğenin ilgili tüm öğelerle ilişkisi sona erecektir.", + "edit": "İlişkiyi düzenle", + "delete-to-relation-title": "'{{entityName}}' varlığına olan ilişkiyi silmek istediğinizden emin misiniz?", + "delete-to-relation-text": "Dikkatli olun, onaydan sonra '{{entityName}}' varlığı mevcut varlıkla ilişkilendirilmemiş olacak.", + "delete-to-relations-title": "{ count, plural, =1 {1 ilişki} other {# ilişki} } silmek istediğinizden emin misiniz?", + "delete-to-relations-text": "Dikkatli olun, onaydan sonra tüm seçili ilişkiler silinecek ve karşılık gelen varlıklarla bağlantı kaldırılacak.", + "delete-from-relation-title": "'{{entityName}}' varlığından olan ilişkiyi silmek istediğinizden emin misiniz?", + "delete-from-relation-text": "Dikkatli olun, onaydan sonra mevcut varlık '{{entityName}}' varlığıyla ilişkilendirilmemiş olacak.", + "delete-from-relations-title": "{ count, plural, =1 {1 ilişki} other {# ilişki} } silmek istediğinizden emin misiniz?", + "delete-from-relations-text": "Dikkatli olun, onaydan sonra tüm seçili ilişkiler silinecek ve mevcut varlık, karşılık gelen varlıklarla ilişkilendirilmemiş olacak.", "remove-relation-filter": "İlişki filtresini kaldır", - "add-relation-filter": "İlişkisi ekle", + "remove-filter": "Filtreyi kaldır", + "add-relation-filter": "İlişki filtresi ekle", "any-relation": "Herhangi bir ilişki", "relation-filters": "İlişki filtreleri", + "relation-filter": "İlişki filtresi", "additional-info": "Ek bilgi (JSON)", - "invalid-additional-info": "Ek bilgi JSON'ı parse edilip işlenemedi.", - "no-relations-text": "İlişki bulunamadı" + "invalid-additional-info": "Ek bilgi json'u ayrıştırılamadı.", + "no-relations-text": "İlişki bulunamadı", + "not": "Değil" }, "resource": { - "add": "Kaynak Ekle", + "add": "Kaynak ekle", + "all-types": "Tümü", "copyId": "Kaynak kimliğini kopyala", "delete": "Kaynağı sil", - "delete-resource-text": "Dikkatli olun, onaydan sonra kaynak kurtarılamaz hale gelecektir..", + "delete-resource-text": "Dikkatli olun, onaydan sonra kaynak geri alınamaz hale gelecek.", "delete-resource-title": "'{{resourceTitle}}' kaynağını silmek istediğinizden emin misiniz?", - "delete-resources-action-title": "{ count, plural, =1 {1 kaynağı} other {# kaynağı} } sil", - "delete-resources-text": "Lütfen seçilen kaynakların cihaz profillerinde kullanılsalar bile silineceğini unutmayın.", - "delete-resources-title": "{ count, plural, =1 {1 kaynağı} other {# kaynağı} } silmek istediğinizden emin misiniz?", + "delete-resources-action-title": "{ count, plural, =1 {1 kaynak} other {# kaynak} } sil", + "delete-resources-text": "Lütfen dikkat, seçilen kaynaklar cihaz profillerinde kullanılsa bile silinecektir.", + "delete-resources-title": "{ count, plural, =1 {1 kaynak} other {# kaynak} } silmek istediğinizden emin misiniz?", "download": "Kaynağı indir", - "drop-file": "Bir kaynak dosyası bırakın veya yüklenecek dosyayı seçmek için tıklayın.", + "drop-file": "Bir kaynak dosyasını bırakın veya yüklemek için tıklayın.", + "drop-resource-file-or": "Bir kaynak dosyasını sürükleyip bırakın ya da", "empty": "Kaynak boş", "file-name": "Dosya adı", - "idCopiedMessage": "Kaynak Kimliği panoya kopyalandı", + "idCopiedMessage": "Kaynak kimliği panoya kopyalandı", "no-resource-matching": "'{{widgetsBundle}}' ile eşleşen kaynak bulunamadı.", - "no-resource-text": "Kaynak bulunamadı", + "no-resource-text": "Hiç kaynak bulunamadı", "open-widgets-bundle": "Widget paketini aç", "resource": "Kaynak", + "resource-file": "Kaynak dosyası", + "resource-files": "Kaynak dosyaları", "resource-library-details": "Kaynak ayrıntıları", "resource-type": "Kaynak türü", - "resources-library": "Kaynak kütüphanesi", - "search": "Kaynak ara", + "resources-library": "Kaynak kitaplığı", + "search": "Kaynakları ara", "selected-resources": "{ count, plural, =1 {1 kaynak} other {# kaynak} } seçildi", "system": "Sistem", "title": "Başlık", - "title-required": "Başlık gerekli." + "title-required": "Başlık gereklidir.", + "title-max-length": "Başlık 256 karakterden kısa olmalıdır", + "type": { + "jks": "JKS", + "js-module": "JS modülü", + "lwm2m-model": "LWM2M modeli", + "pkcs-12": "PKCS #12" + }, + "resource-sub-type": "Alt tür", + "sub-type": { + "image": "resim", + "scada-symbol": "Scada sembolü", + "extension": "Uzantı", + "module": "Modül" + } + }, + "javascript": { + "add": "JavaScript kaynağı ekle", + "delete": "JavaScript kaynağını sil", + "delete-javascript-resource-text": "Dikkatli olun, onaydan sonra JavaScript kaynağı geri alınamaz hale gelecek.", + "delete-javascript-resource-title": "'{{resourceTitle}}' JavaScript kaynağını silmek istediğinizden emin misiniz?", + "delete-javascript-resources-action-title": "JavaScript { count, plural, =1 {1 kaynak} other {# kaynak} } sil", + "delete-javascript-resources-text": "Lütfen dikkat, seçilen JavaScript kaynakları JavaScript fonksiyonlarında kullanılsa bile silinecektir.", + "delete-javascript-resources-title": "JavaScript { count, plural, =1 {1 kaynak} other {# kaynak} } silmek istediğinizden emin misiniz?", + "delete-javascript-resource-in-use-text": "Yine de JavaScript kaynağını silmek istiyorsanız Yine de sil düğmesine tıklayın.", + "download": "JavaScript kaynağını indir", + "upload-from-file": "Dosyadan JavaScript yükle", + "resource-file": "JavaScript kaynak dosyası", + "drop-file": "Bir JavaScript dosyasını bırakın veya yüklemek için tıklayın.", + "drop-resource-file-or": "Bir JavaScript dosyasını sürükleyip bırakın ya da", + "javascript-library": "JavaScript kütüphanesi", + "javascript-type": "JavaScript türü", + "javascript-resource-details": "JavaScript kaynağı ayrıntıları", + "javascript-resource-is-in-use": "JavaScript kaynağı başka varlıklar tarafından kullanılıyor", + "javascript-resources-are-in-use": "JavaScript kaynakları başka varlıklar tarafından kullanılıyor", + "javascript-resource-is-in-use-text": "'{{title}}' JavaScript kaynağı silinmedi çünkü aşağıdaki varlıklar tarafından kullanılıyor:", + "javascript-resources-are-in-use-text": "Tüm JavaScript kaynakları silinmedi çünkü bazıları başka varlıklar tarafından kullanılıyor.
    İlgili varlıkları Referanslar düğmesine tıklayarak görebilirsiniz.
    Bu JavaScript kaynaklarını yine de silmek istiyorsanız, tabloda seçim yaparak Seçileni sil düğmesine tıklayın.", + "search": "JavaScript kaynaklarını ara", + "selected-javascript-resources": "{ count, plural, =1 {1 JavaScript kaynağı} other {# JavaScript kaynağı} } seçildi", + "no-javascript-resource-text": "Hiç JavaScript kaynağı bulunamadı", + "all-types": "Tümü", + "module-script": "Modül betiği" + }, + "rpc": { + "error": { + "target-device-is-not-set": "Hedef cihaz ayarlanmadı!", + "invalid-target-entity": "RPC komutları {{entityType}} varlığı tarafından desteklenmiyor.", + "failed-to-resolve-target-device": "Hedef cihaz çözümlenemedi!", + "request-timeout": "İstek zaman aşımına uğradı", + "rpc-http-error": "Hata: {{status}} - {{statusText}}" + } }, "rulechain": { - "rulechain": "Kural", - "rulechains": "Kurallar", + "rulechain": "Kural zinciri", + "rulechain-events": "Kural zinciri olayları", + "rulechains": "Kural zincirleri", "root": "Kök", - "delete": "Kuralı sil", + "delete": "Kural zincirini sil", "name": "İsim", - "name-required": "İsim gerekli.", + "name-required": "İsim gereklidir.", + "name-max-length": "İsim 256 karakterden kısa olmalıdır", "description": "Açıklama", - "add": "Kural Ekle", - "set-root": "Kural zincirinin kökü yap", - "set-root-rulechain-title": "Kural zincirini {{ruleChainName}} root? Yapmak istediğinizden emin misiniz?", - "set-root-rulechain-text": "Onaydan sonra kural zinciri kökleşecek ve gelen tüm iletilerle ilgilenecek.", - "delete-rulechain-title": "'{{ruleName}}' isimli kuralı silmek istediğinize emin misiniz?", - "delete-rulechain-text": "UYARI: Onaylandıktan sonra kural ve ilişkili tüm veriler geri yüklenemez şekilde silinecektir.", - "delete-rulechains-title": "{ count, plural, =1 {1 kuralı} other {# kuralı} } sikmek istediğinize emin misiniz?", - "delete-rulechains-action-title": "{ count, plural, =1 {1 kuralı} other {# kuralı} } sil", - "delete-rulechains-text": "UYARI: Onaylandıktan sonra seçili tüm kurallar ve ilişkili tüm veriler geri yüklenemez şekilde silinecektir.", - "add-rulechain-text": "Yeni kural ekle", - "no-rulechains-text": "Hiçbir kural bulunamadı", - "rulechain-details": "Kural detayları", + "add": "Kural zinciri ekle", + "set-root": "Kural zincirini kök yap", + "set-root-rulechain-title": "'{{ruleChainName}}' kural zincirini kök yapmak istediğinizden emin misiniz?", + "set-root-rulechain-text": "Onaydan sonra bu kural zinciri kök olacak ve tüm gelen taşıma mesajlarını işleyecek.", + "delete-rulechain-title": "'{{ruleChainName}}' kural zincirini silmek istediğinizden emin misiniz?", + "delete-rulechain-text": "Dikkatli olun, onaydan sonra kural zinciri ve tüm ilgili veriler geri alınamaz hale gelecektir.", + "delete-rulechains-title": "{ count, plural, =1 {1 kural zinciri} other {# kural zinciri} } silmek istediğinizden emin misiniz?", + "delete-rulechains-action-title": "{ count, plural, =1 {1 kural zinciri} other {# kural zinciri} } sil", + "delete-rulechains-text": "Dikkatli olun, onaydan sonra seçili tüm kural zincirleri ve ilgili veriler silinecektir.", + "add-rulechain-text": "Yeni kural zinciri ekle", + "no-rulechains-text": "Hiç kural zinciri bulunamadı", + "rulechain-details": "Kural zinciri detayları", "details": "Detaylar", "events": "Olaylar", "system": "Sistem", - "import": "Kuralı içe aktar", - "export": "Kuralı dışa aktar", - "export-failed-error": "Kural dışa aktarılamadı: {{error}}", - "create-new-rule": "Yeni kural oluştur", - "rulechain-file": "Kural dosyası", - "invalid-rulechain-file-error": "Kural içe aktarılamadı: Geçersiz kural veri yapısı.", - "copyId": "Kural kimliğini kopyala", - "idCopiedMessage": "Kural kimliği panoya kopyalandı", - "select-rulechain": "Kural seç", - "no-rulechains-matching": "'{{entity}}' ile eşleşen kural bulunamadı.", - "rulechain-required": "Kural gerekli", + "import": "Kural zinciri içe aktar", + "export": "Kural zinciri dışa aktar", + "export-failed-error": "Kural zinciri dışa aktarılamadı: {{error}}", + "create-new-rulechain": "Yeni kural zinciri oluştur", + "rulechain-file": "Kural zinciri dosyası", + "invalid-rulechain-file-error": "Kural zinciri içe aktarılamadı: Geçersiz kural zinciri veri yapısı.", + "copyId": "Kural zinciri Id'sini kopyala", + "idCopiedMessage": "Kural zinciri Id panoya kopyalandı", + "select-rulechain": "Kural zinciri seç", + "no-rulechains-matching": "'{{entity}}' ile eşleşen kural zinciri bulunamadı.", + "rulechain-required": "Kural zinciri gereklidir", "management": "Kural yönetimi", "debug-mode": "Hata ayıklama modu", - "search": "Kural Ara", - "selected-rulechains": "{ count, plural, =1 {1 kural} other {# kural} } seçildi", - "open-rulechain": "Kuralı Aç", - "edge-template-root": "Şablon Kökü", - "assign-to-edge": "Uca Ata", - "edge-rulechain": "Uç kuralı zinciri", - "unassign-rulechain-from-edge-text": "Onaydan sonra kural zincirinin ataması kaldırılacak ve kenar tarafından erişilebilir olmayacak.", - "unassign-rulechains-from-edge-title": "{ count, plural, =1 {1 kural zincirinin} other {# kural zincirinin} } atamasını kaldırmak istediğinizden emin misiniz?", - "unassign-rulechains-from-edge-text": "Onaydan sonra, seçilen tüm kural zincirlerinin ataması kaldırılacak ve uç tarafından erişilemeyecek.", - "assign-rulechain-to-edge-title": "Uca Kural Zinciri/Zincirleri Ata", - "assign-rulechain-to-edge-text": "Lütfen uca atanacak kural zincirlerini seçin", - "set-edge-template-root-rulechain": "Kural zincirini uç kök şablonu yap", - "set-edge-template-root-rulechain-title": "'{{ruleChainName}}' kural zincirini uç kök şablonu yapmak istediğinizden emin misiniz?", - "set-edge-template-root-rulechain-text": "Onaydan sonra kural zinciri, uç kök şablonu olacak ve yeni oluşturulan uçlar için kök kural zinciri olacaktır.", - "invalid-rulechain-type-error": "Kural zinciri içe aktarılamıyor: Geçersiz kural zinciri türü. Beklenen tür {{expectedRuleChainType}}.", - "set-auto-assign-to-edge": "Oluşturma sırasında uçlara kural zinciri atayın", - "set-auto-assign-to-edge-title": "Oluşturma sırasında uçlara '{{ruleChainName}}' uç kural zincirini atamak istediğinizden emin misiniz?", - "set-auto-assign-to-edge-text": "Onaydan sonra, uç kuralı zinciri, oluşturma sırasında uç(lar)a otomatik olarak atanacaktır.", - "unset-auto-assign-to-edge": "Oluşturma sırasında uç(lar)a kural zinciri atama", - "unset-auto-assign-to-edge-title": "'{{ruleChainName}}' uç kural zincirini oluşturma sırasında uçlara atamak istemediğinizden emin misiniz?", - "unset-auto-assign-to-edge-text": "Onaydan sonra, kenar kuralı zinciri artık oluşturma sırasında uç(lar)a otomatik olarak atanmayacaktır.", - "unassign-rulechain-title": "'{{ruleChainName}}' kural zincirinin atamasını kaldırmak istediğinizden emin misiniz?", - "unassign-rulechains": "Kural zincirlerinin atamasını kaldır" + "search": "Kural zincirlerini ara", + "selected-rulechains": "{ count, plural, =1 {1 kural zinciri} other {# kural zinciri} } seçildi", + "open-rulechain": "Kural zincirini aç", + "edge-template-root": "Şablon kök", + "assign-to-edge": "Edge'e ata", + "edge-rulechain": "Edge kural zinciri", + "unassign-rulechain-from-edge-text": "Onaydan sonra bu kural zinciri edge'den kaldırılacak ve erişilemeyecek.", + "unassign-rulechains-from-edge-title": "{ count, plural, =1 {1 kural zinciri} other {# kural zinciri} } kaldırmak istediğinizden emin misiniz?", + "unassign-rulechains-from-edge-text": "Onaydan sonra seçilen tüm kural zincirleri edge'den kaldırılacak ve erişilemeyecek.", + "assign-rulechain-to-edge-title": "Kural Zinciri(leri)ni Edge'e Ata", + "assign-rulechain-to-edge-text": "Lütfen edge'e atanacak kural zincirlerini seçin", + "set-edge-template-root-rulechain": "Kural zincirini edge şablon kökü yap", + "set-edge-template-root-rulechain-title": "'{{ruleChainName}}' kural zincirini edge şablon kökü yapmak istediğinizden emin misiniz?", + "set-edge-template-root-rulechain-text": "Onaydan sonra bu kural zinciri edge şablon kökü olacak ve yeni oluşturulan edge'ler için kök kural zinciri olacaktır.", + "invalid-rulechain-type-error": "Kural zinciri içe aktarılamadı: Geçersiz kural zinciri türü. Beklenen tür {{expectedRuleChainType}}.", + "set-auto-assign-to-edge": "Oluşturulduğunda kural zincirini edge'e ata", + "set-auto-assign-to-edge-title": "'{{ruleChainName}}' edge kural zincirini edge'e otomatik olarak atamak istediğinizden emin misiniz?", + "set-auto-assign-to-edge-text": "Onaydan sonra bu edge kural zinciri oluşturma sırasında edge'lere otomatik olarak atanacaktır.", + "unset-auto-assign-to-edge": "Oluşturulduğunda kural zincirini edge'e atama", + "unset-auto-assign-to-edge-title": "'{{ruleChainName}}' edge kural zincirini edge'lere otomatik olarak atamamak istediğinizden emin misiniz?", + "unset-auto-assign-to-edge-text": "Onaydan sonra bu edge kural zinciri artık edge'lere otomatik olarak atanmayacaktır.", + "unassign-rulechain-title": "'{{ruleChainName}}' kural zincirini kaldırmak istediğinizden emin misiniz?", + "unassign-rulechains": "Kural zincirlerini kaldır" }, "rulenode": { - "details": "Ayrıntılar", - "events": "Etkinlikler", - "search": "Arama düğümleri", + "rule-node-events": "Kural düğümü olayları", + "details": "Detaylar", + "events": "Olaylar", + "search": "Düğümleri ara", "open-node-library": "Düğüm kütüphanesini aç", + "close-node-library": "Düğüm kütüphanesini kapat", "add": "Kural düğümü ekle", - "name": "Ad", - "name-required": "İsim gerekli.", + "name": "İsim", + "name-required": "İsim gereklidir.", + "name-max-length": "İsim 256 karakterden kısa olmalıdır", "type": "Tür", - "description": "Açıklama", + "rule-node-description": "Kural düğümü açıklaması", "delete": "Kural düğümünü sil", - "select-all-objects": "Tüm düğümleri ve bağlantıları seç", - "deselect-all-objects": "Tüm düğümlerin ve bağlantıların seçimini kaldırın", - "delete-selected-objects": "Seçilen düğümleri ve bağlantıları sil", - "delete-selected": "Silme seçildi", - "select-all": "Hepsini seç", + "select-all-objects": "Tüm düğüm ve bağlantıları seç", + "deselect-all-objects": "Tüm düğüm ve bağlantıların seçimini kaldır", + "delete-selected-objects": "Seçili düğüm ve bağlantıları sil", + "delete-selected": "Seçilenleri sil", + "create-nested-rulechain": "İç içe kural zinciri oluştur", + "select-all": "Tümünü seç", "copy-selected": "Seçilenleri kopyala", - "deselect-all": "Hiçbirini seçme", - "rulenode-details": "Kural düğümü ayrıntıları", + "deselect-all": "Tüm seçimleri kaldır", + "rulenode-details": "Kural düğümü detayları", "debug-mode": "Hata ayıklama modu", + "singleton": "Tekil", "configuration": "Yapılandırma", "link": "Bağlantı", "link-details": "Kural düğüm bağlantı detayları", - "add-link": "Link ekle", + "add-link": "Bağlantı ekle", "link-label": "Bağlantı etiketi", - "link-label-required": "Bağlantı etiketi gerekli.", + "link-label-required": "Bağlantı etiketi gereklidir.", "custom-link-label": "Özel bağlantı etiketi", - "custom-link-label-required": "Özel bağlantı etiketi gerekli.", - "link-labels": "Link etiketleri", - "link-labels-required": "Link etiketleri gerekli.", + "custom-link-label-required": "Özel bağlantı etiketi gereklidir.", + "link-labels": "Bağlantı etiketleri", + "link-labels-required": "Bağlantı etiketleri gereklidir.", "no-link-labels-found": "Bağlantı etiketi bulunamadı", - "no-link-label-matching": "{{label}} bulunamadı. ", + "no-link-label-matching": "'{{label}}' bulunamadı.", "create-new-link-label": "Yeni bir tane oluştur!", "type-filter": "Filtre", - "type-filter-details": "Gelen iletileri yapılandırılmış koşullara göre filtrele", + "type-filter-details": "Gelen mesajları yapılandırılmış koşullara göre filtrele", "type-enrichment": "Zenginleştirme", - "type-enrichment-details": "Mesaj Meta Verilerine ek bilgi", - "type-transformation": "Dönüşüm", - "type-transformation-details": "Mesaj yükünü ve Meta Verileri Değiştir", - "type-action": "Aksiyon", + "type-enrichment-details": "Mesaj Meta verisine ek bilgi ekle", + "type-transformation": "Dönüştürme", + "type-transformation-details": "Mesaj içeriği ve meta veriyi değiştir", + "type-action": "Eylem", "type-action-details": "Özel eylem gerçekleştir", - "type-external": "Dış", - "type-external-details": "Dış sistemle etkileşir", + "type-external": "Harici", + "type-external-details": "Harici sistemle etkileşim kurar", "type-rule-chain": "Kural Zinciri", - "type-rule-chain-details": "Belirtilen Kural Zincirine gelen mesajları ilet", + "type-rule-chain-details": "Gelen mesajları belirtilen Kural Zincirine iletir", + "type-flow": "Akış", + "type-flow-details": "Mesaj akışını düzenler", "type-input": "Giriş", - "type-input-details": "Kural Zinciri'nin mantıksal girdisi, bir sonraki ilgili Kural Düğümüne gelen iletileri iletme", + "type-input-details": "Kural Zincirinin mantıksal girişi, gelen mesajları ilgili kural düğümüne iletir", "type-unknown": "Bilinmeyen", - "type-unknown-details": "Çözümlenmemiş Kural Düğümü", - "directive-is-not-loaded": "Tanımlanmış yapılandırma yönergesi {{directiveName}} 'mevcut değil. ", - "ui-resources-load-error": "Yapılandırma kullanıcı arayüzü kaynakları yüklenemedi.", - "invalid-target-rulechain": "Hedef kural zinciri çözülemiyor!", - "test-script-function": "Test komut dosyası işlevi", + "type-unknown-details": "Çözümlenememiş Kural Düğümü", + "directive-is-not-loaded": "Tanımlı yapılandırma yönergesi '{{directiveName}}' mevcut değil.", + "ui-resources-load-error": "Yapılandırma arayüz kaynakları yüklenemedi.", + "invalid-target-rulechain": "Hedef kural zinciri çözümlenemedi!", + "test-script-function": "Betik fonksiyonunu test et", + "script-lang-java-script": "JavaScript", + "script-lang-tbel": "TBEL", "message": "Mesaj", - "message-type": "Mesaj tipi", - "select-message-type": "Mesaj tipini seç", - "message-type-required": "Mesaj türü gerekli", + "message-type": "Mesaj türü", + "select-message-type": "Mesaj türü seç", + "message-type-required": "Mesaj türü gereklidir", "metadata": "Meta veri", - "metadata-required": "Meta veri girişleri boş bırakılamaz.", + "metadata-required": "Meta veri girdileri boş olamaz.", "output": "Çıktı", - "test": "Ölçek", - "help": "Yardım et" + "test": "Test", + "help": "Yardım", + "reset-debug-settings": "Tüm düğümlerde hata ayıklama ayarlarını sıfırla", + "test-with-this-message": "Bu mesaj ile {{test}}", + "queue-hint": "Mesajı başka bir kuyruğa iletmek için kuyruk seçin. Varsayılan olarak 'Ana' kuyruk kullanılır.", + "queue-singleton-hint": "Çoklu örnek ortamlarında mesaj yönlendirme için bir kuyruk seçin. Varsayılan olarak 'Ana' kuyruk kullanılır." + }, + "rule-node-config": { + "id": "Id", + "additional-info": "Ek Bilgi", + "advanced-settings": "Gelişmiş ayarlar", + "create-entity-if-not-exists": "Varlık yoksa oluştur", + "create-entity-if-not-exists-hint": "Etkinleştirilirse, belirtilen parametrelerle yeni bir varlık oluşturulur; aksi takdirde mevcut varlık kullanılacaktır.", + "select-device-connectivity-event": "Cihaz bağlantı olayını seç", + "entity-name-pattern": "İsim deseni", + "device-name-pattern": "Cihaz adı", + "asset-name-pattern": "Varlık adı", + "entity-view-name-pattern": "Varlık görünümü adı", + "customer-title-pattern": "Müşteri başlığı", + "dashboard-name-pattern": "Gösterge paneli başlığı", + "user-name-pattern": "Kullanıcı e-posta adresi", + "edge-name-pattern": "Edge adı", + "entity-name-pattern-required": "İsim deseni gereklidir", + "entity-name-pattern-hint": "İsim deseni alanı şablon desteği sağlar. Mesajdan değer almak için $[messageKey], meta veriden almak için ${metadataKey} kullanın.", + "copy-message-type": "Mesaj türünü kopyala", + "entity-type-pattern": "Tür deseni", + "entity-type-pattern-required": "Tür deseni gereklidir", + "message-type-value": "Mesaj türü değeri", + "message-type-value-required": "Mesaj türü değeri gereklidir", + "message-type-value-max-length": "Mesaj türü değeri 256 karakterden kısa olmalıdır", + "output-message-type": "Çıkış mesaj türü", + "entity-cache-expiration": "Varlık önbelleği sonlanma süresi (sn)", + "entity-cache-expiration-hint": "Bulunan varlık kayıtlarının tutulabileceği maksimum süreyi belirtir. 0 değeri, kayıtların hiç süresinin dolmayacağı anlamına gelir.", + "entity-cache-expiration-required": "Varlık önbelleği süresi gereklidir.", + "entity-cache-expiration-range": "Varlık önbelleği süresi 0 veya daha büyük olmalıdır.", + "customer-name-pattern": "Müşteri başlığı", + "customer-name-pattern-required": "Müşteri başlığı gereklidir", + "customer-name-pattern-hint": "Mesajdan değer almak için $[messageKey], meta veriden almak için ${metadataKey} kullanın.", + "create-customer-if-not-exists": "Müşteri yoksa oluştur", + "unassign-from-customer": "Kaynak gösterge paneliyse müşteriden ayır", + "unassign-from-customer-tooltip": "Sadece gösterge panelleri birden fazla müşteriye atanabilir. \nMesajın kaynağı gösterge paneliyse, müşteri başlığını belirtmeniz gerekir.", + "customer-cache-expiration": "Müşteri önbelleği sonlanma süresi (sn)", + "customer-cache-expiration-hint": "Bulunan müşteri kayıtlarının tutulabileceği maksimum süreyi belirtir. 0 değeri, kayıtların hiç süresinin dolmayacağı anlamına gelir.", + "customer-cache-expiration-required": "Müşteri önbelleği süresi gereklidir.", + "customer-cache-expiration-range": "Müşteri önbelleği süresi 0 veya daha büyük olmalıdır.", + "interval-start": "Aralık başlangıcı", + "interval-end": "Aralık bitişi", + "time-unit": "Zaman birimi", + "fetch-mode": "Getirme modu", + "order-by-timestamp": "Zamana göre sırala", + "limit": "Limit", + "limit-hint": "Min limit değeri 2, maks - 1000. Tek bir kayıt almak istiyorsanız 'İlk' veya 'Son' getirme modunu seçin.", + "limit-required": "Limit gereklidir.", + "limit-range": "Limit 2 ile 1000 arasında olmalıdır.", + "time-unit-milliseconds": "Milisaniye", + "time-unit-seconds": "Saniye", + "time-unit-minutes": "Dakika", + "time-unit-hours": "Saat", + "time-unit-days": "Gün", + "time-value-range": "İzin verilen aralık 1 ila 2147483647.", + "start-interval-value-required": "Aralık başlangıcı gereklidir.", + "end-interval-value-required": "Aralık bitişi gereklidir.", + "filter": "Filtre", + "switch": "Anahtar", + "math-templatization-tooltip": "Bu alan şablon desteği sağlar. Mesajdan değer almak için $[messageKey], meta veriden almak için ${metadataKey} kullanın.", + "add-message-type": "Mesaj türü ekle", + "select-message-types-required": "En az bir mesaj türü seçilmelidir.", + "select-message-types": "Mesaj türlerini seç", + "no-message-types-found": "Hiçbir mesaj türü bulunamadı", + "no-message-type-matching": "'{{messageType}}' bulunamadı.", + "create-new-message-type": "Yeni bir tane oluştur.", + "message-types-required": "Mesaj türleri gereklidir.", + "client-attributes": "İstemci nitelikleri", + "shared-attributes": "Paylaşılan nitelikler", + "server-attributes": "Sunucu nitelikleri", + "attributes-keys": "Nitelik anahtarları", + "attributes-keys-required": "Nitelik anahtarları gereklidir", + "attributes-scope": "Nitelik kapsamı", + "attributes-scope-value": "Nitelik kapsamı değeri", + "attributes-scope-value-copy": "Nitelik kapsamı değerini kopyala", + "attributes-scope-hint": "'scope' meta veri anahtarını kullanarak her mesaj için nitelik kapsamını dinamik olarak ayarlayın. Sağlanırsa yapılandırmadaki kapsamın yerini alır.", + "notify-device": "Cihaza bildirimi zorla", + "send-attributes-updated-notification": "Güncellenen nitelik bildirimi gönder", + "send-attributes-updated-notification-hint": "Güncellenen nitelikler hakkında ayrı bir mesaj olarak kural motoru kuyruğuna bildirim gönder.", + "send-attributes-deleted-notification": "Silinen nitelik bildirimi gönder", + "send-attributes-deleted-notification-hint": "Silinen nitelikler hakkında ayrı bir mesaj olarak kural motoru kuyruğuna bildirim gönder.", + "update-attributes-only-on-value-change": "Yalnızca değer değişirse nitelikleri kaydet", + "update-attributes-only-on-value-change-hint": "Değeri değişip değişmediğine bakılmaksızın her gelen mesajda nitelikleri günceller. API kullanımını artırır ve performansı düşürür.", + "update-attributes-only-on-value-change-hint-enabled": "Yalnızca nitelik değeri değiştiyse günceller. Değer değişmediyse, zaman damgası veya değişiklik bildirimi gönderilmez.", + "fetch-credentials-to-metadata": "Kimlik bilgilerini meta veriye getir", + "notify-device-on-update-hint": "Etkinleştirilirse, paylaşılan nitelik güncellemesi hakkında cihaza bildirim zorlanır. Devre dışıysa, bildirim davranışı gelen mesajın meta verisindeki 'notifyDevice' parametresiyle kontrol edilir. Bildirimi kapatmak için, 'notifyDevice' parametresi 'false' olarak ayarlanmalıdır. Diğer tüm durumlarda cihaza bildirim gönderilir.", + "notify-device-on-delete-hint": "Etkinleştirilirse, paylaşılan nitelik silinmesi hakkında cihaza bildirim zorlanır. Devre dışıysa, bildirim davranışı gelen mesajın meta verisindeki 'notifyDevice' parametresiyle kontrol edilir. Bildirimi açmak için, 'notifyDevice' parametresi 'true' olarak ayarlanmalıdır. Diğer tüm durumlarda bildirim gönderilmez.", + "latest-timeseries": "En son zaman serisi veri anahtarları", + "timeseries-keys": "Zaman serisi anahtarları", + "timeseries-keys-required": "En az bir zaman serisi anahtarı seçilmelidir.", + "add-timeseries-key": "Zaman serisi anahtarı ekle", + "add-message-field": "Mesaj alanı ekle", + "relation-search-parameters": "İlişki arama parametreleri", + "relation-parameters": "İlişki parametreleri", + "add-metadata-field": "Meta veri alanı ekle", + "data-keys": "Mesaj alanı adları", + "copy-from": "Kopyala", + "data-to-metadata": "Veriyi meta veriye kopyala", + "metadata-to-data": "Meta veriyi veriye kopyala", + "use-regular-expression-hint": "Anahtarları desenle kopyalamak için normal ifade kullanın.\n\nİpuçları:\n'Enter' tuşuna basarak alan adını tamamlayın.\n'Backspace' ile silin. Birden fazla alan adı desteklenir.", + "interval": "Aralık", + "interval-required": "Aralık gereklidir", + "interval-hint": "Çiftleme önleme aralığı (saniye cinsinden).", + "interval-min-error": "İzin verilen minimum değer 1", + "max-pending-msgs": "Maksimum bekleyen mesaj", + "max-pending-msgs-hint": "Her benzersiz çiftleme kimliği için bellekte saklanabilecek maksimum mesaj sayısı.", + "max-pending-msgs-required": "Maksimum bekleyen mesaj sayısı gereklidir", + "max-pending-msgs-max-error": "İzin verilen maksimum değer 1000", + "max-pending-msgs-min-error": "İzin verilen minimum değer 1", + "max-retries": "Maksimum yeniden deneme", + "max-retries-required": "Maksimum yeniden deneme sayısı gereklidir", + "max-retries-hint": "Çiftlenmemiş mesajları kuyruğa itmek için maksimum yeniden deneme sayısı. Yeniden denemeler arasında 10 saniyelik gecikme kullanılır", + "max-retries-max-error": "İzin verilen maksimum değer 100", + "max-retries-min-error": "İzin verilen minimum değer 0", + "strategy": "Strateji", + "strategy-required": "Strateji gereklidir", + "strategy-all-hint": "Çiftleme önleme süresi boyunca gelen tüm mesajları tek bir JSON dizi mesajı olarak döndürür. Her öğe, msg ve metadata alt özelliklerine sahip bir nesneyi temsil eder.", + "strategy-first-hint": "Çiftleme önleme süresi boyunca ilk gelen mesajı döndürür.", + "strategy-last-hint": "Çiftleme önleme süresi boyunca son gelen mesajı döndürür.", + "first": "İlk", + "last": "Son", + "all": "Tümü", + "output-msg-type-hint": "Çiftleme sonucu mesajın türü.", + "queue-name-hint": "Çiftleme sonucu mesajın yayınlanacağı kuyruk adı.", + "keys": "Anahtarlar", + "keys-required": "Anahtarlar gereklidir", + "rename-keys-in": "Anahtarları yeniden adlandır", + "data": "Veri", + "message": "Mesaj", + "metadata": "Meta veri", + "current-key-name": "Geçerli anahtar adı", + "key-name-required": "Anahtar adı gereklidir", + "new-key-name": "Yeni anahtar adı", + "new-key-name-required": "Yeni anahtar adı gereklidir", + "metadata-keys": "Meta veri alan adları", + "json-path-expression": "JSON yol ifadesi", + "json-path-expression-required": "JSON yol ifadesi gereklidir", + "json-path-expression-hint": "JSONPath, bir JSON yapısındaki öğelere veya öğe kümelerine giden yolu belirtir. '$' kök nesneyi veya diziyi temsil eder.", + "relations-query": "İlişki sorgusu", + "device-relations-query": "Cihaz ilişkisi sorgusu", + "max-relation-level": "Maksimum ilişki seviyesi", + "max-relation-level-error": "Değer 0'dan büyük veya belirtilmemiş olmalıdır.", + "max-relation-level-invalid": "Değer bir tamsayı olmalıdır.", + "relation-type": "İlişki türü", + "relation-type-pattern": "İlişki türü deseni", + "relation-type-pattern-required": "İlişki türü deseni gereklidir", + "relation-types-list": "Yayılacak ilişki türleri", + "relation-types-list-hint": "Yayılacak ilişki türleri seçilmezse, alarmlar ilişki türüne göre filtrelenmeden yayılır.", + "unlimited-level": "Sınırsız seviye", + "latest-telemetry": "En son telemetri", + "add-telemetry-key": "Telemetri anahtarı ekle", + "delete-from": "Şuradan sil", + "use-regular-expression-delete-hint": "Anahtarları desenle silmek için normal ifade kullanın.\n\nİpuçları:\nAlan adını tamamlamak için 'Enter' tuşuna basın.\nSilmek için 'Backspace' tuşuna basın.\nBirden fazla alan adı desteklenir.", + "fetch-into": "Şuraya getir", + "attr-mapping": "Nitelik eşlemesi:", + "source-attribute": "Kaynak nitelik anahtarı", + "source-attribute-required": "Kaynak nitelik anahtarı gereklidir.", + "source-telemetry": "Kaynak telemetri anahtarı", + "source-telemetry-required": "Kaynak telemetri anahtarı gereklidir.", + "target-key": "Hedef anahtar", + "target-key-required": "Hedef anahtar gereklidir.", + "attr-mapping-required": "En az bir eşleme girişi belirtilmelidir.", + "fields-mapping": "Alan eşlemesi", + "fields-mapping-hint": "Mesaj alanı $entityId olarak ayarlanmışsa, mesaj kaynağının kimliği belirtilen tablo sütununa kaydedilecektir.", + "relations-query-config-direction-suffix": "kaynak", + "profile-name": "Profil adı", + "fetch-circle-parameter-info-from-metadata-hint": "Meta veri alanı '{{perimeterKeyName}}' şu biçimde tanımlanmalıdır: {\"latitude\":48.196, \"longitude\":24.6532, \"radius\":100.0, \"radiusUnit\":\"METER\"}", + "fetch-poligon-parameter-info-from-metadata-hint": "Meta veri alanı '{{perimeterKeyName}}' şu biçimde tanımlanmalıdır: [[48.19736,24.65235],[48.19800,24.65060],...,[48.19849,24.65420]]", + "short-templatization-tooltip": "Mesajdan değer almak için $[messageKey], meta veriden almak için ${metadataKey} kullanın.", + "fields-mapping-required": "En az bir alan eşlemesi belirtilmelidir.", + "at-least-one-field-required": "En az bir giriş alanı değeri sağlanmalıdır.", + "originator-fields-sv-map-hint": "Hedef anahtar alanları şablon desteği sağlar. Mesajdan değer almak için $[messageKey], meta veriden almak için ${metadataKey} kullanın.", + "sv-map-hint": "Yalnızca hedef anahtar alanları şablon desteği sağlar. Mesajdan değer almak için $[messageKey], meta veriden almak için ${metadataKey} kullanın.", + "source-field": "Kaynak alan", + "source-field-required": "Kaynak alan gereklidir.", + "originator-source": "Kaynak belirleyici", + "new-originator": "Yeni belirleyici", + "originator-customer": "Müşteri", + "originator-tenant": "Kiracı", + "originator-related": "İlişkili varlık", + "originator-alarm-originator": "Alarm Kaynağı", + "originator-entity": "Ad desenine göre varlık", + "clone-message": "Mesajı klonla", + "transform": "Dönüştür", + "default-ttl": "Varsayılan TTL", + "default-ttl-required": "Varsayılan TTL gereklidir.", + "default-ttl-hint": "Kural düğümü, TTL (Geçerlilik süresi) değerini mesajın meta verilerinden alır. Eğer bir değer yoksa, yapılandırmadaki varsayılan TTL kullanılır. Değer 0 ise, kiracı profilindeki TTL uygulanır.", + "default-ttl-zero-hint": "Değer 0 ise TTL uygulanmaz.", + "min-default-ttl-message": "Yalnızca minimum 0 TTL değeri kabul edilir.", + "generation-parameters": "Oluşturma parametreleri", + "message-count": "Üretilen mesaj limiti (0 - sınırsız)", + "message-count-required": "Üretilen mesaj limiti gereklidir.", + "min-message-count-message": "Yalnızca minimum 0 mesaj sayısı kabul edilir.", + "period-seconds": "Periyot (saniye)", + "period-seconds-required": "Periyot gereklidir.", + "generation-frequency-seconds": "Oluşturma sıklığı (saniye)", + "generation-frequency-required": "Oluşturma sıklığı gereklidir.", + "min-generation-frequency-message": "Minimum 1 saniye gereklidir.", + "script-lang-tbel": "TBEL", + "script-lang-js": "JS", + "use-metadata-period-in-seconds-patterns": "Saniye cinsinden periyot desenini kullan", + "use-metadata-period-in-seconds-patterns-hint": "Seçilirse, kural düğümü mesaj meta verilerinden veya verilerden alınan saniye cinsinden periyot desenini kullanır.", + "period-in-seconds-pattern": "Saniye cinsinden periyot deseni", + "period-in-seconds-pattern-required": "Saniye cinsinden periyot deseni gereklidir", + "min-period-seconds-message": "Minimum 1 saniyelik periyot gereklidir.", + "originator": "Kaynak", + "message-body": "Mesaj içeriği", + "message-metadata": "Mesaj meta verisi", + "generate": "Oluştur", + "current-rule-node": "Geçerli Kural Düğümü", + "current-tenant": "Geçerli Kiracı", + "generator-function": "Oluşturucu fonksiyon", + "test-generator-function": "Oluşturucu fonksiyonu test et", + "generator": "Oluşturucu", + "test-filter-function": "Filtre fonksiyonunu test et", + "test-switch-function": "Anahtar fonksiyonunu test et", + "test-transformer-function": "Dönüştürücü fonksiyonunu test et", + "transformer": "Dönüştürücü", + "alarm-create-condition": "Alarm oluşturma koşulu", + "test-condition-function": "Koşul fonksiyonunu test et", + "alarm-clear-condition": "Alarm temizleme koşulu", + "alarm-details-builder": "Alarm detayları oluşturucu", + "test-details-function": "Detay fonksiyonunu test et", + "alarm-type": "Alarm türü", + "select-entity-types": "Varlık türlerini seç", + "alarm-type-required": "Alarm türü gereklidir.", + "alarm-severity": "Alarm şiddeti", + "alarm-severity-required": "Alarm şiddeti gereklidir", + "alarm-severity-pattern": "Alarm şiddeti deseni", + "alarm-status-filter": "Alarm durumu filtresi", + "alarm-status-list-empty": "Alarm durumu listesi boş", + "no-alarm-status-matching": "Eşleşen alarm durumu bulunamadı.", + "propagate": "Alarmı ilişkili varlıklara yay", + "propagate-to-owner": "Alarmı varlık sahibine (Müşteri veya Kiracı) yay", + "propagate-to-tenant": "Alarmı kiracıya yay", + "condition": "Koşul", + "details": "Detaylar", + "to-string": "Metne dönüştür", + "test-to-string-function": "Metne dönüştürme fonksiyonunu test et", + "from-template": "Kimden", + "from-template-required": "Kimden alanı gereklidir", + "message-to-metadata": "Mesajdan meta veriye", + "metadata-to-message": "Meta veriden mesaja", + "from-message": "Mesajdan", + "from-metadata": "Meta veriden", + "to-template": "Kime", + "to-template-required": "Kime Şablonu gereklidir", + "mail-address-list-template-hint": "Virgül ile ayrılmış adres listesi, meta veriden değer almak için ${metadataKey}, mesajdan değer almak için $[messageKey] kullanın", + "cc-template": "Cc", + "bcc-template": "Bcc", + "subject-template": "Konu", + "subject-template-required": "Konu Şablonu gereklidir", + "body-template": "İçerik", + "body-template-required": "İçerik Şablonu gereklidir", + "dynamic-mail-body-type": "Dinamik e-posta içeriği türü", + "mail-body-type": "E-posta içeriği türü", + "body-type-template": "İçerik türü şablonu", + "reply-routing-configuration": "Yanıt Yönlendirme Yapılandırması", + "rpc-reply-routing-configuration-hint": "Bu yapılandırma parametreleri, yanıtı geri göndermek için kullanılan hizmet, oturum ve istek kimliklerini tanımlayan meta veri anahtar adlarını belirtir.", + "reply-routing-configuration-hint": "Bu yapılandırma parametreleri, yanıtı geri göndermek için hizmet ve istek kimliklerini tanımlayan meta veri anahtar adlarını belirtir.", + "request-id-metadata-attribute": "İstek Kimliği", + "service-id-metadata-attribute": "Hizmet Kimliği", + "session-id-metadata-attribute": "Oturum Kimliği", + "timeout-sec": "Zaman aşımı (saniye)", + "timeout-required": "Zaman aşımı gereklidir", + "min-timeout-message": "Yalnızca minimum 0 zaman aşımı değeri kabul edilir.", + "endpoint-url-pattern": "Uç nokta URL deseni", + "endpoint-url-pattern-required": "Uç nokta URL deseni gereklidir", + "request-method": "İstek yöntemi", + "use-simple-client-http-factory": "Basit HTTP istemci fabrikasını kullan", + "ignore-request-body": "İstek içeriği olmadan", + "parse-to-plain-text": "Düz metne dönüştür", + "parse-to-plain-text-hint": "Seçildiğinde, istek içeriği JSON dizisinden düz metne dönüştürülür, örn. msg = \"Hello,\\t\"world\"\" will be parsed to Hello, \"world\"", + "read-timeout": "Okuma zaman aşımı (milisaniye)", + "read-timeout-hint": "0 değeri, sınırsız zaman aşımı anlamına gelir", + "max-parallel-requests-count": "Maksimum paralel istek sayısı", + "max-parallel-requests-count-hint": "0 değeri, paralel işlemeye sınırsız izin verir", + "max-response-size": "Maksimum yanıt boyutu (KB)", + "max-response-size-hint": "HTTP mesajlarını çözümlerken/şifrelerken ayrılacak maksimum bellek miktarı (örneğin JSON veya XML yükleri için)", + "headers": "Başlıklar", + "headers-hint": "Başlık/değer alanlarında meta veriden değer almak için ${metadataKey}, mesaj içeriğinden değer almak için $[messageKey] kullanın", + "header": "Başlık", + "header-required": "Başlık gereklidir", + "value": "Değer", + "value-required": "Değer gereklidir", + "topic-pattern": "Konu deseni", + "key-pattern": "Anahtar deseni", + "key-pattern-hint": "İsteğe bağlı. Geçerli bir bölüm numarası belirtilirse kayıt gönderilirken kullanılır. Belirtilmemişse anahtar kullanılır. Her ikisi de belirtilmemişse round-robin yöntemi kullanılır.", + "topic-pattern-required": "Konu deseni gereklidir", + "topic": "Konu", + "topic-required": "Konu gereklidir", + "bootstrap-servers": "Başlangıç sunucuları", + "bootstrap-servers-required": "Başlangıç sunucuları değeri gereklidir", + "other-properties": "Diğer özellikler", + "key": "Anahtar", + "key-required": "Anahtar gereklidir", + "retries": "Başarısızlık durumunda otomatik tekrar deneme sayısı", + "min-retries-message": "Yalnızca minimum 0 tekrar denemesi kabul edilir.", + "batch-size-bytes": "Üretici toplu işlem boyutu (bayt)", + "min-batch-size-bytes-message": "Yalnızca minimum 0 toplu işlem boyutu kabul edilir.", + "linger-ms": "Yerel arabellekte bekletme süresi (ms)", + "min-linger-ms-message": "Yalnızca minimum 0 ms değeri kabul edilir.", + "buffer-memory-bytes": "İstemci tamponu maksimum boyutu (bayt)", + "min-buffer-memory-message": "Yalnızca minimum 0 tampon boyutu kabul edilir.", + "memory-buffer-size-range": "Bellek tampon boyutu 0 ile {{max}} KB arasında olmalıdır", + "acks": "Onay sayısı", + "topic-arn-pattern": "Konu ARN deseni", + "topic-arn-pattern-required": "Konu ARN deseni gereklidir", + "aws-access-key-id": "AWS Erişim Anahtarı Kimliği", + "aws-access-key-id-required": "AWS Erişim Anahtarı Kimliği gereklidir", + "aws-secret-access-key": "AWS Gizli Erişim Anahtarı", + "aws-secret-access-key-required": "AWS Gizli Erişim Anahtarı gereklidir", + "aws-region": "AWS Bölgesi", + "aws-region-required": "AWS Bölgesi gereklidir", + "exchange-name-pattern": "Exchange adı deseni", + "routing-key-pattern": "Yönlendirme anahtarı deseni", + "message-properties": "Mesaj özellikleri", + "host": "Sunucu", + "host-required": "Sunucu gereklidir", + "port": "Port", + "port-required": "Port gereklidir", + "port-range": "Port, 1 ile 65535 arasında olmalıdır.", + "virtual-host": "Sanal sunucu", + "username": "Kullanıcı adı", + "password": "Parola", + "automatic-recovery": "Otomatik kurtarma", + "connection-timeout-ms": "Bağlantı zaman aşımı (ms)", + "min-connection-timeout-ms-message": "Yalnızca minimum 0 ms değeri kabul edilir.", + "handshake-timeout-ms": "El sıkışma zaman aşımı (ms)", + "min-handshake-timeout-ms-message": "Yalnızca minimum 0 ms değeri kabul edilir.", + "client-properties": "İstemci özellikleri", + "queue-url-pattern": "Kuyruk URL deseni", + "queue-url-pattern-required": "Kuyruk URL deseni gereklidir", + "delay-seconds": "Gecikme (saniye)", + "min-delay-seconds-message": "Yalnızca minimum 0 saniye değeri kabul edilir.", + "max-delay-seconds-message": "Yalnızca maksimum 900 saniye değeri kabul edilir.", + "name": "Ad", + "name-required": "Ad gereklidir", + "queue-type": "Kuyruk türü", + "sqs-queue-standard": "Standart", + "sqs-queue-fifo": "FIFO", + "gcp-project-id": "GCP proje kimliği", + "gcp-project-id-required": "GCP proje kimliği gereklidir", + "gcp-service-account-key": "GCP servis hesabı anahtar dosyası", + "gcp-service-account-key-required": "GCP servis hesabı anahtar dosyası gereklidir", + "pubsub-topic-name": "Konu adı", + "pubsub-topic-name-required": "Konu adı gereklidir", + "message-attributes": "Mesaj öznitelikleri", + "message-attributes-hint": "Ad/değer alanlarında meta veriden değer almak için ${metadataKey}, mesaj içeriğinden değer almak için $[messageKey] kullanın", + "connect-timeout": "Bağlantı zaman aşımı (saniye)", + "connect-timeout-required": "Bağlantı zaman aşımı gereklidir.", + "connect-timeout-range": "Bağlantı zaman aşımı 1 ile 200 saniye arasında olmalıdır.", + "client-id": "İstemci Kimliği", + "client-id-hint": "İsteğe bağlı. Otomatik oluşturulan istemci kimliği için boş bırakın. Belirli bir istemci kimliği kullanırken dikkatli olun. Çoğu MQTT sunucusu aynı kimlik ile birden fazla bağlantıya izin vermez. Platform mikro servis modunda çalışırken kural düğümünün kopyaları birden fazla servis üzerinde çalışır, bu da aynı kimlikle birden fazla bağlantıya yol açar ve başarısızlıklara neden olabilir. Bu durumu önlemek için aşağıdaki \"İstemci Kimliğine Servis Kimliği ekle\" seçeneğini etkinleştirin.", + "append-client-id-suffix": "İstemci Kimliğine Servis Kimliği ekle", + "client-id-suffix-hint": "İsteğe bağlı. Yalnızca \"İstemci Kimliği\" açıkça belirtildiğinde geçerlidir. Seçildiğinde, Servis Kimliği istemci kimliğine son ek olarak eklenir. Platform mikro servis modunda çalışırken başarısızlıkların önüne geçmek için kullanılır.", + "device-id": "Cihaz Kimliği", + "device-id-required": "Cihaz Kimliği gereklidir.", + "clean-session": "Temiz oturum", + "enable-ssl": "SSL'i etkinleştir", + "credentials": "Kimlik bilgileri", + "credentials-type": "Kimlik bilgisi türü", + "credentials-type-required": "Kimlik bilgisi türü gereklidir.", + "credentials-anonymous": "Anonim", + "credentials-basic": "Temel", + "credentials-pem": "PEM", + "credentials-pem-hint": "En azından Sunucu CA sertifikası veya İstemci sertifikası ile İstemci özel anahtar dosyalarının bir çiftine ihtiyaç vardır", + "credentials-sas": "Paylaşılan Erişim İmzası", + "sas-key": "SAS Anahtarı", + "sas-key-required": "SAS Anahtarı gereklidir.", + "hostname": "Sunucu adı", + "hostname-required": "Sunucu adı gereklidir.", + "azure-ca-cert": "CA sertifika dosyası", + "username-required": "Kullanıcı adı gereklidir.", + "password-required": "Parola gereklidir.", + "ca-cert": "Sunucu CA sertifika dosyası", + "private-key": "İstemci özel anahtar dosyası", + "cert": "İstemci sertifika dosyası", + "no-file": "Dosya seçilmedi.", + "drop-file": "Bir dosya bırakın veya yüklemek için tıklayın.", + "private-key-password": "Özel anahtar parolası", + "use-system-smtp-settings": "Sistem SMTP ayarlarını kullan", + "use-metadata-dynamic-interval": "Dinamik aralığı kullan", + "metadata-dynamic-interval-hint": "Başlangıç ve bitiş aralığı alanları şablonlaştırmayı destekler. Şablon değeri milisaniye cinsinden olmalıdır. Mesajdan değer almak için $[messageKey], metaveriden almak için ${metadataKey} kullanın.", + "use-metadata-interval-patterns-hint": "Seçilirse, başlangıç ve bitiş aralığı desenleri mesaj metaverisi veya verilerinden milisaniye olarak kullanılır.", + "use-message-alarm-data": "Mesaj alarm verilerini kullan", + "overwrite-alarm-details": "Alarm ayrıntılarını üzerine yaz", + "use-alarm-severity-pattern": "Alarm şiddeti desenini kullan", + "check-all-keys": "Tüm belirtilen alanların mevcut olduğunu kontrol et", + "check-all-keys-hint": "Seçilirse, tüm belirtilen anahtarların mesaj verisinde ve metaverisinde bulunduğu kontrol edilir.", + "check-relation-to-specific-entity": "Belirli varlıkla ilişkiyi kontrol et", + "check-relation-to-specific-entity-tooltip": "Etkinleştirilirse, belirli bir varlıkla ilişkinin varlığı kontrol edilir; aksi takdirde herhangi bir varlıkla ilişki kontrol edilir. Her iki durumda da ilişki yön ve türüne göre aranır.", + "check-relation-hint": "Belirli bir varlıkla ya da herhangi bir varlıkla yön ve ilişki türüne göre ilişki varlığını kontrol eder.", + "delete-relation-with-specific-entity": "Belirli varlıkla ilişkiyi sil", + "delete-relation-with-specific-entity-hint": "Etkinleştirilirse, yalnızca belirli bir varlıkla olan ilişki silinir. Aksi takdirde, eşleşen tüm varlıklarla ilişkiler silinir.", + "delete-relation-hint": "Gelen mesajın kaynağı ile belirtilen varlık(lar) arasında yön ve tür temelinde ilişkiyi siler.", + "remove-current-relations": "Mevcut ilişkileri kaldır", + "remove-current-relations-hint": "Gelen mesajın kaynağıyla mevcut ilişkileri yön ve tür temelinde kaldırır.", + "change-originator-to-related-entity": "Kaynağı ilgili varlıkla değiştir", + "change-originator-to-related-entity-hint": "Gönderilen mesajı başka bir varlıktan gelen bir mesaj gibi işlemek için kullanılır.", + "start-interval": "Başlangıç aralığı", + "end-interval": "Bitiş aralığı", + "start-interval-required": "Başlangıç aralığı gereklidir.", + "end-interval-required": "Bitiş aralığı gereklidir.", + "smtp-protocol": "Protokol", + "smtp-host": "SMTP sunucusu", + "smtp-host-required": "SMTP sunucusu gereklidir.", + "smtp-port": "SMTP portu", + "smtp-port-required": "SMTP portu girilmelidir.", + "smtp-port-range": "SMTP portu 1 ile 65535 arasında olmalıdır.", + "timeout-msec": "Zaman aşımı (ms)", + "min-timeout-msec-message": "Yalnızca minimum 0 ms değeri kabul edilir.", + "enter-username": "Kullanıcı adı girin", + "enter-password": "Parola girin", + "enable-tls": "TLS'i etkinleştir", + "tls-version": "TLS sürümü", + "enable-proxy": "Proxy'yi etkinleştir", + "use-system-proxy-properties": "Sistem proxy özelliklerini kullan", + "proxy-host": "Proxy sunucusu", + "proxy-host-required": "Proxy sunucusu gereklidir.", + "proxy-port": "Proxy portu", + "proxy-port-required": "Proxy portu gereklidir.", + "proxy-port-range": "Proxy portu 1 ile 65535 arasında olmalıdır.", + "proxy-user": "Proxy kullanıcısı", + "proxy-password": "Proxy parolası", + "proxy-scheme": "Proxy şeması", + "numbers-to-template": "Telefon Numaraları Şablonu", + "numbers-to-template-required": "Telefon Numaraları Şablonu gereklidir", + "numbers-to-template-hint": "Virgülle ayrılmış telefon numaraları, metaveriden değer almak için ${metadataKey}, mesaj gövdesinden almak için $[messageKey] kullanın", + "sms-message-template": "SMS mesaj şablonu", + "sms-message-template-required": "SMS mesaj şablonu gereklidir", + "use-system-sms-settings": "Sistem SMS sağlayıcı ayarlarını kullan", + "min-period-0-seconds-message": "Yalnızca minimum 0 saniyelik süreye izin verilir.", + "max-pending-messages": "Maksimum bekleyen mesaj", + "max-pending-messages-required": "Maksimum bekleyen mesaj gereklidir.", + "max-pending-messages-range": "Maksimum bekleyen mesaj 1 ile 100000 arasında olmalıdır.", + "originator-types-filter": "Gönderici türleri filtresi", + "interval-seconds": "Zaman aralığı (saniye)", + "interval-seconds-required": "Zaman aralığı gereklidir.", + "int-range": "Değer, maksimum tamsayı sınırını (2147483648) aşmamalıdır", + "min-interval-seconds-message": "En az 1 saniyelik zaman aralığına izin verilir.", + "output-timeseries-key-prefix": "Çıkış zaman serisi anahtar ön eki", + "output-timeseries-key-prefix-required": "Çıkış zaman serisi anahtar ön eki gereklidir.", + "separator-hint": "Alan girişini tamamlamak için \"Enter\" tuşuna basmalısınız.", + "select-details": "Detayları seç", + "entity-details-id": "Id", + "entity-details-title": "Başlık", + "entity-details-country": "Ülke", + "entity-details-state": "Eyalet", + "entity-details-city": "Şehir", + "entity-details-zip": "Posta Kodu", + "entity-details-address": "Adres", + "entity-details-address2": "Adres2", + "entity-details-additional_info": "Ek Bilgi", + "entity-details-phone": "Telefon", + "entity-details-email": "E-posta", + "email-sender": "E-posta gönderen", + "fields-to-check": "Kontrol edilecek alanlar", + "add-detail": "Detay ekle", + "check-all-keys-tooltip": "Etkinleştirilirse, gelen mesaj ve metadata içindeki tüm alan adlarının varlığı kontrol edilir.", + "fields-to-check-hint": "Alan adını tamamlamak için \"Enter\" tuşuna basın. Birden fazla alan adı desteklenir.", + "entity-details-list-empty": "En az bir detay seçilmelidir.", + "alarm-status": "Alarm durumu", + "alarm-required": "En az bir alarm durumu seçilmelidir.", + "no-entity-details-matching": "Eşleşen varlık detayı bulunamadı.", + "custom-table-name": "Özel tablo adı", + "custom-table-name-required": "Tablo adı gereklidir", + "custom-table-hint": "Tablo Cassandra kümenizde oluşturulmuş olmalı ve adı 'cs_tb_' önekiyle başlamalıdır. Ortak TB tablolarına veri eklenmesini önlemek için buraya yalnızca önek olmadan tablo adını girin.", + "message-field": "Mesaj alanı", + "message-field-required": "Mesaj alanı gereklidir.", + "table-col": "Tablo sütunu", + "table-col-required": "Tablo sütunu gereklidir.", + "latitude-field-name": "Enlem alan adı", + "longitude-field-name": "Boylam alan adı", + "latitude-field-name-required": "Enlem alan adı gereklidir.", + "longitude-field-name-required": "Boylam alan adı gereklidir.", + "fetch-perimeter-info-from-metadata": "Çevre bilgilerini metadata'dan al", + "fetch-perimeter-info-from-metadata-tooltip": "Çevre türü 'Polygon' olarak ayarlanmışsa, '{{perimeterKeyName}}' metadata alanının değeri ek işlem olmadan çevre tanımı olarak kullanılır. 'Circle' olarak ayarlanmışsa, bu metadata değeri 'latitude', 'longitude', 'radius', 'radiusUnit' alanlarına ayrıştırılır.", + "perimeter-key-name": "Çevre anahtar adı", + "perimeter-key-name-hint": "Çevre bilgilerini içeren metadata alan adı.", + "perimeter-key-name-required": "Çevre anahtar adı gereklidir.", + "perimeter-circle": "Daire", + "perimeter-polygon": "Poligon", + "perimeter-type": "Çevre türü", + "circle-center-latitude": "Merkez enlemi", + "circle-center-latitude-required": "Merkez enlemi gereklidir.", + "circle-center-longitude": "Merkez boylamı", + "circle-center-longitude-required": "Merkez boylamı gereklidir.", + "range-unit-meter": "Metre", + "range-unit-kilometer": "Kilometre", + "range-unit-foot": "Fit", + "range-unit-mile": "Mil", + "range-unit-nautical-mile": "Deniz mili", + "range-units": "Menzil birimi", + "range-units-required": "Menzil birimi gereklidir.", + "range": "Menzil", + "range-required": "Menzil gereklidir.", + "polygon-definition": "Poligon tanımı", + "polygon-definition-required": "Poligon tanımı gereklidir.", + "polygon-definition-hint": "Poligonu manuel tanımlamak için şu formatı kullanın: [[lat1,lon1],[lat2,lon2], ... ,[latN,lonN]].", + "min-inside-duration": "Minimum içeride kalma süresi", + "min-inside-duration-value-required": "Minimum içeride kalma süresi gereklidir", + "min-inside-duration-time-unit": "Minimum içeride kalma süresi birimi", + "min-outside-duration": "Minimum dışarıda kalma süresi", + "min-outside-duration-value-required": "Minimum dışarıda kalma süresi gereklidir", + "min-outside-duration-time-unit": "Minimum dışarıda kalma süresi birimi", + "tell-failure-if-absent": "Hata bildir", + "tell-failure-if-absent-hint": "Seçilen anahtarlardan en az biri mevcut değilse, çıkan mesaj 'Hata' olarak raporlanacaktır.", + "get-latest-value-with-ts": "En son telemetri değerleri için zaman damgasını al", + "get-latest-value-with-ts-hint": "Seçildiğinde, en son telemetri değerleri zaman damgası ile birlikte verilir, örn: \"temp\": \"{\"ts\":1574329385897, \"value\":42}\"", + "ignore-null-strings": "Boş metinleri yok say", + "ignore-null-strings-hint": "Seçildiğinde, değeri boş olan varlık alanları yok sayılır.", + "add-metadata-key-values-as-kafka-headers": "Mesaj metadata anahtar-değer çiftlerini Kafka başlıklarına ekle", + "add-metadata-key-values-as-kafka-headers-hint": "Seçildiğinde, mesaj metadata'sından gelen anahtar-değer çiftleri, ön tanımlı karakter seti ile bayt dizisi olarak Kafka başlıklarına eklenir.", + "charset-encoding": "Karakter kodlaması", + "charset-encoding-required": "Karakter kodlaması gereklidir.", + "charset-us-ascii": "US-ASCII", + "charset-iso-8859-1": "ISO-8859-1", + "charset-utf-8": "UTF-8", + "charset-utf-16be": "UTF-16BE", + "charset-utf-16le": "UTF-16LE", + "charset-utf-16": "UTF-16", + "select-queue-hint": "Kuyruk adı açılır listeden seçilebilir veya özel bir ad girilebilir.", + "device-profile-node-hint": "Alarm durumunun sürekliliğini sağlamak için süreli veya tekrarlanan koşullarda faydalıdır.", + "persist-alarm-rules": "Alarm kurallarının durumunu sakla", + "persist-alarm-rules-hint": "Etkinleştirildiğinde, kural düğümü işlem durumunu veritabanına kaydeder.", + "fetch-alarm-rules": "Alarm kurallarının durumunu al", + "fetch-alarm-rules-hint": "Etkinleştirildiğinde, kural düğümü başlatıldığında işlem durumunu geri yükler ve sunucu yeniden başlatılsa bile alarmların oluşturulmasını sağlar. Aksi halde, durum ilk cihaz mesajıyla geri yüklenir.", + "input-value-key": "Girdi değer anahtarı", + "input-value-key-required": "Girdi değer anahtarı gereklidir.", + "output-value-key": "Çıktı değer anahtarı", + "output-value-key-required": "Çıktı değer anahtarı gereklidir.", + "number-of-digits-after-floating-point": "Ondalık noktadan sonraki basamak sayısı", + "number-of-digits-after-floating-point-range": "Ondalık noktadan sonraki basamak sayısı 0 ile 15 arasında olmalıdır.", + "failure-if-delta-negative": "Delta negatifse Hata bildir", + "failure-if-delta-negative-tooltip": "Delta değeri negatifse, kural düğümü mesaj işlemini başarısız olarak işaretler.", + "use-caching": "Önbellek kullan", + "use-caching-tooltip": "Kural düğümü gelen mesajdan gelen \"{{inputValueKey}}\" değerini önbelleğe alarak performansı artırır. Ancak, bu değer başka bir yerde değiştirildiyse önbellek güncellenmez.", + "add-time-difference-between-readings": "\"{{inputValueKey}}\" okumaları arasındaki süre farkını ekle", + "add-time-difference-between-readings-tooltip": "Etkinleştirildiğinde, kural düğümü çıkış mesajına \"{{periodValueKey}}\" ekler.", + "period-value-key": "Periyot değer anahtarı", + "period-value-key-required": "Periyot değer anahtarı gereklidir.", + "general-pattern-hint": "Metadata'dan değer almak için ${metadataKey}, mesaj gövdesinden değer almak için $[messageKey] kullanın.", + "alarm-severity-pattern-hint": "Metadata'dan değer almak için ${metadataKey}, mesaj gövdesinden değer almak için $[messageKey] kullanın. Alarm şiddeti sistem seviyesinde olmalıdır (CRITICAL, MAJOR vb.)", + "output-node-name-hint": "Kural düğümü adı, çağıran kural zincirindeki diğer düğümlere mesaj yönlendirmek için kullanılan ilişki türü ile eşleşir.", + "use-server-ts": "Sunucu zaman damgası kullan", + "use-server-ts-hint": "Zaman damgası olmayan zaman serisi verileri için sunucunun mevcut zaman damgasını kullanır. Bu, farklı kaynaklardan gelen mesajların sıralı işlenmesini sağlar.", + "kv-map-pattern-hint": "Tüm giriş alanları templatization destekler. Mesajdan değer almak için $[messageKey], metadata'dan almak için ${metadataKey} kullanın.", + "kv-map-single-pattern-hint": "Giriş alanı templatization destekler. Mesajdan değer almak için $[messageKey], metadata'dan almak için ${metadataKey} kullanın.", + "shared-scope": "Paylaşılan kapsam", + "server-scope": "Sunucu kapsamı", + "client-scope": "İstemci kapsamı", + "attribute-type": "Özellik", + "attribute-type-description": "Veritabanından özellik değeri al", + "attribute-type-result-description": "Sonucu veritabanına varlık özelliği olarak kaydet", + "constant-type": "Sabit", + "constant-type-description": "Sabit değer tanımla", + "time-series-type": "Zaman serisi", + "time-series-type-description": "Veritabanından en son zaman serisi değerini al", + "time-series-type-result-description": "Sonucu veritabanına varlık zaman serisi olarak kaydet", + "message-body-type": "Mesaj", + "message-body-type-description": "Gelen mesajdan argüman değerini al", + "message-body-type-result-description": "Sonucu giden mesaja ekle", + "message-metadata-type": "Metadata", + "message-metadata-type-description": "Gelen mesaj metadata’sından argüman değerini al", + "message-metadata-result-description": "Sonucu giden mesaj metadata’sına ekle", + "argument-tile": "Argümanlar", + "no-arguments-prompt": "Yapılandırılmış argüman yok", + "result-title": "Sonuç", + "functions-field-input": "Fonksiyonlar", + "no-option-found": "Seçenek bulunamadı", + "argument-source-field-input": "Kaynak", + "argument-source-field-input-required": "Argüman kaynağı gereklidir.", + "argument-key-field-input": "Anahtar", + "argument-key-field-input-required": "Argüman anahtarı gereklidir.", + "constant-value-field-input": "Sabit değer", + "constant-value-field-input-required": "Sabit değer gereklidir.", + "attribute-scope-field-input": "Özellik kapsamı", + "attribute-scope-field-input-required": "Özellik kapsamı gereklidir.", + "default-value-field-input": "Varsayılan değer", + "type-field-input": "Tür", + "type-field-input-required": "Tür gereklidir.", + "key-field-input": "Anahtar", + "add-entity-type": "Varlık türü ekle", + "add-device-profile": "Cihaz profili ekle", + "key-field-input-required": "Anahtar gereklidir.", + "number-floating-point-field-input": "Ondalık basamak sayısı", + "number-floating-point-field-input-hint": "Sonucu tam sayıya dönüştürmek için 0 kullanın", + "add-to-message-field-input": "Mesaja ekle", + "add-to-metadata-field-input": "Metadata’ya ekle", + "custom-expression-field-input": "Matematiksel İfade", + "custom-expression-field-input-required": "Matematiksel ifade gereklidir", + "custom-expression-field-input-hint": "Değerlendirilecek bir matematiksel ifade belirtin. Varsayılan ifade Fahrenheit’ı Celsius’a dönüştürmeyi gösterir", + "retained-message": "Kalıcı", + "attributes-mapping": "Özellik eşlemesi", + "latest-telemetry-mapping": "En son telemetri eşlemesi", + "add-mapped-attribute-to": "Eşlenen özellikleri ekle", + "add-mapped-latest-telemetry-to": "Eşlenen en son telemetriyi ekle", + "add-mapped-fields-to": "Eşlenen alanları ekle", + "add-selected-details-to": "Seçilen ayrıntıları ekle", + "clear-selected-types": "Seçilen türleri temizle", + "clear-selected-details": "Seçilen ayrıntıları temizle", + "clear-selected-fields": "Seçilen alanları temizle", + "clear-selected-keys": "Seçilen anahtarları temizle", + "geofence-configuration": "Coğrafi sınır yapılandırması", + "coordinate-field-names": "Koordinat alan adları", + "coordinate-field-hint": "Kural düğümü, belirtilen alanları mesajdan almaya çalışır. Eğer mevcut değillerse metadata’dan arar.", + "presence-monitoring-strategy": "Varlık izleme stratejisi", + "presence-monitoring-strategy-on-first-message": "İlk mesajda", + "presence-monitoring-strategy-on-each-message": "Her mesajda", + "presence-monitoring-strategy-on-first-message-hint": "Önceki varlık durumu 'Girdi' veya 'Çıktı' güncellemesinden sonra yapılandırılmış minimum süre geçtikten sonra gelen ilk mesajda 'İçeride' veya 'Dışarıda' varlık durumunu bildirir.", + "presence-monitoring-strategy-on-each-message-hint": "'Girdi' veya 'Çıktı' varlık durumundan sonra gelen her mesajda 'İçeride' veya 'Dışarıda' durumunu bildirir.", + "fetch-credentials-to": "Kimlik bilgilerini buraya al", + "add-originator-attributes-to": "Başlatan özelliklerini buraya ekle", + "originator-attributes": "Başlatan özellikleri", + "fetch-latest-telemetry-with-timestamp": "Zaman damgası ile en son telemetriyi al", + "fetch-latest-telemetry-with-timestamp-tooltip": "Seçildiğinde, en son telemetri değerleri zaman damgası ile çıkış metadata’sına eklenir, örn: \"{{latestTsKeyName}}\": \"{\"ts\":1574329385897, \"value\":42}\"", + "tell-failure": "Seçilen özelliklerden herhangi biri eksikse Hata bildir", + "tell-failure-tooltip": "Seçilen anahtarlardan en az biri yoksa çıkış mesajı 'Hata' olarak işaretlenir.", + "created-time": "Oluşturulma zamanı", + "chip-help": "'{{inputName}}' girişi tamamlamak için 'Enter' tuşuna basın. \n'{{inputName}}' silmek için 'Backspace' tuşuna basın. \nBirden fazla değer desteklenir.", + "detail": "ayrıntı", + "field-name": "alan adı", + "device-profile": "cihaz profili", + "entity-type": "varlık türü", + "message-type": "mesaj türü", + "timeseries-key": "zaman serisi anahtarı", + "type": "Tür", + "first-name": "Ad", + "last-name": "Soyad", + "label": "Etiket", + "originator-fields-mapping": "Başlatan alan eşlemesi", + "add-mapped-originator-fields-to": "Eşlenen başlatan alanları buraya ekle", + "fields": "Alanlar", + "skip-empty-fields": "Boş alanları atla", + "skip-empty-fields-tooltip": "Boş değerlere sahip alanlar çıkış mesajına/metadata’ya eklenmeyecektir.", + "fetch-interval": "Alım aralığı", + "fetch-strategy": "Alım stratejisi", + "fetch-timeseries-from-to": "{{startInterval}} {{startIntervalTimeUnit}} öncesinden {{endInterval}} {{endIntervalTimeUnit}} öncesine kadar zaman serisini al.", + "fetch-timeseries-from-to-invalid": "Zaman serisi alımı geçersiz (\"Başlangıç aralığı\", \"Bitiş aralığından\" küçük olmalıdır).", + "use-metadata-dynamic-interval-tooltip": "Seçildiğinde, kural düğümü mesaj ve metadata desenlerine göre dinamik aralık başlangıcı ve bitişi kullanacaktır.", + "all-mode-hint": "\"Tümü\" alım modu seçildiğinde, kural düğümü yapılandırılabilir sorgu parametreleri ile alım aralığından telemetri verilerini alacaktır.", + "first-mode-hint": "\"İlk\" alım modu seçildiğinde, kural düğümü alım aralığının başlangıcına en yakın telemetri verisini alacaktır.", + "last-mode-hint": "\"Son\" alım modu seçildiğinde, kural düğümü alım aralığının sonuna en yakın telemetri verisini alacaktır.", + "ascending": "Artan", + "descending": "Azalan", + "min": "Min", + "max": "Maks", + "average": "Ortalama", + "sum": "Toplam", + "count": "Sayı", + "none": "Yok", + "last-level-relation-tooltip": "Seçildiğinde, kural düğümü sadece maksimum ilişki düzeyinde tanımlı düzeydeki ilişkili varlıkları arar.", + "last-level-device-relation-tooltip": "Seçildiğinde, kural düğümü sadece maksimum ilişki düzeyinde tanımlı düzeydeki ilişkili cihazları arar.", + "data-to-fetch": "Alınacak veri", + "mapping-of-customers": "Müşteri eşlemesi", + "map-fields-required": "Tüm eşleme alanları gereklidir.", + "attributes": "Özellikler", + "related-device-attributes": "İlişkili cihaz özellikleri", + "add-selected-attributes-to": "Seçilen özellikleri buraya ekle", + "device-profiles": "Cihaz profilleri", + "mapping-of-tenant": "Kiracı eşlemesi", + "add-attribute-key": "Özellik anahtarı ekle", + "message-template": "Mesaj şablonu", + "message-template-required": "Mesaj şablonu gereklidir", + "use-system-slack-settings": "Sistem Slack ayarlarını kullan", + "slack-api-token": "Slack API anahtarı", + "slack-api-token-required": "Slack API anahtarı gereklidir", + "keys-mapping": "Anahtar eşlemesi", + "add-key": "Anahtar ekle", + "recipients": "Alıcılar", + "message-subject-and-content": "Mesaj konusu ve içeriği", + "template-rules-hint": "Her iki giriş alanı da şablonlaştırmayı destekler. Mesajdan değer çıkarmak için $[messageKey], metadata'dan değer çıkarmak için ${metadataKey} kullanın.", + "originator-customer-desc": "Gelen mesajın başlatanının müşterisini yeni başlatan olarak kullan.", + "originator-tenant-desc": "Mevcut kiracıyı yeni başlatan olarak kullan.", + "originator-related-entity-desc": "İlgili varlığı yeni başlatan olarak kullan. Yapılandırılmış ilişki türü ve yönüne göre arama yapılır.", + "originator-alarm-originator-desc": "Alarm başlatanını yeni başlatan olarak kullan. Yalnızca gelen mesajın başlatanı alarm varlığı ise geçerlidir.", + "originator-entity-by-name-pattern-desc": "Veritabanından alınan varlığı yeni başlatan olarak kullan. Varlık türüne ve belirtilen ad desenine göre arama yapılır.", + "email-from-template-hint": "Mesajdan değer çıkarmak için $[messageKey], metadata'dan değer çıkarmak için ${metadataKey} kullanın.", + "recipients-block-main-hint": "Virgülle ayrılmış adres listesi. Tüm giriş alanları şablonlaştırmayı destekler.", + "forward-msg-default-rule-chain": "Mesajı başlatanın varsayılan kural zincirine yönlendir", + "forward-msg-default-rule-chain-tooltip": "Etkinleştirilirse, mesaj başlatanın varsayılan kural zincirine ya da yapılandırmadan tanımlanmış zincire yönlendirilir. Eğer başlatanın profilinde varsayılan bir zincir tanımlı değilse, yapılandırmadaki zincir kullanılır.", + "exclude-zero-deltas": "Sıfır farkları çıkış mesajından hariç tut", + "exclude-zero-deltas-hint": "Etkinleştirilirse, \"{{outputValueKey}}\" değeri sıfır değilse çıkış mesajına eklenir.", + "exclude-zero-deltas-time-difference-hint": "Etkinleştirilirse, sadece \"{{outputValueKey}}\" değeri sıfır değilse, \"{{outputValueKey}}\" ve \"{{periodValueKey}}\" çıkış anahtarları mesaja eklenir.", + "search-direction-from": "Başlatandan hedef varlığa", + "search-direction-to": "Hedef varlıktan başlatana", + "del-relation-direction-from": "Başlatandan", + "del-relation-direction-to": "Başlatana", + "target-entity": "Hedef varlık", + "function-configuration": "Fonksiyon yapılandırması", + "function-name": "Fonksiyon adı", + "function-name-required": "Fonksiyon adı gereklidir.", + "qualifier": "Niteleyici", + "qualifier-hint": "Eğer niteleyici belirtilmezse, varsayılan niteleyici \"$LATEST\" kullanılacaktır.", + "aws-credentials": "AWS Kimlik Bilgileri", + "connection-timeout": "Bağlantı zaman aşımı", + "connection-timeout-required": "Bağlantı zaman aşımı gereklidir.", + "connection-timeout-min": "Minimum bağlantı zaman aşımı 0'dır.", + "connection-timeout-hint": "Bağlantı kurulurken saniye cinsinden bekleme süresi. 0 değeri sınırsızdır ancak önerilmez.", + "request-timeout": "İstek zaman aşımı", + "request-timeout-required": "İstek zaman aşımı gereklidir", + "request-timeout-min": "Minimum istek zaman aşımı 0'dır", + "request-timeout-hint": "İsteğin tamamlanması için saniye cinsinden bekleme süresi. 0 değeri sınırsızdır ancak önerilmez.", + "units": "Birimler", + "tell-failure-aws-lambda": "AWS Lambda fonksiyonu hata verirse Failure bildir", + "tell-failure-aws-lambda-hint": "Eğer AWS Lambda fonksiyonu hata dönerse, mesaj işleme Failure olarak işaretlenir.", + "basic-mode": "Temel", + "advanced-mode": "Gelişmiş", + "save-time-series": { + "processing-settings": "İşleme ayarları", + "processing-settings-hint": "Gelen mesajların nasıl işlendiğini tanımlar. Temel ayarlar önceden tanımlı stratejiler sunar, gelişmiş ayarlar ise her eylem için bireysel strateji seçmenize olanak tanır.", + "advanced-settings-hint": "İşleme stratejilerini yapılandırırken dikkatli olun. Bazı kombinasyonlar beklenmeyen sonuçlara neden olabilir.", + "strategy": "Strateji", + "deduplication-interval": "Çoğaltmayı önleme aralığı", + "deduplication-interval-required": "Çoğaltmayı önleme aralığı gereklidir", + "deduplication-interval-min-max-range": "Aralık en az 1 saniye ve en fazla 1 gün olmalıdır", + "strategy-type": { + "every-message": "Her mesajda", + "skip": "Atla", + "deduplicate": "Çoğaltmayı önle", + "web-sockets-only": "Yalnızca WebSockets" + }, + "time-series": "Zaman serisi", + "latest": "Son değerler", + "web-sockets": "WebSockets", + "calculated-fields": "Hesaplanmış alanlar" + }, + "save-attribute": { + "processing-settings": "İşleme ayarları", + "processing-settings-hint": "Gelen mesajların nasıl işlendiğini tanımlar. Temel işleme ayarları önceden yapılandırılmış stratejileri seçmenize olanak tanır, Gelişmiş ayarlar ise her işlem için ayrı stratejiler belirlemenizi sağlar.", + "advanced-settings-hint": "İşleme stratejilerini yapılandırırken dikkatli olun. Bazı kombinasyonlar beklenmeyen davranışlara yol açabilir.", + "strategy": "Strateji", + "deduplication-interval": "Çoğaltmayı önleme aralığı", + "deduplication-interval-required": "Çoğaltmayı önleme aralığı gereklidir", + "deduplication-interval-min-max-range": "Çoğaltmayı önleme aralığı en az 1 saniye ve en fazla 1 gün olmalıdır", + "scope": "Kapsam", + "strategy-type": { + "every-message": "Her mesajda", + "skip": "Atla", + "deduplicate": "Çoğaltmayı önle", + "web-sockets-only": "Yalnızca WebSockets" + }, + "attributes": "Öznitelikler" + }, + "key-val": { + "key": "Anahtar", + "value": "Değer", + "see-examples": "Örnekleri gör.", + "remove-entry": "Girdiyi kaldır", + "remove-mapping-entry": "Eşleme girdisini kaldır", + "add-mapping-entry": "Eşleme ekle", + "add-entry": "Girdi ekle", + "copy-key-values-from": "Anahtar-değer çiftlerini kopyala", + "delete-key-values": "Anahtar-değer çiftlerini sil", + "delete-key-values-from": "Şuradan anahtar-değer çiftlerini sil", + "at-least-one-key-error": "En az bir anahtar seçilmelidir.", + "unique-key-value-pair-error": "'{{keyText}}' ile '{{valText}}' farklı olmalıdır!" + }, + "mail-body-types": { + "plain-text": "Düz metin", + "html": "HTML", + "dynamic": "Dinamik", + "use-body-type-template": "Gövde tipi şablonunu kullan", + "plain-text-description": "Özel biçimlendirme ya da stil olmayan basit metin.", + "html-text-description": "E-posta gövdesinde biçimlendirme, bağlantı ve görseller için HTML etiketlerini kullanmanıza olanak tanır.", + "dynamic-text-description": "Şablonlaştırma özelliğine bağlı olarak Düz Metin veya HTML gövde türünü dinamik olarak kullanmanızı sağlar.", + "after-template-evaluation-hint": "Şablon değerlendirmesinden sonra değer HTML için true, Düz metin için false olmalıdır." + }, + "ai": { + "ai-model": "Yapay Zeka modeli", + "model": "Model", + "ai-model-hint": "Bu kural düğümü tarafından gönderilen istekleri işlemek için önceden yapılandırılmış bir yapay zeka modeli seçin veya yeni bir tane yapılandırmak için \"Yeni oluştur\" seçeneğini kullanın.", + "prompt-settings": "İstem ayarları", + "prompt-settings-hint": "İsteğe bağlı sistem istemi, yapay zekanın genel rolünü ve kısıtlamalarını belirlerken, kullanıcı istemi gerçekleştirilmesi gereken belirli görevi tanımlar. Her iki alan da şablonlaştırmayı destekler.", + "system-prompt": "Sistem istemi", + "system-prompt-max-length": "Sistem istemi en fazla 500000 karakter olmalıdır.", + "system-prompt-blank": "Sistem istemi boş olmamalıdır.", + "user-prompt": "Kullanıcı istemi", + "user-prompt-required": "Kullanıcı istemi gereklidir.", + "user-prompt-max-length": "Kullanıcı istemi en fazla 500000 karakter olmalıdır.", + "user-prompt-blank": "Kullanıcı istemi boş olmamalıdır.", + "response-format": "Yanıt formatı", + "response-text": "Metin", + "response-json": "JSON", + "response-json-schema": "JSON Şeması", + "response-format-hint-TEXT": "Modelin geçerli bir JSON nesnesi olup olmayabileceği rastgele metin üretmesine olanak tanır. Çıktı geçerli bir JSON nesnesi değilse, otomatik olarak \"response\" anahtarı altında bir JSON nesnesine sarılır.", + "response-format-hint-JSON": "Modelin geçerli bir JSON yanıtı üretmesi gereklidir. Çıktı geçerli bir JSON nesnesi değilse, otomatik olarak \"response\" anahtarı altında bir JSON nesnesine sarılır.", + "response-format-hint-JSON_SCHEMA": "Modelin, sağlanan şemada tanımlanan belirli yapı ve veri türleriyle eşleşen bir JSON üretmesi gereklidir. Çıktı geçerli bir JSON nesnesi değilse, otomatik olarak \"response\" anahtarı altında bir JSON nesnesine sarılır.", + "response-json-schema-hint": "Geçerli herhangi bir JSON Şeması girilebilir, ancak bu kural düğümü yalnızca sınırlı bir alt kümesini destekler. Ayrıntılar için düğüm belgelerine bakın.", + "response-json-schema-required": "JSON Şeması gereklidir", + "advanced-settings": "Gelişmiş ayarlar", + "timeout": "Zaman aşımı", + "timeout-hint": "Yapay zeka modelinden yanıt beklemek için maksimum süre. \nSüre aşılırsa istek sonlandırılır.", + "timeout-required": "Zaman aşımı gereklidir", + "timeout-validation": "1 saniye ile 10 dakika arasında olmalıdır.", + "force-acknowledgement": "Zorunlu onaylama", + "force-acknowledgement-hint": "Etkinleştirilirse, gelen mesaj anında onaylanır. Modelin yanıtı ayrı, yeni bir mesaj olarak kuyruğa alınır." + } }, "timezone": { - "timezone": "Saat dilimi", - "select-timezone": "Saat dilimini seçin", - "no-timezones-matching": "'{{timezone}}' ile eşleşen saat dilimi bulunamadı.", - "timezone-required": "Saat dilimi gerekli.", - "browser-time": "Tarayıcı Süresi" + "timezone": "Zaman dilimi", + "select-timezone": "Zaman dilimi seç", + "no-timezones-matching": "'{{timezone}}' ile eşleşen zaman dilimi bulunamadı.", + "timezone-required": "Zaman dilimi gereklidir.", + "browser-time": "Tarayıcı saati" }, "queue": { - "select_name": "Kuyruk adını seçin", - "name": "Kuyruk Adı", - "name_required": "Kuyruk Adı gerekli" + "queue-name": "Kuyruk", + "no-queues-found": "Kuyruk bulunamadı.", + "no-queues-matching": "'{{queue}}' ile eşleşen kuyruk bulunamadı.", + "select-name": "Kuyruk adı seç", + "name": "Ad", + "name-required": "Kuyruk adı gereklidir!", + "name-unique": "Kuyruk adı benzersiz değil!", + "name-pattern": "Kuyruk adı yalnızca ASCII harf/rakam, '.', '_' ve '-' karakterlerini içerebilir!", + "queue-required": "Kuyruk gereklidir!", + "topic-required": "Kuyruk konusu gereklidir!", + "poll-interval-required": "Anket aralığı gereklidir!", + "poll-interval-min-value": "Anket aralığı değeri 1'den küçük olamaz", + "partitions-required": "Bölüm sayısı gereklidir!", + "partitions-min-value": "Bölüm sayısı değeri 1'den küçük olamaz", + "pack-processing-timeout-required": "İşleme zaman aşımı gereklidir", + "pack-processing-timeout-min-value": "İşleme zaman aşımı değeri 1'den küçük olamaz", + "batch-size-required": "Toplu işlem boyutu gereklidir!", + "batch-size-min-value": "Toplu işlem boyutu değeri 1'den küçük olamaz", + "retries-required": "Yeniden deneme sayısı gereklidir!", + "retries-min-value": "Yeniden deneme değeri negatif olamaz", + "failure-percentage-required": "Başarısızlık yüzdesi gereklidir!", + "failure-percentage-min-value": "Başarısızlık yüzdesi değeri 0'dan küçük olamaz", + "failure-percentage-max-value": "Başarısızlık yüzdesi değeri 100'den büyük olamaz", + "pause-between-retries-required": "Yeniden denemeler arası bekleme süresi gereklidir!", + "pause-between-retries-min-value": "Yeniden denemeler arası bekleme süresi 1'den küçük olamaz", + "max-pause-between-retries-required": "Maksimum yeniden deneme bekleme süresi gereklidir!", + "max-pause-between-retries-min-value": "Maksimum yeniden deneme bekleme süresi 1'den küçük olamaz", + "submit-strategy-type-required": "Gönderme stratejisi türü gereklidir!", + "processing-strategy-type-required": "İşleme stratejisi türü gereklidir!", + "queues": "Kuyruklar", + "selected-queues": "{ count, plural, =1 {1 kuyruk} other {# kuyruk} } seçildi", + "delete-queue-title": "'{{queueName}}' adlı kuyruğu silmek istediğinizden emin misiniz?", + "delete-queues-title": "{ count, plural, =1 {1 kuyruğu} other {# kuyruğu} } silmek istediğinizden emin misiniz?", + "delete-queue-text": "Dikkatli olun, onaydan sonra kuyruk ve tüm ilişkili veriler geri alınamaz hale gelecektir.", + "delete-queues-text": "Onaydan sonra seçilen tüm kuyruklar silinecek ve erişilemez olacaktır.", + "search": "Kuyruk ara", + "add": "Kuyruk ekle", + "details": "Kuyruk ayrıntıları", + "topic": "Konu", + "submit-settings": "Gönderim ayarları", + "submit-strategy": "Strateji türü *", + "grouping-parameter": "Gruplama parametresi", + "processing-settings": "Yeniden deneme işleme ayarları", + "processing-strategy": "İşleme türü *", + "retries-settings": "Yeniden deneme ayarları", + "polling-settings": "Anket ayarları", + "batch-processing": "Toplu işlem", + "poll-interval": "Anket aralığı", + "partitions": "Bölümler", + "immediate-processing": "Anında işleme", + "consumer-per-partition": "Her tüketici için mesaj anketi gönder", + "consumer-per-partition-hint": "Her bölüm için ayrı tüketici(ler) etkinleştir", + "duplicate-msg-to-all-partitions": "Mesajı tüm bölümlere kopyala", + "processing-timeout": "İşleme süresi (ms)", + "batch-size": "Toplu işlem boyutu", + "retries": "Yeniden deneme sayısı (0 – sınırsız)", + "failure-percentage": "Yeniden deneme atlaması için başarısız mesajlar, %", + "pause-between-retries": "Yeniden deneme süresi, sn", + "max-pause-between-retries": "Ek yeniden deneme süresi, sn", + "delete": "Kuyruğu sil", + "copyId": "Kuyruk Kimliğini kopyala", + "idCopiedMessage": "Kuyruk Kimliği panoya kopyalandı", + "description": "Açıklama", + "description-hint": "Bu metin, seçilen strateji yerine Kuyruk açıklamasında görüntülenecektir", + "alt-description": "Gönderim Stratejisi: {{submitStrategy}}, İşleme Stratejisi: {{processingStrategy}}", + "custom-properties": "Özel özellikler", + "custom-properties-hint": "Özel kuyruk (konu) oluşturma özellikleri, örn. 'retention.ms:604800000;retention.bytes:1048576000'", + "strategies": { + "sequential-by-originator-label": "Kaynak bazlı sıralı", + "sequential-by-originator-hint": "Örn. cihaz A için önceki mesaj onaylanmadan yeni mesaj gönderilmez", + "sequential-by-tenant-label": "Kiracı bazlı sıralı", + "sequential-by-tenant-hint": "Örn. kiracı A için önceki mesaj onaylanmadan yeni mesaj gönderilmez", + "sequential-label": "Sıralı", + "sequential-hint": "Önceki mesaj onaylanmadan yeni mesaj gönderilmez", + "burst-label": "Ani", + "burst-hint": "Tüm mesajlar geldikleri sırayla kural zincirlerine gönderilir", + "batch-label": "Toplu", + "batch-hint": "Yeni toplu işlem, önceki onaylanmadan gönderilmez", + "skip-all-failures-label": "Tüm hataları atla", + "skip-all-failures-hint": "Tüm hataları yok say", + "skip-all-failures-and-timeouts-label": "Tüm hataları ve zaman aşımlarını atla", + "skip-all-failures-and-timeouts-hint": "Tüm hataları ve zaman aşımlarını yok say", + "retry-all-label": "Tümünü yeniden dene", + "retry-all-hint": "İşlem grubundaki tüm mesajları yeniden dene", + "retry-failed-label": "Başarısızları yeniden dene", + "retry-failed-hint": "İşlem grubundaki tüm başarısız mesajları yeniden dene", + "retry-timeout-label": "Zaman aşımı olanları yeniden dene", + "retry-timeout-hint": "İşlem grubundaki zaman aşımına uğramış tüm mesajları yeniden dene", + "retry-failed-and-timeout-label": "Başarısız ve zaman aşımlarını yeniden dene", + "retry-failed-and-timeout-hint": "İşlem grubundaki başarısız ve zaman aşımına uğramış tüm mesajları yeniden dene" + } + }, + "queue-statistics": { + "queue-statistics": "Kuyruk istatistikleri", + "no-queue-statistics-matching": "'{{entity}}' ile eşleşen kuyruk istatistiği bulunamadı.", + "queue-statistics-required": "Kuyruk istatistiği gereklidir.", + "list-of-queue-statistics": "{ count, plural, =1 {Bir kuyruk istatistiği} other {# kuyruk istatistikleri listesi} }", + "selected-queue-statistics": "{ count, plural, =1 {1 kuyruk istatistiği} other {# kuyruk istatistiği} } seçildi", + "no-queue-statistics-text": "Kuyruk istatistiği bulunamadı", + "queue-statistics-starts-with": "Adı '{{prefix}}' ile başlayan kuyruk istatistikleri" + }, + "server-error": { + "general": "Genel sunucu hatası", + "authentication": "Kimlik doğrulama hatası", + "jwt-token-expired": "JWT belirteci süresi doldu", + "tenant-trial-expired": "Kiracı deneme süresi doldu", + "credentials-expired": "Kimlik bilgileri süresi doldu", + "permission-denied": "İzin reddedildi", + "invalid-arguments": "Geçersiz parametreler", + "bad-request-params": "Hatalı istek parametreleri", + "item-not-found": "Öğe bulunamadı", + "too-many-requests": "Çok fazla istek yapıldı", + "too-many-updates": "Çok fazla güncelleme yapıldı" }, "tenant": { - "tenant": "Tenant", - "tenants": "Tenantlar", - "management": "Tenant yönetimi", - "add": "Tenant Ekle", - "admins": "Adminler", - "manage-tenant-admins": "Tenant Adminlerini Yönet", - "delete": "Tenant sil", - "add-tenant-text": "Yeni tenant ekle", - "no-tenants-text": "Hiçbir tenant bulunamadı", - "tenant-details": "Tenant detayları", - "delete-tenant-title": "'{{tenantTitle}}' isimli tenantı silmek istediğinize emin misiniz?", - "delete-tenant-text": "UYARI: Onaylandıktan sonra tenant ve ilişkili tüm veriler geri yüklenemez şekilde silinecektir.", - "delete-tenants-title": "{ count, plural, =1 {1 tenantı} other {# tenantı} } silmek istediğinize emin misiniz?", - "delete-tenants-action-title": "{ count, plural, =1 {1 tenantı} other {# tenantı} } sil", - "delete-tenants-text": "UYARI: Onaylandıktan sonra seçili tüm tenantlar ve ilişkili tüm veriler geri yüklenemez şekilde silinecektir", + "tenant": "Kiracı", + "tenants": "Kiracılar", + "management": "Kiracı yönetimi", + "add": "Kiracı ekle", + "admins": "Yöneticiler", + "manage-tenant-admins": "Kiracı yöneticilerini yönet", + "delete": "Kiracıyı sil", + "add-tenant-text": "Yeni kiracı ekle", + "no-tenants-text": "Hiçbir kiracı bulunamadı", + "tenant-details": "Kiracı detayları", + "title-max-length": "Başlık 256 karakterden kısa olmalıdır", + "delete-tenant-title": "Kiracı '{{tenantTitle}}' silinsin mi?", + "delete-tenant-text": "Dikkatli olun, onaydan sonra kiracı ve ilişkili tüm veriler geri alınamaz şekilde silinecektir.", + "delete-tenants-title": "{ count, plural, =1 {1 kiracı} other {# kiracı} } silinsin mi?", + "delete-tenants-action-title": "{ count, plural, =1 {1 kiracıyı} other {# kiracıyı} } sil", + "delete-tenants-text": "Dikkatli olun, onaydan sonra seçili tüm kiracılar ve ilişkili veriler kalıcı olarak silinecektir.", "title": "Başlık", - "title-required": "Başlık gerekli.", + "title-required": "Başlık gereklidir.", "description": "Açıklama", "details": "Detaylar", "events": "Olaylar", - "copyId": "Tenant kimliğini kopyala", - "idCopiedMessage": "Tenant kimliği panoya kopyalandı", - "select-tenant": "Tenant seç", - "no-tenants-matching": "'{{entity}}' ile eşleşen tenant bulunamadı.", - "tenant-required": "Tenant gerekli", - "search": "Tenantları ara", - "selected-tenants": "{ count, plural, =1 {1 tenant} other {# tenant} } seçildi", - "isolated-tb-rule-engine": "ThingsBoard soyutlanmış kural yönetimi konteynerda işlensin", - "isolated-tb-rule-engine-details": "Her soyutlanmış tenant ayrı bir mikro servis gerektirir" + "copyId": "Kiracı Kimliğini kopyala", + "idCopiedMessage": "Kiracı Kimliği panoya kopyalandı", + "select-tenant": "Kiracı seç", + "no-tenants-matching": "'{{entity}}' ile eşleşen kiracı bulunamadı.", + "tenant-required": "Kiracı gereklidir", + "search": "Kiracı ara", + "selected-tenants": "{ count, plural, =1 {1 kiracı} other {# kiracı} } seçildi", + "isolated-tb-rule-engine": "Ayrı ThingsBoard Kural Motoru kuyrukları kullan", + "isolated-tb-rule-engine-details": "Her kiracı için özel Kural Motoru kuyrukları oluşturulacaktır" }, "tenant-profile": { - "tenant-profile": "Tenant profili", - "tenant-profiles": "Tenant profilleri", - "add": "Tenant profili ekle", - "edit": "Tenant profili düzenle", - "tenant-profile-details": "Tenant profili ayrıntıları", - "no-tenant-profiles-text": "Tenant profili bulunamadı", - "search": "Tenant profillerini ara", - "selected-tenant-profiles": "{ count, plural, =1 {1 tenant profili} other {# tenant profili} } seçildi", - "no-tenant-profiles-matching": "'{{entity}}' ile eşleşen tenant profili bulunamadı.", - "tenant-profile-required": "Tenant profili gerekli", - "idCopiedMessage": "Tenant profili kimliği panoya kopyalandı", - "set-default": "Tenant profilini varsayılan yap", - "delete": "Tenant profilini sil", - "copyId": "Tenant profili kimliğini kopyala", + "tenant-profile": "Kiracı profili", + "tenant-profiles": "Kiracı profilleri", + "add": "Kiracı profili ekle", + "add-profile": "Profil ekle", + "debug": "Hata ayıklama", + "edit": "Kiracı profilini düzenle", + "tenant-profile-details": "Kiracı profili detayları", + "no-tenant-profiles-text": "Kiracı profili bulunamadı", + "name-max-length": "İsim 256 karakterden kısa olmalıdır", + "search": "Kiracı profili ara", + "selected-tenant-profiles": "{ count, plural, =1 {1 kiracı profili} other {# kiracı profili} } seçildi", + "no-tenant-profiles-matching": "'{{entity}}' ile eşleşen kiracı profili bulunamadı.", + "tenant-profile-required": "Kiracı profili gereklidir", + "idCopiedMessage": "Kiracı profili kimliği panoya kopyalandı", + "set-default": "Kiracı profili varsayılan yap", + "delete": "Kiracı profilini sil", + "copyId": "Kiracı profili kimliğini kopyala", "name": "İsim", - "name-required": "İsim gerekli.", + "name-required": "İsim gereklidir.", "data": "Profil verisi", "profile-configuration": "Profil yapılandırması", "description": "Açıklama", "default": "Varsayılan", - "delete-tenant-profile-title": "'{{tenantProfileName}}' tenant profilini silmek istediğinizden emin misiniz?", - "delete-tenant-profile-text": "Dikkatli olun, onaydan sonra tenant profili ve ilgili tüm veriler kurtarılamaz hale gelecektir.", - "delete-tenant-profiles-title": "{ count, plural, =1 {1 tenant profilini} other {# tenant profilini} } silmek istediğinizden emin misiniz?", - "delete-tenant-profiles-text": "Dikkatli olun, onaydan sonra seçilen tüm tenant profilleri kaldırılacak ve ilgili tüm veriler kurtarılamaz hale gelecektir.", - "set-default-tenant-profile-title": "Tenant profilini '{{tenantProfileName}}' varsayılan yapmak istediğinizden emin misiniz?", - "set-default-tenant-profile-text": "Onaydan sonra tenant profili varsayılan olarak işaretlenecek ve profili belirtilmemiş yeni tenantlar için kullanılacaktır.", - "no-tenant-profiles-found": "Tenant profili bulunamadı.", - "create-new-tenant-profile": "Yeni bir tane oluştur!", - "create-tenant-profile": "Yeni tenant profili oluştur", - "import": "Tenant profilini içe aktar", - "export": "Tenant profilini dışa aktar", - "export-failed-error": "Tenant profili dışa aktarılamıyor: {{error}}", - "tenant-profile-file": "Tenant profil dosyası", - "invalid-tenant-profile-file-error": "Tenant profili içe aktarılamıyor: Geçersiz tenant profili veri yapısı.", - "maximum-devices": "Maksimum cihaz sayısı (0 - sınırsız)", - "maximum-devices-required": "Maksimum cihaz sayısı gerekli.", - "maximum-devices-range": "Minimum cihaz sayısı negatif olamaz", - "maximum-assets": "Maksimum varlık sayısı (0 - sınırsız)", - "maximum-assets-required": "Maksimum varlık sayısı gerekli.", + "delete-tenant-profile-title": "Kiracı profili '{{tenantProfileName}}' silinsin mi?", + "delete-tenant-profile-text": "Dikkatli olun, onaydan sonra kiracı profili ve ilişkili tüm veriler geri alınamaz şekilde silinecektir.", + "delete-tenant-profiles-title": "{ count, plural, =1 {1 kiracı profili} other {# kiracı profili} } silinsin mi?", + "delete-tenant-profiles-text": "Dikkatli olun, onaydan sonra seçili tüm kiracı profilleri ve ilişkili veriler kalıcı olarak silinecektir.", + "set-default-tenant-profile-title": "Kiracı profili '{{tenantProfileName}}' varsayılan yapılsın mı?", + "set-default-tenant-profile-text": "Onaydan sonra bu kiracı profili varsayılan olarak işaretlenecek ve profil belirtilmeyen yeni kiracılar için kullanılacaktır.", + "no-tenant-profiles-found": "Kiracı profili bulunamadı.", + "create-new-tenant-profile": "Yeni oluştur!", + "create-tenant-profile": "Yeni kiracı profili oluştur", + "import": "Kiracı profili içe aktar", + "export": "Kiracı profili dışa aktar", + "export-failed-error": "Kiracı profili dışa aktarılamadı: {{error}}", + "tenant-profile-file": "Kiracı profili dosyası", + "invalid-tenant-profile-file-error": "Kiracı profili içe aktarılamadı: Geçersiz kiracı profili veri yapısı.", + "advanced-settings": "Gelişmiş ayarlar", + "entities": "Varlıklar", + "rule-engine": "Kural Motoru", + "time-to-live": "Geçerlilik süresi (TTL)", + "calculated-fields": "Hesaplanmış alanlar", + "alarms-and-notifications": "Alarmlar ve bildirimler", + "ota-files-in-bytes": "Dosyalar", + "ws-title": "WS", + "unlimited": "(0 - sınırsız)", + "maximum-devices": "Maksimum cihaz sayısı", + "maximum-devices-required": "Maksimum cihaz sayısı gereklidir.", + "maximum-devices-range": "Maksimum cihaz sayısı negatif olamaz", + "maximum-assets": "Maksimum varlık sayısı", + "maximum-assets-required": "Maksimum varlık sayısı gereklidir.", "maximum-assets-range": "Maksimum varlık sayısı negatif olamaz", - "maximum-customers": "Maksimum kullanıcı grubu sayısı (0 - sınırsız)", - "maximum-customers-required": "Maksimum kullanıcı grubu sayısı gerekli.", - "maximum-customers-range": "Maksimum kullanıcı grubu sayısı negatif olamaz", - "maximum-users": "Maksimum kullanıcı sayısı (0 - sınırsız)", - "maximum-users-required": "Maksimum kullanıcı sayısı gerekli.", + "maximum-customers": "Maksimum müşteri sayısı", + "maximum-customers-required": "Maksimum müşteri sayısı gereklidir.", + "maximum-customers-range": "Maksimum müşteri sayısı negatif olamaz", + "maximum-users": "Maksimum kullanıcı sayısı", + "maximum-users-required": "Maksimum kullanıcı sayısı gereklidir.", "maximum-users-range": "Maksimum kullanıcı sayısı negatif olamaz", - "maximum-dashboards": "Maksimum gösterge paneli sayısı (0 - sınırsız)", - "maximum-dashboards-required": "Maksimum gösterge paneli sayısı gerekli.", - "maximum-dashboards-range": "Maksimum gösterge paneli sayısı negatif olamaz", - "maximum-edges": "Maksimum gösterge kenar sayısı (0 - sınırsız)", - "maximum-edges-required": "Maksimum gösterge kenar sayısı gerekli.", - "maximum-edges-range": "Maksimum gösterge kenar sayısı negatif olamaz", - "maximum-rule-chains": "Maksimum kural zinciri sayısı (0 - sınırsız)", - "maximum-rule-chains-required": "Maksimum kural zinciri sayısı gerekli.", + "maximum-dashboards": "Maksimum dashboard sayısı", + "maximum-dashboards-required": "Maksimum dashboard sayısı gereklidir.", + "maximum-dashboards-range": "Maksimum dashboard sayısı negatif olamaz", + "maximum-edges": "Maksimum edge sayısı", + "maximum-edges-required": "Maksimum edge sayısı gereklidir.", + "maximum-edges-range": "Maksimum edge sayısı negatif olamaz", + "maximum-rule-chains": "Maksimum kural zinciri sayısı", + "maximum-rule-chains-required": "Maksimum kural zinciri sayısı gereklidir.", "maximum-rule-chains-range": "Maksimum kural zinciri sayısı negatif olamaz", - "maximum-resources-sum-data-size": "Bayt cinsinden kaynak dosyalarının maksimum toplamı (0 - sınırsız)", - "maximum-resources-sum-data-size-required": "Kaynak dosyaları boyutunun maksimum toplamı gerekli.", - "maximum-resources-sum-data-size-range": "Kaynak dosyaları boyutunun maksimum toplamı negatif olamaz", - "maximum-ota-packages-sum-data-size": "Ota paketi dosyalarının bayt cinsinden maksimum toplamı (0 - sınırsız)", - "maximum-ota-package-sum-data-size-required": "Ota paketi dosyalarının maksimum toplamı gerekli.", - "maximum-ota-package-sum-data-size-range": "Ota paketi dosyalarının maksimum toplamı negatif olamaz", - "transport-tenant-telemetry-msg-rate-limit": "Taşıma tenant telemetri iletileri hız sınırı.", - "transport-tenant-telemetry-data-points-rate-limit": "Taşıma tenant telemetri veri noktaları hız sınırı.", - "transport-device-msg-rate-limit": "Taşıma cihazı mesajları hız sınırı.", - "transport-device-telemetry-msg-rate-limit": "Taşıma cihazı telemetri mesajları hız sınırı.", - "transport-device-telemetry-data-points-rate-limit": "Taşıma cihazı telemetri veri noktaları hız sınırı.", - "max-transport-messages": "Maksimum taşıma mesajı sayısı (0 - sınırsız)", - "max-transport-messages-required": "Maksimum taşıma mesajı sayısı gerekli.", + "maximum-resources-sum-data-size": "Kaynak dosyalarının maksimum toplam boyutu (bayt)", + "maximum-resources-sum-data-size-required": "Kaynak dosyalarının maksimum toplam boyutu gereklidir.", + "maximum-resources-sum-data-size-range": "Kaynak dosyalarının maksimum toplam boyutu negatif olamaz", + "maximum-resource-size": "Maksimum kaynak dosyası boyutu (bayt)", + "maximum-resource-size-required": "Maksimum kaynak dosyası boyutu gereklidir", + "maximum-resource-size-range": "Maksimum kaynak dosyası boyutu negatif olamaz", + "maximum-ota-packages-sum-data-size": "OTA paket dosyalarının maksimum toplam boyutu (bayt)", + "maximum-ota-package-sum-data-size-required": "OTA paket dosyalarının maksimum toplam boyutu gereklidir.", + "maximum-ota-package-sum-data-size-range": "OTA paket dosyalarının maksimum toplam boyutu negatif olamaz", + "maximum-debug-duration-min": "Maksimum hata ayıklama süresi (dakika)", + "maximum-debug-duration-min-range": "Maksimum hata ayıklama süresi negatif olamaz", + "rest-requests-for-tenant": "Kiracı için REST istekleri", + "transport-tenant-telemetry-msg-rate-limit": "Taşıma kiracı telemetri mesajları", + "transport-tenant-telemetry-data-points-rate-limit": "Taşıma kiracı telemetri veri noktaları", + "transport-device-msg-rate-limit": "Taşıma cihaz mesajları", + "transport-device-telemetry-msg-rate-limit": "Taşıma cihaz telemetri mesajları", + "transport-device-telemetry-data-points-rate-limit": "Taşıma cihaz telemetri veri noktaları", + "transport-gateway-msg-rate-limit": "Taşıma ağ geçidi mesajları", + "transport-gateway-telemetry-msg-rate-limit": "Taşıma ağ geçidi telemetri mesajları", + "transport-gateway-telemetry-data-points-rate-limit": "Taşıma ağ geçidi telemetri veri noktaları", + "transport-gateway-device-msg-rate-limit": "Taşıma ağ geçidi cihaz mesajları", + "transport-gateway-device-telemetry-msg-rate-limit": "Taşıma ağ geçidi cihaz telemetri mesajları", + "transport-gateway-device-telemetry-data-points-rate-limit": "Taşıma ağ geçidi cihaz telemetri veri noktaları", + "tenant-entity-export-rate-limit": "Varlık sürümü oluşturma", + "tenant-entity-import-rate-limit": "Varlık sürümü yükleme", + "tenant-notification-request-rate-limit": "Bildirim istekleri", + "tenant-notification-requests-per-rule-rate-limit": "Her bildirim kuralı için bildirim istekleri", + "max-calculated-fields": "Varlık başına maksimum hesaplanmış alan sayısı", + "max-calculated-fields-range": "Varlık başına maksimum hesaplanmış alan sayısı negatif olamaz", + "max-calculated-fields-required": "Varlık başına maksimum hesaplanmış alan sayısı gereklidir", + "max-data-points-per-rolling-arg": "Kayan argümanlarda maksimum veri noktası sayısı", + "max-data-points-per-rolling-arg-range": "Kayan argümanlarda maksimum veri noktası sayısı negatif olamaz", + "max-data-points-per-rolling-arg-required": "Kayan argümanlarda maksimum veri noktası sayısı gereklidir", + "max-arguments-per-cf": "Hesaplanmış alan başına maksimum argüman sayısı", + "max-arguments-per-cf-range": "Hesaplanmış alan başına maksimum argüman sayısı negatif olamaz", + "max-arguments-per-cf-required": "Hesaplanmış alan başına maksimum argüman sayısı gereklidir", + "max-state-size": "Durumun maksimum boyutu (KB)", + "max-state-size-range": "Durumun maksimum boyutu (KB) negatif olamaz", + "max-state-size-required": "Durumun maksimum boyutu (KB) gereklidir", + "max-value-argument-size": "Tek bir değer argümanının maksimum boyutu (KB)", + "max-value-argument-size-range": "Tek bir değer argümanının maksimum boyutu (KB) negatif olamaz", + "max-value-argument-size-required": "Tek bir değer argümanının maksimum boyutu (KB) gereklidir", + "max-transport-messages": "Maksimum taşıma mesajı sayısı", + "max-transport-messages-required": "Maksimum taşıma mesajı sayısı gereklidir.", "max-transport-messages-range": "Maksimum taşıma mesajı sayısı negatif olamaz", - "max-transport-data-points": "Maksimum taşıma veri noktası sayısı (0 - sınırsız)", - "max-transport-data-points-required": "Maksimum taşıma veri noktası sayısı gerekli.", + "max-transport-data-points": "Maksimum taşıma veri noktası sayısı", + "max-transport-data-points-required": "Maksimum taşıma veri noktası sayısı gereklidir.", "max-transport-data-points-range": "Maksimum taşıma veri noktası sayısı negatif olamaz", - "max-r-e-executions": "Maksimum Kural Motoru yürütme sayısı (0 - sınırsız)", - "max-r-e-executions-required": "Maksimum Kural Motoru yürütme sayısı gerekli.", + "max-r-e-executions": "Maksimum Kural Motoru yürütme sayısı", + "max-r-e-executions-required": "Maksimum Kural Motoru yürütme sayısı gereklidir.", "max-r-e-executions-range": "Maksimum Kural Motoru yürütme sayısı negatif olamaz", - "max-j-s-executions": "Maksimum JavaScript yürütme sayısı (0 - sınırsız)", - "max-j-s-executions-required": "Maksimum JavaScript yürütme sayısı gerekli.", + "max-j-s-executions": "Maksimum JavaScript yürütme sayısı", + "max-j-s-executions-required": "Maksimum JavaScript yürütme sayısı gereklidir.", "max-j-s-executions-range": "Maksimum JavaScript yürütme sayısı negatif olamaz", - "max-d-p-storage-days": "Maksimum veri noktası depolama günü sayısı (0 - sınırsız)", - "max-d-p-storage-days-required": "Maksimum veri noktası depolama günü sayısı gerekli.", - "max-d-p-storage-days-range": "Maksimum veri noktası depolama günü sayısı negatif olamaz", - "default-storage-ttl-days": "Varsayılan depolama TTL günleri (0 - sınırsız)", - "default-storage-ttl-days-required": "Varsayılan depolama TTL günleri gerekli.", - "default-storage-ttl-days-range": "Varsayılan depolama TTL günleri negatif olamaz", - "alarms-ttl-days": "Alarm TTL günleri (0 - sınırsız)", - "alarms-ttl-days-required": "Alarm TTL günleri gerekli", - "alarms-ttl-days-days-range": "Alarm TTL günleri negatif olamaz", - "rpc-ttl-days": "RPC TTL günleri (0 - sınırsız)", - "rpc-ttl-days-required": "RPC TTL günleri gerekli", - "rpc-ttl-days-days-range": "RPC TTL günleri negatif olamaz", - "max-rule-node-executions-per-message": "Mesaj başına maksimum kural düğümü yürütme sayısı (0 - sınırsız)", - "max-rule-node-executions-per-message-required": "İleti başına maksimum kural düğümü yürütme sayısı gerekli.", - "max-rule-node-executions-per-message-range": "İleti başına maksimum kural düğümü yürütme sayısı negatif olamaz", - "max-emails": "Gönderilen maksimum e-posta sayısı (0 - sınırsız)", - "max-emails-required": "Gönderilen maksimum e-posta sayısı gerekli.", - "max-emails-range": "Gönderilen maksimum e-posta sayısı negatif olamaz", - "max-sms": "Gönderilen maksimum SMS sayısı (0 - sınırsız)", - "max-sms-required": "Gönderilen maksimum SMS sayısı gerekli.", - "max-sms-range": "Gönderilen maksimum SMS sayısı negatif olamaz", - "max-created-alarms": "Oluşturulan maksimum alarm sayısı (0 - sınırsız)", - "max-created-alarms-required": "Oluşturulan maksimum alarm sayısı gerekli.", - "max-created-alarms-range": "Oluşturulan maksimum alarm sayısı negatif olamaz" + "max-tbel-executions": "Maksimum TBEL yürütme sayısı", + "max-tbel-executions-required": "Maksimum TBEL yürütme sayısı gereklidir.", + "max-tbel-executions-range": "Maksimum TBEL yürütme sayısı negatif olamaz", + "max-d-p-storage-days": "Veri noktalarının maksimum saklama süresi (gün)", + "max-d-p-storage-days-required": "Veri noktalarının maksimum saklama süresi gereklidir.", + "max-d-p-storage-days-range": "Veri noktalarının maksimum saklama süresi negatif olamaz", + "default-storage-ttl-days": "Varsayılan depolama TTL süresi (gün)", + "default-storage-ttl-days-required": "Varsayılan depolama TTL süresi gereklidir.", + "default-storage-ttl-days-range": "Varsayılan depolama TTL süresi negatif olamaz", + "alarms-ttl-days": "Alarm TTL süresi (gün)", + "alarms-ttl-days-required": "Alarm TTL süresi gereklidir", + "alarms-ttl-days-days-range": "Alarm TTL süresi negatif olamaz", + "rpc-ttl-days": "RPC TTL süresi (gün)", + "rpc-ttl-days-required": "RPC TTL süresi gereklidir", + "rpc-ttl-days-days-range": "RPC TTL süresi negatif olamaz", + "queue-stats-ttl-days": "Kuyruk istatistikleri TTL süresi (gün)", + "queue-stats-ttl-days-required": "Kuyruk istatistikleri TTL süresi gereklidir", + "queue-stats-ttl-days-range": "Kuyruk istatistikleri TTL süresi negatif olamaz", + "rule-engine-exceptions-ttl-days": "Kural Motoru istisnaları TTL süresi (gün)", + "rule-engine-exceptions-ttl-days-required": "Kural Motoru istisnaları TTL süresi gereklidir", + "rule-engine-exceptions-ttl-days-range": "Kural Motoru istisnaları TTL süresi negatif olamaz", + "max-rule-node-executions-per-message": "Mesaj başına Kural düğümü yürütme maksimum sayısı", + "max-rule-node-executions-per-message-required": "Mesaj başına Kural düğümü yürütme maksimum sayısı gereklidir.", + "max-rule-node-executions-per-message-range": "Mesaj başına Kural düğümü yürütme maksimum sayısı negatif olamaz", + "max-emails": "Gönderilen e-posta maksimum sayısı", + "max-emails-required": "Gönderilen e-posta maksimum sayısı gereklidir.", + "max-emails-range": "Gönderilen e-posta maksimum sayısı negatif olamaz", + "sms-enabled": "SMS etkin", + "max-sms": "Gönderilen SMS maksimum sayısı", + "max-sms-required": "Gönderilen SMS maksimum sayısı gereklidir.", + "max-sms-range": "Gönderilen SMS maksimum sayısı negatif olamaz", + "max-created-alarms": "Oluşturulan alarm maksimum sayısı", + "max-created-alarms-required": "Oluşturulan alarm maksimum sayısı gereklidir.", + "max-created-alarms-range": "Oluşturulan alarm maksimum sayısı negatif olamaz", + "no-queue": "Tanımlı Kuyruk yok", + "add-queue": "Kuyruk Ekle", + "queues-with-count": "Kuyruklar ({{count}})", + "tenant-rest-limits": "Kiracı için REST istekleri", + "customer-rest-limits": "Müşteri için REST istekleri", + "incorrect-pattern-for-rate-limits": "Biçim, iki nokta ile ayrılmış kapasite ve süre (saniye) çiftlerinden oluşmalıdır, örn. 100:1,2000:60", + "too-small-value-zero": "Değer 0'dan büyük olmalıdır", + "too-small-value-one": "Değer 1'den büyük olmalıdır", + "queue-size-is-limited-by-system-configuration": "Kuyruk boyutu sistem yapılandırması ile de sınırlıdır.", + "cassandra-write-tenant-core-limits-configuration": "Rest API Cassandra yazma sorguları", + "cassandra-read-tenant-core-limits-configuration": "Rest API ve WS telemetri Cassandra okuma sorguları", + "cassandra-write-tenant-rule-engine-limits-configuration": "Kural Motoru telemetri Cassandra yazma sorguları", + "cassandra-read-tenant-rule-engine-limits-configuration": "Kural Motoru telemetri Cassandra okuma sorguları", + "ws-limit-max-sessions-per-tenant": "Kiracı başına oturum maksimum sayısı", + "ws-limit-max-sessions-per-customer": "Müşteri başına oturum maksimum sayısı", + "ws-limit-max-sessions-per-regular-user": "Normal kullanıcı başına oturum maksimum sayısı", + "ws-limit-max-sessions-per-public-user": "Genel kullanıcı başına oturum maksimum sayısı", + "ws-limit-queue-per-session": "Oturum başına mesaj kuyruğu maksimum boyutu", + "ws-limit-max-subscriptions-per-tenant": "Kiracı başına abonelik maksimum sayısı", + "ws-limit-max-subscriptions-per-customer": "Müşteri başına abonelik maksimum sayısı", + "ws-limit-max-subscriptions-per-regular-user": "Normal kullanıcı başına abonelik maksimum sayısı", + "ws-limit-max-subscriptions-per-public-user": "Genel kullanıcı başına abonelik maksimum sayısı", + "ws-limit-updates-per-session": "Oturum başına WS güncellemeleri", + "rate-limits": { + "add-limit": "Sınır ekle", + "and-also-less-than": "ve ayrıca daha az", + "advanced-settings": "Gelişmiş ayarlar", + "edit-limit": "Sınırı düzenle", + "calculated-field-debug-event-rate-limit": "Hesaplanmış alan hata ayıklama olayları", + "edit-calculated-field-debug-event-rate-limit": "Hesaplanmış alan hata ayıklama olayları sınırını düzenle", + "edit-transport-tenant-msg-title": "Kiracı taşıma mesaj sınırlarını düzenle", + "edit-transport-tenant-telemetry-msg-title": "Kiracı taşıma telemetri mesaj sınırlarını düzenle", + "edit-transport-tenant-telemetry-data-points-title": "Kiracı taşıma telemetri veri noktası sınırlarını düzenle", + "edit-transport-device-msg-title": "Cihaz taşıma mesaj sınırlarını düzenle", + "edit-transport-device-telemetry-msg-title": "Cihaz taşıma telemetri mesaj sınırlarını düzenle", + "edit-transport-device-telemetry-data-points-title": "Cihaz taşıma telemetri veri noktası sınırlarını düzenle", + "edit-transport-gateway-msg-title": "Ağ geçidi taşıma mesaj sınırlarını düzenle", + "edit-transport-gateway-telemetry-msg-title": "Ağ geçidi taşıma telemetri mesaj sınırlarını düzenle", + "edit-transport-gateway-telemetry-data-points-title": "Ağ geçidi taşıma telemetri veri noktası sınırlarını düzenle", + "edit-transport-gateway-device-msg-title": "Ağ geçidi cihaz taşıma mesaj sınırlarını düzenle", + "edit-transport-gateway-device-telemetry-msg-title": "Ağ geçidi cihaz telemetri mesaj sınırlarını düzenle", + "edit-transport-gateway-device-telemetry-data-points-title": "Ağ geçidi cihaz telemetri veri noktası sınırlarını düzenle", + "edit-tenant-rest-limits-title": "Kiracı için REST istek sınırlarını düzenle", + "edit-customer-rest-limits-title": "Müşteri için REST istek sınırlarını düzenle", + "edit-ws-limit-updates-per-session-title": "Oturum başına WS güncelleme sınırlarını düzenle", + "edit-cassandra-write-tenant-core-limits-configuration": "REST API Cassandra yazma sorgularını düzenle", + "edit-cassandra-read-tenant-core-limits-configuration": "REST API ve WS telemetri Cassandra okuma sorgularını düzenle", + "edit-cassandra-write-tenant-rule-engine-limits-configuration": "Kural Motoru telemetri Cassandra yazma sorgularını düzenle", + "edit-cassandra-read-tenant-rule-engine-limits-configuration": "Kural Motoru telemetri Cassandra okuma sorgularını düzenle", + "edit-tenant-entity-export-rate-limit-title": "Varlık sürümü oluşturma sınırlarını düzenle", + "edit-tenant-entity-import-rate-limit-title": "Varlık sürümü yükleme sınırlarını düzenle", + "edit-tenant-notification-request-rate-limit-title": "Bildirim istekleri sınırlarını düzenle", + "edit-tenant-notification-requests-per-rule-rate-limit-title": "Bildirim kuralı başına istek sınırlarını düzenle", + "edit-edge-events-rate-limit": "Edge olayları sınırlarını düzenle", + "edit-edge-events-per-edge-rate-limit": "Edge başına olay sınırlarını düzenle", + "edge-events-rate-limit": "Edge olayları", + "edge-events-per-edge-rate-limit": "Edge başına olaylar", + "edit-edge-uplink-messages-rate-limit": "Edge uplink mesaj sınırlarını düzenle", + "edit-edge-uplink-messages-per-edge-rate-limit": "Edge başına uplink mesaj sınırlarını düzenle", + "edge-uplink-messages-rate-limit": "Edge uplink mesajları", + "edge-uplink-messages-per-edge-rate-limit": "Edge başına uplink mesajları", + "messages-per": "mesaj/saniye", + "not-set": "Ayarlanmadı", + "number-of-messages": "Mesaj sayısı", + "number-of-messages-required": "Mesaj sayısı gereklidir.", + "number-of-messages-min": "Minimum değer 1 olmalıdır.", + "preview": "Önizleme", + "per-seconds": "Saniye başına", + "per-seconds-required": "Zaman oranı gereklidir.", + "per-seconds-min": "Minimum değer 1 olmalıdır.", + "per-seconds-duplicate": "Zaman oranı tekrarı. Her zaman aralığı benzersiz olmalıdır.", + "rate-limits": "Oran sınırlamaları", + "remove-limit": "Sınırı kaldır", + "transport-tenant-msg": "Kiracı taşıma mesajları", + "transport-tenant-telemetry-msg": "Kiracı taşıma telemetri mesajları", + "transport-tenant-telemetry-data-points": "Kiracı taşıma telemetri veri noktaları", + "transport-device-msg": "Cihaz taşıma mesajları", + "transport-device-telemetry-msg": "Cihaz taşıma telemetri mesajları", + "transport-device-telemetry-data-points": "Cihaz taşıma telemetri veri noktaları", + "transport-gateway-msg": "Ağ geçidi taşıma mesajları", + "transport-gateway-telemetry-msg": "Ağ geçidi taşıma telemetri mesajları", + "transport-gateway-telemetry-data-points": "Ağ geçidi taşıma telemetri veri noktaları", + "transport-gateway-device-msg": "Ağ geçidi cihaz taşıma mesajları", + "transport-gateway-device-telemetry-msg": "Ağ geçidi cihaz telemetri mesajları", + "transport-gateway-device-telemetry-data-points": "Ağ geçidi cihaz telemetri veri noktaları", + "sec": "sn" + } }, "timeinterval": { "seconds-interval": "{ seconds, plural, =1 {1 saniye} other {# saniye} }", @@ -2604,26 +5867,39 @@ "hours": "Saat", "minutes": "Dakika", "seconds": "Saniye", - "advanced": "İleri düzey", + "advanced": "Gelişmiş", + "custom": "Özel", "predefined": { "yesterday": "Dün", - "day-before-yesterday": "Dünden önceki gün", - "this-day-last-week": "Geçen hafta bugün", + "day-before-yesterday": "Evvelsi gün", + "this-day-last-week": "Geçen hafta bu gün", "previous-week": "Önceki hafta (Paz - Cmt)", - "previous-week-iso": "Önceki hafta (Pzt - Paz)", - "previous-month": "Geçen ay", - "previous-year": "Geçen yıl", - "current-hour": "Mevcut saat", + "previous-week-iso": "Önceki hafta (Pts - Paz)", + "previous-month": "Önceki ay", + "previous-quarter": "Önceki çeyrek", + "previous-half-year": "Önceki yarı yıl", + "previous-year": "Önceki yıl", + "current-hour": "Geçerli saat", "current-day": "Bugün", - "current-day-so-far": "Şimdiye kadarki gün", + "current-day-so-far": "Bugüne kadar", "current-week": "Bu hafta (Paz - Cmt)", - "current-week-iso": "Bu hafta (Pzt - Paz)", - "current-week-so-far": "Şu ana kadarki hafta (Paz - Cmt)", - "current-week-iso-so-far": "Şu ana kadarki hafta (Pzt - Paz)", + "current-week-iso": "Bu hafta (Pts - Paz)", + "current-week-so-far": "Bu hafta şimdiye kadar (Paz - Cmt)", + "current-week-iso-so-far": "Bu hafta şimdiye kadar (Pts - Paz)", "current-month": "Bu ay", - "current-month-so-far": "Şimdiye kadarki ay", + "current-month-so-far": "Bu ay şimdiye kadar", + "current-quarter": "Bu çeyrek", + "current-quarter-so-far": "Bu çeyrek şimdiye kadar", + "current-half-year": "Bu yarı yıl", + "current-half-year-so-far": "Bu yarı yıl şimdiye kadar", "current-year": "Bu yıl", - "current-year-so-far": "Şu ana kadarki yıl" + "current-year-so-far": "Bu yıl şimdiye kadar" + }, + "type": { + "week": "Hafta (Paz - Cmt)", + "week-iso": "Hafta (Pts - Paz)", + "month": "Ay", + "quarter": "Çeyrek" } }, "timeunit": { @@ -2634,44 +5910,660 @@ "days": "Gün" }, "timewindow": { + "timewindow": "Zaman aralığı", + "timewindow-settings": "Zaman aralığı ayarları", + "years": "{ years, plural, =1 { yıl } other {# yıl } }", + "years-short": "{{ years }}y", + "months": "{ months, plural, =1 { ay } other {# ay } }", + "months-short": "{{ months }}A", + "weeks": "{ weeks, plural, =1 { hafta } other {# hafta } }", + "weeks-short": "{{ weeks }}h", "days": "{ days, plural, =1 { gün } other {# gün } }", + "days-short": "{{ days }}g", "hours": "{ hours, plural, =0 { saat } =1 {1 saat } other {# saat } }", + "hr": "{{ hr }} sa", + "hr-short": "{{ hr }}s", "minutes": "{ minutes, plural, =0 { dakika } =1 {1 dakika } other {# dakika } }", + "min": "{{ min }} dk", + "min-short": "{{ min }}d", "seconds": "{ seconds, plural, =0 { saniye } =1 {1 saniye } other {# saniye } }", - "realtime": "Gerçek zaman", - "history": "Tarih", + "sec": "{{ sec }} sn", + "sec-short": "{{ sec }}s", + "short": { + "years": "{ years, plural, =1 {1 yıl } other {# yıl } }", + "days": "{ days, plural, =1 {1 gün } other {# gün } }", + "hours": "{ hours, plural, =1 {1 saat } other {# saat } }", + "minutes": "{{minutes}} dk ", + "seconds": "{{seconds}} sn " + }, + "realtime": "Gerçek zamanlı", + "history": "Geçmiş", "last-prefix": "son", - "period": "{{ startTime }}'dan {{ endTime }}'a kadar", + "period": "{{ startTime }} ile {{ endTime }} arası", "edit": "Zaman aralığını düzenle", "date-range": "Tarih aralığı", + "for-all-time": "Tüm zamanlar için", "last": "Son", "time-period": "Zaman periyodu", "hide": "Gizle", - "interval": "Aralık" + "interval": "Aralık", + "just-now": "Şu anda", + "just-now-lower": "şu anda", + "ago": "önce", + "style": "Zaman aralığı stili", + "icon": "Simge", + "icon-position": "Simge konumu", + "icon-position-left": "Sol", + "icon-position-right": "Sağ", + "font": "Yazı tipi", + "color": "Renk", + "displayTypePrefix": "Gerçek zamanlı/Geçmiş öneki göster", + "preview": "Önizleme", + "relative": "Göreceli", + "range": "Aralık", + "hide-timewindow-section": "Zaman aralığı bölümünü son kullanıcılardan gizle", + "hide-last-interval": "Son aralığı son kullanıcılardan gizle", + "hide-relative-interval": "Göreceli aralığı son kullanıcılardan gizle", + "hide-fixed-interval": "Sabit aralığı son kullanıcılardan gizle", + "hide-aggregation": "Toplamayı son kullanıcılardan gizle", + "hide-group-interval": "Gruplama aralığını son kullanıcılardan gizle", + "hide-max-values": "Maksimum değerleri son kullanıcılardan gizle", + "hide-timezone": "Zaman dilimini son kullanıcılardan gizle", + "disable-custom-interval": "Özel aralık seçimini devre dışı bırak", + "edit-aggregation-functions-list": "Toplama işlevleri listesini düzenle", + "edit-aggregation-functions-list-hint": "Kullanılabilir seçeneklerin listesi belirtilebilir.", + "allowed-aggregation-functions": "İzin verilen toplama işlevleri", + "edit-intervals-list": "Aralık listesini düzenle", + "allowed-agg-intervals": "Gruplama aralıkları", + "default-agg-interval": "Varsayılan gruplama aralığı", + "edit-intervals-list-hint": "Kullanılabilir aralık seçeneklerinin listesi belirtilebilir.", + "edit-grouping-intervals-list-hint": "Gruplama aralıkları listesi ve varsayılan gruplama aralığı yapılandırılabilir.", + "all": "Tümü" + }, + "tooltip": { + "trigger": "Tetikleyici", + "trigger-point": "Nokta", + "trigger-axis": "Eksen", + "label": "Etiket", + "value": "Değer", + "date": "Tarih", + "show-date-time-interval": "Tarih ve saat aralığını göster", + "show-date-time-interval-hint": "Veri toplamaya göre tarih ve saat aralığını göster.", + "hide-zero-tooltip-values": "Sıfır değerleri gizle", + "background-color": "Arka plan rengi", + "background-blur": "Arka plan bulanıklığı" + }, + "unit": { + "set-unit-conversion": "Birim dönüşümünü ayarla", + "unit-settings": { + "unit-settings": "Birim ayarları", + "source-unit": "Kaynak birim", + "source-unit-hint": "Bu, saklanan değerin birimidir. Dönüştürmek istediğiniz birim. Kaynak verinizin kullandığı sembolü girin (örn. m, km, ft, in).", + "target-metric-unit": "Hedef metrik birim", + "target-metric-unit-hint": "Kaynak değerinizi dönüştürmek istediğiniz metrik (SI) birimi seçin (örn. cm, mm, km).", + "target-imperial-unit": "Hedef imperial birim", + "target-imperial-unit-hint": "Kaynak değerinizi dönüştürmek istediğiniz imperial birimi seçin (örn. in, ft, yd).", + "target-hybrid-unit": "Hedef hibrit birim", + "target-hybrid-unit-hint": "Kaynak değerinizi dönüştürmek istediğiniz hibrit birimi seçin (örn. cm, in, km). Hibrit birimler metrik veya imperial birimlerin birleşimidir.", + "enable-unit-conversion": "Birim dönüşümünü etkinleştir", + "enable-unit-conversion-hint": "Dönüştürmeyi etkinleştirmek için açın. Kapalıyken kaynak değeri değiştirilmeden iletilir. İlgili ölçüm grubunda yalnızca bir birim varsa devre dışı bırakılır (örn. Işık akısı, Hava Kalite İndeksi)." + }, + "unit-system": "Birim sistemi", + "unit-system-type": { + "AUTO": "Otomatik", + "METRIC": "Metrik", + "IMPERIAL": "Imperial", + "HYBRID": "Hibrit" + }, + "measures": { + "absorbed-dose-rate": "Emilen doz oranı", + "acceleration": "İvme", + "acidity": "Asitlik", + "air-quality-index": "Hava kalite indeksi", + "amount-of-substance": "Madde miktarı", + "angle": "Açı", + "angular-acceleration": "Açısal ivme", + "area": "Alan", + "area-density": "Alan yoğunluğu", + "capacitance": "Kapasitans", + "catalytic-activity": "Katalitik aktivite", + "catalytic-concentration": "Katalitik konsantrasyon", + "charge": "Yük", + "current-density": "Akım yoğunluğu", + "data-transfer-rate": "Veri aktarım hızı", + "density": "Yoğunluk", + "digital": "Dijital", + "dimension-ratio": "Boyut oranı", + "dynamic-viscosity": "Dinamik viskozite", + "earthquake-magnitude": "Deprem büyüklüğü", + "electric-charge-density": "Elektrik yük yoğunluğu", + "electric-current": "Elektrik akımı", + "electric-dipole-moment": "Elektrik dipol momenti", + "electric-field-strength": "Elektrik alan şiddeti", + "electric-flux": "Elektrik akısı", + "electric-permittivity": "Elektrik geçirgenliği", + "electric-polarizability": "Elektrik polarizabilitesi", + "electrical-conductance": "Elektrik iletkenliği", + "electrical-conductivity": "Elektriksel iletkenlik", + "energy": "Enerji", + "energy-density": "Enerji yoğunluğu", + "force": "Kuvvet", + "frequency": "Frekans", + "fuel-efficiency": "Yakıt verimliliği", + "heat-capacity": "Isı kapasitesi", + "illuminance": "Aydınlanma", + "inductance": "Endüktans", + "kinematic-viscosity": "Kinematik viskozite", + "length": "Uzunluk", + "light-exposure": "Işık maruziyeti", + "linear-charge-density": "Doğrusal yük yoğunluğu", + "logarithmic-ratio": "Logaritmik oran", + "luminous-efficacy": "Işık verimi", + "luminous-flux": "Işık akısı", + "luminous-intensity": "Işık şiddeti", + "magnetic-field-gradient": "Manyetik alan gradyanı", + "magnetic-flux": "Manyetik akı", + "magnetic-flux-density": "Manyetik akı yoğunluğu", + "magnetic-moment": "Manyetik moment", + "magnetic-permeability": "Manyetik geçirgenlik", + "mass": "Kütle", + "mass-fraction": "Kütle oranı", + "molar-concentration": "Mol konsantrasyonu", + "molar-energy": "Mol enerjisi", + "molar-heat-capacity": "Mol ısı kapasitesi", + "molar-mass": "Mol kütlesi", + "number-concentration": "Sayı konsantrasyonu", + "parts-per-million": "Milyonda bir oranı (ppm)", + "power": "Güç", + "power-density": "Güç yoğunluğu", + "pressure": "Basınç", + "radiance": "Işınım", + "radiant-intensity": "Işınım şiddeti", + "radiation-dose": "Radyasyon dozu", + "radioactive-decay": "Radyoaktif bozunma", + "radioactivity": "Radyoaktivite", + "radioactivity-concentration": "Radyoaktivite konsantrasyonu", + "reciprocal-length": "Ters uzunluk", + "resistance": "Direnç", + "reynolds-number": "Reynolds sayısı", + "signal-level": "Sinyal seviyesi", + "solid-angle": "Katı açı", + "specific-energy": "Özgül enerji", + "specific-heat-capacity": "Özgül ısı kapasitesi", + "specific-humidity": "Özgül nem", + "specific-volume": "Özgül hacim", + "speed": "Hız", + "surface-charge-density": "Yüzey yük yoğunluğu", + "surface-tension": "Yüzey gerilimi", + "temperature": "Sıcaklık", + "thermal-conductivity": "Isıl iletkenlik", + "time": "Zaman", + "torque": "Tork", + "turbidity": "Bulanıklık", + "voltage": "Gerilim", + "volume": "Hacim", + "volume-flow": "Hacimsel akış" + }, + "millimeter": "Milimetre", + "centimeter": "Santimetre", + "decimeter": "Desimetre", + "angstrom": "Angström", + "nanometer": "Nanometre", + "micrometer": "Mikrometre", + "meter": "Metre", + "kilometer": "Kilometre", + "inch": "İnç", + "foot": "Ayak", + "foot-us": "Ayak (ABD ölçümü)", + "yard": "Yarda", + "mile": "Mil", + "nautical-mile": "Deniz mili", + "astronomical-unit": "Astronomik birim", + "reciprocal-metre": "Ters metre", + "meter-per-meter": "Metre bölü metre", + "steradian": "Steradyan", + "thou": "Mil", + "barleycorn": "Arpa tanesi", + "hand": "El", + "chain": "Zincir", + "furlong": "Furlong", + "league": "Lig", + "fathom": "Fathom", + "cable": "Kablo", + "link": "Bağlantı", + "rod": "Çubuk", + "nanogram": "Nanogram", + "microgram": "Mikrogram", + "milligram": "Miligram", + "gram": "Gram", + "kilogram": "Kilogram", + "tonne": "Ton", + "ounce": "Ons", + "pound": "Pound", + "stone": "Stone", + "hundredweight-count": "Yüzlibre", + "short-tons": "Kısa ton", + "dalton": "Dalton", + "grain": "Grain", + "drachm": "Dirhem", + "quarter": "Çeyrek", + "slug": "Slug", + "carat": "Karat", + "cubic-millimeter": "Milimetreküp", + "cubic-centimeter": "Santimetreküp", + "cubic-meter": "Metreküp", + "cubic-kilometer": "Kilometreküp", + "microliter": "Mikrolitre", + "milliliter": "Mililitre", + "liter": "Litre", + "hectoliter": "Hektolitre", + "cubic-inch": "İnç küp", + "cubic-foot": "Ayak küp", + "cubic-yard": "Yarda küp", + "fluid-ounce": "Sıvı ons", + "fluid-ounce-per-second": "Saniyede sıvı ons", + "pint": "Pint", + "quart": "Quart", + "gallon": "Galon", + "oil-barrels": "Petrol varili", + "cubic-meter-per-kilogram": "Kilogram başına metreküp", + "gill": "Gill", + "hogshead": "Hogshead", + "teaspoon": "Çay kaşığı", + "tablespoon": "Yemek kaşığı", + "cup": "Bardak", + "celsius": "Santigrat", + "kelvin": "Kelvin", + "rankine": "Rankine", + "fahrenheit": "Fahrenheit", + "percent": "Yüzde", + "meter-per-second": "Metre/saniye", + "kilometer-per-hour": "Kilometre/saat", + "foot-per-second": "Ayak/saniye", + "foot-per-minute": "Ayak/dakika", + "mile-per-hour": "Mil/saat", + "knot": "Knot", + "inch-per-second": "İnç/saniye", + "inch-per-hour": "İnç/saat", + "millimeters-per-minute": "Milimetre/dakika", + "meter-per-minute": "Metre/dakika", + "kilometer-per-hour-squared": "Kilometre/saat²", + "foot-per-second-squared": "Ayak/saniye²", + "pascal": "Pascal", + "kilopascal": "Kilopascal", + "megapascal": "Megapascal", + "gigapascal": "Gigapascal", + "millibar": "Milibar", + "bar": "Bar", + "kilobar": "Kilobar", + "newton": "Newton", + "newton-meter": "Newton metre", + "foot-pounds": "Ayak-pound", + "inch-pounds": "İnç-pound", + "newton-per-meter": "Newton/metre", + "atmospheres": "Atmosfer", + "pounds-per-square-inch": "İnç kare başına pound", + "kilopound-per-square-inch": "İnç kare başına kilopound", + "torr": "Torr", + "inches-of-mercury": "Cıva inç", + "pascal-per-square-meter": "Metrekare başına Pascal", + "pound-per-square-inch": "İnç kare başına pound", + "newton-per-square-meter": "Metrekare başına Newton", + "kilogram-force-per-square-meter": "Metrekare başına kilogram kuvveti", + "pascal-per-square-centimeter": "Santimetrekare başına Pascal", + "ton-force-per-square-inch": "İnç kare başına ton kuvveti", + "kilonewton-per-square-meter": "Metrekare başına kilonewton", + "newton-per-square-millimeter": "Milimetrekare başına Newton", + "microjoule": "Mikrojul", + "millijoule": "Milijul", + "joule": "Jul", + "kilojoule": "Kilojul", + "megajoule": "Megajul", + "gigajoule": "Gigajul", + "watt-hour": "Watt-saat", + "watt-minute": "Watt-dakika", + "kilowatt-hour": "Kilowatt-saat", + "milliwatt-hour": "Miliwatt-saat", + "megawatt-hour": "Megawatt-saat", + "gigawatt-hour": "Gigawatt-saat", + "electron-volts": "Elektron volt", + "joules-per-coulomb": "Coulomb başına jul", + "british-thermal-unit": "İngiliz termal birimi (BTU)", + "thousand-british-thermal-unit": "Bin İngiliz termal birimi", + "million-british-thermal-unit": "Milyon İngiliz termal birimi", + "foot-pound": "Ayak-pound", + "calorie": "Kalori", + "small-calorie": "Küçük kalori", + "kilocalorie": "Kilokalori", + "joule-per-kelvin": "Kelvin başına jul", + "joule-per-kilogram-kelvin": "Kilogram-Kelvin başına jul", + "joule-per-kilogram": "Kilogram başına jul", + "watt-per-meter-kelvin": "Metre-Kelvin başına watt", + "joule-per-cubic-meter": "Metreküp başına jul", + "therm": "Term", + "electric-dipole-moment": "Elektrik dipol momenti", + "magnetic-dipole-moment": "Manyetik dipol momenti", + "debye": "Debye", + "coulomb-per-square-meter-per-volt": "Volt başına metrekare başına Coulomb", + "milliwatt": "Miliwatt", + "microwatt": "Mikrowatt", + "watt": "Watt", + "kilowatt": "Kilowatt", + "megawatt": "Megawatt", + "gigawatt": "Gigawatt", + "metric-horsepower": "Metrik beygir gücü", + "milliwatt-per-square-centimeter": "Santimetrekare başına milivat", + "watt-per-square-centimeter": "Santimetrekare başına vat", + "kilowatt-per-square-centimeter": "Santimetrekare başına kilovat", + "milliwatt-per-square-meter": "Metrekare başına milivat", + "watt-per-square-meter": "Metrekare başına vat", + "kilowatt-per-square-meter": "Metrekare başına kilovat", + "watt-per-square-inch": "İnç kare başına vat", + "kilowatt-per-square-inch": "İnç kare başına kilovat", + "horsepower": "Beygir gücü", + "btu-per-hour": "Saat başına İngiliz termal birimi (BTU)", + "btu-per-second": "Saniye başına İngiliz termal birimi (BTU)", + "btu-per-day": "Gün başına İngiliz termal birimi (BTU)", + "mbtu-per-hour": "Saat başına bin İngiliz termal birimi", + "mbtu-per-second": "Saniye başına bin İngiliz termal birimi", + "mbtu-per-day": "Gün başına bin İngiliz termal birimi", + "mmbtu-per-hour": "Saat başına milyon İngiliz termal birimi", + "mmbtu-per-second": "Saniye başına milyon İngiliz termal birimi", + "mmbtu-per-day": "Gün başına milyon İngiliz termal birimi", + "foot-pound-per-second": "Saniye başına ayak-pound", + "coulomb": "Coulomb", + "millicoulomb": "Milicoulomb", + "microcoulomb": "Mikrocoulomb", + "nanocoulomb": "Nanocoulomb", + "picocoulomb": "Picocoulomb", + "coulomb-per-meter": "Metre başına Coulomb", + "coulomb-per-cubic-meter": "Metreküp başına Coulomb", + "coulomb-per-square-meter": "Metrekare başına Coulomb", + "square-millimeter": "Milimetrekare", + "square-centimeter": "Santimetrekare", + "square-meter": "Metrekare", + "hectare": "Hektar", + "square-kilometer": "Kilometrekare", + "square-inch": "İnç kare", + "square-foot": "Ayak kare", + "square-yard": "Yarda kare", + "acre": "Dönüm", + "square-mile": "Mil kare", + "are": "Ar", + "barn": "Barn", + "circular-inch": "Dairesel inç", + "milliampere-hour": "Miliamper-saat", + "ampere-hours": "Amper-saat", + "kiloampere-hours": "Kiloamper-saat", + "nanoampere": "Nanoamper", + "picoampere": "Pikoamper", + "microampere": "Mikroamper", + "milliampere": "Miliamper", + "ampere": "Amper", + "kiloampere": "Kiloamper", + "megaampere": "Megaamper", + "gigaampere": "Gigaamper", + "microampere-per-square-centimeter": "Santimetrekare başına mikroamper", + "ampere-per-square-meter": "Metrekare başına amper", + "ampere-per-meter": "Metre başına amper", + "oersted": "Oersted", + "bohr-magneton": "Bohr magnetonu", + "ampere-meter-squared": "Amper-metre kare", + "nanovolt": "Nanovolt", + "picovolt": "Pikovolt", + "millivolt": "Milivolt", + "microvolt": "Mikrovolt", + "volt": "Volt", + "kilovolt": "Kilovolt", + "megavolt": "Megavolt", + "dbmV": "Desibel volt", + "dbm": "Desibel-milivat", + "volt-meter": "Volt-metre", + "kilovolt-meter": "Kilovolt-metre", + "megavolt-meter": "Megavolt-metre", + "microvolt-meter": "Mikrovolt-metre", + "millivolt-meter": "Milivolt-metre", + "nanovolt-meter": "Nanovolt-metre", + "ohm": "Ohm", + "microohm": "Mikroohm", + "milliohm": "Miliohm", + "kilohm": "Kiloohm", + "megohm": "Megaohm", + "gigohm": "Gigaohm", + "millihertz": "Milihertz", + "hertz": "Hertz", + "kilohertz": "Kilohertz", + "megahertz": "Megahertz", + "gigahertz": "Gigahertz", + "terahertz": "Terahertz", + "rpm": "Dakikada devir (RPM)", + "candela-per-square-meter": "Metrekare başına kandela", + "candela": "Kandela", + "lumen": "Lümen", + "lux": "Lüks", + "foot-candle": "Ayak-mum", + "lumen-per-square-meter": "Metrekare başına lümen", + "lux-second": "Lüks saniye", + "lumen-second": "Lümen saniye", + "lumens-per-watt": "Vat başına lümen", + "mole": "Mol", + "nanomole": "Nanomol", + "micromole": "Mikromol", + "millimole": "Milimol", + "kilomole": "Kilomol", + "mole-per-cubic-meter": "Metreküp başına mol", + "rssi": "Alınan sinyal gücü göstergesi", + "ppm": "Milyonda birim (ppm)", + "ppb": "Milyarda birim (ppb)", + "micrograms-per-cubic-meter": "Metreküp başına mikrogram", + "aqi": "Hava Kalitesi İndeksi (AQI)", + "gram-per-cubic-meter": "Metreküp başına gram", + "gram-per-kilogram": "Özgül nem", + "millimeters-per-second": "Saniyede milimetre", + "neper": "Neper", + "bel": "Bel", + "decibel": "Desibel", + "meters-per-second-squared": "Saniyede metre kare", + "becquerel": "Becquerel", + "curie": "Curie", + "gray": "Gray", + "sievert": "Sievert", + "roentgen": "Roentgen", + "cps": "Saniyede sayım", + "rad": "Rad", + "rem": "Rem", + "dps": "Saniyede ayrışma", + "rutherford": "Rutherford", + "coulombs-per-kilogram": "Kilogram başına coulomb", + "becquerels-per-cubic-meter": "Metreküp başına becquerel", + "curies-per-liter": "Litre başına curie", + "becquerels-per-second": "Saniyede becquerel", + "curies-per-second": "Saniyede curie", + "gy-per-second": "Saniyede gray", + "watt-per-steradian": "Steradyan başına watt", + "watt-per-square-metre-steradian": "Metrekare-steradyan başına watt", + "ph-level": "pH seviyesi", + "turbidity": "Bulanıklık", + "mg-per-liter": "Litre başına miligram", + "microsiemens-per-centimeter": "Santimetre başına mikrosiemens", + "millisiemens-per-meter": "Metre başına milisiemens", + "siemens-per-meter": "Metre başına siemens", + "kilogram-per-cubic-meter": "Metreküp başına kilogram", + "gram-per-cubic-centimeter": "Santimetreküp başına gram", + "kilogram-per-square-meter": "Metrekare başına kilogram", + "milligram-per-milliliter": "Mililitre başına miligram", + "milligram-per-cubic-meter": "Metreküp başına miligram", + "pound-per-cubic-foot": "Fit küp başına pound", + "ounces-per-cubic-inch": "İnç küp başına ons", + "tons-per-cubic-yard": "Yarda küp başına ton", + "particle-density": "Parçacık yoğunluğu", + "kilometers-per-liter": "Litre başına kilometre", + "miles-per-gallon": "Galon başına mil", + "liters-per-100-km": "100 km başına litre", + "gallons-per-mile": "Mil başına galon", + "liters-per-hour": "Saatte litre", + "gallons-per-hour": "Saatte galon", + "beats-per-minute": "Dakikada atım", + "millimeters-of-mercury": "Milimetre cıva", + "milligrams-per-deciliter": "Desilitre başına miligram", + "g-force": "G-kuvveti", + "kilonewton": "Kilonewton", + "kilogram-force": "Kilogram-kuvvet", + "pound-force": "Pound-kuvvet", + "kilopound-force": "Kilopound-kuvvet", + "dyne": "Dyn", + "poundal": "Poundal", + "kip": "Kip", + "gal": "Gal", + "gravity": "Yerçekimi", + "hectopascal": "Hektopascal", + "atmosphere": "Atmosfer", + "millibars": "Milibar", + "inch-of-mercury": "İnç cıva", + "richter-scale": "Richter ölçeği", + "nanosecond": "Nanonsaniye", + "microsecond": "Mikrosaniye", + "millisecond": "Milisaniye", + "second": "Saniye", + "minute": "Dakika", + "hour": "Saat", + "day": "Gün", + "week": "Hafta", + "month": "Ay", + "year": "Yıl", + "cubic-foot-per-minute": "Dakikada fit küp", + "cubic-meters-per-hour": "Saatte metreküp", + "cubic-meters-per-second": "Saniyede metreküp", + "liter-per-second": "Saniyede litre", + "liter-per-minute": "Dakikada litre", + "gallons-per-minute": "Dakikada galon", + "cubic-foot-per-second": "Saniyede fit küp", + "milliliters-per-minute": "Dakikada mililitre", + "cubic-decimeter-per-second": "Saniyede desimetreküp", + "bit": "Bit", + "byte": "Bayt", + "kilobyte": "Kilobayt", + "megabyte": "Megabayt", + "gigabyte": "Gigabayt", + "terabyte": "Terabayt", + "petabyte": "Petabayt", + "exabyte": "Eksabayt", + "zettabyte": "Zettabayt", + "yottabyte": "Yottabayt", + "bit-per-second": "Saniyede bit", + "kilobit-per-second": "Saniyede kilobit", + "megabit-per-second": "Saniyede megabit", + "gigabit-per-second": "Saniyede gigabit", + "terabit-per-second": "Saniyede terabit", + "byte-per-second": "Saniyede bayt", + "kilobyte-per-second": "Saniyede kilobayt", + "megabyte-per-second": "Saniyede megabayt", + "gigabyte-per-second": "Saniyede gigabayt", + "degree": "Derece", + "radian": "Radyan", + "gradian": "Gradyan", + "arcminute": "Yay dakikası", + "arcsecond": "Yay saniyesi", + "milliradian": "Miliradyan", + "revolution": "Devir", + "siemens": "Siemens", + "millisiemens": "Millisimens", + "microsiemens": "Mikrosimens", + "kilosiemens": "Kilosimens", + "megasiemens": "Megasimens", + "gigasiemens": "Gigasimens", + "farad": "Farad", + "millifarad": "Milifarad", + "microfarad": "Mikrofarad", + "nanofarad": "Nanofarad", + "picofarad": "Pikofarad", + "kilofarad": "Kilofarad", + "megafarad": "Megafarad", + "gigafarad": "Gigafarad", + "terfarad": "Terafarad", + "farad-per-meter": "Metre başına farad", + "tesla": "Tesla", + "gauss": "Gauss", + "kilogauss": "Kilogauss", + "millitesla": "Militesla", + "microtesla": "Mikrotesla", + "nanotesla": "Nanotesla", + "kilotesla": "Kilotesla", + "megatesla": "Megatesla", + "millitesla-square-meters": "Militesla metrekare", + "gamma": "Gamma", + "lambda": "Lambda", + "square-meter-per-second": "Saniyede metrekare", + "square-centimeter-per-second": "Saniyede santimetrekare", + "stoke": "Stokes", + "centistokes": "Sentistokes", + "square-foot-per-second": "Saniyede fit kare", + "square-inch-per-second": "Saniyede inç kare", + "pascal-second": "Pascal saniye", + "centipoise": "Sentipoise", + "poise": "Poise", + "reynolds": "Reynolds", + "pound-per-foot-hour": "Fit saat başına pound", + "newton-second-per-square-meter": "Metrekare başına newton saniye", + "dyne-second-per-square-centimeter": "Santimetrekare başına dyne saniye", + "kilogram-per-meter-second": "Metre saniye başına kilogram", + "tesla-square-meters": "Tesla metrekare", + "maxwell": "Maxwell", + "tesla-per-meter": "Metre başına tesla", + "gauss-per-centimeter": "Santimetre başına gauss", + "weber": "Weber", + "microweber": "Mikroweber", + "milliweber": "Miliweber", + "gauss-square-centimeter": "Gauss santimetrekare", + "kilogauss-square-centimeter": "Kilogauss santimetrekare", + "henry": "Henry", + "millihenry": "Milihenry", + "microhenry": "Mikrohenry", + "nanohenry": "Nanohenry", + "henry-per-meter": "Metre başına henry", + "tesla-meter-per-ampere": "Amper başına tesla metre", + "gauss-per-oersted": "Oersted başına gauss", + "kilogram-per-mole": "Mol başına kilogram", + "gram-per-mole": "Mol başına gram", + "milligram-per-mole": "Mol başına miligram", + "joule-per-mole": "Mol başına joule", + "joule-per-mole-kelvin": "Mol-kelvin başına joule", + "millivolts-per-meter": "Metre başına milivolt", + "volts-per-meter": "Metre başına volt", + "kilovolts-per-meter": "Metre başına kilovolt", + "radian-per-second": "Saniyede radyan", + "radian-per-second-squared": "Saniyede kare radyan", + "revolutions-per-minute-per-second": "Açısal ivme", + "deg-per-second": "Saniyede derece", + "rotation-per-minute": "Dakikada devir", + "degrees-brix": "Derece brix", + "katal": "Katal", + "katal-per-cubic-metre": "Metreküp başına katal", + "paris-inch": "Paris inç" }, "user": { "user": "Kullanıcı", "users": "Kullanıcılar", - "customer-users": "Kullanıcılar", - "tenant-admins": "Tenant Adminleri", + "customer-users": "Müşteri kullanıcıları", + "tenant-admins": "Kiracı yöneticileri", "sys-admin": "Sistem yöneticisi", - "tenant-admin": "Tenant yöneticisi", - "customer": "Kullanıcı Grubu", + "tenant-admin": "Kiracı yöneticisi", + "customer": "Müşteri", "anonymous": "Anonim", "add": "Kullanıcı ekle", "delete": "Kullanıcı sil", "add-user-text": "Yeni kullanıcı ekle", - "no-users-text": "Hiçbir kullanıcı bulunamadı", + "no-users-text": "Kullanıcı bulunamadı", "user-details": "Kullanıcı detayları", - "delete-user-title": "'{{userEmail}}' kullanıcısını silmek istediğinize emin misiniz?", - "delete-user-text": "UYARI: Onaylandıktan sonra kullanıcı ve ilişkili tüm verileri geri yüklenemez şekilde silinecektir.", - "delete-users-title": "{ count, plural, =1 {1 kullanıcıyı} other {# kullanıcıyı} } sikmek istediğinize emin misiniz?", - "delete-users-action-title": "{ count, plural, =1 {1 kullancıyı} other {# kullanıcıyı} } sil", - "delete-users-text": "UYARI: Onaylandıktan sonra kullanıcı ve ilişkili tüm verileri geri yüklenemez şekilde silinecektir.", - "activation-email-sent-message": "Etkinleştirme e-postası başarılı bir şekilde gönderildi!", - "resend-activation": "Etkinleştirme e-postasını yeniden gönder", + "delete-user-title": "Kullanıcı '{{userEmail}}' silinsin mi?", + "delete-user-text": "Dikkatli olun, onaydan sonra kullanıcı ve tüm ilişkili veriler geri alınamaz hale gelecektir.", + "delete-users-title": "{ count, plural, =1 {1 kullanıcı} other {# kullanıcı} } silinsin mi?", + "delete-users-action-title": "{ count, plural, =1 {1 kullanıcıyı sil} other {# kullanıcıyı sil} }", + "delete-users-text": "Dikkatli olun, onaydan sonra seçili tüm kullanıcılar ve ilişkili veriler geri alınamaz hale gelecektir.", + "activation-email-sent-message": "Aktivasyon e-postası başarıyla gönderildi!", + "resend-activation": "Aktivasyonu yeniden gönder", "email": "E-posta", - "email-required": "E-posta gerekli.", + "email-required": "E-posta gereklidir.", "invalid-email-format": "Geçersiz e-posta formatı.", "first-name": "Ad", "last-name": "Soyad", @@ -2680,235 +6572,953 @@ "always-fullscreen": "Her zaman tam ekran", "select-user": "Kullanıcı seç", "no-users-matching": "'{{entity}}' ile eşleşen kullanıcı bulunamadı.", - "user-required": "Kullanıcı gerekli", - "activation-method": "Etkinleştirme yöntemi", - "display-activation-link": "Etkinleştirme bağlantısını görüntüle", - "send-activation-mail": "Etkinleştirme e-postası gönder", - "activation-link": "Kullanıcı hesabını etkinleştirme bağlantısı", - "activation-link-text": "Kullanıcı hesabını etkinleştirmek için bağlantıyı kullanın:", - "copy-activation-link": "Etkinleştirme bağlantısını kopyala", - "activation-link-copied-message": "Kullanıcı hesabı etkinleştirme bağlantısı panoya kopyalandı", - "details": "Ayrıntılar", - "login-as-tenant-admin": "Tenant Yönetici Girişi", - "login-as-customer-user": "Kullanıcı olarak giriş yap", - "search": "Kullanıcı ara", + "user-required": "Kullanıcı gereklidir", + "activation-method": "Aktivasyon yöntemi", + "display-activation-link": "Aktivasyon bağlantısını göster", + "send-activation-mail": "Aktivasyon e-postası gönder", + "activation-link": "Kullanıcı aktivasyon bağlantısı", + "activation-link-text": "Kullanıcıyı etkinleştirmek için şu aktivasyon bağlantısını kullanın ({{activationLinkTtl}} içinde sona erer):", + "copy-activation-link": "Aktivasyon bağlantısını kopyala", + "activation-link-copied-message": "Kullanıcı aktivasyon bağlantısı panoya kopyalandı", + "details": "Detaylar", + "login-as-tenant-admin": "Kiracı Yöneticisi olarak giriş yap", + "login-as-customer-user": "Müşteri kullanıcısı olarak giriş yap", + "search": "Kullanıcıları ara", "selected-users": "{ count, plural, =1 {1 kullanıcı} other {# kullanıcı} } seçildi", "disable-account": "Kullanıcı Hesabını Devre Dışı Bırak", "enable-account": "Kullanıcı Hesabını Etkinleştir", "enable-account-message": "Kullanıcı hesabı başarıyla etkinleştirildi!", - "disable-account-message": "Kullanıcı hesabı başarıyla devre dışı bırakıldı!" + "disable-account-message": "Kullanıcı hesabı başarıyla devre dışı bırakıldı!", + "copyId": "Kullanıcı Kimliğini kopyala", + "idCopiedMessage": "Kullanıcı Kimliği panoya kopyalandı", + "user-list": "Kullanıcı listesi", + "user-list-required": "Kullanıcı listesi gereklidir" }, "value": { "type": "Değer türü", - "string": "String", - "string-value": "String değeri", - "string-value-required": "String değeri gerekli", - "integer": "Integer", - "integer-value": "Integer değeri", - "integer-value-required": "Integer değeri gerekli", - "invalid-integer-value": "Geçersiz integer değeri", - "double": "Double", - "double-value": "Double değeri", - "double-value-required": "Double değeri gerekli", - "boolean": "Boolean", - "boolean-value": "Boolean değeri", - "false": "False", - "true": "True", - "long": "Long", + "string": "Metin", + "string-value": "Metin değeri", + "string-value-required": "Metin değeri gereklidir", + "integer": "Tamsayı", + "integer-value": "Tamsayı değeri", + "integer-value-required": "Tamsayı değeri gereklidir", + "invalid-integer-value": "Geçersiz tamsayı değeri", + "double": "Ondalık", + "double-value": "Ondalık değeri", + "double-value-required": "Ondalık değeri gereklidir", + "boolean": "Mantıksal", + "boolean-value": "Mantıksal değer", + "false": "Yanlış", + "true": "Doğru", + "long": "Uzun", "json": "JSON", "json-value": "JSON değeri", - "json-value-invalid": "JSON formatı geçersiz", - "json-value-required": "JSON değeri gerekli." + "json-value-invalid": "JSON değeri geçersiz bir formata sahip", + "json-value-required": "JSON değeri gereklidir." + }, + "version-control": { + "version-control": "Sürüm kontrolü", + "management": "Sürüm kontrol yönetimi", + "search": "Sürümleri ara", + "branch": "Dal", + "default": "Varsayılan", + "select-branch": "Dal seçin", + "branch-required": "Dal gereklidir", + "create-entity-version": "Varlık sürümü oluştur", + "version-name": "Sürüm adı", + "version-name-required": "Sürüm adı gereklidir", + "author": "Yazar", + "export-relations": "İlişkileri dışa aktar", + "export-attributes": "Öznitelikleri dışa aktar", + "export-credentials": "Kimlik bilgilerini dışa aktar", + "export-calculated-fields": "Hesaplanmış alanları dışa aktar", + "entity-versions": "Varlık sürümleri", + "versions": "Sürümler", + "created-time": "Oluşturulma zamanı", + "version-id": "Sürüm kimliği", + "no-entity-versions-text": "Varlık sürümü bulunamadı", + "no-versions-text": "Sürüm bulunamadı", + "copy-full-version-id": "Tam sürüm kimliğini kopyala", + "create-version": "Sürüm oluştur", + "creating-version": "Sürüm oluşturuluyor... Lütfen bekleyin", + "nothing-to-commit": "Kaydedilecek değişiklik yok", + "restore-version": "Sürümü geri yükle", + "restore-entity-from-version": "'{{versionName}}' sürümünden varlığı geri yükle", + "restoring-entity-version": "Varlık sürümü geri yükleniyor... Lütfen bekleyin", + "load-relations": "İlişkileri yükle", + "load-attributes": "Öznitelikleri yükle", + "load-credentials": "Kimlik bilgilerini yükle", + "load-calculated-fields": "Hesaplanmış alanları yükle", + "compare-with-current": "Mevcut ile karşılaştır", + "diff-entity-with-version": "'{{versionName}}' sürümü ile farkları karşılaştır", + "previous-difference": "Önceki fark", + "next-difference": "Sonraki fark", + "current": "Mevcut", + "differences": "{ count, plural, =1 {1 fark} other {# fark} }", + "create-entities-version": "Varlık sürümü oluştur", + "default-sync-strategy": "Varsayılan senkronizasyon stratejisi", + "sync-strategy-merge": "Birleştir", + "sync-strategy-overwrite": "Üzerine yaz", + "entities-to-export": "Dışa aktarılacak varlıklar", + "entities-to-restore": "Geri yüklenecek varlıklar", + "sync-strategy": "Senkronizasyon stratejisi", + "all-entities": "Tüm varlıklar", + "no-entities-to-export-prompt": "Lütfen dışa aktarılacak varlıkları belirtin", + "no-entities-to-restore-prompt": "Lütfen geri yüklenecek varlıkları belirtin", + "add-entity-type": "Varlık türü ekle", + "remove-all": "Tümünü kaldır", + "version-create-result": "{ added, plural, =0 {Hiç varlık} =1 {1 varlık} other {# varlık} } eklendi.
    { modified, plural, =0 {Hiç varlık} =1 {1 varlık} other {# varlık} } değiştirildi.
    { removed, plural, =0 {Hiç varlık} =1 {1 varlık} other {# varlık} } silindi.", + "remove-other-entities": "Diğer varlıkları kaldır", + "find-existing-entity-by-name": "Adına göre mevcut varlığı bul", + "restore-entities-from-version": "'{{versionName}}' sürümünden varlıkları geri yükle", + "restoring-entities-from-version": "Varlıklar geri yükleniyor... Lütfen bekleyin", + "no-entities-restored": "Geri yüklenen varlık yok", + "created": "{{created}} oluşturuldu", + "updated": "{{updated}} güncellendi", + "deleted": "{{deleted}} silindi", + "remove-other-entities-confirm-text": "Dikkatli olun! Bu işlem geri yüklemek istediğiniz sürümde bulunmayan
    tüm mevcut varlıkları kalıcı olarak silmek anlamına gelir.

    Onaylamak için lütfen \"remove other entities\" yazın.", + "auto-commit-to-branch": "{{ branch }} dalına otomatik gönderim", + "default-create-entity-version-name": "{{entityName}} güncellemesi", + "sync-strategy-merge-hint": "Seçili varlıkları depoya ekler veya günceller. Diğer tüm depo varlıkları değiştirilmez.", + "sync-strategy-overwrite-hint": "Seçili varlıkları depoya ekler veya günceller. Diğer tüm depo varlıkları silinir.", + "device-credentials-conflict": "{{entityId}} dış kimliğine sahip cihaz yüklenemedi.
    Aynı kimlik bilgileri başka bir cihaz için veritabanında mevcut.
    Kimlik bilgilerini yükle seçeneğini geri yükleme formunda devre dışı bırakmayı düşünebilirsiniz.", + "missing-referenced-entity": "{{sourceEntityTypeName}} türünde, {{sourceEntityId}} dış kimliğine sahip varlık yüklenemedi.
    Çünkü {{targetEntityTypeName}} türünde ve {{targetEntityId}} kimliğine sahip eksik bir varlığa referans veriyor.", + "runtime-failed": "Başarısız: {{message}}", + "auto-commit-settings-read-only-hint": "Otomatik kayıt özelliği, depo ayarlarında salt okunur seçeneği etkinleştirildiğinde çalışmaz.", + "rollback-on-error": "Hata durumunda geri al", + "rollback-on-error-hint": "Geri yüklenecek çok sayıda varlığınız varsa bu seçeneği devre dışı bırakmak performansı artırabilir.\nNot: Sürüm yükleme sırasında bir hata oluşursa, zaten kaydedilen varlıklar (ilişkiler, öznitelikler vb. ile) aynı şekilde kalır." }, "widget": { - "widget-library": "Gösterge Kütüphanesi", - "widget-bundle": "Gösterge Paketi", - "select-widgets-bundle": "Gösterge paketi seç", - "management": "Gösterge yönetimi", - "editor": "Gösterge düzenleyici", - "widget-type-not-found": "Gösterge yapılandırması yüklenemedi.
    Muhtemelen ilgili\n gösterge türü kaldırılmış.", - "widget-type-load-error": "Gösterge şu sebeplerden dolayı yüklenemedi:", - "remove": "Göstergeyi kaldır", - "edit": "Göstergeyi düzenle", - "remove-widget-title": "'{{widgetTitle}}' isimli göstermeyi kaldırmak istediğinizden emin misiniz?", - "remove-widget-text": "UYARI: Onaylandıktan sonra gösterge ve tüm ilişkili verileri geri yüklenemez şekilde silinecek.", + "widget-library": "Bileşen kitaplığı", + "widget-bundle": "Bileşen Paketi", + "all-bundles": "Tüm paketler", + "select-widgets-bundle": "Bileşen paketi seçin", + "widgets": "Bileşenler", + "all-widgets": "Tüm bileşenler", + "widget": "Bileşen", + "select-widget": "Bileşen seçin", + "no-widgets-matching": "'{{entity}}' ile eşleşen bileşen bulunamadı.", + "no-widgets": "Henüz bileşen yok", + "no-widgets-text": "Bileşen bulunamadı", + "management": "Bileşen yönetimi", + "editor": "Bileşen Editörü", + "confirm-to-exit-editor-html": "Kaydedilmemiş widget ayarlarınız var.
    Bu sayfadan ayrılmak istediğinizden emin misiniz?", + "widget-type-not-found": "Widget yapılandırması yüklenirken sorun oluştu.
    Muhtemelen ilişkili\n widget türü silinmiş.", + "widget-type-load-error": "Widget aşağıdaki hatalar nedeniyle yüklenemedi:", + "remove": "Bileşeni kaldır", + "delete": "Bileşeni sil", + "edit": "Bileşeni düzenle", + "remove-widget-title": "'{{widgetTitle}}' bileşenini kaldırmak istediğinize emin misiniz?", + "remove-widget-text": "Onaydan sonra bileşen ve ilgili tüm veriler geri alınamaz hale gelecektir.", + "replace-reference-with-widget-copy": "Referansı bileşen kopyasıyla değiştir", "timeseries": "Zaman serisi", - "search-data": "Arama verileri", + "search-data": "Veri ara", "no-data-found": "Veri bulunamadı", - "latest": "Son değerler", - "rpc": "Kontrol göstergesi", - "alarm": "Alarm göstergesi", - "static": "Statik gösterge", - "select-widget-type": "Gösterge türü seç", - "missing-widget-title-error": "Gösterge başlığı belirtilmelidir!", - "widget-saved": "Gösterge kaydedildi", - "unable-to-save-widget-error": "Gösterge kaydedilemedi! Göstergede hatalar mevcut!", - "save": "Göstergeyi kaydet", - "saveAs": "Göstergeyi farklı kaydet", - "save-widget-type-as": "Gösterge türünü farklı kaydet", - "save-widget-type-as-text": "Lütfen gösterge başlığı girin veya hedef gösterge paketi seçin", - "toggle-fullscreen": "Tam ekran aç/kapat", - "run": "Göstergeyi çalıştır", - "title": "Gösterge başlığı", - "title-required": "Gösterge başlığı gerekli.", - "type": "Gösterge türü", + "latest": "En son değerler", + "rpc": "Kontrol bileşeni", + "alarm": "Alarm bileşeni", + "static": "Statik bileşen", + "timeseries-short": "seri", + "latest-short": "son", + "rpc-short": "kontrol", + "alarm-short": "alarm", + "static-short": "statik", + "select-widget-type": "Bileşen türü seçin", + "missing-widget-title-error": "Bileşen başlığı belirtilmelidir!", + "widget-saved": "Bileşen kaydedildi", + "unable-to-save-widget-error": "Bileşen kaydedilemedi! Hatalar içeriyor!", + "save": "Bileşeni kaydet", + "saveAs": "Bileşeni farklı kaydet", + "move": "Bileşeni taşı", + "save-widget-as": "Bileşeni farklı kaydet", + "save-widget-as-text": "Lütfen yeni bileşen başlığını girin", + "toggle-fullscreen": "Tam ekranı aç/kapat", + "run": "Bileşeni çalıştır", + "widget-title": "Bileşen başlığı", + "title": "Başlık", + "title-required": "Bileşen başlığı gereklidir.", + "title-max-length": "Başlık 256 karakterden kısa olmalıdır", + "system": "Sistem", + "type": "Bileşen türü", "resources": "Kaynaklar", - "resource-url": "JavaScript / CSS URL", + "resource-url": "JavaScript/CSS URL", + "resource-is-extension": "Eklenti mi", "remove-resource": "Kaynağı kaldır", "add-resource": "Kaynak ekle", "html": "HTML", - "tidy": "Tidy", + "tidy": "Düzenle", "css": "CSS", - "settings-schema": "Ayarlar şeması", - "datakey-settings-schema": "Veri anahtarı ayarları şeması", - "widget-settings": "Gösterge Ayarları", + "settings-form": "Ayar formu", + "data-key-settings-form": "Veri anahtarı ayar formu", + "latest-data-key-settings-form": "Son veri anahtarı ayar formu", + "widget-settings": "Bileşen ayarları", "description": "Açıklama", - "image-preview": "Resim Önizleme", + "tags": "Etiketler", + "image-preview": "Görsel önizleme", + "settings-form-selector": "Ayar formu seçici", + "data-key-settings-form-selector": "Veri anahtarı ayar formu seçici", + "latest-data-key-settings-form-selector": "Son veri anahtarı ayar formu seçici", + "all": "Tümü", + "actual": "Gerçek", + "scada": "SCADA sembolü", + "deprecated": "Kullanımdan kaldırıldı", + "has-basic-mode": "Temel modu var", + "basic-mode-form-selector": "Temel mod formu seçici", + "basic-mode": "Temel", + "advanced-mode": "Gelişmiş", "javascript": "Javascript", "js": "JS", - "add-widget-type": "Yeni gösterge türü ekle", - "widget-template-load-failed-error": "Gösterge şablonu yüklenemedi!", - "add": "Gösterge ekle", - "undo": "Gösterge değişikliklerini geri al", - "export": "Göstergeyi dışa aktar", - "no-data": "Göstergede görüntülenecek veri yok", - "data-overflow": "Gösterge, {{total}} öğeden {{count}} tanesini gösteriyor", - "alarm-data-overflow": "Gösterge, {{totalEntities}} öğeden {{allowedEntities}} (izin verilen maksimum) öğe için alarm görüntüler", - "search": "Gösterge ara", - "filter": "Gösterge filtre türü", - "loading-widgets": "Göstergeler yükleniyor..." + "delete-widget-title": "'{{widgetName}}' bileşenini silmek istediğinize emin misiniz?", + "delete-widget-text": "Onaydan sonra bileşen ve ilgili tüm veriler geri alınamaz hale gelecektir.", + "delete-widgets-title": "{ count, plural, =1 {1 bileşeni} other {# bileşeni} } silmek istediğinize emin misiniz?", + "delete-widgets-text": "Dikkatli olun, onaydan sonra seçilen tüm bileşenler silinecek ve ilgili tüm veriler geri alınamaz hale gelecektir.", + "delete-widget": "Bileşeni sil", + "widget-template-load-failed-error": "Bileşen şablonu yüklenemedi!", + "details": "Detaylar", + "widget-details": "Bileşen detayları", + "add": "Bileşen ekle", + "add-existing-widget": "Mevcut bileşeni ekle", + "add-new-widget": "Yeni bileşen ekle", + "search-widgets": "Bileşenleri ara", + "selected-widgets": "{ count, plural, =1 {1 bileşen} other {# bileşen} } seçildi", + "undo": "Bileşen değişikliklerini geri al", + "export": "Bileşeni dışa aktar", + "export-prompt": "Bileşen görsellerini ve kaynaklarını göm", + "export-widgets": "Bileşenleri dışa aktar", + "export-widgets-prompt": "Bileşen görsellerini ve kaynaklarını göm", + "import": "Bileşen içe aktar", + "no-data": "Bileşende görüntülenecek veri yok", + "data-overflow": "Bileşen {{total}} varlıktan {{count}} tanesini görüntülüyor", + "alarm-data-overflow": "Bileşen, {{totalEntities}} varlıktan yalnızca {{allowedEntities}} (maksimum izin verilen) varlık için alarm gösteriyor", + "search": "Bileşen ara", + "filter": "Bileşen filtre türü", + "loading-widgets": "Bileşenler yükleniyor...", + "widget-template-error": "Geçersiz bileşen HTML şablonu.", + "reference": "Referans" }, "widget-action": { - "header-button": "Gösterge başlık butonu", - "open-dashboard-state": "Yeni kontrol paneli durumunua git", - "update-dashboard-state": "Kontrol paneli durumunu güncelle", - "open-dashboard": "Diğer kontrol paneline git", + "header-button": "Bileşen başlık düğmesi", + "do-nothing": "Hiçbir şey yapma", + "open-dashboard-state": "Yeni pano durumuna git", + "update-dashboard-state": "Mevcut pano durumunu güncelle", + "open-dashboard": "Başka bir panoya git", "custom": "Özel eylem", - "custom-pretty": "Özel Aksiyon (HTML şablonuyla)", - "mobile-action": "Mobil Aksiyon", - "target-dashboard-state": "Hedef kontrol paneli durumu", - "target-dashboard-state-required": "Hedef kontrol paneli durumu gerekli", - "set-entity-from-widget": "Göstergeden öğe belirle", - "target-dashboard": "Hedef kontrol paneli", - "open-right-layout": "Sağdaki kontrol paneli arayüz düzenini aç(mobil görünüm)", - "open-in-separate-dialog": "Ayrı iletişim kutusunda aç", - "dialog-title": "iletişim kutusu başlığı", - "dialog-hide-dashboard-toolbar": "İletişim kutusunda gösterge paneli araç çubuğunu gizle", - "dialog-width": "Görüş alanı genişliğine göre yüzde olarak iletişim kutusu genişliği", - "dialog-height": "Görüş alanı yüksekliğine göre yüzde olarak iletişim kutusu yüksekliği", - "dialog-size-range-error": "İletişim kutusu boyutu yüzde değeri 1 ile 100 arasında olmalıdır.", + "custom-pretty": "Özel eylem (HTML şablonuyla)", + "custom-pretty-error-title": "Özel pencere hatası", + "custom-pretty-template-error": "Geçersiz özel pencere şablonu.", + "custom-pretty-controller-error": "Özel pencere işlevi değerlendirilirken hata oluştu.", + "mobile-action": "Mobil eylem", + "target-dashboard-state": "Hedef pano durumu", + "target-dashboard-state-required": "Hedef pano durumu gerekli", + "set-entity-from-widget": "Varlığı bileşenden ayarla", + "target-dashboard": "Hedef pano", + "select-target-dashboard": "Hedef panoyu seç", + "target-dashboard-required": "Hedef pano gereklidir.", + "open-right-layout": "Sağ pano düzenini aç (mobil görünüm)", + "state-display-type": "Pano durumu görüntüleme seçeneği", + "open-normal": "Normal", + "open-in-separate-dialog": "Ayrı bir pencerede aç", + "open-in-popover": "Açılır pencerede aç", + "dialog-title": "Pencere başlığı", + "dialog-hide-dashboard-toolbar": "Pano araç çubuğunu pencerede gizle", + "dialog-width": "Pencere genişliği (görünüm alanına göre yüzde)", + "dialog-height": "Pencere yüksekliği (görünüm alanına göre yüzde)", + "dialog-size-range-error": "Pencere boyutu yüzde değeri 1 ile 100 arasında olmalıdır.", + "popover-preferred-placement": "Tercih edilen açılır konumu", + "popover-placement-top": "Üst", + "popover-placement-topLeft": "Üst sol", + "popover-placement-topRight": "Üst sağ", + "popover-placement-right": "Sağ", + "popover-placement-rightTop": "Sağ üst", + "popover-placement-rightBottom": "Sağ alt", + "popover-placement-bottom": "Alt", + "popover-placement-bottomLeft": "Alt sol", + "popover-placement-bottomRight": "Alt sağ", + "popover-placement-left": "Sol", + "popover-placement-leftTop": "Sol üst", + "popover-placement-leftBottom": "Sol alt", + "popover-hide-on-click-outside": "Dışarı tıklayınca açılır pencereyi gizle", + "popover-hide-dashboard-toolbar": "Açılır pencerede pano araç çubuğunu gizle", + "popover-width": "Açılır pencere genişliği", + "popover-height": "Açılır pencere yüksekliği", + "popover-style": "Açılır pencere stili", "open-new-browser-tab": "Yeni bir tarayıcı sekmesinde aç", + "open-URL": "URL aç", + "URL": "URL", + "url-required": "URL gereklidir.", "mobile": { - "action-type": "Mobil aksiyon türü", - "action-type-required": "Mobil aksiyon türü gerekli", - "take-picture-from-gallery": "Galeriden resim al", + "device-provision": "Cihaz sağlama", + "action-type": "Mobil eylem türü", + "select-action-type": "Mobil eylem türünü seçin", + "action-type-required": "Mobil eylem türü gereklidir", + "take-picture-from-gallery": "Galeri'den fotoğraf seç", "take-photo": "Fotoğraf çek", - "map-direction": "Harita yol tarifini aç", + "map-direction": "Harita yönlendirmelerini aç", "map-location": "Harita konumunu aç", - "scan-qr-code": "QR Kodunu Tara", + "scan-qr-code": "QR Kodu tara", "make-phone-call": "Telefon araması yap", "get-location": "Telefon konumunu al", "take-screenshot": "Ekran görüntüsü al" + }, + "custom-action-function": "Özel eylem işlevi", + "custom-pretty-function": "Özel eylem (HTML şablonlu) işlevi", + "map-item-type": "Harita öğesi türü", + "map-item": { + "marker": "İşaretçi", + "polygon": "Poligon", + "rectangle": "Dikdörtgen", + "circle": "Daire" + }, + "place-map-item": "Harita öğesi yerleştir", + "map-item-tooltip": { + "customize-map-item-tooltips": "Harita öğesi araç ipuçlarını özelleştir", + "place-marker": "İşaretçi yerleştir", + "start-draw-rectangle": "Dikdörtgen çizmeye başla", + "finish-draw-rectangle": "Dikdörtgen çizimini bitir", + "start-draw-polygon": "Poligon çizmeye başla", + "continue-draw-polygon": "Poligon çizimine devam et", + "finish-draw-polygon": "Poligon çizimini bitir", + "start-draw-circle": "Daire çizmeye başla", + "finish-draw-circle": "Daire çizimini bitir" } }, "widgets-bundle": { - "current": "Şimdiki paket", - "widgets-bundles": "Gösterge Paketleri", - "add": "Gösterge Paketi Ekle", - "delete": "Gösterge paketini sil", + "current": "Geçerli paket", + "widgets-bundles": "Widget paketleri", + "widgets-bundle-widgets": "Widget Paketi Widget'ları", + "add": "Widget paketi ekle", + "delete": "Widget paketini sil", "title": "Başlık", - "title-required": "Başlık gerekli.", + "title-required": "Başlık gereklidir.", + "title-max-length": "Başlık 256 karakterden kısa olmalıdır", "description": "Açıklama", - "image-preview": "Resim Önizleme", - "add-widgets-bundle-text": "Yeni gösterge paketi ekle", - "no-widgets-bundles-text": "Hiçbir gösterge paketi bulunamadı", - "empty": "Gösterge paketi boş", + "image-preview": "Görsel önizleme", + "scada": "SCADA widget paketi", + "order": "Sıra", + "add-widgets-bundle-text": "Yeni widget paketi ekle", + "no-widgets-bundles-text": "Hiç widget paketi bulunamadı", + "empty": "Widget paketi boş", "details": "Detaylar", - "widgets-bundle-details": "Gösterge paketi detayları", - "delete-widgets-bundle-title": "'{{widgetsBundleTitle}}' isimli gösterge paketini silmek istediğinize emin misiniz?", - "delete-widgets-bundle-text": "UYARI: Onaylandıktan sonra gösterge paketi ve ilişkili tüm veriler geri yüklenemez şekilde silinecektir.", - "delete-widgets-bundles-title": "{ count, plural, =1 {1 gösterge paketini} other {# gösterge paketini} } silmek istediğinize emin misiniz?", - "delete-widgets-bundles-action-title": "{ count, plural, =1 {1 gösterge paketini} other {# gösterge paketini} } sil", - "delete-widgets-bundles-text": "UYARI: Onaylandıktan sonra seçili tüm gösterge paketleri ve ilişkili tüm veriler geri yüklenemez şekilde silinecektir.", - "no-widgets-bundles-matching": "'{{widgetsBundle}}' ile eşleşen gösterge paketi bulunamadı.", - "widgets-bundle-required": "Gösterge paketi gerekli.", + "widgets-bundle-details": "Widget paketi detayları", + "delete-widgets-bundle-title": "‘{{widgetsBundleTitle}}’ widget paketini silmek istediğinizden emin misiniz?", + "delete-widgets-bundle-text": "Dikkatli olun, onaydan sonra widget paketi ve tüm ilişkili veriler geri alınamaz hale gelecek.", + "delete-widgets-bundles-title": "{ count, plural, =1 {1 widget paketi} other {# widget paketi} } silmek istediğinizden emin misiniz?", + "delete-widgets-bundles-action-title": "{ count, plural, =1 {1 widget paketi} other {# widget paketi} } sil", + "delete-widgets-bundles-text": "Dikkatli olun, onaydan sonra seçilen tüm widget paketleri ve ilişkili veriler silinecek ve geri alınamaz hale gelecek.", + "no-widgets-bundles-matching": "‘{{widgetsBundle}}’ ile eşleşen widget paketi bulunamadı.", + "widgets-bundle-required": "Widget paketi gereklidir.", "system": "Sistem", - "import": "Gösterge paketini içe aktar", - "export": "Gösterge paketini dışa aktar", - "export-failed-error": "Gösterge paketini dışa aktaramadı: {{error}}", - "create-new-widgets-bundle": "Yeni gösterge paketi oluştur", - "widgets-bundle-file": "Gösterge paketi dosyası", - "invalid-widgets-bundle-file-error": "Gösterge paketi içe aktarılamadı: Geçersiz gösterge paketi veri yapısı.", - "search": "Gösterge paketi ara", - "selected-widgets-bundles": "{ count, plural, =1 {1 gösterge paketi} other {# gösterge paketi} } seçildi", - "open-widgets-bundle": "Gösterge paketlerini aç", - "loading-widgets-bundles": "Gösterge paketleri yükleniyor..." + "import": "Widget paketi içe aktar", + "export": "Widget paketi dışa aktar", + "export-widgets-bundle-widgets-prompt": "Paket içindeki widget'ları dışa aktarılan veriye dahil et (aksi takdirde yalnızca referans verilen widget FQN'leri dışa aktarılır)", + "export-failed-error": "Widget paketi dışa aktarılamadı: {{error}}", + "create-new-widgets-bundle": "Yeni widget paketi oluştur", + "widgets-bundle-file": "Widget paketi dosyası", + "invalid-widgets-bundle-file-error": "Widget paketi içe aktarılamadı: Geçersiz widget paketi veri yapısı.", + "search": "Widget paketlerinde ara", + "selected-widgets-bundles": "{ count, plural, =1 {1 widget paketi} other {# widget paketi} } seçildi", + "open-widgets-bundle": "Widget paketini aç", + "loading-widgets-bundles": "Widget paketleri yükleniyor...", + "create-new": "Yeni widget paketi oluştur" }, "widget-config": { "data": "Veri", "settings": "Ayarlar", - "advanced": "İleri düzey", + "advanced": "Gelişmiş", + "appearance": "Görünüm", + "widget-card": "Widget kartı", + "mobile": "Mobil", "title": "Başlık", - "title-tooltip": "Başlık İpucu", + "title-tooltip": "Başlık Bilgi Balonu", "general-settings": "Genel ayarlar", - "display-title": "Başlığı göster", - "drop-shadow": "Gölge", + "display-title": "Widget başlığını göster", + "card-title": "Kart başlığı", + "drop-shadow": "Gölge efekti", "enable-fullscreen": "Tam ekranı etkinleştir", "background-color": "Arka plan rengi", - "text-color": "Yazı rengi", - "padding": "İç aralık (Padding)", - "margin": "Dış aralık (Margin)", - "widget-style": "Gösterge stili", + "text-color": "Metin rengi", + "border-radius": "Kenar yuvarlama", + "padding": "İç boşluk", + "margin": "Dış boşluk", + "widget-style": "Widget stili", + "widget-css": "Widget CSS", "title-style": "Başlık stili", - "mobile-mode-settings": "Mobil mod ayarları", + "mobile-mode-settings": "Mobil mod", "order": "Sıra", "height": "Yükseklik", - "mobile-hide": "Göstergeyi mobil modda gizle", - "units": "Değerin yanında göstermek için özel simge", - "decimals": "Noktadan sonraki basamak sayısı", + "mobile-hide": "Mobil modda widget'ı gizle", + "desktop-hide": "Masaüstü modda widget'ı gizle", + "units": "Değerin yanına gösterilecek özel sembol", + "units-by-default": "Varsayılan birimler", + "decimals": "Ondalık basamak sayısı", + "decimals-by-default": "Varsayılan ondalık basamak", + "default-data-key-parameter-hint": "Bu parametre, veri anahtarı konfigürasyonu ile geçersiz kılınmadıkça tüm widget değerleri için geçerlidir", + "units-short": "Birimler", + "decimals-short": "Ondalık", + "decimals-suffix": "ondalık", + "digits-suffix": "basamak", "timewindow": "Zaman aralığı", - "use-dashboard-timewindow": "Kontrol paneli zaman aralığı kullan", - "display-timewindow": "Zaman penceresini göster", - "display-legend": "Lejant göster", + "use-dashboard-timewindow": "Dashboard zaman aralığını kullan", + "use-widget-timewindow": "Widget zaman aralığını kullan", + "display-timewindow": "Zaman aralığını göster", + "legend": "Gösterge", + "display-legend": "Göstergeyi göster", "datasources": "Veri kaynakları", - "maximum-datasources": "En fazla { count, plural, =1 {1 veri kaynağı kullanılabilir.} other {# veri kaynağı kullanılabilir} }", + "datasource": "Veri kaynağı", + "maximum-datasources": "En fazla { count, plural, =1 {1 veri kaynağına izin verilir.} other {# veri kaynağına izin verilir} }", + "timeseries-key-error": "En az bir zaman serisi veri anahtarı belirtilmelidir", "datasource-type": "Tür", "datasource-parameters": "Parametreler", "remove-datasource": "Veri kaynağını kaldır", "add-datasource": "Veri kaynağı ekle", - "target-device": "Hedef aygıt", + "target-device": "Hedef cihaz", "alarm-source": "Alarm kaynağı", "actions": "Eylemler", "action": "Eylem", "add-action": "Eylem ekle", - "search-actions": "Eylem ara", - "no-actions-text": "Aksiyon bulunamadı", + "search-actions": "Eylemleri ara", + "no-actions-text": "Hiçbir eylem bulunamadı", "action-source": "Eylem kaynağı", - "action-source-required": "Eylem kaynağı gerekli.", - "action-name": "İsim", - "action-name-required": "Eylem ismi gerekli.", - "action-name-not-unique": "Aynı ada sahip başka bir işlem zaten var.\nEylem adı, aynı eylem kaynağı içinde emsalsiz olmalıdır.", - "action-icon": "İkon", + "select-action-source": "Eylem kaynağını seçin", + "action-source-required": "Eylem kaynağı gereklidir.", + "column-index": "Sütun indeksi", + "select-column-index": "Sütun indeksini seçin", + "column-index-required": "Sütun indeksi gereklidir.", + "not-set": "Ayarlanmadı", + "action-name": "Ad", + "action-name-required": "Eylem adı gereklidir.", + "action-name-not-unique": "Aynı ada sahip başka bir eylem zaten mevcut.\nEylem adı, aynı eylem kaynağı içinde benzersiz olmalıdır.", + "action-icon": "Simge", + "header-button": { + "button-settings": "Düğme ayarları", + "button-type": "Düğme türü", + "button-type-basic": "Temel", + "button-type-raised": "Yükseltilmiş", + "button-type-stroked": "Çerçeveli", + "button-type-flat": "Düz", + "button-type-icon": "Simge", + "button-type-mini-fab": "FAB", + "colors": "Renkler", + "color": "Renk", + "background": "Arka plan", + "border": "Kenarlık", + "advanced-button-style": "Gelişmiş düğme stili", + "button-style": "Düğme stili" + }, + "show-hide-action-using-function": "Eylemi fonksiyonla göster/gizle", + "show-action-function": "Eylem gösterme fonksiyonu", "action-type": "Tür", - "action-type-required": "Eylem türü gerekli.", + "action-type-required": "Eylem türü gereklidir.", "edit-action": "Eylemi düzenle", "delete-action": "Eylemi sil", - "delete-action-title": "Gösterge eylemini sil", - "delete-action-text": "'{{actionName}}' isimli gösterge eylemini silmek istediğinizden emin misiniz?", - "display-icon": "Başlık simgesini görüntüle", - "icon-color": "İkon rengi", - "icon-size": "İkon boyutu" + "delete-action-title": "Widget eylemini sil", + "delete-action-text": "Adı '{{actionName}}' olan widget eylemini silmek istediğinizden emin misiniz?", + "title-icon": "Başlık simgesi", + "display-icon": "Başlık simgesini göster", + "card-icon": "Kart simgesi", + "icon": "Simge", + "icon-color": "Simge rengi", + "icon-size": "Simge boyutu", + "advanced-settings": "Gelişmiş ayarlar", + "data-settings": "Veri ayarları", + "limits": "Sınırlar", + "no-data-display-message": "\"Gösterilecek veri yok\" alternatif mesajı", + "data-page-size": "Veri kaynağı başına maksimum varlık sayısı", + "settings-component-not-found": "'{{selector}}' seçici için ayar formu bileşeni bulunamadı", + "preview": "Önizleme", + "set": "Ayarla", + "set-message": "Mesajı ayarla", + "advanced-title-style": "Gelişmiş başlık stili", + "card-style": "Kart stili", + "text": "Metin", + "background": "Arka plan", + "advanced-widget-style": "Gelişmiş widget stili", + "card-buttons": "Kart düğmeleri", + "show-card-buttons": "Kart düğmelerini göster", + "card-border-radius": "Kart köşe yuvarlaklığı", + "card-padding": "Kart dolgusu", + "card-appearance": "Kart görünümü", + "color": "Renk", + "tooltip": "Araç ipucu", + "units-required": "Birim gereklidir.", + "list-layout": "Liste düzeni", + "layout": "Düzen", + "resize-options": "Yeniden boyutlandırma seçenekleri", + "resizable": "Yeniden boyutlandırılabilir", + "preserve-aspect-ratio": "En-boy oranını koru" }, "widget-type": { - "import": "Gösterge türünü içer aktar", - "export": "Gösterge türünü dışa aktar", - "export-failed-error": "Gösterge türü dışa aktarılamadı: {{error}}", - "create-new-widget-type": "Yeni gösterge türü oluştur", - "widget-type-file": "Gösterge türü dosyası", - "invalid-widget-type-file-error": "Gösterge türü içe aktarılamadı: Geçersiz gösterge türü veri yapısı." + "import": "Widget türünü içe aktar", + "export": "Widget türünü dışa aktar", + "export-failed-error": "Widget dışa aktarılamadı: {{error}}", + "widget-file": "Widget dosyası", + "invalid-widget-file-error": "Widget içe aktarılamadı: Geçersiz widget veri yapısı." + }, + "markdown": { + "edit": "Düzenle", + "preview": "Önizleme", + "copy-code": "Kopyalamak için tıkla", + "copied": "Kopyalandı!" }, "widgets": { + "mobile-app-qr-code": { + "configuration-hint": "Yapılandırma, platform ana ayarlarındaki Mobil uygulama QR kodu bileşenine bağlıdır", + "get-it-on-google-play": "Google Play'den edinin", + "download-on-the-app-store": "App Store'dan indirin" + }, + "action-button": { + "behavior": "Davranış", + "on-click": "Tıklama ile", + "on-click-hint": "Butona tıklandığında tetiklenen eylem", + "first-button-click": "Birinci buton tıklaması", + "first-button-click-hint": "Birinci butona basıldığında gerçekleşen eylem.", + "second-button-click": "İkinci buton tıklaması", + "second-button-click-hint": "İkinci butona basıldığında gerçekleşen eylem.", + "button-click-hint": "Bileşene basıldığında gerçekleşen eylem." + }, + "command-button": { + "behavior": "Davranış", + "on-click": "Tıklama ile", + "on-click-hint": "Butona tıklandığında gerçekleştirilen eylem." + }, + "power-button": { + "behavior": "Davranış", + "power-on": "Güç 'Açık'", + "power-on-hint": "Bileşeni AÇMAK için gerçekleştirilen eylem.", + "power-off": "Güç 'Kapalı'", + "power-off-hint": "Bileşeni KAPATMAK için gerçekleştirilen eylem.", + "on-label": "Açık", + "off-label": "Kapalı", + "layout": "Yerleşim", + "layout-default": "Varsayılan", + "layout-simplified": "Basitleştirilmiş", + "layout-outlined": "Ana Hatlı", + "layout-default-volume": "Varsayılan.Ses", + "layout-simplified-volume": "Basitleştirilmiş.Ses", + "layout-outlined-volume": "Ana Hatlı.Ses", + "layout-default-icon": "Varsayılan.Simge", + "layout-simplified-icon": "Basitleştirilmiş.Simge", + "layout-outlined-icon": "Ana Hatlı.Simge", + "main": "Ana", + "background": "Arka Plan", + "button-icon-on": "Buton simgesi 'Açık'", + "button-icon-off": "Buton simgesi 'Kapalı'", + "power-on-colors": "Güç 'Açık' renkleri", + "power-off-colors": "Güç 'Kapalı' renkleri", + "disabled-colors": "Devre dışı renkler", + "button": "Buton" + }, + "toggle-button": { + "behavior": "Davranış", + "checked": "İşaretli", + "unchecked": "İşaretsiz", + "check": "İşaretle", + "check-hint": "Bileşeni işaretlemek için gerçekleştirilen eylem.", + "uncheck": "İşareti kaldır", + "uncheck-hint": "Bileşenin işaretini kaldırmak için gerçekleştirilen eylem.", + "auto-scale": "Otomatik ölçeklendirme", + "horizontal-fill": "Yatay doldurma", + "vertical-fill": "Dikey doldurma", + "button-appearance": "Buton görünümü" + }, + "segmented-button": { + "layout": "Yerleşim", + "layout-squared": "Kare", + "layout-rounded": "Yuvarlatılmış", + "card-border": "Kart kenarlığı", + "button-appearance": "Buton görünümü", + "first": "Birinci", + "second": "İkinci", + "color-styles": "Renk stilleri", + "selected": "Seçili", + "unselected": "Seçili değil" + }, + "button": { + "layout": "Yerleşim", + "outlined": "Ana hatlı", + "filled": "Dolu", + "underlined": "Altı çizili", + "basic": "Temel", + "auto-scale": "Otomatik ölçeklendirme", + "label": "Etiket", + "icon": "Simge", + "border-radius": "Kenar yarıçapı", + "color-palette": "Renk paleti", + "main": "Ana", + "background": "Arka plan", + "border": "Kenarlık", + "custom-styles": "Özel stiller", + "clear-style": "Stili temizle", + "shadow": "Gölge", + "enabled": "Etkin", + "disabled": "Devre dışı", + "preview": "Önizleme", + "copy-style-from": "Stili kopyala" + }, + "value-stepper": { + "behavior": "Davranış", + "simplified": "Basitleştirilmiş", + "filled": "Dolu", + "outlined": "Ana hatlı", + "volume": "Ses", + "initial-state": "Başlangıç durumu", + "initial-state-hint": "Başlangıç değerini almak için gerçekleştirilen eylem.", + "disabled-state": "Devre dışı durumu", + "disabled-state-hint": "Bileşenin devre dışı bırakılacağı koşulu yapılandırın.", + "right-button-click": "Sağ düğme tıklaması", + "right-button-click-hint": "Sağ düğmeye basıldığında gerçekleştirilen eylem.", + "left-button-click": "Sol düğme tıklaması", + "left-button-click-hint": "Sol düğmeye basıldığında gerçekleştirilen eylem.", + "auto-scale": "Otomatik ölçeklendirme", + "value-range": "Aralık", + "min-range": "Minimum", + "max-range": "Maksimum", + "value-increment-decrement-step": "Değer artırma/azaltma adımı", + "value": "Değer", + "value-box-background": "Değer kutusu arka planı", + "border": "Kenarlık", + "button-appearance": "Buton görünümü", + "left": "Sol", + "right": "Sağ", + "left-button": "Sol düğme", + "right-button": "Sağ düğme", + "icon": "Simge", + "color-palette": "Renk paleti", + "main": "Ana", + "background": "Arka plan", + "button-icon-on": "Buton simgesi 'Açık'", + "button-on-colors": "Güç 'Açık' renkleri", + "disabled-colors": "Devre dışı renkler" + }, + "button-state": { + "activated-state": "Etkin durum", + "activated-state-hint": "Butonun etkin olduğu koşulu yapılandırın.", + "disabled-state": "Devre dışı durumu", + "disabled-state-hint": "Butonun devre dışı bırakıldığı koşulu yapılandırın.", + "selected-state": "Butonu seç", + "selected-state-hint": "Butonun seçili olduğu koşulu yapılandırın.", + "enabled": "Etkin", + "hovered": "Üzerine gelindi", + "pressed": "Basıldı", + "activated": "Etkinleştirildi", + "disabled": "Devre dışı", + "initial": "İlk buton", + "first": "Birinci", + "second": "İkinci" + }, + "background": { + "background": "Arka plan", + "background-settings": "Arka plan ayarları", + "background-type-image": "Görsel", + "background-type-color": "Renk", + "image-url": "Görsel URL'si", + "overlay": "Kaplama", + "enable-overlay": "Kaplamayı etkinleştir", + "blur": "Bulanıklık", + "preview": "Önizleme" + }, + "bar-chart": { + "bar-appearance": "Çubuk görünümü", + "label-on-bar": "Çubuğun üzerinde etiket", + "value-on-bar": "Çubuğun üzerinde değer", + "bar-chart-style": "Çubuk grafik stili", + "bar-axis": "Çubuk ekseni" + }, + "polar-area-chart": { + "polar-axis": "Polar ekseni", + "start-angle": "Başlangıç açısı", + "polar-area-chart-style": "Polar alan grafik stili" + }, + "battery-level": { + "layout": "Yerleşim", + "layout-vertical-solid": "Dikey. Dolu", + "layout-horizontal-solid": "Yatay. Dolu", + "layout-vertical-divided": "Dikey. Bölünmüş", + "layout-horizontal-divided": "Yatay. Bölünmüş", + "icon": "Simge", + "value": "Değer", + "auto-scale": "Otomatik ölçeklendirme", + "battery-level-color": "Pil seviyesi rengi", + "battery-shape-color": "Pil şekli rengi", + "battery-level-card-style": "Pil seviyesi kart stili", + "sections-count": "Bölüm sayısı" + }, + "signal-strength": { + "value": "Değer", + "last-update": "Son güncelleme", + "no-signal": "Sinyal yok", + "layout": "Yerleşim", + "layout-wifi": "Wi-Fi", + "layout-cellular-bar": "Mobil çubuk", + "icon": "Simge", + "date": "Tarih", + "active-bars-color": "Aktif sinyal çubuk rengi", + "inactive-bars-color": "Pasif sinyal çubuk rengi", + "signal-strength-card-style": "Sinyal gücü kart stili", + "no-signal-rssi-value": "\"Sinyal yok\" rssi değeri" + }, + "status-widget": { + "behavior": "Davranış", + "layout": "Yerleşim", + "layout-default": "Varsayılan", + "layout-center": "Merkez", + "layout-icon": "Simge", + "on": "Açık", + "off": "Kapalı", + "label": "Etiket", + "status": "Durum", + "icon": "Simge", + "color-palette": "Renk paleti", + "disabled-color-palette": "Devre dışı renk paleti", + "primary": "Birincil", + "primary-color-hint": "Simge ve etiket rengi", + "secondary": "İkincil", + "secondary-color-hint": "Durum rengi", + "background": "Arka plan" + }, + "chart": { + "common-settings": "Genel ayarlar", + "enable-stacking-mode": "Yığılma modunu etkinleştir", + "selection": "Zaman aralığı seçimi", + "enable-selection-mode": "Seçim modunu etkinleştir", + "line-shadow-size": "Çizgi gölge boyutu", + "display-smooth-lines": "Yumuşak (eğri) çizgileri göster", + "default-bar-width": "Toplanmamış veriler için varsayılan çubuk genişliği (milisaniye)", + "bar-alignment": "Çubuk hizalaması", + "bar-alignment-left": "Sol", + "bar-alignment-right": "Sağ", + "bar-alignment-center": "Orta", + "default-font": "Varsayılan yazı tipi", + "default-font-size": "Varsayılan yazı tipi boyutu", + "default-font-color": "Varsayılan yazı tipi rengi", + "thresholds-line-width": "Tüm eşik değerleri için varsayılan çizgi kalınlığı", + "tooltip-settings": "İpucu ayarları", + "tooltip": "İpucu", + "show-tooltip": "İpucu göster", + "hover-individual-points": "Tek tek noktalarda üzerine gelindiğinde göster", + "show-cumulative-values": "Yığılma modunda kümülatif değerleri göster", + "hide-zero-false-values": "İpucundan sıfır/yanlış değerleri gizle", + "tooltip-value-format-function": "İpucu değer biçimlendirme fonksiyonu", + "grid-settings": "Izgara ayarları", + "show-vertical-lines": "Dikey çizgileri göster", + "show-horizontal-lines": "Yatay çizgileri göster", + "grid-outline-border-width": "Izgara kenar/çerçeve kalınlığı (px)", + "primary-color": "Birincil renk", + "background-color": "Arka plan rengi", + "ticks-color": "İşaret renkleri", + "xaxis-settings": "X ekseni ayarları", + "axis-title": "Eksen başlığı", + "xaxis-tick-labels-settings": "X ekseni işaret etiketleri ayarları", + "show-tick-labels": "Eksen işaret etiketlerini göster", + "yaxis-settings": "Y ekseni ayarları", + "min-scale-value": "Ölçekteki minimum değer", + "max-scale-value": "Ölçekteki maksimum değer", + "yaxis-tick-labels-settings": "Y ekseni işaret etiketleri ayarları", + "tick-step-size": "İşaretler arası adım boyutu", + "number-of-decimals": "Gösterilecek ondalık basamak sayısı", + "ticks-formatter-function": "İşaret biçimlendirme fonksiyonu", + "comparison-settings": "Karşılaştırma ayarları", + "enable-comparison": "Karşılaştırmayı etkinleştir", + "time-for-comparison": "Karşılaştırma dönemi", + "time-for-comparison-previous-interval": "Önceki aralık (varsayılan)", + "time-for-comparison-days": "Bir gün önce", + "time-for-comparison-weeks": "Bir hafta önce", + "time-for-comparison-months": "Bir ay önce", + "time-for-comparison-years": "Bir yıl önce", + "time-for-comparison-custom-interval": "Özel aralık", + "custom-interval-value": "Özel aralık değeri (ms)", + "comparison-x-axis-settings": "Karşılaştırma X ekseni ayarları", + "axis-position": "Eksen konumu", + "axis-position-top": "Üst (varsayılan)", + "axis-position-bottom": "Alt", + "custom-legend-settings": "Özel açıklama ayarları", + "enable-custom-legend": "Özel açıklamayı etkinleştir (bu, anahtar etiketlerinde öznitelik/zaman serisi değerlerini kullanmanıza olanak tanır)", + "key-name": "Anahtar adı", + "key-name-required": "Anahtar adı gerekli", + "key-type": "Anahtar türü", + "key-type-attribute": "Öznitelik", + "key-type-timeseries": "Zaman serisi", + "label-keys-list": "Etiketlerde kullanılacak anahtar listesi", + "no-label-keys": "Yapılandırılmış anahtar yok", + "add-label-key": "Yeni anahtar ekle", + "line-width": "Çizgi kalınlığı", + "color": "Renk", + "data-is-hidden-by-default": "Veri varsayılan olarak gizlidir", + "disable-data-hiding": "Veri gizlemeyi devre dışı bırak", + "remove-from-legend": "Anahtarı açıklamadan kaldır", + "exclude-from-stacking": "Yığılmadan hariç tut (\"Yığılma\" modunda kullanılabilir)", + "line-settings": "Çizgi ayarları", + "show-line": "Çizgiyi göster", + "fill-line": "Çizgiyi doldur", + "fill-line-opacity": "Dolgu opaklığı", + "points-settings": "Nokta ayarları", + "show-points": "Noktaları göster", + "points-line-width": "Noktaların çizgi kalınlığı", + "points-radius": "Noktaların yarıçapı", + "point-shape": "Nokta şekli", + "point-shape-circle": "Daire", + "point-shape-cross": "Çarpı", + "point-shape-diamond": "Elmas", + "point-shape-square": "Kare", + "point-shape-triangle": "Üçgen", + "point-shape-custom": "Özel fonksiyon", + "point-shape-draw-function": "Nokta şekli çizim fonksiyonu", + "show-separate-axis": "Ayrı ekseni göster", + "axis-position-left": "Sol", + "axis-position-right": "Sağ", + "thresholds": "Eşikler", + "no-thresholds": "Yapılandırılmış eşik yok", + "add-threshold": "Eşik ekle", + "show-values-for-comparison": "Karşılaştırma için geçmiş değerleri göster", + "comparison-values-label": "Geçmiş değerler etiketi", + "comparison-line-color": "Karşılaştırma çizgi rengi", + "threshold-settings": "Eşik ayarları", + "use-as-threshold": "Anahtar değerini eşik olarak kullan", + "threshold-line-width": "Eşik çizgi kalınlığı", + "threshold-color": "Eşik rengi", + "common-pie-settings": "Genel pasta grafik ayarları", + "radius": "Yarıçap", + "inner-radius": "İç yarıçap", + "tilt": "Eğim", + "common-pie-settings-range-error": "Değer 0 ile 1 arasında olmalıdır", + "stroke-settings": "Çizgi ayarları", + "width-pixels": "Genişlik (piksel)", + "show-labels": "Etiketleri göster", + "animation-settings": "Animasyon ayarları", + "animated-pie": "Pasta animasyonunu etkinleştir (deneysel)", + "border-settings": "Kenarlık ayarları", + "border-width": "Kenarlık kalınlığı", + "border-color": "Kenarlık rengi", + "legend-settings": "Açıklama ayarları", + "display-legend": "Açıklamayı göster", + "labels-font-color": "Etiket yazı tipi rengi", + "series": "Seriler", + "add-series": "Seri ekle", + "series-settings": "Seri ayarları", + "remove-series": "Seriyi kaldır", + "no-series": "Yapılandırılmış seri yok", + "no-series-error": "En az bir seri belirtilmelidir", + "chart-appearance": "Grafik görünümü", + "vertical-grid-lines": "Dikey kılavuz çizgileri", + "horizontal-grid-lines": "Yatay kılavuz çizgileri", + "chart-background": "Grafik arka planı", + "grid-lines-color": "Kılavuz çizgileri rengi", + "border": "Kenarlık", + "axis": "Eksen", + "vertical-axis": "Dikey eksen", + "ticks": "İşaretler", + "horizontal-axis": "Yatay eksen", + "shape-empty-circle": "Boş daire", + "shape-circle": "Daire", + "shape-rect": "Dikdörtgen", + "shape-round-rect": "Yuvarlatılmış dikdörtgen", + "shape-triangle": "Üçgen", + "shape-diamond": "Elmas", + "shape-pin": "İğne", + "shape-arrow": "Ok", + "shape-none": "Yok", + "line-type-solid": "Düz", + "line-type-dashed": "Kesik", + "line-type-dotted": "Noktalı", + "label-position-top": "Üst", + "label-position-bottom": "Alt", + "label-position-outside": "Dış", + "label-position-inside": "İç", + "fill": "Dolgu", + "fill-type-none": "Yok", + "fill-type-solid": "Düz", + "fill-type-opacity": "Opaklık", + "fill-type-gradient": "Gradyan", + "background": "Arka plan", + "opacity": "Opaklık", + "gradient-stops": "Gradyan durakları", + "gradient-start": "başlangıç", + "gradient-end": "bitiş", + "animation": { + "animation": "Animasyon", + "animation-threshold": "Animasyon eşiği", + "animation-duration": "Animasyon süresi", + "animation-easing": "Animasyon yumuşatma", + "animation-delay": "Animasyon gecikmesi", + "update-animation-duration": "Güncelleme animasyon süresi", + "update-animation-easing": "Güncelleme animasyon yumuşatma", + "update-animation-delay": "Güncelleme animasyon gecikmesi" + }, + "chart-axis": { + "scale": "Ölçek", + "scale-min": "min", + "scale-max": "maks", + "scale-auto": "Otomatik" + }, + "bar": { + "show-border": "Kenarlığı göster", + "border-width": "Kenarlık kalınlığı", + "border-radius": "Kenarlık yarıçapı", + "bar-width": "Çubuk genişliği", + "label": "Etiket", + "label-hint": "Etiketi çubuğun üzerinde göster.", + "series-label-hint": "Değer ile birlikte etiketi çubuğun üzerinde göster.", + "label-background": "Etiket arka planı" + } + }, + "color": { + "color-settings": "Renk ayarları", + "color-type-constant": "Sabit", + "color-type-gradient": "Gradyan", + "color-type-range": "Aralık", + "color-type-function": "Fonksiyon", + "color": "Renk", + "value-range": "Değer aralığı", + "from": "Başlangıç", + "to": "Bitiş", + "color-function": "Renk fonksiyonu", + "copy-color-settings-from": "Renk ayarlarını kopyala", + "copy-from": "Buradan kopyala", + "settings-type": "Ayar tipi", + "basic-mode": "Temel", + "advanced-mode": "Gelişmiş", + "entity-alias": "Varlık takma adı", + "entity-attribute": "Varlık özelliği", + "gradient-color": "Gradyan rengi", + "gradient-color-min": "Renk", + "gradient-start": "Gradyan başlangıç rengi", + "gradient-start-min": "Başlangıç", + "gradient-end": "Gradyan bitiş rengi", + "gradient-end-min": "Bitiş", + "start-value": "Başlangıç değeri", + "end-value": "Bitiş değeri", + "gradient-type": "Gradyan tipi" + }, + "dashboard-state": { + "dashboard-state-settings": "Kontrol paneli durumu ayarları", + "dashboard-state": "Kontrol paneli durumu kimliği", + "autofill-state-layout": "Durum yerleşim yüksekliğini varsayılan olarak otomatik doldur", + "default-margin": "Varsayılan bileşen kenar boşluğu", + "default-background-color": "Varsayılan arka plan rengi", + "sync-parent-state-params": "Durum parametrelerini üst kontrol paneliyle senkronize et" + }, "date-range-navigator": { + "date-range-picker-settings": "Tarih aralığı seçici ayarları", + "hide-date-range-picker": "Tarih aralığı seçiciyi gizle", + "picker-one-panel": "Tarih aralığı seçici tek panel", + "picker-auto-confirm": "Tarih aralığı seçici otomatik onay", + "picker-show-template": "Tarih aralığı seçici şablon göster", + "first-day-of-week": "Haftanın ilk günü", + "interval-settings": "Zaman aralığı ayarları", + "hide-interval": "Zaman aralığını gizle", + "initial-interval": "İlk zaman aralığı", + "interval-hour": "Saat", + "interval-day": "Gün", + "interval-week": "Hafta", + "interval-two-weeks": "2 hafta", + "interval-month": "Ay", + "interval-three-months": "3 ay", + "interval-six-months": "6 ay", + "step-settings": "Adım ayarları", + "hide-step-size": "Adım boyutunu gizle", + "initial-step-size": "İlk adım boyutu", + "hide-labels": "Etiketleri gizle", + "use-session-storage": "Oturum depolamasını kullan", "localizationMap": { "Sun": "Paz", "Mon": "Pzt", @@ -2944,13 +7554,13 @@ "Date Range Template": "Tarih Aralığı Şablonu", "Today": "Bugün", "Yesterday": "Dün", - "This Week": "Bu hafta", - "Last Week": "Geçen hafta", - "This Month": "Bu ay", - "Last Month": "Geçen ay", + "This Week": "Bu Hafta", + "Last Week": "Geçen Hafta", + "This Month": "Bu Ay", + "Last Month": "Geçen Ay", "Year": "Yıl", - "This Year": "Bu yıl", - "Last Year": "Geçen yıl", + "This Year": "Bu Yıl", + "Last Year": "Geçen Yıl", "Date picker": "Tarih seçici", "Hour": "Saat", "Day": "Gün", @@ -2962,76 +7572,1953 @@ "Custom interval": "Özel aralık", "Interval": "Aralık", "Step size": "Adım boyutu", - "Ok": "Ok" + "Ok": "Tamam" } }, + "doughnut": { + "doughnut-appearance": "Halka görünümü", + "layout": "Yerleşim", + "layout-default": "Varsayılan", + "layout-with-total": "Toplam ile", + "central-total-value": "Merkezi toplam değeri", + "doughnut-card-style": "Halka kart stili" + }, + "entities-hierarchy": { + "hierarchy-data-settings": "Hiyerarşi veri ayarları", + "relations-query-function": "Düğüm ilişkileri sorgu fonksiyonu", + "has-children-function": "Düğümün çocukları var fonksiyonu", + "node-state-settings": "Düğüm durumu ayarları", + "node-opened-function": "Varsayılan düğüm açık fonksiyonu", + "node-disabled-function": "Düğüm devre dışı fonksiyonu", + "display-settings": "Görüntüleme ayarları", + "node-icon-function": "Düğüm simgesi fonksiyonu", + "node-text-function": "Düğüm metni fonksiyonu", + "sort-settings": "Sıralama ayarları", + "nodes-sort-function": "Düğümler sıralama fonksiyonu" + }, + "edge": { + "display-default-title": "Varsayılan başlığı göster" + }, + "gateway": { + "general-settings": "Genel ayarlar", + "widget-title": "Widget başlığı", + "default-archive-file-name": "Varsayılan arşiv dosya adı", + "device-type-for-new-gateway": "Yeni ağ geçidi için cihaz tipi", + "messages-settings": "Mesaj ayarları", + "save-config-success-message": "Ağ geçidi yapılandırmasının başarıyla kaydedildiğine dair mesaj", + "device-name-exists-message": "Girilen ada sahip cihaz zaten mevcut mesajı", + "gateway-title": "Ağ geçidi formu", + "read-only": "Salt okunur", + "events-title": "Ağ geçidi olayları form başlığı", + "events-filter": "Olay filtresi", + "event-key-contains": "Olay anahtarı şunu içeriyor...", + "show-connector": "Bağlayıcı için göster", + "connector-state-param-key": "Bağlayıcı durum parametresi anahtarı", + "message": "Mesaj", + "level": "Seviye", + "created-time": "Oluşturulma zamanı" + }, + "gauge": { + "default-color": "Varsayılan renk", + "radial-gauge-settings": "Radyal gösterge ayarları", + "ticks-settings": "İşaret ayarları", + "min-value": "Minimum değer", + "max-value": "Maksimum değer", + "min-value-short": "min", + "max-value-short": "maks", + "start-ticks-angle": "İşaret başlangıç açısı", + "ticks-angle": "İşaret açısı", + "major-ticks": "Ana işaretler", + "major-ticks-count": "Ana işaret sayısı", + "major-ticks-color": "Ana işaret rengi", + "minor-ticks": "Alt işaretler", + "minor-ticks-count": "Alt işaret sayısı", + "minor-ticks-color": "Alt işaret rengi", + "tick-numbers-font": "İşaret numaraları yazı tipi", + "unit-title-settings": "Birim başlığı ayarları", + "show-unit-title": "Birim başlığı", + "unit-title": "Birim başlığı", + "title-font": "Başlık yazı tipi", + "units-settings": "Birim ayarları", + "units-font": "Birim yazı tipi", + "value-box-settings": "Değer kutusu ayarları", + "show-value-box": "Değer kutusunu göster", + "value-box": "Değer kutusu", + "value-int": "Tam sayı kısmı için basamak sayısı", + "value-text": "Değer metni", + "value-text-shadow": "Değer metni gölgesi", + "value-font": "Değer metni yazı tipi", + "rect-stroke-color-start": "Dikdörtgen çizgi rengi - başlangıç gradyanı", + "rect-stroke-color-end": "Dikdörtgen çizgi rengi - bitiş gradyanı", + "background-color": "Arka plan rengi", + "shadow-color": "Gölge rengi", + "value-box-rect-stroke-color": "Değer kutusu dikdörtgen çizgi rengi", + "value-box-rect-stroke-color-end": "Değer kutusu dikdörtgen çizgi rengi - bitiş gradyanı", + "value-box-background-color": "Değer kutusu arka plan rengi", + "value-box-shadow-color": "Değer kutusu gölge rengi", + "plate-settings": "Taban ayarları", + "show-plate-border": "Taban kenarlığı", + "plate-color": "Taban rengi", + "needle-settings": "İbre ayarları", + "needle-circle-size": "İbre dairesi boyutu", + "needle-color": "İbre rengi", + "needle-color-start": "İbre rengi - başlangıç gradyanı", + "needle-color-end": "İbre rengi - bitiş gradyanı", + "needle-color-shadow-up": "İbrenin üst yarısı gölge rengi", + "needle-color-shadow-down": "Gölge efekti", + "highlights-settings": "Vurgu ayarları", + "highlights-width": "Vurgu kalınlığı", + "highlights": "Vurgular", + "highlight-from": "Başlangıç", + "highlight-to": "Bitiş", + "highlight-color": "Renk", + "no-highlights": "Herhangi bir vurgu yapılandırılmamış", + "add-highlight": "Vurgu ekle", + "animation-settings": "Animasyon ayarları", + "enable-animation": "Animasyon", + "animation-duration-rule": "Animasyon süresi ve kuralı", + "animation-duration": "Animasyon süresi", + "animation-rule": "Animasyon kuralı", + "animation-linear": "Doğrusal", + "animation-quad": "İkinci derece", + "animation-quint": "Beşinci derece", + "animation-cycle": "Döngü", + "animation-bounce": "Zıplama", + "animation-elastic": "Esnek", + "animation-dequad": "Ters ikinci derece", + "animation-dequint": "Ters beşinci derece", + "animation-decycle": "Ters döngü", + "animation-debounce": "Ters zıplama", + "animation-delastic": "Ters esnek", + "linear-gauge-settings": "Doğrusal gösterge ayarları", + "bar-stroke": "Çubuk çizgisi", + "bar-stroke-width": "Çubuk çizgi kalınlığı", + "bar-stroke-color": "Çubuk çizgi rengi", + "bar-background-color": "Çubuk arka plan rengi - başlangıç gradyanı", + "bar-background-color-end": "Çubuk arka plan rengi - bitiş gradyanı", + "progress-bar-color": "İlerleme çubuğu rengi", + "progress-bar": "İlerleme çubuğu", + "progress-bar-color-start": "İlerleme çubuğu rengi - başlangıç gradyanı", + "progress-bar-color-end": "İlerleme çubuğu rengi - bitiş gradyanı", + "major-ticks-names": "Ana işaret adları", + "show-stroke-ticks": "İşaret çizgilerini göster", + "major-ticks-font": "Ana işaret yazı tipi", + "border-color": "Kenarlık rengi", + "border-width": "Kenarlık kalınlığı", + "needle-circle": "İbre dairesi", + "needle-circle-color": "İbre dairesi rengi", + "animation-target": "Animasyon hedefi", + "animation-target-needle": "İbre", + "animation-target-plate": "Taban", + "common-settings": "Genel gösterge ayarları", + "gauge-type": "Gösterge türü", + "gauge-type-arc": "Yay", + "gauge-type-donut": "Halka", + "gauge-type-horizontal-bar": "Yatay çubuk", + "gauge-type-vertical-bar": "Dikey çubuk", + "donut-start-angle": "Başlangıç açısı (derece)", + "bar-settings": "Gösterge çubuğu ayarları", + "relative-bar-width": "Göreli çubuk genişliği", + "neon-glow-brightness": "Neon parıltı efekti parlaklığı (0-100)", + "neon-glow-brightness-hint": "0 - efekt devre dışı", + "stripes-thickness": "Şerit kalınlığı", + "stripes-thickness-hint": "0 - şerit yok", + "rounded-line-cap": "Yuvarlak çizgi ucu", + "bar-color-settings": "Çubuk renk ayarları", + "use-precise-level-color-values": "Hassas renk seviyelerini kullan", + "bar-colors": "Alt seviyeden üst seviyeye çubuk renkleri", + "color": "Renk", + "no-bar-colors": "Tanımlı çubuk rengi yok", + "add-bar-color": "Çubuk rengi ekle", + "from": "Başlangıç", + "to": "Bitiş", + "fixed-level-colors": "Sınır değerleri kullanarak çubuk renkleri", + "gauge-title-settings": "Gösterge başlık ayarları", + "show-gauge-title": "Gösterge başlığını göster", + "gauge-title": "Gösterge başlığı", + "gauge-title-font": "Gösterge başlığı yazı tipi", + "unit-title-and-timestamp-settings": "Birim başlığı ve zaman damgası ayarları", + "show-timestamp": "Zaman damgası", + "timestamp-format": "Zaman damgası biçimi", + "label-font": "Değerin altında gösterilen etiketin yazı tipi", + "value-settings": "Değer ayarları", + "show-value": "Değer metnini göster", + "min-max-settings": "Minimum/maksimum etiket ayarları", + "show-min-max": "Min ve max değerleri göster", + "min-max-font": "Min ve max etiket yazı tipi", + "show-ticks": "İşaretleri göster", + "tick-width": "İşaret kalınlığı", + "tick-color": "İşaret rengi", + "tick-values": "İşaret değerleri", + "no-tick-values": "Tanımlı işaret değeri yok", + "add-tick-value": "İşaret değeri ekle", + "gauge-appearance": "Gösterge görünümü", + "units-title": "Birim başlığı", + "value": "Değer", + "ticks": "İşaretler", + "arrow-and-scale-color": "Ok ve ölçek varsayılan rengi", + "scale-settings": "Ölçek ayarları", + "scale": "Ölçek", + "scale-color": "Ölçek renkleri", + "compass-appearance": "Pusula görünümü", + "label": "Etiket", + "labels": "Etiketler", + "label-style": "Etiket stili", + "simple-gauge-type": "Tür", + "gauge-bar-background": "Gösterge çubuğu arka planı", + "bar-color": "Çubuk rengi", + "min-and-max-value": "Minimum ve maksimum değer", + "min-and-max-label": "Minimum ve maksimum etiket", + "font": "Yazı tipi", + "tick-width-and-color": "İşaret kalınlığı ve rengi", + "min-max-validation-text": "Maksimum değer, minimum değerden büyük olmalıdır" + }, + "gpio": { + "pin": "Pin", + "label": "Etiket", + "row": "Satır", + "column": "Sütun", + "color": "Renk", + "panel-settings": "Panel ayarları", + "background-color": "Arka plan rengi", + "gpio-switches": "GPIO anahtarları", + "no-gpio-switches": "Yapılandırılmış GPIO anahtarı yok", + "add-gpio-switch": "GPIO anahtarı ekle", + "gpio-status-request": "GPIO durum isteği", + "method-name": "Yöntem adı", + "method-body": "Yöntem içeriği", + "gpio-status-change-request": "GPIO durum değişiklik isteği", + "parse-gpio-status-function": "GPIO durumu ayrıştırma fonksiyonu", + "gpio-leds": "GPIO LED'leri", + "no-gpio-leds": "Yapılandırılmış GPIO LED'i yok", + "add-gpio-led": "GPIO LED'i ekle" + }, + "html-card": { + "html": "HTML", + "css": "CSS" + }, "input-widgets": { - "attribute-not-allowed": "Öznitelik parametresi bu göstergede kullanılamaz", - "blocked-location": "Tarayıcınızda coğrafi konum engellendi", - "claim-device": "Talep cihaz", + "attribute-not-allowed": "Bu bileşende öznitelik parametresi kullanılamaz", + "blocked-location": "Tarayıcınızda konum erişimi engellenmiş", + "claim-device": "Cihazı talep et", "claim-failed": "Cihaz talep edilemedi!", "claim-not-found": "Cihaz bulunamadı!", "claim-successful": "Cihaz başarıyla talep edildi!", "date": "Tarih", "device-name": "Cihaz adı", - "device-name-required": "Cihaz adı gerekli", - "discard-changes": "Değişiklikleri gözardı et", - "entity-attribute-required": "Öğe özniteliği gerekli", - "entity-coordinate-required": "Enlem ve boylam olmak üzere her iki alan da gereklidir", - "entity-timeseries-required": "Öğe zaman serisi gerekli", + "device-name-required": "Cihaz adı gereklidir", + "discard-changes": "Değişiklikleri iptal et", + "entity-attribute-required": "Varlık özniteliği gereklidir", + "entity-coordinate-required": "Enlem ve boylam alanlarının her ikisi de gereklidir", + "entity-timeseries-required": "Varlık zaman serisi gereklidir", "get-location": "Geçerli konumu al", "invalid-date": "Geçersiz tarih", "latitude": "Enlem", "longitude": "Boylam", "min-value-error": "Minimum değer {{value}}", "max-value-error": "Maksimum değer {{value}}", - "not-allowed-entity": "Seçili öğe, paylaşılan niteliklere sahip olamaz", - "no-attribute-selected": "Hiçbir özellik seçilmedi", - "no-datakey-selected": "Veri anahtarı seçilmedi", + "not-allowed-entity": "Seçilen varlık paylaşılan özniteliklere sahip olamaz", + "no-attribute-selected": "Herhangi bir öznitelik seçilmedi", + "no-datakey-selected": "Herhangi bir veri anahtarı seçilmedi", "no-coordinate-specified": "Enlem/boylam için veri anahtarı belirtilmedi", - "no-entity-selected": "Hiçbir öğe seçilmedi", - "no-image": "Resim yok", - "no-support-geolocation": "Tarayıcınız coğrafi konumu desteklemiyor", - "no-support-web-camera": "Tarayıcınız kameraları desteklemiyor", - "enable-https-use-widget": "Bu göstergeyi kullanmak için lütfen HTTPS'yi etkinleştirin", + "no-entity-selected": "Varlık seçilmedi", + "no-image": "Görüntü yok", + "no-support-geolocation": "Tarayıcınız konum belirlemeyi desteklemiyor", + "no-support-web-camera": "Tarayıcınız kamera desteği sunmuyor", + "enable-https-use-widget": "Bu bileşeni kullanmak için lütfen HTTPS etkinleştirin", "no-found-your-camera": "Kameranız bulunamadı", - "no-permission-camera": "İzin kullanıcı tarafından reddedildi / Bu sitenin kamerayı kullanma izni yok", - "no-timeseries-selected": "Zaman serisi seçilmedi", + "no-permission-camera": "Kullanıcı izni reddetti / Bu sitenin kamerayı kullanma izni yok", + "no-timeseries-selected": "Herhangi bir zaman serisi seçilmedi", "secret-key": "Gizli anahtar", - "secret-key-required": "Gizli anahtar gerekli", - "switch-attribute-value": "Öğe öznitelik değerine geç", - "switch-camera": "Kameraya geç", - "switch-timeseries-value": "Öğe zaman serisi değerine geç", + "secret-key-required": "Gizli anahtar gereklidir", + "switch-attribute-value": "Varlık öznitelik değerini değiştir", + "switch-camera": "Kamerayı değiştir", + "switch-timeseries-value": "Varlık zaman serisi değerini değiştir", "take-photo": "Fotoğraf çek", "time": "Zaman", - "timeseries-not-allowed": "Timeseries parametresi bu göstergede kullanılamaz", - "update-failed": "Güncelleştirme başarısız", - "update-successful": "Güncelleştirme başarılı", + "timeseries-not-allowed": "Bu bileşende zaman serisi parametresi kullanılamaz", + "update-failed": "Güncelleme başarısız", + "update-successful": "Güncelleme başarılı", "update-attribute": "Özniteliği güncelle", "update-timeseries": "Zaman serisini güncelle", - "value": "Değer" + "value": "Değer", + "general-settings": "Genel ayarlar", + "widget-title": "Bileşen başlığı", + "claim-button-label": "Talep etme buton etiketi", + "show-secret-key-field": "'Gizli anahtar' giriş alanını göster", + "labels-settings": "Etiket ayarları", + "show-labels": "Etiketleri göster", + "device-name-label": "Cihaz adı giriş alanı etiketi", + "secret-key-label": "Gizli anahtar giriş alanı etiketi", + "messages-settings": "Mesaj ayarları", + "claim-device-success-message": "Cihazın başarıyla talep edildiği durumdaki metin mesajı", + "claim-device-not-found-message": "Cihaz bulunamadığında gösterilen metin mesajı", + "claim-device-failed-message": "Cihaz talep edilemediğinde gösterilen metin mesajı", + "claim-device-name-required-message": "'Cihaz adı gerekli' hata mesajı", + "claim-device-secret-key-required-message": "'Gizli anahtar gerekli' hata mesajı", + "show-label": "Etiketi göster", + "label": "Etiket", + "required": "Gerekli", + "required-error-message": "'Gerekli' hata mesajı", + "show-result-message": "Sonuç mesajını göster", + "integer-field-settings": "Tamsayı alanı ayarları", + "min-value": "Minimum değer", + "max-value": "Maksimum değer", + "double-field-settings": "Ondalıklı sayı alanı ayarları", + "text-field-settings": "Metin alanı ayarları", + "min-length": "Minimum uzunluk", + "max-length": "Maksimum uzunluk", + "checkbox-settings": "Onay kutusu ayarları", + "true-label": "İşaretli etiket", + "false-label": "İşaretsiz etiket", + "image-input-settings": "Görsel girişi ayarları", + "display-preview": "Önizlemeyi göster", + "display-clear-button": "Temizle butonunu göster", + "display-apply-button": "Uygula butonunu göster", + "display-discard-button": "İptal et butonunu göster", + "datetime-field-settings": "Tarih/saat alanı ayarları", + "display-time-input": "Zaman girişini göster", + "latitude-key-name": "Enlem anahtar adı", + "longitude-key-name": "Boylam anahtar adı", + "show-get-location-button": "'Geçerli konumu al' butonunu göster", + "use-high-accuracy": "Yüksek hassasiyet kullan", + "location-fields-settings": "Konum alanı ayarları", + "latitude-label": "Enlem etiketi", + "longitude-label": "Boylam etiketi", + "input-fields-alignment": "Giriş alanlarının hizalaması", + "input-fields-alignment-column": "Sütun (varsayılan)", + "input-fields-alignment-row": "Satır", + "layout": "Yerleşim", + "row-gap": "Satırlar arası boşluk (piksel)", + "column-gap": "Sütunlar arası boşluk (piksel)", + "latitude-field-required": "Enlem alanı gerekli", + "longitude-field-required": "Boylam alanı gerekli", + "attribute-settings": "Öznitelik ayarları", + "widget-mode": "Bileşen modu", + "widget-mode-update-attribute": "Öznitelik güncelle", + "widget-mode-update-timeseries": "Zaman serisini güncelle", + "attribute-scope": "Öznitelik kapsamı", + "attribute-scope-server": "Sunucu özniteliği", + "attribute-scope-shared": "Paylaşılan öznitelik", + "value-required": "Değer gerekli", + "image-settings": "Görsel ayarları", + "image-format": "Görsel formatı", + "image-format-jpeg": "JPEG", + "image-format-png": "PNG", + "image-format-webp": "WEBP", + "image-quality": "JPEG ve WEBP gibi kayıplı sıkıştırma kullanan görsellerin kalitesi", + "max-image-width": "Maksimum görsel genişliği", + "max-image-height": "Maksimum görsel yüksekliği", + "action-buttons": "İşlem butonları", + "show-action-buttons": "İşlem butonlarını göster", + "update-all-values": "Sadece değiştirilenler değil, tüm değerleri güncelle", + "save-button-label": "'KAYDET' buton etiketi", + "reset-button-label": "'GERİ AL' buton etiketi", + "group-settings": "Grup ayarları", + "show-group-title": "Farklı varlıklarla ilişkili alan grubu için başlık göster", + "group-title": "Grup başlığı", + "fields-alignment": "Alan hizalaması", + "fields-alignment-row": "Satır (varsayılan)", + "fields-alignment-column": "Sütun", + "fields-in-row": "Satırdaki alan sayısı", + "option-value": "Değer (boş seçenek oluşturmak için 'null' yazın)", + "option-label": "Etiket", + "hide-input-field": "Giriş alanını gizle", + "datakey-type": "Veri anahtarı türü", + "datakey-type-server": "Sunucu özniteliği (varsayılan)", + "datakey-type-shared": "Paylaşılan öznitelik", + "datakey-type-timeseries": "Zaman serisi", + "datakey-value-type": "Veri anahtarı değer türü", + "datakey-value-type-string": "Metin", + "datakey-value-type-double": "Ondalıklı", + "datakey-value-type-integer": "Tamsayı", + "datakey-value-type-json": "JSON", + "datakey-value-type-boolean-checkbox": "Boolean (Onay kutusu)", + "datakey-value-type-boolean-switch": "Boolean (Anahtar)", + "datakey-value-type-date-time": "Tarih ve Saat", + "datakey-value-type-date": "Tarih", + "datakey-value-type-time": "Saat", + "datakey-value-type-select": "Seçim", + "datakey-value-type-radio": "Radyo", + "datakey-value-type-color": "Renk", + "value-is-required": "Değer gerekli", + "ability-to-edit-attribute": "Özniteliği düzenleme yetkisi", + "ability-to-edit-attribute-editable": "Düzenlenebilir (varsayılan)", + "ability-to-edit-attribute-disabled": "Devre dışı", + "ability-to-edit-attribute-readonly": "Salt okunur", + "disable-on-datakey-name": "Başka bir veri anahtarının false değeriyle devre dışı bırak (veri anahtarı adını belirtin)", + "field-appearance": "Alan görünümü", + "appearance-fill": "Dolu", + "appearance-outline": "Kenarlıklı", + "subscript-sizing": "Alt simge boyutlandırma", + "subscript-sizing-fixed": "Sabit", + "subscript-sizing-dynamic": "Dinamik", + "slide-toggle-settings": "Anahtar (slide toggle) ayarları", + "slide-toggle-label-position": "Anahtar etiket pozisyonu", + "slide-toggle-label-position-after": "Sonra", + "slide-toggle-label-position-before": "Önce", + "select-options": "Seçenekleri seç", + "no-select-options": "Yapılandırılmış seçim seçeneği yok", + "add-select-option": "Seçenek ekle", + "numeric-field-settings": "Sayısal alan ayarları", + "step-interval": "Değerler arasındaki adım aralığı", + "error-messages": "Hata mesajları", + "min-value-error-message": "'Min değer' hata mesajı", + "max-value-error-message": "'Maksimum değer' hata mesajı", + "invalid-date-error-message": "'Geçersiz tarih' hata mesajı", + "invalid-JSON-error-message": "'Geçersiz JSON' hata mesajı", + "icon-settings": "Simge ayarları", + "dialog-editor-settings": "Diyalog düzenleyici ayarları", + "use-custom-icon": "Özel simge kullan", + "input-cell-icon": "Giriş hücresinin önünde gösterilecek simge", + "value-conversion-settings": "Değer dönüştürme ayarları", + "get-value-settings": "Değer alma ayarları", + "use-get-value-function": "getValue fonksiyonunu kullan", + "get-value-function": "getValue fonksiyonu", + "set-value-settings": "Değer ayarlama ayarları", + "use-set-value-function": "setValue fonksiyonunu kullan", + "set-value-function": "setValue fonksiyonu", + "json-invalid": "JSON değeri geçersiz biçimde", + "title": "Başlık", + "cancel-button-label": "'İptal' buton etiketi", + "radio-button-settings": "Radyo buton ayarları", + "color": "Renk", + "columns": "Sütunlar", + "radio-options": "Radyo seçenekleri", + "no-radio-options": "Yapılandırılmış radyo seçeneği yok", + "add-radio-option": "Radyo seçeneği ekle", + "radio-label-position": "Etiket konumu", + "radio-label-position-before": "Önce", + "radio-label-position-after": "Sonra" + }, + "invalid-qr-code-text": "QR kod için geçersiz giriş metni. Giriş metni dize türünde olmalıdır", + "qr-code": { + "use-qr-code-text-function": "QR kod metin fonksiyonunu kullan", + "qr-code-text-pattern": "QR kod metin deseni (örn. '${entityName} | ${keyName} - bazı metin.')", + "qr-code-text-pattern-hint": "QR kod metin deseni, varlık takma adındaki ilk bulunan anahtarın değerini kullanır.", + "qr-code-text-pattern-required": "QR kod metin deseni gereklidir.", + "qr-code-text-function": "QR kod metin fonksiyonu" + }, + "label-widget": { + "label-pattern": "Desen", + "label-pattern-hint": "İpucu: örn. 'Metin ${keyName} birim.' veya ${#<key index>} birim'", + "label-pattern-required": "Desen gereklidir", + "label-position": "Pozisyon (Arka plana göre yüzde olarak)", + "x-pos": "X", + "y-pos": "Y", + "background-color": "Arka plan rengi", + "font-settings": "Yazı tipi ayarları", + "background-image": "Arka plan resmi", + "labels": "Etiketler", + "no-labels": "Yapılandırılmış etiket yok", + "add-label": "Etiket ekle" }, - "invalid-qr-code-text": "QR kodu için geçersiz giriş metni. Girdi bir string türüne sahip olmalıdır" + "navigation": { + "title": "Başlık", + "navigation-path": "Gezinme yolu", + "filter-type": "Filtre türü", + "filter-type-all": "Tüm öğeler", + "filter-type-include": "Öğeleri dahil et", + "filter-type-exclude": "Öğeleri hariç tut", + "items": "Öğeler", + "enter-urls-to-filter": "Filtrelemek için URL'leri girin..." + }, + "persistent-table": { + "rpc-id": "RPC Kimliği", + "message-type": "Mesaj türü", + "method": "Yöntem", + "params": "Parametreler", + "created-time": "Oluşturulma zamanı", + "expiration-time": "Sona erme zamanı", + "retries": "Yeniden denemeler", + "status": "Durum", + "filter": "Filtre", + "refresh": "Yenile", + "add": "Kalıcı RPC isteği ekle", + "details": "Ayrıntılar", + "delete": "Sil", + "delete-request-title": "Kalıcı RPC isteğini sil", + "delete-request-text": "Bu isteği silmek istediğinizden emin misiniz?", + "details-title": "Ayrıntılar RPC Kimliği: ", + "additional-info": "Ek bilgi", + "response": "Yanıt", + "any-status": "Herhangi bir durum", + "rpc-status-list": "RPC durum listesi", + "no-request-prompt": "Görüntülenecek istek yok", + "send-request": "İstek gönder", + "add-title": "Kalıcı RPC isteği oluştur", + "method-error": "Yöntem gereklidir.", + "timeout-error": "En düşük zaman aşımı değeri 5000 (5 saniye).", + "white-space-error": "Boşluk karakterine izin verilmez.", + "rpc-status": { + "QUEUED": "KUYRUKTA", + "SENT": "GÖNDERİLDİ", + "DELIVERED": "TESLİM EDİLDİ", + "SUCCESSFUL": "BAŞARILI", + "TIMEOUT": "ZAMAN AŞIMI", + "EXPIRED": "SÜRESİ DOLDU", + "FAILED": "BAŞARISIZ" + }, + "rpc-search-status-all": "TÜMÜ", + "message-types": { + "false": "İki yönlü", + "true": "Tek yönlü" + }, + "general-settings": "Genel ayarlar", + "enable-filter": "Filtreyi etkinleştir", + "enable-sticky-header": "Kaydırma sırasında başlığı göster", + "enable-sticky-action": "Kaydırma sırasında eylem sütununu göster", + "display-request-details": "İstek ayrıntılarını göster", + "allow-send-request": "RPC isteği göndermeye izin ver", + "allow-delete-request": "İsteği silmeye izin ver", + "columns-settings": "Sütun ayarları", + "display-columns": "Gösterilecek sütunlar", + "column": "Sütun", + "no-columns-found": "Sütun bulunamadı", + "no-columns-matching": "'{{column}}' bulunamadı." + }, + "range-chart": { + "chart": "Grafik", + "data-zoom": "Veri yakınlaştırma", + "range-chart-appearance": "Aralık grafik görünümü", + "range-colors": "Aralık renkleri", + "out-of-range-color": "Aralık dışı renk", + "show-range-thresholds": "Aralık eşiklerini göster", + "range-thresholds-settings": "Aralık eşik ayarları", + "fill-area": "Alanı doldur", + "fill-area-opacity": "Alan doluluk opaklığı", + "range-chart-style": "Aralık grafik stili" + }, + "knob": { + "behavior": "Davranış", + "initial-value": "Başlangıç değeri", + "initial-value-hint": "Düğmenin başlangıç değerini almak için işlem.", + "on-value-change": "Değer değiştiğinde", + "on-value-change-hint": "Düğmenin değeri değiştirildiğinde tetiklenen işlem.", + "range": "Aralık", + "min": "min", + "max": "maks", + "value": "Değer", + "fallback-initial-value": "Yedek başlangıç değeri" + }, + "rpc": { + "value-settings": "Değer ayarları", + "initial-value": "Başlangıç değeri", + "retrieve-value-settings": "Açık/Kapalı değer alma ayarları", + "retrieve-value-method": "Değeri alma yöntemi", + "retrieve-value-method-none": "Alma", + "retrieve-value-method-rpc": "RPC ile değer alma yöntemini çağır", + "retrieve-value-method-attribute": "Öznitelik için abone ol", + "retrieve-value-method-timeseries": "Zaman serisi için abone ol", + "attribute-value-key": "Öznitelik anahtarı", + "timeseries-value-key": "Zaman serisi anahtarı", + "get-value-method": "RPC değer alma yöntemi", + "parse-value-function": "Değer ayrıştırma fonksiyonu", + "update-value-settings": "Değer güncelleme ayarları", + "set-value-method": "RPC değer atama yöntemi", + "convert-value-function": "Değer dönüştürme fonksiyonu", + "rpc-settings": "RPC ayarları", + "request-timeout": "RPC istek zaman aşımı (ms)", + "persistent-rpc-settings": "Kalıcı RPC ayarları", + "request-persistent": "RPC isteğini kalıcı yap", + "persistent-polling-interval": "Kalıcı RPC yanıtı için anketleme aralığı (ms)", + "common-settings": "Genel ayarlar", + "switch-title": "Anahtar başlığı", + "show-on-off-labels": "Açık/Kapalı etiketlerini göster", + "slide-toggle-label": "Kayan anahtar etiketi", + "label-position": "Etiket konumu", + "label-position-before": "Önce", + "label-position-after": "Sonra", + "slider-color": "Kaydırıcı rengi", + "slider-color-primary": "Birincil", + "slider-color-accent": "Vurgu", + "slider-color-warn": "Uyarı", + "button-style": "Düğme stili", + "button-raised": "Yükseltilmiş düğme", + "button-primary": "Birincil renk", + "button-background-color": "Düğme arka plan rengi", + "button-text-color": "Düğme metin rengi", + "widget-title": "Bileşen başlığı", + "button-label": "Düğme etiketi", + "device-attribute-scope": "Cihaz öznitelik kapsamı", + "server-attribute": "Sunucu özniteliği", + "shared-attribute": "Paylaşılan öznitelik", + "device-attribute-parameters": "Cihaz öznitelik parametreleri", + "is-one-way-command": "Tek yönlü komut", + "rpc-method": "RPC yöntemi", + "rpc-method-params": "RPC yöntem parametreleri", + "show-rpc-error": "RPC komutu yürütme hatasını göster", + "led-title": "LED başlığı", + "led-color": "LED rengi", + "check-status-settings": "Durum kontrol ayarları", + "perform-rpc-status-check": "RPC cihaz durumu kontrolünü gerçekleştir", + "retrieve-led-status-value-method": "LED durum değerini alma yöntemi", + "led-status-value-attribute": "LED durum değerini içeren cihaz özniteliği", + "led-status-value-timeseries": "LED durum değerini içeren cihaz zaman serisi", + "check-status-method": "RPC cihaz durumu kontrol yöntemi", + "parse-led-status-value-function": "LED durum değerini ayrıştırma fonksiyonu", + "knob-title": "Düğme başlığı" + }, + "maps": { + "map-type": { + "type": "Harita türü", + "map": "Harita", + "image": "Görsel" + }, + "image": { + "image-source": "Görsel kaynağı", + "image-source-image": "Görsel", + "image-source-entity-key": "Varlık anahtarı", + "source-entity-alias": "Kaynak varlık takma adı", + "image-url-key": "Görsel URL anahtarı", + "image-url-key-required": "Görsel URL anahtarı gereklidir" + }, + "control": { + "map-controls": "Harita kontrolleri", + "position": "Pozisyon", + "position-topleft": "Sol üst", + "position-topright": "Sağ üst", + "position-bottomleft": "Sol alt", + "position-bottomright": "Sağ alt", + "zoom-actions": "Yakınlaştırma eylemleri", + "zoom-scroll": "Kaydırma", + "zoom-double-click": "Çift tıklama", + "zoom-control-buttons": "Kontrol düğmeleri", + "scale": "Ölçek", + "scale-metric": "Metrik", + "scale-imperial": "İngiliz", + "switch-to-drag-mode-using-button": "Buton kullanarak sürükleme moduna geç" + }, + "timeline": { + "control-panel": "Zaman çizelgesi kontrol paneli", + "time-step": "Zaman adımı", + "speed-options": "Hız seçenekleri", + "timestamp": "Zaman damgası", + "snap-to-real-location": "Gerçek konuma hizala", + "location-snap-filter-function": "Konum hizalama filtre fonksiyonu", + "no-trips-data-available": "Seyahat verisi bulunamadı" + }, + "map-action": { + "map-action-buttons": "Harita eylem düğmeleri", + "label": "Etiket", + "icon": "Simge", + "color": "Renk", + "action": "Eylem", + "add-button": "Düğme ekle", + "no-action-buttons-configured": "Eylem düğmesi yapılandırılmadı", + "remove-action-button": "Eylem düğmesini kaldır", + "map-action-button": "Harita eylem düğmesi", + "button-requires": "Düğme bir etiket veya simge gerektirir" + }, + "common": { + "common-map-settings": "Genel harita ayarları", + "fit-map-bounds": "Tüm işaretçileri kapsayacak şekilde harita sınırlarını ayarla", + "default-map-center-position": "Varsayılan harita merkez konumu", + "default-map-zoom-level": "Varsayılan harita yakınlaştırma seviyesi", + "entities-limit": "Yüklenecek varlık limiti" + }, + "layer": { + "label": "Etiket", + "layer": "Katman", + "layers": "Katmanlar", + "map-layers": "Harita katmanları", + "add-layer": "Katman ekle", + "layer-settings": "Katman ayarları", + "remove-layer": "Katmanı kaldır", + "no-layers": "Yapılandırılmış katman yok", + "roadmap": "Yol haritası", + "satellite": "Uydu", + "hybrid": "Hibrit", + "reference": { + "reference-layer": "Referans katman", + "no-layer": "Katman yok", + "openstreetmap-hybrid": "OpenStreetMap Hibrit", + "world-edition-hybrid": "Dünya Sürümü Hibrit", + "enhanced-contrast-hybrid": "Artırılmış Kontrast Hibrit" + }, + "provider": { + "provider": "Sağlayıcı", + "openstreet": { + "title": "OpenStreet", + "mapnik": "Mapnik", + "hot": "HOT", + "esri-street": "WorldStreetMap", + "esri-topo": "WorldTopoMap", + "esri-imagery": "WorldImagery", + "cartodb-positron": "Positron", + "cartodb-dark-matter": "DarkMatter" + }, + "google": { + "title": "Google", + "roadmap": "Yol haritası", + "satellite": "Uydu", + "hybrid": "Hibrit", + "terrain": "Arazi" + }, + "here": { + "title": "HERE", + "normal-day": "Gündüz (normal)", + "normal-night": "Gece (normal)", + "hybrid-day": "Gündüz (hibrit)", + "terrain-day": "Gündüz (arazi)" + }, + "tencent": { + "title": "Tencent", + "normal": "Normal", + "satellite": "Uydu", + "terrain": "Arazi" + }, + "custom": { + "title": "Özel", + "tile-url": "Döşeme URL'si" + } + }, + "credentials": { + "credentials": "Kimlik bilgileri", + "api-key": "API Anahtarı" + } + }, + "overlays": { + "overlays": "Katmanlar", + "overlays-hint": "Harita varlıkları için veri kaynaklarını, görünümü, davranışı, düzenleme seçeneklerini ve gruplamayı yapılandırın", + "trips": "Rotalar", + "markers": "İşaretçiler", + "polygons": "Poligonlar", + "circles": "Daireler" + }, + "data-layer": { + "source": "Kaynak", + "filter": "Filtre", + "additional-data-keys": "Ek veri anahtarları", + "additional-datasources": "Ek veri kaynakları", + "additional-datasources-hint": "Haritada görüntülenmeyen varlıklardan özniteliklere veya telemetriye erişim için veri kaynağı, harita katmanı işlevlerinde kullanılabilir.", + "more-datasources": "Daha fazla veri kaynağı", + "data-keys": "Veri anahtarları", + "add-datasource": "Veri kaynağı ekle", + "no-datasources": "Tanımlı veri kaynağı yok", + "remove-datasource": "Veri kaynağını kaldır", + "behavior": "Davranış", + "on-click": "Tıklama ile", + "on-click-hint": "Kullanıcı harita öğesine tıkladığında tetiklenecek eylem.", + "groups": "Gruplar", + "groups-hint": "Katmana atanan grup adlarının listesi, harita üzerindeki görünürlüğünü değiştirmek için kullanılır.", + "color": "Renk", + "color-settings": "Renk ayarları", + "color-type-constant": "Sabit", + "color-type-range": "Aralık", + "color-type-function": "Fonksiyon", + "color-range-source-key": "Renk aralığı kaynak anahtarı", + "color-range-source-key-required": "Renk aralığı kaynak anahtarı gerekli", + "color-range": "Renk aralığı", + "color-function": "Renk fonksiyonu", + "label": "Etiket", + "tooltip": "Araç ipucu", + "pattern-type-pattern": "Desen", + "pattern-type-function": "Fonksiyon", + "label-pattern": "Etiket (desen örnekleri: '${entityName}', '${entityName}: (Metin ${keyName} birimleri.)' )", + "label-function": "Etiket fonksiyonu", + "tooltip-pattern": "Araç ipucu (örnek: 'Metin ${keyName} birimleri.' veya Bağlantı metni)", + "tooltip-function": "Araç ipucu fonksiyonu", + "tooltip-trigger": "Araç ipucu tetikleyici", + "tooltip-trigger-click": "Tıklama ile araç ipucunu göster", + "tooltip-trigger-hover": "Üzerine gelince araç ipucunu göster", + "auto-close-tooltips": "Araç ipuçlarını otomatik kapat", + "tooltip-offset": "Araç ipucu konumu", + "tooltip-offset-horizontal": "Yatay", + "tooltip-offset-vertical": "Dikey", + "tooltip-tag-actions": "Etiket eylemleri", + "add-tooltip-tag-action": "Etiket eylemi ekle", + "edit-tooltip-tag-action": "Etiket eylemini düzenle", + "remove-tooltip-tag-action": "Etiket eylemini kaldır", + "action-add": "Ekle", + "action-edit": "Düzenle", + "action-move": "Taşı", + "action-remove": "Kaldır", + "edit-instruments": "Araçları Düzenle", + "persist-location-attribute-scope": "Konumun kalıcı olacağı öznitelik kapsamı", + "enable-snapping": "Diğer köşe noktalarına hizalama özelliğini etkinleştir", + "enable-snapping-hint": "Yeni noktaları mevcut şekillerle otomatik olarak hizalayarak çizimi daha kolay ve hassas hale getirir.", + "drag-drop-mode": "Sürükle-bırak modu", + "trip": { + "no-trips": "Yapılandırılmış rota yok", + "add-trip": "Rota ekle", + "trip-configuration": "Rota yapılandırması", + "remove-trip": "Rotayı kaldır" + }, + "marker": { + "marker": "İşaretçi", + "latitude-key": "Enlem anahtarı", + "longitude-key": "Boylam anahtarı", + "x-pos-key": "X konum anahtarı", + "y-pos-key": "Y konum anahtarı", + "latitude-key-required": "Enlem anahtarı gerekli", + "longitude-key-required": "Boylam anahtarı gerekli", + "x-pos-key-required": "X konum anahtarı gerekli", + "y-pos-key-required": "Y konum anahtarı gerekli", + "no-markers": "Yapılandırılmış işaretçi yok", + "add-marker": "İşaretçi ekle", + "marker-configuration": "İşaretçi yapılandırması", + "remove-marker": "İşaretçiyi kaldır", + "marker-type": "İşaretçi türü", + "marker-type-shape": "Şekil", + "marker-type-icon": "Simge", + "marker-type-image": "Görsel", + "shape": "Şekil", + "icon": "Simge", + "image": "Görsel", + "marker-shapes": "İşaretçi şekilleri", + "marker-icon": "İşaretçi simgesi", + "marker-appearance": "İşaretçi görünümü", + "marker-image": "İşaretçi görseli", + "marker-image-type-image": "Görsel", + "marker-image-type-function": "Fonksiyon", + "custom-marker-image-size": "Özel işaretçi görsel boyutu", + "marker-image-function": "İşaretçi görsel fonksiyonu", + "marker-images": "İşaretçi görselleri", + "marker-offset": "İşaretçi konum kaydırması", + "offset-horizontal": "Yatay", + "offset-vertical": "Dikey", + "rotate-marker": "İşaretçiyi döndür", + "offset-angle": "Kaydırma açısı", + "position-conversion": "Konum dönüştürme", + "position-conversion-function": "Konum dönüştürme fonksiyonu, 0 ile 1 arasında çift değerler olarak x,y koordinatları döndürmelidir", + "clustering": { + "use-map-markers-clustering": "Harita işaretçileri kümelendirmesini kullan", + "zoom-on-cluster-click": "Bir kümeye tıklanınca yakınlaştır", + "max-zoom": "Bir işaretçinin kümeye dahil olabileceği maksimum yakınlaştırma seviyesi (0 - 18)", + "max-radius": "Bir kümenin kapsayacağı maksimum yarıçap", + "zoom-animation": "Yakınlaştırmada işaretçiler için animasyon", + "bounds-on-cluster-mouse-over": "Bir küme üzerine gelindiğinde işaretçilerin sınırları", + "spiderfy-max-zoom-level": "Maksimum yakınlaştırma seviyesinde yayma (tüm küme işaretçilerini görmek için)", + "load-optimization": "Yükleme optimizasyonu", + "chunked-load": "Sayfanın donmaması için işaretçileri parça parça ekle", + "lazy-load": "İşaretçileri eklemek için tembel yükleme kullan", + "use-cluster-marker-color-function": "Küme işaretçi renk fonksiyonunu kullan", + "marker-color-function": "İşaretçi renk fonksiyonu" + }, + "edit": "İşaretçiyi düzenle", + "remove-marker-for": "'{{entityName}}' için işaretçiyi kaldır", + "place-marker": "İşaretçi yerleştir", + "place-marker-hint": "İşaretçi yerleştirmek için tıklayın", + "place-marker-hint-with-entity": "'{{entityName}}' varlığını yerleştirmek için tıklayın" + }, + "path": { + "path": "Yol", + "path-decorator": "Yol süsleyici", + "decorator-symbol": "Süsleyici sembolü", + "decorator-symbol-arrow-head": "Ok", + "decorator-symbol-dash": "Kesik çizgi", + "decorator-arrangement": "Süsleme düzeni", + "decorator-offset": "Başlangıç", + "decorator-end-offset": "Bitiş", + "decorator-repeat": "Tekrarla" + }, + "points": { + "points": "Noktalar", + "point-tooltip": "Nokta araç ipucu" + }, + "shape": { + "fill": "Doldur", + "fill-type-color": "Renk", + "fill-type-stripe": "Şerit", + "fill-type-image": "Görsel", + "color": "Renk", + "stripe": "Şerit", + "image": "Görsel", + "stroke": "Kenarlık", + "fill-image": "Dolgu görseli", + "fill-image-type-image": "Görsel", + "fill-image-type-function": "Fonksiyon", + "preserve-aspect-ratio": "En-boy oranını koru", + "opacity": "Saydamlık", + "angle": "Dönme açısı", + "scale": "Ölçek", + "fill-image-function": "Şekil dolgu görsel fonksiyonu", + "fill-images": "Şekil dolgu görselleri", + "stripe-pattern": "Şerit deseni", + "first-stripe": "Birinci şerit", + "second-stripe": "İkinci şerit" + }, + "polygon": { + "polygon-key": "Poligon anahtarı", + "polygon-key-required": "Poligon anahtarı gerekli", + "no-polygons": "Tanımlı poligon yok", + "add-polygon": "Poligon ekle", + "polygon-configuration": "Poligon yapılandırması", + "remove-polygon": "Poligonu kaldır", + "edit": "Poligonu düzenle", + "remove-polygon-for": "'{{entityName}}' için poligonu kaldır", + "cut": "Poligon alanını kes", + "rotate": "Poligonu döndür", + "draw-rectangle": "Dikdörtgen çiz", + "draw-polygon": "Poligon çiz", + "polygon-place-first-point-cut-hint": "İlk noktayı yerleştirmek için tıklayın", + "continue-polygon-cut-hint": "Çizime devam etmek için tıklayın", + "finish-polygon-cut-hint": "Bitirmek ve kaydetmek için ilk işaretçiye tıklayın", + "polygon-place-first-point-hint": "Poligon: ilk noktayı yerleştirmek için tıklayın", + "polygon-place-first-point-hint-with-entity": "'{{entityName}}' için poligon: ilk noktayı yerleştirmek için tıklayın", + "continue-polygon-hint": "Poligon: çizime devam etmek için tıklayın", + "continue-polygon-hint-with-entity": "'{{entityName}}' için poligon: çizime devam etmek için tıklayın", + "finish-polygon-hint": "Poligon: çizimi tamamlamak için ilk işaretçiye tıklayın", + "finish-polygon-hint-with-entity": "'{{entityName}}' için poligon: tamamlamak ve kaydetmek için ilk işaretçiye tıklayın", + "rectangle-place-first-point-hint": "Dikdörtgen: ilk noktayı yerleştirmek için tıklayın", + "rectangle-place-first-point-hint-with-entity": "'{{entityName}}' için dikdörtgen: ilk noktayı yerleştirmek için tıklayın", + "finish-rectangle-hint": "Dikdörtgen: çizimi tamamlamak için tıklayın", + "finish-rectangle-hint-with-entity": "'{{entityName}}' için dikdörtgen: tamamlamak ve kaydetmek için tıklayın" + }, + "circle": { + "circle-key": "Daire anahtarı", + "circle-key-required": "Daire anahtarı gerekli", + "no-circles": "Tanımlı daire yok", + "add-circle": "Daire ekle", + "circle-configuration": "Daire yapılandırması", + "remove-circle": "Daireyi kaldır", + "edit": "Daireyi düzenle", + "remove-circle-for": "'{{entityName}}' için daireyi kaldır", + "draw-circle": "Daire çiz", + "place-circle-center-hint-with-entity": "'{{entityName}}' için daire: daire merkezini yerleştirmek için tıklayın", + "place-circle-center-hint": "Daire: daire merkezini yerleştirmek için tıklayın", + "finish-circle-hint-with-entity": "'{{entityName}}' için daire: tamamlamak ve kaydetmek için tıklayın", + "finish-circle-hint": "Daire: çizimi tamamlamak için tıklayın" + }, + "select-entity": "Varlık seç", + "select-entity-hint": "İpucu: seçimden sonra haritaya tıklayarak konum belirleyin" + }, + "select-entity": "Varlık seç", + "select-entity-hint": "İpucu: seçimden sonra haritaya tıklayarak konum belirleyin", + "tooltips": { + "placeMarker": "'{{entityName}}' varlığını yerleştirmek için tıklayın", + "firstVertex": "'{{entityName}}' için poligon: ilk noktayı yerleştirmek için tıklayın", + "firstVertex-cut": "İlk noktayı yerleştirmek için tıklayın", + "continueLine": "'{{entityName}}' için poligon: çizime devam etmek için tıklayın", + "continueLine-cut": "Çizime devam etmek için tıklayın", + "finishLine": "Bitirmek için herhangi bir mevcut işaretçiye tıklayın", + "finishPoly": "'{{entityName}}' için poligon: tamamlamak ve kaydetmek için ilk işaretçiye tıklayın", + "finishPoly-cut": "Tamamlamak ve kaydetmek için ilk işaretçiye tıklayın", + "finishRect": "'{{entityName}}' için poligon: tamamlamak ve kaydetmek için tıklayın", + "startCircle": "'{{entityName}}' için daire: daire merkezini yerleştirmek için tıklayın", + "finishCircle": "'{{entityName}}' için daire: daireyi tamamlamak için tıklayın", + "placeCircleMarker": "Daire işaretçisini yerleştirmek için tıklayın" + }, + "actions": { + "finish": "Bitir", + "cancel": "İptal", + "removeLastVertex": "Son noktayı kaldır" + }, + "buttonTitles": { + "drawMarkerButton": "Varlık yerleştir", + "drawPolyButton": "Poligon oluştur", + "drawLineButton": "Çoklu çizgi oluştur", + "drawCircleButton": "Daire oluştur", + "drawRectButton": "Dikdörtgen oluştur", + "editButton": "Düzenleme modu", + "dragButton": "Sürükle-bırak modu", + "cutButton": "Poligon alanını kes", + "deleteButton": "Kaldır", + "drawCircleMarkerButton": "Daire işaretçisi oluştur", + "rotateButton": "Poligonu döndür" + }, + "map-provider-settings": "Harita sağlayıcı ayarları", + "map-provider": "Harita sağlayıcısı", + "map-provider-google": "Google Haritalar", + "map-provider-openstreet": "OpenStreet Haritalar", + "map-provider-here": "HERE Haritalar", + "map-provider-image": "Görsel harita", + "map-provider-tencent": "Tencent Haritalar", + "openstreet-provider": "OpenStreet harita sağlayıcısı", + "openstreet-provider-mapnik": "OpenStreetMap.Mapnik (Varsayılan)", + "openstreet-provider-hot": "OpenStreetMap.HOT", + "openstreet-provider-esri-street": "Esri.WorldStreetMap", + "openstreet-provider-esri-topo": "Esri.WorldTopoMap", + "openstreet-provider-esri-imagery": "Esri.WorldImagery", + "openstreet-provider-cartodb-positron": "CartoDB.Positron", + "openstreet-provider-cartodb-dark-matter": "CartoDB.DarkMatter", + "use-custom-provider": "Özel sağlayıcı kullan", + "custom-provider-tile-url": "Özel sağlayıcı tile URL'si", + "google-maps-api-key": "Google Maps API Anahtarı", + "default-map-type": "Varsayılan harita türü", + "google-map-type-roadmap": "Yol Haritası", + "google-map-type-satelite": "Uydu", + "google-map-type-hybrid": "Hibrit", + "google-map-type-terrain": "Arazi", + "map-layer": "Harita katmanı", + "here-map-normal-day": "HERE.normalDay (Varsayılan)", + "here-map-normal-night": "HERE.normalNight", + "here-map-hybrid-day": "HERE.hybridDay", + "here-map-terrain-day": "HERE.terrainDay", + "credentials": "Kimlik bilgileri", + "here-app-id": "HERE uygulama kimliği", + "here-app-code": "HERE uygulama kodu", + "here-api-key": "HERE API anahtarı", + "here-use-new-version-api-3": "API sürüm 3'ü kullan", + "tencent-maps-api-key": "Tencent Haritalar API Anahtarı", + "tencent-map-type-roadmap": "Yol Haritası", + "tencent-map-type-satelite": "Uydu", + "tencent-map-type-hybrid": "Hibrit", + "image-map-background": "Görsel harita arka planı", + "image-map-background-from-entity-attribute": "Görsel harita arka planını varlık özniteliğinden al", + "image-url-source-entity-alias": "Görsel URL kaynağı varlık takma adı", + "image-url-source-entity-attribute": "Görsel URL kaynağı varlık özniteliği", + "common-map-settings": "Genel harita ayarları", + "x-pos-key-name": "X konum anahtarı adı", + "y-pos-key-name": "Y konum anahtarı adı", + "latitude-key-name": "Enlem anahtarı adı", + "longitude-key-name": "Boylam anahtarı adı", + "default-map-zoom-level": "Varsayılan harita yakınlaştırma seviyesi (0 - 20)", + "default-map-center-position": "Varsayılan harita merkez konumu (0,0)", + "disable-scroll-zooming": "Kaydırarak yakınlaştırmayı devre dışı bırak", + "disable-double-click-zooming": "Çift tıklamayla yakınlaştırmayı devre dışı bırak", + "disable-zoom-control-buttons": "Yakınlaştırma kontrol düğmelerini devre dışı bırak", + "fit-map-bounds": "Tüm işaretçileri kapsayacak şekilde harita sınırlarını uyarla", + "use-default-map-center-position": "Varsayılan harita merkez konumunu kullan", + "entities-limit": "Yüklenecek varlık sayısı sınırı", + "markers-settings": "İşaretçi ayarları", + "marker-offset-x": "İşaretçi X konum kaydırması, konuma göre işaretçi genişliği ile çarpılır", + "marker-offset-y": "İşaretçi Y konum kaydırması, konuma göre işaretçi yüksekliği ile çarpılır", + "position-function": "Konum dönüştürme fonksiyonu, her biri 0 ile 1 arasında x,y koordinatları döndürmelidir", + "draggable-marker": "Sürüklenebilir işaretçi", + "label": "Etiket", + "show-label": "Etiketi göster", + "use-label-function": "Etiket fonksiyonunu kullan", + "label-pattern": "Etiket (desen örnekleri: '${entityName}', '${entityName}: (Metin ${keyName} birimleri.)' )", + "label-function": "Etiket fonksiyonu", + "tooltip": "Araç ipucu", + "show-tooltip": "Araç ipucunu göster", + "show-tooltip-action": "Araç ipucunu gösterme eylemi", + "show-tooltip-action-click": "Tıklama ile araç ipucunu göster (Varsayılan)", + "show-tooltip-action-hover": "Üzerine gelince araç ipucunu göster", + "auto-close-tooltips": "Araç ipuçlarını otomatik kapat", + "use-tooltip-function": "Araç ipucu fonksiyonunu kullan", + "tooltip-pattern": "Araç ipucu (örnek: 'Metin ${keyName} birimleri.' veya Bağlantı metni)", + "tooltip-function": "Araç ipucu fonksiyonu", + "tooltip-offset-x": "Araç ipucu X kaydırması, işaretçi çapası konumuna göre işaretçi genişliği ile çarpılır", + "tooltip-offset-y": "Araç ipucu Y kaydırması, işaretçi çapası konumuna göre işaretçi yüksekliği ile çarpılır", + "color": "Renk", + "use-color-function": "Renk fonksiyonunu kullan", + "color-function": "Renk fonksiyonu", + "marker-image": "İşaretçi görseli", + "use-marker-image-function": "İşaretçi görsel fonksiyonunu kullan", + "custom-marker-image": "Özel işaretçi görseli", + "custom-marker-image-size": "Özel işaretçi görsel boyutu (px)", + "marker-image-function": "İşaretçi görsel fonksiyonu", + "marker-images": "İşaretçi görselleri", + "polygon-settings": "Poligon ayarları", + "show-polygon": "Poligonu göster", + "polygon-key-name": "Poligon anahtarı adı", + "enable-polygon-edit": "Poligon düzenlemeyi etkinleştir", + "polygon-label": "Poligon etiketi", + "show-polygon-label": "Poligon etiketini göster", + "use-polygon-label-function": "Poligon etiket fonksiyonunu kullan", + "polygon-label-pattern": "Poligon etiketi (desen örnekleri: '${entityName}', '${entityName}: (Metin ${keyName} birimleri.)' )", + "polygon-label-function": "Poligon etiket fonksiyonu", + "polygon-tooltip": "Poligon araç ipucu", + "show-polygon-tooltip": "Poligon araç ipucunu göster", + "auto-close-polygon-tooltips": "Poligon araç ipuçlarını otomatik kapat", + "use-polygon-tooltip-function": "Poligon araç ipucu fonksiyonunu kullan", + "polygon-tooltip-pattern": "Araç ipucu (örnek: 'Metin ${keyName} birimleri.' veya Bağlantı metni)", + "polygon-tooltip-function": "Poligon araç ipucu fonksiyonu", + "polygon-color": "Poligon rengi", + "polygon-opacity": "Poligon saydamlığı", + "use-polygon-color-function": "Poligon renk fonksiyonunu kullan", + "polygon-color-function": "Poligon renk fonksiyonu", + "polygon-stroke": "Poligon kenarlığı", + "stroke-color": "Kenarlık rengi", + "stroke-opacity": "Kenarlık saydamlığı", + "stroke-weight": "Kenarlık kalınlığı", + "use-polygon-stroke-color-function": "Poligon kenarlık rengi fonksiyonunu kullan", + "polygon-stroke-color-function": "Poligon kenarlık rengi fonksiyonu", + "circle-settings": "Daire ayarları", + "show-circle": "Daireyi göster", + "circle-key-name": "Daire anahtarı adı", + "enable-circle-edit": "Daire düzenlemeyi etkinleştir", + "circle-label": "Daire etiketi", + "show-circle-label": "Daire etiketini göster", + "use-circle-label-function": "Daire etiket fonksiyonunu kullan", + "circle-label-pattern": "Daire etiketi (desen örnekleri: '${entityName}', '${entityName}: (Metin ${keyName} birimleri.)' )", + "circle-label-function": "Daire etiket fonksiyonu", + "circle-tooltip": "Daire araç ipucu", + "show-circle-tooltip": "Daire araç ipucunu göster", + "auto-close-circle-tooltips": "Daire araç ipuçlarını otomatik kapat", + "use-circle-tooltip-function": "Daire araç ipucu fonksiyonunu kullan", + "circle-tooltip-pattern": "Araç ipucu (örnek: 'Metin ${keyName} birimleri.' veya Bağlantı metni)", + "circle-tooltip-function": "Daire araç ipucu fonksiyonu", + "circle-fill-color": "Daire dolgu rengi", + "circle-fill-color-opacity": "Daire dolgu rengi saydamlığı", + "use-circle-fill-color-function": "Daire dolgu rengi fonksiyonunu kullan", + "circle-fill-color-function": "Daire dolgu rengi fonksiyonu", + "circle-stroke": "Daire kenarlığı", + "use-circle-stroke-color-function": "Daire kenarlık rengi fonksiyonunu kullan", + "circle-stroke-color-function": "Daire kenarlık rengi fonksiyonu", + "markers-clustering-settings": "İşaretçileri kümelendirme ayarları", + "use-map-markers-clustering": "Harita işaretçileri kümelendirmesini kullan", + "zoom-on-cluster-click": "Bir kümeye tıklanınca yakınlaştır", + "max-cluster-zoom": "Bir işaretçinin kümeye dahil olabileceği maksimum yakınlaştırma seviyesi (0 - 18)", + "max-cluster-radius-pixels": "Bir kümenin kapsayacağı maksimum yarıçap (piksel)", + "cluster-zoom-animation": "Yakınlaştırmada işaretçilere animasyon göster", + "show-markers-bounds-on-cluster-mouse-over": "Kümeye fareyle gelindiğinde işaretçilerin sınırlarını göster", + "spiderfy-max-zoom-level": "Maksimum yakınlaştırma seviyesinde yayma (tüm küme işaretçilerini görmek için)", + "load-optimization": "Yükleme optimizasyonu", + "cluster-chunked-loading": "Sayfanın donmaması için işaretçileri parça parça ekle", + "cluster-markers-lazy-load": "İşaretçileri eklemek için tembel yükleme kullan", + "editor-settings": "Düzenleyici ayarları", + "enable-snapping": "Hassas çizim için diğer köşe noktalarına hizalama özelliğini etkinleştir", + "init-draggable-mode": "Haritayı sürüklenebilir modda başlat", + "hide-all-edit-buttons": "Tüm düzenleme kontrol düğmelerini gizle", + "hide-draw-buttons": "Çizim düğmelerini gizle", + "hide-edit-buttons": "Düzenleme düğmelerini gizle", + "hide-remove-button": "Kaldır düğmesini gizle", + "route-map-settings": "Rota haritası ayarları", + "trip-animation-settings": "Rota animasyon ayarları", + "normalization-step": "Veri normalizasyon adımı (ms)", + "tooltip-background-color": "Araç ipucu arka plan rengi", + "tooltip-font-color": "Araç ipucu yazı rengi", + "tooltip-opacity": "Araç ipucu saydamlığı (0-1)", + "auto-close-tooltip": "Araç ipucunu otomatik kapat", + "rotation-angle": "İşaretçi için ek dönme açısı belirle (derece)", + "path-settings": "Yol ayarları", + "path-color": "Yol rengi", + "use-path-color-function": "Yol rengi fonksiyonunu kullan", + "path-color-function": "Yol rengi fonksiyonu", + "path-decorator": "Yol süsleyici", + "use-path-decorator": "Yol süsleyiciyi kullan", + "decorator-symbol": "Süsleyici sembolü", + "decorator-symbol-arrow-head": "Ok", + "decorator-symbol-dash": "Kesik çizgi", + "decorator-symbol-size": "Süsleyici sembol boyutu (px)", + "use-path-decorator-custom-color": "Yol süsleyici özel rengini kullan", + "decorator-custom-color": "Süsleyici özel rengi", + "decorator-offset": "Süsleyici kaydırması", + "end-decorator-offset": "Bitiş süsleyici kaydırması", + "decorator-repeat": "Süsleyici tekrarı", + "points-settings": "Nokta ayarları", + "show-points": "Noktaları göster", + "point-color": "Nokta rengi", + "point-size": "Nokta boyutu (px)", + "use-point-color-function": "Nokta rengi fonksiyonunu kullan", + "point-color-function": "Nokta rengi fonksiyonu", + "use-point-as-anchor": "Noktayı çapa olarak kullan", + "point-as-anchor-function": "Nokta çapa fonksiyonu", + "independent-point-tooltip": "Bağımsız nokta araç ipucu", + "clustering-markers": "İşaretçileri kümelendir", + "use-icon-create-function": "İşaretçi renk fonksiyonunu kullan", + "marker-color-function": "İşaretçi renk fonksiyonu" + }, + "markdown": { + "use-markdown-text-function": "Markdown/HTML değer fonksiyonunu kullan", + "markdown-text-function": "Markdown/HTML değer fonksiyonu", + "markdown-text-pattern": "Markdown/HTML deseni (değişken içeren markdown veya HTML, örn. '${entityName} veya ${keyName} - bazı metinler.')", + "apply-default-markdown-style": "Varsayılan markdown stilini uygula", + "markdown-css": "Markdown/HTML CSS" + }, + "simple-card": { + "label": "Etiket", + "label-position": "Etiket konumu", + "label-position-left": "Sol", + "label-position-top": "Üst" + }, + "single-switch": { + "behavior": "Davranış", + "layout": "Yerleşim", + "layout-right": "Sağ", + "layout-left": "Sol", + "layout-centered": "Ortalanmış", + "auto-scale": "Otomatik ölçekle", + "label": "Etiket", + "icon": "Simge", + "switch-color": "Anahtar rengi", + "on": "Açık", + "off": "Kapalı", + "disabled": "Devre dışı", + "tumbler-color": "Tumbler rengi", + "on-label": "Açık etiketi", + "off-label": "Kapalı etiketi", + "switch": "Anahtar" + }, + "slider": { + "behavior": "Davranış", + "initial-value": "Başlangıç değeri", + "initial-value-hint": "Sürgü bileşeninin başlangıç değerini alma eylemi.", + "on-value-change": "Değer değiştiğinde", + "on-value-change-hint": "Sürgü değeri değiştirildiğinde tetiklenen eylem.", + "layout": "Yerleşim", + "layout-default": "Varsayılan", + "layout-extended": "Genişletilmiş", + "layout-simplified": "Basitleştirilmiş", + "auto-scale": "Otomatik ölçekle", + "icon": "Simge", + "value": "Değer", + "range": "Aralık", + "min": "min", + "max": "maks", + "range-ticks": "Aralık işaretleri", + "tick-marks": "İşaret çizgileri", + "colors": "Renkler", + "main": "Ana", + "background": "Arka plan", + "left-icon": "Sol simge", + "right-icon": "Sağ simge", + "slider": "Sürgü" + }, + "value-card": { + "layout": "Yerleşim", + "layout-square": "Kare", + "layout-vertical": "Dikey", + "layout-centered": "Ortalanmış", + "layout-simplified": "Basitleştirilmiş", + "layout-horizontal": "Yatay", + "layout-horizontal-reversed": "Yatay ters", + "label": "Etiket", + "icon": "Simge", + "value": "Değer", + "date": "Tarih", + "value-card-style": "Değer kartı stili", + "auto-scale": "Otomatik ölçekle" + }, + "label-card": { + "auto-scale": "Otomatik ölçekle", + "label": "Etiket", + "icon": "Simge", + "label-card-style": "Etiket kartı stili" + }, + "label-value-card": { + "value": "Değer", + "label-value-card-style": "Etiket ve değer kartı stili" + }, + "liquid-level-card": { + "layout-simple": "Basit", + "layout-percentage": "Yüzde", + "layout-absolute": "Mutlak", + "layout": "Yerleşim", + "background-overlay": "Değer arka plan kaplaması", + "total-volume": "Toplam hacim", + "total-volume-units": "Toplam hacim birimi", + "tank": "Depo", + "shape": "Şekil", + "datasource-units": "Kaynak birimleri", + "widget-units": "Widget birimleri", + "decimals": "Ondalık basamaklar", + "liquid": "Sıvı", + "liquid-color": "Sıvı rengi", + "value": "Değer", + "value-font": "Değer yazı tipi", + "level": "Seviye", + "last-update": "Son güncelleme", + "shape-by-attribute": "Depo şeklini öznitelik adına göre ayarla", + "tooltip-background": "Arka plan rengi", + "background-blur": "Arka plan bulanıklığı", + "tank-color": "Depo rengi", + "static": "Statik", + "see-examples": "Örnekleri gör", + "attribute": "Öznitelik", + "shape-type": "Tür", + "v-oval": "Dikey Oval", + "v-cylinder": "Dikey Silindir", + "v-capsule": "Dikey Kapsül", + "rectangle": "Dikdörtgen", + "h-oval": "Yatay Oval", + "h-ellipse": "Yatay Elips", + "h-dish-ends": "Yatay Yarım Küre Uçlar", + "h-cylinder": "Yatay Silindir", + "h-capsule": "Yatay Kapsül", + "h-elliptical_2_1": "Yatay 2:1 Eliptik", + "icon": "Kart simgesi", + "title": "Kart başlığı", + "units": "Birimler", + "color-and-font": "Renk ve yazı tipi", + "shape-attribute-name": "Öznitelik adı", + "total-volume-required": "Toplam hacim gereklidir.", + "attribute-name-required": "Öznitelik adı gereklidir.", + "attribute-key-not-set": "'{{attributeName}}' öznitelik anahtarı ayarlanmamış", + "attribute-key-invalid": "'{{attributeName}}' öznitelik anahtarı geçersiz" + }, + "aggregated-value-card": { + "subtitle": "Alt başlık", + "chart": "Grafik", + "values": "Değerler", + "value-appearance": "Değer görünümü", + "position": "Konum", + "position-center": "Orta", + "position-right-top": "Sağ üst", + "position-right-bottom": "Sağ alt", + "position-left-top": "Sol üst", + "position-left-bottom": "Sol alt", + "font": "Yazı tipi", + "color": "Renk", + "arrow": "Ok", + "display-up-down-arrow": "Yukarı/Aşağı ok göster", + "add-value": "Değer ekle", + "remove-value": "Değeri kaldır", + "no-values": "Tanımlı değer yok", + "aggregation": "Toplama", + "aggregated-value-card-style": "Toplam değer kartı stili", + "auto-scale": "Otomatik ölçekle" + }, + "value-chart-card": { + "layout": "Yerleşim", + "layout-left": "Sol", + "layout-right": "Sağ", + "auto-scale": "Otomatik ölçekle", + "icon": "Simge", + "value": "Değer", + "chart": "Grafik", + "value-chart-card-style": "Değer grafik kartı stili" + }, + "progress-bar": { + "layout": "Yerleşim", + "layout-default": "Varsayılan", + "layout-simplified": "Basitleştirilmiş", + "auto-scale": "Otomatik ölçekle", + "icon": "Simge", + "value": "Değer", + "range": "Aralık", + "min": "min", + "max": "maks", + "range-ticks": "Aralık işaretleri", + "bar": "Çubuk", + "bar-color": "Çubuk rengi", + "bar-background": "Çubuk arka planı", + "progress-bar-card-style": "İlerleme çubuğu kartı stili" + }, + "notification": { + "max-notification-display": "Gösterilecek maksimum bildirim sayısı", + "counter": "Sayaç", + "counter-hint": "Eğer \"Widget başlığı\" etkinse sayaç görüntülenir", + "icon": "Simge", + "counter-value": "Değer", + "counter-color": "Renk", + "notification-button": "Bildirim düğmeleri", + "button-view-all": "Tümünü gör", + "button-filter": "Filtrele", + "type-filter": "Tür filtresi", + "button-mark-read": "Tümünü okundu olarak işaretle", + "notification-types": "Bildirim türleri", + "notification-type": "Bildirim türü", + "search-type": "Arama türü", + "any-type": "Herhangi bir tür" + }, + "alarm-count": { + "alarm-count-card-style": "Alarm sayısı kartı stili" + }, + "entity-count": { + "entity-count-card-style": "Varlık sayısı kartı stili" + }, + "count": { + "layout": "Yerleşim", + "layout-column": "Sütun", + "layout-row": "Satır", + "label": "Etiket", + "icon": "Simge", + "icon-background": "Simge arka planı", + "value": "Değer", + "chevron": "Chevron", + "auto-scale": "Otomatik ölçekle" + }, + "table": { + "common-table-settings": "Ortak Tablo Ayarları", + "enable-search": "Aramayı etkinleştir", + "enable-sticky-header": "Başlığı her zaman göster", + "enable-sticky-action": "Eylemler sütununu her zaman göster", + "hidden-cell-button-display-mode": "Gizli hücre buton eylemleri görüntüleme modu", + "show-empty-space-hidden-action": "Gizli hücre buton eylemi yerine boş alan göster", + "dont-reserve-space-hidden-action": "Gizli eylem düğmeleri için alan ayırma", + "display-timestamp": "Zaman damgası", + "display-pagination": "Sayfalama göster", + "default-page-size": "Varsayılan sayfa boyutu", + "page-step-settings": "Sayfa adımı ayarları", + "page-step-count": "Adım sayısı", + "page-step-increment": "Adım artışı", + "page-step-count-format-message": "1 ile 100 arasında bir tam sayı olmalıdır.", + "page-step-increment-format-message": "1 veya daha büyük bir tam sayı olmalıdır.", + "use-entity-label-tab-name": "Sekme adında varlık etiketini kullan", + "hide-empty-lines": "Boş satırları gizle", + "row-style": "Satır stili", + "use-row-style-function": "Satır stili fonksiyonunu kullan", + "row-style-function": "Satır stili fonksiyonu", + "cell-style": "Hücre stili", + "use-cell-style-function": "Hücre stili fonksiyonunu kullan", + "cell-style-function": "Hücre stili fonksiyonu", + "cell-content": "Hücre içeriği", + "use-cell-content-function": "Hücre içeriği fonksiyonunu kullan", + "cell-content-function": "Hücre içeriği fonksiyonu", + "show-latest-data-column": "En son veri sütununu göster", + "latest-data-column-order": "En son veri sütunu sıralaması", + "entities-table-title": "Varlıklar tablosu başlığı", + "enable-select-column-display": "Görüntülenecek sütunları seçmeyi etkinleştir", + "display-entity-name": "Varlık adı sütununu göster", + "entity-name-column-title": "Varlık adı sütunu başlığı", + "display-entity-label": "Varlık etiketi sütununu göster", + "entity-label-column-title": "Varlık etiketi sütunu başlığı", + "display-entity-type": "Varlık türü sütununu göster", + "default-sort-order": "Varsayılan sıralama düzeni", + "custom-title": "Özel başlık", + "column-width": "Sütun genişliği (px veya %)", + "default-column-visibility": "Varsayılan sütun görünürlüğü", + "column-visibility-visible": "Görünür", + "column-visibility-hidden": "Gizli", + "column-visibility-hidden-mobile": "Mobil modda gizli", + "column-selection-to-display": "'Görüntülenecek Sütunlar'da sütun seçimi", + "column-selection-to-display-enabled": "Etkin", + "column-selection-to-display-disabled": "Devre dışı", + "alarms-table-title": "Alarm tablosu başlığı", + "enable-alarms-selection": "Alarm seçimini etkinleştir", + "enable-alarms-search": "Alarm aramasını etkinleştir", + "enable-alarm-filter": "Alarm filtresini etkinleştir", + "display-alarm-details": "Alarm detaylarını göster", + "allow-alarms-ack": "Alarmların onaylanmasına izin ver", + "allow-alarms-clear": "Alarmların temizlenmesine izin ver", + "display-alarm-activity": "Alarm etkinliğini göster", + "allow-alarms-assign": "Alarmların atanmasına izin ver", + "columns": "Sütunlar", + "column-settings": "Sütun ayarları", + "remove-column": "Sütunu kaldır", + "add-column": "Sütun ekle", + "no-columns": "Tanımlı sütun yok", + "columns-to-display": "Görüntülenecek sütunlar", + "table-header": "Tablo başlığı", + "header-buttons": "Başlık düğmeleri", + "table-buttons": "Tablo düğmeleri", + "pagination": "Sayfalama", + "rows": "Satırlar", + "timeseries-column-error": "En az bir zaman serisi sütunu belirtilmelidir", + "alarm-column-error": "En az bir alarm sütunu belirtilmelidir", + "table-tabs": "Tablo sekmeleri", + "show-cell-actions-menu-mobile": "Mobil modda hücre eylem açılır menüsünü göster", + "disable-sorting": "Sıralamayı devre dışı bırak" + }, + "latest-chart": { + "total": "Toplam", + "auto-scale": "Otomatik ölçekle", + "clockwise-layout": "Saat yönünde yerleşim", + "sort-series": "Serileri etikete göre sırala", + "tooltip-value-type-absolute": "Mutlak", + "tooltip-value-type-percentage": "Yüzde" + }, + "pie-chart": { + "pie-chart-appearance": "Pasta grafik görünümü", + "label": "Etiket", + "border": "Kenarlık", + "radius": "Yarıçap", + "pie-chart-card-style": "Pasta grafik kartı stili" + }, + "radar-chart": { + "radar-appearance": "Radar görünümü", + "shape": "Şekil", + "shape-polygon": "Poligon", + "shape-circle": "Daire", + "color": "Renk", + "line": "Çizgi", + "points": "Noktalar", + "points-label": "Nokta etiketleri", + "radar-axis": "Radar ekseni", + "axis-label": "Eksen etiketi", + "ticks-label": "İşaret etiketleri", + "radar-chart-style": "Radar grafik stili", + "max-axes-scaling": "Maksimum eksen ölçekleme", + "max-axes-scaling-hint": "Her radar ekseninin kendi maksimum değerine mi sahip olacağını (Ayrı) yoksa tüm eksenler için en yüksek değeri paylaşacağını (Ortak) belirleyin.", + "separate": "Ayrı", + "common": "Ortak" + }, + "time-series-chart": { + "chart": "Grafik", + "chart-style": "Grafik stili", + "data-zoom": "Veri yakınlaştırma", + "stack-mode": "Yığın modu", + "stack-mode-hint": "Grafikte serileri üst üste yığar. Aynı birime sahip seriler birbirinin üzerine yerleştirilir.", + "axes": "Eksenler", + "y-axes": "Y eksenleri", + "line-type": "Çizgi türü", + "line-width": "Çizgi kalınlığı", + "type-line": "Çizgi", + "type-bar": "Çubuk", + "type-point": "Nokta", + "no-aggregation-bar-width-strategy": "Toplanmamış veriler için çubuk genişliği stratejisi", + "no-aggregation-bar-width-strategy-group": "Grup", + "no-aggregation-bar-width-strategy-separate": "Ayrı", + "bar-group-width": "Çubuk grup genişliği", + "bar-width": "Çubuk genişliği", + "bar-width-relative": "Zaman penceresi yüzdesi", + "bar-width-absolute": "Mutlak (ms)", + "comparison": { + "comparison": "Karşılaştırma", + "comparison-hint": "Karşılaştırma yalnızca geçmiş verilerle çalışır!", + "show": "Göster", + "settings": "Karşılaştırma ayarları", + "show-values-for-comparison": "Karşılaştırma için geçmiş verileri göster", + "comparison-values-label": "Karşılaştırma anahtar etiketi", + "comparison-values-label-auto": "Otomatik", + "comparison-data-color": "Karşılaştırma veri rengi" + }, + "threshold": { + "thresholds": "Eşikler", + "source": "Kaynak", + "key-value": "Anahtar / Değer", + "no-thresholds": "Tanımlı eşik yok", + "add-threshold": "Eşik ekle", + "type-constant": "Sabit", + "type-latest-key": "Anahtar", + "type-entity": "Varlık", + "threshold-settings": "Eşik ayarları", + "remove-threshold": "Eşiği kaldır", + "threshold-value-required": "Eşik değeri gereklidir.", + "key-required": "Anahtar gereklidir.", + "entity-key-required": "Varlık anahtarı gereklidir.", + "line-appearance": "Çizgi görünümü", + "line-color": "Çizgi rengi", + "start-symbol": "Başlangıç simgesi", + "end-symbol": "Bitiş simgesi", + "symbol-size": "Boyut", + "label": "Etiket", + "label-position-start": "Başlangıç", + "label-position-middle": "Orta", + "label-position-end": "Bitiş", + "label-position-inside-start": "İç başlangıç", + "label-position-inside-start-top": "İç başlangıç üst", + "label-position-inside-start-bottom": "İç başlangıç alt", + "label-position-inside-middle": "İç orta", + "label-position-inside-middle-top": "İç orta üst", + "label-position-inside-middle-bottom": "İç orta alt", + "label-position-inside-end": "İç bitiş", + "label-position-inside-end-top": "İç bitiş üst", + "label-position-inside-end-bottom": "İç bitiş alt", + "label-background": "Etiket arka planı" + }, + "state": { + "states": "Durumlar", + "label": "Etiket", + "ticks-value": "İşaret değeri", + "source": "Kaynak", + "value-range": "Değer / Aralık", + "no-states": "Tanımlı durum yok", + "add-state": "Durum ekle", + "type-constant": "Sabit", + "type-range": "Aralık", + "from": "Başlangıç", + "to": "Bitiş", + "remove-state": "Durumu kaldır" + }, + "grid": { + "grid": "Izgara", + "background-color": "Arka plan rengi", + "border": "Kenarlık" + }, + "axis": { + "axes": "Eksenler", + "x-axis": "X ekseni", + "y-axis": "Y ekseni", + "y-axis-settings": "Y ekseni ayarları", + "comparison-x-axis-settings": "Karşılaştırma X ekseni ayarları", + "remove-y-axis": "Y eksenini kaldır", + "id": "Kimlik", + "label": "Etiket", + "position": "Konum", + "position-left": "Sol", + "position-right": "Sağ", + "position-top": "Üst", + "position-bottom": "Alt", + "tick-labels": "İşaret etiketleri", + "ticks-formatter-function": "İşaret biçimlendirici fonksiyonu", + "ticks-generator-function": "İşaret oluşturucu fonksiyonu", + "show-ticks": "İşaretleri göster", + "show-line": "Çizgiyi göster", + "show-split-lines": "Bölme çizgilerini göster", + "show-split-lines-x-axis-hint": "Etkinleştirilirse, grafikte dikey çizgiler gösterilir.", + "show-split-lines-y-axis-hint": "Etkinleştirilirse, grafikte yatay çizgiler gösterilir.", + "ticks-interval": "İşaret aralığı", + "ticks-interval-hint": "Eksen için zorunlu segment aralığı ayarı.", + "split-number": "Bölüm sayısı", + "split-number-hint": "Eksenin bölüneceği segment sayısı.", + "min": "Min", + "max": "Maks", + "show": "Göster", + "add-y-axis": "Y ekseni ekle" + }, + "series": { + "legend-settings": "Gösterge ayarları", + "show-in-legend": "Göstergede göster", + "show-in-legend-hint": "Seri adını ve verisini göstergede göster.", + "hidden-by-default": "Varsayılan olarak gizli", + "hidden-by-default-hint": "Seriyi varsayılan olarak göstergede gizli yap.", + "series-type": "Seri türü", + "type": "Tür", + "type-line": "Çizgi", + "type-bar": "Çubuk", + "line": { + "line": "Çizgi", + "show-line": "Çizgiyi göster", + "step-line": "Adımlı çizgi", + "step-type-start": "Başlangıç", + "step-type-middle": "Orta", + "step-type-end": "Bitiş", + "smooth-line": "Yumuşak çizgi" + }, + "point": { + "points": "Noktalar", + "show-points": "Noktaları göster", + "point-label": "Nokta etiketi", + "point-label-hint": "Seri noktası üzerinde değeriyle birlikte etiketi göster.", + "point-label-background": "Nokta etiketi arka planı", + "point-shape": "Nokta şekli", + "point-size": "Nokta boyutu" + } + } + }, + "wind-speed-direction": { + "layout": "Yerleşim", + "layout-default": "Varsayılan", + "layout-advanced": "Gelişmiş", + "layout-simplified": "Basitleştirilmiş", + "values": "Değerler", + "wind-direction": "Rüzgar yönü", + "center-value": "Merkez değeri", + "icon": "Simge", + "arrow": "Ok", + "ticks": "İşaretler", + "labels-type": "Etiket türü", + "directional-names": "Yön adları", + "degrees": "Dereceler", + "major-ticks": "Ana işaretler", + "minor-ticks": "İkincil işaretler", + "wind-speed-direction-card-style": "Rüzgar hızı ve yönü kartı stili", + "ticks-color": "İşaret rengi", + "ticks-labels-type": "İşaret etiket türü", + "arrow-color": "Ok rengi" + }, + "value-source": { + "value-source": "Değer kaynağı", + "predefined-value": "Sabit", + "entity-attribute": "Varlık özniteliği", + "value": "Değer", + "value-required": "Değer gereklidir.", + "key-required": "Anahtar gereklidir.", + "entity-key-required": "Varlık anahtarı gereklidir.", + "source-entity-alias": "Kaynak varlık takma adı", + "source-entity-attribute": "Kaynak varlık özniteliği", + "type-constant": "Sabit", + "type-latest-key": "Anahtar", + "type-entity": "Varlık" + }, + "rpc-state": { + "initial-state": "Başlangıç durumu", + "initial-state-hint": "Bileşenin ilk durumu (Açık/Kapalı) almak için eylem.", + "disabled-state": "Devre dışı durumu", + "disabled-state-hint": "Bileşenin devre dışı kalacağı durumu yapılandırın.", + "turn-on": "'Aç' durumuna geç", + "turn-on-hint": "Bileşen 'Açık' durumuna alındığında tetiklenen eylem.", + "turn-off": "'Kapat' durumuna geç", + "turn-off-hint": "Bileşen 'Kapalı' durumuna alındığında tetiklenen eylem.", + "on": "Açık", + "off": "Kapalı", + "disabled": "Devre dışı" + }, + "value-action": { + "do-nothing": "Hiçbir şey yapma", + "execute-rpc": "RPC çalıştır", + "get-attribute": "Öznitelik al", + "set-attribute": "Öznitelik ayarla", + "get-time-series": "Zaman serisini al", + "get-alarm-status": "Alarm durumunu al", + "get-dashboard-state": "Dashboard durum kimliğini al", + "get-dashboard-state-object": "Dashboard durum nesnesini al", + "add-time-series": "Zaman serisi ekle", + "execute-rpc-text": "'{{methodName}}' RPC metodunu çalıştır", + "get-time-series-text": "'{{key}}' zaman serisini kullan", + "get-attribute-text": "'{{key}}' özniteliğini kullan", + "get-alarm-status-text": "Alarm durumunu kullan", + "get-dashboard-state-text": "Dashboard durumunu kullan", + "get-dashboard-state-object-text": "Dashboard durum nesnesini kullan", + "when-dashboard-state-is-text": "Dashboard durum kimliği '{{state}}' olduğunda", + "when-dashboard-state-function-is-text": "f(dashboard durum kimliği) '{{state}}' olduğunda", + "when-dashboard-state-object-function-is-text": "f(dashboard durum nesnesi) '{{state}}' olduğunda", + "set-attribute-to-value-text": "'{{key}}' özniteliğini şu değere ayarla: {{value}}", + "add-time-series-value-text": "'{{key}}' zaman serisi değerini ekle: {{value}}", + "set-attribute-text": "'{{key}}' özniteliğini ayarla", + "add-time-series-text": "'{{key}}' zaman serisini ekle", + "action": "Eylem", + "value": "Değer", + "init-value-hint": "Cihazdan veri gelene kadar atanacak başlangıç değeri.", + "method": "Metot", + "method-name-required": "Metot adı gereklidir.", + "request-timeout-ms": "RPC istek zaman aşımı (ms)", + "request-timeout-required": "İstek zaman aşımı gereklidir.", + "min-request-timeout-error": "Zaman aşımı değeri en az 5000 ms (5 saniye) olmalıdır.", + "request-persistent": "RPC isteği kalıcı", + "persistent-polling-interval": "Kalıcı yoklama aralığı (ms)", + "persistent-polling-interval-hint": "Kalıcı RPC komut yanıtını almak için yoklama aralığı (ms)", + "persistent-polling-interval-required": "Kalıcı yoklama aralığı gereklidir.", + "min-persistent-polling-interval-error": "Kalıcı yoklama aralığı değeri en az 1000 ms (1 saniye) olmalıdır.", + "attribute-scope": "Öznitelik kapsamı", + "attribute-key": "Öznitelik anahtarı", + "attribute-key-required": "Öznitelik anahtarı gereklidir.", + "time-series-key": "Zaman serisi anahtarı", + "time-series-key-required": "Zaman serisi anahtarı gereklidir.", + "action-result-converter": "Eylem sonucu dönüştürücü", + "converter-none": "Yok", + "converter-function": "Fonksiyon", + "converter-constant": "Sabit", + "converter-value": "Değer", + "parse-value-function": "Değer ayrıştırma fonksiyonu", + "state-when-result-is": "Sonuç '{{state}}' olduğunda", + "parameters": "Parametreler", + "convert-value-function": "Değeri dönüştürme fonksiyonu", + "error": { + "target-entity-is-not-set": "Hedef varlık ayarlanmadı!", + "failed-to-perform-action": "{{ actionLabel }} eylemi gerçekleştirilemedi.", + "invalid-attribute-scope": "{{scope}} öznitelik kapsamı {{entityType}} varlığı tarafından desteklenmiyor." + } + }, + "widget-font": { + "font-settings": "Yazı tipi ayarları", + "font-family": "Yazı tipi ailesi", + "size": "Boyut", + "relative-font-size": "Göreli yazı tipi boyutu (yüzde)", + "font-style": "Stil", + "font-style-normal": "Normal", + "font-style-italic": "İtalik", + "font-style-oblique": "Eğik", + "font-weight": "Kalınlık", + "font-weight-normal": "Normal", + "font-weight-bold": "Kalın", + "font-weight-bolder": "Daha kalın", + "font-weight-lighter": "Daha ince", + "color": "Renk", + "shadow-color": "Gölge rengi", + "preview": "Önizleme", + "line-height": "Satır yüksekliği", + "auto": "Otomatik" + }, + "home": { + "no-data-available": "Veri yok" + }, + "system-info": { + "cpu": "CPU", + "ram": "RAM", + "disk": "Disk", + "cpu-warning-text": "CPU kullanımı yüksek. Sistem arızasını önlemek için performansı optimize edin.", + "cpu-critical-text": "CPU kullanımı kritik düzeyde yüksek. Sistem arızasını önlemek için performansı optimize edin.", + "ram-warning-text": "RAM rezervi düşük. Sistem arızasını önlemek için performansı optimize edin veya RAM kapasitesini artırın.", + "ram-critical-text": "RAM rezervi kritik düzeyde düşük. Sistem arızasını önlemek için performansı optimize edin veya RAM kapasitesini artırın.", + "disk-warning-text": "Disk alanı azalıyor. Veri kaybını önlemek için disk alanını boşaltın veya genişletin.", + "disk-critical-text": "Disk alanı kritik düzeyde düşük. Veri kaybını önlemek için disk alanını boşaltın veya genişletin." + }, + "cluster-info": { + "service-id": "Servis kimliği", + "service-type": "Servis türü", + "no-data": "Veri yok" + }, + "transport-messages": { + "title": "Taşıma mesajları", + "info": "Cihazlardan gelen tüm mesajlar" + }, + "activity": { + "title": "Aktivite" + }, + "documentation": { + "title": "Dokümantasyon", + "add-link": "Bağlantı ekle", + "add-link-title": "Dokümantasyon bağlantısı ekle", + "name": "Ad", + "name-required": "Ad gerekli.", + "link": "Bağlantı", + "link-required": "Bağlantı gerekli.", + "columns": "Sütunlar" + }, + "quick-links": { + "title": "Hızlı bağlantılar", + "add-link": "Bağlantı ekle", + "add-link-title": "Hızlı bağlantı ekle", + "quick-link": "Hızlı bağlantı", + "quick-link-required": "Hızlı bağlantı gerekli.", + "no-links-matching": "'{{name}}' ile eşleşen bağlantı bulunamadı.", + "columns": "Sütunlar" + }, + "recent-dashboards": { + "title": "Panolar", + "last": "Son görüntülenen", + "starred": "Yıldızlı", + "name": "Ad", + "last-viewed": "Son görüntüleme", + "no-last-viewed-dashboards": "Henüz son görüntülenen pano yok" + }, + "configured-features": { + "title": "Yapılandırılmış özellikler", + "info": "Yapılandırma gerektiren özelliklerin durumu", + "email-feature": "E-posta", + "sms-feature": "SMS", + "slack-feature": "Slack", + "oauth2-feature": "OAuth 2", + "2fa-feature": "2FA", + "feature-configured": "Özellik yapılandırıldı.\nKurulum için tıklayın", + "feature-not-configured": "Özellik yapılandırılmadı.\nKurulum için tıklayın" + }, + "version-info": { + "title": "Sürüm", + "contact-us": "Bize ulaşın", + "current-version": "Mevcut sürüm", + "current": "Mevcut", + "available-version": "Mevcut sürüm", + "available": "Mevcut", + "upgrade": "Güncelle", + "version-is-up-to-date": "Sürüm güncel" + }, + "usage-info": { + "title": "Kullanım", + "entities": "Varlıklar", + "api-calls": "API çağrıları" + }, + "functions": { + "title": "Fonksiyonlar", + "pe-feature-tooltip": "Yalnızca ThingsBoard\nProfesyonel Sürümde", + "switch-to-pe": "PE sürümüne geç", + "alarms": "Alarmlar", + "dashboards": "Panolar", + "entities-and-relations": "Varlıklar ve İlişkiler", + "profiles": "Profiller", + "advanced-features": "Gelişmiş özellikler", + "notification-center": "Bildirim merkezi", + "api-usage": "API kullanımı", + "customers": "Müşteriler", + "customers-hierarchy": "Müşteri hiyerarşisi", + "roles-and-permissions": "Roller ve İzinler", + "groups": "Gruplar", + "integrations": "Entegrasyonlar", + "solution-templates": "Çözüm şablonları", + "scheduler": "Zamanlayıcı", + "white-labeling": "Beyaz etiketleme" + }, + "devices": { + "view-docs": "Belgeleri görüntüle", + "inactive": "Pasif", + "active": "Aktif", + "total": "Toplam" + }, + "alarms": { + "critical": "Kritik", + "assigned-to-me": "Bana atanan", + "total": "Toplam" + }, + "getting-started": { + "get-started": "Başla", + "finish": "Bitir", + "done-welcome-title": "Aramıza hoş geldiniz", + "done-welcome-text": "Harika bir iş çıkardınız!", + "sys-admin": { + "step1": { + "title": "Kiracı ve Kiracı Yöneticisi oluştur", + "content": "

    Bir kiracı, cihazlara ve varlıklara sahip olan veya bunları üreten birey ya da kuruluştur. Kiracının birden fazla kiracı yöneticisi kullanıcısı, müşterisi, cihazı ve varlığı olabilir.

    Kiracı Yöneticisi, cihazları, varlıkları, müşterileri ve panoları oluşturabilir ve yönetebilir.

    Nasıl yapılacağına dair dökümantasyona göz atın:

    ", + "how-to-create-tenant": "Kiracı ve Kiracı Yöneticisi nasıl oluşturulur" + }, + "step2": { + "title": "Özelliği yapılandır: Mail sunucusu", + "content": "

    Mail sunucusu yapılandırması, kullanıcı aktivasyonu, şifre kurtarma ve alarm bildirimi için gereklidir.

    Nasıl yapılacağına dair dökümantasyona göz atın:

    ", + "how-to-configure-mail-server": "Mail sunucusu nasıl yapılandırılır" + }, + "step3": { + "title": "Özelliği yapılandır: SMS sağlayıcısı", + "content": "

    Alarm bildirimlerini müşterilere SMS ile iletmek için SMS sağlayıcılarını yapılandırın.

    Nasıl yapılacağına dair dökümantasyona göz atın:

    ", + "how-to-configure-sms-provider": "SMS sağlayıcısı nasıl yapılandırılır" + }, + "step4": { + "title": "Özelliği yapılandır: Beyaz etiketleme", + "content": "

    Hizmeti yeniden başlatmadan veya kod yazmadan şirketinizin ya da ürününüzün logosunu ve renk şemasını kolayca özelleştirin.

    Nasıl yapılacağına dair dökümantasyona göz atın:

    " + }, + "step5": { + "title": "Özelliği yapılandır: 2FA", + "content": "

    Platform hesaplarının güvenliğini iki faktörlü kimlik doğrulama ile artırın.

    Nasıl yapılacağına dair dökümantasyona göz atın:

    " + }, + "step6": { + "title": "Özelliği yapılandır: OAuth 2", + "content": "

    OAuth 2.0 üzerinden Tek Oturum Açma (SSO) ile kiracı ve müşteri kullanıcılarının giriş işlemlerini basitleştirin.

    Nasıl yapılacağına dair dökümantasyona göz atın:

    " + } + }, + "tenant-admin": { + "step1": { + "title": "Cihaz oluştur", + "content": "

    İlk cihazınızı platforma UI üzerinden tanımlayalım. Nasıl yapılacağına dair dökümantasyona göz atın:

    ", + "how-to-create-device": "Cihaz nasıl oluşturulur" + }, + "step2": { + "title": "Cihazı bağla", + "content-before": "

    Cihazı bağlamak için cihaz kimlik bilgilerini almanız gerekir. Bu rehberde varsayılan otomatik oluşturulan kimlik bilgisi olan erişim jetonunu kullanmanızı öneriyoruz.

    • Cihaz tablosuna gidin
    • Cihaz satırına tıklayarak detayları açın
    • \"Erişim jetonunu kopyala\" butonuna basın

    HTTP üzerinden veri göndermek için basit komutları kullanın. $ACCESS_TOKEN ifadesini cihazınıza ait erişim jetonu ile değiştirmeyi unutmayın:

    ", + "ubuntu": { + "install-curl": "Ubuntu için cURL kurulumu:" + }, + "macos": { + "install-curl": "MacOS için cURL kurulumu:" + }, + "windows": { + "install-curl": "Windows 10 b17063 itibariyle, cURL varsayılan olarak mevcuttur." + }, + "replace-access-token": "$ACCESS_TOKEN ifadesini cihazınızın erişim jetonu ile değiştirin:", + "content-after": "

    Ayrıca MQTT, CoAP gibi diğer protokolleri de kullanabilirsiniz.

    Nasıl yapılacağına dair dökümantasyona göz atın:

    ", + "how-to-connect-device": "Cihaz nasıl bağlanır" + }, + "step3": { + "title": "Pano oluştur", + "content": "

    Varlıklar, cihazlar vb. gibi nesnelerden gelen verileri görselleştirmek için bir pano oluşturun.

    Nasıl yapılacağına dair dökümantasyona göz atın:

    ", + "how-to-create-dashboard": "Pano nasıl oluşturulur" + }, + "step4": { + "title": "Alarm kurallarını yapılandır", + "alarm-rules": "Alarm kuralları", + "content": "

    Sıcaklık 25°C'ye ulaştığında bir alarm oluşturalım. Nasıl yapılacağına dair dökümantasyona göz atın:

    ", + "how-to-configure-alarm-rules": "Alarm kuralları nasıl yapılandırılır" + }, + "step5": { + "title": "Alarm oluştur", + "content-before": "

    Alarmı tetiklemek için 26°C veya daha yüksek bir telemetri verisi gönderin.

    ", + "replace-access-token": "$ACCESS_TOKEN ifadesini cihazınızın erişim jetonu ile değiştirin:", + "content-after": "

    Nasıl yapılacağına dair dökümantasyona göz atın:

    ", + "how-to-create-alarm": "Alarm nasıl oluşturulur" + }, + "step6": { + "title": "Müşteri oluştur ve panoyu paylaş", + "content": "

    Son kullanıcı panoları oluşturarak, müşteri kullanıcısı yalnızca kendi cihazlarını görebilir ve diğer müşterilerin verileri gizli olur.

    Nasıl yapılacağına dair dökümantasyona göz atın:

    " + } + } + } }, "icon": { - "icon": "İkon", - "select-icon": "İkon seç", - "material-icons": "Material konları", - "show-all": "Tüm ikonları göster" + "icon": "Simge", + "icons": "Simgeler", + "select-icon": "Simge seç", + "material-icons": "Material simgeleri", + "show-all": "Tüm simgeleri göster", + "search-icon": "Simge ara", + "no-icons-found": "'{{iconSearch}}' için simge bulunamadı" + }, + "phone-input": { + "phone-input-label": "Telefon numarası", + "phone-input-required": "Telefon numarası gerekli", + "phone-input-validation": "Telefon numarası geçersiz veya mümkün değil", + "phone-input-pattern": "Geçersiz telefon numarası. E.164 formatında olmalıdır, örn. {{phoneNumber}}", + "phone-input-hint": "E.164 formatında telefon numarası, örn. {{phoneNumber}}" }, "custom": { "widget-action": { - "action-cell-button": "Eylem hücre butonu", - "row-click": "Satır tıklama eylemi", - "polygon-click": "Satır tıklama eylemi", - "marker-click": "Çokgen tıklama eylemi", - "tooltip-tag-action": "İpucu etiket eylemi", - "node-selected": "Düğüm seçme eylemi", - "element-click": "HTML eleman tıklama eylemi", - "pie-slice-click": "Pay/dilim tıklama eylemi", - "row-double-click": "Satır çift tıklama eylemi" + "action-cell-button": "Hücre eylem düğmesi", + "row-click": "Satıra tıklanınca", + "cell-click": "Hücreye tıklanınca", + "polygon-click": "Poligona tıklanınca", + "marker-click": "İşaretçiye tıklanınca", + "circle-click": "Daireye tıklanınca", + "tooltip-tag-action": "İpucu etiketi eylemi", + "node-selected": "Düğüm seçildiğinde", + "element-click": "HTML öğesine tıklanınca", + "pie-slice-click": "Dilime tıklanınca", + "row-double-click": "Satıra çift tıklanınca", + "cell-double-click": "Hücreye çift tıklanınca", + "card-click": "Kart tıklanınca", + "click": "Tıklama" } }, + "paginator": { + "items-per-page": "Sayfa başına öğe:", + "first-page-label": "İlk sayfa", + "last-page-label": "Son sayfa", + "next-page-label": "Sonraki sayfa", + "previous-page-label": "Önceki sayfa", + "items-per-page-separator": "toplam" + }, "language": { "language": "Dil" } diff --git a/ui-ngx/src/scss/mixins.scss b/ui-ngx/src/scss/mixins.scss index 8e60483cb1..fb4a51ebce 100644 --- a/ui-ngx/src/scss/mixins.scss +++ b/ui-ngx/src/scss/mixins.scss @@ -26,6 +26,10 @@ width: #{$size}px; height: #{$size}px; } + img { + width: #{$size}px; + height: #{$size}px; + } } @mixin tb-mat-icon-button-size($size) { diff --git a/ui-ngx/src/styles.scss b/ui-ngx/src/styles.scss index d3cc76b687..62cde47c86 100644 --- a/ui-ngx/src/styles.scss +++ b/ui-ngx/src/styles.scss @@ -692,7 +692,7 @@ pre.tb-highlight { mat-toolbar.mat-mdc-table-toolbar:not(.mat-primary), .mat-mdc-cell, .mat-expansion-panel-header, mat-card-header.mat-mdc-card-header { button.mat-mdc-icon-button { .mat-icon { - color: rgba(0, 0, 0, .54); + color:var(--mat-icon-color, rgba(0, 0, 0, .54)); } &[disabled][disabled] { .mat-icon { @@ -760,11 +760,11 @@ pre.tb-highlight { transition: background-color .2s; &:hover:not(.tb-current-entity) { - background-color: #f4f4f4; + background-color: var(--tb-hover-color, #f4f4f4); } &.tb-current-entity { - background-color: #e9e9e9; + background-color: var(--tb-current-entity-color, #e9e9e9); } &.tb-pointer { @@ -816,7 +816,7 @@ pre.tb-highlight { vertical-align: middle; border-width: 0; border-bottom-width: 1px; - border-bottom-color: rgba(0, 0, 0, 0.12); + border-bottom-color: var(--mat-table-row-item-outline-color,rgba(0, 0, 0, 0.12)); border-style: solid; text-overflow: ellipsis; touch-action: auto !important; @@ -896,12 +896,15 @@ pre.tb-highlight { } .mat-icon { - svg { + svg, img { vertical-align: inherit; } &.tb-mat-12 { @include tb-mat-icon-size(12); } + &.tb-mat-14 { + @include tb-mat-icon-size(14); + } &.tb-mat-16 { @include tb-mat-icon-size(16); } @@ -1255,6 +1258,13 @@ pre.tb-highlight { } } + .tb-chip-row-ellipsis { + overflow: hidden; + .mdc-evolution-chip__cell--primary, .mdc-evolution-chip__action--primary, .mdc-evolution-chip__text-label { + overflow: hidden; + } + } + @media #{$mat-lt-md} { .mat-mdc-form-field { .mat-mdc-form-field-infix {