Browse Source

Merge with master

pull/6320/head
Igor Kulikov 4 years ago
parent
commit
d7efbd6eba
  1. 2
      application/src/main/data/json/system/widget_bundles/entity_admin_widgets.json
  2. 29
      application/src/main/data/upgrade/3.3.3/schema_update.sql
  3. 38
      application/src/main/java/org/thingsboard/server/actors/tenant/TenantActor.java
  4. 15
      application/src/main/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateService.java
  5. 12
      application/src/main/java/org/thingsboard/server/service/device/DeviceBulkImportService.java
  6. 2
      application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java
  7. 83
      application/src/main/java/org/thingsboard/server/service/query/DefaultEntityQueryService.java
  8. 45
      application/src/main/java/org/thingsboard/server/service/security/permission/CustomerUserPermissions.java
  9. 2
      application/src/main/resources/thingsboard.yml
  10. 31
      application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java
  11. 230
      application/src/test/java/org/thingsboard/server/controller/BaseAlarmControllerTest.java
  12. 80
      application/src/test/java/org/thingsboard/server/controller/BaseEntityQueryControllerTest.java
  13. 23
      application/src/test/java/org/thingsboard/server/controller/sql/AlarmControllerSqlTest.java
  14. 2
      application/src/test/java/org/thingsboard/server/edge/BaseEdgeTest.java
  15. 19
      application/src/test/java/org/thingsboard/server/rules/flow/AbstractRuleEngineFlowIntegrationTest.java
  16. 4
      common/data/src/main/java/org/thingsboard/server/common/data/device/profile/lwm2m/OtherConfiguration.java
  17. 5
      common/data/src/main/java/org/thingsboard/server/common/data/device/profile/lwm2m/TelemetryMappingConfiguration.java
  18. 11
      common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChainMetaData.java
  19. 27
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/secure/LwM2mDefaultBootstrapSessionManager.java
  20. 31
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/store/LwM2MBootstrapClientInstanceIds.java
  21. 144
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/store/LwM2MBootstrapConfigStoreTaskProvider.java
  22. 26
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/store/LwM2MBootstrapTaskProvider.java
  23. 6
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/DefaultLwM2mTransportService.java
  24. 14
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mServerListener.java
  25. 69
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/uplink/DefaultLwM2mUplinkMsgHandler.java
  26. 3
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/uplink/LwM2mUplinkMsgHandler.java
  27. 1
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/utils/LwM2MTransportUtil.java
  28. 2
      common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java
  29. 4
      common/util/pom.xml
  30. 89
      common/util/src/main/java/org/thingsboard/common/util/KvUtil.java
  31. 38
      dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java
  32. 6
      dao/src/main/resources/sql/schema-entities.sql
  33. 22
      dao/src/test/java/org/thingsboard/server/dao/service/AbstractServiceTest.java
  34. 17
      dao/src/test/java/org/thingsboard/server/dao/service/BaseDeviceProfileServiceTest.java
  35. 1
      dao/src/test/java/org/thingsboard/server/dao/service/BaseOtaPackageServiceTest.java
  36. 2
      dao/src/test/resources/sql/system-test-psql.sql
  37. 2
      dao/src/test/resources/sql/system-test.sql
  38. 53
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCreateAlarmNode.java
  39. 3
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCreateAlarmNodeConfiguration.java
  40. 2
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/TbDeviceProfileNode.java
  41. 2
      rule-engine/rule-engine-components/src/main/resources/public/static/rulenode/rulenode-core-config.js
  42. 54
      ui-ngx/src/app/modules/home/components/import-export/import-export.service.ts
  43. 2
      ui-ngx/src/app/shared/components/ota-package/ota-package-autocomplete.component.ts
  44. 4
      ui-ngx/src/assets/locale/locale.constant-en_US.json
  45. 32
      ui-ngx/src/assets/locale/locale.constant-zh_CN.json

2
application/src/main/data/json/system/widget_bundles/entity_admin_widgets.json

File diff suppressed because one or more lines are too long

29
application/src/main/data/upgrade/3.3.3/schema_update.sql

@ -0,0 +1,29 @@
--
-- Copyright © 2016-2022 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.
--
DELETE from ota_package as op WHERE NOT EXISTS(SELECT * FROM device_profile dp where op.device_profile_id = dp.id);
DO
$$
BEGIN
IF NOT EXISTS(SELECT 1 FROM pg_constraint WHERE conname = 'fk_device_profile_ota_package') THEN
ALTER TABLE ota_package
ADD CONSTRAINT fk_device_profile_ota_package
FOREIGN KEY (device_profile_id) REFERENCES device_profile (id)
ON DELETE CASCADE;
END IF;
END;
$$;

38
application/src/main/java/org/thingsboard/server/actors/tenant/TenantActor.java

@ -26,10 +26,7 @@ import org.thingsboard.server.actors.TbActorRef;
import org.thingsboard.server.actors.TbEntityActorId;
import org.thingsboard.server.actors.TbEntityTypeActorIdPredicate;
import org.thingsboard.server.actors.device.DeviceActorCreator;
import org.thingsboard.server.actors.device.SessionTimeoutCheckMsg;
import org.thingsboard.server.actors.ruleChain.RuleChainInputMsg;
import org.thingsboard.server.actors.ruleChain.RuleChainManagerActor;
import org.thingsboard.server.actors.ruleChain.RuleChainOutputMsg;
import org.thingsboard.server.actors.service.ContextBasedCreator;
import org.thingsboard.server.actors.service.DefaultActorService;
import org.thingsboard.server.common.data.ApiUsageState;
@ -65,7 +62,7 @@ import java.util.Optional;
@Slf4j
public class TenantActor extends RuleChainManagerActor {
private boolean isRuleEngineForCurrentTenant;
private boolean isRuleEngine;
private boolean isCore;
private ApiUsageState apiUsageState;
@ -78,39 +75,37 @@ public class TenantActor extends RuleChainManagerActor {
@Override
public void init(TbActorCtx ctx) throws TbActorException {
super.init(ctx);
log.info("[{}] Starting tenant actor.", tenantId);
log.debug("[{}] Starting tenant actor.", tenantId);
try {
Tenant tenant = systemContext.getTenantService().findTenantById(tenantId);
if (tenant == null) {
cantFindTenant = true;
log.info("[{}] Started tenant actor for missing tenant.", tenantId);
} else {
apiUsageState = new ApiUsageState(systemContext.getApiUsageStateService().getApiUsageState(tenant.getId()));
// This Service may be started for specific tenant only.
Optional<TenantId> isolatedTenantId = systemContext.getServiceInfoProvider().getIsolatedTenant();
TenantProfile tenantProfile = systemContext.getTenantProfileCache().get(tenant.getTenantProfileId());
isCore = systemContext.getServiceInfoProvider().isService(ServiceType.TB_CORE);
isRuleEngineForCurrentTenant = systemContext.getServiceInfoProvider().isService(ServiceType.TB_RULE_ENGINE);
if (isRuleEngineForCurrentTenant) {
isRuleEngine = systemContext.getServiceInfoProvider().isService(ServiceType.TB_RULE_ENGINE);
if (isRuleEngine) {
try {
if (isolatedTenantId.map(id -> id.equals(tenantId)).orElseGet(() -> !tenantProfile.isIsolatedTbRuleEngine())) {
if (apiUsageState.isReExecEnabled()) {
log.info("[{}] Going to init rule chains", tenantId);
if (getApiUsageState().isReExecEnabled()) {
log.debug("[{}] Going to init rule chains", tenantId);
initRuleChains();
} else {
log.info("[{}] Skip init of the rule chains due to API limits", tenantId);
}
} else {
isRuleEngineForCurrentTenant = false;
isRuleEngine = false;
}
} catch (Exception e) {
cantFindTenant = true;
}
}
log.info("[{}] Tenant actor started.", tenantId);
log.debug("[{}] Tenant actor started.", tenantId);
}
} catch (Exception e) {
log.warn("[{}] Unknown failure", tenantId, e);
@ -193,12 +188,12 @@ public class TenantActor extends RuleChainManagerActor {
}
private void onQueueToRuleEngineMsg(QueueToRuleEngineMsg msg) {
if (!isRuleEngineForCurrentTenant) {
if (!isRuleEngine) {
log.warn("RECEIVED INVALID MESSAGE: {}", msg);
return;
}
TbMsg tbMsg = msg.getMsg();
if (apiUsageState.isReExecEnabled()) {
if (getApiUsageState().isReExecEnabled()) {
if (tbMsg.getRuleChainId() == null) {
if (getRootChainActor() != null) {
getRootChainActor().tell(msg);
@ -222,7 +217,7 @@ public class TenantActor extends RuleChainManagerActor {
}
private void onRuleChainMsg(RuleChainAwareMsg msg) {
if (apiUsageState.isReExecEnabled()) {
if (getApiUsageState().isReExecEnabled()) {
getOrCreateActor(msg.getRuleChainId()).tell(msg);
}
}
@ -241,7 +236,7 @@ public class TenantActor extends RuleChainManagerActor {
private void onComponentLifecycleMsg(ComponentLifecycleMsg msg) {
if (msg.getEntityId().getEntityType().equals(EntityType.API_USAGE_STATE)) {
ApiUsageState old = apiUsageState;
ApiUsageState old = getApiUsageState();
apiUsageState = new ApiUsageState(systemContext.getApiUsageStateService().getApiUsageState(tenantId));
if (old.isReExecEnabled() && !apiUsageState.isReExecEnabled()) {
log.info("[{}] Received API state update. Going to DISABLE Rule Engine execution.", tenantId);
@ -261,7 +256,7 @@ public class TenantActor extends RuleChainManagerActor {
edgeRpcService.updateEdge(tenantId, edge);
}
}
} else if (isRuleEngineForCurrentTenant) {
} else if (isRuleEngine) {
TbActorRef target = getEntityActorRef(msg.getEntityId());
if (target != null) {
if (msg.getEntityId().getEntityType() == EntityType.RULE_CHAIN) {
@ -289,6 +284,13 @@ public class TenantActor extends RuleChainManagerActor {
systemContext.getEdgeRpcService().onEdgeEvent(tenantId, msg.getEdgeId());
}
private ApiUsageState getApiUsageState() {
if (apiUsageState == null) {
apiUsageState = new ApiUsageState(systemContext.getApiUsageStateService().getApiUsageState(tenantId));
}
return apiUsageState;
}
public static class ActorCreator extends ContextBasedCreator {
private final TenantId tenantId;

15
application/src/main/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateService.java

@ -236,17 +236,12 @@ public class DefaultTbApiUsageStateService extends AbstractPartitionBasedService
if (partitionService.resolve(ServiceType.TB_CORE, tenantId, tenantId).isMyPartition()) {
return getOrFetchState(tenantId, tenantId).getApiUsageState();
} else {
updateLock.lock();
try {
state = otherUsageStates.get(tenantId);
if (state == null) {
state = apiUsageStateService.findTenantApiUsageState(tenantId);
if (state != null) {
otherUsageStates.put(tenantId, state);
}
state = otherUsageStates.get(tenantId);
if (state == null) {
state = apiUsageStateService.findTenantApiUsageState(tenantId);
if (state != null) {
otherUsageStates.put(tenantId, state);
}
} finally {
updateLock.unlock();
}
return state;
}

12
application/src/main/java/org/thingsboard/server/service/device/DeviceBulkImportService.java

@ -34,11 +34,13 @@ import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.device.credentials.BasicMqttCredentials;
import org.thingsboard.server.common.data.device.credentials.lwm2m.LwM2MClientCredential;
import org.thingsboard.server.common.data.device.credentials.lwm2m.LwM2MSecurityMode;
import org.thingsboard.server.common.data.device.data.PowerMode;
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.DeviceProfileTransportConfiguration;
import org.thingsboard.server.common.data.device.profile.DisabledDeviceProfileProvisionConfiguration;
import org.thingsboard.server.common.data.device.profile.Lwm2mDeviceProfileTransportConfiguration;
import org.thingsboard.server.common.data.device.profile.lwm2m.OtherConfiguration;
import org.thingsboard.server.common.data.device.profile.lwm2m.TelemetryMappingConfiguration;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.security.DeviceCredentials;
import org.thingsboard.server.common.data.security.DeviceCredentialsType;
@ -52,6 +54,7 @@ import org.thingsboard.server.service.importing.BulkImportColumnType;
import org.thingsboard.server.service.security.model.SecurityUser;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumSet;
import java.util.Map;
import java.util.Objects;
@ -225,11 +228,14 @@ public class DeviceBulkImportService extends AbstractBulkImportService<Device> {
deviceProfile.setTransportType(DeviceTransportType.LWM2M);
deviceProfile.setProvisionType(DeviceProfileProvisionType.DISABLED);
Lwm2mDeviceProfileTransportConfiguration transportConfiguration = new Lwm2mDeviceProfileTransportConfiguration();
transportConfiguration.setBootstrap(Collections.emptyList());
transportConfiguration.setClientLwM2mSettings(new OtherConfiguration(1,1,1, PowerMode.DRX, null, null, null, null, null));
transportConfiguration.setObserveAttr(new TelemetryMappingConfiguration(Collections.emptyMap(), Collections.emptySet(), Collections.emptySet(), Collections.emptySet(), Collections.emptyMap()));
DeviceProfileData deviceProfileData = new DeviceProfileData();
DefaultDeviceProfileConfiguration configuration = new DefaultDeviceProfileConfiguration();
DeviceProfileTransportConfiguration transportConfiguration = new Lwm2mDeviceProfileTransportConfiguration();
DisabledDeviceProfileProvisionConfiguration provisionConfiguration = new DisabledDeviceProfileProvisionConfiguration(null);
deviceProfileData.setConfiguration(configuration);
deviceProfileData.setTransportConfiguration(transportConfiguration);
deviceProfileData.setProvisionConfiguration(provisionConfiguration);

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

@ -524,6 +524,8 @@ public class SqlDatabaseUpgradeService implements DatabaseEntitiesUpgradeService
log.info("Updating TTL cleanup procedure for the event table...");
schemaUpdateFile = Paths.get(installScripts.getDataDir(), "upgrade", "3.3.3", "schema_event_ttl_procedure.sql");
loadSql(schemaUpdateFile, conn);
schemaUpdateFile = Paths.get(installScripts.getDataDir(), "upgrade", "3.3.3", SCHEMA_UPDATE_SQL);
loadSql(schemaUpdateFile, conn);
log.info("Updating schema settings...");
conn.createStatement().execute("UPDATE tb_schema_settings SET schema_version = 3003004;");

83
application/src/main/java/org/thingsboard/server/service/query/DefaultEntityQueryService.java

@ -29,12 +29,16 @@ import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import org.springframework.web.context.request.async.DeferredResult;
import org.thingsboard.common.util.KvUtil;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.kv.AttributeKvEntry;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.query.AlarmData;
import org.thingsboard.server.common.data.query.AlarmDataQuery;
import org.thingsboard.server.common.data.query.ComplexFilterPredicate;
import org.thingsboard.server.common.data.query.DynamicValue;
import org.thingsboard.server.common.data.query.EntityCountQuery;
import org.thingsboard.server.common.data.query.EntityData;
import org.thingsboard.server.common.data.query.EntityDataPageLink;
@ -42,6 +46,10 @@ import org.thingsboard.server.common.data.query.EntityDataQuery;
import org.thingsboard.server.common.data.query.EntityDataSortOrder;
import org.thingsboard.server.common.data.query.EntityKey;
import org.thingsboard.server.common.data.query.EntityKeyType;
import org.thingsboard.server.common.data.query.FilterPredicateType;
import org.thingsboard.server.common.data.query.KeyFilter;
import org.thingsboard.server.common.data.query.KeyFilterPredicate;
import org.thingsboard.server.common.data.query.SimpleKeyFilterPredicate;
import org.thingsboard.server.dao.alarm.AlarmService;
import org.thingsboard.server.dao.attributes.AttributesService;
import org.thingsboard.server.dao.entity.EntityService;
@ -52,6 +60,7 @@ import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.executors.DbCallbackExecutorService;
import org.thingsboard.server.service.security.AccessValidator;
import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.subscription.TbAttributeSubscriptionScope;
import java.util.ArrayList;
import java.util.Collection;
@ -59,7 +68,9 @@ import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.function.Consumer;
import java.util.stream.Collectors;
@ -93,9 +104,81 @@ public class DefaultEntityQueryService implements EntityQueryService {
@Override
public PageData<EntityData> findEntityDataByQuery(SecurityUser securityUser, EntityDataQuery query) {
if (query.getKeyFilters() != null) {
resolveDynamicValuesInPredicates(
query.getKeyFilters().stream()
.map(KeyFilter::getPredicate)
.collect(Collectors.toList()),
securityUser
);
}
return entityService.findEntityDataByQuery(securityUser.getTenantId(), securityUser.getCustomerId(), query);
}
private void resolveDynamicValuesInPredicates(List<KeyFilterPredicate> predicates, SecurityUser user) {
predicates.forEach(predicate -> {
if (predicate.getType() == FilterPredicateType.COMPLEX) {
resolveDynamicValuesInPredicates(
((ComplexFilterPredicate) predicate).getPredicates(),
user
);
} else {
setResolvedValue(user, (SimpleKeyFilterPredicate<?>) predicate);
}
});
}
private void setResolvedValue(SecurityUser user, SimpleKeyFilterPredicate<?> predicate) {
DynamicValue<?> dynamicValue = predicate.getValue().getDynamicValue();
if (dynamicValue != null && dynamicValue.getResolvedValue() == null) {
resolveDynamicValue(dynamicValue, user, predicate.getType());
}
}
private <T> void resolveDynamicValue(DynamicValue<T> dynamicValue, SecurityUser user, FilterPredicateType predicateType) {
EntityId entityId;
switch (dynamicValue.getSourceType()) {
case CURRENT_TENANT:
entityId = user.getTenantId();
break;
case CURRENT_CUSTOMER:
entityId = user.getCustomerId();
break;
case CURRENT_USER:
entityId = user.getId();
break;
default:
throw new RuntimeException("Not supported operation for source type: {" + dynamicValue.getSourceType() + "}");
}
try {
Optional<AttributeKvEntry> valueOpt = attributesService.find(user.getTenantId(), entityId,
TbAttributeSubscriptionScope.SERVER_SCOPE.name(), dynamicValue.getSourceAttribute()).get();
if (valueOpt.isPresent()) {
AttributeKvEntry entry = valueOpt.get();
Object resolved = null;
switch (predicateType) {
case STRING:
resolved = KvUtil.getStringValue(entry);
break;
case NUMERIC:
resolved = KvUtil.getDoubleValue(entry);
break;
case BOOLEAN:
resolved = KvUtil.getBoolValue(entry);
break;
case COMPLEX:
break;
}
dynamicValue.setResolvedValue((T) resolved);
}
} catch (InterruptedException | ExecutionException e) {
throw new RuntimeException(e);
}
}
@Override
public PageData<AlarmData> findAlarmDataByQuery(SecurityUser securityUser, AlarmDataQuery query) {
EntityDataQuery entityDataQuery = this.buildEntityDataQuery(query);

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

@ -31,7 +31,7 @@ public class CustomerUserPermissions extends AbstractPermissions {
public CustomerUserPermissions() {
super();
put(Resource.ALARM, TenantAdminPermissions.tenantEntityPermissionChecker);
put(Resource.ALARM, customerAlarmPermissionChecker);
put(Resource.ASSET, customerEntityPermissionChecker);
put(Resource.DEVICE, customerEntityPermissionChecker);
put(Resource.CUSTOMER, customerPermissionChecker);
@ -44,6 +44,19 @@ public class CustomerUserPermissions extends AbstractPermissions {
put(Resource.RPC, rpcPermissionChecker);
}
private static final PermissionChecker customerAlarmPermissionChecker = new PermissionChecker() {
@Override
public boolean hasPermission(SecurityUser user, Operation operation, EntityId entityId, HasTenantId entity) {
if (!user.getTenantId().equals(entity.getTenantId())) {
return false;
}
if (!(entity instanceof HasCustomerId)) {
return false;
}
return user.getCustomerId().equals(((HasCustomerId) entity).getCustomerId());
}
};
private static final PermissionChecker customerEntityPermissionChecker =
new PermissionChecker.GenericPermissionChecker(Operation.READ, Operation.READ_CREDENTIALS,
Operation.READ_ATTRIBUTES, Operation.READ_TELEMETRY, Operation.RPC_CALL, Operation.CLAIM_DEVICES,
@ -62,10 +75,7 @@ public class CustomerUserPermissions extends AbstractPermissions {
if (!(entity instanceof HasCustomerId)) {
return false;
}
if (!operation.equals(Operation.CLAIM_DEVICES) && !user.getCustomerId().equals(((HasCustomerId) entity).getCustomerId())) {
return false;
}
return true;
return operation.equals(Operation.CLAIM_DEVICES) || user.getCustomerId().equals(((HasCustomerId) entity).getCustomerId());
}
};
@ -78,10 +88,7 @@ public class CustomerUserPermissions extends AbstractPermissions {
if (!super.hasPermission(user, operation, entityId, entity)) {
return false;
}
if (!user.getCustomerId().equals(entityId)) {
return false;
}
return true;
return user.getCustomerId().equals(entityId);
}
};
@ -98,10 +105,7 @@ public class CustomerUserPermissions extends AbstractPermissions {
if (!user.getTenantId().equals(dashboard.getTenantId())) {
return false;
}
if (!dashboard.isAssignedToCustomer(user.getCustomerId())) {
return false;
}
return true;
return dashboard.isAssignedToCustomer(user.getCustomerId());
}
};
@ -113,10 +117,7 @@ public class CustomerUserPermissions extends AbstractPermissions {
if (!Authority.CUSTOMER_USER.equals(userEntity.getAuthority())) {
return false;
}
if (!user.getId().equals(userId)) {
return false;
}
return true;
return user.getId().equals(userId);
}
};
@ -132,10 +133,7 @@ public class CustomerUserPermissions extends AbstractPermissions {
if (entity.getTenantId() == null || entity.getTenantId().isNullUid()) {
return true;
}
if (!user.getTenantId().equals(entity.getTenantId())) {
return false;
}
return true;
return user.getTenantId().equals(entity.getTenantId());
}
};
@ -151,10 +149,7 @@ public class CustomerUserPermissions extends AbstractPermissions {
if (entity.getTenantId() == null || entity.getTenantId().isNullUid()) {
return true;
}
if (!user.getTenantId().equals(entity.getTenantId())) {
return false;
}
return true;
return user.getTenantId().equals(entity.getTenantId());
}
};
}

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

@ -165,7 +165,7 @@ ui:
# Help parameters
help:
# Base url for UI help assets
base-url: "${UI_HELP_BASE_URL:https://raw.githubusercontent.com/thingsboard/thingsboard-ui-help/release-3.3.3}"
base-url: "${UI_HELP_BASE_URL:https://raw.githubusercontent.com/thingsboard/thingsboard-ui-help/release-3.3.4}"
database:
ts_max_intervals: "${DATABASE_TS_MAX_INTERVALS:700}" # Max number of DB queries generated by single API call to fetch telemetry records

31
application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java

@ -64,6 +64,7 @@ import org.thingsboard.server.common.data.device.profile.MqttTopics;
import org.thingsboard.server.common.data.device.profile.ProtoTransportPayloadConfiguration;
import org.thingsboard.server.common.data.device.profile.TransportPayloadTypeConfiguration;
import org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.HasId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageData;
@ -124,6 +125,7 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest {
protected String username;
protected TenantId tenantId;
protected CustomerId customerId;
@SuppressWarnings("rawtypes")
private HttpMessageConverter mappingJackson2HttpMessageConverter;
@ -192,6 +194,7 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest {
customer.setTitle("Customer");
customer.setTenantId(tenantId);
Customer savedCustomer = doPost("/api/customer", customer, Customer.class);
customerId = savedCustomer.getId();
User customerUser = new User();
customerUser.setAuthority(Authority.CUSTOMER_USER);
@ -254,9 +257,14 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest {
}
private Tenant savedDifferentTenant;
private Customer savedDifferentCustomer;
protected void loginDifferentTenant() throws Exception {
loginSysAdmin();
if (savedDifferentTenant != null) {
deleteDifferentTenant();
}
Tenant tenant = new Tenant();
tenant.setTitle("Different tenant");
savedDifferentTenant = doPost("/api/tenant", tenant, Tenant.class);
@ -269,10 +277,27 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest {
createUserAndLogin(differentTenantAdmin, "testPassword");
}
protected void loginDifferentCustomer() throws Exception {
loginTenantAdmin();
Customer customer = new Customer();
customer.setTitle("Different customer");
savedDifferentCustomer = doPost("/api/customer", customer, Customer.class);
Assert.assertNotNull(savedDifferentCustomer);
User differentCustomerUser = new User();
differentCustomerUser.setAuthority(Authority.CUSTOMER_USER);
differentCustomerUser.setTenantId(tenantId);
differentCustomerUser.setCustomerId(savedDifferentCustomer.getId());
differentCustomerUser.setEmail("different_customer@thingsboard.org");
createUserAndLogin(differentCustomerUser, "testPassword");
}
protected void deleteDifferentTenant() throws Exception {
loginSysAdmin();
doDelete("/api/tenant/" + savedDifferentTenant.getId().getId().toString())
.andExpect(status().isOk());
if (savedDifferentTenant != null) {
loginSysAdmin();
doDelete("/api/tenant/" + savedDifferentTenant.getId().getId().toString())
.andExpect(status().isOk());
}
}
protected User createUserAndLogin(User user, String password) throws Exception {

230
application/src/test/java/org/thingsboard/server/controller/BaseAlarmControllerTest.java

@ -0,0 +1,230 @@
/**
* Copyright © 2016-2022 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.controller;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.alarm.Alarm;
import org.thingsboard.server.common.data.alarm.AlarmSeverity;
import org.thingsboard.server.common.data.alarm.AlarmStatus;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
public abstract class BaseAlarmControllerTest extends AbstractControllerTest {
public static final String TEST_ALARM_TYPE = "Test";
protected Device customerDevice;
@Before
public void setup() throws Exception {
loginTenantAdmin();
Device device = new Device();
device.setTenantId(tenantId);
device.setName("Test device");
device.setLabel("Label");
device.setType("Type");
device.setCustomerId(customerId);
customerDevice = doPost("/api/device", device, Device.class);
logout();
}
@After
public void teardown() throws Exception {
loginSysAdmin();
deleteDifferentTenant();
}
@Test
public void testCreateAlarmViaCustomer() throws Exception {
loginCustomerUser();
createAlarm(TEST_ALARM_TYPE);
logout();
}
@Test
public void testCreateAlarmViaTenant() throws Exception {
loginTenantAdmin();
createAlarm(TEST_ALARM_TYPE);
logout();
}
@Test
public void testUpdateAlarmViaCustomer() throws Exception {
loginCustomerUser();
Alarm alarm = createAlarm(TEST_ALARM_TYPE);
alarm.setSeverity(AlarmSeverity.MAJOR);
Alarm updatedAlarm = doPost("/api/alarm", alarm, Alarm.class);
Assert.assertNotNull(updatedAlarm);
Assert.assertEquals(AlarmSeverity.MAJOR, updatedAlarm.getSeverity());
logout();
}
@Test
public void testUpdateAlarmViaTenant() throws Exception {
loginTenantAdmin();
Alarm alarm = createAlarm(TEST_ALARM_TYPE);
alarm.setSeverity(AlarmSeverity.MAJOR);
Alarm updatedAlarm = doPost("/api/alarm", alarm, Alarm.class);
Assert.assertNotNull(updatedAlarm);
Assert.assertEquals(AlarmSeverity.MAJOR, updatedAlarm.getSeverity());
logout();
}
@Test
public void testUpdateAlarmViaDifferentTenant() throws Exception {
loginTenantAdmin();
Alarm alarm = createAlarm(TEST_ALARM_TYPE);
alarm.setSeverity(AlarmSeverity.MAJOR);
loginDifferentTenant();
doPost("/api/alarm", alarm).andExpect(status().isForbidden());
logout();
}
@Test
public void testUpdateAlarmViaDifferentCustomer() throws Exception {
loginCustomerUser();
Alarm alarm = createAlarm(TEST_ALARM_TYPE);
loginDifferentCustomer();
alarm.setSeverity(AlarmSeverity.MAJOR);
doPost("/api/alarm", alarm).andExpect(status().isForbidden());
logout();
}
@Test
public void testDeleteAlarmViaCustomer() throws Exception {
loginCustomerUser();
Alarm alarm = createAlarm(TEST_ALARM_TYPE);
doDelete("/api/alarm/" + alarm.getId()).andExpect(status().isOk());
logout();
}
@Test
public void testDeleteAlarmViaTenant() throws Exception {
loginTenantAdmin();
Alarm alarm = createAlarm(TEST_ALARM_TYPE);
doDelete("/api/alarm/" + alarm.getId()).andExpect(status().isOk());
logout();
}
@Test
public void testDeleteAlarmViaDifferentTenant() throws Exception {
loginTenantAdmin();
Alarm alarm = createAlarm(TEST_ALARM_TYPE);
loginDifferentTenant();
doDelete("/api/alarm/" + alarm.getId()).andExpect(status().isForbidden());
logout();
}
@Test
public void testDeleteAlarmViaAnotherCustomer() throws Exception {
loginCustomerUser();
Alarm alarm = createAlarm(TEST_ALARM_TYPE);
loginDifferentCustomer();
doDelete("/api/alarm/" + alarm.getId()).andExpect(status().isForbidden());
logout();
}
@Test
public void testClearAlarmViaCustomer() throws Exception {
loginCustomerUser();
Alarm alarm = createAlarm(TEST_ALARM_TYPE);
doPost("/api/alarm/" + alarm.getId() + "/clear").andExpect(status().isOk());
Alarm foundAlarm = doGet("/api/alarm/" + alarm.getId(), Alarm.class);
Assert.assertNotNull(foundAlarm);
Assert.assertEquals(AlarmStatus.CLEARED_UNACK, foundAlarm.getStatus());
logout();
}
@Test
public void testClearAlarmViaTenant() throws Exception {
loginTenantAdmin();
Alarm alarm = createAlarm(TEST_ALARM_TYPE);
doPost("/api/alarm/" + alarm.getId() + "/clear").andExpect(status().isOk());
Alarm foundAlarm = doGet("/api/alarm/" + alarm.getId(), Alarm.class);
Assert.assertNotNull(foundAlarm);
Assert.assertEquals(AlarmStatus.CLEARED_UNACK, foundAlarm.getStatus());
logout();
}
@Test
public void testAcknowledgeAlarmViaCustomer() throws Exception {
loginCustomerUser();
Alarm alarm = createAlarm(TEST_ALARM_TYPE);
doPost("/api/alarm/" + alarm.getId() + "/ack").andExpect(status().isOk());
Alarm foundAlarm = doGet("/api/alarm/" + alarm.getId(), Alarm.class);
Assert.assertNotNull(foundAlarm);
Assert.assertEquals(AlarmStatus.ACTIVE_ACK, foundAlarm.getStatus());
logout();
}
@Test
public void testClearAlarmViaDifferentCustomer() throws Exception {
loginCustomerUser();
Alarm alarm = createAlarm(TEST_ALARM_TYPE);
loginDifferentCustomer();
doPost("/api/alarm/" + alarm.getId() + "/clear").andExpect(status().isForbidden());
logout();
}
@Test
public void testClearAlarmViaDifferentTenant() throws Exception {
loginTenantAdmin();
Alarm alarm = createAlarm(TEST_ALARM_TYPE);
loginDifferentTenant();
doPost("/api/alarm/" + alarm.getId() + "/clear").andExpect(status().isForbidden());
logout();
}
@Test
public void testAcknowledgeAlarmViaDifferentCustomer() throws Exception {
loginCustomerUser();
Alarm alarm = createAlarm(TEST_ALARM_TYPE);
loginDifferentCustomer();
doPost("/api/alarm/" + alarm.getId() + "/ack").andExpect(status().isForbidden());
logout();
}
@Test
public void testAcknowledgeAlarmViaDifferentTenant() throws Exception {
loginTenantAdmin();
Alarm alarm = createAlarm(TEST_ALARM_TYPE);
loginDifferentTenant();
doPost("/api/alarm/" + alarm.getId() + "/ack").andExpect(status().isForbidden());
logout();
}
private Alarm createAlarm(String type) throws Exception {
Alarm alarm = Alarm.builder()
.tenantId(tenantId)
.customerId(customerId)
.originator(customerDevice.getId())
.status(AlarmStatus.ACTIVE_UNACK)
.severity(AlarmSeverity.CRITICAL)
.type(type)
.build();
alarm = doPost("/api/alarm", alarm, Alarm.class);
Assert.assertNotNull(alarm);
return alarm;
}
}

80
application/src/test/java/org/thingsboard/server/controller/BaseEntityQueryControllerTest.java

@ -16,10 +16,13 @@
package org.thingsboard.server.controller;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode;
import org.awaitility.Awaitility;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.EntityType;
@ -29,6 +32,8 @@ import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.query.DeviceTypeFilter;
import org.thingsboard.server.common.data.query.DynamicValue;
import org.thingsboard.server.common.data.query.DynamicValueSourceType;
import org.thingsboard.server.common.data.query.EntityCountQuery;
import org.thingsboard.server.common.data.query.EntityData;
import org.thingsboard.server.common.data.query.EntityDataPageLink;
@ -41,11 +46,13 @@ import org.thingsboard.server.common.data.query.EntityTypeFilter;
import org.thingsboard.server.common.data.query.FilterPredicateValue;
import org.thingsboard.server.common.data.query.KeyFilter;
import org.thingsboard.server.common.data.query.NumericFilterPredicate;
import org.thingsboard.server.common.data.query.TsValue;
import org.thingsboard.server.common.data.security.Authority;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@ -299,6 +306,79 @@ public abstract class BaseEntityQueryControllerTest extends AbstractControllerTe
List<String> deviceHighTemperatures = highTemperatures.stream().map(aLong -> Long.toString(aLong)).collect(Collectors.toList());
Assert.assertEquals(deviceHighTemperatures, loadedHighTemperatures);
}
@Test
public void testFindEntityDataByQueryWithDynamicValue() throws Exception {
int numOfDevices = 2;
for (int i = 0; i < numOfDevices; i++) {
Device device = new Device();
String name = "Device" + i;
device.setName(name);
device.setType("default");
device.setLabel("testLabel" + (int) (Math.random() * 1000));
Device savedDevice1 = doPost("/api/device?accessToken=" + name, device, Device.class);
JsonNode content = JacksonUtil.toJsonNode("{\"alarmActiveTime\": 1" + i + "}");
doPost("/api/plugins/telemetry/" + EntityType.DEVICE.name() + "/" + savedDevice1.getUuidId() + "/SERVER_SCOPE", content)
.andExpect(status().isOk());
}
JsonNode content = JacksonUtil.toJsonNode("{\"dynamicValue\": 0}");
doPost("/api/plugins/telemetry/" + EntityType.TENANT.name() + "/" + tenantId.getId() + "/SERVER_SCOPE", content)
.andExpect(status().isOk());
DeviceTypeFilter filter = new DeviceTypeFilter();
filter.setDeviceType("default");
filter.setDeviceNameFilter("");
KeyFilter highTemperatureFilter = new KeyFilter();
highTemperatureFilter.setKey(new EntityKey(EntityKeyType.SERVER_ATTRIBUTE, "alarmActiveTime"));
NumericFilterPredicate predicate = new NumericFilterPredicate();
DynamicValue<Double> dynamicValue =
new DynamicValue<>(DynamicValueSourceType.CURRENT_TENANT, "dynamicValue");
FilterPredicateValue<Double> predicateValue = new FilterPredicateValue<>(0.0, null, dynamicValue);
predicate.setValue(predicateValue);
predicate.setOperation(NumericFilterPredicate.NumericOperation.GREATER);
highTemperatureFilter.setPredicate(predicate);
List<KeyFilter> keyFilters = Collections.singletonList(highTemperatureFilter);
EntityDataSortOrder sortOrder = new EntityDataSortOrder(
new EntityKey(EntityKeyType.ENTITY_FIELD, "createdTime"), EntityDataSortOrder.Direction.ASC
);
EntityDataPageLink pageLink = new EntityDataPageLink(10, 0, null, sortOrder);
List<EntityKey> entityFields = Collections.singletonList(new EntityKey(EntityKeyType.ENTITY_FIELD, "name"));
List<EntityKey> latestValues = Collections.singletonList(new EntityKey(EntityKeyType.ATTRIBUTE, "alarmActiveTime"));
EntityDataQuery query = new EntityDataQuery(filter, pageLink, entityFields, latestValues, keyFilters);
Awaitility.await()
.alias("data by query")
.atMost(30, TimeUnit.SECONDS)
.until(() -> {
var data = doPostWithTypedResponse("/api/entitiesQuery/find", query, new TypeReference<PageData<EntityData>>() {});
var loadedEntities = new ArrayList<>(data.getData());
return loadedEntities.size() == numOfDevices;
});
var data = doPostWithTypedResponse("/api/entitiesQuery/find", query, new TypeReference<PageData<EntityData>>() {});
var loadedEntities = new ArrayList<>(data.getData());
Assert.assertEquals(numOfDevices, loadedEntities.size());
for (int i = 0; i < numOfDevices; i++) {
var entity = loadedEntities.get(i);
String name = entity.getLatest().get(EntityKeyType.ENTITY_FIELD).getOrDefault("name", new TsValue(0, "Invalid")).getValue();
String alarmActiveTime = entity.getLatest().get(EntityKeyType.ATTRIBUTE).getOrDefault("alarmActiveTime", new TsValue(0, "-1")).getValue();
Assert.assertEquals("Device" + i, name);
Assert.assertEquals("1" + i, alarmActiveTime);
}
}
}

23
application/src/test/java/org/thingsboard/server/controller/sql/AlarmControllerSqlTest.java

@ -0,0 +1,23 @@
/**
* Copyright © 2016-2022 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.controller.sql;
import org.thingsboard.server.controller.BaseAlarmControllerTest;
import org.thingsboard.server.dao.service.DaoSqlTest;
@DaoSqlTest
public class AlarmControllerSqlTest extends BaseAlarmControllerTest {
}

2
application/src/test/java/org/thingsboard/server/edge/BaseEdgeTest.java

@ -612,8 +612,6 @@ abstract public class BaseEdgeTest extends AbstractControllerTest {
ruleChainMetaData.addConnectionInfo(0, 2, "fail");
ruleChainMetaData.addConnectionInfo(1, 2, "success");
ruleChainMetaData.addRuleChainConnectionInfo(2, edge.getRootRuleChainId(), "success", mapper.createObjectNode());
doPost("/api/ruleChain/metadata", ruleChainMetaData, RuleChainMetaData.class);
}

19
application/src/test/java/org/thingsboard/server/rules/flow/AbstractRuleEngineFlowIntegrationTest.java

@ -25,6 +25,7 @@ import org.mockito.Mockito;
import org.mockito.stubbing.Answer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.util.ReflectionTestUtils;
import org.thingsboard.rule.engine.flow.TbRuleChainInputNodeConfiguration;
import org.thingsboard.rule.engine.metadata.TbGetAttributesNodeConfiguration;
import org.thingsboard.server.actors.ActorSystemContext;
import org.thingsboard.server.common.data.DataConstants;
@ -35,6 +36,7 @@ import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry;
import org.thingsboard.server.common.data.kv.StringDataEntry;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.rule.NodeConnectionInfo;
import org.thingsboard.server.common.data.rule.RuleChain;
import org.thingsboard.server.common.data.rule.RuleChainMetaData;
import org.thingsboard.server.common.data.rule.RuleNode;
@ -240,16 +242,27 @@ public abstract class AbstractRuleEngineFlowIntegrationTest extends AbstractRule
configuration1.setServerAttributeNames(Collections.singletonList("serverAttributeKey1"));
ruleNode1.setConfiguration(mapper.valueToTree(configuration1));
rootMetaData.setNodes(Collections.singletonList(ruleNode1));
RuleNode ruleNode12 = new RuleNode();
ruleNode12.setName("Simple Rule Node 1");
ruleNode12.setType(org.thingsboard.rule.engine.flow.TbRuleChainInputNode.class.getName());
ruleNode12.setDebugMode(true);
TbRuleChainInputNodeConfiguration configuration12 = new TbRuleChainInputNodeConfiguration();
configuration12.setRuleChainId(secondaryRuleChain.getId().getId().toString());
ruleNode12.setConfiguration(mapper.valueToTree(configuration12));
rootMetaData.setNodes(Arrays.asList(ruleNode1, ruleNode12));
rootMetaData.setFirstNodeIndex(0);
rootMetaData.addRuleChainConnectionInfo(0, secondaryRuleChain.getId(), "Success", mapper.createObjectNode());
NodeConnectionInfo connection = new NodeConnectionInfo();
connection.setFromIndex(0);
connection.setToIndex(1);
connection.setType("Success");
rootMetaData.setConnections(Collections.singletonList(connection));
rootMetaData = saveRuleChainMetaData(rootMetaData);
Assert.assertNotNull(rootMetaData);
rootRuleChain = getRuleChain(rootRuleChain.getId());
Assert.assertNotNull(rootRuleChain.getFirstRuleNodeId());
RuleChainMetaData secondaryMetaData = new RuleChainMetaData();
secondaryMetaData.setRuleChainId(secondaryRuleChain.getId());

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

@ -16,11 +16,15 @@
package org.thingsboard.server.common.data.device.profile.lwm2m;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.thingsboard.server.common.data.device.data.PowerMode;
import org.thingsboard.server.common.data.device.data.PowerSavingConfiguration;
@Data
@NoArgsConstructor
@AllArgsConstructor
@JsonIgnoreProperties(ignoreUnknown = true)
public class OtherConfiguration extends PowerSavingConfiguration {

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

@ -15,13 +15,16 @@
*/
package org.thingsboard.server.common.data.device.profile.lwm2m;
import lombok.AllArgsConstructor;
import lombok.Data;
import org.thingsboard.server.common.data.device.profile.lwm2m.ObjectAttributes;
import lombok.NoArgsConstructor;
import java.util.Map;
import java.util.Set;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class TelemetryMappingConfiguration {
private Map<String, String> keyName;

11
common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChainMetaData.java

@ -59,15 +59,4 @@ public class RuleChainMetaData {
}
connections.add(connectionInfo);
}
public void addRuleChainConnectionInfo(int fromIndex, RuleChainId targetRuleChainId, String type, JsonNode additionalInfo) {
RuleChainConnectionInfo connectionInfo = new RuleChainConnectionInfo();
connectionInfo.setFromIndex(fromIndex);
connectionInfo.setTargetRuleChainId(targetRuleChainId);
connectionInfo.setType(type);
connectionInfo.setAdditionalInfo(additionalInfo);
if (ruleChainConnections == null) {
ruleChainConnections = new ArrayList<>();
}
ruleChainConnections.add(connectionInfo);
}
}

27
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/secure/LwM2mDefaultBootstrapSessionManager.java

@ -27,6 +27,7 @@ import org.eclipse.leshan.server.bootstrap.BootstrapSession;
import org.eclipse.leshan.server.bootstrap.BootstrapTaskProvider;
import org.eclipse.leshan.server.bootstrap.DefaultBootstrapSession;
import org.eclipse.leshan.server.bootstrap.DefaultBootstrapSessionManager;
import org.eclipse.leshan.server.bootstrap.InvalidConfigurationException;
import org.eclipse.leshan.server.model.LwM2mBootstrapModelProvider;
import org.eclipse.leshan.server.model.StandardBootstrapModelProvider;
import org.eclipse.leshan.server.security.BootstrapSecurityStore;
@ -35,6 +36,7 @@ import org.eclipse.leshan.server.security.SecurityInfo;
import org.thingsboard.server.common.transport.TransportService;
import org.thingsboard.server.transport.lwm2m.bootstrap.store.LwM2MBootstrapConfigStoreTaskProvider;
import org.thingsboard.server.transport.lwm2m.bootstrap.store.LwM2MBootstrapSecurityStore;
import org.thingsboard.server.transport.lwm2m.bootstrap.store.LwM2MBootstrapTaskProvider;
import org.thingsboard.server.transport.lwm2m.server.client.LwM2MAuthException;
import java.util.ArrayList;
@ -48,10 +50,10 @@ import static org.thingsboard.server.transport.lwm2m.utils.LwM2MTransportUtil.LO
@Slf4j
public class LwM2mDefaultBootstrapSessionManager extends DefaultBootstrapSessionManager {
private BootstrapSecurityStore bsSecurityStore;
private SecurityChecker securityChecker;
private BootstrapTaskProvider tasksProvider;
private LwM2mBootstrapModelProvider modelProvider;
private final BootstrapSecurityStore bsSecurityStore;
private final SecurityChecker securityChecker;
private final LwM2MBootstrapTaskProvider tasksProvider;
private final LwM2mBootstrapModelProvider modelProvider;
private TransportService transportService;
/**
@ -73,7 +75,7 @@ public class LwM2mDefaultBootstrapSessionManager extends DefaultBootstrapSession
* @param securityChecker used to accept or refuse new {@link BootstrapSession}.
*/
public LwM2mDefaultBootstrapSessionManager(BootstrapSecurityStore bsSecurityStore, SecurityChecker securityChecker,
BootstrapTaskProvider tasksProvider, LwM2mBootstrapModelProvider modelProvider) {
LwM2MBootstrapTaskProvider tasksProvider, LwM2mBootstrapModelProvider modelProvider) {
super(bsSecurityStore, securityChecker, tasksProvider, modelProvider);
this.bsSecurityStore = bsSecurityStore;
this.securityChecker = securityChecker;
@ -100,7 +102,12 @@ public class LwM2mDefaultBootstrapSessionManager extends DefaultBootstrapSession
}
DefaultBootstrapSession session = new DefaultBootstrapSession(request, clientIdentity, authorized);
if (authorized) {
this.sendLogs (request.getEndpointName(),
try {
this.tasksProvider.put(session.getEndpoint());
} catch (InvalidConfigurationException e){
log.error("Failed put to lwM2MBootstrapSessionClients by endpoint [{}]", request.getEndpointName(), e);
}
this.sendLogs(request.getEndpointName(),
String.format("%s: Bootstrap session started...", LOG_LWM2M_INFO, request.getEndpointName()));
}
return session;
@ -108,7 +115,7 @@ public class LwM2mDefaultBootstrapSessionManager extends DefaultBootstrapSession
@Override
public boolean hasConfigFor(BootstrapSession session) {
BootstrapTaskProvider.Tasks firstTasks = tasksProvider.getTasks(session, null);
BootstrapTaskProvider.Tasks firstTasks = this.tasksProvider.getTasks(session, null);
if (firstTasks == null) {
return false;
}
@ -147,7 +154,7 @@ public class LwM2mDefaultBootstrapSessionManager extends DefaultBootstrapSession
return requestsToSend.remove(0);
} else {
if (session.hasMoreTasks()) {
BootstrapTaskProvider.Tasks nextTasks = tasksProvider.getTasks(session, session.getResponses());
BootstrapTaskProvider.Tasks nextTasks = this.tasksProvider.getTasks(session, session.getResponses());
if (nextTasks == null) {
session.setMoreTasks(false);
return new BootstrapFinishRequest();
@ -178,6 +185,7 @@ public class LwM2mDefaultBootstrapSessionManager extends DefaultBootstrapSession
// on success for bootstrap finish request we stop the session
this.sendLogs(bsSession.getEndpoint(),
String.format("%s: receives success response for bootstrap finish.", LOG_LWM2M_INFO));
this.tasksProvider.remove(bsSession.getEndpoint());
return BootstrapPolicy.finished();
}
}
@ -199,6 +207,7 @@ public class LwM2mDefaultBootstrapSessionManager extends DefaultBootstrapSession
// on response error for bootstrap finish request we stop the session
this.sendLogs(bsSession.getEndpoint(),
String.format("%s: error response for request bootstrap finish. Stop the session: %s", LOG_LWM2M_ERROR, bsSession.toString()));
this.tasksProvider.remove(bsSession.getEndpoint());
return BootstrapPolicy.failed();
}
}
@ -215,12 +224,14 @@ public class LwM2mDefaultBootstrapSessionManager extends DefaultBootstrapSession
@Override
public void end(BootstrapSession bsSession) {
this.sendLogs(bsSession.getEndpoint(), String.format("%s: Bootstrap session finished.", LOG_LWM2M_INFO));
this.tasksProvider.remove(bsSession.getEndpoint());
}
@Override
public void failed(BootstrapSession bsSession, BootstrapFailureCause cause) {
this.sendLogs(bsSession.getEndpoint(), String.format("%s: Bootstrap session failed because of %s", LOG_LWM2M_ERROR,
cause.toString()));
this.tasksProvider.remove(bsSession.getEndpoint());
}
private void sendLogs(String endpointName, String logMsg) {

31
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/store/LwM2MBootstrapClientInstanceIds.java

@ -0,0 +1,31 @@
/**
* Copyright © 2016-2022 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.bootstrap.store;
import lombok.Data;
import java.util.HashMap;
import java.util.Map;
@Data
public class LwM2MBootstrapClientInstanceIds {
/**
* Map<serverId (shortId), InstanceId>
*/
private Map<Integer, Integer> securityInstances = new HashMap<>();
private Map<Integer, Integer> serverInstances = new HashMap<>();
}

144
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/store/LwM2MBootstrapConfigStoreTaskProvider.java

@ -30,8 +30,8 @@ import org.eclipse.leshan.core.response.LwM2mResponse;
import org.eclipse.leshan.server.bootstrap.BootstrapConfig;
import org.eclipse.leshan.server.bootstrap.BootstrapConfigStore;
import org.eclipse.leshan.server.bootstrap.BootstrapSession;
import org.eclipse.leshan.server.bootstrap.BootstrapTaskProvider;
import org.eclipse.leshan.server.bootstrap.BootstrapUtil;
import org.eclipse.leshan.server.bootstrap.InvalidConfigurationException;
import java.math.BigInteger;
import java.util.ArrayList;
@ -42,28 +42,36 @@ import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.stream.Collectors;
import static org.eclipse.leshan.core.model.ResourceModel.Type.OPAQUE;
import static org.eclipse.leshan.server.bootstrap.BootstrapUtil.toWriteRequest;
import static org.thingsboard.server.transport.lwm2m.utils.LwM2MTransportUtil.BOOTSTRAP_DEFAULT_SHORT_ID;
@Slf4j
public class LwM2MBootstrapConfigStoreTaskProvider implements BootstrapTaskProvider {
public class LwM2MBootstrapConfigStoreTaskProvider implements LwM2MBootstrapTaskProvider {
protected final ReadWriteLock readWriteLock;
protected final Lock writeLock;
private BootstrapConfigStore store;
private Map<Integer, String> supportedObjects;
/**
* Map<serverId, InstanceId>
* Map<sEndpoint, LwM2MBootstrapClientInstanceIds: securityInstances, serverInstances>
*/
protected Map<Integer, Integer> securityInstances;
protected Map<Integer, Integer> serverInstances;
protected Integer bootstrapServerIdOld;
protected Integer bootstrapServerIdNew;
protected Map<String, LwM2MBootstrapClientInstanceIds> lwM2MBootstrapSessionClients;
public LwM2MBootstrapConfigStoreTaskProvider(BootstrapConfigStore store) {
this.store = store;
this.lwM2MBootstrapSessionClients = new ConcurrentHashMap<>();
readWriteLock = new ReentrantReadWriteLock();
writeLock = readWriteLock.writeLock();
}
@Override
@ -91,13 +99,13 @@ public class LwM2MBootstrapConfigStoreTaskProvider implements BootstrapTaskProvi
BootstrapDiscoverResponse discoverResponse = (BootstrapDiscoverResponse) previousResponse.get(0);
if (discoverResponse.isSuccess()) {
this.initAfterBootstrapDiscover(discoverResponse);
findSecurityInstanceId(discoverResponse.getObjectLinks());
findSecurityInstanceId(discoverResponse.getObjectLinks(), session.getEndpoint());
} else {
log.warn(
"Bootstrap Discover return error {} : to continue bootstrap session without autoIdForSecurityObject mode. {}",
discoverResponse, session);
}
if (this.securityInstances.get(0) == null) {
if (this.lwM2MBootstrapSessionClients.get(session.getEndpoint()).getSecurityInstances().get(BOOTSTRAP_DEFAULT_SHORT_ID) == null) {
log.error(
"Unable to find bootstrap server instance in Security Object (0) in response {}: unable to continue bootstrap session with autoIdForSecurityObject mode. {}",
discoverResponse, session);
@ -109,8 +117,12 @@ public class LwM2MBootstrapConfigStoreTaskProvider implements BootstrapTaskProvi
return tasks;
}
BootstrapReadResponse readResponse = (BootstrapReadResponse) previousResponse.get(0);
Integer bootstrapServerIdOld = null;
if (readResponse.isSuccess()) {
findServerInstanceId(readResponse);
findServerInstanceId(readResponse, session.getEndpoint());
if (this.lwM2MBootstrapSessionClients.get(session.getEndpoint()).getSecurityInstances().size() > 0 && this.lwM2MBootstrapSessionClients.get(session.getEndpoint()).getServerInstances().size() > 0) {
bootstrapServerIdOld = this.findBootstrapServerId(session.getEndpoint());
}
} else {
log.warn(
"Bootstrap ReadResponse return error {} : to continue bootstrap session without find Server Instance Id. {}",
@ -118,7 +130,8 @@ public class LwM2MBootstrapConfigStoreTaskProvider implements BootstrapTaskProvi
}
// create requests from config
tasks.requestsToSend = this.toRequests(config,
config.contentFormat != null ? config.contentFormat : session.getContentFormat());
config.contentFormat != null ? config.contentFormat : session.getContentFormat(),
bootstrapServerIdOld, session.getEndpoint());
} else {
// create requests from config
tasks.requestsToSend = BootstrapUtil.toRequests(config,
@ -132,9 +145,15 @@ public class LwM2MBootstrapConfigStoreTaskProvider implements BootstrapTaskProvi
return config.autoIdForSecurityObject;
}
protected void findSecurityInstanceId(Link[] objectLinks) {
/**
* "Short Server ID": This Resource MUST be set when the Bootstrap-Server Resource has a value of 'false'.
* The values ID:0 and ID:65535 values MUST NOT be used for identifying the LwM2M Server.
* "Short Server ID":
* - Link Instance (lwm2m Server) hase linkParams with key = "ssid" value = "shortId" (ver lvm2m = 1.1).
* - Link Instance (bootstrap Server) hase not linkParams with key = "ssid" (ver lvm2m = 1.1).
*/
protected void findSecurityInstanceId(Link[] objectLinks, String endpoint) {
log.info("Object after discover: [{}]", objectLinks);
this.securityInstances = new HashMap<>();
for (Link link : objectLinks) {
if (link.getUriReference().startsWith("/0/")) {
try {
@ -142,15 +161,15 @@ public class LwM2MBootstrapConfigStoreTaskProvider implements BootstrapTaskProvi
if (path.isObjectInstance()) {
if (link.getLinkParams().containsKey("ssid")) {
int serverId = Integer.parseInt(link.getLinkParams().get("ssid").getUnquoted());
if (!this.securityInstances.containsKey(serverId)) {
this.securityInstances.put(serverId, path.getObjectInstanceId());
if (!lwM2MBootstrapSessionClients.get(endpoint).getSecurityInstances().containsKey(serverId)) {
lwM2MBootstrapSessionClients.get(endpoint).getSecurityInstances().put(serverId, path.getObjectInstanceId());
} else {
log.error("Invalid lwm2mSecurityInstance by [{}]", path.getObjectInstanceId());
}
this.securityInstances.put(Integer.valueOf(link.getLinkParams().get("ssid").getUnquoted()), path.getObjectInstanceId());
lwM2MBootstrapSessionClients.get(endpoint).getSecurityInstances().put(Integer.valueOf(link.getLinkParams().get("ssid").getUnquoted()), path.getObjectInstanceId());
} else {
if (!this.securityInstances.containsKey(0)) {
this.securityInstances.put(0, path.getObjectInstanceId());
if (!this.lwM2MBootstrapSessionClients.get(endpoint).getSecurityInstances().containsKey(0)) {
this.lwM2MBootstrapSessionClients.get(endpoint).getSecurityInstances().put(BOOTSTRAP_DEFAULT_SHORT_ID, path.getObjectInstanceId());
} else {
log.error("Invalid bootstrapSecurityInstance by [{}]", path.getObjectInstanceId());
}
@ -164,8 +183,7 @@ public class LwM2MBootstrapConfigStoreTaskProvider implements BootstrapTaskProvi
}
}
protected void findServerInstanceId(BootstrapReadResponse readResponse) {
this.serverInstances = new HashMap<>();
protected void findServerInstanceId(BootstrapReadResponse readResponse, String endpoint) {
try {
((LwM2mObject) readResponse.getContent()).getInstances().values().forEach(instance -> {
var shId = OPAQUE.equals(instance.getResource(0).getType()) ? new BigInteger((byte[]) instance.getResource(0).getValue()).intValue() : instance.getResource(0).getValue();
@ -175,23 +193,22 @@ public class LwM2MBootstrapConfigStoreTaskProvider implements BootstrapTaskProvi
} else {
shortId = (int) shId;
}
serverInstances.put(shortId, instance.getId());
this.lwM2MBootstrapSessionClients.get(endpoint).getServerInstances().put(shortId, instance.getId());
});
} catch (Exception e) {
log.error("Failed find Server Instance Id. ", e);
}
if (this.securityInstances != null && this.securityInstances.size() > 0 && this.serverInstances != null && this.serverInstances.size() > 0) {
this.findBootstrapServerId();
}
}
protected void findBootstrapServerId() {
Map<Integer, Integer> filteredMap = this.serverInstances.entrySet()
.stream().filter(x -> !this.securityInstances.containsKey(x.getKey()))
protected Integer findBootstrapServerId(String endpoint) {
Integer bootstrapServerIdOld = null;
Map<Integer, Integer> filteredMap = this.lwM2MBootstrapSessionClients.get(endpoint).getServerInstances().entrySet()
.stream().filter(x -> !this.lwM2MBootstrapSessionClients.get(endpoint).getSecurityInstances().containsKey(x.getKey()))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
if (filteredMap.size() > 0) {
this.bootstrapServerIdOld = filteredMap.keySet().stream().findFirst().get();
bootstrapServerIdOld = filteredMap.keySet().stream().findFirst().get();
}
return bootstrapServerIdOld;
}
public BootstrapConfigStore getStore() {
@ -213,46 +230,51 @@ public class LwM2MBootstrapConfigStoreTaskProvider implements BootstrapTaskProvi
public List<BootstrapDownlinkRequest<? extends LwM2mResponse>> toRequests(BootstrapConfig bootstrapConfig,
ContentFormat contentFormat) {
ContentFormat contentFormat,
Integer bootstrapServerIdOld,
String endpoint) {
List<BootstrapDownlinkRequest<? extends LwM2mResponse>> requests = new ArrayList<>();
Set<String> pathsDelete = new HashSet<>();
List<BootstrapDownlinkRequest<? extends LwM2mResponse>> requestsWrite = new ArrayList<>();
boolean isBsServer = false;
boolean isLwServer = false;
/** Map<serverId, InstanceId> */
/** Map<serverId ("Short Server ID"), InstanceId> */
Map<Integer, Integer> instances = new HashMap<>();
Integer bootstrapServerIdNew = null;
// handle security
int id = 0;
int lwm2mSecurityInstanceId = 0;
int bootstrapSecurityInstanceId = this.lwM2MBootstrapSessionClients.get(endpoint).getSecurityInstances().get(BOOTSTRAP_DEFAULT_SHORT_ID);
for (BootstrapConfig.ServerSecurity security : new TreeMap<>(bootstrapConfig.security).values()) {
if (security.bootstrapServer) {
requestsWrite.add(toWriteRequest(this.securityInstances.get(0), security, contentFormat));
requestsWrite.add(toWriteRequest(bootstrapSecurityInstanceId, security, contentFormat));
isBsServer = true;
this.bootstrapServerIdNew = security.serverId;
instances.put(security.serverId, this.securityInstances.get(0));
bootstrapServerIdNew = security.serverId;
instances.put(security.serverId, bootstrapSecurityInstanceId);
} else {
if (id == this.securityInstances.get(0)) {
id++;
if (lwm2mSecurityInstanceId == bootstrapSecurityInstanceId) {
lwm2mSecurityInstanceId++;
}
requestsWrite.add(toWriteRequest(id, security, contentFormat));
instances.put(security.serverId, id);
requestsWrite.add(toWriteRequest(lwm2mSecurityInstanceId, security, contentFormat));
instances.put(security.serverId, lwm2mSecurityInstanceId);
isLwServer = true;
if (!isBsServer && this.securityInstances.containsKey(security.serverId) && id != this.securityInstances.get(security.serverId)) {
pathsDelete.add("/0/" + this.securityInstances.get(security.serverId));
if (!isBsServer && this.lwM2MBootstrapSessionClients.get(endpoint).getSecurityInstances().containsKey(security.serverId) &&
lwm2mSecurityInstanceId != this.lwM2MBootstrapSessionClients.get(endpoint).getSecurityInstances().get(security.serverId)) {
pathsDelete.add("/0/" + this.lwM2MBootstrapSessionClients.get(endpoint).getSecurityInstances().get(security.serverId));
}
/**
* If there is an instance in the serverInstances with serverId which we replace in the securityInstances
*/
// find serverId in securityInstances by id (instance)
Integer serverIdOld = null;
for (Map.Entry<Integer, Integer> entry : this.securityInstances.entrySet()) {
if (entry.getValue().equals(id)) {
for (Map.Entry<Integer, Integer> entry : this.lwM2MBootstrapSessionClients.get(endpoint).getSecurityInstances().entrySet()) {
if (entry.getValue().equals(lwm2mSecurityInstanceId)) {
serverIdOld = entry.getKey();
}
}
if (!isBsServer && serverIdOld != null && this.serverInstances.containsKey(serverIdOld)) {
pathsDelete.add("/1/" + this.serverInstances.get(serverIdOld));
if (!isBsServer && serverIdOld != null && this.lwM2MBootstrapSessionClients.get(endpoint).getServerInstances().containsKey(serverIdOld)) {
pathsDelete.add("/1/" + this.lwM2MBootstrapSessionClients.get(endpoint).getServerInstances().get(serverIdOld));
}
id++;
lwm2mSecurityInstanceId++;
}
}
// handle server
@ -261,12 +283,13 @@ public class LwM2MBootstrapConfigStoreTaskProvider implements BootstrapTaskProvi
requestsWrite.add(toWriteRequest(securityInstanceId, server.getValue(), contentFormat));
if (!isBsServer) {
/** Delete instance if bootstrapServerIdNew not equals bootstrapServerIdOld or securityInstanceBsIdNew not equals serverInstanceBsIdOld */
if (this.bootstrapServerIdNew != null && server.getValue().shortId == this.bootstrapServerIdNew &&
(this.bootstrapServerIdNew != this.bootstrapServerIdOld || securityInstanceId != this.serverInstances.get(this.bootstrapServerIdOld))) {
pathsDelete.add("/1/" + this.serverInstances.get(this.bootstrapServerIdOld));
if (bootstrapServerIdNew != null && server.getValue().shortId == bootstrapServerIdNew &&
(bootstrapServerIdNew != bootstrapServerIdOld || securityInstanceId != this.lwM2MBootstrapSessionClients.get(endpoint).getServerInstances().get(bootstrapServerIdOld))) {
pathsDelete.add("/1/" + this.lwM2MBootstrapSessionClients.get(endpoint).getServerInstances().get(bootstrapServerIdOld));
/** Delete instance if serverIdNew is present in serverInstances and securityInstanceIdOld by serverIdNew not equals serverInstanceIdOld */
} else if (this.serverInstances.containsKey(server.getValue().shortId) && securityInstanceId != this.serverInstances.get(server.getValue().shortId)) {
pathsDelete.add("/1/" + this.serverInstances.get(server.getValue().shortId));
} else if (this.lwM2MBootstrapSessionClients.get(endpoint).getServerInstances().containsKey(server.getValue().shortId) &&
securityInstanceId != this.lwM2MBootstrapSessionClients.get(endpoint).getServerInstances().get(server.getValue().shortId)) {
pathsDelete.add("/1/" + this.lwM2MBootstrapSessionClients.get(endpoint).getServerInstances().get(server.getValue().shortId));
}
}
}
@ -288,10 +311,31 @@ public class LwM2MBootstrapConfigStoreTaskProvider implements BootstrapTaskProvi
return (requests);
}
private void initSupportedObjectsDefault() {
this.supportedObjects = new HashMap<>();
this.supportedObjects.put(0, "1.1");
this.supportedObjects.put(1, "1.1");
this.supportedObjects.put(2, "1.0");
}
@Override
public void remove(String endpoint) {
writeLock.lock();
try {
this.lwM2MBootstrapSessionClients.remove(endpoint);
} finally {
writeLock.unlock();
}
}
@Override
public void put(String endpoint) throws InvalidConfigurationException {
writeLock.lock();
try {
this.lwM2MBootstrapSessionClients.put(endpoint, new LwM2MBootstrapClientInstanceIds());
} finally {
writeLock.unlock();
}
}
}

26
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/store/LwM2MBootstrapTaskProvider.java

@ -0,0 +1,26 @@
/**
* Copyright © 2016-2022 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.bootstrap.store;
import org.eclipse.leshan.server.bootstrap.BootstrapTaskProvider;
import org.eclipse.leshan.server.bootstrap.InvalidConfigurationException;
public interface LwM2MBootstrapTaskProvider extends BootstrapTaskProvider {
void put(String endpoint) throws InvalidConfigurationException;
void remove(String endpoint);
}

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

@ -20,11 +20,15 @@ import lombok.extern.slf4j.Slf4j;
import org.eclipse.californium.scandium.config.DtlsConfig;
import org.eclipse.californium.scandium.config.DtlsConnectorConfig;
import org.eclipse.californium.scandium.dtls.cipher.CipherSuite;
import org.eclipse.leshan.core.node.LwM2mNode;
import org.eclipse.leshan.core.node.codec.DefaultLwM2mDecoder;
import org.eclipse.leshan.core.node.codec.DefaultLwM2mEncoder;
import org.eclipse.leshan.core.request.SendRequest;
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.registration.Registration;
import org.eclipse.leshan.server.send.SendListener;
import org.springframework.stereotype.Component;
import org.thingsboard.server.cache.ota.OtaPackageDataCache;
import org.thingsboard.server.common.data.DataConstants;
@ -40,6 +44,7 @@ import org.thingsboard.server.transport.lwm2m.utils.LwM2mValueConverterImpl;
import javax.annotation.PreDestroy;
import java.security.cert.X509Certificate;
import java.util.Map;
import static org.eclipse.californium.scandium.config.DtlsConfig.DTLS_RECOMMENDED_CIPHER_SUITES_ONLY;
import static org.eclipse.californium.scandium.config.DtlsConfig.DTLS_RECOMMENDED_CURVES_ONLY;
@ -94,6 +99,7 @@ public class DefaultLwM2mTransportService implements LwM2MTransportService {
this.server.getRegistrationService().addListener(lhServerCertListener.registrationListener);
this.server.getPresenceService().addListener(lhServerCertListener.presenceListener);
this.server.getObservationService().addListener(lhServerCertListener.observationListener);
this.server.getSendService().addListener(lhServerCertListener.sendListener);
log.info("Started LwM2M transport server.");
}

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

@ -16,9 +16,11 @@
package org.thingsboard.server.transport.lwm2m.server;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.leshan.core.node.LwM2mNode;
import org.eclipse.leshan.core.observation.CompositeObservation;
import org.eclipse.leshan.core.observation.Observation;
import org.eclipse.leshan.core.observation.SingleObservation;
import org.eclipse.leshan.core.request.SendRequest;
import org.eclipse.leshan.core.response.ObserveCompositeResponse;
import org.eclipse.leshan.core.response.ObserveResponse;
import org.eclipse.leshan.server.observation.ObservationListener;
@ -26,9 +28,11 @@ 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.eclipse.leshan.server.send.SendListener;
import org.thingsboard.server.transport.lwm2m.server.uplink.LwM2mUplinkMsgHandler;
import java.util.Collection;
import java.util.Map;
import static org.thingsboard.server.transport.lwm2m.utils.LwM2MTransportUtil.convertObjectIdToVersionedId;
@ -119,4 +123,14 @@ public class LwM2mServerListener {
log.trace("Successful start newObservation {}.", ((SingleObservation)observation).getPath());
}
};
public final SendListener sendListener = new SendListener() {
@Override
public void dataReceived(Registration registration, Map<String, LwM2mNode> map, SendRequest sendRequest) {
if (registration != null) {
service.onUpdateValueWithSendRequest(registration, sendRequest);
}
}
};
}

69
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/uplink/DefaultLwM2mUplinkMsgHandler.java

@ -33,11 +33,7 @@ import org.eclipse.leshan.core.node.LwM2mResource;
import org.eclipse.leshan.core.node.LwM2mResourceInstance;
import org.eclipse.leshan.core.node.LwM2mSingleResource;
import org.eclipse.leshan.core.observation.Observation;
import org.eclipse.leshan.core.request.CreateRequest;
import org.eclipse.leshan.core.request.ObserveRequest;
import org.eclipse.leshan.core.request.ReadRequest;
import org.eclipse.leshan.core.request.WriteCompositeRequest;
import org.eclipse.leshan.core.request.WriteRequest;
import org.eclipse.leshan.core.request.*;
import org.eclipse.leshan.core.request.WriteRequest.Mode;
import org.eclipse.leshan.core.response.ObserveResponse;
import org.eclipse.leshan.core.response.ReadCompositeResponse;
@ -98,16 +94,7 @@ import org.thingsboard.server.transport.lwm2m.utils.LwM2mValueConverterImpl;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Random;
import java.util.Set;
import java.util.UUID;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
@ -347,12 +334,7 @@ public class DefaultLwM2mUplinkMsgHandler extends LwM2MExecutorAwareService impl
this.updateResourcesValue(lwM2MClient, lwM2mResource, path, Mode.UPDATE, responseCode);
}
}
if (clientContext.awake(lwM2MClient)) {
// clientContext.awake calls clientContext.update
log.debug("[{}] Device is awake", lwM2MClient.getEndpoint());
} else {
clientContext.update(lwM2MClient);
}
tryAwake(lwM2MClient);
}
}
@ -373,12 +355,37 @@ public class DefaultLwM2mUplinkMsgHandler extends LwM2MExecutorAwareService impl
}
});
clientContext.update(lwM2MClient);
if (clientContext.awake(lwM2MClient)) {
// clientContext.awake calls clientContext.update
log.debug("[{}] Device is awake", lwM2MClient.getEndpoint());
} else {
clientContext.update(lwM2MClient);
tryAwake(lwM2MClient);
}
}
/**
* Sending updated value to thingsboard from SendListener.dataReceived: object, instance, SingleResource or MultipleResource
*
* @param registration - Registration LwM2M Client
* @param sendRequest - sendRequest
*/
@Override
public void onUpdateValueWithSendRequest(Registration registration, SendRequest sendRequest) {
for(var entry : sendRequest.getNodes().entrySet()) {
LwM2mPath path = entry.getKey();
LwM2mNode node = entry.getValue();
LwM2mClient lwM2MClient = clientContext.getClientByEndpoint(registration.getEndpoint());
String stringPath = convertObjectIdToVersionedId(path.toString(), registration);
ObjectModel objectModelVersion = lwM2MClient.getObjectModel(stringPath, modelProvider);
if (objectModelVersion != null) {
if (node instanceof LwM2mObject) {
LwM2mObject lwM2mObject = (LwM2mObject) node;
this.updateObjectResourceValue(lwM2MClient, lwM2mObject, stringPath, 0);
} else if (node instanceof LwM2mObjectInstance) {
LwM2mObjectInstance lwM2mObjectInstance = (LwM2mObjectInstance) node;
this.updateObjectInstanceResourceValue(lwM2MClient, lwM2mObjectInstance, stringPath, 0);
} else if (node instanceof LwM2mResource) {
LwM2mResource lwM2mResource = (LwM2mResource) node;
this.updateResourcesValue(lwM2MClient, lwM2mResource, stringPath, Mode.UPDATE, 0);
}
}
tryAwake(lwM2MClient);
}
}
@ -989,4 +996,14 @@ public class DefaultLwM2mUplinkMsgHandler extends LwM2MExecutorAwareService impl
client.unlock();
}
}
private void tryAwake(LwM2mClient lwM2MClient) {
if (clientContext.awake(lwM2MClient)) {
// clientContext.awake calls clientContext.update
log.debug("[{}] Device is awake", lwM2MClient.getEndpoint());
} else {
clientContext.update(lwM2MClient);
}
}
}

3
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/uplink/LwM2mUplinkMsgHandler.java

@ -17,6 +17,7 @@ package org.thingsboard.server.transport.lwm2m.server.uplink;
import org.eclipse.leshan.core.observation.Observation;
import org.eclipse.leshan.core.request.CreateRequest;
import org.eclipse.leshan.core.request.SendRequest;
import org.eclipse.leshan.core.request.WriteCompositeRequest;
import org.eclipse.leshan.core.request.WriteRequest;
import org.eclipse.leshan.core.response.ReadCompositeResponse;
@ -46,6 +47,8 @@ public interface LwM2mUplinkMsgHandler {
void onUpdateValueAfterReadCompositeResponse(Registration registration, ReadCompositeResponse response);
void onUpdateValueWithSendRequest(Registration registration, SendRequest sendRequest);
void onDeviceProfileUpdate(TransportProtos.SessionInfoProto sessionInfo, DeviceProfile deviceProfile);
void onDeviceUpdate(TransportProtos.SessionInfoProto sessionInfo, Device device, Optional<DeviceProfile> deviceProfileOpt);

1
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/utils/LwM2MTransportUtil.java

@ -92,6 +92,7 @@ public class LwM2MTransportUtil {
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 static final int BOOTSTRAP_DEFAULT_SHORT_ID = 0;
public enum LwM2MClientStrategy {
CLIENT_STRATEGY_1(1, "Read only resources marked as observation"),

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

@ -5,7 +5,7 @@
* 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
* 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,

4
common/util/pom.xml

@ -84,6 +84,10 @@
<artifactId>awaitility</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.thingsboard.common</groupId>
<artifactId>data</artifactId>
</dependency>
</dependencies>
<build>

89
common/util/src/main/java/org/thingsboard/common/util/KvUtil.java

@ -0,0 +1,89 @@
/**
* Copyright © 2016-2022 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.common.util;
import org.thingsboard.server.common.data.kv.KvEntry;
public class KvUtil {
public static String getStringValue(KvEntry entry) {
switch (entry.getDataType()) {
case LONG:
return entry.getLongValue().map(String::valueOf).orElse(null);
case DOUBLE:
return entry.getDoubleValue().map(String::valueOf).orElse(null);
case BOOLEAN:
return entry.getBooleanValue().map(String::valueOf).orElse(null);
case STRING:
return entry.getStrValue().orElse("");
case JSON:
return entry.getJsonValue().orElse("");
default:
return null;
}
}
public static Double getDoubleValue(KvEntry entry) {
switch (entry.getDataType()) {
case LONG:
return entry.getLongValue().map(Long::doubleValue).orElse(null);
case DOUBLE:
return entry.getDoubleValue().orElse(null);
case BOOLEAN:
return entry.getBooleanValue().map(e -> e ? 1.0 : 0).orElse(null);
case STRING:
try {
return Double.parseDouble(entry.getStrValue().orElse(""));
} catch (RuntimeException e) {
return null;
}
case JSON:
try {
return Double.parseDouble(entry.getJsonValue().orElse(""));
} catch (RuntimeException e) {
return null;
}
default:
return null;
}
}
public static Boolean getBoolValue(KvEntry entry) {
switch (entry.getDataType()) {
case LONG:
return entry.getLongValue().map(e -> e != 0).orElse(null);
case DOUBLE:
return entry.getDoubleValue().map(e -> e != 0).orElse(null);
case BOOLEAN:
return entry.getBooleanValue().orElse(null);
case STRING:
try {
return Boolean.parseBoolean(entry.getStrValue().orElse(""));
} catch (RuntimeException e) {
return null;
}
case JSON:
try {
return Boolean.parseBoolean(entry.getJsonValue().orElse(""));
} catch (RuntimeException e) {
return null;
}
default:
return null;
}
}
}

38
dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java

@ -26,6 +26,7 @@ import org.hibernate.exception.ConstraintViolationException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.BaseData;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.edge.Edge;
@ -199,11 +200,37 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC
}
if (ruleChainMetaData.getRuleChainConnections() != null) {
for (RuleChainConnectionInfo nodeToRuleChainConnection : ruleChainMetaData.getRuleChainConnections()) {
RuleChainId targetRuleChainId = nodeToRuleChainConnection.getTargetRuleChainId();
RuleChain targetRuleChain = findRuleChainById(TenantId.SYS_TENANT_ID, targetRuleChainId);
RuleNode targetNode = new RuleNode();
targetNode.setName(targetRuleChain != null ? targetRuleChain.getName() : "Rule Chain Input");
targetNode.setRuleChainId(ruleChain.getId());
targetNode.setType("org.thingsboard.rule.engine.flow.TbRuleChainInputNode");
var configuration = JacksonUtil.newObjectNode();
configuration.put("ruleChainId", targetRuleChainId.getId().toString());
targetNode.setConfiguration(configuration);
ObjectNode layout = (ObjectNode) nodeToRuleChainConnection.getAdditionalInfo();
layout.remove("description");
layout.remove("ruleChainNodeId");
targetNode.setAdditionalInfo(layout);
targetNode.setDebugMode(false);
targetNode = ruleNodeDao.save(tenantId, targetNode);
EntityRelation sourceRuleChainToRuleNode = new EntityRelation();
sourceRuleChainToRuleNode.setFrom(ruleChain.getId());
sourceRuleChainToRuleNode.setTo(targetNode.getId());
sourceRuleChainToRuleNode.setType(EntityRelation.CONTAINS_TYPE);
sourceRuleChainToRuleNode.setTypeGroup(RelationTypeGroup.RULE_CHAIN);
relationService.saveRelation(tenantId, sourceRuleChainToRuleNode);
EntityRelation sourceRuleNodeToTargetRuleNode = new EntityRelation();
EntityId from = nodes.get(nodeToRuleChainConnection.getFromIndex()).getId();
EntityId to = nodeToRuleChainConnection.getTargetRuleChainId();
String type = nodeToRuleChainConnection.getType();
createRelation(tenantId, new EntityRelation(from, to, type, RelationTypeGroup.RULE_NODE, nodeToRuleChainConnection.getAdditionalInfo()));
}
sourceRuleNodeToTargetRuleNode.setFrom(from);
sourceRuleNodeToTargetRuleNode.setTo(targetNode.getId());
sourceRuleNodeToTargetRuleNode.setType(nodeToRuleChainConnection.getType());
sourceRuleNodeToTargetRuleNode.setTypeGroup(RelationTypeGroup.RULE_NODE);
relationService.saveRelation(tenantId, sourceRuleNodeToTargetRuleNode);
}
}
}
@ -263,8 +290,7 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC
int toIndex = ruleNodeIndexMap.get(toNodeId);
ruleChainMetaData.addConnectionInfo(fromIndex, toIndex, type);
} else if (nodeRelation.getTo().getEntityType() == EntityType.RULE_CHAIN) {
RuleChainId targetRuleChainId = new RuleChainId(nodeRelation.getTo().getId());
ruleChainMetaData.addRuleChainConnectionInfo(fromIndex, targetRuleChainId, type, nodeRelation.getAdditionalInfo());
log.warn("[{}][{}] Unsupported node relation: {}", tenantId, ruleChainId, nodeRelation.getTo());
}
}
}

6
dao/src/main/resources/sql/schema-entities.sql

@ -213,7 +213,6 @@ CREATE TABLE IF NOT EXISTS ota_package (
additional_info varchar,
search_text varchar(255),
CONSTRAINT ota_package_tenant_title_version_unq_key UNIQUE (tenant_id, title, version)
-- CONSTRAINT fk_device_profile_firmware FOREIGN KEY (device_profile_id) REFERENCES device_profile(id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS device_profile (
@ -243,6 +242,11 @@ CREATE TABLE IF NOT EXISTS device_profile (
CONSTRAINT fk_software_device_profile FOREIGN KEY (software_id) REFERENCES ota_package(id)
);
ALTER TABLE ota_package
ADD CONSTRAINT fk_device_profile_ota_package
FOREIGN KEY (device_profile_id) REFERENCES device_profile (id)
ON DELETE CASCADE;
-- We will use one-to-many relation in the first release and extend this feature in case of user requests
-- CREATE TABLE IF NOT EXISTS device_profile_firmware (
-- device_profile_id uuid NOT NULL,

22
dao/src/test/java/org/thingsboard/server/dao/service/AbstractServiceTest.java

@ -33,14 +33,18 @@ import org.thingsboard.server.common.data.DeviceProfileType;
import org.thingsboard.server.common.data.DeviceTransportType;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.Event;
import org.thingsboard.server.common.data.OtaPackage;
import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.device.profile.DefaultDeviceProfileConfiguration;
import org.thingsboard.server.common.data.device.profile.DefaultDeviceProfileTransportConfiguration;
import org.thingsboard.server.common.data.device.profile.DeviceProfileData;
import org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.common.data.id.DeviceProfileId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.HasId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.ota.ChecksumAlgorithm;
import org.thingsboard.server.common.data.ota.OtaPackageType;
import org.thingsboard.server.dao.alarm.AlarmService;
import org.thingsboard.server.dao.asset.AssetService;
import org.thingsboard.server.dao.audit.AuditLogLevelFilter;
@ -70,6 +74,7 @@ import org.thingsboard.server.dao.widget.WidgetTypeService;
import org.thingsboard.server.dao.widget.WidgetsBundleService;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Map;
@ -251,4 +256,21 @@ public abstract class AbstractServiceTest {
edge.setRoutingKey(RandomStringUtils.randomAlphanumeric(20));
return edge;
}
protected OtaPackage constructDefaultOtaPackage(TenantId tenantId, DeviceProfileId deviceProfileId) {
OtaPackage firmware = new OtaPackage();
firmware.setTenantId(tenantId);
firmware.setDeviceProfileId(deviceProfileId);
firmware.setType(OtaPackageType.FIRMWARE);
firmware.setTitle("My firmware");
firmware.setVersion("3.3.3");
firmware.setFileName("filename.txt");
firmware.setContentType("text/plain");
firmware.setChecksumAlgorithm(ChecksumAlgorithm.SHA256);
firmware.setChecksum("4bf5122f344554c53bde2ebb8cd2b7e3d1600ad631c385a5d7cce23c7785459a");
firmware.setData(ByteBuffer.wrap(new byte[]{1}));
firmware.setDataSize(1L);
return firmware;
}
}

17
dao/src/test/java/org/thingsboard/server/dao/service/BaseDeviceProfileServiceTest.java

@ -45,6 +45,7 @@ import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executors;
import java.util.stream.Collectors;
import static org.assertj.core.api.Assertions.assertThat;
import static org.thingsboard.server.common.data.ota.OtaPackageType.FIRMWARE;
public abstract class BaseDeviceProfileServiceTest extends AbstractServiceTest {
@ -252,6 +253,22 @@ public abstract class BaseDeviceProfileServiceTest extends AbstractServiceTest {
deviceProfileService.deleteDeviceProfile(tenantId, savedDeviceProfile.getId());
}
@Test
public void testDeleteDeviceProfileWithExistingOta_cascadeDelete() {
DeviceProfile deviceProfile = this.createDeviceProfile(tenantId, "Device Profile");
deviceProfile = deviceProfileService.saveDeviceProfile(deviceProfile);
OtaPackage otaPackage = constructDefaultOtaPackage(tenantId, deviceProfile.getId());
otaPackage = otaPackageService.saveOtaPackage(otaPackage);
assertThat(deviceProfileService.findDeviceProfileById(tenantId, deviceProfile.getId())).isNotNull();
assertThat(otaPackageService.findOtaPackageById(tenantId, otaPackage.getId())).isNotNull();
deviceProfileService.deleteDeviceProfile(tenantId, deviceProfile.getId());
assertThat(deviceProfileService.findDeviceProfileById(tenantId, deviceProfile.getId())).isNull();
assertThat(otaPackageService.findOtaPackageById(tenantId, otaPackage.getId())).isNull();
}
@Test
public void testDeleteDeviceProfile() {
DeviceProfile deviceProfile = this.createDeviceProfile(tenantId, "Device Profile");

1
dao/src/test/java/org/thingsboard/server/dao/service/BaseOtaPackageServiceTest.java

@ -488,7 +488,6 @@ public abstract class BaseOtaPackageServiceTest extends AbstractServiceTest {
otaPackageService.deleteOtaPackage(tenantId, savedFirmware.getId());
} finally {
deviceProfileService.deleteDeviceProfile(tenantId, savedDeviceProfile.getId());
otaPackageService.deleteOtaPackage(tenantId, savedFirmware.getId());
}
}

2
dao/src/test/resources/sql/system-test-psql.sql

@ -1,2 +1,2 @@
--PostgreSQL specific truncate to fit constraints
TRUNCATE TABLE device_credentials, device, device_profile, rule_node_state, rule_node, rule_chain;
TRUNCATE TABLE device_credentials, device, device_profile, ota_package, rule_node_state, rule_node, rule_chain;

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

@ -1,6 +1,6 @@
TRUNCATE TABLE device_credentials;
TRUNCATE TABLE device;
TRUNCATE TABLE device_profile;
TRUNCATE TABLE device_profile CASCADE;
TRUNCATE TABLE rule_node_state;
TRUNCATE TABLE rule_node;
TRUNCATE TABLE rule_chain;

53
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCreateAlarmNode.java

@ -23,6 +23,7 @@ import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.EnumUtils;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.rule.engine.api.RuleNode;
import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.rule.engine.api.TbNodeConfiguration;
@ -122,40 +123,66 @@ public class TbCreateAlarmNode extends TbAbstractAlarmNode<TbCreateAlarmNodeConf
}
private ListenableFuture<TbAlarmResult> createNewAlarm(TbContext ctx, TbMsg msg, Alarm msgAlarm) {
ListenableFuture<Alarm> asyncAlarm;
if (msgAlarm != null) {
asyncAlarm = Futures.immediateFuture(msgAlarm);
} else {
ListenableFuture<JsonNode> asyncDetails;
boolean buildDetails = !config.isUseMessageAlarmData() || config.isOverwriteAlarmDetails();
if (buildDetails) {
ctx.logJsEvalRequest();
asyncAlarm = Futures.transform(buildAlarmDetails(ctx, msg, null),
details -> {
ctx.logJsEvalResponse();
return buildAlarm(msg, details, ctx.getTenantId());
}, MoreExecutors.directExecutor());
asyncDetails = buildAlarmDetails(ctx, msg, null);
} else {
asyncDetails = Futures.immediateFuture(null);
}
ListenableFuture<Alarm> asyncAlarm = Futures.transform(asyncDetails, details -> {
if (buildDetails) {
ctx.logJsEvalResponse();
}
Alarm newAlarm;
if (msgAlarm != null) {
newAlarm = msgAlarm;
if (buildDetails) {
newAlarm.setDetails(details);
}
} else {
newAlarm = buildAlarm(msg, details, ctx.getTenantId());
}
return newAlarm;
}, MoreExecutors.directExecutor());
ListenableFuture<Alarm> asyncCreated = Futures.transform(asyncAlarm,
alarm -> ctx.getAlarmService().createOrUpdateAlarm(alarm), ctx.getDbCallbackExecutor());
return Futures.transform(asyncCreated, alarm -> new TbAlarmResult(true, false, false, alarm), MoreExecutors.directExecutor());
}
private ListenableFuture<TbAlarmResult> updateAlarm(TbContext ctx, TbMsg msg, Alarm existingAlarm, Alarm msgAlarm) {
ctx.logJsEvalRequest();
ListenableFuture<Alarm> asyncUpdated = Futures.transform(buildAlarmDetails(ctx, msg, existingAlarm.getDetails()), (Function<JsonNode, Alarm>) details -> {
ctx.logJsEvalResponse();
ListenableFuture<JsonNode> asyncDetails;
boolean buildDetails = !config.isUseMessageAlarmData() || config.isOverwriteAlarmDetails();
if (buildDetails) {
ctx.logJsEvalRequest();
asyncDetails = buildAlarmDetails(ctx, msg, existingAlarm.getDetails());
} else {
asyncDetails = Futures.immediateFuture(null);
}
ListenableFuture<Alarm> asyncUpdated = Futures.transform(asyncDetails, (Function<JsonNode, Alarm>) details -> {
if (buildDetails) {
ctx.logJsEvalResponse();
}
if (msgAlarm != null) {
existingAlarm.setSeverity(msgAlarm.getSeverity());
existingAlarm.setPropagate(msgAlarm.isPropagate());
existingAlarm.setPropagateToOwner(msgAlarm.isPropagateToOwner());
existingAlarm.setPropagateToTenant(msgAlarm.isPropagateToTenant());
existingAlarm.setPropagateRelationTypes(msgAlarm.getPropagateRelationTypes());
if (buildDetails) {
existingAlarm.setDetails(details);
} else {
existingAlarm.setDetails(msgAlarm.getDetails());
}
} else {
existingAlarm.setSeverity(processAlarmSeverity(msg));
existingAlarm.setPropagate(config.isPropagate());
existingAlarm.setPropagateToOwner(config.isPropagateToOwner());
existingAlarm.setPropagateToTenant(config.isPropagateToTenant());
existingAlarm.setPropagateRelationTypes(relationTypes);
existingAlarm.setDetails(details);
}
existingAlarm.setDetails(details);
existingAlarm.setEndTs(System.currentTimeMillis());
return ctx.getAlarmService().createOrUpdateAlarm(existingAlarm);
}, ctx.getDbCallbackExecutor());

3
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCreateAlarmNodeConfiguration.java

@ -30,6 +30,7 @@ public class TbCreateAlarmNodeConfiguration extends TbAbstractAlarmNodeConfigura
private boolean propagateToOwner;
private boolean propagateToTenant;
private boolean useMessageAlarmData;
private boolean overwriteAlarmDetails = true;
private boolean dynamicSeverity;
private List<String> relationTypes;
@ -44,8 +45,10 @@ public class TbCreateAlarmNodeConfiguration extends TbAbstractAlarmNodeConfigura
configuration.setPropagateToOwner(false);
configuration.setPropagateToTenant(false);
configuration.setUseMessageAlarmData(false);
configuration.setOverwriteAlarmDetails(false);
configuration.setRelationTypes(Collections.emptyList());
configuration.setDynamicSeverity(false);
return configuration;
}
}

2
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/TbDeviceProfileNode.java

@ -99,7 +99,7 @@ public class TbDeviceProfileNode implements TbNode {
log.info("[{}] Fetched alarm rule state for {} entities", ctx.getSelfId(), fetchCount);
}
if (!config.isPersistAlarmRulesState() && ctx.isLocalEntity(ctx.getSelfId())) {
log.info("[{}] Going to cleanup rule node states", ctx.getSelfId());
log.debug("[{}] Going to cleanup rule node states", ctx.getSelfId());
ctx.clearRuleNodeStates();
}
}

2
rule-engine/rule-engine-components/src/main/resources/public/static/rulenode/rulenode-core-config.js

File diff suppressed because one or more lines are too long

54
ui-ngx/src/app/modules/home/components/import-export/import-export.service.ts

@ -35,7 +35,7 @@ import {
import { MatDialog } from '@angular/material/dialog';
import { ImportDialogComponent, ImportDialogData } from '@home/components/import-export/import-dialog.component';
import { forkJoin, Observable, of } from 'rxjs';
import { catchError, map, mergeMap, tap } from 'rxjs/operators';
import {catchError, map, mergeMap, switchMap, tap} from 'rxjs/operators';
import { DashboardUtilsService } from '@core/services/dashboard-utils.service';
import { EntityService } from '@core/http/entity.service';
import { Widget, WidgetSize, WidgetType, WidgetTypeDetails } from '@shared/models/widget.models';
@ -62,6 +62,7 @@ import { TenantProfileService } from '@core/http/tenant-profile.service';
import { DeviceService } from '@core/http/device.service';
import { AssetService } from '@core/http/asset.service';
import { EdgeService } from '@core/http/edge.service';
import { RuleNode } from '@shared/models/rule-node.models';
// @dynamic
@Injectable()
@ -423,7 +424,7 @@ export class ImportExportService {
public importRuleChain(expectedRuleChainType: RuleChainType): Observable<RuleChainImport> {
return this.openImportDialog('rulechain.import', 'rulechain.rulechain-file').pipe(
map((ruleChainImport: RuleChainImport) => {
mergeMap((ruleChainImport: RuleChainImport) => {
if (!this.validateImportedRuleChain(ruleChainImport)) {
this.store.dispatch(new ActionNotificationShow(
{message: this.translate.instant('rulechain.invalid-rulechain-file-error'),
@ -435,7 +436,7 @@ export class ImportExportService {
type: 'error'}));
throw new Error('Invalid rule chain type');
} else {
return ruleChainImport;
return this.processOldRuleChainConnections(ruleChainImport);
}
}),
catchError((err) => {
@ -444,6 +445,53 @@ export class ImportExportService {
);
}
private processOldRuleChainConnections(ruleChainImport: RuleChainImport): Observable<RuleChainImport> {
const metadata = ruleChainImport.metadata;
if ((metadata as any).ruleChainConnections) {
const ruleChainNameResolveObservables: Observable<void>[] = [];
for (const ruleChainConnection of (metadata as any).ruleChainConnections) {
if (ruleChainConnection.targetRuleChainId && ruleChainConnection.targetRuleChainId.id) {
const ruleChainNode: RuleNode = {
name: '',
debugMode: false,
type: 'org.thingsboard.rule.engine.flow.TbRuleChainInputNode',
configuration: {
ruleChainId: ruleChainConnection.targetRuleChainId.id
},
additionalInfo: ruleChainConnection.additionalInfo
};
ruleChainNameResolveObservables.push(this.ruleChainService.getRuleChain(ruleChainNode.configuration.ruleChainId,
{ignoreErrors: true, ignoreLoading: true}).pipe(
catchError(err => {
return of({name: 'Rule Chain Input'} as RuleChain);
}),
map((ruleChain => {
ruleChainNode.name = ruleChain.name;
return null;
})
)
));
const toIndex = metadata.nodes.length;
metadata.nodes.push(ruleChainNode);
metadata.connections.push({
toIndex,
fromIndex: ruleChainConnection.fromIndex,
type: ruleChainConnection.type
});
}
}
if (ruleChainNameResolveObservables.length) {
return forkJoin(ruleChainNameResolveObservables).pipe(
map(() => ruleChainImport)
);
} else {
return of(ruleChainImport);
}
} else {
return of(ruleChainImport);
}
}
public exportDeviceProfile(deviceProfileId: string) {
this.deviceProfileService.getDeviceProfile(deviceProfileId).subscribe(
(deviceProfile) => {

2
ui-ngx/src/app/shared/components/ota-package/ota-package-autocomplete.component.ts

@ -49,7 +49,7 @@ export class OtaPackageAutocompleteComponent implements ControlValueAccessor, On
modelValue: string | EntityId | null;
private otaUpdateType: OtaUpdateType;
private otaUpdateType: OtaUpdateType = OtaUpdateType.FIRMWARE;
get type(): OtaUpdateType {
return this.otaUpdateType;

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

@ -1119,9 +1119,9 @@
"mobile-dashboard-hint": "Used by mobile application as a device details dashboard",
"select-queue-hint": "Select from a drop-down list.",
"delete-device-profile-title": "Are you sure you want to delete the device profile '{{deviceProfileName}}'?",
"delete-device-profile-text": "Be careful, after the confirmation the device profile and all related data will become unrecoverable.",
"delete-device-profile-text": "Be careful, after the confirmation the device profile and all related data including associated OTA updates will become unrecoverable.",
"delete-device-profiles-title": "Are you sure you want to delete { count, plural, 1 {1 device profile} other {# device profiles} }?",
"delete-device-profiles-text": "Be careful, after the confirmation all selected device profiles will be removed and all related data will become unrecoverable.",
"delete-device-profiles-text": "Be careful, after the confirmation all selected device profiles will be removed and all related data including associated OTA updates will become unrecoverable.",
"set-default-device-profile-title": "Are you sure you want to make the device profile '{{deviceProfileName}}' default?",
"set-default-device-profile-text": "After the confirmation the device profile will be marked as default and will be used for new devices with no profile specified.",
"no-device-profiles-found": "No device profiles found.",

32
ui-ngx/src/assets/locale/locale.constant-zh_CN.json

@ -249,8 +249,8 @@
"created-time": "创建时间",
"details": "详情",
"display-status": {
"ACTIVE_ACK": "Active 已确认",
"ACTIVE_UNACK": "Active 未确认",
"ACTIVE_ACK": "激活已确认",
"ACTIVE_UNACK": "激活未确认",
"CLEARED_ACK": "清除已确认",
"CLEARED_UNACK": "清除未确认"
},
@ -265,15 +265,15 @@
"no-alarms-matching": "没有找到匹配 '{{entity}}' 的警告",
"no-alarms-prompt": "未发现警告",
"no-data": "无数据显示",
"originator": "Originator",
"originator-type": "Originator 类型",
"originator": "发起者",
"originator-type": "发起者类型",
"polling-interval": "警告轮询间隔(秒)",
"polling-interval-required": "警告轮询间隔必填。",
"search": "查找警告",
"search-propagated-alarms": "检索已传递的警报",
"search-status": {
"ACK": "已确认",
"ACTIVE": "Active",
"ACTIVE": "激活",
"ANY": "所有",
"CLEARED": "已清除",
"UNACK": "未确认"
@ -540,9 +540,11 @@
"enter-password": "输入密码",
"enter-search": "输入检索条件",
"enter-username": "输入用户名",
"loading": "Loading...",
"loading": "正在加载中...",
"password": "密码",
"username": "用户名"
"username": "用户名",
"proceed": "继续",
"open-details-page": "打开详情页"
},
"confirm-on-exit": {
"html-message": "您有未保存的更改。<br/> 确定要离开此页面吗?",
@ -687,6 +689,7 @@
"display-filters": "显示筛选器",
"display-title": "显示仪表板标题",
"drop-image": "拖拽图像或单击以选择要上传的文件。",
"maximum-upload-file-size": "最大上传文件大小: {{ size }}",
"edit-state": "仪表板状态编辑",
"export": "导出仪表板",
"export-failed-error": "无法导出仪表板: {{error}}",
@ -762,6 +765,10 @@
"state-name-required": "仪表板状态名必填。",
"states": "仪表板状态",
"title": "标题",
"image": "仪表板图片",
"mobile-app-settings": "移动应用设置",
"mobile-order": "移动应用中的仪表板订单",
"mobile-hide": "在移动应用中隐藏仪表板",
"title-color": "标题颜色",
"title-required": "标题必填。",
"toolbar-always-open": "工具栏常驻",
@ -887,6 +894,8 @@
"create-new-device-profile": "创建一个新的!",
"default": "默认",
"default-rule-chain": "默认规则链",
"mobile-dashboard": "移动仪表板",
"mobile-dashboard-hint": "被移动应用用作设备详情仪表板",
"delete": "删除设备配置",
"delete-device-profile-text": "请注意:确认后设备配置和所有相关数据将不可恢复。",
"delete-device-profile-title": "是否确实要删除设备配置 '{{deviceProfileName}}'?",
@ -995,7 +1004,8 @@
"transport-type-snmp-hint": "指定 SNMP 传输配置",
"type": "配置类型",
"type-default": "默认",
"type-required": "配置类型必填。"
"type-required": "配置类型必填。",
"image": "设备配置图片"
},
"device": {
"access-token": "访问令牌",
@ -1997,7 +2007,11 @@
"change-password": "更改密码",
"current-password": "当前密码",
"last-login-time": "最后登录",
"profile": "属性"
"profile": "属性",
"copy-jwt-token": "复制 JWT 令牌",
"valid-till": "有效期至 {{expirationData}}",
"tokenCopiedSuccessMessage": "JWT 令牌已复制到剪贴板",
"tokenCopiedWarnMessage": "JWT 令牌已过期!请刷新页面。"
},
"queue": {
"name": "队列名称",

Loading…
Cancel
Save