Commit 749fbd68a5694510c37848f5994d3afbdf678dbd

Authored by Dmitriymush
2 parents 2e624994 5d30243e
Showing 56 changed files with 1725 additions and 1628 deletions

Too many changes to show.

To preserve performance only 56 of 372 files are displayed.

... ... @@ -33,3 +33,5 @@ pom.xml.versionsBackup
33 33 **/.env
34 34 .instance_id
35 35 rebuild-docker.sh
  36 +.run/**
  37 +.run
\ No newline at end of file
... ...
... ... @@ -20,7 +20,7 @@
20 20 <modelVersion>4.0.0</modelVersion>
21 21 <parent>
22 22 <groupId>org.thingsboard</groupId>
23   - <version>3.2.0-SNAPSHOT</version>
  23 + <version>3.2.1-SNAPSHOT</version>
24 24 <artifactId>thingsboard</artifactId>
25 25 </parent>
26 26 <artifactId>application</artifactId>
... ... @@ -146,10 +146,6 @@
146 146 <artifactId>spring-boot-starter-websocket</artifactId>
147 147 </dependency>
148 148 <dependency>
149   - <groupId>org.springframework.cloud</groupId>
150   - <artifactId>spring-cloud-starter-oauth2</artifactId>
151   - </dependency>
152   - <dependency>
153 149 <groupId>org.springframework.security</groupId>
154 150 <artifactId>spring-security-oauth2-client</artifactId>
155 151 </dependency>
... ... @@ -198,6 +194,14 @@
198 194 <artifactId>javax.mail</artifactId>
199 195 </dependency>
200 196 <dependency>
  197 + <groupId>com.twilio.sdk</groupId>
  198 + <artifactId>twilio</artifactId>
  199 + </dependency>
  200 + <dependency>
  201 + <groupId>com.amazonaws</groupId>
  202 + <artifactId>aws-java-sdk-sns</artifactId>
  203 + </dependency>
  204 + <dependency>
201 205 <groupId>org.apache.curator</groupId>
202 206 <artifactId>curator-recipes</artifactId>
203 207 </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   - "firstRuleNodeId": null,
6   - "root": true,
7   - "debugMode": false,
8   - "configuration": null
9   - },
10   - "metadata": {
11   - "firstNodeIndex": 3,
12   - "nodes": [
13   - {
14   - "additionalInfo": {
15   - "layoutX": 1069,
16   - "layoutY": 267
17   - },
18   - "type": "org.thingsboard.rule.engine.filter.TbJsFilterNode",
19   - "name": "Is Thermostat?",
20   - "debugMode": false,
21   - "configuration": {
22   - "jsScript": "return msg.id.entityType === \"DEVICE\" && msg.type === \"thermostat\";"
23   - }
24   - },
25   - {
26   - "additionalInfo": {
27   - "layoutX": 824,
28   - "layoutY": 156
29   - },
30   - "type": "org.thingsboard.rule.engine.telemetry.TbMsgTimeseriesNode",
31   - "name": "Save Timeseries",
32   - "debugMode": false,
33   - "configuration": {
34   - "defaultTTL": 0
35   - }
36   - },
37   - {
38   - "additionalInfo": {
39   - "layoutX": 825,
40   - "layoutY": 52
41   - },
42   - "type": "org.thingsboard.rule.engine.telemetry.TbMsgAttributesNode",
43   - "name": "Save Client Attributes",
44   - "debugMode": false,
45   - "configuration": {
46   - "scope": "CLIENT_SCOPE",
47   - "notifyDevice": "false"
48   - }
49   - },
50   - {
51   - "additionalInfo": {
52   - "layoutX": 347,
53   - "layoutY": 149
54   - },
55   - "type": "org.thingsboard.rule.engine.filter.TbMsgTypeSwitchNode",
56   - "name": "Message Type Switch",
57   - "debugMode": false,
58   - "configuration": {
59   - "version": 0
60   - }
61   - },
62   - {
63   - "additionalInfo": {
64   - "layoutX": 839,
65   - "layoutY": 345
66   - },
67   - "type": "org.thingsboard.rule.engine.action.TbLogNode",
68   - "name": "Log RPC from Device",
69   - "debugMode": false,
70   - "configuration": {
71   - "jsScript": "return '\\nIncoming message:\\n' + JSON.stringify(msg) + '\\nIncoming metadata:\\n' + JSON.stringify(metadata);"
72   - }
73   - },
74   - {
75   - "additionalInfo": {
76   - "layoutX": 832,
77   - "layoutY": 407
78   - },
79   - "type": "org.thingsboard.rule.engine.action.TbLogNode",
80   - "name": "Log Other",
81   - "debugMode": false,
82   - "configuration": {
83   - "jsScript": "return '\\nIncoming message:\\n' + JSON.stringify(msg) + '\\nIncoming metadata:\\n' + JSON.stringify(metadata);"
84   - }
85   - },
86   - {
87   - "additionalInfo": {
88   - "layoutX": 825,
89   - "layoutY": 468
90   - },
91   - "type": "org.thingsboard.rule.engine.rpc.TbSendRPCRequestNode",
92   - "name": "RPC Call Request",
93   - "debugMode": false,
94   - "configuration": {
95   - "timeoutInSeconds": 60
96   - }
97   - },
98   - {
99   - "additionalInfo": {
100   - "layoutX": 1069,
101   - "layoutY": 90
102   - },
103   - "type": "org.thingsboard.rule.engine.filter.TbJsFilterNode",
104   - "name": "Is Thermostat?",
105   - "debugMode": false,
106   - "configuration": {
107   - "jsScript": "return metadata[\"deviceType\"] === \"thermostat\";"
108   - }
109   - },
110   - {
111   - "additionalInfo": {
112   - "layoutX": 1090,
113   - "layoutY": 360
114   - },
115   - "type": "org.thingsboard.rule.engine.action.TbCreateRelationNode",
116   - "name": "Relate to Asset",
117   - "debugMode": false,
118   - "configuration": {
119   - "direction": "FROM",
120   - "relationType": "ToAlarmPropagationAsset",
121   - "entityType": "ASSET",
122   - "entityNamePattern": "Thermostat Alarms",
123   - "entityTypePattern": "AlarmPropagationAsset",
124   - "entityCacheExpiration": 300,
125   - "createEntityIfNotExists": true,
126   - "changeOriginatorToRelatedEntity": false,
127   - "removeCurrentRelations": false
128   - }
129   - }
130   - ],
131   - "connections": [
132   - {
133   - "fromIndex": 0,
134   - "toIndex": 8,
135   - "type": "True"
136   - },
137   - {
138   - "fromIndex": 1,
139   - "toIndex": 7,
140   - "type": "Success"
141   - },
142   - {
143   - "fromIndex": 3,
144   - "toIndex": 5,
145   - "type": "Other"
146   - },
147   - {
148   - "fromIndex": 3,
149   - "toIndex": 2,
150   - "type": "Post attributes"
151   - },
152   - {
153   - "fromIndex": 3,
154   - "toIndex": 1,
155   - "type": "Post telemetry"
156   - },
157   - {
158   - "fromIndex": 3,
159   - "toIndex": 4,
160   - "type": "RPC Request from Device"
161   - },
162   - {
163   - "fromIndex": 3,
164   - "toIndex": 6,
165   - "type": "RPC Request to Device"
166   - },
167   - {
168   - "fromIndex": 3,
169   - "toIndex": 0,
170   - "type": "Entity Created"
171   - }
172   - ],
173   - "ruleChainConnections": [
174   - {
175   - "fromIndex": 7,
176   - "targetRuleChainId": {
177   - "entityType": "RULE_CHAIN",
178   - "id": "25e26570-89ed-11ea-a650-cd6e14e633bd"
179   - },
180   - "additionalInfo": {
181   - "layoutX": 1109,
182   - "layoutY": 182,
183   - "ruleChainNodeId": "rule-chain-node-10"
184   - },
185   - "type": "True"
186   - }
187   - ]
188   - }
189   -}
\ No newline at end of file
1   -{
2   - "ruleChain": {
3   - "additionalInfo": null,
4   - "name": "Thermostat Alarms",
5   - "firstRuleNodeId": null,
6   - "root": false,
7   - "debugMode": false,
8   - "configuration": null
9   - },
10   - "metadata": {
11   - "firstNodeIndex": 5,
12   - "nodes": [
13   - {
14   - "additionalInfo": {
15   - "layoutX": 929,
16   - "layoutY": 67
17   - },
18   - "type": "org.thingsboard.rule.engine.action.TbCreateAlarmNode",
19   - "name": "Create Temp Alarm",
20   - "debugMode": false,
21   - "configuration": {
22   - "alarmType": "High Temperature",
23   - "alarmDetailsBuildJs": "var details = {};\nif (metadata.prevAlarmDetails) {\n details = JSON.parse(metadata.prevAlarmDetails);\n}\ndetails.triggerValue = msg.temperature;\nreturn details;",
24   - "severity": "MAJOR",
25   - "propagate": true,
26   - "useMessageAlarmData": false,
27   - "relationTypes": [
28   - "ToAlarmPropagationAsset"
29   - ]
30   - }
31   - },
32   - {
33   - "additionalInfo": {
34   - "layoutX": 930,
35   - "layoutY": 201
36   - },
37   - "type": "org.thingsboard.rule.engine.action.TbClearAlarmNode",
38   - "name": "Clear Temp Alarm",
39   - "debugMode": false,
40   - "configuration": {
41   - "alarmType": "High Temperature",
42   - "alarmDetailsBuildJs": "var details = {};\nif (metadata.prevAlarmDetails) {\n details = JSON.parse(metadata.prevAlarmDetails);\n}\nreturn details;"
43   - }
44   - },
45   - {
46   - "additionalInfo": {
47   - "layoutX": 930,
48   - "layoutY": 131
49   - },
50   - "type": "org.thingsboard.rule.engine.action.TbCreateAlarmNode",
51   - "name": "Create Humidity Alarm",
52   - "debugMode": false,
53   - "configuration": {
54   - "alarmType": "Low Humidity",
55   - "alarmDetailsBuildJs": "var details = {};\nif (metadata.prevAlarmDetails) {\n details = JSON.parse(metadata.prevAlarmDetails);\n}\ndetails.triggerValue = msg.humidity;\nreturn details;",
56   - "severity": "MINOR",
57   - "propagate": true,
58   - "useMessageAlarmData": false,
59   - "relationTypes": [
60   - "ToAlarmPropagationAsset"
61   - ]
62   - }
63   - },
64   - {
65   - "additionalInfo": {
66   - "layoutX": 929,
67   - "layoutY": 275
68   - },
69   - "type": "org.thingsboard.rule.engine.action.TbClearAlarmNode",
70   - "name": "Clear Humidity Alarm",
71   - "debugMode": false,
72   - "configuration": {
73   - "alarmType": "Low Humidity",
74   - "alarmDetailsBuildJs": "var details = {};\nif (metadata.prevAlarmDetails) {\n details = JSON.parse(metadata.prevAlarmDetails);\n}\nreturn details;"
75   - }
76   - },
77   - {
78   - "additionalInfo": {
79   - "layoutX": 586,
80   - "layoutY": 148
81   - },
82   - "type": "org.thingsboard.rule.engine.filter.TbJsSwitchNode",
83   - "name": "Check Alarms",
84   - "debugMode": false,
85   - "configuration": {
86   - "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;"
87   - }
88   - },
89   - {
90   - "additionalInfo": {
91   - "layoutX": 321,
92   - "layoutY": 149
93   - },
94   - "type": "org.thingsboard.rule.engine.metadata.TbGetAttributesNode",
95   - "name": "Fetch Configuration",
96   - "debugMode": false,
97   - "configuration": {
98   - "clientAttributeNames": [],
99   - "sharedAttributeNames": [],
100   - "serverAttributeNames": [
101   - "alarmTemperature",
102   - "thresholdTemperature",
103   - "alarmHumidity",
104   - "thresholdHumidity"
105   - ],
106   - "latestTsKeyNames": [],
107   - "tellFailureIfAbsent": false,
108   - "getLatestValueWithTs": false
109   - }
110   - }
111   - ],
112   - "connections": [
113   - {
114   - "fromIndex": 4,
115   - "toIndex": 0,
116   - "type": "NewTempAlarm"
117   - },
118   - {
119   - "fromIndex": 4,
120   - "toIndex": 1,
121   - "type": "ClearTempAlarm"
122   - },
123   - {
124   - "fromIndex": 4,
125   - "toIndex": 2,
126   - "type": "NewHumidityAlarm"
127   - },
128   - {
129   - "fromIndex": 4,
130   - "toIndex": 3,
131   - "type": "ClearHumidityAlarm"
132   - },
133   - {
134   - "fromIndex": 5,
135   - "toIndex": 4,
136   - "type": "Success"
137   - }
138   - ],
139   - "ruleChainConnections": null
140   - }
141   -}
\ No newline at end of file
... ... @@ -18,7 +18,7 @@
18 18 "controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.entitiesTableWidget.onDataUpdated();\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n hasDataPageLink: true,\n warnOnPageDataOverflow: false,\n dataKeysOptional: true\n };\n}\n\nself.actionSources = function() {\n return {\n 'actionCellButton': {\n name: 'widget-action.action-cell-button',\n multiple: true\n },\n 'rowClick': {\n name: 'widget-action.row-click',\n multiple: false\n },\n 'rowDoubleClick': {\n name: 'widget-action.row-double-click',\n multiple: false\n }\n };\n}\n\nself.onDestroy = function() {\n}\n",
19 19 "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"EntitiesTableSettings\",\n \"properties\": {\n \"entitiesTitle\": {\n \"title\": \"Entities table title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"enableSearch\": {\n \"title\": \"Enable entities search\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"enableSelectColumnDisplay\": {\n \"title\": \"Enable select columns to display\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"displayEntityName\": {\n \"title\": \"Display entity name column\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"entityNameColumnTitle\": {\n \"title\": \"Entity name column title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"displayEntityLabel\": {\n \"title\": \"Display entity label column\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"entityLabelColumnTitle\": {\n \"title\": \"Entity label column title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"displayEntityType\": {\n \"title\": \"Display entity type column\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"displayPagination\": {\n \"title\": \"Display pagination\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"defaultPageSize\": {\n \"title\": \"Default page size\",\n \"type\": \"number\",\n \"default\": 10\n },\n \"defaultSortOrder\": {\n \"title\": \"Default sort order\",\n \"type\": \"string\",\n \"default\": \"entityName\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"entitiesTitle\",\n \"enableSearch\",\n \"enableSelectColumnDisplay\",\n \"displayEntityName\",\n \"entityNameColumnTitle\",\n \"displayEntityLabel\",\n \"entityLabelColumnTitle\",\n \"displayEntityType\",\n \"displayPagination\",\n \"defaultPageSize\",\n \"defaultSortOrder\"\n ]\n}",
20 20 "dataKeySettingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"DataKeySettings\",\n \"properties\": {\n \"columnWidth\": {\n \"title\": \"Column width (px or %)\",\n \"type\": \"string\",\n \"default\": \"0px\"\n },\n \"useCellStyleFunction\": {\n \"title\": \"Use cell style function\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"cellStyleFunction\": {\n \"title\": \"Cell style function: f(value)\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"useCellContentFunction\": {\n \"title\": \"Use cell content function\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"cellContentFunction\": {\n \"title\": \"Cell content function: f(value, entity, filter)\",\n \"type\": \"string\",\n \"default\": \"\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"columnWidth\",\n \"useCellStyleFunction\",\n {\n \"key\": \"cellStyleFunction\",\n \"type\": \"javascript\"\n },\n \"useCellContentFunction\",\n {\n \"key\": \"cellContentFunction\",\n \"type\": \"javascript\"\n }\n ]\n}",
21   - "defaultConfig": "{\"timewindow\":{\"realtime\":{\"interval\":1000,\"timewindowMs\":86400000},\"aggregation\":{\"type\":\"NONE\",\"limit\":200}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"4px\",\"settings\":{\"enableSearch\":true,\"displayPagination\":true,\"defaultPageSize\":10,\"defaultSortOrder\":\"entityName\",\"displayEntityName\":true,\"displayEntityType\":true,\"entitiesTitle\":\"Device admin table\",\"enableSelectColumnDisplay\":true},\"title\":\"Device admin table\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400,\"padding\":\"5px 10px 5px 10px\"},\"useDashboardTimewindow\":false,\"showLegend\":false,\"datasources\":[{\"type\":\"function\",\"name\":\"Simulated\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#f44336\",\"settings\":{\"columnWidth\":\"0px\",\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.6401141393938932,\"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;\"}]}],\"showTitleIcon\":false,\"titleIcon\":\"more_horiz\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"displayTimewindow\":true,\"actions\":{\"headerButton\":[{\"name\":\"Add device\",\"icon\":\"add\",\"type\":\"customPretty\",\"customHtml\":\"<form #addDeviceForm=\\\"ngForm\\\" [formGroup]=\\\"addDeviceFormGroup\\\"\\n (ngSubmit)=\\\"save()\\\" style=\\\"width: 480px;\\\">\\n <mat-toolbar fxLayout=\\\"row\\\" color=\\\"primary\\\">\\n <h2>Add device</h2>\\n <span fxFlex></span>\\n <button mat-button mat-icon-button\\n (click)=\\\"cancel()\\\"\\n 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>\\n <div class=\\\"mat-padding\\\" fxLayout=\\\"column\\\">\\n <mat-form-field class=\\\"mat-block\\\">\\n <mat-label>Device name</mat-label>\\n <input matInput formControlName=\\\"deviceName\\\" required>\\n <mat-error *ngIf=\\\"addDeviceFormGroup.get('deviceName').hasError('required')\\\">\\n Device name is required.\\n </mat-error>\\n </mat-form-field>\\n <div fxFlex fxLayout=\\\"row\\\" fxLayoutGap=\\\"8px\\\">\\n <tb-entity-subtype-autocomplete\\n fxFlex=\\\"50\\\"\\n formControlName=\\\"deviceType\\\"\\n [required]=\\\"true\\\"\\n [entityType]=\\\"'DEVICE'\\\"\\n ></tb-entity-subtype-autocomplete>\\n <mat-form-field fxFlex=\\\"50\\\" class=\\\"mat-block\\\">\\n <mat-label>Label</mat-label>\\n <input matInput formControlName=\\\"deviceLabel\\\">\\n </mat-form-field>\\n </div>\\n <div formGroupName=\\\"attributes\\\" fxFlex fxLayout=\\\"row\\\" fxLayoutGap=\\\"8px\\\">\\n <mat-form-field fxFlex=\\\"50\\\" class=\\\"mat-block\\\">\\n <mat-label>Latitude</mat-label>\\n <input type=\\\"number\\\" step=\\\"any\\\" matInput formControlName=\\\"latitude\\\">\\n </mat-form-field>\\n <mat-form-field fxFlex=\\\"50\\\" class=\\\"mat-block\\\">\\n <mat-label>Longitude</mat-label>\\n <input type=\\\"number\\\" step=\\\"any\\\" matInput formControlName=\\\"longitude\\\">\\n </mat-form-field>\\n </div>\\n </div> \\n </div>\\n <div mat-dialog-actions fxLayout=\\\"row\\\">\\n <span fxFlex></span>\\n <button mat-button mat-raised-button color=\\\"primary\\\"\\n type=\\\"submit\\\"\\n [disabled]=\\\"(isLoading$ | async) || addDeviceForm.invalid || !addDeviceForm.dirty\\\">\\n Create\\n </button>\\n <button mat-button color=\\\"primary\\\"\\n style=\\\"margin-right: 20px;\\\"\\n type=\\\"button\\\"\\n [disabled]=\\\"(isLoading$ | async)\\\"\\n (click)=\\\"cancel()\\\" cdkFocusInitial>\\n Cancel\\n </button>\\n </div>\\n</form>\\n\",\"customCss\":\"\",\"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\\nopenAddDeviceDialog();\\n\\nfunction openAddDeviceDialog() {\\n customDialog.customDialog(htmlTemplate, AddDeviceDialogController).subscribe();\\n}\\n\\nfunction AddDeviceDialogController(instance) {\\n let vm = instance;\\n \\n vm.addDeviceFormGroup = vm.fb.group({\\n deviceName: ['', [vm.validators.required]],\\n deviceType: ['', [vm.validators.required]],\\n deviceLabel: [''],\\n attributes: vm.fb.group({\\n latitude: [null],\\n longitude: [null]\\n }) \\n });\\n \\n vm.cancel = function() {\\n vm.dialogRef.close(null);\\n };\\n \\n vm.save = function() {\\n vm.addDeviceFormGroup.markAsPristine();\\n let device = {\\n name: vm.addDeviceFormGroup.get('deviceName').value,\\n type: vm.addDeviceFormGroup.get('deviceType').value,\\n label: vm.addDeviceFormGroup.get('deviceLabel').value\\n };\\n deviceService.saveDevice(device).subscribe(\\n function (device) {\\n saveAttributes(device.id).subscribe(\\n function () {\\n widgetContext.updateAliases();\\n vm.dialogRef.close(null);\\n }\\n );\\n }\\n );\\n };\\n \\n function saveAttributes(entityId) {\\n let attributes = vm.addDeviceFormGroup.get('attributes').value;\\n let attributesArray = [];\\n for (let key in attributes) {\\n attributesArray.push({key: key, value: attributes[key]});\\n }\\n if (attributesArray.length > 0) {\\n return attributeService.saveEntityAttributes(entityId, \\\"SERVER_SCOPE\\\", attributesArray);\\n } else {\\n return widgetContext.rxjs.of([]);\\n }\\n }\\n}\",\"customResources\":[],\"id\":\"70837a9d-c3de-a9a7-03c5-dccd14998758\"}],\"actionCellButton\":[{\"name\":\"Edit device\",\"icon\":\"edit\",\"type\":\"customPretty\",\"customHtml\":\"<form #editDeviceForm=\\\"ngForm\\\" [formGroup]=\\\"editDeviceFormGroup\\\"\\n (ngSubmit)=\\\"save()\\\" style=\\\"width: 480px;\\\">\\n <mat-toolbar fxLayout=\\\"row\\\" color=\\\"primary\\\">\\n <h2>Edit device</h2>\\n <span fxFlex></span>\\n <button mat-button mat-icon-button\\n (click)=\\\"cancel()\\\"\\n 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>\\n <div class=\\\"mat-padding\\\" fxLayout=\\\"column\\\">\\n <mat-form-field class=\\\"mat-block\\\">\\n <mat-label>Device name</mat-label>\\n <input matInput formControlName=\\\"deviceName\\\" required>\\n <mat-error *ngIf=\\\"editDeviceFormGroup.get('deviceName').hasError('required')\\\">\\n Device name is required.\\n </mat-error>\\n </mat-form-field>\\n <div fxFlex fxLayout=\\\"row\\\" fxLayoutGap=\\\"8px\\\">\\n <tb-entity-subtype-autocomplete\\n fxFlex=\\\"50\\\"\\n formControlName=\\\"deviceType\\\"\\n [required]=\\\"true\\\"\\n [entityType]=\\\"'DEVICE'\\\"\\n ></tb-entity-subtype-autocomplete>\\n <mat-form-field fxFlex=\\\"50\\\" class=\\\"mat-block\\\">\\n <mat-label>Label</mat-label>\\n <input matInput formControlName=\\\"deviceLabel\\\">\\n </mat-form-field>\\n </div>\\n <div formGroupName=\\\"attributes\\\" fxFlex fxLayout=\\\"row\\\" fxLayoutGap=\\\"8px\\\">\\n <mat-form-field fxFlex=\\\"50\\\" class=\\\"mat-block\\\">\\n <mat-label>Latitude</mat-label>\\n <input type=\\\"number\\\" step=\\\"any\\\" matInput formControlName=\\\"latitude\\\">\\n </mat-form-field>\\n <mat-form-field fxFlex=\\\"50\\\" class=\\\"mat-block\\\">\\n <mat-label>Longitude</mat-label>\\n <input type=\\\"number\\\" step=\\\"any\\\" matInput formControlName=\\\"longitude\\\">\\n </mat-form-field>\\n </div>\\n </div> \\n </div>\\n <div mat-dialog-actions fxLayout=\\\"row\\\">\\n <span fxFlex></span>\\n <button mat-button mat-raised-button color=\\\"primary\\\"\\n type=\\\"submit\\\"\\n [disabled]=\\\"(isLoading$ | async) || editDeviceForm.invalid || !editDeviceForm.dirty\\\">\\n Update\\n </button>\\n <button mat-button color=\\\"primary\\\"\\n style=\\\"margin-right: 20px;\\\"\\n type=\\\"button\\\"\\n [disabled]=\\\"(isLoading$ | async)\\\"\\n (click)=\\\"cancel()\\\" cdkFocusInitial>\\n Cancel\\n </button>\\n </div>\\n</form>\\n\",\"customCss\":\"\",\"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\\nopenEditDeviceDialog();\\n\\nfunction openEditDeviceDialog() {\\n customDialog.customDialog(htmlTemplate, EditDeviceDialogController).subscribe();\\n}\\n\\nfunction EditDeviceDialogController(instance) {\\n let vm = instance;\\n \\n vm.device = null;\\n vm.attributes = {};\\n \\n vm.editDeviceFormGroup = vm.fb.group({\\n deviceName: ['', [vm.validators.required]],\\n deviceType: ['', [vm.validators.required]],\\n deviceLabel: [''],\\n attributes: vm.fb.group({\\n latitude: [null],\\n longitude: [null]\\n }) \\n });\\n \\n vm.cancel = function() {\\n vm.dialogRef.close(null);\\n };\\n \\n vm.save = function() {\\n vm.editDeviceFormGroup.markAsPristine();\\n vm.device.name = vm.editDeviceFormGroup.get('deviceName').value,\\n vm.device.type = vm.editDeviceFormGroup.get('deviceType').value,\\n vm.device.label = vm.editDeviceFormGroup.get('deviceLabel').value\\n deviceService.saveDevice(vm.device).subscribe(\\n function () {\\n saveAttributes().subscribe(\\n function () {\\n widgetContext.updateAliases();\\n vm.dialogRef.close(null);\\n }\\n );\\n }\\n );\\n };\\n \\n getEntityInfo();\\n \\n function getEntityInfo() {\\n deviceService.getDevice(entityId.id).subscribe(\\n function (device) {\\n attributeService.getEntityAttributes(entityId, 'SERVER_SCOPE',\\n ['latitude', 'longitude']).subscribe(\\n function (attributes) {\\n for (let i = 0; i < attributes.length; i++) {\\n vm.attributes[attributes[i].key] = attributes[i].value; \\n }\\n vm.device = device;\\n vm.editDeviceFormGroup.patchValue(\\n {\\n deviceName: vm.device.name,\\n deviceType: vm.device.type,\\n deviceLabel: vm.device.label,\\n attributes: {\\n latitude: vm.attributes.latitude,\\n longitude: vm.attributes.longitude\\n }\\n }, {emitEvent: false}\\n );\\n } \\n );\\n }\\n ); \\n }\\n \\n function saveAttributes() {\\n let attributes = vm.editDeviceFormGroup.get('attributes').value;\\n let attributesArray = [];\\n for (let key in attributes) {\\n attributesArray.push({key: key, value: attributes[key]});\\n }\\n if (attributesArray.length > 0) {\\n return attributeService.saveEntityAttributes(entityId, 'SERVER_SCOPE', attributesArray);\\n } else {\\n return widgetContext.rxjs.of([]);\\n }\\n }\\n}\",\"customResources\":[],\"id\":\"93931e52-5d7c-903e-67aa-b9435df44ff4\"},{\"name\":\"Delete device\",\"icon\":\"delete\",\"type\":\"custom\",\"customFunction\":\"let $injector = widgetContext.$scope.$injector;\\nlet dialogs = $injector.get(widgetContext.servicesMap.get('dialogs'));\\nlet deviceService = $injector.get(widgetContext.servicesMap.get('deviceService'));\\n\\nopenDeleteDeviceDialog();\\n\\nfunction openDeleteDeviceDialog() {\\n let title = \\\"Are you sure you want to delete the device \\\" + entityName + \\\"?\\\";\\n let content = \\\"Be careful, after the confirmation, the device and all related data will become unrecoverable!\\\";\\n dialogs.confirm(title, content, 'Cancel', 'Delete').subscribe(\\n function (result) {\\n if (result) {\\n deleteDevice();\\n }\\n }\\n );\\n}\\n\\nfunction deleteDevice() {\\n deviceService.deleteDevice(entityId.id).subscribe(\\n function () {\\n widgetContext.updateAliases();\\n }\\n );\\n}\\n\",\"id\":\"ec2708f6-9ff0-186b-e4fc-7635ebfa3074\"}]}}"
  21 + "defaultConfig": "{\"timewindow\":{\"realtime\":{\"interval\":1000,\"timewindowMs\":86400000},\"aggregation\":{\"type\":\"NONE\",\"limit\":200}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"4px\",\"settings\":{\"enableSearch\":true,\"displayPagination\":true,\"defaultPageSize\":10,\"defaultSortOrder\":\"entityName\",\"displayEntityName\":true,\"displayEntityType\":true,\"entitiesTitle\":\"Device admin table\",\"enableSelectColumnDisplay\":true},\"title\":\"Device admin table\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400,\"padding\":\"5px 10px 5px 10px\"},\"useDashboardTimewindow\":false,\"showLegend\":false,\"datasources\":[{\"type\":\"function\",\"name\":\"Simulated\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#f44336\",\"settings\":{\"columnWidth\":\"0px\",\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.6401141393938932,\"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;\"}]}],\"showTitleIcon\":false,\"titleIcon\":\"more_horiz\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"displayTimewindow\":true,\"actions\":{\"headerButton\":[{\"name\":\"Add device\",\"icon\":\"add\",\"type\":\"customPretty\",\"customHtml\":\"<form #addDeviceForm=\\\"ngForm\\\" [formGroup]=\\\"addDeviceFormGroup\\\"\\n (ngSubmit)=\\\"save()\\\" style=\\\"width: 480px;\\\">\\n <mat-toolbar fxLayout=\\\"row\\\" color=\\\"primary\\\">\\n <h2>Add device</h2>\\n <span fxFlex></span>\\n <button mat-button mat-icon-button\\n (click)=\\\"cancel()\\\"\\n 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>\\n <div class=\\\"mat-padding\\\" fxLayout=\\\"column\\\">\\n <mat-form-field class=\\\"mat-block\\\">\\n <mat-label>Device name</mat-label>\\n <input matInput formControlName=\\\"deviceName\\\" required>\\n <mat-error *ngIf=\\\"addDeviceFormGroup.get('deviceName').hasError('required')\\\">\\n Device name is required.\\n </mat-error>\\n </mat-form-field>\\n <div fxLayout=\\\"row\\\" fxLayoutGap=\\\"8px\\\">\\n <tb-entity-subtype-autocomplete\\n fxFlex=\\\"50\\\"\\n formControlName=\\\"deviceType\\\"\\n [required]=\\\"true\\\"\\n [entityType]=\\\"'DEVICE'\\\"\\n ></tb-entity-subtype-autocomplete>\\n <mat-form-field fxFlex=\\\"50\\\" class=\\\"mat-block\\\">\\n <mat-label>Label</mat-label>\\n <input matInput formControlName=\\\"deviceLabel\\\">\\n </mat-form-field>\\n </div>\\n <div formGroupName=\\\"attributes\\\" fxLayout=\\\"row\\\" fxLayoutGap=\\\"8px\\\">\\n <mat-form-field fxFlex=\\\"50\\\" class=\\\"mat-block\\\">\\n <mat-label>Latitude</mat-label>\\n <input type=\\\"number\\\" step=\\\"any\\\" matInput formControlName=\\\"latitude\\\">\\n </mat-form-field>\\n <mat-form-field fxFlex=\\\"50\\\" class=\\\"mat-block\\\">\\n <mat-label>Longitude</mat-label>\\n <input type=\\\"number\\\" step=\\\"any\\\" matInput formControlName=\\\"longitude\\\">\\n </mat-form-field>\\n </div>\\n </div> \\n </div>\\n <div mat-dialog-actions fxLayout=\\\"row\\\">\\n <span fxFlex></span>\\n <button mat-button color=\\\"primary\\\"\\n type=\\\"button\\\"\\n [disabled]=\\\"(isLoading$ | async)\\\"\\n (click)=\\\"cancel()\\\" cdkFocusInitial>\\n Cancel\\n </button>\\n <button mat-button mat-raised-button color=\\\"primary\\\"\\n style=\\\"margin-right: 20px;\\\"\\n type=\\\"submit\\\"\\n [disabled]=\\\"(isLoading$ | async) || addDeviceForm.invalid || !addDeviceForm.dirty\\\">\\n Create\\n </button>\\n </div>\\n</form>\\n\",\"customCss\":\"\",\"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\\nopenAddDeviceDialog();\\n\\nfunction openAddDeviceDialog() {\\n customDialog.customDialog(htmlTemplate, AddDeviceDialogController).subscribe();\\n}\\n\\nfunction AddDeviceDialogController(instance) {\\n let vm = instance;\\n \\n vm.addDeviceFormGroup = vm.fb.group({\\n deviceName: ['', [vm.validators.required]],\\n deviceType: ['', [vm.validators.required]],\\n deviceLabel: [''],\\n attributes: vm.fb.group({\\n latitude: [null],\\n longitude: [null]\\n }) \\n });\\n \\n vm.cancel = function() {\\n vm.dialogRef.close(null);\\n };\\n \\n vm.save = function() {\\n vm.addDeviceFormGroup.markAsPristine();\\n let device = {\\n name: vm.addDeviceFormGroup.get('deviceName').value,\\n type: vm.addDeviceFormGroup.get('deviceType').value,\\n label: vm.addDeviceFormGroup.get('deviceLabel').value\\n };\\n deviceService.saveDevice(device).subscribe(\\n function (device) {\\n saveAttributes(device.id).subscribe(\\n function () {\\n widgetContext.updateAliases();\\n vm.dialogRef.close(null);\\n }\\n );\\n }\\n );\\n };\\n \\n function saveAttributes(entityId) {\\n let attributes = vm.addDeviceFormGroup.get('attributes').value;\\n let attributesArray = [];\\n for (let key in attributes) {\\n attributesArray.push({key: key, value: attributes[key]});\\n }\\n if (attributesArray.length > 0) {\\n return attributeService.saveEntityAttributes(entityId, \\\"SERVER_SCOPE\\\", attributesArray);\\n } else {\\n return widgetContext.rxjs.of([]);\\n }\\n }\\n}\",\"customResources\":[],\"id\":\"70837a9d-c3de-a9a7-03c5-dccd14998758\"}],\"actionCellButton\":[{\"name\":\"Edit device\",\"icon\":\"edit\",\"type\":\"customPretty\",\"customHtml\":\"<form #editDeviceForm=\\\"ngForm\\\" [formGroup]=\\\"editDeviceFormGroup\\\"\\n (ngSubmit)=\\\"save()\\\" style=\\\"width: 480px;\\\">\\n <mat-toolbar fxLayout=\\\"row\\\" color=\\\"primary\\\">\\n <h2>Edit device</h2>\\n <span fxFlex></span>\\n <button mat-button mat-icon-button\\n (click)=\\\"cancel()\\\"\\n 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>\\n <div class=\\\"mat-padding\\\" fxLayout=\\\"column\\\">\\n <mat-form-field class=\\\"mat-block\\\">\\n <mat-label>Device name</mat-label>\\n <input matInput formControlName=\\\"deviceName\\\" required>\\n <mat-error *ngIf=\\\"editDeviceFormGroup.get('deviceName').hasError('required')\\\">\\n Device name is required.\\n </mat-error>\\n </mat-form-field>\\n <div fxLayout=\\\"row\\\" fxLayoutGap=\\\"8px\\\">\\n <tb-entity-subtype-autocomplete\\n fxFlex=\\\"50\\\"\\n formControlName=\\\"deviceType\\\"\\n [required]=\\\"true\\\"\\n [entityType]=\\\"'DEVICE'\\\"\\n ></tb-entity-subtype-autocomplete>\\n <mat-form-field fxFlex=\\\"50\\\" class=\\\"mat-block\\\">\\n <mat-label>Label</mat-label>\\n <input matInput formControlName=\\\"deviceLabel\\\">\\n </mat-form-field>\\n </div>\\n <div formGroupName=\\\"attributes\\\" fxLayout=\\\"row\\\" fxLayoutGap=\\\"8px\\\">\\n <mat-form-field fxFlex=\\\"50\\\" class=\\\"mat-block\\\">\\n <mat-label>Latitude</mat-label>\\n <input type=\\\"number\\\" step=\\\"any\\\" matInput formControlName=\\\"latitude\\\">\\n </mat-form-field>\\n <mat-form-field fxFlex=\\\"50\\\" class=\\\"mat-block\\\">\\n <mat-label>Longitude</mat-label>\\n <input type=\\\"number\\\" step=\\\"any\\\" matInput formControlName=\\\"longitude\\\">\\n </mat-form-field>\\n </div>\\n </div> \\n </div>\\n <div mat-dialog-actions fxLayout=\\\"row\\\">\\n <span fxFlex></span>\\n <button mat-button color=\\\"primary\\\"\\n type=\\\"button\\\"\\n [disabled]=\\\"(isLoading$ | async)\\\"\\n (click)=\\\"cancel()\\\" cdkFocusInitial>\\n Cancel\\n </button>\\n <button mat-button mat-raised-button color=\\\"primary\\\"\\n style=\\\"margin-right: 20px;\\\"\\n type=\\\"submit\\\"\\n [disabled]=\\\"(isLoading$ | async) || editDeviceForm.invalid || !editDeviceForm.dirty\\\">\\n Update\\n </button>\\n </div>\\n</form>\\n\",\"customCss\":\"\",\"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\\nopenEditDeviceDialog();\\n\\nfunction openEditDeviceDialog() {\\n customDialog.customDialog(htmlTemplate, EditDeviceDialogController).subscribe();\\n}\\n\\nfunction EditDeviceDialogController(instance) {\\n let vm = instance;\\n \\n vm.device = null;\\n vm.attributes = {};\\n \\n vm.editDeviceFormGroup = vm.fb.group({\\n deviceName: ['', [vm.validators.required]],\\n deviceType: ['', [vm.validators.required]],\\n deviceLabel: [''],\\n attributes: vm.fb.group({\\n latitude: [null],\\n longitude: [null]\\n }) \\n });\\n \\n vm.cancel = function() {\\n vm.dialogRef.close(null);\\n };\\n \\n vm.save = function() {\\n vm.editDeviceFormGroup.markAsPristine();\\n vm.device.name = vm.editDeviceFormGroup.get('deviceName').value,\\n vm.device.type = vm.editDeviceFormGroup.get('deviceType').value,\\n vm.device.label = vm.editDeviceFormGroup.get('deviceLabel').value\\n deviceService.saveDevice(vm.device).subscribe(\\n function () {\\n saveAttributes().subscribe(\\n function () {\\n widgetContext.updateAliases();\\n vm.dialogRef.close(null);\\n }\\n );\\n }\\n );\\n };\\n \\n getEntityInfo();\\n \\n function getEntityInfo() {\\n deviceService.getDevice(entityId.id).subscribe(\\n function (device) {\\n attributeService.getEntityAttributes(entityId, 'SERVER_SCOPE',\\n ['latitude', 'longitude']).subscribe(\\n function (attributes) {\\n for (let i = 0; i < attributes.length; i++) {\\n vm.attributes[attributes[i].key] = attributes[i].value; \\n }\\n vm.device = device;\\n vm.editDeviceFormGroup.patchValue(\\n {\\n deviceName: vm.device.name,\\n deviceType: vm.device.type,\\n deviceLabel: vm.device.label,\\n attributes: {\\n latitude: vm.attributes.latitude,\\n longitude: vm.attributes.longitude\\n }\\n }, {emitEvent: false}\\n );\\n } \\n );\\n }\\n ); \\n }\\n \\n function saveAttributes() {\\n let attributes = vm.editDeviceFormGroup.get('attributes').value;\\n let attributesArray = [];\\n for (let key in attributes) {\\n attributesArray.push({key: key, value: attributes[key]});\\n }\\n if (attributesArray.length > 0) {\\n return attributeService.saveEntityAttributes(entityId, 'SERVER_SCOPE', attributesArray);\\n } else {\\n return widgetContext.rxjs.of([]);\\n }\\n }\\n}\",\"customResources\":[],\"id\":\"93931e52-5d7c-903e-67aa-b9435df44ff4\"},{\"name\":\"Delete device\",\"icon\":\"delete\",\"type\":\"custom\",\"customFunction\":\"let $injector = widgetContext.$scope.$injector;\\nlet dialogs = $injector.get(widgetContext.servicesMap.get('dialogs'));\\nlet deviceService = $injector.get(widgetContext.servicesMap.get('deviceService'));\\n\\nopenDeleteDeviceDialog();\\n\\nfunction openDeleteDeviceDialog() {\\n let title = \\\"Are you sure you want to delete the device \\\" + entityName + \\\"?\\\";\\n let content = \\\"Be careful, after the confirmation, the device and all related data will become unrecoverable!\\\";\\n dialogs.confirm(title, content, 'Cancel', 'Delete').subscribe(\\n function (result) {\\n if (result) {\\n deleteDevice();\\n }\\n }\\n );\\n}\\n\\nfunction deleteDevice() {\\n deviceService.deleteDevice(entityId.id).subscribe(\\n function () {\\n widgetContext.updateAliases();\\n }\\n );\\n}\\n\",\"id\":\"ec2708f6-9ff0-186b-e4fc-7635ebfa3074\"}]}}"
22 22 }
23 23 },
24 24 {
... ... @@ -34,8 +34,8 @@
34 34 "controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.entitiesTableWidget.onDataUpdated();\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n hasDataPageLink: true,\n warnOnPageDataOverflow: false,\n dataKeysOptional: true\n };\n}\n\nself.actionSources = function() {\n return {\n 'actionCellButton': {\n name: 'widget-action.action-cell-button',\n multiple: true\n },\n 'rowClick': {\n name: 'widget-action.row-click',\n multiple: false\n },\n 'rowDoubleClick': {\n name: 'widget-action.row-double-click',\n multiple: false\n }\n };\n}\n\nself.onDestroy = function() {\n}\n",
35 35 "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"EntitiesTableSettings\",\n \"properties\": {\n \"entitiesTitle\": {\n \"title\": \"Entities table title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"enableSearch\": {\n \"title\": \"Enable entities search\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"enableSelectColumnDisplay\": {\n \"title\": \"Enable select columns to display\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"displayEntityName\": {\n \"title\": \"Display entity name column\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"entityNameColumnTitle\": {\n \"title\": \"Entity name column title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"displayEntityLabel\": {\n \"title\": \"Display entity label column\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"entityLabelColumnTitle\": {\n \"title\": \"Entity label column title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"displayEntityType\": {\n \"title\": \"Display entity type column\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"displayPagination\": {\n \"title\": \"Display pagination\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"defaultPageSize\": {\n \"title\": \"Default page size\",\n \"type\": \"number\",\n \"default\": 10\n },\n \"defaultSortOrder\": {\n \"title\": \"Default sort order\",\n \"type\": \"string\",\n \"default\": \"entityName\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"entitiesTitle\",\n \"enableSearch\",\n \"enableSelectColumnDisplay\",\n \"displayEntityName\",\n \"entityNameColumnTitle\",\n \"displayEntityLabel\",\n \"entityLabelColumnTitle\",\n \"displayEntityType\",\n \"displayPagination\",\n \"defaultPageSize\",\n \"defaultSortOrder\"\n ]\n}",
36 36 "dataKeySettingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"DataKeySettings\",\n \"properties\": {\n \"columnWidth\": {\n \"title\": \"Column width (px or %)\",\n \"type\": \"string\",\n \"default\": \"0px\"\n },\n \"useCellStyleFunction\": {\n \"title\": \"Use cell style function\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"cellStyleFunction\": {\n \"title\": \"Cell style function: f(value)\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"useCellContentFunction\": {\n \"title\": \"Use cell content function\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"cellContentFunction\": {\n \"title\": \"Cell content function: f(value, entity, filter)\",\n \"type\": \"string\",\n \"default\": \"\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"columnWidth\",\n \"useCellStyleFunction\",\n {\n \"key\": \"cellStyleFunction\",\n \"type\": \"javascript\"\n },\n \"useCellContentFunction\",\n {\n \"key\": \"cellContentFunction\",\n \"type\": \"javascript\"\n }\n ]\n}",
37   - "defaultConfig": "{\"timewindow\":{\"realtime\":{\"interval\":1000,\"timewindowMs\":86400000},\"aggregation\":{\"type\":\"NONE\",\"limit\":200}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"4px\",\"settings\":{\"enableSearch\":true,\"displayPagination\":true,\"defaultPageSize\":10,\"defaultSortOrder\":\"entityName\",\"displayEntityName\":true,\"displayEntityType\":true,\"entitiesTitle\":\"Asset admin table\",\"enableSelectColumnDisplay\":true},\"title\":\"Asset admin table\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400,\"padding\":\"5px 10px 5px 10px\"},\"useDashboardTimewindow\":false,\"showLegend\":false,\"datasources\":[{\"type\":\"function\",\"name\":\"Simulated\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#f44336\",\"settings\":{\"columnWidth\":\"0px\",\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.6401141393938932,\"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;\"}]}],\"showTitleIcon\":false,\"titleIcon\":\"more_horiz\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"displayTimewindow\":true,\"actions\":{\"headerButton\":[{\"name\":\"Add asset\",\"icon\":\"add\",\"type\":\"customPretty\",\"customHtml\":\"<form #addAssetForm=\\\"ngForm\\\" [formGroup]=\\\"addAssetFormGroup\\\"\\n (ngSubmit)=\\\"save()\\\" style=\\\"width: 480px;\\\">\\n <mat-toolbar fxLayout=\\\"row\\\" color=\\\"primary\\\">\\n <h2>Add asset</h2>\\n <span fxFlex></span>\\n <button mat-button mat-icon-button\\n (click)=\\\"cancel()\\\"\\n 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>\\n <div class=\\\"mat-padding\\\" fxLayout=\\\"column\\\">\\n <mat-form-field class=\\\"mat-block\\\">\\n <mat-label>Asset name</mat-label>\\n <input matInput formControlName=\\\"assetName\\\" required>\\n <mat-error *ngIf=\\\"addAssetFormGroup.get('assetName').hasError('required')\\\">\\n Asset name is required.\\n </mat-error>\\n </mat-form-field>\\n <div fxFlex fxLayout=\\\"row\\\" fxLayoutGap=\\\"8px\\\">\\n <tb-entity-subtype-autocomplete\\n fxFlex=\\\"50\\\"\\n formControlName=\\\"assetType\\\"\\n [required]=\\\"true\\\"\\n [entityType]=\\\"'ASSET'\\\"\\n ></tb-entity-subtype-autocomplete>\\n <mat-form-field fxFlex=\\\"50\\\" class=\\\"mat-block\\\">\\n <mat-label>Label</mat-label>\\n <input matInput formControlName=\\\"assetLabel\\\">\\n </mat-form-field>\\n </div>\\n <div formGroupName=\\\"attributes\\\" fxFlex fxLayout=\\\"row\\\" fxLayoutGap=\\\"8px\\\">\\n <mat-form-field fxFlex=\\\"50\\\" class=\\\"mat-block\\\">\\n <mat-label>Latitude</mat-label>\\n <input type=\\\"number\\\" step=\\\"any\\\" matInput formControlName=\\\"latitude\\\">\\n </mat-form-field>\\n <mat-form-field fxFlex=\\\"50\\\" class=\\\"mat-block\\\">\\n <mat-label>Longitude</mat-label>\\n <input type=\\\"number\\\" step=\\\"any\\\" matInput formControlName=\\\"longitude\\\">\\n </mat-form-field>\\n </div>\\n </div> \\n </div>\\n <div mat-dialog-actions fxLayout=\\\"row\\\">\\n <span fxFlex></span>\\n <button mat-button mat-raised-button color=\\\"primary\\\"\\n type=\\\"submit\\\"\\n [disabled]=\\\"(isLoading$ | async) || addAssetForm.invalid || !addAssetForm.dirty\\\">\\n Create\\n </button>\\n <button mat-button color=\\\"primary\\\"\\n style=\\\"margin-right: 20px;\\\"\\n type=\\\"button\\\"\\n [disabled]=\\\"(isLoading$ | async)\\\"\\n (click)=\\\"cancel()\\\" cdkFocusInitial>\\n Cancel\\n </button>\\n </div>\\n</form>\\n\",\"customCss\":\"\",\"customFunction\":\"let $injector = widgetContext.$scope.$injector;\\nlet customDialog = $injector.get(widgetContext.servicesMap.get('customDialog'));\\nlet assetService = $injector.get(widgetContext.servicesMap.get('assetService'));\\nlet attributeService = $injector.get(widgetContext.servicesMap.get('attributeService'));\\n\\nopenAddAssetDialog();\\n\\nfunction openAddAssetDialog() {\\n customDialog.customDialog(htmlTemplate, AddAssetDialogController).subscribe();\\n}\\n\\nfunction AddAssetDialogController(instance) {\\n let vm = instance;\\n \\n vm.addAssetFormGroup = vm.fb.group({\\n assetName: ['', [vm.validators.required]],\\n assetType: ['', [vm.validators.required]],\\n assetLabel: [''],\\n attributes: vm.fb.group({\\n latitude: [null],\\n longitude: [null]\\n }) \\n });\\n \\n vm.cancel = function() {\\n vm.dialogRef.close(null);\\n };\\n \\n vm.save = function() {\\n vm.addAssetFormGroup.markAsPristine();\\n let asset = {\\n name: vm.addAssetFormGroup.get('assetName').value,\\n type: vm.addAssetFormGroup.get('assetType').value,\\n label: vm.addAssetFormGroup.get('assetLabel').value\\n };\\n assetService.saveAsset(asset).subscribe(\\n function (asset) {\\n saveAttributes(asset.id).subscribe(\\n function () {\\n widgetContext.updateAliases();\\n vm.dialogRef.close(null);\\n }\\n );\\n }\\n );\\n };\\n \\n function saveAttributes(entityId) {\\n let attributes = vm.addAssetFormGroup.get('attributes').value;\\n let attributesArray = [];\\n for (let key in attributes) {\\n attributesArray.push({key: key, value: attributes[key]});\\n }\\n if (attributesArray.length > 0) {\\n return attributeService.saveEntityAttributes(entityId, \\\"SERVER_SCOPE\\\", attributesArray);\\n } else {\\n return widgetContext.rxjs.of([]);\\n }\\n }\\n}\",\"customResources\":[],\"id\":\"70837a9d-c3de-a9a7-03c5-dccd14998758\"}],\"actionCellButton\":[{\"name\":\"Edit asset\",\"icon\":\"edit\",\"type\":\"customPretty\",\"customHtml\":\"<form #editAssetForm=\\\"ngForm\\\" [formGroup]=\\\"editAssetFormGroup\\\"\\n (ngSubmit)=\\\"save()\\\" style=\\\"width: 480px;\\\">\\n <mat-toolbar fxLayout=\\\"row\\\" color=\\\"primary\\\">\\n <h2>Edit asset</h2>\\n <span fxFlex></span>\\n <button mat-button mat-icon-button\\n (click)=\\\"cancel()\\\"\\n 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>\\n <div class=\\\"mat-padding\\\" fxLayout=\\\"column\\\">\\n <mat-form-field class=\\\"mat-block\\\">\\n <mat-label>Asset name</mat-label>\\n <input matInput formControlName=\\\"assetName\\\" required>\\n <mat-error *ngIf=\\\"editAssetFormGroup.get('assetName').hasError('required')\\\">\\n Asset name is required.\\n </mat-error>\\n </mat-form-field>\\n <div fxFlex fxLayout=\\\"row\\\" fxLayoutGap=\\\"8px\\\">\\n <tb-entity-subtype-autocomplete\\n fxFlex=\\\"50\\\"\\n formControlName=\\\"assetType\\\"\\n [required]=\\\"true\\\"\\n [entityType]=\\\"'ASSET'\\\"\\n ></tb-entity-subtype-autocomplete>\\n <mat-form-field fxFlex=\\\"50\\\" class=\\\"mat-block\\\">\\n <mat-label>Label</mat-label>\\n <input matInput formControlName=\\\"assetLabel\\\">\\n </mat-form-field>\\n </div>\\n <div formGroupName=\\\"attributes\\\" fxFlex fxLayout=\\\"row\\\" fxLayoutGap=\\\"8px\\\">\\n <mat-form-field fxFlex=\\\"50\\\" class=\\\"mat-block\\\">\\n <mat-label>Latitude</mat-label>\\n <input type=\\\"number\\\" step=\\\"any\\\" matInput formControlName=\\\"latitude\\\">\\n </mat-form-field>\\n <mat-form-field fxFlex=\\\"50\\\" class=\\\"mat-block\\\">\\n <mat-label>Longitude</mat-label>\\n <input type=\\\"number\\\" step=\\\"any\\\" matInput formControlName=\\\"longitude\\\">\\n </mat-form-field>\\n </div>\\n </div> \\n </div>\\n <div mat-dialog-actions fxLayout=\\\"row\\\">\\n <span fxFlex></span>\\n <button mat-button mat-raised-button color=\\\"primary\\\"\\n type=\\\"submit\\\"\\n [disabled]=\\\"(isLoading$ | async) || editAssetForm.invalid || !editAssetForm.dirty\\\">\\n Update\\n </button>\\n <button mat-button color=\\\"primary\\\"\\n style=\\\"margin-right: 20px;\\\"\\n type=\\\"button\\\"\\n [disabled]=\\\"(isLoading$ | async)\\\"\\n (click)=\\\"cancel()\\\" cdkFocusInitial>\\n Cancel\\n </button>\\n </div>\\n</form>\\n\",\"customCss\":\"\",\"customFunction\":\"let $injector = widgetContext.$scope.$injector;\\nlet customDialog = $injector.get(widgetContext.servicesMap.get('customDialog'));\\nlet assetService = $injector.get(widgetContext.servicesMap.get('assetService'));\\nlet attributeService = $injector.get(widgetContext.servicesMap.get('attributeService'));\\n\\nopenEditAssetDialog();\\n\\nfunction openEditAssetDialog() {\\n customDialog.customDialog(htmlTemplate, EditAssetDialogController).subscribe();\\n}\\n\\nfunction EditAssetDialogController(instance) {\\n let vm = instance;\\n \\n vm.asset = null;\\n vm.attributes = {};\\n \\n vm.editAssetFormGroup = vm.fb.group({\\n assetName: ['', [vm.validators.required]],\\n assetType: ['', [vm.validators.required]],\\n assetLabel: [''],\\n attributes: vm.fb.group({\\n latitude: [null],\\n longitude: [null]\\n }) \\n });\\n \\n vm.cancel = function() {\\n vm.dialogRef.close(null);\\n };\\n \\n vm.save = function() {\\n vm.editAssetFormGroup.markAsPristine();\\n vm.asset.name = vm.editAssetFormGroup.get('assetName').value,\\n vm.asset.type = vm.editAssetFormGroup.get('assetType').value,\\n vm.asset.label = vm.editAssetFormGroup.get('assetLabel').value\\n assetService.saveAsset(vm.asset).subscribe(\\n function () {\\n saveAttributes().subscribe(\\n function () {\\n widgetContext.updateAliases();\\n vm.dialogRef.close(null);\\n }\\n );\\n }\\n );\\n };\\n \\n getEntityInfo();\\n \\n function getEntityInfo() {\\n assetService.getAsset(entityId.id).subscribe(\\n function (asset) {\\n attributeService.getEntityAttributes(entityId, 'SERVER_SCOPE',\\n ['latitude', 'longitude']).subscribe(\\n function (attributes) {\\n for (let i = 0; i < attributes.length; i++) {\\n vm.attributes[attributes[i].key] = attributes[i].value; \\n }\\n vm.asset = asset;\\n vm.editAssetFormGroup.patchValue(\\n {\\n assetName: vm.asset.name,\\n assetType: vm.asset.type,\\n assetLabel: vm.asset.label,\\n attributes: {\\n latitude: vm.attributes.latitude,\\n longitude: vm.attributes.longitude\\n }\\n }, {emitEvent: false}\\n );\\n } \\n );\\n }\\n ); \\n }\\n \\n function saveAttributes() {\\n let attributes = vm.editAssetFormGroup.get('attributes').value;\\n let attributesArray = [];\\n for (let key in attributes) {\\n attributesArray.push({key: key, value: attributes[key]});\\n }\\n if (attributesArray.length > 0) {\\n return attributeService.saveEntityAttributes(entityId, 'SERVER_SCOPE', attributesArray);\\n } else {\\n return widgetContext.rxjs.of([]);\\n }\\n }\\n}\",\"customResources\":[],\"id\":\"93931e52-5d7c-903e-67aa-b9435df44ff4\"},{\"name\":\"Delete asset\",\"icon\":\"delete\",\"type\":\"custom\",\"customFunction\":\"let $injector = widgetContext.$scope.$injector;\\nlet dialogs = $injector.get(widgetContext.servicesMap.get('dialogs'));\\nlet assetService = $injector.get(widgetContext.servicesMap.get('assetService'));\\n\\nopenDeleteAssetDialog();\\n\\nfunction openDeleteAssetDialog() {\\n let title = \\\"Are you sure you want to delete the asset \\\" + entityName + \\\"?\\\";\\n let content = \\\"Be careful, after the confirmation, the asset and all related data will become unrecoverable!\\\";\\n dialogs.confirm(title, content, 'Cancel', 'Delete').subscribe(\\n function (result) {\\n if (result) {\\n deleteAsset();\\n }\\n }\\n );\\n}\\n\\nfunction deleteAsset() {\\n assetService.deleteAsset(entityId.id).subscribe(\\n function () {\\n widgetContext.updateAliases();\\n }\\n );\\n}\\n\",\"id\":\"ec2708f6-9ff0-186b-e4fc-7635ebfa3074\"}]}}"
  37 + "defaultConfig": "{\"timewindow\":{\"realtime\":{\"interval\":1000,\"timewindowMs\":86400000},\"aggregation\":{\"type\":\"NONE\",\"limit\":200}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"4px\",\"settings\":{\"enableSearch\":true,\"displayPagination\":true,\"defaultPageSize\":10,\"defaultSortOrder\":\"entityName\",\"displayEntityName\":true,\"displayEntityType\":true,\"entitiesTitle\":\"Asset admin table\",\"enableSelectColumnDisplay\":true},\"title\":\"Asset admin table\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400,\"padding\":\"5px 10px 5px 10px\"},\"useDashboardTimewindow\":false,\"showLegend\":false,\"datasources\":[{\"type\":\"function\",\"name\":\"Simulated\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#f44336\",\"settings\":{\"columnWidth\":\"0px\",\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.6401141393938932,\"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;\"}]}],\"showTitleIcon\":false,\"titleIcon\":\"more_horiz\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"displayTimewindow\":true,\"actions\":{\"headerButton\":[{\"name\":\"Add asset\",\"icon\":\"add\",\"type\":\"customPretty\",\"customHtml\":\"<form #addAssetForm=\\\"ngForm\\\" [formGroup]=\\\"addAssetFormGroup\\\"\\n (ngSubmit)=\\\"save()\\\" style=\\\"width: 480px;\\\">\\n <mat-toolbar fxLayout=\\\"row\\\" color=\\\"primary\\\">\\n <h2>Add asset</h2>\\n <span fxFlex></span>\\n <button mat-button mat-icon-button\\n (click)=\\\"cancel()\\\"\\n 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>\\n <div class=\\\"mat-padding\\\" fxLayout=\\\"column\\\">\\n <mat-form-field class=\\\"mat-block\\\">\\n <mat-label>Asset name</mat-label>\\n <input matInput formControlName=\\\"assetName\\\" required>\\n <mat-error *ngIf=\\\"addAssetFormGroup.get('assetName').hasError('required')\\\">\\n Asset name is required.\\n </mat-error>\\n </mat-form-field>\\n <div fxLayout=\\\"row\\\" fxLayoutGap=\\\"8px\\\">\\n <tb-entity-subtype-autocomplete\\n fxFlex=\\\"50\\\"\\n formControlName=\\\"assetType\\\"\\n [required]=\\\"true\\\"\\n [entityType]=\\\"'ASSET'\\\"\\n ></tb-entity-subtype-autocomplete>\\n <mat-form-field fxFlex=\\\"50\\\" class=\\\"mat-block\\\">\\n <mat-label>Label</mat-label>\\n <input matInput formControlName=\\\"assetLabel\\\">\\n </mat-form-field>\\n </div>\\n <div formGroupName=\\\"attributes\\\" fxLayout=\\\"row\\\" fxLayoutGap=\\\"8px\\\">\\n <mat-form-field fxFlex=\\\"50\\\" class=\\\"mat-block\\\">\\n <mat-label>Latitude</mat-label>\\n <input type=\\\"number\\\" step=\\\"any\\\" matInput formControlName=\\\"latitude\\\">\\n </mat-form-field>\\n <mat-form-field fxFlex=\\\"50\\\" class=\\\"mat-block\\\">\\n <mat-label>Longitude</mat-label>\\n <input type=\\\"number\\\" step=\\\"any\\\" matInput formControlName=\\\"longitude\\\">\\n </mat-form-field>\\n </div>\\n </div> \\n </div>\\n <div mat-dialog-actions fxLayout=\\\"row\\\">\\n <span fxFlex></span>\\n <button mat-button color=\\\"primary\\\"\\n type=\\\"button\\\"\\n [disabled]=\\\"(isLoading$ | async)\\\"\\n (click)=\\\"cancel()\\\" cdkFocusInitial>\\n Cancel\\n </button>\\n <button mat-button mat-raised-button color=\\\"primary\\\"\\n style=\\\"margin-right: 20px;\\\"\\n type=\\\"submit\\\"\\n [disabled]=\\\"(isLoading$ | async) || addAssetForm.invalid || !addAssetForm.dirty\\\">\\n Create\\n </button>\\n </div>\\n</form>\\n\",\"customCss\":\"\",\"customFunction\":\"let $injector = widgetContext.$scope.$injector;\\nlet customDialog = $injector.get(widgetContext.servicesMap.get('customDialog'));\\nlet assetService = $injector.get(widgetContext.servicesMap.get('assetService'));\\nlet attributeService = $injector.get(widgetContext.servicesMap.get('attributeService'));\\n\\nopenAddAssetDialog();\\n\\nfunction openAddAssetDialog() {\\n customDialog.customDialog(htmlTemplate, AddAssetDialogController).subscribe();\\n}\\n\\nfunction AddAssetDialogController(instance) {\\n let vm = instance;\\n \\n vm.addAssetFormGroup = vm.fb.group({\\n assetName: ['', [vm.validators.required]],\\n assetType: ['', [vm.validators.required]],\\n assetLabel: [''],\\n attributes: vm.fb.group({\\n latitude: [null],\\n longitude: [null]\\n }) \\n });\\n \\n vm.cancel = function() {\\n vm.dialogRef.close(null);\\n };\\n \\n vm.save = function() {\\n vm.addAssetFormGroup.markAsPristine();\\n let asset = {\\n name: vm.addAssetFormGroup.get('assetName').value,\\n type: vm.addAssetFormGroup.get('assetType').value,\\n label: vm.addAssetFormGroup.get('assetLabel').value\\n };\\n assetService.saveAsset(asset).subscribe(\\n function (asset) {\\n saveAttributes(asset.id).subscribe(\\n function () {\\n widgetContext.updateAliases();\\n vm.dialogRef.close(null);\\n }\\n );\\n }\\n );\\n };\\n \\n function saveAttributes(entityId) {\\n let attributes = vm.addAssetFormGroup.get('attributes').value;\\n let attributesArray = [];\\n for (let key in attributes) {\\n attributesArray.push({key: key, value: attributes[key]});\\n }\\n if (attributesArray.length > 0) {\\n return attributeService.saveEntityAttributes(entityId, \\\"SERVER_SCOPE\\\", attributesArray);\\n } else {\\n return widgetContext.rxjs.of([]);\\n }\\n }\\n}\",\"customResources\":[],\"id\":\"70837a9d-c3de-a9a7-03c5-dccd14998758\"}],\"actionCellButton\":[{\"name\":\"Edit asset\",\"icon\":\"edit\",\"type\":\"customPretty\",\"customHtml\":\"<form #editAssetForm=\\\"ngForm\\\" [formGroup]=\\\"editAssetFormGroup\\\"\\n (ngSubmit)=\\\"save()\\\" style=\\\"width: 480px;\\\">\\n <mat-toolbar fxLayout=\\\"row\\\" color=\\\"primary\\\">\\n <h2>Edit asset</h2>\\n <span fxFlex></span>\\n <button mat-button mat-icon-button\\n (click)=\\\"cancel()\\\"\\n 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>\\n <div class=\\\"mat-padding\\\" fxLayout=\\\"column\\\">\\n <mat-form-field class=\\\"mat-block\\\">\\n <mat-label>Asset name</mat-label>\\n <input matInput formControlName=\\\"assetName\\\" required>\\n <mat-error *ngIf=\\\"editAssetFormGroup.get('assetName').hasError('required')\\\">\\n Asset name is required.\\n </mat-error>\\n </mat-form-field>\\n <div fxLayout=\\\"row\\\" fxLayoutGap=\\\"8px\\\">\\n <tb-entity-subtype-autocomplete\\n fxFlex=\\\"50\\\"\\n formControlName=\\\"assetType\\\"\\n [required]=\\\"true\\\"\\n [entityType]=\\\"'ASSET'\\\"\\n ></tb-entity-subtype-autocomplete>\\n <mat-form-field fxFlex=\\\"50\\\" class=\\\"mat-block\\\">\\n <mat-label>Label</mat-label>\\n <input matInput formControlName=\\\"assetLabel\\\">\\n </mat-form-field>\\n </div>\\n <div formGroupName=\\\"attributes\\\" fxLayout=\\\"row\\\" fxLayoutGap=\\\"8px\\\">\\n <mat-form-field fxFlex=\\\"50\\\" class=\\\"mat-block\\\">\\n <mat-label>Latitude</mat-label>\\n <input type=\\\"number\\\" step=\\\"any\\\" matInput formControlName=\\\"latitude\\\">\\n </mat-form-field>\\n <mat-form-field fxFlex=\\\"50\\\" class=\\\"mat-block\\\">\\n <mat-label>Longitude</mat-label>\\n <input type=\\\"number\\\" step=\\\"any\\\" matInput formControlName=\\\"longitude\\\">\\n </mat-form-field>\\n </div>\\n </div> \\n </div>\\n <div mat-dialog-actions fxLayout=\\\"row\\\">\\n <span fxFlex></span>\\n <button mat-button color=\\\"primary\\\"\\n type=\\\"button\\\"\\n [disabled]=\\\"(isLoading$ | async)\\\"\\n (click)=\\\"cancel()\\\" cdkFocusInitial>\\n Cancel\\n </button>\\n <button mat-button mat-raised-button color=\\\"primary\\\"\\n type=\\\"submit\\\"\\n style=\\\"margin-right: 20px;\\\"\\n [disabled]=\\\"(isLoading$ | async) || editAssetForm.invalid || !editAssetForm.dirty\\\">\\n Update\\n </button>\\n </div>\\n</form>\\n\",\"customCss\":\"\",\"customFunction\":\"let $injector = widgetContext.$scope.$injector;\\nlet customDialog = $injector.get(widgetContext.servicesMap.get('customDialog'));\\nlet assetService = $injector.get(widgetContext.servicesMap.get('assetService'));\\nlet attributeService = $injector.get(widgetContext.servicesMap.get('attributeService'));\\n\\nopenEditAssetDialog();\\n\\nfunction openEditAssetDialog() {\\n customDialog.customDialog(htmlTemplate, EditAssetDialogController).subscribe();\\n}\\n\\nfunction EditAssetDialogController(instance) {\\n let vm = instance;\\n \\n vm.asset = null;\\n vm.attributes = {};\\n \\n vm.editAssetFormGroup = vm.fb.group({\\n assetName: ['', [vm.validators.required]],\\n assetType: ['', [vm.validators.required]],\\n assetLabel: [''],\\n attributes: vm.fb.group({\\n latitude: [null],\\n longitude: [null]\\n }) \\n });\\n \\n vm.cancel = function() {\\n vm.dialogRef.close(null);\\n };\\n \\n vm.save = function() {\\n vm.editAssetFormGroup.markAsPristine();\\n vm.asset.name = vm.editAssetFormGroup.get('assetName').value,\\n vm.asset.type = vm.editAssetFormGroup.get('assetType').value,\\n vm.asset.label = vm.editAssetFormGroup.get('assetLabel').value\\n assetService.saveAsset(vm.asset).subscribe(\\n function () {\\n saveAttributes().subscribe(\\n function () {\\n widgetContext.updateAliases();\\n vm.dialogRef.close(null);\\n }\\n );\\n }\\n );\\n };\\n \\n getEntityInfo();\\n \\n function getEntityInfo() {\\n assetService.getAsset(entityId.id).subscribe(\\n function (asset) {\\n attributeService.getEntityAttributes(entityId, 'SERVER_SCOPE',\\n ['latitude', 'longitude']).subscribe(\\n function (attributes) {\\n for (let i = 0; i < attributes.length; i++) {\\n vm.attributes[attributes[i].key] = attributes[i].value; \\n }\\n vm.asset = asset;\\n vm.editAssetFormGroup.patchValue(\\n {\\n assetName: vm.asset.name,\\n assetType: vm.asset.type,\\n assetLabel: vm.asset.label,\\n attributes: {\\n latitude: vm.attributes.latitude,\\n longitude: vm.attributes.longitude\\n }\\n }, {emitEvent: false}\\n );\\n } \\n );\\n }\\n ); \\n }\\n \\n function saveAttributes() {\\n let attributes = vm.editAssetFormGroup.get('attributes').value;\\n let attributesArray = [];\\n for (let key in attributes) {\\n attributesArray.push({key: key, value: attributes[key]});\\n }\\n if (attributesArray.length > 0) {\\n return attributeService.saveEntityAttributes(entityId, 'SERVER_SCOPE', attributesArray);\\n } else {\\n return widgetContext.rxjs.of([]);\\n }\\n }\\n}\",\"customResources\":[],\"id\":\"93931e52-5d7c-903e-67aa-b9435df44ff4\"},{\"name\":\"Delete asset\",\"icon\":\"delete\",\"type\":\"custom\",\"customFunction\":\"let $injector = widgetContext.$scope.$injector;\\nlet dialogs = $injector.get(widgetContext.servicesMap.get('dialogs'));\\nlet assetService = $injector.get(widgetContext.servicesMap.get('assetService'));\\n\\nopenDeleteAssetDialog();\\n\\nfunction openDeleteAssetDialog() {\\n let title = \\\"Are you sure you want to delete the asset \\\" + entityName + \\\"?\\\";\\n let content = \\\"Be careful, after the confirmation, the asset and all related data will become unrecoverable!\\\";\\n dialogs.confirm(title, content, 'Cancel', 'Delete').subscribe(\\n function (result) {\\n if (result) {\\n deleteAsset();\\n }\\n }\\n );\\n}\\n\\nfunction deleteAsset() {\\n assetService.deleteAsset(entityId.id).subscribe(\\n function () {\\n widgetContext.updateAliases();\\n }\\n );\\n}\\n\",\"id\":\"ec2708f6-9ff0-186b-e4fc-7635ebfa3074\"}]}}"
38 38 }
39 39 }
40 40 ]
41   -}
\ No newline at end of file
  41 +}
... ...
... ... @@ -33,7 +33,7 @@
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 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 \"selectOptionsType\": {\n \"title\": \"Select options type\",\n \"type\": \"string\",\n \"default\": \"valueWithLabel\"\n },\n \"selectOptionsList\": {\n \"title\": \"Select options list\",\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"properties\": {\n \"value\": {\n \"title\": \"Value\",\n \"type\": \"string\"\n },\n \"label\": {\n \"title\": \"Label\",\n \"type\": \"string\"\n }\n },\n \"required\": [\"value\", \"label\"]\n }\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 \"value\": \"selectOption\",\n \"label\": \"Selectable option\"\n }\n ]\n },\n {\n \"key\": \"selectOptionsType\",\n \"condition\": \"model.dataKeyValueType === 'selectOption'\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"valueWithLabel\",\n \"label\": \"Values with labels\"\n },\n {\n \"value\": \"rawValue\",\n \"label\": \"Raw values\"\n }\n ]\n },\n {\n \"key\": \"selectOptionsList\",\n \"type\": \"array\",\n \"condition\": \"model.dataKeyValueType === 'selectOption'\",\n \"items\": [\n \"selectOptionsList[].value\",\n {\n \"key\": \"selectOptionsList[].label\",\n \"condition\": \"model.selectOptionsType === 'valueWithLabel'\"\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",
  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 }
... ...
... ... @@ -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;
... ... @@ -73,13 +75,14 @@ import org.thingsboard.server.service.executors.ExternalCallExecutorService;
73 75 import org.thingsboard.server.service.executors.SharedEventLoopGroupService;
74 76 import org.thingsboard.server.service.mail.MailExecutorService;
75 77 import org.thingsboard.server.service.profile.TbDeviceProfileCache;
76   -import org.thingsboard.server.service.profile.TbTenantProfileCache;
  78 +import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
77 79 import org.thingsboard.server.service.queue.TbClusterService;
78 80 import org.thingsboard.server.service.rpc.TbCoreDeviceRpcService;
79 81 import org.thingsboard.server.service.rpc.TbRuleEngineDeviceRpcService;
80 82 import org.thingsboard.server.service.script.JsExecutorService;
81 83 import org.thingsboard.server.service.script.JsInvokeService;
82 84 import org.thingsboard.server.service.session.DeviceSessionCacheService;
  85 +import org.thingsboard.server.service.sms.SmsExecutorService;
83 86 import org.thingsboard.server.service.state.DeviceStateService;
84 87 import org.thingsboard.server.service.telemetry.AlarmSubscriptionService;
85 88 import org.thingsboard.server.service.telemetry.TelemetrySubscriptionService;
... ... @@ -230,6 +233,10 @@ public class ActorSystemContext {
230 233
231 234 @Autowired
232 235 @Getter
  236 + private SmsExecutorService smsExecutor;
  237 +
  238 + @Autowired
  239 + @Getter
233 240 private DbCallbackExecutorService dbCallbackExecutor;
234 241
235 242 @Autowired
... ... @@ -246,6 +253,14 @@ public class ActorSystemContext {
246 253
247 254 @Autowired
248 255 @Getter
  256 + private SmsService smsService;
  257 +
  258 + @Autowired
  259 + @Getter
  260 + private SmsSenderFactory smsSenderFactory;
  261 +
  262 + @Autowired
  263 + @Getter
249 264 private ClaimDevicesService claimDevicesService;
250 265
251 266 @Autowired
... ... @@ -325,6 +340,10 @@ public class ActorSystemContext {
325 340 @Getter
326 341 private boolean allowSystemMailService;
327 342
  343 + @Value("${actors.rule.allow_system_sms_service}")
  344 + @Getter
  345 + private boolean allowSystemSmsService;
  346 +
328 347 @Value("${transport.sessions.inactivity_timeout}")
329 348 @Getter
330 349 private long sessionInactivityTimeout;
... ...
... ... @@ -30,7 +30,6 @@ import org.thingsboard.server.common.data.Tenant;
30 30 import org.thingsboard.server.common.data.TenantProfile;
31 31 import org.thingsboard.server.common.data.id.EntityId;
32 32 import org.thingsboard.server.common.data.id.TenantId;
33   -import org.thingsboard.server.common.data.id.TenantProfileId;
34 33 import org.thingsboard.server.common.data.page.PageDataIterable;
35 34 import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
36 35 import org.thingsboard.server.common.msg.MsgType;
... ... @@ -41,7 +40,7 @@ import org.thingsboard.server.common.msg.queue.QueueToRuleEngineMsg;
41 40 import org.thingsboard.server.common.msg.queue.RuleEngineException;
42 41 import org.thingsboard.server.common.msg.queue.ServiceType;
43 42 import org.thingsboard.server.dao.tenant.TenantService;
44   -import org.thingsboard.server.service.profile.TbTenantProfileCache;
  43 +import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
45 44 import org.thingsboard.server.service.transport.msg.TransportToDeviceActorMsgWrapper;
46 45
47 46 import java.util.HashSet;
... ...
... ... @@ -28,14 +28,18 @@ import org.thingsboard.rule.engine.api.RuleEngineDeviceProfileCache;
28 28 import org.thingsboard.rule.engine.api.RuleEngineRpcService;
29 29 import org.thingsboard.rule.engine.api.RuleEngineTelemetryService;
30 30 import org.thingsboard.rule.engine.api.ScriptEngine;
  31 +import org.thingsboard.rule.engine.api.SmsService;
31 32 import org.thingsboard.rule.engine.api.TbContext;
32 33 import org.thingsboard.rule.engine.api.TbRelationTypes;
  34 +import org.thingsboard.rule.engine.api.sms.SmsSenderFactory;
33 35 import org.thingsboard.server.actors.ActorSystemContext;
34 36 import org.thingsboard.server.actors.TbActorRef;
  37 +import org.thingsboard.server.common.data.ApiUsageRecordKey;
35 38 import org.thingsboard.server.common.data.Customer;
36 39 import org.thingsboard.server.common.data.DataConstants;
37 40 import org.thingsboard.server.common.data.Device;
38 41 import org.thingsboard.server.common.data.DeviceProfile;
  42 +import org.thingsboard.server.common.data.TenantProfile;
39 43 import org.thingsboard.server.common.data.alarm.Alarm;
40 44 import org.thingsboard.server.common.data.asset.Asset;
41 45 import org.thingsboard.server.common.data.id.DeviceId;
... ... @@ -303,6 +307,11 @@ class DefaultTbContext implements TbContext {
303 307 }
304 308
305 309 @Override
  310 + public ListeningExecutor getSmsExecutor() {
  311 + return mainCtx.getSmsExecutor();
  312 + }
  313 +
  314 + @Override
306 315 public ListeningExecutor getDbCallbackExecutor() {
307 316 return mainCtx.getDbCallbackExecutor();
308 317 }
... ... @@ -428,6 +437,20 @@ class DefaultTbContext implements TbContext {
428 437 }
429 438
430 439 @Override
  440 + public SmsService getSmsService() {
  441 + if (mainCtx.isAllowSystemSmsService()) {
  442 + return mainCtx.getSmsService();
  443 + } else {
  444 + throw new RuntimeException("Access to System SMS Service is forbidden!");
  445 + }
  446 + }
  447 +
  448 + @Override
  449 + public SmsSenderFactory getSmsSenderFactory() {
  450 + return mainCtx.getSmsSenderFactory();
  451 + }
  452 +
  453 + @Override
431 454 public RuleEngineRpcService getRpcService() {
432 455 return mainCtx.getTbRuleEngineDeviceRpcService();
433 456 }
... ... @@ -489,13 +512,24 @@ class DefaultTbContext implements TbContext {
489 512 }
490 513
491 514 @Override
  515 + public void addTenantProfileListener(Consumer<TenantProfile> listener) {
  516 + mainCtx.getTenantProfileCache().addListener(getTenantId(), getSelfId(), listener);
  517 + }
  518 +
  519 + @Override
492 520 public void addDeviceProfileListeners(Consumer<DeviceProfile> profileListener, BiConsumer<DeviceId, DeviceProfile> deviceListener) {
493 521 mainCtx.getDeviceProfileCache().addListener(getTenantId(), getSelfId(), profileListener, deviceListener);
494 522 }
495 523
496 524 @Override
497   - public void removeProfileListener() {
  525 + public void removeListeners() {
498 526 mainCtx.getDeviceProfileCache().removeListener(getTenantId(), getSelfId());
  527 + mainCtx.getTenantProfileCache().removeListener(getTenantId(), getSelfId());
  528 + }
  529 +
  530 + @Override
  531 + public TenantProfile getTenantProfile() {
  532 + return mainCtx.getTenantProfileCache().get(getTenantId());
499 533 }
500 534
501 535 private TbMsgMetaData getActionMetaData(RuleNodeId ruleNodeId) {
... ...
... ... @@ -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());
... ...
... ... @@ -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 {
... ...
... ... @@ -33,6 +33,7 @@ import org.thingsboard.server.common.data.asset.Asset;
33 33 import org.thingsboard.server.common.data.asset.AssetInfo;
34 34 import org.thingsboard.server.common.data.asset.AssetSearchQuery;
35 35 import org.thingsboard.server.common.data.audit.ActionType;
  36 +import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
36 37 import org.thingsboard.server.common.data.exception.ThingsboardException;
37 38 import org.thingsboard.server.common.data.id.AssetId;
38 39 import org.thingsboard.server.common.data.id.CustomerId;
... ... @@ -51,6 +52,8 @@ import java.util.ArrayList;
51 52 import java.util.List;
52 53 import java.util.stream.Collectors;
53 54
  55 +import static org.thingsboard.server.dao.asset.BaseAssetService.TB_SERVICE_QUEUE;
  56 +
54 57 @RestController
55 58 @TbCoreComponent
56 59 @RequestMapping("/api")
... ... @@ -89,6 +92,10 @@ public class AssetController extends BaseController {
89 92 @ResponseBody
90 93 public Asset saveAsset(@RequestBody Asset asset) throws ThingsboardException {
91 94 try {
  95 + if (TB_SERVICE_QUEUE.equals(asset.getType())) {
  96 + throw new ThingsboardException("Unable to save asset with type " + TB_SERVICE_QUEUE, ThingsboardErrorCode.BAD_REQUEST_PARAMS);
  97 + }
  98 +
92 99 asset.setTenantId(getCurrentUser().getTenantId());
93 100
94 101 checkEntity(asset.getId(), asset, Resource.ASSET);
... ...
... ... @@ -35,7 +35,6 @@ import org.thingsboard.server.common.data.asset.AssetInfo;
35 35 import org.thingsboard.server.common.data.audit.ActionType;
36 36 import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
37 37 import org.thingsboard.server.common.data.exception.ThingsboardException;
38   -import org.thingsboard.server.common.data.id.*;
39 38 import org.thingsboard.server.common.data.id.AlarmId;
40 39 import org.thingsboard.server.common.data.id.AssetId;
41 40 import org.thingsboard.server.common.data.id.CustomerId;
... ... @@ -94,7 +93,7 @@ import org.thingsboard.server.queue.provider.TbQueueProducerProvider;
94 93 import org.thingsboard.server.queue.util.TbCoreComponent;
95 94 import org.thingsboard.server.service.component.ComponentDiscoveryService;
96 95 import org.thingsboard.server.service.profile.TbDeviceProfileCache;
97   -import org.thingsboard.server.service.profile.TbTenantProfileCache;
  96 +import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
98 97 import org.thingsboard.server.service.queue.TbClusterService;
99 98 import org.thingsboard.server.service.security.model.SecurityUser;
100 99 import org.thingsboard.server.service.security.permission.AccessControlService;
... ...
... ... @@ -118,6 +118,7 @@ public class DeviceController extends BaseController {
118 118
119 119 Device savedDevice = checkNotNull(deviceService.saveDeviceWithAccessToken(device, accessToken));
120 120
  121 + tbClusterService.onDeviceChange(savedDevice, null);
121 122 tbClusterService.pushMsgToCore(new DeviceNameOrTypeUpdateMsg(savedDevice.getTenantId(),
122 123 savedDevice.getId(), savedDevice.getName(), savedDevice.getType()), null);
123 124 tbClusterService.onEntityStateChange(savedDevice.getTenantId(), savedDevice.getId(),
... ... @@ -150,6 +151,9 @@ public class DeviceController extends BaseController {
150 151 Device device = checkDeviceId(deviceId, Operation.DELETE);
151 152 deviceService.deleteDevice(getCurrentUser().getTenantId(), deviceId);
152 153
  154 + tbClusterService.onDeviceDeleted(device, null);
  155 + tbClusterService.onEntityStateChange(device.getTenantId(), deviceId, ComponentLifecycleEvent.DELETED);
  156 +
153 157 logEntityAction(deviceId, device,
154 158 device.getCustomerId(),
155 159 ActionType.DELETED, null, strDeviceId);
... ...
... ... @@ -16,6 +16,8 @@
16 16 package org.thingsboard.server.controller;
17 17
18 18 import lombok.extern.slf4j.Slf4j;
  19 +import org.apache.commons.lang3.StringUtils;
  20 +import org.springframework.beans.factory.annotation.Autowired;
19 21 import org.springframework.http.HttpStatus;
20 22 import org.springframework.security.access.prepost.PreAuthorize;
21 23 import org.springframework.web.bind.annotation.PathVariable;
... ... @@ -35,21 +37,30 @@ import org.thingsboard.server.common.data.id.DeviceProfileId;
35 37 import org.thingsboard.server.common.data.page.PageData;
36 38 import org.thingsboard.server.common.data.page.PageLink;
37 39 import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
  40 +import org.thingsboard.server.dao.timeseries.TimeseriesService;
38 41 import org.thingsboard.server.queue.util.TbCoreComponent;
39 42 import org.thingsboard.server.service.security.permission.Operation;
40 43 import org.thingsboard.server.service.security.permission.Resource;
41 44
  45 +import java.util.List;
  46 +import java.util.UUID;
  47 +
42 48 @RestController
43 49 @TbCoreComponent
44 50 @RequestMapping("/api")
45 51 @Slf4j
46 52 public class DeviceProfileController extends BaseController {
47 53
  54 + private static final String DEVICE_PROFILE_ID = "deviceProfileId";
  55 +
  56 + @Autowired
  57 + private TimeseriesService timeseriesService;
  58 +
48 59 @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')")
49 60 @RequestMapping(value = "/deviceProfile/{deviceProfileId}", method = RequestMethod.GET)
50 61 @ResponseBody
51   - public DeviceProfile getDeviceProfileById(@PathVariable("deviceProfileId") String strDeviceProfileId) throws ThingsboardException {
52   - checkParameter("deviceProfileId", strDeviceProfileId);
  62 + public DeviceProfile getDeviceProfileById(@PathVariable(DEVICE_PROFILE_ID) String strDeviceProfileId) throws ThingsboardException {
  63 + checkParameter(DEVICE_PROFILE_ID, strDeviceProfileId);
53 64 try {
54 65 DeviceProfileId deviceProfileId = new DeviceProfileId(toUUID(strDeviceProfileId));
55 66 return checkDeviceProfileId(deviceProfileId, Operation.READ);
... ... @@ -61,8 +72,8 @@ public class DeviceProfileController extends BaseController {
61 72 @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
62 73 @RequestMapping(value = "/deviceProfileInfo/{deviceProfileId}", method = RequestMethod.GET)
63 74 @ResponseBody
64   - public DeviceProfileInfo getDeviceProfileInfoById(@PathVariable("deviceProfileId") String strDeviceProfileId) throws ThingsboardException {
65   - checkParameter("deviceProfileId", strDeviceProfileId);
  75 + public DeviceProfileInfo getDeviceProfileInfoById(@PathVariable(DEVICE_PROFILE_ID) String strDeviceProfileId) throws ThingsboardException {
  76 + checkParameter(DEVICE_PROFILE_ID, strDeviceProfileId);
66 77 try {
67 78 DeviceProfileId deviceProfileId = new DeviceProfileId(toUUID(strDeviceProfileId));
68 79 return checkNotNull(deviceProfileService.findDeviceProfileInfoById(getTenantId(), deviceProfileId));
... ... @@ -82,6 +93,46 @@ public class DeviceProfileController extends BaseController {
82 93 }
83 94 }
84 95
  96 + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')")
  97 + @RequestMapping(value = "/deviceProfile/devices/keys/timeseries", method = RequestMethod.GET)
  98 + @ResponseBody
  99 + public List<String> getTimeseriesKeys(
  100 + @RequestParam(name = DEVICE_PROFILE_ID, required = false) String deviceProfileIdStr) throws ThingsboardException {
  101 + DeviceProfileId deviceProfileId;
  102 + if (StringUtils.isNotEmpty(deviceProfileIdStr)) {
  103 + deviceProfileId = new DeviceProfileId(UUID.fromString(deviceProfileIdStr));
  104 + checkDeviceProfileId(deviceProfileId, Operation.READ);
  105 + } else {
  106 + deviceProfileId = null;
  107 + }
  108 +
  109 + try {
  110 + return timeseriesService.findAllKeysByDeviceProfileId(getTenantId(), deviceProfileId);
  111 + } catch (Exception e) {
  112 + throw handleException(e);
  113 + }
  114 + }
  115 +
  116 + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')")
  117 + @RequestMapping(value = "/deviceProfile/devices/keys/attributes", method = RequestMethod.GET)
  118 + @ResponseBody
  119 + public List<String> getAttributesKeys(
  120 + @RequestParam(name = DEVICE_PROFILE_ID, required = false) String deviceProfileIdStr) throws ThingsboardException {
  121 + DeviceProfileId deviceProfileId;
  122 + if (StringUtils.isNotEmpty(deviceProfileIdStr)) {
  123 + deviceProfileId = new DeviceProfileId(UUID.fromString(deviceProfileIdStr));
  124 + checkDeviceProfileId(deviceProfileId, Operation.READ);
  125 + } else {
  126 + deviceProfileId = null;
  127 + }
  128 +
  129 + try {
  130 + return attributesService.findAllKeysByDeviceProfileId(getTenantId(), deviceProfileId);
  131 + } catch (Exception e) {
  132 + throw handleException(e);
  133 + }
  134 + }
  135 +
85 136 @PreAuthorize("hasAuthority('TENANT_ADMIN')")
86 137 @RequestMapping(value = "/deviceProfile", method = RequestMethod.POST)
87 138 @ResponseBody
... ... @@ -113,8 +164,8 @@ public class DeviceProfileController extends BaseController {
113 164 @PreAuthorize("hasAuthority('TENANT_ADMIN')")
114 165 @RequestMapping(value = "/deviceProfile/{deviceProfileId}", method = RequestMethod.DELETE)
115 166 @ResponseStatus(value = HttpStatus.OK)
116   - public void deleteDeviceProfile(@PathVariable("deviceProfileId") String strDeviceProfileId) throws ThingsboardException {
117   - checkParameter("deviceProfileId", strDeviceProfileId);
  167 + public void deleteDeviceProfile(@PathVariable(DEVICE_PROFILE_ID) String strDeviceProfileId) throws ThingsboardException {
  168 + checkParameter(DEVICE_PROFILE_ID, strDeviceProfileId);
118 169 try {
119 170 DeviceProfileId deviceProfileId = new DeviceProfileId(toUUID(strDeviceProfileId));
120 171 DeviceProfile deviceProfile = checkDeviceProfileId(deviceProfileId, Operation.DELETE);
... ... @@ -139,8 +190,8 @@ public class DeviceProfileController extends BaseController {
139 190 @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')")
140 191 @RequestMapping(value = "/deviceProfile/{deviceProfileId}/default", method = RequestMethod.POST)
141 192 @ResponseBody
142   - public DeviceProfile setDefaultDeviceProfile(@PathVariable("deviceProfileId") String strDeviceProfileId) throws ThingsboardException {
143   - checkParameter("deviceProfileId", strDeviceProfileId);
  193 + public DeviceProfile setDefaultDeviceProfile(@PathVariable(DEVICE_PROFILE_ID) String strDeviceProfileId) throws ThingsboardException {
  194 + checkParameter(DEVICE_PROFILE_ID, strDeviceProfileId);
144 195 try {
145 196 DeviceProfileId deviceProfileId = new DeviceProfileId(toUUID(strDeviceProfileId));
146 197 DeviceProfile deviceProfile = checkDeviceProfileId(deviceProfileId, Operation.WRITE);
... ...
... ... @@ -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));
... ...
... ... @@ -186,6 +186,10 @@ public class ThingsboardInstallService {
186 186 systemDataLoaderService.updateSystemWidgets();
187 187 systemDataLoaderService.createOAuth2Templates();
188 188 break;
  189 + case "3.2.0":
  190 + log.info("Upgrading ThingsBoard from version 3.2.0 to 3.2.1 ...");
  191 + databaseEntitiesUpgradeService.upgradeDatabase("3.2.0");
  192 + break;
189 193 default:
190 194 throw new RuntimeException("Unable to upgrade ThingsBoard, unsupported fromVersion: " + upgradeFromVersion);
191 195
... ...
... ... @@ -17,18 +17,21 @@ package org.thingsboard.server.service.apiusage;
17 17
18 18 import com.google.common.util.concurrent.FutureCallback;
19 19 import lombok.extern.slf4j.Slf4j;
  20 +import org.apache.commons.lang3.StringUtils;
20 21 import org.checkerframework.checker.nullness.qual.Nullable;
21 22 import org.springframework.beans.factory.annotation.Autowired;
22 23 import org.springframework.beans.factory.annotation.Value;
23 24 import org.springframework.context.annotation.Lazy;
24   -import org.springframework.data.util.Pair;
25 25 import org.springframework.stereotype.Service;
  26 +import org.thingsboard.rule.engine.api.MailService;
26 27 import org.thingsboard.server.common.data.ApiFeature;
27 28 import org.thingsboard.server.common.data.ApiUsageRecordKey;
28 29 import org.thingsboard.server.common.data.ApiUsageState;
  30 +import org.thingsboard.server.common.data.ApiUsageStateMailMessage;
29 31 import org.thingsboard.server.common.data.ApiUsageStateValue;
30 32 import org.thingsboard.server.common.data.Tenant;
31 33 import org.thingsboard.server.common.data.TenantProfile;
  34 +import org.thingsboard.server.common.data.exception.ThingsboardException;
32 35 import org.thingsboard.server.common.data.id.ApiUsageStateId;
33 36 import org.thingsboard.server.common.data.id.TenantId;
34 37 import org.thingsboard.server.common.data.id.TenantProfileId;
... ... @@ -42,6 +45,7 @@ import org.thingsboard.server.common.data.tenant.profile.TenantProfileData;
42 45 import org.thingsboard.server.common.msg.queue.ServiceType;
43 46 import org.thingsboard.server.common.msg.queue.TbCallback;
44 47 import org.thingsboard.server.common.msg.tools.SchedulerUtils;
  48 +import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
45 49 import org.thingsboard.server.dao.tenant.TenantService;
46 50 import org.thingsboard.server.dao.timeseries.TimeseriesService;
47 51 import org.thingsboard.server.dao.usagerecord.ApiUsageStateService;
... ... @@ -51,14 +55,13 @@ import org.thingsboard.server.queue.common.TbProtoQueueMsg;
51 55 import org.thingsboard.server.queue.discovery.PartitionChangeEvent;
52 56 import org.thingsboard.server.queue.discovery.PartitionService;
53 57 import org.thingsboard.server.queue.scheduler.SchedulerComponent;
54   -import org.thingsboard.server.queue.util.TbCoreComponent;
55   -import org.thingsboard.server.service.profile.TbTenantProfileCache;
56 58 import org.thingsboard.server.service.queue.TbClusterService;
57 59 import org.thingsboard.server.service.telemetry.InternalTelemetryService;
58 60
59 61 import javax.annotation.PostConstruct;
  62 +import javax.annotation.PreDestroy;
60 63 import java.util.ArrayList;
61   -import java.util.HashMap;
  64 +import java.util.Arrays;
62 65 import java.util.HashSet;
63 66 import java.util.List;
64 67 import java.util.Map;
... ... @@ -66,9 +69,12 @@ import java.util.Set;
66 69 import java.util.UUID;
67 70 import java.util.concurrent.ConcurrentHashMap;
68 71 import java.util.concurrent.ExecutionException;
  72 +import java.util.concurrent.ExecutorService;
  73 +import java.util.concurrent.Executors;
69 74 import java.util.concurrent.TimeUnit;
70 75 import java.util.concurrent.locks.Lock;
71 76 import java.util.concurrent.locks.ReentrantLock;
  77 +import java.util.stream.Collectors;
72 78
73 79 @Slf4j
74 80 @Service
... ... @@ -91,6 +97,7 @@ public class DefaultTbApiUsageStateService implements TbApiUsageStateService {
91 97 private final ApiUsageStateService apiUsageStateService;
92 98 private final SchedulerComponent scheduler;
93 99 private final TbTenantProfileCache tenantProfileCache;
  100 + private final MailService mailService;
94 101
95 102 @Lazy
96 103 @Autowired
... ... @@ -109,13 +116,15 @@ public class DefaultTbApiUsageStateService implements TbApiUsageStateService {
109 116
110 117 private final Lock updateLock = new ReentrantLock();
111 118
  119 + private final ExecutorService mailExecutor;
  120 +
112 121 public DefaultTbApiUsageStateService(TbClusterService clusterService,
113 122 PartitionService partitionService,
114 123 TenantService tenantService,
115 124 TimeseriesService tsService,
116 125 ApiUsageStateService apiUsageStateService,
117 126 SchedulerComponent scheduler,
118   - TbTenantProfileCache tenantProfileCache) {
  127 + TbTenantProfileCache tenantProfileCache, MailService mailService) {
119 128 this.clusterService = clusterService;
120 129 this.partitionService = partitionService;
121 130 this.tenantService = tenantService;
... ... @@ -123,14 +132,16 @@ public class DefaultTbApiUsageStateService implements TbApiUsageStateService {
123 132 this.apiUsageStateService = apiUsageStateService;
124 133 this.scheduler = scheduler;
125 134 this.tenantProfileCache = tenantProfileCache;
  135 + this.mailService = mailService;
  136 + this.mailExecutor = Executors.newSingleThreadExecutor();
126 137 }
127 138
128 139 @PostConstruct
129 140 public void init() {
130 141 if (enabled) {
131 142 log.info("Starting api usage service.");
132   - initStatesFromDataBase();
133 143 scheduler.scheduleAtFixedRate(this::checkStartOfNextCycle, nextCycleCheckInterval, nextCycleCheckInterval, TimeUnit.MILLISECONDS);
  144 + log.info("Started api usage service.");
134 145 }
135 146 }
136 147
... ... @@ -138,6 +149,11 @@ public class DefaultTbApiUsageStateService implements TbApiUsageStateService {
138 149 public void process(TbProtoQueueMsg<ToUsageStatsServiceMsg> msg, TbCallback callback) {
139 150 ToUsageStatsServiceMsg statsMsg = msg.getValue();
140 151 TenantId tenantId = new TenantId(new UUID(statsMsg.getTenantIdMSB(), statsMsg.getTenantIdLSB()));
  152 +
  153 + if (tenantProfileCache.get(tenantId) == null) {
  154 + return;
  155 + }
  156 +
141 157 TenantApiUsageState tenantState;
142 158 List<TsKvEntry> updatedEntries;
143 159 Map<ApiFeature, ApiUsageStateValue> result;
... ... @@ -157,7 +173,7 @@ public class DefaultTbApiUsageStateService implements TbApiUsageStateService {
157 173 long newValue = tenantState.add(recordKey, kvProto.getValue());
158 174 updatedEntries.add(new BasicTsKvEntry(ts, new LongDataEntry(recordKey.getApiCountKey(), newValue)));
159 175 long newHourlyValue = tenantState.addToHourly(recordKey, kvProto.getValue());
160   - updatedEntries.add(new BasicTsKvEntry(hourTs, new LongDataEntry(recordKey.getApiCountKey() + HOURLY, newHourlyValue)));
  176 + updatedEntries.add(new BasicTsKvEntry(newHourTs, new LongDataEntry(recordKey.getApiCountKey() + HOURLY, newHourlyValue)));
161 177 apiFeatures.add(recordKey.getApiFeature());
162 178 }
163 179 result = tenantState.checkStateUpdatedDueToThreshold(apiFeatures);
... ... @@ -218,6 +234,7 @@ public class DefaultTbApiUsageStateService implements TbApiUsageStateService {
218 234
219 235 @Override
220 236 public void onTenantProfileUpdate(TenantProfileId tenantProfileId) {
  237 + log.info("[{}] On Tenant Profile Update", tenantProfileId);
221 238 TenantProfile tenantProfile = tenantProfileCache.get(tenantProfileId);
222 239 updateLock.lock();
223 240 try {
... ... @@ -233,6 +250,7 @@ public class DefaultTbApiUsageStateService implements TbApiUsageStateService {
233 250
234 251 @Override
235 252 public void onTenantUpdate(TenantId tenantId) {
  253 + log.info("[{}] On Tenant Update.", tenantId);
236 254 TenantProfile tenantProfile = tenantProfileCache.get(tenantId);
237 255 updateLock.lock();
238 256 try {
... ... @@ -245,16 +263,16 @@ public class DefaultTbApiUsageStateService implements TbApiUsageStateService {
245 263 }
246 264 }
247 265
248   - private void updateTenantState(TenantApiUsageState state, TenantProfile tenantProfile) {
  266 + private void updateTenantState(TenantApiUsageState state, TenantProfile profile) {
249 267 TenantProfileData oldProfileData = state.getTenantProfileData();
250   - state.setTenantProfileId(tenantProfile.getId());
251   - state.setTenantProfileData(tenantProfile.getProfileData());
  268 + state.setTenantProfileId(profile.getId());
  269 + state.setTenantProfileData(profile.getProfileData());
252 270 Map<ApiFeature, ApiUsageStateValue> result = state.checkStateUpdatedDueToThresholds();
253 271 if (!result.isEmpty()) {
254 272 persistAndNotify(state, result);
255 273 }
256 274 updateProfileThresholds(state.getTenantId(), state.getApiUsageState().getId(),
257   - oldProfileData.getConfiguration(), tenantProfile.getProfileData().getConfiguration());
  275 + oldProfileData.getConfiguration(), profile.getProfileData().getConfiguration());
258 276 }
259 277
260 278 private void updateProfileThresholds(TenantId tenantId, ApiUsageStateId id,
... ... @@ -281,7 +299,49 @@ public class DefaultTbApiUsageStateService implements TbApiUsageStateService {
281 299 List<TsKvEntry> stateTelemetry = new ArrayList<>();
282 300 result.forEach(((apiFeature, aState) -> stateTelemetry.add(new BasicTsKvEntry(ts, new StringDataEntry(apiFeature.getApiStateKey(), aState.name())))));
283 301 tsWsService.saveAndNotifyInternal(state.getTenantId(), state.getApiUsageState().getId(), stateTelemetry, VOID_CALLBACK);
284   - //TODO: notify tenant admin via email!
  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);
285 345 }
286 346
287 347 private void checkStartOfNextCycle() {
... ... @@ -289,8 +349,11 @@ public class DefaultTbApiUsageStateService implements TbApiUsageStateService {
289 349 try {
290 350 long now = System.currentTimeMillis();
291 351 myTenantStates.values().forEach(state -> {
292   - if ((state.getNextCycleTs() > now) && (state.getNextCycleTs() - now < TimeUnit.HOURS.toMillis(1))) {
  352 + if ((state.getNextCycleTs() < now) && (now - state.getNextCycleTs() < TimeUnit.HOURS.toMillis(1))) {
  353 + TenantId tenantId = state.getTenantId();
293 354 state.setCycles(state.getNextCycleTs(), SchedulerUtils.getStartOfNextNextMonth());
  355 + saveNewCounts(state, Arrays.asList(ApiUsageRecordKey.values()));
  356 + updateTenantState(state, tenantProfileCache.get(tenantId));
294 357 }
295 358 });
296 359 } finally {
... ... @@ -298,6 +361,14 @@ public class DefaultTbApiUsageStateService implements TbApiUsageStateService {
298 361 }
299 362 }
300 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 +
301 372 private TenantApiUsageState getOrFetchState(TenantId tenantId) {
302 373 TenantApiUsageState tenantState = myTenantStates.get(tenantId);
303 374 if (tenantState == null) {
... ... @@ -311,6 +382,7 @@ public class DefaultTbApiUsageStateService implements TbApiUsageStateService {
311 382 }
312 383 TenantProfile tenantProfile = tenantProfileCache.get(tenantId);
313 384 tenantState = new TenantApiUsageState(tenantProfile, dbStateEntity);
  385 + List<ApiUsageRecordKey> newCounts = new ArrayList<>();
314 386 try {
315 387 List<TsKvEntry> dbValues = tsService.findAllLatest(tenantId, dbStateEntity.getId()).get();
316 388 for (ApiUsageRecordKey key : ApiUsageRecordKey.values()) {
... ... @@ -319,7 +391,13 @@ public class DefaultTbApiUsageStateService implements TbApiUsageStateService {
319 391 for (TsKvEntry tsKvEntry : dbValues) {
320 392 if (tsKvEntry.getKey().equals(key.getApiCountKey())) {
321 393 cycleEntryFound = true;
322   - tenantState.put(key, tsKvEntry.getTs() == tenantState.getCurrentCycleTs() ? tsKvEntry.getLongValue().get() : 0L);
  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 + }
323 401 } else if (tsKvEntry.getKey().equals(key.getApiCountKey() + HOURLY)) {
324 402 hourlyEntryFound = true;
325 403 tenantState.putHourly(key, tsKvEntry.getTs() == tenantState.getCurrentHourTs() ? tsKvEntry.getLongValue().get() : 0L);
... ... @@ -331,6 +409,7 @@ public class DefaultTbApiUsageStateService implements TbApiUsageStateService {
331 409 }
332 410 log.debug("[{}] Initialized state: {}", tenantId, dbStateEntity);
333 411 myTenantStates.put(tenantId, tenantState);
  412 + saveNewCounts(tenantState, newCounts);
334 413 } catch (InterruptedException | ExecutionException e) {
335 414 log.warn("[{}] Failed to fetch api usage state from db.", tenantId, e);
336 415 }
... ... @@ -340,12 +419,15 @@ public class DefaultTbApiUsageStateService implements TbApiUsageStateService {
340 419
341 420 private void initStatesFromDataBase() {
342 421 try {
  422 + log.info("Initializing tenant states.");
343 423 PageDataIterable<Tenant> tenantIterator = new PageDataIterable<>(tenantService::findTenants, 1024);
344 424 for (Tenant tenant : tenantIterator) {
345 425 if (!myTenantStates.containsKey(tenant.getId()) && partitionService.resolve(ServiceType.TB_CORE, tenant.getId(), tenant.getId()).isMyPartition()) {
  426 + log.debug("[{}] Initializing tenant state.", tenant.getId());
346 427 updateLock.lock();
347 428 try {
348 429 updateTenantState(getOrFetchState(tenant.getId()), tenantProfileCache.get(tenant.getTenantProfileId()));
  430 + log.debug("[{}] Initialized tenant state.", tenant.getId());
349 431 } catch (Exception e) {
350 432 log.warn("[{}] Failed to initialize tenant API state", tenant.getId(), e);
351 433 } finally {
... ... @@ -353,10 +435,16 @@ public class DefaultTbApiUsageStateService implements TbApiUsageStateService {
353 435 }
354 436 }
355 437 }
356   - log.info("Api usage service started.");
  438 + log.info("Initialized tenant states.");
357 439 } catch (Exception e) {
358 440 log.warn("Unknown failure", e);
359 441 }
360 442 }
361 443
  444 + @PreDestroy
  445 + private void destroy() {
  446 + if (mailExecutor != null) {
  447 + mailExecutor.shutdownNow();
  448 + }
  449 + }
362 450 }
... ...
... ... @@ -125,6 +125,10 @@ public class TenantApiUsageState {
125 125 return apiUsageState.getDbStorageState();
126 126 case JS:
127 127 return apiUsageState.getJsExecState();
  128 + case EMAIL:
  129 + return apiUsageState.getEmailExecState();
  130 + case SMS:
  131 + return apiUsageState.getSmsExecState();
128 132 default:
129 133 return ApiUsageStateValue.ENABLED;
130 134 }
... ... @@ -145,6 +149,12 @@ public class TenantApiUsageState {
145 149 case JS:
146 150 apiUsageState.setJsExecState(value);
147 151 break;
  152 + case EMAIL:
  153 + apiUsageState.setEmailExecState(value);
  154 + break;
  155 + case SMS:
  156 + apiUsageState.setSmsExecState(value);
  157 + break;
148 158 }
149 159 return !currentValue.equals(value);
150 160 }
... ...
... ... @@ -18,10 +18,9 @@ package org.thingsboard.server.service.device;
18 18 import com.fasterxml.jackson.core.JsonProcessingException;
19 19 import com.fasterxml.jackson.databind.JsonNode;
20 20 import com.fasterxml.jackson.databind.node.ObjectNode;
21   -import com.google.common.util.concurrent.Futures;
22 21 import com.google.common.util.concurrent.ListenableFuture;
23   -import com.google.common.util.concurrent.MoreExecutors;
24 22 import lombok.extern.slf4j.Slf4j;
  23 +import org.apache.commons.lang.RandomStringUtils;
25 24 import org.springframework.beans.factory.annotation.Autowired;
26 25 import org.springframework.stereotype.Service;
27 26 import org.springframework.util.StringUtils;
... ... @@ -113,6 +112,13 @@ public class DeviceProvisionServiceImpl implements DeviceProvisionService {
113 112 public ProvisionResponse provisionDevice(ProvisionRequest provisionRequest) {
114 113 String provisionRequestKey = provisionRequest.getCredentials().getProvisionDeviceKey();
115 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 + }
116 122
117 123 if (StringUtils.isEmpty(provisionRequestKey) || StringUtils.isEmpty(provisionRequestSecret)) {
118 124 throw new ProvisionFailedException(ProvisionResponseStatus.NOT_FOUND.name());
... ... @@ -188,6 +194,11 @@ public class DeviceProvisionServiceImpl implements DeviceProvisionService {
188 194 Device device = deviceService.findDeviceByTenantIdAndName(profile.getTenantId(), provisionRequest.getDeviceName());
189 195 try {
190 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 + }
191 202 Device savedDevice = deviceService.saveDevice(provisionRequest, profile);
192 203
193 204 deviceStateService.onDeviceAdded(savedDevice);
... ...
... ... @@ -28,12 +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.tenant.profile.DefaultTenantProfileConfiguration;
34   -import org.thingsboard.server.common.data.tenant.profile.TenantProfileData;
35 36 import org.thingsboard.server.common.data.User;
36   -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;
37 46 import org.thingsboard.server.common.data.id.CustomerId;
38 47 import org.thingsboard.server.common.data.id.DeviceId;
39 48 import org.thingsboard.server.common.data.id.DeviceProfileId;
... ... @@ -42,19 +51,29 @@ import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry;
42 51 import org.thingsboard.server.common.data.kv.BooleanDataEntry;
43 52 import org.thingsboard.server.common.data.kv.DoubleDataEntry;
44 53 import org.thingsboard.server.common.data.kv.LongDataEntry;
45   -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;
46 64 import org.thingsboard.server.common.data.security.Authority;
47 65 import org.thingsboard.server.common.data.security.DeviceCredentials;
48 66 import org.thingsboard.server.common.data.security.UserCredentials;
  67 +import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration;
  68 +import org.thingsboard.server.common.data.tenant.profile.TenantProfileData;
49 69 import org.thingsboard.server.common.data.widget.WidgetsBundle;
50   -import org.thingsboard.server.dao.asset.AssetService;
51 70 import org.thingsboard.server.dao.attributes.AttributesService;
52 71 import org.thingsboard.server.dao.customer.CustomerService;
53 72 import org.thingsboard.server.dao.device.DeviceCredentialsService;
54 73 import org.thingsboard.server.dao.device.DeviceProfileService;
55 74 import org.thingsboard.server.dao.device.DeviceService;
56 75 import org.thingsboard.server.dao.exception.DataValidationException;
57   -import org.thingsboard.server.dao.relation.RelationService;
  76 +import org.thingsboard.server.dao.rule.RuleChainService;
58 77 import org.thingsboard.server.dao.settings.AdminSettingsService;
59 78 import org.thingsboard.server.dao.tenant.TenantProfileService;
60 79 import org.thingsboard.server.dao.tenant.TenantService;
... ... @@ -62,6 +81,8 @@ import org.thingsboard.server.dao.user.UserService;
62 81 import org.thingsboard.server.dao.widget.WidgetsBundleService;
63 82
64 83 import java.util.Arrays;
  84 +import java.util.Collections;
  85 +import java.util.TreeMap;
65 86
66 87 @Service
67 88 @Profile("install")
... ... @@ -97,12 +118,6 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService {
97 118 private CustomerService customerService;
98 119
99 120 @Autowired
100   - private RelationService relationService;
101   -
102   - @Autowired
103   - private AssetService assetService;
104   -
105   - @Autowired
106 121 private DeviceService deviceService;
107 122
108 123 @Autowired
... ... @@ -114,6 +129,9 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService {
114 129 @Autowired
115 130 private DeviceCredentialsService deviceCredentialsService;
116 131
  132 + @Autowired
  133 + private RuleChainService ruleChainService;
  134 +
117 135 @Bean
118 136 protected BCryptPasswordEncoder passwordEncoder() {
119 137 return new BCryptPasswordEncoder();
... ... @@ -134,7 +152,6 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService {
134 152 TenantProfile isolatedTbCoreProfile = new TenantProfile();
135 153 isolatedTbCoreProfile.setDefault(false);
136 154 isolatedTbCoreProfile.setName("Isolated TB Core");
137   - isolatedTbCoreProfile.setProfileData(new TenantProfileData());
138 155 isolatedTbCoreProfile.setDescription("Isolated TB Core tenant profile");
139 156 isolatedTbCoreProfile.setIsolatedTbCore(true);
140 157 isolatedTbCoreProfile.setIsolatedTbRuleEngine(false);
... ... @@ -148,7 +165,6 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService {
148 165 TenantProfile isolatedTbRuleEngineProfile = new TenantProfile();
149 166 isolatedTbRuleEngineProfile.setDefault(false);
150 167 isolatedTbRuleEngineProfile.setName("Isolated TB Rule Engine");
151   - isolatedTbRuleEngineProfile.setProfileData(new TenantProfileData());
152 168 isolatedTbRuleEngineProfile.setDescription("Isolated TB Rule Engine tenant profile");
153 169 isolatedTbRuleEngineProfile.setIsolatedTbCore(false);
154 170 isolatedTbRuleEngineProfile.setIsolatedTbRuleEngine(true);
... ... @@ -163,7 +179,6 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService {
163 179 TenantProfile isolatedTbCoreAndTbRuleEngineProfile = new TenantProfile();
164 180 isolatedTbCoreAndTbRuleEngineProfile.setDefault(false);
165 181 isolatedTbCoreAndTbRuleEngineProfile.setName("Isolated TB Core and TB Rule Engine");
166   - isolatedTbCoreAndTbRuleEngineProfile.setProfileData(new TenantProfileData());
167 182 isolatedTbCoreAndTbRuleEngineProfile.setDescription("Isolated TB Core and TB Rule Engine tenant profile");
168 183 isolatedTbCoreAndTbRuleEngineProfile.setIsolatedTbCore(true);
169 184 isolatedTbCoreAndTbRuleEngineProfile.setIsolatedTbRuleEngine(true);
... ... @@ -248,35 +263,149 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService {
248 263 createDevice(demoTenant.getId(), null, defaultDeviceProfile.getId(), "Raspberry Pi Demo Device", "RASPBERRY_PI_DEMO_TOKEN", "Demo device that is used in " +
249 264 "Raspberry Pi GPIO control sample application");
250 265
251   - Asset thermostatAlarms = new Asset();
252   - thermostatAlarms.setTenantId(demoTenant.getId());
253   - thermostatAlarms.setName("Thermostat Alarms");
254   - thermostatAlarms.setType("AlarmPropagationAsset");
255   - thermostatAlarms = assetService.saveAsset(thermostatAlarms);
256   -
257   - DeviceProfile thermostatDeviceProfile = this.deviceProfileService.findOrCreateDeviceProfile(demoTenant.getId(), "thermostat");
258   -
259   - DeviceId t1Id = createDevice(demoTenant.getId(), null, thermostatDeviceProfile.getId(), "Thermostat T1", "T1_TEST_TOKEN", "Demo device for Thermostats dashboard").getId();
260   - DeviceId t2Id = createDevice(demoTenant.getId(), null, thermostatDeviceProfile.getId(), "Thermostat T2", "T2_TEST_TOKEN", "Demo device for Thermostats dashboard").getId();
261   -
262   - relationService.saveRelation(thermostatAlarms.getTenantId(), new EntityRelation(thermostatAlarms.getId(), t1Id, "ToAlarmPropagationAsset"));
263   - relationService.saveRelation(thermostatAlarms.getTenantId(), new EntityRelation(thermostatAlarms.getId(), t2Id, "ToAlarmPropagationAsset"));
  266 + DeviceProfile thermostatDeviceProfile = new DeviceProfile();
  267 + thermostatDeviceProfile.setTenantId(demoTenant.getId());
  268 + thermostatDeviceProfile.setDefault(false);
  269 + thermostatDeviceProfile.setName("thermostat");
  270 + thermostatDeviceProfile.setType(DeviceProfileType.DEFAULT);
  271 + thermostatDeviceProfile.setTransportType(DeviceTransportType.DEFAULT);
  272 + thermostatDeviceProfile.setProvisionType(DeviceProfileProvisionType.DISABLED);
  273 + thermostatDeviceProfile.setDescription("Thermostat device profile");
  274 + thermostatDeviceProfile.setDefaultRuleChainId(ruleChainService.findTenantRuleChains(
  275 + demoTenant.getId(), new PageLink(1, 0, "Thermostat")).getData().get(0).getId());
  276 +
  277 + DeviceProfileData deviceProfileData = new DeviceProfileData();
  278 + DefaultDeviceProfileConfiguration configuration = new DefaultDeviceProfileConfiguration();
  279 + DefaultDeviceProfileTransportConfiguration transportConfiguration = new DefaultDeviceProfileTransportConfiguration();
  280 + DisabledDeviceProfileProvisionConfiguration provisionConfiguration = new DisabledDeviceProfileProvisionConfiguration(null);
  281 + deviceProfileData.setConfiguration(configuration);
  282 + deviceProfileData.setTransportConfiguration(transportConfiguration);
  283 + deviceProfileData.setProvisionConfiguration(provisionConfiguration);
  284 + thermostatDeviceProfile.setProfileData(deviceProfileData);
  285 +
  286 + DeviceProfileAlarm highTemperature = new DeviceProfileAlarm();
  287 + highTemperature.setId("highTemperatureAlarmID");
  288 + highTemperature.setAlarmType("High Temperature");
  289 + AlarmRule temperatureRule = new AlarmRule();
  290 + AlarmCondition temperatureCondition = new AlarmCondition();
  291 + temperatureCondition.setSpec(new SimpleAlarmConditionSpec());
  292 +
  293 + KeyFilter temperatureAlarmFlagAttributeFilter = new KeyFilter();
  294 + temperatureAlarmFlagAttributeFilter.setKey(new EntityKey(EntityKeyType.ATTRIBUTE, "temperatureAlarmFlag"));
  295 + temperatureAlarmFlagAttributeFilter.setValueType(EntityKeyValueType.BOOLEAN);
  296 + BooleanFilterPredicate temperatureAlarmFlagAttributePredicate = new BooleanFilterPredicate();
  297 + temperatureAlarmFlagAttributePredicate.setOperation(BooleanFilterPredicate.BooleanOperation.EQUAL);
  298 + temperatureAlarmFlagAttributePredicate.setValue(new FilterPredicateValue<>(Boolean.TRUE));
  299 + temperatureAlarmFlagAttributeFilter.setPredicate(temperatureAlarmFlagAttributePredicate);
  300 +
  301 + KeyFilter temperatureTimeseriesFilter = new KeyFilter();
  302 + temperatureTimeseriesFilter.setKey(new EntityKey(EntityKeyType.TIME_SERIES, "temperature"));
  303 + temperatureTimeseriesFilter.setValueType(EntityKeyValueType.NUMERIC);
  304 + NumericFilterPredicate temperatureTimeseriesFilterPredicate = new NumericFilterPredicate();
  305 + temperatureTimeseriesFilterPredicate.setOperation(NumericFilterPredicate.NumericOperation.GREATER);
  306 + FilterPredicateValue<Double> temperatureTimeseriesPredicateValue =
  307 + new FilterPredicateValue<>(25.0, null,
  308 + new DynamicValue<>(DynamicValueSourceType.CURRENT_DEVICE, "temperatureAlarmThreshold"));
  309 + temperatureTimeseriesFilterPredicate.setValue(temperatureTimeseriesPredicateValue);
  310 + temperatureTimeseriesFilter.setPredicate(temperatureTimeseriesFilterPredicate);
  311 + temperatureCondition.setCondition(Arrays.asList(temperatureAlarmFlagAttributeFilter, temperatureTimeseriesFilter));
  312 + temperatureRule.setAlarmDetails("Current temperature = ${temperature}");
  313 + temperatureRule.setCondition(temperatureCondition);
  314 + highTemperature.setCreateRules(new TreeMap<>(Collections.singletonMap(AlarmSeverity.MAJOR, temperatureRule)));
  315 +
  316 + AlarmRule clearTemperatureRule = new AlarmRule();
  317 + AlarmCondition clearTemperatureCondition = new AlarmCondition();
  318 + clearTemperatureCondition.setSpec(new SimpleAlarmConditionSpec());
  319 +
  320 + KeyFilter clearTemperatureTimeseriesFilter = new KeyFilter();
  321 + clearTemperatureTimeseriesFilter.setKey(new EntityKey(EntityKeyType.TIME_SERIES, "temperature"));
  322 + clearTemperatureTimeseriesFilter.setValueType(EntityKeyValueType.NUMERIC);
  323 + NumericFilterPredicate clearTemperatureTimeseriesFilterPredicate = new NumericFilterPredicate();
  324 + clearTemperatureTimeseriesFilterPredicate.setOperation(NumericFilterPredicate.NumericOperation.LESS_OR_EQUAL);
  325 + FilterPredicateValue<Double> clearTemperatureTimeseriesPredicateValue =
  326 + new FilterPredicateValue<>(25.0, null,
  327 + new DynamicValue<>(DynamicValueSourceType.CURRENT_DEVICE, "temperatureAlarmThreshold"));
  328 +
  329 + clearTemperatureTimeseriesFilterPredicate.setValue(clearTemperatureTimeseriesPredicateValue);
  330 + clearTemperatureTimeseriesFilter.setPredicate(clearTemperatureTimeseriesFilterPredicate);
  331 + clearTemperatureCondition.setCondition(Collections.singletonList(clearTemperatureTimeseriesFilter));
  332 + clearTemperatureRule.setCondition(clearTemperatureCondition);
  333 + clearTemperatureRule.setAlarmDetails("Current temperature = ${temperature}");
  334 + highTemperature.setClearRule(clearTemperatureRule);
  335 +
  336 + DeviceProfileAlarm lowHumidity = new DeviceProfileAlarm();
  337 + lowHumidity.setId("lowHumidityAlarmID");
  338 + lowHumidity.setAlarmType("Low Humidity");
  339 + AlarmRule humidityRule = new AlarmRule();
  340 + AlarmCondition humidityCondition = new AlarmCondition();
  341 + humidityCondition.setSpec(new SimpleAlarmConditionSpec());
  342 +
  343 + KeyFilter humidityAlarmFlagAttributeFilter = new KeyFilter();
  344 + humidityAlarmFlagAttributeFilter.setKey(new EntityKey(EntityKeyType.ATTRIBUTE, "humidityAlarmFlag"));
  345 + humidityAlarmFlagAttributeFilter.setValueType(EntityKeyValueType.BOOLEAN);
  346 + BooleanFilterPredicate humidityAlarmFlagAttributePredicate = new BooleanFilterPredicate();
  347 + humidityAlarmFlagAttributePredicate.setOperation(BooleanFilterPredicate.BooleanOperation.EQUAL);
  348 + humidityAlarmFlagAttributePredicate.setValue(new FilterPredicateValue<>(Boolean.TRUE));
  349 + humidityAlarmFlagAttributeFilter.setPredicate(humidityAlarmFlagAttributePredicate);
  350 +
  351 + KeyFilter humidityTimeseriesFilter = new KeyFilter();
  352 + humidityTimeseriesFilter.setKey(new EntityKey(EntityKeyType.TIME_SERIES, "humidity"));
  353 + humidityTimeseriesFilter.setValueType(EntityKeyValueType.NUMERIC);
  354 + NumericFilterPredicate humidityTimeseriesFilterPredicate = new NumericFilterPredicate();
  355 + humidityTimeseriesFilterPredicate.setOperation(NumericFilterPredicate.NumericOperation.LESS);
  356 + FilterPredicateValue<Double> humidityTimeseriesPredicateValue =
  357 + new FilterPredicateValue<>(60.0, null,
  358 + new DynamicValue<>(DynamicValueSourceType.CURRENT_DEVICE, "humidityAlarmThreshold"));
  359 + humidityTimeseriesFilterPredicate.setValue(humidityTimeseriesPredicateValue);
  360 + humidityTimeseriesFilter.setPredicate(humidityTimeseriesFilterPredicate);
  361 + humidityCondition.setCondition(Arrays.asList(humidityAlarmFlagAttributeFilter, humidityTimeseriesFilter));
  362 +
  363 + humidityRule.setCondition(humidityCondition);
  364 + humidityRule.setAlarmDetails("Current humidity = ${humidity}");
  365 + lowHumidity.setCreateRules(new TreeMap<>(Collections.singletonMap(AlarmSeverity.MINOR, humidityRule)));
  366 +
  367 + AlarmRule clearHumidityRule = new AlarmRule();
  368 + AlarmCondition clearHumidityCondition = new AlarmCondition();
  369 + clearHumidityCondition.setSpec(new SimpleAlarmConditionSpec());
  370 +
  371 + KeyFilter clearHumidityTimeseriesFilter = new KeyFilter();
  372 + clearHumidityTimeseriesFilter.setKey(new EntityKey(EntityKeyType.TIME_SERIES, "humidity"));
  373 + clearHumidityTimeseriesFilter.setValueType(EntityKeyValueType.NUMERIC);
  374 + NumericFilterPredicate clearHumidityTimeseriesFilterPredicate = new NumericFilterPredicate();
  375 + clearHumidityTimeseriesFilterPredicate.setOperation(NumericFilterPredicate.NumericOperation.GREATER_OR_EQUAL);
  376 + FilterPredicateValue<Double> clearHumidityTimeseriesPredicateValue =
  377 + new FilterPredicateValue<>(60.0, null,
  378 + new DynamicValue<>(DynamicValueSourceType.CURRENT_DEVICE, "humidityAlarmThreshold"));
  379 +
  380 + clearHumidityTimeseriesFilterPredicate.setValue(clearHumidityTimeseriesPredicateValue);
  381 + clearHumidityTimeseriesFilter.setPredicate(clearHumidityTimeseriesFilterPredicate);
  382 + clearHumidityCondition.setCondition(Collections.singletonList(clearHumidityTimeseriesFilter));
  383 + clearHumidityRule.setCondition(clearHumidityCondition);
  384 + clearHumidityRule.setAlarmDetails("Current humidity = ${humidity}");
  385 + lowHumidity.setClearRule(clearHumidityRule);
  386 +
  387 + deviceProfileData.setAlarms(Arrays.asList(highTemperature, lowHumidity));
  388 +
  389 + DeviceProfile savedThermostatDeviceProfile = deviceProfileService.saveDeviceProfile(thermostatDeviceProfile);
  390 +
  391 + DeviceId t1Id = createDevice(demoTenant.getId(), null, savedThermostatDeviceProfile.getId(), "Thermostat T1", "T1_TEST_TOKEN", "Demo device for Thermostats dashboard").getId();
  392 + DeviceId t2Id = createDevice(demoTenant.getId(), null, savedThermostatDeviceProfile.getId(), "Thermostat T2", "T2_TEST_TOKEN", "Demo device for Thermostats dashboard").getId();
264 393
265 394 attributesService.save(demoTenant.getId(), t1Id, DataConstants.SERVER_SCOPE,
266 395 Arrays.asList(new BaseAttributeKvEntry(System.currentTimeMillis(), new DoubleDataEntry("latitude", 37.3948)),
267 396 new BaseAttributeKvEntry(System.currentTimeMillis(), new DoubleDataEntry("longitude", -122.1503)),
268   - new BaseAttributeKvEntry(System.currentTimeMillis(), new BooleanDataEntry("alarmTemperature", true)),
269   - new BaseAttributeKvEntry(System.currentTimeMillis(), new BooleanDataEntry("alarmHumidity", true)),
270   - new BaseAttributeKvEntry(System.currentTimeMillis(), new LongDataEntry("thresholdTemperature", (long) 20)),
271   - new BaseAttributeKvEntry(System.currentTimeMillis(), new LongDataEntry("thresholdHumidity", (long) 50))));
  397 + new BaseAttributeKvEntry(System.currentTimeMillis(), new BooleanDataEntry("temperatureAlarmFlag", true)),
  398 + new BaseAttributeKvEntry(System.currentTimeMillis(), new BooleanDataEntry("humidityAlarmFlag", true)),
  399 + new BaseAttributeKvEntry(System.currentTimeMillis(), new LongDataEntry("temperatureAlarmThreshold", (long) 20)),
  400 + new BaseAttributeKvEntry(System.currentTimeMillis(), new LongDataEntry("humidityAlarmThreshold", (long) 50))));
272 401
273 402 attributesService.save(demoTenant.getId(), t2Id, DataConstants.SERVER_SCOPE,
274 403 Arrays.asList(new BaseAttributeKvEntry(System.currentTimeMillis(), new DoubleDataEntry("latitude", 37.493801)),
275 404 new BaseAttributeKvEntry(System.currentTimeMillis(), new DoubleDataEntry("longitude", -121.948769)),
276   - new BaseAttributeKvEntry(System.currentTimeMillis(), new BooleanDataEntry("alarmTemperature", true)),
277   - new BaseAttributeKvEntry(System.currentTimeMillis(), new BooleanDataEntry("alarmHumidity", true)),
278   - new BaseAttributeKvEntry(System.currentTimeMillis(), new LongDataEntry("thresholdTemperature", (long) 25)),
279   - new BaseAttributeKvEntry(System.currentTimeMillis(), new LongDataEntry("thresholdHumidity", (long) 30))));
  405 + new BaseAttributeKvEntry(System.currentTimeMillis(), new BooleanDataEntry("temperatureAlarmFlag", true)),
  406 + new BaseAttributeKvEntry(System.currentTimeMillis(), new BooleanDataEntry("humidityAlarmFlag", true)),
  407 + new BaseAttributeKvEntry(System.currentTimeMillis(), new LongDataEntry("temperatureAlarmThreshold", (long) 25)),
  408 + new BaseAttributeKvEntry(System.currentTimeMillis(), new LongDataEntry("humidityAlarmThreshold", (long) 30))));
280 409
281 410 installScripts.loadDashboards(demoTenant.getId(), null);
282 411 }
... ...
... ... @@ -210,26 +210,9 @@ public class InstallScripts {
210 210
211 211
212 212 public void loadDemoRuleChains(TenantId tenantId) throws Exception {
213   - Path ruleChainsDir = Paths.get(getDataDir(), JSON_DIR, DEMO_DIR, RULE_CHAINS_DIR);
214 213 try {
215   - JsonNode ruleChainJson = objectMapper.readTree(ruleChainsDir.resolve("thermostat_alarms.json").toFile());
216   - RuleChain ruleChain = objectMapper.treeToValue(ruleChainJson.get("ruleChain"), RuleChain.class);
217   - RuleChainMetaData ruleChainMetaData = objectMapper.treeToValue(ruleChainJson.get("metadata"), RuleChainMetaData.class);
218   - ruleChain.setTenantId(tenantId);
219   - ruleChain = ruleChainService.saveRuleChain(ruleChain);
220   - ruleChainMetaData.setRuleChainId(ruleChain.getId());
221   - ruleChainService.saveRuleChainMetaData(new TenantId(EntityId.NULL_UUID), ruleChainMetaData);
222   -
223   - JsonNode rootChainJson = objectMapper.readTree(ruleChainsDir.resolve("root_rule_chain.json").toFile());
224   - RuleChain rootChain = objectMapper.treeToValue(rootChainJson.get("ruleChain"), RuleChain.class);
225   - RuleChainMetaData rootChainMetaData = objectMapper.treeToValue(rootChainJson.get("metadata"), RuleChainMetaData.class);
226   -
227   - RuleChainId thermostatsRuleChainId = ruleChain.getId();
228   - rootChainMetaData.getRuleChainConnections().forEach(connection -> connection.setTargetRuleChainId(thermostatsRuleChainId));
229   - rootChain.setTenantId(tenantId);
230   - rootChain = ruleChainService.saveRuleChain(rootChain);
231   - rootChainMetaData.setRuleChainId(rootChain.getId());
232   - ruleChainService.saveRuleChainMetaData(new TenantId(EntityId.NULL_UUID), rootChainMetaData);
  214 + createDefaultRuleChains(tenantId);
  215 + createDefaultRuleChain(tenantId, "Thermostat");
233 216 } catch (Exception e) {
234 217 log.error("Unable to load dashboard from json", e);
235 218 throw new RuntimeException("Unable to load dashboard from json", e);
... ...
... ... @@ -367,6 +367,8 @@ public class SqlDatabaseUpgradeService implements DatabaseEntitiesUpgradeService
367 367 " db_storage varchar(32)," +
368 368 " re_exec varchar(32)," +
369 369 " js_exec varchar(32)," +
  370 + " email_exec varchar(32)," +
  371 + " sms_exec varchar(32)," +
370 372 " CONSTRAINT api_usage_state_unq_key UNIQUE (tenant_id, entity_id)\n" +
371 373 ");");
372 374 } catch (Exception e) {
... ... @@ -419,6 +421,18 @@ public class SqlDatabaseUpgradeService implements DatabaseEntitiesUpgradeService
419 421 log.error("Failed updating schema!!!", e);
420 422 }
421 423 break;
  424 + case "3.2.0":
  425 + try (Connection conn = DriverManager.getConnection(dbUrl, dbUserName, dbPassword)) {
  426 + log.info("Updating schema ...");
  427 + try {
  428 + conn.createStatement().execute("CREATE INDEX IF NOT EXISTS idx_device_device_profile_id ON device(tenant_id, device_profile_id);");
  429 + conn.createStatement().execute("UPDATE tb_schema_settings SET schema_version = 3002001;");
  430 + } catch (Exception e) {
  431 + log.error("Failed updating schema!!!", e);
  432 + }
  433 + log.info("Schema updated.");
  434 + }
  435 + break;
422 436 default:
423 437 throw new RuntimeException("Unable to upgrade SQL database, unsupported fromVersion: " + fromVersion);
424 438 }
... ...
... ... @@ -20,8 +20,10 @@ import freemarker.template.Configuration;
20 20 import freemarker.template.Template;
21 21 import lombok.extern.slf4j.Slf4j;
22 22 import org.apache.commons.lang3.StringUtils;
  23 +import org.jetbrains.annotations.NotNull;
23 24 import org.springframework.beans.factory.annotation.Autowired;
24 25 import org.springframework.context.MessageSource;
  26 +import org.springframework.context.annotation.Lazy;
25 27 import org.springframework.core.NestedRuntimeException;
26 28 import org.springframework.mail.javamail.JavaMailSenderImpl;
27 29 import org.springframework.mail.javamail.MimeMessageHelper;
... ... @@ -29,12 +31,18 @@ import org.springframework.stereotype.Service;
29 31 import org.springframework.ui.freemarker.FreeMarkerTemplateUtils;
30 32 import org.thingsboard.rule.engine.api.MailService;
31 33 import org.thingsboard.server.common.data.AdminSettings;
  34 +import org.thingsboard.server.common.data.ApiFeature;
  35 +import org.thingsboard.server.common.data.ApiUsageRecordKey;
  36 +import org.thingsboard.server.common.data.ApiUsageStateMailMessage;
  37 +import org.thingsboard.server.common.data.ApiUsageStateValue;
32 38 import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
33 39 import org.thingsboard.server.common.data.exception.ThingsboardException;
34 40 import org.thingsboard.server.common.data.id.EntityId;
35 41 import org.thingsboard.server.common.data.id.TenantId;
36 42 import org.thingsboard.server.dao.exception.IncorrectParameterException;
37 43 import org.thingsboard.server.dao.settings.AdminSettingsService;
  44 +import org.thingsboard.server.queue.usagestats.TbApiUsageClient;
  45 +import org.thingsboard.server.service.apiusage.TbApiUsageStateService;
38 46
39 47 import javax.annotation.PostConstruct;
40 48 import javax.mail.MessagingException;
... ... @@ -51,18 +59,28 @@ public class DefaultMailService implements MailService {
51 59 public static final String MAIL_PROP = "mail.";
52 60 public static final String TARGET_EMAIL = "targetEmail";
53 61 public static final String UTF_8 = "UTF-8";
54   - @Autowired
55   - private MessageSource messages;
  62 + public static final int _10K = 10000;
  63 + public static final int _1M = 1000000;
  64 +
  65 + private final MessageSource messages;
  66 + private final Configuration freemarkerConfig;
  67 + private final AdminSettingsService adminSettingsService;
  68 + private final TbApiUsageClient apiUsageClient;
56 69
  70 + @Lazy
57 71 @Autowired
58   - private Configuration freemarkerConfig;
  72 + private TbApiUsageStateService apiUsageStateService;
59 73
60 74 private JavaMailSenderImpl mailSender;
61 75
62 76 private String mailFrom;
63 77
64   - @Autowired
65   - private AdminSettingsService adminSettingsService;
  78 + public DefaultMailService(MessageSource messages, Configuration freemarkerConfig, AdminSettingsService adminSettingsService, TbApiUsageClient apiUsageClient) {
  79 + this.messages = messages;
  80 + this.freemarkerConfig = freemarkerConfig;
  81 + this.adminSettingsService = adminSettingsService;
  82 + this.apiUsageClient = apiUsageClient;
  83 + }
66 84
67 85 @PostConstruct
68 86 private void init() {
... ... @@ -141,7 +159,7 @@ public class DefaultMailService implements MailService {
141 159 }
142 160
143 161 @Override
144   - public void sendEmail(String email, String subject, String message) throws ThingsboardException {
  162 + public void sendEmail(TenantId tenantId, String email, String subject, String message) throws ThingsboardException {
145 163 sendMail(mailSender, mailFrom, email, subject, message);
146 164 }
147 165
... ... @@ -216,20 +234,25 @@ public class DefaultMailService implements MailService {
216 234 }
217 235
218 236 @Override
219   - public void send(String from, String to, String cc, String bcc, String subject, String body) throws MessagingException {
220   - MimeMessage mailMsg = mailSender.createMimeMessage();
221   - MimeMessageHelper helper = new MimeMessageHelper(mailMsg, "UTF-8");
222   - helper.setFrom(StringUtils.isBlank(from) ? mailFrom : from);
223   - helper.setTo(to.split("\\s*,\\s*"));
224   - if (!StringUtils.isBlank(cc)) {
225   - helper.setCc(cc.split("\\s*,\\s*"));
226   - }
227   - if (!StringUtils.isBlank(bcc)) {
228   - helper.setBcc(bcc.split("\\s*,\\s*"));
  237 + public void send(TenantId tenantId, String from, String to, String cc, String bcc, String subject, String body) throws MessagingException {
  238 + if (apiUsageStateService.getApiUsageState(tenantId).isEmailSendEnabled()) {
  239 + MimeMessage mailMsg = mailSender.createMimeMessage();
  240 + MimeMessageHelper helper = new MimeMessageHelper(mailMsg, "UTF-8");
  241 + helper.setFrom(StringUtils.isBlank(from) ? mailFrom : from);
  242 + helper.setTo(to.split("\\s*,\\s*"));
  243 + if (!StringUtils.isBlank(cc)) {
  244 + helper.setCc(cc.split("\\s*,\\s*"));
  245 + }
  246 + if (!StringUtils.isBlank(bcc)) {
  247 + helper.setBcc(bcc.split("\\s*,\\s*"));
  248 + }
  249 + helper.setSubject(subject);
  250 + helper.setText(body);
  251 + mailSender.send(helper.getMimeMessage());
  252 + apiUsageClient.report(tenantId, ApiUsageRecordKey.EMAIL_EXEC_COUNT, 1);
  253 + } else {
  254 + throw new RuntimeException("Email sending is disabled due to API limits!");
229 255 }
230   - helper.setSubject(subject);
231   - helper.setText(body);
232   - mailSender.send(helper.getMimeMessage());
233 256 }
234 257
235 258 @Override
... ... @@ -246,6 +269,122 @@ public class DefaultMailService implements MailService {
246 269 sendMail(mailSender, mailFrom, email, subject, message);
247 270 }
248 271
  272 + @Override
  273 + public void sendApiFeatureStateEmail(ApiFeature apiFeature, ApiUsageStateValue stateValue, String email, ApiUsageStateMailMessage msg) throws ThingsboardException {
  274 + String subject = messages.getMessage("api.usage.state", null, Locale.US);
  275 +
  276 + Map<String, Object> model = new HashMap<>();
  277 + model.put("apiFeature", apiFeature.getLabel());
  278 + model.put(TARGET_EMAIL, email);
  279 +
  280 + String message = null;
  281 +
  282 + switch (stateValue) {
  283 + case ENABLED:
  284 + model.put("apiLabel", toEnabledValueLabel(apiFeature));
  285 + message = mergeTemplateIntoString("state.enabled.ftl", model);
  286 + break;
  287 + case WARNING:
  288 + model.put("apiValueLabel", toDisabledValueLabel(apiFeature) + " " + toWarningValueLabel(msg.getKey(), msg.getValue(), msg.getThreshold()));
  289 + message = mergeTemplateIntoString("state.warning.ftl", model);
  290 + break;
  291 + case DISABLED:
  292 + model.put("apiLimitValueLabel", toDisabledValueLabel(apiFeature) + " " + toDisabledValueLabel(msg.getKey(), msg.getThreshold()));
  293 + message = mergeTemplateIntoString("state.disabled.ftl", model);
  294 + break;
  295 + }
  296 + sendMail(mailSender, mailFrom, email, subject, message);
  297 + }
  298 +
  299 + private String toEnabledValueLabel(ApiFeature apiFeature) {
  300 + switch (apiFeature) {
  301 + case DB:
  302 + return "save";
  303 + case TRANSPORT:
  304 + return "receive";
  305 + case JS:
  306 + return "invoke";
  307 + case RE:
  308 + return "process";
  309 + case EMAIL:
  310 + case SMS:
  311 + return "send";
  312 + default:
  313 + throw new RuntimeException("Not implemented!");
  314 + }
  315 + }
  316 +
  317 + private String toDisabledValueLabel(ApiFeature apiFeature) {
  318 + switch (apiFeature) {
  319 + case DB:
  320 + return "saved";
  321 + case TRANSPORT:
  322 + return "received";
  323 + case JS:
  324 + return "invoked";
  325 + case RE:
  326 + return "processed";
  327 + case EMAIL:
  328 + case SMS:
  329 + return "sent";
  330 + default:
  331 + throw new RuntimeException("Not implemented!");
  332 + }
  333 + }
  334 +
  335 + private String toWarningValueLabel(ApiUsageRecordKey key, long value, long threshold) {
  336 + String valueInM = getValueAsString(value);
  337 + String thresholdInM = getValueAsString(threshold);
  338 + switch (key) {
  339 + case STORAGE_DP_COUNT:
  340 + case TRANSPORT_DP_COUNT:
  341 + return valueInM + " out of " + thresholdInM + " allowed data points";
  342 + case TRANSPORT_MSG_COUNT:
  343 + return valueInM + " out of " + thresholdInM + " allowed messages";
  344 + case JS_EXEC_COUNT:
  345 + return valueInM + " out of " + thresholdInM + " allowed JavaScript functions";
  346 + case RE_EXEC_COUNT:
  347 + return valueInM + " out of " + thresholdInM + " allowed Rule Engine messages";
  348 + case EMAIL_EXEC_COUNT:
  349 + return valueInM + " out of " + thresholdInM + " allowed Email messages";
  350 + case SMS_EXEC_COUNT:
  351 + return valueInM + " out of " + thresholdInM + " allowed SMS messages";
  352 + default:
  353 + throw new RuntimeException("Not implemented!");
  354 + }
  355 + }
  356 +
  357 + private String toDisabledValueLabel(ApiUsageRecordKey key, long value) {
  358 + switch (key) {
  359 + case STORAGE_DP_COUNT:
  360 + case TRANSPORT_DP_COUNT:
  361 + return getValueAsString(value) + " data points";
  362 + case TRANSPORT_MSG_COUNT:
  363 + return getValueAsString(value) + " messages";
  364 + case JS_EXEC_COUNT:
  365 + return "JavaScript functions " + getValueAsString(value) + " times";
  366 + case RE_EXEC_COUNT:
  367 + return getValueAsString(value) + " Rule Engine messages";
  368 + case EMAIL_EXEC_COUNT:
  369 + return getValueAsString(value) + " Email messages";
  370 + case SMS_EXEC_COUNT:
  371 + return getValueAsString(value) + " SMS messages";
  372 + default:
  373 + throw new RuntimeException("Not implemented!");
  374 + }
  375 + }
  376 +
  377 + @NotNull
  378 + private String getValueAsString(long value) {
  379 + if (value > _1M && value % _1M < _10K) {
  380 + return value / _1M + "M";
  381 + } else if (value > _10K) {
  382 + return String.format("%.2fM", ((double) value) / 1000000);
  383 + } else {
  384 + return value + "";
  385 + }
  386 + }
  387 +
249 388 private void sendMail(JavaMailSenderImpl mailSender,
250 389 String mailFrom, String email,
251 390 String subject, String message) throws ThingsboardException {
... ...
... ... @@ -22,6 +22,7 @@ import org.springframework.scheduling.annotation.Scheduled;
22 22 import org.springframework.stereotype.Service;
23 23 import org.thingsboard.rule.engine.api.msg.ToDeviceActorNotificationMsg;
24 24 import org.thingsboard.server.common.data.ApiUsageState;
  25 +import org.thingsboard.server.common.data.Device;
25 26 import org.thingsboard.server.common.data.DeviceProfile;
26 27 import org.thingsboard.server.common.data.EntityType;
27 28 import org.thingsboard.server.common.data.HasName;
... ... @@ -32,7 +33,6 @@ import org.thingsboard.server.common.data.id.DeviceProfileId;
32 33 import org.thingsboard.server.common.data.id.EntityId;
33 34 import org.thingsboard.server.common.data.id.RuleChainId;
34 35 import org.thingsboard.server.common.data.id.TenantId;
35   -import org.thingsboard.server.common.data.id.TenantProfileId;
36 36 import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
37 37 import org.thingsboard.server.common.msg.TbMsg;
38 38 import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg;
... ... @@ -237,6 +237,16 @@ public class DefaultTbClusterService implements TbClusterService {
237 237 onEntityDelete(TenantId.SYS_TENANT_ID, entity.getId(), entity.getName(), callback);
238 238 }
239 239
  240 + @Override
  241 + public void onDeviceChange(Device entity, TbQueueCallback callback) {
  242 + onEntityChange(entity.getTenantId(), entity.getId(), entity, callback);
  243 + }
  244 +
  245 + @Override
  246 + public void onDeviceDeleted(Device entity, TbQueueCallback callback) {
  247 + onEntityDelete(entity.getTenantId(), entity.getId(), entity.getName(), callback);
  248 + }
  249 +
240 250 public <T> void onEntityChange(TenantId tenantId, EntityId entityid, T entity, TbQueueCallback callback) {
241 251 String entityName = (entity instanceof HasName) ? ((HasName) entity).getName() : entity.getClass().getName();
242 252 log.trace("[{}][{}][{}] Processing [{}] change event", tenantId, entityid.getEntityType(), entityid.getId(), entityName);
... ... @@ -274,6 +284,7 @@ public class DefaultTbClusterService implements TbClusterService {
274 284 TbQueueProducer<TbProtoQueueMsg<ToRuleEngineNotificationMsg>> toRuleEngineProducer = producerProvider.getRuleEngineNotificationsMsgProducer();
275 285 Set<String> tbRuleEngineServices = new HashSet<>(partitionService.getAllServiceIds(ServiceType.TB_RULE_ENGINE));
276 286 if (msg.getEntityId().getEntityType().equals(EntityType.TENANT)
  287 + || msg.getEntityId().getEntityType().equals(EntityType.TENANT_PROFILE)
277 288 || msg.getEntityId().getEntityType().equals(EntityType.DEVICE_PROFILE)
278 289 || msg.getEntityId().getEntityType().equals(EntityType.API_USAGE_STATE)) {
279 290 TbQueueProducer<TbProtoQueueMsg<ToCoreNotificationMsg>> toCoreNfProducer = producerProvider.getTbCoreNotificationsMsgProducer();
... ...
... ... @@ -15,10 +15,13 @@
15 15 */
16 16 package org.thingsboard.server.service.queue;
17 17
  18 +import lombok.Getter;
  19 +import lombok.Setter;
18 20 import lombok.extern.slf4j.Slf4j;
19 21 import org.springframework.beans.factory.annotation.Value;
20 22 import org.springframework.boot.context.event.ApplicationReadyEvent;
21 23 import org.springframework.context.event.EventListener;
  24 +import org.springframework.core.annotation.Order;
22 25 import org.springframework.scheduling.annotation.Scheduled;
23 26 import org.springframework.stereotype.Service;
24 27 import org.thingsboard.common.util.ThingsBoardThreadFactory;
... ... @@ -54,7 +57,7 @@ import org.thingsboard.server.queue.provider.TbCoreQueueFactory;
54 57 import org.thingsboard.server.queue.util.TbCoreComponent;
55 58 import org.thingsboard.server.service.apiusage.TbApiUsageStateService;
56 59 import org.thingsboard.server.service.profile.TbDeviceProfileCache;
57   -import org.thingsboard.server.service.profile.TbTenantProfileCache;
  60 +import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
58 61 import org.thingsboard.server.service.queue.processing.AbstractConsumerService;
59 62 import org.thingsboard.server.service.rpc.FromDeviceRpcResponse;
60 63 import org.thingsboard.server.service.rpc.TbCoreDeviceRpcService;
... ... @@ -75,6 +78,7 @@ import java.util.concurrent.ConcurrentMap;
75 78 import java.util.concurrent.CountDownLatch;
76 79 import java.util.concurrent.ExecutorService;
77 80 import java.util.concurrent.Executors;
  81 +import java.util.concurrent.Future;
78 82 import java.util.concurrent.TimeUnit;
79 83 import java.util.function.Function;
80 84 import java.util.stream.Collectors;
... ... @@ -140,6 +144,7 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService<ToCore
140 144 }
141 145
142 146 @EventListener(ApplicationReadyEvent.class)
  147 + @Order(value = 2)
143 148 public void onApplicationEvent(ApplicationReadyEvent event) {
144 149 super.onApplicationEvent(event);
145 150 launchUsageStatsConsumer();
... ... @@ -173,39 +178,48 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService<ToCore
173 178 CountDownLatch processingTimeoutLatch = new CountDownLatch(1);
174 179 TbPackProcessingContext<TbProtoQueueMsg<ToCoreMsg>> ctx = new TbPackProcessingContext<>(
175 180 processingTimeoutLatch, pendingMap, new ConcurrentHashMap<>());
176   - pendingMap.forEach((id, msg) -> {
177   - log.trace("[{}] Creating main callback for message: {}", id, msg.getValue());
178   - TbCallback callback = new TbPackCallback<>(id, ctx);
179   - try {
180   - ToCoreMsg toCoreMsg = msg.getValue();
181   - if (toCoreMsg.hasToSubscriptionMgrMsg()) {
182   - log.trace("[{}] Forwarding message to subscription manager service {}", id, toCoreMsg.getToSubscriptionMgrMsg());
183   - forwardToSubMgrService(toCoreMsg.getToSubscriptionMgrMsg(), callback);
184   - } else if (toCoreMsg.hasToDeviceActorMsg()) {
185   - log.trace("[{}] Forwarding message to device actor {}", id, toCoreMsg.getToDeviceActorMsg());
186   - forwardToDeviceActor(toCoreMsg.getToDeviceActorMsg(), callback);
187   - } else if (toCoreMsg.hasDeviceStateServiceMsg()) {
188   - log.trace("[{}] Forwarding message to state service {}", id, toCoreMsg.getDeviceStateServiceMsg());
189   - forwardToStateService(toCoreMsg.getDeviceStateServiceMsg(), callback);
190   - } else if (toCoreMsg.getToDeviceActorNotificationMsg() != null && !toCoreMsg.getToDeviceActorNotificationMsg().isEmpty()) {
191   - Optional<TbActorMsg> actorMsg = encodingService.decode(toCoreMsg.getToDeviceActorNotificationMsg().toByteArray());
192   - if (actorMsg.isPresent()) {
193   - TbActorMsg tbActorMsg = actorMsg.get();
194   - if (tbActorMsg.getMsgType().equals(MsgType.DEVICE_RPC_REQUEST_TO_DEVICE_ACTOR_MSG)) {
195   - tbCoreDeviceRpcService.forwardRpcRequestToDeviceActor((ToDeviceRpcRequestActorMsg) tbActorMsg);
196   - } else {
197   - log.trace("[{}] Forwarding message to App Actor {}", id, actorMsg.get());
198   - actorContext.tell(actorMsg.get());
  181 + PendingMsgHolder pendingMsgHolder = new PendingMsgHolder();
  182 + Future<?> packSubmitFuture = consumersExecutor.submit(() -> {
  183 + pendingMap.forEach((id, msg) -> {
  184 + log.trace("[{}] Creating main callback for message: {}", id, msg.getValue());
  185 + TbCallback callback = new TbPackCallback<>(id, ctx);
  186 + try {
  187 + ToCoreMsg toCoreMsg = msg.getValue();
  188 + pendingMsgHolder.setToCoreMsg(toCoreMsg);
  189 + if (toCoreMsg.hasToSubscriptionMgrMsg()) {
  190 + log.trace("[{}] Forwarding message to subscription manager service {}", id, toCoreMsg.getToSubscriptionMgrMsg());
  191 + forwardToSubMgrService(toCoreMsg.getToSubscriptionMgrMsg(), callback);
  192 + } else if (toCoreMsg.hasToDeviceActorMsg()) {
  193 + log.trace("[{}] Forwarding message to device actor {}", id, toCoreMsg.getToDeviceActorMsg());
  194 + forwardToDeviceActor(toCoreMsg.getToDeviceActorMsg(), callback);
  195 + } else if (toCoreMsg.hasDeviceStateServiceMsg()) {
  196 + log.trace("[{}] Forwarding message to state service {}", id, toCoreMsg.getDeviceStateServiceMsg());
  197 + forwardToStateService(toCoreMsg.getDeviceStateServiceMsg(), callback);
  198 + } else if (toCoreMsg.getToDeviceActorNotificationMsg() != null && !toCoreMsg.getToDeviceActorNotificationMsg().isEmpty()) {
  199 + Optional<TbActorMsg> actorMsg = encodingService.decode(toCoreMsg.getToDeviceActorNotificationMsg().toByteArray());
  200 + if (actorMsg.isPresent()) {
  201 + TbActorMsg tbActorMsg = actorMsg.get();
  202 + if (tbActorMsg.getMsgType().equals(MsgType.DEVICE_RPC_REQUEST_TO_DEVICE_ACTOR_MSG)) {
  203 + tbCoreDeviceRpcService.forwardRpcRequestToDeviceActor((ToDeviceRpcRequestActorMsg) tbActorMsg);
  204 + } else {
  205 + log.trace("[{}] Forwarding message to App Actor {}", id, actorMsg.get());
  206 + actorContext.tell(actorMsg.get());
  207 + }
199 208 }
  209 + callback.onSuccess();
200 210 }
201   - callback.onSuccess();
  211 + } catch (Throwable e) {
  212 + log.warn("[{}] Failed to process message: {}", id, msg, e);
  213 + callback.onFailure(e);
202 214 }
203   - } catch (Throwable e) {
204   - log.warn("[{}] Failed to process message: {}", id, msg, e);
205   - callback.onFailure(e);
206   - }
  215 + });
207 216 });
208 217 if (!processingTimeoutLatch.await(packProcessingTimeout, TimeUnit.MILLISECONDS)) {
  218 + if (!packSubmitFuture.isDone()) {
  219 + packSubmitFuture.cancel(true);
  220 + ToCoreMsg lastSubmitMsg = pendingMsgHolder.getToCoreMsg();
  221 + log.info("Timeout to process message: {}", lastSubmitMsg);
  222 + }
209 223 ctx.getAckMap().forEach((id, msg) -> log.debug("[{}] Timeout to process message: {}", id, msg.getValue()));
210 224 ctx.getFailedMap().forEach((id, msg) -> log.warn("[{}] Failed to process message: {}", id, msg.getValue()));
211 225 }
... ... @@ -225,6 +239,12 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService<ToCore
225 239 });
226 240 }
227 241
  242 + private static class PendingMsgHolder {
  243 + @Getter
  244 + @Setter
  245 + private volatile ToCoreMsg toCoreMsg;
  246 + }
  247 +
228 248 @Override
229 249 protected ServiceType getServiceType() {
230 250 return ServiceType.TB_CORE;
... ... @@ -277,7 +297,7 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService<ToCore
277 297 try {
278 298 handleUsageStats(msg, callback);
279 299 } catch (Throwable e) {
280   - log.warn("[{}] Failed to process usge stats: {}", id, msg, e);
  300 + log.warn("[{}] Failed to process usage stats: {}", id, msg, e);
281 301 callback.onFailure(e);
282 302 }
283 303 });
... ...
... ... @@ -45,7 +45,7 @@ import org.thingsboard.server.queue.settings.TbRuleEngineQueueConfiguration;
45 45 import org.thingsboard.server.queue.util.TbRuleEngineComponent;
46 46 import org.thingsboard.server.service.apiusage.TbApiUsageStateService;
47 47 import org.thingsboard.server.service.profile.TbDeviceProfileCache;
48   -import org.thingsboard.server.service.profile.TbTenantProfileCache;
  48 +import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
49 49 import org.thingsboard.server.service.queue.processing.AbstractConsumerService;
50 50 import org.thingsboard.server.service.queue.processing.TbRuleEngineProcessingDecision;
51 51 import org.thingsboard.server.service.queue.processing.TbRuleEngineProcessingResult;
... ...
... ... @@ -21,11 +21,10 @@ import org.springframework.stereotype.Service;
21 21 import org.thingsboard.server.common.data.Tenant;
22 22 import org.thingsboard.server.common.data.TenantProfile;
23 23 import org.thingsboard.server.common.data.id.TenantId;
24   -import org.thingsboard.server.dao.tenant.TenantProfileService;
25 24 import org.thingsboard.server.dao.tenant.TenantService;
26 25 import org.thingsboard.server.queue.discovery.TenantRoutingInfo;
27 26 import org.thingsboard.server.queue.discovery.TenantRoutingInfoService;
28   -import org.thingsboard.server.service.profile.TbTenantProfileCache;
  27 +import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
29 28
30 29 @Slf4j
31 30 @Service
... ...
... ... @@ -17,6 +17,7 @@ package org.thingsboard.server.service.queue;
17 17
18 18 import org.thingsboard.rule.engine.api.msg.ToDeviceActorNotificationMsg;
19 19 import org.thingsboard.server.common.data.ApiUsageState;
  20 +import org.thingsboard.server.common.data.Device;
20 21 import org.thingsboard.server.common.data.DeviceProfile;
21 22 import org.thingsboard.server.common.data.Tenant;
22 23 import org.thingsboard.server.common.data.TenantProfile;
... ... @@ -66,4 +67,8 @@ public interface TbClusterService {
66 67 void onTenantDelete(Tenant tenant, TbQueueCallback callback);
67 68
68 69 void onApiStateChange(ApiUsageState apiUsageState, TbQueueCallback callback);
  70 +
  71 + void onDeviceChange(Device device, TbQueueCallback callback);
  72 +
  73 + void onDeviceDeleted(Device device, TbQueueCallback callback);
69 74 }
... ...
... ... @@ -20,6 +20,7 @@ import lombok.extern.slf4j.Slf4j;
20 20 import org.springframework.boot.context.event.ApplicationReadyEvent;
21 21 import org.springframework.context.ApplicationListener;
22 22 import org.springframework.context.event.EventListener;
  23 +import org.springframework.core.annotation.Order;
23 24 import org.thingsboard.common.util.ThingsBoardThreadFactory;
24 25 import org.thingsboard.server.actors.ActorSystemContext;
25 26 import org.thingsboard.server.common.data.EntityType;
... ... @@ -37,7 +38,7 @@ import org.thingsboard.server.queue.discovery.PartitionChangeEvent;
37 38 import org.thingsboard.server.common.transport.util.DataDecodingEncodingService;
38 39 import org.thingsboard.server.service.apiusage.TbApiUsageStateService;
39 40 import org.thingsboard.server.service.profile.TbDeviceProfileCache;
40   -import org.thingsboard.server.service.profile.TbTenantProfileCache;
  41 +import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
41 42 import org.thingsboard.server.service.queue.TbPackCallback;
42 43 import org.thingsboard.server.service.queue.TbPackProcessingContext;
43 44
... ... @@ -85,6 +86,7 @@ public abstract class AbstractConsumerService<N extends com.google.protobuf.Gene
85 86 }
86 87
87 88 @EventListener(ApplicationReadyEvent.class)
  89 + @Order(value = 2)
88 90 public void onApplicationEvent(ApplicationReadyEvent event) {
89 91 log.info("Subscribing to notifications: {}", nfConsumer.getTopic());
90 92 this.nfConsumer.subscribe();
... ... @@ -151,6 +153,8 @@ public abstract class AbstractConsumerService<N extends com.google.protobuf.Gene
151 153 TbActorMsg actorMsg = actorMsgOpt.get();
152 154 if (actorMsg instanceof ComponentLifecycleMsg) {
153 155 ComponentLifecycleMsg componentLifecycleMsg = (ComponentLifecycleMsg) actorMsg;
  156 + log.info("[{}][{}][{}] Received Lifecycle event: {}", componentLifecycleMsg.getTenantId(), componentLifecycleMsg.getEntityId().getEntityType(),
  157 + componentLifecycleMsg.getEntityId(), componentLifecycleMsg.getEvent());
154 158 if (EntityType.TENANT_PROFILE.equals(componentLifecycleMsg.getEntityId().getEntityType())) {
155 159 TenantProfileId tenantProfileId = new TenantProfileId(componentLifecycleMsg.getEntityId().getId());
156 160 tenantProfileCache.evict(tenantProfileId);
... ...
... ... @@ -55,6 +55,7 @@ import org.thingsboard.server.dao.customer.CustomerService;
55 55 import org.thingsboard.server.dao.device.DeviceProfileService;
56 56 import org.thingsboard.server.dao.device.DeviceService;
57 57 import org.thingsboard.server.dao.entityview.EntityViewService;
  58 +import org.thingsboard.server.dao.exception.IncorrectParameterException;
58 59 import org.thingsboard.server.dao.rule.RuleChainService;
59 60 import org.thingsboard.server.dao.tenant.TenantService;
60 61 import org.thingsboard.server.dao.usagerecord.ApiUsageStateService;
... ... @@ -158,7 +159,11 @@ public class AccessValidator {
158 159 new FutureCallback<DeferredResult<ResponseEntity>>() {
159 160 @Override
160 161 public void onSuccess(@Nullable DeferredResult<ResponseEntity> result) {
161   - onSuccess.accept(response, currentUser.getTenantId(), entityId);
  162 + try {
  163 + onSuccess.accept(response, currentUser.getTenantId(), entityId);
  164 + } catch (Exception e) {
  165 + onFailure(e);
  166 + }
162 167 }
163 168
164 169 @Override
... ... @@ -434,9 +439,9 @@ public class AccessValidator {
434 439
435 440 public static void handleError(Throwable e, final DeferredResult<ResponseEntity> response, HttpStatus defaultErrorStatus) {
436 441 ResponseEntity responseEntity;
437   - if (e != null && e instanceof ToErrorResponseEntity) {
  442 + if (e instanceof ToErrorResponseEntity) {
438 443 responseEntity = ((ToErrorResponseEntity) e).toErrorResponseEntity();
439   - } else if (e != null && e instanceof IllegalArgumentException) {
  444 + } else if (e instanceof IllegalArgumentException || e instanceof IncorrectParameterException) {
440 445 responseEntity = new ResponseEntity<>(e.getMessage(), HttpStatus.BAD_REQUEST);
441 446 } else {
442 447 responseEntity = new ResponseEntity<>(defaultErrorStatus);
... ...
... ... @@ -35,14 +35,17 @@ import org.thingsboard.server.common.data.oauth2.OAuth2ClientRegistrationInfo;
35 35 import org.thingsboard.server.common.data.oauth2.OAuth2MapperConfig;
36 36 import org.thingsboard.server.common.data.page.PageData;
37 37 import org.thingsboard.server.common.data.page.PageLink;
  38 +import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
38 39 import org.thingsboard.server.common.data.security.Authority;
39 40 import org.thingsboard.server.common.data.security.UserCredentials;
40 41 import org.thingsboard.server.dao.customer.CustomerService;
41 42 import org.thingsboard.server.dao.dashboard.DashboardService;
42 43 import org.thingsboard.server.dao.oauth2.OAuth2User;
  44 +import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
43 45 import org.thingsboard.server.dao.tenant.TenantService;
44 46 import org.thingsboard.server.dao.user.UserService;
45 47 import org.thingsboard.server.service.install.InstallScripts;
  48 +import org.thingsboard.server.service.queue.TbClusterService;
46 49 import org.thingsboard.server.service.security.model.SecurityUser;
47 50 import org.thingsboard.server.service.security.model.UserPrincipal;
48 51
... ... @@ -76,6 +79,12 @@ public abstract class AbstractOAuth2ClientMapper {
76 79 @Autowired
77 80 private InstallScripts installScripts;
78 81
  82 + @Autowired
  83 + protected TbTenantProfileCache tenantProfileCache;
  84 +
  85 + @Autowired
  86 + protected TbClusterService tbClusterService;
  87 +
79 88 private final Lock userCreationLock = new ReentrantLock();
80 89
81 90 protected SecurityUser getOrCreateSecurityUserFromOAuth2User(OAuth2User oauth2User, OAuth2ClientRegistrationInfo clientRegistration) {
... ... @@ -162,6 +171,10 @@ public abstract class AbstractOAuth2ClientMapper {
162 171 tenant.setTitle(tenantName);
163 172 tenant = tenantService.saveTenant(tenant);
164 173 installScripts.createDefaultRuleChains(tenant.getId());
  174 + tenantProfileCache.evict(tenant.getId());
  175 + tbClusterService.onTenantChange(tenant, null);
  176 + tbClusterService.onEntityStateChange(tenant.getId(), tenant.getId(),
  177 + ComponentLifecycleEvent.CREATED);
165 178 } else {
166 179 tenant = tenants.get(0);
167 180 }
... ...
... ... @@ -15,10 +15,15 @@
15 15 */
16 16 package org.thingsboard.server.service.security.auth.oauth2;
17 17
  18 +import org.springframework.beans.factory.annotation.Autowired;
18 19 import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
19 20 import org.springframework.security.core.AuthenticationException;
20 21 import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
21 22 import org.springframework.stereotype.Component;
  23 +import org.thingsboard.server.common.data.id.CustomerId;
  24 +import org.thingsboard.server.common.data.id.EntityId;
  25 +import org.thingsboard.server.common.data.id.TenantId;
  26 +import org.thingsboard.server.service.security.system.SystemSecurityService;
22 27 import org.thingsboard.server.utils.MiscUtils;
23 28
24 29 import javax.servlet.ServletException;
... ... @@ -32,11 +37,18 @@ import java.nio.charset.StandardCharsets;
32 37 @ConditionalOnProperty(prefix = "security.oauth2", value = "enabled", havingValue = "true")
33 38 public class Oauth2AuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {
34 39
  40 + private final SystemSecurityService systemSecurityService;
  41 +
  42 + @Autowired
  43 + public Oauth2AuthenticationFailureHandler(final SystemSecurityService systemSecurityService) {
  44 + this.systemSecurityService = systemSecurityService;
  45 + }
  46 +
35 47 @Override
36 48 public void onAuthenticationFailure(HttpServletRequest request,
37 49 HttpServletResponse response, AuthenticationException exception)
38 50 throws IOException, ServletException {
39   - String baseUrl = MiscUtils.constructBaseUrl(request);
  51 + String baseUrl = this.systemSecurityService.getBaseUrl(TenantId.SYS_TENANT_ID, new CustomerId(EntityId.NULL_UUID), request);
40 52 getRedirectStrategy().sendRedirect(request, response, baseUrl + "/login?loginError=" +
41 53 URLEncoder.encode(exception.getMessage(), StandardCharsets.UTF_8.toString()));
42 54 }
... ...
... ... @@ -22,12 +22,16 @@ import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService;
22 22 import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
23 23 import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler;
24 24 import org.springframework.stereotype.Component;
  25 +import org.thingsboard.server.common.data.id.CustomerId;
  26 +import org.thingsboard.server.common.data.id.EntityId;
  27 +import org.thingsboard.server.common.data.id.TenantId;
25 28 import org.thingsboard.server.common.data.oauth2.OAuth2ClientRegistrationInfo;
26 29 import org.thingsboard.server.dao.oauth2.OAuth2Service;
27 30 import org.thingsboard.server.service.security.auth.jwt.RefreshTokenRepository;
28 31 import org.thingsboard.server.service.security.model.SecurityUser;
29 32 import org.thingsboard.server.service.security.model.token.JwtToken;
30 33 import org.thingsboard.server.service.security.model.token.JwtTokenFactory;
  34 +import org.thingsboard.server.service.security.system.SystemSecurityService;
31 35 import org.thingsboard.server.utils.MiscUtils;
32 36
33 37 import javax.servlet.http.HttpServletRequest;
... ... @@ -45,25 +49,27 @@ public class Oauth2AuthenticationSuccessHandler extends SimpleUrlAuthenticationS
45 49 private final OAuth2ClientMapperProvider oauth2ClientMapperProvider;
46 50 private final OAuth2Service oAuth2Service;
47 51 private final OAuth2AuthorizedClientService oAuth2AuthorizedClientService;
  52 + private final SystemSecurityService systemSecurityService;
48 53
49 54 @Autowired
50 55 public Oauth2AuthenticationSuccessHandler(final JwtTokenFactory tokenFactory,
51 56 final RefreshTokenRepository refreshTokenRepository,
52 57 final OAuth2ClientMapperProvider oauth2ClientMapperProvider,
53 58 final OAuth2Service oAuth2Service,
54   - final OAuth2AuthorizedClientService oAuth2AuthorizedClientService) {
  59 + final OAuth2AuthorizedClientService oAuth2AuthorizedClientService, final SystemSecurityService systemSecurityService) {
55 60 this.tokenFactory = tokenFactory;
56 61 this.refreshTokenRepository = refreshTokenRepository;
57 62 this.oauth2ClientMapperProvider = oauth2ClientMapperProvider;
58 63 this.oAuth2Service = oAuth2Service;
59 64 this.oAuth2AuthorizedClientService = oAuth2AuthorizedClientService;
  65 + this.systemSecurityService = systemSecurityService;
60 66 }
61 67
62 68 @Override
63 69 public void onAuthenticationSuccess(HttpServletRequest request,
64 70 HttpServletResponse response,
65 71 Authentication authentication) throws IOException {
66   - String baseUrl = MiscUtils.constructBaseUrl(request);
  72 + String baseUrl = this.systemSecurityService.getBaseUrl(TenantId.SYS_TENANT_ID, new CustomerId(EntityId.NULL_UUID), request);
67 73 try {
68 74 OAuth2AuthenticationToken token = (OAuth2AuthenticationToken) authentication;
69 75
... ...
... ... @@ -202,16 +202,19 @@ public class DefaultSystemSecurityService implements SystemSecurityService {
202 202
203 203 @Override
204 204 public String getBaseUrl(TenantId tenantId, CustomerId customerId, HttpServletRequest httpServletRequest) {
205   - String baseUrl;
  205 + String baseUrl = null;
206 206 AdminSettings generalSettings = adminSettingsService.findAdminSettingsByKey(TenantId.SYS_TENANT_ID, "general");
207 207
208 208 JsonNode prohibitDifferentUrl = generalSettings.getJsonValue().get("prohibitDifferentUrl");
209 209
210 210 if (prohibitDifferentUrl != null && prohibitDifferentUrl.asBoolean()) {
211 211 baseUrl = generalSettings.getJsonValue().get("baseUrl").asText();
212   - } else {
  212 + }
  213 +
  214 + if (StringUtils.isEmpty(baseUrl)) {
213 215 baseUrl = MiscUtils.constructBaseUrl(httpServletRequest);
214 216 }
  217 +
215 218 return baseUrl;
216 219 }
217 220
... ...
  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.sms;
  17 +
  18 +import lombok.extern.slf4j.Slf4j;
  19 +import org.thingsboard.rule.engine.api.sms.SmsSender;
  20 +import org.thingsboard.rule.engine.api.sms.exception.SmsParseException;
  21 +
  22 +import java.util.regex.Pattern;
  23 +
  24 +@Slf4j
  25 +public abstract class AbstractSmsSender implements SmsSender {
  26 +
  27 + private static final Pattern E_164_PHONE_NUMBER_PATTERN = Pattern.compile("^\\+[1-9]\\d{1,14}$");
  28 +
  29 + private static final int MAX_SMS_MESSAGE_LENGTH = 1600;
  30 + private static final int MAX_SMS_SEGMENT_LENGTH = 70;
  31 +
  32 + protected String validatePhoneNumber(String phoneNumber) throws SmsParseException {
  33 + phoneNumber = phoneNumber.trim();
  34 + if (!E_164_PHONE_NUMBER_PATTERN.matcher(phoneNumber).matches()) {
  35 + throw new SmsParseException("Invalid phone number format. Phone number must be in E.164 format.");
  36 + }
  37 + return phoneNumber;
  38 + }
  39 +
  40 + protected String prepareMessage(String message) {
  41 + message = message.replaceAll("^\"|\"$", "").replaceAll("\\\\n", "\n");
  42 + if (message.length() > MAX_SMS_MESSAGE_LENGTH) {
  43 + log.warn("SMS message exceeds maximum symbols length and will be truncated");
  44 + message = message.substring(0, MAX_SMS_MESSAGE_LENGTH);
  45 + }
  46 + return message;
  47 + }
  48 +
  49 + protected int countMessageSegments(String message) {
  50 + return (int)Math.ceil((double) message.length() / (double) MAX_SMS_SEGMENT_LENGTH);
  51 + }
  52 +
  53 +}
... ...
  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.sms;
  17 +
  18 +import org.springframework.stereotype.Component;
  19 +import org.thingsboard.rule.engine.api.sms.SmsSender;
  20 +import org.thingsboard.rule.engine.api.sms.SmsSenderFactory;
  21 +import org.thingsboard.server.common.data.sms.config.AwsSnsSmsProviderConfiguration;
  22 +import org.thingsboard.server.common.data.sms.config.SmsProviderConfiguration;
  23 +import org.thingsboard.server.common.data.sms.config.TwilioSmsProviderConfiguration;
  24 +import org.thingsboard.server.service.sms.aws.AwsSmsSender;
  25 +import org.thingsboard.server.service.sms.twilio.TwilioSmsSender;
  26 +
  27 +@Component
  28 +public class DefaultSmsSenderFactory implements SmsSenderFactory {
  29 +
  30 + @Override
  31 + public SmsSender createSmsSender(SmsProviderConfiguration config) {
  32 + switch (config.getType()) {
  33 + case AWS_SNS:
  34 + return new AwsSmsSender((AwsSnsSmsProviderConfiguration)config);
  35 + case TWILIO:
  36 + return new TwilioSmsSender((TwilioSmsProviderConfiguration)config);
  37 + default:
  38 + throw new RuntimeException("Unknown SMS provider type " + config.getType());
  39 + }
  40 + }
  41 +
  42 +}
... ...
  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.sms;
  17 +
  18 +import com.fasterxml.jackson.databind.JsonNode;
  19 +import lombok.extern.slf4j.Slf4j;
  20 +import org.springframework.core.NestedRuntimeException;
  21 +import org.springframework.stereotype.Service;
  22 +import org.thingsboard.rule.engine.api.SmsService;
  23 +import org.thingsboard.rule.engine.api.sms.SmsSender;
  24 +import org.thingsboard.rule.engine.api.sms.SmsSenderFactory;
  25 +import org.thingsboard.server.common.data.sms.config.SmsProviderConfiguration;
  26 +import org.thingsboard.server.common.data.sms.config.TestSmsRequest;
  27 +import org.thingsboard.server.common.data.AdminSettings;
  28 +import org.thingsboard.server.common.data.ApiUsageRecordKey;
  29 +import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
  30 +import org.thingsboard.server.common.data.exception.ThingsboardException;
  31 +import org.thingsboard.server.common.data.id.EntityId;
  32 +import org.thingsboard.server.common.data.id.TenantId;
  33 +import org.thingsboard.server.dao.settings.AdminSettingsService;
  34 +import org.thingsboard.server.dao.util.mapping.JacksonUtil;
  35 +import org.thingsboard.server.queue.usagestats.TbApiUsageClient;
  36 +import org.thingsboard.server.service.apiusage.TbApiUsageStateService;
  37 +
  38 +import javax.annotation.PostConstruct;
  39 +import javax.annotation.PreDestroy;
  40 +
  41 +@Service
  42 +@Slf4j
  43 +public class DefaultSmsService implements SmsService {
  44 +
  45 + private final SmsSenderFactory smsSenderFactory;
  46 + private final AdminSettingsService adminSettingsService;
  47 + private final TbApiUsageStateService apiUsageStateService;
  48 + private final TbApiUsageClient apiUsageClient;
  49 +
  50 + private SmsSender smsSender;
  51 +
  52 + public DefaultSmsService(SmsSenderFactory smsSenderFactory, AdminSettingsService adminSettingsService, TbApiUsageStateService apiUsageStateService, TbApiUsageClient apiUsageClient) {
  53 + this.smsSenderFactory = smsSenderFactory;
  54 + this.adminSettingsService = adminSettingsService;
  55 + this.apiUsageStateService = apiUsageStateService;
  56 + this.apiUsageClient = apiUsageClient;
  57 + }
  58 +
  59 + @PostConstruct
  60 + private void init() {
  61 + updateSmsConfiguration();
  62 + }
  63 +
  64 + @PreDestroy
  65 + private void destroy() {
  66 + if (this.smsSender != null) {
  67 + this.smsSender.destroy();
  68 + }
  69 + }
  70 +
  71 + @Override
  72 + public void updateSmsConfiguration() {
  73 + AdminSettings settings = adminSettingsService.findAdminSettingsByKey(new TenantId(EntityId.NULL_UUID), "sms");
  74 + if (settings != null) {
  75 + try {
  76 + JsonNode jsonConfig = settings.getJsonValue();
  77 + SmsProviderConfiguration configuration = JacksonUtil.convertValue(jsonConfig, SmsProviderConfiguration.class);
  78 + SmsSender newSmsSender = this.smsSenderFactory.createSmsSender(configuration);
  79 + if (this.smsSender != null) {
  80 + this.smsSender.destroy();
  81 + }
  82 + this.smsSender = newSmsSender;
  83 + } catch (Exception e) {
  84 + log.error("Failed to create SMS sender", e);
  85 + }
  86 + }
  87 + }
  88 +
  89 + private int sendSms(String numberTo, String message) throws ThingsboardException {
  90 + if (this.smsSender == null) {
  91 + throw new ThingsboardException("Unable to send SMS: no SMS provider configured!", ThingsboardErrorCode.GENERAL);
  92 + }
  93 + return this.sendSms(this.smsSender, numberTo, message);
  94 + }
  95 +
  96 + @Override
  97 + public void sendSms(TenantId tenantId, String[] numbersTo, String message) throws ThingsboardException {
  98 + if (apiUsageStateService.getApiUsageState(tenantId).isSmsSendEnabled()) {
  99 + int smsCount = 0;
  100 + try {
  101 + for (String numberTo : numbersTo) {
  102 + smsCount += this.sendSms(numberTo, message);
  103 + }
  104 + } finally {
  105 + if (smsCount > 0) {
  106 + apiUsageClient.report(tenantId, ApiUsageRecordKey.SMS_EXEC_COUNT, smsCount);
  107 + }
  108 + }
  109 + } else {
  110 + throw new RuntimeException("SMS sending is disabled due to API limits!");
  111 + }
  112 + }
  113 +
  114 + @Override
  115 + public void sendTestSms(TestSmsRequest testSmsRequest) throws ThingsboardException {
  116 + SmsSender testSmsSender;
  117 + try {
  118 + testSmsSender = this.smsSenderFactory.createSmsSender(testSmsRequest.getProviderConfiguration());
  119 + } catch (Exception e) {
  120 + throw handleException(e);
  121 + }
  122 + this.sendSms(testSmsSender, testSmsRequest.getNumberTo(), testSmsRequest.getMessage());
  123 + testSmsSender.destroy();
  124 + }
  125 +
  126 + private int sendSms(SmsSender smsSender, String numberTo, String message) throws ThingsboardException {
  127 + try {
  128 + return smsSender.sendSms(numberTo, message);
  129 + } catch (Exception e) {
  130 + throw handleException(e);
  131 + }
  132 + }
  133 +
  134 + private ThingsboardException handleException(Exception exception) {
  135 + String message;
  136 + if (exception instanceof NestedRuntimeException) {
  137 + message = ((NestedRuntimeException) exception).getMostSpecificCause().getMessage();
  138 + } else {
  139 + message = exception.getMessage();
  140 + }
  141 + log.warn("Unable to send SMS: {}", message);
  142 + return new ThingsboardException(String.format("Unable to send SMS: %s", message),
  143 + ThingsboardErrorCode.GENERAL);
  144 + }
  145 +}
... ...
  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.sms;
  17 +
  18 +import org.springframework.beans.factory.annotation.Value;
  19 +import org.springframework.stereotype.Component;
  20 +import org.thingsboard.common.util.AbstractListeningExecutor;
  21 +
  22 +@Component
  23 +public class SmsExecutorService extends AbstractListeningExecutor {
  24 +
  25 + @Value("${actors.rule.sms_thread_pool_size}")
  26 + private int smsExecutorThreadPoolSize;
  27 +
  28 + @Override
  29 + protected int getThreadPollSize() {
  30 + return smsExecutorThreadPoolSize;
  31 + }
  32 +
  33 +}
... ...
  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.sms.aws;
  17 +
  18 +import com.amazonaws.auth.AWSCredentials;
  19 +import com.amazonaws.auth.AWSStaticCredentialsProvider;
  20 +import com.amazonaws.auth.BasicAWSCredentials;
  21 +import com.amazonaws.services.sns.AmazonSNS;
  22 +import com.amazonaws.services.sns.AmazonSNSClient;
  23 +import com.amazonaws.services.sns.model.PublishRequest;
  24 +import lombok.extern.slf4j.Slf4j;
  25 +import org.apache.commons.lang3.StringUtils;
  26 +import org.thingsboard.server.common.data.sms.config.AwsSnsSmsProviderConfiguration;
  27 +import org.thingsboard.rule.engine.api.sms.exception.SmsException;
  28 +import org.thingsboard.rule.engine.api.sms.exception.SmsSendException;
  29 +import org.thingsboard.server.service.sms.AbstractSmsSender;
  30 +
  31 +@Slf4j
  32 +public class AwsSmsSender extends AbstractSmsSender {
  33 +
  34 + private AmazonSNS snsClient;
  35 +
  36 + public AwsSmsSender(AwsSnsSmsProviderConfiguration config) {
  37 + if (StringUtils.isEmpty(config.getAccessKeyId()) || StringUtils.isEmpty(config.getSecretAccessKey()) || StringUtils.isEmpty(config.getRegion())) {
  38 + throw new IllegalArgumentException("Invalid AWS sms provider configuration: aws accessKeyId, aws secretAccessKey and aws region should be specified!");
  39 + }
  40 + AWSCredentials awsCredentials = new BasicAWSCredentials(config.getAccessKeyId(), config.getSecretAccessKey());
  41 + AWSStaticCredentialsProvider credProvider = new AWSStaticCredentialsProvider(awsCredentials);
  42 + this.snsClient = AmazonSNSClient.builder()
  43 + .withCredentials(credProvider)
  44 + .withRegion(config.getRegion())
  45 + .build();
  46 + }
  47 +
  48 + @Override
  49 + public int sendSms(String numberTo, String message) throws SmsException {
  50 + numberTo = this.validatePhoneNumber(numberTo);
  51 + message = this.prepareMessage(message);
  52 + try {
  53 + PublishRequest publishRequest = new PublishRequest()
  54 + .withPhoneNumber(numberTo)
  55 + .withMessage(message);
  56 + this.snsClient.publish(publishRequest);
  57 + return this.countMessageSegments(message);
  58 + } catch (Exception e) {
  59 + throw new SmsSendException("Failed to send SMS message - " + e.getMessage(), e);
  60 + }
  61 + }
  62 +
  63 + @Override
  64 + public void destroy() {
  65 + if (this.snsClient != null) {
  66 + try {
  67 + this.snsClient.shutdown();
  68 + } catch (Exception e) {
  69 + log.error("Failed to shutdown SNS client during destroy()", e);
  70 + }
  71 + }
  72 + }
  73 +}
... ...
  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.sms.twilio;
  17 +
  18 +import com.twilio.http.TwilioRestClient;
  19 +import com.twilio.rest.api.v2010.account.Message;
  20 +import com.twilio.type.PhoneNumber;
  21 +import org.apache.commons.lang3.StringUtils;
  22 +import org.thingsboard.server.common.data.sms.config.TwilioSmsProviderConfiguration;
  23 +import org.thingsboard.rule.engine.api.sms.exception.SmsException;
  24 +import org.thingsboard.rule.engine.api.sms.exception.SmsSendException;
  25 +import org.thingsboard.server.service.sms.AbstractSmsSender;
  26 +
  27 +public class TwilioSmsSender extends AbstractSmsSender {
  28 +
  29 + private TwilioRestClient twilioRestClient;
  30 + private String numberFrom;
  31 +
  32 + public TwilioSmsSender(TwilioSmsProviderConfiguration config) {
  33 + if (StringUtils.isEmpty(config.getAccountSid()) || StringUtils.isEmpty(config.getAccountToken()) || StringUtils.isEmpty(config.getNumberFrom())) {
  34 + throw new IllegalArgumentException("Invalid twilio sms provider configuration: accountSid, accountToken and numberFrom should be specified!");
  35 + }
  36 + this.numberFrom = this.validatePhoneNumber(config.getNumberFrom());
  37 + this.twilioRestClient = new TwilioRestClient.Builder(config.getAccountSid(), config.getAccountToken()).build();
  38 + }
  39 +
  40 + @Override
  41 + public int sendSms(String numberTo, String message) throws SmsException {
  42 + numberTo = this.validatePhoneNumber(numberTo);
  43 + message = this.prepareMessage(message);
  44 + try {
  45 + String numSegments = Message.creator(new PhoneNumber(numberTo), new PhoneNumber(this.numberFrom), message).create(this.twilioRestClient).getNumSegments();
  46 + return Integer.valueOf(numSegments);
  47 + } catch (Exception e) {
  48 + throw new SmsSendException("Failed to send SMS message - " + e.getMessage(), e);
  49 + }
  50 + }
  51 +
  52 + @Override
  53 + public void destroy() {
  54 +
  55 + }
  56 +}
... ...
... ... @@ -521,27 +521,27 @@ public class DefaultDeviceStateService implements DeviceStateService {
521 521
522 522 private void save(DeviceId deviceId, String key, long value) {
523 523 if (persistToTelemetry) {
524   - tsSubService.saveAndNotify(
  524 + tsSubService.saveAndNotifyInternal(
525 525 TenantId.SYS_TENANT_ID, deviceId,
526 526 Collections.singletonList(new BasicTsKvEntry(System.currentTimeMillis(), new LongDataEntry(key, value))),
527   - new AttributeSaveCallback(deviceId, key, value));
  527 + new AttributeSaveCallback<>(deviceId, key, value));
528 528 } else {
529   - tsSubService.saveAttrAndNotify(TenantId.SYS_TENANT_ID, deviceId, DataConstants.SERVER_SCOPE, key, value, new AttributeSaveCallback(deviceId, key, value));
  529 + tsSubService.saveAttrAndNotify(TenantId.SYS_TENANT_ID, deviceId, DataConstants.SERVER_SCOPE, key, value, new AttributeSaveCallback<>(deviceId, key, value));
530 530 }
531 531 }
532 532
533 533 private void save(DeviceId deviceId, String key, boolean value) {
534 534 if (persistToTelemetry) {
535   - tsSubService.saveAndNotify(
  535 + tsSubService.saveAndNotifyInternal(
536 536 TenantId.SYS_TENANT_ID, deviceId,
537 537 Collections.singletonList(new BasicTsKvEntry(System.currentTimeMillis(), new BooleanDataEntry(key, value))),
538   - new AttributeSaveCallback(deviceId, key, value));
  538 + new AttributeSaveCallback<>(deviceId, key, value));
539 539 } else {
540   - tsSubService.saveAttrAndNotify(TenantId.SYS_TENANT_ID, deviceId, DataConstants.SERVER_SCOPE, key, value, new AttributeSaveCallback(deviceId, key, value));
  540 + tsSubService.saveAttrAndNotify(TenantId.SYS_TENANT_ID, deviceId, DataConstants.SERVER_SCOPE, key, value, new AttributeSaveCallback<>(deviceId, key, value));
541 541 }
542 542 }
543 543
544   - private static class AttributeSaveCallback implements FutureCallback<Void> {
  544 + private static class AttributeSaveCallback<T> implements FutureCallback<T> {
545 545 private final DeviceId deviceId;
546 546 private final String key;
547 547 private final Object value;
... ... @@ -553,7 +553,7 @@ public class DefaultDeviceStateService implements DeviceStateService {
553 553 }
554 554
555 555 @Override
556   - public void onSuccess(@Nullable Void result) {
  556 + public void onSuccess(@Nullable T result) {
557 557 log.trace("[{}] Successfully updated attribute [{}] with value [{}]", deviceId, key, value);
558 558 }
559 559
... ...
... ... @@ -21,12 +21,6 @@ import org.springframework.context.annotation.Lazy;
21 21 import org.springframework.context.event.EventListener;
22 22 import org.springframework.stereotype.Service;
23 23 import org.thingsboard.common.util.ThingsBoardThreadFactory;
24   -import org.thingsboard.server.common.data.EntityType;
25   -import org.thingsboard.server.common.data.EntityView;
26   -import org.thingsboard.server.common.data.id.EntityId;
27   -import org.thingsboard.server.common.data.id.EntityViewId;
28   -import org.thingsboard.server.common.data.id.TenantId;
29   -import org.thingsboard.server.dao.entityview.EntityViewService;
30 24 import org.thingsboard.server.gen.transport.TransportProtos;
31 25 import org.thingsboard.server.queue.discovery.ClusterTopologyChangeEvent;
32 26 import org.thingsboard.server.queue.discovery.PartitionChangeEvent;
... ... @@ -36,7 +30,6 @@ import org.thingsboard.server.common.msg.queue.TopicPartitionInfo;
36 30 import org.thingsboard.server.common.msg.queue.TbCallback;
37 31 import org.thingsboard.server.queue.util.TbCoreComponent;
38 32 import org.thingsboard.server.service.queue.TbClusterService;
39   -import org.thingsboard.server.service.telemetry.DefaultTelemetryWebSocketService;
40 33 import org.thingsboard.server.service.telemetry.sub.AlarmSubscriptionUpdate;
41 34 import org.thingsboard.server.service.telemetry.sub.TelemetrySubscriptionUpdate;
42 35
... ... @@ -49,7 +42,6 @@ import java.util.Set;
49 42 import java.util.concurrent.ConcurrentHashMap;
50 43 import java.util.concurrent.ExecutorService;
51 44 import java.util.concurrent.Executors;
52   -import java.util.stream.Collectors;
53 45
54 46 @Slf4j
55 47 @TbCoreComponent
... ... @@ -60,9 +52,6 @@ public class DefaultTbLocalSubscriptionService implements TbLocalSubscriptionSer
60 52 private final Map<String, Map<Integer, TbSubscription>> subscriptionsBySessionId = new ConcurrentHashMap<>();
61 53
62 54 @Autowired
63   - private EntityViewService entityViewService;
64   -
65   - @Autowired
66 55 private PartitionService partitionService;
67 56
68 57 @Autowired
... ... @@ -72,17 +61,17 @@ public class DefaultTbLocalSubscriptionService implements TbLocalSubscriptionSer
72 61 @Lazy
73 62 private SubscriptionManagerService subscriptionManagerService;
74 63
75   - private ExecutorService wsCallBackExecutor;
  64 + private ExecutorService subscriptionUpdateExecutor;
76 65
77 66 @PostConstruct
78 67 public void initExecutor() {
79   - wsCallBackExecutor = Executors.newSingleThreadExecutor(ThingsBoardThreadFactory.forName("ws-sub-callback"));
  68 + subscriptionUpdateExecutor = Executors.newWorkStealingPool(20);
80 69 }
81 70
82 71 @PreDestroy
83 72 public void shutdownExecutor() {
84   - if (wsCallBackExecutor != null) {
85   - wsCallBackExecutor.shutdownNow();
  73 + if (subscriptionUpdateExecutor != null) {
  74 + subscriptionUpdateExecutor.shutdownNow();
86 75 }
87 76 }
88 77
... ... @@ -108,7 +97,7 @@ public class DefaultTbLocalSubscriptionService implements TbLocalSubscriptionSer
108 97 * Since number of subscriptions is usually much less then number of devices that are pushing data.
109 98 */
110 99 subscriptionsBySessionId.values().forEach(map -> map.values()
111   - .forEach(sub -> pushSubscriptionToManagerService(sub, false)));
  100 + .forEach(sub -> pushSubscriptionToManagerService(sub, true)));
112 101 }
113 102 }
114 103
... ... @@ -148,7 +137,7 @@ public class DefaultTbLocalSubscriptionService implements TbLocalSubscriptionSer
148 137 update.getLatestValues().forEach((key, value) -> attrSub.getKeyStates().put(key, value));
149 138 break;
150 139 }
151   - subscription.getUpdateConsumer().accept(sessionId, update);
  140 + subscriptionUpdateExecutor.submit(() -> subscription.getUpdateConsumer().accept(sessionId, update));
152 141 }
153 142 callback.onSuccess();
154 143 }
... ... @@ -158,7 +147,7 @@ public class DefaultTbLocalSubscriptionService implements TbLocalSubscriptionSer
158 147 TbSubscription subscription = subscriptionsBySessionId
159 148 .getOrDefault(sessionId, Collections.emptyMap()).get(update.getSubscriptionId());
160 149 if (subscription != null && subscription.getType() == TbSubscriptionType.ALARMS) {
161   - subscription.getUpdateConsumer().accept(sessionId, update);
  150 + subscriptionUpdateExecutor.submit(() -> subscription.getUpdateConsumer().accept(sessionId, update));
162 151 }
163 152 callback.onSuccess();
164 153 }
... ...
... ... @@ -25,6 +25,7 @@ import org.thingsboard.common.util.ThingsBoardThreadFactory;
25 25 import org.thingsboard.server.common.data.ApiUsageRecordKey;
26 26 import org.thingsboard.server.common.data.EntityType;
27 27 import org.thingsboard.server.common.data.EntityView;
  28 +import org.thingsboard.server.common.data.TenantProfile;
28 29 import org.thingsboard.server.common.data.id.EntityId;
29 30 import org.thingsboard.server.common.data.id.TenantId;
30 31 import org.thingsboard.server.common.data.kv.AttributeKvEntry;
... ... @@ -34,13 +35,14 @@ import org.thingsboard.server.common.data.kv.DoubleDataEntry;
34 35 import org.thingsboard.server.common.data.kv.LongDataEntry;
35 36 import org.thingsboard.server.common.data.kv.StringDataEntry;
36 37 import org.thingsboard.server.common.data.kv.TsKvEntry;
  38 +import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration;
37 39 import org.thingsboard.server.common.msg.queue.ServiceType;
38 40 import org.thingsboard.server.common.msg.queue.TbCallback;
39 41 import org.thingsboard.server.common.msg.queue.TopicPartitionInfo;
40 42 import org.thingsboard.server.dao.attributes.AttributesService;
41 43 import org.thingsboard.server.dao.entityview.EntityViewService;
  44 +import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
42 45 import org.thingsboard.server.dao.timeseries.TimeseriesService;
43   -import org.thingsboard.server.dao.usagerecord.ApiUsageStateService;
44 46 import org.thingsboard.server.gen.transport.TransportProtos;
45 47 import org.thingsboard.server.queue.discovery.PartitionService;
46 48 import org.thingsboard.server.queue.usagestats.TbApiUsageClient;
... ... @@ -119,11 +121,12 @@ public class DefaultTelemetrySubscriptionService extends AbstractSubscriptionSer
119 121 @Override
120 122 public void saveAndNotify(TenantId tenantId, EntityId entityId, List<TsKvEntry> ts, long ttl, FutureCallback<Void> callback) {
121 123 checkInternalEntity(entityId);
122   - if (apiUsageStateService.getApiUsageState(tenantId).isDbStorageEnabled()) {
  124 + boolean sysTenant = TenantId.SYS_TENANT_ID.equals(tenantId) || tenantId == null;
  125 + if (sysTenant || apiUsageStateService.getApiUsageState(tenantId).isDbStorageEnabled()) {
123 126 saveAndNotifyInternal(tenantId, entityId, ts, ttl, new FutureCallback<Integer>() {
124 127 @Override
125 128 public void onSuccess(Integer result) {
126   - if (result != null && result > 0) {
  129 + if (!sysTenant && result != null && result > 0) {
127 130 apiUsageClient.report(tenantId, ApiUsageRecordKey.STORAGE_DP_COUNT, result);
128 131 }
129 132 callback.onSuccess(null);
... ... @@ -134,7 +137,7 @@ public class DefaultTelemetrySubscriptionService extends AbstractSubscriptionSer
134 137 callback.onFailure(t);
135 138 }
136 139 });
137   - } else{
  140 + } else {
138 141 callback.onFailure(new RuntimeException("DB storage writes are disabled due to API limits!"));
139 142 }
140 143 }
... ...
... ... @@ -54,12 +54,16 @@ import org.thingsboard.server.dao.device.provision.ProvisionResponse;
54 54 import org.thingsboard.server.dao.relation.RelationService;
55 55 import org.thingsboard.server.dao.util.mapping.JacksonUtil;
56 56 import org.thingsboard.server.gen.transport.TransportProtos;
  57 +import org.thingsboard.server.gen.transport.TransportProtos.DeviceCredentialsProto;
57 58 import org.thingsboard.server.gen.transport.TransportProtos.DeviceInfoProto;
58 59 import org.thingsboard.server.gen.transport.TransportProtos.GetOrCreateDeviceFromGatewayRequestMsg;
59 60 import org.thingsboard.server.gen.transport.TransportProtos.GetOrCreateDeviceFromGatewayResponseMsg;
60 61 import org.thingsboard.server.gen.transport.TransportProtos.GetEntityProfileRequestMsg;
61 62 import org.thingsboard.server.gen.transport.TransportProtos.GetEntityProfileResponseMsg;
62 63 import org.thingsboard.server.gen.transport.TransportProtos.ProvisionDeviceRequestMsg;
  64 +import org.thingsboard.server.gen.transport.TransportProtos.ProvisionDeviceResponseMsg;
  65 +import org.thingsboard.server.gen.transport.TransportProtos.ProvisionDeviceResponseMsgOrBuilder;
  66 +import org.thingsboard.server.gen.transport.TransportProtos.ProvisionResponseStatus;
63 67 import org.thingsboard.server.gen.transport.TransportProtos.TransportApiRequestMsg;
64 68 import org.thingsboard.server.gen.transport.TransportProtos.TransportApiResponseMsg;
65 69 import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceCredentialsResponseMsg;
... ... @@ -71,7 +75,7 @@ import org.thingsboard.server.dao.device.provision.ProvisionFailedException;
71 75 import org.thingsboard.server.service.apiusage.TbApiUsageStateService;
72 76 import org.thingsboard.server.service.executors.DbCallbackExecutorService;
73 77 import org.thingsboard.server.service.profile.TbDeviceProfileCache;
74   -import org.thingsboard.server.service.profile.TbTenantProfileCache;
  78 +import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
75 79 import org.thingsboard.server.service.queue.TbClusterService;
76 80 import org.thingsboard.server.service.state.DeviceStateService;
77 81
... ... @@ -164,22 +168,27 @@ public class DefaultTransportApiService implements TransportApiService {
164 168 }
165 169
166 170 private ListenableFuture<TransportApiResponseMsg> validateCredentials(TransportProtos.ValidateBasicMqttCredRequestMsg mqtt) {
167   - DeviceCredentials credentials = deviceCredentialsService.findDeviceCredentialsByCredentialsId(mqtt.getUserName());
168   - if (credentials != null) {
169   - if (credentials.getCredentialsType() == DeviceCredentialsType.ACCESS_TOKEN) {
170   - return getDeviceInfo(credentials.getDeviceId(), credentials);
171   - } else if (credentials.getCredentialsType() == DeviceCredentialsType.MQTT_BASIC) {
172   - if (!checkMqttCredentials(mqtt, credentials)) {
173   - credentials = null;
  171 + DeviceCredentials credentials = null;
  172 + if (!StringUtils.isEmpty(mqtt.getUserName())) {
  173 + credentials = deviceCredentialsService.findDeviceCredentialsByCredentialsId(mqtt.getUserName());
  174 + if (credentials != null) {
  175 + if (credentials.getCredentialsType() == DeviceCredentialsType.ACCESS_TOKEN) {
  176 + return getDeviceInfo(credentials.getDeviceId(), credentials);
  177 + } else if (credentials.getCredentialsType() == DeviceCredentialsType.MQTT_BASIC) {
  178 + if (!checkMqttCredentials(mqtt, credentials)) {
  179 + credentials = null;
  180 + }
  181 + } else {
  182 + return getEmptyTransportApiResponseFuture();
174 183 }
175 184 }
176   - }
177   - if (credentials == null) {
178   - credentials = checkMqttCredentials(mqtt, EncryptionUtil.getSha3Hash("|", mqtt.getClientId(), mqtt.getUserName()));
179 185 if (credentials == null) {
180   - credentials = checkMqttCredentials(mqtt, EncryptionUtil.getSha3Hash(mqtt.getClientId()));
  186 + credentials = checkMqttCredentials(mqtt, EncryptionUtil.getSha3Hash("|", mqtt.getClientId(), mqtt.getUserName()));
181 187 }
182 188 }
  189 + if (credentials == null) {
  190 + credentials = checkMqttCredentials(mqtt, EncryptionUtil.getSha3Hash(mqtt.getClientId()));
  191 + }
183 192 if (credentials != null) {
184 193 return getDeviceInfo(credentials.getDeviceId(), credentials);
185 194 } else {
... ... @@ -289,30 +298,32 @@ public class DefaultTransportApiService implements TransportApiService {
289 298 requestMsg.getProvisionDeviceCredentialsMsg().getProvisionDeviceSecret()))));
290 299 } catch (ProvisionFailedException e) {
291 300 return Futures.immediateFuture(getTransportApiResponseMsg(
292   - TransportProtos.DeviceCredentialsProto.getDefaultInstance(),
  301 + new DeviceCredentials(),
293 302 TransportProtos.ProvisionResponseStatus.valueOf(e.getMessage())));
294 303 }
295   - return Futures.transform(provisionResponseFuture, provisionResponse -> getTransportApiResponseMsg(
296   - getDeviceCredentials(provisionResponse.getDeviceCredentials()), TransportProtos.ProvisionResponseStatus.SUCCESS),
  304 + return Futures.transform(provisionResponseFuture, provisionResponse -> getTransportApiResponseMsg(provisionResponse.getDeviceCredentials(), TransportProtos.ProvisionResponseStatus.SUCCESS),
297 305 dbCallbackExecutorService);
298 306 }
299 307
300   - private TransportApiResponseMsg getTransportApiResponseMsg(TransportProtos.DeviceCredentialsProto deviceCredentials, TransportProtos.ProvisionResponseStatus status) {
301   - return TransportApiResponseMsg.newBuilder()
302   - .setProvisionDeviceResponseMsg(TransportProtos.ProvisionDeviceResponseMsg.newBuilder()
303   - .setDeviceCredentials(deviceCredentials)
304   - .setProvisionResponseStatus(status)
305   - .build())
306   - .build();
307   - }
  308 + private TransportApiResponseMsg getTransportApiResponseMsg(DeviceCredentials deviceCredentials, TransportProtos.ProvisionResponseStatus status) {
  309 + if (!status.equals(ProvisionResponseStatus.SUCCESS)) {
  310 + return TransportApiResponseMsg.newBuilder().setProvisionDeviceResponseMsg(TransportProtos.ProvisionDeviceResponseMsg.newBuilder().setStatus(status).build()).build();
  311 + }
  312 + TransportProtos.ProvisionDeviceResponseMsg.Builder provisionResponse = TransportProtos.ProvisionDeviceResponseMsg.newBuilder()
  313 + .setCredentialsType(TransportProtos.CredentialsType.valueOf(deviceCredentials.getCredentialsType().name()))
  314 + .setStatus(status);
  315 + switch (deviceCredentials.getCredentialsType()){
  316 + case ACCESS_TOKEN:
  317 + provisionResponse.setCredentialsValue(deviceCredentials.getCredentialsId());
  318 + break;
  319 + case MQTT_BASIC:
  320 + case X509_CERTIFICATE:
  321 + provisionResponse.setCredentialsValue(deviceCredentials.getCredentialsValue());
  322 + break;
  323 + }
308 324
309   - private TransportProtos.DeviceCredentialsProto getDeviceCredentials(DeviceCredentials deviceCredentials) {
310   - return TransportProtos.DeviceCredentialsProto.newBuilder()
311   - .setDeviceIdMSB(deviceCredentials.getDeviceId().getId().getMostSignificantBits())
312   - .setDeviceIdLSB(deviceCredentials.getDeviceId().getId().getLeastSignificantBits())
313   - .setCredentialsType(TransportProtos.CredentialsType.valueOf(deviceCredentials.getCredentialsType().name()))
314   - .setCredentialsId(deviceCredentials.getCredentialsId())
315   - .setCredentialsValue(deviceCredentials.getCredentialsValue() != null ? deviceCredentials.getCredentialsValue() : "")
  325 + return TransportApiResponseMsg.newBuilder()
  326 + .setProvisionDeviceResponseMsg(provisionResponse.build())
316 327 .build();
317 328 }
318 329
... ...
... ... @@ -19,6 +19,7 @@ import lombok.extern.slf4j.Slf4j;
19 19 import org.springframework.beans.factory.annotation.Value;
20 20 import org.springframework.boot.context.event.ApplicationReadyEvent;
21 21 import org.springframework.context.event.EventListener;
  22 +import org.springframework.core.annotation.Order;
22 23 import org.springframework.stereotype.Service;
23 24 import org.thingsboard.server.common.stats.MessagesStats;
24 25 import org.thingsboard.server.common.stats.StatsFactory;
... ... @@ -90,6 +91,7 @@ public class TbCoreTransportApiService {
90 91 }
91 92
92 93 @EventListener(ApplicationReadyEvent.class)
  94 + @Order(value = 2)
93 95 public void onApplicationEvent(ApplicationReadyEvent applicationReadyEvent) {
94 96 log.info("Received application ready event. Starting polling for events.");
95 97 transportApiTemplate.init(transportApiService);
... ...
... ... @@ -3,4 +3,5 @@ activation.subject=Your account activation on Thingsboard
3 3 account.activated.subject=Thingsboard - your account has been activated
4 4 reset.password.subject=Thingsboard - Password reset has been requested
5 5 password.was.reset.subject=Thingsboard - your account password has been reset
6   -account.lockout.subject=Thingsboard - User account has been lockout
\ No newline at end of file
  6 +account.lockout.subject=Thingsboard - User account has been lockout
  7 +api.usage.state=Thingsboard - Account limits
\ No newline at end of file
... ...
  1 +<#--
  2 +
  3 + Copyright © 2016-2020 The Thingsboard Authors
  4 +
  5 + Licensed under the Apache License, Version 2.0 (the "License");
  6 + you may not use this file except in compliance with the License.
  7 + You may obtain a copy of the License at
  8 +
  9 + http://www.apache.org/licenses/LICENSE-2.0
  10 +
  11 + Unless required by applicable law or agreed to in writing, software
  12 + distributed under the License is distributed on an "AS IS" BASIS,
  13 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14 + See the License for the specific language governing permissions and
  15 + limitations under the License.
  16 +
  17 +-->
  18 +<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
  19 + "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
  20 +<html xmlns="http://www.w3.org/1999/xhtml"
  21 + style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
  22 +<head>
  23 + <meta name="viewport" content="width=device-width"/>
  24 + <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
  25 + <title>Thingsboard - Api Usage State</title>
  26 +
  27 +
  28 + <style type="text/css">
  29 + img {
  30 + max-width: 100%;
  31 + }
  32 +
  33 + body {
  34 + -webkit-font-smoothing: antialiased;
  35 + -webkit-text-size-adjust: none;
  36 + width: 100% !important;
  37 + height: 100%;
  38 + line-height: 1.6em;
  39 + }
  40 +
  41 + body {
  42 + background-color: #f6f6f6;
  43 + }
  44 +
  45 + @media only screen and (max-width: 640px) {
  46 + body {
  47 + padding: 0 !important;
  48 + }
  49 +
  50 + h1 {
  51 + font-weight: 800 !important;
  52 + margin: 20px 0 5px !important;
  53 + }
  54 +
  55 + h2 {
  56 + font-weight: 800 !important;
  57 + margin: 20px 0 5px !important;
  58 + }
  59 +
  60 + h3 {
  61 + font-weight: 800 !important;
  62 + margin: 20px 0 5px !important;
  63 + }
  64 +
  65 + h4 {
  66 + font-weight: 800 !important;
  67 + margin: 20px 0 5px !important;
  68 + }
  69 +
  70 + h1 {
  71 + font-size: 22px !important;
  72 + }
  73 +
  74 + h2 {
  75 + font-size: 18px !important;
  76 + }
  77 +
  78 + h3 {
  79 + font-size: 16px !important;
  80 + }
  81 +
  82 + .container {
  83 + padding: 0 !important;
  84 + width: 100% !important;
  85 + }
  86 +
  87 + .content {
  88 + padding: 0 !important;
  89 + }
  90 +
  91 + .content-wrap {
  92 + padding: 10px !important;
  93 + }
  94 +
  95 + .invoice {
  96 + width: 100% !important;
  97 + }
  98 + }
  99 + </style>
  100 +</head>
  101 +
  102 +<body itemscope itemtype="http://schema.org/EmailMessage"
  103 + style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; width: 100% !important; height: 100%; line-height: 1.6em; background-color: #f6f6f6; margin: 0;"
  104 + bgcolor="#f6f6f6">
  105 +
  106 +<table class="main" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 14px; box-sizing: border-box; border-radius: 3px; width: 100%; background-color: #f6f6f6; margin: 0px auto;" cellspacing="0" cellpadding="0" bgcolor="#f6f6f6">
  107 + <tbody>
  108 + <tr style="box-sizing: border-box; margin: 0px;">
  109 + <td class="content-wrap" style="box-sizing: border-box; vertical-align: top; margin: 0px; padding: 20px;" align="center" valign="top">
  110 + <table style="box-sizing: border-box; border: 1px solid #e9e9e9; border-radius: 3px; margin: 0px; height: 367px; background-color: #ffffff; width: 600px; max-width: 600px !important;" width="600" cellspacing="0" cellpadding="0">
  111 + <tbody>
  112 + <tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
  113 + <td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; color: #348eda; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0px; padding: 0px; height: 110px;" valign="top"><img src="https://media.thingsboard.io/email/head.png" alt="" width="598" height="91" /></td>
  114 + </tr>
  115 + <tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 16px; margin: 0;">
  116 + <td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; color: #000000; box-sizing: border-box; font-size: 16px; margin: 0px; padding: 0px 32px; height: 66px; vertical-align: middle;" valign="middle">Your ThingsBoard account feature was <strong>disabled</strong></td>
  117 + </tr>
  118 + <tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; line-height: 24px; margin: 0;">
  119 + <td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; margin: 0px; padding: 0px 32px; height: 93px; vertical-align: top;" valign="top">
  120 + <div style="padding: 16px; margin-bottom: 24px; border: solid 2px #EB5757; border-radius: 6px; background: rgba(235, 87, 87, 0.05);"><img style="vertical-align: middle; padding-right: 6px;" src="https://media.thingsboard.io/email/alarm.png" alt="" width="20" height="20" />
  121 + <div style="display: inline; vertical-align: middle;">We have <strong>disabled</strong> the ${apiFeature} for your account because ThingsBoard has already <strong>${apiLimitValueLabel}.</strong></div>
  122 + </div>
  123 + </td>
  124 + </tr>
  125 + <tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
  126 + <td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0px; padding: 0px 32px; height: 59px;" valign="top">Please contact your system administrator to resolve the issue.</td>
  127 + </tr>
  128 + <tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
  129 + <td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0px; padding: 0px 32px; height: 40px;" valign="top">&mdash; The ThingsBoard</td>
  130 + </tr>
  131 + </tbody>
  132 + </table>
  133 + </td>
  134 + </tr>
  135 + </tbody>
  136 +</table>
  137 +<table style="color: #999999; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 14px; box-sizing: border-box; margin: 0px auto; height: 64px; background-color: #f6f6f6; width: 100%;" cellpadding="0px 0px 20px">
  138 + <tbody>
  139 + <tr style="box-sizing: border-box; margin: 0px;">
  140 + <td class="aligncenter content-block" style="box-sizing: border-box; font-size: 12px; margin: 0px; padding: 0px 0px 20px; width: 600px; text-align: center; vertical-align: middle;" align="center" valign="top">This email was sent to&nbsp;<a style="box-sizing: border-box; color: #999999; margin: 0px;" href="mailto:${targetEmail}">${targetEmail}</a>&nbsp;by ThingsBoard.</td>
  141 + </tr>
  142 + </tbody>
  143 +</table>
  144 +</body>
  145 +</html>
... ...
  1 +<#--
  2 +
  3 + Copyright © 2016-2020 The Thingsboard Authors
  4 +
  5 + Licensed under the Apache License, Version 2.0 (the "License");
  6 + you may not use this file except in compliance with the License.
  7 + You may obtain a copy of the License at
  8 +
  9 + http://www.apache.org/licenses/LICENSE-2.0
  10 +
  11 + Unless required by applicable law or agreed to in writing, software
  12 + distributed under the License is distributed on an "AS IS" BASIS,
  13 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14 + See the License for the specific language governing permissions and
  15 + limitations under the License.
  16 +
  17 +-->
  18 +<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
  19 + "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
  20 +<html xmlns="http://www.w3.org/1999/xhtml"
  21 + style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
  22 +<head>
  23 + <meta name="viewport" content="width=device-width"/>
  24 + <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
  25 + <title>Thingsboard - Api Usage State</title>
  26 +
  27 +
  28 + <style type="text/css">
  29 + img {
  30 + max-width: 100%;
  31 + }
  32 +
  33 + body {
  34 + -webkit-font-smoothing: antialiased;
  35 + -webkit-text-size-adjust: none;
  36 + width: 100% !important;
  37 + height: 100%;
  38 + line-height: 1.6em;
  39 + }
  40 +
  41 + body {
  42 + background-color: #f6f6f6;
  43 + }
  44 +
  45 + @media only screen and (max-width: 640px) {
  46 + body {
  47 + padding: 0 !important;
  48 + }
  49 +
  50 + h1 {
  51 + font-weight: 800 !important;
  52 + margin: 20px 0 5px !important;
  53 + }
  54 +
  55 + h2 {
  56 + font-weight: 800 !important;
  57 + margin: 20px 0 5px !important;
  58 + }
  59 +
  60 + h3 {
  61 + font-weight: 800 !important;
  62 + margin: 20px 0 5px !important;
  63 + }
  64 +
  65 + h4 {
  66 + font-weight: 800 !important;
  67 + margin: 20px 0 5px !important;
  68 + }
  69 +
  70 + h1 {
  71 + font-size: 22px !important;
  72 + }
  73 +
  74 + h2 {
  75 + font-size: 18px !important;
  76 + }
  77 +
  78 + h3 {
  79 + font-size: 16px !important;
  80 + }
  81 +
  82 + .container {
  83 + padding: 0 !important;
  84 + width: 100% !important;
  85 + }
  86 +
  87 + .content {
  88 + padding: 0 !important;
  89 + }
  90 +
  91 + .content-wrap {
  92 + padding: 10px !important;
  93 + }
  94 +
  95 + .invoice {
  96 + width: 100% !important;
  97 + }
  98 + }
  99 + </style>
  100 +</head>
  101 +
  102 +<body itemscope itemtype="http://schema.org/EmailMessage"
  103 + style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; width: 100% !important; height: 100%; line-height: 1.6em; background-color: #f6f6f6; margin: 0;"
  104 + bgcolor="#f6f6f6">
  105 +
  106 +<table class="main" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 14px; box-sizing: border-box; border-radius: 3px; width: 100%; background-color: #f6f6f6; margin: 0px auto;" cellspacing="0" cellpadding="0" bgcolor="#f6f6f6">
  107 + <tbody>
  108 + <tr style="box-sizing: border-box; margin: 0px;">
  109 + <td class="content-wrap" style="box-sizing: border-box; vertical-align: top; margin: 0px; padding: 20px;" align="center" valign="top">
  110 + <table style="box-sizing: border-box; border: 1px solid #e9e9e9; border-radius: 3px; margin: 0px; height: 310px; background-color: #ffffff; width: 600px; max-width: 600px !important;" width="600" cellspacing="0" cellpadding="0">
  111 + <tbody>
  112 + <tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
  113 + <td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; color: #348eda; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0px; padding: 0px; height: 110px;" valign="top"><img src="https://media.thingsboard.io/email/head.png" alt="" width="598" height="91" /></td>
  114 + </tr>
  115 + <tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 16px; margin: 0;">
  116 + <td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; color: #000000; box-sizing: border-box; font-size: 16px; margin: 0px; padding: 0px 32px; height: 66px; vertical-align: middle;" valign="middle">Your ThingsBoard account feature was <strong>enabled</strong></td>
  117 + </tr>
  118 + <tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; line-height: 24px; margin: 0;">
  119 + <td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; margin: 0px; padding: 0px 32px; height: 93px; vertical-align: top;" valign="top">
  120 + <div style="padding: 16px; margin-bottom: 24px; border: solid 2px #27AE60; border-radius: 6px; background: rgba(39, 174, 96, 0.05);"><img style="vertical-align: middle; padding-right: 6px;" src="https://media.thingsboard.io/email/confirm.png" alt="" width="20" height="20" />
  121 + <div style="display: inline; vertical-align: middle;">We have enabled the ${apiFeature} for your account and ThingsBoard is already able to ${apiLabel} messages.</div>
  122 + </div>
  123 + </td>
  124 + </tr>
  125 + <tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
  126 + <td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0px; padding: 0px 32px; height: 40px;" valign="top">&mdash; The ThingsBoard</td>
  127 + </tr>
  128 + </tbody>
  129 + </table>
  130 + </td>
  131 + </tr>
  132 + </tbody>
  133 +</table>
  134 +<table style="color: #999999; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 14px; box-sizing: border-box; margin: 0px auto; height: 64px; background-color: #f6f6f6; width: 100%;" cellpadding="0px 0px 20px">
  135 + <tbody>
  136 + <tr style="box-sizing: border-box; margin: 0px;">
  137 + <td class="aligncenter content-block" style="box-sizing: border-box; font-size: 12px; margin: 0px; padding: 0px 0px 20px; width: 600px; text-align: center; vertical-align: middle;" align="center" valign="top">This email was sent to&nbsp;<a style="box-sizing: border-box; color: #999999; margin: 0px;" href="mailto:${targetEmail}">${targetEmail}</a>&nbsp;by ThingsBoard.</td>
  138 + </tr>
  139 + </tbody>
  140 +</table>
  141 +</body>
  142 +</html>
... ...
  1 +<#--
  2 +
  3 + Copyright © 2016-2020 The Thingsboard Authors
  4 +
  5 + Licensed under the Apache License, Version 2.0 (the "License");
  6 + you may not use this file except in compliance with the License.
  7 + You may obtain a copy of the License at
  8 +
  9 + http://www.apache.org/licenses/LICENSE-2.0
  10 +
  11 + Unless required by applicable law or agreed to in writing, software
  12 + distributed under the License is distributed on an "AS IS" BASIS,
  13 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14 + See the License for the specific language governing permissions and
  15 + limitations under the License.
  16 +
  17 +-->
  18 +<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
  19 + "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
  20 +<html xmlns="http://www.w3.org/1999/xhtml"
  21 + style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
  22 +<head>
  23 + <meta name="viewport" content="width=device-width"/>
  24 + <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
  25 + <title>Thingsboard - Api Usage State</title>
  26 +
  27 +
  28 + <style type="text/css">
  29 + img {
  30 + max-width: 100%;
  31 + }
  32 +
  33 + body {
  34 + -webkit-font-smoothing: antialiased;
  35 + -webkit-text-size-adjust: none;
  36 + width: 100% !important;
  37 + height: 100%;
  38 + line-height: 1.6em;
  39 + }
  40 +
  41 + body {
  42 + background-color: #f6f6f6;
  43 + }
  44 +
  45 + @media only screen and (max-width: 640px) {
  46 + body {
  47 + padding: 0 !important;
  48 + }
  49 +
  50 + h1 {
  51 + font-weight: 800 !important;
  52 + margin: 20px 0 5px !important;
  53 + }
  54 +
  55 + h2 {
  56 + font-weight: 800 !important;
  57 + margin: 20px 0 5px !important;
  58 + }
  59 +
  60 + h3 {
  61 + font-weight: 800 !important;
  62 + margin: 20px 0 5px !important;
  63 + }
  64 +
  65 + h4 {
  66 + font-weight: 800 !important;
  67 + margin: 20px 0 5px !important;
  68 + }
  69 +
  70 + h1 {
  71 + font-size: 22px !important;
  72 + }
  73 +
  74 + h2 {
  75 + font-size: 18px !important;
  76 + }
  77 +
  78 + h3 {
  79 + font-size: 16px !important;
  80 + }
  81 +
  82 + .container {
  83 + padding: 0 !important;
  84 + width: 100% !important;
  85 + }
  86 +
  87 + .content {
  88 + padding: 0 !important;
  89 + }
  90 +
  91 + .content-wrap {
  92 + padding: 10px !important;
  93 + }
  94 +
  95 + .invoice {
  96 + width: 100% !important;
  97 + }
  98 + }
  99 + </style>
  100 +</head>
  101 +
  102 +<body itemscope itemtype="http://schema.org/EmailMessage"
  103 + style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; width: 100% !important; height: 100%; line-height: 1.6em; background-color: #f6f6f6; margin: 0;"
  104 + bgcolor="#f6f6f6">
  105 +
  106 +<table class="main" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 14px; box-sizing: border-box; border-radius: 3px; width: 100%; background-color: #f6f6f6; margin: 0px auto;" cellspacing="0" cellpadding="0" bgcolor="#f6f6f6">
  107 + <tbody>
  108 + <tr style="box-sizing: border-box; margin: 0px;">
  109 + <td class="content-wrap" style="box-sizing: border-box; vertical-align: top; margin: 0px; padding: 20px;" align="center" valign="top">
  110 + <table style="box-sizing: border-box; border: 1px solid #e9e9e9; border-radius: 3px; margin: 0px; height: 367px; background-color: #ffffff; width: 600px; max-width: 600px !important;" width="600" cellspacing="0" cellpadding="0">
  111 + <tbody>
  112 + <tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
  113 + <td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; color: #348eda; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0px; padding: 0px; height: 110px;" valign="top"><img src="https://media.thingsboard.io/email/head.png" alt="" width="598" height="91" /></td>
  114 + </tr>
  115 + <tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 16px; margin: 0;">
  116 + <td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; color: #000000; box-sizing: border-box; font-size: 16px; margin: 0px; padding: 0px 32px; height: 66px; vertical-align: middle;" valign="middle"><strong>Warning:</strong> your ThingsBoard account feature may be disabled soon</td>
  117 + </tr>
  118 + <tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; line-height: 24px; margin: 0;">
  119 + <td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; margin: 0px; padding: 0px 32px; height: 93px; vertical-align: top;" valign="top">
  120 + <div style="padding: 16px; margin-bottom: 24px; border: solid 2px #F2994A; border-radius: 6px; background: rgba(242, 153, 74, 0.05);"><img style="vertical-align: middle; padding-right: 6px;" src="https://media.thingsboard.io/email/warning.png" alt="" width="20" height="20" />
  121 + <div style="display: inline; vertical-align: middle;">ThingsBoard has already&nbsp;${apiValueLabel}.<br />${apiFeature} will be <strong>disabled</strong> for your account once the limit will be reached.</div>
  122 + </div>
  123 + </td>
  124 + </tr>
  125 + <tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
  126 + <td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0px; padding: 0px 32px; height: 59px;" valign="top">Please contact your system administrator to resolve the issue.</td>
  127 + </tr>
  128 + <tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
  129 + <td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0px; padding: 0px 32px; height: 40px;" valign="top">&mdash; The ThingsBoard</td>
  130 + </tr>
  131 + </tbody>
  132 + </table>
  133 + </td>
  134 + </tr>
  135 + </tbody>
  136 +</table>
  137 +<table style="color: #999999; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 14px; box-sizing: border-box; margin: 0px auto; height: 64px; background-color: #f6f6f6; width: 100%;" cellpadding="0px 0px 20px">
  138 + <tbody>
  139 + <tr style="box-sizing: border-box; margin: 0px;">
  140 + <td class="aligncenter content-block" style="box-sizing: border-box; font-size: 12px; margin: 0px; padding: 0px 0px 20px; width: 600px; text-align: center; vertical-align: middle;" align="center" valign="top">This email was sent to&nbsp;<a style="box-sizing: border-box; color: #999999; margin: 0px;" href="mailto:${targetEmail}">${targetEmail}</a>&nbsp;by ThingsBoard.</td>
  141 + </tr>
  142 + </tbody>
  143 +</table>
  144 +</body>
  145 +</html>
... ...
... ... @@ -281,8 +281,12 @@ actors:
281 281 js_thread_pool_size: "${ACTORS_RULE_JS_THREAD_POOL_SIZE:50}"
282 282 # Specify thread pool size for mail sender executor service
283 283 mail_thread_pool_size: "${ACTORS_RULE_MAIL_THREAD_POOL_SIZE:50}"
  284 + # Specify thread pool size for sms sender executor service
  285 + sms_thread_pool_size: "${ACTORS_RULE_SMS_THREAD_POOL_SIZE:50}"
284 286 # Whether to allow usage of system mail service for rules
285 287 allow_system_mail_service: "${ACTORS_RULE_ALLOW_SYSTEM_MAIL_SERVICE:true}"
  288 + # Whether to allow usage of system sms service for rules
  289 + allow_system_sms_service: "${ACTORS_RULE_ALLOW_SYSTEM_SMS_SERVICE:true}"
286 290 # Specify thread pool size for external call service
287 291 external_call_thread_pool_size: "${ACTORS_RULE_EXTERNAL_CALL_THREAD_POOL_SIZE:50}"
288 292 chain:
... ... @@ -591,6 +595,7 @@ queue:
591 595 linger.ms: "${TB_KAFKA_LINGER_MS:1}"
592 596 buffer.memory: "${TB_BUFFER_MEMORY:33554432}"
593 597 replication_factor: "${TB_QUEUE_KAFKA_REPLICATION_FACTOR:1}"
  598 + max_poll_interval_ms: "${TB_QUEUE_KAFKA_MAX_POLL_INTERVAL_MS:300000}"
594 599 max_poll_records: "${TB_QUEUE_KAFKA_MAX_POLL_RECORDS:8192}"
595 600 max_partition_fetch_bytes: "${TB_QUEUE_KAFKA_MAX_PARTITION_FETCH_BYTES:16777216}"
596 601 fetch_max_bytes: "${TB_QUEUE_KAFKA_FETCH_MAX_BYTES:134217728}"
... ... @@ -690,8 +695,6 @@ queue:
690 695 max_requests_timeout: "${REMOTE_JS_MAX_REQUEST_TIMEOUT:10000}"
691 696 # JS response poll interval
692 697 response_poll_interval: "${REMOTE_JS_RESPONSE_POLL_INTERVAL_MS:25}"
693   - # JS response auto commit interval
694   - response_auto_commit_interval: "${REMOTE_JS_RESPONSE_AUTO_COMMIT_INTERVAL_MS:100}"
695 698 rule-engine:
696 699 topic: "${TB_QUEUE_RULE_ENGINE_TOPIC:tb_rule_engine}"
697 700 poll-interval: "${TB_QUEUE_RULE_ENGINE_POLL_INTERVAL_MS:25}"
... ... @@ -766,7 +769,6 @@ metrics:
766 769 # Metrics percentiles returned by actuator for timer metrics. List of double values (divided by ,).
767 770 percentiles: "${METRICS_TIMER_PERCENTILES:0.5}"
768 771
769   -
770 772 management:
771 773 endpoints:
772 774 web:
... ...
... ... @@ -15,7 +15,6 @@
15 15 */
16 16 package org.thingsboard.server.controller;
17 17
18   -import com.datastax.oss.driver.api.core.uuid.Uuids;
19 18 import com.fasterxml.jackson.core.type.TypeReference;
20 19 import com.fasterxml.jackson.databind.JsonNode;
21 20 import com.fasterxml.jackson.databind.ObjectMapper;
... ... @@ -33,12 +32,7 @@ import org.junit.Rule;
33 32 import org.junit.rules.TestRule;
34 33 import org.junit.rules.TestWatcher;
35 34 import org.junit.runner.Description;
36   -import org.junit.runner.RunWith;
37 35 import org.springframework.beans.factory.annotation.Autowired;
38   -import org.springframework.boot.test.context.SpringBootContextLoader;
39   -import org.springframework.boot.test.context.SpringBootTest;
40   -import org.springframework.context.annotation.ComponentScan;
41   -import org.springframework.context.annotation.Configuration;
42 36 import org.springframework.http.HttpHeaders;
43 37 import org.springframework.http.MediaType;
44 38 import org.springframework.http.converter.HttpMessageConverter;
... ... @@ -46,10 +40,6 @@ import org.springframework.http.converter.StringHttpMessageConverter;
46 40 import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
47 41 import org.springframework.mock.http.MockHttpInputMessage;
48 42 import org.springframework.mock.http.MockHttpOutputMessage;
49   -import org.springframework.test.annotation.DirtiesContext;
50   -import org.springframework.test.context.ActiveProfiles;
51   -import org.springframework.test.context.ContextConfiguration;
52   -import org.springframework.test.context.junit4.SpringRunner;
53 43 import org.springframework.test.web.servlet.MockMvc;
54 44 import org.springframework.test.web.servlet.MvcResult;
55 45 import org.springframework.test.web.servlet.ResultActions;
... ... @@ -58,7 +48,6 @@ import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilde
58 48 import org.springframework.util.LinkedMultiValueMap;
59 49 import org.springframework.util.MultiValueMap;
60 50 import org.springframework.web.context.WebApplicationContext;
61   -import org.thingsboard.server.common.data.BaseData;
62 51 import org.thingsboard.server.common.data.Customer;
63 52 import org.thingsboard.server.common.data.DeviceProfile;
64 53 import org.thingsboard.server.common.data.DeviceProfileType;
... ... @@ -68,11 +57,13 @@ import org.thingsboard.server.common.data.User;
68 57 import org.thingsboard.server.common.data.device.profile.DefaultDeviceProfileConfiguration;
69 58 import org.thingsboard.server.common.data.device.profile.DefaultDeviceProfileTransportConfiguration;
70 59 import org.thingsboard.server.common.data.device.profile.DeviceProfileData;
71   -import org.thingsboard.server.common.data.device.profile.ProvisionDeviceProfileCredentials;
  60 +import org.thingsboard.server.common.data.device.profile.DeviceProfileTransportConfiguration;
  61 +import org.thingsboard.server.common.data.device.profile.MqttDeviceProfileTransportConfiguration;
  62 +import org.thingsboard.server.common.data.device.profile.MqttTopics;
  63 +import org.thingsboard.server.common.data.device.profile.ProtoTransportPayloadConfiguration;
  64 +import org.thingsboard.server.common.data.device.profile.TransportPayloadTypeConfiguration;
72 65 import org.thingsboard.server.common.data.id.HasId;
73   -import org.thingsboard.server.common.data.id.RuleChainId;
74 66 import org.thingsboard.server.common.data.id.TenantId;
75   -import org.thingsboard.server.common.data.id.UUIDBased;
76 67 import org.thingsboard.server.common.data.page.PageLink;
77 68 import org.thingsboard.server.common.data.page.TimePageLink;
78 69 import org.thingsboard.server.common.data.security.Authority;
... ... @@ -330,7 +321,7 @@ public abstract class AbstractWebTest {
330 321 }
331 322 }
332 323
333   - protected DeviceProfile createDeviceProfile(String name) {
  324 + protected DeviceProfile createDeviceProfile(String name, DeviceProfileTransportConfiguration deviceProfileTransportConfiguration) {
334 325 DeviceProfile deviceProfile = new DeviceProfile();
335 326 deviceProfile.setName(name);
336 327 deviceProfile.setType(DeviceProfileType.DEFAULT);
... ... @@ -338,15 +329,34 @@ public abstract class AbstractWebTest {
338 329 deviceProfile.setDescription(name + " Test");
339 330 DeviceProfileData deviceProfileData = new DeviceProfileData();
340 331 DefaultDeviceProfileConfiguration configuration = new DefaultDeviceProfileConfiguration();
341   - DefaultDeviceProfileTransportConfiguration transportConfiguration = new DefaultDeviceProfileTransportConfiguration();
342 332 deviceProfileData.setConfiguration(configuration);
343   - deviceProfileData.setTransportConfiguration(transportConfiguration);
  333 + if (deviceProfileTransportConfiguration != null) {
  334 + deviceProfileData.setTransportConfiguration(deviceProfileTransportConfiguration);
  335 + } else {
  336 + deviceProfileData.setTransportConfiguration(new DefaultDeviceProfileTransportConfiguration());
  337 + }
344 338 deviceProfile.setProfileData(deviceProfileData);
345 339 deviceProfile.setDefault(false);
346 340 deviceProfile.setDefaultRuleChainId(null);
347 341 return deviceProfile;
348 342 }
349 343
  344 + protected MqttDeviceProfileTransportConfiguration createMqttDeviceProfileTransportConfiguration(TransportPayloadTypeConfiguration transportPayloadTypeConfiguration) {
  345 + MqttDeviceProfileTransportConfiguration mqttDeviceProfileTransportConfiguration = new MqttDeviceProfileTransportConfiguration();
  346 + mqttDeviceProfileTransportConfiguration.setDeviceTelemetryTopic(MqttTopics.DEVICE_TELEMETRY_TOPIC);
  347 + mqttDeviceProfileTransportConfiguration.setDeviceTelemetryTopic(MqttTopics.DEVICE_ATTRIBUTES_TOPIC);
  348 + mqttDeviceProfileTransportConfiguration.setTransportPayloadTypeConfiguration(transportPayloadTypeConfiguration);
  349 + return mqttDeviceProfileTransportConfiguration;
  350 + }
  351 +
  352 + protected ProtoTransportPayloadConfiguration createProtoTransportPayloadConfiguration(String deviceAttributesProtoSchema, String deviceTelemetryProtoSchema) {
  353 + ProtoTransportPayloadConfiguration protoTransportPayloadConfiguration = new ProtoTransportPayloadConfiguration();
  354 + protoTransportPayloadConfiguration.setDeviceAttributesProtoSchema(deviceAttributesProtoSchema);
  355 + protoTransportPayloadConfiguration.setDeviceTelemetryProtoSchema(deviceTelemetryProtoSchema);
  356 + return protoTransportPayloadConfiguration;
  357 + }
  358 +
  359 +
350 360 protected ResultActions doGet(String urlTemplate, Object... urlVariables) throws Exception {
351 361 MockHttpServletRequestBuilder getRequest = get(urlTemplate, urlVariables);
352 362 setJwtToken(getRequest);
... ...