Browse Source

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

pull/4571/head
Volodymyr Babak 5 years ago
parent
commit
afd35ddf94
  1. 4
      application/src/main/data/json/system/widget_bundles/control_widgets.json
  2. 90
      application/src/main/data/upgrade/3.2.2/schema_update_event.sql
  3. 10
      application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java
  4. 9
      application/src/main/java/org/thingsboard/server/controller/RpcController.java
  5. 16
      application/src/main/java/org/thingsboard/server/service/install/PsqlEntityDatabaseSchemaService.java
  6. 27
      application/src/main/java/org/thingsboard/server/service/install/SqlAbstractDatabaseSchemaService.java
  7. 4
      application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java
  8. 33
      application/src/main/java/org/thingsboard/server/service/mail/DefaultMailService.java
  9. 2
      application/src/main/java/org/thingsboard/server/service/rpc/DefaultTbCoreDeviceRpcService.java
  10. 6
      application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java
  11. 5
      application/src/main/java/org/thingsboard/server/service/ttl/EventsCleanUpService.java
  12. 2
      application/src/main/resources/thingsboard.yml
  13. 54
      application/src/test/java/org/thingsboard/server/service/install/PsqlEntityDatabaseSchemaServiceTest.java
  14. 50
      application/src/test/java/org/thingsboard/server/service/ttl/EventsCleanUpServiceTest.java
  15. 1
      application/src/test/java/org/thingsboard/server/transport/coap/rpc/AbstractCoapServerSideRpcIntegrationTest.java
  16. 1
      common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java
  17. 2
      common/data/src/main/java/org/thingsboard/server/common/data/device/data/Lwm2mDeviceTransportConfiguration.java
  18. 1
      common/data/src/main/java/org/thingsboard/server/common/data/device/data/lwm2m/OtherConfiguration.java
  19. 1
      common/queue/src/main/proto/queue.proto
  20. 80
      common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/AbstractCoapTransportResource.java
  21. 11
      common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportContext.java
  22. 82
      common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportResource.java
  23. 5
      common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/OtaPackageTransportResource.java
  24. 95
      common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/TbCoapMessageObserver.java
  25. 2
      common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/adaptors/JsonCoapAdaptor.java
  26. 2
      common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/adaptors/ProtoCoapAdaptor.java
  27. 60
      common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/callback/CoapDeviceAuthCallback.java
  28. 37
      common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/callback/CoapNoOpCallback.java
  29. 50
      common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/callback/CoapOkCallback.java
  30. 2
      common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/efento/CoapEfentoTransportResource.java
  31. 2
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/attributes/DefaultLwM2MAttributesService.java
  32. 7
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClient.java
  33. 2
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClientContext.java
  34. 19
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClientContextImpl.java
  35. 5
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/downlink/DefaultLwM2mDownlinkMsgHandler.java
  36. 10
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/ota/DefaultLwM2MOtaUpdateService.java
  37. 26
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/rpc/DefaultLwM2MRpcRequestHandler.java
  38. 10
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/uplink/DefaultLwM2MUplinkMsgHandler.java
  39. 57
      common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java
  40. 2
      common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/auth/TransportDeviceInfo.java
  41. 1
      common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java
  42. 38
      dao/src/main/resources/sql/schema-entities-idx-psql-addon.sql
  43. 2
      dao/src/main/resources/sql/schema-entities-idx.sql
  44. 2
      dao/src/test/resources/sql-test.properties
  45. 5
      docker/tb-coap-transport.env
  46. 5
      docker/tb-http-transport.env
  47. 3
      docker/tb-lwm2m-transport.env
  48. 5
      docker/tb-mqtt-transport.env
  49. 3
      docker/tb-snmp-transport.env
  50. 2
      msa/black-box-tests/pom.xml
  51. 35
      msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ContainerTestSuite.java
  52. 4
      pom.xml
  53. 4
      rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/MailService.java
  54. 4
      rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbEmail.java
  55. 9
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mail/TbMsgToEmailNode.java
  56. 15
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mail/TbSendEmailNode.java
  57. 2
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rpc/TbSendRPCRequestNode.java
  58. 5
      rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/mail/TbMsgToEmailNodeTest.java
  59. 8
      transport/lwm2m/pom.xml
  60. 2
      ui-ngx/src/app/app-routing.module.ts
  61. 8
      ui-ngx/src/app/core/api/widget-api.models.ts
  62. 12
      ui-ngx/src/app/core/api/widget-subscription.ts
  63. 9
      ui-ngx/src/app/core/http/attribute.service.ts
  64. 0
      ui-ngx/src/app/modules/home/components/device/device-credentials-lwm2m-server.component.html
  65. 10
      ui-ngx/src/app/modules/home/components/device/device-credentials-lwm2m-server.component.ts
  66. 25
      ui-ngx/src/app/modules/home/components/device/device-credentials-lwm2m.component.html
  67. 0
      ui-ngx/src/app/modules/home/components/device/device-credentials-lwm2m.component.scss
  68. 21
      ui-ngx/src/app/modules/home/components/device/device-credentials-lwm2m.component.ts
  69. 48
      ui-ngx/src/app/modules/home/components/device/device-credentials-mqtt-basic.component.html
  70. 133
      ui-ngx/src/app/modules/home/components/device/device-credentials-mqtt-basic.component.ts
  71. 85
      ui-ngx/src/app/modules/home/components/device/device-credentials.component.html
  72. 93
      ui-ngx/src/app/modules/home/components/device/device-credentials.component.ts
  73. 46
      ui-ngx/src/app/modules/home/components/device/device-credentials.module.ts
  74. 16
      ui-ngx/src/app/modules/home/components/home-components.module.ts
  75. 1
      ui-ngx/src/app/modules/home/components/profile/add-device-profile-dialog.component.html
  76. 4
      ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarm.component.html
  77. 11
      ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-profile-transport-configuration.component.html
  78. 18
      ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-profile-transport-configuration.component.ts
  79. 1
      ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-profile-config.models.ts
  80. 1
      ui-ngx/src/app/modules/home/components/widget/data-keys.component.html
  81. 2
      ui-ngx/src/app/modules/home/components/widget/lib/alarm-filter-panel.component.html
  82. 2
      ui-ngx/src/app/modules/home/components/widget/widget-config.component.html
  83. 5
      ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.html
  84. 16
      ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.ts
  85. 1
      ui-ngx/src/app/modules/home/home.component.html
  86. 8
      ui-ngx/src/app/modules/home/models/widget-component.models.ts
  87. 895
      ui-ngx/src/app/modules/home/pages/api-usage/api_usage_json.raw
  88. 11
      ui-ngx/src/app/modules/home/pages/device/data/lwm2m-device-transport-configuration.component.html
  89. 50
      ui-ngx/src/app/modules/home/pages/device/data/lwm2m-device-transport-configuration.component.ts
  90. 27
      ui-ngx/src/app/modules/home/pages/device/device-credentials-dialog.component.html
  91. 41
      ui-ngx/src/app/modules/home/pages/device/device-credentials-dialog.component.ts
  92. 2
      ui-ngx/src/app/modules/home/pages/device/device.module.ts
  93. 4
      ui-ngx/src/app/modules/home/pages/device/devices-table-config.resolver.ts
  94. 1
      ui-ngx/src/app/modules/home/pages/rulechain/link-labels.component.html
  95. 1
      ui-ngx/src/app/shared/components/entity/entity-keys-list.component.html
  96. 1
      ui-ngx/src/app/shared/components/entity/entity-subtype-list.component.html
  97. 16
      ui-ngx/src/app/shared/models/ace/widget-completion.models.ts
  98. 17
      ui-ngx/src/app/shared/models/device.models.ts
  99. 16
      ui-ngx/src/assets/locale/locale.constant-en_US.json

4
application/src/main/data/json/system/widget_bundles/control_widgets.json

@ -18,8 +18,8 @@
"resources": [],
"templateHtml": "<div style=\"height: 100%; overflow-y: auto;\" id=\"device-terminal\"></div>",
"templateCss": ".cmd .cursor.blink {\n -webkit-animation-name: terminal-underline;\n -moz-animation-name: terminal-underline;\n -ms-animation-name: terminal-underline;\n animation-name: terminal-underline;\n}\n.terminal .inverted, .cmd .inverted {\n border-bottom-color: #aaa;\n}\n\n",
"controllerScript": "var requestTimeout = 500;\n\nself.onInit = function() {\n var subscription = self.ctx.defaultSubscription;\n var rpcEnabled = subscription.rpcEnabled;\n var deviceName = 'Simulated';\n var prompt;\n if (subscription.targetDeviceName && subscription.targetDeviceName.length) {\n deviceName = subscription.targetDeviceName;\n }\n if (self.ctx.settings.requestTimeout) {\n requestTimeout = self.ctx.settings.requestTimeout;\n }\n var greetings = 'Welcome to ThingsBoard RPC debug terminal.\\n\\n';\n if (!rpcEnabled) {\n greetings += 'Target device is not set!\\n\\n';\n prompt = '';\n } else {\n greetings += 'Current target device for RPC commands: [[b;#fff;]' + deviceName + ']\\n\\n';\n greetings += 'Please type [[b;#fff;]\\'help\\'] to see usage.\\n';\n prompt = '[[b;#8bc34a;]' + deviceName +']> ';\n }\n \n var terminal = $('#device-terminal', self.ctx.$container).terminal(\n function(command) {\n if (command !== '') {\n try {\n var localCommand = command.trim();\n var requestUUID = uuidv4();\n if (localCommand === 'help') {\n printUsage(this);\n } else {\n var spaceIndex = localCommand.indexOf(' ');\n if (spaceIndex === -1 && !localCommand.length) {\n this.error(\"Wrong number of arguments!\");\n this.echo(' ');\n } else {\n var params;\n if (spaceIndex === -1) {\n spaceIndex = localCommand.length;\n }\n var name = localCommand.substr(0, spaceIndex);\n var args = localCommand.substr(spaceIndex + 1);\n if (args.length) {\n try {\n params = JSON.parse(args);\n } catch (e) {\n params = args;\n }\n }\n performRpc(this, name, params, requestUUID);\n }\n }\n } catch(e) {\n this.error(new String(e));\n }\n } else {\n this.echo('');\n }\n }, {\n greetings: greetings,\n prompt: prompt,\n enabled: rpcEnabled\n });\n \n if (!rpcEnabled) {\n terminal.error('No RPC target detected!').pause();\n }\n}\n\n\nfunction printUsage(terminal) {\n var commandsListText = '\\n[[b;#fff;]Usage:]\\n';\n commandsListText += ' <method> [params body]]\\n\\n';\n commandsListText += '[[b;#fff;]Example 1:]\\n'; \n commandsListText += ' myRemoteMethod1 myText\\n\\n'; \n commandsListText += '[[b;#fff;]Example 2:]\\n'; \n commandsListText += ' myOtherRemoteMethod \"{\\\\\"key1\\\\\": 2, \\\\\"key2\\\\\": \\\\\"myVal\\\\\"}\"\\n'; \n terminal.echo(new String(commandsListText));\n}\n\n\nfunction performRpc(terminal, method, params, requestUUID) {\n terminal.pause();\n self.ctx.controlApi.sendTwoWayCommand(method, params, requestTimeout, requestUUID).subscribe(\n function success(responseBody) {\n terminal.echo(JSON.stringify(responseBody));\n terminal.echo(' ');\n terminal.resume();\n },\n function fail() {\n var errorText = self.ctx.defaultSubscription.rpcErrorText;\n terminal.error(errorText);\n terminal.echo(' ');\n terminal.resume();\n }\n );\n}\n\n\nfunction uuidv4() {\n return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {\n var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);\n return v.toString(16);\n });\n}\n\n \nself.onDestroy = function() {\n}",
"settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"Settings\",\n \"properties\": {\n \"requestTimeout\": {\n \"title\": \"RPC request timeout (ms)\",\n \"type\": \"number\",\n \"default\": 500\n }\n },\n \"required\": [\"requestTimeout\"]\n },\n \"form\": [\n \"requestTimeout\"\n ]\n}",
"controllerScript": "var requestTimeout = 500;\nvar requestPersistent = false;\n\nself.onInit = function() {\n var subscription = self.ctx.defaultSubscription;\n var rpcEnabled = subscription.rpcEnabled;\n var deviceName = 'Simulated';\n var prompt;\n if (subscription.targetDeviceName && subscription.targetDeviceName.length) {\n deviceName = subscription.targetDeviceName;\n }\n if (self.ctx.settings.requestTimeout) {\n requestTimeout = self.ctx.settings.requestTimeout;\n }\n if (self.ctx.settings.requestPersistent) {\n requestPersistent = self.ctx.settings.requestPersistent;\n }\n var greetings = 'Welcome to ThingsBoard RPC debug terminal.\\n\\n';\n if (!rpcEnabled) {\n greetings += 'Target device is not set!\\n\\n';\n prompt = '';\n } else {\n greetings += 'Current target device for RPC commands: [[b;#fff;]' + deviceName + ']\\n\\n';\n greetings += 'Please type [[b;#fff;]\\'help\\'] to see usage.\\n';\n prompt = '[[b;#8bc34a;]' + deviceName +']> ';\n }\n \n var terminal = $('#device-terminal', self.ctx.$container).terminal(\n function(command) {\n if (command !== '') {\n try {\n var localCommand = command.trim();\n var requestUUID = uuidv4();\n if (localCommand === 'help') {\n printUsage(this);\n } else {\n var spaceIndex = localCommand.indexOf(' ');\n if (spaceIndex === -1 && !localCommand.length) {\n this.error(\"Wrong number of arguments!\");\n this.echo(' ');\n } else {\n var params;\n if (spaceIndex === -1) {\n spaceIndex = localCommand.length;\n }\n var name = localCommand.substr(0, spaceIndex);\n var args = localCommand.substr(spaceIndex + 1);\n if (args.length) {\n try {\n params = JSON.parse(args);\n } catch (e) {\n params = args;\n }\n }\n performRpc(this, name, params, requestUUID);\n }\n }\n } catch(e) {\n this.error(new String(e));\n }\n } else {\n this.echo('');\n }\n }, {\n greetings: greetings,\n prompt: prompt,\n enabled: rpcEnabled\n });\n \n if (!rpcEnabled) {\n terminal.error('No RPC target detected!').pause();\n }\n}\n\n\nfunction printUsage(terminal) {\n var commandsListText = '\\n[[b;#fff;]Usage:]\\n';\n commandsListText += ' <method> [params body]]\\n\\n';\n commandsListText += '[[b;#fff;]Example 1:]\\n'; \n commandsListText += ' myRemoteMethod1 myText\\n\\n'; \n commandsListText += '[[b;#fff;]Example 2:]\\n'; \n commandsListText += ' myOtherRemoteMethod \"{\\\\\"key1\\\\\": 2, \\\\\"key2\\\\\": \\\\\"myVal\\\\\"}\"\\n'; \n terminal.echo(new String(commandsListText));\n}\n\n\nfunction performRpc(terminal, method, params, requestUUID) {\n terminal.pause();\n self.ctx.controlApi.sendTwoWayCommand(method, params, requestTimeout, requestPersistent, requestUUID).subscribe(\n function success(responseBody) {\n terminal.echo(JSON.stringify(responseBody));\n terminal.echo(' ');\n terminal.resume();\n },\n function fail() {\n var errorText = self.ctx.defaultSubscription.rpcErrorText;\n terminal.error(errorText);\n terminal.echo(' ');\n terminal.resume();\n }\n );\n}\n\n\nfunction uuidv4() {\n return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {\n var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);\n return v.toString(16);\n });\n}\n\n \nself.onDestroy = function() {\n}",
"settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"Settings\",\n \"properties\": {\n \"requestTimeout\": {\n \"title\": \"RPC request timeout (ms)\",\n \"type\": \"number\",\n \"default\": 500\n },\n \"requestPersistent\": {\n \"title\": \"RPC request persistent\",\n \"type\": \"boolean\",\n \"default\": false\n }\n },\n \"required\": [\"requestTimeout\"]\n },\n \"form\": [\n \"requestTimeout\",\n \"requestPersistent\"\n ]\n}",
"dataKeySettingsSchema": "{}\n",
"defaultConfig": "{\"targetDeviceAliases\":[],\"showTitle\":true,\"backgroundColor\":\"#010101\",\"color\":\"rgba(255, 254, 254, 0.87)\",\"padding\":\"0px\",\"settings\":{\"parseGpioStatusFunction\":\"return body[pin] === true;\",\"gpioStatusChangeRequest\":{\"method\":\"setGpioStatus\",\"paramsBody\":\"{\\n \\\"pin\\\": \\\"{$pin}\\\",\\n \\\"enabled\\\": \\\"{$enabled}\\\"\\n}\"},\"requestTimeout\":500,\"switchPanelBackgroundColor\":\"#b71c1c\",\"gpioStatusRequest\":{\"method\":\"getGpioStatus\",\"paramsBody\":\"{}\"},\"gpioList\":[{\"pin\":1,\"label\":\"GPIO 1\",\"row\":0,\"col\":0,\"_uniqueKey\":0},{\"pin\":2,\"label\":\"GPIO 2\",\"row\":0,\"col\":1,\"_uniqueKey\":1},{\"pin\":3,\"label\":\"GPIO 3\",\"row\":1,\"col\":0,\"_uniqueKey\":2}]},\"title\":\"RPC debug terminal\",\"dropShadow\":true,\"enableFullscreen\":true,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}"
}

90
application/src/main/data/upgrade/3.2.2/schema_update_event.sql

@ -0,0 +1,90 @@
--
-- Copyright © 2016-2021 The Thingsboard Authors
--
-- Licensed under the Apache License, Version 2.0 (the "License");
-- you may not use this file except in compliance with the License.
-- You may obtain a copy of the License at
--
-- http://www.apache.org/licenses/LICENSE-2.0
--
-- Unless required by applicable law or agreed to in writing, software
-- distributed under the License is distributed on an "AS IS" BASIS,
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-- See the License for the specific language governing permissions and
-- limitations under the License.
--
-- PROCEDURE: public.cleanup_events_by_ttl(bigint, bigint, bigint)
DROP PROCEDURE IF EXISTS public.cleanup_events_by_ttl(bigint, bigint, bigint);
CREATE OR REPLACE PROCEDURE public.cleanup_events_by_ttl(
ttl bigint,
debug_ttl bigint,
INOUT deleted bigint)
LANGUAGE 'plpgsql'
AS $BODY$
DECLARE
ttl_ts bigint;
debug_ttl_ts bigint;
ttl_deleted_count bigint DEFAULT 0;
debug_ttl_deleted_count bigint DEFAULT 0;
BEGIN
IF ttl > 0 THEN
ttl_ts := (EXTRACT(EPOCH FROM current_timestamp) * 1000 - ttl::bigint * 1000)::bigint;
DELETE FROM event
WHERE ts < ttl_ts
AND NOT event_type IN ('DEBUG_RULE_NODE', 'DEBUG_RULE_CHAIN', 'DEBUG_CONVERTER', 'DEBUG_INTEGRATION');
GET DIAGNOSTICS ttl_deleted_count = ROW_COUNT;
END IF;
IF debug_ttl > 0 THEN
debug_ttl_ts := (EXTRACT(EPOCH FROM current_timestamp) * 1000 - debug_ttl::bigint * 1000)::bigint;
DELETE FROM event
WHERE ts < debug_ttl_ts
AND event_type IN ('DEBUG_RULE_NODE', 'DEBUG_RULE_CHAIN', 'DEBUG_CONVERTER', 'DEBUG_INTEGRATION');
GET DIAGNOSTICS debug_ttl_deleted_count = ROW_COUNT;
END IF;
RAISE NOTICE 'Events removed by ttl: %', ttl_deleted_count;
RAISE NOTICE 'Debug Events removed by ttl: %', debug_ttl_deleted_count;
deleted := ttl_deleted_count + debug_ttl_deleted_count;
END
$BODY$;
-- Index: idx_event_ts
DROP INDEX IF EXISTS public.idx_event_ts;
-- Hint: add CONCURRENTLY to CREATE INDEX query in case of more then 1 million records or during live update
-- CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_event_ts
CREATE INDEX IF NOT EXISTS idx_event_ts
ON public.event
(ts DESC NULLS LAST)
WITH (FILLFACTOR=95);
COMMENT ON INDEX public.idx_event_ts
IS 'This index helps to delete events by TTL using timestamp';
-- Index: idx_event_tenant_entity_type_entity_event_type_created_time_des
DROP INDEX IF EXISTS public.idx_event_tenant_entity_type_entity_event_type_created_time_des;
-- CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_event_tenant_entity_type_entity_event_type_created_time_des
CREATE INDEX IF NOT EXISTS idx_event_tenant_entity_type_entity_event_type_created_time_des
ON public.event
(tenant_id ASC, entity_type ASC, entity_id ASC, event_type ASC, created_time DESC NULLS LAST)
WITH (FILLFACTOR=95);
COMMENT ON INDEX public.idx_event_tenant_entity_type_entity_event_type_created_time_des
IS 'This index helps to open latest events on UI fast';
-- Index: idx_event_type_entity_id
-- Description: replaced with more suitable idx_event_tenant_entity_type_entity_event_type_created_time_des
DROP INDEX IF EXISTS public.idx_event_type_entity_id;

10
application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java

@ -206,13 +206,13 @@ class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcessor {
syncSessionSet.forEach(rpcSubscriptions::remove);
}
if (persisted && !(sent || request.isOneway())) {
if (persisted) {
ObjectNode response = JacksonUtil.newObjectNode();
response.put("rpcId", request.getId().toString());
systemContext.getTbCoreDeviceRpcService().processRpcResponseFromDeviceActor(new FromDeviceRpcResponse(msg.getMsg().getId(), JacksonUtil.toString(response), null));
}
if (request.isOneway() && sent) {
if (!persisted && request.isOneway() && sent) {
log.debug("[{}] Rpc command response sent [{}]!", deviceId, request.getId());
systemContext.getTbCoreDeviceRpcService().processRpcResponseFromDeviceActor(new FromDeviceRpcResponse(msg.getMsg().getId(), null, null));
} else {
@ -233,7 +233,6 @@ class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcessor {
rpc.setExpirationTime(request.getExpirationTime());
rpc.setRequest(JacksonUtil.valueToTree(request));
rpc.setStatus(status);
systemContext.getTbRpcService().save(tenantId, rpc);
return systemContext.getTbRpcService().save(tenantId, rpc);
}
@ -298,7 +297,7 @@ class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcessor {
toDeviceRpcPendingMap.entrySet().stream().findFirst().ifPresent(processPendingRpc(context, sessionId, sessionInfo.getNodeId(), sentOneWayIds));
}
sentOneWayIds.forEach(toDeviceRpcPendingMap::remove);
sentOneWayIds.stream().filter(id -> !toDeviceRpcPendingMap.get(id).getMsg().getMsg().isPersisted()).forEach(toDeviceRpcPendingMap::remove);
}
private Consumer<Map.Entry<Integer, ToDeviceRpcRequestMetadata>> processPendingRpc(TbActorCtx context, UUID sessionId, String nodeId, Set<Integer> sentOneWayIds) {
@ -503,9 +502,6 @@ class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcessor {
}
} else {
log.debug("[{}] Rpc command response [{}] is stale!", deviceId, responseMsg.getRequestId());
if (requestMd.getMsg().getMsg().isPersisted()) {
systemContext.getTbRpcService().save(tenantId, new RpcId(requestMd.getMsg().getMsg().getId()), RpcStatus.FAILED, JacksonUtil.toJsonNode(responseMsg.getPayload()));
}
}
}

9
application/src/main/java/org/thingsboard/server/controller/RpcController.java

@ -34,6 +34,7 @@ import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.request.async.DeferredResult;
import org.thingsboard.rule.engine.api.RpcError;
import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
import org.thingsboard.server.common.data.exception.ThingsboardException;
@ -100,7 +101,7 @@ public class RpcController extends BaseController {
}
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/persisted/{rpcId}", method = RequestMethod.GET)
@RequestMapping(value = "/persistent/{rpcId}", method = RequestMethod.GET)
@ResponseBody
public Rpc getPersistedRpc(@PathVariable("rpcId") String strRpc) throws ThingsboardException {
checkParameter("RpcId", strRpc);
@ -113,7 +114,7 @@ public class RpcController extends BaseController {
}
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/persisted/{deviceId}", method = RequestMethod.GET)
@RequestMapping(value = "/persistent/device/{deviceId}", method = RequestMethod.GET)
@ResponseBody
public PageData<Rpc> getPersistedRpcByDevice(@PathVariable("deviceId") String strDeviceId,
@RequestParam int pageSize,
@ -134,7 +135,7 @@ public class RpcController extends BaseController {
}
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/persisted/{rpcId}", method = RequestMethod.DELETE)
@RequestMapping(value = "/persistent/{rpcId}", method = RequestMethod.DELETE)
@ResponseBody
public void deleteResource(@PathVariable("rpcId") String strRpc) throws ThingsboardException {
checkParameter("RpcId", strRpc);
@ -155,7 +156,7 @@ public class RpcController extends BaseController {
long timeout = rpcRequestBody.has("timeout") ? rpcRequestBody.get("timeout").asLong() : defaultTimeout;
long expTime = System.currentTimeMillis() + Math.max(minTimeout, timeout);
UUID rpcRequestUUID = rpcRequestBody.has("requestUUID") ? UUID.fromString(rpcRequestBody.get("requestUUID").asText()) : UUID.randomUUID();
boolean persisted = rpcRequestBody.has("persisted") && rpcRequestBody.get("persisted").asBoolean();
boolean persisted = rpcRequestBody.has(DataConstants.PERSISTENT) && rpcRequestBody.get(DataConstants.PERSISTENT).asBoolean();
accessValidator.validate(currentUser, Operation.RPC_CALL, deviceId, new HttpValidationCallback(response, new FutureCallback<DeferredResult<ResponseEntity>>() {
@Override
public void onSuccess(@Nullable DeferredResult<ResponseEntity> result) {

16
application/src/main/java/org/thingsboard/server/service/install/PsqlEntityDatabaseSchemaService.java

@ -15,6 +15,7 @@
*/
package org.thingsboard.server.service.install;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Service;
import org.thingsboard.server.dao.util.PsqlDao;
@ -22,9 +23,22 @@ import org.thingsboard.server.dao.util.PsqlDao;
@Service
@PsqlDao
@Profile("install")
@Slf4j
public class PsqlEntityDatabaseSchemaService extends SqlAbstractDatabaseSchemaService
implements EntityDatabaseSchemaService {
public static final String SCHEMA_ENTITIES_SQL = "schema-entities.sql";
public static final String SCHEMA_ENTITIES_IDX_SQL = "schema-entities-idx.sql";
public static final String SCHEMA_ENTITIES_IDX_PSQL_ADDON_SQL = "schema-entities-idx-psql-addon.sql";
public PsqlEntityDatabaseSchemaService() {
super("schema-entities.sql", "schema-entities-idx.sql");
super(SCHEMA_ENTITIES_SQL, SCHEMA_ENTITIES_IDX_SQL);
}
@Override
public void createDatabaseIndexes() throws Exception {
super.createDatabaseIndexes();
log.info("Installing SQL DataBase schema PostgreSQL specific indexes part: " + SCHEMA_ENTITIES_IDX_PSQL_ADDON_SQL);
executeQueryFromFile(SCHEMA_ENTITIES_IDX_PSQL_ADDON_SQL);
}
}

27
application/src/main/java/org/thingsboard/server/service/install/SqlAbstractDatabaseSchemaService.java

@ -19,7 +19,7 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import java.nio.charset.Charset;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
@ -59,14 +59,8 @@ public abstract class SqlAbstractDatabaseSchemaService implements DatabaseSchema
@Override
public void createDatabaseSchema(boolean createIndexes) throws Exception {
log.info("Installing SQL DataBase schema part: " + schemaSql);
Path schemaFile = Paths.get(installScripts.getDataDir(), SQL_DIR, schemaSql);
try (Connection conn = DriverManager.getConnection(dbUrl, dbUserName, dbPassword)) {
String sql = new String(Files.readAllBytes(schemaFile), Charset.forName("UTF-8"));
conn.createStatement().execute(sql); //NOSONAR, ignoring because method used to load initial thingsboard database schema
}
executeQueryFromFile(schemaSql);
if (createIndexes) {
this.createDatabaseIndexes();
@ -77,11 +71,15 @@ public abstract class SqlAbstractDatabaseSchemaService implements DatabaseSchema
public void createDatabaseIndexes() throws Exception {
if (schemaIdxSql != null) {
log.info("Installing SQL DataBase schema indexes part: " + schemaIdxSql);
Path schemaIdxFile = Paths.get(installScripts.getDataDir(), SQL_DIR, schemaIdxSql);
try (Connection conn = DriverManager.getConnection(dbUrl, dbUserName, dbPassword)) {
String sql = new String(Files.readAllBytes(schemaIdxFile), Charset.forName("UTF-8"));
conn.createStatement().execute(sql); //NOSONAR, ignoring because method used to load initial thingsboard database schema
}
executeQueryFromFile(schemaIdxSql);
}
}
void executeQueryFromFile(String schemaIdxSql) throws SQLException, IOException {
Path schemaIdxFile = Paths.get(installScripts.getDataDir(), SQL_DIR, schemaIdxSql);
String sql = Files.readString(schemaIdxFile);
try (Connection conn = DriverManager.getConnection(dbUrl, dbUserName, dbPassword)) {
conn.createStatement().execute(sql); //NOSONAR, ignoring because method used to load initial thingsboard database schema
}
}
@ -91,7 +89,8 @@ public abstract class SqlAbstractDatabaseSchemaService implements DatabaseSchema
log.info("Successfully executed query: {}", query);
Thread.sleep(5000);
} catch (InterruptedException | SQLException e) {
log.info("Failed to execute query: {} due to: {}", query, e.getMessage());
log.error("Failed to execute query: {} due to: {}", query, e.getMessage());
throw new RuntimeException("Failed to execute query: " + query, e);
}
}

4
application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java

@ -459,6 +459,10 @@ public class SqlDatabaseUpgradeService implements DatabaseEntitiesUpgradeService
schemaUpdateFile = Paths.get(installScripts.getDataDir(), "upgrade", "3.2.2", "schema_update_ttl.sql");
loadSql(schemaUpdateFile, conn);
log.info("Edge TTL functions successfully loaded!");
log.info("Updating indexes and TTL procedure for event table...");
schemaUpdateFile = Paths.get(installScripts.getDataDir(), "upgrade", "3.2.2", "schema_update_event.sql");
loadSql(schemaUpdateFile, conn);
log.info("Updating schema settings...");
conn.createStatement().execute("UPDATE tb_schema_settings SET schema_version = 3003000;");
log.info("Schema updated.");
} catch (Exception e) {

33
application/src/main/java/org/thingsboard/server/service/mail/DefaultMailService.java

@ -31,6 +31,7 @@ import org.springframework.mail.javamail.MimeMessageHelper;
import org.springframework.stereotype.Service;
import org.springframework.ui.freemarker.FreeMarkerTemplateUtils;
import org.thingsboard.rule.engine.api.MailService;
import org.thingsboard.rule.engine.api.TbEmail;
import org.thingsboard.server.common.data.AdminSettings;
import org.thingsboard.server.common.data.ApiFeature;
import org.thingsboard.server.common.data.ApiUsageRecordKey;
@ -250,35 +251,35 @@ public class DefaultMailService implements MailService {
}
@Override
public void send(TenantId tenantId, CustomerId customerId, String from, String to, String cc, String bcc, String subject, String body, boolean isHtml, Map<String, String> images) throws ThingsboardException {
sendMail(tenantId, customerId, from, to, cc, bcc, subject, body, isHtml, images, this.mailSender);
public void send(TenantId tenantId, CustomerId customerId, TbEmail tbEmail) throws ThingsboardException {
sendMail(tenantId, customerId, tbEmail, this.mailSender);
}
@Override
public void send(TenantId tenantId, CustomerId customerId, String from, String to, String cc, String bcc, String subject, String body, boolean isHtml, Map<String, String> images, JavaMailSender javaMailSender) throws ThingsboardException {
sendMail(tenantId, customerId, from, to, cc, bcc, subject, body, isHtml, images, javaMailSender);
public void send(TenantId tenantId, CustomerId customerId, TbEmail tbEmail, JavaMailSender javaMailSender) throws ThingsboardException {
sendMail(tenantId, customerId, tbEmail, javaMailSender);
}
private void sendMail(TenantId tenantId, CustomerId customerId, String from, String to, String cc, String bcc, String subject, String body, boolean isHtml, Map<String, String> images, JavaMailSender javaMailSender) throws ThingsboardException {
private void sendMail(TenantId tenantId, CustomerId customerId, TbEmail tbEmail, JavaMailSender javaMailSender) throws ThingsboardException {
if (apiUsageStateService.getApiUsageState(tenantId).isEmailSendEnabled()) {
try {
MimeMessage mailMsg = javaMailSender.createMimeMessage();
boolean multipart = (images != null && !images.isEmpty());
boolean multipart = (tbEmail.getImages() != null && !tbEmail.getImages().isEmpty());
MimeMessageHelper helper = new MimeMessageHelper(mailMsg, multipart, "UTF-8");
helper.setFrom(StringUtils.isBlank(from) ? mailFrom : from);
helper.setTo(to.split("\\s*,\\s*"));
if (!StringUtils.isBlank(cc)) {
helper.setCc(cc.split("\\s*,\\s*"));
helper.setFrom(StringUtils.isBlank(tbEmail.getFrom()) ? mailFrom : tbEmail.getFrom());
helper.setTo(tbEmail.getTo().split("\\s*,\\s*"));
if (!StringUtils.isBlank(tbEmail.getCc())) {
helper.setCc(tbEmail.getCc().split("\\s*,\\s*"));
}
if (!StringUtils.isBlank(bcc)) {
helper.setBcc(bcc.split("\\s*,\\s*"));
if (!StringUtils.isBlank(tbEmail.getBcc())) {
helper.setBcc(tbEmail.getBcc().split("\\s*,\\s*"));
}
helper.setSubject(subject);
helper.setText(body, isHtml);
helper.setSubject(tbEmail.getSubject());
helper.setText(tbEmail.getBody(), tbEmail.isHtml());
if (multipart) {
for (String imgId : images.keySet()) {
String imgValue = images.get(imgId);
for (String imgId : tbEmail.getImages().keySet()) {
String imgValue = tbEmail.getImages().get(imgId);
String value = imgValue.replaceFirst("^data:image/[^;]*;base64,?", "");
byte[] bytes = javax.xml.bind.DatatypeConverter.parseBase64Binary(value);
String contentType = helper.getFileTypeMap().getContentType(imgId);

2
application/src/main/java/org/thingsboard/server/service/rpc/DefaultTbCoreDeviceRpcService.java

@ -157,7 +157,7 @@ public class DefaultTbCoreDeviceRpcService implements TbCoreDeviceRpcService {
metaData.putValue("originServiceId", serviceId);
metaData.putValue("expirationTime", Long.toString(msg.getExpirationTime()));
metaData.putValue("oneway", Boolean.toString(msg.isOneway()));
metaData.putValue("persisted", Boolean.toString(msg.isPersisted()));
metaData.putValue(DataConstants.PERSISTENT, Boolean.toString(msg.isPersisted()));
Device device = deviceService.findDeviceById(msg.getTenantId(), msg.getDeviceId());
if (device != null) {

6
application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java

@ -462,9 +462,12 @@ public class DefaultTransportApiService implements TransportApiService {
private DeviceInfoProto getDeviceInfoProto(Device device) throws JsonProcessingException {
PowerMode powerMode = null;
Long edrxCycle = null;
switch (device.getDeviceData().getTransportConfiguration().getType()) {
case LWM2M:
powerMode = ((Lwm2mDeviceTransportConfiguration) device.getDeviceData().getTransportConfiguration()).getPowerMode();
Lwm2mDeviceTransportConfiguration transportConfiguration = (Lwm2mDeviceTransportConfiguration) device.getDeviceData().getTransportConfiguration();
powerMode = transportConfiguration.getPowerMode();
edrxCycle = transportConfiguration.getEdrxCycle();
break;
}
@ -482,6 +485,7 @@ public class DefaultTransportApiService implements TransportApiService {
.setAdditionalInfo(mapper.writeValueAsString(device.getAdditionalInfo()));
if (powerMode != null) {
builder.setPowerMode(powerMode.name());
builder.setEdrxCycle(edrxCycle);
}
return builder.build();
}

5
application/src/main/java/org/thingsboard/server/service/ttl/EventsCleanUpService.java

@ -29,6 +29,9 @@ import org.thingsboard.server.service.ttl.AbstractCleanUpService;
@Service
public class EventsCleanUpService extends AbstractCleanUpService {
public static final String RANDOM_DELAY_INTERVAL_MS_EXPRESSION =
"#{T(org.apache.commons.lang3.RandomUtils).nextLong(0, ${sql.ttl.events.execution_interval_ms})}";
@Value("${sql.ttl.events.events_ttl}")
private long ttl;
@ -45,7 +48,7 @@ public class EventsCleanUpService extends AbstractCleanUpService {
this.eventService = eventService;
}
@Scheduled(initialDelayString = "${sql.ttl.events.execution_interval_ms}", fixedDelayString = "${sql.ttl.events.execution_interval_ms}")
@Scheduled(initialDelayString = RANDOM_DELAY_INTERVAL_MS_EXPRESSION, fixedDelayString = "${sql.ttl.events.execution_interval_ms}")
public void cleanUp() {
if (ttlTaskExecutionEnabled && isSystemTenantPartitionMine()) {
eventService.cleanupEvents(ttl, debugTtl);

2
application/src/main/resources/thingsboard.yml

@ -266,7 +266,7 @@ sql:
ts_key_value_ttl: "${SQL_TTL_TS_TS_KEY_VALUE_TTL:0}" # Number of seconds
events:
enabled: "${SQL_TTL_EVENTS_ENABLED:true}"
execution_interval_ms: "${SQL_TTL_EVENTS_EXECUTION_INTERVAL:86400000}" # Number of milliseconds. The current value corresponds to one day
execution_interval_ms: "${SQL_TTL_EVENTS_EXECUTION_INTERVAL:2220000}" # Number of milliseconds (max random initial delay and fixed period). # 37minutes to avoid common interval spikes
events_ttl: "${SQL_TTL_EVENTS_EVENTS_TTL:0}" # Number of seconds
debug_events_ttl: "${SQL_TTL_EVENTS_DEBUG_EVENTS_TTL:604800}" # Number of seconds. The current value corresponds to one week
edge_events:

54
application/src/test/java/org/thingsboard/server/service/install/PsqlEntityDatabaseSchemaServiceTest.java

@ -0,0 +1,54 @@
/**
* Copyright © 2016-2021 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.install;
import org.junit.Test;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.BDDMockito.willDoNothing;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
public class PsqlEntityDatabaseSchemaServiceTest {
@Test
public void givenPsqlDbSchemaService_whenCreateDatabaseSchema_thenVerifyPsqlIndexSpecificCall() throws Exception {
PsqlEntityDatabaseSchemaService service = spy(new PsqlEntityDatabaseSchemaService());
willDoNothing().given(service).executeQueryFromFile(anyString());
service.createDatabaseSchema();
verify(service, times(1)).createDatabaseIndexes();
verify(service, times(1)).executeQueryFromFile(PsqlEntityDatabaseSchemaService.SCHEMA_ENTITIES_SQL);
verify(service, times(1)).executeQueryFromFile(PsqlEntityDatabaseSchemaService.SCHEMA_ENTITIES_IDX_SQL);
verify(service, times(1)).executeQueryFromFile(PsqlEntityDatabaseSchemaService.SCHEMA_ENTITIES_IDX_PSQL_ADDON_SQL);
verify(service, times(3)).executeQueryFromFile(anyString());
}
@Test
public void givenPsqlDbSchemaService_whenCreateDatabaseIndexes_thenVerifyPsqlIndexSpecificCall() throws Exception {
PsqlEntityDatabaseSchemaService service = spy(new PsqlEntityDatabaseSchemaService());
willDoNothing().given(service).executeQueryFromFile(anyString());
service.createDatabaseIndexes();
verify(service, times(1)).executeQueryFromFile(PsqlEntityDatabaseSchemaService.SCHEMA_ENTITIES_IDX_SQL);
verify(service, times(1)).executeQueryFromFile(PsqlEntityDatabaseSchemaService.SCHEMA_ENTITIES_IDX_PSQL_ADDON_SQL);
verify(service, times(2)).executeQueryFromFile(anyString());
}
}

50
application/src/test/java/org/thingsboard/server/service/ttl/EventsCleanUpServiceTest.java

@ -0,0 +1,50 @@
/**
* Copyright © 2016-2021 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.ttl;
import lombok.extern.slf4j.Slf4j;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.greaterThanOrEqualTo;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.lessThanOrEqualTo;
import static org.thingsboard.server.service.ttl.EventsCleanUpService.RANDOM_DELAY_INTERVAL_MS_EXPRESSION;
@RunWith(SpringRunner.class)
@SpringBootTest(classes = EventsCleanUpServiceTest.class)
@Slf4j
public class EventsCleanUpServiceTest {
@Value(RANDOM_DELAY_INTERVAL_MS_EXPRESSION)
long randomDelayMs;
@Value("${sql.ttl.events.execution_interval_ms}")
long executionIntervalMs;
@Test
public void givenInterval_whenRandomDelay_ThenDelayInInterval() {
log.info("randomDelay {}", randomDelayMs);
log.info("executionIntervalMs {}", executionIntervalMs);
assertThat(executionIntervalMs, is(2220000L));
assertThat(randomDelayMs, greaterThanOrEqualTo(0L));
assertThat(randomDelayMs, lessThanOrEqualTo(executionIntervalMs));
}
}

1
application/src/test/java/org/thingsboard/server/transport/coap/rpc/AbstractCoapServerSideRpcIntegrationTest.java

@ -31,6 +31,7 @@ import org.thingsboard.server.common.data.TransportPayloadType;
import org.thingsboard.server.common.msg.session.FeatureType;
import org.thingsboard.server.transport.coap.AbstractCoapIntegrationTest;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

1
common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java

@ -35,6 +35,7 @@ public class DataConstants {
public static final String IS_CLEARED_ALARM = "isClearedAlarm";
public static final String ALARM_CONDITION_REPEATS = "alarmConditionRepeats";
public static final String ALARM_CONDITION_DURATION = "alarmConditionDuration";
public static final String PERSISTENT = "persistent";
public static final String[] allScopes() {
return new String[]{CLIENT_SCOPE, SHARED_SCOPE, SERVER_SCOPE};

2
common/data/src/main/java/org/thingsboard/server/common/data/device/data/Lwm2mDeviceTransportConfiguration.java

@ -29,6 +29,8 @@ public class Lwm2mDeviceTransportConfiguration implements DeviceTransportConfigu
private PowerMode powerMode;
private Long edrxCycle;
@JsonIgnore
private Map<String, Object> properties = new HashMap<>();

1
common/data/src/main/java/org/thingsboard/server/common/data/device/data/lwm2m/OtherConfiguration.java

@ -27,6 +27,7 @@ public class OtherConfiguration {
private Integer swUpdateStrategy;
private Integer clientOnlyObserveAfterConnect;
private PowerMode powerMode;
private Long edrxCycle;
private String fwUpdateResource;
private String swUpdateResource;
private boolean compositeOperationsSupport;

1
common/queue/src/main/proto/queue.proto

@ -116,6 +116,7 @@ message DeviceInfoProto {
int64 customerIdMSB = 10;
int64 customerIdLSB = 11;
string powerMode = 12;
int64 edrxCycle = 13;
}
/**

80
common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/AbstractCoapTransportResource.java

@ -15,11 +15,14 @@
*/
package org.thingsboard.server.transport.coap;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.californium.core.CoapResource;
import org.eclipse.californium.core.coap.CoAP;
import org.eclipse.californium.core.coap.MessageObserver;
import org.eclipse.californium.core.coap.Response;
import org.eclipse.californium.core.server.resources.CoapExchange;
import org.eclipse.californium.elements.EndpointContext;
import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.transport.TransportContext;
import org.thingsboard.server.common.transport.TransportService;
@ -29,8 +32,12 @@ import org.thingsboard.server.common.transport.auth.ValidateDeviceCredentialsRes
import org.thingsboard.server.gen.transport.TransportProtos;
import java.util.UUID;
import java.util.concurrent.ThreadLocalRandom;
import java.util.function.BiConsumer;
import static org.eclipse.californium.core.coap.Message.MAX_MID;
import static org.eclipse.californium.core.coap.Message.NONE;
@Slf4j
public abstract class AbstractCoapTransportResource extends CoapResource {
@ -75,77 +82,8 @@ public abstract class AbstractCoapTransportResource extends CoapResource {
.setEvent(event).build();
}
public static class CoapDeviceAuthCallback implements TransportServiceCallback<ValidateDeviceCredentialsResponse> {
private final TransportContext transportContext;
private final CoapExchange exchange;
private final BiConsumer<TransportProtos.SessionInfoProto, DeviceProfile> onSuccess;
public CoapDeviceAuthCallback(TransportContext transportContext, CoapExchange exchange, BiConsumer<TransportProtos.SessionInfoProto, DeviceProfile> onSuccess) {
this.transportContext = transportContext;
this.exchange = exchange;
this.onSuccess = onSuccess;
}
@Override
public void onSuccess(ValidateDeviceCredentialsResponse msg) {
DeviceProfile deviceProfile = msg.getDeviceProfile();
if (msg.hasDeviceInfo() && deviceProfile != null) {
TransportProtos.SessionInfoProto sessionInfoProto = SessionInfoCreator.create(msg, transportContext, UUID.randomUUID());
onSuccess.accept(sessionInfoProto, deviceProfile);
} else {
exchange.respond(CoAP.ResponseCode.UNAUTHORIZED);
}
}
@Override
public void onError(Throwable e) {
log.warn("Failed to process request", e);
exchange.respond(CoAP.ResponseCode.INTERNAL_SERVER_ERROR);
}
}
public static class CoapOkCallback implements TransportServiceCallback<Void> {
private final CoapExchange exchange;
private final CoAP.ResponseCode onSuccessResponse;
private final CoAP.ResponseCode onFailureResponse;
public CoapOkCallback(CoapExchange exchange, CoAP.ResponseCode onSuccessResponse, CoAP.ResponseCode onFailureResponse) {
this.exchange = exchange;
this.onSuccessResponse = onSuccessResponse;
this.onFailureResponse = onFailureResponse;
}
@Override
public void onSuccess(Void msg) {
Response response = new Response(onSuccessResponse);
response.setAcknowledged(isConRequest());
exchange.respond(response);
}
@Override
public void onError(Throwable e) {
exchange.respond(onFailureResponse);
}
private boolean isConRequest() {
return exchange.advanced().getRequest().isConfirmable();
}
protected int getNextMsgId() {
return ThreadLocalRandom.current().nextInt(NONE, MAX_MID + 1);
}
public static class CoapNoOpCallback implements TransportServiceCallback<Void> {
private final CoapExchange exchange;
CoapNoOpCallback(CoapExchange exchange) {
this.exchange = exchange;
}
@Override
public void onSuccess(Void msg) {
}
@Override
public void onError(Throwable e) {
exchange.respond(CoAP.ResponseCode.INTERNAL_SERVER_ERROR);
}
}
}

11
common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportContext.java

@ -22,10 +22,14 @@ import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
import org.springframework.stereotype.Component;
import org.thingsboard.server.common.transport.TransportContext;
import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.transport.coap.adaptors.JsonCoapAdaptor;
import org.thingsboard.server.transport.coap.adaptors.ProtoCoapAdaptor;
import org.thingsboard.server.transport.coap.efento.adaptor.EfentoCoapAdaptor;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
/**
* Created by ashvayka on 18.10.18.
@ -33,22 +37,21 @@ import org.thingsboard.server.transport.coap.efento.adaptor.EfentoCoapAdaptor;
@Slf4j
@ConditionalOnExpression("'${service.type:null}'=='tb-transport' || ('${service.type:null}'=='monolith' && '${transport.api_enabled:true}'=='true' && '${transport.coap.enabled}'=='true')")
@Component
@Getter
public class CoapTransportContext extends TransportContext {
@Getter
@Value("${transport.sessions.report_timeout}")
private long sessionReportTimeout;
@Getter
@Autowired
private JsonCoapAdaptor jsonCoapAdaptor;
@Getter
@Autowired
private ProtoCoapAdaptor protoCoapAdaptor;
@Getter
@Autowired
private EfentoCoapAdaptor efentoCoapAdaptor;
private final ConcurrentMap<Integer, TransportProtos.ToDeviceRpcRequestMsg> rpcAwaitingAck = new ConcurrentHashMap<>();
}

82
common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportResource.java

@ -19,6 +19,7 @@ import com.google.gson.JsonParseException;
import com.google.protobuf.Descriptors;
import com.google.protobuf.DynamicMessage;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.californium.core.coap.CoAP;
import org.eclipse.californium.core.coap.Request;
@ -53,6 +54,9 @@ import org.thingsboard.server.common.transport.adaptor.AdaptorException;
import org.thingsboard.server.common.transport.adaptor.JsonConverter;
import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.transport.coap.adaptors.CoapTransportAdaptor;
import org.thingsboard.server.transport.coap.callback.CoapDeviceAuthCallback;
import org.thingsboard.server.transport.coap.callback.CoapNoOpCallback;
import org.thingsboard.server.transport.coap.callback.CoapOkCallback;
import java.util.List;
import java.util.Map;
@ -81,9 +85,8 @@ public class CoapTransportResource extends AbstractCoapTransportResource {
private final Set<UUID> rpcSubscriptions = ConcurrentHashMap.newKeySet();
private final Set<UUID> attributeSubscriptions = ConcurrentHashMap.newKeySet();
private ConcurrentMap<String, TbCoapDtlsSessionInfo> dtlsSessionIdMap;
private long timeout;
private long sessionReportTimeout;
private final ConcurrentMap<String, TbCoapDtlsSessionInfo> dtlsSessionIdMap;
private final long timeout;
public CoapTransportResource(CoapTransportContext ctx, CoapServerService coapServerService, String name) {
super(ctx, name);
@ -91,7 +94,7 @@ public class CoapTransportResource extends AbstractCoapTransportResource {
this.addObserver(new CoapResourceObserver());
this.dtlsSessionIdMap = coapServerService.getDtlsSessionsMap();
this.timeout = coapServerService.getTimeout();
this.sessionReportTimeout = ctx.getSessionReportTimeout();
long sessionReportTimeout = ctx.getSessionReportTimeout();
ctx.getScheduler().scheduleAtFixedRate(() -> {
Set<CoapObserveSessionInfo> coapObserveSessionInfos = sessionInfoToObserveRelationMap.keySet();
Set<TransportProtos.SessionInfoProto> observeSessions = coapObserveSessionInfos
@ -110,7 +113,6 @@ public class CoapTransportResource extends AbstractCoapTransportResource {
return; // because request did not try to establish a relation
}
if (CoAP.ResponseCode.isSuccess(response.getCode())) {
if (!relation.isEstablished()) {
relation.setEstablished();
addObserveRelation(relation);
@ -280,8 +282,8 @@ public class CoapTransportResource extends AbstractCoapTransportResource {
CoapObserveSessionInfo currentCoapObserveAttrSessionInfo = tokenToCoapSessionInfoMap.get(getTokenFromRequest(request));
if (currentCoapObserveAttrSessionInfo == null) {
attributeSubscriptions.add(sessionId);
registerAsyncCoapSession(exchange, sessionInfo, coapTransportAdaptor,
transportConfigurationContainer.getRpcRequestDynamicMessageBuilder(), getTokenFromRequest(request));
registerAsyncCoapSession(exchange, coapTransportAdaptor, transportConfigurationContainer.getRpcRequestDynamicMessageBuilder(),
sessionInfo, getTokenFromRequest(request));
transportService.process(sessionInfo,
TransportProtos.SubscribeToAttributeUpdatesMsg.getDefaultInstance(), new CoapNoOpCallback(exchange));
transportService.process(sessionInfo,
@ -305,11 +307,11 @@ public class CoapTransportResource extends AbstractCoapTransportResource {
CoapObserveSessionInfo currentCoapObserveRpcSessionInfo = tokenToCoapSessionInfoMap.get(getTokenFromRequest(request));
if (currentCoapObserveRpcSessionInfo == null) {
rpcSubscriptions.add(sessionId);
registerAsyncCoapSession(exchange, sessionInfo, coapTransportAdaptor,
transportConfigurationContainer.getRpcRequestDynamicMessageBuilder(), getTokenFromRequest(request));
registerAsyncCoapSession(exchange, coapTransportAdaptor, transportConfigurationContainer.getRpcRequestDynamicMessageBuilder()
, sessionInfo, getTokenFromRequest(request));
transportService.process(sessionInfo,
TransportProtos.SubscribeToRPCMsg.getDefaultInstance(),
new CoapOkCallback(exchange, CoAP.ResponseCode.VALID, CoAP.ResponseCode.INTERNAL_SERVER_ERROR)
new CoapOkCallback(exchange, CoAP.ResponseCode.VALID, CoAP.ResponseCode.INTERNAL_SERVER_ERROR)
);
}
break;
@ -359,14 +361,16 @@ public class CoapTransportResource extends AbstractCoapTransportResource {
return tokenToCoapSessionInfoMap.remove(token);
}
private void registerAsyncCoapSession(CoapExchange exchange, TransportProtos.SessionInfoProto sessionInfo, CoapTransportAdaptor coapTransportAdaptor, DynamicMessage.Builder rpcRequestDynamicMessageBuilder, String token) {
private void registerAsyncCoapSession(CoapExchange exchange, CoapTransportAdaptor coapTransportAdaptor,
DynamicMessage.Builder rpcRequestDynamicMessageBuilder, TransportProtos.SessionInfoProto sessionInfo, String token) {
tokenToCoapSessionInfoMap.putIfAbsent(token, new CoapObserveSessionInfo(sessionInfo));
transportService.registerAsyncSession(sessionInfo, getCoapSessionListener(exchange, coapTransportAdaptor, rpcRequestDynamicMessageBuilder, sessionInfo));
transportService.process(sessionInfo, getSessionEventMsg(TransportProtos.SessionEvent.OPEN), null);
}
private CoapSessionListener getCoapSessionListener(CoapExchange exchange, CoapTransportAdaptor coapTransportAdaptor, DynamicMessage.Builder rpcRequestDynamicMessageBuilder, TransportProtos.SessionInfoProto sessionInfo) {
return new CoapSessionListener(this, exchange, coapTransportAdaptor, rpcRequestDynamicMessageBuilder, sessionInfo);
private CoapSessionListener getCoapSessionListener(CoapExchange exchange, CoapTransportAdaptor coapTransportAdaptor,
DynamicMessage.Builder rpcRequestDynamicMessageBuilder, TransportProtos.SessionInfoProto sessionInfo) {
return new CoapSessionListener(exchange, coapTransportAdaptor, rpcRequestDynamicMessageBuilder, sessionInfo);
}
private String getTokenFromRequest(Request request) {
@ -448,22 +452,14 @@ public class CoapTransportResource extends AbstractCoapTransportResource {
}
}
private static class CoapSessionListener implements SessionMsgListener {
@RequiredArgsConstructor
private class CoapSessionListener implements SessionMsgListener {
private final CoapTransportResource coapTransportResource;
private final CoapExchange exchange;
private final CoapTransportAdaptor coapTransportAdaptor;
private final DynamicMessage.Builder rpcRequestDynamicMessageBuilder;
private final TransportProtos.SessionInfoProto sessionInfo;
CoapSessionListener(CoapTransportResource coapTransportResource, CoapExchange exchange, CoapTransportAdaptor coapTransportAdaptor, DynamicMessage.Builder rpcRequestDynamicMessageBuilder, TransportProtos.SessionInfoProto sessionInfo) {
this.coapTransportResource = coapTransportResource;
this.exchange = exchange;
this.coapTransportAdaptor = coapTransportAdaptor;
this.rpcRequestDynamicMessageBuilder = rpcRequestDynamicMessageBuilder;
this.sessionInfo = sessionInfo;
}
@Override
public void onGetAttributesResponse(TransportProtos.GetAttributeResponseMsg msg) {
try {
@ -496,18 +492,30 @@ public class CoapTransportResource extends AbstractCoapTransportResource {
@Override
public void onToDeviceRpcRequest(UUID sessionId, TransportProtos.ToDeviceRpcRequestMsg msg) {
log.trace("[{}] Received RPC command to device", sessionId);
boolean successful = true;
try {
exchange.respond(coapTransportAdaptor.convertToPublish(isConRequest(), msg, rpcRequestDynamicMessageBuilder));
Response response = coapTransportAdaptor.convertToPublish(isConRequest(), msg, rpcRequestDynamicMessageBuilder);
int requestId = getNextMsgId();
response.setMID(requestId);
if (msg.getPersisted()) {
transportContext.getRpcAwaitingAck().put(requestId, msg);
transportContext.getScheduler().schedule(() -> {
TransportProtos.ToDeviceRpcRequestMsg awaitingAckMsg = transportContext.getRpcAwaitingAck().remove(requestId);
if (awaitingAckMsg != null) {
transportService.process(sessionInfo, msg, true, TransportServiceCallback.EMPTY);
}
}, Math.max(0, msg.getExpirationTime() - System.currentTimeMillis()), TimeUnit.MILLISECONDS);
}
response.addMessageObserver(new TbCoapMessageObserver(requestId, id -> {
TransportProtos.ToDeviceRpcRequestMsg rpcRequestMsg = transportContext.getRpcAwaitingAck().remove(id);
if (rpcRequestMsg != null) {
transportService.process(sessionInfo, rpcRequestMsg, false, TransportServiceCallback.EMPTY);
}
}));
exchange.respond(response);
} catch (AdaptorException e) {
log.trace("Failed to reply due to error", e);
closeObserveRelationAndNotify(sessionId, CoAP.ResponseCode.INTERNAL_SERVER_ERROR);
successful = false;
} finally {
coapTransportResource.transportService.process(sessionInfo, msg, !successful, TransportServiceCallback.EMPTY);
if (!successful) {
closeAndDeregister();
}
closeAndDeregister();
}
}
@ -526,8 +534,8 @@ public class CoapTransportResource extends AbstractCoapTransportResource {
}
private void closeObserveRelationAndNotify(UUID sessionId, CoAP.ResponseCode responseCode) {
Map<CoapObserveSessionInfo, ObserveRelation> sessionToObserveRelationMap = coapTransportResource.getCoapSessionInfoToObserveRelationMap();
if (coapTransportResource.getObserverCount() > 0 && !CollectionUtils.isEmpty(sessionToObserveRelationMap)) {
Map<CoapObserveSessionInfo, ObserveRelation> sessionToObserveRelationMap = CoapTransportResource.this.getCoapSessionInfoToObserveRelationMap();
if (CoapTransportResource.this.getObserverCount() > 0 && !CollectionUtils.isEmpty(sessionToObserveRelationMap)) {
Optional<CoapObserveSessionInfo> observeSessionToClose = sessionToObserveRelationMap.keySet().stream().filter(coapObserveSessionInfo -> {
TransportProtos.SessionInfoProto sessionToDelete = coapObserveSessionInfo.getSessionInfoProto();
UUID observeSessionId = new UUID(sessionToDelete.getSessionIdMSB(), sessionToDelete.getSessionIdLSB());
@ -536,16 +544,16 @@ public class CoapTransportResource extends AbstractCoapTransportResource {
if (observeSessionToClose.isPresent()) {
CoapObserveSessionInfo coapObserveSessionInfo = observeSessionToClose.get();
ObserveRelation observeRelation = sessionToObserveRelationMap.get(coapObserveSessionInfo);
coapTransportResource.clearAndNotifyObserveRelation(observeRelation, responseCode);
CoapTransportResource.this.clearAndNotifyObserveRelation(observeRelation, responseCode);
}
}
}
private void closeAndDeregister() {
Request request = exchange.advanced().getRequest();
String token = coapTransportResource.getTokenFromRequest(request);
CoapObserveSessionInfo deleted = coapTransportResource.lookupAsyncSessionInfo(token);
coapTransportResource.closeAndDeregister(deleted.getSessionInfoProto());
String token = CoapTransportResource.this.getTokenFromRequest(request);
CoapObserveSessionInfo deleted = CoapTransportResource.this.lookupAsyncSessionInfo(token);
CoapTransportResource.this.closeAndDeregister(deleted.getSessionInfoProto());
}
}

5
common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/OtaPackageTransportResource.java

@ -20,22 +20,19 @@ import org.eclipse.californium.core.coap.CoAP;
import org.eclipse.californium.core.coap.Request;
import org.eclipse.californium.core.coap.Response;
import org.eclipse.californium.core.network.Exchange;
import org.eclipse.californium.core.observe.ObserveRelation;
import org.eclipse.californium.core.server.resources.CoapExchange;
import org.eclipse.californium.core.server.resources.Resource;
import org.eclipse.californium.core.server.resources.ResourceObserver;
import org.thingsboard.common.util.ThingsBoardExecutors;
import org.thingsboard.server.common.data.DeviceTransportType;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.ota.OtaPackageType;
import org.thingsboard.server.common.data.security.DeviceTokenCredentials;
import org.thingsboard.server.common.transport.TransportServiceCallback;
import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.transport.coap.callback.CoapDeviceAuthCallback;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.ExecutorService;
@Slf4j
public class OtaPackageTransportResource extends AbstractCoapTransportResource {

95
common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/TbCoapMessageObserver.java

@ -0,0 +1,95 @@
/**
* Copyright © 2016-2021 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.transport.coap;
import lombok.RequiredArgsConstructor;
import org.eclipse.californium.core.coap.MessageObserver;
import org.eclipse.californium.core.coap.Response;
import org.eclipse.californium.elements.EndpointContext;
import java.util.function.Consumer;
@RequiredArgsConstructor
public class TbCoapMessageObserver implements MessageObserver {
private final int msgId;
private final Consumer<Integer> onAcknowledge;
@Override
public void onRetransmission() {
}
@Override
public void onResponse(Response response) {
}
@Override
public void onAcknowledgement() {
onAcknowledge.accept(msgId);
}
@Override
public void onReject() {
}
@Override
public void onTimeout() {
}
@Override
public void onCancel() {
}
@Override
public void onReadyToSend() {
}
@Override
public void onConnecting() {
}
@Override
public void onDtlsRetransmission(int flight) {
}
@Override
public void onSent(boolean retransmission) {
}
@Override
public void onSendError(Throwable error) {
}
@Override
public void onContextEstablished(EndpointContext endpointContext) {
}
@Override
public void onComplete() {
}
}

2
common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/adaptors/JsonCoapAdaptor.java

@ -150,7 +150,7 @@ public class JsonCoapAdaptor implements CoapTransportAdaptor {
private Response getObserveNotification(boolean confirmable, JsonElement json) {
Response response = new Response(CoAP.ResponseCode._UNKNOWN_SUCCESS_CODE);
response.setPayload(json.toString());
response.setAcknowledged(confirmable);
response.setConfirmable(confirmable);
return response;
}

2
common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/adaptors/ProtoCoapAdaptor.java

@ -122,7 +122,7 @@ public class ProtoCoapAdaptor implements CoapTransportAdaptor {
@Override
public Response convertToPublish(boolean isConfirmable, TransportProtos.ToServerRpcResponseMsg msg) throws AdaptorException {
Response response = new Response(CoAP.ResponseCode.CONTENT);
response.setAcknowledged(isConfirmable);
response.setConfirmable(isConfirmable);
response.setPayload(msg.toByteArray());
return response;
}

60
common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/callback/CoapDeviceAuthCallback.java

@ -0,0 +1,60 @@
/**
* Copyright © 2016-2021 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.transport.coap.callback;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.californium.core.coap.CoAP;
import org.eclipse.californium.core.server.resources.CoapExchange;
import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.transport.TransportContext;
import org.thingsboard.server.common.transport.TransportServiceCallback;
import org.thingsboard.server.common.transport.auth.SessionInfoCreator;
import org.thingsboard.server.common.transport.auth.ValidateDeviceCredentialsResponse;
import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.transport.coap.AbstractCoapTransportResource;
import java.util.UUID;
import java.util.function.BiConsumer;
@Slf4j
public class CoapDeviceAuthCallback implements TransportServiceCallback<ValidateDeviceCredentialsResponse> {
private final TransportContext transportContext;
private final CoapExchange exchange;
private final BiConsumer<TransportProtos.SessionInfoProto, DeviceProfile> onSuccess;
public CoapDeviceAuthCallback(TransportContext transportContext, CoapExchange exchange, BiConsumer<TransportProtos.SessionInfoProto, DeviceProfile> onSuccess) {
this.transportContext = transportContext;
this.exchange = exchange;
this.onSuccess = onSuccess;
}
@Override
public void onSuccess(ValidateDeviceCredentialsResponse msg) {
DeviceProfile deviceProfile = msg.getDeviceProfile();
if (msg.hasDeviceInfo() && deviceProfile != null) {
TransportProtos.SessionInfoProto sessionInfoProto = SessionInfoCreator.create(msg, transportContext, UUID.randomUUID());
onSuccess.accept(sessionInfoProto, deviceProfile);
} else {
exchange.respond(CoAP.ResponseCode.UNAUTHORIZED);
}
}
@Override
public void onError(Throwable e) {
log.warn("Failed to process request", e);
exchange.respond(CoAP.ResponseCode.INTERNAL_SERVER_ERROR);
}
}

37
common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/callback/CoapNoOpCallback.java

@ -0,0 +1,37 @@
/**
* Copyright © 2016-2021 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.transport.coap.callback;
import org.eclipse.californium.core.coap.CoAP;
import org.eclipse.californium.core.server.resources.CoapExchange;
import org.thingsboard.server.common.transport.TransportServiceCallback;
public class CoapNoOpCallback implements TransportServiceCallback<Void> {
private final CoapExchange exchange;
public CoapNoOpCallback(CoapExchange exchange) {
this.exchange = exchange;
}
@Override
public void onSuccess(Void msg) {
}
@Override
public void onError(Throwable e) {
exchange.respond(CoAP.ResponseCode.INTERNAL_SERVER_ERROR);
}
}

50
common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/callback/CoapOkCallback.java

@ -0,0 +1,50 @@
/**
* Copyright © 2016-2021 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.transport.coap.callback;
import org.eclipse.californium.core.coap.CoAP;
import org.eclipse.californium.core.coap.Response;
import org.eclipse.californium.core.server.resources.CoapExchange;
import org.thingsboard.server.common.transport.TransportServiceCallback;
public class CoapOkCallback implements TransportServiceCallback<Void> {
protected final CoapExchange exchange;
protected final CoAP.ResponseCode onSuccessResponse;
protected final CoAP.ResponseCode onFailureResponse;
public CoapOkCallback(CoapExchange exchange, CoAP.ResponseCode onSuccessResponse, CoAP.ResponseCode onFailureResponse) {
this.exchange = exchange;
this.onSuccessResponse = onSuccessResponse;
this.onFailureResponse = onFailureResponse;
}
@Override
public void onSuccess(Void msg) {
Response response = new Response(onSuccessResponse);
response.setConfirmable(isConRequest());
exchange.respond(response);
}
@Override
public void onError(Throwable e) {
exchange.respond(onFailureResponse);
}
protected boolean isConRequest() {
return exchange.advanced().getRequest().isConfirmable();
}
}

2
common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/efento/CoapEfentoTransportResource.java

@ -35,6 +35,8 @@ import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.gen.transport.coap.MeasurementTypeProtos;
import org.thingsboard.server.gen.transport.coap.MeasurementsProtos;
import org.thingsboard.server.transport.coap.AbstractCoapTransportResource;
import org.thingsboard.server.transport.coap.callback.CoapDeviceAuthCallback;
import org.thingsboard.server.transport.coap.callback.CoapOkCallback;
import org.thingsboard.server.transport.coap.CoapTransportContext;
import org.thingsboard.server.transport.coap.efento.utils.CoapEfentoUtils;

2
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/attributes/DefaultLwM2MAttributesService.java

@ -191,7 +191,7 @@ public class DefaultLwM2MAttributesService implements LwM2MAttributesService {
private void pushUpdateToClientIfNeeded(LwM2mClient lwM2MClient, Object valueOld, Object newValue, String versionedId) {
if (newValue != null && (valueOld == null || !newValue.toString().equals(valueOld.toString()))) {
TbLwM2MWriteReplaceRequest request = TbLwM2MWriteReplaceRequest.builder().versionedId(versionedId).value(newValue).timeout(this.config.getTimeout()).build();
TbLwM2MWriteReplaceRequest request = TbLwM2MWriteReplaceRequest.builder().versionedId(versionedId).value(newValue).timeout(clientContext.getRequestTimeout(lwM2MClient)).build();
downlinkHandler.sendWriteReplaceRequest(lwM2MClient, request, new TbLwM2MWriteResponseCallback(uplinkHandler, logService, lwM2MClient, versionedId));
} else {
log.error("Failed update resource [{}] [{}]", versionedId, newValue);

7
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClient.java

@ -93,6 +93,8 @@ public class LwM2mClient implements Serializable {
@Getter
private PowerMode powerMode;
@Getter
private Long edrxCycle;
@Getter
@Setter
private Registration registration;
@ -115,6 +117,7 @@ public class LwM2mClient implements Serializable {
this.deviceId = new UUID(session.getDeviceIdMSB(), session.getDeviceIdLSB());
this.profileId = new UUID(session.getDeviceProfileIdMSB(), session.getDeviceProfileIdLSB());
this.powerMode = credentials.getDeviceInfo().getPowerMode();
this.edrxCycle = credentials.getDeviceInfo().getEdrxCycle();
}
public void lock() {
@ -133,7 +136,9 @@ public class LwM2mClient implements Serializable {
builder.setDeviceName(device.getName());
deviceProfileOpt.ifPresent(deviceProfile -> updateSession(deviceProfile, builder));
this.session = builder.build();
this.powerMode = ((Lwm2mDeviceTransportConfiguration) device.getDeviceData().getTransportConfiguration()).getPowerMode();
Lwm2mDeviceTransportConfiguration transportConfiguration = (Lwm2mDeviceTransportConfiguration) device.getDeviceData().getTransportConfiguration();
this.powerMode = transportConfiguration.getPowerMode();
this.edrxCycle = transportConfiguration.getEdrxCycle();
}
public void onDeviceProfileUpdate(DeviceProfile deviceProfile) {

2
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClientContext.java

@ -62,4 +62,6 @@ public interface LwM2mClientContext {
void sendMsgsAfterSleeping(LwM2mClient lwM2MClient);
boolean isComposite(LwM2mClient client);
Long getRequestTimeout(LwM2mClient client);
}

19
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClientContextImpl.java

@ -26,6 +26,7 @@ import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.device.data.PowerMode;
import org.thingsboard.server.common.data.device.data.lwm2m.OtherConfiguration;
import org.thingsboard.server.common.data.device.profile.Lwm2mDeviceProfileTransportConfiguration;
import org.thingsboard.server.common.data.id.DeviceProfileId;
import org.thingsboard.server.common.transport.TransportDeviceProfileCache;
@ -346,6 +347,24 @@ public class LwM2mClientContextImpl implements LwM2mClientContext {
getProfile(client.getProfileId()).getClientLwM2mSettings().isCompositeOperationsSupport();
}
@Override
public Long getRequestTimeout(LwM2mClient client) {
Long timeout = null;
if (PowerMode.E_DRX.equals(client.getPowerMode()) && client.getEdrxCycle() != null) {
timeout = client.getEdrxCycle();
} else {
var clientProfile = getProfile(client.getProfileId());
OtherConfiguration clientLwM2mSettings = clientProfile.getClientLwM2mSettings();
if (PowerMode.E_DRX.equals(clientLwM2mSettings.getPowerMode())) {
timeout = clientLwM2mSettings.getEdrxCycle();
}
}
if (timeout == null || timeout == 0L) {
timeout = this.config.getTimeout();
}
return timeout;
}
private boolean validateResourceInModel(LwM2mClient lwM2mClient, String pathIdVer, boolean isWritableNotOptional) {
ResourceModel resourceModel = lwM2mClient.getResourceModel(pathIdVer, this.config
.getModelProvider());

5
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/downlink/DefaultLwM2mDownlinkMsgHandler.java

@ -131,7 +131,7 @@ public class DefaultLwM2mDownlinkMsgHandler extends LwM2MExecutorAwareService im
ContentFormat responseContentFormat = ContentFormat.SENML_JSON;
ReadCompositeRequest downlink = new ReadCompositeRequest(requestContentFormat, responseContentFormat, request.getObjectIds());
sendCompositeRequest(client, downlink, this.config.getTimeout(), callback);
sendCompositeRequest(client, downlink, request.getTimeout(), callback);
}
@Override
@ -248,7 +248,8 @@ public class DefaultLwM2mDownlinkMsgHandler extends LwM2MExecutorAwareService im
ContentFormat contentFormat = ContentFormat.SENML_JSON;
try {
WriteCompositeRequest downlink = new WriteCompositeRequest(contentFormat, rpcWriteCompositeRequest.getNodes());
sendWriteCompositeRequest(client, downlink, this.config.getTimeout(), callback);
//TODO: replace config.getTimeout();
sendWriteCompositeRequest(client, downlink, config.getTimeout(), callback);
} catch (Exception e) {
callback.onError(JacksonUtil.toString(rpcWriteCompositeRequest), e);
}

10
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/ota/DefaultLwM2MOtaUpdateService.java

@ -417,7 +417,7 @@ public class DefaultLwM2MOtaUpdateService extends LwM2MExecutorAwareService impl
private void startUpdateUsingUrl(LwM2mClient client, String id, String url) {
String targetIdVer = convertObjectIdToVersionedId(id, client.getRegistration());
TbLwM2MWriteReplaceRequest request = TbLwM2MWriteReplaceRequest.builder().versionedId(targetIdVer).value(url).timeout(config.getTimeout()).build();
TbLwM2MWriteReplaceRequest request = TbLwM2MWriteReplaceRequest.builder().versionedId(targetIdVer).value(url).timeout(clientContext.getRequestTimeout(client)).build();
downlinkHandler.sendWriteReplaceRequest(client, request, new TbLwM2MWriteResponseCallback(uplinkHandler, logService, client, targetIdVer));
}
@ -486,7 +486,7 @@ public class DefaultLwM2MOtaUpdateService extends LwM2MExecutorAwareService impl
byte[] firmwareChunk = otaPackageDataCache.get(otaPackageId.toString(), 0, 0);
TbLwM2MWriteReplaceRequest writeRequest = TbLwM2MWriteReplaceRequest.builder().versionedId(versionedId)
.value(firmwareChunk).contentFormat(ContentFormat.OPAQUE)
.timeout(config.getTimeout()).build();
.timeout(clientContext.getRequestTimeout(client)).build();
downlinkHandler.sendWriteReplaceRequest(client, writeRequest, new TbLwM2MWriteResponseCallback(uplinkHandler, logService, client, versionedId));
}
@ -501,17 +501,17 @@ public class DefaultLwM2MOtaUpdateService extends LwM2MExecutorAwareService impl
}
private void executeFwUpdate(LwM2mClient client) {
TbLwM2MExecuteRequest request = TbLwM2MExecuteRequest.builder().versionedId(FW_EXECUTE_ID).timeout(config.getTimeout()).build();
TbLwM2MExecuteRequest request = TbLwM2MExecuteRequest.builder().versionedId(FW_EXECUTE_ID).timeout(clientContext.getRequestTimeout(client)).build();
downlinkHandler.sendExecuteRequest(client, request, new TbLwM2MExecuteCallback(logService, client, FW_EXECUTE_ID));
}
private void executeSwInstall(LwM2mClient client) {
TbLwM2MExecuteRequest request = TbLwM2MExecuteRequest.builder().versionedId(SW_INSTALL_ID).timeout(config.getTimeout()).build();
TbLwM2MExecuteRequest request = TbLwM2MExecuteRequest.builder().versionedId(SW_INSTALL_ID).timeout(clientContext.getRequestTimeout(client)).build();
downlinkHandler.sendExecuteRequest(client, request, new TbLwM2MExecuteCallback(logService, client, SW_INSTALL_ID));
}
private void executeSwUninstallForUpdate(LwM2mClient client) {
TbLwM2MExecuteRequest request = TbLwM2MExecuteRequest.builder().versionedId(SW_UN_INSTALL_ID).params("1").timeout(config.getTimeout()).build();
TbLwM2MExecuteRequest request = TbLwM2MExecuteRequest.builder().versionedId(SW_UN_INSTALL_ID).params("1").timeout(clientContext.getRequestTimeout(client)).build();
downlinkHandler.sendExecuteRequest(client, request, new TbLwM2MExecuteCallback(logService, client, SW_INSTALL_ID));
}

26
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/rpc/DefaultLwM2MRpcRequestHandler.java

@ -171,7 +171,7 @@ public class DefaultLwM2MRpcRequestHandler implements LwM2MRpcRequestHandler {
}
private void sendReadRequest(LwM2mClient client, TransportProtos.ToDeviceRpcRequestMsg requestMsg, String versionedId) {
TbLwM2MReadRequest request = TbLwM2MReadRequest.builder().versionedId(versionedId).timeout(this.config.getTimeout()).build();
TbLwM2MReadRequest request = TbLwM2MReadRequest.builder().versionedId(versionedId).timeout(clientContext.getRequestTimeout(client)).build();
var mainCallback = new TbLwM2MReadCallback(uplinkHandler, logService, client, versionedId);
var rpcCallback = new RpcReadResponseCallback<>(transportService, client, requestMsg, mainCallback);
downlinkHandler.sendReadRequest(client, request, rpcCallback);
@ -179,38 +179,38 @@ public class DefaultLwM2MRpcRequestHandler implements LwM2MRpcRequestHandler {
private void sendReadCompositeRequest(LwM2mClient client, TransportProtos.ToDeviceRpcRequestMsg requestMsg) {
String[] versionedIds = getIdsFromParameters(client, requestMsg);
TbLwM2MReadCompositeRequest request = TbLwM2MReadCompositeRequest.builder().versionedIds(versionedIds).timeout(this.config.getTimeout()).build();
TbLwM2MReadCompositeRequest request = TbLwM2MReadCompositeRequest.builder().versionedIds(versionedIds).timeout(clientContext.getRequestTimeout(client)).build();
var mainCallback = new TbLwM2MReadCompositeCallback(uplinkHandler, logService, client, versionedIds);
var rpcCallback = new RpcReadResponseCompositeCallback(transportService, client, requestMsg, mainCallback);
downlinkHandler.sendReadCompositeRequest(client, request, rpcCallback);
}
private void sendObserveRequest(LwM2mClient client, TransportProtos.ToDeviceRpcRequestMsg requestMsg, String versionedId) {
TbLwM2MObserveRequest request = TbLwM2MObserveRequest.builder().versionedId(versionedId).timeout(this.config.getTimeout()).build();
TbLwM2MObserveRequest request = TbLwM2MObserveRequest.builder().versionedId(versionedId).timeout(clientContext.getRequestTimeout(client)).build();
var mainCallback = new TbLwM2MObserveCallback(uplinkHandler, logService, client, versionedId);
var rpcCallback = new RpcReadResponseCallback<>(transportService, client, requestMsg, mainCallback);
downlinkHandler.sendObserveRequest(client, request, rpcCallback);
}
private void sendObserveAllRequest(LwM2mClient client, TransportProtos.ToDeviceRpcRequestMsg requestMsg) {
TbLwM2MObserveAllRequest request = TbLwM2MObserveAllRequest.builder().timeout(this.config.getTimeout()).build();
TbLwM2MObserveAllRequest request = TbLwM2MObserveAllRequest.builder().timeout(clientContext.getRequestTimeout(client)).build();
downlinkHandler.sendObserveAllRequest(client, request, new RpcLinkSetCallback<>(transportService, client, requestMsg, null));
}
private void sendDiscoverAllRequest(LwM2mClient client, TransportProtos.ToDeviceRpcRequestMsg requestMsg) {
TbLwM2MDiscoverAllRequest request = TbLwM2MDiscoverAllRequest.builder().timeout(this.config.getTimeout()).build();
TbLwM2MDiscoverAllRequest request = TbLwM2MDiscoverAllRequest.builder().timeout(clientContext.getRequestTimeout(client)).build();
downlinkHandler.sendDiscoverAllRequest(client, request, new RpcLinkSetCallback<>(transportService, client, requestMsg, null));
}
private void sendDiscoverRequest(LwM2mClient client, TransportProtos.ToDeviceRpcRequestMsg requestMsg, String versionedId) {
TbLwM2MDiscoverRequest request = TbLwM2MDiscoverRequest.builder().versionedId(versionedId).timeout(this.config.getTimeout()).build();
TbLwM2MDiscoverRequest request = TbLwM2MDiscoverRequest.builder().versionedId(versionedId).timeout(clientContext.getRequestTimeout(client)).build();
var mainCallback = new TbLwM2MDiscoverCallback(logService, client, versionedId);
var rpcCallback = new RpcDiscoverCallback(transportService, client, requestMsg, mainCallback);
downlinkHandler.sendDiscoverRequest(client, request, rpcCallback);
}
private void sendExecuteRequest(LwM2mClient client, TransportProtos.ToDeviceRpcRequestMsg requestMsg, String versionedId) {
TbLwM2MExecuteRequest downlink = TbLwM2MExecuteRequest.builder().versionedId(versionedId).timeout(this.config.getTimeout()).build();
TbLwM2MExecuteRequest downlink = TbLwM2MExecuteRequest.builder().versionedId(versionedId).timeout(clientContext.getRequestTimeout(client)).build();
var mainCallback = new TbLwM2MExecuteCallback(logService, client, versionedId);
var rpcCallback = new RpcEmptyResponseCallback<>(transportService, client, requestMsg, mainCallback);
downlinkHandler.sendExecuteRequest(client, downlink, rpcCallback);
@ -220,7 +220,7 @@ public class DefaultLwM2MRpcRequestHandler implements LwM2MRpcRequestHandler {
RpcWriteAttributesRequest requestBody = JacksonUtil.fromString(requestMsg.getParams(), RpcWriteAttributesRequest.class);
TbLwM2MWriteAttributesRequest request = TbLwM2MWriteAttributesRequest.builder().versionedId(versionedId)
.attributes(requestBody.getAttributes())
.timeout(this.config.getTimeout()).build();
.timeout(clientContext.getRequestTimeout(client)).build();
var mainCallback = new TbLwM2MWriteAttributesCallback(logService, client, versionedId);
var rpcCallback = new RpcEmptyResponseCallback<>(transportService, client, requestMsg, mainCallback);
downlinkHandler.sendWriteAttributesRequest(client, request, rpcCallback);
@ -229,7 +229,7 @@ public class DefaultLwM2MRpcRequestHandler implements LwM2MRpcRequestHandler {
private void sendWriteUpdateRequest(LwM2mClient client, TransportProtos.ToDeviceRpcRequestMsg requestMsg, String versionedId) {
RpcWriteUpdateRequest requestBody = JacksonUtil.fromString(requestMsg.getParams(), RpcWriteUpdateRequest.class);
TbLwM2MWriteUpdateRequest.TbLwM2MWriteUpdateRequestBuilder builder = TbLwM2MWriteUpdateRequest.builder().versionedId(versionedId);
builder.value(requestBody.getValue()).timeout(this.config.getTimeout());
builder.value(requestBody.getValue()).timeout(clientContext.getRequestTimeout(client));
var mainCallback = new TbLwM2MWriteResponseCallback(uplinkHandler, logService, client, versionedId);
var rpcCallback = new RpcEmptyResponseCallback<>(transportService, client, requestMsg, mainCallback);
downlinkHandler.sendWriteUpdateRequest(client, builder.build(), rpcCallback);
@ -239,7 +239,7 @@ public class DefaultLwM2MRpcRequestHandler implements LwM2MRpcRequestHandler {
RpcWriteReplaceRequest requestBody = JacksonUtil.fromString(requestMsg.getParams(), RpcWriteReplaceRequest.class);
TbLwM2MWriteReplaceRequest request = TbLwM2MWriteReplaceRequest.builder().versionedId(versionedId)
.value(requestBody.getValue())
.timeout(this.config.getTimeout()).build();
.timeout(clientContext.getRequestTimeout(client)).build();
var mainCallback = new TbLwM2MWriteResponseCallback(uplinkHandler, logService, client, versionedId);
var rpcCallback = new RpcEmptyResponseCallback<>(transportService, client, requestMsg, mainCallback);
downlinkHandler.sendWriteReplaceRequest(client, request, rpcCallback);
@ -261,21 +261,21 @@ public class DefaultLwM2MRpcRequestHandler implements LwM2MRpcRequestHandler {
}
private void sendCancelObserveRequest(LwM2mClient client, TransportProtos.ToDeviceRpcRequestMsg requestMsg, String versionedId) {
TbLwM2MCancelObserveRequest downlink = TbLwM2MCancelObserveRequest.builder().versionedId(versionedId).timeout(this.config.getTimeout()).build();
TbLwM2MCancelObserveRequest downlink = TbLwM2MCancelObserveRequest.builder().versionedId(versionedId).timeout(clientContext.getRequestTimeout(client)).build();
var mainCallback = new TbLwM2MCancelObserveCallback(logService, client, versionedId);
var rpcCallback = new RpcCancelObserveCallback(transportService, client, requestMsg, mainCallback);
downlinkHandler.sendCancelObserveRequest(client, downlink, rpcCallback);
}
private void sendDeleteRequest(LwM2mClient client, TransportProtos.ToDeviceRpcRequestMsg requestMsg, String versionedId) {
TbLwM2MDeleteRequest downlink = TbLwM2MDeleteRequest.builder().versionedId(versionedId).timeout(this.config.getTimeout()).build();
TbLwM2MDeleteRequest downlink = TbLwM2MDeleteRequest.builder().versionedId(versionedId).timeout(clientContext.getRequestTimeout(client)).build();
var mainCallback = new TbLwM2MDeleteCallback(logService, client, versionedId);
var rpcCallback = new RpcEmptyResponseCallback<>(transportService, client, requestMsg, mainCallback);
downlinkHandler.sendDeleteRequest(client, downlink, rpcCallback);
}
private void sendCancelAllObserveRequest(LwM2mClient client, TransportProtos.ToDeviceRpcRequestMsg requestMsg) {
TbLwM2MCancelAllRequest downlink = TbLwM2MCancelAllRequest.builder().timeout(this.config.getTimeout()).build();
TbLwM2MCancelAllRequest downlink = TbLwM2MCancelAllRequest.builder().timeout(clientContext.getRequestTimeout(client)).build();
var mainCallback = new TbLwM2MCancelAllObserveCallback(logService, client);
var rpcCallback = new RpcCancelAllObserveCallback(transportService, client, requestMsg, mainCallback);
downlinkHandler.sendCancelAllRequest(client, downlink, rpcCallback);

10
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/uplink/DefaultLwM2MUplinkMsgHandler.java

@ -457,7 +457,7 @@ public class DefaultLwM2MUplinkMsgHandler extends LwM2MExecutorAwareService impl
}
private void sendDiscoverRequest(LwM2mClient lwM2MClient, String targetId) {
TbLwM2MDiscoverRequest request = TbLwM2MDiscoverRequest.builder().versionedId(targetId).timeout(this.config.getTimeout()).build();
TbLwM2MDiscoverRequest request = TbLwM2MDiscoverRequest.builder().versionedId(targetId).timeout(clientContext.getRequestTimeout(lwM2MClient)).build();
defaultLwM2MDownlinkMsgHandler.sendDiscoverRequest(lwM2MClient, request, new TbLwM2MDiscoverCallback(logService, lwM2MClient, targetId));
}
@ -466,7 +466,7 @@ public class DefaultLwM2MUplinkMsgHandler extends LwM2MExecutorAwareService impl
}
private void sendReadRequest(LwM2mClient lwM2MClient, String versionedId, DownlinkRequestCallback<ReadRequest, ReadResponse> callback) {
TbLwM2MReadRequest request = TbLwM2MReadRequest.builder().versionedId(versionedId).timeout(this.config.getTimeout()).build();
TbLwM2MReadRequest request = TbLwM2MReadRequest.builder().versionedId(versionedId).timeout(clientContext.getRequestTimeout(lwM2MClient)).build();
defaultLwM2MDownlinkMsgHandler.sendReadRequest(lwM2MClient, request, callback);
}
@ -475,17 +475,17 @@ public class DefaultLwM2MUplinkMsgHandler extends LwM2MExecutorAwareService impl
}
private void sendObserveRequest(LwM2mClient lwM2MClient, String versionedId, DownlinkRequestCallback<ObserveRequest, ObserveResponse> callback) {
TbLwM2MObserveRequest request = TbLwM2MObserveRequest.builder().versionedId(versionedId).timeout(this.config.getTimeout()).build();
TbLwM2MObserveRequest request = TbLwM2MObserveRequest.builder().versionedId(versionedId).timeout(clientContext.getRequestTimeout(lwM2MClient)).build();
defaultLwM2MDownlinkMsgHandler.sendObserveRequest(lwM2MClient, request, callback);
}
private void sendWriteAttributesRequest(LwM2mClient lwM2MClient, String targetId, ObjectAttributes params) {
TbLwM2MWriteAttributesRequest request = TbLwM2MWriteAttributesRequest.builder().versionedId(targetId).attributes(params).timeout(this.config.getTimeout()).build();
TbLwM2MWriteAttributesRequest request = TbLwM2MWriteAttributesRequest.builder().versionedId(targetId).attributes(params).timeout(clientContext.getRequestTimeout(lwM2MClient)).build();
defaultLwM2MDownlinkMsgHandler.sendWriteAttributesRequest(lwM2MClient, request, new TbLwM2MWriteAttributesCallback(logService, lwM2MClient, targetId));
}
private void sendCancelObserveRequest(String versionedId, LwM2mClient client) {
TbLwM2MCancelObserveRequest request = TbLwM2MCancelObserveRequest.builder().versionedId(versionedId).timeout(this.config.getTimeout()).build();
TbLwM2MCancelObserveRequest request = TbLwM2MCancelObserveRequest.builder().versionedId(versionedId).timeout(clientContext.getRequestTimeout(client)).build();
defaultLwM2MDownlinkMsgHandler.sendCancelObserveRequest(client, request, new TbLwM2MCancelObserveCallback(logService, client, versionedId));
}

57
common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java

@ -17,7 +17,6 @@ package org.thingsboard.server.transport.mqtt;
import com.fasterxml.jackson.databind.JsonNode;
import com.google.gson.JsonParseException;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.handler.codec.mqtt.MqttConnAckMessage;
@ -40,6 +39,7 @@ import io.netty.util.CharsetUtil;
import io.netty.util.ReferenceCountUtil;
import io.netty.util.concurrent.Future;
import io.netty.util.concurrent.GenericFutureListener;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.thingsboard.server.common.data.DataConstants;
@ -129,6 +129,7 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement
private final ConcurrentHashMap<String, String> otaPackSessions;
private final ConcurrentHashMap<String, Integer> chunkSizes;
private final ConcurrentMap<Integer, TransportProtos.ToDeviceRpcRequestMsg> rpcAwaitingAck;
MqttTransportHandler(MqttTransportContext context, SslHandler sslHandler) {
this.sessionId = UUID.randomUUID();
@ -140,6 +141,7 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement
this.deviceSessionCtx = new DeviceSessionCtx(sessionId, mqttQoSMap, context);
this.otaPackSessions = new ConcurrentHashMap<>();
this.chunkSizes = new ConcurrentHashMap<>();
this.rpcAwaitingAck = new ConcurrentHashMap<>();
}
@Override
@ -243,6 +245,13 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement
processDisconnect(ctx);
}
break;
case PUBACK:
int msgId = ((MqttPubAckMessage) msg).variableHeader().messageId();
TransportProtos.ToDeviceRpcRequestMsg rpcRequest = rpcAwaitingAck.remove(msgId);
if (rpcRequest != null) {
transportService.process(deviceSessionCtx.getSessionInfo(), rpcRequest, false, TransportServiceCallback.EMPTY);
}
break;
default:
break;
}
@ -815,16 +824,22 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement
public void onToDeviceRpcRequest(UUID sessionId, TransportProtos.ToDeviceRpcRequestMsg rpcRequest) {
log.trace("[{}] Received RPC command to device", sessionId);
try {
deviceSessionCtx.getPayloadAdaptor().convertToPublish(deviceSessionCtx, rpcRequest)
.ifPresent(payload -> {
ChannelFuture channelFuture = deviceSessionCtx.getChannel().writeAndFlush(payload);
if (rpcRequest.getPersisted()) {
channelFuture.addListener(future ->
transportService.process(deviceSessionCtx.getSessionInfo(), rpcRequest,
future.cause() != null, TransportServiceCallback.EMPTY)
);
}
});
deviceSessionCtx.getPayloadAdaptor().convertToPublish(deviceSessionCtx, rpcRequest).ifPresent(payload -> {
RequestInfo requestInfo = publish(payload, deviceSessionCtx);
int msgId = requestInfo.getMsgId();
if (isAckExpected(payload)) {
if (rpcRequest.getPersisted()) {
rpcAwaitingAck.put(msgId, rpcRequest);
context.getScheduler().schedule(() -> {
TransportProtos.ToDeviceRpcRequestMsg awaitingAckMsg = rpcAwaitingAck.remove(msgId);
if (awaitingAckMsg != null) {
transportService.process(deviceSessionCtx.getSessionInfo(), rpcRequest, true, TransportServiceCallback.EMPTY);
}
}, Math.max(0, rpcRequest.getExpirationTime() - System.currentTimeMillis()), TimeUnit.MILLISECONDS);
}
}
});
} catch (Exception e) {
log.trace("[{}] Failed to convert device RPC command to MQTT msg", sessionId, e);
}
@ -840,6 +855,19 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement
}
}
private RequestInfo publish(MqttMessage message, DeviceSessionCtx deviceSessionCtx) {
deviceSessionCtx.getChannel().writeAndFlush(message);
int msgId = ((MqttPublishMessage) message).variableHeader().packetId();
RequestInfo requestInfo = new RequestInfo(msgId, System.currentTimeMillis(), deviceSessionCtx.getSessionInfo());
return requestInfo;
}
private boolean isAckExpected(MqttMessage message) {
return message.fixedHeader().qosLevel().value() > 0;
}
@Override
public void onDeviceProfileUpdate(TransportProtos.SessionInfoProto sessionInfo, DeviceProfile deviceProfile) {
deviceSessionCtx.onDeviceProfileUpdate(sessionInfo, deviceProfile);
@ -849,4 +877,11 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement
public void onDeviceUpdate(TransportProtos.SessionInfoProto sessionInfo, Device device, Optional<DeviceProfile> deviceProfileOpt) {
deviceSessionCtx.onDeviceUpdate(sessionInfo, device, deviceProfileOpt);
}
@Data
public static class RequestInfo {
private final int msgId;
private final long requestTime;
private final TransportProtos.SessionInfoProto sessionInfo;
}
}

2
common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/auth/TransportDeviceInfo.java

@ -35,5 +35,5 @@ public class TransportDeviceInfo implements Serializable {
private String deviceType;
private PowerMode powerMode;
private String additionalInfo;
private Long edrxCycle;
}

1
common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java

@ -443,6 +443,7 @@ public class DefaultTransportService implements TransportService {
tdi.setDeviceType(di.getDeviceType());
if (StringUtils.isNotEmpty(di.getPowerMode())) {
tdi.setPowerMode(PowerMode.valueOf(di.getPowerMode()));
tdi.setEdrxCycle(di.getEdrxCycle());
}
return tdi;
}

38
dao/src/main/resources/sql/schema-entities-idx-psql-addon.sql

@ -0,0 +1,38 @@
--
-- Copyright © 2016-2021 The Thingsboard Authors
--
-- Licensed under the Apache License, Version 2.0 (the "License");
-- you may not use this file except in compliance with the License.
-- You may obtain a copy of the License at
--
-- http://www.apache.org/licenses/LICENSE-2.0
--
-- Unless required by applicable law or agreed to in writing, software
-- distributed under the License is distributed on an "AS IS" BASIS,
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-- See the License for the specific language governing permissions and
-- limitations under the License.
--
-- This file describes PostgreSQL-specific indexes that not supported by hsql
-- It is not a stand-alone file! Run schema-entities-idx.sql before!
-- Note: Hibernate DESC order translates to native SQL "ORDER BY .. DESC NULLS LAST"
-- While creating index PostgreSQL transforms short notation (ts DESC) to the full (DESC NULLS FIRST)
-- That difference between NULLS LAST and NULLS FIRST prevents to hit index while querying latest by ts
-- That why we need to define DESC index explicitly as (ts DESC NULLS LAST)
CREATE INDEX IF NOT EXISTS idx_event_ts
ON public.event
(ts DESC NULLS LAST)
WITH (FILLFACTOR=95);
COMMENT ON INDEX public.idx_event_ts
IS 'This index helps to delete events by TTL using timestamp';
CREATE INDEX IF NOT EXISTS idx_event_tenant_entity_type_entity_event_type_created_time_des
ON public.event
(tenant_id ASC, entity_type ASC, entity_id ASC, event_type ASC, created_time DESC NULLS LAST)
WITH (FILLFACTOR=95);
COMMENT ON INDEX public.idx_event_tenant_entity_type_entity_event_type_created_time_des
IS 'This index helps to open latest events on UI fast';

2
dao/src/main/resources/sql/schema-entities-idx.sql

@ -22,8 +22,6 @@ CREATE INDEX IF NOT EXISTS idx_alarm_tenant_created_time ON alarm(tenant_id, cre
CREATE INDEX IF NOT EXISTS idx_alarm_tenant_alarm_type_created_time ON alarm(tenant_id, type, created_time DESC);
CREATE INDEX IF NOT EXISTS idx_event_type_entity_id ON event(tenant_id, event_type, entity_type, entity_id);
CREATE INDEX IF NOT EXISTS idx_relation_to_id ON relation(relation_type_group, to_type, to_id);
CREATE INDEX IF NOT EXISTS idx_relation_from_id ON relation(relation_type_group, from_type, from_id);

2
dao/src/test/resources/sql-test.properties

@ -15,7 +15,7 @@ spring.jpa.database-platform=org.hibernate.dialect.HSQLDialect
spring.datasource.username=sa
spring.datasource.password=
spring.datasource.url=jdbc:hsqldb:file:target/tmp/testDb;sql.enforce_size=false
spring.datasource.url=jdbc:hsqldb:file:target/tmp/testDb;sql.enforce_size=false;sql.syntax_pgs=true
spring.datasource.driverClassName=org.hsqldb.jdbc.JDBCDriver
spring.datasource.hikari.maximumPoolSize = 50

5
docker/tb-coap-transport.env

@ -9,4 +9,7 @@ METRICS_ENABLED=true
METRICS_ENDPOINTS_EXPOSE=prometheus
WEB_APPLICATION_ENABLE=true
WEB_APPLICATION_TYPE=servlet
HTTP_BIND_PORT=8081
HTTP_BIND_PORT=8081
CACHE_TYPE=redis
REDIS_HOST=redis

5
docker/tb-http-transport.env

@ -6,4 +6,7 @@ HTTP_BIND_PORT=8081
HTTP_REQUEST_TIMEOUT=60000
METRICS_ENABLED=true
METRICS_ENDPOINTS_EXPOSE=prometheus
METRICS_ENDPOINTS_EXPOSE=prometheus
CACHE_TYPE=redis
REDIS_HOST=redis

3
docker/tb-lwm2m-transport.env

@ -10,3 +10,6 @@ METRICS_ENDPOINTS_EXPOSE=prometheus
WEB_APPLICATION_ENABLE=true
WEB_APPLICATION_TYPE=servlet
HTTP_BIND_PORT=8081
CACHE_TYPE=redis
REDIS_HOST=redis

5
docker/tb-mqtt-transport.env

@ -9,4 +9,7 @@ METRICS_ENABLED=true
METRICS_ENDPOINTS_EXPOSE=prometheus
WEB_APPLICATION_ENABLE=true
WEB_APPLICATION_TYPE=servlet
HTTP_BIND_PORT=8081
HTTP_BIND_PORT=8081
CACHE_TYPE=redis
REDIS_HOST=redis

3
docker/tb-snmp-transport.env

@ -6,3 +6,6 @@ METRICS_ENDPOINTS_EXPOSE=prometheus
WEB_APPLICATION_ENABLE=true
WEB_APPLICATION_TYPE=servlet
HTTP_BIND_PORT=8081
CACHE_TYPE=redis
REDIS_HOST=redis

2
msa/black-box-tests/pom.xml

@ -35,7 +35,7 @@
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<main.dir>${basedir}/../..</main.dir>
<blackBoxTests.skip>true</blackBoxTests.skip>
<testcontainers.version>1.9.1</testcontainers.version>
<testcontainers.version>1.11.4</testcontainers.version>
<zeroturnaround.version>1.10</zeroturnaround.version>
<httpclient.version>4.5.13</httpclient.version>
</properties>

35
msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ContainerTestSuite.java

@ -15,6 +15,7 @@
*/
package org.thingsboard.server.msa;
import lombok.extern.slf4j.Slf4j;
import org.junit.ClassRule;
import org.junit.extensions.cpsuite.ClasspathSuite;
import org.junit.rules.ExternalResource;
@ -32,6 +33,7 @@ import java.util.Map;
@RunWith(ClasspathSuite.class)
@ClasspathSuite.ClassnameFilters({"org.thingsboard.server.msa.*Test"})
@Slf4j
public class ContainerTestSuite {
private static DockerComposeContainer<?> testContainer;
@ -43,17 +45,28 @@ public class ContainerTestSuite {
public static DockerComposeContainer getTestContainer() {
if (testContainer == null) {
boolean skipTailChildContainers = Boolean.valueOf(System.getProperty("blackBoxTests.skipTailChildContainers"));
testContainer = new DockerComposeContainer<>(
new File("./../../docker/docker-compose.yml"),
new File("./../../docker/docker-compose.postgres.yml"),
new File("./../../docker/docker-compose.postgres.volumes.yml"),
new File("./../../docker/docker-compose.kafka.yml"))
.withPull(false)
.withLocalCompose(true)
.withTailChildContainers(!skipTailChildContainers)
.withEnv(installTb.getEnv())
.withEnv("LOAD_BALANCER_NAME", "")
.withExposedService("haproxy", 80, Wait.forHttp("/swagger-ui.html").withStartupTimeout(Duration.ofSeconds(400)));
try {
String logRegexp = ".*Going to recalculate partitions.*";
testContainer = new DockerComposeContainer<>(
new File("./../../docker/docker-compose.yml"),
new File("./../../docker/docker-compose.postgres.yml"),
new File("./../../docker/docker-compose.postgres.volumes.yml"),
new File("./../../docker/docker-compose.kafka.yml"))
.withPull(false)
.withLocalCompose(true)
.withTailChildContainers(!skipTailChildContainers)
.withEnv(installTb.getEnv())
.withEnv("LOAD_BALANCER_NAME", "")
.withExposedService("haproxy", 80, Wait.forHttp("/swagger-ui.html").withStartupTimeout(Duration.ofSeconds(400)))
.waitingFor("tb-http-transport1", Wait.forLogMessage(logRegexp, 1).withStartupTimeout(Duration.ofSeconds(400)))
.waitingFor("tb-http-transport2", Wait.forLogMessage(logRegexp, 1).withStartupTimeout(Duration.ofSeconds(400)))
.waitingFor("tb-mqtt-transport1", Wait.forLogMessage(logRegexp, 1).withStartupTimeout(Duration.ofSeconds(400)))
.waitingFor("tb-mqtt-transport2", Wait.forLogMessage(logRegexp, 1).withStartupTimeout(Duration.ofSeconds(400)));
} catch (Exception e) {
log.error("Failed to create test container", e);
throw e;
}
}
return testContainer;
}

4
pom.xml

@ -90,7 +90,7 @@
<jts.version>1.15.0</jts.version>
<bouncycastle.version>1.64</bouncycastle.version>
<winsw.version>2.0.1</winsw.version>
<hsqldb.version>2.5.0</hsqldb.version>
<hsqldb.version>2.6.0</hsqldb.version>
<dbunit.version>2.5.3</dbunit.version>
<spring-test-dbunit.version>1.2.1</spring-test-dbunit.version>
<postgresql.driver.version>42.2.20</postgresql.driver.version>
@ -99,7 +99,7 @@
</sonar.exclusions>
<elasticsearch.version>5.0.2</elasticsearch.version>
<delight-nashorn-sandbox.version>0.1.16</delight-nashorn-sandbox.version>
<kafka.version>2.6.0</kafka.version>
<kafka.version>2.8.0</kafka.version>
<bucket4j.version>4.1.1</bucket4j.version>
<fst.version>2.57</fst.version>
<antlr.version>2.7.7</antlr.version>

4
rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/MailService.java

@ -46,9 +46,9 @@ public interface MailService {
void sendAccountLockoutEmail(String lockoutEmail, String email, Integer maxFailedLoginAttempts) throws ThingsboardException;
void send(TenantId tenantId, CustomerId customerId, String from, String to, String cc, String bcc, String subject, String body, boolean isHtml, Map<String, String> images) throws ThingsboardException;
void send(TenantId tenantId, CustomerId customerId, TbEmail tbEmail) throws ThingsboardException;
void send(TenantId tenantId, CustomerId customerId, String from, String to, String cc, String bcc, String subject, String body, boolean isHtml, Map<String, String> images, JavaMailSender javaMailSender) throws ThingsboardException;
void send(TenantId tenantId, CustomerId customerId, TbEmail tbEmail, JavaMailSender javaMailSender) throws ThingsboardException;
void sendApiFeatureStateEmail(ApiFeature apiFeature, ApiUsageStateValue stateValue, String email, ApiUsageStateMailMessage msg) throws ThingsboardException;

4
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mail/EmailPojo.java → rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbEmail.java

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.rule.engine.mail;
package org.thingsboard.rule.engine.api;
import lombok.Builder;
import lombok.Data;
@ -22,7 +22,7 @@ import java.util.Map;
@Data
@Builder
class EmailPojo {
public class TbEmail {
private final String from;
private final String to;

9
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mail/TbMsgToEmailNode.java

@ -22,6 +22,7 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StringUtils;
import org.thingsboard.rule.engine.api.RuleNode;
import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.rule.engine.api.TbEmail;
import org.thingsboard.rule.engine.api.TbNode;
import org.thingsboard.rule.engine.api.TbNodeConfiguration;
import org.thingsboard.rule.engine.api.TbNodeException;
@ -65,7 +66,7 @@ public class TbMsgToEmailNode implements TbNode {
@Override
public void onMsg(TbContext ctx, TbMsg msg) {
try {
EmailPojo email = convert(msg);
TbEmail email = convert(msg);
TbMsg emailMsg = buildEmailMsg(ctx, msg, email);
ctx.tellNext(emailMsg, SUCCESS);
} catch (Exception ex) {
@ -74,13 +75,13 @@ public class TbMsgToEmailNode implements TbNode {
}
}
private TbMsg buildEmailMsg(TbContext ctx, TbMsg msg, EmailPojo email) throws JsonProcessingException {
private TbMsg buildEmailMsg(TbContext ctx, TbMsg msg, TbEmail email) throws JsonProcessingException {
String emailJson = MAPPER.writeValueAsString(email);
return ctx.transformMsg(msg, SEND_EMAIL_TYPE, msg.getOriginator(), msg.getMetaData().copy(), emailJson);
}
private EmailPojo convert(TbMsg msg) throws IOException {
EmailPojo.EmailPojoBuilder builder = EmailPojo.builder();
private TbEmail convert(TbMsg msg) throws IOException {
TbEmail.TbEmailBuilder builder = TbEmail.builder();
builder.from(fromTemplate(this.config.getFromTemplate(), msg));
builder.to(fromTemplate(this.config.getToTemplate(), msg));
builder.cc(fromTemplate(this.config.getCcTemplate(), msg));

15
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mail/TbSendEmailNode.java

@ -21,6 +21,7 @@ import org.apache.commons.lang3.StringUtils;
import org.springframework.mail.javamail.JavaMailSenderImpl;
import org.thingsboard.rule.engine.api.RuleNode;
import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.rule.engine.api.TbEmail;
import org.thingsboard.rule.engine.api.TbNode;
import org.thingsboard.rule.engine.api.TbNodeConfiguration;
import org.thingsboard.rule.engine.api.TbNodeException;
@ -71,7 +72,7 @@ public class TbSendEmailNode implements TbNode {
public void onMsg(TbContext ctx, TbMsg msg) {
try {
validateType(msg.getType());
EmailPojo email = getEmail(msg);
TbEmail email = getEmail(msg);
withCallback(ctx.getMailExecutor().executeAsync(() -> {
sendEmail(ctx, msg, email);
return null;
@ -83,18 +84,16 @@ public class TbSendEmailNode implements TbNode {
}
}
private void sendEmail(TbContext ctx, TbMsg msg, EmailPojo email) throws Exception {
private void sendEmail(TbContext ctx, TbMsg msg, TbEmail email) throws Exception {
if (this.config.isUseSystemSmtpSettings()) {
ctx.getMailService().send(ctx.getTenantId(), msg.getCustomerId(), email.getFrom(), email.getTo(), email.getCc(),
email.getBcc(), email.getSubject(), email.getBody(), email.isHtml(), email.getImages());
ctx.getMailService().send(ctx.getTenantId(), msg.getCustomerId(), email);
} else {
ctx.getMailService().send(ctx.getTenantId(), msg.getCustomerId(), email.getFrom(), email.getTo(), email.getCc(),
email.getBcc(), email.getSubject(), email.getBody(), email.isHtml(), email.getImages(), this.mailSender);
ctx.getMailService().send(ctx.getTenantId(), msg.getCustomerId(), email, this.mailSender);
}
}
private EmailPojo getEmail(TbMsg msg) throws IOException {
EmailPojo email = MAPPER.readValue(msg.getData(), EmailPojo.class);
private TbEmail getEmail(TbMsg msg) throws IOException {
TbEmail email = MAPPER.readValue(msg.getData(), TbEmail.class);
if (StringUtils.isBlank(email.getTo())) {
throw new IllegalStateException("Email destination can not be blank [" + email.getTo() + "]");
}

2
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rpc/TbSendRPCRequestNode.java

@ -81,7 +81,7 @@ public class TbSendRPCRequestNode implements TbNode {
tmp = msg.getMetaData().getValue("oneway");
boolean oneway = !StringUtils.isEmpty(tmp) && Boolean.parseBoolean(tmp);
tmp = msg.getMetaData().getValue("persisted");
tmp = msg.getMetaData().getValue(DataConstants.PERSISTENT);
boolean persisted = !StringUtils.isEmpty(tmp) && Boolean.parseBoolean(tmp);
tmp = msg.getMetaData().getValue("requestUUID");

5
rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/mail/TbMsgToEmailNodeTest.java

@ -23,6 +23,7 @@ import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.rule.engine.api.TbEmail;
import org.thingsboard.rule.engine.api.TbNodeConfiguration;
import org.thingsboard.rule.engine.api.TbNodeException;
import org.thingsboard.server.common.data.id.DeviceId;
@ -79,9 +80,9 @@ public class TbMsgToEmailNodeTest {
assertEquals("oreo", metadataCaptor.getValue().getValue("username"));
assertNotSame(metaData, metadataCaptor.getValue());
EmailPojo actual = new ObjectMapper().readValue(dataCaptor.getValue().getBytes(), EmailPojo.class);
TbEmail actual = new ObjectMapper().readValue(dataCaptor.getValue().getBytes(), TbEmail.class);
EmailPojo expected = new EmailPojo.EmailPojoBuilder()
TbEmail expected = TbEmail.builder()
.from("test@mail.org")
.to("user@email.io")
.subject("Hi oreo there")

8
transport/lwm2m/pom.xml

@ -45,10 +45,10 @@
</properties>
<dependencies>
<!-- <dependency>-->
<!-- <groupId>org.thingsboard.common.transport</groupId>-->
<!-- <artifactId>transport-api</artifactId>-->
<!-- </dependency>-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.thingsboard.common.transport</groupId>
<artifactId>lwm2m</artifactId>

2
ui-ngx/src/app/app-routing.module.ts

@ -30,7 +30,7 @@ const routes: Routes = [
];
@NgModule({
imports: [RouterModule.forRoot(routes, { relativeLinkResolution: 'legacy' })],
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule { }

8
ui-ngx/src/app/core/api/widget-api.models.ts

@ -69,8 +69,8 @@ export interface WidgetSubscriptionApi {
}
export interface RpcApi {
sendOneWayCommand: (method: string, params?: any, timeout?: number, requestUUID?: string) => Observable<any>;
sendTwoWayCommand: (method: string, params?: any, timeout?: number, requestUUID?: string) => Observable<any>;
sendOneWayCommand: (method: string, params?: any, timeout?: number, persistent?: boolean, requestUUID?: string) => Observable<any>;
sendTwoWayCommand: (method: string, params?: any, timeout?: number, persistent?: boolean, requestUUID?: string) => Observable<any>;
}
export interface IWidgetUtils {
@ -299,8 +299,8 @@ export interface IWidgetSubscription {
onResetTimewindow(): void;
updateTimewindowConfig(newTimewindow: Timewindow): void;
sendOneWayCommand(method: string, params?: any, timeout?: number, requestUUID?: string): Observable<any>;
sendTwoWayCommand(method: string, params?: any, timeout?: number, requestUUID?: string): Observable<any>;
sendOneWayCommand(method: string, params?: any, timeout?: number, persistent?: boolean, requestUUID?: string): Observable<any>;
sendTwoWayCommand(method: string, params?: any, timeout?: number, persistent?: boolean, requestUUID?: string): Observable<any>;
clearRpcError(): void;
subscribe(): void;

12
ui-ngx/src/app/core/api/widget-subscription.ts

@ -644,12 +644,12 @@ export class WidgetSubscription implements IWidgetSubscription {
}
}
sendOneWayCommand(method: string, params?: any, timeout?: number, requestUUID?: string): Observable<any> {
return this.sendCommand(true, method, params, timeout, requestUUID);
sendOneWayCommand(method: string, params?: any, timeout?: number, persistent?: boolean, requestUUID?: string): Observable<any> {
return this.sendCommand(true, method, params, timeout, persistent, requestUUID);
}
sendTwoWayCommand(method: string, params?: any, timeout?: number, requestUUID?: string): Observable<any> {
return this.sendCommand(false, method, params, timeout, requestUUID);
sendTwoWayCommand(method: string, params?: any, timeout?: number, persistent?: boolean, requestUUID?: string): Observable<any> {
return this.sendCommand(false, method, params, timeout, persistent, requestUUID);
}
clearRpcError(): void {
@ -658,7 +658,8 @@ export class WidgetSubscription implements IWidgetSubscription {
this.callbacks.onRpcErrorCleared(this);
}
sendCommand(oneWayElseTwoWay: boolean, method: string, params?: any, timeout?: number, requestUUID?: string): Observable<any> {
sendCommand(oneWayElseTwoWay: boolean, method: string, params?: any,
timeout?: number, persistent?: boolean, requestUUID?: string): Observable<any> {
if (!this.rpcEnabled) {
return throwError(new Error('Rpc disabled!'));
} else {
@ -670,6 +671,7 @@ export class WidgetSubscription implements IWidgetSubscription {
const requestBody: any = {
method,
params,
persistent,
requestUUID
};
if (timeout && timeout > 0) {

9
ui-ngx/src/app/core/http/attribute.service.ts

@ -135,4 +135,13 @@ export class AttributeService {
return this.http.get<TimeseriesData>(url, defaultHttpOptionsFromConfig(config));
}
public getEntityTimeseriesLatest(entityId: EntityId, keys?: Array<string>,
useStrictDataTypes = false, config?: RequestConfig): Observable<TimeseriesData> {
let url = `/api/plugins/telemetry/${entityId.entityType}/${entityId.id}/values/timeseries?useStrictDataTypes=${useStrictDataTypes}`;
if (isDefinedAndNotNull(keys) && keys.length) {
url += `&keys=${keys.join(',')}`;
}
return this.http.get<TimeseriesData>(url, defaultHttpOptionsFromConfig(config));
}
}

0
ui-ngx/src/app/modules/home/components/device/security-config-lwm2m-server.component.html → ui-ngx/src/app/modules/home/components/device/device-credentials-lwm2m-server.component.html

10
ui-ngx/src/app/modules/home/components/device/security-config-lwm2m-server.component.ts → ui-ngx/src/app/modules/home/components/device/device-credentials-lwm2m-server.component.ts

@ -39,24 +39,24 @@ import { takeUntil } from 'rxjs/operators';
import { Subject } from 'rxjs';
@Component({
selector: 'tb-security-config-lwm2m-server',
templateUrl: './security-config-lwm2m-server.component.html',
selector: 'tb-device-credentials-lwm2m-server',
templateUrl: './device-credentials-lwm2m-server.component.html',
styleUrls: [],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => SecurityConfigLwm2mServerComponent),
useExisting: forwardRef(() => DeviceCredentialsLwm2mServerComponent),
multi: true
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => SecurityConfigLwm2mServerComponent),
useExisting: forwardRef(() => DeviceCredentialsLwm2mServerComponent),
multi: true
}
]
})
export class SecurityConfigLwm2mServerComponent implements OnDestroy, ControlValueAccessor, Validator {
export class DeviceCredentialsLwm2mServerComponent implements OnDestroy, ControlValueAccessor, Validator {
serverFormGroup: FormGroup;
securityConfigLwM2MType = Lwm2mSecurityType;

25
ui-ngx/src/app/modules/home/components/device/security-config-lwm2m.component.html → ui-ngx/src/app/modules/home/components/device/device-credentials-lwm2m.component.html

@ -15,8 +15,8 @@
limitations under the License.
-->
<mat-tab-group>
<mat-tab label="{{ 'device.lwm2m-security-config.client-tab' | translate }}" [formGroup]="lwm2mConfigFormGroup">
<mat-tab-group [formGroup]="lwm2mConfigFormGroup">
<mat-tab label="{{ 'device.lwm2m-security-config.client-tab' | translate }}">
<ng-container formGroupName="client">
<mat-form-field class="mat-block">
<mat-label translate>device.lwm2m-security-config.endpoint</mat-label>
@ -79,7 +79,7 @@
</mat-form-field>
</ng-container>
</mat-tab>
<mat-tab label="{{ 'device.lwm2m-security-config.bootstrap-tab' | translate }}" [formGroup]="lwm2mConfigFormGroup">
<mat-tab label="{{ 'device.lwm2m-security-config.bootstrap-tab' | translate }}">
<div style="padding: 2px;" formGroupName="bootstrap">
<mat-accordion multi="true" class="mat-body-1">
<mat-expansion-panel>
@ -89,9 +89,9 @@
</mat-panel-title>
</mat-expansion-panel-header>
<ng-template matExpansionPanelContent>
<tb-security-config-lwm2m-server
<tb-device-credentials-lwm2m-server
formControlName="bootstrapServer">
</tb-security-config-lwm2m-server>
</tb-device-credentials-lwm2m-server>
</ng-template>
</mat-expansion-panel>
<mat-expansion-panel>
@ -101,23 +101,12 @@
</mat-panel-title>
</mat-expansion-panel-header>
<ng-template matExpansionPanelContent>
<tb-security-config-lwm2m-server
<tb-device-credentials-lwm2m-server
formControlName="lwm2mServer">
</tb-security-config-lwm2m-server>
</tb-device-credentials-lwm2m-server>
</ng-template>
</mat-expansion-panel>
</mat-accordion>
</div>
</mat-tab>
<mat-tab label="{{ 'device.lwm2m-security-config.config-json-tab' | translate }}">
<ng-template matTabContent>
<tb-json-object-edit
[ngModel]="lwm2mConfigFormGroup.value"
label="{{ 'device.lwm2m-value' | translate }}"
readonly
[required]="true"
[fillHeight]="false">
</tb-json-object-edit>
</ng-template>
</mat-tab>
</mat-tab-group>

0
ui-ngx/src/app/modules/home/components/device/security-config-lwm2m.component.scss → ui-ngx/src/app/modules/home/components/device/device-credentials-lwm2m.component.scss

21
ui-ngx/src/app/modules/home/components/device/security-config-lwm2m.component.ts → ui-ngx/src/app/modules/home/components/device/device-credentials-lwm2m.component.ts

@ -40,24 +40,24 @@ import { takeUntil } from 'rxjs/operators';
import { isDefinedAndNotNull } from '@core/utils';
@Component({
selector: 'tb-security-config-lwm2m',
templateUrl: './security-config-lwm2m.component.html',
styleUrls: ['./security-config-lwm2m.component.scss'],
selector: 'tb-device-credentials-lwm2m',
templateUrl: './device-credentials-lwm2m.component.html',
styleUrls: ['./device-credentials-lwm2m.component.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => SecurityConfigLwm2mComponent),
useExisting: forwardRef(() => DeviceCredentialsLwm2mComponent),
multi: true
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => SecurityConfigLwm2mComponent),
useExisting: forwardRef(() => DeviceCredentialsLwm2mComponent),
multi: true
}
]
})
export class SecurityConfigLwm2mComponent implements ControlValueAccessor, Validator, OnDestroy {
export class DeviceCredentialsLwm2mComponent implements ControlValueAccessor, Validator, OnDestroy {
lwm2mConfigFormGroup: FormGroup;
securityConfigLwM2MType = Lwm2mSecurityType;
@ -89,6 +89,7 @@ export class SecurityConfigLwm2mComponent implements ControlValueAccessor, Valid
this.lwm2mConfigFormGroup.disable({emitEvent: false});
} else {
this.lwm2mConfigFormGroup.enable({emitEvent: false});
this.securityConfigClientUpdateValidators(this.lwm2mConfigFormGroup.get('client.securityConfigClientMode').value);
}
}
@ -126,21 +127,21 @@ export class SecurityConfigLwm2mComponent implements ControlValueAccessor, Valid
switch (mode) {
case Lwm2mSecurityType.NO_SEC:
this.setValidatorsNoSecX509();
this.lwm2mConfigFormGroup.get('client.cert').disable();
this.lwm2mConfigFormGroup.get('client.cert').disable({emitEvent: false});
break;
case Lwm2mSecurityType.X509:
this.setValidatorsNoSecX509();
this.lwm2mConfigFormGroup.get('client.cert').enable();
this.lwm2mConfigFormGroup.get('client.cert').enable({emitEvent: false});
break;
case Lwm2mSecurityType.PSK:
this.lenMaxKeyClient = LEN_MAX_PSK;
this.setValidatorsPskRpk(mode);
this.lwm2mConfigFormGroup.get('client.identity').enable();
this.lwm2mConfigFormGroup.get('client.identity').enable({emitEvent: false});
break;
case Lwm2mSecurityType.RPK:
this.lenMaxKeyClient = LEN_MAX_PUBLIC_KEY_RPK;
this.setValidatorsPskRpk(mode);
this.lwm2mConfigFormGroup.get('client.identity').disable();
this.lwm2mConfigFormGroup.get('client.identity').disable({emitEvent: false});
break;
}
this.lwm2mConfigFormGroup.get('client.identity').updateValueAndValidity({emitEvent: false});

48
ui-ngx/src/app/modules/home/components/device/device-credentials-mqtt-basic.component.html

@ -0,0 +1,48 @@
<!--
Copyright © 2016-2021 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<section [formGroup]="deviceCredentialsMqttFormGroup">
<mat-form-field class="mat-block">
<mat-label translate>device.client-id</mat-label>
<input matInput formControlName="clientId">
<mat-error *ngIf="deviceCredentialsMqttFormGroup.get('clientId').hasError('pattern')">
{{ 'device.client-id-pattern' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field class="mat-block">
<mat-label translate>device.user-name</mat-label>
<input matInput formControlName="userName" [required]="!!deviceCredentialsMqttFormGroup.get('password').value">
<mat-error *ngIf="deviceCredentialsMqttFormGroup.get('userName').hasError('required')">
{{ 'device.user-name-required' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field class="mat-block">
<mat-label translate>device.password</mat-label>
<input matInput formControlName="password"
autocomplete="new-password"
(ngModelChange)="passwordChanged()"
[type]="hidePassword ? 'password' : 'text'">
<button mat-icon-button matSuffix type="button"
(click)="hidePassword = !hidePassword"
[attr.aria-pressed]="hidePassword">
<mat-icon>{{hidePassword ? 'visibility_off' : 'visibility'}}</mat-icon>
</button>
</mat-form-field>
<tb-error style="margin-top: -12px; display: block;"
[error]="deviceCredentialsMqttFormGroup.hasError('atLeastOne') ?
('device.client-id-or-user-name-necessary' | translate) : ''"></tb-error>
</section>

133
ui-ngx/src/app/modules/home/components/device/device-credentials-mqtt-basic.component.ts

@ -0,0 +1,133 @@
///
/// Copyright © 2016-2021 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Component, forwardRef, Input, OnDestroy } from '@angular/core';
import {
ControlValueAccessor,
FormBuilder,
FormGroup,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
ValidationErrors,
Validator,
ValidatorFn,
Validators
} from '@angular/forms';
import { Subject } from 'rxjs';
import { DeviceCredentialMQTTBasic } from '@shared/models/device.models';
import { takeUntil } from 'rxjs/operators';
import { isDefinedAndNotNull, isEmptyStr } from '@core/utils';
@Component({
selector: 'tb-device-credentials-mqtt-basic',
templateUrl: './device-credentials-mqtt-basic.component.html',
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => DeviceCredentialsMqttBasicComponent),
multi: true
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => DeviceCredentialsMqttBasicComponent),
multi: true,
}],
styleUrls: []
})
export class DeviceCredentialsMqttBasicComponent implements ControlValueAccessor, Validator, OnDestroy {
@Input()
disabled: boolean;
deviceCredentialsMqttFormGroup: FormGroup;
hidePassword = true;
private destroy$ = new Subject();
private propagateChange = (v: any) => {};
constructor(public fb: FormBuilder) {
this.deviceCredentialsMqttFormGroup = this.fb.group({
clientId: [null, [Validators.pattern(/^[A-Za-z0-9]+$/)]],
userName: [null],
password: [null]
}, {validators: this.atLeastOne(Validators.required, ['clientId', 'userName'])});
this.deviceCredentialsMqttFormGroup.valueChanges.pipe(
takeUntil(this.destroy$)
).subscribe((value) => {
this.updateView(value);
});
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(fn: any): void {}
setDisabledState(isDisabled: boolean) {
this.disabled = isDisabled;
if (this.disabled) {
this.deviceCredentialsMqttFormGroup.disable({emitEvent: false});
} else {
this.deviceCredentialsMqttFormGroup.enable({emitEvent: false});
}
}
validate(): ValidationErrors | null {
return this.deviceCredentialsMqttFormGroup.valid ? null : {
deviceCredentialsMqttBasic: false
};
}
writeValue(mqttBasic: string) {
if (isDefinedAndNotNull(mqttBasic) && !isEmptyStr(mqttBasic)) {
const value = JSON.parse(mqttBasic);
this.deviceCredentialsMqttFormGroup.patchValue(value, {emitEvent: false});
}
}
updateView(value: DeviceCredentialMQTTBasic) {
const formValue = JSON.stringify(value);
this.propagateChange(formValue);
}
passwordChanged() {
const value = this.deviceCredentialsMqttFormGroup.get('password').value;
if (value !== '') {
this.deviceCredentialsMqttFormGroup.get('userName').setValidators([Validators.required]);
} else {
this.deviceCredentialsMqttFormGroup.get('userName').setValidators([]);
}
this.deviceCredentialsMqttFormGroup.get('userName').updateValueAndValidity({emitEvent: false});
}
private atLeastOne(validator: ValidatorFn, controls: string[] = null) {
return (group: FormGroup): ValidationErrors | null => {
if (!controls) {
controls = Object.keys(group.controls);
}
const hasAtLeastOne = group?.controls && controls.some(k => !validator(group.controls[k]));
return hasAtLeastOne ? null : {atLeastOne: true};
};
}
}

85
ui-ngx/src/app/modules/home/components/device/device-credentials.component.html

@ -16,7 +16,7 @@
-->
<section [formGroup]="deviceCredentialsFormGroup">
<mat-form-field class="mat-block">
<mat-form-field class="mat-block" [fxShow]="credentialsTypes?.length > 1">
<mat-label translate>device.credentials-type</mat-label>
<mat-select formControlName="credentialsType">
<mat-option *ngFor="let credentialsType of credentialsTypes" [value]="credentialsType">
@ -24,58 +24,35 @@
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field *ngIf="deviceCredentialsFormGroup.get('credentialsType').value === deviceCredentialsType.ACCESS_TOKEN"
class="mat-block">
<mat-label translate>device.access-token</mat-label>
<input matInput formControlName="credentialsId" required>
<mat-error *ngIf="deviceCredentialsFormGroup.get('credentialsId').hasError('required')">
{{ 'device.access-token-required' | translate }}
</mat-error>
<mat-error *ngIf="deviceCredentialsFormGroup.get('credentialsId').hasError('pattern')">
{{ 'device.access-token-invalid' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field *ngIf="deviceCredentialsFormGroup.get('credentialsType').value === deviceCredentialsType.X509_CERTIFICATE"
class="mat-block">
<mat-label translate>device.rsa-key</mat-label>
<textarea matInput formControlName="credentialsValue" cols="15" rows="5" required></textarea>
<mat-error *ngIf="deviceCredentialsFormGroup.get('credentialsValue').hasError('required')">
{{ 'device.rsa-key-required' | translate }}
</mat-error>
</mat-form-field>
<section *ngIf="deviceCredentialsFormGroup.get('credentialsType').value === deviceCredentialsType.MQTT_BASIC" formGroupName="credentialsBasic">
<mat-form-field class="mat-block">
<mat-label translate>device.client-id</mat-label>
<input matInput formControlName="clientId">
<mat-error *ngIf="deviceCredentialsFormGroup.get('credentialsBasic.clientId').hasError('pattern')">
{{ 'device.client-id-pattern' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field class="mat-block">
<mat-label translate>device.user-name</mat-label>
<input matInput formControlName="userName" [required]="!!deviceCredentialsFormGroup.get('credentialsBasic.password').value">
<mat-error *ngIf="deviceCredentialsFormGroup.get('credentialsBasic.userName').hasError('required')">
{{ 'device.user-name-required' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field class="mat-block">
<mat-label translate>device.password</mat-label>
<input matInput formControlName="password"
autocomplete="new-password"
(ngModelChange)="passwordChanged()"
[type]="hidePassword ? 'password' : 'text'">
<button mat-icon-button matSuffix type="button"
(click)="hidePassword = !hidePassword"
[attr.aria-pressed]="hidePassword">
<mat-icon>{{hidePassword ? 'visibility_off' : 'visibility'}}</mat-icon>
</button>
</mat-form-field>
<tb-error style="margin-top: -12px; display: block;"
[error]="deviceCredentialsFormGroup.get('credentialsBasic').hasError('atLeastOne') ?
('device.client-id-or-user-name-necessary' | translate) : ''"></tb-error>
</section>
<div *ngIf="deviceCredentialsFormGroup.get('credentialsType').value === deviceCredentialsType.LWM2M_CREDENTIALS">
<tb-security-config-lwm2m formControlName="credentialsValue">
</tb-security-config-lwm2m>
<div [ngSwitch]="deviceCredentialsFormGroup.get('credentialsType').value">
<ng-template [ngSwitchCase]="deviceCredentialsType.ACCESS_TOKEN">
<mat-form-field class="mat-block">
<mat-label translate>device.access-token</mat-label>
<input matInput formControlName="credentialsId" required>
<mat-error *ngIf="deviceCredentialsFormGroup.get('credentialsId').hasError('required')">
{{ 'device.access-token-required' | translate }}
</mat-error>
<mat-error *ngIf="deviceCredentialsFormGroup.get('credentialsId').hasError('pattern')">
{{ 'device.access-token-invalid' | translate }}
</mat-error>
</mat-form-field>
</ng-template>
<ng-template [ngSwitchCase]="deviceCredentialsType.X509_CERTIFICATE">
<mat-form-field class="mat-block">
<mat-label translate>device.rsa-key</mat-label>
<textarea matInput formControlName="credentialsValue" cols="15" rows="5" required></textarea>
<mat-error *ngIf="deviceCredentialsFormGroup.get('credentialsValue').hasError('required')">
{{ 'device.rsa-key-required' | translate }}
</mat-error>
</mat-form-field>
</ng-template>
<ng-template [ngSwitchCase]="deviceCredentialsType.MQTT_BASIC">
<tb-device-credentials-mqtt-basic formControlName="credentialsValue">
</tb-device-credentials-mqtt-basic>
</ng-template>
<ng-template [ngSwitchCase]="deviceCredentialsType.LWM2M_CREDENTIALS">
<tb-device-credentials-lwm2m formControlName="credentialsValue">
</tb-device-credentials-lwm2m>
</ng-template>
</div>
</section>

93
ui-ngx/src/app/modules/home/components/device/device-credentials.component.ts

@ -22,16 +22,15 @@ import {
FormGroup,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
ValidationErrors,
Validator,
ValidatorFn,
Validators
} from '@angular/forms';
import {
credentialTypeNames,
DeviceCredentialMQTTBasic,
credentialTypesByTransportType,
DeviceCredentials,
DeviceCredentialsType
DeviceCredentialsType,
DeviceTransportType
} from '@shared/models/device.models';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
@ -58,32 +57,40 @@ export class DeviceCredentialsComponent implements ControlValueAccessor, OnInit,
@Input()
disabled: boolean;
private deviceTransportTypeValue = DeviceTransportType.DEFAULT;
get deviceTransportType(): DeviceTransportType {
return this.deviceTransportTypeValue;
}
@Input()
set deviceTransportType(type: DeviceTransportType) {
if (type) {
this.deviceTransportTypeValue = type;
this.credentialsTypes = credentialTypesByTransportType.get(type);
const currentType = this.deviceCredentialsFormGroup.get('credentialsType').value;
if (!this.credentialsTypes.includes(currentType)) {
this.deviceCredentialsFormGroup.get('credentialsType').patchValue(this.credentialsTypes[0], {onlySelf: true});
}
}
}
private destroy$ = new Subject();
deviceCredentialsFormGroup: FormGroup;
deviceCredentialsType = DeviceCredentialsType;
credentialsTypes = Object.values(DeviceCredentialsType);
credentialsTypes = credentialTypesByTransportType.get(DeviceTransportType.DEFAULT);
credentialTypeNamesMap = credentialTypeNames;
hidePassword = true;
private propagateChange = (v: any) => {};
constructor(public fb: FormBuilder) {
this.deviceCredentialsFormGroup = this.fb.group({
credentialsType: [DeviceCredentialsType.ACCESS_TOKEN],
credentialsId: [null],
credentialsValue: [null],
credentialsBasic: this.fb.group({
clientId: [null, [Validators.pattern(/^[A-Za-z0-9]+$/)]],
userName: [null],
password: [null]
}, {validators: this.atLeastOne(Validators.required, ['clientId', 'userName'])})
credentialsValue: [null]
});
this.deviceCredentialsFormGroup.get('credentialsBasic').disable();
this.deviceCredentialsFormGroup.valueChanges.pipe(
takeUntil(this.destroy$)
).subscribe(() => {
@ -109,18 +116,11 @@ export class DeviceCredentialsComponent implements ControlValueAccessor, OnInit,
writeValue(value: DeviceCredentials | null): void {
if (isDefinedAndNotNull(value)) {
let credentialsBasic = {clientId: null, userName: null, password: null};
let credentialsValue = null;
if (value.credentialsType === DeviceCredentialsType.MQTT_BASIC) {
credentialsBasic = JSON.parse(value.credentialsValue) as DeviceCredentialMQTTBasic;
} else {
credentialsValue = value.credentialsValue;
}
const credentialsType = this.credentialsTypes.includes(value.credentialsType) ? value.credentialsType : this.credentialsTypes[0];
this.deviceCredentialsFormGroup.patchValue({
credentialsType: value.credentialsType,
credentialsType,
credentialsId: value.credentialsId,
credentialsValue,
credentialsBasic
credentialsValue: value.credentialsValue
}, {emitEvent: false});
this.updateValidators();
}
@ -128,10 +128,6 @@ export class DeviceCredentialsComponent implements ControlValueAccessor, OnInit,
updateView() {
const deviceCredentialsValue = this.deviceCredentialsFormGroup.value;
if (deviceCredentialsValue.credentialsType === DeviceCredentialsType.MQTT_BASIC) {
deviceCredentialsValue.credentialsValue = JSON.stringify(deviceCredentialsValue.credentialsBasic);
}
delete deviceCredentialsValue.credentialsBasic;
this.propagateChange(deviceCredentialsValue);
}
@ -163,14 +159,12 @@ export class DeviceCredentialsComponent implements ControlValueAccessor, OnInit,
credentialsTypeChanged(): void {
this.deviceCredentialsFormGroup.patchValue({
credentialsId: null,
credentialsValue: null,
credentialsBasic: {clientId: '', userName: '', password: ''}
credentialsValue: null
});
this.updateValidators();
}
updateValidators(): void {
this.hidePassword = true;
const credentialsType = this.deviceCredentialsFormGroup.get('credentialsType').value as DeviceCredentialsType;
switch (credentialsType) {
case DeviceCredentialsType.ACCESS_TOKEN:
@ -178,48 +172,13 @@ export class DeviceCredentialsComponent implements ControlValueAccessor, OnInit,
this.deviceCredentialsFormGroup.get('credentialsId').updateValueAndValidity({emitEvent: false});
this.deviceCredentialsFormGroup.get('credentialsValue').setValidators([]);
this.deviceCredentialsFormGroup.get('credentialsValue').updateValueAndValidity({emitEvent: false});
this.deviceCredentialsFormGroup.get('credentialsBasic').disable({emitEvent: false});
break;
case DeviceCredentialsType.X509_CERTIFICATE:
case DeviceCredentialsType.LWM2M_CREDENTIALS:
default:
this.deviceCredentialsFormGroup.get('credentialsValue').setValidators([Validators.required]);
this.deviceCredentialsFormGroup.get('credentialsValue').updateValueAndValidity({emitEvent: false});
this.deviceCredentialsFormGroup.get('credentialsId').setValidators([]);
this.deviceCredentialsFormGroup.get('credentialsId').updateValueAndValidity({emitEvent: false});
this.deviceCredentialsFormGroup.get('credentialsBasic').disable({emitEvent: false});
break;
case DeviceCredentialsType.MQTT_BASIC:
this.deviceCredentialsFormGroup.get('credentialsBasic').enable({emitEvent: false});
this.deviceCredentialsFormGroup.get('credentialsBasic').updateValueAndValidity({emitEvent: false});
this.deviceCredentialsFormGroup.get('credentialsId').setValidators([]);
this.deviceCredentialsFormGroup.get('credentialsId').updateValueAndValidity({emitEvent: false});
this.deviceCredentialsFormGroup.get('credentialsValue').setValidators([]);
this.deviceCredentialsFormGroup.get('credentialsValue').updateValueAndValidity({emitEvent: false});
break;
}
}
private atLeastOne(validator: ValidatorFn, controls: string[] = null) {
return (group: FormGroup): ValidationErrors | null => {
if (!controls) {
controls = Object.keys(group.controls);
}
const hasAtLeastOne = group?.controls && controls.some(k => !validator(group.controls[k]));
return hasAtLeastOne ? null : {atLeastOne: true};
};
}
passwordChanged() {
const value = this.deviceCredentialsFormGroup.get('credentialsBasic.password').value;
if (value !== '') {
this.deviceCredentialsFormGroup.get('credentialsBasic.userName').setValidators([Validators.required]);
} else {
this.deviceCredentialsFormGroup.get('credentialsBasic.userName').setValidators([]);
}
this.deviceCredentialsFormGroup.get('credentialsBasic.userName').updateValueAndValidity({
emitEvent: false,
onlySelf: true
});
}
}

46
ui-ngx/src/app/modules/home/components/device/device-credentials.module.ts

@ -0,0 +1,46 @@
///
/// Copyright © 2016-2021 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { SharedModule } from '@shared/shared.module';
import { CopyDeviceCredentialsComponent } from './copy-device-credentials.component';
import { DeviceCredentialsComponent } from './device-credentials.component';
import { DeviceCredentialsLwm2mComponent } from './device-credentials-lwm2m.component';
import { DeviceCredentialsLwm2mServerComponent } from './device-credentials-lwm2m-server.component';
import { DeviceCredentialsMqttBasicComponent } from './device-credentials-mqtt-basic.component';
@NgModule({
declarations: [
CopyDeviceCredentialsComponent,
DeviceCredentialsComponent,
DeviceCredentialsLwm2mComponent,
DeviceCredentialsLwm2mServerComponent,
DeviceCredentialsMqttBasicComponent
],
imports: [
CommonModule,
SharedModule
],
exports: [
CopyDeviceCredentialsComponent,
DeviceCredentialsComponent,
DeviceCredentialsLwm2mComponent,
DeviceCredentialsLwm2mServerComponent,
DeviceCredentialsMqttBasicComponent
]
})
export class DeviceCredentialsModule { }

16
ui-ngx/src/app/modules/home/components/home-components.module.ts

@ -110,7 +110,6 @@ import { RuleChainAutocompleteComponent } from '@home/components/rule-chain/rule
import { DeviceProfileProvisionConfigurationComponent } from '@home/components/profile/device-profile-provision-configuration.component';
import { AlarmScheduleComponent } from '@home/components/profile/alarm/alarm-schedule.component';
import { DeviceWizardDialogComponent } from '@home/components/wizard/device-wizard-dialog.component';
import { DeviceCredentialsComponent } from '@home/components/device/device-credentials.component';
import { AlarmScheduleInfoComponent } from '@home/components/profile/alarm/alarm-schedule-info.component';
import { AlarmScheduleDialogComponent } from '@home/components/profile/alarm/alarm-schedule-dialog.component';
import { EditAlarmDetailsDialogComponent } from '@home/components/profile/alarm/edit-alarm-details-dialog.component';
@ -120,7 +119,6 @@ import { TenantProfileConfigurationComponent } from '@home/components/profile/te
import { SmsProviderConfigurationComponent } from '@home/components/sms/sms-provider-configuration.component';
import { AwsSnsProviderConfigurationComponent } from '@home/components/sms/aws-sns-provider-configuration.component';
import { TwilioSmsProviderConfigurationComponent } from '@home/components/sms/twilio-sms-provider-configuration.component';
import { CopyDeviceCredentialsComponent } from '@home/components/device/copy-device-credentials.component';
import { Lwm2mProfileComponentsModule } from '@home/components/profile/device/lwm2m/lwm2m-profile-components.module';
import { DashboardPageComponent } from '@home/components/dashboard-page/dashboard-page.component';
import { DashboardToolbarComponent } from '@home/components/dashboard-page/dashboard-toolbar.component';
@ -139,11 +137,10 @@ import { EdgeDownlinkTableComponent } from '@home/components/edge/edge-downlink-
import { EdgeDownlinkTableHeaderComponent } from '@home/components/edge/edge-downlink-table-header.component';
import { DisplayWidgetTypesPanelComponent } from '@home/components/dashboard-page/widget-types-panel.component';
import { AlarmDurationPredicateValueComponent } from '@home/components/profile/alarm/alarm-duration-predicate-value.component';
import { SecurityConfigLwm2mComponent } from '@home/components/device/security-config-lwm2m.component';
import { SecurityConfigLwm2mServerComponent } from '@home/components/device/security-config-lwm2m-server.component';
import { DashboardImageDialogComponent } from '@home/components/dashboard-page/dashboard-image-dialog.component';
import { WidgetContainerComponent } from '@home/components/widget/widget-container.component';
import { SnmpDeviceProfileTransportModule } from '@home/components/profile/device/snpm/snmp-device-profile-transport.module';
import { DeviceCredentialsModule } from '@home/components/device/device-credentials.module';
@NgModule({
declarations:
@ -245,10 +242,6 @@ import { SnmpDeviceProfileTransportModule } from '@home/components/profile/devic
AlarmScheduleComponent,
AlarmDurationPredicateValueComponent,
DeviceWizardDialogComponent,
DeviceCredentialsComponent,
CopyDeviceCredentialsComponent,
SecurityConfigLwm2mComponent,
SecurityConfigLwm2mServerComponent,
AlarmScheduleDialogComponent,
EditAlarmDetailsDialogComponent,
SmsProviderConfigurationComponent,
@ -274,7 +267,8 @@ import { SnmpDeviceProfileTransportModule } from '@home/components/profile/devic
SharedHomeComponentsModule,
Lwm2mProfileComponentsModule,
SnmpDeviceProfileTransportModule,
StatesControllerModule
StatesControllerModule,
DeviceCredentialsModule
],
exports: [
EntitiesTableComponent,
@ -353,10 +347,6 @@ import { SnmpDeviceProfileTransportModule } from '@home/components/profile/devic
AddDeviceProfileDialogComponent,
RuleChainAutocompleteComponent,
DeviceWizardDialogComponent,
DeviceCredentialsComponent,
CopyDeviceCredentialsComponent,
SecurityConfigLwm2mComponent,
SecurityConfigLwm2mServerComponent,
AlarmScheduleInfoComponent,
AlarmScheduleComponent,
AlarmScheduleDialogComponent,

1
ui-ngx/src/app/modules/home/components/profile/add-device-profile-dialog.component.html

@ -19,6 +19,7 @@
<mat-toolbar color="primary">
<h2 translate>device-profile.add</h2>
<span fxFlex></span>
<div [tb-help]="'deviceProfiles'"></div>
<button mat-icon-button
(click)="cancel()"
type="button">

4
ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarm.component.html

@ -73,8 +73,8 @@
style="max-width: 200px;"
[matChipInputFor]="relationTypesChipList"
[matChipInputSeparatorKeyCodes]="separatorKeysCodes"
(matChipInputTokenEnd)="addRelationType($event)"
[matChipInputAddOnBlur]="true">
matChipInputAddOnBlur
(matChipInputTokenEnd)="addRelationType($event)">
</mat-chip-list>
<mat-hint innerHTML="{{ 'device-profile.alarm-rule-relation-types-list-hint' | translate }}"></mat-hint>
</mat-form-field>

11
ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-profile-transport-configuration.component.html

@ -167,6 +167,17 @@
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field class="mat-block" fxFlex *ngIf="lwm2mDeviceProfileFormGroup.get('clientLwM2mSettings.powerMode').value === 'E_DRX'">
<mat-label>{{ 'device-profile.edrx-cycle' | translate }}</mat-label>
<input matInput type="number" min="0" formControlName="edrxCycle" required>
<mat-error *ngIf="lwm2mDeviceProfileFormGroup.get('clientLwM2mSettings.edrxCycle').hasError('required')">
{{ 'device-profile.edrx-cycle-required' | translate }}
</mat-error>
<mat-error *ngIf="lwm2mDeviceProfileFormGroup.get('clientLwM2mSettings.edrxCycle').hasError('pattern') ||
lwm2mDeviceProfileFormGroup.get('clientLwM2mSettings.edrxCycle').hasError('min')">
{{ 'device-profile.edrx-cycle-pattern' | translate }}
</mat-error>
</mat-form-field>
</fieldset>
<mat-slide-toggle class="mat-slider"
formControlName="compositeOperationsSupport">{{ 'device-profile.lwm2m.composite-operations-support' | translate }}</mat-slide-toggle>

18
ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-profile-transport-configuration.component.ts

@ -116,6 +116,7 @@ export class Lwm2mDeviceProfileTransportConfigurationComponent implements Contro
fwUpdateResource: [{value: '', disabled: true}, []],
swUpdateResource: [{value: '', disabled: true}, []],
powerMode: [PowerMode.DRX, Validators.required],
edrxCycle: [0],
compositeOperationsSupport: [false]
})
});
@ -150,6 +151,20 @@ export class Lwm2mDeviceProfileTransportConfigurationComponent implements Contro
}
this.otaUpdateSwStrategyValidate(true);
});
this.lwm2mDeviceProfileFormGroup.get('clientLwM2mSettings.powerMode').valueChanges.pipe(
takeUntil(this.destroy$)
).subscribe((powerMode: PowerMode) => {
if (powerMode === PowerMode.E_DRX) {
this.lwm2mDeviceProfileFormGroup.get('clientLwM2mSettings.edrxCycle').enable({emitEvent: false});
this.lwm2mDeviceProfileFormGroup.get('clientLwM2mSettings.edrxCycle').patchValue(0, {emitEvent: false});
this.lwm2mDeviceProfileFormGroup.get('clientLwM2mSettings.edrxCycle')
.setValidators([Validators.required, Validators.min(0), Validators.pattern('[0-9]*')]);
} else {
this.lwm2mDeviceProfileFormGroup.get('clientLwM2mSettings.edrxCycle').disable({emitEvent: false});
this.lwm2mDeviceProfileFormGroup.get('clientLwM2mSettings.edrxCycle').clearValidators();
}
this.lwm2mDeviceProfileFormGroup.get('clientLwM2mSettings.edrxCycle').updateValueAndValidity({emitEvent: false});
});
this.lwm2mDeviceProfileFormGroup.valueChanges.pipe(
takeUntil(this.destroy$)
).subscribe((value) => {
@ -256,10 +271,13 @@ export class Lwm2mDeviceProfileTransportConfigurationComponent implements Contro
fwUpdateResource: fwResource,
swUpdateResource: swResource,
powerMode: this.configurationValue.clientLwM2mSettings.powerMode || PowerMode.DRX,
edrxCycle: this.configurationValue.clientLwM2mSettings.edrxCycle || 0,
compositeOperationsSupport: this.configurationValue.clientLwM2mSettings.compositeOperationsSupport || false
}
},
{emitEvent: false});
this.lwm2mDeviceProfileFormGroup.get('clientLwM2mSettings.powerMode')
.patchValue(this.configurationValue.clientLwM2mSettings.powerMode || PowerMode.DRX, {emitEvent: false, onlySelf: true});
this.configurationValue.clientLwM2mSettings.fwUpdateResource = fwResource;
this.configurationValue.clientLwM2mSettings.swUpdateResource = swResource;
this.isFwUpdateStrategy = this.configurationValue.clientLwM2mSettings.fwUpdateStrategy === 2;

1
ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-profile-config.models.ts

@ -169,6 +169,7 @@ export interface ClientLwM2mSettings {
fwUpdateResource: string;
swUpdateResource: string;
powerMode: PowerMode;
edrxCycle?: number;
compositeOperationsSupport: boolean;
}

1
ui-ngx/src/app/modules/home/components/widget/data-keys.component.html

@ -86,6 +86,7 @@
[matAutocompleteDisabled]="isEntityCountDatasource"
[matChipInputFor]="chipList"
[matChipInputSeparatorKeyCodes]="separatorKeysCodes"
matChipInputAddOnBlur
(matChipInputTokenEnd)="add($event)">
</mat-chip-list>
<mat-autocomplete #keyAutocomplete="matAutocomplete"

2
ui-ngx/src/app/modules/home/components/widget/lib/alarm-filter-panel.component.html

@ -45,7 +45,7 @@
<input placeholder="{{ !alarmFilterFormGroup.get('alarmTypeList').value?.length ? ('alarm.any-type' | translate) : '' }}"
[matChipInputFor]="alarmTypeChipList"
[matChipInputSeparatorKeyCodes]="separatorKeysCodes"
[matChipInputAddOnBlur]="true"
matChipInputAddOnBlur
(matChipInputTokenEnd)="addAlarmType($event)">
</mat-chip-list>
</mat-form-field>

2
ui-ngx/src/app/modules/home/components/widget/widget-config.component.html

@ -73,7 +73,7 @@
<input placeholder="{{ !dataSettings.get('alarmTypeList').value?.length ? ('alarm.any-type' | translate) : '' }}"
[matChipInputFor]="alarmTypeChipList"
[matChipInputSeparatorKeyCodes]="separatorKeysCodes"
[matChipInputAddOnBlur]="true"
matChipInputAddOnBlur
(matChipInputTokenEnd)="addAlarmType($event)">
</mat-chip-list>
</mat-form-field>

5
ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.html

@ -19,6 +19,7 @@
<mat-toolbar color="primary">
<h2 translate>device.add-device-text</h2>
<span fxFlex></span>
<div [tb-help]="'devices'"></div>
<button mat-icon-button
(click)="cancel()"
type="button">
@ -64,7 +65,8 @@
[ngClass]="{invisible: deviceWizardFormGroup.get('addProfileType').value !== 0}"
[addNewProfile]="false"
[selectDefaultProfile]="true"
[editProfileEnabled]="false">
[editProfileEnabled]="false"
(deviceProfileChanged)="deviceProfileChanged($event)">
</tb-device-profile-autocomplete>
<mat-form-field fxFlex class="mat-block"
[ngClass]="{invisible: deviceWizardFormGroup.get('addProfileType').value !== 1}">
@ -154,6 +156,7 @@
<mat-checkbox style="padding-bottom: 16px;" formControlName="setCredential">{{ 'device.wizard.add-credentials' | translate }}</mat-checkbox>
<tb-device-credentials
[fxShow]="credentialsFormGroup.get('setCredential').value"
[deviceTransportType]="deviceTransportType"
formControlName="credential">
</tb-device-credentials>
</form>

16
ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.ts

@ -25,6 +25,7 @@ import {
createDeviceProfileConfiguration,
createDeviceProfileTransportConfiguration,
DeviceProfile,
DeviceProfileInfo,
DeviceProfileType,
DeviceProvisionConfiguration,
DeviceProvisionType,
@ -91,6 +92,7 @@ export class DeviceWizardDialogComponent extends
serviceType = ServiceType.TB_RULE_ENGINE;
private subscriptions: Subscription[] = [];
private currentDeviceProfileTransportType = DeviceTransportType.DEFAULT;
constructor(protected store: Store<AppState>,
protected router: Router,
@ -265,6 +267,20 @@ export class DeviceWizardDialogComponent extends
}
}
get deviceTransportType(): DeviceTransportType {
if (this.deviceWizardFormGroup.get('addProfileType').value) {
return this.transportConfigFormGroup.get('transportType').value;
} else {
return this.currentDeviceProfileTransportType;
}
}
deviceProfileChanged(deviceProfile: DeviceProfileInfo) {
if (deviceProfile) {
this.currentDeviceProfileTransportType = deviceProfile.transportType;
}
}
private createDeviceProfile(): Observable<EntityId> {
if (this.deviceWizardFormGroup.get('addProfileType').value) {
const deviceProvisionConfiguration: DeviceProvisionConfiguration = this.provisionConfigFormGroup.get('provisionConfiguration').value;

1
ui-ngx/src/app/modules/home/home.component.html

@ -17,6 +17,7 @@
-->
<mat-sidenav-container>
<mat-sidenav #sidenav class="tb-site-sidenav mat-elevation-z2"
[disableClose]="sidenavOpened"
(click)="sidenavClicked()"
[mode]="sidenavMode"
[opened]="sidenavOpened && !forceFullscreen">

8
ui-ngx/src/app/modules/home/models/widget-component.models.ts

@ -189,16 +189,16 @@ export class WidgetContext {
};
controlApi: RpcApi = {
sendOneWayCommand: (method, params, timeout, requestUUID) => {
sendOneWayCommand: (method, params, timeout, persistent, requestUUID) => {
if (this.defaultSubscription) {
return this.defaultSubscription.sendOneWayCommand(method, params, timeout, requestUUID);
return this.defaultSubscription.sendOneWayCommand(method, params, timeout, persistent, requestUUID);
} else {
return of(null);
}
},
sendTwoWayCommand: (method, params, timeout, requestUUID) => {
sendTwoWayCommand: (method, params, timeout, persistent, requestUUID) => {
if (this.defaultSubscription) {
return this.defaultSubscription.sendTwoWayCommand(method, params, timeout, requestUUID);
return this.defaultSubscription.sendTwoWayCommand(method, params, timeout, persistent, requestUUID);
} else {
return of(null);
}

895
ui-ngx/src/app/modules/home/pages/api-usage/api_usage_json.raw

File diff suppressed because one or more lines are too long

11
ui-ngx/src/app/modules/home/pages/device/data/lwm2m-device-transport-configuration.component.html

@ -25,4 +25,15 @@
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field class="mat-block" fxFlex *ngIf="lwm2mDeviceTransportConfigurationFormGroup.get('powerMode').value === 'E_DRX'">
<mat-label>{{ 'device-profile.edrx-cycle' | translate }}</mat-label>
<input matInput type="number" min="0" formControlName="edrxCycle" required>
<mat-error *ngIf="lwm2mDeviceTransportConfigurationFormGroup.get('edrxCycle').hasError('required')">
{{ 'device-profile.edrx-cycle-required' | translate }}
</mat-error>
<mat-error *ngIf="lwm2mDeviceTransportConfigurationFormGroup.get('edrxCycle').hasError('pattern') ||
lwm2mDeviceTransportConfigurationFormGroup.get('edrxCycle').hasError('min')">
{{ 'device-profile.edrx-cycle-pattern' | translate }}
</mat-error>
</mat-form-field>
</form>

50
ui-ngx/src/app/modules/home/pages/device/data/lwm2m-device-transport-configuration.component.ts

@ -14,16 +14,16 @@
/// limitations under the License.
///
import { Component, forwardRef, Input, OnInit } from '@angular/core';
import { Component, forwardRef, Input, OnDestroy, OnInit } from '@angular/core';
import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms';
import { Store } from '@ngrx/store';
import { AppState } from '@app/core/core.state';
import { coerceBooleanProperty } from '@angular/cdk/coercion';
import {
DeviceTransportConfiguration,
DeviceTransportType, Lwm2mDeviceTransportConfiguration
} from '@shared/models/device.models';
import {PowerMode, PowerModeTranslationMap} from "@home/components/profile/device/lwm2m/lwm2m-profile-config.models";
import { DeviceTransportConfiguration, Lwm2mDeviceTransportConfiguration } from '@shared/models/device.models';
import { PowerMode, PowerModeTranslationMap } from '@home/components/profile/device/lwm2m/lwm2m-profile-config.models';
import { takeUntil } from 'rxjs/operators';
import { Subject } from 'rxjs';
import { isDefinedAndNotNull } from '@core/utils';
@Component({
selector: 'tb-lwm2m-device-transport-configuration',
@ -35,7 +35,7 @@ import {PowerMode, PowerModeTranslationMap} from "@home/components/profile/devic
multi: true
}]
})
export class Lwm2mDeviceTransportConfigurationComponent implements ControlValueAccessor, OnInit {
export class Lwm2mDeviceTransportConfigurationComponent implements ControlValueAccessor, OnInit, OnDestroy {
lwm2mDeviceTransportConfigurationFormGroup: FormGroup;
powerMods = Object.values(PowerMode);
@ -53,6 +53,7 @@ export class Lwm2mDeviceTransportConfigurationComponent implements ControlValueA
@Input()
disabled: boolean;
private destroy$ = new Subject();
private propagateChange = (v: any) => { };
constructor(private store: Store<AppState>,
@ -68,13 +69,35 @@ export class Lwm2mDeviceTransportConfigurationComponent implements ControlValueA
ngOnInit() {
this.lwm2mDeviceTransportConfigurationFormGroup = this.fb.group({
powerMode: [null]
powerMode: [null],
edrxCycle: [0]
});
this.lwm2mDeviceTransportConfigurationFormGroup.valueChanges.subscribe(() => {
this.lwm2mDeviceTransportConfigurationFormGroup.get('powerMode').valueChanges.pipe(
takeUntil(this.destroy$)
).subscribe((powerMode: PowerMode) => {
if (powerMode === PowerMode.E_DRX) {
this.lwm2mDeviceTransportConfigurationFormGroup.get('edrxCycle').enable({emitEvent: false});
this.lwm2mDeviceTransportConfigurationFormGroup.get('edrxCycle').patchValue(0, {emitEvent: false});
this.lwm2mDeviceTransportConfigurationFormGroup.get('edrxCycle')
.setValidators([Validators.required, Validators.min(0), Validators.pattern('[0-9]*')]);
} else {
this.lwm2mDeviceTransportConfigurationFormGroup.get('edrxCycle').disable({emitEvent: false});
this.lwm2mDeviceTransportConfigurationFormGroup.get('edrxCycle').clearValidators();
}
this.lwm2mDeviceTransportConfigurationFormGroup.get('edrxCycle').updateValueAndValidity({emitEvent: false});
});
this.lwm2mDeviceTransportConfigurationFormGroup.valueChanges.pipe(
takeUntil(this.destroy$)
).subscribe(() => {
this.updateModel();
});
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
if (this.disabled) {
@ -85,13 +108,18 @@ export class Lwm2mDeviceTransportConfigurationComponent implements ControlValueA
}
writeValue(value: Lwm2mDeviceTransportConfiguration | null): void {
this.lwm2mDeviceTransportConfigurationFormGroup.patchValue(value, {emitEvent: false});
if (isDefinedAndNotNull(value)) {
this.lwm2mDeviceTransportConfigurationFormGroup.get('powerMode').patchValue(value.powerMode, {emitEvent: false, onlySelf: true});
this.lwm2mDeviceTransportConfigurationFormGroup.get('edrxCycle').patchValue(value.edrxCycle || 0, {emitEvent: false});
} else {
this.lwm2mDeviceTransportConfigurationFormGroup.patchValue({powerMode: null, edrxCycle: 0}, {emitEvent: false});
}
}
private updateModel() {
let configuration: DeviceTransportConfiguration = null;
if (this.lwm2mDeviceTransportConfigurationFormGroup.valid) {
configuration = this.lwm2mDeviceTransportConfigurationFormGroup.getRawValue();
configuration = this.lwm2mDeviceTransportConfigurationFormGroup.value;
// configuration.type = DeviceTransportType.LWM2M;
}
this.propagateChange(configuration);

27
ui-ngx/src/app/modules/home/pages/device/device-credentials-dialog.component.html

@ -17,7 +17,7 @@
-->
<form [formGroup]="deviceCredentialsFormGroup" (ngSubmit)="save()" style="min-width: 350px;">
<mat-toolbar color="primary">
<h2 translate>device.device-credentials</h2>
<h2>{{ 'device.device-credentials' | translate }}</h2>
<span fxFlex></span>
<button mat-icon-button
(click)="cancel()"
@ -25,15 +25,26 @@
<mat-icon class="material-icons">close</mat-icon>
</button>
</mat-toolbar>
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async">
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="(isLoading$ | async) && !loadingCredentials">
</mat-progress-bar>
<div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div>
<div style="height: 4px;" *ngIf="!(isLoading$ | async) || loadingCredentials"></div>
<div mat-dialog-content>
<fieldset [disabled]="(isLoading$ | async) || isReadOnly">
<tb-device-credentials
formControlName="credential">
</tb-device-credentials>
</fieldset>
<section *ngIf="!loadingCredentials; else loadCredentials">
<fieldset [disabled]="(isLoading$ | async) || isReadOnly">
<tb-device-credentials
[deviceTransportType]="deviceTransportType"
formControlName="credential">
</tb-device-credentials>
</fieldset>
</section>
<ng-template #loadCredentials>
<div fxLayout="column" fxLayoutAlign="center center">
<mat-spinner color="accent" diameter="65" strokeWidth="4" style="margin-bottom: 18px"></mat-spinner>
<span class="mat-subheading-2" style="margin-bottom: 0">
{{ 'device.loading-device-credentials' | translate }}
</span>
</div>
</ng-template>
</div>
<div mat-dialog-actions fxLayoutAlign="end center">
<button mat-button color="primary"

41
ui-ngx/src/app/modules/home/pages/device/device-credentials-dialog.component.ts

@ -21,13 +21,16 @@ import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { FormBuilder, FormControl, FormGroup, FormGroupDirective, NgForm } from '@angular/forms';
import { DeviceService } from '@core/http/device.service';
import { credentialTypeNames, DeviceCredentials, DeviceCredentialsType } from '@shared/models/device.models';
import { DeviceCredentials, DeviceProfileInfo, DeviceTransportType } from '@shared/models/device.models';
import { DialogComponent } from '@shared/components/dialog.component';
import { Router } from '@angular/router';
import { DeviceProfileService } from '@core/http/device-profile.service';
import { forkJoin } from 'rxjs';
export interface DeviceCredentialsDialogData {
isReadOnly: boolean;
deviceId: string;
deviceProfileId: string;
}
@Component({
@ -40,25 +43,18 @@ export class DeviceCredentialsDialogComponent extends
DialogComponent<DeviceCredentialsDialogComponent, DeviceCredentials> implements OnInit, ErrorStateMatcher {
deviceCredentialsFormGroup: FormGroup;
deviceTransportType: DeviceTransportType;
isReadOnly: boolean;
loadingCredentials = true;
deviceCredentials: DeviceCredentials;
submitted = false;
deviceCredentialsType = DeviceCredentialsType;
credentialsTypes = Object.keys(DeviceCredentialsType);
credentialTypeNamesMap = credentialTypeNames;
hidePassword = true;
private deviceCredentials: DeviceCredentials;
private submitted = false;
constructor(protected store: Store<AppState>,
protected router: Router,
@Inject(MAT_DIALOG_DATA) public data: DeviceCredentialsDialogData,
private deviceService: DeviceService,
private deviceProfileService: DeviceProfileService,
@SkipSelf() private errorStateMatcher: ErrorStateMatcher,
public dialogRef: MatDialogRef<DeviceCredentialsDialogComponent, DeviceCredentials>,
public fb: FormBuilder) {
@ -84,14 +80,17 @@ export class DeviceCredentialsDialogComponent extends
}
loadDeviceCredentials() {
this.deviceService.getDeviceCredentials(this.data.deviceId).subscribe(
(deviceCredentials) => {
this.deviceCredentials = deviceCredentials;
this.deviceCredentialsFormGroup.patchValue({
credential: deviceCredentials
}, {emitEvent: false});
}
);
const task = [];
task.push(this.deviceService.getDeviceCredentials(this.data.deviceId));
task.push(this.deviceProfileService.getDeviceProfileInfo(this.data.deviceProfileId));
forkJoin(task).subscribe(([deviceCredentials, deviceProfile]: [DeviceCredentials, DeviceProfileInfo]) => {
this.deviceTransportType = deviceProfile.transportType;
this.deviceCredentials = deviceCredentials;
this.deviceCredentialsFormGroup.patchValue({
credential: deviceCredentials
}, {emitEvent: false});
this.loadingCredentials = false;
});
}
cancel(): void {

2
ui-ngx/src/app/modules/home/pages/device/device.module.ts

@ -33,6 +33,7 @@ import { MqttDeviceTransportConfigurationComponent } from './data/mqtt-device-tr
import { CoapDeviceTransportConfigurationComponent } from './data/coap-device-transport-configuration.component';
import { Lwm2mDeviceTransportConfigurationComponent } from './data/lwm2m-device-transport-configuration.component';
import { SnmpDeviceTransportConfigurationComponent } from './data/snmp-device-transport-configuration.component';
import { DeviceCredentialsModule } from '@home/components/device/device-credentials.module';
@NgModule({
declarations: [
@ -55,6 +56,7 @@ import { SnmpDeviceTransportConfigurationComponent } from './data/snmp-device-tr
SharedModule,
HomeComponentsModule,
HomeDialogsModule,
DeviceCredentialsModule,
DeviceRoutingModule
]
})

4
ui-ngx/src/app/modules/home/pages/device/devices-table-config.resolver.ts

@ -112,7 +112,8 @@ export class DevicesTableConfigResolver implements Resolve<EntityTableConfig<Dev
));
};
this.config.onEntityAction = action => this.onDeviceAction(action);
this.config.detailsReadonly = () => (this.config.componentsData.deviceScope === 'customer_user' || this.config.componentsData.deviceScope === 'edge_customer_user');
this.config.detailsReadonly = () =>
(this.config.componentsData.deviceScope === 'customer_user' || this.config.componentsData.deviceScope === 'edge_customer_user');
this.config.headerComponent = DeviceTableHeaderComponent;
@ -528,6 +529,7 @@ export class DevicesTableConfigResolver implements Resolve<EntityTableConfig<Dev
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
deviceId: device.id.id,
deviceProfileId: device.deviceProfileId.id,
isReadOnly: this.config.componentsData.deviceScope === 'customer_user' || this.config.componentsData.deviceScope === 'edge_customer_user'
}
}).afterClosed().subscribe(deviceCredentials => {

1
ui-ngx/src/app/modules/home/pages/rulechain/link-labels.component.html

@ -37,6 +37,7 @@
[matAutocomplete]="labelAutocomplete"
[matChipInputFor]="chipList"
[matChipInputSeparatorKeyCodes]="separatorKeysCodes"
matChipInputAddOnBlur
(matChipInputTokenEnd)="add($event)">
</mat-chip-list>
<mat-autocomplete #labelAutocomplete="matAutocomplete"

1
ui-ngx/src/app/shared/components/entity/entity-keys-list.component.html

@ -36,6 +36,7 @@
[matAutocomplete]="keyAutocomplete"
[matChipInputFor]="chipList"
[matChipInputSeparatorKeyCodes]="separatorKeysCodes"
matChipInputAddOnBlur
(matChipInputTokenEnd)="add($event)">
</mat-chip-list>
<mat-autocomplete #keyAutocomplete="matAutocomplete"

1
ui-ngx/src/app/shared/components/entity/entity-subtype-list.component.html

@ -36,6 +36,7 @@
[matAutocomplete]="entitySubtypeAutocomplete"
[matChipInputFor]="chipList"
[matChipInputSeparatorKeyCodes]="separatorKeysCodes"
matChipInputAddOnBlur
(matChipInputTokenEnd)="chipAdd($event)">
</mat-chip-list>
<mat-autocomplete #entitySubtypeAutocomplete="matAutocomplete"

16
ui-ngx/src/app/shared/models/ace/widget-completion.models.ts

@ -126,7 +126,7 @@ export const timewindowCompletion: TbEditorCompletion = {
}
}
}
}
};
export const widgetContextCompletions: TbEditorCompletions = {
ctx: {
@ -465,6 +465,12 @@ export const widgetContextCompletions: TbEditorCompletions = {
description: 'Maximum delay in milliseconds to wait until response/acknowledgement is received.',
type: 'number',
optional: true
},
{
name: 'persistent',
description: 'RPC request persistent',
type: 'boolean',
optional: true
}
],
return: {
@ -492,6 +498,12 @@ export const widgetContextCompletions: TbEditorCompletions = {
description: 'Maximum delay in milliseconds to wait until response/acknowledgement is received.',
type: 'number',
optional: true
},
{
name: 'persistent',
description: 'RPC request persistent',
type: 'boolean',
optional: true
}
],
return: {
@ -657,4 +669,4 @@ export const widgetContextCompletions: TbEditorCompletions = {
...serviceCompletions
}
}
}
};

17
ui-ngx/src/app/shared/models/device.models.ts

@ -30,6 +30,7 @@ import { AbstractControl, ValidationErrors } from '@angular/forms';
import { OtaPackageId } from '@shared/models/id/ota-package-id';
import { DashboardId } from '@shared/models/id/dashboard-id';
import { DataType } from '@shared/models/constants';
import { PowerMode } from '@home/components/profile/device/lwm2m/lwm2m-profile-config.models';
export enum DeviceProfileType {
DEFAULT = 'DEFAULT',
@ -573,6 +574,8 @@ export interface CoapDeviceTransportConfiguration {
}
export interface Lwm2mDeviceTransportConfiguration {
powerMode?: PowerMode | null;
edrxCycle?: number;
[key: string]: any;
}
@ -679,6 +682,20 @@ export const credentialTypeNames = new Map<DeviceCredentialsType, string>(
]
);
export const credentialTypesByTransportType = new Map<DeviceTransportType, DeviceCredentialsType[]>(
[
[DeviceTransportType.DEFAULT, [
DeviceCredentialsType.ACCESS_TOKEN, DeviceCredentialsType.X509_CERTIFICATE, DeviceCredentialsType.MQTT_BASIC
]],
[DeviceTransportType.MQTT, [
DeviceCredentialsType.ACCESS_TOKEN, DeviceCredentialsType.X509_CERTIFICATE, DeviceCredentialsType.MQTT_BASIC
]],
[DeviceTransportType.COAP, [DeviceCredentialsType.ACCESS_TOKEN, DeviceCredentialsType.X509_CERTIFICATE]],
[DeviceTransportType.LWM2M, [DeviceCredentialsType.LWM2M_CREDENTIALS]],
[DeviceTransportType.SNMP, [DeviceCredentialsType.ACCESS_TOKEN]]
]
);
export interface DeviceCredentials extends BaseData<DeviceCredentialsId> {
deviceId: DeviceId;
credentialsType: DeviceCredentialsType;

16
ui-ngx/src/assets/locale/locale.constant-en_US.json

@ -470,12 +470,16 @@
},
"api-usage": {
"api-usage": "Api Usage",
"alarm": "Alarm",
"alarms-created": "Alarms created",
"alarms-created-daily-activity": "Alarms created daily activity",
"alarms-created-hourly-activity": "Alarms created hourly activity",
"alarms-created-monthly-activity": "Alarms created monthly activity",
"data-points": "Data points",
"data-points-storage-days": "Data points storage days",
"email": "Email",
"email-messages": "Email messages",
"email-messages-daily-activity": "Email messages daily activity",
"email-messages-hourly-activity": "Email messages hourly activity",
"email-messages-monthly-activity": "Email messages monthly activity",
"exceptions": "Exceptions",
"executions": "Executions",
@ -487,6 +491,9 @@
"javascript-functions-monthly-activity": "JavaScript functions monthly activity",
"latest-error": "Latest Error",
"messages": "Messages",
"notifications": "Notifications",
"notifications-email-sms": "Notifications (Email/SMS)",
"notifications-hourly-activity": "Notifications hourly activity",
"permanent-failures": "${entityName} Permanent Failures",
"permanent-timeouts": "${entityName} Permanent Timeouts",
"processing-failures": "${entityName} Processing Failures",
@ -504,7 +511,6 @@
"sms": "SMS",
"sms-messages": "SMS messages",
"sms-messages-daily-activity": "SMS messages daily activity",
"sms-messages-hourly-activity": "SMS messages hourly activity",
"sms-messages-monthly-activity": "SMS messages monthly activity",
"successful": "${entityName} Successful",
"telemetry": "Telemetry",
@ -941,13 +947,13 @@
"unassign-devices-title": "Are you sure you want to unassign { count, plural, 1 {1 device} other {# devices} }?",
"unassign-devices-text": "After the confirmation all selected devices will be unassigned and won't be accessible by the customer.",
"device-credentials": "Device Credentials",
"loading-device-credentials": "Loading device credentials...",
"credentials-type": "Credentials type",
"access-token": "Access token",
"access-token-required": "Access token is required.",
"access-token-invalid": "Access token length must be from 1 to 20 characters.",
"rsa-key": "RSA public key",
"rsa-key-required": "RSA public key is required.",
"lwm2m-value": "LwM2M Security config",
"lwm2m-security-config": {
"identity": "Client Identity",
"identity-required": "Client Identity is required.",
@ -971,7 +977,6 @@
"client-secret-key-required": "Client Secret Key is required.",
"client-secret-key-pattern": "Client Secret Key must be hexadecimal format.",
"client-secret-key-length": "Client Secret Key must be {{ count }} characters.",
"config-json-tab": "Json Client Security Config",
"client-public-key": "Client public key",
"client-public-key-hint": "If client public key is empty, the trusted certificate will be used"
},
@ -1222,6 +1227,9 @@
"drx": "Discontinuous Reception (DRX)",
"edrx": "Extended Discontinuous Reception (eDRX)"
},
"edrx-cycle": "eDRX cycle in milliseconds",
"edrx-cycle-required": "eDRX cycle is required.",
"edrx-cycle-pattern": "eDRX cycle must be a positive integer.",
"lwm2m": {
"object-list": "Object list",
"object-list-empty": "No objects selected.",

Loading…
Cancel
Save