diff --git a/application/src/main/data/json/system/scada_symbols/bottom-flow-meter.svg b/application/src/main/data/json/system/scada_symbols/bottom-flow-meter.svg
new file mode 100644
index 0000000000..24927d595e
--- /dev/null
+++ b/application/src/main/data/json/system/scada_symbols/bottom-flow-meter.svg
@@ -0,0 +1,970 @@
+
\ No newline at end of file
diff --git a/application/src/main/data/json/system/scada_symbols/bottom-right-elbow-pipe.svg b/application/src/main/data/json/system/scada_symbols/bottom-right-elbow-pipe.svg
new file mode 100644
index 0000000000..4dc6074864
--- /dev/null
+++ b/application/src/main/data/json/system/scada_symbols/bottom-right-elbow-pipe.svg
@@ -0,0 +1,558 @@
+
\ No newline at end of file
diff --git a/application/src/main/data/json/system/scada_symbols/bottom-tee-pipe.svg b/application/src/main/data/json/system/scada_symbols/bottom-tee-pipe.svg
new file mode 100644
index 0000000000..67eb7d3472
--- /dev/null
+++ b/application/src/main/data/json/system/scada_symbols/bottom-tee-pipe.svg
@@ -0,0 +1,854 @@
+
\ No newline at end of file
diff --git a/application/src/main/data/json/system/scada_symbols/centrifugal-pump.svg b/application/src/main/data/json/system/scada_symbols/centrifugal-pump.svg
new file mode 100644
index 0000000000..c1bef78aa5
--- /dev/null
+++ b/application/src/main/data/json/system/scada_symbols/centrifugal-pump.svg
@@ -0,0 +1,611 @@
+
\ No newline at end of file
diff --git a/application/src/main/data/json/system/scada_symbols/cross-pipe.svg b/application/src/main/data/json/system/scada_symbols/cross-pipe.svg
new file mode 100644
index 0000000000..9edf91c686
--- /dev/null
+++ b/application/src/main/data/json/system/scada_symbols/cross-pipe.svg
@@ -0,0 +1,1047 @@
+
\ No newline at end of file
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
new file mode 100644
index 0000000000..104b7bc864
--- /dev/null
+++ b/application/src/main/data/json/system/scada_symbols/cylindrical-tank.svg
@@ -0,0 +1,1401 @@
+
\ No newline at end of file
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
new file mode 100644
index 0000000000..7874e627df
--- /dev/null
+++ b/application/src/main/data/json/system/scada_symbols/elevated-tank.svg
@@ -0,0 +1,1632 @@
+
\ No newline at end of file
diff --git a/application/src/main/data/json/system/scada_symbols/horizontal-ball-valve.svg b/application/src/main/data/json/system/scada_symbols/horizontal-ball-valve.svg
new file mode 100644
index 0000000000..da140b4383
--- /dev/null
+++ b/application/src/main/data/json/system/scada_symbols/horizontal-ball-valve.svg
@@ -0,0 +1,271 @@
+
\ No newline at end of file
diff --git a/application/src/main/data/json/system/scada_symbols/horizontal-inline-flow-meter.svg b/application/src/main/data/json/system/scada_symbols/horizontal-inline-flow-meter.svg
new file mode 100644
index 0000000000..7028a3f854
--- /dev/null
+++ b/application/src/main/data/json/system/scada_symbols/horizontal-inline-flow-meter.svg
@@ -0,0 +1,948 @@
+
\ No newline at end of file
diff --git a/application/src/main/data/json/system/scada_symbols/horizontal-pipe.svg b/application/src/main/data/json/system/scada_symbols/horizontal-pipe.svg
new file mode 100644
index 0000000000..f166c403b2
--- /dev/null
+++ b/application/src/main/data/json/system/scada_symbols/horizontal-pipe.svg
@@ -0,0 +1,421 @@
+
\ No newline at end of file
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
new file mode 100644
index 0000000000..02832fdce1
--- /dev/null
+++ b/application/src/main/data/json/system/scada_symbols/horizontal-tank.svg
@@ -0,0 +1,1407 @@
+
\ No newline at end of file
diff --git a/application/src/main/data/json/system/scada_symbols/horizontal-wheel-valve.svg b/application/src/main/data/json/system/scada_symbols/horizontal-wheel-valve.svg
new file mode 100644
index 0000000000..a96f61781d
--- /dev/null
+++ b/application/src/main/data/json/system/scada_symbols/horizontal-wheel-valve.svg
@@ -0,0 +1,300 @@
+
\ No newline at end of file
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
new file mode 100644
index 0000000000..33dcc947ba
--- /dev/null
+++ b/application/src/main/data/json/system/scada_symbols/large-cylindrical-tank.svg
@@ -0,0 +1,1392 @@
+
\ No newline at end of file
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
new file mode 100644
index 0000000000..89b72339c2
--- /dev/null
+++ b/application/src/main/data/json/system/scada_symbols/large-stand-cylindrical-tank.svg
@@ -0,0 +1,1453 @@
+
\ No newline at end of file
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
new file mode 100644
index 0000000000..7e5170b582
--- /dev/null
+++ b/application/src/main/data/json/system/scada_symbols/large-stand-vertical-tank.svg
@@ -0,0 +1,1455 @@
+
\ No newline at end of file
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
new file mode 100644
index 0000000000..5958836557
--- /dev/null
+++ b/application/src/main/data/json/system/scada_symbols/large-vertical-tank.svg
@@ -0,0 +1,1394 @@
+
\ No newline at end of file
diff --git a/application/src/main/data/json/system/scada_symbols/left-bottom-elbow-pipe.svg b/application/src/main/data/json/system/scada_symbols/left-bottom-elbow-pipe.svg
new file mode 100644
index 0000000000..6179153171
--- /dev/null
+++ b/application/src/main/data/json/system/scada_symbols/left-bottom-elbow-pipe.svg
@@ -0,0 +1,558 @@
+
\ No newline at end of file
diff --git a/application/src/main/data/json/system/scada_symbols/left-drain-pipe.svg b/application/src/main/data/json/system/scada_symbols/left-drain-pipe.svg
new file mode 100644
index 0000000000..326a263208
--- /dev/null
+++ b/application/src/main/data/json/system/scada_symbols/left-drain-pipe.svg
@@ -0,0 +1,166 @@
+
\ No newline at end of file
diff --git a/application/src/main/data/json/system/scada_symbols/left-elbow-drain-pipe.svg b/application/src/main/data/json/system/scada_symbols/left-elbow-drain-pipe.svg
new file mode 100644
index 0000000000..b9a6aa6057
--- /dev/null
+++ b/application/src/main/data/json/system/scada_symbols/left-elbow-drain-pipe.svg
@@ -0,0 +1,181 @@
+
\ No newline at end of file
diff --git a/application/src/main/data/json/system/scada_symbols/left-flow-meter.svg b/application/src/main/data/json/system/scada_symbols/left-flow-meter.svg
new file mode 100644
index 0000000000..9ea330b68d
--- /dev/null
+++ b/application/src/main/data/json/system/scada_symbols/left-flow-meter.svg
@@ -0,0 +1,964 @@
+
\ No newline at end of file
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
new file mode 100644
index 0000000000..1fa5013fcc
--- /dev/null
+++ b/application/src/main/data/json/system/scada_symbols/left-heat-pump.svg
@@ -0,0 +1,1035 @@
+
\ No newline at end of file
diff --git a/application/src/main/data/json/system/scada_symbols/left-motor-pump.svg b/application/src/main/data/json/system/scada_symbols/left-motor-pump.svg
new file mode 100644
index 0000000000..5be0a454e5
--- /dev/null
+++ b/application/src/main/data/json/system/scada_symbols/left-motor-pump.svg
@@ -0,0 +1,526 @@
+
\ No newline at end of file
diff --git a/application/src/main/data/json/system/scada_symbols/left-tee-pipe.svg b/application/src/main/data/json/system/scada_symbols/left-tee-pipe.svg
new file mode 100644
index 0000000000..d124709a68
--- /dev/null
+++ b/application/src/main/data/json/system/scada_symbols/left-tee-pipe.svg
@@ -0,0 +1,866 @@
+
\ No newline at end of file
diff --git a/application/src/main/data/json/system/scada_symbols/left-top-elbow-pipe.svg b/application/src/main/data/json/system/scada_symbols/left-top-elbow-pipe.svg
new file mode 100644
index 0000000000..aa3fe45323
--- /dev/null
+++ b/application/src/main/data/json/system/scada_symbols/left-top-elbow-pipe.svg
@@ -0,0 +1,558 @@
+
\ No newline at end of file
diff --git a/application/src/main/data/json/system/scada_symbols/long-bottom-filter.svg b/application/src/main/data/json/system/scada_symbols/long-bottom-filter.svg
new file mode 100644
index 0000000000..d9defd33d9
--- /dev/null
+++ b/application/src/main/data/json/system/scada_symbols/long-bottom-filter.svg
@@ -0,0 +1,88 @@
+
\ No newline at end of file
diff --git a/application/src/main/data/json/system/scada_symbols/long-horizontal-pipe.svg b/application/src/main/data/json/system/scada_symbols/long-horizontal-pipe.svg
new file mode 100644
index 0000000000..bd3b223251
--- /dev/null
+++ b/application/src/main/data/json/system/scada_symbols/long-horizontal-pipe.svg
@@ -0,0 +1,487 @@
+
\ No newline at end of file
diff --git a/application/src/main/data/json/system/scada_symbols/long-top-filter.svg b/application/src/main/data/json/system/scada_symbols/long-top-filter.svg
new file mode 100644
index 0000000000..a63b679f0a
--- /dev/null
+++ b/application/src/main/data/json/system/scada_symbols/long-top-filter.svg
@@ -0,0 +1,88 @@
+
\ No newline at end of file
diff --git a/application/src/main/data/json/system/scada_symbols/long-vertical-pipe.svg b/application/src/main/data/json/system/scada_symbols/long-vertical-pipe.svg
new file mode 100644
index 0000000000..769ae94270
--- /dev/null
+++ b/application/src/main/data/json/system/scada_symbols/long-vertical-pipe.svg
@@ -0,0 +1,484 @@
+
\ No newline at end of file
diff --git a/application/src/main/data/json/system/scada_symbols/pool.svg b/application/src/main/data/json/system/scada_symbols/pool.svg
new file mode 100644
index 0000000000..86bf0d5907
--- /dev/null
+++ b/application/src/main/data/json/system/scada_symbols/pool.svg
@@ -0,0 +1,1056 @@
+
\ No newline at end of file
diff --git a/application/src/main/data/json/system/scada_symbols/right-drain-pipe.svg b/application/src/main/data/json/system/scada_symbols/right-drain-pipe.svg
new file mode 100644
index 0000000000..093078c69b
--- /dev/null
+++ b/application/src/main/data/json/system/scada_symbols/right-drain-pipe.svg
@@ -0,0 +1,167 @@
+
\ No newline at end of file
diff --git a/application/src/main/data/json/system/scada_symbols/right-elbow-drain-pipe.svg b/application/src/main/data/json/system/scada_symbols/right-elbow-drain-pipe.svg
new file mode 100644
index 0000000000..63b143eeef
--- /dev/null
+++ b/application/src/main/data/json/system/scada_symbols/right-elbow-drain-pipe.svg
@@ -0,0 +1,178 @@
+
\ No newline at end of file
diff --git a/application/src/main/data/json/system/scada_symbols/right-flow-meter.svg b/application/src/main/data/json/system/scada_symbols/right-flow-meter.svg
new file mode 100644
index 0000000000..11684d2cb8
--- /dev/null
+++ b/application/src/main/data/json/system/scada_symbols/right-flow-meter.svg
@@ -0,0 +1,964 @@
+
\ No newline at end of file
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
new file mode 100644
index 0000000000..f4b718eeb4
--- /dev/null
+++ b/application/src/main/data/json/system/scada_symbols/right-heat-pump.svg
@@ -0,0 +1,1044 @@
+
\ No newline at end of file
diff --git a/application/src/main/data/json/system/scada_symbols/right-motor-pump.svg b/application/src/main/data/json/system/scada_symbols/right-motor-pump.svg
new file mode 100644
index 0000000000..7ada9a91e3
--- /dev/null
+++ b/application/src/main/data/json/system/scada_symbols/right-motor-pump.svg
@@ -0,0 +1,526 @@
+
\ No newline at end of file
diff --git a/application/src/main/data/json/system/scada_symbols/right-tee-pipe.svg b/application/src/main/data/json/system/scada_symbols/right-tee-pipe.svg
new file mode 100644
index 0000000000..fce69fa7a3
--- /dev/null
+++ b/application/src/main/data/json/system/scada_symbols/right-tee-pipe.svg
@@ -0,0 +1,854 @@
+
\ No newline at end of file
diff --git a/application/src/main/data/json/system/scada_symbols/short-bottom-filter.svg b/application/src/main/data/json/system/scada_symbols/short-bottom-filter.svg
new file mode 100644
index 0000000000..b3235c5906
--- /dev/null
+++ b/application/src/main/data/json/system/scada_symbols/short-bottom-filter.svg
@@ -0,0 +1,88 @@
+
\ No newline at end of file
diff --git a/application/src/main/data/json/system/scada_symbols/short-left-drain-pipe.svg b/application/src/main/data/json/system/scada_symbols/short-left-drain-pipe.svg
new file mode 100644
index 0000000000..abf43aacb9
--- /dev/null
+++ b/application/src/main/data/json/system/scada_symbols/short-left-drain-pipe.svg
@@ -0,0 +1,160 @@
+
\ No newline at end of file
diff --git a/application/src/main/data/json/system/scada_symbols/short-right-drain-pipe.svg b/application/src/main/data/json/system/scada_symbols/short-right-drain-pipe.svg
new file mode 100644
index 0000000000..1b2ae3934a
--- /dev/null
+++ b/application/src/main/data/json/system/scada_symbols/short-right-drain-pipe.svg
@@ -0,0 +1,160 @@
+
\ No newline at end of file
diff --git a/application/src/main/data/json/system/scada_symbols/short-top-filter.svg b/application/src/main/data/json/system/scada_symbols/short-top-filter.svg
new file mode 100644
index 0000000000..d50eada2aa
--- /dev/null
+++ b/application/src/main/data/json/system/scada_symbols/short-top-filter.svg
@@ -0,0 +1,88 @@
+
\ No newline at end of file
diff --git a/application/src/main/data/json/system/scada_symbols/small-left-motor-pump.svg b/application/src/main/data/json/system/scada_symbols/small-left-motor-pump.svg
new file mode 100644
index 0000000000..85e617ff22
--- /dev/null
+++ b/application/src/main/data/json/system/scada_symbols/small-left-motor-pump.svg
@@ -0,0 +1,428 @@
+
\ No newline at end of file
diff --git a/application/src/main/data/json/system/scada_symbols/small-right-motor-pump.svg b/application/src/main/data/json/system/scada_symbols/small-right-motor-pump.svg
new file mode 100644
index 0000000000..7b9b1c260d
--- /dev/null
+++ b/application/src/main/data/json/system/scada_symbols/small-right-motor-pump.svg
@@ -0,0 +1,428 @@
+
\ No newline at end of file
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
new file mode 100644
index 0000000000..0acbbc9f38
--- /dev/null
+++ b/application/src/main/data/json/system/scada_symbols/small-spherical-tank.svg
@@ -0,0 +1,1426 @@
+
\ No newline at end of file
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
new file mode 100644
index 0000000000..069c54f444
--- /dev/null
+++ b/application/src/main/data/json/system/scada_symbols/spherical-tank.svg
@@ -0,0 +1,1456 @@
+
\ No newline at end of file
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
new file mode 100644
index 0000000000..2b3b3f010f
--- /dev/null
+++ b/application/src/main/data/json/system/scada_symbols/stand-cylindrical-tank.svg
@@ -0,0 +1,1462 @@
+
\ No newline at end of file
diff --git a/application/src/main/data/json/system/scada_symbols/stand-filter.svg b/application/src/main/data/json/system/scada_symbols/stand-filter.svg
new file mode 100644
index 0000000000..87d578605d
--- /dev/null
+++ b/application/src/main/data/json/system/scada_symbols/stand-filter.svg
@@ -0,0 +1,648 @@
+
\ No newline at end of file
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
new file mode 100644
index 0000000000..bea10fd317
--- /dev/null
+++ b/application/src/main/data/json/system/scada_symbols/stand-horizontal-tank.svg
@@ -0,0 +1,1468 @@
+
\ No newline at end of file
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
new file mode 100644
index 0000000000..f9dcb3b141
--- /dev/null
+++ b/application/src/main/data/json/system/scada_symbols/stand-vertical-short-tank.svg
@@ -0,0 +1,1435 @@
+
\ No newline at end of file
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
new file mode 100644
index 0000000000..078f58d810
--- /dev/null
+++ b/application/src/main/data/json/system/scada_symbols/stand-vertical-tank.svg
@@ -0,0 +1,1455 @@
+
\ No newline at end of file
diff --git a/application/src/main/data/json/system/scada_symbols/top-flow-meter.svg b/application/src/main/data/json/system/scada_symbols/top-flow-meter.svg
new file mode 100644
index 0000000000..8b785bcf46
--- /dev/null
+++ b/application/src/main/data/json/system/scada_symbols/top-flow-meter.svg
@@ -0,0 +1,964 @@
+
\ No newline at end of file
diff --git a/application/src/main/data/json/system/scada_symbols/top-right-elbow-pipe.svg b/application/src/main/data/json/system/scada_symbols/top-right-elbow-pipe.svg
new file mode 100644
index 0000000000..89beca4c85
--- /dev/null
+++ b/application/src/main/data/json/system/scada_symbols/top-right-elbow-pipe.svg
@@ -0,0 +1,558 @@
+
\ No newline at end of file
diff --git a/application/src/main/data/json/system/scada_symbols/top-tee-pipe.svg b/application/src/main/data/json/system/scada_symbols/top-tee-pipe.svg
new file mode 100644
index 0000000000..96199571ef
--- /dev/null
+++ b/application/src/main/data/json/system/scada_symbols/top-tee-pipe.svg
@@ -0,0 +1,854 @@
+
\ No newline at end of file
diff --git a/application/src/main/data/json/system/scada_symbols/vertical-ball-valve.svg b/application/src/main/data/json/system/scada_symbols/vertical-ball-valve.svg
new file mode 100644
index 0000000000..3aee266486
--- /dev/null
+++ b/application/src/main/data/json/system/scada_symbols/vertical-ball-valve.svg
@@ -0,0 +1,271 @@
+
\ No newline at end of file
diff --git a/application/src/main/data/json/system/scada_symbols/vertical-inline-flow-meter.svg b/application/src/main/data/json/system/scada_symbols/vertical-inline-flow-meter.svg
new file mode 100644
index 0000000000..c4485449ea
--- /dev/null
+++ b/application/src/main/data/json/system/scada_symbols/vertical-inline-flow-meter.svg
@@ -0,0 +1,948 @@
+
\ No newline at end of file
diff --git a/application/src/main/data/json/system/scada_symbols/vertical-pipe.svg b/application/src/main/data/json/system/scada_symbols/vertical-pipe.svg
new file mode 100644
index 0000000000..3f6d912459
--- /dev/null
+++ b/application/src/main/data/json/system/scada_symbols/vertical-pipe.svg
@@ -0,0 +1,421 @@
+
\ No newline at end of file
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
new file mode 100644
index 0000000000..1b1e825d48
--- /dev/null
+++ b/application/src/main/data/json/system/scada_symbols/vertical-short-tank.svg
@@ -0,0 +1,1374 @@
+
\ No newline at end of file
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
new file mode 100644
index 0000000000..5c3a4561db
--- /dev/null
+++ b/application/src/main/data/json/system/scada_symbols/vertical-tank.svg
@@ -0,0 +1,1394 @@
+
\ No newline at end of file
diff --git a/application/src/main/data/json/system/scada_symbols/vertical-wheel-valve.svg b/application/src/main/data/json/system/scada_symbols/vertical-wheel-valve.svg
new file mode 100644
index 0000000000..d43083040f
--- /dev/null
+++ b/application/src/main/data/json/system/scada_symbols/vertical-wheel-valve.svg
@@ -0,0 +1,300 @@
+
\ No newline at end of file
diff --git a/application/src/main/data/json/system/widget_bundles/scada_symbols.json b/application/src/main/data/json/system/widget_bundles/scada_symbols.json
new file mode 100644
index 0000000000..f1fbfbc297
--- /dev/null
+++ b/application/src/main/data/json/system/widget_bundles/scada_symbols.json
@@ -0,0 +1,14 @@
+{
+ "widgetsBundle": {
+ "alias": "scada_symbols",
+ "title": "SCADA symbols",
+ "scada": true,
+ "image": null,
+ "description": "Bundle with SCADA symbols",
+ "order": 9200,
+ "name": "SCADA symbols"
+ },
+ "widgetTypeFqns": [
+ "scada_symbol"
+ ]
+}
\ No newline at end of file
diff --git a/application/src/main/data/json/system/widget_bundles/scada_water_system_symbols.json b/application/src/main/data/json/system/widget_bundles/scada_water_system_symbols.json
new file mode 100644
index 0000000000..6f0edcd02f
--- /dev/null
+++ b/application/src/main/data/json/system/widget_bundles/scada_water_system_symbols.json
@@ -0,0 +1,70 @@
+{
+ "widgetsBundle": {
+ "alias": "scada_water_system_symbols",
+ "title": "SCADA water system symbols",
+ "scada": true,
+ "image": null,
+ "description": "Bundle with SCADA symbols for water system",
+ "order": 9300,
+ "name": "SCADA water system symbols"
+ },
+ "widgetTypeFqns": [
+ "horizontal_pipe",
+ "long_horizontal_pipe",
+ "vertical_pipe",
+ "long_vertical_pipe",
+ "left_bottom_elbow_pipe",
+ "bottom_right_elbow_pipe",
+ "top_right_elbow_pipe",
+ "left_top_elbow_pipe",
+ "cross_pipe",
+ "left_tee_pipe",
+ "bottom_tee_pipe",
+ "right_tee_pipe",
+ "top_tee_pipe",
+ "right_elbow_drain_pipe",
+ "left_elbow_drain_pipe",
+ "left_drain_pipe",
+ "right_drain_pipe",
+ "short_left_drain_pipe",
+ "short_right_drain_pipe",
+ "top_flow_meter",
+ "right_flow_meter",
+ "bottom_flow_meter",
+ "left_flow_meter",
+ "horizontal_inline_flow_meter",
+ "vertical_inline_flow_meter",
+ "centrifugal_pump",
+ "small_right_motor_pump",
+ "small_left_motor_pump",
+ "right_motor_pump",
+ "left_motor_pump",
+ "right_heat_pump",
+ "left_heat_pump",
+ "short_bottom_filter",
+ "long_bottom_filter",
+ "short_top_filter",
+ "long_top_filter",
+ "stand_filter",
+ "horizontal_wheel_valve",
+ "vertical_wheel_valve",
+ "horizontal_ball_valve",
+ "vertical_ball_valve",
+ "vertical_tank",
+ "stand_vertical_tank",
+ "cylindrical_tank",
+ "stand_cylindrical_tank",
+ "vertical_short_tank",
+ "stand_vertical_short_tank",
+ "large_cylindrical_tank",
+ "large_stand_cylindrical_tank",
+ "large_vertical_tank",
+ "large_stand_vertical_tank",
+ "horizontal_tank",
+ "stand_horizontal_tank",
+ "spherical_tank",
+ "small_spherical_tank",
+ "elevated_tank",
+ "pool"
+ ]
+}
\ No newline at end of file
diff --git a/application/src/main/data/json/system/widget_types/power_button.json b/application/src/main/data/json/system/widget_types/power_button.json
index 821873f0b4..30a3facd7b 100644
--- a/application/src/main/data/json/system/widget_types/power_button.json
+++ b/application/src/main/data/json/system/widget_types/power_button.json
@@ -2,7 +2,7 @@
"fqn": "power_button",
"name": "Power button",
"deprecated": false,
- "image": "tb-image:cG93ZXItYnV0dG9uLnN2Zw==:IlBvd2VyIGJ1dHRvbiIgc3lzdGVtIHdpZGdldCBpbWFnZQ==;data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAwIiBoZWlnaHQ9IjE2MCIgdmlld0JveD0iMCAwIDIwMCAxNjAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxnIGZpbHRlcj0idXJsKCNmaWx0ZXIwX2lfNDU4OF84ODQyMCkiPgo8cGF0aCBkPSJNMTQ2LjUgODBDMTQ2LjUgMTA1LjEyOSAxMjUuNjgxIDEyNS41IDEwMCAxMjUuNUM3NC4zMTg4IDEyNS41IDUzLjUgMTA1LjEyOSA1My41IDgwQzUzLjUgNTQuODcxIDc0LjMxODggMzQuNSAxMDAgMzQuNUMxMjUuNjgxIDM0LjUgMTQ2LjUgNTQuODcxIDE0Ni41IDgwWiIgZmlsbD0iIzNGNTJERCIvPgo8L2c+CjxwYXRoIGQ9Ik0xNDUuNSA4MEMxNDUuNSAxMDQuNTU2IDEyNS4xNSAxMjQuNSAxMDAgMTI0LjVDNzQuODUwNCAxMjQuNSA1NC41IDEwNC41NTYgNTQuNSA4MEM1NC41IDU1LjQ0MzcgNzQuODUwNCAzNS41IDEwMCAzNS41QzEyNS4xNSAzNS41IDE0NS41IDU1LjQ0MzcgMTQ1LjUgODBaIiBzdHJva2U9IndoaXRlIiBzdHJva2Utd2lkdGg9IjIiLz4KPGcgZmlsdGVyPSJ1cmwoI2ZpbHRlcjFfZF80NTg4Xzg4NDIwKSI+CjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBkPSJNMTAwIDEzNS41QzEzMS4yMDQgMTM1LjUgMTU2LjUgMTEwLjY1MiAxNTYuNSA4MEMxNTYuNSA0OS4zNDgyIDEzMS4yMDQgMjQuNSAxMDAgMjQuNUM2OC43OTU5IDI0LjUgNDMuNSA0OS4zNDgyIDQzLjUgODBDNDMuNSAxMTAuNjUyIDY4Ljc5NTkgMTM1LjUgMTAwIDEzNS41Wk0xMDAgMTI1LjVDMTI1LjY4MSAxMjUuNSAxNDYuNSAxMDUuMTI5IDE0Ni41IDgwQzE0Ni41IDU0Ljg3MSAxMjUuNjgxIDM0LjUgMTAwIDM0LjVDNzQuMzE4OCAzNC41IDUzLjUgNTQuODcxIDUzLjUgODBDNTMuNSAxMDUuMTI5IDc0LjMxODggMTI1LjUgMTAwIDEyNS41WiIgZmlsbD0idXJsKCNwYWludDBfbGluZWFyXzQ1ODhfODg0MjApIi8+CjwvZz4KPHBhdGggZD0iTTk4LjM1NzQgNzkuMTg1NVY4MC4xNzM4Qzk4LjM1NzQgODEuMzQ4MyA5OC4yMTA2IDgyLjQwMSA5Ny45MTcgODMuMzMyQzk3LjYyMzQgODQuMjYzIDk3LjIwMDggODUuMDU0NCA5Ni42NDk0IDg1LjcwNjFDOTYuMDk4IDg2LjM1NzcgOTUuNDM1NSA4Ni44NTU1IDk0LjY2MjEgODcuMTk5MkM5My44OTU4IDg3LjU0MyA5My4wMzY1IDg3LjcxNDggOTIuMDg0IDg3LjcxNDhDOTEuMTYwMiA4Ny43MTQ4IDkwLjMxMTUgODcuNTQzIDg5LjUzODEgODcuMTk5MkM4OC43NzE4IDg2Ljg1NTUgODguMTA1OCA4Ni4zNTc3IDg3LjU0IDg1LjcwNjFDODYuOTgxNCA4NS4wNTQ0IDg2LjU0ODIgODQuMjYzIDg2LjI0MDIgODMuMzMyQzg1LjkzMjMgODIuNDAxIDg1Ljc3ODMgODEuMzQ4MyA4NS43NzgzIDgwLjE3MzhWNzkuMTg1NUM4NS43NzgzIDc4LjAxMTEgODUuOTI4NyA3Ni45NjE5IDg2LjIyOTUgNzYuMDM4MUM4Ni41Mzc0IDc1LjEwNzEgODYuOTcwNyA3NC4zMTU4IDg3LjUyOTMgNzMuNjY0MUM4OC4wODc5IDczLjAwNTIgODguNzUwMyA3Mi41MDM5IDg5LjUxNjYgNzIuMTYwMkM5MC4yOSA3MS44MTY0IDkxLjEzODcgNzEuNjQ0NSA5Mi4wNjI1IDcxLjY0NDVDOTMuMDE1IDcxLjY0NDUgOTMuODc0MyA3MS44MTY0IDk0LjY0MDYgNzIuMTYwMkM5NS40MTQxIDcyLjUwMzkgOTYuMDc2NSA3My4wMDUyIDk2LjYyNzkgNzMuNjY0MUM5Ny4xODY1IDc0LjMxNTggOTcuNjEyNiA3NS4xMDcxIDk3LjkwNjIgNzYuMDM4MUM5OC4yMDcgNzYuOTYxOSA5OC4zNTc0IDc4LjAxMTEgOTguMzU3NCA3OS4xODU1Wk05Ni4zMDU3IDgwLjE3MzhWNzkuMTY0MUM5Ni4zMDU3IDc4LjIzMzEgOTYuMjA5IDc3LjQwOTUgOTYuMDE1NiA3Ni42OTM0Qzk1LjgyOTQgNzUuOTc3MiA5NS41NTM3IDc1LjM3NTcgOTUuMTg4NSA3NC44ODg3Qzk0LjgyMzIgNzQuNDAxNyA5NC4zNzU3IDc0LjAzMjkgOTMuODQ1NyA3My43ODIyQzkzLjMyMjkgNzMuNTMxNiA5Mi43Mjg1IDczLjQwNjIgOTIuMDYyNSA3My40MDYyQzkxLjQxOCA3My40MDYyIDkwLjgzNDMgNzMuNTMxNiA5MC4zMTE1IDczLjc4MjJDODkuNzk1OSA3NC4wMzI5IDg5LjM1MTkgNzQuNDAxNyA4OC45Nzk1IDc0Ljg4ODdDODguNjE0MyA3NS4zNzU3IDg4LjMzMTQgNzUuOTc3MiA4OC4xMzA5IDc2LjY5MzRDODcuOTMwMyA3Ny40MDk1IDg3LjgzMDEgNzguMjMzMSA4Ny44MzAxIDc5LjE2NDFWODAuMTczOEM4Ny44MzAxIDgxLjExMiA4Ny45MzAzIDgxLjk0MjcgODguMTMwOSA4Mi42NjZDODguMzMxNCA4My4zODIyIDg4LjYxNzggODMuOTg3MyA4OC45OTAyIDg0LjQ4MTRDODkuMzY5OCA4NC45Njg0IDg5LjgxNzQgODUuMzM3MiA5MC4zMzMgODUuNTg3OUM5MC44NTU4IDg1LjgzODUgOTEuNDM5NSA4NS45NjM5IDkyLjA4NCA4NS45NjM5QzkyLjc1NzIgODUuOTYzOSA5My4zNTUxIDg1LjgzODUgOTMuODc3OSA4NS41ODc5Qzk0LjQwMDcgODUuMzM3MiA5NC44NDExIDg0Ljk2ODQgOTUuMTk5MiA4NC40ODE0Qzk1LjU2NDUgODMuOTg3MyA5NS44NDAyIDgzLjM4MjIgOTYuMDI2NCA4Mi42NjZDOTYuMjEyNiA4MS45NDI3IDk2LjMwNTcgODEuMTEyIDk2LjMwNTcgODAuMTczOFpNMTEzLjQ5MyA3MS44NTk0Vjg3LjVIMTExLjQwOUwxMDMuNTM1IDc1LjQzNjVWODcuNUgxMDEuNDYyVjcxLjg1OTRIMTAzLjUzNUwxMTEuNDQxIDgzLjk1NTFWNzEuODU5NEgxMTMuNDkzWiIgZmlsbD0id2hpdGUiLz4KPGRlZnM+CjxmaWx0ZXIgaWQ9ImZpbHRlcjBfaV80NTg4Xzg4NDIwIiB4PSI0OC41IiB5PSIzNC41IiB3aWR0aD0iOTgiIGhlaWdodD0iOTYiIGZpbHRlclVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgY29sb3ItaW50ZXJwb2xhdGlvbi1maWx0ZXJzPSJzUkdCIj4KPGZlRmxvb2QgZmxvb2Qtb3BhY2l0eT0iMCIgcmVzdWx0PSJCYWNrZ3JvdW5kSW1hZ2VGaXgiLz4KPGZlQmxlbmQgbW9kZT0ibm9ybWFsIiBpbj0iU291cmNlR3JhcGhpYyIgaW4yPSJCYWNrZ3JvdW5kSW1hZ2VGaXgiIHJlc3VsdD0ic2hhcGUiLz4KPGZlQ29sb3JNYXRyaXggaW49IlNvdXJjZUFscGhhIiB0eXBlPSJtYXRyaXgiIHZhbHVlcz0iMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMTI3IDAiIHJlc3VsdD0iaGFyZEFscGhhIi8+CjxmZU9mZnNldCBkeD0iLTUiIGR5PSI1Ii8+CjxmZUdhdXNzaWFuQmx1ciBzdGREZXZpYXRpb249IjQiLz4KPGZlQ29tcG9zaXRlIGluMj0iaGFyZEFscGhhIiBvcGVyYXRvcj0iYXJpdGhtZXRpYyIgazI9Ii0xIiBrMz0iMSIvPgo8ZmVDb2xvck1hdHJpeCB0eXBlPSJtYXRyaXgiIHZhbHVlcz0iMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMC4xNSAwIi8+CjxmZUJsZW5kIG1vZGU9Im5vcm1hbCIgaW4yPSJzaGFwZSIgcmVzdWx0PSJlZmZlY3QxX2lubmVyU2hhZG93XzQ1ODhfODg0MjAiLz4KPC9maWx0ZXI+CjxmaWx0ZXIgaWQ9ImZpbHRlcjFfZF80NTg4Xzg4NDIwIiB4PSIzNS41IiB5PSIyMC41IiB3aWR0aD0iMTI5IiBoZWlnaHQ9IjEyNyIgZmlsdGVyVW5pdHM9InVzZXJTcGFjZU9uVXNlIiBjb2xvci1pbnRlcnBvbGF0aW9uLWZpbHRlcnM9InNSR0IiPgo8ZmVGbG9vZCBmbG9vZC1vcGFjaXR5PSIwIiByZXN1bHQ9IkJhY2tncm91bmRJbWFnZUZpeCIvPgo8ZmVDb2xvck1hdHJpeCBpbj0iU291cmNlQWxwaGEiIHR5cGU9Im1hdHJpeCIgdmFsdWVzPSIwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAxMjcgMCIgcmVzdWx0PSJoYXJkQWxwaGEiLz4KPGZlT2Zmc2V0IGR5PSI0Ii8+CjxmZUdhdXNzaWFuQmx1ciBzdGREZXZpYXRpb249IjQiLz4KPGZlQ29tcG9zaXRlIGluMj0iaGFyZEFscGhhIiBvcGVyYXRvcj0ib3V0Ii8+CjxmZUNvbG9yTWF0cml4IHR5cGU9Im1hdHJpeCIgdmFsdWVzPSIwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwLjA4IDAiLz4KPGZlQmxlbmQgbW9kZT0ibm9ybWFsIiBpbjI9IkJhY2tncm91bmRJbWFnZUZpeCIgcmVzdWx0PSJlZmZlY3QxX2Ryb3BTaGFkb3dfNDU4OF84ODQyMCIvPgo8ZmVCbGVuZCBtb2RlPSJub3JtYWwiIGluPSJTb3VyY2VHcmFwaGljIiBpbjI9ImVmZmVjdDFfZHJvcFNoYWRvd180NTg4Xzg4NDIwIiByZXN1bHQ9InNoYXBlIi8+CjwvZmlsdGVyPgo8bGluZWFyR3JhZGllbnQgaWQ9InBhaW50MF9saW5lYXJfNDU4OF84ODQyMCIgeDE9IjY1Ljg5NjQiIHkxPSIxMjQuNSIgeDI9IjEyOS41MTkiIHkyPSIzMy45MjQ4IiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+CjxzdG9wIHN0b3AtY29sb3I9IiNDQ0NDQ0MiLz4KPHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSJ3aGl0ZSIvPgo8L2xpbmVhckdyYWRpZW50Pgo8L2RlZnM+Cjwvc3ZnPgo=",
+ "image": "tb-image:cG93ZXItYnV0dG9uLnN2Zw==:IlBvd2VyIGJ1dHRvbiIgc3lzdGVtIHdpZGdldCBpbWFnZQ==:SU1BR0U=;data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAwIiBoZWlnaHQ9IjE2MCIgdmlld0JveD0iMCAwIDIwMCAxNjAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxnIGZpbHRlcj0idXJsKCNmaWx0ZXIwX2lfNDU4OF84ODQyMCkiPgo8cGF0aCBkPSJNMTQ2LjUgODBDMTQ2LjUgMTA1LjEyOSAxMjUuNjgxIDEyNS41IDEwMCAxMjUuNUM3NC4zMTg4IDEyNS41IDUzLjUgMTA1LjEyOSA1My41IDgwQzUzLjUgNTQuODcxIDc0LjMxODggMzQuNSAxMDAgMzQuNUMxMjUuNjgxIDM0LjUgMTQ2LjUgNTQuODcxIDE0Ni41IDgwWiIgZmlsbD0iIzNGNTJERCIvPgo8L2c+CjxwYXRoIGQ9Ik0xNDUuNSA4MEMxNDUuNSAxMDQuNTU2IDEyNS4xNSAxMjQuNSAxMDAgMTI0LjVDNzQuODUwNCAxMjQuNSA1NC41IDEwNC41NTYgNTQuNSA4MEM1NC41IDU1LjQ0MzcgNzQuODUwNCAzNS41IDEwMCAzNS41QzEyNS4xNSAzNS41IDE0NS41IDU1LjQ0MzcgMTQ1LjUgODBaIiBzdHJva2U9IndoaXRlIiBzdHJva2Utd2lkdGg9IjIiLz4KPGcgZmlsdGVyPSJ1cmwoI2ZpbHRlcjFfZF80NTg4Xzg4NDIwKSI+CjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBkPSJNMTAwIDEzNS41QzEzMS4yMDQgMTM1LjUgMTU2LjUgMTEwLjY1MiAxNTYuNSA4MEMxNTYuNSA0OS4zNDgyIDEzMS4yMDQgMjQuNSAxMDAgMjQuNUM2OC43OTU5IDI0LjUgNDMuNSA0OS4zNDgyIDQzLjUgODBDNDMuNSAxMTAuNjUyIDY4Ljc5NTkgMTM1LjUgMTAwIDEzNS41Wk0xMDAgMTI1LjVDMTI1LjY4MSAxMjUuNSAxNDYuNSAxMDUuMTI5IDE0Ni41IDgwQzE0Ni41IDU0Ljg3MSAxMjUuNjgxIDM0LjUgMTAwIDM0LjVDNzQuMzE4OCAzNC41IDUzLjUgNTQuODcxIDUzLjUgODBDNTMuNSAxMDUuMTI5IDc0LjMxODggMTI1LjUgMTAwIDEyNS41WiIgZmlsbD0idXJsKCNwYWludDBfbGluZWFyXzQ1ODhfODg0MjApIi8+CjwvZz4KPHBhdGggZD0iTTk4LjM1NzQgNzkuMTg1NVY4MC4xNzM4Qzk4LjM1NzQgODEuMzQ4MyA5OC4yMTA2IDgyLjQwMSA5Ny45MTcgODMuMzMyQzk3LjYyMzQgODQuMjYzIDk3LjIwMDggODUuMDU0NCA5Ni42NDk0IDg1LjcwNjFDOTYuMDk4IDg2LjM1NzcgOTUuNDM1NSA4Ni44NTU1IDk0LjY2MjEgODcuMTk5MkM5My44OTU4IDg3LjU0MyA5My4wMzY1IDg3LjcxNDggOTIuMDg0IDg3LjcxNDhDOTEuMTYwMiA4Ny43MTQ4IDkwLjMxMTUgODcuNTQzIDg5LjUzODEgODcuMTk5MkM4OC43NzE4IDg2Ljg1NTUgODguMTA1OCA4Ni4zNTc3IDg3LjU0IDg1LjcwNjFDODYuOTgxNCA4NS4wNTQ0IDg2LjU0ODIgODQuMjYzIDg2LjI0MDIgODMuMzMyQzg1LjkzMjMgODIuNDAxIDg1Ljc3ODMgODEuMzQ4MyA4NS43NzgzIDgwLjE3MzhWNzkuMTg1NUM4NS43NzgzIDc4LjAxMTEgODUuOTI4NyA3Ni45NjE5IDg2LjIyOTUgNzYuMDM4MUM4Ni41Mzc0IDc1LjEwNzEgODYuOTcwNyA3NC4zMTU4IDg3LjUyOTMgNzMuNjY0MUM4OC4wODc5IDczLjAwNTIgODguNzUwMyA3Mi41MDM5IDg5LjUxNjYgNzIuMTYwMkM5MC4yOSA3MS44MTY0IDkxLjEzODcgNzEuNjQ0NSA5Mi4wNjI1IDcxLjY0NDVDOTMuMDE1IDcxLjY0NDUgOTMuODc0MyA3MS44MTY0IDk0LjY0MDYgNzIuMTYwMkM5NS40MTQxIDcyLjUwMzkgOTYuMDc2NSA3My4wMDUyIDk2LjYyNzkgNzMuNjY0MUM5Ny4xODY1IDc0LjMxNTggOTcuNjEyNiA3NS4xMDcxIDk3LjkwNjIgNzYuMDM4MUM5OC4yMDcgNzYuOTYxOSA5OC4zNTc0IDc4LjAxMTEgOTguMzU3NCA3OS4xODU1Wk05Ni4zMDU3IDgwLjE3MzhWNzkuMTY0MUM5Ni4zMDU3IDc4LjIzMzEgOTYuMjA5IDc3LjQwOTUgOTYuMDE1NiA3Ni42OTM0Qzk1LjgyOTQgNzUuOTc3MiA5NS41NTM3IDc1LjM3NTcgOTUuMTg4NSA3NC44ODg3Qzk0LjgyMzIgNzQuNDAxNyA5NC4zNzU3IDc0LjAzMjkgOTMuODQ1NyA3My43ODIyQzkzLjMyMjkgNzMuNTMxNiA5Mi43Mjg1IDczLjQwNjIgOTIuMDYyNSA3My40MDYyQzkxLjQxOCA3My40MDYyIDkwLjgzNDMgNzMuNTMxNiA5MC4zMTE1IDczLjc4MjJDODkuNzk1OSA3NC4wMzI5IDg5LjM1MTkgNzQuNDAxNyA4OC45Nzk1IDc0Ljg4ODdDODguNjE0MyA3NS4zNzU3IDg4LjMzMTQgNzUuOTc3MiA4OC4xMzA5IDc2LjY5MzRDODcuOTMwMyA3Ny40MDk1IDg3LjgzMDEgNzguMjMzMSA4Ny44MzAxIDc5LjE2NDFWODAuMTczOEM4Ny44MzAxIDgxLjExMiA4Ny45MzAzIDgxLjk0MjcgODguMTMwOSA4Mi42NjZDODguMzMxNCA4My4zODIyIDg4LjYxNzggODMuOTg3MyA4OC45OTAyIDg0LjQ4MTRDODkuMzY5OCA4NC45Njg0IDg5LjgxNzQgODUuMzM3MiA5MC4zMzMgODUuNTg3OUM5MC44NTU4IDg1LjgzODUgOTEuNDM5NSA4NS45NjM5IDkyLjA4NCA4NS45NjM5QzkyLjc1NzIgODUuOTYzOSA5My4zNTUxIDg1LjgzODUgOTMuODc3OSA4NS41ODc5Qzk0LjQwMDcgODUuMzM3MiA5NC44NDExIDg0Ljk2ODQgOTUuMTk5MiA4NC40ODE0Qzk1LjU2NDUgODMuOTg3MyA5NS44NDAyIDgzLjM4MjIgOTYuMDI2NCA4Mi42NjZDOTYuMjEyNiA4MS45NDI3IDk2LjMwNTcgODEuMTEyIDk2LjMwNTcgODAuMTczOFpNMTEzLjQ5MyA3MS44NTk0Vjg3LjVIMTExLjQwOUwxMDMuNTM1IDc1LjQzNjVWODcuNUgxMDEuNDYyVjcxLjg1OTRIMTAzLjUzNUwxMTEuNDQxIDgzLjk1NTFWNzEuODU5NEgxMTMuNDkzWiIgZmlsbD0id2hpdGUiLz4KPGRlZnM+CjxmaWx0ZXIgaWQ9ImZpbHRlcjBfaV80NTg4Xzg4NDIwIiB4PSI0OC41IiB5PSIzNC41IiB3aWR0aD0iOTgiIGhlaWdodD0iOTYiIGZpbHRlclVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgY29sb3ItaW50ZXJwb2xhdGlvbi1maWx0ZXJzPSJzUkdCIj4KPGZlRmxvb2QgZmxvb2Qtb3BhY2l0eT0iMCIgcmVzdWx0PSJCYWNrZ3JvdW5kSW1hZ2VGaXgiLz4KPGZlQmxlbmQgbW9kZT0ibm9ybWFsIiBpbj0iU291cmNlR3JhcGhpYyIgaW4yPSJCYWNrZ3JvdW5kSW1hZ2VGaXgiIHJlc3VsdD0ic2hhcGUiLz4KPGZlQ29sb3JNYXRyaXggaW49IlNvdXJjZUFscGhhIiB0eXBlPSJtYXRyaXgiIHZhbHVlcz0iMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMTI3IDAiIHJlc3VsdD0iaGFyZEFscGhhIi8+CjxmZU9mZnNldCBkeD0iLTUiIGR5PSI1Ii8+CjxmZUdhdXNzaWFuQmx1ciBzdGREZXZpYXRpb249IjQiLz4KPGZlQ29tcG9zaXRlIGluMj0iaGFyZEFscGhhIiBvcGVyYXRvcj0iYXJpdGhtZXRpYyIgazI9Ii0xIiBrMz0iMSIvPgo8ZmVDb2xvck1hdHJpeCB0eXBlPSJtYXRyaXgiIHZhbHVlcz0iMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMC4xNSAwIi8+CjxmZUJsZW5kIG1vZGU9Im5vcm1hbCIgaW4yPSJzaGFwZSIgcmVzdWx0PSJlZmZlY3QxX2lubmVyU2hhZG93XzQ1ODhfODg0MjAiLz4KPC9maWx0ZXI+CjxmaWx0ZXIgaWQ9ImZpbHRlcjFfZF80NTg4Xzg4NDIwIiB4PSIzNS41IiB5PSIyMC41IiB3aWR0aD0iMTI5IiBoZWlnaHQ9IjEyNyIgZmlsdGVyVW5pdHM9InVzZXJTcGFjZU9uVXNlIiBjb2xvci1pbnRlcnBvbGF0aW9uLWZpbHRlcnM9InNSR0IiPgo8ZmVGbG9vZCBmbG9vZC1vcGFjaXR5PSIwIiByZXN1bHQ9IkJhY2tncm91bmRJbWFnZUZpeCIvPgo8ZmVDb2xvck1hdHJpeCBpbj0iU291cmNlQWxwaGEiIHR5cGU9Im1hdHJpeCIgdmFsdWVzPSIwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAxMjcgMCIgcmVzdWx0PSJoYXJkQWxwaGEiLz4KPGZlT2Zmc2V0IGR5PSI0Ii8+CjxmZUdhdXNzaWFuQmx1ciBzdGREZXZpYXRpb249IjQiLz4KPGZlQ29tcG9zaXRlIGluMj0iaGFyZEFscGhhIiBvcGVyYXRvcj0ib3V0Ii8+CjxmZUNvbG9yTWF0cml4IHR5cGU9Im1hdHJpeCIgdmFsdWVzPSIwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwLjA4IDAiLz4KPGZlQmxlbmQgbW9kZT0ibm9ybWFsIiBpbjI9IkJhY2tncm91bmRJbWFnZUZpeCIgcmVzdWx0PSJlZmZlY3QxX2Ryb3BTaGFkb3dfNDU4OF84ODQyMCIvPgo8ZmVCbGVuZCBtb2RlPSJub3JtYWwiIGluPSJTb3VyY2VHcmFwaGljIiBpbjI9ImVmZmVjdDFfZHJvcFNoYWRvd180NTg4Xzg4NDIwIiByZXN1bHQ9InNoYXBlIi8+CjwvZmlsdGVyPgo8bGluZWFyR3JhZGllbnQgaWQ9InBhaW50MF9saW5lYXJfNDU4OF84ODQyMCIgeDE9IjY1Ljg5NjQiIHkxPSIxMjQuNSIgeDI9IjEyOS41MTkiIHkyPSIzMy45MjQ4IiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+CjxzdG9wIHN0b3AtY29sb3I9IiNDQ0NDQ0MiLz4KPHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSJ3aGl0ZSIvPgo8L2xpbmVhckdyYWRpZW50Pgo8L2RlZnM+Cjwvc3ZnPgo=",
"description": "Sends the command to the device or updates attribute/time series when the user pushes the button. Widget settings will enable you to configure behavior how to fetch the initial state and what to trigger when power on/off states.",
"descriptor": {
"type": "rpc",
@@ -11,7 +11,7 @@
"resources": [],
"templateHtml": "\n",
"templateCss": "",
- "controllerScript": "self.onInit = function() {\n self.ctx.$scope.actionWidget.onInit();\n}\n\nself.typeParameters = function() {\n return {\n previewWidth: '280px',\n previewHeight: '280px',\n embedTitlePanel: true,\n displayRpcMessageToast: false\n };\n};\n\nself.onDestroy = function() {\n}\n",
+ "controllerScript": "self.onInit = function() {\n self.ctx.$scope.actionWidget.onInit();\n}\n\nself.typeParameters = function() {\n return {\n previewWidth: '280px',\n previewHeight: '280px',\n embedTitlePanel: true,\n overflowVisible: true,\n displayRpcMessageToast: false\n };\n};\n\nself.onDestroy = function() {\n}\n",
"settingsSchema": "",
"dataKeySettingsSchema": "{}\n",
"settingsDirective": "tb-power-button-widget-settings",
diff --git a/application/src/main/data/json/system/widget_types/scada_symbol.json b/application/src/main/data/json/system/widget_types/scada_symbol.json
new file mode 100644
index 0000000000..15c12e1b81
--- /dev/null
+++ b/application/src/main/data/json/system/widget_types/scada_symbol.json
@@ -0,0 +1,24 @@
+{
+ "fqn": "scada_symbol",
+ "name": "SCADA symbol",
+ "deprecated": false,
+ "scada": true,
+ "image": null,
+ "description": "",
+ "descriptor": {
+ "type": "rpc",
+ "sizeX": 3,
+ "sizeY": 3,
+ "resources": [],
+ "templateHtml": "\n",
+ "templateCss": "",
+ "controllerScript": "self.onInit = function() {\n self.ctx.$scope.actionWidget.onInit();\n}\n\nself.typeParameters = function() {\n return {\n previewWidth: '300px',\n previewHeight: '320px',\n embedTitlePanel: true,\n targetDeviceOptional: true,\n displayRpcMessageToast: false\n };\n};\n\nself.onDestroy = function() {\n}\n",
+ "settingsSchema": "",
+ "dataKeySettingsSchema": "{}\n",
+ "settingsDirective": "tb-scada-symbol-widget-settings",
+ "hasBasicMode": true,
+ "basicModeDirective": "tb-scada-symbol-basic-config",
+ "defaultConfig": "{\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"background\":{\"type\":\"color\",\"imageUrl\":null,\"color\":\"rgb(255, 255, 255)\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"padding\":\"12px\",\"scadaSymbolUrl\":\"\",\"scadaSymbolObjectSettings\":{\"behavior\":{},\"properties\":{}}},\"title\":\"SCADA symbol\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"actions\":{},\"widgetCss\":\"\",\"noDataDisplayMessage\":\"\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":null,\"lineHeight\":\"1.6\"},\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleStyle\":null,\"pageSize\":1024,\"titleIcon\":\"mdi:lightbulb-outline\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"configMode\":\"basic\",\"targetDevice\":null,\"titleColor\":null,\"borderRadius\":\"0px\",\"margin\":\"0px\"}"
+ },
+ "tags": null
+}
\ No newline at end of file
diff --git a/application/src/main/data/json/system/widget_types/toggle_button.json b/application/src/main/data/json/system/widget_types/toggle_button.json
index f3f3449b13..e756b31e0f 100644
--- a/application/src/main/data/json/system/widget_types/toggle_button.json
+++ b/application/src/main/data/json/system/widget_types/toggle_button.json
@@ -2,7 +2,7 @@
"fqn": "toggle_button",
"name": "Toggle button",
"deprecated": false,
- "image": "tb-image:VG9nZ2xlIGJ1dHRvbnMuc3Zn:IlRvZ2dsZSBidXR0b24iIHN5c3RlbSB3aWRnZXQgaW1hZ2U=;data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjE2IiBoZWlnaHQ9IjE2MCIgdmlld0JveD0iMCAwIDIxNiAxNjAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxnIGZpbHRlcj0idXJsKCNmaWx0ZXIwX2RfNDY5N18zNjcxMykiPgo8cmVjdCB4PSI4Ljc1IiB5PSIxNi43NSIgd2lkdGg9IjE5OC41IiBoZWlnaHQ9IjU4LjUiIHJ4PSIzLjI1IiBzdHJva2U9IiMxOTgwMzgiIHN0cm9rZS13aWR0aD0iMS41IiBzaGFwZS1yZW5kZXJpbmc9ImNyaXNwRWRnZXMiLz4KPG1hc2sgaWQ9Im1hc2swXzQ2OTdfMzY3MTMiIHN0eWxlPSJtYXNrLXR5cGU6YWxwaGEiIG1hc2tVbml0cz0idXNlclNwYWNlT25Vc2UiIHg9IjYyIiB5PSIzNCIgd2lkdGg9IjI1IiBoZWlnaHQ9IjI0Ij4KPHJlY3QgeD0iNjIuNSIgeT0iMzQiIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgZmlsbD0iI0Q5RDlEOSIvPgo8L21hc2s+CjxnIG1hc2s9InVybCgjbWFzazBfNDY5N18zNjcxMykiPgo8cGF0aCBkPSJNNjguNSA0Mkg3Ny41VjQwQzc3LjUgMzkuMTY2NyA3Ny4yMDgzIDM4LjQ1ODMgNzYuNjI1IDM3Ljg3NUM3Ni4wNDE3IDM3LjI5MTcgNzUuMzMzMyAzNyA3NC41IDM3QzczLjY2NjcgMzcgNzIuOTU4MyAzNy4yOTE3IDcyLjM3NSAzNy44NzVDNzEuNzkxNyAzOC40NTgzIDcxLjUgMzkuMTY2NyA3MS41IDQwSDY5LjVDNjkuNSAzOC42MTY3IDY5Ljk4NzUgMzcuNDM3NSA3MC45NjI1IDM2LjQ2MjVDNzEuOTM3NSAzNS40ODc1IDczLjExNjcgMzUgNzQuNSAzNUM3NS44ODMzIDM1IDc3LjA2MjUgMzUuNDg3NSA3OC4wMzc1IDM2LjQ2MjVDNzkuMDEyNSAzNy40Mzc1IDc5LjUgMzguNjE2NyA3OS41IDQwVjQySDgwLjVDODEuMDUgNDIgODEuNTIwOCA0Mi4xOTU4IDgxLjkxMjUgNDIuNTg3NUM4Mi4zMDQyIDQyLjk3OTIgODIuNSA0My40NSA4Mi41IDQ0VjU0QzgyLjUgNTQuNTUgODIuMzA0MiA1NS4wMjA4IDgxLjkxMjUgNTUuNDEyNUM4MS41MjA4IDU1LjgwNDIgODEuMDUgNTYgODAuNSA1Nkg2OC41QzY3Ljk1IDU2IDY3LjQ3OTIgNTUuODA0MiA2Ny4wODc1IDU1LjQxMjVDNjYuNjk1OCA1NS4wMjA4IDY2LjUgNTQuNTUgNjYuNSA1NFY0NEM2Ni41IDQzLjQ1IDY2LjY5NTggNDIuOTc5MiA2Ny4wODc1IDQyLjU4NzVDNjcuNDc5MiA0Mi4xOTU4IDY3Ljk1IDQyIDY4LjUgNDJaTTY4LjUgNTRIODAuNVY0NEg2OC41VjU0Wk03NC41IDUxQzc1LjA1IDUxIDc1LjUyMDggNTAuODA0MiA3NS45MTI1IDUwLjQxMjVDNzYuMzA0MiA1MC4wMjA4IDc2LjUgNDkuNTUgNzYuNSA0OUM3Ni41IDQ4LjQ1IDc2LjMwNDIgNDcuOTc5MiA3NS45MTI1IDQ3LjU4NzVDNzUuNTIwOCA0Ny4xOTU4IDc1LjA1IDQ3IDc0LjUgNDdDNzMuOTUgNDcgNzMuNDc5MiA0Ny4xOTU4IDczLjA4NzUgNDcuNTg3NUM3Mi42OTU4IDQ3Ljk3OTIgNzIuNSA0OC40NSA3Mi41IDQ5QzcyLjUgNDkuNTUgNzIuNjk1OCA1MC4wMjA4IDczLjA4NzUgNTAuNDEyNUM3My40NzkyIDUwLjgwNDIgNzMuOTUgNTEgNzQuNSA1MVoiIGZpbGw9IiMxOTgwMzgiLz4KPC9nPgo8cGF0aCBkPSJNMTAyLjEzMSA0NS4yNVY0NS45NTMxQzEwMi4xMzEgNDYuOTE5OSAxMDIuMDA1IDQ3Ljc4NzEgMTAxLjc1MyA0OC41NTQ3QzEwMS41MDEgNDkuMzIyMyAxMDEuMTQxIDQ5Ljk3NTYgMTAwLjY3MiA1MC41MTQ2QzEwMC4yMDkgNTEuMDUzNyA5OS42NTIzIDUxLjQ2NjggOTkuMDAyIDUxLjc1MzlDOTguMzUxNiA1Mi4wMzUyIDk3LjYzMDkgNTIuMTc1OCA5Ni44Mzk4IDUyLjE3NThDOTYuMDU0NyA1Mi4xNzU4IDk1LjMzNjkgNTIuMDM1MiA5NC42ODY1IDUxLjc1MzlDOTQuMDQyIDUxLjQ2NjggOTMuNDgyNCA1MS4wNTM3IDkzLjAwNzggNTAuNTE0NkM5Mi41MzMyIDQ5Ljk3NTYgOTIuMTY0MSA0OS4zMjIzIDkxLjkwMDQgNDguNTU0N0M5MS42NDI2IDQ3Ljc4NzEgOTEuNTEzNyA0Ni45MTk5IDkxLjUxMzcgNDUuOTUzMVY0NS4yNUM5MS41MTM3IDQ0LjI4MzIgOTEuNjQyNiA0My40MTg5IDkxLjkwMDQgNDIuNjU3MkM5Mi4xNTgyIDQxLjg4OTYgOTIuNTIxNSA0MS4yMzYzIDkyLjk5MDIgNDAuNjk3M0M5My40NjQ4IDQwLjE1MjMgOTQuMDI0NCAzOS43MzkzIDk0LjY2ODkgMzkuNDU4Qzk1LjMxOTMgMzkuMTcwOSA5Ni4wMzcxIDM5LjAyNzMgOTYuODIyMyAzOS4wMjczQzk3LjYxMzMgMzkuMDI3MyA5OC4zMzQgMzkuMTcwOSA5OC45ODQ0IDM5LjQ1OEM5OS42MzQ4IDM5LjczOTMgMTAwLjE5NCA0MC4xNTIzIDEwMC42NjMgNDAuNjk3M0MxMDEuMTMyIDQxLjIzNjMgMTAxLjQ5MiA0MS44ODk2IDEwMS43NDQgNDIuNjU3MkMxMDIuMDAyIDQzLjQxODkgMTAyLjEzMSA0NC4yODMyIDEwMi4xMzEgNDUuMjVaTTk5LjkyNDggNDUuOTUzMVY0NS4yMzI0Qzk5LjkyNDggNDQuNTE3NiA5OS44NTQ1IDQzLjg4NzcgOTkuNzEzOSA0My4zNDI4Qzk5LjU3OTEgNDIuNzkyIDk5LjM3NyA0Mi4zMzIgOTkuMTA3NCA0MS45NjI5Qzk4Ljg0MzggNDEuNTg3OSA5OC41MTg2IDQxLjMwNjYgOTguMTMxOCA0MS4xMTkxQzk3Ljc0NTEgNDAuOTI1OCA5Ny4zMDg2IDQwLjgyOTEgOTYuODIyMyA0MC44MjkxQzk2LjMzNTkgNDAuODI5MSA5NS45MDIzIDQwLjkyNTggOTUuNTIxNSA0MS4xMTkxQzk1LjE0MDYgNDEuMzA2NiA5NC44MTU0IDQxLjU4NzkgOTQuNTQ1OSA0MS45NjI5Qzk0LjI4MjIgNDIuMzMyIDk0LjA4MDEgNDIuNzkyIDkzLjkzOTUgNDMuMzQyOEM5My43OTg4IDQzLjg4NzcgOTMuNzI4NSA0NC41MTc2IDkzLjcyODUgNDUuMjMyNFY0NS45NTMxQzkzLjcyODUgNDYuNjY4IDkzLjc5ODggNDcuMzAwOCA5My45Mzk1IDQ3Ljg1MTZDOTQuMDgwMSA0OC40MDIzIDk0LjI4NTIgNDguODY4MiA5NC41NTQ3IDQ5LjI0OUM5NC44MzAxIDQ5LjYyNCA5NS4xNTgyIDQ5LjkwODIgOTUuNTM5MSA1MC4xMDE2Qzk1LjkxOTkgNTAuMjg5MSA5Ni4zNTM1IDUwLjM4MjggOTYuODM5OCA1MC4zODI4Qzk3LjMzMiA1MC4zODI4IDk3Ljc2ODYgNTAuMjg5MSA5OC4xNDk0IDUwLjEwMTZDOTguNTMwMyA0OS45MDgyIDk4Ljg1MjUgNDkuNjI0IDk5LjExNjIgNDkuMjQ5Qzk5LjM3OTkgNDguODY4MiA5OS41NzkxIDQ4LjQwMjMgOTkuNzEzOSA0Ny44NTE2Qzk5Ljg1NDUgNDcuMzAwOCA5OS45MjQ4IDQ2LjY2OCA5OS45MjQ4IDQ1Ljk1MzFaTTEwNi40MDMgNDQuMzE4NFY1NS42NTYySDEwNC4yODVWNDIuNDkwMkgxMDYuMjM2TDEwNi40MDMgNDQuMzE4NFpNMTEyLjU5OSA0Ny4xNTcyVjQ3LjM0MThDMTEyLjU5OSA0OC4wMzMyIDExMi41MTcgNDguNjc0OCAxMTIuMzUzIDQ5LjI2NjZDMTEyLjE5NSA0OS44NTI1IDExMS45NTggNTAuMzY1MiAxMTEuNjQxIDUwLjgwNDdDMTExLjMzMSA1MS4yMzgzIDExMC45NDcgNTEuNTc1MiAxMTAuNDkgNTEuODE1NEMxMTAuMDMzIDUyLjA1NTcgMTA5LjUwNSA1Mi4xNzU4IDEwOC45MDggNTIuMTc1OEMxMDguMzE2IDUyLjE3NTggMTA3Ljc5NyA1Mi4wNjc0IDEwNy4zNTIgNTEuODUwNkMxMDYuOTEzIDUxLjYyNzkgMTA2LjU0MSA1MS4zMTQ1IDEwNi4yMzYgNTAuOTEwMkMxMDUuOTMxIDUwLjUwNTkgMTA1LjY4NSA1MC4wMzEyIDEwNS40OTggNDkuNDg2M0MxMDUuMzE2IDQ4LjkzNTUgMTA1LjE4NyA0OC4zMzIgMTA1LjExMSA0Ny42NzU4VjQ2Ljk2MzlDMTA1LjE4NyA0Ni4yNjY2IDEwNS4zMTYgNDUuNjMzOCAxMDUuNDk4IDQ1LjA2NTRDMTA1LjY4NSA0NC40OTcxIDEwNS45MzEgNDQuMDA3OCAxMDYuMjM2IDQzLjU5NzdDMTA2LjU0MSA0My4xODc1IDEwNi45MTMgNDIuODcxMSAxMDcuMzUyIDQyLjY0ODRDMTA3Ljc5MiA0Mi40MjU4IDEwOC4zMDQgNDIuMzE0NSAxMDguODkgNDIuMzE0NUMxMDkuNDg4IDQyLjMxNDUgMTEwLjAxOCA0Mi40MzE2IDExMC40ODEgNDIuNjY2QzExMC45NDQgNDIuODk0NSAxMTEuMzM0IDQzLjIyMjcgMTExLjY1IDQzLjY1MDRDMTExLjk2NiA0NC4wNzIzIDExMi4yMDQgNDQuNTgyIDExMi4zNjIgNDUuMTc5N0MxMTIuNTIgNDUuNzcxNSAxMTIuNTk5IDQ2LjQzMDcgMTEyLjU5OSA0Ny4xNTcyWk0xMTAuNDgxIDQ3LjM0MThWNDcuMTU3MkMxMTAuNDgxIDQ2LjcxNzggMTEwLjQ0IDQ2LjMxMDUgMTEwLjM1OCA0NS45MzU1QzExMC4yNzYgNDUuNTU0NyAxMTAuMTQ3IDQ1LjIyMDcgMTA5Ljk3MSA0NC45MzM2QzEwOS43OTYgNDQuNjQ2NSAxMDkuNTcgNDQuNDIzOCAxMDkuMjk1IDQ0LjI2NTZDMTA5LjAyNSA0NC4xMDE2IDEwOC43IDQ0LjAxOTUgMTA4LjMxOSA0NC4wMTk1QzEwNy45NDQgNDQuMDE5NSAxMDcuNjIyIDQ0LjA4NCAxMDcuMzUyIDQ0LjIxMjlDMTA3LjA4MyA0NC4zMzU5IDEwNi44NTcgNDQuNTA4OCAxMDYuNjc1IDQ0LjczMTRDMTA2LjQ5NCA0NC45NTQxIDEwNi4zNTMgNDUuMjE0OCAxMDYuMjU0IDQ1LjUxMzdDMTA2LjE1NCA0NS44MDY2IDEwNi4wODQgNDYuMTI2IDEwNi4wNDMgNDYuNDcxN1Y0OC4xNzY4QzEwNi4xMTMgNDguNTk4NiAxMDYuMjMzIDQ4Ljk4NTQgMTA2LjQwMyA0OS4zMzY5QzEwNi41NzMgNDkuNjg4NSAxMDYuODEzIDQ5Ljk2OTcgMTA3LjEyNCA1MC4xODA3QzEwNy40NCA1MC4zODU3IDEwNy44NDQgNTAuNDg4MyAxMDguMzM3IDUwLjQ4ODNDMTA4LjcxNyA1MC40ODgzIDEwOS4wNDMgNTAuNDA2MiAxMDkuMzEyIDUwLjI0MjJDMTA5LjU4MiA1MC4wNzgxIDEwOS44MDEgNDkuODUyNSAxMDkuOTcxIDQ5LjU2NTRDMTEwLjE0NyA0OS4yNzI1IDExMC4yNzYgNDguOTM1NSAxMTAuMzU4IDQ4LjU1NDdDMTEwLjQ0IDQ4LjE3MzggMTEwLjQ4MSA0Ny43Njk1IDExMC40ODEgNDcuMzQxOFpNMTE4Ljc0MyA1Mi4xNzU4QzExOC4wNCA1Mi4xNzU4IDExNy40MDQgNTIuMDYxNSAxMTYuODM2IDUxLjgzM0MxMTYuMjc0IDUxLjU5ODYgMTE1Ljc5MyA1MS4yNzM0IDExNS4zOTUgNTAuODU3NEMxMTUuMDAyIDUwLjQ0MTQgMTE0LjcgNDkuOTUyMSAxMTQuNDg5IDQ5LjM4OTZDMTE0LjI3OSA0OC44MjcxIDExNC4xNzMgNDguMjIwNyAxMTQuMTczIDQ3LjU3MDNWNDcuMjE4OEMxMTQuMTczIDQ2LjQ3NDYgMTE0LjI4MSA0NS44MDA4IDExNC40OTggNDUuMTk3M0MxMTQuNzE1IDQ0LjU5MzggMTE1LjAxNyA0NC4wNzgxIDExNS40MDQgNDMuNjUwNEMxMTUuNzkgNDMuMjE2OCAxMTYuMjQ3IDQyLjg4NTcgMTE2Ljc3NSA0Mi42NTcyQzExNy4zMDIgNDIuNDI4NyAxMTcuODczIDQyLjMxNDUgMTE4LjQ4OCA0Mi4zMTQ1QzExOS4xNjggNDIuMzE0NSAxMTkuNzYzIDQyLjQyODcgMTIwLjI3MyA0Mi42NTcyQzEyMC43ODIgNDIuODg1NyAxMjEuMjA0IDQzLjIwOCAxMjEuNTM4IDQzLjYyNEMxMjEuODc4IDQ0LjAzNDIgMTIyLjEzIDQ0LjUyMzQgMTIyLjI5NCA0NS4wOTE4QzEyMi40NjQgNDUuNjYwMiAxMjIuNTQ5IDQ2LjI4NzEgMTIyLjU0OSA0Ni45NzI3VjQ3Ljg3NzlIMTE1LjIwMVY0Ni4zNTc0SDEyMC40NTdWNDYuMTkwNEMxMjAuNDQ2IDQ1LjgwOTYgMTIwLjM2OSA0NS40NTIxIDEyMC4yMjkgNDUuMTE4MkMxMjAuMDk0IDQ0Ljc4NDIgMTE5Ljg4NiA0NC41MTQ2IDExOS42MDUgNDQuMzA5NkMxMTkuMzIzIDQ0LjEwNDUgMTE4Ljk0OCA0NC4wMDIgMTE4LjQ4IDQ0LjAwMkMxMTguMTI4IDQ0LjAwMiAxMTcuODE1IDQ0LjA3ODEgMTE3LjUzOSA0NC4yMzA1QzExNy4yNyA0NC4zNzcgMTE3LjA0NCA0NC41OTA4IDExNi44NjIgNDQuODcyMUMxMTYuNjgxIDQ1LjE1MzMgMTE2LjU0IDQ1LjQ5MzIgMTE2LjQ0MSA0NS44OTE2QzExNi4zNDcgNDYuMjg0MiAxMTYuMyA0Ni43MjY2IDExNi4zIDQ3LjIxODhWNDcuNTcwM0MxMTYuMyA0Ny45ODYzIDExNi4zNTYgNDguMzczIDExNi40NjcgNDguNzMwNUMxMTYuNTg0IDQ5LjA4MiAxMTYuNzU0IDQ5LjM4OTYgMTE2Ljk3NyA0OS42NTMzQzExNy4xOTkgNDkuOTE3IDExNy40NjkgNTAuMTI1IDExNy43ODUgNTAuMjc3M0MxMTguMTAyIDUwLjQyMzggMTE4LjQ2MiA1MC40OTcxIDExOC44NjYgNTAuNDk3MUMxMTkuMzc2IDUwLjQ5NzEgMTE5LjgzIDUwLjM5NDUgMTIwLjIyOSA1MC4xODk1QzEyMC42MjcgNDkuOTg0NCAxMjAuOTczIDQ5LjY5NDMgMTIxLjI2NiA0OS4zMTkzTDEyMi4zODIgNTAuNDAwNEMxMjIuMTc3IDUwLjY5OTIgMTIxLjkxIDUwLjk4NjMgMTIxLjU4MiA1MS4yNjE3QzEyMS4yNTQgNTEuNTMxMiAxMjAuODUzIDUxLjc1MSAxMjAuMzc4IDUxLjkyMDlDMTE5LjkwOSA1Mi4wOTA4IDExOS4zNjQgNTIuMTc1OCAxMTguNzQzIDUyLjE3NThaTTEyNi40NTIgNDQuNTIwNVY1MkgxMjQuMzM0VjQyLjQ5MDJIMTI2LjMyOUwxMjYuNDUyIDQ0LjUyMDVaTTEyNi4wNzQgNDYuODkzNkwxMjUuMzg4IDQ2Ljg4NDhDMTI1LjM5NCA0Ni4yMTA5IDEyNS40ODggNDUuNTkyOCAxMjUuNjcgNDUuMDMwM0MxMjUuODU3IDQ0LjQ2NzggMTI2LjExNSA0My45ODQ0IDEyNi40NDMgNDMuNTgwMUMxMjYuNzc3IDQzLjE3NTggMTI3LjE3NiA0Mi44NjUyIDEyNy42MzggNDIuNjQ4NEMxMjguMTAxIDQyLjQyNTggMTI4LjYxNyA0Mi4zMTQ1IDEyOS4xODUgNDIuMzE0NUMxMjkuNjQyIDQyLjMxNDUgMTMwLjA1NSA0Mi4zNzg5IDEzMC40MjUgNDIuNTA3OEMxMzAuOCA0Mi42MzA5IDEzMS4xMTkgNDIuODMzIDEzMS4zODMgNDMuMTE0M0MxMzEuNjUyIDQzLjM5NTUgMTMxLjg1NyA0My43NjE3IDEzMS45OTggNDQuMjEyOUMxMzIuMTM4IDQ0LjY1ODIgMTMyLjIwOSA0NS4yMDYxIDEzMi4yMDkgNDUuODU2NFY1MkgxMzAuMDgyVjQ1Ljg0NzdDMTMwLjA4MiA0NS4zOTA2IDEzMC4wMTQgNDUuMDMwMyAxMjkuODggNDQuNzY2NkMxMjkuNzUxIDQ0LjQ5NzEgMTI5LjU2IDQ0LjMwNjYgMTI5LjMwOCA0NC4xOTUzQzEyOS4wNjIgNDQuMDc4MSAxMjguNzU1IDQ0LjAxOTUgMTI4LjM4NiA0NC4wMTk1QzEyOC4wMjIgNDQuMDE5NSAxMjcuNjk3IDQ0LjA5NTcgMTI3LjQxIDQ0LjI0OEMxMjcuMTIzIDQ0LjQwMDQgMTI2Ljg4IDQ0LjYwODQgMTI2LjY4IDQ0Ljg3MjFDMTI2LjQ4NyA0NS4xMzU3IDEyNi4zMzggNDUuNDQwNCAxMjYuMjMyIDQ1Ljc4NjFDMTI2LjEyNyA0Ni4xMzE4IDEyNi4wNzQgNDYuNTAxIDEyNi4wNzQgNDYuODkzNlpNMTM4LjcxMyA1Mi4xNzU4QzEzOC4wMSA1Mi4xNzU4IDEzNy4zNzQgNTIuMDYxNSAxMzYuODA2IDUxLjgzM0MxMzYuMjQ0IDUxLjU5ODYgMTM1Ljc2MyA1MS4yNzM0IDEzNS4zNjUgNTAuODU3NEMxMzQuOTcyIDUwLjQ0MTQgMTM0LjY3IDQ5Ljk1MjEgMTM0LjQ1OSA0OS4zODk2QzEzNC4yNDggNDguODI3MSAxMzQuMTQzIDQ4LjIyMDcgMTM0LjE0MyA0Ny41NzAzVjQ3LjIxODhDMTM0LjE0MyA0Ni40NzQ2IDEzNC4yNTEgNDUuODAwOCAxMzQuNDY4IDQ1LjE5NzNDMTM0LjY4NSA0NC41OTM4IDEzNC45ODcgNDQuMDc4MSAxMzUuMzczIDQzLjY1MDRDMTM1Ljc2IDQzLjIxNjggMTM2LjIxNyA0Mi44ODU3IDEzNi43NDUgNDIuNjU3MkMxMzcuMjcyIDQyLjQyODcgMTM3Ljg0MyA0Mi4zMTQ1IDEzOC40NTggNDIuMzE0NUMxMzkuMTM4IDQyLjMxNDUgMTM5LjczMyA0Mi40Mjg3IDE0MC4yNDMgNDIuNjU3MkMxNDAuNzUyIDQyLjg4NTcgMTQxLjE3NCA0My4yMDggMTQxLjUwOCA0My42MjRDMTQxLjg0OCA0NC4wMzQyIDE0Mi4xIDQ0LjUyMzQgMTQyLjI2NCA0NS4wOTE4QzE0Mi40MzQgNDUuNjYwMiAxNDIuNTE5IDQ2LjI4NzEgMTQyLjUxOSA0Ni45NzI3VjQ3Ljg3NzlIMTM1LjE3MVY0Ni4zNTc0SDE0MC40MjdWNDYuMTkwNEMxNDAuNDE1IDQ1LjgwOTYgMTQwLjMzOSA0NS40NTIxIDE0MC4xOTkgNDUuMTE4MkMxNDAuMDY0IDQ0Ljc4NDIgMTM5Ljg1NiA0NC41MTQ2IDEzOS41NzUgNDQuMzA5NkMxMzkuMjkzIDQ0LjEwNDUgMTM4LjkxOCA0NC4wMDIgMTM4LjQ1IDQ0LjAwMkMxMzguMDk4IDQ0LjAwMiAxMzcuNzg1IDQ0LjA3ODEgMTM3LjUwOSA0NC4yMzA1QzEzNy4yNCA0NC4zNzcgMTM3LjAxNCA0NC41OTA4IDEzNi44MzIgNDQuODcyMUMxMzYuNjUxIDQ1LjE1MzMgMTM2LjUxIDQ1LjQ5MzIgMTM2LjQxMSA0NS44OTE2QzEzNi4zMTcgNDYuMjg0MiAxMzYuMjcgNDYuNzI2NiAxMzYuMjcgNDcuMjE4OFY0Ny41NzAzQzEzNi4yNyA0Ny45ODYzIDEzNi4zMjYgNDguMzczIDEzNi40MzcgNDguNzMwNUMxMzYuNTU0IDQ5LjA4MiAxMzYuNzI0IDQ5LjM4OTYgMTM2Ljk0NyA0OS42NTMzQzEzNy4xNjkgNDkuOTE3IDEzNy40MzkgNTAuMTI1IDEzNy43NTUgNTAuMjc3M0MxMzguMDcyIDUwLjQyMzggMTM4LjQzMiA1MC40OTcxIDEzOC44MzYgNTAuNDk3MUMxMzkuMzQ2IDUwLjQ5NzEgMTM5LjggNTAuMzk0NSAxNDAuMTk5IDUwLjE4OTVDMTQwLjU5NyA0OS45ODQ0IDE0MC45NDMgNDkuNjk0MyAxNDEuMjM2IDQ5LjMxOTNMMTQyLjM1MiA1MC40MDA0QzE0Mi4xNDcgNTAuNjk5MiAxNDEuODggNTAuOTg2MyAxNDEuNTUyIDUxLjI2MTdDMTQxLjIyNCA1MS41MzEyIDE0MC44MjMgNTEuNzUxIDE0MC4zNDggNTEuOTIwOUMxMzkuODc5IDUyLjA5MDggMTM5LjMzNCA1Mi4xNzU4IDEzOC43MTMgNTIuMTc1OFpNMTUwLjEyMiA1MC4wMzEyVjM4LjVIMTUyLjI0OVY1MkgxNTAuMzI0TDE1MC4xMjIgNTAuMDMxMlpNMTQzLjkzNSA0Ny4zNTA2VjQ3LjE2NkMxNDMuOTM1IDQ2LjQ0NTMgMTQ0LjAyIDQ1Ljc4OTEgMTQ0LjE4OSA0NS4xOTczQzE0NC4zNTkgNDQuNTk5NiAxNDQuNjA1IDQ0LjA4NjkgMTQ0LjkyOCA0My42NTkyQzE0NS4yNSA0My4yMjU2IDE0NS42NDMgNDIuODk0NSAxNDYuMTA1IDQyLjY2NkMxNDYuNTY4IDQyLjQzMTYgMTQ3LjA5IDQyLjMxNDUgMTQ3LjY3IDQyLjMxNDVDMTQ4LjI0NCA0Mi4zMTQ1IDE0OC43NDggNDIuNDI1OCAxNDkuMTgyIDQyLjY0ODRDMTQ5LjYxNSA0Mi44NzExIDE0OS45ODQgNDMuMTkwNCAxNTAuMjg5IDQzLjYwNjRDMTUwLjU5NCA0NC4wMTY2IDE1MC44MzcgNDQuNTA4OCAxNTEuMDE5IDQ1LjA4M0MxNTEuMiA0NS42NTE0IDE1MS4zMjkgNDYuMjg0MiAxNTEuNDA1IDQ2Ljk4MTRWNDcuNTcwM0MxNTEuMzI5IDQ4LjI1IDE1MS4yIDQ4Ljg3MTEgMTUxLjAxOSA0OS40MzM2QzE1MC44MzcgNDkuOTk2MSAxNTAuNTk0IDUwLjQ4MjQgMTUwLjI4OSA1MC44OTI2QzE0OS45ODQgNTEuMzAyNyAxNDkuNjEyIDUxLjYxOTEgMTQ5LjE3MyA1MS44NDE4QzE0OC43MzkgNTIuMDY0NSAxNDguMjMyIDUyLjE3NTggMTQ3LjY1MiA1Mi4xNzU4QzE0Ny4wNzggNTIuMTc1OCAxNDYuNTYgNTIuMDU1NyAxNDYuMDk3IDUxLjgxNTRDMTQ1LjY0IDUxLjU3NTIgMTQ1LjI1IDUxLjIzODMgMTQ0LjkyOCA1MC44MDQ3QzE0NC42MDUgNTAuMzcxMSAxNDQuMzU5IDQ5Ljg2MTMgMTQ0LjE4OSA0OS4yNzU0QzE0NC4wMiA0OC42ODM2IDE0My45MzUgNDguMDQyIDE0My45MzUgNDcuMzUwNlpNMTQ2LjA1MyA0Ny4xNjZWNDcuMzUwNkMxNDYuMDUzIDQ3Ljc4NDIgMTQ2LjA5MSA0OC4xODg1IDE0Ni4xNjcgNDguNTYzNUMxNDYuMjQ5IDQ4LjkzODUgMTQ2LjM3NSA0OS4yNjk1IDE0Ni41NDUgNDkuNTU2NkMxNDYuNzE1IDQ5LjgzNzkgMTQ2LjkzNSA1MC4wNjA1IDE0Ny4yMDQgNTAuMjI0NkMxNDcuNDc5IDUwLjM4MjggMTQ3LjgwOCA1MC40NjE5IDE0OC4xODggNTAuNDYxOUMxNDguNjY5IDUwLjQ2MTkgMTQ5LjA2NCA1MC4zNTY0IDE0OS4zNzUgNTAuMTQ1NUMxNDkuNjg2IDQ5LjkzNDYgMTQ5LjkyOSA0OS42NTA0IDE1MC4xMDQgNDkuMjkzQzE1MC4yODYgNDguOTI5NyAxNTAuNDA5IDQ4LjUyNTQgMTUwLjQ3NCA0OC4wODAxVjQ2LjQ4OTNDMTUwLjQzOCA0Ni4xNDM2IDE1MC4zNjUgNDUuODIxMyAxNTAuMjU0IDQ1LjUyMjVDMTUwLjE0OCA0NS4yMjM2IDE1MC4wMDUgNDQuOTYyOSAxNDkuODIzIDQ0Ljc0MDJDMTQ5LjY0MiA0NC41MTE3IDE0OS40MTYgNDQuMzM1OSAxNDkuMTQ2IDQ0LjIxMjlDMTQ4Ljg4MyA0NC4wODQgMTQ4LjU2OSA0NC4wMTk1IDE0OC4yMDYgNDQuMDE5NUMxNDcuODE5IDQ0LjAxOTUgMTQ3LjQ5MSA0NC4xMDE2IDE0Ny4yMjIgNDQuMjY1NkMxNDYuOTUyIDQ0LjQyOTcgMTQ2LjcyOSA0NC42NTUzIDE0Ni41NTQgNDQuOTQyNEMxNDYuMzg0IDQ1LjIyOTUgMTQ2LjI1OCA0NS41NjM1IDE0Ni4xNzYgNDUuOTQ0M0MxNDYuMDk0IDQ2LjMyNTIgMTQ2LjA1MyA0Ni43MzI0IDE0Ni4wNTMgNDcuMTY2WiIgZmlsbD0iIzE5ODAzOCIvPgo8L2c+CjxtYXNrIGlkPSJwYXRoLTYtaW5zaWRlLTFfNDY5N18zNjcxMyIgZmlsbD0id2hpdGUiPgo8cGF0aCBkPSJNOCA4OEM4IDg1Ljc5MDkgOS43OTA4NiA4NCAxMiA4NEgyMDRDMjA2LjIwOSA4NCAyMDggODUuNzkwOSAyMDggODhWMTQwQzIwOCAxNDIuMjA5IDIwNi4yMDkgMTQ0IDIwNCAxNDRIMTJDOS43OTA4NiAxNDQgOCAxNDIuMjA5IDggMTQwVjg4WiIvPgo8L21hc2s+CjxwYXRoIGQ9Ik04IDg4QzggODUuNzkwOSA5Ljc5MDg2IDg0IDEyIDg0SDIwNEMyMDYuMjA5IDg0IDIwOCA4NS43OTA5IDIwOCA4OFYxNDBDMjA4IDE0Mi4yMDkgMjA2LjIwOSAxNDQgMjA0IDE0NEgxMkM5Ljc5MDg2IDE0NCA4IDE0Mi4yMDkgOCAxNDBWODhaIiBmaWxsPSIjRDEyNzMwIi8+CjxwYXRoIGQ9Ik04IDg0SDIwOEg4Wk0yMDggMTQwQzIwOCAxNDMuMzE0IDIwNS4zMTQgMTQ2IDIwMiAxNDZIMTRDMTAuNjg2MyAxNDYgOCAxNDMuMzE0IDggMTQwQzggMTQxLjEwNSA5Ljc5MDg2IDE0MiAxMiAxNDJIMjA0QzIwNi4yMDkgMTQyIDIwOCAxNDEuMTA1IDIwOCAxNDBaTTggMTQ0Vjg0VjE0NFpNMjA4IDg0VjE0NFY4NFoiIGZpbGw9IiNEMTI3MzAiIG1hc2s9InVybCgjcGF0aC02LWluc2lkZS0xXzQ2OTdfMzY3MTMpIi8+CjxtYXNrIGlkPSJtYXNrMV80Njk3XzM2NzEzIiBzdHlsZT0ibWFzay10eXBlOmFscGhhIiBtYXNrVW5pdHM9InVzZXJTcGFjZU9uVXNlIiB4PSI2NSIgeT0iMTAyIiB3aWR0aD0iMjUiIGhlaWdodD0iMjQiPgo8cmVjdCB4PSI2NS41IiB5PSIxMDIiIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgZmlsbD0iI0Q5RDlEOSIvPgo8L21hc2s+CjxnIG1hc2s9InVybCgjbWFzazFfNDY5N18zNjcxMykiPgo8cGF0aCBkPSJNNzEuNSAxMjRDNzAuOTUgMTI0IDcwLjQ3OTIgMTIzLjgwNCA3MC4wODc1IDEyMy40MTNDNjkuNjk1OCAxMjMuMDIxIDY5LjUgMTIyLjU1IDY5LjUgMTIyVjExMkM2OS41IDExMS40NSA2OS42OTU4IDExMC45NzkgNzAuMDg3NSAxMTAuNTg4QzcwLjQ3OTIgMTEwLjE5NiA3MC45NSAxMTAgNzEuNSAxMTBINzIuNVYxMDhDNzIuNSAxMDYuNjE3IDcyLjk4NzUgMTA1LjQzOCA3My45NjI1IDEwNC40NjJDNzQuOTM3NSAxMDMuNDg3IDc2LjExNjcgMTAzIDc3LjUgMTAzQzc4Ljg4MzMgMTAzIDgwLjA2MjUgMTAzLjQ4NyA4MS4wMzc1IDEwNC40NjJDODIuMDEyNSAxMDUuNDM4IDgyLjUgMTA2LjYxNyA4Mi41IDEwOFYxMTBIODMuNUM4NC4wNSAxMTAgODQuNTIwOCAxMTAuMTk2IDg0LjkxMjUgMTEwLjU4OEM4NS4zMDQyIDExMC45NzkgODUuNSAxMTEuNDUgODUuNSAxMTJWMTIyQzg1LjUgMTIyLjU1IDg1LjMwNDIgMTIzLjAyMSA4NC45MTI1IDEyMy40MTNDODQuNTIwOCAxMjMuODA0IDg0LjA1IDEyNCA4My41IDEyNEg3MS41Wk03MS41IDEyMkg4My41VjExMkg3MS41VjEyMlpNNzcuNSAxMTlDNzguMDUgMTE5IDc4LjUyMDggMTE4LjgwNCA3OC45MTI1IDExOC40MTNDNzkuMzA0MiAxMTguMDIxIDc5LjUgMTE3LjU1IDc5LjUgMTE3Qzc5LjUgMTE2LjQ1IDc5LjMwNDIgMTE1Ljk3OSA3OC45MTI1IDExNS41ODhDNzguNTIwOCAxMTUuMTk2IDc4LjA1IDExNSA3Ny41IDExNUM3Ni45NSAxMTUgNzYuNDc5MiAxMTUuMTk2IDc2LjA4NzUgMTE1LjU4OEM3NS42OTU4IDExNS45NzkgNzUuNSAxMTYuNDUgNzUuNSAxMTdDNzUuNSAxMTcuNTUgNzUuNjk1OCAxMTguMDIxIDc2LjA4NzUgMTE4LjQxM0M3Ni40NzkyIDExOC44MDQgNzYuOTUgMTE5IDc3LjUgMTE5Wk03NC41IDExMEg4MC41VjEwOEM4MC41IDEwNy4xNjcgODAuMjA4MyAxMDYuNDU4IDc5LjYyNSAxMDUuODc1Qzc5LjA0MTcgMTA1LjI5MiA3OC4zMzMzIDEwNSA3Ny41IDEwNUM3Ni42NjY3IDEwNSA3NS45NTgzIDEwNS4yOTIgNzUuMzc1IDEwNS44NzVDNzQuNzkxNyAxMDYuNDU4IDc0LjUgMTA3LjE2NyA3NC41IDEwOFYxMTBaIiBmaWxsPSJ3aGl0ZSIvPgo8L2c+CjxwYXRoIGQ9Ik0xMDIuNjc2IDExNS44MzRIMTA0Ljg3M0MxMDQuODAzIDExNi42NzIgMTA0LjU2OCAxMTcuNDE5IDEwNC4xNyAxMTguMDc1QzEwMy43NzEgMTE4LjcyNiAxMDMuMjEyIDExOS4yMzggMTAyLjQ5MSAxMTkuNjEzQzEwMS43NzEgMTE5Ljk4OCAxMDAuODk1IDEyMC4xNzYgOTkuODYzMyAxMjAuMTc2Qzk5LjA3MjMgMTIwLjE3NiA5OC4zNjA0IDEyMC4wMzUgOTcuNzI3NSAxMTkuNzU0Qzk3LjA5NDcgMTE5LjQ2NyA5Ni41NTI3IDExOS4wNjIgOTYuMTAxNiAxMTguNTQxQzk1LjY1MDQgMTE4LjAxNCA5NS4zMDQ3IDExNy4zNzggOTUuMDY0NSAxMTYuNjM0Qzk0LjgzMDEgMTE1Ljg5IDk0LjcxMjkgMTE1LjA1OCA5NC43MTI5IDExNC4xMzhWMTEzLjA3NEM5NC43MTI5IDExMi4xNTQgOTQuODMzIDExMS4zMjIgOTUuMDczMiAxMTAuNTc4Qzk1LjMxOTMgMTA5LjgzNCA5NS42NzA5IDEwOS4xOTggOTYuMTI3OSAxMDguNjcxQzk2LjU4NSAxMDguMTM4IDk3LjEzMjggMTA3LjczIDk3Ljc3MTUgMTA3LjQ0OUM5OC40MTYgMTA3LjE2OCA5OS4xMzk2IDEwNy4wMjcgOTkuOTQyNCAxMDcuMDI3QzEwMC45NjIgMTA3LjAyNyAxMDEuODIzIDEwNy4yMTUgMTAyLjUyNiAxMDcuNTlDMTAzLjIyOSAxMDcuOTY1IDEwMy43NzQgMTA4LjQ4MyAxMDQuMTYxIDEwOS4xNDZDMTA0LjU1NCAxMDkuODA4IDEwNC43OTQgMTEwLjU2NiAxMDQuODgyIDExMS40MjJIMTAyLjY4NUMxMDIuNjI2IDExMC44NzEgMTAyLjQ5NyAxMTAuMzk5IDEwMi4yOTggMTEwLjAwN0MxMDIuMTA0IDEwOS42MTQgMTAxLjgxNyAxMDkuMzE1IDEwMS40MzcgMTA5LjExQzEwMS4wNTYgMTA4Ljg5OSAxMDAuNTU4IDEwOC43OTQgOTkuOTQyNCAxMDguNzk0Qzk5LjQzODUgMTA4Ljc5NCA5OC45OTkgMTA4Ljg4OCA5OC42MjQgMTA5LjA3NUM5OC4yNDkgMTA5LjI2MyA5Ny45MzU1IDEwOS41MzggOTcuNjgzNiAxMDkuOTAxQzk3LjQzMTYgMTEwLjI2NSA5Ny4yNDEyIDExMC43MTMgOTcuMTEyMyAxMTEuMjQ2Qzk2Ljk4OTMgMTExLjc3MyA5Ni45Mjc3IDExMi4zNzcgOTYuOTI3NyAxMTMuMDU3VjExNC4xMzhDOTYuOTI3NyAxMTQuNzgyIDk2Ljk4MzQgMTE1LjM2OCA5Ny4wOTQ3IDExNS44OTZDOTcuMjExOSAxMTYuNDE3IDk3LjM4NzcgMTE2Ljg2NSA5Ny42MjIxIDExNy4yNEM5Ny44NjIzIDExNy42MTUgOTguMTY3IDExNy45MDUgOTguNTM2MSAxMTguMTFDOTguOTA1MyAxMTguMzE1IDk5LjM0NzcgMTE4LjQxOCA5OS44NjMzIDExOC40MThDMTAwLjQ5IDExOC40MTggMTAwLjk5NyAxMTguMzE4IDEwMS4zODQgMTE4LjExOUMxMDEuNzc2IDExNy45MiAxMDIuMDcyIDExNy42MyAxMDIuMjcxIDExNy4yNDlDMTAyLjQ3NyAxMTYuODYyIDEwMi42MTEgMTE2LjM5MSAxMDIuNjc2IDExNS44MzRaTTEwOS4wODQgMTA2LjVWMTIwSDEwNi45NTdWMTA2LjVIMTA5LjA4NFpNMTExLjE1IDExNS4zNTFWMTE1LjE0OEMxMTEuMTUgMTE0LjQ2MyAxMTEuMjQ5IDExMy44MjcgMTExLjQ0OCAxMTMuMjQxQzExMS42NDggMTEyLjY0OSAxMTEuOTM1IDExMi4xMzcgMTEyLjMxIDExMS43MDNDMTEyLjY5MSAxMTEuMjY0IDExMy4xNTQgMTEwLjkyNCAxMTMuNjk4IDExMC42ODRDMTE0LjI0OSAxMTAuNDM4IDExNC44NyAxMTAuMzE0IDExNS41NjIgMTEwLjMxNEMxMTYuMjU5IDExMC4zMTQgMTE2Ljg4IDExMC40MzggMTE3LjQyNSAxMTAuNjg0QzExNy45NzYgMTEwLjkyNCAxMTguNDQyIDExMS4yNjQgMTE4LjgyMiAxMTEuNzAzQzExOS4yMDMgMTEyLjEzNyAxMTkuNDkzIDExMi42NDkgMTE5LjY5MyAxMTMuMjQxQzExOS44OTIgMTEzLjgyNyAxMTkuOTkxIDExNC40NjMgMTE5Ljk5MSAxMTUuMTQ4VjExNS4zNTFDMTE5Ljk5MSAxMTYuMDM2IDExOS44OTIgMTE2LjY3MiAxMTkuNjkzIDExNy4yNThDMTE5LjQ5MyAxMTcuODQ0IDExOS4yMDMgMTE4LjM1NiAxMTguODIyIDExOC43OTZDMTE4LjQ0MiAxMTkuMjI5IDExNy45NzkgMTE5LjU2OSAxMTcuNDM0IDExOS44MTVDMTE2Ljg4OSAxMjAuMDU2IDExNi4yNzEgMTIwLjE3NiAxMTUuNTc5IDEyMC4xNzZDMTE0Ljg4MiAxMjAuMTc2IDExNC4yNTggMTIwLjA1NiAxMTMuNzA3IDExOS44MTVDMTEzLjE2MiAxMTkuNTY5IDExMi42OTkgMTE5LjIyOSAxMTIuMzE5IDExOC43OTZDMTExLjkzOCAxMTguMzU2IDExMS42NDggMTE3Ljg0NCAxMTEuNDQ4IDExNy4yNThDMTExLjI0OSAxMTYuNjcyIDExMS4xNSAxMTYuMDM2IDExMS4xNSAxMTUuMzUxWk0xMTMuMjY4IDExNS4xNDhWMTE1LjM1MUMxMTMuMjY4IDExNS43NzggMTEzLjMxMiAxMTYuMTgzIDExMy40IDExNi41NjNDMTEzLjQ4NyAxMTYuOTQ0IDExMy42MjUgMTE3LjI3OCAxMTMuODEzIDExNy41NjVDMTE0IDExNy44NTMgMTE0LjI0IDExOC4wNzggMTE0LjUzMyAxMTguMjQyQzExNC44MjYgMTE4LjQwNiAxMTUuMTc1IDExOC40ODggMTE1LjU3OSAxMTguNDg4QzExNS45NzIgMTE4LjQ4OCAxMTYuMzEyIDExOC40MDYgMTE2LjU5OSAxMTguMjQyQzExNi44OTIgMTE4LjA3OCAxMTcuMTMyIDExNy44NTMgMTE3LjMyIDExNy41NjVDMTE3LjUwNyAxMTcuMjc4IDExNy42NDUgMTE2Ljk0NCAxMTcuNzMzIDExNi41NjNDMTE3LjgyNiAxMTYuMTgzIDExNy44NzMgMTE1Ljc3OCAxMTcuODczIDExNS4zNTFWMTE1LjE0OEMxMTcuODczIDExNC43MjcgMTE3LjgyNiAxMTQuMzI4IDExNy43MzMgMTEzLjk1M0MxMTcuNjQ1IDExMy41NzIgMTE3LjUwNCAxMTMuMjM1IDExNy4zMTEgMTEyLjk0MkMxMTcuMTIzIDExMi42NDkgMTE2Ljg4MyAxMTIuNDIxIDExNi41OSAxMTIuMjU3QzExNi4zMDMgMTEyLjA4NyAxMTUuOTYgMTEyLjAwMiAxMTUuNTYyIDExMi4wMDJDMTE1LjE2MyAxMTIuMDAyIDExNC44MTggMTEyLjA4NyAxMTQuNTI1IDExMi4yNTdDMTE0LjIzNyAxMTIuNDIxIDExNCAxMTIuNjQ5IDExMy44MTMgMTEyLjk0MkMxMTMuNjI1IDExMy4yMzUgMTEzLjQ4NyAxMTMuNTcyIDExMy40IDExMy45NTNDMTEzLjMxMiAxMTQuMzI4IDExMy4yNjggMTE0LjcyNyAxMTMuMjY4IDExNS4xNDhaTTEyNy4yNTIgMTE3LjQyNUMxMjcuMjUyIDExNy4yMTQgMTI3LjE5OSAxMTcuMDIzIDEyNy4wOTQgMTE2Ljg1NEMxMjYuOTg4IDExNi42NzggMTI2Ljc4NiAxMTYuNTIgMTI2LjQ4NyAxMTYuMzc5QzEyNi4xOTQgMTE2LjIzOCAxMjUuNzYxIDExNi4xMDkgMTI1LjE4NiAxMTUuOTkyQzEyNC42ODIgMTE1Ljg4MSAxMjQuMjIgMTE1Ljc0OSAxMjMuNzk4IDExNS41OTdDMTIzLjM4MiAxMTUuNDM4IDEyMy4wMjQgMTE1LjI0OCAxMjIuNzI1IDExNS4wMjVDMTIyLjQyNyAxMTQuODAzIDEyMi4xOTUgMTE0LjUzOSAxMjIuMDMxIDExNC4yMzRDMTIxLjg2NyAxMTMuOTMgMTIxLjc4NSAxMTMuNTc4IDEyMS43ODUgMTEzLjE4QzEyMS43ODUgMTEyLjc5MyAxMjEuODcgMTEyLjQyNyAxMjIuMDQgMTEyLjA4MUMxMjIuMjEgMTExLjczNSAxMjIuNDUzIDExMS40MzEgMTIyLjc2OSAxMTEuMTY3QzEyMy4wODYgMTEwLjkwMyAxMjMuNDcgMTEwLjY5NSAxMjMuOTIxIDExMC41NDNDMTI0LjM3OCAxMTAuMzkxIDEyNC44ODggMTEwLjMxNCAxMjUuNDUgMTEwLjMxNEMxMjYuMjQ3IDExMC4zMTQgMTI2LjkyOSAxMTAuNDQ5IDEyNy40OTggMTEwLjcxOUMxMjguMDcyIDExMC45ODIgMTI4LjUxMiAxMTEuMzQzIDEyOC44MTYgMTExLjhDMTI5LjEyMSAxMTIuMjUxIDEyOS4yNzMgMTEyLjc2MSAxMjkuMjczIDExMy4zMjlIMTI3LjE1NUMxMjcuMTU1IDExMy4wNzcgMTI3LjA5MSAxMTIuODQzIDEyNi45NjIgMTEyLjYyNkMxMjYuODM5IDExMi40MDMgMTI2LjY1MSAxMTIuMjI1IDEyNi4zOTkgMTEyLjA5QzEyNi4xNDcgMTExLjk0OSAxMjUuODMxIDExMS44NzkgMTI1LjQ1IDExMS44NzlDMTI1LjA4NyAxMTEuODc5IDEyNC43ODUgMTExLjkzOCAxMjQuNTQ1IDExMi4wNTVDMTI0LjMxIDExMi4xNjYgMTI0LjEzNSAxMTIuMzEyIDEyNC4wMTcgMTEyLjQ5NEMxMjMuOTA2IDExMi42NzYgMTIzLjg1IDExMi44NzUgMTIzLjg1IDExMy4wOTJDMTIzLjg1IDExMy4yNSAxMjMuODggMTEzLjM5NCAxMjMuOTM4IDExMy41MjJDMTI0LjAwMyAxMTMuNjQ2IDEyNC4xMDggMTEzLjc2IDEyNC4yNTUgMTEzLjg2NUMxMjQuNDAxIDExMy45NjUgMTI0LjYgMTE0LjA1OSAxMjQuODUyIDExNC4xNDZDMTI1LjExIDExNC4yMzQgMTI1LjQzMiAxMTQuMzE5IDEyNS44MTkgMTE0LjQwMUMxMjYuNTQ2IDExNC41NTQgMTI3LjE3IDExNC43NSAxMjcuNjkxIDExNC45OUMxMjguMjE5IDExNS4yMjUgMTI4LjYyMyAxMTUuNTI5IDEyOC45MDQgMTE1LjkwNEMxMjkuMTg1IDExNi4yNzMgMTI5LjMyNiAxMTYuNzQyIDEyOS4zMjYgMTE3LjMxMUMxMjkuMzI2IDExNy43MzIgMTI5LjIzNSAxMTguMTE5IDEyOS4wNTQgMTE4LjQ3MUMxMjguODc4IDExOC44MTYgMTI4LjYyIDExOS4xMTggMTI4LjI4IDExOS4zNzZDMTI3Ljk0IDExOS42MjggMTI3LjUzMyAxMTkuODI0IDEyNy4wNTggMTE5Ljk2NUMxMjYuNTkgMTIwLjEwNSAxMjYuMDYyIDEyMC4xNzYgMTI1LjQ3NiAxMjAuMTc2QzEyNC42MTUgMTIwLjE3NiAxMjMuODg2IDEyMC4wMjMgMTIzLjI4OCAxMTkuNzE5QzEyMi42OSAxMTkuNDA4IDEyMi4yMzYgMTE5LjAxMyAxMjEuOTI2IDExOC41MzJDMTIxLjYyMSAxMTguMDQ2IDEyMS40NjkgMTE3LjU0MiAxMjEuNDY5IDExNy4wMjFIMTIzLjUxNkMxMjMuNTQgMTE3LjQxMyAxMjMuNjQ4IDExNy43MjcgMTIzLjg0MiAxMTcuOTYxQzEyNC4wNDEgMTE4LjE4OSAxMjQuMjg3IDExOC4zNTYgMTI0LjU4IDExOC40NjJDMTI0Ljg3OSAxMTguNTYyIDEyNS4xODYgMTE4LjYxMSAxMjUuNTAzIDExOC42MTFDMTI1Ljg4NCAxMTguNjExIDEyNi4yMDMgMTE4LjU2MiAxMjYuNDYxIDExOC40NjJDMTI2LjcxOSAxMTguMzU2IDEyNi45MTUgMTE4LjIxNiAxMjcuMDUgMTE4LjA0QzEyNy4xODQgMTE3Ljg1OCAxMjcuMjUyIDExNy42NTMgMTI3LjI1MiAxMTcuNDI1Wk0xMzUuNTIzIDEyMC4xNzZDMTM0LjgyIDEyMC4xNzYgMTM0LjE4NCAxMjAuMDYyIDEzMy42MTYgMTE5LjgzM0MxMzMuMDUzIDExOS41OTkgMTMyLjU3MyAxMTkuMjczIDEzMi4xNzQgMTE4Ljg1N0MxMzEuNzgyIDExOC40NDEgMTMxLjQ4IDExNy45NTIgMTMxLjI2OSAxMTcuMzlDMTMxLjA1OCAxMTYuODI3IDEzMC45NTMgMTE2LjIyMSAxMzAuOTUzIDExNS41N1YxMTUuMjE5QzEzMC45NTMgMTE0LjQ3NSAxMzEuMDYxIDExMy44MDEgMTMxLjI3OCAxMTMuMTk3QzEzMS40OTUgMTEyLjU5NCAxMzEuNzk2IDExMi4wNzggMTMyLjE4MyAxMTEuNjVDMTMyLjU3IDExMS4yMTcgMTMzLjAyNyAxMTAuODg2IDEzMy41NTQgMTEwLjY1N0MxMzQuMDgxIDExMC40MjkgMTM0LjY1MyAxMTAuMzE0IDEzNS4yNjggMTEwLjMxNEMxMzUuOTQ4IDExMC4zMTQgMTM2LjU0MiAxMTAuNDI5IDEzNy4wNTIgMTEwLjY1N0MxMzcuNTYyIDExMC44ODYgMTM3Ljk4NCAxMTEuMjA4IDEzOC4zMTggMTExLjYyNEMxMzguNjU4IDExMi4wMzQgMTM4LjkxIDExMi41MjMgMTM5LjA3NCAxMTMuMDkyQzEzOS4yNDQgMTEzLjY2IDEzOS4zMjkgMTE0LjI4NyAxMzkuMzI5IDExNC45NzNWMTE1Ljg3OEgxMzEuOTgxVjExNC4zNTdIMTM3LjIzN1YxMTQuMTlDMTM3LjIyNSAxMTMuODEgMTM3LjE0OSAxMTMuNDUyIDEzNy4wMDggMTEzLjExOEMxMzYuODczIDExMi43ODQgMTM2LjY2NSAxMTIuNTE1IDEzNi4zODQgMTEyLjMxQzEzNi4xMDMgMTEyLjEwNCAxMzUuNzI4IDExMi4wMDIgMTM1LjI1OSAxMTIuMDAyQzEzNC45MDggMTEyLjAwMiAxMzQuNTk0IDExMi4wNzggMTM0LjMxOSAxMTIuMjNDMTM0LjA0OSAxMTIuMzc3IDEzMy44MjQgMTEyLjU5MSAxMzMuNjQyIDExMi44NzJDMTMzLjQ2IDExMy4xNTMgMTMzLjMyIDExMy40OTMgMTMzLjIyIDExMy44OTJDMTMzLjEyNiAxMTQuMjg0IDEzMy4wNzkgMTE0LjcyNyAxMzMuMDc5IDExNS4yMTlWMTE1LjU3QzEzMy4wNzkgMTE1Ljk4NiAxMzMuMTM1IDExNi4zNzMgMTMzLjI0NiAxMTYuNzNDMTMzLjM2NCAxMTcuMDgyIDEzMy41MzQgMTE3LjM5IDEzMy43NTYgMTE3LjY1M0MxMzMuOTc5IDExNy45MTcgMTM0LjI0OCAxMTguMTI1IDEzNC41NjUgMTE4LjI3N0MxMzQuODgxIDExOC40MjQgMTM1LjI0MiAxMTguNDk3IDEzNS42NDYgMTE4LjQ5N0MxMzYuMTU2IDExOC40OTcgMTM2LjYxIDExOC4zOTUgMTM3LjAwOCAxMTguMTg5QzEzNy40MDcgMTE3Ljk4NCAxMzcuNzUyIDExNy42OTQgMTM4LjA0NSAxMTcuMzE5TDEzOS4xNjIgMTE4LjRDMTM4Ljk1NiAxMTguNjk5IDEzOC42OSAxMTguOTg2IDEzOC4zNjIgMTE5LjI2MkMxMzguMDM0IDExOS41MzEgMTM3LjYzMiAxMTkuNzUxIDEzNy4xNTggMTE5LjkyMUMxMzYuNjg5IDEyMC4wOTEgMTM2LjE0NCAxMjAuMTc2IDEzNS41MjMgMTIwLjE3NlpNMTQ2LjkzMiAxMTguMDMxVjEwNi41SDE0OS4wNTlWMTIwSDE0Ny4xMzRMMTQ2LjkzMiAxMTguMDMxWk0xNDAuNzQ0IDExNS4zNTFWMTE1LjE2NkMxNDAuNzQ0IDExNC40NDUgMTQwLjgyOSAxMTMuNzg5IDE0MC45OTkgMTEzLjE5N0MxNDEuMTY5IDExMi42IDE0MS40MTUgMTEyLjA4NyAxNDEuNzM3IDExMS42NTlDMTQyLjA2IDExMS4yMjYgMTQyLjQ1MiAxMTAuODk1IDE0Mi45MTUgMTEwLjY2NkMxNDMuMzc4IDExMC40MzIgMTQzLjg5OSAxMTAuMzE0IDE0NC40NzkgMTEwLjMxNEMxNDUuMDU0IDExMC4zMTQgMTQ1LjU1OCAxMTAuNDI2IDE0NS45OTEgMTEwLjY0OEMxNDYuNDI1IDExMC44NzEgMTQ2Ljc5NCAxMTEuMTkgMTQ3LjA5OSAxMTEuNjA2QzE0Ny40MDMgMTEyLjAxNyAxNDcuNjQ2IDExMi41MDkgMTQ3LjgyOCAxMTMuMDgzQzE0OC4wMSAxMTMuNjUxIDE0OC4xMzkgMTE0LjI4NCAxNDguMjE1IDExNC45ODFWMTE1LjU3QzE0OC4xMzkgMTE2LjI1IDE0OC4wMSAxMTYuODcxIDE0Ny44MjggMTE3LjQzNEMxNDcuNjQ2IDExNy45OTYgMTQ3LjQwMyAxMTguNDgyIDE0Ny4wOTkgMTE4Ljg5M0MxNDYuNzk0IDExOS4zMDMgMTQ2LjQyMiAxMTkuNjE5IDE0NS45ODIgMTE5Ljg0MkMxNDUuNTQ5IDEyMC4wNjQgMTQ1LjA0MiAxMjAuMTc2IDE0NC40NjIgMTIwLjE3NkMxNDMuODg4IDEyMC4xNzYgMTQzLjM2OSAxMjAuMDU2IDE0Mi45MDYgMTE5LjgxNUMxNDIuNDQ5IDExOS41NzUgMTQyLjA2IDExOS4yMzggMTQxLjczNyAxMTguODA1QzE0MS40MTUgMTE4LjM3MSAxNDEuMTY5IDExNy44NjEgMTQwLjk5OSAxMTcuMjc1QzE0MC44MjkgMTE2LjY4NCAxNDAuNzQ0IDExNi4wNDIgMTQwLjc0NCAxMTUuMzUxWk0xNDIuODYyIDExNS4xNjZWMTE1LjM1MUMxNDIuODYyIDExNS43ODQgMTQyLjkgMTE2LjE4OCAxNDIuOTc3IDExNi41NjNDMTQzLjA1OSAxMTYuOTM4IDE0My4xODUgMTE3LjI3IDE0My4zNTQgMTE3LjU1N0MxNDMuNTI0IDExNy44MzggMTQzLjc0NCAxMTguMDYxIDE0NC4wMTQgMTE4LjIyNUMxNDQuMjg5IDExOC4zODMgMTQ0LjYxNyAxMTguNDYyIDE0NC45OTggMTE4LjQ2MkMxNDUuNDc5IDExOC40NjIgMTQ1Ljg3NCAxMTguMzU2IDE0Ni4xODUgMTE4LjE0NkMxNDYuNDk1IDExNy45MzUgMTQ2LjczOCAxMTcuNjUgMTQ2LjkxNCAxMTcuMjkzQzE0Ny4wOTYgMTE2LjkzIDE0Ny4yMTkgMTE2LjUyNSAxNDcuMjgzIDExNi4wOFYxMTQuNDg5QzE0Ny4yNDggMTE0LjE0NCAxNDcuMTc1IDExMy44MjEgMTQ3LjA2MyAxMTMuNTIyQzE0Ni45NTggMTEzLjIyNCAxNDYuODE0IDExMi45NjMgMTQ2LjYzMyAxMTIuNzRDMTQ2LjQ1MSAxMTIuNTEyIDE0Ni4yMjYgMTEyLjMzNiAxNDUuOTU2IDExMi4yMTNDMTQ1LjY5MiAxMTIuMDg0IDE0NS4zNzkgMTEyLjAyIDE0NS4wMTYgMTEyLjAyQzE0NC42MjkgMTEyLjAyIDE0NC4zMDEgMTEyLjEwMiAxNDQuMDMxIDExMi4yNjZDMTQzLjc2MiAxMTIuNDMgMTQzLjUzOSAxMTIuNjU1IDE0My4zNjMgMTEyLjk0MkMxNDMuMTkzIDExMy4yMjkgMTQzLjA2NyAxMTMuNTYzIDE0Mi45ODUgMTEzLjk0NEMxNDIuOTAzIDExNC4zMjUgMTQyLjg2MiAxMTQuNzMyIDE0Mi44NjIgMTE1LjE2NloiIGZpbGw9IndoaXRlIi8+CjxkZWZzPgo8ZmlsdGVyIGlkPSJmaWx0ZXIwX2RfNDY5N18zNjcxMyIgeD0iMCIgeT0iMTIiIHdpZHRoPSIyMTYiIGhlaWdodD0iNzYiIGZpbHRlclVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgY29sb3ItaW50ZXJwb2xhdGlvbi1maWx0ZXJzPSJzUkdCIj4KPGZlRmxvb2QgZmxvb2Qtb3BhY2l0eT0iMCIgcmVzdWx0PSJCYWNrZ3JvdW5kSW1hZ2VGaXgiLz4KPGZlQ29sb3JNYXRyaXggaW49IlNvdXJjZUFscGhhIiB0eXBlPSJtYXRyaXgiIHZhbHVlcz0iMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMTI3IDAiIHJlc3VsdD0iaGFyZEFscGhhIi8+CjxmZU9mZnNldCBkeT0iNCIvPgo8ZmVHYXVzc2lhbkJsdXIgc3RkRGV2aWF0aW9uPSI0Ii8+CjxmZUNvbXBvc2l0ZSBpbjI9ImhhcmRBbHBoYSIgb3BlcmF0b3I9Im91dCIvPgo8ZmVDb2xvck1hdHJpeCB0eXBlPSJtYXRyaXgiIHZhbHVlcz0iMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMC4wOCAwIi8+CjxmZUJsZW5kIG1vZGU9Im5vcm1hbCIgaW4yPSJCYWNrZ3JvdW5kSW1hZ2VGaXgiIHJlc3VsdD0iZWZmZWN0MV9kcm9wU2hhZG93XzQ2OTdfMzY3MTMiLz4KPGZlQmxlbmQgbW9kZT0ibm9ybWFsIiBpbj0iU291cmNlR3JhcGhpYyIgaW4yPSJlZmZlY3QxX2Ryb3BTaGFkb3dfNDY5N18zNjcxMyIgcmVzdWx0PSJzaGFwZSIvPgo8L2ZpbHRlcj4KPC9kZWZzPgo8L3N2Zz4K",
+ "image": "tb-image:VG9nZ2xlIGJ1dHRvbnMuc3Zn:IlRvZ2dsZSBidXR0b24iIHN5c3RlbSB3aWRnZXQgaW1hZ2U=:SU1BR0U=;data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjE2IiBoZWlnaHQ9IjE2MCIgdmlld0JveD0iMCAwIDIxNiAxNjAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxnIGZpbHRlcj0idXJsKCNmaWx0ZXIwX2RfNDY5N18zNjcxMykiPgo8cmVjdCB4PSI4Ljc1IiB5PSIxNi43NSIgd2lkdGg9IjE5OC41IiBoZWlnaHQ9IjU4LjUiIHJ4PSIzLjI1IiBzdHJva2U9IiMxOTgwMzgiIHN0cm9rZS13aWR0aD0iMS41IiBzaGFwZS1yZW5kZXJpbmc9ImNyaXNwRWRnZXMiLz4KPG1hc2sgaWQ9Im1hc2swXzQ2OTdfMzY3MTMiIHN0eWxlPSJtYXNrLXR5cGU6YWxwaGEiIG1hc2tVbml0cz0idXNlclNwYWNlT25Vc2UiIHg9IjYyIiB5PSIzNCIgd2lkdGg9IjI1IiBoZWlnaHQ9IjI0Ij4KPHJlY3QgeD0iNjIuNSIgeT0iMzQiIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgZmlsbD0iI0Q5RDlEOSIvPgo8L21hc2s+CjxnIG1hc2s9InVybCgjbWFzazBfNDY5N18zNjcxMykiPgo8cGF0aCBkPSJNNjguNSA0Mkg3Ny41VjQwQzc3LjUgMzkuMTY2NyA3Ny4yMDgzIDM4LjQ1ODMgNzYuNjI1IDM3Ljg3NUM3Ni4wNDE3IDM3LjI5MTcgNzUuMzMzMyAzNyA3NC41IDM3QzczLjY2NjcgMzcgNzIuOTU4MyAzNy4yOTE3IDcyLjM3NSAzNy44NzVDNzEuNzkxNyAzOC40NTgzIDcxLjUgMzkuMTY2NyA3MS41IDQwSDY5LjVDNjkuNSAzOC42MTY3IDY5Ljk4NzUgMzcuNDM3NSA3MC45NjI1IDM2LjQ2MjVDNzEuOTM3NSAzNS40ODc1IDczLjExNjcgMzUgNzQuNSAzNUM3NS44ODMzIDM1IDc3LjA2MjUgMzUuNDg3NSA3OC4wMzc1IDM2LjQ2MjVDNzkuMDEyNSAzNy40Mzc1IDc5LjUgMzguNjE2NyA3OS41IDQwVjQySDgwLjVDODEuMDUgNDIgODEuNTIwOCA0Mi4xOTU4IDgxLjkxMjUgNDIuNTg3NUM4Mi4zMDQyIDQyLjk3OTIgODIuNSA0My40NSA4Mi41IDQ0VjU0QzgyLjUgNTQuNTUgODIuMzA0MiA1NS4wMjA4IDgxLjkxMjUgNTUuNDEyNUM4MS41MjA4IDU1LjgwNDIgODEuMDUgNTYgODAuNSA1Nkg2OC41QzY3Ljk1IDU2IDY3LjQ3OTIgNTUuODA0MiA2Ny4wODc1IDU1LjQxMjVDNjYuNjk1OCA1NS4wMjA4IDY2LjUgNTQuNTUgNjYuNSA1NFY0NEM2Ni41IDQzLjQ1IDY2LjY5NTggNDIuOTc5MiA2Ny4wODc1IDQyLjU4NzVDNjcuNDc5MiA0Mi4xOTU4IDY3Ljk1IDQyIDY4LjUgNDJaTTY4LjUgNTRIODAuNVY0NEg2OC41VjU0Wk03NC41IDUxQzc1LjA1IDUxIDc1LjUyMDggNTAuODA0MiA3NS45MTI1IDUwLjQxMjVDNzYuMzA0MiA1MC4wMjA4IDc2LjUgNDkuNTUgNzYuNSA0OUM3Ni41IDQ4LjQ1IDc2LjMwNDIgNDcuOTc5MiA3NS45MTI1IDQ3LjU4NzVDNzUuNTIwOCA0Ny4xOTU4IDc1LjA1IDQ3IDc0LjUgNDdDNzMuOTUgNDcgNzMuNDc5MiA0Ny4xOTU4IDczLjA4NzUgNDcuNTg3NUM3Mi42OTU4IDQ3Ljk3OTIgNzIuNSA0OC40NSA3Mi41IDQ5QzcyLjUgNDkuNTUgNzIuNjk1OCA1MC4wMjA4IDczLjA4NzUgNTAuNDEyNUM3My40NzkyIDUwLjgwNDIgNzMuOTUgNTEgNzQuNSA1MVoiIGZpbGw9IiMxOTgwMzgiLz4KPC9nPgo8cGF0aCBkPSJNMTAyLjEzMSA0NS4yNVY0NS45NTMxQzEwMi4xMzEgNDYuOTE5OSAxMDIuMDA1IDQ3Ljc4NzEgMTAxLjc1MyA0OC41NTQ3QzEwMS41MDEgNDkuMzIyMyAxMDEuMTQxIDQ5Ljk3NTYgMTAwLjY3MiA1MC41MTQ2QzEwMC4yMDkgNTEuMDUzNyA5OS42NTIzIDUxLjQ2NjggOTkuMDAyIDUxLjc1MzlDOTguMzUxNiA1Mi4wMzUyIDk3LjYzMDkgNTIuMTc1OCA5Ni44Mzk4IDUyLjE3NThDOTYuMDU0NyA1Mi4xNzU4IDk1LjMzNjkgNTIuMDM1MiA5NC42ODY1IDUxLjc1MzlDOTQuMDQyIDUxLjQ2NjggOTMuNDgyNCA1MS4wNTM3IDkzLjAwNzggNTAuNTE0NkM5Mi41MzMyIDQ5Ljk3NTYgOTIuMTY0MSA0OS4zMjIzIDkxLjkwMDQgNDguNTU0N0M5MS42NDI2IDQ3Ljc4NzEgOTEuNTEzNyA0Ni45MTk5IDkxLjUxMzcgNDUuOTUzMVY0NS4yNUM5MS41MTM3IDQ0LjI4MzIgOTEuNjQyNiA0My40MTg5IDkxLjkwMDQgNDIuNjU3MkM5Mi4xNTgyIDQxLjg4OTYgOTIuNTIxNSA0MS4yMzYzIDkyLjk5MDIgNDAuNjk3M0M5My40NjQ4IDQwLjE1MjMgOTQuMDI0NCAzOS43MzkzIDk0LjY2ODkgMzkuNDU4Qzk1LjMxOTMgMzkuMTcwOSA5Ni4wMzcxIDM5LjAyNzMgOTYuODIyMyAzOS4wMjczQzk3LjYxMzMgMzkuMDI3MyA5OC4zMzQgMzkuMTcwOSA5OC45ODQ0IDM5LjQ1OEM5OS42MzQ4IDM5LjczOTMgMTAwLjE5NCA0MC4xNTIzIDEwMC42NjMgNDAuNjk3M0MxMDEuMTMyIDQxLjIzNjMgMTAxLjQ5MiA0MS44ODk2IDEwMS43NDQgNDIuNjU3MkMxMDIuMDAyIDQzLjQxODkgMTAyLjEzMSA0NC4yODMyIDEwMi4xMzEgNDUuMjVaTTk5LjkyNDggNDUuOTUzMVY0NS4yMzI0Qzk5LjkyNDggNDQuNTE3NiA5OS44NTQ1IDQzLjg4NzcgOTkuNzEzOSA0My4zNDI4Qzk5LjU3OTEgNDIuNzkyIDk5LjM3NyA0Mi4zMzIgOTkuMTA3NCA0MS45NjI5Qzk4Ljg0MzggNDEuNTg3OSA5OC41MTg2IDQxLjMwNjYgOTguMTMxOCA0MS4xMTkxQzk3Ljc0NTEgNDAuOTI1OCA5Ny4zMDg2IDQwLjgyOTEgOTYuODIyMyA0MC44MjkxQzk2LjMzNTkgNDAuODI5MSA5NS45MDIzIDQwLjkyNTggOTUuNTIxNSA0MS4xMTkxQzk1LjE0MDYgNDEuMzA2NiA5NC44MTU0IDQxLjU4NzkgOTQuNTQ1OSA0MS45NjI5Qzk0LjI4MjIgNDIuMzMyIDk0LjA4MDEgNDIuNzkyIDkzLjkzOTUgNDMuMzQyOEM5My43OTg4IDQzLjg4NzcgOTMuNzI4NSA0NC41MTc2IDkzLjcyODUgNDUuMjMyNFY0NS45NTMxQzkzLjcyODUgNDYuNjY4IDkzLjc5ODggNDcuMzAwOCA5My45Mzk1IDQ3Ljg1MTZDOTQuMDgwMSA0OC40MDIzIDk0LjI4NTIgNDguODY4MiA5NC41NTQ3IDQ5LjI0OUM5NC44MzAxIDQ5LjYyNCA5NS4xNTgyIDQ5LjkwODIgOTUuNTM5MSA1MC4xMDE2Qzk1LjkxOTkgNTAuMjg5MSA5Ni4zNTM1IDUwLjM4MjggOTYuODM5OCA1MC4zODI4Qzk3LjMzMiA1MC4zODI4IDk3Ljc2ODYgNTAuMjg5MSA5OC4xNDk0IDUwLjEwMTZDOTguNTMwMyA0OS45MDgyIDk4Ljg1MjUgNDkuNjI0IDk5LjExNjIgNDkuMjQ5Qzk5LjM3OTkgNDguODY4MiA5OS41NzkxIDQ4LjQwMjMgOTkuNzEzOSA0Ny44NTE2Qzk5Ljg1NDUgNDcuMzAwOCA5OS45MjQ4IDQ2LjY2OCA5OS45MjQ4IDQ1Ljk1MzFaTTEwNi40MDMgNDQuMzE4NFY1NS42NTYySDEwNC4yODVWNDIuNDkwMkgxMDYuMjM2TDEwNi40MDMgNDQuMzE4NFpNMTEyLjU5OSA0Ny4xNTcyVjQ3LjM0MThDMTEyLjU5OSA0OC4wMzMyIDExMi41MTcgNDguNjc0OCAxMTIuMzUzIDQ5LjI2NjZDMTEyLjE5NSA0OS44NTI1IDExMS45NTggNTAuMzY1MiAxMTEuNjQxIDUwLjgwNDdDMTExLjMzMSA1MS4yMzgzIDExMC45NDcgNTEuNTc1MiAxMTAuNDkgNTEuODE1NEMxMTAuMDMzIDUyLjA1NTcgMTA5LjUwNSA1Mi4xNzU4IDEwOC45MDggNTIuMTc1OEMxMDguMzE2IDUyLjE3NTggMTA3Ljc5NyA1Mi4wNjc0IDEwNy4zNTIgNTEuODUwNkMxMDYuOTEzIDUxLjYyNzkgMTA2LjU0MSA1MS4zMTQ1IDEwNi4yMzYgNTAuOTEwMkMxMDUuOTMxIDUwLjUwNTkgMTA1LjY4NSA1MC4wMzEyIDEwNS40OTggNDkuNDg2M0MxMDUuMzE2IDQ4LjkzNTUgMTA1LjE4NyA0OC4zMzIgMTA1LjExMSA0Ny42NzU4VjQ2Ljk2MzlDMTA1LjE4NyA0Ni4yNjY2IDEwNS4zMTYgNDUuNjMzOCAxMDUuNDk4IDQ1LjA2NTRDMTA1LjY4NSA0NC40OTcxIDEwNS45MzEgNDQuMDA3OCAxMDYuMjM2IDQzLjU5NzdDMTA2LjU0MSA0My4xODc1IDEwNi45MTMgNDIuODcxMSAxMDcuMzUyIDQyLjY0ODRDMTA3Ljc5MiA0Mi40MjU4IDEwOC4zMDQgNDIuMzE0NSAxMDguODkgNDIuMzE0NUMxMDkuNDg4IDQyLjMxNDUgMTEwLjAxOCA0Mi40MzE2IDExMC40ODEgNDIuNjY2QzExMC45NDQgNDIuODk0NSAxMTEuMzM0IDQzLjIyMjcgMTExLjY1IDQzLjY1MDRDMTExLjk2NiA0NC4wNzIzIDExMi4yMDQgNDQuNTgyIDExMi4zNjIgNDUuMTc5N0MxMTIuNTIgNDUuNzcxNSAxMTIuNTk5IDQ2LjQzMDcgMTEyLjU5OSA0Ny4xNTcyWk0xMTAuNDgxIDQ3LjM0MThWNDcuMTU3MkMxMTAuNDgxIDQ2LjcxNzggMTEwLjQ0IDQ2LjMxMDUgMTEwLjM1OCA0NS45MzU1QzExMC4yNzYgNDUuNTU0NyAxMTAuMTQ3IDQ1LjIyMDcgMTA5Ljk3MSA0NC45MzM2QzEwOS43OTYgNDQuNjQ2NSAxMDkuNTcgNDQuNDIzOCAxMDkuMjk1IDQ0LjI2NTZDMTA5LjAyNSA0NC4xMDE2IDEwOC43IDQ0LjAxOTUgMTA4LjMxOSA0NC4wMTk1QzEwNy45NDQgNDQuMDE5NSAxMDcuNjIyIDQ0LjA4NCAxMDcuMzUyIDQ0LjIxMjlDMTA3LjA4MyA0NC4zMzU5IDEwNi44NTcgNDQuNTA4OCAxMDYuNjc1IDQ0LjczMTRDMTA2LjQ5NCA0NC45NTQxIDEwNi4zNTMgNDUuMjE0OCAxMDYuMjU0IDQ1LjUxMzdDMTA2LjE1NCA0NS44MDY2IDEwNi4wODQgNDYuMTI2IDEwNi4wNDMgNDYuNDcxN1Y0OC4xNzY4QzEwNi4xMTMgNDguNTk4NiAxMDYuMjMzIDQ4Ljk4NTQgMTA2LjQwMyA0OS4zMzY5QzEwNi41NzMgNDkuNjg4NSAxMDYuODEzIDQ5Ljk2OTcgMTA3LjEyNCA1MC4xODA3QzEwNy40NCA1MC4zODU3IDEwNy44NDQgNTAuNDg4MyAxMDguMzM3IDUwLjQ4ODNDMTA4LjcxNyA1MC40ODgzIDEwOS4wNDMgNTAuNDA2MiAxMDkuMzEyIDUwLjI0MjJDMTA5LjU4MiA1MC4wNzgxIDEwOS44MDEgNDkuODUyNSAxMDkuOTcxIDQ5LjU2NTRDMTEwLjE0NyA0OS4yNzI1IDExMC4yNzYgNDguOTM1NSAxMTAuMzU4IDQ4LjU1NDdDMTEwLjQ0IDQ4LjE3MzggMTEwLjQ4MSA0Ny43Njk1IDExMC40ODEgNDcuMzQxOFpNMTE4Ljc0MyA1Mi4xNzU4QzExOC4wNCA1Mi4xNzU4IDExNy40MDQgNTIuMDYxNSAxMTYuODM2IDUxLjgzM0MxMTYuMjc0IDUxLjU5ODYgMTE1Ljc5MyA1MS4yNzM0IDExNS4zOTUgNTAuODU3NEMxMTUuMDAyIDUwLjQ0MTQgMTE0LjcgNDkuOTUyMSAxMTQuNDg5IDQ5LjM4OTZDMTE0LjI3OSA0OC44MjcxIDExNC4xNzMgNDguMjIwNyAxMTQuMTczIDQ3LjU3MDNWNDcuMjE4OEMxMTQuMTczIDQ2LjQ3NDYgMTE0LjI4MSA0NS44MDA4IDExNC40OTggNDUuMTk3M0MxMTQuNzE1IDQ0LjU5MzggMTE1LjAxNyA0NC4wNzgxIDExNS40MDQgNDMuNjUwNEMxMTUuNzkgNDMuMjE2OCAxMTYuMjQ3IDQyLjg4NTcgMTE2Ljc3NSA0Mi42NTcyQzExNy4zMDIgNDIuNDI4NyAxMTcuODczIDQyLjMxNDUgMTE4LjQ4OCA0Mi4zMTQ1QzExOS4xNjggNDIuMzE0NSAxMTkuNzYzIDQyLjQyODcgMTIwLjI3MyA0Mi42NTcyQzEyMC43ODIgNDIuODg1NyAxMjEuMjA0IDQzLjIwOCAxMjEuNTM4IDQzLjYyNEMxMjEuODc4IDQ0LjAzNDIgMTIyLjEzIDQ0LjUyMzQgMTIyLjI5NCA0NS4wOTE4QzEyMi40NjQgNDUuNjYwMiAxMjIuNTQ5IDQ2LjI4NzEgMTIyLjU0OSA0Ni45NzI3VjQ3Ljg3NzlIMTE1LjIwMVY0Ni4zNTc0SDEyMC40NTdWNDYuMTkwNEMxMjAuNDQ2IDQ1LjgwOTYgMTIwLjM2OSA0NS40NTIxIDEyMC4yMjkgNDUuMTE4MkMxMjAuMDk0IDQ0Ljc4NDIgMTE5Ljg4NiA0NC41MTQ2IDExOS42MDUgNDQuMzA5NkMxMTkuMzIzIDQ0LjEwNDUgMTE4Ljk0OCA0NC4wMDIgMTE4LjQ4IDQ0LjAwMkMxMTguMTI4IDQ0LjAwMiAxMTcuODE1IDQ0LjA3ODEgMTE3LjUzOSA0NC4yMzA1QzExNy4yNyA0NC4zNzcgMTE3LjA0NCA0NC41OTA4IDExNi44NjIgNDQuODcyMUMxMTYuNjgxIDQ1LjE1MzMgMTE2LjU0IDQ1LjQ5MzIgMTE2LjQ0MSA0NS44OTE2QzExNi4zNDcgNDYuMjg0MiAxMTYuMyA0Ni43MjY2IDExNi4zIDQ3LjIxODhWNDcuNTcwM0MxMTYuMyA0Ny45ODYzIDExNi4zNTYgNDguMzczIDExNi40NjcgNDguNzMwNUMxMTYuNTg0IDQ5LjA4MiAxMTYuNzU0IDQ5LjM4OTYgMTE2Ljk3NyA0OS42NTMzQzExNy4xOTkgNDkuOTE3IDExNy40NjkgNTAuMTI1IDExNy43ODUgNTAuMjc3M0MxMTguMTAyIDUwLjQyMzggMTE4LjQ2MiA1MC40OTcxIDExOC44NjYgNTAuNDk3MUMxMTkuMzc2IDUwLjQ5NzEgMTE5LjgzIDUwLjM5NDUgMTIwLjIyOSA1MC4xODk1QzEyMC42MjcgNDkuOTg0NCAxMjAuOTczIDQ5LjY5NDMgMTIxLjI2NiA0OS4zMTkzTDEyMi4zODIgNTAuNDAwNEMxMjIuMTc3IDUwLjY5OTIgMTIxLjkxIDUwLjk4NjMgMTIxLjU4MiA1MS4yNjE3QzEyMS4yNTQgNTEuNTMxMiAxMjAuODUzIDUxLjc1MSAxMjAuMzc4IDUxLjkyMDlDMTE5LjkwOSA1Mi4wOTA4IDExOS4zNjQgNTIuMTc1OCAxMTguNzQzIDUyLjE3NThaTTEyNi40NTIgNDQuNTIwNVY1MkgxMjQuMzM0VjQyLjQ5MDJIMTI2LjMyOUwxMjYuNDUyIDQ0LjUyMDVaTTEyNi4wNzQgNDYuODkzNkwxMjUuMzg4IDQ2Ljg4NDhDMTI1LjM5NCA0Ni4yMTA5IDEyNS40ODggNDUuNTkyOCAxMjUuNjcgNDUuMDMwM0MxMjUuODU3IDQ0LjQ2NzggMTI2LjExNSA0My45ODQ0IDEyNi40NDMgNDMuNTgwMUMxMjYuNzc3IDQzLjE3NTggMTI3LjE3NiA0Mi44NjUyIDEyNy42MzggNDIuNjQ4NEMxMjguMTAxIDQyLjQyNTggMTI4LjYxNyA0Mi4zMTQ1IDEyOS4xODUgNDIuMzE0NUMxMjkuNjQyIDQyLjMxNDUgMTMwLjA1NSA0Mi4zNzg5IDEzMC40MjUgNDIuNTA3OEMxMzAuOCA0Mi42MzA5IDEzMS4xMTkgNDIuODMzIDEzMS4zODMgNDMuMTE0M0MxMzEuNjUyIDQzLjM5NTUgMTMxLjg1NyA0My43NjE3IDEzMS45OTggNDQuMjEyOUMxMzIuMTM4IDQ0LjY1ODIgMTMyLjIwOSA0NS4yMDYxIDEzMi4yMDkgNDUuODU2NFY1MkgxMzAuMDgyVjQ1Ljg0NzdDMTMwLjA4MiA0NS4zOTA2IDEzMC4wMTQgNDUuMDMwMyAxMjkuODggNDQuNzY2NkMxMjkuNzUxIDQ0LjQ5NzEgMTI5LjU2IDQ0LjMwNjYgMTI5LjMwOCA0NC4xOTUzQzEyOS4wNjIgNDQuMDc4MSAxMjguNzU1IDQ0LjAxOTUgMTI4LjM4NiA0NC4wMTk1QzEyOC4wMjIgNDQuMDE5NSAxMjcuNjk3IDQ0LjA5NTcgMTI3LjQxIDQ0LjI0OEMxMjcuMTIzIDQ0LjQwMDQgMTI2Ljg4IDQ0LjYwODQgMTI2LjY4IDQ0Ljg3MjFDMTI2LjQ4NyA0NS4xMzU3IDEyNi4zMzggNDUuNDQwNCAxMjYuMjMyIDQ1Ljc4NjFDMTI2LjEyNyA0Ni4xMzE4IDEyNi4wNzQgNDYuNTAxIDEyNi4wNzQgNDYuODkzNlpNMTM4LjcxMyA1Mi4xNzU4QzEzOC4wMSA1Mi4xNzU4IDEzNy4zNzQgNTIuMDYxNSAxMzYuODA2IDUxLjgzM0MxMzYuMjQ0IDUxLjU5ODYgMTM1Ljc2MyA1MS4yNzM0IDEzNS4zNjUgNTAuODU3NEMxMzQuOTcyIDUwLjQ0MTQgMTM0LjY3IDQ5Ljk1MjEgMTM0LjQ1OSA0OS4zODk2QzEzNC4yNDggNDguODI3MSAxMzQuMTQzIDQ4LjIyMDcgMTM0LjE0MyA0Ny41NzAzVjQ3LjIxODhDMTM0LjE0MyA0Ni40NzQ2IDEzNC4yNTEgNDUuODAwOCAxMzQuNDY4IDQ1LjE5NzNDMTM0LjY4NSA0NC41OTM4IDEzNC45ODcgNDQuMDc4MSAxMzUuMzczIDQzLjY1MDRDMTM1Ljc2IDQzLjIxNjggMTM2LjIxNyA0Mi44ODU3IDEzNi43NDUgNDIuNjU3MkMxMzcuMjcyIDQyLjQyODcgMTM3Ljg0MyA0Mi4zMTQ1IDEzOC40NTggNDIuMzE0NUMxMzkuMTM4IDQyLjMxNDUgMTM5LjczMyA0Mi40Mjg3IDE0MC4yNDMgNDIuNjU3MkMxNDAuNzUyIDQyLjg4NTcgMTQxLjE3NCA0My4yMDggMTQxLjUwOCA0My42MjRDMTQxLjg0OCA0NC4wMzQyIDE0Mi4xIDQ0LjUyMzQgMTQyLjI2NCA0NS4wOTE4QzE0Mi40MzQgNDUuNjYwMiAxNDIuNTE5IDQ2LjI4NzEgMTQyLjUxOSA0Ni45NzI3VjQ3Ljg3NzlIMTM1LjE3MVY0Ni4zNTc0SDE0MC40MjdWNDYuMTkwNEMxNDAuNDE1IDQ1LjgwOTYgMTQwLjMzOSA0NS40NTIxIDE0MC4xOTkgNDUuMTE4MkMxNDAuMDY0IDQ0Ljc4NDIgMTM5Ljg1NiA0NC41MTQ2IDEzOS41NzUgNDQuMzA5NkMxMzkuMjkzIDQ0LjEwNDUgMTM4LjkxOCA0NC4wMDIgMTM4LjQ1IDQ0LjAwMkMxMzguMDk4IDQ0LjAwMiAxMzcuNzg1IDQ0LjA3ODEgMTM3LjUwOSA0NC4yMzA1QzEzNy4yNCA0NC4zNzcgMTM3LjAxNCA0NC41OTA4IDEzNi44MzIgNDQuODcyMUMxMzYuNjUxIDQ1LjE1MzMgMTM2LjUxIDQ1LjQ5MzIgMTM2LjQxMSA0NS44OTE2QzEzNi4zMTcgNDYuMjg0MiAxMzYuMjcgNDYuNzI2NiAxMzYuMjcgNDcuMjE4OFY0Ny41NzAzQzEzNi4yNyA0Ny45ODYzIDEzNi4zMjYgNDguMzczIDEzNi40MzcgNDguNzMwNUMxMzYuNTU0IDQ5LjA4MiAxMzYuNzI0IDQ5LjM4OTYgMTM2Ljk0NyA0OS42NTMzQzEzNy4xNjkgNDkuOTE3IDEzNy40MzkgNTAuMTI1IDEzNy43NTUgNTAuMjc3M0MxMzguMDcyIDUwLjQyMzggMTM4LjQzMiA1MC40OTcxIDEzOC44MzYgNTAuNDk3MUMxMzkuMzQ2IDUwLjQ5NzEgMTM5LjggNTAuMzk0NSAxNDAuMTk5IDUwLjE4OTVDMTQwLjU5NyA0OS45ODQ0IDE0MC45NDMgNDkuNjk0MyAxNDEuMjM2IDQ5LjMxOTNMMTQyLjM1MiA1MC40MDA0QzE0Mi4xNDcgNTAuNjk5MiAxNDEuODggNTAuOTg2MyAxNDEuNTUyIDUxLjI2MTdDMTQxLjIyNCA1MS41MzEyIDE0MC44MjMgNTEuNzUxIDE0MC4zNDggNTEuOTIwOUMxMzkuODc5IDUyLjA5MDggMTM5LjMzNCA1Mi4xNzU4IDEzOC43MTMgNTIuMTc1OFpNMTUwLjEyMiA1MC4wMzEyVjM4LjVIMTUyLjI0OVY1MkgxNTAuMzI0TDE1MC4xMjIgNTAuMDMxMlpNMTQzLjkzNSA0Ny4zNTA2VjQ3LjE2NkMxNDMuOTM1IDQ2LjQ0NTMgMTQ0LjAyIDQ1Ljc4OTEgMTQ0LjE4OSA0NS4xOTczQzE0NC4zNTkgNDQuNTk5NiAxNDQuNjA1IDQ0LjA4NjkgMTQ0LjkyOCA0My42NTkyQzE0NS4yNSA0My4yMjU2IDE0NS42NDMgNDIuODk0NSAxNDYuMTA1IDQyLjY2NkMxNDYuNTY4IDQyLjQzMTYgMTQ3LjA5IDQyLjMxNDUgMTQ3LjY3IDQyLjMxNDVDMTQ4LjI0NCA0Mi4zMTQ1IDE0OC43NDggNDIuNDI1OCAxNDkuMTgyIDQyLjY0ODRDMTQ5LjYxNSA0Mi44NzExIDE0OS45ODQgNDMuMTkwNCAxNTAuMjg5IDQzLjYwNjRDMTUwLjU5NCA0NC4wMTY2IDE1MC44MzcgNDQuNTA4OCAxNTEuMDE5IDQ1LjA4M0MxNTEuMiA0NS42NTE0IDE1MS4zMjkgNDYuMjg0MiAxNTEuNDA1IDQ2Ljk4MTRWNDcuNTcwM0MxNTEuMzI5IDQ4LjI1IDE1MS4yIDQ4Ljg3MTEgMTUxLjAxOSA0OS40MzM2QzE1MC44MzcgNDkuOTk2MSAxNTAuNTk0IDUwLjQ4MjQgMTUwLjI4OSA1MC44OTI2QzE0OS45ODQgNTEuMzAyNyAxNDkuNjEyIDUxLjYxOTEgMTQ5LjE3MyA1MS44NDE4QzE0OC43MzkgNTIuMDY0NSAxNDguMjMyIDUyLjE3NTggMTQ3LjY1MiA1Mi4xNzU4QzE0Ny4wNzggNTIuMTc1OCAxNDYuNTYgNTIuMDU1NyAxNDYuMDk3IDUxLjgxNTRDMTQ1LjY0IDUxLjU3NTIgMTQ1LjI1IDUxLjIzODMgMTQ0LjkyOCA1MC44MDQ3QzE0NC42MDUgNTAuMzcxMSAxNDQuMzU5IDQ5Ljg2MTMgMTQ0LjE4OSA0OS4yNzU0QzE0NC4wMiA0OC42ODM2IDE0My45MzUgNDguMDQyIDE0My45MzUgNDcuMzUwNlpNMTQ2LjA1MyA0Ny4xNjZWNDcuMzUwNkMxNDYuMDUzIDQ3Ljc4NDIgMTQ2LjA5MSA0OC4xODg1IDE0Ni4xNjcgNDguNTYzNUMxNDYuMjQ5IDQ4LjkzODUgMTQ2LjM3NSA0OS4yNjk1IDE0Ni41NDUgNDkuNTU2NkMxNDYuNzE1IDQ5LjgzNzkgMTQ2LjkzNSA1MC4wNjA1IDE0Ny4yMDQgNTAuMjI0NkMxNDcuNDc5IDUwLjM4MjggMTQ3LjgwOCA1MC40NjE5IDE0OC4xODggNTAuNDYxOUMxNDguNjY5IDUwLjQ2MTkgMTQ5LjA2NCA1MC4zNTY0IDE0OS4zNzUgNTAuMTQ1NUMxNDkuNjg2IDQ5LjkzNDYgMTQ5LjkyOSA0OS42NTA0IDE1MC4xMDQgNDkuMjkzQzE1MC4yODYgNDguOTI5NyAxNTAuNDA5IDQ4LjUyNTQgMTUwLjQ3NCA0OC4wODAxVjQ2LjQ4OTNDMTUwLjQzOCA0Ni4xNDM2IDE1MC4zNjUgNDUuODIxMyAxNTAuMjU0IDQ1LjUyMjVDMTUwLjE0OCA0NS4yMjM2IDE1MC4wMDUgNDQuOTYyOSAxNDkuODIzIDQ0Ljc0MDJDMTQ5LjY0MiA0NC41MTE3IDE0OS40MTYgNDQuMzM1OSAxNDkuMTQ2IDQ0LjIxMjlDMTQ4Ljg4MyA0NC4wODQgMTQ4LjU2OSA0NC4wMTk1IDE0OC4yMDYgNDQuMDE5NUMxNDcuODE5IDQ0LjAxOTUgMTQ3LjQ5MSA0NC4xMDE2IDE0Ny4yMjIgNDQuMjY1NkMxNDYuOTUyIDQ0LjQyOTcgMTQ2LjcyOSA0NC42NTUzIDE0Ni41NTQgNDQuOTQyNEMxNDYuMzg0IDQ1LjIyOTUgMTQ2LjI1OCA0NS41NjM1IDE0Ni4xNzYgNDUuOTQ0M0MxNDYuMDk0IDQ2LjMyNTIgMTQ2LjA1MyA0Ni43MzI0IDE0Ni4wNTMgNDcuMTY2WiIgZmlsbD0iIzE5ODAzOCIvPgo8L2c+CjxtYXNrIGlkPSJwYXRoLTYtaW5zaWRlLTFfNDY5N18zNjcxMyIgZmlsbD0id2hpdGUiPgo8cGF0aCBkPSJNOCA4OEM4IDg1Ljc5MDkgOS43OTA4NiA4NCAxMiA4NEgyMDRDMjA2LjIwOSA4NCAyMDggODUuNzkwOSAyMDggODhWMTQwQzIwOCAxNDIuMjA5IDIwNi4yMDkgMTQ0IDIwNCAxNDRIMTJDOS43OTA4NiAxNDQgOCAxNDIuMjA5IDggMTQwVjg4WiIvPgo8L21hc2s+CjxwYXRoIGQ9Ik04IDg4QzggODUuNzkwOSA5Ljc5MDg2IDg0IDEyIDg0SDIwNEMyMDYuMjA5IDg0IDIwOCA4NS43OTA5IDIwOCA4OFYxNDBDMjA4IDE0Mi4yMDkgMjA2LjIwOSAxNDQgMjA0IDE0NEgxMkM5Ljc5MDg2IDE0NCA4IDE0Mi4yMDkgOCAxNDBWODhaIiBmaWxsPSIjRDEyNzMwIi8+CjxwYXRoIGQ9Ik04IDg0SDIwOEg4Wk0yMDggMTQwQzIwOCAxNDMuMzE0IDIwNS4zMTQgMTQ2IDIwMiAxNDZIMTRDMTAuNjg2MyAxNDYgOCAxNDMuMzE0IDggMTQwQzggMTQxLjEwNSA5Ljc5MDg2IDE0MiAxMiAxNDJIMjA0QzIwNi4yMDkgMTQyIDIwOCAxNDEuMTA1IDIwOCAxNDBaTTggMTQ0Vjg0VjE0NFpNMjA4IDg0VjE0NFY4NFoiIGZpbGw9IiNEMTI3MzAiIG1hc2s9InVybCgjcGF0aC02LWluc2lkZS0xXzQ2OTdfMzY3MTMpIi8+CjxtYXNrIGlkPSJtYXNrMV80Njk3XzM2NzEzIiBzdHlsZT0ibWFzay10eXBlOmFscGhhIiBtYXNrVW5pdHM9InVzZXJTcGFjZU9uVXNlIiB4PSI2NSIgeT0iMTAyIiB3aWR0aD0iMjUiIGhlaWdodD0iMjQiPgo8cmVjdCB4PSI2NS41IiB5PSIxMDIiIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgZmlsbD0iI0Q5RDlEOSIvPgo8L21hc2s+CjxnIG1hc2s9InVybCgjbWFzazFfNDY5N18zNjcxMykiPgo8cGF0aCBkPSJNNzEuNSAxMjRDNzAuOTUgMTI0IDcwLjQ3OTIgMTIzLjgwNCA3MC4wODc1IDEyMy40MTNDNjkuNjk1OCAxMjMuMDIxIDY5LjUgMTIyLjU1IDY5LjUgMTIyVjExMkM2OS41IDExMS40NSA2OS42OTU4IDExMC45NzkgNzAuMDg3NSAxMTAuNTg4QzcwLjQ3OTIgMTEwLjE5NiA3MC45NSAxMTAgNzEuNSAxMTBINzIuNVYxMDhDNzIuNSAxMDYuNjE3IDcyLjk4NzUgMTA1LjQzOCA3My45NjI1IDEwNC40NjJDNzQuOTM3NSAxMDMuNDg3IDc2LjExNjcgMTAzIDc3LjUgMTAzQzc4Ljg4MzMgMTAzIDgwLjA2MjUgMTAzLjQ4NyA4MS4wMzc1IDEwNC40NjJDODIuMDEyNSAxMDUuNDM4IDgyLjUgMTA2LjYxNyA4Mi41IDEwOFYxMTBIODMuNUM4NC4wNSAxMTAgODQuNTIwOCAxMTAuMTk2IDg0LjkxMjUgMTEwLjU4OEM4NS4zMDQyIDExMC45NzkgODUuNSAxMTEuNDUgODUuNSAxMTJWMTIyQzg1LjUgMTIyLjU1IDg1LjMwNDIgMTIzLjAyMSA4NC45MTI1IDEyMy40MTNDODQuNTIwOCAxMjMuODA0IDg0LjA1IDEyNCA4My41IDEyNEg3MS41Wk03MS41IDEyMkg4My41VjExMkg3MS41VjEyMlpNNzcuNSAxMTlDNzguMDUgMTE5IDc4LjUyMDggMTE4LjgwNCA3OC45MTI1IDExOC40MTNDNzkuMzA0MiAxMTguMDIxIDc5LjUgMTE3LjU1IDc5LjUgMTE3Qzc5LjUgMTE2LjQ1IDc5LjMwNDIgMTE1Ljk3OSA3OC45MTI1IDExNS41ODhDNzguNTIwOCAxMTUuMTk2IDc4LjA1IDExNSA3Ny41IDExNUM3Ni45NSAxMTUgNzYuNDc5MiAxMTUuMTk2IDc2LjA4NzUgMTE1LjU4OEM3NS42OTU4IDExNS45NzkgNzUuNSAxMTYuNDUgNzUuNSAxMTdDNzUuNSAxMTcuNTUgNzUuNjk1OCAxMTguMDIxIDc2LjA4NzUgMTE4LjQxM0M3Ni40NzkyIDExOC44MDQgNzYuOTUgMTE5IDc3LjUgMTE5Wk03NC41IDExMEg4MC41VjEwOEM4MC41IDEwNy4xNjcgODAuMjA4MyAxMDYuNDU4IDc5LjYyNSAxMDUuODc1Qzc5LjA0MTcgMTA1LjI5MiA3OC4zMzMzIDEwNSA3Ny41IDEwNUM3Ni42NjY3IDEwNSA3NS45NTgzIDEwNS4yOTIgNzUuMzc1IDEwNS44NzVDNzQuNzkxNyAxMDYuNDU4IDc0LjUgMTA3LjE2NyA3NC41IDEwOFYxMTBaIiBmaWxsPSJ3aGl0ZSIvPgo8L2c+CjxwYXRoIGQ9Ik0xMDIuNjc2IDExNS44MzRIMTA0Ljg3M0MxMDQuODAzIDExNi42NzIgMTA0LjU2OCAxMTcuNDE5IDEwNC4xNyAxMTguMDc1QzEwMy43NzEgMTE4LjcyNiAxMDMuMjEyIDExOS4yMzggMTAyLjQ5MSAxMTkuNjEzQzEwMS43NzEgMTE5Ljk4OCAxMDAuODk1IDEyMC4xNzYgOTkuODYzMyAxMjAuMTc2Qzk5LjA3MjMgMTIwLjE3NiA5OC4zNjA0IDEyMC4wMzUgOTcuNzI3NSAxMTkuNzU0Qzk3LjA5NDcgMTE5LjQ2NyA5Ni41NTI3IDExOS4wNjIgOTYuMTAxNiAxMTguNTQxQzk1LjY1MDQgMTE4LjAxNCA5NS4zMDQ3IDExNy4zNzggOTUuMDY0NSAxMTYuNjM0Qzk0LjgzMDEgMTE1Ljg5IDk0LjcxMjkgMTE1LjA1OCA5NC43MTI5IDExNC4xMzhWMTEzLjA3NEM5NC43MTI5IDExMi4xNTQgOTQuODMzIDExMS4zMjIgOTUuMDczMiAxMTAuNTc4Qzk1LjMxOTMgMTA5LjgzNCA5NS42NzA5IDEwOS4xOTggOTYuMTI3OSAxMDguNjcxQzk2LjU4NSAxMDguMTM4IDk3LjEzMjggMTA3LjczIDk3Ljc3MTUgMTA3LjQ0OUM5OC40MTYgMTA3LjE2OCA5OS4xMzk2IDEwNy4wMjcgOTkuOTQyNCAxMDcuMDI3QzEwMC45NjIgMTA3LjAyNyAxMDEuODIzIDEwNy4yMTUgMTAyLjUyNiAxMDcuNTlDMTAzLjIyOSAxMDcuOTY1IDEwMy43NzQgMTA4LjQ4MyAxMDQuMTYxIDEwOS4xNDZDMTA0LjU1NCAxMDkuODA4IDEwNC43OTQgMTEwLjU2NiAxMDQuODgyIDExMS40MjJIMTAyLjY4NUMxMDIuNjI2IDExMC44NzEgMTAyLjQ5NyAxMTAuMzk5IDEwMi4yOTggMTEwLjAwN0MxMDIuMTA0IDEwOS42MTQgMTAxLjgxNyAxMDkuMzE1IDEwMS40MzcgMTA5LjExQzEwMS4wNTYgMTA4Ljg5OSAxMDAuNTU4IDEwOC43OTQgOTkuOTQyNCAxMDguNzk0Qzk5LjQzODUgMTA4Ljc5NCA5OC45OTkgMTA4Ljg4OCA5OC42MjQgMTA5LjA3NUM5OC4yNDkgMTA5LjI2MyA5Ny45MzU1IDEwOS41MzggOTcuNjgzNiAxMDkuOTAxQzk3LjQzMTYgMTEwLjI2NSA5Ny4yNDEyIDExMC43MTMgOTcuMTEyMyAxMTEuMjQ2Qzk2Ljk4OTMgMTExLjc3MyA5Ni45Mjc3IDExMi4zNzcgOTYuOTI3NyAxMTMuMDU3VjExNC4xMzhDOTYuOTI3NyAxMTQuNzgyIDk2Ljk4MzQgMTE1LjM2OCA5Ny4wOTQ3IDExNS44OTZDOTcuMjExOSAxMTYuNDE3IDk3LjM4NzcgMTE2Ljg2NSA5Ny42MjIxIDExNy4yNEM5Ny44NjIzIDExNy42MTUgOTguMTY3IDExNy45MDUgOTguNTM2MSAxMTguMTFDOTguOTA1MyAxMTguMzE1IDk5LjM0NzcgMTE4LjQxOCA5OS44NjMzIDExOC40MThDMTAwLjQ5IDExOC40MTggMTAwLjk5NyAxMTguMzE4IDEwMS4zODQgMTE4LjExOUMxMDEuNzc2IDExNy45MiAxMDIuMDcyIDExNy42MyAxMDIuMjcxIDExNy4yNDlDMTAyLjQ3NyAxMTYuODYyIDEwMi42MTEgMTE2LjM5MSAxMDIuNjc2IDExNS44MzRaTTEwOS4wODQgMTA2LjVWMTIwSDEwNi45NTdWMTA2LjVIMTA5LjA4NFpNMTExLjE1IDExNS4zNTFWMTE1LjE0OEMxMTEuMTUgMTE0LjQ2MyAxMTEuMjQ5IDExMy44MjcgMTExLjQ0OCAxMTMuMjQxQzExMS42NDggMTEyLjY0OSAxMTEuOTM1IDExMi4xMzcgMTEyLjMxIDExMS43MDNDMTEyLjY5MSAxMTEuMjY0IDExMy4xNTQgMTEwLjkyNCAxMTMuNjk4IDExMC42ODRDMTE0LjI0OSAxMTAuNDM4IDExNC44NyAxMTAuMzE0IDExNS41NjIgMTEwLjMxNEMxMTYuMjU5IDExMC4zMTQgMTE2Ljg4IDExMC40MzggMTE3LjQyNSAxMTAuNjg0QzExNy45NzYgMTEwLjkyNCAxMTguNDQyIDExMS4yNjQgMTE4LjgyMiAxMTEuNzAzQzExOS4yMDMgMTEyLjEzNyAxMTkuNDkzIDExMi42NDkgMTE5LjY5MyAxMTMuMjQxQzExOS44OTIgMTEzLjgyNyAxMTkuOTkxIDExNC40NjMgMTE5Ljk5MSAxMTUuMTQ4VjExNS4zNTFDMTE5Ljk5MSAxMTYuMDM2IDExOS44OTIgMTE2LjY3MiAxMTkuNjkzIDExNy4yNThDMTE5LjQ5MyAxMTcuODQ0IDExOS4yMDMgMTE4LjM1NiAxMTguODIyIDExOC43OTZDMTE4LjQ0MiAxMTkuMjI5IDExNy45NzkgMTE5LjU2OSAxMTcuNDM0IDExOS44MTVDMTE2Ljg4OSAxMjAuMDU2IDExNi4yNzEgMTIwLjE3NiAxMTUuNTc5IDEyMC4xNzZDMTE0Ljg4MiAxMjAuMTc2IDExNC4yNTggMTIwLjA1NiAxMTMuNzA3IDExOS44MTVDMTEzLjE2MiAxMTkuNTY5IDExMi42OTkgMTE5LjIyOSAxMTIuMzE5IDExOC43OTZDMTExLjkzOCAxMTguMzU2IDExMS42NDggMTE3Ljg0NCAxMTEuNDQ4IDExNy4yNThDMTExLjI0OSAxMTYuNjcyIDExMS4xNSAxMTYuMDM2IDExMS4xNSAxMTUuMzUxWk0xMTMuMjY4IDExNS4xNDhWMTE1LjM1MUMxMTMuMjY4IDExNS43NzggMTEzLjMxMiAxMTYuMTgzIDExMy40IDExNi41NjNDMTEzLjQ4NyAxMTYuOTQ0IDExMy42MjUgMTE3LjI3OCAxMTMuODEzIDExNy41NjVDMTE0IDExNy44NTMgMTE0LjI0IDExOC4wNzggMTE0LjUzMyAxMTguMjQyQzExNC44MjYgMTE4LjQwNiAxMTUuMTc1IDExOC40ODggMTE1LjU3OSAxMTguNDg4QzExNS45NzIgMTE4LjQ4OCAxMTYuMzEyIDExOC40MDYgMTE2LjU5OSAxMTguMjQyQzExNi44OTIgMTE4LjA3OCAxMTcuMTMyIDExNy44NTMgMTE3LjMyIDExNy41NjVDMTE3LjUwNyAxMTcuMjc4IDExNy42NDUgMTE2Ljk0NCAxMTcuNzMzIDExNi41NjNDMTE3LjgyNiAxMTYuMTgzIDExNy44NzMgMTE1Ljc3OCAxMTcuODczIDExNS4zNTFWMTE1LjE0OEMxMTcuODczIDExNC43MjcgMTE3LjgyNiAxMTQuMzI4IDExNy43MzMgMTEzLjk1M0MxMTcuNjQ1IDExMy41NzIgMTE3LjUwNCAxMTMuMjM1IDExNy4zMTEgMTEyLjk0MkMxMTcuMTIzIDExMi42NDkgMTE2Ljg4MyAxMTIuNDIxIDExNi41OSAxMTIuMjU3QzExNi4zMDMgMTEyLjA4NyAxMTUuOTYgMTEyLjAwMiAxMTUuNTYyIDExMi4wMDJDMTE1LjE2MyAxMTIuMDAyIDExNC44MTggMTEyLjA4NyAxMTQuNTI1IDExMi4yNTdDMTE0LjIzNyAxMTIuNDIxIDExNCAxMTIuNjQ5IDExMy44MTMgMTEyLjk0MkMxMTMuNjI1IDExMy4yMzUgMTEzLjQ4NyAxMTMuNTcyIDExMy40IDExMy45NTNDMTEzLjMxMiAxMTQuMzI4IDExMy4yNjggMTE0LjcyNyAxMTMuMjY4IDExNS4xNDhaTTEyNy4yNTIgMTE3LjQyNUMxMjcuMjUyIDExNy4yMTQgMTI3LjE5OSAxMTcuMDIzIDEyNy4wOTQgMTE2Ljg1NEMxMjYuOTg4IDExNi42NzggMTI2Ljc4NiAxMTYuNTIgMTI2LjQ4NyAxMTYuMzc5QzEyNi4xOTQgMTE2LjIzOCAxMjUuNzYxIDExNi4xMDkgMTI1LjE4NiAxMTUuOTkyQzEyNC42ODIgMTE1Ljg4MSAxMjQuMjIgMTE1Ljc0OSAxMjMuNzk4IDExNS41OTdDMTIzLjM4MiAxMTUuNDM4IDEyMy4wMjQgMTE1LjI0OCAxMjIuNzI1IDExNS4wMjVDMTIyLjQyNyAxMTQuODAzIDEyMi4xOTUgMTE0LjUzOSAxMjIuMDMxIDExNC4yMzRDMTIxLjg2NyAxMTMuOTMgMTIxLjc4NSAxMTMuNTc4IDEyMS43ODUgMTEzLjE4QzEyMS43ODUgMTEyLjc5MyAxMjEuODcgMTEyLjQyNyAxMjIuMDQgMTEyLjA4MUMxMjIuMjEgMTExLjczNSAxMjIuNDUzIDExMS40MzEgMTIyLjc2OSAxMTEuMTY3QzEyMy4wODYgMTEwLjkwMyAxMjMuNDcgMTEwLjY5NSAxMjMuOTIxIDExMC41NDNDMTI0LjM3OCAxMTAuMzkxIDEyNC44ODggMTEwLjMxNCAxMjUuNDUgMTEwLjMxNEMxMjYuMjQ3IDExMC4zMTQgMTI2LjkyOSAxMTAuNDQ5IDEyNy40OTggMTEwLjcxOUMxMjguMDcyIDExMC45ODIgMTI4LjUxMiAxMTEuMzQzIDEyOC44MTYgMTExLjhDMTI5LjEyMSAxMTIuMjUxIDEyOS4yNzMgMTEyLjc2MSAxMjkuMjczIDExMy4zMjlIMTI3LjE1NUMxMjcuMTU1IDExMy4wNzcgMTI3LjA5MSAxMTIuODQzIDEyNi45NjIgMTEyLjYyNkMxMjYuODM5IDExMi40MDMgMTI2LjY1MSAxMTIuMjI1IDEyNi4zOTkgMTEyLjA5QzEyNi4xNDcgMTExLjk0OSAxMjUuODMxIDExMS44NzkgMTI1LjQ1IDExMS44NzlDMTI1LjA4NyAxMTEuODc5IDEyNC43ODUgMTExLjkzOCAxMjQuNTQ1IDExMi4wNTVDMTI0LjMxIDExMi4xNjYgMTI0LjEzNSAxMTIuMzEyIDEyNC4wMTcgMTEyLjQ5NEMxMjMuOTA2IDExMi42NzYgMTIzLjg1IDExMi44NzUgMTIzLjg1IDExMy4wOTJDMTIzLjg1IDExMy4yNSAxMjMuODggMTEzLjM5NCAxMjMuOTM4IDExMy41MjJDMTI0LjAwMyAxMTMuNjQ2IDEyNC4xMDggMTEzLjc2IDEyNC4yNTUgMTEzLjg2NUMxMjQuNDAxIDExMy45NjUgMTI0LjYgMTE0LjA1OSAxMjQuODUyIDExNC4xNDZDMTI1LjExIDExNC4yMzQgMTI1LjQzMiAxMTQuMzE5IDEyNS44MTkgMTE0LjQwMUMxMjYuNTQ2IDExNC41NTQgMTI3LjE3IDExNC43NSAxMjcuNjkxIDExNC45OUMxMjguMjE5IDExNS4yMjUgMTI4LjYyMyAxMTUuNTI5IDEyOC45MDQgMTE1LjkwNEMxMjkuMTg1IDExNi4yNzMgMTI5LjMyNiAxMTYuNzQyIDEyOS4zMjYgMTE3LjMxMUMxMjkuMzI2IDExNy43MzIgMTI5LjIzNSAxMTguMTE5IDEyOS4wNTQgMTE4LjQ3MUMxMjguODc4IDExOC44MTYgMTI4LjYyIDExOS4xMTggMTI4LjI4IDExOS4zNzZDMTI3Ljk0IDExOS42MjggMTI3LjUzMyAxMTkuODI0IDEyNy4wNTggMTE5Ljk2NUMxMjYuNTkgMTIwLjEwNSAxMjYuMDYyIDEyMC4xNzYgMTI1LjQ3NiAxMjAuMTc2QzEyNC42MTUgMTIwLjE3NiAxMjMuODg2IDEyMC4wMjMgMTIzLjI4OCAxMTkuNzE5QzEyMi42OSAxMTkuNDA4IDEyMi4yMzYgMTE5LjAxMyAxMjEuOTI2IDExOC41MzJDMTIxLjYyMSAxMTguMDQ2IDEyMS40NjkgMTE3LjU0MiAxMjEuNDY5IDExNy4wMjFIMTIzLjUxNkMxMjMuNTQgMTE3LjQxMyAxMjMuNjQ4IDExNy43MjcgMTIzLjg0MiAxMTcuOTYxQzEyNC4wNDEgMTE4LjE4OSAxMjQuMjg3IDExOC4zNTYgMTI0LjU4IDExOC40NjJDMTI0Ljg3OSAxMTguNTYyIDEyNS4xODYgMTE4LjYxMSAxMjUuNTAzIDExOC42MTFDMTI1Ljg4NCAxMTguNjExIDEyNi4yMDMgMTE4LjU2MiAxMjYuNDYxIDExOC40NjJDMTI2LjcxOSAxMTguMzU2IDEyNi45MTUgMTE4LjIxNiAxMjcuMDUgMTE4LjA0QzEyNy4xODQgMTE3Ljg1OCAxMjcuMjUyIDExNy42NTMgMTI3LjI1MiAxMTcuNDI1Wk0xMzUuNTIzIDEyMC4xNzZDMTM0LjgyIDEyMC4xNzYgMTM0LjE4NCAxMjAuMDYyIDEzMy42MTYgMTE5LjgzM0MxMzMuMDUzIDExOS41OTkgMTMyLjU3MyAxMTkuMjczIDEzMi4xNzQgMTE4Ljg1N0MxMzEuNzgyIDExOC40NDEgMTMxLjQ4IDExNy45NTIgMTMxLjI2OSAxMTcuMzlDMTMxLjA1OCAxMTYuODI3IDEzMC45NTMgMTE2LjIyMSAxMzAuOTUzIDExNS41N1YxMTUuMjE5QzEzMC45NTMgMTE0LjQ3NSAxMzEuMDYxIDExMy44MDEgMTMxLjI3OCAxMTMuMTk3QzEzMS40OTUgMTEyLjU5NCAxMzEuNzk2IDExMi4wNzggMTMyLjE4MyAxMTEuNjVDMTMyLjU3IDExMS4yMTcgMTMzLjAyNyAxMTAuODg2IDEzMy41NTQgMTEwLjY1N0MxMzQuMDgxIDExMC40MjkgMTM0LjY1MyAxMTAuMzE0IDEzNS4yNjggMTEwLjMxNEMxMzUuOTQ4IDExMC4zMTQgMTM2LjU0MiAxMTAuNDI5IDEzNy4wNTIgMTEwLjY1N0MxMzcuNTYyIDExMC44ODYgMTM3Ljk4NCAxMTEuMjA4IDEzOC4zMTggMTExLjYyNEMxMzguNjU4IDExMi4wMzQgMTM4LjkxIDExMi41MjMgMTM5LjA3NCAxMTMuMDkyQzEzOS4yNDQgMTEzLjY2IDEzOS4zMjkgMTE0LjI4NyAxMzkuMzI5IDExNC45NzNWMTE1Ljg3OEgxMzEuOTgxVjExNC4zNTdIMTM3LjIzN1YxMTQuMTlDMTM3LjIyNSAxMTMuODEgMTM3LjE0OSAxMTMuNDUyIDEzNy4wMDggMTEzLjExOEMxMzYuODczIDExMi43ODQgMTM2LjY2NSAxMTIuNTE1IDEzNi4zODQgMTEyLjMxQzEzNi4xMDMgMTEyLjEwNCAxMzUuNzI4IDExMi4wMDIgMTM1LjI1OSAxMTIuMDAyQzEzNC45MDggMTEyLjAwMiAxMzQuNTk0IDExMi4wNzggMTM0LjMxOSAxMTIuMjNDMTM0LjA0OSAxMTIuMzc3IDEzMy44MjQgMTEyLjU5MSAxMzMuNjQyIDExMi44NzJDMTMzLjQ2IDExMy4xNTMgMTMzLjMyIDExMy40OTMgMTMzLjIyIDExMy44OTJDMTMzLjEyNiAxMTQuMjg0IDEzMy4wNzkgMTE0LjcyNyAxMzMuMDc5IDExNS4yMTlWMTE1LjU3QzEzMy4wNzkgMTE1Ljk4NiAxMzMuMTM1IDExNi4zNzMgMTMzLjI0NiAxMTYuNzNDMTMzLjM2NCAxMTcuMDgyIDEzMy41MzQgMTE3LjM5IDEzMy43NTYgMTE3LjY1M0MxMzMuOTc5IDExNy45MTcgMTM0LjI0OCAxMTguMTI1IDEzNC41NjUgMTE4LjI3N0MxMzQuODgxIDExOC40MjQgMTM1LjI0MiAxMTguNDk3IDEzNS42NDYgMTE4LjQ5N0MxMzYuMTU2IDExOC40OTcgMTM2LjYxIDExOC4zOTUgMTM3LjAwOCAxMTguMTg5QzEzNy40MDcgMTE3Ljk4NCAxMzcuNzUyIDExNy42OTQgMTM4LjA0NSAxMTcuMzE5TDEzOS4xNjIgMTE4LjRDMTM4Ljk1NiAxMTguNjk5IDEzOC42OSAxMTguOTg2IDEzOC4zNjIgMTE5LjI2MkMxMzguMDM0IDExOS41MzEgMTM3LjYzMiAxMTkuNzUxIDEzNy4xNTggMTE5LjkyMUMxMzYuNjg5IDEyMC4wOTEgMTM2LjE0NCAxMjAuMTc2IDEzNS41MjMgMTIwLjE3NlpNMTQ2LjkzMiAxMTguMDMxVjEwNi41SDE0OS4wNTlWMTIwSDE0Ny4xMzRMMTQ2LjkzMiAxMTguMDMxWk0xNDAuNzQ0IDExNS4zNTFWMTE1LjE2NkMxNDAuNzQ0IDExNC40NDUgMTQwLjgyOSAxMTMuNzg5IDE0MC45OTkgMTEzLjE5N0MxNDEuMTY5IDExMi42IDE0MS40MTUgMTEyLjA4NyAxNDEuNzM3IDExMS42NTlDMTQyLjA2IDExMS4yMjYgMTQyLjQ1MiAxMTAuODk1IDE0Mi45MTUgMTEwLjY2NkMxNDMuMzc4IDExMC40MzIgMTQzLjg5OSAxMTAuMzE0IDE0NC40NzkgMTEwLjMxNEMxNDUuMDU0IDExMC4zMTQgMTQ1LjU1OCAxMTAuNDI2IDE0NS45OTEgMTEwLjY0OEMxNDYuNDI1IDExMC44NzEgMTQ2Ljc5NCAxMTEuMTkgMTQ3LjA5OSAxMTEuNjA2QzE0Ny40MDMgMTEyLjAxNyAxNDcuNjQ2IDExMi41MDkgMTQ3LjgyOCAxMTMuMDgzQzE0OC4wMSAxMTMuNjUxIDE0OC4xMzkgMTE0LjI4NCAxNDguMjE1IDExNC45ODFWMTE1LjU3QzE0OC4xMzkgMTE2LjI1IDE0OC4wMSAxMTYuODcxIDE0Ny44MjggMTE3LjQzNEMxNDcuNjQ2IDExNy45OTYgMTQ3LjQwMyAxMTguNDgyIDE0Ny4wOTkgMTE4Ljg5M0MxNDYuNzk0IDExOS4zMDMgMTQ2LjQyMiAxMTkuNjE5IDE0NS45ODIgMTE5Ljg0MkMxNDUuNTQ5IDEyMC4wNjQgMTQ1LjA0MiAxMjAuMTc2IDE0NC40NjIgMTIwLjE3NkMxNDMuODg4IDEyMC4xNzYgMTQzLjM2OSAxMjAuMDU2IDE0Mi45MDYgMTE5LjgxNUMxNDIuNDQ5IDExOS41NzUgMTQyLjA2IDExOS4yMzggMTQxLjczNyAxMTguODA1QzE0MS40MTUgMTE4LjM3MSAxNDEuMTY5IDExNy44NjEgMTQwLjk5OSAxMTcuMjc1QzE0MC44MjkgMTE2LjY4NCAxNDAuNzQ0IDExNi4wNDIgMTQwLjc0NCAxMTUuMzUxWk0xNDIuODYyIDExNS4xNjZWMTE1LjM1MUMxNDIuODYyIDExNS43ODQgMTQyLjkgMTE2LjE4OCAxNDIuOTc3IDExNi41NjNDMTQzLjA1OSAxMTYuOTM4IDE0My4xODUgMTE3LjI3IDE0My4zNTQgMTE3LjU1N0MxNDMuNTI0IDExNy44MzggMTQzLjc0NCAxMTguMDYxIDE0NC4wMTQgMTE4LjIyNUMxNDQuMjg5IDExOC4zODMgMTQ0LjYxNyAxMTguNDYyIDE0NC45OTggMTE4LjQ2MkMxNDUuNDc5IDExOC40NjIgMTQ1Ljg3NCAxMTguMzU2IDE0Ni4xODUgMTE4LjE0NkMxNDYuNDk1IDExNy45MzUgMTQ2LjczOCAxMTcuNjUgMTQ2LjkxNCAxMTcuMjkzQzE0Ny4wOTYgMTE2LjkzIDE0Ny4yMTkgMTE2LjUyNSAxNDcuMjgzIDExNi4wOFYxMTQuNDg5QzE0Ny4yNDggMTE0LjE0NCAxNDcuMTc1IDExMy44MjEgMTQ3LjA2MyAxMTMuNTIyQzE0Ni45NTggMTEzLjIyNCAxNDYuODE0IDExMi45NjMgMTQ2LjYzMyAxMTIuNzRDMTQ2LjQ1MSAxMTIuNTEyIDE0Ni4yMjYgMTEyLjMzNiAxNDUuOTU2IDExMi4yMTNDMTQ1LjY5MiAxMTIuMDg0IDE0NS4zNzkgMTEyLjAyIDE0NS4wMTYgMTEyLjAyQzE0NC42MjkgMTEyLjAyIDE0NC4zMDEgMTEyLjEwMiAxNDQuMDMxIDExMi4yNjZDMTQzLjc2MiAxMTIuNDMgMTQzLjUzOSAxMTIuNjU1IDE0My4zNjMgMTEyLjk0MkMxNDMuMTkzIDExMy4yMjkgMTQzLjA2NyAxMTMuNTYzIDE0Mi45ODUgMTEzLjk0NEMxNDIuOTAzIDExNC4zMjUgMTQyLjg2MiAxMTQuNzMyIDE0Mi44NjIgMTE1LjE2NloiIGZpbGw9IndoaXRlIi8+CjxkZWZzPgo8ZmlsdGVyIGlkPSJmaWx0ZXIwX2RfNDY5N18zNjcxMyIgeD0iMCIgeT0iMTIiIHdpZHRoPSIyMTYiIGhlaWdodD0iNzYiIGZpbHRlclVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgY29sb3ItaW50ZXJwb2xhdGlvbi1maWx0ZXJzPSJzUkdCIj4KPGZlRmxvb2QgZmxvb2Qtb3BhY2l0eT0iMCIgcmVzdWx0PSJCYWNrZ3JvdW5kSW1hZ2VGaXgiLz4KPGZlQ29sb3JNYXRyaXggaW49IlNvdXJjZUFscGhhIiB0eXBlPSJtYXRyaXgiIHZhbHVlcz0iMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMTI3IDAiIHJlc3VsdD0iaGFyZEFscGhhIi8+CjxmZU9mZnNldCBkeT0iNCIvPgo8ZmVHYXVzc2lhbkJsdXIgc3RkRGV2aWF0aW9uPSI0Ii8+CjxmZUNvbXBvc2l0ZSBpbjI9ImhhcmRBbHBoYSIgb3BlcmF0b3I9Im91dCIvPgo8ZmVDb2xvck1hdHJpeCB0eXBlPSJtYXRyaXgiIHZhbHVlcz0iMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMC4wOCAwIi8+CjxmZUJsZW5kIG1vZGU9Im5vcm1hbCIgaW4yPSJCYWNrZ3JvdW5kSW1hZ2VGaXgiIHJlc3VsdD0iZWZmZWN0MV9kcm9wU2hhZG93XzQ2OTdfMzY3MTMiLz4KPGZlQmxlbmQgbW9kZT0ibm9ybWFsIiBpbj0iU291cmNlR3JhcGhpYyIgaW4yPSJlZmZlY3QxX2Ryb3BTaGFkb3dfNDY5N18zNjcxMyIgcmVzdWx0PSJzaGFwZSIvPgo8L2ZpbHRlcj4KPC9kZWZzPgo8L3N2Zz4K",
"description": "Sends the command to the device or updates attribute/time series when the user toggles the button. Widget settings will enable you to configure behavior how to fetch the initial state and what to trigger when button checked/unchecked states.",
"descriptor": {
"type": "rpc",
@@ -11,7 +11,7 @@
"resources": [],
"templateHtml": "\n",
"templateCss": "",
- "controllerScript": "self.onInit = function() {\n self.ctx.$scope.actionWidget.onInit();\n}\n\nself.typeParameters = function() {\n return {\n previewWidth: '300px',\n previewHeight: '150px',\n embedTitlePanel: true,\n displayRpcMessageToast: false\n };\n};\n\nself.onDestroy = function() {\n}\n",
+ "controllerScript": "self.onInit = function() {\n self.ctx.$scope.actionWidget.onInit();\n}\n\nself.typeParameters = function() {\n return {\n previewWidth: '300px',\n previewHeight: '150px',\n embedTitlePanel: true,\n overflowVisible: true,\n displayRpcMessageToast: false\n };\n};\n\nself.onDestroy = function() {\n}\n",
"settingsSchema": "",
"dataKeySettingsSchema": "{}\n",
"settingsDirective": "tb-toggle-button-widget-settings",
diff --git a/application/src/main/data/json/tenant/dashboards/gateways.json b/application/src/main/data/json/tenant/dashboards/gateways.json
index a62550ed95..8030d3e731 100644
--- a/application/src/main/data/json/tenant/dashboards/gateways.json
+++ b/application/src/main/data/json/tenant/dashboards/gateways.json
@@ -200,7 +200,7 @@
"useShowWidgetActionFunction": null,
"showWidgetActionFunction": "return true;",
"type": "customPretty",
- "customHtml": "\n gateway.gateway-configuration
\n \n \n\n\n\n
\n\n",
+ "customHtml": "\n\n\n",
"customCss": ".gateway-config {\n width: 800px !important;\n padding: 0 !important;\n min-height: 75vh;\n max-width: 100%;\n display: grid !important;\n}\n\n@media screen and (max-width: 599px) {\n .mat-mdc-dialog-content {\n max-height: calc(100% - 60px) !important;\n }\n}",
"customFunction": "let $injector = widgetContext.$scope.$injector;\nlet customDialog = $injector.get(widgetContext.servicesMap.get('customDialog'));\n\nopenAddEntityDialog();\n\nfunction openAddEntityDialog() {\n customDialog.customDialog(htmlTemplate, AddEntityDialogController).subscribe();\n}\n\nfunction AddEntityDialogController(instance) {\n let vm = instance;\n \n vm.device = additionalParams.entity.id;\n\n vm.cancel = function() {\n vm.dialogRef.close(null);\n };\n}\n",
"customResources": [],
diff --git a/application/src/main/data/upgrade/3.7.0/schema_update.sql b/application/src/main/data/upgrade/3.7.0/schema_update.sql
index 6b87dc6dde..d400821fa3 100644
--- a/application/src/main/data/upgrade/3.7.0/schema_update.sql
+++ b/application/src/main/data/upgrade/3.7.0/schema_update.sql
@@ -14,3 +14,185 @@
-- limitations under the License.
--
+-- UPDATE RESOURCE SUB TYPE START
+
+DO
+$$
+ BEGIN
+ IF NOT EXISTS (
+ SELECT FROM information_schema.columns
+ WHERE table_name = 'resource' AND column_name = 'resource_sub_type'
+ ) THEN
+ ALTER TABLE resource ADD COLUMN resource_sub_type varchar(32);
+ UPDATE resource SET resource_sub_type = 'IMAGE' WHERE resource_type = 'IMAGE';
+ END IF;
+ END;
+$$;
+
+-- UPDATE RESOURCE SUB TYPE END
+
+-- UPDATE WIDGETS BUNDLE START
+
+DO
+$$
+ BEGIN
+ IF NOT EXISTS (
+ SELECT FROM information_schema.columns
+ WHERE table_name = 'widgets_bundle' AND column_name = 'scada'
+ ) THEN
+ ALTER TABLE widgets_bundle ADD COLUMN scada boolean NOT NULL DEFAULT false;
+ END IF;
+ END;
+$$;
+
+-- UPDATE WIDGETS BUNDLE END
+
+-- UPDATE WIDGET TYPE START
+
+DO
+$$
+ BEGIN
+ IF NOT EXISTS (
+ SELECT FROM information_schema.columns
+ WHERE table_name = 'widget_type' AND column_name = 'scada'
+ ) THEN
+ ALTER TABLE widget_type ADD COLUMN scada boolean NOT NULL DEFAULT false;
+ END IF;
+ END;
+$$;
+
+-- UPDATE WIDGET TYPE END
+
+-- KV VERSIONING UPDATE START
+
+CREATE SEQUENCE IF NOT EXISTS attribute_kv_version_seq cache 1;
+CREATE SEQUENCE IF NOT EXISTS ts_kv_latest_version_seq cache 1;
+
+ALTER TABLE attribute_kv ADD COLUMN IF NOT EXISTS version bigint default 0;
+ALTER TABLE ts_kv_latest ADD COLUMN IF NOT EXISTS version bigint default 0;
+
+-- KV VERSIONING UPDATE END
+
+-- RELATION VERSIONING UPDATE START
+
+CREATE SEQUENCE IF NOT EXISTS relation_version_seq cache 1;
+ALTER TABLE relation ADD COLUMN IF NOT EXISTS version bigint default 0;
+
+-- RELATION VERSIONING UPDATE END
+
+
+-- ENTITIES VERSIONING UPDATE START
+
+ALTER TABLE device ADD COLUMN IF NOT EXISTS version BIGINT DEFAULT 1;
+ALTER TABLE device_profile ADD COLUMN IF NOT EXISTS version BIGINT DEFAULT 1;
+ALTER TABLE device_credentials ADD COLUMN IF NOT EXISTS version BIGINT DEFAULT 1;
+ALTER TABLE asset ADD COLUMN IF NOT EXISTS version BIGINT DEFAULT 1;
+ALTER TABLE asset_profile ADD COLUMN IF NOT EXISTS version BIGINT DEFAULT 1;
+ALTER TABLE entity_view ADD COLUMN IF NOT EXISTS version BIGINT DEFAULT 1;
+ALTER TABLE tb_user ADD COLUMN IF NOT EXISTS version BIGINT DEFAULT 1;
+ALTER TABLE customer ADD COLUMN IF NOT EXISTS version BIGINT DEFAULT 1;
+ALTER TABLE edge ADD COLUMN IF NOT EXISTS version BIGINT DEFAULT 1;
+ALTER TABLE rule_chain ADD COLUMN IF NOT EXISTS version BIGINT DEFAULT 1;
+ALTER TABLE dashboard ADD COLUMN IF NOT EXISTS version BIGINT DEFAULT 1;
+ALTER TABLE widget_type ADD COLUMN IF NOT EXISTS version BIGINT DEFAULT 1;
+ALTER TABLE widgets_bundle ADD COLUMN IF NOT EXISTS version BIGINT DEFAULT 1;
+ALTER TABLE tenant ADD COLUMN IF NOT EXISTS version BIGINT DEFAULT 1;
+
+-- ENTITIES VERSIONING UPDATE END
+
+-- OAUTH2 UPDATE START
+
+ALTER TABLE IF EXISTS oauth2_mobile RENAME TO mobile_app;
+ALTER TABLE IF EXISTS oauth2_domain RENAME TO domain;
+ALTER TABLE IF EXISTS oauth2_registration RENAME TO oauth2_client;
+
+ALTER TABLE domain ADD COLUMN IF NOT EXISTS oauth2_enabled boolean,
+ ADD COLUMN IF NOT EXISTS edge_enabled boolean,
+ ADD COLUMN IF NOT EXISTS tenant_id uuid DEFAULT '13814000-1dd2-11b2-8080-808080808080',
+ DROP COLUMN IF EXISTS domain_scheme;
+
+-- rename column domain_name to name
+DO
+$$
+ BEGIN
+ IF EXISTS(SELECT 1 FROM information_schema.columns WHERE table_name='domain' and column_name='domain_name') THEN
+ ALTER TABLE domain RENAME COLUMN domain_name TO name;
+ END IF;
+ END
+$$;
+
+-- delete duplicated domains
+DELETE FROM domain d1 USING (
+ SELECT MIN(ctid) as ctid, name
+ FROM domain
+ GROUP BY name HAVING COUNT(*) > 1
+) d2 WHERE d1.name = d2.name AND d1.ctid <> d2.ctid;
+
+ALTER TABLE mobile_app ADD COLUMN IF NOT EXISTS oauth2_enabled boolean,
+ ADD COLUMN IF NOT EXISTS tenant_id uuid DEFAULT '13814000-1dd2-11b2-8080-808080808080';
+
+-- delete duplicated apps
+DELETE FROM mobile_app m1 USING (
+ SELECT MIN(ctid) as ctid, pkg_name
+ FROM mobile_app
+ GROUP BY pkg_name HAVING COUNT(*) > 1
+) m2 WHERE m1.pkg_name = m2.pkg_name AND m1.ctid <> m2.ctid;
+
+ALTER TABLE oauth2_client ADD COLUMN IF NOT EXISTS tenant_id uuid DEFAULT '13814000-1dd2-11b2-8080-808080808080',
+ ADD COLUMN IF NOT EXISTS title varchar(100);
+UPDATE oauth2_client SET title = additional_info::jsonb->>'providerName' WHERE additional_info IS NOT NULL;
+
+CREATE TABLE IF NOT EXISTS domain_oauth2_client (
+ domain_id uuid NOT NULL,
+ oauth2_client_id uuid NOT NULL,
+ CONSTRAINT fk_domain FOREIGN KEY (domain_id) REFERENCES domain(id) ON DELETE CASCADE,
+ CONSTRAINT fk_oauth2_client FOREIGN KEY (oauth2_client_id) REFERENCES oauth2_client(id) ON DELETE CASCADE
+);
+
+CREATE TABLE IF NOT EXISTS mobile_app_oauth2_client (
+ mobile_app_id uuid NOT NULL,
+ oauth2_client_id uuid NOT NULL,
+ CONSTRAINT fk_domain FOREIGN KEY (mobile_app_id) REFERENCES mobile_app(id) ON DELETE CASCADE,
+ CONSTRAINT fk_oauth2_client FOREIGN KEY (oauth2_client_id) REFERENCES oauth2_client(id) ON DELETE CASCADE
+);
+
+-- migrate oauth2_params table
+DO
+$$
+ BEGIN
+ IF EXISTS(SELECT 1 FROM information_schema.tables WHERE table_name = 'oauth2_params') THEN
+ UPDATE domain SET oauth2_enabled = p.enabled,
+ edge_enabled = p.edge_enabled
+ FROM oauth2_params p WHERE p.id = domain.oauth2_params_id;
+
+ UPDATE mobile_app SET oauth2_enabled = p.enabled
+ FROM oauth2_params p WHERE p.id = mobile_app.oauth2_params_id;
+
+ INSERT INTO domain_oauth2_client(domain_id, oauth2_client_id)
+ (SELECT d.id, r.id FROM domain d LEFT JOIN oauth2_client r on d.oauth2_params_id = r.oauth2_params_id
+ WHERE r.platforms IS NULL OR r.platforms IN ('','WEB'));
+
+ INSERT INTO mobile_app_oauth2_client(mobile_app_id, oauth2_client_id)
+ (SELECT m.id, r.id FROM mobile_app m LEFT JOIN oauth2_client r on m.oauth2_params_id = r.oauth2_params_id
+ WHERE r.platforms IS NULL OR r.platforms IN ('','ANDROID','IOS'));
+
+ ALTER TABLE mobile_app RENAME CONSTRAINT oauth2_mobile_pkey TO mobile_app_pkey;
+ ALTER TABLE domain RENAME CONSTRAINT oauth2_domain_pkey TO domain_pkey;
+ ALTER TABLE oauth2_client RENAME CONSTRAINT oauth2_registration_pkey TO oauth2_client_pkey;
+
+ ALTER TABLE domain DROP COLUMN oauth2_params_id;
+ ALTER TABLE mobile_app DROP COLUMN oauth2_params_id;
+ ALTER TABLE oauth2_client DROP COLUMN oauth2_params_id;
+
+ ALTER TABLE mobile_app ADD CONSTRAINT mobile_app_unq_key UNIQUE (pkg_name);
+ ALTER TABLE domain ADD CONSTRAINT domain_unq_key UNIQUE (name);
+
+ DROP TABLE IF EXISTS oauth2_params;
+ -- drop deprecated tables
+ DROP TABLE IF EXISTS oauth2_client_registration_info;
+ DROP TABLE IF EXISTS oauth2_client_registration;
+ END IF;
+ END
+$$;
+
+-- OAUTH2 UPDATE END
\ No newline at end of file
diff --git a/application/src/main/java/org/springframework/http/converter/xml/MappingJackson2XmlHttpMessageConverter.java b/application/src/main/java/org/springframework/http/converter/xml/MappingJackson2XmlHttpMessageConverter.java
new file mode 100644
index 0000000000..158f29a03a
--- /dev/null
+++ b/application/src/main/java/org/springframework/http/converter/xml/MappingJackson2XmlHttpMessageConverter.java
@@ -0,0 +1,68 @@
+/**
+ * Copyright © 2016-2024 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.http.converter.xml;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.dataformat.xml.XmlMapper;
+import org.springframework.http.MediaType;
+import org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter;
+import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
+import org.springframework.util.Assert;
+
+import java.lang.reflect.Type;
+import java.nio.charset.StandardCharsets;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * RestTemplate firstly uses MappingJackson2XmlHttpMessageConverter converter instead of MappingJackson2HttpMessageConverter.
+ * It produces error UnsupportedMediaType, so this converter had to be shadowed for read and write operations to use the correct converter
+ */
+public class MappingJackson2XmlHttpMessageConverter extends AbstractJackson2HttpMessageConverter {
+ private static final List problemDetailMediaTypes;
+
+ public MappingJackson2XmlHttpMessageConverter() {
+ this(Jackson2ObjectMapperBuilder.xml().build());
+ }
+
+ public MappingJackson2XmlHttpMessageConverter(ObjectMapper objectMapper) {
+ super(objectMapper, new MediaType[]{new MediaType("application", "xml", StandardCharsets.UTF_8), new MediaType("text", "xml", StandardCharsets.UTF_8), new MediaType("application", "*+xml", StandardCharsets.UTF_8)});
+ Assert.isInstanceOf(XmlMapper.class, objectMapper, "XmlMapper required");
+ }
+
+ public void setObjectMapper(ObjectMapper objectMapper) {
+ Assert.isInstanceOf(XmlMapper.class, objectMapper, "XmlMapper required");
+ super.setObjectMapper(objectMapper);
+ }
+
+ protected List getMediaTypesForProblemDetail() {
+ return problemDetailMediaTypes;
+ }
+
+ static {
+ problemDetailMediaTypes = Collections.singletonList(MediaType.APPLICATION_PROBLEM_XML);
+ }
+
+ @Override
+ public boolean canRead(Type type, Class> contextClass, MediaType mediaType) {
+ return false;
+ }
+
+ @Override
+ public boolean canWrite(Class> clazz, MediaType mediaType) {
+ return false;
+ }
+}
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 b125daa591..5966716ed8 100644
--- a/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java
+++ b/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java
@@ -68,17 +68,20 @@ import org.thingsboard.server.dao.device.ClaimDevicesService;
import org.thingsboard.server.dao.device.DeviceCredentialsService;
import org.thingsboard.server.dao.device.DeviceProfileService;
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.entity.EntityService;
import org.thingsboard.server.dao.entityview.EntityViewService;
import org.thingsboard.server.dao.event.EventService;
+import org.thingsboard.server.dao.mobile.MobileAppService;
import org.thingsboard.server.dao.nosql.CassandraBufferedRateReadExecutor;
import org.thingsboard.server.dao.nosql.CassandraBufferedRateWriteExecutor;
import org.thingsboard.server.dao.notification.NotificationRequestService;
import org.thingsboard.server.dao.notification.NotificationRuleService;
import org.thingsboard.server.dao.notification.NotificationTargetService;
import org.thingsboard.server.dao.notification.NotificationTemplateService;
+import org.thingsboard.server.dao.oauth2.OAuth2ClientService;
import org.thingsboard.server.dao.ota.OtaPackageService;
import org.thingsboard.server.dao.queue.QueueService;
import org.thingsboard.server.dao.queue.QueueStatsService;
@@ -370,6 +373,18 @@ public class ActorSystemContext {
@Getter
private NotificationRuleService notificationRuleService;
+ @Autowired
+ @Getter
+ private OAuth2ClientService oAuth2ClientService;
+
+ @Autowired
+ @Getter
+ private DomainService domainService;
+
+ @Autowired
+ @Getter
+ private MobileAppService mobileAppService;
+
@Autowired
@Getter
private SlackService slackService;
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 93cf5bd8c7..622812b327 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
@@ -37,12 +37,10 @@ import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
import org.thingsboard.server.common.msg.MsgType;
import org.thingsboard.server.common.msg.TbActorMsg;
import org.thingsboard.server.common.msg.aware.TenantAwareMsg;
-import org.thingsboard.server.common.msg.edge.EdgeSessionMsg;
import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg;
import org.thingsboard.server.common.msg.queue.QueueToRuleEngineMsg;
import org.thingsboard.server.common.msg.queue.RuleEngineException;
import org.thingsboard.server.common.msg.queue.ServiceType;
-import org.thingsboard.server.dao.model.ModelConstants;
import org.thingsboard.server.dao.tenant.TenantService;
import org.thingsboard.server.service.transport.msg.TransportToDeviceActorMsgWrapper;
@@ -110,11 +108,6 @@ public class AppActor extends ContextAwareActor {
case REMOVE_RPC_TO_DEVICE_ACTOR_MSG:
onToDeviceActorMsg((TenantAwareMsg) msg, true);
break;
- case EDGE_EVENT_UPDATE_TO_EDGE_SESSION_MSG:
- case EDGE_SYNC_REQUEST_TO_EDGE_SESSION_MSG:
- case EDGE_SYNC_RESPONSE_FROM_EDGE_SESSION_MSG:
- onToEdgeSessionMsg((EdgeSessionMsg) msg);
- break;
case SESSION_TIMEOUT_MSG:
ctx.broadcastToChildrenByType(msg, EntityType.TENANT);
break;
@@ -206,20 +199,6 @@ public class AppActor extends ContextAwareActor {
() -> true));
}
- private void onToEdgeSessionMsg(EdgeSessionMsg msg) {
- TbActorRef target = null;
- if (ModelConstants.SYSTEM_TENANT.equals(msg.getTenantId())) {
- log.warn("Message has system tenant id: {}", msg);
- } else {
- target = getOrCreateTenantActor(msg.getTenantId()).orElse(null);
- }
- if (target != null) {
- target.tellWithHighPriority(msg);
- } else {
- log.debug("[{}] Invalid edge session msg: {}", msg.getTenantId(), msg);
- }
- }
-
@Override
public ProcessFailureStrategy onProcessFailure(TbActorMsg msg, Throwable t) {
log.error("Failed to process msg: {}", msg, t);
diff --git a/application/src/main/java/org/thingsboard/server/actors/device/DeviceActor.java b/application/src/main/java/org/thingsboard/server/actors/device/DeviceActor.java
index a2ac9d0cdc..4536cb6228 100644
--- a/application/src/main/java/org/thingsboard/server/actors/device/DeviceActor.java
+++ b/application/src/main/java/org/thingsboard/server/actors/device/DeviceActor.java
@@ -16,9 +16,6 @@
package org.thingsboard.server.actors.device;
import lombok.extern.slf4j.Slf4j;
-import org.thingsboard.server.common.msg.rule.engine.DeviceAttributesEventNotificationMsg;
-import org.thingsboard.server.common.msg.rule.engine.DeviceEdgeUpdateMsg;
-import org.thingsboard.server.common.msg.rule.engine.DeviceNameOrTypeUpdateMsg;
import org.thingsboard.server.actors.ActorSystemContext;
import org.thingsboard.server.actors.TbActorCtx;
import org.thingsboard.server.actors.TbActorException;
@@ -26,10 +23,13 @@ import org.thingsboard.server.actors.service.ContextAwareActor;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.msg.TbActorMsg;
-import org.thingsboard.server.common.msg.timeout.DeviceActorServerSideRpcTimeoutMsg;
import org.thingsboard.server.common.msg.rpc.FromDeviceRpcResponseActorMsg;
import org.thingsboard.server.common.msg.rpc.RemoveRpcActorMsg;
import org.thingsboard.server.common.msg.rpc.ToDeviceRpcRequestActorMsg;
+import org.thingsboard.server.common.msg.rule.engine.DeviceAttributesEventNotificationMsg;
+import org.thingsboard.server.common.msg.rule.engine.DeviceEdgeUpdateMsg;
+import org.thingsboard.server.common.msg.rule.engine.DeviceNameOrTypeUpdateMsg;
+import org.thingsboard.server.common.msg.timeout.DeviceActorServerSideRpcTimeoutMsg;
import org.thingsboard.server.service.transport.msg.TransportToDeviceActorMsgWrapper;
@Slf4j
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 7432038096..3e8cd9592f 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
@@ -56,6 +56,7 @@ import org.thingsboard.server.common.data.security.DeviceCredentials;
import org.thingsboard.server.common.data.security.DeviceCredentialsType;
import org.thingsboard.server.common.msg.TbActorMsg;
import org.thingsboard.server.common.msg.TbMsgMetaData;
+import org.thingsboard.server.common.msg.edge.EdgeHighPriorityMsg;
import org.thingsboard.server.common.msg.queue.TbCallback;
import org.thingsboard.server.common.msg.rpc.FromDeviceRpcResponse;
import org.thingsboard.server.common.msg.rpc.FromDeviceRpcResponseActorMsg;
@@ -74,6 +75,7 @@ import org.thingsboard.server.gen.transport.TransportProtos.DeviceSessionsCacheE
import org.thingsboard.server.gen.transport.TransportProtos.GetAttributeRequestMsg;
import org.thingsboard.server.gen.transport.TransportProtos.GetAttributeResponseMsg;
import org.thingsboard.server.gen.transport.TransportProtos.SessionCloseNotificationProto;
+import org.thingsboard.server.gen.transport.TransportProtos.SessionCloseReason;
import org.thingsboard.server.gen.transport.TransportProtos.SessionEvent;
import org.thingsboard.server.gen.transport.TransportProtos.SessionEventMsg;
import org.thingsboard.server.gen.transport.TransportProtos.SessionInfoProto;
@@ -90,7 +92,6 @@ import org.thingsboard.server.gen.transport.TransportProtos.ToTransportUpdateCre
import org.thingsboard.server.gen.transport.TransportProtos.TransportToDeviceActorMsg;
import org.thingsboard.server.gen.transport.TransportProtos.TsKvProto;
import org.thingsboard.server.gen.transport.TransportProtos.UplinkNotificationMsg;
-import org.thingsboard.server.gen.transport.TransportProtos.SessionCloseReason;
import org.thingsboard.server.service.rpc.RpcSubmitStrategy;
import org.thingsboard.server.service.state.DefaultDeviceStateService;
import org.thingsboard.server.service.transport.msg.TransportToDeviceActorMsgWrapper;
@@ -214,7 +215,7 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso
log.debug("[{}][{}] device is related to edge: [{}]. Saving RPC request: [{}][{}] to edge queue", tenantId, deviceId, edgeId.getId(), rpcId, requestId);
try {
if (systemContext.getEdgeService().isEdgeActiveAsync(tenantId, edgeId, DefaultDeviceStateService.ACTIVITY_STATE).get()) {
- saveRpcRequestToEdgeQueue(request, requestId).get();
+ saveRpcRequestToEdgeQueue(request, requestId);
} else {
log.error("[{}][{}][{}] Failed to save RPC request to edge queue {}. The Edge is currently offline or unreachable", tenantId, deviceId, edgeId.getId(), request);
}
@@ -921,7 +922,7 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso
systemContext.getTbCoreToTransportService().process(nodeId, msg);
}
- private ListenableFuture saveRpcRequestToEdgeQueue(ToDeviceRpcRequest msg, Integer requestId) {
+ private void saveRpcRequestToEdgeQueue(ToDeviceRpcRequest msg, Integer requestId) {
ObjectNode body = JacksonUtil.newObjectNode();
body.put("requestId", requestId);
body.put("requestUUID", msg.getId().toString());
@@ -935,7 +936,7 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso
EdgeEvent edgeEvent = EdgeUtils.constructEdgeEvent(tenantId, edgeId, EdgeEventType.DEVICE, EdgeEventActionType.RPC_CALL, deviceId, body);
- return systemContext.getEdgeEventService().saveAsync(edgeEvent);
+ systemContext.getClusterService().onEdgeHighPriorityMsg(new EdgeHighPriorityMsg(tenantId, edgeEvent));
}
void restoreSessions() {
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 0dc9d68984..ec10402821 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
@@ -83,17 +83,20 @@ import org.thingsboard.server.dao.dashboard.DashboardService;
import org.thingsboard.server.dao.device.DeviceCredentialsService;
import org.thingsboard.server.dao.device.DeviceProfileService;
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.entity.EntityService;
import org.thingsboard.server.dao.entityview.EntityViewService;
import org.thingsboard.server.dao.event.EventService;
+import org.thingsboard.server.dao.mobile.MobileAppService;
import org.thingsboard.server.dao.nosql.CassandraStatementTask;
import org.thingsboard.server.dao.nosql.TbResultSetFuture;
import org.thingsboard.server.dao.notification.NotificationRequestService;
import org.thingsboard.server.dao.notification.NotificationRuleService;
import org.thingsboard.server.dao.notification.NotificationTargetService;
import org.thingsboard.server.dao.notification.NotificationTemplateService;
+import org.thingsboard.server.dao.oauth2.OAuth2ClientService;
import org.thingsboard.server.dao.ota.OtaPackageService;
import org.thingsboard.server.dao.queue.QueueService;
import org.thingsboard.server.dao.queue.QueueStatsService;
@@ -106,10 +109,8 @@ import org.thingsboard.server.dao.user.UserService;
import org.thingsboard.server.dao.widget.WidgetTypeService;
import org.thingsboard.server.dao.widget.WidgetsBundleService;
import org.thingsboard.server.gen.transport.TransportProtos;
-import org.thingsboard.server.queue.TbQueueCallback;
-import org.thingsboard.server.queue.TbQueueMsgMetadata;
-import org.thingsboard.server.service.executors.PubSubRuleNodeExecutorProvider;
import org.thingsboard.server.queue.common.SimpleTbQueueCallback;
+import org.thingsboard.server.service.executors.PubSubRuleNodeExecutorProvider;
import org.thingsboard.server.service.script.RuleNodeJsScriptEngine;
import org.thingsboard.server.service.script.RuleNodeTbelScriptEngine;
@@ -827,6 +828,21 @@ class DefaultTbContext implements TbContext {
return mainCtx.getNotificationRuleService();
}
+ @Override
+ public OAuth2ClientService getOAuth2ClientService() {
+ return mainCtx.getOAuth2ClientService();
+ }
+
+ @Override
+ public DomainService getDomainService() {
+ return mainCtx.getDomainService();
+ }
+
+ @Override
+ public MobileAppService getMobileAppService() {
+ return mainCtx.getMobileAppService();
+ }
+
@Override
public SlackService getSlackService() {
return mainCtx.getSlackService();
diff --git a/application/src/main/java/org/thingsboard/server/actors/service/DefaultActorService.java b/application/src/main/java/org/thingsboard/server/actors/service/DefaultActorService.java
index e6dda31675..492940a274 100644
--- a/application/src/main/java/org/thingsboard/server/actors/service/DefaultActorService.java
+++ b/application/src/main/java/org/thingsboard/server/actors/service/DefaultActorService.java
@@ -15,6 +15,8 @@
*/
package org.thingsboard.server.actors.service;
+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;
@@ -36,8 +38,6 @@ import org.thingsboard.server.queue.discovery.TbApplicationEventListener;
import org.thingsboard.server.queue.discovery.event.PartitionChangeEvent;
import org.thingsboard.server.queue.util.AfterStartUp;
-import jakarta.annotation.PostConstruct;
-import jakarta.annotation.PreDestroy;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
diff --git a/application/src/main/java/org/thingsboard/server/actors/shared/ComponentMsgProcessor.java b/application/src/main/java/org/thingsboard/server/actors/shared/ComponentMsgProcessor.java
index 86e9fc6982..298874cc51 100644
--- a/application/src/main/java/org/thingsboard/server/actors/shared/ComponentMsgProcessor.java
+++ b/application/src/main/java/org/thingsboard/server/actors/shared/ComponentMsgProcessor.java
@@ -78,7 +78,7 @@ public abstract class ComponentMsgProcessor extends Abstract
}
public void scheduleStatsPersistTick(TbActorCtx context, long statsPersistFrequency) {
- schedulePeriodicMsgWithDelay(context, new StatsPersistTick(), statsPersistFrequency, statsPersistFrequency);
+ schedulePeriodicMsgWithDelay(context, StatsPersistTick.INSTANCE, statsPersistFrequency, statsPersistFrequency);
}
protected boolean checkMsgValid(TbMsg tbMsg) {
diff --git a/application/src/main/java/org/thingsboard/server/actors/stats/StatsPersistTick.java b/application/src/main/java/org/thingsboard/server/actors/stats/StatsPersistTick.java
index 541da7494f..25271262b5 100644
--- a/application/src/main/java/org/thingsboard/server/actors/stats/StatsPersistTick.java
+++ b/application/src/main/java/org/thingsboard/server/actors/stats/StatsPersistTick.java
@@ -18,7 +18,9 @@ package org.thingsboard.server.actors.stats;
import org.thingsboard.server.common.msg.MsgType;
import org.thingsboard.server.common.msg.TbActorMsg;
-public final class StatsPersistTick implements TbActorMsg {
+public enum StatsPersistTick implements TbActorMsg {
+ INSTANCE;
+
@Override
public MsgType getMsgType() {
return MsgType.STATS_PERSIST_TICK_MSG;
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 d55e08143b..5edecba31a 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
@@ -33,9 +33,7 @@ import org.thingsboard.server.actors.service.DefaultActorService;
import org.thingsboard.server.common.data.ApiUsageState;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.Tenant;
-import org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.common.data.id.DeviceId;
-import org.thingsboard.server.common.data.id.EdgeId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.RuleChainId;
import org.thingsboard.server.common.data.id.TenantId;
@@ -48,14 +46,12 @@ import org.thingsboard.server.common.msg.TbActorStopReason;
import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.aware.DeviceAwareMsg;
import org.thingsboard.server.common.msg.aware.RuleChainAwareMsg;
-import org.thingsboard.server.common.msg.edge.EdgeSessionMsg;
import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg;
import org.thingsboard.server.common.msg.queue.PartitionChangeMsg;
import org.thingsboard.server.common.msg.queue.QueueToRuleEngineMsg;
import org.thingsboard.server.common.msg.queue.RuleEngineException;
import org.thingsboard.server.common.msg.queue.ServiceType;
import org.thingsboard.server.common.msg.rule.engine.DeviceDeleteMsg;
-import org.thingsboard.server.service.edge.rpc.EdgeRpcService;
import org.thingsboard.server.service.transport.msg.TransportToDeviceActorMsgWrapper;
import java.util.HashSet;
@@ -163,11 +159,6 @@ public class TenantActor extends RuleChainManagerActor {
case RULE_CHAIN_TO_RULE_CHAIN_MSG:
onRuleChainMsg((RuleChainAwareMsg) msg);
break;
- case EDGE_EVENT_UPDATE_TO_EDGE_SESSION_MSG:
- case EDGE_SYNC_REQUEST_TO_EDGE_SESSION_MSG:
- case EDGE_SYNC_RESPONSE_FROM_EDGE_SESSION_MSG:
- onToEdgeSessionMsg((EdgeSessionMsg) msg);
- break;
default:
return false;
}
@@ -270,18 +261,6 @@ public class TenantActor extends RuleChainManagerActor {
initRuleChains();
}
}
- if (msg.getEntityId().getEntityType() == EntityType.EDGE && isCore) {
- EdgeId edgeId = new EdgeId(msg.getEntityId().getId());
- EdgeRpcService edgeRpcService = systemContext.getEdgeRpcService();
- if (edgeRpcService != null) {
- if (msg.getEvent() == ComponentLifecycleEvent.DELETED) {
- edgeRpcService.deleteEdge(tenantId, edgeId);
- } else if (msg.getEvent() == ComponentLifecycleEvent.UPDATED) {
- Edge edge = systemContext.getEdgeService().findEdgeById(tenantId, edgeId);
- edgeRpcService.updateEdge(tenantId, edge);
- }
- }
- }
if (msg.getEntityId().getEntityType() == EntityType.DEVICE && ComponentLifecycleEvent.DELETED == msg.getEvent() && isMyPartition(msg.getEntityId())) {
DeviceId deviceId = (DeviceId) msg.getEntityId();
onToDeviceActorMsg(new DeviceDeleteMsg(tenantId, deviceId), true);
@@ -311,10 +290,6 @@ public class TenantActor extends RuleChainManagerActor {
() -> true);
}
- private void onToEdgeSessionMsg(EdgeSessionMsg msg) {
- systemContext.getEdgeRpcService().onToEdgeSessionMsg(tenantId, msg);
- }
-
private ApiUsageState getApiUsageState() {
if (apiUsageState == null) {
apiUsageState = new ApiUsageState(systemContext.getApiUsageStateService().getApiUsageState(tenantId));
diff --git a/application/src/main/java/org/thingsboard/server/config/CustomOAuth2AuthorizationRequestResolver.java b/application/src/main/java/org/thingsboard/server/config/CustomOAuth2AuthorizationRequestResolver.java
index 1784c6fa1b..542a7c71cb 100644
--- a/application/src/main/java/org/thingsboard/server/config/CustomOAuth2AuthorizationRequestResolver.java
+++ b/application/src/main/java/org/thingsboard/server/config/CustomOAuth2AuthorizationRequestResolver.java
@@ -37,8 +37,9 @@ import org.springframework.util.CollectionUtils;
import org.springframework.web.util.UriComponents;
import org.springframework.web.util.UriComponentsBuilder;
import org.thingsboard.server.common.data.StringUtils;
+import org.thingsboard.server.common.data.id.OAuth2ClientId;
import org.thingsboard.server.dao.oauth2.OAuth2Configuration;
-import org.thingsboard.server.dao.oauth2.OAuth2Service;
+import org.thingsboard.server.dao.oauth2.OAuth2ClientService;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.security.auth.oauth2.TbOAuth2ParameterNames;
import org.thingsboard.server.service.security.model.token.OAuth2AppTokenFactory;
@@ -70,7 +71,7 @@ public class CustomOAuth2AuthorizationRequestResolver implements OAuth2Authoriza
private ClientRegistrationRepository clientRegistrationRepository;
@Autowired
- private OAuth2Service oAuth2Service;
+ private OAuth2ClientService oAuth2ClientService;
@Autowired
private OAuth2AppTokenFactory oAuth2AppTokenFactory;
@@ -115,14 +116,14 @@ public class CustomOAuth2AuthorizationRequestResolver implements OAuth2Authoriza
return request.getParameter("appToken");
}
- private OAuth2AuthorizationRequest resolve(HttpServletRequest request, String registrationId, String redirectUriAction, String appPackage, String appToken) {
- if (registrationId == null) {
+ private OAuth2AuthorizationRequest resolve(HttpServletRequest request, String oauth2ClientId, String redirectUriAction, String appPackage, String appToken) {
+ if (oauth2ClientId == null) {
return null;
}
- ClientRegistration clientRegistration = this.clientRegistrationRepository.findByRegistrationId(registrationId);
+ ClientRegistration clientRegistration = this.clientRegistrationRepository.findByRegistrationId(oauth2ClientId);
if (clientRegistration == null) {
- throw new IllegalArgumentException("Invalid Client Registration with Id: " + registrationId);
+ throw new IllegalArgumentException("Invalid Client Registration with Id: " + oauth2ClientId);
}
Map attributes = new HashMap<>();
@@ -131,7 +132,7 @@ public class CustomOAuth2AuthorizationRequestResolver implements OAuth2Authoriza
if (StringUtils.isEmpty(appToken)) {
throw new IllegalArgumentException("Invalid application token.");
} else {
- String appSecret = this.oAuth2Service.findAppSecret(UUID.fromString(registrationId), appPackage);
+ String appSecret = this.oAuth2ClientService.findAppSecret(new OAuth2ClientId(UUID.fromString(oauth2ClientId)), appPackage);
if (StringUtils.isEmpty(appSecret)) {
throw new IllegalArgumentException("Invalid package: " + appPackage + ". No application secret found for Client Registration with given application package.");
}
diff --git a/application/src/main/java/org/thingsboard/server/config/RateLimitProcessingFilter.java b/application/src/main/java/org/thingsboard/server/config/RateLimitProcessingFilter.java
index 73209c1ed6..2aebc8bd06 100644
--- a/application/src/main/java/org/thingsboard/server/config/RateLimitProcessingFilter.java
+++ b/application/src/main/java/org/thingsboard/server/config/RateLimitProcessingFilter.java
@@ -15,6 +15,10 @@
*/
package org.thingsboard.server.config;
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.BadCredentialsException;
@@ -22,18 +26,14 @@ import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
+import org.thingsboard.server.cache.limits.RateLimitService;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.exception.TenantProfileNotFoundException;
import org.thingsboard.server.common.data.limit.LimitedApi;
import org.thingsboard.server.common.msg.tools.TbRateLimitsException;
-import org.thingsboard.server.cache.limits.RateLimitService;
import org.thingsboard.server.exception.ThingsboardErrorResponseHandler;
import org.thingsboard.server.service.security.model.SecurityUser;
-import jakarta.servlet.FilterChain;
-import jakarta.servlet.ServletException;
-import jakarta.servlet.http.HttpServletRequest;
-import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
@Slf4j
diff --git a/application/src/main/java/org/thingsboard/server/config/RequestSizeFilter.java b/application/src/main/java/org/thingsboard/server/config/RequestSizeFilter.java
deleted file mode 100644
index c2be2ed027..0000000000
--- a/application/src/main/java/org/thingsboard/server/config/RequestSizeFilter.java
+++ /dev/null
@@ -1,78 +0,0 @@
-/**
- * Copyright © 2016-2024 The Thingsboard Authors
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.thingsboard.server.config;
-
-import jakarta.servlet.FilterChain;
-import jakarta.servlet.ServletException;
-import jakarta.servlet.http.HttpServletRequest;
-import jakarta.servlet.http.HttpServletResponse;
-import lombok.RequiredArgsConstructor;
-import lombok.extern.slf4j.Slf4j;
-import org.springframework.beans.factory.annotation.Value;
-import org.springframework.stereotype.Component;
-import org.springframework.util.AntPathMatcher;
-import org.springframework.web.filter.OncePerRequestFilter;
-import org.thingsboard.server.common.msg.tools.MaxPayloadSizeExceededException;
-import org.thingsboard.server.exception.ThingsboardErrorResponseHandler;
-
-import java.io.IOException;
-import java.util.List;
-
-@Slf4j
-@Component
-@RequiredArgsConstructor
-public class RequestSizeFilter extends OncePerRequestFilter {
-
- private final List urls = List.of("/api/plugins/rpc/**", "/api/rpc/**");
- private final AntPathMatcher pathMatcher = new AntPathMatcher();
- private final ThingsboardErrorResponseHandler errorResponseHandler;
-
- @Value("${transport.http.max_payload_size:65536}")
- private int maxPayloadSize;
-
- @Override
- public void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
- if (request.getContentLength() > maxPayloadSize) {
- if (log.isDebugEnabled()) {
- log.debug("Too large payload size. Url: {}, client ip: {}, content length: {}", request.getRequestURL(),
- request.getRemoteAddr(), request.getContentLength());
- }
- errorResponseHandler.handle(new MaxPayloadSizeExceededException(), response);
- return;
- }
- chain.doFilter(request, response);
- }
-
- @Override
- protected boolean shouldNotFilter(HttpServletRequest request) {
- for (String url : urls) {
- if (pathMatcher.match(url, request.getRequestURI())) {
- return false;
- }
- }
- return true;
- }
-
- @Override
- protected boolean shouldNotFilterAsyncDispatch() {
- return false;
- }
-
- @Override
- protected boolean shouldNotFilterErrorDispatch() {
- return false;
- }
-}
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 77ad1a1b9e..55b89b05f4 100644
--- a/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java
+++ b/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java
@@ -17,6 +17,7 @@ package org.thingsboard.server.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.security.SecurityProperties;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
@@ -56,6 +57,7 @@ import org.thingsboard.server.service.security.auth.oauth2.HttpCookieOAuth2Autho
import org.thingsboard.server.service.security.auth.rest.RestAuthenticationProvider;
import org.thingsboard.server.service.security.auth.rest.RestLoginProcessingFilter;
import org.thingsboard.server.service.security.auth.rest.RestPublicLoginProcessingFilter;
+import org.thingsboard.server.transport.http.config.PayloadSizeFilter;
import java.util.ArrayList;
import java.util.Arrays;
@@ -82,6 +84,9 @@ public class ThingsboardSecurityConfiguration {
public static final String MAIL_OAUTH2_PROCESSING_ENTRY_POINT = "/api/admin/mail/oauth2/code";
public static final String DEVICE_CONNECTIVITY_CERTIFICATE_DOWNLOAD_ENTRY_POINT = "/api/device-connectivity/mqtts/certificate/download";
+ @Value("${server.http.max_payload_size:/api/image*/**=52428800;/api/resource/**=52428800;/api/**=16777216}")
+ private String maxPayloadSizeConfig;
+
@Autowired
private ThingsboardErrorResponseHandler restAccessDeniedHandler;
@@ -124,8 +129,10 @@ public class ThingsboardSecurityConfiguration {
@Autowired
private RateLimitProcessingFilter rateLimitProcessingFilter;
- @Autowired
- private RequestSizeFilter requestSizeFilter;
+ @Bean
+ protected PayloadSizeFilter payloadSizeFilter() {
+ return new PayloadSizeFilter(maxPayloadSizeConfig);
+ }
@Bean
protected FilterRegistrationBean buildEtagFilter() throws Exception {
@@ -214,7 +221,6 @@ public class ThingsboardSecurityConfiguration {
.authorizeHttpRequests(config -> config
.requestMatchers(NON_TOKEN_BASED_AUTH_ENTRY_POINTS).permitAll() // static resources, user activation and password reset end-points (webjars included)
.requestMatchers(
- DEVICE_API_ENTRY_POINT, // Device HTTP Transport API
FORM_BASED_LOGIN_ENTRY_POINT, // Login end-point
PUBLIC_LOGIN_ENTRY_POINT, // Public login end-point
TOKEN_REFRESH_ENTRY_POINT, // Token refresh end-point
@@ -228,7 +234,7 @@ public class ThingsboardSecurityConfiguration {
.addFilterBefore(buildRestPublicLoginProcessingFilter(), UsernamePasswordAuthenticationFilter.class)
.addFilterBefore(buildJwtTokenAuthenticationProcessingFilter(), UsernamePasswordAuthenticationFilter.class)
.addFilterBefore(buildRefreshTokenProcessingFilter(), UsernamePasswordAuthenticationFilter.class)
- .addFilterBefore(requestSizeFilter, UsernamePasswordAuthenticationFilter.class)
+ .addFilterBefore(payloadSizeFilter(), UsernamePasswordAuthenticationFilter.class)
.addFilterAfter(rateLimitProcessingFilter, UsernamePasswordAuthenticationFilter.class);
if (oauth2Configuration != null) {
http.oauth2Login(login -> login
diff --git a/application/src/main/java/org/thingsboard/server/config/WebConfig.java b/application/src/main/java/org/thingsboard/server/config/WebConfig.java
index 70afc8b99c..5d4830c552 100644
--- a/application/src/main/java/org/thingsboard/server/config/WebConfig.java
+++ b/application/src/main/java/org/thingsboard/server/config/WebConfig.java
@@ -15,12 +15,12 @@
*/
package org.thingsboard.server.config;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.thingsboard.server.utils.MiscUtils;
-import jakarta.servlet.http.HttpServletRequest;
-import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
@Controller
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 65903a4dec..e4e6418168 100644
--- a/application/src/main/java/org/thingsboard/server/controller/AdminController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/AdminController.java
@@ -137,7 +137,7 @@ public class AdminController extends BaseController {
return adminSettings;
}
- @ApiOperation(value = "Get the Administration Settings object using key (getAdminSettings)",
+ @ApiOperation(value = "Creates or Updates the Administration Settings (saveAdminSettings)",
notes = "Creates or Updates the Administration Settings. Platform generates random Administration Settings Id during settings creation. " +
"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)
@@ -160,7 +160,7 @@ public class AdminController extends BaseController {
return adminSettings;
}
- @ApiOperation(value = "Get the Security Settings object",
+ @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)
@@ -237,7 +237,7 @@ public class AdminController extends BaseController {
}
}
- @ApiOperation(value = "Send test sms (sendTestMail)",
+ @ApiOperation(value = "Send test sms (sendTestSms)",
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')")
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 15918f0a08..5865c96d33 100644
--- a/application/src/main/java/org/thingsboard/server/controller/AlarmCommentController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/AlarmCommentController.java
@@ -16,11 +16,8 @@
package org.thingsboard.server.controller;
import io.swagger.v3.oas.annotations.Parameter;
-import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
-import io.swagger.v3.oas.annotations.responses.ApiResponse;
import lombok.RequiredArgsConstructor;
-import org.springframework.http.MediaType;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
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 535700335d..9b786a8d3e 100644
--- a/application/src/main/java/org/thingsboard/server/controller/AlarmController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/AlarmController.java
@@ -17,12 +17,9 @@ package org.thingsboard.server.controller;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.ArraySchema;
-import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
-import io.swagger.v3.oas.annotations.responses.ApiResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
-import org.springframework.http.MediaType;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
diff --git a/application/src/main/java/org/thingsboard/server/controller/AssetController.java b/application/src/main/java/org/thingsboard/server/controller/AssetController.java
index 7b4621db28..c54f5b0013 100644
--- a/application/src/main/java/org/thingsboard/server/controller/AssetController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/AssetController.java
@@ -18,13 +18,10 @@ package org.thingsboard.server.controller;
import com.google.common.util.concurrent.ListenableFuture;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.ArraySchema;
-import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
-import io.swagger.v3.oas.annotations.responses.ApiResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
-import org.springframework.http.MediaType;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
diff --git a/application/src/main/java/org/thingsboard/server/controller/AssetProfileController.java b/application/src/main/java/org/thingsboard/server/controller/AssetProfileController.java
index a4dc7a1cb0..4b40cf3854 100644
--- a/application/src/main/java/org/thingsboard/server/controller/AssetProfileController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/AssetProfileController.java
@@ -16,13 +16,10 @@
package org.thingsboard.server.controller;
import io.swagger.v3.oas.annotations.Parameter;
-import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
-import io.swagger.v3.oas.annotations.responses.ApiResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
-import org.springframework.http.MediaType;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
@@ -40,8 +37,8 @@ import org.thingsboard.server.common.data.id.AssetProfileId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
-import org.thingsboard.server.dao.resource.ImageService;
import org.thingsboard.server.config.annotations.ApiOperation;
+import org.thingsboard.server.dao.resource.ImageService;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.entitiy.asset.profile.TbAssetProfileService;
import org.thingsboard.server.service.security.model.SecurityUser;
diff --git a/application/src/main/java/org/thingsboard/server/controller/AuditLogController.java b/application/src/main/java/org/thingsboard/server/controller/AuditLogController.java
index c8ea4073b1..3db2813086 100644
--- a/application/src/main/java/org/thingsboard/server/controller/AuditLogController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/AuditLogController.java
@@ -16,10 +16,7 @@
package org.thingsboard.server.controller;
import io.swagger.v3.oas.annotations.Parameter;
-import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
-import io.swagger.v3.oas.annotations.responses.ApiResponse;
-import org.springframework.http.MediaType;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
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 b723603e8a..8021e71fd6 100644
--- a/application/src/main/java/org/thingsboard/server/controller/BaseController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/BaseController.java
@@ -63,8 +63,10 @@ import org.thingsboard.server.common.data.asset.Asset;
import org.thingsboard.server.common.data.asset.AssetInfo;
import org.thingsboard.server.common.data.asset.AssetProfile;
import org.thingsboard.server.common.data.audit.ActionType;
+import org.thingsboard.server.common.data.domain.Domain;
import org.thingsboard.server.common.data.edge.Edge;
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.AlarmCommentId;
@@ -75,11 +77,14 @@ import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.DashboardId;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.DeviceProfileId;
+import org.thingsboard.server.common.data.id.DomainId;
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.EntityViewId;
import org.thingsboard.server.common.data.id.HasId;
+import org.thingsboard.server.common.data.id.MobileAppId;
+import org.thingsboard.server.common.data.id.OAuth2ClientId;
import org.thingsboard.server.common.data.id.OtaPackageId;
import org.thingsboard.server.common.data.id.QueueId;
import org.thingsboard.server.common.data.id.RpcId;
@@ -92,6 +97,8 @@ import org.thingsboard.server.common.data.id.UUIDBased;
import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.id.WidgetTypeId;
import org.thingsboard.server.common.data.id.WidgetsBundleId;
+import org.thingsboard.server.common.data.mobile.MobileApp;
+import org.thingsboard.server.common.data.oauth2.OAuth2Client;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.page.SortOrder;
import org.thingsboard.server.common.data.page.TimePageLink;
@@ -118,13 +125,15 @@ import org.thingsboard.server.dao.device.ClaimDevicesService;
import org.thingsboard.server.dao.device.DeviceCredentialsService;
import org.thingsboard.server.dao.device.DeviceProfileService;
import org.thingsboard.server.dao.device.DeviceService;
+import org.thingsboard.server.dao.domain.DomainService;
import org.thingsboard.server.dao.edge.EdgeService;
import org.thingsboard.server.dao.entityview.EntityViewService;
import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.dao.exception.IncorrectParameterException;
+import org.thingsboard.server.dao.mobile.MobileAppService;
import org.thingsboard.server.dao.model.ModelConstants;
import org.thingsboard.server.dao.oauth2.OAuth2ConfigTemplateService;
-import org.thingsboard.server.dao.oauth2.OAuth2Service;
+import org.thingsboard.server.dao.oauth2.OAuth2ClientService;
import org.thingsboard.server.dao.ota.OtaPackageService;
import org.thingsboard.server.dao.queue.QueueService;
import org.thingsboard.server.dao.relation.RelationService;
@@ -161,6 +170,9 @@ import org.thingsboard.server.service.sync.vc.EntitiesVersionControlService;
import org.thingsboard.server.service.telemetry.AlarmSubscriptionService;
import org.thingsboard.server.service.telemetry.TelemetrySubscriptionService;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
@@ -235,7 +247,13 @@ public abstract class BaseController {
protected DashboardService dashboardService;
@Autowired
- protected OAuth2Service oAuth2Service;
+ protected OAuth2ClientService oAuth2ClientService;
+
+ @Autowired
+ protected DomainService domainService;
+
+ @Autowired
+ protected MobileAppService mobileAppService;
@Autowired
protected OAuth2ConfigTemplateService oAuth2ConfigTemplateService;
@@ -390,6 +408,8 @@ public abstract class BaseController {
} else {
return new ThingsboardException("Database error", ThingsboardErrorCode.GENERAL);
}
+ } else if (exception instanceof EntityVersionMismatchException) {
+ return new ThingsboardException(exception.getMessage(), exception, ThingsboardErrorCode.VERSION_CONFLICT);
}
return new ThingsboardException(exception.getMessage(), exception, ThingsboardErrorCode.GENERAL);
}
@@ -609,6 +629,15 @@ public abstract class BaseController {
case QUEUE:
checkQueueId(new QueueId(entityId.getId()), operation);
return;
+ case OAUTH2_CLIENT:
+ checkOauth2ClientId(new OAuth2ClientId(entityId.getId()), operation);
+ return;
+ case DOMAIN:
+ checkDomainId(new DomainId(entityId.getId()), operation);
+ return;
+ case MOBILE_APP:
+ checkMobileAppId(new MobileAppId(entityId.getId()), operation);
+ return;
default:
checkEntityId(entityId, entitiesService::findEntityByTenantIdAndId, operation);
}
@@ -785,6 +814,18 @@ public abstract class BaseController {
return queue;
}
+ OAuth2Client checkOauth2ClientId(OAuth2ClientId oAuth2ClientId, Operation operation) throws ThingsboardException {
+ return checkEntityId(oAuth2ClientId, oAuth2ClientService::findOAuth2ClientById, operation);
+ }
+
+ Domain checkDomainId(DomainId domainId, Operation operation) throws ThingsboardException {
+ return checkEntityId(domainId, domainService::findDomainById, operation);
+ }
+
+ MobileApp checkMobileAppId(MobileAppId mobileAppId, Operation operation) throws ThingsboardException {
+ return checkEntityId(mobileAppId, mobileAppService::findMobileAppById, operation);
+ }
+
protected I emptyId(EntityType entityType) {
return (I) EntityIdFactory.getByTypeAndUuid(entityType, ModelConstants.NULL_UUID);
}
@@ -871,4 +912,17 @@ public abstract class BaseController {
}
}
+ protected List getOAuth2ClientIds(UUID[] ids) throws ThingsboardException {
+ if (ids == null) {
+ return Collections.emptyList();
+ }
+ List oAuth2ClientIds = new ArrayList<>();
+ for (UUID id : ids) {
+ OAuth2ClientId oauth2ClientId = new OAuth2ClientId(id);
+ checkOauth2ClientId(oauth2ClientId, Operation.READ);
+ oAuth2ClientIds.add(oauth2ClientId);
+ }
+ return oAuth2ClientIds;
+ }
+
}
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 e4b919a616..4d83d4debb 100644
--- a/application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java
+++ b/application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java
@@ -117,6 +117,8 @@ public class ControllerConstants {
protected static final String RESOURCE_INFO_DESCRIPTION = "Resource Info is a lightweight object that includes main information about the Resource excluding the heavyweight data. ";
protected static final String RESOURCE_DESCRIPTION = "Resource is a heavyweight object that includes main information about the Resource and also data. ";
+ protected static final String RESOURCE_IMAGE_SUB_TYPE_DESCRIPTION = "A string value representing resource sub-type.";
+
protected static final String RESOURCE_INCLUDE_SYSTEM_IMAGES_DESCRIPTION = "Use 'true' to include system images. Disabled by default. Ignored for requests by users with system administrator authority.";
protected static final String RESOURCE_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'substring' filter based on the resource title.";
@@ -791,7 +793,7 @@ public class ControllerConstants {
" * 'SHARED_ATTRIBUTE' - used for shared attributes; \n" +
" * 'SERVER_ATTRIBUTE' - used for server attributes; \n" +
" * 'ATTRIBUTE' - used for any of the above; \n" +
- " * 'TIME_SERIES' - used for time-series values; \n" +
+ " * 'TIME_SERIES' - used for time series values; \n" +
" * 'ENTITY_FIELD' - used for accessing entity fields like 'name', 'label', etc. The list of available fields depends on the entity type; \n" +
" * 'ALARM_FIELD' - similar to entity field, but is used in alarm queries only; \n" +
"\n\n Let's review the example:\n\n" +
@@ -902,7 +904,7 @@ public class ControllerConstants {
protected static final String KEY_FILTERS =
"\n\n # Key Filters" +
- "\nKey Filter allows you to define complex logical expressions over entity field, attribute or latest time-series value. The filter is defined using 'key', 'valueType' and 'predicate' objects. " +
+ "\nKey Filter allows you to define complex logical expressions over entity field, attribute or latest time series value. The filter is defined using 'key', 'valueType' and 'predicate' objects. " +
"Single Entity Query may have zero, one or multiple predicates. If multiple filters are defined, they are evaluated using logical 'AND'. " +
"The example below checks that temperature of the entity is above 20 degrees:" +
"\n\n" + MARKDOWN_CODE_BLOCK_START +
@@ -933,7 +935,7 @@ public class ControllerConstants {
"For example, \"find all devices with profile 'Moisture Sensor'\" or \"Find all devices related to asset 'Building A'\"" +
"\n\nOptional **key filters** allow to filter results of the entity filter by complex criteria against " +
"main entity fields (name, label, type, etc), attributes and telemetry. " +
- "For example, \"temperature > 20 or temperature< 10\" or \"name starts with 'T', and attribute 'model' is 'T1000', and timeseries field 'batteryLevel' > 40\"." +
+ "For example, \"temperature > 20 or temperature< 10\" or \"name starts with 'T', and attribute 'model' is 'T1000', and time series field 'batteryLevel' > 40\"." +
"\n\nLet's review the example:" +
"\n\n" + MARKDOWN_CODE_BLOCK_START +
"{\n" +
@@ -968,13 +970,13 @@ public class ControllerConstants {
protected static final String ENTITY_DATA_QUERY_DESCRIPTION =
"Allows to run complex queries over platform entities (devices, assets, customers, etc) " +
"based on the combination of main entity filter and multiple key filters. " +
- "Returns the paginated result of the query that contains requested entity fields and latest values of requested attributes and time-series data.\n\n" +
+ "Returns the paginated result of the query that contains requested entity fields and latest values of requested attributes and time series data.\n\n" +
"# Query Definition\n\n" +
"\n\nMain **entity filter** is mandatory and defines generic search criteria. " +
"For example, \"find all devices with profile 'Moisture Sensor'\" or \"Find all devices related to asset 'Building A'\"" +
"\n\nOptional **key filters** allow to filter results of the **entity filter** by complex criteria against " +
"main entity fields (name, label, type, etc), attributes and telemetry. " +
- "For example, \"temperature > 20 or temperature< 10\" or \"name starts with 'T', and attribute 'model' is 'T1000', and timeseries field 'batteryLevel' > 40\"." +
+ "For example, \"temperature > 20 or temperature< 10\" or \"name starts with 'T', and attribute 'model' is 'T1000', and time series field 'batteryLevel' > 40\"." +
"\n\nThe **entity fields** and **latest values** contains list of entity fields and latest attribute/telemetry fields to fetch for each entity." +
"\n\nThe **page link** contains information about the page to fetch and the sort ordering." +
"\n\nLet's review the example:" +
@@ -1052,7 +1054,7 @@ public class ControllerConstants {
protected static final String ALARM_DATA_QUERY_DESCRIPTION = "This method description defines how Alarm Data Query extends the Entity Data Query. " +
"See method 'Find Entity Data by Query' first to get the info about 'Entity Data Query'." +
"\n\n The platform will first search the entities that match the entity and key filters. Then, the platform will use 'Alarm Page Link' to filter the alarms related to those entities. " +
- "Finally, platform fetch the properties of alarm that are defined in the **'alarmFields'** and combine them with the other entity, attribute and latest time-series fields to return the result. " +
+ "Finally, platform fetch the properties of alarm that are defined in the **'alarmFields'** and combine them with the other entity, attribute and latest time series fields to return the result. " +
"\n\n See example of the alarm query below. The query will search first 100 active alarms with type 'Temperature Alarm' or 'Fire Alarm' for any device with current temperature > 0. " +
"The query will return combination of the entity fields: name of the device, device model and latest temperature reading and alarms fields: createdTime, type, severity and status: " +
"\n\n" + MARKDOWN_CODE_BLOCK_START +
@@ -1173,7 +1175,7 @@ public class ControllerConstants {
protected static final String ALARM_FILTER_KEY = "## Alarm Filter Key" + NEW_LINE +
"Filter Key defines either entity field, attribute, telemetry or constant. It is a JSON object that consists the key name and type. The following filter key types are supported:\n" +
" * 'ATTRIBUTE' - used for attributes values;\n" +
- " * 'TIME_SERIES' - used for time-series values;\n" +
+ " * 'TIME_SERIES' - used for time series values;\n" +
" * 'ENTITY_FIELD' - used for accessing entity fields like 'name', 'label', etc. The list of available fields depends on the entity type;\n" +
" * 'CONSTANT' - constant value specified." + NEW_LINE + "Let's review the example:" + NEW_LINE +
MARKDOWN_CODE_BLOCK_START +
@@ -1291,7 +1293,7 @@ public class ControllerConstants {
protected static final String KEY_FILTERS_DESCRIPTION = "# Key Filters" + NEW_LINE +
"Key filter objects are created under the **'condition'** array. They allow you to define complex logical expressions over entity field, " +
- "attribute, latest time-series value or constant. The filter is defined using 'key', 'valueType', " +
+ "attribute, latest time series value or constant. The filter is defined using 'key', 'valueType', " +
"'value' (refers to the value of the 'CONSTANT' alarm filter key type) and 'predicate' objects. Let's review each object:" + NEW_LINE +
ALARM_FILTER_KEY + FILTER_VALUE_TYPE + NEW_LINE + DEVICE_PROFILE_FILTER_PREDICATE + NEW_LINE;
@@ -1604,7 +1606,7 @@ public class ControllerConstants {
protected static final String ATTRIBUTES_JSON_REQUEST_DESCRIPTION = "A string value representing the json object. For example, '{\"key\":\"value\"}'. See API call description for more details.";
protected static final String TELEMETRY_KEYS_BASE_DESCRIPTION = "A string value representing the comma-separated list of telemetry keys.";
- protected static final String TELEMETRY_KEYS_DESCRIPTION = TELEMETRY_KEYS_BASE_DESCRIPTION + " If keys are not selected, the result will return all latest timeseries. For example, 'temperature,humidity'.";
+ protected static final String TELEMETRY_KEYS_DESCRIPTION = TELEMETRY_KEYS_BASE_DESCRIPTION + " If keys are not selected, the result will return all latest time series. For example, 'temperature,humidity'.";
protected static final String TELEMETRY_SCOPE_DESCRIPTION = "Value is deprecated, reserved for backward compatibility and not used in the API call implementation. Specify any scope for compatibility";
protected static final String TELEMETRY_JSON_REQUEST_DESCRIPTION = "A JSON with the telemetry values. See API call description for more details.";
@@ -1620,11 +1622,11 @@ public class ControllerConstants {
protected static final String SAVE_ENTITY_ATTRIBUTES_STATUS_UNAUTHORIZED = "User is not authorized to save entity attributes for selected entity. Most likely, User belongs to different Customer or Tenant.";
protected static final String SAVE_ENTITY_ATTRIBUTES_STATUS_INTERNAL_SERVER_ERROR = "The exception was thrown during processing the request. " +
"Platform creates an audit log event about entity attributes updates with action type 'ATTRIBUTES_UPDATED' that includes an error stacktrace.";
- protected static final String SAVE_ENTITY_TIMESERIES_STATUS_OK = "Timeseries from the request was created or updated. " +
- "Platform creates an audit log event about entity timeseries updates with action type 'TIMESERIES_UPDATED'.";
- protected static final String SAVE_ENTITY_TIMESERIES_STATUS_UNAUTHORIZED = "User is not authorized to save entity timeseries for selected entity. Most likely, User belongs to different Customer or Tenant.";
+ protected static final String SAVE_ENTITY_TIMESERIES_STATUS_OK = "Time series from the request was created or updated. " +
+ "Platform creates an audit log event about entity time series updates with action type 'TIMESERIES_UPDATED'.";
+ protected static final String SAVE_ENTITY_TIMESERIES_STATUS_UNAUTHORIZED = "User is not authorized to save entity time series for selected entity. Most likely, User belongs to different Customer or Tenant.";
protected static final String SAVE_ENTITY_TIMESERIES_STATUS_INTERNAL_SERVER_ERROR = "The exception was thrown during processing the request. " +
- "Platform creates an audit log event about entity timeseries updates with action type 'TIMESERIES_UPDATED' that includes an error stacktrace.";
+ "Platform creates an audit log event about entity time series updates with action type 'TIMESERIES_UPDATED' that includes an error stacktrace.";
protected static final String ENTITY_ATTRIBUTE_SCOPES_TEMPLATE = " List of possible attribute scopes depends on the entity type: " +
"\n\n * SERVER_SCOPE - supported for all entity types;" +
diff --git a/application/src/main/java/org/thingsboard/server/controller/DashboardController.java b/application/src/main/java/org/thingsboard/server/controller/DashboardController.java
index 013575361d..2949df35a7 100644
--- a/application/src/main/java/org/thingsboard/server/controller/DashboardController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/DashboardController.java
@@ -25,7 +25,6 @@ import io.swagger.v3.oas.annotations.responses.ApiResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
-import org.springframework.http.MediaType;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
@@ -51,8 +50,8 @@ import org.thingsboard.server.common.data.id.EdgeId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
-import org.thingsboard.server.dao.resource.ImageService;
import org.thingsboard.server.config.annotations.ApiOperation;
+import org.thingsboard.server.dao.resource.ImageService;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.entitiy.dashboard.TbDashboardService;
import org.thingsboard.server.service.security.model.SecurityUser;
diff --git a/application/src/main/java/org/thingsboard/server/controller/DeviceController.java b/application/src/main/java/org/thingsboard/server/controller/DeviceController.java
index 416991a996..02baf7f411 100644
--- a/application/src/main/java/org/thingsboard/server/controller/DeviceController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/DeviceController.java
@@ -21,14 +21,12 @@ import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.ArraySchema;
-import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
-import io.swagger.v3.oas.annotations.responses.ApiResponse;
import jakarta.annotation.Nullable;
+import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
-import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.PathVariable;
@@ -79,7 +77,6 @@ import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.permission.Operation;
import org.thingsboard.server.service.security.permission.Resource;
-import jakarta.validation.Valid;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
diff --git a/application/src/main/java/org/thingsboard/server/controller/DeviceProfileController.java b/application/src/main/java/org/thingsboard/server/controller/DeviceProfileController.java
index b84e801d31..e7915fc22c 100644
--- a/application/src/main/java/org/thingsboard/server/controller/DeviceProfileController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/DeviceProfileController.java
@@ -16,14 +16,11 @@
package org.thingsboard.server.controller;
import io.swagger.v3.oas.annotations.Parameter;
-import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
-import io.swagger.v3.oas.annotations.responses.ApiResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
-import org.springframework.http.MediaType;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
@@ -42,8 +39,8 @@ import org.thingsboard.server.common.data.id.DeviceProfileId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
-import org.thingsboard.server.dao.resource.ImageService;
import org.thingsboard.server.config.annotations.ApiOperation;
+import org.thingsboard.server.dao.resource.ImageService;
import org.thingsboard.server.dao.timeseries.TimeseriesService;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.entitiy.device.profile.TbDeviceProfileService;
@@ -128,8 +125,8 @@ public class DeviceProfileController extends BaseController {
return checkNotNull(deviceProfileService.findDefaultDeviceProfileInfo(getTenantId()));
}
- @ApiOperation(value = "Get time-series keys (getTimeseriesKeys)",
- notes = "Get a set of unique time-series keys used by devices that belong to specified profile. " +
+ @ApiOperation(value = "Get time series keys (getTimeseriesKeys)",
+ notes = "Get a set of unique time series keys used by devices that belong to specified profile. " +
"If profile is not set returns a list of unique keys among all profiles. " +
"The call is used for auto-complete in the UI forms. " +
"The implementation limits the number of devices that participate in search to 100 as a trade of between accurate results and time-consuming queries. " +
diff --git a/application/src/main/java/org/thingsboard/server/controller/DomainController.java b/application/src/main/java/org/thingsboard/server/controller/DomainController.java
new file mode 100644
index 0000000000..41eea463df
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/controller/DomainController.java
@@ -0,0 +1,132 @@
+/**
+ * Copyright © 2016-2024 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.controller;
+
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.media.ArraySchema;
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.Valid;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+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.PutMapping;
+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.thingsboard.server.common.data.domain.Domain;
+import org.thingsboard.server.common.data.domain.DomainInfo;
+import org.thingsboard.server.common.data.exception.ThingsboardException;
+import org.thingsboard.server.common.data.id.DomainId;
+import org.thingsboard.server.common.data.id.OAuth2ClientId;
+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.queue.util.TbCoreComponent;
+import org.thingsboard.server.service.entitiy.domain.TbDomainService;
+import org.thingsboard.server.service.security.permission.Operation;
+import org.thingsboard.server.service.security.permission.Resource;
+
+import java.util.List;
+import java.util.UUID;
+
+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.SYSTEM_AUTHORITY_PARAGRAPH;
+import static org.thingsboard.server.controller.ControllerConstants.UUID_WIKI_LINK;
+
+@RestController
+@TbCoreComponent
+@RequestMapping("/api")
+@RequiredArgsConstructor
+@Slf4j
+public class DomainController extends BaseController {
+
+ private final TbDomainService tbDomainService;
+
+ @ApiOperation(value = "Save or Update Domain (saveDomain)",
+ notes = "Create or update the Domain. When creating domain, platform generates Domain Id as " + UUID_WIKI_LINK +
+ "The newly created Domain Id will be present in the response. " +
+ "Specify existing Domain Id to update the domain. " +
+ "Referencing non-existing Domain Id will cause 'Not Found' error." +
+ "\n\nDomain name is unique for entire platform setup.\n\n" + SYSTEM_AUTHORITY_PARAGRAPH)
+ @PreAuthorize("hasAnyAuthority('SYS_ADMIN')")
+ @PostMapping(value = "/domain")
+ public Domain saveDomain(
+ @Parameter(description = "A JSON value representing the Domain.", required = true)
+ @RequestBody @Valid Domain domain,
+ @Parameter(description = "A list of oauth2 client registration ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string")))
+ @RequestParam(name = "oauth2ClientIds", required = false) UUID[] ids) throws Exception {
+ domain.setTenantId(getTenantId());
+ checkEntity(domain.getId(), domain, Resource.DOMAIN);
+ return tbDomainService.save(domain, getOAuth2ClientIds(ids), getCurrentUser());
+ }
+
+ @ApiOperation(value = "Update oauth2 clients (updateOauth2Clients)",
+ notes = "Update oauth2 clients for the specified domain. ")
+ @PreAuthorize("hasAnyAuthority('SYS_ADMIN')")
+ @PutMapping(value = "/domain/{id}/oauth2Clients")
+ public void updateOauth2Clients(@PathVariable UUID id,
+ @RequestBody UUID[] clientIds) throws ThingsboardException {
+ DomainId domainId = new DomainId(id);
+ Domain domain = checkDomainId(domainId, Operation.WRITE);
+ List oAuth2ClientIds = getOAuth2ClientIds(clientIds);
+ tbDomainService.updateOauth2Clients(domain, oAuth2ClientIds, getCurrentUser());
+ }
+
+ @ApiOperation(value = "Get Domain infos (getTenantDomainInfos)", notes = SYSTEM_AUTHORITY_PARAGRAPH)
+ @PreAuthorize("hasAnyAuthority('SYS_ADMIN')")
+ @GetMapping(value = "/domain/infos")
+ public PageData getTenantDomainInfos(@Parameter(description = PAGE_SIZE_DESCRIPTION, required = true)
+ @RequestParam int pageSize,
+ @Parameter(description = PAGE_NUMBER_DESCRIPTION, required = true)
+ @RequestParam int page,
+ @Parameter(description = "Case-insensitive 'substring' filter based on domain's name")
+ @RequestParam(required = false) String textSearch,
+ @Parameter(description = SORT_PROPERTY_DESCRIPTION)
+ @RequestParam(required = false) String sortProperty,
+ @Parameter(description = SORT_ORDER_DESCRIPTION)
+ @RequestParam(required = false) String sortOrder) throws ThingsboardException {
+ accessControlService.checkPermission(getCurrentUser(), Resource.DOMAIN, Operation.READ);
+ PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder);
+ return domainService.findDomainInfosByTenantId(getTenantId(), pageLink);
+ }
+
+ @ApiOperation(value = "Get Domain info by Id (getDomainInfoById)", notes = SYSTEM_AUTHORITY_PARAGRAPH)
+ @PreAuthorize("hasAnyAuthority('SYS_ADMIN')")
+ @GetMapping(value = "/domain/info/{id}")
+ public DomainInfo getDomainInfoById(@PathVariable UUID id) throws ThingsboardException {
+ DomainId domainId = new DomainId(id);
+ return checkEntityId(domainId, domainService::findDomainInfoById, Operation.READ);
+ }
+
+ @ApiOperation(value = "Delete Domain by ID (deleteDomain)",
+ notes = "Deletes Domain by ID. Referencing non-existing domain Id will cause an error." + SYSTEM_AUTHORITY_PARAGRAPH)
+ @PreAuthorize("hasAuthority('SYS_ADMIN')")
+ @DeleteMapping(value = "/domain/{id}")
+ public void deleteDomain(@PathVariable UUID id) throws Exception {
+ DomainId domainId = new DomainId(id);
+ Domain domain = checkDomainId(domainId, Operation.DELETE);
+ tbDomainService.delete(domain, getCurrentUser());
+ }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/controller/EdgeController.java b/application/src/main/java/org/thingsboard/server/controller/EdgeController.java
index 33b6045248..2cb8eb7378 100644
--- a/application/src/main/java/org/thingsboard/server/controller/EdgeController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/EdgeController.java
@@ -478,8 +478,7 @@ public class EdgeController extends BaseController {
edgeId = checkNotNull(edgeId);
SecurityUser user = getCurrentUser();
TenantId tenantId = user.getTenantId();
- ToEdgeSyncRequest request = new ToEdgeSyncRequest(UUID.randomUUID(), tenantId, edgeId);
- edgeRpcServiceOpt.get().processSyncRequest(request, fromEdgeSyncResponse -> reply(response, fromEdgeSyncResponse));
+ edgeRpcServiceOpt.get().processSyncRequest(tenantId, edgeId, fromEdgeSyncResponse -> reply(response, fromEdgeSyncResponse));
} else {
throw new ThingsboardException("Edges support disabled", ThingsboardErrorCode.GENERAL);
}
@@ -490,7 +489,7 @@ public class EdgeController extends BaseController {
if (fromEdgeSyncResponse.isSuccess()) {
response.setResult(new ResponseEntity<>(HttpStatus.OK));
} else {
- response.setErrorResult(new ThingsboardException("Edge is not connected", ThingsboardErrorCode.GENERAL));
+ response.setErrorResult(new ThingsboardException(fromEdgeSyncResponse.getError(), ThingsboardErrorCode.GENERAL));
}
}
diff --git a/application/src/main/java/org/thingsboard/server/controller/EntityRelationController.java b/application/src/main/java/org/thingsboard/server/controller/EntityRelationController.java
index 0cd31a729b..ccf29301d8 100644
--- a/application/src/main/java/org/thingsboard/server/controller/EntityRelationController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/EntityRelationController.java
@@ -16,11 +16,8 @@
package org.thingsboard.server.controller;
import io.swagger.v3.oas.annotations.Parameter;
-import io.swagger.v3.oas.annotations.media.Content;
-import io.swagger.v3.oas.annotations.responses.ApiResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
-import org.springframework.http.MediaType;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
@@ -82,7 +79,23 @@ public class EntityRelationController extends BaseController {
@RequestMapping(value = "/relation", method = RequestMethod.POST)
@ResponseStatus(value = HttpStatus.OK)
public void saveRelation(@Parameter(description = "A JSON value representing the relation.", required = true)
- @RequestBody EntityRelation relation) throws ThingsboardException {
+ @RequestBody EntityRelation relation) throws ThingsboardException {
+ doSave(relation);
+ }
+
+ @ApiOperation(value = "Create Relation (saveRelationV2)",
+ notes = "Creates or updates a relation between two entities in the platform. " +
+ "Relations unique key is a combination of from/to entity id and relation type group and relation type. " +
+ SECURITY_CHECKS_ENTITIES_DESCRIPTION)
+ @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
+ @RequestMapping(value = "/v2/relation", method = RequestMethod.POST)
+ @ResponseStatus(value = HttpStatus.OK)
+ public EntityRelation saveRelationV2(@Parameter(description = "A JSON value representing the relation.", required = true)
+ @RequestBody EntityRelation relation) throws ThingsboardException {
+ return doSave(relation);
+ }
+
+ private EntityRelation doSave(EntityRelation relation) throws ThingsboardException {
checkNotNull(relation);
checkCanCreateRelation(relation.getFrom());
checkCanCreateRelation(relation.getTo());
@@ -90,7 +103,7 @@ public class EntityRelationController extends BaseController {
relation.setTypeGroup(RelationTypeGroup.COMMON);
}
- tbEntityRelationService.save(getTenantId(), getCurrentUser().getCustomerId(), relation, getCurrentUser());
+ return tbEntityRelationService.save(getTenantId(), getCurrentUser().getCustomerId(), relation, getCurrentUser());
}
@ApiOperation(value = "Delete Relation (deleteRelation)",
@@ -104,6 +117,24 @@ public class EntityRelationController extends BaseController {
@Parameter(description = RELATION_TYPE_GROUP_PARAM_DESCRIPTION) @RequestParam(value = "relationTypeGroup", required = false) String strRelationTypeGroup,
@Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @RequestParam(TO_ID) String strToId,
@Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true) @RequestParam(TO_TYPE) String strToType) throws ThingsboardException {
+ doDelete(strFromId, strFromType, strRelationType, strRelationTypeGroup, strToId, strToType);
+ }
+
+ @ApiOperation(value = "Delete Relation (deleteRelationV2)",
+ notes = "Deletes a relation between two entities in the platform. " + SECURITY_CHECKS_ENTITIES_DESCRIPTION)
+ @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
+ @RequestMapping(value = "/v2/relation", method = RequestMethod.DELETE, params = {FROM_ID, FROM_TYPE, RELATION_TYPE, TO_ID, TO_TYPE})
+ @ResponseStatus(value = HttpStatus.OK)
+ public EntityRelation deleteRelationV2(@Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @RequestParam(FROM_ID) String strFromId,
+ @Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true) @RequestParam(FROM_TYPE) String strFromType,
+ @Parameter(description = RELATION_TYPE_PARAM_DESCRIPTION, required = true) @RequestParam(RELATION_TYPE) String strRelationType,
+ @Parameter(description = RELATION_TYPE_GROUP_PARAM_DESCRIPTION) @RequestParam(value = "relationTypeGroup", required = false) String strRelationTypeGroup,
+ @Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @RequestParam(TO_ID) String strToId,
+ @Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true) @RequestParam(TO_TYPE) String strToType) throws ThingsboardException {
+ return doDelete(strFromId, strFromType, strRelationType, strRelationTypeGroup, strToId, strToType);
+ }
+
+ private EntityRelation doDelete(String strFromId, String strFromType, String strRelationType, String strRelationTypeGroup, String strToId, String strToType) throws ThingsboardException {
checkParameter(FROM_ID, strFromId);
checkParameter(FROM_TYPE, strFromType);
checkParameter(RELATION_TYPE, strRelationType);
@@ -116,7 +147,7 @@ public class EntityRelationController extends BaseController {
RelationTypeGroup relationTypeGroup = parseRelationTypeGroup(strRelationTypeGroup, RelationTypeGroup.COMMON);
EntityRelation relation = new EntityRelation(fromId, toId, strRelationType, relationTypeGroup);
- tbEntityRelationService.delete(getTenantId(), getCurrentUser().getCustomerId(), relation, getCurrentUser());
+ return tbEntityRelationService.delete(getTenantId(), getCurrentUser().getCustomerId(), relation, getCurrentUser());
}
@ApiOperation(value = "Delete common relations (deleteCommonRelations)",
diff --git a/application/src/main/java/org/thingsboard/server/controller/EntityViewController.java b/application/src/main/java/org/thingsboard/server/controller/EntityViewController.java
index 783b451a6d..73ba0a053d 100644
--- a/application/src/main/java/org/thingsboard/server/controller/EntityViewController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/EntityViewController.java
@@ -17,13 +17,10 @@ package org.thingsboard.server.controller;
import com.google.common.util.concurrent.ListenableFuture;
import io.swagger.v3.oas.annotations.Parameter;
-import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
-import io.swagger.v3.oas.annotations.responses.ApiResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
-import org.springframework.http.MediaType;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
diff --git a/application/src/main/java/org/thingsboard/server/controller/EventController.java b/application/src/main/java/org/thingsboard/server/controller/EventController.java
index 372d724912..6945278064 100644
--- a/application/src/main/java/org/thingsboard/server/controller/EventController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/EventController.java
@@ -16,12 +16,9 @@
package org.thingsboard.server.controller;
import io.swagger.v3.oas.annotations.Parameter;
-import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
-import io.swagger.v3.oas.annotations.responses.ApiResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
-import org.springframework.http.MediaType;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
diff --git a/application/src/main/java/org/thingsboard/server/controller/ImageController.java b/application/src/main/java/org/thingsboard/server/controller/ImageController.java
index e8e7ca1d8f..6ebb6fffe8 100644
--- a/application/src/main/java/org/thingsboard/server/controller/ImageController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/ImageController.java
@@ -41,6 +41,7 @@ import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import org.thingsboard.server.common.data.ImageDescriptor;
import org.thingsboard.server.common.data.ImageExportData;
+import org.thingsboard.server.common.data.ResourceSubType;
import org.thingsboard.server.common.data.ResourceType;
import org.thingsboard.server.common.data.TbImageDeleteResult;
import org.thingsboard.server.common.data.TbResource;
@@ -65,6 +66,7 @@ import java.util.concurrent.TimeUnit;
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.RESOURCE_IMAGE_SUB_TYPE_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.RESOURCE_INCLUDE_SYSTEM_IMAGES_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.RESOURCE_TEXT_SEARCH_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.SORT_ORDER_DESCRIPTION;
@@ -96,7 +98,8 @@ public class ImageController extends BaseController {
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@PostMapping(value = "/api/image", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public TbResourceInfo uploadImage(@RequestPart MultipartFile file,
- @RequestPart(required = false) String title) throws Exception {
+ @RequestPart(required = false) String title,
+ @RequestPart(required = false) String imageSubType) throws Exception {
SecurityUser user = getCurrentUser();
TbResource image = new TbResource();
image.setTenantId(user.getTenantId());
@@ -109,7 +112,14 @@ public class ImageController extends BaseController {
} else {
image.setTitle(file.getOriginalFilename());
}
+
+ ResourceSubType subType = ResourceSubType.IMAGE;
+ if (StringUtils.isNotEmpty(imageSubType)) {
+ subType = ResourceSubType.valueOf(imageSubType);
+ }
+
image.setResourceType(ResourceType.IMAGE);
+ image.setResourceSubType(subType);
ImageDescriptor descriptor = new ImageDescriptor();
descriptor.setMediaType(file.getContentType());
image.setDescriptorValue(descriptor);
@@ -194,6 +204,7 @@ public class ImageController extends BaseController {
.mediaType(descriptor.getMediaType())
.fileName(imageInfo.getFileName())
.title(imageInfo.getTitle())
+ .subType(imageInfo.getResourceSubType().name())
.resourceKey(imageInfo.getResourceKey())
.isPublic(imageInfo.isPublic())
.publicResourceKey(imageInfo.getPublicResourceKey())
@@ -215,6 +226,11 @@ public class ImageController extends BaseController {
} else {
image.setTitle(imageData.getFileName());
}
+ if (StringUtils.isNotEmpty(imageData.getSubType())) {
+ image.setResourceSubType(ResourceSubType.valueOf(imageData.getSubType()));
+ } else {
+ image.setResourceSubType(ResourceSubType.IMAGE);
+ }
image.setResourceType(ResourceType.IMAGE);
image.setResourceKey(imageData.getResourceKey());
image.setPublic(imageData.isPublic());
@@ -251,6 +267,8 @@ public class ImageController extends BaseController {
@RequestParam int pageSize,
@Parameter(description = PAGE_NUMBER_DESCRIPTION, required = true)
@RequestParam int page,
+ @Parameter(description = RESOURCE_IMAGE_SUB_TYPE_DESCRIPTION, schema = @Schema(allowableValues = {"IMAGE", "SCADA_SYMBOL"}))
+ @RequestParam(required = false) String imageSubType,
@Parameter(description = RESOURCE_INCLUDE_SYSTEM_IMAGES_DESCRIPTION)
@RequestParam(required = false) boolean includeSystemImages,
@Parameter(description = RESOURCE_TEXT_SEARCH_DESCRIPTION)
@@ -262,10 +280,14 @@ public class ImageController extends BaseController {
// PE: generic permission
PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder);
TenantId tenantId = getTenantId();
+ ResourceSubType subType = ResourceSubType.IMAGE;
+ if (StringUtils.isNotEmpty(imageSubType)) {
+ subType = ResourceSubType.valueOf(imageSubType);
+ }
if (getCurrentUser().getAuthority() == Authority.SYS_ADMIN || !includeSystemImages) {
- return checkNotNull(imageService.getImagesByTenantId(tenantId, pageLink));
+ return checkNotNull(imageService.getImagesByTenantId(tenantId, subType, pageLink));
} else {
- return checkNotNull(imageService.getAllImagesByTenantId(tenantId, pageLink));
+ return checkNotNull(imageService.getAllImagesByTenantId(tenantId, subType, pageLink));
}
}
diff --git a/application/src/main/java/org/thingsboard/server/controller/Lwm2mController.java b/application/src/main/java/org/thingsboard/server/controller/Lwm2mController.java
index 25e49eb198..719767ab43 100644
--- a/application/src/main/java/org/thingsboard/server/controller/Lwm2mController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/Lwm2mController.java
@@ -16,12 +16,9 @@
package org.thingsboard.server.controller;
import io.swagger.v3.oas.annotations.Parameter;
-import io.swagger.v3.oas.annotations.media.Content;
-import io.swagger.v3.oas.annotations.responses.ApiResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
-import org.springframework.http.MediaType;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
diff --git a/application/src/main/java/org/thingsboard/server/controller/MobileAppController.java b/application/src/main/java/org/thingsboard/server/controller/MobileAppController.java
new file mode 100644
index 0000000000..fba17d7119
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/controller/MobileAppController.java
@@ -0,0 +1,133 @@
+/**
+ * Copyright © 2016-2024 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.controller;
+
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.media.ArraySchema;
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.Valid;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+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.PutMapping;
+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.thingsboard.server.common.data.exception.ThingsboardException;
+import org.thingsboard.server.common.data.id.MobileAppId;
+import org.thingsboard.server.common.data.id.OAuth2ClientId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.mobile.MobileApp;
+import org.thingsboard.server.common.data.mobile.MobileAppInfo;
+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.queue.util.TbCoreComponent;
+import org.thingsboard.server.service.entitiy.mobile.TbMobileAppService;
+import org.thingsboard.server.service.security.permission.Operation;
+import org.thingsboard.server.service.security.permission.Resource;
+
+import java.util.List;
+import java.util.UUID;
+
+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.SYSTEM_AUTHORITY_PARAGRAPH;
+import static org.thingsboard.server.controller.ControllerConstants.UUID_WIKI_LINK;
+
+@RestController
+@TbCoreComponent
+@RequestMapping("/api")
+@RequiredArgsConstructor
+@Slf4j
+public class MobileAppController extends BaseController {
+
+ private final TbMobileAppService tbMobileAppService;
+
+ @ApiOperation(value = "Save Or update Mobile app (saveMobileApp)",
+ notes = "Create or update the Mobile app. When creating mobile app, platform generates Mobile App Id as " + UUID_WIKI_LINK +
+ "The newly created Mobile App Id will be present in the response. " +
+ "Specify existing Mobile App Id to update the mobile app. " +
+ "Referencing non-existing Mobile App Id will cause 'Not Found' error." +
+ "\n\nMobile app package name is unique for entire platform setup.\n\n" + SYSTEM_AUTHORITY_PARAGRAPH)
+ @PreAuthorize("hasAnyAuthority('SYS_ADMIN')")
+ @PostMapping(value = "/mobileApp")
+ public MobileApp saveMobileApp(
+ @Parameter(description = "A JSON value representing the Mobile Application.", required = true)
+ @RequestBody @Valid MobileApp mobileApp,
+ @Parameter(description = "A list of entity oauth2 client ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string")))
+ @RequestParam(name = "oauth2ClientIds", required = false) UUID[] ids) throws Exception {
+ mobileApp.setTenantId(getTenantId());
+ checkEntity(mobileApp.getId(), mobileApp, Resource.MOBILE_APP);
+ return tbMobileAppService.save(mobileApp, getOAuth2ClientIds(ids), getCurrentUser());
+ }
+
+ @ApiOperation(value = "Update oauth2 clients (updateOauth2Clients)",
+ notes = "Update oauth2 clients of the specified mobile app. ")
+ @PreAuthorize("hasAnyAuthority('SYS_ADMIN')")
+ @PutMapping(value = "/mobileApp/{id}/oauth2Clients")
+ public void updateOauth2Clients(@PathVariable UUID id,
+ @RequestBody UUID[] clientIds) throws ThingsboardException {
+ MobileAppId mobileAppId = new MobileAppId(id);
+ MobileApp mobileApp = checkMobileAppId(mobileAppId, Operation.WRITE);
+ List oAuth2ClientIds = getOAuth2ClientIds(clientIds);
+ tbMobileAppService.updateOauth2Clients(mobileApp, oAuth2ClientIds, getCurrentUser());
+ }
+
+ @ApiOperation(value = "Get mobile app infos (getTenantMobileAppInfos)", notes = SYSTEM_AUTHORITY_PARAGRAPH)
+ @PreAuthorize("hasAnyAuthority('SYS_ADMIN')")
+ @GetMapping(value = "/mobileApp/infos")
+ public PageData getTenantMobileAppInfos(@Parameter(description = PAGE_SIZE_DESCRIPTION, required = true)
+ @RequestParam int pageSize,
+ @Parameter(description = PAGE_NUMBER_DESCRIPTION, required = true)
+ @RequestParam int page,
+ @Parameter(description = "Case-insensitive 'substring' filter based on app's name")
+ @RequestParam(required = false) String textSearch,
+ @Parameter(description = SORT_PROPERTY_DESCRIPTION)
+ @RequestParam(required = false) String sortProperty,
+ @Parameter(description = SORT_ORDER_DESCRIPTION)
+ @RequestParam(required = false) String sortOrder) throws ThingsboardException {
+ accessControlService.checkPermission(getCurrentUser(), Resource.MOBILE_APP, Operation.READ);
+ PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder);
+ return mobileAppService.findMobileAppInfosByTenantId(getTenantId(), pageLink);
+ }
+
+ @ApiOperation(value = "Get mobile info by id (getMobileAppInfoById)", notes = SYSTEM_AUTHORITY_PARAGRAPH)
+ @PreAuthorize("hasAnyAuthority('SYS_ADMIN')")
+ @GetMapping(value = "/mobileApp/info/{id}")
+ public MobileAppInfo getMobileAppInfoById(@PathVariable UUID id) throws ThingsboardException {
+ MobileAppId mobileAppId = new MobileAppId(id);
+ return checkEntityId(mobileAppId, mobileAppService::findMobileAppInfoById, Operation.READ);
+ }
+
+ @ApiOperation(value = "Delete Mobile App by ID (deleteMobileApp)",
+ notes = "Deletes Mobile App by ID. Referencing non-existing mobile app Id will cause an error." + SYSTEM_AUTHORITY_PARAGRAPH)
+ @PreAuthorize("hasAuthority('SYS_ADMIN')")
+ @DeleteMapping(value = "/mobileApp/{id}")
+ public void deleteMobileApp(@PathVariable UUID id) throws Exception {
+ MobileAppId mobileAppId = new MobileAppId(id);
+ MobileApp mobileApp = checkMobileAppId(mobileAppId, Operation.DELETE);
+ tbMobileAppService.delete(mobileApp, getCurrentUser());
+ }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/controller/NotificationController.java b/application/src/main/java/org/thingsboard/server/controller/NotificationController.java
index fb32ebd30b..c1248a1fbf 100644
--- a/application/src/main/java/org/thingsboard/server/controller/NotificationController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/NotificationController.java
@@ -300,9 +300,15 @@ public class NotificationController extends BaseController {
request.setOriginatorEntityId(user.getId());
List targets = request.getTargets().stream()
.map(NotificationTargetId::new)
- .map(targetId -> notificationTargetService.findNotificationTargetById(user.getTenantId(), targetId))
+ .map(targetId -> {
+ NotificationTarget target = notificationTargetService.findNotificationTargetById(user.getTenantId(), targetId);
+ if (target == null) {
+ throw new IllegalArgumentException("Notification target for id " + targetId + " not found");
+ }
+ return target;
+ })
.sorted(Comparator.comparing(target -> target.getConfiguration().getType()))
- .collect(Collectors.toList());
+ .toList();
NotificationRequestPreview preview = new NotificationRequestPreview();
diff --git a/application/src/main/java/org/thingsboard/server/controller/OAuth2Controller.java b/application/src/main/java/org/thingsboard/server/controller/OAuth2Controller.java
index 325cc3b025..8a8055c4af 100644
--- a/application/src/main/java/org/thingsboard/server/controller/OAuth2Controller.java
+++ b/application/src/main/java/org/thingsboard/server/controller/OAuth2Controller.java
@@ -16,60 +16,75 @@
package org.thingsboard.server.controller;
import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.servlet.http.HttpServletRequest;
+import jakarta.validation.Valid;
+import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
-import org.springframework.beans.factory.annotation.Autowired;
-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.StringUtils;
import org.thingsboard.server.common.data.exception.ThingsboardException;
+import org.thingsboard.server.common.data.id.OAuth2ClientId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.oauth2.OAuth2Client;
import org.thingsboard.server.common.data.oauth2.OAuth2ClientInfo;
-import org.thingsboard.server.common.data.oauth2.OAuth2Info;
+import org.thingsboard.server.common.data.oauth2.OAuth2ClientLoginInfo;
import org.thingsboard.server.common.data.oauth2.PlatformType;
+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.oauth2.OAuth2Configuration;
import org.thingsboard.server.queue.util.TbCoreComponent;
+import org.thingsboard.server.service.entitiy.oauth2client.TbOauth2ClientService;
import org.thingsboard.server.service.security.permission.Operation;
import org.thingsboard.server.service.security.permission.Resource;
import org.thingsboard.server.utils.MiscUtils;
+import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
+import java.util.UUID;
+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.SYSTEM_AUTHORITY_PARAGRAPH;
@RestController
@TbCoreComponent
@RequestMapping("/api")
+@RequiredArgsConstructor
@Slf4j
public class OAuth2Controller extends BaseController {
- @Autowired
- private OAuth2Configuration oAuth2Configuration;
+ private final OAuth2Configuration oAuth2Configuration;
+
+ private final TbOauth2ClientService tbOauth2ClientService;
@ApiOperation(value = "Get OAuth2 clients (getOAuth2Clients)", notes = "Get the list of OAuth2 clients " +
"to log in with, available for such domain scheme (HTTP or HTTPS) (if x-forwarded-proto request header is present - " +
"the scheme is known from it) and domain name and port (port may be known from x-forwarded-port header)")
- @RequestMapping(value = "/noauth/oauth2Clients", method = RequestMethod.POST)
- @ResponseBody
- public List getOAuth2Clients(HttpServletRequest request,
- @Parameter(description = "Mobile application package name, to find OAuth2 clients " +
- "where there is configured mobile application with such package name")
- @RequestParam(required = false) String pkgName,
- @Parameter(description = "Platform type to search OAuth2 clients for which " +
- "the usage with this platform type is allowed in the settings. " +
- "If platform type is not one of allowable values - it will just be ignored",
- schema = @Schema(allowableValues = {"WEB", "ANDROID", "IOS"}))
- @RequestParam(required = false) String platform) throws ThingsboardException {
+ @PostMapping(value = "/noauth/oauth2Clients")
+ public List getOAuth2Clients(HttpServletRequest request,
+ @Parameter(description = "Mobile application package name, to find OAuth2 clients " +
+ "where there is configured mobile application with such package name")
+ @RequestParam(required = false) String pkgName,
+ @Parameter(description = "Platform type to search OAuth2 clients for which " +
+ "the usage with this platform type is allowed in the settings. " +
+ "If platform type is not one of allowable values - it will just be ignored",
+ schema = @Schema(allowableValues = {"WEB", "ANDROID", "IOS"}))
+ @RequestParam(required = false) String platform) {
if (log.isDebugEnabled()) {
log.debug("Executing getOAuth2Clients: [{}][{}][{}]", request.getScheme(), request.getServerName(), request.getServerPort());
Enumeration headerNames = request.getHeaderNames();
@@ -80,31 +95,70 @@ public class OAuth2Controller extends BaseController {
}
PlatformType platformType = null;
if (StringUtils.isNotEmpty(platform)) {
- try {
- platformType = PlatformType.valueOf(platform);
- } catch (Exception e) {
- }
+ platformType = PlatformType.valueOf(platform);
+ }
+ if (StringUtils.isNotEmpty(pkgName)) {
+ return oAuth2ClientService.findOAuth2ClientLoginInfosByMobilePkgNameAndPlatformType(pkgName, platformType);
+ } else {
+ return oAuth2ClientService.findOAuth2ClientLoginInfosByDomainName(MiscUtils.getDomainNameAndPort(request));
}
- return oAuth2Service.getOAuth2Clients(MiscUtils.getScheme(request), MiscUtils.getDomainNameAndPort(request), pkgName, platformType);
}
- @ApiOperation(value = "Get current OAuth2 settings (getCurrentOAuth2Info)", notes = SYSTEM_AUTHORITY_PARAGRAPH)
+ @ApiOperation(value = "Save OAuth2 Client (saveOAuth2Client)", notes = SYSTEM_AUTHORITY_PARAGRAPH)
+ @PreAuthorize("hasAnyAuthority('SYS_ADMIN')")
+ @PostMapping(value = "/oauth2/client")
+ public OAuth2Client saveOAuth2Client(@RequestBody @Valid OAuth2Client oAuth2Client) throws Exception {
+ TenantId tenantId = getTenantId();
+ oAuth2Client.setTenantId(tenantId);
+ checkEntity(oAuth2Client.getId(), oAuth2Client, Resource.OAUTH2_CLIENT);
+ return tbOauth2ClientService.save(oAuth2Client, getCurrentUser());
+ }
+
+ @ApiOperation(value = "Get OAuth2 Client infos (findTenantOAuth2ClientInfos)", notes = SYSTEM_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN')")
- @RequestMapping(value = "/oauth2/config", method = RequestMethod.GET, produces = "application/json")
- @ResponseBody
- public OAuth2Info getCurrentOAuth2Info() throws ThingsboardException {
- accessControlService.checkPermission(getCurrentUser(), Resource.OAUTH2_CONFIGURATION_INFO, Operation.READ);
- return oAuth2Service.findOAuth2Info();
+ @GetMapping(value = "/oauth2/client/infos")
+ public PageData findTenantOAuth2ClientInfos(@Parameter(description = PAGE_SIZE_DESCRIPTION, required = true)
+ @RequestParam int pageSize,
+ @Parameter(description = PAGE_NUMBER_DESCRIPTION, required = true)
+ @RequestParam int page,
+ @Parameter(description = "Case-insensitive 'substring' filter based on client's title")
+ @RequestParam(required = false) String textSearch,
+ @Parameter(description = SORT_PROPERTY_DESCRIPTION)
+ @RequestParam(required = false) String sortProperty,
+ @Parameter(description = SORT_ORDER_DESCRIPTION)
+ @RequestParam(required = false) String sortOrder) throws ThingsboardException {
+ accessControlService.checkPermission(getCurrentUser(), Resource.OAUTH2_CLIENT, Operation.READ);
+ PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder);
+ return oAuth2ClientService.findOAuth2ClientInfosByTenantId(getTenantId(), pageLink);
}
- @ApiOperation(value = "Save OAuth2 settings (saveOAuth2Info)", notes = SYSTEM_AUTHORITY_PARAGRAPH)
+ @ApiOperation(value = "Get OAuth2 Client infos By Ids (findTenantOAuth2ClientInfosByIds)",
+ notes = "Fetch OAuth2 Client info objects based on the provided ids. ")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN')")
- @RequestMapping(value = "/oauth2/config", method = RequestMethod.POST)
- @ResponseStatus(value = HttpStatus.OK)
- public OAuth2Info saveOAuth2Info(@RequestBody OAuth2Info oauth2Info) throws ThingsboardException {
- accessControlService.checkPermission(getCurrentUser(), Resource.OAUTH2_CONFIGURATION_INFO, Operation.WRITE);
- oAuth2Service.saveOAuth2Info(oauth2Info);
- return oAuth2Service.findOAuth2Info();
+ @GetMapping(value = "/oauth2/client/infos", params = {"clientIds"})
+ public List findTenantOAuth2ClientInfosByIds(
+ @Parameter(description = "A list of oauth2 ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string")), required = true)
+ @RequestParam("clientIds") UUID[] clientIds) throws ThingsboardException {
+ List oAuth2ClientIds = getOAuth2ClientIds(clientIds);
+ return oAuth2ClientService.findOAuth2ClientInfosByIds(getTenantId(), oAuth2ClientIds);
+ }
+
+ @ApiOperation(value = "Get OAuth2 Client by id (getOAuth2ClientById)", notes = SYSTEM_AUTHORITY_PARAGRAPH)
+ @PreAuthorize("hasAnyAuthority('SYS_ADMIN')")
+ @GetMapping(value = "/oauth2/client/{id}")
+ public OAuth2Client getOAuth2ClientById(@PathVariable UUID id) throws ThingsboardException {
+ OAuth2ClientId oAuth2ClientId = new OAuth2ClientId(id);
+ return checkEntityId(oAuth2ClientId, oAuth2ClientService::findOAuth2ClientById, Operation.READ);
+ }
+
+ @ApiOperation(value = "Delete oauth2 client (deleteOauth2Client)",
+ notes = "Deletes the oauth2 client. Referencing non-existing oauth2 client Id will cause an error." + SYSTEM_AUTHORITY_PARAGRAPH)
+ @PreAuthorize("hasAuthority('SYS_ADMIN')")
+ @DeleteMapping(value = "/oauth2/client/{id}")
+ public void deleteOauth2Client(@PathVariable UUID id) throws Exception {
+ OAuth2ClientId oAuth2ClientId = new OAuth2ClientId(id);
+ OAuth2Client oAuth2Client = checkOauth2ClientId(oAuth2ClientId, Operation.DELETE);
+ tbOauth2ClientService.delete(oAuth2Client, getCurrentUser());
}
@ApiOperation(value = "Get OAuth2 log in processing URL (getLoginProcessingUrl)", notes = "Returns the URL enclosed in " +
@@ -112,10 +166,9 @@ public class OAuth2Controller extends BaseController {
"further log in processing. This URL may be configured as 'security.oauth2.loginProcessingUrl' property in yml configuration file, or " +
"as 'SECURITY_OAUTH2_LOGIN_PROCESSING_URL' env variable. By default it is '/login/oauth2/code/'" + SYSTEM_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN')")
- @RequestMapping(value = "/oauth2/loginProcessingUrl", method = RequestMethod.GET)
- @ResponseBody
+ @GetMapping(value = "/oauth2/loginProcessingUrl")
public String getLoginProcessingUrl() throws ThingsboardException {
- accessControlService.checkPermission(getCurrentUser(), Resource.OAUTH2_CONFIGURATION_INFO, Operation.READ);
+ accessControlService.checkPermission(getCurrentUser(), Resource.OAUTH2_CLIENT, Operation.READ);
return "\"" + oAuth2Configuration.getLoginProcessingUrl() + "\"";
}
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 ee7bfcc0dc..58e8b3138d 100644
--- a/application/src/main/java/org/thingsboard/server/controller/OtaPackageController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/OtaPackageController.java
@@ -18,7 +18,6 @@ package org.thingsboard.server.controller;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
-import io.swagger.v3.oas.annotations.responses.ApiResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.io.ByteArrayResource;
@@ -52,7 +51,6 @@ import org.thingsboard.server.service.security.permission.Resource;
import java.io.IOException;
-import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;
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;
diff --git a/application/src/main/java/org/thingsboard/server/controller/RpcV2Controller.java b/application/src/main/java/org/thingsboard/server/controller/RpcV2Controller.java
index 382d17bb98..67ff1b3fc5 100644
--- a/application/src/main/java/org/thingsboard/server/controller/RpcV2Controller.java
+++ b/application/src/main/java/org/thingsboard/server/controller/RpcV2Controller.java
@@ -46,10 +46,10 @@ import org.thingsboard.server.common.data.rpc.Rpc;
import org.thingsboard.server.common.data.rpc.RpcStatus;
import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.TbMsgMetaData;
+import org.thingsboard.server.common.msg.rpc.RemoveRpcActorMsg;
import org.thingsboard.server.config.annotations.ApiOperation;
import org.thingsboard.server.exception.ToErrorResponseEntity;
import org.thingsboard.server.queue.util.TbCoreComponent;
-import org.thingsboard.server.common.msg.rpc.RemoveRpcActorMsg;
import org.thingsboard.server.service.security.permission.Operation;
import java.util.UUID;
diff --git a/application/src/main/java/org/thingsboard/server/controller/TbResourceController.java b/application/src/main/java/org/thingsboard/server/controller/TbResourceController.java
index 74bc087abf..8feb898a65 100644
--- a/application/src/main/java/org/thingsboard/server/controller/TbResourceController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/TbResourceController.java
@@ -17,9 +17,7 @@ package org.thingsboard.server.controller;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.ArraySchema;
-import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
-import io.swagger.v3.oas.annotations.responses.ApiResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
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 6833a7ddfb..b1733a9af0 100644
--- a/application/src/main/java/org/thingsboard/server/controller/TelemetryController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/TelemetryController.java
@@ -243,8 +243,8 @@ public class TelemetryController extends BaseController {
(result, tenantId, entityId) -> getAttributeValuesCallback(result, user, entityId, scope, keysStr));
}
- @ApiOperation(value = "Get time-series keys (getTimeseriesKeys)",
- notes = "Returns a set of unique time-series key names for the selected entity. " +
+ @ApiOperation(value = "Get time series keys (getTimeseriesKeys)",
+ notes = "Returns a set of unique time series key names for the selected entity. " +
"\n\n" + INVALID_ENTITY_ID_OR_ENTITY_TYPE_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/{entityType}/{entityId}/keys/timeseries", method = RequestMethod.GET)
@@ -256,10 +256,10 @@ public class TelemetryController extends BaseController {
(result, tenantId, entityId) -> Futures.addCallback(tsService.findAllLatest(tenantId, entityId), getTsKeysToResponseCallback(result), MoreExecutors.directExecutor()));
}
- @ApiOperation(value = "Get latest time-series value (getLatestTimeseries)",
- notes = "Returns all time-series that belong to specified entity. Use optional 'keys' parameter to return specific time-series." +
+ @ApiOperation(value = "Get latest time series value (getLatestTimeseries)",
+ notes = "Returns all time series that belong to specified entity. Use optional 'keys' parameter to return specific time series." +
" The result is a JSON object. The format of the values depends on the 'useStrictDataTypes' parameter." +
- " By default, all time-series values are converted to strings: \n\n"
+ " By default, all time series values are converted to strings: \n\n"
+ MARKDOWN_CODE_BLOCK_START
+ LATEST_TS_NON_STRICT_DATA_EXAMPLE
+ MARKDOWN_CODE_BLOCK_END
@@ -282,8 +282,8 @@ public class TelemetryController extends BaseController {
(result, tenantId, entityId) -> getLatestTimeseriesValuesCallback(result, user, entityId, keysStr, useStrictDataTypes));
}
- @ApiOperation(value = "Get time-series data (getTimeseries)",
- notes = "Returns a range of time-series values for specified entity. " +
+ @ApiOperation(value = "Get time series data (getTimeseries)",
+ notes = "Returns a range of time series values for specified entity. " +
"Returns not aggregated data by default. " +
"Use aggregation function ('agg') and aggregation interval ('interval') to enable aggregation of the results on the database / server side. " +
"The aggregation is generally more efficient then fetching all records. \n\n"
@@ -308,7 +308,7 @@ public class TelemetryController extends BaseController {
@RequestParam(name = "interval", defaultValue = "0") Long interval,
@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 timeseries data points to fetch." +
+ @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"))
@RequestParam(name = "limit", defaultValue = "100") Integer limit,
@Parameter(description = "A string value representing the aggregation function. " +
@@ -406,8 +406,8 @@ public class TelemetryController extends BaseController {
}
- @ApiOperation(value = "Save or update time-series data (saveEntityTelemetry)",
- notes = "Creates or updates the entity time-series data based on the Entity Id and request payload." +
+ @ApiOperation(value = "Save or update time series data (saveEntityTelemetry)",
+ notes = "Creates or updates the entity time series data based on the Entity Id and request payload." +
SAVE_TIMESERIES_REQUEST_PAYLOAD +
"\n\n The scope parameter is not used in the API call implementation but should be specified whatever value because it is used as a path variable. "
+ INVALID_ENTITY_ID_OR_ENTITY_TYPE_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@@ -429,8 +429,8 @@ public class TelemetryController extends BaseController {
return saveTelemetry(getTenantId(), entityId, requestBody, 0L);
}
- @ApiOperation(value = "Save or update time-series data with TTL (saveEntityTelemetryWithTTL)",
- notes = "Creates or updates the entity time-series data based on the Entity Id and request payload." +
+ @ApiOperation(value = "Save or update time series data with TTL (saveEntityTelemetryWithTTL)",
+ notes = "Creates or updates the entity time series data based on the Entity Id and request payload." +
SAVE_TIMESERIES_REQUEST_PAYLOAD +
"\n\n The scope parameter is not used in the API call implementation but should be specified whatever value because it is used as a path variable. "
+ "\n\nThe ttl parameter takes affect only in case of Cassandra DB."
@@ -454,21 +454,21 @@ public class TelemetryController extends BaseController {
return saveTelemetry(getTenantId(), entityId, requestBody, ttl);
}
- @ApiOperation(value = "Delete entity time-series data (deleteEntityTimeseries)",
- notes = "Delete time-series for selected entity based on entity id, entity type and keys." +
- " Use 'deleteAllDataForKeys' to delete all time-series data." +
+ @ApiOperation(value = "Delete entity time series data (deleteEntityTimeseries)",
+ notes = "Delete time series for selected entity based on entity id, entity type and keys." +
+ " Use 'deleteAllDataForKeys' to delete all time series data." +
" Use 'startTs' and 'endTs' to specify time-range instead. " +
" Use 'deleteLatest' to delete latest value (stored in separate table for performance) if the value's timestamp matches the time-range. " +
" Use 'rewriteLatestIfDeleted' to rewrite latest value (stored in separate table for performance) if the value's timestamp matches the time-range and 'deleteLatest' param is true." +
- " The replacement value will be fetched from the 'time-series' table, and its timestamp will be the most recent one before the defined time-range. " +
+ " The replacement value will be fetched from the 'time series' table, and its timestamp will be the most recent one before the defined time-range. " +
TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@ApiResponses(value = {
- @ApiResponse(responseCode = "200", description = "Timeseries for the selected keys in the request was removed. " +
- "Platform creates an audit log event about entity timeseries removal with action type 'TIMESERIES_DELETED'."),
+ @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'."),
@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 timeseries for selected entity. Most likely, User belongs to different Customer or Tenant."),
+ @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 timeseries 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)
@@ -649,12 +649,12 @@ public class TelemetryController extends BaseController {
try {
telemetryJson = JsonParser.parseString(requestBody);
} catch (Exception e) {
- return getImmediateDeferredResult("Unable to parse timeseries payload: Invalid JSON body!", HttpStatus.BAD_REQUEST);
+ return getImmediateDeferredResult("Unable to parse time series payload: Invalid JSON body!", HttpStatus.BAD_REQUEST);
}
try {
telemetryRequest = JsonConverter.convertToTelemetry(telemetryJson, System.currentTimeMillis());
} catch (Exception e) {
- return getImmediateDeferredResult("Unable to parse timeseries payload. Invalid JSON body: " + e.getMessage(), HttpStatus.BAD_REQUEST);
+ return getImmediateDeferredResult("Unable to parse time series payload. Invalid JSON body: " + e.getMessage(), HttpStatus.BAD_REQUEST);
}
List entries = new ArrayList<>();
for (Map.Entry> entry : telemetryRequest.entrySet()) {
@@ -663,7 +663,7 @@ public class TelemetryController extends BaseController {
}
}
if (entries.isEmpty()) {
- return getImmediateDeferredResult("No timeseries data found in request body!", HttpStatus.BAD_REQUEST);
+ return getImmediateDeferredResult("No time series data found in request body!", HttpStatus.BAD_REQUEST);
}
SecurityUser user = getCurrentUser();
return accessValidator.validateEntityAndCallback(getCurrentUser(), Operation.WRITE_TELEMETRY, entityIdSrc, (result, tenantId, entityId) -> {
diff --git a/application/src/main/java/org/thingsboard/server/controller/WidgetTypeController.java b/application/src/main/java/org/thingsboard/server/controller/WidgetTypeController.java
index 4c48dbd434..03f1c52faf 100644
--- a/application/src/main/java/org/thingsboard/server/controller/WidgetTypeController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/WidgetTypeController.java
@@ -19,7 +19,6 @@ import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.RequiredArgsConstructor;
-import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.PathVariable;
@@ -42,8 +41,10 @@ import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.common.data.widget.DeprecatedFilter;
import org.thingsboard.server.common.data.widget.WidgetType;
import org.thingsboard.server.common.data.widget.WidgetTypeDetails;
+import org.thingsboard.server.common.data.widget.WidgetTypeFilter;
import org.thingsboard.server.common.data.widget.WidgetTypeInfo;
import org.thingsboard.server.common.data.widget.WidgetsBundle;
+import org.thingsboard.server.common.data.widget.WidgetsBundleFilter;
import org.thingsboard.server.config.annotations.ApiOperation;
import org.thingsboard.server.dao.model.ModelConstants;
import org.thingsboard.server.dao.resource.ImageService;
@@ -84,6 +85,7 @@ public class WidgetTypeController extends AutoCommitController {
private static final String WIDGET_TYPE_INFO_DESCRIPTION = "Widget Type Info is a lightweight object that represents Widget Type but does not contain the heavyweight widget descriptor JSON";
private static final String TENANT_ONLY_PARAM_DESCRIPTION = "Optional boolean parameter indicating whether only tenant widget types should be returned";
private static final String FULL_SEARCH_PARAM_DESCRIPTION = "Optional boolean parameter indicating whether search widgets by description not only by name";
+ private static final String SCADA_FIRST_PARAM_DESCRIPTION = "Optional boolean parameter indicating whether to fetch SCADA symbol widgets first";
private static final String DEPRECATED_FILTER_PARAM_DESCRIPTION = "Optional string parameter indicating whether to include deprecated widgets";
private static final String UPDATE_EXISTING_BY_FQN_PARAM_DESCRIPTION = "Optional boolean parameter indicating whether to update existing widget type by FQN if present instead of creating new one";
private static final String WIDGET_TYPE_ARRAY_DESCRIPTION = "A list of string values separated by comma ',' representing one of the widget type value";
@@ -187,18 +189,26 @@ public class WidgetTypeController extends AutoCommitController {
@Parameter(description = DEPRECATED_FILTER_PARAM_DESCRIPTION, schema = @Schema(allowableValues = {"ALL", "ACTUAL", "DEPRECATED"}))
@RequestParam(required = false) String deprecatedFilter,
@Parameter(description = WIDGET_TYPE_ARRAY_DESCRIPTION, array = @ArraySchema(schema = @Schema(type = "string", allowableValues = {"timeseries", "latest", "control", "alarm", "static"})))
- @RequestParam(required = false) String[] widgetTypeList) throws ThingsboardException {
+ @RequestParam(required = false) String[] widgetTypeList,
+ @Parameter(description = SCADA_FIRST_PARAM_DESCRIPTION)
+ @RequestParam(required = false) Boolean scadaFirst) throws ThingsboardException {
PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder);
List widgetTypes = widgetTypeList != null ? Arrays.asList(widgetTypeList) : Collections.emptyList();
- boolean fullSearchBool = fullSearch != null && fullSearch;
DeprecatedFilter widgetTypeDeprecatedFilter = StringUtils.isNotEmpty(deprecatedFilter) ? DeprecatedFilter.valueOf(deprecatedFilter) : DeprecatedFilter.ALL;
+ WidgetTypeFilter widgetTypeFilter = WidgetTypeFilter.builder()
+ .tenantId(getTenantId())
+ .widgetTypes(widgetTypes)
+ .deprecatedFilter(widgetTypeDeprecatedFilter)
+ .fullSearch(fullSearch != null && fullSearch)
+ .scadaFirst(scadaFirst != null && scadaFirst)
+ .build();
if (Authority.SYS_ADMIN.equals(getCurrentUser().getAuthority())) {
- return checkNotNull(widgetTypeService.findSystemWidgetTypesByPageLink(getTenantId(), fullSearchBool, widgetTypeDeprecatedFilter, widgetTypes, pageLink));
+ return checkNotNull(widgetTypeService.findSystemWidgetTypesByPageLink(widgetTypeFilter, pageLink));
} else {
if (tenantOnly != null && tenantOnly) {
- return checkNotNull(widgetTypeService.findTenantWidgetTypesByTenantIdAndPageLink(getTenantId(), fullSearchBool, widgetTypeDeprecatedFilter, widgetTypes, pageLink));
+ return checkNotNull(widgetTypeService.findTenantWidgetTypesByTenantIdAndPageLink(widgetTypeFilter, pageLink));
} else {
- return checkNotNull(widgetTypeService.findAllTenantWidgetTypesByTenantIdAndPageLink(getTenantId(), fullSearchBool, widgetTypeDeprecatedFilter, widgetTypes, pageLink));
+ return checkNotNull(widgetTypeService.findAllTenantWidgetTypesByTenantIdAndPageLink(widgetTypeFilter, pageLink));
}
}
}
diff --git a/application/src/main/java/org/thingsboard/server/controller/WidgetsBundleController.java b/application/src/main/java/org/thingsboard/server/controller/WidgetsBundleController.java
index f652e78b85..8d39e6c83d 100644
--- a/application/src/main/java/org/thingsboard/server/controller/WidgetsBundleController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/WidgetsBundleController.java
@@ -36,8 +36,9 @@ import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.common.data.widget.WidgetsBundle;
-import org.thingsboard.server.dao.resource.ImageService;
+import org.thingsboard.server.common.data.widget.WidgetsBundleFilter;
import org.thingsboard.server.config.annotations.ApiOperation;
+import org.thingsboard.server.dao.resource.ImageService;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.entitiy.widgets.bundle.TbWidgetsBundleService;
import org.thingsboard.server.service.security.permission.Operation;
@@ -72,6 +73,7 @@ public class WidgetsBundleController extends BaseController {
private static final String WIDGET_BUNDLE_DESCRIPTION = "Widget Bundle represents a group(bundle) of widgets. Widgets are grouped into bundle by type or use case. ";
private static final String FULL_SEARCH_PARAM_DESCRIPTION = "Optional boolean parameter indicating extended search of widget bundles by description and by name / description of related widget types";
+ private static final String SCADA_FIRST_PARAM_DESCRIPTION = "Optional boolean parameter indicating whether to fetch widgets bundles with SCADA symbols first. Works only when fullSearch parameter is enabled";
private static final String TENANT_BUNDLES_ONLY_DESCRIPTION = "Optional boolean parameter to include only tenant-level bundles without system";
@ApiOperation(value = "Get Widget Bundle (getWidgetsBundleById)",
@@ -198,16 +200,22 @@ public class WidgetsBundleController extends BaseController {
@Parameter(description = TENANT_BUNDLES_ONLY_DESCRIPTION)
@RequestParam(required = false) Boolean tenantOnly,
@Parameter(description = FULL_SEARCH_PARAM_DESCRIPTION)
- @RequestParam(required = false) Boolean fullSearch) throws ThingsboardException {
+ @RequestParam(required = false) Boolean fullSearch,
+ @Parameter(description = SCADA_FIRST_PARAM_DESCRIPTION)
+ @RequestParam(required = false) Boolean scadaFirst) throws ThingsboardException {
PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder);
+ WidgetsBundleFilter widgetsBundleFilter = WidgetsBundleFilter.builder()
+ .tenantId(getTenantId())
+ .fullSearch(fullSearch != null && fullSearch)
+ .scadaFirst(scadaFirst != null && scadaFirst)
+ .build();
if (Authority.SYS_ADMIN.equals(getCurrentUser().getAuthority())) {
- return checkNotNull(widgetsBundleService.findSystemWidgetsBundlesByPageLink(getTenantId(), fullSearch != null && fullSearch, pageLink));
+ return checkNotNull(widgetsBundleService.findSystemWidgetsBundlesByPageLink(widgetsBundleFilter, pageLink));
} else {
- TenantId tenantId = getCurrentUser().getTenantId();
if (tenantOnly != null && tenantOnly) {
- return checkNotNull(widgetsBundleService.findTenantWidgetsBundlesByTenantIdAndPageLink(tenantId, fullSearch != null && fullSearch, pageLink));
+ return checkNotNull(widgetsBundleService.findTenantWidgetsBundlesByTenantIdAndPageLink(widgetsBundleFilter, pageLink));
} else {
- return checkNotNull(widgetsBundleService.findAllTenantWidgetsBundlesByTenantIdAndPageLink(tenantId, fullSearch != null && fullSearch, pageLink));
+ return checkNotNull(widgetsBundleService.findAllTenantWidgetsBundlesByTenantIdAndPageLink(widgetsBundleFilter, pageLink));
}
}
}
diff --git a/application/src/main/java/org/thingsboard/server/controller/plugin/TbWebSocketHandler.java b/application/src/main/java/org/thingsboard/server/controller/plugin/TbWebSocketHandler.java
index f386cc8e42..7fbe040091 100644
--- a/application/src/main/java/org/thingsboard/server/controller/plugin/TbWebSocketHandler.java
+++ b/application/src/main/java/org/thingsboard/server/controller/plugin/TbWebSocketHandler.java
@@ -18,6 +18,7 @@ package org.thingsboard.server.controller.plugin;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.RemovalCause;
+import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import jakarta.websocket.RemoteEndpoint;
import jakarta.websocket.SendHandler;
@@ -39,6 +40,7 @@ import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.adapter.NativeWebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler;
import org.thingsboard.common.util.JacksonUtil;
+import org.thingsboard.server.cache.limits.RateLimitService;
import org.thingsboard.server.common.data.TenantProfile;
import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
import org.thingsboard.server.common.data.id.CustomerId;
@@ -48,7 +50,6 @@ import org.thingsboard.server.common.data.limit.LimitedApi;
import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration;
import org.thingsboard.server.config.WebSocketConfiguration;
import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
-import org.thingsboard.server.cache.limits.RateLimitService;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.security.auth.jwt.JwtAuthenticationProvider;
import org.thingsboard.server.service.security.exception.JwtExpiredTokenException;
@@ -65,7 +66,6 @@ import org.thingsboard.server.service.ws.WsCommandsWrapper;
import org.thingsboard.server.service.ws.notification.cmd.NotificationCmdsWrapper;
import org.thingsboard.server.service.ws.telemetry.cmd.TelemetryCmdsWrapper;
-import javax.annotation.PostConstruct;
import java.io.IOException;
import java.security.InvalidParameterException;
import java.util.Optional;
diff --git a/application/src/main/java/org/thingsboard/server/exception/ThingsboardErrorResponseHandler.java b/application/src/main/java/org/thingsboard/server/exception/ThingsboardErrorResponseHandler.java
index fd73a424a3..256302f30e 100644
--- a/application/src/main/java/org/thingsboard/server/exception/ThingsboardErrorResponseHandler.java
+++ b/application/src/main/java/org/thingsboard/server/exception/ThingsboardErrorResponseHandler.java
@@ -92,6 +92,7 @@ public class ThingsboardErrorResponseHandler extends ResponseEntityExceptionHand
errorCodeToStatusMap.put(ThingsboardErrorCode.TOO_MANY_REQUESTS, HttpStatus.TOO_MANY_REQUESTS);
errorCodeToStatusMap.put(ThingsboardErrorCode.TOO_MANY_UPDATES, HttpStatus.TOO_MANY_REQUESTS);
errorCodeToStatusMap.put(ThingsboardErrorCode.SUBSCRIPTION_VIOLATION, HttpStatus.FORBIDDEN);
+ errorCodeToStatusMap.put(ThingsboardErrorCode.VERSION_CONFLICT, HttpStatus.CONFLICT);
}
private static ThingsboardErrorCode statusToErrorCode(HttpStatus status) {
diff --git a/application/src/main/java/org/thingsboard/server/service/component/AnnotationComponentDiscoveryService.java b/application/src/main/java/org/thingsboard/server/service/component/AnnotationComponentDiscoveryService.java
index 63ee2cb9eb..67d1e40ffa 100644
--- a/application/src/main/java/org/thingsboard/server/service/component/AnnotationComponentDiscoveryService.java
+++ b/application/src/main/java/org/thingsboard/server/service/component/AnnotationComponentDiscoveryService.java
@@ -17,6 +17,7 @@ package org.thingsboard.server.service.component;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
+import jakarta.annotation.PostConstruct;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
@@ -41,7 +42,6 @@ import org.thingsboard.server.common.data.plugin.ComponentType;
import org.thingsboard.server.common.data.rule.RuleChainType;
import org.thingsboard.server.dao.component.ComponentDescriptorService;
-import jakarta.annotation.PostConstruct;
import java.lang.annotation.Annotation;
import java.util.ArrayList;
import java.util.Arrays;
diff --git a/application/src/main/java/org/thingsboard/server/service/component/ComponentDiscoveryService.java b/application/src/main/java/org/thingsboard/server/service/component/ComponentDiscoveryService.java
index 973ce17574..b3f9faa711 100644
--- a/application/src/main/java/org/thingsboard/server/service/component/ComponentDiscoveryService.java
+++ b/application/src/main/java/org/thingsboard/server/service/component/ComponentDiscoveryService.java
@@ -19,9 +19,7 @@ import org.thingsboard.server.common.data.plugin.ComponentDescriptor;
import org.thingsboard.server.common.data.plugin.ComponentType;
import org.thingsboard.server.common.data.rule.RuleChainType;
-import java.util.Collection;
import java.util.List;
-import java.util.Map;
import java.util.Optional;
import java.util.Set;
diff --git a/application/src/main/java/org/thingsboard/server/service/device/DeviceProvisionServiceImpl.java b/application/src/main/java/org/thingsboard/server/service/device/DeviceProvisionServiceImpl.java
index bedf9c7537..f5b6411bf7 100644
--- a/application/src/main/java/org/thingsboard/server/service/device/DeviceProvisionServiceImpl.java
+++ b/application/src/main/java/org/thingsboard/server/service/device/DeviceProvisionServiceImpl.java
@@ -240,7 +240,7 @@ public class DeviceProvisionServiceImpl implements DeviceProvisionService {
return deviceCredentialsService.updateDeviceCredentials(tenantId, deviceCredentials);
}
- private ListenableFuture> saveProvisionStateAttribute(Device device) {
+ private ListenableFuture> saveProvisionStateAttribute(Device device) {
return attributesService.save(device.getTenantId(), device.getId(), AttributeScope.SERVER_SCOPE,
Collections.singletonList(new BaseAttributeKvEntry(new StringDataEntry(DEVICE_PROVISION_STATE, PROVISIONED_STATE),
System.currentTimeMillis())));
diff --git a/application/src/main/java/org/thingsboard/server/service/edge/DefaultEdgeNotificationService.java b/application/src/main/java/org/thingsboard/server/service/edge/DefaultEdgeNotificationService.java
deleted file mode 100644
index f29255e414..0000000000
--- a/application/src/main/java/org/thingsboard/server/service/edge/DefaultEdgeNotificationService.java
+++ /dev/null
@@ -1,222 +0,0 @@
-/**
- * Copyright © 2016-2024 The Thingsboard Authors
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.thingsboard.server.service.edge;
-
-import com.fasterxml.jackson.databind.node.ObjectNode;
-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.context.ApplicationEventPublisher;
-import org.springframework.stereotype.Service;
-import org.thingsboard.common.util.JacksonUtil;
-import org.thingsboard.common.util.ThingsBoardExecutors;
-import org.thingsboard.server.common.data.audit.ActionType;
-import org.thingsboard.server.common.data.edge.Edge;
-import org.thingsboard.server.common.data.edge.EdgeEventType;
-import org.thingsboard.server.common.data.id.RuleChainId;
-import org.thingsboard.server.common.data.id.TenantId;
-import org.thingsboard.server.common.msg.queue.TbCallback;
-import org.thingsboard.server.dao.edge.EdgeService;
-import org.thingsboard.server.dao.eventsourcing.ActionEntityEvent;
-import org.thingsboard.server.gen.transport.TransportProtos;
-import org.thingsboard.server.queue.util.TbCoreComponent;
-import org.thingsboard.server.service.edge.rpc.processor.alarm.AlarmEdgeProcessor;
-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.customer.CustomerEdgeProcessor;
-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;
-import org.thingsboard.server.service.edge.rpc.processor.edge.EdgeProcessor;
-import org.thingsboard.server.service.edge.rpc.processor.entityview.EntityViewEdgeProcessor;
-import org.thingsboard.server.service.edge.rpc.processor.notification.NotificationEdgeProcessor;
-import org.thingsboard.server.service.edge.rpc.processor.oauth2.OAuth2EdgeProcessor;
-import org.thingsboard.server.service.edge.rpc.processor.ota.OtaPackageEdgeProcessor;
-import org.thingsboard.server.service.edge.rpc.processor.queue.QueueEdgeProcessor;
-import org.thingsboard.server.service.edge.rpc.processor.relation.RelationEdgeProcessor;
-import org.thingsboard.server.service.edge.rpc.processor.resource.ResourceEdgeProcessor;
-import org.thingsboard.server.service.edge.rpc.processor.rule.RuleChainEdgeProcessor;
-import org.thingsboard.server.service.edge.rpc.processor.tenant.TenantEdgeProcessor;
-import org.thingsboard.server.service.edge.rpc.processor.tenant.TenantProfileEdgeProcessor;
-import org.thingsboard.server.service.edge.rpc.processor.user.UserEdgeProcessor;
-import org.thingsboard.server.service.edge.rpc.processor.widget.WidgetBundleEdgeProcessor;
-import org.thingsboard.server.service.edge.rpc.processor.widget.WidgetTypeEdgeProcessor;
-
-import java.util.UUID;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.TimeUnit;
-
-@Service
-@TbCoreComponent
-@Slf4j
-public class DefaultEdgeNotificationService implements EdgeNotificationService {
-
- public static final String EDGE_IS_ROOT_BODY_KEY = "isRoot";
-
- @Autowired
- private EdgeService edgeService;
-
- @Autowired
- private EdgeProcessor edgeProcessor;
-
- @Autowired
- private AssetEdgeProcessor assetProcessor;
-
- @Autowired
- private AssetProfileEdgeProcessor assetProfileEdgeProcessor;
-
- @Autowired
- private DeviceEdgeProcessor deviceProcessor;
-
- @Autowired
- private DeviceProfileEdgeProcessor deviceProfileEdgeProcessor;
-
- @Autowired
- private EntityViewEdgeProcessor entityViewProcessor;
-
- @Autowired
- private DashboardEdgeProcessor dashboardProcessor;
-
- @Autowired
- private RuleChainEdgeProcessor ruleChainProcessor;
-
- @Autowired
- private UserEdgeProcessor userProcessor;
-
- @Autowired
- private CustomerEdgeProcessor customerProcessor;
-
- @Autowired
- private OtaPackageEdgeProcessor otaPackageProcessor;
-
- @Autowired
- private WidgetBundleEdgeProcessor widgetBundleProcessor;
-
- @Autowired
- private WidgetTypeEdgeProcessor widgetTypeProcessor;
-
- @Autowired
- private QueueEdgeProcessor queueProcessor;
-
- @Autowired
- private TenantEdgeProcessor tenantEdgeProcessor;
-
- @Autowired
- private TenantProfileEdgeProcessor tenantProfileEdgeProcessor;
-
- @Autowired
- private AlarmEdgeProcessor alarmProcessor;
-
- @Autowired
- private RelationEdgeProcessor relationProcessor;
-
- @Autowired
- private ResourceEdgeProcessor resourceEdgeProcessor;
-
- @Autowired
- private NotificationEdgeProcessor notificationEdgeProcessor;
-
- @Autowired
- private OAuth2EdgeProcessor oAuth2EdgeProcessor;
-
- @Autowired
- protected ApplicationEventPublisher eventPublisher;
-
- @Value("${actors.system.edge_dispatcher_pool_size:4}")
- private int edgeDispatcherSize;
-
- private ExecutorService executor;
-
- @PostConstruct
- public void initExecutor() {
- executor = ThingsBoardExecutors.newWorkStealingPool(edgeDispatcherSize, "edge-notifications");
- }
-
- @PreDestroy
- public void shutdownExecutor() {
- if (executor != null) {
- executor.shutdownNow();
- }
- }
-
- @Override
- public Edge setEdgeRootRuleChain(TenantId tenantId, Edge edge, RuleChainId ruleChainId) {
- edge.setRootRuleChainId(ruleChainId);
- Edge savedEdge = edgeService.saveEdge(edge);
- ObjectNode isRootBody = JacksonUtil.newObjectNode();
- isRootBody.put(EDGE_IS_ROOT_BODY_KEY, Boolean.TRUE);
- eventPublisher.publishEvent(ActionEntityEvent.builder().tenantId(tenantId).edgeId(edge.getId()).entityId(ruleChainId)
- .body(JacksonUtil.toString(isRootBody)).actionType(ActionType.UPDATED).build());
- return savedEdge;
- }
-
- @Override
- public void pushNotificationToEdge(TransportProtos.EdgeNotificationMsgProto edgeNotificationMsg, TbCallback callback) {
- TenantId tenantId = TenantId.fromUUID(new UUID(edgeNotificationMsg.getTenantIdMSB(), edgeNotificationMsg.getTenantIdLSB()));
- log.debug("[{}] Pushing notification to edge {}", tenantId, edgeNotificationMsg);
- final long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(60);
- try {
- executor.submit(() -> {
- try {
- if (deadline < System.nanoTime()) {
- log.warn("[{}] Skipping notification message because deadline reached {}", tenantId, edgeNotificationMsg);
- return;
- }
- EdgeEventType type = EdgeEventType.valueOf(edgeNotificationMsg.getType());
- switch (type) {
- case EDGE -> edgeProcessor.processEdgeNotification(tenantId, edgeNotificationMsg);
- case ASSET -> assetProcessor.processEntityNotification(tenantId, edgeNotificationMsg);
- case ASSET_PROFILE -> assetProfileEdgeProcessor.processEntityNotification(tenantId, edgeNotificationMsg);
- case DEVICE -> deviceProcessor.processEntityNotification(tenantId, edgeNotificationMsg);
- case DEVICE_PROFILE -> deviceProfileEdgeProcessor.processEntityNotification(tenantId, edgeNotificationMsg);
- case ENTITY_VIEW -> entityViewProcessor.processEntityNotification(tenantId, edgeNotificationMsg);
- case DASHBOARD -> dashboardProcessor.processEntityNotification(tenantId, edgeNotificationMsg);
- case RULE_CHAIN -> ruleChainProcessor.processEntityNotification(tenantId, edgeNotificationMsg);
- case USER -> userProcessor.processEntityNotification(tenantId, edgeNotificationMsg);
- case CUSTOMER -> customerProcessor.processCustomerNotification(tenantId, edgeNotificationMsg);
- case OTA_PACKAGE -> otaPackageProcessor.processEntityNotification(tenantId, edgeNotificationMsg);
- case WIDGETS_BUNDLE -> widgetBundleProcessor.processEntityNotification(tenantId, edgeNotificationMsg);
- case WIDGET_TYPE -> widgetTypeProcessor.processEntityNotification(tenantId, edgeNotificationMsg);
- case QUEUE -> queueProcessor.processEntityNotification(tenantId, edgeNotificationMsg);
- case ALARM -> alarmProcessor.processAlarmNotification(tenantId, edgeNotificationMsg);
- case ALARM_COMMENT -> alarmProcessor.processAlarmCommentNotification(tenantId, edgeNotificationMsg);
- case RELATION -> relationProcessor.processRelationNotification(tenantId, edgeNotificationMsg);
- case TENANT -> tenantEdgeProcessor.processEntityNotification(tenantId, edgeNotificationMsg);
- case TENANT_PROFILE -> tenantProfileEdgeProcessor.processEntityNotification(tenantId, edgeNotificationMsg);
- case NOTIFICATION_RULE, NOTIFICATION_TARGET, NOTIFICATION_TEMPLATE ->
- notificationEdgeProcessor.processEntityNotification(tenantId, edgeNotificationMsg);
- case TB_RESOURCE -> resourceEdgeProcessor.processEntityNotification(tenantId, edgeNotificationMsg);
- case OAUTH2 -> oAuth2EdgeProcessor.processOAuth2Notification(tenantId, edgeNotificationMsg);
- default -> log.warn("[{}] Edge event type [{}] is not designed to be pushed to edge", tenantId, type);
- }
- } catch (Exception e) {
- callBackFailure(tenantId, edgeNotificationMsg, callback, e);
- }
- });
- callback.onSuccess();
- } catch (Exception e) {
- callBackFailure(tenantId, edgeNotificationMsg, callback, e);
- }
- }
-
- private void callBackFailure(TenantId tenantId, TransportProtos.EdgeNotificationMsgProto edgeNotificationMsg, TbCallback callback, Throwable throwable) {
- log.error("[{}] Can't push to edge updates, edgeNotificationMsg [{}]", tenantId, edgeNotificationMsg, throwable);
- callback.onFailure(throwable);
- }
-
-}
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 489fc6b6b8..f0f41e3083 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,13 +29,13 @@ import org.thingsboard.server.dao.customer.CustomerService;
import org.thingsboard.server.dao.dashboard.DashboardService;
import org.thingsboard.server.dao.device.DeviceProfileService;
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.entityview.EntityViewService;
import org.thingsboard.server.dao.notification.NotificationRuleService;
import org.thingsboard.server.dao.notification.NotificationTargetService;
import org.thingsboard.server.dao.notification.NotificationTemplateService;
-import org.thingsboard.server.dao.oauth2.OAuth2Service;
import org.thingsboard.server.dao.ota.OtaPackageService;
import org.thingsboard.server.dao.queue.QueueService;
import org.thingsboard.server.dao.resource.ResourceService;
@@ -48,6 +48,7 @@ import org.thingsboard.server.dao.widget.WidgetTypeService;
import org.thingsboard.server.dao.widget.WidgetsBundleService;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.edge.rpc.EdgeEventStorageSettings;
+import org.thingsboard.server.service.edge.rpc.EdgeRpcService;
import org.thingsboard.server.service.edge.rpc.constructor.edge.EdgeMsgConstructor;
import org.thingsboard.server.service.edge.rpc.processor.alarm.AlarmEdgeProcessor;
import org.thingsboard.server.service.edge.rpc.processor.alarm.AlarmEdgeProcessorFactory;
@@ -97,6 +98,9 @@ public class EdgeContextComponent {
@Autowired
private EdgeService edgeService;
+ @Autowired(required = false)
+ private EdgeRpcService edgeRpcService;
+
@Autowired
private EdgeEventService edgeEventService;
@@ -167,7 +171,7 @@ public class EdgeContextComponent {
private NotificationTemplateService notificationTemplateService;
@Autowired
- private OAuth2Service oAuth2Service;
+ private DomainService domainService;
@Autowired
private RateLimitService rateLimitService;
@@ -224,19 +228,19 @@ public class EdgeContextComponent {
private AdminSettingsEdgeProcessor adminSettingsProcessor;
@Autowired
- private OtaPackageEdgeProcessor otaPackageEdgeProcessor;
+ private OtaPackageEdgeProcessor otaPackageProcessor;
@Autowired
- private QueueEdgeProcessor queueEdgeProcessor;
+ private QueueEdgeProcessor queueProcessor;
@Autowired
- private TenantEdgeProcessor tenantEdgeProcessor;
+ private TenantEdgeProcessor tenantProcessor;
@Autowired
- private TenantProfileEdgeProcessor tenantProfileEdgeProcessor;
+ private TenantProfileEdgeProcessor tenantProfileProcessor;
@Autowired
- private ResourceEdgeProcessor resourceEdgeProcessor;
+ private ResourceEdgeProcessor resourceProcessor;
@Autowired
private NotificationEdgeProcessor notificationEdgeProcessor;
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 3de4c95cc3..840750a781 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
@@ -17,6 +17,7 @@ package org.thingsboard.server.service.edge;
import com.fasterxml.jackson.databind.node.NullNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
+import jakarta.annotation.PostConstruct;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
@@ -32,10 +33,10 @@ 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.domain.Domain;
import org.thingsboard.server.common.data.edge.EdgeEventActionType;
import org.thingsboard.server.common.data.edge.EdgeEventType;
import org.thingsboard.server.common.data.id.TenantId;
-import org.thingsboard.server.common.data.oauth2.OAuth2Info;
import org.thingsboard.server.common.data.relation.EntityRelation;
import org.thingsboard.server.common.data.relation.RelationTypeGroup;
import org.thingsboard.server.common.data.rule.RuleChain;
@@ -49,8 +50,6 @@ import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent;
import org.thingsboard.server.dao.tenant.TenantService;
import org.thingsboard.server.dao.user.UserServiceImpl;
-import javax.annotation.PostConstruct;
-
/**
* This event listener does not support async event processing because relay on ThreadLocal
* Another possible approach is to implement a special annotation and a bunch of classes similar to TransactionalApplicationListener
@@ -76,7 +75,7 @@ public class EdgeEventSourcingListener {
@PostConstruct
public void init() {
- log.info("EdgeEventSourcingListener initiated");
+ log.debug("EdgeEventSourcingListener initiated");
}
@TransactionalEventListener(fallbackExecution = true)
@@ -131,8 +130,7 @@ public class EdgeEventSourcingListener {
@TransactionalEventListener(fallbackExecution = true)
public void handleEvent(ActionEntityEvent> event) {
- if (EntityType.DEVICE.equals(event.getEntityId().getEntityType())
- && ActionType.ASSIGNED_TO_TENANT.equals(event.getActionType())) {
+ if (EntityType.DEVICE.equals(event.getEntityId().getEntityType()) && ActionType.ASSIGNED_TO_TENANT.equals(event.getActionType())) {
return;
}
try {
@@ -182,7 +180,8 @@ public class EdgeEventSourcingListener {
return false;
}
if (oldEntity != null) {
- User oldUser = (User) oldEntity;
+ user = JacksonUtil.clone(user);
+ User oldUser = JacksonUtil.clone((User) oldEntity);
cleanUpUserAdditionalInfo(oldUser);
cleanUpUserAdditionalInfo(user);
return !user.equals(oldUser);
@@ -203,11 +202,12 @@ public class EdgeEventSourcingListener {
return !event.getCreated();
case API_USAGE_STATE, EDGE:
return false;
+ case DOMAIN:
+ if (entity instanceof Domain domain) {
+ return domain.isPropagateToEdge();
+ }
}
}
- if (entity instanceof OAuth2Info oAuth2Info) {
- return oAuth2Info.isEdgeEnabled();
- }
// Default: If the entity doesn't match any of the conditions, consider it as valid.
return true;
}
@@ -226,13 +226,12 @@ public class EdgeEventSourcingListener {
user.setAdditionalInfo(additionalInfo);
}
}
+ user.setVersion(null);
}
private EdgeEventType getEdgeEventTypeForEntityEvent(Object entity) {
if (entity instanceof AlarmComment) {
return EdgeEventType.ALARM_COMMENT;
- } else if (entity instanceof OAuth2Info) {
- return EdgeEventType.OAUTH2;
}
return null;
}
@@ -240,8 +239,6 @@ public class EdgeEventSourcingListener {
private String getBodyMsgForEntityEvent(Object entity) {
if (entity instanceof AlarmComment) {
return JacksonUtil.toString(entity);
- } else if (entity instanceof OAuth2Info) {
- return JacksonUtil.toString(entity);
}
return null;
}
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
new file mode 100644
index 0000000000..851f59c3ab
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/edge/RelatedEdgesSourcingListener.java
@@ -0,0 +1,80 @@
+/**
+ * Copyright © 2016-2024 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.service.edge;
+
+import jakarta.annotation.PostConstruct;
+import jakarta.annotation.PreDestroy;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+import org.springframework.transaction.event.TransactionalEventListener;
+import org.thingsboard.common.util.ThingsBoardThreadFactory;
+import org.thingsboard.server.dao.edge.RelatedEdgesService;
+import org.thingsboard.server.dao.eventsourcing.ActionEntityEvent;
+import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent;
+
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+@Component
+@RequiredArgsConstructor
+@Slf4j
+public class RelatedEdgesSourcingListener {
+
+ private final RelatedEdgesService relatedEdgesService;
+
+ private ExecutorService executorService;
+
+ @PostConstruct
+ public void init() {
+ log.debug("RelatedEdgesSourcingListener initiated");
+ executorService = Executors.newSingleThreadExecutor(ThingsBoardThreadFactory.forName("related-edges-listener"));
+ }
+
+ @PreDestroy
+ public void destroy() {
+ log.debug("RelatedEdgesSourcingListener destroy");
+ if (executorService != null && !executorService.isShutdown()) {
+ executorService.shutdown();
+ }
+ }
+
+ @TransactionalEventListener(fallbackExecution = true)
+ public void handleEvent(ActionEntityEvent> event) {
+ executorService.submit(() -> {
+ try {
+ switch (event.getActionType()) {
+ case ASSIGNED_TO_EDGE, UNASSIGNED_FROM_EDGE ->
+ relatedEdgesService.publishRelatedEdgeIdsEvictEvent(event.getTenantId(), event.getEntityId());
+ }
+ } catch (Exception e) {
+ log.error("[{}] failed to process ActionEntityEvent: {}", event.getTenantId(), event, e);
+ }
+ });
+ }
+
+ @TransactionalEventListener(fallbackExecution = true)
+ public void handleEvent(DeleteEntityEvent> event) {
+ executorService.submit(() -> {
+ try {
+ relatedEdgesService.publishRelatedEdgeIdsEvictEvent(event.getTenantId(), event.getEntityId());
+ } catch (Exception e) {
+ log.error("[{}] failed to process DeleteEntityEvent: {}", event.getTenantId(), event, e);
+ }
+ });
+ }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/edge/instructions/DefaultEdgeInstallInstructionsService.java b/application/src/main/java/org/thingsboard/server/service/edge/instructions/DefaultEdgeInstallInstructionsService.java
index 64f96aa4e4..7c8489067c 100644
--- a/application/src/main/java/org/thingsboard/server/service/edge/instructions/DefaultEdgeInstallInstructionsService.java
+++ b/application/src/main/java/org/thingsboard/server/service/edge/instructions/DefaultEdgeInstallInstructionsService.java
@@ -94,4 +94,5 @@ public class DefaultEdgeInstallInstructionsService extends BaseEdgeInstallUpgrad
protected String getBaseDirName() {
return INSTALL_DIR;
}
+
}
diff --git a/application/src/main/java/org/thingsboard/server/service/edge/instructions/DefaultEdgeUpgradeInstructionsService.java b/application/src/main/java/org/thingsboard/server/service/edge/instructions/DefaultEdgeUpgradeInstructionsService.java
index 275de6bafe..2f783c98ab 100644
--- a/application/src/main/java/org/thingsboard/server/service/edge/instructions/DefaultEdgeUpgradeInstructionsService.java
+++ b/application/src/main/java/org/thingsboard/server/service/edge/instructions/DefaultEdgeUpgradeInstructionsService.java
@@ -74,7 +74,8 @@ public class DefaultEdgeUpgradeInstructionsService extends BaseEdgeInstallUpgrad
Optional attributeKvEntryOpt = attributesService.find(tenantId, edgeId, AttributeScope.SERVER_SCOPE, DataConstants.EDGE_VERSION_ATTR_KEY).get();
if (attributeKvEntryOpt.isPresent()) {
String edgeVersionFormatted = convertEdgeVersionToDocsFormat(attributeKvEntryOpt.get().getValueAsString());
- return isVersionGreaterOrEqualsThan(edgeVersionFormatted, "3.6.0") && !isVersionGreaterOrEqualsThan(edgeVersionFormatted, appVersion);
+ String appVersionFormatted = appVersion.replace("-SNAPSHOT", "");
+ return isVersionGreaterOrEqualsThan(edgeVersionFormatted, "3.6.0") && !isVersionGreaterOrEqualsThan(edgeVersionFormatted, appVersionFormatted);
}
return false;
}
diff --git a/application/src/main/java/org/thingsboard/server/service/edge/instructions/EdgeUpgradeInstructionsService.java b/application/src/main/java/org/thingsboard/server/service/edge/instructions/EdgeUpgradeInstructionsService.java
index 2517b615a8..47bde24bc7 100644
--- a/application/src/main/java/org/thingsboard/server/service/edge/instructions/EdgeUpgradeInstructionsService.java
+++ b/application/src/main/java/org/thingsboard/server/service/edge/instructions/EdgeUpgradeInstructionsService.java
@@ -31,4 +31,5 @@ public interface EdgeUpgradeInstructionsService {
void setAppVersion(String version);
boolean isUpgradeAvailable(TenantId tenantId, EdgeId edgeId) throws Exception;
+
}
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 04e6fe2185..5d45e1c3d5 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
@@ -28,14 +28,17 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.common.util.ThingsBoardThreadFactory;
+import org.thingsboard.server.cache.TbTransactionalCache;
import org.thingsboard.server.cluster.TbClusterService;
import org.thingsboard.server.common.data.AttributeScope;
import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.ResourceUtils;
import org.thingsboard.server.common.data.edge.Edge;
+import org.thingsboard.server.common.data.edge.EdgeEvent;
import org.thingsboard.server.common.data.id.EdgeId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.kv.BasicTsKvEntry;
@@ -47,12 +50,14 @@ import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.TbMsgDataType;
import org.thingsboard.server.common.msg.TbMsgMetaData;
import org.thingsboard.server.common.msg.edge.EdgeEventUpdateMsg;
+import org.thingsboard.server.common.msg.edge.EdgeHighPriorityMsg;
import org.thingsboard.server.common.msg.edge.EdgeSessionMsg;
import org.thingsboard.server.common.msg.edge.FromEdgeSyncResponse;
import org.thingsboard.server.common.msg.edge.ToEdgeSyncRequest;
import org.thingsboard.server.gen.edge.v1.EdgeRpcServiceGrpc;
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.util.TbCoreComponent;
import org.thingsboard.server.service.edge.EdgeContextComponent;
import org.thingsboard.server.service.state.DefaultDeviceStateService;
@@ -84,7 +89,6 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i
private final ConcurrentMap sessionNewEventsLocks = new ConcurrentHashMap<>();
private final Map sessionNewEvents = new HashMap<>();
private final ConcurrentMap> sessionEdgeEventChecks = new ConcurrentHashMap<>();
-
private final ConcurrentMap> localSyncEdgeRequests = new ConcurrentHashMap<>();
@Value("${edges.rpc.port}")
@@ -111,7 +115,11 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i
@Value("${edges.send_scheduler_pool_size}")
private int sendSchedulerPoolSize;
+ @Value("${edges.max_high_priority_queue_size_per_session:10000}")
+ private int maxHighPriorityQueueSizePerSession;
+
@Autowired
+ @Lazy
private EdgeContextComponent ctx;
@Autowired
@@ -120,6 +128,12 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i
@Autowired
private TbClusterService clusterService;
+ @Autowired
+ private TbServiceInfoProvider serviceInfoProvider;
+
+ @Autowired
+ private TbTransactionalCache edgeIdServiceIdCache;
+
private Server server;
private ScheduledExecutorService edgeEventProcessingExecutorService;
@@ -188,79 +202,100 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i
@Override
public StreamObserver handleMsgs(StreamObserver outputStream) {
- return new EdgeGrpcSession(ctx, outputStream, this::onEdgeConnect, this::onEdgeDisconnect, sendDownlinkExecutorService, this.maxInboundMessageSize).getInputStream();
+ return new EdgeGrpcSession(ctx,
+ outputStream,
+ this::onEdgeConnect,
+ this::onEdgeDisconnect,
+ sendDownlinkExecutorService,
+ this.maxInboundMessageSize,
+ this.maxHighPriorityQueueSizePerSession).getInputStream();
}
@Override
public void onToEdgeSessionMsg(TenantId tenantId, EdgeSessionMsg msg) {
- executorService.execute(() -> {
- switch (msg.getMsgType()) {
- case EDGE_EVENT_UPDATE_TO_EDGE_SESSION_MSG:
- EdgeEventUpdateMsg edgeEventUpdateMsg = (EdgeEventUpdateMsg) msg;
- log.trace("[{}] onToEdgeSessionMsg [{}]", tenantId, msg);
- onEdgeEvent(tenantId, edgeEventUpdateMsg.getEdgeId());
- break;
- case EDGE_SYNC_REQUEST_TO_EDGE_SESSION_MSG:
- ToEdgeSyncRequest toEdgeSyncRequest = (ToEdgeSyncRequest) msg;
- log.trace("[{}] toEdgeSyncRequest [{}]", tenantId, msg);
- startSyncProcess(tenantId, toEdgeSyncRequest.getEdgeId(), toEdgeSyncRequest.getId());
- break;
- case EDGE_SYNC_RESPONSE_FROM_EDGE_SESSION_MSG:
- FromEdgeSyncResponse fromEdgeSyncResponse = (FromEdgeSyncResponse) msg;
- log.trace("[{}] fromEdgeSyncResponse [{}]", tenantId, msg);
- processSyncResponse(fromEdgeSyncResponse);
- break;
+ switch (msg.getMsgType()) {
+ case EDGE_HIGH_PRIORITY_TO_EDGE_SESSION_MSG -> {
+ EdgeHighPriorityMsg edgeHighPriorityMsg = (EdgeHighPriorityMsg) msg;
+ log.trace("[{}] edgeEventMsg [{}]", tenantId, msg);
+ onEdgeHighPriorityEvent(edgeHighPriorityMsg);
}
- });
+ case EDGE_EVENT_UPDATE_TO_EDGE_SESSION_MSG -> {
+ EdgeEventUpdateMsg edgeEventUpdateMsg = (EdgeEventUpdateMsg) msg;
+ log.trace("[{}] onToEdgeEventUpdateMsg [{}]", tenantId, msg);
+ onEdgeEventUpdate(tenantId, edgeEventUpdateMsg.getEdgeId());
+ }
+ case EDGE_SYNC_REQUEST_TO_EDGE_SESSION_MSG -> {
+ ToEdgeSyncRequest toEdgeSyncRequest = (ToEdgeSyncRequest) msg;
+ log.trace("[{}] toEdgeSyncRequest [{}]", tenantId, msg);
+ startSyncProcess(tenantId, toEdgeSyncRequest.getEdgeId(), toEdgeSyncRequest.getId(), toEdgeSyncRequest.getServiceId());
+ }
+ case EDGE_SYNC_RESPONSE_FROM_EDGE_SESSION_MSG -> {
+ FromEdgeSyncResponse fromEdgeSyncResponse = (FromEdgeSyncResponse) msg;
+ log.trace("[{}] fromEdgeSyncResponse [{}]", tenantId, msg);
+ processSyncResponse(fromEdgeSyncResponse);
+ }
+ }
}
@Override
public void updateEdge(TenantId tenantId, Edge edge) {
- executorService.execute(() -> {
- EdgeGrpcSession session = sessions.get(edge.getId());
- if (session != null && session.isConnected()) {
- log.debug("[{}] Updating configuration for edge [{}] [{}]", tenantId, edge.getName(), edge.getId());
- session.onConfigurationUpdate(edge);
- } else {
- log.debug("[{}] Session doesn't exist for edge [{}] [{}]", tenantId, edge.getName(), edge.getId());
- }
- });
+ EdgeGrpcSession session = sessions.get(edge.getId());
+ if (session != null && session.isConnected()) {
+ log.debug("[{}] Updating configuration for edge [{}] [{}]", tenantId, edge.getName(), edge.getId());
+ session.onConfigurationUpdate(edge);
+ } else {
+ log.debug("[{}] Session doesn't exist for edge [{}] [{}]", tenantId, edge.getName(), edge.getId());
+ }
}
@Override
public void deleteEdge(TenantId tenantId, EdgeId edgeId) {
- executorService.execute(() -> {
- EdgeGrpcSession session = sessions.get(edgeId);
- if (session != null && session.isConnected()) {
- log.info("[{}] Closing and removing session for edge [{}]", tenantId, edgeId);
- session.close();
- sessions.remove(edgeId);
- final Lock newEventLock = sessionNewEventsLocks.computeIfAbsent(edgeId, id -> new ReentrantLock());
- newEventLock.lock();
- try {
- sessionNewEvents.remove(edgeId);
- } finally {
- newEventLock.unlock();
- }
- cancelScheduleEdgeEventsCheck(edgeId);
- }
- });
- }
-
- private void onEdgeEvent(TenantId tenantId, EdgeId edgeId) {
EdgeGrpcSession session = sessions.get(edgeId);
if (session != null && session.isConnected()) {
- log.trace("[{}] onEdgeEvent [{}]", tenantId, edgeId.getId());
+ log.info("[{}] Closing and removing session for edge [{}]", tenantId, edgeId);
+ session.close();
+ sessions.remove(edgeId);
final Lock newEventLock = sessionNewEventsLocks.computeIfAbsent(edgeId, id -> new ReentrantLock());
newEventLock.lock();
try {
- if (Boolean.FALSE.equals(sessionNewEvents.get(edgeId))) {
- log.trace("[{}] set session new events flag to true [{}]", tenantId, edgeId.getId());
- sessionNewEvents.put(edgeId, true);
- }
+ sessionNewEvents.remove(edgeId);
} finally {
newEventLock.unlock();
}
+ cancelScheduleEdgeEventsCheck(edgeId);
+ }
+ }
+
+ private void onEdgeEventUpdate(TenantId tenantId, EdgeId edgeId) {
+ EdgeGrpcSession session = sessions.get(edgeId);
+ if (session != null && session.isConnected()) {
+ log.trace("[{}] onEdgeEventUpdate [{}]", tenantId, edgeId.getId());
+ updateSessionEventsFlag(tenantId, edgeId);
+ }
+ }
+
+ private void onEdgeHighPriorityEvent(EdgeHighPriorityMsg msg) {
+ TenantId tenantId = msg.getTenantId();
+ EdgeEvent edgeEvent = msg.getEdgeEvent();
+ EdgeId edgeId = edgeEvent.getEdgeId();
+ EdgeGrpcSession session = sessions.get(edgeId);
+ if (session != null && session.isConnected()) {
+ log.trace("[{}] onEdgeEvent [{}]", tenantId, edgeId);
+ session.addEventToHighPriorityQueue(edgeEvent);
+ updateSessionEventsFlag(tenantId, edgeId);
+ }
+ }
+
+ private void updateSessionEventsFlag(TenantId tenantId, EdgeId edgeId) {
+ final Lock newEventLock = sessionNewEventsLocks.computeIfAbsent(edgeId, id -> new ReentrantLock());
+ newEventLock.lock();
+ try {
+ if (Boolean.FALSE.equals(sessionNewEvents.get(edgeId))) {
+ log.trace("[{}] set session new events flag to true [{}]", tenantId, edgeId.getId());
+ sessionNewEvents.put(edgeId, true);
+ }
+ } finally {
+ newEventLock.unlock();
}
}
@@ -279,30 +314,42 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i
save(tenantId, edgeId, DefaultDeviceStateService.ACTIVITY_STATE, true);
long lastConnectTs = System.currentTimeMillis();
save(tenantId, edgeId, DefaultDeviceStateService.LAST_CONNECT_TIME, lastConnectTs);
+ edgeIdServiceIdCache.put(edgeId, serviceInfoProvider.getServiceId());
pushRuleEngineMessage(tenantId, edge, lastConnectTs, TbMsgType.CONNECT_EVENT);
cancelScheduleEdgeEventsCheck(edgeId);
scheduleEdgeEventsCheck(edgeGrpcSession);
}
- private void startSyncProcess(TenantId tenantId, EdgeId edgeId, UUID requestId) {
+ private void startSyncProcess(TenantId tenantId, EdgeId edgeId, UUID requestId, String requestServiceId) {
EdgeGrpcSession session = sessions.get(edgeId);
if (session != null) {
- boolean success = false;
- if (session.isConnected()) {
- session.startSyncProcess(true);
- success = true;
+ if (!session.isSyncCompleted()) {
+ clusterService.pushEdgeSyncResponseToCore(new FromEdgeSyncResponse(requestId, tenantId, edgeId, false, "Sync process is active at the moment"), requestServiceId);
+ } else {
+ boolean success = false;
+ if (session.isConnected()) {
+ session.startSyncProcess(true);
+ success = true;
+ }
+ clusterService.pushEdgeSyncResponseToCore(new FromEdgeSyncResponse(requestId, tenantId, edgeId, success, ""), requestServiceId);
}
- clusterService.pushEdgeSyncResponseToCore(new FromEdgeSyncResponse(requestId, tenantId, edgeId, success));
}
}
@Override
- public void processSyncRequest(ToEdgeSyncRequest request, Consumer responseConsumer) {
- log.trace("[{}][{}] Processing sync edge request [{}]", request.getTenantId(), request.getId(), request.getEdgeId());
+ public void processSyncRequest(TenantId tenantId, EdgeId edgeId, Consumer responseConsumer) {
+ ToEdgeSyncRequest request = new ToEdgeSyncRequest(UUID.randomUUID(), tenantId, edgeId, serviceInfoProvider.getServiceId());
+
UUID requestId = request.getId();
- localSyncEdgeRequests.put(requestId, responseConsumer);
- clusterService.pushEdgeSyncRequestToCore(request);
- scheduleSyncRequestTimeout(request, requestId);
+ EdgeGrpcSession session = sessions.get(request.getEdgeId());
+ if (session != null && !session.isSyncCompleted()) {
+ responseConsumer.accept(new FromEdgeSyncResponse(requestId, request.getTenantId(), request.getEdgeId(), false, "Sync process is active at the moment"));
+ } else {
+ log.trace("[{}][{}] Processing sync edge request [{}], serviceId [{}]", request.getTenantId(), request.getId(), request.getEdgeId(), request.getServiceId());
+ localSyncEdgeRequests.put(requestId, responseConsumer);
+ clusterService.pushEdgeSyncRequestToEdge(request);
+ scheduleSyncRequestTimeout(request, requestId);
+ }
}
private void scheduleSyncRequestTimeout(ToEdgeSyncRequest request, UUID requestId) {
@@ -312,7 +359,7 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i
Consumer consumer = localSyncEdgeRequests.remove(requestId);
if (consumer != null) {
log.trace("[{}] timeout for processing sync edge request.", requestId);
- consumer.accept(new FromEdgeSyncResponse(requestId, request.getTenantId(), request.getEdgeId(), false));
+ consumer.accept(new FromEdgeSyncResponse(requestId, request.getTenantId(), request.getEdgeId(), false, "Edge is not connected"));
}
}, 20, TimeUnit.SECONDS);
}
@@ -406,6 +453,7 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i
} else {
log.debug("[{}] edge session [{}] is not available anymore, nothing to remove. most probably this session is already outdated!", edgeId, sessionId);
}
+ edgeIdServiceIdCache.evict(edgeId);
}
private void save(TenantId tenantId, EdgeId edgeId, String key, long value) {
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 55e8e051ab..c397807c5f 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
@@ -42,6 +42,7 @@ import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.page.SortOrder;
import org.thingsboard.server.common.data.page.TimePageLink;
+import org.thingsboard.server.common.msg.edge.EdgeEventUpdateMsg;
import org.thingsboard.server.gen.edge.v1.AlarmCommentUpdateMsg;
import org.thingsboard.server.gen.edge.v1.AlarmUpdateMsg;
import org.thingsboard.server.gen.edge.v1.AssetProfileUpdateMsg;
@@ -96,6 +97,7 @@ import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
+import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
@@ -106,6 +108,7 @@ import java.util.function.BiConsumer;
public final class EdgeGrpcSession implements Closeable {
private static final ReentrantLock downlinkMsgLock = new ReentrantLock();
+ private static final ConcurrentLinkedQueue highPriorityQueue = new ConcurrentLinkedQueue<>();
private static final int MAX_DOWNLINK_ATTEMPTS = 10; // max number of attemps to send downlink message if edge connected
@@ -125,7 +128,7 @@ public final class EdgeGrpcSession implements Closeable {
private StreamObserver inputStream;
private StreamObserver outputStream;
private boolean connected;
- private boolean syncCompleted;
+ private volatile boolean syncCompleted;
private Long newStartTs;
private Long previousStartTs;
@@ -137,11 +140,12 @@ public final class EdgeGrpcSession implements Closeable {
private int maxInboundMessageSize;
private int clientMaxInboundMessageSize;
+ private int maxHighPriorityQueueSizePerSession;
private ScheduledExecutorService sendDownlinkExecutorService;
EdgeGrpcSession(EdgeContextComponent ctx, StreamObserver outputStream, BiConsumer sessionOpenListener,
- BiConsumer sessionCloseListener, ScheduledExecutorService sendDownlinkExecutorService, int maxInboundMessageSize) {
+ BiConsumer sessionCloseListener, ScheduledExecutorService sendDownlinkExecutorService, int maxInboundMessageSize, int maxHighPriorityQueueSizePerSession) {
this.sessionId = UUID.randomUUID();
this.ctx = ctx;
this.outputStream = outputStream;
@@ -149,6 +153,7 @@ public final class EdgeGrpcSession implements Closeable {
this.sessionCloseListener = sessionCloseListener;
this.sendDownlinkExecutorService = sendDownlinkExecutorService;
this.maxInboundMessageSize = maxInboundMessageSize;
+ this.maxHighPriorityQueueSizePerSession = maxHighPriorityQueueSizePerSession;
initInputStream();
}
@@ -223,7 +228,7 @@ public final class EdgeGrpcSession implements Closeable {
}
public void startSyncProcess(boolean fullSync) {
- log.trace("[{}][{}][{}] Staring edge sync process", this.tenantId, edge.getId(), this.sessionId);
+ log.info("[{}][{}][{}] Staring edge sync process", this.tenantId, edge.getId(), this.sessionId);
syncCompleted = false;
interruptGeneralProcessingOnSync();
doSync(new EdgeSyncCursor(ctx, edge, fullSync));
@@ -247,6 +252,7 @@ public final class EdgeGrpcSession implements Closeable {
}
}, ctx.getGrpcCallbackExecutorService());
} else {
+ log.info("[{}][{}] sync process completed", this.tenantId, edge.getId());
DownlinkMsg syncCompleteDownlinkMsg = DownlinkMsg.newBuilder()
.setDownlinkMsgId(EdgeUtils.nextPositiveInt())
.setSyncCompletedMsg(SyncCompletedMsg.newBuilder().build())
@@ -254,18 +260,23 @@ public final class EdgeGrpcSession implements Closeable {
Futures.addCallback(sendDownlinkMsgsPack(Collections.singletonList(syncCompleteDownlinkMsg)), new FutureCallback<>() {
@Override
public void onSuccess(Boolean isInterrupted) {
- syncCompleted = true;
- ctx.getClusterService().onEdgeEventUpdate(edge.getTenantId(), edge.getId());
+ markSyncCompletedSendEdgeEventUpdate();
}
@Override
public void onFailure(Throwable t) {
log.error("[{}][{}] Exception during sending sync complete", tenantId, edge.getId(), t);
+ markSyncCompletedSendEdgeEventUpdate();
}
}, ctx.getGrpcCallbackExecutorService());
}
}
+ private void markSyncCompletedSendEdgeEventUpdate() {
+ syncCompleted = true;
+ ctx.getClusterService().onEdgeEventUpdate(new EdgeEventUpdateMsg(edge.getTenantId(), edge.getId()));
+ }
+
private void onUplinkMsg(UplinkMsg uplinkMsg) {
if (isRateLimitViolated(uplinkMsg)) {
return;
@@ -325,7 +336,11 @@ public final class EdgeGrpcSession implements Closeable {
}
private void sendDownlinkMsg(ResponseMsg downlinkMsg) {
- log.trace("[{}][{}] Sending downlink msg [{}]", this.tenantId, this.sessionId, downlinkMsg);
+ if (downlinkMsg.getDownlinkMsg().getWidgetTypeUpdateMsgCount() > 0) {
+ log.trace("[{}][{}] Sending downlink widgetTypeUpdateMsg, downlinkMsgId = {}", this.tenantId, this.sessionId, downlinkMsg.getDownlinkMsg().getDownlinkMsgId());
+ } else {
+ log.trace("[{}][{}] Sending downlink msg [{}]", this.tenantId, this.sessionId, downlinkMsg);
+ }
if (isConnected()) {
downlinkMsgLock.lock();
try {
@@ -337,7 +352,7 @@ public final class EdgeGrpcSession implements Closeable {
} finally {
downlinkMsgLock.unlock();
}
- log.trace("[{}][{}] Response msg successfully sent [{}]", this.tenantId, this.sessionId, downlinkMsg);
+ log.trace("[{}][{}] Response msg successfully sent. downlinkMsgId = {}", this.tenantId, this.sessionId, downlinkMsg.getDownlinkMsg().getDownlinkMsgId());
}
}
@@ -371,10 +386,10 @@ public final class EdgeGrpcSession implements Closeable {
@Override
public void onSuccess(@Nullable Pair newStartTsAndSeqId) {
if (newStartTsAndSeqId != null) {
- ListenableFuture> updateFuture = updateQueueStartTsAndSeqId(newStartTsAndSeqId);
+ ListenableFuture> updateFuture = updateQueueStartTsAndSeqId(newStartTsAndSeqId);
Futures.addCallback(updateFuture, new FutureCallback<>() {
@Override
- public void onSuccess(@Nullable List list) {
+ public void onSuccess(@Nullable List list) {
log.debug("[{}][{}] queue offset was updated [{}]", tenantId, sessionId, newStartTsAndSeqId);
if (fetcher.isSeqIdNewCycleStarted()) {
seqIdEnd = fetcher.getSeqIdEnd();
@@ -424,6 +439,9 @@ public final class EdgeGrpcSession implements Closeable {
private void processEdgeEvents(EdgeEventFetcher fetcher, PageLink pageLink, SettableFuture> result) {
try {
+ if (!highPriorityQueue.isEmpty()) {
+ processHighPriorityEvents();
+ }
PageData pageData = fetcher.fetchEdgeEvents(edge.getTenantId(), edge, pageLink);
if (isConnected() && !pageData.getData().isEmpty()) {
log.trace("[{}][{}][{}] event(s) are going to be processed.", this.tenantId, this.sessionId, pageData.getData().size());
@@ -467,6 +485,20 @@ public final class EdgeGrpcSession implements Closeable {
}
}
+ private void processHighPriorityEvents() {
+ try {
+ List highPriorityEvents = new ArrayList<>();
+ EdgeEvent event;
+ while ((event = highPriorityQueue.poll()) != null) {
+ highPriorityEvents.add(event);
+ }
+ List downlinkMsgsPack = convertToDownlinkMsgsPack(highPriorityEvents);
+ sendDownlinkMsgsPack(downlinkMsgsPack).get();
+ } catch (Exception e) {
+ log.error("[{}] Failed to process high priority events", this.sessionId, e);
+ }
+ }
+
private ListenableFuture sendDownlinkMsgsPack(List downlinkMsgsPack) {
interruptPreviousSendDownlinkMsgsTask();
@@ -551,9 +583,15 @@ public final class EdgeGrpcSession implements Closeable {
DownlinkMsg downlinkMsg = null;
try {
switch (edgeEvent.getAction()) {
- case UPDATED, ADDED, DELETED, ASSIGNED_TO_EDGE, UNASSIGNED_FROM_EDGE, ALARM_ACK, ALARM_CLEAR, ALARM_DELETE, CREDENTIALS_UPDATED, RELATION_ADD_OR_UPDATE, RELATION_DELETED, CREDENTIALS_REQUEST, RPC_CALL, ASSIGNED_TO_CUSTOMER, UNASSIGNED_FROM_CUSTOMER, ADDED_COMMENT, UPDATED_COMMENT, DELETED_COMMENT -> {
+ case UPDATED, ADDED, DELETED, ASSIGNED_TO_EDGE, UNASSIGNED_FROM_EDGE, ALARM_ACK, ALARM_CLEAR, ALARM_DELETE,
+ CREDENTIALS_UPDATED, RELATION_ADD_OR_UPDATE, RELATION_DELETED, RPC_CALL,
+ ASSIGNED_TO_CUSTOMER, UNASSIGNED_FROM_CUSTOMER, ADDED_COMMENT, UPDATED_COMMENT, DELETED_COMMENT -> {
downlinkMsg = convertEntityEventToDownlink(edgeEvent);
- log.trace("[{}][{}] entity message processed [{}]", this.tenantId, this.sessionId, downlinkMsg);
+ if (downlinkMsg != null && downlinkMsg.getWidgetTypeUpdateMsgCount() > 0) {
+ log.trace("[{}][{}] widgetTypeUpdateMsg message processed, downlinkMsgId = {}", this.tenantId, this.sessionId, downlinkMsg.getDownlinkMsgId());
+ } else {
+ log.trace("[{}][{}] entity message processed [{}]", this.tenantId, this.sessionId, downlinkMsg);
+ }
}
case ATTRIBUTES_UPDATED, POST_ATTRIBUTES, ATTRIBUTES_DELETED, TIMESERIES_UPDATED ->
downlinkMsg = ctx.getTelemetryProcessor().convertTelemetryEventToDownlink(edge, edgeEvent);
@@ -626,7 +664,7 @@ public final class EdgeGrpcSession implements Closeable {
return startSeqId;
}
- private ListenableFuture> updateQueueStartTsAndSeqId(Pair pair) {
+ private ListenableFuture> updateQueueStartTsAndSeqId(Pair pair) {
this.newStartTs = pair.getFirst();
this.newStartSeqId = pair.getSecond();
log.trace("[{}] updateQueueStartTsAndSeqId [{}][{}][{}]", this.sessionId, edge.getId(), this.newStartTs, this.newStartSeqId);
@@ -674,23 +712,25 @@ public final class EdgeGrpcSession implements Closeable {
case ADMIN_SETTINGS:
return ctx.getAdminSettingsProcessor().convertAdminSettingsEventToDownlink(edgeEvent, this.edgeVersion);
case OTA_PACKAGE:
- return ctx.getOtaPackageEdgeProcessor().convertOtaPackageEventToDownlink(edgeEvent, this.edgeVersion);
+ return ctx.getOtaPackageProcessor().convertOtaPackageEventToDownlink(edgeEvent, this.edgeVersion);
case TB_RESOURCE:
- return ctx.getResourceEdgeProcessor().convertResourceEventToDownlink(edgeEvent, this.edgeVersion);
+ return ctx.getResourceProcessor().convertResourceEventToDownlink(edgeEvent, this.edgeVersion);
case QUEUE:
- return ctx.getQueueEdgeProcessor().convertQueueEventToDownlink(edgeEvent, this.edgeVersion);
+ return ctx.getQueueProcessor().convertQueueEventToDownlink(edgeEvent, this.edgeVersion);
case TENANT:
- return ctx.getTenantEdgeProcessor().convertTenantEventToDownlink(edgeEvent, this.edgeVersion);
+ return ctx.getTenantProcessor().convertTenantEventToDownlink(edgeEvent, this.edgeVersion);
case TENANT_PROFILE:
- return ctx.getTenantProfileEdgeProcessor().convertTenantProfileEventToDownlink(edgeEvent, this.edgeVersion);
+ return ctx.getTenantProfileProcessor().convertTenantProfileEventToDownlink(edgeEvent, this.edgeVersion);
case NOTIFICATION_RULE:
return ctx.getNotificationEdgeProcessor().convertNotificationRuleToDownlink(edgeEvent);
case NOTIFICATION_TARGET:
return ctx.getNotificationEdgeProcessor().convertNotificationTargetToDownlink(edgeEvent);
case NOTIFICATION_TEMPLATE:
return ctx.getNotificationEdgeProcessor().convertNotificationTemplateToDownlink(edgeEvent);
- case OAUTH2:
- return ctx.getOAuth2EdgeProcessor().convertOAuth2EventToDownlink(edgeEvent);
+ case OAUTH2_CLIENT:
+ return ctx.getOAuth2EdgeProcessor().convertOAuth2ClientEventToDownlink(edgeEvent, this.edgeVersion);
+ case DOMAIN:
+ return ctx.getOAuth2EdgeProcessor().convertOAuth2DomainEventToDownlink(edgeEvent, this.edgeVersion);
default:
log.warn("[{}] Unsupported edge event type [{}]", this.tenantId, edgeEvent);
return null;
@@ -903,4 +943,15 @@ public final class EdgeGrpcSession implements Closeable {
}
}
+ public void addEventToHighPriorityQueue(EdgeEvent edgeEvent) {
+ while (highPriorityQueue.size() > maxHighPriorityQueueSizePerSession) {
+ EdgeEvent oldestHighPriority = highPriorityQueue.poll();
+ if (oldestHighPriority != null) {
+ log.warn("[{}][{}][{}] High priority queue is full. Removing oldest high priority event from queue {}",
+ this.tenantId, edge.getId(), this.sessionId, oldestHighPriority);
+ }
+ }
+ highPriorityQueue.add(edgeEvent);
+ }
+
}
diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeRpcService.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeRpcService.java
index 12203db0c2..7c832ca760 100644
--- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeRpcService.java
+++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeRpcService.java
@@ -32,5 +32,6 @@ public interface EdgeRpcService {
void deleteEdge(TenantId tenantId, EdgeId edgeId);
- void processSyncRequest(ToEdgeSyncRequest request, Consumer responseConsumer);
+ void processSyncRequest(TenantId tenantId, EdgeId edgeId, Consumer responseConsumer);
+
}
diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeSessionState.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeSessionState.java
index 6218b1f9a6..3f925c3787 100644
--- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeSessionState.java
+++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeSessionState.java
@@ -30,4 +30,5 @@ public class EdgeSessionState {
private final Map pendingMsgsMap = Collections.synchronizedMap(new LinkedHashMap<>());
private SettableFuture sendDownlinkMsgsFuture;
private ScheduledFuture> scheduledSendDownlinkTask;
+
}
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 6321991586..7426207a56 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
@@ -15,6 +15,7 @@
*/
package org.thingsboard.server.service.edge.rpc;
+import lombok.Getter;
import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.common.data.id.EntityId;
@@ -53,6 +54,7 @@ public class EdgeSyncCursor {
private final List fetchers = new LinkedList<>();
+ @Getter
private int currentIdx = 0;
public EdgeSyncCursor(EdgeContextComponent ctx, Edge edge, boolean fullSync) {
@@ -62,12 +64,12 @@ public class EdgeSyncCursor {
fetchers.add(new RuleChainsEdgeEventFetcher(ctx.getRuleChainService()));
fetchers.add(new AdminSettingsEdgeEventFetcher(ctx.getAdminSettingsService()));
fetchers.add(new TenantAdminUsersEdgeEventFetcher(ctx.getUserService()));
- Customer publicCustomer = ctx.getCustomerService().findOrCreatePublicCustomer(edge.getTenantId());
- fetchers.add(new CustomerEdgeEventFetcher(publicCustomer.getId()));
- if (edge.getCustomerId() != null && !EntityId.NULL_UUID.equals(edge.getCustomerId().getId())) {
- fetchers.add(new CustomerEdgeEventFetcher(edge.getCustomerId()));
- fetchers.add(new CustomerUsersEdgeEventFetcher(ctx.getUserService(), edge.getCustomerId()));
- }
+ }
+ Customer publicCustomer = ctx.getCustomerService().findOrCreatePublicCustomer(edge.getTenantId());
+ fetchers.add(new CustomerEdgeEventFetcher(publicCustomer.getId()));
+ if (edge.getCustomerId() != null && !EntityId.NULL_UUID.equals(edge.getCustomerId().getId())) {
+ fetchers.add(new CustomerEdgeEventFetcher(edge.getCustomerId()));
+ fetchers.add(new CustomerUsersEdgeEventFetcher(ctx.getUserService(), edge.getCustomerId()));
}
fetchers.add(new DashboardsEdgeEventFetcher(ctx.getDashboardService()));
fetchers.add(new DefaultProfilesEdgeEventFetcher(ctx.getDeviceProfileService(), ctx.getAssetProfileService()));
@@ -86,7 +88,7 @@ public class EdgeSyncCursor {
fetchers.add(new TenantWidgetsBundlesEdgeEventFetcher(ctx.getWidgetsBundleService()));
fetchers.add(new OtaPackagesEdgeEventFetcher(ctx.getOtaPackageService()));
fetchers.add(new TenantResourcesEdgeEventFetcher(ctx.getResourceService()));
- fetchers.add(new OAuth2EdgeEventFetcher(ctx.getOAuth2Service()));
+ fetchers.add(new OAuth2EdgeEventFetcher(ctx.getDomainService()));
}
}
@@ -102,9 +104,4 @@ public class EdgeSyncCursor {
currentIdx++;
return edgeEventFetcher;
}
-
- public int getCurrentIdx() {
- return currentIdx;
- }
-
}
diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/oauth2/OAuth2MsgConstructor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/oauth2/OAuth2MsgConstructor.java
index c628b042a2..7ed25924b6 100644
--- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/oauth2/OAuth2MsgConstructor.java
+++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/oauth2/OAuth2MsgConstructor.java
@@ -17,16 +17,44 @@ package org.thingsboard.server.service.edge.rpc.constructor.oauth2;
import org.springframework.stereotype.Component;
import org.thingsboard.common.util.JacksonUtil;
-import org.thingsboard.server.common.data.oauth2.OAuth2Info;
-import org.thingsboard.server.gen.edge.v1.OAuth2UpdateMsg;
+import org.thingsboard.server.common.data.domain.DomainInfo;
+import org.thingsboard.server.common.data.id.DomainId;
+import org.thingsboard.server.common.data.id.OAuth2ClientId;
+import org.thingsboard.server.common.data.oauth2.OAuth2Client;
+import org.thingsboard.server.gen.edge.v1.OAuth2ClientUpdateMsg;
+import org.thingsboard.server.gen.edge.v1.OAuth2DomainUpdateMsg;
+import org.thingsboard.server.gen.edge.v1.UpdateMsgType;
import org.thingsboard.server.queue.util.TbCoreComponent;
@Component
@TbCoreComponent
public class OAuth2MsgConstructor {
- public OAuth2UpdateMsg constructOAuth2UpdateMsg(OAuth2Info oAuth2Info) {
- return OAuth2UpdateMsg.newBuilder().setEntity(JacksonUtil.toString(oAuth2Info)).build();
+ public OAuth2ClientUpdateMsg constructOAuth2ClientUpdateMsg(UpdateMsgType msgType, OAuth2Client oAuth2Client) {
+ return OAuth2ClientUpdateMsg.newBuilder().setMsgType(msgType).setEntity(JacksonUtil.toString(oAuth2Client))
+ .setIdMSB(oAuth2Client.getId().getId().getMostSignificantBits())
+ .setIdLSB(oAuth2Client.getId().getId().getLeastSignificantBits()).build();
+ }
+
+ public OAuth2ClientUpdateMsg constructOAuth2ClientDeleteMsg(OAuth2ClientId oAuth2ClientId) {
+ return OAuth2ClientUpdateMsg.newBuilder()
+ .setMsgType(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE)
+ .setIdMSB(oAuth2ClientId.getId().getMostSignificantBits())
+ .setIdLSB(oAuth2ClientId.getId().getLeastSignificantBits()).build();
+ }
+
+ public OAuth2DomainUpdateMsg constructOAuth2DomainUpdateMsg(UpdateMsgType msgType, DomainInfo domainInfo) {
+ return OAuth2DomainUpdateMsg.newBuilder().setMsgType(msgType).setEntity(JacksonUtil.toString(domainInfo))
+ .setIdMSB(domainInfo.getId().getId().getMostSignificantBits())
+ .setIdLSB(domainInfo.getId().getId().getLeastSignificantBits()).build();
+ }
+
+ public OAuth2DomainUpdateMsg constructOAuth2DomainDeleteMsg(DomainId domainId) {
+ return OAuth2DomainUpdateMsg.newBuilder()
+ .setMsgType(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE)
+ .setIdMSB(domainId.getId().getMostSignificantBits())
+ .setIdLSB(domainId.getId().getLeastSignificantBits())
+ .build();
}
}
diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/BasePageableEdgeEventFetcher.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/BasePageableEdgeEventFetcher.java
index 747dc5722f..40fc3559ed 100644
--- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/BasePageableEdgeEventFetcher.java
+++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/BasePageableEdgeEventFetcher.java
@@ -35,7 +35,7 @@ public abstract class BasePageableEdgeEventFetcher implements EdgeEventFetche
@Override
public PageData fetchEdgeEvents(TenantId tenantId, Edge edge, PageLink pageLink) {
- log.trace("[{}] start fetching edge events [{}]", tenantId, edge.getId());
+ log.trace("[{}][{}] start fetching edge events [{}], pageLink {}", getClass().getSimpleName(), tenantId, edge.getId(), pageLink);
PageData entities = fetchEntities(tenantId, edge, pageLink);
List result = new ArrayList<>();
if (!entities.getData().isEmpty()) {
diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/OAuth2EdgeEventFetcher.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/OAuth2EdgeEventFetcher.java
index 982c1a8eaf..c93135be37 100644
--- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/OAuth2EdgeEventFetcher.java
+++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/OAuth2EdgeEventFetcher.java
@@ -17,43 +17,32 @@ package org.thingsboard.server.service.edge.rpc.fetch;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
-import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.EdgeUtils;
+import org.thingsboard.server.common.data.domain.DomainInfo;
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.TenantId;
-import org.thingsboard.server.common.data.oauth2.OAuth2Info;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
-import org.thingsboard.server.dao.oauth2.OAuth2Service;
-
-import java.util.ArrayList;
-import java.util.List;
+import org.thingsboard.server.dao.domain.DomainService;
@AllArgsConstructor
@Slf4j
-public class OAuth2EdgeEventFetcher implements EdgeEventFetcher {
+public class OAuth2EdgeEventFetcher extends BasePageableEdgeEventFetcher {
- private final OAuth2Service oAuth2Service;
+ private final DomainService domainService;
@Override
- public PageLink getPageLink(int pageSize) {
- return null;
+ PageData fetchEntities(TenantId tenantId, Edge edge, PageLink pageLink) {
+ return domainService.findDomainInfosByTenantId(TenantId.SYS_TENANT_ID, pageLink);
}
@Override
- public PageData fetchEdgeEvents(TenantId tenantId, Edge edge, PageLink pageLink) {
- OAuth2Info oAuth2Info = oAuth2Service.findOAuth2Info();
- if (!oAuth2Info.isEdgeEnabled()) {
- return new PageData<>();
- }
- List result = new ArrayList<>();
- result.add(EdgeUtils.constructEdgeEvent(tenantId, edge.getId(), EdgeEventType.OAUTH2,
- EdgeEventActionType.ADDED, null, JacksonUtil.valueToTree(oAuth2Info)));
- // returns PageData object to be in sync with other fetchers
- return new PageData<>(result, 1, result.size(), false);
+ EdgeEvent constructEdgeEvent(TenantId tenantId, Edge edge, DomainInfo domainInfo) {
+ return EdgeUtils.constructEdgeEvent(TenantId.SYS_TENANT_ID, edge.getId(), EdgeEventType.DOMAIN,
+ EdgeEventActionType.ADDED, domainInfo.getId(), null);
}
}
diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/RuleChainsEdgeEventFetcher.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/RuleChainsEdgeEventFetcher.java
index 5569c30018..328072aeea 100644
--- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/RuleChainsEdgeEventFetcher.java
+++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/RuleChainsEdgeEventFetcher.java
@@ -30,7 +30,7 @@ import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.rule.RuleChain;
import org.thingsboard.server.dao.rule.RuleChainService;
-import static org.thingsboard.server.service.edge.DefaultEdgeNotificationService.EDGE_IS_ROOT_BODY_KEY;
+import static org.thingsboard.server.dao.edge.EdgeServiceImpl.EDGE_IS_ROOT_BODY_KEY;
@Slf4j
@AllArgsConstructor
diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/SystemWidgetTypesEdgeEventFetcher.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/SystemWidgetTypesEdgeEventFetcher.java
index 7e957fd0fe..2cf47c6abd 100644
--- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/SystemWidgetTypesEdgeEventFetcher.java
+++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/SystemWidgetTypesEdgeEventFetcher.java
@@ -20,6 +20,7 @@ import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.widget.DeprecatedFilter;
+import org.thingsboard.server.common.data.widget.WidgetTypeFilter;
import org.thingsboard.server.common.data.widget.WidgetTypeInfo;
import org.thingsboard.server.dao.widget.WidgetTypeService;
@@ -32,6 +33,12 @@ public class SystemWidgetTypesEdgeEventFetcher extends BaseWidgetTypesEdgeEventF
@Override
protected PageData findWidgetTypes(TenantId tenantId, PageLink pageLink) {
- return widgetTypeService.findSystemWidgetTypesByPageLink(tenantId, false, DeprecatedFilter.ALL, null, pageLink);
+ return widgetTypeService.findSystemWidgetTypesByPageLink(
+ WidgetTypeFilter.builder()
+ .tenantId(tenantId)
+ .fullSearch(false)
+ .deprecatedFilter(DeprecatedFilter.ALL)
+ .widgetTypes(null).build(),
+ pageLink);
}
}
diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/SystemWidgetsBundlesEdgeEventFetcher.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/SystemWidgetsBundlesEdgeEventFetcher.java
index 5ec2af5fb2..1e6a345db7 100644
--- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/SystemWidgetsBundlesEdgeEventFetcher.java
+++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/SystemWidgetsBundlesEdgeEventFetcher.java
@@ -20,6 +20,7 @@ import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.widget.WidgetsBundle;
+import org.thingsboard.server.common.data.widget.WidgetsBundleFilter;
import org.thingsboard.server.dao.widget.WidgetsBundleService;
@Slf4j
@@ -31,6 +32,6 @@ public class SystemWidgetsBundlesEdgeEventFetcher extends BaseWidgetsBundlesEdge
@Override
protected PageData findWidgetsBundles(TenantId tenantId, PageLink pageLink) {
- return widgetsBundleService.findSystemWidgetsBundlesByPageLink(tenantId, false, pageLink);
+ return widgetsBundleService.findSystemWidgetsBundlesByPageLink(WidgetsBundleFilter.fromTenantId(tenantId), pageLink);
}
}
diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/TenantWidgetTypesEdgeEventFetcher.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/TenantWidgetTypesEdgeEventFetcher.java
index c2a971a0c0..62fac80fbf 100644
--- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/TenantWidgetTypesEdgeEventFetcher.java
+++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/TenantWidgetTypesEdgeEventFetcher.java
@@ -20,6 +20,7 @@ import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.widget.DeprecatedFilter;
+import org.thingsboard.server.common.data.widget.WidgetTypeFilter;
import org.thingsboard.server.common.data.widget.WidgetTypeInfo;
import org.thingsboard.server.dao.widget.WidgetTypeService;
@@ -31,6 +32,12 @@ public class TenantWidgetTypesEdgeEventFetcher extends BaseWidgetTypesEdgeEventF
}
@Override
protected PageData findWidgetTypes(TenantId tenantId, PageLink pageLink) {
- return widgetTypeService.findTenantWidgetTypesByTenantIdAndPageLink(tenantId, false, DeprecatedFilter.ALL, null, pageLink);
+ return widgetTypeService.findTenantWidgetTypesByTenantIdAndPageLink(
+ WidgetTypeFilter.builder()
+ .tenantId(tenantId)
+ .fullSearch(false)
+ .deprecatedFilter(DeprecatedFilter.ALL)
+ .widgetTypes(null).build(),
+ pageLink);
}
}
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 f46c07ab5a..3c18e02324 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
@@ -69,6 +69,7 @@ import org.thingsboard.server.dao.dashboard.DashboardService;
import org.thingsboard.server.dao.device.DeviceCredentialsService;
import org.thingsboard.server.dao.device.DeviceProfileService;
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.EdgeSynchronizationManager;
@@ -76,7 +77,7 @@ import org.thingsboard.server.dao.entityview.EntityViewService;
import org.thingsboard.server.dao.notification.NotificationRuleService;
import org.thingsboard.server.dao.notification.NotificationTargetService;
import org.thingsboard.server.dao.notification.NotificationTemplateService;
-import org.thingsboard.server.dao.oauth2.OAuth2Service;
+import org.thingsboard.server.dao.oauth2.OAuth2ClientService;
import org.thingsboard.server.dao.ota.OtaPackageService;
import org.thingsboard.server.dao.queue.QueueService;
import org.thingsboard.server.dao.relation.RelationService;
@@ -132,14 +133,14 @@ import java.util.UUID;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
+import static org.thingsboard.server.dao.edge.BaseRelatedEdgesService.RELATED_EDGES_CACHE_ITEMS;
+
@Slf4j
public abstract class BaseEdgeProcessor {
protected static final Lock deviceCreationLock = new ReentrantLock();
protected static final Lock assetCreationLock = new ReentrantLock();
- protected static final int DEFAULT_PAGE_SIZE = 100;
-
@Autowired
protected TelemetrySubscriptionService tsSubService;
@@ -240,7 +241,10 @@ public abstract class BaseEdgeProcessor {
protected NotificationTemplateService notificationTemplateService;
@Autowired
- protected OAuth2Service oAuth2Service;
+ protected OAuth2ClientService oAuth2ClientService;
+
+ @Autowired
+ protected DomainService domainService;
@Autowired
@Lazy
@@ -370,23 +374,20 @@ public abstract class BaseEdgeProcessor {
}, dbCallbackExecutorService);
}
- private boolean doSaveIfEdgeIsOffline(EdgeEventType type,
- EdgeEventActionType action) {
+ private boolean doSaveIfEdgeIsOffline(EdgeEventType type, EdgeEventActionType action) {
return switch (action) {
- case TIMESERIES_UPDATED, ALARM_ACK, ALARM_CLEAR, ALARM_ASSIGNED, ALARM_UNASSIGNED, CREDENTIALS_REQUEST, ADDED_COMMENT, UPDATED_COMMENT ->
+ case TIMESERIES_UPDATED, ALARM_ACK, ALARM_CLEAR, ALARM_ASSIGNED, ALARM_UNASSIGNED, ADDED_COMMENT, UPDATED_COMMENT ->
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;
+ ADMIN_SETTINGS, OTA_PACKAGE, QUEUE, RELATION, NOTIFICATION_TEMPLATE, NOTIFICATION_TARGET, NOTIFICATION_RULE -> true;
default -> false;
};
};
}
private ListenableFuture doSaveEdgeEvent(TenantId tenantId, EdgeId edgeId, EdgeEventType type, EdgeEventActionType action, EntityId entityId, JsonNode body) {
- log.debug("Pushing event to edge queue. tenantId [{}], edgeId [{}], type[{}], " +
- "action [{}], entityId [{}], body [{}]",
+ log.debug("Pushing event to edge queue. tenantId [{}], edgeId [{}], type[{}], action [{}], entityId [{}], body [{}]",
tenantId, edgeId, type, action, entityId, body);
EdgeEvent edgeEvent = EdgeUtils.constructEdgeEvent(tenantId, edgeId, type, action, entityId, body);
@@ -503,7 +504,7 @@ public abstract class BaseEdgeProcessor {
EdgeEventActionType actionType, EdgeId sourceEdgeId) {
List> futures = new ArrayList<>();
PageDataIterableByTenantIdEntityId edgeIds =
- new PageDataIterableByTenantIdEntityId<>(edgeService::findRelatedEdgeIdsByEntityId, tenantId, entityId, DEFAULT_PAGE_SIZE);
+ new PageDataIterableByTenantIdEntityId<>(edgeService::findRelatedEdgeIdsByEntityId, tenantId, entityId, RELATED_EDGES_CACHE_ITEMS);
for (EdgeId relatedEdgeId : edgeIds) {
if (!relatedEdgeId.equals(sourceEdgeId)) {
futures.add(saveEdgeEvent(tenantId, relatedEdgeId, type, actionType, entityId, null));
@@ -649,7 +650,7 @@ public abstract class BaseEdgeProcessor {
private boolean isEntityNotAssignedToEdge(TenantId tenantId, EntityId entityId, EdgeId edgeId) {
PageDataIterableByTenantIdEntityId edgeIds =
- new PageDataIterableByTenantIdEntityId<>(edgeService::findRelatedEdgeIdsByEntityId, tenantId, entityId, DEFAULT_PAGE_SIZE);
+ new PageDataIterableByTenantIdEntityId<>(edgeService::findRelatedEdgeIdsByEntityId, tenantId, entityId, RELATED_EDGES_CACHE_ITEMS);
for (EdgeId edgeId1 : edgeIds) {
if (edgeId1.equals(edgeId)) {
return false;
diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/alarm/AlarmEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/alarm/AlarmEdgeProcessor.java
index 22d76dc86e..009c5854cd 100644
--- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/alarm/AlarmEdgeProcessor.java
+++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/alarm/AlarmEdgeProcessor.java
@@ -43,6 +43,8 @@ import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
+import static org.thingsboard.server.dao.edge.BaseRelatedEdgesService.RELATED_EDGES_CACHE_ITEMS;
+
@Slf4j
public abstract class AlarmEdgeProcessor extends BaseAlarmProcessor implements AlarmProcessor {
@@ -148,7 +150,7 @@ public abstract class AlarmEdgeProcessor extends BaseAlarmProcessor implements A
EdgeEventType edgeEventType) {
List> futures = new ArrayList<>();
PageDataIterableByTenantIdEntityId edgeIds =
- new PageDataIterableByTenantIdEntityId<>(edgeService::findRelatedEdgeIdsByEntityId, tenantId, originatorId, DEFAULT_PAGE_SIZE);
+ new PageDataIterableByTenantIdEntityId<>(edgeService::findRelatedEdgeIdsByEntityId, tenantId, originatorId, RELATED_EDGES_CACHE_ITEMS);
for (EdgeId relatedEdgeId : edgeIds) {
if (!relatedEdgeId.equals(sourceEdgeId)) {
futures.add(saveEdgeEvent(tenantId, relatedEdgeId, edgeEventType, actionType, alarmId, body));
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 5d264cd83d..3e3c9dfb64 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
@@ -107,6 +107,7 @@ public abstract class AssetEdgeProcessor extends BaseAssetProcessor implements A
public DownlinkMsg convertAssetEventToDownlink(EdgeEvent edgeEvent, EdgeId edgeId, EdgeVersion edgeVersion) {
AssetId assetId = new AssetId(edgeEvent.getEntityId());
DownlinkMsg downlinkMsg = null;
+ var msgConstructor = (AssetMsgConstructor) assetMsgConstructorFactory.getMsgConstructorByEdgeVersion(edgeVersion);
switch (edgeEvent.getAction()) {
case ADDED, UPDATED, ASSIGNED_TO_EDGE, ASSIGNED_TO_CUSTOMER, UNASSIGNED_FROM_CUSTOMER -> {
Asset asset = assetService.findAssetById(edgeEvent.getTenantId(), assetId);
@@ -120,20 +121,15 @@ public abstract class AssetEdgeProcessor extends BaseAssetProcessor implements A
if (UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE.equals(msgType)) {
AssetProfile assetProfile = assetProfileService.findAssetProfileById(edgeEvent.getTenantId(), asset.getAssetProfileId());
assetProfile = checkIfAssetProfileDefaultFieldsAssignedToEdge(edgeEvent.getTenantId(), edgeId, assetProfile, edgeVersion);
- builder.addAssetProfileUpdateMsg(((AssetMsgConstructor) assetMsgConstructorFactory.getMsgConstructorByEdgeVersion(edgeVersion))
- .constructAssetProfileUpdatedMsg(msgType, assetProfile));
+ builder.addAssetProfileUpdateMsg(msgConstructor.constructAssetProfileUpdatedMsg(msgType, assetProfile));
}
downlinkMsg = builder.build();
}
}
- case DELETED, UNASSIGNED_FROM_EDGE -> {
- AssetUpdateMsg assetUpdateMsg = ((AssetMsgConstructor)
- assetMsgConstructorFactory.getMsgConstructorByEdgeVersion(edgeVersion)).constructAssetDeleteMsg(assetId);
- downlinkMsg = DownlinkMsg.newBuilder()
- .setDownlinkMsgId(EdgeUtils.nextPositiveInt())
- .addAssetUpdateMsg(assetUpdateMsg)
- .build();
- }
+ case DELETED, UNASSIGNED_FROM_EDGE -> downlinkMsg = DownlinkMsg.newBuilder()
+ .setDownlinkMsgId(EdgeUtils.nextPositiveInt())
+ .addAssetUpdateMsg(msgConstructor.constructAssetDeleteMsg(assetId))
+ .build();
}
return downlinkMsg;
}
diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/dashboard/DashboardEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/dashboard/DashboardEdgeProcessor.java
index aab35f89d8..f5f25b1c56 100644
--- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/dashboard/DashboardEdgeProcessor.java
+++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/dashboard/DashboardEdgeProcessor.java
@@ -100,27 +100,23 @@ public abstract class DashboardEdgeProcessor extends BaseDashboardProcessor impl
public DownlinkMsg convertDashboardEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) {
DashboardId dashboardId = new DashboardId(edgeEvent.getEntityId());
DownlinkMsg downlinkMsg = null;
+ var msgConstructor = (DashboardMsgConstructor) dashboardMsgConstructorFactory.getMsgConstructorByEdgeVersion(edgeVersion);
switch (edgeEvent.getAction()) {
case ADDED, UPDATED, ASSIGNED_TO_EDGE, ASSIGNED_TO_CUSTOMER, UNASSIGNED_FROM_CUSTOMER -> {
Dashboard dashboard = dashboardService.findDashboardById(edgeEvent.getTenantId(), dashboardId);
if (dashboard != null) {
UpdateMsgType msgType = getUpdateMsgType(edgeEvent.getAction());
- DashboardUpdateMsg dashboardUpdateMsg = ((DashboardMsgConstructor)
- dashboardMsgConstructorFactory.getMsgConstructorByEdgeVersion(edgeVersion)).constructDashboardUpdatedMsg(msgType, dashboard);
+ DashboardUpdateMsg dashboardUpdateMsg = msgConstructor.constructDashboardUpdatedMsg(msgType, dashboard);
downlinkMsg = DownlinkMsg.newBuilder()
.setDownlinkMsgId(EdgeUtils.nextPositiveInt())
.addDashboardUpdateMsg(dashboardUpdateMsg)
.build();
}
}
- case DELETED, UNASSIGNED_FROM_EDGE -> {
- DashboardUpdateMsg dashboardUpdateMsg = ((DashboardMsgConstructor)
- dashboardMsgConstructorFactory.getMsgConstructorByEdgeVersion(edgeVersion)).constructDashboardDeleteMsg(dashboardId);
- downlinkMsg = DownlinkMsg.newBuilder()
- .setDownlinkMsgId(EdgeUtils.nextPositiveInt())
- .addDashboardUpdateMsg(dashboardUpdateMsg)
- .build();
- }
+ case DELETED, UNASSIGNED_FROM_EDGE -> downlinkMsg = DownlinkMsg.newBuilder()
+ .setDownlinkMsgId(EdgeUtils.nextPositiveInt())
+ .addDashboardUpdateMsg(msgConstructor.constructDashboardDeleteMsg(dashboardId))
+ .build();
}
return downlinkMsg;
}
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 5e73034b1c..52f09971ab 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
@@ -43,7 +43,6 @@ import org.thingsboard.server.common.msg.TbMsgMetaData;
import org.thingsboard.server.common.msg.rpc.FromDeviceRpcResponse;
import org.thingsboard.server.common.msg.rpc.FromDeviceRpcResponseActorMsg;
import org.thingsboard.server.dao.exception.DataValidationException;
-import org.thingsboard.server.gen.edge.v1.DeviceCredentialsRequestMsg;
import org.thingsboard.server.gen.edge.v1.DeviceCredentialsUpdateMsg;
import org.thingsboard.server.gen.edge.v1.DeviceRpcCallMsg;
import org.thingsboard.server.gen.edge.v1.DeviceUpdateMsg;
@@ -70,7 +69,7 @@ public abstract class DeviceEdgeProcessor extends BaseDeviceProcessor implements
case ENTITY_CREATED_RPC_MESSAGE:
case ENTITY_UPDATED_RPC_MESSAGE:
saveOrUpdateDevice(tenantId, deviceId, deviceUpdateMsg, edge);
- return saveEdgeEvent(tenantId, edge.getId(), EdgeEventType.DEVICE, EdgeEventActionType.CREDENTIALS_REQUEST, deviceId, null);
+ return Futures.immediateFuture(null);
case ENTITY_DELETED_RPC_MESSAGE:
Device deviceToDelete = deviceService.findDeviceById(tenantId, deviceId);
if (deviceToDelete != null) {
@@ -217,6 +216,7 @@ public abstract class DeviceEdgeProcessor extends BaseDeviceProcessor implements
public DownlinkMsg convertDeviceEventToDownlink(EdgeEvent edgeEvent, EdgeId edgeId, EdgeVersion edgeVersion) {
DeviceId deviceId = new DeviceId(edgeEvent.getEntityId());
DownlinkMsg downlinkMsg = null;
+ var msgConstructor = (DeviceMsgConstructor) deviceMsgConstructorFactory.getMsgConstructorByEdgeVersion(edgeVersion);
switch (edgeEvent.getAction()) {
case ADDED:
case UPDATED:
@@ -226,65 +226,46 @@ public abstract class DeviceEdgeProcessor extends BaseDeviceProcessor implements
Device device = deviceService.findDeviceById(edgeEvent.getTenantId(), deviceId);
if (device != null) {
UpdateMsgType msgType = getUpdateMsgType(edgeEvent.getAction());
- DeviceUpdateMsg deviceUpdateMsg = ((DeviceMsgConstructor)
- deviceMsgConstructorFactory.getMsgConstructorByEdgeVersion(edgeVersion))
- .constructDeviceUpdatedMsg(msgType, device);
+ DeviceUpdateMsg deviceUpdateMsg = msgConstructor.constructDeviceUpdatedMsg(msgType, device);
DownlinkMsg.Builder builder = DownlinkMsg.newBuilder()
.setDownlinkMsgId(EdgeUtils.nextPositiveInt())
.addDeviceUpdateMsg(deviceUpdateMsg);
+ DeviceCredentials deviceCredentials = deviceCredentialsService.findDeviceCredentialsByDeviceId(edgeEvent.getTenantId(), deviceId);
+ if (deviceCredentials != null) {
+ DeviceCredentialsUpdateMsg deviceCredentialsUpdateMsg = msgConstructor.constructDeviceCredentialsUpdatedMsg(deviceCredentials);
+ builder.addDeviceCredentialsUpdateMsg(deviceCredentialsUpdateMsg).build();
+ }
if (UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE.equals(msgType)) {
DeviceProfile deviceProfile = deviceProfileService.findDeviceProfileById(edgeEvent.getTenantId(), device.getDeviceProfileId());
deviceProfile = checkIfDeviceProfileDefaultFieldsAssignedToEdge(edgeEvent.getTenantId(), edgeId, deviceProfile, edgeVersion);
- builder.addDeviceProfileUpdateMsg(((DeviceMsgConstructor)
- deviceMsgConstructorFactory.getMsgConstructorByEdgeVersion(edgeVersion))
- .constructDeviceProfileUpdatedMsg(msgType, deviceProfile));
+ builder.addDeviceProfileUpdateMsg(msgConstructor.constructDeviceProfileUpdatedMsg(msgType, deviceProfile));
}
downlinkMsg = builder.build();
}
break;
case DELETED:
case UNASSIGNED_FROM_EDGE:
- DeviceUpdateMsg deviceUpdateMsg = ((DeviceMsgConstructor)
- deviceMsgConstructorFactory.getMsgConstructorByEdgeVersion(edgeVersion)).constructDeviceDeleteMsg(deviceId);
downlinkMsg = DownlinkMsg.newBuilder()
.setDownlinkMsgId(EdgeUtils.nextPositiveInt())
- .addDeviceUpdateMsg(deviceUpdateMsg)
+ .addDeviceUpdateMsg(msgConstructor.constructDeviceDeleteMsg(deviceId))
.build();
break;
case CREDENTIALS_UPDATED:
DeviceCredentials deviceCredentials = deviceCredentialsService.findDeviceCredentialsByDeviceId(edgeEvent.getTenantId(), deviceId);
if (deviceCredentials != null) {
- DeviceCredentialsUpdateMsg deviceCredentialsUpdateMsg = ((DeviceMsgConstructor)
- deviceMsgConstructorFactory.getMsgConstructorByEdgeVersion(edgeVersion)).constructDeviceCredentialsUpdatedMsg(deviceCredentials);
downlinkMsg = DownlinkMsg.newBuilder()
.setDownlinkMsgId(EdgeUtils.nextPositiveInt())
- .addDeviceCredentialsUpdateMsg(deviceCredentialsUpdateMsg)
+ .addDeviceCredentialsUpdateMsg(msgConstructor.constructDeviceCredentialsUpdatedMsg(deviceCredentials))
.build();
}
break;
case RPC_CALL:
return DownlinkMsg.newBuilder()
.setDownlinkMsgId(EdgeUtils.nextPositiveInt())
- .addDeviceRpcCallMsg(((DeviceMsgConstructor)
- deviceMsgConstructorFactory.getMsgConstructorByEdgeVersion(edgeVersion))
- .constructDeviceRpcCallMsg(edgeEvent.getEntityId(), edgeEvent.getBody()))
+ .addDeviceRpcCallMsg(msgConstructor.constructDeviceRpcCallMsg(edgeEvent.getEntityId(), edgeEvent.getBody()))
.build();
- case CREDENTIALS_REQUEST:
- return convertCredentialsRequestEventToDownlink(edgeEvent);
}
return downlinkMsg;
}
- private DownlinkMsg convertCredentialsRequestEventToDownlink(EdgeEvent edgeEvent) {
- DeviceId deviceId = new DeviceId(edgeEvent.getEntityId());
- DeviceCredentialsRequestMsg deviceCredentialsRequestMsg = DeviceCredentialsRequestMsg.newBuilder()
- .setDeviceIdMSB(deviceId.getId().getMostSignificantBits())
- .setDeviceIdLSB(deviceId.getId().getLeastSignificantBits())
- .build();
- DownlinkMsg.Builder builder = DownlinkMsg.newBuilder()
- .setDownlinkMsgId(EdgeUtils.nextPositiveInt())
- .addDeviceCredentialsRequestMsg(deviceCredentialsRequestMsg);
- return builder.build();
- }
-
}
diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/profile/DeviceProfileEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/profile/DeviceProfileEdgeProcessor.java
index 464fa3ba54..f547cbc3a1 100644
--- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/profile/DeviceProfileEdgeProcessor.java
+++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/profile/DeviceProfileEdgeProcessor.java
@@ -96,14 +96,14 @@ public abstract class DeviceProfileEdgeProcessor extends BaseDeviceProfileProces
public DownlinkMsg convertDeviceProfileEventToDownlink(EdgeEvent edgeEvent, EdgeId edgeId, EdgeVersion edgeVersion) {
DeviceProfileId deviceProfileId = new DeviceProfileId(edgeEvent.getEntityId());
DownlinkMsg downlinkMsg = null;
+ var msgConstructor = (DeviceMsgConstructor) deviceMsgConstructorFactory.getMsgConstructorByEdgeVersion(edgeVersion);
switch (edgeEvent.getAction()) {
case ADDED, UPDATED -> {
DeviceProfile deviceProfile = deviceProfileService.findDeviceProfileById(edgeEvent.getTenantId(), deviceProfileId);
if (deviceProfile != null) {
UpdateMsgType msgType = getUpdateMsgType(edgeEvent.getAction());
deviceProfile = checkIfDeviceProfileDefaultFieldsAssignedToEdge(edgeEvent.getTenantId(), edgeId, deviceProfile, edgeVersion);
- DeviceProfileUpdateMsg deviceProfileUpdateMsg = ((DeviceMsgConstructor)
- deviceMsgConstructorFactory.getMsgConstructorByEdgeVersion(edgeVersion)).constructDeviceProfileUpdatedMsg(msgType, deviceProfile);
+ DeviceProfileUpdateMsg deviceProfileUpdateMsg = msgConstructor.constructDeviceProfileUpdatedMsg(msgType, deviceProfile);
downlinkMsg = DownlinkMsg.newBuilder()
.setDownlinkMsgId(EdgeUtils.nextPositiveInt())
.addDeviceProfileUpdateMsg(deviceProfileUpdateMsg)
@@ -111,8 +111,7 @@ public abstract class DeviceProfileEdgeProcessor extends BaseDeviceProfileProces
}
}
case DELETED -> {
- DeviceProfileUpdateMsg deviceProfileUpdateMsg = ((DeviceMsgConstructor)
- deviceMsgConstructorFactory.getMsgConstructorByEdgeVersion(edgeVersion)).constructDeviceProfileDeleteMsg(deviceProfileId);
+ DeviceProfileUpdateMsg deviceProfileUpdateMsg = msgConstructor.constructDeviceProfileDeleteMsg(deviceProfileId);
downlinkMsg = DownlinkMsg.newBuilder()
.setDownlinkMsgId(EdgeUtils.nextPositiveInt())
.addDeviceProfileUpdateMsg(deviceProfileUpdateMsg)
diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/edge/EdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/edge/EdgeProcessor.java
index 7bb631b330..f49c4b0a00 100644
--- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/edge/EdgeProcessor.java
+++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/edge/EdgeProcessor.java
@@ -79,7 +79,7 @@ public class EdgeProcessor extends BaseEdgeProcessor {
List> futures = new ArrayList<>();
futures.add(saveEdgeEvent(edge.getTenantId(), edge.getId(), EdgeEventType.CUSTOMER, EdgeEventActionType.ADDED, customerId, null));
futures.add(saveEdgeEvent(edge.getTenantId(), edge.getId(), EdgeEventType.EDGE, EdgeEventActionType.ASSIGNED_TO_CUSTOMER, edgeId, null));
- PageLink pageLink = new PageLink(DEFAULT_PAGE_SIZE);
+ PageLink pageLink = new PageLink(1000);
PageData pageData;
do {
pageData = userService.findCustomerUsers(tenantId, customerId, pageLink);
diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/entityview/EntityViewEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/entityview/EntityViewEdgeProcessor.java
index 70d9d9686f..4ef37f703b 100644
--- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/entityview/EntityViewEdgeProcessor.java
+++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/entityview/EntityViewEdgeProcessor.java
@@ -104,27 +104,23 @@ public abstract class EntityViewEdgeProcessor extends BaseEntityViewProcessor im
public DownlinkMsg convertEntityViewEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) {
EntityViewId entityViewId = new EntityViewId(edgeEvent.getEntityId());
DownlinkMsg downlinkMsg = null;
+ var msgConstructor = (EntityViewMsgConstructor) entityViewMsgConstructorFactory.getMsgConstructorByEdgeVersion(edgeVersion);
switch (edgeEvent.getAction()) {
case ADDED, UPDATED, ASSIGNED_TO_EDGE, ASSIGNED_TO_CUSTOMER, UNASSIGNED_FROM_CUSTOMER -> {
EntityView entityView = entityViewService.findEntityViewById(edgeEvent.getTenantId(), entityViewId);
if (entityView != null) {
UpdateMsgType msgType = getUpdateMsgType(edgeEvent.getAction());
- EntityViewUpdateMsg entityViewUpdateMsg = ((EntityViewMsgConstructor)
- entityViewMsgConstructorFactory.getMsgConstructorByEdgeVersion(edgeVersion)).constructEntityViewUpdatedMsg(msgType, entityView);
+ EntityViewUpdateMsg entityViewUpdateMsg = msgConstructor.constructEntityViewUpdatedMsg(msgType, entityView);
downlinkMsg = DownlinkMsg.newBuilder()
.setDownlinkMsgId(EdgeUtils.nextPositiveInt())
.addEntityViewUpdateMsg(entityViewUpdateMsg)
.build();
}
}
- case DELETED, UNASSIGNED_FROM_EDGE -> {
- EntityViewUpdateMsg entityViewUpdateMsg = ((EntityViewMsgConstructor)
- entityViewMsgConstructorFactory.getMsgConstructorByEdgeVersion(edgeVersion)).constructEntityViewDeleteMsg(entityViewId);
- downlinkMsg = DownlinkMsg.newBuilder()
- .setDownlinkMsgId(EdgeUtils.nextPositiveInt())
- .addEntityViewUpdateMsg(entityViewUpdateMsg)
- .build();
- }
+ case DELETED, UNASSIGNED_FROM_EDGE -> downlinkMsg = DownlinkMsg.newBuilder()
+ .setDownlinkMsgId(EdgeUtils.nextPositiveInt())
+ .addEntityViewUpdateMsg(msgConstructor.constructEntityViewDeleteMsg(entityViewId))
+ .build();
}
return downlinkMsg;
}
diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/oauth2/OAuth2EdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/oauth2/OAuth2EdgeProcessor.java
index adfb6f7d9f..999defcd02 100644
--- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/oauth2/OAuth2EdgeProcessor.java
+++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/oauth2/OAuth2EdgeProcessor.java
@@ -15,49 +15,95 @@
*/
package org.thingsboard.server.service.edge.rpc.processor.oauth2;
-import com.google.common.util.concurrent.Futures;
-import com.google.common.util.concurrent.ListenableFuture;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
-import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.EdgeUtils;
+import org.thingsboard.server.common.data.domain.DomainInfo;
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.TenantId;
-import org.thingsboard.server.common.data.oauth2.OAuth2Info;
+import org.thingsboard.server.common.data.id.DomainId;
+import org.thingsboard.server.common.data.id.OAuth2ClientId;
+import org.thingsboard.server.common.data.oauth2.OAuth2Client;
import org.thingsboard.server.gen.edge.v1.DownlinkMsg;
-import org.thingsboard.server.gen.edge.v1.OAuth2UpdateMsg;
-import org.thingsboard.server.gen.transport.TransportProtos;
+import org.thingsboard.server.gen.edge.v1.EdgeVersion;
+import org.thingsboard.server.gen.edge.v1.OAuth2ClientUpdateMsg;
+import org.thingsboard.server.gen.edge.v1.OAuth2DomainUpdateMsg;
+import org.thingsboard.server.gen.edge.v1.UpdateMsgType;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor;
+import org.thingsboard.server.service.edge.rpc.utils.EdgeVersionUtils;
@Slf4j
@Component
@TbCoreComponent
public class OAuth2EdgeProcessor extends BaseEdgeProcessor {
- public DownlinkMsg convertOAuth2EventToDownlink(EdgeEvent edgeEvent) {
+ public DownlinkMsg convertOAuth2DomainEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) {
+ if (EdgeVersionUtils.isEdgeVersionOlderThan(edgeVersion, EdgeVersion.V_3_7_1)) {
+ return null;
+ }
+ DomainId domainId = new DomainId(edgeEvent.getEntityId());
DownlinkMsg downlinkMsg = null;
- OAuth2Info oAuth2Info = JacksonUtil.convertValue(edgeEvent.getBody(), OAuth2Info.class);
- if (oAuth2Info != null && oAuth2Info.isEdgeEnabled()) {
- OAuth2UpdateMsg oAuth2UpdateMsg = oAuth2MsgConstructor.constructOAuth2UpdateMsg(oAuth2Info);
- downlinkMsg = DownlinkMsg.newBuilder()
- .setDownlinkMsgId(EdgeUtils.nextPositiveInt())
- .addOAuth2UpdateMsg(oAuth2UpdateMsg)
- .build();
+
+ switch (edgeEvent.getAction()) {
+ case ADDED, UPDATED -> {
+ DomainInfo domainInfo = domainService.findDomainInfoById(edgeEvent.getTenantId(), domainId);
+ if (domainInfo != null && domainInfo.isPropagateToEdge()) {
+ UpdateMsgType msgType = getUpdateMsgType(edgeEvent.getAction());
+ OAuth2DomainUpdateMsg oAuth2DomainUpdateMsg = oAuth2MsgConstructor.constructOAuth2DomainUpdateMsg(msgType, domainInfo);
+ DownlinkMsg.Builder builder = DownlinkMsg.newBuilder()
+ .setDownlinkMsgId(EdgeUtils.nextPositiveInt())
+ .addOAuth2DomainUpdateMsg(oAuth2DomainUpdateMsg);
+ domainInfo.getOauth2ClientInfos().forEach(clientInfo -> {
+ OAuth2Client oauth2Client = oAuth2ClientService.findOAuth2ClientById(edgeEvent.getTenantId(), clientInfo.getId());
+ OAuth2ClientUpdateMsg oAuth2ClientUpdateMsg = oAuth2MsgConstructor.constructOAuth2ClientUpdateMsg(msgType, oauth2Client);
+ builder.addOAuth2ClientUpdateMsg(oAuth2ClientUpdateMsg);
+ });
+ downlinkMsg = builder.build();
+ }
+ }
+ case DELETED -> {
+ OAuth2DomainUpdateMsg oAuth2DomainUpdateMsg = oAuth2MsgConstructor.constructOAuth2DomainDeleteMsg(domainId);
+ downlinkMsg = DownlinkMsg.newBuilder()
+ .setDownlinkMsgId(EdgeUtils.nextPositiveInt())
+ .addOAuth2DomainUpdateMsg(oAuth2DomainUpdateMsg)
+ .build();
+ }
}
return downlinkMsg;
}
- public ListenableFuture processOAuth2Notification(TenantId tenantId, TransportProtos.EdgeNotificationMsgProto edgeNotificationMsg) {
- OAuth2Info oAuth2Info = JacksonUtil.fromString(edgeNotificationMsg.getBody(), OAuth2Info.class);
- if (oAuth2Info == null) {
- return Futures.immediateFuture(null);
+ public DownlinkMsg convertOAuth2ClientEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) {
+ if (EdgeVersionUtils.isEdgeVersionOlderThan(edgeVersion, EdgeVersion.V_3_7_1)) {
+ return null;
+ }
+ OAuth2ClientId oAuth2ClientId = new OAuth2ClientId(edgeEvent.getEntityId());
+ DownlinkMsg downlinkMsg = null;
+
+ switch (edgeEvent.getAction()) {
+ case ADDED, UPDATED -> {
+ boolean isPropagateToEdge = oAuth2ClientService.isPropagateOAuth2ClientToEdge(edgeEvent.getTenantId(), oAuth2ClientId);
+ if (!isPropagateToEdge) {
+ return null;
+ }
+ OAuth2Client oAuth2Client = oAuth2ClientService.findOAuth2ClientById(edgeEvent.getTenantId(), oAuth2ClientId);
+ if (oAuth2Client != null) {
+ UpdateMsgType msgType = getUpdateMsgType(edgeEvent.getAction());
+ OAuth2ClientUpdateMsg oAuth2ClientUpdateMsg = oAuth2MsgConstructor.constructOAuth2ClientUpdateMsg(msgType, oAuth2Client);
+ downlinkMsg = DownlinkMsg.newBuilder()
+ .setDownlinkMsgId(EdgeUtils.nextPositiveInt())
+ .addOAuth2ClientUpdateMsg(oAuth2ClientUpdateMsg)
+ .build();
+ }
+ }
+ case DELETED -> {
+ OAuth2ClientUpdateMsg oAuth2ClientDeleteMsg = oAuth2MsgConstructor.constructOAuth2ClientDeleteMsg(oAuth2ClientId);
+ downlinkMsg = DownlinkMsg.newBuilder()
+ .setDownlinkMsgId(EdgeUtils.nextPositiveInt())
+ .addOAuth2ClientUpdateMsg(oAuth2ClientDeleteMsg)
+ .build();
+ }
}
- EdgeEventType type = EdgeEventType.valueOf(edgeNotificationMsg.getType());
- EdgeEventActionType actionType = EdgeEventActionType.valueOf(edgeNotificationMsg.getAction());
- return processActionForAllEdges(tenantId, type, actionType, null, JacksonUtil.toJsonNode(edgeNotificationMsg.getBody()), null);
+ return downlinkMsg;
}
}
diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/rule/RuleChainEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/rule/RuleChainEdgeProcessor.java
index 30f74ae42d..310d8d9b9b 100644
--- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/rule/RuleChainEdgeProcessor.java
+++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/rule/RuleChainEdgeProcessor.java
@@ -32,7 +32,7 @@ import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.edge.rpc.constructor.rule.RuleChainMsgConstructor;
import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor;
-import static org.thingsboard.server.service.edge.DefaultEdgeNotificationService.EDGE_IS_ROOT_BODY_KEY;
+import static org.thingsboard.server.dao.edge.EdgeServiceImpl.EDGE_IS_ROOT_BODY_KEY;
@Slf4j
@Component
@@ -42,6 +42,7 @@ public class RuleChainEdgeProcessor extends BaseEdgeProcessor {
public DownlinkMsg convertRuleChainEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) {
RuleChainId ruleChainId = new RuleChainId(edgeEvent.getEntityId());
DownlinkMsg downlinkMsg = null;
+ var msgConstructor = (RuleChainMsgConstructor) ruleChainMsgConstructorFactory.getMsgConstructorByEdgeVersion(edgeVersion);
switch (edgeEvent.getAction()) {
case ADDED, UPDATED, ASSIGNED_TO_EDGE -> {
RuleChain ruleChain = ruleChainService.findRuleChainById(edgeEvent.getTenantId(), ruleChainId);
@@ -58,19 +59,25 @@ public class RuleChainEdgeProcessor extends BaseEdgeProcessor {
isRoot = edge.getRootRuleChainId().equals(ruleChainId);
}
UpdateMsgType msgType = getUpdateMsgType(edgeEvent.getAction());
- RuleChainUpdateMsg ruleChainUpdateMsg = ((RuleChainMsgConstructor)
- ruleChainMsgConstructorFactory.getMsgConstructorByEdgeVersion(edgeVersion))
- .constructRuleChainUpdatedMsg(msgType, ruleChain, isRoot);
- downlinkMsg = DownlinkMsg.newBuilder()
+ RuleChainUpdateMsg ruleChainUpdateMsg = msgConstructor.constructRuleChainUpdatedMsg(msgType, ruleChain, isRoot);
+
+ DownlinkMsg.Builder builder = DownlinkMsg.newBuilder()
.setDownlinkMsgId(EdgeUtils.nextPositiveInt())
- .addRuleChainUpdateMsg(ruleChainUpdateMsg)
- .build();
+ .addRuleChainUpdateMsg(ruleChainUpdateMsg);
+
+ RuleChainMetaData ruleChainMetaData = ruleChainService.loadRuleChainMetaData(edgeEvent.getTenantId(), ruleChainId);
+ RuleChainMetadataUpdateMsg ruleChainMetadataUpdateMsg = ((RuleChainMsgConstructor)
+ ruleChainMsgConstructorFactory.getMsgConstructorByEdgeVersion(edgeVersion))
+ .constructRuleChainMetadataUpdatedMsg(edgeEvent.getTenantId(), msgType, ruleChainMetaData, edgeVersion);
+ if (ruleChainMetadataUpdateMsg != null) {
+ builder.addRuleChainMetadataUpdateMsg(ruleChainMetadataUpdateMsg);
+ }
+ downlinkMsg = builder.build();
}
}
case DELETED, UNASSIGNED_FROM_EDGE -> downlinkMsg = DownlinkMsg.newBuilder()
.setDownlinkMsgId(EdgeUtils.nextPositiveInt())
- .addRuleChainUpdateMsg(((RuleChainMsgConstructor) ruleChainMsgConstructorFactory.getMsgConstructorByEdgeVersion(edgeVersion))
- .constructRuleChainDeleteMsg(ruleChainId))
+ .addRuleChainUpdateMsg(msgConstructor.constructRuleChainDeleteMsg(ruleChainId))
.build();
}
return downlinkMsg;
@@ -80,11 +87,11 @@ public class RuleChainEdgeProcessor extends BaseEdgeProcessor {
RuleChainId ruleChainId = new RuleChainId(edgeEvent.getEntityId());
RuleChain ruleChain = ruleChainService.findRuleChainById(edgeEvent.getTenantId(), ruleChainId);
DownlinkMsg downlinkMsg = null;
+ var msgConstructor = (RuleChainMsgConstructor) ruleChainMsgConstructorFactory.getMsgConstructorByEdgeVersion(edgeVersion);
if (ruleChain != null) {
RuleChainMetaData ruleChainMetaData = ruleChainService.loadRuleChainMetaData(edgeEvent.getTenantId(), ruleChainId);
UpdateMsgType msgType = getUpdateMsgType(edgeEvent.getAction());
- RuleChainMetadataUpdateMsg ruleChainMetadataUpdateMsg = ((RuleChainMsgConstructor)
- ruleChainMsgConstructorFactory.getMsgConstructorByEdgeVersion(edgeVersion))
+ RuleChainMetadataUpdateMsg ruleChainMetadataUpdateMsg = msgConstructor
.constructRuleChainMetadataUpdatedMsg(edgeEvent.getTenantId(), msgType, ruleChainMetaData, edgeVersion);
if (ruleChainMetadataUpdateMsg != null) {
downlinkMsg = DownlinkMsg.newBuilder()
diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/UserEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/UserEdgeProcessor.java
index 607c5e665c..cd072b4a1e 100644
--- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/UserEdgeProcessor.java
+++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/UserEdgeProcessor.java
@@ -43,10 +43,16 @@ public class UserEdgeProcessor extends BaseEdgeProcessor {
User user = userService.findUserById(edgeEvent.getTenantId(), userId);
if (user != null) {
UpdateMsgType msgType = getUpdateMsgType(edgeEvent.getAction());
- downlinkMsg = DownlinkMsg.newBuilder()
+ DownlinkMsg.Builder builder = DownlinkMsg.newBuilder()
.setDownlinkMsgId(EdgeUtils.nextPositiveInt())
- .addUserUpdateMsg(((UserMsgConstructor) userMsgConstructorFactory.getMsgConstructorByEdgeVersion(edgeVersion)).constructUserUpdatedMsg(msgType, user))
- .build();
+ .addUserUpdateMsg(((UserMsgConstructor) userMsgConstructorFactory.getMsgConstructorByEdgeVersion(edgeVersion)).constructUserUpdatedMsg(msgType, user));
+ UserCredentials userCredentialsByUserId = userService.findUserCredentialsByUserId(edgeEvent.getTenantId(), userId);
+ if (userCredentialsByUserId != null && userCredentialsByUserId.isEnabled()) {
+ UserCredentialsUpdateMsg userCredentialsUpdateMsg =
+ ((UserMsgConstructor) userMsgConstructorFactory.getMsgConstructorByEdgeVersion(edgeVersion)).constructUserCredentialsUpdatedMsg(userCredentialsByUserId);
+ builder.addUserCredentialsUpdateMsg(userCredentialsUpdateMsg);
+ }
+ downlinkMsg = builder.build();
}
}
case DELETED -> downlinkMsg = DownlinkMsg.newBuilder()
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 3f61734801..46f1d7ef57 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
@@ -82,8 +82,6 @@ import java.util.UUID;
@Slf4j
public class DefaultEdgeRequestsService implements EdgeRequestsService {
- private static final int DEFAULT_PAGE_SIZE = 1000;
-
@Autowired
private EdgeEventService edgeEventService;
@@ -115,10 +113,8 @@ public class DefaultEdgeRequestsService implements EdgeRequestsService {
if (ruleChainMetadataRequestMsg.getRuleChainIdMSB() == 0 || ruleChainMetadataRequestMsg.getRuleChainIdLSB() == 0) {
return Futures.immediateFuture(null);
}
- RuleChainId ruleChainId =
- new RuleChainId(new UUID(ruleChainMetadataRequestMsg.getRuleChainIdMSB(), ruleChainMetadataRequestMsg.getRuleChainIdLSB()));
- return saveEdgeEvent(tenantId, edge.getId(),
- EdgeEventType.RULE_CHAIN_METADATA, EdgeEventActionType.ADDED, ruleChainId, null);
+ RuleChainId ruleChainId = new RuleChainId(new UUID(ruleChainMetadataRequestMsg.getRuleChainIdMSB(), ruleChainMetadataRequestMsg.getRuleChainIdLSB()));
+ return saveEdgeEvent(tenantId, edge.getId(), EdgeEventType.RULE_CHAIN_METADATA, EdgeEventActionType.ADDED, ruleChainId, null);
}
@Override
@@ -142,8 +138,10 @@ public class DefaultEdgeRequestsService implements EdgeRequestsService {
private ListenableFuture processEntityAttributesAndAddToEdgeQueue(TenantId tenantId, EntityId entityId, Edge edge,
EdgeEventType entityType, String scope, List ssAttributes,
AttributesRequestMsg attributesRequestMsg) {
+ Map entityData = null;
+ ObjectNode attributes = null;
+ ListenableFuture future;
try {
- ListenableFuture future;
if (ssAttributes == null || ssAttributes.isEmpty()) {
log.trace("[{}][{}] No attributes found for entity {} [{}]", tenantId,
edge.getName(),
@@ -151,8 +149,8 @@ public class DefaultEdgeRequestsService implements EdgeRequestsService {
entityId.getId());
future = Futures.immediateFuture(null);
} else {
- Map entityData = new HashMap<>();
- ObjectNode attributes = JacksonUtil.newObjectNode();
+ entityData = new HashMap<>();
+ attributes = JacksonUtil.newObjectNode();
for (AttributeKvEntry attr : ssAttributes) {
if (DefaultDeviceStateService.PERSISTENT_ATTRIBUTES.contains(attr.getKey())
&& !DefaultDeviceStateService.INACTIVITY_TIMEOUT.equals(attr.getKey())) {
@@ -170,7 +168,7 @@ public class DefaultEdgeRequestsService implements EdgeRequestsService {
attributes.put(attr.getKey(), attr.getValueAsString());
}
}
- if (attributes.size() > 0) {
+ if (!attributes.isEmpty()) {
entityData.put("kv", attributes);
entityData.put("scope", scope);
JsonNode body = JacksonUtil.valueToTree(entityData);
@@ -182,12 +180,13 @@ public class DefaultEdgeRequestsService implements EdgeRequestsService {
}
return Futures.transformAsync(future, v -> processLatestTimeseriesAndAddToEdgeQueue(tenantId, entityId, edge, entityType), dbCallbackExecutorService);
} catch (Exception e) {
- String errMsg = String.format("[%s][%s] Failed to save attribute updates to the edge [%s]", tenantId, edge.getId(), attributesRequestMsg);
+ String errMsg = String.format("[%s][%s] Failed to save attribute updates to the edge [%s], scope = %s, entityData = %s, attributes = %s",
+ tenantId, edge.getId(), attributesRequestMsg, scope, entityData, attributes);
log.error(errMsg, e);
return Futures.immediateFailedFuture(new RuntimeException(errMsg, e));
}
}
-
+
private ListenableFuture processLatestTimeseriesAndAddToEdgeQueue(TenantId tenantId, EntityId entityId, Edge edge,
EdgeEventType entityType) {
ListenableFuture> getAllLatestFuture = timeseriesService.findAllLatest(tenantId, entityId);
diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/EntityStateSourcingListener.java b/application/src/main/java/org/thingsboard/server/service/entitiy/EntityStateSourcingListener.java
index 4a7c1e1e87..863be23e42 100644
--- a/application/src/main/java/org/thingsboard/server/service/entitiy/EntityStateSourcingListener.java
+++ b/application/src/main/java/org/thingsboard/server/service/entitiy/EntityStateSourcingListener.java
@@ -16,6 +16,7 @@
package org.thingsboard.server.service.entitiy;
import com.fasterxml.jackson.core.type.TypeReference;
+import jakarta.annotation.PostConstruct;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
@@ -34,7 +35,6 @@ import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.common.data.edge.EdgeEvent;
import org.thingsboard.server.common.data.id.DeviceId;
-import org.thingsboard.server.common.data.id.EdgeId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.RuleChainId;
import org.thingsboard.server.common.data.id.TenantId;
@@ -47,13 +47,14 @@ import org.thingsboard.server.common.data.security.DeviceCredentials;
import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.TbMsgDataType;
import org.thingsboard.server.common.msg.TbMsgMetaData;
+import org.thingsboard.server.common.msg.edge.EdgeEventUpdateMsg;
+import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg;
import org.thingsboard.server.common.msg.rule.engine.DeviceCredentialsUpdateNotificationMsg;
import org.thingsboard.server.dao.eventsourcing.ActionEntityEvent;
import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent;
import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent;
import org.thingsboard.server.dao.tenant.TenantService;
-import javax.annotation.PostConstruct;
import java.util.Set;
@Component
@@ -66,7 +67,7 @@ public class EntityStateSourcingListener {
@PostConstruct
public void init() {
- log.info("EntityStateSourcingListener initiated");
+ log.debug("EntityStateSourcingListener initiated");
}
@TransactionalEventListener(fallbackExecution = true)
@@ -107,7 +108,7 @@ public class EntityStateSourcingListener {
onDeviceProfileUpdate(deviceProfile, event.getOldEntity(), isCreated);
}
case EDGE -> {
- handleEdgeEvent(tenantId, entityId, event.getEntity(), lifecycleEvent);
+ onEdgeEvent(tenantId, entityId, event.getEntity(), lifecycleEvent);
}
case TB_RESOURCE -> {
TbResource tbResource = (TbResource) event.getEntity();
@@ -221,10 +222,7 @@ public class EntityStateSourcingListener {
}
private DeviceProfile getOldDeviceProfile(Object oldEntity) {
- if (oldEntity instanceof DeviceProfile) {
- return (DeviceProfile) oldEntity;
- }
- return null;
+ return oldEntity instanceof DeviceProfile ? (DeviceProfile) oldEntity : null;
}
private void onDeviceProfileDelete(TenantId tenantId, EntityId entityId, DeviceProfile deviceProfile) {
@@ -241,11 +239,11 @@ public class EntityStateSourcingListener {
tbClusterService.onDeviceUpdated(device, oldDevice);
}
- private void handleEdgeEvent(TenantId tenantId, EntityId entityId, Object entity, ComponentLifecycleEvent lifecycleEvent) {
+ private void onEdgeEvent(TenantId tenantId, EntityId entityId, Object entity, ComponentLifecycleEvent lifecycleEvent) {
if (entity instanceof Edge) {
- tbClusterService.broadcastEntityStateChangeEvent(tenantId, entityId, lifecycleEvent);
- } else if (entity instanceof EdgeEvent) {
- tbClusterService.onEdgeEventUpdate(tenantId, (EdgeId) entityId);
+ tbClusterService.onEdgeStateChangeEvent(new ComponentLifecycleMsg(tenantId, entityId, lifecycleEvent));
+ } else if (entity instanceof EdgeEvent edgeEvent) {
+ tbClusterService.onEdgeEventUpdate(new EdgeEventUpdateMsg(tenantId, edgeEvent.getEdgeId()));
}
}
@@ -264,4 +262,5 @@ public class EntityStateSourcingListener {
metaData.putValue("assignedFromTenantName", tenant.getName());
return metaData;
}
+
}
diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmService.java
index 89e0bc944f..e54f93be8f 100644
--- a/application/src/main/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmService.java
+++ b/application/src/main/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmService.java
@@ -196,7 +196,7 @@ public class DefaultTbAlarmService extends AbstractTbEntityService implements Tb
public Boolean delete(Alarm alarm, User user) {
TenantId tenantId = alarm.getTenantId();
logEntityActionService.logEntityAction(tenantId, alarm.getOriginator(), alarm, alarm.getCustomerId(),
- ActionType.ALARM_DELETE, user);
+ ActionType.ALARM_DELETE, user, alarm.getId());
return alarmSubscriptionService.deleteAlarm(tenantId, alarm.getId());
}
diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/customer/DefaultTbCustomerService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/customer/DefaultTbCustomerService.java
index f7c19773ae..b14e605b15 100644
--- a/application/src/main/java/org/thingsboard/server/service/entitiy/customer/DefaultTbCustomerService.java
+++ b/application/src/main/java/org/thingsboard/server/service/entitiy/customer/DefaultTbCustomerService.java
@@ -23,7 +23,6 @@ import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.TenantId;
-import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
import org.thingsboard.server.service.entitiy.AbstractTbEntityService;
@Service
diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/dashboard/DefaultTbDashboardService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/dashboard/DefaultTbDashboardService.java
index 685eda0858..ee8b2d7e47 100644
--- a/application/src/main/java/org/thingsboard/server/service/entitiy/dashboard/DefaultTbDashboardService.java
+++ b/application/src/main/java/org/thingsboard/server/service/entitiy/dashboard/DefaultTbDashboardService.java
@@ -30,7 +30,6 @@ import org.thingsboard.server.common.data.id.DashboardId;
import org.thingsboard.server.common.data.id.EdgeId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.dao.dashboard.DashboardService;
-import org.thingsboard.server.dao.resource.ImageService;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.entitiy.AbstractTbEntityService;
diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/device/DefaultTbDeviceService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/device/DefaultTbDeviceService.java
index b05ece9c61..d527492d98 100644
--- a/application/src/main/java/org/thingsboard/server/service/entitiy/device/DefaultTbDeviceService.java
+++ b/application/src/main/java/org/thingsboard/server/service/entitiy/device/DefaultTbDeviceService.java
@@ -183,7 +183,7 @@ public class DefaultTbDeviceService extends AbstractTbEntityService implements T
try {
DeviceCredentials result = checkNotNull(deviceCredentialsService.updateDeviceCredentials(tenantId, deviceCredentials));
logEntityActionService.logEntityAction(tenantId, deviceId, device, device.getCustomerId(),
- actionType, user, deviceCredentials);
+ actionType, user, result);
return result;
} catch (Exception e) {
logEntityActionService.logEntityAction(tenantId, emptyId(EntityType.DEVICE),
diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/domain/DefaultTbDomainService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/domain/DefaultTbDomainService.java
new file mode 100644
index 0000000000..dc876c2cfa
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/entitiy/domain/DefaultTbDomainService.java
@@ -0,0 +1,84 @@
+/**
+ * Copyright © 2016-2024 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.service.entitiy.domain;
+
+import lombok.AllArgsConstructor;
+import org.apache.commons.collections4.CollectionUtils;
+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.audit.ActionType;
+import org.thingsboard.server.common.data.domain.Domain;
+import org.thingsboard.server.common.data.id.DomainId;
+import org.thingsboard.server.common.data.id.OAuth2ClientId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.dao.domain.DomainService;
+import org.thingsboard.server.service.entitiy.AbstractTbEntityService;
+
+import java.util.List;
+
+@Service
+@AllArgsConstructor
+public class DefaultTbDomainService extends AbstractTbEntityService implements TbDomainService {
+
+ private final DomainService domainService;
+
+ @Override
+ public Domain save(Domain domain, List oAuth2Clients, User user) throws Exception {
+ ActionType actionType = domain.getId() == null ? ActionType.ADDED : ActionType.UPDATED;
+ TenantId tenantId = domain.getTenantId();
+ try {
+ Domain savedDomain = checkNotNull(domainService.saveDomain(tenantId, domain));
+ if (CollectionUtils.isNotEmpty(oAuth2Clients)) {
+ domainService.updateOauth2Clients(domain.getTenantId(), savedDomain.getId(), oAuth2Clients);
+ }
+ logEntityActionService.logEntityAction(tenantId, savedDomain.getId(), savedDomain, actionType, user, oAuth2Clients);
+ return savedDomain;
+ } catch (Exception e) {
+ logEntityActionService.logEntityAction(tenantId, emptyId(EntityType.DOMAIN), domain, actionType, user, e, oAuth2Clients);
+ throw e;
+ }
+ }
+
+ @Override
+ public void updateOauth2Clients(Domain domain, List oAuth2ClientIds, User user) {
+ ActionType actionType = ActionType.UPDATED;
+ TenantId tenantId = domain.getTenantId();
+ DomainId domainId = domain.getId();
+ try {
+ domainService.updateOauth2Clients(tenantId, domainId, oAuth2ClientIds);
+ logEntityActionService.logEntityAction(tenantId, domainId, domain, actionType, user, oAuth2ClientIds);
+ } catch (Exception e) {
+ logEntityActionService.logEntityAction(tenantId, domainId, domain, actionType, user, e, oAuth2ClientIds);
+ throw e;
+ }
+ }
+
+ @Override
+ public void delete(Domain domain, User user) {
+ ActionType actionType = ActionType.DELETED;
+ TenantId tenantId = domain.getTenantId();
+ DomainId domainId = domain.getId();
+ try {
+ domainService.deleteDomainById(tenantId, domainId);
+ logEntityActionService.logEntityAction(tenantId, domainId, domain, actionType, user);
+ } catch (Exception e) {
+ logEntityActionService.logEntityAction(tenantId, domainId, domain, actionType, user, e);
+ throw e;
+ }
+ }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/domain/TbDomainService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/domain/TbDomainService.java
new file mode 100644
index 0000000000..744005ff36
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/entitiy/domain/TbDomainService.java
@@ -0,0 +1,32 @@
+/**
+ * Copyright © 2016-2024 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.service.entitiy.domain;
+
+import org.thingsboard.server.common.data.User;
+import org.thingsboard.server.common.data.domain.Domain;
+import org.thingsboard.server.common.data.id.OAuth2ClientId;
+
+import java.util.List;
+
+public interface TbDomainService {
+
+ Domain save(Domain domain, List oAuth2Clients, User user) throws Exception;
+
+ void updateOauth2Clients(Domain domain, List oAuth2ClientIds, User user);
+
+ void delete(Domain domain, User user);
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/edge/DefaultTbEdgeService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/edge/DefaultTbEdgeService.java
index 9fcd8e2187..8771b48c35 100644
--- a/application/src/main/java/org/thingsboard/server/service/entitiy/edge/DefaultTbEdgeService.java
+++ b/application/src/main/java/org/thingsboard/server/service/entitiy/edge/DefaultTbEdgeService.java
@@ -31,7 +31,6 @@ import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.rule.RuleChain;
import org.thingsboard.server.dao.rule.RuleChainService;
import org.thingsboard.server.queue.util.TbCoreComponent;
-import org.thingsboard.server.service.edge.EdgeNotificationService;
import org.thingsboard.server.service.entitiy.AbstractTbEntityService;
@AllArgsConstructor
@@ -40,7 +39,6 @@ import org.thingsboard.server.service.entitiy.AbstractTbEntityService;
@Slf4j
public class DefaultTbEdgeService extends AbstractTbEntityService implements TbEdgeService {
- private final EdgeNotificationService edgeNotificationService;
private final RuleChainService ruleChainService;
@Override
@@ -56,7 +54,7 @@ public class DefaultTbEdgeService extends AbstractTbEntityService implements TbE
if (ActionType.ADDED.equals(actionType)) {
ruleChainService.assignRuleChainToEdge(tenantId, edgeTemplateRootRuleChain.getId(), edgeId);
- edgeNotificationService.setEdgeRootRuleChain(tenantId, savedEdge, edgeTemplateRootRuleChain.getId());
+ savedEdge = edgeService.setEdgeRootRuleChain(tenantId, savedEdge, edgeTemplateRootRuleChain.getId());
edgeService.assignDefaultRuleChainsToEdge(tenantId, edgeId);
}
@@ -143,7 +141,7 @@ public class DefaultTbEdgeService extends AbstractTbEntityService implements TbE
TenantId tenantId = edge.getTenantId();
EdgeId edgeId = edge.getId();
try {
- Edge updatedEdge = edgeNotificationService.setEdgeRootRuleChain(tenantId, edge, ruleChainId);
+ Edge updatedEdge = edgeService.setEdgeRootRuleChain(tenantId, edge, ruleChainId);
logEntityActionService.logEntityAction(tenantId, edgeId, edge, null, ActionType.UPDATED, user);
return updatedEdge;
} catch (Exception e) {
@@ -152,4 +150,5 @@ public class DefaultTbEdgeService extends AbstractTbEntityService implements TbE
throw e;
}
}
+
}
diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/edge/TbEdgeService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/edge/TbEdgeService.java
index 8dc447b731..3afd32c3fd 100644
--- a/application/src/main/java/org/thingsboard/server/service/entitiy/edge/TbEdgeService.java
+++ b/application/src/main/java/org/thingsboard/server/service/entitiy/edge/TbEdgeService.java
@@ -25,6 +25,7 @@ import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.rule.RuleChain;
public interface TbEdgeService {
+
Edge save(Edge edge, RuleChain edgeTemplateRootRuleChain, User user) throws Exception;
void delete(Edge edge, User user);
@@ -36,4 +37,5 @@ public interface TbEdgeService {
Edge assignEdgeToPublicCustomer(TenantId tenantId, EdgeId edgeId, User user) throws ThingsboardException;
Edge setEdgeRootRuleChain(Edge edge, RuleChainId ruleChainId, User user) throws Exception;
+
}
diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/entity/relation/DefaultTbEntityRelationService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/entity/relation/DefaultTbEntityRelationService.java
index 724c077e62..e9cfc109f8 100644
--- a/application/src/main/java/org/thingsboard/server/service/entitiy/entity/relation/DefaultTbEntityRelationService.java
+++ b/application/src/main/java/org/thingsboard/server/service/entitiy/entity/relation/DefaultTbEntityRelationService.java
@@ -39,12 +39,13 @@ public class DefaultTbEntityRelationService extends AbstractTbEntityService impl
private final RelationService relationService;
@Override
- public void save(TenantId tenantId, CustomerId customerId, EntityRelation relation, User user) throws ThingsboardException {
+ public EntityRelation save(TenantId tenantId, CustomerId customerId, EntityRelation relation, User user) throws ThingsboardException {
ActionType actionType = ActionType.RELATION_ADD_OR_UPDATE;
try {
- relationService.saveRelation(tenantId, relation);
+ var savedRelation = relationService.saveRelation(tenantId, relation);
logEntityActionService.logEntityRelationAction(tenantId, customerId,
- relation, user, actionType, null, relation);
+ savedRelation, user, actionType, null, savedRelation);
+ return savedRelation;
} catch (Exception e) {
logEntityActionService.logEntityRelationAction(tenantId, customerId,
relation, user, actionType, e, relation);
@@ -53,14 +54,15 @@ public class DefaultTbEntityRelationService extends AbstractTbEntityService impl
}
@Override
- public void delete(TenantId tenantId, CustomerId customerId, EntityRelation relation, User user) throws ThingsboardException {
+ public EntityRelation delete(TenantId tenantId, CustomerId customerId, EntityRelation relation, User user) throws ThingsboardException {
ActionType actionType = ActionType.RELATION_DELETED;
try {
- boolean found = relationService.deleteRelation(tenantId, relation.getFrom(), relation.getTo(), relation.getType(), relation.getTypeGroup());
- if (!found) {
+ var found = relationService.deleteRelation(tenantId, relation.getFrom(), relation.getTo(), relation.getType(), relation.getTypeGroup());
+ if (found == null) {
throw new ThingsboardException("Requested item wasn't found!", ThingsboardErrorCode.ITEM_NOT_FOUND);
}
- logEntityActionService.logEntityRelationAction(tenantId, customerId, relation, user, actionType, null, relation);
+ logEntityActionService.logEntityRelationAction(tenantId, customerId, found, user, actionType, null, found);
+ return found;
} catch (Exception e) {
logEntityActionService.logEntityRelationAction(tenantId, customerId,
relation, user, actionType, e, relation);
diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/entity/relation/TbEntityRelationService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/entity/relation/TbEntityRelationService.java
index 7b732ff9ee..0ef75d0354 100644
--- a/application/src/main/java/org/thingsboard/server/service/entitiy/entity/relation/TbEntityRelationService.java
+++ b/application/src/main/java/org/thingsboard/server/service/entitiy/entity/relation/TbEntityRelationService.java
@@ -24,9 +24,9 @@ import org.thingsboard.server.common.data.relation.EntityRelation;
public interface TbEntityRelationService {
- void save(TenantId tenantId, CustomerId customerId, EntityRelation entity, User user) throws ThingsboardException;
+ EntityRelation save(TenantId tenantId, CustomerId customerId, EntityRelation entity, User user) throws ThingsboardException;
- void delete(TenantId tenantId, CustomerId customerId, EntityRelation entity, User user) throws ThingsboardException;
+ EntityRelation delete(TenantId tenantId, CustomerId customerId, EntityRelation entity, User user) throws ThingsboardException;
void deleteCommonRelations(TenantId tenantId, CustomerId customerId, EntityId entityId, User user) throws ThingsboardException;
diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/mobile/DefaultTbMobileAppService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/mobile/DefaultTbMobileAppService.java
new file mode 100644
index 0000000000..8dbef1fc1e
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/entitiy/mobile/DefaultTbMobileAppService.java
@@ -0,0 +1,83 @@
+/**
+ * Copyright © 2016-2024 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.service.entitiy.mobile;
+
+import lombok.AllArgsConstructor;
+import org.apache.commons.collections4.CollectionUtils;
+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.audit.ActionType;
+import org.thingsboard.server.common.data.id.MobileAppId;
+import org.thingsboard.server.common.data.id.OAuth2ClientId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.mobile.MobileApp;
+import org.thingsboard.server.dao.mobile.MobileAppService;
+import org.thingsboard.server.service.entitiy.AbstractTbEntityService;
+
+import java.util.List;
+
+@Service
+@AllArgsConstructor
+public class DefaultTbMobileAppService extends AbstractTbEntityService implements TbMobileAppService {
+
+ private final MobileAppService mobileAppService;
+
+ @Override
+ public MobileApp save(MobileApp mobileApp, List oauth2Clients, User user) throws Exception {
+ ActionType actionType = mobileApp.getId() == null ? ActionType.ADDED : ActionType.UPDATED;
+ TenantId tenantId = mobileApp.getTenantId();
+ try {
+ MobileApp savedMobileApp = checkNotNull(mobileAppService.saveMobileApp(tenantId, mobileApp));
+ if (CollectionUtils.isNotEmpty(oauth2Clients)) {
+ mobileAppService.updateOauth2Clients(tenantId, savedMobileApp.getId(), oauth2Clients);
+ }
+ logEntityActionService.logEntityAction(tenantId, savedMobileApp.getId(), savedMobileApp, actionType, user);
+ return savedMobileApp;
+ } catch (Exception e) {
+ logEntityActionService.logEntityAction(tenantId, emptyId(EntityType.MOBILE_APP), mobileApp, actionType, user, e);
+ throw e;
+ }
+ }
+
+ @Override
+ public void updateOauth2Clients(MobileApp mobileApp, List oAuth2ClientIds, User user) {
+ ActionType actionType = ActionType.UPDATED;
+ TenantId tenantId = mobileApp.getTenantId();
+ MobileAppId mobileAppId = mobileApp.getId();
+ try {
+ mobileAppService.updateOauth2Clients(tenantId, mobileAppId, oAuth2ClientIds);
+ logEntityActionService.logEntityAction(tenantId, mobileAppId, mobileApp, actionType, user, oAuth2ClientIds);
+ } catch (Exception e) {
+ logEntityActionService.logEntityAction(tenantId, mobileAppId, mobileApp, actionType, user, e, oAuth2ClientIds);
+ throw e;
+ }
+ }
+
+ @Override
+ public void delete(MobileApp mobileApp, User user) {
+ ActionType actionType = ActionType.DELETED;
+ TenantId tenantId = mobileApp.getTenantId();
+ MobileAppId mobileAppId = mobileApp.getId();
+ try {
+ mobileAppService.deleteMobileAppById(tenantId, mobileAppId);
+ logEntityActionService.logEntityAction(tenantId, mobileAppId, mobileApp, actionType, user);
+ } catch (Exception e) {
+ logEntityActionService.logEntityAction(tenantId, mobileAppId, mobileApp, actionType, user, e);
+ throw e;
+ }
+ }
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/mobile/TbMobileAppService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/mobile/TbMobileAppService.java
new file mode 100644
index 0000000000..bcc9cf7004
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/entitiy/mobile/TbMobileAppService.java
@@ -0,0 +1,32 @@
+/**
+ * Copyright © 2016-2024 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.service.entitiy.mobile;
+
+import org.thingsboard.server.common.data.User;
+import org.thingsboard.server.common.data.id.OAuth2ClientId;
+import org.thingsboard.server.common.data.mobile.MobileApp;
+
+import java.util.List;
+
+public interface TbMobileAppService {
+
+ MobileApp save(MobileApp mobileApp, List oauth2Clients, User user) throws Exception;
+
+ void updateOauth2Clients(MobileApp mobileApp, List oAuth2ClientIds, User user);
+
+ void delete(MobileApp mobileApp, User user);
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/oauth2client/DefaultTbOauth2ClientService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/oauth2client/DefaultTbOauth2ClientService.java
new file mode 100644
index 0000000000..999bf3976c
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/entitiy/oauth2client/DefaultTbOauth2ClientService.java
@@ -0,0 +1,62 @@
+/**
+ * Copyright © 2016-2024 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.service.entitiy.oauth2client;
+
+import lombok.AllArgsConstructor;
+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.audit.ActionType;
+import org.thingsboard.server.common.data.id.OAuth2ClientId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.oauth2.OAuth2Client;
+import org.thingsboard.server.dao.oauth2.OAuth2ClientService;
+import org.thingsboard.server.service.entitiy.AbstractTbEntityService;
+
+@Service
+@AllArgsConstructor
+public class DefaultTbOauth2ClientService extends AbstractTbEntityService implements TbOauth2ClientService {
+
+ private final OAuth2ClientService oAuth2ClientService;
+
+ @Override
+ public OAuth2Client save(OAuth2Client oAuth2Client, User user) throws Exception {
+ ActionType actionType = oAuth2Client.getId() == null ? ActionType.ADDED : ActionType.UPDATED;
+ TenantId tenantId = oAuth2Client.getTenantId();
+ try {
+ OAuth2Client savedClient = checkNotNull(oAuth2ClientService.saveOAuth2Client(tenantId, oAuth2Client));
+ logEntityActionService.logEntityAction(tenantId, savedClient.getId(), savedClient, actionType, user);
+ return savedClient;
+ } catch (Exception e) {
+ logEntityActionService.logEntityAction(tenantId, emptyId(EntityType.OAUTH2_CLIENT), oAuth2Client, actionType, user, e);
+ throw e;
+ }
+ }
+
+ @Override
+ public void delete(OAuth2Client oAuth2Client, User user) {
+ ActionType actionType = ActionType.DELETED;
+ TenantId tenantId = oAuth2Client.getTenantId();
+ OAuth2ClientId oAuth2ClientId = oAuth2Client.getId();
+ try {
+ oAuth2ClientService.deleteOAuth2ClientById(tenantId, oAuth2ClientId);
+ logEntityActionService.logEntityAction(tenantId, oAuth2ClientId, oAuth2Client, actionType, user);
+ } catch (Exception e) {
+ logEntityActionService.logEntityAction(tenantId, oAuth2ClientId, oAuth2Client, actionType, user, e);
+ throw e;
+ }
+ }
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2DomainDao.java b/application/src/main/java/org/thingsboard/server/service/entitiy/oauth2client/TbOauth2ClientService.java
similarity index 64%
rename from dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2DomainDao.java
rename to application/src/main/java/org/thingsboard/server/service/entitiy/oauth2client/TbOauth2ClientService.java
index 52bde2cd8c..c4ac3c7839 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2DomainDao.java
+++ b/application/src/main/java/org/thingsboard/server/service/entitiy/oauth2client/TbOauth2ClientService.java
@@ -13,16 +13,15 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.thingsboard.server.dao.oauth2;
+package org.thingsboard.server.service.entitiy.oauth2client;
-import org.thingsboard.server.common.data.oauth2.OAuth2Domain;
-import org.thingsboard.server.dao.Dao;
+import org.thingsboard.server.common.data.User;
+import org.thingsboard.server.common.data.oauth2.OAuth2Client;
-import java.util.List;
-import java.util.UUID;
+public interface TbOauth2ClientService {
-public interface OAuth2DomainDao extends Dao {
+ OAuth2Client save(OAuth2Client oAuth2Client, User user) throws Exception;
- List findByOAuth2ParamsId(UUID oauth2ParamsId);
+ void delete(OAuth2Client oAuth2Client, User user);
}
diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/tenant/DefaultTbTenantService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/tenant/DefaultTbTenantService.java
index 74061ee9e5..bb855d527a 100644
--- a/application/src/main/java/org/thingsboard/server/service/entitiy/tenant/DefaultTbTenantService.java
+++ b/application/src/main/java/org/thingsboard/server/service/entitiy/tenant/DefaultTbTenantService.java
@@ -17,7 +17,6 @@ package org.thingsboard.server.service.entitiy.tenant;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
-import org.springframework.beans.factory.annotation.Value;
import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.TenantProfile;
import org.thingsboard.server.common.data.id.TenantId;
diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/user/DefaultUserService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/user/DefaultUserService.java
index 68595bed2e..299d499daa 100644
--- a/application/src/main/java/org/thingsboard/server/service/entitiy/user/DefaultUserService.java
+++ b/application/src/main/java/org/thingsboard/server/service/entitiy/user/DefaultUserService.java
@@ -15,6 +15,7 @@
*/
package org.thingsboard.server.service.entitiy.user;
+import jakarta.servlet.http.HttpServletRequest;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
@@ -32,8 +33,6 @@ import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.entitiy.AbstractTbEntityService;
import org.thingsboard.server.service.security.system.SystemSecurityService;
-import jakarta.servlet.http.HttpServletRequest;
-
import static org.thingsboard.server.controller.UserController.ACTIVATE_URL_PATTERN;
@Service
diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/user/TbUserService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/user/TbUserService.java
index ae6b40e0fe..388db9df40 100644
--- a/application/src/main/java/org/thingsboard/server/service/entitiy/user/TbUserService.java
+++ b/application/src/main/java/org/thingsboard/server/service/entitiy/user/TbUserService.java
@@ -15,13 +15,12 @@
*/
package org.thingsboard.server.service.entitiy.user;
+import jakarta.servlet.http.HttpServletRequest;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.TenantId;
-import jakarta.servlet.http.HttpServletRequest;
-
public interface TbUserService {
User save(TenantId tenantId, CustomerId customerId, User tbUser, boolean sendActivationMail, HttpServletRequest request, User user) throws ThingsboardException;
diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/widgets/type/DefaultWidgetTypeService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/widgets/type/DefaultWidgetTypeService.java
index fab6029886..76027f3621 100644
--- a/application/src/main/java/org/thingsboard/server/service/entitiy/widgets/type/DefaultWidgetTypeService.java
+++ b/application/src/main/java/org/thingsboard/server/service/entitiy/widgets/type/DefaultWidgetTypeService.java
@@ -16,7 +16,6 @@
package org.thingsboard.server.service.entitiy.widgets.type;
import lombok.AllArgsConstructor;
-import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.StringUtils;
@@ -25,7 +24,6 @@ import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.widget.WidgetType;
import org.thingsboard.server.common.data.widget.WidgetTypeDetails;
-import org.thingsboard.server.dao.resource.ImageService;
import org.thingsboard.server.dao.widget.WidgetTypeService;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.entitiy.AbstractTbEntityService;
diff --git a/application/src/main/java/org/thingsboard/server/service/executors/PubSubRuleNodeExecutorProvider.java b/application/src/main/java/org/thingsboard/server/service/executors/PubSubRuleNodeExecutorProvider.java
index 8bfe8cf8a5..c8e30ba471 100644
--- a/application/src/main/java/org/thingsboard/server/service/executors/PubSubRuleNodeExecutorProvider.java
+++ b/application/src/main/java/org/thingsboard/server/service/executors/PubSubRuleNodeExecutorProvider.java
@@ -15,6 +15,8 @@
*/
package org.thingsboard.server.service.executors;
+import jakarta.annotation.PostConstruct;
+import jakarta.annotation.PreDestroy;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;
@@ -22,8 +24,6 @@ import org.thingsboard.common.util.ExecutorProvider;
import org.thingsboard.common.util.ThingsBoardThreadFactory;
import org.thingsboard.server.queue.util.TbRuleEngineComponent;
-import javax.annotation.PostConstruct;
-import javax.annotation.PreDestroy;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
diff --git a/application/src/main/java/org/thingsboard/server/service/executors/SharedEventLoopGroupService.java b/application/src/main/java/org/thingsboard/server/service/executors/SharedEventLoopGroupService.java
index d6d4d8f626..9bc62fbdab 100644
--- a/application/src/main/java/org/thingsboard/server/service/executors/SharedEventLoopGroupService.java
+++ b/application/src/main/java/org/thingsboard/server/service/executors/SharedEventLoopGroupService.java
@@ -17,11 +17,11 @@ package org.thingsboard.server.service.executors;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
+import jakarta.annotation.PostConstruct;
+import jakarta.annotation.PreDestroy;
import lombok.Getter;
import org.springframework.stereotype.Component;
-import jakarta.annotation.PostConstruct;
-import jakarta.annotation.PreDestroy;
import java.util.concurrent.TimeUnit;
@Component
diff --git a/application/src/main/java/org/thingsboard/server/service/housekeeper/HousekeeperReprocessingService.java b/application/src/main/java/org/thingsboard/server/service/housekeeper/HousekeeperReprocessingService.java
index 37a55c99d6..f0a6a58849 100644
--- a/application/src/main/java/org/thingsboard/server/service/housekeeper/HousekeeperReprocessingService.java
+++ b/application/src/main/java/org/thingsboard/server/service/housekeeper/HousekeeperReprocessingService.java
@@ -15,6 +15,7 @@
*/
package org.thingsboard.server.service.housekeeper;
+import jakarta.annotation.PreDestroy;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.springframework.context.annotation.Lazy;
@@ -33,7 +34,6 @@ import org.thingsboard.server.queue.provider.TbCoreQueueFactory;
import org.thingsboard.server.queue.util.AfterStartUp;
import org.thingsboard.server.queue.util.TbCoreComponent;
-import javax.annotation.PreDestroy;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
diff --git a/application/src/main/java/org/thingsboard/server/service/housekeeper/HousekeeperService.java b/application/src/main/java/org/thingsboard/server/service/housekeeper/HousekeeperService.java
index eea92eb75b..933f6fad3e 100644
--- a/application/src/main/java/org/thingsboard/server/service/housekeeper/HousekeeperService.java
+++ b/application/src/main/java/org/thingsboard/server/service/housekeeper/HousekeeperService.java
@@ -15,6 +15,7 @@
*/
package org.thingsboard.server.service.housekeeper;
+import jakarta.annotation.PreDestroy;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
@@ -35,7 +36,6 @@ import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.housekeeper.processor.HousekeeperTaskProcessor;
import org.thingsboard.server.service.housekeeper.stats.HousekeeperStatsService;
-import javax.annotation.PreDestroy;
import java.util.List;
import java.util.Map;
import java.util.Optional;
diff --git a/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/AlarmsUnassignTaskProcessor.java b/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/AlarmsUnassignTaskProcessor.java
index 3ba6ecde52..ba596da500 100644
--- a/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/AlarmsUnassignTaskProcessor.java
+++ b/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/AlarmsUnassignTaskProcessor.java
@@ -18,10 +18,10 @@ package org.thingsboard.server.service.housekeeper.processor;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
+import org.thingsboard.server.common.data.housekeeper.AlarmsUnassignHousekeeperTask;
+import org.thingsboard.server.common.data.housekeeper.HousekeeperTaskType;
import org.thingsboard.server.common.data.id.AlarmId;
import org.thingsboard.server.common.data.id.UserId;
-import org.thingsboard.server.common.data.housekeeper.HousekeeperTaskType;
-import org.thingsboard.server.common.data.housekeeper.AlarmsUnassignHousekeeperTask;
import org.thingsboard.server.service.entitiy.alarm.TbAlarmService;
import java.util.List;
diff --git a/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/AttributesDeletionTaskProcessor.java b/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/AttributesDeletionTaskProcessor.java
index 5514921e2e..337f5ed73c 100644
--- a/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/AttributesDeletionTaskProcessor.java
+++ b/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/AttributesDeletionTaskProcessor.java
@@ -18,9 +18,9 @@ package org.thingsboard.server.service.housekeeper.processor;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
-import org.thingsboard.server.dao.attributes.AttributesService;
import org.thingsboard.server.common.data.housekeeper.HousekeeperTask;
import org.thingsboard.server.common.data.housekeeper.HousekeeperTaskType;
+import org.thingsboard.server.dao.attributes.AttributesService;
@Component
@RequiredArgsConstructor
diff --git a/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/EventsDeletionTaskProcessor.java b/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/EventsDeletionTaskProcessor.java
index bf9c528eb7..4b4865ea56 100644
--- a/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/EventsDeletionTaskProcessor.java
+++ b/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/EventsDeletionTaskProcessor.java
@@ -17,9 +17,9 @@ package org.thingsboard.server.service.housekeeper.processor;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
-import org.thingsboard.server.dao.event.EventService;
import org.thingsboard.server.common.data.housekeeper.HousekeeperTask;
import org.thingsboard.server.common.data.housekeeper.HousekeeperTaskType;
+import org.thingsboard.server.dao.event.EventService;
@Component
@RequiredArgsConstructor
diff --git a/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java b/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java
index 7588726a73..52ef9b203f 100644
--- a/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java
+++ b/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java
@@ -69,7 +69,7 @@ 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.LongDataEntry;
-import org.thingsboard.server.common.data.oauth2.OAuth2Mobile;
+import org.thingsboard.server.common.data.mobile.MobileApp;
import org.thingsboard.server.common.data.page.PageDataIterable;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.query.BooleanFilterPredicate;
@@ -100,7 +100,7 @@ import org.thingsboard.server.dao.device.DeviceService;
import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.dao.notification.NotificationSettingsService;
import org.thingsboard.server.dao.notification.NotificationTargetService;
-import org.thingsboard.server.dao.oauth2.OAuth2MobileDao;
+import org.thingsboard.server.dao.mobile.MobileAppDao;
import org.thingsboard.server.dao.queue.QueueService;
import org.thingsboard.server.dao.rule.RuleChainService;
import org.thingsboard.server.dao.settings.AdminSettingsService;
@@ -149,7 +149,7 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService {
private final DeviceConnectivityConfiguration connectivityConfiguration;
private final QueueService queueService;
private final JwtSettingsService jwtSettingsService;
- private final OAuth2MobileDao oAuth2MobileDao;
+ private final MobileAppDao mobileAppDao;
private final NotificationSettingsService notificationSettingsService;
private final NotificationTargetService notificationTargetService;
@@ -308,17 +308,17 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService {
jwtSettingsService.saveJwtSettings(jwtSettings);
}
- List mobiles = oAuth2MobileDao.find(TenantId.SYS_TENANT_ID);
+ List mobiles = mobileAppDao.findByTenantId(TenantId.SYS_TENANT_ID, new PageLink(Integer.MAX_VALUE,0)).getData();
if (CollectionUtils.isNotEmpty(mobiles)) {
mobiles.stream()
- .filter(config -> !validateKeyLength(config.getAppSecret()))
- .forEach(config -> {
+ .filter(mobileApp -> !validateKeyLength(mobileApp.getAppSecret()))
+ .forEach(mobileApp -> {
log.warn("WARNING: The App secret is shorter than 512 bits, which is a security risk. " +
"A new Application Secret has been added automatically for Mobile Application [{}]. " +
"You can change the Application Secret using the Web UI: " +
- "Navigate to \"Security settings -> OAuth2 -> Mobile applications\" while logged in as a System Administrator.", config.getPkgName());
- config.setAppSecret(generateRandomKey());
- oAuth2MobileDao.save(TenantId.SYS_TENANT_ID, config);
+ "Navigate to \"Security settings -> OAuth2 -> Mobile applications\" while logged in as a System Administrator.", mobileApp.getPkgName());
+ mobileApp.setAppSecret(generateRandomKey());
+ mobileAppDao.save(TenantId.SYS_TENANT_ID, mobileApp);
});
}
}
@@ -577,7 +577,7 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService {
Collections.singletonList(new BasicTsKvEntry(System.currentTimeMillis(), new BooleanDataEntry(key, value))), 0L);
addTsCallback(saveFuture, new TelemetrySaveCallback<>(deviceId, key, value));
} else {
- ListenableFuture> saveFuture = attributesService.save(TenantId.SYS_TENANT_ID, deviceId, AttributeScope.SERVER_SCOPE,
+ ListenableFuture> saveFuture = attributesService.save(TenantId.SYS_TENANT_ID, deviceId, AttributeScope.SERVER_SCOPE,
Collections.singletonList(new BaseAttributeKvEntry(new BooleanDataEntry(key, value)
, System.currentTimeMillis())));
addTsCallback(saveFuture, new TelemetrySaveCallback<>(deviceId, key, value));
diff --git a/application/src/main/java/org/thingsboard/server/service/install/InstallScripts.java b/application/src/main/java/org/thingsboard/server/service/install/InstallScripts.java
index 9db2a99e7f..7f76ce860f 100644
--- a/application/src/main/java/org/thingsboard/server/service/install/InstallScripts.java
+++ b/application/src/main/java/org/thingsboard/server/service/install/InstallScripts.java
@@ -16,6 +16,7 @@
package org.thingsboard.server.service.install;
import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
import lombok.Getter;
import lombok.Setter;
import lombok.SneakyThrows;
@@ -26,8 +27,12 @@ import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.Dashboard;
+import org.thingsboard.server.common.data.DataConstants;
+import org.thingsboard.server.common.data.ImageDescriptor;
+import org.thingsboard.server.common.data.ResourceSubType;
import org.thingsboard.server.common.data.ResourceType;
import org.thingsboard.server.common.data.TbResource;
+import org.thingsboard.server.common.data.TbResourceInfo;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.TenantId;
@@ -43,8 +48,10 @@ import org.thingsboard.server.common.data.widget.WidgetsBundle;
import org.thingsboard.server.dao.dashboard.DashboardService;
import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.dao.oauth2.OAuth2ConfigTemplateService;
+import org.thingsboard.server.dao.resource.ImageService;
import org.thingsboard.server.dao.resource.ResourceService;
import org.thingsboard.server.dao.rule.RuleChainService;
+import org.thingsboard.server.dao.util.ImageUtils;
import org.thingsboard.server.dao.widget.WidgetTypeService;
import org.thingsboard.server.dao.widget.WidgetsBundleService;
import org.thingsboard.server.service.install.update.ImagesUpdater;
@@ -84,12 +91,14 @@ public class InstallScripts {
public static final String RULE_CHAINS_DIR = "rule_chains";
public static final String WIDGET_TYPES_DIR = "widget_types";
public static final String WIDGET_BUNDLES_DIR = "widget_bundles";
+ public static final String SCADA_SYMBOLS_DIR = "scada_symbols";
public static final String OAUTH2_CONFIG_TEMPLATES_DIR = "oauth2_config_templates";
public static final String DASHBOARDS_DIR = "dashboards";
public static final String MODELS_LWM2M_DIR = "lwm2m-registry";
public static final String CREDENTIALS_DIR = "credentials";
public static final String JSON_EXT = ".json";
+ public static final String SVG_EXT = ".svg";
public static final String XML_EXT = ".xml";
@Value("${install.data_dir:}")
@@ -118,6 +127,9 @@ public class InstallScripts {
@Getter @Setter
private boolean updateImages = false;
+ @Autowired
+ private ImageService imageService;
+
Path getTenantRuleChainsDir() {
return Paths.get(getDataDir(), JSON_DIR, TENANT_DIR, RULE_CHAINS_DIR);
}
@@ -253,6 +265,7 @@ public class InstallScripts {
);
}
}
+ this.loadSystemScadaSymbols();
for (var widgetsBundleDescriptorEntry : widgetsBundlesMap.entrySet()) {
Path path = widgetsBundleDescriptorEntry.getKey();
try {
@@ -290,6 +303,94 @@ public class InstallScripts {
}
}
+ private void loadSystemScadaSymbols() throws Exception {
+ log.info("Loading system SCADA symbols");
+ Path scadaSymbolsDir = Paths.get(getDataDir(), JSON_DIR, SYSTEM_DIR, SCADA_SYMBOLS_DIR);
+ if (Files.exists(scadaSymbolsDir)) {
+ WidgetTypeDetails scadaSymbolWidgetTemplate = widgetTypeService.findWidgetTypeDetailsByTenantIdAndFqn(TenantId.SYS_TENANT_ID, "scada_symbol");
+ try (DirectoryStream dirStream = Files.newDirectoryStream(scadaSymbolsDir, path -> path.toString().endsWith(SVG_EXT))) {
+ dirStream.forEach(
+ path -> {
+ try {
+ var fileName = path.getFileName().toString();
+ var scadaSymbolData = Files.readAllBytes(path);
+ var metadata = ImageUtils.processScadaSymbolMetadata(fileName, scadaSymbolData);
+ TbResourceInfo savedScadaSymbol = saveScadaSymbol(metadata, fileName, scadaSymbolData);
+ if (scadaSymbolWidgetTemplate != null) {
+ saveScadaSymbolWidget(scadaSymbolWidgetTemplate, savedScadaSymbol, metadata);
+ }
+ } catch (Exception e) {
+ log.error("Unable to load SCADA symbol from file: [{}]", path.toString());
+ throw new RuntimeException("Unable to load SCADA symbol from file", e);
+ }
+ }
+ );
+ }
+ }
+ }
+
+ private TbResourceInfo saveScadaSymbol(ImageUtils.ScadaSymbolMetadataInfo metadata, String fileName, byte[] scadaSymbolData) {
+ String etag = imageService.calculateImageEtag(scadaSymbolData);
+ var existingImage = imageService.findSystemOrTenantImageByEtag(TenantId.SYS_TENANT_ID, etag);
+ if (existingImage != null && ResourceSubType.SCADA_SYMBOL.equals(existingImage.getResourceSubType())) {
+ return existingImage;
+ } else {
+ var existing = imageService.getImageInfoByTenantIdAndKey(TenantId.SYS_TENANT_ID, fileName);
+ if (existing != null && ResourceSubType.SCADA_SYMBOL.equals(existing.getResourceSubType())) {
+ imageService.deleteImage(existing, true);
+ }
+ TbResource image = new TbResource();
+ image.setTenantId(TenantId.SYS_TENANT_ID);
+ image.setFileName(fileName);
+ image.setTitle(metadata.getTitle());
+ image.setResourceSubType(ResourceSubType.SCADA_SYMBOL);
+ image.setResourceType(ResourceType.IMAGE);
+ image.setPublic(true);
+ ImageDescriptor descriptor = new ImageDescriptor();
+ descriptor.setMediaType("image/svg+xml");
+ image.setDescriptorValue(descriptor);
+ image.setData(scadaSymbolData);
+ return imageService.saveImage(image);
+ }
+ }
+
+ private WidgetTypeDetails saveScadaSymbolWidget(WidgetTypeDetails template, TbResourceInfo scadaSymbol,
+ ImageUtils.ScadaSymbolMetadataInfo metadata) {
+ String symbolUrl = DataConstants.TB_IMAGE_PREFIX + scadaSymbol.getLink();
+ WidgetTypeDetails scadaSymbolWidget = new WidgetTypeDetails();
+ JsonNode descriptor = JacksonUtil.clone(template.getDescriptor());
+ scadaSymbolWidget.setDescriptor(descriptor);
+ scadaSymbolWidget.setName(metadata.getTitle());
+ scadaSymbolWidget.setImage(symbolUrl);
+ scadaSymbolWidget.setDescription(metadata.getDescription());
+ scadaSymbolWidget.setTags(metadata.getSearchTags());
+ scadaSymbolWidget.setScada(true);
+ ObjectNode defaultConfig = null;
+ if (descriptor.has("defaultConfig")) {
+ defaultConfig = JacksonUtil.fromString(descriptor.get("defaultConfig").asText(), ObjectNode.class);
+ }
+ if (defaultConfig == null) {
+ defaultConfig = JacksonUtil.newObjectNode();
+ }
+ defaultConfig.put("title", metadata.getTitle());
+ ObjectNode settings;
+ if (defaultConfig.has("settings")) {
+ settings = (ObjectNode)defaultConfig.get("settings");
+ } else {
+ settings = JacksonUtil.newObjectNode();
+ defaultConfig.set("settings", settings);
+ }
+ settings.put("scadaSymbolUrl", symbolUrl);
+ ((ObjectNode)descriptor).put("defaultConfig", JacksonUtil.toString(defaultConfig));
+ ((ObjectNode)descriptor).put("sizeX", metadata.getWidgetSizeX());
+ ((ObjectNode)descriptor).put("sizeY", metadata.getWidgetSizeY());
+ String controllerScript = descriptor.get("controllerScript").asText();
+ controllerScript = controllerScript.replaceAll("previewWidth: '\\d*px'", "previewWidth: '" + (metadata.getWidgetSizeX() * 100) + "px'");
+ controllerScript = controllerScript.replaceAll("previewHeight: '\\d*px'", "previewHeight: '" + (metadata.getWidgetSizeY() * 100 + 20) + "px'");
+ ((ObjectNode)descriptor).put("controllerScript", controllerScript);
+ return widgetTypeService.saveWidgetType(scadaSymbolWidget);
+ }
+
private void deleteSystemWidgetBundle(String bundleAlias) {
WidgetsBundle widgetsBundle = widgetsBundleService.findWidgetsBundleByTenantIdAndAlias(TenantId.SYS_TENANT_ID, bundleAlias);
if (widgetsBundle != null) {
diff --git a/application/src/main/java/org/thingsboard/server/service/install/migrate/CassandraTsLatestToSqlMigrateService.java b/application/src/main/java/org/thingsboard/server/service/install/migrate/CassandraTsLatestToSqlMigrateService.java
index 9cad1d614f..b2ea8f9f59 100644
--- a/application/src/main/java/org/thingsboard/server/service/install/migrate/CassandraTsLatestToSqlMigrateService.java
+++ b/application/src/main/java/org/thingsboard/server/service/install/migrate/CassandraTsLatestToSqlMigrateService.java
@@ -24,8 +24,8 @@ import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.UUIDConverter;
import org.thingsboard.server.dao.cassandra.CassandraCluster;
-import org.thingsboard.server.dao.model.sqlts.dictionary.KeyDictionaryEntry;
import org.thingsboard.server.dao.model.sqlts.dictionary.KeyDictionaryCompositeKey;
+import org.thingsboard.server.dao.model.sqlts.dictionary.KeyDictionaryEntry;
import org.thingsboard.server.dao.model.sqlts.latest.TsKvLatestEntity;
import org.thingsboard.server.dao.sqlts.dictionary.KeyDictionaryRepository;
import org.thingsboard.server.dao.sqlts.insert.latest.InsertLatestTsRepository;
diff --git a/application/src/main/java/org/thingsboard/server/service/install/update/DefaultCacheCleanupService.java b/application/src/main/java/org/thingsboard/server/service/install/update/DefaultCacheCleanupService.java
index 7fd92bc5c6..0085df35d7 100644
--- a/application/src/main/java/org/thingsboard/server/service/install/update/DefaultCacheCleanupService.java
+++ b/application/src/main/java/org/thingsboard/server/service/install/update/DefaultCacheCleanupService.java
@@ -61,6 +61,10 @@ public class DefaultCacheCleanupService implements CacheCleanupService {
log.info("Clearing cache to upgrade from version 3.6.4 to 3.7.0");
clearAll();
break;
+ case "3.7.0":
+ log.info("Clearing cache to upgrade from version 3.7.0 to 3.7.1");
+ clearAll();
+ break;
default:
//Do nothing, since cache cleanup is optional.
}
@@ -81,7 +85,7 @@ public class DefaultCacheCleanupService implements CacheCleanupService {
if (redisTemplate.isPresent()) {
log.info("Flushing all caches");
redisTemplate.get().execute((RedisCallback