Commit 9829dd17cc3fc1489bfb405a25367d4a3e72629c

Authored by Viacheslav Kukhtyn
2 parents c03356e9 bb643df2

Merge branch 'master' into feature/log-telemetry-updated

Showing 46 changed files with 2022 additions and 1609 deletions

Too many changes to show.

To preserve performance only 46 of 352 files are displayed.

@@ -33,3 +33,5 @@ pom.xml.versionsBackup @@ -33,3 +33,5 @@ pom.xml.versionsBackup
33 **/.env 33 **/.env
34 .instance_id 34 .instance_id
35 rebuild-docker.sh 35 rebuild-docker.sh
  36 +.run/**
  37 +.run
@@ -20,7 +20,7 @@ @@ -20,7 +20,7 @@
20 <modelVersion>4.0.0</modelVersion> 20 <modelVersion>4.0.0</modelVersion>
21 <parent> 21 <parent>
22 <groupId>org.thingsboard</groupId> 22 <groupId>org.thingsboard</groupId>
23 - <version>3.2.0-SNAPSHOT</version> 23 + <version>3.2.1-SNAPSHOT</version>
24 <artifactId>thingsboard</artifactId> 24 <artifactId>thingsboard</artifactId>
25 </parent> 25 </parent>
26 <artifactId>application</artifactId> 26 <artifactId>application</artifactId>
@@ -146,10 +146,6 @@ @@ -146,10 +146,6 @@
146 <artifactId>spring-boot-starter-websocket</artifactId> 146 <artifactId>spring-boot-starter-websocket</artifactId>
147 </dependency> 147 </dependency>
148 <dependency> 148 <dependency>
149 - <groupId>org.springframework.cloud</groupId>  
150 - <artifactId>spring-cloud-starter-oauth2</artifactId>  
151 - </dependency>  
152 - <dependency>  
153 <groupId>org.springframework.security</groupId> 149 <groupId>org.springframework.security</groupId>
154 <artifactId>spring-security-oauth2-client</artifactId> 150 <artifactId>spring-security-oauth2-client</artifactId>
155 </dependency> 151 </dependency>
@@ -198,6 +194,14 @@ @@ -198,6 +194,14 @@
198 <artifactId>javax.mail</artifactId> 194 <artifactId>javax.mail</artifactId>
199 </dependency> 195 </dependency>
200 <dependency> 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 <groupId>org.apache.curator</groupId> 205 <groupId>org.apache.curator</groupId>
202 <artifactId>curator-recipes</artifactId> 206 <artifactId>curator-recipes</artifactId>
203 </dependency> 207 </dependency>
@@ -955,7 +955,7 @@ @@ -955,7 +955,7 @@
955 }, 955 },
956 "methodName": "gateway_restart", 956 "methodName": "gateway_restart",
957 "methodParams": "{}", 957 "methodParams": "{}",
958 - "buttonText": "gateway restart" 958 + "buttonText": "GATEWAY RESTART"
959 }, 959 },
960 "title": "New RPC Button", 960 "title": "New RPC Button",
961 "dropShadow": true, 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 -}  
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 -}  
@@ -147,9 +147,9 @@ @@ -147,9 +147,9 @@
147 "name": "Add", 147 "name": "Add",
148 "icon": "add", 148 "icon": "add",
149 "type": "customPretty", 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 "customCss": ".add-entity-form{\n width: 300px;\n}\n", 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 "customResources": [], 153 "customResources": [],
154 "id": "8ab5a518-67d2-b6a2-956d-81fd512294b2" 154 "id": "8ab5a518-67d2-b6a2-956d-81fd512294b2"
155 } 155 }
@@ -167,9 +167,9 @@ @@ -167,9 +167,9 @@
167 "name": "Edit", 167 "name": "Edit",
168 "icon": "edit", 168 "icon": "edit",
169 "type": "customPretty", 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 "customCss": ".edit-entity-form{\n width: 300px;\n}", 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 "customResources": [], 173 "customResources": [],
174 "id": "7506576f-87ba-d3a0-88fb-e304d451776d" 174 "id": "7506576f-87ba-d3a0-88fb-e304d451776d"
175 }, 175 },
@@ -219,8 +219,8 @@ @@ -219,8 +219,8 @@
219 "defaultPageSize": 10, 219 "defaultPageSize": 10,
220 "defaultSortOrder": "-createdTime", 220 "defaultSortOrder": "-createdTime",
221 "enableSelectColumnDisplay": false, 221 "enableSelectColumnDisplay": false,
222 - "enableStatusFilter": true,  
223 - "alarmsTitle": "Alarms" 222 + "alarmsTitle": "Alarms",
  223 + "enableFilter": true
224 }, 224 },
225 "title": "New Alarms table", 225 "title": "New Alarms table",
226 "dropShadow": true, 226 "dropShadow": true,
@@ -234,6 +234,9 @@ @@ -234,6 +234,9 @@
234 "showLegend": false, 234 "showLegend": false,
235 "alarmSource": { 235 "alarmSource": {
236 "type": "entity", 236 "type": "entity",
  237 + "name": "alarms",
  238 + "entityAliasId": "68a058e1-fdda-8482-715b-3ae4a488568e",
  239 + "filterId": null,
237 "dataKeys": [ 240 "dataKeys": [
238 { 241 {
239 "name": "createdTime", 242 "name": "createdTime",
@@ -275,9 +278,7 @@ @@ -275,9 +278,7 @@
275 "settings": {}, 278 "settings": {},
276 "_hash": 0.7977920750136249 279 "_hash": 0.7977920750136249
277 } 280 }
278 - ],  
279 - "entityAliasId": "ce27a9d0-93bf-b7a4-054d-d0369a8cf813",  
280 - "name": "alarms" 281 + ]
281 }, 282 },
282 "alarmSearchStatus": "ANY", 283 "alarmSearchStatus": "ANY",
283 "alarmsPollingInterval": 5, 284 "alarmsPollingInterval": 5,
@@ -549,7 +550,7 @@ @@ -549,7 +550,7 @@
549 "type": "entity", 550 "type": "entity",
550 "dataKeys": [ 551 "dataKeys": [
551 { 552 {
552 - "name": "alarmTemperature", 553 + "name": "temperatureAlarmFlag",
553 "type": "attribute", 554 "type": "attribute",
554 "label": "High temperature alarm", 555 "label": "High temperature alarm",
555 "color": "#4caf50", 556 "color": "#4caf50",
@@ -564,7 +565,7 @@ @@ -564,7 +565,7 @@
564 "_hash": 0.8725278440159361 565 "_hash": 0.8725278440159361
565 }, 566 },
566 { 567 {
567 - "name": "thresholdTemperature", 568 + "name": "temperatureAlarmThreshold",
568 "type": "attribute", 569 "type": "attribute",
569 "label": "High temperature threshold, °C", 570 "label": "High temperature threshold, °C",
570 "color": "#f44336", 571 "color": "#f44336",
@@ -575,12 +576,12 @@ @@ -575,12 +576,12 @@
575 "isEditable": "editable", 576 "isEditable": "editable",
576 "dataKeyHidden": false, 577 "dataKeyHidden": false,
577 "step": 1, 578 "step": 1,
578 - "disabledOnDataKey": "alarmTemperature" 579 + "disabledOnDataKey": "temperatureAlarmFlag"
579 }, 580 },
580 "_hash": 0.7316078472857874 581 "_hash": 0.7316078472857874
581 }, 582 },
582 { 583 {
583 - "name": "alarmHumidity", 584 + "name": "humidityAlarmFlag",
584 "type": "attribute", 585 "type": "attribute",
585 "label": "Low humidity alarm", 586 "label": "Low humidity alarm",
586 "color": "#ffc107", 587 "color": "#ffc107",
@@ -595,7 +596,7 @@ @@ -595,7 +596,7 @@
595 "_hash": 0.5339673667431057 596 "_hash": 0.5339673667431057
596 }, 597 },
597 { 598 {
598 - "name": "thresholdHumidity", 599 + "name": "humidityAlarmThreshold",
599 "type": "attribute", 600 "type": "attribute",
600 "label": "Low humidity threshold, %", 601 "label": "Low humidity threshold, %",
601 "color": "#607d8b", 602 "color": "#607d8b",
@@ -606,7 +607,7 @@ @@ -606,7 +607,7 @@
606 "isEditable": "editable", 607 "isEditable": "editable",
607 "dataKeyHidden": false, 608 "dataKeyHidden": false,
608 "step": 1, 609 "step": 1,
609 - "disabledOnDataKey": "alarmHumidity" 610 + "disabledOnDataKey": "humidityAlarmFlag"
610 }, 611 },
611 "_hash": 0.2687091190358901 612 "_hash": 0.2687091190358901
612 } 613 }
@@ -1031,7 +1032,8 @@ @@ -1031,7 +1032,8 @@
1031 "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 "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 "useLabelFunction": true, 1033 "useLabelFunction": true,
1033 "provider": "openstreet-map", 1034 "provider": "openstreet-map",
1034 - "draggableMarker": true 1035 + "draggableMarker": true,
  1036 + "editablePolygon": true
1035 }, 1037 },
1036 "title": "New Markers Placement - OpenStreetMap", 1038 "title": "New Markers Placement - OpenStreetMap",
1037 "dropShadow": true, 1039 "dropShadow": true,
@@ -1062,61 +1064,6 @@ @@ -1062,61 +1064,6 @@
1062 "displayTimewindow": true 1064 "displayTimewindow": true
1063 }, 1065 },
1064 "id": "0a430429-9078-9ae6-2b67-e4a15a2bf8bf" 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 "states": { 1069 "states": {
@@ -1215,12 +1162,6 @@ @@ -1215,12 +1162,6 @@
1215 "sizeY": 6, 1162 "sizeY": 6,
1216 "row": 6, 1163 "row": 6,
1217 "col": 0 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 "gridSettings": { 1167 "gridSettings": {
@@ -1257,16 +1198,6 @@ @@ -1257,16 +1198,6 @@
1257 "stateEntityParamName": null, 1198 "stateEntityParamName": null,
1258 "defaultStateEntity": null 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 "timewindow": { 1203 "timewindow": {
@@ -1301,7 +1232,8 @@ @@ -1301,7 +1232,8 @@
1301 "showDashboardTimewindow": true, 1232 "showDashboardTimewindow": true,
1302 "showDashboardExport": true, 1233 "showDashboardExport": true,
1303 "toolbarAlwaysOpen": true 1234 "toolbarAlwaysOpen": true
1304 - } 1235 + },
  1236 + "filters": {}
1305 }, 1237 },
1306 "name": "Thermostats" 1238 "name": "Thermostats"
1307 } 1239 }
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 -}  
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 -}  
@@ -18,7 +18,7 @@ @@ -18,7 +18,7 @@
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", 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 "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}", 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 "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}", 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,8 +34,8 @@
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", 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 "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}", 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 "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}", 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 -}  
  41 +}
@@ -33,7 +33,7 @@ @@ -33,7 +33,7 @@
33 "templateCss": ".tb-toast {\n min-width: 0;\n font-size: 14px !important;\n}", 33 "templateCss": ".tb-toast {\n min-width: 0;\n font-size: 14px !important;\n}",
34 "controllerScript": "self.onInit = function() {\r\n}\r\n\r\nself.onDataUpdated = function() {\r\n self.ctx.$scope.multipleInputWidget.onDataUpdated();\r\n}\r\n", 34 "controllerScript": "self.onInit = function() {\r\n}\r\n\r\nself.onDataUpdated = function() {\r\n self.ctx.$scope.multipleInputWidget.onDataUpdated();\r\n}\r\n",
35 "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"MultipleInput\",\n \"properties\": {\n \"widgetTitle\": {\n \"title\": \"Widget title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"showActionButtons\":{\n \"title\":\"Show action buttons\",\n \"type\":\"boolean\",\n \"default\": true\n },\n \"updateAllValues\": {\n \"title\":\"Update all values, not only modified\",\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}", 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 "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\":{}}" 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 },
@@ -32,6 +32,8 @@ import org.springframework.data.redis.core.RedisTemplate; @@ -32,6 +32,8 @@ import org.springframework.data.redis.core.RedisTemplate;
32 import org.springframework.scheduling.annotation.Scheduled; 32 import org.springframework.scheduling.annotation.Scheduled;
33 import org.springframework.stereotype.Component; 33 import org.springframework.stereotype.Component;
34 import org.thingsboard.rule.engine.api.MailService; 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 import org.thingsboard.server.actors.service.ActorService; 37 import org.thingsboard.server.actors.service.ActorService;
36 import org.thingsboard.server.actors.tenant.DebugTbRateLimits; 38 import org.thingsboard.server.actors.tenant.DebugTbRateLimits;
37 import org.thingsboard.server.common.data.DataConstants; 39 import org.thingsboard.server.common.data.DataConstants;
@@ -80,6 +82,7 @@ import org.thingsboard.server.service.rpc.TbRuleEngineDeviceRpcService; @@ -80,6 +82,7 @@ import org.thingsboard.server.service.rpc.TbRuleEngineDeviceRpcService;
80 import org.thingsboard.server.service.script.JsExecutorService; 82 import org.thingsboard.server.service.script.JsExecutorService;
81 import org.thingsboard.server.service.script.JsInvokeService; 83 import org.thingsboard.server.service.script.JsInvokeService;
82 import org.thingsboard.server.service.session.DeviceSessionCacheService; 84 import org.thingsboard.server.service.session.DeviceSessionCacheService;
  85 +import org.thingsboard.server.service.sms.SmsExecutorService;
83 import org.thingsboard.server.service.state.DeviceStateService; 86 import org.thingsboard.server.service.state.DeviceStateService;
84 import org.thingsboard.server.service.telemetry.AlarmSubscriptionService; 87 import org.thingsboard.server.service.telemetry.AlarmSubscriptionService;
85 import org.thingsboard.server.service.telemetry.TelemetrySubscriptionService; 88 import org.thingsboard.server.service.telemetry.TelemetrySubscriptionService;
@@ -230,6 +233,10 @@ public class ActorSystemContext { @@ -230,6 +233,10 @@ public class ActorSystemContext {
230 233
231 @Autowired 234 @Autowired
232 @Getter 235 @Getter
  236 + private SmsExecutorService smsExecutor;
  237 +
  238 + @Autowired
  239 + @Getter
233 private DbCallbackExecutorService dbCallbackExecutor; 240 private DbCallbackExecutorService dbCallbackExecutor;
234 241
235 @Autowired 242 @Autowired
@@ -246,6 +253,14 @@ public class ActorSystemContext { @@ -246,6 +253,14 @@ public class ActorSystemContext {
246 253
247 @Autowired 254 @Autowired
248 @Getter 255 @Getter
  256 + private SmsService smsService;
  257 +
  258 + @Autowired
  259 + @Getter
  260 + private SmsSenderFactory smsSenderFactory;
  261 +
  262 + @Autowired
  263 + @Getter
249 private ClaimDevicesService claimDevicesService; 264 private ClaimDevicesService claimDevicesService;
250 265
251 @Autowired 266 @Autowired
@@ -325,6 +340,10 @@ public class ActorSystemContext { @@ -325,6 +340,10 @@ public class ActorSystemContext {
325 @Getter 340 @Getter
326 private boolean allowSystemMailService; 341 private boolean allowSystemMailService;
327 342
  343 + @Value("${actors.rule.allow_system_sms_service}")
  344 + @Getter
  345 + private boolean allowSystemSmsService;
  346 +
328 @Value("${transport.sessions.inactivity_timeout}") 347 @Value("${transport.sessions.inactivity_timeout}")
329 @Getter 348 @Getter
330 private long sessionInactivityTimeout; 349 private long sessionInactivityTimeout;
@@ -28,14 +28,18 @@ import org.thingsboard.rule.engine.api.RuleEngineDeviceProfileCache; @@ -28,14 +28,18 @@ import org.thingsboard.rule.engine.api.RuleEngineDeviceProfileCache;
28 import org.thingsboard.rule.engine.api.RuleEngineRpcService; 28 import org.thingsboard.rule.engine.api.RuleEngineRpcService;
29 import org.thingsboard.rule.engine.api.RuleEngineTelemetryService; 29 import org.thingsboard.rule.engine.api.RuleEngineTelemetryService;
30 import org.thingsboard.rule.engine.api.ScriptEngine; 30 import org.thingsboard.rule.engine.api.ScriptEngine;
  31 +import org.thingsboard.rule.engine.api.SmsService;
31 import org.thingsboard.rule.engine.api.TbContext; 32 import org.thingsboard.rule.engine.api.TbContext;
32 import org.thingsboard.rule.engine.api.TbRelationTypes; 33 import org.thingsboard.rule.engine.api.TbRelationTypes;
  34 +import org.thingsboard.rule.engine.api.sms.SmsSenderFactory;
33 import org.thingsboard.server.actors.ActorSystemContext; 35 import org.thingsboard.server.actors.ActorSystemContext;
34 import org.thingsboard.server.actors.TbActorRef; 36 import org.thingsboard.server.actors.TbActorRef;
  37 +import org.thingsboard.server.common.data.ApiUsageRecordKey;
35 import org.thingsboard.server.common.data.Customer; 38 import org.thingsboard.server.common.data.Customer;
36 import org.thingsboard.server.common.data.DataConstants; 39 import org.thingsboard.server.common.data.DataConstants;
37 import org.thingsboard.server.common.data.Device; 40 import org.thingsboard.server.common.data.Device;
38 import org.thingsboard.server.common.data.DeviceProfile; 41 import org.thingsboard.server.common.data.DeviceProfile;
  42 +import org.thingsboard.server.common.data.TenantProfile;
39 import org.thingsboard.server.common.data.alarm.Alarm; 43 import org.thingsboard.server.common.data.alarm.Alarm;
40 import org.thingsboard.server.common.data.asset.Asset; 44 import org.thingsboard.server.common.data.asset.Asset;
41 import org.thingsboard.server.common.data.id.DeviceId; 45 import org.thingsboard.server.common.data.id.DeviceId;
@@ -240,8 +244,18 @@ class DefaultTbContext implements TbContext { @@ -240,8 +244,18 @@ class DefaultTbContext implements TbContext {
240 if (nodeCtx.getSelf().isDebugMode()) { 244 if (nodeCtx.getSelf().isDebugMode()) {
241 mainCtx.persistDebugOutput(nodeCtx.getTenantId(), nodeCtx.getSelf().getId(), msg, TbRelationTypes.FAILURE, th); 245 mainCtx.persistDebugOutput(nodeCtx.getTenantId(), nodeCtx.getSelf().getId(), msg, TbRelationTypes.FAILURE, th);
242 } 246 }
  247 + String failureMessage;
  248 + if (th != null) {
  249 + if (!StringUtils.isEmpty(th.getMessage())) {
  250 + failureMessage = th.getMessage();
  251 + } else {
  252 + failureMessage = th.getClass().getSimpleName();
  253 + }
  254 + } else {
  255 + failureMessage = null;
  256 + }
243 nodeCtx.getChainActor().tell(new RuleNodeToRuleChainTellNextMsg(nodeCtx.getSelf().getId(), Collections.singleton(TbRelationTypes.FAILURE), 257 nodeCtx.getChainActor().tell(new RuleNodeToRuleChainTellNextMsg(nodeCtx.getSelf().getId(), Collections.singleton(TbRelationTypes.FAILURE),
244 - msg, th != null ? th.getMessage() : null)); 258 + msg, failureMessage));
245 } 259 }
246 260
247 public void updateSelf(RuleNode self) { 261 public void updateSelf(RuleNode self) {
@@ -303,6 +317,11 @@ class DefaultTbContext implements TbContext { @@ -303,6 +317,11 @@ class DefaultTbContext implements TbContext {
303 } 317 }
304 318
305 @Override 319 @Override
  320 + public ListeningExecutor getSmsExecutor() {
  321 + return mainCtx.getSmsExecutor();
  322 + }
  323 +
  324 + @Override
306 public ListeningExecutor getDbCallbackExecutor() { 325 public ListeningExecutor getDbCallbackExecutor() {
307 return mainCtx.getDbCallbackExecutor(); 326 return mainCtx.getDbCallbackExecutor();
308 } 327 }
@@ -428,6 +447,20 @@ class DefaultTbContext implements TbContext { @@ -428,6 +447,20 @@ class DefaultTbContext implements TbContext {
428 } 447 }
429 448
430 @Override 449 @Override
  450 + public SmsService getSmsService() {
  451 + if (mainCtx.isAllowSystemSmsService()) {
  452 + return mainCtx.getSmsService();
  453 + } else {
  454 + throw new RuntimeException("Access to System SMS Service is forbidden!");
  455 + }
  456 + }
  457 +
  458 + @Override
  459 + public SmsSenderFactory getSmsSenderFactory() {
  460 + return mainCtx.getSmsSenderFactory();
  461 + }
  462 +
  463 + @Override
431 public RuleEngineRpcService getRpcService() { 464 public RuleEngineRpcService getRpcService() {
432 return mainCtx.getTbRuleEngineDeviceRpcService(); 465 return mainCtx.getTbRuleEngineDeviceRpcService();
433 } 466 }
@@ -489,13 +522,24 @@ class DefaultTbContext implements TbContext { @@ -489,13 +522,24 @@ class DefaultTbContext implements TbContext {
489 } 522 }
490 523
491 @Override 524 @Override
  525 + public void addTenantProfileListener(Consumer<TenantProfile> listener) {
  526 + mainCtx.getTenantProfileCache().addListener(getTenantId(), getSelfId(), listener);
  527 + }
  528 +
  529 + @Override
492 public void addDeviceProfileListeners(Consumer<DeviceProfile> profileListener, BiConsumer<DeviceId, DeviceProfile> deviceListener) { 530 public void addDeviceProfileListeners(Consumer<DeviceProfile> profileListener, BiConsumer<DeviceId, DeviceProfile> deviceListener) {
493 mainCtx.getDeviceProfileCache().addListener(getTenantId(), getSelfId(), profileListener, deviceListener); 531 mainCtx.getDeviceProfileCache().addListener(getTenantId(), getSelfId(), profileListener, deviceListener);
494 } 532 }
495 533
496 @Override 534 @Override
497 - public void removeProfileListener() { 535 + public void removeListeners() {
498 mainCtx.getDeviceProfileCache().removeListener(getTenantId(), getSelfId()); 536 mainCtx.getDeviceProfileCache().removeListener(getTenantId(), getSelfId());
  537 + mainCtx.getTenantProfileCache().removeListener(getTenantId(), getSelfId());
  538 + }
  539 +
  540 + @Override
  541 + public TenantProfile getTenantProfile() {
  542 + return mainCtx.getTenantProfileCache().get(getTenantId());
499 } 543 }
500 544
501 private TbMsgMetaData getActionMetaData(RuleNodeId ruleNodeId) { 545 private TbMsgMetaData getActionMetaData(RuleNodeId ruleNodeId) {
@@ -56,6 +56,7 @@ import java.util.HashMap; @@ -56,6 +56,7 @@ import java.util.HashMap;
56 import java.util.List; 56 import java.util.List;
57 import java.util.Map; 57 import java.util.Map;
58 import java.util.Set; 58 import java.util.Set;
  59 +import java.util.UUID;
59 import java.util.stream.Collectors; 60 import java.util.stream.Collectors;
60 61
61 /** 62 /**
@@ -288,10 +289,10 @@ public class RuleChainActorMessageProcessor extends ComponentMsgProcessor<RuleCh @@ -288,10 +289,10 @@ public class RuleChainActorMessageProcessor extends ComponentMsgProcessor<RuleCh
288 private void putToQueue(TopicPartitionInfo tpi, TbMsg msg, TbQueueCallback callbackWrapper, EntityId target) { 289 private void putToQueue(TopicPartitionInfo tpi, TbMsg msg, TbQueueCallback callbackWrapper, EntityId target) {
289 switch (target.getEntityType()) { 290 switch (target.getEntityType()) {
290 case RULE_NODE: 291 case RULE_NODE:
291 - putToQueue(tpi, msg.copyWithRuleNodeId(entityId, new RuleNodeId(target.getId())), callbackWrapper); 292 + putToQueue(tpi, msg.copyWithRuleNodeId(entityId, new RuleNodeId(target.getId()), UUID.randomUUID()), callbackWrapper);
292 break; 293 break;
293 case RULE_CHAIN: 294 case RULE_CHAIN:
294 - putToQueue(tpi, msg.copyWithRuleChainId(new RuleChainId(target.getId())), callbackWrapper); 295 + putToQueue(tpi, msg.copyWithRuleChainId(new RuleChainId(target.getId()), UUID.randomUUID()), callbackWrapper);
295 break; 296 break;
296 } 297 }
297 } 298 }
@@ -25,6 +25,8 @@ import org.springframework.web.bind.annotation.RequestMethod; @@ -25,6 +25,8 @@ import org.springframework.web.bind.annotation.RequestMethod;
25 import org.springframework.web.bind.annotation.ResponseBody; 25 import org.springframework.web.bind.annotation.ResponseBody;
26 import org.springframework.web.bind.annotation.RestController; 26 import org.springframework.web.bind.annotation.RestController;
27 import org.thingsboard.rule.engine.api.MailService; 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 import org.thingsboard.server.common.data.AdminSettings; 30 import org.thingsboard.server.common.data.AdminSettings;
29 import org.thingsboard.server.common.data.UpdateMessage; 31 import org.thingsboard.server.common.data.UpdateMessage;
30 import org.thingsboard.server.common.data.exception.ThingsboardException; 32 import org.thingsboard.server.common.data.exception.ThingsboardException;
@@ -46,6 +48,9 @@ public class AdminController extends BaseController { @@ -46,6 +48,9 @@ public class AdminController extends BaseController {
46 private MailService mailService; 48 private MailService mailService;
47 49
48 @Autowired 50 @Autowired
  51 + private SmsService smsService;
  52 +
  53 + @Autowired
49 private AdminSettingsService adminSettingsService; 54 private AdminSettingsService adminSettingsService;
50 55
51 @Autowired 56 @Autowired
@@ -80,6 +85,8 @@ public class AdminController extends BaseController { @@ -80,6 +85,8 @@ public class AdminController extends BaseController {
80 if (adminSettings.getKey().equals("mail")) { 85 if (adminSettings.getKey().equals("mail")) {
81 mailService.updateMailConfiguration(); 86 mailService.updateMailConfiguration();
82 ((ObjectNode) adminSettings.getJsonValue()).put("password", ""); 87 ((ObjectNode) adminSettings.getJsonValue()).put("password", "");
  88 + } else if (adminSettings.getKey().equals("sms")) {
  89 + smsService.updateSmsConfiguration();
83 } 90 }
84 return adminSettings; 91 return adminSettings;
85 } catch (Exception e) { 92 } catch (Exception e) {
@@ -128,6 +135,17 @@ public class AdminController extends BaseController { @@ -128,6 +135,17 @@ public class AdminController extends BaseController {
128 } 135 }
129 136
130 @PreAuthorize("hasAuthority('SYS_ADMIN')") 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 @RequestMapping(value = "/updates", method = RequestMethod.GET) 149 @RequestMapping(value = "/updates", method = RequestMethod.GET)
132 @ResponseBody 150 @ResponseBody
133 public UpdateMessage checkUpdates() throws ThingsboardException { 151 public UpdateMessage checkUpdates() throws ThingsboardException {
@@ -33,6 +33,7 @@ import org.thingsboard.server.common.data.asset.Asset; @@ -33,6 +33,7 @@ import org.thingsboard.server.common.data.asset.Asset;
33 import org.thingsboard.server.common.data.asset.AssetInfo; 33 import org.thingsboard.server.common.data.asset.AssetInfo;
34 import org.thingsboard.server.common.data.asset.AssetSearchQuery; 34 import org.thingsboard.server.common.data.asset.AssetSearchQuery;
35 import org.thingsboard.server.common.data.audit.ActionType; 35 import org.thingsboard.server.common.data.audit.ActionType;
  36 +import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
36 import org.thingsboard.server.common.data.exception.ThingsboardException; 37 import org.thingsboard.server.common.data.exception.ThingsboardException;
37 import org.thingsboard.server.common.data.id.AssetId; 38 import org.thingsboard.server.common.data.id.AssetId;
38 import org.thingsboard.server.common.data.id.CustomerId; 39 import org.thingsboard.server.common.data.id.CustomerId;
@@ -51,6 +52,8 @@ import java.util.ArrayList; @@ -51,6 +52,8 @@ import java.util.ArrayList;
51 import java.util.List; 52 import java.util.List;
52 import java.util.stream.Collectors; 53 import java.util.stream.Collectors;
53 54
  55 +import static org.thingsboard.server.dao.asset.BaseAssetService.TB_SERVICE_QUEUE;
  56 +
54 @RestController 57 @RestController
55 @TbCoreComponent 58 @TbCoreComponent
56 @RequestMapping("/api") 59 @RequestMapping("/api")
@@ -89,6 +92,10 @@ public class AssetController extends BaseController { @@ -89,6 +92,10 @@ public class AssetController extends BaseController {
89 @ResponseBody 92 @ResponseBody
90 public Asset saveAsset(@RequestBody Asset asset) throws ThingsboardException { 93 public Asset saveAsset(@RequestBody Asset asset) throws ThingsboardException {
91 try { 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 asset.setTenantId(getCurrentUser().getTenantId()); 99 asset.setTenantId(getCurrentUser().getTenantId());
93 100
94 checkEntity(asset.getId(), asset, Resource.ASSET); 101 checkEntity(asset.getId(), asset, Resource.ASSET);
@@ -118,6 +118,7 @@ public class DeviceController extends BaseController { @@ -118,6 +118,7 @@ public class DeviceController extends BaseController {
118 118
119 Device savedDevice = checkNotNull(deviceService.saveDeviceWithAccessToken(device, accessToken)); 119 Device savedDevice = checkNotNull(deviceService.saveDeviceWithAccessToken(device, accessToken));
120 120
  121 + tbClusterService.onDeviceChange(savedDevice, null);
121 tbClusterService.pushMsgToCore(new DeviceNameOrTypeUpdateMsg(savedDevice.getTenantId(), 122 tbClusterService.pushMsgToCore(new DeviceNameOrTypeUpdateMsg(savedDevice.getTenantId(),
122 savedDevice.getId(), savedDevice.getName(), savedDevice.getType()), null); 123 savedDevice.getId(), savedDevice.getName(), savedDevice.getType()), null);
123 tbClusterService.onEntityStateChange(savedDevice.getTenantId(), savedDevice.getId(), 124 tbClusterService.onEntityStateChange(savedDevice.getTenantId(), savedDevice.getId(),
@@ -150,6 +151,9 @@ public class DeviceController extends BaseController { @@ -150,6 +151,9 @@ public class DeviceController extends BaseController {
150 Device device = checkDeviceId(deviceId, Operation.DELETE); 151 Device device = checkDeviceId(deviceId, Operation.DELETE);
151 deviceService.deleteDevice(getCurrentUser().getTenantId(), deviceId); 152 deviceService.deleteDevice(getCurrentUser().getTenantId(), deviceId);
152 153
  154 + tbClusterService.onDeviceDeleted(device, null);
  155 + tbClusterService.onEntityStateChange(device.getTenantId(), deviceId, ComponentLifecycleEvent.DELETED);
  156 +
153 logEntityAction(deviceId, device, 157 logEntityAction(deviceId, device,
154 device.getCustomerId(), 158 device.getCustomerId(),
155 ActionType.DELETED, null, strDeviceId); 159 ActionType.DELETED, null, strDeviceId);
@@ -16,6 +16,8 @@ @@ -16,6 +16,8 @@
16 package org.thingsboard.server.controller; 16 package org.thingsboard.server.controller;
17 17
18 import lombok.extern.slf4j.Slf4j; 18 import lombok.extern.slf4j.Slf4j;
  19 +import org.apache.commons.lang3.StringUtils;
  20 +import org.springframework.beans.factory.annotation.Autowired;
19 import org.springframework.http.HttpStatus; 21 import org.springframework.http.HttpStatus;
20 import org.springframework.security.access.prepost.PreAuthorize; 22 import org.springframework.security.access.prepost.PreAuthorize;
21 import org.springframework.web.bind.annotation.PathVariable; 23 import org.springframework.web.bind.annotation.PathVariable;
@@ -35,21 +37,30 @@ import org.thingsboard.server.common.data.id.DeviceProfileId; @@ -35,21 +37,30 @@ import org.thingsboard.server.common.data.id.DeviceProfileId;
35 import org.thingsboard.server.common.data.page.PageData; 37 import org.thingsboard.server.common.data.page.PageData;
36 import org.thingsboard.server.common.data.page.PageLink; 38 import org.thingsboard.server.common.data.page.PageLink;
37 import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; 39 import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
  40 +import org.thingsboard.server.dao.timeseries.TimeseriesService;
38 import org.thingsboard.server.queue.util.TbCoreComponent; 41 import org.thingsboard.server.queue.util.TbCoreComponent;
39 import org.thingsboard.server.service.security.permission.Operation; 42 import org.thingsboard.server.service.security.permission.Operation;
40 import org.thingsboard.server.service.security.permission.Resource; 43 import org.thingsboard.server.service.security.permission.Resource;
41 44
  45 +import java.util.List;
  46 +import java.util.UUID;
  47 +
42 @RestController 48 @RestController
43 @TbCoreComponent 49 @TbCoreComponent
44 @RequestMapping("/api") 50 @RequestMapping("/api")
45 @Slf4j 51 @Slf4j
46 public class DeviceProfileController extends BaseController { 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 @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") 59 @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')")
49 @RequestMapping(value = "/deviceProfile/{deviceProfileId}", method = RequestMethod.GET) 60 @RequestMapping(value = "/deviceProfile/{deviceProfileId}", method = RequestMethod.GET)
50 @ResponseBody 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 try { 64 try {
54 DeviceProfileId deviceProfileId = new DeviceProfileId(toUUID(strDeviceProfileId)); 65 DeviceProfileId deviceProfileId = new DeviceProfileId(toUUID(strDeviceProfileId));
55 return checkDeviceProfileId(deviceProfileId, Operation.READ); 66 return checkDeviceProfileId(deviceProfileId, Operation.READ);
@@ -61,8 +72,8 @@ public class DeviceProfileController extends BaseController { @@ -61,8 +72,8 @@ public class DeviceProfileController extends BaseController {
61 @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") 72 @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
62 @RequestMapping(value = "/deviceProfileInfo/{deviceProfileId}", method = RequestMethod.GET) 73 @RequestMapping(value = "/deviceProfileInfo/{deviceProfileId}", method = RequestMethod.GET)
63 @ResponseBody 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 try { 77 try {
67 DeviceProfileId deviceProfileId = new DeviceProfileId(toUUID(strDeviceProfileId)); 78 DeviceProfileId deviceProfileId = new DeviceProfileId(toUUID(strDeviceProfileId));
68 return checkNotNull(deviceProfileService.findDeviceProfileInfoById(getTenantId(), deviceProfileId)); 79 return checkNotNull(deviceProfileService.findDeviceProfileInfoById(getTenantId(), deviceProfileId));
@@ -82,6 +93,46 @@ public class DeviceProfileController extends BaseController { @@ -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 @PreAuthorize("hasAuthority('TENANT_ADMIN')") 136 @PreAuthorize("hasAuthority('TENANT_ADMIN')")
86 @RequestMapping(value = "/deviceProfile", method = RequestMethod.POST) 137 @RequestMapping(value = "/deviceProfile", method = RequestMethod.POST)
87 @ResponseBody 138 @ResponseBody
@@ -113,8 +164,8 @@ public class DeviceProfileController extends BaseController { @@ -113,8 +164,8 @@ public class DeviceProfileController extends BaseController {
113 @PreAuthorize("hasAuthority('TENANT_ADMIN')") 164 @PreAuthorize("hasAuthority('TENANT_ADMIN')")
114 @RequestMapping(value = "/deviceProfile/{deviceProfileId}", method = RequestMethod.DELETE) 165 @RequestMapping(value = "/deviceProfile/{deviceProfileId}", method = RequestMethod.DELETE)
115 @ResponseStatus(value = HttpStatus.OK) 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 try { 169 try {
119 DeviceProfileId deviceProfileId = new DeviceProfileId(toUUID(strDeviceProfileId)); 170 DeviceProfileId deviceProfileId = new DeviceProfileId(toUUID(strDeviceProfileId));
120 DeviceProfile deviceProfile = checkDeviceProfileId(deviceProfileId, Operation.DELETE); 171 DeviceProfile deviceProfile = checkDeviceProfileId(deviceProfileId, Operation.DELETE);
@@ -139,8 +190,8 @@ public class DeviceProfileController extends BaseController { @@ -139,8 +190,8 @@ public class DeviceProfileController extends BaseController {
139 @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") 190 @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')")
140 @RequestMapping(value = "/deviceProfile/{deviceProfileId}/default", method = RequestMethod.POST) 191 @RequestMapping(value = "/deviceProfile/{deviceProfileId}/default", method = RequestMethod.POST)
141 @ResponseBody 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 try { 195 try {
145 DeviceProfileId deviceProfileId = new DeviceProfileId(toUUID(strDeviceProfileId)); 196 DeviceProfileId deviceProfileId = new DeviceProfileId(toUUID(strDeviceProfileId));
146 DeviceProfile deviceProfile = checkDeviceProfileId(deviceProfileId, Operation.WRITE); 197 DeviceProfile deviceProfile = checkDeviceProfileId(deviceProfileId, Operation.WRITE);
@@ -45,6 +45,7 @@ import org.thingsboard.common.util.ThingsBoardThreadFactory; @@ -45,6 +45,7 @@ import org.thingsboard.common.util.ThingsBoardThreadFactory;
45 import org.thingsboard.rule.engine.api.msg.DeviceAttributesEventNotificationMsg; 45 import org.thingsboard.rule.engine.api.msg.DeviceAttributesEventNotificationMsg;
46 import org.thingsboard.server.common.data.DataConstants; 46 import org.thingsboard.server.common.data.DataConstants;
47 import org.thingsboard.server.common.data.EntityType; 47 import org.thingsboard.server.common.data.EntityType;
  48 +import org.thingsboard.server.common.data.TenantProfile;
48 import org.thingsboard.server.common.data.audit.ActionType; 49 import org.thingsboard.server.common.data.audit.ActionType;
49 import org.thingsboard.server.common.data.exception.ThingsboardException; 50 import org.thingsboard.server.common.data.exception.ThingsboardException;
50 import org.thingsboard.server.common.data.id.DeviceId; 51 import org.thingsboard.server.common.data.id.DeviceId;
@@ -69,6 +70,7 @@ import org.thingsboard.server.common.data.kv.LongDataEntry; @@ -69,6 +70,7 @@ import org.thingsboard.server.common.data.kv.LongDataEntry;
69 import org.thingsboard.server.common.data.kv.ReadTsKvQuery; 70 import org.thingsboard.server.common.data.kv.ReadTsKvQuery;
70 import org.thingsboard.server.common.data.kv.StringDataEntry; 71 import org.thingsboard.server.common.data.kv.StringDataEntry;
71 import org.thingsboard.server.common.data.kv.TsKvEntry; 72 import org.thingsboard.server.common.data.kv.TsKvEntry;
  73 +import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration;
72 import org.thingsboard.server.common.transport.adaptor.JsonConverter; 74 import org.thingsboard.server.common.transport.adaptor.JsonConverter;
73 import org.thingsboard.server.dao.timeseries.TimeseriesService; 75 import org.thingsboard.server.dao.timeseries.TimeseriesService;
74 import org.thingsboard.server.queue.util.TbCoreComponent; 76 import org.thingsboard.server.queue.util.TbCoreComponent;
@@ -93,6 +95,7 @@ import java.util.Map; @@ -93,6 +95,7 @@ import java.util.Map;
93 import java.util.Set; 95 import java.util.Set;
94 import java.util.concurrent.ExecutorService; 96 import java.util.concurrent.ExecutorService;
95 import java.util.concurrent.Executors; 97 import java.util.concurrent.Executors;
  98 +import java.util.concurrent.TimeUnit;
96 import java.util.stream.Collectors; 99 import java.util.stream.Collectors;
97 100
98 /** 101 /**
@@ -205,7 +208,7 @@ public class TelemetryController extends BaseController { @@ -205,7 +208,7 @@ public class TelemetryController extends BaseController {
205 @RequestParam(name = "interval", defaultValue = "0") Long interval, 208 @RequestParam(name = "interval", defaultValue = "0") Long interval,
206 @RequestParam(name = "limit", defaultValue = "100") Integer limit, 209 @RequestParam(name = "limit", defaultValue = "100") Integer limit,
207 @RequestParam(name = "agg", defaultValue = "NONE") String aggStr, 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 @RequestParam(name = "useStrictDataTypes", required = false, defaultValue = "false") Boolean useStrictDataTypes) throws ThingsboardException { 212 @RequestParam(name = "useStrictDataTypes", required = false, defaultValue = "false") Boolean useStrictDataTypes) throws ThingsboardException {
210 return accessValidator.validateEntityAndCallback(getCurrentUser(), Operation.READ_TELEMETRY, entityType, entityIdStr, 213 return accessValidator.validateEntityAndCallback(getCurrentUser(), Operation.READ_TELEMETRY, entityType, entityIdStr,
211 (result, tenantId, entityId) -> { 214 (result, tenantId, entityId) -> {
@@ -392,7 +395,7 @@ public class TelemetryController extends BaseController { @@ -392,7 +395,7 @@ public class TelemetryController extends BaseController {
392 if (attributes.isEmpty()) { 395 if (attributes.isEmpty()) {
393 return getImmediateDeferredResult("No attributes data found in request body!", HttpStatus.BAD_REQUEST); 396 return getImmediateDeferredResult("No attributes data found in request body!", HttpStatus.BAD_REQUEST);
394 } 397 }
395 - for (AttributeKvEntry attributeKvEntry: attributes) { 398 + for (AttributeKvEntry attributeKvEntry : attributes) {
396 if (attributeKvEntry.getKey().isEmpty() || attributeKvEntry.getKey().trim().length() == 0) { 399 if (attributeKvEntry.getKey().isEmpty() || attributeKvEntry.getKey().trim().length() == 0) {
397 return getImmediateDeferredResult("Key cannot be empty or contains only spaces", HttpStatus.BAD_REQUEST); 400 return getImmediateDeferredResult("Key cannot be empty or contains only spaces", HttpStatus.BAD_REQUEST);
398 } 401 }
@@ -440,9 +443,13 @@ public class TelemetryController extends BaseController { @@ -440,9 +443,13 @@ public class TelemetryController extends BaseController {
440 if (entries.isEmpty()) { 443 if (entries.isEmpty()) {
441 return getImmediateDeferredResult("No timeseries data found in request body!", HttpStatus.BAD_REQUEST); 444 return getImmediateDeferredResult("No timeseries data found in request body!", HttpStatus.BAD_REQUEST);
442 } 445 }
443 - SecurityUser user = getCurrentUser();  
444 return accessValidator.validateEntityAndCallback(getCurrentUser(), Operation.WRITE_TELEMETRY, entityIdSrc, (result, tenantId, entityId) -> { 446 return accessValidator.validateEntityAndCallback(getCurrentUser(), Operation.WRITE_TELEMETRY, entityIdSrc, (result, tenantId, entityId) -> {
445 - 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>() {
446 @Override 453 @Override
447 public void onSuccess(@Nullable Void tmp) { 454 public void onSuccess(@Nullable Void tmp) {
448 logTelemetryUpdated(user, entityId, entries, null); 455 logTelemetryUpdated(user, entityId, entries, null);
@@ -186,6 +186,10 @@ public class ThingsboardInstallService { @@ -186,6 +186,10 @@ public class ThingsboardInstallService {
186 systemDataLoaderService.updateSystemWidgets(); 186 systemDataLoaderService.updateSystemWidgets();
187 systemDataLoaderService.createOAuth2Templates(); 187 systemDataLoaderService.createOAuth2Templates();
188 break; 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 default: 193 default:
190 throw new RuntimeException("Unable to upgrade ThingsBoard, unsupported fromVersion: " + upgradeFromVersion); 194 throw new RuntimeException("Unable to upgrade ThingsBoard, unsupported fromVersion: " + upgradeFromVersion);
191 195
@@ -17,21 +17,21 @@ package org.thingsboard.server.service.apiusage; @@ -17,21 +17,21 @@ package org.thingsboard.server.service.apiusage;
17 17
18 import com.google.common.util.concurrent.FutureCallback; 18 import com.google.common.util.concurrent.FutureCallback;
19 import lombok.extern.slf4j.Slf4j; 19 import lombok.extern.slf4j.Slf4j;
  20 +import org.apache.commons.lang3.StringUtils;
20 import org.checkerframework.checker.nullness.qual.Nullable; 21 import org.checkerframework.checker.nullness.qual.Nullable;
21 import org.springframework.beans.factory.annotation.Autowired; 22 import org.springframework.beans.factory.annotation.Autowired;
22 import org.springframework.beans.factory.annotation.Value; 23 import org.springframework.beans.factory.annotation.Value;
23 -import org.springframework.boot.context.event.ApplicationReadyEvent;  
24 import org.springframework.context.annotation.Lazy; 24 import org.springframework.context.annotation.Lazy;
25 -import org.springframework.context.event.EventListener;  
26 -import org.springframework.core.annotation.Order;  
27 -import org.springframework.data.util.Pair;  
28 import org.springframework.stereotype.Service; 25 import org.springframework.stereotype.Service;
  26 +import org.thingsboard.rule.engine.api.MailService;
29 import org.thingsboard.server.common.data.ApiFeature; 27 import org.thingsboard.server.common.data.ApiFeature;
30 import org.thingsboard.server.common.data.ApiUsageRecordKey; 28 import org.thingsboard.server.common.data.ApiUsageRecordKey;
31 import org.thingsboard.server.common.data.ApiUsageState; 29 import org.thingsboard.server.common.data.ApiUsageState;
  30 +import org.thingsboard.server.common.data.ApiUsageStateMailMessage;
32 import org.thingsboard.server.common.data.ApiUsageStateValue; 31 import org.thingsboard.server.common.data.ApiUsageStateValue;
33 import org.thingsboard.server.common.data.Tenant; 32 import org.thingsboard.server.common.data.Tenant;
34 import org.thingsboard.server.common.data.TenantProfile; 33 import org.thingsboard.server.common.data.TenantProfile;
  34 +import org.thingsboard.server.common.data.exception.ThingsboardException;
35 import org.thingsboard.server.common.data.id.ApiUsageStateId; 35 import org.thingsboard.server.common.data.id.ApiUsageStateId;
36 import org.thingsboard.server.common.data.id.TenantId; 36 import org.thingsboard.server.common.data.id.TenantId;
37 import org.thingsboard.server.common.data.id.TenantProfileId; 37 import org.thingsboard.server.common.data.id.TenantProfileId;
@@ -45,6 +45,7 @@ import org.thingsboard.server.common.data.tenant.profile.TenantProfileData; @@ -45,6 +45,7 @@ import org.thingsboard.server.common.data.tenant.profile.TenantProfileData;
45 import org.thingsboard.server.common.msg.queue.ServiceType; 45 import org.thingsboard.server.common.msg.queue.ServiceType;
46 import org.thingsboard.server.common.msg.queue.TbCallback; 46 import org.thingsboard.server.common.msg.queue.TbCallback;
47 import org.thingsboard.server.common.msg.tools.SchedulerUtils; 47 import org.thingsboard.server.common.msg.tools.SchedulerUtils;
  48 +import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
48 import org.thingsboard.server.dao.tenant.TenantService; 49 import org.thingsboard.server.dao.tenant.TenantService;
49 import org.thingsboard.server.dao.timeseries.TimeseriesService; 50 import org.thingsboard.server.dao.timeseries.TimeseriesService;
50 import org.thingsboard.server.dao.usagerecord.ApiUsageStateService; 51 import org.thingsboard.server.dao.usagerecord.ApiUsageStateService;
@@ -54,14 +55,13 @@ import org.thingsboard.server.queue.common.TbProtoQueueMsg; @@ -54,14 +55,13 @@ import org.thingsboard.server.queue.common.TbProtoQueueMsg;
54 import org.thingsboard.server.queue.discovery.PartitionChangeEvent; 55 import org.thingsboard.server.queue.discovery.PartitionChangeEvent;
55 import org.thingsboard.server.queue.discovery.PartitionService; 56 import org.thingsboard.server.queue.discovery.PartitionService;
56 import org.thingsboard.server.queue.scheduler.SchedulerComponent; 57 import org.thingsboard.server.queue.scheduler.SchedulerComponent;
57 -import org.thingsboard.server.queue.util.TbCoreComponent;  
58 -import org.thingsboard.server.dao.tenant.TbTenantProfileCache;  
59 import org.thingsboard.server.service.queue.TbClusterService; 58 import org.thingsboard.server.service.queue.TbClusterService;
60 import org.thingsboard.server.service.telemetry.InternalTelemetryService; 59 import org.thingsboard.server.service.telemetry.InternalTelemetryService;
61 60
62 import javax.annotation.PostConstruct; 61 import javax.annotation.PostConstruct;
  62 +import javax.annotation.PreDestroy;
63 import java.util.ArrayList; 63 import java.util.ArrayList;
64 -import java.util.HashMap; 64 +import java.util.Arrays;
65 import java.util.HashSet; 65 import java.util.HashSet;
66 import java.util.List; 66 import java.util.List;
67 import java.util.Map; 67 import java.util.Map;
@@ -69,9 +69,12 @@ import java.util.Set; @@ -69,9 +69,12 @@ import java.util.Set;
69 import java.util.UUID; 69 import java.util.UUID;
70 import java.util.concurrent.ConcurrentHashMap; 70 import java.util.concurrent.ConcurrentHashMap;
71 import java.util.concurrent.ExecutionException; 71 import java.util.concurrent.ExecutionException;
  72 +import java.util.concurrent.ExecutorService;
  73 +import java.util.concurrent.Executors;
72 import java.util.concurrent.TimeUnit; 74 import java.util.concurrent.TimeUnit;
73 import java.util.concurrent.locks.Lock; 75 import java.util.concurrent.locks.Lock;
74 import java.util.concurrent.locks.ReentrantLock; 76 import java.util.concurrent.locks.ReentrantLock;
  77 +import java.util.stream.Collectors;
75 78
76 @Slf4j 79 @Slf4j
77 @Service 80 @Service
@@ -94,6 +97,7 @@ public class DefaultTbApiUsageStateService implements TbApiUsageStateService { @@ -94,6 +97,7 @@ public class DefaultTbApiUsageStateService implements TbApiUsageStateService {
94 private final ApiUsageStateService apiUsageStateService; 97 private final ApiUsageStateService apiUsageStateService;
95 private final SchedulerComponent scheduler; 98 private final SchedulerComponent scheduler;
96 private final TbTenantProfileCache tenantProfileCache; 99 private final TbTenantProfileCache tenantProfileCache;
  100 + private final MailService mailService;
97 101
98 @Lazy 102 @Lazy
99 @Autowired 103 @Autowired
@@ -112,13 +116,15 @@ public class DefaultTbApiUsageStateService implements TbApiUsageStateService { @@ -112,13 +116,15 @@ public class DefaultTbApiUsageStateService implements TbApiUsageStateService {
112 116
113 private final Lock updateLock = new ReentrantLock(); 117 private final Lock updateLock = new ReentrantLock();
114 118
  119 + private final ExecutorService mailExecutor;
  120 +
115 public DefaultTbApiUsageStateService(TbClusterService clusterService, 121 public DefaultTbApiUsageStateService(TbClusterService clusterService,
116 PartitionService partitionService, 122 PartitionService partitionService,
117 TenantService tenantService, 123 TenantService tenantService,
118 TimeseriesService tsService, 124 TimeseriesService tsService,
119 ApiUsageStateService apiUsageStateService, 125 ApiUsageStateService apiUsageStateService,
120 SchedulerComponent scheduler, 126 SchedulerComponent scheduler,
121 - TbTenantProfileCache tenantProfileCache) { 127 + TbTenantProfileCache tenantProfileCache, MailService mailService) {
122 this.clusterService = clusterService; 128 this.clusterService = clusterService;
123 this.partitionService = partitionService; 129 this.partitionService = partitionService;
124 this.tenantService = tenantService; 130 this.tenantService = tenantService;
@@ -126,6 +132,8 @@ public class DefaultTbApiUsageStateService implements TbApiUsageStateService { @@ -126,6 +132,8 @@ public class DefaultTbApiUsageStateService implements TbApiUsageStateService {
126 this.apiUsageStateService = apiUsageStateService; 132 this.apiUsageStateService = apiUsageStateService;
127 this.scheduler = scheduler; 133 this.scheduler = scheduler;
128 this.tenantProfileCache = tenantProfileCache; 134 this.tenantProfileCache = tenantProfileCache;
  135 + this.mailService = mailService;
  136 + this.mailExecutor = Executors.newSingleThreadExecutor();
129 } 137 }
130 138
131 @PostConstruct 139 @PostConstruct
@@ -141,6 +149,11 @@ public class DefaultTbApiUsageStateService implements TbApiUsageStateService { @@ -141,6 +149,11 @@ public class DefaultTbApiUsageStateService implements TbApiUsageStateService {
141 public void process(TbProtoQueueMsg<ToUsageStatsServiceMsg> msg, TbCallback callback) { 149 public void process(TbProtoQueueMsg<ToUsageStatsServiceMsg> msg, TbCallback callback) {
142 ToUsageStatsServiceMsg statsMsg = msg.getValue(); 150 ToUsageStatsServiceMsg statsMsg = msg.getValue();
143 TenantId tenantId = new TenantId(new UUID(statsMsg.getTenantIdMSB(), statsMsg.getTenantIdLSB())); 151 TenantId tenantId = new TenantId(new UUID(statsMsg.getTenantIdMSB(), statsMsg.getTenantIdLSB()));
  152 +
  153 + if (tenantProfileCache.get(tenantId) == null) {
  154 + return;
  155 + }
  156 +
144 TenantApiUsageState tenantState; 157 TenantApiUsageState tenantState;
145 List<TsKvEntry> updatedEntries; 158 List<TsKvEntry> updatedEntries;
146 Map<ApiFeature, ApiUsageStateValue> result; 159 Map<ApiFeature, ApiUsageStateValue> result;
@@ -160,7 +173,7 @@ public class DefaultTbApiUsageStateService implements TbApiUsageStateService { @@ -160,7 +173,7 @@ public class DefaultTbApiUsageStateService implements TbApiUsageStateService {
160 long newValue = tenantState.add(recordKey, kvProto.getValue()); 173 long newValue = tenantState.add(recordKey, kvProto.getValue());
161 updatedEntries.add(new BasicTsKvEntry(ts, new LongDataEntry(recordKey.getApiCountKey(), newValue))); 174 updatedEntries.add(new BasicTsKvEntry(ts, new LongDataEntry(recordKey.getApiCountKey(), newValue)));
162 long newHourlyValue = tenantState.addToHourly(recordKey, kvProto.getValue()); 175 long newHourlyValue = tenantState.addToHourly(recordKey, kvProto.getValue());
163 - updatedEntries.add(new BasicTsKvEntry(hourTs, new LongDataEntry(recordKey.getApiCountKey() + HOURLY, newHourlyValue))); 176 + updatedEntries.add(new BasicTsKvEntry(newHourTs, new LongDataEntry(recordKey.getApiCountKey() + HOURLY, newHourlyValue)));
164 apiFeatures.add(recordKey.getApiFeature()); 177 apiFeatures.add(recordKey.getApiFeature());
165 } 178 }
166 result = tenantState.checkStateUpdatedDueToThreshold(apiFeatures); 179 result = tenantState.checkStateUpdatedDueToThreshold(apiFeatures);
@@ -286,7 +299,49 @@ public class DefaultTbApiUsageStateService implements TbApiUsageStateService { @@ -286,7 +299,49 @@ public class DefaultTbApiUsageStateService implements TbApiUsageStateService {
286 List<TsKvEntry> stateTelemetry = new ArrayList<>(); 299 List<TsKvEntry> stateTelemetry = new ArrayList<>();
287 result.forEach(((apiFeature, aState) -> stateTelemetry.add(new BasicTsKvEntry(ts, new StringDataEntry(apiFeature.getApiStateKey(), aState.name()))))); 300 result.forEach(((apiFeature, aState) -> stateTelemetry.add(new BasicTsKvEntry(ts, new StringDataEntry(apiFeature.getApiStateKey(), aState.name())))));
288 tsWsService.saveAndNotifyInternal(state.getTenantId(), state.getApiUsageState().getId(), stateTelemetry, VOID_CALLBACK); 301 tsWsService.saveAndNotifyInternal(state.getTenantId(), state.getApiUsageState().getId(), stateTelemetry, VOID_CALLBACK);
289 - //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);
290 } 345 }
291 346
292 private void checkStartOfNextCycle() { 347 private void checkStartOfNextCycle() {
@@ -294,8 +349,11 @@ public class DefaultTbApiUsageStateService implements TbApiUsageStateService { @@ -294,8 +349,11 @@ public class DefaultTbApiUsageStateService implements TbApiUsageStateService {
294 try { 349 try {
295 long now = System.currentTimeMillis(); 350 long now = System.currentTimeMillis();
296 myTenantStates.values().forEach(state -> { 351 myTenantStates.values().forEach(state -> {
297 - 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();
298 state.setCycles(state.getNextCycleTs(), SchedulerUtils.getStartOfNextNextMonth()); 354 state.setCycles(state.getNextCycleTs(), SchedulerUtils.getStartOfNextNextMonth());
  355 + saveNewCounts(state, Arrays.asList(ApiUsageRecordKey.values()));
  356 + updateTenantState(state, tenantProfileCache.get(tenantId));
299 } 357 }
300 }); 358 });
301 } finally { 359 } finally {
@@ -303,6 +361,14 @@ public class DefaultTbApiUsageStateService implements TbApiUsageStateService { @@ -303,6 +361,14 @@ public class DefaultTbApiUsageStateService implements TbApiUsageStateService {
303 } 361 }
304 } 362 }
305 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 +
306 private TenantApiUsageState getOrFetchState(TenantId tenantId) { 372 private TenantApiUsageState getOrFetchState(TenantId tenantId) {
307 TenantApiUsageState tenantState = myTenantStates.get(tenantId); 373 TenantApiUsageState tenantState = myTenantStates.get(tenantId);
308 if (tenantState == null) { 374 if (tenantState == null) {
@@ -316,6 +382,7 @@ public class DefaultTbApiUsageStateService implements TbApiUsageStateService { @@ -316,6 +382,7 @@ public class DefaultTbApiUsageStateService implements TbApiUsageStateService {
316 } 382 }
317 TenantProfile tenantProfile = tenantProfileCache.get(tenantId); 383 TenantProfile tenantProfile = tenantProfileCache.get(tenantId);
318 tenantState = new TenantApiUsageState(tenantProfile, dbStateEntity); 384 tenantState = new TenantApiUsageState(tenantProfile, dbStateEntity);
  385 + List<ApiUsageRecordKey> newCounts = new ArrayList<>();
319 try { 386 try {
320 List<TsKvEntry> dbValues = tsService.findAllLatest(tenantId, dbStateEntity.getId()).get(); 387 List<TsKvEntry> dbValues = tsService.findAllLatest(tenantId, dbStateEntity.getId()).get();
321 for (ApiUsageRecordKey key : ApiUsageRecordKey.values()) { 388 for (ApiUsageRecordKey key : ApiUsageRecordKey.values()) {
@@ -324,7 +391,13 @@ public class DefaultTbApiUsageStateService implements TbApiUsageStateService { @@ -324,7 +391,13 @@ public class DefaultTbApiUsageStateService implements TbApiUsageStateService {
324 for (TsKvEntry tsKvEntry : dbValues) { 391 for (TsKvEntry tsKvEntry : dbValues) {
325 if (tsKvEntry.getKey().equals(key.getApiCountKey())) { 392 if (tsKvEntry.getKey().equals(key.getApiCountKey())) {
326 cycleEntryFound = true; 393 cycleEntryFound = true;
327 - 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 + }
328 } else if (tsKvEntry.getKey().equals(key.getApiCountKey() + HOURLY)) { 401 } else if (tsKvEntry.getKey().equals(key.getApiCountKey() + HOURLY)) {
329 hourlyEntryFound = true; 402 hourlyEntryFound = true;
330 tenantState.putHourly(key, tsKvEntry.getTs() == tenantState.getCurrentHourTs() ? tsKvEntry.getLongValue().get() : 0L); 403 tenantState.putHourly(key, tsKvEntry.getTs() == tenantState.getCurrentHourTs() ? tsKvEntry.getLongValue().get() : 0L);
@@ -336,6 +409,7 @@ public class DefaultTbApiUsageStateService implements TbApiUsageStateService { @@ -336,6 +409,7 @@ public class DefaultTbApiUsageStateService implements TbApiUsageStateService {
336 } 409 }
337 log.debug("[{}] Initialized state: {}", tenantId, dbStateEntity); 410 log.debug("[{}] Initialized state: {}", tenantId, dbStateEntity);
338 myTenantStates.put(tenantId, tenantState); 411 myTenantStates.put(tenantId, tenantState);
  412 + saveNewCounts(tenantState, newCounts);
339 } catch (InterruptedException | ExecutionException e) { 413 } catch (InterruptedException | ExecutionException e) {
340 log.warn("[{}] Failed to fetch api usage state from db.", tenantId, e); 414 log.warn("[{}] Failed to fetch api usage state from db.", tenantId, e);
341 } 415 }
@@ -367,4 +441,10 @@ public class DefaultTbApiUsageStateService implements TbApiUsageStateService { @@ -367,4 +441,10 @@ public class DefaultTbApiUsageStateService implements TbApiUsageStateService {
367 } 441 }
368 } 442 }
369 443
  444 + @PreDestroy
  445 + private void destroy() {
  446 + if (mailExecutor != null) {
  447 + mailExecutor.shutdownNow();
  448 + }
  449 + }
370 } 450 }
@@ -125,6 +125,10 @@ public class TenantApiUsageState { @@ -125,6 +125,10 @@ public class TenantApiUsageState {
125 return apiUsageState.getDbStorageState(); 125 return apiUsageState.getDbStorageState();
126 case JS: 126 case JS:
127 return apiUsageState.getJsExecState(); 127 return apiUsageState.getJsExecState();
  128 + case EMAIL:
  129 + return apiUsageState.getEmailExecState();
  130 + case SMS:
  131 + return apiUsageState.getSmsExecState();
128 default: 132 default:
129 return ApiUsageStateValue.ENABLED; 133 return ApiUsageStateValue.ENABLED;
130 } 134 }
@@ -145,6 +149,12 @@ public class TenantApiUsageState { @@ -145,6 +149,12 @@ public class TenantApiUsageState {
145 case JS: 149 case JS:
146 apiUsageState.setJsExecState(value); 150 apiUsageState.setJsExecState(value);
147 break; 151 break;
  152 + case EMAIL:
  153 + apiUsageState.setEmailExecState(value);
  154 + break;
  155 + case SMS:
  156 + apiUsageState.setSmsExecState(value);
  157 + break;
148 } 158 }
149 return !currentValue.equals(value); 159 return !currentValue.equals(value);
150 } 160 }
@@ -171,7 +181,7 @@ public class TenantApiUsageState { @@ -171,7 +181,7 @@ public class TenantApiUsageState {
171 long threshold = getProfileThreshold(recordKey); 181 long threshold = getProfileThreshold(recordKey);
172 long warnThreshold = getProfileWarnThreshold(recordKey); 182 long warnThreshold = getProfileWarnThreshold(recordKey);
173 ApiUsageStateValue tmpValue; 183 ApiUsageStateValue tmpValue;
174 - if (threshold == 0 || value < warnThreshold) { 184 + if (threshold == 0 || value == 0 || value < warnThreshold) {
175 tmpValue = ApiUsageStateValue.ENABLED; 185 tmpValue = ApiUsageStateValue.ENABLED;
176 } else if (value < threshold) { 186 } else if (value < threshold) {
177 tmpValue = ApiUsageStateValue.WARNING; 187 tmpValue = ApiUsageStateValue.WARNING;
@@ -18,10 +18,9 @@ package org.thingsboard.server.service.device; @@ -18,10 +18,9 @@ package org.thingsboard.server.service.device;
18 import com.fasterxml.jackson.core.JsonProcessingException; 18 import com.fasterxml.jackson.core.JsonProcessingException;
19 import com.fasterxml.jackson.databind.JsonNode; 19 import com.fasterxml.jackson.databind.JsonNode;
20 import com.fasterxml.jackson.databind.node.ObjectNode; 20 import com.fasterxml.jackson.databind.node.ObjectNode;
21 -import com.google.common.util.concurrent.Futures;  
22 import com.google.common.util.concurrent.ListenableFuture; 21 import com.google.common.util.concurrent.ListenableFuture;
23 -import com.google.common.util.concurrent.MoreExecutors;  
24 import lombok.extern.slf4j.Slf4j; 22 import lombok.extern.slf4j.Slf4j;
  23 +import org.apache.commons.lang.RandomStringUtils;
25 import org.springframework.beans.factory.annotation.Autowired; 24 import org.springframework.beans.factory.annotation.Autowired;
26 import org.springframework.stereotype.Service; 25 import org.springframework.stereotype.Service;
27 import org.springframework.util.StringUtils; 26 import org.springframework.util.StringUtils;
@@ -113,6 +112,13 @@ public class DeviceProvisionServiceImpl implements DeviceProvisionService { @@ -113,6 +112,13 @@ public class DeviceProvisionServiceImpl implements DeviceProvisionService {
113 public ProvisionResponse provisionDevice(ProvisionRequest provisionRequest) { 112 public ProvisionResponse provisionDevice(ProvisionRequest provisionRequest) {
114 String provisionRequestKey = provisionRequest.getCredentials().getProvisionDeviceKey(); 113 String provisionRequestKey = provisionRequest.getCredentials().getProvisionDeviceKey();
115 String provisionRequestSecret = provisionRequest.getCredentials().getProvisionDeviceSecret(); 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 if (StringUtils.isEmpty(provisionRequestKey) || StringUtils.isEmpty(provisionRequestSecret)) { 123 if (StringUtils.isEmpty(provisionRequestKey) || StringUtils.isEmpty(provisionRequestSecret)) {
118 throw new ProvisionFailedException(ProvisionResponseStatus.NOT_FOUND.name()); 124 throw new ProvisionFailedException(ProvisionResponseStatus.NOT_FOUND.name());
@@ -188,6 +194,11 @@ public class DeviceProvisionServiceImpl implements DeviceProvisionService { @@ -188,6 +194,11 @@ public class DeviceProvisionServiceImpl implements DeviceProvisionService {
188 Device device = deviceService.findDeviceByTenantIdAndName(profile.getTenantId(), provisionRequest.getDeviceName()); 194 Device device = deviceService.findDeviceByTenantIdAndName(profile.getTenantId(), provisionRequest.getDeviceName());
189 try { 195 try {
190 if (device == null) { 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 Device savedDevice = deviceService.saveDevice(provisionRequest, profile); 202 Device savedDevice = deviceService.saveDevice(provisionRequest, profile);
192 203
193 deviceStateService.onDeviceAdded(savedDevice); 204 deviceStateService.onDeviceAdded(savedDevice);
@@ -28,12 +28,21 @@ import org.thingsboard.server.common.data.Customer; @@ -28,12 +28,21 @@ import org.thingsboard.server.common.data.Customer;
28 import org.thingsboard.server.common.data.DataConstants; 28 import org.thingsboard.server.common.data.DataConstants;
29 import org.thingsboard.server.common.data.Device; 29 import org.thingsboard.server.common.data.Device;
30 import org.thingsboard.server.common.data.DeviceProfile; 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 import org.thingsboard.server.common.data.Tenant; 34 import org.thingsboard.server.common.data.Tenant;
32 import org.thingsboard.server.common.data.TenantProfile; 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 import org.thingsboard.server.common.data.User; 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 import org.thingsboard.server.common.data.id.CustomerId; 46 import org.thingsboard.server.common.data.id.CustomerId;
38 import org.thingsboard.server.common.data.id.DeviceId; 47 import org.thingsboard.server.common.data.id.DeviceId;
39 import org.thingsboard.server.common.data.id.DeviceProfileId; 48 import org.thingsboard.server.common.data.id.DeviceProfileId;
@@ -42,19 +51,29 @@ import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry; @@ -42,19 +51,29 @@ import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry;
42 import org.thingsboard.server.common.data.kv.BooleanDataEntry; 51 import org.thingsboard.server.common.data.kv.BooleanDataEntry;
43 import org.thingsboard.server.common.data.kv.DoubleDataEntry; 52 import org.thingsboard.server.common.data.kv.DoubleDataEntry;
44 import org.thingsboard.server.common.data.kv.LongDataEntry; 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 import org.thingsboard.server.common.data.security.Authority; 64 import org.thingsboard.server.common.data.security.Authority;
47 import org.thingsboard.server.common.data.security.DeviceCredentials; 65 import org.thingsboard.server.common.data.security.DeviceCredentials;
48 import org.thingsboard.server.common.data.security.UserCredentials; 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 import org.thingsboard.server.common.data.widget.WidgetsBundle; 69 import org.thingsboard.server.common.data.widget.WidgetsBundle;
50 -import org.thingsboard.server.dao.asset.AssetService;  
51 import org.thingsboard.server.dao.attributes.AttributesService; 70 import org.thingsboard.server.dao.attributes.AttributesService;
52 import org.thingsboard.server.dao.customer.CustomerService; 71 import org.thingsboard.server.dao.customer.CustomerService;
53 import org.thingsboard.server.dao.device.DeviceCredentialsService; 72 import org.thingsboard.server.dao.device.DeviceCredentialsService;
54 import org.thingsboard.server.dao.device.DeviceProfileService; 73 import org.thingsboard.server.dao.device.DeviceProfileService;
55 import org.thingsboard.server.dao.device.DeviceService; 74 import org.thingsboard.server.dao.device.DeviceService;
56 import org.thingsboard.server.dao.exception.DataValidationException; 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 import org.thingsboard.server.dao.settings.AdminSettingsService; 77 import org.thingsboard.server.dao.settings.AdminSettingsService;
59 import org.thingsboard.server.dao.tenant.TenantProfileService; 78 import org.thingsboard.server.dao.tenant.TenantProfileService;
60 import org.thingsboard.server.dao.tenant.TenantService; 79 import org.thingsboard.server.dao.tenant.TenantService;
@@ -62,6 +81,8 @@ import org.thingsboard.server.dao.user.UserService; @@ -62,6 +81,8 @@ import org.thingsboard.server.dao.user.UserService;
62 import org.thingsboard.server.dao.widget.WidgetsBundleService; 81 import org.thingsboard.server.dao.widget.WidgetsBundleService;
63 82
64 import java.util.Arrays; 83 import java.util.Arrays;
  84 +import java.util.Collections;
  85 +import java.util.TreeMap;
65 86
66 @Service 87 @Service
67 @Profile("install") 88 @Profile("install")
@@ -97,12 +118,6 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService { @@ -97,12 +118,6 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService {
97 private CustomerService customerService; 118 private CustomerService customerService;
98 119
99 @Autowired 120 @Autowired
100 - private RelationService relationService;  
101 -  
102 - @Autowired  
103 - private AssetService assetService;  
104 -  
105 - @Autowired  
106 private DeviceService deviceService; 121 private DeviceService deviceService;
107 122
108 @Autowired 123 @Autowired
@@ -114,6 +129,9 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService { @@ -114,6 +129,9 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService {
114 @Autowired 129 @Autowired
115 private DeviceCredentialsService deviceCredentialsService; 130 private DeviceCredentialsService deviceCredentialsService;
116 131
  132 + @Autowired
  133 + private RuleChainService ruleChainService;
  134 +
117 @Bean 135 @Bean
118 protected BCryptPasswordEncoder passwordEncoder() { 136 protected BCryptPasswordEncoder passwordEncoder() {
119 return new BCryptPasswordEncoder(); 137 return new BCryptPasswordEncoder();
@@ -245,35 +263,149 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService { @@ -245,35 +263,149 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService {
245 createDevice(demoTenant.getId(), null, defaultDeviceProfile.getId(), "Raspberry Pi Demo Device", "RASPBERRY_PI_DEMO_TOKEN", "Demo device that is used in " + 263 createDevice(demoTenant.getId(), null, defaultDeviceProfile.getId(), "Raspberry Pi Demo Device", "RASPBERRY_PI_DEMO_TOKEN", "Demo device that is used in " +
246 "Raspberry Pi GPIO control sample application"); 264 "Raspberry Pi GPIO control sample application");
247 265
248 - Asset thermostatAlarms = new Asset();  
249 - thermostatAlarms.setTenantId(demoTenant.getId());  
250 - thermostatAlarms.setName("Thermostat Alarms");  
251 - thermostatAlarms.setType("AlarmPropagationAsset");  
252 - thermostatAlarms = assetService.saveAsset(thermostatAlarms);  
253 -  
254 - DeviceProfile thermostatDeviceProfile = this.deviceProfileService.findOrCreateDeviceProfile(demoTenant.getId(), "thermostat");  
255 -  
256 - DeviceId t1Id = createDevice(demoTenant.getId(), null, thermostatDeviceProfile.getId(), "Thermostat T1", "T1_TEST_TOKEN", "Demo device for Thermostats dashboard").getId();  
257 - DeviceId t2Id = createDevice(demoTenant.getId(), null, thermostatDeviceProfile.getId(), "Thermostat T2", "T2_TEST_TOKEN", "Demo device for Thermostats dashboard").getId();  
258 -  
259 - relationService.saveRelation(thermostatAlarms.getTenantId(), new EntityRelation(thermostatAlarms.getId(), t1Id, "ToAlarmPropagationAsset"));  
260 - 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();
261 393
262 attributesService.save(demoTenant.getId(), t1Id, DataConstants.SERVER_SCOPE, 394 attributesService.save(demoTenant.getId(), t1Id, DataConstants.SERVER_SCOPE,
263 Arrays.asList(new BaseAttributeKvEntry(System.currentTimeMillis(), new DoubleDataEntry("latitude", 37.3948)), 395 Arrays.asList(new BaseAttributeKvEntry(System.currentTimeMillis(), new DoubleDataEntry("latitude", 37.3948)),
264 new BaseAttributeKvEntry(System.currentTimeMillis(), new DoubleDataEntry("longitude", -122.1503)), 396 new BaseAttributeKvEntry(System.currentTimeMillis(), new DoubleDataEntry("longitude", -122.1503)),
265 - new BaseAttributeKvEntry(System.currentTimeMillis(), new BooleanDataEntry("alarmTemperature", true)),  
266 - new BaseAttributeKvEntry(System.currentTimeMillis(), new BooleanDataEntry("alarmHumidity", true)),  
267 - new BaseAttributeKvEntry(System.currentTimeMillis(), new LongDataEntry("thresholdTemperature", (long) 20)),  
268 - 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))));
269 401
270 attributesService.save(demoTenant.getId(), t2Id, DataConstants.SERVER_SCOPE, 402 attributesService.save(demoTenant.getId(), t2Id, DataConstants.SERVER_SCOPE,
271 Arrays.asList(new BaseAttributeKvEntry(System.currentTimeMillis(), new DoubleDataEntry("latitude", 37.493801)), 403 Arrays.asList(new BaseAttributeKvEntry(System.currentTimeMillis(), new DoubleDataEntry("latitude", 37.493801)),
272 new BaseAttributeKvEntry(System.currentTimeMillis(), new DoubleDataEntry("longitude", -121.948769)), 404 new BaseAttributeKvEntry(System.currentTimeMillis(), new DoubleDataEntry("longitude", -121.948769)),
273 - new BaseAttributeKvEntry(System.currentTimeMillis(), new BooleanDataEntry("alarmTemperature", true)),  
274 - new BaseAttributeKvEntry(System.currentTimeMillis(), new BooleanDataEntry("alarmHumidity", true)),  
275 - new BaseAttributeKvEntry(System.currentTimeMillis(), new LongDataEntry("thresholdTemperature", (long) 25)),  
276 - 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))));
277 409
278 installScripts.loadDashboards(demoTenant.getId(), null); 410 installScripts.loadDashboards(demoTenant.getId(), null);
279 } 411 }
@@ -210,26 +210,9 @@ public class InstallScripts { @@ -210,26 +210,9 @@ public class InstallScripts {
210 210
211 211
212 public void loadDemoRuleChains(TenantId tenantId) throws Exception { 212 public void loadDemoRuleChains(TenantId tenantId) throws Exception {
213 - Path ruleChainsDir = Paths.get(getDataDir(), JSON_DIR, DEMO_DIR, RULE_CHAINS_DIR);  
214 try { 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 } catch (Exception e) { 216 } catch (Exception e) {
234 log.error("Unable to load dashboard from json", e); 217 log.error("Unable to load dashboard from json", e);
235 throw new RuntimeException("Unable to load dashboard from json", e); 218 throw new RuntimeException("Unable to load dashboard from json", e);
@@ -367,6 +367,8 @@ public class SqlDatabaseUpgradeService implements DatabaseEntitiesUpgradeService @@ -367,6 +367,8 @@ public class SqlDatabaseUpgradeService implements DatabaseEntitiesUpgradeService
367 " db_storage varchar(32)," + 367 " db_storage varchar(32)," +
368 " re_exec varchar(32)," + 368 " re_exec varchar(32)," +
369 " js_exec varchar(32)," + 369 " js_exec varchar(32)," +
  370 + " email_exec varchar(32)," +
  371 + " sms_exec varchar(32)," +
370 " CONSTRAINT api_usage_state_unq_key UNIQUE (tenant_id, entity_id)\n" + 372 " CONSTRAINT api_usage_state_unq_key UNIQUE (tenant_id, entity_id)\n" +
371 ");"); 373 ");");
372 } catch (Exception e) { 374 } catch (Exception e) {
@@ -419,6 +421,19 @@ public class SqlDatabaseUpgradeService implements DatabaseEntitiesUpgradeService @@ -419,6 +421,19 @@ public class SqlDatabaseUpgradeService implements DatabaseEntitiesUpgradeService
419 log.error("Failed updating schema!!!", e); 421 log.error("Failed updating schema!!!", e);
420 } 422 }
421 break; 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("ALTER TABLE dashboard ALTER COLUMN configuration TYPE varchar;");
  430 + conn.createStatement().execute("UPDATE tb_schema_settings SET schema_version = 3002001;");
  431 + } catch (Exception e) {
  432 + log.error("Failed updating schema!!!", e);
  433 + }
  434 + log.info("Schema updated.");
  435 + }
  436 + break;
422 default: 437 default:
423 throw new RuntimeException("Unable to upgrade SQL database, unsupported fromVersion: " + fromVersion); 438 throw new RuntimeException("Unable to upgrade SQL database, unsupported fromVersion: " + fromVersion);
424 } 439 }
@@ -20,8 +20,10 @@ import freemarker.template.Configuration; @@ -20,8 +20,10 @@ import freemarker.template.Configuration;
20 import freemarker.template.Template; 20 import freemarker.template.Template;
21 import lombok.extern.slf4j.Slf4j; 21 import lombok.extern.slf4j.Slf4j;
22 import org.apache.commons.lang3.StringUtils; 22 import org.apache.commons.lang3.StringUtils;
  23 +import org.jetbrains.annotations.NotNull;
23 import org.springframework.beans.factory.annotation.Autowired; 24 import org.springframework.beans.factory.annotation.Autowired;
24 import org.springframework.context.MessageSource; 25 import org.springframework.context.MessageSource;
  26 +import org.springframework.context.annotation.Lazy;
25 import org.springframework.core.NestedRuntimeException; 27 import org.springframework.core.NestedRuntimeException;
26 import org.springframework.mail.javamail.JavaMailSenderImpl; 28 import org.springframework.mail.javamail.JavaMailSenderImpl;
27 import org.springframework.mail.javamail.MimeMessageHelper; 29 import org.springframework.mail.javamail.MimeMessageHelper;
@@ -29,12 +31,18 @@ import org.springframework.stereotype.Service; @@ -29,12 +31,18 @@ import org.springframework.stereotype.Service;
29 import org.springframework.ui.freemarker.FreeMarkerTemplateUtils; 31 import org.springframework.ui.freemarker.FreeMarkerTemplateUtils;
30 import org.thingsboard.rule.engine.api.MailService; 32 import org.thingsboard.rule.engine.api.MailService;
31 import org.thingsboard.server.common.data.AdminSettings; 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 import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; 38 import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
33 import org.thingsboard.server.common.data.exception.ThingsboardException; 39 import org.thingsboard.server.common.data.exception.ThingsboardException;
34 import org.thingsboard.server.common.data.id.EntityId; 40 import org.thingsboard.server.common.data.id.EntityId;
35 import org.thingsboard.server.common.data.id.TenantId; 41 import org.thingsboard.server.common.data.id.TenantId;
36 import org.thingsboard.server.dao.exception.IncorrectParameterException; 42 import org.thingsboard.server.dao.exception.IncorrectParameterException;
37 import org.thingsboard.server.dao.settings.AdminSettingsService; 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 import javax.annotation.PostConstruct; 47 import javax.annotation.PostConstruct;
40 import javax.mail.MessagingException; 48 import javax.mail.MessagingException;
@@ -51,18 +59,28 @@ public class DefaultMailService implements MailService { @@ -51,18 +59,28 @@ public class DefaultMailService implements MailService {
51 public static final String MAIL_PROP = "mail."; 59 public static final String MAIL_PROP = "mail.";
52 public static final String TARGET_EMAIL = "targetEmail"; 60 public static final String TARGET_EMAIL = "targetEmail";
53 public static final String UTF_8 = "UTF-8"; 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 @Autowired 71 @Autowired
58 - private Configuration freemarkerConfig; 72 + private TbApiUsageStateService apiUsageStateService;
59 73
60 private JavaMailSenderImpl mailSender; 74 private JavaMailSenderImpl mailSender;
61 75
62 private String mailFrom; 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 @PostConstruct 85 @PostConstruct
68 private void init() { 86 private void init() {
@@ -141,7 +159,7 @@ public class DefaultMailService implements MailService { @@ -141,7 +159,7 @@ public class DefaultMailService implements MailService {
141 } 159 }
142 160
143 @Override 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 sendMail(mailSender, mailFrom, email, subject, message); 163 sendMail(mailSender, mailFrom, email, subject, message);
146 } 164 }
147 165
@@ -216,20 +234,25 @@ public class DefaultMailService implements MailService { @@ -216,20 +234,25 @@ public class DefaultMailService implements MailService {
216 } 234 }
217 235
218 @Override 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 @Override 258 @Override
@@ -246,6 +269,122 @@ public class DefaultMailService implements MailService { @@ -246,6 +269,122 @@ public class DefaultMailService implements MailService {
246 sendMail(mailSender, mailFrom, email, subject, message); 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 private void sendMail(JavaMailSenderImpl mailSender, 388 private void sendMail(JavaMailSenderImpl mailSender,
250 String mailFrom, String email, 389 String mailFrom, String email,
251 String subject, String message) throws ThingsboardException { 390 String subject, String message) throws ThingsboardException {
@@ -22,6 +22,7 @@ import org.springframework.scheduling.annotation.Scheduled; @@ -22,6 +22,7 @@ import org.springframework.scheduling.annotation.Scheduled;
22 import org.springframework.stereotype.Service; 22 import org.springframework.stereotype.Service;
23 import org.thingsboard.rule.engine.api.msg.ToDeviceActorNotificationMsg; 23 import org.thingsboard.rule.engine.api.msg.ToDeviceActorNotificationMsg;
24 import org.thingsboard.server.common.data.ApiUsageState; 24 import org.thingsboard.server.common.data.ApiUsageState;
  25 +import org.thingsboard.server.common.data.Device;
25 import org.thingsboard.server.common.data.DeviceProfile; 26 import org.thingsboard.server.common.data.DeviceProfile;
26 import org.thingsboard.server.common.data.EntityType; 27 import org.thingsboard.server.common.data.EntityType;
27 import org.thingsboard.server.common.data.HasName; 28 import org.thingsboard.server.common.data.HasName;
@@ -32,7 +33,6 @@ import org.thingsboard.server.common.data.id.DeviceProfileId; @@ -32,7 +33,6 @@ import org.thingsboard.server.common.data.id.DeviceProfileId;
32 import org.thingsboard.server.common.data.id.EntityId; 33 import org.thingsboard.server.common.data.id.EntityId;
33 import org.thingsboard.server.common.data.id.RuleChainId; 34 import org.thingsboard.server.common.data.id.RuleChainId;
34 import org.thingsboard.server.common.data.id.TenantId; 35 import org.thingsboard.server.common.data.id.TenantId;
35 -import org.thingsboard.server.common.data.id.TenantProfileId;  
36 import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; 36 import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
37 import org.thingsboard.server.common.msg.TbMsg; 37 import org.thingsboard.server.common.msg.TbMsg;
38 import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg; 38 import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg;
@@ -237,6 +237,16 @@ public class DefaultTbClusterService implements TbClusterService { @@ -237,6 +237,16 @@ public class DefaultTbClusterService implements TbClusterService {
237 onEntityDelete(TenantId.SYS_TENANT_ID, entity.getId(), entity.getName(), callback); 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 public <T> void onEntityChange(TenantId tenantId, EntityId entityid, T entity, TbQueueCallback callback) { 250 public <T> void onEntityChange(TenantId tenantId, EntityId entityid, T entity, TbQueueCallback callback) {
241 String entityName = (entity instanceof HasName) ? ((HasName) entity).getName() : entity.getClass().getName(); 251 String entityName = (entity instanceof HasName) ? ((HasName) entity).getName() : entity.getClass().getName();
242 log.trace("[{}][{}][{}] Processing [{}] change event", tenantId, entityid.getEntityType(), entityid.getId(), entityName); 252 log.trace("[{}][{}][{}] Processing [{}] change event", tenantId, entityid.getEntityType(), entityid.getId(), entityName);
@@ -15,6 +15,8 @@ @@ -15,6 +15,8 @@
15 */ 15 */
16 package org.thingsboard.server.service.queue; 16 package org.thingsboard.server.service.queue;
17 17
  18 +import lombok.Getter;
  19 +import lombok.Setter;
18 import lombok.extern.slf4j.Slf4j; 20 import lombok.extern.slf4j.Slf4j;
19 import org.springframework.beans.factory.annotation.Value; 21 import org.springframework.beans.factory.annotation.Value;
20 import org.springframework.boot.context.event.ApplicationReadyEvent; 22 import org.springframework.boot.context.event.ApplicationReadyEvent;
@@ -76,6 +78,7 @@ import java.util.concurrent.ConcurrentMap; @@ -76,6 +78,7 @@ import java.util.concurrent.ConcurrentMap;
76 import java.util.concurrent.CountDownLatch; 78 import java.util.concurrent.CountDownLatch;
77 import java.util.concurrent.ExecutorService; 79 import java.util.concurrent.ExecutorService;
78 import java.util.concurrent.Executors; 80 import java.util.concurrent.Executors;
  81 +import java.util.concurrent.Future;
79 import java.util.concurrent.TimeUnit; 82 import java.util.concurrent.TimeUnit;
80 import java.util.function.Function; 83 import java.util.function.Function;
81 import java.util.stream.Collectors; 84 import java.util.stream.Collectors;
@@ -175,39 +178,48 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService<ToCore @@ -175,39 +178,48 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService<ToCore
175 CountDownLatch processingTimeoutLatch = new CountDownLatch(1); 178 CountDownLatch processingTimeoutLatch = new CountDownLatch(1);
176 TbPackProcessingContext<TbProtoQueueMsg<ToCoreMsg>> ctx = new TbPackProcessingContext<>( 179 TbPackProcessingContext<TbProtoQueueMsg<ToCoreMsg>> ctx = new TbPackProcessingContext<>(
177 processingTimeoutLatch, pendingMap, new ConcurrentHashMap<>()); 180 processingTimeoutLatch, pendingMap, new ConcurrentHashMap<>());
178 - pendingMap.forEach((id, msg) -> {  
179 - log.trace("[{}] Creating main callback for message: {}", id, msg.getValue());  
180 - TbCallback callback = new TbPackCallback<>(id, ctx);  
181 - try {  
182 - ToCoreMsg toCoreMsg = msg.getValue();  
183 - if (toCoreMsg.hasToSubscriptionMgrMsg()) {  
184 - log.trace("[{}] Forwarding message to subscription manager service {}", id, toCoreMsg.getToSubscriptionMgrMsg());  
185 - forwardToSubMgrService(toCoreMsg.getToSubscriptionMgrMsg(), callback);  
186 - } else if (toCoreMsg.hasToDeviceActorMsg()) {  
187 - log.trace("[{}] Forwarding message to device actor {}", id, toCoreMsg.getToDeviceActorMsg());  
188 - forwardToDeviceActor(toCoreMsg.getToDeviceActorMsg(), callback);  
189 - } else if (toCoreMsg.hasDeviceStateServiceMsg()) {  
190 - log.trace("[{}] Forwarding message to state service {}", id, toCoreMsg.getDeviceStateServiceMsg());  
191 - forwardToStateService(toCoreMsg.getDeviceStateServiceMsg(), callback);  
192 - } else if (toCoreMsg.getToDeviceActorNotificationMsg() != null && !toCoreMsg.getToDeviceActorNotificationMsg().isEmpty()) {  
193 - Optional<TbActorMsg> actorMsg = encodingService.decode(toCoreMsg.getToDeviceActorNotificationMsg().toByteArray());  
194 - if (actorMsg.isPresent()) {  
195 - TbActorMsg tbActorMsg = actorMsg.get();  
196 - if (tbActorMsg.getMsgType().equals(MsgType.DEVICE_RPC_REQUEST_TO_DEVICE_ACTOR_MSG)) {  
197 - tbCoreDeviceRpcService.forwardRpcRequestToDeviceActor((ToDeviceRpcRequestActorMsg) tbActorMsg);  
198 - } else {  
199 - log.trace("[{}] Forwarding message to App Actor {}", id, actorMsg.get());  
200 - 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 + }
201 } 208 }
  209 + callback.onSuccess();
202 } 210 }
203 - callback.onSuccess(); 211 + } catch (Throwable e) {
  212 + log.warn("[{}] Failed to process message: {}", id, msg, e);
  213 + callback.onFailure(e);
204 } 214 }
205 - } catch (Throwable e) {  
206 - log.warn("[{}] Failed to process message: {}", id, msg, e);  
207 - callback.onFailure(e);  
208 - } 215 + });
209 }); 216 });
210 if (!processingTimeoutLatch.await(packProcessingTimeout, TimeUnit.MILLISECONDS)) { 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 + }
211 ctx.getAckMap().forEach((id, msg) -> log.debug("[{}] Timeout to process message: {}", id, msg.getValue())); 223 ctx.getAckMap().forEach((id, msg) -> log.debug("[{}] Timeout to process message: {}", id, msg.getValue()));
212 ctx.getFailedMap().forEach((id, msg) -> log.warn("[{}] Failed to process message: {}", id, msg.getValue())); 224 ctx.getFailedMap().forEach((id, msg) -> log.warn("[{}] Failed to process message: {}", id, msg.getValue()));
213 } 225 }
@@ -227,6 +239,12 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService<ToCore @@ -227,6 +239,12 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService<ToCore
227 }); 239 });
228 } 240 }
229 241
  242 + private static class PendingMsgHolder {
  243 + @Getter
  244 + @Setter
  245 + private volatile ToCoreMsg toCoreMsg;
  246 + }
  247 +
230 @Override 248 @Override
231 protected ServiceType getServiceType() { 249 protected ServiceType getServiceType() {
232 return ServiceType.TB_CORE; 250 return ServiceType.TB_CORE;
@@ -279,7 +297,7 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService<ToCore @@ -279,7 +297,7 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService<ToCore
279 try { 297 try {
280 handleUsageStats(msg, callback); 298 handleUsageStats(msg, callback);
281 } catch (Throwable e) { 299 } catch (Throwable e) {
282 - log.warn("[{}] Failed to process usge stats: {}", id, msg, e); 300 + log.warn("[{}] Failed to process usage stats: {}", id, msg, e);
283 callback.onFailure(e); 301 callback.onFailure(e);
284 } 302 }
285 }); 303 });
@@ -17,6 +17,7 @@ package org.thingsboard.server.service.queue; @@ -17,6 +17,7 @@ package org.thingsboard.server.service.queue;
17 17
18 import org.thingsboard.rule.engine.api.msg.ToDeviceActorNotificationMsg; 18 import org.thingsboard.rule.engine.api.msg.ToDeviceActorNotificationMsg;
19 import org.thingsboard.server.common.data.ApiUsageState; 19 import org.thingsboard.server.common.data.ApiUsageState;
  20 +import org.thingsboard.server.common.data.Device;
20 import org.thingsboard.server.common.data.DeviceProfile; 21 import org.thingsboard.server.common.data.DeviceProfile;
21 import org.thingsboard.server.common.data.Tenant; 22 import org.thingsboard.server.common.data.Tenant;
22 import org.thingsboard.server.common.data.TenantProfile; 23 import org.thingsboard.server.common.data.TenantProfile;
@@ -66,4 +67,8 @@ public interface TbClusterService { @@ -66,4 +67,8 @@ public interface TbClusterService {
66 void onTenantDelete(Tenant tenant, TbQueueCallback callback); 67 void onTenantDelete(Tenant tenant, TbQueueCallback callback);
67 68
68 void onApiStateChange(ApiUsageState apiUsageState, TbQueueCallback callback); 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 }
  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.MessageAttributeValue;
  24 +import com.amazonaws.services.sns.model.PublishRequest;
  25 +import lombok.extern.slf4j.Slf4j;
  26 +import org.apache.commons.lang3.StringUtils;
  27 +import org.thingsboard.server.common.data.sms.config.AwsSnsSmsProviderConfiguration;
  28 +import org.thingsboard.rule.engine.api.sms.exception.SmsException;
  29 +import org.thingsboard.rule.engine.api.sms.exception.SmsSendException;
  30 +import org.thingsboard.server.service.sms.AbstractSmsSender;
  31 +
  32 +import java.util.HashMap;
  33 +import java.util.Map;
  34 +
  35 +@Slf4j
  36 +public class AwsSmsSender extends AbstractSmsSender {
  37 +
  38 + private static final Map<String, MessageAttributeValue> SMS_ATTRIBUTES = new HashMap<>();
  39 +
  40 + static {
  41 + SMS_ATTRIBUTES.put("AWS.SNS.SMS.SMSType", new MessageAttributeValue()
  42 + .withStringValue("Transactional")
  43 + .withDataType("String"));
  44 + }
  45 +
  46 + private AmazonSNS snsClient;
  47 +
  48 + public AwsSmsSender(AwsSnsSmsProviderConfiguration config) {
  49 + if (StringUtils.isEmpty(config.getAccessKeyId()) || StringUtils.isEmpty(config.getSecretAccessKey()) || StringUtils.isEmpty(config.getRegion())) {
  50 + throw new IllegalArgumentException("Invalid AWS sms provider configuration: aws accessKeyId, aws secretAccessKey and aws region should be specified!");
  51 + }
  52 + AWSCredentials awsCredentials = new BasicAWSCredentials(config.getAccessKeyId(), config.getSecretAccessKey());
  53 + AWSStaticCredentialsProvider credProvider = new AWSStaticCredentialsProvider(awsCredentials);
  54 + this.snsClient = AmazonSNSClient.builder()
  55 + .withCredentials(credProvider)
  56 + .withRegion(config.getRegion())
  57 + .build();
  58 + }
  59 +
  60 + @Override
  61 + public int sendSms(String numberTo, String message) throws SmsException {
  62 + numberTo = this.validatePhoneNumber(numberTo);
  63 + message = this.prepareMessage(message);
  64 + try {
  65 + PublishRequest publishRequest = new PublishRequest()
  66 + .withMessageAttributes(SMS_ATTRIBUTES)
  67 + .withPhoneNumber(numberTo)
  68 + .withMessage(message);
  69 + this.snsClient.publish(publishRequest);
  70 + return this.countMessageSegments(message);
  71 + } catch (Exception e) {
  72 + throw new SmsSendException("Failed to send SMS message - " + e.getMessage(), e);
  73 + }
  74 + }
  75 +
  76 + @Override
  77 + public void destroy() {
  78 + if (this.snsClient != null) {
  79 + try {
  80 + this.snsClient.shutdown();
  81 + } catch (Exception e) {
  82 + log.error("Failed to shutdown SNS client during destroy()", e);
  83 + }
  84 + }
  85 + }
  86 +}
  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,27 +521,27 @@ public class DefaultDeviceStateService implements DeviceStateService {
521 521
522 private void save(DeviceId deviceId, String key, long value) { 522 private void save(DeviceId deviceId, String key, long value) {
523 if (persistToTelemetry) { 523 if (persistToTelemetry) {
524 - tsSubService.saveAndNotify( 524 + tsSubService.saveAndNotifyInternal(
525 TenantId.SYS_TENANT_ID, deviceId, 525 TenantId.SYS_TENANT_ID, deviceId,
526 Collections.singletonList(new BasicTsKvEntry(System.currentTimeMillis(), new LongDataEntry(key, value))), 526 Collections.singletonList(new BasicTsKvEntry(System.currentTimeMillis(), new LongDataEntry(key, value))),
527 - new AttributeSaveCallback(deviceId, key, value)); 527 + new AttributeSaveCallback<>(deviceId, key, value));
528 } else { 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 private void save(DeviceId deviceId, String key, boolean value) { 533 private void save(DeviceId deviceId, String key, boolean value) {
534 if (persistToTelemetry) { 534 if (persistToTelemetry) {
535 - tsSubService.saveAndNotify( 535 + tsSubService.saveAndNotifyInternal(
536 TenantId.SYS_TENANT_ID, deviceId, 536 TenantId.SYS_TENANT_ID, deviceId,
537 Collections.singletonList(new BasicTsKvEntry(System.currentTimeMillis(), new BooleanDataEntry(key, value))), 537 Collections.singletonList(new BasicTsKvEntry(System.currentTimeMillis(), new BooleanDataEntry(key, value))),
538 - new AttributeSaveCallback(deviceId, key, value)); 538 + new AttributeSaveCallback<>(deviceId, key, value));
539 } else { 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 private final DeviceId deviceId; 545 private final DeviceId deviceId;
546 private final String key; 546 private final String key;
547 private final Object value; 547 private final Object value;
@@ -553,7 +553,7 @@ public class DefaultDeviceStateService implements DeviceStateService { @@ -553,7 +553,7 @@ public class DefaultDeviceStateService implements DeviceStateService {
553 } 553 }
554 554
555 @Override 555 @Override
556 - public void onSuccess(@Nullable Void result) { 556 + public void onSuccess(@Nullable T result) {
557 log.trace("[{}] Successfully updated attribute [{}] with value [{}]", deviceId, key, value); 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,12 +21,6 @@ import org.springframework.context.annotation.Lazy;
21 import org.springframework.context.event.EventListener; 21 import org.springframework.context.event.EventListener;
22 import org.springframework.stereotype.Service; 22 import org.springframework.stereotype.Service;
23 import org.thingsboard.common.util.ThingsBoardThreadFactory; 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 import org.thingsboard.server.gen.transport.TransportProtos; 24 import org.thingsboard.server.gen.transport.TransportProtos;
31 import org.thingsboard.server.queue.discovery.ClusterTopologyChangeEvent; 25 import org.thingsboard.server.queue.discovery.ClusterTopologyChangeEvent;
32 import org.thingsboard.server.queue.discovery.PartitionChangeEvent; 26 import org.thingsboard.server.queue.discovery.PartitionChangeEvent;
@@ -36,7 +30,6 @@ import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; @@ -36,7 +30,6 @@ import org.thingsboard.server.common.msg.queue.TopicPartitionInfo;
36 import org.thingsboard.server.common.msg.queue.TbCallback; 30 import org.thingsboard.server.common.msg.queue.TbCallback;
37 import org.thingsboard.server.queue.util.TbCoreComponent; 31 import org.thingsboard.server.queue.util.TbCoreComponent;
38 import org.thingsboard.server.service.queue.TbClusterService; 32 import org.thingsboard.server.service.queue.TbClusterService;
39 -import org.thingsboard.server.service.telemetry.DefaultTelemetryWebSocketService;  
40 import org.thingsboard.server.service.telemetry.sub.AlarmSubscriptionUpdate; 33 import org.thingsboard.server.service.telemetry.sub.AlarmSubscriptionUpdate;
41 import org.thingsboard.server.service.telemetry.sub.TelemetrySubscriptionUpdate; 34 import org.thingsboard.server.service.telemetry.sub.TelemetrySubscriptionUpdate;
42 35
@@ -49,7 +42,6 @@ import java.util.Set; @@ -49,7 +42,6 @@ import java.util.Set;
49 import java.util.concurrent.ConcurrentHashMap; 42 import java.util.concurrent.ConcurrentHashMap;
50 import java.util.concurrent.ExecutorService; 43 import java.util.concurrent.ExecutorService;
51 import java.util.concurrent.Executors; 44 import java.util.concurrent.Executors;
52 -import java.util.stream.Collectors;  
53 45
54 @Slf4j 46 @Slf4j
55 @TbCoreComponent 47 @TbCoreComponent
@@ -60,9 +52,6 @@ public class DefaultTbLocalSubscriptionService implements TbLocalSubscriptionSer @@ -60,9 +52,6 @@ public class DefaultTbLocalSubscriptionService implements TbLocalSubscriptionSer
60 private final Map<String, Map<Integer, TbSubscription>> subscriptionsBySessionId = new ConcurrentHashMap<>(); 52 private final Map<String, Map<Integer, TbSubscription>> subscriptionsBySessionId = new ConcurrentHashMap<>();
61 53
62 @Autowired 54 @Autowired
63 - private EntityViewService entityViewService;  
64 -  
65 - @Autowired  
66 private PartitionService partitionService; 55 private PartitionService partitionService;
67 56
68 @Autowired 57 @Autowired
@@ -72,17 +61,17 @@ public class DefaultTbLocalSubscriptionService implements TbLocalSubscriptionSer @@ -72,17 +61,17 @@ public class DefaultTbLocalSubscriptionService implements TbLocalSubscriptionSer
72 @Lazy 61 @Lazy
73 private SubscriptionManagerService subscriptionManagerService; 62 private SubscriptionManagerService subscriptionManagerService;
74 63
75 - private ExecutorService wsCallBackExecutor; 64 + private ExecutorService subscriptionUpdateExecutor;
76 65
77 @PostConstruct 66 @PostConstruct
78 public void initExecutor() { 67 public void initExecutor() {
79 - wsCallBackExecutor = Executors.newSingleThreadExecutor(ThingsBoardThreadFactory.forName("ws-sub-callback")); 68 + subscriptionUpdateExecutor = Executors.newWorkStealingPool(20);
80 } 69 }
81 70
82 @PreDestroy 71 @PreDestroy
83 public void shutdownExecutor() { 72 public void shutdownExecutor() {
84 - if (wsCallBackExecutor != null) {  
85 - wsCallBackExecutor.shutdownNow(); 73 + if (subscriptionUpdateExecutor != null) {
  74 + subscriptionUpdateExecutor.shutdownNow();
86 } 75 }
87 } 76 }
88 77
@@ -148,7 +137,7 @@ public class DefaultTbLocalSubscriptionService implements TbLocalSubscriptionSer @@ -148,7 +137,7 @@ public class DefaultTbLocalSubscriptionService implements TbLocalSubscriptionSer
148 update.getLatestValues().forEach((key, value) -> attrSub.getKeyStates().put(key, value)); 137 update.getLatestValues().forEach((key, value) -> attrSub.getKeyStates().put(key, value));
149 break; 138 break;
150 } 139 }
151 - subscription.getUpdateConsumer().accept(sessionId, update); 140 + subscriptionUpdateExecutor.submit(() -> subscription.getUpdateConsumer().accept(sessionId, update));
152 } 141 }
153 callback.onSuccess(); 142 callback.onSuccess();
154 } 143 }
@@ -158,7 +147,7 @@ public class DefaultTbLocalSubscriptionService implements TbLocalSubscriptionSer @@ -158,7 +147,7 @@ public class DefaultTbLocalSubscriptionService implements TbLocalSubscriptionSer
158 TbSubscription subscription = subscriptionsBySessionId 147 TbSubscription subscription = subscriptionsBySessionId
159 .getOrDefault(sessionId, Collections.emptyMap()).get(update.getSubscriptionId()); 148 .getOrDefault(sessionId, Collections.emptyMap()).get(update.getSubscriptionId());
160 if (subscription != null && subscription.getType() == TbSubscriptionType.ALARMS) { 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 callback.onSuccess(); 152 callback.onSuccess();
164 } 153 }
@@ -25,6 +25,7 @@ import org.thingsboard.common.util.ThingsBoardThreadFactory; @@ -25,6 +25,7 @@ import org.thingsboard.common.util.ThingsBoardThreadFactory;
25 import org.thingsboard.server.common.data.ApiUsageRecordKey; 25 import org.thingsboard.server.common.data.ApiUsageRecordKey;
26 import org.thingsboard.server.common.data.EntityType; 26 import org.thingsboard.server.common.data.EntityType;
27 import org.thingsboard.server.common.data.EntityView; 27 import org.thingsboard.server.common.data.EntityView;
  28 +import org.thingsboard.server.common.data.TenantProfile;
28 import org.thingsboard.server.common.data.id.EntityId; 29 import org.thingsboard.server.common.data.id.EntityId;
29 import org.thingsboard.server.common.data.id.TenantId; 30 import org.thingsboard.server.common.data.id.TenantId;
30 import org.thingsboard.server.common.data.kv.AttributeKvEntry; 31 import org.thingsboard.server.common.data.kv.AttributeKvEntry;
@@ -34,13 +35,14 @@ import org.thingsboard.server.common.data.kv.DoubleDataEntry; @@ -34,13 +35,14 @@ import org.thingsboard.server.common.data.kv.DoubleDataEntry;
34 import org.thingsboard.server.common.data.kv.LongDataEntry; 35 import org.thingsboard.server.common.data.kv.LongDataEntry;
35 import org.thingsboard.server.common.data.kv.StringDataEntry; 36 import org.thingsboard.server.common.data.kv.StringDataEntry;
36 import org.thingsboard.server.common.data.kv.TsKvEntry; 37 import org.thingsboard.server.common.data.kv.TsKvEntry;
  38 +import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration;
37 import org.thingsboard.server.common.msg.queue.ServiceType; 39 import org.thingsboard.server.common.msg.queue.ServiceType;
38 import org.thingsboard.server.common.msg.queue.TbCallback; 40 import org.thingsboard.server.common.msg.queue.TbCallback;
39 import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; 41 import org.thingsboard.server.common.msg.queue.TopicPartitionInfo;
40 import org.thingsboard.server.dao.attributes.AttributesService; 42 import org.thingsboard.server.dao.attributes.AttributesService;
41 import org.thingsboard.server.dao.entityview.EntityViewService; 43 import org.thingsboard.server.dao.entityview.EntityViewService;
  44 +import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
42 import org.thingsboard.server.dao.timeseries.TimeseriesService; 45 import org.thingsboard.server.dao.timeseries.TimeseriesService;
43 -import org.thingsboard.server.dao.usagerecord.ApiUsageStateService;  
44 import org.thingsboard.server.gen.transport.TransportProtos; 46 import org.thingsboard.server.gen.transport.TransportProtos;
45 import org.thingsboard.server.queue.discovery.PartitionService; 47 import org.thingsboard.server.queue.discovery.PartitionService;
46 import org.thingsboard.server.queue.usagestats.TbApiUsageClient; 48 import org.thingsboard.server.queue.usagestats.TbApiUsageClient;
@@ -119,11 +121,12 @@ public class DefaultTelemetrySubscriptionService extends AbstractSubscriptionSer @@ -119,11 +121,12 @@ public class DefaultTelemetrySubscriptionService extends AbstractSubscriptionSer
119 @Override 121 @Override
120 public void saveAndNotify(TenantId tenantId, EntityId entityId, List<TsKvEntry> ts, long ttl, FutureCallback<Void> callback) { 122 public void saveAndNotify(TenantId tenantId, EntityId entityId, List<TsKvEntry> ts, long ttl, FutureCallback<Void> callback) {
121 checkInternalEntity(entityId); 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 saveAndNotifyInternal(tenantId, entityId, ts, ttl, new FutureCallback<Integer>() { 126 saveAndNotifyInternal(tenantId, entityId, ts, ttl, new FutureCallback<Integer>() {
124 @Override 127 @Override
125 public void onSuccess(Integer result) { 128 public void onSuccess(Integer result) {
126 - if (result != null && result > 0) { 129 + if (!sysTenant && result != null && result > 0) {
127 apiUsageClient.report(tenantId, ApiUsageRecordKey.STORAGE_DP_COUNT, result); 130 apiUsageClient.report(tenantId, ApiUsageRecordKey.STORAGE_DP_COUNT, result);
128 } 131 }
129 callback.onSuccess(null); 132 callback.onSuccess(null);
@@ -134,7 +137,7 @@ public class DefaultTelemetrySubscriptionService extends AbstractSubscriptionSer @@ -134,7 +137,7 @@ public class DefaultTelemetrySubscriptionService extends AbstractSubscriptionSer
134 callback.onFailure(t); 137 callback.onFailure(t);
135 } 138 }
136 }); 139 });
137 - } else{ 140 + } else {
138 callback.onFailure(new RuntimeException("DB storage writes are disabled due to API limits!")); 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,12 +54,16 @@ import org.thingsboard.server.dao.device.provision.ProvisionResponse;
54 import org.thingsboard.server.dao.relation.RelationService; 54 import org.thingsboard.server.dao.relation.RelationService;
55 import org.thingsboard.server.dao.util.mapping.JacksonUtil; 55 import org.thingsboard.server.dao.util.mapping.JacksonUtil;
56 import org.thingsboard.server.gen.transport.TransportProtos; 56 import org.thingsboard.server.gen.transport.TransportProtos;
  57 +import org.thingsboard.server.gen.transport.TransportProtos.DeviceCredentialsProto;
57 import org.thingsboard.server.gen.transport.TransportProtos.DeviceInfoProto; 58 import org.thingsboard.server.gen.transport.TransportProtos.DeviceInfoProto;
58 import org.thingsboard.server.gen.transport.TransportProtos.GetOrCreateDeviceFromGatewayRequestMsg; 59 import org.thingsboard.server.gen.transport.TransportProtos.GetOrCreateDeviceFromGatewayRequestMsg;
59 import org.thingsboard.server.gen.transport.TransportProtos.GetOrCreateDeviceFromGatewayResponseMsg; 60 import org.thingsboard.server.gen.transport.TransportProtos.GetOrCreateDeviceFromGatewayResponseMsg;
60 import org.thingsboard.server.gen.transport.TransportProtos.GetEntityProfileRequestMsg; 61 import org.thingsboard.server.gen.transport.TransportProtos.GetEntityProfileRequestMsg;
61 import org.thingsboard.server.gen.transport.TransportProtos.GetEntityProfileResponseMsg; 62 import org.thingsboard.server.gen.transport.TransportProtos.GetEntityProfileResponseMsg;
62 import org.thingsboard.server.gen.transport.TransportProtos.ProvisionDeviceRequestMsg; 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 import org.thingsboard.server.gen.transport.TransportProtos.TransportApiRequestMsg; 67 import org.thingsboard.server.gen.transport.TransportProtos.TransportApiRequestMsg;
64 import org.thingsboard.server.gen.transport.TransportProtos.TransportApiResponseMsg; 68 import org.thingsboard.server.gen.transport.TransportProtos.TransportApiResponseMsg;
65 import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceCredentialsResponseMsg; 69 import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceCredentialsResponseMsg;
@@ -174,6 +178,8 @@ public class DefaultTransportApiService implements TransportApiService { @@ -174,6 +178,8 @@ public class DefaultTransportApiService implements TransportApiService {
174 if (!checkMqttCredentials(mqtt, credentials)) { 178 if (!checkMqttCredentials(mqtt, credentials)) {
175 credentials = null; 179 credentials = null;
176 } 180 }
  181 + } else {
  182 + return getEmptyTransportApiResponseFuture();
177 } 183 }
178 } 184 }
179 if (credentials == null) { 185 if (credentials == null) {
@@ -292,30 +298,32 @@ public class DefaultTransportApiService implements TransportApiService { @@ -292,30 +298,32 @@ public class DefaultTransportApiService implements TransportApiService {
292 requestMsg.getProvisionDeviceCredentialsMsg().getProvisionDeviceSecret())))); 298 requestMsg.getProvisionDeviceCredentialsMsg().getProvisionDeviceSecret()))));
293 } catch (ProvisionFailedException e) { 299 } catch (ProvisionFailedException e) {
294 return Futures.immediateFuture(getTransportApiResponseMsg( 300 return Futures.immediateFuture(getTransportApiResponseMsg(
295 - TransportProtos.DeviceCredentialsProto.getDefaultInstance(), 301 + new DeviceCredentials(),
296 TransportProtos.ProvisionResponseStatus.valueOf(e.getMessage()))); 302 TransportProtos.ProvisionResponseStatus.valueOf(e.getMessage())));
297 } 303 }
298 - return Futures.transform(provisionResponseFuture, provisionResponse -> getTransportApiResponseMsg(  
299 - getDeviceCredentials(provisionResponse.getDeviceCredentials()), TransportProtos.ProvisionResponseStatus.SUCCESS), 304 + return Futures.transform(provisionResponseFuture, provisionResponse -> getTransportApiResponseMsg(provisionResponse.getDeviceCredentials(), TransportProtos.ProvisionResponseStatus.SUCCESS),
300 dbCallbackExecutorService); 305 dbCallbackExecutorService);
301 } 306 }
302 307
303 - private TransportApiResponseMsg getTransportApiResponseMsg(TransportProtos.DeviceCredentialsProto deviceCredentials, TransportProtos.ProvisionResponseStatus status) {  
304 - return TransportApiResponseMsg.newBuilder()  
305 - .setProvisionDeviceResponseMsg(TransportProtos.ProvisionDeviceResponseMsg.newBuilder()  
306 - .setDeviceCredentials(deviceCredentials)  
307 - .setProvisionResponseStatus(status)  
308 - .build())  
309 - .build();  
310 - } 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 + }
311 324
312 - private TransportProtos.DeviceCredentialsProto getDeviceCredentials(DeviceCredentials deviceCredentials) {  
313 - return TransportProtos.DeviceCredentialsProto.newBuilder()  
314 - .setDeviceIdMSB(deviceCredentials.getDeviceId().getId().getMostSignificantBits())  
315 - .setDeviceIdLSB(deviceCredentials.getDeviceId().getId().getLeastSignificantBits())  
316 - .setCredentialsType(TransportProtos.CredentialsType.valueOf(deviceCredentials.getCredentialsType().name()))  
317 - .setCredentialsId(deviceCredentials.getCredentialsId())  
318 - .setCredentialsValue(deviceCredentials.getCredentialsValue() != null ? deviceCredentials.getCredentialsValue() : "") 325 + return TransportApiResponseMsg.newBuilder()
  326 + .setProvisionDeviceResponseMsg(provisionResponse.build())
319 .build(); 327 .build();
320 } 328 }
321 329
@@ -3,4 +3,5 @@ activation.subject=Your account activation on Thingsboard @@ -3,4 +3,5 @@ activation.subject=Your account activation on Thingsboard
3 account.activated.subject=Thingsboard - your account has been activated 3 account.activated.subject=Thingsboard - your account has been activated
4 reset.password.subject=Thingsboard - Password reset has been requested 4 reset.password.subject=Thingsboard - Password reset has been requested
5 password.was.reset.subject=Thingsboard - your account password has been reset 5 password.was.reset.subject=Thingsboard - your account password has been reset
6 -account.lockout.subject=Thingsboard - User account has been lockout  
  6 +account.lockout.subject=Thingsboard - User account has been lockout
  7 +api.usage.state=Thingsboard - Account limits
  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>
@@ -64,9 +64,9 @@ server: @@ -64,9 +64,9 @@ server:
64 # Minimum value of the server side RPC timeout. May override value provided in the REST API call. 64 # Minimum value of the server side RPC timeout. May override value provided in the REST API call.
65 # Since 2.5 migration to queues, the RPC delay depends on the size of the pending messages in the queue, 65 # Since 2.5 migration to queues, the RPC delay depends on the size of the pending messages in the queue,
66 # so default UI parameter of 500ms may not be sufficient for loaded environments. 66 # so default UI parameter of 500ms may not be sufficient for loaded environments.
67 - min_timeout: "${MIN_SERVER_SIDE_RPC_TIMEOUT:5000}" 67 + min_timeout: "${MIN_SERVER_SIDE_RPC_TIMEOUT:5000}"
68 # Default value of the server side RPC timeout. 68 # Default value of the server side RPC timeout.
69 - default_timeout: "${DEFAULT_SERVER_SIDE_RPC_TIMEOUT:10000}" 69 + default_timeout: "${DEFAULT_SERVER_SIDE_RPC_TIMEOUT:10000}"
70 70
71 # Zookeeper connection parameters. Used for service discovery. 71 # Zookeeper connection parameters. Used for service discovery.
72 zk: 72 zk:
@@ -281,8 +281,12 @@ actors: @@ -281,8 +281,12 @@ actors:
281 js_thread_pool_size: "${ACTORS_RULE_JS_THREAD_POOL_SIZE:50}" 281 js_thread_pool_size: "${ACTORS_RULE_JS_THREAD_POOL_SIZE:50}"
282 # Specify thread pool size for mail sender executor service 282 # Specify thread pool size for mail sender executor service
283 mail_thread_pool_size: "${ACTORS_RULE_MAIL_THREAD_POOL_SIZE:50}" 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 # Whether to allow usage of system mail service for rules 286 # Whether to allow usage of system mail service for rules
285 allow_system_mail_service: "${ACTORS_RULE_ALLOW_SYSTEM_MAIL_SERVICE:true}" 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 # Specify thread pool size for external call service 290 # Specify thread pool size for external call service
287 external_call_thread_pool_size: "${ACTORS_RULE_EXTERNAL_CALL_THREAD_POOL_SIZE:50}" 291 external_call_thread_pool_size: "${ACTORS_RULE_EXTERNAL_CALL_THREAD_POOL_SIZE:50}"
288 chain: 292 chain:
@@ -518,7 +522,7 @@ transport: @@ -518,7 +522,7 @@ transport:
518 # Maximum allowed string value length when processing Telemetry/Attributes JSON (0 value disables string value length check) 522 # Maximum allowed string value length when processing Telemetry/Attributes JSON (0 value disables string value length check)
519 max_string_value_length: "${JSON_MAX_STRING_VALUE_LENGTH:0}" 523 max_string_value_length: "${JSON_MAX_STRING_VALUE_LENGTH:0}"
520 client_side_rpc: 524 client_side_rpc:
521 - timeout: "${CLIENT_SIDE_RPC_TIMEOUT:60000}" 525 + timeout: "${CLIENT_SIDE_RPC_TIMEOUT:60000}"
522 # Enable/disable http/mqtt/coap transport protocols (has higher priority than certain protocol's 'enabled' property) 526 # Enable/disable http/mqtt/coap transport protocols (has higher priority than certain protocol's 'enabled' property)
523 api_enabled: "${TB_TRANSPORT_API_ENABLED:true}" 527 api_enabled: "${TB_TRANSPORT_API_ENABLED:true}"
524 # Local HTTP transport parameters 528 # Local HTTP transport parameters
@@ -591,6 +595,7 @@ queue: @@ -591,6 +595,7 @@ queue:
591 linger.ms: "${TB_KAFKA_LINGER_MS:1}" 595 linger.ms: "${TB_KAFKA_LINGER_MS:1}"
592 buffer.memory: "${TB_BUFFER_MEMORY:33554432}" 596 buffer.memory: "${TB_BUFFER_MEMORY:33554432}"
593 replication_factor: "${TB_QUEUE_KAFKA_REPLICATION_FACTOR:1}" 597 replication_factor: "${TB_QUEUE_KAFKA_REPLICATION_FACTOR:1}"
  598 + max_poll_interval_ms: "${TB_QUEUE_KAFKA_MAX_POLL_INTERVAL_MS:300000}"
594 max_poll_records: "${TB_QUEUE_KAFKA_MAX_POLL_RECORDS:8192}" 599 max_poll_records: "${TB_QUEUE_KAFKA_MAX_POLL_RECORDS:8192}"
595 max_partition_fetch_bytes: "${TB_QUEUE_KAFKA_MAX_PARTITION_FETCH_BYTES:16777216}" 600 max_partition_fetch_bytes: "${TB_QUEUE_KAFKA_MAX_PARTITION_FETCH_BYTES:16777216}"
596 fetch_max_bytes: "${TB_QUEUE_KAFKA_FETCH_MAX_BYTES:134217728}" 601 fetch_max_bytes: "${TB_QUEUE_KAFKA_FETCH_MAX_BYTES:134217728}"
@@ -690,8 +695,6 @@ queue: @@ -690,8 +695,6 @@ queue:
690 max_requests_timeout: "${REMOTE_JS_MAX_REQUEST_TIMEOUT:10000}" 695 max_requests_timeout: "${REMOTE_JS_MAX_REQUEST_TIMEOUT:10000}"
691 # JS response poll interval 696 # JS response poll interval
692 response_poll_interval: "${REMOTE_JS_RESPONSE_POLL_INTERVAL_MS:25}" 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 rule-engine: 698 rule-engine:
696 topic: "${TB_QUEUE_RULE_ENGINE_TOPIC:tb_rule_engine}" 699 topic: "${TB_QUEUE_RULE_ENGINE_TOPIC:tb_rule_engine}"
697 poll-interval: "${TB_QUEUE_RULE_ENGINE_POLL_INTERVAL_MS:25}" 700 poll-interval: "${TB_QUEUE_RULE_ENGINE_POLL_INTERVAL_MS:25}"
@@ -15,7 +15,6 @@ @@ -15,7 +15,6 @@
15 */ 15 */
16 package org.thingsboard.server.controller; 16 package org.thingsboard.server.controller;
17 17
18 -import com.datastax.oss.driver.api.core.uuid.Uuids;  
19 import com.fasterxml.jackson.core.type.TypeReference; 18 import com.fasterxml.jackson.core.type.TypeReference;
20 import com.fasterxml.jackson.databind.JsonNode; 19 import com.fasterxml.jackson.databind.JsonNode;
21 import com.fasterxml.jackson.databind.ObjectMapper; 20 import com.fasterxml.jackson.databind.ObjectMapper;
@@ -33,12 +32,7 @@ import org.junit.Rule; @@ -33,12 +32,7 @@ import org.junit.Rule;
33 import org.junit.rules.TestRule; 32 import org.junit.rules.TestRule;
34 import org.junit.rules.TestWatcher; 33 import org.junit.rules.TestWatcher;
35 import org.junit.runner.Description; 34 import org.junit.runner.Description;
36 -import org.junit.runner.RunWith;  
37 import org.springframework.beans.factory.annotation.Autowired; 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 import org.springframework.http.HttpHeaders; 36 import org.springframework.http.HttpHeaders;
43 import org.springframework.http.MediaType; 37 import org.springframework.http.MediaType;
44 import org.springframework.http.converter.HttpMessageConverter; 38 import org.springframework.http.converter.HttpMessageConverter;
@@ -46,10 +40,6 @@ import org.springframework.http.converter.StringHttpMessageConverter; @@ -46,10 +40,6 @@ import org.springframework.http.converter.StringHttpMessageConverter;
46 import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; 40 import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
47 import org.springframework.mock.http.MockHttpInputMessage; 41 import org.springframework.mock.http.MockHttpInputMessage;
48 import org.springframework.mock.http.MockHttpOutputMessage; 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 import org.springframework.test.web.servlet.MockMvc; 43 import org.springframework.test.web.servlet.MockMvc;
54 import org.springframework.test.web.servlet.MvcResult; 44 import org.springframework.test.web.servlet.MvcResult;
55 import org.springframework.test.web.servlet.ResultActions; 45 import org.springframework.test.web.servlet.ResultActions;
@@ -58,7 +48,6 @@ import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilde @@ -58,7 +48,6 @@ import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilde
58 import org.springframework.util.LinkedMultiValueMap; 48 import org.springframework.util.LinkedMultiValueMap;
59 import org.springframework.util.MultiValueMap; 49 import org.springframework.util.MultiValueMap;
60 import org.springframework.web.context.WebApplicationContext; 50 import org.springframework.web.context.WebApplicationContext;
61 -import org.thingsboard.server.common.data.BaseData;  
62 import org.thingsboard.server.common.data.Customer; 51 import org.thingsboard.server.common.data.Customer;
63 import org.thingsboard.server.common.data.DeviceProfile; 52 import org.thingsboard.server.common.data.DeviceProfile;
64 import org.thingsboard.server.common.data.DeviceProfileType; 53 import org.thingsboard.server.common.data.DeviceProfileType;
@@ -68,11 +57,13 @@ import org.thingsboard.server.common.data.User; @@ -68,11 +57,13 @@ import org.thingsboard.server.common.data.User;
68 import org.thingsboard.server.common.data.device.profile.DefaultDeviceProfileConfiguration; 57 import org.thingsboard.server.common.data.device.profile.DefaultDeviceProfileConfiguration;
69 import org.thingsboard.server.common.data.device.profile.DefaultDeviceProfileTransportConfiguration; 58 import org.thingsboard.server.common.data.device.profile.DefaultDeviceProfileTransportConfiguration;
70 import org.thingsboard.server.common.data.device.profile.DeviceProfileData; 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 import org.thingsboard.server.common.data.id.HasId; 65 import org.thingsboard.server.common.data.id.HasId;
73 -import org.thingsboard.server.common.data.id.RuleChainId;  
74 import org.thingsboard.server.common.data.id.TenantId; 66 import org.thingsboard.server.common.data.id.TenantId;
75 -import org.thingsboard.server.common.data.id.UUIDBased;  
76 import org.thingsboard.server.common.data.page.PageLink; 67 import org.thingsboard.server.common.data.page.PageLink;
77 import org.thingsboard.server.common.data.page.TimePageLink; 68 import org.thingsboard.server.common.data.page.TimePageLink;
78 import org.thingsboard.server.common.data.security.Authority; 69 import org.thingsboard.server.common.data.security.Authority;
@@ -330,7 +321,7 @@ public abstract class AbstractWebTest { @@ -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 DeviceProfile deviceProfile = new DeviceProfile(); 325 DeviceProfile deviceProfile = new DeviceProfile();
335 deviceProfile.setName(name); 326 deviceProfile.setName(name);
336 deviceProfile.setType(DeviceProfileType.DEFAULT); 327 deviceProfile.setType(DeviceProfileType.DEFAULT);
@@ -338,15 +329,34 @@ public abstract class AbstractWebTest { @@ -338,15 +329,34 @@ public abstract class AbstractWebTest {
338 deviceProfile.setDescription(name + " Test"); 329 deviceProfile.setDescription(name + " Test");
339 DeviceProfileData deviceProfileData = new DeviceProfileData(); 330 DeviceProfileData deviceProfileData = new DeviceProfileData();
340 DefaultDeviceProfileConfiguration configuration = new DefaultDeviceProfileConfiguration(); 331 DefaultDeviceProfileConfiguration configuration = new DefaultDeviceProfileConfiguration();
341 - DefaultDeviceProfileTransportConfiguration transportConfiguration = new DefaultDeviceProfileTransportConfiguration();  
342 deviceProfileData.setConfiguration(configuration); 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 deviceProfile.setProfileData(deviceProfileData); 338 deviceProfile.setProfileData(deviceProfileData);
345 deviceProfile.setDefault(false); 339 deviceProfile.setDefault(false);
346 deviceProfile.setDefaultRuleChainId(null); 340 deviceProfile.setDefaultRuleChainId(null);
347 return deviceProfile; 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 protected ResultActions doGet(String urlTemplate, Object... urlVariables) throws Exception { 360 protected ResultActions doGet(String urlTemplate, Object... urlVariables) throws Exception {
351 MockHttpServletRequestBuilder getRequest = get(urlTemplate, urlVariables); 361 MockHttpServletRequestBuilder getRequest = get(urlTemplate, urlVariables);
352 setJwtToken(getRequest); 362 setJwtToken(getRequest);
@@ -16,6 +16,12 @@ @@ -16,6 +16,12 @@
16 package org.thingsboard.server.controller; 16 package org.thingsboard.server.controller;
17 17
18 import com.fasterxml.jackson.core.type.TypeReference; 18 import com.fasterxml.jackson.core.type.TypeReference;
  19 +import com.github.os72.protobuf.dynamic.DynamicSchema;
  20 +import com.google.protobuf.Descriptors;
  21 +import com.google.protobuf.DynamicMessage;
  22 +import com.google.protobuf.InvalidProtocolBufferException;
  23 +import com.google.protobuf.util.JsonFormat;
  24 +import com.squareup.wire.schema.internal.parser.ProtoFileElement;
19 import org.junit.After; 25 import org.junit.After;
20 import org.junit.Assert; 26 import org.junit.Assert;
21 import org.junit.Before; 27 import org.junit.Before;
@@ -28,7 +34,10 @@ import org.thingsboard.server.common.data.DeviceProfileType; @@ -28,7 +34,10 @@ import org.thingsboard.server.common.data.DeviceProfileType;
28 import org.thingsboard.server.common.data.DeviceTransportType; 34 import org.thingsboard.server.common.data.DeviceTransportType;
29 import org.thingsboard.server.common.data.Tenant; 35 import org.thingsboard.server.common.data.Tenant;
30 import org.thingsboard.server.common.data.User; 36 import org.thingsboard.server.common.data.User;
31 -import org.thingsboard.server.common.data.device.profile.ProvisionDeviceProfileCredentials; 37 +import org.thingsboard.server.common.data.device.profile.DeviceProfileTransportConfiguration;
  38 +import org.thingsboard.server.common.data.device.profile.MqttDeviceProfileTransportConfiguration;
  39 +import org.thingsboard.server.common.data.device.profile.ProtoTransportPayloadConfiguration;
  40 +import org.thingsboard.server.common.data.device.profile.TransportPayloadTypeConfiguration;
32 import org.thingsboard.server.common.data.page.PageData; 41 import org.thingsboard.server.common.data.page.PageData;
33 import org.thingsboard.server.common.data.page.PageLink; 42 import org.thingsboard.server.common.data.page.PageLink;
34 import org.thingsboard.server.common.data.security.Authority; 43 import org.thingsboard.server.common.data.security.Authority;
@@ -36,9 +45,13 @@ import org.thingsboard.server.common.data.security.Authority; @@ -36,9 +45,13 @@ import org.thingsboard.server.common.data.security.Authority;
36 import java.util.ArrayList; 45 import java.util.ArrayList;
37 import java.util.Collections; 46 import java.util.Collections;
38 import java.util.List; 47 import java.util.List;
  48 +import java.util.Set;
39 import java.util.stream.Collectors; 49 import java.util.stream.Collectors;
40 50
41 import static org.hamcrest.Matchers.containsString; 51 import static org.hamcrest.Matchers.containsString;
  52 +import static org.junit.Assert.assertEquals;
  53 +import static org.junit.Assert.assertNotNull;
  54 +import static org.junit.Assert.assertTrue;
42 import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; 55 import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
43 56
44 public abstract class BaseDeviceProfileControllerTest extends AbstractControllerTest { 57 public abstract class BaseDeviceProfileControllerTest extends AbstractControllerTest {
@@ -78,7 +91,7 @@ public abstract class BaseDeviceProfileControllerTest extends AbstractController @@ -78,7 +91,7 @@ public abstract class BaseDeviceProfileControllerTest extends AbstractController
78 91
79 @Test 92 @Test
80 public void testSaveDeviceProfile() throws Exception { 93 public void testSaveDeviceProfile() throws Exception {
81 - DeviceProfile deviceProfile = this.createDeviceProfile("Device Profile"); 94 + DeviceProfile deviceProfile = this.createDeviceProfile("Device Profile", null);
82 DeviceProfile savedDeviceProfile = doPost("/api/deviceProfile", deviceProfile, DeviceProfile.class); 95 DeviceProfile savedDeviceProfile = doPost("/api/deviceProfile", deviceProfile, DeviceProfile.class);
83 Assert.assertNotNull(savedDeviceProfile); 96 Assert.assertNotNull(savedDeviceProfile);
84 Assert.assertNotNull(savedDeviceProfile.getId()); 97 Assert.assertNotNull(savedDeviceProfile.getId());
@@ -96,7 +109,7 @@ public abstract class BaseDeviceProfileControllerTest extends AbstractController @@ -96,7 +109,7 @@ public abstract class BaseDeviceProfileControllerTest extends AbstractController
96 109
97 @Test 110 @Test
98 public void testFindDeviceProfileById() throws Exception { 111 public void testFindDeviceProfileById() throws Exception {
99 - DeviceProfile deviceProfile = this.createDeviceProfile("Device Profile"); 112 + DeviceProfile deviceProfile = this.createDeviceProfile("Device Profile", null);
100 DeviceProfile savedDeviceProfile = doPost("/api/deviceProfile", deviceProfile, DeviceProfile.class); 113 DeviceProfile savedDeviceProfile = doPost("/api/deviceProfile", deviceProfile, DeviceProfile.class);
101 DeviceProfile foundDeviceProfile = doGet("/api/deviceProfile/"+savedDeviceProfile.getId().getId().toString(), DeviceProfile.class); 114 DeviceProfile foundDeviceProfile = doGet("/api/deviceProfile/"+savedDeviceProfile.getId().getId().toString(), DeviceProfile.class);
102 Assert.assertNotNull(foundDeviceProfile); 115 Assert.assertNotNull(foundDeviceProfile);
@@ -105,7 +118,7 @@ public abstract class BaseDeviceProfileControllerTest extends AbstractController @@ -105,7 +118,7 @@ public abstract class BaseDeviceProfileControllerTest extends AbstractController
105 118
106 @Test 119 @Test
107 public void testFindDeviceProfileInfoById() throws Exception { 120 public void testFindDeviceProfileInfoById() throws Exception {
108 - DeviceProfile deviceProfile = this.createDeviceProfile("Device Profile"); 121 + DeviceProfile deviceProfile = this.createDeviceProfile("Device Profile", null);
109 DeviceProfile savedDeviceProfile = doPost("/api/deviceProfile", deviceProfile, DeviceProfile.class); 122 DeviceProfile savedDeviceProfile = doPost("/api/deviceProfile", deviceProfile, DeviceProfile.class);
110 DeviceProfileInfo foundDeviceProfileInfo = doGet("/api/deviceProfileInfo/"+savedDeviceProfile.getId().getId().toString(), DeviceProfileInfo.class); 123 DeviceProfileInfo foundDeviceProfileInfo = doGet("/api/deviceProfileInfo/"+savedDeviceProfile.getId().getId().toString(), DeviceProfileInfo.class);
111 Assert.assertNotNull(foundDeviceProfileInfo); 124 Assert.assertNotNull(foundDeviceProfileInfo);
@@ -127,7 +140,7 @@ public abstract class BaseDeviceProfileControllerTest extends AbstractController @@ -127,7 +140,7 @@ public abstract class BaseDeviceProfileControllerTest extends AbstractController
127 140
128 @Test 141 @Test
129 public void testSetDefaultDeviceProfile() throws Exception { 142 public void testSetDefaultDeviceProfile() throws Exception {
130 - DeviceProfile deviceProfile = this.createDeviceProfile("Device Profile 1"); 143 + DeviceProfile deviceProfile = this.createDeviceProfile("Device Profile 1", null);
131 DeviceProfile savedDeviceProfile = doPost("/api/deviceProfile", deviceProfile, DeviceProfile.class); 144 DeviceProfile savedDeviceProfile = doPost("/api/deviceProfile", deviceProfile, DeviceProfile.class);
132 DeviceProfile defaultDeviceProfile = doPost("/api/deviceProfile/"+savedDeviceProfile.getId().getId().toString()+"/default", null, DeviceProfile.class); 145 DeviceProfile defaultDeviceProfile = doPost("/api/deviceProfile/"+savedDeviceProfile.getId().getId().toString()+"/default", null, DeviceProfile.class);
133 Assert.assertNotNull(defaultDeviceProfile); 146 Assert.assertNotNull(defaultDeviceProfile);
@@ -147,19 +160,19 @@ public abstract class BaseDeviceProfileControllerTest extends AbstractController @@ -147,19 +160,19 @@ public abstract class BaseDeviceProfileControllerTest extends AbstractController
147 160
148 @Test 161 @Test
149 public void testSaveDeviceProfileWithSameName() throws Exception { 162 public void testSaveDeviceProfileWithSameName() throws Exception {
150 - DeviceProfile deviceProfile = this.createDeviceProfile("Device Profile"); 163 + DeviceProfile deviceProfile = this.createDeviceProfile("Device Profile", null);
151 doPost("/api/deviceProfile", deviceProfile).andExpect(status().isOk()); 164 doPost("/api/deviceProfile", deviceProfile).andExpect(status().isOk());
152 - DeviceProfile deviceProfile2 = this.createDeviceProfile("Device Profile"); 165 + DeviceProfile deviceProfile2 = this.createDeviceProfile("Device Profile", null);
153 doPost("/api/deviceProfile", deviceProfile2).andExpect(status().isBadRequest()) 166 doPost("/api/deviceProfile", deviceProfile2).andExpect(status().isBadRequest())
154 .andExpect(statusReason(containsString("Device profile with such name already exists"))); 167 .andExpect(statusReason(containsString("Device profile with such name already exists")));
155 } 168 }
156 169
157 @Test 170 @Test
158 public void testSaveDeviceProfileWithSameProvisionDeviceKey() throws Exception { 171 public void testSaveDeviceProfileWithSameProvisionDeviceKey() throws Exception {
159 - DeviceProfile deviceProfile = this.createDeviceProfile("Device Profile"); 172 + DeviceProfile deviceProfile = this.createDeviceProfile("Device Profile", null);
160 deviceProfile.setProvisionDeviceKey("testProvisionDeviceKey"); 173 deviceProfile.setProvisionDeviceKey("testProvisionDeviceKey");
161 doPost("/api/deviceProfile", deviceProfile).andExpect(status().isOk()); 174 doPost("/api/deviceProfile", deviceProfile).andExpect(status().isOk());
162 - DeviceProfile deviceProfile2 = this.createDeviceProfile("Device Profile 2"); 175 + DeviceProfile deviceProfile2 = this.createDeviceProfile("Device Profile 2", null);
163 deviceProfile2.setProvisionDeviceKey("testProvisionDeviceKey"); 176 deviceProfile2.setProvisionDeviceKey("testProvisionDeviceKey");
164 doPost("/api/deviceProfile", deviceProfile2).andExpect(status().isBadRequest()) 177 doPost("/api/deviceProfile", deviceProfile2).andExpect(status().isBadRequest())
165 .andExpect(statusReason(containsString("Device profile with such provision device key already exists"))); 178 .andExpect(statusReason(containsString("Device profile with such provision device key already exists")));
@@ -168,7 +181,7 @@ public abstract class BaseDeviceProfileControllerTest extends AbstractController @@ -168,7 +181,7 @@ public abstract class BaseDeviceProfileControllerTest extends AbstractController
168 @Ignore 181 @Ignore
169 @Test 182 @Test
170 public void testChangeDeviceProfileTypeWithExistingDevices() throws Exception { 183 public void testChangeDeviceProfileTypeWithExistingDevices() throws Exception {
171 - DeviceProfile deviceProfile = this.createDeviceProfile("Device Profile"); 184 + DeviceProfile deviceProfile = this.createDeviceProfile("Device Profile", null);
172 DeviceProfile savedDeviceProfile = doPost("/api/deviceProfile", deviceProfile, DeviceProfile.class); 185 DeviceProfile savedDeviceProfile = doPost("/api/deviceProfile", deviceProfile, DeviceProfile.class);
173 Device device = new Device(); 186 Device device = new Device();
174 device.setName("Test device"); 187 device.setName("Test device");
@@ -183,7 +196,7 @@ public abstract class BaseDeviceProfileControllerTest extends AbstractController @@ -183,7 +196,7 @@ public abstract class BaseDeviceProfileControllerTest extends AbstractController
183 196
184 @Test 197 @Test
185 public void testChangeDeviceProfileTransportTypeWithExistingDevices() throws Exception { 198 public void testChangeDeviceProfileTransportTypeWithExistingDevices() throws Exception {
186 - DeviceProfile deviceProfile = this.createDeviceProfile("Device Profile"); 199 + DeviceProfile deviceProfile = this.createDeviceProfile("Device Profile", null);
187 DeviceProfile savedDeviceProfile = doPost("/api/deviceProfile", deviceProfile, DeviceProfile.class); 200 DeviceProfile savedDeviceProfile = doPost("/api/deviceProfile", deviceProfile, DeviceProfile.class);
188 Device device = new Device(); 201 Device device = new Device();
189 device.setName("Test device"); 202 device.setName("Test device");
@@ -197,7 +210,7 @@ public abstract class BaseDeviceProfileControllerTest extends AbstractController @@ -197,7 +210,7 @@ public abstract class BaseDeviceProfileControllerTest extends AbstractController
197 210
198 @Test 211 @Test
199 public void testDeleteDeviceProfileWithExistingDevice() throws Exception { 212 public void testDeleteDeviceProfileWithExistingDevice() throws Exception {
200 - DeviceProfile deviceProfile = this.createDeviceProfile("Device Profile"); 213 + DeviceProfile deviceProfile = this.createDeviceProfile("Device Profile", null);
201 DeviceProfile savedDeviceProfile = doPost("/api/deviceProfile", deviceProfile, DeviceProfile.class); 214 DeviceProfile savedDeviceProfile = doPost("/api/deviceProfile", deviceProfile, DeviceProfile.class);
202 215
203 Device device = new Device(); 216 Device device = new Device();
@@ -214,7 +227,7 @@ public abstract class BaseDeviceProfileControllerTest extends AbstractController @@ -214,7 +227,7 @@ public abstract class BaseDeviceProfileControllerTest extends AbstractController
214 227
215 @Test 228 @Test
216 public void testDeleteDeviceProfile() throws Exception { 229 public void testDeleteDeviceProfile() throws Exception {
217 - DeviceProfile deviceProfile = this.createDeviceProfile("Device Profile"); 230 + DeviceProfile deviceProfile = this.createDeviceProfile("Device Profile", null);
218 DeviceProfile savedDeviceProfile = doPost("/api/deviceProfile", deviceProfile, DeviceProfile.class); 231 DeviceProfile savedDeviceProfile = doPost("/api/deviceProfile", deviceProfile, DeviceProfile.class);
219 232
220 doDelete("/api/deviceProfile/" + savedDeviceProfile.getId().getId().toString()) 233 doDelete("/api/deviceProfile/" + savedDeviceProfile.getId().getId().toString())
@@ -235,7 +248,7 @@ public abstract class BaseDeviceProfileControllerTest extends AbstractController @@ -235,7 +248,7 @@ public abstract class BaseDeviceProfileControllerTest extends AbstractController
235 deviceProfiles.addAll(pageData.getData()); 248 deviceProfiles.addAll(pageData.getData());
236 249
237 for (int i=0;i<28;i++) { 250 for (int i=0;i<28;i++) {
238 - DeviceProfile deviceProfile = this.createDeviceProfile("Device Profile"+i); 251 + DeviceProfile deviceProfile = this.createDeviceProfile("Device Profile"+i, null);
239 deviceProfiles.add(doPost("/api/deviceProfile", deviceProfile, DeviceProfile.class)); 252 deviceProfiles.add(doPost("/api/deviceProfile", deviceProfile, DeviceProfile.class));
240 } 253 }
241 254
@@ -280,7 +293,7 @@ public abstract class BaseDeviceProfileControllerTest extends AbstractController @@ -280,7 +293,7 @@ public abstract class BaseDeviceProfileControllerTest extends AbstractController
280 deviceProfiles.addAll(deviceProfilePageData.getData()); 293 deviceProfiles.addAll(deviceProfilePageData.getData());
281 294
282 for (int i=0;i<28;i++) { 295 for (int i=0;i<28;i++) {
283 - DeviceProfile deviceProfile = this.createDeviceProfile("Device Profile"+i); 296 + DeviceProfile deviceProfile = this.createDeviceProfile("Device Profile"+i, null);
284 deviceProfiles.add(doPost("/api/deviceProfile", deviceProfile, DeviceProfile.class)); 297 deviceProfiles.add(doPost("/api/deviceProfile", deviceProfile, DeviceProfile.class));
285 } 298 }
286 299
@@ -318,4 +331,341 @@ public abstract class BaseDeviceProfileControllerTest extends AbstractController @@ -318,4 +331,341 @@ public abstract class BaseDeviceProfileControllerTest extends AbstractController
318 Assert.assertEquals(1, pageData.getTotalElements()); 331 Assert.assertEquals(1, pageData.getTotalElements());
319 } 332 }
320 333
  334 + @Test
  335 + public void testSaveProtoDeviceProfileWithInvalidProtoFile() throws Exception {
  336 + testSaveDeviceProfileWithInvalidProtoSchema("syntax = \"proto3\";\n" +
  337 + "\n" +
  338 + "package schemavalidation;\n" +
  339 + "\n" +
  340 + "message SchemaValidationTest {\n" +
  341 + " required int32 parameter = 1;\n" +
  342 + "}", "[Transport Configuration] failed to parse attributes proto schema due to: Syntax error in :6:4: 'required' label forbidden in proto3 field declarations");
  343 + }
  344 +
  345 + @Test
  346 + public void testSaveProtoDeviceProfileWithInvalidProtoSyntax() throws Exception {
  347 + testSaveDeviceProfileWithInvalidProtoSchema("syntax = \"proto2\";\n" +
  348 + "\n" +
  349 + "package schemavalidation;\n" +
  350 + "\n" +
  351 + "message SchemaValidationTest {\n" +
  352 + " required int32 parameter = 1;\n" +
  353 + "}", "[Transport Configuration] invalid schema syntax: proto2 for attributes proto schema provided! Only proto3 allowed!");
  354 + }
  355 +
  356 + @Test
  357 + public void testSaveProtoDeviceProfileOptionsNotSupported() throws Exception {
  358 + testSaveDeviceProfileWithInvalidProtoSchema("syntax = \"proto3\";\n" +
  359 + "\n" +
  360 + "option java_package = \"com.test.schemavalidation\";\n" +
  361 + "option java_multiple_files = true;\n" +
  362 + "\n" +
  363 + "package schemavalidation;\n" +
  364 + "\n" +
  365 + "message SchemaValidationTest {\n" +
  366 + " int32 parameter = 1;\n" +
  367 + "}", "[Transport Configuration] invalid attributes proto schema provided! Schema options don't support!");
  368 + }
  369 +
  370 + @Test
  371 + public void testSaveProtoDeviceProfilePublicImportsNotSupported() throws Exception {
  372 + testSaveDeviceProfileWithInvalidProtoSchema("syntax = \"proto3\";\n" +
  373 + "\n" +
  374 + "import public \"oldschema.proto\";\n" +
  375 + "\n" +
  376 + "package schemavalidation;\n" +
  377 + "\n" +
  378 + "message SchemaValidationTest {\n" +
  379 + " int32 parameter = 1;\n" +
  380 + "}", "[Transport Configuration] invalid attributes proto schema provided! Schema public imports don't support!");
  381 + }
  382 +
  383 + @Test
  384 + public void testSaveProtoDeviceProfileImportsNotSupported() throws Exception {
  385 + testSaveDeviceProfileWithInvalidProtoSchema("syntax = \"proto3\";\n" +
  386 + "\n" +
  387 + "import \"oldschema.proto\";\n" +
  388 + "\n" +
  389 + "package schemavalidation;\n" +
  390 + "\n" +
  391 + "message SchemaValidationTest {\n" +
  392 + " int32 parameter = 1;\n" +
  393 + "}", "[Transport Configuration] invalid attributes proto schema provided! Schema imports don't support!");
  394 + }
  395 +
  396 + @Test
  397 + public void testSaveProtoDeviceProfileExtendDeclarationsNotSupported() throws Exception {
  398 + testSaveDeviceProfileWithInvalidProtoSchema("syntax = \"proto3\";\n" +
  399 + "\n" +
  400 + "package schemavalidation;\n" +
  401 + "\n" +
  402 + "extend google.protobuf.MethodOptions {\n" +
  403 + " MyMessage my_method_option = 50007;\n" +
  404 + "}", "[Transport Configuration] invalid attributes proto schema provided! Schema extend declarations don't support!");
  405 + }
  406 +
  407 + @Test
  408 + public void testSaveProtoDeviceProfileEnumOptionsNotSupported() throws Exception {
  409 + testSaveDeviceProfileWithInvalidProtoSchema("syntax = \"proto3\";\n" +
  410 + "\n" +
  411 + "package schemavalidation;\n" +
  412 + "\n" +
  413 + "enum testEnum {\n" +
  414 + " option allow_alias = true;\n" +
  415 + " DEFAULT = 0;\n" +
  416 + " STARTED = 1;\n" +
  417 + " RUNNING = 2;\n" +
  418 + "}\n" +
  419 + "\n" +
  420 + "message testMessage {\n" +
  421 + " int32 parameter = 1;\n" +
  422 + "}", "[Transport Configuration] invalid attributes proto schema provided! Enum definitions options are not supported!");
  423 + }
  424 +
  425 + @Test
  426 + public void testSaveProtoDeviceProfileNoOneMessageTypeExists() throws Exception {
  427 + testSaveDeviceProfileWithInvalidProtoSchema("syntax = \"proto3\";\n" +
  428 + "\n" +
  429 + "package schemavalidation;\n" +
  430 + "\n" +
  431 + "enum testEnum {\n" +
  432 + " DEFAULT = 0;\n" +
  433 + " STARTED = 1;\n" +
  434 + " RUNNING = 2;\n" +
  435 + "}", "[Transport Configuration] invalid attributes proto schema provided! At least one Message definition should exists!");
  436 + }
  437 +
  438 + @Test
  439 + public void testSaveProtoDeviceProfileMessageTypeOptionsNotSupported() throws Exception {
  440 + testSaveDeviceProfileWithInvalidProtoSchema("syntax = \"proto3\";\n" +
  441 + "\n" +
  442 + "package schemavalidation;\n" +
  443 + "\n" +
  444 + "message testMessage {\n" +
  445 + " option allow_alias = true;\n" +
  446 + " int32 parameter = 1;\n" +
  447 + "}", "[Transport Configuration] invalid attributes proto schema provided! Message definition options don't support!");
  448 + }
  449 +
  450 + @Test
  451 + public void testSaveProtoDeviceProfileMessageTypeExtensionsNotSupported() throws Exception {
  452 + testSaveDeviceProfileWithInvalidProtoSchema("syntax = \"proto3\";\n" +
  453 + "\n" +
  454 + "package schemavalidation;\n" +
  455 + "\n" +
  456 + "message TestMessage {\n" +
  457 + " extensions 100 to 199;\n" +
  458 + "}", "[Transport Configuration] invalid attributes proto schema provided! Message definition extensions don't support!");
  459 + }
  460 +
  461 + @Test
  462 + public void testSaveProtoDeviceProfileMessageTypeReservedElementsNotSupported() throws Exception {
  463 + testSaveDeviceProfileWithInvalidProtoSchema("syntax = \"proto3\";\n" +
  464 + "\n" +
  465 + "package schemavalidation;\n" +
  466 + "\n" +
  467 + "message Foo {\n" +
  468 + " reserved 2, 15, 9 to 11;\n" +
  469 + " reserved \"foo\", \"bar\";\n" +
  470 + "}", "[Transport Configuration] invalid attributes proto schema provided! Message definition reserved elements don't support!");
  471 + }
  472 +
  473 + @Test
  474 + public void testSaveProtoDeviceProfileMessageTypeGroupsElementsNotSupported() throws Exception {
  475 + testSaveDeviceProfileWithInvalidProtoSchema("syntax = \"proto3\";\n" +
  476 + "\n" +
  477 + "package schemavalidation;\n" +
  478 + "\n" +
  479 + "message TestMessage {\n" +
  480 + " repeated group Result = 1 {\n" +
  481 + " string url = 2;\n" +
  482 + " string title = 3;\n" +
  483 + " repeated string snippets = 4;\n" +
  484 + " }\n" +
  485 + "}", "[Transport Configuration] invalid attributes proto schema provided! Message definition groups don't support!");
  486 + }
  487 +
  488 + @Test
  489 + public void testSaveProtoDeviceProfileOneOfsGroupsElementsNotSupported() throws Exception {
  490 + testSaveDeviceProfileWithInvalidProtoSchema("syntax = \"proto3\";\n" +
  491 + "\n" +
  492 + "package schemavalidation;\n" +
  493 + "\n" +
  494 + "message SampleMessage {\n" +
  495 + " oneof test_oneof {\n" +
  496 + " string name = 1;\n" +
  497 + " group Result = 2 {\n" +
  498 + " \tstring url = 3;\n" +
  499 + " \tstring title = 4;\n" +
  500 + " \trepeated string snippets = 5;\n" +
  501 + " }\n" +
  502 + " }" +
  503 + "}", "[Transport Configuration] invalid attributes proto schema provided! OneOf definition groups don't support!");
  504 + }
  505 +
  506 + @Test
  507 + public void testSaveProtoDeviceProfileWithMessageNestedTypes() throws Exception {
  508 + String schema = "syntax = \"proto3\";\n" +
  509 + "\n" +
  510 + "package testnested;\n" +
  511 + "\n" +
  512 + "message Outer {\n" +
  513 + " message MiddleAA {\n" +
  514 + " message Inner {\n" +
  515 + " int64 ival = 1;\n" +
  516 + " bool booly = 2;\n" +
  517 + " }\n" +
  518 + " Inner inner = 1;\n" +
  519 + " }\n" +
  520 + " message MiddleBB {\n" +
  521 + " message Inner {\n" +
  522 + " int32 ival = 1;\n" +
  523 + " bool booly = 2;\n" +
  524 + " }\n" +
  525 + " Inner inner = 1;\n" +
  526 + " }\n" +
  527 + " MiddleAA middleAA = 1;\n" +
  528 + " MiddleBB middleBB = 2;\n" +
  529 + "}";
  530 + DynamicSchema dynamicSchema = getDynamicSchema(schema);
  531 + assertNotNull(dynamicSchema);
  532 + Set<String> messageTypes = dynamicSchema.getMessageTypes();
  533 + assertEquals(5, messageTypes.size());
  534 + assertTrue(messageTypes.contains("testnested.Outer"));
  535 + assertTrue(messageTypes.contains("testnested.Outer.MiddleAA"));
  536 + assertTrue(messageTypes.contains("testnested.Outer.MiddleAA.Inner"));
  537 + assertTrue(messageTypes.contains("testnested.Outer.MiddleBB"));
  538 + assertTrue(messageTypes.contains("testnested.Outer.MiddleBB.Inner"));
  539 +
  540 + DynamicMessage.Builder middleAAInnerMsgBuilder = dynamicSchema.newMessageBuilder("testnested.Outer.MiddleAA.Inner");
  541 + Descriptors.Descriptor middleAAInnerMsgDescriptor = middleAAInnerMsgBuilder.getDescriptorForType();
  542 + DynamicMessage middleAAInnerMsg = middleAAInnerMsgBuilder
  543 + .setField(middleAAInnerMsgDescriptor.findFieldByName("ival"), 1L)
  544 + .setField(middleAAInnerMsgDescriptor.findFieldByName("booly"), true)
  545 + .build();
  546 +
  547 + DynamicMessage.Builder middleAAMsgBuilder = dynamicSchema.newMessageBuilder("testnested.Outer.MiddleAA");
  548 + Descriptors.Descriptor middleAAMsgDescriptor = middleAAMsgBuilder.getDescriptorForType();
  549 + DynamicMessage middleAAMsg = middleAAMsgBuilder
  550 + .setField(middleAAMsgDescriptor.findFieldByName("inner"), middleAAInnerMsg)
  551 + .build();
  552 +
  553 + DynamicMessage.Builder middleBBInnerMsgBuilder = dynamicSchema.newMessageBuilder("testnested.Outer.MiddleAA.Inner");
  554 + Descriptors.Descriptor middleBBInnerMsgDescriptor = middleBBInnerMsgBuilder.getDescriptorForType();
  555 + DynamicMessage middleBBInnerMsg = middleBBInnerMsgBuilder
  556 + .setField(middleBBInnerMsgDescriptor.findFieldByName("ival"), 0L)
  557 + .setField(middleBBInnerMsgDescriptor.findFieldByName("booly"), false)
  558 + .build();
  559 +
  560 + DynamicMessage.Builder middleBBMsgBuilder = dynamicSchema.newMessageBuilder("testnested.Outer.MiddleBB");
  561 + Descriptors.Descriptor middleBBMsgDescriptor = middleBBMsgBuilder.getDescriptorForType();
  562 + DynamicMessage middleBBMsg = middleBBMsgBuilder
  563 + .setField(middleBBMsgDescriptor.findFieldByName("inner"), middleBBInnerMsg)
  564 + .build();
  565 +
  566 +
  567 + DynamicMessage.Builder outerMsgBuilder = dynamicSchema.newMessageBuilder("testnested.Outer");
  568 + Descriptors.Descriptor outerMsgBuilderDescriptor = outerMsgBuilder.getDescriptorForType();
  569 + DynamicMessage outerMsg = outerMsgBuilder
  570 + .setField(outerMsgBuilderDescriptor.findFieldByName("middleAA"), middleAAMsg)
  571 + .setField(outerMsgBuilderDescriptor.findFieldByName("middleBB"), middleBBMsg)
  572 + .build();
  573 +
  574 + assertEquals("{\n" +
  575 + " \"middleAA\": {\n" +
  576 + " \"inner\": {\n" +
  577 + " \"ival\": \"1\",\n" +
  578 + " \"booly\": true\n" +
  579 + " }\n" +
  580 + " },\n" +
  581 + " \"middleBB\": {\n" +
  582 + " \"inner\": {\n" +
  583 + " \"ival\": 0,\n" +
  584 + " \"booly\": false\n" +
  585 + " }\n" +
  586 + " }\n" +
  587 + "}", dynamicMsgToJson(outerMsgBuilderDescriptor, outerMsg.toByteArray()));
  588 + }
  589 +
  590 + @Test
  591 + public void testSaveProtoDeviceProfileWithMessageOneOfs() throws Exception {
  592 + String schema = "syntax = \"proto3\";\n" +
  593 + "\n" +
  594 + "package testoneofs;\n" +
  595 + "\n" +
  596 + "message SubMessage {\n" +
  597 + " repeated string name = 1;\n" +
  598 + "}\n" +
  599 + "\n" +
  600 + "message SampleMessage {\n" +
  601 + " oneof testOneOf {\n" +
  602 + " string name = 4;\n" +
  603 + " SubMessage subMessage = 9;\n" +
  604 + " }\n" +
  605 + "}";
  606 + DynamicSchema dynamicSchema = getDynamicSchema(schema);
  607 + assertNotNull(dynamicSchema);
  608 + Set<String> messageTypes = dynamicSchema.getMessageTypes();
  609 + assertEquals(2, messageTypes.size());
  610 + assertTrue(messageTypes.contains("testoneofs.SubMessage"));
  611 + assertTrue(messageTypes.contains("testoneofs.SampleMessage"));
  612 +
  613 + DynamicMessage.Builder sampleMsgBuilder = dynamicSchema.newMessageBuilder("testoneofs.SampleMessage");
  614 + Descriptors.Descriptor sampleMsgDescriptor = sampleMsgBuilder.getDescriptorForType();
  615 + assertNotNull(sampleMsgDescriptor);
  616 +
  617 + List<Descriptors.FieldDescriptor> fields = sampleMsgDescriptor.getFields();
  618 + assertEquals(2, fields.size());
  619 + DynamicMessage sampleMsg = sampleMsgBuilder
  620 + .setField(sampleMsgDescriptor.findFieldByName("name"), "Bob")
  621 + .build();
  622 + assertEquals("{\n" + " \"name\": \"Bob\"\n" + "}", dynamicMsgToJson(sampleMsgDescriptor, sampleMsg.toByteArray()));
  623 +
  624 + DynamicMessage.Builder subMsgBuilder = dynamicSchema.newMessageBuilder("testoneofs.SubMessage");
  625 + Descriptors.Descriptor subMsgDescriptor = subMsgBuilder.getDescriptorForType();
  626 + DynamicMessage subMsg = subMsgBuilder
  627 + .addRepeatedField(subMsgDescriptor.findFieldByName("name"), "Alice")
  628 + .addRepeatedField(subMsgDescriptor.findFieldByName("name"), "John")
  629 + .build();
  630 +
  631 + DynamicMessage sampleMsgWithOneOfSubMessage = sampleMsgBuilder.setField(sampleMsgDescriptor.findFieldByName("subMessage"), subMsg).build();
  632 + assertEquals("{\n" + " \"subMessage\": {\n" + " \"name\": [\"Alice\", \"John\"]\n" + " }\n" + "}",
  633 + dynamicMsgToJson(sampleMsgDescriptor, sampleMsgWithOneOfSubMessage.toByteArray()));
  634 + }
  635 +
  636 + private DeviceProfile testSaveDeviceProfileWithProtoPayloadType(String schema) throws Exception {
  637 + ProtoTransportPayloadConfiguration protoTransportPayloadConfiguration = this.createProtoTransportPayloadConfiguration(schema, schema);
  638 + MqttDeviceProfileTransportConfiguration mqttDeviceProfileTransportConfiguration = this.createMqttDeviceProfileTransportConfiguration(protoTransportPayloadConfiguration);
  639 + DeviceProfile deviceProfile = this.createDeviceProfile("Device Profile", mqttDeviceProfileTransportConfiguration);
  640 + DeviceProfile savedDeviceProfile = doPost("/api/deviceProfile", deviceProfile, DeviceProfile.class);
  641 + DeviceProfile foundDeviceProfile = doGet("/api/deviceProfile/"+savedDeviceProfile.getId().getId().toString(), DeviceProfile.class);
  642 + Assert.assertEquals(savedDeviceProfile.getName(), foundDeviceProfile.getName());
  643 + return savedDeviceProfile;
  644 + }
  645 +
  646 + private void testSaveDeviceProfileWithInvalidProtoSchema(String schema, String errorMsg) throws Exception {
  647 + ProtoTransportPayloadConfiguration protoTransportPayloadConfiguration = this.createProtoTransportPayloadConfiguration(schema, schema);
  648 + MqttDeviceProfileTransportConfiguration mqttDeviceProfileTransportConfiguration = this.createMqttDeviceProfileTransportConfiguration(protoTransportPayloadConfiguration);
  649 + DeviceProfile deviceProfile = this.createDeviceProfile("Device Profile", mqttDeviceProfileTransportConfiguration);
  650 + doPost("/api/deviceProfile", deviceProfile).andExpect(status().isBadRequest())
  651 + .andExpect(statusReason(containsString(errorMsg)));
  652 + }
  653 +
  654 + private DynamicSchema getDynamicSchema(String schema) throws Exception {
  655 + DeviceProfile deviceProfile = testSaveDeviceProfileWithProtoPayloadType(schema);
  656 + DeviceProfileTransportConfiguration transportConfiguration = deviceProfile.getProfileData().getTransportConfiguration();
  657 + assertTrue(transportConfiguration instanceof MqttDeviceProfileTransportConfiguration);
  658 + MqttDeviceProfileTransportConfiguration mqttDeviceProfileTransportConfiguration = (MqttDeviceProfileTransportConfiguration) transportConfiguration;
  659 + TransportPayloadTypeConfiguration transportPayloadTypeConfiguration = mqttDeviceProfileTransportConfiguration.getTransportPayloadTypeConfiguration();
  660 + assertTrue(transportPayloadTypeConfiguration instanceof ProtoTransportPayloadConfiguration);
  661 + ProtoTransportPayloadConfiguration protoTransportPayloadConfiguration = (ProtoTransportPayloadConfiguration) transportPayloadTypeConfiguration;
  662 + ProtoFileElement protoFile = protoTransportPayloadConfiguration.getTransportProtoSchema(schema);
  663 + return protoTransportPayloadConfiguration.getDynamicSchema(protoFile, ProtoTransportPayloadConfiguration.ATTRIBUTES_PROTO_SCHEMA);
  664 + }
  665 +
  666 + private String dynamicMsgToJson(Descriptors.Descriptor descriptor, byte[] payload) throws InvalidProtocolBufferException {
  667 + DynamicMessage dynamicMessage = DynamicMessage.parseFrom(descriptor, payload);
  668 + return JsonFormat.printer().includingDefaultValueFields().print(dynamicMessage);
  669 + }
  670 +
321 } 671 }