Commit d79d00aa87a676a907e108cf7990095f126a21c5

Authored by Volodymyr Babak
2 parents 2ddbe0af 146e6126

Merge remote-tracking branch 'upstream/master' into develop/3.3-edge

Showing 48 changed files with 2157 additions and 1574 deletions

Too many changes to show.

To preserve performance only 48 of 683 files are displayed.

... ... @@ -33,4 +33,6 @@ pom.xml.versionsBackup
33 33 **/.env
34 34 .instance_id
35 35 rebuild-docker.sh
36   -.run/
\ No newline at end of file
  36 +.run/
  37 +.run/**
  38 +.run
\ No newline at end of file
... ...
... ... @@ -150,10 +150,6 @@
150 150 <artifactId>spring-boot-starter-websocket</artifactId>
151 151 </dependency>
152 152 <dependency>
153   - <groupId>org.springframework.cloud</groupId>
154   - <artifactId>spring-cloud-starter-oauth2</artifactId>
155   - </dependency>
156   - <dependency>
157 153 <groupId>org.springframework.security</groupId>
158 154 <artifactId>spring-security-oauth2-client</artifactId>
159 155 </dependency>
... ... @@ -202,6 +198,14 @@
202 198 <artifactId>javax.mail</artifactId>
203 199 </dependency>
204 200 <dependency>
  201 + <groupId>com.twilio.sdk</groupId>
  202 + <artifactId>twilio</artifactId>
  203 + </dependency>
  204 + <dependency>
  205 + <groupId>com.amazonaws</groupId>
  206 + <artifactId>aws-java-sdk-sns</artifactId>
  207 + </dependency>
  208 + <dependency>
205 209 <groupId>org.apache.curator</groupId>
206 210 <artifactId>curator-recipes</artifactId>
207 211 </dependency>
... ...
... ... @@ -955,7 +955,7 @@
955 955 },
956 956 "methodName": "gateway_restart",
957 957 "methodParams": "{}",
958   - "buttonText": "gateway restart"
  958 + "buttonText": "GATEWAY RESTART"
959 959 },
960 960 "title": "New RPC Button",
961 961 "dropShadow": true,
... ...
1   -{
2   - "title": "Raspberry PI GPIO Demo Dashboard",
3   - "configuration": {
4   - "description": "Demo dashboard for Raspberry PI GPIO Demo",
5   - "widgets": {
6   - "602177f6-267b-cb87-4e8f-e23d7fb2f61c": {
7   - "isSystemType": true,
8   - "bundleAlias": "gpio_widgets",
9   - "typeAlias": "raspberry_pi_gpio_control",
10   - "type": "rpc",
11   - "title": "New widget",
12   - "sizeX": 6,
13   - "sizeY": 10,
14   - "config": {
15   - "targetDeviceAliases": [],
16   - "showTitle": true,
17   - "backgroundColor": "#fff",
18   - "color": "rgba(0, 0, 0, 0.87)",
19   - "padding": "0px",
20   - "settings": {
21   - "parseGpioStatusFunction": "return body[pin] === true;",
22   - "gpioStatusChangeRequest": {
23   - "method": "setGpioStatus",
24   - "paramsBody": "{\n \"pin\": \"{$pin}\",\n \"enabled\": \"{$enabled}\"\n}"
25   - },
26   - "requestTimeout": 500,
27   - "switchPanelBackgroundColor": "#008a00",
28   - "gpioStatusRequest": {
29   - "method": "getGpioStatus",
30   - "paramsBody": "{}"
31   - },
32   - "gpioList": [
33   - {
34   - "pin": 7,
35   - "label": "GPIO 4 (GPCLK0)",
36   - "row": 3,
37   - "col": 0,
38   - "_uniqueKey": 0
39   - },
40   - {
41   - "pin": 11,
42   - "label": "GPIO 17",
43   - "row": 5,
44   - "col": 0,
45   - "_uniqueKey": 1
46   - },
47   - {
48   - "pin": 12,
49   - "label": "GPIO 18",
50   - "row": 5,
51   - "col": 1,
52   - "_uniqueKey": 2
53   - },
54   - {
55   - "_uniqueKey": 3,
56   - "pin": 13,
57   - "label": "GPIO 27",
58   - "row": 6,
59   - "col": 0
60   - },
61   - {
62   - "_uniqueKey": 4,
63   - "pin": 15,
64   - "label": "GPIO 22",
65   - "row": 7,
66   - "col": 0
67   - },
68   - {
69   - "_uniqueKey": 5,
70   - "pin": 16,
71   - "label": "GPIO 23",
72   - "row": 7,
73   - "col": 1
74   - },
75   - {
76   - "_uniqueKey": 6,
77   - "pin": 18,
78   - "label": "GPIO 24",
79   - "row": 8,
80   - "col": 1
81   - },
82   - {
83   - "_uniqueKey": 7,
84   - "pin": 22,
85   - "label": "GPIO 25",
86   - "row": 10,
87   - "col": 1
88   - },
89   - {
90   - "_uniqueKey": 8,
91   - "pin": 29,
92   - "label": "GPIO 5",
93   - "row": 14,
94   - "col": 0
95   - },
96   - {
97   - "_uniqueKey": 9,
98   - "pin": 31,
99   - "label": "GPIO 6",
100   - "row": 15,
101   - "col": 0
102   - },
103   - {
104   - "_uniqueKey": 10,
105   - "pin": 32,
106   - "label": "GPIO 12",
107   - "row": 15,
108   - "col": 1
109   - },
110   - {
111   - "_uniqueKey": 11,
112   - "pin": 33,
113   - "label": "GPIO 13",
114   - "row": 16,
115   - "col": 0
116   - },
117   - {
118   - "_uniqueKey": 12,
119   - "pin": 35,
120   - "label": "GPIO 19",
121   - "row": 17,
122   - "col": 0
123   - },
124   - {
125   - "_uniqueKey": 13,
126   - "pin": 36,
127   - "label": "GPIO 16",
128   - "row": 17,
129   - "col": 1
130   - },
131   - {
132   - "_uniqueKey": 14,
133   - "pin": 37,
134   - "label": "GPIO 26",
135   - "row": 18,
136   - "col": 0
137   - },
138   - {
139   - "_uniqueKey": 15,
140   - "pin": 38,
141   - "label": "GPIO 20",
142   - "row": 18,
143   - "col": 1
144   - },
145   - {
146   - "_uniqueKey": 16,
147   - "pin": 40,
148   - "label": "GPIO 21",
149   - "row": 19,
150   - "col": 1
151   - }
152   - ]
153   - },
154   - "title": "Raspberry Pi GPIO Control Panel",
155   - "datasources": [],
156   - "targetDeviceAliasIds": [
157   - "f26b12b6-6938-e1a0-85ec-d88a1f23e382"
158   - ]
159   - },
160   - "row": 0,
161   - "col": 0,
162   - "id": "602177f6-267b-cb87-4e8f-e23d7fb2f61c"
163   - },
164   - "3cca52a5-e874-eb43-b444-8efa01e663c8": {
165   - "isSystemType": true,
166   - "bundleAlias": "gpio_widgets",
167   - "typeAlias": "raspberry_pi_gpio_panel",
168   - "type": "latest",
169   - "title": "New widget",
170   - "sizeX": 7,
171   - "sizeY": 10,
172   - "config": {
173   - "showTitle": true,
174   - "backgroundColor": "#fff",
175   - "color": "rgba(0, 0, 0, 0.87)",
176   - "padding": "0px",
177   - "settings": {
178   - "gpioList": [
179   - {
180   - "pin": 1,
181   - "label": "3.3V",
182   - "row": 0,
183   - "col": 0,
184   - "color": "#fc9700",
185   - "_uniqueKey": 0
186   - },
187   - {
188   - "pin": 2,
189   - "label": "5V",
190   - "row": 0,
191   - "col": 1,
192   - "color": "#fb0000",
193   - "_uniqueKey": 1
194   - },
195   - {
196   - "pin": 3,
197   - "label": "GPIO 2 (I2C1_SDA)",
198   - "row": 1,
199   - "col": 0,
200   - "color": "#02fefb",
201   - "_uniqueKey": 2
202   - },
203   - {
204   - "color": "#fb0000",
205   - "pin": 4,
206   - "label": "5V",
207   - "row": 1,
208   - "col": 1
209   - },
210   - {
211   - "color": "#02fefb",
212   - "pin": 5,
213   - "label": "GPIO 3 (I2C1_SCL)",
214   - "row": 2,
215   - "col": 0
216   - },
217   - {
218   - "color": "#000000",
219   - "pin": 6,
220   - "label": "GND",
221   - "row": 2,
222   - "col": 1
223   - },
224   - {
225   - "color": "#00fd00",
226   - "pin": 7,
227   - "label": "GPIO 4 (GPCLK0)",
228   - "row": 3,
229   - "col": 0
230   - },
231   - {
232   - "color": "#fdfb00",
233   - "pin": 8,
234   - "label": "GPIO 14 (UART_TXD)",
235   - "row": 3,
236   - "col": 1
237   - },
238   - {
239   - "color": "#000000",
240   - "pin": 9,
241   - "label": "GND",
242   - "row": 4,
243   - "col": 0
244   - },
245   - {
246   - "color": "#fdfb00",
247   - "pin": 10,
248   - "label": "GPIO 15 (UART_RXD)",
249   - "row": 4,
250   - "col": 1
251   - },
252   - {
253   - "color": "#00fd00",
254   - "pin": 11,
255   - "label": "GPIO 17",
256   - "row": 5,
257   - "col": 0
258   - },
259   - {
260   - "color": "#00fd00",
261   - "pin": 12,
262   - "label": "GPIO 18",
263   - "row": 5,
264   - "col": 1
265   - },
266   - {
267   - "color": "#00fd00",
268   - "pin": 13,
269   - "label": "GPIO 27",
270   - "row": 6,
271   - "col": 0
272   - },
273   - {
274   - "color": "#000000",
275   - "pin": 14,
276   - "label": "GND",
277   - "row": 6,
278   - "col": 1
279   - },
280   - {
281   - "color": "#00fd00",
282   - "pin": 15,
283   - "label": "GPIO 22",
284   - "row": 7,
285   - "col": 0
286   - },
287   - {
288   - "color": "#00fd00",
289   - "pin": 16,
290   - "label": "GPIO 23",
291   - "row": 7,
292   - "col": 1
293   - },
294   - {
295   - "color": "#fc9700",
296   - "pin": 17,
297   - "label": "3.3V",
298   - "row": 8,
299   - "col": 0
300   - },
301   - {
302   - "color": "#00fd00",
303   - "pin": 18,
304   - "label": "GPIO 24",
305   - "row": 8,
306   - "col": 1
307   - },
308   - {
309   - "color": "#fd01fd",
310   - "pin": 19,
311   - "label": "GPIO 10 (SPI_MOSI)",
312   - "row": 9,
313   - "col": 0
314   - },
315   - {
316   - "color": "#000000",
317   - "pin": 20,
318   - "label": "GND",
319   - "row": 9,
320   - "col": 1
321   - },
322   - {
323   - "color": "#fd01fd",
324   - "pin": 21,
325   - "label": "GPIO 9 (SPI_MISO)",
326   - "row": 10,
327   - "col": 0
328   - },
329   - {
330   - "color": "#00fd00",
331   - "pin": 22,
332   - "label": "GPIO 25",
333   - "row": 10,
334   - "col": 1
335   - },
336   - {
337   - "color": "#fd01fd",
338   - "pin": 23,
339   - "label": "GPIO 11 (SPI_SCLK)",
340   - "row": 11,
341   - "col": 0
342   - },
343   - {
344   - "color": "#fd01fd",
345   - "pin": 24,
346   - "label": "GPIO 8 (SPI_CE0)",
347   - "row": 11,
348   - "col": 1
349   - },
350   - {
351   - "color": "#000000",
352   - "pin": 25,
353   - "label": "GND",
354   - "row": 12,
355   - "col": 0
356   - },
357   - {
358   - "color": "#fd01fd",
359   - "pin": 26,
360   - "label": "GPIO 7 (SPI_CE1)",
361   - "row": 12,
362   - "col": 1
363   - },
364   - {
365   - "color": "#ffffff",
366   - "pin": 27,
367   - "label": "ID_SD",
368   - "row": 13,
369   - "col": 0
370   - },
371   - {
372   - "color": "#ffffff",
373   - "pin": 28,
374   - "label": "ID_SC",
375   - "row": 13,
376   - "col": 1
377   - },
378   - {
379   - "color": "#00fd00",
380   - "pin": 29,
381   - "label": "GPIO 5",
382   - "row": 14,
383   - "col": 0
384   - },
385   - {
386   - "color": "#000000",
387   - "pin": 30,
388   - "label": "GND",
389   - "row": 14,
390   - "col": 1
391   - },
392   - {
393   - "color": "#00fd00",
394   - "pin": 31,
395   - "label": "GPIO 6",
396   - "row": 15,
397   - "col": 0
398   - },
399   - {
400   - "color": "#00fd00",
401   - "pin": 32,
402   - "label": "GPIO 12",
403   - "row": 15,
404   - "col": 1
405   - },
406   - {
407   - "color": "#00fd00",
408   - "pin": 33,
409   - "label": "GPIO 13",
410   - "row": 16,
411   - "col": 0
412   - },
413   - {
414   - "color": "#000000",
415   - "pin": 34,
416   - "label": "GND",
417   - "row": 16,
418   - "col": 1
419   - },
420   - {
421   - "color": "#00fd00",
422   - "pin": 35,
423   - "label": "GPIO 19",
424   - "row": 17,
425   - "col": 0
426   - },
427   - {
428   - "color": "#00fd00",
429   - "pin": 36,
430   - "label": "GPIO 16",
431   - "row": 17,
432   - "col": 1
433   - },
434   - {
435   - "color": "#00fd00",
436   - "pin": 37,
437   - "label": "GPIO 26",
438   - "row": 18,
439   - "col": 0
440   - },
441   - {
442   - "color": "#00fd00",
443   - "pin": 38,
444   - "label": "GPIO 20",
445   - "row": 18,
446   - "col": 1
447   - },
448   - {
449   - "color": "#000000",
450   - "pin": 39,
451   - "label": "GND",
452   - "row": 19,
453   - "col": 0
454   - },
455   - {
456   - "color": "#00fd00",
457   - "pin": 40,
458   - "label": "GPIO 21",
459   - "row": 19,
460   - "col": 1
461   - }
462   - ],
463   - "ledPanelBackgroundColor": "#008a00"
464   - },
465   - "title": "Raspberry Pi GPIO Status Panel",
466   - "datasources": [
467   - {
468   - "type": "entity",
469   - "dataKeys": [
470   - {
471   - "name": "7",
472   - "type": "attribute",
473   - "label": "7",
474   - "color": "#2196f3",
475   - "settings": {},
476   - "_hash": 0.20925966435886978
477   - },
478   - {
479   - "name": "11",
480   - "type": "attribute",
481   - "label": "11",
482   - "color": "#4caf50",
483   - "settings": {},
484   - "_hash": 0.330267349594344
485   - },
486   - {
487   - "name": "12",
488   - "type": "attribute",
489   - "label": "12",
490   - "color": "#f44336",
491   - "settings": {},
492   - "_hash": 0.5040578704481748
493   - },
494   - {
495   - "name": "13",
496   - "type": "attribute",
497   - "label": "13",
498   - "color": "#ffc107",
499   - "settings": {},
500   - "_hash": 0.588956328191639
501   - },
502   - {
503   - "name": "15",
504   - "type": "attribute",
505   - "label": "15",
506   - "color": "#607d8b",
507   - "settings": {},
508   - "_hash": 0.9229040530336119
509   - },
510   - {
511   - "name": "16",
512   - "type": "attribute",
513   - "label": "16",
514   - "color": "#9c27b0",
515   - "settings": {},
516   - "_hash": 0.8692315253041654
517   - },
518   - {
519   - "name": "18",
520   - "type": "attribute",
521   - "label": "18",
522   - "color": "#8bc34a",
523   - "settings": {},
524   - "_hash": 0.41465562857521543
525   - },
526   - {
527   - "name": "22",
528   - "type": "attribute",
529   - "label": "22",
530   - "color": "#3f51b5",
531   - "settings": {},
532   - "_hash": 0.36135260043112827
533   - },
534   - {
535   - "name": "29",
536   - "type": "attribute",
537   - "label": "29",
538   - "color": "#e91e63",
539   - "settings": {},
540   - "_hash": 0.9904592276182183
541   - },
542   - {
543   - "name": "31",
544   - "type": "attribute",
545   - "label": "31",
546   - "color": "#ffeb3b",
547   - "settings": {},
548   - "_hash": 0.038330985429919195
549   - },
550   - {
551   - "name": "32",
552   - "type": "attribute",
553   - "label": "32",
554   - "color": "#03a9f4",
555   - "settings": {},
556   - "_hash": 0.4334683890135089
557   - },
558   - {
559   - "name": "33",
560   - "type": "attribute",
561   - "label": "33",
562   - "color": "#ff9800",
563   - "settings": {},
564   - "_hash": 0.6487255992492305
565   - },
566   - {
567   - "name": "35",
568   - "type": "attribute",
569   - "label": "35",
570   - "color": "#673ab7",
571   - "settings": {},
572   - "_hash": 0.971555321150732
573   - },
574   - {
575   - "name": "36",
576   - "type": "attribute",
577   - "label": "36",
578   - "color": "#cddc39",
579   - "settings": {},
580   - "_hash": 0.7826129728424382
581   - },
582   - {
583   - "name": "37",
584   - "type": "attribute",
585   - "label": "37",
586   - "color": "#009688",
587   - "settings": {},
588   - "_hash": 0.44925676517537627
589   - },
590   - {
591   - "name": "38",
592   - "type": "attribute",
593   - "label": "38",
594   - "color": "#795548",
595   - "settings": {},
596   - "_hash": 0.051518155759787465
597   - },
598   - {
599   - "name": "40",
600   - "type": "attribute",
601   - "label": "40",
602   - "color": "#00bcd4",
603   - "settings": {},
604   - "_hash": 0.8733296686871144
605   - }
606   - ],
607   - "name": "RPi",
608   - "entityAliasId": "f26b12b6-6938-e1a0-85ec-d88a1f23e382"
609   - }
610   - ],
611   - "timewindow": {
612   - "realtime": {
613   - "timewindowMs": 60000
614   - }
615   - }
616   - },
617   - "row": 0,
618   - "col": 6,
619   - "id": "3cca52a5-e874-eb43-b444-8efa01e663c8"
620   - }
621   - },
622   - "states": {
623   - "default": {
624   - "name": "Default",
625   - "root": true,
626   - "layouts": {
627   - "main": {
628   - "widgets": {
629   - "602177f6-267b-cb87-4e8f-e23d7fb2f61c": {
630   - "sizeX": 6,
631   - "sizeY": 10,
632   - "row": 0,
633   - "col": 0
634   - },
635   - "3cca52a5-e874-eb43-b444-8efa01e663c8": {
636   - "sizeX": 7,
637   - "sizeY": 10,
638   - "row": 0,
639   - "col": 6
640   - }
641   - },
642   - "gridSettings": {
643   - "backgroundColor": "#eeeeee",
644   - "color": "rgba(0,0,0,0.870588)",
645   - "columns": 24,
646   - "margins": [
647   - 10,
648   - 10
649   - ],
650   - "backgroundSizeMode": "100%"
651   - }
652   - }
653   - }
654   - }
655   - },
656   - "entityAliases": {
657   - "f26b12b6-6938-e1a0-85ec-d88a1f23e382": {
658   - "id": "f26b12b6-6938-e1a0-85ec-d88a1f23e382",
659   - "alias": "RPi",
660   - "filter": {
661   - "type": "entityName",
662   - "resolveMultiple": false,
663   - "entityType": "DEVICE",
664   - "entityNameFilter": "Raspberry Pi Demo Device"
665   - }
666   - }
667   - },
668   - "timewindow": {
669   - "displayValue": "",
670   - "selectedTab": 0,
671   - "realtime": {
672   - "interval": 1000,
673   - "timewindowMs": 60000
674   - },
675   - "history": {
676   - "historyType": 0,
677   - "interval": 1000,
678   - "timewindowMs": 60000,
679   - "fixedTimewindow": {
680   - "startTimeMs": 1498653734150,
681   - "endTimeMs": 1498740134150
682   - }
683   - },
684   - "aggregation": {
685   - "type": "AVG",
686   - "limit": 200
687   - }
688   - },
689   - "settings": {
690   - "stateControllerId": "default",
691   - "showTitle": true,
692   - "showDashboardsSelect": true,
693   - "showEntitiesSelect": true,
694   - "showDashboardTimewindow": true,
695   - "showDashboardExport": true,
696   - "toolbarAlwaysOpen": false
697   - }
698   - },
699   - "name": "Raspberry PI GPIO Demo Dashboard"
700   -}
\ No newline at end of file
1   -{
2   - "title": "Temperature & Humidity Demo Dashboard",
3   - "configuration": {
4   - "description": "Demo dashboard for sample applications that upload temperature and humidity received from DHT11 or DHT22 sensors",
5   - "widgets": {
6   - "03e06986-1c50-e9e4-267c-2bae930ad9a2": {
7   - "isSystemType": true,
8   - "bundleAlias": "digital_gauges",
9   - "typeAlias": "digital_thermometer",
10   - "type": "latest",
11   - "title": "New widget",
12   - "sizeX": 5,
13   - "sizeY": 5,
14   - "config": {
15   - "datasources": [
16   - {
17   - "type": "entity",
18   - "dataKeys": [
19   - {
20   - "name": "temperature",
21   - "type": "timeseries",
22   - "label": "temperature",
23   - "color": "#2196f3",
24   - "settings": {},
25   - "_hash": 0.3720839051412099
26   - }
27   - ],
28   - "name": "DHT11",
29   - "entityAliasId": "63a93238-c13f-4403-4bcc-9ccc86bd6a62"
30   - }
31   - ],
32   - "timewindow": {
33   - "realtime": {
34   - "timewindowMs": 60000
35   - }
36   - },
37   - "showTitle": false,
38   - "backgroundColor": "#000000",
39   - "color": "rgba(0, 0, 0, 0.87)",
40   - "padding": "0px",
41   - "settings": {
42   - "maxValue": 50,
43   - "donutStartAngle": 90,
44   - "showValue": true,
45   - "showMinMax": true,
46   - "gaugeWidthScale": 1,
47   - "levelColors": [
48   - "#304ffe",
49   - "#7e57c2",
50   - "#ff4081",
51   - "#d32f2f"
52   - ],
53   - "refreshAnimationType": "<>",
54   - "refreshAnimationTime": 700,
55   - "startAnimationType": "<>",
56   - "startAnimationTime": 700,
57   - "titleFont": {
58   - "family": "RobotoDraft",
59   - "size": 12,
60   - "style": "normal",
61   - "weight": "500"
62   - },
63   - "labelFont": {
64   - "family": "RobotoDraft",
65   - "size": 8,
66   - "style": "normal",
67   - "weight": "500"
68   - },
69   - "valueFont": {
70   - "family": "Segment7Standard",
71   - "style": "normal",
72   - "weight": "500",
73   - "size": 18
74   - },
75   - "minMaxFont": {
76   - "family": "Segment7Standard",
77   - "size": 12,
78   - "style": "normal",
79   - "weight": "500"
80   - },
81   - "dashThickness": 1.5,
82   - "decimals": 0,
83   - "minValue": 0,
84   - "units": "°C",
85   - "gaugeColor": "#333333",
86   - "neonGlowBrightness": 35,
87   - "gaugeType": "donut",
88   - "showTitle": false
89   - },
90   - "title": "Temperature"
91   - },
92   - "row": 0,
93   - "col": 0,
94   - "id": "03e06986-1c50-e9e4-267c-2bae930ad9a2"
95   - },
96   - "88808eb1-d381-9970-c852-e3499df68bd8": {
97   - "isSystemType": true,
98   - "bundleAlias": "digital_gauges",
99   - "typeAlias": "digital_vertical_bar",
100   - "type": "latest",
101   - "title": "New widget",
102   - "sizeX": 3,
103   - "sizeY": 5,
104   - "config": {
105   - "datasources": [
106   - {
107   - "type": "entity",
108   - "dataKeys": [
109   - {
110   - "name": "humidity",
111   - "type": "timeseries",
112   - "label": "humidity",
113   - "color": "#2196f3",
114   - "settings": {},
115   - "_hash": 0.9492802776509441
116   - }
117   - ],
118   - "name": "DHT11",
119   - "entityAliasId": "63a93238-c13f-4403-4bcc-9ccc86bd6a62"
120   - }
121   - ],
122   - "timewindow": {
123   - "realtime": {
124   - "timewindowMs": 60000
125   - }
126   - },
127   - "showTitle": false,
128   - "backgroundColor": "#000000",
129   - "color": "rgba(0, 0, 0, 0.87)",
130   - "padding": "0px",
131   - "settings": {
132   - "maxValue": 100,
133   - "donutStartAngle": 90,
134   - "showValue": true,
135   - "showMinMax": true,
136   - "gaugeWidthScale": 0.75,
137   - "levelColors": [
138   - "#3d5afe",
139   - "#f44336"
140   - ],
141   - "refreshAnimationType": "<>",
142   - "refreshAnimationTime": 700,
143   - "startAnimationType": "<>",
144   - "startAnimationTime": 700,
145   - "titleFont": {
146   - "family": "RobotoDraft",
147   - "size": 12,
148   - "style": "normal",
149   - "weight": "500"
150   - },
151   - "labelFont": {
152   - "family": "RobotoDraft",
153   - "size": 8,
154   - "style": "normal",
155   - "weight": "500"
156   - },
157   - "valueFont": {
158   - "family": "Segment7Standard",
159   - "style": "normal",
160   - "weight": "500",
161   - "size": 14
162   - },
163   - "minMaxFont": {
164   - "family": "Segment7Standard",
165   - "size": 8,
166   - "style": "normal",
167   - "weight": "normal",
168   - "color": "#cccccc"
169   - },
170   - "neonGlowBrightness": 20,
171   - "decimals": 0,
172   - "showUnitTitle": true,
173   - "gaugeColor": "#171a1c",
174   - "gaugeType": "verticalBar",
175   - "showTitle": false,
176   - "minValue": 0,
177   - "dashThickness": 1.2
178   - },
179   - "title": "Humidity"
180   - },
181   - "row": 0,
182   - "col": 5,
183   - "id": "88808eb1-d381-9970-c852-e3499df68bd8"
184   - }
185   - },
186   - "states": {
187   - "default": {
188   - "name": "Default",
189   - "root": true,
190   - "layouts": {
191   - "main": {
192   - "widgets": {
193   - "03e06986-1c50-e9e4-267c-2bae930ad9a2": {
194   - "sizeX": 5,
195   - "sizeY": 5,
196   - "row": 0,
197   - "col": 0
198   - },
199   - "88808eb1-d381-9970-c852-e3499df68bd8": {
200   - "sizeX": 3,
201   - "sizeY": 5,
202   - "row": 0,
203   - "col": 5
204   - }
205   - },
206   - "gridSettings": {
207   - "backgroundColor": "#eeeeee",
208   - "color": "rgba(0,0,0,0.870588)",
209   - "columns": 24,
210   - "margins": [
211   - 10,
212   - 10
213   - ],
214   - "backgroundSizeMode": "100%"
215   - }
216   - }
217   - }
218   - }
219   - },
220   - "entityAliases": {
221   - "63a93238-c13f-4403-4bcc-9ccc86bd6a62": {
222   - "id": "63a93238-c13f-4403-4bcc-9ccc86bd6a62",
223   - "alias": "DHT11",
224   - "filter": {
225   - "type": "entityName",
226   - "resolveMultiple": false,
227   - "entityType": "DEVICE",
228   - "entityNameFilter": "DHT11 Demo Device"
229   - }
230   - }
231   - },
232   - "timewindow": {
233   - "displayValue": "",
234   - "selectedTab": 0,
235   - "realtime": {
236   - "interval": 1000,
237   - "timewindowMs": 60000
238   - },
239   - "history": {
240   - "historyType": 0,
241   - "interval": 1000,
242   - "timewindowMs": 60000,
243   - "fixedTimewindow": {
244   - "startTimeMs": 1498653790019,
245   - "endTimeMs": 1498740190019
246   - }
247   - },
248   - "aggregation": {
249   - "type": "AVG",
250   - "limit": 200
251   - }
252   - },
253   - "settings": {
254   - "stateControllerId": "default",
255   - "showTitle": true,
256   - "showDashboardsSelect": true,
257   - "showEntitiesSelect": true,
258   - "showDashboardTimewindow": true,
259   - "showDashboardExport": true,
260   - "toolbarAlwaysOpen": false
261   - }
262   - },
263   - "name": "Temperature & Humidity Demo Dashboard"
264   -}
\ No newline at end of file
... ... @@ -147,9 +147,9 @@
147 147 "name": "Add",
148 148 "icon": "add",
149 149 "type": "customPretty",
150   - "customHtml": "<form #addEntityForm=\"ngForm\" [formGroup]=\"addEntityFormGroup\"\n (ngSubmit)=\"save()\" class=\"add-entity-form\">\n <mat-toolbar color=\"primary\">\n <h2>Add thermostat</h2>\n <span fxFlex></span>\n <button mat-icon-button (click)=\"cancel()\" type=\"button\">\n <mat-icon class=\"material-icons\">close</mat-icon>\n </button>\n </mat-toolbar>\n <mat-progress-bar color=\"warn\" mode=\"indeterminate\" *ngIf=\"isLoading$ | async\">\n </mat-progress-bar>\n <div style=\"height: 4px;\" *ngIf=\"!(isLoading$ | async)\"></div>\n <div mat-dialog-content fxLayout=\"column\">\n <mat-form-field fxFlex class=\"mat-block\">\n <mat-label>Thermostat name</mat-label>\n <input matInput formControlName=\"entityName\" required>\n <mat-error *ngIf=\"addEntityFormGroup.get('entityName').hasError('required')\">\n Thermostat name is required.\n </mat-error>\n </mat-form-field>\n <div formGroupName=\"attributes\" fxLayout=\"column\">\n <mat-slide-toggle formControlName=\"alarmTemperature\">\n High temperature alarm\n </mat-slide-toggle>\n <mat-form-field fxFlex class=\"mat-block\">\n <mat-label>High temperature threshold, °C</mat-label>\n <input type=\"number\" step=\"any\" matInput\n [required] = \"addEntityFormGroup.get('attributes').get('alarmTemperature').value\"\n formControlName=\"thresholdTemperature\">\n <mat-error *ngIf=\"addEntityFormGroup.get('attributes').get('thresholdTemperature').hasError('required')\">\n High temperature threshold is required.\n </mat-error>\n </mat-form-field>\n \n <mat-slide-toggle formControlName=\"alarmHumidity\">\n Low humidity alarm\n </mat-slide-toggle>\n \n <mat-form-field fxFlex class=\"mat-block\">\n <mat-label>Low humidity threshold, %</mat-label>\n <input type=\"number\" step=\"any\" matInput\n [required] = \"addEntityFormGroup.get('attributes').get('alarmHumidity').value\"\n formControlName=\"thresholdHumidity\">\n <mat-error *ngIf=\"addEntityFormGroup.get('attributes').get('thresholdHumidity').hasError('required')\">\n Low humidity threshold is required.\n </mat-error>\n </mat-form-field>\n </div>\n </div>\n <div mat-dialog-actions fxLayout=\"row\" fxLayoutAlign=\"end center\">\n <button mat-button mat-raised-button color=\"primary\"\n type=\"submit\"\n [disabled]=\"(isLoading$ | async) || addEntityForm.invalid || !addEntityForm.dirty\">\n Create\n </button>\n <button mat-button color=\"primary\"\n type=\"button\"\n [disabled]=\"(isLoading$ | async)\"\n (click)=\"cancel()\" cdkFocusInitial>\n Cancel\n </button>\n </div>\n</form>",
  150 + "customHtml": "<form #addEntityForm=\"ngForm\" [formGroup]=\"addEntityFormGroup\"\n (ngSubmit)=\"save()\" class=\"add-entity-form\">\n <mat-toolbar color=\"primary\">\n <h2>Add thermostat</h2>\n <span fxFlex></span>\n <button mat-icon-button (click)=\"cancel()\" type=\"button\">\n <mat-icon class=\"material-icons\">close</mat-icon>\n </button>\n </mat-toolbar>\n <mat-progress-bar color=\"warn\" mode=\"indeterminate\" *ngIf=\"isLoading$ | async\">\n </mat-progress-bar>\n <div style=\"height: 4px;\" *ngIf=\"!(isLoading$ | async)\"></div>\n <div mat-dialog-content fxLayout=\"column\">\n <mat-form-field fxFlex class=\"mat-block\">\n <mat-label>Thermostat name</mat-label>\n <input matInput formControlName=\"entityName\" required>\n <mat-error *ngIf=\"addEntityFormGroup.get('entityName').hasError('required')\">\n Thermostat name is required.\n </mat-error>\n </mat-form-field>\n <div formGroupName=\"attributes\" fxLayout=\"column\">\n <mat-slide-toggle formControlName=\"temperatureAlarmFlag\">\n High temperature alarm\n </mat-slide-toggle>\n <mat-form-field fxFlex class=\"mat-block\">\n <mat-label>High temperature threshold, °C</mat-label>\n <input type=\"number\" step=\"any\" matInput\n [required] = \"addEntityFormGroup.get('attributes').get('temperatureAlarmFlag').value\"\n formControlName=\"temperatureAlarmThreshold\">\n <mat-error *ngIf=\"addEntityFormGroup.get('attributes').get('temperatureAlarmThreshold').hasError('required')\">\n High temperature threshold is required.\n </mat-error>\n </mat-form-field>\n \n <mat-slide-toggle formControlName=\"humidityAlarmFlag\">\n Low humidity alarm\n </mat-slide-toggle>\n \n <mat-form-field fxFlex class=\"mat-block\">\n <mat-label>Low humidity threshold, %</mat-label>\n <input type=\"number\" step=\"any\" matInput\n [required] = \"addEntityFormGroup.get('attributes').get('humidityAlarmFlag').value\"\n formControlName=\"humidityAlarmThreshold\">\n <mat-error *ngIf=\"addEntityFormGroup.get('attributes').get('humidityAlarmThreshold').hasError('required')\">\n Low humidity threshold is required.\n </mat-error>\n </mat-form-field>\n </div>\n </div>\n <div mat-dialog-actions fxLayout=\"row\" fxLayoutAlign=\"end center\">\n <button mat-button mat-raised-button color=\"primary\"\n type=\"submit\"\n [disabled]=\"(isLoading$ | async) || addEntityForm.invalid || !addEntityForm.dirty\">\n Create\n </button>\n <button mat-button color=\"primary\"\n type=\"button\"\n [disabled]=\"(isLoading$ | async)\"\n (click)=\"cancel()\" cdkFocusInitial>\n Cancel\n </button>\n </div>\n</form>",
151 151 "customCss": ".add-entity-form{\n width: 300px;\n}\n",
152   - "customFunction": "let $injector = widgetContext.$scope.$injector;\nlet customDialog = $injector.get(widgetContext.servicesMap.get('customDialog'));\nlet deviceService = $injector.get(widgetContext.servicesMap.get('deviceService'));\nlet attributeService = $injector.get(widgetContext.servicesMap.get('attributeService'));\n\nopenAddEntityDialog();\n\nfunction openAddEntityDialog() {\n customDialog.customDialog(htmlTemplate, AddEntityDialogController).subscribe();\n}\n\nfunction AddEntityDialogController(instance) {\n let vm = instance;\n \n vm.addEntityFormGroup = vm.fb.group({\n entityName: ['', [vm.validators.required]],\n attributes: vm.fb.group({\n alarmTemperature: [false],\n thresholdTemperature: [{value: null, disabled: true}],\n alarmHumidity: [false],\n thresholdHumidity: [{value: null, disabled: true}]\n })\n });\n \n vm.addEntityFormGroup.get('attributes').get('alarmTemperature').valueChanges\n .subscribe(activate => {\n if (activate) {\n vm.addEntityFormGroup.get('attributes').get('thresholdTemperature').enable();\n } else {\n vm.addEntityFormGroup.get('attributes').get('thresholdTemperature').disable();\n }\n });\n \n vm.addEntityFormGroup.get('attributes').get('alarmHumidity').valueChanges\n .subscribe(activate => {\n if (activate) {\n vm.addEntityFormGroup.get('attributes').get('thresholdHumidity').enable();\n } else {\n vm.addEntityFormGroup.get('attributes').get('thresholdHumidity').disable();\n }\n });\n\n vm.save = function() {\n vm.addEntityFormGroup.markAsPristine();\n saveEntityObservable().subscribe(\n function (entity) {\n saveAttributes(entity.id).subscribe(\n function () {\n widgetContext.updateAliases();\n vm.dialogRef.close(null);\n }\n );\n }\n );\n };\n \n vm.cancel = function() {\n vm.dialogRef.close(null);\n };\n \n function saveEntityObservable() {\n const formValues = vm.addEntityFormGroup.value;\n let entity = {\n name: formValues.entityName,\n type: \"thermostat\"\n };\n return deviceService.saveDevice(entity);\n }\n \n function saveAttributes(entityId) {\n let attributes = vm.addEntityFormGroup.get('attributes').value;\n let attributesArray = [];\n for (let key in attributes) {\n if(attributes[key] !== null) {\n attributesArray.push({key: key, value: attributes[key]});\n }\n }\n if (attributesArray.length > 0) {\n return attributeService.saveEntityAttributes(entityId, \"SERVER_SCOPE\", attributesArray);\n } else {\n return widgetContext.rxjs.of([]);\n }\n }\n}",
  152 + "customFunction": "let $injector = widgetContext.$scope.$injector;\nlet customDialog = $injector.get(widgetContext.servicesMap.get('customDialog'));\nlet deviceService = $injector.get(widgetContext.servicesMap.get('deviceService'));\nlet attributeService = $injector.get(widgetContext.servicesMap.get('attributeService'));\n\nopenAddEntityDialog();\n\nfunction openAddEntityDialog() {\n customDialog.customDialog(htmlTemplate, AddEntityDialogController).subscribe();\n}\n\nfunction AddEntityDialogController(instance) {\n let vm = instance;\n \n vm.addEntityFormGroup = vm.fb.group({\n entityName: ['', [vm.validators.required]],\n attributes: vm.fb.group({\n temperatureAlarmFlag: [false],\n temperatureAlarmThreshold: [{value: null, disabled: true}],\n humidityAlarmFlag: [false],\n humidityAlarmThreshold: [{value: null, disabled: true}]\n })\n });\n \n vm.addEntityFormGroup.get('attributes').get('temperatureAlarmFlag').valueChanges\n .subscribe(activate => {\n if (activate) {\n vm.addEntityFormGroup.get('attributes').get('temperatureAlarmThreshold').enable();\n } else {\n vm.addEntityFormGroup.get('attributes').get('temperatureAlarmThreshold').disable();\n }\n });\n \n vm.addEntityFormGroup.get('attributes').get('humidityAlarmFlag').valueChanges\n .subscribe(activate => {\n if (activate) {\n vm.addEntityFormGroup.get('attributes').get('humidityAlarmThreshold').enable();\n } else {\n vm.addEntityFormGroup.get('attributes').get('humidityAlarmThreshold').disable();\n }\n });\n\n vm.save = function() {\n vm.addEntityFormGroup.markAsPristine();\n saveEntityObservable().subscribe(\n function (entity) {\n saveAttributes(entity.id).subscribe(\n function () {\n widgetContext.updateAliases();\n vm.dialogRef.close(null);\n }\n );\n }\n );\n };\n \n vm.cancel = function() {\n vm.dialogRef.close(null);\n };\n \n function saveEntityObservable() {\n const formValues = vm.addEntityFormGroup.value;\n let entity = {\n name: formValues.entityName,\n type: \"thermostat\"\n };\n return deviceService.saveDevice(entity);\n }\n \n function saveAttributes(entityId) {\n let attributes = vm.addEntityFormGroup.get('attributes').value;\n let attributesArray = [];\n for (let key in attributes) {\n if(attributes[key] !== null) {\n attributesArray.push({key: key, value: attributes[key]});\n }\n }\n if (attributesArray.length > 0) {\n return attributeService.saveEntityAttributes(entityId, \"SERVER_SCOPE\", attributesArray);\n } else {\n return widgetContext.rxjs.of([]);\n }\n }\n}",
153 153 "customResources": [],
154 154 "id": "8ab5a518-67d2-b6a2-956d-81fd512294b2"
155 155 }
... ... @@ -167,9 +167,9 @@
167 167 "name": "Edit",
168 168 "icon": "edit",
169 169 "type": "customPretty",
170   - "customHtml": "<form #editEntityForm=\"ngForm\" [formGroup]=\"editEntityFormGroup\"\n (ngSubmit)=\"save()\" class=\"edit-entity-form\">\n <mat-toolbar color=\"primary\">\n <h2>Edit thermostat {{entityName}}</h2>\n <span fxFlex></span>\n <button mat-icon-button (click)=\"cancel()\" type=\"button\">\n <mat-icon class=\"material-icons\">close</mat-icon>\n </button>\n </mat-toolbar>\n <mat-progress-bar color=\"warn\" mode=\"indeterminate\" *ngIf=\"isLoading$ | async\">\n </mat-progress-bar>\n <div style=\"height: 4px;\" *ngIf=\"!(isLoading$ | async)\"></div>\n <div mat-dialog-content fxLayout=\"column\">\n <mat-form-field fxFlex class=\"mat-block\">\n <mat-label>Thermostat name</mat-label>\n <input matInput formControlName=\"entityName\" readonly>\n </mat-form-field>\n <div formGroupName=\"attributes\" fxLayout=\"column\">\n <mat-slide-toggle formControlName=\"alarmTemperature\">\n High temperature alarm\n </mat-slide-toggle>\n <mat-form-field fxFlex class=\"mat-block\">\n <mat-label>High temperature threshold, °C</mat-label>\n <input type=\"number\" step=\"any\" matInput\n [required] = \"editEntityFormGroup.get('attributes').get('alarmTemperature').value\"\n formControlName=\"thresholdTemperature\">\n <mat-error *ngIf=\"editEntityFormGroup.get('attributes').get('thresholdTemperature').hasError('required')\">\n High temperature threshold is required.\n </mat-error>\n </mat-form-field>\n\n <mat-slide-toggle formControlName=\"alarmHumidity\">\n Low humidity alarm\n </mat-slide-toggle>\n\n <mat-form-field fxFlex class=\"mat-block\">\n <mat-label>Low humidity threshold, %</mat-label>\n <input type=\"number\" step=\"any\" matInput\n [required] = \"editEntityFormGroup.get('attributes').get('alarmHumidity').value\"\n formControlName=\"thresholdHumidity\">\n <mat-error *ngIf=\"editEntityFormGroup.get('attributes').get('thresholdHumidity').hasError('required')\">\n Low humidity threshold is required.\n </mat-error>\n </mat-form-field>\n </div>\n </div>\n <div mat-dialog-actions fxLayout=\"row\" fxLayoutAlign=\"end center\">\n <button mat-raised-button color=\"primary\"\n type=\"submit\"\n [disabled]=\"(isLoading$ | async) || editEntityForm.invalid || !editEntityForm.dirty\">\n Save\n </button>\n <button mat-button color=\"primary\"\n type=\"button\"\n [disabled]=\"(isLoading$ | async)\"\n (click)=\"cancel()\" cdkFocusInitial>\n Cancel\n </button>\n </div>\n</form>",
  170 + "customHtml": "<form #editEntityForm=\"ngForm\" [formGroup]=\"editEntityFormGroup\"\n (ngSubmit)=\"save()\" class=\"edit-entity-form\">\n <mat-toolbar color=\"primary\">\n <h2>Edit thermostat {{entityName}}</h2>\n <span fxFlex></span>\n <button mat-icon-button (click)=\"cancel()\" type=\"button\">\n <mat-icon class=\"material-icons\">close</mat-icon>\n </button>\n </mat-toolbar>\n <mat-progress-bar color=\"warn\" mode=\"indeterminate\" *ngIf=\"isLoading$ | async\">\n </mat-progress-bar>\n <div style=\"height: 4px;\" *ngIf=\"!(isLoading$ | async)\"></div>\n <div mat-dialog-content fxLayout=\"column\">\n <mat-form-field fxFlex class=\"mat-block\">\n <mat-label>Thermostat name</mat-label>\n <input matInput formControlName=\"entityName\" readonly>\n </mat-form-field>\n <div formGroupName=\"attributes\" fxLayout=\"column\">\n <mat-slide-toggle formControlName=\"temperatureAlarmFlag\">\n High temperature alarm\n </mat-slide-toggle>\n <mat-form-field fxFlex class=\"mat-block\">\n <mat-label>High temperature threshold, °C</mat-label>\n <input type=\"number\" step=\"any\" matInput\n [required] = \"editEntityFormGroup.get('attributes').get('temperatureAlarmFlag').value\"\n formControlName=\"temperatureAlarmThreshold\">\n <mat-error *ngIf=\"editEntityFormGroup.get('attributes').get('temperatureAlarmThreshold').hasError('required')\">\n High temperature threshold is required.\n </mat-error>\n </mat-form-field>\n\n <mat-slide-toggle formControlName=\"humidityAlarmFlag\">\n Low humidity alarm\n </mat-slide-toggle>\n\n <mat-form-field fxFlex class=\"mat-block\">\n <mat-label>Low humidity threshold, %</mat-label>\n <input type=\"number\" step=\"any\" matInput\n [required] = \"editEntityFormGroup.get('attributes').get('humidityAlarmFlag').value\"\n formControlName=\"humidityAlarmThreshold\">\n <mat-error *ngIf=\"editEntityFormGroup.get('attributes').get('humidityAlarmThreshold').hasError('required')\">\n Low humidity threshold is required.\n </mat-error>\n </mat-form-field>\n </div>\n </div>\n <div mat-dialog-actions fxLayout=\"row\" fxLayoutAlign=\"end center\">\n <button mat-raised-button color=\"primary\"\n type=\"submit\"\n [disabled]=\"(isLoading$ | async) || editEntityForm.invalid || !editEntityForm.dirty\">\n Save\n </button>\n <button mat-button color=\"primary\"\n type=\"button\"\n [disabled]=\"(isLoading$ | async)\"\n (click)=\"cancel()\" cdkFocusInitial>\n Cancel\n </button>\n </div>\n</form>",
171 171 "customCss": ".edit-entity-form{\n width: 300px;\n}",
172   - "customFunction": "let $injector = widgetContext.$scope.$injector;\nlet customDialog = $injector.get(widgetContext.servicesMap.get('customDialog'));\nlet attributeService = $injector.get(widgetContext.servicesMap.get('attributeService'));\n\nopenEditEntityDialog();\n\nfunction openEditEntityDialog() {\n customDialog.customDialog(htmlTemplate, EditEntityDialogController).subscribe();\n}\n\nfunction EditEntityDialogController(instance) {\n let vm = instance;\n \n vm.entityId = entityId;\n vm.entityName = entityName;\n vm.attributes = {};\n \n vm.editEntityFormGroup = vm.fb.group({\n entityName: [''],\n attributes: vm.fb.group({\n alarmTemperature: [false],\n thresholdTemperature: [{value: null, disabled: true}],\n alarmHumidity: [false],\n thresholdHumidity: [{value: null, disabled: true}]\n })\n });\n \n vm.editEntityFormGroup.get('attributes').get('alarmTemperature').valueChanges\n .subscribe(activate => {\n if (activate) {\n vm.editEntityFormGroup.get('attributes').get('thresholdTemperature').enable();\n } else {\n vm.editEntityFormGroup.get('attributes').get('thresholdTemperature').disable();\n }\n });\n \n vm.editEntityFormGroup.get('attributes').get('alarmHumidity').valueChanges\n .subscribe(activate => {\n if (activate) {\n vm.editEntityFormGroup.get('attributes').get('thresholdHumidity').enable();\n } else {\n vm.editEntityFormGroup.get('attributes').get('thresholdHumidity').disable();\n }\n });\n \n \n getEntityInfo();\n \n \n vm.save = function() {\n vm.editEntityFormGroup.markAsPristine();\n saveAttributes(entityId).subscribe(\n function () {\n vm.dialogRef.close(null);\n }\n );\n };\n \n vm.cancel = function() {\n vm.dialogRef.close(null);\n };\n \n function getEntityAttributes(attributes) {\n for (var i = 0; i < attributes.length; i++) {\n vm.attributes[attributes[i].key] = attributes[i].value;\n }\n }\n \n function getEntityInfo() {\n attributeService.getEntityAttributes(entityId, 'SERVER_SCOPE').subscribe(\n function (attributes) {\n getEntityAttributes(attributes);\n vm.editEntityFormGroup.patchValue({\n entityName: vm.entityName,\n attributes: vm.attributes\n });\n // if(vm.attributes.alarmTemperature) {\n // vm.editEntityFormGroup.get('attributes').get('thresholdTemperature').enable();\n // }\n // if(vm.attributes.alarmHumidity) {\n // vm.editEntityFormGroup.get('attributes').get('thresholdHumidity').enable();\n // }\n }\n );\n }\n \n function saveAttributes(entityId) {\n let attributes = vm.editEntityFormGroup.get('attributes').value;\n let attributesArray = [];\n for (let key in attributes) {\n if (attributes[key] !== vm.attributes[key]) {\n attributesArray.push({key: key, value: attributes[key]});\n }\n }\n if (attributesArray.length > 0) {\n return attributeService.saveEntityAttributes(entityId, \"SERVER_SCOPE\", attributesArray);\n } else {\n return widgetContext.rxjs.of([]);\n }\n }\n}",
  172 + "customFunction": "let $injector = widgetContext.$scope.$injector;\nlet customDialog = $injector.get(widgetContext.servicesMap.get('customDialog'));\nlet attributeService = $injector.get(widgetContext.servicesMap.get('attributeService'));\n\nopenEditEntityDialog();\n\nfunction openEditEntityDialog() {\n customDialog.customDialog(htmlTemplate, EditEntityDialogController).subscribe();\n}\n\nfunction EditEntityDialogController(instance) {\n let vm = instance;\n \n vm.entityId = entityId;\n vm.entityName = entityName;\n vm.attributes = {};\n \n vm.editEntityFormGroup = vm.fb.group({\n entityName: [''],\n attributes: vm.fb.group({\n temperatureAlarmFlag: [false],\n temperatureAlarmThreshold: [{value: null, disabled: true}],\n humidityAlarmFlag: [false],\n humidityAlarmThreshold: [{value: null, disabled: true}]\n })\n });\n \n vm.editEntityFormGroup.get('attributes').get('temperatureAlarmFlag').valueChanges\n .subscribe(activate => {\n if (activate) {\n vm.editEntityFormGroup.get('attributes').get('temperatureAlarmThreshold').enable();\n } else {\n vm.editEntityFormGroup.get('attributes').get('temperatureAlarmThreshold').disable();\n }\n });\n \n vm.editEntityFormGroup.get('attributes').get('humidityAlarmFlag').valueChanges\n .subscribe(activate => {\n if (activate) {\n vm.editEntityFormGroup.get('attributes').get('humidityAlarmThreshold').enable();\n } else {\n vm.editEntityFormGroup.get('attributes').get('humidityAlarmThreshold').disable();\n }\n });\n \n \n getEntityInfo();\n \n \n vm.save = function() {\n vm.editEntityFormGroup.markAsPristine();\n saveAttributes(entityId).subscribe(\n function () {\n vm.dialogRef.close(null);\n }\n );\n };\n \n vm.cancel = function() {\n vm.dialogRef.close(null);\n };\n \n function getEntityAttributes(attributes) {\n for (var i = 0; i < attributes.length; i++) {\n vm.attributes[attributes[i].key] = attributes[i].value;\n }\n }\n \n function getEntityInfo() {\n attributeService.getEntityAttributes(entityId, 'SERVER_SCOPE').subscribe(\n function (attributes) {\n getEntityAttributes(attributes);\n vm.editEntityFormGroup.patchValue({\n entityName: vm.entityName,\n attributes: vm.attributes\n });\n // if(vm.attributes.temperatureAlarmFlag) {\n // vm.editEntityFormGroup.get('attributes').get('temperatureAlarmThreshold').enable();\n // }\n // if(vm.attributes.humidityAlarmFlag) {\n // vm.editEntityFormGroup.get('attributes').get('humidityAlarmThreshold').enable();\n // }\n }\n );\n }\n \n function saveAttributes(entityId) {\n let attributes = vm.editEntityFormGroup.get('attributes').value;\n let attributesArray = [];\n for (let key in attributes) {\n if (attributes[key] !== vm.attributes[key]) {\n attributesArray.push({key: key, value: attributes[key]});\n }\n }\n if (attributesArray.length > 0) {\n return attributeService.saveEntityAttributes(entityId, \"SERVER_SCOPE\", attributesArray);\n } else {\n return widgetContext.rxjs.of([]);\n }\n }\n}",
173 173 "customResources": [],
174 174 "id": "7506576f-87ba-d3a0-88fb-e304d451776d"
175 175 },
... ... @@ -219,8 +219,8 @@
219 219 "defaultPageSize": 10,
220 220 "defaultSortOrder": "-createdTime",
221 221 "enableSelectColumnDisplay": false,
222   - "enableStatusFilter": true,
223   - "alarmsTitle": "Alarms"
  222 + "alarmsTitle": "Alarms",
  223 + "enableFilter": true
224 224 },
225 225 "title": "New Alarms table",
226 226 "dropShadow": true,
... ... @@ -234,6 +234,9 @@
234 234 "showLegend": false,
235 235 "alarmSource": {
236 236 "type": "entity",
  237 + "name": "alarms",
  238 + "entityAliasId": "68a058e1-fdda-8482-715b-3ae4a488568e",
  239 + "filterId": null,
237 240 "dataKeys": [
238 241 {
239 242 "name": "createdTime",
... ... @@ -275,9 +278,7 @@
275 278 "settings": {},
276 279 "_hash": 0.7977920750136249
277 280 }
278   - ],
279   - "entityAliasId": "ce27a9d0-93bf-b7a4-054d-d0369a8cf813",
280   - "name": "alarms"
  281 + ]
281 282 },
282 283 "alarmSearchStatus": "ANY",
283 284 "alarmsPollingInterval": 5,
... ... @@ -549,7 +550,7 @@
549 550 "type": "entity",
550 551 "dataKeys": [
551 552 {
552   - "name": "alarmTemperature",
  553 + "name": "temperatureAlarmFlag",
553 554 "type": "attribute",
554 555 "label": "High temperature alarm",
555 556 "color": "#4caf50",
... ... @@ -564,7 +565,7 @@
564 565 "_hash": 0.8725278440159361
565 566 },
566 567 {
567   - "name": "thresholdTemperature",
  568 + "name": "temperatureAlarmThreshold",
568 569 "type": "attribute",
569 570 "label": "High temperature threshold, °C",
570 571 "color": "#f44336",
... ... @@ -575,12 +576,12 @@
575 576 "isEditable": "editable",
576 577 "dataKeyHidden": false,
577 578 "step": 1,
578   - "disabledOnDataKey": "alarmTemperature"
  579 + "disabledOnDataKey": "temperatureAlarmFlag"
579 580 },
580 581 "_hash": 0.7316078472857874
581 582 },
582 583 {
583   - "name": "alarmHumidity",
  584 + "name": "humidityAlarmFlag",
584 585 "type": "attribute",
585 586 "label": "Low humidity alarm",
586 587 "color": "#ffc107",
... ... @@ -595,7 +596,7 @@
595 596 "_hash": 0.5339673667431057
596 597 },
597 598 {
598   - "name": "thresholdHumidity",
  599 + "name": "humidityAlarmThreshold",
599 600 "type": "attribute",
600 601 "label": "Low humidity threshold, %",
601 602 "color": "#607d8b",
... ... @@ -606,7 +607,7 @@
606 607 "isEditable": "editable",
607 608 "dataKeyHidden": false,
608 609 "step": 1,
609   - "disabledOnDataKey": "alarmHumidity"
  610 + "disabledOnDataKey": "humidityAlarmFlag"
610 611 },
611 612 "_hash": 0.2687091190358901
612 613 }
... ... @@ -1031,7 +1032,8 @@
1031 1032 "markerImageFunction": "var res;\nif(dsData[dsIndex].active !== \"true\"){\n\tvar res = {\n\t url: images[0],\n\t size: 48\n\t}\n} else {\n var res = {\n\t url: images[1],\n\t size: 48\n\t}\n}\nreturn res;",
1032 1033 "useLabelFunction": true,
1033 1034 "provider": "openstreet-map",
1034   - "draggableMarker": true
  1035 + "draggableMarker": true,
  1036 + "editablePolygon": true
1035 1037 },
1036 1038 "title": "New Markers Placement - OpenStreetMap",
1037 1039 "dropShadow": true,
... ... @@ -1062,61 +1064,6 @@
1062 1064 "displayTimewindow": true
1063 1065 },
1064 1066 "id": "0a430429-9078-9ae6-2b67-e4a15a2bf8bf"
1065   - },
1066   - "f4bb2f2d-0164-60bc-f3e8-9b1e7b5a59b3": {
1067   - "isSystemType": true,
1068   - "bundleAlias": "input_widgets",
1069   - "typeAlias": "update_double_timeseries",
1070   - "type": "latest",
1071   - "title": "New widget",
1072   - "sizeX": 7.5,
1073   - "sizeY": 3,
1074   - "config": {
1075   - "datasources": [
1076   - {
1077   - "type": "entity",
1078   - "name": null,
1079   - "entityAliasId": "12ae98c7-1ea2-52cf-64d5-763e9d993547",
1080   - "dataKeys": [
1081   - {
1082   - "name": "temperature",
1083   - "type": "timeseries",
1084   - "label": "temperature",
1085   - "color": "#2196f3",
1086   - "settings": {},
1087   - "_hash": 0.4164505192982848
1088   - }
1089   - ]
1090   - }
1091   - ],
1092   - "timewindow": {
1093   - "realtime": {
1094   - "timewindowMs": 60000
1095   - }
1096   - },
1097   - "showTitle": true,
1098   - "backgroundColor": "#fff",
1099   - "color": "rgba(0, 0, 0, 0.87)",
1100   - "padding": "8px",
1101   - "settings": {
1102   - "showResultMessage": true,
1103   - "showLabel": true
1104   - },
1105   - "title": "New Update double timeseries",
1106   - "dropShadow": true,
1107   - "enableFullscreen": false,
1108   - "widgetStyle": {},
1109   - "titleStyle": {
1110   - "fontSize": "16px",
1111   - "fontWeight": 400
1112   - },
1113   - "useDashboardTimewindow": true,
1114   - "showLegend": false,
1115   - "actions": {}
1116   - },
1117   - "row": 0,
1118   - "col": 0,
1119   - "id": "f4bb2f2d-0164-60bc-f3e8-9b1e7b5a59b3"
1120 1067 }
1121 1068 },
1122 1069 "states": {
... ... @@ -1215,12 +1162,6 @@
1215 1162 "sizeY": 6,
1216 1163 "row": 6,
1217 1164 "col": 0
1218   - },
1219   - "f4bb2f2d-0164-60bc-f3e8-9b1e7b5a59b3": {
1220   - "sizeX": 7.5,
1221   - "sizeY": 3,
1222   - "row": 12,
1223   - "col": 0
1224 1165 }
1225 1166 },
1226 1167 "gridSettings": {
... ... @@ -1257,16 +1198,6 @@
1257 1198 "stateEntityParamName": null,
1258 1199 "defaultStateEntity": null
1259 1200 }
1260   - },
1261   - "ce27a9d0-93bf-b7a4-054d-d0369a8cf813": {
1262   - "id": "ce27a9d0-93bf-b7a4-054d-d0369a8cf813",
1263   - "alias": "Thermostat-alarm",
1264   - "filter": {
1265   - "type": "entityName",
1266   - "resolveMultiple": false,
1267   - "entityType": "ASSET",
1268   - "entityNameFilter": "Thermostat Alarms"
1269   - }
1270 1201 }
1271 1202 },
1272 1203 "timewindow": {
... ... @@ -1301,7 +1232,8 @@
1301 1232 "showDashboardTimewindow": true,
1302 1233 "showDashboardExport": true,
1303 1234 "toolbarAlwaysOpen": true
1304   - }
  1235 + },
  1236 + "filters": {}
1305 1237 },
1306 1238 "name": "Thermostats"
1307 1239 }
\ No newline at end of file
... ...
1   -{
2   - "ruleChain": {
3   - "additionalInfo": null,
4   - "name": "Root Rule Chain",
5   - "type": "CORE",
6   - "firstRuleNodeId": null,
7   - "root": true,
8   - "debugMode": false,
9   - "configuration": null
10   - },
11   - "metadata": {
12   - "firstNodeIndex": 3,
13   - "nodes": [
14   - {
15   - "additionalInfo": {
16   - "layoutX": 1069,
17   - "layoutY": 267
18   - },
19   - "type": "org.thingsboard.rule.engine.filter.TbJsFilterNode",
20   - "name": "Is Thermostat?",
21   - "debugMode": false,
22   - "configuration": {
23   - "jsScript": "return msg.id.entityType === \"DEVICE\" && msg.type === \"thermostat\";"
24   - }
25   - },
26   - {
27   - "additionalInfo": {
28   - "layoutX": 824,
29   - "layoutY": 156
30   - },
31   - "type": "org.thingsboard.rule.engine.telemetry.TbMsgTimeseriesNode",
32   - "name": "Save Timeseries",
33   - "debugMode": false,
34   - "configuration": {
35   - "defaultTTL": 0
36   - }
37   - },
38   - {
39   - "additionalInfo": {
40   - "layoutX": 825,
41   - "layoutY": 52
42   - },
43   - "type": "org.thingsboard.rule.engine.telemetry.TbMsgAttributesNode",
44   - "name": "Save Client Attributes",
45   - "debugMode": false,
46   - "configuration": {
47   - "scope": "CLIENT_SCOPE",
48   - "notifyDevice": "false"
49   - }
50   - },
51   - {
52   - "additionalInfo": {
53   - "layoutX": 347,
54   - "layoutY": 149
55   - },
56   - "type": "org.thingsboard.rule.engine.filter.TbMsgTypeSwitchNode",
57   - "name": "Message Type Switch",
58   - "debugMode": false,
59   - "configuration": {
60   - "version": 0
61   - }
62   - },
63   - {
64   - "additionalInfo": {
65   - "layoutX": 839,
66   - "layoutY": 345
67   - },
68   - "type": "org.thingsboard.rule.engine.action.TbLogNode",
69   - "name": "Log RPC from Device",
70   - "debugMode": false,
71   - "configuration": {
72   - "jsScript": "return '\\nIncoming message:\\n' + JSON.stringify(msg) + '\\nIncoming metadata:\\n' + JSON.stringify(metadata);"
73   - }
74   - },
75   - {
76   - "additionalInfo": {
77   - "layoutX": 832,
78   - "layoutY": 407
79   - },
80   - "type": "org.thingsboard.rule.engine.action.TbLogNode",
81   - "name": "Log Other",
82   - "debugMode": false,
83   - "configuration": {
84   - "jsScript": "return '\\nIncoming message:\\n' + JSON.stringify(msg) + '\\nIncoming metadata:\\n' + JSON.stringify(metadata);"
85   - }
86   - },
87   - {
88   - "additionalInfo": {
89   - "layoutX": 825,
90   - "layoutY": 468
91   - },
92   - "type": "org.thingsboard.rule.engine.rpc.TbSendRPCRequestNode",
93   - "name": "RPC Call Request",
94   - "debugMode": false,
95   - "configuration": {
96   - "timeoutInSeconds": 60
97   - }
98   - },
99   - {
100   - "additionalInfo": {
101   - "layoutX": 1069,
102   - "layoutY": 90
103   - },
104   - "type": "org.thingsboard.rule.engine.filter.TbJsFilterNode",
105   - "name": "Is Thermostat?",
106   - "debugMode": false,
107   - "configuration": {
108   - "jsScript": "return metadata[\"deviceType\"] === \"thermostat\";"
109   - }
110   - },
111   - {
112   - "additionalInfo": {
113   - "layoutX": 1090,
114   - "layoutY": 360
115   - },
116   - "type": "org.thingsboard.rule.engine.action.TbCreateRelationNode",
117   - "name": "Relate to Asset",
118   - "debugMode": false,
119   - "configuration": {
120   - "direction": "FROM",
121   - "relationType": "ToAlarmPropagationAsset",
122   - "entityType": "ASSET",
123   - "entityNamePattern": "Thermostat Alarms",
124   - "entityTypePattern": "AlarmPropagationAsset",
125   - "entityCacheExpiration": 300,
126   - "createEntityIfNotExists": true,
127   - "changeOriginatorToRelatedEntity": false,
128   - "removeCurrentRelations": false
129   - }
130   - }
131   - ],
132   - "connections": [
133   - {
134   - "fromIndex": 0,
135   - "toIndex": 8,
136   - "type": "True"
137   - },
138   - {
139   - "fromIndex": 1,
140   - "toIndex": 7,
141   - "type": "Success"
142   - },
143   - {
144   - "fromIndex": 3,
145   - "toIndex": 5,
146   - "type": "Other"
147   - },
148   - {
149   - "fromIndex": 3,
150   - "toIndex": 2,
151   - "type": "Post attributes"
152   - },
153   - {
154   - "fromIndex": 3,
155   - "toIndex": 1,
156   - "type": "Post telemetry"
157   - },
158   - {
159   - "fromIndex": 3,
160   - "toIndex": 4,
161   - "type": "RPC Request from Device"
162   - },
163   - {
164   - "fromIndex": 3,
165   - "toIndex": 6,
166   - "type": "RPC Request to Device"
167   - },
168   - {
169   - "fromIndex": 3,
170   - "toIndex": 0,
171   - "type": "Entity Created"
172   - }
173   - ],
174   - "ruleChainConnections": [
175   - {
176   - "fromIndex": 7,
177   - "targetRuleChainId": {
178   - "entityType": "RULE_CHAIN",
179   - "id": "25e26570-89ed-11ea-a650-cd6e14e633bd"
180   - },
181   - "additionalInfo": {
182   - "layoutX": 1109,
183   - "layoutY": 182,
184   - "ruleChainNodeId": "rule-chain-node-10"
185   - },
186   - "type": "True"
187   - }
188   - ]
189   - }
190   -}
\ No newline at end of file
1   -{
2   - "ruleChain": {
3   - "additionalInfo": null,
4   - "name": "Thermostat Alarms",
5   - "type": "CORE",
6   - "firstRuleNodeId": null,
7   - "root": false,
8   - "debugMode": false,
9   - "configuration": null
10   - },
11   - "metadata": {
12   - "firstNodeIndex": 5,
13   - "nodes": [
14   - {
15   - "additionalInfo": {
16   - "layoutX": 929,
17   - "layoutY": 67
18   - },
19   - "type": "org.thingsboard.rule.engine.action.TbCreateAlarmNode",
20   - "name": "Create Temp Alarm",
21   - "debugMode": false,
22   - "configuration": {
23   - "alarmType": "High Temperature",
24   - "alarmDetailsBuildJs": "var details = {};\nif (metadata.prevAlarmDetails) {\n details = JSON.parse(metadata.prevAlarmDetails);\n}\ndetails.triggerValue = msg.temperature;\nreturn details;",
25   - "severity": "MAJOR",
26   - "propagate": true,
27   - "useMessageAlarmData": false,
28   - "relationTypes": [
29   - "ToAlarmPropagationAsset"
30   - ]
31   - }
32   - },
33   - {
34   - "additionalInfo": {
35   - "layoutX": 930,
36   - "layoutY": 201
37   - },
38   - "type": "org.thingsboard.rule.engine.action.TbClearAlarmNode",
39   - "name": "Clear Temp Alarm",
40   - "debugMode": false,
41   - "configuration": {
42   - "alarmType": "High Temperature",
43   - "alarmDetailsBuildJs": "var details = {};\nif (metadata.prevAlarmDetails) {\n details = JSON.parse(metadata.prevAlarmDetails);\n}\nreturn details;"
44   - }
45   - },
46   - {
47   - "additionalInfo": {
48   - "layoutX": 930,
49   - "layoutY": 131
50   - },
51   - "type": "org.thingsboard.rule.engine.action.TbCreateAlarmNode",
52   - "name": "Create Humidity Alarm",
53   - "debugMode": false,
54   - "configuration": {
55   - "alarmType": "Low Humidity",
56   - "alarmDetailsBuildJs": "var details = {};\nif (metadata.prevAlarmDetails) {\n details = JSON.parse(metadata.prevAlarmDetails);\n}\ndetails.triggerValue = msg.humidity;\nreturn details;",
57   - "severity": "MINOR",
58   - "propagate": true,
59   - "useMessageAlarmData": false,
60   - "relationTypes": [
61   - "ToAlarmPropagationAsset"
62   - ]
63   - }
64   - },
65   - {
66   - "additionalInfo": {
67   - "layoutX": 929,
68   - "layoutY": 275
69   - },
70   - "type": "org.thingsboard.rule.engine.action.TbClearAlarmNode",
71   - "name": "Clear Humidity Alarm",
72   - "debugMode": false,
73   - "configuration": {
74   - "alarmType": "Low Humidity",
75   - "alarmDetailsBuildJs": "var details = {};\nif (metadata.prevAlarmDetails) {\n details = JSON.parse(metadata.prevAlarmDetails);\n}\nreturn details;"
76   - }
77   - },
78   - {
79   - "additionalInfo": {
80   - "layoutX": 586,
81   - "layoutY": 148
82   - },
83   - "type": "org.thingsboard.rule.engine.filter.TbJsSwitchNode",
84   - "name": "Check Alarms",
85   - "debugMode": false,
86   - "configuration": {
87   - "jsScript": "var relations = [];\nif(metadata[\"ss_alarmTemperature\"] === \"true\"){\n if(msg.temperature > metadata[\"ss_thresholdTemperature\"]){\n relations.push(\"NewTempAlarm\");\n } else {\n relations.push(\"ClearTempAlarm\");\n }\n}\nif(metadata[\"ss_alarmHumidity\"] === \"true\"){\n if(msg.humidity < metadata[\"ss_thresholdHumidity\"]){\n relations.push(\"NewHumidityAlarm\");\n } else {\n relations.push(\"ClearHumidityAlarm\");\n }\n}\n\nreturn relations;"
88   - }
89   - },
90   - {
91   - "additionalInfo": {
92   - "layoutX": 321,
93   - "layoutY": 149
94   - },
95   - "type": "org.thingsboard.rule.engine.metadata.TbGetAttributesNode",
96   - "name": "Fetch Configuration",
97   - "debugMode": false,
98   - "configuration": {
99   - "clientAttributeNames": [],
100   - "sharedAttributeNames": [],
101   - "serverAttributeNames": [
102   - "alarmTemperature",
103   - "thresholdTemperature",
104   - "alarmHumidity",
105   - "thresholdHumidity"
106   - ],
107   - "latestTsKeyNames": [],
108   - "tellFailureIfAbsent": false,
109   - "getLatestValueWithTs": false
110   - }
111   - }
112   - ],
113   - "connections": [
114   - {
115   - "fromIndex": 4,
116   - "toIndex": 0,
117   - "type": "NewTempAlarm"
118   - },
119   - {
120   - "fromIndex": 4,
121   - "toIndex": 1,
122   - "type": "ClearTempAlarm"
123   - },
124   - {
125   - "fromIndex": 4,
126   - "toIndex": 2,
127   - "type": "NewHumidityAlarm"
128   - },
129   - {
130   - "fromIndex": 4,
131   - "toIndex": 3,
132   - "type": "ClearHumidityAlarm"
133   - },
134   - {
135   - "fromIndex": 5,
136   - "toIndex": 4,
137   - "type": "Success"
138   - }
139   - ],
140   - "ruleChainConnections": null
141   - }
142   -}
\ No newline at end of file
  1 +{
  2 + "providerId": "Facebook",
  3 + "accessTokenUri": "https://graph.facebook.com/v2.8/oauth/access_token",
  4 + "authorizationUri": "https://www.facebook.com/v2.8/dialog/oauth",
  5 + "scope": ["email","public_profile"],
  6 + "jwkSetUri": null,
  7 + "userInfoUri": "https://graph.facebook.com/me?fields=id,name,first_name,last_name,email",
  8 + "clientAuthenticationMethod": "BASIC",
  9 + "userNameAttributeName": "email",
  10 + "mapperConfig": {
  11 + "type": "BASIC",
  12 + "basic": {
  13 + "emailAttributeKey": "email",
  14 + "firstNameAttributeKey": "first_name",
  15 + "lastNameAttributeKey": "last_name",
  16 + "tenantNameStrategy": "DOMAIN"
  17 + }
  18 + },
  19 + "comment": null,
  20 + "loginButtonIcon": "facebook-logo",
  21 + "loginButtonLabel": "Facebook",
  22 + "helpLink": "https://developers.facebook.com/docs/facebook-login/web#logindialog"
  23 +}
... ...
  1 +{
  2 + "providerId": "Github",
  3 + "accessTokenUri": "https://github.com/login/oauth/access_token",
  4 + "authorizationUri": "https://github.com/login/oauth/authorize",
  5 + "scope": ["read:user","user:email"],
  6 + "jwkSetUri": null,
  7 + "userInfoUri": "https://api.github.com/user",
  8 + "clientAuthenticationMethod": "BASIC",
  9 + "userNameAttributeName": "login",
  10 + "mapperConfig": {
  11 + "type": "GITHUB",
  12 + "basic": {
  13 + "firstNameAttributeKey": "name",
  14 + "tenantNameStrategy": "DOMAIN"
  15 + }
  16 + },
  17 + "comment": "In order to log into ThingsBoard you need to have user's email. You may configure and use Custom OAuth2 Mapper to get email information. Please refer to <a href=\"https://docs.github.com/en/rest/reference/users#list-email-addresses-for-the-authenticated-user\">Github Documentation</a>",
  18 + "loginButtonIcon": "github-logo",
  19 + "loginButtonLabel": "Github",
  20 + "helpLink": "https://docs.github.com/en/developers/apps/creating-an-oauth-app"
  21 +}
... ...
  1 +{
  2 + "providerId": "Google",
  3 + "additionalInfo": null,
  4 + "accessTokenUri": "https://oauth2.googleapis.com/token",
  5 + "authorizationUri": "https://accounts.google.com/o/oauth2/v2/auth",
  6 + "scope": ["email","openid","profile"],
  7 + "jwkSetUri": "https://www.googleapis.com/oauth2/v3/certs",
  8 + "userInfoUri": "https://openidconnect.googleapis.com/v1/userinfo",
  9 + "clientAuthenticationMethod": "BASIC",
  10 + "userNameAttributeName": "email",
  11 + "mapperConfig": {
  12 + "type": "BASIC",
  13 + "basic": {
  14 + "emailAttributeKey": "email",
  15 + "firstNameAttributeKey": "given_name",
  16 + "lastNameAttributeKey": "family_name",
  17 + "tenantNameStrategy": "DOMAIN"
  18 + }
  19 + },
  20 + "comment": null,
  21 + "loginButtonIcon": "google-logo",
  22 + "loginButtonLabel": "Google",
  23 + "helpLink": "https://developers.google.com/adwords/api/docs/guides/authentication"
  24 +}
... ...
... ... @@ -19,7 +19,7 @@
19 19 ],
20 20 "templateHtml": "<canvas id=\"barChart\"></canvas>\n",
21 21 "templateCss": "",
22   - "controllerScript": "self.onInit = function() {\n var barData = {\n labels: [],\n datasets: []\n };\n \n for (var i = 0; i < self.ctx.datasources.length; i++) {\n var datasource = self.ctx.datasources[i];\n for (var d = 0; d < datasource.dataKeys.length; d++) {\n var dataset = {\n label: datasource.dataKeys[d].label,\n data: [0],\n backgroundColor: [datasource.dataKeys[d].color],\n borderColor: [datasource.dataKeys[d].color],\n borderWidth: 1\n }\n barData.datasets.push(dataset);\n }\n }\n\n var ctx = $('#barChart', self.ctx.$container);\n self.ctx.chart = new Chart(ctx, {\n type: 'bar',\n data: barData,\n options: {\n responsive: false,\n maintainAspectRatio: false,\n scales: {\n yAxes: [{\n ticks: {\n beginAtZero:true\n }\n }]\n }\n }\n });\n \n self.onResize();\n}\n\nself.onDataUpdated = function() {\n var c = 0;\n for (var i = 0; i < self.ctx.chart.data.datasets.length; i++) {\n var dataset = self.ctx.chart.data.datasets[i];\n var cellData = self.ctx.data[i]; \n if (cellData.data.length > 0) {\n var tvPair = cellData.data[cellData.data.length - 1];\n var value = tvPair[1];\n dataset.data[0] = parseFloat(value);\n }\n }\n self.ctx.chart.update();\n}\n\nself.onResize = function() {\n self.ctx.chart.resize();\n}\n\n",
  22 + "controllerScript": "self.onInit = function() {\n var barData = {\n labels: [],\n datasets: []\n };\n \n for (var i = 0; i < self.ctx.datasources.length; i++) {\n var datasource = self.ctx.datasources[i];\n for (var d = 0; d < datasource.dataKeys.length; d++) {\n var dataKey = datasource.dataKeys[d];\n var units = dataKey.units && dataKey.units.length ? dataKey.units : self.ctx.units;\n units = units ? (' (' + units + ')') : '';\n var dataset = {\n label: dataKey.label + units,\n data: [0],\n backgroundColor: [dataKey.color],\n borderColor: [dataKey.color],\n borderWidth: 1\n }\n barData.datasets.push(dataset);\n }\n }\n\n var ctx = $('#barChart', self.ctx.$container);\n self.ctx.chart = new Chart(ctx, {\n type: 'bar',\n data: barData,\n options: {\n responsive: false,\n maintainAspectRatio: false,\n scales: {\n yAxes: [{\n ticks: {\n beginAtZero:true\n }\n }]\n }\n }\n });\n \n self.onResize();\n}\n\nself.onDataUpdated = function() {\n var c = 0;\n for (var i = 0; i < self.ctx.chart.data.datasets.length; i++) {\n var dataset = self.ctx.chart.data.datasets[i];\n var cellData = self.ctx.data[i]; \n if (cellData.data.length > 0) {\n var decimals;\n if (typeof cellData.dataKey.decimals !== 'undefined' \n && cellData.dataKey.decimals !== null ) {\n decimals = cellData.dataKey.decimals; \n } else {\n decimals = self.ctx.decimals;\n }\n var tvPair = cellData.data[cellData.data.length - 1];\n var value = self.ctx.utils.formatValue(tvPair[1], decimals);\n dataset.data[0] = parseFloat(value);\n }\n }\n self.ctx.chart.update();\n}\n\nself.onResize = function() {\n self.ctx.chart.resize();\n}\n\n",
23 23 "settingsSchema": "{}",
24 24 "dataKeySettingsSchema": "{}\n",
25 25 "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"First\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = (prevValue-50) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+50;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Second\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.545701115289893,\"funcBody\":\"var value = (prevValue-20) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+20;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Third\",\"color\":\"#f44336\",\"settings\":{},\"_hash\":0.2592906835158064,\"funcBody\":\"var value = (prevValue-40) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+40;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Fourth\",\"color\":\"#ffc107\",\"settings\":{},\"_hash\":0.12880275585455747,\"funcBody\":\"var value = (prevValue-50) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+50;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Bars - Chart.js\"}"
... ... @@ -55,7 +55,7 @@
55 55 ],
56 56 "templateHtml": "<canvas id=\"pieChart\"></canvas>\n",
57 57 "templateCss": "",
58   - "controllerScript": "self.onInit = function() {\n var pieData = {\n labels: [],\n datasets: []\n };\n\n var dataset = {\n data: [],\n backgroundColor: [],\n borderColor: [],\n borderWidth: [],\n hoverBackgroundColor: []\n }\n \n var borderColor = self.ctx.settings.borderColor || '#fff';\n var borderWidth = typeof self.ctx.settings.borderWidth !== 'undefined' ? self.ctx.settings.borderWidth : 5;\n \n pieData.datasets.push(dataset);\n \n for (var i=0; i < self.ctx.data.length; i++) {\n var dataKey = self.ctx.data[i].dataKey;\n pieData.labels.push(dataKey.label);\n dataset.data.push(0);\n var hoverBackgroundColor = tinycolor(dataKey.color).lighten(15);\n dataset.backgroundColor.push(dataKey.color);\n dataset.borderColor.push(borderColor);\n dataset.borderWidth.push(borderWidth);\n dataset.hoverBackgroundColor.push(hoverBackgroundColor.toRgbString());\n }\n\n var options = {\n responsive: false,\n maintainAspectRatio: false,\n legend: {\n display: true,\n labels: {\n fontColor: '#666'\n }\n },\n tooltips: {\n callbacks: {\n label: function(tooltipItem, data) {\n var label = data.labels[tooltipItem.index];\n var value = data.datasets[tooltipItem.datasetIndex].data[tooltipItem.index];\n var content = label + ': ' + value;\n var units = self.ctx.settings.units ? self.ctx.settings.units : self.ctx.units;\n if (units) {\n content += ' ' + units;\n } \n return content;\n }\n }\n }\n };\n\n if (self.ctx.settings.legend) {\n options.legend.display = self.ctx.settings.legend.display !== false;\n options.legend.labels.fontColor = self.ctx.settings.legend.labelsFontColor || '#666';\n }\n\n var ctx = $('#pieChart', self.ctx.$container);\n self.ctx.chart = new Chart(ctx, {\n type: 'doughnut',\n data: pieData,\n options: options\n });\n \n self.onResize();\n}\n\nself.onDataUpdated = function() {\n for (var i = 0; i < self.ctx.data.length; i++) {\n var cellData = self.ctx.data[i];\n if (cellData.data.length > 0) {\n var tvPair = cellData.data[cellData.data.length - 1];\n var value = tvPair[1];\n self.ctx.chart.data.datasets[0].data[i] = parseFloat(value);\n }\n }\n self.ctx.chart.update();\n}\n\nself.onResize = function() {\n self.ctx.chart.resize();\n}\n\n",
  58 + "controllerScript": "self.onInit = function() {\n var pieData = {\n labels: [],\n datasets: []\n };\n\n var dataset = {\n data: [],\n backgroundColor: [],\n borderColor: [],\n borderWidth: [],\n hoverBackgroundColor: []\n }\n \n var borderColor = self.ctx.settings.borderColor || '#fff';\n var borderWidth = typeof self.ctx.settings.borderWidth !== 'undefined' ? self.ctx.settings.borderWidth : 5;\n \n pieData.datasets.push(dataset);\n \n for (var i=0; i < self.ctx.data.length; i++) {\n var dataKey = self.ctx.data[i].dataKey;\n var units = dataKey.units && dataKey.units.length ? dataKey.units : self.ctx.units;\n units = units ? (' (' + units + ')') : '';\n pieData.labels.push(dataKey.label + units);\n dataset.data.push(0);\n var hoverBackgroundColor = tinycolor(dataKey.color).lighten(15);\n dataset.backgroundColor.push(dataKey.color);\n dataset.borderColor.push(borderColor);\n dataset.borderWidth.push(borderWidth);\n dataset.hoverBackgroundColor.push(hoverBackgroundColor.toRgbString());\n }\n\n var options = {\n responsive: false,\n maintainAspectRatio: false,\n legend: {\n display: true,\n labels: {\n fontColor: '#666'\n }\n },\n tooltips: {\n callbacks: {\n label: function(tooltipItem, data) {\n var label = data.labels[tooltipItem.index];\n var value = data.datasets[tooltipItem.datasetIndex].data[tooltipItem.index];\n var content = label + ': ' + value;\n var units = self.ctx.settings.units ? self.ctx.settings.units : self.ctx.units;\n if (units) {\n content += ' ' + units;\n } \n return content;\n }\n }\n }\n };\n\n if (self.ctx.settings.legend) {\n options.legend.display = self.ctx.settings.legend.display !== false;\n options.legend.labels.fontColor = self.ctx.settings.legend.labelsFontColor || '#666';\n }\n\n var ctx = $('#pieChart', self.ctx.$container);\n self.ctx.chart = new Chart(ctx, {\n type: 'doughnut',\n data: pieData,\n options: options\n });\n \n self.onResize();\n}\n\nself.onDataUpdated = function() {\n for (var i = 0; i < self.ctx.data.length; i++) {\n var cellData = self.ctx.data[i];\n if (cellData.data.length > 0) {\n var decimals;\n if (typeof cellData.dataKey.decimals !== 'undefined' \n && cellData.dataKey.decimals !== null ) {\n decimals = cellData.dataKey.decimals; \n } else {\n decimals = self.ctx.decimals;\n }\n var tvPair = cellData.data[cellData.data.length - 1];\n var value = self.ctx.utils.formatValue(tvPair[1], decimals);\n self.ctx.chart.data.datasets[0].data[i] = parseFloat(value);\n }\n }\n self.ctx.chart.update();\n}\n\nself.onResize = function() {\n self.ctx.chart.resize();\n}\n\n",
59 59 "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"Settings\",\n \"properties\": {\n \"borderWidth\": {\n \"title\": \"Border width\",\n \"type\": \"number\",\n \"default\": 5\n },\n \"borderColor\": {\n \"title\": \"Border color\",\n \"type\": \"string\",\n \"default\": \"#fff\"\n },\n \"legend\": {\n \"title\": \"Legend settings\",\n \"type\": \"object\",\n \"properties\": {\n \"display\": {\n \"title\": \"Display legend\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"labelsFontColor\": {\n \"title\": \"Labels font color\",\n \"type\": \"string\",\n \"default\": \"#666\"\n }\n }\n }\n },\n \"required\": []\n },\n \"form\": [\n \"borderWidth\", \n {\n \"key\": \"borderColor\",\n \"type\": \"color\"\n }, \n {\n \"key\": \"legend\",\n \"items\": [\n \"legend.display\",\n {\n \"key\": \"legend.labelsFontColor\",\n \"type\": \"color\"\n }\n ]\n }\n ]\n}",
60 60 "dataKeySettingsSchema": "{}\n",
61 61 "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"First\",\"color\":\"#26a69a\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = (prevValue-50) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+50;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Second\",\"color\":\"#f57c00\",\"settings\":{},\"_hash\":0.545701115289893,\"funcBody\":\"var value = (prevValue-20) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+20;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Third\",\"color\":\"#afb42b\",\"settings\":{},\"_hash\":0.2592906835158064,\"funcBody\":\"var value = (prevValue-40) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+40;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Fourth\",\"color\":\"#673ab7\",\"settings\":{},\"_hash\":0.12880275585455747,\"funcBody\":\"var value = (prevValue-50) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+50;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"borderWidth\":5,\"borderColor\":\"#fff\",\"legend\":{\"display\":true,\"labelsFontColor\":\"#666666\"}},\"title\":\"Doughnut - Chart.js\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400}}"
... ... @@ -91,7 +91,7 @@
91 91 ],
92 92 "templateHtml": "<canvas id=\"pieChart\"></canvas>\n",
93 93 "templateCss": "",
94   - "controllerScript": "self.onInit = function() {\n \n var pieData = {\n labels: [],\n datasets: []\n };\n\n var dataset = {\n data: [],\n backgroundColor: [],\n borderColor: [],\n borderWidth: [],\n hoverBackgroundColor: []\n }\n \n pieData.datasets.push(dataset);\n \n for (var i=0; i < self.ctx.data.length; i++) {\n var dataKey = self.ctx.data[i].dataKey;\n pieData.labels.push(dataKey.label);\n dataset.data.push(0);\n var hoverBackgroundColor = tinycolor(dataKey.color).lighten(15);\n var borderColor = tinycolor(dataKey.color).darken();\n dataset.backgroundColor.push(dataKey.color);\n dataset.borderColor.push('#fff');\n dataset.borderWidth.push(5);\n dataset.hoverBackgroundColor.push(hoverBackgroundColor.toRgbString());\n }\n\n var ctx = $('#pieChart', self.ctx.$container);\n self.ctx.chart = new Chart(ctx, {\n type: 'pie',\n data: pieData,\n options: {\n responsive: false,\n maintainAspectRatio: false\n }\n }); \n \n self.onResize();\n}\n\nself.onDataUpdated = function() {\n for (var i = 0; i < self.ctx.data.length; i++) {\n var cellData = self.ctx.data[i];\n if (cellData.data.length > 0) {\n var tvPair = cellData.data[cellData.data.length - 1];\n var value = tvPair[1];\n self.ctx.chart.data.datasets[0].data[i] = parseFloat(value);\n }\n }\n self.ctx.chart.update();\n}\n\nself.onResize = function() {\n self.ctx.chart.resize();\n}\n",
  94 + "controllerScript": "self.onInit = function() {\n \n var pieData = {\n labels: [],\n datasets: []\n };\n\n var dataset = {\n data: [],\n backgroundColor: [],\n borderColor: [],\n borderWidth: [],\n hoverBackgroundColor: []\n }\n \n pieData.datasets.push(dataset);\n \n for (var i=0; i < self.ctx.data.length; i++) {\n var dataKey = self.ctx.data[i].dataKey;\n var units = dataKey.units && dataKey.units.length ? dataKey.units : self.ctx.units;\n units = units ? (' (' + units + ')') : '';\n pieData.labels.push(dataKey.label + units);\n dataset.data.push(0);\n var hoverBackgroundColor = tinycolor(dataKey.color).lighten(15);\n var borderColor = tinycolor(dataKey.color).darken();\n dataset.backgroundColor.push(dataKey.color);\n dataset.borderColor.push('#fff');\n dataset.borderWidth.push(5);\n dataset.hoverBackgroundColor.push(hoverBackgroundColor.toRgbString());\n }\n\n var ctx = $('#pieChart', self.ctx.$container);\n self.ctx.chart = new Chart(ctx, {\n type: 'pie',\n data: pieData,\n options: {\n responsive: false,\n maintainAspectRatio: false\n }\n }); \n \n self.onResize();\n}\n\nself.onDataUpdated = function() {\n for (var i = 0; i < self.ctx.data.length; i++) {\n var cellData = self.ctx.data[i];\n if (cellData.data.length > 0) {\n var decimals;\n if (typeof cellData.dataKey.decimals !== 'undefined' \n && cellData.dataKey.decimals !== null ) {\n decimals = cellData.dataKey.decimals; \n } else {\n decimals = self.ctx.decimals;\n }\n var tvPair = cellData.data[cellData.data.length - 1];\n var value = self.ctx.utils.formatValue(tvPair[1], decimals);\n self.ctx.chart.data.datasets[0].data[i] = parseFloat(value);\n }\n }\n self.ctx.chart.update();\n}\n\nself.onResize = function() {\n self.ctx.chart.resize();\n}\n",
95 95 "settingsSchema": "{}",
96 96 "dataKeySettingsSchema": "{}\n",
97 97 "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"First\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = (prevValue-50) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+50;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Second\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.545701115289893,\"funcBody\":\"var value = (prevValue-20) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+20;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Third\",\"color\":\"#f44336\",\"settings\":{},\"_hash\":0.2592906835158064,\"funcBody\":\"var value = (prevValue-40) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+40;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Fourth\",\"color\":\"#ffc107\",\"settings\":{},\"_hash\":0.12880275585455747,\"funcBody\":\"var value = (prevValue-50) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+50;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Pie - Chart.js\"}"
... ... @@ -111,7 +111,7 @@
111 111 ],
112 112 "templateHtml": "<canvas id=\"pieChart\"></canvas>\n",
113 113 "templateCss": "",
114   - "controllerScript": "self.onInit = function() {\n var pieData = {\n labels: [],\n datasets: []\n };\n\n var dataset = {\n data: [],\n backgroundColor: [],\n borderColor: [],\n borderWidth: [],\n hoverBackgroundColor: []\n }\n \n pieData.datasets.push(dataset);\n \n for (var i = 0; i < self.ctx.data.length; i++) {\n var dataKey = self.ctx.data[i].dataKey;\n pieData.labels.push(dataKey.label);\n dataset.data.push(0);\n var hoverBackgroundColor = tinycolor(dataKey.color).lighten(15);\n var borderColor = tinycolor(dataKey.color).darken();\n dataset.backgroundColor.push(dataKey.color);\n dataset.borderColor.push('#fff');\n dataset.borderWidth.push(5);\n dataset.hoverBackgroundColor.push(hoverBackgroundColor.toRgbString());\n }\n\n var ctx = $('#pieChart', self.ctx.$container);\n self.ctx.chart = new Chart(ctx, {\n type: 'polarArea',\n data: pieData,\n options: {\n responsive: false,\n maintainAspectRatio: false\n }\n });\n \n self.onResize();\n}\n\nself.onDataUpdated = function() {\n for (var i = 0; i < self.ctx.data.length; i++) {\n var cellData = self.ctx.data[i];\n if (cellData.data.length > 0) {\n var tvPair = cellData.data[cellData.data.length - 1];\n var value = tvPair[1];\n self.ctx.chart.data.datasets[0].data[i] = parseFloat(value);\n }\n }\n \n self.ctx.chart.update();\n}\n\nself.onResize = function() {\n if (self.ctx.height >= 70) {\n try {\n self.ctx.chart.resize();\n } catch (e) {}\n }\n}\n",
  114 + "controllerScript": "self.onInit = function() {\n var pieData = {\n labels: [],\n datasets: []\n };\n\n var dataset = {\n data: [],\n backgroundColor: [],\n borderColor: [],\n borderWidth: [],\n hoverBackgroundColor: []\n }\n \n pieData.datasets.push(dataset);\n \n for (var i = 0; i < self.ctx.data.length; i++) {\n var dataKey = self.ctx.data[i].dataKey;\n var units = dataKey.units && dataKey.units.length ? dataKey.units : self.ctx.units;\n units = units ? (' (' + units + ')') : '';\n pieData.labels.push(dataKey.label + units);\n dataset.data.push(0);\n var hoverBackgroundColor = tinycolor(dataKey.color).lighten(15);\n var borderColor = tinycolor(dataKey.color).darken();\n dataset.backgroundColor.push(dataKey.color);\n dataset.borderColor.push('#fff');\n dataset.borderWidth.push(5);\n dataset.hoverBackgroundColor.push(hoverBackgroundColor.toRgbString());\n }\n\n var ctx = $('#pieChart', self.ctx.$container);\n self.ctx.chart = new Chart(ctx, {\n type: 'polarArea',\n data: pieData,\n options: {\n responsive: false,\n maintainAspectRatio: false\n }\n });\n \n self.onResize();\n}\n\nself.onDataUpdated = function() {\n for (var i = 0; i < self.ctx.data.length; i++) {\n var cellData = self.ctx.data[i];\n if (cellData.data.length > 0) {\n var decimals;\n if (typeof cellData.dataKey.decimals !== 'undefined' \n && cellData.dataKey.decimals !== null ) {\n decimals = cellData.dataKey.decimals; \n } else {\n decimals = self.ctx.decimals;\n }\n var tvPair = cellData.data[cellData.data.length - 1];\n var value = self.ctx.utils.formatValue(tvPair[1], decimals);\n self.ctx.chart.data.datasets[0].data[i] = parseFloat(value);\n }\n }\n \n self.ctx.chart.update();\n}\n\nself.onResize = function() {\n if (self.ctx.height >= 70) {\n try {\n self.ctx.chart.resize();\n } catch (e) {}\n }\n}\n",
115 115 "settingsSchema": "{}",
116 116 "dataKeySettingsSchema": "{}\n",
117 117 "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"First\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = (prevValue-50) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+50;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Second\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.545701115289893,\"funcBody\":\"var value = (prevValue-20) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+20;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Third\",\"color\":\"#f44336\",\"settings\":{},\"_hash\":0.2592906835158064,\"funcBody\":\"var value = (prevValue-40) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+40;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Fourth\",\"color\":\"#ffc107\",\"settings\":{},\"_hash\":0.12880275585455747,\"funcBody\":\"var value = (prevValue-50) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+50;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Fifth\",\"color\":\"#607d8b\",\"settings\":{},\"_hash\":0.2074391823443591,\"funcBody\":\"var value = (prevValue-50) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+50;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Polar Area - Chart.js\"}"
... ... @@ -131,7 +131,7 @@
131 131 ],
132 132 "templateHtml": "<canvas id=\"radarChart\"></canvas>\n",
133 133 "templateCss": "",
134   - "controllerScript": "self.onInit = function() {\n var barData = {\n labels: [],\n datasets: []\n };\n\n var backgroundColor = tinycolor(self.ctx.data[0].dataKey.color);\n backgroundColor.setAlpha(0.2);\n var borderColor = tinycolor(self.ctx.data[0].dataKey.color);\n borderColor.setAlpha(1);\n var dataset = {\n label: self.ctx.datasources[0].name,\n data: [],\n backgroundColor: backgroundColor.toRgbString(),\n borderColor: borderColor.toRgbString(),\n pointBackgroundColor: borderColor.toRgbString(),\n pointBorderColor: borderColor.darken().toRgbString(),\n borderWidth: 1\n }\n \n barData.datasets.push(dataset);\n \n for (var i = 0; i < self.ctx.data.length; i++) {\n var dataKey = self.ctx.data[i].dataKey;\n barData.labels.push(dataKey.label);\n dataset.data.push(0);\n }\n\n var ctx = $('#radarChart', self.ctx.$container);\n self.ctx.chart = new Chart(ctx, {\n type: 'radar',\n data: barData,\n options: {\n responsive: false,\n maintainAspectRatio: false\n }\n });\n \n self.onResize();\n}\n\nself.onDataUpdated = function() {\n for (var i = 0; i < self.ctx.data.length; i++) {\n var cellData = self.ctx.data[i];\n if (cellData.data.length > 0) {\n var tvPair = cellData.data[cellData.data.length - 1];\n var value = tvPair[1];\n self.ctx.chart.data.datasets[0].data[i] = parseFloat(value);\n }\n } \n self.ctx.chart.update();\n}\n\nself.onResize = function() {\n if (self.ctx.height >= 70) {\n self.ctx.chart.resize();\n }\n}\n",
  134 + "controllerScript": "self.onInit = function() {\n var barData = {\n labels: [],\n datasets: []\n };\n\n var backgroundColor = tinycolor(self.ctx.data[0].dataKey.color);\n backgroundColor.setAlpha(0.2);\n var borderColor = tinycolor(self.ctx.data[0].dataKey.color);\n borderColor.setAlpha(1);\n var dataset = {\n label: self.ctx.datasources[0].name,\n data: [],\n backgroundColor: backgroundColor.toRgbString(),\n borderColor: borderColor.toRgbString(),\n pointBackgroundColor: borderColor.toRgbString(),\n pointBorderColor: borderColor.darken().toRgbString(),\n borderWidth: 1\n }\n \n barData.datasets.push(dataset);\n \n for (var i = 0; i < self.ctx.data.length; i++) {\n var dataKey = self.ctx.data[i].dataKey;\n var units = dataKey.units && dataKey.units.length ? dataKey.units : self.ctx.units;\n units = units ? (' (' + units + ')') : '';\n barData.labels.push(dataKey.label + units);\n dataset.data.push(0);\n }\n\n var ctx = $('#radarChart', self.ctx.$container);\n self.ctx.chart = new Chart(ctx, {\n type: 'radar',\n data: barData,\n options: {\n responsive: false,\n maintainAspectRatio: false\n }\n });\n \n self.onResize();\n}\n\nself.onDataUpdated = function() {\n for (var i = 0; i < self.ctx.data.length; i++) {\n var cellData = self.ctx.data[i];\n if (cellData.data.length > 0) {\n var decimals;\n if (typeof cellData.dataKey.decimals !== 'undefined' \n && cellData.dataKey.decimals !== null ) {\n decimals = cellData.dataKey.decimals; \n } else {\n decimals = self.ctx.decimals;\n }\n var tvPair = cellData.data[cellData.data.length - 1];\n var value = self.ctx.utils.formatValue(tvPair[1], decimals);\n self.ctx.chart.data.datasets[0].data[i] = parseFloat(value);\n }\n } \n self.ctx.chart.update();\n}\n\nself.onResize = function() {\n if (self.ctx.height >= 70) {\n self.ctx.chart.resize();\n }\n}\n",
135 135 "settingsSchema": "{}",
136 136 "dataKeySettingsSchema": "{}\n",
137 137 "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"First\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = (prevValue-50) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+50;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Second\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.545701115289893,\"funcBody\":\"var value = (prevValue-20) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+20;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Third\",\"color\":\"#f44336\",\"settings\":{},\"_hash\":0.2592906835158064,\"funcBody\":\"var value = (prevValue-40) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+40;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Fourth\",\"color\":\"#ffc107\",\"settings\":{},\"_hash\":0.12880275585455747,\"funcBody\":\"var value = (prevValue-50) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+50;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Radar - Chart.js\"}"
... ...
... ... @@ -32,8 +32,8 @@
32 32 "templateHtml": "<tb-multiple-input-widget \n [ctx]=\"ctx\">\n</tb-multiple-input-widget>",
33 33 "templateCss": ".tb-toast {\n min-width: 0;\n font-size: 14px !important;\n}",
34 34 "controllerScript": "self.onInit = function() {\r\n}\r\n\r\nself.onDataUpdated = function() {\r\n self.ctx.$scope.multipleInputWidget.onDataUpdated();\r\n}\r\n",
35   - "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"MultipleInput\",\n \"properties\": {\n \"widgetTitle\": {\n \"title\": \"Widget title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"showActionButtons\":{\n \"title\":\"Show action buttons\",\n \"type\":\"boolean\",\n \"default\": true\n },\n \"updateAllValues\": {\n \"title\":\"Update all values, not only modified (only if action buttons are visible)\",\n \"type\":\"boolean\",\n \"default\": false\n },\n \"showResultMessage\":{\n \"title\":\"Show result message\",\n \"type\":\"boolean\",\n \"default\": true\n },\n \"showGroupTitle\": {\n \"title\":\"Show title for group of fields, related to different entities\",\n \"type\":\"boolean\",\n \"default\": false\n },\n \"groupTitle\": {\n \"title\": \"Group title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"fieldsAlignment\": {\n \"title\": \"Fields alignment\",\n \"type\": \"string\",\n \"default\": \"row\"\n },\n \"fieldsInRow\": {\n \"title\": \"Number of fields in the row\",\n \"type\": \"number\",\n \"default\": \"2\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"widgetTitle\",\n \"showActionButtons\",\n \"updateAllValues\",\n \"showResultMessage\",\n \"showGroupTitle\",\n \"groupTitle\",\n {\n \"key\": \"fieldsAlignment\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"row\",\n \"label\": \"Row (default)\"\n },\n {\n \"value\": \"column\",\n \"label\": \"Column\"\n }\n ]\n },\n \"fieldsInRow\"\n ]\n}",
36   - "dataKeySettingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"DataKeySettings\",\n \"properties\": {\n \"dataKeyType\": {\n \"title\": \"Datakey type\",\n \"type\": \"string\",\n \"default\": \"server\"\n },\n \"dataKeyValueType\": {\n \"title\": \"Datakey value type\",\n \"type\": \"string\",\n \"default\": \"string\"\n },\n \"required\": {\n \"title\": \"Value is required\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"isEditable\": {\n \"title\": \"Ability to edit attribute\",\n \"type\": \"string\",\n \"default\": \"editable\"\n },\n \"disabledOnDataKey\": {\n \"title\": \"Disable on false value of another datakey (specify datakey name)\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"dataKeyHidden\": {\n \"title\": \"Hide input field\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"step\": {\n \"title\": \"Step interval between values (only for numbers)\",\n \"type\": \"number\",\n \"default\": \"1\"\n },\n \"requiredErrorMessage\": {\n \"title\": \"'Required' error message\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"icon\": {\n \"title\": \"Icon to show before input cell\",\n \"type\": \"string\",\n \"default\": \"\"\n }\n },\n \"required\": []\n },\n \"form\": [\n {\n \"key\": \"dataKeyType\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"server\",\n \"label\": \"Server attribute (default)\"\n },\n {\n \"value\": \"shared\",\n \"label\": \"Shared attribute\"\n },\n {\n \"value\": \"timeseries\",\n \"label\": \"Timeseries\"\n }\n ]\n },\n {\n \"key\": \"dataKeyValueType\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"string\",\n \"label\": \"String\"\n },\n {\n \"value\": \"double\",\n \"label\": \"Double\"\n },\n {\n \"value\": \"integer\",\n \"label\": \"Integer\"\n },\n {\n \"value\": \"booleanCheckbox\",\n \"label\": \"Boolean (Checkbox)\"\n },\n {\n \"value\": \"booleanSwitch\",\n \"label\": \"Boolean (Switch)\"\n },\n {\n \"value\": \"dateTime\",\n \"label\": \"Date & Time\"\n },\n {\n \"value\": \"date\",\n \"label\": \"Date\"\n },\n {\n \"value\": \"time\",\n \"label\": \"Time\"\n }\n ]\n },\n \"required\",\n {\n \"key\": \"isEditable\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"editable\",\n \"label\": \"Editable (default)\"\n },\n {\n \"value\": \"disabled\",\n \"label\": \"Disabled\"\n },\n {\n \"value\": \"readonly\",\n \"label\": \"Read-only\"\n }\n ]\n },\n \"disabledOnDataKey\",\n \"dataKeyHidden\",\n \"step\",\n \"requiredErrorMessage\",\n\t\t{\n \t\t\"key\": \"icon\",\n\t\t\t\"type\": \"icon\"\n\t\t}\n ]\n}\n",
  35 + "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"MultipleInput\",\n \"properties\": {\n \"widgetTitle\": {\n \"title\": \"Widget title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"showActionButtons\":{\n \"title\":\"Show action buttons\",\n \"type\":\"boolean\",\n \"default\": true\n },\n \"updateAllValues\": {\n \"title\":\"Update all values, not only modified\",\n \"type\":\"boolean\",\n \"default\": false\n },\n \"saveButtonLabel\": {\n \"title\": \"'SAVE' button label\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"resetButtonLabel\": {\n \"title\": \"'UNDO' button label\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"showResultMessage\":{\n \"title\":\"Show result message\",\n \"type\":\"boolean\",\n \"default\": true\n },\n \"showGroupTitle\": {\n \"title\":\"Show title for group of fields, related to different entities\",\n \"type\":\"boolean\",\n \"default\": false\n },\n \"groupTitle\": {\n \"title\": \"Group title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"fieldsAlignment\": {\n \"title\": \"Fields alignment\",\n \"type\": \"string\",\n \"default\": \"row\"\n },\n \"fieldsInRow\": {\n \"title\": \"Number of fields in the row\",\n \"type\": \"number\",\n \"default\": \"2\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"widgetTitle\",\n \"showActionButtons\",\n {\n \"key\": \"updateAllValues\",\n \"condition\": \"model.showActionButtons === true\"\n },\n {\n \"key\": \"saveButtonLabel\",\n \"condition\": \"model.showActionButtons === true\"\n },\n {\n \"key\": \"resetButtonLabel\",\n \"condition\": \"model.showActionButtons === true\"\n },\n \"showResultMessage\",\n \"showGroupTitle\",\n \"groupTitle\",\n {\n \"key\": \"fieldsAlignment\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"row\",\n \"label\": \"Row (default)\"\n },\n {\n \"value\": \"column\",\n \"label\": \"Column\"\n }\n ]\n },\n {\n \"key\": \"fieldsInRow\",\n \"condition\": \"model.fieldsAlignment === 'row'\"\n }\n ]\n}",
  36 + "dataKeySettingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"DataKeySettings\",\n \"properties\": {\n \"dataKeyType\": {\n \"title\": \"Datakey type\",\n \"type\": \"string\",\n \"default\": \"server\"\n },\n \"dataKeyValueType\": {\n \"title\": \"Datakey value type\",\n \"type\": \"string\",\n \"default\": \"string\"\n },\n \"step\": {\n \"title\": \"Step interval between values\",\n \"type\": \"number\",\n \"default\": \"1\"\n },\n \"minValue\": {\n \"title\": \"Minimum value\",\n \"type\": \"number\"\n },\n \"maxValue\": {\n \"title\": \"Maximum value\",\n \"type\": \"number\"\n },\n \"required\": {\n \"title\": \"Value is required\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"requiredErrorMessage\": {\n \"title\": \"'Required' error message\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"minValueErrorMessage\": {\n \"title\": \"'Min Value' error message\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"maxValueErrorMessage\": {\n \"title\": \"'Max Value' error message\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"invalidDateErrorMessage\": {\n \"title\": \"'Invalid Date' error message\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"isEditable\": {\n \"title\": \"Ability to edit attribute\",\n \"type\": \"string\",\n \"default\": \"editable\"\n },\n \"disabledOnDataKey\": {\n \"title\": \"Disable on false value of another datakey (specify datakey name)\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"dataKeyHidden\": {\n \"title\": \"Hide input field\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"icon\": {\n \"title\": \"Icon to show before input cell\",\n \"type\": \"string\",\n \"default\": \"\"\n }\n },\n \"required\": []\n },\n \"form\": [\n {\n \"key\": \"dataKeyType\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"server\",\n \"label\": \"Server attribute (default)\"\n },\n {\n \"value\": \"shared\",\n \"label\": \"Shared attribute\"\n },\n {\n \"value\": \"timeseries\",\n \"label\": \"Timeseries\"\n }\n ]\n },\n {\n \"key\": \"dataKeyValueType\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"string\",\n \"label\": \"String\"\n },\n {\n \"value\": \"double\",\n \"label\": \"Double\"\n },\n {\n \"value\": \"integer\",\n \"label\": \"Integer\"\n },\n {\n \"value\": \"booleanCheckbox\",\n \"label\": \"Boolean (Checkbox)\"\n },\n {\n \"value\": \"booleanSwitch\",\n \"label\": \"Boolean (Switch)\"\n },\n {\n \"value\": \"dateTime\",\n \"label\": \"Date & Time\"\n },\n {\n \"value\": \"date\",\n \"label\": \"Date\"\n },\n {\n \"value\": \"time\",\n \"label\": \"Time\"\n }\n ]\n },\n {\n \"key\": \"step\",\n \"condition\": \"model.dataKeyValueType === 'double' || model.dataKeyValueType === 'integer'\"\n },\n {\n \"key\": \"minValue\",\n \"condition\": \"model.dataKeyValueType === 'double' || model.dataKeyValueType === 'integer'\"\n },\n {\n \"key\": \"maxValue\",\n \"condition\": \"model.dataKeyValueType === 'double' || model.dataKeyValueType === 'integer'\"\n },\n \"required\",\n {\n \"key\": \"requiredErrorMessage\",\n \"condition\": \"model.required === true\"\n },\n {\n \"key\": \"invalidDateErrorMessage\",\n \"condition\": \"model.dataKeyValueType === 'dateTime' || model.dataKeyValueType === 'date' || model.dataKeyValueType === 'time'\"\n },\n {\n \"key\": \"minValueErrorMessage\",\n \"condition\": \"model.dataKeyValueType === 'double' || model.dataKeyValueType === 'integer'\"\n },\n {\n \"key\": \"maxValueErrorMessage\",\n \"condition\": \"model.dataKeyValueType === 'double' || model.dataKeyValueType === 'integer'\"\n },\n {\n \"key\": \"isEditable\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"editable\",\n \"label\": \"Editable (default)\"\n },\n {\n \"value\": \"disabled\",\n \"label\": \"Disabled\"\n },\n {\n \"value\": \"readonly\",\n \"label\": \"Read-only\"\n }\n ]\n },\n \"disabledOnDataKey\",\n \"dataKeyHidden\",\n\t\t{\n \t\t\"key\": \"icon\",\n\t\t\t\"type\": \"icon\"\n\t\t}\n ]\n}\n",
37 37 "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Sin\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.23592248334107624,\"funcBody\":\"return Math.round(1000*Math.sin(time/5000));\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Update Multiple Attributes\",\"dropShadow\":true,\"enableFullscreen\":false,\"enableDataExport\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}"
38 38 }
39 39 },
... ... @@ -391,16 +391,16 @@
391 391 },
392 392 {
393 393 "alias": "web_camera_input",
394   - "name": "Web Camera Input",
  394 + "name": "Photo camera input",
395 395 "descriptor": {
396 396 "type": "latest",
397 397 "sizeX": 7.5,
398 398 "sizeY": 3,
399 399 "resources": [],
400   - "templateHtml": "<tb-web-camera-widget \n [ctx]=\"ctx\">\n</tb-web-camera-widget>",
  400 + "templateHtml": "<tb-photo-camera-widget \n [ctx]=\"ctx\">\n</tb-photo-camera-widget>",
401 401 "templateCss": "",
402   - "controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.webCameraInputWidget.onDataUpdated();\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true\n }\n}\n\nself.onDestroy = function() {\n}\n",
403   - "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"Web Camera\",\n \"properties\": {\n \"widgetTitle\": {\n \"title\": \"Widget title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"imageFormat\": {\n \"title\": \"Image Format\",\n \"type\": \"string\",\n \"default\": \"image/png\"\n },\n \"imageQuality\":{\n \"title\":\"Image quality that use lossy compression such as jpeg and webp\",\n \"type\":\"number\",\n \"default\": 0.92,\n \"min\": 0,\n \"max\": 1\n },\n \"maxWidth\": {\n \"title\": \"The maximal image width\",\n \"type\": \"number\",\n \"default\": 640\n }, \n \"maxHeight\": {\n \"title\": \"The maximal image heigth\",\n \"type\": \"number\",\n \"default\": 480\n }\n },\n \"required\": []\n },\n \"form\": [\n \"widgetTitle\",\n {\n \"key\": \"imageFormat\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"image/jpeg\",\n \"label\": \"JPEG\"\n },\n {\n \"value\": \"image/png\",\n \"label\": \"PNG\"\n },\n {\n \"value\": \"image/webp\",\n \"label\": \"WEBP\"\n }\n ]\n },\n \"imageQuality\",\n \"maxWidth\",\n \"maxHeight\"\n ]\n}",
  402 + "controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.photoCameraInputWidget.onDataUpdated();\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true\n }\n}\n\nself.onDestroy = function() {\n}\n",
  403 + "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"Photo Camera\",\n \"properties\": {\n \"widgetTitle\": {\n \"title\": \"Widget title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"imageFormat\": {\n \"title\": \"Image Format\",\n \"type\": \"string\",\n \"default\": \"image/png\"\n },\n \"imageQuality\":{\n \"title\":\"Image quality that use lossy compression such as jpeg and webp\",\n \"type\":\"number\",\n \"default\": 0.92,\n \"min\": 0,\n \"max\": 1\n },\n \"maxWidth\": {\n \"title\": \"The maximal image width\",\n \"type\": \"number\",\n \"default\": 640\n }, \n \"maxHeight\": {\n \"title\": \"The maximal image heigth\",\n \"type\": \"number\",\n \"default\": 480\n }\n },\n \"required\": []\n },\n \"form\": [\n \"widgetTitle\",\n {\n \"key\": \"imageFormat\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"image/jpeg\",\n \"label\": \"JPEG\"\n },\n {\n \"value\": \"image/png\",\n \"label\": \"PNG\"\n },\n {\n \"value\": \"image/webp\",\n \"label\": \"WEBP\"\n }\n ]\n },\n \"imageQuality\",\n \"maxWidth\",\n \"maxHeight\"\n ]\n}",
404 404 "dataKeySettingsSchema": "{}\n",
405 405 "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Web Camera Input\",\"showTitleIcon\":false,\"titleIcon\":\"more_horiz\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"displayTimewindow\":true,\"showLegend\":false,\"actions\":{}}"
406 406 }
... ...
... ... @@ -94,7 +94,8 @@
94 94 "name": "Device Profile Node",
95 95 "debugMode": false,
96 96 "configuration": {
97   - "persistAlarmRulesState": false
  97 + "persistAlarmRulesState": false,
  98 + "fetchAlarmRulesStateOnStart": false
98 99 }
99 100 }
100 101 ],
... ...
... ... @@ -14,19 +14,92 @@
14 14 -- limitations under the License.
15 15 --
16 16
  17 +CREATE TABLE IF NOT EXISTS oauth2_client_registration_info (
  18 + id uuid NOT NULL CONSTRAINT oauth2_client_registration_info_pkey PRIMARY KEY,
  19 + enabled boolean,
  20 + created_time bigint NOT NULL,
  21 + additional_info varchar,
  22 + client_id varchar(255),
  23 + client_secret varchar(255),
  24 + authorization_uri varchar(255),
  25 + token_uri varchar(255),
  26 + scope varchar(255),
  27 + user_info_uri varchar(255),
  28 + user_name_attribute_name varchar(255),
  29 + jwk_set_uri varchar(255),
  30 + client_authentication_method varchar(255),
  31 + login_button_label varchar(255),
  32 + login_button_icon varchar(255),
  33 + allow_user_creation boolean,
  34 + activate_user boolean,
  35 + type varchar(31),
  36 + basic_email_attribute_key varchar(31),
  37 + basic_first_name_attribute_key varchar(31),
  38 + basic_last_name_attribute_key varchar(31),
  39 + basic_tenant_name_strategy varchar(31),
  40 + basic_tenant_name_pattern varchar(255),
  41 + basic_customer_name_pattern varchar(255),
  42 + basic_default_dashboard_name varchar(255),
  43 + basic_always_full_screen boolean,
  44 + custom_url varchar(255),
  45 + custom_username varchar(255),
  46 + custom_password varchar(255),
  47 + custom_send_token boolean
  48 +);
  49 +
  50 +CREATE TABLE IF NOT EXISTS oauth2_client_registration (
  51 + id uuid NOT NULL CONSTRAINT oauth2_client_registration_pkey PRIMARY KEY,
  52 + created_time bigint NOT NULL,
  53 + domain_name varchar(255),
  54 + domain_scheme varchar(31),
  55 + client_registration_info_id uuid
  56 +);
  57 +
  58 +CREATE TABLE IF NOT EXISTS oauth2_client_registration_template (
  59 + id uuid NOT NULL CONSTRAINT oauth2_client_registration_template_pkey PRIMARY KEY,
  60 + created_time bigint NOT NULL,
  61 + additional_info varchar,
  62 + provider_id varchar(255),
  63 + authorization_uri varchar(255),
  64 + token_uri varchar(255),
  65 + scope varchar(255),
  66 + user_info_uri varchar(255),
  67 + user_name_attribute_name varchar(255),
  68 + jwk_set_uri varchar(255),
  69 + client_authentication_method varchar(255),
  70 + type varchar(31),
  71 + basic_email_attribute_key varchar(31),
  72 + basic_first_name_attribute_key varchar(31),
  73 + basic_last_name_attribute_key varchar(31),
  74 + basic_tenant_name_strategy varchar(31),
  75 + basic_tenant_name_pattern varchar(255),
  76 + basic_customer_name_pattern varchar(255),
  77 + basic_default_dashboard_name varchar(255),
  78 + basic_always_full_screen boolean,
  79 + comment varchar,
  80 + login_button_icon varchar(255),
  81 + login_button_label varchar(255),
  82 + help_link varchar(255),
  83 + CONSTRAINT oauth2_template_provider_id_unq_key UNIQUE (provider_id)
  84 +);
  85 +
17 86 CREATE TABLE IF NOT EXISTS device_profile (
18 87 id uuid NOT NULL CONSTRAINT device_profile_pkey PRIMARY KEY,
19 88 created_time bigint NOT NULL,
20 89 name varchar(255),
21 90 type varchar(255),
22 91 transport_type varchar(255),
  92 + provision_type varchar(255),
23 93 profile_data jsonb,
24 94 description varchar,
25 95 search_text varchar(255),
26 96 is_default boolean,
27 97 tenant_id uuid,
28 98 default_rule_chain_id uuid,
  99 + default_queue_name varchar(255),
  100 + provision_device_key varchar,
29 101 CONSTRAINT device_profile_name_unq_key UNIQUE (tenant_id, name),
  102 + CONSTRAINT device_provision_key_unq_key UNIQUE (provision_device_key),
30 103 CONSTRAINT fk_default_rule_chain_device_profile FOREIGN KEY (default_rule_chain_id) REFERENCES rule_chain(id)
31 104 );
32 105
... ...
... ... @@ -32,6 +32,8 @@ import org.springframework.data.redis.core.RedisTemplate;
32 32 import org.springframework.scheduling.annotation.Scheduled;
33 33 import org.springframework.stereotype.Component;
34 34 import org.thingsboard.rule.engine.api.MailService;
  35 +import org.thingsboard.rule.engine.api.SmsService;
  36 +import org.thingsboard.rule.engine.api.sms.SmsSenderFactory;
35 37 import org.thingsboard.server.actors.service.ActorService;
36 38 import org.thingsboard.server.actors.tenant.DebugTbRateLimits;
37 39 import org.thingsboard.server.common.data.DataConstants;
... ... @@ -67,6 +69,8 @@ import org.thingsboard.server.dao.timeseries.TimeseriesService;
67 69 import org.thingsboard.server.dao.user.UserService;
68 70 import org.thingsboard.server.queue.discovery.PartitionService;
69 71 import org.thingsboard.server.queue.discovery.TbServiceInfoProvider;
  72 +import org.thingsboard.server.queue.usagestats.TbApiUsageClient;
  73 +import org.thingsboard.server.service.apiusage.TbApiUsageStateService;
70 74 import org.thingsboard.server.service.component.ComponentDiscoveryService;
71 75 import org.thingsboard.server.service.edge.rpc.EdgeRpcService;
72 76 import org.thingsboard.server.service.executors.DbCallbackExecutorService;
... ... @@ -74,12 +78,14 @@ import org.thingsboard.server.service.executors.ExternalCallExecutorService;
74 78 import org.thingsboard.server.service.executors.SharedEventLoopGroupService;
75 79 import org.thingsboard.server.service.mail.MailExecutorService;
76 80 import org.thingsboard.server.service.profile.TbDeviceProfileCache;
  81 +import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
77 82 import org.thingsboard.server.service.queue.TbClusterService;
78 83 import org.thingsboard.server.service.rpc.TbCoreDeviceRpcService;
79 84 import org.thingsboard.server.service.rpc.TbRuleEngineDeviceRpcService;
80 85 import org.thingsboard.server.service.script.JsExecutorService;
81 86 import org.thingsboard.server.service.script.JsInvokeService;
82 87 import org.thingsboard.server.service.session.DeviceSessionCacheService;
  88 +import org.thingsboard.server.service.sms.SmsExecutorService;
83 89 import org.thingsboard.server.service.state.DeviceStateService;
84 90 import org.thingsboard.server.service.telemetry.AlarmSubscriptionService;
85 91 import org.thingsboard.server.service.telemetry.TelemetrySubscriptionService;
... ... @@ -109,6 +115,14 @@ public class ActorSystemContext {
109 115
110 116 @Autowired
111 117 @Getter
  118 + private TbApiUsageStateService apiUsageStateService;
  119 +
  120 + @Autowired
  121 + @Getter
  122 + private TbApiUsageClient apiUsageClient;
  123 +
  124 + @Autowired
  125 + @Getter
112 126 @Setter
113 127 private TbServiceInfoProvider serviceInfoProvider;
114 128
... ... @@ -131,6 +145,10 @@ public class ActorSystemContext {
131 145
132 146 @Autowired
133 147 @Getter
  148 + private TbTenantProfileCache tenantProfileCache;
  149 +
  150 + @Autowired
  151 + @Getter
134 152 private TbDeviceProfileCache deviceProfileCache;
135 153
136 154 @Autowired
... ... @@ -218,6 +236,10 @@ public class ActorSystemContext {
218 236
219 237 @Autowired
220 238 @Getter
  239 + private SmsExecutorService smsExecutor;
  240 +
  241 + @Autowired
  242 + @Getter
221 243 private DbCallbackExecutorService dbCallbackExecutor;
222 244
223 245 @Autowired
... ... @@ -234,6 +256,14 @@ public class ActorSystemContext {
234 256
235 257 @Autowired
236 258 @Getter
  259 + private SmsService smsService;
  260 +
  261 + @Autowired
  262 + @Getter
  263 + private SmsSenderFactory smsSenderFactory;
  264 +
  265 + @Autowired
  266 + @Getter
237 267 private ClaimDevicesService claimDevicesService;
238 268
239 269 @Autowired
... ... @@ -325,6 +355,10 @@ public class ActorSystemContext {
325 355 @Getter
326 356 private boolean allowSystemMailService;
327 357
  358 + @Value("${actors.rule.allow_system_sms_service}")
  359 + @Getter
  360 + private boolean allowSystemSmsService;
  361 +
328 362 @Value("${transport.sessions.inactivity_timeout}")
329 363 @Getter
330 364 private long sessionInactivityTimeout;
... ... @@ -553,7 +587,11 @@ public class ActorSystemContext {
553 587
554 588 public void scheduleMsgWithDelay(TbActorRef ctx, TbActorMsg msg, long delayInMs) {
555 589 log.debug("Scheduling msg {} with delay {} ms", msg, delayInMs);
556   - getScheduler().schedule(() -> ctx.tell(msg), delayInMs, TimeUnit.MILLISECONDS);
  590 + if (delayInMs > 0) {
  591 + getScheduler().schedule(() -> ctx.tell(msg), delayInMs, TimeUnit.MILLISECONDS);
  592 + } else {
  593 + ctx.tell(msg);
  594 + }
557 595 }
558 596
559 597 }
... ...
... ... @@ -39,8 +39,8 @@ import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg;
39 39 import org.thingsboard.server.common.msg.queue.QueueToRuleEngineMsg;
40 40 import org.thingsboard.server.common.msg.queue.RuleEngineException;
41 41 import org.thingsboard.server.common.msg.queue.ServiceType;
42   -import org.thingsboard.server.dao.model.ModelConstants;
43 42 import org.thingsboard.server.dao.tenant.TenantService;
  43 +import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
44 44 import org.thingsboard.server.service.transport.msg.TransportToDeviceActorMsgWrapper;
45 45
46 46 import java.util.HashSet;
... ... @@ -50,13 +50,14 @@ import java.util.Set;
50 50 @Slf4j
51 51 public class AppActor extends ContextAwareActor {
52 52
53   - private static final TenantId SYSTEM_TENANT = new TenantId(ModelConstants.NULL_UUID);
  53 + private final TbTenantProfileCache tenantProfileCache;
54 54 private final TenantService tenantService;
55 55 private final Set<TenantId> deletedTenants;
56 56 private boolean ruleChainsInitialized;
57 57
58 58 private AppActor(ActorSystemContext systemContext) {
59 59 super(systemContext);
  60 + this.tenantProfileCache = systemContext.getTenantProfileCache();
60 61 this.tenantService = systemContext.getTenantService();
61 62 this.deletedTenants = new HashSet<>();
62 63 }
... ... @@ -117,8 +118,7 @@ public class AppActor extends ContextAwareActor {
117 118 boolean isRuleEngine = systemContext.getServiceInfoProvider().isService(ServiceType.TB_RULE_ENGINE);
118 119 boolean isCore = systemContext.getServiceInfoProvider().isService(ServiceType.TB_CORE);
119 120 for (Tenant tenant : tenantIterator) {
120   - // TODO: Tenant Profile from cache
121   - TenantProfile tenantProfile = systemContext.getTenantProfileService().findTenantProfileById(TenantId.SYS_TENANT_ID, tenant.getTenantProfileId());
  121 + TenantProfile tenantProfile = tenantProfileCache.get(tenant.getTenantProfileId());
122 122 if (isCore || (isRuleEngine && !tenantProfile.isIsolatedTbRuleEngine())) {
123 123 log.debug("[{}] Creating tenant actor", tenant.getId());
124 124 getOrCreateTenantActor(tenant.getId());
... ... @@ -133,7 +133,7 @@ public class AppActor extends ContextAwareActor {
133 133 }
134 134
135 135 private void onQueueToRuleEngineMsg(QueueToRuleEngineMsg msg) {
136   - if (SYSTEM_TENANT.equals(msg.getTenantId())) {
  136 + if (TenantId.SYS_TENANT_ID.equals(msg.getTenantId())) {
137 137 msg.getTbMsg().getCallback().onFailure(new RuleEngineException("Message has system tenant id!"));
138 138 } else {
139 139 if (!deletedTenants.contains(msg.getTenantId())) {
... ... @@ -146,15 +146,20 @@ public class AppActor extends ContextAwareActor {
146 146
147 147 private void onComponentLifecycleMsg(ComponentLifecycleMsg msg) {
148 148 TbActorRef target = null;
149   - if (SYSTEM_TENANT.equals(msg.getTenantId())) {
150   - log.warn("Message has system tenant id: {}", msg);
  149 + if (TenantId.SYS_TENANT_ID.equals(msg.getTenantId())) {
  150 + if (!EntityType.TENANT_PROFILE.equals(msg.getEntityId().getEntityType())) {
  151 + log.warn("Message has system tenant id: {}", msg);
  152 + }
151 153 } else {
152   - if (msg.getEntityId().getEntityType() == EntityType.TENANT
153   - && msg.getEvent() == ComponentLifecycleEvent.DELETED) {
154   - log.info("[{}] Handling tenant deleted notification: {}", msg.getTenantId(), msg);
  154 + if (EntityType.TENANT.equals(msg.getEntityId().getEntityType())) {
155 155 TenantId tenantId = new TenantId(msg.getEntityId().getId());
156   - deletedTenants.add(tenantId);
157   - ctx.stop(new TbEntityActorId(tenantId));
  156 + if (msg.getEvent() == ComponentLifecycleEvent.DELETED) {
  157 + log.info("[{}] Handling tenant deleted notification: {}", msg.getTenantId(), msg);
  158 + deletedTenants.add(tenantId);
  159 + ctx.stop(new TbEntityActorId(tenantId));
  160 + } else {
  161 + target = getOrCreateTenantActor(msg.getTenantId());
  162 + }
158 163 } else {
159 164 target = getOrCreateTenantActor(msg.getTenantId());
160 165 }
... ...
... ... @@ -20,6 +20,7 @@ import com.fasterxml.jackson.databind.ObjectMapper;
20 20 import io.netty.channel.EventLoopGroup;
21 21 import lombok.extern.slf4j.Slf4j;
22 22 import org.springframework.data.redis.core.RedisTemplate;
  23 +import org.springframework.util.StringUtils;
23 24 import org.thingsboard.common.util.ListeningExecutor;
24 25 import org.thingsboard.rule.engine.api.MailService;
25 26 import org.thingsboard.rule.engine.api.RuleEngineAlarmService;
... ... @@ -27,15 +28,21 @@ import org.thingsboard.rule.engine.api.RuleEngineDeviceProfileCache;
27 28 import org.thingsboard.rule.engine.api.RuleEngineRpcService;
28 29 import org.thingsboard.rule.engine.api.RuleEngineTelemetryService;
29 30 import org.thingsboard.rule.engine.api.ScriptEngine;
  31 +import org.thingsboard.rule.engine.api.SmsService;
30 32 import org.thingsboard.rule.engine.api.TbContext;
31 33 import org.thingsboard.rule.engine.api.TbRelationTypes;
  34 +import org.thingsboard.rule.engine.api.sms.SmsSenderFactory;
32 35 import org.thingsboard.server.actors.ActorSystemContext;
33 36 import org.thingsboard.server.actors.TbActorRef;
  37 +import org.thingsboard.server.common.data.ApiUsageRecordKey;
34 38 import org.thingsboard.server.common.data.Customer;
35 39 import org.thingsboard.server.common.data.DataConstants;
36 40 import org.thingsboard.server.common.data.Device;
  41 +import org.thingsboard.server.common.data.DeviceProfile;
  42 +import org.thingsboard.server.common.data.TenantProfile;
37 43 import org.thingsboard.server.common.data.alarm.Alarm;
38 44 import org.thingsboard.server.common.data.asset.Asset;
  45 +import org.thingsboard.server.common.data.id.DeviceId;
39 46 import org.thingsboard.server.common.data.id.EntityId;
40 47 import org.thingsboard.server.common.data.id.RuleChainId;
41 48 import org.thingsboard.server.common.data.id.RuleNodeId;
... ... @@ -47,6 +54,7 @@ import org.thingsboard.server.common.data.rule.RuleNodeState;
47 54 import org.thingsboard.server.common.msg.TbActorMsg;
48 55 import org.thingsboard.server.common.msg.TbMsg;
49 56 import org.thingsboard.server.common.msg.TbMsgMetaData;
  57 +import org.thingsboard.server.common.msg.queue.ServiceQueue;
50 58 import org.thingsboard.server.common.msg.queue.ServiceType;
51 59 import org.thingsboard.server.common.msg.queue.TopicPartitionInfo;
52 60 import org.thingsboard.server.dao.asset.AssetService;
... ... @@ -72,6 +80,7 @@ import org.thingsboard.server.service.script.RuleNodeJsScriptEngine;
72 80
73 81 import java.util.Collections;
74 82 import java.util.Set;
  83 +import java.util.function.BiConsumer;
75 84 import java.util.function.Consumer;
76 85
77 86 /**
... ... @@ -185,6 +194,9 @@ class DefaultTbContext implements TbContext {
185 194 }
186 195
187 196 private TopicPartitionInfo resolvePartition(TbMsg tbMsg, String queueName) {
  197 + if (StringUtils.isEmpty(queueName)) {
  198 + queueName = ServiceQueue.MAIN;
  199 + }
188 200 return mainCtx.resolve(ServiceType.TB_RULE_ENGINE, queueName, getTenantId(), tbMsg.getOriginator());
189 201 }
190 202
... ... @@ -253,26 +265,26 @@ class DefaultTbContext implements TbContext {
253 265 }
254 266
255 267 public TbMsg customerCreatedMsg(Customer customer, RuleNodeId ruleNodeId) {
256   - return entityCreatedMsg(customer, customer.getId(), ruleNodeId);
  268 + return entityActionMsg(customer, customer.getId(), ruleNodeId, DataConstants.ENTITY_CREATED);
257 269 }
258 270
259 271 public TbMsg deviceCreatedMsg(Device device, RuleNodeId ruleNodeId) {
260   - return entityCreatedMsg(device, device.getId(), ruleNodeId);
  272 + return entityActionMsg(device, device.getId(), ruleNodeId, DataConstants.ENTITY_CREATED);
261 273 }
262 274
263 275 public TbMsg assetCreatedMsg(Asset asset, RuleNodeId ruleNodeId) {
264   - return entityCreatedMsg(asset, asset.getId(), ruleNodeId);
  276 + return entityActionMsg(asset, asset.getId(), ruleNodeId, DataConstants.ENTITY_CREATED);
265 277 }
266 278
267   - public TbMsg alarmCreatedMsg(Alarm alarm, RuleNodeId ruleNodeId) {
268   - return entityCreatedMsg(alarm, alarm.getId(), ruleNodeId);
  279 + public TbMsg alarmActionMsg(Alarm alarm, RuleNodeId ruleNodeId, String action) {
  280 + return entityActionMsg(alarm, alarm.getId(), ruleNodeId, action);
269 281 }
270 282
271   - public <E, I extends EntityId> TbMsg entityCreatedMsg(E entity, I id, RuleNodeId ruleNodeId) {
  283 + public <E, I extends EntityId> TbMsg entityActionMsg(E entity, I id, RuleNodeId ruleNodeId, String action) {
272 284 try {
273   - return TbMsg.newMsg(DataConstants.ENTITY_CREATED, id, getActionMetaData(ruleNodeId), mapper.writeValueAsString(mapper.valueToTree(entity)));
  285 + return TbMsg.newMsg(action, id, getActionMetaData(ruleNodeId), mapper.writeValueAsString(mapper.valueToTree(entity)));
274 286 } catch (JsonProcessingException | IllegalArgumentException e) {
275   - throw new RuntimeException("Failed to process " + id.getEntityType().name().toLowerCase() + " created msg: " + e);
  287 + throw new RuntimeException("Failed to process " + id.getEntityType().name().toLowerCase() + " " + action + " msg: " + e);
276 288 }
277 289 }
278 290
... ... @@ -297,6 +309,11 @@ class DefaultTbContext implements TbContext {
297 309 }
298 310
299 311 @Override
  312 + public ListeningExecutor getSmsExecutor() {
  313 + return mainCtx.getSmsExecutor();
  314 + }
  315 +
  316 + @Override
300 317 public ListeningExecutor getDbCallbackExecutor() {
301 318 return mainCtx.getDbCallbackExecutor();
302 319 }
... ... @@ -308,7 +325,7 @@ class DefaultTbContext implements TbContext {
308 325
309 326 @Override
310 327 public ScriptEngine createJsScriptEngine(String script, String... argNames) {
311   - return new RuleNodeJsScriptEngine(mainCtx.getJsSandbox(), nodeCtx.getSelf().getId(), script, argNames);
  328 + return new RuleNodeJsScriptEngine(getTenantId(), mainCtx.getJsSandbox(), nodeCtx.getSelf().getId(), script, argNames);
312 329 }
313 330
314 331 @Override
... ... @@ -432,6 +449,20 @@ class DefaultTbContext implements TbContext {
432 449 }
433 450
434 451 @Override
  452 + public SmsService getSmsService() {
  453 + if (mainCtx.isAllowSystemSmsService()) {
  454 + return mainCtx.getSmsService();
  455 + } else {
  456 + throw new RuntimeException("Access to System SMS Service is forbidden!");
  457 + }
  458 + }
  459 +
  460 + @Override
  461 + public SmsSenderFactory getSmsSenderFactory() {
  462 + return mainCtx.getSmsSenderFactory();
  463 + }
  464 +
  465 + @Override
435 466 public RuleEngineRpcService getRpcService() {
436 467 return mainCtx.getTbRuleEngineDeviceRpcService();
437 468 }
... ... @@ -476,6 +507,43 @@ class DefaultTbContext implements TbContext {
476 507 return mainCtx.getRuleNodeStateService().save(getTenantId(), state);
477 508 }
478 509
  510 + @Override
  511 + public void clearRuleNodeStates() {
  512 + if (log.isDebugEnabled()) {
  513 + log.debug("[{}][{}] Going to clear rule node states", getTenantId(), getSelfId());
  514 + }
  515 + mainCtx.getRuleNodeStateService().removeByRuleNodeId(getTenantId(), getSelfId());
  516 + }
  517 +
  518 + @Override
  519 + public void removeRuleNodeStateForEntity(EntityId entityId) {
  520 + if (log.isDebugEnabled()) {
  521 + log.debug("[{}][{}][{}] Remove Rule Node State for entity.", getTenantId(), getSelfId(), entityId);
  522 + }
  523 + mainCtx.getRuleNodeStateService().removeByRuleNodeIdAndEntityId(getTenantId(), getSelfId(), entityId);
  524 + }
  525 +
  526 + @Override
  527 + public void addTenantProfileListener(Consumer<TenantProfile> listener) {
  528 + mainCtx.getTenantProfileCache().addListener(getTenantId(), getSelfId(), listener);
  529 + }
  530 +
  531 + @Override
  532 + public void addDeviceProfileListeners(Consumer<DeviceProfile> profileListener, BiConsumer<DeviceId, DeviceProfile> deviceListener) {
  533 + mainCtx.getDeviceProfileCache().addListener(getTenantId(), getSelfId(), profileListener, deviceListener);
  534 + }
  535 +
  536 + @Override
  537 + public void removeListeners() {
  538 + mainCtx.getDeviceProfileCache().removeListener(getTenantId(), getSelfId());
  539 + mainCtx.getTenantProfileCache().removeListener(getTenantId(), getSelfId());
  540 + }
  541 +
  542 + @Override
  543 + public TenantProfile getTenantProfile() {
  544 + return mainCtx.getTenantProfileCache().get(getTenantId());
  545 + }
  546 +
479 547 private TbMsgMetaData getActionMetaData(RuleNodeId ruleNodeId) {
480 548 TbMsgMetaData metaData = new TbMsgMetaData();
481 549 metaData.putValue("ruleNodeId", ruleNodeId.toString());
... ...
... ... @@ -23,6 +23,7 @@ import org.thingsboard.server.actors.TbActorRef;
23 23 import org.thingsboard.server.actors.TbEntityActorId;
24 24 import org.thingsboard.server.actors.service.DefaultActorService;
25 25 import org.thingsboard.server.actors.shared.ComponentMsgProcessor;
  26 +import org.thingsboard.server.common.data.ApiUsageRecordKey;
26 27 import org.thingsboard.server.common.data.EntityType;
27 28 import org.thingsboard.server.common.data.id.EntityId;
28 29 import org.thingsboard.server.common.data.id.RuleChainId;
... ... @@ -47,6 +48,7 @@ import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg;
47 48 import org.thingsboard.server.queue.TbQueueCallback;
48 49 import org.thingsboard.server.queue.common.MultipleTbQueueTbMsgCallbackWrapper;
49 50 import org.thingsboard.server.queue.common.TbQueueTbMsgCallbackWrapper;
  51 +import org.thingsboard.server.queue.usagestats.TbApiUsageClient;
50 52 import org.thingsboard.server.service.queue.TbClusterService;
51 53
52 54 import java.util.ArrayList;
... ... @@ -69,15 +71,16 @@ public class RuleChainActorMessageProcessor extends ComponentMsgProcessor<RuleCh
69 71 private final Map<RuleNodeId, List<RuleNodeRelation>> nodeRoutes;
70 72 private final RuleChainService service;
71 73 private final TbClusterService clusterService;
  74 + private final TbApiUsageClient apiUsageClient;
72 75 private String ruleChainName;
73 76
74 77 private RuleNodeId firstId;
75 78 private RuleNodeCtx firstNode;
76 79 private boolean started;
77 80
78   - RuleChainActorMessageProcessor(TenantId tenantId, RuleChain ruleChain, ActorSystemContext systemContext
79   - , TbActorRef parent, TbActorRef self) {
  81 + RuleChainActorMessageProcessor(TenantId tenantId, RuleChain ruleChain, ActorSystemContext systemContext, TbActorRef parent, TbActorRef self) {
80 82 super(systemContext, tenantId, ruleChain.getId());
  83 + this.apiUsageClient = systemContext.getApiUsageClient();
81 84 this.ruleChainName = ruleChain.getName();
82 85 this.parent = parent;
83 86 this.self = self;
... ...
... ... @@ -64,6 +64,12 @@ public abstract class RuleChainManagerActor extends ContextAwareActor {
64 64 }
65 65 }
66 66
  67 + protected void destroyRuleChains() {
  68 + for (RuleChain ruleChain : new PageDataIterable<>(link -> ruleChainService.findTenantRuleChainsByType(tenantId, RuleChainType.CORE, link), ContextAwareActor.ENTITY_PACK_LIMIT)) {
  69 + ctx.stop(new TbEntityActorId(ruleChain.getId()));
  70 + }
  71 + }
  72 +
67 73 protected void visit(RuleChain entity, TbActorRef actorRef) {
68 74 if (entity != null && entity.isRoot() && entity.getType().equals(RuleChainType.CORE)) {
69 75 rootChain = entity;
... ...
... ... @@ -21,13 +21,18 @@ import org.thingsboard.server.actors.ActorSystemContext;
21 21 import org.thingsboard.server.actors.TbActorCtx;
22 22 import org.thingsboard.server.actors.TbActorRef;
23 23 import org.thingsboard.server.actors.shared.ComponentMsgProcessor;
  24 +import org.thingsboard.server.common.data.ApiUsageRecordKey;
  25 +import org.thingsboard.server.common.data.TenantProfile;
24 26 import org.thingsboard.server.common.data.id.RuleNodeId;
25 27 import org.thingsboard.server.common.data.id.TenantId;
26 28 import org.thingsboard.server.common.data.plugin.ComponentLifecycleState;
27 29 import org.thingsboard.server.common.data.rule.RuleNode;
  30 +import org.thingsboard.server.common.data.tenant.profile.TenantProfileConfiguration;
  31 +import org.thingsboard.server.common.msg.TbMsg;
28 32 import org.thingsboard.server.common.msg.queue.PartitionChangeMsg;
29 33 import org.thingsboard.server.common.msg.queue.RuleNodeException;
30 34 import org.thingsboard.server.common.msg.queue.RuleNodeInfo;
  35 +import org.thingsboard.server.queue.usagestats.TbApiUsageClient;
31 36
32 37 /**
33 38 * @author Andrew Shvayka
... ... @@ -36,6 +41,7 @@ public class RuleNodeActorMessageProcessor extends ComponentMsgProcessor<RuleNod
36 41
37 42 private final String ruleChainName;
38 43 private final TbActorRef self;
  44 + private final TbApiUsageClient apiUsageClient;
39 45 private RuleNode ruleNode;
40 46 private TbNode tbNode;
41 47 private DefaultTbContext defaultCtx;
... ... @@ -44,6 +50,7 @@ public class RuleNodeActorMessageProcessor extends ComponentMsgProcessor<RuleNod
44 50 RuleNodeActorMessageProcessor(TenantId tenantId, String ruleChainName, RuleNodeId ruleNodeId, ActorSystemContext systemContext
45 51 , TbActorRef parent, TbActorRef self) {
46 52 super(systemContext, tenantId, ruleNodeId);
  53 + this.apiUsageClient = systemContext.getApiUsageClient();
47 54 this.ruleChainName = ruleChainName;
48 55 this.self = self;
49 56 this.ruleNode = systemContext.getRuleChainService().findRuleNodeById(tenantId, entityId);
... ... @@ -92,26 +99,42 @@ public class RuleNodeActorMessageProcessor extends ComponentMsgProcessor<RuleNod
92 99
93 100 public void onRuleToSelfMsg(RuleNodeToSelfMsg msg) throws Exception {
94 101 checkActive(msg.getMsg());
95   - if (ruleNode.isDebugMode()) {
96   - systemContext.persistDebugInput(tenantId, entityId, msg.getMsg(), "Self");
97   - }
98   - try {
99   - tbNode.onMsg(defaultCtx, msg.getMsg());
100   - } catch (Exception e) {
101   - defaultCtx.tellFailure(msg.getMsg(), e);
  102 + TbMsg tbMsg = msg.getMsg();
  103 + int ruleNodeCount = tbMsg.getAndIncrementRuleNodeCounter();
  104 + int maxRuleNodeExecutionsPerMessage = getTenantProfileConfiguration().getMaxRuleNodeExecsPerMessage();
  105 + if (maxRuleNodeExecutionsPerMessage == 0 || ruleNodeCount < maxRuleNodeExecutionsPerMessage) {
  106 + apiUsageClient.report(tenantId, ApiUsageRecordKey.RE_EXEC_COUNT);
  107 + if (ruleNode.isDebugMode()) {
  108 + systemContext.persistDebugInput(tenantId, entityId, msg.getMsg(), "Self");
  109 + }
  110 + try {
  111 + tbNode.onMsg(defaultCtx, msg.getMsg());
  112 + } catch (Exception e) {
  113 + defaultCtx.tellFailure(msg.getMsg(), e);
  114 + }
  115 + } else {
  116 + tbMsg.getCallback().onFailure(new RuleNodeException("Message is processed by more then " + maxRuleNodeExecutionsPerMessage + " rule nodes!", ruleChainName, ruleNode));
102 117 }
103 118 }
104 119
105 120 void onRuleChainToRuleNodeMsg(RuleChainToRuleNodeMsg msg) throws Exception {
106 121 msg.getMsg().getCallback().onProcessingStart(info);
107 122 checkActive(msg.getMsg());
108   - if (ruleNode.isDebugMode()) {
109   - systemContext.persistDebugInput(tenantId, entityId, msg.getMsg(), msg.getFromRelationType());
110   - }
111   - try {
112   - tbNode.onMsg(msg.getCtx(), msg.getMsg());
113   - } catch (Exception e) {
114   - msg.getCtx().tellFailure(msg.getMsg(), e);
  123 + TbMsg tbMsg = msg.getMsg();
  124 + int ruleNodeCount = tbMsg.getAndIncrementRuleNodeCounter();
  125 + int maxRuleNodeExecutionsPerMessage = getTenantProfileConfiguration().getMaxRuleNodeExecsPerMessage();
  126 + if (maxRuleNodeExecutionsPerMessage == 0 || ruleNodeCount < maxRuleNodeExecutionsPerMessage) {
  127 + apiUsageClient.report(tenantId, ApiUsageRecordKey.RE_EXEC_COUNT);
  128 + if (ruleNode.isDebugMode()) {
  129 + systemContext.persistDebugInput(tenantId, entityId, msg.getMsg(), msg.getFromRelationType());
  130 + }
  131 + try {
  132 + tbNode.onMsg(msg.getCtx(), msg.getMsg());
  133 + } catch (Exception e) {
  134 + msg.getCtx().tellFailure(msg.getMsg(), e);
  135 + }
  136 + } else {
  137 + tbMsg.getCallback().onFailure(new RuleNodeException("Message is processed by more then " + maxRuleNodeExecutionsPerMessage + " rule nodes!", ruleChainName, ruleNode));
115 138 }
116 139 }
117 140
... ...
... ... @@ -20,6 +20,7 @@ import org.springframework.beans.factory.annotation.Autowired;
20 20 import org.springframework.beans.factory.annotation.Value;
21 21 import org.springframework.boot.context.event.ApplicationReadyEvent;
22 22 import org.springframework.context.event.EventListener;
  23 +import org.springframework.core.annotation.Order;
23 24 import org.springframework.stereotype.Service;
24 25 import org.thingsboard.common.util.ThingsBoardThreadFactory;
25 26 import org.thingsboard.server.actors.ActorSystemContext;
... ... @@ -113,6 +114,7 @@ public class DefaultActorService implements ActorService {
113 114 }
114 115
115 116 @EventListener(ApplicationReadyEvent.class)
  117 + @Order(value = 2)
116 118 public void onApplicationEvent(ApplicationReadyEvent applicationReadyEvent) {
117 119 log.info("Received application ready event. Sending application init message to actor system");
118 120 appActor.tellWithHighPriority(new AppInitMsg());
... ...
... ... @@ -19,9 +19,11 @@ import lombok.extern.slf4j.Slf4j;
19 19 import org.thingsboard.server.actors.ActorSystemContext;
20 20 import org.thingsboard.server.actors.TbActorCtx;
21 21 import org.thingsboard.server.actors.stats.StatsPersistTick;
  22 +import org.thingsboard.server.common.data.TenantProfile;
22 23 import org.thingsboard.server.common.data.id.EntityId;
23 24 import org.thingsboard.server.common.data.id.TenantId;
24 25 import org.thingsboard.server.common.data.plugin.ComponentLifecycleState;
  26 +import org.thingsboard.server.common.data.tenant.profile.TenantProfileConfiguration;
25 27 import org.thingsboard.server.common.msg.TbMsg;
26 28 import org.thingsboard.server.common.msg.queue.PartitionChangeMsg;
27 29 import org.thingsboard.server.common.msg.queue.RuleNodeException;
... ... @@ -39,6 +41,10 @@ public abstract class ComponentMsgProcessor<T extends EntityId> extends Abstract
39 41 this.entityId = id;
40 42 }
41 43
  44 + protected TenantProfileConfiguration getTenantProfileConfiguration() {
  45 + return systemContext.getTenantProfileCache().get(tenantId).getProfileData().getConfiguration();
  46 + }
  47 +
42 48 public abstract String getComponentName();
43 49
44 50 public abstract void start(TbActorCtx context) throws Exception;
... ...
... ... @@ -29,6 +29,7 @@ import org.thingsboard.server.actors.device.DeviceActorCreator;
29 29 import org.thingsboard.server.actors.ruleChain.RuleChainManagerActor;
30 30 import org.thingsboard.server.actors.service.ContextBasedCreator;
31 31 import org.thingsboard.server.actors.service.DefaultActorService;
  32 +import org.thingsboard.server.common.data.ApiUsageState;
32 33 import org.thingsboard.server.common.data.EntityType;
33 34 import org.thingsboard.server.common.data.Tenant;
34 35 import org.thingsboard.server.common.data.TenantProfile;
... ... @@ -62,6 +63,7 @@ public class TenantActor extends RuleChainManagerActor {
62 63
63 64 private boolean isRuleEngineForCurrentTenant;
64 65 private boolean isCore;
  66 + private ApiUsageState apiUsageState;
65 67
66 68 private TenantActor(ActorSystemContext systemContext, TenantId tenantId) {
67 69 super(systemContext, tenantId);
... ... @@ -79,21 +81,24 @@ public class TenantActor extends RuleChainManagerActor {
79 81 cantFindTenant = true;
80 82 log.info("[{}] Started tenant actor for missing tenant.", tenantId);
81 83 } else {
  84 + apiUsageState = new ApiUsageState(systemContext.getApiUsageStateService().getApiUsageState(tenant.getId()));
  85 +
82 86 // This Service may be started for specific tenant only.
83 87 Optional<TenantId> isolatedTenantId = systemContext.getServiceInfoProvider().getIsolatedTenant();
84 88
85   - // TODO: Tenant Profile from cache
86   -
87   - TenantProfile tenantProfile = systemContext.getTenantProfileService().findTenantProfileById(tenantId, tenant.getTenantProfileId());
  89 + TenantProfile tenantProfile = systemContext.getTenantProfileCache().get(tenant.getTenantProfileId());
88 90
89   - isRuleEngineForCurrentTenant = systemContext.getServiceInfoProvider().isService(ServiceType.TB_RULE_ENGINE);
90 91 isCore = systemContext.getServiceInfoProvider().isService(ServiceType.TB_CORE);
91   -
  92 + isRuleEngineForCurrentTenant = systemContext.getServiceInfoProvider().isService(ServiceType.TB_RULE_ENGINE);
92 93 if (isRuleEngineForCurrentTenant) {
93 94 try {
94 95 if (isolatedTenantId.map(id -> id.equals(tenantId)).orElseGet(() -> !tenantProfile.isIsolatedTbRuleEngine())) {
95   - log.info("[{}] Going to init rule chains", tenantId);
96   - initRuleChains();
  96 + if (apiUsageState.isReExecEnabled()) {
  97 + log.info("[{}] Going to init rule chains", tenantId);
  98 + initRuleChains();
  99 + } else {
  100 + log.info("[{}] Skip init of the rule chains due to API limits", tenantId);
  101 + }
97 102 } else {
98 103 isRuleEngineForCurrentTenant = false;
99 104 }
... ... @@ -105,8 +110,6 @@ public class TenantActor extends RuleChainManagerActor {
105 110 }
106 111 } catch (Exception e) {
107 112 log.warn("[{}] Unknown failure", tenantId, e);
108   -// TODO: throw this in 3.1?
109   -// throw new TbActorException("Failed to init actor", e);
110 113 }
111 114 }
112 115
... ... @@ -122,7 +125,7 @@ public class TenantActor extends RuleChainManagerActor {
122 125 if (msg.getMsgType().equals(MsgType.QUEUE_TO_RULE_ENGINE_MSG)) {
123 126 QueueToRuleEngineMsg queueMsg = (QueueToRuleEngineMsg) msg;
124 127 queueMsg.getTbMsg().getCallback().onSuccess();
125   - } else if (msg.getMsgType().equals(MsgType.TRANSPORT_TO_DEVICE_ACTOR_MSG)){
  128 + } else if (msg.getMsgType().equals(MsgType.TRANSPORT_TO_DEVICE_ACTOR_MSG)) {
126 129 TransportToDeviceActorMsgWrapper transportMsg = (TransportToDeviceActorMsgWrapper) msg;
127 130 transportMsg.getCallback().onSuccess();
128 131 }
... ... @@ -180,26 +183,33 @@ public class TenantActor extends RuleChainManagerActor {
180 183 return;
181 184 }
182 185 TbMsg tbMsg = msg.getTbMsg();
183   - if (tbMsg.getRuleChainId() == null) {
184   - if (getRootChainActor() != null) {
185   - getRootChainActor().tell(msg);
  186 + if (apiUsageState.isReExecEnabled()) {
  187 + if (tbMsg.getRuleChainId() == null) {
  188 + if (getRootChainActor() != null) {
  189 + getRootChainActor().tell(msg);
  190 + } else {
  191 + tbMsg.getCallback().onFailure(new RuleEngineException("No Root Rule Chain available!"));
  192 + log.info("[{}] No Root Chain: {}", tenantId, msg);
  193 + }
186 194 } else {
187   - tbMsg.getCallback().onFailure(new RuleEngineException("No Root Rule Chain available!"));
188   - log.info("[{}] No Root Chain: {}", tenantId, msg);
  195 + try {
  196 + ctx.tell(new TbEntityActorId(tbMsg.getRuleChainId()), msg);
  197 + } catch (TbActorNotRegisteredException ex) {
  198 + log.trace("Received message for non-existing rule chain: [{}]", tbMsg.getRuleChainId());
  199 + //TODO: 3.1 Log it to dead letters queue;
  200 + tbMsg.getCallback().onSuccess();
  201 + }
189 202 }
190 203 } else {
191   - try {
192   - ctx.tell(new TbEntityActorId(tbMsg.getRuleChainId()), msg);
193   - } catch (TbActorNotRegisteredException ex) {
194   - log.trace("Received message for non-existing rule chain: [{}]", tbMsg.getRuleChainId());
195   - //TODO: 3.1 Log it to dead letters queue;
196   - tbMsg.getCallback().onSuccess();
197   - }
  204 + log.trace("[{}] Ack message because Rule Engine is disabled", tenantId);
  205 + tbMsg.getCallback().onSuccess();
198 206 }
199 207 }
200 208
201 209 private void onRuleChainMsg(RuleChainAwareMsg msg) {
202   - getOrCreateActor(msg.getRuleChainId()).tell(msg);
  210 + if (apiUsageState.isReExecEnabled()) {
  211 + getOrCreateActor(msg.getRuleChainId()).tell(msg);
  212 + }
203 213 }
204 214
205 215 private void onToDeviceActorMsg(DeviceAwareMsg msg, boolean priority) {
... ... @@ -215,7 +225,17 @@ public class TenantActor extends RuleChainManagerActor {
215 225 }
216 226
217 227 private void onComponentLifecycleMsg(ComponentLifecycleMsg msg) {
218   - if (msg.getEntityId().getEntityType() == EntityType.EDGE) {
  228 + if (msg.getEntityId().getEntityType().equals(EntityType.API_USAGE_STATE)) {
  229 + ApiUsageState old = apiUsageState;
  230 + apiUsageState = new ApiUsageState(systemContext.getApiUsageStateService().getApiUsageState(tenantId));
  231 + if (old.isReExecEnabled() && !apiUsageState.isReExecEnabled()) {
  232 + log.info("[{}] Received API state update. Going to DISABLE Rule Engine execution.", tenantId);
  233 + destroyRuleChains();
  234 + } else if (!old.isReExecEnabled() && apiUsageState.isReExecEnabled()) {
  235 + log.info("[{}] Received API state update. Going to ENABLE Rule Engine execution.", tenantId);
  236 + initRuleChains();
  237 + }
  238 + } else if (msg.getEntityId().getEntityType() == EntityType.EDGE) {
219 239 EdgeId edgeId = new EdgeId(msg.getEntityId().getId());
220 240 EdgeRpcService edgeRpcService = systemContext.getEdgeRpcService();
221 241 if (msg.getEvent() == ComponentLifecycleEvent.DELETED) {
... ...
  1 +/**
  2 + * Copyright © 2016-2020 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.server.config;
  17 +
  18 +import lombok.extern.slf4j.Slf4j;
  19 +import org.springframework.beans.factory.annotation.Autowired;
  20 +import org.springframework.security.crypto.keygen.Base64StringKeyGenerator;
  21 +import org.springframework.security.crypto.keygen.StringKeyGenerator;
  22 +import org.springframework.security.oauth2.client.registration.ClientRegistration;
  23 +import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
  24 +import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestResolver;
  25 +import org.springframework.security.oauth2.core.AuthorizationGrantType;
  26 +import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
  27 +import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
  28 +import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
  29 +import org.springframework.security.oauth2.core.endpoint.PkceParameterNames;
  30 +import org.springframework.security.oauth2.core.oidc.OidcScopes;
  31 +import org.springframework.security.oauth2.core.oidc.endpoint.OidcParameterNames;
  32 +import org.springframework.security.web.util.UrlUtils;
  33 +import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
  34 +import org.springframework.stereotype.Service;
  35 +import org.springframework.util.CollectionUtils;
  36 +import org.springframework.util.StringUtils;
  37 +import org.springframework.web.util.UriComponents;
  38 +import org.springframework.web.util.UriComponentsBuilder;
  39 +import org.thingsboard.server.dao.oauth2.OAuth2Configuration;
  40 +import org.thingsboard.server.utils.MiscUtils;
  41 +
  42 +import javax.servlet.http.HttpServletRequest;
  43 +import java.nio.charset.StandardCharsets;
  44 +import java.security.MessageDigest;
  45 +import java.security.NoSuchAlgorithmException;
  46 +import java.util.Base64;
  47 +import java.util.HashMap;
  48 +import java.util.Map;
  49 +
  50 +@Service
  51 +@Slf4j
  52 +public class CustomOAuth2AuthorizationRequestResolver implements OAuth2AuthorizationRequestResolver {
  53 + public static final String DEFAULT_AUTHORIZATION_REQUEST_BASE_URI = "/oauth2/authorization";
  54 + public static final String DEFAULT_LOGIN_PROCESSING_URI = "/login/oauth2/code/";
  55 + private static final String REGISTRATION_ID_URI_VARIABLE_NAME = "registrationId";
  56 + private static final char PATH_DELIMITER = '/';
  57 +
  58 + private final AntPathRequestMatcher authorizationRequestMatcher = new AntPathRequestMatcher(
  59 + DEFAULT_AUTHORIZATION_REQUEST_BASE_URI + "/{" + REGISTRATION_ID_URI_VARIABLE_NAME + "}");
  60 + private final StringKeyGenerator stateGenerator = new Base64StringKeyGenerator(Base64.getUrlEncoder());
  61 + private final StringKeyGenerator secureKeyGenerator = new Base64StringKeyGenerator(Base64.getUrlEncoder().withoutPadding(), 96);
  62 +
  63 + @Autowired
  64 + private ClientRegistrationRepository clientRegistrationRepository;
  65 +
  66 + @Autowired(required = false)
  67 + private OAuth2Configuration oauth2Configuration;
  68 +
  69 +
  70 + @Override
  71 + public OAuth2AuthorizationRequest resolve(HttpServletRequest request) {
  72 + String registrationId = this.resolveRegistrationId(request);
  73 + String redirectUriAction = getAction(request, "login");
  74 + return resolve(request, registrationId, redirectUriAction);
  75 + }
  76 +
  77 + @Override
  78 + public OAuth2AuthorizationRequest resolve(HttpServletRequest request, String registrationId) {
  79 + if (registrationId == null) {
  80 + return null;
  81 + }
  82 + String redirectUriAction = getAction(request, "authorize");
  83 + return resolve(request, registrationId, redirectUriAction);
  84 + }
  85 +
  86 + private String getAction(HttpServletRequest request, String defaultAction) {
  87 + String action = request.getParameter("action");
  88 + if (action == null) {
  89 + return defaultAction;
  90 + }
  91 + return action;
  92 + }
  93 +
  94 + private OAuth2AuthorizationRequest resolve(HttpServletRequest request, String registrationId, String redirectUriAction) {
  95 + if (registrationId == null) {
  96 + return null;
  97 + }
  98 +
  99 + ClientRegistration clientRegistration = this.clientRegistrationRepository.findByRegistrationId(registrationId);
  100 + if (clientRegistration == null) {
  101 + throw new IllegalArgumentException("Invalid Client Registration with Id: " + registrationId);
  102 + }
  103 +
  104 + Map<String, Object> attributes = new HashMap<>();
  105 + attributes.put(OAuth2ParameterNames.REGISTRATION_ID, clientRegistration.getRegistrationId());
  106 +
  107 + OAuth2AuthorizationRequest.Builder builder;
  108 + if (AuthorizationGrantType.AUTHORIZATION_CODE.equals(clientRegistration.getAuthorizationGrantType())) {
  109 + builder = OAuth2AuthorizationRequest.authorizationCode();
  110 + Map<String, Object> additionalParameters = new HashMap<>();
  111 + if (!CollectionUtils.isEmpty(clientRegistration.getScopes()) &&
  112 + clientRegistration.getScopes().contains(OidcScopes.OPENID)) {
  113 + // Section 3.1.2.1 Authentication Request - https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest
  114 + // scope
  115 + // REQUIRED. OpenID Connect requests MUST contain the "openid" scope value.
  116 + addNonceParameters(attributes, additionalParameters);
  117 + }
  118 + if (ClientAuthenticationMethod.NONE.equals(clientRegistration.getClientAuthenticationMethod())) {
  119 + addPkceParameters(attributes, additionalParameters);
  120 + }
  121 + builder.additionalParameters(additionalParameters);
  122 + } else if (AuthorizationGrantType.IMPLICIT.equals(clientRegistration.getAuthorizationGrantType())) {
  123 + builder = OAuth2AuthorizationRequest.implicit();
  124 + } else {
  125 + throw new IllegalArgumentException("Invalid Authorization Grant Type (" +
  126 + clientRegistration.getAuthorizationGrantType().getValue() +
  127 + ") for Client Registration with Id: " + clientRegistration.getRegistrationId());
  128 + }
  129 +
  130 + String redirectUriStr = expandRedirectUri(request, clientRegistration, redirectUriAction);
  131 +
  132 + return builder
  133 + .clientId(clientRegistration.getClientId())
  134 + .authorizationUri(clientRegistration.getProviderDetails().getAuthorizationUri())
  135 + .redirectUri(redirectUriStr)
  136 + .scopes(clientRegistration.getScopes())
  137 + .state(this.stateGenerator.generateKey())
  138 + .attributes(attributes)
  139 + .build();
  140 + }
  141 +
  142 + private String resolveRegistrationId(HttpServletRequest request) {
  143 + if (this.authorizationRequestMatcher.matches(request)) {
  144 + return this.authorizationRequestMatcher
  145 + .matcher(request).getVariables().get(REGISTRATION_ID_URI_VARIABLE_NAME);
  146 + }
  147 + return null;
  148 + }
  149 +
  150 + /**
  151 + * Expands the {@link ClientRegistration#getRedirectUriTemplate()} with following provided variables:<br/>
  152 + * - baseUrl (e.g. https://localhost/app) <br/>
  153 + * - baseScheme (e.g. https) <br/>
  154 + * - baseHost (e.g. localhost) <br/>
  155 + * - basePort (e.g. :8080) <br/>
  156 + * - basePath (e.g. /app) <br/>
  157 + * - registrationId (e.g. google) <br/>
  158 + * - action (e.g. login) <br/>
  159 + * <p/>
  160 + * Null variables are provided as empty strings.
  161 + * <p/>
  162 + * Default redirectUriTemplate is: {@link org.springframework.security.config.oauth2.client}.CommonOAuth2Provider#DEFAULT_REDIRECT_URL
  163 + *
  164 + * @return expanded URI
  165 + */
  166 + private String expandRedirectUri(HttpServletRequest request, ClientRegistration clientRegistration, String action) {
  167 + Map<String, String> uriVariables = new HashMap<>();
  168 + uriVariables.put("registrationId", clientRegistration.getRegistrationId());
  169 +
  170 + UriComponents uriComponents = UriComponentsBuilder.fromHttpUrl(UrlUtils.buildFullRequestUrl(request))
  171 + .replacePath(request.getContextPath())
  172 + .replaceQuery(null)
  173 + .fragment(null)
  174 + .build();
  175 + String scheme = uriComponents.getScheme();
  176 + uriVariables.put("baseScheme", scheme == null ? "" : scheme);
  177 + String host = uriComponents.getHost();
  178 + uriVariables.put("baseHost", host == null ? "" : host);
  179 + // following logic is based on HierarchicalUriComponents#toUriString()
  180 + int port = uriComponents.getPort();
  181 + uriVariables.put("basePort", port == -1 ? "" : ":" + port);
  182 + String path = uriComponents.getPath();
  183 + if (StringUtils.hasLength(path)) {
  184 + if (path.charAt(0) != PATH_DELIMITER) {
  185 + path = PATH_DELIMITER + path;
  186 + }
  187 + }
  188 + uriVariables.put("basePath", path == null ? "" : path);
  189 + uriVariables.put("baseUrl", uriComponents.toUriString());
  190 +
  191 + uriVariables.put("action", action == null ? "" : action);
  192 +
  193 + String redirectUri = getRedirectUri(request);
  194 + log.trace("Redirect URI - {}.", redirectUri);
  195 +
  196 + return UriComponentsBuilder.fromUriString(redirectUri)
  197 + .buildAndExpand(uriVariables)
  198 + .toUriString();
  199 + }
  200 +
  201 + private String getRedirectUri(HttpServletRequest request) {
  202 + String loginProcessingUri = oauth2Configuration != null ? oauth2Configuration.getLoginProcessingUrl() : DEFAULT_LOGIN_PROCESSING_URI;
  203 +
  204 + String scheme = MiscUtils.getScheme(request);
  205 + String domainName = MiscUtils.getDomainName(request);
  206 + int port = MiscUtils.getPort(request);
  207 + String baseUrl = scheme + "://" + domainName;
  208 + if (needsPort(scheme, port)){
  209 + baseUrl += ":" + port;
  210 + }
  211 + return baseUrl + loginProcessingUri;
  212 + }
  213 +
  214 + private boolean needsPort(String scheme, int port) {
  215 + boolean isHttpDefault = "http".equals(scheme.toLowerCase()) && port == 80;
  216 + boolean isHttpsDefault = "https".equals(scheme.toLowerCase()) && port == 443;
  217 + return !isHttpDefault && !isHttpsDefault;
  218 + }
  219 +
  220 + /**
  221 + * Creates nonce and its hash for use in OpenID Connect 1.0 Authentication Requests.
  222 + *
  223 + * @param attributes where the {@link OidcParameterNames#NONCE} is stored for the authentication request
  224 + * @param additionalParameters where the {@link OidcParameterNames#NONCE} hash is added for the authentication request
  225 + *
  226 + * @since 5.2
  227 + * @see <a target="_blank" href="https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest">3.1.2.1. Authentication Request</a>
  228 + */
  229 + private void addNonceParameters(Map<String, Object> attributes, Map<String, Object> additionalParameters) {
  230 + try {
  231 + String nonce = this.secureKeyGenerator.generateKey();
  232 + String nonceHash = createHash(nonce);
  233 + attributes.put(OidcParameterNames.NONCE, nonce);
  234 + additionalParameters.put(OidcParameterNames.NONCE, nonceHash);
  235 + } catch (NoSuchAlgorithmException e) { }
  236 + }
  237 +
  238 + /**
  239 + * Creates and adds additional PKCE parameters for use in the OAuth 2.0 Authorization and Access Token Requests
  240 + *
  241 + * @param attributes where {@link PkceParameterNames#CODE_VERIFIER} is stored for the token request
  242 + * @param additionalParameters where {@link PkceParameterNames#CODE_CHALLENGE} and, usually,
  243 + * {@link PkceParameterNames#CODE_CHALLENGE_METHOD} are added to be used in the authorization request.
  244 + *
  245 + * @since 5.2
  246 + * @see <a target="_blank" href="https://tools.ietf.org/html/rfc7636#section-1.1">1.1. Protocol Flow</a>
  247 + * @see <a target="_blank" href="https://tools.ietf.org/html/rfc7636#section-4.1">4.1. Client Creates a Code Verifier</a>
  248 + * @see <a target="_blank" href="https://tools.ietf.org/html/rfc7636#section-4.2">4.2. Client Creates the Code Challenge</a>
  249 + */
  250 + private void addPkceParameters(Map<String, Object> attributes, Map<String, Object> additionalParameters) {
  251 + String codeVerifier = this.secureKeyGenerator.generateKey();
  252 + attributes.put(PkceParameterNames.CODE_VERIFIER, codeVerifier);
  253 + try {
  254 + String codeChallenge = createHash(codeVerifier);
  255 + additionalParameters.put(PkceParameterNames.CODE_CHALLENGE, codeChallenge);
  256 + additionalParameters.put(PkceParameterNames.CODE_CHALLENGE_METHOD, "S256");
  257 + } catch (NoSuchAlgorithmException e) {
  258 + additionalParameters.put(PkceParameterNames.CODE_CHALLENGE, codeVerifier);
  259 + }
  260 + }
  261 +
  262 + private static String createHash(String value) throws NoSuchAlgorithmException {
  263 + MessageDigest md = MessageDigest.getInstance("SHA-256");
  264 + byte[] digest = md.digest(value.getBytes(StandardCharsets.US_ASCII));
  265 + return Base64.getUrlEncoder().withoutPadding().encodeToString(digest);
  266 + }
  267 +}
... ...
... ... @@ -32,6 +32,7 @@ import org.springframework.security.config.annotation.web.configuration.EnableWe
32 32 import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
33 33 import org.springframework.security.config.http.SessionCreationPolicy;
34 34 import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
  35 +import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestResolver;
35 36 import org.springframework.security.web.authentication.AuthenticationFailureHandler;
36 37 import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
37 38 import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
... ... @@ -175,6 +176,9 @@ public class ThingsboardSecurityConfiguration extends WebSecurityConfigurerAdapt
175 176 web.ignoring().antMatchers("/*.js","/*.css","/*.ico","/assets/**","/static/**");
176 177 }
177 178
  179 + @Autowired
  180 + private OAuth2AuthorizationRequestResolver oAuth2AuthorizationRequestResolver;
  181 +
178 182 @Override
179 183 protected void configure(HttpSecurity http) throws Exception {
180 184 http.headers().cacheControl().and().frameOptions().disable()
... ... @@ -207,8 +211,10 @@ public class ThingsboardSecurityConfiguration extends WebSecurityConfigurerAdapt
207 211 .addFilterBefore(buildRefreshTokenProcessingFilter(), UsernamePasswordAuthenticationFilter.class)
208 212 .addFilterBefore(buildWsJwtTokenAuthenticationProcessingFilter(), UsernamePasswordAuthenticationFilter.class)
209 213 .addFilterAfter(rateLimitProcessingFilter, UsernamePasswordAuthenticationFilter.class);
210   - if (oauth2Configuration != null && oauth2Configuration.isEnabled()) {
  214 + if (oauth2Configuration != null) {
211 215 http.oauth2Login()
  216 + .authorizationEndpoint().authorizationRequestResolver(oAuth2AuthorizationRequestResolver)
  217 + .and()
212 218 .loginPage("/oauth2Login")
213 219 .loginProcessingUrl(oauth2Configuration.getLoginProcessingUrl())
214 220 .successHandler(oauth2AuthenticationSuccessHandler)
... ...
... ... @@ -25,6 +25,8 @@ import org.springframework.web.bind.annotation.RequestMethod;
25 25 import org.springframework.web.bind.annotation.ResponseBody;
26 26 import org.springframework.web.bind.annotation.RestController;
27 27 import org.thingsboard.rule.engine.api.MailService;
  28 +import org.thingsboard.rule.engine.api.SmsService;
  29 +import org.thingsboard.server.common.data.sms.config.TestSmsRequest;
28 30 import org.thingsboard.server.common.data.AdminSettings;
29 31 import org.thingsboard.server.common.data.UpdateMessage;
30 32 import org.thingsboard.server.common.data.exception.ThingsboardException;
... ... @@ -46,6 +48,9 @@ public class AdminController extends BaseController {
46 48 private MailService mailService;
47 49
48 50 @Autowired
  51 + private SmsService smsService;
  52 +
  53 + @Autowired
49 54 private AdminSettingsService adminSettingsService;
50 55
51 56 @Autowired
... ... @@ -80,6 +85,8 @@ public class AdminController extends BaseController {
80 85 if (adminSettings.getKey().equals("mail")) {
81 86 mailService.updateMailConfiguration();
82 87 ((ObjectNode) adminSettings.getJsonValue()).put("password", "");
  88 + } else if (adminSettings.getKey().equals("sms")) {
  89 + smsService.updateSmsConfiguration();
83 90 }
84 91 return adminSettings;
85 92 } catch (Exception e) {
... ... @@ -128,6 +135,17 @@ public class AdminController extends BaseController {
128 135 }
129 136
130 137 @PreAuthorize("hasAuthority('SYS_ADMIN')")
  138 + @RequestMapping(value = "/settings/testSms", method = RequestMethod.POST)
  139 + public void sendTestSms(@RequestBody TestSmsRequest testSmsRequest) throws ThingsboardException {
  140 + try {
  141 + accessControlService.checkPermission(getCurrentUser(), Resource.ADMIN_SETTINGS, Operation.READ);
  142 + smsService.sendTestSms(testSmsRequest);
  143 + } catch (Exception e) {
  144 + throw handleException(e);
  145 + }
  146 + }
  147 +
  148 + @PreAuthorize("hasAuthority('SYS_ADMIN')")
131 149 @RequestMapping(value = "/updates", method = RequestMethod.GET)
132 150 @ResponseBody
133 151 public UpdateMessage checkUpdates() throws ThingsboardException {
... ...
... ... @@ -133,6 +133,7 @@ public class AlarmController extends BaseController {
133 133 long ackTs = System.currentTimeMillis();
134 134 alarmService.ackAlarm(getCurrentUser().getTenantId(), alarmId, ackTs).get();
135 135 alarm.setAckTs(ackTs);
  136 + alarm.setStatus(alarm.getStatus().isCleared() ? AlarmStatus.CLEARED_ACK : AlarmStatus.ACTIVE_ACK);
136 137 logEntityAction(alarm.getOriginator(), alarm, getCurrentUser().getCustomerId(), ActionType.ALARM_ACK, null);
137 138
138 139 sendNotificationMsgToEdgeService(getTenantId(), alarmId, ActionType.ALARM_ACK);
... ... @@ -152,6 +153,7 @@ public class AlarmController extends BaseController {
152 153 long clearTs = System.currentTimeMillis();
153 154 alarmService.clearAlarm(getCurrentUser().getTenantId(), alarmId, null, clearTs).get();
154 155 alarm.setClearTs(clearTs);
  156 + alarm.setStatus(alarm.getStatus().isAck() ? AlarmStatus.CLEARED_ACK : AlarmStatus.CLEARED_UNACK);
155 157 logEntityAction(alarm.getOriginator(), alarm, getCurrentUser().getCustomerId(), ActionType.ALARM_CLEAR, null);
156 158
157 159 sendNotificationMsgToEdgeService(getTenantId(), alarmId, ActionType.ALARM_CLEAR);
... ...
... ... @@ -36,6 +36,7 @@ import org.thingsboard.server.common.data.asset.AssetSearchQuery;
36 36 import org.thingsboard.server.common.data.audit.ActionType;
37 37 import org.thingsboard.server.common.data.edge.Edge;
38 38 import org.thingsboard.server.common.data.edge.EdgeEventType;
  39 +import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
39 40 import org.thingsboard.server.common.data.exception.ThingsboardException;
40 41 import org.thingsboard.server.common.data.id.AssetId;
41 42 import org.thingsboard.server.common.data.id.CustomerId;
... ... @@ -55,6 +56,8 @@ import java.util.ArrayList;
55 56 import java.util.List;
56 57 import java.util.stream.Collectors;
57 58
  59 +import static org.thingsboard.server.dao.asset.BaseAssetService.TB_SERVICE_QUEUE;
  60 +
58 61 import static org.thingsboard.server.controller.EdgeController.EDGE_ID;
59 62
60 63 @RestController
... ... @@ -95,6 +98,10 @@ public class AssetController extends BaseController {
95 98 @ResponseBody
96 99 public Asset saveAsset(@RequestBody Asset asset) throws ThingsboardException {
97 100 try {
  101 + if (TB_SERVICE_QUEUE.equals(asset.getType())) {
  102 + throw new ThingsboardException("Unable to save asset with type " + TB_SERVICE_QUEUE, ThingsboardErrorCode.BAD_REQUEST_PARAMS);
  103 + }
  104 +
98 105 asset.setTenantId(getCurrentUser().getTenantId());
99 106
100 107 checkEntity(asset.getId(), asset, Resource.ASSET);
... ...
... ... @@ -39,10 +39,8 @@ import org.thingsboard.server.common.data.edge.EdgeEventType;
39 39 import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
40 40 import org.thingsboard.server.common.data.exception.ThingsboardException;
41 41 import org.thingsboard.server.common.data.id.TenantId;
42   -import org.thingsboard.server.common.data.oauth2.OAuth2ClientInfo;
43 42 import org.thingsboard.server.common.data.security.UserCredentials;
44 43 import org.thingsboard.server.dao.audit.AuditLogService;
45   -import org.thingsboard.server.dao.oauth2.OAuth2Service;
46 44 import org.thingsboard.server.queue.util.TbCoreComponent;
47 45 import org.thingsboard.server.service.security.auth.jwt.RefreshTokenRepository;
48 46 import org.thingsboard.server.service.security.auth.rest.RestAuthenticationDetails;
... ... @@ -85,9 +83,6 @@ public class AuthController extends BaseController {
85 83 @Autowired
86 84 private AuditLogService auditLogService;
87 85
88   - @Autowired
89   - private OAuth2Service oauth2Service;
90   -
91 86 @PreAuthorize("isAuthenticated()")
92 87 @RequestMapping(value = "/auth/user", method = RequestMethod.GET)
93 88 public @ResponseBody User getUser() throws ThingsboardException {
... ... @@ -175,7 +170,8 @@ public class AuthController extends BaseController {
175 170 try {
176 171 String email = resetPasswordByEmailRequest.get("email").asText();
177 172 UserCredentials userCredentials = userService.requestPasswordReset(TenantId.SYS_TENANT_ID, email);
178   - String baseUrl = MiscUtils.constructBaseUrl(request);
  173 + User user = userService.findUserById(TenantId.SYS_TENANT_ID, userCredentials.getUserId());
  174 + String baseUrl = systemSecurityService.getBaseUrl(user.getTenantId(), user.getCustomerId(), request);
179 175 String resetUrl = String.format("%s/api/noauth/resetPassword?resetToken=%s", baseUrl,
180 176 userCredentials.getResetToken());
181 177
... ... @@ -223,7 +219,7 @@ public class AuthController extends BaseController {
223 219 User user = userService.findUserById(TenantId.SYS_TENANT_ID, credentials.getUserId());
224 220 UserPrincipal principal = new UserPrincipal(UserPrincipal.Type.USER_NAME, user.getEmail());
225 221 SecurityUser securityUser = new SecurityUser(user, credentials.isEnabled(), principal);
226   - String baseUrl = MiscUtils.constructBaseUrl(request);
  222 + String baseUrl = systemSecurityService.getBaseUrl(user.getTenantId(), user.getCustomerId(), request);
227 223 String loginUrl = String.format("%s/login", baseUrl);
228 224 String email = user.getEmail();
229 225
... ... @@ -272,7 +268,7 @@ public class AuthController extends BaseController {
272 268 User user = userService.findUserById(TenantId.SYS_TENANT_ID, userCredentials.getUserId());
273 269 UserPrincipal principal = new UserPrincipal(UserPrincipal.Type.USER_NAME, user.getEmail());
274 270 SecurityUser securityUser = new SecurityUser(user, userCredentials.isEnabled(), principal);
275   - String baseUrl = MiscUtils.constructBaseUrl(request);
  271 + String baseUrl = systemSecurityService.getBaseUrl(user.getTenantId(), user.getCustomerId(), request);
276 272 String loginUrl = String.format("%s/login", baseUrl);
277 273 String email = user.getEmail();
278 274 mailService.sendPasswordWasResetEmail(loginUrl, email);
... ... @@ -342,14 +338,4 @@ public class AuthController extends BaseController {
342 338 throw handleException(e);
343 339 }
344 340 }
345   -
346   - @RequestMapping(value = "/noauth/oauth2Clients", method = RequestMethod.POST)
347   - @ResponseBody
348   - public List<OAuth2ClientInfo> getOAuth2Clients() throws ThingsboardException {
349   - try {
350   - return oauth2Service.getOAuth2Clients();
351   - } catch (Exception e) {
352   - throw handleException(e);
353   - }
354   - }
355 341 }
... ...
... ... @@ -100,6 +100,8 @@ import org.thingsboard.server.dao.entityview.EntityViewService;
100 100 import org.thingsboard.server.dao.exception.DataValidationException;
101 101 import org.thingsboard.server.dao.exception.IncorrectParameterException;
102 102 import org.thingsboard.server.dao.model.ModelConstants;
  103 +import org.thingsboard.server.dao.oauth2.OAuth2ConfigTemplateService;
  104 +import org.thingsboard.server.dao.oauth2.OAuth2Service;
103 105 import org.thingsboard.server.dao.relation.RelationService;
104 106 import org.thingsboard.server.dao.rule.RuleChainService;
105 107 import org.thingsboard.server.dao.tenant.TenantProfileService;
... ... @@ -117,6 +119,7 @@ import org.thingsboard.server.service.edge.EdgeNotificationService;
117 119 import org.thingsboard.server.service.edge.rpc.EdgeGrpcService;
118 120 import org.thingsboard.server.service.edge.rpc.init.SyncEdgeService;
119 121 import org.thingsboard.server.service.profile.TbDeviceProfileCache;
  122 +import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
120 123 import org.thingsboard.server.service.queue.TbClusterService;
121 124 import org.thingsboard.server.service.security.model.SecurityUser;
122 125 import org.thingsboard.server.service.security.permission.AccessControlService;
... ... @@ -187,6 +190,12 @@ public abstract class BaseController {
187 190 protected DashboardService dashboardService;
188 191
189 192 @Autowired
  193 + protected OAuth2Service oAuth2Service;
  194 +
  195 + @Autowired
  196 + protected OAuth2ConfigTemplateService oAuth2ConfigTemplateService;
  197 +
  198 + @Autowired
190 199 protected ComponentDiscoveryService componentDescriptorService;
191 200
192 201 @Autowired
... ... @@ -223,6 +232,9 @@ public abstract class BaseController {
223 232 protected TbQueueProducerProvider producerProvider;
224 233
225 234 @Autowired
  235 + protected TbTenantProfileCache tenantProfileCache;
  236 +
  237 + @Autowired
226 238 protected TbDeviceProfileCache deviceProfileCache;
227 239
228 240 @Autowired(required = false)
... ... @@ -747,6 +759,12 @@ public abstract class BaseController {
747 759 case ASSIGNED_TO_TENANT:
748 760 msgType = DataConstants.ENTITY_ASSIGNED_TO_TENANT;
749 761 break;
  762 + case PROVISION_SUCCESS:
  763 + msgType = DataConstants.PROVISION_SUCCESS;
  764 + break;
  765 + case PROVISION_FAILURE:
  766 + msgType = DataConstants.PROVISION_FAILURE;
  767 + break;
750 768 case ASSIGNED_TO_EDGE:
751 769 msgType = DataConstants.ENTITY_ASSIGNED_TO_EDGE;
752 770 break;
... ...
... ... @@ -54,6 +54,7 @@ import org.thingsboard.server.common.data.id.TenantId;
54 54 import org.thingsboard.server.common.data.page.PageData;
55 55 import org.thingsboard.server.common.data.page.PageLink;
56 56 import org.thingsboard.server.common.data.page.TimePageLink;
  57 +import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
57 58 import org.thingsboard.server.common.data.security.DeviceCredentials;
58 59 import org.thingsboard.server.common.msg.TbMsg;
59 60 import org.thingsboard.server.common.msg.TbMsgDataType;
... ... @@ -122,8 +123,11 @@ public class DeviceController extends BaseController {
122 123
123 124 Device savedDevice = checkNotNull(deviceService.saveDeviceWithAccessToken(device, accessToken));
124 125
  126 + tbClusterService.onDeviceChange(savedDevice, null);
125 127 tbClusterService.pushMsgToCore(new DeviceNameOrTypeUpdateMsg(savedDevice.getTenantId(),
126 128 savedDevice.getId(), savedDevice.getName(), savedDevice.getType()), null);
  129 + tbClusterService.onEntityStateChange(savedDevice.getTenantId(), savedDevice.getId(),
  130 + device.getId() == null ? ComponentLifecycleEvent.CREATED : ComponentLifecycleEvent.UPDATED);
127 131
128 132 if (device.getId() != null) {
129 133 sendNotificationMsgToEdgeService(savedDevice.getTenantId(), savedDevice.getId(), ActionType.UPDATED);
... ... @@ -156,6 +160,9 @@ public class DeviceController extends BaseController {
156 160 Device device = checkDeviceId(deviceId, Operation.DELETE);
157 161 deviceService.deleteDevice(getCurrentUser().getTenantId(), deviceId);
158 162
  163 + tbClusterService.onDeviceDeleted(device, null);
  164 + tbClusterService.onEntityStateChange(device.getTenantId(), deviceId, ComponentLifecycleEvent.DELETED);
  165 +
159 166 logEntityAction(deviceId, device,
160 167 device.getCustomerId(),
161 168 ActionType.DELETED, null, strDeviceId);
... ...
... ... @@ -94,7 +94,6 @@ public class DeviceProfileController extends BaseController {
94 94
95 95 DeviceProfile savedDeviceProfile = checkNotNull(deviceProfileService.saveDeviceProfile(deviceProfile));
96 96
97   - deviceProfileCache.put(savedDeviceProfile);
98 97 tbClusterService.onDeviceProfileChange(savedDeviceProfile, null);
99 98 tbClusterService.onEntityStateChange(deviceProfile.getTenantId(), savedDeviceProfile.getId(),
100 99 created ? ComponentLifecycleEvent.CREATED : ComponentLifecycleEvent.UPDATED);
... ... @@ -120,7 +119,6 @@ public class DeviceProfileController extends BaseController {
120 119 DeviceProfileId deviceProfileId = new DeviceProfileId(toUUID(strDeviceProfileId));
121 120 DeviceProfile deviceProfile = checkDeviceProfileId(deviceProfileId, Operation.DELETE);
122 121 deviceProfileService.deleteDeviceProfile(getTenantId(), deviceProfileId);
123   - deviceProfileCache.evict(deviceProfileId);
124 122
125 123 tbClusterService.onDeviceProfileDelete(deviceProfile, null);
126 124 tbClusterService.onEntityStateChange(deviceProfile.getTenantId(), deviceProfile.getId(), ComponentLifecycleEvent.DELETED);
... ... @@ -192,10 +190,11 @@ public class DeviceProfileController extends BaseController {
192 190 @RequestParam int page,
193 191 @RequestParam(required = false) String textSearch,
194 192 @RequestParam(required = false) String sortProperty,
195   - @RequestParam(required = false) String sortOrder) throws ThingsboardException {
  193 + @RequestParam(required = false) String sortOrder,
  194 + @RequestParam(required = false) String transportType) throws ThingsboardException {
196 195 try {
197 196 PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder);
198   - return checkNotNull(deviceProfileService.findDeviceProfileInfos(getTenantId(), pageLink));
  197 + return checkNotNull(deviceProfileService.findDeviceProfileInfos(getTenantId(), pageLink, transportType));
199 198 } catch (Exception e) {
200 199 throw handleException(e);
201 200 }
... ...
  1 +/**
  2 + * Copyright © 2016-2020 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.server.controller;
  17 +
  18 +import lombok.extern.slf4j.Slf4j;
  19 +import org.springframework.http.HttpStatus;
  20 +import org.springframework.security.access.prepost.PreAuthorize;
  21 +import org.springframework.web.bind.annotation.*;
  22 +import org.thingsboard.server.common.data.EntityType;
  23 +import org.thingsboard.server.common.data.audit.ActionType;
  24 +import org.thingsboard.server.common.data.exception.ThingsboardException;
  25 +import org.thingsboard.server.common.data.id.OAuth2ClientRegistrationTemplateId;
  26 +import org.thingsboard.server.common.data.oauth2.OAuth2ClientRegistrationTemplate;
  27 +import org.thingsboard.server.queue.util.TbCoreComponent;
  28 +import org.thingsboard.server.service.security.permission.Operation;
  29 +import org.thingsboard.server.service.security.permission.Resource;
  30 +
  31 +import java.util.List;
  32 +
  33 +@RestController
  34 +@TbCoreComponent
  35 +@RequestMapping("/api/oauth2/config/template")
  36 +@Slf4j
  37 +public class OAuth2ConfigTemplateController extends BaseController {
  38 + private static final String CLIENT_REGISTRATION_TEMPLATE_ID = "clientRegistrationTemplateId";
  39 +
  40 + @PreAuthorize("hasAnyAuthority('SYS_ADMIN')")
  41 + @RequestMapping(method = RequestMethod.POST)
  42 + @ResponseStatus(value = HttpStatus.OK)
  43 + public OAuth2ClientRegistrationTemplate saveClientRegistrationTemplate(@RequestBody OAuth2ClientRegistrationTemplate clientRegistrationTemplate) throws ThingsboardException {
  44 + try {
  45 + accessControlService.checkPermission(getCurrentUser(), Resource.OAUTH2_CONFIGURATION_TEMPLATE, Operation.WRITE);
  46 + return oAuth2ConfigTemplateService.saveClientRegistrationTemplate(clientRegistrationTemplate);
  47 + } catch (Exception e) {
  48 + throw handleException(e);
  49 + }
  50 + }
  51 +
  52 + @PreAuthorize("hasAnyAuthority('SYS_ADMIN')")
  53 + @RequestMapping(value = "/{clientRegistrationTemplateId}", method = RequestMethod.DELETE)
  54 + @ResponseStatus(value = HttpStatus.OK)
  55 + public void deleteClientRegistrationTemplate(@PathVariable(CLIENT_REGISTRATION_TEMPLATE_ID) String strClientRegistrationTemplateId) throws ThingsboardException {
  56 + checkParameter(CLIENT_REGISTRATION_TEMPLATE_ID, strClientRegistrationTemplateId);
  57 + try {
  58 + accessControlService.checkPermission(getCurrentUser(), Resource.OAUTH2_CONFIGURATION_TEMPLATE, Operation.DELETE);
  59 + OAuth2ClientRegistrationTemplateId clientRegistrationTemplateId = new OAuth2ClientRegistrationTemplateId(toUUID(strClientRegistrationTemplateId));
  60 + oAuth2ConfigTemplateService.deleteClientRegistrationTemplateById(clientRegistrationTemplateId);
  61 + } catch (Exception e) {
  62 + throw handleException(e);
  63 + }
  64 + }
  65 +
  66 + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
  67 + @RequestMapping(method = RequestMethod.GET, produces = "application/json")
  68 + @ResponseBody
  69 + public List<OAuth2ClientRegistrationTemplate> getClientRegistrationTemplates() throws ThingsboardException {
  70 + try {
  71 + accessControlService.checkPermission(getCurrentUser(), Resource.OAUTH2_CONFIGURATION_TEMPLATE, Operation.READ);
  72 + return oAuth2ConfigTemplateService.findAllClientRegistrationTemplates();
  73 + } catch (Exception e) {
  74 + throw handleException(e);
  75 + }
  76 + }
  77 +}
... ...
  1 +/**
  2 + * Copyright © 2016-2020 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.server.controller;
  17 +
  18 +import lombok.extern.slf4j.Slf4j;
  19 +import org.springframework.beans.factory.annotation.Autowired;
  20 +import org.springframework.http.HttpStatus;
  21 +import org.springframework.security.access.prepost.PreAuthorize;
  22 +import org.springframework.web.bind.annotation.*;
  23 +import org.thingsboard.server.common.data.exception.ThingsboardException;
  24 +import org.thingsboard.server.common.data.oauth2.OAuth2ClientInfo;
  25 +import org.thingsboard.server.common.data.oauth2.OAuth2ClientsParams;
  26 +import org.thingsboard.server.common.data.oauth2.SchemeType;
  27 +import org.thingsboard.server.dao.oauth2.OAuth2Configuration;
  28 +import org.thingsboard.server.queue.util.TbCoreComponent;
  29 +import org.thingsboard.server.service.security.permission.Operation;
  30 +import org.thingsboard.server.service.security.permission.Resource;
  31 +import org.thingsboard.server.utils.MiscUtils;
  32 +
  33 +import javax.servlet.http.HttpServletRequest;
  34 +import java.util.List;
  35 +
  36 +@RestController
  37 +@TbCoreComponent
  38 +@RequestMapping("/api")
  39 +@Slf4j
  40 +public class OAuth2Controller extends BaseController {
  41 +
  42 + @Autowired
  43 + private OAuth2Configuration oAuth2Configuration;
  44 +
  45 + @RequestMapping(value = "/noauth/oauth2Clients", method = RequestMethod.POST)
  46 + @ResponseBody
  47 + public List<OAuth2ClientInfo> getOAuth2Clients(HttpServletRequest request) throws ThingsboardException {
  48 + try {
  49 + return oAuth2Service.getOAuth2Clients(MiscUtils.getScheme(request), MiscUtils.getDomainNameAndPort(request));
  50 + } catch (Exception e) {
  51 + throw handleException(e);
  52 + }
  53 + }
  54 +
  55 + @PreAuthorize("hasAnyAuthority('SYS_ADMIN')")
  56 + @RequestMapping(value = "/oauth2/config", method = RequestMethod.GET, produces = "application/json")
  57 + @ResponseBody
  58 + public OAuth2ClientsParams getCurrentOAuth2Params() throws ThingsboardException {
  59 + try {
  60 + accessControlService.checkPermission(getCurrentUser(), Resource.OAUTH2_CONFIGURATION_INFO, Operation.READ);
  61 + return oAuth2Service.findOAuth2Params();
  62 + } catch (Exception e) {
  63 + throw handleException(e);
  64 + }
  65 + }
  66 +
  67 + @PreAuthorize("hasAnyAuthority('SYS_ADMIN')")
  68 + @RequestMapping(value = "/oauth2/config", method = RequestMethod.POST)
  69 + @ResponseStatus(value = HttpStatus.OK)
  70 + public OAuth2ClientsParams saveOAuth2Params(@RequestBody OAuth2ClientsParams oauth2Params) throws ThingsboardException {
  71 + try {
  72 + accessControlService.checkPermission(getCurrentUser(), Resource.OAUTH2_CONFIGURATION_INFO, Operation.WRITE);
  73 + oAuth2Service.saveOAuth2Params(oauth2Params);
  74 + return oAuth2Service.findOAuth2Params();
  75 + } catch (Exception e) {
  76 + throw handleException(e);
  77 + }
  78 + }
  79 +
  80 + @PreAuthorize("hasAnyAuthority('SYS_ADMIN')")
  81 + @RequestMapping(value = "/oauth2/loginProcessingUrl", method = RequestMethod.GET)
  82 + @ResponseBody
  83 + public String getLoginProcessingUrl() throws ThingsboardException {
  84 + try {
  85 + accessControlService.checkPermission(getCurrentUser(), Resource.OAUTH2_CONFIGURATION_INFO, Operation.READ);
  86 + return "\"" + oAuth2Configuration.getLoginProcessingUrl() + "\"";
  87 + } catch (Exception e) {
  88 + throw handleException(e);
  89 + }
  90 + }
  91 +}
... ...
... ... @@ -173,10 +173,12 @@ public class RuleChainController extends BaseController {
173 173 public RuleChain saveRuleChain(@RequestBody DefaultRuleChainCreateRequest request) throws ThingsboardException {
174 174 try {
175 175 checkNotNull(request);
176   - checkNotNull(request.getName());
  176 + checkParameter(request.getName(), "name");
177 177
178 178 RuleChain savedRuleChain = installScripts.createDefaultRuleChain(getCurrentUser().getTenantId(), request.getName());
179 179
  180 + tbClusterService.onEntityStateChange(savedRuleChain.getTenantId(), savedRuleChain.getId(), ComponentLifecycleEvent.CREATED);
  181 +
180 182 logEntityAction(savedRuleChain.getId(), savedRuleChain, null, ActionType.ADDED, null);
181 183
182 184 return savedRuleChain;
... ... @@ -377,7 +379,7 @@ public class RuleChainController extends BaseController {
377 379 String errorText = "";
378 380 ScriptEngine engine = null;
379 381 try {
380   - engine = new RuleNodeJsScriptEngine(jsInvokeService, getCurrentUser().getId(), script, argNames);
  382 + engine = new RuleNodeJsScriptEngine(getTenantId(), jsInvokeService, getCurrentUser().getId(), script, argNames);
381 383 TbMsg inMsg = TbMsg.newMsg(msgType, null, new TbMsgMetaData(metadata), TbMsgDataType.JSON, data);
382 384 switch (scriptType) {
383 385 case "update":
... ...
... ... @@ -45,6 +45,7 @@ import org.thingsboard.common.util.ThingsBoardThreadFactory;
45 45 import org.thingsboard.rule.engine.api.msg.DeviceAttributesEventNotificationMsg;
46 46 import org.thingsboard.server.common.data.DataConstants;
47 47 import org.thingsboard.server.common.data.EntityType;
  48 +import org.thingsboard.server.common.data.TenantProfile;
48 49 import org.thingsboard.server.common.data.audit.ActionType;
49 50 import org.thingsboard.server.common.data.exception.ThingsboardException;
50 51 import org.thingsboard.server.common.data.id.DeviceId;
... ... @@ -69,6 +70,7 @@ import org.thingsboard.server.common.data.kv.LongDataEntry;
69 70 import org.thingsboard.server.common.data.kv.ReadTsKvQuery;
70 71 import org.thingsboard.server.common.data.kv.StringDataEntry;
71 72 import org.thingsboard.server.common.data.kv.TsKvEntry;
  73 +import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration;
72 74 import org.thingsboard.server.common.transport.adaptor.JsonConverter;
73 75 import org.thingsboard.server.dao.timeseries.TimeseriesService;
74 76 import org.thingsboard.server.queue.util.TbCoreComponent;
... ... @@ -93,6 +95,7 @@ import java.util.Map;
93 95 import java.util.Set;
94 96 import java.util.concurrent.ExecutorService;
95 97 import java.util.concurrent.Executors;
  98 +import java.util.concurrent.TimeUnit;
96 99 import java.util.stream.Collectors;
97 100
98 101 /**
... ... @@ -205,7 +208,7 @@ public class TelemetryController extends BaseController {
205 208 @RequestParam(name = "interval", defaultValue = "0") Long interval,
206 209 @RequestParam(name = "limit", defaultValue = "100") Integer limit,
207 210 @RequestParam(name = "agg", defaultValue = "NONE") String aggStr,
208   - @RequestParam(name= "orderBy", defaultValue = "DESC") String orderBy,
  211 + @RequestParam(name = "orderBy", defaultValue = "DESC") String orderBy,
209 212 @RequestParam(name = "useStrictDataTypes", required = false, defaultValue = "false") Boolean useStrictDataTypes) throws ThingsboardException {
210 213 return accessValidator.validateEntityAndCallback(getCurrentUser(), Operation.READ_TELEMETRY, entityType, entityIdStr,
211 214 (result, tenantId, entityId) -> {
... ... @@ -392,6 +395,11 @@ public class TelemetryController extends BaseController {
392 395 if (attributes.isEmpty()) {
393 396 return getImmediateDeferredResult("No attributes data found in request body!", HttpStatus.BAD_REQUEST);
394 397 }
  398 + for (AttributeKvEntry attributeKvEntry : attributes) {
  399 + if (attributeKvEntry.getKey().isEmpty() || attributeKvEntry.getKey().trim().length() == 0) {
  400 + return getImmediateDeferredResult("Key cannot be empty or contains only spaces", HttpStatus.BAD_REQUEST);
  401 + }
  402 + }
395 403 SecurityUser user = getCurrentUser();
396 404 return accessValidator.validateEntityAndCallback(getCurrentUser(), Operation.WRITE_ATTRIBUTES, entityIdSrc, (result, tenantId, entityId) -> {
397 405 tsSubService.saveAndNotify(tenantId, entityId, scope, attributes, new FutureCallback<Void>() {
... ... @@ -435,9 +443,13 @@ public class TelemetryController extends BaseController {
435 443 if (entries.isEmpty()) {
436 444 return getImmediateDeferredResult("No timeseries data found in request body!", HttpStatus.BAD_REQUEST);
437 445 }
438   - SecurityUser user = getCurrentUser();
439 446 return accessValidator.validateEntityAndCallback(getCurrentUser(), Operation.WRITE_TELEMETRY, entityIdSrc, (result, tenantId, entityId) -> {
440   - tsSubService.saveAndNotify(tenantId, entityId, entries, ttl, new FutureCallback<Void>() {
  447 + long tenantTtl = ttl;
  448 + if (!TenantId.SYS_TENANT_ID.equals(tenantId) && tenantTtl == 0) {
  449 + TenantProfile tenantProfile = tenantProfileCache.get(tenantId);
  450 + tenantTtl = TimeUnit.DAYS.toSeconds(((DefaultTenantProfileConfiguration) tenantProfile.getProfileData().getConfiguration()).getDefaultStorageTtlDays());
  451 + }
  452 + tsSubService.saveAndNotify(tenantId, entityId, entries, tenantTtl, new FutureCallback<Void>() {
441 453 @Override
442 454 public void onSuccess(@Nullable Void tmp) {
443 455 result.setResult(new ResponseEntity(HttpStatus.OK));
... ...
... ... @@ -91,6 +91,10 @@ public class TenantController extends BaseController {
91 91 if (newTenant) {
92 92 installScripts.createDefaultRuleChains(tenant.getId());
93 93 }
  94 + tenantProfileCache.evict(tenant.getId());
  95 + tbClusterService.onTenantChange(tenant, null);
  96 + tbClusterService.onEntityStateChange(tenant.getId(), tenant.getId(),
  97 + newTenant ? ComponentLifecycleEvent.CREATED : ComponentLifecycleEvent.UPDATED);
94 98 return tenant;
95 99 } catch (Exception e) {
96 100 throw handleException(e);
... ... @@ -104,8 +108,10 @@ public class TenantController extends BaseController {
104 108 checkParameter("tenantId", strTenantId);
105 109 try {
106 110 TenantId tenantId = new TenantId(toUUID(strTenantId));
107   - checkTenantId(tenantId, Operation.DELETE);
  111 + Tenant tenant = checkTenantId(tenantId, Operation.DELETE);
108 112 tenantService.deleteTenant(tenantId);
  113 + tenantProfileCache.evict(tenantId);
  114 + tbClusterService.onTenantDelete(tenant, null);
109 115 tbClusterService.onEntityStateChange(tenantId, tenantId, ComponentLifecycleEvent.DELETED);
110 116 } catch (Exception e) {
111 117 throw handleException(e);
... ...
... ... @@ -29,9 +29,12 @@ import org.springframework.web.bind.annotation.RestController;
29 29 import org.thingsboard.server.common.data.EntityInfo;
30 30 import org.thingsboard.server.common.data.TenantProfile;
31 31 import org.thingsboard.server.common.data.exception.ThingsboardException;
  32 +import org.thingsboard.server.common.data.id.TenantId;
32 33 import org.thingsboard.server.common.data.id.TenantProfileId;
33 34 import org.thingsboard.server.common.data.page.PageData;
34 35 import org.thingsboard.server.common.data.page.PageLink;
  36 +import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
  37 +import org.thingsboard.server.dao.exception.DataValidationException;
35 38 import org.thingsboard.server.queue.util.TbCoreComponent;
36 39 import org.thingsboard.server.service.security.permission.Operation;
37 40 import org.thingsboard.server.service.security.permission.Resource;
... ... @@ -93,8 +96,12 @@ public class TenantProfileController extends BaseController {
93 96 }
94 97
95 98 tenantProfile = checkNotNull(tenantProfileService.saveTenantProfile(getTenantId(), tenantProfile));
  99 + tenantProfileCache.put(tenantProfile);
  100 + tbClusterService.onTenantProfileChange(tenantProfile, null);
  101 + tbClusterService.onEntityStateChange(TenantId.SYS_TENANT_ID, tenantProfile.getId(),
  102 + newTenantProfile ? ComponentLifecycleEvent.CREATED : ComponentLifecycleEvent.UPDATED);
96 103 return tenantProfile;
97   - } catch (Exception e) {
  104 + } catch (Exception e) {
98 105 throw handleException(e);
99 106 }
100 107 }
... ... @@ -106,8 +113,9 @@ public class TenantProfileController extends BaseController {
106 113 checkParameter("tenantProfileId", strTenantProfileId);
107 114 try {
108 115 TenantProfileId tenantProfileId = new TenantProfileId(toUUID(strTenantProfileId));
109   - checkTenantProfileId(tenantProfileId, Operation.DELETE);
  116 + TenantProfile profile = checkTenantProfileId(tenantProfileId, Operation.DELETE);
110 117 tenantProfileService.deleteTenantProfile(getTenantId(), tenantProfileId);
  118 + tbClusterService.onTenantProfileDelete(profile, null);
111 119 } catch (Exception e) {
112 120 throw handleException(e);
113 121 }
... ...
... ... @@ -52,6 +52,7 @@ import org.thingsboard.server.service.security.model.token.JwtToken;
52 52 import org.thingsboard.server.service.security.model.token.JwtTokenFactory;
53 53 import org.thingsboard.server.service.security.permission.Operation;
54 54 import org.thingsboard.server.service.security.permission.Resource;
  55 +import org.thingsboard.server.service.security.system.SystemSecurityService;
55 56 import org.thingsboard.server.utils.MiscUtils;
56 57
57 58 import javax.servlet.http.HttpServletRequest;
... ... @@ -78,6 +79,9 @@ public class UserController extends BaseController {
78 79 @Autowired
79 80 private RefreshTokenRepository refreshTokenRepository;
80 81
  82 + @Autowired
  83 + private SystemSecurityService systemSecurityService;
  84 +
81 85
82 86 @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
83 87 @RequestMapping(value = "/user/{userId}", method = RequestMethod.GET)
... ... @@ -146,7 +150,7 @@ public class UserController extends BaseController {
146 150 if (sendEmail) {
147 151 SecurityUser authUser = getCurrentUser();
148 152 UserCredentials userCredentials = userService.findUserCredentialsByUserId(authUser.getTenantId(), savedUser.getId());
149   - String baseUrl = MiscUtils.constructBaseUrl(request);
  153 + String baseUrl = systemSecurityService.getBaseUrl(getTenantId(), getCurrentUser().getCustomerId(), request);
150 154 String activateUrl = String.format(ACTIVATE_URL_PATTERN, baseUrl,
151 155 userCredentials.getActivateToken());
152 156 String email = savedUser.getEmail();
... ... @@ -189,7 +193,7 @@ public class UserController extends BaseController {
189 193
190 194 UserCredentials userCredentials = userService.findUserCredentialsByUserId(getCurrentUser().getTenantId(), user.getId());
191 195 if (!userCredentials.isEnabled()) {
192   - String baseUrl = MiscUtils.constructBaseUrl(request);
  196 + String baseUrl = systemSecurityService.getBaseUrl(getTenantId(), getCurrentUser().getCustomerId(), request);
193 197 String activateUrl = String.format(ACTIVATE_URL_PATTERN, baseUrl,
194 198 userCredentials.getActivateToken());
195 199 mailService.sendActivationEmail(activateUrl, email);
... ... @@ -214,7 +218,7 @@ public class UserController extends BaseController {
214 218 SecurityUser authUser = getCurrentUser();
215 219 UserCredentials userCredentials = userService.findUserCredentialsByUserId(authUser.getTenantId(), user.getId());
216 220 if (!userCredentials.isEnabled()) {
217   - String baseUrl = MiscUtils.constructBaseUrl(request);
  221 + String baseUrl = systemSecurityService.getBaseUrl(getTenantId(), getCurrentUser().getCustomerId(), request);
218 222 String activateUrl = String.format(ACTIVATE_URL_PATTERN, baseUrl,
219 223 userCredentials.getActivateToken());
220 224 return activateUrl;
... ...
... ... @@ -194,6 +194,7 @@ public class ThingsboardInstallService {
194 194 dataUpdateService.updateData("3.1.1");
195 195 log.info("Updating system data...");
196 196 systemDataLoaderService.updateSystemWidgets();
  197 + systemDataLoaderService.createOAuth2Templates();
197 198 break;
198 199 default:
199 200 throw new RuntimeException("Unable to upgrade ThingsBoard, unsupported fromVersion: " + upgradeFromVersion);
... ... @@ -226,6 +227,7 @@ public class ThingsboardInstallService {
226 227 systemDataLoaderService.createDefaultTenantProfiles();
227 228 systemDataLoaderService.createAdminSettings();
228 229 systemDataLoaderService.loadSystemWidgets();
  230 + systemDataLoaderService.createOAuth2Templates();
229 231 // systemDataLoaderService.loadSystemPlugins();
230 232 // systemDataLoaderService.loadSystemRules();
231 233
... ...
  1 +/**
  2 + * Copyright © 2016-2020 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.server.service.apiusage;
  17 +
  18 +import com.google.common.util.concurrent.FutureCallback;
  19 +import lombok.extern.slf4j.Slf4j;
  20 +import org.apache.commons.lang3.StringUtils;
  21 +import org.checkerframework.checker.nullness.qual.Nullable;
  22 +import org.springframework.beans.factory.annotation.Autowired;
  23 +import org.springframework.beans.factory.annotation.Value;
  24 +import org.springframework.context.annotation.Lazy;
  25 +import org.springframework.stereotype.Service;
  26 +import org.thingsboard.rule.engine.api.MailService;
  27 +import org.thingsboard.server.common.data.ApiFeature;
  28 +import org.thingsboard.server.common.data.ApiUsageRecordKey;
  29 +import org.thingsboard.server.common.data.ApiUsageState;
  30 +import org.thingsboard.server.common.data.ApiUsageStateMailMessage;
  31 +import org.thingsboard.server.common.data.ApiUsageStateValue;
  32 +import org.thingsboard.server.common.data.Tenant;
  33 +import org.thingsboard.server.common.data.TenantProfile;
  34 +import org.thingsboard.server.common.data.exception.ThingsboardException;
  35 +import org.thingsboard.server.common.data.id.ApiUsageStateId;
  36 +import org.thingsboard.server.common.data.id.TenantId;
  37 +import org.thingsboard.server.common.data.id.TenantProfileId;
  38 +import org.thingsboard.server.common.data.kv.BasicTsKvEntry;
  39 +import org.thingsboard.server.common.data.kv.LongDataEntry;
  40 +import org.thingsboard.server.common.data.kv.StringDataEntry;
  41 +import org.thingsboard.server.common.data.kv.TsKvEntry;
  42 +import org.thingsboard.server.common.data.page.PageDataIterable;
  43 +import org.thingsboard.server.common.data.tenant.profile.TenantProfileConfiguration;
  44 +import org.thingsboard.server.common.data.tenant.profile.TenantProfileData;
  45 +import org.thingsboard.server.common.msg.queue.ServiceType;
  46 +import org.thingsboard.server.common.msg.queue.TbCallback;
  47 +import org.thingsboard.server.common.msg.tools.SchedulerUtils;
  48 +import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
  49 +import org.thingsboard.server.dao.tenant.TenantService;
  50 +import org.thingsboard.server.dao.timeseries.TimeseriesService;
  51 +import org.thingsboard.server.dao.usagerecord.ApiUsageStateService;
  52 +import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceMsg;
  53 +import org.thingsboard.server.gen.transport.TransportProtos.UsageStatsKVProto;
  54 +import org.thingsboard.server.queue.common.TbProtoQueueMsg;
  55 +import org.thingsboard.server.queue.discovery.PartitionChangeEvent;
  56 +import org.thingsboard.server.queue.discovery.PartitionService;
  57 +import org.thingsboard.server.queue.scheduler.SchedulerComponent;
  58 +import org.thingsboard.server.service.queue.TbClusterService;
  59 +import org.thingsboard.server.service.telemetry.InternalTelemetryService;
  60 +
  61 +import javax.annotation.PostConstruct;
  62 +import javax.annotation.PreDestroy;
  63 +import java.util.ArrayList;
  64 +import java.util.Arrays;
  65 +import java.util.HashSet;
  66 +import java.util.List;
  67 +import java.util.Map;
  68 +import java.util.Set;
  69 +import java.util.UUID;
  70 +import java.util.concurrent.ConcurrentHashMap;
  71 +import java.util.concurrent.ExecutionException;
  72 +import java.util.concurrent.ExecutorService;
  73 +import java.util.concurrent.Executors;
  74 +import java.util.concurrent.TimeUnit;
  75 +import java.util.concurrent.locks.Lock;
  76 +import java.util.concurrent.locks.ReentrantLock;
  77 +import java.util.stream.Collectors;
  78 +
  79 +@Slf4j
  80 +@Service
  81 +public class DefaultTbApiUsageStateService implements TbApiUsageStateService {
  82 +
  83 + public static final String HOURLY = "Hourly";
  84 + public static final FutureCallback<Integer> VOID_CALLBACK = new FutureCallback<Integer>() {
  85 + @Override
  86 + public void onSuccess(@Nullable Integer result) {
  87 + }
  88 +
  89 + @Override
  90 + public void onFailure(Throwable t) {
  91 + }
  92 + };
  93 + private final TbClusterService clusterService;
  94 + private final PartitionService partitionService;
  95 + private final TenantService tenantService;
  96 + private final TimeseriesService tsService;
  97 + private final ApiUsageStateService apiUsageStateService;
  98 + private final SchedulerComponent scheduler;
  99 + private final TbTenantProfileCache tenantProfileCache;
  100 + private final MailService mailService;
  101 +
  102 + @Lazy
  103 + @Autowired
  104 + private InternalTelemetryService tsWsService;
  105 +
  106 + // Tenants that should be processed on this server
  107 + private final Map<TenantId, TenantApiUsageState> myTenantStates = new ConcurrentHashMap<>();
  108 + // Tenants that should be processed on other servers
  109 + private final Map<TenantId, ApiUsageState> otherTenantStates = new ConcurrentHashMap<>();
  110 +
  111 + @Value("${usage.stats.report.enabled:true}")
  112 + private boolean enabled;
  113 +
  114 + @Value("${usage.stats.check.cycle:60000}")
  115 + private long nextCycleCheckInterval;
  116 +
  117 + private final Lock updateLock = new ReentrantLock();
  118 +
  119 + private final ExecutorService mailExecutor;
  120 +
  121 + public DefaultTbApiUsageStateService(TbClusterService clusterService,
  122 + PartitionService partitionService,
  123 + TenantService tenantService,
  124 + TimeseriesService tsService,
  125 + ApiUsageStateService apiUsageStateService,
  126 + SchedulerComponent scheduler,
  127 + TbTenantProfileCache tenantProfileCache, MailService mailService) {
  128 + this.clusterService = clusterService;
  129 + this.partitionService = partitionService;
  130 + this.tenantService = tenantService;
  131 + this.tsService = tsService;
  132 + this.apiUsageStateService = apiUsageStateService;
  133 + this.scheduler = scheduler;
  134 + this.tenantProfileCache = tenantProfileCache;
  135 + this.mailService = mailService;
  136 + this.mailExecutor = Executors.newSingleThreadExecutor();
  137 + }
  138 +
  139 + @PostConstruct
  140 + public void init() {
  141 + if (enabled) {
  142 + log.info("Starting api usage service.");
  143 + scheduler.scheduleAtFixedRate(this::checkStartOfNextCycle, nextCycleCheckInterval, nextCycleCheckInterval, TimeUnit.MILLISECONDS);
  144 + log.info("Started api usage service.");
  145 + }
  146 + }
  147 +
  148 + @Override
  149 + public void process(TbProtoQueueMsg<ToUsageStatsServiceMsg> msg, TbCallback callback) {
  150 + ToUsageStatsServiceMsg statsMsg = msg.getValue();
  151 + TenantId tenantId = new TenantId(new UUID(statsMsg.getTenantIdMSB(), statsMsg.getTenantIdLSB()));
  152 +
  153 + if (tenantProfileCache.get(tenantId) == null) {
  154 + return;
  155 + }
  156 +
  157 + TenantApiUsageState tenantState;
  158 + List<TsKvEntry> updatedEntries;
  159 + Map<ApiFeature, ApiUsageStateValue> result;
  160 + updateLock.lock();
  161 + try {
  162 + tenantState = getOrFetchState(tenantId);
  163 + long ts = tenantState.getCurrentCycleTs();
  164 + long hourTs = tenantState.getCurrentHourTs();
  165 + long newHourTs = SchedulerUtils.getStartOfCurrentHour();
  166 + if (newHourTs != hourTs) {
  167 + tenantState.setHour(newHourTs);
  168 + }
  169 + updatedEntries = new ArrayList<>(ApiUsageRecordKey.values().length);
  170 + Set<ApiFeature> apiFeatures = new HashSet<>();
  171 + for (UsageStatsKVProto kvProto : statsMsg.getValuesList()) {
  172 + ApiUsageRecordKey recordKey = ApiUsageRecordKey.valueOf(kvProto.getKey());
  173 + long newValue = tenantState.add(recordKey, kvProto.getValue());
  174 + updatedEntries.add(new BasicTsKvEntry(ts, new LongDataEntry(recordKey.getApiCountKey(), newValue)));
  175 + long newHourlyValue = tenantState.addToHourly(recordKey, kvProto.getValue());
  176 + updatedEntries.add(new BasicTsKvEntry(newHourTs, new LongDataEntry(recordKey.getApiCountKey() + HOURLY, newHourlyValue)));
  177 + apiFeatures.add(recordKey.getApiFeature());
  178 + }
  179 + result = tenantState.checkStateUpdatedDueToThreshold(apiFeatures);
  180 + } finally {
  181 + updateLock.unlock();
  182 + }
  183 + tsWsService.saveAndNotifyInternal(tenantId, tenantState.getApiUsageState().getId(), updatedEntries, VOID_CALLBACK);
  184 + if (!result.isEmpty()) {
  185 + persistAndNotify(tenantState, result);
  186 + }
  187 + callback.onSuccess();
  188 + }
  189 +
  190 + @Override
  191 + public void onApplicationEvent(PartitionChangeEvent partitionChangeEvent) {
  192 + if (partitionChangeEvent.getServiceType().equals(ServiceType.TB_CORE)) {
  193 + myTenantStates.entrySet().removeIf(entry -> !partitionService.resolve(ServiceType.TB_CORE, entry.getKey(), entry.getKey()).isMyPartition());
  194 + otherTenantStates.entrySet().removeIf(entry -> partitionService.resolve(ServiceType.TB_CORE, entry.getKey(), entry.getKey()).isMyPartition());
  195 + initStatesFromDataBase();
  196 + }
  197 + }
  198 +
  199 + @Override
  200 + public ApiUsageState getApiUsageState(TenantId tenantId) {
  201 + TenantApiUsageState tenantState = myTenantStates.get(tenantId);
  202 + if (tenantState != null) {
  203 + return tenantState.getApiUsageState();
  204 + } else {
  205 + ApiUsageState state = otherTenantStates.get(tenantId);
  206 + if (state != null) {
  207 + return state;
  208 + } else {
  209 + if (partitionService.resolve(ServiceType.TB_CORE, tenantId, tenantId).isMyPartition()) {
  210 + return getOrFetchState(tenantId).getApiUsageState();
  211 + } else {
  212 + updateLock.lock();
  213 + try {
  214 + state = otherTenantStates.get(tenantId);
  215 + if (state == null) {
  216 + state = apiUsageStateService.findTenantApiUsageState(tenantId);
  217 + if (state != null) {
  218 + otherTenantStates.put(tenantId, state);
  219 + }
  220 + }
  221 + } finally {
  222 + updateLock.unlock();
  223 + }
  224 + return state;
  225 + }
  226 + }
  227 + }
  228 + }
  229 +
  230 + @Override
  231 + public void onApiUsageStateUpdate(TenantId tenantId) {
  232 + otherTenantStates.remove(tenantId);
  233 + }
  234 +
  235 + @Override
  236 + public void onTenantProfileUpdate(TenantProfileId tenantProfileId) {
  237 + log.info("[{}] On Tenant Profile Update", tenantProfileId);
  238 + TenantProfile tenantProfile = tenantProfileCache.get(tenantProfileId);
  239 + updateLock.lock();
  240 + try {
  241 + myTenantStates.values().forEach(state -> {
  242 + if (tenantProfile.getId().equals(state.getTenantProfileId())) {
  243 + updateTenantState(state, tenantProfile);
  244 + }
  245 + });
  246 + } finally {
  247 + updateLock.unlock();
  248 + }
  249 + }
  250 +
  251 + @Override
  252 + public void onTenantUpdate(TenantId tenantId) {
  253 + log.info("[{}] On Tenant Update.", tenantId);
  254 + TenantProfile tenantProfile = tenantProfileCache.get(tenantId);
  255 + updateLock.lock();
  256 + try {
  257 + TenantApiUsageState state = myTenantStates.get(tenantId);
  258 + if (state != null && !state.getTenantProfileId().equals(tenantProfile.getId())) {
  259 + updateTenantState(state, tenantProfile);
  260 + }
  261 + } finally {
  262 + updateLock.unlock();
  263 + }
  264 + }
  265 +
  266 + private void updateTenantState(TenantApiUsageState state, TenantProfile profile) {
  267 + TenantProfileData oldProfileData = state.getTenantProfileData();
  268 + state.setTenantProfileId(profile.getId());
  269 + state.setTenantProfileData(profile.getProfileData());
  270 + Map<ApiFeature, ApiUsageStateValue> result = state.checkStateUpdatedDueToThresholds();
  271 + if (!result.isEmpty()) {
  272 + persistAndNotify(state, result);
  273 + }
  274 + updateProfileThresholds(state.getTenantId(), state.getApiUsageState().getId(),
  275 + oldProfileData.getConfiguration(), profile.getProfileData().getConfiguration());
  276 + }
  277 +
  278 + private void updateProfileThresholds(TenantId tenantId, ApiUsageStateId id,
  279 + TenantProfileConfiguration oldData, TenantProfileConfiguration newData) {
  280 + long ts = System.currentTimeMillis();
  281 + List<TsKvEntry> profileThresholds = new ArrayList<>();
  282 + for (ApiUsageRecordKey key : ApiUsageRecordKey.values()) {
  283 + long newProfileThreshold = newData.getProfileThreshold(key);
  284 + if (oldData == null || oldData.getProfileThreshold(key) != newProfileThreshold) {
  285 + log.info("[{}] Updating profile threshold [{}]:[{}]", tenantId, key, newProfileThreshold);
  286 + profileThresholds.add(new BasicTsKvEntry(ts, new LongDataEntry(key.getApiLimitKey(), newProfileThreshold)));
  287 + }
  288 + }
  289 + if (!profileThresholds.isEmpty()) {
  290 + tsWsService.saveAndNotifyInternal(tenantId, id, profileThresholds, VOID_CALLBACK);
  291 + }
  292 + }
  293 +
  294 + private void persistAndNotify(TenantApiUsageState state, Map<ApiFeature, ApiUsageStateValue> result) {
  295 + log.info("[{}] Detected update of the API state: {}", state.getTenantId(), result);
  296 + apiUsageStateService.update(state.getApiUsageState());
  297 + clusterService.onApiStateChange(state.getApiUsageState(), null);
  298 + long ts = System.currentTimeMillis();
  299 + List<TsKvEntry> stateTelemetry = new ArrayList<>();
  300 + result.forEach(((apiFeature, aState) -> stateTelemetry.add(new BasicTsKvEntry(ts, new StringDataEntry(apiFeature.getApiStateKey(), aState.name())))));
  301 + tsWsService.saveAndNotifyInternal(state.getTenantId(), state.getApiUsageState().getId(), stateTelemetry, VOID_CALLBACK);
  302 +
  303 + String email = tenantService.findTenantById(state.getTenantId()).getEmail();
  304 +
  305 + if (StringUtils.isNotEmpty(email)) {
  306 + result.forEach((apiFeature, stateValue) -> {
  307 + mailExecutor.submit(() -> {
  308 + try {
  309 + mailService.sendApiFeatureStateEmail(apiFeature, stateValue, email, createStateMailMessage(state, apiFeature, stateValue));
  310 + } catch (ThingsboardException e) {
  311 + log.warn("[{}] Can't send update of the API state to tenant with provided email [{}]", state.getTenantId(), email, e);
  312 + }
  313 + });
  314 + });
  315 + } else {
  316 + log.warn("[{}] Can't send update of the API state to tenant with empty email!", state.getTenantId());
  317 + }
  318 + }
  319 +
  320 + private ApiUsageStateMailMessage createStateMailMessage(TenantApiUsageState state, ApiFeature apiFeature, ApiUsageStateValue stateValue) {
  321 + StateChecker checker = getStateChecker(stateValue);
  322 + for (ApiUsageRecordKey apiUsageRecordKey : ApiUsageRecordKey.getKeys(apiFeature)) {
  323 + long threshold = state.getProfileThreshold(apiUsageRecordKey);
  324 + long warnThreshold = state.getProfileWarnThreshold(apiUsageRecordKey);
  325 + long value = state.get(apiUsageRecordKey);
  326 + if (checker.check(threshold, warnThreshold, value)) {
  327 + return new ApiUsageStateMailMessage(apiUsageRecordKey, threshold, value);
  328 + }
  329 + }
  330 + return null;
  331 + }
  332 +
  333 + private StateChecker getStateChecker(ApiUsageStateValue stateValue) {
  334 + if (ApiUsageStateValue.ENABLED.equals(stateValue)) {
  335 + return (t, wt, v) -> true;
  336 + } else if (ApiUsageStateValue.WARNING.equals(stateValue)) {
  337 + return (t, wt, v) -> v < t && v >= wt;
  338 + } else {
  339 + return (t, wt, v) -> v >= t;
  340 + }
  341 + }
  342 +
  343 + private interface StateChecker {
  344 + boolean check(long threshold, long warnThreshold, long value);
  345 + }
  346 +
  347 + private void checkStartOfNextCycle() {
  348 + updateLock.lock();
  349 + try {
  350 + long now = System.currentTimeMillis();
  351 + myTenantStates.values().forEach(state -> {
  352 + if ((state.getNextCycleTs() < now) && (now - state.getNextCycleTs() < TimeUnit.HOURS.toMillis(1))) {
  353 + TenantId tenantId = state.getTenantId();
  354 + state.setCycles(state.getNextCycleTs(), SchedulerUtils.getStartOfNextNextMonth());
  355 + saveNewCounts(state, Arrays.asList(ApiUsageRecordKey.values()));
  356 + updateTenantState(state, tenantProfileCache.get(tenantId));
  357 + }
  358 + });
  359 + } finally {
  360 + updateLock.unlock();
  361 + }
  362 + }
  363 +
  364 + private void saveNewCounts(TenantApiUsageState state, List<ApiUsageRecordKey> keys) {
  365 + List<TsKvEntry> counts = keys.stream()
  366 + .map(key -> new BasicTsKvEntry(state.getCurrentCycleTs(), new LongDataEntry(key.getApiCountKey(), 0L)))
  367 + .collect(Collectors.toList());
  368 +
  369 + tsWsService.saveAndNotifyInternal(state.getTenantId(), state.getApiUsageState().getId(), counts, VOID_CALLBACK);
  370 + }
  371 +
  372 + private TenantApiUsageState getOrFetchState(TenantId tenantId) {
  373 + TenantApiUsageState tenantState = myTenantStates.get(tenantId);
  374 + if (tenantState == null) {
  375 + ApiUsageState dbStateEntity = apiUsageStateService.findTenantApiUsageState(tenantId);
  376 + if (dbStateEntity == null) {
  377 + try {
  378 + dbStateEntity = apiUsageStateService.createDefaultApiUsageState(tenantId);
  379 + } catch (Exception e) {
  380 + dbStateEntity = apiUsageStateService.findTenantApiUsageState(tenantId);
  381 + }
  382 + }
  383 + TenantProfile tenantProfile = tenantProfileCache.get(tenantId);
  384 + tenantState = new TenantApiUsageState(tenantProfile, dbStateEntity);
  385 + List<ApiUsageRecordKey> newCounts = new ArrayList<>();
  386 + try {
  387 + List<TsKvEntry> dbValues = tsService.findAllLatest(tenantId, dbStateEntity.getId()).get();
  388 + for (ApiUsageRecordKey key : ApiUsageRecordKey.values()) {
  389 + boolean cycleEntryFound = false;
  390 + boolean hourlyEntryFound = false;
  391 + for (TsKvEntry tsKvEntry : dbValues) {
  392 + if (tsKvEntry.getKey().equals(key.getApiCountKey())) {
  393 + cycleEntryFound = true;
  394 +
  395 + boolean oldCount = tsKvEntry.getTs() == tenantState.getCurrentCycleTs();
  396 + tenantState.put(key, oldCount ? tsKvEntry.getLongValue().get() : 0L);
  397 +
  398 + if (!oldCount) {
  399 + newCounts.add(key);
  400 + }
  401 + } else if (tsKvEntry.getKey().equals(key.getApiCountKey() + HOURLY)) {
  402 + hourlyEntryFound = true;
  403 + tenantState.putHourly(key, tsKvEntry.getTs() == tenantState.getCurrentHourTs() ? tsKvEntry.getLongValue().get() : 0L);
  404 + }
  405 + if (cycleEntryFound && hourlyEntryFound) {
  406 + break;
  407 + }
  408 + }
  409 + }
  410 + log.debug("[{}] Initialized state: {}", tenantId, dbStateEntity);
  411 + myTenantStates.put(tenantId, tenantState);
  412 + saveNewCounts(tenantState, newCounts);
  413 + } catch (InterruptedException | ExecutionException e) {
  414 + log.warn("[{}] Failed to fetch api usage state from db.", tenantId, e);
  415 + }
  416 + }
  417 + return tenantState;
  418 + }
  419 +
  420 + private void initStatesFromDataBase() {
  421 + try {
  422 + log.info("Initializing tenant states.");
  423 + PageDataIterable<Tenant> tenantIterator = new PageDataIterable<>(tenantService::findTenants, 1024);
  424 + for (Tenant tenant : tenantIterator) {
  425 + if (!myTenantStates.containsKey(tenant.getId()) && partitionService.resolve(ServiceType.TB_CORE, tenant.getId(), tenant.getId()).isMyPartition()) {
  426 + log.debug("[{}] Initializing tenant state.", tenant.getId());
  427 + updateLock.lock();
  428 + try {
  429 + updateTenantState(getOrFetchState(tenant.getId()), tenantProfileCache.get(tenant.getTenantProfileId()));
  430 + log.debug("[{}] Initialized tenant state.", tenant.getId());
  431 + } catch (Exception e) {
  432 + log.warn("[{}] Failed to initialize tenant API state", tenant.getId(), e);
  433 + } finally {
  434 + updateLock.unlock();
  435 + }
  436 + }
  437 + }
  438 + log.info("Initialized tenant states.");
  439 + } catch (Exception e) {
  440 + log.warn("Unknown failure", e);
  441 + }
  442 + }
  443 +
  444 + @PreDestroy
  445 + private void destroy() {
  446 + if (mailExecutor != null) {
  447 + mailExecutor.shutdownNow();
  448 + }
  449 + }
  450 +}
... ...
  1 +/**
  2 + * Copyright © 2016-2020 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.server.service.apiusage;
  17 +
  18 +import org.springframework.context.ApplicationListener;
  19 +import org.thingsboard.server.common.data.ApiUsageState;
  20 +import org.thingsboard.server.common.data.id.TenantId;
  21 +import org.thingsboard.server.common.data.id.TenantProfileId;
  22 +import org.thingsboard.server.common.msg.queue.TbCallback;
  23 +import org.thingsboard.server.gen.transport.TransportProtos.ToUsageStatsServiceMsg;
  24 +import org.thingsboard.server.queue.common.TbProtoQueueMsg;
  25 +import org.thingsboard.server.queue.discovery.PartitionChangeEvent;
  26 +
  27 +public interface TbApiUsageStateService extends ApplicationListener<PartitionChangeEvent> {
  28 +
  29 + void process(TbProtoQueueMsg<ToUsageStatsServiceMsg> msg, TbCallback callback);
  30 +
  31 + ApiUsageState getApiUsageState(TenantId tenantId);
  32 +
  33 + void onTenantProfileUpdate(TenantProfileId tenantProfileId);
  34 +
  35 + void onTenantUpdate(TenantId tenantId);
  36 +
  37 + void onApiUsageStateUpdate(TenantId tenantId);
  38 +}
... ...
  1 +/**
  2 + * Copyright © 2016-2020 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.server.service.apiusage;
  17 +
  18 +import lombok.Getter;
  19 +import lombok.Setter;
  20 +import org.springframework.data.util.Pair;
  21 +import org.thingsboard.server.common.data.ApiFeature;
  22 +import org.thingsboard.server.common.data.ApiUsageRecordKey;
  23 +import org.thingsboard.server.common.data.ApiUsageState;
  24 +import org.thingsboard.server.common.data.ApiUsageStateValue;
  25 +import org.thingsboard.server.common.data.TenantProfile;
  26 +import org.thingsboard.server.common.data.id.TenantId;
  27 +import org.thingsboard.server.common.data.id.TenantProfileId;
  28 +import org.thingsboard.server.common.data.tenant.profile.TenantProfileData;
  29 +import org.thingsboard.server.common.msg.tools.SchedulerUtils;
  30 +
  31 +import java.util.Arrays;
  32 +import java.util.HashMap;
  33 +import java.util.HashSet;
  34 +import java.util.Map;
  35 +import java.util.Set;
  36 +import java.util.concurrent.ConcurrentHashMap;
  37 +
  38 +public class TenantApiUsageState {
  39 +
  40 + private final Map<ApiUsageRecordKey, Long> currentCycleValues = new ConcurrentHashMap<>();
  41 + private final Map<ApiUsageRecordKey, Long> currentHourValues = new ConcurrentHashMap<>();
  42 +
  43 + @Getter
  44 + @Setter
  45 + private TenantProfileId tenantProfileId;
  46 + @Getter
  47 + @Setter
  48 + private TenantProfileData tenantProfileData;
  49 + @Getter
  50 + private final ApiUsageState apiUsageState;
  51 + @Getter
  52 + private volatile long currentCycleTs;
  53 + @Getter
  54 + private volatile long nextCycleTs;
  55 + @Getter
  56 + private volatile long currentHourTs;
  57 +
  58 + public TenantApiUsageState(TenantProfile tenantProfile, ApiUsageState apiUsageState) {
  59 + this.tenantProfileId = tenantProfile.getId();
  60 + this.tenantProfileData = tenantProfile.getProfileData();
  61 + this.apiUsageState = apiUsageState;
  62 + this.currentCycleTs = SchedulerUtils.getStartOfCurrentMonth();
  63 + this.nextCycleTs = SchedulerUtils.getStartOfNextMonth();
  64 + this.currentHourTs = SchedulerUtils.getStartOfCurrentHour();
  65 + }
  66 +
  67 + public void put(ApiUsageRecordKey key, Long value) {
  68 + currentCycleValues.put(key, value);
  69 + }
  70 +
  71 + public void putHourly(ApiUsageRecordKey key, Long value) {
  72 + currentHourValues.put(key, value);
  73 + }
  74 +
  75 + public long add(ApiUsageRecordKey key, long value) {
  76 + long result = currentCycleValues.getOrDefault(key, 0L) + value;
  77 + currentCycleValues.put(key, result);
  78 + return result;
  79 + }
  80 +
  81 + public long get(ApiUsageRecordKey key) {
  82 + return currentCycleValues.getOrDefault(key, 0L);
  83 + }
  84 +
  85 + public long addToHourly(ApiUsageRecordKey key, long value) {
  86 + long result = currentHourValues.getOrDefault(key, 0L) + value;
  87 + currentHourValues.put(key, result);
  88 + return result;
  89 + }
  90 +
  91 + public void setHour(long currentHourTs) {
  92 + this.currentHourTs = currentHourTs;
  93 + for (ApiUsageRecordKey key : ApiUsageRecordKey.values()) {
  94 + currentHourValues.put(key, 0L);
  95 + }
  96 + }
  97 +
  98 + public void setCycles(long currentCycleTs, long nextCycleTs) {
  99 + this.currentCycleTs = currentCycleTs;
  100 + this.nextCycleTs = nextCycleTs;
  101 + for (ApiUsageRecordKey key : ApiUsageRecordKey.values()) {
  102 + currentCycleValues.put(key, 0L);
  103 + }
  104 + }
  105 +
  106 + public long getProfileThreshold(ApiUsageRecordKey key) {
  107 + return tenantProfileData.getConfiguration().getProfileThreshold(key);
  108 + }
  109 +
  110 + public long getProfileWarnThreshold(ApiUsageRecordKey key) {
  111 + return tenantProfileData.getConfiguration().getWarnThreshold(key);
  112 + }
  113 +
  114 + public TenantId getTenantId() {
  115 + return apiUsageState.getTenantId();
  116 + }
  117 +
  118 + public ApiUsageStateValue getFeatureValue(ApiFeature feature) {
  119 + switch (feature) {
  120 + case TRANSPORT:
  121 + return apiUsageState.getTransportState();
  122 + case RE:
  123 + return apiUsageState.getReExecState();
  124 + case DB:
  125 + return apiUsageState.getDbStorageState();
  126 + case JS:
  127 + return apiUsageState.getJsExecState();
  128 + case EMAIL:
  129 + return apiUsageState.getEmailExecState();
  130 + case SMS:
  131 + return apiUsageState.getSmsExecState();
  132 + default:
  133 + return ApiUsageStateValue.ENABLED;
  134 + }
  135 + }
  136 +
  137 + public boolean setFeatureValue(ApiFeature feature, ApiUsageStateValue value) {
  138 + ApiUsageStateValue currentValue = getFeatureValue(feature);
  139 + switch (feature) {
  140 + case TRANSPORT:
  141 + apiUsageState.setTransportState(value);
  142 + break;
  143 + case RE:
  144 + apiUsageState.setReExecState(value);
  145 + break;
  146 + case DB:
  147 + apiUsageState.setDbStorageState(value);
  148 + break;
  149 + case JS:
  150 + apiUsageState.setJsExecState(value);
  151 + break;
  152 + case EMAIL:
  153 + apiUsageState.setEmailExecState(value);
  154 + break;
  155 + case SMS:
  156 + apiUsageState.setSmsExecState(value);
  157 + break;
  158 + }
  159 + return !currentValue.equals(value);
  160 + }
  161 +
  162 + public Map<ApiFeature, ApiUsageStateValue> checkStateUpdatedDueToThresholds() {
  163 + return checkStateUpdatedDueToThreshold(new HashSet<>(Arrays.asList(ApiFeature.values())));
  164 + }
  165 +
  166 + public Map<ApiFeature, ApiUsageStateValue> checkStateUpdatedDueToThreshold(Set<ApiFeature> features) {
  167 + Map<ApiFeature, ApiUsageStateValue> result = new HashMap<>();
  168 + for (ApiFeature feature : features) {
  169 + Pair<ApiFeature, ApiUsageStateValue> tmp = checkStateUpdatedDueToThreshold(feature);
  170 + if (tmp != null) {
  171 + result.put(tmp.getFirst(), tmp.getSecond());
  172 + }
  173 + }
  174 + return result;
  175 + }
  176 +
  177 + public Pair<ApiFeature, ApiUsageStateValue> checkStateUpdatedDueToThreshold(ApiFeature feature) {
  178 + ApiUsageStateValue featureValue = ApiUsageStateValue.ENABLED;
  179 + for (ApiUsageRecordKey recordKey : ApiUsageRecordKey.getKeys(feature)) {
  180 + long value = get(recordKey);
  181 + long threshold = getProfileThreshold(recordKey);
  182 + long warnThreshold = getProfileWarnThreshold(recordKey);
  183 + ApiUsageStateValue tmpValue;
  184 + if (threshold == 0 || value < warnThreshold) {
  185 + tmpValue = ApiUsageStateValue.ENABLED;
  186 + } else if (value < threshold) {
  187 + tmpValue = ApiUsageStateValue.WARNING;
  188 + } else {
  189 + tmpValue = ApiUsageStateValue.DISABLED;
  190 + }
  191 + featureValue = ApiUsageStateValue.toMoreRestricted(featureValue, tmpValue);
  192 + }
  193 + return setFeatureValue(feature, featureValue) ? Pair.of(feature, featureValue) : null;
  194 + }
  195 +
  196 +}
... ...
  1 +/**
  2 + * Copyright © 2016-2020 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.server.service.device;
  17 +
  18 +import com.fasterxml.jackson.core.JsonProcessingException;
  19 +import com.fasterxml.jackson.databind.JsonNode;
  20 +import com.fasterxml.jackson.databind.node.ObjectNode;
  21 +import com.google.common.util.concurrent.ListenableFuture;
  22 +import lombok.extern.slf4j.Slf4j;
  23 +import org.apache.commons.lang.RandomStringUtils;
  24 +import org.springframework.beans.factory.annotation.Autowired;
  25 +import org.springframework.stereotype.Service;
  26 +import org.springframework.util.StringUtils;
  27 +import org.thingsboard.server.common.data.DataConstants;
  28 +import org.thingsboard.server.common.data.Device;
  29 +import org.thingsboard.server.common.data.DeviceProfile;
  30 +import org.thingsboard.server.common.data.audit.ActionType;
  31 +import org.thingsboard.server.common.data.id.CustomerId;
  32 +import org.thingsboard.server.common.data.id.TenantId;
  33 +import org.thingsboard.server.common.data.id.UserId;
  34 +import org.thingsboard.server.common.data.kv.AttributeKvEntry;
  35 +import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry;
  36 +import org.thingsboard.server.common.data.kv.StringDataEntry;
  37 +import org.thingsboard.server.common.data.security.DeviceCredentials;
  38 +import org.thingsboard.server.common.msg.TbMsg;
  39 +import org.thingsboard.server.common.msg.TbMsgMetaData;
  40 +import org.thingsboard.server.common.msg.queue.ServiceType;
  41 +import org.thingsboard.server.common.msg.queue.TopicPartitionInfo;
  42 +import org.thingsboard.server.dao.attributes.AttributesService;
  43 +import org.thingsboard.server.dao.audit.AuditLogService;
  44 +import org.thingsboard.server.dao.device.DeviceCredentialsService;
  45 +import org.thingsboard.server.dao.device.DeviceDao;
  46 +import org.thingsboard.server.dao.device.DeviceProfileDao;
  47 +import org.thingsboard.server.dao.device.DeviceProvisionService;
  48 +import org.thingsboard.server.dao.device.DeviceService;
  49 +import org.thingsboard.server.dao.device.provision.ProvisionFailedException;
  50 +import org.thingsboard.server.dao.device.provision.ProvisionRequest;
  51 +import org.thingsboard.server.dao.device.provision.ProvisionResponse;
  52 +import org.thingsboard.server.dao.device.provision.ProvisionResponseStatus;
  53 +import org.thingsboard.server.dao.util.mapping.JacksonUtil;
  54 +import org.thingsboard.server.gen.transport.TransportProtos;
  55 +import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg;
  56 +import org.thingsboard.server.queue.TbQueueCallback;
  57 +import org.thingsboard.server.queue.TbQueueProducer;
  58 +import org.thingsboard.server.queue.common.TbProtoQueueMsg;
  59 +import org.thingsboard.server.queue.discovery.PartitionService;
  60 +import org.thingsboard.server.queue.provider.TbQueueProducerProvider;
  61 +import org.thingsboard.server.queue.util.TbCoreComponent;
  62 +import org.thingsboard.server.service.state.DeviceStateService;
  63 +
  64 +import java.util.Collections;
  65 +import java.util.List;
  66 +import java.util.Optional;
  67 +import java.util.concurrent.ExecutionException;
  68 +import java.util.concurrent.locks.ReentrantLock;
  69 +
  70 +
  71 +@Service
  72 +@Slf4j
  73 +@TbCoreComponent
  74 +public class DeviceProvisionServiceImpl implements DeviceProvisionService {
  75 +
  76 + protected TbQueueProducer<TbProtoQueueMsg<ToRuleEngineMsg>> ruleEngineMsgProducer;
  77 +
  78 + private static final String DEVICE_PROVISION_STATE = "provisionState";
  79 + private static final String PROVISIONED_STATE = "provisioned";
  80 +
  81 + private final ReentrantLock deviceCreationLock = new ReentrantLock();
  82 +
  83 + @Autowired
  84 + DeviceDao deviceDao;
  85 +
  86 + @Autowired
  87 + DeviceProfileDao deviceProfileDao;
  88 +
  89 + @Autowired
  90 + DeviceService deviceService;
  91 +
  92 + @Autowired
  93 + DeviceCredentialsService deviceCredentialsService;
  94 +
  95 + @Autowired
  96 + AttributesService attributesService;
  97 +
  98 + @Autowired
  99 + DeviceStateService deviceStateService;
  100 +
  101 + @Autowired
  102 + AuditLogService auditLogService;
  103 +
  104 + @Autowired
  105 + PartitionService partitionService;
  106 +
  107 + public DeviceProvisionServiceImpl(TbQueueProducerProvider producerProvider) {
  108 + ruleEngineMsgProducer = producerProvider.getRuleEngineMsgProducer();
  109 + }
  110 +
  111 + @Override
  112 + public ProvisionResponse provisionDevice(ProvisionRequest provisionRequest) {
  113 + String provisionRequestKey = provisionRequest.getCredentials().getProvisionDeviceKey();
  114 + String provisionRequestSecret = provisionRequest.getCredentials().getProvisionDeviceSecret();
  115 + if (!StringUtils.isEmpty(provisionRequest.getDeviceName())) {
  116 + provisionRequest.setDeviceName(provisionRequest.getDeviceName().trim());
  117 + if (StringUtils.isEmpty(provisionRequest.getDeviceName())) {
  118 + log.warn("Provision request contains empty device name!");
  119 + throw new ProvisionFailedException(ProvisionResponseStatus.FAILURE.name());
  120 + }
  121 + }
  122 +
  123 + if (StringUtils.isEmpty(provisionRequestKey) || StringUtils.isEmpty(provisionRequestSecret)) {
  124 + throw new ProvisionFailedException(ProvisionResponseStatus.NOT_FOUND.name());
  125 + }
  126 +
  127 + DeviceProfile targetProfile = deviceProfileDao.findByProvisionDeviceKey(provisionRequestKey);
  128 +
  129 + if (targetProfile == null || targetProfile.getProfileData().getProvisionConfiguration() == null ||
  130 + targetProfile.getProfileData().getProvisionConfiguration().getProvisionDeviceSecret() == null) {
  131 + throw new ProvisionFailedException(ProvisionResponseStatus.NOT_FOUND.name());
  132 + }
  133 +
  134 + Device targetDevice = deviceDao.findDeviceByTenantIdAndName(targetProfile.getTenantId().getId(), provisionRequest.getDeviceName()).orElse(null);
  135 +
  136 + switch (targetProfile.getProvisionType()) {
  137 + case ALLOW_CREATE_NEW_DEVICES:
  138 + if (targetProfile.getProfileData().getProvisionConfiguration().getProvisionDeviceSecret().equals(provisionRequestSecret)) {
  139 + if (targetDevice != null) {
  140 + log.warn("[{}] The device is present and could not be provisioned once more!", targetDevice.getName());
  141 + notify(targetDevice, provisionRequest, DataConstants.PROVISION_FAILURE, false);
  142 + throw new ProvisionFailedException(ProvisionResponseStatus.FAILURE.name());
  143 + } else {
  144 + return createDevice(provisionRequest, targetProfile);
  145 + }
  146 + }
  147 + break;
  148 + case CHECK_PRE_PROVISIONED_DEVICES:
  149 + if (targetProfile.getProfileData().getProvisionConfiguration().getProvisionDeviceSecret().equals(provisionRequestSecret)) {
  150 + if (targetDevice != null && targetDevice.getDeviceProfileId().equals(targetProfile.getId())) {
  151 + return processProvision(targetDevice, provisionRequest);
  152 + } else {
  153 + log.warn("[{}] Failed to find pre provisioned device!", provisionRequest.getDeviceName());
  154 + throw new ProvisionFailedException(ProvisionResponseStatus.FAILURE.name());
  155 + }
  156 + }
  157 + break;
  158 + }
  159 + throw new ProvisionFailedException(ProvisionResponseStatus.NOT_FOUND.name());
  160 + }
  161 +
  162 + private ProvisionResponse processProvision(Device device, ProvisionRequest provisionRequest) {
  163 + try {
  164 + Optional<AttributeKvEntry> provisionState = attributesService.find(device.getTenantId(), device.getId(),
  165 + DataConstants.SERVER_SCOPE, DEVICE_PROVISION_STATE).get();
  166 + if (provisionState != null && provisionState.isPresent() && !provisionState.get().getValueAsString().equals(PROVISIONED_STATE)) {
  167 + notify(device, provisionRequest, DataConstants.PROVISION_FAILURE, false);
  168 + throw new ProvisionFailedException(ProvisionResponseStatus.FAILURE.name());
  169 + } else {
  170 + saveProvisionStateAttribute(device).get();
  171 + notify(device, provisionRequest, DataConstants.PROVISION_SUCCESS, true);
  172 + }
  173 + } catch (InterruptedException | ExecutionException e) {
  174 + throw new ProvisionFailedException(ProvisionResponseStatus.FAILURE.name());
  175 + }
  176 + return new ProvisionResponse(deviceCredentialsService.findDeviceCredentialsByDeviceId(device.getTenantId(), device.getId()), ProvisionResponseStatus.SUCCESS);
  177 + }
  178 +
  179 + private ProvisionResponse createDevice(ProvisionRequest provisionRequest, DeviceProfile profile) {
  180 + deviceCreationLock.lock();
  181 + try {
  182 + return processCreateDevice(provisionRequest, profile);
  183 + } finally {
  184 + deviceCreationLock.unlock();
  185 + }
  186 + }
  187 +
  188 + private void notify(Device device, ProvisionRequest provisionRequest, String type, boolean success) {
  189 + pushProvisionEventToRuleEngine(provisionRequest, device, type);
  190 + logAction(device.getTenantId(), device.getCustomerId(), device, success, provisionRequest);
  191 + }
  192 +
  193 + private ProvisionResponse processCreateDevice(ProvisionRequest provisionRequest, DeviceProfile profile) {
  194 + Device device = deviceService.findDeviceByTenantIdAndName(profile.getTenantId(), provisionRequest.getDeviceName());
  195 + try {
  196 + if (device == null) {
  197 + if (StringUtils.isEmpty(provisionRequest.getDeviceName())) {
  198 + String newDeviceName = RandomStringUtils.randomAlphanumeric(20);
  199 + log.info("Device name not found in provision request. Generated name is: {}", newDeviceName);
  200 + provisionRequest.setDeviceName(newDeviceName);
  201 + }
  202 + Device savedDevice = deviceService.saveDevice(provisionRequest, profile);
  203 +
  204 + deviceStateService.onDeviceAdded(savedDevice);
  205 + saveProvisionStateAttribute(savedDevice).get();
  206 + pushDeviceCreatedEventToRuleEngine(savedDevice);
  207 + notify(savedDevice, provisionRequest, DataConstants.PROVISION_SUCCESS, true);
  208 +
  209 + return new ProvisionResponse(getDeviceCredentials(savedDevice), ProvisionResponseStatus.SUCCESS);
  210 + } else {
  211 + log.warn("[{}] The device is already provisioned!", device.getName());
  212 + notify(device, provisionRequest, DataConstants.PROVISION_FAILURE, false);
  213 + throw new ProvisionFailedException(ProvisionResponseStatus.FAILURE.name());
  214 + }
  215 + } catch (InterruptedException | ExecutionException e) {
  216 + throw new ProvisionFailedException(ProvisionResponseStatus.FAILURE.name());
  217 + }
  218 + }
  219 +
  220 + private ListenableFuture<List<Void>> saveProvisionStateAttribute(Device device) {
  221 + return attributesService.save(device.getTenantId(), device.getId(), DataConstants.SERVER_SCOPE,
  222 + Collections.singletonList(new BaseAttributeKvEntry(new StringDataEntry(DEVICE_PROVISION_STATE, PROVISIONED_STATE),
  223 + System.currentTimeMillis())));
  224 + }
  225 +
  226 + private DeviceCredentials getDeviceCredentials(Device device) {
  227 + return deviceCredentialsService.findDeviceCredentialsByDeviceId(device.getTenantId(), device.getId());
  228 + }
  229 +
  230 + private void pushProvisionEventToRuleEngine(ProvisionRequest request, Device device, String type) {
  231 + try {
  232 + JsonNode entityNode = JacksonUtil.valueToTree(request);
  233 + TbMsg msg = TbMsg.newMsg(type, device.getId(), createTbMsgMetaData(device), JacksonUtil.toString(entityNode));
  234 + sendToRuleEngine(device.getTenantId(), msg, null);
  235 + } catch (IllegalArgumentException e) {
  236 + log.warn("[{}] Failed to push device action to rule engine: {}", device.getId(), type, e);
  237 + }
  238 + }
  239 +
  240 + private void pushDeviceCreatedEventToRuleEngine(Device device) {
  241 + try {
  242 + ObjectNode entityNode = JacksonUtil.OBJECT_MAPPER.valueToTree(device);
  243 + TbMsg msg = TbMsg.newMsg(DataConstants.ENTITY_CREATED, device.getId(), createTbMsgMetaData(device), JacksonUtil.OBJECT_MAPPER.writeValueAsString(entityNode));
  244 + sendToRuleEngine(device.getTenantId(), msg, null);
  245 + } catch (JsonProcessingException | IllegalArgumentException e) {
  246 + log.warn("[{}] Failed to push device action to rule engine: {}", device.getId(), DataConstants.ENTITY_CREATED, e);
  247 + }
  248 + }
  249 +
  250 + protected void sendToRuleEngine(TenantId tenantId, TbMsg tbMsg, TbQueueCallback callback) {
  251 + TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_RULE_ENGINE, tenantId, tbMsg.getOriginator());
  252 + TransportProtos.ToRuleEngineMsg msg = TransportProtos.ToRuleEngineMsg.newBuilder().setTbMsg(TbMsg.toByteString(tbMsg))
  253 + .setTenantIdMSB(tenantId.getId().getMostSignificantBits())
  254 + .setTenantIdLSB(tenantId.getId().getLeastSignificantBits()).build();
  255 + ruleEngineMsgProducer.send(tpi, new TbProtoQueueMsg<>(tbMsg.getId(), msg), callback);
  256 + }
  257 +
  258 + private TbMsgMetaData createTbMsgMetaData(Device device) {
  259 + TbMsgMetaData metaData = new TbMsgMetaData();
  260 + metaData.putValue("tenantId", device.getTenantId().toString());
  261 + return metaData;
  262 + }
  263 +
  264 + private void logAction(TenantId tenantId, CustomerId customerId, Device device, boolean success, ProvisionRequest provisionRequest) {
  265 + ActionType actionType = success ? ActionType.PROVISION_SUCCESS : ActionType.PROVISION_FAILURE;
  266 + auditLogService.logEntityAction(tenantId, customerId, new UserId(UserId.NULL_UUID), device.getName(), device.getId(), device, actionType, null, provisionRequest);
  267 + }
  268 +}
... ...
... ... @@ -28,11 +28,21 @@ import org.thingsboard.server.common.data.Customer;
28 28 import org.thingsboard.server.common.data.DataConstants;
29 29 import org.thingsboard.server.common.data.Device;
30 30 import org.thingsboard.server.common.data.DeviceProfile;
  31 +import org.thingsboard.server.common.data.DeviceProfileProvisionType;
  32 +import org.thingsboard.server.common.data.DeviceProfileType;
  33 +import org.thingsboard.server.common.data.DeviceTransportType;
31 34 import org.thingsboard.server.common.data.Tenant;
32 35 import org.thingsboard.server.common.data.TenantProfile;
33   -import org.thingsboard.server.common.data.TenantProfileData;
34 36 import org.thingsboard.server.common.data.User;
35   -import org.thingsboard.server.common.data.asset.Asset;
  37 +import org.thingsboard.server.common.data.alarm.AlarmSeverity;
  38 +import org.thingsboard.server.common.data.device.profile.AlarmCondition;
  39 +import org.thingsboard.server.common.data.device.profile.AlarmRule;
  40 +import org.thingsboard.server.common.data.device.profile.DefaultDeviceProfileConfiguration;
  41 +import org.thingsboard.server.common.data.device.profile.DefaultDeviceProfileTransportConfiguration;
  42 +import org.thingsboard.server.common.data.device.profile.DeviceProfileAlarm;
  43 +import org.thingsboard.server.common.data.device.profile.DeviceProfileData;
  44 +import org.thingsboard.server.common.data.device.profile.DisabledDeviceProfileProvisionConfiguration;
  45 +import org.thingsboard.server.common.data.device.profile.SimpleAlarmConditionSpec;
36 46 import org.thingsboard.server.common.data.id.CustomerId;
37 47 import org.thingsboard.server.common.data.id.DeviceId;
38 48 import org.thingsboard.server.common.data.id.DeviceProfileId;
... ... @@ -41,19 +51,30 @@ import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry;
41 51 import org.thingsboard.server.common.data.kv.BooleanDataEntry;
42 52 import org.thingsboard.server.common.data.kv.DoubleDataEntry;
43 53 import org.thingsboard.server.common.data.kv.LongDataEntry;
44   -import org.thingsboard.server.common.data.relation.EntityRelation;
  54 +import org.thingsboard.server.common.data.page.PageLink;
  55 +import org.thingsboard.server.common.data.query.BooleanFilterPredicate;
  56 +import org.thingsboard.server.common.data.query.DynamicValue;
  57 +import org.thingsboard.server.common.data.query.DynamicValueSourceType;
  58 +import org.thingsboard.server.common.data.query.EntityKey;
  59 +import org.thingsboard.server.common.data.query.EntityKeyType;
  60 +import org.thingsboard.server.common.data.query.EntityKeyValueType;
  61 +import org.thingsboard.server.common.data.query.FilterPredicateValue;
  62 +import org.thingsboard.server.common.data.query.KeyFilter;
  63 +import org.thingsboard.server.common.data.query.NumericFilterPredicate;
  64 +import org.thingsboard.server.common.data.rule.RuleChainType;
45 65 import org.thingsboard.server.common.data.security.Authority;
46 66 import org.thingsboard.server.common.data.security.DeviceCredentials;
47 67 import org.thingsboard.server.common.data.security.UserCredentials;
  68 +import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration;
  69 +import org.thingsboard.server.common.data.tenant.profile.TenantProfileData;
48 70 import org.thingsboard.server.common.data.widget.WidgetsBundle;
49   -import org.thingsboard.server.dao.asset.AssetService;
50 71 import org.thingsboard.server.dao.attributes.AttributesService;
51 72 import org.thingsboard.server.dao.customer.CustomerService;
52 73 import org.thingsboard.server.dao.device.DeviceCredentialsService;
53 74 import org.thingsboard.server.dao.device.DeviceProfileService;
54 75 import org.thingsboard.server.dao.device.DeviceService;
55 76 import org.thingsboard.server.dao.exception.DataValidationException;
56   -import org.thingsboard.server.dao.relation.RelationService;
  77 +import org.thingsboard.server.dao.rule.RuleChainService;
57 78 import org.thingsboard.server.dao.settings.AdminSettingsService;
58 79 import org.thingsboard.server.dao.tenant.TenantProfileService;
59 80 import org.thingsboard.server.dao.tenant.TenantService;
... ... @@ -61,6 +82,8 @@ import org.thingsboard.server.dao.user.UserService;
61 82 import org.thingsboard.server.dao.widget.WidgetsBundleService;
62 83
63 84 import java.util.Arrays;
  85 +import java.util.Collections;
  86 +import java.util.TreeMap;
64 87
65 88 @Service
66 89 @Profile("install")
... ... @@ -96,12 +119,6 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService {
96 119 private CustomerService customerService;
97 120
98 121 @Autowired
99   - private RelationService relationService;
100   -
101   - @Autowired
102   - private AssetService assetService;
103   -
104   - @Autowired
105 122 private DeviceService deviceService;
106 123
107 124 @Autowired
... ... @@ -113,6 +130,9 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService {
113 130 @Autowired
114 131 private DeviceCredentialsService deviceCredentialsService;
115 132
  133 + @Autowired
  134 + private RuleChainService ruleChainService;
  135 +
116 136 @Bean
117 137 protected BCryptPasswordEncoder passwordEncoder() {
118 138 return new BCryptPasswordEncoder();
... ... @@ -127,13 +147,16 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService {
127 147 public void createDefaultTenantProfiles() throws Exception {
128 148 tenantProfileService.findOrCreateDefaultTenantProfile(TenantId.SYS_TENANT_ID);
129 149
  150 + TenantProfileData tenantProfileData = new TenantProfileData();
  151 + tenantProfileData.setConfiguration(new DefaultTenantProfileConfiguration());
  152 +
130 153 TenantProfile isolatedTbCoreProfile = new TenantProfile();
131 154 isolatedTbCoreProfile.setDefault(false);
132 155 isolatedTbCoreProfile.setName("Isolated TB Core");
133   - isolatedTbCoreProfile.setProfileData(new TenantProfileData());
134 156 isolatedTbCoreProfile.setDescription("Isolated TB Core tenant profile");
135 157 isolatedTbCoreProfile.setIsolatedTbCore(true);
136 158 isolatedTbCoreProfile.setIsolatedTbRuleEngine(false);
  159 + isolatedTbCoreProfile.setProfileData(tenantProfileData);
137 160 try {
138 161 tenantProfileService.saveTenantProfile(TenantId.SYS_TENANT_ID, isolatedTbCoreProfile);
139 162 } catch (DataValidationException e) {
... ... @@ -143,10 +166,11 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService {
143 166 TenantProfile isolatedTbRuleEngineProfile = new TenantProfile();
144 167 isolatedTbRuleEngineProfile.setDefault(false);
145 168 isolatedTbRuleEngineProfile.setName("Isolated TB Rule Engine");
146   - isolatedTbRuleEngineProfile.setProfileData(new TenantProfileData());
147 169 isolatedTbRuleEngineProfile.setDescription("Isolated TB Rule Engine tenant profile");
148 170 isolatedTbRuleEngineProfile.setIsolatedTbCore(false);
149 171 isolatedTbRuleEngineProfile.setIsolatedTbRuleEngine(true);
  172 + isolatedTbRuleEngineProfile.setProfileData(tenantProfileData);
  173 +
150 174 try {
151 175 tenantProfileService.saveTenantProfile(TenantId.SYS_TENANT_ID, isolatedTbRuleEngineProfile);
152 176 } catch (DataValidationException e) {
... ... @@ -156,10 +180,11 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService {
156 180 TenantProfile isolatedTbCoreAndTbRuleEngineProfile = new TenantProfile();
157 181 isolatedTbCoreAndTbRuleEngineProfile.setDefault(false);
158 182 isolatedTbCoreAndTbRuleEngineProfile.setName("Isolated TB Core and TB Rule Engine");
159   - isolatedTbCoreAndTbRuleEngineProfile.setProfileData(new TenantProfileData());
160 183 isolatedTbCoreAndTbRuleEngineProfile.setDescription("Isolated TB Core and TB Rule Engine tenant profile");
161 184 isolatedTbCoreAndTbRuleEngineProfile.setIsolatedTbCore(true);
162 185 isolatedTbCoreAndTbRuleEngineProfile.setIsolatedTbRuleEngine(true);
  186 + isolatedTbCoreAndTbRuleEngineProfile.setProfileData(tenantProfileData);
  187 +
163 188 try {
164 189 tenantProfileService.saveTenantProfile(TenantId.SYS_TENANT_ID, isolatedTbCoreAndTbRuleEngineProfile);
165 190 } catch (DataValidationException e) {
... ... @@ -173,6 +198,7 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService {
173 198 generalSettings.setKey("general");
174 199 ObjectNode node = objectMapper.createObjectNode();
175 200 node.put("baseUrl", "http://localhost:8080");
  201 + node.put("prohibitDifferentUrl", true);
176 202 generalSettings.setJsonValue(node);
177 203 adminSettingsService.saveAdminSettings(TenantId.SYS_TENANT_ID, generalSettings);
178 204
... ... @@ -194,6 +220,11 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService {
194 220 }
195 221
196 222 @Override
  223 + public void createOAuth2Templates() throws Exception {
  224 + installScripts.createOAuth2Templates();
  225 + }
  226 +
  227 + @Override
197 228 public void loadDemoData() throws Exception {
198 229 Tenant demoTenant = new Tenant();
199 230 demoTenant.setRegion("Global");
... ... @@ -233,35 +264,149 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService {
233 264 createDevice(demoTenant.getId(), null, defaultDeviceProfile.getId(), "Raspberry Pi Demo Device", "RASPBERRY_PI_DEMO_TOKEN", "Demo device that is used in " +
234 265 "Raspberry Pi GPIO control sample application");
235 266
236   - Asset thermostatAlarms = new Asset();
237   - thermostatAlarms.setTenantId(demoTenant.getId());
238   - thermostatAlarms.setName("Thermostat Alarms");
239   - thermostatAlarms.setType("AlarmPropagationAsset");
240   - thermostatAlarms = assetService.saveAsset(thermostatAlarms);
241   -
242   - DeviceProfile thermostatDeviceProfile = this.deviceProfileService.findOrCreateDeviceProfile(demoTenant.getId(), "thermostat");
243   -
244   - DeviceId t1Id = createDevice(demoTenant.getId(), null, thermostatDeviceProfile.getId(), "Thermostat T1", "T1_TEST_TOKEN", "Demo device for Thermostats dashboard").getId();
245   - DeviceId t2Id = createDevice(demoTenant.getId(), null, thermostatDeviceProfile.getId(), "Thermostat T2", "T2_TEST_TOKEN", "Demo device for Thermostats dashboard").getId();
246   -
247   - relationService.saveRelation(thermostatAlarms.getTenantId(), new EntityRelation(thermostatAlarms.getId(), t1Id, "ToAlarmPropagationAsset"));
248   - relationService.saveRelation(thermostatAlarms.getTenantId(), new EntityRelation(thermostatAlarms.getId(), t2Id, "ToAlarmPropagationAsset"));
  267 + DeviceProfile thermostatDeviceProfile = new DeviceProfile();
  268 + thermostatDeviceProfile.setTenantId(demoTenant.getId());
  269 + thermostatDeviceProfile.setDefault(false);
  270 + thermostatDeviceProfile.setName("thermostat");
  271 + thermostatDeviceProfile.setType(DeviceProfileType.DEFAULT);
  272 + thermostatDeviceProfile.setTransportType(DeviceTransportType.DEFAULT);
  273 + thermostatDeviceProfile.setProvisionType(DeviceProfileProvisionType.DISABLED);
  274 + thermostatDeviceProfile.setDescription("Thermostat device profile");
  275 + thermostatDeviceProfile.setDefaultRuleChainId(ruleChainService.findTenantRuleChainsByType(
  276 + demoTenant.getId(), RuleChainType.CORE, new PageLink(1, 0, "Thermostat")).getData().get(0).getId());
  277 +
  278 + DeviceProfileData deviceProfileData = new DeviceProfileData();
  279 + DefaultDeviceProfileConfiguration configuration = new DefaultDeviceProfileConfiguration();
  280 + DefaultDeviceProfileTransportConfiguration transportConfiguration = new DefaultDeviceProfileTransportConfiguration();
  281 + DisabledDeviceProfileProvisionConfiguration provisionConfiguration = new DisabledDeviceProfileProvisionConfiguration(null);
  282 + deviceProfileData.setConfiguration(configuration);
  283 + deviceProfileData.setTransportConfiguration(transportConfiguration);
  284 + deviceProfileData.setProvisionConfiguration(provisionConfiguration);
  285 + thermostatDeviceProfile.setProfileData(deviceProfileData);
  286 +
  287 + DeviceProfileAlarm highTemperature = new DeviceProfileAlarm();
  288 + highTemperature.setId("highTemperatureAlarmID");
  289 + highTemperature.setAlarmType("High Temperature");
  290 + AlarmRule temperatureRule = new AlarmRule();
  291 + AlarmCondition temperatureCondition = new AlarmCondition();
  292 + temperatureCondition.setSpec(new SimpleAlarmConditionSpec());
  293 +
  294 + KeyFilter temperatureAlarmFlagAttributeFilter = new KeyFilter();
  295 + temperatureAlarmFlagAttributeFilter.setKey(new EntityKey(EntityKeyType.ATTRIBUTE, "temperatureAlarmFlag"));
  296 + temperatureAlarmFlagAttributeFilter.setValueType(EntityKeyValueType.BOOLEAN);
  297 + BooleanFilterPredicate temperatureAlarmFlagAttributePredicate = new BooleanFilterPredicate();
  298 + temperatureAlarmFlagAttributePredicate.setOperation(BooleanFilterPredicate.BooleanOperation.EQUAL);
  299 + temperatureAlarmFlagAttributePredicate.setValue(new FilterPredicateValue<>(Boolean.TRUE));
  300 + temperatureAlarmFlagAttributeFilter.setPredicate(temperatureAlarmFlagAttributePredicate);
  301 +
  302 + KeyFilter temperatureTimeseriesFilter = new KeyFilter();
  303 + temperatureTimeseriesFilter.setKey(new EntityKey(EntityKeyType.TIME_SERIES, "temperature"));
  304 + temperatureTimeseriesFilter.setValueType(EntityKeyValueType.NUMERIC);
  305 + NumericFilterPredicate temperatureTimeseriesFilterPredicate = new NumericFilterPredicate();
  306 + temperatureTimeseriesFilterPredicate.setOperation(NumericFilterPredicate.NumericOperation.GREATER);
  307 + FilterPredicateValue<Double> temperatureTimeseriesPredicateValue =
  308 + new FilterPredicateValue<>(25.0, null,
  309 + new DynamicValue<>(DynamicValueSourceType.CURRENT_DEVICE, "temperatureAlarmThreshold"));
  310 + temperatureTimeseriesFilterPredicate.setValue(temperatureTimeseriesPredicateValue);
  311 + temperatureTimeseriesFilter.setPredicate(temperatureTimeseriesFilterPredicate);
  312 + temperatureCondition.setCondition(Arrays.asList(temperatureAlarmFlagAttributeFilter, temperatureTimeseriesFilter));
  313 + temperatureRule.setAlarmDetails("Current temperature = ${temperature}");
  314 + temperatureRule.setCondition(temperatureCondition);
  315 + highTemperature.setCreateRules(new TreeMap<>(Collections.singletonMap(AlarmSeverity.MAJOR, temperatureRule)));
  316 +
  317 + AlarmRule clearTemperatureRule = new AlarmRule();
  318 + AlarmCondition clearTemperatureCondition = new AlarmCondition();
  319 + clearTemperatureCondition.setSpec(new SimpleAlarmConditionSpec());
  320 +
  321 + KeyFilter clearTemperatureTimeseriesFilter = new KeyFilter();
  322 + clearTemperatureTimeseriesFilter.setKey(new EntityKey(EntityKeyType.TIME_SERIES, "temperature"));
  323 + clearTemperatureTimeseriesFilter.setValueType(EntityKeyValueType.NUMERIC);
  324 + NumericFilterPredicate clearTemperatureTimeseriesFilterPredicate = new NumericFilterPredicate();
  325 + clearTemperatureTimeseriesFilterPredicate.setOperation(NumericFilterPredicate.NumericOperation.LESS_OR_EQUAL);
  326 + FilterPredicateValue<Double> clearTemperatureTimeseriesPredicateValue =
  327 + new FilterPredicateValue<>(25.0, null,
  328 + new DynamicValue<>(DynamicValueSourceType.CURRENT_DEVICE, "temperatureAlarmThreshold"));
  329 +
  330 + clearTemperatureTimeseriesFilterPredicate.setValue(clearTemperatureTimeseriesPredicateValue);
  331 + clearTemperatureTimeseriesFilter.setPredicate(clearTemperatureTimeseriesFilterPredicate);
  332 + clearTemperatureCondition.setCondition(Collections.singletonList(clearTemperatureTimeseriesFilter));
  333 + clearTemperatureRule.setCondition(clearTemperatureCondition);
  334 + clearTemperatureRule.setAlarmDetails("Current temperature = ${temperature}");
  335 + highTemperature.setClearRule(clearTemperatureRule);
  336 +
  337 + DeviceProfileAlarm lowHumidity = new DeviceProfileAlarm();
  338 + lowHumidity.setId("lowHumidityAlarmID");
  339 + lowHumidity.setAlarmType("Low Humidity");
  340 + AlarmRule humidityRule = new AlarmRule();
  341 + AlarmCondition humidityCondition = new AlarmCondition();
  342 + humidityCondition.setSpec(new SimpleAlarmConditionSpec());
  343 +
  344 + KeyFilter humidityAlarmFlagAttributeFilter = new KeyFilter();
  345 + humidityAlarmFlagAttributeFilter.setKey(new EntityKey(EntityKeyType.ATTRIBUTE, "humidityAlarmFlag"));
  346 + humidityAlarmFlagAttributeFilter.setValueType(EntityKeyValueType.BOOLEAN);
  347 + BooleanFilterPredicate humidityAlarmFlagAttributePredicate = new BooleanFilterPredicate();
  348 + humidityAlarmFlagAttributePredicate.setOperation(BooleanFilterPredicate.BooleanOperation.EQUAL);
  349 + humidityAlarmFlagAttributePredicate.setValue(new FilterPredicateValue<>(Boolean.TRUE));
  350 + humidityAlarmFlagAttributeFilter.setPredicate(humidityAlarmFlagAttributePredicate);
  351 +
  352 + KeyFilter humidityTimeseriesFilter = new KeyFilter();
  353 + humidityTimeseriesFilter.setKey(new EntityKey(EntityKeyType.TIME_SERIES, "humidity"));
  354 + humidityTimeseriesFilter.setValueType(EntityKeyValueType.NUMERIC);
  355 + NumericFilterPredicate humidityTimeseriesFilterPredicate = new NumericFilterPredicate();
  356 + humidityTimeseriesFilterPredicate.setOperation(NumericFilterPredicate.NumericOperation.LESS);
  357 + FilterPredicateValue<Double> humidityTimeseriesPredicateValue =
  358 + new FilterPredicateValue<>(60.0, null,
  359 + new DynamicValue<>(DynamicValueSourceType.CURRENT_DEVICE, "humidityAlarmThreshold"));
  360 + humidityTimeseriesFilterPredicate.setValue(humidityTimeseriesPredicateValue);
  361 + humidityTimeseriesFilter.setPredicate(humidityTimeseriesFilterPredicate);
  362 + humidityCondition.setCondition(Arrays.asList(humidityAlarmFlagAttributeFilter, humidityTimeseriesFilter));
  363 +
  364 + humidityRule.setCondition(humidityCondition);
  365 + humidityRule.setAlarmDetails("Current humidity = ${humidity}");
  366 + lowHumidity.setCreateRules(new TreeMap<>(Collections.singletonMap(AlarmSeverity.MINOR, humidityRule)));
  367 +
  368 + AlarmRule clearHumidityRule = new AlarmRule();
  369 + AlarmCondition clearHumidityCondition = new AlarmCondition();
  370 + clearHumidityCondition.setSpec(new SimpleAlarmConditionSpec());
  371 +
  372 + KeyFilter clearHumidityTimeseriesFilter = new KeyFilter();
  373 + clearHumidityTimeseriesFilter.setKey(new EntityKey(EntityKeyType.TIME_SERIES, "humidity"));
  374 + clearHumidityTimeseriesFilter.setValueType(EntityKeyValueType.NUMERIC);
  375 + NumericFilterPredicate clearHumidityTimeseriesFilterPredicate = new NumericFilterPredicate();
  376 + clearHumidityTimeseriesFilterPredicate.setOperation(NumericFilterPredicate.NumericOperation.GREATER_OR_EQUAL);
  377 + FilterPredicateValue<Double> clearHumidityTimeseriesPredicateValue =
  378 + new FilterPredicateValue<>(60.0, null,
  379 + new DynamicValue<>(DynamicValueSourceType.CURRENT_DEVICE, "humidityAlarmThreshold"));
  380 +
  381 + clearHumidityTimeseriesFilterPredicate.setValue(clearHumidityTimeseriesPredicateValue);
  382 + clearHumidityTimeseriesFilter.setPredicate(clearHumidityTimeseriesFilterPredicate);
  383 + clearHumidityCondition.setCondition(Collections.singletonList(clearHumidityTimeseriesFilter));
  384 + clearHumidityRule.setCondition(clearHumidityCondition);
  385 + clearHumidityRule.setAlarmDetails("Current humidity = ${humidity}");
  386 + lowHumidity.setClearRule(clearHumidityRule);
  387 +
  388 + deviceProfileData.setAlarms(Arrays.asList(highTemperature, lowHumidity));
  389 +
  390 + DeviceProfile savedThermostatDeviceProfile = deviceProfileService.saveDeviceProfile(thermostatDeviceProfile);
  391 +
  392 + DeviceId t1Id = createDevice(demoTenant.getId(), null, savedThermostatDeviceProfile.getId(), "Thermostat T1", "T1_TEST_TOKEN", "Demo device for Thermostats dashboard").getId();
  393 + DeviceId t2Id = createDevice(demoTenant.getId(), null, savedThermostatDeviceProfile.getId(), "Thermostat T2", "T2_TEST_TOKEN", "Demo device for Thermostats dashboard").getId();
249 394
250 395 attributesService.save(demoTenant.getId(), t1Id, DataConstants.SERVER_SCOPE,
251 396 Arrays.asList(new BaseAttributeKvEntry(System.currentTimeMillis(), new DoubleDataEntry("latitude", 37.3948)),
252 397 new BaseAttributeKvEntry(System.currentTimeMillis(), new DoubleDataEntry("longitude", -122.1503)),
253   - new BaseAttributeKvEntry(System.currentTimeMillis(), new BooleanDataEntry("alarmTemperature", true)),
254   - new BaseAttributeKvEntry(System.currentTimeMillis(), new BooleanDataEntry("alarmHumidity", true)),
255   - new BaseAttributeKvEntry(System.currentTimeMillis(), new LongDataEntry("thresholdTemperature", (long) 20)),
256   - new BaseAttributeKvEntry(System.currentTimeMillis(), new LongDataEntry("thresholdHumidity", (long) 50))));
  398 + new BaseAttributeKvEntry(System.currentTimeMillis(), new BooleanDataEntry("temperatureAlarmFlag", true)),
  399 + new BaseAttributeKvEntry(System.currentTimeMillis(), new BooleanDataEntry("humidityAlarmFlag", true)),
  400 + new BaseAttributeKvEntry(System.currentTimeMillis(), new LongDataEntry("temperatureAlarmThreshold", (long) 20)),
  401 + new BaseAttributeKvEntry(System.currentTimeMillis(), new LongDataEntry("humidityAlarmThreshold", (long) 50))));
257 402
258 403 attributesService.save(demoTenant.getId(), t2Id, DataConstants.SERVER_SCOPE,
259 404 Arrays.asList(new BaseAttributeKvEntry(System.currentTimeMillis(), new DoubleDataEntry("latitude", 37.493801)),
260 405 new BaseAttributeKvEntry(System.currentTimeMillis(), new DoubleDataEntry("longitude", -121.948769)),
261   - new BaseAttributeKvEntry(System.currentTimeMillis(), new BooleanDataEntry("alarmTemperature", true)),
262   - new BaseAttributeKvEntry(System.currentTimeMillis(), new BooleanDataEntry("alarmHumidity", true)),
263   - new BaseAttributeKvEntry(System.currentTimeMillis(), new LongDataEntry("thresholdTemperature", (long) 25)),
264   - new BaseAttributeKvEntry(System.currentTimeMillis(), new LongDataEntry("thresholdHumidity", (long) 30))));
  406 + new BaseAttributeKvEntry(System.currentTimeMillis(), new BooleanDataEntry("temperatureAlarmFlag", true)),
  407 + new BaseAttributeKvEntry(System.currentTimeMillis(), new BooleanDataEntry("humidityAlarmFlag", true)),
  408 + new BaseAttributeKvEntry(System.currentTimeMillis(), new LongDataEntry("temperatureAlarmThreshold", (long) 25)),
  409 + new BaseAttributeKvEntry(System.currentTimeMillis(), new LongDataEntry("humidityAlarmThreshold", (long) 30))));
265 410
266 411 installScripts.loadDashboards(demoTenant.getId(), null);
267 412 }
... ...
... ... @@ -26,11 +26,13 @@ import org.thingsboard.server.common.data.id.CustomerId;
26 26 import org.thingsboard.server.common.data.id.EntityId;
27 27 import org.thingsboard.server.common.data.id.RuleChainId;
28 28 import org.thingsboard.server.common.data.id.TenantId;
  29 +import org.thingsboard.server.common.data.oauth2.OAuth2ClientRegistrationTemplate;
29 30 import org.thingsboard.server.common.data.rule.RuleChain;
30 31 import org.thingsboard.server.common.data.rule.RuleChainMetaData;
31 32 import org.thingsboard.server.common.data.widget.WidgetType;
32 33 import org.thingsboard.server.common.data.widget.WidgetsBundle;
33 34 import org.thingsboard.server.dao.dashboard.DashboardService;
  35 +import org.thingsboard.server.dao.oauth2.OAuth2ConfigTemplateService;
34 36 import org.thingsboard.server.dao.rule.RuleChainService;
35 37 import org.thingsboard.server.dao.widget.WidgetTypeService;
36 38 import org.thingsboard.server.dao.widget.WidgetsBundleService;
... ... @@ -40,6 +42,7 @@ import java.nio.file.DirectoryStream;
40 42 import java.nio.file.Files;
41 43 import java.nio.file.Path;
42 44 import java.nio.file.Paths;
  45 +import java.util.Optional;
43 46
44 47 import static org.thingsboard.server.service.install.DatabaseHelper.objectMapper;
45 48
... ... @@ -61,6 +64,7 @@ public class InstallScripts {
61 64 public static final String DEMO_DIR = "demo";
62 65 public static final String RULE_CHAINS_DIR = "rule_chains";
63 66 public static final String WIDGET_BUNDLES_DIR = "widget_bundles";
  67 + public static final String OAUTH2_CONFIG_TEMPLATES_DIR = "oauth2_config_templates";
64 68 public static final String DASHBOARDS_DIR = "dashboards";
65 69
66 70 public static final String JSON_EXT = ".json";
... ... @@ -80,6 +84,9 @@ public class InstallScripts {
80 84 @Autowired
81 85 private WidgetsBundleService widgetsBundleService;
82 86
  87 + @Autowired
  88 + private OAuth2ConfigTemplateService oAuth2TemplateService;
  89 +
83 90 public Path getTenantRuleChainsDir() {
84 91 return Paths.get(getDataDir(), JSON_DIR, TENANT_DIR, RULE_CHAINS_DIR);
85 92 }
... ... @@ -146,12 +153,6 @@ public class InstallScripts {
146 153 return ruleChain;
147 154 }
148 155
149   -
150   - public void createDefaultEdgeRuleChains(TenantId tenantId) {
151   - Path tenantChainsDir = getTenantRuleChainsDir();
152   - loadRuleChainFromFile(tenantId, tenantChainsDir.resolve("edge_root_rule_chain.json"));
153   - }
154   -
155 156 public void loadSystemWidgets() throws Exception {
156 157 Path widgetBundlesDir = Paths.get(getDataDir(), JSON_DIR, SYSTEM_DIR, WIDGET_BUNDLES_DIR);
157 158 try (DirectoryStream<Path> dirStream = Files.newDirectoryStream(widgetBundlesDir, path -> path.toString().endsWith(JSON_EXT))) {
... ... @@ -206,50 +207,42 @@ public class InstallScripts {
206 207 }
207 208 }
208 209
209   -
210 210 public void loadDemoRuleChains(TenantId tenantId) throws Exception {
211   - Path ruleChainsDir = Paths.get(getDataDir(), JSON_DIR, DEMO_DIR, RULE_CHAINS_DIR);
212 211 try {
213   - JsonNode ruleChainJson = objectMapper.readTree(ruleChainsDir.resolve("thermostat_alarms.json").toFile());
214   - RuleChain ruleChain = objectMapper.treeToValue(ruleChainJson.get("ruleChain"), RuleChain.class);
215   - RuleChainMetaData ruleChainMetaData = objectMapper.treeToValue(ruleChainJson.get("metadata"), RuleChainMetaData.class);
216   - ruleChain.setTenantId(tenantId);
217   - ruleChain = ruleChainService.saveRuleChain(ruleChain);
218   - ruleChainMetaData.setRuleChainId(ruleChain.getId());
219   - ruleChainService.saveRuleChainMetaData(new TenantId(EntityId.NULL_UUID), ruleChainMetaData);
220   -
221   - JsonNode rootChainJson = objectMapper.readTree(ruleChainsDir.resolve("root_rule_chain.json").toFile());
222   - RuleChain rootChain = objectMapper.treeToValue(rootChainJson.get("ruleChain"), RuleChain.class);
223   - RuleChainMetaData rootChainMetaData = objectMapper.treeToValue(rootChainJson.get("metadata"), RuleChainMetaData.class);
224   -
225   - RuleChainId thermostatsRuleChainId = ruleChain.getId();
226   - rootChainMetaData.getRuleChainConnections().forEach(connection -> connection.setTargetRuleChainId(thermostatsRuleChainId));
227   - rootChain.setTenantId(tenantId);
228   - rootChain = ruleChainService.saveRuleChain(rootChain);
229   - rootChainMetaData.setRuleChainId(rootChain.getId());
230   - ruleChainService.saveRuleChainMetaData(new TenantId(EntityId.NULL_UUID), rootChainMetaData);
231   -
232   - loadRuleChainFromFile(tenantId, ruleChainsDir.resolve("edge_root_rule_chain.json"));
  212 + createDefaultRuleChains(tenantId);
  213 + createDefaultRuleChain(tenantId, "Thermostat");
  214 + createDefaultEdgeRuleChains(tenantId);
233 215 } catch (Exception e) {
234 216 log.error("Unable to load dashboard from json", e);
235 217 throw new RuntimeException("Unable to load dashboard from json", e);
236 218 }
237 219 }
238 220
239   - private void loadRuleChainFromFile(TenantId tenantId, Path ruleChainPath) {
240   - try {
241   - JsonNode ruleChainJson = objectMapper.readTree(ruleChainPath.toFile());
242   - RuleChain ruleChain = objectMapper.treeToValue(ruleChainJson.get("ruleChain"), RuleChain.class);
243   - RuleChainMetaData ruleChainMetaData = objectMapper.treeToValue(ruleChainJson.get("metadata"), RuleChainMetaData.class);
244   -
245   - ruleChain.setTenantId(tenantId);
246   - ruleChain = ruleChainService.saveRuleChain(ruleChain);
  221 + public void createDefaultEdgeRuleChains(TenantId tenantId) throws IOException {
  222 + Path tenantChainsDir = getTenantRuleChainsDir();
  223 + createRuleChainFromFile(tenantId, tenantChainsDir.resolve("edge_root_rule_chain.json"), null);
  224 + }
247 225
248   - ruleChainMetaData.setRuleChainId(ruleChain.getId());
249   - ruleChainService.saveRuleChainMetaData(new TenantId(EntityId.NULL_UUID), ruleChainMetaData);
250   - } catch (Exception e) {
251   - log.error("Unable to load rule chain from json: [{}]", ruleChainPath.toString());
252   - throw new RuntimeException("Unable to load rule chain from json", e);
  226 + public void createOAuth2Templates() throws Exception {
  227 + Path oauth2ConfigTemplatesDir = Paths.get(getDataDir(), JSON_DIR, SYSTEM_DIR, OAUTH2_CONFIG_TEMPLATES_DIR);
  228 + try (DirectoryStream<Path> dirStream = Files.newDirectoryStream(oauth2ConfigTemplatesDir, path -> path.toString().endsWith(JSON_EXT))) {
  229 + dirStream.forEach(
  230 + path -> {
  231 + try {
  232 + JsonNode oauth2ConfigTemplateJson = objectMapper.readTree(path.toFile());
  233 + OAuth2ClientRegistrationTemplate clientRegistrationTemplate = objectMapper.treeToValue(oauth2ConfigTemplateJson, OAuth2ClientRegistrationTemplate.class);
  234 + Optional<OAuth2ClientRegistrationTemplate> existingClientRegistrationTemplate =
  235 + oAuth2TemplateService.findClientRegistrationTemplateByProviderId(clientRegistrationTemplate.getProviderId());
  236 + if (existingClientRegistrationTemplate.isPresent()) {
  237 + clientRegistrationTemplate.setId(existingClientRegistrationTemplate.get().getId());
  238 + }
  239 + oAuth2TemplateService.saveClientRegistrationTemplate(clientRegistrationTemplate);
  240 + } catch (Exception e) {
  241 + log.error("Unable to load oauth2 config templates from json: [{}]", path.toString());
  242 + throw new RuntimeException("Unable to load oauth2 config templates from json", e);
  243 + }
  244 + }
  245 + );
253 246 }
254 247 }
255 248 }
... ...
... ... @@ -28,6 +28,7 @@ import org.thingsboard.server.dao.dashboard.DashboardService;
28 28 import org.thingsboard.server.dao.device.DeviceProfileService;
29 29 import org.thingsboard.server.dao.device.DeviceService;
30 30 import org.thingsboard.server.dao.tenant.TenantService;
  31 +import org.thingsboard.server.dao.usagerecord.ApiUsageStateService;
31 32 import org.thingsboard.server.service.install.sql.SqlDbHelper;
32 33
33 34 import java.nio.charset.Charset;
... ... @@ -96,6 +97,9 @@ public class SqlDatabaseUpgradeService implements DatabaseEntitiesUpgradeService
96 97 @Autowired
97 98 private DeviceProfileService deviceProfileService;
98 99
  100 + @Autowired
  101 + private ApiUsageStateService apiUsageStateService;
  102 +
99 103
100 104 @Override
101 105 public void upgradeDatabase(String fromVersion) throws Exception {
... ... @@ -364,6 +368,24 @@ public class SqlDatabaseUpgradeService implements DatabaseEntitiesUpgradeService
364 368 } catch (Exception e) {
365 369 }
366 370
  371 + try {
  372 + conn.createStatement().execute("CREATE TABLE IF NOT EXISTS api_usage_state (" +
  373 + " id uuid NOT NULL CONSTRAINT usage_record_pkey PRIMARY KEY," +
  374 + " created_time bigint NOT NULL," +
  375 + " tenant_id uuid," +
  376 + " entity_type varchar(32)," +
  377 + " entity_id uuid," +
  378 + " transport varchar(32)," +
  379 + " db_storage varchar(32)," +
  380 + " re_exec varchar(32)," +
  381 + " js_exec varchar(32)," +
  382 + " email_exec varchar(32)," +
  383 + " sms_exec varchar(32)," +
  384 + " CONSTRAINT api_usage_state_unq_key UNIQUE (tenant_id, entity_id)\n" +
  385 + ");");
  386 + } catch (Exception e) {
  387 + }
  388 +
367 389 schemaUpdateFile = Paths.get(installScripts.getDataDir(), "upgrade", "3.1.1", "schema_update_before.sql");
368 390 loadSql(schemaUpdateFile, conn);
369 391
... ... @@ -379,6 +401,10 @@ public class SqlDatabaseUpgradeService implements DatabaseEntitiesUpgradeService
379 401 do {
380 402 pageData = tenantService.findTenants(pageLink);
381 403 for (Tenant tenant : pageData.getData()) {
  404 + try {
  405 + apiUsageStateService.createDefaultApiUsageState(tenant.getId());
  406 + } catch (Exception e) {
  407 + }
382 408 List<EntitySubtype> deviceTypes = deviceService.findDeviceTypesByTenantId(tenant.getId()).get();
383 409 try {
384 410 deviceProfileService.createDefaultDeviceProfile(tenant.getId());
... ...