Browse Source

Merge branch 'master' into cache-attribute-service-performance-improvement

# Conflicts:
#	dao/src/test/java/org/thingsboard/server/dao/attributes/CachedAttributesServiceTest.java
pull/4832/head
Sergey Matvienko 5 years ago
parent
commit
0b47942986
  1. 174
      application/src/main/data/upgrade/2.4.3/schema_update_psql_drop_partitions.sql
  2. 14
      application/src/main/data/upgrade/3.2.2/schema_update.sql
  3. 23
      application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java
  4. 2
      application/src/main/java/org/thingsboard/server/actors/device/DeviceActor.java
  5. 126
      application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java
  6. 18
      application/src/main/java/org/thingsboard/server/controller/BaseController.java
  7. 10
      application/src/main/java/org/thingsboard/server/controller/DeviceController.java
  8. 56
      application/src/main/java/org/thingsboard/server/controller/RpcController.java
  9. 14
      application/src/main/java/org/thingsboard/server/controller/RuleChainController.java
  10. 11
      application/src/main/java/org/thingsboard/server/service/resource/DefaultTbResourceService.java
  11. 1
      application/src/main/java/org/thingsboard/server/service/rpc/DefaultTbCoreDeviceRpcService.java
  12. 2
      application/src/main/java/org/thingsboard/server/service/rpc/DefaultTbRuleEngineRpcService.java
  13. 77
      application/src/main/java/org/thingsboard/server/service/rpc/TbRpcService.java
  14. 14
      application/src/main/java/org/thingsboard/server/service/script/AbstractJsInvokeService.java
  15. 2
      application/src/main/java/org/thingsboard/server/service/script/AbstractNashornJsInvokeService.java
  16. 19
      application/src/main/java/org/thingsboard/server/service/script/RemoteJsInvokeService.java
  17. 158
      application/src/main/java/org/thingsboard/server/service/script/RuleNodeJsScriptEngine.java
  18. 25
      application/src/main/java/org/thingsboard/server/service/security/AccessValidator.java
  19. 19
      application/src/main/java/org/thingsboard/server/service/security/permission/CustomerUserPermissions.java
  20. 3
      application/src/main/java/org/thingsboard/server/service/security/permission/Resource.java
  21. 1
      application/src/main/java/org/thingsboard/server/service/security/permission/TenantAdminPermissions.java
  22. 46
      application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java
  23. 44
      application/src/main/java/org/thingsboard/server/service/ttl/AbstractCleanUpService.java
  24. 3
      application/src/main/java/org/thingsboard/server/service/ttl/AlarmsCleanUpService.java
  25. 32
      application/src/main/java/org/thingsboard/server/service/ttl/EdgeEventsCleanUpService.java
  26. 32
      application/src/main/java/org/thingsboard/server/service/ttl/EventsCleanUpService.java
  27. 29
      application/src/main/java/org/thingsboard/server/service/ttl/TimeseriesCleanUpService.java
  28. 83
      application/src/main/java/org/thingsboard/server/service/ttl/rpc/RpcCleanUpService.java
  29. 44
      application/src/main/java/org/thingsboard/server/service/ttl/timeseries/PsqlTimeseriesCleanUpService.java
  30. 36
      application/src/main/java/org/thingsboard/server/service/ttl/timeseries/TimescaleTimeseriesCleanUpService.java
  31. 10
      application/src/main/resources/thingsboard.yml
  32. 127
      application/src/test/java/org/thingsboard/server/transport/lwm2m/AbstractLwM2MIntegrationTest.java
  33. 65
      application/src/test/java/org/thingsboard/server/transport/lwm2m/NoSecLwM2MIntegrationTest.java
  34. 43
      application/src/test/java/org/thingsboard/server/transport/lwm2m/PskLwm2mIntegrationTest.java
  35. 39
      application/src/test/java/org/thingsboard/server/transport/lwm2m/RpkLwM2MIntegrationTest.java
  36. 137
      application/src/test/java/org/thingsboard/server/transport/lwm2m/X509LwM2MIntegrationTest.java
  37. 2
      application/src/test/resources/logback.xml
  38. 20
      common/coap-server/src/main/java/org/thingsboard/server/coapserver/DefaultCoapServerService.java
  39. 2
      common/dao-api/src/main/java/org/thingsboard/server/dao/edge/EdgeService.java
  40. 2
      common/dao-api/src/main/java/org/thingsboard/server/dao/event/EventService.java
  41. 39
      common/dao-api/src/main/java/org/thingsboard/server/dao/rpc/RpcService.java
  42. 2
      common/dao-api/src/main/java/org/thingsboard/server/dao/timeseries/TimeseriesService.java
  43. 6
      common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java
  44. 2
      common/data/src/main/java/org/thingsboard/server/common/data/EntityType.java
  45. 23
      common/data/src/main/java/org/thingsboard/server/common/data/device/credentials/lwm2m/AbstractLwM2MClientCredentialsWithKey.java
  46. 2
      common/data/src/main/java/org/thingsboard/server/common/data/device/credentials/lwm2m/PSKClientCredentials.java
  47. 2
      common/data/src/main/java/org/thingsboard/server/common/data/device/credentials/lwm2m/RPKClientCredentials.java
  48. 3
      common/data/src/main/java/org/thingsboard/server/common/data/device/data/Lwm2mDeviceTransportConfiguration.java
  49. 20
      common/data/src/main/java/org/thingsboard/server/common/data/device/data/PowerMode.java
  50. 9
      common/data/src/main/java/org/thingsboard/server/common/data/device/data/SnmpDeviceTransportConfiguration.java
  51. 30
      common/data/src/main/java/org/thingsboard/server/common/data/device/data/lwm2m/BootstrapConfiguration.java
  52. 33
      common/data/src/main/java/org/thingsboard/server/common/data/device/data/lwm2m/ObjectAttributes.java
  53. 32
      common/data/src/main/java/org/thingsboard/server/common/data/device/data/lwm2m/OtherConfiguration.java
  54. 32
      common/data/src/main/java/org/thingsboard/server/common/data/device/data/lwm2m/TelemetryMappingConfiguration.java
  55. 25
      common/data/src/main/java/org/thingsboard/server/common/data/device/profile/Lwm2mDeviceProfileTransportConfiguration.java
  56. 2
      common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java
  57. 39
      common/data/src/main/java/org/thingsboard/server/common/data/id/RpcId.java
  58. 54
      common/data/src/main/java/org/thingsboard/server/common/data/rpc/Rpc.java
  59. 20
      common/data/src/main/java/org/thingsboard/server/common/data/rpc/RpcStatus.java
  60. 1
      common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java
  61. 1
      common/message/src/main/java/org/thingsboard/server/common/msg/rpc/ToDeviceRpcRequest.java
  62. 2
      common/message/src/main/java/org/thingsboard/server/common/msg/session/FeatureType.java
  63. 5
      common/message/src/main/java/org/thingsboard/server/common/msg/session/SessionMsgType.java
  64. 2
      common/queue/src/main/java/org/thingsboard/server/queue/TbQueueRequestTemplate.java
  65. 242
      common/queue/src/main/java/org/thingsboard/server/queue/common/DefaultTbQueueRequestTemplate.java
  66. 10
      common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaConsumerTemplate.java
  67. 15
      common/queue/src/main/proto/queue.proto
  68. 211
      common/queue/src/test/java/org/thingsboard/server/queue/common/DefaultTbQueueRequestTemplateTest.java
  69. 213
      common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportResource.java
  70. 5
      common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportService.java
  71. 150
      common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/OtaPackageTransportResource.java
  72. 32
      common/transport/http/src/main/java/org/thingsboard/server/transport/http/DeviceApiController.java
  73. 3
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/secure/LwM2MBootstrapConfig.java
  74. 71
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/secure/LwM2MBootstrapSecurityStore.java
  75. 47
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/config/LwM2MTransportServerConfig.java
  76. 66
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/config/LwM2mVersion.java
  77. 29
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/secure/LwM2mCredentialsSecurityInfoValidator.java
  78. 11
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/secure/TbLwM2MAuthorizer.java
  79. 17
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/secure/TbLwM2MDtlsCertificateVerifier.java
  80. 4
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/secure/TbLwM2MSecurityInfo.java
  81. 4
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/secure/TbX509DtlsSessionInfo.java
  82. 1453
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/DefaultLwM2MTransportMsgHandler.java
  83. 12
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/DefaultLwM2mTransportService.java
  84. 6
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mNetworkConfig.java
  85. 94
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mOperationType.java
  86. 26
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mServerListener.java
  87. 38
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mSessionMsgListener.java
  88. 28
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportCoapResource.java
  89. 613
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportRequest.java
  90. 55
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportServerHelper.java
  91. 863
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportUtil.java
  92. 2
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mVersionedModelProvider.java
  93. 221
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/attributes/DefaultLwM2MAttributesService.java
  94. 34
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/attributes/LwM2MAttributesService.java
  95. 22
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2MAuthException.java
  96. 205
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClient.java
  97. 24
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClientContext.java
  98. 261
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClientContextImpl.java
  99. 113
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClientProfile.java
  100. 281
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClientRpcRequest.java

174
application/src/main/data/upgrade/2.4.3/schema_update_psql_drop_partitions.sql

@ -18,17 +18,18 @@ CREATE OR REPLACE PROCEDURE drop_partitions_by_max_ttl(IN partition_type varchar
LANGUAGE plpgsql AS
$$
DECLARE
max_tenant_ttl bigint;
max_customer_ttl bigint;
max_ttl bigint;
date timestamp;
partition_by_max_ttl_date varchar;
partition_month varchar;
partition_day varchar;
partition_year varchar;
partition varchar;
partition_to_delete varchar;
max_tenant_ttl bigint;
max_customer_ttl bigint;
max_ttl bigint;
date timestamp;
partition_by_max_ttl_date varchar;
partition_by_max_ttl_month varchar;
partition_by_max_ttl_day varchar;
partition_by_max_ttl_year varchar;
partition varchar;
partition_year integer;
partition_month integer;
partition_day integer;
BEGIN
SELECT max(attribute_kv.long_v)
@ -45,53 +46,138 @@ BEGIN
if max_ttl IS NOT NULL AND max_ttl > 0 THEN
date := to_timestamp(EXTRACT(EPOCH FROM current_timestamp) - max_ttl);
partition_by_max_ttl_date := get_partition_by_max_ttl_date(partition_type, date);
RAISE NOTICE 'Date by max ttl: %', date;
RAISE NOTICE 'Partition by max ttl: %', partition_by_max_ttl_date;
IF partition_by_max_ttl_date IS NOT NULL THEN
CASE
WHEN partition_type = 'DAYS' THEN
partition_year := SPLIT_PART(partition_by_max_ttl_date, '_', 3);
partition_month := SPLIT_PART(partition_by_max_ttl_date, '_', 4);
partition_day := SPLIT_PART(partition_by_max_ttl_date, '_', 5);
partition_by_max_ttl_year := SPLIT_PART(partition_by_max_ttl_date, '_', 3);
partition_by_max_ttl_month := SPLIT_PART(partition_by_max_ttl_date, '_', 4);
partition_by_max_ttl_day := SPLIT_PART(partition_by_max_ttl_date, '_', 5);
WHEN partition_type = 'MONTHS' THEN
partition_year := SPLIT_PART(partition_by_max_ttl_date, '_', 3);
partition_month := SPLIT_PART(partition_by_max_ttl_date, '_', 4);
partition_by_max_ttl_year := SPLIT_PART(partition_by_max_ttl_date, '_', 3);
partition_by_max_ttl_month := SPLIT_PART(partition_by_max_ttl_date, '_', 4);
ELSE
partition_year := SPLIT_PART(partition_by_max_ttl_date, '_', 3);
partition_by_max_ttl_year := SPLIT_PART(partition_by_max_ttl_date, '_', 3);
END CASE;
FOR partition IN SELECT tablename
FROM pg_tables
WHERE schemaname = 'public'
AND tablename like 'ts_kv_' || '%'
AND tablename != 'ts_kv_latest'
AND tablename != 'ts_kv_dictionary'
AND tablename != 'ts_kv_indefinite'
LOOP
IF partition != partition_by_max_ttl_date THEN
IF partition_year IS NOT NULL THEN
IF SPLIT_PART(partition, '_', 3)::integer < partition_year::integer THEN
partition_to_delete := partition;
ELSE
IF partition_month IS NOT NULL THEN
IF SPLIT_PART(partition, '_', 4)::integer < partition_month::integer THEN
partition_to_delete := partition;
IF partition_by_max_ttl_year IS NULL THEN
RAISE NOTICE 'Failed to remove partitions by max ttl date due to partition_by_max_ttl_year is null!';
ELSE
IF partition_type = 'YEARS' THEN
FOR partition IN SELECT tablename
FROM pg_tables
WHERE schemaname = 'public'
AND tablename like 'ts_kv_' || '%'
AND tablename != 'ts_kv_latest'
AND tablename != 'ts_kv_dictionary'
AND tablename != 'ts_kv_indefinite'
AND tablename != partition_by_max_ttl_date
LOOP
partition_year := SPLIT_PART(partition, '_', 3)::integer;
IF partition_year < partition_by_max_ttl_year::integer THEN
RAISE NOTICE 'Partition to delete by max ttl: %', partition;
EXECUTE format('DROP TABLE IF EXISTS %I', partition);
deleted := deleted + 1;
END IF;
END LOOP;
ELSE
IF partition_type = 'MONTHS' THEN
IF partition_by_max_ttl_month IS NULL THEN
RAISE NOTICE 'Failed to remove months partitions by max ttl date due to partition_by_max_ttl_month is null!';
ELSE
FOR partition IN SELECT tablename
FROM pg_tables
WHERE schemaname = 'public'
AND tablename like 'ts_kv_' || '%'
AND tablename != 'ts_kv_latest'
AND tablename != 'ts_kv_dictionary'
AND tablename != 'ts_kv_indefinite'
AND tablename != partition_by_max_ttl_date
LOOP
partition_year := SPLIT_PART(partition, '_', 3)::integer;
IF partition_year > partition_by_max_ttl_year::integer THEN
RAISE NOTICE 'Skip iteration! Partition: % is valid!', partition;
CONTINUE;
ELSE
IF partition_day IS NOT NULL THEN
IF SPLIT_PART(partition, '_', 5)::integer < partition_day::integer THEN
partition_to_delete := partition;
IF partition_year < partition_by_max_ttl_year::integer THEN
RAISE NOTICE 'Partition to delete by max ttl: %', partition;
EXECUTE format('DROP TABLE IF EXISTS %I', partition);
deleted := deleted + 1;
ELSE
partition_month := SPLIT_PART(partition, '_', 4)::integer;
IF partition_year = partition_by_max_ttl_year::integer THEN
IF partition_month >= partition_by_max_ttl_month::integer THEN
RAISE NOTICE 'Skip iteration! Partition: % is valid!', partition;
CONTINUE;
ELSE
RAISE NOTICE 'Partition to delete by max ttl: %', partition;
EXECUTE format('DROP TABLE IF EXISTS %I', partition);
deleted := deleted + 1;
END IF;
END IF;
END IF;
END IF;
END LOOP;
END IF;
ELSE
IF partition_type = 'DAYS' THEN
IF partition_by_max_ttl_month IS NULL THEN
RAISE NOTICE 'Failed to remove days partitions by max ttl date due to partition_by_max_ttl_month is null!';
ELSE
IF partition_by_max_ttl_day IS NULL THEN
RAISE NOTICE 'Failed to remove days partitions by max ttl date due to partition_by_max_ttl_day is null!';
ELSE
FOR partition IN SELECT tablename
FROM pg_tables
WHERE schemaname = 'public'
AND tablename like 'ts_kv_' || '%'
AND tablename != 'ts_kv_latest'
AND tablename != 'ts_kv_dictionary'
AND tablename != 'ts_kv_indefinite'
AND tablename != partition_by_max_ttl_date
LOOP
partition_year := SPLIT_PART(partition, '_', 3)::integer;
IF partition_year > partition_by_max_ttl_year::integer THEN
RAISE NOTICE 'Skip iteration! Partition: % is valid!', partition;
CONTINUE;
ELSE
IF partition_year < partition_by_max_ttl_year::integer THEN
RAISE NOTICE 'Partition to delete by max ttl: %', partition;
EXECUTE format('DROP TABLE IF EXISTS %I', partition);
deleted := deleted + 1;
ELSE
partition_month := SPLIT_PART(partition, '_', 4)::integer;
IF partition_month > partition_by_max_ttl_month::integer THEN
RAISE NOTICE 'Skip iteration! Partition: % is valid!', partition;
CONTINUE;
ELSE
IF partition_month < partition_by_max_ttl_month::integer THEN
RAISE NOTICE 'Partition to delete by max ttl: %', partition;
EXECUTE format('DROP TABLE IF EXISTS %I', partition);
deleted := deleted + 1;
ELSE
partition_day := SPLIT_PART(partition, '_', 5)::integer;
IF partition_day >= partition_by_max_ttl_day::integer THEN
RAISE NOTICE 'Skip iteration! Partition: % is valid!', partition;
CONTINUE;
ELSE
IF partition_day < partition_by_max_ttl_day::integer THEN
RAISE NOTICE 'Partition to delete by max ttl: %', partition;
EXECUTE format('DROP TABLE IF EXISTS %I', partition);
deleted := deleted + 1;
END IF;
END IF;
END IF;
END IF;
END IF;
END IF;
END LOOP;
END IF;
END IF;
END IF;
IF partition_to_delete IS NOT NULL THEN
RAISE NOTICE 'Partition to delete by max ttl: %', partition_to_delete;
EXECUTE format('DROP TABLE IF EXISTS %I', partition_to_delete);
partition_to_delete := NULL;
deleted := deleted + 1;
END IF;
END IF;
END LOOP;
END IF;
END IF;
END IF;
END IF;
END
@ -107,8 +193,6 @@ BEGIN
partition := 'ts_kv_' || to_char(date, 'yyyy') || '_' || to_char(date, 'MM');
WHEN partition_type = 'YEARS' THEN
partition := 'ts_kv_' || to_char(date, 'yyyy');
WHEN partition_type = 'INDEFINITE' THEN
partition := NULL;
ELSE
partition := NULL;
END CASE;

14
application/src/main/data/upgrade/3.2.2/schema_update.sql

@ -197,3 +197,17 @@ $$;
ALTER TABLE api_usage_state
ADD COLUMN IF NOT EXISTS alarm_exec VARCHAR(32);
UPDATE api_usage_state SET alarm_exec = 'ENABLED' WHERE alarm_exec IS NULL;
CREATE TABLE IF NOT EXISTS rpc (
id uuid NOT NULL CONSTRAINT rpc_pkey PRIMARY KEY,
created_time bigint NOT NULL,
tenant_id uuid NOT NULL,
device_id uuid NOT NULL,
expiration_time bigint NOT NULL,
request varchar(10000000) NOT NULL,
response varchar(10000000),
status varchar(255) NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_rpc_tenant_id_device_id ON rpc(tenant_id, device_id);

23
application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java

@ -65,6 +65,7 @@ import org.thingsboard.server.dao.relation.RelationService;
import org.thingsboard.server.dao.resource.ResourceService;
import org.thingsboard.server.dao.rule.RuleChainService;
import org.thingsboard.server.dao.rule.RuleNodeStateService;
import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
import org.thingsboard.server.dao.tenant.TenantProfileService;
import org.thingsboard.server.dao.tenant.TenantService;
import org.thingsboard.server.dao.timeseries.TimeseriesService;
@ -80,9 +81,9 @@ import org.thingsboard.server.service.executors.ExternalCallExecutorService;
import org.thingsboard.server.service.executors.SharedEventLoopGroupService;
import org.thingsboard.server.service.mail.MailExecutorService;
import org.thingsboard.server.service.profile.TbDeviceProfileCache;
import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
import org.thingsboard.server.service.queue.TbClusterService;
import org.thingsboard.server.service.rpc.TbCoreDeviceRpcService;
import org.thingsboard.server.service.rpc.TbRpcService;
import org.thingsboard.server.service.rpc.TbRuleEngineDeviceRpcService;
import org.thingsboard.server.service.script.JsExecutorService;
import org.thingsboard.server.service.script.JsInvokeService;
@ -303,23 +304,33 @@ public class ActorSystemContext {
@Lazy
@Autowired(required = false)
@Getter private EdgeService edgeService;
@Getter
private EdgeService edgeService;
@Lazy
@Autowired(required = false)
@Getter private EdgeEventService edgeEventService;
@Getter
private EdgeEventService edgeEventService;
@Lazy
@Autowired(required = false)
@Getter private EdgeRpcService edgeRpcService;
@Getter
private EdgeRpcService edgeRpcService;
@Lazy
@Autowired(required = false)
@Getter private ResourceService resourceService;
@Getter
private ResourceService resourceService;
@Lazy
@Autowired(required = false)
@Getter private OtaPackageService otaPackageService;
@Getter
private OtaPackageService otaPackageService;
@Lazy
@Autowired(required = false)
@Getter
private TbRpcService tbRpcService;
@Value("${actors.session.max_concurrent_sessions_per_device:1}")
@Getter

2
application/src/main/java/org/thingsboard/server/actors/device/DeviceActor.java

@ -46,7 +46,7 @@ public class DeviceActor extends ContextAwareActor {
super.init(ctx);
log.debug("[{}][{}] Starting device actor.", processor.tenantId, processor.deviceId);
try {
processor.initSessionTimeout(ctx);
processor.init(ctx);
log.debug("[{}][{}] Device actor started.", processor.tenantId, processor.deviceId);
} catch (Exception e) {
log.warn("[{}][{}] Unknown failure", processor.tenantId, processor.deviceId, e);

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

@ -23,6 +23,7 @@ import com.google.common.util.concurrent.MoreExecutors;
import com.google.protobuf.InvalidProtocolBufferException;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections.CollectionUtils;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.rule.engine.api.RpcError;
import org.thingsboard.rule.engine.api.msg.DeviceAttributesEventNotificationMsg;
import org.thingsboard.rule.engine.api.msg.DeviceCredentialsUpdateNotificationMsg;
@ -38,12 +39,17 @@ import org.thingsboard.server.common.data.edge.EdgeEventActionType;
import org.thingsboard.server.common.data.edge.EdgeEventType;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.EdgeId;
import org.thingsboard.server.common.data.id.RpcId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.kv.AttributeKey;
import org.thingsboard.server.common.data.kv.AttributeKvEntry;
import org.thingsboard.server.common.data.kv.KvEntry;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.relation.EntityRelation;
import org.thingsboard.server.common.data.relation.RelationTypeGroup;
import org.thingsboard.server.common.data.rpc.Rpc;
import org.thingsboard.server.common.data.rpc.RpcStatus;
import org.thingsboard.server.common.data.rpc.ToDeviceRpcRequestBody;
import org.thingsboard.server.common.data.security.DeviceCredentials;
import org.thingsboard.server.common.data.security.DeviceCredentialsType;
@ -52,8 +58,8 @@ import org.thingsboard.server.common.msg.TbMsgMetaData;
import org.thingsboard.server.common.msg.queue.TbCallback;
import org.thingsboard.server.common.msg.rpc.ToDeviceRpcRequest;
import org.thingsboard.server.common.msg.timeout.DeviceActorServerSideRpcTimeoutMsg;
import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.gen.transport.TransportProtos.AttributeUpdateNotificationMsg;
import org.thingsboard.server.gen.transport.TransportProtos.ClaimDeviceMsg;
import org.thingsboard.server.gen.transport.TransportProtos.DeviceSessionsCacheEntry;
import org.thingsboard.server.gen.transport.TransportProtos.GetAttributeRequestMsg;
import org.thingsboard.server.gen.transport.TransportProtos.GetAttributeResponseMsg;
@ -68,10 +74,12 @@ import org.thingsboard.server.gen.transport.TransportProtos.SessionType;
import org.thingsboard.server.gen.transport.TransportProtos.SubscribeToAttributeUpdatesMsg;
import org.thingsboard.server.gen.transport.TransportProtos.SubscribeToRPCMsg;
import org.thingsboard.server.gen.transport.TransportProtos.SubscriptionInfoProto;
import org.thingsboard.server.gen.transport.TransportProtos.ToDevicePersistedRpcResponseMsg;
import org.thingsboard.server.gen.transport.TransportProtos.ToDeviceRpcRequestMsg;
import org.thingsboard.server.gen.transport.TransportProtos.ToDeviceRpcResponseMsg;
import org.thingsboard.server.gen.transport.TransportProtos.ToServerRpcResponseMsg;
import org.thingsboard.server.gen.transport.TransportProtos.ToTransportMsg;
import org.thingsboard.server.gen.transport.TransportProtos.ToTransportUpdateCredentialsProto;
import org.thingsboard.server.gen.transport.TransportProtos.TransportToDeviceActorMsg;
import org.thingsboard.server.gen.transport.TransportProtos.TsKvProto;
import org.thingsboard.server.service.rpc.FromDeviceRpcResponse;
@ -162,20 +170,19 @@ class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcessor {
void processRpcRequest(TbActorCtx context, ToDeviceRpcRequestActorMsg msg) {
ToDeviceRpcRequest request = msg.getMsg();
ToDeviceRpcRequestBody body = request.getBody();
ToDeviceRpcRequestMsg rpcRequest = ToDeviceRpcRequestMsg.newBuilder()
.setRequestId(rpcSeq++)
.setMethodName(body.getMethod())
.setParams(body.getParams())
.setExpirationTime(request.getExpirationTime())
.setRequestIdMSB(request.getId().getMostSignificantBits())
.setRequestIdLSB(request.getId().getLeastSignificantBits())
.build();
ToDeviceRpcRequestMsg rpcRequest = creteToDeviceRpcRequestMsg(request);
long timeout = request.getExpirationTime() - System.currentTimeMillis();
boolean persisted = request.isPersisted();
if (timeout <= 0) {
log.debug("[{}][{}] Ignoring message due to exp time reached, {}", deviceId, request.getId(), request.getExpirationTime());
if (persisted) {
createRpc(request, RpcStatus.TIMEOUT);
}
return;
} else if (persisted) {
createRpc(request, RpcStatus.QUEUED);
}
boolean sent;
@ -192,10 +199,16 @@ class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcessor {
syncSessionSet.add(key);
}
});
log.trace("46) Rpc syncSessionSet [{}] subscription after sent [{}]",syncSessionSet, rpcSubscriptions);
log.trace("46) Rpc syncSessionSet [{}] subscription after sent [{}]", syncSessionSet, rpcSubscriptions);
syncSessionSet.forEach(rpcSubscriptions::remove);
}
if (persisted && !(sent || request.isOneway())) {
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) {
log.debug("[{}] Rpc command response sent [{}]!", deviceId, request.getId());
systemContext.getTbCoreDeviceRpcService().processRpcResponseFromDeviceActor(new FromDeviceRpcResponse(msg.getMsg().getId(), null, null));
@ -209,6 +222,32 @@ class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcessor {
}
}
private Rpc createRpc(ToDeviceRpcRequest request, RpcStatus status) {
Rpc rpc = new Rpc(new RpcId(request.getId()));
rpc.setCreatedTime(System.currentTimeMillis());
rpc.setTenantId(tenantId);
rpc.setDeviceId(deviceId);
rpc.setExpirationTime(request.getExpirationTime());
rpc.setRequest(JacksonUtil.valueToTree(request));
rpc.setStatus(status);
systemContext.getTbRpcService().save(tenantId, rpc);
return systemContext.getTbRpcService().save(tenantId, rpc);
}
private ToDeviceRpcRequestMsg creteToDeviceRpcRequestMsg(ToDeviceRpcRequest request) {
ToDeviceRpcRequestBody body = request.getBody();
return ToDeviceRpcRequestMsg.newBuilder()
.setRequestId(rpcSeq++)
.setMethodName(body.getMethod())
.setParams(body.getParams())
.setExpirationTime(request.getExpirationTime())
.setRequestIdMSB(request.getId().getMostSignificantBits())
.setRequestIdLSB(request.getId().getLeastSignificantBits())
.setOneway(request.isOneway())
.setPersisted(request.isPersisted())
.build();
}
void processRpcResponsesFromEdge(TbActorCtx context, FromDeviceRpcResponseActorMsg responseMsg) {
log.debug("[{}] Processing rpc command response from edge session", deviceId);
ToDeviceRpcRequestMetadata requestMd = toDeviceRpcPendingMap.remove(responseMsg.getRequestId());
@ -230,6 +269,9 @@ class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcessor {
ToDeviceRpcRequestMetadata requestMd = toDeviceRpcPendingMap.remove(msg.getId());
if (requestMd != null) {
log.debug("[{}] RPC request [{}] timeout detected!", deviceId, msg.getId());
if (requestMd.getMsg().getMsg().isPersisted()) {
systemContext.getTbRpcService().save(tenantId, new RpcId(requestMd.getMsg().getMsg().getId()), RpcStatus.TIMEOUT, null);
}
systemContext.getTbCoreDeviceRpcService().processRpcResponseFromDeviceActor(new FromDeviceRpcResponse(requestMd.getMsg().getMsg().getId(),
null, requestMd.isSent() ? RpcError.TIMEOUT : RpcError.NO_ACTIVE_CONNECTION));
}
@ -271,7 +313,10 @@ class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcessor {
.setExpirationTime(request.getExpirationTime())
.setRequestIdMSB(request.getId().getMostSignificantBits())
.setRequestIdLSB(request.getId().getLeastSignificantBits())
.setOneway(request.isOneway())
.setPersisted(request.isPersisted())
.build();
sendToTransport(rpcRequest, sessionId, nodeId);
};
}
@ -279,31 +324,39 @@ class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcessor {
void process(TbActorCtx context, TransportToDeviceActorMsgWrapper wrapper) {
TransportToDeviceActorMsg msg = wrapper.getMsg();
TbCallback callback = wrapper.getCallback();
var sessionInfo = msg.getSessionInfo();
if (msg.hasSessionEvent()) {
processSessionStateMsgs(msg.getSessionInfo(), msg.getSessionEvent());
processSessionStateMsgs(sessionInfo, msg.getSessionEvent());
}
if (msg.hasSubscribeToAttributes()) {
processSubscriptionCommands(context, msg.getSessionInfo(), msg.getSubscribeToAttributes());
processSubscriptionCommands(context, sessionInfo, msg.getSubscribeToAttributes());
}
if (msg.hasSubscribeToRPC()) {
processSubscriptionCommands(context, msg.getSessionInfo(), msg.getSubscribeToRPC());
processSubscriptionCommands(context, sessionInfo, msg.getSubscribeToRPC());
}
if (msg.hasSendPendingRPC()) {
sendPendingRequests(context, getSessionId(sessionInfo), sessionInfo);
}
if (msg.hasGetAttributes()) {
handleGetAttributesRequest(context, msg.getSessionInfo(), msg.getGetAttributes());
handleGetAttributesRequest(context, sessionInfo, msg.getGetAttributes());
}
if (msg.hasToDeviceRPCCallResponse()) {
processRpcResponses(context, msg.getSessionInfo(), msg.getToDeviceRPCCallResponse());
processRpcResponses(context, sessionInfo, msg.getToDeviceRPCCallResponse());
}
if (msg.hasSubscriptionInfo()) {
handleSessionActivity(context, msg.getSessionInfo(), msg.getSubscriptionInfo());
handleSessionActivity(context, sessionInfo, msg.getSubscriptionInfo());
}
if (msg.hasClaimDevice()) {
handleClaimDeviceMsg(context, msg.getSessionInfo(), msg.getClaimDevice());
handleClaimDeviceMsg(context, sessionInfo, msg.getClaimDevice());
}
if (msg.hasPersistedRpcResponseMsg()) {
processPersistedRpcResponses(context, sessionInfo, msg.getPersistedRpcResponseMsg());
}
callback.onSuccess();
}
private void handleClaimDeviceMsg(TbActorCtx context, SessionInfoProto sessionInfo, TransportProtos.ClaimDeviceMsg msg) {
private void handleClaimDeviceMsg(TbActorCtx context, SessionInfoProto sessionInfo, ClaimDeviceMsg msg) {
DeviceId deviceId = new DeviceId(new UUID(msg.getDeviceIdMSB(), msg.getDeviceIdLSB()));
systemContext.getClaimDevicesService().registerClaimingInfo(tenantId, deviceId, msg.getSecretKey(), msg.getDurationMs());
}
@ -442,11 +495,22 @@ class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcessor {
if (success) {
systemContext.getTbCoreDeviceRpcService().processRpcResponseFromDeviceActor(new FromDeviceRpcResponse(requestMd.getMsg().getMsg().getId(),
responseMsg.getPayload(), null));
if (requestMd.getMsg().getMsg().isPersisted()) {
systemContext.getTbRpcService().save(tenantId, new RpcId(requestMd.getMsg().getMsg().getId()), RpcStatus.SUCCESSFUL, JacksonUtil.toJsonNode(responseMsg.getPayload()));
}
} 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()));
}
}
}
private void processPersistedRpcResponses(TbActorCtx context, SessionInfoProto sessionInfo, ToDevicePersistedRpcResponseMsg responseMsg) {
UUID rpcId = new UUID(responseMsg.getRequestIdMSB(), responseMsg.getRequestIdLSB());
systemContext.getTbRpcService().save(tenantId, new RpcId(rpcId), RpcStatus.valueOf(responseMsg.getStatus()), null);
}
private void processSubscriptionCommands(TbActorCtx context, SessionInfoProto sessionInfo, SubscribeToAttributeUpdatesMsg subscribeCmd) {
UUID sessionId = getSessionId(sessionInfo);
if (subscribeCmd.getUnsubscribe()) {
@ -565,7 +629,7 @@ class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcessor {
void notifyTransportAboutProfileUpdate(UUID sessionId, SessionInfoMetaData sessionMd, DeviceCredentials deviceCredentials) {
log.info("2) LwM2Mtype: ");
TransportProtos.ToTransportUpdateCredentialsProto.Builder notification = TransportProtos.ToTransportUpdateCredentialsProto.newBuilder();
ToTransportUpdateCredentialsProto.Builder notification = ToTransportUpdateCredentialsProto.newBuilder();
notification.addCredentialsId(deviceCredentials.getCredentialsId());
notification.addCredentialsValue(deviceCredentials.getCredentialsValue());
ToTransportMsg msg = ToTransportMsg.newBuilder()
@ -640,7 +704,7 @@ class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcessor {
ListenableFuture<EdgeEvent> future = systemContext.getEdgeEventService().saveAsync(edgeEvent);
Futures.addCallback(future, new FutureCallback<EdgeEvent>() {
@Override
public void onSuccess( EdgeEvent result) {
public void onSuccess(EdgeEvent result) {
systemContext.getClusterService().onEdgeEventUpdate(tenantId, edgeId);
}
@ -756,8 +820,26 @@ class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcessor {
.addAllSessions(sessionsList).build().toByteArray());
}
void initSessionTimeout(TbActorCtx ctx) {
void init(TbActorCtx ctx) {
schedulePeriodicMsgWithDelay(ctx, SessionTimeoutCheckMsg.instance(), systemContext.getSessionReportTimeout(), systemContext.getSessionReportTimeout());
PageLink pageLink = new PageLink(1024);
PageData<Rpc> pageData;
do {
pageData = systemContext.getTbRpcService().findAllByDeviceIdAndStatus(tenantId, deviceId, RpcStatus.QUEUED, pageLink);
pageData.getData().forEach(rpc -> {
ToDeviceRpcRequest msg = JacksonUtil.convertValue(rpc.getRequest(), ToDeviceRpcRequest.class);
long timeout = rpc.getExpirationTime() - System.currentTimeMillis();
if (timeout <= 0) {
rpc.setStatus(RpcStatus.TIMEOUT);
systemContext.getTbRpcService().save(tenantId, rpc);
} else {
registerPendingRpcRequest(ctx, new ToDeviceRpcRequestActorMsg(systemContext.getServiceId(), msg), false, creteToDeviceRpcRequestMsg(msg), timeout);
}
});
if (pageData.hasNext()) {
pageLink = pageLink.nextPageLink();
}
} while (pageData.hasNext());
}
void checkSessionsTimeout() {

18
application/src/main/java/org/thingsboard/server/controller/BaseController.java

@ -69,6 +69,7 @@ import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.EntityIdFactory;
import org.thingsboard.server.common.data.id.EntityViewId;
import org.thingsboard.server.common.data.id.OtaPackageId;
import org.thingsboard.server.common.data.id.RpcId;
import org.thingsboard.server.common.data.id.TbResourceId;
import org.thingsboard.server.common.data.id.RuleChainId;
import org.thingsboard.server.common.data.id.RuleNodeId;
@ -83,6 +84,7 @@ import org.thingsboard.server.common.data.page.TimePageLink;
import org.thingsboard.server.common.data.plugin.ComponentDescriptor;
import org.thingsboard.server.common.data.plugin.ComponentType;
import org.thingsboard.server.common.data.relation.EntityRelation;
import org.thingsboard.server.common.data.rpc.Rpc;
import org.thingsboard.server.common.data.rule.RuleChain;
import org.thingsboard.server.common.data.rule.RuleChainType;
import org.thingsboard.server.common.data.rule.RuleNode;
@ -106,6 +108,7 @@ import org.thingsboard.server.dao.model.ModelConstants;
import org.thingsboard.server.dao.oauth2.OAuth2ConfigTemplateService;
import org.thingsboard.server.dao.oauth2.OAuth2Service;
import org.thingsboard.server.dao.relation.RelationService;
import org.thingsboard.server.dao.rpc.RpcService;
import org.thingsboard.server.dao.rule.RuleChainService;
import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
import org.thingsboard.server.dao.tenant.TenantProfileService;
@ -245,6 +248,9 @@ public abstract class BaseController {
@Autowired
protected OtaPackageStateService otaPackageStateService;
@Autowired
protected RpcService rpcService;
@Autowired
protected TbQueueProducerProvider producerProvider;
@ -786,6 +792,18 @@ public abstract class BaseController {
}
}
Rpc checkRpcId(RpcId rpcId, Operation operation) throws ThingsboardException {
try {
validateId(rpcId, "Incorrect rpcId " + rpcId);
Rpc rpc = rpcService.findById(getCurrentUser().getTenantId(), rpcId);
checkNotNull(rpc);
accessControlService.checkPermission(getCurrentUser(), Resource.RPC, operation, rpcId, rpc);
return rpc;
} catch (Exception e) {
throw handleException(e, false);
}
}
@SuppressWarnings("unchecked")
protected <I extends EntityId> I emptyId(EntityType entityType) {
return (I) EntityIdFactory.getByTypeAndUuid(entityType, ModelConstants.NULL_UUID);

10
application/src/main/java/org/thingsboard/server/controller/DeviceController.java

@ -782,15 +782,17 @@ public class DeviceController extends BaseController {
}
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/devices/count/{otaPackageType}", method = RequestMethod.GET)
@RequestMapping(value = "/devices/count/{otaPackageType}/{deviceProfileId}", method = RequestMethod.GET)
@ResponseBody
public Long countDevicesByTenantIdAndDeviceProfileIdAndEmptyOtaPackage(@PathVariable("otaPackageType") String otaPackageType,
@RequestParam String deviceProfileId) throws ThingsboardException {
public Long countByDeviceProfileAndEmptyOtaPackage(@PathVariable("otaPackageType") String otaPackageType,
@PathVariable("deviceProfileId") String deviceProfileId) throws ThingsboardException {
checkParameter("OtaPackageType", otaPackageType);
checkParameter("DeviceProfileId", deviceProfileId);
try {
return deviceService.countDevicesByTenantIdAndDeviceProfileIdAndEmptyOtaPackage(
getCurrentUser().getTenantId(), new DeviceProfileId(UUID.fromString(deviceProfileId)), OtaPackageType.valueOf(otaPackageType));
getTenantId(),
new DeviceProfileId(UUID.fromString(deviceProfileId)),
OtaPackageType.valueOf(otaPackageType));
} catch (Exception e) {
throw handleException(e);
}

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

@ -29,6 +29,7 @@ import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.request.async.DeferredResult;
@ -38,8 +39,13 @@ import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.RpcId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.UUIDBased;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.rpc.Rpc;
import org.thingsboard.server.common.data.rpc.RpcStatus;
import org.thingsboard.server.common.data.rpc.ToDeviceRpcRequestBody;
import org.thingsboard.server.common.msg.rpc.ToDeviceRpcRequest;
import org.thingsboard.server.queue.util.TbCoreComponent;
@ -93,6 +99,52 @@ public class RpcController extends BaseController {
return handleDeviceRPCRequest(false, new DeviceId(UUID.fromString(deviceIdStr)), requestBody);
}
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/persisted/{rpcId}", method = RequestMethod.GET)
@ResponseBody
public Rpc getPersistedRpc(@PathVariable("rpcId") String strRpc) throws ThingsboardException {
checkParameter("RpcId", strRpc);
try {
RpcId rpcId = new RpcId(UUID.fromString(strRpc));
return checkRpcId(rpcId, Operation.READ);
} catch (Exception e) {
throw handleException(e);
}
}
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/persisted/{deviceId}", method = RequestMethod.GET)
@ResponseBody
public PageData<Rpc> getPersistedRpcByDevice(@PathVariable("deviceId") String strDeviceId,
@RequestParam int pageSize,
@RequestParam int page,
@RequestParam RpcStatus rpcStatus,
@RequestParam(required = false) String textSearch,
@RequestParam(required = false) String sortProperty,
@RequestParam(required = false) String sortOrder) throws ThingsboardException {
checkParameter("DeviceId", strDeviceId);
try {
TenantId tenantId = getCurrentUser().getTenantId();
PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder);
DeviceId deviceId = new DeviceId(UUID.fromString(strDeviceId));
return checkNotNull(rpcService.findAllByDeviceIdAndStatus(tenantId, deviceId, rpcStatus, pageLink));
} catch (Exception e) {
throw handleException(e);
}
}
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/persisted/{rpcId}", method = RequestMethod.DELETE)
@ResponseBody
public void deleteResource(@PathVariable("rpcId") String strRpc) throws ThingsboardException {
checkParameter("RpcId", strRpc);
try {
rpcService.deleteRpc(getTenantId(), new RpcId(UUID.fromString(strRpc)));
} catch (Exception e) {
throw handleException(e);
}
}
private DeferredResult<ResponseEntity> handleDeviceRPCRequest(boolean oneWay, DeviceId deviceId, String requestBody) throws ThingsboardException {
try {
JsonNode rpcRequestBody = jsonMapper.readTree(requestBody);
@ -103,6 +155,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();
accessValidator.validate(currentUser, Operation.RPC_CALL, deviceId, new HttpValidationCallback(response, new FutureCallback<DeferredResult<ResponseEntity>>() {
@Override
public void onSuccess(@Nullable DeferredResult<ResponseEntity> result) {
@ -111,7 +164,8 @@ public class RpcController extends BaseController {
deviceId,
oneWay,
expTime,
body
body,
persisted
);
deviceRpcService.processRestApiRpcRequest(rpcRequest, fromDeviceRpcResponse -> reply(new LocalRequestMetaData(rpcRequest, currentUser, result), fromDeviceRpcResponse), currentUser);
}

14
application/src/main/java/org/thingsboard/server/controller/RuleChainController.java

@ -74,6 +74,7 @@ import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
@Slf4j
@ -86,6 +87,7 @@ public class RuleChainController extends BaseController {
public static final String RULE_NODE_ID = "ruleNodeId";
private static final ObjectMapper objectMapper = new ObjectMapper();
public static final int TIMEOUT = 20;
@Autowired
private InstallScripts installScripts;
@ -388,25 +390,25 @@ public class RuleChainController extends BaseController {
TbMsg inMsg = TbMsg.newMsg(msgType, null, new TbMsgMetaData(metadata), TbMsgDataType.JSON, data);
switch (scriptType) {
case "update":
output = msgToOutput(engine.executeUpdate(inMsg));
output = msgToOutput(engine.executeUpdateAsync(inMsg).get(TIMEOUT, TimeUnit.SECONDS));
break;
case "generate":
output = msgToOutput(engine.executeGenerate(inMsg));
output = msgToOutput(engine.executeGenerateAsync(inMsg).get(TIMEOUT, TimeUnit.SECONDS));
break;
case "filter":
boolean result = engine.executeFilter(inMsg);
boolean result = engine.executeFilterAsync(inMsg).get(TIMEOUT, TimeUnit.SECONDS);
output = Boolean.toString(result);
break;
case "switch":
Set<String> states = engine.executeSwitch(inMsg);
Set<String> states = engine.executeSwitchAsync(inMsg).get(TIMEOUT, TimeUnit.SECONDS);
output = objectMapper.writeValueAsString(states);
break;
case "json":
JsonNode json = engine.executeJson(inMsg);
JsonNode json = engine.executeJsonAsync(inMsg).get(TIMEOUT, TimeUnit.SECONDS);
output = objectMapper.writeValueAsString(json);
break;
case "string":
output = engine.executeToString(inMsg);
output = engine.executeToStringAsync(inMsg).get(TIMEOUT, TimeUnit.SECONDS);
break;
default:
throw new IllegalArgumentException("Unsupported script type: " + scriptType);

11
application/src/main/java/org/thingsboard/server/service/resource/DefaultTbResourceService.java

@ -86,7 +86,10 @@ public class DefaultTbResourceService implements TbResourceService {
} else {
throw new DataValidationException(String.format("Could not parse the XML of objectModel with name %s", resource.getSearchText()));
}
} catch (InvalidDDFFileException | IOException e) {
} catch (InvalidDDFFileException e) {
log.error("Failed to parse file {}", resource.getFileName(), e);
throw new DataValidationException("Failed to parse file " + resource.getFileName());
} catch (IOException e) {
throw new ThingsboardException(e, ThingsboardErrorCode.GENERAL);
}
if (resource.getResourceType().equals(ResourceType.LWM2M_MODEL) && toLwM2mObject(resource, true) == null) {
@ -194,8 +197,7 @@ public class DefaultTbResourceService implements TbResourceService {
if (isSave) {
LwM2mResourceObserve lwM2MResourceObserve = new LwM2mResourceObserve(k, v.name, false, false, false);
resources.add(lwM2MResourceObserve);
}
else if (v.operations.isReadable()) {
} else if (v.operations.isReadable()) {
LwM2mResourceObserve lwM2MResourceObserve = new LwM2mResourceObserve(k, v.name, false, false, false);
resources.add(lwM2MResourceObserve);
}
@ -204,8 +206,7 @@ public class DefaultTbResourceService implements TbResourceService {
instance.setResources(resources.toArray(LwM2mResourceObserve[]::new));
lwM2mObject.setInstances(new LwM2mInstance[]{instance});
return lwM2mObject;
}
else {
} else {
return null;
}
}

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

@ -157,6 +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()));
Device device = deviceService.findDeviceById(msg.getTenantId(), msg.getDeviceId());
if (device != null) {

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

@ -100,7 +100,7 @@ public class DefaultTbRuleEngineRpcService implements TbRuleEngineDeviceRpcServi
@Override
public void sendRpcRequestToDevice(RuleEngineDeviceRpcRequest src, Consumer<RuleEngineDeviceRpcResponse> consumer) {
ToDeviceRpcRequest request = new ToDeviceRpcRequest(src.getRequestUUID(), src.getTenantId(), src.getDeviceId(),
src.isOneway(), src.getExpirationTime(), new ToDeviceRpcRequestBody(src.getMethod(), src.getBody()));
src.isOneway(), src.getExpirationTime(), new ToDeviceRpcRequestBody(src.getMethod(), src.getBody()), src.isPersisted());
forwardRpcRequestToDeviceActor(request, response -> {
if (src.isRestApiCall()) {
sendRpcResponseToTbCore(src.getOriginServiceId(), response);

77
application/src/main/java/org/thingsboard/server/service/rpc/TbRpcService.java

@ -0,0 +1,77 @@
/**
* 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.rpc;
import com.fasterxml.jackson.databind.JsonNode;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.RpcId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.rpc.Rpc;
import org.thingsboard.server.common.data.rpc.RpcStatus;
import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.TbMsgMetaData;
import org.thingsboard.server.dao.rpc.RpcService;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.queue.TbClusterService;
@TbCoreComponent
@Service
@RequiredArgsConstructor
@Slf4j
public class TbRpcService {
private final RpcService rpcService;
private final TbClusterService tbClusterService;
public Rpc save(TenantId tenantId, Rpc rpc) {
Rpc saved = rpcService.save(rpc);
pushRpcMsgToRuleEngine(tenantId, saved);
return saved;
}
public void save(TenantId tenantId, RpcId rpcId, RpcStatus newStatus, JsonNode response) {
Rpc foundRpc = rpcService.findById(tenantId, rpcId);
if (foundRpc != null) {
foundRpc.setStatus(newStatus);
if (response != null) {
foundRpc.setResponse(response);
}
Rpc saved = rpcService.save(foundRpc);
pushRpcMsgToRuleEngine(tenantId, saved);
} else {
log.warn("[{}] Failed to update RPC status because RPC was already deleted", rpcId);
}
}
private void pushRpcMsgToRuleEngine(TenantId tenantId, Rpc rpc) {
TbMsg msg = TbMsg.newMsg("RPC_" + rpc.getStatus().name(), rpc.getDeviceId(), TbMsgMetaData.EMPTY, JacksonUtil.toString(rpc));
tbClusterService.pushMsgToRuleEngine(tenantId, rpc.getId(), msg, null);
}
public Rpc findRpcById(TenantId tenantId, RpcId rpcId) {
return rpcService.findById(tenantId, rpcId);
}
public PageData<Rpc> findAllByDeviceIdAndStatus(TenantId tenantId, DeviceId deviceId, RpcStatus rpcStatus, PageLink pageLink) {
return rpcService.findAllByDeviceIdAndStatus(tenantId, deviceId, rpcStatus, pageLink);
}
}

14
application/src/main/java/org/thingsboard/server/service/script/AbstractJsInvokeService.java

@ -30,6 +30,7 @@ import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicInteger;
/**
@ -84,8 +85,10 @@ public abstract class AbstractJsInvokeService implements JsInvokeService {
apiUsageClient.report(tenantId, customerId, ApiUsageRecordKey.JS_EXEC_COUNT, 1);
return doInvokeFunction(scriptId, functionName, args);
} else {
return Futures.immediateFailedFuture(
new RuntimeException("Script invocation is blocked due to maximum error count " + getMaxErrors() + "!"));
String message = "Script invocation is blocked due to maximum error count "
+ getMaxErrors() + ", scriptId " + scriptId + "!";
log.warn(message);
return Futures.immediateFailedFuture(new RuntimeException(message));
}
} else {
return Futures.immediateFailedFuture(new RuntimeException("JS Execution is disabled due to API limits!"));
@ -117,8 +120,11 @@ public abstract class AbstractJsInvokeService implements JsInvokeService {
protected abstract long getMaxBlacklistDuration();
protected void onScriptExecutionError(UUID scriptId) {
disabledFunctions.computeIfAbsent(scriptId, key -> new DisableListInfo()).incrementAndGet();
protected void onScriptExecutionError(UUID scriptId, Throwable t, String scriptBody) {
DisableListInfo disableListInfo = disabledFunctions.computeIfAbsent(scriptId, key -> new DisableListInfo());
log.warn("Script has exception and will increment counter {} on disabledFunctions for id {}, exception {}, cause {}, scriptBody {}",
disableListInfo.get(), scriptId, t, t.getCause(), scriptBody);
disableListInfo.incrementAndGet();
}
private String generateJsScript(JsScriptType scriptType, String functionName, String scriptBody, String... argNames) {

2
application/src/main/java/org/thingsboard/server/service/script/AbstractNashornJsInvokeService.java

@ -160,7 +160,7 @@ public abstract class AbstractNashornJsInvokeService extends AbstractJsInvokeSer
return ((Invocable) engine).invokeFunction(functionName, args);
}
} catch (Exception e) {
onScriptExecutionError(scriptId);
onScriptExecutionError(scriptId, e, functionName);
throw new ExecutionException(e);
}
});

19
application/src/main/java/org/thingsboard/server/service/script/RemoteJsInvokeService.java

@ -18,7 +18,6 @@ package org.thingsboard.server.service.script;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
@ -26,6 +25,7 @@ import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import org.springframework.util.StopWatch;
import org.thingsboard.common.util.ThingsBoardThreadFactory;
import org.thingsboard.server.gen.js.JsInvokeProtos;
import org.thingsboard.server.queue.TbQueueRequestTemplate;
@ -161,7 +161,8 @@ public class RemoteJsInvokeService extends AbstractJsInvokeService {
@Override
protected ListenableFuture<Object> doInvokeFunction(UUID scriptId, String functionName, Object[] args) {
String scriptBody = scriptIdToBodysMap.get(scriptId);
log.trace("doInvokeFunction js-request for uuid {} with timeout {}ms", scriptId, maxRequestsTimeout);
final String scriptBody = scriptIdToBodysMap.get(scriptId);
if (scriptBody == null) {
return Futures.immediateFailedFuture(new RuntimeException("No script body found for scriptId: [" + scriptId + "]!"));
}
@ -170,7 +171,7 @@ public class RemoteJsInvokeService extends AbstractJsInvokeService {
.setScriptIdLSB(scriptId.getLeastSignificantBits())
.setFunctionName(functionName)
.setTimeout((int) maxRequestsTimeout)
.setScriptBody(scriptIdToBodysMap.get(scriptId));
.setScriptBody(scriptBody);
for (Object arg : args) {
jsRequestBuilder.addArgs(arg.toString());
@ -180,6 +181,9 @@ public class RemoteJsInvokeService extends AbstractJsInvokeService {
.setInvokeRequest(jsRequestBuilder.build())
.build();
StopWatch stopWatch = new StopWatch();
stopWatch.start();
ListenableFuture<TbProtoQueueMsg<JsInvokeProtos.RemoteJsResponse>> future = requestTemplate.send(new TbProtoJsQueueMsg<>(UUID.randomUUID(), jsRequestWrapper));
if (maxRequestsTimeout > 0) {
future = Futures.withTimeout(future, maxRequestsTimeout, TimeUnit.MILLISECONDS, timeoutExecutorService);
@ -193,7 +197,7 @@ public class RemoteJsInvokeService extends AbstractJsInvokeService {
@Override
public void onFailure(Throwable t) {
onScriptExecutionError(scriptId);
onScriptExecutionError(scriptId, t, scriptBody);
if (t instanceof TimeoutException || (t.getCause() != null && t.getCause() instanceof TimeoutException)) {
queueTimeoutMsgs.incrementAndGet();
}
@ -201,13 +205,16 @@ public class RemoteJsInvokeService extends AbstractJsInvokeService {
}
}, callbackExecutor);
return Futures.transform(future, response -> {
stopWatch.stop();
log.trace("doInvokeFunction js-response took {}ms for uuid {}", stopWatch.getTotalTimeMillis(), response.getKey());
JsInvokeProtos.JsInvokeResponse invokeResult = response.getValue().getInvokeResponse();
if (invokeResult.getSuccess()) {
return invokeResult.getResult();
} else {
onScriptExecutionError(scriptId);
final RuntimeException e = new RuntimeException(invokeResult.getErrorDetails());
onScriptExecutionError(scriptId, e, scriptBody);
log.debug("[{}] Failed to compile script due to [{}]: {}", scriptId, invokeResult.getErrorCode().name(), invokeResult.getErrorDetails());
throw new RuntimeException(invokeResult.getErrorDetails());
throw e;
}
}, callbackExecutor);
}

158
application/src/main/java/org/thingsboard/server/service/script/RuleNodeJsScriptEngine.java

@ -18,12 +18,12 @@ package org.thingsboard.server.service.script;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.collect.Sets;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.msg.TbMsg;
@ -32,6 +32,7 @@ import org.thingsboard.server.common.msg.TbMsgMetaData;
import javax.script.ScriptException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
@ -102,140 +103,115 @@ public class RuleNodeJsScriptEngine implements org.thingsboard.rule.engine.api.S
String newMessageType = !StringUtils.isEmpty(messageType) ? messageType : msg.getType();
return TbMsg.transformMsg(msg, newMessageType, msg.getOriginator(), newMetadata, newData);
} catch (Throwable th) {
th.printStackTrace();
throw new RuntimeException("Failed to unbind message data from javascript result", th);
}
}
@Override
public List<TbMsg> executeUpdate(TbMsg msg) throws ScriptException {
JsonNode result = executeScript(msg);
if (result.isObject()) {
return Collections.singletonList(unbindMsg(result, msg));
} else if (result.isArray()){
List<TbMsg> res = new ArrayList<>(result.size());
result.forEach(jsonObject -> res.add(unbindMsg(jsonObject, msg)));
return res;
} else {
log.warn("Wrong result type: {}", result.getNodeType());
throw new ScriptException("Wrong result type: " + result.getNodeType());
public ListenableFuture<List<TbMsg>> executeUpdateAsync(TbMsg msg) {
ListenableFuture<JsonNode> result = executeScriptAsync(msg);
return Futures.transformAsync(result,
json -> executeUpdateTransform(msg, json),
MoreExecutors.directExecutor());
}
ListenableFuture<List<TbMsg>> executeUpdateTransform(TbMsg msg, JsonNode json) {
if (json.isObject()) {
return Futures.immediateFuture(Collections.singletonList(unbindMsg(json, msg)));
} else if (json.isArray()) {
List<TbMsg> res = new ArrayList<>(json.size());
json.forEach(jsonObject -> res.add(unbindMsg(jsonObject, msg)));
return Futures.immediateFuture(res);
}
log.warn("Wrong result type: {}", json.getNodeType());
return Futures.immediateFailedFuture(new ScriptException("Wrong result type: " + json.getNodeType()));
}
@Override
public ListenableFuture<List<TbMsg>> executeUpdateAsync(TbMsg msg) {
ListenableFuture<JsonNode> result = executeScriptAsync(msg);
return Futures.transformAsync(result, json -> {
if (json.isObject()) {
return Futures.immediateFuture(Collections.singletonList(unbindMsg(json, msg)));
} else if (json.isArray()){
List<TbMsg> res = new ArrayList<>(json.size());
json.forEach(jsonObject -> res.add(unbindMsg(jsonObject, msg)));
return Futures.immediateFuture(res);
}
else{
log.warn("Wrong result type: {}", json.getNodeType());
return Futures.immediateFailedFuture(new ScriptException("Wrong result type: " + json.getNodeType()));
}
}, MoreExecutors.directExecutor());
public ListenableFuture<TbMsg> executeGenerateAsync(TbMsg prevMsg) {
return Futures.transformAsync(executeScriptAsync(prevMsg),
result -> executeGenerateTransform(prevMsg, result),
MoreExecutors.directExecutor());
}
@Override
public TbMsg executeGenerate(TbMsg prevMsg) throws ScriptException {
JsonNode result = executeScript(prevMsg);
ListenableFuture<TbMsg> executeGenerateTransform(TbMsg prevMsg, JsonNode result) {
if (!result.isObject()) {
log.warn("Wrong result type: {}", result.getNodeType());
throw new ScriptException("Wrong result type: " + result.getNodeType());
Futures.immediateFailedFuture(new ScriptException("Wrong result type: " + result.getNodeType()));
}
return unbindMsg(result, prevMsg);
return Futures.immediateFuture(unbindMsg(result, prevMsg));
}
@Override
public JsonNode executeJson(TbMsg msg) throws ScriptException {
return executeScript(msg);
}
@Override
public ListenableFuture<JsonNode> executeJsonAsync(TbMsg msg) throws ScriptException {
public ListenableFuture<JsonNode> executeJsonAsync(TbMsg msg) {
return executeScriptAsync(msg);
}
@Override
public String executeToString(TbMsg msg) throws ScriptException {
JsonNode result = executeScript(msg);
if (!result.isTextual()) {
log.warn("Wrong result type: {}", result.getNodeType());
throw new ScriptException("Wrong result type: " + result.getNodeType());
}
return result.asText();
public ListenableFuture<String> executeToStringAsync(TbMsg msg) {
return Futures.transformAsync(executeScriptAsync(msg),
this::executeToStringTransform,
MoreExecutors.directExecutor());
}
@Override
public boolean executeFilter(TbMsg msg) throws ScriptException {
JsonNode result = executeScript(msg);
if (!result.isBoolean()) {
log.warn("Wrong result type: {}", result.getNodeType());
throw new ScriptException("Wrong result type: " + result.getNodeType());
ListenableFuture<String> executeToStringTransform(JsonNode result) {
if (result.isTextual()) {
return Futures.immediateFuture(result.asText());
}
return result.asBoolean();
log.warn("Wrong result type: {}", result.getNodeType());
return Futures.immediateFailedFuture(new ScriptException("Wrong result type: " + result.getNodeType()));
}
@Override
public ListenableFuture<Boolean> executeFilterAsync(TbMsg msg) {
ListenableFuture<JsonNode> result = executeScriptAsync(msg);
return Futures.transformAsync(result, json -> {
if (!json.isBoolean()) {
log.warn("Wrong result type: {}", json.getNodeType());
return Futures.immediateFailedFuture(new ScriptException("Wrong result type: " + json.getNodeType()));
} else {
return Futures.immediateFuture(json.asBoolean());
}
}, MoreExecutors.directExecutor());
return Futures.transformAsync(executeScriptAsync(msg),
this::executeFilterTransform,
MoreExecutors.directExecutor());
}
@Override
public Set<String> executeSwitch(TbMsg msg) throws ScriptException {
JsonNode result = executeScript(msg);
ListenableFuture<Boolean> executeFilterTransform(JsonNode json) {
if (json.isBoolean()) {
return Futures.immediateFuture(json.asBoolean());
}
log.warn("Wrong result type: {}", json.getNodeType());
return Futures.immediateFailedFuture(new ScriptException("Wrong result type: " + json.getNodeType()));
}
ListenableFuture<Set<String>> executeSwitchTransform(JsonNode result) {
if (result.isTextual()) {
return Collections.singleton(result.asText());
} else if (result.isArray()) {
Set<String> nextStates = Sets.newHashSet();
return Futures.immediateFuture(Collections.singleton(result.asText()));
}
if (result.isArray()) {
Set<String> nextStates = new HashSet<>();
for (JsonNode val : result) {
if (!val.isTextual()) {
log.warn("Wrong result type: {}", val.getNodeType());
throw new ScriptException("Wrong result type: " + val.getNodeType());
return Futures.immediateFailedFuture(new ScriptException("Wrong result type: " + val.getNodeType()));
} else {
nextStates.add(val.asText());
}
}
return nextStates;
} else {
log.warn("Wrong result type: {}", result.getNodeType());
throw new ScriptException("Wrong result type: " + result.getNodeType());
return Futures.immediateFuture(nextStates);
}
log.warn("Wrong result type: {}", result.getNodeType());
return Futures.immediateFailedFuture(new ScriptException("Wrong result type: " + result.getNodeType()));
}
private JsonNode executeScript(TbMsg msg) throws ScriptException {
try {
String[] inArgs = prepareArgs(msg);
String eval = sandboxService.invokeFunction(tenantId, msg.getCustomerId(), this.scriptId, inArgs[0], inArgs[1], inArgs[2]).get().toString();
return mapper.readTree(eval);
} catch (ExecutionException e) {
if (e.getCause() instanceof ScriptException) {
throw (ScriptException) e.getCause();
} else if (e.getCause() instanceof RuntimeException) {
throw new ScriptException(e.getCause().getMessage());
} else {
throw new ScriptException(e);
}
} catch (Exception e) {
throw new ScriptException(e);
}
@Override
public ListenableFuture<Set<String>> executeSwitchAsync(TbMsg msg) {
return Futures.transformAsync(executeScriptAsync(msg),
this::executeSwitchTransform,
MoreExecutors.directExecutor()); //usually runs in a callbackExecutor
}
private ListenableFuture<JsonNode> executeScriptAsync(TbMsg msg) {
ListenableFuture<JsonNode> executeScriptAsync(TbMsg msg) {
log.trace("execute script async, msg {}", msg);
String[] inArgs = prepareArgs(msg);
return Futures.transformAsync(sandboxService.invokeFunction(tenantId, msg.getCustomerId(), this.scriptId, inArgs[0], inArgs[1], inArgs[2]),
return executeScriptAsync(msg.getCustomerId(), inArgs[0], inArgs[1], inArgs[2]);
}
ListenableFuture<JsonNode> executeScriptAsync(CustomerId customerId, Object... args) {
return Futures.transformAsync(sandboxService.invokeFunction(tenantId, customerId, this.scriptId, args),
o -> {
try {
return Futures.immediateFuture(mapper.readTree(o.toString()));

25
application/src/main/java/org/thingsboard/server/service/security/AccessValidator.java

@ -47,11 +47,13 @@ import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.EntityIdFactory;
import org.thingsboard.server.common.data.id.EntityViewId;
import org.thingsboard.server.common.data.id.OtaPackageId;
import org.thingsboard.server.common.data.id.RpcId;
import org.thingsboard.server.common.data.id.RuleChainId;
import org.thingsboard.server.common.data.id.RuleNodeId;
import org.thingsboard.server.common.data.id.TbResourceId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.rpc.Rpc;
import org.thingsboard.server.common.data.rule.RuleChain;
import org.thingsboard.server.common.data.rule.RuleNode;
import org.thingsboard.server.controller.HttpValidationCallback;
@ -65,6 +67,7 @@ import org.thingsboard.server.dao.entityview.EntityViewService;
import org.thingsboard.server.dao.exception.IncorrectParameterException;
import org.thingsboard.server.dao.ota.OtaPackageService;
import org.thingsboard.server.dao.resource.ResourceService;
import org.thingsboard.server.dao.rpc.RpcService;
import org.thingsboard.server.dao.rule.RuleChainService;
import org.thingsboard.server.dao.tenant.TenantService;
import org.thingsboard.server.dao.usagerecord.ApiUsageStateService;
@ -137,6 +140,9 @@ public class AccessValidator {
@Autowired
protected OtaPackageService otaPackageService;
@Autowired
protected RpcService rpcService;
private ExecutorService executor;
@PostConstruct
@ -235,6 +241,9 @@ public class AccessValidator {
case OTA_PACKAGE:
validateOtaPackage(currentUser, operation, entityId, callback);
return;
case RPC:
validateRpc(currentUser, operation, entityId, callback);
return;
default:
//TODO: add support of other entities
throw new IllegalStateException("Not Implemented!");
@ -261,6 +270,22 @@ public class AccessValidator {
}
}
private void validateRpc(final SecurityUser currentUser, Operation operation, EntityId entityId, FutureCallback<ValidationResult> callback) {
ListenableFuture<Rpc> rpcFurure = rpcService.findRpcByIdAsync(currentUser.getTenantId(), new RpcId(entityId.getId()));
Futures.addCallback(rpcFurure, getCallback(callback, rpc -> {
if (rpc == null) {
return ValidationResult.entityNotFound("Rpc with requested id wasn't found!");
} else {
try {
accessControlService.checkPermission(currentUser, Resource.RPC, operation, entityId, rpc);
} catch (ThingsboardException e) {
return ValidationResult.accessDenied(e.getMessage());
}
return ValidationResult.ok(rpc);
}
}), executor);
}
private void validateDeviceProfile(final SecurityUser currentUser, Operation operation, EntityId entityId, FutureCallback<ValidationResult> callback) {
if (currentUser.isSystemAdmin()) {
callback.onSuccess(ValidationResult.accessDenied(SYSTEM_ADMINISTRATOR_IS_NOT_ALLOWED_TO_PERFORM_THIS_OPERATION));

19
application/src/main/java/org/thingsboard/server/service/security/permission/CustomerUserPermissions.java

@ -41,6 +41,7 @@ public class CustomerUserPermissions extends AbstractPermissions {
put(Resource.WIDGETS_BUNDLE, widgetsPermissionChecker);
put(Resource.WIDGET_TYPE, widgetsPermissionChecker);
put(Resource.EDGE, customerEntityPermissionChecker);
put(Resource.RPC, rpcPermissionChecker);
}
private static final PermissionChecker customerEntityPermissionChecker =
@ -138,4 +139,22 @@ public class CustomerUserPermissions extends AbstractPermissions {
}
};
private static final PermissionChecker rpcPermissionChecker = new PermissionChecker.GenericPermissionChecker(Operation.READ) {
@Override
@SuppressWarnings("unchecked")
public boolean hasPermission(SecurityUser user, Operation operation, EntityId entityId, HasTenantId entity) {
if (!super.hasPermission(user, operation, entityId, entity)) {
return false;
}
if (entity.getTenantId() == null || entity.getTenantId().isNullUid()) {
return true;
}
if (!user.getTenantId().equals(entity.getTenantId())) {
return false;
}
return true;
}
};
}

3
application/src/main/java/org/thingsboard/server/service/security/permission/Resource.java

@ -39,7 +39,8 @@ public enum Resource {
API_USAGE_STATE(EntityType.API_USAGE_STATE),
TB_RESOURCE(EntityType.TB_RESOURCE),
OTA_PACKAGE(EntityType.OTA_PACKAGE),
EDGE(EntityType.EDGE);
EDGE(EntityType.EDGE),
RPC(EntityType.RPC);
private final EntityType entityType;

1
application/src/main/java/org/thingsboard/server/service/security/permission/TenantAdminPermissions.java

@ -44,6 +44,7 @@ public class TenantAdminPermissions extends AbstractPermissions {
put(Resource.TB_RESOURCE, tbResourcePermissionChecker);
put(Resource.OTA_PACKAGE, tenantEntityPermissionChecker);
put(Resource.EDGE, tenantEntityPermissionChecker);
put(Resource.RPC, tenantEntityPermissionChecker);
}
public static final PermissionChecker tenantEntityPermissionChecker = new PermissionChecker() {

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

@ -22,6 +22,7 @@ import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
import com.google.protobuf.ByteString;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
@ -40,14 +41,16 @@ import org.thingsboard.server.common.data.TbResource;
import org.thingsboard.server.common.data.TenantProfile;
import org.thingsboard.server.common.data.device.credentials.BasicMqttCredentials;
import org.thingsboard.server.common.data.device.credentials.ProvisionDeviceCredentialsData;
import org.thingsboard.server.common.data.device.data.Lwm2mDeviceTransportConfiguration;
import org.thingsboard.server.common.data.device.data.PowerMode;
import org.thingsboard.server.common.data.device.profile.ProvisionDeviceProfileCredentials;
import org.thingsboard.server.common.data.ota.OtaPackageType;
import org.thingsboard.server.common.data.ota.OtaPackageUtil;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.DeviceProfileId;
import org.thingsboard.server.common.data.id.OtaPackageId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.ota.OtaPackageType;
import org.thingsboard.server.common.data.ota.OtaPackageUtil;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.relation.EntityRelation;
@ -108,6 +111,7 @@ import java.util.stream.Collectors;
@Slf4j
@Service
@TbCoreComponent
@RequiredArgsConstructor
public class DefaultTransportApiService implements TransportApiService {
private static final ObjectMapper mapper = new ObjectMapper();
@ -129,28 +133,6 @@ public class DefaultTransportApiService implements TransportApiService {
private final ConcurrentMap<String, ReentrantLock> deviceCreationLocks = new ConcurrentHashMap<>();
public DefaultTransportApiService(TbDeviceProfileCache deviceProfileCache,
TbTenantProfileCache tenantProfileCache, TbApiUsageStateService apiUsageStateService, DeviceService deviceService,
RelationService relationService, DeviceCredentialsService deviceCredentialsService,
DeviceStateService deviceStateService, DbCallbackExecutorService dbCallbackExecutorService,
TbClusterService tbClusterService, DataDecodingEncodingService dataDecodingEncodingService,
DeviceProvisionService deviceProvisionService, TbResourceService resourceService, OtaPackageService otaPackageService, OtaPackageDataCache otaPackageDataCache) {
this.deviceProfileCache = deviceProfileCache;
this.tenantProfileCache = tenantProfileCache;
this.apiUsageStateService = apiUsageStateService;
this.deviceService = deviceService;
this.relationService = relationService;
this.deviceCredentialsService = deviceCredentialsService;
this.deviceStateService = deviceStateService;
this.dbCallbackExecutorService = dbCallbackExecutorService;
this.tbClusterService = tbClusterService;
this.dataDecodingEncodingService = dataDecodingEncodingService;
this.deviceProvisionService = deviceProvisionService;
this.resourceService = resourceService;
this.otaPackageService = otaPackageService;
this.otaPackageDataCache = otaPackageDataCache;
}
@Override
public ListenableFuture<TbProtoQueueMsg<TransportApiResponseMsg>> handle(TbProtoQueueMsg<TransportApiRequestMsg> tbProtoQueueMsg) {
TransportApiRequestMsg transportApiRequestMsg = tbProtoQueueMsg.getValue();
@ -479,7 +461,14 @@ public class DefaultTransportApiService implements TransportApiService {
}
private DeviceInfoProto getDeviceInfoProto(Device device) throws JsonProcessingException {
return DeviceInfoProto.newBuilder()
PowerMode powerMode = null;
switch (device.getDeviceData().getTransportConfiguration().getType()) {
case LWM2M:
powerMode = ((Lwm2mDeviceTransportConfiguration) device.getDeviceData().getTransportConfiguration()).getPowerMode();
break;
}
DeviceInfoProto.Builder builder = DeviceInfoProto.newBuilder()
.setTenantIdMSB(device.getTenantId().getId().getMostSignificantBits())
.setTenantIdLSB(device.getTenantId().getId().getLeastSignificantBits())
.setCustomerIdMSB(Optional.ofNullable(device.getCustomerId()).map(customerId -> customerId.getId().getMostSignificantBits()).orElse(0L))
@ -490,8 +479,11 @@ public class DefaultTransportApiService implements TransportApiService {
.setDeviceType(device.getType())
.setDeviceProfileIdMSB(device.getDeviceProfileId().getId().getMostSignificantBits())
.setDeviceProfileIdLSB(device.getDeviceProfileId().getId().getLeastSignificantBits())
.setAdditionalInfo(mapper.writeValueAsString(device.getAdditionalInfo()))
.build();
.setAdditionalInfo(mapper.writeValueAsString(device.getAdditionalInfo()));
if (powerMode != null) {
builder.setPowerMode(powerMode.name());
}
return builder.build();
}
private ListenableFuture<TransportApiResponseMsg> getEmptyTransportApiResponseFuture() {

44
application/src/main/java/org/thingsboard/server/service/ttl/AbstractCleanUpService.java

@ -15,8 +15,13 @@
*/
package org.thingsboard.server.service.ttl;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.msg.queue.ServiceType;
import org.thingsboard.server.queue.discovery.PartitionService;
import java.sql.Connection;
import java.sql.DriverManager;
@ -27,43 +32,12 @@ import java.sql.Statement;
@Slf4j
@RequiredArgsConstructor
public abstract class AbstractCleanUpService {
@Value("${spring.datasource.url}")
protected String dbUrl;
private final PartitionService partitionService;
@Value("${spring.datasource.username}")
protected String dbUserName;
@Value("${spring.datasource.password}")
protected String dbPassword;
protected long executeQuery(Connection conn, String query) throws SQLException {
try (Statement statement = conn.createStatement(); ResultSet resultSet = statement.executeQuery(query)) {
if (log.isDebugEnabled()) {
getWarnings(statement);
}
resultSet.next();
return resultSet.getLong(1);
}
}
protected void getWarnings(Statement statement) throws SQLException {
SQLWarning warnings = statement.getWarnings();
if (warnings != null) {
log.debug("{}", warnings.getMessage());
SQLWarning nextWarning = warnings.getNextWarning();
while (nextWarning != null) {
log.debug("{}", nextWarning.getMessage());
nextWarning = nextWarning.getNextWarning();
}
}
protected boolean isSystemTenantPartitionMine(){
return partitionService.resolve(ServiceType.TB_CORE, TenantId.SYS_TENANT_ID, TenantId.SYS_TENANT_ID).isMyPartition();
}
protected abstract void doCleanUp(Connection connection) throws SQLException;
protected Connection getConnection() throws SQLException {
return DriverManager.getConnection(dbUrl, dbUserName, dbPassword);
}
}

3
application/src/main/java/org/thingsboard/server/service/ttl/alarms/AlarmsCleanUpService.java → application/src/main/java/org/thingsboard/server/service/ttl/AlarmsCleanUpService.java

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.ttl.alarms;
package org.thingsboard.server.service.ttl;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@ -52,6 +52,7 @@ import java.util.concurrent.TimeUnit;
@Slf4j
@RequiredArgsConstructor
public class AlarmsCleanUpService {
@Value("${sql.ttl.alarms.removal_batch_size}")
private Integer removalBatchSize;

32
application/src/main/java/org/thingsboard/server/service/ttl/edge/EdgeEventsCleanUpService.java → application/src/main/java/org/thingsboard/server/service/ttl/EdgeEventsCleanUpService.java

@ -13,20 +13,18 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.ttl.edge;
package org.thingsboard.server.service.ttl;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import org.thingsboard.server.dao.util.PsqlDao;
import org.thingsboard.server.dao.edge.EdgeService;
import org.thingsboard.server.queue.discovery.PartitionService;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.ttl.AbstractCleanUpService;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
@PsqlDao
@TbCoreComponent
@Slf4j
@Service
public class EdgeEventsCleanUpService extends AbstractCleanUpService {
@ -37,20 +35,18 @@ public class EdgeEventsCleanUpService extends AbstractCleanUpService {
@Value("${sql.ttl.edge_events.enabled}")
private boolean ttlTaskExecutionEnabled;
private final EdgeService edgeService;
public EdgeEventsCleanUpService(PartitionService partitionService, EdgeService edgeService) {
super(partitionService);
this.edgeService = edgeService;
}
@Scheduled(initialDelayString = "${sql.ttl.edge_events.execution_interval_ms}", fixedDelayString = "${sql.ttl.edge_events.execution_interval_ms}")
public void cleanUp() {
if (ttlTaskExecutionEnabled) {
try (Connection conn = getConnection()) {
doCleanUp(conn);
} catch (SQLException e) {
log.error("SQLException occurred during TTL task execution ", e);
}
if (ttlTaskExecutionEnabled && isSystemTenantPartitionMine()) {
edgeService.cleanupEvents(ttl);
}
}
@Override
protected void doCleanUp(Connection connection) throws SQLException {
long totalEdgeEventsRemoved = executeQuery(connection, "call cleanup_edge_events_by_ttl(" + ttl + ", 0);");
log.info("Total edge events removed by TTL: [{}]", totalEdgeEventsRemoved);
}
}

32
application/src/main/java/org/thingsboard/server/service/ttl/events/EventsCleanUpService.java → application/src/main/java/org/thingsboard/server/service/ttl/EventsCleanUpService.java

@ -13,20 +13,18 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.ttl.events;
package org.thingsboard.server.service.ttl;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import org.thingsboard.server.dao.util.PsqlDao;
import org.thingsboard.server.dao.event.EventService;
import org.thingsboard.server.queue.discovery.PartitionService;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.ttl.AbstractCleanUpService;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
@PsqlDao
@TbCoreComponent
@Slf4j
@Service
public class EventsCleanUpService extends AbstractCleanUpService {
@ -40,20 +38,18 @@ public class EventsCleanUpService extends AbstractCleanUpService {
@Value("${sql.ttl.events.enabled}")
private boolean ttlTaskExecutionEnabled;
private final EventService eventService;
public EventsCleanUpService(PartitionService partitionService, EventService eventService) {
super(partitionService);
this.eventService = eventService;
}
@Scheduled(initialDelayString = "${sql.ttl.events.execution_interval_ms}", fixedDelayString = "${sql.ttl.events.execution_interval_ms}")
public void cleanUp() {
if (ttlTaskExecutionEnabled) {
try (Connection conn = getConnection()) {
doCleanUp(conn);
} catch (SQLException e) {
log.error("SQLException occurred during TTL task execution ", e);
}
if (ttlTaskExecutionEnabled && isSystemTenantPartitionMine()) {
eventService.cleanupEvents(ttl, debugTtl);
}
}
@Override
protected void doCleanUp(Connection connection) throws SQLException {
long totalEventsRemoved = executeQuery(connection, "call cleanup_events_by_ttl(" + ttl + ", " + debugTtl + ", 0);");
log.info("Total events removed by TTL: [{}]", totalEventsRemoved);
}
}

29
application/src/main/java/org/thingsboard/server/service/ttl/timeseries/AbstractTimeseriesCleanUpService.java → application/src/main/java/org/thingsboard/server/service/ttl/TimeseriesCleanUpService.java

@ -13,19 +13,21 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.ttl.timeseries;
package org.thingsboard.server.service.ttl;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import org.thingsboard.server.dao.timeseries.TimeseriesService;
import org.thingsboard.server.queue.discovery.PartitionService;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.ttl.AbstractCleanUpService;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
@TbCoreComponent
@Slf4j
public abstract class AbstractTimeseriesCleanUpService extends AbstractCleanUpService {
@Service
public class TimeseriesCleanUpService extends AbstractCleanUpService {
@Value("${sql.ttl.ts.ts_key_value_ttl}")
protected long systemTtl;
@ -33,14 +35,17 @@ public abstract class AbstractTimeseriesCleanUpService extends AbstractCleanUpSe
@Value("${sql.ttl.ts.enabled}")
private boolean ttlTaskExecutionEnabled;
private final TimeseriesService timeseriesService;
public TimeseriesCleanUpService(PartitionService partitionService, TimeseriesService timeseriesService) {
super(partitionService);
this.timeseriesService = timeseriesService;
}
@Scheduled(initialDelayString = "${sql.ttl.ts.execution_interval_ms}", fixedDelayString = "${sql.ttl.ts.execution_interval_ms}")
public void cleanUp() {
if (ttlTaskExecutionEnabled) {
try (Connection conn = getConnection()) {
doCleanUp(conn);
} catch (SQLException e) {
log.error("SQLException occurred during TTL task execution ", e);
}
if (ttlTaskExecutionEnabled && isSystemTenantPartitionMine()) {
timeseriesService.cleanup(systemTtl);
}
}

83
application/src/main/java/org/thingsboard/server/service/ttl/rpc/RpcCleanUpService.java

@ -0,0 +1,83 @@
/**
* 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.rpc;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration;
import org.thingsboard.server.common.msg.queue.ServiceType;
import org.thingsboard.server.dao.rpc.RpcDao;
import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
import org.thingsboard.server.dao.tenant.TenantDao;
import org.thingsboard.server.queue.discovery.PartitionService;
import org.thingsboard.server.queue.util.TbCoreComponent;
import java.util.Date;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
@TbCoreComponent
@Service
@Slf4j
@RequiredArgsConstructor
public class RpcCleanUpService {
@Value("${sql.ttl.rpc.enabled}")
private boolean ttlTaskExecutionEnabled;
private final TenantDao tenantDao;
private final PartitionService partitionService;
private final TbTenantProfileCache tenantProfileCache;
private final RpcDao rpcDao;
@Scheduled(initialDelayString = "#{T(org.apache.commons.lang3.RandomUtils).nextLong(0, ${sql.ttl.rpc.checking_interval})}", fixedDelayString = "${sql.ttl.rpc.checking_interval}")
public void cleanUp() {
if (ttlTaskExecutionEnabled) {
PageLink tenantsBatchRequest = new PageLink(10_000, 0);
PageData<TenantId> tenantsIds;
do {
tenantsIds = tenantDao.findTenantsIds(tenantsBatchRequest);
for (TenantId tenantId : tenantsIds.getData()) {
if (!partitionService.resolve(ServiceType.TB_CORE, tenantId, tenantId).isMyPartition()) {
continue;
}
Optional<DefaultTenantProfileConfiguration> tenantProfileConfiguration = tenantProfileCache.get(tenantId).getProfileConfiguration();
if (tenantProfileConfiguration.isEmpty() || tenantProfileConfiguration.get().getRpcTtlDays() == 0) {
continue;
}
long ttl = TimeUnit.DAYS.toMillis(tenantProfileConfiguration.get().getRpcTtlDays());
long expirationTime = System.currentTimeMillis() - ttl;
long totalRemoved = rpcDao.deleteOutdatedRpcByTenantId(tenantId, expirationTime);
if (totalRemoved > 0) {
log.info("Removed {} outdated rpc(s) for tenant {} older than {}", totalRemoved, tenantId, new Date(expirationTime));
}
}
tenantsBatchRequest = tenantsBatchRequest.nextPageLink();
} while (tenantsIds.hasNext());
}
}
}

44
application/src/main/java/org/thingsboard/server/service/ttl/timeseries/PsqlTimeseriesCleanUpService.java

@ -1,44 +0,0 @@
/**
* 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.timeseries;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.thingsboard.server.dao.model.ModelConstants;
import org.thingsboard.server.dao.util.PsqlDao;
import org.thingsboard.server.dao.util.SqlTsDao;
import java.sql.Connection;
import java.sql.SQLException;
@SqlTsDao
@PsqlDao
@Service
@Slf4j
public class PsqlTimeseriesCleanUpService extends AbstractTimeseriesCleanUpService {
@Value("${sql.postgres.ts_key_value_partitioning}")
private String partitionType;
@Override
protected void doCleanUp(Connection connection) throws SQLException {
long totalPartitionsRemoved = executeQuery(connection, "call drop_partitions_by_max_ttl('" + partitionType + "'," + systemTtl + ", 0);");
log.info("Total partitions removed by TTL: [{}]", totalPartitionsRemoved);
long totalEntitiesTelemetryRemoved = executeQuery(connection, "call cleanup_timeseries_by_ttl('" + ModelConstants.NULL_UUID + "'," + systemTtl + ", 0);");
log.info("Total telemetry removed stats by TTL for entities: [{}]", totalEntitiesTelemetryRemoved);
}
}

36
application/src/main/java/org/thingsboard/server/service/ttl/timeseries/TimescaleTimeseriesCleanUpService.java

@ -1,36 +0,0 @@
/**
* 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.timeseries;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.thingsboard.server.dao.model.ModelConstants;
import org.thingsboard.server.dao.util.TimescaleDBTsDao;
import java.sql.Connection;
import java.sql.SQLException;
@TimescaleDBTsDao
@Service
@Slf4j
public class TimescaleTimeseriesCleanUpService extends AbstractTimeseriesCleanUpService {
@Override
protected void doCleanUp(Connection connection) throws SQLException {
long totalEntitiesTelemetryRemoved = executeQuery(connection, "call cleanup_timeseries_by_ttl('" + ModelConstants.NULL_UUID + "'," + systemTtl + ", 0);");
log.info("Total telemetry removed stats by TTL for entities: [{}]", totalEntitiesTelemetryRemoved);
}
}

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

@ -276,6 +276,9 @@ sql:
alarms:
checking_interval: "${SQL_ALARMS_TTL_CHECKING_INTERVAL:7200000}" # Number of milliseconds. The current value corresponds to two hours
removal_batch_size: "${SQL_ALARMS_TTL_REMOVAL_BATCH_SIZE:3000}" # To delete outdated alarms not all at once but in batches
rpc:
enabled: "${SQL_TTL_RPC_ENABLED:true}"
checking_interval: "${SQL_RPC_TTL_CHECKING_INTERVAL:7200000}" # Number of milliseconds. The current value corresponds to two hours
# Actor system parameters
actors:
@ -678,12 +681,11 @@ transport:
timeout: "${LWM2M_TIMEOUT:120000}"
recommended_ciphers: "${LWM2M_RECOMMENDED_CIPHERS:false}"
recommended_supported_groups: "${LWM2M_RECOMMENDED_SUPPORTED_GROUPS:true}"
response_pool_size: "${LWM2M_RESPONSE_POOL_SIZE:100}"
registered_pool_size: "${LWM2M_REGISTERED_POOL_SIZE:10}"
uplink_pool_size: "${LWM2M_UPLINK_POOL_SIZE:10}"
downlink_pool_size: "${LWM2M_DOWNLINK_POOL_SIZE:10}"
ota_pool_size: "${LWM2M_OTA_POOL_SIZE:10}"
registration_store_pool_size: "${LWM2M_REGISTRATION_STORE_POOL_SIZE:100}"
clean_period_in_sec: "${LWM2M_CLEAN_PERIOD_IN_SEC:2}"
update_registered_pool_size: "${LWM2M_UPDATE_REGISTERED_POOL_SIZE:10}"
un_registered_pool_size: "${LWM2M_UN_REGISTERED_POOL_SIZE:10}"
log_max_length: "${LWM2M_LOG_MAX_LENGTH:100}"
# Use redis for Security and Registration stores
redis.enabled: "${LWM2M_REDIS_ENABLED:false}"

127
application/src/test/java/org/thingsboard/server/transport/lwm2m/AbstractLwM2MIntegrationTest.java

@ -17,24 +17,47 @@ package org.thingsboard.server.transport.lwm2m;
import com.fasterxml.jackson.core.type.TypeReference;
import org.apache.commons.io.IOUtils;
import org.eclipse.californium.core.network.config.NetworkConfig;
import org.eclipse.leshan.client.object.Security;
import org.eclipse.leshan.core.util.Hex;
import org.jetbrains.annotations.NotNull;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.test.web.servlet.request.MockMultipartHttpServletRequestBuilder;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.DeviceProfileProvisionType;
import org.thingsboard.server.common.data.DeviceProfileType;
import org.thingsboard.server.common.data.DeviceTransportType;
import org.thingsboard.server.common.data.OtaPackageInfo;
import org.thingsboard.server.common.data.ResourceType;
import org.thingsboard.server.common.data.TbResource;
import org.thingsboard.server.common.data.device.credentials.lwm2m.LwM2MClientCredentials;
import org.thingsboard.server.common.data.device.profile.DefaultDeviceProfileConfiguration;
import org.thingsboard.server.common.data.device.profile.DeviceProfileData;
import org.thingsboard.server.common.data.device.profile.DisabledDeviceProfileProvisionConfiguration;
import org.thingsboard.server.common.data.device.profile.Lwm2mDeviceProfileTransportConfiguration;
import org.thingsboard.server.common.data.query.EntityData;
import org.thingsboard.server.common.data.query.EntityDataPageLink;
import org.thingsboard.server.common.data.query.EntityDataQuery;
import org.thingsboard.server.common.data.query.EntityKey;
import org.thingsboard.server.common.data.query.EntityKeyType;
import org.thingsboard.server.common.data.query.SingleEntityFilter;
import org.thingsboard.server.common.data.security.DeviceCredentials;
import org.thingsboard.server.common.data.security.DeviceCredentialsType;
import org.thingsboard.server.controller.AbstractWebsocketTest;
import org.thingsboard.server.controller.TbTestWebSocketClient;
import org.thingsboard.server.dao.service.DaoSqlTest;
import org.thingsboard.server.service.telemetry.cmd.TelemetryPluginCmdsWrapper;
import org.thingsboard.server.service.telemetry.cmd.v2.EntityDataCmd;
import org.thingsboard.server.service.telemetry.cmd.v2.EntityDataUpdate;
import org.thingsboard.server.service.telemetry.cmd.v2.LatestValueCmd;
import org.thingsboard.server.transport.lwm2m.client.LwM2MTestClient;
import org.thingsboard.server.transport.lwm2m.secure.credentials.LwM2MCredentials;
import java.io.IOException;
import java.io.InputStream;
@ -54,9 +77,15 @@ import java.security.spec.ECPrivateKeySpec;
import java.security.spec.ECPublicKeySpec;
import java.security.spec.KeySpec;
import java.util.Base64;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import static org.eclipse.leshan.client.object.Security.noSec;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.thingsboard.server.common.data.ota.OtaPackageType.FIRMWARE;
@DaoSqlTest
public class AbstractLwM2MIntegrationTest extends AbstractWebsocketTest {
@ -103,7 +132,9 @@ public class AbstractLwM2MIntegrationTest extends AbstractWebsocketTest {
" }\n" +
" },\n" +
" \"clientLwM2mSettings\": {\n" +
" \"clientOnlyObserveAfterConnect\": 1\n" +
" \"clientOnlyObserveAfterConnect\": 1,\n" +
" \"fwUpdateStrategy\": 1,\n" +
" \"swUpdateStrategy\": 1\n" +
" }\n" +
"}";
@ -137,6 +168,15 @@ public class AbstractLwM2MIntegrationTest extends AbstractWebsocketTest {
// certificates trustedby the server (should contain rootCA)
protected final Certificate[] trustedCertificates = new Certificate[1];
protected static final int SECURE_PORT = 5686;
protected static final NetworkConfig SECURE_COAP_CONFIG = new NetworkConfig().setString("COAP_SECURE_PORT", Integer.toString(SECURE_PORT));
protected static final String ENDPOINT = "deviceAEndpoint";
protected static final String SECURE_URI = "coaps://localhost:" + SECURE_PORT;
protected static final int PORT = 5685;
protected static final Security SECURITY = noSec("coap://localhost:" + PORT, 123);
protected static final NetworkConfig COAP_CONFIG = new NetworkConfig().setString("COAP_PORT", Integer.toString(PORT));
public AbstractLwM2MIntegrationTest() {
// create client credentials
try {
@ -260,10 +300,95 @@ public class AbstractLwM2MIntegrationTest extends AbstractWebsocketTest {
Assert.assertNotNull(deviceProfile);
}
@NotNull
protected Device createDevice(LwM2MClientCredentials clientCredentials) throws Exception {
Device device = new Device();
device.setName("Device A");
device.setDeviceProfileId(deviceProfile.getId());
device.setTenantId(tenantId);
device = doPost("/api/device", device, Device.class);
Assert.assertNotNull(device);
DeviceCredentials deviceCredentials =
doGet("/api/device/" + device.getId().getId().toString() + "/credentials", DeviceCredentials.class);
Assert.assertEquals(device.getId(), deviceCredentials.getDeviceId());
deviceCredentials.setCredentialsType(DeviceCredentialsType.LWM2M_CREDENTIALS);
LwM2MCredentials credentials = new LwM2MCredentials();
credentials.setClient(clientCredentials);
deviceCredentials.setCredentialsValue(JacksonUtil.toString(credentials));
doPost("/api/device/credentials", deviceCredentials).andExpect(status().isOk());
return device;
}
protected OtaPackageInfo createFirmware() throws Exception {
String CHECKSUM = "4bf5122f344554c53bde2ebb8cd2b7e3d1600ad631c385a5d7cce23c7785459a";
OtaPackageInfo firmwareInfo = new OtaPackageInfo();
firmwareInfo.setDeviceProfileId(deviceProfile.getId());
firmwareInfo.setType(FIRMWARE);
firmwareInfo.setTitle("My firmware");
firmwareInfo.setVersion("v1.0");
OtaPackageInfo savedFirmwareInfo = doPost("/api/otaPackage", firmwareInfo, OtaPackageInfo.class);
MockMultipartFile testData = new MockMultipartFile("file", "filename.txt", "text/plain", new byte[]{1});
return savaData("/api/otaPackage/" + savedFirmwareInfo.getId().getId().toString() + "?checksum={checksum}&checksumAlgorithm={checksumAlgorithm}", testData, CHECKSUM, "SHA256");
}
protected OtaPackageInfo savaData(String urlTemplate, MockMultipartFile content, String... params) throws Exception {
MockMultipartHttpServletRequestBuilder postRequest = MockMvcRequestBuilders.multipart(urlTemplate, params);
postRequest.file(content);
setJwtToken(postRequest);
return readResponse(mockMvc.perform(postRequest).andExpect(status().isOk()), OtaPackageInfo.class);
}
@After
public void after() {
executor.shutdownNow();
wsClient.close();
}
public void basicTestConnectionObserveTelemetry(Security security,
LwM2MClientCredentials credentials,
NetworkConfig coapConfig,
String endpoint) throws Exception {
createDeviceProfile(TRANSPORT_CONFIGURATION);
Device device = createDevice(credentials);
SingleEntityFilter sef = new SingleEntityFilter();
sef.setSingleEntity(device.getId());
LatestValueCmd latestCmd = new LatestValueCmd();
latestCmd.setKeys(Collections.singletonList(new EntityKey(EntityKeyType.TIME_SERIES, "batteryLevel")));
EntityDataQuery edq = new EntityDataQuery(sef, new EntityDataPageLink(1, 0, null, null),
Collections.emptyList(), Collections.emptyList(), Collections.emptyList());
EntityDataCmd cmd = new EntityDataCmd(1, edq, null, latestCmd, null);
TelemetryPluginCmdsWrapper wrapper = new TelemetryPluginCmdsWrapper();
wrapper.setEntityDataCmds(Collections.singletonList(cmd));
wsClient.send(mapper.writeValueAsString(wrapper));
wsClient.waitForReply();
wsClient.registerWaitForUpdate();
LwM2MTestClient client = new LwM2MTestClient(executor, endpoint);
client.init(security, coapConfig);
String msg = wsClient.waitForUpdate();
EntityDataUpdate update = mapper.readValue(msg, EntityDataUpdate.class);
Assert.assertEquals(1, update.getCmdId());
List<EntityData> eData = update.getUpdate();
Assert.assertNotNull(eData);
Assert.assertEquals(1, eData.size());
Assert.assertEquals(device.getId(), eData.get(0).getEntityId());
Assert.assertNotNull(eData.get(0).getLatest().get(EntityKeyType.TIME_SERIES));
var tsValue = eData.get(0).getLatest().get(EntityKeyType.TIME_SERIES).get("batteryLevel");
Assert.assertEquals(42, Long.parseLong(tsValue.getValue()));
client.destroy();
}
}

65
application/src/test/java/org/thingsboard/server/transport/lwm2m/NoSecLwM2MIntegrationTest.java

@ -15,13 +15,10 @@
*/
package org.thingsboard.server.transport.lwm2m;
import org.eclipse.californium.core.network.config.NetworkConfig;
import org.eclipse.leshan.client.object.Security;
import org.jetbrains.annotations.NotNull;
import org.junit.Assert;
import org.junit.Test;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.OtaPackageInfo;
import org.thingsboard.server.common.data.device.credentials.lwm2m.NoSecClientCredentials;
import org.thingsboard.server.common.data.query.EntityData;
import org.thingsboard.server.common.data.query.EntityDataPageLink;
@ -29,61 +26,48 @@ import org.thingsboard.server.common.data.query.EntityDataQuery;
import org.thingsboard.server.common.data.query.EntityKey;
import org.thingsboard.server.common.data.query.EntityKeyType;
import org.thingsboard.server.common.data.query.SingleEntityFilter;
import org.thingsboard.server.common.data.security.DeviceCredentials;
import org.thingsboard.server.common.data.security.DeviceCredentialsType;
import org.thingsboard.server.service.telemetry.cmd.TelemetryPluginCmdsWrapper;
import org.thingsboard.server.service.telemetry.cmd.v2.EntityDataCmd;
import org.thingsboard.server.service.telemetry.cmd.v2.EntityDataUpdate;
import org.thingsboard.server.service.telemetry.cmd.v2.LatestValueCmd;
import org.thingsboard.server.transport.lwm2m.client.LwM2MTestClient;
import org.thingsboard.server.transport.lwm2m.secure.credentials.LwM2MCredentials;
import java.util.Collections;
import java.util.List;
import static org.eclipse.leshan.client.object.Security.noSec;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
public class NoSecLwM2MIntegrationTest extends AbstractLwM2MIntegrationTest {
private final int PORT = 5685;
private final Security SECURITY = noSec("coap://localhost:" + PORT, 123);
private final NetworkConfig COAP_CONFIG = new NetworkConfig().setString("COAP_PORT", Integer.toString(PORT));
private final String ENDPOINT = "deviceAEndpoint";
@NotNull
private Device createDevice() throws Exception {
Device device = new Device();
device.setName("Device A");
device.setDeviceProfileId(deviceProfile.getId());
device.setTenantId(tenantId);
device = doPost("/api/device", device, Device.class);
Assert.assertNotNull(device);
DeviceCredentials deviceCredentials =
doGet("/api/device/" + device.getId().getId().toString() + "/credentials", DeviceCredentials.class);
Assert.assertEquals(device.getId(), deviceCredentials.getDeviceId());
deviceCredentials.setCredentialsType(DeviceCredentialsType.LWM2M_CREDENTIALS);
LwM2MCredentials noSecCredentials = new LwM2MCredentials();
@Test
public void testConnectAndObserveTelemetry() throws Exception {
NoSecClientCredentials clientCredentials = new NoSecClientCredentials();
clientCredentials.setEndpoint(ENDPOINT);
noSecCredentials.setClient(clientCredentials);
deviceCredentials.setCredentialsValue(JacksonUtil.toString(noSecCredentials));
doPost("/api/device/credentials", deviceCredentials).andExpect(status().isOk());
return device;
super.basicTestConnectionObserveTelemetry(SECURITY, clientCredentials, COAP_CONFIG, ENDPOINT);
}
@Test
public void testConnectAndObserveTelemetry() throws Exception {
public void testFirmwareUpdateWithClientWithoutFirmwareInfo() throws Exception {
createDeviceProfile(TRANSPORT_CONFIGURATION);
NoSecClientCredentials clientCredentials = new NoSecClientCredentials();
clientCredentials.setEndpoint(ENDPOINT);
Device device = createDevice(clientCredentials);
OtaPackageInfo firmware = createFirmware();
Device device = createDevice();
LwM2MTestClient client = new LwM2MTestClient(executor, ENDPOINT);
client.init(SECURITY, COAP_CONFIG);
Thread.sleep(1000);
device.setFirmwareId(firmware.getId());
device = doPost("/api/device", device, Device.class);
Thread.sleep(1000);
SingleEntityFilter sef = new SingleEntityFilter();
sef.setSingleEntity(device.getId());
LatestValueCmd latestCmd = new LatestValueCmd();
latestCmd.setKeys(Collections.singletonList(new EntityKey(EntityKeyType.TIME_SERIES, "batteryLevel")));
latestCmd.setKeys(Collections.singletonList(new EntityKey(EntityKeyType.TIME_SERIES, "fw_state")));
EntityDataQuery edq = new EntityDataQuery(sef, new EntityDataPageLink(1, 0, null, null),
Collections.emptyList(), Collections.emptyList(), Collections.emptyList());
@ -95,8 +79,7 @@ public class NoSecLwM2MIntegrationTest extends AbstractLwM2MIntegrationTest {
wsClient.waitForReply();
wsClient.registerWaitForUpdate();
LwM2MTestClient client = new LwM2MTestClient(executor, ENDPOINT);
client.init(SECURITY, COAP_CONFIG);
String msg = wsClient.waitForUpdate();
EntityDataUpdate update = mapper.readValue(msg, EntityDataUpdate.class);
@ -106,8 +89,8 @@ public class NoSecLwM2MIntegrationTest extends AbstractLwM2MIntegrationTest {
Assert.assertEquals(1, eData.size());
Assert.assertEquals(device.getId(), eData.get(0).getEntityId());
Assert.assertNotNull(eData.get(0).getLatest().get(EntityKeyType.TIME_SERIES));
var tsValue = eData.get(0).getLatest().get(EntityKeyType.TIME_SERIES).get("batteryLevel");
Assert.assertEquals(42, Long.parseLong(tsValue.getValue()));
var tsValue = eData.get(0).getLatest().get(EntityKeyType.TIME_SERIES).get("fw_state");
Assert.assertEquals("FAILED", tsValue.getValue());
client.destroy();
}

43
application/src/test/java/org/thingsboard/server/transport/lwm2m/PskLwm2mIntegrationTest.java

@ -0,0 +1,43 @@
/**
* 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.lwm2m;
import org.eclipse.leshan.client.object.Security;
import org.eclipse.leshan.core.util.Hex;
import org.junit.Test;
import org.thingsboard.server.common.data.device.credentials.lwm2m.PSKClientCredentials;
import java.nio.charset.StandardCharsets;
import static org.eclipse.leshan.client.object.Security.psk;
public class PskLwm2mIntegrationTest extends AbstractLwM2MIntegrationTest {
@Test
public void testConnectWithPSKAndObserveTelemetry() throws Exception {
String pskIdentity = "SOME_PSK_ID";
String pskKey = "73656372657450534b";
PSKClientCredentials clientCredentials = new PSKClientCredentials();
clientCredentials.setEndpoint(ENDPOINT);
clientCredentials.setKey(pskKey);
clientCredentials.setIdentity(pskIdentity);
Security security = psk(SECURE_URI,
123,
pskIdentity.getBytes(StandardCharsets.UTF_8),
Hex.decodeHex(pskKey.toCharArray()));
super.basicTestConnectionObserveTelemetry(security, clientCredentials, SECURE_COAP_CONFIG, ENDPOINT);
}
}

39
application/src/test/java/org/thingsboard/server/transport/lwm2m/RpkLwM2MIntegrationTest.java

@ -0,0 +1,39 @@
/**
* 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.lwm2m;
import org.eclipse.leshan.client.object.Security;
import org.eclipse.leshan.core.util.Hex;
import org.junit.Test;
import org.thingsboard.server.common.data.device.credentials.lwm2m.RPKClientCredentials;
import static org.eclipse.leshan.client.object.Security.rpk;
public class RpkLwM2MIntegrationTest extends AbstractLwM2MIntegrationTest {
@Test
public void testConnectWithRPKAndObserveTelemetry() throws Exception {
RPKClientCredentials rpkClientCredentials = new RPKClientCredentials();
rpkClientCredentials.setEndpoint(ENDPOINT);
rpkClientCredentials.setKey(Hex.encodeHexString(clientPublicKey.getEncoded()));
Security security = rpk(SECURE_URI,
123,
clientPublicKey.getEncoded(),
clientPrivateKey.getEncoded(),
serverX509Cert.getPublicKey().getEncoded());
super.basicTestConnectionObserveTelemetry(security, rpkClientCredentials, SECURE_COAP_CONFIG, ENDPOINT);
}
}

137
application/src/test/java/org/thingsboard/server/transport/lwm2m/X509LwM2MIntegrationTest.java

@ -15,145 +15,40 @@
*/
package org.thingsboard.server.transport.lwm2m;
import org.eclipse.californium.core.network.config.NetworkConfig;
import org.eclipse.leshan.client.object.Security;
import org.jetbrains.annotations.NotNull;
import org.junit.Assert;
import org.junit.Ignore;
import org.junit.Test;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.device.credentials.lwm2m.X509ClientCredentials;
import org.thingsboard.server.common.data.query.EntityData;
import org.thingsboard.server.common.data.query.EntityDataPageLink;
import org.thingsboard.server.common.data.query.EntityDataQuery;
import org.thingsboard.server.common.data.query.EntityKey;
import org.thingsboard.server.common.data.query.EntityKeyType;
import org.thingsboard.server.common.data.query.SingleEntityFilter;
import org.thingsboard.server.common.data.security.DeviceCredentials;
import org.thingsboard.server.common.data.security.DeviceCredentialsType;
import org.thingsboard.server.common.transport.util.SslUtil;
import org.thingsboard.server.service.telemetry.cmd.TelemetryPluginCmdsWrapper;
import org.thingsboard.server.service.telemetry.cmd.v2.EntityDataCmd;
import org.thingsboard.server.service.telemetry.cmd.v2.EntityDataUpdate;
import org.thingsboard.server.service.telemetry.cmd.v2.LatestValueCmd;
import org.thingsboard.server.transport.lwm2m.client.LwM2MTestClient;
import org.thingsboard.server.transport.lwm2m.secure.credentials.LwM2MCredentials;
import java.util.Collections;
import java.util.List;
import static org.eclipse.leshan.client.object.Security.x509;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
public class X509LwM2MIntegrationTest extends AbstractLwM2MIntegrationTest {
private final int port = 5686;
private final NetworkConfig coapConfig = new NetworkConfig().setString("COAP_SECURE_PORT", Integer.toString(port));
private final String endpoint = "deviceAEndpoint";
private final String serverUri = "coaps://localhost:" + port;
@NotNull
private Device createDevice(X509ClientCredentials clientCredentials) throws Exception {
Device device = new Device();
device.setName("Device A");
device.setDeviceProfileId(deviceProfile.getId());
device.setTenantId(tenantId);
device = doPost("/api/device", device, Device.class);
Assert.assertNotNull(device);
DeviceCredentials deviceCredentials =
doGet("/api/device/" + device.getId().getId().toString() + "/credentials", DeviceCredentials.class);
Assert.assertEquals(device.getId(), deviceCredentials.getDeviceId());
deviceCredentials.setCredentialsType(DeviceCredentialsType.LWM2M_CREDENTIALS);
LwM2MCredentials credentials = new LwM2MCredentials();
credentials.setClient(clientCredentials);
deviceCredentials.setCredentialsValue(JacksonUtil.toString(credentials));
doPost("/api/device/credentials", deviceCredentials).andExpect(status().isOk());
return device;
}
@Test
public void testConnectAndObserveTelemetry() throws Exception {
createDeviceProfile(TRANSPORT_CONFIGURATION);
X509ClientCredentials credentials = new X509ClientCredentials();
credentials.setEndpoint(endpoint);
Device device = createDevice(credentials);
SingleEntityFilter sef = new SingleEntityFilter();
sef.setSingleEntity(device.getId());
LatestValueCmd latestCmd = new LatestValueCmd();
latestCmd.setKeys(Collections.singletonList(new EntityKey(EntityKeyType.TIME_SERIES, "batteryLevel")));
EntityDataQuery edq = new EntityDataQuery(sef, new EntityDataPageLink(1, 0, null, null),
Collections.emptyList(), Collections.emptyList(), Collections.emptyList());
EntityDataCmd cmd = new EntityDataCmd(1, edq, null, latestCmd, null);
TelemetryPluginCmdsWrapper wrapper = new TelemetryPluginCmdsWrapper();
wrapper.setEntityDataCmds(Collections.singletonList(cmd));
wsClient.send(mapper.writeValueAsString(wrapper));
wsClient.waitForReply();
wsClient.registerWaitForUpdate();
LwM2MTestClient client = new LwM2MTestClient(executor, endpoint);
Security security = x509(serverUri, 123, clientX509Cert.getEncoded(), clientPrivateKeyFromCert.getEncoded(), serverX509Cert.getEncoded());
client.init(security, coapConfig);
String msg = wsClient.waitForUpdate();
EntityDataUpdate update = mapper.readValue(msg, EntityDataUpdate.class);
Assert.assertEquals(1, update.getCmdId());
List<EntityData> eData = update.getUpdate();
Assert.assertNotNull(eData);
Assert.assertEquals(1, eData.size());
Assert.assertEquals(device.getId(), eData.get(0).getEntityId());
Assert.assertNotNull(eData.get(0).getLatest().get(EntityKeyType.TIME_SERIES));
var tsValue = eData.get(0).getLatest().get(EntityKeyType.TIME_SERIES).get("batteryLevel");
Assert.assertEquals(42, Long.parseLong(tsValue.getValue()));
client.destroy();
credentials.setEndpoint(ENDPOINT);
Security security = x509(SECURE_URI,
123,
clientX509Cert.getEncoded(),
clientPrivateKeyFromCert.getEncoded(),
serverX509Cert.getEncoded());
super.basicTestConnectionObserveTelemetry(security, credentials, SECURE_COAP_CONFIG, ENDPOINT);
}
@Ignore //See LwM2mClientContextImpl.unregister
@Test
public void testConnectWithCertAndObserveTelemetry() throws Exception {
createDeviceProfile(TRANSPORT_CONFIGURATION);
X509ClientCredentials credentials = new X509ClientCredentials();
credentials.setEndpoint(endpoint);
credentials.setEndpoint(ENDPOINT);
credentials.setCert(SslUtil.getCertificateString(clientX509CertNotTrusted));
Device device = createDevice(credentials);
SingleEntityFilter sef = new SingleEntityFilter();
sef.setSingleEntity(device.getId());
LatestValueCmd latestCmd = new LatestValueCmd();
latestCmd.setKeys(Collections.singletonList(new EntityKey(EntityKeyType.TIME_SERIES, "batteryLevel")));
EntityDataQuery edq = new EntityDataQuery(sef, new EntityDataPageLink(1, 0, null, null),
Collections.emptyList(), Collections.emptyList(), Collections.emptyList());
EntityDataCmd cmd = new EntityDataCmd(1, edq, null, latestCmd, null);
TelemetryPluginCmdsWrapper wrapper = new TelemetryPluginCmdsWrapper();
wrapper.setEntityDataCmds(Collections.singletonList(cmd));
wsClient.send(mapper.writeValueAsString(wrapper));
wsClient.waitForReply();
wsClient.registerWaitForUpdate();
LwM2MTestClient client = new LwM2MTestClient(executor, endpoint);
Security security = x509(serverUri, 123, clientX509CertNotTrusted.getEncoded(), clientPrivateKeyFromCert.getEncoded(), serverX509Cert.getEncoded());
client.init(security, coapConfig);
String msg = wsClient.waitForUpdate();
EntityDataUpdate update = mapper.readValue(msg, EntityDataUpdate.class);
Assert.assertEquals(1, update.getCmdId());
List<EntityData> eData = update.getUpdate();
Assert.assertNotNull(eData);
Assert.assertEquals(1, eData.size());
Assert.assertEquals(device.getId(), eData.get(0).getEntityId());
Assert.assertNotNull(eData.get(0).getLatest().get(EntityKeyType.TIME_SERIES));
var tsValue = eData.get(0).getLatest().get(EntityKeyType.TIME_SERIES).get("batteryLevel");
Assert.assertEquals(42, Long.parseLong(tsValue.getValue()));
client.destroy();
Security security = x509(SECURE_URI,
123,
clientX509CertNotTrusted.getEncoded(),
clientPrivateKeyFromCert.getEncoded(),
serverX509Cert.getEncoded());
super.basicTestConnectionObserveTelemetry(security, credentials, SECURE_COAP_CONFIG, ENDPOINT);
}
}

2
application/src/test/resources/logback.xml

@ -14,7 +14,7 @@
<logger name="org.springframework.boot.test" level="WARN"/>
<logger name="org.apache.cassandra" level="WARN"/>
<logger name="org.cassandraunit" level="INFO"/>
<logger name="org.eclipse.leshan" level="TRACE"/>
<logger name="org.eclipse.leshan" level="INFO"/>
<root level="WARN">

20
common/coap-server/src/main/java/org/thingsboard/server/coapserver/DefaultCoapServerService.java

@ -36,6 +36,8 @@ import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import static org.eclipse.californium.core.network.config.NetworkConfigDefaults.DEFAULT_BLOCKWISE_STATUS_LIFETIME;
@Slf4j
@Component
@TbCoapServerComponent
@ -85,20 +87,32 @@ public class DefaultCoapServerService implements CoapServerService {
}
private CoapServer createCoapServer() throws UnknownHostException {
server = new CoapServer();
NetworkConfig networkConfig = new NetworkConfig();
networkConfig.setBoolean(NetworkConfig.Keys.BLOCKWISE_STRICT_BLOCK2_OPTION, true);
networkConfig.setBoolean(NetworkConfig.Keys.BLOCKWISE_ENTITY_TOO_LARGE_AUTO_FAILOVER, true);
networkConfig.setLong(NetworkConfig.Keys.BLOCKWISE_STATUS_LIFETIME, DEFAULT_BLOCKWISE_STATUS_LIFETIME);
networkConfig.setInt(NetworkConfig.Keys.MAX_RESOURCE_BODY_SIZE, 256 * 1024 * 1024);
networkConfig.setString(NetworkConfig.Keys.RESPONSE_MATCHING, "RELAXED");
networkConfig.setInt(NetworkConfig.Keys.PREFERRED_BLOCK_SIZE, 1024);
networkConfig.setInt(NetworkConfig.Keys.MAX_MESSAGE_SIZE, 1024);
networkConfig.setInt(NetworkConfig.Keys.MAX_RETRANSMIT, 4);
networkConfig.setInt(NetworkConfig.Keys.COAP_PORT, coapServerContext.getPort());
server = new CoapServer(networkConfig);
CoapEndpoint.Builder noSecCoapEndpointBuilder = new CoapEndpoint.Builder();
InetAddress addr = InetAddress.getByName(coapServerContext.getHost());
InetSocketAddress sockAddr = new InetSocketAddress(addr, coapServerContext.getPort());
noSecCoapEndpointBuilder.setInetSocketAddress(sockAddr);
noSecCoapEndpointBuilder.setNetworkConfig(NetworkConfig.getStandard());
noSecCoapEndpointBuilder.setNetworkConfig(networkConfig);
CoapEndpoint noSecCoapEndpoint = noSecCoapEndpointBuilder.build();
server.addEndpoint(noSecCoapEndpoint);
if (isDtlsEnabled()) {
CoapEndpoint.Builder dtlsCoapEndpointBuilder = new CoapEndpoint.Builder();
TbCoapDtlsSettings dtlsSettings = coapServerContext.getDtlsSettings();
DtlsConnectorConfig dtlsConnectorConfig = dtlsSettings.dtlsConnectorConfig();
networkConfig.setInt(NetworkConfig.Keys.COAP_SECURE_PORT, dtlsConnectorConfig.getAddress().getPort());
dtlsCoapEndpointBuilder.setNetworkConfig(networkConfig);
DTLSConnector connector = new DTLSConnector(dtlsConnectorConfig);
dtlsCoapEndpointBuilder.setConnector(connector);
CoapEndpoint dtlsCoapEndpoint = dtlsCoapEndpointBuilder.build();

2
common/dao-api/src/main/java/org/thingsboard/server/dao/edge/EdgeService.java

@ -93,4 +93,6 @@ public interface EdgeService {
Object activateInstance(String licenseSecret, String releaseDate);
String findMissingToRelatedRuleChains(TenantId tenantId, EdgeId edgeId);
void cleanupEvents(long ttl);
}

2
common/dao-api/src/main/java/org/thingsboard/server/dao/event/EventService.java

@ -46,4 +46,6 @@ public interface EventService {
void removeEvents(TenantId tenantId, EntityId entityId);
void cleanupEvents(long ttl, long debugTtl);
}

39
common/dao-api/src/main/java/org/thingsboard/server/dao/rpc/RpcService.java

@ -0,0 +1,39 @@
/**
* 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.dao.rpc;
import com.google.common.util.concurrent.ListenableFuture;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.RpcId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.rpc.Rpc;
import org.thingsboard.server.common.data.rpc.RpcStatus;
public interface RpcService {
Rpc save(Rpc rpc);
void deleteRpc(TenantId tenantId, RpcId id);
void deleteAllRpcByTenantId(TenantId tenantId);
Rpc findById(TenantId tenantId, RpcId id);
ListenableFuture<Rpc> findRpcByIdAsync(TenantId tenantId, RpcId id);
PageData<Rpc> findAllByDeviceIdAndStatus(TenantId tenantId, DeviceId deviceId, RpcStatus rpcStatus, PageLink pageLink);
}

2
common/dao-api/src/main/java/org/thingsboard/server/dao/timeseries/TimeseriesService.java

@ -52,4 +52,6 @@ public interface TimeseriesService {
List<String> findAllKeysByDeviceProfileId(TenantId tenantId, DeviceProfileId deviceProfileId);
List<String> findAllKeysByEntityIds(TenantId tenantId, List<EntityId> entityIds);
void cleanup(long systemTtl);
}

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

@ -76,6 +76,12 @@ public class DataConstants {
public static final String RPC_CALL_FROM_SERVER_TO_DEVICE = "RPC_CALL_FROM_SERVER_TO_DEVICE";
public static final String RPC_QUEUED = "RPC_QUEUED";
public static final String RPC_DELIVERED = "RPC_DELIVERED";
public static final String RPC_SUCCESSFUL = "RPC_SUCCESSFUL";
public static final String RPC_TIMEOUT = "RPC_TIMEOUT";
public static final String RPC_FAILED = "RPC_FAILED";
public static final String DEFAULT_SECRET_KEY = "";
public static final String SECRET_KEY_FIELD_NAME = "secretKey";
public static final String DURATION_MS_FIELD_NAME = "durationMs";

2
common/data/src/main/java/org/thingsboard/server/common/data/EntityType.java

@ -19,5 +19,5 @@ package org.thingsboard.server.common.data;
* @author Andrew Shvayka
*/
public enum EntityType {
TENANT, CUSTOMER, USER, DASHBOARD, ASSET, DEVICE, ALARM, RULE_CHAIN, RULE_NODE, ENTITY_VIEW, WIDGETS_BUNDLE, WIDGET_TYPE, TENANT_PROFILE, DEVICE_PROFILE, API_USAGE_STATE, TB_RESOURCE, OTA_PACKAGE, EDGE;
TENANT, CUSTOMER, USER, DASHBOARD, ASSET, DEVICE, ALARM, RULE_CHAIN, RULE_NODE, ENTITY_VIEW, WIDGETS_BUNDLE, WIDGET_TYPE, TENANT_PROFILE, DEVICE_PROFILE, API_USAGE_STATE, TB_RESOURCE, OTA_PACKAGE, EDGE, RPC;
}

23
common/data/src/main/java/org/thingsboard/server/common/data/device/credentials/lwm2m/HasKey.java → common/data/src/main/java/org/thingsboard/server/common/data/device/credentials/lwm2m/AbstractLwM2MClientCredentialsWithKey.java

@ -15,20 +15,25 @@
*/
package org.thingsboard.server.common.data.device.credentials.lwm2m;
import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.Getter;
import lombok.Setter;
import lombok.SneakyThrows;
import org.apache.commons.codec.binary.Hex;
public abstract class HasKey extends AbstractLwM2MClientCredentials {
private byte[] key;
public abstract class AbstractLwM2MClientCredentialsWithKey extends AbstractLwM2MClientCredentials {
@Getter
@Setter
private String key;
private byte[] keyInBytes;
@SneakyThrows
public void setKey(String key) {
if (key != null) {
this.key = Hex.decodeHex(key.toLowerCase().toCharArray());
@JsonIgnore
public byte[] getDecodedKey() {
if (keyInBytes == null) {
keyInBytes = Hex.decodeHex(key.toLowerCase().toCharArray());
}
}
public byte[] getKey() {
return key;
return keyInBytes;
}
}

2
common/data/src/main/java/org/thingsboard/server/common/data/device/credentials/lwm2m/PSKClientCredentials.java

@ -20,7 +20,7 @@ import lombok.Setter;
@Getter
@Setter
public class PSKClientCredentials extends HasKey {
public class PSKClientCredentials extends AbstractLwM2MClientCredentialsWithKey {
private String identity;
@Override

2
common/data/src/main/java/org/thingsboard/server/common/data/device/credentials/lwm2m/RPKClientCredentials.java

@ -15,7 +15,7 @@
*/
package org.thingsboard.server.common.data.device.credentials.lwm2m;
public class RPKClientCredentials extends HasKey {
public class RPKClientCredentials extends AbstractLwM2MClientCredentialsWithKey {
@Override
public LwM2MSecurityMode getSecurityConfigClientMode() {

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

@ -19,7 +19,6 @@ import com.fasterxml.jackson.annotation.JsonAnyGetter;
import com.fasterxml.jackson.annotation.JsonAnySetter;
import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.Data;
import org.thingsboard.server.common.data.DeviceProfileType;
import org.thingsboard.server.common.data.DeviceTransportType;
import java.util.HashMap;
@ -28,6 +27,8 @@ import java.util.Map;
@Data
public class Lwm2mDeviceTransportConfiguration implements DeviceTransportConfiguration {
private PowerMode powerMode;
@JsonIgnore
private Map<String, Object> properties = new HashMap<>();

20
common/data/src/main/java/org/thingsboard/server/common/data/device/data/PowerMode.java

@ -0,0 +1,20 @@
/**
* 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.common.data.device.data;
public enum PowerMode {
PSM, DRX, E_DRX
}

9
common/data/src/main/java/org/thingsboard/server/common/data/device/data/SnmpDeviceTransportConfiguration.java

@ -51,6 +51,13 @@ public class SnmpDeviceTransportConfiguration implements DeviceTransportConfigur
private String privacyPassphrase;
private String engineId;
public SnmpDeviceTransportConfiguration() {
this.host = "localhost";
this.port = 161;
this.protocolVersion = SnmpProtocolVersion.V2C;
this.community = "public";
}
@Override
public DeviceTransportType getType() {
return DeviceTransportType.SNMP;
@ -76,7 +83,7 @@ public class SnmpDeviceTransportConfiguration implements DeviceTransportConfigur
isValid = StringUtils.isNotBlank(username) && StringUtils.isNotBlank(securityName)
&& contextName != null && authenticationProtocol != null
&& StringUtils.isNotBlank(authenticationPassphrase)
&& privacyProtocol != null && privacyPassphrase != null && engineId != null;
&& privacyProtocol != null && StringUtils.isNotBlank(privacyPassphrase) && engineId != null;
break;
}
}

30
common/data/src/main/java/org/thingsboard/server/common/data/device/data/lwm2m/BootstrapConfiguration.java

@ -0,0 +1,30 @@
/**
* 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.common.data.device.data.lwm2m;
import lombok.Data;
import java.util.Map;
@Data
public class BootstrapConfiguration {
//TODO: define the objects;
private Map<String, Object> servers;
private Map<String, Object> lwm2mServer;
private Map<String, Object> bootstrapServer;
}

33
common/data/src/main/java/org/thingsboard/server/common/data/device/data/lwm2m/ObjectAttributes.java

@ -0,0 +1,33 @@
/**
* 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.common.data.device.data.lwm2m;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Data;
@Data
@JsonInclude(JsonInclude.Include.NON_NULL)
public class ObjectAttributes {
private Long dim;
private String ver;
private Long pmin;
private Long pmax;
private Double gt;
private Double lt;
private Double st;
}

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

@ -0,0 +1,32 @@
/**
* 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.common.data.device.data.lwm2m;
import lombok.Data;
import org.thingsboard.server.common.data.device.data.PowerMode;
@Data
public class OtherConfiguration {
private Integer fwUpdateStrategy;
private Integer swUpdateStrategy;
private Integer clientOnlyObserveAfterConnect;
private PowerMode powerMode;
private String fwUpdateResource;
private String swUpdateResource;
private boolean compositeOperationsSupport;
}

32
common/data/src/main/java/org/thingsboard/server/common/data/device/data/lwm2m/TelemetryMappingConfiguration.java

@ -0,0 +1,32 @@
/**
* 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.common.data.device.data.lwm2m;
import lombok.Data;
import java.util.Map;
import java.util.Set;
@Data
public class TelemetryMappingConfiguration {
private Map<String, String> keyName;
private Set<String> observe;
private Set<String> attribute;
private Set<String> telemetry;
private Map<String, ObjectAttributes> attributeLwm2m;
}

25
common/data/src/main/java/org/thingsboard/server/common/data/device/profile/Lwm2mDeviceProfileTransportConfiguration.java

@ -15,31 +15,20 @@
*/
package org.thingsboard.server.common.data.device.profile;
import com.fasterxml.jackson.annotation.JsonAnyGetter;
import com.fasterxml.jackson.annotation.JsonAnySetter;
import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.Data;
import org.thingsboard.server.common.data.DeviceProfileType;
import org.thingsboard.server.common.data.DeviceTransportType;
import java.util.HashMap;
import java.util.Map;
import org.thingsboard.server.common.data.device.data.lwm2m.BootstrapConfiguration;
import org.thingsboard.server.common.data.device.data.lwm2m.OtherConfiguration;
import org.thingsboard.server.common.data.device.data.lwm2m.TelemetryMappingConfiguration;
@Data
public class Lwm2mDeviceProfileTransportConfiguration implements DeviceProfileTransportConfiguration {
@JsonIgnore
private Map<String, Object> properties = new HashMap<>();
@JsonAnyGetter
public Map<String, Object> properties() {
return this.properties;
}
private static final long serialVersionUID = 6257277825459600068L;
@JsonAnySetter
public void put(String name, Object value) {
this.properties.put(name, value);
}
private TelemetryMappingConfiguration observeAttr;
private BootstrapConfiguration bootstrap;
private OtherConfiguration clientLwM2mSettings;
@Override
public DeviceTransportType getType() {

2
common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java

@ -75,6 +75,8 @@ public class EntityIdFactory {
return new OtaPackageId(uuid);
case EDGE:
return new EdgeId(uuid);
case RPC:
return new RpcId(uuid);
}
throw new IllegalArgumentException("EntityType " + type + " is not supported!");
}

39
common/data/src/main/java/org/thingsboard/server/common/data/id/RpcId.java

@ -0,0 +1,39 @@
/**
* 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.common.data.id;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.thingsboard.server.common.data.EntityType;
import java.util.UUID;
public final class RpcId extends UUIDBased implements EntityId {
private static final long serialVersionUID = 1L;
@JsonCreator
public RpcId(@JsonProperty("id") UUID id) {
super(id);
}
@JsonIgnore
@Override
public EntityType getEntityType() {
return EntityType.RPC;
}
}

54
common/data/src/main/java/org/thingsboard/server/common/data/rpc/Rpc.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.common.data.rpc;
import com.fasterxml.jackson.databind.JsonNode;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.thingsboard.server.common.data.BaseData;
import org.thingsboard.server.common.data.HasTenantId;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.RpcId;
import org.thingsboard.server.common.data.id.TenantId;
@Data
@EqualsAndHashCode(callSuper = true)
public class Rpc extends BaseData<RpcId> implements HasTenantId {
private TenantId tenantId;
private DeviceId deviceId;
private long expirationTime;
private JsonNode request;
private JsonNode response;
private RpcStatus status;
public Rpc() {
super();
}
public Rpc(RpcId id) {
super(id);
}
public Rpc(Rpc rpc) {
super(rpc);
this.tenantId = rpc.getTenantId();
this.deviceId = rpc.getDeviceId();
this.expirationTime = rpc.getExpirationTime();
this.request = rpc.getRequest();
this.response = rpc.getResponse();
this.status = rpc.getStatus();
}
}

20
common/data/src/main/java/org/thingsboard/server/common/data/rpc/RpcStatus.java

@ -0,0 +1,20 @@
/**
* 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.common.data.rpc;
public enum RpcStatus {
QUEUED, DELIVERED, SUCCESSFUL, TIMEOUT, FAILED
}

1
common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java

@ -56,6 +56,7 @@ public class DefaultTenantProfileConfiguration implements TenantProfileConfigura
private int defaultStorageTtlDays;
private int alarmsTtlDays;
private int rpcTtlDays;
private double warnThreshold;

1
common/message/src/main/java/org/thingsboard/server/common/msg/rpc/ToDeviceRpcRequest.java

@ -34,5 +34,6 @@ public class ToDeviceRpcRequest implements Serializable {
private final boolean oneway;
private final long expirationTime;
private final ToDeviceRpcRequestBody body;
private final boolean persisted;
}

2
common/message/src/main/java/org/thingsboard/server/common/msg/session/FeatureType.java

@ -16,5 +16,5 @@
package org.thingsboard.server.common.msg.session;
public enum FeatureType {
ATTRIBUTES, TELEMETRY, RPC, CLAIM, PROVISION, FIRMWARE, SOFTWARE
ATTRIBUTES, TELEMETRY, RPC, CLAIM, PROVISION
}

5
common/message/src/main/java/org/thingsboard/server/common/msg/session/SessionMsgType.java

@ -30,10 +30,7 @@ public enum SessionMsgType {
SESSION_OPEN, SESSION_CLOSE,
CLAIM_REQUEST(),
GET_FIRMWARE_REQUEST,
GET_SOFTWARE_REQUEST;
CLAIM_REQUEST();
private final boolean requiresRulesProcessing;

2
common/queue/src/main/java/org/thingsboard/server/queue/TbQueueRequestTemplate.java

@ -24,6 +24,8 @@ public interface TbQueueRequestTemplate<Request extends TbQueueMsg, Response ext
ListenableFuture<Response> send(Request request);
ListenableFuture<Response> send(Request request, long timeoutNs);
void stop();
void setMessagesStats(MessagesStats messagesStats);

242
common/queue/src/main/java/org/thingsboard/server/queue/common/DefaultTbQueueRequestTemplate.java

@ -19,7 +19,9 @@ import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.SettableFuture;
import lombok.Builder;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.common.util.TbStopWatch;
import org.thingsboard.common.util.ThingsBoardThreadFactory;
import org.thingsboard.server.common.msg.queue.TopicPartitionInfo;
import org.thingsboard.server.queue.TbQueueAdmin;
@ -31,13 +33,17 @@ import org.thingsboard.server.queue.TbQueueProducer;
import org.thingsboard.server.queue.TbQueueRequestTemplate;
import org.thingsboard.server.common.stats.MessagesStats;
import javax.annotation.Nullable;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.LockSupport;
import java.util.concurrent.locks.ReentrantLock;
@Slf4j
public class DefaultTbQueueRequestTemplate<Request extends TbQueueMsg, Response extends TbQueueMsg> extends AbstractTbQueueTemplate
@ -46,15 +52,15 @@ public class DefaultTbQueueRequestTemplate<Request extends TbQueueMsg, Response
private final TbQueueAdmin queueAdmin;
private final TbQueueProducer<Request> requestTemplate;
private final TbQueueConsumer<Response> responseTemplate;
private final ConcurrentMap<UUID, DefaultTbQueueRequestTemplate.ResponseMetaData<Response>> pendingRequests;
private final boolean internalExecutor;
private final ExecutorService executor;
private final long maxRequestTimeout;
private final long maxPendingRequests;
private final long pollInterval;
private volatile long tickTs = 0L;
private volatile long tickSize = 0L;
private volatile boolean stopped = false;
final ConcurrentHashMap<UUID, DefaultTbQueueRequestTemplate.ResponseMetaData<Response>> pendingRequests = new ConcurrentHashMap<>();
final boolean internalExecutor;
final ExecutorService executor;
final long maxRequestTimeoutNs;
final long maxPendingRequests;
final long pollInterval;
volatile boolean stopped = false;
long nextCleanupNs = 0L;
private final Lock cleanerLock = new ReentrantLock();
private MessagesStats messagesStats;
@ -65,79 +71,113 @@ public class DefaultTbQueueRequestTemplate<Request extends TbQueueMsg, Response
long maxRequestTimeout,
long maxPendingRequests,
long pollInterval,
ExecutorService executor) {
@Nullable ExecutorService executor) {
this.queueAdmin = queueAdmin;
this.requestTemplate = requestTemplate;
this.responseTemplate = responseTemplate;
this.pendingRequests = new ConcurrentHashMap<>();
this.maxRequestTimeout = maxRequestTimeout;
this.maxRequestTimeoutNs = TimeUnit.MILLISECONDS.toNanos(maxRequestTimeout);
this.maxPendingRequests = maxPendingRequests;
this.pollInterval = pollInterval;
if (executor != null) {
internalExecutor = false;
this.executor = executor;
} else {
internalExecutor = true;
this.executor = Executors.newSingleThreadExecutor(ThingsBoardThreadFactory.forName("tb-queue-request-template-" + responseTemplate.getTopic()));
}
this.internalExecutor = (executor == null);
this.executor = internalExecutor ? createExecutor() : executor;
}
ExecutorService createExecutor() {
return Executors.newSingleThreadExecutor(ThingsBoardThreadFactory.forName("tb-queue-request-template-" + responseTemplate.getTopic()));
}
@Override
public void init() {
queueAdmin.createTopicIfNotExists(responseTemplate.getTopic());
this.requestTemplate.init();
tickTs = System.currentTimeMillis();
requestTemplate.init();
responseTemplate.subscribe();
executor.submit(() -> {
long nextCleanupMs = 0L;
while (!stopped) {
try {
List<Response> responses = responseTemplate.poll(pollInterval);
if (responses.size() > 0) {
log.trace("Polling responses completed, consumer records count [{}]", responses.size());
}
responses.forEach(response -> {
byte[] requestIdHeader = response.getHeaders().get(REQUEST_ID_HEADER);
UUID requestId;
if (requestIdHeader == null) {
log.error("[{}] Missing requestId in header and body", response);
} else {
requestId = bytesToUuid(requestIdHeader);
log.trace("[{}] Response received: {}", requestId, response);
ResponseMetaData<Response> expectedResponse = pendingRequests.remove(requestId);
if (expectedResponse == null) {
log.trace("[{}] Invalid or stale request", requestId);
} else {
expectedResponse.future.set(response);
}
executor.submit(this::mainLoop);
}
void mainLoop() {
while (!stopped) {
TbStopWatch sw = TbStopWatch.startNew();
try {
fetchAndProcessResponses();
} catch (Throwable e) {
long sleepNanos = TimeUnit.MILLISECONDS.toNanos(this.pollInterval) - sw.stopAndGetTotalTimeNanos();
log.warn("Failed to obtain and process responses from queue. Going to sleep " + sleepNanos + "ns", e);
sleep(sleepNanos);
}
}
}
void fetchAndProcessResponses() {
final long pendingRequestsCount = pendingRequests.mappingCount();
log.trace("Starting template pool topic {}, for pendingRequests {}", responseTemplate.getTopic(), pendingRequestsCount);
List<Response> responses = doPoll(); //poll js responses
log.trace("Completed template poll topic {}, for pendingRequests [{}], received [{}] responses", responseTemplate.getTopic(), pendingRequestsCount, responses.size());
responses.forEach(this::processResponse); //this can take a long time
responseTemplate.commit();
tryCleanStaleRequests();
}
private boolean tryCleanStaleRequests() {
if (!cleanerLock.tryLock()) {
return false;
}
try {
log.trace("tryCleanStaleRequest...");
final long currentNs = getCurrentClockNs();
if (nextCleanupNs < currentNs) {
pendingRequests.forEach((key, value) -> {
if (value.expTime < currentNs) {
ResponseMetaData<Response> staleRequest = pendingRequests.remove(key);
if (staleRequest != null) {
setTimeoutException(key, staleRequest, currentNs);
}
});
responseTemplate.commit();
tickTs = System.currentTimeMillis();
tickSize = pendingRequests.size();
if (nextCleanupMs < tickTs) {
//cleanup;
pendingRequests.forEach((key, value) -> {
if (value.expTime < tickTs) {
ResponseMetaData<Response> staleRequest = pendingRequests.remove(key);
if (staleRequest != null) {
log.trace("[{}] Request timeout detected, expTime [{}], tickTs [{}]", key, staleRequest.expTime, tickTs);
staleRequest.future.setException(new TimeoutException());
}
}
});
nextCleanupMs = tickTs + maxRequestTimeout;
}
} catch (Throwable e) {
log.warn("Failed to obtain responses from queue.", e);
try {
Thread.sleep(pollInterval);
} catch (InterruptedException e2) {
log.trace("Failed to wait until the server has capacity to handle new responses", e2);
}
}
});
setupNextCleanup();
}
});
} finally {
cleanerLock.unlock();
}
return true;
}
void setupNextCleanup() {
nextCleanupNs = getCurrentClockNs() + maxRequestTimeoutNs;
log.trace("setupNextCleanup {}", nextCleanupNs);
}
List<Response> doPoll() {
return responseTemplate.poll(pollInterval);
}
void sleep(long nanos) {
LockSupport.parkNanos(nanos);
}
void setTimeoutException(UUID key, ResponseMetaData<Response> staleRequest, long currentNs) {
if (currentNs >= staleRequest.getSubmitTime() + staleRequest.getTimeout()) {
log.warn("Request timeout detected, currentNs [{}], {}, key [{}]", currentNs, staleRequest, key);
} else {
log.error("Request timeout detected, currentNs [{}], {}, key [{}]", currentNs, staleRequest, key);
}
staleRequest.future.setException(new TimeoutException());
}
void processResponse(Response response) {
byte[] requestIdHeader = response.getHeaders().get(REQUEST_ID_HEADER);
UUID requestId;
if (requestIdHeader == null) {
log.error("[{}] Missing requestId in header and body", response);
} else {
requestId = bytesToUuid(requestIdHeader);
log.trace("[{}] Response received: {}", requestId, String.valueOf(response).replace("\n", " ")); //TODO remove overhead
ResponseMetaData<Response> expectedResponse = pendingRequests.remove(requestId);
if (expectedResponse == null) {
log.warn("[{}] Invalid or stale request, response: {}", requestId, String.valueOf(response).replace("\n", " "));
} else {
expectedResponse.future.set(response);
}
}
}
@Override
@ -164,17 +204,48 @@ public class DefaultTbQueueRequestTemplate<Request extends TbQueueMsg, Response
@Override
public ListenableFuture<Response> send(Request request) {
if (tickSize > maxPendingRequests) {
return send(request, this.maxRequestTimeoutNs);
}
@Override
public ListenableFuture<Response> send(Request request, long requestTimeoutNs) {
if (pendingRequests.mappingCount() >= maxPendingRequests) {
log.warn("Pending request map is full [{}]! Consider to increase maxPendingRequests or increase processing performance", maxPendingRequests);
return Futures.immediateFailedFuture(new RuntimeException("Pending request map is full!"));
}
UUID requestId = UUID.randomUUID();
request.getHeaders().put(REQUEST_ID_HEADER, uuidToBytes(requestId));
request.getHeaders().put(RESPONSE_TOPIC_HEADER, stringToBytes(responseTemplate.getTopic()));
request.getHeaders().put(REQUEST_TIME, longToBytes(System.currentTimeMillis()));
request.getHeaders().put(REQUEST_TIME, longToBytes(getCurrentTimeMs()));
long currentClockNs = getCurrentClockNs();
SettableFuture<Response> future = SettableFuture.create();
ResponseMetaData<Response> responseMetaData = new ResponseMetaData<>(tickTs + maxRequestTimeout, future);
pendingRequests.putIfAbsent(requestId, responseMetaData);
log.trace("[{}] Sending request, key [{}], expTime [{}]", requestId, request.getKey(), responseMetaData.expTime);
ResponseMetaData<Response> responseMetaData = new ResponseMetaData<>(currentClockNs + requestTimeoutNs, future, currentClockNs, requestTimeoutNs);
log.trace("pending {}", responseMetaData);
if (pendingRequests.putIfAbsent(requestId, responseMetaData) != null) {
log.warn("Pending request already exists [{}]!", maxPendingRequests);
return Futures.immediateFailedFuture(new RuntimeException("Pending request already exists !" + requestId));
}
sendToRequestTemplate(request, requestId, future, responseMetaData);
return future;
}
/**
* MONOTONIC clock instead jumping wall clock.
* Wrapped into the method for the test purposes to travel through the time
* */
long getCurrentClockNs() {
return System.nanoTime();
}
/**
* Wall clock to send timestamp to an external service
* */
long getCurrentTimeMs() {
return System.currentTimeMillis();
}
void sendToRequestTemplate(Request request, UUID requestId, SettableFuture<Response> future, ResponseMetaData<Response> responseMetaData) {
log.trace("[{}] Sending request, key [{}], expTime [{}], request {}", requestId, request.getKey(), responseMetaData.expTime, request);
if (messagesStats != null) {
messagesStats.incrementTotal();
}
@ -184,7 +255,7 @@ public class DefaultTbQueueRequestTemplate<Request extends TbQueueMsg, Response
if (messagesStats != null) {
messagesStats.incrementSuccessful();
}
log.trace("[{}] Request sent: {}", requestId, metadata);
log.trace("[{}] Request sent: {}, request {}", requestId, metadata, request);
}
@Override
@ -196,17 +267,32 @@ public class DefaultTbQueueRequestTemplate<Request extends TbQueueMsg, Response
future.setException(t);
}
});
return future;
}
private static class ResponseMetaData<T> {
@Getter
static class ResponseMetaData<T> {
private final long submitTime;
private final long timeout;
private final long expTime;
private final SettableFuture<T> future;
ResponseMetaData(long ts, SettableFuture<T> future) {
ResponseMetaData(long ts, SettableFuture<T> future, long submitTime, long timeout) {
this.submitTime = submitTime;
this.timeout = timeout;
this.expTime = ts;
this.future = future;
}
@Override
public String toString() {
return "ResponseMetaData{" +
"submitTime=" + submitTime +
", calculatedExpTime=" + (submitTime + timeout) +
", deltaMs=" + (expTime - submitTime) +
", expTime=" + expTime +
", future=" + future +
'}';
}
}
}

10
common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaConsumerTemplate.java

@ -21,6 +21,7 @@ import org.apache.kafka.clients.consumer.ConsumerConfig;
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.apache.kafka.clients.consumer.ConsumerRecords;
import org.apache.kafka.clients.consumer.KafkaConsumer;
import org.springframework.util.StopWatch;
import org.thingsboard.server.queue.TbQueueAdmin;
import org.thingsboard.server.queue.TbQueueMsg;
import org.thingsboard.server.queue.common.AbstractTbQueueConsumerTemplate;
@ -82,7 +83,16 @@ public class TbKafkaConsumerTemplate<T extends TbQueueMsg> extends AbstractTbQue
@Override
protected List<ConsumerRecord<String, byte[]>> doPoll(long durationInMillis) {
StopWatch stopWatch = new StopWatch();
stopWatch.start();
log.trace("poll topic {} maxDuration {}", getTopic(), durationInMillis);
ConsumerRecords<String, byte[]> records = consumer.poll(Duration.ofMillis(durationInMillis));
stopWatch.stop();
log.trace("poll topic {} took {}ms", getTopic(), stopWatch.getTotalTimeMillis());
if (records.isEmpty()) {
return Collections.emptyList();
} else {

15
common/queue/src/main/proto/queue.proto

@ -115,6 +115,7 @@ message DeviceInfoProto {
int64 deviceProfileIdLSB = 9;
int64 customerIdMSB = 10;
int64 customerIdLSB = 11;
string powerMode = 12;
}
/**
@ -318,6 +319,9 @@ message SubscribeToRPCMsg {
SessionType sessionType = 2;
}
message SendPendingRPCMsg {
}
message ToDeviceRpcRequestMsg {
int32 requestId = 1;
string methodName = 2;
@ -325,6 +329,8 @@ message ToDeviceRpcRequestMsg {
int64 expirationTime = 4;
int64 requestIdMSB = 5;
int64 requestIdLSB = 6;
bool oneway = 7;
bool persisted = 8;
}
message ToDeviceRpcResponseMsg {
@ -332,6 +338,13 @@ message ToDeviceRpcResponseMsg {
string payload = 2;
}
message ToDevicePersistedRpcResponseMsg {
int32 requestId = 1;
int64 requestIdMSB = 2;
int64 requestIdLSB = 3;
string status = 4;
}
message ToServerRpcRequestMsg {
int32 requestId = 1;
string methodName = 2;
@ -435,6 +448,8 @@ message TransportToDeviceActorMsg {
SubscriptionInfoProto subscriptionInfo = 7;
ClaimDeviceMsg claimDevice = 8;
ProvisionDeviceRequestMsg provisionDevice = 9;
ToDevicePersistedRpcResponseMsg persistedRpcResponseMsg = 10;
SendPendingRPCMsg sendPendingRPC = 11;
}
message TransportToRuleEngineMsg {

211
common/queue/src/test/java/org/thingsboard/server/queue/common/DefaultTbQueueRequestTemplateTest.java

@ -0,0 +1,211 @@
/**
* 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.queue.common;
import lombok.extern.slf4j.Slf4j;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import org.thingsboard.server.queue.TbQueueAdmin;
import org.thingsboard.server.queue.TbQueueConsumer;
import org.thingsboard.server.queue.TbQueueMsg;
import org.thingsboard.server.queue.TbQueueProducer;
import java.util.Collections;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.greaterThanOrEqualTo;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.lessThan;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.BDDMockito.willAnswer;
import static org.mockito.BDDMockito.willDoNothing;
import static org.mockito.BDDMockito.willReturn;
import static org.mockito.Mockito.RETURNS_DEEP_STUBS;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.mockito.hamcrest.MockitoHamcrest.longThat;
@Slf4j
@RunWith(MockitoJUnitRunner.class)
public class DefaultTbQueueRequestTemplateTest {
@Mock
TbQueueAdmin queueAdmin;
@Mock
TbQueueProducer<TbQueueMsg> requestTemplate;
@Mock
TbQueueConsumer<TbQueueMsg> responseTemplate;
@Mock
ExecutorService executorMock;
ExecutorService executor;
String topic = "js-responses-tb-node-0";
long maxRequestTimeout = 10;
long maxPendingRequests = 32;
long pollInterval = 5;
DefaultTbQueueRequestTemplate inst;
@Before
public void setUp() throws Exception {
willReturn(topic).given(responseTemplate).getTopic();
inst = spy(new DefaultTbQueueRequestTemplate(
queueAdmin, requestTemplate, responseTemplate,
maxRequestTimeout, maxPendingRequests, pollInterval, executorMock));
}
@After
public void tearDown() throws Exception {
if (executor != null) {
executor.shutdownNow();
}
}
@Test
public void givenInstance_whenVerifyInitialParameters_thenOK() {
assertThat(inst.maxPendingRequests, equalTo(maxPendingRequests));
assertThat(inst.maxRequestTimeoutNs, equalTo(TimeUnit.MILLISECONDS.toNanos(maxRequestTimeout)));
assertThat(inst.pollInterval, equalTo(pollInterval));
assertThat(inst.executor, is(executorMock));
assertThat(inst.stopped, is(false));
assertThat(inst.internalExecutor, is(false));
}
@Test
public void givenExternalExecutor_whenInitStop_thenOK() {
inst.init();
assertThat(inst.nextCleanupNs, equalTo(0L));
verify(queueAdmin, times(1)).createTopicIfNotExists(topic);
verify(requestTemplate, times(1)).init();
verify(responseTemplate, times(1)).subscribe();
verify(executorMock, times(1)).submit(any(Runnable.class));
inst.stop();
assertThat(inst.stopped, is(true));
verify(responseTemplate, times(1)).unsubscribe();
verify(requestTemplate, times(1)).stop();
verify(executorMock, never()).shutdownNow();
}
@Test
public void givenMainLoop_whenLoopFewTimes_thenVerifyInvocationCount() throws InterruptedException {
executor = inst.createExecutor();
CountDownLatch latch = new CountDownLatch(5);
willDoNothing().given(inst).sleep(anyLong());
willAnswer(invocation -> {
if (latch.getCount() == 1) {
inst.stop(); //stop the loop in natural way
}
if (latch.getCount() == 3 || latch.getCount() == 4) {
latch.countDown();
throw new RuntimeException("test catch block");
}
latch.countDown();
return null;
}).given(inst).fetchAndProcessResponses();
executor.submit(inst::mainLoop);
latch.await(10, TimeUnit.SECONDS);
verify(inst, times(5)).fetchAndProcessResponses();
verify(inst, times(2)).sleep(longThat(lessThan(TimeUnit.MILLISECONDS.toNanos(inst.pollInterval))));
}
@Test
public void givenMessages_whenSend_thenOK() {
willDoNothing().given(inst).sendToRequestTemplate(any(), any(), any(), any());
inst.init();
final int msgCount = 10;
for (int i = 0; i < msgCount; i++) {
inst.send(getRequestMsgMock());
}
assertThat(inst.pendingRequests.mappingCount(), equalTo((long) msgCount));
verify(inst, times(msgCount)).sendToRequestTemplate(any(), any(), any(), any());
}
@Test
public void givenMessagesOverMaxPendingRequests_whenSend_thenImmediateFailedFutureForTheOfRequests() {
willDoNothing().given(inst).sendToRequestTemplate(any(), any(), any(), any());
inst.init();
int msgOverflowCount = 10;
for (int i = 0; i < inst.maxPendingRequests; i++) {
assertThat(inst.send(getRequestMsgMock()).isDone(), is(false)); //SettableFuture future - pending only
}
for (int i = 0; i < msgOverflowCount; i++) {
assertThat("max pending requests overflow", inst.send(getRequestMsgMock()).isDone(), is(true)); //overflow, immediate failed future
}
assertThat(inst.pendingRequests.mappingCount(), equalTo(inst.maxPendingRequests));
verify(inst, times((int) inst.maxPendingRequests)).sendToRequestTemplate(any(), any(), any(), any());
}
@Test
public void givenNothing_whenSendAndFetchAndProcessResponsesWithTimeout_thenFail() {
//given
AtomicLong currentTime = new AtomicLong();
willAnswer(x -> {
log.info("currentTime={}", currentTime.get());
return currentTime.get();
}).given(inst).getCurrentClockNs();
inst.init();
inst.setupNextCleanup();
willReturn(Collections.emptyList()).given(inst).doPoll();
//when
long stepNs = TimeUnit.MILLISECONDS.toNanos(1);
for (long i = 0; i <= inst.maxRequestTimeoutNs * 2; i = i + stepNs) {
currentTime.addAndGet(stepNs);
assertThat(inst.send(getRequestMsgMock()).isDone(), is(false)); //SettableFuture future - pending only
if (i % (inst.maxRequestTimeoutNs * 3 / 2) == 0) {
inst.fetchAndProcessResponses();
}
}
//then
ArgumentCaptor<DefaultTbQueueRequestTemplate.ResponseMetaData> argumentCaptorResp = ArgumentCaptor.forClass(DefaultTbQueueRequestTemplate.ResponseMetaData.class);
ArgumentCaptor<UUID> argumentCaptorUUID = ArgumentCaptor.forClass(UUID.class);
ArgumentCaptor<Long> argumentCaptorLong = ArgumentCaptor.forClass(Long.class);
verify(inst, atLeastOnce()).setTimeoutException(argumentCaptorUUID.capture(), argumentCaptorResp.capture(), argumentCaptorLong.capture());
List<DefaultTbQueueRequestTemplate.ResponseMetaData> responseMetaDataList = argumentCaptorResp.getAllValues();
List<Long> tickTsList = argumentCaptorLong.getAllValues();
for (int i = 0; i < responseMetaDataList.size(); i++) {
assertThat("tickTs >= calculatedExpTime", tickTsList.get(i), greaterThanOrEqualTo(responseMetaDataList.get(i).getSubmitTime() + responseMetaDataList.get(i).getTimeout()));
}
}
TbQueueMsg getRequestMsgMock() {
return mock(TbQueueMsg.class, RETURNS_DEEP_STUBS);
}
}

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

@ -44,7 +44,6 @@ import org.thingsboard.server.common.data.device.profile.DeviceProfileTransportC
import org.thingsboard.server.common.data.device.profile.JsonTransportPayloadConfiguration;
import org.thingsboard.server.common.data.device.profile.ProtoTransportPayloadConfiguration;
import org.thingsboard.server.common.data.device.profile.TransportPayloadTypeConfiguration;
import org.thingsboard.server.common.data.ota.OtaPackageType;
import org.thingsboard.server.common.data.security.DeviceTokenCredentials;
import org.thingsboard.server.common.msg.session.FeatureType;
import org.thingsboard.server.common.msg.session.SessionMsgType;
@ -65,6 +64,7 @@ import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
@Slf4j
public class CoapTransportResource extends AbstractCoapTransportResource {
@ -76,9 +76,8 @@ public class CoapTransportResource extends AbstractCoapTransportResource {
private static final int REQUEST_ID_POSITION_CERTIFICATE_REQUEST = 4;
private static final String DTLS_SESSION_ID_KEY = "DTLS_SESSION_ID";
private final ConcurrentMap<String, TransportProtos.SessionInfoProto> tokenToSessionInfoMap = new ConcurrentHashMap<>();
private final ConcurrentMap<String, AtomicInteger> tokenToObserveNotificationSeqMap = new ConcurrentHashMap<>();
private final ConcurrentMap<TransportProtos.SessionInfoProto, ObserveRelation> sessionInfoToObserveRelationMap = new ConcurrentHashMap<>();
private final ConcurrentMap<String, CoapObserveSessionInfo> tokenToCoapSessionInfoMap = new ConcurrentHashMap<>();
private final ConcurrentMap<CoapObserveSessionInfo, ObserveRelation> sessionInfoToObserveRelationMap = new ConcurrentHashMap<>();
private final Set<UUID> rpcSubscriptions = ConcurrentHashMap.newKeySet();
private final Set<UUID> attributeSubscriptions = ConcurrentHashMap.newKeySet();
@ -94,7 +93,11 @@ public class CoapTransportResource extends AbstractCoapTransportResource {
this.timeout = coapServerService.getTimeout();
this.sessionReportTimeout = ctx.getSessionReportTimeout();
ctx.getScheduler().scheduleAtFixedRate(() -> {
Set<TransportProtos.SessionInfoProto> observeSessions = sessionInfoToObserveRelationMap.keySet();
Set<CoapObserveSessionInfo> coapObserveSessionInfos = sessionInfoToObserveRelationMap.keySet();
Set<TransportProtos.SessionInfoProto> observeSessions = coapObserveSessionInfos
.stream()
.map(CoapObserveSessionInfo::getSessionInfoProto)
.collect(Collectors.toSet());
observeSessions.forEach(this::reportActivity);
}, new Random().nextInt((int) sessionReportTimeout), sessionReportTimeout, TimeUnit.MILLISECONDS);
}
@ -112,17 +115,17 @@ public class CoapTransportResource extends AbstractCoapTransportResource {
relation.setEstablished();
addObserveRelation(relation);
}
AtomicInteger notificationCounter = tokenToObserveNotificationSeqMap.computeIfAbsent(token, s -> new AtomicInteger(0));
response.getOptions().setObserve(notificationCounter.getAndIncrement());
AtomicInteger observeNotificationCounter = tokenToCoapSessionInfoMap.get(token).getObserveNotificationCounter();
response.getOptions().setObserve(observeNotificationCounter.getAndIncrement());
} // ObserveLayer takes care of the else case
}
public void clearAndNotifyObserveRelation(ObserveRelation relation, CoAP.ResponseCode code) {
private void clearAndNotifyObserveRelation(ObserveRelation relation, CoAP.ResponseCode code) {
relation.cancel();
relation.getExchange().sendResponse(new Response(code));
}
public Map<TransportProtos.SessionInfoProto, ObserveRelation> getSessionInfoToObserveRelationMap() {
private Map<CoapObserveSessionInfo, ObserveRelation> getCoapSessionInfoToObserveRelationMap() {
return sessionInfoToObserveRelationMap;
}
@ -139,10 +142,6 @@ public class CoapTransportResource extends AbstractCoapTransportResource {
processExchangeGetRequest(exchange, featureType.get());
} else if (featureType.get() == FeatureType.ATTRIBUTES) {
processRequest(exchange, SessionMsgType.GET_ATTRIBUTES_REQUEST);
} else if (featureType.get() == FeatureType.FIRMWARE) {
processRequest(exchange, SessionMsgType.GET_FIRMWARE_REQUEST);
} else if (featureType.get() == FeatureType.SOFTWARE) {
processRequest(exchange, SessionMsgType.GET_SOFTWARE_REQUEST);
} else {
log.trace("Invalid feature type parameter");
exchange.respond(CoAP.ResponseCode.BAD_REQUEST);
@ -282,8 +281,8 @@ public class CoapTransportResource extends AbstractCoapTransportResource {
new CoapOkCallback(exchange, CoAP.ResponseCode.CREATED, CoAP.ResponseCode.INTERNAL_SERVER_ERROR));
break;
case SUBSCRIBE_ATTRIBUTES_REQUEST:
TransportProtos.SessionInfoProto currentAttrSession = tokenToSessionInfoMap.get(getTokenFromRequest(request));
if (currentAttrSession == null) {
CoapObserveSessionInfo currentCoapObserveAttrSessionInfo = tokenToCoapSessionInfoMap.get(getTokenFromRequest(request));
if (currentCoapObserveAttrSessionInfo == null) {
attributeSubscriptions.add(sessionId);
registerAsyncCoapSession(exchange, sessionInfo, coapTransportAdaptor,
transportConfigurationContainer.getRpcRequestDynamicMessageBuilder(), getTokenFromRequest(request));
@ -295,20 +294,20 @@ public class CoapTransportResource extends AbstractCoapTransportResource {
}
break;
case UNSUBSCRIBE_ATTRIBUTES_REQUEST:
TransportProtos.SessionInfoProto attrSession = lookupAsyncSessionInfo(getTokenFromRequest(request));
if (attrSession != null) {
CoapObserveSessionInfo coapObserveAttrSessionInfo = lookupAsyncSessionInfo(getTokenFromRequest(request));
if (coapObserveAttrSessionInfo != null) {
TransportProtos.SessionInfoProto attrSession = coapObserveAttrSessionInfo.getSessionInfoProto();
UUID attrSessionId = toSessionId(attrSession);
attributeSubscriptions.remove(attrSessionId);
sessionInfoToObserveRelationMap.remove(attrSession);
transportService.process(attrSession,
TransportProtos.SubscribeToAttributeUpdatesMsg.newBuilder().setUnsubscribe(true).build(),
new CoapOkCallback(exchange, CoAP.ResponseCode.DELETED, CoAP.ResponseCode.INTERNAL_SERVER_ERROR));
closeAndDeregister(sessionInfo);
}
closeAndDeregister(sessionInfo);
break;
case SUBSCRIBE_RPC_COMMANDS_REQUEST:
TransportProtos.SessionInfoProto currentRpcSession = tokenToSessionInfoMap.get(getTokenFromRequest(request));
if (currentRpcSession == null) {
CoapObserveSessionInfo currentCoapObserveRpcSessionInfo = tokenToCoapSessionInfoMap.get(getTokenFromRequest(request));
if (currentCoapObserveRpcSessionInfo == null) {
rpcSubscriptions.add(sessionId);
registerAsyncCoapSession(exchange, sessionInfo, coapTransportAdaptor,
transportConfigurationContainer.getRpcRequestDynamicMessageBuilder(), getTokenFromRequest(request));
@ -319,16 +318,16 @@ public class CoapTransportResource extends AbstractCoapTransportResource {
}
break;
case UNSUBSCRIBE_RPC_COMMANDS_REQUEST:
TransportProtos.SessionInfoProto rpcSession = lookupAsyncSessionInfo(getTokenFromRequest(request));
if (rpcSession != null) {
CoapObserveSessionInfo coapObserveRpcSessionInfo = lookupAsyncSessionInfo(getTokenFromRequest(request));
if (coapObserveRpcSessionInfo != null) {
TransportProtos.SessionInfoProto rpcSession = coapObserveRpcSessionInfo.getSessionInfoProto();
UUID rpcSessionId = toSessionId(rpcSession);
rpcSubscriptions.remove(rpcSessionId);
sessionInfoToObserveRelationMap.remove(rpcSession);
transportService.process(rpcSession,
TransportProtos.SubscribeToRPCMsg.newBuilder().setUnsubscribe(true).build(),
new CoapOkCallback(exchange, CoAP.ResponseCode.DELETED, CoAP.ResponseCode.INTERNAL_SERVER_ERROR));
closeAndDeregister(sessionInfo);
}
closeAndDeregister(sessionInfo);
break;
case TO_DEVICE_RPC_RESPONSE:
transportService.process(sessionInfo,
@ -337,24 +336,18 @@ public class CoapTransportResource extends AbstractCoapTransportResource {
break;
case TO_SERVER_RPC_REQUEST:
transportService.registerSyncSession(sessionInfo, getCoapSessionListener(exchange, coapTransportAdaptor,
transportConfigurationContainer.getRpcRequestDynamicMessageBuilder()), timeout);
transportConfigurationContainer.getRpcRequestDynamicMessageBuilder(), sessionInfo), timeout);
transportService.process(sessionInfo,
coapTransportAdaptor.convertToServerRpcRequest(sessionId, request),
new CoapNoOpCallback(exchange));
break;
case GET_ATTRIBUTES_REQUEST:
transportService.registerSyncSession(sessionInfo, getCoapSessionListener(exchange, coapTransportAdaptor,
transportConfigurationContainer.getRpcRequestDynamicMessageBuilder()), timeout);
transportConfigurationContainer.getRpcRequestDynamicMessageBuilder(), sessionInfo), timeout);
transportService.process(sessionInfo,
coapTransportAdaptor.convertToGetAttributes(sessionId, request),
new CoapNoOpCallback(exchange));
break;
case GET_FIRMWARE_REQUEST:
getOtaPackageCallback(sessionInfo, exchange, OtaPackageType.FIRMWARE);
break;
case GET_SOFTWARE_REQUEST:
getOtaPackageCallback(sessionInfo, exchange, OtaPackageType.SOFTWARE);
break;
}
} catch (AdaptorException e) {
log.trace("[{}] Failed to decode message: ", sessionId, e);
@ -366,29 +359,18 @@ public class CoapTransportResource extends AbstractCoapTransportResource {
return new UUID(sessionInfoProto.getSessionIdMSB(), sessionInfoProto.getSessionIdLSB());
}
private void getOtaPackageCallback(TransportProtos.SessionInfoProto sessionInfo, CoapExchange exchange, OtaPackageType firmwareType) {
TransportProtos.GetOtaPackageRequestMsg requestMsg = TransportProtos.GetOtaPackageRequestMsg.newBuilder()
.setTenantIdMSB(sessionInfo.getTenantIdMSB())
.setTenantIdLSB(sessionInfo.getTenantIdLSB())
.setDeviceIdMSB(sessionInfo.getDeviceIdMSB())
.setDeviceIdLSB(sessionInfo.getDeviceIdLSB())
.setType(firmwareType.name()).build();
transportContext.getTransportService().process(sessionInfo, requestMsg, new OtaPackageCallback(exchange));
}
private TransportProtos.SessionInfoProto lookupAsyncSessionInfo(String token) {
tokenToObserveNotificationSeqMap.remove(token);
return tokenToSessionInfoMap.remove(token);
private CoapObserveSessionInfo lookupAsyncSessionInfo(String token) {
return tokenToCoapSessionInfoMap.remove(token);
}
private void registerAsyncCoapSession(CoapExchange exchange, TransportProtos.SessionInfoProto sessionInfo, CoapTransportAdaptor coapTransportAdaptor, DynamicMessage.Builder rpcRequestDynamicMessageBuilder, String token) {
tokenToSessionInfoMap.putIfAbsent(token, sessionInfo);
transportService.registerAsyncSession(sessionInfo, getCoapSessionListener(exchange, coapTransportAdaptor, rpcRequestDynamicMessageBuilder));
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) {
return new CoapSessionListener(this, exchange, coapTransportAdaptor, rpcRequestDynamicMessageBuilder);
private CoapSessionListener getCoapSessionListener(CoapExchange exchange, CoapTransportAdaptor coapTransportAdaptor, DynamicMessage.Builder rpcRequestDynamicMessageBuilder, TransportProtos.SessionInfoProto sessionInfo) {
return new CoapSessionListener(this, exchange, coapTransportAdaptor, rpcRequestDynamicMessageBuilder, sessionInfo);
}
private String getTokenFromRequest(Request request) {
@ -470,69 +452,20 @@ public class CoapTransportResource extends AbstractCoapTransportResource {
}
}
private class OtaPackageCallback implements TransportServiceCallback<TransportProtos.GetOtaPackageResponseMsg> {
private final CoapExchange exchange;
OtaPackageCallback(CoapExchange exchange) {
this.exchange = exchange;
}
@Override
public void onSuccess(TransportProtos.GetOtaPackageResponseMsg msg) {
String title = exchange.getQueryParameter("title");
String version = exchange.getQueryParameter("version");
if (msg.getResponseStatus().equals(TransportProtos.ResponseStatus.SUCCESS)) {
String firmwareId = new UUID(msg.getOtaPackageIdMSB(), msg.getOtaPackageIdLSB()).toString();
if (msg.getTitle().equals(title) && msg.getVersion().equals(version)) {
String strChunkSize = exchange.getQueryParameter("size");
String strChunk = exchange.getQueryParameter("chunk");
int chunkSize = StringUtils.isEmpty(strChunkSize) ? 0 : Integer.parseInt(strChunkSize);
int chunk = StringUtils.isEmpty(strChunk) ? 0 : Integer.parseInt(strChunk);
exchange.respond(CoAP.ResponseCode.CONTENT, transportContext.getOtaPackageDataCache().get(firmwareId, chunkSize, chunk));
}
else if (firmwareId != null) {
sendOtaData(exchange, firmwareId);
} else {
exchange.respond(CoAP.ResponseCode.BAD_REQUEST);
}
} else {
exchange.respond(CoAP.ResponseCode.NOT_FOUND);
}
}
@Override
public void onError(Throwable e) {
log.warn("Failed to process request", e);
exchange.respond(CoAP.ResponseCode.INTERNAL_SERVER_ERROR);
}
}
private void sendOtaData(CoapExchange exchange, String firmwareId) {
Response response = new Response(CoAP.ResponseCode.CONTENT);
byte[] fwData = transportContext.getOtaPackageDataCache().get(firmwareId);
if (fwData != null && fwData.length > 0) {
response.setPayload(fwData);
if (exchange.getRequestOptions().getBlock2() != null) {
int chunkSize = exchange.getRequestOptions().getBlock2().getSzx();
boolean moreFlag = fwData.length > chunkSize;
response.getOptions().setBlock2(chunkSize, moreFlag, 0);
}
exchange.respond(response);
}
}
private static 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) {
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
@ -546,40 +479,39 @@ public class CoapTransportResource extends AbstractCoapTransportResource {
}
@Override
public void onAttributeUpdate(TransportProtos.AttributeUpdateNotificationMsg msg) {
public void onAttributeUpdate(UUID sessionId, TransportProtos.AttributeUpdateNotificationMsg msg) {
log.trace("[{}] Received attributes update notification to device", sessionId);
try {
exchange.respond(coapTransportAdaptor.convertToPublish(isConRequest(), msg));
} catch (AdaptorException e) {
log.trace("Failed to reply due to error", e);
exchange.respond(CoAP.ResponseCode.INTERNAL_SERVER_ERROR);
closeObserveRelationAndNotify(sessionId, CoAP.ResponseCode.INTERNAL_SERVER_ERROR);
closeAndDeregister();
}
}
@Override
public void onRemoteSessionCloseCommand(UUID sessionId, TransportProtos.SessionCloseNotificationProto sessionCloseNotification) {
log.trace("[{}] Received the remote command to close the session: {}", sessionId, sessionCloseNotification.getMessage());
Map<TransportProtos.SessionInfoProto, ObserveRelation> sessionToObserveRelationMap = coapTransportResource.getSessionInfoToObserveRelationMap();
if (coapTransportResource.getObserverCount() > 0 && !CollectionUtils.isEmpty(sessionToObserveRelationMap)) {
Set<TransportProtos.SessionInfoProto> observeSessions = sessionToObserveRelationMap.keySet();
Optional<TransportProtos.SessionInfoProto> observeSessionToClose = observeSessions.stream().filter(sessionInfoProto -> {
UUID observeSessionId = new UUID(sessionInfoProto.getSessionIdMSB(), sessionInfoProto.getSessionIdLSB());
return observeSessionId.equals(sessionId);
}).findFirst();
if (observeSessionToClose.isPresent()) {
TransportProtos.SessionInfoProto sessionInfoProto = observeSessionToClose.get();
ObserveRelation observeRelation = sessionToObserveRelationMap.get(sessionInfoProto);
coapTransportResource.clearAndNotifyObserveRelation(observeRelation, CoAP.ResponseCode.SERVICE_UNAVAILABLE);
}
}
closeObserveRelationAndNotify(sessionId, CoAP.ResponseCode.SERVICE_UNAVAILABLE);
closeAndDeregister();
}
@Override
public void onToDeviceRpcRequest(TransportProtos.ToDeviceRpcRequestMsg msg) {
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));
} catch (AdaptorException e) {
log.trace("Failed to reply due to error", e);
exchange.respond(CoAP.ResponseCode.INTERNAL_SERVER_ERROR);
closeObserveRelationAndNotify(sessionId, CoAP.ResponseCode.INTERNAL_SERVER_ERROR);
successful = false;
} finally {
coapTransportResource.transportService.process(sessionInfo, msg, !successful, TransportServiceCallback.EMPTY);
if (!successful) {
closeAndDeregister();
}
}
}
@ -596,6 +528,30 @@ public class CoapTransportResource extends AbstractCoapTransportResource {
private boolean isConRequest() {
return exchange.advanced().getRequest().isConfirmable();
}
private void closeObserveRelationAndNotify(UUID sessionId, CoAP.ResponseCode responseCode) {
Map<CoapObserveSessionInfo, ObserveRelation> sessionToObserveRelationMap = coapTransportResource.getCoapSessionInfoToObserveRelationMap();
if (coapTransportResource.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());
return observeSessionId.equals(sessionId);
}).findFirst();
if (observeSessionToClose.isPresent()) {
CoapObserveSessionInfo coapObserveSessionInfo = observeSessionToClose.get();
ObserveRelation observeRelation = sessionToObserveRelationMap.get(coapObserveSessionInfo);
coapTransportResource.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());
}
}
public class CoapResourceObserver implements ResourceObserver {
@ -620,7 +576,7 @@ public class CoapTransportResource extends AbstractCoapTransportResource {
public void addedObserveRelation(ObserveRelation relation) {
Request request = relation.getExchange().getRequest();
String token = getTokenFromRequest(request);
sessionInfoToObserveRelationMap.putIfAbsent(tokenToSessionInfoMap.get(token), relation);
sessionInfoToObserveRelationMap.putIfAbsent(tokenToCoapSessionInfoMap.get(token), relation);
log.trace("Added Observe relation for token: {}", token);
}
@ -628,8 +584,7 @@ public class CoapTransportResource extends AbstractCoapTransportResource {
public void removedObserveRelation(ObserveRelation relation) {
Request request = relation.getExchange().getRequest();
String token = getTokenFromRequest(request);
TransportProtos.SessionInfoProto session = tokenToSessionInfoMap.get(token);
sessionInfoToObserveRelationMap.remove(session);
sessionInfoToObserveRelationMap.remove(tokenToCoapSessionInfoMap.get(token));
log.trace("Relation removed for token: {}", token);
}
}
@ -640,7 +595,6 @@ public class CoapTransportResource extends AbstractCoapTransportResource {
transportService.deregisterSession(session);
rpcSubscriptions.remove(sessionId);
attributeSubscriptions.remove(sessionId);
sessionInfoToObserveRelationMap.remove(session);
}
private TransportConfigurationContainer getTransportConfigurationContainer(DeviceProfile deviceProfile) throws AdaptorException {
@ -706,4 +660,17 @@ public class CoapTransportResource extends AbstractCoapTransportResource {
this.jsonPayload = jsonPayload;
}
}
@Data
private static class CoapObserveSessionInfo {
private final TransportProtos.SessionInfoProto sessionInfoProto;
private final AtomicInteger observeNotificationCounter;
private CoapObserveSessionInfo(TransportProtos.SessionInfoProto sessionInfoProto) {
this.sessionInfoProto = sessionInfoProto;
this.observeNotificationCounter = new AtomicInteger(0);
}
}
}

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

@ -20,9 +20,10 @@ import org.eclipse.californium.core.CoapResource;
import org.eclipse.californium.core.CoapServer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.TbTransportService;
import org.thingsboard.server.coapserver.CoapServerService;
import org.thingsboard.server.coapserver.TbCoapServerComponent;
import org.thingsboard.server.common.data.TbTransportService;
import org.thingsboard.server.common.data.ota.OtaPackageType;
import org.thingsboard.server.transport.coap.efento.CoapEfentoTransportResource;
import javax.annotation.PostConstruct;
@ -59,6 +60,8 @@ public class CoapTransportService implements TbTransportService {
efento.add(efentoMeasurementsTransportResource);
coapServer.add(api);
coapServer.add(efento);
coapServer.add(new OtaPackageTransportResource(coapTransportContext, OtaPackageType.FIRMWARE));
coapServer.add(new OtaPackageTransportResource(coapTransportContext, OtaPackageType.SOFTWARE));
log.info("CoAP transport started!");
}

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

@ -0,0 +1,150 @@
/**
* 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.extern.slf4j.Slf4j;
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 java.util.List;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.ExecutorService;
@Slf4j
public class OtaPackageTransportResource extends AbstractCoapTransportResource {
private static final int ACCESS_TOKEN_POSITION = 2;
private final OtaPackageType otaPackageType;
public OtaPackageTransportResource(CoapTransportContext ctx, OtaPackageType otaPackageType) {
super(ctx, otaPackageType.getKeyPrefix());
this.otaPackageType = otaPackageType;
this.setObservable(true);
}
@Override
protected void processHandleGet(CoapExchange exchange) {
log.trace("Processing {}", exchange.advanced().getRequest());
exchange.accept();
Exchange advanced = exchange.advanced();
Request request = advanced.getRequest();
processAccessTokenRequest(exchange, request);
}
@Override
protected void processHandlePost(CoapExchange exchange) {
exchange.respond(CoAP.ResponseCode.METHOD_NOT_ALLOWED);
}
private void processAccessTokenRequest(CoapExchange exchange, Request request) {
Optional<DeviceTokenCredentials> credentials = decodeCredentials(request);
if (credentials.isEmpty()) {
exchange.respond(CoAP.ResponseCode.UNAUTHORIZED);
return;
}
transportService.process(DeviceTransportType.COAP, TransportProtos.ValidateDeviceTokenRequestMsg.newBuilder().setToken(credentials.get().getCredentialsId()).build(),
new CoapDeviceAuthCallback(transportContext, exchange, (sessionInfo, deviceProfile) -> {
getOtaPackageCallback(sessionInfo, exchange, otaPackageType);
}));
}
private void getOtaPackageCallback(TransportProtos.SessionInfoProto sessionInfo, CoapExchange exchange, OtaPackageType firmwareType) {
TransportProtos.GetOtaPackageRequestMsg requestMsg = TransportProtos.GetOtaPackageRequestMsg.newBuilder()
.setTenantIdMSB(sessionInfo.getTenantIdMSB())
.setTenantIdLSB(sessionInfo.getTenantIdLSB())
.setDeviceIdMSB(sessionInfo.getDeviceIdMSB())
.setDeviceIdLSB(sessionInfo.getDeviceIdLSB())
.setType(firmwareType.name()).build();
transportContext.getTransportService().process(sessionInfo, requestMsg, new OtaPackageCallback(exchange));
}
private Optional<DeviceTokenCredentials> decodeCredentials(Request request) {
List<String> uriPath = request.getOptions().getUriPath();
if (uriPath.size() == ACCESS_TOKEN_POSITION) {
return Optional.of(new DeviceTokenCredentials(uriPath.get(ACCESS_TOKEN_POSITION - 1)));
} else {
return Optional.empty();
}
}
@Override
public Resource getChild(String name) {
return this;
}
private class OtaPackageCallback implements TransportServiceCallback<TransportProtos.GetOtaPackageResponseMsg> {
private final CoapExchange exchange;
OtaPackageCallback(CoapExchange exchange) {
this.exchange = exchange;
}
@Override
public void onSuccess(TransportProtos.GetOtaPackageResponseMsg msg) {
String title = exchange.getQueryParameter("title");
String version = exchange.getQueryParameter("version");
if (msg.getResponseStatus().equals(TransportProtos.ResponseStatus.SUCCESS)) {
String firmwareId = new UUID(msg.getOtaPackageIdMSB(), msg.getOtaPackageIdLSB()).toString();
if ((title == null || msg.getTitle().equals(title)) && (version == null || msg.getVersion().equals(version))) {
String strChunkSize = exchange.getQueryParameter("size");
String strChunk = exchange.getQueryParameter("chunk");
int chunkSize = StringUtils.isEmpty(strChunkSize) ? 0 : Integer.parseInt(strChunkSize);
int chunk = StringUtils.isEmpty(strChunk) ? 0 : Integer.parseInt(strChunk);
respondOtaPackage(exchange, transportContext.getOtaPackageDataCache().get(firmwareId, chunkSize, chunk));
} else {
exchange.respond(CoAP.ResponseCode.BAD_REQUEST);
}
} else {
exchange.respond(CoAP.ResponseCode.NOT_FOUND);
}
}
@Override
public void onError(Throwable e) {
log.warn("Failed to process request", e);
exchange.respond(CoAP.ResponseCode.INTERNAL_SERVER_ERROR);
}
}
private void respondOtaPackage(CoapExchange exchange, byte[] data) {
Response response = new Response(CoAP.ResponseCode.CONTENT);
if (data != null && data.length > 0) {
response.setPayload(data);
if (exchange.getRequestOptions().getBlock2() != null) {
int chunkSize = exchange.getRequestOptions().getBlock2().getSzx();
boolean lastFlag = data.length <= chunkSize;
response.getOptions().setBlock2(chunkSize, lastFlag, 0);
}
transportContext.getExecutor().submit(() -> exchange.respond(response));
}
}
}

32
common/transport/http/src/main/java/org/thingsboard/server/transport/http/DeviceApiController.java

@ -17,6 +17,7 @@ package org.thingsboard.server.transport.http;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
@ -34,9 +35,10 @@ import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.request.async.DeferredResult;
import org.thingsboard.server.common.data.DeviceTransportType;
import org.thingsboard.server.common.data.ota.OtaPackageType;
import org.thingsboard.server.common.data.TbTransportService;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.ota.OtaPackageType;
import org.thingsboard.server.common.data.rpc.RpcStatus;
import org.thingsboard.server.common.transport.SessionMsgListener;
import org.thingsboard.server.common.transport.TransportContext;
import org.thingsboard.server.common.transport.TransportService;
@ -95,7 +97,9 @@ public class DeviceApiController implements TbTransportService {
request.addAllSharedAttributeNames(sharedKeySet);
}
TransportService transportService = transportContext.getTransportService();
transportService.registerSyncSession(sessionInfo, new HttpSessionListener(responseWriter), transportContext.getDefaultTimeout());
transportService.registerSyncSession(sessionInfo,
new HttpSessionListener(responseWriter, transportContext.getTransportService(), sessionInfo),
transportContext.getDefaultTimeout());
transportService.process(sessionInfo, request.build(), new SessionCloseOnErrorCallback(transportService, sessionInfo));
}));
return responseWriter;
@ -151,7 +155,8 @@ public class DeviceApiController implements TbTransportService {
transportContext.getTransportService().process(DeviceTransportType.DEFAULT, ValidateDeviceTokenRequestMsg.newBuilder().setToken(deviceToken).build(),
new DeviceAuthCallback(transportContext, responseWriter, sessionInfo -> {
TransportService transportService = transportContext.getTransportService();
transportService.registerSyncSession(sessionInfo, new HttpSessionListener(responseWriter),
transportService.registerSyncSession(sessionInfo,
new HttpSessionListener(responseWriter, transportContext.getTransportService(), sessionInfo),
timeout == 0 ? transportContext.getDefaultTimeout() : timeout);
transportService.process(sessionInfo, SubscribeToRPCMsg.getDefaultInstance(),
new SessionCloseOnErrorCallback(transportService, sessionInfo));
@ -181,7 +186,9 @@ public class DeviceApiController implements TbTransportService {
new DeviceAuthCallback(transportContext, responseWriter, sessionInfo -> {
JsonObject request = new JsonParser().parse(json).getAsJsonObject();
TransportService transportService = transportContext.getTransportService();
transportService.registerSyncSession(sessionInfo, new HttpSessionListener(responseWriter), transportContext.getDefaultTimeout());
transportService.registerSyncSession(sessionInfo,
new HttpSessionListener(responseWriter, transportContext.getTransportService(), sessionInfo),
transportContext.getDefaultTimeout());
transportService.process(sessionInfo, ToServerRpcRequestMsg.newBuilder().setRequestId(0)
.setMethodName(request.get("method").getAsString())
.setParams(request.get("params").toString()).build(),
@ -198,7 +205,8 @@ public class DeviceApiController implements TbTransportService {
transportContext.getTransportService().process(DeviceTransportType.DEFAULT, ValidateDeviceTokenRequestMsg.newBuilder().setToken(deviceToken).build(),
new DeviceAuthCallback(transportContext, responseWriter, sessionInfo -> {
TransportService transportService = transportContext.getTransportService();
transportService.registerSyncSession(sessionInfo, new HttpSessionListener(responseWriter),
transportService.registerSyncSession(sessionInfo,
new HttpSessionListener(responseWriter, transportContext.getTransportService(), sessionInfo),
timeout == 0 ? transportContext.getDefaultTimeout() : timeout);
transportService.process(sessionInfo, SubscribeToAttributeUpdatesMsg.getDefaultInstance(),
new SessionCloseOnErrorCallback(transportService, sessionInfo));
@ -372,13 +380,12 @@ public class DeviceApiController implements TbTransportService {
}
}
@RequiredArgsConstructor
private static class HttpSessionListener implements SessionMsgListener {
private final DeferredResult<ResponseEntity> responseWriter;
HttpSessionListener(DeferredResult<ResponseEntity> responseWriter) {
this.responseWriter = responseWriter;
}
private final TransportService transportService;
private final SessionInfoProto sessionInfo;
@Override
public void onGetAttributesResponse(GetAttributeResponseMsg msg) {
@ -386,7 +393,8 @@ public class DeviceApiController implements TbTransportService {
}
@Override
public void onAttributeUpdate(AttributeUpdateNotificationMsg msg) {
public void onAttributeUpdate(UUID sessionId, AttributeUpdateNotificationMsg msg) {
log.trace("[{}] Received attributes update notification to device", sessionId);
responseWriter.setResult(new ResponseEntity<>(JsonConverter.toJson(msg).toString(), HttpStatus.OK));
}
@ -397,8 +405,10 @@ public class DeviceApiController implements TbTransportService {
}
@Override
public void onToDeviceRpcRequest(ToDeviceRpcRequestMsg msg) {
public void onToDeviceRpcRequest(UUID sessionId, ToDeviceRpcRequestMsg msg) {
log.trace("[{}] Received RPC command to device", sessionId);
responseWriter.setResult(new ResponseEntity<>(JsonConverter.toJson(msg, true).toString(), HttpStatus.OK));
transportService.process(sessionInfo, msg, false, TransportServiceCallback.EMPTY);
}
@Override

3
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/secure/LwM2MBootstrapConfig.java

@ -21,10 +21,11 @@ import org.eclipse.leshan.core.request.BindingMode;
import org.eclipse.leshan.core.util.Hex;
import org.eclipse.leshan.server.bootstrap.BootstrapConfig;
import java.io.Serializable;
import java.nio.charset.StandardCharsets;
@Data
public class LwM2MBootstrapConfig {
public class LwM2MBootstrapConfig implements Serializable {
/*
interface BootstrapSecurityConfig
servers: BootstrapServersSecurityConfig,

71
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/secure/LwM2MBootstrapSecurityStore.java

@ -15,9 +15,6 @@
*/
package org.thingsboard.server.transport.lwm2m.bootstrap.secure;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.gson.JsonObject;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.leshan.core.SecurityMode;
import org.eclipse.leshan.core.util.Hex;
@ -29,13 +26,14 @@ import org.eclipse.leshan.server.security.BootstrapSecurityStore;
import org.eclipse.leshan.server.security.SecurityInfo;
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
import org.springframework.stereotype.Service;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.device.data.lwm2m.BootstrapConfiguration;
import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.transport.lwm2m.secure.TbLwM2MSecurityInfo;
import org.thingsboard.server.transport.lwm2m.secure.LwM2mCredentialsSecurityInfoValidator;
import org.thingsboard.server.transport.lwm2m.secure.TbLwM2MSecurityInfo;
import org.thingsboard.server.transport.lwm2m.server.LwM2mSessionMsgListener;
import org.thingsboard.server.transport.lwm2m.server.LwM2mTransportContext;
import org.thingsboard.server.transport.lwm2m.server.LwM2mTransportServerHelper;
import org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil;
import java.io.IOException;
import java.security.GeneralSecurityException;
@ -43,13 +41,11 @@ import java.util.Collections;
import java.util.Iterator;
import java.util.UUID;
import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.BOOTSTRAP_SERVER;
import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.LOG_LW2M_ERROR;
import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.LOG_LW2M_INFO;
import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.LOG_LW2M_TELEMETRY;
import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.LWM2M_SERVER;
import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.SERVERS;
import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.LOG_LWM2M_ERROR;
import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.LOG_LWM2M_INFO;
import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.LOG_LWM2M_TELEMETRY;
import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.getBootstrapParametersFromThingsboard;
import static org.thingsboard.server.transport.lwm2m.server.uplink.LwM2mTypeServer.BOOTSTRAP;
@Slf4j
@Service("LwM2MBootstrapSecurityStore")
@ -72,7 +68,7 @@ public class LwM2MBootstrapSecurityStore implements BootstrapSecurityStore {
@Override
public Iterator<SecurityInfo> getAllByEndpoint(String endPoint) {
TbLwM2MSecurityInfo store = lwM2MCredentialsSecurityInfoValidator.getEndpointSecurityInfoByCredentialsId(endPoint, LwM2mTransportUtil.LwM2mTypeServer.BOOTSTRAP);
TbLwM2MSecurityInfo store = lwM2MCredentialsSecurityInfoValidator.getEndpointSecurityInfoByCredentialsId(endPoint, BOOTSTRAP);
if (store.getBootstrapCredentialConfig() != null && store.getSecurityMode() != null) {
/* add value to store from BootstrapJson */
this.setBootstrapConfigScurityInfo(store);
@ -96,7 +92,7 @@ public class LwM2MBootstrapSecurityStore implements BootstrapSecurityStore {
@Override
public SecurityInfo getByIdentity(String identity) {
TbLwM2MSecurityInfo store = lwM2MCredentialsSecurityInfoValidator.getEndpointSecurityInfoByCredentialsId(identity, LwM2mTransportUtil.LwM2mTypeServer.BOOTSTRAP);
TbLwM2MSecurityInfo store = lwM2MCredentialsSecurityInfoValidator.getEndpointSecurityInfoByCredentialsId(identity, BOOTSTRAP);
if (store.getBootstrapCredentialConfig() != null && store.getSecurityMode() != null) {
/* add value to store from BootstrapJson */
this.setBootstrapConfigScurityInfo(store);
@ -151,35 +147,30 @@ public class LwM2MBootstrapSecurityStore implements BootstrapSecurityStore {
}
private LwM2MBootstrapConfig getParametersBootstrap(TbLwM2MSecurityInfo store) {
try {
LwM2MBootstrapConfig lwM2MBootstrapConfig = store.getBootstrapCredentialConfig();
if (lwM2MBootstrapConfig != null) {
ObjectMapper mapper = new ObjectMapper();
JsonObject bootstrapObject = getBootstrapParametersFromThingsboard(store.getDeviceProfile());
lwM2MBootstrapConfig.servers = mapper.readValue(bootstrapObject.get(SERVERS).toString(), LwM2MBootstrapServers.class);
LwM2MServerBootstrap profileServerBootstrap = mapper.readValue(bootstrapObject.get(BOOTSTRAP_SERVER).toString(), LwM2MServerBootstrap.class);
LwM2MServerBootstrap profileLwm2mServer = mapper.readValue(bootstrapObject.get(LWM2M_SERVER).toString(), LwM2MServerBootstrap.class);
UUID sessionUUiD = UUID.randomUUID();
TransportProtos.SessionInfoProto sessionInfo = helper.getValidateSessionInfo(store.getMsg(), sessionUUiD.getMostSignificantBits(), sessionUUiD.getLeastSignificantBits());
context.getTransportService().registerAsyncSession(sessionInfo, new LwM2mSessionMsgListener(null, sessionInfo));
if (this.getValidatedSecurityMode(lwM2MBootstrapConfig.bootstrapServer, profileServerBootstrap, lwM2MBootstrapConfig.lwm2mServer, profileLwm2mServer)) {
lwM2MBootstrapConfig.bootstrapServer = new LwM2MServerBootstrap(lwM2MBootstrapConfig.bootstrapServer, profileServerBootstrap);
lwM2MBootstrapConfig.lwm2mServer = new LwM2MServerBootstrap(lwM2MBootstrapConfig.lwm2mServer, profileLwm2mServer);
String logMsg = String.format("%s: getParametersBootstrap: %s Access connect client with bootstrap server.", LOG_LW2M_INFO, store.getEndpoint());
helper.sendParametersOnThingsboardTelemetry(helper.getKvStringtoThingsboard(LOG_LW2M_TELEMETRY, logMsg), sessionInfo);
return lwM2MBootstrapConfig;
} else {
log.error(" [{}] Different values SecurityMode between of client and profile.", store.getEndpoint());
log.error("{} getParametersBootstrap: [{}] Different values SecurityMode between of client and profile.", LOG_LW2M_ERROR, store.getEndpoint());
String logMsg = String.format("%s: getParametersBootstrap: %s Different values SecurityMode between of client and profile.", LOG_LW2M_ERROR, store.getEndpoint());
helper.sendParametersOnThingsboardTelemetry(helper.getKvStringtoThingsboard(LOG_LW2M_TELEMETRY, logMsg), sessionInfo);
return null;
}
LwM2MBootstrapConfig lwM2MBootstrapConfig = store.getBootstrapCredentialConfig();
if (lwM2MBootstrapConfig != null) {
BootstrapConfiguration bootstrapObject = getBootstrapParametersFromThingsboard(store.getDeviceProfile());
lwM2MBootstrapConfig.servers = JacksonUtil.fromString(JacksonUtil.toString(bootstrapObject.getServers()), LwM2MBootstrapServers.class);
LwM2MServerBootstrap profileServerBootstrap = JacksonUtil.fromString(JacksonUtil.toString(bootstrapObject.getBootstrapServer()), LwM2MServerBootstrap.class);
LwM2MServerBootstrap profileLwm2mServer = JacksonUtil.fromString(JacksonUtil.toString(bootstrapObject.getLwm2mServer()), LwM2MServerBootstrap.class);
UUID sessionUUiD = UUID.randomUUID();
TransportProtos.SessionInfoProto sessionInfo = helper.getValidateSessionInfo(store.getMsg(), sessionUUiD.getMostSignificantBits(), sessionUUiD.getLeastSignificantBits());
context.getTransportService().registerAsyncSession(sessionInfo, new LwM2mSessionMsgListener(null, null, null, sessionInfo, context.getTransportService()));
if (this.getValidatedSecurityMode(lwM2MBootstrapConfig.bootstrapServer, profileServerBootstrap, lwM2MBootstrapConfig.lwm2mServer, profileLwm2mServer)) {
lwM2MBootstrapConfig.bootstrapServer = new LwM2MServerBootstrap(lwM2MBootstrapConfig.bootstrapServer, profileServerBootstrap);
lwM2MBootstrapConfig.lwm2mServer = new LwM2MServerBootstrap(lwM2MBootstrapConfig.lwm2mServer, profileLwm2mServer);
String logMsg = String.format("%s: getParametersBootstrap: %s Access connect client with bootstrap server.", LOG_LWM2M_INFO, store.getEndpoint());
helper.sendParametersOnThingsboardTelemetry(helper.getKvStringtoThingsboard(LOG_LWM2M_TELEMETRY, logMsg), sessionInfo);
return lwM2MBootstrapConfig;
} else {
log.error(" [{}] Different values SecurityMode between of client and profile.", store.getEndpoint());
log.error("{} getParametersBootstrap: [{}] Different values SecurityMode between of client and profile.", LOG_LWM2M_ERROR, store.getEndpoint());
String logMsg = String.format("%s: getParametersBootstrap: %s Different values SecurityMode between of client and profile.", LOG_LWM2M_ERROR, store.getEndpoint());
helper.sendParametersOnThingsboardTelemetry(helper.getKvStringtoThingsboard(LOG_LWM2M_TELEMETRY, logMsg), sessionInfo);
return null;
}
} catch (JsonProcessingException e) {
log.error("Unable to decode Json or Certificate for [{}] [{}]", store.getEndpoint(), e.getMessage());
return null;
}
log.error("Unable to decode Json or Certificate for [{}]", store.getEndpoint());
return null;
}

47
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/config/LwM2MTransportServerConfig.java

@ -57,36 +57,28 @@ public class LwM2MTransportServerConfig implements LwM2MSecureServerConfig {
private boolean recommendedSupportedGroups;
@Getter
@Value("${transport.lwm2m.response_pool_size:}")
private int responsePoolSize;
@Value("${transport.lwm2m.downlink_pool_size:}")
private int downlinkPoolSize;
@Getter
@Value("${transport.lwm2m.registered_pool_size:}")
private int registeredPoolSize;
@Value("${transport.lwm2m.uplink_pool_size:}")
private int uplinkPoolSize;
@Getter
@Value("${transport.lwm2m.registration_store_pool_size:}")
private int registrationStorePoolSize;
@Value("${transport.lwm2m.ota_pool_size:}")
private int otaPoolSize;
@Getter
@Value("${transport.lwm2m.clean_period_in_sec:}")
private int cleanPeriodInSec;
@Getter
@Value("${transport.lwm2m.update_registered_pool_size:}")
private int updateRegisteredPoolSize;
@Getter
@Value("${transport.lwm2m.un_registered_pool_size:}")
private int unRegisteredPoolSize;
@Getter
@Value("${transport.lwm2m.security.key_store_type:}")
private String keyStoreType;
@Getter
@Value("${transport.lwm2m.security.key_store:}")
private String keyStorePathFile;
private String keyStoreFilePath;
@Getter
@Setter
@ -149,14 +141,27 @@ public class LwM2MTransportServerConfig implements LwM2MSecureServerConfig {
public void init() {
URI uri = null;
try {
uri = Resources.getResource(keyStorePathFile).toURI();
log.error("URI: {}", uri);
File keyStoreFile = new File(uri);
InputStream inKeyStore = new FileInputStream(keyStoreFile);
InputStream keyStoreInputStream;
File keyStoreFile = new File(keyStoreFilePath);
if (keyStoreFile.exists()) {
log.info("Reading key store from file {}", keyStoreFilePath);
keyStoreInputStream = new FileInputStream(keyStoreFile);
} else {
InputStream classPathStream = this.getClass().getClassLoader().getResourceAsStream(keyStoreFilePath);
if (classPathStream != null) {
log.info("Reading key store from class path {}", keyStoreFilePath);
keyStoreInputStream = classPathStream;
} else {
uri = Resources.getResource(keyStoreFilePath).toURI();
log.info("Reading key store from URI {}", keyStoreFilePath);
keyStoreInputStream = new FileInputStream(new File(uri));
}
}
keyStoreValue = KeyStore.getInstance(keyStoreType);
keyStoreValue.load(inKeyStore, keyStorePassword == null ? null : keyStorePassword.toCharArray());
keyStoreValue.load(keyStoreInputStream, keyStorePassword == null ? null : keyStorePassword.toCharArray());
} catch (Exception e) {
log.info("Unable to lookup LwM2M keystore. Reason: {}, {}" , uri, e.getMessage());
log.info("Unable to lookup LwM2M keystore. Reason: {}, {}", uri, e.getMessage());
}
}
}

66
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/config/LwM2mVersion.java

@ -0,0 +1,66 @@
/**
* 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.lwm2m.config;
import lombok.Getter;
import org.eclipse.leshan.core.LwM2m.Version;
import org.eclipse.leshan.core.request.ContentFormat;
public enum LwM2mVersion {
VERSION_1_0(0, Version.V1_0, ContentFormat.TLV),
VERSION_1_1(1, Version.V1_1, ContentFormat.TEXT);
@Getter
private final int code;
@Getter
private final Version version;
@Getter
private final ContentFormat contentFormat;
LwM2mVersion(int code, Version version, ContentFormat contentFormat) {
this.code = code;
this.version = version;
this.contentFormat = contentFormat;
}
public static LwM2mVersion fromVersion(Version version) {
for (LwM2mVersion to : LwM2mVersion.values()) {
if (to.version.equals(version)) {
return to;
}
}
throw new IllegalArgumentException(String.format("Unsupported typeLwM2mVersion type : %s", version));
}
public static LwM2mVersion fromVersionStr(String versionStr) {
for (LwM2mVersion to : LwM2mVersion.values()) {
if (to.version.toString().equals(versionStr)) {
return to;
}
}
throw new IllegalArgumentException(String.format("Unsupported contentFormatLwM2mVersion version : %s", versionStr));
}
public static LwM2mVersion fromCode(int code) {
for (LwM2mVersion to : LwM2mVersion.values()) {
if (to.code == code) {
return to;
}
}
throw new IllegalArgumentException(String.format("Unsupported codeLwM2mVersion code : %d", code));
}
}

29
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/secure/LwM2mCredentialsSecurityInfoValidator.java

@ -33,7 +33,8 @@ import org.thingsboard.server.queue.util.TbLwM2mTransportComponent;
import org.thingsboard.server.transport.lwm2m.config.LwM2MTransportServerConfig;
import org.thingsboard.server.transport.lwm2m.secure.credentials.LwM2MCredentials;
import org.thingsboard.server.transport.lwm2m.server.LwM2mTransportContext;
import org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil;
import org.thingsboard.server.transport.lwm2m.server.client.LwM2MAuthException;
import org.thingsboard.server.transport.lwm2m.server.uplink.LwM2mTypeServer;
import java.io.IOException;
import java.security.GeneralSecurityException;
@ -45,6 +46,7 @@ import static org.eclipse.leshan.core.SecurityMode.NO_SEC;
import static org.eclipse.leshan.core.SecurityMode.PSK;
import static org.eclipse.leshan.core.SecurityMode.RPK;
import static org.eclipse.leshan.core.SecurityMode.X509;
import static org.thingsboard.server.transport.lwm2m.server.uplink.LwM2mTypeServer.BOOTSTRAP;
@Slf4j
@Component
@ -55,13 +57,15 @@ public class LwM2mCredentialsSecurityInfoValidator {
private final LwM2mTransportContext context;
private final LwM2MTransportServerConfig config;
public TbLwM2MSecurityInfo getEndpointSecurityInfoByCredentialsId(String credentialsId, LwM2mTransportUtil.LwM2mTypeServer keyValue) {
public TbLwM2MSecurityInfo getEndpointSecurityInfoByCredentialsId(String credentialsId, LwM2mTypeServer keyValue) {
CountDownLatch latch = new CountDownLatch(1);
final TbLwM2MSecurityInfo[] resultSecurityStore = new TbLwM2MSecurityInfo[1];
log.trace("Validating credentials [{}]", credentialsId);
context.getTransportService().process(ValidateDeviceLwM2MCredentialsRequestMsg.newBuilder().setCredentialsId(credentialsId).build(),
new TransportServiceCallback<>() {
@Override
public void onSuccess(ValidateDeviceCredentialsResponse msg) {
log.trace("Validated credentials: [{}] [{}]", credentialsId, msg);
String credentialsBody = msg.getCredentials();
resultSecurityStore[0] = createSecurityInfo(credentialsId, credentialsBody, keyValue);
resultSecurityStore[0].setMsg(msg);
@ -81,7 +85,14 @@ public class LwM2mCredentialsSecurityInfoValidator {
} catch (InterruptedException e) {
log.error("Failed to await credentials!", e);
}
return resultSecurityStore[0];
TbLwM2MSecurityInfo securityInfo = resultSecurityStore[0];
if (securityInfo.getSecurityMode() == null) {
throw new LwM2MAuthException();
}
return securityInfo;
}
/**
@ -91,11 +102,11 @@ public class LwM2mCredentialsSecurityInfoValidator {
* @param keyValue -
* @return SecurityInfo
*/
private TbLwM2MSecurityInfo createSecurityInfo(String endpoint, String jsonStr, LwM2mTransportUtil.LwM2mTypeServer keyValue) {
private TbLwM2MSecurityInfo createSecurityInfo(String endpoint, String jsonStr, LwM2mTypeServer keyValue) {
TbLwM2MSecurityInfo result = new TbLwM2MSecurityInfo();
LwM2MCredentials credentials = JacksonUtil.fromString(jsonStr, LwM2MCredentials.class);
if (credentials != null) {
if (keyValue.equals(LwM2mTransportUtil.LwM2mTypeServer.BOOTSTRAP)) {
if (keyValue.equals(BOOTSTRAP)) {
result.setBootstrapCredentialConfig(credentials.getBootstrap());
if (LwM2MSecurityMode.PSK.equals(credentials.getClient().getSecurityConfigClientMode())) {
PSKClientCredentials pskClientConfig = (PSKClientCredentials) credentials.getClient();
@ -135,10 +146,10 @@ public class LwM2mCredentialsSecurityInfoValidator {
PSKClientCredentials pskConfig = (PSKClientCredentials) clientCredentialsConfig;
if (StringUtils.isNotEmpty(pskConfig.getIdentity())) {
try {
if (pskConfig.getKey() != null && pskConfig.getKey().length > 0) {
if (pskConfig.getDecodedKey() != null && pskConfig.getDecodedKey().length > 0) {
endpoint = StringUtils.isNotEmpty(pskConfig.getEndpoint()) ? pskConfig.getEndpoint() : endpoint;
if (endpoint != null && !endpoint.isEmpty()) {
result.setSecurityInfo(SecurityInfo.newPreSharedKeyInfo(endpoint, pskConfig.getIdentity(), pskConfig.getKey()));
result.setSecurityInfo(SecurityInfo.newPreSharedKeyInfo(endpoint, pskConfig.getIdentity(), pskConfig.getDecodedKey()));
result.setSecurityMode(PSK);
}
}
@ -153,8 +164,8 @@ public class LwM2mCredentialsSecurityInfoValidator {
private void createClientSecurityInfoRPK(TbLwM2MSecurityInfo result, String endpoint, LwM2MClientCredentials clientCredentialsConfig) {
RPKClientCredentials rpkConfig = (RPKClientCredentials) clientCredentialsConfig;
try {
if (rpkConfig.getKey() != null) {
PublicKey key = SecurityUtil.publicKey.decode(rpkConfig.getKey());
if (rpkConfig.getDecodedKey() != null) {
PublicKey key = SecurityUtil.publicKey.decode(rpkConfig.getDecodedKey());
result.setSecurityInfo(SecurityInfo.newRawPublicKeyInfo(endpoint, key));
result.setSecurityMode(RPK);
} else {

11
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/secure/TbLwM2MAuthorizer.java

@ -16,6 +16,7 @@
package org.thingsboard.server.transport.lwm2m.secure;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.leshan.core.request.Identity;
import org.eclipse.leshan.core.request.UplinkRequest;
import org.eclipse.leshan.server.registration.Registration;
@ -24,14 +25,15 @@ import org.eclipse.leshan.server.security.SecurityChecker;
import org.eclipse.leshan.server.security.SecurityInfo;
import org.springframework.stereotype.Component;
import org.thingsboard.server.queue.util.TbLwM2mTransportComponent;
import org.thingsboard.server.transport.lwm2m.server.client.LwM2MAuthException;
import org.thingsboard.server.transport.lwm2m.server.client.LwM2mClientContext;
import org.thingsboard.server.transport.lwm2m.server.store.TbLwM2MDtlsSessionStore;
import org.thingsboard.server.transport.lwm2m.server.store.TbLwM2mSecurityStore;
import org.thingsboard.server.transport.lwm2m.server.store.TbSecurityStore;
@Component
@RequiredArgsConstructor
@TbLwM2mTransportComponent
@Slf4j
public class TbLwM2MAuthorizer implements Authorizer {
private final TbLwM2MDtlsSessionStore sessionStorage;
@ -57,7 +59,12 @@ public class TbLwM2MAuthorizer implements Authorizer {
}
SecurityInfo expectedSecurityInfo = null;
if (securityStore != null) {
expectedSecurityInfo = securityStore.getByEndpoint(registration.getEndpoint());
try {
expectedSecurityInfo = securityStore.getByEndpoint(registration.getEndpoint());
} catch (LwM2MAuthException e) {
log.warn("Registration failed: FORBIDDEN, endpointId: [{}]", registration.getEndpoint());
return null;
}
}
if (securityChecker.checkSecurityInfo(registration.getEndpoint(), senderIdentity, expectedSecurityInfo)) {
return registration;

17
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/secure/TbLwM2MDtlsCertificateVerifier.java

@ -42,9 +42,9 @@ import org.thingsboard.server.common.transport.util.SslUtil;
import org.thingsboard.server.queue.util.TbLwM2mTransportComponent;
import org.thingsboard.server.transport.lwm2m.config.LwM2MTransportServerConfig;
import org.thingsboard.server.transport.lwm2m.secure.credentials.LwM2MCredentials;
import org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil;
import org.thingsboard.server.transport.lwm2m.server.store.TbEditableSecurityStore;
import org.thingsboard.server.transport.lwm2m.server.client.LwM2MAuthException;
import org.thingsboard.server.transport.lwm2m.server.store.TbLwM2MDtlsSessionStore;
import org.thingsboard.server.transport.lwm2m.server.store.TbMainSecurityStore;
import javax.annotation.PostConstruct;
import javax.security.auth.x500.X500Principal;
@ -57,6 +57,8 @@ import java.security.cert.X509Certificate;
import java.util.Arrays;
import java.util.List;
import static org.thingsboard.server.transport.lwm2m.server.uplink.LwM2mTypeServer.CLIENT;
@Slf4j
@Component
@TbLwM2mTransportComponent
@ -66,7 +68,7 @@ public class TbLwM2MDtlsCertificateVerifier implements NewAdvancedCertificateVer
private final TbLwM2MDtlsSessionStore sessionStorage;
private final LwM2MTransportServerConfig config;
private final LwM2mCredentialsSecurityInfoValidator securityInfoValidator;
private final TbEditableSecurityStore securityStore;
private final TbMainSecurityStore securityStore;
@SuppressWarnings("deprecation")
private StaticCertificateVerifier staticCertificateVerifier;
@ -117,7 +119,12 @@ public class TbLwM2MDtlsCertificateVerifier implements NewAdvancedCertificateVer
String strCert = SslUtil.getCertificateString(cert);
String sha3Hash = EncryptionUtil.getSha3Hash(strCert);
TbLwM2MSecurityInfo securityInfo = securityInfoValidator.getEndpointSecurityInfoByCredentialsId(sha3Hash, LwM2mTransportUtil.LwM2mTypeServer.CLIENT);
TbLwM2MSecurityInfo securityInfo;
try {
securityInfo = securityInfoValidator.getEndpointSecurityInfoByCredentialsId(sha3Hash, CLIENT);
} catch (LwM2MAuthException e) {
securityInfo = null;
}
ValidateDeviceCredentialsResponse msg = securityInfo != null ? securityInfo.getMsg() : null;
if (msg != null && org.thingsboard.server.common.data.StringUtils.isNotEmpty(msg.getCredentials())) {
LwM2MCredentials credentials = JacksonUtil.fromString(msg.getCredentials(), LwM2MCredentials.class);
@ -133,7 +140,7 @@ public class TbLwM2MDtlsCertificateVerifier implements NewAdvancedCertificateVer
if (msg.hasDeviceInfo() && deviceProfile != null) {
sessionStorage.put(endpoint, new TbX509DtlsSessionInfo(cert.getSubjectX500Principal().getName(), msg));
try {
securityStore.put(securityInfo);
securityStore.putX509(securityInfo);
} catch (NonUniqueSecurityInfoException e) {
log.trace("Failed to add security info: {}", securityInfo, e);
}

4
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/secure/TbLwM2MSecurityInfo.java

@ -23,8 +23,10 @@ import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.transport.auth.ValidateDeviceCredentialsResponse;
import org.thingsboard.server.transport.lwm2m.bootstrap.secure.LwM2MBootstrapConfig;
import java.io.Serializable;
@Data
public class TbLwM2MSecurityInfo {
public class TbLwM2MSecurityInfo implements Serializable {
private ValidateDeviceCredentialsResponse msg;
private SecurityInfo securityInfo;
private SecurityMode securityMode;

4
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/secure/TbX509DtlsSessionInfo.java

@ -18,8 +18,10 @@ package org.thingsboard.server.transport.lwm2m.secure;
import lombok.Data;
import org.thingsboard.server.common.transport.auth.ValidateDeviceCredentialsResponse;
import java.io.Serializable;
@Data
public class TbX509DtlsSessionInfo {
public class TbX509DtlsSessionInfo implements Serializable {
private final String x509CommonName;
private final ValidateDeviceCredentialsResponse credentials;

1453
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/DefaultLwM2MTransportMsgHandler.java

File diff suppressed because it is too large

12
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/DefaultLwM2mTransportService.java

@ -26,8 +26,8 @@ import org.eclipse.leshan.server.californium.LeshanServer;
import org.eclipse.leshan.server.californium.LeshanServerBuilder;
import org.eclipse.leshan.server.californium.registration.CaliforniumRegistrationStore;
import org.eclipse.leshan.server.model.LwM2mModelProvider;
import org.eclipse.leshan.server.security.EditableSecurityStore;
import org.springframework.stereotype.Component;
import org.thingsboard.server.cache.ota.OtaPackageDataCache;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.queue.util.TbLwM2mTransportComponent;
import org.thingsboard.server.transport.lwm2m.config.LwM2MTransportServerConfig;
@ -35,8 +35,8 @@ import org.thingsboard.server.transport.lwm2m.secure.LWM2MGenerationPSkRPkECC;
import org.thingsboard.server.transport.lwm2m.secure.TbLwM2MAuthorizer;
import org.thingsboard.server.transport.lwm2m.secure.TbLwM2MDtlsCertificateVerifier;
import org.thingsboard.server.transport.lwm2m.server.client.LwM2mClientContext;
import org.thingsboard.server.transport.lwm2m.server.store.TbEditableSecurityStore;
import org.thingsboard.server.transport.lwm2m.server.store.TbSecurityStore;
import org.thingsboard.server.transport.lwm2m.server.uplink.DefaultLwM2MUplinkMsgHandler;
import org.thingsboard.server.transport.lwm2m.utils.LwM2mValueConverterImpl;
import javax.annotation.PostConstruct;
@ -65,7 +65,7 @@ import static org.eclipse.californium.scandium.dtls.cipher.CipherSuite.TLS_ECDHE
import static org.eclipse.californium.scandium.dtls.cipher.CipherSuite.TLS_PSK_WITH_AES_128_CBC_SHA256;
import static org.eclipse.californium.scandium.dtls.cipher.CipherSuite.TLS_PSK_WITH_AES_128_CCM_8;
import static org.thingsboard.server.transport.lwm2m.server.LwM2mNetworkConfig.getCoapConfig;
import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.FIRMWARE_UPDATE_COAP_RECOURSE;
import static org.thingsboard.server.transport.lwm2m.server.ota.DefaultLwM2MOtaUpdateService.FIRMWARE_UPDATE_COAP_RESOURCE;
@Slf4j
@Component
@ -81,7 +81,8 @@ public class DefaultLwM2mTransportService implements LwM2MTransportService {
private final LwM2mTransportContext context;
private final LwM2MTransportServerConfig config;
private final LwM2mTransportServerHelper helper;
private final DefaultLwM2MTransportMsgHandler handler;
private final OtaPackageDataCache otaPackageDataCache;
private final DefaultLwM2MUplinkMsgHandler handler;
private final CaliforniumRegistrationStore registrationStore;
private final TbSecurityStore securityStore;
private final LwM2mClientContext lwM2mClientContext;
@ -104,8 +105,7 @@ public class DefaultLwM2mTransportService implements LwM2MTransportService {
* "coap://host:port/{path}/{token}/{nameFile}"
*/
LwM2mTransportCoapResource otaCoapResource = new LwM2mTransportCoapResource(handler, FIRMWARE_UPDATE_COAP_RECOURSE);
LwM2mTransportCoapResource otaCoapResource = new LwM2mTransportCoapResource(otaPackageDataCache, FIRMWARE_UPDATE_COAP_RESOURCE);
this.server.coap().getServer().add(otaCoapResource);
this.startLhServer();
this.context.setServer(server);

6
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mNetworkConfig.java

@ -84,10 +84,10 @@ public class LwM2mNetworkConfig {
Create new instance of udp endpoint context matcher.
Params:
checkAddress
– true with address check, (STRICT, UDP)
– true with address check, (STRICT, UDP) - if port Registration of client is changed - it is bad
- false, without
*/
coapConfig.setString(NetworkConfig.Keys.RESPONSE_MATCHING, "STRICT");
coapConfig.setString(NetworkConfig.Keys.RESPONSE_MATCHING, "RELAXED");
/**
https://tools.ietf.org/html/rfc7959#section-2.9.3
The block size (number of bytes) to use when doing a blockwise transfer. \
@ -103,7 +103,7 @@ public class LwM2mNetworkConfig {
*/
coapConfig.setInt(NetworkConfig.Keys.MAX_MESSAGE_SIZE, 1024);
coapConfig.setInt(NetworkConfig.Keys.MAX_RETRANSMIT, 4);
coapConfig.setInt(NetworkConfig.Keys.MAX_RETRANSMIT, 10);
return coapConfig;
}

94
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mOperationType.java

@ -0,0 +1,94 @@
/**
* 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.lwm2m.server;
import lombok.Getter;
/**
* Define the behavior of a write request.
*/
public enum LwM2mOperationType {
READ(0, "Read", true),
READ_COMPOSITE(1, "ReadComposite", false, true),
DISCOVER(2, "Discover", true),
DISCOVER_ALL(3, "DiscoverAll", false),
OBSERVE_READ_ALL(4, "ObserveReadAll", false),
OBSERVE(5, "Observe", true),
OBSERVE_COMPOSITE(6, "ObserveComposite", false, true),
OBSERVE_CANCEL(7, "ObserveCancel", true),
OBSERVE_COMPOSITE_CANCEL(8, "ObserveCompositeCancel", false, true),
OBSERVE_CANCEL_ALL(9, "ObserveCancelAll", false),
EXECUTE(10, "Execute", true),
/**
* Replaces the Object Instance or the Resource(s) with the new value provided in the “Write” operation. (see
* section 5.3.3 of the LW M2M spec).
* if all resources are to be replaced
*/
WRITE_REPLACE(11, "WriteReplace", true),
/**
* Adds or updates Resources provided in the new value and leaves other existing Resources unchanged. (see section
* 5.3.3 of the LW M2M spec).
* if this is a partial update request
*/
WRITE_UPDATE(12, "WriteUpdate", true),
WRITE_COMPOSITE(14, "WriteComposite", false, true),
WRITE_ATTRIBUTES(15, "WriteAttributes", true),
DELETE(16, "Delete", true),
// only for RPC
FW_UPDATE(17, "FirmwareUpdate", false);
// FW_READ_INFO(18, "FirmwareReadInfo"),
// SW_READ_INFO(19, "SoftwareReadInfo"),
// SW_UPDATE(20, "SoftwareUpdate"),
// SW_UNINSTALL(21, "SoftwareUninstall");
@Getter
private final int code;
@Getter
private final String type;
@Getter
private final boolean hasObjectId;
@Getter
private final boolean composite;
LwM2mOperationType(int code, String type, boolean hasObjectId) {
this(code, type, hasObjectId, false);
}
LwM2mOperationType(int code, String type, boolean hasObjectId, boolean composite) {
this.code = code;
this.type = type;
this.hasObjectId = hasObjectId;
this.composite = composite;
if(hasObjectId && composite){
throw new IllegalArgumentException("Can't set both Composite and hasObjectId for the same operation!");
}
}
public static LwM2mOperationType fromType(String type) {
for (LwM2mOperationType to : LwM2mOperationType.values()) {
if (to.type.equals(type)) {
return to;
}
}
return null;
}
}

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

@ -23,18 +23,18 @@ import org.eclipse.leshan.server.queue.PresenceListener;
import org.eclipse.leshan.server.registration.Registration;
import org.eclipse.leshan.server.registration.RegistrationListener;
import org.eclipse.leshan.server.registration.RegistrationUpdate;
import org.thingsboard.server.transport.lwm2m.server.uplink.LwM2mUplinkMsgHandler;
import java.util.Collection;
import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.LOG_LW2M_INFO;
import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.convertPathFromObjectIdToIdVer;
import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.convertObjectIdToVersionedId;
@Slf4j
public class LwM2mServerListener {
private final LwM2mTransportMsgHandler service;
private final LwM2mUplinkMsgHandler service;
public LwM2mServerListener(LwM2mTransportMsgHandler service) {
public LwM2mServerListener(LwM2mUplinkMsgHandler service) {
this.service = service;
}
@ -71,13 +71,13 @@ public class LwM2mServerListener {
public final PresenceListener presenceListener = new PresenceListener() {
@Override
public void onSleeping(Registration registration) {
log.info("onSleeping");
log.info("[{}] onSleeping", registration.getEndpoint());
service.onSleepingDev(registration);
}
@Override
public void onAwake(Registration registration) {
log.info("onAwake");
log.info("[{}] onAwake", registration.getEndpoint());
service.onAwakeDev(registration);
}
};
@ -86,30 +86,24 @@ public class LwM2mServerListener {
@Override
public void cancelled(Observation observation) {
String msg = String.format("%s: Canceled Observation %s.", LOG_LW2M_INFO, observation.getPath());
service.sendLogsToThingsboard(observation.getRegistrationId(), msg);
log.warn(msg);
log.trace("Canceled Observation {}.", observation.getPath());
}
@Override
public void onResponse(Observation observation, Registration registration, ObserveResponse response) {
if (registration != null) {
service.onUpdateValueAfterReadResponse(registration, convertPathFromObjectIdToIdVer(observation.getPath().toString(),
registration), response, null);
service.onUpdateValueAfterReadResponse(registration, convertObjectIdToVersionedId(observation.getPath().toString(), registration), response);
}
}
@Override
public void onError(Observation observation, Registration registration, Exception error) {
log.error(String.format("Unable to handle notification of [%s:%s]", observation.getRegistrationId(), observation.getPath()), error);
log.error("Unable to handle notification of [{}:{}]", observation.getRegistrationId(), observation.getPath(), error);
}
@Override
public void newObservation(Observation observation, Registration registration) {
String msg = String.format("%s: Successful start newObservation %s.", LOG_LW2M_INFO,
observation.getPath());
log.warn(msg);
service.sendLogsToThingsboard(registration.getId(), msg);
log.trace("Successful start newObservation {}.", observation.getPath());
}
};
}

38
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mSessionMsgListener.java

@ -17,12 +17,15 @@ package org.thingsboard.server.transport.lwm2m.server;
import io.netty.util.concurrent.Future;
import io.netty.util.concurrent.GenericFutureListener;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.NotNull;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.ResourceType;
import org.thingsboard.server.common.transport.SessionMsgListener;
import org.thingsboard.server.common.transport.TransportService;
import org.thingsboard.server.common.transport.TransportServiceCallback;
import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.gen.transport.TransportProtos.AttributeUpdateNotificationMsg;
import org.thingsboard.server.gen.transport.TransportProtos.GetAttributeResponseMsg;
@ -30,29 +33,32 @@ import org.thingsboard.server.gen.transport.TransportProtos.SessionCloseNotifica
import org.thingsboard.server.gen.transport.TransportProtos.ToDeviceRpcRequestMsg;
import org.thingsboard.server.gen.transport.TransportProtos.ToServerRpcResponseMsg;
import org.thingsboard.server.gen.transport.TransportProtos.ToTransportUpdateCredentialsProto;
import org.thingsboard.server.transport.lwm2m.server.attributes.LwM2MAttributesService;
import org.thingsboard.server.transport.lwm2m.server.rpc.LwM2MRpcRequestHandler;
import org.thingsboard.server.transport.lwm2m.server.uplink.LwM2mUplinkMsgHandler;
import java.util.Optional;
import java.util.UUID;
@Slf4j
@RequiredArgsConstructor
public class LwM2mSessionMsgListener implements GenericFutureListener<Future<? super Void>>, SessionMsgListener {
private DefaultLwM2MTransportMsgHandler handler;
private TransportProtos.SessionInfoProto sessionInfo;
public LwM2mSessionMsgListener(DefaultLwM2MTransportMsgHandler handler, TransportProtos.SessionInfoProto sessionInfo) {
this.handler = handler;
this.sessionInfo = sessionInfo;
}
private final LwM2mUplinkMsgHandler handler;
private final LwM2MAttributesService attributesService;
private final LwM2MRpcRequestHandler rpcHandler;
private final TransportProtos.SessionInfoProto sessionInfo;
private final TransportService transportService;
@Override
public void onGetAttributesResponse(GetAttributeResponseMsg getAttributesResponse) {
this.handler.onGetAttributesResponse(getAttributesResponse, this.sessionInfo);
this.attributesService.onGetAttributesResponse(getAttributesResponse, this.sessionInfo);
}
@Override
public void onAttributeUpdate(AttributeUpdateNotificationMsg attributeUpdateNotification) {
this.handler.onAttributeUpdate(attributeUpdateNotification, this.sessionInfo);
}
public void onAttributeUpdate(UUID sessionId, AttributeUpdateNotificationMsg attributeUpdateNotification) {
log.trace("[{}] Received attributes update notification to device", sessionId);
this.attributesService.onAttributesUpdate(attributeUpdateNotification, this.sessionInfo);
}
@Override
public void onRemoteSessionCloseCommand(UUID sessionId, SessionCloseNotificationProto sessionCloseNotification) {
@ -61,7 +67,7 @@ public class LwM2mSessionMsgListener implements GenericFutureListener<Future<? s
@Override
public void onToTransportUpdateCredentials(ToTransportUpdateCredentialsProto updateCredentials) {
this.handler.onToTransportUpdateCredentials(updateCredentials);
this.handler.onToTransportUpdateCredentials(sessionInfo, updateCredentials);
}
@Override
@ -75,13 +81,15 @@ public class LwM2mSessionMsgListener implements GenericFutureListener<Future<? s
}
@Override
public void onToDeviceRpcRequest(ToDeviceRpcRequestMsg toDeviceRequest) {
this.handler.onToDeviceRpcRequest(toDeviceRequest,this.sessionInfo);
public void onToDeviceRpcRequest(UUID sessionId, ToDeviceRpcRequestMsg toDeviceRequest) {
log.trace("[{}] Received RPC command to device", sessionId);
this.rpcHandler.onToDeviceRpcRequest(toDeviceRequest, this.sessionInfo);
transportService.process(sessionInfo, toDeviceRequest, false, TransportServiceCallback.EMPTY);
}
@Override
public void onToServerRpcResponse(ToServerRpcResponseMsg toServerResponse) {
this.handler.onToServerRpcResponse(toServerResponse);
this.rpcHandler.onToServerRpcResponse(toServerResponse);
}
@Override

28
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportCoapResource.java

@ -24,24 +24,25 @@ 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.server.cache.ota.OtaPackageDataCache;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.atomic.AtomicInteger;
import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.FIRMWARE_UPDATE_COAP_RECOURSE;
import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.SOFTWARE_UPDATE_COAP_RECOURSE;
import static org.thingsboard.server.transport.lwm2m.server.ota.DefaultLwM2MOtaUpdateService.FIRMWARE_UPDATE_COAP_RESOURCE;
import static org.thingsboard.server.transport.lwm2m.server.ota.DefaultLwM2MOtaUpdateService.SOFTWARE_UPDATE_COAP_RESOURCE;
@Slf4j
public class LwM2mTransportCoapResource extends AbstractLwM2mTransportResource {
private final ConcurrentMap<String, ObserveRelation> tokenToObserveRelationMap = new ConcurrentHashMap<>();
private final ConcurrentMap<String, AtomicInteger> tokenToObserveNotificationSeqMap = new ConcurrentHashMap<>();
private final LwM2mTransportMsgHandler handler;
private final OtaPackageDataCache otaPackageDataCache;
public LwM2mTransportCoapResource(LwM2mTransportMsgHandler handler, String name) {
public LwM2mTransportCoapResource(OtaPackageDataCache otaPackageDataCache, String name) {
super(name);
this.handler = handler;
this.otaPackageDataCache = otaPackageDataCache;
this.setObservable(true); // enable observing
this.addObserver(new CoapResourceObserver());
}
@ -70,8 +71,8 @@ public class LwM2mTransportCoapResource extends AbstractLwM2mTransportResource {
protected void processHandleGet(CoapExchange exchange) {
log.warn("90) processHandleGet [{}]", exchange);
if (exchange.getRequestOptions().getUriPath().size() >= 2 &&
(FIRMWARE_UPDATE_COAP_RECOURSE.equals(exchange.getRequestOptions().getUriPath().get(exchange.getRequestOptions().getUriPath().size()-2)) ||
SOFTWARE_UPDATE_COAP_RECOURSE.equals(exchange.getRequestOptions().getUriPath().get(exchange.getRequestOptions().getUriPath().size()-2)))) {
(FIRMWARE_UPDATE_COAP_RESOURCE.equals(exchange.getRequestOptions().getUriPath().get(exchange.getRequestOptions().getUriPath().size() - 2)) ||
SOFTWARE_UPDATE_COAP_RESOURCE.equals(exchange.getRequestOptions().getUriPath().get(exchange.getRequestOptions().getUriPath().size() - 2)))) {
this.sendOtaData(exchange);
}
}
@ -130,7 +131,7 @@ public class LwM2mTransportCoapResource extends AbstractLwM2mTransportResource {
}
private void sendOtaData(CoapExchange exchange) {
String idStr = exchange.getRequestOptions().getUriPath().get(exchange.getRequestOptions().getUriPath().size()-1
String idStr = exchange.getRequestOptions().getUriPath().get(exchange.getRequestOptions().getUriPath().size() - 1
);
UUID currentId = UUID.fromString(idStr);
Response response = new Response(CoAP.ResponseCode.CONTENT);
@ -140,11 +141,10 @@ public class LwM2mTransportCoapResource extends AbstractLwM2mTransportResource {
response.setPayload(fwData);
if (exchange.getRequestOptions().getBlock2() != null) {
int chunkSize = exchange.getRequestOptions().getBlock2().getSzx();
boolean moreFlag = fwData.length > chunkSize;
response.getOptions().setBlock2(chunkSize, moreFlag, 0);
log.warn("92) with blokc2 Send currentId: [{}], length: [{}], chunkSize [{}], moreFlag [{}]", currentId.toString(), fwData.length, chunkSize, moreFlag);
}
else {
boolean lastFlag = fwData.length <= chunkSize;
response.getOptions().setBlock2(chunkSize, lastFlag, 0);
log.warn("92) with blokc2 Send currentId: [{}], length: [{}], chunkSize [{}], moreFlag [{}]", currentId.toString(), fwData.length, chunkSize, lastFlag);
} else {
log.warn("92) with block1 Send currentId: [{}], length: [{}], ", currentId.toString(), fwData.length);
}
exchange.respond(response);
@ -152,7 +152,7 @@ public class LwM2mTransportCoapResource extends AbstractLwM2mTransportResource {
}
private byte[] getOtaData(UUID currentId) {
return ((DefaultLwM2MTransportMsgHandler) handler).otaPackageDataCache.get(currentId.toString());
return otaPackageDataCache.get(currentId.toString());
}
}

613
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportRequest.java

@ -1,613 +0,0 @@
/**
* 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.lwm2m.server;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.californium.core.coap.CoAP;
import org.eclipse.californium.core.coap.Response;
import org.eclipse.leshan.core.Link;
import org.eclipse.leshan.core.model.ResourceModel;
import org.eclipse.leshan.core.node.LwM2mNode;
import org.eclipse.leshan.core.node.LwM2mObject;
import org.eclipse.leshan.core.node.LwM2mObjectInstance;
import org.eclipse.leshan.core.node.LwM2mPath;
import org.eclipse.leshan.core.node.LwM2mResource;
import org.eclipse.leshan.core.node.LwM2mSingleResource;
import org.eclipse.leshan.core.node.ObjectLink;
import org.eclipse.leshan.core.observation.Observation;
import org.eclipse.leshan.core.request.ContentFormat;
import org.eclipse.leshan.core.request.DeleteRequest;
import org.eclipse.leshan.core.request.DiscoverRequest;
import org.eclipse.leshan.core.request.ExecuteRequest;
import org.eclipse.leshan.core.request.ObserveRequest;
import org.eclipse.leshan.core.request.ReadRequest;
import org.eclipse.leshan.core.request.SimpleDownlinkRequest;
import org.eclipse.leshan.core.request.WriteAttributesRequest;
import org.eclipse.leshan.core.request.WriteRequest;
import org.eclipse.leshan.core.request.exception.ClientSleepingException;
import org.eclipse.leshan.core.response.DeleteResponse;
import org.eclipse.leshan.core.response.DiscoverResponse;
import org.eclipse.leshan.core.response.ExecuteResponse;
import org.eclipse.leshan.core.response.LwM2mResponse;
import org.eclipse.leshan.core.response.ReadResponse;
import org.eclipse.leshan.core.response.ResponseCallback;
import org.eclipse.leshan.core.response.WriteAttributesResponse;
import org.eclipse.leshan.core.response.WriteResponse;
import org.eclipse.leshan.core.util.Hex;
import org.eclipse.leshan.core.util.NamedThreadFactory;
import org.eclipse.leshan.server.registration.Registration;
import org.springframework.stereotype.Service;
import org.thingsboard.server.queue.util.TbLwM2mTransportComponent;
import org.thingsboard.server.transport.lwm2m.config.LwM2MTransportServerConfig;
import org.thingsboard.server.transport.lwm2m.server.client.LwM2mClient;
import org.thingsboard.server.transport.lwm2m.server.client.LwM2mClientContext;
import org.thingsboard.server.transport.lwm2m.server.client.LwM2mClientRpcRequest;
import org.thingsboard.server.transport.lwm2m.utils.LwM2mValueConverterImpl;
import javax.annotation.PostConstruct;
import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.stream.Collectors;
import static org.eclipse.californium.core.coap.CoAP.ResponseCode.CONTENT;
import static org.eclipse.leshan.core.ResponseCode.BAD_REQUEST;
import static org.eclipse.leshan.core.ResponseCode.NOT_FOUND;
import static org.thingsboard.server.common.data.ota.OtaPackageUpdateStatus.DOWNLOADED;
import static org.thingsboard.server.common.data.ota.OtaPackageUpdateStatus.FAILED;
import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportServerHelper.getContentFormatByResourceModelType;
import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.DEFAULT_TIMEOUT;
import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.FW_PACKAGE_5_ID;
import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.FW_UPDATE_ID;
import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.LOG_LW2M_ERROR;
import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.LOG_LW2M_INFO;
import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.LOG_LW2M_VALUE;
import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.LwM2mTypeOper;
import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.LwM2mTypeOper.DISCOVER;
import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.LwM2mTypeOper.EXECUTE;
import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.LwM2mTypeOper.OBSERVE_CANCEL;
import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.LwM2mTypeOper.OBSERVE_READ_ALL;
import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.LwM2mTypeOper.WRITE_ATTRIBUTES;
import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.LwM2mTypeOper.WRITE_REPLACE;
import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.LwM2mTypeOper.WRITE_UPDATE;
import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.RESPONSE_REQUEST_CHANNEL;
import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.SW_INSTALL_ID;
import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.SW_PACKAGE_ID;
import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.convertPathFromIdVerToObjectId;
import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.convertPathFromObjectIdToIdVer;
import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.createWriteAttributeRequest;
@Slf4j
@Service
@TbLwM2mTransportComponent
@RequiredArgsConstructor
public class LwM2mTransportRequest {
private ExecutorService responseRequestExecutor;
public LwM2mValueConverterImpl converter;
private final LwM2mTransportContext context;
private final LwM2MTransportServerConfig config;
private final LwM2mClientContext lwM2mClientContext;
private final DefaultLwM2MTransportMsgHandler handler;
@PostConstruct
public void init() {
this.converter = LwM2mValueConverterImpl.getInstance();
responseRequestExecutor = Executors.newFixedThreadPool(this.config.getResponsePoolSize(),
new NamedThreadFactory(String.format("LwM2M %s channel response after request", RESPONSE_REQUEST_CHANNEL)));
}
public void sendAllRequest(LwM2mClient lwM2MClient, String targetIdVer, LwM2mTypeOper typeOper, Object params, long timeoutInMs, LwM2mClientRpcRequest lwm2mClientRpcRequest) {
sendAllRequest(lwM2MClient, targetIdVer, typeOper, lwM2MClient.getDefaultContentFormat(), params, timeoutInMs, lwm2mClientRpcRequest);
}
public void sendAllRequest(LwM2mClient lwM2MClient, String targetIdVer, LwM2mTypeOper typeOper,
ContentFormat contentFormat, Object params, long timeoutInMs, LwM2mClientRpcRequest lwm2mClientRpcRequest) {
Registration registration = lwM2MClient.getRegistration();
try {
String target = convertPathFromIdVerToObjectId(targetIdVer);
if(contentFormat == null){
contentFormat = ContentFormat.DEFAULT;
}
LwM2mPath resultIds = target != null ? new LwM2mPath(target) : null;
if (!OBSERVE_CANCEL.name().equals(typeOper.name()) && resultIds != null && registration != null && resultIds.getObjectId() >= 0 && lwM2MClient != null) {
if (lwM2MClient.isValidObjectVersion(targetIdVer)) {
timeoutInMs = timeoutInMs > 0 ? timeoutInMs : DEFAULT_TIMEOUT;
SimpleDownlinkRequest request = createRequest(registration, lwM2MClient, typeOper, contentFormat, target,
targetIdVer, resultIds, params, lwm2mClientRpcRequest);
if (request != null) {
try {
this.sendRequest(registration, lwM2MClient, request, timeoutInMs, lwm2mClientRpcRequest);
} catch (ClientSleepingException e) {
SimpleDownlinkRequest finalRequest = request;
long finalTimeoutInMs = timeoutInMs;
LwM2mClientRpcRequest finalRpcRequest = lwm2mClientRpcRequest;
lwM2MClient.getQueuedRequests().add(() -> sendRequest(registration, lwM2MClient, finalRequest, finalTimeoutInMs, finalRpcRequest));
} catch (Exception e) {
log.error("[{}] [{}] [{}] Failed to send downlink.", registration.getEndpoint(), targetIdVer, typeOper.name(), e);
}
} else if (WRITE_UPDATE.name().equals(typeOper.name())) {
if (lwm2mClientRpcRequest != null) {
String errorMsg = String.format("Path %s params is not valid", targetIdVer);
handler.sentRpcResponse(lwm2mClientRpcRequest, BAD_REQUEST.getName(), errorMsg, LOG_LW2M_ERROR);
}
} else if (WRITE_REPLACE.name().equals(typeOper.name()) || EXECUTE.name().equals(typeOper.name())) {
if (lwm2mClientRpcRequest != null) {
String errorMsg = String.format("Path %s object model is absent", targetIdVer);
handler.sentRpcResponse(lwm2mClientRpcRequest, BAD_REQUEST.getName(), errorMsg, LOG_LW2M_ERROR);
}
} else if (!OBSERVE_CANCEL.name().equals(typeOper.name())) {
log.error("[{}], [{}] - [{}] error SendRequest", registration.getEndpoint(), typeOper.name(), targetIdVer);
if (lwm2mClientRpcRequest != null) {
ResourceModel resourceModel = lwM2MClient.getResourceModel(targetIdVer, this.config.getModelProvider());
String errorMsg = resourceModel == null ? String.format("Path %s not found in object version", targetIdVer) : "SendRequest - null";
handler.sentRpcResponse(lwm2mClientRpcRequest, NOT_FOUND.getName(), errorMsg, LOG_LW2M_ERROR);
}
}
} else if (lwm2mClientRpcRequest != null) {
String errorMsg = String.format("Path %s not found in object version", targetIdVer);
handler.sentRpcResponse(lwm2mClientRpcRequest, NOT_FOUND.getName(), errorMsg, LOG_LW2M_ERROR);
}
} else {
switch (typeOper) {
case OBSERVE_READ_ALL:
case DISCOVER_ALL:
Set<String> paths;
if (OBSERVE_READ_ALL.name().equals(typeOper.name())) {
Set<Observation> observations = context.getServer().getObservationService().getObservations(registration);
paths = observations.stream().map(observation -> observation.getPath().toString()).collect(Collectors.toUnmodifiableSet());
} else {
assert registration != null;
Link[] objectLinks = registration.getSortedObjectLinks();
paths = Arrays.stream(objectLinks).map(Link::toString).collect(Collectors.toUnmodifiableSet());
}
String msg = String.format("%s: type operation %s paths - %s", LOG_LW2M_INFO,
typeOper.name(), paths);
this.handler.sendLogsToThingsboard(lwM2MClient, msg);
if (lwm2mClientRpcRequest != null) {
String valueMsg = String.format("Paths - %s", paths);
handler.sentRpcResponse(lwm2mClientRpcRequest, CONTENT.name(), valueMsg, LOG_LW2M_VALUE);
}
break;
case OBSERVE_CANCEL:
case OBSERVE_CANCEL_ALL:
int observeCancelCnt = 0;
String observeCancelMsg = null;
if (OBSERVE_CANCEL.name().equals(typeOper)) {
observeCancelCnt = context.getServer().getObservationService().cancelObservations(registration, target);
observeCancelMsg = String.format("%s: type operation %s paths: %s count: %d", LOG_LW2M_INFO,
OBSERVE_CANCEL.name(), target, observeCancelCnt);
} else {
observeCancelCnt = context.getServer().getObservationService().cancelObservations(registration);
observeCancelMsg = String.format("%s: type operation %s paths: All count: %d", LOG_LW2M_INFO,
OBSERVE_CANCEL.name(), observeCancelCnt);
}
this.afterObserveCancel(lwM2MClient, observeCancelCnt, observeCancelMsg, lwm2mClientRpcRequest);
break;
// lwm2mClientRpcRequest != null
case FW_UPDATE:
handler.getInfoFirmwareUpdate(lwM2MClient, lwm2mClientRpcRequest);
break;
}
}
} catch (Exception e) {
String msg = String.format("%s: type operation %s %s", LOG_LW2M_ERROR,
typeOper.name(), e.getMessage());
handler.sendLogsToThingsboard(lwM2MClient, msg);
if (lwm2mClientRpcRequest != null) {
String errorMsg = String.format("Path %s type operation %s %s", targetIdVer, typeOper.name(), e.getMessage());
handler.sentRpcResponse(lwm2mClientRpcRequest, NOT_FOUND.getName(), errorMsg, LOG_LW2M_ERROR);
}
}
}
private SimpleDownlinkRequest createRequest(Registration registration, LwM2mClient lwM2MClient, LwM2mTypeOper typeOper,
ContentFormat contentFormat, String target, String targetIdVer,
LwM2mPath resultIds, Object params, LwM2mClientRpcRequest rpcRequest) {
SimpleDownlinkRequest request = null;
switch (typeOper) {
case READ:
request = new ReadRequest(contentFormat, target);
break;
case DISCOVER:
request = new DiscoverRequest(target);
break;
case OBSERVE:
String msg = String.format("%s: Send Observation %s.", LOG_LW2M_INFO, targetIdVer);
log.warn(msg);
if (resultIds.isResource()) {
Set<Observation> observations = context.getServer().getObservationService().getObservations(registration);
Set<Observation> paths = observations.stream().filter(observation -> observation.getPath().equals(resultIds)).collect(Collectors.toSet());
if (paths.size() == 0) {
request = new ObserveRequest(contentFormat, resultIds.getObjectId(), resultIds.getObjectInstanceId(), resultIds.getResourceId());
} else {
request = new ReadRequest(contentFormat, target);
}
} else if (resultIds.isObjectInstance()) {
request = new ObserveRequest(contentFormat, resultIds.getObjectId(), resultIds.getObjectInstanceId());
} else if (resultIds.getObjectId() >= 0) {
request = new ObserveRequest(contentFormat, resultIds.getObjectId());
}
break;
case EXECUTE:
ResourceModel resourceModelExecute = lwM2MClient.getResourceModel(targetIdVer, this.config.getModelProvider());
if (resourceModelExecute != null) {
if (params != null && !resourceModelExecute.multiple) {
request = new ExecuteRequest(target, (String) this.converter.convertValue(params, resourceModelExecute.type, ResourceModel.Type.STRING, resultIds));
} else {
request = new ExecuteRequest(target);
}
}
break;
case WRITE_REPLACE:
/**
* Request to write a <b>String Single-Instance Resource</b> using the TLV content format.
* Type from resourceModel -> STRING, INTEGER, FLOAT, BOOLEAN, OPAQUE, TIME, OBJLNK
* contentFormat -> TLV, TLV, TLV, TLV, OPAQUE, TLV, LINK
* JSON, TEXT;
**/
ResourceModel resourceModelWrite = lwM2MClient.getResourceModel(targetIdVer, this.config.getModelProvider());
if (resourceModelWrite != null) {
contentFormat = getContentFormatByResourceModelType(resourceModelWrite, contentFormat);
request = this.getWriteRequestSingleResource(contentFormat, resultIds.getObjectId(),
resultIds.getObjectInstanceId(), resultIds.getResourceId(), params, resourceModelWrite.type,
lwM2MClient, rpcRequest);
}
break;
case WRITE_UPDATE:
if (resultIds.isResource()) {
/**
* send request: path = '/3/0' node == wM2mObjectInstance
* with params == "\"resources\": {15: resource:{id:15. value:'+01'...}}
**/
Collection<LwM2mResource> resources = lwM2MClient.getNewResourceForInstance(
targetIdVer, params,
this.config.getModelProvider(),
this.converter);
contentFormat = getContentFormatByResourceModelType(lwM2MClient.getResourceModel(targetIdVer, this.config.getModelProvider()),
contentFormat);
request = new WriteRequest(WriteRequest.Mode.UPDATE, contentFormat, resultIds.getObjectId(),
resultIds.getObjectInstanceId(), resources);
}
/**
* params = "{\"id\":0,\"resources\":[{\"id\":14,\"value\":\"+5\"},{\"id\":15,\"value\":\"+9\"}]}"
* int rscId = resultIds.getObjectInstanceId();
* contentFormat – Format of the payload (TLV or JSON).
*/
else if (resultIds.isObjectInstance()) {
if (((ConcurrentHashMap) params).size() > 0) {
Collection<LwM2mResource> resources = lwM2MClient.getNewResourcesForInstance(
targetIdVer, params,
this.config.getModelProvider(),
this.converter);
if (resources.size() > 0) {
contentFormat = contentFormat.equals(ContentFormat.JSON) ? contentFormat : ContentFormat.TLV;
request = new WriteRequest(WriteRequest.Mode.UPDATE, contentFormat, resultIds.getObjectId(),
resultIds.getObjectInstanceId(), resources);
}
}
} else if (resultIds.getObjectId() >= 0) {
request = new ObserveRequest(resultIds.getObjectId());
}
break;
case WRITE_ATTRIBUTES:
request = createWriteAttributeRequest(target, params, this.handler);
break;
case DELETE:
request = new DeleteRequest(target);
break;
}
return request;
}
/**
* @param registration -
* @param request -
* @param timeoutInMs -
*/
@SuppressWarnings({"error sendRequest"})
private void sendRequest(Registration registration, LwM2mClient lwM2MClient, SimpleDownlinkRequest request,
long timeoutInMs, LwM2mClientRpcRequest rpcRequest) {
context.getServer().send(registration, request, timeoutInMs, (ResponseCallback<?>) response -> {
if (!lwM2MClient.isInit()) {
lwM2MClient.initReadValue(this.handler, convertPathFromObjectIdToIdVer(request.getPath().toString(), registration));
}
if (CoAP.ResponseCode.isSuccess(((Response) response.getCoapResponse()).getCode())) {
this.handleResponse(lwM2MClient, request.getPath().toString(), response, request, rpcRequest);
} else {
String msg = String.format("%s: SendRequest %s: CoapCode - %s Lwm2m code - %d name - %s Resource path - %s", LOG_LW2M_ERROR, request.getClass().getName().toString(),
((Response) response.getCoapResponse()).getCode(), response.getCode().getCode(), response.getCode().getName(), request.getPath().toString());
handler.sendLogsToThingsboard(lwM2MClient, msg);
log.error("[{}] [{}], [{}] - [{}] [{}] error SendRequest", request.getClass().getName().toString(), registration.getEndpoint(),
((Response) response.getCoapResponse()).getCode(), response.getCode(), request.getPath().toString());
if (!lwM2MClient.isInit()) {
lwM2MClient.initReadValue(this.handler, convertPathFromObjectIdToIdVer(request.getPath().toString(), registration));
}
/** Not Found */
if (rpcRequest != null) {
handler.sentRpcResponse(rpcRequest, response.getCode().getName(), response.getErrorMessage(), LOG_LW2M_ERROR);
}
/** Not Found
set setClient_fw_info... = empty
**/
if (lwM2MClient.getFwUpdate() != null && lwM2MClient.getFwUpdate().isInfoFwSwUpdate()) {
lwM2MClient.getFwUpdate().initReadValue(handler, this, request.getPath().toString());
}
if (lwM2MClient.getSwUpdate() != null && lwM2MClient.getSwUpdate().isInfoFwSwUpdate()) {
lwM2MClient.getSwUpdate().initReadValue(handler, this, request.getPath().toString());
}
if (request.getPath().toString().equals(FW_PACKAGE_5_ID) || request.getPath().toString().equals(SW_PACKAGE_ID)) {
this.afterWriteFwSWUpdateError(registration, request, response.getErrorMessage());
}
if (request.getPath().toString().equals(FW_UPDATE_ID) || request.getPath().toString().equals(SW_INSTALL_ID)) {
this.afterExecuteFwSwUpdateError(registration, request, response.getErrorMessage());
}
}
}, e -> {
/** version == null
set setClient_fw_info... = empty
**/
if (lwM2MClient.getFwUpdate() != null && lwM2MClient.getFwUpdate().isInfoFwSwUpdate()) {
lwM2MClient.getFwUpdate().initReadValue(handler, this, request.getPath().toString());
}
if (lwM2MClient.getSwUpdate() != null && lwM2MClient.getSwUpdate().isInfoFwSwUpdate()) {
lwM2MClient.getSwUpdate().initReadValue(handler, this, request.getPath().toString());
}
if (request.getPath().toString().equals(FW_PACKAGE_5_ID) || request.getPath().toString().equals(SW_PACKAGE_ID)) {
this.afterWriteFwSWUpdateError(registration, request, e.getMessage());
}
if (request.getPath().toString().equals(FW_UPDATE_ID) || request.getPath().toString().equals(SW_INSTALL_ID)) {
this.afterExecuteFwSwUpdateError(registration, request, e.getMessage());
}
if (!lwM2MClient.isInit()) {
lwM2MClient.initReadValue(this.handler, convertPathFromObjectIdToIdVer(request.getPath().toString(), registration));
}
String msg = String.format("%s: SendRequest %s: Resource path - %s msg error - %s",
LOG_LW2M_ERROR, request.getClass().getName().toString(), request.getPath().toString(), e.getMessage());
handler.sendLogsToThingsboard(lwM2MClient, msg);
log.error("[{}] [{}] - [{}] error SendRequest", request.getClass().getName().toString(), request.getPath().toString(), e.toString());
if (rpcRequest != null) {
handler.sentRpcResponse(rpcRequest, CoAP.CodeClass.ERROR_RESPONSE.name(), e.getMessage(), LOG_LW2M_ERROR);
}
});
}
private WriteRequest getWriteRequestSingleResource(ContentFormat contentFormat, Integer objectId, Integer instanceId,
Integer resourceId, Object value, ResourceModel.Type type,
LwM2mClient client, LwM2mClientRpcRequest rpcRequest) {
try {
if (type != null) {
switch (type) {
case STRING: // String
return (contentFormat == null) ? new WriteRequest(objectId, instanceId, resourceId, value.toString()) : new WriteRequest(contentFormat, objectId, instanceId, resourceId, value.toString());
case INTEGER: // Long
final long valueInt = Integer.toUnsignedLong(Integer.parseInt(value.toString()));
return (contentFormat == null) ? new WriteRequest(objectId, instanceId, resourceId, valueInt) : new WriteRequest(contentFormat, objectId, instanceId, resourceId, valueInt);
case OBJLNK: // ObjectLink
return (contentFormat == null) ? new WriteRequest(objectId, instanceId, resourceId, ObjectLink.fromPath(value.toString())) : new WriteRequest(contentFormat, objectId, instanceId, resourceId, ObjectLink.fromPath(value.toString()));
case BOOLEAN: // Boolean
return (contentFormat == null) ? new WriteRequest(objectId, instanceId, resourceId, Boolean.parseBoolean(value.toString())) : new WriteRequest(contentFormat, objectId, instanceId, resourceId, Boolean.parseBoolean(value.toString()));
case FLOAT: // Double
return (contentFormat == null) ? new WriteRequest(objectId, instanceId, resourceId, Double.parseDouble(value.toString())) : new WriteRequest(contentFormat, objectId, instanceId, resourceId, Double.parseDouble(value.toString()));
case TIME: // Date
Date date = new Date(Long.decode(value.toString()));
return (contentFormat == null) ? new WriteRequest(objectId, instanceId, resourceId, date) : new WriteRequest(contentFormat, objectId, instanceId, resourceId, date);
case OPAQUE: // byte[] value, base64
byte[] valueRequest = value instanceof byte[] ? (byte[]) value : Hex.decodeHex(value.toString().toCharArray());
return (contentFormat == null) ? new WriteRequest(objectId, instanceId, resourceId, valueRequest) :
new WriteRequest(contentFormat, objectId, instanceId, resourceId, valueRequest);
default:
}
}
if (rpcRequest != null) {
String patn = "/" + objectId + "/" + instanceId + "/" + resourceId;
String errorMsg = String.format("Bad ResourceModel Operations (E): Resource path - %s ResourceModel type - %s", patn, type);
rpcRequest.setErrorMsg(errorMsg);
}
return null;
} catch (NumberFormatException e) {
String patn = "/" + objectId + "/" + instanceId + "/" + resourceId;
String msg = String.format(LOG_LW2M_ERROR + ": NumberFormatException: Resource path - %s type - %s value - %s msg error - %s SendRequest to Client",
patn, type, value, e.toString());
handler.sendLogsToThingsboard(client, msg);
log.error("Path: [{}] type: [{}] value: [{}] errorMsg: [{}]]", patn, type, value, e.toString());
if (rpcRequest != null) {
String errorMsg = String.format("NumberFormatException: Resource path - %s type - %s value - %s", patn, type, value);
handler.sentRpcResponse(rpcRequest, BAD_REQUEST.getName(), errorMsg, LOG_LW2M_ERROR);
}
return null;
}
}
private void handleResponse(LwM2mClient lwM2mClient, final String path, LwM2mResponse response,
SimpleDownlinkRequest request, LwM2mClientRpcRequest rpcRequest) {
responseRequestExecutor.submit(() -> {
try {
this.sendResponse(lwM2mClient, path, response, request, rpcRequest);
} catch (Exception e) {
log.error("[{}] endpoint [{}] path [{}] Exception Unable to after send response.", lwM2mClient.getRegistration().getEndpoint(), path, e);
}
});
}
/**
* processing a response from a client
*
* @param path -
* @param response -
*/
private void sendResponse(LwM2mClient lwM2mClient, String path, LwM2mResponse response,
SimpleDownlinkRequest request, LwM2mClientRpcRequest rpcRequest) {
Registration registration = lwM2mClient.getRegistration();
String pathIdVer = convertPathFromObjectIdToIdVer(path, registration);
String msgLog = "";
if (response instanceof ReadResponse) {
handler.onUpdateValueAfterReadResponse(registration, pathIdVer, (ReadResponse) response, rpcRequest);
} else if (response instanceof DeleteResponse) {
log.warn("11) [{}] Path [{}] DeleteResponse", pathIdVer, response);
if (rpcRequest != null) {
rpcRequest.setInfoMsg(null);
handler.sentRpcResponse(rpcRequest, response.getCode().getName(), null, null);
}
} else if (response instanceof DiscoverResponse) {
String discoverValue = Link.serialize(((DiscoverResponse) response).getObjectLinks());
msgLog = String.format("%s: type operation: %s path: %s value: %s",
LOG_LW2M_INFO, DISCOVER.name(), request.getPath().toString(), discoverValue);
handler.sendLogsToThingsboard(lwM2mClient, msgLog);
log.warn("DiscoverResponse: [{}]", (DiscoverResponse) response);
if (rpcRequest != null) {
handler.sentRpcResponse(rpcRequest, response.getCode().getName(), discoverValue, LOG_LW2M_VALUE);
}
} else if (response instanceof ExecuteResponse) {
msgLog = String.format("%s: type operation: %s path: %s",
LOG_LW2M_INFO, EXECUTE.name(), request.getPath().toString());
log.warn("9) [{}] ", msgLog);
handler.sendLogsToThingsboard(lwM2mClient, msgLog);
if (rpcRequest != null) {
msgLog = String.format("Start %s path: %S. Preparation finished: %s", EXECUTE.name(), path, rpcRequest.getInfoMsg());
rpcRequest.setInfoMsg(msgLog);
handler.sentRpcResponse(rpcRequest, response.getCode().getName(), path, LOG_LW2M_INFO);
}
} else if (response instanceof WriteAttributesResponse) {
msgLog = String.format("%s: type operation: %s path: %s value: %s",
LOG_LW2M_INFO, WRITE_ATTRIBUTES.name(), request.getPath().toString(), ((WriteAttributesRequest) request).getAttributes().toString());
handler.sendLogsToThingsboard(lwM2mClient, msgLog);
log.warn("12) [{}] Path [{}] WriteAttributesResponse", pathIdVer, response);
if (rpcRequest != null) {
handler.sentRpcResponse(rpcRequest, response.getCode().getName(), response.toString(), LOG_LW2M_VALUE);
}
} else if (response instanceof WriteResponse) {
msgLog = String.format("Type operation: Write path: %s", pathIdVer);
log.warn("10) [{}] response: [{}]", msgLog, response);
this.infoWriteResponse(lwM2mClient, response, request, rpcRequest);
handler.onWriteResponseOk(registration, pathIdVer, (WriteRequest) request);
}
}
private void infoWriteResponse(LwM2mClient lwM2mClient, LwM2mResponse response, SimpleDownlinkRequest request, LwM2mClientRpcRequest rpcRequest) {
try {
Registration registration = lwM2mClient.getRegistration();
LwM2mNode node = ((WriteRequest) request).getNode();
String msg = null;
Object value;
if (node instanceof LwM2mObject) {
msg = String.format("%s: Update finished successfully: Lwm2m code - %d Source path: %s value: %s",
LOG_LW2M_INFO, response.getCode().getCode(), request.getPath().toString(), ((LwM2mObject) node).toString());
} else if (node instanceof LwM2mObjectInstance) {
msg = String.format("%s: Update finished successfully: Lwm2m code - %d Source path: %s value: %s",
LOG_LW2M_INFO, response.getCode().getCode(), request.getPath().toString(), ((LwM2mObjectInstance) node).prettyPrint());
} else if (node instanceof LwM2mSingleResource) {
LwM2mSingleResource singleResource = (LwM2mSingleResource) node;
if (singleResource.getType() == ResourceModel.Type.STRING || singleResource.getType() == ResourceModel.Type.OPAQUE) {
int valueLength;
if (singleResource.getType() == ResourceModel.Type.STRING) {
valueLength = ((String) singleResource.getValue()).length();
value = ((String) singleResource.getValue())
.substring(Math.min(valueLength, config.getLogMaxLength())).trim();
} else {
valueLength = ((byte[]) singleResource.getValue()).length;
value = new String(Arrays.copyOf(((byte[]) singleResource.getValue()),
Math.min(valueLength, config.getLogMaxLength()))).trim();
}
value = valueLength > config.getLogMaxLength() ? value + "..." : value;
msg = String.format("%s: Update finished successfully: Lwm2m code - %d Resource path: %s length: %s value: %s",
LOG_LW2M_INFO, response.getCode().getCode(), request.getPath().toString(), valueLength, value);
} else {
value = this.converter.convertValue(singleResource.getValue(),
singleResource.getType(), ResourceModel.Type.STRING, request.getPath());
msg = String.format("%s: Update finished successfully. Lwm2m code: %d Resource path: %s value: %s",
LOG_LW2M_INFO, response.getCode().getCode(), request.getPath().toString(), value);
}
}
if (msg != null) {
handler.sendLogsToThingsboard(lwM2mClient, msg);
if (request.getPath().toString().equals(FW_PACKAGE_5_ID) || request.getPath().toString().equals(SW_PACKAGE_ID)) {
this.afterWriteSuccessFwSwUpdate(registration, request);
if (rpcRequest != null) {
rpcRequest.setInfoMsg(msg);
}
}
else if (rpcRequest != null) {
handler.sentRpcResponse(rpcRequest, response.getCode().getName(), msg, LOG_LW2M_INFO);
}
}
} catch (Exception e) {
log.trace("Fail convert value from request to string. ", e);
}
}
/**
* After finish operation FwSwUpdate Write (success):
* fw_state/sw_state = DOWNLOADED
* send operation Execute
*/
private void afterWriteSuccessFwSwUpdate(Registration registration, SimpleDownlinkRequest request) {
LwM2mClient lwM2MClient = this.lwM2mClientContext.getClientByRegistrationId(registration.getId());
if (request.getPath().toString().equals(FW_PACKAGE_5_ID) && lwM2MClient.getFwUpdate() != null) {
lwM2MClient.getFwUpdate().setStateUpdate(DOWNLOADED.name());
lwM2MClient.getFwUpdate().sendLogs(this.handler, WRITE_REPLACE.name(), LOG_LW2M_INFO, null);
}
if (request.getPath().toString().equals(SW_PACKAGE_ID) && lwM2MClient.getSwUpdate() != null) {
lwM2MClient.getSwUpdate().setStateUpdate(DOWNLOADED.name());
lwM2MClient.getSwUpdate().sendLogs(this.handler, WRITE_REPLACE.name(), LOG_LW2M_INFO, null);
}
}
/**
* After finish operation FwSwUpdate Write (error): fw_state = FAILED
*/
private void afterWriteFwSWUpdateError(Registration registration, SimpleDownlinkRequest request, String msgError) {
LwM2mClient lwM2MClient = this.lwM2mClientContext.getClientByRegistrationId(registration.getId());
if (request.getPath().toString().equals(FW_PACKAGE_5_ID) && lwM2MClient.getFwUpdate() != null) {
lwM2MClient.getFwUpdate().setStateUpdate(FAILED.name());
lwM2MClient.getFwUpdate().sendLogs(this.handler, WRITE_REPLACE.name(), LOG_LW2M_ERROR, msgError);
}
if (request.getPath().toString().equals(SW_PACKAGE_ID) && lwM2MClient.getSwUpdate() != null) {
lwM2MClient.getSwUpdate().setStateUpdate(FAILED.name());
lwM2MClient.getSwUpdate().sendLogs(this.handler, WRITE_REPLACE.name(), LOG_LW2M_ERROR, msgError);
}
}
private void afterExecuteFwSwUpdateError(Registration registration, SimpleDownlinkRequest request, String msgError) {
LwM2mClient lwM2MClient = this.lwM2mClientContext.getClientByRegistrationId(registration.getId());
if (request.getPath().toString().equals(FW_UPDATE_ID) && lwM2MClient.getFwUpdate() != null) {
lwM2MClient.getFwUpdate().sendLogs(this.handler, EXECUTE.name(), LOG_LW2M_ERROR, msgError);
}
if (request.getPath().toString().equals(SW_INSTALL_ID) && lwM2MClient.getSwUpdate() != null) {
lwM2MClient.getSwUpdate().sendLogs(this.handler, EXECUTE.name(), LOG_LW2M_ERROR, msgError);
}
}
private void afterObserveCancel(LwM2mClient lwM2mClient, int observeCancelCnt, String observeCancelMsg, LwM2mClientRpcRequest rpcRequest) {
handler.sendLogsToThingsboard(lwM2mClient, observeCancelMsg);
log.warn("[{}]", observeCancelMsg);
if (rpcRequest != null) {
rpcRequest.setInfoMsg(String.format("Count: %d", observeCancelCnt));
handler.sentRpcResponse(rpcRequest, CONTENT.name(), null, LOG_LW2M_INFO);
}
}
}

55
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportServerHelper.java

@ -37,6 +37,8 @@ import org.eclipse.leshan.core.model.DefaultDDFFileValidator;
import org.eclipse.leshan.core.model.InvalidDDFFileException;
import org.eclipse.leshan.core.model.ObjectModel;
import org.eclipse.leshan.core.model.ResourceModel;
import org.eclipse.leshan.core.node.LwM2mPath;
import org.eclipse.leshan.core.node.LwM2mResource;
import org.eclipse.leshan.core.node.codec.CodecException;
import org.eclipse.leshan.core.request.ContentFormat;
import org.springframework.stereotype.Component;
@ -48,6 +50,8 @@ import org.thingsboard.server.gen.transport.TransportProtos.PostTelemetryMsg;
import org.thingsboard.server.gen.transport.TransportProtos.SessionInfoProto;
import org.thingsboard.server.queue.util.TbLwM2mTransportComponent;
import org.thingsboard.server.transport.lwm2m.server.adaptors.LwM2MJsonAdaptor;
import org.thingsboard.server.transport.lwm2m.server.client.LwM2mClient;
import org.thingsboard.server.transport.lwm2m.server.client.ResourceValue;
import java.io.ByteArrayInputStream;
import java.io.IOException;
@ -57,6 +61,7 @@ import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import static org.thingsboard.server.gen.transport.TransportProtos.KeyValueType.BOOLEAN_V;
import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.fromVersionedIdToObjectId;
@Slf4j
@Component
@ -67,37 +72,15 @@ public class LwM2mTransportServerHelper {
private final LwM2mTransportContext context;
private final AtomicInteger atomicTs = new AtomicInteger(0);
public long getTS() {
return TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis()) * 1000L + (atomicTs.getAndIncrement() % 1000);
}
/**
* send to Thingsboard Attribute || Telemetry
*
* @param msg - JsonObject: [{name: value}]
* @return - dummyWriteReplace {\"targetIdVer\":\"/19_1.0/0/0\",\"value\":0082}
*/
private <T> TransportServiceCallback<Void> getPubAckCallbackSendAttrTelemetry(final T msg) {
return new TransportServiceCallback<>() {
@Override
public void onSuccess(Void dummy) {
log.trace("Success to publish msg: {}, dummy: {}", msg, dummy);
}
@Override
public void onError(Throwable e) {
log.trace("[{}] Failed to publish msg: {}", msg, e);
}
};
}
public void sendParametersOnThingsboardAttribute(List<TransportProtos.KeyValueProto> result, SessionInfoProto sessionInfo) {
PostAttributeMsg.Builder request = PostAttributeMsg.newBuilder();
request.addAllKv(result);
PostAttributeMsg postAttributeMsg = request.build();
TransportServiceCallback call = this.getPubAckCallbackSendAttrTelemetry(postAttributeMsg);
context.getTransportService().process(sessionInfo, postAttributeMsg, this.getPubAckCallbackSendAttrTelemetry(call));
context.getTransportService().process(sessionInfo, postAttributeMsg, TransportServiceCallback.EMPTY);
}
public void sendParametersOnThingsboardTelemetry(List<TransportProtos.KeyValueProto> result, SessionInfoProto sessionInfo) {
@ -107,8 +90,7 @@ public class LwM2mTransportServerHelper {
builder.addAllKv(result);
request.addTsKvList(builder.build());
PostTelemetryMsg postTelemetryMsg = request.build();
TransportServiceCallback call = this.getPubAckCallbackSendAttrTelemetry(postTelemetryMsg);
context.getTransportService().process(sessionInfo, postTelemetryMsg, this.getPubAckCallbackSendAttrTelemetry(call));
context.getTransportService().process(sessionInfo, postTelemetryMsg, TransportServiceCallback.EMPTY);
}
/**
@ -210,28 +192,6 @@ public class LwM2mTransportServerHelper {
throw new CodecException("Invalid ResourceModel_Type for resource %s, got %s", resourcePath, currentType);
}
public static ContentFormat convertResourceModelTypeToContentFormat(ResourceModel.Type type) {
switch (type) {
case BOOLEAN:
case STRING:
case TIME:
case INTEGER:
case FLOAT:
return ContentFormat.TLV;
case OPAQUE:
return ContentFormat.OPAQUE;
case OBJLNK:
return ContentFormat.LINK;
default:
}
throw new CodecException("Invalid ResourceModel_Type for %s ContentFormat.", type);
}
public static ContentFormat getContentFormatByResourceModelType(ResourceModel resourceModel, ContentFormat contentFormat) {
return contentFormat.equals(ContentFormat.TLV) ? convertResourceModelTypeToContentFormat(resourceModel.type) :
contentFormat;
}
public static Object getValueFromKvProto(TransportProtos.KeyValueProto kv) {
switch (kv.getType()) {
case BOOLEAN_V:
@ -247,4 +207,5 @@ public class LwM2mTransportServerHelper {
}
return null;
}
}

863
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportUtil.java

@ -16,51 +16,38 @@
package org.thingsboard.server.transport.lwm2m.server;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.collect.Sets;
import com.google.gson.Gson;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.google.gson.JsonSyntaxException;
import com.google.gson.reflect.TypeToken;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.eclipse.leshan.core.attributes.Attribute;
import org.eclipse.leshan.core.attributes.AttributeSet;
import org.eclipse.leshan.core.model.ObjectModel;
import org.eclipse.leshan.core.model.ResourceModel;
import org.eclipse.leshan.core.node.LwM2mMultipleResource;
import org.eclipse.leshan.core.node.LwM2mNode;
import org.eclipse.leshan.core.node.LwM2mObject;
import org.eclipse.leshan.core.node.LwM2mObjectInstance;
import org.eclipse.leshan.core.node.LwM2mPath;
import org.eclipse.leshan.core.node.LwM2mSingleResource;
import org.eclipse.leshan.core.node.LwM2mResource;
import org.eclipse.leshan.core.node.codec.CodecException;
import org.eclipse.leshan.core.request.SimpleDownlinkRequest;
import org.eclipse.leshan.core.request.WriteAttributesRequest;
import org.eclipse.leshan.core.util.Hex;
import org.eclipse.leshan.server.registration.Registration;
import org.nustaq.serialization.FSTConfiguration;
import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.DeviceTransportType;
import org.thingsboard.server.common.data.device.data.lwm2m.BootstrapConfiguration;
import org.thingsboard.server.common.data.device.profile.DeviceProfileTransportConfiguration;
import org.thingsboard.server.common.data.device.profile.Lwm2mDeviceProfileTransportConfiguration;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.ota.OtaPackageKey;
import org.thingsboard.server.common.data.ota.OtaPackageType;
import org.thingsboard.server.common.data.ota.OtaPackageUpdateStatus;
import org.thingsboard.server.common.data.ota.OtaPackageUtil;
import org.thingsboard.server.common.transport.TransportServiceCallback;
import org.thingsboard.server.transport.lwm2m.config.LwM2mVersion;
import org.thingsboard.server.transport.lwm2m.server.client.LwM2mClient;
import org.thingsboard.server.transport.lwm2m.server.client.LwM2mClientProfile;
import org.thingsboard.server.transport.lwm2m.server.client.ResourceValue;
import org.thingsboard.server.transport.lwm2m.server.ota.firmware.FirmwareUpdateResult;
import org.thingsboard.server.transport.lwm2m.server.ota.firmware.FirmwareUpdateState;
import org.thingsboard.server.transport.lwm2m.server.ota.software.SoftwareUpdateResult;
import org.thingsboard.server.transport.lwm2m.server.ota.software.SoftwareUpdateState;
import org.thingsboard.server.transport.lwm2m.server.uplink.DefaultLwM2MUplinkMsgHandler;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import static org.eclipse.leshan.core.attributes.Attribute.DIMENSION;
@ -79,469 +66,20 @@ import static org.eclipse.leshan.core.model.ResourceModel.Type.STRING;
import static org.eclipse.leshan.core.model.ResourceModel.Type.TIME;
import static org.thingsboard.server.common.data.lwm2m.LwM2mConstants.LWM2M_SEPARATOR_KEY;
import static org.thingsboard.server.common.data.lwm2m.LwM2mConstants.LWM2M_SEPARATOR_PATH;
import static org.thingsboard.server.common.data.ota.OtaPackageUpdateStatus.DOWNLOADED;
import static org.thingsboard.server.common.data.ota.OtaPackageUpdateStatus.DOWNLOADING;
import static org.thingsboard.server.common.data.ota.OtaPackageUpdateStatus.FAILED;
import static org.thingsboard.server.common.data.ota.OtaPackageUpdateStatus.UPDATED;
import static org.thingsboard.server.common.data.ota.OtaPackageUpdateStatus.UPDATING;
import static org.thingsboard.server.common.data.ota.OtaPackageUpdateStatus.VERIFIED;
import static org.thingsboard.server.transport.lwm2m.server.ota.DefaultLwM2MOtaUpdateService.FW_RESULT_ID;
import static org.thingsboard.server.transport.lwm2m.server.ota.DefaultLwM2MOtaUpdateService.FW_STATE_ID;
import static org.thingsboard.server.transport.lwm2m.server.ota.DefaultLwM2MOtaUpdateService.SW_RESULT_ID;
import static org.thingsboard.server.transport.lwm2m.server.ota.DefaultLwM2MOtaUpdateService.SW_UPDATE_STATE_ID;
@Slf4j
public class LwM2mTransportUtil {
public static final String EVENT_AWAKE = "AWAKE";
public static final String RESPONSE_REQUEST_CHANNEL = "RESP_REQ";
public static final String RESPONSE_CHANNEL = "RESP";
public static final String OBSERVE_CHANNEL = "OBSERVE";
public static final String LWM2M_OBJECT_VERSION_DEFAULT = "1.0";
public static final String TRANSPORT_DEFAULT_LWM2M_VERSION = "1.0";
public static final String CLIENT_LWM2M_SETTINGS = "clientLwM2mSettings";
public static final String BOOTSTRAP = "bootstrap";
public static final String SERVERS = "servers";
public static final String LWM2M_SERVER = "lwm2mServer";
public static final String BOOTSTRAP_SERVER = "bootstrapServer";
public static final String OBSERVE_ATTRIBUTE_TELEMETRY = "observeAttr";
public static final String ATTRIBUTE = "attribute";
public static final String TELEMETRY = "telemetry";
public static final String KEY_NAME = "keyName";
public static final String OBSERVE_LWM2M = "observe";
public static final String ATTRIBUTE_LWM2M = "attributeLwm2m";
private static final String REQUEST = "/request";
private static final String ATTRIBUTES = "/" + ATTRIBUTE;
public static final String TELEMETRIES = "/" + TELEMETRY;
public static final String ATTRIBUTES_REQUEST = ATTRIBUTES + REQUEST;
public static final String DEVICE_ATTRIBUTES_REQUEST = ATTRIBUTES_REQUEST + "/";
public static final long DEFAULT_TIMEOUT = 2 * 60 * 1000L; // 2min in ms
public static final String LOG_LW2M_TELEMETRY = "logLwm2m";
public static final String LOG_LW2M_INFO = "info";
public static final String LOG_LW2M_ERROR = "error";
public static final String LOG_LW2M_WARN = "warn";
public static final String LOG_LW2M_VALUE = "value";
public static final String CLIENT_NOT_AUTHORIZED = "Client not authorized";
public static final String LWM2M_VERSION_DEFAULT = "1.0";
// RPC
public static final String TYPE_OPER_KEY = "typeOper";
public static final String TARGET_ID_VER_KEY = "targetIdVer";
public static final String KEY_NAME_KEY = "key";
public static final String VALUE_KEY = "value";
public static final String PARAMS_KEY = "params";
public static final String SEPARATOR_KEY = ":";
public static final String FINISH_VALUE_KEY = ",";
public static final String START_JSON_KEY = "{";
public static final String FINISH_JSON_KEY = "}";
public static final String INFO_KEY = "info";
public static final String RESULT_KEY = "result";
public static final String ERROR_KEY = "error";
public static final String METHOD_KEY = "methodName";
// Firmware
public static final String FIRMWARE_UPDATE_COAP_RECOURSE = "firmwareUpdateCoapRecourse";
public static final String FW_UPDATE = "Firmware update";
public static final Integer FW_5_ID = 5;
public static final Integer FW_19_ID = 19;
// Package W
public static final String FW_PACKAGE_5_ID = "/5/0/0";
public static final String FW_PACKAGE_19_ID = "/19/0/0";
// Package URI
public static final String FW_PACKAGE_URI_ID = "/5/0/1";
// State R
public static final String FW_STATE_ID = "/5/0/3";
// Update Result R
public static final String FW_RESULT_ID = "/5/0/5";
// PkgName R
public static final String FW_NAME_ID = "/5/0/6";
// PkgVersion R
public static final String FW_5_VER_ID = "/5/0/7";
/**
* Quectel@Hi15RM1-HLB_V1.0@BC68JAR01A10,V150R100C20B300SP7,V150R100C20B300SP7@8
* BC68JAR01A10
* # Request prodct type number
* ATI
* Quectel
* BC68
* Revision:BC68JAR01A10
*/
public static final String FW_3_VER_ID = "/3/0/3";
// Update E
public static final String FW_UPDATE_ID = "/5/0/2";
// Software
public static final String SOFTWARE_UPDATE_COAP_RECOURSE = "softwareUpdateCoapRecourse";
public static final String SW_UPDATE = "Software update";
public static final Integer SW_ID = 9;
// Package W
public static final String SW_PACKAGE_ID = "/9/0/2";
// Package URI
public static final String SW_PACKAGE_URI_ID = "/9/0/3";
// Update State R
public static final String SW_UPDATE_STATE_ID = "/9/0/7";
// Update Result R
public static final String SW_RESULT_ID = "/9/0/9";
// PkgName R
public static final String SW_NAME_ID = "/9/0/0";
// PkgVersion R
public static final String SW_VER_ID = "/9/0/1";
// Install E
public static final String SW_INSTALL_ID = "/9/0/4";
// Uninstall E
public static final String SW_UN_INSTALL_ID = "/9/0/6";
public enum LwM2mTypeServer {
BOOTSTRAP(0, "bootstrap"),
CLIENT(1, "client");
public int code;
public String type;
LwM2mTypeServer(int code, String type) {
this.code = code;
this.type = type;
}
public static LwM2mTypeServer fromLwM2mTypeServer(String type) {
for (LwM2mTypeServer sm : LwM2mTypeServer.values()) {
if (sm.type.equals(type)) {
return sm;
}
}
throw new IllegalArgumentException(String.format("Unsupported typeServer type : %d", type));
}
}
/**
* Define the behavior of a write request.
*/
public enum LwM2mTypeOper {
/**
* GET
*/
READ(0, "Read"),
DISCOVER(1, "Discover"),
DISCOVER_ALL(2, "DiscoverAll"),
OBSERVE_READ_ALL(3, "ObserveReadAll"),
/**
* POST
*/
OBSERVE(4, "Observe"),
OBSERVE_CANCEL(5, "ObserveCancel"),
OBSERVE_CANCEL_ALL(6, "ObserveCancelAll"),
EXECUTE(7, "Execute"),
/**
* Replaces the Object Instance or the Resource(s) with the new value provided in the “Write” operation. (see
* section 5.3.3 of the LW M2M spec).
* if all resources are to be replaced
*/
WRITE_REPLACE(8, "WriteReplace"),
/*
PUT
*/
/**
* Adds or updates Resources provided in the new value and leaves other existing Resources unchanged. (see section
* 5.3.3 of the LW M2M spec).
* if this is a partial update request
*/
WRITE_UPDATE(9, "WriteUpdate"),
WRITE_ATTRIBUTES(10, "WriteAttributes"),
DELETE(11, "Delete"),
// only for RPC
FW_UPDATE(12, "FirmwareUpdate");
// FW_READ_INFO(12, "FirmwareReadInfo"),
// SW_READ_INFO(15, "SoftwareReadInfo"),
// SW_UPDATE(16, "SoftwareUpdate"),
// SW_UNINSTALL(18, "SoftwareUninstall");
public int code;
public String type;
LwM2mTypeOper(int code, String type) {
this.code = code;
this.type = type;
}
public static LwM2mTypeOper fromLwLwM2mTypeOper(String type) {
for (LwM2mTypeOper to : LwM2mTypeOper.values()) {
if (to.type.equals(type)) {
return to;
}
}
throw new IllegalArgumentException(String.format("Unsupported typeOper type : %s", type));
}
}
/**
* /** State R
* 0: Idle (before downloading or after successful updating)
* 1: Downloading (The data sequence is on the way)
* 2: Downloaded
* 3: Updating
*/
public enum StateFw {
IDLE(0, "Idle"),
DOWNLOADING(1, "Downloading"),
DOWNLOADED(2, "Downloaded"),
UPDATING(3, "Updating");
public int code;
public String type;
StateFw(int code, String type) {
this.code = code;
this.type = type;
}
public static StateFw fromStateFwByType(String type) {
for (StateFw to : StateFw.values()) {
if (to.type.equals(type)) {
return to;
}
}
throw new IllegalArgumentException(String.format("Unsupported FW State type : %s", type));
}
public static StateFw fromStateFwByCode(int code) {
for (StateFw to : StateFw.values()) {
if (to.code == code) {
return to;
}
}
throw new IllegalArgumentException(String.format("Unsupported FW State code : %s", code));
}
}
/**
* FW Update Result
* 0: Initial value. Once the updating process is initiated (Download /Update), this Resource MUST be reset to Initial value.
* 1: Firmware updated successfully.
* 2: Not enough flash memory for the new firmware package.
* 3: Out of RAM during downloading process.
* 4: Connection lost during downloading process.
* 5: Integrity check failure for new downloaded package.
* 6: Unsupported package type.
* 7: Invalid URI.
* 8: Firmware update failed.
* 9: Unsupported protocol.
*/
public enum UpdateResultFw {
INITIAL(0, "Initial value", false),
UPDATE_SUCCESSFULLY(1, "Firmware updated successfully", false),
NOT_ENOUGH(2, "Not enough flash memory for the new firmware package", false),
OUT_OFF_MEMORY(3, "Out of RAM during downloading process", false),
CONNECTION_LOST(4, "Connection lost during downloading process", true),
INTEGRITY_CHECK_FAILURE(5, "Integrity check failure for new downloaded package", true),
UNSUPPORTED_TYPE(6, "Unsupported package type", false),
INVALID_URI(7, "Invalid URI", false),
UPDATE_FAILED(8, "Firmware update failed", false),
UNSUPPORTED_PROTOCOL(9, "Unsupported protocol", false);
public int code;
public String type;
public boolean isAgain;
UpdateResultFw(int code, String type, boolean isAgain) {
this.code = code;
this.type = type;
this.isAgain = isAgain;
}
public static UpdateResultFw fromUpdateResultFwByType(String type) {
for (UpdateResultFw to : UpdateResultFw.values()) {
if (to.type.equals(type)) {
return to;
}
}
throw new IllegalArgumentException(String.format("Unsupported FW Update Result type : %s", type));
}
public static UpdateResultFw fromUpdateResultFwByCode(int code) {
for (UpdateResultFw to : UpdateResultFw.values()) {
if (to.code == code) {
return to;
}
}
throw new IllegalArgumentException(String.format("Unsupported FW Update Result code : %s", code));
}
}
/**
* FirmwareUpdateStatus {
* DOWNLOADING, DOWNLOADED, VERIFIED, UPDATING, UPDATED, FAILED
*/
public static OtaPackageUpdateStatus equalsFwSateFwResultToFirmwareUpdateStatus(StateFw stateFw, UpdateResultFw updateResultFw) {
switch (updateResultFw) {
case INITIAL:
return equalsFwSateToFirmwareUpdateStatus(stateFw);
case UPDATE_SUCCESSFULLY:
return UPDATED;
case NOT_ENOUGH:
case OUT_OFF_MEMORY:
case CONNECTION_LOST:
case INTEGRITY_CHECK_FAILURE:
case UNSUPPORTED_TYPE:
case INVALID_URI:
case UPDATE_FAILED:
case UNSUPPORTED_PROTOCOL:
return FAILED;
default:
throw new CodecException("Invalid value stateFw %s %s for FirmwareUpdateStatus.", stateFw.name(), updateResultFw.name());
}
}
public static OtaPackageUpdateStatus equalsFwResultToFirmwareUpdateStatus(UpdateResultFw updateResultFw) {
switch (updateResultFw) {
case INITIAL:
return VERIFIED;
case UPDATE_SUCCESSFULLY:
return UPDATED;
case NOT_ENOUGH:
case OUT_OFF_MEMORY:
case CONNECTION_LOST:
case INTEGRITY_CHECK_FAILURE:
case UNSUPPORTED_TYPE:
case INVALID_URI:
case UPDATE_FAILED:
case UNSUPPORTED_PROTOCOL:
return FAILED;
default:
throw new CodecException("Invalid value stateFw %s for FirmwareUpdateStatus.", updateResultFw.name());
}
}
public static OtaPackageUpdateStatus equalsFwSateToFirmwareUpdateStatus(StateFw stateFw) {
switch (stateFw) {
case IDLE:
return VERIFIED;
case DOWNLOADING:
return DOWNLOADING;
case DOWNLOADED:
return DOWNLOADED;
case UPDATING:
return UPDATING;
default:
throw new CodecException("Invalid value stateFw %d for FirmwareUpdateStatus.", stateFw);
}
}
/**
* SW Update State R
* 0: INITIAL Before downloading. (see 5.1.2.1)
* 1: DOWNLOAD STARTED The downloading process has started and is on-going. (see 5.1.2.2)
* 2: DOWNLOADED The package has been completely downloaded (see 5.1.2.3)
* 3: DELIVERED In that state, the package has been correctly downloaded and is ready to be installed. (see 5.1.2.4)
* If executing the Install Resource failed, the state remains at DELIVERED.
* If executing the Install Resource was successful, the state changes from DELIVERED to INSTALLED.
* After executing the UnInstall Resource, the state changes to INITIAL.
* 4: INSTALLED
*/
public enum UpdateStateSw {
INITIAL(0, "Initial"),
DOWNLOAD_STARTED(1, "DownloadStarted"),
DOWNLOADED(2, "Downloaded"),
DELIVERED(3, "Delivered"),
INSTALLED(4, "Installed");
public int code;
public String type;
UpdateStateSw(int code, String type) {
this.code = code;
this.type = type;
}
public static UpdateStateSw fromUpdateStateSwByType(String type) {
for (UpdateStateSw to : UpdateStateSw.values()) {
if (to.type.equals(type)) {
return to;
}
}
throw new IllegalArgumentException(String.format("Unsupported SW State type : %s", type));
}
public static UpdateStateSw fromUpdateStateSwByCode(int code) {
for (UpdateStateSw to : UpdateStateSw.values()) {
if (to.code == code) {
return to;
}
}
throw new IllegalArgumentException(String.format("Unsupported SW State type : %s", code));
}
}
/**
* SW Update Result
* Contains the result of downloading or installing/uninstalling the software
* 0: Initial value.
* - Prior to download any new package in the Device, Update Result MUST be reset to this initial value.
* - One side effect of executing the Uninstall resource is to reset Update Result to this initial value "0".
* 1: Downloading.
* - The package downloading process is on-going.
* 2: Software successfully installed.
* 3: Successfully Downloaded and package integrity verified
* (( 4-49, for expansion, of other scenarios))
* ** Failed
* 50: Not enough storage for the new software package.
* 51: Out of memory during downloading process.
* 52: Connection lost during downloading process.
* 53: Package integrity check failure.
* 54: Unsupported package type.
* 56: Invalid URI
* 57: Device defined update error
* 58: Software installation failure
* 59: Uninstallation Failure during forUpdate(arg=0)
* 60-200 : (for expansion, selection to be in blocks depending on new introduction of features)
* This Resource MAY be reported by sending Observe operation.
*/
public enum UpdateResultSw {
INITIAL(0, "Initial value", false),
DOWNLOADING(1, "Downloading", false),
SUCCESSFULLY_INSTALLED(2, "Software successfully installed", false),
SUCCESSFULLY_DOWNLOADED_VERIFIED(3, "Successfully Downloaded and package integrity verified", false),
NOT_ENOUGH_STORAGE(50, "Not enough storage for the new software package", true),
OUT_OFF_MEMORY(51, "Out of memory during downloading process", true),
CONNECTION_LOST(52, "Connection lost during downloading process", false),
PACKAGE_CHECK_FAILURE(53, "Package integrity check failure.", false),
UNSUPPORTED_PACKAGE_TYPE(54, "Unsupported package type", false),
INVALID_URI(56, "Invalid URI", true),
UPDATE_ERROR(57, "Device defined update error", true),
INSTALL_FAILURE(58, "Software installation failure", true),
UN_INSTALL_FAILURE(59, "Uninstallation Failure during forUpdate(arg=0)", true);
public int code;
public String type;
public boolean isAgain;
UpdateResultSw(int code, String type, boolean isAgain) {
this.code = code;
this.type = type;
this.isAgain = isAgain;
}
public static UpdateResultSw fromUpdateResultSwByType(String type) {
for (UpdateResultSw to : UpdateResultSw.values()) {
if (to.type.equals(type)) {
return to;
}
}
throw new IllegalArgumentException(String.format("Unsupported SW Update Result type : %s", type));
}
public static UpdateResultSw fromUpdateResultSwByCode(int code) {
for (UpdateResultSw to : UpdateResultSw.values()) {
if (to.code == code) {
return to;
}
}
throw new IllegalArgumentException(String.format("Unsupported SW Update Result code : %s", code));
}
}
public static final String LOG_LWM2M_TELEMETRY = "transportLog";
public static final String LOG_LWM2M_INFO = "info";
public static final String LOG_LWM2M_ERROR = "error";
public static final String LOG_LWM2M_WARN = "warn";
public enum LwM2MClientStrategy {
CLIENT_STRATEGY_1(1, "Read only resources marked as observation"),
@ -574,108 +112,6 @@ public class LwM2mTransportUtil {
}
}
public enum LwM2MFirmwareUpdateStrategy {
OBJ_5_BINARY(1, "ObjectId 5, Binary"),
OBJ_5_TEMP_URL(2, "ObjectId 5, URI"),
OBJ_19_BINARY(3, "ObjectId 19, Binary");
public int code;
public String type;
LwM2MFirmwareUpdateStrategy(int code, String type) {
this.code = code;
this.type = type;
}
public static LwM2MFirmwareUpdateStrategy fromStrategyFwByType(String type) {
for (LwM2MFirmwareUpdateStrategy to : LwM2MFirmwareUpdateStrategy.values()) {
if (to.type.equals(type)) {
return to;
}
}
throw new IllegalArgumentException(String.format("Unsupported FW State type : %s", type));
}
public static LwM2MFirmwareUpdateStrategy fromStrategyFwByCode(int code) {
for (LwM2MFirmwareUpdateStrategy to : LwM2MFirmwareUpdateStrategy.values()) {
if (to.code == code) {
return to;
}
}
throw new IllegalArgumentException(String.format("Unsupported FW Strategy code : %s", code));
}
}
public enum LwM2MSoftwareUpdateStrategy {
BINARY(1, "ObjectId 9, Binary"),
TEMP_URL(2, "ObjectId 9, URI");
public int code;
public String type;
LwM2MSoftwareUpdateStrategy(int code, String type) {
this.code = code;
this.type = type;
}
public static LwM2MSoftwareUpdateStrategy fromStrategySwByType(String type) {
for (LwM2MSoftwareUpdateStrategy to : LwM2MSoftwareUpdateStrategy.values()) {
if (to.type.equals(type)) {
return to;
}
}
throw new IllegalArgumentException(String.format("Unsupported SW Strategy type : %s", type));
}
public static LwM2MSoftwareUpdateStrategy fromStrategySwByCode(int code) {
for (LwM2MSoftwareUpdateStrategy to : LwM2MSoftwareUpdateStrategy.values()) {
if (to.code == code) {
return to;
}
}
throw new IllegalArgumentException(String.format("Unsupported SW Strategy code : %s", code));
}
}
/**
* FirmwareUpdateStatus {
* DOWNLOADING, DOWNLOADED, VERIFIED, UPDATING, UPDATED, FAILED
*/
public static OtaPackageUpdateStatus EqualsSwSateToFirmwareUpdateStatus(UpdateStateSw updateStateSw, UpdateResultSw updateResultSw) {
switch (updateResultSw) {
case INITIAL:
switch (updateStateSw) {
case INITIAL:
case DOWNLOAD_STARTED:
return DOWNLOADING;
case DOWNLOADED:
return DOWNLOADED;
case DELIVERED:
return VERIFIED;
}
case DOWNLOADING:
return DOWNLOADING;
case SUCCESSFULLY_INSTALLED:
return UPDATED;
case SUCCESSFULLY_DOWNLOADED_VERIFIED:
return VERIFIED;
case NOT_ENOUGH_STORAGE:
case OUT_OFF_MEMORY:
case CONNECTION_LOST:
case PACKAGE_CHECK_FAILURE:
case UNSUPPORTED_PACKAGE_TYPE:
case INVALID_URI:
case UPDATE_ERROR:
case INSTALL_FAILURE:
case UN_INSTALL_FAILURE:
return FAILED;
default:
throw new CodecException("Invalid value stateFw %s %s for FirmwareUpdateStatus.", updateStateSw.name(), updateResultSw.name());
}
}
public static boolean equalsResourceValue(Object valueOld, Object valueNew, ResourceModel.Type type, LwM2mPath
resourcePath) throws CodecException {
switch (type) {
@ -695,28 +131,25 @@ public class LwM2mTransportUtil {
}
}
public static LwM2mOtaConvert convertOtaUpdateValueToString (String pathIdVer, Object value, ResourceModel.Type currentType) {
String path = convertPathFromIdVerToObjectId(pathIdVer);
public static LwM2mOtaConvert convertOtaUpdateValueToString(String pathIdVer, Object value, ResourceModel.Type currentType) {
String path = fromVersionedIdToObjectId(pathIdVer);
LwM2mOtaConvert lwM2mOtaConvert = new LwM2mOtaConvert();
if (path != null) {
if (FW_STATE_ID.equals(path)) {
lwM2mOtaConvert.setCurrentType(STRING);
lwM2mOtaConvert.setValue(StateFw.fromStateFwByCode(((Long) value).intValue()).type);
lwM2mOtaConvert.setValue(FirmwareUpdateState.fromStateFwByCode(((Long) value).intValue()).type);
return lwM2mOtaConvert;
}
else if (FW_RESULT_ID.equals(path)) {
} else if (FW_RESULT_ID.equals(path)) {
lwM2mOtaConvert.setCurrentType(STRING);
lwM2mOtaConvert.setValue(UpdateResultFw.fromUpdateResultFwByCode(((Long) value).intValue()).type);
lwM2mOtaConvert.setValue(FirmwareUpdateResult.fromUpdateResultFwByCode(((Long) value).intValue()).getType());
return lwM2mOtaConvert;
}
else if (SW_UPDATE_STATE_ID.equals(path)) {
} else if (SW_UPDATE_STATE_ID.equals(path)) {
lwM2mOtaConvert.setCurrentType(STRING);
lwM2mOtaConvert.setValue(UpdateStateSw.fromUpdateStateSwByCode(((Long) value).intValue()).type);
lwM2mOtaConvert.setValue(SoftwareUpdateState.fromUpdateStateSwByCode(((Long) value).intValue()).type);
return lwM2mOtaConvert;
}
else if (SW_RESULT_ID.equals(path)) {
} else if (SW_RESULT_ID.equals(path)) {
lwM2mOtaConvert.setCurrentType(STRING);
lwM2mOtaConvert.setValue(UpdateResultSw.fromUpdateResultSwByCode(((Long) value).intValue()).type);
lwM2mOtaConvert.setValue(SoftwareUpdateResult.fromUpdateResultSwByCode(((Long) value).intValue()).type);
return lwM2mOtaConvert;
}
}
@ -725,183 +158,25 @@ public class LwM2mTransportUtil {
return lwM2mOtaConvert;
}
public static LwM2mNode getLvM2mNodeToObject(LwM2mNode content) {
if (content instanceof LwM2mObject) {
return (LwM2mObject) content;
} else if (content instanceof LwM2mObjectInstance) {
return (LwM2mObjectInstance) content;
} else if (content instanceof LwM2mSingleResource) {
return (LwM2mSingleResource) content;
} else if (content instanceof LwM2mMultipleResource) {
return (LwM2mMultipleResource) content;
}
return null;
}
public static LwM2mClientProfile getNewProfileParameters(JsonObject profilesConfigData, TenantId tenantId) {
LwM2mClientProfile lwM2MClientProfile = new LwM2mClientProfile();
lwM2MClientProfile.setTenantId(tenantId);
lwM2MClientProfile.setPostClientLwM2mSettings(profilesConfigData.get(CLIENT_LWM2M_SETTINGS).getAsJsonObject());
lwM2MClientProfile.setPostKeyNameProfile(profilesConfigData.get(OBSERVE_ATTRIBUTE_TELEMETRY).getAsJsonObject().get(KEY_NAME).getAsJsonObject());
lwM2MClientProfile.setPostAttributeProfile(profilesConfigData.get(OBSERVE_ATTRIBUTE_TELEMETRY).getAsJsonObject().get(ATTRIBUTE).getAsJsonArray());
lwM2MClientProfile.setPostTelemetryProfile(profilesConfigData.get(OBSERVE_ATTRIBUTE_TELEMETRY).getAsJsonObject().get(TELEMETRY).getAsJsonArray());
lwM2MClientProfile.setPostObserveProfile(profilesConfigData.get(OBSERVE_ATTRIBUTE_TELEMETRY).getAsJsonObject().get(OBSERVE_LWM2M).getAsJsonArray());
lwM2MClientProfile.setPostAttributeLwm2mProfile(profilesConfigData.get(OBSERVE_ATTRIBUTE_TELEMETRY).getAsJsonObject().get(ATTRIBUTE_LWM2M).getAsJsonObject());
return lwM2MClientProfile;
}
/**
* @return deviceProfileBody with Observe&Attribute&Telemetry From Thingsboard
* Example:
* property: {"clientLwM2mSettings": {
* clientUpdateValueAfterConnect: false;
* }
* property: "observeAttr"
* {"keyName": {
* "/3/0/1": "modelNumber",
* "/3/0/0": "manufacturer",
* "/3/0/2": "serialNumber"
* },
* "attribute":["/2/0/1","/3/0/9"],
* "telemetry":["/1/0/1","/2/0/1","/6/0/1"],
* "observe":["/2/0","/2/0/0","/4/0/2"]}
* "attributeLwm2m": {"/3_1.0": {"ver": "currentTimeTest11"},
* "/3_1.0/0": {"gt": 17},
* "/3_1.0/0/9": {"pmax": 45}, "/3_1.2": {ver": "3_1.2"}}
*/
public static LwM2mClientProfile toLwM2MClientProfile(DeviceProfile deviceProfile) {
if (((Lwm2mDeviceProfileTransportConfiguration) deviceProfile.getProfileData().getTransportConfiguration()).getProperties().size() > 0) {
Object profile = ((Lwm2mDeviceProfileTransportConfiguration) deviceProfile.getProfileData().getTransportConfiguration()).getProperties();
try {
ObjectMapper mapper = new ObjectMapper();
String profileStr = mapper.writeValueAsString(profile);
JsonObject profileJson = (profileStr != null) ? validateJson(profileStr) : null;
return getValidateCredentialsBodyFromThingsboard(profileJson) ? LwM2mTransportUtil.getNewProfileParameters(profileJson, deviceProfile.getTenantId()) : null;
} catch (IOException e) {
log.error("", e);
}
}
return null;
}
public static JsonObject getBootstrapParametersFromThingsboard(DeviceProfile deviceProfile) {
if (deviceProfile != null && ((Lwm2mDeviceProfileTransportConfiguration) deviceProfile.getProfileData().getTransportConfiguration()).getProperties().size() > 0) {
Object bootstrap = ((Lwm2mDeviceProfileTransportConfiguration) deviceProfile.getProfileData().getTransportConfiguration()).getProperties();
try {
ObjectMapper mapper = new ObjectMapper();
String bootstrapStr = mapper.writeValueAsString(bootstrap);
JsonObject objectMsg = (bootstrapStr != null) ? validateJson(bootstrapStr) : null;
return (getValidateBootstrapProfileFromThingsboard(objectMsg)) ? objectMsg.get(BOOTSTRAP).getAsJsonObject() : null;
} catch (IOException e) {
log.error("", e);
}
public static Lwm2mDeviceProfileTransportConfiguration toLwM2MClientProfile(DeviceProfile deviceProfile) {
DeviceProfileTransportConfiguration transportConfiguration = deviceProfile.getProfileData().getTransportConfiguration();
if (transportConfiguration.getType().equals(DeviceTransportType.LWM2M)) {
return (Lwm2mDeviceProfileTransportConfiguration) transportConfiguration;
} else {
log.warn("[{}] Received profile with invalid transport configuration: {}", deviceProfile.getId(), deviceProfile.getProfileData().getTransportConfiguration());
throw new IllegalArgumentException("Received profile with invalid transport configuration: " + transportConfiguration.getType());
}
return null;
}
private static boolean getValidateCredentialsBodyFromThingsboard(JsonObject objectMsg) {
return (objectMsg != null &&
!objectMsg.isJsonNull() &&
objectMsg.has(CLIENT_LWM2M_SETTINGS) &&
!objectMsg.get(CLIENT_LWM2M_SETTINGS).isJsonNull() &&
objectMsg.get(CLIENT_LWM2M_SETTINGS).isJsonObject() &&
objectMsg.has(OBSERVE_ATTRIBUTE_TELEMETRY) &&
!objectMsg.get(OBSERVE_ATTRIBUTE_TELEMETRY).isJsonNull() &&
objectMsg.get(OBSERVE_ATTRIBUTE_TELEMETRY).isJsonObject() &&
objectMsg.get(OBSERVE_ATTRIBUTE_TELEMETRY).getAsJsonObject().has(KEY_NAME) &&
!objectMsg.get(OBSERVE_ATTRIBUTE_TELEMETRY).getAsJsonObject().get(KEY_NAME).isJsonNull() &&
objectMsg.get(OBSERVE_ATTRIBUTE_TELEMETRY).getAsJsonObject().get(KEY_NAME).isJsonObject() &&
objectMsg.get(OBSERVE_ATTRIBUTE_TELEMETRY).getAsJsonObject().has(ATTRIBUTE) &&
!objectMsg.get(OBSERVE_ATTRIBUTE_TELEMETRY).getAsJsonObject().get(ATTRIBUTE).isJsonNull() &&
objectMsg.get(OBSERVE_ATTRIBUTE_TELEMETRY).getAsJsonObject().get(ATTRIBUTE).isJsonArray() &&
objectMsg.get(OBSERVE_ATTRIBUTE_TELEMETRY).getAsJsonObject().has(TELEMETRY) &&
!objectMsg.get(OBSERVE_ATTRIBUTE_TELEMETRY).getAsJsonObject().get(TELEMETRY).isJsonNull() &&
objectMsg.get(OBSERVE_ATTRIBUTE_TELEMETRY).getAsJsonObject().get(TELEMETRY).isJsonArray() &&
objectMsg.get(OBSERVE_ATTRIBUTE_TELEMETRY).getAsJsonObject().has(OBSERVE_LWM2M) &&
!objectMsg.get(OBSERVE_ATTRIBUTE_TELEMETRY).getAsJsonObject().get(OBSERVE_LWM2M).isJsonNull() &&
objectMsg.get(OBSERVE_ATTRIBUTE_TELEMETRY).getAsJsonObject().get(OBSERVE_LWM2M).isJsonArray() &&
objectMsg.get(OBSERVE_ATTRIBUTE_TELEMETRY).getAsJsonObject().has(ATTRIBUTE_LWM2M) &&
!objectMsg.get(OBSERVE_ATTRIBUTE_TELEMETRY).getAsJsonObject().get(ATTRIBUTE_LWM2M).isJsonNull() &&
objectMsg.get(OBSERVE_ATTRIBUTE_TELEMETRY).getAsJsonObject().get(ATTRIBUTE_LWM2M).isJsonObject());
}
private static boolean getValidateBootstrapProfileFromThingsboard(JsonObject objectMsg) {
return (objectMsg != null &&
!objectMsg.isJsonNull() &&
objectMsg.has(BOOTSTRAP) &&
objectMsg.get(BOOTSTRAP).isJsonObject() &&
!objectMsg.get(BOOTSTRAP).isJsonNull() &&
objectMsg.get(BOOTSTRAP).getAsJsonObject().has(SERVERS) &&
!objectMsg.get(BOOTSTRAP).getAsJsonObject().get(SERVERS).isJsonNull() &&
objectMsg.get(BOOTSTRAP).getAsJsonObject().get(SERVERS).isJsonObject() &&
objectMsg.get(BOOTSTRAP).getAsJsonObject().has(BOOTSTRAP_SERVER) &&
!objectMsg.get(BOOTSTRAP).getAsJsonObject().get(BOOTSTRAP_SERVER).isJsonNull() &&
objectMsg.get(BOOTSTRAP).getAsJsonObject().get(BOOTSTRAP_SERVER).isJsonObject() &&
objectMsg.get(BOOTSTRAP).getAsJsonObject().has(LWM2M_SERVER) &&
!objectMsg.get(BOOTSTRAP).getAsJsonObject().get(LWM2M_SERVER).isJsonNull() &&
objectMsg.get(BOOTSTRAP).getAsJsonObject().get(LWM2M_SERVER).isJsonObject());
public static BootstrapConfiguration getBootstrapParametersFromThingsboard(DeviceProfile deviceProfile) {
return toLwM2MClientProfile(deviceProfile).getBootstrap();
}
public static JsonObject validateJson(String jsonStr) {
JsonObject object = null;
if (jsonStr != null && !jsonStr.isEmpty()) {
String jsonValidFlesh = jsonStr.replaceAll("\\\\", "");
jsonValidFlesh = jsonValidFlesh.replaceAll("\n", "");
jsonValidFlesh = jsonValidFlesh.replaceAll("\t", "");
jsonValidFlesh = jsonValidFlesh.replaceAll(" ", "");
String jsonValid = (jsonValidFlesh.charAt(0) == '"' && jsonValidFlesh.charAt(jsonValidFlesh.length() - 1) == '"') ? jsonValidFlesh.substring(1, jsonValidFlesh.length() - 1) : jsonValidFlesh;
try {
object = new JsonParser().parse(jsonValid).getAsJsonObject();
} catch (JsonSyntaxException e) {
log.error("[{}] Fail validateJson [{}]", jsonStr, e.getMessage());
}
}
return object;
}
@SuppressWarnings("unchecked")
public static <T> Optional<T> decode(byte[] byteArray) {
public static String fromVersionedIdToObjectId(String pathIdVer) {
try {
FSTConfiguration config = FSTConfiguration.createDefaultConfiguration();
T msg = (T) config.asObject(byteArray);
return Optional.ofNullable(msg);
} catch (IllegalArgumentException e) {
log.error("Error during deserialization message, [{}]", e.getMessage());
return Optional.empty();
}
}
public static String splitCamelCaseString(String s) {
LinkedList<String> linkedListOut = new LinkedList<>();
LinkedList<String> linkedList = new LinkedList<String>((Arrays.asList(s.split(" "))));
linkedList.forEach(str -> {
String strOut = str.replaceAll("\\W", "").replaceAll("_", "").toUpperCase();
if (strOut.length() > 1) linkedListOut.add(strOut.charAt(0) + strOut.substring(1).toLowerCase());
else linkedListOut.add(strOut);
});
linkedListOut.set(0, (linkedListOut.get(0).substring(0, 1).toLowerCase() + linkedListOut.get(0).substring(1)));
return StringUtils.join(linkedListOut, "");
}
public static <T> TransportServiceCallback<Void> getAckCallback(LwM2mClient lwM2MClient,
int requestId, String typeTopic) {
return new TransportServiceCallback<Void>() {
@Override
public void onSuccess(Void dummy) {
log.trace("[{}] [{}] - requestId [{}] - EndPoint , Access AckCallback", typeTopic, requestId, lwM2MClient.getEndpoint());
}
@Override
public void onError(Throwable e) {
log.trace("[{}] Failed to publish msg", e.toString());
if (pathIdVer == null) {
return null;
}
};
}
public static String convertPathFromIdVerToObjectId(String pathIdVer) {
try {
String[] keyArray = pathIdVer.split(LWM2M_SEPARATOR_PATH);
if (keyArray.length > 1 && keyArray[1].split(LWM2M_SEPARATOR_KEY).length == 2) {
keyArray[1] = keyArray[1].split(LWM2M_SEPARATOR_KEY)[0];
@ -910,7 +185,8 @@ public class LwM2mTransportUtil {
return pathIdVer;
}
} catch (Exception e) {
return null;
log.warn("Issue converting path with version [{}] to path without version: ", pathIdVer, e);
throw new RuntimeException(e);
}
}
@ -940,15 +216,14 @@ public class LwM2mTransportUtil {
if (keyArray.length > 1 && keyArray[1].split(LWM2M_SEPARATOR_KEY).length == 2) {
return pathIdVer;
} else {
LwM2mPath pathObjId = new LwM2mPath(pathIdVer);
return convertPathFromObjectIdToIdVer(pathIdVer, registration);
return convertObjectIdToVersionedId(pathIdVer, registration);
}
}
}
public static String convertPathFromObjectIdToIdVer(String path, Registration registration) {
public static String convertObjectIdToVersionedId(String path, Registration registration) {
String ver = registration.getSupportedObject().get(new LwM2mPath(path).getObjectId());
ver = ver != null ? ver : LWM2M_VERSION_DEFAULT;
ver = ver != null ? ver : LwM2mVersion.VERSION_1_0.getVersion().toString();
try {
String[] keyArray = path.split(LWM2M_SEPARATOR_PATH);
if (keyArray.length > 1) {
@ -1001,12 +276,12 @@ public class LwM2mTransportUtil {
* Attribute pmax = new Attribute(MAXIMUM_PERIOD, "60");
* Attribute [] attrs = {gt, st};
*/
public static SimpleDownlinkRequest createWriteAttributeRequest(String target, Object params, DefaultLwM2MTransportMsgHandler serviceImpl) {
public static SimpleDownlinkRequest createWriteAttributeRequest(String target, Object params, DefaultLwM2MUplinkMsgHandler serviceImpl) {
AttributeSet attrSet = new AttributeSet(createWriteAttributes(params, serviceImpl, target));
return attrSet.getAttributes().size() > 0 ? new WriteAttributesRequest(target, attrSet) : null;
}
private static Attribute[] createWriteAttributes(Object params, DefaultLwM2MTransportMsgHandler serviceImpl, String target) {
private static Attribute[] createWriteAttributes(Object params, DefaultLwM2MUplinkMsgHandler serviceImpl, String target) {
List<Attribute> attributeLists = new ArrayList<>();
ObjectMapper oMapper = new ObjectMapper();
Map<String, Object> map = oMapper.convertValue(params, ConcurrentHashMap.class);
@ -1024,12 +299,6 @@ public class LwM2mTransportUtil {
return attributeLists.toArray(Attribute[]::new);
}
public static Set<String> convertJsonArrayToSet(JsonArray jsonArray) {
List<String> attributeListOld = new Gson().fromJson(jsonArray, new TypeToken<List<String>>() {
}.getType());
return Sets.newConcurrentHashSet(attributeListOld);
}
public static ResourceModel.Type equalsResourceTypeGetSimpleName(Object value) {
switch (value.getClass().getSimpleName()) {
case "Double":
@ -1051,15 +320,7 @@ public class LwM2mTransportUtil {
}
}
public static LwM2mTypeOper setValidTypeOper(String typeOper) {
try {
return LwM2mTransportUtil.LwM2mTypeOper.fromLwLwM2mTypeOper(typeOper);
} catch (Exception e) {
return null;
}
}
public static Object convertWriteAttributes(String type, Object value, DefaultLwM2MTransportMsgHandler serviceImpl, String target) {
public static Object convertWriteAttributes(String type, Object value, DefaultLwM2MUplinkMsgHandler serviceImpl, String target) {
switch (type) {
/** Integer [0:255]; */
case DIMENSION:
@ -1094,16 +355,20 @@ public class LwM2mTransportUtil {
}
}
public static boolean isFwSwWords(String pathName) {
return OtaPackageUtil.getAttributeKey(OtaPackageType.FIRMWARE, OtaPackageKey.VERSION).equals(pathName)
|| OtaPackageUtil.getAttributeKey(OtaPackageType.FIRMWARE, OtaPackageKey.TITLE).equals(pathName)
|| OtaPackageUtil.getAttributeKey(OtaPackageType.FIRMWARE, OtaPackageKey.CHECKSUM).equals(pathName)
|| OtaPackageUtil.getAttributeKey(OtaPackageType.FIRMWARE, OtaPackageKey.CHECKSUM_ALGORITHM).equals(pathName)
|| OtaPackageUtil.getAttributeKey(OtaPackageType.FIRMWARE, OtaPackageKey.SIZE).equals(pathName)
|| OtaPackageUtil.getAttributeKey(OtaPackageType.SOFTWARE, OtaPackageKey.VERSION).equals(pathName)
|| OtaPackageUtil.getAttributeKey(OtaPackageType.SOFTWARE, OtaPackageKey.TITLE).equals(pathName)
|| OtaPackageUtil.getAttributeKey(OtaPackageType.SOFTWARE, OtaPackageKey.CHECKSUM).equals(pathName)
|| OtaPackageUtil.getAttributeKey(OtaPackageType.SOFTWARE, OtaPackageKey.CHECKSUM_ALGORITHM).equals(pathName)
|| OtaPackageUtil.getAttributeKey(OtaPackageType.SOFTWARE, OtaPackageKey.SIZE).equals(pathName);
/**
* @param lwM2MClient -
* @param path -
* @return - return value of Resource by idPath
*/
public static LwM2mResource getResourceValueFromLwM2MClient(LwM2mClient lwM2MClient, String path) {
LwM2mResource lwm2mResourceValue = null;
ResourceValue resourceValue = lwM2MClient.getResources().get(path);
if (resourceValue != null) {
if (new LwM2mPath(fromVersionedIdToObjectId(path)).isResource()) {
lwm2mResourceValue = lwM2MClient.getResources().get(path).getLwM2mResource();
}
}
return lwm2mResourceValue;
}
}

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

@ -72,7 +72,7 @@ public class LwM2mVersionedModelProvider implements LwM2mModelProvider {
public DynamicModel(Registration registration) {
this.registration = registration;
this.tenantId = lwM2mClientContext.getProfile(registration).getTenantId();
this.tenantId = lwM2mClientContext.getClientByEndpoint(registration.getEndpoint()).getTenantId();
}
@Override

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

@ -0,0 +1,221 @@
/**
* 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.lwm2m.server.attributes;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.SettableFuture;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.leshan.core.model.ResourceModel;
import org.eclipse.leshan.core.node.LwM2mPath;
import org.eclipse.leshan.core.node.LwM2mResource;
import org.springframework.stereotype.Service;
import org.thingsboard.server.common.transport.TransportService;
import org.thingsboard.server.common.transport.TransportServiceCallback;
import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.gen.transport.TransportProtos.GetAttributeResponseMsg;
import org.thingsboard.server.queue.util.TbLwM2mTransportComponent;
import org.thingsboard.server.transport.lwm2m.config.LwM2MTransportServerConfig;
import org.thingsboard.server.transport.lwm2m.server.LwM2mTransportServerHelper;
import org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil;
import org.thingsboard.server.transport.lwm2m.server.client.LwM2mClient;
import org.thingsboard.server.transport.lwm2m.server.client.LwM2mClientContext;
import org.thingsboard.server.transport.lwm2m.server.downlink.LwM2mDownlinkMsgHandler;
import org.thingsboard.server.transport.lwm2m.server.downlink.TbLwM2MWriteReplaceRequest;
import org.thingsboard.server.transport.lwm2m.server.downlink.TbLwM2MWriteResponseCallback;
import org.thingsboard.server.transport.lwm2m.server.log.LwM2MTelemetryLogService;
import org.thingsboard.server.transport.lwm2m.server.ota.DefaultLwM2MOtaUpdateService;
import org.thingsboard.server.transport.lwm2m.server.ota.LwM2MOtaUpdateService;
import org.thingsboard.server.transport.lwm2m.server.uplink.LwM2mUplinkMsgHandler;
import org.thingsboard.server.transport.lwm2m.utils.LwM2mValueConverterImpl;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicInteger;
import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportServerHelper.getValueFromKvProto;
import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.LOG_LWM2M_ERROR;
import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.fromVersionedIdToObjectId;
@Slf4j
@Service
@TbLwM2mTransportComponent
@RequiredArgsConstructor
public class DefaultLwM2MAttributesService implements LwM2MAttributesService {
//TODO: add timeout logic
private final AtomicInteger reqIdSeq = new AtomicInteger();
private final Map<Integer, SettableFuture<List<TransportProtos.TsKvProto>>> futures;
private final TransportService transportService;
private final LwM2mTransportServerHelper helper;
private final LwM2mClientContext clientContext;
private final LwM2MTransportServerConfig config;
private final LwM2mUplinkMsgHandler uplinkHandler;
private final LwM2mDownlinkMsgHandler downlinkHandler;
private final LwM2MTelemetryLogService logService;
private final LwM2MOtaUpdateService otaUpdateService;
@Override
public ListenableFuture<List<TransportProtos.TsKvProto>> getSharedAttributes(LwM2mClient client, Collection<String> keys) {
SettableFuture<List<TransportProtos.TsKvProto>> future = SettableFuture.create();
int requestId = reqIdSeq.incrementAndGet();
futures.put(requestId, future);
transportService.process(client.getSession(), TransportProtos.GetAttributeRequestMsg.newBuilder().setRequestId(requestId).
addAllSharedAttributeNames(keys).build(), new TransportServiceCallback<Void>() {
@Override
public void onSuccess(Void msg) {
}
@Override
public void onError(Throwable e) {
SettableFuture<List<TransportProtos.TsKvProto>> callback = futures.remove(requestId);
if (callback != null) {
callback.setException(e);
}
}
});
return future;
}
@Override
public void onGetAttributesResponse(GetAttributeResponseMsg getAttributesResponse, TransportProtos.SessionInfoProto sessionInfo) {
var callback = futures.remove(getAttributesResponse.getRequestId());
if (callback != null) {
callback.set(getAttributesResponse.getSharedAttributeListList());
}
}
/**
* Update - send request in change value resources in Client
* 1. FirmwareUpdate:
* - If msg.getSharedUpdatedList().forEach(tsKvProto -> {tsKvProto.getKv().getKey().indexOf(FIRMWARE_UPDATE_PREFIX, 0) == 0
* 2. Shared Other AttributeUpdate
* -- Path to resources from profile equal keyName or from ModelObject equal name
* -- Only for resources: isWritable && isPresent as attribute in profile -> LwM2MClientProfile (format: CamelCase)
* 3. Delete - nothing
*
* @param msg -
*/
@Override
public void onAttributesUpdate(TransportProtos.AttributeUpdateNotificationMsg msg, TransportProtos.SessionInfoProto sessionInfo) {
LwM2mClient lwM2MClient = clientContext.getClientBySessionInfo(sessionInfo);
if (msg.getSharedUpdatedCount() > 0 && lwM2MClient != null) {
String newFirmwareTitle = null;
String newFirmwareVersion = null;
String newFirmwareUrl = null;
String newSoftwareTitle = null;
String newSoftwareVersion = null;
List<TransportProtos.TsKvProto> otherAttributes = new ArrayList<>();
for (TransportProtos.TsKvProto tsKvProto : msg.getSharedUpdatedList()) {
String attrName = tsKvProto.getKv().getKey();
if (DefaultLwM2MOtaUpdateService.FIRMWARE_TITLE.equals(attrName)) {
newFirmwareTitle = getStrValue(tsKvProto);
} else if (DefaultLwM2MOtaUpdateService.FIRMWARE_VERSION.equals(attrName)) {
newFirmwareVersion = getStrValue(tsKvProto);
} else if (DefaultLwM2MOtaUpdateService.FIRMWARE_URL.equals(attrName)) {
newFirmwareUrl = getStrValue(tsKvProto);
} else if (DefaultLwM2MOtaUpdateService.SOFTWARE_TITLE.equals(attrName)) {
newSoftwareTitle = getStrValue(tsKvProto);
} else if (DefaultLwM2MOtaUpdateService.SOFTWARE_VERSION.equals(attrName)) {
newSoftwareVersion = getStrValue(tsKvProto);
} else {
otherAttributes.add(tsKvProto);
}
}
if (newFirmwareTitle != null || newFirmwareVersion != null) {
otaUpdateService.onTargetFirmwareUpdate(lwM2MClient, newFirmwareTitle, newFirmwareVersion, Optional.ofNullable(newFirmwareUrl));
}
if (newSoftwareTitle != null || newSoftwareVersion != null) {
otaUpdateService.onTargetSoftwareUpdate(lwM2MClient, newSoftwareTitle, newSoftwareVersion);
}
if (!otherAttributes.isEmpty()) {
onAttributesUpdate(lwM2MClient, otherAttributes);
}
} else if (lwM2MClient == null) {
log.error("OnAttributeUpdate, lwM2MClient is null");
}
}
/**
* #1.1 If two names have equal path => last time attribute
* #2.1 if there is a difference in values between the current resource values and the shared attribute values
* => send to client Request Update of value (new value from shared attribute)
* and LwM2MClient.delayedRequests.add(path)
* #2.1 if there is not a difference in values between the current resource values and the shared attribute values
*
*/
@Override
public void onAttributesUpdate(LwM2mClient lwM2MClient, List<TransportProtos.TsKvProto> tsKvProtos) {
log.trace("[{}] onAttributesUpdate [{}]", lwM2MClient.getEndpoint(), tsKvProtos);
tsKvProtos.forEach(tsKvProto -> {
String pathIdVer = clientContext.getObjectIdByKeyNameFromProfile(lwM2MClient, tsKvProto.getKv().getKey());
if (pathIdVer != null) {
// #1.1
if (lwM2MClient.getSharedAttributes().containsKey(pathIdVer)) {
if (tsKvProto.getTs() > lwM2MClient.getSharedAttributes().get(pathIdVer).getTs()) {
lwM2MClient.getSharedAttributes().put(pathIdVer, tsKvProto);
}
} else {
lwM2MClient.getSharedAttributes().put(pathIdVer, tsKvProto);
}
}
});
clientContext.update(lwM2MClient);
// #2.1
lwM2MClient.getSharedAttributes().forEach((pathIdVer, tsKvProto) -> {
this.pushUpdateToClientIfNeeded(lwM2MClient, this.getResourceValueFormatKv(lwM2MClient, pathIdVer),
getValueFromKvProto(tsKvProto.getKv()), pathIdVer);
});
}
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();
downlinkHandler.sendWriteReplaceRequest(lwM2MClient, request, new TbLwM2MWriteResponseCallback(uplinkHandler, logService, lwM2MClient, versionedId));
} else {
log.error("Failed update resource [{}] [{}]", versionedId, newValue);
String logMsg = String.format("%s: Failed update resource versionedId - %s value - %s. Value is not changed or bad",
LOG_LWM2M_ERROR, versionedId, newValue);
logService.log(lwM2MClient, logMsg);
log.info("Failed update resource [{}] [{}]", versionedId, newValue);
}
}
/**
* @param pathIdVer - path resource
* @return - value of Resource into format KvProto or null
*/
private Object getResourceValueFormatKv(LwM2mClient lwM2MClient, String pathIdVer) {
LwM2mResource resourceValue = LwM2mTransportUtil.getResourceValueFromLwM2MClient(lwM2MClient, pathIdVer);
if (resourceValue != null) {
ResourceModel.Type currentType = resourceValue.getType();
ResourceModel.Type expectedType = helper.getResourceModelTypeEqualsKvProtoValueType(currentType, pathIdVer);
return LwM2mValueConverterImpl.getInstance().convertValue(resourceValue.getValue(), currentType, expectedType,
new LwM2mPath(fromVersionedIdToObjectId(pathIdVer)));
} else {
return null;
}
}
private String getStrValue(TransportProtos.TsKvProto tsKvProto) {
return tsKvProto.getKv().getStringV();
}
}

34
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/attributes/LwM2MAttributesService.java

@ -0,0 +1,34 @@
/**
* 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.lwm2m.server.attributes;
import com.google.common.util.concurrent.ListenableFuture;
import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.transport.lwm2m.server.client.LwM2mClient;
import java.util.Collection;
import java.util.List;
public interface LwM2MAttributesService {
ListenableFuture<List<TransportProtos.TsKvProto>> getSharedAttributes(LwM2mClient client, Collection<String> keys);
void onGetAttributesResponse(TransportProtos.GetAttributeResponseMsg getAttributesResponse, TransportProtos.SessionInfoProto sessionInfo);
void onAttributesUpdate(TransportProtos.AttributeUpdateNotificationMsg attributeUpdateNotification, TransportProtos.SessionInfoProto sessionInfo);
void onAttributesUpdate(LwM2mClient lwM2MClient, List<TransportProtos.TsKvProto> tsKvProtos);
}

22
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2MAuthException.java

@ -0,0 +1,22 @@
/**
* 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.lwm2m.server.client;
public class LwM2MAuthException extends RuntimeException {
private static final long serialVersionUID = 4202690897971364044L;
}

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

@ -15,103 +15,85 @@
*/
package org.thingsboard.server.transport.lwm2m.server.client;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.leshan.core.model.ObjectModel;
import org.eclipse.leshan.core.model.ResourceModel;
import org.eclipse.leshan.core.node.LwM2mMultipleResource;
import org.eclipse.leshan.core.node.LwM2mObject;
import org.eclipse.leshan.core.node.LwM2mObjectInstance;
import org.eclipse.leshan.core.node.LwM2mPath;
import org.eclipse.leshan.core.node.LwM2mResource;
import org.eclipse.leshan.core.node.LwM2mSingleResource;
import org.eclipse.leshan.core.node.codec.LwM2mValueConverter;
import org.eclipse.leshan.core.request.ContentFormat;
import org.eclipse.leshan.server.model.LwM2mModelProvider;
import org.eclipse.leshan.server.registration.Registration;
import org.eclipse.leshan.server.security.SecurityInfo;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.ota.OtaPackageType;
import org.thingsboard.server.common.data.device.data.Lwm2mDeviceTransportConfiguration;
import org.thingsboard.server.common.data.device.data.PowerMode;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.transport.auth.ValidateDeviceCredentialsResponse;
import org.thingsboard.server.gen.transport.TransportProtos.SessionInfoProto;
import org.thingsboard.server.gen.transport.TransportProtos.TsKvProto;
import org.thingsboard.server.transport.lwm2m.server.DefaultLwM2MTransportMsgHandler;
import org.thingsboard.server.transport.lwm2m.server.LwM2mQueuedRequest;
import org.thingsboard.server.transport.lwm2m.utils.LwM2mValueConverterImpl;
import org.thingsboard.server.transport.lwm2m.config.LwM2mVersion;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.Serializable;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Queue;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.stream.Collectors;
import static org.eclipse.leshan.core.model.ResourceModel.Type.OPAQUE;
import static org.eclipse.leshan.core.model.ResourceModel.Type.STRING;
import static org.thingsboard.server.common.data.lwm2m.LwM2mConstants.LWM2M_SEPARATOR_PATH;
import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.TRANSPORT_DEFAULT_LWM2M_VERSION;
import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.convertPathFromIdVerToObjectId;
import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.convertPathFromObjectIdToIdVer;
import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.LWM2M_OBJECT_VERSION_DEFAULT;
import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.convertObjectIdToVersionedId;
import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.equalsResourceTypeGetSimpleName;
import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.fromVersionedIdToObjectId;
import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.getVerFromPathIdVerOrId;
@Slf4j
public class LwM2mClient implements Cloneable {
@EqualsAndHashCode(of = {"endpoint"})
public class LwM2mClient implements Serializable {
private static final long serialVersionUID = 8793482946289222623L;
private final String nodeId;
@Getter
private final String endpoint;
private final Lock lock;
@Getter
@Setter
private LwM2MClientState state;
@Getter
private final Map<String, ResourceValue> resources;
@Getter
private final Map<String, TsKvProto> delayedRequests;
@Getter
private final List<String> pendingReadRequests;
@Getter
private final Queue<LwM2mQueuedRequest> queuedRequests;
private transient Lock lock;
@Getter
private String deviceName;
private final Map<String, ResourceValue> resources;
@Getter
private String deviceProfileName;
private final Map<String, TsKvProto> sharedAttributes;
@Getter
private String identity;
private TenantId tenantId;
@Getter
private SecurityInfo securityInfo;
private UUID profileId;
@Getter
private UUID deviceId;
@Getter
private SessionInfoProto session;
@Getter
private UUID profileId;
@Getter
@Setter
private volatile LwM2mFwSwUpdate fwUpdate;
private LwM2MClientState state;
@Getter
@Setter
private volatile LwM2mFwSwUpdate swUpdate;
private SessionInfoProto session;
@Getter
private PowerMode powerMode;
@Getter
@Setter
private Registration registration;
private ValidateDeviceCredentialsResponse credentials;
@Getter
private boolean init;
public Object clone() throws CloneNotSupportedException {
return super.clone();
}
@ -119,27 +101,18 @@ public class LwM2mClient implements Cloneable {
public LwM2mClient(String nodeId, String endpoint) {
this.nodeId = nodeId;
this.endpoint = endpoint;
this.lock = new ReentrantLock();
this.delayedRequests = new ConcurrentHashMap<>();
this.pendingReadRequests = new CopyOnWriteArrayList<>();
this.sharedAttributes = new ConcurrentHashMap<>();
this.resources = new ConcurrentHashMap<>();
this.queuedRequests = new ConcurrentLinkedQueue<>();
this.state = LwM2MClientState.CREATED;
this.lock = new ReentrantLock();
}
public void init(String identity, SecurityInfo securityInfo, ValidateDeviceCredentialsResponse credentials, UUID profileId, UUID sessionId) {
this.identity = identity;
this.securityInfo = securityInfo;
this.credentials = credentials;
this.profileId = profileId;
this.init = false;
if (this.credentials != null && this.credentials.hasDeviceInfo()) {
this.session = createSession(nodeId, sessionId, credentials);
this.deviceId = new UUID(session.getDeviceIdMSB(), session.getDeviceIdLSB());
this.profileId = new UUID(session.getDeviceProfileIdMSB(), session.getDeviceProfileIdLSB());
this.deviceName = session.getDeviceName();
this.deviceProfileName = session.getDeviceType();
}
public void init(ValidateDeviceCredentialsResponse credentials, UUID sessionId) {
this.session = createSession(nodeId, sessionId, credentials);
this.tenantId = new TenantId(new UUID(session.getTenantIdMSB(), session.getTenantIdLSB()));
this.deviceId = new UUID(session.getDeviceIdMSB(), session.getDeviceIdLSB());
this.profileId = new UUID(session.getDeviceProfileIdMSB(), session.getDeviceProfileIdLSB());
this.powerMode = credentials.getDeviceInfo().getPowerMode();
}
public void lock() {
@ -153,12 +126,12 @@ public class LwM2mClient implements Cloneable {
public void onDeviceUpdate(Device device, Optional<DeviceProfile> deviceProfileOpt) {
SessionInfoProto.Builder builder = SessionInfoProto.newBuilder().mergeFrom(session);
this.deviceId = device.getUuidId();
this.deviceName = device.getName();
builder.setDeviceIdMSB(deviceId.getMostSignificantBits());
builder.setDeviceIdLSB(deviceId.getLeastSignificantBits());
builder.setDeviceName(deviceName);
builder.setDeviceName(device.getName());
deviceProfileOpt.ifPresent(deviceProfile -> updateSession(deviceProfile, builder));
this.session = builder.build();
this.powerMode = ((Lwm2mDeviceTransportConfiguration) device.getDeviceData().getTransportConfiguration()).getPowerMode();
}
public void onDeviceProfileUpdate(DeviceProfile deviceProfile) {
@ -168,13 +141,22 @@ public class LwM2mClient implements Cloneable {
}
private void updateSession(DeviceProfile deviceProfile, SessionInfoProto.Builder builder) {
this.deviceProfileName = deviceProfile.getName();
this.profileId = deviceProfile.getUuidId();
builder.setDeviceProfileIdMSB(profileId.getMostSignificantBits());
builder.setDeviceProfileIdLSB(profileId.getLeastSignificantBits());
builder.setDeviceType(this.deviceProfileName);
builder.setDeviceType(deviceProfile.getName());
}
public void refreshSessionId(String nodeId) {
UUID newId = UUID.randomUUID();
SessionInfoProto.Builder builder = SessionInfoProto.newBuilder().mergeFrom(session);
builder.setNodeId(nodeId);
builder.setSessionIdMSB(newId.getMostSignificantBits());
builder.setSessionIdLSB(newId.getLeastSignificantBits());
this.session = builder.build();
}
private SessionInfoProto createSession(String nodeId, UUID sessionId, ValidateDeviceCredentialsResponse msg) {
return SessionInfoProto.newBuilder()
.setNodeId(nodeId)
@ -198,7 +180,7 @@ public class LwM2mClient implements Cloneable {
this.resources.get(pathRezIdVer).setLwM2mResource(rez);
return true;
} else {
LwM2mPath pathIds = new LwM2mPath(convertPathFromIdVerToObjectId(pathRezIdVer));
LwM2mPath pathIds = new LwM2mPath(fromVersionedIdToObjectId(pathRezIdVer));
ResourceModel resourceModel = modelProvider.getObjectModel(registration).getResourceModel(pathIds.getObjectId(), pathIds.getResourceId());
if (resourceModel != null) {
this.resources.put(pathRezIdVer, new ResourceValue(rez, resourceModel));
@ -210,7 +192,7 @@ public class LwM2mClient implements Cloneable {
}
public Object getResourceValue(String pathRezIdVer, String pathRezId) {
String pathRez = pathRezIdVer == null ? convertPathFromObjectIdToIdVer(pathRezId, this.registration) : pathRezIdVer;
String pathRez = pathRezIdVer == null ? convertObjectIdToVersionedId(pathRezId, this.registration) : pathRezIdVer;
if (this.resources.get(pathRez) != null) {
return this.resources.get(pathRez).getLwM2mResource().getValue();
}
@ -218,7 +200,7 @@ public class LwM2mClient implements Cloneable {
}
public Object getResourceNameByRezId(String pathRezIdVer, String pathRezId) {
String pathRez = pathRezIdVer == null ? convertPathFromObjectIdToIdVer(pathRezId, this.registration) : pathRezIdVer;
String pathRez = pathRezIdVer == null ? convertObjectIdToVersionedId(pathRezId, this.registration) : pathRezIdVer;
if (this.resources.get(pathRez) != null) {
return this.resources.get(pathRez).getResourceModel().name;
}
@ -226,7 +208,7 @@ public class LwM2mClient implements Cloneable {
}
public String getRezIdByResourceNameAndObjectInstanceId(String resourceName, String pathObjectInstanceIdVer, LwM2mModelProvider modelProvider) {
LwM2mPath pathIds = new LwM2mPath(convertPathFromIdVerToObjectId(pathObjectInstanceIdVer));
LwM2mPath pathIds = new LwM2mPath(fromVersionedIdToObjectId(pathObjectInstanceIdVer));
if (pathIds.isObjectInstance()) {
Set<Integer> rezIds = modelProvider.getObjectModel(registration)
.getObjectModel(pathIds.getObjectId()).resources.entrySet()
@ -240,26 +222,30 @@ public class LwM2mClient implements Cloneable {
}
public ResourceModel getResourceModel(String pathIdVer, LwM2mModelProvider modelProvider) {
LwM2mPath pathIds = new LwM2mPath(convertPathFromIdVerToObjectId(pathIdVer));
LwM2mPath pathIds = new LwM2mPath(fromVersionedIdToObjectId(pathIdVer));
String verSupportedObject = registration.getSupportedObject().get(pathIds.getObjectId());
String verRez = getVerFromPathIdVerOrId(pathIdVer);
return verRez == null || verRez.equals(verSupportedObject) ? modelProvider.getObjectModel(registration)
.getResourceModel(pathIds.getObjectId(), pathIds.getResourceId()) : null;
}
public boolean isResourceMultiInstances(String pathIdVer, LwM2mModelProvider modelProvider) {
return getResourceModel(pathIdVer, modelProvider).multiple;
}
public ObjectModel getObjectModel(String pathIdVer, LwM2mModelProvider modelProvider) {
LwM2mPath pathIds = new LwM2mPath(convertPathFromIdVerToObjectId(pathIdVer));
LwM2mPath pathIds = new LwM2mPath(fromVersionedIdToObjectId(pathIdVer));
String verSupportedObject = registration.getSupportedObject().get(pathIds.getObjectId());
String verRez = getVerFromPathIdVerOrId(pathIdVer);
return verRez == null || verRez.equals(verSupportedObject) ? modelProvider.getObjectModel(registration)
.getObjectModel(pathIds.getObjectId()) : null;
}
public String objectToString(LwM2mObject lwM2mObject, LwM2mValueConverterImpl converter, String pathIdVer) {
public String objectToString(LwM2mObject lwM2mObject) {
StringBuilder builder = new StringBuilder();
builder.append("LwM2mObject [id=").append(lwM2mObject.getId()).append(", instances={");
lwM2mObject.getInstances().forEach((instId, inst) -> {
builder.append(instId).append("=").append(this.instanceToString(inst, converter, pathIdVer)).append(", ");
builder.append(instId).append("=").append(this.instanceToString(inst)).append(", ");
});
int startInd = builder.lastIndexOf(", ");
if (startInd > 0) {
@ -269,11 +255,11 @@ public class LwM2mClient implements Cloneable {
return builder.toString();
}
public String instanceToString(LwM2mObjectInstance objectInstance, LwM2mValueConverterImpl converter, String pathIdVer) {
public String instanceToString(LwM2mObjectInstance objectInstance) {
StringBuilder builder = new StringBuilder();
builder.append("LwM2mObjectInstance [id=").append(objectInstance.getId()).append(", resources={");
objectInstance.getResources().forEach((resId, res) -> {
builder.append(resId).append("=").append(this.resourceToString(res, converter, pathIdVer)).append(", ");
builder.append(resId).append("=").append(this.resourceToString(res)).append(", ");
});
int startInd = builder.lastIndexOf(", ");
if (startInd > 0) {
@ -283,25 +269,18 @@ public class LwM2mClient implements Cloneable {
return builder.toString();
}
public String resourceToString(LwM2mResource lwM2mResource, LwM2mValueConverterImpl converter, String pathIdVer) {
if (!OPAQUE.equals(lwM2mResource.getType())) {
return lwM2mResource.isMultiInstances() ? ((LwM2mMultipleResource) lwM2mResource).toString() :
((LwM2mSingleResource) lwM2mResource).toString();
} else {
return String.format("LwM2mSingleResource [id=%s, value=%s, type=%s]", lwM2mResource.getId(),
converter.convertValue(lwM2mResource.getValue(),
OPAQUE, STRING, new LwM2mPath(convertPathFromIdVerToObjectId(pathIdVer))), lwM2mResource.getType().name());
}
public String resourceToString(LwM2mResource lwM2mResource) {
return lwM2mResource.isMultiInstances() ? lwM2mResource.getInstances().toString() : lwM2mResource.getValue().toString();
}
public Collection<LwM2mResource> getNewResourceForInstance(String pathRezIdVer, Object params, LwM2mModelProvider modelProvider,
LwM2mValueConverterImpl converter) {
LwM2mPath pathIds = new LwM2mPath(convertPathFromIdVerToObjectId(pathRezIdVer));
LwM2mValueConverter converter) {
LwM2mPath pathIds = new LwM2mPath(fromVersionedIdToObjectId(pathRezIdVer));
Collection<LwM2mResource> resources = ConcurrentHashMap.newKeySet();
Map<Integer, ResourceModel> resourceModels = modelProvider.getObjectModel(registration)
.getObjectModel(pathIds.getObjectId()).resources;
resourceModels.forEach((resId, resourceModel) -> {
if (resId == pathIds.getResourceId()) {
if (resId.equals(pathIds.getResourceId())) {
resources.add(LwM2mSingleResource.newResource(resId, converter.convertValue(params,
equalsResourceTypeGetSimpleName(params), resourceModel.type, pathIds), resourceModel.type));
@ -311,14 +290,14 @@ public class LwM2mClient implements Cloneable {
}
public Collection<LwM2mResource> getNewResourcesForInstance(String pathRezIdVer, Object params, LwM2mModelProvider modelProvider,
LwM2mValueConverterImpl converter) {
LwM2mPath pathIds = new LwM2mPath(convertPathFromIdVerToObjectId(pathRezIdVer));
LwM2mValueConverter converter) {
LwM2mPath pathIds = new LwM2mPath(fromVersionedIdToObjectId(pathRezIdVer));
Collection<LwM2mResource> resources = ConcurrentHashMap.newKeySet();
Map<Integer, ResourceModel> resourceModels = modelProvider.getObjectModel(registration)
.getObjectModel(pathIds.getObjectId()).resources;
resourceModels.forEach((resId, resourceModel) -> {
if (((ConcurrentHashMap) params).containsKey(String.valueOf(resId))) {
Object value = ((ConcurrentHashMap) params).get((String.valueOf(resId)));
if (((Map) params).containsKey(String.valueOf(resId))) {
Object value = ((Map) params).get((String.valueOf(resId)));
resources.add(LwM2mSingleResource.newResource(resId,
converter.convertValue(value, equalsResourceTypeGetSimpleName(value), resourceModel.type, pathIds), resourceModel.type));
@ -327,11 +306,14 @@ public class LwM2mClient implements Cloneable {
return resources;
}
public boolean isValidObjectVersion(String path) {
LwM2mPath pathIds = new LwM2mPath(convertPathFromIdVerToObjectId(path));
public void isValidObjectVersion(String path) {
LwM2mPath pathIds = new LwM2mPath(fromVersionedIdToObjectId(path));
String verSupportedObject = registration.getSupportedObject().get(pathIds.getObjectId());
String verRez = getVerFromPathIdVerOrId(path);
return verRez == null ? TRANSPORT_DEFAULT_LWM2M_VERSION.equals(verSupportedObject) : verRez.equals(verSupportedObject);
if ((verRez != null && !verRez.equals(verSupportedObject)) ||
(verRez == null && !LWM2M_OBJECT_VERSION_DEFAULT.equals(verSupportedObject))) {
throw new IllegalArgumentException(String.format("Specified resource id %s is not valid version! Must be version: %s", path, verSupportedObject));
}
}
/**
@ -341,7 +323,7 @@ public class LwM2mClient implements Cloneable {
public void deleteResources(String pathIdVer, LwM2mModelProvider modelProvider) {
Set<String> key = getKeysEqualsIdVer(pathIdVer);
key.forEach(pathRez -> {
LwM2mPath pathIds = new LwM2mPath(convertPathFromIdVerToObjectId(pathRez));
LwM2mPath pathIds = new LwM2mPath(fromVersionedIdToObjectId(pathRez));
ResourceModel resourceModel = modelProvider.getObjectModel(registration).getResourceModel(pathIds.getObjectId(), pathIds.getResourceId());
if (resourceModel != null) {
this.resources.get(pathRez).setResourceModel(resourceModel);
@ -361,7 +343,7 @@ public class LwM2mClient implements Cloneable {
}
private void saveResourceModel(String pathRez, LwM2mModelProvider modelProvider) {
LwM2mPath pathIds = new LwM2mPath(convertPathFromIdVerToObjectId(pathRez));
LwM2mPath pathIds = new LwM2mPath(fromVersionedIdToObjectId(pathRez));
ResourceModel resourceModel = modelProvider.getObjectModel(registration).getResourceModel(pathIds.getObjectId(), pathIds.getResourceId());
this.resources.get(pathRez).setResourceModel(resourceModel);
}
@ -373,40 +355,17 @@ public class LwM2mClient implements Cloneable {
.collect(Collectors.toSet());
}
public void initReadValue(DefaultLwM2MTransportMsgHandler serviceImpl, String path) {
if (path != null) {
this.pendingReadRequests.remove(path);
}
if (this.pendingReadRequests.size() == 0) {
this.init = true;
serviceImpl.putDelayedUpdateResourcesThingsboard(this);
}
}
public ContentFormat getDefaultContentFormat() {
if (registration == null) {
return ContentFormat.DEFAULT;
} else if (registration.getLwM2mVersion().equals("1.0")) {
return ContentFormat.TLV;
} else {
return ContentFormat.TEXT;
return LwM2mVersion.fromVersionStr(registration.getLwM2mVersion()).getContentFormat();
}
}
public LwM2mFwSwUpdate getFwUpdate (LwM2mClientContext clientContext) {
if (this.fwUpdate == null) {
LwM2mClientProfile lwM2mClientProfile = clientContext.getProfile(this.getProfileId());
this.fwUpdate = new LwM2mFwSwUpdate(this, OtaPackageType.FIRMWARE, lwM2mClientProfile.getFwUpdateStrategy());
}
return this.fwUpdate;
}
public LwM2mFwSwUpdate getSwUpdate (LwM2mClientContext clientContext) {
if (this.swUpdate == null) {
LwM2mClientProfile lwM2mClientProfile = clientContext.getProfile(this.getProfileId());
this.swUpdate = new LwM2mFwSwUpdate(this, OtaPackageType.SOFTWARE, lwM2mClientProfile.getSwUpdateStrategy());
}
return this.fwUpdate;
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject();
this.lock = new ReentrantLock();
}
}

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

@ -17,23 +17,22 @@ package org.thingsboard.server.transport.lwm2m.server.client;
import org.eclipse.leshan.server.registration.Registration;
import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.device.profile.Lwm2mDeviceProfileTransportConfiguration;
import org.thingsboard.server.common.transport.auth.ValidateDeviceCredentialsResponse;
import org.thingsboard.server.gen.transport.TransportProtos;
import java.util.Collection;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
public interface LwM2mClientContext {
LwM2mClient getClientByRegistrationId(String registrationId);
LwM2mClient getClientByEndpoint(String endpoint);
LwM2mClient getClientBySessionInfo(TransportProtos.SessionInfoProto sessionInfo);
void register(LwM2mClient lwM2MClient, Registration registration) throws LwM2MClientStateException;
Optional<TransportProtos.SessionInfoProto> register(LwM2mClient lwM2MClient, Registration registration) throws LwM2MClientStateException;
void updateRegistration(LwM2mClient client, Registration registration) throws LwM2MClientStateException;
@ -41,21 +40,24 @@ public interface LwM2mClientContext {
Collection<LwM2mClient> getLwM2mClients();
Map<UUID, LwM2mClientProfile> getProfiles();
LwM2mClientProfile getProfile(UUID profileUuId);
LwM2mClientProfile getProfile(Registration registration);
//TODO: replace UUID with DeviceProfileId
Lwm2mDeviceProfileTransportConfiguration getProfile(UUID profileUuId);
Map<UUID, LwM2mClientProfile> setProfiles(Map<UUID, LwM2mClientProfile> profiles);
Lwm2mDeviceProfileTransportConfiguration getProfile(Registration registration);
LwM2mClientProfile profileUpdate(DeviceProfile deviceProfile);
Lwm2mDeviceProfileTransportConfiguration profileUpdate(DeviceProfile deviceProfile);
Set<String> getSupportedIdVerInClient(LwM2mClient registration);
LwM2mClient getClientByDeviceId(UUID deviceId);
String getObjectIdByKeyNameFromProfile(LwM2mClient lwM2mClient, String keyName);
void registerClient(Registration registration, ValidateDeviceCredentialsResponse credentials);
void update(LwM2mClient lwM2MClient);
void removeCredentials(TransportProtos.SessionInfoProto sessionInfo);
void sendMsgsAfterSleeping(LwM2mClient lwM2MClient);
}

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

@ -17,17 +17,32 @@ package org.thingsboard.server.transport.lwm2m.server.client;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.leshan.core.SecurityMode;
import org.eclipse.leshan.core.model.ResourceModel;
import org.eclipse.leshan.core.node.LwM2mPath;
import org.eclipse.leshan.server.registration.Registration;
import org.springframework.beans.factory.annotation.Autowired;
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.profile.Lwm2mDeviceProfileTransportConfiguration;
import org.thingsboard.server.common.data.id.DeviceProfileId;
import org.thingsboard.server.common.transport.TransportDeviceProfileCache;
import org.thingsboard.server.common.transport.TransportServiceCallback;
import org.thingsboard.server.common.transport.auth.ValidateDeviceCredentialsResponse;
import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.queue.util.AfterStartUp;
import org.thingsboard.server.queue.util.TbLwM2mTransportComponent;
import org.thingsboard.server.transport.lwm2m.config.LwM2MTransportServerConfig;
import org.thingsboard.server.transport.lwm2m.secure.TbLwM2MSecurityInfo;
import org.thingsboard.server.transport.lwm2m.server.LwM2mTransportContext;
import org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil;
import org.thingsboard.server.transport.lwm2m.server.store.TbEditableSecurityStore;
import org.thingsboard.server.transport.lwm2m.server.ota.LwM2MOtaUpdateService;
import org.thingsboard.server.transport.lwm2m.server.session.LwM2MSessionManager;
import org.thingsboard.server.transport.lwm2m.server.store.TbLwM2MClientStore;
import org.thingsboard.server.transport.lwm2m.server.store.TbMainSecurityStore;
import org.thingsboard.server.transport.lwm2m.server.uplink.DefaultLwM2MUplinkMsgHandler;
import java.util.Arrays;
import java.util.Collection;
@ -39,7 +54,9 @@ import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Predicate;
import static org.eclipse.leshan.core.SecurityMode.NO_SEC;
import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.convertPathFromObjectIdToIdVer;
import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.convertObjectIdToVersionedId;
import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.fromVersionedIdToObjectId;
import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.validateObjectVerFromKey;
@Slf4j
@Service
@ -48,75 +65,135 @@ import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.c
public class LwM2mClientContextImpl implements LwM2mClientContext {
private final LwM2mTransportContext context;
private final TbEditableSecurityStore securityStore;
private final LwM2MTransportServerConfig config;
private final TbMainSecurityStore securityStore;
private final TbLwM2MClientStore clientStore;
private final LwM2MSessionManager sessionManager;
private final TransportDeviceProfileCache deviceProfileCache;
@Autowired
@Lazy
private DefaultLwM2MUplinkMsgHandler defaultLwM2MUplinkMsgHandler;
@Autowired
@Lazy
private LwM2MOtaUpdateService otaUpdateService;
private final Map<String, LwM2mClient> lwM2mClientsByEndpoint = new ConcurrentHashMap<>();
private final Map<String, LwM2mClient> lwM2mClientsByRegistrationId = new ConcurrentHashMap<>();
private Map<UUID, LwM2mClientProfile> profiles = new ConcurrentHashMap<>();
private final Map<UUID, Lwm2mDeviceProfileTransportConfiguration> profiles = new ConcurrentHashMap<>();
@AfterStartUp
public void init() {
String nodeId = context.getNodeId();
Set<LwM2mClient> fetchedClients = clientStore.getAll();
log.debug("Fetched clients from store: {}", fetchedClients);
fetchedClients.forEach(client -> {
lwM2mClientsByEndpoint.put(client.getEndpoint(), client);
updateFetchedClient(nodeId, client);
});
}
@Override
public LwM2mClient getClientByEndpoint(String endpoint) {
return lwM2mClientsByEndpoint.computeIfAbsent(endpoint, ep -> new LwM2mClient(context.getNodeId(), ep));
return lwM2mClientsByEndpoint.computeIfAbsent(endpoint, ep -> {
LwM2mClient client = clientStore.get(ep);
String nodeId = context.getNodeId();
if (client == null) {
log.info("[{}] initialized new client.", endpoint);
client = new LwM2mClient(nodeId, ep);
} else {
log.debug("[{}] fetched client from store: {}", endpoint, client);
updateFetchedClient(nodeId, client);
}
return client;
});
}
private void updateFetchedClient(String nodeId, LwM2mClient client) {
boolean updated = false;
if (client.getRegistration() != null) {
lwM2mClientsByRegistrationId.put(client.getRegistration().getId(), client);
}
if (client.getSession() != null) {
client.refreshSessionId(nodeId);
sessionManager.register(client.getSession());
updated = true;
}
if (updated) {
clientStore.put(client);
}
}
@Override
public void register(LwM2mClient lwM2MClient, Registration registration) throws LwM2MClientStateException {
lwM2MClient.lock();
public Optional<TransportProtos.SessionInfoProto> register(LwM2mClient client, Registration registration) throws LwM2MClientStateException {
TransportProtos.SessionInfoProto oldSession = null;
client.lock();
try {
if (LwM2MClientState.UNREGISTERED.equals(lwM2MClient.getState())) {
throw new LwM2MClientStateException(lwM2MClient.getState(), "Client is in invalid state.");
if (LwM2MClientState.UNREGISTERED.equals(client.getState())) {
throw new LwM2MClientStateException(client.getState(), "Client is in invalid state.");
}
TbLwM2MSecurityInfo securityInfo = securityStore.getTbLwM2MSecurityInfoByEndpoint(lwM2MClient.getEndpoint());
oldSession = client.getSession();
TbLwM2MSecurityInfo securityInfo = securityStore.getTbLwM2MSecurityInfoByEndpoint(client.getEndpoint());
if (securityInfo.getSecurityMode() != null) {
if (SecurityMode.X509.equals(securityInfo.getSecurityMode())) {
securityStore.registerX509(registration.getEndpoint(), registration.getId());
}
if (securityInfo.getDeviceProfile() != null) {
UUID profileUuid = profileUpdate(securityInfo.getDeviceProfile()) != null ? securityInfo.getDeviceProfile().getUuidId() : null;
profileUpdate(securityInfo.getDeviceProfile());
if (securityInfo.getSecurityInfo() != null) {
lwM2MClient.init(securityInfo.getSecurityInfo().getIdentity(), securityInfo.getSecurityInfo(), securityInfo.getMsg(), profileUuid, UUID.randomUUID());
client.init(securityInfo.getMsg(), UUID.randomUUID());
} else if (NO_SEC.equals(securityInfo.getSecurityMode())) {
lwM2MClient.init(null, null, securityInfo.getMsg(), profileUuid, UUID.randomUUID());
client.init(securityInfo.getMsg(), UUID.randomUUID());
} else {
throw new RuntimeException(String.format("Registration failed: device %s not found.", lwM2MClient.getEndpoint()));
throw new RuntimeException(String.format("Registration failed: device %s not found.", client.getEndpoint()));
}
} else {
throw new RuntimeException(String.format("Registration failed: device %s not found.", lwM2MClient.getEndpoint()));
throw new RuntimeException(String.format("Registration failed: device %s not found.", client.getEndpoint()));
}
} else {
throw new RuntimeException(String.format("Registration failed: FORBIDDEN, endpointId: %s", lwM2MClient.getEndpoint()));
throw new RuntimeException(String.format("Registration failed: FORBIDDEN, endpointId: %s", client.getEndpoint()));
}
lwM2MClient.setRegistration(registration);
this.lwM2mClientsByRegistrationId.put(registration.getId(), lwM2MClient);
lwM2MClient.setState(LwM2MClientState.REGISTERED);
client.setRegistration(registration);
this.lwM2mClientsByRegistrationId.put(registration.getId(), client);
client.setState(LwM2MClientState.REGISTERED);
clientStore.put(client);
} finally {
lwM2MClient.unlock();
client.unlock();
}
return Optional.ofNullable(oldSession);
}
@Override
public void updateRegistration(LwM2mClient lwM2MClient, Registration registration) throws LwM2MClientStateException {
lwM2MClient.lock();
public void updateRegistration(LwM2mClient client, Registration registration) throws LwM2MClientStateException {
client.lock();
try {
if (!LwM2MClientState.REGISTERED.equals(lwM2MClient.getState())) {
throw new LwM2MClientStateException(lwM2MClient.getState(), "Client is in invalid state.");
if (!LwM2MClientState.REGISTERED.equals(client.getState())) {
throw new LwM2MClientStateException(client.getState(), "Client is in invalid state.");
}
lwM2MClient.setRegistration(registration);
client.setRegistration(registration);
clientStore.put(client);
sendMsgsAfterSleeping(client);
} finally {
lwM2MClient.unlock();
client.unlock();
}
}
@Override
public void unregister(LwM2mClient lwM2MClient, Registration registration) throws LwM2MClientStateException {
lwM2MClient.lock();
public void unregister(LwM2mClient client, Registration registration) throws LwM2MClientStateException {
client.lock();
try {
if (!LwM2MClientState.REGISTERED.equals(lwM2MClient.getState())) {
throw new LwM2MClientStateException(lwM2MClient.getState(), "Client is in invalid state.");
if (!LwM2MClientState.REGISTERED.equals(client.getState())) {
throw new LwM2MClientStateException(client.getState(), "Client is in invalid state.");
}
lwM2mClientsByRegistrationId.remove(registration.getId());
Registration currentRegistration = lwM2MClient.getRegistration();
Registration currentRegistration = client.getRegistration();
if (currentRegistration.getId().equals(registration.getId())) {
lwM2MClient.setState(LwM2MClientState.UNREGISTERED);
lwM2mClientsByEndpoint.remove(lwM2MClient.getEndpoint());
this.securityStore.remove(lwM2MClient.getEndpoint());
UUID profileId = lwM2MClient.getProfileId();
client.setState(LwM2MClientState.UNREGISTERED);
lwM2mClientsByEndpoint.remove(client.getEndpoint());
// TODO: change tests to use new certificate.
// this.securityStore.remove(client.getEndpoint(), registration.getId());
clientStore.remove(client.getEndpoint());
UUID profileId = client.getProfileId();
if (profileId != null) {
Optional<LwM2mClient> otherClients = lwM2mClientsByRegistrationId.values().stream().filter(e -> e.getProfileId().equals(profileId)).findFirst();
if (otherClients.isEmpty()) {
@ -124,24 +201,19 @@ public class LwM2mClientContextImpl implements LwM2mClientContext {
}
}
} else {
throw new LwM2MClientStateException(lwM2MClient.getState(), "Client has different registration.");
throw new LwM2MClientStateException(client.getState(), "Client has different registration.");
}
} finally {
lwM2MClient.unlock();
client.unlock();
}
}
@Override
public LwM2mClient getClientByRegistrationId(String registrationId) {
return lwM2mClientsByRegistrationId.get(registrationId);
}
@Override
public LwM2mClient getClientBySessionInfo(TransportProtos.SessionInfoProto sessionInfo) {
LwM2mClient lwM2mClient = null;
UUID sessionId = new UUID(sessionInfo.getSessionIdMSB(), sessionInfo.getSessionIdLSB());
Predicate<LwM2mClient> isClientFilter = c ->
(new UUID(sessionInfo.getSessionIdMSB(), sessionInfo.getSessionIdLSB()))
.equals((new UUID(c.getSession().getSessionIdMSB(), c.getSession().getSessionIdLSB())));
sessionId.equals((new UUID(c.getSession().getSessionIdMSB(), c.getSession().getSessionIdLSB())));
if (this.lwM2mClientsByEndpoint.size() > 0) {
lwM2mClient = this.lwM2mClientsByEndpoint.values().stream().filter(isClientFilter).findAny().orElse(null);
}
@ -149,13 +221,21 @@ public class LwM2mClientContextImpl implements LwM2mClientContext {
lwM2mClient = this.lwM2mClientsByRegistrationId.values().stream().filter(isClientFilter).findAny().orElse(null);
}
if (lwM2mClient == null) {
log.warn("Device TimeOut? lwM2mClient is null.");
log.warn("SessionInfo input [{}], lwM2mClientsByEndpoint size: [{}] lwM2mClientsByRegistrationId: [{}]", sessionInfo, lwM2mClientsByEndpoint.values(), lwM2mClientsByRegistrationId.values());
log.error("", new RuntimeException());
log.error("[{}] Failed to lookup client by session id.", sessionId);
}
return lwM2mClient;
}
@Override
public String getObjectIdByKeyNameFromProfile(LwM2mClient client, String keyName) {
Lwm2mDeviceProfileTransportConfiguration profile = getProfile(client.getProfileId());
return profile.getObserveAttr().getKeyName().entrySet().stream()
.filter(e -> e.getValue().equals(keyName) && validateResourceInModel(client, e.getKey(), false)).findFirst().orElseThrow(
() -> new IllegalArgumentException(keyName + " is not configured in the device profile!")
).getKey();
}
public Registration getRegistration(String registrationId) {
return this.lwM2mClientsByRegistrationId.get(registrationId).getRegistration();
}
@ -163,46 +243,83 @@ public class LwM2mClientContextImpl implements LwM2mClientContext {
@Override
public void registerClient(Registration registration, ValidateDeviceCredentialsResponse credentials) {
LwM2mClient client = getClientByEndpoint(registration.getEndpoint());
client.init(null, null, credentials, credentials.getDeviceProfile().getUuidId(), UUID.randomUUID());
client.init(credentials, UUID.randomUUID());
lwM2mClientsByRegistrationId.put(registration.getId(), client);
profileUpdate(credentials.getDeviceProfile());
}
@Override
public Collection<LwM2mClient> getLwM2mClients() {
return lwM2mClientsByEndpoint.values();
public void update(LwM2mClient client) {
client.lock();
try {
clientStore.put(client);
} finally {
client.unlock();
}
}
@Override
public Map<UUID, LwM2mClientProfile> getProfiles() {
return profiles;
public void removeCredentials(TransportProtos.SessionInfoProto sessionInfo) {
//TODO: implement
}
@Override
public LwM2mClientProfile getProfile(UUID profileId) {
return profiles.get(profileId);
public void sendMsgsAfterSleeping(LwM2mClient lwM2MClient) {
if (LwM2MClientState.REGISTERED.equals(lwM2MClient.getState())) {
PowerMode powerMode = lwM2MClient.getPowerMode();
if (powerMode == null) {
Lwm2mDeviceProfileTransportConfiguration deviceProfile = getProfile(lwM2MClient.getProfileId());
powerMode = deviceProfile.getClientLwM2mSettings().getPowerMode();
}
if (PowerMode.PSM.equals(powerMode) || PowerMode.E_DRX.equals(powerMode)) {
defaultLwM2MUplinkMsgHandler.initAttributes(lwM2MClient);
TransportProtos.TransportToDeviceActorMsg persistentRpcRequestMsg = TransportProtos.TransportToDeviceActorMsg
.newBuilder()
.setSessionInfo(lwM2MClient.getSession())
.setSendPendingRPC(TransportProtos.SendPendingRPCMsg.newBuilder().build())
.build();
context.getTransportService().process(persistentRpcRequestMsg, TransportServiceCallback.EMPTY);
otaUpdateService.init(lwM2MClient);
}
}
}
@Override
public LwM2mClientProfile getProfile(Registration registration) {
return this.getProfiles().get(getClientByEndpoint(registration.getEndpoint()).getProfileId());
public Collection<LwM2mClient> getLwM2mClients() {
return lwM2mClientsByEndpoint.values();
}
@Override
public Map<UUID, LwM2mClientProfile> setProfiles(Map<UUID, LwM2mClientProfile> profiles) {
return this.profiles = profiles;
public Lwm2mDeviceProfileTransportConfiguration getProfile(UUID profileId) {
return doGetAndCache(profileId);
}
@Override
public LwM2mClientProfile profileUpdate(DeviceProfile deviceProfile) {
LwM2mClientProfile lwM2MClientProfile = deviceProfile != null ?
LwM2mTransportUtil.toLwM2MClientProfile(deviceProfile) : null;
if (lwM2MClientProfile != null) {
profiles.put(deviceProfile.getUuidId(), lwM2MClientProfile);
return lwM2MClientProfile;
} else {
return null;
public Lwm2mDeviceProfileTransportConfiguration getProfile(Registration registration) {
UUID profileId = getClientByEndpoint(registration.getEndpoint()).getProfileId();
return doGetAndCache(profileId);
}
private Lwm2mDeviceProfileTransportConfiguration doGetAndCache(UUID profileId) {
Lwm2mDeviceProfileTransportConfiguration result = profiles.get(profileId);
if (result == null) {
log.debug("Fetching profile [{}]", profileId);
DeviceProfile deviceProfile = deviceProfileCache.get(new DeviceProfileId(profileId));
if (deviceProfile != null) {
result = profileUpdate(deviceProfile);
} else {
log.info("Device profile was not found! Most probably device profile [{}] has been removed from the database.", profileId);
}
}
return result;
}
@Override
public Lwm2mDeviceProfileTransportConfiguration profileUpdate(DeviceProfile deviceProfile) {
Lwm2mDeviceProfileTransportConfiguration clientProfile = LwM2mTransportUtil.toLwM2MClientProfile(deviceProfile);
profiles.put(deviceProfile.getUuidId(), clientProfile);
return clientProfile;
}
@Override
@ -211,7 +328,7 @@ public class LwM2mClientContextImpl implements LwM2mClientContext {
Arrays.stream(client.getRegistration().getObjectLinks()).forEach(link -> {
LwM2mPath pathIds = new LwM2mPath(link.getUrl());
if (!pathIds.isRoot()) {
clientObjects.add(convertPathFromObjectIdToIdVer(link.getUrl(), client.getRegistration()));
clientObjects.add(convertObjectIdToVersionedId(link.getUrl(), client.getRegistration()));
}
});
return (clientObjects.size() > 0) ? clientObjects : null;
@ -222,4 +339,14 @@ public class LwM2mClientContextImpl implements LwM2mClientContext {
return lwM2mClientsByRegistrationId.values().stream().filter(e -> deviceId.equals(e.getDeviceId())).findFirst().orElse(null);
}
private boolean validateResourceInModel(LwM2mClient lwM2mClient, String pathIdVer, boolean isWritableNotOptional) {
ResourceModel resourceModel = lwM2mClient.getResourceModel(pathIdVer, this.config
.getModelProvider());
Integer objectId = new LwM2mPath(fromVersionedIdToObjectId(pathIdVer)).getObjectId();
String objectVer = validateObjectVerFromKey(pathIdVer);
return resourceModel != null && (isWritableNotOptional ?
objectId != null && objectVer != null && objectVer.equals(lwM2mClient.getRegistration().getSupportedVersion(objectId)) && resourceModel.operations.isWritable() :
objectId != null && objectVer != null && objectVer.equals(lwM2mClient.getRegistration().getSupportedVersion(objectId)));
}
}

113
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClientProfile.java

@ -1,113 +0,0 @@
/**
* 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.lwm2m.server.client;
import com.google.gson.Gson;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import lombok.Data;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.LwM2MClientStrategy;
import org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.LwM2MFirmwareUpdateStrategy;
import org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.LwM2MSoftwareUpdateStrategy;
@Data
public class LwM2mClientProfile {
private final String clientStrategyStr = "clientStrategy";
private final String fwUpdateStrategyStr = "fwUpdateStrategy";
private final String swUpdateStrategyStr = "swUpdateStrategy";
private TenantId tenantId;
/**
* "clientLwM2mSettings": {
* "fwUpdateStrategy": "1",
* "swUpdateStrategy": "1",
* "clientStrategy": "1"
* }
**/
private JsonObject postClientLwM2mSettings;
/**
* {"keyName": {
* "/3_1.0/0/1": "modelNumber",
* "/3_1.0/0/0": "manufacturer",
* "/3_1.0/0/2": "serialNumber"
* }
**/
private JsonObject postKeyNameProfile;
/**
* [ "/3_1.0/0/0", "/3_1.0/0/1"]
*/
private JsonArray postAttributeProfile;
/**
* [ "/3_1.0/0/0", "/3_1.0/0/2"]
*/
private JsonArray postTelemetryProfile;
/**
* [ "/3_1.0/0", "/3_1.0/0/1, "/3_1.0/0/2"]
*/
private JsonArray postObserveProfile;
/**
* "attributeLwm2m": {"/3_1.0": {"ver": "currentTimeTest11"},
* "/3_1.0/0": {"gt": 17},
* "/3_1.0/0/9": {"pmax": 45}, "/3_1.2": {ver": "3_1.2"}}
*/
private JsonObject postAttributeLwm2mProfile;
public LwM2mClientProfile clone() {
LwM2mClientProfile lwM2mClientProfile = new LwM2mClientProfile();
lwM2mClientProfile.postClientLwM2mSettings = this.deepCopy(this.postClientLwM2mSettings, JsonObject.class);
lwM2mClientProfile.postKeyNameProfile = this.deepCopy(this.postKeyNameProfile, JsonObject.class);
lwM2mClientProfile.postAttributeProfile = this.deepCopy(this.postAttributeProfile, JsonArray.class);
lwM2mClientProfile.postTelemetryProfile = this.deepCopy(this.postTelemetryProfile, JsonArray.class);
lwM2mClientProfile.postObserveProfile = this.deepCopy(this.postObserveProfile, JsonArray.class);
lwM2mClientProfile.postAttributeLwm2mProfile = this.deepCopy(this.postAttributeLwm2mProfile, JsonObject.class);
return lwM2mClientProfile;
}
private <T> T deepCopy(T elements, Class<T> type) {
try {
Gson gson = new Gson();
return gson.fromJson(gson.toJson(elements), type);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
public int getClientStrategy() {
return this.postClientLwM2mSettings.getAsJsonObject().has(this.clientStrategyStr) ?
Integer.parseInt(this.postClientLwM2mSettings.getAsJsonObject().get(this.clientStrategyStr).getAsString()) :
LwM2MClientStrategy.CLIENT_STRATEGY_1.code;
}
public int getFwUpdateStrategy() {
return this.postClientLwM2mSettings.getAsJsonObject().has(this.fwUpdateStrategyStr) ?
Integer.parseInt(this.postClientLwM2mSettings.getAsJsonObject().get(this.fwUpdateStrategyStr).getAsString()) :
LwM2MFirmwareUpdateStrategy.OBJ_5_BINARY.code;
}
public int getSwUpdateStrategy() {
return this.postClientLwM2mSettings.getAsJsonObject().has(this.swUpdateStrategyStr) ?
Integer.parseInt(this.postClientLwM2mSettings.getAsJsonObject().get(this.swUpdateStrategyStr).getAsString()) :
LwM2MSoftwareUpdateStrategy.BINARY.code;
}
}

281
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClientRpcRequest.java

@ -1,281 +0,0 @@
/**
* 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.lwm2m.server.client;
import com.google.gson.Gson;
import com.google.gson.JsonObject;
import com.google.gson.reflect.TypeToken;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.eclipse.leshan.core.node.LwM2mPath;
import org.eclipse.leshan.server.registration.Registration;
import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.transport.lwm2m.server.DefaultLwM2MTransportMsgHandler;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeoutException;
import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.ERROR_KEY;
import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.FINISH_JSON_KEY;
import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.FINISH_VALUE_KEY;
import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.INFO_KEY;
import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.KEY_NAME_KEY;
import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.LwM2mTypeOper;
import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.LwM2mTypeOper.DISCOVER_ALL;
import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.LwM2mTypeOper.EXECUTE;
import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.LwM2mTypeOper.FW_UPDATE;
import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.LwM2mTypeOper.OBSERVE_CANCEL;
import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.LwM2mTypeOper.OBSERVE_READ_ALL;
import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.LwM2mTypeOper.WRITE_ATTRIBUTES;
import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.LwM2mTypeOper.WRITE_REPLACE;
import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.LwM2mTypeOper.WRITE_UPDATE;
import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.METHOD_KEY;
import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.PARAMS_KEY;
import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.RESULT_KEY;
import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.SEPARATOR_KEY;
import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.START_JSON_KEY;
import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.TARGET_ID_VER_KEY;
import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.VALUE_KEY;
import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.convertPathFromIdVerToObjectId;
import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.validPathIdVer;
@Slf4j
@Data
public class LwM2mClientRpcRequest {
private Registration registration;
private TransportProtos.SessionInfoProto sessionInfo;
private String bodyParams;
private int requestId;
private LwM2mTypeOper typeOper;
private String key;
private String targetIdVer;
private Object value;
private Map<String, Object> params;
private String errorMsg;
private String valueMsg;
private String infoMsg;
private String responseCode;
public LwM2mClientRpcRequest() {
}
public LwM2mClientRpcRequest(LwM2mTypeOper lwM2mTypeOper, String bodyParams, int requestId,
TransportProtos.SessionInfoProto sessionInfo, Registration registration, DefaultLwM2MTransportMsgHandler handler) {
this.registration = registration;
this.sessionInfo = sessionInfo;
this.requestId = requestId;
if (lwM2mTypeOper != null) {
this.typeOper = lwM2mTypeOper;
} else {
this.errorMsg = METHOD_KEY + " - " + typeOper + " is not valid.";
}
if (this.errorMsg == null && !bodyParams.equals("null")) {
this.bodyParams = bodyParams;
this.init(handler);
}
}
public TransportProtos.ToDeviceRpcResponseMsg getDeviceRpcResponseResultMsg() {
JsonObject payloadResp = new JsonObject();
payloadResp.addProperty(RESULT_KEY, this.responseCode);
if (this.errorMsg != null) {
payloadResp.addProperty(ERROR_KEY, this.errorMsg);
} else if (this.valueMsg != null) {
payloadResp.addProperty(VALUE_KEY, this.valueMsg);
} else if (this.infoMsg != null) {
payloadResp.addProperty(INFO_KEY, this.infoMsg);
}
return TransportProtos.ToDeviceRpcResponseMsg.newBuilder()
.setPayload(payloadResp.getAsJsonObject().toString())
.setRequestId(this.requestId)
.build();
}
private void init(DefaultLwM2MTransportMsgHandler handler) {
try {
// #1
if (this.bodyParams.contains(KEY_NAME_KEY)) {
String targetIdVerStr = this.getValueKeyFromBody(KEY_NAME_KEY);
if (targetIdVerStr != null) {
String targetIdVer = handler.getPresentPathIntoProfile(sessionInfo, targetIdVerStr);
if (targetIdVer != null) {
this.targetIdVer = targetIdVer;
this.setInfoMsg(String.format("Changed by: key - %s, pathIdVer - %s",
targetIdVerStr, targetIdVer));
}
}
}
if (this.getTargetIdVer() == null && this.bodyParams.contains(TARGET_ID_VER_KEY)) {
this.setValidTargetIdVerKey();
}
if (this.bodyParams.contains(VALUE_KEY)) {
this.value = this.getValueKeyFromBody(VALUE_KEY);
}
try {
if (this.bodyParams.contains(PARAMS_KEY)) {
this.setValidParamsKey(handler);
}
} catch (Exception e) {
this.setErrorMsg(String.format("Params of request is bad Json format. %s", e.getMessage()));
}
if (this.getTargetIdVer() == null
&& !(OBSERVE_READ_ALL == this.getTypeOper()
|| DISCOVER_ALL == this.getTypeOper()
|| OBSERVE_CANCEL == this.getTypeOper()
|| FW_UPDATE == this.getTypeOper())) {
this.setErrorMsg(TARGET_ID_VER_KEY + " and " +
KEY_NAME_KEY + " is null or bad format");
}
/**
* EXECUTE && WRITE_REPLACE - only for Resource or ResourceInstance
*/
else if (this.getTargetIdVer() != null
&& (EXECUTE == this.getTypeOper()
|| WRITE_REPLACE == this.getTypeOper())
&& !(new LwM2mPath(Objects.requireNonNull(convertPathFromIdVerToObjectId(this.getTargetIdVer()))).isResource()
|| new LwM2mPath(Objects.requireNonNull(convertPathFromIdVerToObjectId(this.getTargetIdVer()))).isResourceInstance())) {
this.setErrorMsg("Invalid parameter " + TARGET_ID_VER_KEY
+ ". Only Resource or ResourceInstance can be this operation");
}
} catch (Exception e) {
this.setErrorMsg(String.format("Bad format request. %s", e.getMessage()));
}
}
private void setValidTargetIdVerKey() {
String targetIdVerStr = this.getValueKeyFromBody(TARGET_ID_VER_KEY);
// targetIdVer without ver - ok
try {
// targetIdVer with/without ver - ok
this.targetIdVer = validPathIdVer(targetIdVerStr, this.registration);
if (this.targetIdVer != null) {
this.infoMsg = String.format("Changed by: pathIdVer - %s", this.targetIdVer);
}
} catch (Exception e) {
if (this.targetIdVer == null) {
this.errorMsg = TARGET_ID_VER_KEY + " - " + targetIdVerStr + " is not valid.";
}
}
}
private void setValidParamsKey(DefaultLwM2MTransportMsgHandler handler) {
String paramsStr = this.getValueKeyFromBody(PARAMS_KEY);
if (paramsStr != null) {
String params2Json =
START_JSON_KEY
+ "\""
+ paramsStr
.replaceAll(SEPARATOR_KEY, "\"" + SEPARATOR_KEY + "\"")
.replaceAll(FINISH_VALUE_KEY, "\"" + FINISH_VALUE_KEY + "\"")
+ "\""
+ FINISH_JSON_KEY;
// jsonObject
Map<String, Object> params = new Gson().fromJson(params2Json, new TypeToken<ConcurrentHashMap<String, Object>>() {
}.getType());
if (WRITE_UPDATE == this.getTypeOper()) {
if (this.targetIdVer != null) {
Map<String, Object> paramsResourceId = this.convertParamsToResourceId((ConcurrentHashMap<String, Object>) params, handler);
if (paramsResourceId.size() > 0) {
this.setParams(paramsResourceId);
}
}
} else if (WRITE_ATTRIBUTES == this.getTypeOper()) {
this.setParams(params);
}
}
}
private String getValueKeyFromBody(String key) {
String valueKey = null;
int startInd = -1;
int finishInd = -1;
try {
switch (key) {
case KEY_NAME_KEY:
case TARGET_ID_VER_KEY:
case VALUE_KEY:
startInd = this.bodyParams.indexOf(SEPARATOR_KEY, this.bodyParams.indexOf(key));
finishInd = this.bodyParams.indexOf(FINISH_VALUE_KEY, this.bodyParams.indexOf(key));
if (startInd >= 0 && finishInd < 0) {
finishInd = this.bodyParams.indexOf(FINISH_JSON_KEY, this.bodyParams.indexOf(key));
}
break;
case PARAMS_KEY:
startInd = this.bodyParams.indexOf(START_JSON_KEY, this.bodyParams.indexOf(key));
finishInd = this.bodyParams.indexOf(FINISH_JSON_KEY, this.bodyParams.indexOf(key));
}
if (startInd >= 0 && finishInd > 0) {
valueKey = this.bodyParams.substring(startInd + 1, finishInd);
}
} catch (Exception e) {
log.error("", new TimeoutException());
}
/**
* ReplaceAll "\""
*/
if (StringUtils.trimToNull(valueKey) != null) {
char[] chars = valueKey.toCharArray();
for (int i = 0; i < chars.length; i++) {
if (chars[i] == 92 || chars[i] == 34) chars[i] = 32;
}
return key.equals(PARAMS_KEY) ? String.valueOf(chars) : String.valueOf(chars).replaceAll(" ", "");
}
return null;
}
private ConcurrentHashMap<String, Object> convertParamsToResourceId(ConcurrentHashMap<String, Object> params,
DefaultLwM2MTransportMsgHandler serviceImpl) {
Map<String, Object> paramsIdVer = new ConcurrentHashMap<>();
LwM2mPath targetId = new LwM2mPath(Objects.requireNonNull(convertPathFromIdVerToObjectId(this.targetIdVer)));
if (targetId.isObjectInstance()) {
params.forEach((k, v) -> {
try {
int id = Integer.parseInt(k);
paramsIdVer.put(String.valueOf(id), v);
} catch (NumberFormatException e) {
String targetIdVer = serviceImpl.getPresentPathIntoProfile(sessionInfo, k);
if (targetIdVer != null) {
LwM2mPath lwM2mPath = new LwM2mPath(Objects.requireNonNull(convertPathFromIdVerToObjectId(targetIdVer)));
paramsIdVer.put(String.valueOf(lwM2mPath.getResourceId()), v);
}
/** WRITE_UPDATE*/
else {
String rezId = this.getRezIdByResourceNameAndObjectInstanceId(k, serviceImpl);
if (rezId != null) {
paramsIdVer.put(rezId, v);
}
}
}
});
}
return (ConcurrentHashMap<String, Object>) paramsIdVer;
}
private String getRezIdByResourceNameAndObjectInstanceId(String resourceName, DefaultLwM2MTransportMsgHandler handler) {
LwM2mClient lwM2mClient = handler.clientContext.getClientBySessionInfo(this.sessionInfo);
return lwM2mClient != null ?
lwM2mClient.getRezIdByResourceNameAndObjectInstanceId(resourceName, this.targetIdVer, handler.config.getModelProvider()) :
null;
}
}

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save