diff --git a/README.md b/README.md
index bffd87ffa5..6267cc3a7a 100644
--- a/README.md
+++ b/README.md
@@ -1,43 +1,150 @@
-# ThingsBoard
-[](https://builds.thingsboard.io/viewType.html?buildTypeId=ThingsBoard_Build&guest=1)
+
+
+
+
+# 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, 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.
+
+
+
+
+
+
+
+
+
+
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.
+
+
+
+
+
+
+
+
+
+
+
+
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.
+
+
+
+
+
+
+
+
+
+
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.
+
+
+
+
+
+
+
+
+
+## ⚙️ 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.
+
+[](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/)
+
+[](https://thingsboard.io/use-cases/smart-energy/)
+
+[**SCADA swimming pool**](https://thingsboard.io/use-cases/scada/)
+
+[](https://thingsboard.io/use-cases/scada/)
+
+[**Fleet tracking**](https://thingsboard.io/use-cases/fleet-tracking/)
+
+[](https://thingsboard.io/use-cases/fleet-tracking/)
+
+[**Smart farming**](https://thingsboard.io/use-cases/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/)
-[](https://thingsboard.io/smart-energy/)
-
-[**SCADA Swimming pool**](https://thingsboard.io/use-cases/scada/)
-[](https://thingsboard.io/use-cases/scada/)
-
-[**Fleet tracking**](https://thingsboard.io/fleet-tracking/)
-[](https://thingsboard.io/fleet-tracking/)
-
-[**Smart farming**](https://thingsboard.io/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/)
-[](https://thingsboard.io/docs/user-guide/rule-engine-2-0/re-getting-started/)
+[](https://thingsboard.io/smart-metering/)
-[**Smart metering**](https://thingsboard.io/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 @@
}
]
}]]>
-220 220 220 v
+220 220 220 v
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 @@
}
]
}]]>
-000023 kWh T1
+000023 kWh T1
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 @@
}
]
}]]>
-T1 T2 T3 Export 000223 000223 000223 000223 kWh
+T1 T2 T3 Export 000223 000223 000223 000223 kWh
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 @@
}
]
}]]>
-T1 T2 T3 000223 000223 000223 kWh
+T1 T2 T3 000223 000223 000223 kWh
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 @@
}
]
}]]>
-T1 T2 000023 000023 kWh
+T1 T2 000023 000023 kWh
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 @@
}
]
}]]>
-220 v
+220 v
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 @@
}
]
}]]>
-220 230 in out
+220 230 in out
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 extends EntityId>) 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