Commit 9829dd17cc3fc1489bfb405a25367d4a3e72629c
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.
@@ -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, |
application/src/main/data/json/demo/dashboards/raspberry_pi_gpio_demo_dashboard.json
deleted
100644 → 0
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 | -} |
application/src/main/data/json/demo/dashboards/temperature___humidity_demo_dashboard.json
deleted
100644 → 0
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 | } |
application/src/main/data/json/demo/rule_chains/root_rule_chain.json
deleted
100644 → 0
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 | -} |
application/src/main/data/json/demo/rule_chains/thermostat_alarms.json
deleted
100644 → 0
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 |
application/src/main/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateService.java
@@ -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); |
application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java
@@ -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 | +} |
application/src/main/java/org/thingsboard/server/service/sms/DefaultSmsSenderFactory.java
0 → 100644
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">— 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 <a style="box-sizing: border-box; color: #999999; margin: 0px;" href="mailto:${targetEmail}">${targetEmail}</a> 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">— 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 <a style="box-sizing: border-box; color: #999999; margin: 0px;" href="mailto:${targetEmail}">${targetEmail}</a> 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 ${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">— 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 <a style="box-sizing: border-box; color: #999999; margin: 0px;" href="mailto:${targetEmail}">${targetEmail}</a> 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 | } |