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 c2400f50e5..dba7d27c1b 100644 --- a/application/pom.xml +++ b/application/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.0-SNAPSHOT + 4.3.0-SNAPSHOT thingsboard application @@ -186,8 +186,8 @@ jjwt - org.freemarker - freemarker + org.springframework.boot + spring-boot-starter-freemarker commons-io @@ -381,6 +381,44 @@ org.rocksdb rocksdbjni + + dev.langchain4j + langchain4j-open-ai + + + dev.langchain4j + langchain4j-azure-open-ai + + + dev.langchain4j + langchain4j-google-ai-gemini + + + dev.langchain4j + langchain4j-vertex-ai-gemini + + + dev.langchain4j + langchain4j-mistral-ai + + + dev.langchain4j + langchain4j-anthropic + + + dev.langchain4j + langchain4j-bedrock + + + dev.langchain4j + langchain4j-github-models + + + com.azure + azure-core-test + + + diff --git a/application/src/main/data/json/edge/instructions/upgrade/docker/upgrade_preparing.md b/application/src/main/data/json/edge/instructions/upgrade/docker/upgrade_preparing.md index a0536f113a..f12eaa5fc0 100644 --- a/application/src/main/data/json/edge/instructions/upgrade/docker/upgrade_preparing.md +++ b/application/src/main/data/json/edge/instructions/upgrade/docker/upgrade_preparing.md @@ -32,19 +32,7 @@ docker run --rm -v tb-edge-postgres-data:/volume -v ~/.mytb-edge-data/db:/backup After completing the data migration to the newly created Docker volumes, you'll need to update the volume mounts in your Docker Compose configuration. Modify the `docker-compose.yml` file for ThingsBoard Edge to update the volume settings. -First, please update docker compose file version. Find next snippet: -```text -version: '3.0' -... -``` - -And replace it with: -```text -version: '3.8' -... -``` - -Then update volume mounts. Locate the following snippet: +Update volume mounts. Locate the following snippet: ```text volumes: - ~/.mytb-edge-data:/data 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/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/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/upgrade/basic/schema_update.sql b/application/src/main/data/upgrade/basic/schema_update.sql index e4bbe4c69e..320d3e5bdd 100644 --- a/application/src/main/data/upgrade/basic/schema_update.sql +++ b/application/src/main/data/upgrade/basic/schema_update.sql @@ -14,48 +14,35 @@ -- limitations under the License. -- --- UPDATE TENANT PROFILE CASSANDRA RATE LIMITS START +-- UPDATE TENANT PROFILE CONFIGURATION START UPDATE tenant_profile SET profile_data = jsonb_set( profile_data, '{configuration}', - ( - (profile_data -> 'configuration') - 'cassandraQueryTenantRateLimitsConfiguration' - || - COALESCE( - CASE - WHEN profile_data -> 'configuration' -> - 'cassandraQueryTenantRateLimitsConfiguration' IS NOT NULL THEN - jsonb_build_object( - 'cassandraReadQueryTenantCoreRateLimits', - profile_data -> 'configuration' -> 'cassandraQueryTenantRateLimitsConfiguration', - 'cassandraWriteQueryTenantCoreRateLimits', - profile_data -> 'configuration' -> 'cassandraQueryTenantRateLimitsConfiguration', - 'cassandraReadQueryTenantRuleEngineRateLimits', - profile_data -> 'configuration' -> 'cassandraQueryTenantRateLimitsConfiguration', - 'cassandraWriteQueryTenantRuleEngineRateLimits', - profile_data -> 'configuration' -> 'cassandraQueryTenantRateLimitsConfiguration' - ) - END, - '{}'::jsonb - ) - ) + (profile_data -> 'configuration') + || jsonb_strip_nulls( + jsonb_build_object( + 'minAllowedScheduledUpdateIntervalInSecForCF', + CASE + WHEN (profile_data -> 'configuration') ? 'minAllowedScheduledUpdateIntervalInSecForCF' + THEN NULL + ELSE to_jsonb(3600) + END, + 'maxRelationLevelPerCfArgument', + CASE + WHEN (profile_data -> 'configuration') ? 'maxRelationLevelPerCfArgument' + THEN NULL + ELSE to_jsonb(10) + END + ) + ), + false ) -WHERE profile_data -> 'configuration' ? 'cassandraQueryTenantRateLimitsConfiguration'; +WHERE NOT ( + (profile_data -> 'configuration') ? 'minAllowedScheduledUpdateIntervalInSecForCF' + AND + (profile_data -> 'configuration') ? 'maxRelationLevelPerCfArgument' + ); --- UPDATE TENANT PROFILE CASSANDRA RATE LIMITS END - --- UPDATE NOTIFICATION RULE CASSANDRA RATE LIMITS START - -UPDATE notification_rule -SET trigger_config = REGEXP_REPLACE( - trigger_config, - '"CASSANDRA_QUERIES"', - '"CASSANDRA_WRITE_QUERIES_CORE","CASSANDRA_READ_QUERIES_CORE","CASSANDRA_WRITE_QUERIES_RULE_ENGINE","CASSANDRA_READ_QUERIES_RULE_ENGINE","CASSANDRA_WRITE_QUERIES_MONOLITH","CASSANDRA_READ_QUERIES_MONOLITH"', - 'g' - ) -WHERE trigger_type = 'RATE_LIMITS' - AND trigger_config LIKE '%"CASSANDRA_QUERIES"%'; - --- UPDATE NOTIFICATION RULE CASSANDRA RATE LIMITS END +-- UPDATE TENANT PROFILE CONFIGURATION 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 0125c7c07d..ea46ce86eb 100644 --- a/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java +++ b/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java @@ -35,6 +35,7 @@ import org.thingsboard.rule.engine.api.JobManager; import org.thingsboard.rule.engine.api.MailService; import org.thingsboard.rule.engine.api.MqttClientSettings; import org.thingsboard.rule.engine.api.NotificationCenter; +import org.thingsboard.rule.engine.api.RuleEngineAiChatModelService; import org.thingsboard.rule.engine.api.SmsService; import org.thingsboard.rule.engine.api.notification.SlackService; import org.thingsboard.rule.engine.api.sms.SmsSenderFactory; @@ -62,6 +63,7 @@ import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.common.msg.tools.TbRateLimits; import org.thingsboard.server.common.stats.TbApiUsageReportClient; +import org.thingsboard.server.dao.ai.AiModelService; import org.thingsboard.server.dao.alarm.AlarmCommentService; import org.thingsboard.server.dao.asset.AssetProfileService; import org.thingsboard.server.dao.asset.AssetService; @@ -311,6 +313,14 @@ public class ActorSystemContext { @Getter private AuditLogService auditLogService; + @Autowired + @Getter + private RuleEngineAiChatModelService aiChatModelService; + + @Autowired + @Getter + private AiModelService aiModelService; + @Autowired @Getter private EntityViewService entityViewService; 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..27bdec2422 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 @@ -114,9 +114,6 @@ public class AppActor extends ContextAwareActor { 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. 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/CalculatedFieldDynamicArgumentsRefreshMsg.java similarity index 74% 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/CalculatedFieldDynamicArgumentsRefreshMsg.java index d142eb78d8..301fe22dfb 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/CalculatedFieldDynamicArgumentsRefreshMsg.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.CalculatedFieldId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.msg.MsgType; import org.thingsboard.server.common.msg.ToCalculatedFieldSystemMsg; @Data -public class CalculatedFieldLinkInitMsg implements ToCalculatedFieldSystemMsg { +public class CalculatedFieldDynamicArgumentsRefreshMsg implements ToCalculatedFieldSystemMsg { private final TenantId tenantId; - private final CalculatedFieldLink link; + private final CalculatedFieldId cfId; @Override public MsgType getMsgType() { - return MsgType.CF_LINK_INIT_MSG; + return MsgType.CF_DYNAMIC_ARGUMENTS_REFRESH_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..2a5f3c3cfd 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 @@ -51,7 +51,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 @@ -75,6 +75,9 @@ public class CalculatedFieldEntityActor extends AbstractCalculatedFieldActor { case CF_LINKED_TELEMETRY_MSG: processor.process((EntityCalculatedFieldLinkedTelemetryMsg) msg); break; + case CF_ENTITY_DYNAMIC_ARGUMENTS_REFRESH_MSG: + processor.process((EntityCalculatedFieldDynamicArgumentsRefreshMsg) msg); + break; default: return false; } 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 ebc4e60709..7513ca41e2 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 @@ -48,6 +48,7 @@ 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 java.util.ArrayList; import java.util.Collection; @@ -68,7 +69,7 @@ import java.util.stream.Collectors; */ @Slf4j public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareMsgProcessor { - // (1 for result persistence + 1 for the state persistence ) + // (1 for result persistence + 1 for the state persistence) public static final int CALLBACKS_PER_CF = 2; final TenantId tenantId; @@ -91,16 +92,18 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM this.ctx = 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.clear(); ctx.stop(ctx.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); } } @@ -224,6 +227,18 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM } } + public void process(EntityCalculatedFieldDynamicArgumentsRefreshMsg msg) throws CalculatedFieldException { + log.debug("[{}][{}] Processing CF dynamic arguments refresh msg.", entityId, msg.getCfId()); + CalculatedFieldState currentState = states.get(msg.getCfId()); + if (currentState == null) { + log.debug("[{}][{}] Failed to find CF state for entity.", entityId, msg.getCfId()); + } else { + currentState.setDirty(true); + log.debug("[{}][{}] CF state marked as dirty.", entityId, msg.getCfId()); + } + msg.getCallback().onSuccess(); + } + private void processTelemetry(CalculatedFieldCtx ctx, CalculatedFieldTelemetryMsgProto proto, List cfIdList, MultipleTbCallback callback) throws CalculatedFieldException { processArgumentValuesUpdate(ctx, cfIdList, callback, mapToArguments(ctx, proto.getTsDataList()), toTbMsgId(proto), toTbMsgType(proto)); } @@ -251,6 +266,15 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM if (state == null) { state = getOrInitState(ctx); justRestored = true; + } else if (state.isDirty()) { + log.debug("[{}][{}] Going to update dirty CF state.", entityId, ctx.getCfId()); + try { + Map dynamicArgsFromDb = cfService.fetchDynamicArgsFromDb(ctx, entityId); + dynamicArgsFromDb.forEach(newArgValues::putIfAbsent); + state.setDirty(false); + } catch (Exception e) { + throw CalculatedFieldException.builder().ctx(ctx).eventEntity(entityId).cause(e).build(); + } } if (state.isSizeOk()) { if (state.updateState(ctx, newArgValues) || justRestored) { @@ -271,7 +295,7 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM if (state != null) { return state; } else { - ListenableFuture stateFuture = systemContext.getCalculatedFieldProcessingService().fetchStateFromDb(ctx, entityId); + ListenableFuture stateFuture = cfService.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, @@ -288,7 +312,7 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM boolean stateSizeChecked = false; try { if (ctx.isInitialized() && state.isReady()) { - CalculatedFieldResult calculationResult = state.performCalculation(ctx).get(systemContext.getCfCalculationResultTimeout(), TimeUnit.SECONDS); + CalculatedFieldResult calculationResult = state.performCalculation(entityId, ctx).get(systemContext.getCfCalculationResultTimeout(), TimeUnit.SECONDS); state.checkStateSize(ctxId, ctx.getMaxStateSize()); stateSizeChecked = true; if (state.isSizeOk()) { @@ -298,7 +322,7 @@ 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.toStringOrElseNull(), null); } } } else { @@ -367,7 +391,7 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM } private Map mapToArguments(CalculatedFieldCtx ctx, AttributeScopeProto scope, List attrDataList) { - return mapToArguments(ctx.getMainEntityArguments(), scope, attrDataList); + return mapToArguments(entityId, ctx.getMainEntityArguments(), ctx.getMainEntityGeofencingArgumentNames(), scope, attrDataList); } private Map mapToArguments(CalculatedFieldCtx ctx, EntityId entityId, AttributeScopeProto scope, List attrDataList) { @@ -375,17 +399,23 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM if (argNames.isEmpty()) { return Collections.emptyMap(); } - return mapToArguments(argNames, scope, attrDataList); + List geofencingArgumentNames = ctx.getLinkedEntityGeofencingArgumentNames(); + return mapToArguments(entityId, argNames, geofencingArgumentNames, scope, attrDataList); } - private Map mapToArguments(Map argNames, AttributeScopeProto scope, List attrDataList) { + private Map mapToArguments(EntityId entityId, Map argNames, List geoArgNames, AttributeScopeProto scope, List attrDataList) { 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)); + if (argName == null) { + continue; + } + if (geoArgNames.contains(argName)) { + arguments.put(argName, new GeofencingArgumentEntry(entityId, item)); + continue; } + arguments.put(argName, new SingleValueArgumentEntry(item)); } return arguments; } 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..ab6cb34176 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 @@ -24,9 +24,6 @@ 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,15 +67,6 @@ 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; @@ -91,6 +79,9 @@ public class CalculatedFieldManagerActor extends AbstractCalculatedFieldActor { case CF_LINKED_TELEMETRY_MSG: processor.onLinkedTelemetryMsg((CalculatedFieldLinkedTelemetryMsg) msg); break; + case CF_DYNAMIC_ARGUMENTS_REFRESH_MSG: + processor.onDynamicArgumentsRefreshMsg((CalculatedFieldDynamicArgumentsRefreshMsg) msg); + break; default: return false; } 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..75ca0b4c9b 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 @@ -27,6 +27,7 @@ import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.ProfileEntityIdInfo; import org.thingsboard.server.common.data.cf.CalculatedField; import org.thingsboard.server.common.data.cf.CalculatedFieldLink; +import org.thingsboard.server.common.data.cf.configuration.ScheduledUpdateSupportedCalculatedFieldConfiguration; import org.thingsboard.server.common.data.id.AssetId; import org.thingsboard.server.common.data.id.CalculatedFieldId; import org.thingsboard.server.common.data.id.DeviceId; @@ -35,9 +36,6 @@ import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageDataIterable; 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; @@ -59,7 +57,11 @@ import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import java.util.function.BiConsumer; import static org.thingsboard.server.utils.CalculatedFieldUtils.fromProto; @@ -72,6 +74,7 @@ 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> cfDynamicArgumentsRefreshTasks = new ConcurrentHashMap<>(); private final CalculatedFieldProcessingService cfExecService; private final CalculatedFieldStateService cfStateService; @@ -110,6 +113,8 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware calculatedFields.clear(); entityIdCalculatedFields.clear(); entityIdCalculatedFieldLinks.clear(); + cfDynamicArgumentsRefreshTasks.values().forEach(future -> future.cancel(true)); + cfDynamicArgumentsRefreshTasks.clear(); ctx.stop(ctx.getSelf()); } @@ -120,37 +125,6 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware 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); - msg.getCallback().onSuccess(); - } - public void onStateRestoreMsg(CalculatedFieldStateRestoreMsg msg) { var cfId = msg.getId().cfId(); var calculatedField = calculatedFields.get(cfId); @@ -289,7 +263,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 +274,16 @@ 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); + scheduleDynamicArgumentsRefreshTaskForCfIfNeeded(cfCtx); + applyToTargetCfEntityActors(cfCtx, callback, (id, cb) -> initCfForEntity(id, cfCtx, false, cb)); } } } + private CalculatedFieldCtx getCfCtx(CalculatedField cf) { + return new CalculatedFieldCtx(cf, systemContext.getTbelInvokeService(), systemContext.getApiLimitService(), systemContext.getRelationService()); + } + private void onCfUpdated(ComponentLifecycleMsg msg, TbCallback callback) throws CalculatedFieldException { var cfId = new CalculatedFieldId(msg.getEntityId().getId()); var oldCfCtx = calculatedFields.get(cfId); @@ -316,7 +295,7 @@ 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) { @@ -324,6 +303,13 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware } calculatedFields.put(newCf.getId(), newCfCtx); List oldCfList = entityIdCalculatedFields.get(newCf.getEntityId()); + + boolean hasSchedulingConfigChanges = newCfCtx.hasSchedulingConfigChanges(oldCfCtx); + if (hasSchedulingConfigChanges) { + cancelCfDynamicArgumentsRefreshTaskIfExists(cfId, false); + scheduleDynamicArgumentsRefreshTaskForCfIfNeeded(newCfCtx); + } + List newCfList = new CopyOnWriteArrayList<>(); boolean found = false; for (CalculatedFieldCtx oldCtx : oldCfList) { @@ -346,7 +332,7 @@ 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) var stateChanges = newCfCtx.hasStateChanges(oldCfCtx); if (stateChanges || newCfCtx.hasOtherSignificantChanges(oldCfCtx)) { - initCf(newCfCtx, callback, stateChanges); + applyToTargetCfEntityActors(newCfCtx, callback, (id, cb) -> initCfForEntity(id, newCfCtx, stateChanges, cb)); } else { callback.onSuccess(); } @@ -360,30 +346,20 @@ 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); + cancelCfDynamicArgumentsRefreshTaskIfExists(cfId, true); + applyToTargetCfEntityActors(cfCtx, callback, (id, cb) -> deleteCfForEntity(id, cfId, cb)); + } + + private void cancelCfDynamicArgumentsRefreshTaskIfExists(CalculatedFieldId cfId, boolean cfDeleted) { + var existingTask = cfDynamicArgumentsRefreshTasks.remove(cfId); + if (existingTask != null) { + existingTask.cancel(false); + String reason = cfDeleted ? "deletion" : "update"; + log.debug("[{}][{}] Cancelled dynamic arguments refresh task due to CF {}!", tenantId, cfId, reason); } } @@ -423,31 +399,11 @@ 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); - } - }); - } 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); - } - } + withTargetEntities(link.entityId(), callback, (ids, cb) -> { + var linkedTelemetryMsg = new EntityCalculatedFieldLinkedTelemetryMsg(tenantId, sourceEntityId, proto.getMsg(), cf, cb); + ids.forEach(id -> linkedTelemetryMsgForEntity(id, linkedTelemetryMsg)); + }); } } @@ -486,26 +442,46 @@ 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 void scheduleDynamicArgumentsRefreshTaskForCfIfNeeded(CalculatedFieldCtx cfCtx) { + CalculatedField cf = cfCtx.getCalculatedField(); + if (!(cf.getConfiguration() instanceof ScheduledUpdateSupportedCalculatedFieldConfiguration scheduledCfConfig)) { + return; + } + if (!scheduledCfConfig.isScheduledUpdateEnabled()) { + return; + } + if (cfDynamicArgumentsRefreshTasks.containsKey(cf.getId())) { + log.debug("[{}][{}] Dynamic arguments refresh task for CF already exists!", tenantId, cf.getId()); + return; } + long refreshDynamicSourceInterval = TimeUnit.SECONDS.toMillis(scheduledCfConfig.getScheduledUpdateInterval()); + var scheduledMsg = new CalculatedFieldDynamicArgumentsRefreshMsg(tenantId, cfCtx.getCfId()); + + ScheduledFuture scheduledFuture = systemContext + .schedulePeriodicMsgWithDelay(ctx, scheduledMsg, refreshDynamicSourceInterval, refreshDynamicSourceInterval); + cfDynamicArgumentsRefreshTasks.put(cf.getId(), scheduledFuture); + log.debug("[{}][{}] Scheduled dynamic arguments refresh task for CF!", tenantId, cf.getId()); + } + + public void onDynamicArgumentsRefreshMsg(CalculatedFieldDynamicArgumentsRefreshMsg msg) { + log.debug("[{}] [{}] Processing CF dynamic arguments refresh task.", tenantId, msg.getCfId()); + CalculatedFieldCtx cfCtx = calculatedFields.get(msg.getCfId()); + if (cfCtx == null) { + log.debug("[{}][{}] Failed to find CF context, going to stop dynamic arguments refresh task for CF.", tenantId, msg.getCfId()); + cancelCfDynamicArgumentsRefreshTaskIfExists(msg.getCfId(), true); + return; + } + applyToTargetCfEntityActors(cfCtx, msg.getCallback(), (id, cb) -> refreshDynamicArgumentsForEntity(id, msg.getCfId(), cb)); + } + + private void refreshDynamicArgumentsForEntity(EntityId entityId, CalculatedFieldId cfId, TbCallback callback) { + log.debug("Pushing CF dynamic arguments refresh msg to specific actor [{}]", entityId); + getOrCreateActor(entityId).tell(new EntityCalculatedFieldDynamicArgumentsRefreshMsg(tenantId, cfId, callback)); + } + + private void linkedTelemetryMsgForEntity(EntityId entityId, EntityCalculatedFieldLinkedTelemetryMsg msg) { + log.debug("Pushing linked telemetry msg to specific actor [{}]", entityId); + getOrCreateActor(entityId).tell(msg); } private void deleteCfForEntity(EntityId entityId, CalculatedFieldId cfId, TbCallback callback) { @@ -566,20 +542,38 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware cfs.forEach(cf -> { log.trace("Processing calculated field record: {}", cf); try { - onFieldInitMsg(new CalculatedFieldInitMsg(cf.getTenantId(), cf)); + initCalculatedField(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)); + log.trace("Processing calculated field link record: {}", link); + initCalculatedFieldLink(link); }); } + private void initCalculatedField(CalculatedField cf) throws CalculatedFieldException { + var cfCtx = new CalculatedFieldCtx(cf, systemContext.getTbelInvokeService(), systemContext.getApiLimitService(), systemContext.getRelationService()); + 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); + scheduleDynamicArgumentsRefreshTaskForCfIfNeeded(cfCtx); + } + + private void initCalculatedFieldLink(CalculatedFieldLink link) { + // 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); + } + private void initEntityProfileCache() { PageDataIterable deviceIdInfos = new PageDataIterable<>(pageLink -> deviceService.findProfileEntityIdInfosByTenantId(tenantId, pageLink), cfSettings.getInitTenantFetchPackSize()); for (ProfileEntityIdInfo idInfo : deviceIdInfos) { @@ -601,4 +595,30 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware } } + 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/CalculatedFieldInitProfileEntityMsg.java b/application/src/main/java/org/thingsboard/server/actors/calculatedField/EntityCalculatedFieldDynamicArgumentsRefreshMsg.java similarity index 68% 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/EntityCalculatedFieldDynamicArgumentsRefreshMsg.java index 66cd2ac441..fdf864611f 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/EntityCalculatedFieldDynamicArgumentsRefreshMsg.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.CalculatedFieldId; 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 CalculatedFieldInitProfileEntityMsg implements ToCalculatedFieldSystemMsg { +public class EntityCalculatedFieldDynamicArgumentsRefreshMsg implements ToCalculatedFieldSystemMsg { private final TenantId tenantId; - private final EntityId profileEntityId; - private final EntityId entityId; + private final CalculatedFieldId cfId; + private final TbCallback callback; @Override public MsgType getMsgType() { - return MsgType.CF_INIT_PROFILE_ENTITY_MSG; + return MsgType.CF_ENTITY_DYNAMIC_ARGUMENTS_REFRESH_MSG; } } 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 f5033cce80..c143214e2b 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 @@ -270,10 +270,19 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso rpc.setExpirationTime(request.getExpirationTime()); rpc.setRequest(JacksonUtil.valueToTree(request)); rpc.setStatus(status); - rpc.setAdditionalInfo(JacksonUtil.toJsonNode(request.getAdditionalInfo())); + rpc.setAdditionalInfo(getAdditionalInfo(request)); systemContext.getTbRpcService().save(tenantId, rpc); } + private JsonNode getAdditionalInfo(ToDeviceRpcRequest request) { + try { + return JacksonUtil.toJsonNode(request.getAdditionalInfo()); + } catch (IllegalArgumentException e) { + log.debug("Failed to parse additional info [{}]", request.getAdditionalInfo()); + return JacksonUtil.valueToTree(request.getAdditionalInfo()); + } + } + private ToDeviceRpcRequestMsg createToDeviceRpcRequestMsg(ToDeviceRpcRequest request) { ToDeviceRpcRequestBody body = request.getBody(); return ToDeviceRpcRequestMsg.newBuilder() 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 dff0cd4cf1..6374e4016d 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 @@ -28,6 +28,7 @@ import org.thingsboard.rule.engine.api.JobManager; import org.thingsboard.rule.engine.api.MailService; import org.thingsboard.rule.engine.api.MqttClientSettings; import org.thingsboard.rule.engine.api.NotificationCenter; +import org.thingsboard.rule.engine.api.RuleEngineAiChatModelService; import org.thingsboard.rule.engine.api.RuleEngineAlarmService; import org.thingsboard.rule.engine.api.RuleEngineApiUsageStateService; import org.thingsboard.rule.engine.api.RuleEngineAssetProfileCache; @@ -76,6 +77,7 @@ import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.common.msg.TbMsgProcessingStackItem; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; +import org.thingsboard.server.dao.ai.AiModelService; import org.thingsboard.server.dao.alarm.AlarmCommentService; import org.thingsboard.server.dao.asset.AssetProfileService; import org.thingsboard.server.dao.asset.AssetService; @@ -1024,6 +1026,16 @@ public class DefaultTbContext implements TbContext { return mainCtx.getAuditLogService(); } + @Override + public RuleEngineAiChatModelService getAiChatModelService() { + return mainCtx.getAiChatModelService(); + } + + @Override + public AiModelService getAiModelService() { + return mainCtx.getAiModelService(); + } + @Override public MqttClientSettings getMqttClientSettings() { return mainCtx.getMqttClientSettings(); 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..91d25a633b 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 @@ -180,9 +180,6 @@ 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); 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 93cf9da8e0..ca741ea8c6 100644 --- a/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java +++ b/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java @@ -26,13 +26,12 @@ import org.springframework.context.annotation.Configuration; import org.springframework.core.annotation.Order; import org.springframework.http.HttpHeaders; import org.springframework.security.authentication.AuthenticationManager; -import org.springframework.security.authentication.DefaultAuthenticationEventPublisher; -import org.springframework.security.config.annotation.ObjectPostProcessor; -import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +import org.springframework.security.authentication.ProviderManager; import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; 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; @@ -40,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; @@ -183,15 +183,12 @@ public class ThingsboardSecurityConfiguration { } @Bean - public AuthenticationManager authenticationManager(ObjectPostProcessor objectPostProcessor) throws Exception { - DefaultAuthenticationEventPublisher eventPublisher = objectPostProcessor - .postProcess(new DefaultAuthenticationEventPublisher()); - var auth = new AuthenticationManagerBuilder(objectPostProcessor); - auth.authenticationEventPublisher(eventPublisher); - auth.authenticationProvider(restAuthenticationProvider); - auth.authenticationProvider(jwtAuthenticationProvider); - auth.authenticationProvider(refreshTokenAuthenticationProvider); - return auth.build(); + public AuthenticationManager authenticationManager() { + return new ProviderManager(List.of( + restAuthenticationProvider, + jwtAuthenticationProvider, + refreshTokenAuthenticationProvider + )); } @Autowired @@ -215,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 -> {}) @@ -265,4 +261,5 @@ public class ThingsboardSecurityConfiguration { return new CorsFilter(source); } } + } diff --git a/application/src/main/java/org/thingsboard/server/controller/AdminController.java b/application/src/main/java/org/thingsboard/server/controller/AdminController.java index 6b3132a1d2..371ca4d3e4 100644 --- a/application/src/main/java/org/thingsboard/server/controller/AdminController.java +++ b/application/src/main/java/org/thingsboard/server/controller/AdminController.java @@ -37,14 +37,13 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpStatus; import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.context.request.async.DeferredResult; @@ -125,14 +124,13 @@ public class AdminController extends BaseController { @ApiOperation(value = "Get the Administration Settings object using key (getAdminSettings)", notes = "Get the Administration Settings object using specified string key. Referencing non-existing key will cause an error." + SYSTEM_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('SYS_ADMIN')") - @RequestMapping(value = "/settings/{key}", method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/settings/{key}") public AdminSettings getAdminSettings( @Parameter(description = "A string value of the key (e.g. 'general' or 'mail').") @PathVariable("key") String key) throws ThingsboardException { accessControlService.checkPermission(getCurrentUser(), Resource.ADMIN_SETTINGS, Operation.READ); AdminSettings adminSettings = checkNotNull(adminSettingsService.findAdminSettingsByKey(TenantId.SYS_TENANT_ID, key), "No Administration settings found for key: " + key); - if (adminSettings.getKey().equals("mail")) { + if (adminSettings.getKey().equals(MAIL_SETTINGS_KEY)) { ((ObjectNode) adminSettings.getJsonValue()).remove("password"); ((ObjectNode) adminSettings.getJsonValue()).remove("refreshToken"); } @@ -144,15 +142,14 @@ public class AdminController extends BaseController { "The Administration Settings Id will be present in the response. Specify the Administration Settings Id when you would like to update the Administration Settings. " + "Referencing non-existing Administration Settings Id will cause an error." + SYSTEM_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('SYS_ADMIN')") - @RequestMapping(value = "/settings", method = RequestMethod.POST) - @ResponseBody + @PostMapping(value = "/settings") public AdminSettings saveAdminSettings( @Parameter(description = "A JSON value representing the Administration Settings.") @RequestBody AdminSettings adminSettings) throws ThingsboardException { accessControlService.checkPermission(getCurrentUser(), Resource.ADMIN_SETTINGS, Operation.WRITE); adminSettings.setTenantId(getTenantId()); adminSettings = checkNotNull(adminSettingsService.saveAdminSettings(TenantId.SYS_TENANT_ID, adminSettings)); - if (adminSettings.getKey().equals("mail")) { + if (adminSettings.getKey().equals(MAIL_SETTINGS_KEY)) { mailService.updateMailConfiguration(); ((ObjectNode) adminSettings.getJsonValue()).remove("password"); ((ObjectNode) adminSettings.getJsonValue()).remove("refreshToken"); @@ -165,8 +162,7 @@ public class AdminController extends BaseController { @ApiOperation(value = "Get the Security Settings object (getSecuritySettings)", notes = "Get the Security Settings object that contains password policy, etc." + SYSTEM_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('SYS_ADMIN')") - @RequestMapping(value = "/securitySettings", method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/securitySettings") public SecuritySettings getSecuritySettings() throws ThingsboardException { accessControlService.checkPermission(getCurrentUser(), Resource.ADMIN_SETTINGS, Operation.READ); return checkNotNull(securitySettingsService.getSecuritySettings()); @@ -175,8 +171,7 @@ public class AdminController extends BaseController { @ApiOperation(value = "Update Security Settings (saveSecuritySettings)", notes = "Updates the Security Settings object that contains password policy, etc." + SYSTEM_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('SYS_ADMIN')") - @RequestMapping(value = "/securitySettings", method = RequestMethod.POST) - @ResponseBody + @PostMapping(value = "/securitySettings") public SecuritySettings saveSecuritySettings( @Parameter(description = "A JSON value representing the Security Settings.") @RequestBody SecuritySettings securitySettings) throws ThingsboardException { @@ -188,8 +183,7 @@ public class AdminController extends BaseController { @ApiOperation(value = "Get the JWT Settings object (getJwtSettings)", notes = "Get the JWT Settings object that contains JWT token policy, etc. " + SYSTEM_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('SYS_ADMIN')") - @RequestMapping(value = "/jwtSettings", method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/jwtSettings") public JwtSettings getJwtSettings() throws ThingsboardException { accessControlService.checkPermission(getCurrentUser(), Resource.ADMIN_SETTINGS, Operation.READ); return checkNotNull(jwtSettingsService.getJwtSettings()); @@ -198,8 +192,7 @@ public class AdminController extends BaseController { @ApiOperation(value = "Update JWT Settings (saveJwtSettings)", notes = "Updates the JWT Settings object that contains JWT token policy, etc. The tokenSigningKey field is a Base64 encoded string." + SYSTEM_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('SYS_ADMIN')") - @RequestMapping(value = "/jwtSettings", method = RequestMethod.POST) - @ResponseBody + @PostMapping(value = "/jwtSettings") public JwtPair saveJwtSettings( @Parameter(description = "A JSON value representing the JWT Settings.") @RequestBody JwtSettings jwtSettings) throws ThingsboardException { @@ -213,15 +206,15 @@ public class AdminController extends BaseController { notes = "Attempts to send test email to the System Administrator User using Mail Settings provided as a parameter. " + "You may change the 'To' email in the user profile of the System Administrator. " + SYSTEM_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('SYS_ADMIN')") - @RequestMapping(value = "/settings/testMail", method = RequestMethod.POST) + @PostMapping(value = "/settings/testMail") public void sendTestMail( @Parameter(description = "A JSON value representing the Mail Settings.") @RequestBody AdminSettings adminSettings) throws ThingsboardException { accessControlService.checkPermission(getCurrentUser(), Resource.ADMIN_SETTINGS, Operation.READ); adminSettings = checkNotNull(adminSettings); - if (adminSettings.getKey().equals("mail")) { + if (adminSettings.getKey().equals(MAIL_SETTINGS_KEY)) { if (adminSettings.getJsonValue().has("enableOauth2") && adminSettings.getJsonValue().get("enableOauth2").asBoolean()) { - AdminSettings mailSettings = checkNotNull(adminSettingsService.findAdminSettingsByKey(TenantId.SYS_TENANT_ID, "mail")); + AdminSettings mailSettings = checkNotNull(adminSettingsService.findAdminSettingsByKey(TenantId.SYS_TENANT_ID, MAIL_SETTINGS_KEY)); JsonNode refreshToken = mailSettings.getJsonValue().get("refreshToken"); if (refreshToken == null) { throw new ThingsboardException("Refresh token was not generated. Please, generate refresh token.", ThingsboardErrorCode.GENERAL); @@ -230,7 +223,7 @@ public class AdminController extends BaseController { settings.put("refreshToken", refreshToken.asText()); } else { if (!adminSettings.getJsonValue().has("password")) { - AdminSettings mailSettings = checkNotNull(adminSettingsService.findAdminSettingsByKey(TenantId.SYS_TENANT_ID, "mail")); + AdminSettings mailSettings = checkNotNull(adminSettingsService.findAdminSettingsByKey(TenantId.SYS_TENANT_ID, MAIL_SETTINGS_KEY)); ((ObjectNode) adminSettings.getJsonValue()).put("password", mailSettings.getJsonValue().get("password").asText()); } } @@ -251,7 +244,7 @@ public class AdminController extends BaseController { notes = "Attempts to send test sms to the System Administrator User using SMS Settings and phone number provided as a parameters of the request. " + SYSTEM_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('SYS_ADMIN')") - @RequestMapping(value = "/settings/testSms", method = RequestMethod.POST) + @PostMapping(value = "/settings/testSms") public void sendTestSms( @Parameter(description = "A JSON value representing the Test SMS request.") @RequestBody TestSmsRequest testSmsRequest) throws ThingsboardException { @@ -325,7 +318,7 @@ public class AdminController extends BaseController { notes = "Deletes the repository settings." + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('TENANT_ADMIN')") - @RequestMapping(value = "/repositorySettings", method = RequestMethod.DELETE) + @DeleteMapping(value = "/repositorySettings") @ResponseStatus(value = HttpStatus.OK) public DeferredResult deleteRepositorySettings() throws Exception { accessControlService.checkPermission(getCurrentUser(), Resource.VERSION_CONTROL, Operation.DELETE); @@ -335,7 +328,7 @@ public class AdminController extends BaseController { @ApiOperation(value = "Check repository access (checkRepositoryAccess)", notes = "Attempts to check repository access. " + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('TENANT_ADMIN')") - @RequestMapping(value = "/repositorySettings/checkAccess", method = RequestMethod.POST) + @PostMapping(value = "/repositorySettings/checkAccess") public DeferredResult checkRepositoryAccess( @Parameter(description = "A JSON value representing the Repository Settings.") @RequestBody RepositorySettings settings) throws Exception { @@ -376,7 +369,7 @@ public class AdminController extends BaseController { notes = "Deletes the auto commit settings." + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('TENANT_ADMIN')") - @RequestMapping(value = "/autoCommitSettings", method = RequestMethod.DELETE) + @DeleteMapping(value = "/autoCommitSettings") @ResponseStatus(value = HttpStatus.OK) public void deleteAutoCommitSettings() throws ThingsboardException { accessControlService.checkPermission(getCurrentUser(), Resource.VERSION_CONTROL, Operation.DELETE); @@ -387,9 +380,8 @@ public class AdminController extends BaseController { notes = "Check notifications about new platform releases. " + SYSTEM_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('SYS_ADMIN')") - @RequestMapping(value = "/updates", method = RequestMethod.GET) - @ResponseBody - public UpdateMessage checkUpdates() throws ThingsboardException { + @GetMapping(value = "/updates") + public UpdateMessage checkUpdates() { return updateService.checkUpdates(); } @@ -397,9 +389,8 @@ public class AdminController extends BaseController { notes = "Get main information about system. " + SYSTEM_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('SYS_ADMIN')") - @RequestMapping(value = "/systemInfo", method = RequestMethod.GET) - @ResponseBody - public SystemInfo getSystemInfo() throws ThingsboardException { + @GetMapping(value = "/systemInfo") + public SystemInfo getSystemInfo() { return systemInfoService.getSystemInfo(); } @@ -407,8 +398,7 @@ public class AdminController extends BaseController { notes = "Get information about enabled/disabled features. " + SYSTEM_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('SYS_ADMIN')") - @RequestMapping(value = "/featuresInfo", method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/featuresInfo") public FeaturesInfo getFeaturesInfo() { return systemInfoService.getFeaturesInfo(); } @@ -417,8 +407,7 @@ public class AdminController extends BaseController { "double quotes. After successful authentication with OAuth2 provider and user consent for requested scope, it makes a redirect to this path so that the platform can do " + "further log in processing and generating access tokens. " + SYSTEM_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('SYS_ADMIN')") - @RequestMapping(value = "/mail/oauth2/loginProcessingUrl", method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/mail/oauth2/loginProcessingUrl") public String getMailProcessingUrl() throws ThingsboardException { accessControlService.checkPermission(getCurrentUser(), Resource.ADMIN_SETTINGS, Operation.READ); return "\"/api/admin/mail/oauth2/code\""; @@ -427,7 +416,7 @@ public class AdminController extends BaseController { @ApiOperation(value = "Redirect user to mail provider login page. ", notes = "After user logged in and provided access" + "provider sends authorization code to specified redirect uri.)") @PreAuthorize("hasAuthority('SYS_ADMIN')") - @RequestMapping(value = "/mail/oauth2/authorize", method = RequestMethod.GET, produces = "application/text") + @GetMapping(value = "/mail/oauth2/authorize", produces = "application/text") public String getAuthorizationUrl(HttpServletRequest request, HttpServletResponse response) throws ThingsboardException { String state = StringUtils.generateSafeToken(); if (request.getParameter(PREV_URI_PATH_PARAMETER) != null) { @@ -452,7 +441,7 @@ public class AdminController extends BaseController { .build() + "\""; } - @RequestMapping(value = "/mail/oauth2/code", params = {"code", "state"}, method = RequestMethod.GET) + @GetMapping(value = "/mail/oauth2/code", params = {"code", "state"}) public void codeProcessingUrl( @RequestParam(value = "code") String code, @RequestParam(value = "state") String state, HttpServletRequest request, HttpServletResponse response) throws ThingsboardException, IOException { diff --git a/application/src/main/java/org/thingsboard/server/controller/AiModelController.java b/application/src/main/java/org/thingsboard/server/controller/AiModelController.java new file mode 100644 index 0000000000..34a0bbebfc --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/controller/AiModelController.java @@ -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. + */ +package org.thingsboard.server.controller; + +import com.google.common.util.concurrent.ListenableFuture; +import dev.langchain4j.model.chat.request.ChatRequest; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.context.request.async.DeferredResult; +import org.thingsboard.server.common.data.ai.AiModel; +import org.thingsboard.server.common.data.ai.dto.TbChatRequest; +import org.thingsboard.server.common.data.ai.dto.TbChatResponse; +import org.thingsboard.server.common.data.ai.model.chat.AiChatModelConfig; +import org.thingsboard.server.common.data.exception.ThingsboardException; +import org.thingsboard.server.common.data.id.AiModelId; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.config.annotations.ApiOperation; +import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.ai.AiChatModelService; +import org.thingsboard.server.service.security.permission.Operation; +import org.thingsboard.server.service.security.permission.Resource; + +import java.time.Duration; +import java.util.Optional; +import java.util.UUID; + +import static com.google.common.util.concurrent.MoreExecutors.directExecutor; +import static org.thingsboard.server.controller.ControllerConstants.AI_MODEL_TEXT_SEARCH_DESCRIPTION; +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; +import static org.thingsboard.server.controller.ControllerConstants.SORT_ORDER_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.SORT_PROPERTY_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.TENANT_AUTHORITY_PARAGRAPH; + +@Validated +@RestController +@TbCoreComponent +@RequiredArgsConstructor +@RequestMapping("/api/ai/model") +class AiModelController extends BaseController { + + private final AiChatModelService aiChatModelService; + + @ApiOperation( + value = "Create or update AI model (saveAiModel)", + notes = "Creates or updates an AI model record.\n\n" + + "• **Create:** Omit the `id` to create a new record. The platform assigns a UUID to the new record and returns it in the `id` field of the response.\n\n" + + "• **Update:** Include an existing `id` to modify that record. If no matching record exists, the API responds with **404 Not Found**.\n\n" + + "Tenant ID for the AI model will be taken from the authenticated user making the request, regardless of any value provided in the request body." + + TENANT_AUTHORITY_PARAGRAPH + ) + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @PostMapping + public AiModel saveAiModel(@RequestBody @Valid AiModel model) throws ThingsboardException { + var user = getCurrentUser(); + model.setTenantId(user.getTenantId()); + checkEntity(model.getId(), model, Resource.AI_MODEL); + return tbAiModelService.save(model, user); + } + + @ApiOperation( + value = "Get AI model by ID (getAiModelById)", + notes = "Fetches an AI model record by its `id`." + + TENANT_AUTHORITY_PARAGRAPH + ) + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @GetMapping("/{modelUuid}") + public AiModel getAiModelById( + @Parameter( + description = "ID of the AI model record", + required = true, + example = "de7900d4-30e2-11f0-9cd2-0242ac120002" + ) + @PathVariable UUID modelUuid + ) throws ThingsboardException { + return checkAiModelId(new AiModelId(modelUuid), Operation.READ); + } + + @ApiOperation( + value = "Get AI models (getAiModels)", + notes = "Returns a page of AI models. " + + PAGE_DATA_PARAMETERS + TENANT_AUTHORITY_PARAGRAPH + ) + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @GetMapping + public PageData getAiModels( + @Parameter(description = PAGE_SIZE_DESCRIPTION, required = true) + @RequestParam int pageSize, + @Parameter(description = PAGE_NUMBER_DESCRIPTION, required = true) + @RequestParam int page, + @Parameter(description = AI_MODEL_TEXT_SEARCH_DESCRIPTION) + @RequestParam(required = false) String textSearch, + @Parameter(description = SORT_PROPERTY_DESCRIPTION, schema = @Schema(allowableValues = {"createdTime", "name", "provider", "modelId"})) + @RequestParam(required = false) String sortProperty, + @Parameter(description = SORT_ORDER_DESCRIPTION, schema = @Schema(allowableValues = {"ASC", "DESC"})) + @RequestParam(required = false) String sortOrder + ) throws ThingsboardException { + var user = getCurrentUser(); + accessControlService.checkPermission(user, Resource.AI_MODEL, Operation.READ); + var pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); + return aiModelService.findAiModelsByTenantId(user.getTenantId(), pageLink); + } + + @ApiOperation( + value = "Delete AI model by ID (deleteAiModelById)", + notes = "Deletes the AI model record by its `id`. " + + "If a record with the specified `id` exists, the record is deleted and the endpoint returns `true`. " + + "If no such record exists, the endpoint returns `false`." + + TENANT_AUTHORITY_PARAGRAPH + ) + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @DeleteMapping("/{modelUuid}") + public boolean deleteAiModelById( + @Parameter( + description = "ID of the AI model record", + required = true, + example = "de7900d4-30e2-11f0-9cd2-0242ac120002" + ) + @PathVariable UUID modelUuid + ) throws ThingsboardException { + var user = getCurrentUser(); + var modelId = new AiModelId(modelUuid); + accessControlService.checkPermission(user, Resource.AI_MODEL, Operation.DELETE); + Optional toDelete = aiModelService.findAiModelByTenantIdAndId(user.getTenantId(), modelId); + if (toDelete.isEmpty()) { + return false; + } + accessControlService.checkPermission(user, Resource.AI_MODEL, Operation.DELETE, modelId, toDelete.get()); + return tbAiModelService.delete(toDelete.get(), user); + } + + @ApiOperation( + value = "Send request to AI chat model (sendChatRequest)", + notes = "Submits a single prompt - made up of an optional system message and a required user message - to the specified AI chat model " + + "and returns either the generated answer or an error envelope." + + TENANT_AUTHORITY_PARAGRAPH + ) + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @PostMapping("/chat") + public DeferredResult sendChatRequest(@Valid @RequestBody TbChatRequest tbChatRequest) { + ChatRequest langChainChatRequest = tbChatRequest.toLangChainChatRequest(); + AiChatModelConfig chatModelConfig = tbChatRequest.chatModelConfig(); + + ListenableFuture future = aiChatModelService.sendChatRequestAsync(chatModelConfig, langChainChatRequest) + .transform(chatResponse -> (TbChatResponse) new TbChatResponse.Success(chatResponse.aiMessage().text()), directExecutor()) + .catching(Throwable.class, ex -> new TbChatResponse.Failure(ex.getMessage()), directExecutor()); + + Integer requestTimeoutSeconds = chatModelConfig.timeoutSeconds(); + return requestTimeoutSeconds != null ? wrapFuture(future, Duration.ofSeconds(requestTimeoutSeconds).toMillis()) : wrapFuture(future); + } + +} diff --git a/application/src/main/java/org/thingsboard/server/controller/AlarmCommentController.java b/application/src/main/java/org/thingsboard/server/controller/AlarmCommentController.java index 9d123319ef..b12a253b36 100644 --- a/application/src/main/java/org/thingsboard/server/controller/AlarmCommentController.java +++ b/application/src/main/java/org/thingsboard/server/controller/AlarmCommentController.java @@ -19,12 +19,13 @@ import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.media.Schema; import lombok.RequiredArgsConstructor; import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.RestController; import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.alarm.AlarmComment; @@ -54,6 +55,7 @@ import static org.thingsboard.server.controller.ControllerConstants.UUID_WIKI_LI @RequiredArgsConstructor @RequestMapping("/api") public class AlarmCommentController extends BaseController { + public static final String ALARM_ID = "alarmId"; public static final String ALARM_COMMENT_ID = "commentId"; @@ -68,8 +70,7 @@ public class AlarmCommentController extends BaseController { "\n\n If comment type is not specified the default value 'OTHER' will be saved. If 'alarmId' or 'userId' specified in body it will be ignored." + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") - @RequestMapping(value = "/alarm/{alarmId}/comment", method = RequestMethod.POST) - @ResponseBody + @PostMapping(value = "/alarm/{alarmId}/comment") public AlarmComment saveAlarmComment(@Parameter(description = ALARM_ID_PARAM_DESCRIPTION) @PathVariable(ALARM_ID) String strAlarmId, @io.swagger.v3.oas.annotations.parameters.RequestBody(description = "A JSON value representing the comment.") @RequestBody AlarmComment alarmComment) throws ThingsboardException { checkParameter(ALARM_ID, strAlarmId); @@ -82,8 +83,7 @@ public class AlarmCommentController extends BaseController { @ApiOperation(value = "Delete Alarm comment (deleteAlarmComment)", notes = "Deletes the Alarm comment. Referencing non-existing Alarm comment Id will cause an error." + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") - @RequestMapping(value = "/alarm/{alarmId}/comment/{commentId}", method = RequestMethod.DELETE) - @ResponseBody + @DeleteMapping(value = "/alarm/{alarmId}/comment/{commentId}") public void deleteAlarmComment(@Parameter(description = ALARM_ID_PARAM_DESCRIPTION) @PathVariable(ALARM_ID) String strAlarmId, @Parameter(description = ALARM_COMMENT_ID_PARAM_DESCRIPTION) @PathVariable(ALARM_COMMENT_ID) String strCommentId) throws ThingsboardException { checkParameter(ALARM_ID, strAlarmId); AlarmId alarmId = new AlarmId(toUUID(strAlarmId)); @@ -98,8 +98,7 @@ public class AlarmCommentController extends BaseController { notes = "Returns a page of alarm comments for specified alarm. " + PAGE_DATA_PARAMETERS + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") - @RequestMapping(value = "/alarm/{alarmId}/comment", method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/alarm/{alarmId}/comment") public PageData getAlarmComments( @Parameter(description = ALARM_ID_PARAM_DESCRIPTION, required = true) @PathVariable(ALARM_ID) String strAlarmId, @@ -118,4 +117,5 @@ public class AlarmCommentController extends BaseController { PageLink pageLink = createPageLink(pageSize, page, null, sortProperty, sortOrder); return checkNotNull(alarmCommentService.findAlarmComments(alarm.getTenantId(), alarmId, pageLink)); } + } diff --git a/application/src/main/java/org/thingsboard/server/controller/AlarmController.java b/application/src/main/java/org/thingsboard/server/controller/AlarmController.java index dd8d883145..f5a04e1e48 100644 --- a/application/src/main/java/org/thingsboard/server/controller/AlarmController.java +++ b/application/src/main/java/org/thingsboard/server/controller/AlarmController.java @@ -21,12 +21,13 @@ import io.swagger.v3.oas.annotations.media.Schema; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestController; import org.thingsboard.server.common.data.EntitySubtype; @@ -58,7 +59,6 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.UUID; -import java.util.concurrent.ExecutionException; import static org.thingsboard.server.controller.ControllerConstants.ALARM_ID_PARAM_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.ALARM_INFO_DESCRIPTION; @@ -104,8 +104,7 @@ public class AlarmController extends BaseController { @ApiOperation(value = "Get Alarm (getAlarmById)", notes = "Fetch the Alarm object based on the provided Alarm Id. " + ALARM_SECURITY_CHECK) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") - @RequestMapping(value = "/alarm/{alarmId}", method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/alarm/{alarmId}") public Alarm getAlarmById(@Parameter(description = ALARM_ID_PARAM_DESCRIPTION) @PathVariable(ALARM_ID) String strAlarmId) throws ThingsboardException { checkParameter(ALARM_ID, strAlarmId); @@ -117,8 +116,7 @@ public class AlarmController extends BaseController { notes = "Fetch the Alarm Info object based on the provided Alarm Id. " + ALARM_SECURITY_CHECK + ALARM_INFO_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") - @RequestMapping(value = "/alarm/info/{alarmId}", method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/alarm/info/{alarmId}") public AlarmInfo getAlarmInfoById(@Parameter(description = ALARM_ID_PARAM_DESCRIPTION) @PathVariable(ALARM_ID) String strAlarmId) throws ThingsboardException { checkParameter(ALARM_ID, strAlarmId); @@ -136,11 +134,9 @@ public class AlarmController extends BaseController { "If the user tries to create 'HighTemperature' alarm for the same device again, the previous alarm will be updated (the 'end_ts' will be set to current timestamp). " + "If the user clears the alarm (see 'Clear Alarm(clearAlarm)'), than new alarm with the same type and same device may be created. " + "Remove 'id', 'tenantId' and optionally 'customerId' from the request body example (below) to create new Alarm entity. " + - TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH - ) + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") - @RequestMapping(value = "/alarm", method = RequestMethod.POST) - @ResponseBody + @PostMapping(value = "/alarm") public Alarm saveAlarm(@io.swagger.v3.oas.annotations.parameters.RequestBody(description = "A JSON value representing the alarm.") @RequestBody Alarm alarm) throws ThingsboardException { alarm.setTenantId(getTenantId()); checkNotNull(alarm.getOriginator()); @@ -155,8 +151,7 @@ public class AlarmController extends BaseController { @ApiOperation(value = "Delete Alarm (deleteAlarm)", notes = "Deletes the Alarm. Referencing non-existing Alarm Id will cause an error." + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") - @RequestMapping(value = "/alarm/{alarmId}", method = RequestMethod.DELETE) - @ResponseBody + @DeleteMapping(value = "/alarm/{alarmId}") public boolean deleteAlarm(@Parameter(description = ALARM_ID_PARAM_DESCRIPTION) @PathVariable(ALARM_ID) String strAlarmId) throws ThingsboardException { checkParameter(ALARM_ID, strAlarmId); AlarmId alarmId = new AlarmId(toUUID(strAlarmId)); @@ -169,7 +164,7 @@ public class AlarmController extends BaseController { "Once acknowledged, the 'ack_ts' field will be set to current timestamp and special rule chain event 'ALARM_ACK' will be generated. " + "Referencing non-existing Alarm Id will cause an error." + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") - @RequestMapping(value = "/alarm/{alarmId}/ack", method = RequestMethod.POST) + @PostMapping(value = "/alarm/{alarmId}/ack") @ResponseStatus(value = HttpStatus.OK) public AlarmInfo ackAlarm(@Parameter(description = ALARM_ID_PARAM_DESCRIPTION) @PathVariable(ALARM_ID) String strAlarmId) throws Exception { checkParameter(ALARM_ID, strAlarmId); @@ -184,7 +179,7 @@ public class AlarmController extends BaseController { "Once cleared, the 'clear_ts' field will be set to current timestamp and special rule chain event 'ALARM_CLEAR' will be generated. " + "Referencing non-existing Alarm Id will cause an error." + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") - @RequestMapping(value = "/alarm/{alarmId}/clear", method = RequestMethod.POST) + @PostMapping(value = "/alarm/{alarmId}/clear") @ResponseStatus(value = HttpStatus.OK) public AlarmInfo clearAlarm(@Parameter(description = ALARM_ID_PARAM_DESCRIPTION) @PathVariable(ALARM_ID) String strAlarmId) throws Exception { checkParameter(ALARM_ID, strAlarmId); @@ -200,7 +195,7 @@ public class AlarmController extends BaseController { "(or ALARM_REASSIGNED in case of assigning already assigned alarm) will be generated. " + "Referencing non-existing Alarm Id will cause an error." + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") - @RequestMapping(value = "/alarm/{alarmId}/assign/{assigneeId}", method = RequestMethod.POST) + @PostMapping(value = "/alarm/{alarmId}/assign/{assigneeId}") @ResponseStatus(value = HttpStatus.OK) public Alarm assignAlarm(@Parameter(description = ALARM_ID_PARAM_DESCRIPTION) @PathVariable(ALARM_ID) String strAlarmId, @@ -221,7 +216,7 @@ public class AlarmController extends BaseController { "Once unassigned, the 'assign_ts' field will be set to current timestamp and special rule chain event 'ALARM_UNASSIGNED' will be generated. " + "Referencing non-existing Alarm Id will cause an error." + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") - @RequestMapping(value = "/alarm/{alarmId}/assign", method = RequestMethod.DELETE) + @DeleteMapping(value = "/alarm/{alarmId}/assign") @ResponseStatus(value = HttpStatus.OK) public Alarm unassignAlarm(@Parameter(description = ALARM_ID_PARAM_DESCRIPTION) @PathVariable(ALARM_ID) String strAlarmId @@ -236,8 +231,7 @@ public class AlarmController extends BaseController { notes = "Returns a page of alarms for the selected entity. Specifying both parameters 'searchStatus' and 'status' at the same time will cause an error. " + PAGE_DATA_PARAMETERS + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") - @RequestMapping(value = "/alarm/{entityType}/{entityId}", method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/alarm/{entityType}/{entityId}") public PageData getAlarms( @Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, schema = @Schema(defaultValue = "DEVICE")) @PathVariable(ENTITY_TYPE) String strEntityType, @@ -265,7 +259,7 @@ public class AlarmController extends BaseController { @RequestParam(required = false) Long endTime, @Parameter(description = ALARM_QUERY_FETCH_ORIGINATOR_DESCRIPTION) @RequestParam(required = false) Boolean fetchOriginator - ) throws ThingsboardException, ExecutionException, InterruptedException { + ) throws ThingsboardException { checkParameter("EntityId", strEntityId); checkParameter("EntityType", strEntityType); EntityId entityId = EntityIdFactory.getByTypeAndId(strEntityType, strEntityId); @@ -292,8 +286,7 @@ public class AlarmController extends BaseController { "Specifying both parameters 'searchStatus' and 'status' at the same time will cause an error. " + PAGE_DATA_PARAMETERS + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") - @RequestMapping(value = "/alarms", method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/alarms") public PageData getAllAlarms( @Parameter(description = ALARM_QUERY_SEARCH_STATUS_DESCRIPTION, schema = @Schema(allowableValues = {"ANY", "ACTIVE", "CLEARED", "ACK", "UNACK"})) @RequestParam(required = false) String searchStatus, @@ -317,7 +310,7 @@ public class AlarmController extends BaseController { @RequestParam(required = false) Long endTime, @Parameter(description = ALARM_QUERY_FETCH_ORIGINATOR_DESCRIPTION) @RequestParam(required = false) Boolean fetchOriginator - ) throws ThingsboardException, ExecutionException, InterruptedException { + ) throws ThingsboardException { AlarmSearchStatus alarmSearchStatus = StringUtils.isEmpty(searchStatus) ? null : AlarmSearchStatus.valueOf(searchStatus); AlarmStatus alarmStatus = StringUtils.isEmpty(status) ? null : AlarmStatus.valueOf(status); if (alarmSearchStatus != null && alarmStatus != null) { @@ -341,8 +334,7 @@ public class AlarmController extends BaseController { notes = "Returns a page of alarms for the selected entity. " + PAGE_DATA_PARAMETERS + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") - @RequestMapping(value = "/v2/alarm/{entityType}/{entityId}", method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/v2/alarm/{entityType}/{entityId}") public PageData getAlarmsV2( @Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, schema = @Schema(defaultValue = "DEVICE")) @PathVariable(ENTITY_TYPE) String strEntityType, @@ -370,7 +362,7 @@ public class AlarmController extends BaseController { @RequestParam(required = false) Long startTime, @Parameter(description = ALARM_QUERY_END_TIME_DESCRIPTION) @RequestParam(required = false) Long endTime - ) throws ThingsboardException, ExecutionException, InterruptedException { + ) throws ThingsboardException { checkParameter("EntityId", strEntityId); checkParameter("EntityType", strEntityType); EntityId entityId = EntityIdFactory.getByTypeAndId(strEntityType, strEntityId); @@ -407,8 +399,7 @@ public class AlarmController extends BaseController { "If the user has the authority of 'Customer User', the server returns alarms that belongs to the customer of current user. " + PAGE_DATA_PARAMETERS + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") - @RequestMapping(value = "/v2/alarms", method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/v2/alarms") public PageData getAllAlarmsV2( @Parameter(description = ALARM_QUERY_SEARCH_STATUS_ARRAY_DESCRIPTION, array = @ArraySchema(schema = @Schema(type = "string", allowableValues = {"ANY", "ACTIVE", "CLEARED", "ACK", "UNACK"}))) @RequestParam(required = false) String[] statusList, @@ -432,7 +423,7 @@ public class AlarmController extends BaseController { @RequestParam(required = false) Long startTime, @Parameter(description = ALARM_QUERY_END_TIME_DESCRIPTION) @RequestParam(required = false) Long endTime - ) throws ThingsboardException, ExecutionException, InterruptedException { + ) throws ThingsboardException { List alarmStatusList = new ArrayList<>(); if (statusList != null) { for (String strStatus : statusList) { @@ -465,11 +456,9 @@ public class AlarmController extends BaseController { @ApiOperation(value = "Get Highest Alarm Severity (getHighestAlarmSeverity)", notes = "Search the alarms by originator ('entityType' and entityId') and optional 'status' or 'searchStatus' filters and returns the highest AlarmSeverity(CRITICAL, MAJOR, MINOR, WARNING or INDETERMINATE). " + - "Specifying both parameters 'searchStatus' and 'status' at the same time will cause an error." + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH - ) + "Specifying both parameters 'searchStatus' and 'status' at the same time will cause an error." + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") - @RequestMapping(value = "/alarm/highestSeverity/{entityType}/{entityId}", method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/alarm/highestSeverity/{entityType}/{entityId}") public AlarmSeverity getHighestAlarmSeverity( @Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, schema = @Schema(defaultValue = "DEVICE")) @PathVariable(ENTITY_TYPE) String strEntityType, @@ -499,8 +488,7 @@ public class AlarmController extends BaseController { @ApiOperation(value = "Get Alarm Types (getAlarmTypes)", notes = "Returns a set of unique alarm types based on alarms that are either owned by the tenant or assigned to the customer which user is performing the request.") @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") - @RequestMapping(value = "/alarm/types", method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/alarm/types") public PageData getAlarmTypes(@Parameter(description = PAGE_SIZE_DESCRIPTION, required = true) @RequestParam int pageSize, @Parameter(description = PAGE_NUMBER_DESCRIPTION, required = true) @@ -508,7 +496,7 @@ public class AlarmController extends BaseController { @Parameter(description = ALARM_QUERY_TEXT_SEARCH_DESCRIPTION) @RequestParam(required = false) String textSearch, @Parameter(description = SORT_ORDER_DESCRIPTION, schema = @Schema(allowableValues = {"ASC", "DESC"})) - @RequestParam(required = false) String sortOrder) throws ThingsboardException, ExecutionException, InterruptedException { + @RequestParam(required = false) String sortOrder) throws ThingsboardException { PageLink pageLink = createPageLink(pageSize, page, textSearch, "type", sortOrder); return checkNotNull(alarmService.findAlarmTypesByTenantId(getTenantId(), pageLink)); } diff --git a/application/src/main/java/org/thingsboard/server/controller/BaseController.java b/application/src/main/java/org/thingsboard/server/controller/BaseController.java index 8daee6800b..26f116b083 100644 --- a/application/src/main/java/org/thingsboard/server/controller/BaseController.java +++ b/application/src/main/java/org/thingsboard/server/controller/BaseController.java @@ -37,6 +37,7 @@ import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.context.request.async.AsyncRequestTimeoutException; import org.springframework.web.context.request.async.DeferredResult; +import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; import org.thingsboard.common.util.DonAsynchron; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.cluster.TbClusterService; @@ -61,6 +62,7 @@ import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.TenantInfo; 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.alarm.AlarmInfo; @@ -75,6 +77,7 @@ import org.thingsboard.server.common.data.edge.EdgeInfo; import org.thingsboard.server.common.data.exception.EntityVersionMismatchException; import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; import org.thingsboard.server.common.data.exception.ThingsboardException; +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; @@ -129,6 +132,7 @@ import org.thingsboard.server.common.data.util.ThrowingBiFunction; import org.thingsboard.server.common.data.widget.WidgetTypeDetails; import org.thingsboard.server.common.data.widget.WidgetTypeInfo; import org.thingsboard.server.common.data.widget.WidgetsBundle; +import org.thingsboard.server.dao.ai.AiModelService; import org.thingsboard.server.dao.alarm.AlarmCommentService; import org.thingsboard.server.dao.asset.AssetProfileService; import org.thingsboard.server.dao.asset.AssetService; @@ -175,6 +179,7 @@ import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.action.EntityActionService; import org.thingsboard.server.service.component.ComponentDiscoveryService; import org.thingsboard.server.service.entitiy.TbLogEntityActionService; +import org.thingsboard.server.service.entitiy.ai.TbAiModelService; import org.thingsboard.server.service.entitiy.user.TbUserSettingsService; import org.thingsboard.server.service.ota.OtaPackageStateService; import org.thingsboard.server.service.profile.TbAssetProfileCache; @@ -378,6 +383,12 @@ public abstract class BaseController { @Autowired protected CalculatedFieldService calculatedFieldService; + @Autowired + protected AiModelService aiModelService; + + @Autowired + protected TbAiModelService tbAiModelService; + @Value("${server.log_controller_error_stack_trace}") @Getter private boolean logControllerErrorStackTrace; @@ -390,7 +401,7 @@ public abstract class BaseController { public void handleControllerException(Exception e, HttpServletResponse response) { ThingsboardException thingsboardException = handleException(e); if (thingsboardException.getErrorCode() == ThingsboardErrorCode.GENERAL && thingsboardException.getCause() instanceof Exception - && StringUtils.equals(thingsboardException.getCause().getMessage(), thingsboardException.getMessage())) { + && StringUtils.equals(thingsboardException.getCause().getMessage(), thingsboardException.getMessage())) { e = (Exception) thingsboardException.getCause(); } else { e = thingsboardException; @@ -438,7 +449,7 @@ public abstract class BaseController { if (exception instanceof ThingsboardException) { return (ThingsboardException) exception; } else if (exception instanceof IllegalArgumentException || exception instanceof IncorrectParameterException - || exception instanceof DataValidationException || cause instanceof IncorrectParameterException) { + || exception instanceof DataValidationException || cause instanceof IncorrectParameterException) { return new ThingsboardException(exception.getMessage(), ThingsboardErrorCode.BAD_REQUEST_PARAMS); } else if (exception instanceof MessagingException) { return new ThingsboardException("Unable to send mail", ThingsboardErrorCode.GENERAL); @@ -448,6 +459,8 @@ public abstract class BaseController { return new ThingsboardException(exception, ThingsboardErrorCode.DATABASE); } else if (exception instanceof EntityVersionMismatchException) { return new ThingsboardException(exception.getMessage(), exception, ThingsboardErrorCode.VERSION_CONFLICT); + } else if (exception instanceof MethodArgumentTypeMismatchException) { + return new ThingsboardException(exception.getMessage(), exception, ThingsboardErrorCode.BAD_REQUEST_PARAMS); } return new ThingsboardException(exception.getMessage(), exception, ThingsboardErrorCode.GENERAL); } @@ -634,6 +647,7 @@ public abstract class BaseController { case MOBILE_APP -> checkMobileAppId(new MobileAppId(entityId.getId()), operation); case MOBILE_APP_BUNDLE -> checkMobileAppBundleId(new MobileAppBundleId(entityId.getId()), operation); case CALCULATED_FIELD -> checkCalculatedFieldId(new CalculatedFieldId(entityId.getId()), operation); + case AI_MODEL -> checkAiModelId(new AiModelId(entityId.getId()), operation); default -> (HasId) checkEntityId(entityId, entitiesService::findEntityByTenantIdAndId, operation); }; } catch (Exception e) { @@ -837,6 +851,10 @@ public abstract class BaseController { return checkEntityId(jobId, jobService::findJobById, operation); } + AiModel checkAiModelId(AiModelId settingsId, Operation operation) throws ThingsboardException { + return checkEntityId(settingsId, (tenantId, id) -> aiModelService.findAiModelByTenantIdAndId(tenantId, id).orElse(null), operation); + } + protected I emptyId(EntityType entityType) { return (I) EntityIdFactory.getByTypeAndUuid(entityType, ModelConstants.NULL_UUID); } 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 2dcb32cf39..c5b077c128 100644 --- a/application/src/main/java/org/thingsboard/server/controller/CalculatedFieldController.java +++ b/application/src/main/java/org/thingsboard/server/controller/CalculatedFieldController.java @@ -17,19 +17,21 @@ package org.thingsboard.server.controller; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.node.ObjectNode; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.media.Schema; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.ObjectUtils; +import org.apache.commons.lang3.exception.ExceptionUtils; import org.springframework.http.HttpStatus; import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestController; import org.thingsboard.common.util.JacksonUtil; @@ -41,7 +43,6 @@ import org.thingsboard.script.api.tbel.TbelCfTsRollingArg; import org.thingsboard.script.api.tbel.TbelInvokeService; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EventInfo; -import org.thingsboard.server.common.data.HasTenantId; import org.thingsboard.server.common.data.cf.CalculatedField; import org.thingsboard.server.common.data.cf.configuration.CalculatedFieldConfiguration; import org.thingsboard.server.common.data.event.EventType; @@ -49,17 +50,14 @@ 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.EntityIdFactory; -import org.thingsboard.server.common.data.id.HasId; 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.config.annotations.ApiOperation; import org.thingsboard.server.dao.event.EventService; import org.thingsboard.server.queue.util.TbCoreComponent; -import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldScriptEngine; import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldTbelScriptEngine; import org.thingsboard.server.service.entitiy.cf.TbCalculatedFieldService; -import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.permission.Operation; import java.util.ArrayList; @@ -100,30 +98,30 @@ public class CalculatedFieldController extends BaseController { private static final String TEST_SCRIPT_EXPRESSION = "Execute the Script expression and return the result. The format of request: \n\n" - + MARKDOWN_CODE_BLOCK_START - + "{\n" + - " \"expression\": \"var temp = 0; foreach(element: temperature.values) {temp += element.value;} var avgTemperature = temp / temperature.values.size(); var adjustedTemperature = avgTemperature + 0.1 * humidity.value; return {\\\"adjustedTemperature\\\": adjustedTemperature};\",\n" + - " \"arguments\": {\n" + - " \"temperature\": {\n" + - " \"type\": \"TS_ROLLING\",\n" + - " \"timeWindow\": {\n" + - " \"startTs\": 1739775630002,\n" + - " \"endTs\": 65432211,\n" + - " \"limit\": 5\n" + - " },\n" + - " \"values\": [\n" + - " { \"ts\": 1739775639851, \"value\": 23 },\n" + - " { \"ts\": 1739775664561, \"value\": 43 },\n" + - " { \"ts\": 1739775713079, \"value\": 15 },\n" + - " { \"ts\": 1739775999522, \"value\": 34 },\n" + - " { \"ts\": 1739776228452, \"value\": 22 }\n" + - " ]\n" + - " },\n" + - " \"humidity\": { \"type\": \"SINGLE_VALUE\", \"ts\": 1739776478057, \"value\": 23 }\n" + - " }\n" + - "}" - + MARKDOWN_CODE_BLOCK_END - + "\n\n Expected result JSON contains \"output\" and \"error\"."; + + MARKDOWN_CODE_BLOCK_START + + "{\n" + + " \"expression\": \"var temp = 0; foreach(element: temperature.values) {temp += element.value;} var avgTemperature = temp / temperature.values.size(); var adjustedTemperature = avgTemperature + 0.1 * humidity.value; return {\\\"adjustedTemperature\\\": adjustedTemperature};\",\n" + + " \"arguments\": {\n" + + " \"temperature\": {\n" + + " \"type\": \"TS_ROLLING\",\n" + + " \"timeWindow\": {\n" + + " \"startTs\": 1739775630002,\n" + + " \"endTs\": 65432211,\n" + + " \"limit\": 5\n" + + " },\n" + + " \"values\": [\n" + + " { \"ts\": 1739775639851, \"value\": 23 },\n" + + " { \"ts\": 1739775664561, \"value\": 43 },\n" + + " { \"ts\": 1739775713079, \"value\": 15 },\n" + + " { \"ts\": 1739775999522, \"value\": 34 },\n" + + " { \"ts\": 1739776228452, \"value\": 22 }\n" + + " ]\n" + + " },\n" + + " \"humidity\": { \"type\": \"SINGLE_VALUE\", \"ts\": 1739776478057, \"value\": 23 }\n" + + " }\n" + + "}" + + MARKDOWN_CODE_BLOCK_END + + "\n\n Expected result JSON contains \"output\" and \"error\"."; @ApiOperation(value = "Create Or Update Calculated Field (saveCalculatedField)", notes = "Creates or Updates the Calculated Field. When creating calculated field, platform generates Calculated Field Id as " + UUID_WIKI_LINK + @@ -133,13 +131,12 @@ public class CalculatedFieldController extends BaseController { "Remove 'id', 'tenantId' from the request body example (below) to create new Calculated Field entity. " + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") - @RequestMapping(value = "/calculatedField", method = RequestMethod.POST) - @ResponseBody + @PostMapping("/calculatedField") public CalculatedField saveCalculatedField(@io.swagger.v3.oas.annotations.parameters.RequestBody(description = "A JSON value representing the calculated field.") @RequestBody CalculatedField calculatedField) throws Exception { calculatedField.setTenantId(getTenantId()); checkEntityId(calculatedField.getEntityId(), Operation.WRITE_CALCULATED_FIELD); - checkReferencedEntities(calculatedField.getConfiguration(), getCurrentUser()); + checkReferencedEntities(calculatedField.getConfiguration()); return tbCalculatedFieldService.save(calculatedField, getCurrentUser()); } @@ -147,8 +144,7 @@ public class CalculatedFieldController extends BaseController { notes = "Fetch the Calculated Field object based on the provided Calculated Field Id." ) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") - @RequestMapping(value = "/calculatedField/{calculatedFieldId}", method = RequestMethod.GET) - @ResponseBody + @GetMapping("/calculatedField/{calculatedFieldId}") public CalculatedField getCalculatedFieldById(@Parameter @PathVariable(CALCULATED_FIELD_ID) String strCalculatedFieldId) throws ThingsboardException { checkParameter(CALCULATED_FIELD_ID, strCalculatedFieldId); CalculatedFieldId calculatedFieldId = new CalculatedFieldId(toUUID(strCalculatedFieldId)); @@ -162,8 +158,7 @@ public class CalculatedFieldController extends BaseController { notes = "Fetch the Calculated Fields based on the provided Entity Id." ) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") - @RequestMapping(value = "/{entityType}/{entityId}/calculatedFields", params = {"pageSize", "page"}, method = RequestMethod.GET) - @ResponseBody + @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, @@ -182,8 +177,8 @@ public class CalculatedFieldController extends BaseController { @ApiOperation(value = "Delete Calculated Field (deleteCalculatedField)", notes = "Deletes the calculated field. Referencing non-existing Calculated Field Id will cause an error." + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('TENANT_ADMIN')") - @RequestMapping(value = "/calculatedField/{calculatedFieldId}", method = RequestMethod.DELETE) - @ResponseStatus(value = HttpStatus.OK) + @DeleteMapping("/calculatedField/{calculatedFieldId}") + @ResponseStatus(HttpStatus.OK) public void deleteCalculatedField(@PathVariable(CALCULATED_FIELD_ID) String strCalculatedFieldId) throws Exception { checkParameter(CALCULATED_FIELD_ID, strCalculatedFieldId); CalculatedFieldId calculatedFieldId = new CalculatedFieldId(toUUID(strCalculatedFieldId)); @@ -196,8 +191,7 @@ public class CalculatedFieldController extends BaseController { notes = "Gets latest calculated field debug event for specified calculated field id. " + "Referencing non-existing calculated field id will cause an error. " + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") - @RequestMapping(value = "/calculatedField/{calculatedFieldId}/debug", method = RequestMethod.GET) - @ResponseBody + @GetMapping("/calculatedField/{calculatedFieldId}/debug") public JsonNode getLatestCalculatedFieldDebugEvent(@Parameter @PathVariable(CALCULATED_FIELD_ID) String strCalculatedFieldId) throws ThingsboardException { checkParameter(CALCULATED_FIELD_ID, strCalculatedFieldId); CalculatedFieldId calculatedFieldId = new CalculatedFieldId(toUUID(strCalculatedFieldId)); @@ -212,15 +206,13 @@ public class CalculatedFieldController extends BaseController { @ApiOperation(value = "Test Script expression", notes = TEST_SCRIPT_EXPRESSION + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('TENANT_ADMIN')") - @RequestMapping(value = "/calculatedField/testScript", method = RequestMethod.POST) - @ResponseBody + @PostMapping("/calculatedField/testScript") public JsonNode testScript( @io.swagger.v3.oas.annotations.parameters.RequestBody(description = "Test calculated field TBEL expression.") @RequestBody JsonNode inputParams) { String expression = inputParams.get("expression").asText(); Map arguments = Objects.requireNonNullElse( - JacksonUtil.convertValue(inputParams.get("arguments"), new TypeReference<>() { - }), + JacksonUtil.convertValue(inputParams.get("arguments"), new TypeReference<>() {}), Collections.emptyMap() ); @@ -231,12 +223,13 @@ public class CalculatedFieldController extends BaseController { String output = ""; String errorText = ""; + CalculatedFieldTbelScriptEngine engine = null; try { if (tbelInvokeService == null) { throw new IllegalArgumentException("TBEL script engine is disabled!"); } - CalculatedFieldScriptEngine calculatedFieldScriptEngine = new CalculatedFieldTbelScriptEngine( + engine = new CalculatedFieldTbelScriptEngine( getTenantId(), tbelInvokeService, expression, @@ -254,17 +247,20 @@ public class CalculatedFieldController extends BaseController { } } - JsonNode json = calculatedFieldScriptEngine.executeJsonAsync(args).get(TIMEOUT, TimeUnit.SECONDS); + JsonNode json = engine.executeJsonAsync(args).get(TIMEOUT, TimeUnit.SECONDS); output = JacksonUtil.toString(json); } catch (Exception e) { log.error("Error evaluating expression", e); - errorText = e.getMessage(); + Throwable rootCause = ExceptionUtils.getRootCause(e); + errorText = ObjectUtils.firstNonNull(rootCause.getMessage(), e.getMessage(), e.getClass().getSimpleName()); + } finally { + if (engine != null) { + engine.destroy(); + } } - - ObjectNode result = JacksonUtil.newObjectNode(); - result.put("output", output); - result.put("error", errorText); - return result; + return JacksonUtil.newObjectNode() + .put("output", output) + .put("error", errorText); } private long getLatestTimestamp(Map arguments) { @@ -281,7 +277,7 @@ public class CalculatedFieldController extends BaseController { return lastUpdateTimestamp == -1 ? System.currentTimeMillis() : lastUpdateTimestamp; } - private & HasTenantId, I extends EntityId> void checkReferencedEntities(CalculatedFieldConfiguration calculatedFieldConfig, SecurityUser user) throws ThingsboardException { + private void checkReferencedEntities(CalculatedFieldConfiguration calculatedFieldConfig) throws ThingsboardException { List referencedEntityIds = calculatedFieldConfig.getReferencedEntities(); for (EntityId referencedEntityId : referencedEntityIds) { EntityType entityType = referencedEntityId.getEntityType(); @@ -290,11 +286,9 @@ public class CalculatedFieldController extends BaseController { return; } case CUSTOMER, ASSET, DEVICE -> checkEntityId(referencedEntityId, Operation.READ); - default -> - throw new IllegalArgumentException("Calculated fields do not support '" + entityType + "' for referenced entities."); + 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 8817c24efe..a87864726b 100644 --- a/application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java +++ b/application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java @@ -31,7 +31,7 @@ public class ControllerConstants { protected static final String ASSIGNEE_ID = "assigneeId"; protected static final String PAGE_DATA_PARAMETERS = "You can specify parameters to filter the results. " + "The result is wrapped with PageData object that allows you to iterate over result set using pagination. " + - "See the 'Model' tab of the Response Class for more details. "; + "See response schema for more details. "; protected static final String INLINE_IMAGES = "inlineImages"; protected static final String INLINE_IMAGES_DESCRIPTION = "Inline images as a data URL (Base64)"; @@ -90,6 +90,7 @@ public class ControllerConstants { protected static final String TENANT_PROFILE_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'substring' filter based on the tenant profile name."; protected static final String RULE_CHAIN_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'substring' filter based on the rule chain name."; protected static final String DEVICE_PROFILE_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'substring' filter based on the device profile name."; + protected static final String AI_MODEL_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'substring' filter based on the AI model name, provider and model ID."; protected static final String ASSET_PROFILE_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'substring' filter based on the asset profile name."; protected static final String CUSTOMER_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'substring' filter based on the customer title."; diff --git a/application/src/main/java/org/thingsboard/server/controller/EntityQueryController.java b/application/src/main/java/org/thingsboard/server/controller/EntityQueryController.java index 3a93f318d1..70fa3a5ed0 100644 --- a/application/src/main/java/org/thingsboard/server/controller/EntityQueryController.java +++ b/application/src/main/java/org/thingsboard/server/controller/EntityQueryController.java @@ -42,6 +42,7 @@ import org.thingsboard.server.common.data.query.EntityCountQuery; import org.thingsboard.server.common.data.query.EntityData; import org.thingsboard.server.common.data.query.EntityDataPageLink; import org.thingsboard.server.common.data.query.EntityDataQuery; +import org.thingsboard.server.common.data.query.EntityFilter; import org.thingsboard.server.common.msg.edqs.EdqsApiService; import org.thingsboard.server.common.msg.edqs.EdqsService; import org.thingsboard.server.config.annotations.ApiOperation; @@ -76,6 +77,7 @@ public class EntityQueryController extends BaseController { @Parameter(description = "A JSON value representing the entity count query. See API call notes above for more details.") @RequestBody EntityCountQuery query) throws ThingsboardException { checkNotNull(query); + resolveQuery(query); return this.entityQueryService.countEntitiesByQuery(getCurrentUser(), query); } @@ -87,6 +89,7 @@ public class EntityQueryController extends BaseController { @Parameter(description = "A JSON value representing the entity data query. See API call notes above for more details.") @RequestBody EntityDataQuery query) throws ThingsboardException { checkNotNull(query); + resolveQuery(query); return this.entityQueryService.findEntityDataByQuery(getCurrentUser(), query); } @@ -103,6 +106,7 @@ public class EntityQueryController extends BaseController { if (assigneeId != null) { checkUserId(assigneeId, Operation.READ); } + resolveQuery(query); return this.entityQueryService.findAlarmDataByQuery(getCurrentUser(), query); } @@ -117,6 +121,7 @@ public class EntityQueryController extends BaseController { if (assigneeId != null) { checkUserId(assigneeId, Operation.READ); } + resolveQuery(query); return this.entityQueryService.countAlarmsByQuery(getCurrentUser(), query); } @@ -136,6 +141,7 @@ public class EntityQueryController extends BaseController { @RequestParam(value = "scope", required = false) String scope) throws ThingsboardException { TenantId tenantId = getTenantId(); checkNotNull(query); + resolveQuery(query); EntityDataPageLink pageLink = query.getPageLink(); if (pageLink.getPageSize() > MAX_PAGE_SIZE) { pageLink.setPageSize(MAX_PAGE_SIZE); @@ -155,4 +161,13 @@ public class EntityQueryController extends BaseController { return edqsService.getState(); } + private void resolveQuery(EntityCountQuery query) throws ThingsboardException { + if (query.getEntityFilter() != null) { + var user = getCurrentUser(); + var customerId = user.getCustomerId(); + var ownerId = customerId != null && !customerId.isNullUid() ? customerId : getTenantId(); + EntityFilter.resolveEntityFilter(query.getEntityFilter(), getTenantId(), user.getId(), ownerId); + } + } + } diff --git a/application/src/main/java/org/thingsboard/server/controller/OtaPackageController.java b/application/src/main/java/org/thingsboard/server/controller/OtaPackageController.java index 43ebf89b41..d4f932a643 100644 --- a/application/src/main/java/org/thingsboard/server/controller/OtaPackageController.java +++ b/application/src/main/java/org/thingsboard/server/controller/OtaPackageController.java @@ -24,13 +24,14 @@ import org.springframework.core.io.ByteArrayResource; import org.springframework.http.HttpHeaders; import org.springframework.http.ResponseEntity; import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestPart; -import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; import org.thingsboard.server.common.data.OtaPackage; @@ -49,8 +50,6 @@ import org.thingsboard.server.service.entitiy.ota.TbOtaPackageService; import org.thingsboard.server.service.security.permission.Operation; import org.thingsboard.server.service.security.permission.Resource; -import java.io.IOException; - import static org.springframework.http.MediaType.MULTIPART_FORM_DATA_VALUE; import static org.thingsboard.server.controller.ControllerConstants.DEVICE_PROFILE_ID_PARAM_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.OTA_PACKAGE_DESCRIPTION; @@ -80,8 +79,7 @@ public class OtaPackageController extends BaseController { @ApiOperation(value = "Download OTA Package (downloadOtaPackage)", notes = "Download OTA Package based on the provided OTA Package Id." + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority( 'TENANT_ADMIN')") - @RequestMapping(value = "/otaPackage/{otaPackageId}/download", method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/otaPackage/{otaPackageId}/download") public ResponseEntity downloadOtaPackage(@Parameter(description = OTA_PACKAGE_ID_PARAM_DESCRIPTION) @PathVariable(OTA_PACKAGE_ID) String strOtaPackageId) throws ThingsboardException { checkParameter(OTA_PACKAGE_ID, strOtaPackageId); @@ -105,8 +103,7 @@ public class OtaPackageController extends BaseController { notes = "Fetch the OTA Package Info object based on the provided OTA Package Id. " + OTA_PACKAGE_INFO_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") - @RequestMapping(value = "/otaPackage/info/{otaPackageId}", method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/otaPackage/info/{otaPackageId}") public OtaPackageInfo getOtaPackageInfoById(@Parameter(description = OTA_PACKAGE_ID_PARAM_DESCRIPTION) @PathVariable(OTA_PACKAGE_ID) String strOtaPackageId) throws ThingsboardException { checkParameter(OTA_PACKAGE_ID, strOtaPackageId); @@ -118,8 +115,7 @@ public class OtaPackageController extends BaseController { notes = "Fetch the OTA Package object based on the provided OTA Package Id. " + "The server checks that the OTA Package is owned by the same tenant. " + OTA_PACKAGE_DESCRIPTION + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") - @RequestMapping(value = "/otaPackage/{otaPackageId}", method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/otaPackage/{otaPackageId}") public OtaPackage getOtaPackageById(@Parameter(description = OTA_PACKAGE_ID_PARAM_DESCRIPTION) @PathVariable(OTA_PACKAGE_ID) String strOtaPackageId) throws ThingsboardException { checkParameter(OTA_PACKAGE_ID, strOtaPackageId); @@ -134,10 +130,9 @@ public class OtaPackageController extends BaseController { "Referencing non-existing OTA Package Id will cause 'Not Found' error. " + "\n\nOTA Package combination of the title with the version is unique in the scope of tenant. " + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") - @RequestMapping(value = "/otaPackage", method = RequestMethod.POST) - @ResponseBody + @PostMapping(value = "/otaPackage") public OtaPackageInfo saveOtaPackageInfo(@Parameter(description = "A JSON value representing the OTA Package.") - @RequestBody SaveOtaPackageInfoRequest otaPackageInfo) throws ThingsboardException { + @RequestBody SaveOtaPackageInfoRequest otaPackageInfo) throws Exception { otaPackageInfo.setTenantId(getTenantId()); checkEntity(otaPackageInfo.getId(), otaPackageInfo, Resource.OTA_PACKAGE); @@ -148,8 +143,7 @@ public class OtaPackageController extends BaseController { notes = "Update the OTA Package. Adds the date to the existing OTA Package Info" + TENANT_AUTHORITY_PARAGRAPH, requestBody = @io.swagger.v3.oas.annotations.parameters.RequestBody(content = @Content(mediaType = MULTIPART_FORM_DATA_VALUE))) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") - @RequestMapping(value = "/otaPackage/{otaPackageId}", method = RequestMethod.POST, consumes = MULTIPART_FORM_DATA_VALUE) - @ResponseBody + @PostMapping(value = "/otaPackage/{otaPackageId}", consumes = MULTIPART_FORM_DATA_VALUE) public OtaPackageInfo saveOtaPackageData(@Parameter(description = OTA_PACKAGE_ID_PARAM_DESCRIPTION) @PathVariable(OTA_PACKAGE_ID) String strOtaPackageId, @Parameter(description = "OTA Package checksum. For example, '0xd87f7e0c'") @@ -157,7 +151,7 @@ public class OtaPackageController extends BaseController { @Parameter(description = "OTA Package checksum algorithm.", schema = @Schema(allowableValues = {"MD5", "SHA256", "SHA384", "SHA512", "CRC32", "MURMUR3_32", "MURMUR3_128"})) @RequestParam(CHECKSUM_ALGORITHM) String checksumAlgorithmStr, @Parameter(description = "OTA Package data.") - @RequestPart MultipartFile file) throws ThingsboardException, IOException { + @RequestPart MultipartFile file) throws Exception { checkParameter(OTA_PACKAGE_ID, strOtaPackageId); checkParameter(CHECKSUM_ALGORITHM, checksumAlgorithmStr); OtaPackageId otaPackageId = new OtaPackageId(toUUID(strOtaPackageId)); @@ -172,8 +166,7 @@ public class OtaPackageController extends BaseController { notes = "Returns a page of OTA Package Info objects owned by tenant. " + PAGE_DATA_PARAMETERS + OTA_PACKAGE_INFO_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") - @RequestMapping(value = "/otaPackages", method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/otaPackages") public PageData getOtaPackages(@Parameter(description = PAGE_SIZE_DESCRIPTION, required = true) @RequestParam int pageSize, @Parameter(description = PAGE_NUMBER_DESCRIPTION, required = true) @@ -192,8 +185,7 @@ public class OtaPackageController extends BaseController { notes = "Returns a page of OTA Package Info objects owned by tenant. " + PAGE_DATA_PARAMETERS + OTA_PACKAGE_INFO_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") - @RequestMapping(value = "/otaPackages/{deviceProfileId}/{type}", method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/otaPackages/{deviceProfileId}/{type}") public PageData getOtaPackages(@Parameter(description = DEVICE_PROFILE_ID_PARAM_DESCRIPTION) @PathVariable("deviceProfileId") String strDeviceProfileId, @Parameter(description = "OTA Package type.", schema = @Schema(allowableValues = {"FIRMWARE", "SOFTWARE"})) @@ -219,8 +211,7 @@ public class OtaPackageController extends BaseController { notes = "Deletes the OTA Package. Referencing non-existing OTA Package Id will cause an error. " + "Can't delete the OTA Package if it is referenced by existing devices or device profile." + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") - @RequestMapping(value = "/otaPackage/{otaPackageId}", method = RequestMethod.DELETE) - @ResponseBody + @DeleteMapping(value = "/otaPackage/{otaPackageId}") public void deleteOtaPackage(@Parameter(description = OTA_PACKAGE_ID_PARAM_DESCRIPTION) @PathVariable("otaPackageId") String strOtaPackageId) throws ThingsboardException { checkParameter(OTA_PACKAGE_ID, strOtaPackageId); diff --git a/application/src/main/java/org/thingsboard/server/controller/RuleChainController.java b/application/src/main/java/org/thingsboard/server/controller/RuleChainController.java index 1674db8755..efe3e61893 100644 --- a/application/src/main/java/org/thingsboard/server/controller/RuleChainController.java +++ b/application/src/main/java/org/thingsboard/server/controller/RuleChainController.java @@ -15,7 +15,6 @@ */ package org.thingsboard.server.controller; -import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ArrayNode; @@ -23,16 +22,19 @@ import com.fasterxml.jackson.databind.node.ObjectNode; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.media.Schema; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.ObjectUtils; +import org.apache.commons.lang3.exception.ExceptionUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpStatus; import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestController; import org.thingsboard.common.util.JacksonUtil; @@ -155,9 +157,8 @@ public class RuleChainController extends BaseController { @ApiOperation(value = "Get Rule Chain (getRuleChainById)", notes = "Fetch the Rule Chain object based on the provided Rule Chain Id. " + RULE_CHAIN_DESCRIPTION + TENANT_AUTHORITY_PARAGRAPH) - @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") - @RequestMapping(value = "/ruleChain/{ruleChainId}", method = RequestMethod.GET) - @ResponseBody + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @GetMapping("/ruleChain/{ruleChainId}") public RuleChain getRuleChainById( @Parameter(description = RULE_CHAIN_ID_PARAM_DESCRIPTION) @PathVariable(RULE_CHAIN_ID) String strRuleChainId) throws ThingsboardException { @@ -169,9 +170,8 @@ public class RuleChainController extends BaseController { @ApiOperation(value = "Get Rule Chain output labels (getRuleChainOutputLabels)", notes = "Fetch the unique labels for the \"output\" Rule Nodes that belong to the Rule Chain based on the provided Rule Chain Id. " + RULE_CHAIN_DESCRIPTION + TENANT_AUTHORITY_PARAGRAPH) - @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") - @RequestMapping(value = "/ruleChain/{ruleChainId}/output/labels", method = RequestMethod.GET) - @ResponseBody + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @GetMapping("/ruleChain/{ruleChainId}/output/labels") public Set getRuleChainOutputLabels( @Parameter(description = RULE_CHAIN_ID_PARAM_DESCRIPTION) @PathVariable(RULE_CHAIN_ID) String strRuleChainId) throws ThingsboardException { @@ -184,9 +184,8 @@ public class RuleChainController extends BaseController { @ApiOperation(value = "Get output labels usage (getRuleChainOutputLabelsUsage)", notes = "Fetch the list of rule chains and the relation types (labels) they use to process output of the current rule chain based on the provided Rule Chain Id. " + RULE_CHAIN_DESCRIPTION + TENANT_AUTHORITY_PARAGRAPH) - @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") - @RequestMapping(value = "/ruleChain/{ruleChainId}/output/labels/usage", method = RequestMethod.GET) - @ResponseBody + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @GetMapping("/ruleChain/{ruleChainId}/output/labels/usage") public List getRuleChainOutputLabelsUsage( @Parameter(description = RULE_CHAIN_ID_PARAM_DESCRIPTION) @PathVariable(RULE_CHAIN_ID) String strRuleChainId) throws ThingsboardException { @@ -198,9 +197,8 @@ public class RuleChainController extends BaseController { @ApiOperation(value = "Get Rule Chain (getRuleChainById)", notes = "Fetch the Rule Chain Metadata object based on the provided Rule Chain Id. " + RULE_CHAIN_METADATA_DESCRIPTION + TENANT_AUTHORITY_PARAGRAPH) - @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") - @RequestMapping(value = "/ruleChain/{ruleChainId}/metadata", method = RequestMethod.GET) - @ResponseBody + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @GetMapping("/ruleChain/{ruleChainId}/metadata") public RuleChainMetaData getRuleChainMetaData( @Parameter(description = RULE_CHAIN_ID_PARAM_DESCRIPTION) @PathVariable(RULE_CHAIN_ID) String strRuleChainId) throws ThingsboardException { @@ -218,9 +216,8 @@ public class RuleChainController extends BaseController { "\n\n" + RULE_CHAIN_DESCRIPTION + "Remove 'id', 'tenantId' from the request body example (below) to create new Rule Chain entity." + TENANT_AUTHORITY_PARAGRAPH) - @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") - @RequestMapping(value = "/ruleChain", method = RequestMethod.POST) - @ResponseBody + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @PostMapping("/ruleChain") public RuleChain saveRuleChain( @Parameter(description = "A JSON value representing the rule chain.") @RequestBody RuleChain ruleChain) throws Exception { @@ -232,9 +229,8 @@ public class RuleChainController extends BaseController { @ApiOperation(value = "Create Default Rule Chain", notes = "Create rule chain from template, based on the specified name in the request. " + "Creates the rule chain based on the template that is used to create root rule chain. " + TENANT_AUTHORITY_PARAGRAPH) - @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") - @RequestMapping(value = "/ruleChain/device/default", method = RequestMethod.POST) - @ResponseBody + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @PostMapping("/ruleChain/device/default") public RuleChain saveRuleChain( @Parameter(description = "A JSON value representing the request.") @RequestBody DefaultRuleChainCreateRequest request) throws Exception { @@ -245,9 +241,8 @@ public class RuleChainController extends BaseController { @ApiOperation(value = "Set Root Rule Chain (setRootRuleChain)", notes = "Makes the rule chain to be root rule chain. Updates previous root rule chain as well. " + TENANT_AUTHORITY_PARAGRAPH) - @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") - @RequestMapping(value = "/ruleChain/{ruleChainId}/root", method = RequestMethod.POST) - @ResponseBody + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @PostMapping("/ruleChain/{ruleChainId}/root") public RuleChain setRootRuleChain( @Parameter(description = RULE_CHAIN_ID_PARAM_DESCRIPTION) @PathVariable(RULE_CHAIN_ID) String strRuleChainId) throws ThingsboardException { @@ -259,9 +254,8 @@ public class RuleChainController extends BaseController { @ApiOperation(value = "Update Rule Chain Metadata", notes = "Updates the rule chain metadata. " + RULE_CHAIN_METADATA_DESCRIPTION + TENANT_AUTHORITY_PARAGRAPH) - @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") - @RequestMapping(value = "/ruleChain/metadata", method = RequestMethod.POST) - @ResponseBody + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @PostMapping("/ruleChain/metadata") public RuleChainMetaData saveRuleChainMetaData( @Parameter(description = "A JSON value representing the rule chain metadata.") @RequestBody RuleChainMetaData ruleChainMetaData, @@ -284,8 +278,7 @@ public class RuleChainController extends BaseController { @ApiOperation(value = "Get Rule Chains (getRuleChains)", notes = "Returns a page of Rule Chains owned by tenant. " + RULE_CHAIN_DESCRIPTION + PAGE_DATA_PARAMETERS + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('TENANT_ADMIN')") - @RequestMapping(value = "/ruleChains", params = {"pageSize", "page"}, method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/ruleChains", params = {"pageSize", "page"}) public PageData getRuleChains( @Parameter(description = PAGE_SIZE_DESCRIPTION, required = true) @RequestParam int pageSize, @@ -302,7 +295,7 @@ public class RuleChainController extends BaseController { TenantId tenantId = getCurrentUser().getTenantId(); PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); RuleChainType type = RuleChainType.CORE; - if (typeStr != null && typeStr.trim().length() > 0) { + if (StringUtils.isNotBlank(typeStr)) { type = RuleChainType.valueOf(typeStr); } return checkNotNull(ruleChainService.findTenantRuleChainsByType(tenantId, type, pageLink)); @@ -311,9 +304,9 @@ public class RuleChainController extends BaseController { @ApiOperation(value = "Delete rule chain (deleteRuleChain)", notes = "Deletes the rule chain. Referencing non-existing rule chain Id will cause an error. " + "Referencing rule chain that is used in the device profiles will cause an error." + TENANT_AUTHORITY_PARAGRAPH) - @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") - @RequestMapping(value = "/ruleChain/{ruleChainId}", method = RequestMethod.DELETE) - @ResponseStatus(value = HttpStatus.OK) + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @DeleteMapping("/ruleChain/{ruleChainId}") + @ResponseStatus(HttpStatus.OK) public void deleteRuleChain( @Parameter(description = RULE_CHAIN_ID_PARAM_DESCRIPTION) @PathVariable(RULE_CHAIN_ID) String strRuleChainId) throws ThingsboardException { @@ -326,9 +319,8 @@ public class RuleChainController extends BaseController { @ApiOperation(value = "Get latest input message (getLatestRuleNodeDebugInput)", notes = "Gets the input message from the debug events for specified Rule Chain Id. " + "Referencing non-existing rule chain Id will cause an error. " + TENANT_AUTHORITY_PARAGRAPH) - @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") - @RequestMapping(value = "/ruleNode/{ruleNodeId}/debugIn", method = RequestMethod.GET) - @ResponseBody + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @GetMapping("/ruleNode/{ruleNodeId}/debugIn") public JsonNode getLatestRuleNodeDebugInput( @Parameter(description = RULE_NODE_ID_PARAM_DESCRIPTION) @PathVariable(RULE_NODE_ID) String strRuleNodeId) throws ThingsboardException { @@ -343,8 +335,7 @@ public class RuleChainController extends BaseController { @ApiOperation(value = "Is TBEL script executor enabled", notes = "Returns 'True' if the TBEL script execution is enabled" + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('TENANT_ADMIN')") - @RequestMapping(value = "/ruleChain/tbelEnabled", method = RequestMethod.GET) - @ResponseBody + @GetMapping("/ruleChain/tbelEnabled") public Boolean isTbelEnabled() { return tbelEnabled; } @@ -352,13 +343,12 @@ public class RuleChainController extends BaseController { @ApiOperation(value = "Test Script function", notes = TEST_SCRIPT_FUNCTION + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('TENANT_ADMIN')") - @RequestMapping(value = "/ruleChain/testScript", method = RequestMethod.POST) - @ResponseBody + @PostMapping("/ruleChain/testScript") public JsonNode testScript( @Parameter(description = "Script language: JS or TBEL") @RequestParam(required = false) ScriptLanguage scriptLang, @io.swagger.v3.oas.annotations.parameters.RequestBody(description = "Test JS request. See API call description above.") - @RequestBody JsonNode inputParams) throws ThingsboardException, JsonProcessingException { + @RequestBody JsonNode inputParams) { String script = inputParams.get("script").asText(); String scriptType = inputParams.get("scriptType").asText(); JsonNode argNamesJson = inputParams.get("argNames"); @@ -366,8 +356,7 @@ public class RuleChainController extends BaseController { String data = inputParams.get("msg").asText(); JsonNode metadataJson = inputParams.get("metadata"); - Map metadata = JacksonUtil.convertValue(metadataJson, new TypeReference>() { - }); + Map metadata = JacksonUtil.convertValue(metadataJson, new TypeReference<>() {}); String msgType = inputParams.get("msgType").asText(); String output = ""; String errorText = ""; @@ -384,55 +373,40 @@ public class RuleChainController extends BaseController { } engine = new RuleNodeTbelScriptEngine(getTenantId(), tbelInvokeService, script, argNames); } - TbMsg inMsg = TbMsg.newMsg() + + var inMsg = TbMsg.newMsg() .type(msgType) .copyMetaData(new TbMsgMetaData(metadata)) .dataType(TbMsgDataType.JSON) .data(data) .build(); - switch (scriptType) { - case "update": - output = msgToOutput(engine.executeUpdateAsync(inMsg).get(TIMEOUT, TimeUnit.SECONDS)); - break; - case "generate": - output = msgToOutput(engine.executeGenerateAsync(inMsg).get(TIMEOUT, TimeUnit.SECONDS)); - break; - case "filter": - boolean result = engine.executeFilterAsync(inMsg).get(TIMEOUT, TimeUnit.SECONDS); - output = Boolean.toString(result); - break; - case "switch": - Set states = engine.executeSwitchAsync(inMsg).get(TIMEOUT, TimeUnit.SECONDS); - output = JacksonUtil.toString(states); - break; - case "json": - JsonNode json = engine.executeJsonAsync(inMsg).get(TIMEOUT, TimeUnit.SECONDS); - output = JacksonUtil.toString(json); - break; - case "string": - output = engine.executeToStringAsync(inMsg).get(TIMEOUT, TimeUnit.SECONDS); - break; - default: - throw new IllegalArgumentException("Unsupported script type: " + scriptType); - } + + output = switch (scriptType) { + case "update" -> msgToOutput(engine.executeUpdateAsync(inMsg).get(TIMEOUT, TimeUnit.SECONDS)); + case "generate" -> msgToOutput(engine.executeGenerateAsync(inMsg).get(TIMEOUT, TimeUnit.SECONDS)); + case "filter" -> Boolean.toString(engine.executeFilterAsync(inMsg).get(TIMEOUT, TimeUnit.SECONDS)); + case "switch" -> JacksonUtil.toString(engine.executeSwitchAsync(inMsg).get(TIMEOUT, TimeUnit.SECONDS)); + case "json" -> JacksonUtil.toString(engine.executeJsonAsync(inMsg).get(TIMEOUT, TimeUnit.SECONDS)); + case "string" -> engine.executeToStringAsync(inMsg).get(TIMEOUT, TimeUnit.SECONDS); + default -> throw new IllegalArgumentException("Unsupported script type: " + scriptType); + }; } catch (Exception e) { log.error("Error evaluating JS function", e); - errorText = e.getMessage(); + Throwable rootCause = ExceptionUtils.getRootCause(e); + errorText = ObjectUtils.firstNonNull(rootCause.getMessage(), e.getMessage(), e.getClass().getSimpleName()); } finally { if (engine != null) { engine.destroy(); } } - ObjectNode result = JacksonUtil.newObjectNode(); - result.put("output", output); - result.put("error", errorText); - return result; + return JacksonUtil.newObjectNode() + .put("output", output) + .put("error", errorText); } @ApiOperation(value = "Export Rule Chains", notes = "Exports all tenant rule chains as one JSON." + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('TENANT_ADMIN')") - @RequestMapping(value = "/ruleChains/export", params = {"limit"}, method = RequestMethod.GET) - @ResponseBody + @GetMapping(value = "/ruleChains/export", params = {"limit"}) public RuleChainData exportRuleChains( @Parameter(description = "A limit of rule chains to export.", required = true) @RequestParam("limit") int limit) throws ThingsboardException { @@ -443,8 +417,7 @@ public class RuleChainController extends BaseController { @ApiOperation(value = "Import Rule Chains", notes = "Imports all tenant rule chains as one JSON." + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('TENANT_ADMIN')") - @RequestMapping(value = "/ruleChains/import", method = RequestMethod.POST) - @ResponseBody + @PostMapping("/ruleChains/import") public List importRuleChains( @Parameter(description = "A JSON value representing the rule chains.") @RequestBody RuleChainData ruleChainData, @@ -454,12 +427,12 @@ public class RuleChainController extends BaseController { return ruleChainService.importTenantRuleChains(tenantId, ruleChainData, overwrite, tbRuleChainService::updateRuleNodeConfiguration); } - private String msgToOutput(TbMsg msg) throws Exception { + private String msgToOutput(TbMsg msg) { JsonNode resultNode = convertMsgToOut(msg); return JacksonUtil.toString(resultNode); } - private String msgToOutput(List msgs) throws Exception { + private String msgToOutput(List msgs) { JsonNode resultNode; if (msgs.size() > 1) { resultNode = JacksonUtil.newArrayNode(); @@ -473,7 +446,7 @@ public class RuleChainController extends BaseController { return JacksonUtil.toString(resultNode); } - private JsonNode convertMsgToOut(TbMsg msg) throws Exception { + private JsonNode convertMsgToOut(TbMsg msg) { ObjectNode msgData = JacksonUtil.newObjectNode(); if (!StringUtils.isEmpty(msg.getData())) { msgData.set("msg", JacksonUtil.toJsonNode(msg.getData())); @@ -492,8 +465,7 @@ public class RuleChainController extends BaseController { "Third, once rule chain will be delivered to edge service, it's going to start processing messages locally. " + "\n\nOnly rule chain with type 'EDGE' can be assigned to edge." + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('TENANT_ADMIN')") - @RequestMapping(value = "/edge/{edgeId}/ruleChain/{ruleChainId}", method = RequestMethod.POST) - @ResponseBody + @PostMapping("/edge/{edgeId}/ruleChain/{ruleChainId}") public RuleChain assignRuleChainToEdge(@PathVariable("edgeId") String strEdgeId, @PathVariable(RULE_CHAIN_ID) String strRuleChainId) throws ThingsboardException { checkParameter("edgeId", strEdgeId); @@ -514,8 +486,7 @@ public class RuleChainController extends BaseController { EDGE_UNASSIGN_RECEIVE_STEP_DESCRIPTION + "Third, once 'unassign' command will be delivered to edge service, it's going to remove rule chain locally." + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('TENANT_ADMIN')") - @RequestMapping(value = "/edge/{edgeId}/ruleChain/{ruleChainId}", method = RequestMethod.DELETE) - @ResponseBody + @DeleteMapping("/edge/{edgeId}/ruleChain/{ruleChainId}") public RuleChain unassignRuleChainFromEdge(@PathVariable("edgeId") String strEdgeId, @PathVariable(RULE_CHAIN_ID) String strRuleChainId) throws ThingsboardException { checkParameter("edgeId", strEdgeId); @@ -530,9 +501,8 @@ public class RuleChainController extends BaseController { @ApiOperation(value = "Get Edge Rule Chains (getEdgeRuleChains)", notes = "Returns a page of Rule Chains assigned to the specified edge. " + RULE_CHAIN_DESCRIPTION + PAGE_DATA_PARAMETERS + TENANT_AUTHORITY_PARAGRAPH) - @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") - @RequestMapping(value = "/edge/{edgeId}/ruleChains", params = {"pageSize", "page"}, method = RequestMethod.GET) - @ResponseBody + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @GetMapping(value = "/edge/{edgeId}/ruleChains", params = {"pageSize", "page"}) public PageData getEdgeRuleChains( @Parameter(description = EDGE_ID_PARAM_DESCRIPTION, required = true) @PathVariable(EDGE_ID) String strEdgeId, @@ -557,9 +527,8 @@ public class RuleChainController extends BaseController { @ApiOperation(value = "Set Edge Template Root Rule Chain (setEdgeTemplateRootRuleChain)", notes = "Makes the rule chain to be root rule chain for any new edge that will be created. " + "Does not update root rule chain for already created edges. " + TENANT_AUTHORITY_PARAGRAPH) - @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") - @RequestMapping(value = "/ruleChain/{ruleChainId}/edgeTemplateRoot", method = RequestMethod.POST) - @ResponseBody + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @PostMapping("/ruleChain/{ruleChainId}/edgeTemplateRoot") public RuleChain setEdgeTemplateRootRuleChain(@Parameter(description = RULE_CHAIN_ID_PARAM_DESCRIPTION) @PathVariable(RULE_CHAIN_ID) String strRuleChainId) throws ThingsboardException { checkParameter(RULE_CHAIN_ID, strRuleChainId); @@ -572,8 +541,7 @@ public class RuleChainController extends BaseController { notes = "Makes the rule chain to be automatically assigned for any new edge that will be created. " + "Does not assign this rule chain for already created edges. " + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('TENANT_ADMIN')") - @RequestMapping(value = "/ruleChain/{ruleChainId}/autoAssignToEdge", method = RequestMethod.POST) - @ResponseBody + @PostMapping("/ruleChain/{ruleChainId}/autoAssignToEdge") public RuleChain setAutoAssignToEdgeRuleChain(@Parameter(description = RULE_CHAIN_ID_PARAM_DESCRIPTION) @PathVariable(RULE_CHAIN_ID) String strRuleChainId) throws ThingsboardException { checkParameter(RULE_CHAIN_ID, strRuleChainId); @@ -586,8 +554,7 @@ public class RuleChainController extends BaseController { notes = "Removes the rule chain from the list of rule chains that are going to be automatically assigned for any new edge that will be created. " + "Does not unassign this rule chain for already assigned edges. " + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('TENANT_ADMIN')") - @RequestMapping(value = "/ruleChain/{ruleChainId}/autoAssignToEdge", method = RequestMethod.DELETE) - @ResponseBody + @DeleteMapping("/ruleChain/{ruleChainId}/autoAssignToEdge") public RuleChain unsetAutoAssignToEdgeRuleChain(@Parameter(description = RULE_CHAIN_ID_PARAM_DESCRIPTION) @PathVariable(RULE_CHAIN_ID) String strRuleChainId) throws ThingsboardException { checkParameter(RULE_CHAIN_ID, strRuleChainId); @@ -599,9 +566,8 @@ public class RuleChainController extends BaseController { // TODO: @voba refactor this - add new config to edge rule chain to set it as auto-assign @ApiOperation(value = "Get Auto Assign To Edge Rule Chains (getAutoAssignToEdgeRuleChains)", notes = "Returns a list of Rule Chains that will be assigned to a newly created edge. " + RULE_CHAIN_DESCRIPTION + TENANT_AUTHORITY_PARAGRAPH) - @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") - @RequestMapping(value = "/ruleChain/autoAssignToEdgeRuleChains", method = RequestMethod.GET) - @ResponseBody + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @GetMapping("/ruleChain/autoAssignToEdgeRuleChains") public List getAutoAssignToEdgeRuleChains() throws ThingsboardException { TenantId tenantId = getCurrentUser().getTenantId(); List result = new ArrayList<>(); @@ -612,4 +578,5 @@ public class RuleChainController extends BaseController { } return checkNotNull(result); } + } 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..b9968aefa9 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,8 @@ public class SystemInfoController extends BaseController { } systemParams.setMaxArgumentsPerCF(tenantProfileConfiguration.getMaxArgumentsPerCF()); systemParams.setMaxDataPointsPerRollingArg(tenantProfileConfiguration.getMaxDataPointsPerRollingArg()); + systemParams.setMinAllowedScheduledUpdateIntervalInSecForCF(tenantProfileConfiguration.getMinAllowedScheduledUpdateIntervalInSecForCF()); + systemParams.setMaxRelationLevelPerCfArgument(tenantProfileConfiguration.getMaxRelationLevelPerCfArgument()); 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/TelemetryController.java b/application/src/main/java/org/thingsboard/server/controller/TelemetryController.java index bf9713f58e..c98ae0dcb8 100644 --- a/application/src/main/java/org/thingsboard/server/controller/TelemetryController.java +++ b/application/src/main/java/org/thingsboard/server/controller/TelemetryController.java @@ -33,11 +33,11 @@ import jakarta.annotation.PostConstruct; import jakarta.annotation.PreDestroy; 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; import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; @@ -65,25 +65,17 @@ import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.UUIDBased; 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.BaseDeleteTsKvQuery; import org.thingsboard.server.common.data.kv.BasicTsKvEntry; -import org.thingsboard.server.common.data.kv.BooleanDataEntry; import org.thingsboard.server.common.data.kv.DataType; import org.thingsboard.server.common.data.kv.DeleteTsKvQuery; -import org.thingsboard.server.common.data.kv.DoubleDataEntry; import org.thingsboard.server.common.data.kv.IntervalType; -import org.thingsboard.server.common.data.kv.JsonDataEntry; import org.thingsboard.server.common.data.kv.KvEntry; -import org.thingsboard.server.common.data.kv.LongDataEntry; -import org.thingsboard.server.common.data.kv.StringDataEntry; import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; import org.thingsboard.server.common.msg.rule.engine.DeviceAttributesEventNotificationMsg; import org.thingsboard.server.config.annotations.ApiOperation; import org.thingsboard.server.dao.timeseries.TimeseriesService; -import org.thingsboard.server.exception.InvalidParametersException; -import org.thingsboard.server.exception.UncheckedApiException; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.security.AccessValidator; import org.thingsboard.server.service.security.model.SecurityUser; @@ -156,9 +148,6 @@ public class TelemetryController extends BaseController { @Autowired private TbTelemetryService tbTelemetryService; - @Value("${transport.json.max_string_value_length:0}") - private int maxStringValueLength; - private ExecutorService executor; @PostConstruct @@ -314,10 +303,10 @@ public class TelemetryController extends BaseController { @Parameter(description = "A string value representing the timezone that will be used to calculate exact timestamps for 'WEEK', 'WEEK_ISO', 'MONTH' and 'QUARTER' interval types.") @RequestParam(name = "timeZone", required = false) String timeZone, @Parameter(description = "An integer value that represents a max number of time series data points to fetch." + - " This parameter is used only in the case if 'agg' parameter is set to 'NONE'.", schema = @Schema(defaultValue = "100")) + " This parameter is used only in the case if 'agg' parameter is set to 'NONE'.", schema = @Schema(defaultValue = "100")) @RequestParam(name = "limit", defaultValue = "100") Integer limit, @Parameter(description = "A string value representing the aggregation function. " + - "If the interval is not specified, 'agg' parameter will use 'NONE' value.", + "If the interval is not specified, 'agg' parameter will use 'NONE' value.", schema = @Schema(allowableValues = {"MIN", "MAX", "AVG", "SUM", "COUNT", "NONE"})) @RequestParam(name = "agg", defaultValue = "NONE") String aggStr, @Parameter(description = SORT_ORDER_DESCRIPTION, schema = @Schema(allowableValues = {"ASC", "DESC"})) @@ -337,20 +326,21 @@ public class TelemetryController extends BaseController { + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) @ApiResponses(value = { @ApiResponse(responseCode = "200", description = SAVE_ATTIRIBUTES_STATUS_OK + - "Platform creates an audit log event about device attributes updates with action type 'ATTRIBUTES_UPDATED', " + - "and also sends event msg to the rule engine with msg type 'ATTRIBUTES_UPDATED'."), + "Platform creates an audit log event about device attributes updates with action type 'ATTRIBUTES_UPDATED', " + + "and also sends event msg to the rule engine with msg type 'ATTRIBUTES_UPDATED'."), @ApiResponse(responseCode = "400", description = SAVE_ATTIRIBUTES_STATUS_BAD_REQUEST), @ApiResponse(responseCode = "401", description = "User is not authorized to save device attributes for selected device. Most likely, User belongs to different Customer or Tenant."), @ApiResponse(responseCode = "500", description = "The exception was thrown during processing the request. " + - "Platform creates an audit log event about device attributes updates with action type 'ATTRIBUTES_UPDATED' that includes an error stacktrace."), + "Platform creates an audit log event about device attributes updates with action type 'ATTRIBUTES_UPDATED' that includes an error stacktrace."), }) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") - @RequestMapping(value = "/{deviceId}/{scope}", method = RequestMethod.POST) - @ResponseBody - public DeferredResult saveDeviceAttributes( - @Parameter(description = DEVICE_ID_PARAM_DESCRIPTION, required = true) @PathVariable("deviceId") String deviceIdStr, - @Parameter(description = ATTRIBUTES_SCOPE_DESCRIPTION, schema = @Schema(allowableValues = {"SERVER_SCOPE", "SHARED_SCOPE"}, requiredMode = Schema.RequiredMode.REQUIRED)) @PathVariable("scope") AttributeScope scope, - @io.swagger.v3.oas.annotations.parameters.RequestBody(description = ATTRIBUTES_JSON_REQUEST_DESCRIPTION, required = true) @RequestBody JsonNode request) throws ThingsboardException { + @PostMapping(value = "/{deviceId}/{scope}") + public DeferredResult saveDeviceAttributes(@Parameter(description = DEVICE_ID_PARAM_DESCRIPTION, required = true) + @PathVariable("deviceId") String deviceIdStr, + @Parameter(description = ATTRIBUTES_SCOPE_DESCRIPTION, schema = @Schema(allowableValues = {"SERVER_SCOPE", "SHARED_SCOPE"}, requiredMode = Schema.RequiredMode.REQUIRED)) + @PathVariable("scope") AttributeScope scope, + @io.swagger.v3.oas.annotations.parameters.RequestBody(description = ATTRIBUTES_JSON_REQUEST_DESCRIPTION, required = true) + @RequestBody String request) throws ThingsboardException { EntityId entityId = EntityIdFactory.getByTypeAndUuid(EntityType.DEVICE, deviceIdStr); return saveAttributes(getTenantId(), entityId, scope, request); } @@ -367,13 +357,15 @@ public class TelemetryController extends BaseController { @ApiResponse(responseCode = "500", description = SAVE_ENTITY_ATTRIBUTES_STATUS_INTERNAL_SERVER_ERROR), }) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") - @RequestMapping(value = "/{entityType}/{entityId}/{scope}", method = RequestMethod.POST) - @ResponseBody - public DeferredResult saveEntityAttributesV1( - @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"})) @PathVariable("scope") AttributeScope scope, - @io.swagger.v3.oas.annotations.parameters.RequestBody(description = ATTRIBUTES_JSON_REQUEST_DESCRIPTION, required = true) @RequestBody JsonNode request) throws ThingsboardException { + @PostMapping(value = "/{entityType}/{entityId}/{scope}") + public DeferredResult saveEntityAttributesV1(@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"})) + @PathVariable("scope") AttributeScope scope, + @io.swagger.v3.oas.annotations.parameters.RequestBody(description = ATTRIBUTES_JSON_REQUEST_DESCRIPTION, required = true) + @RequestBody String request) throws ThingsboardException { EntityId entityId = EntityIdFactory.getByTypeAndId(entityType, entityIdStr); return saveAttributes(getTenantId(), entityId, scope, request); } @@ -390,13 +382,15 @@ public class TelemetryController extends BaseController { @ApiResponse(responseCode = "500", description = SAVE_ENTITY_ATTRIBUTES_STATUS_INTERNAL_SERVER_ERROR), }) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") - @RequestMapping(value = "/{entityType}/{entityId}/attributes/{scope}", method = RequestMethod.POST) - @ResponseBody - public DeferredResult saveEntityAttributesV2( - @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"}, requiredMode = Schema.RequiredMode.REQUIRED)) @PathVariable("scope") AttributeScope scope, - @io.swagger.v3.oas.annotations.parameters.RequestBody(description = ATTRIBUTES_JSON_REQUEST_DESCRIPTION, required = true) @RequestBody JsonNode request) throws ThingsboardException { + @PostMapping(value = "/{entityType}/{entityId}/attributes/{scope}") + public DeferredResult saveEntityAttributesV2(@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"}, requiredMode = Schema.RequiredMode.REQUIRED)) + @PathVariable("scope") AttributeScope scope, + @io.swagger.v3.oas.annotations.parameters.RequestBody(description = ATTRIBUTES_JSON_REQUEST_DESCRIPTION, required = true) + @RequestBody String request) throws ThingsboardException { EntityId entityId = EntityIdFactory.getByTypeAndId(entityType, entityIdStr); return saveAttributes(getTenantId(), entityId, scope, request); } @@ -460,11 +454,11 @@ public class TelemetryController extends BaseController { TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "Time series for the selected keys in the request was removed. " + - "Platform creates an audit log event about entity time series removal with action type 'TIMESERIES_DELETED'."), + "Platform creates an audit log event about entity time series removal with action type 'TIMESERIES_DELETED'."), @ApiResponse(responseCode = "400", description = "Platform returns a bad request in case if keys list is empty or start and end timestamp values is empty when deleteAllDataForKeys is set to false."), @ApiResponse(responseCode = "401", description = "User is not authorized to delete entity time series for selected entity. Most likely, User belongs to different Customer or Tenant."), @ApiResponse(responseCode = "500", description = "The exception was thrown during processing the request. " + - "Platform creates an audit log event about entity time series removal with action type 'TIMESERIES_DELETED' that includes an error stacktrace."), + "Platform creates an audit log event about entity time series removal with action type 'TIMESERIES_DELETED' that includes an error stacktrace."), }) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/{entityType}/{entityId}/timeseries/delete", method = RequestMethod.DELETE) @@ -541,11 +535,11 @@ public class TelemetryController extends BaseController { "Referencing a non-existing Device Id will cause an error" + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "Device attributes was removed for the selected keys in the request. " + - "Platform creates an audit log event about device attributes removal with action type 'ATTRIBUTES_DELETED'."), + "Platform creates an audit log event about device attributes removal with action type 'ATTRIBUTES_DELETED'."), @ApiResponse(responseCode = "400", description = "Platform returns a bad request in case if keys or scope are not specified."), @ApiResponse(responseCode = "401", description = "User is not authorized to delete device attributes for selected entity. Most likely, User belongs to different Customer or Tenant."), @ApiResponse(responseCode = "500", description = "The exception was thrown during processing the request. " + - "Platform creates an audit log event about device attributes removal with action type 'ATTRIBUTES_DELETED' that includes an error stacktrace."), + "Platform creates an audit log event about device attributes removal with action type 'ATTRIBUTES_DELETED' that includes an error stacktrace."), }) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/{deviceId}/{scope}", method = RequestMethod.DELETE) @@ -563,11 +557,11 @@ public class TelemetryController extends BaseController { INVALID_ENTITY_ID_OR_ENTITY_TYPE_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "Entity attributes was removed for the selected keys in the request. " + - "Platform creates an audit log event about entity attributes removal with action type 'ATTRIBUTES_DELETED'."), + "Platform creates an audit log event about entity attributes removal with action type 'ATTRIBUTES_DELETED'."), @ApiResponse(responseCode = "400", description = "Platform returns a bad request in case if keys or scope are not specified."), @ApiResponse(responseCode = "401", description = "User is not authorized to delete entity attributes for selected entity. Most likely, User belongs to different Customer or Tenant."), @ApiResponse(responseCode = "500", description = "The exception was thrown during processing the request. " + - "Platform creates an audit log event about entity attributes removal with action type 'ATTRIBUTES_DELETED' that includes an error stacktrace."), + "Platform creates an audit log event about entity attributes removal with action type 'ATTRIBUTES_DELETED' that includes an error stacktrace."), }) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/{entityType}/{entityId}/{scope}", method = RequestMethod.DELETE) @@ -616,18 +610,24 @@ public class TelemetryController extends BaseController { }); } - private DeferredResult saveAttributes(TenantId srcTenantId, EntityId entityIdSrc, AttributeScope scope, JsonNode json) throws ThingsboardException { + private DeferredResult saveAttributes(TenantId srcTenantId, EntityId entityIdSrc, AttributeScope scope, String jsonStr) throws ThingsboardException { if (AttributeScope.SERVER_SCOPE != scope && AttributeScope.SHARED_SCOPE != scope) { return getImmediateDeferredResult("Invalid scope: " + scope, HttpStatus.BAD_REQUEST); } - if (json.isObject()) { - List attributes = extractRequestAttributes(json); + JsonElement json; + try { + json = JsonParser.parseString(jsonStr); + } catch (Exception e) { + return getImmediateDeferredResult("Invalid JSON", HttpStatus.BAD_REQUEST); + } + if (json.isJsonObject()) { + List attributes = JsonConverter.convertToAttributes(json); if (attributes.isEmpty()) { return getImmediateDeferredResult("No attributes data found in request body!", HttpStatus.BAD_REQUEST); } for (AttributeKvEntry attributeKvEntry : attributes) { - if (attributeKvEntry.getKey().isEmpty() || attributeKvEntry.getKey().trim().length() == 0) { - return getImmediateDeferredResult("Key cannot be empty or contains only spaces", HttpStatus.BAD_REQUEST); + if (attributeKvEntry.getKey().isBlank()) { + return getImmediateDeferredResult("Key cannot be blank", HttpStatus.BAD_REQUEST); } } SecurityUser user = getCurrentUser(); @@ -885,43 +885,6 @@ public class TelemetryController extends BaseController { return result; } - private List extractRequestAttributes(JsonNode jsonNode) { - long ts = System.currentTimeMillis(); - List attributes = new ArrayList<>(); - jsonNode.fields().forEachRemaining(entry -> { - String key = entry.getKey(); - JsonNode value = entry.getValue(); - if (entry.getValue().isObject() || entry.getValue().isArray()) { - attributes.add(new BaseAttributeKvEntry(new JsonDataEntry(key, toJsonStr(value)), ts)); - } else if (entry.getValue().isTextual()) { - if (maxStringValueLength > 0 && entry.getValue().textValue().length() > maxStringValueLength) { - String message = String.format("String value length [%d] for key [%s] is greater than maximum allowed [%d]", entry.getValue().textValue().length(), key, maxStringValueLength); - throw new UncheckedApiException(new InvalidParametersException(message)); - } - attributes.add(new BaseAttributeKvEntry(new StringDataEntry(key, value.textValue()), ts)); - } else if (entry.getValue().isBoolean()) { - attributes.add(new BaseAttributeKvEntry(new BooleanDataEntry(key, value.booleanValue()), ts)); - } else if (entry.getValue().isDouble()) { - attributes.add(new BaseAttributeKvEntry(new DoubleDataEntry(key, value.doubleValue()), ts)); - } else if (entry.getValue().isNumber()) { - if (entry.getValue().isBigInteger()) { - throw new UncheckedApiException(new InvalidParametersException("Big integer values are not supported!")); - } else { - attributes.add(new BaseAttributeKvEntry(new LongDataEntry(key, value.longValue()), ts)); - } - } - }); - return attributes; - } - - private String toJsonStr(JsonNode value) { - try { - return JacksonUtil.toString(value); - } catch (IllegalArgumentException e) { - throw new JsonParseException("Can't parse jsonValue: " + value, e); - } - } - private JsonNode toJsonNode(String value) { try { return JacksonUtil.toJsonNode(value); diff --git a/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java b/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java index 5e5185ac8b..6765e95246 100644 --- a/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java +++ b/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java @@ -116,7 +116,6 @@ public class ThingsboardInstallService { entityDatabaseSchemaService.createDatabaseIndexes(); // TODO: cleanup update code after each release - systemDataLoaderService.updateDefaultNotificationConfigs(false); // Runs upgrade scripts that are not possible in plain SQL. dataUpdateService.updateData(); diff --git a/application/src/main/java/org/thingsboard/server/service/ai/AiChatModelService.java b/application/src/main/java/org/thingsboard/server/service/ai/AiChatModelService.java new file mode 100644 index 0000000000..9e00c8ddfd --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/ai/AiChatModelService.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.service.ai; + +import org.thingsboard.rule.engine.api.RuleEngineAiChatModelService; + +public interface AiChatModelService extends RuleEngineAiChatModelService {} 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 new file mode 100644 index 0000000000..d6252f57a6 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/ai/AiChatModelServiceImpl.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.service.ai; + +import com.google.common.util.concurrent.FluentFuture; +import dev.langchain4j.model.chat.ChatModel; +import dev.langchain4j.model.chat.request.ChatRequest; +import dev.langchain4j.model.chat.response.ChatResponse; +import lombok.RequiredArgsConstructor; +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; + +@Service +@RequiredArgsConstructor +class AiChatModelServiceImpl implements AiChatModelService { + + private final Langchain4jChatModelConfigurer chatModelConfigurer; + private final AiRequestsExecutor aiRequestsExecutor; + + @Override + public > FluentFuture sendChatRequestAsync(AiChatModelConfig chatModelConfig, ChatRequest chatRequest) { + ChatModel langChainChatModel = chatModelConfig.configure(chatModelConfigurer); + return aiRequestsExecutor.sendChatRequestAsync(langChainChatModel, chatRequest); + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/ai/AiRequestsExecutor.java b/application/src/main/java/org/thingsboard/server/service/ai/AiRequestsExecutor.java new file mode 100644 index 0000000000..75de36c36d --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/ai/AiRequestsExecutor.java @@ -0,0 +1,27 @@ +/** + * 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.ai; + +import com.google.common.util.concurrent.FluentFuture; +import dev.langchain4j.model.chat.ChatModel; +import dev.langchain4j.model.chat.request.ChatRequest; +import dev.langchain4j.model.chat.response.ChatResponse; + +public interface AiRequestsExecutor { + + FluentFuture sendChatRequestAsync(ChatModel chatModel, ChatRequest chatRequest); + +} diff --git a/application/src/main/java/org/thingsboard/server/service/ai/DefaultAiRequestsExecutor.java b/application/src/main/java/org/thingsboard/server/service/ai/DefaultAiRequestsExecutor.java new file mode 100644 index 0000000000..2d0121d30a --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/ai/DefaultAiRequestsExecutor.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.ai; + +import com.google.common.util.concurrent.FluentFuture; +import com.google.common.util.concurrent.ListeningExecutorService; +import com.google.common.util.concurrent.MoreExecutors; +import dev.langchain4j.model.chat.ChatModel; +import dev.langchain4j.model.chat.request.ChatRequest; +import dev.langchain4j.model.chat.response.ChatResponse; +import jakarta.annotation.PostConstruct; +import jakarta.annotation.PreDestroy; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; +import lombok.Data; +import lombok.RequiredArgsConstructor; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Component; +import org.springframework.validation.annotation.Validated; +import org.thingsboard.common.util.ThingsBoardThreadFactory; + +import java.time.Duration; +import java.util.concurrent.Executors; + +@Lazy +@Component +@RequiredArgsConstructor +class DefaultAiRequestsExecutor implements AiRequestsExecutor { + + private final AiRequestsExecutorProperties properties; + + @Data + @Validated + @Configuration + @ConfigurationProperties(prefix = "actors.rule.ai-requests-thread-pool") + private static class AiRequestsExecutorProperties { + + @NotBlank(message = "Pool name must be not blank") + private String poolName = "ai-requests"; + + @Min(value = 1, message = "Pool size must be at least 1") + private int poolSize = 50; + + @Min(value = 1, message = "Termination timeout must be at least 1 second") + private int terminationTimeoutSeconds = 60; + + } + + private ListeningExecutorService executorService; + + @PostConstruct + private void init() { + executorService = MoreExecutors.listeningDecorator( + Executors.newFixedThreadPool(properties.getPoolSize(), ThingsBoardThreadFactory.forName(properties.getPoolName())) + ); + } + + @Override + public FluentFuture sendChatRequestAsync(ChatModel chatModel, ChatRequest chatRequest) { + return FluentFuture.from(executorService.submit(() -> chatModel.chat(chatRequest))); + } + + @PreDestroy + private void destroy() { + if (executorService != null) { + MoreExecutors.shutdownAndAwaitTermination(executorService, Duration.ofSeconds(properties.getTerminationTimeoutSeconds())); + executorService = null; + } + } + +} 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 new file mode 100644 index 0000000000..69dd98f47f --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/ai/Langchain4jChatModelConfigurerImpl.java @@ -0,0 +1,269 @@ +/** + * 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.ai; + +import com.google.api.gax.core.FixedCredentialsProvider; +import com.google.api.gax.retrying.RetrySettings; +import com.google.auth.oauth2.ServiceAccountCredentials; +import com.google.cloud.vertexai.Transport; +import com.google.cloud.vertexai.VertexAI; +import com.google.cloud.vertexai.api.GenerationConfig; +import com.google.cloud.vertexai.api.PredictionServiceClient; +import com.google.cloud.vertexai.api.PredictionServiceSettings; +import com.google.cloud.vertexai.generativeai.GenerativeModel; +import dev.langchain4j.model.anthropic.AnthropicChatModel; +import dev.langchain4j.model.azure.AzureOpenAiChatModel; +import dev.langchain4j.model.bedrock.BedrockChatModel; +import dev.langchain4j.model.chat.ChatModel; +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.openai.OpenAiChatModel; +import dev.langchain4j.model.vertexai.gemini.VertexAiGeminiChatModel; +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; +import org.thingsboard.server.common.data.ai.model.chat.AzureOpenAiChatModelConfig; +import org.thingsboard.server.common.data.ai.model.chat.GitHubModelsChatModelConfig; +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.Langchain4jChatModelConfigurer; +import org.thingsboard.server.common.data.ai.model.chat.MistralAiChatModelConfig; +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 software.amazon.awssdk.auth.credentials.AwsBasicCredentials; +import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.bedrockruntime.BedrockRuntimeClient; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.time.Duration; + +@Component +class Langchain4jChatModelConfigurerImpl implements Langchain4jChatModelConfigurer { + + @Override + public ChatModel configureChatModel(OpenAiChatModelConfig chatModelConfig) { + return OpenAiChatModel.builder() + .apiKey(chatModelConfig.providerConfig().apiKey()) + .modelName(chatModelConfig.modelId()) + .temperature(chatModelConfig.temperature()) + .topP(chatModelConfig.topP()) + .frequencyPenalty(chatModelConfig.frequencyPenalty()) + .presencePenalty(chatModelConfig.presencePenalty()) + .maxTokens(chatModelConfig.maxOutputTokens()) + .timeout(toDuration(chatModelConfig.timeoutSeconds())) + .maxRetries(chatModelConfig.maxRetries()) + .build(); + } + + @Override + public ChatModel configureChatModel(AzureOpenAiChatModelConfig chatModelConfig) { + AzureOpenAiProviderConfig providerConfig = chatModelConfig.providerConfig(); + return AzureOpenAiChatModel.builder() + .endpoint(providerConfig.endpoint()) + .serviceVersion(providerConfig.serviceVersion()) + .apiKey(providerConfig.apiKey()) + .deploymentName(chatModelConfig.modelId()) + .temperature(chatModelConfig.temperature()) + .topP(chatModelConfig.topP()) + .frequencyPenalty(chatModelConfig.frequencyPenalty()) + .presencePenalty(chatModelConfig.presencePenalty()) + .maxTokens(chatModelConfig.maxOutputTokens()) + .timeout(toDuration(chatModelConfig.timeoutSeconds())) + .maxRetries(chatModelConfig.maxRetries()) + .build(); + } + + @Override + public ChatModel configureChatModel(GoogleAiGeminiChatModelConfig chatModelConfig) { + return GoogleAiGeminiChatModel.builder() + .apiKey(chatModelConfig.providerConfig().apiKey()) + .modelName(chatModelConfig.modelId()) + .temperature(chatModelConfig.temperature()) + .topP(chatModelConfig.topP()) + .topK(chatModelConfig.topK()) + .frequencyPenalty(chatModelConfig.frequencyPenalty()) + .presencePenalty(chatModelConfig.presencePenalty()) + .maxOutputTokens(chatModelConfig.maxOutputTokens()) + .timeout(toDuration(chatModelConfig.timeoutSeconds())) + .maxRetries(chatModelConfig.maxRetries()) + .build(); + } + + @Override + public ChatModel configureChatModel(GoogleVertexAiGeminiChatModelConfig chatModelConfig) { + GoogleVertexAiGeminiProviderConfig providerConfig = chatModelConfig.providerConfig(); + + // construct service account credentials using service account key JSON + ServiceAccountCredentials serviceAccountCredentials; + try { + serviceAccountCredentials = ServiceAccountCredentials.fromStream(new ByteArrayInputStream(providerConfig.serviceAccountKey().getBytes())); + } catch (IOException e) { + throw new RuntimeException("Failed to parse service account key JSON", e); + } + + PredictionServiceSettings predictionServiceClientSettings; + try { + // create prediction service settings for REST transport with service account key credentials + PredictionServiceSettings.Builder settingsBuilder = PredictionServiceSettings.newHttpJsonBuilder() + .setCredentialsProvider(FixedCredentialsProvider.create(serviceAccountCredentials)); + + // get the retry settings that control request timeout for generateContent RPC + RetrySettings.Builder retrySettings = settingsBuilder + .generateContentSettings() + .getRetrySettings() + .toBuilder(); + + // set request timeout from model config + if (chatModelConfig.timeoutSeconds() != null) { + retrySettings.setTotalTimeout(org.threeten.bp.Duration.ofSeconds(chatModelConfig.timeoutSeconds())); + } + + // set updated retry settings + settingsBuilder.generateContentSettings().setRetrySettings(retrySettings.build()); + + // build the client settings + predictionServiceClientSettings = settingsBuilder.build(); + } catch (IOException e) { + throw new RuntimeException("Failed to create prediction service client settings", e); + } + + // construct Vertex AI instance + var vertexAI = new VertexAI.Builder() + .setProjectId(providerConfig.projectId()) + .setLocation(providerConfig.location()) + .setPredictionClientSupplier(() -> createPredictionServiceClient(predictionServiceClientSettings)) + .setTransport(Transport.REST) // GRPC also possible, but likely does not work with service account keys + .build(); + + // map model config to generation config + var generationConfigBuilder = GenerationConfig.newBuilder(); + if (chatModelConfig.temperature() != null) { + generationConfigBuilder.setTemperature(chatModelConfig.temperature().floatValue()); + } + if (chatModelConfig.topP() != null) { + generationConfigBuilder.setTopP(chatModelConfig.topP().floatValue()); + } + if (chatModelConfig.topK() != null) { + generationConfigBuilder.setTopK(chatModelConfig.topK()); + } + if (chatModelConfig.frequencyPenalty() != null) { + generationConfigBuilder.setFrequencyPenalty(chatModelConfig.frequencyPenalty().floatValue()); + } + if (chatModelConfig.frequencyPenalty() != null) { + generationConfigBuilder.setPresencePenalty(chatModelConfig.frequencyPenalty().floatValue()); + } + if (chatModelConfig.maxOutputTokens() != null) { + generationConfigBuilder.setMaxOutputTokens(chatModelConfig.maxOutputTokens()); + } + var generationConfig = generationConfigBuilder.build(); + + // construct generative model instance + var generativeModel = new GenerativeModel(chatModelConfig.modelId(), vertexAI).withGenerationConfig(generationConfig); + + return new VertexAiGeminiChatModel(generativeModel, generationConfig, chatModelConfig.maxRetries()); + } + + private static PredictionServiceClient createPredictionServiceClient(PredictionServiceSettings settings) { + try { + return PredictionServiceClient.create(settings); + } catch (IOException e) { + throw new RuntimeException("Failed to create prediction service client", e); + } + } + + @Override + public ChatModel configureChatModel(MistralAiChatModelConfig chatModelConfig) { + return MistralAiChatModel.builder() + .apiKey(chatModelConfig.providerConfig().apiKey()) + .modelName(chatModelConfig.modelId()) + .temperature(chatModelConfig.temperature()) + .topP(chatModelConfig.topP()) + .frequencyPenalty(chatModelConfig.frequencyPenalty()) + .presencePenalty(chatModelConfig.presencePenalty()) + .maxTokens(chatModelConfig.maxOutputTokens()) + .timeout(toDuration(chatModelConfig.timeoutSeconds())) + .maxRetries(chatModelConfig.maxRetries()) + .build(); + } + + @Override + public ChatModel configureChatModel(AnthropicChatModelConfig chatModelConfig) { + return AnthropicChatModel.builder() + .apiKey(chatModelConfig.providerConfig().apiKey()) + .modelName(chatModelConfig.modelId()) + .temperature(chatModelConfig.temperature()) + .topP(chatModelConfig.topP()) + .topK(chatModelConfig.topK()) + .maxTokens(chatModelConfig.maxOutputTokens()) + .timeout(toDuration(chatModelConfig.timeoutSeconds())) + .maxRetries(chatModelConfig.maxRetries()) + .build(); + } + + @Override + public ChatModel configureChatModel(AmazonBedrockChatModelConfig chatModelConfig) { + AmazonBedrockProviderConfig providerConfig = chatModelConfig.providerConfig(); + + var credentialsProvider = StaticCredentialsProvider.create( + AwsBasicCredentials.create(providerConfig.accessKeyId(), providerConfig.secretAccessKey()) + ); + + var bedrockClient = BedrockRuntimeClient.builder() + .region(Region.of(providerConfig.region())) + .credentialsProvider(credentialsProvider) + .build(); + + var defaultChatRequestParams = ChatRequestParameters.builder() + .temperature(chatModelConfig.temperature()) + .topP(chatModelConfig.topP()) + .maxOutputTokens(chatModelConfig.maxOutputTokens()) + .build(); + + return BedrockChatModel.builder() + .client(bedrockClient) + .modelId(chatModelConfig.modelId()) + .defaultRequestParameters(defaultChatRequestParams) + .timeout(toDuration(chatModelConfig.timeoutSeconds())) + .maxRetries(chatModelConfig.maxRetries()) + .build(); + } + + @Override + public ChatModel configureChatModel(GitHubModelsChatModelConfig chatModelConfig) { + return GitHubModelsChatModel.builder() + .gitHubToken(chatModelConfig.providerConfig().personalAccessToken()) + .modelName(chatModelConfig.modelId()) + .temperature(chatModelConfig.temperature()) + .topP(chatModelConfig.topP()) + .frequencyPenalty(chatModelConfig.frequencyPenalty()) + .presencePenalty(chatModelConfig.presencePenalty()) + .maxTokens(chatModelConfig.maxOutputTokens()) + .timeout(toDuration(chatModelConfig.timeoutSeconds())) + .maxRetries(chatModelConfig.maxRetries()) + .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/cf/AbstractCalculatedFieldProcessingService.java b/application/src/main/java/org/thingsboard/server/service/cf/AbstractCalculatedFieldProcessingService.java new file mode 100644 index 0000000000..45305ca9e3 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/cf/AbstractCalculatedFieldProcessingService.java @@ -0,0 +1,257 @@ +/** + * 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.configuration.Argument; +import org.thingsboard.server.common.data.cf.configuration.ArgumentType; +import org.thingsboard.server.common.data.cf.configuration.RelationQueryDynamicSourceConfiguration; +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.RelationTypeGroup; +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.CalculatedFieldState; + +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.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.utils.CalculatedFieldArgumentUtils.createDefaultKvEntry; +import static org.thingsboard.server.utils.CalculatedFieldArgumentUtils.createStateByType; +import static org.thingsboard.server.utils.CalculatedFieldArgumentUtils.transformSingleValueArgument; + +@Data +@Slf4j +public abstract class AbstractCalculatedFieldProcessingService { + + protected final AttributesService attributesService; + protected final TimeseriesService timeseriesService; + protected final ApiLimitService apiLimitService; + protected final RelationService relationService; + + 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(); + + public ListenableFuture fetchStateFromDb(CalculatedFieldCtx ctx, EntityId entityId) { + Map> argFutures = switch (ctx.getCalculatedField().getType()) { + case GEOFENCING -> fetchGeofencingCalculatedFieldArguments(ctx, entityId, false); + case SIMPLE, SCRIPT -> { + Map> futures = new HashMap<>(); + for (var entry : ctx.getArguments().entrySet()) { + var argEntityId = resolveEntityId(entityId, entry.getValue()); + var argValueFuture = fetchArgumentValue(ctx.getTenantId(), argEntityId, entry.getValue(), System.currentTimeMillis()); + futures.put(entry.getKey(), argValueFuture); + } + yield futures; + } + }; + return Futures.whenAllComplete(argFutures.values()).call(() -> { + var result = createStateByType(ctx); + result.updateState(ctx, resolveArgumentFutures(argFutures)); + return result; + }, MoreExecutors.directExecutor()); + } + + protected EntityId resolveEntityId(EntityId entityId, Argument argument) { + return argument.getRefEntityId() != null ? argument.getRefEntityId() : entityId; + } + + protected Map resolveArgumentFutures(Map> argFutures) { + return argFutures.entrySet().stream() + .collect(Collectors.toMap( + Map.Entry::getKey, // Keep the key as is + entry -> { + try { + return entry.getValue().get(); + } catch (ExecutionException e) { + Throwable cause = e.getCause(); + throw new RuntimeException("Failed to fetch " + entry.getKey() + ": " + cause.getMessage(), cause); + } catch (InterruptedException e) { + throw new RuntimeException("Failed to fetch" + entry.getKey(), e); + } + } + )); + } + + protected Map> fetchGeofencingCalculatedFieldArguments(CalculatedFieldCtx ctx, EntityId entityId, boolean dynamicArgumentsOnly) { + Map> argFutures = new HashMap<>(); + Set> entries = ctx.getArguments().entrySet(); + if (dynamicArgumentsOnly) { + entries = entries.stream() + .filter(entry -> entry.getValue().hasDynamicSource()) + .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(), System.currentTimeMillis())); + 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; + } + + 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)); + } + var refDynamicSourceConfiguration = value.getRefDynamicSourceConfiguration(); + return switch (refDynamicSourceConfiguration.getType()) { + case RELATION_QUERY -> { + var configuration = (RelationQueryDynamicSourceConfiguration) refDynamicSourceConfiguration; + if (configuration.isSimpleRelation()) { + yield switch (configuration.getDirection()) { + case FROM -> + Futures.transform(relationService.findByFromAndTypeAsync(tenantId, entityId, configuration.getRelationType(), RelationTypeGroup.COMMON), + configuration::resolveEntityIds, calculatedFieldCallbackExecutor); + case TO -> + Futures.transform(relationService.findByToAndTypeAsync(tenantId, entityId, configuration.getRelationType(), RelationTypeGroup.COMMON), + configuration::resolveEntityIds, calculatedFieldCallbackExecutor); + }; + } + yield Futures.transform(relationService.findByQuery(tenantId, configuration.toEntityRelationsQuery(entityId)), + configuration::resolveEntityIds, calculatedFieldCallbackExecutor); + } + }; + } + + 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(() -> + new BaseAttributeKvEntry(createDefaultKvEntry(argument), System.currentTimeMillis(), 0L))), + 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()); + } + + 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); + }; + } + + 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); + + 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 ArgumentEntry.createTsRollingArgument(tsRolling, query.getLimit(), argTimeWindow); + }, calculatedFieldCallbackExecutor); + } + + 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, 0L)); + return transformSingleValueArgument(Optional.of(attributeKvEntry)); + }, calculatedFieldCallbackExecutor); + } + + protected ListenableFuture fetchTsLatest(TenantId tenantId, EntityId entityId, Argument argument, long startTs) { + 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(System.currentTimeMillis(), createDefaultKvEntry(argument), 0L))); + }, 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/CalculatedFieldProcessingService.java b/application/src/main/java/org/thingsboard/server/service/cf/CalculatedFieldProcessingService.java index 847caccaff..86ed174485 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 @@ -34,6 +34,8 @@ public interface CalculatedFieldProcessingService { ListenableFuture fetchStateFromDb(CalculatedFieldCtx ctx, EntityId entityId); + Map fetchDynamicArgsFromDb(CalculatedFieldCtx ctx, EntityId entityId); + Map fetchArgsFromDb(TenantId tenantId, EntityId entityId, Map arguments); void pushMsgToRuleEngine(TenantId tenantId, EntityId entityId, CalculatedFieldResult calculationResult, List cfIds, 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..c779c27419 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 @@ -34,4 +34,8 @@ public final class CalculatedFieldResult { (result.isTextual() && result.asText().isEmpty()); } + public String toStringOrElseNull() { + return result == null ? null : result.toString(); + } + } 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..dfe30a0e55 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 @@ -19,11 +19,9 @@ import lombok.Getter; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; 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.cf.CalculatedField; import org.thingsboard.server.common.data.cf.CalculatedFieldLink; import org.thingsboard.server.common.data.cf.configuration.CalculatedFieldConfiguration; @@ -31,9 +29,8 @@ 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.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.relation.RelationService; import org.thingsboard.server.dao.usagerecord.ApiLimitService; import org.thingsboard.server.queue.util.AfterStartUp; import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx; @@ -56,8 +53,7 @@ public class DefaultCalculatedFieldCache implements CalculatedFieldCache { private final CalculatedFieldService calculatedFieldService; private final TbelInvokeService tbelInvokeService; private final ApiLimitService apiLimitService; - @Lazy - private final ActorSystemContext actorSystemContext; + private final RelationService relationService; private final ConcurrentMap calculatedFields = new ConcurrentHashMap<>(); private final ConcurrentMap> entityIdCalculatedFields = new ConcurrentHashMap<>(); @@ -75,7 +71,6 @@ public class DefaultCalculatedFieldCache implements CalculatedFieldCache { cfs.forEach(cf -> { if (cf != null) { calculatedFields.putIfAbsent(cf.getId(), cf); - actorSystemContext.tell(new CalculatedFieldInitMsg(cf.getTenantId(), cf)); } }); calculatedFields.values().forEach(cf -> { @@ -84,7 +79,6 @@ public class DefaultCalculatedFieldCache implements CalculatedFieldCache { 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) @@ -119,7 +113,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, tbelInvokeService, apiLimitService, relationService); calculatedFieldsCtx.put(calculatedFieldId, ctx); log.debug("[{}] Put calculated field ctx into cache: {}", calculatedFieldId, ctx); } 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..17dca5dd64 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,28 @@ */ 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.CalculatedFieldType; import org.thingsboard.server.common.data.cf.configuration.Argument; 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.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; @@ -70,20 +52,12 @@ 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 java.util.ArrayList; 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.utils.CalculatedFieldUtils.toProto; @@ -91,76 +65,53 @@ 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; - - @PostConstruct - public void init() { - calculatedFieldCallbackExecutor = MoreExecutors.listeningDecorator(ThingsBoardExecutors.newWorkStealingPool( - Math.max(4, Runtime.getRuntime().availableProcessors()), "calculated-field-callback")); + public DefaultCalculatedFieldProcessingService(AttributesService attributesService, + TimeseriesService timeseriesService, + ApiLimitService apiLimitService, + RelationService relationService, + TbClusterService clusterService, + PartitionService partitionService) { + super(attributesService, timeseriesService, apiLimitService, relationService); + this.clusterService = clusterService; + this.partitionService = partitionService; } - @PreDestroy - public void stop() { - if (calculatedFieldCallbackExecutor != null) { - calculatedFieldCallbackExecutor.shutdownNow(); - } + @Override + protected String getExecutorNamePrefix() { + return "calculated-field-callback"; } @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); + return super.fetchStateFromDb(ctx, entityId); + } + + @Override + public Map fetchDynamicArgsFromDb(CalculatedFieldCtx ctx, EntityId entityId) { + // only geofencing calculated fields supports dynamic arguments scheduled updates + if (!ctx.getCalculatedField().getType().equals(CalculatedFieldType.GEOFENCING)) { + return Map.of(); } - 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); + return resolveArgumentFutures(fetchGeofencingCalculatedFieldArguments(ctx, entityId, true)); } @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().hasDynamicSource()) { + continue; + } + var argEntityId = resolveEntityId(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 @@ -169,7 +120,7 @@ public class DefaultCalculatedFieldProcessingService implements CalculatedFieldP 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(); + TbMsg msg = TbMsg.newMsg().type(msgType).originator(entityId).previousCalculatedFieldIds(cfIds).metaData(md).data(calculatedFieldResult.toStringOrElseNull()).build(); clusterService.pushMsgToRuleEngine(tenantId, entityId, msg, new TbQueueCallback() { @Override public void onSuccess(TbQueueMsgMetadata metadata) { @@ -241,69 +192,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; 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..2d43883131 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 @@ -19,10 +19,13 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonTypeInfo; 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.geofencing.GeofencingArgumentEntry; import java.util.List; +import java.util.Map; @JsonTypeInfo( use = JsonTypeInfo.Id.NAME, @@ -31,7 +34,8 @@ 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") }) public interface ArgumentEntry { @@ -58,4 +62,8 @@ public interface ArgumentEntry { return new TsRollingArgumentEntry(kvEntries, limit, timeWindow); } + static ArgumentEntry createGeofencingValueArgument(Map entityIdkvEntryMap) { + return new GeofencingArgumentEntry(entityIdkvEntryMap); + } + } 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..876bfa2a3f 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 } 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..9a1d06cf24 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 @@ -25,8 +25,6 @@ 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 { @@ -95,19 +93,7 @@ public abstract class BaseCalculatedFieldState implements CalculatedFieldState { } } - @Override - public void checkArgumentSize(String name, ArgumentEntry entry, CalculatedFieldCtx ctx) { - if (entry instanceof TsRollingArgumentEntry) { - 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."); - } - } - } - - protected abstract void validateNewEntry(ArgumentEntry newEntry); + protected void validateNewEntry(ArgumentEntry newEntry) {} private void updateLastUpdateTimestamp(ArgumentEntry entry) { long newTs = this.latestTimestamp; 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..c2cc083853 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 @@ -25,10 +25,13 @@ 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.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.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.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; @@ -36,6 +39,7 @@ import org.thingsboard.server.common.data.kv.AttributeKvEntry; import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; import org.thingsboard.server.common.util.ProtoUtils; +import org.thingsboard.server.dao.relation.RelationService; import org.thingsboard.server.dao.usagerecord.ApiLimitService; import org.thingsboard.server.gen.transport.TransportProtos.CalculatedFieldTelemetryMsgProto; import org.thingsboard.server.service.cf.ctx.CalculatedFieldEntityCtxId; @@ -64,6 +68,7 @@ public class CalculatedFieldCtx { private String expression; private boolean useLatestTs; private TbelInvokeService tbelInvokeService; + private RelationService relationService; private CalculatedFieldScriptEngine calculatedFieldScriptEngine; private ThreadLocal customExpression; @@ -73,31 +78,56 @@ public class CalculatedFieldCtx { private long maxStateSize; private long maxSingleValueArgumentSize; - public CalculatedFieldCtx(CalculatedField calculatedField, TbelInvokeService tbelInvokeService, ApiLimitService apiLimitService) { + private List mainEntityGeofencingArgumentNames; + private List linkedEntityGeofencingArgumentNames; + + public CalculatedFieldCtx(CalculatedField calculatedField, TbelInvokeService tbelInvokeService, ApiLimitService apiLimitService, RelationService relationService) { 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.argNames = new ArrayList<>(); + this.mainEntityGeofencingArgumentNames = new ArrayList<>(); + this.linkedEntityGeofencingArgumentNames = 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 && entry.getValue().hasDynamicSource()) { + continue; + } + if (refId == null || refId.equals(calculatedField.getEntityId())) { + mainEntityArguments.put(refKey, entry.getKey()); + } else { + linkedEntityArguments.computeIfAbsent(refId, key -> new HashMap<>()).put(refKey, entry.getKey()); + } + } + this.argNames.addAll(arguments.keySet()); + 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)) { + linkedEntityGeofencingArgumentNames.add(zoneGroupName); + } + }); } } - 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; + this.relationService = relationService; this.maxDataPointsPerRollingArg = apiLimitService.getLimit(tenantId, DefaultTenantProfileConfiguration::getMaxDataPointsPerRollingArg); this.maxStateSize = apiLimitService.getLimit(tenantId, DefaultTenantProfileConfiguration::getMaxStateSizeInKBytes) * 1024; @@ -105,25 +135,29 @@ public class CalculatedFieldCtx { } public void init() { - if (CalculatedFieldType.SCRIPT.equals(cfType)) { - try { - this.calculatedFieldScriptEngine = initEngine(tenantId, expression, tbelInvokeService); - initialized = true; - } catch (Exception e) { - throw new RuntimeException("Failed to init calculated field ctx. Invalid expression syntax.", e); + switch (cfType) { + case SCRIPT -> { + try { + this.calculatedFieldScriptEngine = initEngine(tenantId, expression, tbelInvokeService); + 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() - ); - initialized = true; - } else { - throw new RuntimeException("Failed to init calculated field ctx. Invalid expression syntax."); + case GEOFENCING -> initialized = true; + case SIMPLE -> { + if (isValidExpression(expression)) { + this.customExpression = ThreadLocal.withInitial(() -> + new ExpressionBuilder(expression) + .functions(userDefinedFunctions) + .implicitMultiplication(true) + .variables(this.arguments.keySet()) + .build() + ); + initialized = true; + } else { + throw new RuntimeException("Failed to init calculated field ctx. Invalid expression syntax."); + } } } } @@ -293,7 +327,7 @@ public class CalculatedFieldCtx { } public boolean hasOtherSignificantChanges(CalculatedFieldCtx other) { - boolean expressionChanged = !expression.equals(other.expression); + boolean expressionChanged = calculatedField.getConfiguration() instanceof ExpressionBasedCalculatedFieldConfiguration && !expression.equals(other.expression); boolean outputChanged = !output.equals(other.output); return expressionChanged || outputChanged; } @@ -304,6 +338,16 @@ public class CalculatedFieldCtx { return typeChanged || argumentsChanged; } + public boolean hasSchedulingConfigChanges(CalculatedFieldCtx other) { + if (calculatedField.getConfiguration() instanceof ScheduledUpdateSupportedCalculatedFieldConfiguration thisConfig + && other.calculatedField.getConfiguration() instanceof ScheduledUpdateSupportedCalculatedFieldConfiguration otherConfig) { + boolean refreshTriggerChanged = thisConfig.isScheduledUpdateEnabled() != otherConfig.isScheduledUpdateEnabled(); + boolean refreshIntervalChanged = thisConfig.getScheduledUpdateInterval() != otherConfig.getScheduledUpdateInterval(); + return refreshTriggerChanged || refreshIntervalChanged; + } + return false; + } + public String getSizeExceedsLimitMessage() { return "Failed to init CF state. State size exceeds limit of " + (maxStateSize / 1024) + "Kb!"; } 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..5f8e7538c4 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 @@ -20,12 +20,17 @@ import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonTypeInfo; import com.google.common.util.concurrent.ListenableFuture; import org.thingsboard.server.common.data.cf.CalculatedFieldType; +import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.service.cf.CalculatedFieldResult; import org.thingsboard.server.service.cf.ctx.CalculatedFieldEntityCtxId; +import org.thingsboard.server.service.cf.ctx.state.geofencing.GeofencingArgumentEntry; +import org.thingsboard.server.service.cf.ctx.state.geofencing.GeofencingCalculatedFieldState; import java.util.List; import java.util.Map; +import static org.thingsboard.server.utils.CalculatedFieldUtils.toSingleValueArgumentProto; + @JsonTypeInfo( use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, @@ -34,6 +39,7 @@ import java.util.Map; @JsonSubTypes({ @JsonSubTypes.Type(value = SimpleCalculatedFieldState.class, name = "SIMPLE"), @JsonSubTypes.Type(value = ScriptCalculatedFieldState.class, name = "SCRIPT"), + @JsonSubTypes.Type(value = GeofencingCalculatedFieldState.class, name = "GEOFENCING"), }) public interface CalculatedFieldState { @@ -44,11 +50,18 @@ public interface CalculatedFieldState { long getLatestTimestamp(); + default void setDirty(boolean dirty) { + } + + default boolean isDirty() { + return false; + } + void setRequiredArguments(List requiredArguments); boolean updateState(CalculatedFieldCtx ctx, Map argumentValues); - ListenableFuture performCalculation(CalculatedFieldCtx ctx); + ListenableFuture performCalculation(EntityId entityId, CalculatedFieldCtx ctx); @JsonIgnore boolean isReady(); @@ -62,6 +75,15 @@ public interface CalculatedFieldState { 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."); + } + } + } } 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..fe7dfa04d0 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 @@ -20,6 +20,7 @@ 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.EqualsAndHashCode; import lombok.NoArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.thingsboard.script.api.tbel.TbelCfArg; @@ -27,6 +28,7 @@ import org.thingsboard.script.api.tbel.TbelCfCtx; import org.thingsboard.script.api.tbel.TbelCfSingleValueArg; 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 java.util.ArrayList; @@ -37,6 +39,7 @@ import java.util.Map; @Data @Slf4j @NoArgsConstructor +@EqualsAndHashCode(callSuper = true) public class ScriptCalculatedFieldState extends BaseCalculatedFieldState { public ScriptCalculatedFieldState(List requiredArguments) { @@ -49,11 +52,7 @@ public class ScriptCalculatedFieldState extends BaseCalculatedFieldState { } @Override - protected void validateNewEntry(ArgumentEntry newEntry) { - } - - @Override - public ListenableFuture performCalculation(CalculatedFieldCtx ctx) { + public ListenableFuture performCalculation(EntityId entityId, 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; 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..80b650fc7c 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 @@ -20,11 +20,13 @@ 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.EqualsAndHashCode; import lombok.NoArgsConstructor; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.script.api.tbel.TbUtils; 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.common.data.kv.BasicKvEntry; import org.thingsboard.server.service.cf.CalculatedFieldResult; @@ -33,6 +35,7 @@ import java.util.Map; @Data @NoArgsConstructor +@EqualsAndHashCode(callSuper = true) public class SimpleCalculatedFieldState extends BaseCalculatedFieldState { public SimpleCalculatedFieldState(List requiredArguments) { @@ -52,7 +55,7 @@ public class SimpleCalculatedFieldState extends BaseCalculatedFieldState { } @Override - public ListenableFuture performCalculation(CalculatedFieldCtx ctx) { + public ListenableFuture performCalculation(EntityId entityId, CalculatedFieldCtx ctx) { var expr = ctx.getCustomExpression().get(); for (Map.Entry entry : this.arguments.entrySet()) { 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..1ceea2c621 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 @@ -101,6 +101,11 @@ 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); } 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..f526cc00ab --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/geofencing/GeofencingArgumentEntry.java @@ -0,0 +1,103 @@ +/** + * 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.TbelCfTsGeofencingArg; +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.zoneStates = toZones(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()); + } + 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 TbelCfTsGeofencingArg(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; + } + 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..506ddcff78 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/geofencing/GeofencingCalculatedFieldState.java @@ -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. + */ +package org.thingsboard.server.service.cf.ctx.state.geofencing; + +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.Data; +import lombok.EqualsAndHashCode; +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.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.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.HashMap; +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; + +@Data +@Slf4j +@EqualsAndHashCode(callSuper = true) +public class GeofencingCalculatedFieldState extends BaseCalculatedFieldState { + + private boolean dirty; + + public GeofencingCalculatedFieldState() { + super(new ArrayList<>(), new HashMap<>(), false, -1); + this.dirty = false; + } + + public GeofencingCalculatedFieldState(List argNames) { + super(argNames); + } + + @Override + public CalculatedFieldType getType() { + return CalculatedFieldType.GEOFENCING; + } + + @Override + public boolean updateState(CalculatedFieldCtx ctx, Map argumentValues) { + if (arguments == null) { + arguments = new HashMap<>(); + } + + boolean stateUpdated = false; + + for (var entry : argumentValues.entrySet()) { + String key = entry.getKey(); + ArgumentEntry newEntry = entry.getValue(); + + checkArgumentSize(key, newEntry, ctx); + + ArgumentEntry existingEntry = arguments.get(key); + boolean entryUpdated; + + if (existingEntry == null || newEntry.isForceResetPrevious()) { + entryUpdated = switch (key) { + case ENTITY_ID_LATITUDE_ARGUMENT_KEY, ENTITY_ID_LONGITUDE_ARGUMENT_KEY -> { + if (!(newEntry instanceof SingleValueArgumentEntry singleValueArgumentEntry)) { + throw new IllegalArgumentException("Unsupported argument entry type for " + key + " argument: " + newEntry.getType() + ". " + + "Only SINGLE_VALUE type is allowed."); + } + arguments.put(key, singleValueArgumentEntry); + yield true; + } + default -> { + if (!(newEntry instanceof GeofencingArgumentEntry geofencingArgumentEntry)) { + throw new IllegalArgumentException("Unsupported argument entry type for " + key + " argument: " + newEntry.getType() + ". " + + "Only GEOFENCING type is allowed."); + } + arguments.put(key, geofencingArgumentEntry); + yield true; + } + }; + } else { + entryUpdated = existingEntry.updateEntry(newEntry); + } + if (entryUpdated) { + stateUpdated = true; + } + } + return stateUpdated; + } + + @Override + public ListenableFuture performCalculation(EntityId entityId, 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 resultNode = 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); + } + }); + updateResultNode(argumentKey, zoneResults, zoneGroupCfg.getReportStrategy(), resultNode); + }); + + var result = new CalculatedFieldResult(ctx.getOutput().getType(), ctx.getOutput().getScope(), resultNode); + if (relationFutures.isEmpty()) { + return Futures.immediateFuture(result); + } + return Futures.whenAllComplete(relationFutures).call(() -> result, MoreExecutors.directExecutor()); + } + + 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 updateResultNode(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 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/edge/EdgeContextComponent.java b/application/src/main/java/org/thingsboard/server/service/edge/EdgeContextComponent.java index 00ae4a45bd..5c5eea32fd 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 @@ -29,6 +29,7 @@ import org.thingsboard.server.dao.alarm.AlarmService; import org.thingsboard.server.dao.asset.AssetProfileService; import org.thingsboard.server.dao.asset.AssetService; import org.thingsboard.server.dao.attributes.AttributesService; +import org.thingsboard.server.dao.cf.CalculatedFieldService; import org.thingsboard.server.dao.customer.CustomerService; import org.thingsboard.server.dao.dashboard.DashboardService; import org.thingsboard.server.dao.device.DeviceCredentialsService; @@ -37,6 +38,7 @@ import org.thingsboard.server.dao.device.DeviceService; import org.thingsboard.server.dao.domain.DomainService; import org.thingsboard.server.dao.edge.EdgeEventService; import org.thingsboard.server.dao.edge.EdgeService; +import org.thingsboard.server.dao.edge.stats.EdgeStatsCounterService; import org.thingsboard.server.dao.entityview.EntityViewService; import org.thingsboard.server.dao.notification.NotificationRuleService; import org.thingsboard.server.dao.notification.NotificationTargetService; @@ -61,6 +63,7 @@ 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; import org.thingsboard.server.service.edge.rpc.processor.asset.profile.AssetProfileEdgeProcessor; +import org.thingsboard.server.service.edge.rpc.processor.cf.CalculatedFieldProcessor; import org.thingsboard.server.service.edge.rpc.processor.dashboard.DashboardEdgeProcessor; import org.thingsboard.server.service.edge.rpc.processor.device.DeviceEdgeProcessor; import org.thingsboard.server.service.edge.rpc.processor.device.profile.DeviceProfileEdgeProcessor; @@ -76,6 +79,7 @@ import org.thingsboard.server.service.executors.GrpcCallbackExecutorService; import java.util.EnumMap; import java.util.List; import java.util.Map; +import java.util.Optional; @Lazy @Data @@ -196,6 +200,9 @@ public class EdgeContextComponent { @Autowired private WidgetsBundleService widgetsBundleService; + @Autowired + private Optional statsCounterService; + // processors @Autowired @@ -248,6 +255,12 @@ public class EdgeContextComponent { @Autowired private GrpcCallbackExecutorService grpcCallbackExecutorService; + @Autowired + private CalculatedFieldService calculatedFieldService; + + @Autowired + private CalculatedFieldProcessor calculatedFieldProcessor; + 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 7bafd53644..fda9a1d17b 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 @@ -33,6 +33,7 @@ import org.thingsboard.server.common.data.alarm.AlarmApiCallResult; import org.thingsboard.server.common.data.alarm.AlarmComment; import org.thingsboard.server.common.data.alarm.EntityAlarm; import org.thingsboard.server.common.data.audit.ActionType; +import org.thingsboard.server.common.data.cf.CalculatedField; import org.thingsboard.server.common.data.domain.Domain; import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.edge.EdgeEventActionType; @@ -112,7 +113,7 @@ public class EdgeEventSourcingListener { return; } try { - if (EntityType.TENANT.equals(entityType) || EntityType.EDGE.equals(entityType)) { + if (EntityType.TENANT == entityType || EntityType.EDGE == entityType || EntityType.AI_MODEL == entityType) { return; } log.trace("[{}] DeleteEntityEvent called: {}", tenantId, event); @@ -226,7 +227,7 @@ public class EdgeEventSourcingListener { break; case TENANT: return !event.getCreated(); - case API_USAGE_STATE, EDGE: + case API_USAGE_STATE, EDGE, AI_MODEL: return false; case DOMAIN: if (entity instanceof Domain domain) { @@ -262,6 +263,8 @@ public class EdgeEventSourcingListener { private String getBodyMsgForEntityEvent(Object entity) { if (entity instanceof AlarmComment) { return JacksonUtil.toString(entity); + } else if (entity instanceof CalculatedField calculatedField) { + return JacksonUtil.toString(calculatedField.getEntityId()); } return null; } 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 5f5fd771cf..192c56692d 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 @@ -48,11 +48,13 @@ import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.alarm.AlarmComment; 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.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.AssetId; import org.thingsboard.server.common.data.id.AssetProfileId; +import org.thingsboard.server.common.data.id.CalculatedFieldId; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DashboardId; import org.thingsboard.server.common.data.id.DeviceId; @@ -89,6 +91,7 @@ import org.thingsboard.server.gen.edge.v1.AlarmUpdateMsg; import org.thingsboard.server.gen.edge.v1.AssetProfileUpdateMsg; import org.thingsboard.server.gen.edge.v1.AssetUpdateMsg; import org.thingsboard.server.gen.edge.v1.AttributeDeleteMsg; +import org.thingsboard.server.gen.edge.v1.CalculatedFieldUpdateMsg; import org.thingsboard.server.gen.edge.v1.CustomerUpdateMsg; import org.thingsboard.server.gen.edge.v1.DashboardUpdateMsg; import org.thingsboard.server.gen.edge.v1.DeviceCredentialsUpdateMsg; @@ -638,4 +641,17 @@ public class EdgeMsgConstructorUtils { .build(); } + public static CalculatedFieldUpdateMsg constructCalculatedFieldUpdatedMsg(UpdateMsgType msgType, CalculatedField calculatedField) { + return CalculatedFieldUpdateMsg.newBuilder().setMsgType(msgType).setEntity(JacksonUtil.toString(calculatedField)) + .setIdMSB(calculatedField.getId().getId().getMostSignificantBits()) + .setIdLSB(calculatedField.getId().getId().getLeastSignificantBits()).build(); + } + + public static CalculatedFieldUpdateMsg constructCalculatedFieldDeleteMsg(CalculatedFieldId calculatedFieldId) { + return CalculatedFieldUpdateMsg.newBuilder() + .setMsgType(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE) + .setIdMSB(calculatedFieldId.getId().getMostSignificantBits()) + .setIdLSB(calculatedFieldId.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 61405e17e1..8a111e4d9d 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 @@ -58,8 +58,7 @@ public class RelatedEdgesSourcingListener { log.trace("[{}] ActionEntityEvent called: {}", event.getTenantId(), event); try { switch (event.getActionType()) { - case ASSIGNED_TO_EDGE, UNASSIGNED_FROM_EDGE -> - relatedEdgesService.publishRelatedEdgeIdsEvictEvent(event.getTenantId(), event.getEntityId()); + 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); @@ -67,7 +66,10 @@ public class RelatedEdgesSourcingListener { }); } - @TransactionalEventListener(fallbackExecution = true) + @TransactionalEventListener( + fallbackExecution = true, + condition = "#event.entityId.getEntityType() != T(org.thingsboard.server.common.data.EntityType).AI_MODEL" + ) public void handleEvent(DeleteEntityEvent event) { executorService.submit(() -> { log.trace("[{}] DeleteEntityEvent called: {}", event.getTenantId(), event); 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 5671ffb2ab..7340696788 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,6 +93,8 @@ 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<>(); @@ -151,10 +152,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; @@ -230,8 +228,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); @@ -283,9 +281,8 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i EdgeGrpcSession session = sessions.get(edgeId); if (session != null && session.isConnected()) { log.info("[{}] Closing and removing session for edge [{}]", tenantId, edgeId); - session.destroy(); + destroySession(session); session.cleanUp(); - session.close(); sessions.remove(edgeId); final Lock newEventLock = sessionNewEventsLocks.computeIfAbsent(edgeId, id -> new ReentrantLock()); newEventLock.lock(); @@ -521,7 +518,15 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i private void destroySession(EdgeGrpcSession session) { try (session) { - session.destroy(); + for (int i = 0; i < DESTROY_SESSION_MAX_ATTEMPTS; i++) { + if (session.destroy()) { + break; + } else { + try { + Thread.sleep(100); + } catch (InterruptedException ignored) {} + } + } } } @@ -634,22 +639,25 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i 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()); } } for (EdgeId edgeId : toRemove) { log.info("[{}] Destroying session for edge because edge is not connected", edgeId); - EdgeGrpcSession removed = sessions.remove(edgeId); + EdgeGrpcSession removed = sessions.get(edgeId); if (removed instanceof KafkaEdgeGrpcSession kafkaSession) { - kafkaSession.destroy(); + if (kafkaSession.destroy()) { + sessions.remove(edgeId); + } } } } 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 b6ecd848ad..a86409c6af 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 @@ -45,11 +45,14 @@ import org.thingsboard.server.common.data.page.PageData; 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.AlarmCommentUpdateMsg; import org.thingsboard.server.gen.edge.v1.AlarmUpdateMsg; import org.thingsboard.server.gen.edge.v1.AssetProfileUpdateMsg; import org.thingsboard.server.gen.edge.v1.AssetUpdateMsg; import org.thingsboard.server.gen.edge.v1.AttributesRequestMsg; +import org.thingsboard.server.gen.edge.v1.CalculatedFieldRequestMsg; +import org.thingsboard.server.gen.edge.v1.CalculatedFieldUpdateMsg; import org.thingsboard.server.gen.edge.v1.ConnectRequestMsg; import org.thingsboard.server.gen.edge.v1.ConnectResponseCode; import org.thingsboard.server.gen.edge.v1.ConnectResponseMsg; @@ -296,6 +299,10 @@ public abstract class EdgeGrpcSession implements Closeable { processHighPriorityEvents(); PageData pageData = fetcher.fetchEdgeEvents(edge.getTenantId(), edge, pageLink); if (isConnected() && !pageData.getData().isEmpty()) { + if (fetcher instanceof GeneralEdgeEventFetcher) { + long queueSize = pageData.getTotalElements() - ((long) pageLink.getPageSize() * pageLink.getPage()); + ctx.getStatsCounterService().ifPresent(statsCounterService -> statsCounterService.setDownlinkMsgsLag(edge.getTenantId(), edge.getId(), queueSize)); + } log.trace("[{}][{}][{}] event(s) are going to be processed.", tenantId, edge.getId(), pageData.getData().size()); List downlinkMsgsPack = convertToDownlinkMsgsPack(pageData.getData()); Futures.addCallback(sendDownlinkMsgsPack(downlinkMsgsPack), new FutureCallback<>() { @@ -452,14 +459,17 @@ public abstract class EdgeGrpcSession implements Closeable { List copy = new ArrayList<>(sessionState.getPendingMsgsMap().values()); if (attempt > 1) { String error = "Failed to deliver the batch"; - String failureMsg = String.format("{%s}: {%s}", error, copy); + String failureMsg = String.format("{%s} (size: {%s})", error, copy.size()); if (attempt == 2) { // Send a failure notification only on the second attempt. // This ensures that failure alerts are sent just once to avoid redundant notifications. ctx.getRuleProcessor().process(EdgeCommunicationFailureTrigger.builder().tenantId(tenantId) .edgeId(edge.getId()).customerId(edge.getCustomerId()).edgeName(edge.getName()).failureMsg(failureMsg).error(error).build()); } - log.warn("[{}][{}] {}, attempt: {}", tenantId, edge.getId(), failureMsg, attempt); + ctx.getStatsCounterService().ifPresent(statsCounterService -> + statsCounterService.recordEvent(EdgeStatsKey.DOWNLINK_MSGS_TMP_FAILED, edge.getTenantId(), edge.getId(), 1)); + log.warn("[{}][{}] {} on attempt {}", tenantId, edge.getId(), failureMsg, attempt); + log.debug("[{}][{}] entities in failed batch: {}", tenantId, edge.getId(), copy); } log.trace("[{}][{}][{}] downlink msg(s) are going to be send.", tenantId, edge.getId(), copy.size()); for (DownlinkMsg downlinkMsg : copy) { @@ -471,6 +481,8 @@ public abstract class EdgeGrpcSession implements Closeable { log.error("[{}][{}][{}] {} Message {}", tenantId, edge.getId(), sessionId, message, downlinkMsg); ctx.getRuleProcessor().process(EdgeCommunicationFailureTrigger.builder().tenantId(tenantId) .edgeId(edge.getId()).customerId(edge.getCustomerId()).edgeName(edge.getName()).failureMsg(message).error(error).build()); + ctx.getStatsCounterService().ifPresent(statsCounterService -> + statsCounterService.recordEvent(EdgeStatsKey.DOWNLINK_MSGS_PERMANENTLY_FAILED, edge.getTenantId(), edge.getId(), 1)); sessionState.getPendingMsgsMap().remove(downlinkMsg.getDownlinkMsgId()); } else { sendDownlinkMsg(ResponseMsg.newBuilder() @@ -487,6 +499,7 @@ public abstract class EdgeGrpcSession implements Closeable { ctx.getRuleProcessor().process(EdgeCommunicationFailureTrigger.builder().tenantId(tenantId).edgeId(edge.getId()) .customerId(edge.getCustomerId()).edgeName(edge.getName()).failureMsg(failureMsg) .error("Failed to deliver messages after " + MAX_DOWNLINK_ATTEMPTS + " attempts").build()); + ctx.getStatsCounterService().ifPresent(statsCounterService -> statsCounterService.recordEvent(EdgeStatsKey.DOWNLINK_MSGS_PERMANENTLY_FAILED, edge.getTenantId(), edge.getId(), copy.size())); stopCurrentSendDownlinkMsgsTask(false); } } else { @@ -526,6 +539,7 @@ public abstract class EdgeGrpcSession implements Closeable { try { if (msg.getSuccess()) { sessionState.getPendingMsgsMap().remove(msg.getDownlinkMsgId()); + ctx.getStatsCounterService().ifPresent(statsCounterService -> statsCounterService.recordEvent(EdgeStatsKey.DOWNLINK_MSGS_PUSHED, edge.getTenantId(), edge.getId(), 1)); log.debug("[{}][{}][{}] Msg has been processed successfully! Msg Id: [{}], Msg: {}", tenantId, edge.getId(), sessionId, msg.getDownlinkMsgId(), msg); } else { log.debug("[{}][{}][{}] Msg processing failed! Msg Id: [{}], Error msg: {}", tenantId, edge.getId(), sessionId, msg.getDownlinkMsgId(), msg.getErrorMsg()); @@ -575,7 +589,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<>() { @@ -646,7 +661,8 @@ public abstract class EdgeGrpcSession implements Closeable { log.trace("[{}][{}] entity message processed [{}]", tenantId, edge.getId(), downlinkMsg); } } - case ATTRIBUTES_UPDATED, POST_ATTRIBUTES, ATTRIBUTES_DELETED, TIMESERIES_UPDATED -> downlinkMsg = ctx.getTelemetryProcessor().convertTelemetryEventToDownlink(edge, edgeEvent); + case ATTRIBUTES_UPDATED, POST_ATTRIBUTES, ATTRIBUTES_DELETED, TIMESERIES_UPDATED -> + downlinkMsg = ctx.getTelemetryProcessor().convertTelemetryEventToDownlink(edge, edgeEvent); default -> log.warn("[{}][{}] Unsupported action type [{}]", tenantId, edge.getId(), edgeEvent.getAction()); } } catch (Exception e) { @@ -792,6 +808,7 @@ public abstract class EdgeGrpcSession implements Closeable { } } highPriorityQueue.add(edgeEvent); + ctx.getStatsCounterService().ifPresent(statsCounterService -> statsCounterService.recordEvent(EdgeStatsKey.DOWNLINK_MSGS_ADDED, edge.getTenantId(), edgeEvent.getEdgeId(), 1)); } protected ListenableFuture> processUplinkMsg(UplinkMsg uplinkMsg) { @@ -882,6 +899,11 @@ public abstract class EdgeGrpcSession implements Closeable { result.add(ctx.getEdgeRequestsService().processRelationRequestMsg(edge.getTenantId(), edge, relationRequestMsg)); } } + if (uplinkMsg.getCalculatedFieldRequestMsgCount() > 0) { + for (CalculatedFieldRequestMsg calculatedFieldRequestMsg : uplinkMsg.getCalculatedFieldRequestMsgList()) { + result.add(ctx.getEdgeRequestsService().processCalculatedFieldRequestMsg(edge.getTenantId(), edge, calculatedFieldRequestMsg)); + } + } if (uplinkMsg.getUserCredentialsRequestMsgCount() > 0) { for (UserCredentialsRequestMsg userCredentialsRequestMsg : uplinkMsg.getUserCredentialsRequestMsgList()) { result.add(ctx.getEdgeRequestsService().processUserCredentialsRequestMsg(edge.getTenantId(), edge, userCredentialsRequestMsg)); @@ -907,6 +929,11 @@ public abstract class EdgeGrpcSession implements Closeable { result.add(ctx.getEdgeRequestsService().processEntityViewsRequestMsg(edge.getTenantId(), edge, entityViewRequestMsg)); } } + if (uplinkMsg.getCalculatedFieldUpdateMsgCount() > 0) { + for (CalculatedFieldUpdateMsg calculatedFieldUpdateMsg : uplinkMsg.getCalculatedFieldUpdateMsgList()) { + result.add(ctx.getCalculatedFieldProcessor().processCalculatedFieldMsgFromEdge(edge.getTenantId(), edge, calculatedFieldUpdateMsg)); + } + } } 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); @@ -917,7 +944,9 @@ public abstract class EdgeGrpcSession implements Closeable { return Futures.allAsList(result); } - protected void destroy() {} + protected boolean destroy() { + return true; + } protected void cleanUp() {} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeSyncCursor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeSyncCursor.java index 351c9b411b..adab9b812f 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeSyncCursor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeSyncCursor.java @@ -107,4 +107,5 @@ public class EdgeSyncCursor { currentIdx++; return edgeEventFetcher; } + } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/KafkaEdgeEventService.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/KafkaEdgeEventService.java index 8c3d897c1e..bc00ef4481 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/KafkaEdgeEventService.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/KafkaEdgeEventService.java @@ -25,11 +25,14 @@ import org.thingsboard.server.common.data.edge.EdgeEvent; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.common.util.ProtoUtils; import org.thingsboard.server.dao.edge.BaseEdgeEventService; +import org.thingsboard.server.dao.edge.stats.EdgeStatsCounterService; +import org.thingsboard.server.dao.edge.stats.EdgeStatsKey; import org.thingsboard.server.gen.transport.TransportProtos.ToEdgeEventNotificationMsg; import org.thingsboard.server.queue.common.TbProtoQueueMsg; import org.thingsboard.server.queue.discovery.TopicService; import org.thingsboard.server.queue.provider.TbQueueProducerProvider; +import java.util.Optional; import java.util.UUID; @Slf4j @@ -40,6 +43,7 @@ public class KafkaEdgeEventService extends BaseEdgeEventService { private final TopicService topicService; private final TbQueueProducerProvider producerProvider; + private final Optional statsCounterService; @Override public ListenableFuture saveAsync(EdgeEvent edgeEvent) { @@ -48,7 +52,7 @@ public class KafkaEdgeEventService extends BaseEdgeEventService { TopicPartitionInfo tpi = topicService.getEdgeEventNotificationsTopic(edgeEvent.getTenantId(), edgeEvent.getEdgeId()); ToEdgeEventNotificationMsg msg = ToEdgeEventNotificationMsg.newBuilder().setEdgeEventMsg(ProtoUtils.toProto(edgeEvent)).build(); producerProvider.getTbEdgeEventsMsgProducer().send(tpi, new TbProtoQueueMsg<>(UUID.randomUUID(), msg), null); - + statsCounterService.ifPresent(statsCounterService -> statsCounterService.recordEvent(EdgeStatsKey.DOWNLINK_MSGS_ADDED, edgeEvent.getTenantId(), edgeEvent.getEdgeId(), 1)); return Futures.immediateFuture(null); } 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 daffe9db11..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; } @@ -135,25 +130,30 @@ public class KafkaEdgeGrpcSession extends EdgeGrpcSession { } @Override - public void destroy() { + public boolean destroy() { try { if (consumer != null) { consumer.stop(); } - } finally { - consumer = null; + } catch (Exception e) { + log.warn("[{}][{}] Failed to stop edge event consumer", tenantId, edge.getId(), e); + return false; } + consumer = null; try { if (consumerExecutor != null) { consumerExecutor.shutdown(); } - } catch (Exception ignored) {} + } catch (Exception e) { + log.warn("[{}][{}] Failed to shutdown consumer executor", tenantId, edge.getId(), e); + return false; + } + return true; } @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 4eadcfc5e8..a2243a88d2 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,8 +139,8 @@ 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, NOTIFICATION_TEMPLATE, NOTIFICATION_TARGET, - NOTIFICATION_RULE -> true; + WIDGETS_BUNDLE, WIDGET_TYPE, ADMIN_SETTINGS, OTA_PACKAGE, QUEUE, RELATION, CALCULATED_FIELD, NOTIFICATION_TEMPLATE, + NOTIFICATION_TARGET, NOTIFICATION_RULE -> true; default -> false; }; }; @@ -222,7 +222,7 @@ public abstract class BaseEdgeProcessor implements EdgeProcessor { if (edgeId != null && !edgeId.equals(originatorEdgeId)) { return saveEdgeEvent(tenantId, edgeId, type, actionType, entityId, body); } else { - return processNotificationToRelatedEdges(tenantId, entityId, type, actionType, originatorEdgeId); + return processNotificationToRelatedEdges(tenantId, entityId, entityId, type, actionType, originatorEdgeId); } case DELETED: EdgeEventActionType deleted = EdgeEventActionType.DELETED; @@ -260,11 +260,11 @@ public abstract class BaseEdgeProcessor implements EdgeProcessor { } } - private ListenableFuture processNotificationToRelatedEdges(TenantId tenantId, EntityId entityId, EdgeEventType type, - EdgeEventActionType actionType, EdgeId sourceEdgeId) { + protected ListenableFuture processNotificationToRelatedEdges(TenantId tenantId, EntityId ownerEntityId, EntityId entityId, EdgeEventType type, + EdgeEventActionType actionType, EdgeId sourceEdgeId) { List> futures = new ArrayList<>(); PageDataIterableByTenantIdEntityId edgeIds = - new PageDataIterableByTenantIdEntityId<>(edgeCtx.getEdgeService()::findRelatedEdgeIdsByEntityId, tenantId, entityId, RELATED_EDGES_CACHE_ITEMS); + new PageDataIterableByTenantIdEntityId<>(edgeCtx.getEdgeService()::findRelatedEdgeIdsByEntityId, tenantId, ownerEntityId, RELATED_EDGES_CACHE_ITEMS); for (EdgeId relatedEdgeId : edgeIds) { if (!relatedEdgeId.equals(sourceEdgeId)) { futures.add(saveEdgeEvent(tenantId, relatedEdgeId, type, actionType, entityId, null)); diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/AssetEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/AssetEdgeProcessor.java index 47f4c11362..cba2b62af1 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/AssetEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/AssetEdgeProcessor.java @@ -119,6 +119,7 @@ public class AssetEdgeProcessor extends BaseAssetProcessor implements AssetProce DownlinkMsg.Builder builder = DownlinkMsg.newBuilder() .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) .addAssetUpdateMsg(assetUpdateMsg); + if (UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE.equals(msgType)) { AssetProfile assetProfile = edgeCtx.getAssetProfileService().findAssetProfileById(edgeEvent.getTenantId(), asset.getAssetProfileId()); builder.addAssetProfileUpdateMsg(EdgeMsgConstructorUtils.constructAssetProfileUpdatedMsg(msgType, assetProfile)); 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 new file mode 100644 index 0000000000..4ef6ec7ba2 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/cf/BaseCalculatedFieldProcessor.java @@ -0,0 +1,79 @@ +/** + * 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.cf; + +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.cf.CalculatedField; +import org.thingsboard.server.common.data.id.CalculatedFieldId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.dao.service.DataValidator; +import org.thingsboard.server.gen.edge.v1.CalculatedFieldUpdateMsg; +import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor; + +@Slf4j +public abstract class BaseCalculatedFieldProcessor extends BaseEdgeProcessor { + + @Autowired + private DataValidator calculatedFieldValidator; + + protected Pair saveOrUpdateCalculatedField(TenantId tenantId, CalculatedFieldId calculatedFieldId, CalculatedFieldUpdateMsg calculatedFieldUpdateMsg) { + boolean isCreated = false; + boolean isNameUpdated = false; + try { + CalculatedField calculatedField = JacksonUtil.fromString(calculatedFieldUpdateMsg.getEntity(), CalculatedField.class, true); + if (calculatedField == null) { + throw new RuntimeException("[{" + tenantId + "}] calculatedFieldUpdateMsg {" + calculatedFieldUpdateMsg + " } cannot be converted to calculatedField"); + } + + CalculatedField calculatedFieldById = edgeCtx.getCalculatedFieldService().findById(tenantId, calculatedFieldId); + if (calculatedFieldById == null) { + calculatedField.setCreatedTime(Uuids.unixTimestamp(calculatedFieldId.getId())); + isCreated = true; + calculatedField.setId(null); + } else { + calculatedField.setId(calculatedFieldId); + } + + String calculatedFieldName = calculatedField.getName(); + CalculatedField calculatedFieldByName = edgeCtx.getCalculatedFieldService().findByEntityIdAndName(calculatedField.getEntityId(), calculatedFieldName); + if (calculatedFieldByName != null && !calculatedFieldByName.getId().equals(calculatedFieldId)) { + calculatedFieldName = calculatedFieldName + "_" + StringUtils.randomAlphabetic(15); + log.warn("[{}] calculatedField with name {} already exists. Renaming calculatedField name to {}", + tenantId, calculatedField.getName(), calculatedFieldByName.getName()); + isNameUpdated = true; + } + calculatedField.setName(calculatedFieldName); + + calculatedFieldValidator.validate(calculatedField, CalculatedField::getTenantId); + + if (isCreated) { + calculatedField.setId(calculatedFieldId); + } + + edgeCtx.getCalculatedFieldService().save(calculatedField, false); + } catch (Exception e) { + log.error("[{}] Failed to process calculatedField update msg [{}]", tenantId, calculatedFieldUpdateMsg, e); + throw e; + } + return Pair.of(isCreated, isNameUpdated); + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/cf/CalculatedFieldEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/cf/CalculatedFieldEdgeProcessor.java new file mode 100644 index 0000000000..cab4b5ecc1 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/cf/CalculatedFieldEdgeProcessor.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.edge.rpc.processor.cf; + +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.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.EntityType; +import org.thingsboard.server.common.data.cf.CalculatedField; +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.CalculatedFieldId; +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.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.CalculatedFieldUpdateMsg; +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.gen.transport.TransportProtos; +import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.edge.EdgeMsgConstructorUtils; + +import java.util.UUID; + +@Slf4j +@Component +@TbCoreComponent +public class CalculatedFieldEdgeProcessor extends BaseCalculatedFieldProcessor implements CalculatedFieldProcessor { + + @Override + public ListenableFuture processCalculatedFieldMsgFromEdge(TenantId tenantId, Edge edge, CalculatedFieldUpdateMsg calculatedFieldUpdateMsg) { + CalculatedFieldId calculatedFieldId = new CalculatedFieldId(new UUID(calculatedFieldUpdateMsg.getIdMSB(), calculatedFieldUpdateMsg.getIdLSB())); + try { + edgeSynchronizationManager.getEdgeId().set(edge.getId()); + + switch (calculatedFieldUpdateMsg.getMsgType()) { + case ENTITY_CREATED_RPC_MESSAGE: + case ENTITY_UPDATED_RPC_MESSAGE: + processCalculatedField(tenantId, calculatedFieldId, calculatedFieldUpdateMsg, edge); + return Futures.immediateFuture(null); + case ENTITY_DELETED_RPC_MESSAGE: + CalculatedField calculatedField = edgeCtx.getCalculatedFieldService().findById(tenantId, calculatedFieldId); + if (calculatedField != null) { + edgeCtx.getCalculatedFieldService().deleteCalculatedField(tenantId, calculatedFieldId); + } + return Futures.immediateFuture(null); + case UNRECOGNIZED: + default: + return handleUnsupportedMsgType(calculatedFieldUpdateMsg.getMsgType()); + } + } catch (DataValidationException e) { + if (e.getMessage().contains("limit reached")) { + log.warn("[{}] Number of allowed calculatedField violated {}", tenantId, calculatedFieldUpdateMsg, e); + return Futures.immediateFuture(null); + } else { + return Futures.immediateFailedFuture(e); + } + } finally { + edgeSynchronizationManager.getEdgeId().remove(); + } + } + + @Override + public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) { + CalculatedFieldId calculatedFieldId = new CalculatedFieldId(edgeEvent.getEntityId()); + switch (edgeEvent.getAction()) { + case ADDED, UPDATED -> { + CalculatedField calculatedField = edgeCtx.getCalculatedFieldService().findById(edgeEvent.getTenantId(), calculatedFieldId); + if (calculatedField != null) { + UpdateMsgType msgType = getUpdateMsgType(edgeEvent.getAction()); + CalculatedFieldUpdateMsg calculatedFieldUpdateMsg = EdgeMsgConstructorUtils.constructCalculatedFieldUpdatedMsg(msgType, calculatedField); + return DownlinkMsg.newBuilder() + .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) + .addCalculatedFieldUpdateMsg(calculatedFieldUpdateMsg) + .build(); + } + } + case DELETED -> { + CalculatedFieldUpdateMsg calculatedFieldUpdateMsg = EdgeMsgConstructorUtils.constructCalculatedFieldDeleteMsg(calculatedFieldId); + return DownlinkMsg.newBuilder() + .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) + .addCalculatedFieldUpdateMsg(calculatedFieldUpdateMsg) + .build(); + } + } + return null; + } + + @Override + public EdgeEventType getEdgeEventType() { + return EdgeEventType.CALCULATED_FIELD; + } + + @Override + public ListenableFuture processEntityNotification(TenantId tenantId, TransportProtos.EdgeNotificationMsgProto edgeNotificationMsg) { + EdgeEventType type = EdgeEventType.valueOf(edgeNotificationMsg.getType()); + EdgeEventActionType actionType = EdgeEventActionType.valueOf(edgeNotificationMsg.getAction()); + EntityId entityId = EntityIdFactory.getByEdgeEventTypeAndUuid(type, new UUID(edgeNotificationMsg.getEntityIdMSB(), edgeNotificationMsg.getEntityIdLSB())); + EdgeId originatorEdgeId = safeGetEdgeId(edgeNotificationMsg.getOriginatorEdgeIdMSB(), edgeNotificationMsg.getOriginatorEdgeIdLSB()); + + switch (actionType) { + case UPDATED: + case ADDED: + EntityId calculatedFieldOwnerId = JacksonUtil.fromString(edgeNotificationMsg.getBody(), EntityId.class); + if (calculatedFieldOwnerId != null && + (EntityType.DEVICE.equals(calculatedFieldOwnerId.getEntityType()) || EntityType.ASSET.equals(calculatedFieldOwnerId.getEntityType()))) { + JsonNode body = JacksonUtil.toJsonNode(edgeNotificationMsg.getBody()); + EdgeId edgeId = safeGetEdgeId(edgeNotificationMsg.getEdgeIdMSB(), edgeNotificationMsg.getEdgeIdLSB()); + + return edgeId != null ? + saveEdgeEvent(tenantId, edgeId, type, actionType, entityId, body) : + processNotificationToRelatedEdges(tenantId, calculatedFieldOwnerId, entityId, type, actionType, originatorEdgeId); + } else { + return processActionForAllEdges(tenantId, type, actionType, entityId, null, originatorEdgeId); + } + default: + return super.processEntityNotification(tenantId, edgeNotificationMsg); + } + } + + private void processCalculatedField(TenantId tenantId, CalculatedFieldId calculatedFieldId, CalculatedFieldUpdateMsg calculatedFieldUpdateMsg, Edge edge) { + Pair resultPair = super.saveOrUpdateCalculatedField(tenantId, calculatedFieldId, calculatedFieldUpdateMsg); + Boolean wasCreated = resultPair.getFirst(); + if (wasCreated) { + pushCalculatedFieldCreatedEventToRuleEngine(tenantId, edge, calculatedFieldId); + } + Boolean nameWasUpdated = resultPair.getSecond(); + if (nameWasUpdated) { + saveEdgeEvent(tenantId, edge.getId(), EdgeEventType.CALCULATED_FIELD, EdgeEventActionType.UPDATED, calculatedFieldId, null); + } + } + + private void pushCalculatedFieldCreatedEventToRuleEngine(TenantId tenantId, Edge edge, CalculatedFieldId calculatedFieldId) { + try { + CalculatedField calculatedField = edgeCtx.getCalculatedFieldService().findById(tenantId, calculatedFieldId); + String calculatedFieldAsString = JacksonUtil.toString(calculatedField); + TbMsgMetaData msgMetaData = getEdgeActionTbMsgMetaData(edge, edge.getCustomerId()); + pushEntityEventToRuleEngine(tenantId, calculatedFieldId, edge.getCustomerId(), TbMsgType.ENTITY_CREATED, calculatedFieldAsString, msgMetaData); + } catch (Exception e) { + log.warn("[{}][{}] Failed to push calculatedField action to rule engine: {}", tenantId, calculatedFieldId, TbMsgType.ENTITY_CREATED.name(), e); + } + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/cf/CalculatedFieldProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/cf/CalculatedFieldProcessor.java new file mode 100644 index 0000000000..d21af858f0 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/cf/CalculatedFieldProcessor.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.cf; + +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.CalculatedFieldUpdateMsg; +import org.thingsboard.server.service.edge.rpc.processor.EdgeProcessor; + +public interface CalculatedFieldProcessor extends EdgeProcessor { + + ListenableFuture processCalculatedFieldMsgFromEdge(TenantId tenantId, Edge edge, CalculatedFieldUpdateMsg calculatedFieldUpdateMsg); + +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/DeviceEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/DeviceEdgeProcessor.java index ab01f83cd8..763821f11c 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/DeviceEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/DeviceEdgeProcessor.java @@ -243,6 +243,7 @@ public class DeviceEdgeProcessor extends BaseDeviceProcessor implements DevicePr DeviceCredentialsUpdateMsg deviceCredentialsUpdateMsg = EdgeMsgConstructorUtils.constructDeviceCredentialsUpdatedMsg(deviceCredentials); builder.addDeviceCredentialsUpdateMsg(deviceCredentialsUpdateMsg).build(); } + if (UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE.equals(msgType)) { DeviceProfile deviceProfile = edgeCtx.getDeviceProfileService().findDeviceProfileById(edgeEvent.getTenantId(), device.getDeviceProfileId()); builder.addDeviceProfileUpdateMsg(EdgeMsgConstructorUtils.constructDeviceProfileUpdatedMsg(msgType, deviceProfile)); diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/telemetry/BaseTelemetryProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/telemetry/BaseTelemetryProcessor.java index eeaa80d6a2..068af492bc 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/telemetry/BaseTelemetryProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/telemetry/BaseTelemetryProcessor.java @@ -266,7 +266,7 @@ public abstract class BaseTelemetryProcessor extends BaseEdgeProcessor { SettableFuture futureToSet = SettableFuture.create(); JsonObject json = JsonUtils.getJsonObject(msg.getKvList()); AttributeScope scope = AttributeScope.valueOf(metaData.getValue(DataConstants.SCOPE)); - List attributes = new ArrayList<>(JsonConverter.convertToAttributes(json, ts)); + List attributes = JsonConverter.convertToAttributes(json, ts); ListenableFuture> future = filterAttributesByTs(tenantId, entityId, scope, attributes); Futures.addCallback(future, new FutureCallback<>() { @Override @@ -314,7 +314,7 @@ public abstract class BaseTelemetryProcessor extends BaseEdgeProcessor { SettableFuture futureToSet = SettableFuture.create(); JsonObject json = JsonUtils.getJsonObject(msg.getKvList()); AttributeScope scope = AttributeScope.valueOf(metaData.getValue(DataConstants.SCOPE)); - List attributes = new ArrayList<>(JsonConverter.convertToAttributes(json, ts)); + List attributes = JsonConverter.convertToAttributes(json, ts); ListenableFuture> future = filterAttributesByTs(tenantId, entityId, scope, attributes); Futures.addCallback(future, new FutureCallback<>() { @Override diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/sync/DefaultEdgeRequestsService.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/sync/DefaultEdgeRequestsService.java index 6fc6d5bad9..66ff05b45a 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/sync/DefaultEdgeRequestsService.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/sync/DefaultEdgeRequestsService.java @@ -54,12 +54,14 @@ import org.thingsboard.server.common.data.relation.RelationsSearchParameters; import org.thingsboard.server.common.data.widget.WidgetType; import org.thingsboard.server.common.data.widget.WidgetsBundle; import org.thingsboard.server.dao.attributes.AttributesService; +import org.thingsboard.server.dao.cf.CalculatedFieldService; import org.thingsboard.server.dao.edge.EdgeEventService; import org.thingsboard.server.dao.relation.RelationService; import org.thingsboard.server.dao.timeseries.TimeseriesService; import org.thingsboard.server.dao.widget.WidgetTypeService; import org.thingsboard.server.dao.widget.WidgetsBundleService; import org.thingsboard.server.gen.edge.v1.AttributesRequestMsg; +import org.thingsboard.server.gen.edge.v1.CalculatedFieldRequestMsg; import org.thingsboard.server.gen.edge.v1.DeviceCredentialsRequestMsg; import org.thingsboard.server.gen.edge.v1.EntityViewsRequestMsg; import org.thingsboard.server.gen.edge.v1.RelationRequestMsg; @@ -90,7 +92,7 @@ public class DefaultEdgeRequestsService implements EdgeRequestsService { @Autowired private TimeseriesService timeseriesService; - + @Autowired private RelationService relationService; @@ -104,6 +106,9 @@ public class DefaultEdgeRequestsService implements EdgeRequestsService { @Autowired private WidgetTypeService widgetTypeService; + @Autowired + private CalculatedFieldService calculatedFieldService; + @Autowired private DbCallbackExecutorService dbCallbackExecutorService; @@ -293,6 +298,44 @@ public class DefaultEdgeRequestsService implements EdgeRequestsService { return futureToSet; } + @Override + public ListenableFuture processCalculatedFieldRequestMsg(TenantId tenantId, Edge edge, CalculatedFieldRequestMsg calculatedFieldRequestMsg) { + log.trace("[{}] processCalculatedFieldRequestMsg [{}][{}]", tenantId, edge.getName(), calculatedFieldRequestMsg); + + EntityId entityId = EntityIdFactory.getByTypeAndUuid( + EntityType.valueOf(calculatedFieldRequestMsg.getEntityType()), + new UUID(calculatedFieldRequestMsg.getEntityIdMSB(), calculatedFieldRequestMsg.getEntityIdLSB())); + + log.trace("[{}] processCalculatedField [{}][{}] for entity [{}][{}]", tenantId, edge.getName(), calculatedFieldRequestMsg, entityId.getEntityType(), entityId.getId()); + return saveCalculatedFieldsToEdge(tenantId, edge.getId(), entityId); + } + + private ListenableFuture saveCalculatedFieldsToEdge(TenantId tenantId, EdgeId edgeId, EntityId entityId) { + return Futures.transformAsync( + dbCallbackExecutorService.submit(() -> calculatedFieldService.findCalculatedFieldsByEntityId(tenantId, entityId)), + calculatedFields -> { + log.trace("[{}][{}][{}][{}] calculatedField(s) are going to be pushed to edge.", tenantId, edgeId, entityId, calculatedFields.size()); + + List> futures = calculatedFields.stream().map(calculatedField -> { + try { + return saveEdgeEvent(tenantId, edgeId, EdgeEventType.CALCULATED_FIELD, + EdgeEventActionType.ADDED, calculatedField.getId(), JacksonUtil.valueToTree(calculatedField)); + } catch (Exception e) { + log.error("[{}][{}] Exception during loading calculatedField [{}] to edge on sync!", tenantId, edgeId, calculatedField, e); + return Futures.immediateFailedFuture(e); + } + }).toList(); + + return Futures.transform( + Futures.allAsList(futures), + voids -> null, + dbCallbackExecutorService + ); + }, + dbCallbackExecutorService + ); + } + private ListenableFuture> findRelationByQuery(TenantId tenantId, Edge edge, EntityId entityId, EntitySearchDirection direction) { EntityRelationsQuery query = new EntityRelationsQuery(); query.setParameters(new RelationsSearchParameters(entityId, direction, 1, false)); diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/sync/EdgeRequestsService.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/sync/EdgeRequestsService.java index 2a115eeace..a147a7054b 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/sync/EdgeRequestsService.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/sync/EdgeRequestsService.java @@ -19,6 +19,7 @@ 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.AttributesRequestMsg; +import org.thingsboard.server.gen.edge.v1.CalculatedFieldRequestMsg; import org.thingsboard.server.gen.edge.v1.DeviceCredentialsRequestMsg; import org.thingsboard.server.gen.edge.v1.EntityViewsRequestMsg; import org.thingsboard.server.gen.edge.v1.RelationRequestMsg; @@ -35,6 +36,8 @@ public interface EdgeRequestsService { ListenableFuture processRelationRequestMsg(TenantId tenantId, Edge edge, RelationRequestMsg relationRequestMsg); + ListenableFuture processCalculatedFieldRequestMsg(TenantId tenantId, Edge edge, CalculatedFieldRequestMsg calculatedFieldRequestMsg); + @Deprecated(since = "3.9.1", forRemoval = true) ListenableFuture processDeviceCredentialsRequestMsg(TenantId tenantId, Edge edge, DeviceCredentialsRequestMsg deviceCredentialsRequestMsg); @@ -46,4 +49,5 @@ public interface EdgeRequestsService { @Deprecated(since = "3.9.1", forRemoval = true) ListenableFuture processEntityViewsRequestMsg(TenantId tenantId, Edge edge, EntityViewsRequestMsg entityViewsRequestMsg); + } 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 new file mode 100644 index 0000000000..48b2a47cfb --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/stats/EdgeStatsService.java @@ -0,0 +1,150 @@ +/** + * 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.stats; + +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 lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; +import org.thingsboard.server.common.data.id.EdgeId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.kv.BasicTsKvEntry; +import org.thingsboard.server.common.data.kv.LongDataEntry; +import org.thingsboard.server.common.data.kv.TimeseriesSaveResult; +import org.thingsboard.server.common.data.kv.TsKvEntry; +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.KafkaAdmin; +import org.thingsboard.server.queue.util.TbCoreComponent; + +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.concurrent.TimeUnit; +import java.util.stream.Collectors; + +import static org.thingsboard.server.dao.edge.stats.EdgeStatsKey.DOWNLINK_MSGS_ADDED; +import static org.thingsboard.server.dao.edge.stats.EdgeStatsKey.DOWNLINK_MSGS_LAG; +import static org.thingsboard.server.dao.edge.stats.EdgeStatsKey.DOWNLINK_MSGS_PERMANENTLY_FAILED; +import static org.thingsboard.server.dao.edge.stats.EdgeStatsKey.DOWNLINK_MSGS_PUSHED; +import static org.thingsboard.server.dao.edge.stats.EdgeStatsKey.DOWNLINK_MSGS_TMP_FAILED; + +@TbCoreComponent +@ConditionalOnProperty(prefix = "edges.stats", name = "enabled", havingValue = "true", matchIfMissing = false) +@RequiredArgsConstructor +@Service +@Slf4j +public class EdgeStatsService { + + private final TimeseriesService tsService; + private final EdgeStatsCounterService statsCounterService; + private final TopicService topicService; + private final Optional kafkaAdmin; + + @Value("${edges.stats.ttl:30}") + private int edgesStatsTtlDays; + @Value("${edges.stats.report-interval-millis:600000}") + private long reportIntervalMillis; + + + @Scheduled( + fixedDelayString = "${edges.stats.report-interval-millis:600000}", + initialDelayString = "${edges.stats.report-interval-millis:600000}" + ) + public void reportStats() { + log.debug("Reporting Edge communication stats..."); + long now = System.currentTimeMillis(); + long ts = now - (now % reportIntervalMillis); + + Map countersByEdge = statsCounterService.getCounterByEdge(); + Map lagByEdgeId = kafkaAdmin.isPresent() ? getEdgeLagByEdgeId(countersByEdge) : Collections.emptyMap(); + Map countersByEdgeSnapshot = new HashMap<>(statsCounterService.getCounterByEdge()); + countersByEdgeSnapshot.forEach((edgeId, counters) -> { + TenantId tenantId = counters.getTenantId(); + + if (kafkaAdmin.isPresent()) { + counters.getMsgsLag().set(lagByEdgeId.getOrDefault(edgeId, 0L)); + } + List statsEntries = List.of( + entry(ts, DOWNLINK_MSGS_ADDED.getKey(), counters.getMsgsAdded().get()), + entry(ts, DOWNLINK_MSGS_PUSHED.getKey(), counters.getMsgsPushed().get()), + entry(ts, DOWNLINK_MSGS_PERMANENTLY_FAILED.getKey(), counters.getMsgsPermanentlyFailed().get()), + entry(ts, DOWNLINK_MSGS_TMP_FAILED.getKey(), counters.getMsgsTmpFailed().get()), + entry(ts, DOWNLINK_MSGS_LAG.getKey(), counters.getMsgsLag().get()) + ); + + log.trace("Reported Edge communication stats: {} tenantId - {}, edgeId - {}", statsEntries, tenantId, edgeId); + saveTs(tenantId, edgeId, statsEntries); + }); + } + + private Map getEdgeLagByEdgeId(Map countersByEdge) { + Map edgeToTopicMap = countersByEdge.entrySet().stream() + .collect(Collectors.toMap( + Map.Entry::getKey, + e -> topicService.buildEdgeEventNotificationsTopicPartitionInfo(e.getValue().getTenantId(), e.getKey()).getTopic() + )); + + Map lagByTopic = kafkaAdmin.get().getTotalLagForGroupsBulk(new HashSet<>(edgeToTopicMap.values())); + + return edgeToTopicMap.entrySet().stream() + .collect(Collectors.toMap( + Map.Entry::getKey, + e -> lagByTopic.getOrDefault(e.getValue(), 0L) + )); + } + + private void saveTs(TenantId tenantId, EdgeId edgeId, List statsEntries) { + try { + ListenableFuture future = tsService.save( + tenantId, + edgeId, + statsEntries, + TimeUnit.DAYS.toSeconds(edgesStatsTtlDays) + ); + + Futures.addCallback(future, new FutureCallback<>() { + @Override + public void onSuccess(TimeseriesSaveResult result) { + log.debug("Successfully saved edge time-series stats: {} for edge: {}", statsEntries, edgeId); + } + + @Override + public void onFailure(Throwable t) { + log.warn("Failed to save edge time-series stats for edge: {}", edgeId, t); + } + }, MoreExecutors.directExecutor()); + } finally { + statsCounterService.clear(edgeId); + } + } + + private BasicTsKvEntry entry(long ts, String key, long value) { + return new BasicTsKvEntry(ts, new LongDataEntry(key, value)); + } + +} 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/AbstractTbEntityService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/AbstractTbEntityService.java index 476a4ef5ca..81fd1478b5 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/AbstractTbEntityService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/AbstractTbEntityService.java @@ -97,7 +97,7 @@ public abstract class AbstractTbEntityService { return (I) EntityIdFactory.getByTypeAndUuid(entityType, ModelConstants.NULL_UUID); } - protected ListenableFuture autoCommit(User user, EntityId entityId) throws Exception { + protected ListenableFuture autoCommit(User user, EntityId entityId) { if (vcService != null) { return vcService.autoCommit(user, entityId); } else { @@ -106,7 +106,7 @@ public abstract class AbstractTbEntityService { } } - protected ListenableFuture autoCommit(User user, EntityType entityType, List entityIds) throws Exception { + protected ListenableFuture autoCommit(User user, EntityType entityType, List entityIds) { if (vcService != null) { return vcService.autoCommit(user, entityType, entityIds); } else { diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/ai/DefaultTbAiModelService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/ai/DefaultTbAiModelService.java new file mode 100644 index 0000000000..264b82dd33 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/ai/DefaultTbAiModelService.java @@ -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. + */ +package org.thingsboard.server.service.entitiy.ai; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.ai.AiModel; +import org.thingsboard.server.common.data.audit.ActionType; +import org.thingsboard.server.dao.ai.AiModelService; +import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.entitiy.AbstractTbEntityService; + +import static java.util.Objects.requireNonNullElseGet; + +@Service +@TbCoreComponent +@RequiredArgsConstructor +class DefaultTbAiModelService extends AbstractTbEntityService implements TbAiModelService { + + private final AiModelService aiModelService; + + @Override + public AiModel save(AiModel model, User user) { + var actionType = model.getId() == null ? ActionType.ADDED : ActionType.UPDATED; + + var tenantId = user.getTenantId(); + model.setTenantId(tenantId); + + AiModel savedModel; + try { + savedModel = aiModelService.save(model); + autoCommit(user, savedModel.getId()); + } catch (Exception e) { + logEntityActionService.logEntityAction(tenantId, requireNonNullElseGet(model.getId(), () -> emptyId(EntityType.AI_MODEL)), model, actionType, user, e); + throw e; + } + + logEntityActionService.logEntityAction(tenantId, savedModel.getId(), savedModel, actionType, user); + + return savedModel; + } + + @Override + public boolean delete(AiModel model, User user) { + var actionType = ActionType.DELETED; + + var tenantId = user.getTenantId(); + var modelId = model.getId(); + + boolean deleted; + try { + deleted = aiModelService.deleteByTenantIdAndId(tenantId, modelId); + } catch (Exception e) { + logEntityActionService.logEntityAction(tenantId, modelId, model, actionType, user, e, modelId.toString()); + throw e; + } + + if (deleted) { + logEntityActionService.logEntityAction(tenantId, modelId, model, actionType, user, modelId.toString()); + } + + return deleted; + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/ai/TbAiModelService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/ai/TbAiModelService.java new file mode 100644 index 0000000000..0b09423ffa --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/ai/TbAiModelService.java @@ -0,0 +1,27 @@ +/** + * 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.entitiy.ai; + +import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.ai.AiModel; + +public interface TbAiModelService { + + AiModel save(AiModel model, User user); + + boolean delete(AiModel model, User user); + +} diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmCommentService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmCommentService.java index de4d0ac9cb..73f6e0a861 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmCommentService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmCommentService.java @@ -32,7 +32,7 @@ import org.thingsboard.server.service.entitiy.AbstractTbEntityService; @Service @AllArgsConstructor -public class DefaultTbAlarmCommentService extends AbstractTbEntityService implements TbAlarmCommentService{ +public class DefaultTbAlarmCommentService extends AbstractTbEntityService implements TbAlarmCommentService { @Autowired private AlarmCommentService alarmCommentService; @@ -68,4 +68,5 @@ public class DefaultTbAlarmCommentService extends AbstractTbEntityService implem throw new ThingsboardException("System comment could not be deleted", ThingsboardErrorCode.BAD_REQUEST_PARAMS); } } + } diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/ota/DefaultTbOtaPackageService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/ota/DefaultTbOtaPackageService.java index af8bbeb669..2d597f7053 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/ota/DefaultTbOtaPackageService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/ota/DefaultTbOtaPackageService.java @@ -110,4 +110,5 @@ public class DefaultTbOtaPackageService extends AbstractTbEntityService implemen throw e; } } + } 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/housekeeper/processor/AlarmsDeletionTaskProcessor.java b/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/AlarmsDeletionTaskProcessor.java index 55107a8326..1c7d312e8b 100644 --- a/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/AlarmsDeletionTaskProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/AlarmsDeletionTaskProcessor.java @@ -43,33 +43,30 @@ public class AlarmsDeletionTaskProcessor extends HousekeeperTaskProcessor> alarms = alarmService.findAlarmIdsByOriginatorId(tenantId, entityId, lastCreatedTime, lastId, 128); - if (alarms.isEmpty()) { - break; - } + if (task.getAlarms() == null) { + AlarmId lastId = null; + long lastCreatedTime = 0; + while (true) { + List> alarms = alarmService.findAlarmIdsByOriginatorId(tenantId, entityId, lastCreatedTime, lastId, 128); + if (alarms.isEmpty()) { + break; + } - housekeeperClient.submitTask(new AlarmsDeletionHousekeeperTask(tenantId, entityId, alarms.stream().map(TbPair::getFirst).toList())); + housekeeperClient.submitTask(new AlarmsDeletionHousekeeperTask(tenantId, entityId, alarms.stream().map(TbPair::getFirst).toList())); - TbPair last = alarms.get(alarms.size() - 1); - lastId = new AlarmId(last.getFirst()); - lastCreatedTime = last.getSecond(); - log.debug("[{}][{}][{}] Submitted task for deleting {} alarms", tenantId, entityType, entityId, alarms.size()); - } - } else { - for (UUID alarmId : task.getAlarms()) { - alarmService.delAlarm(tenantId, new AlarmId(alarmId)); - } - log.debug("[{}][{}][{}] Deleted {} alarms", tenantId, entityType, entityId, task.getAlarms().size()); + TbPair last = alarms.get(alarms.size() - 1); + lastId = new AlarmId(last.getFirst()); + lastCreatedTime = last.getSecond(); + log.debug("[{}][{}][{}] Submitted task for deleting {} alarms", tenantId, entityType, entityId, alarms.size()); } + int count = alarmService.deleteEntityAlarmRecords(tenantId, entityId); + log.debug("[{}][{}][{}] Deleted {} entity alarms", tenantId, entityType, entityId, count); + } else { + for (UUID alarmId : task.getAlarms()) { + alarmService.delAlarm(tenantId, new AlarmId(alarmId)); + } + log.debug("[{}][{}][{}] Deleted {} alarms", tenantId, entityType, entityId, task.getAlarms().size()); } - - int count = alarmService.deleteEntityAlarmRecords(tenantId, entityId); - log.debug("[{}][{}][{}] Deleted {} entity alarms", tenantId, entityType, entityId, count); } @Override 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 90c5966f3f..f41a530630 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 @@ -32,7 +32,7 @@ public class DefaultDatabaseSchemaSettingsService implements DatabaseSchemaSetti // 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.0.0", "4.0.1"); + private static final List SUPPORTED_VERSIONS_FOR_UPGRADE = List.of("4.2.0"); private final ProjectInfo projectInfo; private final JdbcTemplate jdbcTemplate; 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 c3f1cee046..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,30 +15,17 @@ */ 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; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Profile; import org.springframework.stereotype.Service; -import org.thingsboard.server.common.data.TenantProfile; -import org.thingsboard.server.common.data.alarm.AlarmSeverity; -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.page.PageDataIterable; -import org.thingsboard.server.common.data.query.DynamicValue; -import org.thingsboard.server.common.data.query.FilterPredicateValue; -import org.thingsboard.server.common.data.relation.EntityRelation; -import org.thingsboard.server.common.data.relation.RelationTypeGroup; -import org.thingsboard.server.common.data.rule.RuleNode; -import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; -import org.thingsboard.server.dao.relation.RelationService; import org.thingsboard.server.dao.rule.RuleChainService; -import org.thingsboard.server.dao.tenant.TenantProfileService; import org.thingsboard.server.service.component.ComponentDiscoveryService; import org.thingsboard.server.service.component.RuleNodeClassInfo; import org.thingsboard.server.service.install.DbUpgradeExecutorService; @@ -46,108 +33,27 @@ import org.thingsboard.server.utils.TbNodeUpgradeUtils; import java.util.ArrayList; import java.util.List; -import java.util.Optional; -import java.util.UUID; import java.util.concurrent.ExecutionException; -import static org.thingsboard.server.dao.rule.BaseRuleChainService.TB_RULE_CHAIN_INPUT_NODE; - @Service @Profile("install") @Slf4j +@RequiredArgsConstructor public class DefaultDataUpdateService implements DataUpdateService { private static final int MAX_PENDING_SAVE_RULE_NODE_FUTURES = 256; private static final int DEFAULT_PAGE_SIZE = 1024; - @Autowired - private RuleChainService ruleChainService; - - @Autowired - private RelationService relationService; - - @Autowired - private ComponentDiscoveryService componentDiscoveryService; - - @Autowired - private DbUpgradeExecutorService executorService; - - @Autowired - private TenantProfileService tenantProfileService; + private final RuleChainService ruleChainService; + private final ComponentDiscoveryService componentDiscoveryService; + private final DbUpgradeExecutorService executorService; @Override public void updateData() throws Exception { log.info("Updating data ..."); //TODO: should be cleaned after each release - updateInputNodes(); - deduplicateRateLimitsPerSecondsConfigurations(); - log.info("Data updated."); - } - - private void deduplicateRateLimitsPerSecondsConfigurations() { - log.info("Starting update of tenant profiles..."); - - int totalProfiles = 0; - int updatedTenantProfiles = 0; - int skippedProfiles = 0; - int failedProfiles = 0; - - var tenantProfiles = new PageDataIterable<>( - pageLink -> tenantProfileService.findTenantProfiles(TenantId.SYS_TENANT_ID, pageLink), 1024); - - for (TenantProfile tenantProfile : tenantProfiles) { - totalProfiles++; - String profileName = tenantProfile.getName(); - UUID profileId = tenantProfile.getId().getId(); - try { - Optional profileConfiguration = tenantProfile.getProfileConfiguration(); - if (profileConfiguration.isEmpty()) { - log.debug("[{}][{}] Skipping tenant profile with non-default configuration.", profileId, profileName); - skippedProfiles++; - continue; - } - - DefaultTenantProfileConfiguration defaultTenantProfileConfiguration = profileConfiguration.get(); - defaultTenantProfileConfiguration.deduplicateRateLimitsConfigs(); - tenantProfileService.saveTenantProfile(TenantId.SYS_TENANT_ID, tenantProfile); - updatedTenantProfiles++; - log.debug("[{}][{}] Successfully updated tenant profile.", profileId, profileName); - } catch (Exception e) { - log.error("[{}][{}] Failed to updated tenant profile: ", profileId, profileName, e); - failedProfiles++; - } - } - - log.info("Tenant profiles update completed. Total: {}, Updated: {}, Skipped: {}, Failed: {}", - totalProfiles, updatedTenantProfiles, skippedProfiles, failedProfiles); - } - - private void updateInputNodes() { - log.info("Creating relations for input nodes..."); - int n = 0; - var inputNodes = new PageDataIterable<>(pageLink -> ruleChainService.findAllRuleNodesByType(TB_RULE_CHAIN_INPUT_NODE, pageLink), 1024); - for (RuleNode inputNode : inputNodes) { - try { - RuleChainId targetRuleChainId = Optional.ofNullable(inputNode.getConfiguration().get("ruleChainId")) - .filter(JsonNode::isTextual).map(JsonNode::asText).map(id -> new RuleChainId(UUID.fromString(id))) - .orElse(null); - if (targetRuleChainId == null) { - continue; - } - - EntityRelation relation = new EntityRelation(); - relation.setFrom(inputNode.getRuleChainId()); - relation.setTo(targetRuleChainId); - relation.setType(EntityRelation.USES_TYPE); - relation.setTypeGroup(RelationTypeGroup.COMMON); - relationService.saveRelation(TenantId.SYS_TENANT_ID, relation); - n++; - } catch (Exception e) { - log.error("Failed to save relation for input node: {}", inputNode, e); - } - } - log.info("Created {} relations for input nodes", n); + log.info("Data updated."); } @Override @@ -218,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/mail/DefaultMailService.java b/application/src/main/java/org/thingsboard/server/service/mail/DefaultMailService.java index 99faf1a1d0..c1cfe791da 100644 --- a/application/src/main/java/org/thingsboard/server/service/mail/DefaultMailService.java +++ b/application/src/main/java/org/thingsboard/server/service/mail/DefaultMailService.java @@ -22,9 +22,9 @@ import freemarker.template.Template; import jakarta.annotation.PostConstruct; import jakarta.annotation.PreDestroy; import jakarta.mail.internet.MimeMessage; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.exception.ExceptionUtils; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.MessageSource; import org.springframework.context.annotation.Lazy; @@ -64,55 +64,37 @@ import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; -@Service @Slf4j +@Service +@RequiredArgsConstructor public class DefaultMailService implements MailService { - public static final String TARGET_EMAIL = "targetEmail"; - public static final String UTF_8 = "UTF-8"; + private static final String TARGET_EMAIL = "targetEmail"; + private static final String UTF_8 = "UTF-8"; + private static final long DEFAULT_TIMEOUT = 10_000; + + private final ScheduledExecutorService timeoutScheduler = ThingsBoardExecutors.newSingleThreadScheduledExecutor("mail-service-watchdog"); private final MessageSource messages; private final Configuration freemarkerConfig; private final AdminSettingsService adminSettingsService; private final TbApiUsageReportClient apiUsageClient; - - private static final long DEFAULT_TIMEOUT = 10_000; - @Lazy - @Autowired - private TbApiUsageStateService apiUsageStateService; - - @Autowired - private MailSenderInternalExecutorService mailExecutorService; - - @Autowired - private PasswordResetExecutorService passwordResetExecutorService; - - @Autowired - private TbMailContextComponent ctx; - - @Autowired - private RateLimitService rateLimitService; + private final TbApiUsageStateService apiUsageStateService; + private final MailSenderInternalExecutorService mailExecutorService; + private final PasswordResetExecutorService passwordResetExecutorService; + private final TbMailContextComponent ctx; + private final RateLimitService rateLimitService; @Value("${mail.per_tenant_rate_limits:}") private String perTenantRateLimitConfig; - private final ScheduledExecutorService timeoutScheduler; - private TbMailSender mailSender; private String mailFrom; private long timeout; - public DefaultMailService(MessageSource messages, Configuration freemarkerConfig, AdminSettingsService adminSettingsService, TbApiUsageReportClient apiUsageClient) { - this.messages = messages; - this.freemarkerConfig = freemarkerConfig; - this.adminSettingsService = adminSettingsService; - this.apiUsageClient = apiUsageClient; - this.timeoutScheduler = ThingsBoardExecutors.newSingleThreadScheduledExecutor("mail-service-watchdog"); - } - @PostConstruct private void init() { updateMailConfiguration(); @@ -120,9 +102,7 @@ public class DefaultMailService implements MailService { @PreDestroy public void destroy() { - if (timeoutScheduler != null) { - timeoutScheduler.shutdownNow(); - } + timeoutScheduler.shutdownNow(); } @Override @@ -311,22 +291,21 @@ public class DefaultMailService implements MailService { model.put("apiFeature", apiFeature.getLabel()); model.put(TARGET_EMAIL, email); - String message = null; - - switch (stateValue) { - case ENABLED: + String message = switch (stateValue) { + case ENABLED -> { model.put("apiLabel", toEnabledValueLabel(apiFeature)); - message = mergeTemplateIntoString("state.enabled.ftl", model); - break; - case WARNING: + yield mergeTemplateIntoString("state.enabled.ftl", model); + } + case WARNING -> { model.put("apiValueLabel", toDisabledValueLabel(apiFeature) + " " + toWarningValueLabel(recordState)); - message = mergeTemplateIntoString("state.warning.ftl", model); - break; - case DISABLED: + yield mergeTemplateIntoString("state.warning.ftl", model); + } + case DISABLED -> { model.put("apiLimitValueLabel", toDisabledValueLabel(apiFeature) + " " + toDisabledValueLabel(recordState)); - message = mergeTemplateIntoString("state.disabled.ftl", model); - break; - } + yield mergeTemplateIntoString("state.disabled.ftl", model); + } + }; + sendMail(mailSender, mailFrom, email, subject, message, timeout); } @@ -341,89 +320,55 @@ public class DefaultMailService implements MailService { } private String toEnabledValueLabel(ApiFeature apiFeature) { - switch (apiFeature) { - case DB: - return "save"; - case TRANSPORT: - return "receive"; - case JS: - return "invoke"; - case RE: - return "process"; - case EMAIL: - case SMS: - return "send"; - case ALARM: - return "create"; - default: - throw new RuntimeException("Not implemented!"); - } + return switch (apiFeature) { + case DB -> "save"; + case TRANSPORT -> "receive"; + case JS -> "invoke"; + case RE -> "process"; + case EMAIL, SMS -> "send"; + case ALARM -> "create"; + default -> throw new RuntimeException("Not implemented!"); + }; } private String toDisabledValueLabel(ApiFeature apiFeature) { - switch (apiFeature) { - case DB: - return "saved"; - case TRANSPORT: - return "received"; - case JS: - return "invoked"; - case RE: - return "processed"; - case EMAIL: - case SMS: - return "sent"; - case ALARM: - return "created"; - default: - throw new RuntimeException("Not implemented!"); - } + return switch (apiFeature) { + case DB -> "saved"; + case TRANSPORT -> "received"; + case JS -> "invoked"; + case RE -> "processed"; + case EMAIL, SMS -> "sent"; + case ALARM -> "created"; + default -> throw new RuntimeException("Not implemented!"); + }; } private String toWarningValueLabel(ApiUsageRecordState recordState) { String valueInM = recordState.getValueAsString(); String thresholdInM = recordState.getThresholdAsString(); - switch (recordState.getKey()) { - case STORAGE_DP_COUNT: - case TRANSPORT_DP_COUNT: - return valueInM + " out of " + thresholdInM + " allowed data points"; - case TRANSPORT_MSG_COUNT: - return valueInM + " out of " + thresholdInM + " allowed messages"; - case JS_EXEC_COUNT: - return valueInM + " out of " + thresholdInM + " allowed JavaScript functions"; - case TBEL_EXEC_COUNT: - return valueInM + " out of " + thresholdInM + " allowed Tbel functions"; - case RE_EXEC_COUNT: - return valueInM + " out of " + thresholdInM + " allowed Rule Engine messages"; - case EMAIL_EXEC_COUNT: - return valueInM + " out of " + thresholdInM + " allowed Email messages"; - case SMS_EXEC_COUNT: - return valueInM + " out of " + thresholdInM + " allowed SMS messages"; - default: - throw new RuntimeException("Not implemented!"); - } + return switch (recordState.getKey()) { + case STORAGE_DP_COUNT, TRANSPORT_DP_COUNT -> valueInM + " out of " + thresholdInM + " allowed data points"; + case TRANSPORT_MSG_COUNT -> valueInM + " out of " + thresholdInM + " allowed messages"; + case JS_EXEC_COUNT -> valueInM + " out of " + thresholdInM + " allowed JavaScript functions"; + case TBEL_EXEC_COUNT -> valueInM + " out of " + thresholdInM + " allowed Tbel functions"; + case RE_EXEC_COUNT -> valueInM + " out of " + thresholdInM + " allowed Rule Engine messages"; + case EMAIL_EXEC_COUNT -> valueInM + " out of " + thresholdInM + " allowed Email messages"; + case SMS_EXEC_COUNT -> valueInM + " out of " + thresholdInM + " allowed SMS messages"; + default -> throw new RuntimeException("Not implemented!"); + }; } private String toDisabledValueLabel(ApiUsageRecordState recordState) { - switch (recordState.getKey()) { - case STORAGE_DP_COUNT: - case TRANSPORT_DP_COUNT: - return recordState.getValueAsString() + " data points"; - case TRANSPORT_MSG_COUNT: - return recordState.getValueAsString() + " messages"; - case JS_EXEC_COUNT: - return "JavaScript functions " + recordState.getValueAsString() + " times"; - case TBEL_EXEC_COUNT: - return "TBEL functions " + recordState.getValueAsString() + " times"; - case RE_EXEC_COUNT: - return recordState.getValueAsString() + " Rule Engine messages"; - case EMAIL_EXEC_COUNT: - return recordState.getValueAsString() + " Email messages"; - case SMS_EXEC_COUNT: - return recordState.getValueAsString() + " SMS messages"; - default: - throw new RuntimeException("Not implemented!"); - } + return switch (recordState.getKey()) { + case STORAGE_DP_COUNT, TRANSPORT_DP_COUNT -> recordState.getValueAsString() + " data points"; + case TRANSPORT_MSG_COUNT -> recordState.getValueAsString() + " messages"; + case JS_EXEC_COUNT -> "JavaScript functions " + recordState.getValueAsString() + " times"; + case TBEL_EXEC_COUNT -> "TBEL functions " + recordState.getValueAsString() + " times"; + case RE_EXEC_COUNT -> recordState.getValueAsString() + " Rule Engine messages"; + case EMAIL_EXEC_COUNT -> recordState.getValueAsString() + " Email messages"; + case SMS_EXEC_COUNT -> recordState.getValueAsString() + " SMS messages"; + default -> throw new RuntimeException("Not implemented!"); + }; } private void sendMail(JavaMailSenderImpl mailSender, String mailFrom, String email, diff --git a/application/src/main/java/org/thingsboard/server/service/mail/TbMailSender.java b/application/src/main/java/org/thingsboard/server/service/mail/TbMailSender.java index 8d7356a507..914a53e8a6 100644 --- a/application/src/main/java/org/thingsboard/server/service/mail/TbMailSender.java +++ b/application/src/main/java/org/thingsboard/server/service/mail/TbMailSender.java @@ -25,6 +25,7 @@ import com.google.api.client.http.javanet.NetHttpTransport; import com.google.api.client.json.gson.GsonFactory; import jakarta.mail.MessagingException; import jakarta.mail.internet.MimeMessage; +import lombok.Getter; import lombok.extern.slf4j.Slf4j; import org.springframework.lang.Nullable; import org.springframework.mail.MailException; @@ -50,8 +51,10 @@ public class TbMailSender extends JavaMailSenderImpl { private final TbMailContextComponent ctx; private final Lock lock; + @Getter private final Boolean oauth2Enabled; private volatile String accessToken; + @Getter private volatile long tokenExpires; public TbMailSender(TbMailContextComponent ctx, JsonNode jsonConfig) { @@ -70,14 +73,6 @@ public class TbMailSender extends JavaMailSenderImpl { setJavaMailProperties(createJavaMailProperties(jsonConfig)); } - public Boolean getOauth2Enabled() { - return oauth2Enabled; - } - - public long getTokenExpires() { - return tokenExpires; - } - @Override protected void doSend(MimeMessage[] mimeMessages, @Nullable Object[] originalMessages) throws MailException { updateOauth2PasswordIfExpired(); @@ -98,8 +93,8 @@ public class TbMailSender extends JavaMailSenderImpl { super.testConnection(); } - public void updateOauth2PasswordIfExpired() { - if (getOauth2Enabled() && (System.currentTimeMillis() > getTokenExpires())){ + public void updateOauth2PasswordIfExpired() { + if (getOauth2Enabled() && (System.currentTimeMillis() > getTokenExpires())) { refreshAccessToken(); setPassword(accessToken); } @@ -168,8 +163,8 @@ public class TbMailSender extends JavaMailSenderImpl { .setClientAuthentication(new ClientParametersAuthentication(clientId, clientSecret)) .execute(); if (MailOauth2Provider.OFFICE_365.name().equals(providerId)) { - ((ObjectNode)jsonValue).put("refreshToken", tokenResponse.getRefreshToken()); - ((ObjectNode)jsonValue).put("refreshTokenExpires", Instant.now().plus(Duration.ofDays(AZURE_DEFAULT_REFRESH_TOKEN_LIFETIME_IN_DAYS)).toEpochMilli()); + ((ObjectNode) jsonValue).put("refreshToken", tokenResponse.getRefreshToken()); + ((ObjectNode) jsonValue).put("refreshTokenExpires", Instant.now().plus(Duration.ofDays(AZURE_DEFAULT_REFRESH_TOKEN_LIFETIME_IN_DAYS)).toEpochMilli()); ctx.getAdminSettingsService().saveAdminSettings(TenantId.SYS_TENANT_ID, settings); } accessToken = tokenResponse.getAccessToken(); @@ -190,4 +185,5 @@ public class TbMailSender extends JavaMailSenderImpl { throw new IncorrectParameterException(String.format("Invalid smtp port value: %s", strPort)); } } -} \ No newline at end of file + +} diff --git a/application/src/main/java/org/thingsboard/server/service/notification/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/query/DefaultEntityQueryService.java b/application/src/main/java/org/thingsboard/server/service/query/DefaultEntityQueryService.java index b3fe76f154..45a73072f5 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,7 +233,7 @@ 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; } 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 9d20b4d74e..265f14c4e2 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 @@ -583,17 +583,18 @@ public class DefaultTbClusterService implements TbClusterService { TbQueueProducer> toRuleEngineProducer = producerProvider.getRuleEngineNotificationsMsgProducer(); Set tbRuleEngineServices = partitionService.getAllServiceIds(ServiceType.TB_RULE_ENGINE); EntityType entityType = msg.getEntityId().getEntityType(); - if (entityType.equals(EntityType.TENANT) - || entityType.equals(EntityType.TENANT_PROFILE) - || entityType.equals(EntityType.DEVICE_PROFILE) - || (entityType.equals(EntityType.ASSET) && msg.getEvent() == ComponentLifecycleEvent.UPDATED) - || entityType.equals(EntityType.ASSET_PROFILE) - || entityType.equals(EntityType.API_USAGE_STATE) - || (entityType.equals(EntityType.DEVICE) && msg.getEvent() == ComponentLifecycleEvent.UPDATED) - || entityType.equals(EntityType.ENTITY_VIEW) - || entityType.equals(EntityType.NOTIFICATION_RULE) - || entityType.equals(EntityType.CALCULATED_FIELD) - || entityType.equals(EntityType.JOB) + if (entityType.isOneOf( + EntityType.TENANT, + EntityType.API_USAGE_STATE, + EntityType.ENTITY_VIEW, + EntityType.NOTIFICATION_RULE, + EntityType.CALCULATED_FIELD, + EntityType.TENANT_PROFILE, + EntityType.DEVICE_PROFILE, + EntityType.ASSET_PROFILE, + EntityType.JOB) + || (entityType == EntityType.ASSET && msg.getEvent() == ComponentLifecycleEvent.UPDATED) + || (entityType == EntityType.DEVICE && msg.getEvent() == ComponentLifecycleEvent.UPDATED) ) { TbQueueProducer> toCoreNfProducer = producerProvider.getTbCoreNotificationsMsgProducer(); Set tbCoreServices = partitionService.getAllServiceIds(ServiceType.TB_CORE); diff --git a/application/src/main/java/org/thingsboard/server/service/script/RuleNodeJsScriptEngine.java b/application/src/main/java/org/thingsboard/server/service/script/RuleNodeJsScriptEngine.java index b7490af487..e0b8351ff6 100644 --- a/application/src/main/java/org/thingsboard/server/service/script/RuleNodeJsScriptEngine.java +++ b/application/src/main/java/org/thingsboard/server/service/script/RuleNodeJsScriptEngine.java @@ -17,18 +17,16 @@ package org.thingsboard.server.service.script; import com.fasterxml.jackson.core.type.TypeReference; 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.common.util.JacksonUtil; import org.thingsboard.script.api.RuleNodeScriptFactory; +import org.thingsboard.script.api.TbScriptException; import org.thingsboard.script.api.js.JsInvokeService; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; -import javax.script.ScriptException; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; @@ -36,8 +34,6 @@ import java.util.List; import java.util.Map; import java.util.Set; - -@Slf4j public class RuleNodeJsScriptEngine extends RuleNodeScriptEngine { public RuleNodeJsScriptEngine(TenantId tenantId, JsInvokeService scriptInvokeService, String script, String... argNames) { @@ -45,87 +41,81 @@ public class RuleNodeJsScriptEngine extends RuleNodeScriptEngine executeJsonAsync(TbMsg msg) { - return executeScriptAsync(msg); + protected Object[] prepareArgs(TbMsg msg) { + String[] args = new String[3]; + if (msg.getData() != null) { + args[0] = msg.getData(); + } else { + args[0] = ""; + } + args[1] = JacksonUtil.toString(msg.getMetaData().getData()); + args[2] = msg.getType(); + return args; } @Override - protected ListenableFuture> executeUpdateTransform(TbMsg msg, JsonNode json) { + protected List executeUpdateTransform(TbMsg msg, JsonNode json) { if (json.isObject()) { - return Futures.immediateFuture(Collections.singletonList(unbindMsg(json, msg))); + return Collections.singletonList(unbindMsg(json, msg)); } else if (json.isArray()) { List res = new ArrayList<>(json.size()); json.forEach(jsonObject -> res.add(unbindMsg(jsonObject, msg))); - return Futures.immediateFuture(res); + return res; } - log.warn("Wrong result type: {}", json.getNodeType()); - return Futures.immediateFailedFuture(new ScriptException("Wrong result type: " + json.getNodeType())); + throw wrongResultType(json); } @Override - protected ListenableFuture executeGenerateTransform(TbMsg prevMsg, JsonNode result) { + protected TbMsg executeGenerateTransform(TbMsg prevMsg, JsonNode result) { if (!result.isObject()) { - log.warn("Wrong result type: {}", result.getNodeType()); - Futures.immediateFailedFuture(new ScriptException("Wrong result type: " + result.getNodeType())); - } - return Futures.immediateFuture(unbindMsg(result, prevMsg)); - } - - @Override - protected JsonNode convertResult(Object result) { - return JacksonUtil.toJsonNode(result != null ? result.toString() : null); - } - - @Override - protected ListenableFuture executeToStringTransform(JsonNode result) { - if (result.isTextual()) { - return Futures.immediateFuture(result.asText()); + throw wrongResultType(result); } - log.warn("Wrong result type: {}", result.getNodeType()); - return Futures.immediateFailedFuture(new ScriptException("Wrong result type: " + result.getNodeType())); + return unbindMsg(result, prevMsg); } @Override - protected ListenableFuture executeFilterTransform(JsonNode json) { + protected boolean executeFilterTransform(JsonNode json) { if (json.isBoolean()) { - return Futures.immediateFuture(json.asBoolean()); + return json.asBoolean(); } - log.warn("Wrong result type: {}", json.getNodeType()); - return Futures.immediateFailedFuture(new ScriptException("Wrong result type: " + json.getNodeType())); + throw wrongResultType(json); } @Override - protected ListenableFuture> executeSwitchTransform(JsonNode result) { + protected Set executeSwitchTransform(JsonNode result) { if (result.isTextual()) { - return Futures.immediateFuture(Collections.singleton(result.asText())); + return Collections.singleton(result.asText()); } if (result.isArray()) { Set nextStates = new HashSet<>(); for (JsonNode val : result) { if (!val.isTextual()) { - log.warn("Wrong result type: {}", val.getNodeType()); - return Futures.immediateFailedFuture(new ScriptException("Wrong result type: " + val.getNodeType())); + throw wrongResultType(val); } else { nextStates.add(val.asText()); } } - return Futures.immediateFuture(nextStates); + return nextStates; } - log.warn("Wrong result type: {}", result.getNodeType()); - return Futures.immediateFailedFuture(new ScriptException("Wrong result type: " + result.getNodeType())); + throw wrongResultType(result); } @Override - protected Object[] prepareArgs(TbMsg msg) { - String[] args = new String[3]; - if (msg.getData() != null) { - args[0] = msg.getData(); - } else { - args[0] = ""; + public ListenableFuture executeJsonAsync(TbMsg msg) { + return executeScriptAsync(msg); + } + + @Override + protected String executeToStringTransform(JsonNode result) { + if (result.isTextual()) { + return result.asText(); } - args[1] = JacksonUtil.toString(msg.getMetaData().getData()); - args[2] = msg.getType(); - return args; + throw wrongResultType(result); + } + + @Override + protected JsonNode convertResult(Object result) { + return JacksonUtil.toJsonNode(result != null ? result.toString() : null); } private static TbMsg unbindMsg(JsonNode msgData, TbMsg msg) { @@ -138,19 +128,23 @@ public class RuleNodeJsScriptEngine extends RuleNodeScriptEngine() { - }); + metadata = JacksonUtil.convertValue(msgMetadata, new TypeReference<>() {}); } if (msgData.has(RuleNodeScriptFactory.MSG_TYPE)) { messageType = msgData.get(RuleNodeScriptFactory.MSG_TYPE).asText(); } String newData = data != null ? data : msg.getData(); TbMsgMetaData newMetadata = metadata != null ? new TbMsgMetaData(metadata) : msg.getMetaData().copy(); - String newMessageType = !StringUtils.isEmpty(messageType) ? messageType : msg.getType(); + String newMessageType = StringUtils.isNotEmpty(messageType) ? messageType : msg.getType(); return msg.transform() .type(newMessageType) .metaData(newMetadata) .data(newData) .build(); } + + private TbScriptException wrongResultType(JsonNode result) { + return new TbScriptException(scriptId, TbScriptException.ErrorCode.RUNTIME, null, new ClassCastException("Wrong result type: " + result.getNodeType())); + } + } diff --git a/application/src/main/java/org/thingsboard/server/service/script/RuleNodeScriptEngine.java b/application/src/main/java/org/thingsboard/server/service/script/RuleNodeScriptEngine.java index 8f19aeb0a3..ec9c2fd983 100644 --- a/application/src/main/java/org/thingsboard/server/service/script/RuleNodeScriptEngine.java +++ b/application/src/main/java/org/thingsboard/server/service/script/RuleNodeScriptEngine.java @@ -17,41 +17,44 @@ package org.thingsboard.server.service.script; 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.ScriptEngine; import org.thingsboard.script.api.ScriptInvokeService; import org.thingsboard.script.api.ScriptType; +import org.thingsboard.script.api.TbScriptException; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.msg.TbMsg; -import javax.script.ScriptException; import java.util.List; import java.util.Set; import java.util.UUID; import java.util.concurrent.ExecutionException; +import static com.google.common.util.concurrent.MoreExecutors.directExecutor; @Slf4j public abstract class RuleNodeScriptEngine implements ScriptEngine { private final T scriptInvokeService; - private final UUID scriptId; + protected final UUID scriptId; private final TenantId tenantId; public RuleNodeScriptEngine(TenantId tenantId, T scriptInvokeService, String script, String... argNames) { this.tenantId = tenantId; this.scriptInvokeService = scriptInvokeService; try { - this.scriptId = this.scriptInvokeService.eval(tenantId, ScriptType.RULE_NODE_SCRIPT, script, argNames).get(); + scriptId = this.scriptInvokeService.eval(tenantId, ScriptType.RULE_NODE_SCRIPT, script, argNames).get(); } catch (Exception e) { Throwable t = e; if (e instanceof ExecutionException) { t = e.getCause(); } - throw new IllegalArgumentException("Can't compile script: " + t.getMessage(), t); + if (t instanceof TbScriptException scriptException) { + throw scriptException; + } + throw new RuntimeException("Unexpected error when creating script engine: " + t.getMessage(), t); } } @@ -60,74 +63,53 @@ public abstract class RuleNodeScriptEngine imp @Override public ListenableFuture> executeUpdateAsync(TbMsg msg) { ListenableFuture result = executeScriptAsync(msg); - return Futures.transformAsync(result, - json -> executeUpdateTransform(msg, json), - MoreExecutors.directExecutor()); + return Futures.transform(result, json -> executeUpdateTransform(msg, json), directExecutor()); } - protected abstract ListenableFuture> executeUpdateTransform(TbMsg msg, R result); + protected abstract List executeUpdateTransform(TbMsg msg, R result); @Override public ListenableFuture executeGenerateAsync(TbMsg prevMsg) { - return Futures.transformAsync(executeScriptAsync(prevMsg), - result -> executeGenerateTransform(prevMsg, result), - MoreExecutors.directExecutor()); + return Futures.transform(executeScriptAsync(prevMsg), result -> executeGenerateTransform(prevMsg, result), directExecutor()); } - protected abstract ListenableFuture executeGenerateTransform(TbMsg prevMsg, R result); + protected abstract TbMsg executeGenerateTransform(TbMsg prevMsg, R result); @Override - public ListenableFuture executeToStringAsync(TbMsg msg) { - return Futures.transformAsync(executeScriptAsync(msg), this::executeToStringTransform, MoreExecutors.directExecutor()); + public ListenableFuture executeFilterAsync(TbMsg msg) { + return Futures.transform(executeScriptAsync(msg), this::executeFilterTransform, directExecutor()); } + protected abstract boolean executeFilterTransform(R result); @Override - public ListenableFuture executeFilterAsync(TbMsg msg) { - return Futures.transformAsync(executeScriptAsync(msg), - this::executeFilterTransform, - MoreExecutors.directExecutor()); + public ListenableFuture> executeSwitchAsync(TbMsg msg) { + return Futures.transform(executeScriptAsync(msg), this::executeSwitchTransform, directExecutor()); // usually runs on a callbackExecutor } - protected abstract ListenableFuture executeToStringTransform(R result); - - protected abstract ListenableFuture executeFilterTransform(R result); - - protected abstract ListenableFuture> executeSwitchTransform(R result); + protected abstract Set executeSwitchTransform(R result); @Override - public ListenableFuture> executeSwitchAsync(TbMsg msg) { - return Futures.transformAsync(executeScriptAsync(msg), - this::executeSwitchTransform, - MoreExecutors.directExecutor()); //usually runs in a callbackExecutor + public ListenableFuture executeToStringAsync(TbMsg msg) { + return Futures.transform(executeScriptAsync(msg), this::executeToStringTransform, directExecutor()); } + protected abstract String executeToStringTransform(R result); + ListenableFuture executeScriptAsync(TbMsg msg) { log.trace("execute script async, msg {}", msg); Object[] inArgs = prepareArgs(msg); return executeScriptAsync(msg.getCustomerId(), inArgs[0], inArgs[1], inArgs[2]); } - ListenableFuture executeScriptAsync(CustomerId customerId, Object... args) { - return Futures.transformAsync(scriptInvokeService.invokeScript(tenantId, customerId, this.scriptId, args), - o -> { - try { - return Futures.immediateFuture(convertResult(o)); - } catch (Exception e) { - if (e.getCause() instanceof ScriptException) { - return Futures.immediateFailedFuture(e.getCause()); - } else if (e.getCause() instanceof RuntimeException) { - return Futures.immediateFailedFuture(new ScriptException(e.getCause().getMessage())); - } else { - return Futures.immediateFailedFuture(new ScriptException(e)); - } - } - }, MoreExecutors.directExecutor()); + private ListenableFuture executeScriptAsync(CustomerId customerId, Object... args) { + return Futures.transform(scriptInvokeService.invokeScript(tenantId, customerId, scriptId, args), this::convertResult, directExecutor()); } public void destroy() { - scriptInvokeService.release(this.scriptId); + scriptInvokeService.release(scriptId); } protected abstract R convertResult(Object result); + } diff --git a/application/src/main/java/org/thingsboard/server/service/script/RuleNodeTbelScriptEngine.java b/application/src/main/java/org/thingsboard/server/service/script/RuleNodeTbelScriptEngine.java index 5e197d0e5d..991be63f81 100644 --- a/application/src/main/java/org/thingsboard/server/service/script/RuleNodeTbelScriptEngine.java +++ b/application/src/main/java/org/thingsboard/server/service/script/RuleNodeTbelScriptEngine.java @@ -19,17 +19,15 @@ import com.fasterxml.jackson.core.type.TypeReference; 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.thingsboard.common.util.JacksonUtil; import org.thingsboard.script.api.RuleNodeScriptFactory; +import org.thingsboard.script.api.TbScriptException; import org.thingsboard.script.api.tbel.TbelInvokeService; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; -import javax.script.ScriptException; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; @@ -40,8 +38,8 @@ import java.util.Map; import java.util.Set; import java.util.stream.Collectors; +import static com.google.common.util.concurrent.MoreExecutors.directExecutor; -@Slf4j public class RuleNodeTbelScriptEngine extends RuleNodeScriptEngine { public RuleNodeTbelScriptEngine(TenantId tenantId, TbelInvokeService scriptInvokeService, String script, String... argNames) { @@ -49,70 +47,74 @@ public class RuleNodeTbelScriptEngine extends RuleNodeScriptEngine executeFilterTransform(Object result) { - if (result instanceof Boolean) { - return Futures.immediateFuture((Boolean) result); + protected Object[] prepareArgs(TbMsg msg) { + Object[] args = new Object[3]; + if (msg.getData() != null) { + args[0] = JacksonUtil.fromString(msg.getData(), Object.class); + } else { + args[0] = new HashMap<>(); } - return wrongResultType(result); + args[1] = new HashMap<>(msg.getMetaData().getData()); + args[2] = msg.getType(); + return args; } @Override - protected ListenableFuture> executeUpdateTransform(TbMsg msg, Object result) { - if (result instanceof Map) { - return Futures.immediateFuture(Collections.singletonList(unbindMsg((Map) result, msg))); - } else if (result instanceof Collection) { - List res = new ArrayList<>(); - for (Object resObject : (Collection) result) { - if (resObject instanceof Map) { - res.add(unbindMsg((Map) resObject, msg)); + protected List executeUpdateTransform(TbMsg msg, Object result) { + if (result instanceof Map msgData) { + return Collections.singletonList(unbindMsg(msgData, msg)); + } else if (result instanceof Collection resultCollection) { + List res = new ArrayList<>(resultCollection.size()); + for (Object resObject : resultCollection) { + if (resObject instanceof Map msgData) { + res.add(unbindMsg(msgData, msg)); } else { - return wrongResultType(resObject); + throw wrongResultType(resObject); } } - return Futures.immediateFuture(res); + return res; } - return wrongResultType(result); + throw wrongResultType(result); } @Override - protected ListenableFuture executeGenerateTransform(TbMsg prevMsg, Object result) { - if (result instanceof Map) { - return Futures.immediateFuture(unbindMsg((Map) result, prevMsg)); + protected TbMsg executeGenerateTransform(TbMsg prevMsg, Object result) { + if (result instanceof Map msgData) { + return unbindMsg(msgData, prevMsg); } - return wrongResultType(result); + throw wrongResultType(result); } @Override - protected ListenableFuture executeToStringTransform(Object result) { - if (result instanceof String) { - return Futures.immediateFuture((String) result); - } else { - return Futures.immediateFuture(JacksonUtil.toString(result)); + protected boolean executeFilterTransform(Object result) { + if (result instanceof Boolean b) { + return b; } + throw wrongResultType(result); } @Override - protected ListenableFuture> executeSwitchTransform(Object result) { - if (result instanceof String) { - return Futures.immediateFuture(Collections.singleton((String) result)); - } else if (result instanceof Collection) { - Set res = new HashSet<>(); - for (Object resObject : (Collection) result) { - if (resObject instanceof String) { - res.add((String) resObject); + protected Set executeSwitchTransform(Object result) { + if (result instanceof String str) { + return Collections.singleton(str); + } + if (result instanceof Collection resultCollection) { + Set res = new HashSet<>(resultCollection.size()); + for (Object resObject : resultCollection) { + if (resObject instanceof String str) { + res.add(str); } else { - return wrongResultType(resObject); + throw wrongResultType(resObject); } } - return Futures.immediateFuture(res); + return res; } - return wrongResultType(result); + throw wrongResultType(result); } @Override public ListenableFuture executeJsonAsync(TbMsg msg) { - return Futures.transform(executeScriptAsync(msg), JacksonUtil::valueToTree, MoreExecutors.directExecutor()); - + return Futures.transform(executeScriptAsync(msg), JacksonUtil::valueToTree, directExecutor()); } @Override @@ -121,16 +123,8 @@ public class RuleNodeTbelScriptEngine extends RuleNodeScriptEngine(); - } - args[1] = new HashMap<>(msg.getMetaData().getData()); - args[2] = msg.getType(); - return args; + protected String executeToStringTransform(Object result) { + return result instanceof String str ? str : JacksonUtil.toString(result); } private static TbMsg unbindMsg(Map msgData, TbMsg msg) { @@ -142,12 +136,12 @@ public class RuleNodeTbelScriptEngine extends RuleNodeScriptEngine) msgMetadataObj).entrySet().stream().filter(e -> e.getValue() != null) + if (msgMetadataObj instanceof Map msgMetadataObjAsMap) { + metadata = msgMetadataObjAsMap.entrySet().stream() + .filter(e -> e.getValue() != null) .collect(Collectors.toMap(e -> e.getKey().toString(), e -> e.getValue().toString())); } else { - metadata = JacksonUtil.convertValue(msgMetadataObj, new TypeReference<>() { - }); + metadata = JacksonUtil.convertValue(msgMetadataObj, new TypeReference<>() {}); } } if (msgData.containsKey(RuleNodeScriptFactory.MSG_TYPE)) { @@ -155,7 +149,7 @@ public class RuleNodeTbelScriptEngine extends RuleNodeScriptEngine ListenableFuture wrongResultType(Object result) { + private TbScriptException wrongResultType(Object result) { String className = toClassName(result); - log.warn("Wrong result type: {}", className); - return Futures.immediateFailedFuture(new ScriptException("Wrong result type: " + className)); + return new TbScriptException(scriptId, TbScriptException.ErrorCode.RUNTIME, null, new ClassCastException("Wrong result type: " + className)); } private static String toClassName(Object result) { return result != null ? result.getClass().getSimpleName() : "null"; } + } diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/Oauth2AuthenticationSuccessHandler.java b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/Oauth2AuthenticationSuccessHandler.java index 9a34fa08df..111234500f 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/Oauth2AuthenticationSuccessHandler.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/Oauth2AuthenticationSuccessHandler.java @@ -121,7 +121,7 @@ public class Oauth2AuthenticationSuccessHandler extends SimpleUrlAuthenticationS errorPrefix = "/login?loginError="; } getRedirectStrategy().sendRedirect(request, response, baseUrl + errorPrefix + - URLEncoder.encode(e.getMessage(), StandardCharsets.UTF_8.toString())); + URLEncoder.encode(e.getMessage(), StandardCharsets.UTF_8)); } } @@ -138,4 +138,5 @@ public class Oauth2AuthenticationSuccessHandler extends SimpleUrlAuthenticationS } return baseUrl + "accessToken=" + tokenPair.getToken() + "&refreshToken=" + tokenPair.getRefreshToken(); } + } diff --git a/application/src/main/java/org/thingsboard/server/service/security/permission/Resource.java b/application/src/main/java/org/thingsboard/server/service/security/permission/Resource.java index 2a92c040e3..8a4208c457 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/permission/Resource.java +++ b/application/src/main/java/org/thingsboard/server/service/security/permission/Resource.java @@ -21,7 +21,8 @@ import java.util.Collections; import java.util.Set; public enum Resource { - ADMIN_SETTINGS(), + + ADMIN_SETTINGS(EntityType.ADMIN_SETTINGS), ALARM(EntityType.ALARM), DEVICE(EntityType.DEVICE), ASSET(EntityType.ASSET), @@ -51,7 +52,8 @@ public enum Resource { NOTIFICATION(EntityType.NOTIFICATION_TARGET, EntityType.NOTIFICATION_TEMPLATE, EntityType.NOTIFICATION_REQUEST, EntityType.NOTIFICATION_RULE), MOBILE_APP_SETTINGS, - JOB(EntityType.JOB); + JOB(EntityType.JOB), + AI_MODEL(EntityType.AI_MODEL); private final Set entityTypes; @@ -75,4 +77,5 @@ public enum Resource { } throw new IllegalArgumentException("Unknown EntityType: " + entityType.name()); } + } 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 bbad7b52d4..95d8da11da 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 @@ -18,6 +18,8 @@ package org.thingsboard.server.service.security.permission; import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.HasTenantId; import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.ai.AiModel; +import org.thingsboard.server.common.data.id.AiModelId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.security.Authority; @@ -56,6 +58,7 @@ public class TenantAdminPermissions extends AbstractPermissions { put(Resource.MOBILE_APP, tenantEntityPermissionChecker); put(Resource.MOBILE_APP_BUNDLE, tenantEntityPermissionChecker); put(Resource.JOB, tenantEntityPermissionChecker); + put(Resource.AI_MODEL, aiModelPermissionChecker); } public static final PermissionChecker tenantEntityPermissionChecker = new PermissionChecker() { @@ -146,4 +149,18 @@ public class TenantAdminPermissions extends AbstractPermissions { }; + private static final PermissionChecker aiModelPermissionChecker = new PermissionChecker<>() { + + @Override + public boolean hasPermission(SecurityUser user, Operation operation) { + return true; + } + + @Override + public boolean hasPermission(SecurityUser user, Operation operation, AiModelId entityId, AiModel entity) { + return user.getTenantId().equals(entity.getTenantId()); + } + + }; + } diff --git a/application/src/main/java/org/thingsboard/server/service/sms/DefaultSmsSenderFactory.java b/application/src/main/java/org/thingsboard/server/service/sms/DefaultSmsSenderFactory.java index 5522047deb..475c9c2c75 100644 --- a/application/src/main/java/org/thingsboard/server/service/sms/DefaultSmsSenderFactory.java +++ b/application/src/main/java/org/thingsboard/server/service/sms/DefaultSmsSenderFactory.java @@ -31,16 +31,12 @@ public class DefaultSmsSenderFactory implements SmsSenderFactory { @Override public SmsSender createSmsSender(SmsProviderConfiguration config) { - switch (config.getType()) { - case AWS_SNS: - return new AwsSmsSender((AwsSnsSmsProviderConfiguration)config); - case TWILIO: - return new TwilioSmsSender((TwilioSmsProviderConfiguration)config); - case SMPP: - return new SmppSmsSender((SmppSmsProviderConfiguration) config); - default: - throw new RuntimeException("Unknown SMS provider type " + config.getType()); - } + return switch (config.getType()) { + case AWS_SNS -> new AwsSmsSender((AwsSnsSmsProviderConfiguration) config); + case TWILIO -> new TwilioSmsSender((TwilioSmsProviderConfiguration) config); + case SMPP -> new SmppSmsSender((SmppSmsProviderConfiguration) config); + default -> throw new RuntimeException("Unknown SMS provider type " + config.getType()); + }; } } diff --git a/application/src/main/java/org/thingsboard/server/service/sms/DefaultSmsService.java b/application/src/main/java/org/thingsboard/server/service/sms/DefaultSmsService.java index 779a3adfa6..a313dcb9a6 100644 --- a/application/src/main/java/org/thingsboard/server/service/sms/DefaultSmsService.java +++ b/application/src/main/java/org/thingsboard/server/service/sms/DefaultSmsService.java @@ -18,6 +18,7 @@ package org.thingsboard.server.service.sms; import com.fasterxml.jackson.databind.JsonNode; import jakarta.annotation.PostConstruct; import jakarta.annotation.PreDestroy; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.core.NestedRuntimeException; import org.springframework.stereotype.Service; @@ -37,8 +38,9 @@ import org.thingsboard.server.common.stats.TbApiUsageReportClient; import org.thingsboard.server.dao.settings.AdminSettingsService; import org.thingsboard.server.service.apiusage.TbApiUsageStateService; -@Service @Slf4j +@Service +@RequiredArgsConstructor public class DefaultSmsService implements SmsService { private final SmsSenderFactory smsSenderFactory; @@ -48,13 +50,6 @@ public class DefaultSmsService implements SmsService { private SmsSender smsSender; - public DefaultSmsService(SmsSenderFactory smsSenderFactory, AdminSettingsService adminSettingsService, TbApiUsageStateService apiUsageStateService, TbApiUsageReportClient apiUsageClient) { - this.smsSenderFactory = smsSenderFactory; - this.adminSettingsService = adminSettingsService; - this.apiUsageStateService = apiUsageStateService; - this.apiUsageClient = apiUsageClient; - } - @PostConstruct private void init() { updateSmsConfiguration(); @@ -148,4 +143,5 @@ public class DefaultSmsService implements SmsService { return new ThingsboardException(String.format("Unable to send SMS: %s", message), ThingsboardErrorCode.GENERAL); } + } diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/TbAbstractEntityQuerySubCtx.java b/application/src/main/java/org/thingsboard/server/service/subscription/TbAbstractEntityQuerySubCtx.java index 9d51c2ec4a..b471bf418a 100644 --- a/application/src/main/java/org/thingsboard/server/service/subscription/TbAbstractEntityQuerySubCtx.java +++ b/application/src/main/java/org/thingsboard/server/service/subscription/TbAbstractEntityQuerySubCtx.java @@ -32,6 +32,7 @@ import org.thingsboard.server.common.data.query.ComplexFilterPredicate; import org.thingsboard.server.common.data.query.DynamicValue; import org.thingsboard.server.common.data.query.DynamicValueSourceType; import org.thingsboard.server.common.data.query.EntityCountQuery; +import org.thingsboard.server.common.data.query.EntityFilter; import org.thingsboard.server.common.data.query.FilterPredicateType; import org.thingsboard.server.common.data.query.KeyFilter; import org.thingsboard.server.common.data.query.KeyFilterPredicate; @@ -94,9 +95,14 @@ public abstract class TbAbstractEntityQuerySubCtx ex public void setAndResolveQuery(T query) { dynamicValues.clear(); this.query = query; - if (query != null && query.getKeyFilters() != null) { - for (KeyFilter filter : query.getKeyFilters()) { - registerDynamicValues(filter.getPredicate()); + if (query != null) { + if (query.getEntityFilter() != null) { + EntityFilter.resolveEntityFilter(query.getEntityFilter(), getTenantId(), getUserId(), getOwnerId()); + } + if (query.getKeyFilters() != null) { + for (KeyFilter filter : query.getKeyFilters()) { + registerDynamicValues(filter.getPredicate()); + } } } resolve(getTenantId(), getCustomerId(), getUserId()); diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/TbAbstractSubCtx.java b/application/src/main/java/org/thingsboard/server/service/subscription/TbAbstractSubCtx.java index 6d4b6e8ed3..70bd34886a 100644 --- a/application/src/main/java/org/thingsboard/server/service/subscription/TbAbstractSubCtx.java +++ b/application/src/main/java/org/thingsboard/server/service/subscription/TbAbstractSubCtx.java @@ -107,6 +107,11 @@ public abstract class TbAbstractSubCtx { return sessionRef.getSecurityCtx().getId(); } + public EntityId getOwnerId() { + var customerId = getCustomerId(); + return customerId != null && !customerId.isNullUid() ? customerId : getTenantId(); + } + public void sendWsMsg(CmdUpdate update) { wsLock.lock(); try { diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/DefaultEntitiesExportImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/DefaultEntitiesExportImportService.java index db7e37b368..506b48bf8c 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/DefaultEntitiesExportImportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/DefaultEntitiesExportImportService.java @@ -67,9 +67,10 @@ public class DefaultEntitiesExportImportService implements EntitiesExportImportS protected static final List SUPPORTED_ENTITY_TYPES = List.of( EntityType.CUSTOMER, EntityType.RULE_CHAIN, EntityType.TB_RESOURCE, EntityType.DASHBOARD, EntityType.ASSET_PROFILE, EntityType.ASSET, - EntityType.DEVICE_PROFILE, EntityType.DEVICE, + EntityType.DEVICE_PROFILE, EntityType.OTA_PACKAGE, EntityType.DEVICE, EntityType.ENTITY_VIEW, EntityType.WIDGET_TYPE, EntityType.WIDGETS_BUNDLE, - EntityType.NOTIFICATION_TEMPLATE, EntityType.NOTIFICATION_TARGET, EntityType.NOTIFICATION_RULE + EntityType.NOTIFICATION_TEMPLATE, EntityType.NOTIFICATION_TARGET, EntityType.NOTIFICATION_RULE, + EntityType.AI_MODEL ); @Override diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/AiModelExportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/AiModelExportService.java new file mode 100644 index 0000000000..8d6097b726 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/AiModelExportService.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.sync.ie.exporting.impl; + +import org.springframework.stereotype.Service; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.ai.AiModel; +import org.thingsboard.server.common.data.id.AiModelId; +import org.thingsboard.server.common.data.sync.ie.EntityExportData; +import org.thingsboard.server.queue.util.TbCoreComponent; + +import java.util.Set; + +@Service +@TbCoreComponent +class AiModelExportService extends BaseEntityExportService> { + + @Override + public Set getSupportedEntityTypes() { + return Set.of(EntityType.AI_MODEL); + } + +} 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..b7b027d785 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,7 @@ 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.exception.ThingsboardException; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityIdFactory; @@ -153,11 +154,13 @@ 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 configuration) { + configuration.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/exporting/impl/DeviceExportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DeviceExportService.java index 7d5f7ee57e..2f5355f637 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DeviceExportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DeviceExportService.java @@ -38,6 +38,8 @@ public class DeviceExportService extends BaseEntityExportService ctx, Device device, DeviceExportData exportData) { device.setCustomerId(getExternalIdOrElseInternal(ctx, device.getCustomerId())); device.setDeviceProfileId(getExternalIdOrElseInternal(ctx, device.getDeviceProfileId())); + device.setFirmwareId(getExternalIdOrElseInternal(ctx, device.getFirmwareId())); + device.setSoftwareId(getExternalIdOrElseInternal(ctx, device.getSoftwareId())); if (ctx.getSettings().isExportCredentials()) { var credentials = deviceCredentialsService.findDeviceCredentialsByDeviceId(ctx.getTenantId(), device.getId()); credentials.setId(null); diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DeviceProfileExportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DeviceProfileExportService.java index 6c212f0684..6a98bd2a7c 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DeviceProfileExportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DeviceProfileExportService.java @@ -34,6 +34,8 @@ public class DeviceProfileExportService extends BaseEntityExportService { + + @Override + protected void setRelatedEntities(EntitiesExportCtx ctx, OtaPackage otaPackage, OtaPackageExportData exportData) { + otaPackage.setDeviceProfileId(getExternalIdOrElseInternal(ctx, otaPackage.getDeviceProfileId())); + } + + @Override + protected OtaPackageExportData newExportData() { + return new OtaPackageExportData(); + } + + @Override + public Set getSupportedEntityTypes() { + return Set.of(EntityType.OTA_PACKAGE); + } + +} 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 3ea50ea327..9850e2d1a1 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 @@ -67,7 +67,6 @@ import org.thingsboard.server.service.security.permission.Resource; import org.thingsboard.server.service.telemetry.TelemetrySubscriptionService; import org.thingsboard.server.utils.CsvUtils; -import java.util.ArrayList; import java.util.Arrays; import java.util.LinkedHashMap; import java.util.List; @@ -235,7 +234,7 @@ public abstract class AbstractBulkImportService kvsEntry, BulkImportColumnType kvType) { String scope = kvType.getKey(); - List attributes = new ArrayList<>(JsonConverter.convertToAttributes(kvsEntry.getValue())); + List attributes = JsonConverter.convertToAttributes(kvsEntry.getValue()); accessValidator.validateEntityAndCallback(user, Operation.WRITE_ATTRIBUTES, entity.getId(), (result, tenantId, entityId) -> { tsSubscriptionService.saveAttributes(AttributesSaveRequest.builder() diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/AiModelImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/AiModelImportService.java new file mode 100644 index 0000000000..34e70adb11 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/AiModelImportService.java @@ -0,0 +1,77 @@ +/** + * 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.sync.ie.importing.impl; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.thingsboard.server.common.data.EntityType; +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.common.data.sync.ie.EntityExportData; +import org.thingsboard.server.dao.ai.AiModelService; +import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.sync.vc.data.EntitiesImportCtx; + +@Service +@TbCoreComponent +@RequiredArgsConstructor +class AiModelImportService extends BaseEntityImportService> { + + private final AiModelService aiModelService; + + @Override + protected void setOwner( + TenantId tenantId, + AiModel model, + BaseEntityImportService>.IdProvider idProvider + ) { + model.setTenantId(tenantId); + } + + @Override + protected AiModel prepare( + EntitiesImportCtx ctx, + AiModel model, + AiModel oldModel, + EntityExportData exportData, + BaseEntityImportService>.IdProvider idProvider + ) { + return model; + } + + @Override + protected AiModel deepCopy(AiModel model) { + return new AiModel(model); + } + + @Override + protected AiModel saveOrUpdate( + EntitiesImportCtx ctx, + AiModel model, + EntityExportData exportData, + BaseEntityImportService>.IdProvider idProvider, + CompareResult compareResult + ) { + return aiModelService.save(model); + } + + @Override + public EntityType getEntityType() { + return EntityType.AI_MODEL; + } + +} 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 57b4737be6..2b606a2fe8 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,7 @@ 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.exception.ThingsboardException; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityIdFactory; @@ -71,7 +72,6 @@ import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.UUID; -import java.util.function.Function; import java.util.regex.Pattern; import java.util.stream.Collectors; @@ -148,6 +148,7 @@ public abstract class BaseEntityImportService importResult, D exportData, IdProvider idProvider) throws ThingsboardException { E savedEntity = importResult.getSavedEntity(); E oldEntity = importResult.getOldEntity(); @@ -322,11 +322,13 @@ 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 configuration) { + configuration.getArguments().values().forEach(argument -> { + if (argument.getRefEntityId() != null) { + argument.setRefEntityId(idProvider.getInternalId(argument.getRefEntityId(), ctx.isFinalImportAttempt())); + } + }); + } }).toList(); for (CalculatedField existingField : existing) { @@ -405,7 +407,9 @@ public abstract class BaseEntityImportService ID getInternalId(ID externalId, boolean throwExceptionIfNotFound) { - if (externalId == null || externalId.isNullUid()) return null; + if (externalId == null || externalId.isNullUid()) { + return null; + } if (EntityType.TENANT.equals(externalId.getEntityType())) { return (ID) ctx.getTenantId(); @@ -432,7 +436,9 @@ public abstract class BaseEntityImportService getInternalIdByUuid(UUID externalUuid, boolean fetchAllUUIDs, Set hints) { - if (externalUuid.equals(EntityId.NULL_UUID)) return Optional.empty(); + if (externalUuid.equals(EntityId.NULL_UUID)) { + return Optional.empty(); + } for (EntityType entityType : EntityType.values()) { Optional externalId = buildEntityId(entityType, externalUuid); @@ -483,10 +489,6 @@ public abstract class BaseEntityImportService T getOldEntityField(O oldEntity, Function getter) { - return oldEntity == null ? null : getter.apply(oldEntity); - } - protected void replaceIdsRecursively(EntitiesImportCtx ctx, IdProvider idProvider, JsonNode json, Set skippedRootFields, Pattern includedFieldsPattern, LinkedHashSet hints) { diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/DeviceImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/DeviceImportService.java index 4ace9ff938..0cbb471b6f 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/DeviceImportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/DeviceImportService.java @@ -44,8 +44,8 @@ public class DeviceImportService extends BaseEntityImportService exportData, IdProvider idProvider, CompareResult compareResult) { + boolean toUpdate = ctx.isFinalImportAttempt() || ctx.getCurrentImportResult().isUpdatedAllExternalIds(); + if (toUpdate) { + deviceProfile.setFirmwareId(idProvider.getInternalId(deviceProfile.getFirmwareId())); + deviceProfile.setSoftwareId(idProvider.getInternalId(deviceProfile.getSoftwareId())); + } DeviceProfile saved = deviceProfileService.saveDeviceProfile(deviceProfile); - if (ctx.isFinalImportAttempt() || ctx.getCurrentImportResult().isUpdatedAllExternalIds()) { + if (toUpdate) { importCalculatedFields(ctx, saved, exportData, idProvider); } return saved; @@ -73,8 +78,6 @@ public class DeviceProfileImportService extends BaseEntityImportService { + + private final OtaPackageService otaPackageService; + + @Override + protected void setOwner(TenantId tenantId, OtaPackage otaPackage, IdProvider idProvider) { + otaPackage.setTenantId(tenantId); + } + + @Override + protected OtaPackage prepare(EntitiesImportCtx ctx, OtaPackage otaPackage, OtaPackage oldOtaPackage, OtaPackageExportData exportData, IdProvider idProvider) { + otaPackage.setDeviceProfileId(idProvider.getInternalId(otaPackage.getDeviceProfileId())); + return otaPackage; + } + + @Override + protected OtaPackage findExistingEntity(EntitiesImportCtx ctx, OtaPackage otaPackage, IdProvider idProvider) { + OtaPackage existingOtaPackage = super.findExistingEntity(ctx, otaPackage, idProvider); + if (existingOtaPackage == null && ctx.isFindExistingByName()) { + existingOtaPackage = otaPackageService.findOtaPackageByTenantIdAndTitleAndVersion(ctx.getTenantId(), otaPackage.getTitle(), otaPackage.getVersion()); + } + return existingOtaPackage; + } + + @Override + protected OtaPackage deepCopy(OtaPackage otaPackage) { + return new OtaPackage(otaPackage); + } + + @Override + protected OtaPackage saveOrUpdate(EntitiesImportCtx ctx, OtaPackage otaPackage, OtaPackageExportData exportData, IdProvider idProvider, CompareResult compareResult) { + if (otaPackage.hasUrl()) { + OtaPackageInfo info = new OtaPackageInfo(otaPackage); + return new OtaPackage(otaPackageService.saveOtaPackageInfo(info, info.hasUrl())); + } + return otaPackageService.saveOtaPackage(otaPackage); + } + + @Override + public EntityType getEntityType() { + return EntityType.OTA_PACKAGE; + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/sync/vc/DefaultEntitiesVersionControlService.java b/application/src/main/java/org/thingsboard/server/service/sync/vc/DefaultEntitiesVersionControlService.java index 420e85dc6c..5fe891c847 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/vc/DefaultEntitiesVersionControlService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/vc/DefaultEntitiesVersionControlService.java @@ -114,9 +114,8 @@ public class DefaultEntitiesVersionControlService implements EntitiesVersionCont private final TbTransactionalCache taskCache; private final VersionControlExecutor executor; - @SuppressWarnings("UnstableApiUsage") @Override - public ListenableFuture saveEntitiesVersion(User user, VersionCreateRequest request) throws Exception { + public ListenableFuture saveEntitiesVersion(User user, VersionCreateRequest request) { checkBranchName(request.getBranch()); var pendingCommit = gitServiceQueue.prepareCommit(user, request); DonAsynchron.withCallback(pendingCommit, commit -> { @@ -546,7 +545,7 @@ public class DefaultEntitiesVersionControlService implements EntitiesVersionCont } @Override - public ListenableFuture autoCommit(User user, EntityId entityId) throws Exception { + public ListenableFuture autoCommit(User user, EntityId entityId) { var repositorySettings = repositorySettingsService.get(user.getTenantId()); if (repositorySettings == null || repositorySettings.isReadOnly()) { return Futures.immediateFuture(null); @@ -573,7 +572,7 @@ public class DefaultEntitiesVersionControlService implements EntitiesVersionCont } @Override - public ListenableFuture autoCommit(User user, EntityType entityType, List entityIds) throws Exception { + public ListenableFuture autoCommit(User user, EntityType entityType, List entityIds) { var repositorySettings = repositorySettingsService.get(user.getTenantId()); if (repositorySettings == null || repositorySettings.isReadOnly()) { return Futures.immediateFuture(null); diff --git a/application/src/main/java/org/thingsboard/server/service/sync/vc/EntitiesVersionControlService.java b/application/src/main/java/org/thingsboard/server/service/sync/vc/EntitiesVersionControlService.java index 3f22120d9c..e5ff89ebbd 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/vc/EntitiesVersionControlService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/vc/EntitiesVersionControlService.java @@ -69,9 +69,9 @@ public interface EntitiesVersionControlService { ListenableFuture checkVersionControlAccess(TenantId tenantId, RepositorySettings settings) throws Exception; - ListenableFuture autoCommit(User user, EntityId entityId) throws Exception; + ListenableFuture autoCommit(User user, EntityId entityId); - ListenableFuture autoCommit(User user, EntityType entityType, List entityIds) throws Exception; + ListenableFuture autoCommit(User user, EntityType entityType, List entityIds); ListenableFuture getEntityDataInfo(User user, EntityId entityId, String versionId); 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 fd9d8b7141..8c4a375fae 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 @@ -16,6 +16,7 @@ package org.thingsboard.server.service.telemetry; 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; @@ -171,6 +172,11 @@ public class DefaultAlarmSubscriptionService extends AbstractSubscriptionService return alarmService.findLatestActiveByOriginatorAndType(tenantId, originator, type); } + @Override + public FluentFuture findLatestActiveByOriginatorAndTypeAsync(TenantId tenantId, EntityId originator, String type) { + return alarmService.findLatestActiveByOriginatorAndTypeAsync(tenantId, originator, type); + } + @Override public Alarm findLatestByOriginatorAndType(TenantId tenantId, EntityId originator, String type) { return alarmService.findLatestActiveByOriginatorAndType(tenantId, originator, type); 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..934eadd98f --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/utils/CalculatedFieldArgumentUtils.java @@ -0,0 +1,75 @@ +/** + * 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.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.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.geofencing.GeofencingCalculatedFieldState; +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 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 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 CalculatedFieldState createStateByType(CalculatedFieldCtx ctx) { + return switch (ctx.getCfType()) { + case SIMPLE -> new SimpleCalculatedFieldState(ctx.getArgNames()); + case SCRIPT -> new ScriptCalculatedFieldState(ctx.getArgNames()); + case GEOFENCING -> new GeofencingCalculatedFieldState(ctx.getArgNames()); + }; + } + +} 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..4e93c8233e 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,43 @@ */ package org.thingsboard.server.utils; +import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.StringUtils; 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.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.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.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 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 { @@ -79,6 +91,8 @@ public class CalculatedFieldUtils { builder.addSingleValueArguments(toSingleValueArgumentProto(argName, singleValueArgumentEntry)); } else if (argEntry instanceof TsRollingArgumentEntry rollingArgumentEntry) { builder.addRollingValueArguments(toRollingArgumentProto(argName, rollingArgumentEntry)); + } else if (argEntry instanceof GeofencingArgumentEntry geofencingArgumentEntry) { + builder.addGeofencingArguments(toGeofencingArgumentProto(argName, geofencingArgumentEntry)); } }); return builder.build(); @@ -108,6 +122,27 @@ public class CalculatedFieldUtils { return builder.build(); } + 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(CalculatedFieldStateProto proto) { if (StringUtils.isEmpty(proto.getType())) { return null; @@ -118,6 +153,7 @@ public class CalculatedFieldUtils { CalculatedFieldState state = switch (type) { case SIMPLE -> new SimpleCalculatedFieldState(); case SCRIPT -> new ScriptCalculatedFieldState(); + case GEOFENCING -> new GeofencingCalculatedFieldState(); }; proto.getSingleValueArgumentsList().forEach(argProto -> @@ -128,6 +164,11 @@ public class CalculatedFieldUtils { state.getArguments().put(argProto.getKey(), fromRollingArgumentProto(argProto))); } + if (CalculatedFieldType.GEOFENCING.equals(type)) { + proto.getGeofencingArgumentsList().forEach(argProto -> + state.getArguments().put(argProto.getArgName(), fromGeofencingArgumentProto(argProto))); + } + return state; } @@ -149,4 +190,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/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 c3d28f9fce..68cd479411 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: @@ -208,7 +211,7 @@ ui: # Help parameters help: # Base URL for UI help assets - base-url: "${UI_HELP_BASE_URL:https://raw.githubusercontent.com/thingsboard/thingsboard-ui-help/release-4.1}" + base-url: "${UI_HELP_BASE_URL:https://raw.githubusercontent.com/thingsboard/thingsboard-ui-help/release-4.3}" # Database telemetry parameters database: @@ -323,7 +326,7 @@ cassandra: poll_ms: "${CASSANDRA_QUERY_POLL_MS:50}" # Interval in milliseconds for printing Cassandra query queue statistic rate_limit_print_interval_ms: "${CASSANDRA_QUERY_RATE_LIMIT_PRINT_MS:10000}" - # set all data type values except target to null for the same ts on save + # When saving a value, set other data types to null (to avoid having multiple telemetry values with the same timestamp). set_null_values_enabled: "${CASSANDRA_QUERY_SET_NULL_VALUES_ENABLED:true}" # log one of cassandra queries with specified frequency (0 - logging is disabled) print_queries_freq: "${CASSANDRA_QUERY_PRINT_FREQ:0}" @@ -464,6 +467,14 @@ actors: allow_system_sms_service: "${ACTORS_RULE_ALLOW_SYSTEM_SMS_SERVICE:true}" # Specify thread pool size for external call service external_call_thread_pool_size: "${ACTORS_RULE_EXTERNAL_CALL_THREAD_POOL_SIZE:50}" + # Configuration for the thread pool that executes HTTP calls to AI provider APIs + ai-requests-thread-pool: + # The base name for threads + pool-name: "${ACTORS_RULE_AI_REQUESTS_THREAD_POOL_NAME:ai-requests}" + # The maximum number of concurrent HTTP requests + pool-size: "${ACTORS_RULE_AI_REQUESTS_THREAD_POOL_SIZE:50}" + # The maximum time in seconds to wait for active tasks to complete during graceful shutdown + termination-timeout-seconds: "${ACTORS_RULE_AI_REQUESTS_THREAD_POOL_TERMINATION_TIMEOUT_SECONDS:60}" chain: # Errors for particular actors are persisted once per specified amount of milliseconds error_persist_frequency: "${ACTORS_RULE_CHAIN_ERROR_FREQUENCY:3000}" @@ -648,6 +659,9 @@ cache: trendzSettings: timeToLiveInMinutes: "${CACHE_SPECS_TRENDZ_SETTINGS_TTL:1440}" # Trendz settings cache TTL maxSize: "${CACHE_SPECS_TRENDZ_SETTINGS_MAX_SIZE:10000}" # 0 means the cache is disabled + aiModel: + timeToLiveInMinutes: "${CACHE_SPECS_AI_MODEL_TTL:1440}" # AI model cache TTL + maxSize: "${CACHE_SPECS_AI_MODEL_MAX_SIZE:10000}" # 0 means the cache is disabled # Deliberately placed outside the 'specs' group above notificationRules: @@ -731,9 +745,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 @@ -847,22 +861,23 @@ audit-log: # Allowed values: OFF (disable), W (log write operations), RW (log read and write operations) logging-level: mask: - "device": "${AUDIT_LOG_MASK_DEVICE:W}" # Device logging levels. Allowed values: OFF (disable), W (log write operations), RW (log read and write operation - "asset": "${AUDIT_LOG_MASK_ASSET:W}" # Asset logging levels. Allowed values: OFF (disable), W (log write operations), RW (log read and write operation - "dashboard": "${AUDIT_LOG_MASK_DASHBOARD:W}" # Dashboard logging levels. Allowed values: OFF (disable), W (log write operations), RW (log read and write operation - "widget_type": "${AUDIT_LOG_MASK_WIDGET_TYPE:W}" # Widget type logging levels. Allowed values: OFF (disable), W (log write operations), RW (log read and write operation - "widgets_bundle": "${AUDIT_LOG_MASK_WIDGETS_BUNDLE:W}" # Widget bundles logging levels. Allowed values: OFF (disable), W (log write operations), RW (log read and write operation - "customer": "${AUDIT_LOG_MASK_CUSTOMER:W}" # Customer logging levels. Allowed values: OFF (disable), W (log write operations), RW (log read and write operation - "user": "${AUDIT_LOG_MASK_USER:W}" # User logging levels. Allowed values: OFF (disable), W (log write operations), RW (log read and write operation - "rule_chain": "${AUDIT_LOG_MASK_RULE_CHAIN:W}" # Rule chain logging levels. Allowed values: OFF (disable), W (log write operations), RW (log read and write operation - "alarm": "${AUDIT_LOG_MASK_ALARM:W}" # Alarm logging levels. Allowed values: OFF (disable), W (log write operations), RW (log read and write operation - "entity_view": "${AUDIT_LOG_MASK_ENTITY_VIEW:W}" # Entity view logging levels. Allowed values: OFF (disable), W (log write operations), RW (log read and write operation - "device_profile": "${AUDIT_LOG_MASK_DEVICE_PROFILE:W}" # Device profile logging levels. Allowed values: OFF (disable), W (log write operations), RW (log read and write operation - "asset_profile": "${AUDIT_LOG_MASK_ASSET_PROFILE:W}" # Asset profile logging levels. Allowed values: OFF (disable), W (log write operations), RW (log read and write operation - "edge": "${AUDIT_LOG_MASK_EDGE:W}" # Edge logging levels. Allowed values: OFF (disable), W (log write operations), RW (log read and write operation - "tb_resource": "${AUDIT_LOG_MASK_RESOURCE:W}" # TB resource logging levels. Allowed values: OFF (disable), W (log write operations), RW (log read and write operation - "ota_package": "${AUDIT_LOG_MASK_OTA_PACKAGE:W}" # Ota package logging levels. Allowed values: OFF (disable), W (log write operations), RW (log read and write operation - "calculated_field": "${AUDIT_LOG_MASK_CALCULATED_FIELD:W}" # Calculated field logging levels. Allowed values: OFF (disable), W (log write operations), RW (log read and write operation + "device": "${AUDIT_LOG_MASK_DEVICE:W}" # Device logging levels. + "asset": "${AUDIT_LOG_MASK_ASSET:W}" # Asset logging levels. + "dashboard": "${AUDIT_LOG_MASK_DASHBOARD:W}" # Dashboard logging levels. + "widget_type": "${AUDIT_LOG_MASK_WIDGET_TYPE:W}" # Widget type logging levels. + "widgets_bundle": "${AUDIT_LOG_MASK_WIDGETS_BUNDLE:W}" # Widget bundles logging levels. + "customer": "${AUDIT_LOG_MASK_CUSTOMER:W}" # Customer logging levels. + "user": "${AUDIT_LOG_MASK_USER:W}" # User logging levels. + "rule_chain": "${AUDIT_LOG_MASK_RULE_CHAIN:W}" # Rule chain logging levels. + "alarm": "${AUDIT_LOG_MASK_ALARM:W}" # Alarm logging levels. + "entity_view": "${AUDIT_LOG_MASK_ENTITY_VIEW:W}" # Entity view logging levels. + "device_profile": "${AUDIT_LOG_MASK_DEVICE_PROFILE:W}" # Device profile logging levels. + "asset_profile": "${AUDIT_LOG_MASK_ASSET_PROFILE:W}" # Asset profile logging levels. + "edge": "${AUDIT_LOG_MASK_EDGE:W}" # Edge logging levels. + "tb_resource": "${AUDIT_LOG_MASK_RESOURCE:W}" # TB resource logging levels. + "ota_package": "${AUDIT_LOG_MASK_OTA_PACKAGE:W}" # Ota package logging levels. + "calculated_field": "${AUDIT_LOG_MASK_CALCULATED_FIELD:W}" # Calculated field logging levels. + "ai_model": "${AUDIT_LOG_MASK_AI_MODEL:W}" # AI model logging levels. sink: # Type of external sink. possible options: none, elasticsearch type: "${AUDIT_LOG_SINK_TYPE:none}" @@ -1468,6 +1483,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 @@ -1479,6 +1499,13 @@ edges: state: # Persist state of edge (active, last connect, last disconnect) into timeseries or attributes tables. 'false' means to store edge state into attributes table persistToTelemetry: "${EDGES_PERSIST_STATE_TO_TELEMETRY:false}" + stats: + # Enable or disable reporting of edge communication stats (true or false) + enabled: "${EDGES_STATS_ENABLED:true}" + # Time-to-live in days for stored edge communication stats in timeseries + ttl: "${EDGES_STATS_TTL:30}" + # How often to report edge communication stats in milliseconds + report-interval-millis: "${EDGES_STATS_REPORT_INTERVAL_MS:600000}" # Spring doc common parameters springdoc: @@ -1515,6 +1542,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, list or full) + doc_expansion: "${SWAGGER_DOC_EXPANSION:list}" # Queue configuration parameters queue: @@ -1661,7 +1690,7 @@ queue: # Kafka properties for Notifications topics notifications: "${TB_QUEUE_KAFKA_NOTIFICATIONS_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:52428800;retention.bytes:1048576000;partitions:1;min.insync.replicas:1}" # Kafka properties for JS Executor topics - js-executor: "${TB_QUEUE_KAFKA_JE_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:52428800;retention.bytes:104857600;partitions:100;min.insync.replicas:1}" + js-executor: "${TB_QUEUE_KAFKA_JE_TOPIC_PROPERTIES:retention.ms:86400000;segment.bytes:52428800;retention.bytes:104857600;partitions:30;min.insync.replicas:1}" # Kafka properties for OTA updates topic ota-updates: "${TB_QUEUE_KAFKA_OTA_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:52428800;retention.bytes:1048576000;partitions:10;min.insync.replicas:1}" # Kafka properties for Version Control topic 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..b6a1faa1ed 100644 --- a/application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.cf; +import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ObjectNode; import org.junit.Test; @@ -23,28 +24,42 @@ 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.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.ReferencedEntityKey; +import org.thingsboard.server.common.data.cf.configuration.RelationQueryDynamicSourceConfiguration; 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.controller.CalculatedFieldControllerTest; 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.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 { @@ -121,7 +136,7 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes 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) @@ -606,6 +621,339 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes }); } + @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 RelationQueryDynamicSourceConfiguration(); + allowedZoneDynamicSourceConfiguration.setDirection(EntitySearchDirection.FROM); + allowedZoneDynamicSourceConfiguration.setRelationType("AllowedZone"); + allowedZoneDynamicSourceConfiguration.setMaxLevel(1); + allowedZoneDynamicSourceConfiguration.setFetchLastLevelOnly(true); + allowedZonesGroup.setRefDynamicSourceConfiguration(allowedZoneDynamicSourceConfiguration); + + ZoneGroupConfiguration restrictedZonesGroup = new ZoneGroupConfiguration("zone", REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS, false); + var restrictedZoneDynamicSourceConfiguration = new RelationQueryDynamicSourceConfiguration(); + restrictedZoneDynamicSourceConfiguration.setDirection(EntitySearchDirection.FROM); + restrictedZoneDynamicSourceConfiguration.setRelationType("RestrictedZone"); + restrictedZoneDynamicSourceConfiguration.setMaxLevel(1); + restrictedZoneDynamicSourceConfiguration.setFetchLastLevelOnly(true); + 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(); + foundTenantProfile.getDefaultProfileConfiguration().setMinAllowedScheduledUpdateIntervalInSecForCF(TIMEOUT / 10); + TenantProfile savedTenantProfile = doPost("/api/tenantProfile", foundTenantProfile, TenantProfile.class); + assertThat(savedTenantProfile).isNotNull(); + assertThat(savedTenantProfile.getDefaultProfileConfiguration().getMinAllowedScheduledUpdateIntervalInSecForCF()).isEqualTo(TIMEOUT / 10); + 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 RelationQueryDynamicSourceConfiguration(); + allowedZoneDynamicSourceConfiguration.setDirection(EntitySearchDirection.FROM); + allowedZoneDynamicSourceConfiguration.setRelationType("AllowedZone"); + allowedZoneDynamicSourceConfiguration.setMaxLevel(1); + allowedZoneDynamicSourceConfiguration.setFetchLastLevelOnly(true); + 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(6); + + 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()); + + awaitForCalculatedFieldEntityMessageProcessorToRegisterCfStateAsDirty(device.getId(), savedCalculatedField.getId()); + + // --- 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"); + }); + } + 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 +969,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/controller/AbstractWebTest.java b/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java index 43cab3ca3f..e3fbef46a6 100644 --- a/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java @@ -68,7 +68,10 @@ 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; @@ -99,6 +102,7 @@ import org.thingsboard.server.common.data.device.profile.ProtoTransportPayloadCo import org.thingsboard.server.common.data.device.profile.TransportPayloadTypeConfiguration; import org.thingsboard.server.common.data.edge.Edge; 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; @@ -151,6 +155,7 @@ 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.entitiy.tenant.profile.TbTenantProfileService; import org.thingsboard.server.service.security.auth.jwt.RefreshTokenRequest; import org.thingsboard.server.service.security.auth.rest.LoginRequest; @@ -1105,6 +1110,17 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest { }); } + protected void awaitForCalculatedFieldEntityMessageProcessorToRegisterCfStateAsDirty(EntityId entityId, CalculatedFieldId cfId) { + CalculatedFieldEntityMessageProcessor processor = getCalculatedFieldEntityMessageProcessor(entityId); + Map statesMap = (Map) ReflectionTestUtils.getField(processor, "states"); + Awaitility.await("CF state for entity actor marked as dirty").atMost(5, TimeUnit.SECONDS).until(() -> { + CalculatedFieldState calculatedFieldState = statesMap.get(cfId); + boolean stateDirty = calculatedFieldState != null && calculatedFieldState.isDirty(); + log.warn("entityId {}, cfId {}, state dirty == {}", entityId, cfId, stateDirty); + return stateDirty; + }); + } + protected static String getMapName(FeatureType featureType) { switch (featureType) { case ATTRIBUTES: @@ -1126,6 +1142,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(); diff --git a/application/src/test/java/org/thingsboard/server/controller/AiModelControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/AiModelControllerTest.java new file mode 100644 index 0000000000..ae2972b0cc --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/controller/AiModelControllerTest.java @@ -0,0 +1,615 @@ +/** + * 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.controller; + +import com.datastax.oss.driver.api.core.uuid.Uuids; +import com.fasterxml.jackson.core.type.TypeReference; +import org.junit.Test; +import org.springframework.test.web.servlet.ResultActions; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.ai.AiModel; +import org.thingsboard.server.common.data.ai.model.chat.AnthropicChatModelConfig; +import org.thingsboard.server.common.data.ai.model.chat.GoogleAiGeminiChatModelConfig; +import org.thingsboard.server.common.data.ai.model.chat.OpenAiChatModelConfig; +import org.thingsboard.server.common.data.ai.provider.AnthropicProviderConfig; +import org.thingsboard.server.common.data.ai.provider.GoogleAiGeminiProviderConfig; +import org.thingsboard.server.common.data.ai.provider.OpenAiProviderConfig; +import org.thingsboard.server.common.data.id.AiModelId; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.common.data.page.SortOrder; +import org.thingsboard.server.dao.service.DaoSqlTest; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.is; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@DaoSqlTest +public class AiModelControllerTest extends AbstractControllerTest { + + /* --- Save API tests --- */ + + @Test + public void saveAiModel_whenUserIsSysAdmin_shouldReturnForbidden() throws Exception { + // GIVEN + loginSysAdmin(); + + AiModel model = constructValidOpenAiModel("Test model"); + + // WHEN + ResultActions result = doPost("/api/ai/model", model); + + // THEN + result.andExpect(status().isForbidden()).andExpect(statusReason(equalTo(msgErrorPermission))); + } + + @Test + public void saveAiModel_whenUserIsCustomerUser_shouldReturnForbidden() throws Exception { + // GIVEN + loginCustomerUser(); + + AiModel model = constructValidOpenAiModel("Test model"); + + // WHEN + ResultActions result = doPost("/api/ai/model", model); + + // THEN + result.andExpect(status().isForbidden()).andExpect(statusReason(equalTo(msgErrorPermission))); + } + + @Test + public void saveAiModel_whenCreatingValidModelAsTenantAdmin_shouldSucceed() throws Exception { + // GIVEN + loginTenantAdmin(); + + AiModel model = constructValidOpenAiModel("Test model"); + + // WHEN + var savedModel = doPost("/api/ai/model", model, AiModel.class); + + // THEN + + // verify returned object + assertThat(savedModel.getId()).isNotNull(); + assertThat(savedModel.getUuidId()).isNotNull().isNotEqualTo(EntityId.NULL_UUID); + assertThat(savedModel.getId().getEntityType()).isEqualTo(EntityType.AI_MODEL); + assertThat(savedModel.getCreatedTime()).isPositive(); + assertThat(savedModel.getVersion()).isEqualTo(1); + assertThat(savedModel.getTenantId()).isEqualTo(tenantId); + assertThat(savedModel.getName()).isEqualTo("Test model"); + assertThat(savedModel.getConfiguration()).isEqualTo(model.getConfiguration()); + assertThat(savedModel.getExternalId()).isNull(); + } + + @Test + public void saveAiModel_whenUpdatingExistingModelAsTenantAdmin_shouldSucceed() throws Exception { + // GIVEN + loginTenantAdmin(); + + var model = doPost("/api/ai/model", constructValidOpenAiModel("Test model"), AiModel.class); + + var newModelConfig = OpenAiChatModelConfig.builder() + .providerConfig(new OpenAiProviderConfig("test-api-key-updated")) + .modelId("o4-mini") + .temperature(0.2) + .topP(0.4) + .frequencyPenalty(0.2) + .presencePenalty(0.5) + .maxOutputTokens(2000) + .timeoutSeconds(20) + .maxRetries(0) + .build(); + + model.setName("Test model updated"); + model.setConfiguration(newModelConfig); + + // WHEN + var updatedModel = doPost("/api/ai/model", model, AiModel.class); + + // THEN + + // verify returned object + assertThat(updatedModel.getId()).isEqualTo(model.getId()); + assertThat(updatedModel.getCreatedTime()).isEqualTo(model.getCreatedTime()); + assertThat(updatedModel.getVersion()).isEqualTo(2); + assertThat(updatedModel.getTenantId()).isEqualTo(tenantId); + assertThat(updatedModel.getName()).isEqualTo("Test model updated"); + assertThat(updatedModel.getConfiguration()).isEqualTo(newModelConfig); + assertThat(updatedModel.getExternalId()).isNull(); + } + + /* --- Get by ID API tests --- */ + + @Test + public void getAiModelById_whenUserIsSysAdmin_shouldReturnForbidden() throws Exception { + // GIVEN + loginSysAdmin(); + + // WHEN + ResultActions result = doGet("/api/ai/model/" + Uuids.timeBased()); + + // THEN + result.andExpect(status().isForbidden()).andExpect(statusReason(equalTo(msgErrorPermission))); + } + + @Test + public void getAiModelById_whenUserIsCustomerUser_shouldReturnForbidden() throws Exception { + // GIVEN + loginCustomerUser(); + + // WHEN + ResultActions result = doGet("/api/ai/model/" + Uuids.timeBased()); + + // THEN + result.andExpect(status().isForbidden()).andExpect(statusReason(equalTo(msgErrorPermission))); + } + + @Test + public void getAiModelById_whenGettingExistingModelAsTenantAdmin_shouldReturnModel() throws Exception { + // GIVEN + loginTenantAdmin(); + + var saved = doPost("/api/ai/model", constructValidOpenAiModel("Test model"), AiModel.class); + + // WHEN + AiModel actual = doGet("/api/ai/model/" + saved.getId(), AiModel.class); + + // THEN + assertThat(actual).isEqualTo(saved); + } + + @Test + public void getAiModelById_whenGettingNonexistentModelAsTenantAdmin_shouldReturnNotFound() throws Exception { + // GIVEN + loginTenantAdmin(); + + var nonexistentModelId = new AiModelId(Uuids.timeBased()); + + // WHEN + ResultActions result = doGet("/api/ai/model/" + nonexistentModelId); + + // THEN + result.andExpect(status().isNotFound()) + .andExpect(statusReason(is("AI model with id [" + nonexistentModelId + "] is not found"))); + } + + /* --- Get paged API tests --- */ + + @Test + public void getAiModels_whenUserIsSysAdmin_shouldReturnForbidden() throws Exception { + // GIVEN + loginSysAdmin(); + + // WHEN + ResultActions result = doGet("/api/ai/model?pageSize=10&page=0"); + + // THEN + result.andExpect(status().isForbidden()).andExpect(statusReason(equalTo(msgErrorPermission))); + } + + @Test + public void getAiModels_whenUserIsCustomerUser_shouldReturnForbidden() throws Exception { + // GIVEN + loginCustomerUser(); + + // WHEN + ResultActions result = doGet("/api/ai/model?pageSize=10&page=0"); + + // THEN + result.andExpect(status().isForbidden()).andExpect(statusReason(equalTo(msgErrorPermission))); + } + + @Test + public void getAiModels_testPagination() throws Exception { + // GIVEN + loginTenantAdmin(); + + var model1 = doPost("/api/ai/model", constructValidOpenAiModel("Test model 1"), AiModel.class); + var model2 = doPost("/api/ai/model", constructValidOpenAiModel("Test model 2"), AiModel.class); + var model3 = doPost("/api/ai/model", constructValidOpenAiModel("Test model 3"), AiModel.class); + var model4 = doPost("/api/ai/model", constructValidOpenAiModel("Test model 4"), AiModel.class); + var model5 = doPost("/api/ai/model", constructValidOpenAiModel("Test model 5"), AiModel.class); + + // WHEN + PageData result = doGetTypedWithPageLink("/api/ai/model?", new TypeReference<>() {}, new PageLink(2, 1)); + + // THEN + assertThat(result.getData()).containsExactly(model3, model4); + assertThat(result.getTotalPages()).isEqualTo(3); + assertThat(result.getTotalElements()).isEqualTo(5); + assertThat(result.hasNext()).isTrue(); + } + + @Test + public void getAiModels_testSearchAndSortAppliedBeforePagination() throws Exception { + // GIVEN + loginTenantAdmin(); + + // Create 5 models: 3 with "Alpha" in name, 2 with "Beta" in name + var alpha1 = doPost("/api/ai/model", constructValidOpenAiModel("Alpha Model 1"), AiModel.class); + var beta1 = doPost("/api/ai/model", constructValidOpenAiModel("Beta Model 1"), AiModel.class); + var alpha2 = doPost("/api/ai/model", constructValidOpenAiModel("Alpha Model 2"), AiModel.class); + var beta2 = doPost("/api/ai/model", constructValidOpenAiModel("Beta Model 2"), AiModel.class); + var alpha3 = doPost("/api/ai/model", constructValidOpenAiModel("Alpha Model 3"), AiModel.class); + + // WHEN + // Search for "Alpha", sort by name DESC, get the first page with size 2 + PageData result = doGetTypedWithPageLink("/api/ai/model?", + new TypeReference<>() {}, + new PageLink(2, 0, "Alpha", SortOrder.of("name", SortOrder.Direction.DESC))); + + // THEN + // Should find only 3 "Alpha" models, sort them DESC (3, 2, 1), then return first 2 + assertThat(result.getData()).containsExactly(alpha3, alpha2); + assertThat(result.getTotalPages()).isEqualTo(2); // One more "Alpha" model on the next page + assertThat(result.getTotalElements()).isEqualTo(3); // Only 3 models match "Alpha", not 5 + assertThat(result.hasNext()).isTrue(); // One more "Alpha" model on the next page + } + + @Test + public void getAiModels_testTextSearch() throws Exception { + // GIVEN + loginTenantAdmin(); + + var model1 = doPost("/api/ai/model", AiModel.builder() + .tenantId(tenantId) + .name("Test model 1") + .configuration(OpenAiChatModelConfig.builder() + .providerConfig(new OpenAiProviderConfig("test-api-key")) + .modelId("o3-pro") + .build()) + .build(), AiModel.class); + var model2 = doPost("/api/ai/model", AiModel.builder() + .tenantId(tenantId) + .name("Test model 2") + .configuration(GoogleAiGeminiChatModelConfig.builder() + .providerConfig(new GoogleAiGeminiProviderConfig("test-api-key")) + .modelId("gemini-2.5-flash") + .build()) + .build(), AiModel.class); + var model3 = doPost("/api/ai/model", AiModel.builder() + .tenantId(tenantId) + .name("Test model 3") + .configuration(GoogleAiGeminiChatModelConfig.builder() + .providerConfig(new GoogleAiGeminiProviderConfig("test-api-key")) + .modelId("gemini-2.5-pro") + .build()) + .build(), AiModel.class); + + // WHEN + int pageSize = 10; + int page = 0; + SortOrder sortOrder = null; + + PageData result1 = doGetTypedWithPageLink("/api/ai/model?", new TypeReference<>() {}, new PageLink(pageSize, page, "google ai", sortOrder)); + + PageData result2 = doGetTypedWithPageLink("/api/ai/model?", new TypeReference<>() {}, new PageLink(pageSize, page, "pro", sortOrder)); + + PageData result3 = doGetTypedWithPageLink("/api/ai/model?", new TypeReference<>() {}, new PageLink(pageSize, page, "test", sortOrder)); + + PageData result4 = doGetTypedWithPageLink("/api/ai/model?", new TypeReference<>() {}, new PageLink(pageSize, page, "anthropic", sortOrder)); + + // THEN + + // should find google models + assertThat(result1.getData()).containsExactly(model2, model3); + assertThat(result1.getTotalPages()).isEqualTo(1); + assertThat(result1.getTotalElements()).isEqualTo(2); + assertThat(result1.hasNext()).isFalse(); + + // should find "o3-pro" and "gemini-2.5-pro" models + assertThat(result2.getData()).containsExactly(model1, model3); + assertThat(result2.getTotalPages()).isEqualTo(1); + assertThat(result2.getTotalElements()).isEqualTo(2); + assertThat(result2.hasNext()).isFalse(); + + // should find all models (all contain "Test" in their names) + assertThat(result3.getData()).containsExactly(model1, model2, model3); + assertThat(result3.getTotalPages()).isEqualTo(1); + assertThat(result3.getTotalElements()).isEqualTo(3); + assertThat(result3.hasNext()).isFalse(); + + // should find no models (nothing matches "anthropic") + assertThat(result4.getData()).isEmpty(); + assertThat(result4.getTotalPages()).isEqualTo(0); + assertThat(result4.getTotalElements()).isEqualTo(0); + assertThat(result4.hasNext()).isFalse(); + } + + @Test + public void getAiModels_testSortingByCreatedTime() throws Exception { + // GIVEN + loginTenantAdmin(); + + var model1 = doPost("/api/ai/model", constructValidOpenAiModel("Test model 1"), AiModel.class); + var model2 = doPost("/api/ai/model", constructValidOpenAiModel("Test model 2"), AiModel.class); + + // WHEN + int pageSize = 2; + int page = 0; + String textSearch = null; + + PageData resultAsc = doGetTypedWithPageLink( + "/api/ai/model?", new TypeReference<>() {}, + new PageLink(pageSize, page, textSearch, SortOrder.of("createdTime", SortOrder.Direction.ASC)) + ); + PageData resultDesc = doGetTypedWithPageLink( + "/api/ai/model?", new TypeReference<>() {}, + new PageLink(pageSize, page, textSearch, SortOrder.of("createdTime", SortOrder.Direction.DESC)) + ); + + // THEN + assertThat(resultAsc.getData()).containsExactly(model1, model2); + assertThat(resultAsc.getTotalPages()).isEqualTo(1); + assertThat(resultAsc.getTotalElements()).isEqualTo(2); + assertThat(resultAsc.hasNext()).isFalse(); + + assertThat(resultDesc.getData()).containsExactly(model2, model1); + assertThat(resultDesc.getTotalPages()).isEqualTo(1); + assertThat(resultDesc.getTotalElements()).isEqualTo(2); + assertThat(resultDesc.hasNext()).isFalse(); + } + + @Test + public void getAiModels_testSortingByName() throws Exception { + // GIVEN + loginTenantAdmin(); + + var modelA = doPost("/api/ai/model", constructValidOpenAiModel("Test model A"), AiModel.class); + var modelB = doPost("/api/ai/model", constructValidOpenAiModel("Test model B"), AiModel.class); + + // WHEN + int pageSize = 2; + int page = 0; + String textSearch = null; + + PageData resultAsc = doGetTypedWithPageLink( + "/api/ai/model?", new TypeReference<>() {}, + new PageLink(pageSize, page, textSearch, SortOrder.of("name", SortOrder.Direction.ASC)) + ); + PageData resultDesc = doGetTypedWithPageLink( + "/api/ai/model?", new TypeReference<>() {}, + new PageLink(pageSize, page, textSearch, SortOrder.of("name", SortOrder.Direction.DESC)) + ); + + // THEN + assertThat(resultAsc.getData()).containsExactly(modelA, modelB); + assertThat(resultAsc.getTotalPages()).isEqualTo(1); + assertThat(resultAsc.getTotalElements()).isEqualTo(2); + assertThat(resultAsc.hasNext()).isFalse(); + + assertThat(resultDesc.getData()).containsExactly(modelB, modelA); + assertThat(resultDesc.getTotalPages()).isEqualTo(1); + assertThat(resultDesc.getTotalElements()).isEqualTo(2); + assertThat(resultDesc.hasNext()).isFalse(); + } + + @Test + public void getAiModels_testSortingByProvider() throws Exception { + // GIVEN + loginTenantAdmin(); + + var anthropicModel = doPost("/api/ai/model", AiModel.builder() + .tenantId(tenantId) + .name("Test model 1") + .configuration(AnthropicChatModelConfig.builder() + .providerConfig(new AnthropicProviderConfig("test-api-key")) + .modelId("claude-sonnet-4-0") + .build()) + .build(), AiModel.class); + var geminiModel = doPost("/api/ai/model", AiModel.builder() + .tenantId(tenantId) + .name("Test model 2") + .configuration(GoogleAiGeminiChatModelConfig.builder() + .providerConfig(new GoogleAiGeminiProviderConfig("test-api-key")) + .modelId("gemini-2.5-pro") + .build()) + .build(), AiModel.class); + + // WHEN + int pageSize = 2; + int page = 0; + String textSearch = null; + + PageData resultAsc = doGetTypedWithPageLink( + "/api/ai/model?", new TypeReference<>() {}, + new PageLink(pageSize, page, textSearch, SortOrder.of("provider", SortOrder.Direction.ASC)) + ); + PageData resultDesc = doGetTypedWithPageLink( + "/api/ai/model?", new TypeReference<>() {}, + new PageLink(pageSize, page, textSearch, SortOrder.of("provider", SortOrder.Direction.DESC)) + ); + + // THEN + assertThat(resultAsc.getData()).containsExactly(anthropicModel, geminiModel); + assertThat(resultAsc.getTotalPages()).isEqualTo(1); + assertThat(resultAsc.getTotalElements()).isEqualTo(2); + assertThat(resultAsc.hasNext()).isFalse(); + + assertThat(resultDesc.getData()).containsExactly(geminiModel, anthropicModel); + assertThat(resultDesc.getTotalPages()).isEqualTo(1); + assertThat(resultDesc.getTotalElements()).isEqualTo(2); + assertThat(resultDesc.hasNext()).isFalse(); + } + + @Test + public void getAiModels_testSortingByModelId() throws Exception { + // GIVEN + loginTenantAdmin(); + + var modelA = doPost("/api/ai/model", AiModel.builder() + .tenantId(tenantId) + .name("Test model 1") + .configuration(AnthropicChatModelConfig.builder() + .providerConfig(new AnthropicProviderConfig("test-api-key")) + .modelId("model-a") + .build()) + .build(), AiModel.class); + + var modelB = doPost("/api/ai/model", AiModel.builder() + .tenantId(tenantId) + .name("Test model 2") + .configuration(GoogleAiGeminiChatModelConfig.builder() + .providerConfig(new GoogleAiGeminiProviderConfig("test-api-key")) + .modelId("model-b") + .build()) + .build(), AiModel.class); + + // WHEN + int pageSize = 2; + int page = 0; + String textSearch = null; + + PageData resultAsc = doGetTypedWithPageLink( + "/api/ai/model?", new TypeReference<>() {}, + new PageLink(pageSize, page, textSearch, SortOrder.of("modelId", SortOrder.Direction.ASC)) + ); + PageData resultDesc = doGetTypedWithPageLink( + "/api/ai/model?", new TypeReference<>() {}, + new PageLink(pageSize, page, textSearch, SortOrder.of("modelId", SortOrder.Direction.DESC)) + ); + + // THEN + assertThat(resultAsc.getData()).containsExactly(modelA, modelB); + assertThat(resultAsc.getTotalPages()).isEqualTo(1); + assertThat(resultAsc.getTotalElements()).isEqualTo(2); + assertThat(resultAsc.hasNext()).isFalse(); + + assertThat(resultDesc.getData()).containsExactly(modelB, modelA); + assertThat(resultDesc.getTotalPages()).isEqualTo(1); + assertThat(resultDesc.getTotalElements()).isEqualTo(2); + assertThat(resultDesc.hasNext()).isFalse(); + } + + @Test + public void getAiModels_testSortingByIdTieBreaker() throws Exception { + // GIVEN + loginTenantAdmin(); + + // Both models are from OpenAI and sorting will be done on provider + var modelA = doPost("/api/ai/model", constructValidOpenAiModel("Test model A"), AiModel.class); + var modelB = doPost("/api/ai/model", constructValidOpenAiModel("Test model B"), AiModel.class); + + // WHEN + int pageSize = 2; + int page = 0; + String textSearch = null; + + PageData resultAsc = doGetTypedWithPageLink( + "/api/ai/model?", new TypeReference<>() {}, + new PageLink(pageSize, page, textSearch, SortOrder.of("provider", SortOrder.Direction.ASC)) + ); + PageData resultDesc = doGetTypedWithPageLink( + "/api/ai/model?", new TypeReference<>() {}, + new PageLink(pageSize, page, textSearch, SortOrder.of("provider", SortOrder.Direction.DESC)) + ); + + // THEN + + // in both cases result should be the same since in case of ties (both models have OpenAI as provider, sorting by ID ascending is used) + assertThat(resultAsc.getData()).containsExactly(modelA, modelB); + assertThat(resultAsc.getTotalPages()).isEqualTo(1); + assertThat(resultAsc.getTotalElements()).isEqualTo(2); + assertThat(resultAsc.hasNext()).isFalse(); + + assertThat(resultDesc.getData()).containsExactly(modelA, modelB); + assertThat(resultDesc.getTotalPages()).isEqualTo(1); + assertThat(resultDesc.getTotalElements()).isEqualTo(2); + assertThat(resultDesc.hasNext()).isFalse(); + } + + /* --- Delete API tests --- */ + + @Test + public void deleteAiModelById_whenUserIsSysAdmin_shouldReturnForbidden() throws Exception { + // GIVEN + loginSysAdmin(); + + // WHEN + ResultActions result = doDelete("/api/ai/model/" + Uuids.timeBased()); + + // THEN + result.andExpect(status().isForbidden()).andExpect(statusReason(equalTo(msgErrorPermission))); + } + + @Test + public void deleteAiModelById_whenUserIsCustomerUser_shouldReturnForbidden() throws Exception { + // GIVEN + loginCustomerUser(); + + // WHEN + ResultActions result = doDelete("/api/ai/model/" + Uuids.timeBased()); + + // THEN + result.andExpect(status().isForbidden()).andExpect(statusReason(equalTo(msgErrorPermission))); + } + + @Test + public void deleteAiModelById_whenDeletingExistingModelAsTenantAdmin_shouldSucceedAndReturnTrue() throws Exception { + // GIVEN + loginTenantAdmin(); + + var model = doPost("/api/ai/model", constructValidOpenAiModel("Test model"), AiModel.class); + + // WHEN + boolean deleted = doDelete("/api/ai/model/" + model.getId(), Boolean.class); + + // THEN + assertThat(deleted).isTrue(); + + // verify model cannot be found anymore + doGet("/api/ai/model/" + model.getId()) + .andExpect(status().isNotFound()) + .andExpect(statusReason(is("AI model with id [" + model.getId() + "] is not found"))); + } + + @Test + public void deleteAiModelById_whenDeletingNonexistentModelAsTenantAdmin_shouldSucceedAndReturnFalse() throws Exception { + // GIVEN + loginTenantAdmin(); + + var nonexistentModelId = new AiModelId(Uuids.timeBased()); + + // WHEN + boolean deleted = doDelete("/api/ai/model/" + nonexistentModelId, Boolean.class); + + // THEN + assertThat(deleted).isFalse(); + } + + private AiModel constructValidOpenAiModel(String name) { + var modelConfig = OpenAiChatModelConfig.builder() + .providerConfig(new OpenAiProviderConfig("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.builder() + .tenantId(tenantId) + .name(name) + .configuration(modelConfig) + .build(); + } + +} 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 bf1f9c3e82..1f80c1f379 100644 --- a/application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java @@ -44,6 +44,7 @@ import org.thingsboard.server.common.data.query.AlarmCountQuery; import org.thingsboard.server.common.data.query.AlarmData; import org.thingsboard.server.common.data.query.AlarmDataPageLink; import org.thingsboard.server.common.data.query.AlarmDataQuery; +import org.thingsboard.server.common.data.query.AliasEntityId; import org.thingsboard.server.common.data.query.DeviceTypeFilter; import org.thingsboard.server.common.data.query.DynamicValue; import org.thingsboard.server.common.data.query.DynamicValueSourceType; @@ -277,8 +278,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); @@ -368,8 +368,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); @@ -437,9 +436,7 @@ public class EntityQueryControllerTest extends AbstractControllerTest { 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()); + PageData alarmPageData = findAlarmsByQueryAndCheck(assetAlarmQuery, 10); List retrievedAlarmTypes = alarmPageData.getData().stream().map(Alarm::getType).toList(); assertThat(retrievedAlarmTypes).containsExactlyInAnyOrderElementsOf(assetAlarmTypes); @@ -510,9 +507,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); @@ -707,7 +702,7 @@ public class EntityQueryControllerTest extends AbstractControllerTest { } RelationsQueryFilter filter = new RelationsQueryFilter(); - filter.setRootEntity(mainDevice.getId()); + filter.setRootEntity(AliasEntityId.fromEntityId(mainDevice.getId())); filter.setDirection(EntitySearchDirection.FROM); filter.setNegate(true); filter.setFilters(List.of(new RelationEntityTypeFilter("CONTAINS", List.of(EntityType.DEVICE), false))); @@ -923,7 +918,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)); @@ -939,8 +934,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()); @@ -955,7 +953,7 @@ public class EntityQueryControllerTest extends AbstractControllerTest { }); - EntityCountQuery countQuery = new EntityCountQuery(entityTypeFilter); + EntityCountQuery countQuery = new EntityCountQuery(entityTypeFilter, keyFilters); countByQueryAndCheck(countQuery, 97); } @@ -982,10 +980,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)); @@ -1031,10 +1029,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 @@ -1140,31 +1138,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/TbResourceControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/TbResourceControllerTest.java index 569ff840ca..3b194de7b4 100644 --- a/application/src/test/java/org/thingsboard/server/controller/TbResourceControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/TbResourceControllerTest.java @@ -202,6 +202,60 @@ public class TbResourceControllerTest extends AbstractControllerTest { Assert.assertEquals(savedResource.getFileName(), foundResource.getFileName()); } + @Test + public void testFindSystemResourceInfoById() throws Exception { + loginSysAdmin(); + TbResource resource = new TbResource(); + resource.setResourceType(ResourceType.JS_MODULE); + resource.setTitle("My system resource"); + resource.setFileName(DEFAULT_FILE_NAME); + resource.setEncodedData(TEST_DATA); + TbResourceInfo savedResourceInfo = save(resource); + assertThat(savedResourceInfo.getFileName()).isEqualTo(DEFAULT_FILE_NAME); + + TbResourceInfo resourceInfo = findResourceInfo(savedResourceInfo.getId()); + assertThat(resourceInfo).isEqualTo(savedResourceInfo); + loginTenantAdmin(); + resourceInfo = findResourceInfo(savedResourceInfo.getId()); + assertThat(resourceInfo).isEqualTo(savedResourceInfo); + + loginSysAdmin(); + resource = new TbResource(savedResourceInfo); + resource.setFileName(DEFAULT_FILE_NAME_2); + resource.setEncodedData(TEST_DATA); + savedResourceInfo = save(resource); + assertThat(savedResourceInfo.getFileName()).isEqualTo(DEFAULT_FILE_NAME_2); + + resourceInfo = findResourceInfo(savedResourceInfo.getId()); + assertThat(resourceInfo).isEqualTo(savedResourceInfo); + loginTenantAdmin(); + resourceInfo = findResourceInfo(savedResourceInfo.getId()); + assertThat(resourceInfo).isEqualTo(savedResourceInfo); + } + + @Test + public void testFindTenantResourceInfoById() throws Exception { + TbResource resource = new TbResource(); + resource.setResourceType(ResourceType.JS_MODULE); + resource.setTitle("My tenant resource"); + resource.setFileName(DEFAULT_FILE_NAME); + resource.setEncodedData(TEST_DATA); + TbResourceInfo savedResourceInfo = save(resource); + assertThat(savedResourceInfo.getFileName()).isEqualTo(DEFAULT_FILE_NAME); + + TbResourceInfo resourceInfo = findResourceInfo(savedResourceInfo.getId()); + assertThat(resourceInfo).isEqualTo(savedResourceInfo); + + resource = new TbResource(savedResourceInfo); + resource.setFileName(DEFAULT_FILE_NAME_2); + resource.setEncodedData(TEST_DATA); + savedResourceInfo = save(resource); + assertThat(savedResourceInfo.getFileName()).isEqualTo(DEFAULT_FILE_NAME_2); + + resourceInfo = findResourceInfo(savedResourceInfo.getId()); + assertThat(resourceInfo).isEqualTo(savedResourceInfo); + } + @Test public void testDeleteTbResource() throws Exception { TbResource resource = new TbResource(); @@ -878,6 +932,10 @@ public class TbResourceControllerTest extends AbstractControllerTest { }); } + private TbResourceInfo findResourceInfo(TbResourceId id) throws Exception { + return doGet("/api/resource/info/" + id, TbResourceInfo.class); + } + private byte[] download(TbResourceId resourceId) throws Exception { return doGet("/api/resource/" + resourceId + "/download") .andExpect(status().isOk()) 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 a1570c4903..ad3c6d5312 100644 --- a/application/src/test/java/org/thingsboard/server/controller/TelemetryControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/TelemetryControllerTest.java @@ -19,10 +19,12 @@ import com.fasterxml.jackson.databind.node.ObjectNode; import org.junit.Assert; import org.junit.Test; import org.springframework.test.context.TestPropertySource; +import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.SaveDeviceWithCredentialsRequest; import org.thingsboard.server.common.data.kv.BasicTsKvEntry; import org.thingsboard.server.common.data.kv.LongDataEntry; +import org.thingsboard.server.common.data.query.AliasEntityId; import org.thingsboard.server.common.data.query.EntityKey; import org.thingsboard.server.common.data.query.SingleEntityFilter; import org.thingsboard.server.common.data.security.DeviceCredentials; @@ -32,6 +34,7 @@ import org.thingsboard.server.dao.service.DaoSqlTest; import java.util.List; import java.util.concurrent.TimeUnit; +import static org.assertj.core.api.Assertions.assertThat; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import static org.thingsboard.server.common.data.query.EntityKeyType.TIME_SERIES; @@ -115,7 +118,7 @@ public class TelemetryControllerTest extends AbstractControllerTest { Device device = createDevice(); SingleEntityFilter filter = new SingleEntityFilter(); - filter.setSingleEntity(device.getId()); + filter.setSingleEntity(AliasEntityId.fromEntityId(device.getId())); getWsClient().subscribeLatestUpdate(List.of(new EntityKey(TIME_SERIES, "data")), filter); @@ -159,7 +162,7 @@ public class TelemetryControllerTest extends AbstractControllerTest { Device device = createDevice(); SingleEntityFilter filter = new SingleEntityFilter(); - filter.setSingleEntity(device.getId()); + filter.setSingleEntity(AliasEntityId.fromEntityId(device.getId())); getWsClient().subscribeLatestUpdate(List.of(new EntityKey(TIME_SERIES, "data")), filter); @@ -207,6 +210,15 @@ public class TelemetryControllerTest extends AbstractControllerTest { doPostAsync("/api/plugins/telemetry/DEVICE/" + device.getId() + "/timeseries/smth", invalidRequestBody, String.class, status().isBadRequest()); } + @Test + public void testBadRequestReturnedWhenMethodArgumentTypeMismatch() throws Exception { + loginTenantAdmin(); + String content = "{\"key\": \"value\"}"; + doPost("/api/plugins/telemetry/DEVICE/20b559f5-849f-4361-b4f6-b6d0b76687e9/INVALID_SCOPE", content, (String) null) + .andExpect(status().isBadRequest()) + .andExpect(result -> assertThat(result.getResolvedException()).isInstanceOf(MethodArgumentTypeMismatchException.class)); + } + @Test public void testEmptyKeyIsProhibited() throws Exception { loginTenantAdmin(); diff --git a/application/src/test/java/org/thingsboard/server/controller/WebsocketApiTest.java b/application/src/test/java/org/thingsboard/server/controller/WebsocketApiTest.java index 3842cf917b..bebdd5ecb9 100644 --- a/application/src/test/java/org/thingsboard/server/controller/WebsocketApiTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/WebsocketApiTest.java @@ -44,6 +44,7 @@ import org.thingsboard.server.common.data.kv.StringDataEntry; import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.query.AlarmCountQuery; +import org.thingsboard.server.common.data.query.AliasEntityId; import org.thingsboard.server.common.data.query.DeviceTypeFilter; import org.thingsboard.server.common.data.query.EntityCountQuery; import org.thingsboard.server.common.data.query.EntityData; @@ -332,7 +333,7 @@ public class WebsocketApiTest extends AbstractControllerTest { loginTenantAdmin(); SingleEntityFilter singleEntityFilter = new SingleEntityFilter(); - singleEntityFilter.setSingleEntity(tenantId); + singleEntityFilter.setSingleEntity(AliasEntityId.fromEntityId(tenantId)); AlarmCountQuery alarmCountQuery = new AlarmCountQuery(singleEntityFilter); AlarmCountCmd cmd1 = new AlarmCountCmd(1, alarmCountQuery); @@ -356,7 +357,7 @@ public class WebsocketApiTest extends AbstractControllerTest { Assert.assertEquals(1, update.getCount()); // set wrong entity id in filter, check count = 0 - singleEntityFilter.setSingleEntity(tenantAdminUserId); + singleEntityFilter.setSingleEntity(AliasEntityId.fromEntityId(tenantAdminUserId)); AlarmCountCmd cmd3 = new AlarmCountCmd(2, alarmCountQuery); getWsClient().send(cmd3); @@ -865,7 +866,7 @@ public class WebsocketApiTest extends AbstractControllerTest { public void testAttributesSubscription_sysAdmin() throws Exception { loginSysAdmin(); SingleEntityFilter entityFilter = new SingleEntityFilter(); - entityFilter.setSingleEntity(tenantId); + entityFilter.setSingleEntity(AliasEntityId.fromEntityId(tenantId)); assertThatNoException().as("subscribeForAttributes").isThrownBy(() -> { JsonNode update = getWsClient().subscribeForAttributes(tenantId, TbAttributeSubscriptionScope.SERVER_SCOPE.name(), List.of("attr")); diff --git a/application/src/test/java/org/thingsboard/server/edge/AbstractEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/AbstractEdgeTest.java index c02e72a35f..84d879c993 100644 --- a/application/src/test/java/org/thingsboard/server/edge/AbstractEdgeTest.java +++ b/application/src/test/java/org/thingsboard/server/edge/AbstractEdgeTest.java @@ -75,6 +75,7 @@ import org.thingsboard.server.common.data.queue.Queue; 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.security.DeviceCredentials; import org.thingsboard.server.common.data.security.model.JwtSettings; import org.thingsboard.server.controller.AbstractControllerTest; import org.thingsboard.server.dao.edge.EdgeEventService; @@ -565,7 +566,8 @@ abstract public class AbstractEdgeTest extends AbstractControllerTest { protected Device saveDeviceOnCloudAndVerifyDeliveryToEdge() throws Exception { // create device and assign to edge Device savedDevice = saveDevice(StringUtils.randomAlphanumeric(15), thermostatDeviceProfile.getName()); - edgeImitator.expectMessageAmount(2); // device and device profile messages + DeviceCredentials deviceCredentials = doGet("/api/device/" + savedDevice.getId().getId() + "/credentials", DeviceCredentials.class); + edgeImitator.expectMessageAmount(3); // device and device profile messages and device credentials doPost("/api/edge/" + edge.getUuidId() + "/device/" + savedDevice.getUuidId(), Device.class); Assert.assertTrue(edgeImitator.waitForMessages()); @@ -582,6 +584,15 @@ abstract public class AbstractEdgeTest extends AbstractControllerTest { Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, deviceProfileUpdateMsg.getMsgType()); Assert.assertEquals(thermostatDeviceProfile.getUuidId().getMostSignificantBits(), deviceProfileUpdateMsg.getIdMSB()); Assert.assertEquals(thermostatDeviceProfile.getUuidId().getLeastSignificantBits(), deviceProfileUpdateMsg.getIdLSB()); + + Optional deviceCredentialsUpdateMsgOpt = edgeImitator.findMessageByType(DeviceCredentialsUpdateMsg.class); + Assert.assertTrue(deviceCredentialsUpdateMsgOpt.isPresent()); + DeviceCredentialsUpdateMsg deviceCredentialsUpdateMsg = deviceCredentialsUpdateMsgOpt.get(); + DeviceCredentials deviceCredentialsMsg = JacksonUtil.fromString(deviceCredentialsUpdateMsg.getEntity(), DeviceCredentials.class, true); + Assert.assertNotNull(deviceCredentialsMsg); + Assert.assertEquals(savedDevice.getId(), deviceCredentialsMsg.getDeviceId()); + Assert.assertEquals(deviceCredentials, deviceCredentialsMsg); + return savedDevice; } diff --git a/application/src/test/java/org/thingsboard/server/edge/CalculatedFieldEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/CalculatedFieldEdgeTest.java new file mode 100644 index 0000000000..268e19345c --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/edge/CalculatedFieldEdgeTest.java @@ -0,0 +1,267 @@ +/** + * 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.Device; +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.SimpleCalculatedFieldConfiguration; +import org.thingsboard.server.common.data.debug.DebugSettings; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.dao.service.DaoSqlTest; +import org.thingsboard.server.gen.edge.v1.CalculatedFieldRequestMsg; +import org.thingsboard.server.gen.edge.v1.CalculatedFieldUpdateMsg; +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.Map; +import java.util.Optional; +import java.util.UUID; + +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@DaoSqlTest +public class CalculatedFieldEdgeTest extends AbstractEdgeTest { + private static final String DEFAULT_CF_NAME = "Edge Test CalculatedField"; + private static final String UPDATED_CF_NAME = "Updated Edge Test CalculatedField"; + + @Test + public void testCalculatedField_create_update_delete() throws Exception { + Device savedDevice = saveDeviceOnCloudAndVerifyDeliveryToEdge(); + + // create calculatedField + SimpleCalculatedFieldConfiguration config = new SimpleCalculatedFieldConfiguration(); + CalculatedField calculatedField = createSimpleCalculatedField(savedDevice.getId(), config); + + edgeImitator.expectMessageAmount(1); + CalculatedField savedCalculatedField = doPost("/api/calculatedField", calculatedField, CalculatedField.class); + Assert.assertTrue(edgeImitator.waitForMessages()); + + AbstractMessage latestMessage = edgeImitator.getLatestMessage(); + Assert.assertTrue(latestMessage instanceof CalculatedFieldUpdateMsg); + CalculatedFieldUpdateMsg calculatedFieldUpdateMsg = (CalculatedFieldUpdateMsg) latestMessage; + Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, calculatedFieldUpdateMsg.getMsgType()); + Assert.assertEquals(savedCalculatedField.getUuidId().getMostSignificantBits(), calculatedFieldUpdateMsg.getIdMSB()); + Assert.assertEquals(savedCalculatedField.getUuidId().getLeastSignificantBits(), calculatedFieldUpdateMsg.getIdLSB()); + CalculatedField calculatedFieldFromMsg = JacksonUtil.fromString(calculatedFieldUpdateMsg.getEntity(), CalculatedField.class, true); + Assert.assertNotNull(calculatedFieldFromMsg); + + Assert.assertEquals(DEFAULT_CF_NAME, calculatedFieldFromMsg.getName()); + Assert.assertEquals(savedDevice.getId(), calculatedFieldFromMsg.getEntityId()); + Assert.assertEquals(config, calculatedFieldFromMsg.getConfiguration()); + + edgeImitator.expectMessageAmount(1); + savedCalculatedField.setName(UPDATED_CF_NAME); + savedCalculatedField = doPost("/api/calculatedField", savedCalculatedField, CalculatedField.class); + Assert.assertTrue(edgeImitator.waitForMessages()); + + latestMessage = edgeImitator.getLatestMessage(); + Assert.assertTrue(latestMessage instanceof CalculatedFieldUpdateMsg); + calculatedFieldUpdateMsg = (CalculatedFieldUpdateMsg) latestMessage; + calculatedFieldFromMsg = JacksonUtil.fromString(calculatedFieldUpdateMsg.getEntity(), CalculatedField.class, true); + Assert.assertNotNull(calculatedFieldFromMsg); + Assert.assertEquals(UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE, calculatedFieldUpdateMsg.getMsgType()); + Assert.assertEquals(UPDATED_CF_NAME, calculatedFieldFromMsg.getName()); + + // delete calculatedField + edgeImitator.expectMessageAmount(1); + doDelete("/api/calculatedField/" + savedCalculatedField.getUuidId()) + .andExpect(status().isOk()); + Assert.assertTrue(edgeImitator.waitForMessages()); + + latestMessage = edgeImitator.getLatestMessage(); + Assert.assertTrue(latestMessage instanceof CalculatedFieldUpdateMsg); + calculatedFieldUpdateMsg = (CalculatedFieldUpdateMsg) latestMessage; + Assert.assertEquals(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE, calculatedFieldUpdateMsg.getMsgType()); + Assert.assertEquals(savedCalculatedField.getUuidId().getMostSignificantBits(), calculatedFieldUpdateMsg.getIdMSB()); + Assert.assertEquals(savedCalculatedField.getUuidId().getLeastSignificantBits(), calculatedFieldUpdateMsg.getIdLSB()); + } + + @Test + public void testSendCalculatedFieldToCloud() throws Exception { + Device savedDevice = saveDeviceOnCloudAndVerifyDeliveryToEdge(); + + // create calculatedField + SimpleCalculatedFieldConfiguration config = new SimpleCalculatedFieldConfiguration(); + CalculatedField calculatedField = createSimpleCalculatedField(savedDevice.getId(), config); + UUID uuid = Uuids.timeBased(); + UplinkMsg uplinkMsg = getUplinkMsg(uuid, calculatedField, UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE); + + checkCalculatedFieldOnCloud(uplinkMsg, uuid, calculatedField.getName()); + } + + @Test + public void testSendCalculatedFieldRequestToCloud() throws Exception { + Device savedDevice = saveDeviceOnCloudAndVerifyDeliveryToEdge(); + + // create calculatedField + SimpleCalculatedFieldConfiguration config = new SimpleCalculatedFieldConfiguration(); + CalculatedField calculatedField = createSimpleCalculatedField(savedDevice.getId(), config); + + edgeImitator.expectMessageAmount(1); + CalculatedField savedCalculatedField = doPost("/api/calculatedField", calculatedField, CalculatedField.class); + Assert.assertTrue(edgeImitator.waitForMessages()); + + UplinkMsg.Builder uplinkMsgBuilder = UplinkMsg.newBuilder(); + CalculatedFieldRequestMsg.Builder calculatedFieldRequestMsgBuilder = CalculatedFieldRequestMsg.newBuilder(); + calculatedFieldRequestMsgBuilder.setEntityIdMSB(savedDevice.getId().getId().getMostSignificantBits()); + calculatedFieldRequestMsgBuilder.setEntityIdLSB(savedDevice.getId().getId().getLeastSignificantBits()); + calculatedFieldRequestMsgBuilder.setEntityType(savedDevice.getId().getEntityType().name()); + testAutoGeneratedCodeByProtobuf(calculatedFieldRequestMsgBuilder); + + uplinkMsgBuilder.addCalculatedFieldRequestMsg(calculatedFieldRequestMsgBuilder.build()); + testAutoGeneratedCodeByProtobuf(uplinkMsgBuilder); + + edgeImitator.expectResponsesAmount(1); + edgeImitator.expectMessageAmount(1); + edgeImitator.sendUplinkMsg(uplinkMsgBuilder.build()); + Assert.assertTrue(edgeImitator.waitForResponses()); + Assert.assertTrue(edgeImitator.waitForMessages()); + + AbstractMessage latestMessage = edgeImitator.getLatestMessage(); + Assert.assertTrue(latestMessage instanceof CalculatedFieldUpdateMsg); + CalculatedFieldUpdateMsg calculatedFieldUpdateMsg = (CalculatedFieldUpdateMsg) latestMessage; + CalculatedField calculatedFieldFromEdge = JacksonUtil.fromString(calculatedFieldUpdateMsg.getEntity(), CalculatedField.class, true); + Assert.assertNotNull(calculatedFieldFromEdge); + Assert.assertEquals(savedCalculatedField, calculatedFieldFromEdge); + Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, calculatedFieldUpdateMsg.getMsgType()); + } + + @Test + public void testUpdateCalculatedFieldNameOnCloud() throws Exception { + Device savedDevice = saveDeviceOnCloudAndVerifyDeliveryToEdge(); + + // create calculatedField + SimpleCalculatedFieldConfiguration config = new SimpleCalculatedFieldConfiguration(); + CalculatedField calculatedField = createSimpleCalculatedField(savedDevice.getId(), config); + UUID uuid = Uuids.timeBased(); + UplinkMsg uplinkMsg = getUplinkMsg(uuid, calculatedField, UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE); + + checkCalculatedFieldOnCloud(uplinkMsg, uuid, calculatedField.getName()); + + calculatedField.setName(UPDATED_CF_NAME); + UplinkMsg updatedUplinkMsg = getUplinkMsg(uuid, calculatedField, UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE); + + checkCalculatedFieldOnCloud(updatedUplinkMsg, uuid, calculatedField.getName()); + } + + @Test + public void testCalculatedFieldToCloudWithNameThatAlreadyExistsOnCloud() throws Exception { + Device savedDevice = saveDeviceOnCloudAndVerifyDeliveryToEdge(); + + // create calculatedField + SimpleCalculatedFieldConfiguration config = new SimpleCalculatedFieldConfiguration(); + CalculatedField calculatedField = createSimpleCalculatedField(savedDevice.getId(), config); + + edgeImitator.expectMessageAmount(1); + CalculatedField savedCalculatedField = doPost("/api/calculatedField", calculatedField, CalculatedField.class); + Assert.assertTrue(edgeImitator.waitForMessages()); + + UUID uuid = Uuids.timeBased(); + + UplinkMsg uplinkMsg = getUplinkMsg(uuid, calculatedField, UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE); + + edgeImitator.expectResponsesAmount(1); + edgeImitator.expectMessageAmount(1); + + edgeImitator.sendUplinkMsg(uplinkMsg); + + Assert.assertTrue(edgeImitator.waitForResponses()); + Assert.assertTrue(edgeImitator.waitForMessages()); + + Optional calculatedFieldUpdateMsgOpt = edgeImitator.findMessageByType(CalculatedFieldUpdateMsg.class); + Assert.assertTrue(calculatedFieldUpdateMsgOpt.isPresent()); + CalculatedFieldUpdateMsg latestCalculatedFieldUpdateMsg = calculatedFieldUpdateMsgOpt.get(); + CalculatedField calculatedFieldFromMsg = JacksonUtil.fromString(latestCalculatedFieldUpdateMsg.getEntity(), CalculatedField.class, true); + Assert.assertNotNull(calculatedFieldFromMsg); + Assert.assertNotEquals(DEFAULT_CF_NAME, calculatedFieldFromMsg.getName()); + + Assert.assertNotEquals(savedCalculatedField.getUuidId(), uuid); + + CalculatedField calculatedFieldFromCloud = doGet("/api/calculatedField/" + uuid, CalculatedField.class); + Assert.assertNotNull(calculatedFieldFromCloud); + Assert.assertNotEquals(DEFAULT_CF_NAME, calculatedFieldFromCloud.getName()); + } + + private CalculatedField createSimpleCalculatedField(EntityId entityId, SimpleCalculatedFieldConfiguration config) { + CalculatedField calculatedField = new CalculatedField(); + calculatedField.setEntityId(entityId); + calculatedField.setTenantId(tenantId); + calculatedField.setType(CalculatedFieldType.SIMPLE); + calculatedField.setName(DEFAULT_CF_NAME); + calculatedField.setDebugSettings(DebugSettings.all()); + + Argument argument = new Argument(); + ReferencedEntityKey refEntityKey = new ReferencedEntityKey("temperature", ArgumentType.TS_LATEST, null); + argument.setRefEntityKey(refEntityKey); + argument.setDefaultValue("12"); // not used because real telemetry value in db is present + config.setArguments(Map.of("T", argument)); + + config.setExpression("(T * 9/5) + 32"); + + Output output = new Output(); + output.setName("fahrenheitTemp"); + output.setType(OutputType.TIME_SERIES); + output.setDecimalsByDefault(2); + config.setOutput(output); + + calculatedField.setConfiguration(config); + + return calculatedField; + } + + private UplinkMsg getUplinkMsg(UUID uuid, CalculatedField calculatedField, UpdateMsgType updateMsgType) throws InvalidProtocolBufferException { + UplinkMsg.Builder uplinkMsgBuilder = UplinkMsg.newBuilder(); + CalculatedFieldUpdateMsg.Builder calculatedFieldUpdateMsgBuilder = CalculatedFieldUpdateMsg.newBuilder(); + calculatedFieldUpdateMsgBuilder.setIdMSB(uuid.getMostSignificantBits()); + calculatedFieldUpdateMsgBuilder.setIdLSB(uuid.getLeastSignificantBits()); + calculatedFieldUpdateMsgBuilder.setEntity(JacksonUtil.toString(calculatedField)); + calculatedFieldUpdateMsgBuilder.setMsgType(updateMsgType); + testAutoGeneratedCodeByProtobuf(calculatedFieldUpdateMsgBuilder); + uplinkMsgBuilder.addCalculatedFieldUpdateMsg(calculatedFieldUpdateMsgBuilder.build()); + + testAutoGeneratedCodeByProtobuf(uplinkMsgBuilder); + + return uplinkMsgBuilder.build(); + } + + private void checkCalculatedFieldOnCloud(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()); + + CalculatedField calculatedField = doGet("/api/calculatedField/" + uuid, CalculatedField.class); + Assert.assertNotNull(calculatedField); + Assert.assertEquals(resourceTitle, calculatedField.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 8b259cf8fc..16d10d9b6e 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 @@ -33,6 +33,7 @@ import org.thingsboard.server.gen.edge.v1.AlarmCommentUpdateMsg; import org.thingsboard.server.gen.edge.v1.AlarmUpdateMsg; import org.thingsboard.server.gen.edge.v1.AssetProfileUpdateMsg; import org.thingsboard.server.gen.edge.v1.AssetUpdateMsg; +import org.thingsboard.server.gen.edge.v1.CalculatedFieldUpdateMsg; import org.thingsboard.server.gen.edge.v1.CustomerUpdateMsg; import org.thingsboard.server.gen.edge.v1.DashboardUpdateMsg; import org.thingsboard.server.gen.edge.v1.DeviceCredentialsRequestMsg; @@ -352,6 +353,11 @@ public class EdgeImitator { result.add(saveDownlinkMsg(notificationTargetUpdateMsg)); } } + if (downlinkMsg.getCalculatedFieldUpdateMsgCount() > 0) { + for (CalculatedFieldUpdateMsg calculatedFieldUpdateMsg : downlinkMsg.getCalculatedFieldUpdateMsgList()) { + result.add(saveDownlinkMsg(calculatedFieldUpdateMsg)); + } + } if (downlinkMsg.hasEdgeConfiguration()) { result.add(saveDownlinkMsg(downlinkMsg.getEdgeConfiguration())); } 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..691a1f7ec4 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldStateTest.java @@ -0,0 +1,483 @@ +/** + * 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.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.thingsboard.common.util.JacksonUtil; +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.RelationQueryDynamicSourceConfiguration; +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.dao.relation.RelationService; +import org.thingsboard.server.dao.usagerecord.ApiLimitService; +import org.thingsboard.server.service.cf.CalculatedFieldResult; +import org.thingsboard.server.service.cf.ctx.state.geofencing.GeofencingArgumentEntry; +import org.thingsboard.server.service.cf.ctx.state.geofencing.GeofencingCalculatedFieldState; + +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; + + @BeforeEach + void setUp() { + when(apiLimitService.getLimit(any(), any())).thenReturn(1000L); + ctx = new CalculatedFieldCtx(getCalculatedField(), null, apiLimitService, relationService); + ctx.init(); + state = new GeofencingCalculatedFieldState(ctx.getArgNames()); + } + + @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.updateState(ctx, newArgs); + + 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.updateState(ctx, Map.of(ENTITY_ID_LATITUDE_ARGUMENT_KEY, geofencingAllowedZoneArgEntry))) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Unsupported argument entry type for latitude argument: GEOFENCING. Only SINGLE_VALUE type is allowed."); + } + + @Test + void testUpdateStateWithInvalidArgumentTypeForLongitudeArgument() { + assertThatThrownBy(() -> state.updateState(ctx, Map.of(ENTITY_ID_LONGITUDE_ARGUMENT_KEY, geofencingAllowedZoneArgEntry))) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Unsupported argument entry type for longitude argument: GEOFENCING. Only SINGLE_VALUE type is allowed."); + } + + @Test + void testUpdateStateWithInvalidArgumentTypeForGeofencingArgument() { + assertThatThrownBy(() -> state.updateState(ctx, Map.of("someArgumentName", latitudeArgEntry))) + .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.updateState(ctx, newArgs); + + 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.updateState(ctx, newArgs); + + 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.updateState(ctx, Map.of(ENTITY_ID_LATITUDE_ARGUMENT_KEY, geofencingAllowedZoneArgEntry))) + .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.updateState(ctx, Map.of("allowedZones", latitudeArgEntry))) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Unsupported argument entry type for geofencing argument entry: SINGLE_VALUE"); + } + + @Test + void testIsReadyWhenNotAllArgPresent() { + assertThat(state.isReady()).isFalse(); + } + + @Test + void testIsReadyWhenAllArgPresent() { + state.arguments = new HashMap<>(Map.of( + ENTITY_ID_LATITUDE_ARGUMENT_KEY, latitudeArgEntry, + ENTITY_ID_LONGITUDE_ARGUMENT_KEY, longitudeArgEntry, + "allowedZones", geofencingAllowedZoneArgEntry, + "restrictedZones", geofencingRestrictedZoneArgEntry + )); + assertThat(state.isReady()).isTrue(); + } + + @Test + void testIsReadyWhenEmptyEntryPresents() { + state.arguments = new HashMap<>(Map.of( + ENTITY_ID_LATITUDE_ARGUMENT_KEY, latitudeArgEntry, + ENTITY_ID_LONGITUDE_ARGUMENT_KEY, longitudeArgEntry, + "allowedZones", geofencingAllowedZoneArgEntry, + "restrictedZones", geofencingRestrictedZoneArgEntry + )); + + state.getArguments().put("noParkingZones", new GeofencingArgumentEntry()); + + assertThat(state.isReady()).isFalse(); + } + + @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)); + + CalculatedFieldResult result = state.performCalculation(ctx.getEntityId(), ctx).get(); + + 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.updateState(ctx, Map.of(ENTITY_ID_LATITUDE_ARGUMENT_KEY, newLatitude, ENTITY_ID_LONGITUDE_ARGUMENT_KEY, newLongitude)); + + CalculatedFieldResult result2 = state.performCalculation(ctx.getEntityId(), ctx).get(); + + assertThat(result2).isNotNull(); + assertThat(result2.getType()).isEqualTo(output.getType()); + assertThat(result2.getScope()).isEqualTo(output.getScope()); + assertThat(result2.getResult()).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)); + + CalculatedFieldResult result = state.performCalculation(ctx.getEntityId(), ctx).get(); + + 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.updateState(ctx, Map.of(ENTITY_ID_LATITUDE_ARGUMENT_KEY, newLatitude, ENTITY_ID_LONGITUDE_ARGUMENT_KEY, newLongitude)); + + CalculatedFieldResult result2 = state.performCalculation(ctx.getEntityId(), ctx).get(); + + assertThat(result2).isNotNull(); + assertThat(result2.getType()).isEqualTo(output.getType()); + assertThat(result2.getScope()).isEqualTo(output.getScope()); + assertThat(result2.getResult()).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)); + + CalculatedFieldResult result = state.performCalculation(ctx.getEntityId(), ctx).get(); + + 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.updateState(ctx, Map.of(ENTITY_ID_LATITUDE_ARGUMENT_KEY, newLatitude, ENTITY_ID_LONGITUDE_ARGUMENT_KEY, newLongitude)); + + CalculatedFieldResult result2 = state.performCalculation(ctx.getEntityId(), ctx).get(); + + assertThat(result2).isNotNull(); + assertThat(result2.getType()).isEqualTo(output.getType()); + assertThat(result2.getScope()).isEqualTo(output.getScope()); + assertThat(result2.getResult()).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 RelationQueryDynamicSourceConfiguration(); + allowedZoneDynamicSourceConfiguration.setDirection(EntitySearchDirection.TO); + allowedZoneDynamicSourceConfiguration.setRelationType("AllowedZone"); + allowedZoneDynamicSourceConfiguration.setMaxLevel(1); + allowedZoneDynamicSourceConfiguration.setFetchLastLevelOnly(true); + allowedZonesGroup.setRefDynamicSourceConfiguration(allowedZoneDynamicSourceConfiguration); + allowedZonesGroup.setRelationType("CurrentZone"); + allowedZonesGroup.setDirection(EntitySearchDirection.TO); + + ZoneGroupConfiguration restrictedZonesGroup = new ZoneGroupConfiguration("zone", reportStrategy, true); + var restrictedZoneDynamicSourceConfiguration = new RelationQueryDynamicSourceConfiguration(); + restrictedZoneDynamicSourceConfiguration.setDirection(EntitySearchDirection.TO); + restrictedZoneDynamicSourceConfiguration.setRelationType("RestrictedZone"); + restrictedZoneDynamicSourceConfiguration.setMaxLevel(1); + restrictedZoneDynamicSourceConfiguration.setFetchLastLevelOnly(true); + 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; + } + +} 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/ScriptCalculatedFieldStateTest.java b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/ScriptCalculatedFieldStateTest.java index 91eced64f6..8c714bc0e7 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 @@ -20,7 +20,7 @@ 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.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; @@ -44,6 +44,7 @@ import org.thingsboard.server.dao.usagerecord.ApiLimitService; import org.thingsboard.server.service.cf.CalculatedFieldResult; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.TreeMap; import java.util.UUID; @@ -60,7 +61,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,13 +72,13 @@ public class ScriptCalculatedFieldStateTest { @Autowired private TbelInvokeService tbelInvokeService; - @MockBean + @MockitoBean private ApiLimitService apiLimitService; @BeforeEach void setUp() { when(apiLimitService.getLimit(any(), any())).thenReturn(1000L); - ctx = new CalculatedFieldCtx(getCalculatedField(), tbelInvokeService, apiLimitService); + ctx = new CalculatedFieldCtx(getCalculatedField(), tbelInvokeService, apiLimitService, null); ctx.init(); state = new ScriptCalculatedFieldState(ctx.getArgNames()); } @@ -124,7 +125,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(); + CalculatedFieldResult result = state.performCalculation(ctx.getEntityId(), ctx).get(); assertThat(result).isNotNull(); Output output = getCalculatedFieldConfig().getOutput(); @@ -133,6 +134,22 @@ 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) + )); + + CalculatedFieldResult result = state.performCalculation(ctx.getEntityId(), ctx).get(); + + 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(); @@ -193,7 +210,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); 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..8c631ecf6f 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 @@ -71,7 +71,7 @@ public class SimpleCalculatedFieldStateTest { @BeforeEach void setUp() { when(apiLimitService.getLimit(any(), any())).thenReturn(1000L); - ctx = new CalculatedFieldCtx(getCalculatedField(), null, apiLimitService); + ctx = new CalculatedFieldCtx(getCalculatedField(), null, apiLimitService, null); ctx.init(); state = new SimpleCalculatedFieldState(ctx.getArgNames()); } @@ -134,7 +134,7 @@ public class SimpleCalculatedFieldStateTest { "key3", key3ArgEntry )); - CalculatedFieldResult result = state.performCalculation(ctx).get(); + CalculatedFieldResult result = state.performCalculation(ctx.getEntityId(), ctx).get(); assertThat(result).isNotNull(); Output output = getCalculatedFieldConfig().getOutput(); @@ -151,7 +151,7 @@ public class SimpleCalculatedFieldStateTest { "key3", key3ArgEntry )); - assertThatThrownBy(() -> state.performCalculation(ctx)) + assertThatThrownBy(() -> state.performCalculation(ctx.getEntityId(), ctx)) .isInstanceOf(IllegalArgumentException.class) .hasMessage("Argument 'key2' is not a number."); } @@ -164,7 +164,7 @@ public class SimpleCalculatedFieldStateTest { "key3", key3ArgEntry )); - CalculatedFieldResult result = state.performCalculation(ctx).get(); + CalculatedFieldResult result = state.performCalculation(ctx.getEntityId(), ctx).get(); assertThat(result).isNotNull(); Output output = getCalculatedFieldConfig().getOutput(); @@ -185,7 +185,7 @@ public class SimpleCalculatedFieldStateTest { output.setDecimalsByDefault(3); ctx.setOutput(output); - CalculatedFieldResult result = state.performCalculation(ctx).get(); + CalculatedFieldResult result = state.performCalculation(ctx.getEntityId(), ctx).get(); assertThat(result).isNotNull(); assertThat(result.getType()).isEqualTo(output.getType()); 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 new file mode 100644 index 0000000000..25ff0f1b5d --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/service/edge/EdgeStatsTest.java @@ -0,0 +1,173 @@ +/** + * 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; + +import com.google.common.util.concurrent.Futures; +import org.junit.jupiter.api.Assertions; +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.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.test.util.ReflectionTestUtils; +import org.thingsboard.server.common.data.id.EdgeId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.kv.TimeseriesSaveResult; +import org.thingsboard.server.common.data.kv.TsKvEntry; +import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; +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.KafkaAdmin; +import org.thingsboard.server.service.edge.stats.EdgeStatsService; + +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; + +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.thingsboard.server.dao.edge.stats.EdgeStatsKey.DOWNLINK_MSGS_ADDED; +import static org.thingsboard.server.dao.edge.stats.EdgeStatsKey.DOWNLINK_MSGS_LAG; +import static org.thingsboard.server.dao.edge.stats.EdgeStatsKey.DOWNLINK_MSGS_PERMANENTLY_FAILED; +import static org.thingsboard.server.dao.edge.stats.EdgeStatsKey.DOWNLINK_MSGS_PUSHED; +import static org.thingsboard.server.dao.edge.stats.EdgeStatsKey.DOWNLINK_MSGS_TMP_FAILED; + +@ExtendWith(MockitoExtension.class) +public class EdgeStatsTest { + + @Mock + private TimeseriesService tsService; + @Mock + private TopicService topicService; + @Mock + private EdgeStatsCounterService statsCounterService; + private EdgeStatsService edgeStatsService; + + private final TenantId tenantId = TenantId.fromUUID(UUID.randomUUID()); + private final EdgeId edgeId = new EdgeId(UUID.randomUUID()); + + @BeforeEach + void setUp() { + edgeStatsService = new EdgeStatsService( + tsService, + statsCounterService, + topicService, + Optional.empty() + ); + + ReflectionTestUtils.setField(edgeStatsService, "edgesStatsTtlDays", 30); + ReflectionTestUtils.setField(edgeStatsService, "reportIntervalMillis", 600_000L); + } + + @Test + public void testReportStatsSavesTelemetry() { + // given + MsgCounters counters = new MsgCounters(tenantId); + counters.getMsgsAdded().set(5); + counters.getMsgsPushed().set(3); + counters.getMsgsPermanentlyFailed().set(1); + counters.getMsgsTmpFailed().set(0); + counters.getMsgsLag().set(10); + + ConcurrentHashMap countersByEdge = new ConcurrentHashMap<>(); + countersByEdge.put(edgeId, counters); + + when(statsCounterService.getCounterByEdge()).thenReturn(countersByEdge); + + ArgumentCaptor> captor = ArgumentCaptor.forClass(List.class); + when(tsService.save(eq(tenantId), eq(edgeId), captor.capture(), anyLong())) + .thenReturn(Futures.immediateFuture(mock(TimeseriesSaveResult.class))); + + // when + edgeStatsService.reportStats(); + + // then + List entries = captor.getValue(); + Assertions.assertEquals(5, entries.size()); + + Map valuesByKey = entries.stream() + .collect(Collectors.toMap(TsKvEntry::getKey, e -> e.getLongValue().orElse(-1L))); + + Assertions.assertEquals(5L, valuesByKey.get(DOWNLINK_MSGS_ADDED.getKey()).longValue()); + Assertions.assertEquals(3L, valuesByKey.get(DOWNLINK_MSGS_PUSHED.getKey()).longValue()); + Assertions.assertEquals(1L, valuesByKey.get(DOWNLINK_MSGS_PERMANENTLY_FAILED.getKey()).longValue()); + Assertions.assertEquals(0L, valuesByKey.get(DOWNLINK_MSGS_TMP_FAILED.getKey()).longValue()); + Assertions.assertEquals(10L, valuesByKey.get(DOWNLINK_MSGS_LAG.getKey()).longValue()); + + + verify(statsCounterService).clear(edgeId); + } + + @Test + public void testReportStatsWithKafkaLag() { + // given + MsgCounters counters = new MsgCounters(tenantId); + counters.getMsgsAdded().set(2); + counters.getMsgsPushed().set(2); + counters.getMsgsPermanentlyFailed().set(0); + counters.getMsgsTmpFailed().set(1); + counters.getMsgsLag().set(0); + + ConcurrentHashMap countersByEdge = new ConcurrentHashMap<>(); + countersByEdge.put(edgeId, counters); + + // mocks + when(statsCounterService.getCounterByEdge()).thenReturn(countersByEdge); + + String topic = "edge-topic"; + TopicPartitionInfo partitionInfo = new TopicPartitionInfo(topic, tenantId, 0, false); + when(topicService.buildEdgeEventNotificationsTopicPartitionInfo(tenantId, edgeId)).thenReturn(partitionInfo); + + KafkaAdmin kafkaAdmin = mock(KafkaAdmin.class); + when(kafkaAdmin.getTotalLagForGroupsBulk(Set.of(topic))) + .thenReturn(Map.of(topic, 15L)); + + ArgumentCaptor> captor = ArgumentCaptor.forClass(List.class); + when(tsService.save(eq(tenantId), eq(edgeId), captor.capture(), anyLong())) + .thenReturn(Futures.immediateFuture(mock(TimeseriesSaveResult.class))); + + edgeStatsService = new EdgeStatsService( + tsService, + statsCounterService, + topicService, + Optional.of(kafkaAdmin) + ); + ReflectionTestUtils.setField(edgeStatsService, "edgesStatsTtlDays", 30); + ReflectionTestUtils.setField(edgeStatsService, "reportIntervalMillis", 600_000L); + + // when + edgeStatsService.reportStats(); + + // then + List entries = captor.getValue(); + Map valuesByKey = entries.stream() + .collect(Collectors.toMap(TsKvEntry::getKey, e -> e.getLongValue().orElse(-1L))); + + Assertions.assertEquals(15L, valuesByKey.get(DOWNLINK_MSGS_LAG.getKey())); + verify(statsCounterService).clear(edgeId); + } + +} 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 d8bcd42bc8..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 @@ -54,6 +54,7 @@ import org.thingsboard.server.common.data.kv.StringDataEntry; import org.thingsboard.server.common.data.kv.TimeseriesSaveResult; import org.thingsboard.server.common.data.objects.TelemetryEntityView; import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.query.AliasEntityId; import org.thingsboard.server.common.data.query.ApiUsageStateFilter; import org.thingsboard.server.common.data.query.AssetSearchQueryFilter; import org.thingsboard.server.common.data.query.AssetTypeFilter; @@ -78,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; @@ -117,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 @@ -217,7 +221,7 @@ public class EntityServiceTest extends AbstractControllerTest { createTestHierarchy(tenantId, assets, devices, new ArrayList<>(), new ArrayList<>(), new ArrayList<>(), new ArrayList<>()); RelationsQueryFilter filter = new RelationsQueryFilter(); - filter.setRootEntity(tenantId); + filter.setRootEntity(AliasEntityId.fromEntityId(tenantId)); filter.setDirection(EntitySearchDirection.FROM); EntityCountQuery countQuery = new EntityCountQuery(filter); @@ -226,13 +230,13 @@ public class EntityServiceTest extends AbstractControllerTest { filter.setFilters(Collections.singletonList(new RelationEntityTypeFilter("Contains", Collections.singletonList(EntityType.DEVICE)))); countByQueryAndCheck(countQuery, 25); - filter.setRootEntity(devices.get(0).getId()); + filter.setRootEntity(AliasEntityId.fromEntityId(devices.get(0).getId())); filter.setDirection(EntitySearchDirection.TO); filter.setFilters(Collections.singletonList(new RelationEntityTypeFilter("Manages", Collections.singletonList(EntityType.TENANT)))); countByQueryAndCheck(countQuery, 1); DeviceSearchQueryFilter filter2 = new DeviceSearchQueryFilter(); - filter2.setRootEntity(tenantId); + filter2.setRootEntity(AliasEntityId.fromEntityId(tenantId)); filter2.setDirection(EntitySearchDirection.FROM); filter2.setRelationType("Contains"); @@ -242,12 +246,12 @@ public class EntityServiceTest extends AbstractControllerTest { filter2.setDeviceTypes(Arrays.asList("default0", "default1")); countByQueryAndCheck(countQuery, 10); - filter2.setRootEntity(devices.get(0).getId()); + filter2.setRootEntity(AliasEntityId.fromEntityId(devices.get(0).getId())); filter2.setDirection(EntitySearchDirection.TO); countByQueryAndCheck(countQuery, 0); AssetSearchQueryFilter filter3 = new AssetSearchQueryFilter(); - filter3.setRootEntity(tenantId); + filter3.setRootEntity(AliasEntityId.fromEntityId(tenantId)); filter3.setDirection(EntitySearchDirection.FROM); filter3.setRelationType("Manages"); @@ -257,7 +261,7 @@ public class EntityServiceTest extends AbstractControllerTest { filter3.setAssetTypes(Arrays.asList("type0", "type1")); countByQueryAndCheck(countQuery, 2); - filter3.setRootEntity(devices.get(0).getId()); + filter3.setRootEntity(AliasEntityId.fromEntityId(devices.get(0).getId())); filter3.setDirection(EntitySearchDirection.TO); countByQueryAndCheck(countQuery, 0); } @@ -268,7 +272,7 @@ public class EntityServiceTest extends AbstractControllerTest { createTestUserRelations(tenantId, users); RelationsQueryFilter filter = new RelationsQueryFilter(); - filter.setRootEntity(tenantId); + filter.setRootEntity(AliasEntityId.fromEntityId(tenantId)); filter.setDirection(EntitySearchDirection.FROM); EntityDataPageLink pageLink = new EntityDataPageLink(10, 0, null, null); @@ -352,7 +356,7 @@ public class EntityServiceTest extends AbstractControllerTest { } EdgeSearchQueryFilter filter = new EdgeSearchQueryFilter(); - filter.setRootEntity(tenantId); + filter.setRootEntity(AliasEntityId.fromEntityId(tenantId)); filter.setDirection(EntitySearchDirection.FROM); filter.setRelationType("Manages"); @@ -404,7 +408,7 @@ public class EntityServiceTest extends AbstractControllerTest { Futures.allAsList(attributeFutures).get(); RelationsQueryFilter filter = new RelationsQueryFilter(); - filter.setRootEntity(tenantId); + filter.setRootEntity(AliasEntityId.fromEntityId(tenantId)); filter.setDirection(EntitySearchDirection.FROM); filter.setFilters(Collections.singletonList(new RelationEntityTypeFilter("Contains", Collections.singletonList(EntityType.DEVICE)))); filter.setMaxLevel(maxLevel); @@ -554,7 +558,7 @@ public class EntityServiceTest extends AbstractControllerTest { Futures.allAsList(attributeFutures).get(); DeviceSearchQueryFilter filter = new DeviceSearchQueryFilter(); - filter.setRootEntity(tenantId); + filter.setRootEntity(AliasEntityId.fromEntityId(tenantId)); filter.setDirection(EntitySearchDirection.FROM); filter.setRelationType("Contains"); filter.setMaxLevel(2); @@ -603,12 +607,12 @@ 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(); AssetSearchQueryFilter filter = new AssetSearchQueryFilter(); - filter.setRootEntity(tenantId); + filter.setRootEntity(AliasEntityId.fromEntityId(tenantId)); filter.setDirection(EntitySearchDirection.FROM); filter.setRelationType("Manages"); @@ -1101,7 +1105,7 @@ public class EntityServiceTest extends AbstractControllerTest { } SingleEntityFilter singleEntityFilter = new SingleEntityFilter(); - singleEntityFilter.setSingleEntity(devices.get(0).getId()); + singleEntityFilter.setSingleEntity(AliasEntityId.fromEntityId(devices.get(0).getId())); List entityFields = List.of( new EntityKey(EntityKeyType.ENTITY_FIELD, "name") @@ -1120,7 +1124,7 @@ public class EntityServiceTest extends AbstractControllerTest { @Test public void testFindCustomerBySingleEntityFilter() { SingleEntityFilter singleEntityFilter = new SingleEntityFilter(); - singleEntityFilter.setSingleEntity(customerId); + singleEntityFilter.setSingleEntity(AliasEntityId.fromEntityId(customerId)); List entityFields = List.of( new EntityKey(EntityKeyType.ENTITY_FIELD, "name") ); @@ -1192,7 +1196,7 @@ public class EntityServiceTest extends AbstractControllerTest { List keyFiltersEqualString = createStringKeyFilters("name", EntityKeyType.ENTITY_FIELD, StringOperation.STARTS_WITH, "Test device "); for (Asset asset : assets) { - filter.setRootEntity(asset.getId()); + filter.setRootEntity(AliasEntityId.fromEntityId(asset.getId())); EntityDataQuery query = new EntityDataQuery(filter, pageLink, Collections.emptyList(), Collections.emptyList(), keyFiltersEqualString); findByQueryAndCheck(customer.getId(), query, relationsCnt); @@ -1384,7 +1388,7 @@ public class EntityServiceTest extends AbstractControllerTest { } SingleEntityFilter singleEntityFilter = new SingleEntityFilter(); - singleEntityFilter.setSingleEntity(customerDevices.get(0).getId()); + singleEntityFilter.setSingleEntity(AliasEntityId.fromEntityId(customerDevices.get(0).getId())); List entityFields = List.of( new EntityKey(EntityKeyType.ENTITY_FIELD, "name") ); @@ -1403,7 +1407,7 @@ public class EntityServiceTest extends AbstractControllerTest { // try to find tenant device by customer user SingleEntityFilter tenantDeviceFilter = new SingleEntityFilter(); - tenantDeviceFilter.setSingleEntity(tenantDevices.get(0).getId()); + tenantDeviceFilter.setSingleEntity(AliasEntityId.fromEntityId(tenantDevices.get(0).getId())); EntityDataQuery customerQuery2 = new EntityDataQuery(tenantDeviceFilter, pageLink, entityFields, null, null); findByQueryAndCheck(customerId, customerQuery2, 0); } @@ -1744,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 new file mode 100644 index 0000000000..2321446b44 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/service/entitiy/ai/DefaultTbAiModelServiceTest.java @@ -0,0 +1,268 @@ +/** + * 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.entitiy.ai; + +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.Spy; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.test.util.ReflectionTestUtils; +import org.thingsboard.server.common.data.User; +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.OpenAiChatModelConfig; +import org.thingsboard.server.common.data.ai.provider.OpenAiProviderConfig; +import org.thingsboard.server.common.data.audit.ActionType; +import org.thingsboard.server.common.data.id.AiModelId; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.dao.ai.AiModelService; +import org.thingsboard.server.service.entitiy.TbLogEntityActionService; +import org.thingsboard.server.service.sync.vc.EntitiesVersionControlService; + +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.never; + +@ExtendWith(MockitoExtension.class) +class DefaultTbAiModelServiceTest { + + @Mock + EntitiesVersionControlService vcServiceMock; + + @Mock + AiModelService aiModelServiceMock; + + @Mock + TbLogEntityActionService logEntityActionServiceMock; + + @Spy + @InjectMocks + DefaultTbAiModelService service; + + TenantId tenantId = TenantId.fromUUID(UUID.randomUUID()); + + User user; + + @BeforeEach + void setUp() { + user = new User(); + user.setTenantId(tenantId); + + service = new DefaultTbAiModelService(aiModelServiceMock); + ReflectionTestUtils.setField(service, "vcService", vcServiceMock); + ReflectionTestUtils.setField(service, "logEntityActionService", logEntityActionServiceMock); + } + + @Test + void save_whenCreatingNewModel_shouldAutoCommitAndLogAddedActionAndUseTenantIdFromUser() { + // GIVEN + var modelToSave = AiModel.builder() + .name("Model to save") + .configuration(constructValidOpenAiModelConfig()) + .build(); + + var savedModel = new AiModel(modelToSave); + savedModel.setId(new AiModelId(UUID.randomUUID())); + savedModel.setTenantId(user.getTenantId()); + savedModel.setVersion(1L); + savedModel.setCreatedTime(System.currentTimeMillis()); + + given(aiModelServiceMock.save(modelToSave)).willReturn(savedModel); + + // WHEN + AiModel result = service.save(modelToSave, user); + + // THEN + assertThat(result).isEqualTo(savedModel); + + then(aiModelServiceMock).should().save(modelToSave); + then(vcServiceMock).should().autoCommit(user, savedModel.getId()); + then(logEntityActionServiceMock).should().logEntityAction(tenantId, savedModel.getId(), savedModel, ActionType.ADDED, user); + } + + @Test + void save_whenUpdatingExistingModel_shouldAutoCommitAndLogUpdatedAction() { + // GIVEN + var modelToUpdate = AiModel.builder() + .tenantId(tenantId) + .version(1L) + .name("Model to update") + .configuration(constructValidOpenAiModelConfig()) + .build(); + modelToUpdate.setId(new AiModelId(UUID.randomUUID())); + modelToUpdate.setCreatedTime(System.currentTimeMillis()); + + var updatedModel = new AiModel(modelToUpdate); + updatedModel.setVersion(2L); + updatedModel.setName("Updated model"); + + given(aiModelServiceMock.save(modelToUpdate)).willReturn(updatedModel); + + // WHEN + AiModel result = service.save(modelToUpdate, user); + + // THEN + assertThat(result).isEqualTo(updatedModel); + + then(aiModelServiceMock).should().save(modelToUpdate); + then(vcServiceMock).should().autoCommit(user, updatedModel.getId()); + then(logEntityActionServiceMock).should().logEntityAction(tenantId, updatedModel.getId(), updatedModel, ActionType.UPDATED, user); + } + + @Test + void save_whenCreatingNewModelThrowsException_shouldUseEmptyIdAndLogError() { + // GIVEN + var modelToSave = AiModel.builder() + .tenantId(tenantId) + .name("Model to save") + .configuration(constructValidOpenAiModelConfig()) + .build(); + + var exception = new RuntimeException("Failed to save"); + + given(aiModelServiceMock.save(modelToSave)).willThrow(exception); + + // WHEN-THEN + assertThatThrownBy(() -> service.save(modelToSave, user)) + .isInstanceOf(RuntimeException.class) + .hasMessageContaining("Failed to save"); + + then(aiModelServiceMock).should().save(modelToSave); + then(vcServiceMock).should(never()).autoCommit(any(), any()); + then(logEntityActionServiceMock).should().logEntityAction(tenantId, new AiModelId(EntityId.NULL_UUID), modelToSave, ActionType.ADDED, user, exception); + } + + @Test + void save_whenUpdatingExistingModelThrowsException_shouldUseExistingModelIdAndLogError() { + // GIVEN + var modelToUpdate = AiModel.builder() + .tenantId(tenantId) + .version(1L) + .name("Model to update") + .configuration(constructValidOpenAiModelConfig()) + .build(); + modelToUpdate.setId(new AiModelId(UUID.randomUUID())); + modelToUpdate.setCreatedTime(System.currentTimeMillis()); + + var exception = new RuntimeException("Failed to save"); + + given(aiModelServiceMock.save(modelToUpdate)).willThrow(exception); + + // WHEN-THEN + assertThatThrownBy(() -> service.save(modelToUpdate, user)) + .isInstanceOf(RuntimeException.class) + .hasMessageContaining("Failed to save"); + + then(aiModelServiceMock).should().save(modelToUpdate); + then(vcServiceMock).should(never()).autoCommit(any(), any()); + then(logEntityActionServiceMock).should().logEntityAction(tenantId, modelToUpdate.getId(), modelToUpdate, ActionType.UPDATED, user, exception); + } + + @Test + void delete_whenDeleteSuccessful_shouldLogDeletedAction() { + // GIVEN + var modelToDelete = AiModel.builder() + .tenantId(tenantId) + .version(1L) + .name("Model to delete") + .configuration(constructValidOpenAiModelConfig()) + .build(); + modelToDelete.setId(new AiModelId(UUID.randomUUID())); + modelToDelete.setCreatedTime(System.currentTimeMillis()); + + given(aiModelServiceMock.deleteByTenantIdAndId(tenantId, modelToDelete.getId())).willReturn(true); + + // WHEN + boolean result = service.delete(modelToDelete, user); + + // THEN + assertThat(result).isTrue(); + then(aiModelServiceMock).should().deleteByTenantIdAndId(tenantId, modelToDelete.getId()); + then(logEntityActionServiceMock).should().logEntityAction(tenantId, modelToDelete.getId(), modelToDelete, ActionType.DELETED, user, modelToDelete.getId().toString()); + } + + @Test + void delete_whenDeleteReturnsFalse_shouldNotLogAction() { + // GIVEN + var modelToDelete = AiModel.builder() + .tenantId(tenantId) + .version(1L) + .name("Model to delete") + .configuration(constructValidOpenAiModelConfig()) + .build(); + modelToDelete.setId(new AiModelId(UUID.randomUUID())); + modelToDelete.setCreatedTime(System.currentTimeMillis()); + + given(aiModelServiceMock.deleteByTenantIdAndId(tenantId, modelToDelete.getId())).willReturn(false); + + // WHEN + boolean result = service.delete(modelToDelete, user); + + // THEN + assertThat(result).isFalse(); + then(aiModelServiceMock).should().deleteByTenantIdAndId(tenantId, modelToDelete.getId()); + then(logEntityActionServiceMock).should(never()).logEntityAction(tenantId, modelToDelete.getId(), modelToDelete, ActionType.DELETED, user, modelToDelete.getId().toString()); + } + + @Test + void delete_whenDeleteThrowsException_shouldLogError() { + // GIVEN + var modelToDelete = AiModel.builder() + .tenantId(tenantId) + .version(1L) + .name("Model to delete") + .configuration(constructValidOpenAiModelConfig()) + .build(); + modelToDelete.setId(new AiModelId(UUID.randomUUID())); + modelToDelete.setCreatedTime(System.currentTimeMillis()); + + var exception = new RuntimeException("Failed to delete"); + + given(aiModelServiceMock.deleteByTenantIdAndId(tenantId, modelToDelete.getId())).willThrow(exception); + + // WHEN-THEN + assertThatThrownBy(() -> service.delete(modelToDelete, user)) + .isInstanceOf(RuntimeException.class) + .hasMessageContaining("Failed to delete"); + + then(aiModelServiceMock).should().deleteByTenantIdAndId(tenantId, modelToDelete.getId()); + then(logEntityActionServiceMock).should().logEntityAction(tenantId, modelToDelete.getId(), modelToDelete, ActionType.DELETED, user, exception, modelToDelete.getId().toString()); + } + + private static AiModelConfig constructValidOpenAiModelConfig() { + return OpenAiChatModelConfig.builder() + .providerConfig(new OpenAiProviderConfig("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(); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/service/housekeeper/HousekeeperServiceTest.java b/application/src/test/java/org/thingsboard/server/service/housekeeper/HousekeeperServiceTest.java index d701ce6113..a276a9c5ff 100644 --- a/application/src/test/java/org/thingsboard/server/service/housekeeper/HousekeeperServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/housekeeper/HousekeeperServiceTest.java @@ -31,6 +31,8 @@ import org.thingsboard.rule.engine.metadata.TbGetAttributesNode; import org.thingsboard.rule.engine.metadata.TbGetAttributesNodeConfiguration; import org.thingsboard.server.common.data.ApiUsageState; import org.thingsboard.server.common.data.AttributeScope; +import org.thingsboard.server.common.data.Customer; +import org.thingsboard.server.common.data.Dashboard; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.EventInfo; import org.thingsboard.server.common.data.StringUtils; @@ -77,6 +79,8 @@ import org.thingsboard.server.controller.AbstractControllerTest; import org.thingsboard.server.dao.alarm.AlarmDao; import org.thingsboard.server.dao.alarm.AlarmService; import org.thingsboard.server.dao.attributes.AttributesService; +import org.thingsboard.server.dao.customer.CustomerService; +import org.thingsboard.server.dao.dashboard.DashboardService; import org.thingsboard.server.dao.entity.EntityServiceRegistry; import org.thingsboard.server.dao.event.EventService; import org.thingsboard.server.dao.relation.RelationService; @@ -145,6 +149,10 @@ public class HousekeeperServiceTest extends AbstractControllerTest { private ApiUsageStateDao apiUsageStateDao; @Autowired private EntityServiceRegistry entityServiceRegistry; + @Autowired + private CustomerService customerService; + @Autowired + private DashboardService dashboardService; @SpyBean private TsHistoryDeletionTaskProcessor tsHistoryDeletionTaskProcessor; @@ -238,11 +246,67 @@ public class HousekeeperServiceTest extends AbstractControllerTest { doDelete("/api/device/" + device.getId()).andExpect(status().isOk()); - await().atMost(30, TimeUnit.SECONDS).untilAsserted(() -> { + await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> { verifyNoAlarms(device.getId()); }); } + @Test + public void whenAssetIsDeleted_thenDeleteAllAlarms() throws Exception { + Asset asset = createAsset(); + for (int i = 1; i <= 1000; i++) { + createAlarm(asset.getId()); + } + + doDelete("/api/asset/" + asset.getId()).andExpect(status().isOk()); + + await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> { + verifyNoAlarms(asset.getId()); + }); + } + + @Test + public void whenDashboardIsDeleted_thenDeleteAllAlarms() throws Exception { + Dashboard dashboard = createDashboard(); + for (int i = 1; i <= 1000; i++) { + createAlarm(dashboard.getId()); + } + + doDelete("/api/dashboard/" + dashboard.getId()).andExpect(status().isOk()); + + await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> { + verifyNoAlarms(dashboard.getId()); + }); + } + + @Test + public void whenCustomerIsDeleted_thenDeleteAllAlarms() throws Exception { + Customer customer = createCustomer(); + for (int i = 1; i <= 1000; i++) { + createAlarm(customer.getId()); + } + + doDelete("/api/customer/" + customer.getId()).andExpect(status().isOk()); + + await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> { + verifyNoAlarms(customer.getId()); + }); + } + + @Test + public void whenUserIsDeleted_thenDeleteAllAlarms() throws Exception { + UserId userId = customerUserId; + for (int i = 1; i <= 1000; i++) { + createAlarm(userId); + } + + doDelete("/api/user/" + userId).andExpect(status().isOk()); + + await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> { + verifyNoAlarms(userId); + }); + } + @Test public void whenTenantIsDeleted_thenDeleteAllEntitiesAndCleanUpRelatedData() throws Exception { loginDifferentTenant(); @@ -335,7 +399,7 @@ public class HousekeeperServiceTest extends AbstractControllerTest { doDelete("/api/device/" + device.getId()).andExpect(status().isOk()); int attempts = 2; - await().atMost(30, TimeUnit.SECONDS).pollInterval(1, TimeUnit.SECONDS).untilAsserted(() -> { + await().atMost(TIMEOUT, TimeUnit.SECONDS).pollInterval(1, TimeUnit.SECONDS).untilAsserted(() -> { for (int i = 0; i <= attempts; i++) { int attempt = i; verify(housekeeperReprocessingService).submitForReprocessing(argThat(getTaskMatcher(device.getId(), HousekeeperTaskType.DELETE_TS_HISTORY, @@ -345,7 +409,7 @@ public class HousekeeperServiceTest extends AbstractControllerTest { assertThat(getTimeseriesHistory(device.getId())).isNotEmpty(); doCallRealMethod().when(tsHistoryDeletionTaskProcessor).process(any()); - await().atMost(30, TimeUnit.SECONDS).untilAsserted(() -> { + await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> { assertThat(getTimeseriesHistory(device.getId())).isEmpty(); }); } @@ -379,7 +443,7 @@ public class HousekeeperServiceTest extends AbstractControllerTest { doDelete("/api/device/" + device.getId()).andExpect(status().isOk()); int attempts = 2; - await().atMost(30, TimeUnit.SECONDS).pollInterval(1, TimeUnit.SECONDS).untilAsserted(() -> { + await().atMost(TIMEOUT, TimeUnit.SECONDS).pollInterval(1, TimeUnit.SECONDS).untilAsserted(() -> { for (int i = 0; i <= attempts; i++) { int attempt = i; verify(housekeeperReprocessingService).submitForReprocessing(argThat(getTaskMatcher(device.getId(), HousekeeperTaskType.DELETE_TS_HISTORY, @@ -393,7 +457,7 @@ public class HousekeeperServiceTest extends AbstractControllerTest { doCallRealMethod().when(tsHistoryDeletionTaskProcessor).process(any()); someExecutor.shutdown(); - await().atMost(30, TimeUnit.SECONDS).untilAsserted(() -> { + await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> { assertThat(getTimeseriesHistory(device.getId())).isEmpty(); }); } @@ -409,7 +473,7 @@ public class HousekeeperServiceTest extends AbstractControllerTest { doDelete("/api/device/" + device.getId()).andExpect(status().isOk()); int maxAttempts = 5; - await().atMost(30, TimeUnit.SECONDS).untilAsserted(() -> { + await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> { for (int i = 1; i <= maxAttempts; i++) { verifyTaskProcessing(device.getId(), HousekeeperTaskType.DELETE_TS_HISTORY, i); } @@ -479,7 +543,6 @@ public class HousekeeperServiceTest extends AbstractControllerTest { eventService.saveAsync(event); await().atMost(10, TimeUnit.SECONDS) .until(() -> !getEvents(entityId).isEmpty()); - } private void createRelation(DeviceId to, AssetId from) { @@ -502,14 +565,14 @@ public class HousekeeperServiceTest extends AbstractControllerTest { assertThat(alarmService.findAlarmIdsByOriginatorId(tenantId, deviceId, 0, null, 10)).isNotEmpty(); } - private void createAlarm(DeviceId deviceId) { - Alarm alarm = doPost("/api/alarm", Alarm.builder() + private void createAlarm(EntityId entityId) { + doPost("/api/alarm", Alarm.builder() .tenantId(tenantId) - .originator(deviceId) + .originator(entityId) .severity(AlarmSeverity.CRITICAL) - .type("test alarm for " + deviceId + " " + RandomStringUtils.randomAlphabetic(10)) + .type("test alarm for " + entityId + " " + RandomStringUtils.randomAlphabetic(10)) .build(), Alarm.class); - assertThat(alarmService.findAlarmIdsByOriginatorId(tenantId, deviceId, 0, null, 10)).isNotEmpty(); + assertThat(alarmService.findAlarmIdsByOriginatorId(tenantId, entityId, 0, null, 10)).isNotEmpty(); } private TsKvEntry getLatestTelemetry(EntityId entityId) throws Exception { @@ -534,6 +597,20 @@ public class HousekeeperServiceTest extends AbstractControllerTest { return doPost("/api/asset", asset, Asset.class); } + private Customer createCustomer() { + Customer customer = new Customer(); + customer.setTenantId(tenantId); + customer.setTitle(StringUtils.randomAlphabetic(10)); + return customerService.saveCustomer(customer); + } + + private Dashboard createDashboard() { + Dashboard dashboard = new Dashboard(); + dashboard.setTenantId(tenantId); + dashboard.setTitle(StringUtils.randomAlphabetic(10)); + return dashboardService.saveDashboard(dashboard); + } + private RuleChainMetaData createRuleChain() { RuleChain ruleChain = new RuleChain(); ruleChain.setTenantId(tenantId); 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/queue/ruleengine/TbRuleEngineQueueConsumerManagerTest.java b/application/src/test/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineQueueConsumerManagerTest.java index 09bd02e5a4..bcbe52b5c9 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()); }); diff --git a/application/src/test/java/org/thingsboard/server/service/script/NashornJsInvokeServiceTest.java b/application/src/test/java/org/thingsboard/server/service/script/NashornJsInvokeServiceTest.java index 0942cef75d..b8ab48b38d 100644 --- a/application/src/test/java/org/thingsboard/server/service/script/NashornJsInvokeServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/script/NashornJsInvokeServiceTest.java @@ -25,11 +25,13 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.test.context.TestPropertySource; import org.thingsboard.common.util.TbStopWatch; import org.thingsboard.script.api.ScriptType; +import org.thingsboard.script.api.TbScriptException; import org.thingsboard.script.api.js.NashornJsInvokeService; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.controller.AbstractControllerTest; import org.thingsboard.server.dao.service.DaoSqlTest; +import javax.script.ScriptException; import java.util.ArrayList; import java.util.List; import java.util.UUID; @@ -39,6 +41,7 @@ import java.util.concurrent.TimeoutException; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.api.InstanceOfAssertFactories.type; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.thingsboard.server.common.data.msg.TbMsgType.POST_TELEMETRY_REQUEST; @@ -59,6 +62,25 @@ class NashornJsInvokeServiceTest extends AbstractControllerTest { @Value("${js.local.max_errors}") private int maxJsErrors; + @Test + void givenUncompilableScript_whenEvaluating_thenThrowsErrorWithCompilationErrorCode() { + // GIVEN + var uncompilableScript = "return msg.temperature?.value;"; + + // WHEN-THEN + assertThatThrownBy(() -> evalScript(uncompilableScript)) + .isInstanceOf(ExecutionException.class) + .cause() + .isInstanceOf(TbScriptException.class) + .asInstanceOf(type(TbScriptException.class)) + .satisfies(ex -> { + assertThat(ex.getScriptId()).isNotNull(); + assertThat(ex.getErrorCode()).isEqualTo(TbScriptException.ErrorCode.COMPILATION); + assertThat(ex.getBody()).contains(uncompilableScript); + assertThat(ex.getCause()).isInstanceOf(ScriptException.class); + }); + } + @Test void givenSimpleScriptTestPerformance() throws ExecutionException, InterruptedException { int iterations = 1000; diff --git a/application/src/test/java/org/thingsboard/server/service/script/RemoteJsInvokeServiceTest.java b/application/src/test/java/org/thingsboard/server/service/script/RemoteJsInvokeServiceTest.java index 363f21fa10..36990d9768 100644 --- a/application/src/test/java/org/thingsboard/server/service/script/RemoteJsInvokeServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/script/RemoteJsInvokeServiceTest.java @@ -23,9 +23,9 @@ import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; import org.springframework.test.util.ReflectionTestUtils; import org.thingsboard.script.api.ScriptType; +import org.thingsboard.script.api.TbScriptException; import org.thingsboard.server.common.data.ApiUsageState; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.stats.DefaultStatsFactory; import org.thingsboard.server.common.stats.StatsCounter; import org.thingsboard.server.common.stats.StatsFactory; import org.thingsboard.server.common.stats.TbApiUsageReportClient; @@ -42,8 +42,11 @@ import java.util.List; import java.util.Optional; import java.util.Set; 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.assertj.core.api.InstanceOfAssertFactories.type; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.Mockito.doAnswer; @@ -60,7 +63,6 @@ class RemoteJsInvokeServiceTest { private RemoteJsInvokeService remoteJsInvokeService; private TbQueueRequestTemplate, TbProtoQueueMsg> jsRequestTemplate; - @BeforeEach public void beforeEach() { TbApiUsageStateClient apiUsageStateClient = mock(TbApiUsageStateClient.class); @@ -74,7 +76,7 @@ class RemoteJsInvokeServiceTest { remoteJsInvokeService.requestTemplate = jsRequestTemplate; StatsFactory statsFactory = mock(StatsFactory.class); when(statsFactory.createStatsCounter(any(), any())).thenReturn(mock(StatsCounter.class)); - ReflectionTestUtils.setField(remoteJsInvokeService, "statsFactory",statsFactory); + ReflectionTestUtils.setField(remoteJsInvokeService, "statsFactory", statsFactory); remoteJsInvokeService.init(); } @@ -84,7 +86,36 @@ class RemoteJsInvokeServiceTest { } @Test - public void whenInvokingFunction_thenDoNotSendScriptBody() throws Exception { + void givenUncompilableScript_whenEvaluating_thenThrowsErrorWithCompilationErrorCode() { + // GIVEN + doAnswer(methodCall -> Futures.immediateFuture(new TbProtoJsQueueMsg<>(UUID.randomUUID(), RemoteJsResponse.newBuilder() + .setCompileResponse(JsInvokeProtos.JsCompileResponse.newBuilder() + .setSuccess(false) + .setErrorCode(JsInvokeProtos.JsInvokeErrorCode.COMPILATION_ERROR) + .setErrorDetails("SyntaxError: Unexpected token 'const'") + .setScriptHash(methodCall.>getArgument(0).getValue().getCompileRequest().getScriptHash()) + .build()) + .build()))) + .when(jsRequestTemplate).send(argThat(jsQueueMsg -> jsQueueMsg.getValue().hasCompileRequest())); + + var uncompilableScript = "let const = 'this is not allowed';"; + + // WHEN-THEN + assertThatThrownBy(() -> remoteJsInvokeService.eval(TenantId.SYS_TENANT_ID, ScriptType.RULE_NODE_SCRIPT, uncompilableScript).get()) + .isInstanceOf(ExecutionException.class) + .cause() + .isInstanceOf(TbScriptException.class) + .asInstanceOf(type(TbScriptException.class)) + .satisfies(ex -> { + assertThat(ex.getScriptId()).isNotNull(); + assertThat(ex.getErrorCode()).isEqualTo(TbScriptException.ErrorCode.COMPILATION); + assertThat(ex.getBody()).contains(uncompilableScript); + assertThat(ex.getCause()).isInstanceOf(RuntimeException.class).hasMessage("SyntaxError: Unexpected token 'const'"); + }); + } + + @Test + void whenInvokingFunction_thenDoNotSendScriptBody() throws Exception { mockJsEvalResponse(); String scriptBody = "return { a: 'b'};"; UUID scriptId = remoteJsInvokeService.eval(TenantId.SYS_TENANT_ID, ScriptType.RULE_NODE_SCRIPT, scriptBody).get(); @@ -110,7 +141,7 @@ class RemoteJsInvokeServiceTest { } @Test - public void whenInvokingFunctionAndRemoteJsExecutorRemovedScript_thenHandleNotFoundErrorAndMakeInvokeRequestWithScriptBody() throws Exception { + void whenInvokingFunctionAndRemoteJsExecutorRemovedScript_thenHandleNotFoundErrorAndMakeInvokeRequestWithScriptBody() throws Exception { mockJsEvalResponse(); String scriptBody = "return { a: 'b'};"; UUID scriptId = remoteJsInvokeService.eval(TenantId.SYS_TENANT_ID, ScriptType.RULE_NODE_SCRIPT, scriptBody).get(); @@ -156,7 +187,7 @@ class RemoteJsInvokeServiceTest { } @Test - public void whenDoingEval_thenSaveScriptByHashOfTenantIdAndScriptBody() throws Exception { + void whenDoingEval_thenSaveScriptByHashOfTenantIdAndScriptBody() throws Exception { mockJsEvalResponse(); TenantId tenantId1 = TenantId.fromUUID(UUID.randomUUID()); @@ -187,7 +218,7 @@ class RemoteJsInvokeServiceTest { } @Test - public void whenReleasingScript_thenCheckForHashUsages() throws Exception { + void whenReleasingScript_thenCheckForHashUsages() throws Exception { mockJsEvalResponse(); String scriptBody = "return { a: 'b'};"; UUID scriptId1 = remoteJsInvokeService.eval(TenantId.SYS_TENANT_ID, ScriptType.RULE_NODE_SCRIPT, scriptBody).get(); diff --git a/application/src/test/java/org/thingsboard/server/service/script/TbelInvokeDocsIoTest.java b/application/src/test/java/org/thingsboard/server/service/script/TbelInvokeDocsIoTest.java index a90f9f19b0..a7affe6dff 100644 --- a/application/src/test/java/org/thingsboard/server/service/script/TbelInvokeDocsIoTest.java +++ b/application/src/test/java/org/thingsboard/server/service/script/TbelInvokeDocsIoTest.java @@ -26,8 +26,10 @@ import java.util.Base64; import java.util.Collections; import java.util.Comparator; import java.util.LinkedHashMap; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.concurrent.ExecutionException; import java.util.concurrent.atomic.AtomicReference; @@ -750,6 +752,284 @@ class TbelInvokeDocsIoTest extends AbstractTbelInvokeTest { assertEquals(expected, actual); } + + // Sets + @Test + public void setsCreateNewSetFromMap_Test() throws ExecutionException, InterruptedException { + msgStr = """ + {"list": ["B", "A", "C", "A"]} + """; + decoderStr = """ + var originalMap = {}; + var set1 = originalMap.entrySet(); // create new Set from map, Empty + var set2 = set1.clone(); // clone new Set, Empty + var result1 = set1.addAll(msg.list); // addAll list, no sort, size = 3 ("A" - duplicate) + return {set1: set1, + set2: set2, + result1: result1 + } + """; + Set expectedSet1 = new LinkedHashSet(List.of("B", "A", "C", "A")); + Set expectedSet2 = new LinkedHashSet(); + Map expected = new LinkedHashMap<>(); + expected.put("set1", expectedSet1); + expected.put("set2", expectedSet2); + expected.put("result1", true); + Object actual = invokeScript(evalScript(decoderStr), msgStr); + assertEquals(expected.toString(), actual.toString()); + } + + @Test + public void setsCreateNewSetFromCreateSetTbMethod_Test() throws ExecutionException, InterruptedException { + msgStr = """ + {"list": ["B", "A", "C", "A"]} + """; + decoderStr = """ + var set1 = toSet(msg.list); // create new Set from toSet() with list, no sort, size = 3 ("A" - duplicate) + var set2 = newSet(); // create new Set from newSet(), Empty + return {set1: set1, + set2: set2 + } + """; + Set expectedSet1 = new LinkedHashSet(List.of("B", "A", "C", "A")); + Set expectedSet2 = new LinkedHashSet(); + Map expected = new LinkedHashMap<>(); + expected.put("set1", expectedSet1); + expected.put("set2", expectedSet2); + Object actual = invokeScript(evalScript(decoderStr), msgStr); + assertEquals(expected.toString(), actual.toString()); + } + + @Test + public void setsForeachForLoop_Test() throws ExecutionException, InterruptedException { + msgStr = """ + {"list": ["A", "B", "C"]} + """; + decoderStr = """ + var set2 = toSet(msg.list); // create new from list, size = 3 + var set2_0 = set2.toArray()[0]; // return "A", value with index = 0 from Set + var set2Size = set2.size(); // return size = 3 + var smthForeach = ""; + foreach (item : set2) { // foreach for Set + smthForeach += item; // return "ABC" + } + var smthForLoop= ""; + var set2Array = set2.toArray(); // for loop for Set (Set to array)) + for (var i =0; i < set2.size; i++) { + smthForLoop += set2Array[i]; // return "ABC" + } + return { + set2: set2, + set2_0: set2_0, + set2Size: set2Size, + smthForeach: smthForeach, + smthForLoop: smthForLoop + } + """; + Set expectedSet2 = new LinkedHashSet(List.of("A", "B", "C")); + Map expected = new LinkedHashMap<>(); + expected.put("set2", expectedSet2); + expected.put("set2_0", expectedSet2.toArray()[0]); + expected.put("set2Size", expectedSet2.size()); + AtomicReference smth = new AtomicReference<>(""); + expectedSet2.forEach(s -> smth.updateAndGet(v -> v + s)); + expected.put("smthForeach", smth.get()); + expected.put("smthForLoop", smth.get()); + Object actual = invokeScript(evalScript(decoderStr), msgStr); + assertEquals(expected.toString(), actual.toString()); + } + + /** + * add + * delete/remove + * setCreate, setCreatList + */ + @Test + public void setsAddRemove_Test() throws ExecutionException, InterruptedException { + msgStr = """ + {"list": ["B", "C", "A", "B", "C", "hello", 34567]} + """; + decoderStr = """ + // add + var setAdd = toSet(["thigsboard", 4, 67]); // create new, size = 3 + var setAdd1_value = setAdd.clone(); // clone setAdd, size = 3 + var setAdd2_result = setAdd.add(35); // add value = 35, result = true + var setAdd2_value = setAdd.clone(); // clone setAdd (fixing the result add = 35), size = 4 + var setAddList1 = toSet(msg.list); // create new from list without duplicate value ("B" and "C" - only one), size = 5 + var setAdd3_result = setAdd.addAll(setAddList1); // add all without duplicate values, result = true + var setAdd3_value = setAdd.clone(); // clone setAdd (with addAll), size = 9 + var setAdd4_result = setAdd.add(35); // add duplicate value = 35, result = false + var setAdd4_value = setAdd.clone(); // clone setAdd (after add duplicate value = 35), size = 9 + var setAddList2 = toSet(msg.list); // create new from list without duplicate value ("B" and "C" - only one), start: size = 5, finish: size = 7 + var setAdd5_result1 = setAddList2.add(72); // add is not duplicate value = 72, result = true + var setAdd5_result2 = setAddList2.add(72); // add duplicate value = 72, result = false + var setAdd5_result3 = setAddList2.add("hello25"); // add is not duplicate value = "hello25", result = true + var setAdd5_value = setAddList2.clone(); // clone setAddList2, size = 7 + var setAdd6_result = setAdd.addAll(setAddList2); // add all with duplicate values, result = true + var setAdd6_value = setAdd.clone(); // clone setAdd (after addAll setAddList2), before size = 9, after size = 11, added only is not duplicate values {"hello25", 72} + + // remove + var setAdd7_value = setAdd6_value.clone(); // clone setAdd6_value, before size = 11, after remove value = 4 size = 10 + var setAdd7_result = setAdd7_value.remove(4); // remove value = 4, result = true + var setAdd8_value = setAdd7_value.clone(); // clone setAdd7_value, before size = 10, after clear size = 0 + setAdd8_value.clear(); // setAdd8_value clear, result size = 0 + return { + "setAdd1_value": setAdd1_value, + "setAdd2_result": setAdd2_result, + "setAdd2_value": setAdd2_value, + "setAddList1": setAddList1, + "setAdd3_result": setAdd3_result, + "setAdd3_value": setAdd3_value, + "setAdd4_result": setAdd4_result, + "setAdd4_value": setAdd4_value, + "setAdd5_result1": setAdd5_result1, + "setAdd5_result2": setAdd5_result2, + "setAdd5_result3": setAdd5_result3, + "setAddList2": setAddList2, + "setAdd5_value": setAdd5_value, + "setAdd6_result": setAdd6_result, + "setAdd6_value": setAdd6_value, + "setAdd7_result": setAdd7_result, + "setAdd7_value": setAdd7_value, + "setAdd8_value": setAdd8_value + }; + """; + ArrayList list = new ArrayList<>(List.of("B", "C", "A", "B", "C", "hello", 34567)); + ArrayList listAdd = new ArrayList<>(List.of("thigsboard", 4, 67)); + Set setAdd = new LinkedHashSet<>(listAdd); + Set setAdd1_value = new LinkedHashSet<>(setAdd); + boolean setAdd2_result = setAdd.add(35); + Set setAdd2_value = new LinkedHashSet<>(setAdd); + Set setAddList1 = new LinkedHashSet<>(list); + boolean setAdd3_result = setAdd.addAll(setAddList1); + Set setAdd3_value = new LinkedHashSet<>(setAdd); + boolean setAdd4_result = setAdd.add(35); + Set setAdd4_value = new LinkedHashSet<>(setAdd); + Set setAddList2 = new LinkedHashSet<>(list); + boolean setAdd5_result1 = setAddList2.add(72); + boolean setAdd5_result2 = setAddList2.add(72); + boolean setAdd5_result3 = setAddList2.add("hello25"); + Set setAdd5_value = new LinkedHashSet<>(setAddList2); + boolean setAdd6_result = setAdd.addAll(setAddList2); + Set setAdd6_value = new LinkedHashSet<>(setAdd); + // remove + Set setAdd7_value = new LinkedHashSet<>(setAdd6_value); + boolean setAdd7_result = setAdd7_value.remove(4); + Set setAdd8_value = new LinkedHashSet<>(setAdd7_value); + setAdd8_value.clear(); + + LinkedHashMap expected = new LinkedHashMap<>(); + expected.put("setAdd1_value", setAdd1_value); + expected.put("setAdd2_result", setAdd2_result); + expected.put("setAdd2_value", setAdd2_value); + expected.put("setAddList1", setAddList1); + expected.put("setAdd3_result", setAdd3_result); + expected.put("setAdd3_value", setAdd3_value); + expected.put("setAdd4_result", setAdd4_result); + expected.put("setAdd4_value", setAdd4_value); + expected.put("setAdd5_result1", setAdd5_result1); + expected.put("setAdd5_result2", setAdd5_result2); + expected.put("setAdd5_result3", setAdd5_result3); + expected.put("setAddList2", setAddList2); + expected.put("setAdd5_value", setAdd5_value); + expected.put("setAdd6_result", setAdd6_result); + expected.put("setAdd6_value", setAdd6_value); + expected.put("setAdd7_result", setAdd7_result); + expected.put("setAdd7_value", setAdd7_value); + expected.put("setAdd8_value", setAdd8_value); + + Object actual = invokeScript(evalScript(decoderStr), msgStr); + assertEquals(expected.toString(), actual.toString()); + } + + @Test + public void setsSort_Test() throws ExecutionException, InterruptedException { + msgStr = """ + {"list": ["C", "B", "A", 34567, "B", "C", "hello", 34]} + """; + decoderStr = """ + var set1 = toSet(msg.list); // create new from method toSet(List list) no sort, size = 6 ("A" and "C" is duplicated) + var set2 = toSet(msg.list); // create new from method toSet(List list) no sort, size = 6 ("A" and "C" is duplicated) + var set1_asc = set1.clone(); // clone set1, size = 6 + var set1_desc = set1.clone(); // clone set1, size = 6 + set1.sort(); // sort set1 -> asc + set1_asc.sort(true); // sort set1_asc -> asc + set1_desc.sort(false); // sort set1_desc -> desc + var set3 = set2.toSorted(); // toSorted set3 -> asc + var set3_asc = set2.toSorted(true); // toSorted set3 -> asc + var set3_desc = set2.toSorted(false); // toSorted set3 -> desc + return { + "set1": set1, + "set1_asc": set1_asc, + "set1_desc": set1_desc, + "set2": set2, + "set3": set3, + "set3_asc": set3_asc, + "set3_desc": set3_desc, + } + """; + ArrayList list = new ArrayList<>(List.of("C", "B", "A", 34567, "hello", 34)); + Set expected = new LinkedHashSet<>(list); + ArrayList listSortAsc = new ArrayList<>(List.of(34, 34567, "A", "B", "C", "hello")); + Set expectedAsc = new LinkedHashSet<>(listSortAsc); + ArrayList listSortDesc = new ArrayList<>(List.of("hello", "C", "B", "A", 34567, 34)); + Set expectedDesc = new LinkedHashSet<>(listSortDesc); + Object actual = invokeScript(evalScript(decoderStr), msgStr); + assertEquals(expectedAsc.toString(), ((LinkedHashMap)actual).get("set1").toString()); + assertEquals(expectedAsc.toString(), ((LinkedHashMap)actual).get("set1_asc").toString()); + assertEquals(expectedDesc.toString(), ((LinkedHashMap)actual).get("set1_desc").toString()); + assertEquals(expected.toString(), ((LinkedHashMap)actual).get("set2").toString()); + assertEquals(expectedAsc.toString(), ((LinkedHashMap)actual).get("set3").toString()); + assertEquals(expectedAsc.toString(), ((LinkedHashMap)actual).get("set3_asc").toString()); + assertEquals(expectedDesc.toString(), ((LinkedHashMap)actual).get("set3_desc").toString()); + } + + @Test + public void setsContains_Test() throws ExecutionException, InterruptedException { + msgStr = """ + {"list": ["C", "B", "A", 34567, "B", "C", "hello", 34]} + """; + decoderStr = """ + var set1 = toSet(msg.list); // create new from method toSet(List list) no sort, size = 6 ("A" and "C" is duplicated) + var result1 = set1.contains("A"); // return true + var result2 = set1.contains("H"); // return false + return { + "set1": set1, + "result1": result1, + "result2": result2 + } + """; + List listOrigin = new ArrayList<>(List.of("C", "B", "A", 34567, "B", "C", "hello", 34)); + Set expectedSet = new LinkedHashSet<>(listOrigin); + Object actual = invokeScript(evalScript(decoderStr), msgStr); + assertEquals(expectedSet.toString(), ((LinkedHashMap)actual).get("set1").toString()); + assertEquals(true, ((LinkedHashMap)actual).get("result1")); + assertEquals(false, ((LinkedHashMap)actual).get("result2")); + } + + @Test + public void setsToList_Test() throws ExecutionException, InterruptedException { + msgStr = """ + {"list": ["C", "B", "A", 34567, "B", "C", "hello", 34]} + """; + decoderStr = """ + var set1 = toSet(msg.list); // create new from method toSet(List list) no sort, size = 6 ("A" and "C" is duplicated) + var tolist = set1.toList(); // create new List from Set, size = 6 + return { + "list": msg.list, + "set1": set1, + "tolist": tolist + } + """; + List listOrigin = new ArrayList<>(List.of("C", "B", "A", 34567, "B", "C", "hello", 34)); + Set expectedSet = new LinkedHashSet<>(listOrigin); + List expectedToList = new ArrayList<>(expectedSet); + Object actual = invokeScript(evalScript(decoderStr), msgStr); + assertEquals(listOrigin.toString(), ((LinkedHashMap)actual).get("list").toString()); + assertEquals(expectedSet.toString(), ((LinkedHashMap)actual).get("set1").toString()); + assertEquals(expectedToList.toString(), ((LinkedHashMap)actual).get("tolist").toString()); + } + @Test public void arraysWillCauseArrayIndexOutOfBoundsException_Test() throws ExecutionException, InterruptedException { msgStr = """ @@ -2399,25 +2679,19 @@ class TbelInvokeDocsIoTest extends AbstractTbelInvokeTest { list.add(0x35); return isList(list); """); + } + + @Test + public void isSet_Test() throws ExecutionException, InterruptedException { + msgStr = """ + {"list": ["C", "B", "A", 34567, "B", "C", "hello", 34]} + """; + decoderStr = """ + return isSet(toSet(msg.list)); // return true + """; Object actual = invokeScript(evalScript(decoderStr), msgStr); assertInstanceOf(Boolean.class, actual); assertTrue((Boolean) actual); - decoderStr = String.format(""" - var list = []; - list.add(0x35); - return isMap(list); - """); - actual = invokeScript(evalScript(decoderStr), msgStr); - assertInstanceOf(Boolean.class, actual); - assertFalse((Boolean) actual); - decoderStr = String.format(""" - var list = []; - list.add(0x35); - return isArray(list); - """); - actual = invokeScript(evalScript(decoderStr), msgStr); - assertInstanceOf(Boolean.class, actual); - assertFalse((Boolean) actual); } @Test @@ -2435,23 +2709,13 @@ class TbelInvokeDocsIoTest extends AbstractTbelInvokeTest { Object actual = invokeScript(evalScript(decoderStr), msgStr); assertInstanceOf(Boolean.class, actual); assertTrue((Boolean) actual); - decoderStr = """ - var array = new int[3]; - array[0] = 1; - array[1] = 2; - array[2] = 3; - return isList(array); - """; - actual = invokeScript(evalScript(decoderStr), msgStr); - assertInstanceOf(Boolean.class, actual); - assertFalse((Boolean) actual); } @Test public void isInsidePolygon_Test() throws ExecutionException, InterruptedException { msgStr = "{}"; decoderStr = """ - String perimeter = "[[[37.7810,-122.4210],[37.7890,-122.3900],[37.7700,-122.3800],[37.7600,-122.4000],[37.7700,-122.4250],[37.7810,-122.4210]],[[37.7730,-122.4050],[37.7700,-122.3950],[37.7670,-122.3980],[37.7690,-122.4100],[37.7730,-122.4050]]]"; + var perimeter = "[[[37.7810,-122.4210],[37.7890,-122.3900],[37.7700,-122.3800],[37.7600,-122.4000],[37.7700,-122.4250],[37.7810,-122.4210]],[[37.7730,-122.4050],[37.7700,-122.3950],[37.7670,-122.3980],[37.7690,-122.4100],[37.7730,-122.4050]]]"; return{ outsidePolygon: isInsidePolygon(37.8000, -122.4300, perimeter), insidePolygon: isInsidePolygon(37.7725, -122.4010, perimeter), @@ -2470,7 +2734,7 @@ class TbelInvokeDocsIoTest extends AbstractTbelInvokeTest { public void isInsideCircle_Test() throws ExecutionException, InterruptedException { msgStr = "{}"; decoderStr = """ - String perimeter = "{\\"latitude\\":37.7749,\\"longitude\\":-122.4194,\\"radius\\":3000,\\"radiusUnit\\":\\"METER\\"}"; + var perimeter = "{\\"latitude\\":37.7749,\\"longitude\\":-122.4194,\\"radius\\":3000,\\"radiusUnit\\":\\"METER\\"}"; return{ outsideCircle: isInsideCircle(37.8044, -122.2712, perimeter), insideCircle: isInsideCircle(37.7599, -122.4148, perimeter) diff --git a/application/src/test/java/org/thingsboard/server/service/script/TbelInvokeServiceTest.java b/application/src/test/java/org/thingsboard/server/service/script/TbelInvokeServiceTest.java index 732f31f044..fc66f806d7 100644 --- a/application/src/test/java/org/thingsboard/server/service/script/TbelInvokeServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/script/TbelInvokeServiceTest.java @@ -20,10 +20,12 @@ import com.github.benmanes.caffeine.cache.Cache; import org.junit.Assert; import org.junit.Ignore; import org.junit.jupiter.api.Test; +import org.mvel2.CompileException; import org.springframework.beans.factory.annotation.Value; import org.springframework.test.context.TestPropertySource; import org.springframework.test.util.ReflectionTestUtils; import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.script.api.TbScriptException; import org.thingsboard.script.api.tbel.TbelScript; import java.io.Serializable; @@ -37,6 +39,7 @@ import java.util.concurrent.TimeUnit; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.api.InstanceOfAssertFactories.type; @TestPropertySource(properties = { "tbel.max_script_body_size=100", @@ -50,6 +53,25 @@ class TbelInvokeServiceTest extends AbstractTbelInvokeTest { @Value("${tbel.max_errors}") private int maxJsErrors; + @Test + void givenUncompilableScript_whenEvaluating_thenThrowsErrorWithCompilationErrorCode() { + // GIVEN + var uncompilableScript = "return msg.property !== undefined;"; + + // WHEN-THEN + assertThatThrownBy(() -> evalScript(uncompilableScript)) + .isInstanceOf(ExecutionException.class) + .cause() + .isInstanceOf(TbScriptException.class) + .asInstanceOf(type(TbScriptException.class)) + .satisfies(ex -> { + assertThat(ex.getScriptId()).isNotNull(); + assertThat(ex.getErrorCode()).isEqualTo(TbScriptException.ErrorCode.COMPILATION); + assertThat(ex.getBody()).isEqualTo(uncompilableScript); + assertThat(ex.getCause()).isInstanceOf(CompileException.class); + }); + } + @Test void givenSimpleScriptTestPerformance() throws ExecutionException, InterruptedException { int iterations = 100000; diff --git a/application/src/test/java/org/thingsboard/server/service/sync/ie/ExportImportServiceSqlTest.java b/application/src/test/java/org/thingsboard/server/service/sync/ie/ExportImportServiceSqlTest.java index da3b214afb..e70a0cd37c 100644 --- a/application/src/test/java/org/thingsboard/server/service/sync/ie/ExportImportServiceSqlTest.java +++ b/application/src/test/java/org/thingsboard/server/service/sync/ie/ExportImportServiceSqlTest.java @@ -66,6 +66,7 @@ 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.EntityViewId; +import org.thingsboard.server.common.data.id.OtaPackageId; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.msg.TbNodeConnectionType; @@ -203,11 +204,12 @@ public class ExportImportServiceSqlTest extends AbstractControllerTest { AssetProfile assetProfile = createAssetProfile(tenantId1, ruleChain.getId(), dashboard.getId(), "Asset profile 1"); Asset asset = createAsset(tenantId1, null, assetProfile.getId(), "Asset 1"); DeviceProfile deviceProfile = createDeviceProfile(tenantId1, ruleChain.getId(), dashboard.getId(), "Device profile 1"); - Device device = createDevice(tenantId1, null, deviceProfile.getId(), "Device 1"); + OtaPackage firmware = createOtaPackage(tenantId1, deviceProfile.getId(), OtaPackageType.FIRMWARE); + Device device = createDevice(tenantId1, null, deviceProfile.getId(), "Device 1", firmware.getId(), null); CalculatedField calculatedField = createCalculatedField(tenantId1, device.getId(), asset.getId()); Map entitiesExportData = Stream.of(customer.getId(), asset.getId(), device.getId(), - ruleChain.getId(), dashboard.getId(), assetProfile.getId(), deviceProfile.getId()) + ruleChain.getId(), dashboard.getId(), assetProfile.getId(), deviceProfile.getId(), firmware.getId()) .map(entityId -> { try { return exportEntity(tenantAdmin1, entityId, EntityExportSettings.builder() @@ -275,12 +277,17 @@ public class ExportImportServiceSqlTest extends AbstractControllerTest { verify(tbClusterService).sendNotificationMsgToEdge(any(), any(), eq(importedDeviceProfile.getId()), any(), any(), eq(EdgeEventActionType.ADDED), any()); verify(otaPackageStateService).update(eq(importedDeviceProfile), eq(false), eq(false)); + OtaPackage importedFirmware = (OtaPackage) importEntity(tenantAdmin2, getAndClone(entitiesExportData, EntityType.OTA_PACKAGE)).getSavedEntity(); + verify(entityActionService).logEntityAction(any(), eq(importedFirmware.getId()), eq(importedFirmware), + any(), eq(ActionType.ADDED), isNull()); + Device importedDevice = (Device) importEntity(tenantAdmin2, getAndClone(entitiesExportData, EntityType.DEVICE)).getSavedEntity(); verify(entityActionService).logEntityAction(any(), eq(importedDevice.getId()), eq(importedDevice), any(), eq(ActionType.ADDED), isNull()); verify(tbClusterService).onDeviceUpdated(eq(importedDevice), isNull()); importEntity(tenantAdmin2, getAndClone(entitiesExportData, EntityType.DEVICE)); verify(tbClusterService, Mockito.never()).onDeviceUpdated(eq(importedDevice), eq(importedDevice)); + assertThat(importedDevice.getFirmwareId()).isEqualTo(importedFirmware.getId()); // calculated field of imported device: List calculatedFields = calculatedFieldService.findCalculatedFieldsByEntityId(tenantId2, importedDevice.getId()); @@ -318,14 +325,15 @@ public class ExportImportServiceSqlTest extends AbstractControllerTest { assetProfile = assetProfileService.saveAssetProfile(assetProfile); DeviceProfile deviceProfile = createDeviceProfile(tenantId1, ruleChain.getId(), dashboard.getId(), "Device profile 1"); - Device device = createDevice(tenantId1, customer.getId(), deviceProfile.getId(), "Device 1"); + OtaPackage firmware = createOtaPackage(tenantId1, deviceProfile.getId(), OtaPackageType.FIRMWARE); + Device device = createDevice(tenantId1, customer.getId(), deviceProfile.getId(), "Device 1", firmware.getId(), null); EntityView entityView = createEntityView(tenantId1, customer.getId(), device.getId(), "Entity view 1"); CalculatedField calculatedField = createCalculatedField(tenantId1, device.getId(), device.getId()); Map ids = new HashMap<>(); for (EntityId entityId : List.of(customer.getId(), ruleChain.getId(), dashboard.getId(), assetProfile.getId(), asset.getId(), - deviceProfile.getId(), device.getId(), entityView.getId(), ruleChain.getId(), dashboard.getId())) { + deviceProfile.getId(), firmware.getId(), device.getId(), entityView.getId(), ruleChain.getId(), dashboard.getId())) { EntityExportData exportData = exportEntity(getSecurityUser(tenantAdmin1), entityId); EntityImportResult importResult = importEntity(getSecurityUser(tenantAdmin2), exportData, EntityImportSettings.builder() .saveCredentials(false) @@ -359,12 +367,17 @@ public class ExportImportServiceSqlTest extends AbstractControllerTest { assertThat(exportedDeviceProfile.getDefaultRuleChainId()).isEqualTo(ruleChain.getId()); assertThat(exportedDeviceProfile.getDefaultDashboardId()).isEqualTo(dashboard.getId()); - EntityExportData entityExportData = exportEntity(tenantAdmin2, (DeviceId) ids.get(device.getId())); + OtaPackage exportedFirmware = (OtaPackage) exportEntity(tenantAdmin2, (OtaPackageId) ids.get(firmware.getId())).getEntity(); + assertThat(exportedFirmware.getDeviceProfileId()).isEqualTo(exportedDeviceProfile.getId()); + assertThat(exportedFirmware.getId()).isEqualTo(firmware.getId()); + + EntityExportData entityExportData = exportEntity(tenantAdmin2, (DeviceId) ids.get(device.getId())); Device exportedDevice = entityExportData.getEntity(); assertThat(exportedDevice.getCustomerId()).isEqualTo(customer.getId()); assertThat(exportedDevice.getDeviceProfileId()).isEqualTo(deviceProfile.getId()); + assertThat(exportedDevice.getFirmwareId()).isEqualTo(firmware.getId()); - List calculatedFields = ((DeviceExportData) entityExportData).getCalculatedFields(); + List calculatedFields = entityExportData.getCalculatedFields(); assertThat(calculatedFields.size()).isOne(); CalculatedField field = calculatedFields.get(0); assertThat(field.getName()).isEqualTo(calculatedField.getName()); @@ -380,13 +393,15 @@ public class ExportImportServiceSqlTest extends AbstractControllerTest { deviceProfileService.saveDeviceProfile(importedDeviceProfile); } - protected Device createDevice(TenantId tenantId, CustomerId customerId, DeviceProfileId deviceProfileId, String name) { + protected Device createDevice(TenantId tenantId, CustomerId customerId, DeviceProfileId deviceProfileId, String name, OtaPackageId firmwareId, OtaPackageId softwareId) { Device device = new Device(); device.setTenantId(tenantId); device.setCustomerId(customerId); device.setName(name); device.setLabel("lbl"); device.setDeviceProfileId(deviceProfileId); + device.setFirmwareId(firmwareId); + device.setSoftwareId(softwareId); DeviceData deviceData = new DeviceData(); deviceData.setTransportConfiguration(new DefaultDeviceTransportConfiguration()); device.setDeviceData(deviceData); 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 c7d4068210..2db78871d4 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 @@ -116,8 +116,8 @@ import java.util.stream.Collectors; 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.controller.TbResourceControllerTest.TEST_DATA; import static org.thingsboard.server.controller.TbResourceControllerTest.JS_TEST_FILE_NAME; +import static org.thingsboard.server.controller.TbResourceControllerTest.TEST_DATA; @DaoSqlTest public class VersionControlTest extends AbstractControllerTest { @@ -262,19 +262,24 @@ public class VersionControlTest extends AbstractControllerTest { } @Test - public void testDeviceVc_withProfile_betweenTenants() throws Exception { + public void testDeviceVc_withProfileAndOtaPackage_betweenTenants() throws Exception { DeviceProfile deviceProfile = createDeviceProfile(null, null, "Device profile of tenant 1"); createVersion("profiles", EntityType.DEVICE_PROFILE); - Device device = createDevice(null, deviceProfile.getId(), "Device of tenant 1", "test1"); - String versionId = createVersion("devices", EntityType.DEVICE); + 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 -> { + newDevice.setFirmwareId(firmware.getId()); + newDevice.setSoftwareId(software.getId()); + }); + String versionId = createVersion("devices with ota", EntityType.DEVICE, EntityType.OTA_PACKAGE); DeviceCredentials deviceCredentials = findDeviceCredentials(device.getId()); DeviceCredentials newCredentials = new DeviceCredentials(deviceCredentials); newCredentials.setCredentialsId("new access token"); // updating access token to avoid constraint errors on import doPost("/api/device/credentials", newCredentials, DeviceCredentials.class); - assertThat(listVersions()).extracting(EntityVersion::getName).containsExactly("devices", "profiles"); + assertThat(listVersions()).extracting(EntityVersion::getName).containsExactly("devices with ota", "profiles"); loginTenant2(); - Map result = loadVersion(versionId, EntityType.DEVICE, EntityType.DEVICE_PROFILE); + Map result = loadVersion(versionId, EntityType.DEVICE, EntityType.DEVICE_PROFILE, EntityType.OTA_PACKAGE); assertThat(result.get(EntityType.DEVICE).getCreated()).isEqualTo(1); assertThat(result.get(EntityType.DEVICE_PROFILE).getCreated()).isEqualTo(1); @@ -293,6 +298,13 @@ public class VersionControlTest extends AbstractControllerTest { assertThat(importedCredentials.getCredentialsId()).isEqualTo(deviceCredentials.getCredentialsId()); assertThat(importedCredentials.getCredentialsValue()).isEqualTo(deviceCredentials.getCredentialsValue()); assertThat(importedCredentials.getCredentialsType()).isEqualTo(deviceCredentials.getCredentialsType()); + + OtaPackage importedFirmwareOta = findOtaPackage(firmware.getTitle()); + OtaPackage importedSoftwareOta = findOtaPackage(software.getTitle()); + checkImportedEntity(tenantId1, firmware, tenantId2, importedFirmwareOta); + checkImportedOtaPackageData(firmware, importedFirmwareOta); + checkImportedEntity(tenantId1, software, tenantId2, importedSoftwareOta); + checkImportedOtaPackageData(software, importedSoftwareOta); } @Test @@ -626,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()); @@ -635,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()); }); } @@ -653,6 +669,57 @@ public class VersionControlTest extends AbstractControllerTest { assertThat(importedCalculatedField.getType()).isEqualTo(calculatedField.getType()); } + @Test + public void testOtaPackageVc_sameTenant() throws Exception { + 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); + String versionId = createVersion("ota packages", EntityType.OTA_PACKAGE); + + OtaPackage firmwareOta = findOtaPackage(firmware.getTitle()); + OtaPackage softwareOta = findOtaPackage(software.getTitle()); + + loadVersion(versionId, EntityType.OTA_PACKAGE); + OtaPackage importedFirmwareOta = findOtaPackage(firmwareOta.getTitle()); + OtaPackage importedSoftwareOta = findOtaPackage(softwareOta.getTitle()); + checkImportedEntity(tenantId1, firmwareOta, tenantId1, importedFirmwareOta); + checkImportedOtaPackageData(firmwareOta, importedFirmwareOta); + checkImportedEntity(tenantId1, softwareOta, tenantId1, importedSoftwareOta); + checkImportedOtaPackageData(softwareOta, importedSoftwareOta); + } + + @Test + public void testOtaPackageVcWithProfile_betweenTenants() throws Exception { + 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); + deviceProfile.setFirmwareId(firmware.getId()); + deviceProfile.setSoftwareId(software.getId()); + deviceProfile = doPost("/api/deviceProfile", deviceProfile, DeviceProfile.class); + String versionId = createVersion("ota packages", EntityType.DEVICE_PROFILE, EntityType.OTA_PACKAGE); + + loginTenant2(); + loadVersion(versionId, EntityType.DEVICE_PROFILE, EntityType.OTA_PACKAGE); + DeviceProfile importedProfile = findDeviceProfile(deviceProfile.getName()); + OtaPackage importedFirmwareOta = findOtaPackage(firmware.getTitle()); + OtaPackage importedSoftwareOta = findOtaPackage(software.getTitle()); + checkImportedEntity(tenantId1, deviceProfile, tenantId2, importedProfile); + checkImportedDeviceProfileData(deviceProfile, importedProfile); + checkImportedEntity(tenantId1, firmware, tenantId2, importedFirmwareOta); + checkImportedOtaPackageData(firmware, importedFirmwareOta); + checkImportedEntity(tenantId1, software, tenantId2, importedSoftwareOta); + checkImportedOtaPackageData(software, importedSoftwareOta); + assertThat(importedProfile.getFirmwareId()).isEqualTo(importedFirmwareOta.getId()); + assertThat(importedProfile.getSoftwareId()).isEqualTo(importedSoftwareOta.getId()); + } + + protected void checkImportedOtaPackageData(OtaPackage otaPackage, OtaPackage importedOtaPackage) { + assertThat(importedOtaPackage.getName()).isEqualTo(otaPackage.getName()); + assertThat(importedOtaPackage.getTag()).isEqualTo(otaPackage.getTag()); + assertThat(importedOtaPackage.getType()).isEqualTo(otaPackage.getType()); + assertThat(importedOtaPackage.getFileName()).isEqualTo(otaPackage.getFileName()); + } + @Test public void testResourceVc_sameTenant() throws Exception { TbResourceInfo resourceInfo = createResource("Test resource"); @@ -923,6 +990,7 @@ public class VersionControlTest extends AbstractControllerTest { otaPackage.setDeviceProfileId(deviceProfileId); otaPackage.setType(type); otaPackage.setTitle("My " + type); + otaPackage.setTag("My " + type); otaPackage.setVersion("v1.0"); otaPackage.setFileName("filename.txt"); otaPackage.setContentType("text/plain"); @@ -933,6 +1001,10 @@ public class VersionControlTest extends AbstractControllerTest { return otaPackageService.saveOtaPackage(otaPackage); } + private OtaPackage findOtaPackage(String title) throws Exception { + return doGetTypedWithPageLink("/api/otaPackages?", new TypeReference>() {}, new PageLink(100, 0, title)).getData().get(0); + } + protected Dashboard createDashboard(CustomerId customerId, String name) { Dashboard dashboard = new Dashboard(); dashboard.setTitle(name); diff --git a/application/src/test/java/org/thingsboard/server/transport/coap/attributes/AbstractCoapAttributesIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/coap/attributes/AbstractCoapAttributesIntegrationTest.java index fb430c8963..e053ad9c9e 100644 --- a/application/src/test/java/org/thingsboard/server/transport/coap/attributes/AbstractCoapAttributesIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/coap/attributes/AbstractCoapAttributesIntegrationTest.java @@ -35,6 +35,7 @@ import org.thingsboard.server.common.data.device.profile.DefaultCoapDeviceTypeCo import org.thingsboard.server.common.data.device.profile.DeviceProfileTransportConfiguration; import org.thingsboard.server.common.data.device.profile.ProtoTransportPayloadConfiguration; import org.thingsboard.server.common.data.device.profile.TransportPayloadTypeConfiguration; +import org.thingsboard.server.common.data.query.AliasEntityId; import org.thingsboard.server.common.data.query.EntityKey; import org.thingsboard.server.common.data.query.EntityKeyType; import org.thingsboard.server.common.data.query.SingleEntityFilter; @@ -170,7 +171,7 @@ public abstract class AbstractCoapAttributesIntegrationTest extends AbstractCoap protected void processJsonTestRequestAttributesValuesFromTheServer() throws Exception { client = new CoapTestClient(accessToken, FeatureType.ATTRIBUTES); SingleEntityFilter dtf = new SingleEntityFilter(); - dtf.setSingleEntity(savedDevice.getId()); + dtf.setSingleEntity(AliasEntityId.fromEntityId(savedDevice.getId())); String clientKeysStr = "clientStr,clientBool,clientDbl,clientLong,clientJson"; String sharedKeysStr = "sharedStr,sharedBool,sharedDbl,sharedLong,sharedJson"; List clientKeysList = List.of(clientKeysStr.split(",")); @@ -200,7 +201,7 @@ public abstract class AbstractCoapAttributesIntegrationTest extends AbstractCoap protected void processProtoTestRequestAttributesValuesFromTheServer() throws Exception { client = new CoapTestClient(accessToken, FeatureType.ATTRIBUTES); SingleEntityFilter dtf = new SingleEntityFilter(); - dtf.setSingleEntity(savedDevice.getId()); + dtf.setSingleEntity(AliasEntityId.fromEntityId(savedDevice.getId())); String clientKeysStr = "clientStr,clientBool,clientDbl,clientLong,clientJson"; String sharedKeysStr = "sharedStr,sharedBool,sharedDbl,sharedLong,sharedJson"; List clientKeysList = List.of(clientKeysStr.split(",")); diff --git a/application/src/test/java/org/thingsboard/server/transport/coap/client/CoapClientIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/coap/client/CoapClientIntegrationTest.java index 4943d1b5c3..3845cc0c3c 100644 --- a/application/src/test/java/org/thingsboard/server/transport/coap/client/CoapClientIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/coap/client/CoapClientIntegrationTest.java @@ -29,6 +29,7 @@ import org.junit.Before; import org.junit.Test; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.query.AliasEntityId; import org.thingsboard.server.common.data.query.EntityKey; import org.thingsboard.server.common.data.query.EntityKeyType; import org.thingsboard.server.common.data.query.SingleEntityFilter; @@ -161,7 +162,7 @@ public class CoapClientIntegrationTest extends AbstractCoapIntegrationTest { protected void processTestRequestAttributesValuesFromTheServer(boolean confirmable) throws Exception { client = createClientForFeatureWithConfirmableParameter(FeatureType.ATTRIBUTES, confirmable); SingleEntityFilter dtf = new SingleEntityFilter(); - dtf.setSingleEntity(savedDevice.getId()); + dtf.setSingleEntity(AliasEntityId.fromEntityId(savedDevice.getId())); List csKeys = getEntityKeys(CLIENT_ATTRIBUTE); List shKeys = getEntityKeys(SHARED_ATTRIBUTE); List keys = new ArrayList<>(); 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 587f9ade1c..ddf8bca43f 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,8 +21,14 @@ import com.google.gson.JsonArray; import com.google.gson.JsonElement; import lombok.extern.slf4j.Slf4j; import org.apache.commons.io.IOUtils; +import org.eclipse.leshan.client.LeshanClient; import org.eclipse.leshan.client.object.Security; +import org.eclipse.leshan.client.servers.LwM2mServer; import org.eclipse.leshan.core.ResponseCode; +import org.eclipse.leshan.core.request.ContentFormat; +import org.eclipse.leshan.core.response.ErrorCallback; +import org.eclipse.leshan.core.response.ResponseCallback; +import org.eclipse.leshan.core.response.SendResponse; import org.eclipse.leshan.server.registration.Registration; import org.junit.After; import org.junit.Assert; @@ -57,6 +63,7 @@ import org.thingsboard.server.common.data.device.profile.lwm2m.bootstrap.Abstrac import org.thingsboard.server.common.data.device.profile.lwm2m.bootstrap.LwM2MBootstrapServerCredential; import org.thingsboard.server.common.data.device.profile.lwm2m.bootstrap.NoSecLwM2MBootstrapServerCredential; import org.thingsboard.server.common.data.id.DeviceProfileId; +import org.thingsboard.server.common.data.query.AliasEntityId; import org.thingsboard.server.common.data.query.EntityData; import org.thingsboard.server.common.data.query.EntityDataPageLink; import org.thingsboard.server.common.data.query.EntityDataQuery; @@ -73,6 +80,7 @@ import org.thingsboard.server.service.ws.telemetry.cmd.v2.LatestValueCmd; import org.thingsboard.server.transport.AbstractTransportIntegrationTest; import org.thingsboard.server.transport.lwm2m.client.LwM2MTestClient; import org.thingsboard.server.transport.lwm2m.server.client.LwM2mClientContext; +import org.thingsboard.server.transport.lwm2m.server.client.ResourceUpdateResult; import org.thingsboard.server.transport.lwm2m.server.uplink.DefaultLwM2mUplinkMsgHandler; import org.thingsboard.server.transport.lwm2m.server.uplink.LwM2mUplinkMsgHandler; @@ -82,6 +90,7 @@ import java.util.Arrays; import java.util.Collections; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Set; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; @@ -93,6 +102,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.timeout; import static org.mockito.Mockito.verify; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -340,7 +350,7 @@ public abstract class AbstractLwM2MIntegrationTest extends AbstractTransportInte Device device = createLwm2mDevice(deviceCredentials, endpoint, deviceProfile.getId()); SingleEntityFilter sef = new SingleEntityFilter(); - sef.setSingleEntity(device.getId()); + sef.setSingleEntity(AliasEntityId.fromEntityId(device.getId())); LatestValueCmd latestCmd = new LatestValueCmd(); latestCmd.setKeys(Collections.singletonList(new EntityKey(EntityKeyType.TIME_SERIES, "batteryLevel"))); EntityDataQuery edq = new EntityDataQuery(sef, new EntityDataPageLink(1, 0, null, null), @@ -351,7 +361,7 @@ public abstract class AbstractLwM2MIntegrationTest extends AbstractTransportInte getWsClient().waitForReply(); getWsClient().registerWaitForUpdate(); - this.createNewClient(security, null, false, endpoint, null, queueMode, device.getId().getId().toString()); + this.createNewClient(security, null, false, endpoint, null, queueMode, device.getId().getId().toString(), null); awaitObserveReadAll(1, lwM2MTestClient.getDeviceIdStr()); String msg = getWsClient().waitForUpdate(); @@ -407,7 +417,7 @@ public abstract class AbstractLwM2MIntegrationTest extends AbstractTransportInte Device device = createLwm2mDevice(deviceCredentials, endpoint, deviceProfile.getId()); SingleEntityFilter sef = new SingleEntityFilter(); - sef.setSingleEntity(device.getId()); + sef.setSingleEntity(AliasEntityId.fromEntityId(device.getId())); LatestValueCmd latestCmd = new LatestValueCmd(); String key1 = "pkgname"; String key2 = "pkgversion"; @@ -422,7 +432,7 @@ public abstract class AbstractLwM2MIntegrationTest extends AbstractTransportInte getWsClient().waitForReply(); getWsClient().registerWaitForUpdate(); - this.createNewClient(security, null, false, endpoint, null, true, device.getId().getId().toString()); + this.createNewClient(security, null, false, endpoint, null, true, device.getId().getId().toString(), null); awaitObserveReadAll(cntObserve, lwM2MTestClient.getDeviceIdStr()); String msg = getWsClient().waitForUpdate(); @@ -543,16 +553,17 @@ public abstract class AbstractLwM2MIntegrationTest extends AbstractTransportInte public void createNewClient(Security security, Security securityBs, boolean isRpc, String endpoint, String deviceIdStr) throws Exception { - this.createNewClient(security, securityBs, isRpc, endpoint, null, false, deviceIdStr); + this.createNewClient(security, securityBs, isRpc, endpoint, null, false, deviceIdStr, null); } public void createNewClient(Security security, Security securityBs, boolean isRpc, String endpoint, Integer clientDtlsCidLength, String deviceIdStr) throws Exception { - this.createNewClient(security, securityBs, isRpc, endpoint, clientDtlsCidLength, false, deviceIdStr); + this.createNewClient(security, securityBs, isRpc, endpoint, clientDtlsCidLength, false, deviceIdStr, null); } public void createNewClient(Security security, Security securityBs, boolean isRpc, - String endpoint, Integer clientDtlsCidLength, boolean queueMode, String deviceIdStr) throws Exception { + String endpoint, Integer clientDtlsCidLength, boolean queueMode, + String deviceIdStr, Integer value3_0_9) throws Exception { this.clientDestroy(false); lwM2MTestClient = new LwM2MTestClient(this.executor, endpoint, resources); @@ -560,11 +571,86 @@ public abstract class AbstractLwM2MIntegrationTest extends AbstractTransportInte int clientPort = socket.getLocalPort(); lwM2MTestClient.init(security, securityBs, clientPort, isRpc, this.defaultLwM2mUplinkMsgHandlerTest, this.clientContextTest, - clientDtlsCidLength, queueMode, supportFormatOnly_SenMLJSON_SenMLCBOR); + clientDtlsCidLength, queueMode, supportFormatOnly_SenMLJSON_SenMLCBOR, value3_0_9); } lwM2MTestClient.setDeviceIdStr(deviceIdStr); } + /** + * Test: "/3/0/9" value = 44 (constant); count = 10; send from client to telemetry without observe + * @param security + * @param deviceCredentials + * @param endpoint + * @param queueMode + * @throws Exception + */ + public void testConnectionWithoutObserveWithDataReceivedSingleTelemetry(Security security, + LwM2MDeviceCredentials deviceCredentials, + String endpoint, + boolean queueMode) throws Exception { + Lwm2mDeviceProfileTransportConfiguration transportConfiguration = getTransportConfiguration(TELEMETRY_WITH_ONE_OBSERVE, getBootstrapServerCredentialsNoSec(NONE)); + DeviceProfile deviceProfile = createLwm2mDeviceProfile("profileFor" + endpoint, transportConfiguration); + Device device = createLwm2mDevice(deviceCredentials, endpoint, deviceProfile.getId()); + + + + SingleEntityFilter sef = new SingleEntityFilter(); + sef.setSingleEntity(AliasEntityId.fromEntityId(device.getId())); + LatestValueCmd latestCmd = new LatestValueCmd(); + latestCmd.setKeys(Collections.singletonList(new EntityKey(EntityKeyType.TIME_SERIES, "batteryLevel"))); + EntityDataQuery edq = new EntityDataQuery(sef, new EntityDataPageLink(1, 0, null, null), + Collections.emptyList(), Collections.emptyList(), Collections.emptyList()); + + EntityDataCmd cmd = new EntityDataCmd(1, edq, null, latestCmd, null); + getWsClient().send(cmd); + getWsClient().waitForReply(); + + getWsClient().registerWaitForUpdate(); + + this.createNewClient(security, null, false, endpoint, null, queueMode, device.getId().getId().toString(), 44); + awaitObserveReadAll(1, lwM2MTestClient.getDeviceIdStr()); + + LeshanClient leshanClient = lwM2MTestClient.getLeshanClient(); + Map registeredServers = leshanClient.getRegisteredServers(); + List paths = List.of("/3/0/9"); + int cntUpdate = 10; + int cntLast = cntUpdate; + for (final LwM2mServer server : registeredServers.values()) { + log.info("Sending Data to {} using {}.", server, ContentFormat.SENML_CBOR); + ResponseCallback responseCallback = (response) -> { + if (response.isSuccess()) + log.warn("Data sent successfully to {} [{}].", server, response.getCode()); + else + log.warn("Send data to {} failed [{}] : {}.", server, response.getCode(), + response.getErrorMessage() == null ? "" : response.getErrorMessage()); + }; + ErrorCallback errorCallback = (e) -> log.warn("Unable to send data to {}.", server, e); + while(cntLast > 0) { + leshanClient.getSendService().sendData(server, ContentFormat.SENML_CBOR, paths, + 2000, responseCallback, errorCallback); + cntLast-- ; + } + } + + + verify(defaultUplinkMsgHandlerTest, timeout(10000).atLeast(cntUpdate)) + .updateAttrTelemetry(Mockito.any(ResourceUpdateResult.class), eq(null)); + + String msg = getWsClient().waitForUpdate(); + EntityDataUpdate update = JacksonUtil.fromString(msg, EntityDataUpdate.class); + Assert.assertEquals(1, update.getCmdId()); + List eData = update.getUpdate(); + Assert.assertNotNull(eData); + Assert.assertEquals(1, eData.size()); + Assert.assertEquals(device.getId(), eData.get(0).getEntityId()); + Assert.assertNotNull(eData.get(0).getLatest().get(EntityKeyType.TIME_SERIES)); + var tsValue = eData.get(0).getLatest().get(EntityKeyType.TIME_SERIES).get("batteryLevel"); + assertThat(Long.parseLong(tsValue.getValue()), instanceOf(Long.class)); + int expected = 44; + assertEquals(expected, Long.parseLong(tsValue.getValue())); + } + + private void clientDestroy(boolean isAfter) { try { if (lwM2MTestClient != null && lwM2MTestClient.getLeshanClient() != null) { 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 06e41fe29f..2ae1432cac 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 @@ -144,7 +144,7 @@ public class LwM2MTestClient { public void init(Security security, Security securityBs, int port, boolean isRpc, LwM2mUplinkMsgHandler defaultLwM2mUplinkMsgHandler, LwM2mClientContext clientContext, Integer cIdLength, boolean queueMode, - boolean supportFormatOnly_SenMLJSON_SenMLCBOR) throws InvalidDDFFileException, IOException { + boolean supportFormatOnly_SenMLJSON_SenMLCBOR, Integer value3_0_9) throws InvalidDDFFileException, IOException { Assert.assertNull("client already initialized", leshanClient); this.defaultLwM2mUplinkMsgHandlerTest = defaultLwM2mUplinkMsgHandler; this.clientContext = clientContext; @@ -197,7 +197,7 @@ public class LwM2MTestClient { initializer.setInstancesForObject(SERVER, lwm2mServer); } - initializer.setInstancesForObject(DEVICE, lwM2MDevice = new SimpleLwM2MDevice(executor)); + initializer.setInstancesForObject(DEVICE, lwM2MDevice = new SimpleLwM2MDevice(executor, value3_0_9)); initializer.setInstancesForObject(FIRMWARE, fwLwM2MDevice = new FwLwM2MDevice()); initializer.setInstancesForObject(SOFTWARE_MANAGEMENT, swLwM2MDevice = new SwLwM2MDevice()); initializer.setClassForObject(ACCESS_CONTROL, DummyInstanceEnabler.class); diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/SimpleLwM2MDevice.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/SimpleLwM2MDevice.java index 447cc051fc..5157d5597d 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/SimpleLwM2MDevice.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/SimpleLwM2MDevice.java @@ -85,18 +85,22 @@ public class SimpleLwM2MDevice extends BaseInstanceEnabler implements Destroyabl */ private static Map errorCode = Map.of(0, 0L); // 0-32 + private Integer value3_0_9; public SimpleLwM2MDevice() { } - public SimpleLwM2MDevice(ScheduledExecutorService executorService) { + public SimpleLwM2MDevice(ScheduledExecutorService executorService, Integer value3_0_9) { + this.value3_0_9 = value3_0_9; try { - executorService.scheduleWithFixedDelay(() -> { - fireResourceChange(9); - fireResourceChange(20); - } - , 1, 1, TimeUnit.SECONDS); // 2 sec + if ( this.value3_0_9 == null) { + executorService.scheduleWithFixedDelay(() -> { + fireResourceChange(9); + fireResourceChange(20); + } + , 1, 1, TimeUnit.SECONDS); // 2 sec // , 1800000, 1800000, TimeUnit.MILLISECONDS); // 30 MIN + } } catch (Throwable e) { log.error("[{}]Throwable", e.toString()); e.printStackTrace(); @@ -211,8 +215,14 @@ public class SimpleLwM2MDevice extends BaseInstanceEnabler implements Destroyabl } private int getBatteryLevel() { - int valBattery = randomIterator.nextInt(); - log.trace("Send from client [3/0/9] val: [{}]", valBattery); + int valBattery; + if (this.value3_0_9 == null) { + valBattery = randomIterator.nextInt(); + log.trace("Send from client [3/0/9] val: [{}]", valBattery); + } else { + valBattery = this.value3_0_9; + log.warn("Send from client [3/0/9] constant value: [{}]", valBattery); + } return valBattery; } diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationDataReceivedFromClientTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationDataReceivedFromClientTest.java new file mode 100644 index 0000000000..8cb5e06248 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationDataReceivedFromClientTest.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.rpc.sql; + +import org.junit.Test; +import org.thingsboard.server.common.data.device.credentials.lwm2m.LwM2MDeviceCredentials; +import org.thingsboard.server.transport.lwm2m.security.AbstractSecurityLwM2MIntegrationTest; + +public class RpcLwm2mIntegrationDataReceivedFromClientTest extends AbstractSecurityLwM2MIntegrationTest { + + @Test + public void testWithNoSecConnectLwm2mSuccessAndObserveTelemetry() throws Exception { + String clientEndpoint = CLIENT_ENDPOINT_NO_SEC; + LwM2MDeviceCredentials clientCredentials = getDeviceCredentialsNoSec(createNoSecClientCredentials(clientEndpoint)); + super.testConnectionWithoutObserveWithDataReceivedSingleTelemetry(SECURITY_NO_SEC, clientCredentials, clientEndpoint, false); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/attributes/AbstractMqttAttributesIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/attributes/AbstractMqttAttributesIntegrationTest.java index 40afe76e01..5c70f05b94 100644 --- a/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/attributes/AbstractMqttAttributesIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/attributes/AbstractMqttAttributesIntegrationTest.java @@ -34,6 +34,7 @@ import org.thingsboard.server.common.data.device.profile.ProtoTransportPayloadCo import org.thingsboard.server.common.data.device.profile.TransportPayloadTypeConfiguration; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.query.AliasEntityId; import org.thingsboard.server.common.data.query.DeviceTypeFilter; import org.thingsboard.server.common.data.query.EntityData; import org.thingsboard.server.common.data.query.EntityKey; @@ -339,7 +340,7 @@ public abstract class AbstractMqttAttributesIntegrationTest extends AbstractMqtt MqttTestClient client = new MqttTestClient(); client.connectAndWait(accessToken); SingleEntityFilter dtf = new SingleEntityFilter(); - dtf.setSingleEntity(savedDevice.getId()); + dtf.setSingleEntity(AliasEntityId.fromEntityId(savedDevice.getId())); String clientKeysStr = "clientStr,clientBool,clientDbl,clientLong,clientJson"; String sharedKeysStr = "sharedStr,sharedBool,sharedDbl,sharedLong,sharedJson"; List clientKeysList = List.of(clientKeysStr.split(",")); @@ -426,7 +427,7 @@ public abstract class AbstractMqttAttributesIntegrationTest extends AbstractMqtt }); SingleEntityFilter dtf = new SingleEntityFilter(); - dtf.setSingleEntity(device.getId()); + dtf.setSingleEntity(AliasEntityId.fromEntityId(device.getId())); String sharedKeysStr = "sharedStr,sharedBool,sharedDbl,sharedLong,sharedJson"; List clientKeysList = List.of(clientKeysStr.split(",")); List sharedKeysList = List.of(sharedKeysStr.split(",")); @@ -481,7 +482,7 @@ public abstract class AbstractMqttAttributesIntegrationTest extends AbstractMqtt assertNotNull(device); SingleEntityFilter dtf = new SingleEntityFilter(); - dtf.setSingleEntity(device.getId()); + dtf.setSingleEntity(AliasEntityId.fromEntityId(device.getId())); String sharedKeysStr = "sharedStr,sharedBool,sharedDbl,sharedLong,sharedJson"; List sharedKeysList = List.of(sharedKeysStr.split(",")); List csKeys = getEntityKeys(clientKeysList, CLIENT_ATTRIBUTE); 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..2697b2b804 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/utils/CalculatedFieldUtilsTest.java @@ -0,0 +1,109 @@ +/** + * 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.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.geofencing.GeofencingArgumentEntry; +import org.thingsboard.server.service.cf.ctx.state.geofencing.GeofencingCalculatedFieldState; +import org.thingsboard.server.service.cf.ctx.state.geofencing.GeofencingZoneState; + +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.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(List.of("geofencingArgumentTest")); + state.updateState(mock(CalculatedFieldCtx.class), Map.of("geofencingArgumentTest", geofencingArgumentEntry)); + + // when + CalculatedFieldStateProto proto = toProto(stateId, state); + + // then + CalculatedFieldState fromProto = CalculatedFieldUtils.fromProto(proto); + assertThat(fromProto) + .usingRecursiveComparison() + .ignoringFields("requiredArguments") + .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(); + } + +} diff --git a/common/actor/pom.xml b/common/actor/pom.xml index 5a7c3fd665..92b174ea61 100644 --- a/common/actor/pom.xml +++ b/common/actor/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.0-SNAPSHOT + 4.3.0-SNAPSHOT common org.thingsboard.common diff --git a/common/actor/src/main/java/org/thingsboard/server/actors/TbActorMailbox.java b/common/actor/src/main/java/org/thingsboard/server/actors/TbActorMailbox.java index 6cd28fa98d..c20726d765 100644 --- a/common/actor/src/main/java/org/thingsboard/server/actors/TbActorMailbox.java +++ b/common/actor/src/main/java/org/thingsboard/server/actors/TbActorMailbox.java @@ -19,8 +19,8 @@ import lombok.Getter; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.common.util.RecoveryAware; import org.thingsboard.server.common.msg.MsgType; -import org.thingsboard.server.common.msg.TbActorError; import org.thingsboard.server.common.msg.TbActorMsg; import org.thingsboard.server.common.msg.TbActorStopReason; @@ -35,6 +35,7 @@ import java.util.function.Supplier; @Getter @RequiredArgsConstructor public final class TbActorMailbox implements TbActorCtx { + private static final boolean HIGH_PRIORITY = true; private static final boolean NORMAL_PRIORITY = false; @@ -100,7 +101,7 @@ public final class TbActorMailbox implements TbActorCtx { if (t instanceof TbActorException && t.getCause() != null) { t = t.getCause(); } - return t instanceof TbActorError && ((TbActorError) t).isUnrecoverable(); + return t instanceof RecoveryAware recoveryAware && recoveryAware.isUnrecoverable(); } private void enqueue(TbActorMsg msg, boolean highPriority) { diff --git a/common/cache/pom.xml b/common/cache/pom.xml index 3094e1601f..fcf52e01b7 100644 --- a/common/cache/pom.xml +++ b/common/cache/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.0-SNAPSHOT + 4.3.0-SNAPSHOT common org.thingsboard.common 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/cache/src/main/java/org/thingsboard/server/cache/resourceInfo/ResourceInfoCacheKey.java b/common/cache/src/main/java/org/thingsboard/server/cache/resourceInfo/ResourceInfoCacheKey.java index cb19b36059..e1877ebe4e 100644 --- a/common/cache/src/main/java/org/thingsboard/server/cache/resourceInfo/ResourceInfoCacheKey.java +++ b/common/cache/src/main/java/org/thingsboard/server/cache/resourceInfo/ResourceInfoCacheKey.java @@ -20,7 +20,6 @@ import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.RequiredArgsConstructor; import org.thingsboard.server.common.data.id.TbResourceId; -import org.thingsboard.server.common.data.id.TenantId; import java.io.Serial; import java.io.Serializable; @@ -34,12 +33,11 @@ public class ResourceInfoCacheKey implements Serializable { @Serial private static final long serialVersionUID = 2100510964692846992L; - private final TenantId tenantId; private final TbResourceId tbResourceId; @Override public String toString() { - return tenantId + "_" + tbResourceId; + return tbResourceId.toString(); } } diff --git a/common/cluster-api/pom.xml b/common/cluster-api/pom.xml index a8a419aba3..1f4b5ff41b 100644 --- a/common/cluster-api/pom.xml +++ b/common/cluster-api/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.0-SNAPSHOT + 4.3.0-SNAPSHOT common org.thingsboard.common 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..1805788007 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 @@ -38,7 +38,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 +80,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); 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/TbQueueHandler.java b/common/cluster-api/src/main/java/org/thingsboard/server/queue/TbQueueHandler.java index bb5bc1d668..523a97e13c 100644 --- a/common/cluster-api/src/main/java/org/thingsboard/server/queue/TbQueueHandler.java +++ b/common/cluster-api/src/main/java/org/thingsboard/server/queue/TbQueueHandler.java @@ -17,11 +17,12 @@ package org.thingsboard.server.queue; import com.google.common.util.concurrent.ListenableFuture; -/** - * Created by ashvayka on 05.10.18. - */ public interface TbQueueHandler { ListenableFuture handle(Request request); + default Response constructErrorResponseMsg(Request request, Throwable cause) { + return null; + } + } diff --git a/common/coap-server/pom.xml b/common/coap-server/pom.xml index 64867e9020..27ff645a0f 100644 --- a/common/coap-server/pom.xml +++ b/common/coap-server/pom.xml @@ -22,7 +22,7 @@ 4.0.0 org.thingsboard - 4.2.0-SNAPSHOT + 4.3.0-SNAPSHOT common org.thingsboard.common diff --git a/common/coap-server/src/main/java/org/thingsboard/server/coapserver/TbCoapServerComponent.java b/common/coap-server/src/main/java/org/thingsboard/server/coapserver/TbCoapServerComponent.java index 98df973c65..5093a5025e 100644 --- a/common/coap-server/src/main/java/org/thingsboard/server/coapserver/TbCoapServerComponent.java +++ b/common/coap-server/src/main/java/org/thingsboard/server/coapserver/TbCoapServerComponent.java @@ -17,10 +17,11 @@ package org.thingsboard.server.coapserver; import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; +import java.lang.annotation.Inherited; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +@Inherited @Retention(RetentionPolicy.RUNTIME) @ConditionalOnExpression("'${service.type:null}'=='tb-transport' || ('${service.type:null}'=='monolith' && '${transport.api_enabled:true}'=='true' && '${coap.server.enabled}'=='true')") -public @interface TbCoapServerComponent { -} +public @interface TbCoapServerComponent {} diff --git a/common/coap-server/src/main/java/org/thingsboard/server/coapserver/TbCoapTransportComponent.java b/common/coap-server/src/main/java/org/thingsboard/server/coapserver/TbCoapTransportComponent.java index 558ccf16ef..a68810f10c 100644 --- a/common/coap-server/src/main/java/org/thingsboard/server/coapserver/TbCoapTransportComponent.java +++ b/common/coap-server/src/main/java/org/thingsboard/server/coapserver/TbCoapTransportComponent.java @@ -17,11 +17,12 @@ package org.thingsboard.server.coapserver; import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; +import java.lang.annotation.Inherited; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +@Inherited @Retention(RetentionPolicy.RUNTIME) @ConditionalOnExpression("'${service.type:null}'=='tb-transport' || " + "('${service.type:null}'=='monolith' && '${transport.api_enabled:true}'=='true' && '${coap.server.enabled}'=='true' && '${transport.coap.enabled}'=='true')") -public @interface TbCoapTransportComponent { -} +public @interface TbCoapTransportComponent {} diff --git a/common/dao-api/pom.xml b/common/dao-api/pom.xml index 7f95023057..160351d55d 100644 --- a/common/dao-api/pom.xml +++ b/common/dao-api/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.0-SNAPSHOT + 4.3.0-SNAPSHOT common org.thingsboard.common 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 new file mode 100644 index 0000000000..3ad12048cf --- /dev/null +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/ai/AiModelService.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.dao.ai; + +import com.google.common.util.concurrent.FluentFuture; +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.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.dao.entity.EntityDaoService; + +import java.util.Optional; + +public interface AiModelService extends EntityDaoService { + + AiModel save(AiModel model); + + Optional findAiModelById(TenantId tenantId, AiModelId modelId); + + PageData findAiModelsByTenantId(TenantId tenantId, PageLink pageLink); + + Optional findAiModelByTenantIdAndId(TenantId tenantId, AiModelId modelId); + + FluentFuture> findAiModelByTenantIdAndIdAsync(TenantId tenantId, AiModelId modelId); + + 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 7be61d15d5..e26955d465 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 @@ -16,6 +16,7 @@ package org.thingsboard.server.dao.alarm; import com.fasterxml.jackson.databind.JsonNode; +import com.google.common.util.concurrent.FluentFuture; import com.google.common.util.concurrent.ListenableFuture; import org.thingsboard.server.common.data.EntitySubtype; import org.thingsboard.server.common.data.alarm.Alarm; @@ -105,6 +106,8 @@ public interface AlarmService extends EntityDaoService { Alarm findLatestActiveByOriginatorAndType(TenantId tenantId, EntityId originator, String type); + FluentFuture findLatestActiveByOriginatorAndTypeAsync(TenantId tenantId, EntityId originator, String type); + PageData findAlarmDataByQueryForEntities(TenantId tenantId, AlarmDataQuery query, Collection orderedEntityIds); 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 5101d6d57e..85cd8d24fd 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 @@ -31,8 +31,12 @@ public interface CalculatedFieldService extends EntityDaoService { CalculatedField save(CalculatedField calculatedField); + CalculatedField save(CalculatedField calculatedField, boolean doValidate); + CalculatedField findById(TenantId tenantId, CalculatedFieldId calculatedFieldId); + CalculatedField findByEntityIdAndName(EntityId entityId, String name); + List findCalculatedFieldIdsByEntityId(TenantId tenantId, EntityId entityId); List findCalculatedFieldsByEntityId(TenantId tenantId, EntityId entityId); diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/ota/OtaPackageService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/ota/OtaPackageService.java index 12c8e5053b..b0cdb4d384 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/ota/OtaPackageService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/ota/OtaPackageService.java @@ -41,6 +41,8 @@ public interface OtaPackageService extends EntityDaoService { OtaPackageInfo findOtaPackageInfoById(TenantId tenantId, OtaPackageId otaPackageId); + OtaPackage findOtaPackageByTenantIdAndTitleAndVersion(TenantId tenantId, String title, String version); + ListenableFuture findOtaPackageInfoByIdAsync(TenantId tenantId, OtaPackageId otaPackageId); PageData findTenantOtaPackagesByTenantId(TenantId tenantId, PageLink pageLink); @@ -52,4 +54,5 @@ public interface OtaPackageService extends EntityDaoService { void deleteOtaPackagesByTenantId(TenantId tenantId); long sumDataSizeByTenantId(TenantId tenantId); + } diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/settings/AdminSettingsService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/settings/AdminSettingsService.java index b803700582..5223a2a9c9 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/settings/AdminSettingsService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/settings/AdminSettingsService.java @@ -18,8 +18,9 @@ package org.thingsboard.server.dao.settings; import org.thingsboard.server.common.data.AdminSettings; import org.thingsboard.server.common.data.id.AdminSettingsId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.dao.entity.EntityDaoService; -public interface AdminSettingsService { +public interface AdminSettingsService extends EntityDaoService { AdminSettings findAdminSettingsById(TenantId tenantId, AdminSettingsId adminSettingsId); @@ -31,6 +32,4 @@ public interface AdminSettingsService { boolean deleteAdminSettingsByTenantIdAndKey(TenantId tenantId, String key); - void deleteAdminSettingsByTenantId(TenantId tenantId); - } diff --git a/common/data/pom.xml b/common/data/pom.xml index a27011f890..779565ed8e 100644 --- a/common/data/pom.xml +++ b/common/data/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.0-SNAPSHOT + 4.3.0-SNAPSHOT common org.thingsboard.common @@ -112,6 +112,10 @@ leshan-core compile + + dev.langchain4j + langchain4j-core + diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/BaseData.java b/common/data/src/main/java/org/thingsboard/server/common/data/BaseData.java index 2ac78b04b6..614cf67054 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/BaseData.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/BaseData.java @@ -16,6 +16,7 @@ package org.thingsboard.server.common.data; import com.fasterxml.jackson.databind.ObjectMapper; +import io.swagger.v3.oas.annotations.media.Schema; import org.thingsboard.server.common.data.id.IdBased; import org.thingsboard.server.common.data.id.UUIDBased; @@ -41,6 +42,11 @@ public abstract class BaseData extends IdBased implement this.createdTime = data.getCreatedTime(); } + @Schema( + description = "Entity creation timestamp in milliseconds since Unix epoch", + example = "1746028547220", + accessMode = Schema.AccessMode.READ_ONLY + ) public long getCreatedTime() { return createdTime; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java b/common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java index 5b167c88a2..b55453f393 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java @@ -15,7 +15,10 @@ */ package org.thingsboard.server.common.data; -public class CacheConstants { +public final class CacheConstants { + + private CacheConstants() {} + public static final String DEVICE_CREDENTIALS_CACHE = "deviceCredentials"; public static final String RELATIONS_CACHE = "relations"; public static final String DEVICE_CACHE = "devices"; @@ -36,6 +39,7 @@ public class CacheConstants { public static final String NOTIFICATION_SETTINGS_CACHE = "notificationSettings"; public static final String SENT_NOTIFICATIONS_CACHE = "sentNotifications"; public static final String TRENDZ_SETTINGS_CACHE = "trendzSettings"; + public static final String AI_MODEL_CACHE = "aiModel"; public static final String ASSET_PROFILE_CACHE = "assetProfiles"; public static final String ATTRIBUTES_CACHE = "attributes"; @@ -54,4 +58,5 @@ public class CacheConstants { public static final String ALARM_TYPES_CACHE = "alarmTypes"; public static final String QR_CODE_SETTINGS_CACHE = "qrCodeSettings"; public static final String MOBILE_SECRET_KEY_CACHE = "mobileSecretKey"; + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java b/common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java index b2d9d59cca..8a72b26a28 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java @@ -15,9 +15,6 @@ */ package org.thingsboard.server.common.data; -/** - * @author Andrew Shvayka - */ public class DataConstants { public static final String TENANT = "TENANT"; 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 af5fec1827..110052b57f 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 @@ -22,9 +22,6 @@ import java.util.Arrays; import java.util.EnumSet; import java.util.List; -/** - * @author Andrew Shvayka - */ public enum EntityType { TENANT(1), CUSTOMER(2), @@ -65,7 +62,14 @@ public enum EntityType { MOBILE_APP_BUNDLE(38), CALCULATED_FIELD(39), CALCULATED_FIELD_LINK(40), - JOB(41); + JOB(41), + ADMIN_SETTINGS(42), + AI_MODEL(43, "ai_model") { + @Override + public String getNormalName() { + return "AI model"; + } + }; @Getter private final int protoNumber; // Corresponds to EntityTypeProto diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/OtaPackage.java b/common/data/src/main/java/org/thingsboard/server/common/data/OtaPackage.java index 26376bbda3..abcccdfa9a 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/OtaPackage.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/OtaPackage.java @@ -20,6 +20,7 @@ import lombok.Data; import lombok.EqualsAndHashCode; import org.thingsboard.server.common.data.id.OtaPackageId; +import java.io.Serial; import java.nio.ByteBuffer; @Schema @@ -27,6 +28,7 @@ import java.nio.ByteBuffer; @EqualsAndHashCode(callSuper = true) public class OtaPackage extends OtaPackageInfo { + @Serial private static final long serialVersionUID = 3091601761339422546L; @Schema(description = "OTA Package data.", accessMode = Schema.AccessMode.READ_ONLY) @@ -44,4 +46,10 @@ public class OtaPackage extends OtaPackageInfo { super(otaPackage); this.data = otaPackage.getData(); } + + public OtaPackage(OtaPackageInfo otaPackageInfo) { + super(otaPackageInfo); + this.data = null; + } + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/OtaPackageInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/OtaPackageInfo.java index dfdbd83e80..9f010db823 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/OtaPackageInfo.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/OtaPackageInfo.java @@ -16,6 +16,7 @@ package org.thingsboard.server.common.data; import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.JsonNode; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; @@ -29,12 +30,15 @@ import org.thingsboard.server.common.data.ota.OtaPackageType; import org.thingsboard.server.common.data.validation.Length; import org.thingsboard.server.common.data.validation.NoXss; +import java.io.Serial; + @Schema @Slf4j @Data @EqualsAndHashCode(callSuper = true) -public class OtaPackageInfo extends BaseDataWithAdditionalInfo implements HasName, HasTenantId, HasTitle { +public class OtaPackageInfo extends BaseDataWithAdditionalInfo implements HasName, HasTenantId, HasTitle, ExportableEntity { + @Serial private static final long serialVersionUID = 3168391583570815419L; @Schema(description = "JSON object with Tenant Id. Tenant Id of the ota package can't be changed.", accessMode = Schema.AccessMode.READ_ONLY) @@ -77,6 +81,8 @@ public class OtaPackageInfo extends BaseDataWithAdditionalInfo imp @Schema(description = "OTA Package data size.", example = "8", accessMode = Schema.AccessMode.READ_ONLY) private Long dataSize; + private OtaPackageId externalId; + public OtaPackageInfo() { super(); } @@ -100,6 +106,7 @@ public class OtaPackageInfo extends BaseDataWithAdditionalInfo imp this.checksumAlgorithm = otaPackageInfo.getChecksumAlgorithm(); this.checksum = otaPackageInfo.getChecksum(); this.dataSize = otaPackageInfo.getDataSize(); + this.externalId = otaPackageInfo.getExternalId(); } @Schema(description = "JSON object with the ota package Id. " + @@ -118,7 +125,7 @@ public class OtaPackageInfo extends BaseDataWithAdditionalInfo imp } @Override - @JsonIgnore + @JsonProperty(access = JsonProperty.Access.READ_ONLY) public String getName() { return title; } @@ -133,4 +140,5 @@ public class OtaPackageInfo extends BaseDataWithAdditionalInfo imp public JsonNode getAdditionalInfo() { return super.getAdditionalInfo(); } + } 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..f83a812529 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,7 @@ public class SystemParams { String calculatedFieldDebugPerTenantLimitsConfiguration; long maxArgumentsPerCF; long maxDataPointsPerRollingArg; + int minAllowedScheduledUpdateIntervalInSecForCF; + int maxRelationLevelPerCfArgument; 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 498fa5be3e..ba37067106 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 @@ -23,6 +23,7 @@ import lombok.EqualsAndHashCode; import lombok.extern.slf4j.Slf4j; import org.thingsboard.server.common.data.id.TbResourceId; +import java.io.Serial; import java.util.Base64; import java.util.Optional; @@ -31,6 +32,7 @@ import java.util.Optional; @EqualsAndHashCode(callSuper = true) public class TbResource extends TbResourceInfo { + @Serial private static final long serialVersionUID = 7379609705527272306L; private byte[] data; @@ -88,4 +90,5 @@ public class TbResource extends TbResourceInfo { public String toString() { return super.toString(); } + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/AiModel.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/AiModel.java new file mode 100644 index 0000000000..4d7bb21930 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/AiModel.java @@ -0,0 +1,102 @@ +/** + * 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; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.thingsboard.server.common.data.BaseData; +import org.thingsboard.server.common.data.ExportableEntity; +import org.thingsboard.server.common.data.HasTenantId; +import org.thingsboard.server.common.data.HasVersion; +import org.thingsboard.server.common.data.ai.model.AiModelConfig; +import org.thingsboard.server.common.data.id.AiModelId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.validation.Length; +import org.thingsboard.server.common.data.validation.NoNullChar; + +import java.io.Serial; + +@Data +@Builder +@AllArgsConstructor +@EqualsAndHashCode(callSuper = true) +public final class AiModel extends BaseData implements HasTenantId, HasVersion, ExportableEntity { + + @Serial + private static final long serialVersionUID = 9017108678716011604L; + + @Schema( + requiredMode = Schema.RequiredMode.REQUIRED, + accessMode = Schema.AccessMode.READ_ONLY, + description = "JSON object representing the ID of the tenant associated with this AI model", + example = "e3c4b7d2-5678-4a9b-0c1d-2e3f4a5b6c7d" + ) + private TenantId tenantId; + + @Schema( + requiredMode = Schema.RequiredMode.REQUIRED, + accessMode = Schema.AccessMode.READ_ONLY, + description = "Version of the AI model record; increments automatically whenever the record is changed", + example = "7", + defaultValue = "1" + ) + private Long version; + + @NotBlank + @NoNullChar + @Length(min = 1, max = 255) + @Schema( + requiredMode = Schema.RequiredMode.REQUIRED, + accessMode = Schema.AccessMode.READ_WRITE, + description = "Display name for this AI model configuration; not the technical model identifier", + example = "Fast and cost-efficient model" + ) + private String name; + + @NotNull + @Valid + @Schema( + requiredMode = Schema.RequiredMode.NOT_REQUIRED, + accessMode = Schema.AccessMode.READ_WRITE, + description = "Configuration of the AI model" + ) + private AiModelConfig configuration; + + private AiModelId externalId; + + public AiModel() {} + + public AiModel(AiModelId id) { + super(id); + } + + public AiModel(AiModel model) { + super(model.getId()); + createdTime = model.getCreatedTime(); + tenantId = model.getTenantId(); + version = model.getVersion(); + name = model.getName(); + configuration = model.getConfiguration(); + externalId = model.getExternalId() == null ? null : new AiModelId(model.getExternalId().getId()); + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/dto/TbChatRequest.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/dto/TbChatRequest.java new file mode 100644 index 0000000000..7e43520b79 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/dto/TbChatRequest.java @@ -0,0 +1,79 @@ +/** + * 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.dto; + +import dev.langchain4j.data.message.ChatMessage; +import dev.langchain4j.data.message.Content; +import dev.langchain4j.data.message.SystemMessage; +import dev.langchain4j.data.message.UserMessage; +import dev.langchain4j.model.chat.request.ChatRequest; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; +import org.thingsboard.server.common.data.ai.model.chat.AiChatModelConfig; + +import java.util.ArrayList; +import java.util.List; + +public record TbChatRequest( + @Schema( + requiredMode = Schema.RequiredMode.NOT_REQUIRED, + accessMode = Schema.AccessMode.READ_WRITE, + description = "A system-level instruction that frames the user's input, setting the persona, tone, and constraints for the generated response", + example = "You are a helpful assistant. Only output valid JSON." + ) + String systemMessage, + + @Schema( + requiredMode = Schema.RequiredMode.REQUIRED, + accessMode = Schema.AccessMode.READ_WRITE, + description = "The actual user prompt that will be answered by the AI model" + ) + @NotNull @Valid + TbUserMessage userMessage, + + @Schema( + requiredMode = Schema.RequiredMode.REQUIRED, + accessMode = Schema.AccessMode.READ_WRITE, + description = "Configuration of the AI chat model that should execute the request" + ) + @NotNull @Valid + AiChatModelConfig chatModelConfig +) { + + public ChatRequest toLangChainChatRequest() { + return ChatRequest.builder() + .messages(getLangChainMessages()) + .build(); + } + + private List getLangChainMessages() { + List messages = new ArrayList<>(2); + + if (systemMessage != null) { + messages.add(SystemMessage.from(systemMessage)); + } + + List langChainContents = userMessage.contents().stream() + .map(TbContent::toLangChainContent) + .toList(); + + messages.add(UserMessage.from(langChainContents)); + + return messages; + } + +} 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 new file mode 100644 index 0000000000..2cc17e4553 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/dto/TbChatResponse.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.ai.dto; + +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import io.swagger.v3.oas.annotations.media.Schema; + +@JsonTypeInfo( + use = JsonTypeInfo.Id.NAME, + property = "status", + include = JsonTypeInfo.As.PROPERTY, + visible = true +) +@JsonSubTypes({ + @JsonSubTypes.Type(value = TbChatResponse.Success.class, name = "SUCCESS"), + @JsonSubTypes.Type(value = TbChatResponse.Failure.class, name = "FAILURE") +}) +public sealed interface TbChatResponse permits TbChatResponse.Success, TbChatResponse.Failure { + + @Schema( + description = "Indicates whether the request was successful or not", + example = "SUCCESS" + ) + String getStatus(); + + record Success( + @Schema(description = "The text content generated by the model") + String generatedContent + ) implements TbChatResponse { + + @Override + @Schema(example = "SUCCESS") + public String getStatus() { + return "SUCCESS"; + } + + } + + record Failure( + @Schema( + description = "A string containing details about the failure" + ) + String errorDetails + ) implements TbChatResponse { + + @Override + @Schema(example = "FAILURE") + public String getStatus() { + return "FAILURE"; + } + + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/dto/TbContent.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/dto/TbContent.java new file mode 100644 index 0000000000..23543121a4 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/dto/TbContent.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.common.data.ai.dto; + +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import dev.langchain4j.data.message.Content; +import dev.langchain4j.data.message.TextContent; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; + +import static org.thingsboard.server.common.data.ai.dto.TbContent.TbTextContent; + +@JsonTypeInfo( + use = JsonTypeInfo.Id.NAME, + include = JsonTypeInfo.As.PROPERTY, + property = "contentType", + visible = true +) +@JsonSubTypes({ + @JsonSubTypes.Type(value = TbTextContent.class, name = "TEXT") +}) +public sealed interface TbContent permits TbTextContent { + + TbContentType contentType(); + + Content toLangChainContent(); + + enum TbContentType { + + TEXT + + } + + @Schema( + description = "Text-based content part of a user's prompt" + ) + record TbTextContent( + @NotBlank + @Schema( + requiredMode = Schema.RequiredMode.REQUIRED, + description = "The text content", + example = "What is the weather like in Kyiv today?" + ) + String text + ) implements TbContent { + + @Override + public TbContentType contentType() { + return TbContentType.TEXT; + } + + @Override + public Content toLangChainContent() { + return TextContent.from(text); + } + + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/dto/TbUserMessage.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/dto/TbUserMessage.java new file mode 100644 index 0000000000..fdd7d8dc63 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/dto/TbUserMessage.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.ai.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotEmpty; + +import java.util.List; + +public record TbUserMessage( + @NotEmpty + @Valid + @Schema( + requiredMode = Schema.RequiredMode.REQUIRED, + description = "A list of content parts that make up the complete user prompt" + ) + List contents +) {} 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 new file mode 100644 index 0000000000..bfaa29a6e3 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/AiModelConfig.java @@ -0,0 +1,78 @@ +/** + * 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; + +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import org.thingsboard.server.common.data.ai.model.chat.AmazonBedrockChatModelConfig; +import org.thingsboard.server.common.data.ai.model.chat.AnthropicChatModelConfig; +import org.thingsboard.server.common.data.ai.model.chat.AzureOpenAiChatModelConfig; +import org.thingsboard.server.common.data.ai.model.chat.GitHubModelsChatModelConfig; +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.OpenAiChatModelConfig; +import org.thingsboard.server.common.data.ai.provider.AiProvider; +import org.thingsboard.server.common.data.ai.provider.AiProviderConfig; +import org.thingsboard.server.common.data.ai.provider.AmazonBedrockProviderConfig; +import org.thingsboard.server.common.data.ai.provider.AnthropicProviderConfig; +import org.thingsboard.server.common.data.ai.provider.AzureOpenAiProviderConfig; +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.OpenAiProviderConfig; + +@JsonTypeInfo( + use = JsonTypeInfo.Id.NAME, + include = JsonTypeInfo.As.EXISTING_PROPERTY, + property = "provider", + visible = true +) +@JsonSubTypes({ + @JsonSubTypes.Type(value = OpenAiChatModelConfig.class, name = "OPENAI"), + @JsonSubTypes.Type(value = AzureOpenAiChatModelConfig.class, name = "AZURE_OPENAI"), + @JsonSubTypes.Type(value = GoogleAiGeminiChatModelConfig.class, name = "GOOGLE_AI_GEMINI"), + @JsonSubTypes.Type(value = GoogleVertexAiGeminiChatModelConfig.class, name = "GOOGLE_VERTEX_AI_GEMINI"), + @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") +}) +public interface AiModelConfig { + + AiProvider provider(); + + @JsonTypeInfo( + use = JsonTypeInfo.Id.NAME, + include = JsonTypeInfo.As.EXTERNAL_PROPERTY, + property = "provider" + ) + @JsonSubTypes({ + @JsonSubTypes.Type(value = OpenAiProviderConfig.class, name = "OPENAI"), + @JsonSubTypes.Type(value = AzureOpenAiProviderConfig.class, name = "AZURE_OPENAI"), + @JsonSubTypes.Type(value = GoogleAiGeminiProviderConfig.class, name = "GOOGLE_AI_GEMINI"), + @JsonSubTypes.Type(value = GoogleVertexAiGeminiProviderConfig.class, name = "GOOGLE_VERTEX_AI_GEMINI"), + @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") + }) + AiProviderConfig providerConfig(); + + AiModelType modelType(); + +} diff --git a/application/src/main/java/org/thingsboard/server/service/cf/CalculatedFieldInitService.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/AiModelType.java similarity index 87% rename from application/src/main/java/org/thingsboard/server/service/cf/CalculatedFieldInitService.java rename to common/data/src/main/java/org/thingsboard/server/common/data/ai/model/AiModelType.java index 6505dae581..d6299cf5e6 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/CalculatedFieldInitService.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/AiModelType.java @@ -13,7 +13,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.service.cf; +package org.thingsboard.server.common.data.ai.model; + +public enum AiModelType { + + CHAT -public interface CalculatedFieldInitService { } 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 new file mode 100644 index 0000000000..2bc28cfce0 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AiChatModelConfig.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.ai.model.chat; + +import com.fasterxml.jackson.annotation.JsonProperty; +import dev.langchain4j.model.chat.ChatModel; +import org.thingsboard.server.common.data.ai.model.AiModelConfig; +import org.thingsboard.server.common.data.ai.model.AiModelType; + +public sealed interface AiChatModelConfig> extends AiModelConfig + permits + OpenAiChatModelConfig, AzureOpenAiChatModelConfig, GoogleAiGeminiChatModelConfig, + GoogleVertexAiGeminiChatModelConfig, MistralAiChatModelConfig, AnthropicChatModelConfig, + AmazonBedrockChatModelConfig, GitHubModelsChatModelConfig { + + ChatModel configure(Langchain4jChatModelConfigurer configurer); + + @Override + @JsonProperty(value = "modelType", access = JsonProperty.Access.READ_ONLY) + default AiModelType modelType() { + return AiModelType.CHAT; + } + + Integer timeoutSeconds(); + + Integer maxRetries(); + + C withTimeoutSeconds(Integer timeoutSeconds); + + C withMaxRetries(Integer maxRetries); + + boolean supportsJsonMode(); + +} 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 new file mode 100644 index 0000000000..2bb4de5aa8 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AmazonBedrockChatModelConfig.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.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.AmazonBedrockProviderConfig; + +@Builder +public record AmazonBedrockChatModelConfig( + @NotNull @Valid AmazonBedrockProviderConfig providerConfig, + @NotBlank String modelId, + @PositiveOrZero Double temperature, + @Positive @Max(1) Double topP, + @Positive Integer maxOutputTokens, + @With @Positive Integer timeoutSeconds, + @With @PositiveOrZero Integer maxRetries +) implements AiChatModelConfig { + + @Override + public AiProvider provider() { + return AiProvider.AMAZON_BEDROCK; + } + + @Override + public ChatModel configure(Langchain4jChatModelConfigurer configurer) { + return configurer.configureChatModel(this); + } + + @Override + public boolean supportsJsonMode() { + return false; + } + +} 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 new file mode 100644 index 0000000000..69b5578fb3 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AnthropicChatModelConfig.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.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.AnthropicProviderConfig; + +@Builder +public record AnthropicChatModelConfig( + @NotNull @Valid AnthropicProviderConfig providerConfig, + @NotBlank String modelId, + @PositiveOrZero Double temperature, + @Positive @Max(1) Double topP, + @PositiveOrZero Integer topK, + @Positive Integer maxOutputTokens, + @With @Positive Integer timeoutSeconds, + @With @PositiveOrZero Integer maxRetries +) implements AiChatModelConfig { + + @Override + public AiProvider provider() { + return AiProvider.ANTHROPIC; + } + + @Override + public ChatModel configure(Langchain4jChatModelConfigurer configurer) { + return configurer.configureChatModel(this); + } + + @Override + public boolean supportsJsonMode() { + return false; + } + +} 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 new file mode 100644 index 0000000000..47e7e96c37 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AzureOpenAiChatModelConfig.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.AzureOpenAiProviderConfig; + +@Builder +public record AzureOpenAiChatModelConfig( + @NotNull @Valid AzureOpenAiProviderConfig providerConfig, + @NotBlank String modelId, + @PositiveOrZero Double temperature, + @Positive @Max(1) Double topP, + Double frequencyPenalty, + Double presencePenalty, + @Positive Integer maxOutputTokens, + @With @Positive Integer timeoutSeconds, + @With @PositiveOrZero Integer maxRetries +) implements AiChatModelConfig { + + @Override + public AiProvider provider() { + return AiProvider.AZURE_OPENAI; + } + + @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/GitHubModelsChatModelConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GitHubModelsChatModelConfig.java new file mode 100644 index 0000000000..b509254f77 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GitHubModelsChatModelConfig.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.GitHubModelsProviderConfig; + +@Builder +public record GitHubModelsChatModelConfig( + @NotNull @Valid GitHubModelsProviderConfig providerConfig, + @NotBlank String modelId, + @PositiveOrZero Double temperature, + @Positive @Max(1) Double topP, + Double frequencyPenalty, + Double presencePenalty, + @Positive Integer maxOutputTokens, + @With @Positive Integer timeoutSeconds, + @With @PositiveOrZero Integer maxRetries +) implements AiChatModelConfig { + + @Override + public AiProvider provider() { + return AiProvider.GITHUB_MODELS; + } + + @Override + public ChatModel configure(Langchain4jChatModelConfigurer configurer) { + return configurer.configureChatModel(this); + } + + @Override + public boolean supportsJsonMode() { + return false; + } + +} 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 new file mode 100644 index 0000000000..fe11a11460 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleAiGeminiChatModelConfig.java @@ -0,0 +1,59 @@ +/** + * 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.GoogleAiGeminiProviderConfig; + +@Builder +public record GoogleAiGeminiChatModelConfig( + @NotNull @Valid GoogleAiGeminiProviderConfig providerConfig, + @NotBlank String modelId, + @PositiveOrZero Double temperature, + @Positive @Max(1) Double topP, + @PositiveOrZero Integer topK, + Double frequencyPenalty, + Double presencePenalty, + @Positive Integer maxOutputTokens, + @With @Positive Integer timeoutSeconds, + @With @PositiveOrZero Integer maxRetries +) implements AiChatModelConfig { + + @Override + public AiProvider provider() { + return AiProvider.GOOGLE_AI_GEMINI; + } + + @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/GoogleVertexAiGeminiChatModelConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleVertexAiGeminiChatModelConfig.java new file mode 100644 index 0000000000..609e14f86e --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleVertexAiGeminiChatModelConfig.java @@ -0,0 +1,59 @@ +/** + * 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.GoogleVertexAiGeminiProviderConfig; + +@Builder +public record GoogleVertexAiGeminiChatModelConfig( + @NotNull @Valid GoogleVertexAiGeminiProviderConfig providerConfig, + @NotBlank String modelId, + @PositiveOrZero Double temperature, + @Positive @Max(1) Double topP, + @PositiveOrZero Integer topK, + Double frequencyPenalty, + Double presencePenalty, + @Positive Integer maxOutputTokens, + @With @Positive Integer timeoutSeconds, + @With @PositiveOrZero Integer maxRetries +) implements AiChatModelConfig { + + @Override + public AiProvider provider() { + return AiProvider.GOOGLE_VERTEX_AI_GEMINI; + } + + @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/Langchain4jChatModelConfigurer.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/Langchain4jChatModelConfigurer.java new file mode 100644 index 0000000000..c9c1bc3173 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/Langchain4jChatModelConfigurer.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.ai.model.chat; + +import dev.langchain4j.model.chat.ChatModel; + +public interface Langchain4jChatModelConfigurer { + + ChatModel configureChatModel(OpenAiChatModelConfig chatModelConfig); + + ChatModel configureChatModel(AzureOpenAiChatModelConfig chatModelConfig); + + ChatModel configureChatModel(GoogleAiGeminiChatModelConfig chatModelConfig); + + ChatModel configureChatModel(GoogleVertexAiGeminiChatModelConfig chatModelConfig); + + ChatModel configureChatModel(MistralAiChatModelConfig chatModelConfig); + + ChatModel configureChatModel(AnthropicChatModelConfig chatModelConfig); + + ChatModel configureChatModel(AmazonBedrockChatModelConfig chatModelConfig); + + ChatModel configureChatModel(GitHubModelsChatModelConfig 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 new file mode 100644 index 0000000000..f603e99c53 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/MistralAiChatModelConfig.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.MistralAiProviderConfig; + +@Builder +public record MistralAiChatModelConfig( + @NotNull @Valid MistralAiProviderConfig providerConfig, + @NotBlank String modelId, + @PositiveOrZero Double temperature, + @Positive @Max(1) Double topP, + Double frequencyPenalty, + Double presencePenalty, + @Positive Integer maxOutputTokens, + @With @Positive Integer timeoutSeconds, + @With @PositiveOrZero Integer maxRetries +) implements AiChatModelConfig { + + @Override + public AiProvider provider() { + return AiProvider.MISTRAL_AI; + } + + @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 new file mode 100644 index 0000000000..00b5115d7d --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/OpenAiChatModelConfig.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.OpenAiProviderConfig; + +@Builder +public record OpenAiChatModelConfig( + @NotNull @Valid OpenAiProviderConfig providerConfig, + @NotBlank String modelId, + @PositiveOrZero Double temperature, + @Positive @Max(1) Double topP, + Double frequencyPenalty, + Double presencePenalty, + @Positive Integer maxOutputTokens, + @With @Positive Integer timeoutSeconds, + @With @PositiveOrZero Integer maxRetries +) implements AiChatModelConfig { + + @Override + public AiProvider provider() { + return AiProvider.OPENAI; + } + + @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/provider/AiProvider.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/AiProvider.java new file mode 100644 index 0000000000..d0a5bd0510 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/AiProvider.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.ai.provider; + +public enum AiProvider { + + OPENAI, + AZURE_OPENAI, + GOOGLE_AI_GEMINI, + GOOGLE_VERTEX_AI_GEMINI, + MISTRAL_AI, + ANTHROPIC, + AMAZON_BEDROCK, + GITHUB_MODELS + +} 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 new file mode 100644 index 0000000000..bd32c88efb --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/AiProviderConfig.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.ai.provider; + +public sealed interface AiProviderConfig + permits + OpenAiProviderConfig, AzureOpenAiProviderConfig, GoogleAiGeminiProviderConfig, + GoogleVertexAiGeminiProviderConfig, MistralAiProviderConfig, AnthropicProviderConfig, + AmazonBedrockProviderConfig, GitHubModelsProviderConfig {} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/AmazonBedrockProviderConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/AmazonBedrockProviderConfig.java new file mode 100644 index 0000000000..e705b545c2 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/AmazonBedrockProviderConfig.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.ai.provider; + +import jakarta.validation.constraints.NotNull; + +public record AmazonBedrockProviderConfig( + @NotNull String region, + @NotNull String accessKeyId, + @NotNull String secretAccessKey +) implements AiProviderConfig {} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/AnthropicProviderConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/AnthropicProviderConfig.java new file mode 100644 index 0000000000..d6db07941b --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/AnthropicProviderConfig.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.ai.provider; + +import jakarta.validation.constraints.NotNull; + +public record AnthropicProviderConfig( + @NotNull String apiKey +) implements AiProviderConfig {} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/AzureOpenAiProviderConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/AzureOpenAiProviderConfig.java new file mode 100644 index 0000000000..ea7ffebe3a --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/AzureOpenAiProviderConfig.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.ai.provider; + +import jakarta.validation.constraints.NotNull; + +public record AzureOpenAiProviderConfig( + @NotNull String endpoint, + String serviceVersion, + @NotNull String apiKey +) implements AiProviderConfig {} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/GitHubModelsProviderConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/GitHubModelsProviderConfig.java new file mode 100644 index 0000000000..f5240fe836 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/GitHubModelsProviderConfig.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.ai.provider; + +import jakarta.validation.constraints.NotNull; + +public record GitHubModelsProviderConfig( + @NotNull String personalAccessToken +) implements AiProviderConfig {} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/GoogleAiGeminiProviderConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/GoogleAiGeminiProviderConfig.java new file mode 100644 index 0000000000..bfa729e66d --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/GoogleAiGeminiProviderConfig.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.ai.provider; + +import jakarta.validation.constraints.NotNull; + +public record GoogleAiGeminiProviderConfig( + @NotNull String apiKey +) implements AiProviderConfig {} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/GoogleVertexAiGeminiProviderConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/GoogleVertexAiGeminiProviderConfig.java new file mode 100644 index 0000000000..b0efac764c --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/GoogleVertexAiGeminiProviderConfig.java @@ -0,0 +1,26 @@ +/** + * 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 jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; + +public record GoogleVertexAiGeminiProviderConfig( + @NotBlank String fileName, // not used on BE, but needed for UI + @NotNull String projectId, + @NotNull String location, + @NotNull String serviceAccountKey +) implements AiProviderConfig {} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/MistralAiProviderConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/MistralAiProviderConfig.java new file mode 100644 index 0000000000..eb62557a15 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/MistralAiProviderConfig.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.ai.provider; + +import jakarta.validation.constraints.NotNull; + +public record MistralAiProviderConfig( + @NotNull String apiKey +) implements AiProviderConfig {} 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 new file mode 100644 index 0000000000..09ffda837b --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/OpenAiProviderConfig.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.ai.provider; + +import jakarta.validation.constraints.NotNull; + +public record OpenAiProviderConfig( + @NotNull String apiKey +) implements AiProviderConfig {} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/Alarm.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/Alarm.java index a48a865fe4..519b30a356 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/Alarm.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/Alarm.java @@ -37,13 +37,11 @@ import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.validation.Length; import org.thingsboard.server.common.data.validation.NoXss; +import java.io.Serial; import java.util.List; import java.util.Optional; import java.util.UUID; -/** - * Created by ashvayka on 11.05.17. - */ @Schema @Data @EqualsAndHashCode(callSuper = true) @@ -52,6 +50,9 @@ import java.util.UUID; @JsonIgnoreProperties(ignoreUnknown = true) public class Alarm extends BaseData implements HasName, HasTenantId, HasCustomerId { + @Serial + private static final long serialVersionUID = -1935800187424953611L; + @Schema(description = "JSON object with Tenant Id", accessMode = Schema.AccessMode.READ_ONLY) private TenantId tenantId; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmComment.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmComment.java index 972887e702..0654ba5a9d 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmComment.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmComment.java @@ -30,11 +30,17 @@ import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.validation.Length; import org.thingsboard.server.common.data.validation.NoXss; +import java.io.Serial; + @Schema @Data @Builder @AllArgsConstructor public class AlarmComment extends BaseData implements HasName { + + @Serial + private static final long serialVersionUID = -5454905526404017592L; + @Schema(description = "JSON object with Alarm id.", accessMode = Schema.AccessMode.READ_ONLY) private AlarmId alarmId; @Schema(description = "JSON object with User id.", accessMode = Schema.AccessMode.READ_ONLY) @@ -85,4 +91,5 @@ public class AlarmComment extends BaseData implements HasName { this.comment = alarmComment.getComment(); this.userId = alarmComment.getUserId(); } + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmQuery.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmQuery.java index 83df975ad4..b2d01c8f52 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmQuery.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmQuery.java @@ -22,9 +22,6 @@ import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.page.TimePageLink; -/** - * Created by ashvayka on 11.05.17. - */ @Data @Builder @AllArgsConstructor diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmSeverity.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmSeverity.java index 18e989c1bd..1eaa2ea393 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmSeverity.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmSeverity.java @@ -15,9 +15,6 @@ */ package org.thingsboard.server.common.data.alarm; -/** - * Created by ashvayka on 11.05.17. - */ public enum AlarmSeverity { CRITICAL, MAJOR, MINOR, WARNING, INDETERMINATE; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmStatus.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmStatus.java index 9a69ae8e23..c97b0d24f2 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmStatus.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmStatus.java @@ -15,9 +15,6 @@ */ package org.thingsboard.server.common.data.alarm; -/** - * Created by ashvayka on 11.05.17. - */ public enum AlarmStatus { ACTIVE_UNACK, ACTIVE_ACK, CLEARED_UNACK, CLEARED_ACK; 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..d4dd2c5812 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 @@ -17,6 +17,6 @@ package org.thingsboard.server.common.data.cf; public enum CalculatedFieldType { - SIMPLE, SCRIPT + SIMPLE, SCRIPT, GEOFENCING } 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..52935c3411 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,15 @@ 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; + } + } 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..225278e776 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/ArgumentsBasedCalculatedFieldConfiguration.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; + +import java.util.Map; + +public interface ArgumentsBasedCalculatedFieldConfiguration extends CalculatedFieldConfiguration { + + Map getArguments(); + +} 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..c270874605 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 @@ -27,7 +27,7 @@ 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; @@ -42,20 +42,13 @@ public abstract class BaseCalculatedFieldConfiguration implements CalculatedFiel } @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; + public void validate() { + if (arguments.containsKey("ctx")) { + throw new IllegalArgumentException("Argument name 'ctx' is reserved and cannot be used."); + } + if (arguments.values().stream().anyMatch(Argument::hasDynamicSource)) { + throw new IllegalArgumentException("Calculated field with type: '" + getType() + "' doesn't support dynamic source configuration!"); + } } } diff --git a/ui-ngx/src/app/shared/components/string-items-list.component.scss b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/CFArgumentDynamicSourceType.java similarity index 80% rename from ui-ngx/src/app/shared/components/string-items-list.component.scss rename to common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/CFArgumentDynamicSourceType.java index 726344037c..bd2e9b0c00 100644 --- a/ui-ngx/src/app/shared/components/string-items-list.component.scss +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/CFArgumentDynamicSourceType.java @@ -13,10 +13,10 @@ * 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; - } - } +package org.thingsboard.server.common.data.cf.configuration; + +public enum CFArgumentDynamicSourceType { + + RELATION_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..972a3e0ee9 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 @@ -21,12 +21,14 @@ import com.fasterxml.jackson.annotation.JsonSubTypes; 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.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.stream.Collectors; @JsonTypeInfo( use = JsonTypeInfo.Id.NAME, @@ -35,7 +37,8 @@ import java.util.Map; ) @JsonSubTypes({ @JsonSubTypes.Type(value = SimpleCalculatedFieldConfiguration.class, name = "SIMPLE"), - @JsonSubTypes.Type(value = ScriptCalculatedFieldConfiguration.class, name = "SCRIPT") + @JsonSubTypes.Type(value = ScriptCalculatedFieldConfiguration.class, name = "SCRIPT"), + @JsonSubTypes.Type(value = GeofencingCalculatedFieldConfiguration.class, name = "GEOFENCING") }) @JsonIgnoreProperties(ignoreUnknown = true) public interface CalculatedFieldConfiguration { @@ -43,19 +46,28 @@ public interface CalculatedFieldConfiguration { @JsonIgnore CalculatedFieldType getType(); - Map getArguments(); - - String getExpression(); - - void setExpression(String expression); - Output getOutput(); + void validate(); + @JsonIgnore - List getReferencedEntities(); + default List getReferencedEntities() { + return Collections.emptyList(); + } - List buildCalculatedFieldLinks(TenantId tenantId, EntityId cfEntityId, CalculatedFieldId calculatedFieldId); + default CalculatedFieldLink buildCalculatedFieldLink(TenantId tenantId, EntityId referencedEntityId, CalculatedFieldId calculatedFieldId) { + CalculatedFieldLink link = new CalculatedFieldLink(); + link.setTenantId(tenantId); + link.setEntityId(referencedEntityId); + link.setCalculatedFieldId(calculatedFieldId); + return link; + } - CalculatedFieldLink buildCalculatedFieldLink(TenantId tenantId, EntityId referencedEntityId, 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()); + } } 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..f36071615e --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/CfArgumentDynamicSourceConfiguration.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.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 = RelationQueryDynamicSourceConfiguration.class, name = "RELATION_QUERY") +}) +@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/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/RelationQueryDynamicSourceConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/RelationQueryDynamicSourceConfiguration.java new file mode 100644 index 0000000000..4e9b4252c9 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/RelationQueryDynamicSourceConfiguration.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.common.data.cf.configuration; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import lombok.Data; +import org.thingsboard.server.common.data.StringUtils; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.relation.EntityRelation; +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.RelationsSearchParameters; + +import java.util.Collections; +import java.util.List; + +@Data +public class RelationQueryDynamicSourceConfiguration implements CfArgumentDynamicSourceConfiguration { + + private int maxLevel; + private boolean fetchLastLevelOnly; + private EntitySearchDirection direction; + private String relationType; + + @Override + public CFArgumentDynamicSourceType getType() { + return CFArgumentDynamicSourceType.RELATION_QUERY; + } + + @Override + public void validate() { + if (maxLevel < 1) { + throw new IllegalArgumentException("Relation query dynamic source configuration max relation level can't be less than 1!"); + } + if (direction == null) { + throw new IllegalArgumentException("Relation query dynamic source configuration direction must be specified!"); + } + if (StringUtils.isBlank(relationType)) { + throw new IllegalArgumentException("Relation query dynamic source configuration relation type must be specified!"); + } + } + + @JsonIgnore + public boolean isSimpleRelation() { + return maxLevel == 1; + } + + public void validateMaxRelationLevel(String argumentName, int maxAllowedRelationLevel) { + if (maxLevel > maxAllowedRelationLevel) { + throw new IllegalArgumentException("Max relation level is greater than configured " + + "maximum allowed relation level in tenant profile: " + maxAllowedRelationLevel + " for argument: " + argumentName); + } + } + + public EntityRelationsQuery toEntityRelationsQuery(EntityId rootEntityId) { + if (isSimpleRelation()) { + throw new IllegalArgumentException("Entity relations query can't be created for a simple relation!"); + } + var entityRelationsQuery = new EntityRelationsQuery(); + entityRelationsQuery.setParameters(new RelationsSearchParameters(rootEntityId, direction, maxLevel, fetchLastLevelOnly)); + entityRelationsQuery.setFilters(Collections.singletonList(new RelationEntityTypeFilter(relationType, Collections.emptyList()))); + return entityRelationsQuery; + } + + public List resolveEntityIds(List relations) { + return switch (direction) { + case FROM -> relations.stream().map(EntityRelation::getTo).toList(); + case TO -> relations.stream().map(EntityRelation::getFrom).toList(); + }; + } + +} 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..7902a9cf5b --- /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 com.fasterxml.jackson.annotation.JsonIgnore; + +public interface ScheduledUpdateSupportedCalculatedFieldConfiguration extends CalculatedFieldConfiguration { + + @JsonIgnore + boolean isScheduledUpdateEnabled(); + + 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/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..9ea5c19e8c --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/geofencing/EntityCoordinates.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.common.data.cf.configuration.geofencing; + + +import lombok.Data; +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.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"; + + private final String latitudeKeyName; + private final String longitudeKeyName; + + public void validate() { + if (StringUtils.isBlank(latitudeKeyName)) { + throw new IllegalArgumentException("Entity coordinates latitude key name must be specified!"); + } + if (StringUtils.isBlank(longitudeKeyName)) { + throw new IllegalArgumentException("Entity coordinates longitude key name must be specified!"); + } + } + + 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..dc331f5876 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/geofencing/GeofencingCalculatedFieldConfiguration.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.common.data.cf.configuration.geofencing; + +import com.fasterxml.jackson.annotation.JsonIgnore; +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.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +@Data +public class GeofencingCalculatedFieldConfiguration implements ArgumentsBasedCalculatedFieldConfiguration, ScheduledUpdateSupportedCalculatedFieldConfiguration { + + private EntityCoordinates entityCoordinates; + private Map zoneGroups; + 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 List getReferencedEntities() { + return zoneGroups.values().stream().map(ZoneGroupConfiguration::getRefEntityId).filter(Objects::nonNull).toList(); + } + + @Override + public Output getOutput() { + return output; + } + + @Override + public boolean isScheduledUpdateEnabled() { + return scheduledUpdateInterval > 0 && zoneGroups.values().stream().anyMatch(ZoneGroupConfiguration::hasDynamicSource); + } + + @Override + public void validate() { + if (entityCoordinates == null) { + throw new IllegalArgumentException("Geofencing calculated field entity coordinates must be specified!"); + } + entityCoordinates.validate(); + if (zoneGroups == null || zoneGroups.isEmpty()) { + throw new IllegalArgumentException("Geofencing calculated field must contain at least one geofencing zone group defined!"); + } + 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..2feb6e49d0 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/geofencing/ZoneGroupConfiguration.java @@ -0,0 +1,95 @@ +/** + * 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 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; + + private final String perimeterKeyName; + + 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 (StringUtils.isBlank(perimeterKeyName)) { + throw new IllegalArgumentException("Perimeter key name must be specified for '" + name + "' zone group!"); + } + if (reportStrategy == null) { + throw new IllegalArgumentException("Report strategy must be specified for '" + name + "' zone group!"); + } + if (hasDynamicSource()) { + 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 hasDynamicSource() { + return refDynamicSourceConfiguration != null; + } + + @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/edge/EdgeEventType.java b/common/data/src/main/java/org/thingsboard/server/common/data/edge/EdgeEventType.java index 6f1ae8150f..0d5c3f34ad 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 @@ -41,12 +41,13 @@ public enum EdgeEventType { ADMIN_SETTINGS(true, null), OTA_PACKAGE(true, EntityType.OTA_PACKAGE), QUEUE(true, EntityType.QUEUE), - NOTIFICATION_RULE (true, EntityType.NOTIFICATION_RULE), - NOTIFICATION_TARGET (true, EntityType.NOTIFICATION_TARGET), - NOTIFICATION_TEMPLATE (true, EntityType.NOTIFICATION_TEMPLATE), + NOTIFICATION_RULE(true, EntityType.NOTIFICATION_RULE), + NOTIFICATION_TARGET(true, EntityType.NOTIFICATION_TARGET), + NOTIFICATION_TEMPLATE(true, EntityType.NOTIFICATION_TEMPLATE), TB_RESOURCE(true, EntityType.TB_RESOURCE), OAUTH2_CLIENT(true, EntityType.OAUTH2_CLIENT), - DOMAIN(true, EntityType.DOMAIN); + DOMAIN(true, EntityType.DOMAIN), + CALCULATED_FIELD(false, EntityType.CALCULATED_FIELD); private final boolean allEdgesRelated; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/AdminSettingsId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/AdminSettingsId.java index 8f2104b5fd..05194b8df5 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/AdminSettingsId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/AdminSettingsId.java @@ -17,14 +17,26 @@ package org.thingsboard.server.common.data.id; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import org.thingsboard.server.common.data.EntityType; +import java.io.Serial; import java.util.UUID; -public class AdminSettingsId extends UUIDBased { +public class AdminSettingsId extends UUIDBased implements EntityId { + + @Serial + private static final long serialVersionUID = -4208011957475806567L; @JsonCreator - public AdminSettingsId(@JsonProperty("id") UUID id){ + public AdminSettingsId(@JsonProperty("id") UUID id) { super(id); } - + + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "ADMIN_SETTINGS", allowableValues = "ADMIN_SETTINGS") + @Override + public EntityType getEntityType() { + return EntityType.ADMIN_SETTINGS; + } + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/AiModelId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/AiModelId.java new file mode 100644 index 0000000000..cac9e8200c --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/AiModelId.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.id; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import org.thingsboard.server.common.data.EntityType; + +import java.io.Serial; +import java.util.UUID; + +public final class AiModelId extends UUIDBased implements EntityId { + + @Serial + private static final long serialVersionUID = 3021036138554389754L; + + @JsonCreator + public AiModelId(@JsonProperty("id") UUID id) { + super(id); + } + + @Override + @Schema( + requiredMode = Schema.RequiredMode.REQUIRED, + description = "Entity type of the AI model", + example = "AI_MODEL", + allowableValues = "AI_MODEL" + ) + public EntityType getEntityType() { + return EntityType.AI_MODEL; + } + + public static AiModelId fromString(String uuid) { + return new AiModelId(UUID.fromString(uuid)); + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/EdgeId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/EdgeId.java index 9a20be4eb9..4ccabd39b8 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/EdgeId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/EdgeId.java @@ -23,10 +23,12 @@ import org.springframework.util.ConcurrentReferenceHashMap; import org.springframework.util.ConcurrentReferenceHashMap.ReferenceType; import org.thingsboard.server.common.data.EntityType; +import java.io.Serial; import java.util.UUID; public class EdgeId extends UUIDBased implements EntityId { + @Serial private static final long serialVersionUID = 1L; @JsonIgnore @@ -51,4 +53,5 @@ public class EdgeId extends UUIDBased implements EntityId { public static EdgeId fromUUID(@JsonProperty("id") UUID id) { return edges.computeIfAbsent(id, EdgeId::new); } + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityId.java index 24196e28b5..ddfcd85dc8 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityId.java @@ -24,10 +24,6 @@ import org.thingsboard.server.common.data.EntityType; import java.io.Serializable; import java.util.UUID; -/** - * @author Andrew Shvayka - */ - @JsonDeserialize(using = EntityIdDeserializer.class) @JsonSerialize(using = EntityIdSerializer.class) @Schema 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 dcf59a4ea4..fc1c1cd1a9 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 @@ -20,9 +20,6 @@ import org.thingsboard.server.common.data.edge.EdgeEventType; import java.util.UUID; -/** - * Created by ashvayka on 25.04.17. - */ public class EntityIdFactory { public static EntityId getByTypeAndUuid(int type, String uuid) { @@ -50,129 +47,74 @@ public class EntityIdFactory { } public static EntityId getByTypeAndUuid(EntityType type, UUID uuid) { - switch (type) { - case TENANT: - return TenantId.fromUUID(uuid); - case CUSTOMER: - return new CustomerId(uuid); - case USER: - return new UserId(uuid); - case DASHBOARD: - return new DashboardId(uuid); - case DEVICE: - return new DeviceId(uuid); - case ASSET: - return new AssetId(uuid); - case ALARM: - return new AlarmId(uuid); - case RULE_CHAIN: - return new RuleChainId(uuid); - case RULE_NODE: - return new RuleNodeId(uuid); - case ENTITY_VIEW: - return new EntityViewId(uuid); - case WIDGETS_BUNDLE: - return new WidgetsBundleId(uuid); - case WIDGET_TYPE: - return new WidgetTypeId(uuid); - case DEVICE_PROFILE: - return new DeviceProfileId(uuid); - case ASSET_PROFILE: - return new AssetProfileId(uuid); - case TENANT_PROFILE: - return new TenantProfileId(uuid); - case API_USAGE_STATE: - return new ApiUsageStateId(uuid); - case TB_RESOURCE: - return new TbResourceId(uuid); - case OTA_PACKAGE: - return new OtaPackageId(uuid); - case EDGE: - return new EdgeId(uuid); - case RPC: - return new RpcId(uuid); - case QUEUE: - return new QueueId(uuid); - case NOTIFICATION_TARGET: - return new NotificationTargetId(uuid); - case NOTIFICATION_REQUEST: - return new NotificationRequestId(uuid); - case NOTIFICATION_RULE: - return new NotificationRuleId(uuid); - case NOTIFICATION_TEMPLATE: - return new NotificationTemplateId(uuid); - case NOTIFICATION: - return new NotificationId(uuid); - case QUEUE_STATS: - return new QueueStatsId(uuid); - case OAUTH2_CLIENT: - return new OAuth2ClientId(uuid); - case MOBILE_APP: - return new MobileAppId(uuid); - case DOMAIN: - return new DomainId(uuid); - case MOBILE_APP_BUNDLE: - return new MobileAppBundleId(uuid); - case CALCULATED_FIELD: - return new CalculatedFieldId(uuid); - case CALCULATED_FIELD_LINK: - return new CalculatedFieldLinkId(uuid); - case JOB: - return new JobId(uuid); - } - throw new IllegalArgumentException("EntityType " + type + " is not supported!"); + return switch (type) { + case TENANT -> TenantId.fromUUID(uuid); + case CUSTOMER -> new CustomerId(uuid); + case USER -> new UserId(uuid); + case DASHBOARD -> new DashboardId(uuid); + case DEVICE -> new DeviceId(uuid); + case ASSET -> new AssetId(uuid); + case ALARM -> new AlarmId(uuid); + case RULE_CHAIN -> new RuleChainId(uuid); + case RULE_NODE -> new RuleNodeId(uuid); + case ENTITY_VIEW -> new EntityViewId(uuid); + case WIDGETS_BUNDLE -> new WidgetsBundleId(uuid); + case WIDGET_TYPE -> new WidgetTypeId(uuid); + case DEVICE_PROFILE -> new DeviceProfileId(uuid); + case ASSET_PROFILE -> new AssetProfileId(uuid); + case TENANT_PROFILE -> new TenantProfileId(uuid); + case API_USAGE_STATE -> new ApiUsageStateId(uuid); + case TB_RESOURCE -> new TbResourceId(uuid); + case OTA_PACKAGE -> new OtaPackageId(uuid); + case EDGE -> new EdgeId(uuid); + case RPC -> new RpcId(uuid); + case QUEUE -> new QueueId(uuid); + case NOTIFICATION_TARGET -> new NotificationTargetId(uuid); + case NOTIFICATION_REQUEST -> new NotificationRequestId(uuid); + case NOTIFICATION_RULE -> new NotificationRuleId(uuid); + case NOTIFICATION_TEMPLATE -> new NotificationTemplateId(uuid); + case NOTIFICATION -> new NotificationId(uuid); + case QUEUE_STATS -> new QueueStatsId(uuid); + case OAUTH2_CLIENT -> new OAuth2ClientId(uuid); + case MOBILE_APP -> new MobileAppId(uuid); + 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); + }; } public static EntityId getByEdgeEventTypeAndUuid(EdgeEventType edgeEventType, UUID uuid) { - switch (edgeEventType) { - case TENANT: - return TenantId.fromUUID(uuid); - case CUSTOMER: - return new CustomerId(uuid); - case USER: - return new UserId(uuid); - case DASHBOARD: - return new DashboardId(uuid); - case DEVICE: - return new DeviceId(uuid); - case ASSET: - return new AssetId(uuid); - case ALARM: - return new AlarmId(uuid); - case RULE_CHAIN: - return new RuleChainId(uuid); - case ENTITY_VIEW: - return new EntityViewId(uuid); - case WIDGETS_BUNDLE: - return new WidgetsBundleId(uuid); - case WIDGET_TYPE: - return new WidgetTypeId(uuid); - case DEVICE_PROFILE: - return new DeviceProfileId(uuid); - case ASSET_PROFILE: - return new AssetProfileId(uuid); - case TENANT_PROFILE: - return new TenantProfileId(uuid); - case OTA_PACKAGE: - return new OtaPackageId(uuid); - case EDGE: - return new EdgeId(uuid); - case QUEUE: - return new QueueId(uuid); - case TB_RESOURCE: - return new TbResourceId(uuid); - case NOTIFICATION_RULE: - return new NotificationRuleId(uuid); - case NOTIFICATION_TARGET: - return new NotificationTargetId(uuid); - case NOTIFICATION_TEMPLATE: - return new NotificationTemplateId(uuid); - case OAUTH2_CLIENT: - return new OAuth2ClientId(uuid); - case DOMAIN: - return new DomainId(uuid); - } - throw new IllegalArgumentException("EdgeEventType " + edgeEventType + " is not supported!"); + return switch (edgeEventType) { + case TENANT -> TenantId.fromUUID(uuid); + case CUSTOMER -> new CustomerId(uuid); + case USER -> new UserId(uuid); + case DASHBOARD -> new DashboardId(uuid); + case DEVICE -> new DeviceId(uuid); + case ASSET -> new AssetId(uuid); + case ALARM -> new AlarmId(uuid); + case RULE_CHAIN -> new RuleChainId(uuid); + case ENTITY_VIEW -> new EntityViewId(uuid); + case WIDGETS_BUNDLE -> new WidgetsBundleId(uuid); + case WIDGET_TYPE -> new WidgetTypeId(uuid); + case DEVICE_PROFILE -> new DeviceProfileId(uuid); + case ASSET_PROFILE -> new AssetProfileId(uuid); + case TENANT_PROFILE -> new TenantProfileId(uuid); + case OTA_PACKAGE -> new OtaPackageId(uuid); + case EDGE -> EdgeId.fromUUID(uuid); + case QUEUE -> new QueueId(uuid); + case TB_RESOURCE -> new TbResourceId(uuid); + case NOTIFICATION_RULE -> new NotificationRuleId(uuid); + case NOTIFICATION_TARGET -> new NotificationTargetId(uuid); + case NOTIFICATION_TEMPLATE -> new NotificationTemplateId(uuid); + case OAUTH2_CLIENT -> new OAuth2ClientId(uuid); + case DOMAIN -> new DomainId(uuid); + case CALCULATED_FIELD -> new CalculatedFieldId(uuid); + default -> throw new IllegalArgumentException("EdgeEventType " + edgeEventType + " is not supported!"); + }; } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/JobId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/JobId.java index 76678b8b31..372452825f 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/JobId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/JobId.java @@ -20,10 +20,14 @@ import com.fasterxml.jackson.annotation.JsonProperty; import io.swagger.v3.oas.annotations.media.Schema; import org.thingsboard.server.common.data.EntityType; +import java.io.Serial; import java.util.UUID; public class JobId extends UUIDBased implements EntityId { + @Serial + private static final long serialVersionUID = -2225072123132918395L; + @JsonCreator public JobId(@JsonProperty("id") UUID id) { super(id); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/OtaPackageId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/OtaPackageId.java index 2a8efc5bfb..a246ab5f94 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/OtaPackageId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/OtaPackageId.java @@ -20,10 +20,12 @@ import com.fasterxml.jackson.annotation.JsonProperty; import io.swagger.v3.oas.annotations.media.Schema; import org.thingsboard.server.common.data.EntityType; +import java.io.Serial; import java.util.UUID; public class OtaPackageId extends UUIDBased implements EntityId { + @Serial private static final long serialVersionUID = 1L; @JsonCreator diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/TenantId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/TenantId.java index 57561fb7ee..7cad1698d7 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/TenantId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/TenantId.java @@ -23,6 +23,7 @@ import org.springframework.util.ConcurrentReferenceHashMap; import org.springframework.util.ConcurrentReferenceHashMap.ReferenceType; import org.thingsboard.server.common.data.EntityType; +import java.io.Serial; import java.util.UUID; public final class TenantId extends UUIDBased implements EntityId { @@ -33,6 +34,7 @@ public final class TenantId extends UUIDBased implements EntityId { @JsonIgnore public static final TenantId SYS_TENANT_ID = TenantId.fromUUID(EntityId.NULL_UUID); + @Serial private static final long serialVersionUID = 1L; @JsonCreator diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/limit/RateLimitUtil.java b/common/data/src/main/java/org/thingsboard/server/common/data/limit/RateLimitUtil.java index f2ee164dcd..f6e9085489 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/limit/RateLimitUtil.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/limit/RateLimitUtil.java @@ -77,16 +77,4 @@ public class RateLimitUtil { return true; } - @Deprecated(forRemoval = true, since = "4.1") - public static String deduplicateByDuration(String configStr) { - if (configStr == null) { - return null; - } - Set distinctDurations = new HashSet<>(); - return parseConfig(configStr).stream() - .filter(entry -> distinctDurations.add(entry.durationSeconds())) - .map(RateLimitEntry::toString) - .collect(Collectors.joining(",")); - } - } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/app/MobileApp.java b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/app/MobileApp.java index e5fefd6072..ef1580c0e3 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/app/MobileApp.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/app/MobileApp.java @@ -43,6 +43,9 @@ public class MobileApp extends BaseData implements HasTenantId, Has @NotBlank @Length(fieldName = "pkgName") private String pkgName; + @Schema(description = "Application title") + @Length(fieldName = "title") + private String title; @Schema(description = "Application secret. The length must be at least 16 characters", requiredMode = Schema.RequiredMode.REQUIRED) @NotEmpty @Length(fieldName = "appSecret", min = 16, max = 2048, message = "must be at least 16 and max 2048 characters") @@ -72,6 +75,7 @@ public class MobileApp extends BaseData implements HasTenantId, Has super(mobile); this.tenantId = mobile.tenantId; this.pkgName = mobile.pkgName; + this.title = mobile.title; this.appSecret = mobile.appSecret; this.platformType = mobile.platformType; this.status = mobile.status; 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/query/AliasEntityId.java b/common/data/src/main/java/org/thingsboard/server/common/data/query/AliasEntityId.java new file mode 100644 index 0000000000..51c3dcdd53 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/query/AliasEntityId.java @@ -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. + */ +package org.thingsboard.server.common.data.query; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import io.swagger.v3.oas.annotations.media.Schema; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.UserId; + +@JsonDeserialize(using = AliasEntityIdDeserializer.class) +@JsonSerialize(using = AliasEntityIdSerializer.class) +@Schema +public interface AliasEntityId extends EntityId { + + AliasEntityType getAliasEntityType(); + + EntityId defaultEntityId(); + + EntityId toEntityId(); + + @JsonIgnore + default boolean isAliasEntityId() { + return getAliasEntityType() != null; + } + + static AliasEntityId fromEntityId(EntityId entityId) { + if (entityId != null) { + return new AliasEntityIdImpl(entityId); + } else { + return null; + } + } + + static AliasEntityId resolveAliasEntityId(AliasEntityId aliasEntityId, TenantId tenantId, UserId userId, EntityId userOwnerId) { + if (aliasEntityId != null) { + if (aliasEntityId.isAliasEntityId()) { + AliasEntityType aliasEntityType = aliasEntityId.getAliasEntityType(); + switch (aliasEntityType) { + case CURRENT_CUSTOMER -> { + if (EntityType.CUSTOMER.equals(userOwnerId.getEntityType())) { + return fromEntityId(userOwnerId); + } else { + return fromEntityId(aliasEntityId.defaultEntityId()); + } + } + case CURRENT_TENANT -> { + return fromEntityId(tenantId); + } + case CURRENT_USER -> { + return fromEntityId(userId); + } + case CURRENT_USER_OWNER -> { + return fromEntityId(userOwnerId); + } + } + } else { + return aliasEntityId; + } + } + return null; + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/query/AliasEntityIdDeserializer.java b/common/data/src/main/java/org/thingsboard/server/common/data/query/AliasEntityIdDeserializer.java new file mode 100644 index 0000000000..488bd3eb3b --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/query/AliasEntityIdDeserializer.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.query; + +import com.fasterxml.jackson.core.JacksonException; +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.node.ObjectNode; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.EntityIdFactory; + +import java.io.IOException; +import java.util.UUID; + +public class AliasEntityIdDeserializer extends JsonDeserializer { + + @Override + public AliasEntityId deserialize(JsonParser jsonParser, DeserializationContext ctx) throws IOException, JacksonException { + ObjectCodec oc = jsonParser.getCodec(); + ObjectNode node = oc.readTree(jsonParser); + if (node.has("entityType")) { + String entityType = node.get("entityType").asText(); + try { + EntityType.valueOf(entityType); + if (!node.has("id")) { + throw new IOException("Missing entityType or id!"); + } + EntityId entityId = EntityIdFactory.getByTypeAndId(node.get("entityType").asText(), node.get("id").asText()); + return new AliasEntityIdImpl(entityId); + } catch (IllegalArgumentException e) { + AliasEntityType aliasEntityType = AliasEntityType.valueOf(entityType); + UUID id = null; + if (node.has("id")) { + id = UUID.fromString(node.get("id").asText()); + } + return new AliasEntityIdImpl(aliasEntityType, id); + } + } else { + throw new IOException("Missing entityType!"); + } + } +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/query/AliasEntityIdImpl.java b/common/data/src/main/java/org/thingsboard/server/common/data/query/AliasEntityIdImpl.java new file mode 100644 index 0000000000..6ab1e9875d --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/query/AliasEntityIdImpl.java @@ -0,0 +1,104 @@ +/** + * 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.query; + +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.id.EntityIdFactory; + +import java.util.UUID; + +class AliasEntityIdImpl implements AliasEntityId { + + private UUID id; + private EntityType entityType; + private AliasEntityType aliasEntityType; + private EntityId defaultEntityId; + + protected AliasEntityIdImpl(EntityId entityId) { + this.id = entityId.getId(); + this.entityType = entityId.getEntityType(); + } + + protected AliasEntityIdImpl(AliasEntityType aliasEntityType, UUID id) { + this.aliasEntityType = aliasEntityType; + if (id != null) { + switch (this.aliasEntityType) { + case CURRENT_CUSTOMER: + this.defaultEntityId = new CustomerId(id); + break; + } + } + } + + @Override + public AliasEntityType getAliasEntityType() { + return aliasEntityType; + } + + @Override + public EntityId defaultEntityId() { + return defaultEntityId; + } + + @Override + public EntityId toEntityId() { + return EntityIdFactory.getByTypeAndUuid(entityType, id); + } + + @Override + public UUID getId() { + return id; + } + + @Override + public EntityType getEntityType() { + return entityType; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (!(obj instanceof EntityId otherEntityId)) + return false; + if (obj instanceof AliasEntityId otherAliasEntityId) { + if (otherAliasEntityId.isAliasEntityId()) { + if (!this.isAliasEntityId()) { + return false; + } + if (this.aliasEntityType != otherAliasEntityId.getAliasEntityType()) { + return false; + } + if (this.defaultEntityId != null && !this.defaultEntityId.equals(otherAliasEntityId.defaultEntityId())) { + return false; + } + if (this.defaultEntityId == null && otherAliasEntityId.defaultEntityId() != null) { + return false; + } + } + } + if (this.isAliasEntityId()) { + return false; + } + if (id == null) { + return otherEntityId.getId() == null; + } else return id.equals(otherEntityId.getId()); + } +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/query/AliasEntityIdSerializer.java b/common/data/src/main/java/org/thingsboard/server/common/data/query/AliasEntityIdSerializer.java new file mode 100644 index 0000000000..cd115f2ca6 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/query/AliasEntityIdSerializer.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.query; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; + +import java.io.IOException; +import java.util.UUID; + +public class AliasEntityIdSerializer extends JsonSerializer { + @Override + public void serialize(AliasEntityId value, JsonGenerator gen, SerializerProvider serializers) throws IOException { + gen.writeStartObject(); + String entityType; + if (value.isAliasEntityId()) { + entityType = value.getAliasEntityType().name(); + } else { + entityType = value.getEntityType().name(); + } + gen.writeStringField("entityType", entityType); + UUID id = null; + if (value.getId() != null) { + id = value.getId(); + } else if (value.defaultEntityId() != null) { + id = value.defaultEntityId().getId(); + } + if (id != null) { + gen.writeStringField("id", id.toString()); + } + gen.writeEndObject(); + } +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/query/AliasEntityType.java b/common/data/src/main/java/org/thingsboard/server/common/data/query/AliasEntityType.java new file mode 100644 index 0000000000..ebcb41a2eb --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/query/AliasEntityType.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.query; + +public enum AliasEntityType { + CURRENT_CUSTOMER, + CURRENT_TENANT, + CURRENT_USER, + CURRENT_USER_OWNER +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityFilter.java b/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityFilter.java index 5507f53f08..9c0696f97c 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityFilter.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityFilter.java @@ -19,6 +19,11 @@ 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 org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.UserId; + +import static org.thingsboard.server.common.data.query.AliasEntityId.resolveAliasEntityId; @JsonIgnoreProperties(ignoreUnknown = true) @JsonTypeInfo( @@ -45,4 +50,17 @@ public interface EntityFilter { @JsonIgnore EntityFilterType getType(); + + static void resolveEntityFilter(EntityFilter filter, TenantId tenantId, UserId userId, EntityId userOwnerId) { + if (filter instanceof SingleEntityFilter singleEntityFilter) { + AliasEntityId resolved = resolveAliasEntityId(singleEntityFilter.getSingleEntity(), tenantId, userId, userOwnerId); + singleEntityFilter.setSingleEntity(resolved); + } else if (filter instanceof RelationsQueryFilter queryFilter) { + AliasEntityId resolved = resolveAliasEntityId(queryFilter.getRootEntity(), tenantId, userId, userOwnerId); + queryFilter.setRootEntity(resolved); + } else if (filter instanceof EntitySearchQueryFilter queryFilter) { + AliasEntityId resolved = resolveAliasEntityId(queryFilter.getRootEntity(), tenantId, userId, userOwnerId); + queryFilter.setRootEntity(resolved); + } + } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/query/EntitySearchQueryFilter.java b/common/data/src/main/java/org/thingsboard/server/common/data/query/EntitySearchQueryFilter.java index babcc466bd..873aadbebc 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/query/EntitySearchQueryFilter.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/query/EntitySearchQueryFilter.java @@ -22,10 +22,12 @@ import org.thingsboard.server.common.data.relation.EntitySearchDirection; @Data public abstract class EntitySearchQueryFilter implements EntityFilter { - private EntityId rootEntity; + private AliasEntityId rootEntity; private String relationType; private EntitySearchDirection direction; private int maxLevel; private boolean fetchLastLevelOnly; + private boolean rootStateEntity; + private AliasEntityId defaultStateEntity; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/query/RelationsQueryFilter.java b/common/data/src/main/java/org/thingsboard/server/common/data/query/RelationsQueryFilter.java index 83aad4eaaf..fb19857428 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/query/RelationsQueryFilter.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/query/RelationsQueryFilter.java @@ -32,7 +32,7 @@ public class RelationsQueryFilter implements EntityFilter { return EntityFilterType.RELATIONS_QUERY; } - private EntityId rootEntity; + private AliasEntityId rootEntity; private boolean isMultiRoot; private EntityType multiRootEntitiesType; private Set multiRootEntityIds; @@ -41,5 +41,7 @@ public class RelationsQueryFilter implements EntityFilter { private int maxLevel; private boolean fetchLastLevelOnly; private boolean negate; + private boolean rootStateEntity; + private AliasEntityId defaultStateEntity; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/query/SingleEntityFilter.java b/common/data/src/main/java/org/thingsboard/server/common/data/query/SingleEntityFilter.java index 2e538806b9..8aa7d4e63b 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/query/SingleEntityFilter.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/query/SingleEntityFilter.java @@ -25,6 +25,6 @@ public class SingleEntityFilter implements EntityFilter { return EntityFilterType.SINGLE_ENTITY; } - private EntityId singleEntity; + private AliasEntityId singleEntity; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/sync/JsonTbEntity.java b/common/data/src/main/java/org/thingsboard/server/common/data/sync/JsonTbEntity.java index c763daeb7f..79dfbe6be2 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/sync/JsonTbEntity.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/sync/JsonTbEntity.java @@ -25,7 +25,9 @@ import org.thingsboard.server.common.data.Dashboard; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.EntityView; +import org.thingsboard.server.common.data.OtaPackage; import org.thingsboard.server.common.data.TbResource; +import org.thingsboard.server.common.data.ai.AiModel; import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.asset.AssetProfile; import org.thingsboard.server.common.data.notification.rule.NotificationRule; @@ -58,8 +60,9 @@ import java.lang.annotation.Target; @Type(name = "NOTIFICATION_TEMPLATE", value = NotificationTemplate.class), @Type(name = "NOTIFICATION_TARGET", value = NotificationTarget.class), @Type(name = "NOTIFICATION_RULE", value = NotificationRule.class), - @Type(name = "TB_RESOURCE", value = TbResource.class) + @Type(name = "TB_RESOURCE", value = TbResource.class), + @Type(name = "OTA_PACKAGE", value = OtaPackage.class), + @Type(name = "AI_MODEL", value = AiModel.class) }) @JsonIgnoreProperties(value = {"tenantId", "createdTime", "version"}, ignoreUnknown = true) -public @interface JsonTbEntity { -} +public @interface JsonTbEntity {} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/EntityExportData.java b/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/EntityExportData.java index a1692aef04..5e1f98638a 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/EntityExportData.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/EntityExportData.java @@ -41,7 +41,8 @@ import java.util.Map; @Type(name = "DEVICE", value = DeviceExportData.class), @Type(name = "RULE_CHAIN", value = RuleChainExportData.class), @Type(name = "WIDGET_TYPE", value = WidgetTypeExportData.class), - @Type(name = "WIDGETS_BUNDLE", value = WidgetsBundleExportData.class) + @Type(name = "WIDGETS_BUNDLE", value = WidgetsBundleExportData.class), + @Type(name = "OTA_PACKAGE", value = OtaPackageExportData.class) }) @JsonInclude(JsonInclude.Include.NON_NULL) @Data diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/OtaPackageExportData.java b/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/OtaPackageExportData.java new file mode 100644 index 0000000000..44e2f7857c --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/OtaPackageExportData.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.sync.ie; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import lombok.EqualsAndHashCode; +import org.thingsboard.server.common.data.OtaPackage; + +@EqualsAndHashCode(callSuper = true) +public class OtaPackageExportData extends EntityExportData { + + /* + * OtaPackage is not a versioned entity; its 'version' field is part of the domain model (not used for optimistic locking) + * We override both methods to ensure 'version' is not ignored during (de)serialization. + */ + @JsonIgnoreProperties(value = {"tenantId", "createdTime"}, ignoreUnknown = true) + @Override + public OtaPackage getEntity() { + return super.getEntity(); + } + + @JsonIgnoreProperties(value = {"tenantId", "createdTime"}, ignoreUnknown = true) + @Override + public void setEntity(OtaPackage entity) { + super.setEntity(entity); + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/request/create/AutoVersionCreateConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/request/create/AutoVersionCreateConfig.java index 8e61991b38..b5e2813134 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/request/create/AutoVersionCreateConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/request/create/AutoVersionCreateConfig.java @@ -18,10 +18,13 @@ package org.thingsboard.server.common.data.sync.vc.request.create; import lombok.Data; import lombok.EqualsAndHashCode; +import java.io.Serial; + @EqualsAndHashCode(callSuper = true) @Data public class AutoVersionCreateConfig extends VersionCreateConfig { + @Serial private static final long serialVersionUID = 8245450889383315551L; private String branch; 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 a4ff47c340..4c8b9e06bd 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 @@ -24,7 +24,6 @@ import lombok.NoArgsConstructor; import org.thingsboard.server.common.data.ApiUsageRecordKey; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.TenantProfileType; -import org.thingsboard.server.common.data.limit.RateLimitUtil; import org.thingsboard.server.common.data.validation.RateLimit; import java.io.Serial; @@ -173,6 +172,10 @@ public class DefaultTenantProfileConfiguration implements TenantProfileConfigura private long maxCalculatedFieldsPerEntity = 5; @Schema(example = "10") private long maxArgumentsPerCF = 10; + @Schema(example = "3600") + private int minAllowedScheduledUpdateIntervalInSecForCF = 3600; + @Schema(example = "10") + private int maxRelationLevelPerCfArgument = 10; @Builder.Default @Min(value = 1, message = "must be at least 1") @Schema(example = "1000") @@ -236,43 +239,4 @@ public class DefaultTenantProfileConfiguration implements TenantProfileConfigura return maxRuleNodeExecutionsPerMessage; } - @Deprecated(forRemoval = true, since = "4.1") - public void deduplicateRateLimitsConfigs() { - this.transportTenantMsgRateLimit = RateLimitUtil.deduplicateByDuration(transportTenantMsgRateLimit); - this.transportTenantTelemetryMsgRateLimit = RateLimitUtil.deduplicateByDuration(transportTenantTelemetryMsgRateLimit); - this.transportTenantTelemetryDataPointsRateLimit = RateLimitUtil.deduplicateByDuration(transportTenantTelemetryDataPointsRateLimit); - - this.transportDeviceMsgRateLimit = RateLimitUtil.deduplicateByDuration(transportDeviceMsgRateLimit); - this.transportDeviceTelemetryMsgRateLimit = RateLimitUtil.deduplicateByDuration(transportDeviceTelemetryMsgRateLimit); - this.transportDeviceTelemetryDataPointsRateLimit = RateLimitUtil.deduplicateByDuration(transportDeviceTelemetryDataPointsRateLimit); - - this.transportGatewayMsgRateLimit = RateLimitUtil.deduplicateByDuration(transportGatewayMsgRateLimit); - this.transportGatewayTelemetryMsgRateLimit = RateLimitUtil.deduplicateByDuration(transportGatewayTelemetryMsgRateLimit); - this.transportGatewayTelemetryDataPointsRateLimit = RateLimitUtil.deduplicateByDuration(transportGatewayTelemetryDataPointsRateLimit); - - this.transportGatewayDeviceMsgRateLimit = RateLimitUtil.deduplicateByDuration(transportGatewayDeviceMsgRateLimit); - this.transportGatewayDeviceTelemetryMsgRateLimit = RateLimitUtil.deduplicateByDuration(transportGatewayDeviceTelemetryMsgRateLimit); - this.transportGatewayDeviceTelemetryDataPointsRateLimit = RateLimitUtil.deduplicateByDuration(transportGatewayDeviceTelemetryDataPointsRateLimit); - - this.tenantEntityExportRateLimit = RateLimitUtil.deduplicateByDuration(tenantEntityExportRateLimit); - this.tenantEntityImportRateLimit = RateLimitUtil.deduplicateByDuration(tenantEntityImportRateLimit); - this.tenantNotificationRequestsRateLimit = RateLimitUtil.deduplicateByDuration(tenantNotificationRequestsRateLimit); - this.tenantNotificationRequestsPerRuleRateLimit = RateLimitUtil.deduplicateByDuration(tenantNotificationRequestsPerRuleRateLimit); - - this.cassandraReadQueryTenantCoreRateLimits = RateLimitUtil.deduplicateByDuration(cassandraReadQueryTenantCoreRateLimits); - this.cassandraWriteQueryTenantCoreRateLimits = RateLimitUtil.deduplicateByDuration(cassandraWriteQueryTenantCoreRateLimits); - this.cassandraReadQueryTenantRuleEngineRateLimits = RateLimitUtil.deduplicateByDuration(cassandraReadQueryTenantRuleEngineRateLimits); - this.cassandraWriteQueryTenantRuleEngineRateLimits = RateLimitUtil.deduplicateByDuration(cassandraWriteQueryTenantRuleEngineRateLimits); - - this.edgeEventRateLimits = RateLimitUtil.deduplicateByDuration(edgeEventRateLimits); - this.edgeEventRateLimitsPerEdge = RateLimitUtil.deduplicateByDuration(edgeEventRateLimitsPerEdge); - this.edgeUplinkMessagesRateLimits = RateLimitUtil.deduplicateByDuration(edgeUplinkMessagesRateLimits); - this.edgeUplinkMessagesRateLimitsPerEdge = RateLimitUtil.deduplicateByDuration(edgeUplinkMessagesRateLimitsPerEdge); - - this.wsUpdatesPerSessionRateLimit = RateLimitUtil.deduplicateByDuration(wsUpdatesPerSessionRateLimit); - - this.tenantServerRestLimitsConfiguration = RateLimitUtil.deduplicateByDuration(tenantServerRestLimitsConfiguration); - this.customerServerRestLimitsConfiguration = RateLimitUtil.deduplicateByDuration(customerServerRestLimitsConfiguration); - } - } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/validation/NoNullChar.java b/common/data/src/main/java/org/thingsboard/server/common/data/validation/NoNullChar.java new file mode 100644 index 0000000000..51399a9634 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/validation/NoNullChar.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.validation; + +import jakarta.validation.Constraint; +import jakarta.validation.Payload; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Documented +@Constraint(validatedBy = {}) +@Target({ElementType.FIELD, ElementType.PARAMETER, ElementType.RECORD_COMPONENT}) +@Retention(RetentionPolicy.RUNTIME) +public @interface NoNullChar { + + String message() default "should not contain 0x00 symbol"; + + Class[] groups() default {}; + + Class[] payload() default {}; + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/validation/ValidJsonSchema.java b/common/data/src/main/java/org/thingsboard/server/common/data/validation/ValidJsonSchema.java new file mode 100644 index 0000000000..d37d7eb9e7 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/validation/ValidJsonSchema.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.validation; + +import jakarta.validation.Constraint; +import jakarta.validation.Payload; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Documented +@Constraint(validatedBy = {}) +@Target({ElementType.FIELD, ElementType.PARAMETER}) +@Retention(RetentionPolicy.RUNTIME) +public @interface ValidJsonSchema { + + String message() default "must conform to the Draft 2020-12 meta-schema"; + + Class[] groups() default {}; + + Class[] payload() default {}; + +} 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..fd59317649 --- /dev/null +++ b/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/ArgumentTest.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; + + +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 validateShouldReturnTrueIfDynamicSourceConfigurationIsNotNull() { + var argument = new Argument(); + argument.setRefDynamicSourceConfiguration(new RelationQueryDynamicSourceConfiguration()); + assertThat(argument.hasDynamicSource()).isTrue(); + } + +} diff --git a/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/RelationQueryDynamicSourceConfigurationTest.java b/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/RelationQueryDynamicSourceConfigurationTest.java new file mode 100644 index 0000000000..86fa52ba66 --- /dev/null +++ b/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/RelationQueryDynamicSourceConfigurationTest.java @@ -0,0 +1,216 @@ +/** + * 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.junit.jupiter.params.provider.ValueSource; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.thingsboard.server.common.data.EntityType; +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.RelationEntityTypeFilter; +import org.thingsboard.server.common.data.relation.RelationsSearchParameters; + +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.when; + +@ExtendWith(MockitoExtension.class) +public class RelationQueryDynamicSourceConfigurationTest { + + @Mock + EntityId rootEntityId; + + @Mock + EntityRelation rel1; + @Mock + EntityRelation rel2; + + @Test + void typeShouldBeRelationQuery() { + var cfg = new RelationQueryDynamicSourceConfiguration(); + assertThat(cfg.getType()).isEqualTo(CFArgumentDynamicSourceType.RELATION_QUERY); + } + + @Test + void validateShouldThrowWhenMaxLevelLessThanOne() { + var cfg = new RelationQueryDynamicSourceConfiguration(); + cfg.setMaxLevel(0); + cfg.setDirection(EntitySearchDirection.FROM); + cfg.setRelationType(EntityRelation.CONTAINS_TYPE); + + assertThatThrownBy(cfg::validate) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Relation query dynamic source configuration max relation level can't be less than 1!"); + } + + @Test + void validateShouldThrowWhenMaxLevelGreaterThanMaxAllowedLevelFromTenantProfile() { + int maxAllowedRelationLevel = 2; + int argumentMaxRelationLevel = 3; + + var cfg = new RelationQueryDynamicSourceConfiguration(); + cfg.setMaxLevel(argumentMaxRelationLevel); + cfg.setDirection(EntitySearchDirection.FROM); + cfg.setRelationType(EntityRelation.CONTAINS_TYPE); + + String testRelationArgument = "testRelationArgument"; + assertThatThrownBy(() -> cfg.validateMaxRelationLevel(testRelationArgument, maxAllowedRelationLevel)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Max relation level is greater than configured " + + "maximum allowed relation level in tenant profile: " + maxAllowedRelationLevel + " for argument: " + testRelationArgument); + } + + @Test + void validateShouldPassValidationWhenMaxLevelLessThanMaxAllowedLevelFromTenantProfile() { + int maxAllowedRelationLevel = 5; + int argumentMaxRelationLevel = 2; + + var cfg = new RelationQueryDynamicSourceConfiguration(); + cfg.setMaxLevel(argumentMaxRelationLevel); + cfg.setDirection(EntitySearchDirection.FROM); + cfg.setRelationType(EntityRelation.CONTAINS_TYPE); + + String testRelationArgument = "testRelationArgument"; + assertThatCode(() -> cfg.validateMaxRelationLevel(testRelationArgument, maxAllowedRelationLevel)).doesNotThrowAnyException(); + } + + @Test + void validateShouldThrowWhenDirectionIsNull() { + var cfg = new RelationQueryDynamicSourceConfiguration(); + cfg.setMaxLevel(1); + cfg.setDirection(null); + cfg.setRelationType(EntityRelation.CONTAINS_TYPE); + + assertThatThrownBy(cfg::validate) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Relation query dynamic source configuration direction must be specified!"); + } + + @ParameterizedTest + @ValueSource(strings = {" "}) + @NullAndEmptySource + void validateShouldThrowWhenRelationTypeIsNull(String relationType) { + var cfg = new RelationQueryDynamicSourceConfiguration(); + cfg.setMaxLevel(1); + cfg.setDirection(EntitySearchDirection.TO); + cfg.setRelationType(relationType); + + assertThatThrownBy(cfg::validate) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Relation query dynamic source configuration relation type must be specified!"); + } + + @ParameterizedTest + @NullAndEmptySource + void isSimpleRelationTrueWhenLevelIsOneAndEntityTypesEmptyOrNull(List entityTypes) { + var cfg = new RelationQueryDynamicSourceConfiguration(); + cfg.setMaxLevel(1); + assertThat(cfg.isSimpleRelation()).isTrue(); + } + + @Test + void isSimpleRelationFalseWhenMaxLevelNotOne() { + var cfg = new RelationQueryDynamicSourceConfiguration(); + cfg.setMaxLevel(2); + assertThat(cfg.isSimpleRelation()).isFalse(); + } + + @ParameterizedTest + @NullAndEmptySource + void toEntityRelationsQueryShouldThrowForSimpleRelation(List entityTypes) { + var cfg = new RelationQueryDynamicSourceConfiguration(); + cfg.setMaxLevel(1); + cfg.setFetchLastLevelOnly(false); + cfg.setDirection(EntitySearchDirection.FROM); + cfg.setRelationType(EntityRelation.CONTAINS_TYPE); + + assertThatThrownBy(() -> cfg.toEntityRelationsQuery(rootEntityId)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Entity relations query can't be created for a simple relation!"); + } + + @Test + void toEntityRelationsQueryShouldBuildQueryForNonSimpleRelation() { + var cfg = new RelationQueryDynamicSourceConfiguration(); + cfg.setMaxLevel(2); + cfg.setFetchLastLevelOnly(true); + cfg.setDirection(EntitySearchDirection.TO); + cfg.setRelationType(EntityRelation.MANAGES_TYPE); + + var query = cfg.toEntityRelationsQuery(rootEntityId); + + assertThat(query).isNotNull(); + RelationsSearchParameters params = query.getParameters(); + assertThat(params).isNotNull(); + assertThat(params.getRootId()).isEqualTo(rootEntityId.getId()); + assertThat(params.getDirection()).isEqualTo(EntitySearchDirection.TO); + assertThat(params.getMaxLevel()).isEqualTo(2); + assertThat(params.isFetchLastLevelOnly()).isTrue(); + + assertThat(query.getFilters()).hasSize(1); + assertThat(query.getFilters().get(0)).isInstanceOf(RelationEntityTypeFilter.class); + RelationEntityTypeFilter filter = query.getFilters().get(0); + assertThat(filter.getRelationType()).isEqualTo(EntityRelation.MANAGES_TYPE); + } + + @Test + void resolveEntityIdsFromDirectionFROMReturnsToIds() { + when(rel1.getTo()).thenReturn(mock(EntityId.class)); + when(rel2.getTo()).thenReturn(mock(EntityId.class)); + + var cfg = new RelationQueryDynamicSourceConfiguration(); + cfg.setDirection(EntitySearchDirection.FROM); + + var out = cfg.resolveEntityIds(List.of(rel1, rel2)); + + assertThat(out).containsExactly(rel1.getTo(), rel2.getTo()); + } + + @Test + void resolveEntityIdsFromDirectionTOReturnsFromIds() { + when(rel1.getFrom()).thenReturn(mock(EntityId.class)); + when(rel2.getFrom()).thenReturn(mock(EntityId.class)); + + var cfg = new RelationQueryDynamicSourceConfiguration(); + cfg.setDirection(EntitySearchDirection.TO); + + var out = cfg.resolveEntityIds(List.of(rel1, rel2)); + + assertThat(out).containsExactly(rel1.getFrom(), rel2.getFrom()); + } + + @Test + void validateShouldPassForValidConfig() { + var cfg = new RelationQueryDynamicSourceConfiguration(); + cfg.setMaxLevel(2); + cfg.setFetchLastLevelOnly(false); + cfg.setDirection(EntitySearchDirection.FROM); + cfg.setRelationType(EntityRelation.CONTAINS_TYPE); + + assertThatCode(cfg::validate).doesNotThrowAnyException(); + } + +} 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..3c0956bd08 --- /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 validateShouldThrowWhenScheduledUpdateIntervalIsSetButTimeUnitIsNotSupported() { + 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/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..c5d627c3e6 --- /dev/null +++ b/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/geofencing/EntityCoordinatesTest.java @@ -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. + */ +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.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.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +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 { + + @ParameterizedTest + @ValueSource(strings = " ") + @NullAndEmptySource + void validateShouldThrowWhenLatitudeCoordinateIsNullEmptyOrBlank(String latitudeKey) { + var entityCoordinates = new EntityCoordinates(latitudeKey, "longitude"); + assertThatThrownBy(entityCoordinates::validate) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Entity coordinates latitude key name must be specified!"); + } + + @ParameterizedTest + @ValueSource(strings = " ") + @NullAndEmptySource + void validateShouldThrowWhenLongitudeCoordinateIsNullEmptyOrBlank(String longitudeKey) { + var entityCoordinates = new EntityCoordinates("latitude", longitudeKey); + assertThatThrownBy(entityCoordinates::validate) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Entity coordinates longitude key name must be specified!"); + } + + @Test + void validateShouldPassOnMinimalValidConfig() { + var entityCoordinates = new EntityCoordinates("latitude", "longitude"); + assertThatCode(entityCoordinates::validate).doesNotThrowAnyException(); + } + + @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..91a47aac57 --- /dev/null +++ b/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/geofencing/GeofencingCalculatedFieldConfigurationTest.java @@ -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. + */ +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.List; +import java.util.Map; + +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.never; +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; + +@ExtendWith(MockitoExtension.class) +public class GeofencingCalculatedFieldConfigurationTest { + + @Test + void typeShouldBeGeofencing() { + var cfg = new GeofencingCalculatedFieldConfiguration(); + assertThat(cfg.getType()).isEqualTo(CalculatedFieldType.GEOFENCING); + } + + @Test + void validateShouldThrowWhenEntityCoordinatesNull() { + var cfg = new GeofencingCalculatedFieldConfiguration(); + cfg.setEntityCoordinates(null); + + assertThatThrownBy(cfg::validate) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Geofencing calculated field entity coordinates must be specified!"); + } + + @Test + void validateShouldThrowWhenZoneGroupsNull() { + var cfg = new GeofencingCalculatedFieldConfiguration(); + cfg.setEntityCoordinates(new EntityCoordinates(ENTITY_ID_LATITUDE_ARGUMENT_KEY, ENTITY_ID_LONGITUDE_ARGUMENT_KEY)); + cfg.setZoneGroups(null); + + assertThatThrownBy(cfg::validate) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Geofencing calculated field must contain at least one geofencing zone group defined!"); + } + + @Test + void validateShouldCallValidateOnEntityCoordinatesAndZoneGroups() { + 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(entityCoordinatesMock).validate(); + verify(zoneGroupConfiguration).validate("someGroupName"); + } + + @Test + void validateShouldCallValidateOnEntityCoordinatesAndZoneGroupsWithoutAnyExceptions() { + 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(entityCoordinatesMock).validate(); + verify(zoneGroupConfigurationA).validate(zoneGroupAName); + verify(zoneGroupConfigurationB).validate(zoneGroupBName); + } + + @Test + void scheduledUpdateDisabledWhenIntervalIsZero() { + var cfg = new GeofencingCalculatedFieldConfiguration(); + cfg.setScheduledUpdateInterval(0); + assertThat(cfg.isScheduledUpdateEnabled()).isFalse(); + } + + @Test + void scheduledUpdateDisabledWhenIntervalIsGreaterThanZeroButNoZonesWithDynamicArguments() { + var cfg = new GeofencingCalculatedFieldConfiguration(); + var zoneGroupConfigurationMock = mock(ZoneGroupConfiguration.class); + when(zoneGroupConfigurationMock.hasDynamicSource()).thenReturn(false); + cfg.setZoneGroups(Map.of("someGroupName", zoneGroupConfigurationMock)); + cfg.setScheduledUpdateInterval(60); + assertThat(cfg.isScheduledUpdateEnabled()).isFalse(); + } + + @Test + void scheduledUpdateEnabledWhenIntervalIsGreaterThanZeroAndDynamicArgumentsPresent() { + var cfg = new GeofencingCalculatedFieldConfiguration(); + var zoneGroupConfigurationMock = mock(ZoneGroupConfiguration.class); + when(zoneGroupConfigurationMock.hasDynamicSource()).thenReturn(true); + cfg.setZoneGroups(Map.of("someGroupName", zoneGroupConfigurationMock)); + cfg.setScheduledUpdateInterval(60); + assertThat(cfg.isScheduledUpdateEnabled()).isTrue(); + } + + @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..4eb822d93c --- /dev/null +++ b/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/geofencing/ZoneGroupConfigurationTest.java @@ -0,0 +1,123 @@ +/** + * 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.ReferencedEntityKey; +import org.thingsboard.server.common.data.cf.configuration.RelationQueryDynamicSourceConfiguration; +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 validateShouldThrowWhenPerimeterKeyNameIsNullEmptyOrBlank(String perimeterKeyName) { + var zoneGroupConfiguration = new ZoneGroupConfiguration(perimeterKeyName, REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS, false); + assertThatThrownBy(() -> zoneGroupConfiguration.validate("allowedZonesGroup")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Perimeter key name must be specified for 'allowedZonesGroup' zone group!"); + } + + @Test + void validateShouldThrowWhenReportStrategyIsNull() { + var zoneGroupConfiguration = new ZoneGroupConfiguration("perimeter", null, false); + assertThatThrownBy(() -> zoneGroupConfiguration.validate("allowedZonesGroup")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Report strategy must be specified for 'allowedZonesGroup' 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 whenHasDynamicSourceCalled_shouldReturnTrueIfDynamicSourceConfigurationIsNotNull() { + var zoneGroupConfiguration = new ZoneGroupConfiguration("perimeter", REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS, false); + zoneGroupConfiguration.setRefDynamicSourceConfiguration(new RelationQueryDynamicSourceConfiguration()); + assertThat(zoneGroupConfiguration.hasDynamicSource()).isTrue(); + } + + @Test + void whenHasDynamicSourceCalled_shouldReturnTrueIfDynamicSourceConfigurationIsNull() { + var zoneGroupConfiguration = mock(ZoneGroupConfiguration.class); + assertThat(zoneGroupConfiguration.getRefDynamicSourceConfiguration()).isNull(); + assertThat(zoneGroupConfiguration.hasDynamicSource()).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/discovery-api/pom.xml b/common/discovery-api/pom.xml index e66d86ff41..3ae12c1bdb 100644 --- a/common/discovery-api/pom.xml +++ b/common/discovery-api/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.0-SNAPSHOT + 4.3.0-SNAPSHOT common org.thingsboard.common diff --git a/common/edge-api/pom.xml b/common/edge-api/pom.xml index a153ee7a9f..7c2967cf3d 100644 --- a/common/edge-api/pom.xml +++ b/common/edge-api/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.0-SNAPSHOT + 4.3.0-SNAPSHOT common org.thingsboard.common 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 509b30feb4..0eff7ad053 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_0_0) + .setEdgeVersion(EdgeVersion.V_4_2_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 023ac00634..dbda462a99 100644 --- a/common/edge-api/src/main/proto/edge.proto +++ b/common/edge-api/src/main/proto/edge.proto @@ -42,6 +42,8 @@ enum EdgeVersion { V_3_8_0 = 8; V_3_9_0 = 9; V_4_0_0 = 10; + V_4_1_0 = 11; + V_4_2_0 = 12; V_LATEST = 999; } @@ -124,6 +126,14 @@ enum UpdateMsgType { // use 6 as a next number } +message CalculatedFieldUpdateMsg{ + UpdateMsgType msgType = 1; + int64 idMSB = 2; + int64 idLSB = 3; + string entity = 4; +} + + message EntityDataProto { int64 entityIdMSB = 1; int64 entityIdLSB = 2; @@ -325,6 +335,12 @@ message RelationRequestMsg { string entityType = 3; } +message CalculatedFieldRequestMsg { + int64 entityIdMSB = 1; + int64 entityIdLSB = 2; + string entityType = 3; +} + // DEPRECATED. FOR REMOVAL message UserCredentialsRequestMsg { option deprecated = true; @@ -423,6 +439,8 @@ message UplinkMsg { repeated AlarmCommentUpdateMsg alarmCommentUpdateMsg = 22; repeated RuleChainUpdateMsg ruleChainUpdateMsg = 23; repeated RuleChainMetadataUpdateMsg ruleChainMetadataUpdateMsg = 24; + repeated CalculatedFieldUpdateMsg calculatedFieldUpdateMsg = 25; + repeated CalculatedFieldRequestMsg calculatedFieldRequestMsg = 26; } message UplinkResponseMsg { @@ -472,4 +490,5 @@ message DownlinkMsg { repeated NotificationTargetUpdateMsg notificationTargetUpdateMsg = 32; repeated NotificationTemplateUpdateMsg notificationTemplateUpdateMsg = 33; repeated OAuth2DomainUpdateMsg oAuth2DomainUpdateMsg = 34; + repeated CalculatedFieldUpdateMsg calculatedFieldUpdateMsg = 35; } diff --git a/common/edqs/pom.xml b/common/edqs/pom.xml index fa5012a180..f7616d5b37 100644 --- a/common/edqs/pom.xml +++ b/common/edqs/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.0-SNAPSHOT + 4.3.0-SNAPSHOT common org.thingsboard.common diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/data/dp/StringDataPoint.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/data/dp/StringDataPoint.java index 52205e2f72..6c67b467eb 100644 --- a/common/edqs/src/main/java/org/thingsboard/server/edqs/data/dp/StringDataPoint.java +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/data/dp/StringDataPoint.java @@ -33,6 +33,16 @@ public class StringDataPoint extends AbstractDataPoint { this.value = deduplicate ? TbStringPool.intern(value) : value; } + @Override + public double getDouble() { + return Double.parseDouble(value); + } + + @Override + public long getLong() { + return Long.parseLong(value); + } + @Override public DataType getType() { return DataType.STRING; diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/processor/EdqsProcessor.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/processor/EdqsProcessor.java index 0e74cb98fa..13a54973e6 100644 --- a/common/edqs/src/main/java/org/thingsboard/server/edqs/processor/EdqsProcessor.java +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/processor/EdqsProcessor.java @@ -203,6 +203,25 @@ public class EdqsProcessor implements TbQueueHandler, }); } + @Override + public TbProtoQueueMsg constructErrorResponseMsg(TbProtoQueueMsg request, Throwable e) { + EdqsResponse response = new EdqsResponse(); + String errorMessage; + if (e instanceof org.apache.kafka.common.errors.RecordTooLargeException) { + errorMessage = "Result set is too large"; + } else if (e instanceof IllegalArgumentException || e instanceof NullPointerException) { + errorMessage = "Invalid request format or missing data: " + ExceptionUtil.getMessage(e); + } else { + errorMessage = ExceptionUtil.getMessage(e); + } + response.setError(errorMessage); + return new TbProtoQueueMsg<>(request.getKey(), FromEdqsMsg.newBuilder() + .setResponseMsg(TransportProtos.EdqsResponseMsg.newBuilder() + .setValue(JacksonUtil.toString(response)) + .build()) + .build(), request.getHeaders()); + } + private EdqsResponse processRequest(TenantId tenantId, CustomerId customerId, EdqsRequest request) { EdqsResponse response = new EdqsResponse(); try { 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/pom.xml b/common/message/pom.xml index e34250fc23..757c5de251 100644 --- a/common/message/pom.xml +++ b/common/message/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.0-SNAPSHOT + 4.3.0-SNAPSHOT common org.thingsboard.common 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..48b07af29b 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,9 +137,6 @@ 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_PARTITIONS_CHANGE_MSG, // Sent when cluster event occures; @@ -150,7 +147,10 @@ public enum MsgType { /* CF Manager Actor -> CF Entity actor */ CF_ENTITY_TELEMETRY_MSG, CF_ENTITY_INIT_CF_MSG, - CF_ENTITY_DELETE_MSG; + CF_ENTITY_DELETE_MSG, + + CF_DYNAMIC_ARGUMENTS_REFRESH_MSG, + CF_ENTITY_DYNAMIC_ARGUMENTS_REFRESH_MSG; @Getter private final boolean ignoreOnStart; diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/TbMsgMetaData.java b/common/message/src/main/java/org/thingsboard/server/common/msg/TbMsgMetaData.java index e703183883..60fb1df6aa 100644 --- a/common/message/src/main/java/org/thingsboard/server/common/msg/TbMsgMetaData.java +++ b/common/message/src/main/java/org/thingsboard/server/common/msg/TbMsgMetaData.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.common.msg; +import com.fasterxml.jackson.annotation.JsonIgnore; import lombok.Data; import java.io.Serializable; @@ -23,9 +24,6 @@ import java.util.HashMap; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; -/** - * Created by ashvayka on 13.01.18. - */ @Data public final class TbMsgMetaData implements Serializable { @@ -34,7 +32,7 @@ public final class TbMsgMetaData implements Serializable { private final Map data; public TbMsgMetaData() { - this.data = new ConcurrentHashMap<>(); + data = new ConcurrentHashMap<>(); } public TbMsgMetaData(Map data) { @@ -46,24 +44,30 @@ public final class TbMsgMetaData implements Serializable { * Internal constructor to create immutable TbMsgMetaData.EMPTY * */ private TbMsgMetaData(int ignored) { - this.data = Collections.emptyMap(); + data = Collections.emptyMap(); } public String getValue(String key) { - return this.data.get(key); + return data.get(key); } public void putValue(String key, String value) { if (key != null && value != null) { - this.data.put(key, value); + data.put(key, value); } } public Map values() { - return new HashMap<>(this.data); + return new HashMap<>(data); } public TbMsgMetaData copy() { - return new TbMsgMetaData(this.data); + return new TbMsgMetaData(data); } + + @JsonIgnore + public boolean isEmpty() { + return data == null || data.isEmpty(); + } + } diff --git a/common/pom.xml b/common/pom.xml index 1bfb9904ed..a9195e49d4 100644 --- a/common/pom.xml +++ b/common/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.0-SNAPSHOT + 4.3.0-SNAPSHOT thingsboard common diff --git a/common/proto/pom.xml b/common/proto/pom.xml index ad2243776b..09f9596d0c 100644 --- a/common/proto/pom.xml +++ b/common/proto/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.0-SNAPSHOT + 4.3.0-SNAPSHOT common org.thingsboard.common diff --git a/common/proto/src/main/java/org/thingsboard/server/common/adaptor/JsonConverter.java b/common/proto/src/main/java/org/thingsboard/server/common/adaptor/JsonConverter.java index 2a208923d9..6518cfc3fe 100644 --- a/common/proto/src/main/java/org/thingsboard/server/common/adaptor/JsonConverter.java +++ b/common/proto/src/main/java/org/thingsboard/server/common/adaptor/JsonConverter.java @@ -56,11 +56,9 @@ import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceX509Ce import java.math.BigDecimal; import java.util.ArrayList; import java.util.HashMap; -import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Map.Entry; -import java.util.Set; import java.util.TreeMap; import java.util.function.Consumer; @@ -538,13 +536,13 @@ public class JsonConverter { return result; } - public static Set convertToAttributes(JsonElement element) { + public static List convertToAttributes(JsonElement element) { long ts = System.currentTimeMillis(); return convertToAttributes(element, ts); } - public static Set convertToAttributes(JsonElement element, long ts) { - return new HashSet<>(parseValues(element.getAsJsonObject()).stream().map(kv -> new BaseAttributeKvEntry(kv, ts)).toList()); + public static List convertToAttributes(JsonElement element, long ts) { + return parseValues(element.getAsJsonObject()).stream().map(kv -> new BaseAttributeKvEntry(kv, ts)).toList(); } private static List parseValues(JsonObject valuesObject) { 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 bc96952ca7..f5a07cf07e 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 @@ -252,7 +252,7 @@ public class ProtoUtils { public static EdgeEvent fromProto(TransportProtos.EdgeEventMsgProto proto) { EdgeEvent edgeEvent = new EdgeEvent(); - TenantId tenantId = new TenantId(new UUID(proto.getTenantIdMSB(), proto.getTenantIdLSB())); + TenantId tenantId = TenantId.fromUUID(new UUID(proto.getTenantIdMSB(), proto.getTenantIdLSB())); edgeEvent.setTenantId(tenantId); edgeEvent.setType(EdgeEventType.valueOf(proto.getEntityType())); edgeEvent.setAction(EdgeEventActionType.valueOf(proto.getAction())); @@ -522,15 +522,21 @@ public class ProtoUtils { } private static TransportProtos.ToDeviceRpcRequestActorMsgProto toProto(ToDeviceRpcRequestActorMsg msg) { - TransportProtos.ToDeviceRpcRequestMsg proto = TransportProtos.ToDeviceRpcRequestMsg.newBuilder() + TransportProtos.ToDeviceRpcRequestMsg.Builder builder = TransportProtos.ToDeviceRpcRequestMsg.newBuilder() .setMethodName(msg.getMsg().getBody().getMethod()) .setParams(msg.getMsg().getBody().getParams()) .setExpirationTime(msg.getMsg().getExpirationTime()) .setRequestIdMSB(msg.getMsg().getId().getMostSignificantBits()) .setRequestIdLSB(msg.getMsg().getId().getLeastSignificantBits()) .setOneway(msg.getMsg().isOneway()) - .setPersisted(msg.getMsg().isPersisted()) - .build(); + .setPersisted(msg.getMsg().isPersisted()); + if (msg.getMsg().getAdditionalInfo() != null) { + builder.setAdditionalInfo(msg.getMsg().getAdditionalInfo()); + } + if (msg.getMsg().getRetries() != null) { + builder.setRetries(msg.getMsg().getRetries()); + } + TransportProtos.ToDeviceRpcRequestMsg proto = builder.build(); return TransportProtos.ToDeviceRpcRequestActorMsgProto.newBuilder() .setTenantIdMSB(msg.getTenantId().getId().getMostSignificantBits()) @@ -551,7 +557,9 @@ public class ProtoUtils { toDeviceRpcRequestMsg.getOneway(), toDeviceRpcRequestMsg.getExpirationTime(), new ToDeviceRpcRequestBody(toDeviceRpcRequestMsg.getMethodName(), toDeviceRpcRequestMsg.getParams()), - toDeviceRpcRequestMsg.getPersisted(), 0, ""); + toDeviceRpcRequestMsg.getPersisted(), + toDeviceRpcRequestMsg.hasRetries() ? toDeviceRpcRequestMsg.getRetries() : null, + toDeviceRpcRequestMsg.hasAdditionalInfo() ? toDeviceRpcRequestMsg.getAdditionalInfo() : null); return new ToDeviceRpcRequestActorMsg(proto.getServiceId(), toDeviceRpcRequest); } @@ -841,7 +849,7 @@ public class ProtoUtils { public static Device fromProto(TransportProtos.DeviceProto proto) { Device device = new Device(getEntityId(proto.getDeviceIdMSB(), proto.getDeviceIdLSB(), DeviceId::new)); device.setCreatedTime(proto.getCreatedTime()); - device.setTenantId(getEntityId(proto.getTenantIdMSB(), proto.getTenantIdLSB(), TenantId::new)); + device.setTenantId(getEntityId(proto.getTenantIdMSB(), proto.getTenantIdLSB(), TenantId::fromUUID)); device.setName(proto.getDeviceName()); device.setType(proto.getDeviceType()); device.setDeviceProfileId(getEntityId(proto.getDeviceProfileIdMSB(), proto.getDeviceProfileIdLSB(), DeviceProfileId::new)); @@ -933,7 +941,7 @@ public class ProtoUtils { public static DeviceProfile fromProto(TransportProtos.DeviceProfileProto proto) { DeviceProfile deviceProfile = new DeviceProfile(getEntityId(proto.getDeviceProfileIdMSB(), proto.getDeviceProfileIdLSB(), DeviceProfileId::new)); deviceProfile.setCreatedTime(proto.getCreatedTime()); - deviceProfile.setTenantId(getEntityId(proto.getTenantIdMSB(), proto.getTenantIdLSB(), TenantId::new)); + deviceProfile.setTenantId(getEntityId(proto.getTenantIdMSB(), proto.getTenantIdLSB(), TenantId::fromUUID)); deviceProfile.setName(proto.getName()); deviceProfile.setDefault(proto.getIsDefault()); deviceProfile.setType(DeviceProfileType.valueOf(proto.getType())); @@ -1024,7 +1032,7 @@ public class ProtoUtils { } public static Tenant fromProto(TransportProtos.TenantProto proto) { - Tenant tenant = new Tenant(getEntityId(proto.getTenantIdMSB(), proto.getTenantIdLSB(), TenantId::new)); + Tenant tenant = new Tenant(getEntityId(proto.getTenantIdMSB(), proto.getTenantIdLSB(), TenantId::fromUUID)); tenant.setCreatedTime(proto.getCreatedTime()); tenant.setTenantProfileId(getEntityId(proto.getTenantProfileIdMSB(), proto.getTenantProfileIdLSB(), TenantProfileId::new)); tenant.setTitle(proto.getTitle()); @@ -1138,7 +1146,7 @@ public class ProtoUtils { public static TbResource fromProto(TransportProtos.TbResourceProto proto) { TbResource resource = new TbResource(getEntityId(proto.getResourceIdMSB(), proto.getResourceIdLSB(), TbResourceId::new)); - resource.setTenantId(getEntityId(proto.getTenantIdMSB(), proto.getTenantIdLSB(), TenantId::new)); + resource.setTenantId(getEntityId(proto.getTenantIdMSB(), proto.getTenantIdLSB(), TenantId::fromUUID)); resource.setCreatedTime(proto.getCreatedTime()); resource.setTitle(proto.getTitle()); resource.setResourceType(ResourceType.valueOf(proto.getResourceType())); @@ -1194,7 +1202,7 @@ public class ProtoUtils { public static ApiUsageState fromProto(TransportProtos.ApiUsageStateProto proto) { ApiUsageState apiUsageState = new ApiUsageState(getEntityId(proto.getApiUsageStateIdMSB(), proto.getApiUsageStateIdLSB(), ApiUsageStateId::new)); - apiUsageState.setTenantId(getEntityId(proto.getTenantProfileIdMSB(), proto.getTenantProfileIdLSB(), TenantId::new)); + apiUsageState.setTenantId(getEntityId(proto.getTenantProfileIdMSB(), proto.getTenantProfileIdLSB(), TenantId::fromUUID)); apiUsageState.setCreatedTime(proto.getCreatedTime()); apiUsageState.setEntityId(EntityIdFactory.getByTypeAndUuid(fromProto(proto.getEntityType()), new UUID(proto.getEntityIdMSB(), proto.getEntityIdLSB()))); apiUsageState.setTransportState(ApiUsageStateValue.valueOf(proto.getTransportState())); @@ -1369,6 +1377,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 c413ecb3c1..a05fdd5d36 100644 --- a/common/proto/src/main/proto/queue.proto +++ b/common/proto/src/main/proto/queue.proto @@ -64,6 +64,8 @@ enum EntityTypeProto { CALCULATED_FIELD = 39; CALCULATED_FIELD_LINK = 40; JOB = 41; + ADMIN_SETTINGS = 42; + AI_MODEL = 43; } enum ApiUsageRecordKeyProto { @@ -80,6 +82,12 @@ enum ApiUsageRecordKeyProto { INACTIVE_DEVICES = 10; } +message EntityIdProto { + int64 entityIdMSB = 1; + int64 entityIdLSB = 2; + EntityTypeProto type = 4; +} + /** * Service Discovery Data Structures; */ @@ -696,6 +704,8 @@ message ToDeviceRpcRequestMsg { int64 requestIdLSB = 6; bool oneway = 7; bool persisted = 8; + optional int32 retries = 9; + optional string additionalInfo = 10; } message ToDeviceRpcResponseMsg { @@ -892,11 +902,25 @@ 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 CalculatedFieldStateProto { CalculatedFieldEntityCtxIdProto id = 1; string type = 2; repeated SingleValueArgumentProto singleValueArguments = 3; repeated TsRollingArgumentProto rollingValueArguments = 4; + repeated GeofencingArgumentProto geofencingArguments = 5; } //Used to report session state to tb-Service and persist this state in the cache on the tb-Service level. diff --git a/common/proto/src/test/java/org/thingsboard/server/common/adaptor/JsonConverterTest.java b/common/proto/src/test/java/org/thingsboard/server/common/adaptor/JsonConverterTest.java index 28b65a7be8..38ef604ab0 100644 --- a/common/proto/src/test/java/org/thingsboard/server/common/adaptor/JsonConverterTest.java +++ b/common/proto/src/test/java/org/thingsboard/server/common/adaptor/JsonConverterTest.java @@ -23,8 +23,6 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.parallel.Isolated; -import java.util.ArrayList; - @Isolated("JsonConverter static settings being modified") public class JsonConverterTest { @@ -53,7 +51,7 @@ public class JsonConverterTest { @Test public void testParseAttributesBigDecimalAsLong() { - var result = new ArrayList<>(JsonConverter.convertToAttributes(JsonParser.parseString("{\"meterReadingDelta\": 1E1}"))); + var result = JsonConverter.convertToAttributes(JsonParser.parseString("{\"meterReadingDelta\": 1E1}")); Assertions.assertEquals(10L, result.get(0).getLongValue().get().longValue()); } @@ -108,4 +106,5 @@ public class JsonConverterTest { JsonConverter.convertToTelemetry(JsonParser.parseString("{\"meterReadingDelta\": 9.9701010061400066E19}"), 0L); }); } + } 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 25788721dc..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 @@ -21,6 +21,10 @@ import org.jeasy.random.EasyRandomParameters; 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; import org.thingsboard.server.common.data.ApiUsageState; import org.thingsboard.server.common.data.Device; @@ -40,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; @@ -56,6 +61,7 @@ import org.thingsboard.server.common.data.security.DeviceCredentialsType; import org.thingsboard.server.common.data.sync.vc.RepositorySettings; import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; import org.thingsboard.server.common.data.tenant.profile.TenantProfileConfiguration; +import org.thingsboard.server.common.msg.ToDeviceActorNotificationMsg; import org.thingsboard.server.common.msg.edge.EdgeEventUpdateMsg; import org.thingsboard.server.common.msg.edge.EdgeHighPriorityMsg; import org.thingsboard.server.common.msg.edge.FromEdgeSyncResponse; @@ -279,4 +285,85 @@ class ProtoUtilsTest { assertThat(actual).as(String.format(description, entityName, entityName)).isEqualTo(expected); } + @ParameterizedTest + @NullAndEmptySource + @ValueSource(strings = {"{\"key\":\"value\"}"}) + void testRpcWithVariousAdditionalInfoToProtoAndBack(String additionalInfo) { + UUID requestId = UUID.fromString("93405c57-5787-46ff-806e-670bb60f49b6"); + String methodName = "reboot"; + String params = ""; + String serviceId = "serviceId"; + long expirationTime = System.currentTimeMillis(); + int retries = 3; + + ToDeviceRpcRequest request = new ToDeviceRpcRequest( + requestId, + tenantId, + deviceId, + false, + expirationTime, + new ToDeviceRpcRequestBody(methodName, params), + true, + retries, + additionalInfo + ); + ToDeviceRpcRequestActorMsg msg = new ToDeviceRpcRequestActorMsg(serviceId, request); + + // Serialize + TransportProtos.ToDeviceActorNotificationMsgProto toProto = ProtoUtils.toProto(msg); + assertThat(toProto).isNotNull(); + assertThat(toProto.hasToDeviceRpcRequestMsg()).isTrue(); + + TransportProtos.ToDeviceRpcRequestActorMsgProto toDeviceRpcRequestActorMsgProto = toProto.getToDeviceRpcRequestMsg(); + assertThat(toDeviceRpcRequestActorMsgProto.hasToDeviceRpcRequestMsg()).isTrue(); + + TransportProtos.ToDeviceRpcRequestMsg toDeviceRpcRequestMsg = toDeviceRpcRequestActorMsgProto.getToDeviceRpcRequestMsg(); + assertThat(toDeviceRpcRequestMsg.getRequestIdMSB()).isEqualTo(requestId.getMostSignificantBits()); + assertThat(toDeviceRpcRequestMsg.getRequestIdLSB()).isEqualTo(requestId.getLeastSignificantBits()); + assertThat(toDeviceRpcRequestMsg.getMethodName()).isEqualTo(methodName); + assertThat(toDeviceRpcRequestMsg.getParams()).isEqualTo(params); + assertThat(toDeviceRpcRequestMsg.getExpirationTime()).isEqualTo(expirationTime); + assertThat(toDeviceRpcRequestMsg.getOneway()).isFalse(); + assertThat(toDeviceRpcRequestMsg.getPersisted()).isTrue(); + assertThat(toDeviceRpcRequestMsg.getRetries()).isEqualTo(retries); + + if (additionalInfo != null) { + assertThat(toDeviceRpcRequestMsg.hasAdditionalInfo()).isTrue(); + assertThat(toDeviceRpcRequestMsg.getAdditionalInfo()).isEqualTo(additionalInfo); + } else { + assertThat(toDeviceRpcRequestMsg.hasAdditionalInfo()).isFalse(); + } + + // Deserialize + ToDeviceActorNotificationMsg fromProto = ProtoUtils.fromProto(toProto); + assertThat(fromProto).isNotNull(); + assertThat(fromProto).isInstanceOf(ToDeviceRpcRequestActorMsg.class); + ToDeviceRpcRequestActorMsg toDeviceRpcRequestActorMsg = (ToDeviceRpcRequestActorMsg) fromProto; + + assertThat(toDeviceRpcRequestActorMsg.getDeviceId()).isEqualTo(deviceId); + assertThat(toDeviceRpcRequestActorMsg.getTenantId()).isEqualTo(tenantId); + assertThat(toDeviceRpcRequestActorMsg.getServiceId()).isEqualTo(serviceId); + 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/pom.xml b/common/queue/pom.xml index e934be308e..ffee41f8a9 100644 --- a/common/queue/pom.xml +++ b/common/queue/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.0-SNAPSHOT + 4.3.0-SNAPSHOT common org.thingsboard.common 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/PartitionedQueueResponseTemplate.java b/common/queue/src/main/java/org/thingsboard/server/queue/common/PartitionedQueueResponseTemplate.java index 7e913009f0..ef1682b97f 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/common/PartitionedQueueResponseTemplate.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/common/PartitionedQueueResponseTemplate.java @@ -21,9 +21,11 @@ import lombok.extern.slf4j.Slf4j; import org.thingsboard.common.util.ThingsBoardExecutors; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.common.stats.MessagesStats; +import org.thingsboard.server.queue.TbQueueCallback; import org.thingsboard.server.queue.TbQueueConsumer; import org.thingsboard.server.queue.TbQueueHandler; import org.thingsboard.server.queue.TbQueueMsg; +import org.thingsboard.server.queue.TbQueueMsgMetadata; import org.thingsboard.server.queue.TbQueueProducer; import org.thingsboard.server.queue.common.consumer.PartitionedQueueConsumerManager; @@ -119,8 +121,20 @@ public class PartitionedQueueResponseTemplate { pendingRequestCount.decrementAndGet(); response.getHeaders().put(REQUEST_ID_HEADER, uuidToBytes(requestId)); - responseProducer.send(TopicPartitionInfo.builder().topic(responseTopic).build(), response, null); - stats.incrementSuccessful(); + TopicPartitionInfo tpi = TopicPartitionInfo.builder().topic(responseTopic).build(); + responseProducer.send(tpi, response, new TbQueueCallback() { + @Override + public void onSuccess(TbQueueMsgMetadata metadata) { + stats.incrementSuccessful(); + } + + @Override + public void onFailure(Throwable t) { + log.error("[{}] Failed to send response {}", requestId, response, t); + sendErrorResponse(requestId, tpi, request, t); + stats.incrementFailed(); + } + }); }, e -> { pendingRequestCount.decrementAndGet(); @@ -144,6 +158,15 @@ public class PartitionedQueueResponseTemplate partitions) { requestConsumer.update(partitions); } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/edqs/EdqsComponent.java b/common/queue/src/main/java/org/thingsboard/server/queue/edqs/EdqsComponent.java index c3658d098a..dd8afd735e 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/edqs/EdqsComponent.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/edqs/EdqsComponent.java @@ -17,12 +17,13 @@ package org.thingsboard.server.queue.edqs; import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; +import java.lang.annotation.Inherited; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +@Inherited @Retention(RetentionPolicy.RUNTIME) @ConditionalOnExpression("'${queue.edqs.sync.enabled:true}'=='true' && ('${service.type:null}'=='edqs' || " + "(('${service.type:null}'=='monolith' || '${service.type:null}'=='tb-core') && " + "'${queue.edqs.mode:null}'=='local'))") -public @interface EdqsComponent { -} +public @interface EdqsComponent {} diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/edqs/InMemoryEdqsComponent.java b/common/queue/src/main/java/org/thingsboard/server/queue/edqs/InMemoryEdqsComponent.java index e414d24fd9..3e1d1411aa 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/edqs/InMemoryEdqsComponent.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/edqs/InMemoryEdqsComponent.java @@ -17,10 +17,11 @@ package org.thingsboard.server.queue.edqs; import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; +import java.lang.annotation.Inherited; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +@Inherited @Retention(RetentionPolicy.RUNTIME) @ConditionalOnExpression("'${queue.edqs.sync.enabled:true}'=='true' && '${service.type:null}'=='monolith' && '${queue.edqs.mode:null}'=='local' && '${queue.type:null}'=='in-memory'") -public @interface InMemoryEdqsComponent { -} +public @interface InMemoryEdqsComponent {} diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/edqs/KafkaEdqsComponent.java b/common/queue/src/main/java/org/thingsboard/server/queue/edqs/KafkaEdqsComponent.java index 3a2b282724..36593bfa1c 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/edqs/KafkaEdqsComponent.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/edqs/KafkaEdqsComponent.java @@ -17,12 +17,13 @@ package org.thingsboard.server.queue.edqs; import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; +import java.lang.annotation.Inherited; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +@Inherited @Retention(RetentionPolicy.RUNTIME) @ConditionalOnExpression("'${queue.edqs.sync.enabled:true}'=='true' && ('${service.type:null}'=='edqs' || " + "(('${service.type:null}'=='monolith' || '${service.type:null}'=='tb-core') && " + "'${queue.edqs.mode:null}'=='local' && '${queue.type:null}'=='kafka'))") -public @interface KafkaEdqsComponent { -} +public @interface KafkaEdqsComponent {} diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/kafka/KafkaAdmin.java b/common/queue/src/main/java/org/thingsboard/server/queue/kafka/KafkaAdmin.java new file mode 100644 index 0000000000..6261e81497 --- /dev/null +++ b/common/queue/src/main/java/org/thingsboard/server/queue/kafka/KafkaAdmin.java @@ -0,0 +1,285 @@ +/** + * 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.kafka; + +import jakarta.annotation.PreDestroy; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.concurrent.ConcurrentException; +import org.apache.commons.lang3.concurrent.LazyInitializer; +import org.apache.kafka.clients.admin.AdminClient; +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.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Component; +import org.thingsboard.common.util.CachedValue; +import org.thingsboard.server.queue.util.TbKafkaComponent; + +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; + +@TbKafkaComponent +@Component +@Slf4j +public class KafkaAdmin { + + /* + * TODO: Get rid of per consumer/producer TbKafkaAdmin, + * use single KafkaAdmin instance that accepts topicConfigs. + * */ + + private final TbKafkaSettings settings; + + @Value("${queue.kafka.request.timeout.ms:30000}") + private int requestTimeoutMs; + @Value("${queue.kafka.topics_cache_ttl_ms:300000}") // 5 minutes by default + private int topicsCacheTtlMs; + + private final LazyInitializer 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 1e0064a5c8..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,29 +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.List; import java.util.Map; -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. @@ -50,214 +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; - } - } - } 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/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/TbCoreComponent.java b/common/queue/src/main/java/org/thingsboard/server/queue/util/TbCoreComponent.java index bf6bfc7a2d..fb6ce213c2 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/util/TbCoreComponent.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/util/TbCoreComponent.java @@ -17,10 +17,11 @@ package org.thingsboard.server.queue.util; import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; +import java.lang.annotation.Inherited; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +@Inherited @Retention(RetentionPolicy.RUNTIME) @ConditionalOnExpression("'${service.type:null}'=='monolith' || '${service.type:null}'=='tb-core'") -public @interface TbCoreComponent { -} +public @interface TbCoreComponent {} 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/main/java/org/thingsboard/server/queue/util/TbLwM2mBootstrapTransportComponent.java b/common/queue/src/main/java/org/thingsboard/server/queue/util/TbLwM2mBootstrapTransportComponent.java index 9216b40c7a..735df499fe 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/util/TbLwM2mBootstrapTransportComponent.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/util/TbLwM2mBootstrapTransportComponent.java @@ -17,10 +17,11 @@ package org.thingsboard.server.queue.util; import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; +import java.lang.annotation.Inherited; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +@Inherited @Retention(RetentionPolicy.RUNTIME) @ConditionalOnExpression("('${service.type:null}'=='tb-transport' || ('${service.type:null}'=='monolith' && '${transport.api_enabled:true}'=='true' && '${transport.lwm2m.enabled}'=='true')) && '${transport.lwm2m.bootstrap.enabled:false}'=='true'") -public @interface TbLwM2mBootstrapTransportComponent { -} +public @interface TbLwM2mBootstrapTransportComponent {} diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/util/TbLwM2mTransportComponent.java b/common/queue/src/main/java/org/thingsboard/server/queue/util/TbLwM2mTransportComponent.java index ddc3093349..8055870872 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/util/TbLwM2mTransportComponent.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/util/TbLwM2mTransportComponent.java @@ -17,10 +17,11 @@ package org.thingsboard.server.queue.util; import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; +import java.lang.annotation.Inherited; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +@Inherited @Retention(RetentionPolicy.RUNTIME) @ConditionalOnExpression("'${service.type:null}'=='tb-transport' || ('${service.type:null}'=='monolith' && '${transport.api_enabled:true}'=='true' && '${transport.lwm2m.enabled}'=='true')") -public @interface TbLwM2mTransportComponent { -} +public @interface TbLwM2mTransportComponent {} diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/util/TbRuleEngineComponent.java b/common/queue/src/main/java/org/thingsboard/server/queue/util/TbRuleEngineComponent.java index ef77763a07..0b03de3570 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/util/TbRuleEngineComponent.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/util/TbRuleEngineComponent.java @@ -17,10 +17,11 @@ package org.thingsboard.server.queue.util; import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; +import java.lang.annotation.Inherited; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +@Inherited @Retention(RetentionPolicy.RUNTIME) @ConditionalOnExpression("'${service.type:null}'=='monolith' || '${service.type:null}'=='tb-rule-engine'") -public @interface TbRuleEngineComponent { -} +public @interface TbRuleEngineComponent {} diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/util/TbSnmpTransportComponent.java b/common/queue/src/main/java/org/thingsboard/server/queue/util/TbSnmpTransportComponent.java index ca84ea7305..914b5456ff 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/util/TbSnmpTransportComponent.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/util/TbSnmpTransportComponent.java @@ -18,12 +18,13 @@ package org.thingsboard.server.queue.util; import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +@Inherited @ConditionalOnExpression("'${service.type:null}'=='tb-transport' || ('${service.type:null}'=='monolith' && '${transport.api_enabled:true}'=='true' && '${transport.snmp.enabled}'=='true')") @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE, ElementType.METHOD}) -public @interface TbSnmpTransportComponent { -} +public @interface TbSnmpTransportComponent {} diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/util/TbTransportComponent.java b/common/queue/src/main/java/org/thingsboard/server/queue/util/TbTransportComponent.java index c879deadf3..3d6a0416e8 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/util/TbTransportComponent.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/util/TbTransportComponent.java @@ -17,10 +17,11 @@ package org.thingsboard.server.queue.util; import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; +import java.lang.annotation.Inherited; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +@Inherited @Retention(RetentionPolicy.RUNTIME) @ConditionalOnExpression("('${service.type:null}'=='monolith' && '${transport.api_enabled:true}'=='true') || '${service.type:null}'=='tb-transport'") -public @interface TbTransportComponent { -} +public @interface TbTransportComponent {} diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/util/TbVersionControlComponent.java b/common/queue/src/main/java/org/thingsboard/server/queue/util/TbVersionControlComponent.java index 99132f1877..1c1fdd041d 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/util/TbVersionControlComponent.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/util/TbVersionControlComponent.java @@ -17,10 +17,11 @@ package org.thingsboard.server.queue.util; import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; +import java.lang.annotation.Inherited; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +@Inherited @Retention(RetentionPolicy.RUNTIME) @ConditionalOnExpression("'${service.type:null}'=='monolith' || '${service.type:null}'=='tb-vc-executor'") -public @interface TbVersionControlComponent { -} +public @interface TbVersionControlComponent {} 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/pom.xml b/common/script/pom.xml index 999d895513..d0a3b47218 100644 --- a/common/script/pom.xml +++ b/common/script/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.0-SNAPSHOT + 4.3.0-SNAPSHOT common org.thingsboard.common diff --git a/common/script/remote-js-client/pom.xml b/common/script/remote-js-client/pom.xml index d92e8e78cd..b1547d2e4b 100644 --- a/common/script/remote-js-client/pom.xml +++ b/common/script/remote-js-client/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.common - 4.2.0-SNAPSHOT + 4.3.0-SNAPSHOT script org.thingsboard.common.script diff --git a/common/script/script-api/pom.xml b/common/script/script-api/pom.xml index bda91efa78..b0fcf1c4ae 100644 --- a/common/script/script-api/pom.xml +++ b/common/script/script-api/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.common - 4.2.0-SNAPSHOT + 4.3.0-SNAPSHOT script org.thingsboard.common.script diff --git a/common/script/script-api/src/main/java/org/thingsboard/script/api/TbScriptException.java b/common/script/script-api/src/main/java/org/thingsboard/script/api/TbScriptException.java index 77888db255..347490b3fb 100644 --- a/common/script/script-api/src/main/java/org/thingsboard/script/api/TbScriptException.java +++ b/common/script/script-api/src/main/java/org/thingsboard/script/api/TbScriptException.java @@ -16,13 +16,24 @@ package org.thingsboard.script.api; import lombok.Getter; +import org.thingsboard.common.util.RecoveryAware; +import java.io.Serial; import java.util.UUID; -public class TbScriptException extends RuntimeException { +public class TbScriptException extends RuntimeException implements RecoveryAware { + + @Serial private static final long serialVersionUID = -1958193538782818284L; - public static enum ErrorCode {COMPILATION, TIMEOUT, RUNTIME, OTHER} + public enum ErrorCode { + + COMPILATION, + TIMEOUT, + RUNTIME, + OTHER + + } @Getter private final UUID scriptId; @@ -37,4 +48,10 @@ public class TbScriptException extends RuntimeException { this.errorCode = errorCode; this.body = body; } + + @Override + public boolean isUnrecoverable() { + return errorCode == ErrorCode.COMPILATION; + } + } diff --git a/common/script/script-api/src/main/java/org/thingsboard/script/api/js/NashornJsInvokeService.java b/common/script/script-api/src/main/java/org/thingsboard/script/api/js/NashornJsInvokeService.java index 3507dd87e6..4aedab8081 100644 --- a/common/script/script-api/src/main/java/org/thingsboard/script/api/js/NashornJsInvokeService.java +++ b/common/script/script-api/src/main/java/org/thingsboard/script/api/js/NashornJsInvokeService.java @@ -20,6 +20,7 @@ import com.google.common.util.concurrent.ListeningExecutorService; import com.google.common.util.concurrent.MoreExecutors; import delight.nashornsandbox.NashornSandbox; import delight.nashornsandbox.NashornSandboxes; +import delight.nashornsandbox.exceptions.ScriptCPUAbuseException; import jakarta.annotation.PostConstruct; import jakarta.annotation.PreDestroy; import lombok.Getter; @@ -153,8 +154,12 @@ public class NashornJsInvokeService extends AbstractJsInvokeService { } scriptInfoMap.put(scriptId, scriptInfo); return scriptId; - } catch (Exception e) { + } catch (ScriptException e) { throw new TbScriptException(scriptId, TbScriptException.ErrorCode.COMPILATION, jsScript, e); + } catch (ScriptCPUAbuseException e) { + throw new TbScriptException(scriptId, TbScriptException.ErrorCode.TIMEOUT, jsScript, e); + } catch (Exception e) { + throw new TbScriptException(scriptId, TbScriptException.ErrorCode.OTHER, jsScript, e); } }); } diff --git a/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/DefaultTbelInvokeService.java b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/DefaultTbelInvokeService.java index 25a7ede547..e2195a6bbf 100644 --- a/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/DefaultTbelInvokeService.java +++ b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/DefaultTbelInvokeService.java @@ -27,6 +27,7 @@ import jakarta.annotation.PreDestroy; import lombok.Getter; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; +import org.mvel2.CompileException; import org.mvel2.ExecutionContext; import org.mvel2.MVEL; import org.mvel2.ParserContext; @@ -52,11 +53,11 @@ import java.io.Serializable; import java.nio.charset.StandardCharsets; import java.util.Calendar; import java.util.Collections; -import java.util.Map; import java.util.Optional; import java.util.Random; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; import java.util.concurrent.Executor; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; @@ -66,9 +67,9 @@ import java.util.concurrent.locks.ReentrantLock; @Service public class DefaultTbelInvokeService extends AbstractScriptInvokeService implements TbelInvokeService { - protected final Map scriptIdToHash = new ConcurrentHashMap<>(); - protected final Map scriptMap = new ConcurrentHashMap<>(); - protected Cache compiledScriptsCache; + private final ConcurrentMap scriptIdToHash = new ConcurrentHashMap<>(); + private final ConcurrentMap scriptMap = new ConcurrentHashMap<>(); + private Cache compiledScriptsCache; private SandboxedParserConfiguration parserConfig; private final Optional apiUsageStateClient; @@ -204,8 +205,10 @@ public class DefaultTbelInvokeService extends AbstractScriptInvokeService implem lock.unlock(); } return scriptId; - } catch (Exception e) { + } catch (CompileException e) { throw new TbScriptException(scriptId, TbScriptException.ErrorCode.COMPILATION, scriptBody, e); + } catch (Exception e) { + throw new TbScriptException(scriptId, TbScriptException.ErrorCode.OTHER, scriptBody, e); } }); } @@ -246,7 +249,7 @@ public class DefaultTbelInvokeService extends AbstractScriptInvokeService implem } } - private Serializable compileScript(String scriptBody) { + private static Serializable compileScript(String scriptBody) throws CompileException { return MVEL.compileExpression(scriptBody, new ParserContext()); } @@ -269,4 +272,5 @@ public class DefaultTbelInvokeService extends AbstractScriptInvokeService implem protected StatsType getStatsType() { return StatsType.TBEL_INVOKE; } + } 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 b782040a99..072a17835d 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 @@ -23,6 +23,7 @@ import org.mvel2.ExecutionContext; import org.mvel2.ParserConfiguration; import org.mvel2.execution.ExecutionArrayList; import org.mvel2.execution.ExecutionHashMap; +import org.mvel2.execution.ExecutionLinkedHashSet; import org.mvel2.util.MethodStub; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.geo.Coordinates; @@ -46,6 +47,7 @@ import java.util.Base64; import java.util.Collection; import java.util.Collections; import java.util.LinkedHashMap; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; @@ -386,6 +388,12 @@ public class TbUtils { Object.class))); parserConfig.addImport("isArray", new MethodStub(TbUtils.class.getMethod("isArray", Object.class))); + parserConfig.addImport("newSet", new MethodStub(TbUtils.class.getMethod("newSet", + ExecutionContext.class))); + parserConfig.addImport("toSet", new MethodStub(TbUtils.class.getMethod("toSet", + ExecutionContext.class, List.class))); + parserConfig.addImport("isSet", new MethodStub(TbUtils.class.getMethod("isSet", + Object.class))); } public static String btoa(String input) { @@ -1481,6 +1489,19 @@ public class TbUtils { return obj != null && obj.getClass().isArray(); } + public static Set newSet(ExecutionContext ctx) { + return new ExecutionLinkedHashSet<>(ctx); + } + + public static Set toSet(ExecutionContext ctx, List list) { + Set newSet = new LinkedHashSet<>(list); + return new ExecutionLinkedHashSet<>(newSet, ctx); + } + + public static boolean isSet(Object obj) { + return obj instanceof Set; + } + private static byte isValidIntegerToByte(Integer val) { if (val > 255 || val < -128) { throw new NumberFormatException("The value '" + val + "' could not be correctly converted to a byte. " + 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..73a2183564 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,8 @@ 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 = TbelCfTsGeofencingArg.class, name = "GEOFENCING_CF_ARGUMENT_VALUE"), }) public interface TbelCfArg extends TbelCfObject { diff --git a/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelCfTsGeofencingArg.java b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelCfTsGeofencingArg.java new file mode 100644 index 0000000000..f1e8ec16db --- /dev/null +++ b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelCfTsGeofencingArg.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 TbelCfTsGeofencingArg implements TbelCfArg { + + private final Object value; + + @JsonCreator + public TbelCfTsGeofencingArg(@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/test/java/org/thingsboard/script/api/TbScriptExceptionTest.java b/common/script/script-api/src/test/java/org/thingsboard/script/api/TbScriptExceptionTest.java new file mode 100644 index 0000000000..330b895504 --- /dev/null +++ b/common/script/script-api/src/test/java/org/thingsboard/script/api/TbScriptExceptionTest.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.script.api; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; + +import static org.assertj.core.api.Assertions.assertThat; + +class TbScriptExceptionTest { + + @Test + void givenCompilationError_whenCheckingIsUnrecoverable_thenReturnsTrue() { + // GIVEN + var exception = new TbScriptException(null, TbScriptException.ErrorCode.COMPILATION, null, null); + + // WHEN-THEN + assertThat(exception.isUnrecoverable()).isTrue(); + } + + @ParameterizedTest + @EnumSource( + value = TbScriptException.ErrorCode.class, + mode = EnumSource.Mode.EXCLUDE, + names = "COMPILATION" + ) + void givenRecoverableErrorCodes_whenCheckingIsUnrecoverable_thenReturnsFalse(TbScriptException.ErrorCode errorCode) { + // GIVEN + var exception = new TbScriptException(null, errorCode, null, null); + + // WHEN-THEN + assertThat(exception.isUnrecoverable()).isFalse(); + } + +} 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 6d793f2c8d..4dcbd2d69c 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 @@ -28,6 +28,7 @@ import org.mvel2.ParserContext; import org.mvel2.SandboxedParserConfiguration; import org.mvel2.execution.ExecutionArrayList; import org.mvel2.execution.ExecutionHashMap; +import org.mvel2.execution.ExecutionLinkedHashSet; import java.io.IOException; import java.math.BigDecimal; @@ -39,14 +40,18 @@ import java.util.Base64; import java.util.Calendar; import java.util.Collections; import java.util.LinkedHashMap; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Random; +import java.util.Set; import java.util.concurrent.ExecutionException; import static java.lang.Character.MAX_RADIX; import static java.lang.Character.MIN_RADIX; +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.assertTrue; @Slf4j @@ -1184,10 +1189,11 @@ public class TbUtilsTest { @Test public void isList() throws ExecutionException, InterruptedException { - List liat = List.of(0x35); - assertTrue(TbUtils.isList(liat)); - assertFalse(TbUtils.isMap(liat)); - assertFalse(TbUtils.isArray(liat)); + List list = List.of(0x35); + assertTrue(TbUtils.isList(list)); + assertFalse(TbUtils.isMap(list)); + assertFalse(TbUtils.isArray(list)); + assertFalse(TbUtils.isSet(list)); } @Test @@ -1195,6 +1201,52 @@ public class TbUtilsTest { byte [] array = new byte[]{1, 2, 3}; assertTrue(TbUtils.isArray(array)); assertFalse(TbUtils.isList(array)); + assertFalse(TbUtils.isSet(array)); + } + + @Test + public void isSet() throws ExecutionException, InterruptedException { + Set set = toSet(new byte[]{(byte) 0xDD, (byte) 0xCC, (byte) 0xBB, (byte) 0xAA}); + assertTrue(TbUtils.isSet(set)); + assertFalse(TbUtils.isList(set)); + assertFalse(TbUtils.isArray(set)); + } + @Test + public void setTest() throws ExecutionException, InterruptedException { + Set actual = TbUtils.newSet(ctx); + Set expected = toSet(new byte[]{(byte) 0xDD, (byte) 0xCC, (byte) 0xCC}); + actual.add((byte) 0xDD); + actual.add((byte) 0xCC); + actual.add((byte) 0xCC); + assertTrue(expected.containsAll(actual)); + List list = toList(new byte[]{(byte) 0xDD, (byte) 0xCC, (byte) 0xBB, (byte) 0xAA}); + actual.addAll(list); + assertEquals(4, actual.size()); + assertTrue(actual.containsAll(expected)); + actual = TbUtils.toSet(ctx, list); + expected = toSet(new byte[]{(byte) 0xDD, (byte) 0xCC, (byte) 0xDA}); + actual.add((byte) 0xDA); + actual.remove((byte) 0xBB); + actual.remove((byte) 0xAA); + assertTrue(expected.containsAll(actual)); + assertEquals(actual.size(), 3); + actual.clear(); + assertTrue(actual.isEmpty()); + actual = TbUtils.toSet(ctx, list); + Set actualClone = TbUtils.toSet(ctx, list); + Set actualClone_asc = TbUtils.toSet(ctx, list); + Set actualClone_desc = TbUtils.toSet(ctx, list); + ((ExecutionLinkedHashSet)actualClone).sort(); + ((ExecutionLinkedHashSet)actualClone_asc).sort(true); + ((ExecutionLinkedHashSet)actualClone_desc).sort(false); + assertEquals(list.toString(), actual.toString()); + assertNotEquals(list.toString(), actualClone.toString()); + Collections.sort(list); + assertEquals(list.toString(), actualClone.toString()); + assertEquals(list.toString(), actualClone_asc.toString()); + Collections.sort(list, Collections.reverseOrder()); + assertNotEquals(list.toString(), actualClone_asc.toString()); + assertEquals(list.toString(), actualClone_desc.toString()); } private static List toList(byte[] data) { @@ -1204,5 +1256,13 @@ public class TbUtilsTest { } return result; } + + private static Set toSet(byte[] data) { + Set result = new LinkedHashSet<>(); + for (Byte b : data) { + result.add(b); + } + return result; + } } diff --git a/common/stats/pom.xml b/common/stats/pom.xml index f2d18688fa..e44e098a39 100644 --- a/common/stats/pom.xml +++ b/common/stats/pom.xml @@ -22,7 +22,7 @@ 4.0.0 org.thingsboard - 4.2.0-SNAPSHOT + 4.3.0-SNAPSHOT common org.thingsboard.common diff --git a/common/transport/coap/pom.xml b/common/transport/coap/pom.xml index c4f6f85e64..37f33470b2 100644 --- a/common/transport/coap/pom.xml +++ b/common/transport/coap/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.common - 4.2.0-SNAPSHOT + 4.3.0-SNAPSHOT transport org.thingsboard.common.transport diff --git a/common/transport/http/pom.xml b/common/transport/http/pom.xml index 6cdc499a41..e49af5b0a2 100644 --- a/common/transport/http/pom.xml +++ b/common/transport/http/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.common - 4.2.0-SNAPSHOT + 4.3.0-SNAPSHOT transport org.thingsboard.common.transport diff --git a/common/transport/lwm2m/pom.xml b/common/transport/lwm2m/pom.xml index 72a5731f8f..1787c851cd 100644 --- a/common/transport/lwm2m/pom.xml +++ b/common/transport/lwm2m/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.common - 4.2.0-SNAPSHOT + 4.3.0-SNAPSHOT transport org.thingsboard.common.transport 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 37f7829c12..431b623da9 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 @@ -383,7 +383,7 @@ public class DefaultLwM2mUplinkMsgHandler extends LwM2MExecutorAwareService impl LwM2mPath path = instant.getKey(); LwM2mNode node = instant.getValue(); LwM2mClient lwM2MClient = clientContext.getClientByEndpoint(registration.getEndpoint()); - ObjectModel objectModelVersion = lwM2MClient.getObjectModel(path.toString(), modelProvider); + ObjectModel objectModelVersion = lwM2MClient.getObjectModel(convertObjectIdToVersionedId(path.toString(), lwM2MClient), modelProvider); if (objectModelVersion != null) { ResourceUpdateResult updateResource = new ResourceUpdateResult(lwM2MClient); if (node instanceof LwM2mObject) { diff --git a/common/transport/mqtt/pom.xml b/common/transport/mqtt/pom.xml index 71a1de3228..9df8cf918e 100644 --- a/common/transport/mqtt/pom.xml +++ b/common/transport/mqtt/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.common - 4.2.0-SNAPSHOT + 4.3.0-SNAPSHOT transport org.thingsboard.common.transport diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/TbMqttTransportComponent.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/TbMqttTransportComponent.java index 8977591391..b517cda103 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/TbMqttTransportComponent.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/TbMqttTransportComponent.java @@ -17,10 +17,11 @@ package org.thingsboard.server.transport.mqtt; import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; +import java.lang.annotation.Inherited; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +@Inherited @Retention(RetentionPolicy.RUNTIME) @ConditionalOnExpression("'${service.type:null}'=='tb-transport' || ('${service.type:null}'=='monolith' && '${transport.api_enabled:true}'=='true' && '${transport.mqtt.enabled}'=='true')") -public @interface TbMqttTransportComponent { -} +public @interface TbMqttTransportComponent {} diff --git a/common/transport/pom.xml b/common/transport/pom.xml index dcb627b1b0..3bd21c96de 100644 --- a/common/transport/pom.xml +++ b/common/transport/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.0-SNAPSHOT + 4.3.0-SNAPSHOT common org.thingsboard.common diff --git a/common/transport/snmp/pom.xml b/common/transport/snmp/pom.xml index ecf85b1dac..1286073a5f 100644 --- a/common/transport/snmp/pom.xml +++ b/common/transport/snmp/pom.xml @@ -21,7 +21,7 @@ org.thingsboard.common - 4.2.0-SNAPSHOT + 4.3.0-SNAPSHOT transport diff --git a/common/transport/transport-api/pom.xml b/common/transport/transport-api/pom.xml index 366d2b3d9b..19b075cb61 100644 --- a/common/transport/transport-api/pom.xml +++ b/common/transport/transport-api/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.common - 4.2.0-SNAPSHOT + 4.3.0-SNAPSHOT transport org.thingsboard.common.transport 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 0884208aee..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 @@ -20,14 +20,17 @@ import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; import org.springframework.boot.autoconfigure.web.ServerProperties; import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.ssl.SslBundle; +import org.springframework.boot.ssl.SslBundles; +import org.springframework.boot.ssl.SslStoreBundle; import org.springframework.boot.web.server.Ssl; -import org.springframework.boot.web.server.SslStoreProvider; import org.springframework.boot.web.server.WebServerFactoryCustomizer; import org.springframework.boot.web.servlet.server.ConfigurableServletWebServerFactory; import org.springframework.context.annotation.Bean; import org.springframework.stereotype.Component; -import java.security.KeyStore; +import java.util.List; +import java.util.function.Consumer; @Component @ConditionalOnExpression("'${spring.main.web-environment:true}'=='true' && '${server.ssl.enabled:false}'=='true'") @@ -43,6 +46,9 @@ public class SslCredentialsWebServerCustomizer implements WebServerFactoryCustom @Qualifier("httpServerSslCredentials") private SslCredentialsConfig httpServerSslCredentialsConfig; + @Autowired + SslBundles sslBundles; + private final ServerProperties serverProperties; public SslCredentialsWebServerCustomizer(ServerProperties serverProperties) { @@ -53,19 +59,36 @@ 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); - factory.setSslStoreProvider(new SslStoreProvider() { + factory.setSslBundles(sslBundles); + } + + @Bean + public SslBundles sslBundles() { + SslStoreBundle storeBundle = SslStoreBundle.of( + httpServerSslCredentialsConfig.getCredentials().getKeyStore(), + httpServerSslCredentialsConfig.getCredentials().getKeyPassword(), + null + ); + return new SslBundles() { @Override - public KeyStore getKeyStore() { - return sslCredentials.getKeyStore(); + public SslBundle getBundle(String name) { + return SslBundle.of(storeBundle); } @Override - public KeyStore getTrustStore() { - return null; + public List getBundleNames() { + return List.of("default"); } - }); + + @Override + public void addBundleUpdateHandler(String name, Consumer handler) { + // no-op + } + }; } + } diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/limits/DefaultTransportRateLimitService.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/limits/DefaultTransportRateLimitService.java index a5ac06bfb6..d03a3d03f2 100644 --- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/limits/DefaultTransportRateLimitService.java +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/limits/DefaultTransportRateLimitService.java @@ -82,16 +82,18 @@ public class DefaultTransportRateLimitService implements TransportRateLimitServi if (!checkEntityRateLimit(dataPoints, getTenantRateLimits(tenantId))) { return TbPair.of(EntityType.TENANT, false); } - if (isGateway && !checkEntityRateLimit(dataPoints, getGatewayDeviceRateLimits(tenantId, deviceId))) { - return TbPair.of(EntityType.DEVICE, true); + if (isGateway) { + if (!checkEntityRateLimit(dataPoints, getGatewayDeviceRateLimits(tenantId, deviceId))) { + return TbPair.of(EntityType.DEVICE, true); + } + } else if (gatewayId == null && deviceId != null) { + if (!checkEntityRateLimit(dataPoints, getDeviceRateLimits(tenantId, deviceId))) { + return TbPair.of(EntityType.DEVICE, false); + } } if (gatewayId != null && !checkEntityRateLimit(dataPoints, getGatewayRateLimits(tenantId, gatewayId))) { return TbPair.of(EntityType.DEVICE, true); } - if (!isGateway && deviceId != null && !checkEntityRateLimit(dataPoints, getDeviceRateLimits(tenantId, deviceId))) { - return TbPair.of(EntityType.DEVICE, false); - } - return null; } diff --git a/common/util/pom.xml b/common/util/pom.xml index 7727b88694..804ed0de38 100644 --- a/common/util/pom.xml +++ b/common/util/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.0-SNAPSHOT + 4.3.0-SNAPSHOT common org.thingsboard.common @@ -114,7 +114,10 @@ net.objecthunter exp4j - ${exp4j.version} + + + com.networknt + json-schema-validator com.github.ben-manes.caffeine diff --git a/common/util/src/main/java/org/thingsboard/common/util/AzureIotHubUtil.java b/common/util/src/main/java/org/thingsboard/common/util/AzureIotHubUtil.java index 2c214460f6..001513b008 100644 --- a/common/util/src/main/java/org/thingsboard/common/util/AzureIotHubUtil.java +++ b/common/util/src/main/java/org/thingsboard/common/util/AzureIotHubUtil.java @@ -26,11 +26,13 @@ import java.nio.file.DirectoryStream; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.time.Clock; import java.util.Base64; import java.util.Iterator; @Slf4j public final class AzureIotHubUtil { + private static final String BASE_DIR_PATH = System.getProperty("user.dir"); private static final String APP_DIR = "application"; private static final String SRC_DIR = "src"; @@ -52,41 +54,37 @@ public final class AzureIotHubUtil { } } - private static final long SAS_TOKEN_VALID_SECS = 365 * 24 * 60 * 60; - private static final long ONE_SECOND_IN_MILLISECONDS = 1000; + private static final long SAS_TOKEN_VALID_SECS = 365 * 24 * 60 * 60; // one year private static final String SAS_TOKEN_FORMAT = "SharedAccessSignature sr=%s&sig=%s&se=%s"; private static final String USERNAME_FORMAT = "%s/%s/?api-version=2018-06-30"; - private AzureIotHubUtil() { - } + private AzureIotHubUtil() {} public static String buildUsername(String host, String deviceId) { return String.format(USERNAME_FORMAT, host, deviceId); } - public static String buildSasToken(String host, String sasKey) { + public static String buildSasToken(String host, String sasKey, Clock clock) { try { - final String targetUri = URLEncoder.encode(host.toLowerCase(), "UTF-8"); - final long expiryTime = buildExpiresOn(); + final String targetUri = URLEncoder.encode(host.toLowerCase(), StandardCharsets.UTF_8); + final long expiryTime = buildExpiresOn(clock); String toSign = targetUri + "\n" + expiryTime; byte[] keyBytes = Base64.getDecoder().decode(sasKey.getBytes(StandardCharsets.UTF_8)); SecretKeySpec signingKey = new SecretKeySpec(keyBytes, "HmacSHA256"); Mac mac = Mac.getInstance("HmacSHA256"); mac.init(signingKey); byte[] rawHmac = mac.doFinal(toSign.getBytes(StandardCharsets.UTF_8)); - String signature = URLEncoder.encode(Base64.getEncoder().encodeToString(rawHmac), "UTF-8"); + String signature = URLEncoder.encode(Base64.getEncoder().encodeToString(rawHmac), StandardCharsets.UTF_8); return String.format(SAS_TOKEN_FORMAT, targetUri, signature, expiryTime); } catch (Exception e) { - throw new RuntimeException("Failed to build SAS token!!!", e); + throw new RuntimeException("Failed to build SAS token!", e); } } - private static long buildExpiresOn() { - long expiresOnDate = System.currentTimeMillis(); - expiresOnDate += SAS_TOKEN_VALID_SECS * ONE_SECOND_IN_MILLISECONDS; - return expiresOnDate / ONE_SECOND_IN_MILLISECONDS; + private static long buildExpiresOn(Clock clock) { + return clock.instant().plusSeconds(SAS_TOKEN_VALID_SECS).getEpochSecond(); } public static String getDefaultCaCert() { 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/JacksonUtil.java b/common/util/src/main/java/org/thingsboard/common/util/JacksonUtil.java index d153501b92..cd61c7ac20 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 @@ -62,9 +62,6 @@ import java.util.function.BiFunction; import java.util.function.UnaryOperator; import java.util.regex.Pattern; -/** - * Created by Valerii Sosliuk on 5/12/2017. - */ @Slf4j public class JacksonUtil { diff --git a/common/util/src/main/java/org/thingsboard/common/util/JsonSchemaUtils.java b/common/util/src/main/java/org/thingsboard/common/util/JsonSchemaUtils.java new file mode 100644 index 0000000000..57ddd8a5ac --- /dev/null +++ b/common/util/src/main/java/org/thingsboard/common/util/JsonSchemaUtils.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.common.util; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.networknt.schema.JsonSchemaFactory; +import com.networknt.schema.SchemaId; +import com.networknt.schema.SchemaLocation; +import com.networknt.schema.SpecVersion; +import com.networknt.schema.ValidationMessage; + +import java.util.Set; + +public final class JsonSchemaUtils { + + private JsonSchemaUtils() {} + + /** + * Validates that the provided ObjectNode is a valid JSON Schema (Draft 2020-12). + * + * @param schemaNode the JSON Schema document as an ObjectNode + * @return true if the schema is well-formed, false otherwise + */ + public static boolean isValidJsonSchema(ObjectNode schemaNode) { + Set errors = JsonSchemaFactory + .getInstance(SpecVersion.VersionFlag.V202012) + .getSchema(SchemaLocation.of(SchemaId.V202012)) + .validate(schemaNode); + return errors.isEmpty(); + } + +} diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/TbActorError.java b/common/util/src/main/java/org/thingsboard/common/util/RecoveryAware.java similarity index 89% rename from common/message/src/main/java/org/thingsboard/server/common/msg/TbActorError.java rename to common/util/src/main/java/org/thingsboard/common/util/RecoveryAware.java index 8c322d8eb5..e1553bec36 100644 --- a/common/message/src/main/java/org/thingsboard/server/common/msg/TbActorError.java +++ b/common/util/src/main/java/org/thingsboard/common/util/RecoveryAware.java @@ -13,9 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.common.msg; +package org.thingsboard.common.util; -public interface TbActorError { +public interface RecoveryAware { boolean isUnrecoverable(); 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/message/src/main/java/org/thingsboard/server/common/msg/cf/CalculatedFieldInitMsg.java b/common/util/src/main/java/org/thingsboard/common/util/geo/PolygonPerimeterDefinition.java similarity index 57% rename from common/message/src/main/java/org/thingsboard/server/common/msg/cf/CalculatedFieldInitMsg.java rename to common/util/src/main/java/org/thingsboard/common/util/geo/PolygonPerimeterDefinition.java index e453d2963c..2d8ca6ef56 100644 --- a/common/message/src/main/java/org/thingsboard/server/common/msg/cf/CalculatedFieldInitMsg.java +++ b/common/util/src/main/java/org/thingsboard/common/util/geo/PolygonPerimeterDefinition.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.common.util.geo; import lombok.Data; -import org.thingsboard.server.common.data.cf.CalculatedField; -import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.msg.MsgType; -import org.thingsboard.server.common.msg.ToCalculatedFieldSystemMsg; @Data -public class CalculatedFieldInitMsg implements ToCalculatedFieldSystemMsg { +public class PolygonPerimeterDefinition implements PerimeterDefinition { - private final TenantId tenantId; - private final CalculatedField cf; + private final String polygonDefinition; @Override - public MsgType getMsgType() { - return MsgType.CF_INIT_MSG; + 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/common/version-control/pom.xml b/common/version-control/pom.xml index 5d6079b313..58c059915c 100644 --- a/common/version-control/pom.xml +++ b/common/version-control/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.0-SNAPSHOT + 4.3.0-SNAPSHOT common org.thingsboard.common 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/pom.xml b/dao/pom.xml index 187c7e6ea5..fd6af62330 100644 --- a/dao/pom.xml +++ b/dao/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.0-SNAPSHOT + 4.3.0-SNAPSHOT thingsboard dao diff --git a/dao/src/main/java/org/thingsboard/server/dao/DaoUtil.java b/dao/src/main/java/org/thingsboard/server/dao/DaoUtil.java index 43b651bb24..7b158fe9a0 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/DaoUtil.java +++ b/dao/src/main/java/org/thingsboard/server/dao/DaoUtil.java @@ -43,10 +43,9 @@ import java.util.function.Consumer; import java.util.function.Function; import java.util.stream.Collectors; -public abstract class DaoUtil { +public final class DaoUtil { - private DaoUtil() { - } + private DaoUtil() {} public static PageData toPageData(Page> page) { List data = convertDataList(page.getContent()); @@ -98,17 +97,17 @@ public abstract class DaoUtil { return PageRequest.of(pageLink.getPage(), pageLink.getPageSize(), pageLink.toSort(sortOrders, columnMap, addDefaultSorting)); } - public static List convertDataList(Collection> toDataList) { - List list = Collections.emptyList(); - if (toDataList != null && !toDataList.isEmpty()) { - list = new ArrayList<>(); - for (ToData object : toDataList) { - if (object != null) { - list.add(object.toData()); - } + public static List convertDataList(Collection> toConvert) { + if (CollectionUtils.isEmpty(toConvert)) { + return Collections.emptyList(); + } + List converted = new ArrayList<>(toConvert.size()); + for (ToData object : toConvert) { + if (object != null) { + converted.add(object.toData()); } } - return list; + return converted; } public static T getData(ToData data) { diff --git a/dao/src/main/java/org/thingsboard/server/dao/ai/AiModelCacheEvictEvent.java b/dao/src/main/java/org/thingsboard/server/dao/ai/AiModelCacheEvictEvent.java new file mode 100644 index 0000000000..b0d4b6fdb6 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/ai/AiModelCacheEvictEvent.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.dao.ai; + +import org.thingsboard.server.common.data.ai.AiModel; + +import static java.util.Objects.requireNonNull; +import static org.thingsboard.server.dao.ai.AiModelCacheEvictEvent.Deleted; +import static org.thingsboard.server.dao.ai.AiModelCacheEvictEvent.Saved; + +sealed interface AiModelCacheEvictEvent permits Saved, Deleted { + + AiModelCacheKey cacheKey(); + + record Saved(AiModelCacheKey cacheKey, AiModel savedModel) implements AiModelCacheEvictEvent { + + public Saved { + requireNonNull(cacheKey); + requireNonNull(savedModel); + } + + } + + record Deleted(AiModelCacheKey cacheKey) implements AiModelCacheEvictEvent { + + public Deleted { + requireNonNull(cacheKey); + } + + } + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/ai/AiModelCacheKey.java b/dao/src/main/java/org/thingsboard/server/dao/ai/AiModelCacheKey.java new file mode 100644 index 0000000000..6b73ad7b28 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/ai/AiModelCacheKey.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.dao.ai; + +import org.checkerframework.checker.nullness.qual.NonNull; +import org.thingsboard.server.cache.VersionedCacheKey; +import org.thingsboard.server.common.data.id.AiModelId; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.TenantId; + +import java.util.UUID; + +import static java.util.Objects.requireNonNull; + +record AiModelCacheKey(UUID tenantId, UUID modelId) implements VersionedCacheKey { + + AiModelCacheKey { + requireNonNull(tenantId); + requireNonNull(modelId); + + if (TenantId.SYS_TENANT_ID.getId().equals(tenantId)) { + throw new IllegalArgumentException("Tenant ID must not be the system tenant ID"); + } + if (EntityId.NULL_UUID.equals(modelId)) { + throw new IllegalArgumentException("Model ID must not be reserved null UUID"); + } + } + + static AiModelCacheKey of(TenantId tenantId, AiModelId modelId) { + return new AiModelCacheKey(tenantId.getId(), modelId.getId()); + } + + @Override + public boolean isVersioned() { + return true; + } + + @NonNull + @Override + public String toString() { + return /* cache name */ "_" + tenantId + "_" + modelId; + } + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/ai/AiModelCaffeineCache.java b/dao/src/main/java/org/thingsboard/server/dao/ai/AiModelCaffeineCache.java new file mode 100644 index 0000000000..165efcd4e2 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/ai/AiModelCaffeineCache.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.dao.ai; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.cache.CacheManager; +import org.springframework.stereotype.Component; +import org.thingsboard.server.cache.VersionedCaffeineTbCache; +import org.thingsboard.server.common.data.CacheConstants; +import org.thingsboard.server.common.data.ai.AiModel; + +@Component("AiModelCache") +@ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "caffeine", matchIfMissing = true) +class AiModelCaffeineCache extends VersionedCaffeineTbCache { + + AiModelCaffeineCache(CacheManager cacheManager) { + super(cacheManager, CacheConstants.AI_MODEL_CACHE); + } + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/ai/AiModelDao.java b/dao/src/main/java/org/thingsboard/server/dao/ai/AiModelDao.java new file mode 100644 index 0000000000..e788685bfa --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/ai/AiModelDao.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.dao.ai; + +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.ExportableEntityDao; +import org.thingsboard.server.dao.TenantEntityDao; + +import java.util.Optional; +import java.util.Set; + +public interface AiModelDao extends TenantEntityDao, ExportableEntityDao { + + Optional findByTenantIdAndId(TenantId tenantId, AiModelId modelId); + + boolean deleteById(TenantId tenantId, AiModelId modelId); + + Set deleteByTenantId(TenantId tenantId); + + boolean deleteByTenantIdAndId(TenantId tenantId, AiModelId modelId); + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/ai/AiModelRedisCache.java b/dao/src/main/java/org/thingsboard/server/dao/ai/AiModelRedisCache.java new file mode 100644 index 0000000000..7bec37875f --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/ai/AiModelRedisCache.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.dao.ai; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.stereotype.Component; +import org.thingsboard.server.cache.CacheSpecsMap; +import org.thingsboard.server.cache.TBRedisCacheConfiguration; +import org.thingsboard.server.cache.TbJsonRedisSerializer; +import org.thingsboard.server.cache.VersionedRedisTbCache; +import org.thingsboard.server.common.data.CacheConstants; +import org.thingsboard.server.common.data.ai.AiModel; + +@Component("AiModelCache") +@ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "redis") +class AiModelRedisCache extends VersionedRedisTbCache { + + AiModelRedisCache(TBRedisCacheConfiguration configuration, CacheSpecsMap cacheSpecsMap, RedisConnectionFactory connectionFactory) { + super(CacheConstants.AI_MODEL_CACHE, cacheSpecsMap, connectionFactory, configuration, new TbJsonRedisSerializer<>(AiModel.class)); + } + +} 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 new file mode 100644 index 0000000000..b091a29247 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/ai/AiModelServiceImpl.java @@ -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. + */ +package org.thingsboard.server.dao.ai; + +import com.google.common.util.concurrent.FluentFuture; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.event.TransactionalEventListener; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.ai.AiModel; +import org.thingsboard.server.common.data.id.AiModelId; +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.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.dao.entity.CachedVersionedEntityService; +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 static org.thingsboard.server.dao.service.Validator.validatePageLink; + +@Service +@RequiredArgsConstructor +class AiModelServiceImpl extends CachedVersionedEntityService implements AiModelService { + + private final DataValidator aiModelValidator; + + private final JpaExecutorService jpaExecutor; + private final AiModelDao aiModelDao; + + @Override + @TransactionalEventListener + public void handleEvictEvent(AiModelCacheEvictEvent event) { + var cacheKey = event.cacheKey(); + if (event instanceof AiModelCacheEvictEvent.Saved savedEvent) { + cache.put(cacheKey, savedEvent.savedModel()); + } else if (event instanceof AiModelCacheEvictEvent.Deleted) { + cache.evict(cacheKey); + } else { + throw new UnsupportedOperationException("Unsupported event type: " + event.getClass().getSimpleName()); + } + } + + @Override + @Transactional + public AiModel save(AiModel model) { + aiModelValidator.validate(model, AiModel::getTenantId); + + AiModel savedModel; + try { + savedModel = aiModelDao.saveAndFlush(model.getTenantId(), model); + } catch (Exception e) { + checkConstraintViolation(e, + "ai_model_name_unq_key", "AI model with such name already exist!", + "ai_model_external_id_unq_key", "AI model with such external ID already exists!"); + throw e; + } + + var cacheKey = AiModelCacheKey.of(savedModel.getTenantId(), savedModel.getId()); + publishEvictEvent(new AiModelCacheEvictEvent.Saved(cacheKey, savedModel)); + + return savedModel; + } + + @Override + public Optional findAiModelById(TenantId tenantId, AiModelId modelId) { + return Optional.ofNullable(aiModelDao.findById(tenantId, modelId.getId())); + } + + @Override + public PageData findAiModelsByTenantId(TenantId tenantId, PageLink pageLink) { + validatePageLink(pageLink, AiModelEntity.ALLOWED_SORT_PROPERTIES); + return aiModelDao.findAllByTenantId(tenantId, pageLink); + } + + @Override + public Optional findAiModelByTenantIdAndId(TenantId tenantId, AiModelId modelId) { + var cacheKey = AiModelCacheKey.of(tenantId, modelId); + return Optional.ofNullable(cache.get(cacheKey, () -> aiModelDao.findByTenantIdAndId(tenantId, modelId).orElse(null))); + } + + @Override + public FluentFuture> findAiModelByTenantIdAndIdAsync(TenantId tenantId, AiModelId modelId) { + return FluentFuture.from(jpaExecutor.submit(() -> findAiModelByTenantIdAndId(tenantId, modelId))); + } + + @Override + @Transactional + public boolean deleteByTenantIdAndId(TenantId tenantId, AiModelId modelId) { + return deleteByTenantIdAndIdInternal(tenantId, modelId); + } + + @Override + public Optional> findEntity(TenantId tenantId, EntityId entityId) { + return findAiModelByTenantIdAndId(tenantId, (AiModelId) entityId) + .map(model -> model); // necessary to cast to HasId + } + + @Override + public long countByTenantId(TenantId tenantId) { + return aiModelDao.countByTenantId(tenantId); + } + + @Override + @Transactional + public void deleteEntity(TenantId tenantId, EntityId id, boolean force) { + deleteByTenantIdAndIdInternal(tenantId, new AiModelId(id.getId())); + } + + private boolean deleteByTenantIdAndIdInternal(TenantId tenantId, AiModelId modelId) { + boolean deleted = aiModelDao.deleteByTenantIdAndId(tenantId, modelId); + if (deleted) { + publishEvictEvent(new AiModelCacheEvictEvent.Deleted(AiModelCacheKey.of(tenantId, modelId))); + } + return deleted; + } + + @Override + @Transactional + public void deleteByTenantId(TenantId tenantId) { + Set deleted = aiModelDao.deleteByTenantId(tenantId); + deleted.forEach(id -> publishEvictEvent(new AiModelCacheEvictEvent.Deleted(AiModelCacheKey.of(tenantId, id)))); + } + + @Override + public EntityType getEntityType() { + return EntityType.AI_MODEL; + } + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmDao.java b/dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmDao.java index 0351490f70..d001258ba4 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmDao.java @@ -16,6 +16,7 @@ package org.thingsboard.server.dao.alarm; import com.fasterxml.jackson.databind.JsonNode; +import com.google.common.util.concurrent.FluentFuture; import com.google.common.util.concurrent.ListenableFuture; import org.thingsboard.server.common.data.EntitySubtype; import org.thingsboard.server.common.data.alarm.Alarm; @@ -47,15 +48,14 @@ import java.util.List; import java.util.Set; import java.util.UUID; -/** - * Created by ashvayka on 11.05.17. - */ public interface AlarmDao extends Dao { Alarm findLatestByOriginatorAndType(TenantId tenantId, EntityId originator, String type); Alarm findLatestActiveByOriginatorAndType(TenantId tenantId, EntityId originator, String type); + FluentFuture findLatestActiveByOriginatorAndTypeAsync(TenantId tenantId, EntityId originator, String type); + ListenableFuture findLatestByOriginatorAndTypeAsync(TenantId tenantId, EntityId originator, String type); Alarm findAlarmById(TenantId tenantId, UUID key); diff --git a/dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmService.java b/dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmService.java index 23e05e1bfa..5c3e4f3c79 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmService.java @@ -17,7 +17,7 @@ package org.thingsboard.server.dao.alarm; import com.fasterxml.jackson.databind.JsonNode; -import com.google.common.base.Function; +import com.google.common.util.concurrent.FluentFuture; import com.google.common.util.concurrent.ListenableFuture; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -67,7 +67,6 @@ 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.service.ConstraintValidator; -import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.dao.tenant.TenantService; import java.util.ArrayList; @@ -97,7 +96,6 @@ public class BaseAlarmService extends AbstractCachedEntityService alarmDataValidator; @TransactionalEventListener(classes = AlarmTypesCacheEvictEvent.class) @Override @@ -169,6 +167,11 @@ public class BaseAlarmService extends AbstractCachedEntityService findLatestActiveByOriginatorAndTypeAsync(TenantId tenantId, EntityId originator, String type) { + return alarmDao.findLatestActiveByOriginatorAndTypeAsync(tenantId, originator, type); + } + @Override public PageData findAlarmDataByQueryForEntities(TenantId tenantId, AlarmDataQuery query, Collection orderedEntityIds) { @@ -438,12 +441,6 @@ public class BaseAlarmService extends AbstractCachedEntityService T getAndUpdate(TenantId tenantId, AlarmId alarmId, Function function) { - validateId(alarmId, "Alarm id should be specified!"); - Alarm entity = alarmDao.findAlarmById(tenantId, alarmId.getId()); - return function.apply(entity); - } - @Override public Optional> findEntity(TenantId tenantId, EntityId entityId) { return Optional.ofNullable(findAlarmById(tenantId, new AlarmId(entityId.getId()))); 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 0c5df18e80..c0cb886747 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 @@ -58,6 +58,22 @@ public class BaseCalculatedFieldService extends AbstractEntityService implements @Override public CalculatedField save(CalculatedField calculatedField) { CalculatedField oldCalculatedField = calculatedFieldDataValidator.validate(calculatedField, CalculatedField::getTenantId); + return doSave(calculatedField, oldCalculatedField); + } + + @Override + public CalculatedField save(CalculatedField calculatedField, boolean doValidate) { + CalculatedField oldCalculatedField = null; + if (doValidate) { + oldCalculatedField = calculatedFieldDataValidator.validate(calculatedField, CalculatedField::getTenantId); + } else if (calculatedField.getId() != null) { + oldCalculatedField = findById(calculatedField.getTenantId(), calculatedField.getId()); + } + return doSave(calculatedField, oldCalculatedField); + } + + + private CalculatedField doSave(CalculatedField calculatedField, CalculatedField oldCalculatedField) { try { TenantId tenantId = calculatedField.getTenantId(); log.trace("Executing save calculated field, [{}]", calculatedField); @@ -83,6 +99,13 @@ public class BaseCalculatedFieldService extends AbstractEntityService implements return calculatedFieldDao.findById(tenantId, calculatedFieldId.getId()); } + @Override + public CalculatedField findByEntityIdAndName(EntityId entityId, String name) { + log.trace("Executing findByEntityIdAndName [{}], calculatedFieldName[{}]", entityId, name); + validateId(entityId.getId(), id -> INCORRECT_ENTITY_ID + id); + return calculatedFieldDao.findByEntityIdAndName(entityId, name); + } + @Override public List findCalculatedFieldIdsByEntityId(TenantId tenantId, EntityId entityId) { log.trace("Executing findCalculatedFieldIdsByEntityId [{}]", entityId); 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 aadae93893..d5465cb8a1 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 @@ -35,6 +35,8 @@ public interface CalculatedFieldDao extends Dao { List findAll(); + CalculatedField findByEntityIdAndName(EntityId entityId, String name); + PageData findAll(PageLink pageLink); PageData findAllByTenantId(TenantId tenantId, PageLink pageLink); diff --git a/dao/src/main/java/org/thingsboard/server/dao/edge/PostgresEdgeEventService.java b/dao/src/main/java/org/thingsboard/server/dao/edge/PostgresEdgeEventService.java index 90939858bc..ecf1376a73 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/edge/PostgresEdgeEventService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/edge/PostgresEdgeEventService.java @@ -28,8 +28,11 @@ import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Service; import org.thingsboard.common.util.ThingsBoardThreadFactory; import org.thingsboard.server.common.data.edge.EdgeEvent; +import org.thingsboard.server.dao.edge.stats.EdgeStatsCounterService; +import org.thingsboard.server.dao.edge.stats.EdgeStatsKey; import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent; +import java.util.Optional; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -41,6 +44,7 @@ public class PostgresEdgeEventService extends BaseEdgeEventService { private final EdgeEventDao edgeEventDao; private final ApplicationEventPublisher eventPublisher; + private final Optional statsCounterService; private ExecutorService edgeEventExecutor; @@ -64,6 +68,7 @@ public class PostgresEdgeEventService extends BaseEdgeEventService { Futures.addCallback(saveFuture, new FutureCallback<>() { @Override public void onSuccess(Void result) { + statsCounterService.ifPresent(statsCounterService -> statsCounterService.recordEvent(EdgeStatsKey.DOWNLINK_MSGS_ADDED, edgeEvent.getTenantId(), edgeEvent.getEdgeId(), 1)); eventPublisher.publishEvent(SaveEntityEvent.builder() .tenantId(edgeEvent.getTenantId()) .entityId(edgeEvent.getEdgeId()) diff --git a/dao/src/main/java/org/thingsboard/server/dao/edge/stats/EdgeStatsCounterService.java b/dao/src/main/java/org/thingsboard/server/dao/edge/stats/EdgeStatsCounterService.java new file mode 100644 index 0000000000..16111cf514 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/edge/stats/EdgeStatsCounterService.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.dao.edge.stats; + +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.stereotype.Service; +import org.thingsboard.server.common.data.id.EdgeId; +import org.thingsboard.server.common.data.id.TenantId; + +import java.util.concurrent.ConcurrentHashMap; + +@ConditionalOnProperty(prefix = "edges.stats", name = "enabled", havingValue = "true", matchIfMissing = false) +@Service +@Slf4j +@Getter +public class EdgeStatsCounterService { + + private final ConcurrentHashMap counterByEdge = new ConcurrentHashMap<>(); + + public void recordEvent(EdgeStatsKey type, TenantId tenantId, EdgeId edgeId, long value) { + MsgCounters counters = getOrCreateCounters(tenantId, edgeId); + switch (type) { + case DOWNLINK_MSGS_ADDED -> counters.getMsgsAdded().addAndGet(value); + case DOWNLINK_MSGS_PUSHED -> counters.getMsgsPushed().addAndGet(value); + case DOWNLINK_MSGS_PERMANENTLY_FAILED -> counters.getMsgsPermanentlyFailed().addAndGet(value); + case DOWNLINK_MSGS_TMP_FAILED -> counters.getMsgsTmpFailed().addAndGet(value); + } + } + + public void setDownlinkMsgsLag(TenantId tenantId, EdgeId edgeId, long value) { + getOrCreateCounters(tenantId, edgeId).getMsgsLag().set(value); + } + + public void clear(EdgeId edgeId) { + counterByEdge.remove(edgeId); + } + + public MsgCounters getOrCreateCounters(TenantId tenantId, EdgeId edgeId) { + return counterByEdge.computeIfAbsent(edgeId, id -> new MsgCounters(tenantId)); + } + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/edge/stats/EdgeStatsKey.java b/dao/src/main/java/org/thingsboard/server/dao/edge/stats/EdgeStatsKey.java new file mode 100644 index 0000000000..e017f6969e --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/edge/stats/EdgeStatsKey.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.dao.edge.stats; + +import lombok.Getter; + +@Getter +public enum EdgeStatsKey { + DOWNLINK_MSGS_ADDED("downlinkMsgsAdded"), + DOWNLINK_MSGS_PUSHED("downlinkMsgsPushed"), + DOWNLINK_MSGS_PERMANENTLY_FAILED("downlinkMsgsPermanentlyFailed"), + DOWNLINK_MSGS_TMP_FAILED("downlinkMsgsTmpFailed"), + DOWNLINK_MSGS_LAG("downlinkMsgsLag"); + + private final String key; + + EdgeStatsKey(String key) { + this.key = key; + } + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/edge/stats/MsgCounters.java b/dao/src/main/java/org/thingsboard/server/dao/edge/stats/MsgCounters.java new file mode 100644 index 0000000000..7596da0ecf --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/edge/stats/MsgCounters.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.edge.stats; + +import lombok.Data; +import org.thingsboard.server.common.data.id.TenantId; + +import java.util.concurrent.atomic.AtomicLong; + +@Data +public class MsgCounters { + + private final TenantId tenantId; + private final AtomicLong msgsAdded = new AtomicLong(); + private final AtomicLong msgsPushed = new AtomicLong(); + private final AtomicLong msgsPermanentlyFailed = new AtomicLong(); + private final AtomicLong msgsTmpFailed = new AtomicLong(); + private final AtomicLong msgsLag = new AtomicLong(); + + public void clear() { + msgsAdded.set(0); + msgsPushed.set(0); + msgsPermanentlyFailed.set(0); + msgsTmpFailed.set(0); + msgsLag.set(0); + } + +} 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..a34e968acc 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 @@ -81,7 +81,7 @@ public abstract class AbstractEntityService { @Autowired @Lazy - private TbTenantProfileCache tbTenantProfileCache; + protected TbTenantProfileCache tbTenantProfileCache; @Value("${debug.settings.default_duration:15}") private int defaultDebugDurationMinutes; diff --git a/dao/src/main/java/org/thingsboard/server/dao/housekeeper/CleanUpService.java b/dao/src/main/java/org/thingsboard/server/dao/housekeeper/CleanUpService.java index 0340cd0fde..2f23d20f00 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/housekeeper/CleanUpService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/housekeeper/CleanUpService.java @@ -47,7 +47,7 @@ public class CleanUpService { private final Set skippedEntities = EnumSet.of( EntityType.ALARM, EntityType.QUEUE, EntityType.TB_RESOURCE, EntityType.OTA_PACKAGE, EntityType.NOTIFICATION_REQUEST, EntityType.NOTIFICATION_TEMPLATE, - EntityType.NOTIFICATION_TARGET, EntityType.NOTIFICATION_RULE + EntityType.NOTIFICATION_TARGET, EntityType.NOTIFICATION_RULE, EntityType.AI_MODEL ); @TransactionalEventListener(fallbackExecution = true) // after transaction commit diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/BaseSqlEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/BaseSqlEntity.java index 084df4455c..dda45539f9 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/BaseSqlEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/BaseSqlEntity.java @@ -27,6 +27,7 @@ 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.dao.DaoUtil; +import org.thingsboard.server.dao.sql.IdGenerator.GeneratedId; import java.util.Arrays; import java.util.Collections; @@ -44,6 +45,7 @@ public abstract class BaseSqlEntity implements BaseEntity { @Id @Column(name = ModelConstants.ID_PROPERTY, columnDefinition = "uuid") + @GeneratedId protected UUID id; @Column(name = ModelConstants.CREATED_TIME_PROPERTY, updatable = false) 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 23324eccb5..3960c67525 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 @@ -457,6 +457,7 @@ public class ModelConstants { */ public static final String MOBILE_APP_TABLE_NAME = "mobile_app"; public static final String MOBILE_APP_PKG_NAME_PROPERTY = "pkg_name"; + public static final String MOBILE_APP_TITLE_PROPERTY = "title"; public static final String MOBILE_APP_APP_SECRET_PROPERTY = "app_secret"; public static final String MOBILE_APP_PLATFORM_TYPE_PROPERTY = "platform_type"; public static final String MOBILE_APP_STATUS_PROPERTY = "status"; @@ -751,6 +752,14 @@ public class ModelConstants { public static final String JOB_CONFIGURATION_PROPERTY = "configuration"; public static final String JOB_RESULT_PROPERTY = "result"; + /** + * AI model constants. + */ + public static final String AI_MODEL_TABLE_NAME = "ai_model"; + public static final String AI_MODEL_TENANT_ID_COLUMN_NAME = TENANT_ID_COLUMN; + public static final String AI_MODEL_NAME_COLUMN_NAME = NAME_PROPERTY; + public static final String AI_MODEL_CONFIGURATION_COLUMN_NAME = "configuration"; + protected static final String[] NONE_AGGREGATION_COLUMNS = new String[]{LONG_VALUE_COLUMN, DOUBLE_VALUE_COLUMN, BOOLEAN_VALUE_COLUMN, STRING_VALUE_COLUMN, JSON_VALUE_COLUMN, KEY_COLUMN, TS_COLUMN}; protected static final String[] COUNT_AGGREGATION_COLUMNS = new String[]{count(LONG_VALUE_COLUMN), count(DOUBLE_VALUE_COLUMN), count(BOOLEAN_VALUE_COLUMN), count(STRING_VALUE_COLUMN), count(JSON_VALUE_COLUMN), max(TS_COLUMN)}; diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/AiModelEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AiModelEntity.java new file mode 100644 index 0000000000..d4f3d36db6 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AiModelEntity.java @@ -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. + */ +package org.thingsboard.server.dao.model.sql; + +import io.hypersistence.utils.hibernate.type.json.JsonBinaryType; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Table; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import org.hibernate.annotations.Type; +import org.hibernate.proxy.HibernateProxy; +import org.thingsboard.server.common.data.ai.AiModel; +import org.thingsboard.server.common.data.ai.model.AiModelConfig; +import org.thingsboard.server.common.data.id.AiModelId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.dao.model.BaseVersionedEntity; +import org.thingsboard.server.dao.model.ModelConstants; + +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.UUID; + +@Getter +@Setter +@ToString +@Entity +@Table(name = ModelConstants.AI_MODEL_TABLE_NAME) +public class AiModelEntity extends BaseVersionedEntity { + + public static final Map COLUMN_MAP = Map.of( + "createdTime", "created_time", + "provider", "(configuration ->> 'provider')", + "modelId", "(configuration ->> 'modelId')" + ); + + public static final Set ALLOWED_SORT_PROPERTIES = Collections.unmodifiableSet( + new LinkedHashSet<>(List.of("createdTime", "name", "provider", "modelId")) + ); + + @Column(name = ModelConstants.AI_MODEL_TENANT_ID_COLUMN_NAME, nullable = false, columnDefinition = "UUID") + private UUID tenantId; + + @Column(name = ModelConstants.AI_MODEL_NAME_COLUMN_NAME, nullable = false) + private String name; + + @Type(JsonBinaryType.class) + @Column(name = ModelConstants.AI_MODEL_CONFIGURATION_COLUMN_NAME, nullable = false, columnDefinition = "JSONB") + private AiModelConfig configuration; + + @Column(name = ModelConstants.EXTERNAL_ID_PROPERTY, columnDefinition = "UUID") + private UUID externalId; + + public AiModelEntity() {} + + public AiModelEntity(AiModel aiModel) { + super(aiModel); + tenantId = getTenantUuid(aiModel.getTenantId()); + name = aiModel.getName(); + configuration = aiModel.getConfiguration(); + externalId = getUuid(aiModel.getExternalId()); + } + + @Override + public AiModel toData() { + var model = new AiModel(new AiModelId(id)); + model.setCreatedTime(createdTime); + model.setVersion(version); + model.setTenantId(TenantId.fromUUID(tenantId)); + model.setName(name); + model.setConfiguration(configuration); + model.setExternalId(getEntityId(externalId, AiModelId::new)); + return model; + } + + @Override + public final boolean equals(Object o) { + if (this == o) return true; + if (o == null) return false; + Class oEffectiveClass = o instanceof HibernateProxy ? ((HibernateProxy) o).getHibernateLazyInitializer().getPersistentClass() : o.getClass(); + Class thisEffectiveClass = this instanceof HibernateProxy ? ((HibernateProxy) this).getHibernateLazyInitializer().getPersistentClass() : this.getClass(); + if (thisEffectiveClass != oEffectiveClass) return false; + AiModelEntity that = (AiModelEntity) o; + return getId() != null && Objects.equals(getId(), that.getId()); + } + + @Override + public final int hashCode() { + return this instanceof HibernateProxy ? ((HibernateProxy) this).getHibernateLazyInitializer().getPersistentClass().hashCode() : getClass().hashCode(); + } + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/MobileAppEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/MobileAppEntity.java index 70f711750d..d9c22c6e9b 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/MobileAppEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/MobileAppEntity.java @@ -53,6 +53,9 @@ public class MobileAppEntity extends BaseSqlEntity { @Column(name = ModelConstants.MOBILE_APP_PKG_NAME_PROPERTY) private String pkgName; + @Column(name = ModelConstants.MOBILE_APP_TITLE_PROPERTY) + private String title; + @Column(name = ModelConstants.MOBILE_APP_APP_SECRET_PROPERTY) private String appSecret; @@ -82,6 +85,7 @@ public class MobileAppEntity extends BaseSqlEntity { this.tenantId = mobile.getTenantId().getId(); } this.pkgName = mobile.getPkgName(); + this.title = mobile.getTitle(); this.appSecret = mobile.getAppSecret(); this.platformType = mobile.getPlatformType(); this.status = mobile.getStatus(); @@ -98,6 +102,7 @@ public class MobileAppEntity extends BaseSqlEntity { } mobile.setCreatedTime(createdTime); mobile.setPkgName(pkgName); + mobile.setTitle(title); mobile.setAppSecret(appSecret); mobile.setPlatformType(platformType); mobile.setStatus(status); diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/OtaPackageEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/OtaPackageEntity.java index 1ffafd305e..6ee4bdd89f 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/OtaPackageEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/OtaPackageEntity.java @@ -38,6 +38,7 @@ import org.thingsboard.server.dao.util.mapping.JsonConverter; import java.nio.ByteBuffer; import java.util.UUID; +import static org.thingsboard.server.dao.model.ModelConstants.EXTERNAL_ID_PROPERTY; import static org.thingsboard.server.dao.model.ModelConstants.OTA_PACKAGE_CHECKSUM_ALGORITHM_COLUMN; import static org.thingsboard.server.dao.model.ModelConstants.OTA_PACKAGE_CHECKSUM_COLUMN; import static org.thingsboard.server.dao.model.ModelConstants.OTA_PACKAGE_CONTENT_TYPE_COLUMN; @@ -105,6 +106,9 @@ public class OtaPackageEntity extends BaseSqlEntity { @Column(name = ModelConstants.OTA_PACKAGE_ADDITIONAL_INFO_COLUMN) private JsonNode additionalInfo; + @Column(name = EXTERNAL_ID_PROPERTY) + private UUID externalId; + public OtaPackageEntity() { super(); } @@ -128,6 +132,7 @@ public class OtaPackageEntity extends BaseSqlEntity { this.data = otaPackage.getData().array(); this.dataSize = otaPackage.getDataSize(); this.additionalInfo = otaPackage.getAdditionalInfo(); + this.externalId = getUuid(otaPackage.getExternalId()); } @Override @@ -153,6 +158,8 @@ public class OtaPackageEntity extends BaseSqlEntity { otaPackage.setHasData(true); } otaPackage.setAdditionalInfo(additionalInfo); + otaPackage.setExternalId(getEntityId(externalId, OtaPackageId::new)); return otaPackage; } + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/OtaPackageInfoEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/OtaPackageInfoEntity.java index c1e83cf511..a1625f2595 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/OtaPackageInfoEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/OtaPackageInfoEntity.java @@ -100,6 +100,9 @@ public class OtaPackageInfoEntity extends BaseSqlEntity { @Column(name = ModelConstants.OTA_PACKAGE_ADDITIONAL_INFO_COLUMN) private JsonNode additionalInfo; + @Column(name = ModelConstants.EXTERNAL_ID_PROPERTY) + private UUID externalId; + @Transient private boolean hasData; @@ -125,11 +128,12 @@ public class OtaPackageInfoEntity extends BaseSqlEntity { this.checksum = otaPackageInfo.getChecksum(); this.dataSize = otaPackageInfo.getDataSize(); this.additionalInfo = otaPackageInfo.getAdditionalInfo(); + this.externalId = getUuid(otaPackageInfo.getExternalId()); } public OtaPackageInfoEntity(UUID id, long createdTime, UUID tenantId, UUID deviceProfileId, OtaPackageType type, String title, String version, String tag, String url, String fileName, String contentType, ChecksumAlgorithm checksumAlgorithm, String checksum, Long dataSize, - Object additionalInfo, boolean hasData) { + Object additionalInfo, UUID externalId, boolean hasData) { this.id = id; this.createdTime = createdTime; this.tenantId = tenantId; @@ -146,6 +150,7 @@ public class OtaPackageInfoEntity extends BaseSqlEntity { this.dataSize = dataSize; this.hasData = hasData; this.additionalInfo = JacksonUtil.convertValue(additionalInfo, JsonNode.class); + this.externalId = externalId; } @Override @@ -168,6 +173,8 @@ public class OtaPackageInfoEntity extends BaseSqlEntity { otaPackageInfo.setDataSize(dataSize); otaPackageInfo.setAdditionalInfo(additionalInfo); otaPackageInfo.setHasData(hasData); + otaPackageInfo.setExternalId(getEntityId(externalId, OtaPackageId::new)); return otaPackageInfo; } + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/oauth2/HybridClientRegistrationRepository.java b/dao/src/main/java/org/thingsboard/server/dao/oauth2/HybridClientRegistrationRepository.java index bc84a58cd9..70112fa72f 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/oauth2/HybridClientRegistrationRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/oauth2/HybridClientRegistrationRepository.java @@ -29,6 +29,7 @@ import java.util.UUID; @Component public class HybridClientRegistrationRepository implements ClientRegistrationRepository { + private static final String defaultRedirectUriTemplate = "{baseUrl}/login/oauth2/code/{registrationId}"; @Autowired @@ -37,11 +38,13 @@ public class HybridClientRegistrationRepository implements ClientRegistrationRep @Override public ClientRegistration findByRegistrationId(String registrationId) { OAuth2Client oAuth2Client = oAuth2ClientService.findOAuth2ClientById(TenantId.SYS_TENANT_ID, new OAuth2ClientId(UUID.fromString(registrationId))); - return oAuth2Client == null ? - null : toSpringClientRegistration(oAuth2Client); + if (oAuth2Client == null) { + return null; + } + return toSpringClientRegistration(oAuth2Client); } - private ClientRegistration toSpringClientRegistration(OAuth2Client oAuth2Client){ + private ClientRegistration toSpringClientRegistration(OAuth2Client oAuth2Client) { String registrationId = oAuth2Client.getUuidId().toString(); // NONE is used if we need pkce-based code challenge @@ -67,4 +70,5 @@ public class HybridClientRegistrationRepository implements ClientRegistrationRep .redirectUri(defaultRedirectUriTemplate) .build(); } + } 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 16d8517b6d..343a2485ce 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 @@ -54,6 +54,7 @@ import static org.thingsboard.server.dao.service.Validator.validatePageLink; @Slf4j @RequiredArgsConstructor public class BaseOtaPackageService extends AbstractCachedEntityService implements OtaPackageService { + public static final String INCORRECT_OTA_PACKAGE_ID = "Incorrect otaPackageId "; public static final String INCORRECT_TENANT_ID = "Incorrect tenantId "; @@ -73,7 +74,7 @@ public class BaseOtaPackageService extends AbstractCachedEntityService Hashing.md5(); + case SHA256 -> Hashing.sha256(); + case SHA384 -> Hashing.sha384(); + case SHA512 -> Hashing.sha512(); + case CRC32 -> Hashing.crc32(); + case MURMUR3_32 -> Hashing.murmur3_32(); + case MURMUR3_128 -> Hashing.murmur3_128(); + default -> throw new DataValidationException("Unknown checksum algorithm!"); + }; } @Override @@ -171,6 +160,12 @@ public class BaseOtaPackageService extends AbstractCachedEntityService otaPackageInfoDao.findById(tenantId, otaPackageId.getId()), true); } + @Override + public OtaPackage findOtaPackageByTenantIdAndTitleAndVersion(TenantId tenantId, String title, String version) { + log.trace("Executing findOtaPackageByTenantIdAndTitle [{}] [{}] [{}]", tenantId, title, version); + return otaPackageDao.findOtaPackageByTenantIdAndTitleAndVersion(tenantId, title, version); + } + @Override public ListenableFuture findOtaPackageInfoByIdAsync(TenantId tenantId, OtaPackageId otaPackageId) { log.trace("Executing findOtaPackageInfoByIdAsync [{}]", otaPackageId); diff --git a/dao/src/main/java/org/thingsboard/server/dao/ota/OtaPackageDao.java b/dao/src/main/java/org/thingsboard/server/dao/ota/OtaPackageDao.java index f8f877e55e..cc1ff58ded 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/ota/OtaPackageDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/ota/OtaPackageDao.java @@ -16,12 +16,17 @@ package org.thingsboard.server.dao.ota; import org.thingsboard.server.common.data.OtaPackage; +import org.thingsboard.server.common.data.id.OtaPackageId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.ota.OtaPackageType; import org.thingsboard.server.dao.Dao; +import org.thingsboard.server.dao.ExportableEntityDao; import org.thingsboard.server.dao.TenantEntityWithDataDao; -public interface OtaPackageDao extends Dao, TenantEntityWithDataDao { +public interface OtaPackageDao extends Dao, TenantEntityWithDataDao, ExportableEntityDao { Long sumDataSizeByTenantId(TenantId tenantId); + OtaPackage findOtaPackageByTenantIdAndTitleAndVersion(TenantId tenantId, String title, String version); + } 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 bf73c59708..bf941256f5 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 @@ -263,7 +263,7 @@ public class BaseResourceService extends AbstractCachedEntityService INCORRECT_RESOURCE_ID + id); - return cache.getAndPutInTransaction(new ResourceInfoCacheKey(tenantId, resourceId), + return cache.getAndPutInTransaction(new ResourceInfoCacheKey(resourceId), () -> resourceInfoDao.findById(tenantId, resourceId.getId()), true); } @@ -712,7 +712,7 @@ public class BaseResourceService extends AbstractCachedEntityService { + + @Override + public boolean isValid(ObjectNode schema, ConstraintValidatorContext context) { + return schema == null || JsonSchemaUtils.isValidJsonSchema(schema); + } + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/NoNullCharValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/NoNullCharValidator.java new file mode 100644 index 0000000000..afbcabe91b --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/service/NoNullCharValidator.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.dao.service; + +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; +import org.thingsboard.server.common.data.validation.NoNullChar; + +public final class NoNullCharValidator implements ConstraintValidator { + + @Override + public boolean isValid(String value, ConstraintValidatorContext context) { + return value == null || !value.contains("\u0000"); + } + +} 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.java b/dao/src/main/java/org/thingsboard/server/dao/service/Validator.java index e051e99dc5..f94026ce89 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/Validator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/Validator.java @@ -26,11 +26,14 @@ import org.thingsboard.server.common.data.query.EntityKeyType; import org.thingsboard.server.dao.exception.IncorrectParameterException; import java.util.List; +import java.util.Set; import java.util.UUID; import java.util.function.Function; import java.util.regex.Pattern; -public class Validator { +public final class Validator { + + private Validator() {} public static final Pattern PROPERTY_PATTERN = Pattern.compile("^[\\p{L}0-9_-]+$"); // Unicode letters, numbers, '_' and '-' allowed @@ -204,22 +207,61 @@ public class Validator { } /** - * This method validate PageLink page link. If pageLink is invalid than throw - * IncorrectParameterException exception + * Validates the specified PageLink object delegating to {@link #validatePageLink(PageLink, Set)} + * with no restrictions on allowed sort properties. * - * @param pageLink the page link + * @param pageLink the PageLink object to validate + * @throws IncorrectParameterException if the pageLink is null, has invalid page size, + * invalid page number, or invalid sort property + * @see #validatePageLink(PageLink, Set) */ public static void validatePageLink(PageLink pageLink) { + validatePageLink(pageLink, null); + } + + /** + * Validates the specified PageLink object ensuring that: + *
    + *
  • The PageLink object is not null
  • + *
  • The page size is greater than zero
  • + *
  • The page number is non-negative
  • + *
  • If sorting is specified, the sort property is valid and allowed
  • + *
+ * + *

When {@code allowedSortProperties} is provided, the sort property + * must be contained within this set. If {@code allowedSortProperties} is null, + * only basic sort property validation is performed. + * + * @param pageLink the PageLink object to validate. + * @param allowedSortProperties a Set of allowed sort property names, or null to skip + * this validation. If provided and the PageLink contains + * a sort order, the sort property must be in this set. + * @throws IncorrectParameterException if any of the following conditions are met: + *

    + *
  • {@code pageLink} is null
  • + *
  • page size is less than 1
  • + *
  • page number is negative
  • + *
  • sort property is malformed
  • + *
  • sort property is not in the {@code allowedSortProperties} set (when the set is provided and not null)
  • + *
+ */ + public static void validatePageLink(PageLink pageLink, Set allowedSortProperties) { if (pageLink == null) { throw new IncorrectParameterException("Page link must be specified."); } else if (pageLink.getPageSize() < 1) { - throw new IncorrectParameterException("Incorrect page link page size '"+pageLink.getPageSize()+"'. Page size must be greater than zero."); + throw new IncorrectParameterException("Incorrect page link page size '" + pageLink.getPageSize() + "'. Page size must be greater than zero."); } else if (pageLink.getPage() < 0) { - throw new IncorrectParameterException("Incorrect page link page '"+pageLink.getPage()+"'. Page must be positive integer."); + throw new IncorrectParameterException("Incorrect page link page '" + pageLink.getPage() + "'. Page must be positive integer."); } else if (pageLink.getSortOrder() != null) { - if (!isValidProperty(pageLink.getSortOrder().getProperty())) { + String sortProperty = pageLink.getSortOrder().getProperty(); + if (!isValidProperty(sortProperty)) { throw new IncorrectParameterException("Invalid page link sort property"); } + if (allowedSortProperties != null && !allowedSortProperties.contains(sortProperty)) { + throw new IncorrectParameterException( + "Unsupported sort property '" + sortProperty + "'. Only '" + String.join("', '", allowedSortProperties) + "' are allowed." + ); + } } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/AiModelDataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/AiModelDataValidator.java new file mode 100644 index 0000000000..fdccf2955f --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/service/validator/AiModelDataValidator.java @@ -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. + */ +package org.thingsboard.server.dao.service.validator; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.ai.AiModel; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.dao.ai.AiModelDao; +import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.dao.service.DataValidator; +import org.thingsboard.server.dao.tenant.TenantService; + +import java.util.Optional; + +@Component +@RequiredArgsConstructor +class AiModelDataValidator extends DataValidator { + + private final TenantService tenantService; + private final AiModelDao aiModelDao; + + @Override + protected AiModel validateUpdate(TenantId tenantId, AiModel model) { + Optional existing = aiModelDao.findByTenantIdAndId(tenantId, model.getId()); + if (existing.isEmpty()) { + throw new DataValidationException("Cannot update non-existent AI model!"); + } + return existing.get(); + } + + @Override + protected void validateDataImpl(TenantId tenantId, AiModel model) { + // ID validation + if (model.getId() != null) { + if (model.getUuidId() == null) { + throw new DataValidationException("AI model UUID should be specified!"); + } + if (model.getId().isNullUid()) { + throw new DataValidationException("AI model UUID must not be the reserved null value!"); + } + } + + // tenant ID validation + if (model.getTenantId() == null || model.getTenantId().getId() == null) { + throw new DataValidationException("AI model should be assigned to tenant!"); + } + if (model.getTenantId().isSysTenantId()) { + throw new DataValidationException("AI model cannot be assigned to the system tenant!"); + } + if (!tenantService.tenantExists(tenantId)) { + throw new DataValidationException("AI model reference a non-existent tenant!"); + } + } + +} 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..05b782c26c 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,9 @@ 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.configuration.ArgumentsBasedCalculatedFieldConfiguration; +import org.thingsboard.server.common.data.cf.configuration.RelationQueryDynamicSourceConfiguration; +import org.thingsboard.server.common.data.cf.configuration.ScheduledUpdateSupportedCalculatedFieldConfiguration; 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 +28,9 @@ 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.stream.Collectors; + @Component public class CalculatedFieldDataValidator extends DataValidator { @@ -36,10 +41,22 @@ 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); + } + + @Override + protected void validateCreate(TenantId tenantId, CalculatedField calculatedField) { + long maxCFsPerEntity = apiLimitService.getLimit(tenantId, DefaultTenantProfileConfiguration::getMaxCalculatedFieldsPerEntity); + if (maxCFsPerEntity <= 0) { + return; + } + if (calculatedFieldDao.countCFByEntityId(tenantId, calculatedField.getEntityId()) >= maxCFsPerEntity) { + throw new DataValidationException("Calculated fields per entity limit reached!"); + } } @Override @@ -48,34 +65,56 @@ public class CalculatedFieldDataValidator extends DataValidator if (old == null) { throw new DataValidationException("Can't update non existing calculated field!"); } - validateNumberOfArgumentsPerCF(tenantId, calculatedField); - validateArgumentNames(calculatedField); return old; } - private void validateNumberOfCFsPerEntity(TenantId tenantId, EntityId entityId) { - long maxCFsPerEntity = apiLimitService.getLimit(tenantId, DefaultTenantProfileConfiguration::getMaxCalculatedFieldsPerEntity); - if (maxCFsPerEntity <= 0) { + private void validateNumberOfArgumentsPerCF(TenantId tenantId, CalculatedField calculatedField) { + if (!(calculatedField instanceof ArgumentsBasedCalculatedFieldConfiguration argumentsBasedCfg)) { return; } - if (calculatedFieldDao.countCFByEntityId(tenantId, entityId) >= maxCFsPerEntity) { - throw new DataValidationException("Calculated fields per entity limit reached!"); - } - } - - private void validateNumberOfArgumentsPerCF(TenantId tenantId, CalculatedField calculatedField) { 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().hasDynamicSource()) + .collect(Collectors.toMap(Map.Entry::getKey, entry -> (RelationQueryDynamicSourceConfiguration) 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 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/OtaPackageDataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/OtaPackageDataValidator.java index 47911a8966..c4d3f7255d 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/validator/OtaPackageDataValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/validator/OtaPackageDataValidator.java @@ -103,4 +103,5 @@ public class OtaPackageDataValidator extends BaseOtaPackageDataValidator { - /** - * Save or update admin settings object - * - * @param adminSettings the admin settings object - * @return saved admin settings object - */ AdminSettings save(TenantId tenantId, AdminSettings adminSettings); - - /** - * Find admin settings by key. - * - * @param key the key - * @return the admin settings object - */ + AdminSettings findByTenantIdAndKey(UUID tenantId, String key); boolean removeByTenantIdAndKey(UUID tenantId, String key); 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 b1b3b25a19..604a37f4bb 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 @@ -21,11 +21,16 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.AdminSettings; +import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.id.AdminSettingsId; +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.dao.service.DataValidator; import org.thingsboard.server.dao.service.Validator; +import java.util.Optional; + @Service @Slf4j public class AdminSettingsServiceImpl implements AdminSettingsService { @@ -87,10 +92,25 @@ public class AdminSettingsServiceImpl implements AdminSettingsService { } @Override - public void deleteAdminSettingsByTenantId(TenantId tenantId) { + public void deleteByTenantId(TenantId tenantId) { adminSettingsDao.removeByTenantId(tenantId.getId()); } + @Override + public void deleteEntity(TenantId tenantId, EntityId id, boolean force) { + adminSettingsDao.removeById(tenantId, id.getId()); + } + + @Override + public Optional> findEntity(TenantId tenantId, EntityId entityId) { + return Optional.ofNullable(adminSettingsDao.findById(tenantId, entityId.getId())); + } + + @Override + public EntityType getEntityType() { + return EntityType.ADMIN_SETTINGS; + } + private void dropTokenIfProviderInfoChanged(JsonNode newJsonValue, JsonNode oldJsonValue) { if (newJsonValue.has("enableOauth2") && newJsonValue.get("enableOauth2").asBoolean()) { if (!newJsonValue.get("providerId").equals(oldJsonValue.get("providerId")) || diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/IdGenerator.java b/dao/src/main/java/org/thingsboard/server/dao/sql/IdGenerator.java new file mode 100644 index 0000000000..dc8a0da32f --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/IdGenerator.java @@ -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. + */ +package org.thingsboard.server.dao.sql; + +import com.datastax.oss.driver.api.core.uuid.Uuids; +import lombok.extern.slf4j.Slf4j; +import org.hibernate.annotations.IdGeneratorType; +import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.generator.BeforeExecutionGenerator; +import org.hibernate.generator.EventType; +import org.hibernate.generator.EventTypeSets; +import org.thingsboard.server.dao.model.BaseEntity; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.EnumSet; + +@Slf4j +public class IdGenerator implements BeforeExecutionGenerator { + + @Override + public Object generate(SharedSessionContractImplementor session, Object owner, Object currentValue, EventType eventType) { + if (owner instanceof BaseEntity entity && entity.getUuid() != null) { + return entity.getUuid(); + } + return Uuids.timeBased(); + } + + @Override + public boolean allowAssignedIdentifiers() { + return true; + } + + @Override + public EnumSet getEventTypes() { + return EventTypeSets.INSERT_ONLY; + } + + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.FIELD) + @IdGeneratorType(IdGenerator.class) + public @interface GeneratedId { + } + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/JpaAbstractDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/JpaAbstractDao.java index 05577b68e1..fd37b3c9dd 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/JpaAbstractDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/JpaAbstractDao.java @@ -22,6 +22,7 @@ import jakarta.persistence.EntityManager; import jakarta.persistence.OptimisticLockException; import jakarta.persistence.PersistenceContext; import lombok.extern.slf4j.Slf4j; +import org.hibernate.Session; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.jdbc.core.JdbcTemplate; @@ -40,9 +41,6 @@ import java.util.List; import java.util.Optional; import java.util.UUID; -/** - * @author Valerii Sosliuk - */ @Slf4j @SqlDao public abstract class JpaAbstractDao, D> @@ -72,9 +70,15 @@ public abstract class JpaAbstractDao, D> log.debug("Saving entity {}", entity); boolean isNew = entity.getUuid() == null; if (isNew) { - UUID uuid = Uuids.timeBased(); - entity.setUuid(uuid); - entity.setCreatedTime(Uuids.unixTimestamp(uuid)); + entity.setCreatedTime(System.currentTimeMillis()); + } else { + if (entity.getCreatedTime() == 0) { + if (entity.getUuid().version() == 1) { + entity.setCreatedTime(Uuids.unixTimestamp(entity.getUuid())); + } else { + entity.setCreatedTime(System.currentTimeMillis()); + } + } } try { entity = doSave(entity, isNew, flush); @@ -88,35 +92,16 @@ public abstract class JpaAbstractDao, D> boolean flushed = false; EntityManager entityManager = getEntityManager(); if (isNew) { - entityManager.persist(entity); - if (entity instanceof HasVersion versionedEntity) { - versionedEntity.setVersion(1L); - } + entity = create(entity); } else { - if (entity instanceof HasVersion versionedEntity) { - if (versionedEntity.getVersion() == null) { - HasVersion existingEntity = entityManager.find(versionedEntity.getClass(), entity.getUuid()); - if (existingEntity != null) { - /* - * manually resetting the version to latest to allow force overwrite of the entity - * */ - versionedEntity.setVersion(existingEntity.getVersion()); - } else { - return doSave(entity, true, flush); - } - } - versionedEntity = entityManager.merge(versionedEntity); - entity = (E) versionedEntity; - /* - * by default, Hibernate doesn't issue an update query and thus version increment - * if the entity was not modified. to bypass this and always increment the version, we do it manually - * */ - versionedEntity.setVersion(versionedEntity.getVersion() + 1); - } else { - entity = entityManager.merge(entity); - } + entity = update(entity); } if (entity instanceof HasVersion versionedEntity) { + /* + * by default, Hibernate doesn't issue an update query and thus version increment + * if the entity was not modified. to bypass this and always increment the version, we do it manually + * */ + versionedEntity.setVersion(versionedEntity.getVersion() + 1); /* * flushing and then removing the entity from the persistence context so that it is not affected * by next flushes (e.g. when a transaction is committed) to avoid double version increment @@ -131,6 +116,48 @@ public abstract class JpaAbstractDao, D> return entity; } + private E create(E entity) { + if (entity instanceof HasVersion versionedEntity) { + versionedEntity.setVersion(0L); + } + if (entity.getUuid() == null) { + getEntityManager().persist(entity); + } else { + if (entity instanceof HasVersion) { + /* + * Hibernate 6 does not allow creating versioned entities with preset IDs. + * Bypassing by calling the underlying session directly + * */ + Session session = getEntityManager().unwrap(Session.class); + session.save(entity); + } else { + entity = getEntityManager().merge(entity); + } + } + return entity; + } + + private E update(E entity) { + if (entity instanceof HasVersion versionedEntity) { + if (versionedEntity.getVersion() == null) { + HasVersion existingEntity = entityManager.find(versionedEntity.getClass(), entity.getUuid()); + if (existingEntity != null) { + /* + * manually resetting the version to latest to allow force overwriting of the entity + * */ + versionedEntity.setVersion(existingEntity.getVersion()); + } else { + return create(entity); + } + } + versionedEntity = entityManager.merge(versionedEntity); + entity = (E) versionedEntity; + } else { + entity = entityManager.merge(entity); + } + return entity; + } + @Override @Transactional public D saveAndFlush(TenantId tenantId, D domain) { diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/ai/AiModelRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/ai/AiModelRepository.java new file mode 100644 index 0000000000..cbe681fb58 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/ai/AiModelRepository.java @@ -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. + */ +package org.thingsboard.server.dao.sql.ai; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.transaction.annotation.Transactional; +import org.thingsboard.server.dao.ExportableEntityRepository; +import org.thingsboard.server.dao.model.sql.AiModelEntity; + +import java.util.Optional; +import java.util.Set; +import java.util.UUID; + +interface AiModelRepository extends JpaRepository, ExportableEntityRepository { + + Optional findByTenantIdAndId(UUID tenantId, UUID id); + + Optional findByTenantIdAndName(UUID tenantId, String name); + + @Query( + value = """ + SELECT * + FROM ai_model model + WHERE model.tenant_id = :tenantId + AND (:textSearch IS NULL + OR model.name ILIKE '%' || :textSearch || '%' + OR REPLACE(model.configuration ->> 'provider', '_', ' ') ILIKE '%' || :textSearch || '%' + OR model.configuration ->> 'modelId' ILIKE '%' || :textSearch || '%') + """, + countQuery = """ + SELECT COUNT(*) + FROM ai_model model + WHERE model.tenant_id = :tenantId + AND (:textSearch IS NULL + OR model.name ILIKE '%' || :textSearch || '%' + OR REPLACE(model.configuration ->> 'provider', '_', ' ') ILIKE '%' || :textSearch || '%' + OR (model.configuration ->> 'modelId') ILIKE '%' || :textSearch || '%') + """, + nativeQuery = true + ) + Page findByTenantId(@Param("tenantId") UUID tenantId, @Param("textSearch") String textSearch, Pageable pageable); + + @Query("SELECT ai_model.id FROM AiModelEntity ai_model WHERE ai_model.tenantId = :tenantId") + Page findIdsByTenantId(@Param("tenantId") UUID tenantId, Pageable pageable); + + @Query("SELECT externalId FROM AiModelEntity WHERE id = :id") + Optional getExternalIdById(@Param("id") UUID id); + + long countByTenantId(UUID tenantId); + + @Transactional + @Modifying + @Query("DELETE FROM AiModelEntity ai_model WHERE ai_model.id IN (:ids)") + int deleteByIdIn(@Param("ids") Set ids); + + @Transactional + @Modifying + @Query(value = """ + DELETE FROM ai_model + WHERE tenant_id = :tenantId + RETURNING id + """, nativeQuery = true + ) + Set deleteByTenantId(@Param("tenantId") UUID tenantId); + + @Transactional + @Modifying + @Query("DELETE FROM AiModelEntity ai_model WHERE ai_model.tenantId = :tenantId AND ai_model.id IN (:ids)") + int deleteByTenantIdAndIdIn(@Param("tenantId") UUID tenantId, @Param("ids") Set ids); + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/ai/JpaAiModelDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/ai/JpaAiModelDao.java new file mode 100644 index 0000000000..882c86555b --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/ai/JpaAiModelDao.java @@ -0,0 +1,139 @@ +/** + * 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.ai; + +import lombok.RequiredArgsConstructor; +import org.apache.commons.lang3.StringUtils; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; +import org.springframework.data.jpa.domain.JpaSort; +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.ai.AiModel; +import org.thingsboard.server.common.data.id.AiModelId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.common.data.page.SortOrder; +import org.thingsboard.server.dao.DaoUtil; +import org.thingsboard.server.dao.ai.AiModelDao; +import org.thingsboard.server.dao.model.sql.AiModelEntity; +import org.thingsboard.server.dao.sql.JpaAbstractDao; +import org.thingsboard.server.dao.util.SqlDao; + +import java.util.Optional; +import java.util.Set; +import java.util.UUID; + +import static java.util.stream.Collectors.toSet; + +@SqlDao +@Component +@RequiredArgsConstructor +class JpaAiModelDao extends JpaAbstractDao implements AiModelDao { + + private final AiModelRepository aiModelRepository; + + @Override + public Optional findByTenantIdAndId(TenantId tenantId, AiModelId modelId) { + return aiModelRepository.findByTenantIdAndId(tenantId.getId(), modelId.getId()).map(DaoUtil::getData); + } + + @Override + public AiModel findByTenantIdAndName(UUID tenantId, String name) { + return DaoUtil.getData(aiModelRepository.findByTenantIdAndName(tenantId, name)); + } + + @Override + public AiModel findByTenantIdAndExternalId(UUID tenantId, UUID externalId) { + return DaoUtil.getData(aiModelRepository.findByTenantIdAndExternalId(tenantId, externalId)); + } + + @Override + public PageData findAllByTenantId(TenantId tenantId, PageLink pageLink) { + return findByTenantId(tenantId.getId(), pageLink); + } + + @Override + public PageData findByTenantId(UUID tenantId, PageLink pageLink) { + return DaoUtil.toPageData(aiModelRepository.findByTenantId( + tenantId, StringUtils.defaultIfEmpty(pageLink.getTextSearch(), null), toPageRequest(pageLink)) + ); + } + + @Override + public PageData findIdsByTenantId(UUID tenantId, PageLink pageLink) { + return DaoUtil.pageToPageData(aiModelRepository.findIdsByTenantId(tenantId, toPageRequest(pageLink)).map(AiModelId::new)); + } + + private static PageRequest toPageRequest(PageLink pageLink) { + Sort sort; + SortOrder sortOrder = pageLink.getSortOrder(); + if (sortOrder == null) { + sort = Sort.by(Sort.Direction.ASC, "id"); + } else { + sort = JpaSort.unsafe( + Sort.Direction.fromString(sortOrder.getDirection().name()), + AiModelEntity.COLUMN_MAP.getOrDefault(sortOrder.getProperty(), sortOrder.getProperty()) + ).and(Sort.by(Sort.Direction.ASC, "id")); + } + return PageRequest.of(pageLink.getPage(), pageLink.getPageSize(), sort); + } + + @Override + public AiModelId getExternalIdByInternal(AiModelId internalId) { + return aiModelRepository.getExternalIdById(internalId.getId()).map(AiModelId::new).orElse(null); + } + + @Override + public Long countByTenantId(TenantId tenantId) { + return aiModelRepository.countByTenantId(tenantId.getId()); + } + + @Override + public boolean deleteById(TenantId tenantId, AiModelId modelId) { + return aiModelRepository.deleteByIdIn(Set.of(modelId.getId())) > 0; + } + + @Override + public Set deleteByTenantId(TenantId tenantId) { + return aiModelRepository.deleteByTenantId(tenantId.getId()).stream() + .map(AiModelId::new) + .collect(toSet()); + } + + @Override + public boolean deleteByTenantIdAndId(TenantId tenantId, AiModelId modelId) { + return aiModelRepository.deleteByTenantIdAndIdIn(tenantId.getId(), Set.of(modelId.getId())) > 0; + } + + @Override + public EntityType getEntityType() { + return EntityType.AI_MODEL; + } + + @Override + protected Class getEntityClass() { + return AiModelEntity.class; + } + + @Override + protected JpaRepository getRepository() { + return aiModelRepository; + } + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/AlarmRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/AlarmRepository.java index b9c1abe416..376f610d66 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/AlarmRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/AlarmRepository.java @@ -32,9 +32,6 @@ import java.util.List; import java.util.Set; import java.util.UUID; -/** - * Created by Valerii Sosliuk on 5/21/2017. - */ public interface AlarmRepository extends JpaRepository { @Query("SELECT a FROM AlarmEntity a WHERE a.originatorId = :originatorId AND a.type = :alarmType ORDER BY a.startTs DESC") diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDao.java index c21d8ae928..647a46aeda 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDao.java @@ -16,6 +16,7 @@ package org.thingsboard.server.dao.sql.alarm; 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; @@ -79,9 +80,6 @@ import static org.thingsboard.server.common.data.page.SortOrder.Direction.ASC; import static org.thingsboard.server.dao.DaoUtil.convertTenantEntityTypesToDto; import static org.thingsboard.server.dao.DaoUtil.toPageable; -/** - * Created by Valerii Sosliuk on 5/19/2017. - */ @Slf4j @Component @SqlDao @@ -124,6 +122,11 @@ public class JpaAlarmDao extends JpaAbstractDao implements A return latest.isEmpty() ? null : DaoUtil.getData(latest.get(0)); } + @Override + public FluentFuture findLatestActiveByOriginatorAndTypeAsync(TenantId tenantId, EntityId originator, String type) { + return FluentFuture.from(service.submit(() -> findLatestActiveByOriginatorAndType(tenantId, originator, type))); + } + @Override public ListenableFuture findLatestByOriginatorAndTypeAsync(TenantId tenantId, EntityId originator, String type) { return service.submit(() -> findLatestByOriginatorAndType(tenantId, originator, type)); 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 4e99fb57e4..4b55884792 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 @@ -48,9 +48,6 @@ import java.util.UUID; import static org.thingsboard.server.dao.DaoUtil.convertTenantEntityInfosToDto; -/** - * Created by Valerii Sosliuk on 5/19/2017. - */ @Component @SqlDao @Slf4j 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 be122816ba..7755ef036b 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,13 +29,18 @@ public interface CalculatedFieldRepository extends JpaRepository findCalculatedFieldIdsByTenantIdAndEntityId(UUID tenantId, UUID entityId); List findAllByTenantIdAndEntityId(UUID tenantId, UUID entityId); Page 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 (:textSearch IS NULL OR ilike(cf.name, CONCAT('%', :textSearch, '%')) = true)") + Page findAllByTenantIdAndEntityId(UUID tenantId, UUID entityId, String textSearch, Pageable pageable); List findAllByTenantId(UUID tenantId); 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 4bb52c29db..385839dded 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 @@ -65,6 +65,11 @@ public class JpaCalculatedFieldDao extends JpaAbstractDao findAll(PageLink pageLink) { log.debug("Try to find calculated fields by pageLink [{}]", pageLink); @@ -80,7 +85,7 @@ 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))); + return DaoUtil.toPageData(calculatedFieldRepository.findAllByTenantIdAndEntityId(tenantId.getId(), entityId.getId(), pageLink.getTextSearch(), DaoUtil.toPageable(pageLink))); } @Override diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/ota/JpaOtaPackageDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/ota/JpaOtaPackageDao.java index 780f67932e..7322ea4cb6 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/ota/JpaOtaPackageDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/ota/JpaOtaPackageDao.java @@ -22,6 +22,7 @@ import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Transactional; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.OtaPackage; +import org.thingsboard.server.common.data.id.OtaPackageId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; @@ -43,24 +44,52 @@ public class JpaOtaPackageDao extends JpaAbstractDao getEntityClass() { - return OtaPackageEntity.class; + public Long sumDataSizeByTenantId(TenantId tenantId) { + return otaPackageRepository.sumDataSizeByTenantId(tenantId.getId()); } + @Transactional @Override - protected JpaRepository getRepository() { - return otaPackageRepository; + public OtaPackage findOtaPackageByTenantIdAndTitleAndVersion(TenantId tenantId, String title, String version) { + return DaoUtil.getData(otaPackageRepository.findByTenantIdAndTitleAndVersion(tenantId.getId(), title, version)); } + @Transactional @Override - public Long sumDataSizeByTenantId(TenantId tenantId) { - return otaPackageRepository.sumDataSizeByTenantId(tenantId.getId()); + public PageData findAllByTenantId(TenantId tenantId, PageLink pageLink) { + return DaoUtil.toPageData(otaPackageRepository.findByTenantId(tenantId.getId(), DaoUtil.toPageable(pageLink))); } @Transactional @Override - public PageData findAllByTenantId(TenantId tenantId, PageLink pageLink) { - return DaoUtil.toPageData(otaPackageRepository.findByTenantId(tenantId.getId(), DaoUtil.toPageable(pageLink))); + public PageData findByTenantId(UUID tenantId, PageLink pageLink) { + return findAllByTenantId(TenantId.fromUUID(tenantId), pageLink); + } + + @Override + public PageData findIdsByTenantId(UUID tenantId, PageLink pageLink) { + return DaoUtil.pageToPageData(otaPackageRepository.findIdsByTenantId(tenantId, DaoUtil.toPageable(pageLink)).map(OtaPackageId::new)); + } + + @Transactional + @Override + public OtaPackage findByTenantIdAndExternalId(UUID tenantId, UUID externalId) { + return DaoUtil.getData(otaPackageRepository.findByTenantIdAndExternalId(tenantId, externalId)); + } + + @Override + public OtaPackageId getExternalIdByInternal(OtaPackageId internalId) { + return DaoUtil.toEntityId(otaPackageRepository.getExternalIdById(internalId.getId()), OtaPackageId::new); + } + + @Override + protected Class getEntityClass() { + return OtaPackageEntity.class; + } + + @Override + protected JpaRepository getRepository() { + return otaPackageRepository; } @Override diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/ota/OtaPackageInfoRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/ota/OtaPackageInfoRepository.java index fe7e2bf015..c5ca894a00 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/ota/OtaPackageInfoRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/ota/OtaPackageInfoRepository.java @@ -26,14 +26,15 @@ import org.thingsboard.server.dao.model.sql.OtaPackageInfoEntity; import java.util.UUID; public interface OtaPackageInfoRepository extends JpaRepository { - @Query("SELECT new OtaPackageInfoEntity(f.id, f.createdTime, f.tenantId, f.deviceProfileId, f.type, f.title, f.version, f.tag, f.url, f.fileName, f.contentType, f.checksumAlgorithm, f.checksum, f.dataSize, f.additionalInfo, CASE WHEN (f.data IS NOT NULL OR f.url IS NOT NULL) THEN true ELSE false END) FROM OtaPackageEntity f WHERE " + + + @Query("SELECT new OtaPackageInfoEntity(f.id, f.createdTime, f.tenantId, f.deviceProfileId, f.type, f.title, f.version, f.tag, f.url, f.fileName, f.contentType, f.checksumAlgorithm, f.checksum, f.dataSize, f.additionalInfo, f.externalId, CASE WHEN (f.data IS NOT NULL OR f.url IS NOT NULL) THEN true ELSE false END) FROM OtaPackageEntity f WHERE " + "f.tenantId = :tenantId " + "AND (:searchText IS NULL OR ilike(f.title, CONCAT('%', :searchText, '%')) = true)") Page findAllByTenantId(@Param("tenantId") UUID tenantId, @Param("searchText") String searchText, Pageable pageable); - @Query("SELECT new OtaPackageInfoEntity(f.id, f.createdTime, f.tenantId, f.deviceProfileId, f.type, f.title, f.version, f.tag, f.url, f.fileName, f.contentType, f.checksumAlgorithm, f.checksum, f.dataSize, f.additionalInfo, true) FROM OtaPackageEntity f WHERE " + + @Query("SELECT new OtaPackageInfoEntity(f.id, f.createdTime, f.tenantId, f.deviceProfileId, f.type, f.title, f.version, f.tag, f.url, f.fileName, f.contentType, f.checksumAlgorithm, f.checksum, f.dataSize, f.additionalInfo, f.externalId, true) FROM OtaPackageEntity f WHERE " + "f.tenantId = :tenantId " + "AND f.deviceProfileId = :deviceProfileId " + "AND f.type = :type " + @@ -45,7 +46,7 @@ public interface OtaPackageInfoRepository extends JpaRepository { +public interface OtaPackageRepository extends JpaRepository, ExportableEntityRepository { @Query(value = "SELECT COALESCE(SUM(ota.data_size), 0) FROM ota_package ota WHERE ota.tenant_id = :tenantId AND ota.data IS NOT NULL", nativeQuery = true) Long sumDataSizeByTenantId(@Param("tenantId") UUID tenantId); Page findByTenantId(UUID tenantId, Pageable pageable); + OtaPackageEntity findByTenantIdAndTitleAndVersion(UUID tenantId, String title, String version); + + @Query("SELECT externalId FROM OtaPackageEntity WHERE id = :id") + UUID getExternalIdById(@Param("id") UUID id); + + @Query("SELECT r.id FROM OtaPackageEntity r WHERE r.tenantId = :tenantId") + Page findIdsByTenantId(@Param("tenantId") UUID tenantId, Pageable pageable); + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/settings/AdminSettingsRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/settings/AdminSettingsRepository.java index c27602421d..889de7cb74 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/settings/AdminSettingsRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/settings/AdminSettingsRepository.java @@ -22,9 +22,6 @@ import org.thingsboard.server.dao.model.sql.AdminSettingsEntity; import java.util.UUID; -/** - * Created by Valerii Sosliuk on 5/6/2017. - */ public interface AdminSettingsRepository extends JpaRepository { AdminSettingsEntity findByTenantIdAndKey(UUID tenantId, String key); diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/settings/JpaAdminSettingsDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/settings/JpaAdminSettingsDao.java index 68ce5e9d22..3dafa79c3f 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/settings/JpaAdminSettingsDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/settings/JpaAdminSettingsDao.java @@ -15,12 +15,12 @@ */ package org.thingsboard.server.dao.sql.settings; -import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Autowired; +import lombok.RequiredArgsConstructor; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Transactional; import org.thingsboard.server.common.data.AdminSettings; +import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; @@ -35,21 +35,10 @@ import java.util.UUID; @Component @SqlDao -@Slf4j +@RequiredArgsConstructor public class JpaAdminSettingsDao extends JpaAbstractDao implements AdminSettingsDao, TenantEntityDao { - @Autowired - private AdminSettingsRepository adminSettingsRepository; - - @Override - protected Class getEntityClass() { - return AdminSettingsEntity.class; - } - - @Override - protected JpaRepository getRepository() { - return adminSettingsRepository; - } + private final AdminSettingsRepository adminSettingsRepository; @Override public AdminSettings findByTenantIdAndKey(UUID tenantId, String key) { @@ -77,4 +66,19 @@ public class JpaAdminSettingsDao extends JpaAbstractDao getEntityClass() { + return AdminSettingsEntity.class; + } + + @Override + protected JpaRepository getRepository() { + return adminSettingsRepository; + } + + @Override + public EntityType getEntityType() { + return EntityType.ADMIN_SETTINGS; + } + } 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 338ca9594e..5c69680d16 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 @@ -43,7 +43,6 @@ import org.thingsboard.server.dao.notification.NotificationSettingsService; import org.thingsboard.server.dao.service.PaginatedRemover; import org.thingsboard.server.dao.service.Validator; import org.thingsboard.server.dao.service.validator.TenantDataValidator; -import org.thingsboard.server.dao.settings.AdminSettingsService; import org.thingsboard.server.dao.trendz.TrendzSettingsService; import org.thingsboard.server.dao.usagerecord.ApiUsageStateService; import org.thingsboard.server.dao.user.UserService; @@ -76,8 +75,6 @@ public class TenantServiceImpl extends AbstractCachedEntityService tenantDao.existsById(tenantId, tenantId.getId()), false); } - private PaginatedRemover tenantsRemover = new PaginatedRemover<>() { + private final PaginatedRemover tenantsRemover = new PaginatedRemover<>() { @Override protected PageData findEntities(TenantId tenantId, TenantId id, PageLink pageLink) { 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/resources/sql/schema-entities-idx.sql b/dao/src/main/resources/sql/schema-entities-idx.sql index ad311f00df..12f314590a 100644 --- a/dao/src/main/resources/sql/schema-entities-idx.sql +++ b/dao/src/main/resources/sql/schema-entities-idx.sql @@ -73,22 +73,6 @@ CREATE INDEX IF NOT EXISTS idx_edge_event_id ON edge_event(id); CREATE INDEX IF NOT EXISTS idx_rpc_tenant_id_device_id ON rpc(tenant_id, device_id); -CREATE INDEX IF NOT EXISTS idx_device_external_id ON device(tenant_id, external_id); - -CREATE INDEX IF NOT EXISTS idx_device_profile_external_id ON device_profile(tenant_id, external_id); - -CREATE INDEX IF NOT EXISTS idx_asset_external_id ON asset(tenant_id, external_id); - -CREATE INDEX IF NOT EXISTS idx_entity_view_external_id ON entity_view(tenant_id, external_id); - -CREATE INDEX IF NOT EXISTS idx_rule_chain_external_id ON rule_chain(tenant_id, external_id); - -CREATE INDEX IF NOT EXISTS idx_dashboard_external_id ON dashboard(tenant_id, external_id); - -CREATE INDEX IF NOT EXISTS idx_customer_external_id ON customer(tenant_id, external_id); - -CREATE INDEX IF NOT EXISTS idx_widgets_bundle_external_id ON widgets_bundle(tenant_id, external_id); - CREATE INDEX IF NOT EXISTS idx_rule_node_external_id ON rule_node(rule_chain_id, external_id); CREATE INDEX IF NOT EXISTS idx_rule_node_type_id_configuration_version ON rule_node(type, id, configuration_version); @@ -131,3 +115,5 @@ CREATE INDEX IF NOT EXISTS idx_resource_type_public_resource_key ON resource(res CREATE INDEX IF NOT EXISTS mobile_app_bundle_tenant_id ON mobile_app_bundle(tenant_id); CREATE INDEX IF NOT EXISTS idx_job_tenant_id ON job(tenant_id); + +CREATE INDEX IF NOT EXISTS idx_ai_model_tenant_id ON ai_model(tenant_id); diff --git a/dao/src/main/resources/sql/schema-entities.sql b/dao/src/main/resources/sql/schema-entities.sql index f2a0bc26c1..6ccf2f6d95 100644 --- a/dao/src/main/resources/sql/schema-entities.sql +++ b/dao/src/main/resources/sql/schema-entities.sql @@ -216,7 +216,9 @@ CREATE TABLE IF NOT EXISTS ota_package ( data oid, data_size bigint, additional_info varchar, - CONSTRAINT ota_package_tenant_title_version_unq_key UNIQUE (tenant_id, title, version) + external_id uuid, + CONSTRAINT ota_package_tenant_title_version_unq_key UNIQUE (tenant_id, title, version), + CONSTRAINT ota_package_external_id_unq_key UNIQUE (tenant_id, external_id) ); CREATE TABLE IF NOT EXISTS queue ( @@ -624,6 +626,7 @@ CREATE TABLE IF NOT EXISTS mobile_app ( created_time bigint NOT NULL, tenant_id uuid, pkg_name varchar(255), + title varchar(255), app_secret varchar(2048), platform_type varchar(32), status varchar(32), @@ -961,3 +964,15 @@ CREATE TABLE IF NOT EXISTS job ( configuration varchar NOT NULL, result varchar ); + +CREATE TABLE IF NOT EXISTS ai_model ( + id UUID NOT NULL PRIMARY KEY, + external_id UUID, + created_time BIGINT NOT NULL, + tenant_id UUID NOT NULL, + version BIGINT NOT NULL DEFAULT 1, + name VARCHAR(255) NOT NULL, + configuration JSONB NOT NULL, + CONSTRAINT ai_model_name_unq_key UNIQUE (tenant_id, name), + CONSTRAINT ai_model_external_id_unq_key UNIQUE (tenant_id, external_id) +); 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/CalculatedFieldServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/CalculatedFieldServiceTest.java index 2985aa7620..49bdcca0fb 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 @@ -31,18 +31,24 @@ 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.RelationQueryDynamicSourceConfiguration; 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.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.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.geofencing.GeofencingReportStrategy.REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS; @DaoSqlTest public class CalculatedFieldServiceTest extends AbstractServiceTest { @@ -51,6 +57,8 @@ public class CalculatedFieldServiceTest extends AbstractServiceTest { private CalculatedFieldService calculatedFieldService; @Autowired private DeviceService deviceService; + @Autowired + private TbTenantProfileCache tbTenantProfileCache; private ListeningExecutorService executor; @@ -90,6 +98,182 @@ public class CalculatedFieldServiceTest extends AbstractServiceTest { calculatedFieldService.deleteCalculatedField(tenantId, savedCalculatedField.getId()); } + @Test + public void testSaveGeofencingCalculatedField_shouldNotChangeScheduledInterval() { + // 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) — no DYNAMIC configuration, so no scheduling even if the scheduled interval is set + ZoneGroupConfiguration zoneGroupConfiguration = new ZoneGroupConfiguration("allowed", REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS, false); + zoneGroupConfiguration.setRefEntityId(device.getId()); + cfg.setZoneGroups(Map.of("allowed", zoneGroupConfiguration)); + + // Set a scheduled interval to some value + cfg.setScheduledUpdateInterval(600); + + // Create & save Calculated Field + CalculatedField cf = new CalculatedField(); + cf.setTenantId(tenantId); + cf.setEntityId(device.getId()); + cf.setType(CalculatedFieldType.GEOFENCING); + cf.setName("GF clamp 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(); + + // Assert: the interval is saved, but scheduling is not enabled + int savedInterval = geofencingCalculatedFieldConfiguration.getScheduledUpdateInterval(); + boolean scheduledUpdateEnabled = geofencingCalculatedFieldConfiguration.isScheduledUpdateEnabled(); + + assertThat(savedInterval).isEqualTo(600); + assertThat(scheduledUpdateEnabled).isFalse(); + + calculatedFieldService.deleteCalculatedField(tenantId, saved.getId()); + } + + @Test + public void testSaveGeofencingCalculatedField_shouldThrowWhenScheduledIntervalIsLessThanMinAllowedIntervalInTenantProfile() { + // 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 RelationQueryDynamicSourceConfiguration(); + dynamicSourceConfiguration.setDirection(EntitySearchDirection.FROM); + dynamicSourceConfiguration.setMaxLevel(1); + dynamicSourceConfiguration.setRelationType(EntityRelation.CONTAINS_TYPE); + zoneGroupConfiguration.setRefDynamicSourceConfiguration(dynamicSourceConfiguration); + cfg.setZoneGroups(Map.of("allowed", zoneGroupConfiguration)); + + // Enable scheduling with an interval below tenant min + cfg.setScheduledUpdateInterval(600); + + // Create & save Calculated Field + CalculatedField cf = new CalculatedField(); + cf.setTenantId(tenantId); + cf.setEntityId(device.getId()); + cf.setType(CalculatedFieldType.GEOFENCING); + cf.setName("GF clamp 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_shouldThrowWhenRelationLevelIsGreaterThanMaxAllowedRelationLevelInTenantProfile() { + // 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 RelationQueryDynamicSourceConfiguration(); + dynamicSourceConfiguration.setDirection(EntitySearchDirection.FROM); + dynamicSourceConfiguration.setMaxLevel(Integer.MAX_VALUE); + dynamicSourceConfiguration.setRelationType(EntityRelation.CONTAINS_TYPE); + 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 clamp 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_shouldUseScheduledIntervalFromConfig() { + // 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 RelationQueryDynamicSourceConfiguration(); + dynamicSourceConfiguration.setDirection(EntitySearchDirection.FROM); + dynamicSourceConfiguration.setMaxLevel(1); + dynamicSourceConfiguration.setRelationType(EntityRelation.CONTAINS_TYPE); + zoneGroupConfiguration.setRefDynamicSourceConfiguration(dynamicSourceConfiguration); + cfg.setZoneGroups(Map.of("allowed", zoneGroupConfiguration)); + + // Get tenant profile min. + int min = tbTenantProfileCache.get(tenantId) + .getDefaultProfileConfiguration() + .getMinAllowedScheduledUpdateIntervalInSecForCF(); + + + // Enable scheduling with an interval greater than tenant min + int valueFromConfig = min + 100; + 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 clamp 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(); + + // Assert: the interval is clamped up to tenant profile min (or stays >= original if already >= min) + int savedInterval = geofencingCalculatedFieldConfiguration.getScheduledUpdateInterval(); + assertThat(savedInterval).isEqualTo(valueFromConfig); + + calculatedFieldService.deleteCalculatedField(tenantId, saved.getId()); + } + @Test public void testSaveCalculatedFieldWithExistingName() { Device device = createTestDevice(); 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..32767043d7 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 @@ -486,6 +486,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/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/sql/JdbcTemplateTest.java b/dao/src/test/java/org/thingsboard/server/dao/sql/JdbcTemplateTest.java index d0af99021a..94292548e5 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/sql/JdbcTemplateTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/sql/JdbcTemplateTest.java @@ -17,7 +17,7 @@ package org.thingsboard.server.dao.sql; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.dao.DataAccessResourceFailureException; +import org.springframework.dao.QueryTimeoutException; import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; import org.springframework.test.context.TestPropertySource; import org.thingsboard.server.dao.AbstractJpaDaoTest; @@ -34,6 +34,6 @@ public class JdbcTemplateTest extends AbstractJpaDaoTest { @Test public void queryTimeoutTest() { - assertThrows(DataAccessResourceFailureException.class, () -> jdbcTemplate.query("SELECT pg_sleep(10)", rs -> {})); + assertThrows(QueryTimeoutException.class, () -> jdbcTemplate.query("SELECT pg_sleep(10)", rs -> {})); } } diff --git a/dao/src/test/resources/application-test.properties b/dao/src/test/resources/application-test.properties index a44303107c..1c2c0c5519 100644 --- a/dao/src/test/resources/application-test.properties +++ b/dao/src/test/resources/application-test.properties @@ -111,6 +111,9 @@ cache.specs.mobileSecretKey.maxSize=10000 cache.specs.trendzSettings.timeToLiveInMinutes=1440 cache.specs.trendzSettings.maxSize=10000 +cache.specs.aiModel.timeToLiveInMinutes=1440 +cache.specs.aiModel.maxSize=10000 + redis.connection.host=localhost redis.connection.port=6379 redis.connection.db=0 @@ -158,4 +161,4 @@ queue.core.poll-interval=5 queue.core.partitions=2 queue.rule-engine.poll-interval=5 -spring.jpa.properties.hibernate.dialect=org.thingsboard.server.dao.ThingsboardPostgreSQLDialect \ No newline at end of file +spring.jpa.properties.hibernate.dialect=org.thingsboard.server.dao.ThingsboardPostgreSQLDialect diff --git a/docker/docker-compose.cassandra.volumes.yml b/docker/docker-compose.cassandra.volumes.yml index c6d64c484e..8e92069a06 100644 --- a/docker/docker-compose.cassandra.volumes.yml +++ b/docker/docker-compose.cassandra.volumes.yml @@ -14,8 +14,6 @@ # limitations under the License. # -version: '3.0' - services: cassandra: volumes: diff --git a/docker/docker-compose.confluent.yml b/docker/docker-compose.confluent.yml index e832b72519..46cb0235ee 100644 --- a/docker/docker-compose.confluent.yml +++ b/docker/docker-compose.confluent.yml @@ -14,8 +14,6 @@ # limitations under the License. # -version: '3.0' - services: tb-js-executor: env_file: diff --git a/docker/docker-compose.edqs.volumes.yml b/docker/docker-compose.edqs.volumes.yml index 3a45542b99..cebc7dd290 100644 --- a/docker/docker-compose.edqs.volumes.yml +++ b/docker/docker-compose.edqs.volumes.yml @@ -14,8 +14,6 @@ # limitations under the License. # -version: '3.0' - services: tb-edqs1: volumes: diff --git a/docker/docker-compose.edqs.yml b/docker/docker-compose.edqs.yml index 21bace143a..3ebb526a2d 100644 --- a/docker/docker-compose.edqs.yml +++ b/docker/docker-compose.edqs.yml @@ -14,8 +14,6 @@ # limitations under the License. # -version: '3.0' - services: tb-core1: env_file: diff --git a/docker/docker-compose.hybrid.yml b/docker/docker-compose.hybrid.yml index 3ced5385f1..6b4129ec56 100644 --- a/docker/docker-compose.hybrid.yml +++ b/docker/docker-compose.hybrid.yml @@ -14,8 +14,6 @@ # limitations under the License. # -version: '3.0' - services: postgres: restart: always diff --git a/docker/docker-compose.kafka.yml b/docker/docker-compose.kafka.yml index 7e6fcae09c..06bafafc61 100644 --- a/docker/docker-compose.kafka.yml +++ b/docker/docker-compose.kafka.yml @@ -14,12 +14,10 @@ # limitations under the License. # -version: '3.0' - 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.postgres.volumes.yml b/docker/docker-compose.postgres.volumes.yml index 3d2e026ef4..696189a639 100644 --- a/docker/docker-compose.postgres.volumes.yml +++ b/docker/docker-compose.postgres.volumes.yml @@ -14,8 +14,6 @@ # limitations under the License. # -version: '3.0' - services: postgres: volumes: diff --git a/docker/docker-compose.postgres.yml b/docker/docker-compose.postgres.yml index cd0373af42..bf43e1823d 100644 --- a/docker/docker-compose.postgres.yml +++ b/docker/docker-compose.postgres.yml @@ -14,8 +14,6 @@ # limitations under the License. # -version: '3.0' - services: postgres: restart: always diff --git a/docker/docker-compose.prometheus-grafana.yml b/docker/docker-compose.prometheus-grafana.yml index 81650c6b26..38699f1040 100644 --- a/docker/docker-compose.prometheus-grafana.yml +++ b/docker/docker-compose.prometheus-grafana.yml @@ -14,8 +14,6 @@ # limitations under the License. # -version: '3.0' - volumes: prometheus_data: {} grafana_data: {} diff --git a/docker/docker-compose.valkey-cluster.volumes.yml b/docker/docker-compose.valkey-cluster.volumes.yml index 7e78d04505..e8da9844d9 100644 --- a/docker/docker-compose.valkey-cluster.volumes.yml +++ b/docker/docker-compose.valkey-cluster.volumes.yml @@ -14,8 +14,6 @@ # limitations under the License. # -version: '3.0' - services: # Valkey cluster valkey-node-0: diff --git a/docker/docker-compose.valkey-cluster.yml b/docker/docker-compose.valkey-cluster.yml index 02757240dc..b8ce0f96ec 100644 --- a/docker/docker-compose.valkey-cluster.yml +++ b/docker/docker-compose.valkey-cluster.yml @@ -14,13 +14,11 @@ # limitations under the License. # -version: '3.0' - 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: @@ -28,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: @@ -38,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: @@ -48,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: @@ -58,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: @@ -68,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.volumes.yml b/docker/docker-compose.valkey-sentinel.volumes.yml index 0f53ee005d..3f4a09a912 100644 --- a/docker/docker-compose.valkey-sentinel.volumes.yml +++ b/docker/docker-compose.valkey-sentinel.volumes.yml @@ -14,8 +14,6 @@ # limitations under the License. # -version: '3.0' - services: # Valkey sentinel valkey-primary: diff --git a/docker/docker-compose.valkey-sentinel.yml b/docker/docker-compose.valkey-sentinel.yml index d4b791643a..e528a1bb9a 100644 --- a/docker/docker-compose.valkey-sentinel.yml +++ b/docker/docker-compose.valkey-sentinel.yml @@ -14,13 +14,11 @@ # limitations under the License. # -version: '3.0' - 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: @@ -28,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: @@ -40,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.volumes.yml b/docker/docker-compose.valkey.volumes.yml index 7da5f154a6..02a7c397a3 100644 --- a/docker/docker-compose.valkey.volumes.yml +++ b/docker/docker-compose.valkey.volumes.yml @@ -14,8 +14,6 @@ # limitations under the License. # -version: '3.0' - services: valkey: volumes: diff --git a/docker/docker-compose.valkey.yml b/docker/docker-compose.valkey.yml index 6b8cf3feb3..3eba086a9b 100644 --- a/docker/docker-compose.valkey.yml +++ b/docker/docker-compose.valkey.yml @@ -14,14 +14,12 @@ # limitations under the License. # -version: '3.0' - services: # Valkey standalone # 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/docker/docker-compose.volumes.yml b/docker/docker-compose.volumes.yml index 06a904e7fc..a01247c25d 100644 --- a/docker/docker-compose.volumes.yml +++ b/docker/docker-compose.volumes.yml @@ -14,8 +14,6 @@ # limitations under the License. # -version: '3.0' - services: tb-core1: volumes: diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 1cee5ad5ad..224f94c103 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -14,9 +14,6 @@ # limitations under the License. # - -version: '3.0' - services: zookeeper: restart: always diff --git a/docker/queue-confluent.env b/docker/queue-confluent.env index 868a135de3..900504c4ea 100644 --- a/docker/queue-confluent.env +++ b/docker/queue-confluent.env @@ -15,4 +15,4 @@ TB_QUEUE_KAFKA_RE_TOPIC_PROPERTIES=retention.ms:604800000;segment.bytes:52428800 TB_QUEUE_KAFKA_CORE_TOPIC_PROPERTIES=retention.ms:604800000;segment.bytes:52428800;retention.bytes:1048576000 TB_QUEUE_KAFKA_TA_TOPIC_PROPERTIES=retention.ms:604800000;segment.bytes:52428800;retention.bytes:1048576000 TB_QUEUE_KAFKA_NOTIFICATIONS_TOPIC_PROPERTIES=retention.ms:604800000;segment.bytes:52428800;retention.bytes:1048576000 -TB_QUEUE_KAFKA_JE_TOPIC_PROPERTIES=retention.ms:604800000;segment.bytes:52428800;retention.bytes:104857600 +TB_QUEUE_KAFKA_JE_TOPIC_PROPERTIES=retention.ms:86400000;segment.bytes:52428800;retention.bytes:104857600 diff --git a/edqs/pom.xml b/edqs/pom.xml index 4cca1bd89e..4a4c05be71 100644 --- a/edqs/pom.xml +++ b/edqs/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.0-SNAPSHOT + 4.3.0-SNAPSHOT thingsboard edqs diff --git a/edqs/src/test/java/org/thingsboard/server/edqs/repo/AssetSearchQueryFilterTest.java b/edqs/src/test/java/org/thingsboard/server/edqs/repo/AssetSearchQueryFilterTest.java index 1f90babf01..7444d24b99 100644 --- a/edqs/src/test/java/org/thingsboard/server/edqs/repo/AssetSearchQueryFilterTest.java +++ b/edqs/src/test/java/org/thingsboard/server/edqs/repo/AssetSearchQueryFilterTest.java @@ -26,6 +26,7 @@ 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.page.PageData; +import org.thingsboard.server.common.data.query.AliasEntityId; import org.thingsboard.server.common.data.query.AssetSearchQueryFilter; import org.thingsboard.server.common.data.query.EntityDataPageLink; import org.thingsboard.server.common.data.query.EntityDataQuery; @@ -132,7 +133,7 @@ public class AssetSearchQueryFilterTest extends AbstractEDQTest { private PageData findData(CustomerId customerId, EntityId rootId, EntitySearchDirection direction, String relationType, int maxLevel, boolean lastLevelOnly, List assetTypes) { AssetSearchQueryFilter filter = new AssetSearchQueryFilter(); - filter.setRootEntity(rootId); + filter.setRootEntity(AliasEntityId.fromEntityId(rootId)); filter.setDirection(direction); filter.setRelationType(relationType); filter.setAssetTypes(assetTypes); diff --git a/edqs/src/test/java/org/thingsboard/server/edqs/repo/DeviceSearchQueryFilterTest.java b/edqs/src/test/java/org/thingsboard/server/edqs/repo/DeviceSearchQueryFilterTest.java index b3f15c2f61..9e30997b88 100644 --- a/edqs/src/test/java/org/thingsboard/server/edqs/repo/DeviceSearchQueryFilterTest.java +++ b/edqs/src/test/java/org/thingsboard/server/edqs/repo/DeviceSearchQueryFilterTest.java @@ -27,6 +27,7 @@ import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DeviceProfileId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.query.AliasEntityId; import org.thingsboard.server.common.data.query.DeviceSearchQueryFilter; import org.thingsboard.server.common.data.query.EntityDataPageLink; import org.thingsboard.server.common.data.query.EntityDataQuery; @@ -135,7 +136,7 @@ public class DeviceSearchQueryFilterTest extends AbstractEDQTest { private PageData findData(CustomerId customerId, EntityId rootId, EntitySearchDirection direction, String relationType, int maxLevel, boolean lastLevelOnly, List deviceTypes) { DeviceSearchQueryFilter filter = new DeviceSearchQueryFilter(); - filter.setRootEntity(rootId); + filter.setRootEntity(AliasEntityId.fromEntityId(rootId)); filter.setDirection(direction); filter.setRelationType(relationType); filter.setDeviceTypes(deviceTypes); diff --git a/edqs/src/test/java/org/thingsboard/server/edqs/repo/EdgeSearchQueryFilterTest.java b/edqs/src/test/java/org/thingsboard/server/edqs/repo/EdgeSearchQueryFilterTest.java index f0911570ea..42c64ed602 100644 --- a/edqs/src/test/java/org/thingsboard/server/edqs/repo/EdgeSearchQueryFilterTest.java +++ b/edqs/src/test/java/org/thingsboard/server/edqs/repo/EdgeSearchQueryFilterTest.java @@ -24,6 +24,7 @@ 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.page.PageData; +import org.thingsboard.server.common.data.query.AliasEntityId; import org.thingsboard.server.common.data.query.EdgeSearchQueryFilter; import org.thingsboard.server.common.data.query.EntityDataPageLink; import org.thingsboard.server.common.data.query.EntityDataQuery; @@ -101,7 +102,7 @@ public class EdgeSearchQueryFilterTest extends AbstractEDQTest { private PageData findData(CustomerId customerId, EntityId rootId, EntitySearchDirection direction, String relationType, int maxLevel, boolean lastLevelOnly, List edgeTypes) { EdgeSearchQueryFilter filter = new EdgeSearchQueryFilter(); - filter.setRootEntity(rootId); + filter.setRootEntity(AliasEntityId.fromEntityId(rootId)); filter.setDirection(direction); filter.setRelationType(relationType); filter.setEdgeTypes(edgeTypes); diff --git a/edqs/src/test/java/org/thingsboard/server/edqs/repo/EntityTypeFilterTest.java b/edqs/src/test/java/org/thingsboard/server/edqs/repo/EntityTypeFilterTest.java index 8fab142dc7..e7fc4419ef 100644 --- a/edqs/src/test/java/org/thingsboard/server/edqs/repo/EntityTypeFilterTest.java +++ b/edqs/src/test/java/org/thingsboard/server/edqs/repo/EntityTypeFilterTest.java @@ -26,6 +26,7 @@ import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.DeviceProfileId; 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.StringDataEntry; import org.thingsboard.server.common.data.query.EntityDataPageLink; import org.thingsboard.server.common.data.query.EntityDataQuery; @@ -36,7 +37,9 @@ import org.thingsboard.server.common.data.query.EntityKeyValueType; import org.thingsboard.server.common.data.query.EntityTypeFilter; import org.thingsboard.server.common.data.query.FilterPredicateValue; import org.thingsboard.server.common.data.query.KeyFilter; +import org.thingsboard.server.common.data.query.NumericFilterPredicate; import org.thingsboard.server.common.data.query.StringFilterPredicate; +import org.thingsboard.server.edqs.util.RepositoryUtils; import java.util.Arrays; import java.util.List; @@ -61,6 +64,10 @@ public class EntityTypeFilterTest extends AbstractEDQTest { addOrUpdate(new LatestTsKv(device.getId(), new BasicTsKvEntry(43, new StringDataEntry("state", "enabled")), 0L)); addOrUpdate(new LatestTsKv(device2.getId(), new BasicTsKvEntry(43, new StringDataEntry("state", "disabled")), 0L)); addOrUpdate(new LatestTsKv(device3.getId(), new BasicTsKvEntry(43, new BooleanDataEntry("free", true)), 0L)); + + addOrUpdate(new LatestTsKv(device.getId(), new BasicTsKvEntry(43, new StringDataEntry("temperature", "26.0")), 0L)); + addOrUpdate(new LatestTsKv(device2.getId(), new BasicTsKvEntry(43, new DoubleDataEntry("temperature", 25.0)), 0L)); + addOrUpdate(new LatestTsKv(device3.getId(), new BasicTsKvEntry(43, new DoubleDataEntry("temperature", 19.0)), 0L)); } @After @@ -87,6 +94,11 @@ public class EntityTypeFilterTest extends AbstractEDQTest { // find asset entities result = repository.findEntityDataByQuery(tenantId, null, getEntityTypeQuery(EntityType.ASSET, null), false); Assert.assertEquals(0, result.getTotalElements()); + + // find all tenant devices with filter by temperature + KeyFilter tempFilter = getTemperatureFilter(NumericFilterPredicate.NumericOperation.GREATER_OR_EQUAL, 20.0); + result = repository.findEntityDataByQuery(tenantId, null, getEntityTypeQuery(EntityType.DEVICE, List.of(tempFilter)), false); + Assert.assertEquals(2, result.getTotalElements()); } @Test @@ -143,4 +155,15 @@ public class EntityTypeFilterTest extends AbstractEDQTest { return nameFilter; } + private static KeyFilter getTemperatureFilter(NumericFilterPredicate.NumericOperation operation, Double predicateValue) { + KeyFilter tempFilter = new KeyFilter(); + tempFilter.setKey(new EntityKey(EntityKeyType.TIME_SERIES, "temperature")); + var predicate = new NumericFilterPredicate(); + predicate.setOperation(operation); + predicate.setValue(new FilterPredicateValue<>(predicateValue)); + tempFilter.setPredicate(predicate); + tempFilter.setValueType(EntityKeyValueType.NUMERIC); + return tempFilter; + } + } diff --git a/edqs/src/test/java/org/thingsboard/server/edqs/repo/EntityViewSearchQueryFilterTest.java b/edqs/src/test/java/org/thingsboard/server/edqs/repo/EntityViewSearchQueryFilterTest.java index fb32759045..c484ef78ae 100644 --- a/edqs/src/test/java/org/thingsboard/server/edqs/repo/EntityViewSearchQueryFilterTest.java +++ b/edqs/src/test/java/org/thingsboard/server/edqs/repo/EntityViewSearchQueryFilterTest.java @@ -24,6 +24,7 @@ import org.thingsboard.server.common.data.id.AssetId; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.query.AliasEntityId; import org.thingsboard.server.common.data.query.EntityDataPageLink; import org.thingsboard.server.common.data.query.EntityDataQuery; import org.thingsboard.server.common.data.query.EntityKeyType; @@ -115,7 +116,7 @@ public class EntityViewSearchQueryFilterTest extends AbstractEDQTest { private PageData findData(CustomerId customerId, EntityId rootId, EntitySearchDirection direction, String relationType, int maxLevel, boolean lastLevelOnly, List entityViewTypes) { EntityViewSearchQueryFilter filter = new EntityViewSearchQueryFilter(); - filter.setRootEntity(rootId); + filter.setRootEntity(AliasEntityId.fromEntityId(rootId)); filter.setDirection(direction); filter.setRelationType(relationType); filter.setEntityViewTypes(entityViewTypes); diff --git a/edqs/src/test/java/org/thingsboard/server/edqs/repo/RelationsQueryFilterTest.java b/edqs/src/test/java/org/thingsboard/server/edqs/repo/RelationsQueryFilterTest.java index 094d46f977..a376e3f672 100644 --- a/edqs/src/test/java/org/thingsboard/server/edqs/repo/RelationsQueryFilterTest.java +++ b/edqs/src/test/java/org/thingsboard/server/edqs/repo/RelationsQueryFilterTest.java @@ -24,6 +24,7 @@ import org.thingsboard.server.common.data.id.AssetId; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.query.AliasEntityId; import org.thingsboard.server.common.data.query.EntityDataPageLink; import org.thingsboard.server.common.data.query.EntityDataQuery; import org.thingsboard.server.common.data.query.EntityKeyType; @@ -146,7 +147,7 @@ public class RelationsQueryFilterTest extends AbstractEDQTest { private PageData filter(CustomerId customerId, EntityId rootId, int maxLevel, boolean lastLevelOnly, RelationEntityTypeFilter... relationEntityTypeFilters) { RelationsQueryFilter filter = new RelationsQueryFilter(); - filter.setRootEntity(rootId); + filter.setRootEntity(AliasEntityId.fromEntityId(rootId)); filter.setFilters(Arrays.asList(relationEntityTypeFilters)); filter.setDirection(EntitySearchDirection.FROM); filter.setFetchLastLevelOnly(lastLevelOnly); diff --git a/edqs/src/test/java/org/thingsboard/server/edqs/repo/SingleEntityFilterTest.java b/edqs/src/test/java/org/thingsboard/server/edqs/repo/SingleEntityFilterTest.java index 133816f576..93786553c5 100644 --- a/edqs/src/test/java/org/thingsboard/server/edqs/repo/SingleEntityFilterTest.java +++ b/edqs/src/test/java/org/thingsboard/server/edqs/repo/SingleEntityFilterTest.java @@ -26,6 +26,7 @@ import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.DeviceProfileId; import org.thingsboard.server.common.data.kv.BasicTsKvEntry; import org.thingsboard.server.common.data.kv.StringDataEntry; +import org.thingsboard.server.common.data.query.AliasEntityId; import org.thingsboard.server.common.data.query.EntityDataPageLink; import org.thingsboard.server.common.data.query.EntityDataQuery; import org.thingsboard.server.common.data.query.EntityDataSortOrder; @@ -113,7 +114,7 @@ public class SingleEntityFilterTest extends AbstractEDQTest { private static EntityDataQuery getEntityDataQuery(DeviceId deviceId) { SingleEntityFilter filter = new SingleEntityFilter(); - filter.setSingleEntity(deviceId); + filter.setSingleEntity(AliasEntityId.fromEntityId(deviceId)); var pageLink = new EntityDataPageLink(20, 0, null, new EntityDataSortOrder(new EntityKey(EntityKeyType.TIME_SERIES, "state"), EntityDataSortOrder.Direction.DESC), false); var entityFields = Arrays.asList(new EntityKey(EntityKeyType.ENTITY_FIELD, "name"), new EntityKey(EntityKeyType.ENTITY_FIELD, "createdTime")); diff --git a/monitoring/pom.xml b/monitoring/pom.xml index 30a2386713..6dbcf85f07 100644 --- a/monitoring/pom.xml +++ b/monitoring/pom.xml @@ -21,7 +21,7 @@ 4.0.0 org.thingsboard - 4.2.0-SNAPSHOT + 4.3.0-SNAPSHOT thingsboard diff --git a/msa/black-box-tests/pom.xml b/msa/black-box-tests/pom.xml index d3b47daa47..358c524e06 100644 --- a/msa/black-box-tests/pom.xml +++ b/msa/black-box-tests/pom.xml @@ -21,7 +21,7 @@ org.thingsboard - 4.2.0-SNAPSHOT + 4.3.0-SNAPSHOT msa org.thingsboard.msa diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ContainerTestSuite.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ContainerTestSuite.java index 3a337dbb8b..77e80f25ac 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ContainerTestSuite.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ContainerTestSuite.java @@ -38,6 +38,7 @@ import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.CoreMatchers.not; import static org.hamcrest.MatcherAssert.assertThat; import static org.testng.Assert.fail; +import static org.thingsboard.server.msa.TestUtils.addComposeVersion; @Slf4j public class ContainerTestSuite { @@ -53,7 +54,7 @@ public class ContainerTestSuite { private static final String TB_JS_EXECUTOR_LOG_REGEXP = ".*template started.*"; private static final Duration CONTAINER_STARTUP_TIMEOUT = Duration.ofSeconds(400); - private DockerComposeContainer testContainer; + private DockerComposeContainerImpl testContainer; private ThingsBoardDbInstaller installTb; private boolean isActive; @@ -78,8 +79,6 @@ public class ContainerTestSuite { } public void start() { - installTb = new ThingsBoardDbInstaller(); - installTb.createVolumes(); log.info("System property of blackBoxTests.redisCluster is {}", IS_VALKEY_CLUSTER); log.info("System property of blackBoxTests.redisSentinel is {}", IS_VALKEY_SENTINEL); log.info("System property of blackBoxTests.redisSsl is {}", IS_VALKEY_SSL); @@ -93,17 +92,8 @@ public class ContainerTestSuite { FileUtils.copyDirectory(new File("src/test/resources"), new File(targetDir)); - class DockerComposeContainerImpl> extends DockerComposeContainer { - public DockerComposeContainerImpl(List composeFiles) { - super(composeFiles); - } - - @Override - public void stop() { - super.stop(); - tryDeleteDir(targetDir); - } - } + installTb = new ThingsBoardDbInstaller(targetDir); + installTb.createVolumes(); if (IS_VALKEY_SSL) { addToFile(targetDir, "cache-valkey.env", @@ -132,7 +122,9 @@ public class ContainerTestSuite { composeFiles.add(new File(targetDir + "docker-compose.cassandra.volumes.yml")); } - testContainer = new DockerComposeContainerImpl<>(composeFiles) + addComposeVersion(composeFiles, "3.0"); + + testContainer = new DockerComposeContainerImpl(targetDir, composeFiles) .withPull(false) .withLocalCompose(true) .withOptions("--compatibility") @@ -194,7 +186,8 @@ public class ContainerTestSuite { public void stop() { if (isActive) { testContainer.stop(); - installTb.savaLogsAndRemoveVolumes(); + installTb.saveLogsAndRemoveVolumes(); + testContainer.cleanup(); setActive(false); } } @@ -261,4 +254,23 @@ public class ContainerTestSuite { public DockerComposeContainer getTestContainer() { return testContainer; } + + static class DockerComposeContainerImpl extends DockerComposeContainer { + + private final String targetDir; + + public DockerComposeContainerImpl(String targetDir, List composeFiles) { + super(composeFiles); + this.targetDir = targetDir; + } + + @Override + public void stop() { + super.stop(); + } + + public void cleanup() { + tryDeleteDir(this.targetDir); + } + } } 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..0a15d6e8aa 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,13 @@ 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 JsonNode getLatestTelemetry(EntityId entityId) { @@ -367,6 +368,16 @@ 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 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/TestUtils.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/TestUtils.java new file mode 100644 index 0000000000..e70e020e88 --- /dev/null +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/TestUtils.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.msa; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; + +public class TestUtils { + + public static void addComposeVersion(List composeFiles, String version) throws IOException { + for (File composeFile : composeFiles) { + addComposeVersion(composeFile, version); + } + } + + public static void addComposeVersion(File composeFile, String version) throws IOException { + Path composeFilePath = composeFile.toPath(); + String data = Files.readString(composeFilePath); + String versionString = "version: '" + version + "'"; + if (!data.contains(versionString)) { + data += "\n" + versionString + "\n"; + } + Files.writeString(composeFilePath, data); + } + +} diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ThingsBoardDbInstaller.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ThingsBoardDbInstaller.java index 3c57e08487..72be05922b 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ThingsBoardDbInstaller.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ThingsBoardDbInstaller.java @@ -20,6 +20,7 @@ import org.testcontainers.utility.Base58; import org.thingsboard.server.common.data.StringUtils; import java.io.File; +import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; @@ -29,6 +30,8 @@ import java.util.StringJoiner; import java.util.stream.Collectors; import java.util.stream.IntStream; +import static org.thingsboard.server.msa.TestUtils.addComposeVersion; + @Slf4j public class ThingsBoardDbInstaller { @@ -53,6 +56,7 @@ public class ThingsBoardDbInstaller { private final DockerComposeExecutor dockerCompose; + private final String targetDir; private final String postgresDataVolume; private final String cassandraDataVolume; @@ -69,27 +73,30 @@ public class ThingsBoardDbInstaller { private final String tbEdqsLogVolume; private final Map env; - public ThingsBoardDbInstaller() { + public ThingsBoardDbInstaller(String targetDir) throws IOException { + this.targetDir = targetDir; log.info("System property of blackBoxTests.redisCluster is {}", IS_VALKEY_CLUSTER); - log.info("System property of blackBoxTests.redisCluster is {}", IS_VALKEY_SENTINEL); + log.info("System property of blackBoxTests.redisSentinel is {}", IS_VALKEY_SENTINEL); log.info("System property of blackBoxTests.hybridMode is {}", IS_HYBRID_MODE); List composeFiles = new ArrayList<>(Arrays.asList( - new File("./../../docker/docker-compose.yml"), - new File("./../../docker/docker-compose.volumes.yml"), + new File(targetDir + "docker-compose.yml"), + new File(targetDir + "docker-compose.volumes.yml"), IS_HYBRID_MODE - ? new File("./../../docker/docker-compose.hybrid.yml") - : new File("./../../docker/docker-compose.postgres.yml"), - new File("./../../docker/docker-compose.postgres.volumes.yml"), - resolveValkeyComposeFile(), - resolveValkeyComposeVolumesFile() + ? new File(targetDir + "docker-compose.hybrid.yml") + : new File(targetDir + "docker-compose.postgres.yml"), + new File(targetDir + "docker-compose.postgres.volumes.yml"), + resolveValkeyComposeFile(targetDir), + resolveValkeyComposeVolumesFile(targetDir) )); if (IS_HYBRID_MODE) { - composeFiles.add(new File("./../../docker/docker-compose.cassandra.volumes.yml")); - composeFiles.add(new File("src/test/resources/docker-compose.hybrid-test-extras.yml")); + composeFiles.add(new File(targetDir + "docker-compose.cassandra.volumes.yml")); + composeFiles.add(new File(targetDir + "docker-compose.hybrid-test-extras.yml")); } else { - composeFiles.add(new File("src/test/resources/docker-compose.postgres-test-extras.yml")); + composeFiles.add(new File(targetDir + "docker-compose.postgres-test-extras.yml")); } + addComposeVersion(composeFiles, "3.0"); + String identifier = Base58.randomString(6).toLowerCase(); String project = identifier + Base58.randomString(6).toLowerCase(); @@ -137,24 +144,24 @@ public class ThingsBoardDbInstaller { dockerCompose.withEnv(env); } - private static File resolveValkeyComposeVolumesFile() { + private static File resolveValkeyComposeVolumesFile(String targetDir) { if (IS_VALKEY_CLUSTER) { - return new File("./../../docker/docker-compose.valkey-cluster.volumes.yml"); + return new File(targetDir + "docker-compose.valkey-cluster.volumes.yml"); } if (IS_VALKEY_SENTINEL) { - return new File("./../../docker/docker-compose.valkey-sentinel.volumes.yml"); + return new File(targetDir + "docker-compose.valkey-sentinel.volumes.yml"); } - return new File("./../../docker/docker-compose.valkey.volumes.yml"); + return new File(targetDir + "docker-compose.valkey.volumes.yml"); } - private static File resolveValkeyComposeFile() { + private static File resolveValkeyComposeFile(String targetDir) { if (IS_VALKEY_CLUSTER) { - return new File("./../../docker/docker-compose.valkey-cluster.yml"); + return new File(targetDir + "docker-compose.valkey-cluster.yml"); } if (IS_VALKEY_SENTINEL) { - return new File("./../../docker/docker-compose.valkey-sentinel.yml"); + return new File(targetDir + "docker-compose.valkey-sentinel.yml"); } - return new File("./../../docker/docker-compose.valkey.yml"); + return new File(targetDir + "docker-compose.valkey.yml"); } public Map getEnv() { @@ -240,7 +247,7 @@ public class ThingsBoardDbInstaller { } } - public void savaLogsAndRemoveVolumes() { + public void saveLogsAndRemoveVolumes() { copyLogs(tbLogVolume, "./target/tb-logs/"); copyLogs(tbCoapTransportLogVolume, "./target/tb-coap-transport-logs/"); copyLogs(tbLwm2mTransportLogVolume, "./target/tb-lwm2m-transport-logs/"); 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..7f2dfba937 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,13 @@ package org.thingsboard.server.msa.cf; import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ArrayNode; import org.testcontainers.shaded.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; @@ -34,8 +33,12 @@ 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.RelationQueryDynamicSourceConfiguration; 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 +48,19 @@ 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.msa.AbstractContainerTest; import org.thingsboard.server.msa.ui.utils.EntityPrototypes; +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.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 +73,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 +106,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 +141,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 +154,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 +165,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 +181,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 +203,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 +214,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 +235,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 +254,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 +277,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 +292,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 +304,125 @@ 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 RelationQueryDynamicSourceConfiguration(); + allowedDynamicSourceConfiguration.setDirection(EntitySearchDirection.FROM); + allowedDynamicSourceConfiguration.setMaxLevel(1); + allowedDynamicSourceConfiguration.setFetchLastLevelOnly(true); + allowedDynamicSourceConfiguration.setRelationType("AllowedZone"); + allowedZoneGroupConfiguration.setRefDynamicSourceConfiguration(allowedDynamicSourceConfiguration); + + ZoneGroupConfiguration restrictedZoneGroupConfiguration = new ZoneGroupConfiguration("zone", REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS, false); + var restrictedDynamicSourceConfiguration = new RelationQueryDynamicSourceConfiguration(); + restrictedDynamicSourceConfiguration.setDirection(EntitySearchDirection.FROM); + restrictedDynamicSourceConfiguration.setMaxLevel(1); + restrictedDynamicSourceConfiguration.setFetchLastLevelOnly(true); + restrictedDynamicSourceConfiguration.setRelationType("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()); + } + private CalculatedField createSimpleCalculatedField() { return createSimpleCalculatedField(device.getId()); } @@ -342,10 +456,10 @@ public class CalculatedFieldTest extends AbstractContainerTest { } private CalculatedField createScriptCalculatedField() { - return createScriptCalculatedField(device.getId()); + return createScriptCalculatedField(device.getId(), asset.getId()); } - private CalculatedField createScriptCalculatedField(EntityId entityId) { + private CalculatedField createScriptCalculatedField(EntityId entityId, EntityId refEntityId) { CalculatedField calculatedField = new CalculatedField(); calculatedField.setEntityId(entityId); calculatedField.setType(CalculatedFieldType.SCRIPT); @@ -355,12 +469,12 @@ public class CalculatedFieldTest extends AbstractContainerTest { 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 +510,12 @@ 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; + } + } 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/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.hybrid-test-extras.yml b/msa/black-box-tests/src/test/resources/docker-compose.hybrid-test-extras.yml index 0d527e9eb5..c213a2cb57 100644 --- a/msa/black-box-tests/src/test/resources/docker-compose.hybrid-test-extras.yml +++ b/msa/black-box-tests/src/test/resources/docker-compose.hybrid-test-extras.yml @@ -14,8 +14,6 @@ # limitations under the License. # -version: '3.0' - services: cassandra: environment: diff --git a/msa/black-box-tests/src/test/resources/docker-compose.mosquitto.yml b/msa/black-box-tests/src/test/resources/docker-compose.mosquitto.yml index ccc3f34599..cd09476924 100644 --- a/msa/black-box-tests/src/test/resources/docker-compose.mosquitto.yml +++ b/msa/black-box-tests/src/test/resources/docker-compose.mosquitto.yml @@ -14,7 +14,6 @@ # limitations under the License. # -version: '3.0' services: broker: image: eclipse-mosquitto diff --git a/msa/black-box-tests/src/test/resources/docker-compose.postgres-test-extras.yml b/msa/black-box-tests/src/test/resources/docker-compose.postgres-test-extras.yml index f88f3543fb..42b3f144a2 100644 --- a/msa/black-box-tests/src/test/resources/docker-compose.postgres-test-extras.yml +++ b/msa/black-box-tests/src/test/resources/docker-compose.postgres-test-extras.yml @@ -14,6 +14,4 @@ # limitations under the License. # -version: '3.0' - # Placeholder diff --git a/msa/black-box-tests/src/test/resources/docker-compose.rabbitmq-server.yml b/msa/black-box-tests/src/test/resources/docker-compose.rabbitmq-server.yml index 309bb6c0fa..0c89d2d6e7 100644 --- a/msa/black-box-tests/src/test/resources/docker-compose.rabbitmq-server.yml +++ b/msa/black-box-tests/src/test/resources/docker-compose.rabbitmq-server.yml @@ -14,8 +14,6 @@ # limitations under the License. # -version: '3.0' - services: rabbitmq: restart: always diff --git a/msa/black-box-tests/src/test/resources/docker-compose.valkey-ssl.volumes.yml b/msa/black-box-tests/src/test/resources/docker-compose.valkey-ssl.volumes.yml index 7da5f154a6..02a7c397a3 100644 --- a/msa/black-box-tests/src/test/resources/docker-compose.valkey-ssl.volumes.yml +++ b/msa/black-box-tests/src/test/resources/docker-compose.valkey-ssl.volumes.yml @@ -14,8 +14,6 @@ # limitations under the License. # -version: '3.0' - services: valkey: volumes: 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 73de64b303..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 @@ -14,14 +14,12 @@ # limitations under the License. # -version: '3.0' - services: # Valkey standalone # 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/edqs/pom.xml b/msa/edqs/pom.xml index 882ba588e1..2dd4a9a584 100644 --- a/msa/edqs/pom.xml +++ b/msa/edqs/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.0-SNAPSHOT + 4.3.0-SNAPSHOT msa org.thingsboard.msa 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 4d417081d8..fe56867dbe 100644 --- a/msa/js-executor/package.json +++ b/msa/js-executor/package.json @@ -1,25 +1,25 @@ { "name": "thingsboard-js-executor", "private": true, - "version": "4.2.0", + "version": "4.3.0", "description": "ThingsBoard JavaScript Executor Microservice", "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 95e4516044..9730e7f33b 100644 --- a/msa/js-executor/pom.xml +++ b/msa/js-executor/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.0-SNAPSHOT + 4.3.0-SNAPSHOT msa org.thingsboard.msa @@ -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/monitoring/pom.xml b/msa/monitoring/pom.xml index a7a8ff1b89..cb5b609e43 100644 --- a/msa/monitoring/pom.xml +++ b/msa/monitoring/pom.xml @@ -22,7 +22,7 @@ 4.0.0 org.thingsboard - 4.2.0-SNAPSHOT + 4.3.0-SNAPSHOT msa diff --git a/msa/pom.xml b/msa/pom.xml index b980bd703e..7fbc5d3c02 100644 --- a/msa/pom.xml +++ b/msa/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.0-SNAPSHOT + 4.3.0-SNAPSHOT thingsboard msa diff --git a/msa/tb-node/pom.xml b/msa/tb-node/pom.xml index 9f5c7cf358..8df294260b 100644 --- a/msa/tb-node/pom.xml +++ b/msa/tb-node/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.0-SNAPSHOT + 4.3.0-SNAPSHOT msa org.thingsboard.msa diff --git a/msa/tb/pom.xml b/msa/tb/pom.xml index 421cf1290f..1cbbf9d1ec 100644 --- a/msa/tb/pom.xml +++ b/msa/tb/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.0-SNAPSHOT + 4.3.0-SNAPSHOT msa org.thingsboard.msa diff --git a/msa/transport/coap/pom.xml b/msa/transport/coap/pom.xml index f0a46069db..256220078d 100644 --- a/msa/transport/coap/pom.xml +++ b/msa/transport/coap/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.msa - 4.2.0-SNAPSHOT + 4.3.0-SNAPSHOT transport org.thingsboard.msa.transport diff --git a/msa/transport/http/pom.xml b/msa/transport/http/pom.xml index 45f9323ab7..7e5871729d 100644 --- a/msa/transport/http/pom.xml +++ b/msa/transport/http/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.msa - 4.2.0-SNAPSHOT + 4.3.0-SNAPSHOT transport org.thingsboard.msa.transport diff --git a/msa/transport/lwm2m/pom.xml b/msa/transport/lwm2m/pom.xml index b95402ba14..ca6977eac6 100644 --- a/msa/transport/lwm2m/pom.xml +++ b/msa/transport/lwm2m/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.msa - 4.2.0-SNAPSHOT + 4.3.0-SNAPSHOT transport org.thingsboard.msa.transport diff --git a/msa/transport/mqtt/pom.xml b/msa/transport/mqtt/pom.xml index 2fbfc5b9f2..172c4facb0 100644 --- a/msa/transport/mqtt/pom.xml +++ b/msa/transport/mqtt/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.msa - 4.2.0-SNAPSHOT + 4.3.0-SNAPSHOT transport org.thingsboard.msa.transport diff --git a/msa/transport/pom.xml b/msa/transport/pom.xml index 5b57136d88..a6cff4fd0b 100644 --- a/msa/transport/pom.xml +++ b/msa/transport/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.0-SNAPSHOT + 4.3.0-SNAPSHOT msa org.thingsboard.msa diff --git a/msa/transport/snmp/pom.xml b/msa/transport/snmp/pom.xml index ee79717ca4..79734822aa 100644 --- a/msa/transport/snmp/pom.xml +++ b/msa/transport/snmp/pom.xml @@ -21,7 +21,7 @@ org.thingsboard.msa transport - 4.2.0-SNAPSHOT + 4.3.0-SNAPSHOT org.thingsboard.msa.transport diff --git a/msa/vc-executor-docker/pom.xml b/msa/vc-executor-docker/pom.xml index b81504aa06..85634d8688 100644 --- a/msa/vc-executor-docker/pom.xml +++ b/msa/vc-executor-docker/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.0-SNAPSHOT + 4.3.0-SNAPSHOT msa org.thingsboard.msa diff --git a/msa/vc-executor/pom.xml b/msa/vc-executor/pom.xml index e15fc6699c..0acd863612 100644 --- a/msa/vc-executor/pom.xml +++ b/msa/vc-executor/pom.xml @@ -21,7 +21,7 @@ org.thingsboard - 4.2.0-SNAPSHOT + 4.3.0-SNAPSHOT msa org.thingsboard.msa 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 de0c04fc9d..1eacf42683 100644 --- a/msa/web-ui/package.json +++ b/msa/web-ui/package.json @@ -1,26 +1,26 @@ { "name": "thingsboard-web-ui", "private": true, - "version": "4.2.0", + "version": "4.3.0", "description": "ThingsBoard Web UI Microservice", "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 4aeb5227c8..db7ed1424a 100644 --- a/msa/web-ui/pom.xml +++ b/msa/web-ui/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.0-SNAPSHOT + 4.3.0-SNAPSHOT msa org.thingsboard.msa @@ -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/netty-mqtt/pom.xml b/netty-mqtt/pom.xml index b30f1e2da3..a09d1e2409 100644 --- a/netty-mqtt/pom.xml +++ b/netty-mqtt/pom.xml @@ -19,11 +19,11 @@ 4.0.0 org.thingsboard - 4.2.0-SNAPSHOT + 4.3.0-SNAPSHOT thingsboard netty-mqtt - 4.2.0-SNAPSHOT + 4.3.0-SNAPSHOT jar Netty MQTT Client diff --git a/netty-mqtt/src/main/java/org/thingsboard/mqtt/ChannelClosedException.java b/netty-mqtt/src/main/java/org/thingsboard/mqtt/ChannelClosedException.java index 0b50b1883a..a1987cfe98 100644 --- a/netty-mqtt/src/main/java/org/thingsboard/mqtt/ChannelClosedException.java +++ b/netty-mqtt/src/main/java/org/thingsboard/mqtt/ChannelClosedException.java @@ -15,11 +15,11 @@ */ package org.thingsboard.mqtt; -/** - * Created by Valerii Sosliuk on 12/26/2017. - */ +import java.io.Serial; + public class ChannelClosedException extends RuntimeException { + @Serial private static final long serialVersionUID = 6266638352424706909L; public ChannelClosedException() { @@ -40,4 +40,5 @@ public class ChannelClosedException extends RuntimeException { public ChannelClosedException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { super(message, cause, enableSuppression, writableStackTrace); } + } diff --git a/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttClientCallback.java b/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttClientCallback.java index 85b4499e36..ae5168e466 100644 --- a/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttClientCallback.java +++ b/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttClientCallback.java @@ -21,9 +21,6 @@ import io.netty.handler.codec.mqtt.MqttPubAckMessage; import io.netty.handler.codec.mqtt.MqttSubAckMessage; import io.netty.handler.codec.mqtt.MqttUnsubAckMessage; -/** - * Created by Valerii Sosliuk on 12/30/2017. - */ public interface MqttClientCallback { /** @@ -53,4 +50,5 @@ public interface MqttClientCallback { default void onDisconnect(MqttMessage mqttDisconnectMessage) { } + } diff --git a/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttClientConfig.java b/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttClientConfig.java index 24feb3e58e..e2b6967f06 100644 --- a/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttClientConfig.java +++ b/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttClientConfig.java @@ -28,23 +28,46 @@ import java.util.Random; @SuppressWarnings({"WeakerAccess", "unused"}) public final class MqttClientConfig { + + @Getter private final SslContext sslContext; private final String randomClientId; @Getter @Setter private String ownerId; // [TenantId][IntegrationId] or [TenantId][RuleNodeId] for exceptions logging purposes + @Nonnull + @Getter private String clientId; + @Getter private int timeoutSeconds = 60; + @Getter private MqttVersion protocolVersion = MqttVersion.MQTT_3_1; - @Nullable private String username = null; - @Nullable private String password = null; + @Nullable + @Getter + @Setter + private String username = null; + @Nullable + @Getter + @Setter + private String password = null; + @Getter + @Setter private boolean cleanSession = true; - @Nullable private MqttLastWill lastWill; + @Nullable + @Getter + @Setter + private MqttLastWill lastWill; + @Setter + @Getter private Class channelClass = NioSocketChannel.class; + @Getter + @Setter private boolean reconnect = true; + @Getter private long reconnectDelay = 1L; + @Getter private int maxBytesInMessage = 8092; @Getter @@ -74,109 +97,37 @@ public final class MqttClientConfig { public MqttClientConfig(SslContext sslContext) { this.sslContext = sslContext; Random random = new Random(); - String id = "netty-mqtt/"; + StringBuilder id = new StringBuilder("netty-mqtt/"); String[] options = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789".split(""); - for(int i = 0; i < 8; i++){ - id += options[random.nextInt(options.length)]; + for (int i = 0; i < 8; i++) { + id.append(options[random.nextInt(options.length)]); } - this.clientId = id; - this.randomClientId = id; - } - - @Nonnull - public String getClientId() { - return clientId; + this.clientId = id.toString(); + this.randomClientId = id.toString(); } public void setClientId(@Nullable String clientId) { - if(clientId == null){ + if (clientId == null) { this.clientId = randomClientId; - }else{ + } else { this.clientId = clientId; } } - public int getTimeoutSeconds() { - return timeoutSeconds; - } - public void setTimeoutSeconds(int timeoutSeconds) { - if(timeoutSeconds != -1 && timeoutSeconds <= 0){ + if (timeoutSeconds != -1 && timeoutSeconds <= 0) { throw new IllegalArgumentException("timeoutSeconds must be > 0 or -1"); } this.timeoutSeconds = timeoutSeconds; } - public MqttVersion getProtocolVersion() { - return protocolVersion; - } - public void setProtocolVersion(MqttVersion protocolVersion) { - if(protocolVersion == null){ + if (protocolVersion == null) { throw new NullPointerException("protocolVersion"); } this.protocolVersion = protocolVersion; } - @Nullable - public String getUsername() { - return username; - } - - public void setUsername(@Nullable String username) { - this.username = username; - } - - @Nullable - public String getPassword() { - return password; - } - - public void setPassword(@Nullable String password) { - this.password = password; - } - - public boolean isCleanSession() { - return cleanSession; - } - - public void setCleanSession(boolean cleanSession) { - this.cleanSession = cleanSession; - } - - @Nullable - public MqttLastWill getLastWill() { - return lastWill; - } - - public void setLastWill(@Nullable MqttLastWill lastWill) { - this.lastWill = lastWill; - } - - public Class getChannelClass() { - return channelClass; - } - - public void setChannelClass(Class channelClass) { - this.channelClass = channelClass; - } - - public SslContext getSslContext() { - return sslContext; - } - - public boolean isReconnect() { - return reconnect; - } - - public void setReconnect(boolean reconnect) { - this.reconnect = reconnect; - } - - public long getReconnectDelay() { - return reconnectDelay; - } - /** * Sets the reconnect delay in seconds. Defaults to 1 second. * @param reconnectDelay @@ -189,10 +140,6 @@ public final class MqttClientConfig { this.reconnectDelay = reconnectDelay; } - public int getMaxBytesInMessage() { - return maxBytesInMessage; - } - /** * Sets the maximum number of bytes in the message for the {@link io.netty.handler.codec.mqtt.MqttDecoder}. * Default value is 8092 as specified by Netty. The absolute maximum size is 256MB as set by the MQTT spec. @@ -206,4 +153,5 @@ public final class MqttClientConfig { } this.maxBytesInMessage = maxBytesInMessage; } + } diff --git a/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttClientImpl.java b/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttClientImpl.java index 801470284b..f7901b4bb5 100644 --- a/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttClientImpl.java +++ b/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttClientImpl.java @@ -46,7 +46,9 @@ import io.netty.handler.timeout.IdleStateHandler; import io.netty.util.concurrent.DefaultPromise; import io.netty.util.concurrent.Future; import io.netty.util.concurrent.Promise; +import lombok.AccessLevel; import lombok.Getter; +import lombok.Setter; import lombok.extern.slf4j.Slf4j; import org.thingsboard.common.util.ListeningExecutor; @@ -67,18 +69,28 @@ import java.util.concurrent.atomic.AtomicInteger; @Slf4j final class MqttClientImpl implements MqttClient { + @Getter(AccessLevel.PACKAGE) private final Set serverSubscriptions = new HashSet<>(); + @Getter(AccessLevel.PACKAGE) private final ConcurrentMap pendingServerUnsubscribes = new ConcurrentHashMap<>(); + @Getter(AccessLevel.PACKAGE) private final ConcurrentMap qos2PendingIncomingPublishes = new ConcurrentHashMap<>(); + @Getter(AccessLevel.PACKAGE) private final ConcurrentMap pendingPublishes = new ConcurrentHashMap<>(); + @Getter(AccessLevel.PACKAGE) private final HashMultimap subscriptions = HashMultimap.create(); + @Getter(AccessLevel.PACKAGE) private final ConcurrentMap pendingSubscriptions = new ConcurrentHashMap<>(); + @Getter(AccessLevel.PACKAGE) private final Set pendingSubscribeTopics = new HashSet<>(); + @Getter(AccessLevel.PACKAGE) private final HashMultimap handlerToSubscription = HashMultimap.create(); private final AtomicInteger nextMessageId = new AtomicInteger(1); + @Getter private final MqttClientConfig clientConfig; + @Getter(AccessLevel.PACKAGE) private final MqttHandler defaultHandler; private final ReconnectStrategy reconnectStrategy; @@ -88,12 +100,15 @@ final class MqttClientImpl implements MqttClient { private volatile Channel channel; private volatile boolean disconnected = false; + @Getter private volatile boolean reconnect = false; private String host; private int port; @Getter + @Setter private MqttClientCallback callback; + @Getter private final ListeningExecutor handlerExecutor; private final static int DISCONNECT_FALLBACK_DELAY_SECS = 1; @@ -192,14 +207,14 @@ final class MqttClientImpl implements MqttClient { } private void scheduleConnectIfRequired(String host, int port, boolean reconnect) { - log.trace("[{}] Scheduling connect to server, isReconnect - {}", channel != null ? channel.id() : "UNKNOWN", reconnect); + log.trace("[{}][{}][{}] Scheduling connect to server, isReconnect - {}", host, port, channel != null ? channel.id() : "UNKNOWN", reconnect); if (clientConfig.isReconnect() && !disconnected) { if (reconnect) { this.reconnect = true; } final long nextReconnectDelay = reconnectStrategy.getNextReconnectDelay(); - log.info("[{}] Scheduling reconnect in [{}] sec", channel != null ? channel.id() : "UNKNOWN", nextReconnectDelay); + log.debug("[{}][{}][{}] Scheduling reconnect in [{}] sec", host, port, channel != null ? channel.id() : "UNKNOWN", nextReconnectDelay); eventLoop.schedule((Runnable) () -> connect(host, port, reconnect), nextReconnectDelay, TimeUnit.SECONDS); } } @@ -240,11 +255,6 @@ final class MqttClientImpl implements MqttClient { this.eventLoop = eventLoop; } - @Override - public ListeningExecutor getHandlerExecutor() { - return this.handlerExecutor; - } - /** * Subscribe on the given topic. When a message is received, MqttClient will invoke the {@link MqttHandler#onMessage(String, ByteBuf)} function of the given handler * @@ -446,59 +456,38 @@ final class MqttClientImpl implements MqttClient { return future; } - /** - * Retrieve the MqttClient configuration - * - * @return The {@link MqttClientConfig} instance we use - */ - @Override - public MqttClientConfig getClientConfig() { - return clientConfig; - } - @Override public void disconnect() { if (disconnected) { return; } + disconnected = true; log.trace("[{}] Disconnecting from server", channel != null ? channel.id() : "UNKNOWN"); if (this.channel != null) { MqttMessage message = new MqttMessage(new MqttFixedHeader(MqttMessageType.DISCONNECT, false, MqttQoS.AT_MOST_ONCE, false, 0)); sendAndFlushPacket(message).addListener((ChannelFutureListener) future -> { future.channel().close(); - disconnected = true; }); eventLoop.schedule(() -> { if (channel.isOpen()) { log.trace("[{}] Channel still open after {} second; forcing close now", channel.id(), DISCONNECT_FALLBACK_DELAY_SECS); this.channel.close(); - disconnected = true; } }, DISCONNECT_FALLBACK_DELAY_SECS, TimeUnit.SECONDS); } } - @Override - public void setCallback(MqttClientCallback callback) { - this.callback = callback; - } - ///////////////////////////////////////////// PRIVATE API ///////////////////////////////////////////// - public boolean isReconnect() { - return reconnect; - } - public void onSuccessfulReconnect() { if (callback != null) { callback.onSuccessfulReconnect(); } } - ChannelFuture sendAndFlushPacket(Object message) { if (this.channel == null) { return null; @@ -576,7 +565,7 @@ final class MqttClientImpl implements MqttClient { } private void checkSubscriptions(String topic, Promise promise) { - if (!(this.subscriptions.containsKey(topic) && this.subscriptions.get(topic).size() != 0) && this.serverSubscriptions.contains(topic)) { + if (!(this.subscriptions.containsKey(topic) && !this.subscriptions.get(topic).isEmpty()) && this.serverSubscriptions.contains(topic)) { MqttFixedHeader fixedHeader = new MqttFixedHeader(MqttMessageType.UNSUBSCRIBE, false, MqttQoS.AT_LEAST_ONCE, false, 0); MqttMessageIdVariableHeader variableHeader = getNewMessageId(); MqttUnsubscribePayload payload = new MqttUnsubscribePayload(Collections.singletonList(topic)); @@ -614,38 +603,6 @@ final class MqttClientImpl implements MqttClient { } } - ConcurrentMap getPendingSubscriptions() { - return pendingSubscriptions; - } - - HashMultimap getSubscriptions() { - return subscriptions; - } - - Set getPendingSubscribeTopics() { - return pendingSubscribeTopics; - } - - HashMultimap getHandlerToSubscription() { - return handlerToSubscription; - } - - Set getServerSubscriptions() { - return serverSubscriptions; - } - - ConcurrentMap getPendingServerUnsubscribes() { - return pendingServerUnsubscribes; - } - - ConcurrentMap getPendingPublishes() { - return pendingPublishes; - } - - ConcurrentMap getQos2PendingIncomingPublishes() { - return qos2PendingIncomingPublishes; - } - private class MqttChannelInitializer extends ChannelInitializer { private final Promise connectFuture; @@ -673,10 +630,7 @@ final class MqttClientImpl implements MqttClient { ch.pipeline().addLast("mqttPingHandler", new MqttPingHandler(MqttClientImpl.this.clientConfig.getTimeoutSeconds())); ch.pipeline().addLast("mqttHandler", new MqttChannelHandler(MqttClientImpl.this, connectFuture)); } - } - MqttHandler getDefaultHandler() { - return defaultHandler; } } diff --git a/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttConnectResult.java b/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttConnectResult.java index 67757d2a7a..909955d062 100644 --- a/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttConnectResult.java +++ b/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttConnectResult.java @@ -17,14 +17,18 @@ package org.thingsboard.mqtt; import io.netty.channel.ChannelFuture; import io.netty.handler.codec.mqtt.MqttConnectReturnCode; +import lombok.Getter; import lombok.ToString; @ToString @SuppressWarnings({"WeakerAccess", "unused"}) public final class MqttConnectResult { + @Getter private final boolean success; + @Getter private final MqttConnectReturnCode returnCode; + @Getter private final ChannelFuture closeFuture; MqttConnectResult(boolean success, MqttConnectReturnCode returnCode, ChannelFuture closeFuture) { @@ -33,16 +37,4 @@ public final class MqttConnectResult { this.closeFuture = closeFuture; } - public boolean isSuccess() { - return success; - } - - public MqttConnectReturnCode getReturnCode() { - return returnCode; - } - - public ChannelFuture getCloseFuture() { - return closeFuture; - } - } diff --git a/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttSubscription.java b/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttSubscription.java index 7ad93462ab..d5125757da 100644 --- a/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttSubscription.java +++ b/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttSubscription.java @@ -15,16 +15,23 @@ */ package org.thingsboard.mqtt; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.Setter; + import java.util.regex.Pattern; final class MqttSubscription { + @Getter(AccessLevel.PACKAGE) private final String topic; private final Pattern topicRegex; + @Getter private final MqttHandler handler; - + @Getter(AccessLevel.PACKAGE) private final boolean once; - + @Getter(AccessLevel.PACKAGE) + @Setter(AccessLevel.PACKAGE) private volatile boolean called; MqttSubscription(String topic, MqttHandler handler, boolean once) { @@ -40,22 +47,6 @@ final class MqttSubscription { this.topicRegex = Pattern.compile(topic.replace("+", "[^/]+").replace("#", ".+") + "$"); } - String getTopic() { - return topic; - } - - public MqttHandler getHandler() { - return handler; - } - - boolean isOnce() { - return once; - } - - boolean isCalled() { - return called; - } - boolean matches(String topic) { return this.topicRegex.matcher(topic).matches(); } @@ -78,7 +69,4 @@ final class MqttSubscription { return result; } - void setCalled(boolean called) { - this.called = called; - } } diff --git a/packaging/java/build.gradle b/packaging/java/build.gradle index 19cbeb126e..499c34c412 100644 --- a/packaging/java/build.gradle +++ b/packaging/java/build.gradle @@ -92,7 +92,7 @@ buildRpm { archiveVersion = projectVersion.replace('-', '') archiveFileName = "${pkgName}.rpm" - requires("java-17") + requires("(java-17 or java-17-headless or jre-17 or jre-17-headless)") // .or() notation does work in RPM plugin from("${buildDir}/conf") { include "${pkgName}.conf" diff --git a/pom.xml b/pom.xml index 64cdd63eaf..fc3da6c968 100755 --- a/pom.xml +++ b/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard thingsboard - 4.2.0-SNAPSHOT + 4.3.0-SNAPSHOT pom Thingsboard @@ -38,76 +38,48 @@ ${project.name} /var/log/${pkg.name} /usr/share/${pkg.name} - 3.0.0 - 4.0.2 + 3.4.8 2.4.0-b180830.0359 - 4.0.5 - 10.1.42 - 2.5.2 - 3.2.12 - 3.2.12 - 3.2.12 - 6.1.21 - 6.2.11 - 6.3.9 5.1.5 0.12.5 - 2.0.17 - 2.24.3 - 1.5.6 0.10 4.17.0 4.2.25 5.0.4 33.1.0-jre - 3.1.8 - 3.14.0 - 1.16.1 + 3.18.0 2.16.1 1.3.1 1.10.0 - 5.3.1 - 5.2.4 + 10.0.2 4.5.14 - 4.4.16 2.12.7 - 2.17.2 - 2.17.2 - 1.7.0 4.4.0 1.5.6 0.6.12 3.12.1 2.0.0-M15 - 2.10.1 - 2.3.32 2.0.1 5.6.0 3.9.3 3.25.5 - 1.63.0 - 1.2.6 - 1.18.32 + 1.68.1 + 1.2.8 + 1.18.38 1.2.5 1.2.5 - 4.1.119.Final - 2.0.65.Final - 1.1.18 1.7.1 - 5.21.0 3.2.5 3.4.0 - 2.4.0TB - 2.2.21 + 2.8.8TB + 2.2.30 0.8 1.19.0 1.78.1 2.0.1 - 42.7.7 org/thingsboard/server/gen/**/*, org/thingsboard/server/extensions/core/plugin/telemetry/gen/**/* - 8.13.2 0.4.5 15.4 4.0.2 - 3.0.2 1.7.5 3.8.0 - 2.9.0 + 1.1.0 + 2.38.0 + 1.24 + 1.11.0 + 3.49.3 + 0.27.0 + 1.7.0 - 4.2.1 2.7.3 1.5.6 - 5.10.5 5.15.0 1.3.0 1.2.7 5.0.0 7.10.1 - 3.25.3 - 5.4.0 - 2.2 - 1.20.4 - 1.0.1 + 1.20.6 + 1.0.2 1.12 - 4.19.1 5.8.0 2.27.0 2.12.0 @@ -170,6 +146,7 @@ 9.2.0 1.1.10.5 9.10.0 + 4.1.124.Final @@ -919,6 +896,27 @@ + + io.netty + netty-bom + ${netty.version} + pom + import + + + org.springframework.boot + spring-boot-dependencies + ${spring-boot.version} + pom + import + + + dev.langchain4j + langchain4j-bom + ${langchain4j.version} + pom + import + org.thingsboard netty-mqtt @@ -1134,100 +1132,11 @@ test-jar test - - jakarta.annotation - jakarta.annotation-api - ${jakarta-annotation.version} - - - jakarta.xml.bind - jakarta.xml.bind-api - ${jakarta.xml.bind-api.version} - javax.xml.bind jaxb-api ${javax.xml.bind-api.version} - - org.glassfish.jaxb - jaxb-runtime - ${jaxb-runtime.version} - - - org.apache.tomcat.embed - tomcat-embed-core - ${tomcat.version} - - - org.apache.tomcat.embed - tomcat-embed-el - ${tomcat.version} - - - org.apache.tomcat.embed - tomcat-embed-websocket - ${tomcat.version} - - - - net.minidev - json-smart - ${net.minidev.json-smart} - - - - org.springframework.boot - spring-boot-starter - ${spring-boot.version} - - - org.springframework.boot - spring-boot-starter-security - ${spring-boot.version} - - - org.springframework.security - spring-security-oauth2-client - ${spring-security.version} - - - org.springframework.security - spring-security-oauth2-jose - ${spring-security.version} - - - - org.springframework.security - spring-security-config - ${spring-security.version} - - - org.springframework.security - spring-security-web - ${spring-security.version} - - - - org.springframework - spring-core - ${spring.version} - - - org.springframework.boot - spring-boot-starter-web - ${spring-boot.version} - - - org.springframework.boot - spring-boot-starter-websocket - ${spring-boot.version} - - - org.springframework.boot - spring-boot-autoconfigure - ${spring-boot.version} - org.springframework.boot spring-boot-starter-test @@ -1240,62 +1149,11 @@ - - org.springframework.boot - spring-boot-starter-data-jpa - ${spring-boot.version} - - - org.springframework.data - spring-data-commons - ${spring-data.version} - - - org.springframework.boot - spring-boot-starter-webflux - ${spring-boot.version} - - - io.projectreactor.netty - reactor-netty-http - ${reactor-netty.version} - org.apache.kafka kafka-clients ${kafka.version} - - org.postgresql - postgresql - ${postgresql.driver.version} - - - org.springframework - spring-context - ${spring.version} - - - org.springframework - spring-context-support - ${spring.version} - - - org.springframework - spring-tx - ${spring.version} - - - org.springframework - spring-web - ${spring.version} - - - org.springframework.security - spring-security-test - ${spring-security.version} - test - com.github.springtestdbunit spring-test-dbunit @@ -1307,11 +1165,6 @@ jjwt ${jjwt.version} - - org.freemarker - freemarker - ${freemarker.version} - org.yaml snakeyaml @@ -1322,11 +1175,6 @@ antlr ${antlr.version} - - com.rabbitmq - amqp-client - ${rabbitmq.version} - com.sun.mail jakarta.mail @@ -1354,150 +1202,16 @@ - - com.jayway.jsonpath - json-path - ${json-path.version} - - - com.jayway.jsonpath - json-path-assert - ${json-path.version} - test - - - io.netty - netty-all - ${netty.version} - - - io.netty - netty-tcnative-boringssl-static - ${netty-tcnative.version} - - - io.netty - netty-tcnative-classes - ${netty-tcnative.version} - - - io.netty - netty-buffer - ${netty.version} - - - io.netty - netty-codec - ${netty.version} - - - io.netty - netty-codec-dns - ${netty.version} - - - io.netty - netty-codec-http - ${netty.version} - - - io.netty - netty-codec-http2 - ${netty.version} - - - io.netty - netty-codec-mqtt - ${netty.version} - - - io.netty - netty-codec-socks - ${netty.version} - - - io.netty - netty-common - ${netty.version} - - - io.netty - netty-handler - ${netty.version} - - - io.netty - netty-handler-proxy - ${netty.version} - - - io.netty - netty-resolver - ${netty.version} - - - io.netty - netty-resolver-dns - ${netty.version} - - - io.netty - netty-resolver-dns-classes-macos - ${netty.version} - io.netty netty-resolver-dns-native-macos - ${netty.version} - - - io.netty - netty-resolver-dns-native-macos - ${netty.version} osx-x86_64 - - io.netty - netty-transport - ${netty.version} - - - io.netty - netty-transport-classes-epoll - ${netty.version} - - - io.netty - netty-transport-classes-kqueue - ${netty.version} - - - io.netty - netty-transport-native-epoll - ${netty.version} - - - io.netty - netty-transport-native-epoll - ${netty.version} - linux-x86_64 - - - io.netty - netty-transport-native-kqueue - ${netty.version} - io.netty netty-transport-native-kqueue - ${netty.version} osx-x86_64 - - io.netty - netty-transport-native-unix-common - ${netty.version} - com.datastax.oss java-driver-core @@ -1523,11 +1237,6 @@ commons-io ${commons-io.version} - - commons-codec - commons-codec - ${commons-codec.version} - commons-logging commons-logging @@ -1538,16 +1247,6 @@ commons-csv ${commons-csv.version} - - org.apache.httpcomponents.client5 - httpclient5 - ${apache-httpclient5.version} - - - org.apache.httpcomponents.core5 - httpcore5 - ${apache-httpcore5.version} - org.apache.httpcomponents httpclient @@ -1559,66 +1258,11 @@ - - org.apache.httpcomponents - httpcore - ${apache-httpcore.version} - joda-time joda-time ${joda-time.version} - - com.fasterxml.jackson.core - jackson-databind - ${jackson-databind.version} - - - com.fasterxml.jackson.core - jackson-core - ${jackson.version} - - - com.fasterxml.jackson.core - jackson-annotations - ${jackson.version} - - - com.fasterxml.jackson.dataformat - jackson-dataformat-cbor - ${jackson.version} - - - com.fasterxml.jackson.dataformat - jackson-dataformat-yaml - ${jackson.version} - - - com.fasterxml.jackson.datatype - jackson-datatype-jdk8 - ${jackson.version} - - - com.fasterxml.jackson.datatype - jackson-datatype-joda - ${jackson.version} - - - com.fasterxml.jackson.datatype - jackson-datatype-jsr310 - ${jackson.version} - - - com.fasterxml.jackson.module - jackson-module-parameter-names - ${jackson.version} - - - com.fasterxml - classmate - ${fasterxml-classmate.version} - com.auth0 java-jwt @@ -1673,61 +1317,11 @@ scandium ${californium.version} - - com.google.code.gson - gson - ${gson.version} - - - org.slf4j - slf4j-api - ${slf4j.version} - - - org.slf4j - log4j-over-slf4j - ${slf4j.version} - - - org.slf4j - jul-to-slf4j - ${slf4j.version} - - - org.apache.logging.log4j - log4j-api - ${log4j.version} - - - org.apache.logging.log4j - log4j-core - ${log4j.version} - - - org.apache.logging.log4j - log4j-to-slf4j - ${log4j.version} - - - ch.qos.logback - logback-core - ${logback.version} - - - ch.qos.logback - logback-classic - ${logback.version} - com.google.guava guava ${guava.version} - - com.github.ben-manes.caffeine - caffeine - ${caffeine.version} - com.google.protobuf protobuf-java @@ -1823,12 +1417,6 @@ tbel ${tbel.version} - - org.springframework - spring-test - ${spring.version} - test - io.takari.junit takari-cpsuite @@ -1846,42 +1434,12 @@ cassandra-all ${cassandra-all.version} - - org.junit.vintage - junit-vintage-engine - ${jupiter.version} - test - - - org.hamcrest - hamcrest-core - - - org.testng testng ${testng.version} test - - org.assertj - assertj-core - ${assertj.version} - test - - - io.rest-assured - rest-assured - ${rest-assured.version} - test - - - org.seleniumhq.selenium - selenium-java - ${selenium.version} - test - io.github.bonigarcia webdrivermanager @@ -1900,18 +1458,6 @@ ${allure-maven.version} test - - org.hamcrest - hamcrest - ${hamcrest.version} - test - - - org.awaitility - awaitility - ${awaitility.version} - test - org.dbunit dbunit @@ -1969,40 +1515,6 @@ bcprov-ext-jdk18on ${bouncycastle.version} - - org.testcontainers - cassandra - ${testcontainers.version} - test - - - org.testcontainers - postgresql - ${testcontainers.version} - test - - - org.testcontainers - jdbc - ${testcontainers.version} - test - - - org.testcontainers - hivemq - ${testcontainers.version} - test - - - org.springframework.data - spring-data-redis - ${spring-data-redis.version} - - - org.springframework.integration - spring-integration-redis - ${spring-redis.version} - redis.clients jedis @@ -2016,17 +1528,6 @@ exe provided - - org.elasticsearch.client - elasticsearch-rest-client - ${elasticsearch.version} - - - commons-logging - commons-logging - - - org.javadelight delight-nashorn-sandbox @@ -2079,10 +1580,55 @@ google-cloud-pubsub ${pubsub.client.version} + + com.google.auth + google-auth-library-credentials + ${google-auth-library.version} + + + com.google.auth + google-auth-library-oauth2-http + ${google-auth-library.version} + + + com.google.http-client + google-http-client + ${google-http-client.version} + + + com.google.http-client + google-http-client-gson + ${google-http-client.version} + + + com.google.api + api-common + ${google-api-common.version} + + + com.google.api + gax + ${google-api-gax.version} + + + com.google.api + gax-grpc + ${google-api-gax.version} + + + com.google.api + gax-httpjson + ${google-api-gax.version} + com.google.api.grpc proto-google-common-protos - ${google.common.protos.version} + ${google-proto-common.version} + + + com.google.api.grpc + proto-google-iam-v1 + ${google-proto-iam-v1.version} org.passay @@ -2110,21 +1656,6 @@ ${java-websocket.version} test - - org.springframework.boot - spring-boot-starter-actuator - ${spring-boot.version} - - - io.micrometer - micrometer-core - ${micrometer.version} - - - io.micrometer - micrometer-registry-prometheus - ${micrometer.version} - org.thingsboard protobuf-dynamic @@ -2154,11 +1685,6 @@ - - org.hibernate.validator - hibernate-validator - ${hibernate-validator.version} - io.hypersistence hypersistence-utils-hibernate-63 @@ -2169,11 +1695,6 @@ jakarta.el ${jakarta.el.version} - - jakarta.validation - jakarta.validation-api - ${jakarta.validation-api.version} - org.owasp.antisamy antisamy @@ -2248,6 +1769,11 @@ + + com.nimbusds + nimbus-jose-jwt + ${nimbus-jose-jwt.version} + org.mock-server mockserver-client-java @@ -2336,6 +1862,36 @@ rocksdbjni ${rocksdbjni.version} + + com.google.errorprone + error_prone_annotations + ${error_prone_annotations.version} + + + org.codehaus.mojo + animal-sniffer-annotations + ${animal-sniffer-annotations.version} + + + com.google.auto.value + auto-value-annotations + ${auto-value-annotations.version} + + + org.checkerframework + checker-qual + ${checker-qual.version} + + + io.perfmark + perfmark-api + ${perfmark-api.version} + + + org.threeten + threetenbp + ${threetenbp.version} + diff --git a/rest-client/pom.xml b/rest-client/pom.xml index 392c3b360f..c3cf2460dd 100644 --- a/rest-client/pom.xml +++ b/rest-client/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.0-SNAPSHOT + 4.3.0-SNAPSHOT thingsboard rest-client 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..0e559efd46 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 @@ -127,6 +127,7 @@ 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; @@ -2476,7 +2477,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 +2496,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)); diff --git a/rule-engine/pom.xml b/rule-engine/pom.xml index aa561832b3..7abb7be3a3 100644 --- a/rule-engine/pom.xml +++ b/rule-engine/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.0-SNAPSHOT + 4.3.0-SNAPSHOT thingsboard rule-engine diff --git a/rule-engine/rule-engine-api/pom.xml b/rule-engine/rule-engine-api/pom.xml index 6ab5e66904..59397d4a92 100644 --- a/rule-engine/rule-engine-api/pom.xml +++ b/rule-engine/rule-engine-api/pom.xml @@ -22,7 +22,7 @@ 4.0.0 org.thingsboard - 4.2.0-SNAPSHOT + 4.3.0-SNAPSHOT rule-engine org.thingsboard.rule-engine @@ -98,6 +98,10 @@ jakarta.mail provided + + dev.langchain4j + langchain4j + org.springframework.boot spring-boot-starter-test diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleEngineAiChatModelService.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleEngineAiChatModelService.java new file mode 100644 index 0000000000..e76cb6b3b2 --- /dev/null +++ b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleEngineAiChatModelService.java @@ -0,0 +1,27 @@ +/** + * 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.api; + +import com.google.common.util.concurrent.FluentFuture; +import dev.langchain4j.model.chat.request.ChatRequest; +import dev.langchain4j.model.chat.response.ChatResponse; +import org.thingsboard.server.common.data.ai.model.chat.AiChatModelConfig; + +public interface RuleEngineAiChatModelService { + + > FluentFuture sendChatRequestAsync(AiChatModelConfig chatModelConfig, ChatRequest chatRequest); + +} 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 48fda3b781..edcbffcfbc 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 @@ -16,6 +16,7 @@ package org.thingsboard.rule.engine.api; import com.fasterxml.jackson.databind.JsonNode; +import com.google.common.util.concurrent.FluentFuture; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import org.thingsboard.server.common.data.EntitySubtype; @@ -41,9 +42,6 @@ import org.thingsboard.server.common.data.query.AlarmDataQuery; import java.util.Collection; -/** - * Created by ashvayka on 02.04.18. - */ public interface RuleEngineAlarmService { /* @@ -78,6 +76,8 @@ public interface RuleEngineAlarmService { Alarm findLatestActiveByOriginatorAndType(TenantId tenantId, EntityId originator, String type); + FluentFuture findLatestActiveByOriginatorAndTypeAsync(TenantId tenantId, EntityId originator, String type); + Alarm findLatestByOriginatorAndType(TenantId tenantId, EntityId originator, String type); AlarmInfo findAlarmInfoById(TenantId tenantId, AlarmId alarmId); 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 16f2936964..d2687a1b10 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 @@ -44,6 +44,7 @@ import org.thingsboard.server.common.data.rule.RuleNodeState; import org.thingsboard.server.common.data.script.ScriptLanguage; 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.alarm.AlarmCommentService; import org.thingsboard.server.dao.asset.AssetProfileService; import org.thingsboard.server.dao.asset.AssetService; @@ -422,6 +423,10 @@ public interface TbContext { AuditLogService getAuditLogService(); + RuleEngineAiChatModelService getAiChatModelService(); + + AiModelService getAiModelService(); + // Configuration parameters for the MQTT client that is used in the MQTT node and Azure IoT hub node MqttClientSettings getMqttClientSettings(); diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbNodeException.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbNodeException.java index 874a37792d..7d5a7443d3 100644 --- a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbNodeException.java +++ b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbNodeException.java @@ -16,12 +16,9 @@ package org.thingsboard.rule.engine.api; import lombok.Getter; -import org.thingsboard.server.common.msg.TbActorError; +import org.thingsboard.common.util.RecoveryAware; -/** - * Created by ashvayka on 19.01.18. - */ -public class TbNodeException extends Exception implements TbActorError { +public class TbNodeException extends Exception implements RecoveryAware { @Getter private final boolean unrecoverable; 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-api/src/main/java/org/thingsboard/rule/engine/api/util/TbNodeUtils.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/util/TbNodeUtils.java index 78ddba15d1..ae8faecb0b 100644 --- a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/util/TbNodeUtils.java +++ b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/util/TbNodeUtils.java @@ -16,11 +16,11 @@ package org.thingsboard.rule.engine.api.util; import com.fasterxml.jackson.databind.JsonNode; -import org.springframework.util.CollectionUtils; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.server.common.data.StringUtils; +import org.thingsboard.server.common.data.util.CollectionsUtil; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; @@ -29,15 +29,18 @@ import java.util.List; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; -import java.util.stream.Collectors; -/** - * Created by ashvayka on 19.01.18. - */ -public class TbNodeUtils { +public final class TbNodeUtils { + + private TbNodeUtils() { + throw new IllegalStateException("Utility class"); + } private static final Pattern DATA_PATTERN = Pattern.compile("(\\$\\[)(.*?)(])"); + private static final String ALL_DATA_TEMPLATE = "$[*]"; + private static final String ALL_METADATA_TEMPLATE = "${*}"; + public static T convert(TbNodeConfiguration configuration, Class clazz) throws TbNodeException { try { return JacksonUtil.treeToValue(configuration.getData(), clazz); @@ -47,16 +50,19 @@ public class TbNodeUtils { } public static List processPatterns(List patterns, TbMsg tbMsg) { - if (!CollectionUtils.isEmpty(patterns)) { - return patterns.stream().map(p -> processPattern(p, tbMsg)).collect(Collectors.toList()); + if (CollectionsUtil.isEmpty(patterns)) { + return Collections.emptyList(); } - return Collections.emptyList(); + return patterns.stream().map(p -> processPattern(p, tbMsg)).toList(); } public static String processPattern(String pattern, TbMsg tbMsg) { try { String result = processPattern(pattern, tbMsg.getMetaData()); JsonNode json = JacksonUtil.toJsonNode(tbMsg.getData()); + + result = result.replace(ALL_DATA_TEMPLATE, JacksonUtil.toString(json)); + if (json.isObject()) { Matcher matcher = DATA_PATTERN.matcher(result); while (matcher.find()) { @@ -64,7 +70,7 @@ public class TbNodeUtils { String[] keys = group.split("\\."); JsonNode jsonNode = json; for (String key : keys) { - if (!StringUtils.isEmpty(key) && jsonNode != null) { + if (StringUtils.isNotEmpty(key) && jsonNode != null) { jsonNode = jsonNode.get(key); } else { jsonNode = null; @@ -83,15 +89,9 @@ public class TbNodeUtils { } } - @Deprecated(since = "3.6.1", forRemoval = true) - public static List processPatterns(List patterns, TbMsgMetaData metaData) { - if (!CollectionUtils.isEmpty(patterns)) { - return patterns.stream().map(p -> processPattern(p, metaData)).collect(Collectors.toList()); - } - return Collections.emptyList(); - } - - public static String processPattern(String pattern, TbMsgMetaData metaData) { + private static String processPattern(String pattern, TbMsgMetaData metaData) { + String replacement = metaData.isEmpty() ? "{}" : JacksonUtil.toString(metaData.getData()); + pattern = pattern.replace(ALL_METADATA_TEMPLATE, replacement); return processTemplate(pattern, metaData.values()); } @@ -108,10 +108,11 @@ public class TbNodeUtils { } static String formatDataVarTemplate(String key) { - return "$[" + key + ']'; + return "$[" + key + "]"; } static String formatMetadataVarTemplate(String key) { - return "${" + key + '}'; + return "${" + key + "}"; } + } diff --git a/rule-engine/rule-engine-api/src/test/java/org/thingsboard/rule/engine/api/util/TbNodeUtilsTest.java b/rule-engine/rule-engine-api/src/test/java/org/thingsboard/rule/engine/api/util/TbNodeUtilsTest.java index 7651a46b62..89d775f305 100644 --- a/rule-engine/rule-engine-api/src/test/java/org/thingsboard/rule/engine/api/util/TbNodeUtilsTest.java +++ b/rule-engine/rule-engine-api/src/test/java/org/thingsboard/rule/engine/api/util/TbNodeUtilsTest.java @@ -26,6 +26,8 @@ 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.Map; + import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; @@ -167,4 +169,160 @@ public class TbNodeUtilsTest { assertThat(TbNodeUtils.formatMetadataVarTemplate(null), is("${null}")); assertThat(TbNodeUtils.formatMetadataVarTemplate(null), is(String.format(METADATA_VARIABLE_TEMPLATE, (String) null))); } + + @Test + public void testAllMetadataTemplateReplacement() { + // GIVEN + String pattern = "META ${*}"; + var metadata = new TbMsgMetaData(); + metadata.putValue("meta_key", "meta_value"); + + var msg = TbMsg.newMsg() + .data(TbMsg.EMPTY_JSON_OBJECT) + .metaData(metadata) + .build(); + + // WHEN + String actual = TbNodeUtils.processPattern(pattern, msg); + + // THEN + String expected = "META {\"meta_key\":\"meta_value\"}"; + assertThat(actual, is(expected)); + } + + @Test + public void testMultipleAllMetadataTemplatesReplacement() { + // GIVEN + String pattern = "${*} then again ${*}"; + var metadata = new TbMsgMetaData(); + metadata.putValue("meta_key", "meta_value"); + + var msg = TbMsg.newMsg() + .data(TbMsg.EMPTY_JSON_OBJECT) + .metaData(metadata) + .build(); + + // WHEN + String actual = TbNodeUtils.processPattern(pattern, msg); + + // THEN + String expected = "{\"meta_key\":\"meta_value\"} then again {\"meta_key\":\"meta_value\"}"; + assertThat(actual, is(expected)); + } + + @Test + public void testAllDataTemplateReplacement() { + // GIVEN + String pattern = "DATA $[*]"; + var dataJson = JacksonUtil.newObjectNode().put("data_key", "data_value"); + + var msg = TbMsg.newMsg() + .data(JacksonUtil.toString(dataJson)) + .metaData(TbMsgMetaData.EMPTY) + .build(); + + // WHEN + String actual = TbNodeUtils.processPattern(pattern, msg); + + // THEN + String expected = "DATA {\"data_key\":\"data_value\"}"; + assertThat(actual, is(expected)); + } + + @Test + public void testMultipleAllDataTemplatesReplacement() { + // GIVEN + String pattern = "$[*] then again $[*]"; + var dataJson = JacksonUtil.newObjectNode().put("data_key", "data_value"); + + var msg = TbMsg.newMsg() + .data(JacksonUtil.toString(dataJson)) + .metaData(TbMsgMetaData.EMPTY) + .build(); + + // WHEN + String actual = TbNodeUtils.processPattern(pattern, msg); + + // THEN + String expected = "{\"data_key\":\"data_value\"} then again {\"data_key\":\"data_value\"}"; + assertThat(actual, is(expected)); + } + + @Test + public void testAllDataAndAllMetadataTemplatesSimultaneously() { + // GIVEN + String pattern = "META ${*} DATA $[*]"; + + var metadata = new TbMsgMetaData(Map.of("meta_key", "meta_value")); + var dataJson = JacksonUtil.newObjectNode().put("data_key", "data_value"); + + var msg = TbMsg.newMsg() + .data(JacksonUtil.toString(dataJson)) + .metaData(metadata) + .build(); + + // WHEN + String actual = TbNodeUtils.processPattern(pattern, msg); + + // THEN + String expected = "META {\"meta_key\":\"meta_value\"} DATA {\"data_key\":\"data_value\"}"; + assertThat(actual, is(expected)); + } + + @Test + public void testAllDataAndAllMetadataTemplatesSimultaneouslyEmpty() { + // GIVEN + String pattern = "META ${*} DATA $[*]"; + + var msg = TbMsg.newMsg() + .data(TbMsg.EMPTY_JSON_OBJECT) + .metaData(TbMsgMetaData.EMPTY) + .build(); + + // WHEN + String actual = TbNodeUtils.processPattern(pattern, msg); + + // THEN + String expected = "META {} DATA {}"; + assertThat(actual, is(expected)); + } + + @Test + public void testAllDataTemplateArray() { + // GIVEN + String pattern = "DATA $[*]"; + + var msg = TbMsg.newMsg() + .data("[1, \"two\", true]") + .metaData(TbMsgMetaData.EMPTY) + .build(); + + // WHEN + String actual = TbNodeUtils.processPattern(pattern, msg); + + // THEN + String expected = "DATA [1,\"two\",true]"; + assertThat(actual, is(expected)); + } + + @Test + public void testMixedAllDataMetadataAndNormalTemplates() { + // GIVEN + String pattern = "fullMeta=${*}, singleMeta=${meta_key}, fullData=$[*], singleData=$[data_key]"; + var metadata = new TbMsgMetaData(Map.of("meta_key", "meta_value")); + var dataJson = JacksonUtil.newObjectNode().put("data_key", "data_value"); + + var msg = TbMsg.newMsg() + .data(JacksonUtil.toString(dataJson)) + .metaData(metadata) + .build(); + + // WHEN + String actual = TbNodeUtils.processPattern(pattern, msg); + + // THEN + String expected = "fullMeta={\"meta_key\":\"meta_value\"}, singleMeta=meta_value, fullData={\"data_key\":\"data_value\"}, singleData=data_value"; + assertThat(actual, is(expected)); + } + } diff --git a/rule-engine/rule-engine-components/pom.xml b/rule-engine/rule-engine-components/pom.xml index f2edca7b94..98c7fde665 100644 --- a/rule-engine/rule-engine-components/pom.xml +++ b/rule-engine/rule-engine-components/pom.xml @@ -22,7 +22,7 @@ 4.0.0 org.thingsboard - 4.2.0-SNAPSHOT + 4.3.0-SNAPSHOT rule-engine org.thingsboard.rule-engine @@ -153,6 +153,10 @@ com.jayway.jsonpath json-path + + dev.langchain4j + langchain4j + 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..fc541ce4e4 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,18 +29,21 @@ 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" ) @@ -55,22 +56,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 +84,5 @@ public class TbClearAlarmNode extends TbAbstractAlarmNode attributes = JsonConverter.convertToAttributes(JsonParser.parseString(msg.getData())); - List filteredAttributes = - attributes.stream().filter(attr -> attributeContainsInEntityView(scope, attr.getKey(), entityView)).collect(Collectors.toList()); + List attributes = JsonConverter.convertToAttributes(JsonParser.parseString(msg.getData())).stream() + .filter(attr -> attributeContainsInEntityView(scope, attr.getKey(), entityView)).toList(); ctx.getTelemetryService().saveAttributes(AttributesSaveRequest.builder() .tenantId(ctx.getTenantId()) .entityId(entityView.getId()) .scope(scope) - .entries(filteredAttributes) + .entries(attributes) .callback(getFutureCallback(ctx, msg, entityView)) .build()); } 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 78411d7cec..bd576be936 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 @@ -70,7 +70,6 @@ public class TbCreateAlarmNode extends TbAbstractAlarmNode { + if (existingAlarm == null || existingAlarm.getStatus().isCleared()) { + return createNewAlarm(ctx, msg, msgAlarm); + } else { + return updateAlarm(ctx, msg, existingAlarm, msgAlarm); + } + }, ctx.getDbCallbackExecutor()); } private Alarm getAlarmFromMessage(TbContext ctx, TbMsg msg) throws IOException { diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/Langchain4jJsonSchemaAdapter.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/Langchain4jJsonSchemaAdapter.java new file mode 100644 index 0000000000..b04d4592a2 --- /dev/null +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/Langchain4jJsonSchemaAdapter.java @@ -0,0 +1,136 @@ +/** + * 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.ai; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import dev.langchain4j.model.chat.request.json.JsonArraySchema; +import dev.langchain4j.model.chat.request.json.JsonBooleanSchema; +import dev.langchain4j.model.chat.request.json.JsonEnumSchema; +import dev.langchain4j.model.chat.request.json.JsonIntegerSchema; +import dev.langchain4j.model.chat.request.json.JsonNullSchema; +import dev.langchain4j.model.chat.request.json.JsonNumberSchema; +import dev.langchain4j.model.chat.request.json.JsonObjectSchema; +import dev.langchain4j.model.chat.request.json.JsonSchema; +import dev.langchain4j.model.chat.request.json.JsonSchemaElement; +import dev.langchain4j.model.chat.request.json.JsonStringSchema; + +import java.util.ArrayList; +import java.util.List; + +/** + * Converts a Jackson {@link ObjectNode} JSON Schema into a Langchain4j {@link JsonSchema} model. + */ +final class Langchain4jJsonSchemaAdapter { + + private Langchain4jJsonSchemaAdapter() { + throw new AssertionError("Can't instantiate utility class"); + } + + /** + * Creates a Langchain4j {@link JsonSchema} from the given root JSON Schema node. + * + * @param rootSchemaNode a valid JSON Schema as a Jackson {@link ObjectNode} + * @return the corresponding Langchain4j {@link JsonSchema} + */ + public static JsonSchema fromObjectNode(ObjectNode rootSchemaNode) { + return JsonSchema.builder() + .name(rootSchemaNode.get("title").textValue()) + .rootElement(parse(rootSchemaNode)) + .build(); + } + + private static JsonSchemaElement parse(JsonNode schemaNode) { + String description = schemaNode.hasNonNull("description") ? schemaNode.get("description").textValue() : null; + + if (schemaNode.has("enum")) { // enum schemas can be defined without 'type' + return parseEnum(schemaNode).description(description).build(); + } + + String type = schemaNode.get("type").textValue(); + + return switch (type) { + case "string" -> JsonStringSchema.builder().description(description).build(); + case "integer" -> JsonIntegerSchema.builder().description(description).build(); + case "boolean" -> JsonBooleanSchema.builder().description(description).build(); + case "number" -> JsonNumberSchema.builder().description(description).build(); + case "null" -> new JsonNullSchema(); + case "object" -> parseObject(schemaNode).description(description).build(); + case "array" -> parseArray(schemaNode).description(description).build(); + default -> throw new IllegalArgumentException("Unsupported JSON Schema type: " + type); + }; + } + + private static JsonEnumSchema.Builder parseEnum(JsonNode enumSchema) { + var builder = new JsonEnumSchema.Builder(); + + List enumValues = new ArrayList<>(); + for (JsonNode element : enumSchema.get("enum")) { + if (!element.isTextual()) { + throw new IllegalArgumentException("Expected each 'enum' element to be a string, but found: " + element.getNodeType()); + } + enumValues.add(element.textValue()); + } + builder.enumValues(enumValues); + + return builder; + } + + private static JsonObjectSchema.Builder parseObject(JsonNode objectSchema) { + var builder = new JsonObjectSchema.Builder(); + + JsonNode propertiesNode = objectSchema.get("properties"); + if (propertiesNode != null) { + propertiesNode.fields().forEachRemaining(entry -> { + String key = entry.getKey(); + JsonNode value = entry.getValue(); + builder.addProperty(key, parse(value)); + }); + } + + List required = new ArrayList<>(); + JsonNode requiredNode = objectSchema.get("required"); + if (requiredNode != null) { + for (JsonNode value : requiredNode) { + required.add(value.textValue()); + } + } + builder.required(required); + + boolean additionalProperties = true; // default value if 'additionalProperties' is not set + JsonNode additionalPropertiesNode = objectSchema.get("additionalProperties"); + if (additionalPropertiesNode != null) { + if (!additionalPropertiesNode.isBoolean()) { + throw new IllegalArgumentException("Expected 'additionalProperties' to be a boolean, but found: " + additionalPropertiesNode.getNodeType()); + } + additionalProperties = additionalPropertiesNode.booleanValue(); + } + builder.additionalProperties(additionalProperties); + + return builder; + } + + private static JsonArraySchema.Builder parseArray(JsonNode arraySchema) { + var builder = new JsonArraySchema.Builder(); + + if (arraySchema.hasNonNull("items")) { + builder.items(parse(arraySchema.get("items"))); + } + + return builder; + } + +} 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 new file mode 100644 index 0000000000..3497795771 --- /dev/null +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNode.java @@ -0,0 +1,204 @@ +/** + * 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.ai; + +import com.fasterxml.jackson.databind.JsonNode; +import com.google.common.util.concurrent.FluentFuture; +import com.google.common.util.concurrent.FutureCallback; +import dev.langchain4j.data.message.ChatMessage; +import dev.langchain4j.data.message.SystemMessage; +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 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; +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.rule.engine.external.TbAbstractExternalNode; +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.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 java.util.ArrayList; +import java.util.List; +import java.util.NoSuchElementException; +import java.util.Optional; + +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; + +@RuleNode( + type = ComponentType.EXTERNAL, + name = "AI request", + nodeDescription = "Sends a request to an AI model using system and user prompts. Supports JSON mode.", + nodeDetails = """ + Interact with large language models (LLMs) by sending dynamic requests from your rule chain. + You can select a specific AI model and define its behavior using a system prompt (optional context or role) and a user prompt (the main task). + Both prompts can be populated with data and metadata from the incoming message using patterns. + For example, the $[*] and ${*} patterns allow you to access the all message body and all metadata, respectively. +

+ After sending the request, the node waits for a response within a configured timeout. + You can specify the desired response format as Text, JSON, or provide a specific JSON Schema to structure the output. + The AI-generated content is forwarded as the body of the outgoing message; the originator, message type, and metadata from the incoming message remain unchanged. +

+ Output connections: Success, Failure. + """, + 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 +) +public final class TbAiNode extends TbAbstractExternalNode implements TbNode { + + private String systemPrompt; + private String userPrompt; + private ResponseFormat responseFormat; + private int timeoutSeconds; + private AiModelId modelId; + + @Override + public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException { + super.init(ctx); + + var config = TbNodeUtils.convert(configuration, TbAiNodeConfiguration.class); + String errorPrefix = "'" + ctx.getSelf().getName() + "' node configuration is invalid: "; + try { + validateFields(config, errorPrefix); + } catch (DataValidationException e) { + throw new TbNodeException(e, true); + } + + modelId = config.getModelId(); + Optional modelOpt = ctx.getAiModelService().findAiModelByTenantIdAndId(ctx.getTenantId(), modelId); + if (modelOpt.isEmpty()) { + throw new TbNodeException("[" + ctx.getTenantId() + "] AI model with ID: [" + modelId + "] was not found", true); + } + AiModel model = modelOpt.get(); + AiModelType modelType = model.getConfiguration().modelType(); + if (modelType != AiModelType.CHAT) { + throw new TbNodeException("[" + ctx.getTenantId() + "] AI model with ID: [" + modelId + "] must be of type CHAT, but was " + modelType, true); + } + AiChatModelConfig chatModelConfig = (AiChatModelConfig) model.getConfiguration(); + if (isJsonModeConfigured(config)) { + if (!chatModelConfig.supportsJsonMode()) { + throw new TbNodeException("[" + ctx.getTenantId() + "] AI model with ID: [" + modelId + "] does not support '" + config.getResponseFormat().type() + "' response format", true); + } + // LangChain4j AnthropicChatModel rejects requests with non-null ResponseFormat even if ResponseFormatType is TEXT + responseFormat = config.getResponseFormat().toLangChainResponseFormat(); + } + + systemPrompt = config.getSystemPrompt(); + userPrompt = config.getUserPrompt(); + timeoutSeconds = config.getTimeoutSeconds(); + super.forceAck = config.isForceAck() || super.forceAck; // force ack if node config says so, or if env variable (super.forceAck) says so + } + + private static boolean isJsonModeConfigured(TbAiNodeConfiguration config) { + var responseFormatType = config.getResponseFormat().type(); + return responseFormatType == TbResponseFormatType.JSON || responseFormatType == TbResponseFormatType.JSON_SCHEMA; + } + + @Override + public void onMsg(TbContext ctx, TbMsg msg) { + var ackedMsg = ackIfNeeded(ctx, msg); + + List chatMessages = new ArrayList<>(2); + if (systemPrompt != null) { + chatMessages.add(SystemMessage.from(TbNodeUtils.processPattern(systemPrompt, ackedMsg))); + } + chatMessages.add(UserMessage.from(TbNodeUtils.processPattern(userPrompt, ackedMsg))); + + var chatRequest = ChatRequest.builder() + .messages(chatMessages) + .responseFormat(responseFormat) + .build(); + + sendChatRequestAsync(ctx, chatRequest).addCallback(new FutureCallback<>() { + @Override + public void onSuccess(ChatResponse chatResponse) { + String response = chatResponse.aiMessage().text(); + if (!isValidJsonObject(response)) { + response = wrapInJsonObject(response); + } + tellSuccess(ctx, ackedMsg.transform() + .data(response) + .build()); + } + + @Override + public void onFailure(@NonNull Throwable t) { + tellFailure(ctx, ackedMsg, t); + } + }, directExecutor()); + } + + private > FluentFuture sendChatRequestAsync(TbContext ctx, ChatRequest chatRequest) { + return ctx.getAiModelService().findAiModelByTenantIdAndIdAsync(ctx.getTenantId(), modelId).transformAsync(modelOpt -> { + if (modelOpt.isEmpty()) { + throw new NoSuchElementException("[" + ctx.getTenantId() + "] AI model with ID: [" + modelId + "] was not found"); + } + AiModel model = modelOpt.get(); + AiModelType modelType = model.getConfiguration().modelType(); + if (modelType != AiModelType.CHAT) { + throw new IllegalStateException("[" + ctx.getTenantId() + "] AI model with ID: [" + modelId + "] must be of type CHAT, but was " + modelType); + } + + @SuppressWarnings("unchecked") + AiChatModelConfig chatModelConfig = (AiChatModelConfig) model.getConfiguration(); + + chatModelConfig = chatModelConfig + .withTimeoutSeconds(timeoutSeconds) + .withMaxRetries(0); // disable retries to respect timeout set in rule node config + + return ctx.getAiChatModelService().sendChatRequestAsync(chatModelConfig, chatRequest); + }, ctx.getDbCallbackExecutor()); + } + + private static boolean isValidJsonObject(String jsonString) { + try { + JsonNode result = JacksonUtil.toJsonNode(jsonString); + return result != null && result.isObject(); + } catch (IllegalArgumentException e) { + return false; + } + } + + private static String wrapInJsonObject(String response) { + return JacksonUtil.newObjectNode().put("response", response).toString(); + } + + @Override + public void destroy() { + super.destroy(); + systemPrompt = null; + userPrompt = 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 new file mode 100644 index 0000000000..48392aa76e --- /dev/null +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNodeConfiguration.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.rule.engine.ai; + +import jakarta.validation.Valid; +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 static org.thingsboard.rule.engine.ai.TbResponseFormat.TbJsonResponseFormat; + +@Data +public class TbAiNodeConfiguration implements NodeConfiguration { + + @NotNull + private AiModelId modelId; + + @Length(min = 1, max = 500_000) + private String systemPrompt; + + @NotBlank + @Length(min = 1, max = 500_000) + private String userPrompt; + + @NotNull + @Valid + private TbResponseFormat responseFormat; + + @Min(value = 1, message = "must be at least 1 second") + @Max(value = 600, message = "cannot exceed 600 seconds (10 minutes)") + private int timeoutSeconds; + + private boolean forceAck; + + @Override + public TbAiNodeConfiguration defaultConfiguration() { + var configuration = new TbAiNodeConfiguration(); + configuration.setSystemPrompt( + "You are a helpful AI assistant. Your primary function is to process the user's request and respond with a valid JSON object. " + + "Do not include any text, explanations, or markdown formatting before or after the JSON output." + ); + configuration.setResponseFormat(new TbJsonResponseFormat()); + configuration.setTimeoutSeconds(60); + configuration.setForceAck(true); + return configuration; + } + +} 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 new file mode 100644 index 0000000000..5c891a9c74 --- /dev/null +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbResponseFormat.java @@ -0,0 +1,103 @@ +/** + * 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.ai; + +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.databind.node.ObjectNode; +import dev.langchain4j.model.chat.request.ResponseFormat; +import dev.langchain4j.model.chat.request.ResponseFormatType; +import jakarta.validation.constraints.NotNull; +import org.thingsboard.server.common.data.validation.ValidJsonSchema; + +import static org.thingsboard.rule.engine.ai.TbResponseFormat.TbJsonResponseFormat; +import static org.thingsboard.rule.engine.ai.TbResponseFormat.TbJsonSchemaResponseFormat; +import static org.thingsboard.rule.engine.ai.TbResponseFormat.TbTextResponseFormat; + +@JsonTypeInfo( + use = JsonTypeInfo.Id.NAME, + include = JsonTypeInfo.As.PROPERTY, + property = "type" +) +@JsonSubTypes({ + @JsonSubTypes.Type(value = TbTextResponseFormat.class, name = "TEXT"), + @JsonSubTypes.Type(value = TbJsonResponseFormat.class, name = "JSON"), + @JsonSubTypes.Type(value = TbJsonSchemaResponseFormat.class, name = "JSON_SCHEMA") +}) +public sealed interface TbResponseFormat permits TbTextResponseFormat, TbJsonResponseFormat, TbJsonSchemaResponseFormat { + + TbResponseFormatType type(); + + ResponseFormat toLangChainResponseFormat(); + + enum TbResponseFormatType { + + TEXT, + JSON, + JSON_SCHEMA + + } + + record TbTextResponseFormat() implements TbResponseFormat { + + @Override + public TbResponseFormatType type() { + return TbResponseFormatType.TEXT; + } + + @Override + public ResponseFormat toLangChainResponseFormat() { + return ResponseFormat.builder() + .type(ResponseFormatType.TEXT) + .build(); + } + + } + + record TbJsonResponseFormat() implements TbResponseFormat { + + @Override + public TbResponseFormatType type() { + return TbResponseFormatType.JSON; + } + + @Override + public ResponseFormat toLangChainResponseFormat() { + return ResponseFormat.builder() + .type(ResponseFormatType.JSON) + .build(); + } + + } + + record TbJsonSchemaResponseFormat(@NotNull @ValidJsonSchema ObjectNode schema) implements TbResponseFormat { + + @Override + public TbResponseFormatType type() { + return TbResponseFormatType.JSON_SCHEMA; + } + + @Override + public ResponseFormat toLangChainResponseFormat() { + return ResponseFormat.builder() + .type(ResponseFormatType.JSON) + .jsonSchema(Langchain4jJsonSchemaAdapter.fromObjectNode(schema)) + .build(); + } + + } + +} diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/external/TbAbstractExternalNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/external/TbAbstractExternalNode.java index 374cfec1bf..42d9bc85e5 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/external/TbAbstractExternalNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/external/TbAbstractExternalNode.java @@ -22,7 +22,7 @@ import org.thingsboard.server.common.msg.TbMsg; public abstract class TbAbstractExternalNode implements TbNode { - private boolean forceAck; + protected boolean forceAck; public void init(TbContext ctx) { this.forceAck = ctx.isExternalNodeForceAck(); 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 694fed1bf8..87643ae46d 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 @@ -45,7 +45,6 @@ import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; import javax.net.ssl.SSLException; -import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; @@ -64,12 +63,10 @@ import java.util.concurrent.TimeoutException; ) public class TbMqttNode extends TbAbstractExternalNode { - private static final Charset UTF8 = StandardCharsets.UTF_8; - - private static final String ERROR = "error"; + private static final int MQTT_3_MAX_CLIENT_ID_LENGTH = 23; + private static final int MQTT_5_MAX_CLIENT_ID_LENGTH = 256; protected TbMqttNodeConfiguration mqttNodeConfiguration; - protected MqttClient mqttClient; @Override @@ -87,9 +84,9 @@ public class TbMqttNode extends TbAbstractExternalNode { @Override public void onMsg(TbContext ctx, TbMsg msg) { - String topic = TbNodeUtils.processPattern(this.mqttNodeConfiguration.getTopicPattern(), msg); + String topic = TbNodeUtils.processPattern(mqttNodeConfiguration.getTopicPattern(), msg); var tbMsg = ackIfNeeded(ctx, msg); - this.mqttClient.publish(topic, Unpooled.wrappedBuffer(getData(tbMsg, mqttNodeConfiguration.isParseToPlainText()).getBytes(UTF8)), + this.mqttClient.publish(topic, Unpooled.wrappedBuffer(getData(tbMsg, mqttNodeConfiguration.isParseToPlainText()).getBytes(StandardCharsets.UTF_8)), MqttQoS.AT_LEAST_ONCE, mqttNodeConfiguration.isRetainedMessage()) .addListener(future -> { if (future.isSuccess()) { @@ -103,7 +100,7 @@ public class TbMqttNode extends TbAbstractExternalNode { private TbMsg processException(TbMsg origMsg, Throwable e) { TbMsgMetaData metaData = origMsg.getMetaData().copy(); - metaData.putValue(ERROR, e.getClass() + ": " + e.getMessage()); + metaData.putValue("error", e.getClass() + ": " + e.getMessage()); return origMsg.transform() .metaData(metaData) .build(); @@ -111,8 +108,8 @@ public class TbMqttNode extends TbAbstractExternalNode { @Override public void destroy() { - if (this.mqttClient != null) { - this.mqttClient.disconnect(); + if (mqttClient != null) { + mqttClient.disconnect(); } } @@ -123,11 +120,11 @@ public class TbMqttNode extends TbAbstractExternalNode { protected MqttClient initClient(TbContext ctx) throws Exception { MqttClientConfig config = new MqttClientConfig(getSslContext()); config.setOwnerId(getOwnerId(ctx)); - if (!StringUtils.isEmpty(this.mqttNodeConfiguration.getClientId())) { + if (!StringUtils.isEmpty(mqttNodeConfiguration.getClientId())) { config.setClientId(getClientId(ctx)); } - config.setCleanSession(this.mqttNodeConfiguration.isCleanSession()); - config.setProtocolVersion(this.mqttNodeConfiguration.getProtocolVersion()); + config.setCleanSession(mqttNodeConfiguration.isCleanSession()); + config.setProtocolVersion(mqttNodeConfiguration.getProtocolVersion()); MqttClientSettings mqttClientSettings = ctx.getMqttClientSettings(); config.setRetransmissionConfig(new MqttClientConfig.RetransmissionConfig( @@ -139,32 +136,32 @@ public class TbMqttNode extends TbAbstractExternalNode { prepareMqttClientConfig(config); MqttClient client = getMqttClient(ctx, config); client.setEventLoop(ctx.getSharedEventLoop()); - Promise connectFuture = client.connect(this.mqttNodeConfiguration.getHost(), this.mqttNodeConfiguration.getPort()); + Promise connectFuture = client.connect(mqttNodeConfiguration.getHost(), mqttNodeConfiguration.getPort()); MqttConnectResult result; try { - result = connectFuture.get(this.mqttNodeConfiguration.getConnectTimeoutSec(), TimeUnit.SECONDS); + result = connectFuture.get(mqttNodeConfiguration.getConnectTimeoutSec(), TimeUnit.SECONDS); } catch (TimeoutException ex) { connectFuture.cancel(true); client.disconnect(); - String hostPort = this.mqttNodeConfiguration.getHost() + ":" + this.mqttNodeConfiguration.getPort(); + String hostPort = mqttNodeConfiguration.getHost() + ":" + mqttNodeConfiguration.getPort(); throw new RuntimeException(String.format("Failed to connect to MQTT broker at %s.", hostPort)); } if (!result.isSuccess()) { connectFuture.cancel(true); client.disconnect(); - String hostPort = this.mqttNodeConfiguration.getHost() + ":" + this.mqttNodeConfiguration.getPort(); + String hostPort = mqttNodeConfiguration.getHost() + ":" + mqttNodeConfiguration.getPort(); throw new RuntimeException(String.format("Failed to connect to MQTT broker at %s. Result code is: %s", hostPort, result.getReturnCode())); } return client; } private String getClientId(TbContext ctx) throws TbNodeException { - String clientId = this.mqttNodeConfiguration.isAppendClientIdSuffix() ? - this.mqttNodeConfiguration.getClientId() + "_" + ctx.getServiceId() : - this.mqttNodeConfiguration.getClientId(); - if (clientId.length() > 23) { - throw new TbNodeException("Client ID is too long '" + clientId + "'. " + - "The length of Client ID cannot be longer than 23, but current length is " + clientId.length() + ".", true); + String clientId = mqttNodeConfiguration.isAppendClientIdSuffix() ? + mqttNodeConfiguration.getClientId() + "_" + ctx.getServiceId() : + mqttNodeConfiguration.getClientId(); + int maxLength = mqttNodeConfiguration.getProtocolVersion() == MqttVersion.MQTT_3_1 ? MQTT_3_MAX_CLIENT_ID_LENGTH : MQTT_5_MAX_CLIENT_ID_LENGTH; + if (clientId.length() > maxLength) { + throw new TbNodeException("The length of Client ID cannot be longer than " + maxLength + ", but current length is " + clientId.length() + ".", true); } return clientId; } @@ -174,7 +171,7 @@ public class TbMqttNode extends TbAbstractExternalNode { } protected void prepareMqttClientConfig(MqttClientConfig config) { - ClientCredentials credentials = this.mqttNodeConfiguration.getCredentials(); + ClientCredentials credentials = mqttNodeConfiguration.getCredentials(); if (credentials.getType() == CredentialsType.BASIC) { BasicCredentials basicCredentials = (BasicCredentials) credentials; config.setUsername(basicCredentials.getUsername()); @@ -183,7 +180,7 @@ public class TbMqttNode extends TbAbstractExternalNode { } private SslContext getSslContext() throws SSLException { - return this.mqttNodeConfiguration.isSsl() ? this.mqttNodeConfiguration.getCredentials().initSslContext() : null; + return mqttNodeConfiguration.isSsl() ? mqttNodeConfiguration.getCredentials().initSslContext() : null; } private String getData(TbMsg tbMsg, boolean parseToPlainText) { 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 2ea56ce799..26c5b3fa42 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 @@ -17,6 +17,7 @@ package org.thingsboard.rule.engine.mqtt.azure; 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; @@ -36,6 +37,8 @@ import org.thingsboard.server.common.data.plugin.ComponentClusteringMode; import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.data.util.TbPair; +import java.time.Clock; + @Slf4j @RuleNode( type = ComponentType.EXTERNAL, @@ -49,6 +52,8 @@ import org.thingsboard.server.common.data.util.TbPair; ) public class TbAzureIotHubNode extends TbMqttNode { + private Clock clock = Clock.systemUTC(); + @Override public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException { super.init(ctx); @@ -73,7 +78,7 @@ public class TbAzureIotHubNode extends TbMqttNode { config.setUsername(AzureIotHubUtil.buildUsername(mqttNodeConfiguration.getHost(), config.getClientId())); ClientCredentials credentials = mqttNodeConfiguration.getCredentials(); if (CredentialsType.SAS == credentials.getType()) { - config.setPassword(AzureIotHubUtil.buildSasToken(mqttNodeConfiguration.getHost(), ((AzureIotHubSasCredentials) credentials).getSasKey())); + config.setPassword(AzureIotHubUtil.buildSasToken(mqttNodeConfiguration.getHost(), ((AzureIotHubSasCredentials) credentials).getSasKey(), clock)); } } @@ -81,6 +86,11 @@ public class TbAzureIotHubNode extends TbMqttNode { return initClient(ctx); } + @VisibleForTesting + void setClock(Clock clock) { + this.clock = clock; + } + @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/profile/DeviceState.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/DeviceState.java index 193c179df2..4bd81050db 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 @@ -258,7 +258,7 @@ class DeviceState { private boolean processAttributes(TbContext ctx, TbMsg msg, String scope) throws ExecutionException, InterruptedException { boolean stateChanged = false; - Set attributes = JsonConverter.convertToAttributes(JsonParser.parseString(msg.getData())); + List attributes = JsonConverter.convertToAttributes(JsonParser.parseString(msg.getData())); if (!attributes.isEmpty()) { SnapshotUpdate update = merge(latestValues, attributes, scope); for (DeviceProfileAlarm alarm : deviceProfile.getAlarmSettings()) { @@ -321,7 +321,7 @@ class DeviceState { return new SnapshotUpdate(AlarmConditionKeyType.TIME_SERIES, keys); } - private SnapshotUpdate merge(DataSnapshot latestValues, Set attributes, String scope) { + private SnapshotUpdate merge(DataSnapshot latestValues, List attributes, String scope) { long newTs = 0; Set keys = new HashSet<>(); for (AttributeKvEntry entry : attributes) { 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 4e319500d1..e703e9dd25 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 @@ -103,8 +103,7 @@ public class TbCalculatedFieldsNode implements TbNode { } private void processPostAttributesRequest(TbContext ctx, TbMsg msg) { - List newAttributes = new ArrayList<>(JsonConverter.convertToAttributes(JsonParser.parseString(msg.getData()))); - + List newAttributes = JsonConverter.convertToAttributes(JsonParser.parseString(msg.getData())); if (newAttributes.isEmpty()) { ctx.tellSuccess(msg); return; 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 c04f5b474d..280d8de824 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 @@ -41,7 +41,6 @@ import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.data.util.TbPair; import org.thingsboard.server.common.msg.TbMsg; -import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Objects; @@ -133,7 +132,7 @@ public class TbMsgAttributesNode implements TbNode { return; } String src = msg.getData(); - List newAttributes = new ArrayList<>(JsonConverter.convertToAttributes(JsonParser.parseString(src))); + List newAttributes = JsonConverter.convertToAttributes(JsonParser.parseString(src)); if (newAttributes.isEmpty()) { ctx.tellSuccess(msg); return; 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 93fad4c0e7..8ea0f70a3f 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 @@ -19,6 +19,7 @@ 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; @@ -146,6 +147,7 @@ public class TenantIdLoader { tenantEntity = ctx.getNotificationRequestService().findNotificationRequestById(ctxTenantId, new NotificationRequestId(id)); break; case NOTIFICATION: + case ADMIN_SETTINGS: return ctxTenantId; case NOTIFICATION_RULE: tenantEntity = ctx.getNotificationRuleService().findNotificationRuleById(ctxTenantId, new NotificationRuleId(id)); @@ -179,6 +181,9 @@ public class TenantIdLoader { case JOB: tenantEntity = ctx.getJobService().findJobById(ctxTenantId, new JobId(id)); break; + case AI_MODEL: + tenantEntity = ctx.getAiModelService().findAiModelById(ctxTenantId, new AiModelId(id)).orElse(null); + break; default: throw new RuntimeException("Unexpected entity type: " + entityId.getEntityType()); } 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/action/TbCreateAlarmNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbCreateAlarmNodeTest.java index 6cb8299b67..24fd912997 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbCreateAlarmNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbCreateAlarmNodeTest.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.DisplayName; import org.junit.jupiter.api.Test; @@ -60,6 +60,8 @@ import java.util.List; import java.util.Map; import java.util.concurrent.ExecutionException; +import static com.google.common.util.concurrent.Futures.immediateFailedFuture; +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.anyString; @@ -67,6 +69,7 @@ import static org.mockito.BDDMockito.given; import static org.mockito.BDDMockito.then; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.eq; +import static org.mockito.Mockito.lenient; import static org.mockito.Mockito.never; @ExtendWith(MockitoExtension.class) @@ -99,6 +102,8 @@ class TbCreateAlarmNodeTest { dbExecutor = new TestDbCallbackExecutor(); metadata = new TbMsgMetaData(); config = new TbCreateAlarmNodeConfiguration(); + + lenient().when(ctxMock.getDbCallbackExecutor()).thenReturn(dbExecutor); } @Test @@ -212,10 +217,9 @@ class TbCreateAlarmNodeTest { // mocks given(ctxMock.getTenantId()).willReturn(tenantId); given(ctxMock.getAlarmService()).willReturn(alarmServiceMock); - given(ctxMock.getDbCallbackExecutor()).willReturn(dbExecutor); given(ctxMock.getSelfId()).willReturn(ruleNodeSelfId); - given(alarmServiceMock.findLatestActiveByOriginatorAndType(tenantId, msgOriginator, alarmType)).willReturn(existingAlarm); - given(alarmDetailsScriptMock.executeJsonAsync(incomingMsg)).willReturn(Futures.immediateFuture(alarmDetails)); + given(alarmServiceMock.findLatestActiveByOriginatorAndTypeAsync(tenantId, msgOriginator, alarmType)).willReturn(FluentFuture.from(immediateFuture(existingAlarm))); + given(alarmDetailsScriptMock.executeJsonAsync(incomingMsg)).willReturn(immediateFuture(alarmDetails)); var apiCallResult = AlarmApiCallResult.builder() .successful(true) .created(true) @@ -230,11 +234,11 @@ class TbCreateAlarmNodeTest { given(ctxMock.alarmActionMsg(expectedCreatedAlarmInfo, ruleNodeSelfId, TbMsgType.ENTITY_CREATED)).willReturn(alarmActionMsgMock); given(ctxMock.transformMsg(any(TbMsg.class), any(TbMsgType.class), any(EntityId.class), any(TbMsgMetaData.class), anyString())) .willAnswer(answer -> answer.getArgument(0, TbMsg.class).transform() - .type(answer.getArgument(1, TbMsgType.class)) - .originator(answer.getArgument(2, EntityId.class)) - .metaData(answer.getArgument(3, TbMsgMetaData.class)) - .data(answer.getArgument(4, String.class)) - .build() + .type(answer.getArgument(1, TbMsgType.class)) + .originator(answer.getArgument(2, EntityId.class)) + .metaData(answer.getArgument(3, TbMsgMetaData.class)) + .data(answer.getArgument(4, String.class)) + .build() ); given(ctxMock.createScriptEngine(ScriptLanguage.TBEL, TbAbstractAlarmNodeConfiguration.ALARM_DETAILS_BUILD_TBEL_TEMPLATE)).willReturn(alarmDetailsScriptMock); @@ -384,10 +388,9 @@ class TbCreateAlarmNodeTest { // mocks given(ctxMock.getTenantId()).willReturn(tenantId); given(ctxMock.getAlarmService()).willReturn(alarmServiceMock); - given(ctxMock.getDbCallbackExecutor()).willReturn(dbExecutor); given(ctxMock.getSelfId()).willReturn(ruleNodeSelfId); - given(alarmServiceMock.findLatestActiveByOriginatorAndType(tenantId, msgOriginator, alarmType)).willReturn(existingClearedAlarm); - given(alarmDetailsScriptMock.executeJsonAsync(incomingMsg)).willReturn(Futures.immediateFuture(alarmDetails)); + given(alarmServiceMock.findLatestActiveByOriginatorAndTypeAsync(tenantId, msgOriginator, alarmType)).willReturn(FluentFuture.from(immediateFuture(existingClearedAlarm))); + given(alarmDetailsScriptMock.executeJsonAsync(incomingMsg)).willReturn(immediateFuture(alarmDetails)); var apiCallResult = AlarmApiCallResult.builder() .successful(true) .created(true) @@ -402,11 +405,11 @@ class TbCreateAlarmNodeTest { given(ctxMock.alarmActionMsg(expectedCreatedAlarmInfo, ruleNodeSelfId, TbMsgType.ENTITY_CREATED)).willReturn(alarmActionMsgMock); given(ctxMock.transformMsg(any(TbMsg.class), any(TbMsgType.class), any(EntityId.class), any(TbMsgMetaData.class), anyString())) .willAnswer(answer -> answer.getArgument(0, TbMsg.class).transform() - .type(answer.getArgument(1, TbMsgType.class)) - .originator(answer.getArgument(2, EntityId.class)) - .metaData(answer.getArgument(3, TbMsgMetaData.class)) - .data(answer.getArgument(4, String.class)) - .build() + .type(answer.getArgument(1, TbMsgType.class)) + .originator(answer.getArgument(2, EntityId.class)) + .metaData(answer.getArgument(3, TbMsgMetaData.class)) + .data(answer.getArgument(4, String.class)) + .build() ); given(ctxMock.createScriptEngine(ScriptLanguage.JS, config.getAlarmDetailsBuildJs())).willReturn(alarmDetailsScriptMock); @@ -576,10 +579,9 @@ class TbCreateAlarmNodeTest { // mocks given(ctxMock.getTenantId()).willReturn(tenantId); given(ctxMock.getAlarmService()).willReturn(alarmServiceMock); - given(ctxMock.getDbCallbackExecutor()).willReturn(dbExecutor); given(ctxMock.getSelfId()).willReturn(ruleNodeSelfId); - given(alarmServiceMock.findLatestActiveByOriginatorAndType(tenantId, msgOriginator, alarmType)).willReturn(existingActiveAlarm); - given(alarmDetailsScriptMock.executeJsonAsync(any())).willReturn(Futures.immediateFuture(newAlarmDetails)); + given(alarmServiceMock.findLatestActiveByOriginatorAndTypeAsync(tenantId, msgOriginator, alarmType)).willReturn(FluentFuture.from(immediateFuture(existingActiveAlarm))); + given(alarmDetailsScriptMock.executeJsonAsync(any())).willReturn(immediateFuture(newAlarmDetails)); doReturn(newEndTs).when(nodeSpy).currentTimeMillis(); var apiCallResult = AlarmApiCallResult.builder() .successful(true) @@ -595,11 +597,11 @@ class TbCreateAlarmNodeTest { given(ctxMock.alarmActionMsg(expectedUpdatedAlarmInfo, ruleNodeSelfId, TbMsgType.ENTITY_UPDATED)).willReturn(alarmActionMsgMock); given(ctxMock.transformMsg(any(TbMsg.class), any(TbMsgType.class), any(EntityId.class), any(TbMsgMetaData.class), anyString())) .willAnswer(answer -> answer.getArgument(0, TbMsg.class).transform() - .type(answer.getArgument(1, TbMsgType.class)) - .originator(answer.getArgument(2, EntityId.class)) - .metaData(answer.getArgument(3, TbMsgMetaData.class)) - .data(answer.getArgument(4, String.class)) - .build() + .type(answer.getArgument(1, TbMsgType.class)) + .originator(answer.getArgument(2, EntityId.class)) + .metaData(answer.getArgument(3, TbMsgMetaData.class)) + .data(answer.getArgument(4, String.class)) + .build() ); given(ctxMock.createScriptEngine(ScriptLanguage.TBEL, config.getAlarmDetailsBuildTbel())).willReturn(alarmDetailsScriptMock); @@ -753,9 +755,8 @@ class TbCreateAlarmNodeTest { // mocks given(ctxMock.getTenantId()).willReturn(tenantId); given(ctxMock.getAlarmService()).willReturn(alarmServiceMock); - given(ctxMock.getDbCallbackExecutor()).willReturn(dbExecutor); given(ctxMock.getSelfId()).willReturn(ruleNodeSelfId); - given(alarmServiceMock.findLatestActiveByOriginatorAndType(tenantId, msgOriginator, alarmType)).willReturn(existingClearedAlarm); + given(alarmServiceMock.findLatestActiveByOriginatorAndTypeAsync(tenantId, msgOriginator, alarmType)).willReturn(FluentFuture.from(immediateFuture(existingClearedAlarm))); var apiCallResult = AlarmApiCallResult.builder() .successful(true) .created(true) @@ -770,11 +771,11 @@ class TbCreateAlarmNodeTest { given(ctxMock.alarmActionMsg(expectedCreatedAlarmInfo, ruleNodeSelfId, TbMsgType.ENTITY_CREATED)).willReturn(alarmActionMsgMock); given(ctxMock.transformMsg(any(TbMsg.class), any(TbMsgType.class), any(EntityId.class), any(TbMsgMetaData.class), anyString())) .willAnswer(answer -> answer.getArgument(0, TbMsg.class).transform() - .type(answer.getArgument(1, TbMsgType.class)) - .originator(answer.getArgument(2, EntityId.class)) - .metaData(answer.getArgument(3, TbMsgMetaData.class)) - .data(answer.getArgument(4, String.class)) - .build() + .type(answer.getArgument(1, TbMsgType.class)) + .originator(answer.getArgument(2, EntityId.class)) + .metaData(answer.getArgument(3, TbMsgMetaData.class)) + .data(answer.getArgument(4, String.class)) + .build() ); given(ctxMock.createScriptEngine(ScriptLanguage.TBEL, config.getAlarmDetailsBuildTbel())).willReturn(alarmDetailsScriptMock); @@ -941,10 +942,9 @@ class TbCreateAlarmNodeTest { // mocks given(ctxMock.getTenantId()).willReturn(tenantId); given(ctxMock.getAlarmService()).willReturn(alarmServiceMock); - given(ctxMock.getDbCallbackExecutor()).willReturn(dbExecutor); given(ctxMock.getSelfId()).willReturn(ruleNodeSelfId); - given(alarmServiceMock.findLatestActiveByOriginatorAndType(tenantId, msgOriginator, alarmType)).willReturn(existingActiveAlarm); - given(alarmDetailsScriptMock.executeJsonAsync(any())).willReturn(Futures.immediateFuture(newAlarmDetails)); + given(alarmServiceMock.findLatestActiveByOriginatorAndTypeAsync(tenantId, msgOriginator, alarmType)).willReturn(FluentFuture.from(immediateFuture(existingActiveAlarm))); + given(alarmDetailsScriptMock.executeJsonAsync(any())).willReturn(immediateFuture(newAlarmDetails)); doReturn(newEndTs).when(nodeSpy).currentTimeMillis(); var apiCallResult = AlarmApiCallResult.builder() .successful(true) @@ -960,11 +960,11 @@ class TbCreateAlarmNodeTest { given(ctxMock.alarmActionMsg(expectedUpdatedAlarmInfo, ruleNodeSelfId, TbMsgType.ENTITY_UPDATED)).willReturn(alarmActionMsgMock); given(ctxMock.transformMsg(any(TbMsg.class), any(TbMsgType.class), any(EntityId.class), any(TbMsgMetaData.class), anyString())) .willAnswer(answer -> answer.getArgument(0, TbMsg.class).transform() - .type(answer.getArgument(1, TbMsgType.class)) - .originator(answer.getArgument(2, EntityId.class)) - .metaData(answer.getArgument(3, TbMsgMetaData.class)) - .data(answer.getArgument(4, String.class)) - .build() + .type(answer.getArgument(1, TbMsgType.class)) + .originator(answer.getArgument(2, EntityId.class)) + .metaData(answer.getArgument(3, TbMsgMetaData.class)) + .data(answer.getArgument(4, String.class)) + .build() ); given(ctxMock.createScriptEngine(ScriptLanguage.TBEL, config.getAlarmDetailsBuildTbel())).willReturn(alarmDetailsScriptMock); @@ -1125,10 +1125,9 @@ class TbCreateAlarmNodeTest { // mocks given(ctxMock.getTenantId()).willReturn(tenantId); given(ctxMock.getAlarmService()).willReturn(alarmServiceMock); - given(ctxMock.getDbCallbackExecutor()).willReturn(dbExecutor); given(ctxMock.getSelfId()).willReturn(ruleNodeSelfId); - given(alarmServiceMock.findLatestActiveByOriginatorAndType(tenantId, msgOriginator, alarmType)).willReturn(existingActiveAlarm); - given(alarmDetailsScriptMock.executeJsonAsync(any())).willReturn(Futures.immediateFuture(alarmDetails)); + given(alarmServiceMock.findLatestActiveByOriginatorAndTypeAsync(tenantId, msgOriginator, alarmType)).willReturn(FluentFuture.from(immediateFuture(existingActiveAlarm))); + given(alarmDetailsScriptMock.executeJsonAsync(any())).willReturn(immediateFuture(alarmDetails)); doReturn(endTs).when(nodeSpy).currentTimeMillis(); var apiCallResult = AlarmApiCallResult.builder() .successful(true) @@ -1144,11 +1143,11 @@ class TbCreateAlarmNodeTest { given(ctxMock.alarmActionMsg(expectedUpdatedAlarmInfo, ruleNodeSelfId, TbMsgType.ENTITY_UPDATED)).willReturn(alarmActionMsgMock); given(ctxMock.transformMsg(any(TbMsg.class), any(TbMsgType.class), any(EntityId.class), any(TbMsgMetaData.class), anyString())) .willAnswer(answer -> answer.getArgument(0, TbMsg.class).transform() - .type(answer.getArgument(1, TbMsgType.class)) - .originator(answer.getArgument(2, EntityId.class)) - .metaData(answer.getArgument(3, TbMsgMetaData.class)) - .data(answer.getArgument(4, String.class)) - .build() + .type(answer.getArgument(1, TbMsgType.class)) + .originator(answer.getArgument(2, EntityId.class)) + .metaData(answer.getArgument(3, TbMsgMetaData.class)) + .data(answer.getArgument(4, String.class)) + .build() ); given(ctxMock.createScriptEngine(ScriptLanguage.TBEL, config.getAlarmDetailsBuildTbel())).willReturn(alarmDetailsScriptMock); @@ -1216,11 +1215,11 @@ class TbCreateAlarmNodeTest { given(ctxMock.getTenantId()).willReturn(tenantId); given(ctxMock.getAlarmService()).willReturn(alarmServiceMock); - given(ctxMock.getDbCallbackExecutor()).willReturn(dbExecutor); given(ctxMock.createScriptEngine(ScriptLanguage.TBEL, config.getAlarmDetailsBuildTbel())).willReturn(alarmDetailsScriptMock); + given(alarmServiceMock.findLatestActiveByOriginatorAndTypeAsync(tenantId, msgOriginator, config.getAlarmType())).willReturn(FluentFuture.from(immediateFuture(null))); var expectedException = new ExecutionException("Failed to execute script.", new RuntimeException("Something went wrong!")); - given(alarmDetailsScriptMock.executeJsonAsync(incomingMsg)).willReturn(Futures.immediateFailedFuture(expectedException)); + given(alarmDetailsScriptMock.executeJsonAsync(incomingMsg)).willReturn(immediateFailedFuture(expectedException)); nodeSpy.init(ctxMock, new TbNodeConfiguration(JacksonUtil.valueToTree(config))); 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 new file mode 100644 index 0000000000..f21aa559d4 --- /dev/null +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/ai/TbAiNodeTest.java @@ -0,0 +1,953 @@ +/** + * 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.ai; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.google.common.util.concurrent.FluentFuture; +import dev.langchain4j.data.message.AiMessage; +import dev.langchain4j.data.message.SystemMessage; +import dev.langchain4j.data.message.UserMessage; +import dev.langchain4j.model.chat.request.ResponseFormat; +import dev.langchain4j.model.chat.request.ResponseFormatType; +import dev.langchain4j.model.chat.request.json.JsonObjectSchema; +import dev.langchain4j.model.chat.request.json.JsonSchema; +import dev.langchain4j.model.chat.response.ChatResponse; +import org.junit.jupiter.api.BeforeEach; +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.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.rule.engine.TestDbCallbackExecutor; +import org.thingsboard.rule.engine.ai.TbResponseFormat.TbJsonResponseFormat; +import org.thingsboard.rule.engine.ai.TbResponseFormat.TbJsonSchemaResponseFormat; +import org.thingsboard.rule.engine.ai.TbResponseFormat.TbTextResponseFormat; +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.ai.AiModel; +import org.thingsboard.server.common.data.ai.model.AiModelConfig; +import org.thingsboard.server.common.data.ai.model.chat.AnthropicChatModelConfig; +import org.thingsboard.server.common.data.ai.model.chat.OpenAiChatModelConfig; +import org.thingsboard.server.common.data.ai.provider.AnthropicProviderConfig; +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.TenantId; +import org.thingsboard.server.common.data.msg.TbNodeConnectionType; +import org.thingsboard.server.common.data.rule.RuleNode; +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 java.util.Map; +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.assertThatNoException; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.argThat; +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.mockito.Mockito.never; + +@ExtendWith(MockitoExtension.class) +class TbAiNodeTest { + + @Mock + TbContext ctxMock; + @Mock + AiModelService aiModelServiceMock; + @Mock + RuleEngineAiChatModelService aiChatModelServiceMock; + + TbAiNode aiNode; + TbAiNodeConfiguration config; + + TenantId tenantId = TenantId.fromUUID(UUID.randomUUID()); + DeviceId deviceId = new DeviceId(UUID.randomUUID()); + AiModelId modelId = new AiModelId(UUID.randomUUID()); + RuleNodeId ruleNodeId = new RuleNodeId(UUID.randomUUID()); + + RuleNode ruleNode; + + AiModel model; + AiModelConfig modelConfig; + + boolean externalNodeForceAck = false; + + @BeforeEach + void setup() { + aiNode = new TbAiNode(); + config = new TbAiNodeConfiguration(); + + modelConfig = OpenAiChatModelConfig.builder() + .providerConfig(new OpenAiProviderConfig("test-api-key")) + .modelId("gpt-4o") + .temperature(0.5) + .topP(0.3) + .frequencyPenalty(0.1) + .presencePenalty(0.2) + .maxOutputTokens(1000) + .timeoutSeconds(100) + .maxRetries(2) + .build(); + + model = AiModel.builder() + .tenantId(tenantId) + .name("Test model") + .configuration(modelConfig) + .build(); + + model.setId(modelId); + model.setVersion(1L); + model.setCreatedTime(123L); + lenient().when(aiModelServiceMock.findAiModelByTenantIdAndId(tenantId, modelId)).thenReturn(Optional.of(model)); + lenient().when(aiModelServiceMock.findAiModelByTenantIdAndIdAsync(tenantId, modelId)).thenReturn(FluentFuture.from(immediateFuture(Optional.of(model)))); + + ruleNode = new RuleNode(); + ruleNode.setId(ruleNodeId); + ruleNode.setName("Test AI node"); + lenient().when(ctxMock.getSelf()).thenReturn(ruleNode); + + lenient().when(ctxMock.isExternalNodeForceAck()).thenReturn(externalNodeForceAck); + lenient().when(ctxMock.getTenantId()).thenReturn(tenantId); + lenient().when(ctxMock.getAiModelService()).thenReturn(aiModelServiceMock); + lenient().when(ctxMock.getAiChatModelService()).thenReturn(aiChatModelServiceMock); + lenient().when(ctxMock.getDbCallbackExecutor()).thenReturn(new TestDbCallbackExecutor()); + } + + @Test + void givenDefaultConfig_whenCalled_thenSetsCorrectValues() { + // GIVEN-WHEN + config = config.defaultConfiguration(); + + // THEN + assertThat(config.getModelId()).isNull(); + assertThat(config.getSystemPrompt()).isEqualTo( + "You are a helpful AI assistant. Your primary function is to process the user's request and respond with a valid JSON object. " + + "Do not include any text, explanations, or markdown formatting before or after the JSON output." + ); + assertThat(config.getUserPrompt()).isNull(); + assertThat(config.getResponseFormat()).isEqualTo(new TbJsonResponseFormat()); + assertThat(config.getTimeoutSeconds()).isEqualTo(60); + assertThat(config.isForceAck()).isTrue(); + } + + /* -- Node initialization tests -- */ + + @Test + void givenNullModelId_whenInit_thenThrowsUnrecoverableTbNodeException() { + // GIVEN + config = constructValidConfig(); + config.setModelId(null); + + // WHEN-THEN + assertThatThrownBy(() -> aiNode.init(ctxMock, new TbNodeConfiguration(JacksonUtil.valueToTree(config)))) + .isInstanceOf(TbNodeException.class) + .hasRootCauseInstanceOf(DataValidationException.class) + .hasRootCauseMessage("'" + ruleNode.getName() + "' node configuration is invalid: modelId must not be null") + .matches(e -> ((TbNodeException) e).isUnrecoverable()); + } + + @ParameterizedTest + @MethodSource("invalidSystemPrompts") + void givenInvalidSystemPrompt_whenInit_thenThrowsUnrecoverableTbNodeException(String invalidSystemPrompt) { + // GIVEN + config = constructValidConfig(); + config.setSystemPrompt(invalidSystemPrompt); + + // WHEN-THEN + assertThatThrownBy(() -> aiNode.init(ctxMock, new TbNodeConfiguration(JacksonUtil.valueToTree(config)))) + .isInstanceOf(TbNodeException.class) + .matches(e -> ((TbNodeException) e).isUnrecoverable()) + .rootCause() + .isInstanceOf(DataValidationException.class) + .hasMessageContaining("'" + ruleNode.getName() + "' node configuration is invalid: systemPrompt"); + } + + static Stream invalidSystemPrompts() { + String tooLongString = "a".repeat(500_001); + return Stream.of( + Arguments.of(""), + Arguments.of(tooLongString) + ); + } + + @ParameterizedTest + @MethodSource("validSystemPrompts") + void givenValidSystemPrompt_whenInit_thenInitializesSuccessfully(String validSystemPrompt) { + // GIVEN + config = constructValidConfig(); + config.setSystemPrompt(validSystemPrompt); + + // WHEN-THEN + assertThatNoException().isThrownBy(() -> aiNode.init(ctxMock, new TbNodeConfiguration(JacksonUtil.valueToTree(config)))); + } + + static Stream validSystemPrompts() { + 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(""" + first sentence + + second sentence + """) + ); + } + + @ParameterizedTest + @MethodSource("invalidUserPrompts") + void givenInvalidUserPrompt_whenInit_thenThrowsUnrecoverableTbNodeException(String invalidUserPrompt) { + // GIVEN + config = constructValidConfig(); + config.setUserPrompt(invalidUserPrompt); + + // WHEN-THEN + assertThatThrownBy(() -> aiNode.init(ctxMock, new TbNodeConfiguration(JacksonUtil.valueToTree(config)))) + .isInstanceOf(TbNodeException.class) + .matches(e -> ((TbNodeException) e).isUnrecoverable()) + .rootCause() + .isInstanceOf(DataValidationException.class) + .hasMessageContaining("'" + ruleNode.getName() + "' node configuration is invalid: userPrompt"); + } + + static Stream invalidUserPrompts() { + String tooLongString = "a".repeat(500_001); + return Stream.of( + Arguments.of((String) null), + Arguments.of(""), + Arguments.of(" "), + Arguments.of(tooLongString) + ); + } + + @ParameterizedTest + @MethodSource("validUserPrompts") + void givenValidUserPrompt_whenInit_thenInitializesSuccessfully(String validUserPrompt) { + // GIVEN + config = constructValidConfig(); + config.setUserPrompt(validUserPrompt); + + // WHEN-THEN + assertThatNoException().isThrownBy(() -> aiNode.init(ctxMock, new TbNodeConfiguration(JacksonUtil.valueToTree(config)))); + } + + static Stream validUserPrompts() { + String longString = "a".repeat(500_000); + return Stream.of( + Arguments.of("a"), + Arguments.of("Test user prompt"), + Arguments.of(longString), + Arguments.of(""" + first sentence + + second sentence + """) + ); + } + + @Test + void givenNullResponseFormat_whenInit_thenThrowsUnrecoverableTbNodeException() { + // GIVEN + config = constructValidConfig(); + config.setResponseFormat(null); + + // WHEN-THEN + assertThatThrownBy(() -> aiNode.init(ctxMock, new TbNodeConfiguration(JacksonUtil.valueToTree(config)))) + .isInstanceOf(TbNodeException.class) + .hasRootCauseInstanceOf(DataValidationException.class) + .hasRootCauseMessage("'" + ruleNode.getName() + "' node configuration is invalid: responseFormat must not be null") + .matches(e -> ((TbNodeException) e).isUnrecoverable()); + } + + @ParameterizedTest + @ValueSource(ints = {Integer.MIN_VALUE, 0, 601, Integer.MAX_VALUE}) + void givenInvalidTimeoutSeconds_whenInit_thenThrowsUnrecoverableTbNodeException(int invalidTimeoutSeconds) { + // GIVEN + config = constructValidConfig(); + config.setTimeoutSeconds(invalidTimeoutSeconds); + + // WHEN-THEN + assertThatThrownBy(() -> aiNode.init(ctxMock, new TbNodeConfiguration(JacksonUtil.valueToTree(config)))) + .isInstanceOf(TbNodeException.class) + .matches(e -> ((TbNodeException) e).isUnrecoverable()) + .rootCause() + .isInstanceOf(DataValidationException.class) + .hasMessageContaining("'" + ruleNode.getName() + "' node configuration is invalid: timeoutSeconds"); + } + + @ParameterizedTest + @ValueSource(ints = {1, 60, 600}) + void givenValidTimeoutSeconds_whenInit_thenInitializesSuccessfully(int validTimeoutSeconds) { + // GIVEN + config = constructValidConfig(); + config.setTimeoutSeconds(validTimeoutSeconds); + + // WHEN-THEN + assertThatNoException().isThrownBy(() -> aiNode.init(ctxMock, new TbNodeConfiguration(JacksonUtil.valueToTree(config)))); + } + + @Test + void givenAiModelNotFound_whenInit_thenThrowsUnrecoverableTbNodeException() { + // GIVEN + config = constructValidConfig(); + given(aiModelServiceMock.findAiModelByTenantIdAndId(tenantId, modelId)).willReturn(Optional.empty()); + + // WHEN-THEN + assertThatThrownBy(() -> aiNode.init(ctxMock, new TbNodeConfiguration(JacksonUtil.valueToTree(config)))) + .isInstanceOf(TbNodeException.class) + .hasMessage("[" + tenantId + "] AI model with ID: [" + modelId + "] was not found") + .matches(e -> ((TbNodeException) e).isUnrecoverable()); + } + + TbAiNodeConfiguration constructValidConfig() { + var config = new TbAiNodeConfiguration(); + config.setModelId(modelId); + config.setSystemPrompt("Test system prompt"); + config.setUserPrompt("Test user prompt"); + config.setResponseFormat(new TbJsonResponseFormat()); + config.setTimeoutSeconds(60); + config.setForceAck(true); + return config; + } + + + @Test + void givenJsonModeConfiguredButModelDoesNotSupportIt_whenInit_thenThrowsUnrecoverableTbNodeException() { + // GIVEN + config = constructValidConfig(); + config.setResponseFormat(new TbJsonResponseFormat()); + + modelConfig = AnthropicChatModelConfig.builder() + .providerConfig(new AnthropicProviderConfig("test-api-key")) + .modelId("claude-sonnet-4-0") + .build(); + + model = AiModel.builder() + .tenantId(tenantId) + .name("Test model") + .configuration(modelConfig) + .build(); + + model.setId(modelId); + model.setVersion(1L); + model.setCreatedTime(123L); + + given(aiModelServiceMock.findAiModelByTenantIdAndId(tenantId, modelId)).willReturn(Optional.of(model)); + + // WHEN-THEN + assertThatThrownBy(() -> aiNode.init(ctxMock, new TbNodeConfiguration(JacksonUtil.valueToTree(config)))) + .isInstanceOf(TbNodeException.class) + .hasMessage("[" + tenantId + "] AI model with ID: [" + modelId + "] does not support 'JSON' response format") + .matches(e -> ((TbNodeException) e).isUnrecoverable()); + } + + /* -- Message processing tests -- */ + + @Test + void givenForceAckIsFalse_whenOnMsg_thenTellSuccessIsCalled() throws TbNodeException { + // GIVEN + config.setModelId(modelId); + config.setSystemPrompt("Respond with valid JSON"); + config.setUserPrompt("Tell me a joke"); + config.setResponseFormat(new TbJsonResponseFormat()); + config.setTimeoutSeconds(10); + config.setForceAck(false); + + 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(ctxMock).should().tellSuccess(any()); + + then(ctxMock).should(never()).enqueueForTellNext(any(), any(String.class)); + then(ctxMock).should(never()).enqueueForTellFailure(any(), any(Throwable.class)); + then(ctxMock).should(never()).tellNext(any(), any(String.class)); + then(ctxMock).should(never()).tellFailure(any(), any()); + } + + @Test + void givenLocalForceAckIsFalseButExternalIsTold_whenOnMsg_thenEnqueuesForTellNext() throws TbNodeException { + // GIVEN + config.setModelId(modelId); + config.setSystemPrompt("Respond with valid JSON"); + config.setUserPrompt("Tell me a joke"); + config.setResponseFormat(new TbJsonResponseFormat()); + config.setTimeoutSeconds(10); + config.setForceAck(false); + + given(ctxMock.isExternalNodeForceAck()).willReturn(true); + + 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(ctxMock).should().enqueueForTellNext(any(), eq(TbNodeConnectionType.SUCCESS)); + + then(ctxMock).should(never()).tellSuccess(any()); + then(ctxMock).should(never()).enqueueForTellFailure(any(), any(Throwable.class)); + then(ctxMock).should(never()).tellNext(any(), any(String.class)); + then(ctxMock).should(never()).tellFailure(any(), any()); + } + + @Test + void givenForceAckIsTrue_whenOnMsg_thenEnqueuesForTellNext() throws TbNodeException { + // GIVEN + config.setModelId(modelId); + config.setSystemPrompt("Respond with valid JSON"); + config.setUserPrompt("Tell me a joke"); + config.setResponseFormat(new TbJsonResponseFormat()); + config.setTimeoutSeconds(10); + config.setForceAck(true); + + 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(ctxMock).should().enqueueForTellNext(any(), eq(TbNodeConnectionType.SUCCESS)); + + then(ctxMock).should(never()).tellSuccess(any()); + then(ctxMock).should(never()).enqueueForTellFailure(any(), any(Throwable.class)); + then(ctxMock).should(never()).tellNext(any(), any(String.class)); + then(ctxMock).should(never()).tellFailure(any(), any()); + } + + @Test + void givenOnlyUserPromptConfigured_whenOnMsg_thenRequestContainsOnlyUserMessage() throws TbNodeException { + // GIVEN + config.setModelId(modelId); + config.setSystemPrompt(null); + config.setUserPrompt("Tell me a joke"); + config.setResponseFormat(new TbJsonResponseFormat()); + config.setTimeoutSeconds(10); + config.setForceAck(true); + + 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(1); + assertThat(actualChatRequest.messages().get(0)).isEqualTo(UserMessage.from("Tell me a joke")); + return true; + }) + ); + } + + @Test + void givenSystemAndUserPromptsConfigured_whenOnMsg_thenRequestContainsBothSystemAndUserMessages() throws TbNodeException { + // GIVEN + config.setModelId(modelId); + config.setSystemPrompt("Respond with valid JSON"); + config.setUserPrompt("Tell me a joke"); + config.setResponseFormat(new TbJsonResponseFormat()); + config.setTimeoutSeconds(10); + config.setForceAck(true); + + 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("Respond with valid JSON")); + assertThat(actualChatRequest.messages().get(1)).isEqualTo(UserMessage.from("Tell me a joke")); + return true; + }) + ); + } + + @Test + void givenTemplatedPrompts_whenOnMsg_thenRequestContainsSubstitutedMessages() throws TbNodeException { + // GIVEN + config.setModelId(modelId); + config.setSystemPrompt("Respond with $[responseFormat]"); + config.setUserPrompt("Tell me a joke about ${jokeIdea}"); + config.setResponseFormat(new TbJsonResponseFormat()); + config.setTimeoutSeconds(10); + config.setForceAck(true); + + aiNode.init(ctxMock, new TbNodeConfiguration(JacksonUtil.valueToTree(config))); + + var msg = TbMsg.newMsg() + .originator(deviceId) + .data("{\"responseFormat\":\"valid JSON\"}") + .metaData(new TbMsgMetaData(Map.of("jokeIdea", "JSON"))) + .build(); + + var chatResponse = ChatResponse.builder() + .aiMessage(AiMessage.from("{\"joke\":\"Why did the JSON go to therapy?\",\"punchline\":\"Because it had too many unresolved references!\"}")) + .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("Respond with valid JSON")); + assertThat(actualChatRequest.messages().get(1)).isEqualTo(UserMessage.from("Tell me a joke about JSON")); + return true; + }) + ); + } + + @Test + void givenNodeTimeoutIsConfigured_whenOnMsg_thenRequestUsesNodeTimeout() throws TbNodeException { + // GIVEN + config.setModelId(modelId); + config.setSystemPrompt("Respond with valid JSON"); + config.setUserPrompt("Tell me a joke"); + config.setResponseFormat(new TbJsonResponseFormat()); + config.setTimeoutSeconds(10); + config.setForceAck(true); + + 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( + argThat(actualChatModelConfig -> { + assertThat(actualChatModelConfig.timeoutSeconds()).isEqualTo(config.getTimeoutSeconds()); + return true; + }), any() + ); + } + + @Test + void givenAnyConfig_whenOnMsg_thenRequestHasRetriesDisabled() throws TbNodeException { + // GIVEN + config.setModelId(modelId); + config.setSystemPrompt("Respond with valid JSON"); + config.setUserPrompt("Tell me a joke"); + config.setResponseFormat(new TbJsonResponseFormat()); + config.setTimeoutSeconds(10); + config.setForceAck(true); + + 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( + argThat(actualChatModelConfig -> { + assertThat(actualChatModelConfig.maxRetries()).isZero(); + return true; + }), any() + ); + } + + @Test + void givenTextResponseFormatAndNonJsonResponse_whenOnMsg_thenWrapsResponseInJsonObject() throws TbNodeException { + // GIVEN + config.setModelId(modelId); + config.setUserPrompt("Tell me a joke about JSON"); + config.setResponseFormat(new TbTextResponseFormat()); + config.setTimeoutSeconds(10); + config.setForceAck(false); + + 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(""" + Why did the JSON file break up with the XML file? + Because it found someone less complicated and more flexible!""")) + .build(); + + given(aiChatModelServiceMock.sendChatRequestAsync(any(), any())).willReturn(FluentFuture.from(immediateFuture(chatResponse))); + + // WHEN + aiNode.onMsg(ctxMock, msg); + + // THEN + then(ctxMock).should().tellSuccess(argThat( + resultMsg -> resultMsg.getData().equals(JacksonUtil.newObjectNode().put("response", chatResponse.aiMessage().text()).toString())) + ); + } + + @Test + void givenModelIsConfigured_whenOnMsg_thenRequestUsesCorrectModelConfig() throws TbNodeException { + // GIVEN + config.setModelId(modelId); + config.setSystemPrompt("Respond with valid JSON"); + config.setUserPrompt("Tell me a joke"); + config.setResponseFormat(new TbJsonResponseFormat()); + config.setTimeoutSeconds(10); + config.setForceAck(true); + + 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( + argThat(actualChatModelConfig -> { + assertThat(actualChatModelConfig) + .usingRecursiveComparison() + .ignoringFields("timeoutSeconds", "maxRetries") + .isEqualTo(modelConfig); + return true; + }), + any() + ); + } + + @Test + void givenTextResponseFormat_whenOnMsg_thenRequestResponseFormatIsNull() throws TbNodeException { + // GIVEN + config.setModelId(modelId); + config.setUserPrompt("Tell me a joke"); + config.setResponseFormat(new TbTextResponseFormat()); + config.setTimeoutSeconds(10); + config.setForceAck(true); + + 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(""" + Why did the JSON file break up with the XML file? + Because it found someone less complicated and more flexible!""")) + .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.responseFormat()).isNull(); + return true; + }) + ); + } + + @Test + void givenJsonResponseFormat_whenOnMsg_thenRequestResponseFormatIsJson() throws TbNodeException { + // GIVEN + config.setModelId(modelId); + config.setUserPrompt("Tell me a joke"); + config.setResponseFormat(new TbJsonResponseFormat()); + config.setTimeoutSeconds(10); + config.setForceAck(true); + + 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(""" + Why did the JSON file break up with the XML file? + Because it found someone less complicated and more flexible!""")) + .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.responseFormat()).isEqualTo(ResponseFormat.builder().type(ResponseFormatType.JSON).build()); + return true; + }) + ); + } + + @Test + void givenJsonSchemaResponseFormat_whenOnMsg_thenRequestResponseFormatIsJsonWithSchema() throws TbNodeException { + // GIVEN + var jsonSchema = """ + { + "title": "Joke", + "type": "object", + "properties": { + "joke": { + "type": "string" + }, + "punchline": { + "type": "string" + } + }, + "required": [ + "joke", + "punchline" + ] + } + """; + + config.setModelId(modelId); + config.setSystemPrompt("Respond with valid JSON"); + config.setUserPrompt("Tell me a joke"); + config.setResponseFormat(new TbJsonSchemaResponseFormat((ObjectNode) JacksonUtil.toJsonNode(jsonSchema))); + config.setTimeoutSeconds(10); + config.setForceAck(true); + + 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(""" + { + "joke": "Why do programmers prefer JSON over XML?", + "punchline": "Because it’s less taxing to read!" + }""")) + .build(); + + given(aiChatModelServiceMock.sendChatRequestAsync(any(), any())).willReturn(FluentFuture.from(immediateFuture(chatResponse))); + + // WHEN + aiNode.onMsg(ctxMock, msg); + + // THEN + var expectedJsonSchema = JsonSchema.builder() + .name("Joke") + .rootElement(JsonObjectSchema.builder() + .addStringProperty("joke") + .addStringProperty("punchline") + .required("joke", "punchline") + .additionalProperties(true) + .build()) + .build(); + + then(aiChatModelServiceMock).should().sendChatRequestAsync( + any(), + argThat(actualChatRequest -> { + assertThat(actualChatRequest.responseFormat()).isEqualTo(ResponseFormat.builder().type(ResponseFormatType.JSON).jsonSchema(expectedJsonSchema).build()); + return true; + }) + ); + } + + @Test + void givenComprehensiveConfig_whenOnMsg_thenProcessesMessageAndTellsSuccessCorrectly() throws TbNodeException { + // GIVEN + config.setModelId(modelId); + config.setSystemPrompt("Respond with valid JSON"); + config.setUserPrompt("Tell me a joke"); + config.setResponseFormat(new TbJsonResponseFormat()); + config.setTimeoutSeconds(10); + config.setForceAck(false); + + 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( + argThat(actualChatModelConfig -> { + assertThat(actualChatModelConfig) + .usingRecursiveComparison() + .ignoringFields("timeoutSeconds", "maxRetries") + .isEqualTo(modelConfig); + assertThat(actualChatModelConfig.timeoutSeconds()).isEqualTo(config.getTimeoutSeconds()); + assertThat(actualChatModelConfig.maxRetries()).isEqualTo(0); + return true; + }), + argThat(actualChatRequest -> { + assertThat(actualChatRequest.messages()).hasSize(2); + assertThat(actualChatRequest.messages().get(0)).isEqualTo(SystemMessage.from("Respond with valid JSON")); + assertThat(actualChatRequest.messages().get(1)).isEqualTo(UserMessage.from("Tell me a joke")); + assertThat(actualChatRequest.responseFormat()).isEqualTo(ResponseFormat.builder().type(ResponseFormatType.JSON).build()); + return true; + }) + ); + + then(ctxMock).should().tellSuccess(argThat(resultMsg -> + resultMsg.getData().equals(chatResponse.aiMessage().text()) && + resultMsg.getMetaData().equals(msg.getMetaData()) && + resultMsg.getType().equals(msg.getType()) && + resultMsg.getOriginator().equals(msg.getOriginator())) + ); + + then(ctxMock).should(never()).enqueueForTellNext(any(), any(String.class)); + then(ctxMock).should(never()).enqueueForTellFailure(any(), any(Throwable.class)); + then(ctxMock).should(never()).tellNext(any(), any(String.class)); + then(ctxMock).should(never()).tellFailure(any(), any()); + } + +} diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/mqtt/TbMqttNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/mqtt/TbMqttNodeTest.java index 1e877b80dc..5e93f6910f 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/mqtt/TbMqttNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/mqtt/TbMqttNodeTest.java @@ -212,40 +212,45 @@ public class TbMqttNodeTest extends AbstractRuleNodeUpgradeTest { assertThatNoException().isThrownBy(() -> mqttNode.init(ctxMock, new TbNodeConfiguration(JacksonUtil.valueToTree(mqttNodeConfig)))); } - @Test - public void givenClientIdIsTooLong_whenInit_thenThrowsException() { - String invalidClientId = "vhfrbeb38ygwfwrgfwefgterhytjytj"; - mqttNodeConfig.setClientId(invalidClientId); + @ParameterizedTest + @MethodSource("provideInvalidClientIdScenarios") + public void givenInvalidClientId_whenInit_thenThrowsException(MqttVersion version, int maxLength, int repeat, String serviceId, boolean appendSuffix) { + String baseClientId = "x".repeat(repeat); + mqttNodeConfig.setClientId(baseClientId); + mqttNodeConfig.setAppendClientIdSuffix(appendSuffix); + mqttNodeConfig.setProtocolVersion(version); given(ctxMock.getTenantId()).willReturn(TENANT_ID); given(ctxMock.getSelf()).willReturn(new RuleNode(RULE_NODE_ID)); + String clientId = appendSuffix ? baseClientId + "_" + serviceId : baseClientId; + if (appendSuffix) { + given(ctxMock.getServiceId()).willReturn(serviceId); + } + + String expectedMessage = "The length of Client ID cannot be longer than " + maxLength + ", but current length is " + clientId.length() + "."; + assertThatThrownBy(() -> mqttNode.init(ctxMock, new TbNodeConfiguration(JacksonUtil.valueToTree(mqttNodeConfig)))) .isInstanceOf(TbNodeException.class) - .hasMessage("Client ID is too long '" + invalidClientId + "'. " + - "The length of Client ID cannot be longer than 23, but current length is " + invalidClientId.length() + ".") + .hasMessage(expectedMessage) .extracting(e -> ((TbNodeException) e).isUnrecoverable()) .isEqualTo(true); } - @Test - public void givenClientIdIsOkAndAppendClientIdSuffixIsTrue_whenInit_thenClientIdBecomesInvalidAndThrowsException() { - String validClientId = "fertjnhnjj4ge"; - mqttNodeConfig.setClientId("fertjnhnjj4ge"); - mqttNodeConfig.setAppendClientIdSuffix(true); + private static Stream provideInvalidClientIdScenarios() { + return Stream.of( + // MQTT_5, too long clientId + Arguments.of(MqttVersion.MQTT_5, 256, 257, null, false), - given(ctxMock.getTenantId()).willReturn(TENANT_ID); - given(ctxMock.getSelf()).willReturn(new RuleNode(RULE_NODE_ID)); - String serviceId = "test-service"; - given(ctxMock.getServiceId()).willReturn(serviceId); + // MQTT_5, base + suffix exceeds + Arguments.of(MqttVersion.MQTT_5, 256, 250, "test-service", true), - String resultedClientId = validClientId + "_" + serviceId; - assertThatThrownBy(() -> mqttNode.init(ctxMock, new TbNodeConfiguration(JacksonUtil.valueToTree(mqttNodeConfig)))) - .isInstanceOf(TbNodeException.class) - .hasMessage("Client ID is too long '" + resultedClientId + "'. " + - "The length of Client ID cannot be longer than 23, but current length is " + resultedClientId.length() + ".") - .extracting(e -> ((TbNodeException) e).isUnrecoverable()) - .isEqualTo(true); + // MQTT_3_1, too long clientId + Arguments.of(MqttVersion.MQTT_3_1, 23, 24, null, false), + + // MQTT_3_1, base + suffix exceeds + Arguments.of(MqttVersion.MQTT_3_1, 23, 5, "verylongservicename", true) + ); } @Test diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/mqtt/azure/TbAzureIotHubNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/mqtt/azure/TbAzureIotHubNodeTest.java index 433d5d4673..c8c1553fa5 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/mqtt/azure/TbAzureIotHubNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/mqtt/azure/TbAzureIotHubNodeTest.java @@ -34,6 +34,9 @@ import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.credentials.CertPemCredentials; import org.thingsboard.rule.engine.mqtt.TbMqttNodeConfiguration; +import java.time.Clock; +import java.time.Instant; +import java.time.ZoneOffset; import java.util.stream.Stream; import static org.assertj.core.api.Assertions.assertThat; @@ -77,7 +80,10 @@ public class TbAzureIotHubNodeTest extends AbstractRuleNodeUpgradeTest { @Test public void verifyPrepareMqttClientConfigMethodWithAzureIotHubSasCredentials() throws Exception { - AzureIotHubSasCredentials credentials = new AzureIotHubSasCredentials(); + var fixedClock = Clock.fixed(Instant.parse("2030-01-01T00:00:00Z"), ZoneOffset.UTC); + azureIotHubNode.setClock(fixedClock); + + var credentials = new AzureIotHubSasCredentials(); credentials.setSasKey("testSasKey"); credentials.setCaCert("test-ca-cert.pem"); azureIotHubNodeConfig.setCredentials(credentials); @@ -89,7 +95,7 @@ public class TbAzureIotHubNodeTest extends AbstractRuleNodeUpgradeTest { azureIotHubNode.prepareMqttClientConfig(mqttClientConfig); assertThat(mqttClientConfig.getUsername()).isEqualTo(AzureIotHubUtil.buildUsername(azureIotHubNodeConfig.getHost(), mqttClientConfig.getClientId())); - assertThat(mqttClientConfig.getPassword()).isEqualTo(AzureIotHubUtil.buildSasToken(azureIotHubNodeConfig.getHost(), credentials.getSasKey())); + assertThat(mqttClientConfig.getPassword()).isEqualTo(AzureIotHubUtil.buildSasToken(azureIotHubNodeConfig.getHost(), credentials.getSasKey(), fixedClock)); } @Test 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 4cbc091bdc..16698b2841 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,6 +29,7 @@ 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; @@ -40,6 +41,7 @@ import org.thingsboard.server.common.data.OtaPackage; import org.thingsboard.server.common.data.TbResource; 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.asset.Asset; import org.thingsboard.server.common.data.asset.AssetProfile; @@ -69,6 +71,7 @@ import org.thingsboard.server.common.data.rule.RuleChain; import org.thingsboard.server.common.data.rule.RuleNode; import org.thingsboard.server.common.data.widget.WidgetType; import org.thingsboard.server.common.data.widget.WidgetsBundle; +import org.thingsboard.server.dao.ai.AiModelService; import org.thingsboard.server.dao.asset.AssetService; import org.thingsboard.server.dao.cf.CalculatedFieldService; import org.thingsboard.server.dao.customer.CustomerService; @@ -94,6 +97,7 @@ import org.thingsboard.server.dao.user.UserService; import org.thingsboard.server.dao.widget.WidgetTypeService; import org.thingsboard.server.dao.widget.WidgetsBundleService; +import java.util.Optional; import java.util.UUID; import static org.mockito.ArgumentMatchers.any; @@ -164,10 +168,11 @@ public class TenantIdLoaderTest { private CalculatedFieldService calculatedFieldService; @Mock private JobService jobService; + @Mock + private AiModelService aiModelService; private TenantId tenantId; private TenantProfileId tenantProfileId; - private NotificationId notificationId; private AbstractListeningExecutor dbExecutor; @BeforeEach @@ -179,9 +184,8 @@ public class TenantIdLoaderTest { } }; dbExecutor.init(); - this.tenantId = new TenantId(UUID.randomUUID()); + this.tenantId = TenantId.fromUUID(UUID.randomUUID()); this.tenantProfileId = new TenantProfileId(UUID.randomUUID()); - this.notificationId = new NotificationId(UUID.randomUUID()); when(ctx.getTenantId()).thenReturn(tenantId); @@ -199,6 +203,7 @@ public class TenantIdLoaderTest { switch (entityType) { case TENANT: case NOTIFICATION: + case ADMIN_SETTINGS: break; case CUSTOMER: Customer customer = new Customer(); @@ -429,6 +434,12 @@ public class TenantIdLoaderTest { when(ctx.getJobService()).thenReturn(jobService); doReturn(job).when(jobService).findJobById(eq(tenantId), any()); break; + case AI_MODEL: + AiModel aiModel = new AiModel(); + aiModel.setTenantId(tenantId); + when(ctx.getAiModelService()).thenReturn(aiModelService); + doReturn(Optional.of(aiModel)).when(aiModelService).findAiModelById(eq(tenantId), any()); + break; default: throw new RuntimeException("Unexpected originator EntityType " + entityType); } @@ -465,7 +476,7 @@ public class TenantIdLoaderTest { @Test public void test_findEntityIdAsync_other_tenant() { - checkTenant(new TenantId(UUID.randomUUID()), false); + checkTenant(TenantId.fromUUID(UUID.randomUUID()), false); } } diff --git a/tools/pom.xml b/tools/pom.xml index d748e649ab..0b63d84b0a 100644 --- a/tools/pom.xml +++ b/tools/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.0-SNAPSHOT + 4.3.0-SNAPSHOT thingsboard tools diff --git a/transport/coap/pom.xml b/transport/coap/pom.xml index 1d829679ae..67f16917d3 100644 --- a/transport/coap/pom.xml +++ b/transport/coap/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.0-SNAPSHOT + 4.3.0-SNAPSHOT transport org.thingsboard.transport diff --git a/transport/coap/src/main/resources/tb-coap-transport.yml b/transport/coap/src/main/resources/tb-coap-transport.yml index c54bf038f2..f40a09c753 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 diff --git a/transport/http/pom.xml b/transport/http/pom.xml index 10ffdfc07e..371ecb84ca 100644 --- a/transport/http/pom.xml +++ b/transport/http/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.0-SNAPSHOT + 4.3.0-SNAPSHOT transport org.thingsboard.transport 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/pom.xml b/transport/lwm2m/pom.xml index 1c83f8b443..66a3066a88 100644 --- a/transport/lwm2m/pom.xml +++ b/transport/lwm2m/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.0-SNAPSHOT + 4.3.0-SNAPSHOT transport org.thingsboard.transport diff --git a/transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml b/transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml index 60568a6a4e..0895bfa676 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 diff --git a/transport/mqtt/pom.xml b/transport/mqtt/pom.xml index ea8c750b6d..57a73b1bbe 100644 --- a/transport/mqtt/pom.xml +++ b/transport/mqtt/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.0-SNAPSHOT + 4.3.0-SNAPSHOT transport org.thingsboard.transport 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/pom.xml b/transport/pom.xml index eeb4873e48..a8658bbf40 100644 --- a/transport/pom.xml +++ b/transport/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.0-SNAPSHOT + 4.3.0-SNAPSHOT thingsboard transport diff --git a/transport/snmp/pom.xml b/transport/snmp/pom.xml index 071c4423b6..d5c3d1fe02 100644 --- a/transport/snmp/pom.xml +++ b/transport/snmp/pom.xml @@ -21,7 +21,7 @@ org.thingsboard - 4.2.0-SNAPSHOT + 4.3.0-SNAPSHOT transport 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/package.json b/ui-ngx/package.json index 9514f5bfaf..580b63a588 100644 --- a/ui-ngx/package.json +++ b/ui-ngx/package.json @@ -1,6 +1,6 @@ { "name": "thingsboard", - "version": "4.2.0", + "version": "4.3.0", "scripts": { "ng": "ng", "start": "node --max_old_space_size=8048 ./node_modules/@angular/cli/bin/ng serve --configuration development --host 0.0.0.0 --open", @@ -140,6 +140,7 @@ "tinymce": "6.8.5", "rollup": "4.22.4", "@babel/core": "7.25.2", - "esbuild": "0.23.0" + "esbuild": "0.23.0", + "jquery.terminal/coveralls-next/form-data": "4.0.4" } } diff --git a/ui-ngx/pom.xml b/ui-ngx/pom.xml index 772921ee7a..56fc4ed628 100644 --- a/ui-ngx/pom.xml +++ b/ui-ngx/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.0-SNAPSHOT + 4.3.0-SNAPSHOT thingsboard org.thingsboard @@ -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.actions.ts b/ui-ngx/src/app/core/auth/auth.actions.ts index dedd67bcb9..3e2b22ff63 100644 --- a/ui-ngx/src/app/core/auth/auth.actions.ts +++ b/ui-ngx/src/app/core/auth/auth.actions.ts @@ -18,6 +18,7 @@ import { Action } from '@ngrx/store'; import { AuthUser, User } from '@shared/models/user.model'; import { AuthPayload } from '@core/auth/auth.models'; import { UserSettings } from '@shared/models/user-settings.models'; +import { TrendzSettings } from "@shared/models/trendz-settings.models"; export enum AuthActionTypes { AUTHENTICATED = '[Auth] Authenticated', @@ -31,6 +32,7 @@ export enum AuthActionTypes { UPDATE_OPENED_MENU_SECTION = '[Preferences] Update Opened Menu Section', PUT_USER_SETTINGS = '[Preferences] Put user settings', DELETE_USER_SETTINGS = '[Preferences] Delete user settings', + UPDATE_TRENDZ_SETTINGS = '[Auth] Update Trendz Settings', } export class ActionAuthAuthenticated implements Action { @@ -97,7 +99,13 @@ export class ActionPreferencesDeleteUserSettings implements Action { constructor(readonly payload: Array>) {} } +export class ActionAuthUpdateTrendzSettings implements Action { + readonly type = AuthActionTypes.UPDATE_TRENDZ_SETTINGS; + + constructor(readonly payload: TrendzSettings) {} +} + export type AuthActions = ActionAuthAuthenticated | ActionAuthUnauthenticated | ActionAuthLoadUser | ActionAuthUpdateUserDetails | ActionAuthUpdateLastPublicDashboardId | ActionAuthUpdateHasRepository | ActionPreferencesUpdateOpenedMenuSection | ActionPreferencesPutUserSettings | ActionPreferencesDeleteUserSettings | - ActionAuthUpdateAuthUser | ActionUpdateMobileQrCodeEnabled; + ActionAuthUpdateAuthUser | ActionUpdateMobileQrCodeEnabled | ActionAuthUpdateTrendzSettings; diff --git a/ui-ngx/src/app/core/auth/auth.models.ts b/ui-ngx/src/app/core/auth/auth.models.ts index 142d845cf4..d6612f2427 100644 --- a/ui-ngx/src/app/core/auth/auth.models.ts +++ b/ui-ngx/src/app/core/auth/auth.models.ts @@ -31,6 +31,8 @@ export interface SysParamsState { maxDebugModeDurationMinutes: number; maxDataPointsPerRollingArg: number; maxArgumentsPerCF: 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 fde778284d..8cfbc04197 100644 --- a/ui-ngx/src/app/core/auth/auth.reducer.ts +++ b/ui-ngx/src/app/core/auth/auth.reducer.ts @@ -33,6 +33,8 @@ const emptyUserAuthState: AuthPayload = { mobileQrEnabled: false, maxResourceSize: 0, maxArgumentsPerCF: 0, + minAllowedScheduledUpdateIntervalInSecForCF: 0, + maxRelationLevelPerCfArgument: 0, maxDataPointsPerRollingArg: 0, maxDebugModeDurationMinutes: 0, userSettings: initialUserSettings, @@ -99,6 +101,9 @@ export const authReducer = ( action.payload.forEach(path => unset(userSettings, path)); return { ...state, ...{ userSettings }}; + case AuthActionTypes.UPDATE_TRENDZ_SETTINGS: + return { ...state, trendzSettings: action.payload }; + default: return state; } diff --git a/ui-ngx/src/app/core/http/ai-model.service.ts b/ui-ngx/src/app/core/http/ai-model.service.ts new file mode 100644 index 0000000000..64e003c30e --- /dev/null +++ b/ui-ngx/src/app/core/http/ai-model.service.ts @@ -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. +/// + +import { Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { defaultHttpOptionsFromConfig, RequestConfig } from '@core/http/http-utils'; +import { Observable } from 'rxjs'; +import { AiModel, AiModelWithUserMsg, CheckConnectivityResult } from '@shared/models/ai-model.models'; +import { PageLink } from '@shared/models/page/page-link'; +import { PageData } from '@shared/models/page/page-data'; + +@Injectable({ + providedIn: 'root' +}) +export class AiModelService { + + constructor( + private http: HttpClient + ) {} + + public saveAiModel(aiModel: AiModel, config?: RequestConfig): Observable { + return this.http.post('/api/ai/model', aiModel, defaultHttpOptionsFromConfig(config)); + } + + public getAiModels(pageLink: PageLink, config?: RequestConfig): Observable> { + return this.http.get>(`/api/ai/model${pageLink.toQuery()}`, defaultHttpOptionsFromConfig(config)); + } + + public getAiModelById(aiModelId: string, config?: RequestConfig): Observable { + return this.http.get(`/api/ai/model/${aiModelId}`, defaultHttpOptionsFromConfig(config)); + } + + public deleteAiModel(aiModelId: string, config?: RequestConfig) { + return this.http.delete(`/api/ai/model/${aiModelId}`, defaultHttpOptionsFromConfig(config)); + } + + public checkConnectivity(aiModelWithUserMsg: AiModelWithUserMsg, config?: RequestConfig): Observable { + return this.http.post('/api/ai/model/chat', aiModelWithUserMsg, defaultHttpOptionsFromConfig(config)); + } + +} diff --git a/ui-ngx/src/app/core/http/entity.service.ts b/ui-ngx/src/app/core/http/entity.service.ts index 01fed28bf3..572d6cc473 100644 --- a/ui-ngx/src/app/core/http/entity.service.ts +++ b/ui-ngx/src/app/core/http/entity.service.ts @@ -99,6 +99,7 @@ import { ResourceService } from '@core/http/resource.service'; 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'; @Injectable({ providedIn: 'root' @@ -131,6 +132,7 @@ export class EntityService { private resourceService: ResourceService, private oauth2Service: OAuth2Service, private mobileAppService: MobileAppService, + private aiModelService: AiModelService, ) { } private getEntityObservable(entityType: EntityType, entityId: string, @@ -183,6 +185,9 @@ export class EntityService { case EntityType.MOBILE_APP_BUNDLE: observable = this.mobileAppService.getMobileAppBundleInfoById(entityId, config); break; + case EntityType.AI_MODEL: + observable = this.aiModelService.getAiModelById(entityId, config); + break; } return observable; } @@ -485,6 +490,10 @@ export class EntityService { pageLink.sortOrder.property = 'title'; entitiesObservable = this.mobileAppService.getTenantMobileAppBundleInfos(pageLink, config); break; + case EntityType.AI_MODEL: + pageLink.sortOrder.property = 'name'; + entitiesObservable = this.aiModelService.getAiModels(pageLink, config); + break; } return entitiesObservable; } @@ -1022,11 +1031,7 @@ export class EntityService { const stateEntityId = stateEntityInfo.entityId; switch (filter.type) { case AliasFilterType.singleEntity: - const aliasEntityId = this.resolveAliasEntityId(filter.singleEntity.entityType, filter.singleEntity.id); - result.entityFilter = { - type: AliasFilterType.singleEntity, - singleEntity: aliasEntityId - }; + result.entityFilter = deepClone(filter); return of(result); case AliasFilterType.entityList: result.entityFilter = deepClone(filter); @@ -1077,9 +1082,8 @@ export class EntityService { rootEntityId = filter.rootEntity.id; } if (rootEntityType && rootEntityId) { - const queryRootEntityId = this.resolveAliasEntityId(rootEntityType, rootEntityId); result.entityFilter = deepClone(filter); - result.entityFilter.rootEntity = queryRootEntityId; + result.entityFilter.rootEntity = {entityType: rootEntityType, id: rootEntityId}; return of(result); } else { return of(result); @@ -1378,44 +1382,9 @@ export class EntityService { if (!entityId) { entityId = filter.defaultStateEntity; } - if (entityId) { - entityId = this.resolveAliasEntityId(entityId.entityType, entityId.id); - } return {entityId}; } - private resolveAliasEntityId(entityType: EntityType | AliasEntityType, id: string): EntityId { - const entityId: EntityId = { - entityType, - id - }; - if (entityType === AliasEntityType.CURRENT_CUSTOMER) { - const authUser = getCurrentAuthUser(this.store); - entityId.entityType = EntityType.CUSTOMER; - if (authUser.authority === Authority.CUSTOMER_USER) { - entityId.id = authUser.customerId; - } - } else if (entityType === AliasEntityType.CURRENT_TENANT){ - const authUser = getCurrentAuthUser(this.store); - entityId.entityType = EntityType.TENANT; - entityId.id = authUser.tenantId; - } else if (entityType === AliasEntityType.CURRENT_USER){ - const authUser = getCurrentAuthUser(this.store); - entityId.entityType = EntityType.USER; - entityId.id = authUser.userId; - } else if (entityType === AliasEntityType.CURRENT_USER_OWNER){ - const authUser = getCurrentAuthUser(this.store); - if (authUser.authority === Authority.TENANT_ADMIN) { - entityId.entityType = EntityType.TENANT; - entityId.id = authUser.tenantId; - } else if (authUser.authority === Authority.CUSTOMER_USER) { - entityId.entityType = EntityType.CUSTOMER; - entityId.id = authUser.customerId; - } - } - return entityId; - } - private createDatasourceFromSubscriptionInfo(subscriptionInfo: SubscriptionInfo): Datasource { subscriptionInfo = this.validateSubscriptionInfo(subscriptionInfo); let datasource: Datasource = null; diff --git a/ui-ngx/src/app/core/http/public-api.ts b/ui-ngx/src/app/core/http/public-api.ts index c28d80d173..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,9 +46,11 @@ 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'; export * from './widget.service'; export * from './usage-info.service'; export * from './trendz-settings.service' +export * from './ai-model.service' 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/menu.models.ts b/ui-ngx/src/app/core/services/menu.models.ts index 607c5c6dff..4277f96747 100644 --- a/ui-ngx/src/app/core/services/menu.models.ts +++ b/ui-ngx/src/app/core/services/menu.models.ts @@ -105,7 +105,8 @@ export enum MenuId { otaUpdates = 'otaUpdates', version_control = 'version_control', api_usage = 'api_usage', - trendz_settings = 'trendz_settings' + trendz_settings = 'trendz_settings', + ai_models = 'ai_models' } declare type MenuFilter = (authState: AuthState) => boolean; @@ -286,6 +287,16 @@ export const menuSectionMap = new Map([ icon: 'mdi:message-cog' } ], + [ + MenuId.ai_models, + { + id: MenuId.ai_models, + name: 'ai-models.ai-models', + type: 'link', + path: '/settings/ai-models', + icon: 'auto_awesome' + } + ], [ MenuId.mobile_center, { @@ -856,7 +867,8 @@ const defaultUserMenuMap = new Map([ {id: MenuId.notification_settings}, {id: MenuId.repository_settings}, {id: MenuId.auto_commit_settings}, - {id: MenuId.trendz_settings} + {id: MenuId.trendz_settings}, + {id: MenuId.ai_models} ] }, { diff --git a/ui-ngx/src/app/core/utils.ts b/ui-ngx/src/app/core/utils.ts index 353727e006..198737bfed 100644 --- a/ui-ngx/src/app/core/utils.ts +++ b/ui-ngx/src/app/core/utils.ts @@ -27,7 +27,8 @@ 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'; @@ -196,6 +197,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 +791,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; @@ -958,3 +1003,28 @@ 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; +} diff --git a/ui-ngx/src/app/modules/common/modules-map.ts b/ui-ngx/src/app/modules/common/modules-map.ts index 70432e8cdf..0890b9e623 100644 --- a/ui-ngx/src/app/modules/common/modules-map.ts +++ b/ui-ngx/src/app/modules/common/modules-map.ts @@ -336,6 +336,7 @@ import * as DatapointsLimitComponent from '@shared/components/time/datapoints-li import * as AggregationTypeSelectComponent from '@shared/components/time/aggregation/aggregation-type-select.component'; import * as AggregationOptionsConfigComponent from '@shared/components/time/aggregation/aggregation-options-config-panel.component'; import * as IntervalOptionsConfigPanelComponent from '@shared/components/time/interval-options-config-panel.component'; +import * as AIModelDialogComponent from '@home/components/ai-model/ai-model-dialog.component'; import { IModulesMap } from '@modules/common/modules-map.models'; import { Observable, of } from 'rxjs'; @@ -668,7 +669,8 @@ class ModulesMap implements IModulesMap { '@home/components/dashboard-page/dashboard-image-dialog.component': DashboardImageDialogComponent, '@home/components/widget/widget-container.component': WidgetContainerComponent, '@home/components/profile/queue/tenant-profile-queues.component': TenantProfileQueuesComponent, - '@home/components/queue/queue-form.component': QueueFormComponent + '@home/components/queue/queue-form.component': QueueFormComponent, + '@home/components/ai-model/ai-model-dialog.component': AIModelDialogComponent, }; init(): Observable { 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 new file mode 100644 index 0000000000..5f9c189399 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/ai-model/ai-model-dialog.component.html @@ -0,0 +1,293 @@ + + +

{{ dialogTitle | translate }}

+ +
+ +
+ + +
+
+ +
+ + ai-models.name + + + + {{ 'ai-models.name-required' | translate }} + + + {{ 'ai-models.name-max-length' | translate }} + + +
+
+
+
ai-models.provider
+
+ + ai-models.ai-provider + + + {{providerTranslationMap.get(provider) | translate}} + + + +
+ @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 }} + + + } + @if (providerFieldsList.includes('serviceAccountKey')) { + + + } + @if (providerFieldsList.includes('endpoint')) { + + ai-models.endpoint + + + {{ 'ai-models.endpoint-required' | translate }} + + + } + @if (providerFieldsList.includes('serviceVersion')) { + + ai-models.service-version + + + } + @if (providerFieldsList.includes('apiKey')) { + + ai-models.api-key + + + + {{ '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 }} + + + } + @if (providerFieldsList.includes('secretAccessKey')) { + + ai-models.secret-access-key + + + + {{ 'ai-models.secret-access-key-required' | translate }} + + + } +
+
+
+
+
ai-models.configuration
+
+
+ + +
+ @if (modelFieldsList.includes('temperature')) { +
+
+ {{ 'ai-models.temperature' | translate }} +
+ + + + warning + + +
+ } + @if (modelFieldsList.includes('topP')) { +
+
+ {{ 'ai-models.top-p' | translate }} +
+ + + + warning + + +
+ } + @if (modelFieldsList.includes('topK')) { +
+
+ {{ 'ai-models.top-k' | translate }} +
+ + + + warning + + +
+ } + @if (modelFieldsList.includes('presencePenalty')) { +
+
+ {{ 'ai-models.presence-penalty' | translate }} +
+ + + +
+ } + + @if (modelFieldsList.includes('frequencyPenalty')) { +
+
+ {{ 'ai-models.frequency-penalty' | translate }} +
+ + + +
+ } + @if (modelFieldsList.includes('maxOutputTokens')) { +
+
+ {{ 'ai-models.max-output-tokens' | translate }} +
+ + + + warning + + +
+ } +
+
+
+ +
+
+ + + + +
diff --git a/ui-ngx/src/app/modules/home/components/ai-model/ai-model-dialog.component.scss b/ui-ngx/src/app/modules/home/components/ai-model/ai-model-dialog.component.scss new file mode 100644 index 0000000000..c55453c388 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/ai-model/ai-model-dialog.component.scss @@ -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. + */ + +:host { + width: 850px; + height: 100%; + max-width: 100%; + max-height: 100vh; + display: grid; +} 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 new file mode 100644 index 0000000000..b74c113d9f --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/ai-model/ai-model-dialog.component.ts @@ -0,0 +1,173 @@ +/// +/// 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 { 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, MatDialog, MatDialogRef } from '@angular/material/dialog'; +import { Observable, of } from 'rxjs'; +import { StepperOrientation } from '@angular/cdk/stepper'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { EntityType } from '@shared/models/entity-type.models'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { + AiModel, + AiModelMap, + AiProvider, + AiProviderTranslations, + 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'; + +export interface AIModelDialogData { + AIModel?: AiModel; + isAdd?: boolean; + name?: string; +} + +@Component({ + selector: 'tb-ai-model-dialog', + templateUrl: './ai-model-dialog.component.html', + styleUrls: ['./ai-model-dialog.component.scss'] +}) +export class AIModelDialogComponent extends DialogComponent { + + readonly entityType = EntityType; + + selectedIndex = 0; + + dialogTitle = 'ai-models.ai-model'; + + stepperOrientation: Observable; + + aiProvider = AiProvider; + providerMap: AiProvider[] = Object.keys(AiProvider) as AiProvider[]; + providerTranslationMap = AiProviderTranslations; + + provider: AiProvider = AiProvider.OPENAI; + + aiModelForms: FormGroup; + + isAdd = false; + + constructor(protected store: Store, + protected router: Router, + protected dialogRef: MatDialogRef, + @Inject(MAT_DIALOG_DATA) public data: AIModelDialogData, + private fb: FormBuilder, + private aiModelService: AiModelService, + private dialog: MatDialog) { + super(store, router, dialogRef); + + if (this.data.isAdd) { + this.isAdd = true; + } + + this.provider = this.data.AIModel ? this.data.AIModel.configuration.provider : AiProvider.OPENAI; + + this.aiModelForms = this.fb.group({ + name: [this.data.AIModel ? this.data.AIModel.name : '', [Validators.required, Validators.maxLength(255), Validators.pattern(/.*\S.*/)]], + modelType: [ModelType.CHAT], + configuration: this.fb.group({ + provider: [this.provider, []], + providerConfig: this.fb.group({ + apiKey: [this.data.AIModel ? this.data.AIModel.configuration.providerConfig?.apiKey : '', [Validators.required]], + personalAccessToken: [this.data.AIModel ? this.data.AIModel.configuration.providerConfig?.personalAccessToken : '', [Validators.required]], + endpoint: [this.data.AIModel ? this.data.AIModel.configuration.providerConfig?.endpoint : '', [Validators.required]], + serviceVersion: [this.data.AIModel ? this.data.AIModel.configuration.providerConfig?.serviceVersion : ''], + projectId: [this.data.AIModel ? this.data.AIModel.configuration.providerConfig?.projectId : '', [Validators.required]], + location: [this.data.AIModel ? this.data.AIModel.configuration.providerConfig?.location : '', [Validators.required]], + serviceAccountKey: [this.data.AIModel ? this.data.AIModel.configuration.providerConfig?.serviceAccountKey : '', [Validators.required]], + fileName: [this.data.AIModel ? this.data.AIModel.configuration.providerConfig?.fileName : '', [Validators.required]], + region: [this.data.AIModel ? this.data.AIModel.configuration.providerConfig?.region : '', [Validators.required]], + accessKeyId: [this.data.AIModel ? this.data.AIModel.configuration.providerConfig?.accessKeyId : '', [Validators.required]], + secretAccessKey: [this.data.AIModel ? this.data.AIModel.configuration.providerConfig?.secretAccessKey : '', [Validators.required]], + }), + modelId: [this.data.AIModel ? this.data.AIModel.configuration?.modelId : '', [Validators.required]], + temperature: [this.data.AIModel ? this.data.AIModel.configuration?.temperature : null, [Validators.min(0)]], + topP: [this.data.AIModel ? this.data.AIModel.configuration?.topP : null, [Validators.min(0.1), Validators.max(1)]], + topK: [this.data.AIModel ? this.data.AIModel.configuration?.topK : null, [Validators.min(0)]], + frequencyPenalty: [this.data.AIModel ? this.data.AIModel.configuration?.frequencyPenalty : null], + presencePenalty: [this.data.AIModel ? this.data.AIModel.configuration?.presencePenalty : null], + maxOutputTokens: [this.data.AIModel ? this.data.AIModel.configuration?.maxOutputTokens : null, [Validators.min(1)]] + }) + }); + + if (this.data.name) { + this.aiModelForms.get('name').patchValue(this.data.name, {emitEvent: false}); + } + + this.aiModelForms.get('configuration.provider').valueChanges.pipe( + takeUntilDestroyed() + ).subscribe((provider: AiProvider) => { + this.provider = provider; + this.aiModelForms.get('configuration.modelId').reset(''); + this.aiModelForms.get('configuration.providerConfig').reset({}); + this.updateValidation(provider); + }) + + this.updateValidation(this.provider); + } + + fetchOptions(searchText: string): Observable> { + const search = searchText ? searchText?.toLowerCase() : ''; + return of(this.provider ? AiModelMap.get(this.provider).modelList || [] : []).pipe( + map(name => name?.filter(option => option.toLowerCase().includes(search))), + ); + } + + private updateValidation(provider: AiProvider) { + ProviderFieldsAllList.forEach(key => { + if (AiModelMap.get(provider).providerFieldsList.includes(key)) { + this.aiModelForms.get('configuration.providerConfig').get(key).enable(); + } else { + this.aiModelForms.get('configuration.providerConfig').get(key).disable(); + } + }) + } + + get providerFieldsList(): string[] { + return AiModelMap.get(this.provider).providerFieldsList; + } + get modelFieldsList(): string[] { + return AiModelMap.get(this.provider).modelFieldsList; + } + + cancel(): void { + this.dialogRef.close(null); + } + + checkConnectivity() { + return this.dialog.open(CheckConnectivityDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + AIModel: this.aiModelForms.value + } + }).afterClosed(); + } + + add(): void { + const aiModel = {...this.data.AIModel, ...this.aiModelForms.value} as AiModel; + this.aiModelService.saveAiModel(deepTrim(aiModel)).subscribe(aiModel => this.dialogRef.close(aiModel)); + } +} diff --git a/ui-ngx/src/app/modules/home/components/ai-model/check-connectivity-dialog.component.html b/ui-ngx/src/app/modules/home/components/ai-model/check-connectivity-dialog.component.html new file mode 100644 index 0000000000..afc881b10b --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/ai-model/check-connectivity-dialog.component.html @@ -0,0 +1,57 @@ + + +

ai-models.check-connectivity

+ + +
+
+
+ + +
+
+ check_circle +
+ {{ "ai-models.check-connectivity-success" | translate }} +
+
+
+ cancel +
+
{{ "ai-models.check-connectivity-failed" | translate }}
+ + +
+
+
+
+ +
diff --git a/ui-ngx/src/app/modules/home/components/ai-model/check-connectivity-dialog.component.scss b/ui-ngx/src/app/modules/home/components/ai-model/check-connectivity-dialog.component.scss new file mode 100644 index 0000000000..a85d0111de --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/ai-model/check-connectivity-dialog.component.scss @@ -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. + */ + +:host { + width: 560px; + height: 100%; + max-width: 100%; + max-height: 100vh; + display: grid; + + .transparent { + background-color: transparent; + } + + .connection-status { + font-weight: 500; + letter-spacing: 0.25px; + text-align: center; + } + + .connection-icon { + height: 32px; + font-size: 32px; + width: 32px; + margin-bottom: 4px; + } + + .error_msg { + text-align: center; + margin-top: 8px; + font-size: 14px; + line-height: 130%; + letter-spacing: 0.25px; + opacity: 0.9; + } + + .success { + color: #198038; + } + + .error { + color: #D12730; + } + + ::ng-deep { + .json-editor { + .tb-json-object-toolbar { + display: none; + } + .tb-json-panel { + margin: 0; + } + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/ai-model/check-connectivity-dialog.component.ts b/ui-ngx/src/app/modules/home/components/ai-model/check-connectivity-dialog.component.ts new file mode 100644 index 0000000000..dd2c27cb47 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/ai-model/check-connectivity-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 } 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 { AiModel, AiModelWithUserMsg, ModelType } from '@shared/models/ai-model.models'; +import { AiModelService } from '@core/http/ai-model.service'; + +export interface AIModelDialogData { + AIModel?: AiModel; +} + +@Component({ + selector: 'tb-check-connectivity-dialog', + templateUrl: './check-connectivity-dialog.component.html', + styleUrls: ['./check-connectivity-dialog.component.scss'] +}) +export class CheckConnectivityDialogComponent extends DialogComponent { + + showCheckSuccess = false; + checkErrMsg = ''; + + constructor(protected store: Store, + protected router: Router, + protected dialogRef: MatDialogRef, + @Inject(MAT_DIALOG_DATA) public data: AIModelDialogData, + private aiModelService: AiModelService) { + super(store, router, dialogRef); + + if (this.data.AIModel) { + const aiModelWithMsg: AiModelWithUserMsg = { + userMessage: { + contents: [ + { + contentType: "TEXT", + text: "What is the capital of Ukraine?" + } + ] + }, + chatModelConfig: { + modelType: ModelType.CHAT, + provider: this.data.AIModel.configuration.provider, + providerConfig: {...this.data.AIModel.configuration.providerConfig}, + modelId: this.data.AIModel.configuration.modelId, + maxRetries: 0, + timeoutSeconds: 20 + } + } + this.aiModelService.checkConnectivity(aiModelWithMsg, { + ignoreErrors: true, + ignoreLoading: true + }).subscribe({ + next: (result) => { + if (result.status === 'SUCCESS') { + this.showCheckSuccess = true; + } else { + try { + this.checkErrMsg = JSON.parse(result.errorDetails); + } catch (e) { + this.checkErrMsg = result.errorDetails; + } + } + }, + error: err => this.checkErrMsg = err.error.message + }); + } + } + + cancel(): void { + this.dialogRef.close(null); + } +} 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/alias/entity-alias-dialog.component.html b/ui-ngx/src/app/modules/home/components/alias/entity-alias-dialog.component.html index be29713097..f735491e78 100644 --- a/ui-ngx/src/app/modules/home/components/alias/entity-alias-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/alias/entity-alias-dialog.component.html @@ -28,9 +28,9 @@
-
-
- +
+
+ alias.name diff --git a/ui-ngx/src/app/modules/home/components/alias/entity-alias-dialog.component.scss b/ui-ngx/src/app/modules/home/components/alias/entity-alias-dialog.component.scss index 0e1847f157..0aa1a0884f 100644 --- a/ui-ngx/src/app/modules/home/components/alias/entity-alias-dialog.component.scss +++ b/ui-ngx/src/app/modules/home/components/alias/entity-alias-dialog.component.scss @@ -19,7 +19,7 @@ .tb-resolve-multiple-switch { padding: 18px 0 0 18px; @media #{$mat-xs} { - padding: 0 0 18px 0; + padding: 0 0 22px 0; } } } diff --git a/ui-ngx/src/app/modules/home/components/alias/entity-aliases-dialog.component.html b/ui-ngx/src/app/modules/home/components/alias/entity-aliases-dialog.component.html index 5c1bd2c219..c067517aff 100644 --- a/ui-ngx/src/app/modules/home/components/alias/entity-aliases-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/alias/entity-aliases-dialog.component.html @@ -38,14 +38,14 @@
-
+
{{$index + 1}}.
- + {{ 'entity.alias-required' | translate }} 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-fields-table-config.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table-config.ts index 07f8f0293c..5f4b894448 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 @@ -113,16 +113,16 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig { 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('type', 'common.type', '70px', entity => this.translate.instant(CalculatedFieldTypeTranslations.get(entity.type)))); this.columns.push(expressionColumn); this.cellActionDescriptors.push( @@ -159,7 +159,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; } 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/arguments-table/calculated-field-arguments-table.component.html index b4b5a939e1..8cf040538c 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/arguments-table/calculated-field-arguments-table.component.html @@ -59,7 +59,7 @@
@if (argument.refEntityId?.id && argument.refEntityId?.entityType !== ArgumentEntityType.Tenant) { - {{ entityNameMap.get(argument.refEntityId.id) ?? '' }} @@ -80,7 +80,7 @@ {{ 'entity.key' | translate }} - +
{{ argument.refEntityKey.key }}
diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/arguments-table/calculated-field-arguments-table.component.scss b/ui-ngx/src/app/modules/home/components/calculated-fields/components/arguments-table/calculated-field-arguments-table.component.scss index ae8fd25170..430958d0f4 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/arguments-table/calculated-field-arguments-table.component.scss +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/arguments-table/calculated-field-arguments-table.component.scss @@ -55,12 +55,6 @@ } :host ::ng-deep { - .mat-mdc-standard-chip { - .mdc-evolution-chip__cell--primary, .mdc-evolution-chip__action--primary, .mdc-evolution-chip__text-label { - overflow: hidden; - } - } - .arguments-table:not(.arguments-table-with-error) { .mdc-data-table__row:last-child .mat-mdc-cell { border-bottom: none; diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/arguments-table/calculated-field-arguments-table.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/arguments-table/calculated-field-arguments-table.component.ts index 7763fd6f03..28a3f09126 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/arguments-table/calculated-field-arguments-table.component.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/arguments-table/calculated-field-arguments-table.component.ts @@ -179,7 +179,7 @@ export class CalculatedFieldArgumentsTableComponent implements ControlValueAcces renderer: this.renderer, componentType: CalculatedFieldArgumentPanelComponent, hostView: this.viewContainerRef, - preferredPlacement: isExists ? 'left' : 'right', + preferredPlacement: isExists ? ['left', 'leftTop', 'leftBottom'] : ['topRight', 'right', 'rightTop'], context: ctx, isModal: true }); diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.html b/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.html index 7b69d26a60..4b12d9f31a 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.html @@ -63,76 +63,116 @@
-
-
{{ 'calculated-fields.arguments' | translate }}
- -
-
-
- {{ (fieldFormGroup.get('type').value === CalculatedFieldType.SIMPLE ? 'calculated-fields.expression' : 'calculated-fields.type.script' ) | translate }} + @if (fieldFormGroup.get('type').value !== CalculatedFieldType.GEOFENCING) { +
+
{{ '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 }}
- -
-
- + + +
+
+ @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 }}
+ +
+
+ +
-
+ } @else { +
+
+ {{ 'calculated-fields.entity-coordinates' | translate }} +
+
+ + +
+
+ +
+
+ {{ 'calculated-fields.geofencing-zone-groups' | translate }} +
+ +
+
{{ 'calculated-fields.zone-group-refresh-interval' | translate }}
+
+ + +
+
+
+ }
{{ 'calculated-fields.output' | translate }}
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..3037b3a4ce 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 @@ -22,19 +22,22 @@ import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { Router } from '@angular/router'; import { DialogComponent } from '@shared/components/dialog.component'; import { + ArgumentEntityType, CalculatedField, CalculatedFieldConfiguration, calculatedFieldDefaultScript, + CalculatedFieldGeofencing, CalculatedFieldTestScriptFn, CalculatedFieldType, CalculatedFieldTypeTranslations, getCalculatedFieldArgumentsEditorCompleter, getCalculatedFieldArgumentsHighlights, + getCalculatedFieldCurrentEntityFilter, OutputType, OutputTypeTranslations } from '@shared/models/calculated-field.models'; import { digitsRegex, oneSpaceInsideRegex } from '@shared/models/regex.constants'; -import { AttributeScope } from '@shared/models/telemetry/telemetry.models'; +import { AttributeScope, DataKeyType } from '@shared/models/telemetry/telemetry.models'; import { EntityType } from '@shared/models/entity-type.models'; import { map, startWith, switchMap } from 'rxjs/operators'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @@ -43,6 +46,8 @@ 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 { EntityFilter } from '@shared/models/query/query.models'; +import { getCurrentAuthState } from '@core/auth/auth.selectors'; export interface CalculatedFieldDialogData { value?: CalculatedField; @@ -63,12 +68,20 @@ export interface CalculatedFieldDialogData { }) export class CalculatedFieldDialogComponent extends DialogComponent { + readonly minAllowedScheduledUpdateIntervalInSecForCF = getCurrentAuthState(this.store).minAllowedScheduledUpdateIntervalInSecForCF; + fieldFormGroup = this.fb.group({ name: ['', [Validators.required, Validators.pattern(oneSpaceInsideRegex), Validators.maxLength(255)]], type: [CalculatedFieldType.SIMPLE], debugSettings: [], configuration: this.fb.group({ + entityCoordinates: this.fb.group({ + latitudeKeyName: [null, [Validators.required]], + longitudeKeyName: [null, [Validators.required]], + }), arguments: this.fb.control({}), + zoneGroups: this.fb.control({}), + scheduledUpdateInterval: [this.minAllowedScheduledUpdateIntervalInSecForCF], expressionSIMPLE: ['', [Validators.required, Validators.pattern(oneSpaceInsideRegex), Validators.maxLength(255)]], expressionSCRIPT: [calculatedFieldDefaultScript], output: this.fb.group({ @@ -104,6 +117,10 @@ export class CalculatedFieldDialogComponent extends DialogComponent this.data.additionalDebugActionConfig.action({ id: this.data.value.id, ...this.fromGroupValue }), } : null; + currentEntityFilter: EntityFilter; + + isRelatedEntity: boolean; + readonly OutputTypeTranslations = OutputTypeTranslations; readonly OutputType = OutputType; readonly AttributeScope = AttributeScope; @@ -113,6 +130,7 @@ export class CalculatedFieldDialogComponent extends DialogComponent, protected router: Router, @@ -125,6 +143,8 @@ export class CalculatedFieldDialogComponent extends DialogComponent this.toggleKeyByCalculatedFieldType(type)); } + private observeZoneChanges(): void { + this.configFormGroup.get('zoneGroups').valueChanges + .pipe(takeUntilDestroyed()) + .subscribe((zoneGroups: CalculatedFieldGeofencing) => + this.checkRelatedEntity(zoneGroups) + ); + this.checkRelatedEntity(this.configFormGroup.get('zoneGroups').value); + } + + private checkRelatedEntity(zoneGroups: CalculatedFieldGeofencing) { + this.isRelatedEntity = Object.values(zoneGroups).some(zone => zone.refDynamicSourceConfiguration?.type === ArgumentEntityType.RelationQuery); + } + private toggleScopeByOutputType(type: OutputType): void { if (type === OutputType.Attribute) { this.outputFormGroup.get('scope').enable({emitEvent: false}); @@ -222,20 +270,36 @@ export class CalculatedFieldDialogComponent extends DialogComponent +
+
+ + + +
{{ '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.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-zone-grups-table/calculated-field-geofencing-zone-groups-table.component.scss b/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-zone-grups-table/calculated-field-geofencing-zone-groups-table.component.scss new file mode 100644 index 0000000000..430958d0f4 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-zone-grups-table/calculated-field-geofencing-zone-groups-table.component.scss @@ -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. + */ +:host { + .arguments-table { + min-height: 108px; + + &-with-error { + min-height: 150px; + } + + .mat-mdc-table { + table-layout: fixed; + } + + .key-text { + font-size: 13px; + } + + .copy-argument-name { + visibility: hidden; + transition: visibility 0.1s; + } + + .argument-name-cell:hover { + .copy-argument-name { + visibility: visible; + } + } + } + + .max-args-warning { + .mat-icon { + color: #FAA405; + } + } + + .tb-form-table-row-cell-buttons { + --mat-badge-legacy-small-size-container-size: 8px; + --mat-badge-small-size-container-overlap-offset: -5px; + --mat-badge-small-size-text-size: 0; + } +} + +:host ::ng-deep { + .arguments-table:not(.arguments-table-with-error) { + .mdc-data-table__row:last-child .mat-mdc-cell { + border-bottom: none; + } + } + + .arguments-table { + .mat-mdc-header-row.mat-row-select .mat-mdc-header-cell.entity-type-header { + padding: 0 28px 0 0; + } + } + + .copy-argument-name { + .mat-icon { + font-size: 16px; + padding: 4px; + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-zone-grups-table/calculated-field-geofencing-zone-groups-table.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-zone-grups-table/calculated-field-geofencing-zone-groups-table.component.ts new file mode 100644 index 0000000000..a11ae4cea5 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-zone-grups-table/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, + CalculatedFieldType, + 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/panel/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-geofencing-zone-groups-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() entityId: EntityId; + @Input() tenantId: string; + @Input() entityName: string; + + @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, + calculatedFieldType: CalculatedFieldType.GEOFENCING, + buttonTitle: isExists ? 'action.apply' : 'action.add', + tenantId: this.tenantId, + entityName: this.entityName, + 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 ? ['left', 'leftTop', 'leftBottom'] : ['topRight', 'right', 'rightTop'], + 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/panel/calculated-field-argument-panel.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-argument-panel.component.ts index 6f46e47af9..8ccaa4fe49 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/panel/calculated-field-argument-panel.component.ts @@ -86,7 +86,7 @@ export class CalculatedFieldArgumentPanelComponent implements OnInit, AfterViewI entityFilter: EntityFilter; entityNameSubject = new BehaviorSubject(null); - readonly argumentEntityTypes = Object.values(ArgumentEntityType) as ArgumentEntityType[]; + readonly argumentEntityTypes = Object.values(ArgumentEntityType).filter(value => value !== ArgumentEntityType.RelationQuery) as ArgumentEntityType[]; readonly ArgumentEntityTypeTranslations = ArgumentEntityTypeTranslations; readonly ArgumentType = ArgumentType; readonly DataKeyType = DataKeyType; diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-geofencing-zone-groups-panel.component.html b/ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-geofencing-zone-groups-panel.component.html new file mode 100644 index 0000000000..a660158ab9 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-geofencing-zone-groups-panel.component.html @@ -0,0 +1,224 @@ + +
+
+
{{ '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.relation-query' | translate }}* + +
+
{{ 'calculated-fields.direction' | translate }}
+ + + @for (direction of GeofencingDirectionList; track direction) { + {{ GeofencingDirectionTranslations.get(direction) | translate }} + } + + +
+
+
{{ 'calculated-fields.relation-type' | translate }}
+ + +
+
+
{{ 'calculated-fields.relation-level' | translate }}
+ + + @if (refDynamicSourceFormGroup.get('maxLevel').touched && refDynamicSourceFormGroup.get('maxLevel').hasError('required')) { + + warning + + } @else if (refDynamicSourceFormGroup.get('maxLevel').touched && refDynamicSourceFormGroup.get('maxLevel').hasError('min')) { + + warning + + } @else if (refDynamicSourceFormGroup.get('maxLevel').touched && refDynamicSourceFormGroup.get('maxLevel').hasError('max')) { + + warning + + } + +
+
+ + {{ 'calculated-fields.fetch-last-available-level' | translate }} + +
+
+
+
+ +
+
+ {{ 'calculated-fields.perimeter-attribute-key' | translate }} +
+ +
+
+
{{ '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/panel/calculated-field-geofencing-zone-groups-panel.component.scss b/ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-geofencing-zone-groups-panel.component.scss new file mode 100644 index 0000000000..bedaf2eeb0 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-geofencing-zone-groups-panel.component.scss @@ -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. + */ +@import '../../../../../../../scss/constants'; + +$panel-width: 520px; + +:host { + display: flex; + width: $panel-width; + max-width: 100%; + max-height: 80vh; + + .fixed-title-width { + @media #{$mat-xs} { + min-width: 120px; + } + } + + .limit-field-row { + @media screen and (max-width: $panel-width) { + display: flex; + flex-direction: column; + + .fixed-title-width { + align-self: flex-start; + padding-top: 8px; + } + } + } +} + +:host ::ng-deep { + .time-interval-field { + .advanced-input { + flex-direction: column; + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-geofencing-zone-groups-panel.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-geofencing-zone-groups-panel.component.ts new file mode 100644 index 0000000000..72ea58919f --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-geofencing-zone-groups-panel.component.ts @@ -0,0 +1,291 @@ +/// +/// 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 { FormBuilder, FormControl, FormGroup, ValidatorFn, Validators } from '@angular/forms'; +import { charsWithNumRegex, oneSpaceInsideRegex } from '@shared/models/regex.constants'; +import { + ArgumentEntityType, + ArgumentEntityTypeParamsMap, + ArgumentEntityTypeTranslations, + CalculatedFieldGeofencing, + CalculatedFieldGeofencingValue, + CalculatedFieldType, + GeofencingDirectionTranslations, + GeofencingReportStrategy, + GeofencingReportStrategyTranslations, + getCalculatedFieldCurrentEntityFilter +} from '@shared/models/calculated-field.models'; +import { debounceTime, delay, distinctUntilChanged, filter, map } from 'rxjs/operators'; +import { EntityType } from '@shared/models/entity-type.models'; +import { 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'; + +@Component({ + selector: 'tb-calculated-field-geofencing-zone-groups-panel', + templateUrl: './calculated-field-geofencing-zone-groups-panel.component.html', + styleUrls: ['./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() calculatedFieldType: CalculatedFieldType; + @Input() usedNames: string[]; + + @ViewChild('entityAutocomplete') entityAutocomplete: EntityAutocompleteComponent; + + geofencingDataApplied = output(); + + readonly maxRelationLevelPerCfArgument = getCurrentAuthState(this.store).maxRelationLevelPerCfArgument; + + geofencingFormGroup = this.fb.group({ + name: ['', [Validators.required, this.uniqNameRequired(), this.forbiddenNameValidator(), Validators.pattern(charsWithNumRegex), Validators.maxLength(255)]], + refEntityId: this.fb.group({ + entityType: [ArgumentEntityType.Current], + id: [''] + }), + refDynamicSourceConfiguration: this.fb.group({ + direction: [EntitySearchDirection.TO], + relationType: ['', [Validators.required]], + maxLevel: [1, [Validators.required, Validators.min(1), Validators.max(this.maxRelationLevelPerCfArgument)]], + fetchLastLevelOnly: [false], + }), + perimeterKeyName: ['', [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; + + private currentEntityFilter: EntityFilter; + + constructor( + private fb: FormBuilder, + private cd: ChangeDetectorRef, + private popover: TbPopoverComponent, + private store: Store + ) { + + this.observeMaxLevelChanges(); + this.observeEntityFilterChanges(); + this.observeEntityTypeChanges(); + this.observeUpdatePosition(); + 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.geofencingFormGroup.patchValue(this.zone, {emitEvent: false}); + if (this.zone.refDynamicSourceConfiguration?.type) { + this.refEntityIdFormGroup.get('entityType').setValue(this.zone.refDynamicSourceConfiguration.type, {emitEvent: false}); + } + this.validateFetchLastLevelOnly(this.zone?.refDynamicSourceConfiguration?.maxLevel); + 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 observeMaxLevelChanges(): void { + this.refDynamicSourceFormGroup.get('maxLevel').valueChanges + .pipe(takeUntilDestroyed()) + .subscribe(value => this.validateFetchLastLevelOnly(value)); + } + + private observeCreateRelationZonesChanges(): void { + this.geofencingFormGroup.get('createRelationsWithMatchedZones').valueChanges + .pipe(takeUntilDestroyed()) + .subscribe(value => this.validateDirectionAndRelationType(value)); + } + + private validateFetchLastLevelOnly(maxLevel = 1): void { + if (maxLevel > 1) { + this.refDynamicSourceFormGroup.get('fetchLastLevelOnly').enable({emitEvent: false}); + } else { + this.refDynamicSourceFormGroup.get('fetchLastLevelOnly').disable({emitEvent: false}); + } + } + + 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.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.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(filter(Boolean)), + ) + .pipe(debounceTime(50), takeUntilDestroyed()) + .subscribe(() => this.updateEntityFilter(this.entityType)); + } + + private observeEntityTypeChanges(): void { + this.refEntityIdFormGroup.get('entityType').valueChanges + .pipe(distinctUntilChanged(), takeUntilDestroyed()) + .subscribe(type => { + this.geofencingFormGroup.get('refEntityId').get('id').setValue(''); + 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 uniqNameRequired(): ValidatorFn { + return (control: FormControl) => { + const newName = control.value.trim().toLowerCase(); + const isDuplicate = this.usedNames?.some(name => name.toLowerCase() === newName); + + return isDuplicate ? { duplicateName: true } : null; + }; + } + + private forbiddenNameValidator(): ValidatorFn { + return (control: FormControl) => { + const trimmedValue = control.value.trim().toLowerCase(); + const forbiddenNames = ['ctx', 'e', 'pi']; + return forbiddenNames.includes(trimmedValue) ? { forbiddenName: true } : null; + }; + } + + private observeUpdatePosition(): void { + merge( + this.refEntityIdFormGroup.get('entityType').valueChanges, + this.geofencingFormGroup.get('createRelationsWithMatchedZones').valueChanges + ) + .pipe(delay(50), takeUntilDestroyed()) + .subscribe(() => this.popover.updatePosition()); + } + +} 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..0099fa83a7 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'; @@ -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 107d5c2686..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,72 +246,65 @@ export class ManageDashboardStatesDialogComponent } duplicateState($event: Event, state: DashboardStateInfo) { - const originalState = state; - const newStateName = this.getNextDuplicatedName(state.name); - if (newStateName) { - const duplicatedStates = deepClone(originalState); - const duplicatedWidgets = deepClone(this.widgets); + 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 duplicatedState = deepClone(state); const mainWidgets = {}; const rightWidgets = {}; - duplicatedStates.id = newStateName.toLowerCase().replace(/\W/g, '_'); - 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/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 22dfb34a64..25bb1d9700 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 @@ -15,8 +15,8 @@ limitations under the License. --> -
- +
+ alias.filter-type @@ -30,18 +30,21 @@
@@ -49,12 +52,13 @@ - + entity.name-starts-with help @@ -66,6 +70,7 @@ - + alias.state-entity-parameter-name -
- - +
{{ 'alias.default-state-entity' | translate }}
+ @@ -88,10 +93,13 @@ - + asset.name-starts-with help @@ -100,10 +108,13 @@ - + device.name-starts-with help @@ -112,10 +123,13 @@ - + entity-view.name-starts-with help @@ -124,36 +138,51 @@ - + edge.name-starts-with
-
+
alias.root-entity
- + {{ 'alias.root-state-entity' | translate }} -
- - alias.state-entity-parameter-name - - - + @if (filterFormGroup.get('rootStateEntity').value) { +
+ + alias.state-entity-parameter-name + + +
+
{{ 'alias.default-state-entity' | translate }}
+ + +
+
+ } @else { + -
- - -
- + } +
+
+
alias.query-options
+
+ relation.direction @@ -161,7 +190,7 @@ - + alias.max-relation-level
+ class="mat-slide" formControlName="fetchLastLevelOnly"> {{ 'alias.last-level-relation' | translate }}
@@ -191,98 +220,113 @@ entityFilterFormGroup.get('type').value === aliasFilterType.edgeSearchQuery || entityFilterFormGroup.get('type').value === aliasFilterType.entityViewSearchQuery ? entityFilterFormGroup.get('type').value : ''"> - -
- - - -
-
- - -
-
- - alias.state-entity-parameter-name - - -
- - - +
+
+
alias.root-entity
+ + {{ 'alias.root-state-entity' | translate }} + + @if (filterFormGroup.get('rootStateEntity').value) { +
+ + alias.state-entity-parameter-name + + +
+
{{ 'alias.default-state-entity' | translate }}
+ + +
+
+ } @else { + + + }
-
-
-
- +
+
alias.query-options
+
+ + relation.direction + + + {{ directionTypeTranslations.get(directionTypeEnum[type]) | translate }} + + + + + alias.max-relation-level + + +
+ + {{ 'alias.last-level-relation' | translate }} - -
-
-
- - relation.direction - - - {{ directionTypeTranslations.get(directionTypeEnum[type]) | translate }} - - - - - alias.max-relation-level - - -
-
relation.relation-type
- - - - -
asset.asset-types
- - -
- -
device.device-types
- - -
- -
edge.edge-types
- - -
- -
entity-view.entity-view-types
- - +
+
+
relation.relation-filter
+ + + + + + + + + + + + + + + + + + + + +
+
+
diff --git a/ui-ngx/src/app/modules/home/components/entity/entity-filter.component.scss b/ui-ngx/src/app/modules/home/components/entity/entity-filter.component.scss index 686a55eb35..89e816eb35 100644 --- a/ui-ngx/src/app/modules/home/components/entity/entity-filter.component.scss +++ b/ui-ngx/src/app/modules/home/components/entity/entity-filter.component.scss @@ -15,25 +15,7 @@ */ :host { .tb-entity-filter { - #relationsQueryFilter { - padding-top: 20px; - - tb-entity-select { - min-height: 92px; - } - } - - .tb-root-state-entity-switch { - padding-left: 10px; - padding-bottom: 10px; - - .root-state-entity-switch { - margin: 0; - } - - .root-state-entity-label { - margin: 5px 0 5px 10px; - } - } + display: flex; + flex-direction: column; } } diff --git a/ui-ngx/src/app/modules/home/components/home-components.module.ts b/ui-ngx/src/app/modules/home/components/home-components.module.ts index ac0296e2a5..7298038bd0 100644 --- a/ui-ngx/src/app/modules/home/components/home-components.module.ts +++ b/ui-ngx/src/app/modules/home/components/home-components.module.ts @@ -203,6 +203,14 @@ import { import { CalculatedFieldTestArgumentsComponent } from '@home/components/calculated-fields/components/test-arguments/calculated-field-test-arguments.component'; +import { CheckConnectivityDialogComponent } from '@home/components/ai-model/check-connectivity-dialog.component'; +import { AIModelDialogComponent } from '@home/components/ai-model/ai-model-dialog.component'; +import { + CalculatedFieldGeofencingZoneGroupsTableComponent +} from '@home/components/calculated-fields/components/geofencing-zone-grups-table/calculated-field-geofencing-zone-groups-table.component'; +import { + CalculatedFieldGeofencingZoneGroupsPanelComponent +} from '@home/components/calculated-fields/components/panel/calculated-field-geofencing-zone-groups-panel.component'; @NgModule({ declarations: @@ -354,6 +362,10 @@ import { CalculatedFieldDebugDialogComponent, CalculatedFieldScriptTestDialogComponent, CalculatedFieldTestArgumentsComponent, + CalculatedFieldGeofencingZoneGroupsTableComponent, + CalculatedFieldGeofencingZoneGroupsPanelComponent, + CheckConnectivityDialogComponent, + AIModelDialogComponent, ], imports: [ CommonModule, @@ -499,6 +511,10 @@ import { CalculatedFieldDebugDialogComponent, CalculatedFieldScriptTestDialogComponent, CalculatedFieldTestArgumentsComponent, + CalculatedFieldGeofencingZoneGroupsTableComponent, + CalculatedFieldGeofencingZoneGroupsPanelComponent, + CheckConnectivityDialogComponent, + AIModelDialogComponent, ], providers: [ WidgetComponentService, diff --git a/ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.html b/ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.html index 3c5c8774d5..1540a3c87f 100644 --- a/ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.html @@ -275,6 +275,34 @@
+ + tenant-profile.max-related-level-per-argument + + + {{ 'tenant-profile.max-related-level-per-argument-required' | translate}} + + + {{ 'tenant-profile.max-related-level-per-argument-range' | translate}} + + + +
+
+ + tenant-profile.min-allowed-scheduled-update-interval + + + {{ 'tenant-profile.min-allowed-scheduled-update-interval-required' | translate}} + + + {{ 'tenant-profile.min-allowed-scheduled-update-interval-range' | 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..0dd1453648 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 @@ -124,6 +124,8 @@ export class DefaultTenantProfileConfigurationComponent implements ControlValueA edgeUplinkMessagesRateLimitsPerEdge: [null, []], maxCalculatedFieldsPerEntity: [null, [Validators.required, Validators.min(0)]], maxArgumentsPerCF: [null, [Validators.required, Validators.min(0)]], + maxRelationLevelPerCfArgument: [null, [Validators.required, Validators.min(1)]], + minAllowedScheduledUpdateIntervalInSecForCF: [null, [Validators.required, Validators.min(0)]], maxDataPointsPerRollingArg: [null, [Validators.required, Validators.min(0)]], maxStateSizeInKBytes: [null, [Validators.required, Validators.min(0)]], calculatedFieldDebugEventsRateLimit: [null, []], 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/rule-node/action/generator-config.component.scss b/ui-ngx/src/app/modules/home/components/rule-node/action/generator-config.component.scss index 18c2498a64..b3e579787b 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/action/generator-config.component.scss +++ b/ui-ngx/src/app/modules/home/components/rule-node/action/generator-config.component.scss @@ -31,21 +31,5 @@ } } } - .tb-entity-select { - @media screen and (min-width: 599px) { - display: flex; - flex-direction: row; - gap: 16px; - } - tb-entity-type-select { - flex: 1; - } - tb-entity-autocomplete { - flex: 1; - mat-form-field { - width: 100% !important; - } - } - } } } diff --git a/ui-ngx/src/app/modules/home/components/rule-node/common/arguments-map-config.component.ts b/ui-ngx/src/app/modules/home/components/rule-node/common/arguments-map-config.component.ts index 0cf67f9344..f60b8b0f97 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/common/arguments-map-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/rule-node/common/arguments-map-config.component.ts @@ -194,7 +194,7 @@ export class ArgumentsMapConfigComponent extends PageComponent implements Contro key: [property?.key, [Validators.required]], name: [ArgumentName[index], [Validators.required]], attributeScope: [property?.attributeScope ?? AttributeScope.SERVER_SCOPE, [Validators.required]], - defaultValue: [property?.defaultValue ? property?.defaultValue : null] + defaultValue: [property?.defaultValue ?? null] }); this.updateArgumentControlValidators(argumentControl); argumentControl.get('type').valueChanges.pipe( 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/common/time-unit-input.component.html b/ui-ngx/src/app/modules/home/components/rule-node/common/time-unit-input.component.html deleted file mode 100644 index 37b0a2983c..0000000000 --- a/ui-ngx/src/app/modules/home/components/rule-node/common/time-unit-input.component.html +++ /dev/null @@ -1,44 +0,0 @@ - -
- - {{ labelText }} - -
- -
- - - {{ requiredText }} - - - {{ minErrorText }} - - - {{ maxErrorText }} - -
- - rule-node-config.units - - @for (timeUnit of timeUnits; track timeUnit) { - {{ timeUnitTranslations.get(timeUnit) | translate }} - } - - -
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 new file mode 100644 index 0000000000..5966448e9f --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/rule-node/external/ai-config.component.html @@ -0,0 +1,134 @@ + +
+
+
+ {{ 'rule-node-config.ai.ai-model' | translate }} +
+
+ + +
+
+ +
+ + + + {{'rule-node-config.ai.prompt-settings' | translate}} + + +
+ + + + rule-node-config.ai.system-prompt + + + {{ 'rule-node-config.ai.system-prompt-max-length' | translate }} + + + {{ 'rule-node-config.ai.system-prompt-blank' | translate }} + + + + rule-node-config.ai.user-prompt + + + {{ 'rule-node-config.ai.user-prompt-required' | translate }} + + + {{ 'rule-node-config.ai.user-prompt-max-length' | translate }} + + + {{ 'rule-node-config.ai.user-prompt-blank' | translate }} + + +
+
+
+ +
+
+
+ {{ '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 }} + +
+ + + +
+ +
+ + + rule-node-config.ai.advanced-settings + + +
+
+
{{ 'rule-node-config.ai.timeout' | translate }}
+
+ + +
+
+
+ + {{ 'rule-node-config.ai.force-acknowledgement' | translate }} + +
+
+
+
+
+
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 new file mode 100644 index 0000000000..e4b21b3f18 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/rule-node/external/ai-config.component.ts @@ -0,0 +1,121 @@ +/// +/// 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 } from '@angular/core'; +import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; +import { RuleNodeConfiguration, RuleNodeConfigurationComponent } from '@shared/models/rule-node.models'; +import { EntityType } from '@shared/models/entity-type.models'; +import { MatDialog } from '@angular/material/dialog'; +import { AIModelDialogComponent, AIModelDialogData } from '@home/components/ai-model/ai-model-dialog.component'; +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'; + +@Component({ + selector: 'tb-external-node-ai-config', + templateUrl: './ai-config.component.html', + styleUrls: [] +}) +export class AiConfigComponent extends RuleNodeConfigurationComponent { + + aiConfigForm: UntypedFormGroup; + + entityType = EntityType; + + responseFormat = ResponseFormat; + + constructor(private fb: UntypedFormBuilder, + private translate: TranslateService, + private dialog: MatDialog) { + super(); + } + + protected configForm(): UntypedFormGroup { + return this.aiConfigForm; + } + + protected onConfigurationSet(configuration: RuleNodeConfiguration) { + this.aiConfigForm = this.fb.group({ + modelId: [configuration?.modelId ?? null, [Validators.required]], + systemPrompt: [configuration?.systemPrompt ?? '', [Validators.maxLength(500_000), Validators.pattern(/.*\S.*/)]], + userPrompt: [configuration?.userPrompt ?? '', [Validators.required, Validators.maxLength(500_000), Validators.pattern(/.*\S.*/)]], + responseFormat: this.fb.group({ + type: [configuration?.responseFormat?.type ?? ResponseFormat.JSON, []], + schema: [configuration?.responseFormat?.schema ?? null, [jsonRequired]], + }), + timeoutSeconds: [configuration?.timeoutSeconds ?? 60, []], + forceAck: [configuration?.forceAck ?? true, []] + }); + } + + protected validatorTriggers(): string[] { + return ['responseFormat.type']; + } + + protected updateValidators(emitEvent: boolean) { + if (this.aiConfigForm.get('responseFormat.type').value === ResponseFormat.JSON_SCHEMA) { + this.aiConfigForm.get('responseFormat.schema').enable({emitEvent: false}); + } else { + this.aiConfigForm.get('responseFormat.schema').disable({emitEvent: false}); + } + } + + protected prepareOutputConfig(): RuleNodeConfiguration { + const config = this.configForm().getRawValue(); + if (!this.aiConfigForm.get('systemPrompt').value) { + delete config.systemPrompt; + } + if (this.aiConfigForm.get('responseFormat.type').value !== ResponseFormat.JSON_SCHEMA) { + delete config.responseFormat.schema; + } + return deepTrim(config); + } + + onEntityChange($event: AiModel) { + if ($event) { + if (AiRuleNodeResponseFormatTypeOnlyText.includes($event.configuration.provider)) { + if (this.aiConfigForm.get('responseFormat.type').value !== ResponseFormat.TEXT) { + this.aiConfigForm.get('responseFormat.type').patchValue(ResponseFormat.TEXT, {emitEvent: true}); + } + this.aiConfigForm.get('responseFormat.type').disable({emitEvent: false}); + } + } else { + this.aiConfigForm.get('responseFormat.type').enable({emitEvent: false}); + } + } + + get getResponseFormatHint() { + return this.translate.instant(`rule-node-config.ai.response-format-hint-${this.aiConfigForm.get('responseFormat.type').value}`); + } + + createModelAi(name: string, formControl: string) { + this.dialog.open(AIModelDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + isAdd: true, + name + } + }).afterClosed() + .subscribe((model) => { + if (model) { + this.aiConfigForm.get(formControl).patchValue(model.id); + this.aiConfigForm.get(formControl).markAsDirty(); + } + }); + } +} diff --git a/ui-ngx/src/app/modules/home/components/rule-node/external/azure-iot-hub-config.component.html b/ui-ngx/src/app/modules/home/components/rule-node/external/azure-iot-hub-config.component.html index 79c4a2f00d..bab0c6b196 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/external/azure-iot-hub-config.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/external/azure-iot-hub-config.component.html @@ -38,7 +38,9 @@ {{ 'rule-node-config.device-id-required' | translate }}
- + + diff --git a/ui-ngx/src/app/modules/home/components/rule-node/external/azure-iot-hub-config.component.ts b/ui-ngx/src/app/modules/home/components/rule-node/external/azure-iot-hub-config.component.ts index 7dd934ccd9..dff5f06e54 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/external/azure-iot-hub-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/rule-node/external/azure-iot-hub-config.component.ts @@ -22,6 +22,7 @@ import { azureIotHubCredentialsTypes, azureIotHubCredentialsTypeTranslations } from '@home/components/rule-node/rule-node-config.models'; +import { MqttVersion } from '@shared/models/mqtt.models'; @Component({ selector: 'tb-external-node-azure-iot-hub-config', @@ -34,6 +35,7 @@ export class AzureIotHubConfigComponent extends RuleNodeConfigurationComponent { allAzureIotHubCredentialsTypes = azureIotHubCredentialsTypes; azureIotHubCredentialsTypeTranslationsMap = azureIotHubCredentialsTypeTranslations; + MqttVersion = MqttVersion; constructor(private fb: UntypedFormBuilder) { super(); diff --git a/ui-ngx/src/app/modules/home/components/rule-node/external/external-rule-node-config.module.ts b/ui-ngx/src/app/modules/home/components/rule-node/external/external-rule-node-config.module.ts index d2ed9c4f26..955c555989 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/external/external-rule-node-config.module.ts +++ b/ui-ngx/src/app/modules/home/components/rule-node/external/external-rule-node-config.module.ts @@ -32,6 +32,7 @@ import { HomeComponentsModule } from '@home/components/public-api'; import { CommonRuleNodeConfigModule } from '../common/common-rule-node-config.module'; import { SlackConfigComponent } from './slack-config.component'; import { LambdaConfigComponent } from './lambda-config.component'; +import { AiConfigComponent } from '@home/components/rule-node/external/ai-config.component'; @NgModule({ declarations: [ @@ -47,7 +48,8 @@ import { LambdaConfigComponent } from './lambda-config.component'; SendEmailConfigComponent, AzureIotHubConfigComponent, SendSmsConfigComponent, - SlackConfigComponent + SlackConfigComponent, + AiConfigComponent ], imports: [ CommonModule, @@ -68,7 +70,8 @@ import { LambdaConfigComponent } from './lambda-config.component'; SendEmailConfigComponent, AzureIotHubConfigComponent, SendSmsConfigComponent, - SlackConfigComponent + SlackConfigComponent, + AiConfigComponent ] }) export class ExternalRuleNodeConfigModule { 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/version-control.scss b/ui-ngx/src/app/modules/home/components/vc/version-control.scss index 35a0932f19..f221fc66ad 100644 --- a/ui-ngx/src/app/modules/home/components/vc/version-control.scss +++ b/ui-ngx/src/app/modules/home/components/vc/version-control.scss @@ -16,9 +16,11 @@ :host { .vc-result-message { max-width: 65vw; + max-height: 65vh; padding: 0 8px; text-align: center; word-wrap: break-word; + overflow: auto; &:first-child { padding-top: 48px; } diff --git a/ui-ngx/src/app/modules/home/components/widget/config/datasource.component.html b/ui-ngx/src/app/modules/home/components/widget/config/datasource.component.html index 1f48728166..d56fb4aaa7 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/datasource.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/datasource.component.html @@ -15,8 +15,8 @@ limitations under the License. --> -
- +
+ widget-config.datasource-type @@ -44,18 +44,20 @@ + class="flex-1" appearance="outline"> @@ -64,6 +66,7 @@
- +
+
+
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..7af2711e8f --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/api-usage-settings.component.models.ts @@ -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. +/// + +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; + dataKeys: 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: '', + dataKeys: [ + 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' +}; + 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..16fca5a94b --- /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 +} 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('dataKeys', this.prepareDataKeysFormArray(settings?.dataKeys), {emitEvent: false}); + } + + dataKeysFormArray(): UntypedFormArray { + return this.apiUsageWidgetSettingsForm.get('dataKeys') 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('dataKeys') as UntypedFormArray).removeAt(index); + } + + addDataKey() { + const dataKey = { + label: '', + state: '', + status: null, + maxLimit: null, + current: null + }; + const dataKeysArray = this.apiUsageWidgetSettingsForm.get('dataKeys') 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, + dataKeys: settings?.dataKeys, + targetDashboardState: settings?.targetDashboardState, + background: settings?.background, + padding: settings.padding + }; + } + + protected onSettingsSet(settings: WidgetSettings) { + this.apiUsageWidgetSettingsForm = this.fb.group({ + dsEntityAliasId: [settings?.dsEntityAliasId], + dataKeys: this.prepareDataKeysFormArray(settings?.dataKeys), + 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/common/action/custom-action-pretty-editor.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/custom-action-pretty-editor.component.html index 3d2e69e210..3b2c14c539 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/custom-action-pretty-editor.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/custom-action-pretty-editor.component.html @@ -43,7 +43,7 @@
-
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/custom-action-pretty-editor.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/custom-action-pretty-editor.component.scss index 002ccbeeb3..f0e09589ea 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/custom-action-pretty-editor.component.scss +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/custom-action-pretty-editor.component.scss @@ -19,25 +19,66 @@ padding: 8px; background-color: #fff; + .css-panel, .html-panel{ + border-color: #c0c0c0; + border-width: 0 1px 1px 1px; + border-style: solid; + } + + .tb-js-func-toolbar{ + padding: 0 3px; + } + + .tb-js-func { + &:not(.tb-fullscreen) { + &.tb-hide-brackets { + padding-bottom: 0; + } + } + } + .tb-fullscreen-panel { .tb-custom-action-editor-container { + height: 100%; + } + + .css-panel, .html-panel{ + border: none; + } + + .left-panel, .right-panel{ height: calc(100% - 40px); } .right-panel { - padding-top: 8px; - padding-left: 3px; + padding: 8px 0 0; } - tb-js-func .tb-js-func-panel { - box-sizing: border-box; + .tb-js-func { + .tb-js-func-panel { + box-sizing: border-box; + } + &.fill-height { + .tb-js-func-toolbar{ + padding: 0 5px; + } + + &.tb-hide-brackets { + .tb-js-func-panel { + border: none; + border-top: 1px solid #c0c0c0; + } + } + } } .mat-mdc-tab-group { .mat-mdc-tab-body-wrapper { height: 100%; + .mat-mdc-tab-body { height: 100%; + & > div { height: 100%; } @@ -70,7 +111,7 @@ .tb-split.tb-split-horizontal, .gutter.gutter-horizontal { float: left; - height: 100%; + height: calc(100% - 40px); } .tb-action-expand-button { @@ -81,20 +122,7 @@ &.tb-fullscreen-editor { position: relative; right: 0; - /* .mat-mdc-button { - .mat-icon { - margin-right: 5px; - } - } */ } - - /* .mat-mdc-button { - min-width: 36px; - padding: 0; - .mat-icon { - margin-right: 0; - } - } */ } .tb-custom-action-editor { @@ -105,3 +133,4 @@ } + 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/alias/entity-alias-select.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/alias/entity-alias-select.component.html index f2a2ee4b9c..882e27b01b 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/alias/entity-alias-select.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/alias/entity-alias-select.component.html @@ -74,8 +74,7 @@
- {{ translate.get('entity.no-alias-matching', - {alias: truncate.transform(searchText, true, 6, '...')}) | async }} + {{ 'entity.no-alias-matching' | translate : {alias: (searchText | truncate: true: 6: '...')} }} 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 2d170f55e3..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 @@ -14,7 +14,7 @@ /// limitations under the License. /// -import { Component, ElementRef, forwardRef, Input, OnInit, SkipSelf, ViewChild } from '@angular/core'; +import { Component, DestroyRef, ElementRef, forwardRef, Input, OnInit, SkipSelf, ViewChild } from '@angular/core'; import { ControlValueAccessor, FormBuilder, @@ -26,18 +26,17 @@ import { } from '@angular/forms'; import { Observable, of } from 'rxjs'; import { map, mergeMap, share, tap } from 'rxjs/operators'; -import { TranslateService } from '@ngx-translate/core'; import { EntityType } from '@shared/models/entity-type.models'; import { EntityService } from '@core/http/entity.service'; import { coerceBoolean } from '@shared/decorators/coercion'; import { EntityAlias } from '@shared/models/alias.models'; import { IAliasController } from '@core/api/widget-api.models'; -import { TruncatePipe } from '@shared/pipe/truncate.pipe'; -import { MatAutocomplete, MatAutocompleteTrigger } from '@angular/material/autocomplete'; +import { MatAutocomplete } from '@angular/material/autocomplete'; import { EntityAliasSelectCallbacks } from './entity-alias-select.component.models'; import { ENTER } from '@angular/cdk/keycodes'; import { ErrorStateMatcher } from '@angular/material/core'; import { MatFormFieldAppearance, SubscriptSizing } from '@angular/material/form-field'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @Component({ selector: 'tb-entity-alias-select', @@ -47,11 +46,7 @@ import { MatFormFieldAppearance, SubscriptSizing } from '@angular/material/form- provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => EntityAliasSelectComponent), multi: true - }/*, - { - provide: ErrorStateMatcher, - useExisting: EntityAliasSelectComponent - }*/] + }] }) export class EntityAliasSelectComponent implements ControlValueAccessor, OnInit, ErrorStateMatcher { @@ -72,7 +67,6 @@ export class EntityAliasSelectComponent implements ControlValueAccessor, OnInit, showLabel: boolean; @ViewChild('entityAliasAutocomplete') entityAliasAutocomplete: MatAutocomplete; - @ViewChild('autocomplete', { read: MatAutocompleteTrigger }) autoCompleteTrigger: MatAutocompleteTrigger; @Input() @coerceBoolean() @@ -93,21 +87,18 @@ export class EntityAliasSelectComponent implements ControlValueAccessor, OnInit, @ViewChild('entityAliasInput', {static: true}) entityAliasInput: ElementRef; - entityAliasList: Array = []; - filteredEntityAliases: Observable>; searchText = ''; private dirty = false; - + private entityAliasList: Array = []; private propagateChange = (_v: any) => { }; constructor(@SkipSelf() private errorStateMatcher: ErrorStateMatcher, private entityService: EntityService, - public translate: TranslateService, - public truncate: TruncatePipe, - private fb: FormBuilder) { + private fb: FormBuilder, + private destroyRef: DestroyRef) { this.selectEntityAliasFormGroup = this.fb.group({ entityAlias: [null] }); @@ -121,15 +112,7 @@ export class EntityAliasSelectComponent implements ControlValueAccessor, OnInit, } ngOnInit() { - const entityAliases = this.aliasController.getEntityAliases(); - for (const aliasId of Object.keys(entityAliases)) { - if (this.allowedEntityTypes && this.allowedEntityTypes.length) { - if (!this.entityService.filterAliasByEntityTypes(entityAliases[aliasId], this.allowedEntityTypes)) { - continue; - } - } - this.entityAliasList.push(entityAliases[aliasId]); - } + this.loadEntityAliases(); this.filteredEntityAliases = this.selectEntityAliasFormGroup.get('entityAlias').valueChanges .pipe( @@ -149,6 +132,12 @@ export class EntityAliasSelectComponent implements ControlValueAccessor, OnInit, mergeMap(name => this.fetchEntityAliases(name) ), share() ); + + this.aliasController.entityAliasesChanged.pipe( + takeUntilDestroyed(this.destroyRef), + ).subscribe(() => { + this.loadEntityAliases(); + }); } isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean { @@ -262,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); @@ -271,4 +259,18 @@ export class EntityAliasSelectComponent implements ControlValueAccessor, OnInit, ); } } + + private loadEntityAliases(): void { + this.entityAliasList = []; + const entityAliases = this.aliasController.getEntityAliases(); + for (const aliasId of Object.keys(entityAliases)) { + if (this.allowedEntityTypes?.length) { + if (!this.entityService.filterAliasByEntityTypes(entityAliases[aliasId], this.allowedEntityTypes)) { + continue; + } + } + this.entityAliasList.push(entityAliases[aliasId]); + } + this.dirty = true; + } } 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/filter/filter-select.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/filter/filter-select.component.html index ff1bcb06a4..7b11058228 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/filter/filter-select.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/filter/filter-select.component.html @@ -66,8 +66,7 @@
- {{ translate.get('filter.no-filter-matching', - {filter: truncate.transform(searchText, true, 6, '...')}) | async }} + {{ 'filter.no-filter-matching' | translate : {filter: (searchText | truncate: true: 6: '...')} }} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/filter/filter-select.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/filter/filter-select.component.ts index 6484f2ca18..abc6f77e31 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/filter/filter-select.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/filter/filter-select.component.ts @@ -14,31 +14,27 @@ /// limitations under the License. /// -import { AfterViewInit, Component, ElementRef, forwardRef, Input, OnInit, SkipSelf, ViewChild } from '@angular/core'; +import { Component, DestroyRef, ElementRef, forwardRef, Input, OnInit, SkipSelf, ViewChild } from '@angular/core'; import { ControlValueAccessor, - UntypedFormBuilder, - UntypedFormControl, - UntypedFormGroup, FormGroupDirective, NG_VALUE_ACCESSOR, - NgForm + NgForm, + UntypedFormBuilder, + UntypedFormControl, + UntypedFormGroup } from '@angular/forms'; import { Observable, of } from 'rxjs'; import { map, mergeMap, share, tap } from 'rxjs/operators'; -import { Store } from '@ngrx/store'; -import { AppState } from '@app/core/core.state'; -import { TranslateService } from '@ngx-translate/core'; -import { coerceBooleanProperty } from '@angular/cdk/coercion'; import { IAliasController } from '@core/api/widget-api.models'; -import { TruncatePipe } from '@shared/pipe/truncate.pipe'; -import { MatAutocomplete, MatAutocompleteTrigger } from '@angular/material/autocomplete'; +import { MatAutocomplete } from '@angular/material/autocomplete'; import { ENTER } from '@angular/cdk/keycodes'; import { ErrorStateMatcher } from '@angular/material/core'; import { FilterSelectCallbacks } from './filter-select.component.models'; import { Filter } from '@shared/models/query/query.models'; import { coerceBoolean } from '@shared/decorators/coercion'; import { MatFormFieldAppearance, SubscriptSizing } from '@angular/material/form-field'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @Component({ selector: 'tb-filter-select', @@ -54,7 +50,7 @@ import { MatFormFieldAppearance, SubscriptSizing } from '@angular/material/form- useExisting: FilterSelectComponent }] }) -export class FilterSelectComponent implements ControlValueAccessor, OnInit, AfterViewInit, ErrorStateMatcher { +export class FilterSelectComponent implements ControlValueAccessor, OnInit, ErrorStateMatcher { selectFilterFormGroup: UntypedFormGroup; @@ -81,40 +77,27 @@ export class FilterSelectComponent implements ControlValueAccessor, OnInit, Afte subscriptSizing: SubscriptSizing = 'fixed'; @ViewChild('filterAutocomplete') filterAutocomplete: MatAutocomplete; - @ViewChild('autocomplete', { read: MatAutocompleteTrigger }) autoCompleteTrigger: MatAutocompleteTrigger; - - private requiredValue: boolean; - get tbRequired(): boolean { - return this.requiredValue; - } @Input() - set tbRequired(value: boolean) { - this.requiredValue = coerceBooleanProperty(value); - } + @coerceBoolean() + tbRequired: boolean; @Input() disabled: boolean; @ViewChild('filterInput', {static: true}) filterInput: ElementRef; - filterList: Array = []; - filteredFilters: Observable>; searchText = ''; private dirty = false; + private filterList: Array = []; + private propagateChange = (_v: any) => { }; - private creatingFilter = false; - - private propagateChange = (v: any) => { }; - - constructor(private store: Store, - @SkipSelf() private errorStateMatcher: ErrorStateMatcher, - public translate: TranslateService, - public truncate: TruncatePipe, - private fb: UntypedFormBuilder) { + constructor(@SkipSelf() private errorStateMatcher: ErrorStateMatcher, + private fb: UntypedFormBuilder, + private destroyRef: DestroyRef) { this.selectFilterFormGroup = this.fb.group({ filter: [null] }); @@ -124,19 +107,16 @@ export class FilterSelectComponent implements ControlValueAccessor, OnInit, Afte this.propagateChange = fn; } - registerOnTouched(fn: any): void { + registerOnTouched(_fn: any): void { } ngOnInit() { - const filters = this.aliasController.getFilters(); - for (const filterId of Object.keys(filters)) { - this.filterList.push(filters[filterId]); - } + this.loadFilters(); this.filteredFilters = this.selectFilterFormGroup.get('filter').valueChanges .pipe( tap(value => { - let modelValue; + let modelValue: Filter; if (typeof value === 'string' || !value) { modelValue = null; } else { @@ -151,6 +131,12 @@ export class FilterSelectComponent implements ControlValueAccessor, OnInit, Afte mergeMap(name => this.fetchFilters(name) ), share() ); + + this.aliasController.filtersChanged.pipe( + takeUntilDestroyed(this.destroyRef), + ).subscribe(() => { + this.loadFilters(); + }); } isErrorState(control: UntypedFormControl | null, form: FormGroupDirective | NgForm | null): boolean { @@ -159,8 +145,6 @@ export class FilterSelectComponent implements ControlValueAccessor, OnInit, Afte return originalErrorState || customErrorState; } - ngAfterViewInit(): void {} - setDisabledState(isDisabled: boolean): void { this.disabled = isDisabled; if (this.disabled) { @@ -227,7 +211,7 @@ export class FilterSelectComponent implements ControlValueAccessor, OnInit, Afte } textIsNotEmpty(text: string): boolean { - return (text && text != null && text.length > 0) ? true : false; + return text?.length > 0; } filterEnter($event: KeyboardEvent) { @@ -242,7 +226,6 @@ export class FilterSelectComponent implements ControlValueAccessor, OnInit, Afte createFilter($event: Event, filter: string, focusOnCancel = true) { $event.preventDefault(); $event.stopPropagation(); - this.creatingFilter = true; if (this.callbacks && this.callbacks.createFilter) { this.callbacks.createFilter(filter).subscribe((newFilter) => { if (!newFilter) { @@ -253,7 +236,6 @@ export class FilterSelectComponent implements ControlValueAccessor, OnInit, Afte }, 0); } } else { - this.filterList.push(newFilter); this.modelValue = newFilter.id; this.selectFilterFormGroup.get('filter').patchValue(newFilter, {emitEvent: true}); this.propagateChange(this.modelValue); @@ -262,4 +244,13 @@ export class FilterSelectComponent implements ControlValueAccessor, OnInit, Afte ); } } + + private loadFilters(): void { + this.filterList = []; + const filters = this.aliasController.getFilters(); + for (const filterId of Object.keys(filters)) { + this.filterList.push(filters[filterId]); + } + this.dirty = true; + } } 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/timeseries-table-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.html index 99ef5f7481..9f13f20331 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 @@ -78,7 +78,7 @@
diff --git a/ui-ngx/src/app/modules/home/pages/admin/oauth2/domains/domain.component.html b/ui-ngx/src/app/modules/home/pages/admin/oauth2/domains/domain.component.html index 4f37d8f480..9873ca29f7 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/oauth2/domains/domain.component.html +++ b/ui-ngx/src/app/modules/home/pages/admin/oauth2/domains/domain.component.html @@ -34,7 +34,7 @@
admin.oauth2.redirect-url-template - + resource ? diff --git a/ui-ngx/src/app/modules/home/pages/admin/resource/resource-library-tabs.component.html b/ui-ngx/src/app/modules/home/pages/admin/resource/resource-library-tabs.component.html new file mode 100644 index 0000000000..4effdaad53 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/admin/resource/resource-library-tabs.component.html @@ -0,0 +1,23 @@ + + + + diff --git a/ui-ngx/src/app/modules/home/pages/admin/resource/resource-library-tabs.component.ts b/ui-ngx/src/app/modules/home/pages/admin/resource/resource-library-tabs.component.ts new file mode 100644 index 0000000000..a85bf48db3 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/admin/resource/resource-library-tabs.component.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 { Component } from '@angular/core'; +import { EntityTabsComponent } from '@home/components/entity/entity-tabs.component'; +import { Resource } from '@shared/models/resource.models'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { NULL_UUID } from '@shared/models/id/has-uuid'; + +@Component({ + selector: 'tb-resource-library-tabs', + templateUrl: './resource-library-tabs.component.html', + styleUrls: [] +}) +export class ResourceLibraryTabsComponent extends EntityTabsComponent { + + readonly NULL_UUID = NULL_UUID; + + constructor(protected store: Store) { + super(store); + } +} diff --git a/ui-ngx/src/app/modules/home/pages/admin/resource/resources-library-table-config.resolve.ts b/ui-ngx/src/app/modules/home/pages/admin/resource/resources-library-table-config.resolve.ts index 91327b7465..f39ed9ff7b 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/resource/resources-library-table-config.resolve.ts +++ b/ui-ngx/src/app/modules/home/pages/admin/resource/resources-library-table-config.resolve.ts @@ -37,6 +37,7 @@ import { PageLink } from '@shared/models/page/page-link'; import { EntityAction } from '@home/models/entity/entity-component.models'; import { map } from 'rxjs/operators'; import { ResourcesTableHeaderComponent } from '@home/pages/admin/resource/resources-table-header.component'; +import { ResourceLibraryTabsComponent } from '@home/pages/admin/resource/resource-library-tabs.component'; @Injectable() export class ResourcesLibraryTableConfigResolver { @@ -55,6 +56,7 @@ export class ResourcesLibraryTableConfigResolver { this.config.entityTranslations = entityTypeTranslations.get(EntityType.TB_RESOURCE); this.config.entityResources = entityTypeResources.get(EntityType.TB_RESOURCE); this.config.headerComponent = ResourcesTableHeaderComponent; + this.config.entityTabsComponent = ResourceLibraryTabsComponent; this.config.entityTitle = (resource) => resource ? resource.title : ''; diff --git a/ui-ngx/src/app/modules/home/pages/admin/trendz-settings.component.ts b/ui-ngx/src/app/modules/home/pages/admin/trendz-settings.component.ts index 16ae6bf1da..f477b6fdaf 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/trendz-settings.component.ts +++ b/ui-ngx/src/app/modules/home/pages/admin/trendz-settings.component.ts @@ -21,6 +21,9 @@ import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { TrendzSettingsService } from '@core/http/trendz-settings.service'; import { TrendzSettings } from '@shared/models/trendz-settings.models'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { Store } from "@ngrx/store"; +import { AppState } from "@core/core.state"; +import { ActionAuthUpdateTrendzSettings } from "@core/auth/auth.actions"; @Component({ selector: 'tb-trendz-settings', @@ -31,7 +34,8 @@ export class TrendzSettingsComponent extends PageComponent implements OnInit, Ha trendzSettingsForm: FormGroup; - constructor(private fb: FormBuilder, + constructor(protected store: Store, + private fb: FormBuilder, private trendzSettingsService: TrendzSettingsService, private destroyRef: DestroyRef) { super(); @@ -93,6 +97,7 @@ export class TrendzSettingsComponent extends PageComponent implements OnInit, Ha this.trendzSettingsService.saveTrendzSettings(trendzSettings) .subscribe(() => { this.setTrendzSettings(trendzSettings); + this.store.dispatch(new ActionAuthUpdateTrendzSettings(trendzSettings)) }) } } diff --git a/ui-ngx/src/app/modules/home/pages/ai-model/ai-model-routing.module.ts b/ui-ngx/src/app/modules/home/pages/ai-model/ai-model-routing.module.ts new file mode 100644 index 0000000000..82fff06270 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/ai-model/ai-model-routing.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 { RouterModule, Routes } from '@angular/router'; +import { EntitiesTableComponent } from '@home/components/entity/entities-table.component'; +import { Authority } from '@shared/models/authority.enum'; +import { MenuId } from '@core/services/menu.models'; +import { AiModelsTableConfigResolver } from '@home/pages/ai-model/ai-model-table-config.resolve'; +import { NgModule } from '@angular/core'; + +export const aiModelRoutes: Routes = [ + { + path: 'ai-models', + component: EntitiesTableComponent, + data: { + auth: [Authority.TENANT_ADMIN], + title: 'ai-models.ai-models', + breadcrumb: { + menuId: MenuId.ai_models + } + }, + resolve: { + entitiesTableConfig: AiModelsTableConfigResolver + } + } +]; + +@NgModule({ + providers: [ + AiModelsTableConfigResolver + ], + imports: [RouterModule.forChild(aiModelRoutes)], + exports: [RouterModule], +}) +export class AiModelRoutingModule { } diff --git a/ui-ngx/src/app/modules/home/pages/ai-model/ai-model-table-config.resolve.ts b/ui-ngx/src/app/modules/home/pages/ai-model/ai-model-table-config.resolve.ts new file mode 100644 index 0000000000..21a5f50a2f --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/ai-model/ai-model-table-config.resolve.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 { Injectable } from '@angular/core'; +import { + CellActionDescriptor, + DateEntityTableColumn, + EntityTableColumn, + EntityTableConfig +} from '@home/models/entity/entities-table-config.models'; +import { ActivatedRouteSnapshot } from '@angular/router'; +import { EntityType, entityTypeResources, entityTypeTranslations } from '@shared/models/entity-type.models'; +import { Direction } from '@shared/models/page/sort-order'; +import { DatePipe } from '@angular/common'; +import { TranslateService } from '@ngx-translate/core'; +import { MatDialog } from '@angular/material/dialog'; +import { Observable } from 'rxjs'; +import { AiModel, AiProviderTranslations } from '@shared/models/ai-model.models'; +import { AiModelService } from '@core/http/ai-model.service'; +import { AiModelTableHeaderComponent } from '@home/pages/ai-model/ai-model-table-header.component'; +import { AIModelDialogComponent, AIModelDialogData } from '@home/components/ai-model/ai-model-dialog.component'; +import { map } from 'rxjs/operators'; + +@Injectable() +export class AiModelsTableConfigResolver { + + private readonly config: EntityTableConfig = new EntityTableConfig(); + + constructor( + private datePipe: DatePipe, + private aiModelService: AiModelService, + private translate : TranslateService, + private dialog: MatDialog + ) { + this.config.selectionEnabled = true; + this.config.entityType = EntityType.AI_MODEL; + this.config.addAsTextButton = true; + this.config.rowPointer = true; + this.config.detailsPanelEnabled = false; + this.config.entityTranslations = entityTypeTranslations.get(EntityType.AI_MODEL); + this.config.entityResources = entityTypeResources.get(EntityType.AI_MODEL); + + this.config.headerComponent = AiModelTableHeaderComponent; + this.config.addDialogStyle = {width: '850px', maxHeight: '100vh'}; + this.config.defaultSortOrder = {property: 'createdTime', direction: Direction.DESC}; + + this.config.addEntity = () => this.addModel(null, true); + + this.config.columns.push( + new DateEntityTableColumn('createdTime', 'common.created-time', this.datePipe, '170px'), + new EntityTableColumn('name', 'ai-models.name', '33%'), + new EntityTableColumn('provider', 'ai-models.provider', '33%', + entity => this.translate.instant(AiProviderTranslations.get(entity.configuration.provider)) + ), + new EntityTableColumn('modelId', 'ai-models.model', '33%', entity => entity.configuration.modelId) + ) + + this.config.deleteEntityTitle = model => this.translate.instant('ai-models.delete-model-title', {modelName: model.name}); + this.config.deleteEntityContent = () => this.translate.instant('ai-models.delete-model-text'); + this.config.deleteEntitiesTitle = count => this.translate.instant('ai-models.delete-models-title', {count}); + this.config.deleteEntitiesContent = () => this.translate.instant('ai-models.delete-models-text'); + + this.config.deleteEntity = id => this.aiModelService.deleteAiModel(id.id); + + this.config.entitiesFetchFunction = pageLink => this.aiModelService.getAiModels(pageLink); + + this.config.cellActionDescriptors = this.configureCellActions(); + + this.config.handleRowClick = ($event, model) => { + this.editModel($event, model); + return true; + }; + } + + resolve(_route: ActivatedRouteSnapshot): EntityTableConfig { + return this.config; + } + + private configureCellActions(): Array> { + return [ + { + name: this.translate.instant('action.edit'), + icon: 'edit', + isEnabled: () => true, + onAction: ($event, entity) => this.editModel($event, entity) + } + ]; + } + + private editModel($event, AIModel: AiModel): void { + $event?.stopPropagation(); + this.addModel(AIModel, false).subscribe(res => res ? this.config.updateData() : null); + } + + private addModel(AIModel: AiModel, isAdd = false): Observable { + return this.dialog.open(AIModelDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + isAdd, + AIModel + } + }).afterClosed(); + } +} diff --git a/ui-ngx/src/app/modules/home/pages/ai-model/ai-model-table-header.component.html b/ui-ngx/src/app/modules/home/pages/ai-model/ai-model-table-header.component.html new file mode 100644 index 0000000000..43f1dbf0db --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/ai-model/ai-model-table-header.component.html @@ -0,0 +1,21 @@ + +
+
ai-models.ai-models
+
+
diff --git a/ui-ngx/src/app/modules/home/pages/ai-model/ai-model-table-header.component.ts b/ui-ngx/src/app/modules/home/pages/ai-model/ai-model-table-header.component.ts new file mode 100644 index 0000000000..013642dcb2 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/ai-model/ai-model-table-header.component.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 { Component } from '@angular/core'; +import { EntityTableHeaderComponent } from '@home/components/entity/entity-table-header.component'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { AiModel } from '@shared/models/ai-model.models'; + +@Component({ + selector: 'tb-ai-model-table-header', + templateUrl: './ai-model-table-header.component.html', + styles: [` + :host { + width: 100%; + } + `], + styleUrls: [] +}) +export class AiModelTableHeaderComponent extends EntityTableHeaderComponent { + + constructor(protected store: Store) { + super(store); + } +} diff --git a/ui-ngx/src/app/modules/home/pages/ai-model/ai-model.module.ts b/ui-ngx/src/app/modules/home/pages/ai-model/ai-model.module.ts new file mode 100644 index 0000000000..bd20612e8a --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/ai-model/ai-model.module.ts @@ -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. +/// + +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { SharedModule } from '@shared/shared.module'; +import { AiModelRoutingModule } from '@home/pages/ai-model/ai-model-routing.module'; +import { AiModelTableHeaderComponent } from '@home/pages/ai-model/ai-model-table-header.component'; + +@NgModule({ + declarations: [ + AiModelTableHeaderComponent + ], + imports: [ + CommonModule, + SharedModule, + AiModelRoutingModule + ] +}) +export class AiModelModule { } diff --git a/ui-ngx/src/app/modules/home/pages/home-pages.module.ts b/ui-ngx/src/app/modules/home/pages/home-pages.module.ts index 39c442cc49..5bb3954cae 100644 --- a/ui-ngx/src/app/modules/home/pages/home-pages.module.ts +++ b/ui-ngx/src/app/modules/home/pages/home-pages.module.ts @@ -46,6 +46,7 @@ import { AccountModule } from '@home/pages/account/account.module'; import { ScadaSymbolModule } from '@home/pages/scada-symbol/scada-symbol.module'; import { GatewaysModule } from '@home/pages/gateways/gateways.module'; import { MobileModule } from '@home/pages/mobile/mobile.module'; +import { AiModelModule } from '@home/pages/ai-model/ai-model.module'; @NgModule({ exports: [ @@ -78,7 +79,8 @@ import { MobileModule } from '@home/pages/mobile/mobile.module'; UserModule, VcModule, AccountModule, - ScadaSymbolModule + ScadaSymbolModule, + AiModelModule, ] }) export class HomePagesModule { } 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/applications/mobile-app-table-config.resolver.ts b/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app-table-config.resolver.ts index 0a0732362e..6a05558905 100644 --- a/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app-table-config.resolver.ts +++ b/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app-table-config.resolver.ts @@ -83,6 +83,7 @@ export class MobileAppTableConfigResolver { onAction: (_$event, entity) => entity.pkgName, type: CellActionDescriptorType.COPY_BUTTON }), + new EntityTableColumn('title', 'mobile.mobile-package-title', '20%'), new EntityTableColumn('appSecret', 'mobile.application-secret', '15%', (entity) => this.truncatePipe.transform(entity.appSecret, true, 10, '…'), () => ({}), true, () => ({}), () => undefined, false, diff --git a/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app.component.html b/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app.component.html index daad184b9e..bbafb83e52 100644 --- a/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app.component.html +++ b/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app.component.html @@ -15,7 +15,7 @@ limitations under the License. --> -
+
mobile.mobile-package @@ -38,6 +38,14 @@ {{ 'mobile.mobile-package-pattern' | translate }} + + mobile.mobile-package-title + + + + {{ 'mobile.mobile-package-title-max-length' | translate }} + + mobile.platform-type diff --git a/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app.component.scss b/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app.component.scss index 1925568b81..2462c55555 100644 --- a/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app.component.scss +++ b/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app.component.scss @@ -14,5 +14,5 @@ * limitations under the License. */ :host { - --mdc-outlined-text-field-outline-color: rgba(0,0,0,0.12); + --mat-form-field-disabled-trailing-icon-color: rgba(0, 0, 0, 0.56); } diff --git a/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app.component.ts b/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app.component.ts index eceda5ac9a..141c274422 100644 --- a/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app.component.ts +++ b/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app.component.ts @@ -63,9 +63,10 @@ export class MobileAppComponent extends EntityComponent { buildForm(entity: MobileApp): FormGroup { const form = this.fb.group({ - pkgName: [entity?.pkgName ? entity.pkgName : '', [Validators.required, Validators.maxLength(255), + pkgName: [entity?.pkgName ?? '', [Validators.required, Validators.maxLength(255), Validators.pattern(/^[a-zA-Z][a-zA-Z\d_]*(?:\.[a-zA-Z][a-zA-Z\d_]*)+$/)]], - platformType: [entity?.platformType ? entity.platformType : PlatformType.ANDROID], + title: [entity?.title ?? '', [Validators.maxLength(255)]], + platformType: [entity?.platformType ?? PlatformType.ANDROID], appSecret: [entity?.appSecret ? entity.appSecret : btoa(randomAlphanumeric(64)), [Validators.required, this.base64Format]], status: [entity?.status ? entity.status : MobileAppStatus.DRAFT], versionInfo: this.fb.group({ diff --git a/ui-ngx/src/app/modules/home/pages/mobile/bundes/mobile-app-configuration-dialog.component.html b/ui-ngx/src/app/modules/home/pages/mobile/bundes/mobile-app-configuration-dialog.component.html index 3c11be8930..afaa0da3c1 100644 --- a/ui-ngx/src/app/modules/home/pages/mobile/bundes/mobile-app-configuration-dialog.component.html +++ b/ui-ngx/src/app/modules/home/pages/mobile/bundes/mobile-app-configuration-dialog.component.html @@ -43,23 +43,14 @@ [data]=createMarkDownCommand(gitRepositoryLink)>
-
mobile.configuration-step.configure-api-title
-
mobile.configuration-step.configure-api-text
- -
mobile.configuration-step.configure-api-hint
- -
-
-
mobile.configuration-step.configure-package-title
-
mobile.configuration-step.configure-package-text
-
mobile.configuration-step.configure-package-text-install
- -
mobile.configuration-step.configure-package-run-commands
- +
mobile.configuration-step.configure-app-settings-title
+
+
mobile.configuration-step.configure-app-settings-text
+ +
mobile.configuration-step.run-app-title
diff --git a/ui-ngx/src/app/modules/home/pages/mobile/bundes/mobile-app-configuration-dialog.component.ts b/ui-ngx/src/app/modules/home/pages/mobile/bundes/mobile-app-configuration-dialog.component.ts index 9168f14245..5644620b27 100644 --- a/ui-ngx/src/app/modules/home/pages/mobile/bundes/mobile-app-configuration-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/pages/mobile/bundes/mobile-app-configuration-dialog.component.ts @@ -21,12 +21,15 @@ import { AppState } from '@core/core.state'; import { Router } from '@angular/router'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { ActionPreferencesPutUserSettings } from '@core/auth/auth.actions'; -import { MobileApp } from '@shared/models/mobile-app.models'; +import { MobileApp, MobileAppBundleInfo } from '@shared/models/mobile-app.models'; +import { ImportExportService } from '@shared/import-export/import-export.service'; +import { isNotEmptyStr } from '@core/utils'; export interface MobileAppConfigurationDialogData { afterAdd: boolean; androidApp: MobileApp; iosApp: MobileApp; + bundle: MobileAppBundleInfo; } @Component({ @@ -36,53 +39,22 @@ export interface MobileAppConfigurationDialogData { }) export class MobileAppConfigurationDialogComponent extends DialogComponent { - notShowAgain = false; - setApplication = false; + private fileName = 'configs'; + notShowAgain = false; showDontShowAgain: boolean; gitRepositoryLink = 'git clone -b master https://github.com/thingsboard/flutter_thingsboard_app.git'; - pathToConstants = 'lib/constants/app_constants.dart'; - flutterRunCommand = 'flutter run'; - flutterInstallRenameCommand = 'flutter pub global activate rename'; - - configureApi: string; - - renameCommands: string[] = []; + flutterRunCommand = `flutter run --dart-define-from-file ${this.fileName}.json`; constructor(protected store: Store, protected router: Router, @Inject(MAT_DIALOG_DATA) private data: MobileAppConfigurationDialogData, protected dialogRef: MatDialogRef, + private importExportService: ImportExportService, ) { super(store, router, dialogRef); - this.showDontShowAgain = this.data.afterAdd; - - this.setApplication = !!this.data.androidApp || !!this.data.iosApp; - - this.configureApi = `static const thingsBoardApiEndpoint = '${window.location.origin}';`; - if (this.setApplication) { - this.configureApi += '\n'; - if (!!this.data.androidApp) { - this.configureApi += `\nstatic const thingsboardAndroidAppSecret = '${this.data.androidApp.appSecret}';`; - } - if (!!this.data.iosApp) { - this.configureApi += `\nstatic const thingsboardIOSAppSecret = '${this.data.iosApp.appSecret}';`; - } - } - if (this.setApplication) { - if (this.data.androidApp?.pkgName === this.data.iosApp?.pkgName) { - this.renameCommands.push(`rename setBundleId --targets android, ios --value "${this.data.androidApp.pkgName}"`); - } else { - if (!!this.data.androidApp) { - this.renameCommands.push(`rename setBundleId --targets android --value "${this.data.androidApp.pkgName}"`); - } - if (!!this.data.iosApp) { - this.renameCommands.push(`rename setBundleId --targets ios --value "${this.data.iosApp.pkgName}"`); - } - } - } } close(): void { @@ -94,14 +66,28 @@ export class MobileAppConfigurationDialogComponent extends DialogComponent = []; - commands.forEach(command => formatCommands.push(this.createMarkDownSingleCommand(command))); - return formatCommands.join(`\n
\n\n`); - } else { - return this.createMarkDownSingleCommand(commands); + createMarkDownCommand(commands: string): string { + return this.createMarkDownSingleCommand(commands); + } + + downloadSettings(): void { + const settings: any = { + thingsboardApiEndpoint: window.location.origin, + appLinksUrlHost: window.location.host, + appLinksUrlScheme: window.location.protocol.slice(0, -1), + }; + if (!!this.data.androidApp) { + settings.androidApplicationId = this.data.androidApp.pkgName; + settings.androidApplicationName = isNotEmptyStr(this.data.androidApp.title) ? this.data.androidApp.title : this.data.bundle.title; + settings.thingsboardOAuth2CallbackUrlScheme = this.data.androidApp.pkgName + '.auth'; + settings.thingsboardAndroidAppSecret = this.data.androidApp.appSecret; + } + if (!!this.data.iosApp) { + settings.iosApplicationId = this.data.iosApp.pkgName; + settings.iosApplicationName = isNotEmptyStr(this.data.iosApp.title) ? this.data.iosApp.title : this.data.bundle.title; + settings.thingsboardIosAppSecret = this.data.iosApp.appSecret; } + this.importExportService.exportJson(settings, this.fileName); } private createMarkDownSingleCommand(command: string): string { 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/bundes/mobile-bundle-table-config.resolve.ts b/ui-ngx/src/app/modules/home/pages/mobile/bundes/mobile-bundle-table-config.resolve.ts index 6a0dc25d70..0b8d1efc21 100644 --- a/ui-ngx/src/app/modules/home/pages/mobile/bundes/mobile-bundle-table-config.resolve.ts +++ b/ui-ngx/src/app/modules/home/pages/mobile/bundes/mobile-bundle-table-config.resolve.ts @@ -182,7 +182,8 @@ export class MobileBundleTableConfigResolver { data: { afterAdd, androidApp: data.androidApp, - iosApp: data.iosApp + iosApp: data.iosApp, + bundle: entity } }).afterClosed() .subscribe(); 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 951aac77a2..f88244add1 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,10 +39,9 @@
{{ 'mobile.bundle' | translate }}
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/ota-update/ota-update-table-config.resolve.ts b/ui-ngx/src/app/modules/home/pages/ota-update/ota-update-table-config.resolve.ts index cc3ca4046b..0dae778d03 100644 --- a/ui-ngx/src/app/modules/home/pages/ota-update/ota-update-table-config.resolve.ts +++ b/ui-ngx/src/app/modules/home/pages/ota-update/ota-update-table-config.resolve.ts @@ -36,6 +36,7 @@ import { PageLink } from '@shared/models/page/page-link'; import { OtaUpdateComponent } from '@home/pages/ota-update/ota-update.component'; import { EntityAction } from '@home/models/entity/entity-component.models'; import { FileSizePipe } from '@shared/pipe/file-size.pipe'; +import { OtaUpdateTabsComponent } from '@home/pages/ota-update/ota-update-tabs.component'; @Injectable() export class OtaUpdateTableConfigResolve { @@ -50,6 +51,7 @@ export class OtaUpdateTableConfigResolve { private fileSize: FileSizePipe) { this.config.entityType = EntityType.OTA_PACKAGE; this.config.entityComponent = OtaUpdateComponent; + this.config.entityTabsComponent = OtaUpdateTabsComponent; this.config.entityTranslations = entityTypeTranslations.get(EntityType.OTA_PACKAGE); this.config.entityResources = entityTypeResources.get(EntityType.OTA_PACKAGE); diff --git a/ui-ngx/src/app/modules/home/pages/ota-update/ota-update-tabs.component.html b/ui-ngx/src/app/modules/home/pages/ota-update/ota-update-tabs.component.html new file mode 100644 index 0000000000..a8cdae4256 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/ota-update/ota-update-tabs.component.html @@ -0,0 +1,23 @@ + + + + diff --git a/ui-ngx/src/app/modules/home/pages/ota-update/ota-update-tabs.component.ts b/ui-ngx/src/app/modules/home/pages/ota-update/ota-update-tabs.component.ts new file mode 100644 index 0000000000..44d17f3c11 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/ota-update/ota-update-tabs.component.ts @@ -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. +/// + +import { Component } from '@angular/core'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { EntityTabsComponent } from '../../components/entity/entity-tabs.component'; +import { WidgetsBundle } from '@shared/models/widgets-bundle.model'; +import { NULL_UUID } from '@shared/models/id/has-uuid'; +import { OtaPackage } from '@shared/models/ota-package.models'; + +@Component({ + selector: 'tb-ota-update-tabs', + templateUrl: './ota-update-tabs.component.html', + styleUrls: [] +}) +export class OtaUpdateTabsComponent extends EntityTabsComponent { + + constructor(protected store: Store) { + super(store); + } + + isTenantOtaUpdate() { + return this.entity && this.entity.tenantId.id !== NULL_UUID; + } + +} diff --git a/ui-ngx/src/app/modules/home/pages/ota-update/ota-update.component.html b/ui-ngx/src/app/modules/home/pages/ota-update/ota-update.component.html index e028a2714a..ee8e34a4a2 100644 --- a/ui-ngx/src/app/modules/home/pages/ota-update/ota-update.component.html +++ b/ui-ngx/src/app/modules/home/pages/ota-update/ota-update.component.html @@ -63,7 +63,7 @@
-
+
ota-update.title diff --git a/ui-ngx/src/app/modules/home/pages/ota-update/ota-update.module.ts b/ui-ngx/src/app/modules/home/pages/ota-update/ota-update.module.ts index 139591f14f..fe24da31b2 100644 --- a/ui-ngx/src/app/modules/home/pages/ota-update/ota-update.module.ts +++ b/ui-ngx/src/app/modules/home/pages/ota-update/ota-update.module.ts @@ -20,10 +20,12 @@ import { SharedModule } from '@shared/shared.module'; import { HomeComponentsModule } from '@home/components/home-components.module'; import { OtaUpdateRoutingModule } from '@home/pages/ota-update/ota-update-routing.module'; import { OtaUpdateComponent } from '@home/pages/ota-update/ota-update.component'; +import { OtaUpdateTabsComponent } from '@home/pages/ota-update/ota-update-tabs.component'; @NgModule({ declarations: [ - OtaUpdateComponent + OtaUpdateComponent, + OtaUpdateTabsComponent ], imports: [ CommonModule, 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/shared/components/entity/entity-autocomplete.component.html b/ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.html index 92a316efe9..db7300530e 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 @@ -71,6 +71,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 ac8054283a..9137a3b2d7 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 @@ -14,16 +14,7 @@ /// limitations under the License. /// -import { - Component, - ElementRef, - EventEmitter, - forwardRef, - Input, - OnInit, - Output, - ViewChild -} from '@angular/core'; +import { Component, ElementRef, EventEmitter, forwardRef, Input, OnInit, Output, ViewChild } from '@angular/core'; import { MatFormFieldAppearance, SubscriptSizing } from '@angular/material/form-field'; import { ControlValueAccessor, NG_VALUE_ACCESSOR, UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; import { firstValueFrom, merge, Observable, of, Subject } from 'rxjs'; @@ -151,7 +142,7 @@ export class EntityAutocompleteComponent implements ControlValueAccessor, OnInit entityChanged = new EventEmitter>(); @Output() - createNew = new EventEmitter(); + createNew = new EventEmitter(); @ViewChild('entityInput', {static: true}) entityInput: ElementRef; @@ -300,6 +291,12 @@ export class EntityAutocompleteComponent implements ControlValueAccessor, OnInit this.entityRequiredText = 'notification.notification-recipient-required'; this.notFoundEntities = 'notification.no-recipients-text'; break; + case EntityType.AI_MODEL: + this.entityText = 'ai-models.ai-model'; + this.noEntitiesMatchingText = 'ai-models.no-model-matching'; + this.entityRequiredText = 'ai-models.model-required'; + this.notFoundEntities = 'ai-models.no-model-text'; + break; case AliasEntityType.CURRENT_CUSTOMER: this.entityText = 'customer.default-customer'; this.noEntitiesMatchingText = 'customer.no-customers-matching'; @@ -454,9 +451,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..efc90ec048 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,7 +16,7 @@ --> - 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..a86fc70115 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(); @@ -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()) 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 e1b951b40b..6bf7cdb78d 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 @@ -21,10 +21,11 @@ [class.tb-chips]="inlineField" [class.flex]="inlineField" [subscriptSizing]="inlineField ? 'dynamic' : subscriptSizing"> - {{ labelText }} {{ labelText }} + {{ labelText }} {{entity.name}} 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 50d3abc435..2b07af9e08 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 @@ -15,9 +15,11 @@ limitations under the License. --> -
+
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 a85589fae4..01b1f03388 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 @@ -25,6 +25,7 @@ import { EntityId } from '@shared/models/id/entity-id'; import { NULL_UUID } from '@shared/models/id/has-uuid'; import { coerceBoolean } from '@shared/decorators/coercion'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { MatFormFieldAppearance } from '@angular/material/form-field'; @Component({ selector: 'tb-entity-select', @@ -58,6 +59,9 @@ export class EntitySelectComponent implements ControlValueAccessor, OnInit, Afte @Input() additionEntityTypes: {[entityType in string]: string} = {}; + @Input() + appearance: MatFormFieldAppearance = 'fill'; + 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..6635921d7f 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)}} diff --git a/ui-ngx/src/app/shared/components/help-popup.component.html b/ui-ngx/src/app/shared/components/help-popup.component.html index c24b5234b9..8bd3dbcfa9 100644 --- a/ui-ngx/src/app/shared/components/help-popup.component.html +++ b/ui-ngx/src/app/shared/components/help-popup.component.html @@ -17,9 +17,9 @@ -->
-
+
+ - - -
- - - - +
+ 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/mqtt-version-select.component.ts b/ui-ngx/src/app/shared/components/mqtt-version-select.component.ts index 6bde856a7b..0a9b75be2d 100644 --- a/ui-ngx/src/app/shared/components/mqtt-version-select.component.ts +++ b/ui-ngx/src/app/shared/components/mqtt-version-select.component.ts @@ -14,7 +14,7 @@ /// limitations under the License. /// -import { Component, forwardRef, Input } from '@angular/core'; +import { Component, forwardRef, Input, OnChanges, SimpleChanges } from '@angular/core'; import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; import { coerceBoolean } from '@shared/decorators/coercion'; import { SubscriptSizing, MatFormFieldAppearance } from '@angular/material/form-field'; @@ -30,7 +30,7 @@ import { MqttVersionTranslation, MqttVersion } from '@shared/models/mqtt.models' multi: true }] }) -export class MqttVersionSelectComponent implements ControlValueAccessor { +export class MqttVersionSelectComponent implements ControlValueAccessor, OnChanges { @Input() disabled: boolean; @@ -41,7 +41,10 @@ export class MqttVersionSelectComponent implements ControlValueAccessor { @Input() appearance: MatFormFieldAppearance = 'fill'; - mqttVersions = Object.values(MqttVersion); + @Input() + excludeVersions: MqttVersion[]; + + mqttVersions = Object.values(MqttVersion); mqttVersionTranslation = MqttVersionTranslation; modelValue: MqttVersion; @@ -54,6 +57,20 @@ export class MqttVersionSelectComponent implements ControlValueAccessor { constructor() { } + ngOnChanges(changes: SimpleChanges): void { + for (const propName of Object.keys(changes)) { + const change = changes[propName]; + if (propName === 'excludeVersions' && change.currentValue !== change.previousValue) { + const excludeVersions = change.currentValue; + if (excludeVersions?.length) { + this.mqttVersions = Object.values(MqttVersion).filter(v => !excludeVersions.includes(v)); + } else { + this.mqttVersions = Object.values(MqttVersion); + } + } + } + } + registerOnChange(fn: any): void { this.propagateChange = fn; } 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/string-autocomplete.component.html b/ui-ngx/src/app/shared/components/string-autocomplete.component.html index 8a9a1a38d0..8eb22707f9 100644 --- a/ui-ngx/src/app/shared/components/string-autocomplete.component.html +++ b/ui-ngx/src/app/shared/components/string-autocomplete.component.html @@ -29,7 +29,7 @@ (click)="clear()"> close - + [panelWidth]="panelWidth"> diff --git a/ui-ngx/src/app/shared/components/string-autocomplete.component.ts b/ui-ngx/src/app/shared/components/string-autocomplete.component.ts index 2f1ea5db14..70b1115b5b 100644 --- a/ui-ngx/src/app/shared/components/string-autocomplete.component.ts +++ b/ui-ngx/src/app/shared/components/string-autocomplete.component.ts @@ -76,6 +76,9 @@ export class StringAutocompleteComponent implements ControlValueAccessor, OnInit @Input() label: string; + @Input() + panelWidth: string = 'fit-content'; + @Input() tooltipClass = 'tb-error-tooltip'; 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/shared/components/time-unit-input.component.html b/ui-ngx/src/app/shared/components/time-unit-input.component.html new file mode 100644 index 0000000000..d5f6319576 --- /dev/null +++ b/ui-ngx/src/app/shared/components/time-unit-input.component.html @@ -0,0 +1,56 @@ + +
+ + @if (labelText && !inlineField) { + {{ labelText }} + } + +
+ +
+ @if (inlineField) { + + warning + + } + + + {{ hasError }} + +
+ + @if (!inlineField) { + rule-node-config.units + } + + @for (timeUnit of timeUnits; track timeUnit) { + {{ timeUnitTranslations.get(timeUnit) | translate }} + } + + +
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 79% 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 b0d0a97641..44f1be514a 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 @@ -25,12 +25,12 @@ import { Validator, 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'; import { DAY, HOUR, MINUTE, SECOND } from '@shared/models/time/time.models'; -import { SubscriptSizing } from '@angular/material/form-field'; +import { MatFormFieldAppearance, SubscriptSizing } from '@angular/material/form-field'; interface TimeUnitInputModel { time: number; @@ -79,6 +79,13 @@ export class TimeUnitInputComponent implements ControlValueAccessor, Validator, @Input() subscriptSizing: SubscriptSizing = 'fixed'; + @Input() + appearance: MatFormFieldAppearance = 'fill'; + + @Input() + @coerceBoolean() + inlineField: boolean; + timeUnits = Object.values(TimeUnit).filter(item => item !== TimeUnit.MILLISECONDS) as TimeUnit[]; timeUnitTranslations = timeUnitTranslations; @@ -104,6 +111,16 @@ 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(this.required || this.maxTime) { const timeControl = this.timeInputForm.get('time'); const validators = [Validators.pattern(/^\d*$/)]; @@ -116,7 +133,9 @@ 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) + ); } timeControl.setValidators(validators); @@ -137,6 +156,16 @@ export class TimeUnitInputComponent implements ControlValueAccessor, Validator, }); } + get hasError(): string { + if (this.timeInputForm.get('time').hasError('required') && this.requiredText) { + return this.requiredText; + } else if (this.timeInputForm.get('time').hasError('min') && this.minErrorText) { + return this.minErrorText; + } else if (this.timeInputForm.get('time').hasError('max') && this.maxErrorText) { + return this.maxErrorText; + } + } + registerOnChange(fn: any) { this.propagateChange = fn; } 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..42e04cfc46 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({ @@ -423,7 +431,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 +442,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..2f0a1aa603 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()); }); } } @@ -406,27 +407,29 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit, O } 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 +440,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 +555,6 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit, O } openTimewindowConfig() { - this.prepareTimewindowConfig(); this.dialog.open( TimewindowConfigDialogComponent, { autoFocus: false, @@ -555,7 +563,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 +576,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 +590,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/import-export/import-export.service.ts b/ui-ngx/src/app/shared/import-export/import-export.service.ts index 2ad60f0139..94960cb838 100644 --- a/ui-ngx/src/app/shared/import-export/import-export.service.ts +++ b/ui-ngx/src/app/shared/import-export/import-export.service.ts @@ -1188,7 +1188,7 @@ export class ImportExportService { this.exportJson(data, filename); } - private exportJson(data: any, filename: string) { + public exportJson(data: any, filename: string) { if (isObject(data)) { data = JSON.stringify(data, null, 2); } diff --git a/ui-ngx/src/app/shared/models/ai-model.models.ts b/ui-ngx/src/app/shared/models/ai-model.models.ts new file mode 100644 index 0000000000..f3161263b7 --- /dev/null +++ b/ui-ngx/src/app/shared/models/ai-model.models.ts @@ -0,0 +1,230 @@ +/// +/// 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 { BaseData, ExportableEntity } from '@shared/models/base-data'; +import { HasTenantId } from '@shared/models/entity.models'; +import { AiModelId } from '@shared/models/id/ai-model-id'; + +export interface AiModel extends Omit, 'label'>, HasTenantId, ExportableEntity { + modelType: ModelType; + configuration: { + provider: AiProvider + providerConfig: { + apiKey?: string; + personalAccessToken?: string; + endpoint?: string; + serviceVersion?: string; + projectId?: string; + location?: string; + serviceAccountKey?: string; + fileName?: string; + region?: string; + accessKeyId?: string; + secretAccessKey?: string; + }; + modelId: string; + temperature?: number; + topP?: number; + topK?: number; + frequencyPenalty?: number; + presencePenalty?: number; + maxOutputTokens?: number; + } +} + +export enum ModelType { + CHAT = 'CHAT' +} + +export enum AiProvider { + 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' +} + +export const AiProviderTranslations = new Map( + [ + [AiProvider.OPENAI , 'ai-models.ai-providers.openai'], + [AiProvider.AZURE_OPENAI , 'ai-models.ai-providers.azure-openai'], + [AiProvider.GOOGLE_AI_GEMINI , 'ai-models.ai-providers.google-ai-gemini'], + [AiProvider.GOOGLE_VERTEX_AI_GEMINI , 'ai-models.ai-providers.google-vertex-ai-gemini'], + [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'] + ] +); + +export const ProviderFieldsAllList = [ + 'apiKey', + 'personalAccessToken', + 'projectId', + 'location', + 'serviceAccountKey', + 'fileName', + 'endpoint', + 'serviceVersion', + 'region', + 'accessKeyId', + 'secretAccessKey' +]; + +export const ModelFieldsAllList = ['temperature', 'topP', 'topK', 'frequencyPenalty', 'presencePenalty', 'maxOutputTokens']; + +export const AiModelMap = new Map([ + [ + AiProvider.OPENAI, + { + modelList: [ + 'o4-mini', + 'o3-pro', + 'o3', + 'o3-mini', + 'o1', + 'gpt-4.1', + 'gpt-4.1-mini', + 'gpt-4.1-nano', + 'gpt-4o', + 'gpt-4o-mini', + ], + providerFieldsList: ['apiKey'], + modelFieldsList: ['temperature', 'topP', 'frequencyPenalty', 'presencePenalty', 'maxOutputTokens'], + }, + ], + [ + AiProvider.AZURE_OPENAI, + { + modelList: [], + providerFieldsList: ['apiKey', 'endpoint', 'serviceVersion'], + modelFieldsList: ['temperature', 'topP', 'frequencyPenalty', 'presencePenalty', 'maxOutputTokens'], + }, + ], + [ + AiProvider.GOOGLE_AI_GEMINI, + { + modelList: [ + 'gemini-2.5-pro', + 'gemini-2.5-flash', + 'gemini-2.0-flash', + 'gemini-2.0-flash-lite', + ], + providerFieldsList: ['apiKey'], + modelFieldsList: ['temperature', 'topP', 'topK', 'frequencyPenalty', 'presencePenalty', 'maxOutputTokens'], + }, + ], + [ + AiProvider.GOOGLE_VERTEX_AI_GEMINI, + { + modelList: [ + 'gemini-2.5-pro', + 'gemini-2.5-flash', + 'gemini-2.0-flash', + 'gemini-2.0-flash-lite', + ], + providerFieldsList: ['projectId', 'location', 'serviceAccountKey', 'fileName'], + modelFieldsList: ['temperature', 'topP', 'topK', 'frequencyPenalty', 'presencePenalty', 'maxOutputTokens'], + }, + ], + [ + AiProvider.MISTRAL_AI, + { + modelList: [ + 'magistral-medium-latest', + 'magistral-small-latest', + 'mistral-large-latest', + 'mistral-medium-latest', + 'mistral-small-latest', + 'pixtral-large-latest', + 'ministral-8b-latest', + 'ministral-3b-latest', + 'open-mistral-nemo', + ], + providerFieldsList: ['apiKey'], + modelFieldsList: ['temperature', 'topP', 'frequencyPenalty', 'presencePenalty', 'maxOutputTokens'], + }, + ], + [ + AiProvider.ANTHROPIC, + { + modelList: [ + 'claude-opus-4-0', + 'claude-sonnet-4-0', + 'claude-3-7-sonnet-latest', + 'claude-3-5-sonnet-latest', + 'claude-3-5-haiku-latest', + ], + providerFieldsList: ['apiKey'], + modelFieldsList: ['temperature', 'topP', 'topK', 'maxOutputTokens'], + }, + ], + [ + AiProvider.AMAZON_BEDROCK, + { + modelList: [], + providerFieldsList: ['region', 'accessKeyId', 'secretAccessKey'], + modelFieldsList: ['temperature', 'topP', 'maxOutputTokens'], + }, + ], + [ + AiProvider.GITHUB_MODELS, + { + modelList: [], + providerFieldsList: ['personalAccessToken'], + modelFieldsList: ['temperature', 'topP', 'frequencyPenalty', 'presencePenalty', 'maxOutputTokens'], + }, + ], +]); + +export const AiRuleNodeResponseFormatTypeOnlyText: AiProvider[] = [AiProvider.AMAZON_BEDROCK, AiProvider.ANTHROPIC, AiProvider.GITHUB_MODELS]; + +export enum ResponseFormat { + TEXT = 'TEXT', + JSON = 'JSON', + JSON_SCHEMA = 'JSON_SCHEMA' +} + +export interface AiModelWithUserMsg { + userMessage: { + contents: Array<{contentType: string; text: string}>; + } + chatModelConfig: { + modelType: string; + provider: AiProvider + providerConfig: { + apiKey?: string; + personalAccessToken?: string; + endpoint?: string; + serviceVersion?: string; + projectId?: string; + location?: string; + serviceAccountKey?: string; + fileName?: string + }; + modelId: string; + maxRetries: number; + timeoutSeconds: number; + } +} + +export interface CheckConnectivityResult { + status: string; + errorDetails: string; +} diff --git a/ui-ngx/src/app/shared/models/alarm.models.ts b/ui-ngx/src/app/shared/models/alarm.models.ts index 61407e1248..bcf0d7b75d 100644 --- a/ui-ngx/src/app/shared/models/alarm.models.ts +++ b/ui-ngx/src/app/shared/models/alarm.models.ts @@ -271,6 +271,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/calculated-field.models.ts b/ui-ngx/src/app/shared/models/calculated-field.models.ts index 76b775b2fd..a86943c86e 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,6 +29,7 @@ import { dotOperatorHighlightRule, endGroupHighlightRule } from '@shared/models/ace/ace.models'; +import { EntitySearchDirection } from '@shared/models/relation.models'; export interface CalculatedField extends Omit, 'label'>, HasVersion, HasEntityDebugSettings, HasTenantId, ExportableEntity { configuration: CalculatedFieldConfiguration; @@ -43,19 +40,23 @@ export interface CalculatedField extends Omit, 'labe export enum CalculatedFieldType { SIMPLE = 'SIMPLE', SCRIPT = 'SCRIPT', + GEOFENCING = 'GEOFENCING' } export const CalculatedFieldTypeTranslations = new Map( [ [CalculatedFieldType.SIMPLE, 'calculated-fields.type.simple'], [CalculatedFieldType.SCRIPT, 'calculated-fields.type.script'], + [CalculatedFieldType.GEOFENCING, 'calculated-fields.type.geofencing'], ] ) export interface CalculatedFieldConfiguration { type: CalculatedFieldType; - expression: string; - arguments: Record; + expression?: string; + arguments?: Record; + zoneGroups?: Record; + scheduledUpdateInterval?: number; output: CalculatedFieldOutput; } @@ -72,6 +73,7 @@ export enum ArgumentEntityType { Asset = 'ASSET', Customer = 'CUSTOMER', Tenant = 'TENANT', + RelationQuery = 'RELATION_QUERY', } export const ArgumentEntityTypeTranslations = new Map( @@ -81,6 +83,28 @@ 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'], ] ) @@ -131,6 +155,29 @@ export interface CalculatedFieldArgument { timeWindow?: number; } +export interface CalculatedFieldGeofencing { + perimeterKeyName: string; + reportStrategy: GeofencingReportStrategy; + refEntityId?: RefEntityId; + refDynamicSourceConfiguration: RefDynamicSourceConfiguration; + createRelationsWithMatchedZones: boolean; + relationType: string; + direction: EntitySearchDirection; +} + +export interface RefDynamicSourceConfiguration { + type?: ArgumentEntityType.RelationQuery; + direction: EntitySearchDirection; + relationType: string; + maxLevel: number; + fetchLastLevelOnly?: boolean; +} + +export interface CalculatedFieldGeofencingValue extends CalculatedFieldGeofencing { + name: string; + entityName?: string; +} + export interface RefEntityKey { key: string; type: ArgumentType; diff --git a/ui-ngx/src/app/shared/models/constants.ts b/ui-ngx/src/app/shared/models/constants.ts index fbb3e73216..f88c6a1216 100644 --- a/ui-ngx/src/app/shared/models/constants.ts +++ b/ui-ngx/src/app/shared/models/constants.ts @@ -126,8 +126,11 @@ export const HelpLinks = { 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`, @@ -135,10 +138,12 @@ export const HelpLinks = { 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`, + 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`, @@ -154,6 +159,7 @@ export const HelpLinks = { 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`, @@ -171,6 +177,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`, @@ -200,6 +207,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}/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/entity-type.models.ts b/ui-ngx/src/app/shared/models/entity-type.models.ts index 48cad42e7b..6e7ba24578 100644 --- a/ui-ngx/src/app/shared/models/entity-type.models.ts +++ b/ui-ngx/src/app/shared/models/entity-type.models.ts @@ -51,6 +51,7 @@ export enum EntityType { MOBILE_APP_BUNDLE = 'MOBILE_APP_BUNDLE', MOBILE_APP = 'MOBILE_APP', CALCULATED_FIELD = 'CALCULATED_FIELD', + AI_MODEL = 'AI_MODEL', } export enum AliasEntityType { @@ -346,6 +347,8 @@ export const entityTypeTranslations = new Map, HasTenantId { pkgName: string; + title?: string; appSecret: string; platformType: PlatformType; status: MobileAppStatus; diff --git a/ui-ngx/src/app/shared/models/ota-package.models.ts b/ui-ngx/src/app/shared/models/ota-package.models.ts index e40eb15dea..bba7ea3d04 100644 --- a/ui-ngx/src/app/shared/models/ota-package.models.ts +++ b/ui-ngx/src/app/shared/models/ota-package.models.ts @@ -14,7 +14,7 @@ /// limitations under the License. /// -import { BaseData } from '@shared/models/base-data'; +import { BaseData, ExportableEntity } from '@shared/models/base-data'; import { TenantId } from '@shared/models/id/tenant-id'; import { OtaPackageId } from '@shared/models/id/ota-package-id'; import { DeviceProfileId } from '@shared/models/id/device-profile-id'; @@ -86,7 +86,7 @@ export interface OtaPagesIds { softwareId?: OtaPackageId; } -export interface OtaPackageInfo extends BaseData, HasTenantId { +export interface OtaPackageInfo extends Omit, 'label'>, HasTenantId, ExportableEntity { tenantId?: TenantId; type: OtaUpdateType; deviceProfileId?: DeviceProfileId; diff --git a/ui-ngx/src/app/shared/models/public-api.ts b/ui-ngx/src/app/shared/models/public-api.ts index d128eaec43..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'; @@ -63,4 +69,5 @@ export * from './window-message.model'; export * from './usage.models'; export * from './query/query.models'; export * from './regex.constants'; -export * from './trendz-settings.models' +export * from './trendz-settings.models'; +export * from './ai-model.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..c1e692e04f 100644 --- a/ui-ngx/src/app/shared/models/resource.models.ts +++ b/ui-ngx/src/app/shared/models/resource.models.ts @@ -188,6 +188,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 dff6e53654..8225fbf18f 100644 --- a/ui-ngx/src/app/shared/models/rule-node.models.ts +++ b/ui-ngx/src/app/shared/models/rule-node.models.ts @@ -483,8 +483,11 @@ const ruleNodeClazzHelpLinkMap = { 'org.thingsboard.rule.engine.telemetry.TbCalculatedFieldsNode': 'ruleNodeCalculatedFields', 'org.thingsboard.rule.engine.action.TbClearAlarmNode': 'ruleNodeClearAlarm', 'org.thingsboard.rule.engine.action.TbCreateAlarmNode': 'ruleNodeCreateAlarm', + 'org.thingsboard.rule.engine.action.TbCopyAttributesToEntityViewNode': 'ruleNodeCopyToView', 'org.thingsboard.rule.engine.action.TbCreateRelationNode': 'ruleNodeCreateRelation', 'org.thingsboard.rule.engine.action.TbDeleteRelationNode': 'ruleNodeDeleteRelation', + 'org.thingsboard.rule.engine.action.TbDeviceStateNode': 'ruleNodeDeviceState', + 'org.thingsboard.rule.engine.action.TbMsgCountNode': 'ruleNodeMessageCount', 'org.thingsboard.rule.engine.delay.TbMsgDelayNode': 'ruleNodeMsgDelay', 'org.thingsboard.rule.engine.debug.TbMsgGeneratorNode': 'ruleNodeMsgGenerator', 'org.thingsboard.rule.engine.geo.TbGpsGeofencingActionNode': 'ruleNodeGpsGeofencingEvents', @@ -492,9 +495,11 @@ const ruleNodeClazzHelpLinkMap = { 'org.thingsboard.rule.engine.rpc.TbSendRPCReplyNode': 'ruleNodeRpcCallReply', 'org.thingsboard.rule.engine.rpc.TbSendRPCRequestNode': 'ruleNodeRpcCallRequest', 'org.thingsboard.rule.engine.telemetry.TbMsgAttributesNode': 'ruleNodeSaveAttributes', + 'org.thingsboard.rule.engine.telemetry.TbMsgDeleteAttributesNode': 'ruleNodeDeleteAttributes', 'org.thingsboard.rule.engine.telemetry.TbMsgTimeseriesNode': 'ruleNodeSaveTimeseries', 'org.thingsboard.rule.engine.action.TbSaveToCustomCassandraTableNode': 'ruleNodeSaveToCustomTable', 'org.thingsboard.rule.engine.aws.lambda.TbAwsLambdaNode': 'ruleNodeAwsLambda', + 'org.thingsboard.rule.engine.ai.TbAiNode': 'ruleNodeAiRequest', 'org.thingsboard.rule.engine.aws.sns.TbSnsNode': 'ruleNodeAwsSns', 'org.thingsboard.rule.engine.aws.sqs.TbSqsNode': 'ruleNodeAwsSqs', 'org.thingsboard.rule.engine.kafka.TbKafkaNode': 'ruleNodeKafka', @@ -506,6 +511,7 @@ const ruleNodeClazzHelpLinkMap = { 'org.thingsboard.rule.engine.sms.TbSendSmsNode': 'ruleNodeSendSms', 'org.thingsboard.rule.engine.edge.TbMsgPushToCloudNode': 'ruleNodePushToCloud', 'org.thingsboard.rule.engine.edge.TbMsgPushToEdgeNode': 'ruleNodePushToEdge', + 'org.thingsboard.rule.engine.profile.TbDeviceProfileNode': 'ruleNodeDeviceProfile', 'org.thingsboard.rule.engine.flow.TbRuleChainInputNode': 'ruleNodeRuleChain', 'org.thingsboard.rule.engine.flow.TbRuleChainOutputNode': 'ruleNodeOutputNode', 'org.thingsboard.rule.engine.flow.TbAckNode': 'ruleNodeAcknowledge', 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..35ff3a0572 100644 --- a/ui-ngx/src/app/shared/models/time/time.models.ts +++ b/ui-ngx/src/app/shared/models/time/time.models.ts @@ -15,7 +15,7 @@ /// 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'; @@ -281,19 +281,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 +297,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 +314,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 +388,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 +446,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 +456,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, @@ -1098,19 +1096,91 @@ 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 => { + 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) { + 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) { + delete timewindow.history.interval; + } + } + + if (!hasAggregation) { + delete timewindow.aggregation; + } + + 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/units/speed.ts b/ui-ngx/src/app/shared/models/units/speed.ts index b930b0739c..78433c60ae 100644 --- a/ui-ngx/src/app/shared/models/units/speed.ts +++ b/ui-ngx/src/app/shared/models/units/speed.ts @@ -18,8 +18,8 @@ import { TbMeasure, TbMeasureUnits } from '@shared/models/unit.models'; export type SpeedUnits = SpeedMetricUnits | SpeedImperialUnits; -export type SpeedMetricUnits = 'm/s' | 'km/h' | 'mm/min' | 'mm/s'; -export type SpeedImperialUnits = 'mph' | 'kt' | 'ft/s' | 'ft/min' | 'in/h'; +export type SpeedMetricUnits = 'm/s' | 'km/h' | 'mm/min' | 'm/min' | 'mm/s'; +export type SpeedImperialUnits = 'mph' | 'kt' | 'ft/s' | 'ft/min' | 'in/s' | 'in/h'; const METRIC: TbMeasureUnits = { ratio: 1 / 1.609344, @@ -37,6 +37,11 @@ const METRIC: TbMeasureUnits = { 'mm/min': { name: 'unit.millimeters-per-minute', tags: ['feed rate', 'cutting feed rate'], + to_anchor: 0.00006, + }, + 'm/min': { + name: 'unit.meter-per-minute', + tags: ['velocity', 'pace'], to_anchor: 0.06, }, 'mm/s': { @@ -70,6 +75,11 @@ const IMPERIAL: TbMeasureUnits = { tags: ['velocity', 'pace'], to_anchor: 0.0113636, }, + 'in/s': { + name: 'unit.inch-per-second', + tags: ['velocity', 'pace'], + to_anchor: 0.0568182, + }, 'in/h': { name: 'unit.inch-per-hour', tags: ['velocity', 'pace'], diff --git a/ui-ngx/src/app/shared/models/vc.models.ts b/ui-ngx/src/app/shared/models/vc.models.ts index 3795518ffc..9a4a68e005 100644 --- a/ui-ngx/src/app/shared/models/vc.models.ts +++ b/ui-ngx/src/app/shared/models/vc.models.ts @@ -33,16 +33,20 @@ export const exportableEntityTypes: Array = [ EntityType.WIDGET_TYPE, EntityType.WIDGETS_BUNDLE, EntityType.TB_RESOURCE, + EntityType.OTA_PACKAGE, EntityType.NOTIFICATION_TEMPLATE, EntityType.NOTIFICATION_TARGET, - EntityType.NOTIFICATION_RULE + EntityType.NOTIFICATION_RULE, + EntityType.AI_MODEL, ]; -export const entityTypesWithoutRelatedData: Set = new Set([ +export const entityTypesWithoutRelatedData = new Set([ EntityType.NOTIFICATION_TEMPLATE, EntityType.NOTIFICATION_TARGET, EntityType.NOTIFICATION_RULE, - EntityType.TB_RESOURCE + EntityType.TB_RESOURCE, + EntityType.OTA_PACKAGE, + EntityType.AI_MODEL, ]); export interface VersionCreateConfig { diff --git a/ui-ngx/src/app/shared/models/widget.models.ts b/ui-ngx/src/app/shared/models/widget.models.ts index 00b62cfb72..01ac1b2aaa 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,6 +631,30 @@ 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', @@ -675,6 +708,7 @@ export interface MobileLaunchResult { export interface MobileImageResult { imageUrl: string; + imageInfo?: ImageResourceInfo; } export interface MobileQrCodeResult { @@ -706,10 +740,12 @@ export interface WidgetMobileActionResult { export interface ProvisionSuccessDescriptor { handleProvisionSuccessFunction: TbFunction; + provisionType?: string; } export interface ProcessImageDescriptor { processImageFunction: TbFunction; + saveToGallery?: boolean; } export interface ProcessLaunchResultDescriptor { @@ -743,6 +779,7 @@ export interface WidgetMobileActionDescriptor extends WidgetMobileActionDescript type: WidgetMobileActionType; handleErrorFunction?: TbFunction; handleEmptyResultFunction?: TbFunction; + handleNonMobileFallbackFunction?: TbFunction; } export interface CustomActionDescriptor { 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..8013889017 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,13 +17,18 @@ 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 { DataKey, Datasource, datasourcesHasAggregation, DatasourceType, Widget } from '@shared/models/widget.models'; import { + additionalMapDataSourcesToDatasources, BaseMapSettings, + CirclesDataLayerSettings, MapDataLayerSettings, + MapDataLayerType, MapDataSourceSettings, mapDataSourceSettingsToDatasource, - MapType + MapType, + MarkersDataLayerSettings, + PolygonsDataLayerSettings } from '@shared/models/widget/maps/map.models'; import { WidgetModelDefinition } from '@shared/models/widget/widget-model.definition'; @@ -124,6 +129,27 @@ export const MapModelDefinition: WidgetModelDefinition = { datasources.push(...getMapDataLayersDatasources(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(...getMapLatestDataLayersDatasources(settings.markers, 'markers')); + } + if (settings.polygons?.length) { + datasources.push(...getMapLatestDataLayersDatasources(settings.polygons, 'polygons')); + } + if (settings.circles?.length) { + datasources.push(...getMapLatestDataLayersDatasources(settings.circles, 'circles')); + } + if (settings.additionalDataSources?.length) { + datasources.push(...additionalMapDataSourcesToDatasources(settings.additionalDataSources)); + } + return datasourcesHasAggregation(datasources); + } } }; @@ -223,3 +249,40 @@ const getMapDataLayersDatasources = (settings: MapDataLayerSettings[] | MapDataS }); return datasources; }; + +const getMapLatestDataLayersDatasources = (settings: MapDataLayerSettings[], + dataLayerType: MapDataLayerType): Datasource[] => { + const datasources: Datasource[] = []; + settings.forEach((dsSettings) => { + const dataKeys: DataKey[] = getMapLatestDataLayerDatasourceDataKeys(dsSettings, dataLayerType); + const datasource: Datasource = mapDataSourceSettingsToDatasource(dsSettings); + datasource.dataKeys.push(...dataKeys); + datasources.push(datasource); + if ((dsSettings).additionalDataSources?.length) { + (dsSettings).additionalDataSources.forEach((ds) => { + const additionalDatasource: Datasource = mapDataSourceSettingsToDatasource(ds); + additionalDatasource.dataKeys.push(...dataKeys); + datasources.push(additionalDatasource); + }); + } + }); + return datasources; +}; + +const getMapLatestDataLayerDatasourceDataKeys = (settings: MapDataLayerSettings, + dataLayerType: MapDataLayerType): DataKey[] => { + const dataKeys = settings.additionalDataKeys || []; + switch (dataLayerType) { + 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; + } + return dataKeys; +}; 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..21dc801b3f 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 @@ -25,6 +25,7 @@ export interface WidgetModelDefinition { 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[] = [ 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..3e414577ec 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; @@ -443,6 +444,7 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) ScadaSymbolInputComponent, EntityKeyAutocompleteComponent, MqttVersionSelectComponent, + TimeUnitInputComponent, ], imports: [ CommonModule, @@ -707,6 +709,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..3fc49e171f 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, @@ -121,395 +120,2995 @@ "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" - }, - "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" + "labelColor": "rgba(0, 0, 0, 0.54)", + "position": "bottom", + "showTickLabels": true, + "tickLabelFont": { + "family": "Roboto", + "size": 10, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" }, - { - "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" - } - ] - } - }, - "row": 0, + "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.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": "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 + }, + { + "name": "tmpTimeout", + "type": "timeseries", + "label": "{i18n:api-usage.processing-timeouts}", + "color": "#9c27b0", + "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": 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 + } + ] + } + ], + "timewindow": { + "hideAggregation": false, + "hideAggInterval": false, + "selectedTab": 0, + "realtime": { + "timewindowMs": 3600000, + "interval": 1000 + }, + "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": { + "hideAggregation": false, + "hideAggInterval": false, + "hideTimezone": false, + "selectedTab": 0, + "realtime": { + "realtimeType": 0, + "interval": 3600000, + "timewindowMs": 86400000, + "quickInterval": "CURRENT_DAY", + "hideInterval": false, + "hideLastInterval": false, + "hideQuickInterval": false + }, + "history": { + "historyType": 0, + "interval": 1000, + "timewindowMs": 60000, + "fixedTimewindow": null, + "quickInterval": "CURRENT_DAY", + "hideInterval": false, + "hideLastInterval": false, + "hideFixedInterval": false, + "hideQuickInterval": false + }, + "aggregation": { + "type": "SUM", + "limit": 50000 + }, + "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": 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": { + "hideAggregation": false, + "hideAggInterval": false, + "hideTimezone": false, + "selectedTab": 0, + "realtime": { + "realtimeType": 0, + "interval": 3600000, + "timewindowMs": 86400000, + "quickInterval": "CURRENT_DAY", + "hideInterval": false, + "hideLastInterval": false, + "hideQuickInterval": false + }, + "history": { + "historyType": 0, + "interval": 1000, + "timewindowMs": 60000, + "fixedTimewindow": null, + "quickInterval": "CURRENT_DAY", + "hideInterval": false, + "hideLastInterval": false, + "hideFixedInterval": false, + "hideQuickInterval": false + }, + "aggregation": { + "type": "SUM", + "limit": 50000 + }, + "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": 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": { + "hideAggregation": false, + "hideAggInterval": false, + "hideTimezone": false, + "selectedTab": 0, + "realtime": { + "realtimeType": 0, + "interval": 3600000, + "timewindowMs": 86400000, + "quickInterval": "CURRENT_YEAR", + "hideInterval": false, + "hideLastInterval": false, + "hideQuickInterval": false + }, + "history": { + "historyType": 0, + "interval": 1000, + "timewindowMs": 60000, + "fixedTimewindow": null, + "quickInterval": "CURRENT_DAY", + "hideInterval": false, + "hideLastInterval": false, + "hideFixedInterval": false, + "hideQuickInterval": false + }, + "aggregation": { + "type": "SUM", + "limit": 50000 + }, + "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": 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.rule-engine-hourly-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" + } + ] + }, + "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": "4544080d-9b6f-b592-9cd4-0e0335d33857" + }, + "5d0f2f57-499d-1324-8e1b-cfbc0b3149d2": { + "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": "storageDataPointsCountHourly", + "type": "timeseries", + "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": null, + "postFuncBody": null, + "aggregationType": null + } + ], + "alarmFilterConfig": { + "statusList": [ + "ACTIVE" + ] + } + } + ], + "timewindow": { + "hideAggregation": false, + "hideAggInterval": false, + "hideTimezone": false, + "selectedTab": 0, + "realtime": { + "realtimeType": 0, + "interval": 3600000, + "timewindowMs": 86400000, + "quickInterval": "CURRENT_DAY", + "hideInterval": false, + "hideLastInterval": false, + "hideQuickInterval": false + }, + "history": { + "historyType": 0, + "interval": 1000, + "timewindowMs": 60000, + "fixedTimewindow": null, + "quickInterval": "CURRENT_DAY", + "hideInterval": false, + "hideLastInterval": false, + "hideFixedInterval": false, + "hideQuickInterval": false + }, + "aggregation": { + "type": "SUM", + "limit": 50000 + }, + "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": 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.telemetry-persistence-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": "5d0f2f57-499d-1324-8e1b-cfbc0b3149d2" + }, + "51608a74-f213-d8c9-8df8-b42238ef93a6": { + "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": null, + "postFuncBody": null + } + ], + "alarmFilterConfig": { + "statusList": [ + "ACTIVE" + ] + } + } + ], + "timewindow": { + "hideAggregation": false, + "hideAggInterval": false, + "hideTimezone": false, + "selectedTab": 0, + "realtime": { + "realtimeType": 0, + "interval": 3600000, + "timewindowMs": 86400000, + "quickInterval": "CURRENT_DAY", + "hideInterval": false, + "hideLastInterval": false, + "hideQuickInterval": false + }, + "history": { + "historyType": 0, + "interval": 86400000, + "timewindowMs": 2592000000, + "fixedTimewindow": null, + "quickInterval": "CURRENT_DAY", + "hideInterval": false, + "hideLastInterval": false, + "hideFixedInterval": false, + "hideQuickInterval": false + }, + "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" + }, + "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-msg-hourly-activity}", + "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": "51608a74-f213-d8c9-8df8-b42238ef93a6" }, - "d70d26d4-e22d-4ca9-9ea7-f9c87c093321": { - "typeFullFqn": "system.cards.markdown_card", - "type": "latest", - "sizeX": 5, - "sizeY": 3.5, + "fb155957-1af4-233e-e2fb-09e648e75d6e": { + "typeFullFqn": "system.time_series_chart", + "type": "timeseries", + "sizeX": 8, + "sizeY": 5, "config": { "datasources": [ { @@ -519,130 +3118,40 @@ "filterId": null, "dataKeys": [ { - "name": "jsExecutionApiState", + "name": "transportMsgCountHourly", "type": "timeseries", - "label": "jsApiState", + "label": "{i18n:api-usage.transport-messages}", "color": "#2196f3", - "settings": {}, - "_hash": 0.8830669138660703, - "units": null, - "decimals": null, - "funcBody": null, - "usePostProcessing": true, - "postFuncBody": "return value ? value : 'ENABLED';", - "aggregationType": "NONE" - }, - { - "name": "jsExecutionLimit", - "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", - "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": { + "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": true, - "postFuncBody": "return \"{i18n:api-usage.tbel}\";" + "usePostProcessing": null, + "postFuncBody": null } ], "alarmFilterConfig": { @@ -650,89 +3159,317 @@ "ACTIVE" ] } - } - ], - "timewindow": { - "displayValue": "", - "selectedTab": 0, - "realtime": { - "realtimeType": 1, - "interval": 1000, - "timewindowMs": 60000, - "quickInterval": "CURRENT_DAY" + } + ], + "timewindow": { + "hideAggregation": false, + "hideAggInterval": false, + "hideTimezone": false, + "selectedTab": 1, + "history": { + "historyType": 0, + "timewindowMs": 2592000000, + "interval": 86400000, + "fixedTimewindow": { + "startTimeMs": 1709729389667, + "endTimeMs": 1709815789667 + }, + "quickInterval": "CURRENT_DAY" + }, + "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 + } }, - "history": { - "historyType": 0, - "interval": 1000, - "timewindowMs": 60000, - "fixedTimewindow": { - "startTimeMs": 1708518962586, - "endTimeMs": 1708605362586 + "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 [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" + "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": "JavaScript functions", + "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": "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" - } - ] - } + "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": "d70d26d4-e22d-4ca9-9ea7-f9c87c093321" + "id": "fb155957-1af4-233e-e2fb-09e648e75d6e" }, - "4d3ea95c-3188-9872-1817-2f989c7729e0": { - "typeFullFqn": "system.cards.markdown_card", - "type": "latest", - "sizeX": 5, - "sizeY": 3.5, + "4817e33b-87be-5be3-eaca-ca68a2eb4e0c": { + "typeFullFqn": "system.time_series_chart", + "type": "timeseries", + "sizeX": 8, + "sizeY": 5, "config": { "datasources": [ { @@ -742,158 +3479,363 @@ "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" - }, - { - "name": "storageDataPointsCount", - "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": "dbApiState", - "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": "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", + "name": "transportMsgCountHourly", "type": "timeseries", - "label": "unit", - "color": "#8bc34a", - "settings": {}, - "_hash": 0.0027742924142306613, + "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": true, - "postFuncBody": "return \"{i18n:api-usage.data-points-storage-days}\";" - } - ], - "alarmFilterConfig": { - "statusList": [ - "ACTIVE" - ] - } + "usePostProcessing": null, + "postFuncBody": null + } + ] } ], "timewindow": { - "displayValue": "", - "selectedTab": 0, + "hideAggregation": false, + "hideAggInterval": false, + "hideTimezone": false, + "selectedTab": 1, "realtime": { - "realtimeType": 1, + "realtimeType": 0, "interval": 1000, "timewindowMs": 60000, - "quickInterval": "CURRENT_DAY" + "quickInterval": "CURRENT_DAY", + "hideInterval": false, + "hideLastInterval": false, + "hideQuickInterval": false }, "history": { "historyType": 0, - "interval": 1000, - "timewindowMs": 60000, - "fixedTimewindow": { - "startTimeMs": 1708518962586, - "endTimeMs": 1708605362586 - }, - "quickInterval": "CURRENT_DAY" + "interval": 2592000000, + "timewindowMs": 31536000000, + "fixedTimewindow": null, + "quickInterval": "CURRENT_DAY", + "hideInterval": false, + "hideLastInterval": false, + "hideFixedInterval": false, + "hideQuickInterval": false }, "aggregation": { - "type": "AVG", + "type": "NONE", "limit": 25000 - } + }, + "timezone": null }, - "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": 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" + }, + "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.transport-msg-monthly-activity}", "dropShadow": true, - "enableFullscreen": false, - "widgetStyle": {}, - "titleStyle": { - "fontSize": "16px", - "fontWeight": 400 + "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" }, - "showLegend": false, - "useDashboardTimewindow": true, - "displayTimewindow": true, + "titleColor": "rgba(0, 0, 0, 0.87)", + "titleTooltip": "", + "widgetStyle": {}, "widgetCss": "", "pageSize": 1024, + "units": "", + "decimals": null, "noDataDisplayMessage": "", - "actions": { - "elementClick": [ - { - "name": "telemetry_persistence_details", - "icon": "insert_chart", - "type": "openDashboardState", - "targetDashboardStateId": "telemetry_persistence", - "setEntityId": false, - "stateEntityParamName": null, - "openRightLayout": false, - "id": "6248831c-5b3f-8879-8548-afcf43f10610" - } - ] - } + "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": "4817e33b-87be-5be3-eaca-ca68a2eb4e0c" }, - "2d0d6ff6-cd59-51d4-b916-38e22cdd0702": { - "typeFullFqn": "system.cards.markdown_card", - "type": "latest", - "sizeX": 5, - "sizeY": 3.5, + "9e00cc90-520d-2108-1d2f-bba68ed5cbf1": { + "typeFullFqn": "system.time_series_chart", + "type": "timeseries", + "sizeX": 8, + "sizeY": 5, "config": { "datasources": [ { @@ -903,72 +3845,42 @@ "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": "transportDataPointsCountHourly", "type": "timeseries", - "label": "unit", - "color": "#8bc34a", - "settings": {}, - "_hash": 0.9964061963495883, + "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", + "yAxisId": "default" + }, + "_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,90 +3888,327 @@ "ACTIVE" ] } - } - ], - "timewindow": { - "displayValue": "", - "selectedTab": 0, - "realtime": { - "realtimeType": 1, - "interval": 1000, - "timewindowMs": 60000, - "quickInterval": "CURRENT_DAY" + } + ], + "timewindow": { + "hideAggregation": false, + "hideAggInterval": false, + "hideTimezone": false, + "selectedTab": 0, + "realtime": { + "realtimeType": 0, + "interval": 3600000, + "timewindowMs": 86400000, + "quickInterval": "CURRENT_DAY", + "hideInterval": false, + "hideLastInterval": false, + "hideQuickInterval": false + }, + "history": { + "historyType": 0, + "interval": 86400000, + "timewindowMs": 2592000000, + "fixedTimewindow": null, + "quickInterval": "CURRENT_DAY", + "hideInterval": false, + "hideLastInterval": false, + "hideFixedInterval": false, + "hideQuickInterval": false + }, + "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 + } }, - "history": { - "historyType": 0, - "interval": 1000, - "timewindowMs": 60000, - "fixedTimewindow": { - "startTimeMs": 1708518962586, - "endTimeMs": 1708605362586 + "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 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" + "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": "Alarm created", + "title": "{i18n:api-usage.transport-data-points-hourly-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": "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" + "id": "9e00cc90-520d-2108-1d2f-bba68ed5cbf1" }, - "120573cc-e246-eb49-7d80-68e5d3b3c0cc": { - "typeFullFqn": "system.cards.markdown_card", - "type": "latest", - "sizeX": 5, - "sizeY": 3.5, + "79056202-c92b-1dae-ce49-318ec52e2d3b": { + "typeFullFqn": "system.time_series_chart", + "type": "timeseries", + "sizeX": 8, + "sizeY": 5, "config": { "datasources": [ { @@ -1069,128 +4218,42 @@ "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" - }, - { - "name": "smsApiState", - "type": "timeseries", - "label": "apiStatePoint", - "color": "#e91e63", - "settings": {}, - "_hash": 0.2969682764607864, - "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", + "name": "transportDataPointsCountHourly", "type": "timeseries", - "label": "pointsUnit", - "color": "#e91e63", - "settings": {}, - "_hash": 0.12117146988088967, - "aggregationType": "NONE", + "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", + "yAxisId": "default" + }, + "_hash": 0.0661644137210089, "units": null, "decimals": null, "funcBody": null, - "usePostProcessing": true, - "postFuncBody": "return '{i18n:api-usage.sms}';" + "usePostProcessing": null, + "postFuncBody": null, + "aggregationType": null } ], "alarmFilterConfig": { @@ -1201,83 +4264,310 @@ } ], "timewindow": { - "displayValue": "", - "selectedTab": 0, - "realtime": { - "realtimeType": 1, - "interval": 1000, - "timewindowMs": 60000, - "quickInterval": "CURRENT_DAY" - }, + "hideAggregation": false, + "hideAggInterval": false, + "hideTimezone": false, + "selectedTab": 1, "history": { "historyType": 0, - "interval": 1000, - "timewindowMs": 60000, + "timewindowMs": 2592000000, + "interval": 86400000, "fixedTimewindow": { - "startTimeMs": 1708518962586, - "endTimeMs": 1708605362586 + "startTimeMs": 1709729389667, + "endTimeMs": 1709815789667 }, "quickInterval": "CURRENT_DAY" }, "aggregation": { - "type": "AVG", + "type": "SUM", "limit": 25000 - } + }, + "timezone": null }, - "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\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" + "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" + }, + "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-points-daily-activity}", + "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" }, - "title": "Notifications (Email/SMS)", - "showTitleIcon": false, - "iconColor": "rgba(0, 0, 0, 0.87)", - "iconSize": "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": "79056202-c92b-1dae-ce49-318ec52e2d3b" }, - "63f99d90-23ab-f8c2-3290-1e693ded5a2e": { + "966ffee7-ba0d-8e54-f903-e8d015ca8cd2": { "typeFullFqn": "system.time_series_chart", "type": "timeseries", "sizeX": 8, @@ -1290,73 +4580,82 @@ "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 - }, { "name": "transportDataPointsCountHourly", "type": "timeseries", "label": "{i18n:api-usage.transport-data-points}", - "color": "#4caf50", + "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" + "yAxisId": "default", + "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)", + "enablePointLabelBackground": false, + "pointLabelBackground": "rgba(255,255,255,0.56)", + "pointShape": "emptyCircle", + "pointSize": 4, + "fillAreaSettings": { + "type": "none", + "opacity": 0.4, + "gradient": { + "start": 100, + "end": 0 + } } - ], - "comparisonSettings": { - "showValuesForComparison": true }, - "type": "bar" + "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)", + "enableLabelBackground": false, + "labelBackground": "rgba(255,255,255,0.56)", + "backgroundSettings": { + "type": "none", + "opacity": 0.4, + "gradient": { + "start": 100, + "end": 0 + } + } + }, + "comparisonSettings": { + "showValuesForComparison": false, + "comparisonValuesLabel": "", + "color": "" + } }, - "_hash": 0.46849996721308895, + "_hash": 0.12814821361119078, + "aggregationType": null, "units": null, "decimals": null, "funcBody": null, @@ -1372,22 +4671,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 }, @@ -1460,7 +4770,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 +4782,9 @@ "noAggregationBarWidthSettings": { "strategy": "group", "groupWidth": { - "relative": false, - "relativeWidth": 2, - "absoluteWidth": 3600000 + "relative": true, + "relativeWidth": 6, + "absoluteWidth": 1800000 }, "barWidth": { "relative": true, @@ -1499,76 +4810,135 @@ "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)" }, - "showTooltip": true, - "tooltipTrigger": "axis", - "tooltipValueFont": { + "grid": { + "show": false, + "backgroundColor": null, + "borderWidth": 1, + "borderColor": "#ccc" + }, + "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-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", - "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 +4977,14 @@ "displayTypePrefix": true }, "margin": "0px", - "borderRadius": "0px", + "borderRadius": "4px", "iconSize": "0px" }, "row": 0, "col": 0, - "id": "63f99d90-23ab-f8c2-3290-1e693ded5a2e" + "id": "966ffee7-ba0d-8e54-f903-e8d015ca8cd2" }, - "a2b7e906-2d8a-41a8-99a6-409531bfa743": { + "b1a9a51f-e5a6-9d5f-ef5c-25c2a68af1b0": { "typeFullFqn": "system.time_series_chart", "type": "timeseries", "sizeX": 8, @@ -1631,8 +5001,9 @@ "name": "ruleEngineExecutionCountHourly", "type": "timeseries", "label": "{i18n:api-usage.rule-engine-executions}", - "color": "#ab00ff", + "color": "#AB00FF", "settings": { + "yAxisId": "default", "showInLegend": true, "dataHiddenByDefault": false, "type": "bar", @@ -1655,6 +5026,8 @@ "lineHeight": "1" }, "pointLabelColor": "rgba(0, 0, 0, 0.76)", + "enablePointLabelBackground": false, + "pointLabelBackground": "rgba(255,255,255,0.56)", "pointShape": "emptyCircle", "pointSize": 4, "fillAreaSettings": { @@ -1681,6 +5054,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, @@ -1689,15 +5064,20 @@ "end": 0 } } + }, + "comparisonSettings": { + "showValuesForComparison": false, + "comparisonValuesLabel": "", + "color": "" } }, - "_hash": 0.0661644137210089, + "_hash": 0.5078724779454146, + "aggregationType": null, "units": null, "decimals": null, "funcBody": null, "usePostProcessing": null, - "postFuncBody": null, - "aggregationType": null + "postFuncBody": null } ], "alarmFilterConfig": { @@ -1708,22 +5088,33 @@ } ], "timewindow": { - "hideInterval": false, - "hideLastInterval": false, - "hideQuickInterval": false, "hideAggregation": false, "hideAggInterval": false, "hideTimezone": false, "selectedTab": 0, "realtime": { "realtimeType": 0, + "interval": 3600000, "timewindowMs": 86400000, - "quickInterval": "CURRENT_YEAR", - "interval": 7200000 + "quickInterval": "CURRENT_DAY", + "hideInterval": false, + "hideLastInterval": false, + "hideQuickInterval": false + }, + "history": { + "historyType": 0, + "interval": 86400000, + "timewindowMs": 2592000000, + "fixedTimewindow": null, + "quickInterval": "CURRENT_DAY", + "hideInterval": false, + "hideLastInterval": false, + "hideFixedInterval": false, + "hideQuickInterval": false }, "aggregation": { - "type": "NONE", - "limit": 50000 + "type": "SUM", + "limit": 25000 }, "timezone": null }, @@ -1797,6 +5188,7 @@ "lineHeight": "1" }, "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, "showTicks": true, "ticksColor": "rgba(0, 0, 0, 0.54)", "showLine": true, @@ -1807,9 +5199,9 @@ "noAggregationBarWidthSettings": { "strategy": "group", "groupWidth": { - "relative": false, - "relativeWidth": 2, - "absoluteWidth": 3600000 + "relative": true, + "relativeWidth": 6, + "absoluteWidth": 1800000 }, "barWidth": { "relative": true, @@ -1835,7 +5227,8 @@ "showMax": false, "showAvg": false, "showTotal": true, - "showLatest": false + "showLatest": false, + "valueFormat": null }, "showTooltip": true, "tooltipTrigger": "axis", @@ -1852,7 +5245,9 @@ "tooltipDateFormat": { "format": "yyyy-MM-dd HH:mm:ss", "lastUpdateAgo": false, - "custom": false + "custom": false, + "auto": true, + "autoDateFormatSettings": {} }, "tooltipDateFont": { "family": "Roboto", @@ -1884,7 +5279,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.rule-engine-hourly-activity}", "dropShadow": true, @@ -1895,23 +5359,20 @@ "headerButton": [ { "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": "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, + "setEntityId": true, "stateEntityParamName": null, "openRightLayout": false, - "id": "1aec196b-44ba-ddf4-c4dc-c3f60c1eb6fc" + "openInSeparateDialog": false, + "openInPopover": false, + "id": "8b57e118-84fc-4add-2536-d3cfde018b83" } ] }, @@ -1932,125 +5393,54 @@ "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": "a2b7e906-2d8a-41a8-99a6-409531bfa743" - }, - "ca996b66-ab7e-f977-152c-98e4ebf2a901": { - "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": "jsExecutionCountHourly", - "type": "timeseries", - "label": "{i18n:api-usage.javascript-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 - } - } - }, - "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 - }, + "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": "b1a9a51f-e5a6-9d5f-ef5c-25c2a68af1b0" + }, + "84fbe63a-bcb6-7bc1-8af0-46b3b1ee5adc": { + "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": "tbelExecutionCountHourly", + "name": "ruleEngineExecutionCountHourly", "type": "timeseries", - "label": "{i18n:api-usage.tbel-executions}", - "color": "#4caf50", + "label": "{i18n:api-usage.rule-engine-executions}", + "color": "#AB00FF", "settings": { + "yAxisId": "default", "showInLegend": true, "dataHiddenByDefault": false, "type": "bar", @@ -2073,6 +5463,8 @@ "lineHeight": "1" }, "pointLabelColor": "rgba(0, 0, 0, 0.76)", + "enablePointLabelBackground": false, + "pointLabelBackground": "rgba(255,255,255,0.56)", "pointShape": "emptyCircle", "pointSize": 4, "fillAreaSettings": { @@ -2099,6 +5491,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, @@ -2107,9 +5501,14 @@ "end": 0 } } + }, + "comparisonSettings": { + "showValuesForComparison": false, + "comparisonValuesLabel": "", + "color": "" } }, - "_hash": 0.6818645685001823, + "_hash": 0.01948850513940492, "aggregationType": null, "units": null, "decimals": null, @@ -2126,22 +5525,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": 3600000 + "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 }, @@ -2215,6 +5615,7 @@ "lineHeight": "1" }, "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, "showTicks": true, "ticksColor": "rgba(0, 0, 0, 0.54)", "showLine": true, @@ -2225,9 +5626,9 @@ "noAggregationBarWidthSettings": { "strategy": "group", "groupWidth": { - "relative": false, - "relativeWidth": 2, - "absoluteWidth": 3600000 + "relative": true, + "relativeWidth": 6, + "absoluteWidth": 1800000 }, "barWidth": { "relative": true, @@ -2253,7 +5654,8 @@ "showMax": false, "showAvg": false, "showTotal": true, - "showLatest": false + "showLatest": false, + "valueFormat": null }, "showTooltip": true, "tooltipTrigger": "axis", @@ -2270,7 +5672,9 @@ "tooltipDateFormat": { "format": "yyyy-MM-dd HH:mm:ss", "lastUpdateAgo": false, - "custom": false + "custom": false, + "auto": true, + "autoDateFormatSettings": {} }, "tooltipDateFont": { "family": "Roboto", @@ -2302,9 +5706,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-hourly-activity}", + "title": "{i18n:api-usage.rule-engine-daily-activity}", "dropShadow": true, "enableFullscreen": true, "titleStyle": null, @@ -2312,18 +5785,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": "script_functions", - "setEntityId": false, + "targetDashboardStateId": "rule_engine_statistics", + "setEntityId": true, "stateEntityParamName": null, "openRightLayout": false, "openInSeparateDialog": false, "openInPopover": false, - "id": "4687d3f6-8800-a3b6-26e5-0d33f3b828a9" + "id": "2592147a-3f62-987a-78c0-cdb775fb4233" } ] }, @@ -2365,14 +5841,14 @@ "displayTypePrefix": true }, "margin": "0px", - "borderRadius": "0px", + "borderRadius": "4px", "iconSize": "0px" }, "row": 0, "col": 0, - "id": "ca996b66-ab7e-f977-152c-98e4ebf2a901" + "id": "84fbe63a-bcb6-7bc1-8af0-46b3b1ee5adc" }, - "a3c2f1bb-7d3a-f11c-7b3d-28cd84fdfe34": { + "43a2b982-6c02-d9bd-71ee-34e8e6cf8893": { "typeFullFqn": "system.time_series_chart", "type": "timeseries", "sizeX": 8, @@ -2386,11 +5862,12 @@ "filterId": null, "dataKeys": [ { - "name": "storageDataPointsCountHourly", + "name": "ruleEngineExecutionCount", "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 +5890,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 +5918,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 +5928,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": { @@ -2466,22 +5952,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 }, @@ -2555,6 +6052,7 @@ "lineHeight": "1" }, "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, "showTicks": true, "ticksColor": "rgba(0, 0, 0, 0.54)", "showLine": true, @@ -2565,9 +6063,9 @@ "noAggregationBarWidthSettings": { "strategy": "group", "groupWidth": { - "relative": false, - "relativeWidth": 2, - "absoluteWidth": 3600000 + "relative": true, + "relativeWidth": 6, + "absoluteWidth": 1800000 }, "barWidth": { "relative": true, @@ -2593,7 +6091,8 @@ "showMax": false, "showAvg": false, "showTotal": true, - "showLatest": false + "showLatest": false, + "valueFormat": null }, "showTooltip": true, "tooltipTrigger": "axis", @@ -2610,7 +6109,9 @@ "tooltipDateFormat": { "format": "yyyy-MM-dd HH:mm:ss", "lastUpdateAgo": false, - "custom": false + "custom": false, + "auto": true, + "autoDateFormatSettings": {} }, "tooltipDateFont": { "family": "Roboto", @@ -2642,9 +6143,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-monthly-activity}", "dropShadow": true, "enableFullscreen": true, "titleStyle": null, @@ -2652,14 +6222,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": "b6ba96cf-48b8-f40f-f010-10b95e7dc819" } ] }, @@ -2701,14 +6278,14 @@ "displayTypePrefix": true }, "margin": "0px", - "borderRadius": "0px", + "borderRadius": "4px", "iconSize": "0px" }, "row": 0, "col": 0, - "id": "a3c2f1bb-7d3a-f11c-7b3d-28cd84fdfe34" + "id": "43a2b982-6c02-d9bd-71ee-34e8e6cf8893" }, - "5cebd4f1-ff6e-62f9-025c-8e7583c3d66a": { + "76fe83c9-c30f-00a5-6299-40c759ca6705": { "typeFullFqn": "system.time_series_chart", "type": "timeseries", "sizeX": 8, @@ -2722,68 +6299,34 @@ "filterId": null, "dataKeys": [ { - "name": "createdAlarmsCountHourly", + "name": "jsExecutionCountHourly", "type": "timeseries", - "label": "{i18n:api-usage.alarms-created}", - "color": "#d35a00", + "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 - } - } - } + "type": "bar", + "yAxisId": "default" }, "_hash": 0.0661644137210089, "units": null, @@ -2802,22 +6345,33 @@ } ], "timewindow": { - "hideInterval": false, - "hideLastInterval": false, - "hideQuickInterval": false, "hideAggregation": false, "hideAggInterval": false, "hideTimezone": false, "selectedTab": 0, "realtime": { "realtimeType": 0, + "interval": 3600000, "timewindowMs": 86400000, "quickInterval": "CURRENT_DAY", - "interval": 300000 + "hideInterval": false, + "hideLastInterval": false, + "hideQuickInterval": false + }, + "history": { + "historyType": 0, + "interval": 86400000, + "timewindowMs": 2592000000, + "fixedTimewindow": null, + "quickInterval": "CURRENT_DAY", + "hideInterval": false, + "hideLastInterval": false, + "hideFixedInterval": false, + "hideQuickInterval": false }, "aggregation": { - "type": "NONE", - "limit": 50000 + "type": "SUM", + "limit": 25000 }, "timezone": null }, @@ -2891,6 +6445,7 @@ "lineHeight": "1" }, "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, "showTicks": true, "ticksColor": "rgba(0, 0, 0, 0.54)", "showLine": true, @@ -2901,9 +6456,9 @@ "noAggregationBarWidthSettings": { "strategy": "group", "groupWidth": { - "relative": false, - "relativeWidth": 2, - "absoluteWidth": 3600000 + "relative": true, + "relativeWidth": 6, + "absoluteWidth": 1800000 }, "barWidth": { "relative": true, @@ -2929,7 +6484,8 @@ "showMax": false, "showAvg": false, "showTotal": true, - "showLatest": false + "showLatest": false, + "valueFormat": null }, "showTooltip": true, "tooltipTrigger": "axis", @@ -2946,7 +6502,9 @@ "tooltipDateFormat": { "format": "yyyy-MM-dd HH:mm:ss", "lastUpdateAgo": false, - "custom": false + "custom": false, + "auto": true, + "autoDateFormatSettings": {} }, "tooltipDateFont": { "family": "Roboto", @@ -2978,32 +6536,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.alarms-created-hourly-activity}", + "title": "{i18n:api-usage.javascript-function-executions-hourly-activity}", "dropShadow": true, "enableFullscreen": true, "titleStyle": null, "configMode": "basic", - "actions": { - "headerButton": [ - { - "name": "{i18n:api-usage.view-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": "371882f9-ea23-3abc-fca8-9449c5dfdd6b" - } - ] - }, + "actions": {}, "showTitleIcon": false, "titleIcon": "thermostat", "iconColor": "#1F6BDD", @@ -3042,14 +6651,14 @@ "displayTypePrefix": true }, "margin": "0px", - "borderRadius": "0px", + "borderRadius": "4px", "iconSize": "0px" }, "row": 0, "col": 0, - "id": "5cebd4f1-ff6e-62f9-025c-8e7583c3d66a" + "id": "76fe83c9-c30f-00a5-6299-40c759ca6705" }, - "bc0c8840-a9b5-5583-de7b-9e9450f5d8fe": { + "a43598d1-7bfd-f329-ee61-c343f34f069f": { "typeFullFqn": "system.time_series_chart", "type": "timeseries", "sizeX": 8, @@ -3063,140 +6672,34 @@ "filterId": null, "dataKeys": [ { - "name": "emailCountHourly", - "type": "timeseries", - "label": "{i18n:api-usage.email-messages}", - "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 - } - } - } - }, - "_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 - } + "name": "jsExecutionCountHourly", + "type": "timeseries", + "label": "{i18n:api-usage.javascript-function-executions}", + "color": "#FF9900", + "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", + "yAxisId": "default" }, "_hash": 0.0661644137210089, "units": null, @@ -3215,22 +6718,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 }, @@ -3304,6 +6808,7 @@ "lineHeight": "1" }, "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, "showTicks": true, "ticksColor": "rgba(0, 0, 0, 0.54)", "showLine": true, @@ -3314,9 +6819,9 @@ "noAggregationBarWidthSettings": { "strategy": "group", "groupWidth": { - "relative": false, - "relativeWidth": 2, - "absoluteWidth": 3600000 + "relative": true, + "relativeWidth": 6, + "absoluteWidth": 1800000 }, "barWidth": { "relative": true, @@ -3342,7 +6847,8 @@ "showMax": false, "showAvg": false, "showTotal": true, - "showLatest": false + "showLatest": false, + "valueFormat": null }, "showTooltip": true, "tooltipTrigger": "axis", @@ -3359,7 +6865,9 @@ "tooltipDateFormat": { "format": "yyyy-MM-dd HH:mm:ss", "lastUpdateAgo": false, - "custom": false + "custom": false, + "auto": true, + "autoDateFormatSettings": {} }, "tooltipDateFont": { "family": "Roboto", @@ -3391,32 +6899,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.notifications-hourly-activity}", + "title": "{i18n:api-usage.javascript-function-executions-daily-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" - } - ] - }, + "actions": {}, "showTitleIcon": false, "titleIcon": "thermostat", "iconColor": "#1F6BDD", @@ -3455,14 +7014,14 @@ "displayTypePrefix": true }, "margin": "0px", - "borderRadius": "0px", + "borderRadius": "4px", "iconSize": "0px" }, "row": 0, "col": 0, - "id": "bc0c8840-a9b5-5583-de7b-9e9450f5d8fe" + "id": "a43598d1-7bfd-f329-ee61-c343f34f069f" }, - "0b091dc3-eec3-847e-d0ad-fdf12d474e7a": { + "3ebd62a8-dcb7-c96b-8571-e61084248f5b": { "typeFullFqn": "system.time_series_chart", "type": "timeseries", "sizeX": 8, @@ -3476,10 +7035,10 @@ "filterId": null, "dataKeys": [ { - "name": "transportMsgCountHourly", + "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, @@ -3502,74 +7061,47 @@ "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 } ] } ], "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, - "timewindowMs": 2592000000, - "interval": 86400000, - "fixedTimewindow": { - "startTimeMs": 1709729389667, - "endTimeMs": 1709815789667 - }, - "quickInterval": "CURRENT_DAY" + "interval": 2592000000, + "timewindowMs": 31536000000, + "fixedTimewindow": null, + "quickInterval": "CURRENT_DAY", + "hideInterval": false, + "hideLastInterval": false, + "hideFixedInterval": false, + "hideQuickInterval": false }, "aggregation": { - "type": "SUM", + "type": "NONE", "limit": 25000 }, "timezone": null @@ -3644,6 +7176,7 @@ "lineHeight": "1" }, "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, "showTicks": true, "ticksColor": "rgba(0, 0, 0, 0.54)", "showLine": true, @@ -3654,8 +7187,8 @@ "noAggregationBarWidthSettings": { "strategy": "group", "groupWidth": { - "relative": false, - "relativeWidth": 2, + "relative": true, + "relativeWidth": 6, "absoluteWidth": 1800000 }, "barWidth": { @@ -3682,11 +7215,109 @@ "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", @@ -3694,46 +7325,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, - "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, @@ -3777,14 +7382,14 @@ "displayTypePrefix": true }, "margin": "0px", - "borderRadius": "0px", + "borderRadius": "4px", "iconSize": "0px" }, "row": 0, "col": 0, - "id": "0b091dc3-eec3-847e-d0ad-fdf12d474e7a" + "id": "3ebd62a8-dcb7-c96b-8571-e61084248f5b" }, - "536d7104-49f8-fde6-5827-61b8419f15ec": { + "88e25971-e5cb-eebb-3c7c-1ce33a8a38f4": { "typeFullFqn": "system.time_series_chart", "type": "timeseries", "sizeX": 8, @@ -3798,10 +7403,10 @@ "filterId": null, "dataKeys": [ { - "name": "transportMsgCount", + "name": "tbelExecutionCountHourly", "type": "timeseries", - "label": "{i18n:api-usage.transport-messages}", - "color": "#2196f3", + "label": "{i18n:api-usage.tbel-function-executions}", + "color": "#4CAF50", "settings": { "excludeFromStacking": false, "hideDataByDefault": false, @@ -3824,7 +7429,8 @@ "comparisonSettings": { "showValuesForComparison": true }, - "type": "bar" + "type": "bar", + "yAxisId": "default" }, "_hash": 0.0661644137210089, "units": null, @@ -3833,43 +7439,6 @@ "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": { @@ -3880,26 +7449,33 @@ } ], "timewindow": { - "hideInterval": false, - "hideLastInterval": false, - "hideQuickInterval": false, "hideAggregation": false, "hideAggInterval": false, "hideTimezone": false, - "selectedTab": 1, + "selectedTab": 0, + "realtime": { + "realtimeType": 0, + "interval": 3600000, + "timewindowMs": 86400000, + "quickInterval": "CURRENT_DAY", + "hideInterval": false, + "hideLastInterval": false, + "hideQuickInterval": false + }, "history": { "historyType": 0, - "timewindowMs": 31536000000, "interval": 86400000, - "fixedTimewindow": { - "startTimeMs": 1709729389667, - "endTimeMs": 1709815789667 - }, - "quickInterval": "CURRENT_DAY" + "timewindowMs": 2592000000, + "fixedTimewindow": null, + "quickInterval": "CURRENT_DAY", + "hideInterval": false, + "hideLastInterval": false, + "hideFixedInterval": false, + "hideQuickInterval": false }, "aggregation": { - "type": "NONE", - "limit": 1000 + "type": "SUM", + "limit": 25000 }, "timezone": null }, @@ -3973,6 +7549,7 @@ "lineHeight": "1" }, "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, "showTicks": true, "ticksColor": "rgba(0, 0, 0, 0.54)", "showLine": true, @@ -3985,7 +7562,7 @@ "groupWidth": { "relative": true, "relativeWidth": 6, - "absoluteWidth": 900000000 + "absoluteWidth": 1800000 }, "barWidth": { "relative": true, @@ -4011,7 +7588,8 @@ "showMax": false, "showAvg": false, "showTotal": true, - "showLatest": false + "showLatest": false, + "valueFormat": null }, "showTooltip": true, "tooltipTrigger": "axis", @@ -4028,7 +7606,9 @@ "tooltipDateFormat": { "format": "yyyy-MM-dd HH:mm:ss", "lastUpdateAgo": false, - "custom": false + "custom": false, + "auto": true, + "autoDateFormatSettings": {} }, "tooltipDateFont": { "family": "Roboto", @@ -4060,9 +7640,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.tbel-function-executions-hourly-activity}", "dropShadow": true, "enableFullscreen": true, "titleStyle": null, @@ -4106,14 +7755,14 @@ "displayTypePrefix": true }, "margin": "0px", - "borderRadius": "0px", + "borderRadius": "4px", "iconSize": "0px" }, "row": 0, "col": 0, - "id": "536d7104-49f8-fde6-5827-61b8419f15ec" + "id": "88e25971-e5cb-eebb-3c7c-1ce33a8a38f4" }, - "c77e417c-ad9d-8e23-3ea1-c75edd653bc0": { + "a1b5731c-e3b3-8cfb-7c50-3abcdce891d2": { "typeFullFqn": "system.time_series_chart", "type": "timeseries", "sizeX": 8, @@ -4127,10 +7776,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,22 +7802,26 @@ "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, @@ -4178,8 +7831,8 @@ "timewindowMs": 2592000000, "interval": 86400000, "fixedTimewindow": { - "startTimeMs": 1709729900300, - "endTimeMs": 1709816300300 + "startTimeMs": 1709729389667, + "endTimeMs": 1709815789667 }, "quickInterval": "CURRENT_DAY" }, @@ -4259,6 +7912,7 @@ "lineHeight": "1" }, "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, "showTicks": true, "ticksColor": "rgba(0, 0, 0, 0.54)", "showLine": true, @@ -4269,8 +7923,8 @@ "noAggregationBarWidthSettings": { "strategy": "group", "groupWidth": { - "relative": false, - "relativeWidth": 2, + "relative": true, + "relativeWidth": 6, "absoluteWidth": 1800000 }, "barWidth": { @@ -4297,58 +7951,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.rule-engine-daily-activity}", + "title": "{i18n:api-usage.tbel-function-executions-daily-activity}", "dropShadow": true, "enableFullscreen": true, "titleStyle": null, @@ -4392,14 +8118,14 @@ "displayTypePrefix": true }, "margin": "0px", - "borderRadius": "0px", + "borderRadius": "4px", "iconSize": "0px" }, "row": 0, "col": 0, - "id": "c77e417c-ad9d-8e23-3ea1-c75edd653bc0" + "id": "a1b5731c-e3b3-8cfb-7c50-3abcdce891d2" }, - "870904d2-d2e1-a1b9-ce56-b03fd47259b5": { + "efc8d4e9-dee2-b677-c378-c1a666543bf4": { "typeFullFqn": "system.time_series_chart", "type": "timeseries", "sizeX": 8, @@ -4413,10 +8139,10 @@ "filterId": null, "dataKeys": [ { - "name": "ruleEngineExecutionCount", + "name": "tbelExecutionCount", "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 +8165,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", @@ -4536,6 +8280,7 @@ "lineHeight": "1" }, "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, "showTicks": true, "ticksColor": "rgba(0, 0, 0, 0.54)", "showLine": true, @@ -4548,7 +8293,7 @@ "groupWidth": { "relative": true, "relativeWidth": 6, - "absoluteWidth": 900000000 + "absoluteWidth": 1800000 }, "barWidth": { "relative": true, @@ -4574,7 +8319,8 @@ "showMax": false, "showAvg": false, "showTotal": true, - "showLatest": false + "showLatest": false, + "valueFormat": null }, "showTooltip": true, "tooltipTrigger": "axis", @@ -4591,7 +8337,9 @@ "tooltipDateFormat": { "format": "yyyy-MM-dd HH:mm:ss", "lastUpdateAgo": false, - "custom": false + "custom": false, + "auto": true, + "autoDateFormatSettings": {} }, "tooltipDateFont": { "family": "Roboto", @@ -4623,9 +8371,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-monthly-activity}", "dropShadow": true, "enableFullscreen": true, "titleStyle": null, @@ -4669,14 +8486,14 @@ "displayTypePrefix": true }, "margin": "0px", - "borderRadius": "0px", + "borderRadius": "4px", "iconSize": "0px" }, "row": 0, "col": 0, - "id": "870904d2-d2e1-a1b9-ce56-b03fd47259b5" + "id": "efc8d4e9-dee2-b677-c378-c1a666543bf4" }, - "c66e5060-57fd-11e7-6616-65b82c294ac2": { + "61a23bd5-329f-aae7-3168-8a14a51dc10b": { "typeFullFqn": "system.time_series_chart", "type": "timeseries", "sizeX": 8, @@ -4690,10 +8507,10 @@ "filterId": null, "dataKeys": [ { - "name": "jsExecutionCountHourly", + "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, @@ -4716,48 +8533,55 @@ "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 } - ] + ], + "alarmFilterConfig": { + "statusList": [ + "ACTIVE" + ] + } } ], "timewindow": { - "hideInterval": false, "hideAggregation": false, "hideAggInterval": false, - "selectedTab": 1, + "hideTimezone": false, + "selectedTab": 0, + "realtime": { + "realtimeType": 0, + "interval": 3600000, + "timewindowMs": 86400000, + "quickInterval": "CURRENT_DAY", + "hideInterval": false, + "hideLastInterval": false, + "hideQuickInterval": false + }, "history": { "historyType": 0, + "interval": 86400000, "timewindowMs": 2592000000, - "interval": 86400000 + "fixedTimewindow": null, + "quickInterval": "CURRENT_DAY", + "hideInterval": false, + "hideLastInterval": false, + "hideFixedInterval": false, + "hideQuickInterval": false }, "aggregation": { "type": "SUM", - "limit": 1000 - } + "limit": 25000 + }, + "timezone": null }, "showTitle": true, "backgroundColor": "#FFFFFF", @@ -4829,6 +8653,7 @@ "lineHeight": "1" }, "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, "showTicks": true, "ticksColor": "rgba(0, 0, 0, 0.54)", "showLine": true, @@ -4839,8 +8664,8 @@ "noAggregationBarWidthSettings": { "strategy": "group", "groupWidth": { - "relative": false, - "relativeWidth": 2, + "relative": true, + "relativeWidth": 6, "absoluteWidth": 1800000 }, "barWidth": { @@ -4867,7 +8692,8 @@ "showMax": false, "showAvg": false, "showTotal": true, - "showLatest": false + "showLatest": false, + "valueFormat": null }, "showTooltip": true, "tooltipTrigger": "axis", @@ -4884,7 +8710,9 @@ "tooltipDateFormat": { "format": "yyyy-MM-dd HH:mm:ss", "lastUpdateAgo": false, - "custom": false + "custom": false, + "auto": true, + "autoDateFormatSettings": {} }, "tooltipDateFont": { "family": "Roboto", @@ -4916,9 +8744,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.data-points-storage-days-hourly-activity}", "dropShadow": true, "enableFullscreen": true, "titleStyle": null, @@ -4962,14 +8859,14 @@ "displayTypePrefix": true }, "margin": "0px", - "borderRadius": "0px", + "borderRadius": "4px", "iconSize": "0px" }, "row": 0, "col": 0, - "id": "c66e5060-57fd-11e7-6616-65b82c294ac2" + "id": "61a23bd5-329f-aae7-3168-8a14a51dc10b" }, - "d0e8603e-5d2e-9287-e2c6-8ccbe9c66806": { + "1249d3e2-6b3a-4e4a-65e9-6ed22959871e": { "typeFullFqn": "system.time_series_chart", "type": "timeseries", "sizeX": 8, @@ -4983,10 +8880,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 +8906,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 +9016,7 @@ "lineHeight": "1" }, "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, "showTicks": true, "ticksColor": "rgba(0, 0, 0, 0.54)", "showLine": true, @@ -5134,12 +9029,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 +9055,8 @@ "showMax": false, "showAvg": false, "showTotal": true, - "showLatest": false + "showLatest": false, + "valueFormat": null }, "showTooltip": true, "tooltipTrigger": "axis", @@ -5177,7 +9073,9 @@ "tooltipDateFormat": { "format": "yyyy-MM-dd HH:mm:ss", "lastUpdateAgo": false, - "custom": false + "custom": false, + "auto": true, + "autoDateFormatSettings": {} }, "tooltipDateFont": { "family": "Roboto", @@ -5209,9 +9107,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 +9222,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 +9243,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 +9269,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 +9389,7 @@ "lineHeight": "1" }, "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, "showTicks": true, "ticksColor": "rgba(0, 0, 0, 0.54)", "showLine": true, @@ -5409,8 +9400,8 @@ "noAggregationBarWidthSettings": { "strategy": "group", "groupWidth": { - "relative": false, - "relativeWidth": 2, + "relative": true, + "relativeWidth": 6, "absoluteWidth": 1800000 }, "barWidth": { @@ -5437,7 +9428,8 @@ "showMax": false, "showAvg": false, "showTotal": true, - "showLatest": false + "showLatest": false, + "valueFormat": null }, "showTooltip": true, "tooltipTrigger": "axis", @@ -5454,7 +9446,9 @@ "tooltipDateFormat": { "format": "yyyy-MM-dd HH:mm:ss", "lastUpdateAgo": false, - "custom": false + "custom": false, + "auto": true, + "autoDateFormatSettings": {} }, "tooltipDateFont": { "family": "Roboto", @@ -5486,9 +9480,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 +9595,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 +9616,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,32 +9642,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, - "selectedTab": 1, + "hideTimezone": false, + "selectedTab": 0, + "realtime": { + "realtimeType": 0, + "interval": 3600000, + "timewindowMs": 86400000, + "quickInterval": "CURRENT_DAY", + "hideInterval": false, + "hideLastInterval": false, + "hideQuickInterval": false + }, "history": { "historyType": 0, - "timewindowMs": 31536000000, - "interval": 1000 + "interval": 86400000, + "timewindowMs": 2592000000, + "fixedTimewindow": null, + "quickInterval": "CURRENT_DAY", + "hideInterval": false, + "hideLastInterval": false, + "hideFixedInterval": false, + "hideQuickInterval": false }, "aggregation": { - "type": "NONE", - "limit": 1000 - } + "type": "SUM", + "limit": 25000 + }, + "timezone": null }, "showTitle": true, "backgroundColor": "#FFFFFF", @@ -5676,6 +9762,7 @@ "lineHeight": "1" }, "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, "showTicks": true, "ticksColor": "rgba(0, 0, 0, 0.54)", "showLine": true, @@ -5688,7 +9775,7 @@ "groupWidth": { "relative": true, "relativeWidth": 6, - "absoluteWidth": 900000000 + "absoluteWidth": 1800000 }, "barWidth": { "relative": true, @@ -5714,7 +9801,8 @@ "showMax": false, "showAvg": false, "showTotal": true, - "showLatest": false + "showLatest": false, + "valueFormat": null }, "showTooltip": true, "tooltipTrigger": "axis", @@ -5731,7 +9819,9 @@ "tooltipDateFormat": { "format": "yyyy-MM-dd HH:mm:ss", "lastUpdateAgo": false, - "custom": false + "custom": false, + "auto": true, + "autoDateFormatSettings": {} }, "tooltipDateFont": { "family": "Roboto", @@ -5763,9 +9853,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 +9968,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 +9992,7 @@ "name": "createdAlarmsCountHourly", "type": "timeseries", "label": "{i18n:api-usage.alarms-created}", - "color": "#d35a00", + "color": "#D35A00", "settings": { "excludeFromStacking": false, "hideDataByDefault": false, @@ -5843,7 +10002,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 +10015,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 +10125,7 @@ "lineHeight": "1" }, "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, "showTicks": true, "ticksColor": "rgba(0, 0, 0, 0.54)", "showLine": true, @@ -5963,8 +10136,8 @@ "noAggregationBarWidthSettings": { "strategy": "group", "groupWidth": { - "relative": false, - "relativeWidth": 2, + "relative": true, + "relativeWidth": 6, "absoluteWidth": 1800000 }, "barWidth": { @@ -5991,7 +10164,8 @@ "showMax": false, "showAvg": false, "showTotal": true, - "showLatest": false + "showLatest": false, + "valueFormat": null }, "showTooltip": true, "tooltipTrigger": "axis", @@ -6008,7 +10182,9 @@ "tooltipDateFormat": { "format": "yyyy-MM-dd HH:mm:ss", "lastUpdateAgo": false, - "custom": false + "custom": false, + "auto": true, + "autoDateFormatSettings": {} }, "tooltipDateFont": { "family": "Roboto", @@ -6040,7 +10216,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 +10331,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 +10355,7 @@ "name": "createdAlarmsCount", "type": "timeseries", "label": "{i18n:api-usage.alarms-created}", - "color": "#d35a00", + "color": "#D35A00", "settings": { "excludeFromStacking": false, "hideDataByDefault": false, @@ -6120,7 +10365,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 +10378,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 +10498,7 @@ "lineHeight": "1" }, "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, "showTicks": true, "ticksColor": "rgba(0, 0, 0, 0.54)", "showLine": true, @@ -6242,7 +10511,7 @@ "groupWidth": { "relative": true, "relativeWidth": 6, - "absoluteWidth": 900000000 + "absoluteWidth": 1800000 }, "barWidth": { "relative": true, @@ -6268,7 +10537,8 @@ "showMax": false, "showAvg": false, "showTotal": true, - "showLatest": false + "showLatest": false, + "valueFormat": null }, "showTooltip": true, "tooltipTrigger": "axis", @@ -6285,7 +10555,9 @@ "tooltipDateFormat": { "format": "yyyy-MM-dd HH:mm:ss", "lastUpdateAgo": false, - "custom": false + "custom": false, + "auto": true, + "autoDateFormatSettings": {} }, "tooltipDateFont": { "family": "Roboto", @@ -6317,7 +10589,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 +10704,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 +10728,7 @@ "name": "emailCountHourly", "type": "timeseries", "label": "{i18n:api-usage.email-messages}", - "color": "#d35a00", + "color": "#F36021", "settings": { "excludeFromStacking": false, "hideDataByDefault": false, @@ -6410,32 +10751,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, - "selectedTab": 1, + "hideTimezone": false, + "selectedTab": 0, + "realtime": { + "realtimeType": 0, + "interval": 3600000, + "timewindowMs": 86400000, + "quickInterval": "CURRENT_DAY", + "hideInterval": false, + "hideLastInterval": false, + "hideQuickInterval": false + }, "history": { "historyType": 0, + "interval": 86400000, "timewindowMs": 2592000000, - "interval": 86400000 + "fixedTimewindow": null, + "quickInterval": "CURRENT_DAY", + "hideInterval": false, + "hideLastInterval": false, + "hideFixedInterval": false, + "hideQuickInterval": false }, "aggregation": { "type": "SUM", - "limit": 1000 - } + "limit": 25000 + }, + "timezone": null }, "showTitle": true, "backgroundColor": "#FFFFFF", @@ -6507,6 +10871,7 @@ "lineHeight": "1" }, "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, "showTicks": true, "ticksColor": "rgba(0, 0, 0, 0.54)", "showLine": true, @@ -6517,8 +10882,8 @@ "noAggregationBarWidthSettings": { "strategy": "group", "groupWidth": { - "relative": false, - "relativeWidth": 2, + "relative": true, + "relativeWidth": 6, "absoluteWidth": 1800000 }, "barWidth": { @@ -6545,7 +10910,8 @@ "showMax": false, "showAvg": false, "showTotal": true, - "showLatest": false + "showLatest": false, + "valueFormat": null }, "showTooltip": true, "tooltipTrigger": "axis", @@ -6562,7 +10928,9 @@ "tooltipDateFormat": { "format": "yyyy-MM-dd HH:mm:ss", "lastUpdateAgo": false, - "custom": false + "custom": false, + "auto": true, + "autoDateFormatSettings": {} }, "tooltipDateFont": { "family": "Roboto", @@ -6594,9 +10962,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 +11077,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 +11098,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 +11124,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 +11234,7 @@ "lineHeight": "1" }, "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, "showTicks": true, "ticksColor": "rgba(0, 0, 0, 0.54)", "showLine": true, @@ -6794,8 +11245,8 @@ "noAggregationBarWidthSettings": { "strategy": "group", "groupWidth": { - "relative": false, - "relativeWidth": 2, + "relative": true, + "relativeWidth": 6, "absoluteWidth": 1800000 }, "barWidth": { @@ -6822,7 +11273,8 @@ "showMax": false, "showAvg": false, "showTotal": true, - "showLatest": false + "showLatest": false, + "valueFormat": null }, "showTooltip": true, "tooltipTrigger": "axis", @@ -6839,7 +11291,9 @@ "tooltipDateFormat": { "format": "yyyy-MM-dd HH:mm:ss", "lastUpdateAgo": false, - "custom": false + "custom": false, + "auto": true, + "autoDateFormatSettings": {} }, "tooltipDateFont": { "family": "Roboto", @@ -6871,9 +11325,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 +11440,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 +11464,7 @@ "name": "emailCount", "type": "timeseries", "label": "{i18n:api-usage.email-messages}", - "color": "#d35a00", + "color": "#F36021", "settings": { "excludeFromStacking": false, "hideDataByDefault": false, @@ -6964,32 +11487,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 +11602,7 @@ "lineHeight": "1" }, "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, "showTicks": true, "ticksColor": "rgba(0, 0, 0, 0.54)", "showLine": true, @@ -7073,7 +11615,7 @@ "groupWidth": { "relative": true, "relativeWidth": 6, - "absoluteWidth": 900000000 + "absoluteWidth": 1800000 }, "barWidth": { "relative": true, @@ -7099,7 +11641,8 @@ "showMax": false, "showAvg": false, "showTotal": true, - "showLatest": false + "showLatest": false, + "valueFormat": null }, "showTooltip": true, "tooltipTrigger": "axis", @@ -7116,7 +11659,9 @@ "tooltipDateFormat": { "format": "yyyy-MM-dd HH:mm:ss", "lastUpdateAgo": false, - "custom": false + "custom": false, + "auto": true, + "autoDateFormatSettings": {} }, "tooltipDateFont": { "family": "Roboto", @@ -7148,9 +11693,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 +11808,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 +11829,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,32 +11855,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, - "selectedTab": 1, + "hideTimezone": false, + "selectedTab": 0, + "realtime": { + "realtimeType": 0, + "interval": 3600000, + "timewindowMs": 86400000, + "quickInterval": "CURRENT_DAY", + "hideInterval": false, + "hideLastInterval": false, + "hideQuickInterval": false + }, "history": { "historyType": 0, - "timewindowMs": 31536000000, - "interval": 1000 + "interval": 86400000, + "timewindowMs": 2592000000, + "fixedTimewindow": null, + "quickInterval": "CURRENT_DAY", + "hideInterval": false, + "hideLastInterval": false, + "hideFixedInterval": false, + "hideQuickInterval": false }, "aggregation": { - "type": "NONE", - "limit": 1000 - } + "type": "SUM", + "limit": 25000 + }, + "timezone": null }, "showTitle": true, "backgroundColor": "#FFFFFF", @@ -7338,6 +11975,7 @@ "lineHeight": "1" }, "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, "showTicks": true, "ticksColor": "rgba(0, 0, 0, 0.54)", "showLine": true, @@ -7350,7 +11988,7 @@ "groupWidth": { "relative": true, "relativeWidth": 6, - "absoluteWidth": 900000000 + "absoluteWidth": 1800000 }, "barWidth": { "relative": true, @@ -7376,58 +12014,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.sms-messages-monthly-activity}", + "title": "{i18n:api-usage.sms-messages-hourly-activity}", "dropShadow": true, "enableFullscreen": true, "titleStyle": null, @@ -7471,14 +12181,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 +12197,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", - "type": "timeseries", - "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 - } + "name": "smsCountHourly", + "type": "timeseries", + "label": "{i18n:api-usage.sms-messages}", + "color": "#F36021", + "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", + "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 +12338,7 @@ "lineHeight": "1" }, "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, "showTicks": true, "ticksColor": "rgba(0, 0, 0, 0.54)", "showLine": true, @@ -7828,8 +12349,8 @@ "noAggregationBarWidthSettings": { "strategy": "group", "groupWidth": { - "relative": false, - "relativeWidth": 2, + "relative": true, + "relativeWidth": 6, "absoluteWidth": 1800000 }, "barWidth": { @@ -7852,11 +12373,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 +12395,9 @@ "tooltipDateFormat": { "format": "yyyy-MM-dd HH:mm:ss", "lastUpdateAgo": false, - "custom": false + "custom": false, + "auto": true, + "autoDateFormatSettings": {} }, "tooltipDateFont": { "family": "Roboto", @@ -7885,13 +12409,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,255 +12430,211 @@ "blur": 3 } }, - "padding": "12px" - }, - "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": { + "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", - "family": "Roboto", + "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" }, - "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 - }, + "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}", + "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": "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 +12706,7 @@ "lineHeight": "1" }, "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, "showTicks": true, "ticksColor": "rgba(0, 0, 0, 0.54)", "showLine": true, @@ -8237,8 +12717,8 @@ "noAggregationBarWidthSettings": { "strategy": "group", "groupWidth": { - "relative": false, - "relativeWidth": 2, + "relative": true, + "relativeWidth": 6, "absoluteWidth": 1800000 }, "barWidth": { @@ -8261,11 +12741,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", @@ -8282,7 +12763,9 @@ "tooltipDateFormat": { "format": "yyyy-MM-dd HH:mm:ss", "lastUpdateAgo": false, - "custom": false + "custom": false, + "auto": true, + "autoDateFormatSettings": {} }, "tooltipDateFont": { "family": "Roboto", @@ -8294,31 +12777,98 @@ }, "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, "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.processing-failures-and-timeouts}", + "title": "{i18n:api-usage.sms-messages-monthly-activity}", "dropShadow": true, "enableFullscreen": true, "titleStyle": null, @@ -8362,12 +12912,348 @@ "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": [] + } + ], + "timewindow": { + "displayValue": "", + "selectedTab": 0, + "realtime": { + "realtimeType": 1, + "interval": 1000, + "timewindowMs": 60000, + "quickInterval": "CURRENT_DAY", + "hideInterval": false, + "hideLastInterval": false, + "hideQuickInterval": false + }, + "history": { + "historyType": 0, + "interval": 1000, + "timewindowMs": 60000, + "fixedTimewindow": { + "startTimeMs": 1756302747649, + "endTimeMs": 1756389147649 + }, + "quickInterval": "CURRENT_DAY", + "hideInterval": false, + "hideLastInterval": false, + "hideFixedInterval": false, + "hideQuickInterval": false + }, + "aggregation": { + "type": "AVG", + "limit": 25000 + } + }, + "showTitle": true, + "backgroundColor": "#fff", + "color": "rgba(0, 0, 0, 0.87)", + "padding": "0", + "settings": { + "dsEntityAliasId": "40193437-33ac-3172-eefd-0b08eb849062", + "dataKeys": [ + { + "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": "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" + }, + "row": 0, + "col": 0, + "id": "07e3a570-c961-b72d-3371-5b29f3617b73" } }, "states": { @@ -8377,346 +13263,847 @@ "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": { + "51608a74-f213-d8c9-8df8-b42238ef93a6": { + "sizeX": 12, "sizeY": 4, - "row": 2, - "col": 8 + "row": 0, + "col": 0, + "resizable": true, + "mobileHeight": 6 }, - "ca996b66-ab7e-f977-152c-98e4ebf2a901": { - "sizeX": 8, + "fb155957-1af4-233e-e2fb-09e648e75d6e": { + "sizeX": 6, "sizeY": 4, - "row": 2, - "col": 16 + "row": 4, + "col": 0, + "resizable": true, + "mobileHeight": 6 }, - "a3c2f1bb-7d3a-f11c-7b3d-28cd84fdfe34": { - "sizeX": 8, + "4817e33b-87be-5be3-eaca-ca68a2eb4e0c": { + "sizeX": 6, "sizeY": 4, - "row": 6, + "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 + } + } + } + }, + "transport_data_points": { + "name": "{i18n:api-usage.transport-data-points}", + "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": { + "9e00cc90-520d-2108-1d2f-bba68ed5cbf1": { + "sizeX": 12, + "sizeY": 4, + "row": 0, + "col": 0, + "resizable": true, + "mobileHeight": 6 }, - "5cebd4f1-ff6e-62f9-025c-8e7583c3d66a": { - "sizeX": 8, + "79056202-c92b-1dae-ce49-318ec52e2d3b": { + "sizeX": 6, "sizeY": 4, - "row": 6, - "col": 8 + "row": 4, + "col": 0, + "resizable": true, + "mobileHeight": 6 }, - "bc0c8840-a9b5-5583-de7b-9e9450f5d8fe": { - "sizeX": 8, + "966ffee7-ba0d-8e54-f903-e8d015ca8cd2": { + "sizeX": 6, "sizeY": 4, - "row": 6, - "col": 16 + "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": 100, - "outerMargin": true + "mobileRowHeight": 70, + "mobileDisplayLayoutFirst": false } } } }, - "transport": { - "name": "{i18n:api-usage.transport}", + "rule_engine_executions": { + "name": "{i18n:api-usage.rule-engine-executions}", "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": { + "b1a9a51f-e5a6-9d5f-ef5c-25c2a68af1b0": { + "sizeX": 12, + "sizeY": 4, + "row": 0, + "col": 0, + "resizable": true, + "mobileHeight": 6 }, - "536d7104-49f8-fde6-5827-61b8419f15ec": { - "sizeX": 24, - "sizeY": 6, - "row": 6, - "col": 0 + "84fbe63a-bcb6-7bc1-8af0-46b3b1ee5adc": { + "sizeX": 6, + "sizeY": 4, + "row": 4, + "col": 0, + "resizable": true, + "mobileHeight": 6 + }, + "43a2b982-6c02-d9bd-71ee-34e8e6cf8893": { + "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_execution": { - "name": "{i18n:api-usage.rule-engine-executions}", + "javascript_function_executions": { + "name": "{i18n:api-usage.javascript-function-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": { + "76fe83c9-c30f-00a5-6299-40c759ca6705": { + "sizeX": 12, + "sizeY": 4, + "row": 0, + "col": 0, + "resizable": true, + "mobileHeight": 6 }, - "870904d2-d2e1-a1b9-ce56-b03fd47259b5": { - "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 } } } }, - "telemetry_persistence": { - "name": "{i18n:api-usage.telemetry-persistence}", + "tbel_function_executions": { + "name": "{i18n:api-usage.tbel-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": { + "88e25971-e5cb-eebb-3c7c-1ce33a8a38f4": { + "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 + "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", - "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}", + "data_points_storage_days": { + "name": "{i18n:api-usage.data-points-storage-days}", "root": false, "layouts": { "main": { "widgets": { - "a669cf86-e715-efa4-dd9a-b839abf499e9": { + "07e3a570-c961-b72d-3371-5b29f3617b73": { "sizeX": 24, - "sizeY": 5, - "row": 7, - "col": 0 - }, - "fa938580-33db-f1b3-fafc-bc3e3784ad57": { - "sizeX": 12, - "sizeY": 7, + "sizeY": 39, "row": 0, "col": 0 - }, - "2ee89893-4e38-5331-95b7-3fd4f310c5a7": { + } + }, + "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": { + "61a23bd5-329f-aae7-3168-8a14a51dc10b": { "sizeX": 12, - "sizeY": 7, + "sizeY": 4, "row": 0, - "col": 12 + "col": 0, + "resizable": true, + "mobileHeight": 6 + }, + "1249d3e2-6b3a-4e4a-65e9-6ed22959871e": { + "sizeX": 6, + "sizeY": 4, + "row": 4, + "col": 0, + "resizable": true, + "mobileHeight": 6 + }, + "c2f2da29-741d-54f6-5f1d-6f6ae616ea02": { + "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 } } } }, - "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 +14130,6 @@ }, "filters": {}, "timewindow": { - "hideInterval": false, - "hideLastInterval": false, - "hideQuickInterval": false, "hideAggregation": false, "hideAggInterval": false, "hideTimezone": false, @@ -8776,7 +14160,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/rulenode/ai_node_prompt_settings.md b/ui-ngx/src/assets/help/en_US/rulenode/ai_node_prompt_settings.md new file mode 100644 index 0000000000..1c2e537efc --- /dev/null +++ b/ui-ngx/src/assets/help/en_US/rulenode/ai_node_prompt_settings.md @@ -0,0 +1,246 @@ +#### 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. + +##### Scenario +An IoT freezer unit generates a "High Temperature" alarm. We want the AI to process this alarm data to create a clear summary and a recommended action plan for an operator. + +1. **Incoming message structure** + +Message body (represents an alarm, some alarm fields omitted for brevity): +```json +{ + "type": "High Temperature", + "details": { + "currentTemp_C": -5, + "threshold_C": -18 + } +} +``` + +Message metadata: +```json +{ + "deviceName": "Freezer-B7", + "deviceType": "CommercialFreezer" +} +``` + +2. **Prompt configuration** + +**System prompt** + +Here, we set the AI's role and enforce a strict JSON output format. This ensures the output is always machine-parsable. +``` +You are an expert AI assistant for IoT operations. +Your task is to analyze device data and respond with a single, valid JSON object. +Do not include any text, explanations, or markdown formatting before or after the JSON output. +``` + +**User prompt** + +This prompt defines the specific task, using templates to dynamically insert data from the incoming alarm message. +``` +Analyze the following alarm from a "${deviceType}" unit named "${deviceName}". + +Alarm Data: +$[*] + +Based on the alarm data, generate a JSON object with two keys: +1. "summary": A brief, human-readable summary of the event. +2. "action": A concrete, recommended next step for an operator. +``` + +3. **How it works** + +When the alarm message from "Freezer-B7" is processed by the AI node, the templates are substituted with the actual data: +- `${deviceName}` becomes "Freezer-B7" +- `${deviceType}` becomes "CommercialFreezer" +- `$[*]` is replaced by the entire message body JSON: `{"type":"High Temperature","details":{"currentTemp_C":-5,"threshold_C":-18}}` +> **Note:** You can also use `${*}`. In this case, it will be replaced with the entire message metadata JSON. + +The final instruction sent to the AI is a combination of the system and the substituted user prompt. + +4. **Expected AI output** + +Given the combined instructions, the AI would generate the following structured JSON output, which can then be used in subsequent rule nodes (e.g., to send an enriched email). + +```json +{ + "summary": "Critical high temperature alert on freezer unit Freezer-B7. The current temperature is -5°C, which is significantly above the required threshold of -18°C.", + "action": "Dispatch technician immediately to inspect the unit's cooling system and ensure the door is properly sealed. Investigate for potential power issues." +} +``` + +> **Note:** The scenario above is a hypothetical example designed to illustrate the functionality of the node and its templating capabilities. +> The specific details, such as freezer alarms, are used for demonstration purposes and are not intended to suggest or limit the potential use cases. 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 06a461b412..dbacae6ad3 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -721,6 +721,7 @@ "state-entity-parameter-name": "State entity parameter name", "default-state-entity": "Default state entity", "default-entity-parameter-name": "By default", + "query-options": "Query options", "max-relation-level": "Max relation level", "unlimited-level": "Unlimited level", "state-entity": "Dashboard state entity", @@ -864,15 +865,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", @@ -898,14 +902,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", @@ -915,13 +920,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", @@ -1020,12 +1052,14 @@ "selected-fields": "{ count, plural, =1 {1 calculated field} other {# calculated fields} } selected", "type": { "simple": "Simple", - "script": "Script" + "script": "Script", + "geofencing" : "Geofencing" }, "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", @@ -1037,6 +1071,7 @@ "argument-asset": "Asset", "argument-customer": "Customer", "argument-tenant": "Current tenant", + "argument-relation-query": "Related entities", "argument-type": "Argument type", "see-debug-events": "See debug events", "attribute": "Attribute", @@ -1070,6 +1105,34 @@ "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", + "relation-query": "Relations query", + "direction": "Direction", + "direction-from": "From source entity", + "direction-to": "To source entity", + "relation-type": "Relation type", + "create-relation-with-matched-zones": "Create relations 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", "hint": { "arguments-simple-with-rolling": "Simple type calculated field should not contain keys with time series rolling type.", "arguments-empty": "Arguments should not be empty.", @@ -1081,14 +1144,111 @@ "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.", + "name-required": "Mame 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. Set to 0 to disable scheduled refresh.", + "zone-group-refresh-interval-required": "Zone groups refresh interval is required.", + "zone-group-refresh-interval-min": "Zone group refresh interval is below the minimum allowed system interval." } }, + "ai-models": { + "ai-models": "AI models", + "ai-model": "AI model", + "model": "Model", + "name": "Name", + "ai-provider": "AI provider", + "no-found": "No AI models found", + "list": "{ count, plural, =1 {One model} other {List of # models} }", + "selected-fields": "{ count, plural, =1 {1 model} other {# models} } selected", + "add": "Add model", + "delete-model-title": "Are you sure you want to delete the model '{{modelName}}'?", + "delete-model-text": "Be careful, after the confirmation the model and all related data will become unrecoverable.", + "delete-models-title": "Are you sure you want to delete { count, plural, =1 {1 model} other {# models} }?", + "delete-models-text": "Be careful, after the confirmation all selected models will be removed and all related data will become unrecoverable.", + "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 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.", + "project-id": "Project ID", + "project-id-required": "Project ID is required", + "location": "Location", + "location-required": "Location is required.", + "service-account-key-file": "Service account key file", + "service-account-key-file-required": "Service account key file is required.", + "no-file": "No file selected.", + "drop-file": "Drop a file or click to select a file to upload.", + "personal-access-token": "Personal access token", + "personal-access-token-required": "Personal access token is required.", + "configuration": "Configuration", + "model-id": "Model ID", + "model-id-required": "Model ID is required.", + "deployment-name": "Deployment name", + "deployment-name-required": "Deployment name is required", + "set": "Set", + "region": "Region", + "region-required": "Region is required.", + "access-key-id": "Access key ID", + "access-key-id-required": "Access key ID is required.", + "secret-access-key": "Secret access key", + "secret-access-key-required": "Secret access key is required.", + "temperature": "Temperature", + "temperature-hint": "Adjusts the level of randomness in the model's output. Higher values increase randomness, while lower values decrease it.", + "temperature-min": "Must be 0 or greater.", + "top-p": "Top P", + "top-p-hint": "Creates a pool of the most probable tokens for the model to choose from. Higher values create a larger and more diverse pool, while lower values create a smaller one.", + "top-p-min-max": "Must be greater than 0 and up to 1.", + "top-k": "Top K", + "top-k-hint": "Restricts the model's choices to a fixed set of the \"K\" most likely tokens.", + "top-k-min": "Must be 0 or greater.", + "presence-penalty": "Presence penalty", + "presence-penalty-hint": "Applies a fixed penalty to the likelihood of a token if it has already appeared in the text.", + "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.", + "endpoint": "Endpoint", + "endpoint-required": "Endpoint 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." + }, "confirm-on-exit": { "message": "You have unsaved changes. Are you sure you want to leave this page?", "html-message": "You have unsaved changes.
Are you sure you want to leave this page?", @@ -1770,6 +1930,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", @@ -2555,6 +2716,8 @@ "type-current-user-owner": "Current User Owner", "type-calculated-field": "Calculated field", "type-calculated-fields": "Calculated fields", + "type-ai-model": "AI model", + "type-ai-models": "AI models", "type-widgets-bundle": "Widgets bundle", "type-widgets-bundles": "Widgets bundles", "list-of-widgets-bundles": "{ count, plural, =1 {One widgets bundle} other {List of # widget bundles} }", @@ -2584,6 +2747,8 @@ "type-tb-resources": "Resources", "list-of-tb-resources": "{ count, plural, =1 {One resource} other {List of # resources} }", "type-ota-package": "OTA package", + "type-ota-packages": "OTA packages", + "list-of-ota-packages": "{ count, plural, =1 {One OTA package} other {List of # OTA packages} }", "type-rpc": "RPC", "type-queue": "Queue", "type-queue-stats": "Queue statistics", @@ -3729,9 +3894,9 @@ }, "mobile": { "add-application": "Add application", - "app-id": "App ID", - "app-id-required": "App ID is required", - "app-id-pattern": "Invalid format App ID", + "app-id": "App Site Association ID", + "app-id-required": "App Site Association ID is required", + "app-id-pattern": "Invalid format App Site Association ID", "app-store-link": "App Store link", "app-store-link-required": "App Store link is required", "application-details": "Application details", @@ -3765,6 +3930,8 @@ "mobile-package-max-length": "Application package should be less than 256", "mobile-package-required": "Application package is required.", "mobile-package-pattern": "Application package invalid format", + "mobile-package-title": "Application title", + "mobile-package-title-max-length": "Application title should be less than 256", "no-application": "No applications found", "no-bundles": "No bundles found", "platform-type": "Platform type", @@ -3848,17 +4015,13 @@ "prepare-environment-text": "Flutter ThingsBoard Mobile Application requires Flutter SDK. Follow instructions to set up Flutter SDK.", "get-source-code-title": "Get app source code", "get-source-code-text": "You can get Flutter ThingsBoard Mobile Application source code by cloning it from the GitHub repository:", - "configure-api-title": "Configure ThingsBoard API endpoint", - "configure-api-text": "Open the flutter_thingsboard_app project in your editor/IDE. Edit:", - "configure-api-hint": "Set the value of the thingsBoardApiEndpoint constant to match the API endpoint of your ThingsBoard server instance. Do not use “localhost” or “127.0.0.1” hostnames.", + "configure-app-settings-title": "Configure app settings", + "configure-app-settings-text": "Download the configuration file and place it into the root directory of the project you cloned in the previous step.", + "download-file": "Download file", "run-app-title": "Run the app", "run-app-text": "Run the app as described in your IDE.\nIf using the terminal, run the app with the following command:", "more-information": "Detailed information may be found in our Getting Started documentation.", - "getting-started": "Getting Started", - "configure-package-title": "Configure application package", - "configure-package-text": "You can manually change the Application Package or use third party CLI tool.", - "configure-package-text-install": "To install the Rename CLI Tool, execute the following command:", - "configure-package-run-commands": "Run these commands in the root directory of your project:" + "getting-started": "Getting Started" } }, "notification": { @@ -3991,6 +4154,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", @@ -4365,6 +4529,7 @@ "add-relation-filter": "Add relation filter", "any-relation": "Any relation", "relation-filters": "Relation filters", + "relation-filter": "Relation filter", "additional-info": "Additional info (JSON)", "invalid-additional-info": "Unable to parse additional info json.", "no-relations-text": "No relations found", @@ -5355,6 +5520,36 @@ "html-text-description": "Allows you to use HTML tags for formatting, links and images in your mai body.", "dynamic-text-description": "Allows to use Plain Text or HTML body type dynamically based on templatization feature.", "after-template-evaluation-hint": "After template evaluation value should be true for HTML, and false for Plain text." + }, + "ai": { + "ai-model": "AI model", + "model": "Model", + "ai-model-hint": "Select the pre-configured AI model to process requests sent by this rule node, or use \"Create new\" to configure a new one.", + "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 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 500000 characters or less.", + "user-prompt-blank": "User prompt must not be blank.", + "response-format": "Response format", + "response-text": "Text", + "response-json": "JSON", + "response-json-schema": "JSON Schema", + "response-format-hint-TEXT": "Allows the model to generate arbitrary text, which may or may not be a valid JSON object. If the output is not a valid JSON object, it will be automatically wrapped within a JSON object under the \"response\" key.", + "response-format-hint-JSON": "The model is required to generate a response that is a valid JSON. If the output is not a valid JSON object, it will be automatically wrapped within a JSON object under the \"response\" key.", + "response-format-hint-JSON_SCHEMA": "The model is required to generate a JSON that matches the specific structure and data types defined in the provided schema. If the output is not a valid JSON object, it will be automatically wrapped within a JSON object under the \"response\" key.", + "response-json-schema-hint": "While any valid JSON Schema can be entered, this rule node only supports a limited subset of its features. See node documentation for details.", + "response-json-schema-required": "JSON Schema is required", + "advanced-settings": "Advanced settings", + "timeout": "Timeout", + "timeout-hint": "Maximum time to wait for a response \nfrom the AI model before the request is terminated.", + "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." } }, "timezone": { @@ -5615,6 +5810,12 @@ "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-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", @@ -6014,9 +6215,9 @@ "foot-us": "Foot (US survey)", "yard": "Yard", "mile": "Mile", - "nautical-mile": "Nautical Mile", - "astronomical-unit": "Astronomical Unit", - "reciprocal-metre": "Reciprocal Metre", + "nautical-mile": "Nautical mile", + "astronomical-unit": "Astronomical unit", + "reciprocal-metre": "Reciprocal metre", "meter-per-meter": "Meter per meter", "steradian": "Steradian", "thou": "Thou", @@ -6046,24 +6247,24 @@ "quarter": "Quarter", "slug": "Slug", "carat": "Carat", - "cubic-millimeter": "Cubic Millimeter", - "cubic-centimeter": "Cubic Centimeter", - "cubic-meter": "Cubic Meter", - "cubic-kilometer": "Cubic Kilometer", + "cubic-millimeter": "Cubic millimeter", + "cubic-centimeter": "Cubic centimeter", + "cubic-meter": "Cubic meter", + "cubic-kilometer": "Cubic kilometer", "microliter": "Microliter", "milliliter": "Milliliter", "liter": "Liter", "hectoliter": "Hectolitre", - "cubic-inch": "Cubic Inch", - "cubic-foot": "Cubic Foot", - "cubic-yard": "Cubic Yard", - "fluid-ounce": "Fluid Ounce", - "fluid-ounce-per-second": "Fluid Ounce per second", + "cubic-inch": "Cubic inch", + "cubic-foot": "Cubic foot", + "cubic-yard": "Cubic yard", + "fluid-ounce": "Fluid ounce", + "fluid-ounce-per-second": "Fluid ounce per second", "pint": "Pint", "quart": "Quart", "gallon": "Gallon", - "oil-barrels": "Oil Barrel", - "cubic-meter-per-kilogram": "Cubic Meter per Kilogram", + "oil-barrels": "Oil barrel", + "cubic-meter-per-kilogram": "Cubic meter per kilogram", "gill": "Gill", "hogshead": "Hogshead", "teaspoon": "Teaspoon", @@ -6074,14 +6275,16 @@ "rankine": "Rankine", "fahrenheit": "Fahrenheit", "percent": "Percent", - "meter-per-second": "Meter per Second", - "kilometer-per-hour": "Kilometer per Hour", - "foot-per-second": "Foot per Second", - "foot-per-minute": "Foot per Minute", - "mile-per-hour": "Mile per Hour", + "meter-per-second": "Meter per second", + "kilometer-per-hour": "Kilometer per hour", + "foot-per-second": "Foot per second", + "foot-per-minute": "Foot per minute", + "mile-per-hour": "Mile per hour", "knot": "Knot", - "inch-per-hour": "Inch per Hour", + "inch-per-second": "Inch per second", + "inch-per-hour": "Inch per hour", "millimeters-per-minute": "Millimeters per minute", + "meter-per-minute": "Meter per minute", "kilometer-per-hour-squared": "Kilometer per hour squared", "foot-per-second-squared": "Foot per second squared", "pascal": "Pascal", @@ -6097,18 +6300,18 @@ "inch-pounds": "Inch-pounds", "newton-per-meter": "Newton per meter", "atmospheres": "Atmospheres", - "pounds-per-square-inch": "Pounds per Square Inch", - "kilopound-per-square-inch": "Kilopound per Square Inch", + "pounds-per-square-inch": "Pounds per square inch", + "kilopound-per-square-inch": "Kilopound 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", + "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", @@ -6122,31 +6325,31 @@ "megawatt-hour": "Megawatt-hour", "gigawatt-hour": "Gigawatt-hour", "electron-volts": "Electron volts", - "joules-per-coulomb": "Joules per Coulomb", - "british-thermal-unit": "British Thermal Units", - "thousand-british-thermal-unit": "Thousand British Thermal Units", - "million-british-thermal-unit": "Million British Thermal Units", + "joules-per-coulomb": "Joules per coulomb", + "british-thermal-unit": "British thermal units", + "thousand-british-thermal-unit": "Thousand British thermal units", + "million-british-thermal-unit": "Million British thermal units", "foot-pound": "Foot-pound", "calorie": "Calorie", - "small-calorie": "Small 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", + "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", + "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", + "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", + "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", @@ -6165,28 +6368,28 @@ "mmbtu-per-hour": "Million British thermal units per hour", "mmbtu-per-second": "Million British thermal units per second", "mmbtu-per-day": "Million British thermal units per day", - "foot-pound-per-second": "foot-pound per second", + "foot-pound-per-second": "Foot-pound per second", "coulomb": "Coulomb", "millicoulomb": "Millicoulombs", "microcoulomb": "Microcoulomb", "nanocoulomb": "Nanocoulomb", "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", + "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", + "square-kilometer": "Square kilometer", + "square-inch": "Square inch", + "square-foot": "Square foot", + "square-yard": "Square yard", "acre": "Acre", - "square-mile": "Square Mile", + "square-mile": "Square mile", "are": "Are", "barn": "Barn", - "circular-inch": "Circular Inch", + "circular-inch": "Circular inch", "milliampere-hour": "Milliampere-hour", "ampere-hours": "Ampere-hours", "kiloampere-hours": "Kiloampere-hours", @@ -6199,11 +6402,11 @@ "megaampere": "Megaampere", "gigaampere": "Gigaampere", "microampere-per-square-centimeter": "Microampere per square centimeter", - "ampere-per-square-meter": "Ampere per Square Meter", - "ampere-per-meter": "Ampere per Meter", + "ampere-per-square-meter": "Ampere per square meter", + "ampere-per-meter": "Ampere per meter", "oersted": "Oersted", - "bohr-magneton": "Bohr Magneton", - "ampere-meter-squared": "Ampere-Meter Squared", + "bohr-magneton": "Bohr magneton", + "ampere-meter-squared": "Ampere-meter squared", "nanovolt": "Nanovolt", "picovolt": "Picovolt", "millivolt": "Millivolts", @@ -6213,12 +6416,12 @@ "megavolt": "Megavolt", "dbmV": "Decibel volt", "dbm": "Decibel-milliwatts", - "volt-meter": "Volt-Meter", - "kilovolt-meter": "Kilovolt-Meter", - "megavolt-meter": "Megavolt-Meter", - "microvolt-meter": "Microvolt-Meter", - "millivolt-meter": "Millivolt-Meter", - "nanovolt-meter": "Nanovolt-Meter", + "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", @@ -6231,7 +6434,7 @@ "megahertz": "Megahertz", "gigahertz": "Gigahertz", "terahertz": "Terahertz", - "rpm": "Revolutions Per Minute", + "rpm": "Revolutions per minute", "candela-per-square-meter": "Candela per square meter", "candela": "Candela", "lumen": "Lumen", @@ -6243,17 +6446,17 @@ "lumens-per-watt": "Lumens per watt", "mole": "Mole", "nanomole": "Nanomole", - "micromole": "MicroMole", + "micromole": "Micromole", "millimole": "Millimole", "kilomole": "Kilomole", - "mole-per-cubic-meter": "Mole per Cubic Meter", + "mole-per-cubic-meter": "Mole per cubic meter", "rssi": "Received signal strength indicator", - "ppm": "Parts Per Million", - "ppb": "Parts Per Billion", - "micrograms-per-cubic-meter": "Micrograms per Cubic Meter", - "aqi": "AQI", + "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", + "gram-per-kilogram": "Specific humidity", "millimeters-per-second": "Millimeters per second", "neper": "Neper", "bel": "Bel", @@ -6264,7 +6467,7 @@ "gray": "Gray", "sievert": "Sievert", "roentgen": "Roentgen", - "cps": "Counts per Second", + "cps": "Counts per second", "rad": "Rad", "rem": "Rem", "dps": "Disintegrations per second", @@ -6274,10 +6477,10 @@ "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", + "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", @@ -6303,9 +6506,9 @@ "milligrams-per-deciliter": "Milligrams per deciliter", "g-force": "G-force", "kilonewton": "Kilonewton", - "kilogram-force": "Kilogram-Force", - "pound-force": "Pound-Force", - "kilopound-force": "Kilopound-Force", + "kilogram-force": "Kilogram-force", + "pound-force": "Pound-force", + "kilopound-force": "Kilopound-force", "dyne": "Dyne", "poundal": "Poundal", "kip": "Kip", @@ -6315,7 +6518,7 @@ "atmosphere": "Atmosphere", "millibars": "Millibars", "inch-of-mercury": "One inch of mercury", - "richter-scale": "Richter Scale", + "richter-scale": "Richter scale", "nanosecond": "Nanosecond", "microsecond": "Microsecond", "millisecond": "Millisecond", @@ -6326,12 +6529,12 @@ "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-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", "cubic-decimeter-per-second": "Cubic decimeter per second", @@ -6376,7 +6579,7 @@ "megafarad": "Megafarad", "gigafarad": "Gigafarad", "terfarad": "Terfarad", - "farad-per-meter": "Farad per Meter", + "farad-per-meter": "Farad per meter", "tesla": "Tesla", "gauss": "Gauss", "kilogauss": "Kilogauss", @@ -6385,7 +6588,7 @@ "nanotesla": "Nanotesla", "kilotesla": "Kilotesla", "megatesla": "Megatesla", - "millitesla-square-meters": "millitesla square meters", + "millitesla-square-meters": "Millitesla square meters", "gamma": "Gamma", "lambda": "Lambda", "square-meter-per-second": "Square meter per second", @@ -6404,25 +6607,25 @@ "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", + "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", + "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", + "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", + "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", @@ -6433,7 +6636,7 @@ "rotation-per-minute": "Rotation per minute", "degrees-brix": "Degrees brix", "katal": "Katal", - "katal-per-cubic-metre": "Katal per Cubic Metre", + "katal-per-cubic-metre": "Katal per cubic metre", "paris-inch": "Paris inch" }, "user": { @@ -6753,7 +6956,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", @@ -9368,6 +9587,21 @@ "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" } }, "color": { @@ -9376,6 +9610,7 @@ "icon": { "icon": "Icon", "icons": "Icons", + "custom": "Custom", "select-icon": "Select icon", "material-icons": "Material icons", "show-all": "Show all icons", 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-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/form.scss b/ui-ngx/src/form.scss index e516810895..b88145331c 100644 --- a/ui-ngx/src/form.scss +++ b/ui-ngx/src/form.scss @@ -163,6 +163,9 @@ .tb-form-panel-title { font-weight: 500; font-size: 16px; + &.tb-normal { + font-weight: normal; + } &.tb-required::after { font-size: 13px; @@ -819,4 +822,41 @@ } } } + + .tb-form-panel.outlined { + --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); + + box-shadow: none; + gap: 0; + padding-bottom: 0; + + &:not(.stroked) { + border-radius: 0; + } + + &:not(.mat-padding,.padding) { + padding: 0; + } + + & > .tb-form-panel-title { + margin-bottom: 16px; + } + + .tb-form-panel { + @media #{$mat-xs} { + gap: 16px; + } + } + + .tb-form-row { + height: 56px; + margin-bottom: 22px; + &.disabled { + border-color: var(--mdc-outlined-text-field-disabled-outline-color); + color: var(--mdc-outlined-text-field-disabled-input-text-color); + } + } + } } 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..6d49e9f698 100644 --- a/ui-ngx/src/styles.scss +++ b/ui-ngx/src/styles.scss @@ -896,7 +896,7 @@ pre.tb-highlight { } .mat-icon { - svg { + svg, img { vertical-align: inherit; } &.tb-mat-12 { @@ -1255,6 +1255,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 { diff --git a/ui-ngx/yarn.lock b/ui-ngx/yarn.lock index 6e86f3c2b0..dc79929cc8 100644 --- a/ui-ngx/yarn.lock +++ b/ui-ngx/yarn.lock @@ -3740,6 +3740,14 @@ cacache@^18.0.0: tar "^6.1.11" unique-filename "^3.0.0" +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-errors "^1.3.0" + function-bind "^1.1.2" + call-bind@^1.0.2, call-bind@^1.0.5, call-bind@^1.0.6, call-bind@^1.0.7: version "1.0.7" resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.7.tgz#06016599c40c56498c18769d2730be242b6fa3b9" @@ -4788,6 +4796,15 @@ domutils@^3.0.1: domelementtype "^2.3.0" domhandler "^5.0.3" +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" + earcut@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/earcut/-/earcut-3.0.1.tgz#f60b3f671c5657cca9d3e131c5527c5dde00ef38" @@ -4968,6 +4985,11 @@ es-define-property@^1.0.0: 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.2.1, es-errors@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f" @@ -4985,6 +5007,13 @@ es-object-atoms@^1.0.0: dependencies: es-errors "^1.3.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" + es-set-tostringtag@^2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/es-set-tostringtag/-/es-set-tostringtag-2.0.3.tgz#8bb60f0a440c2e4281962428438d58545af39777" @@ -4994,6 +5023,16 @@ es-set-tostringtag@^2.0.3: has-tostringtag "^1.0.2" hasown "^2.0.1" +es-set-tostringtag@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz#f31dbbe0c183b00a6d26eb6325c810c0fd18bd4d" + integrity sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA== + dependencies: + es-errors "^1.3.0" + get-intrinsic "^1.2.6" + has-tostringtag "^1.0.2" + hasown "^2.0.2" + es-shim-unscopables@^1.0.0, es-shim-unscopables@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/es-shim-unscopables/-/es-shim-unscopables-1.0.2.tgz#1f6942e71ecc7835ed1c8a83006d8771a63a3763" @@ -5508,13 +5547,15 @@ foreground-child@^3.1.0: cross-spawn "^7.0.0" signal-exit "^4.0.1" -form-data@4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452" - integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww== +form-data@4.0.0, form-data@4.0.4: + version "4.0.4" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.4.tgz#784cdcce0669a9d68e94d11ac4eea98088edd2c4" + integrity sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow== dependencies: asynckit "^0.4.0" combined-stream "^1.0.8" + es-set-tostringtag "^2.1.0" + hasown "^2.0.2" mime-types "^2.1.12" formdata-polyfill@^4.0.10: @@ -5635,6 +5676,30 @@ get-intrinsic@^1.1.3, get-intrinsic@^1.2.1, get-intrinsic@^1.2.3, get-intrinsic@ has-symbols "^1.0.3" hasown "^2.0.0" +get-intrinsic@^1.2.6: + 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" + 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" + get-stream@^6.0.0, get-stream@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7" @@ -5750,6 +5815,11 @@ gopd@^1.0.1: 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.11, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.11, graceful-fs@^4.2.4, graceful-fs@^4.2.6: version "4.2.11" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" @@ -5807,6 +5877,11 @@ has-symbols@^1.0.2, has-symbols@^1.0.3: resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== +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== + has-tostringtag@^1.0.0, has-tostringtag@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz#2cdc42d40bef2e5b4eeab7c01a73c54ce7ab5abc" @@ -6970,6 +7045,11 @@ marked@~12.0.2: resolved "https://registry.yarnpkg.com/marked/-/marked-12.0.2.tgz#b31578fe608b599944c69807b00f18edab84647e" integrity sha512-qXUm7e/YKFoqFPYPa3Ukg9xlI5cyAtGmyEIzMfW//m6kXwCy2Ps9DYf5ioijFKQ8qyuscrHoY04iJGctu2Kg0Q== +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== + media-typer@0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"