Browse Source

Merge pull request #13666 from thingsboard/rc

rc
pull/13670/head
Viacheslav Klimov 11 months ago
committed by GitHub
parent
commit
a776a214ee
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 8
      application/src/main/java/org/thingsboard/server/service/edge/EdgeContextComponent.java
  2. 3
      application/src/main/java/org/thingsboard/server/service/edge/EdgeEventSourcingListener.java
  3. 16
      application/src/main/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtils.java
  4. 21
      application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java
  5. 21
      application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java
  6. 1
      application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeSyncCursor.java
  7. 14
      application/src/main/java/org/thingsboard/server/service/edge/rpc/KafkaEdgeGrpcSession.java
  8. 12
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessor.java
  9. 1
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/AssetEdgeProcessor.java
  10. 79
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/cf/BaseCalculatedFieldProcessor.java
  11. 169
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/cf/CalculatedFieldEdgeProcessor.java
  12. 28
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/cf/CalculatedFieldProcessor.java
  13. 1
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/DeviceEdgeProcessor.java
  14. 45
      application/src/main/java/org/thingsboard/server/service/edge/rpc/sync/DefaultEdgeRequestsService.java
  15. 4
      application/src/main/java/org/thingsboard/server/service/edge/rpc/sync/EdgeRequestsService.java
  16. 58
      application/src/test/java/org/thingsboard/server/controller/TbResourceControllerTest.java
  17. 13
      application/src/test/java/org/thingsboard/server/edge/AbstractEdgeTest.java
  18. 267
      application/src/test/java/org/thingsboard/server/edge/CalculatedFieldEdgeTest.java
  19. 6
      application/src/test/java/org/thingsboard/server/edge/imitator/EdgeImitator.java
  20. 4
      common/cache/src/main/java/org/thingsboard/server/cache/resourceInfo/ResourceInfoCacheKey.java
  21. 4
      common/dao-api/src/main/java/org/thingsboard/server/dao/cf/CalculatedFieldService.java
  22. 9
      common/data/src/main/java/org/thingsboard/server/common/data/edge/EdgeEventType.java
  23. 2
      common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java
  24. 2
      common/edge-api/src/main/java/org/thingsboard/edge/rpc/EdgeGrpcClient.java
  25. 18
      common/edge-api/src/main/proto/edge.proto
  26. 10
      common/proto/src/main/java/org/thingsboard/server/common/util/ProtoUtils.java
  27. 2
      common/proto/src/main/proto/queue.proto
  28. 23
      dao/src/main/java/org/thingsboard/server/dao/cf/BaseCalculatedFieldService.java
  29. 2
      dao/src/main/java/org/thingsboard/server/dao/cf/CalculatedFieldDao.java
  30. 6
      dao/src/main/java/org/thingsboard/server/dao/resource/BaseResourceService.java
  31. 2
      dao/src/main/java/org/thingsboard/server/dao/sql/cf/CalculatedFieldRepository.java
  32. 5
      dao/src/main/java/org/thingsboard/server/dao/sql/cf/JpaCalculatedFieldDao.java
  33. 4
      ui-ngx/src/app/modules/home/components/rule-node/external/azure-iot-hub-config.component.html
  34. 2
      ui-ngx/src/app/modules/home/components/rule-node/external/azure-iot-hub-config.component.ts
  35. 1
      ui-ngx/src/app/modules/home/components/widget/lib/alarm/alarms-table-widget.component.ts
  36. 2
      ui-ngx/src/app/modules/home/components/widget/lib/chart/bar-chart-with-labels-widget.component.html
  37. 3
      ui-ngx/src/app/modules/home/components/widget/lib/chart/bar-chart-with-labels-widget.component.ts
  38. 2
      ui-ngx/src/app/modules/home/components/widget/lib/chart/range-chart-widget.component.html
  39. 19
      ui-ngx/src/app/modules/home/components/widget/lib/chart/range-chart-widget.component.ts
  40. 22
      ui-ngx/src/app/modules/home/components/widget/lib/chart/range-chart-widget.models.ts
  41. 2
      ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart-widget.component.html
  42. 3
      ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart-widget.component.ts
  43. 4
      ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.models.ts
  44. 1
      ui-ngx/src/app/modules/home/components/widget/lib/entity/entities-table-widget.component.ts
  45. 2
      ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget.component.html
  46. 4
      ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget.component.ts
  47. 23
      ui-ngx/src/app/shared/components/mqtt-version-select.component.ts
  48. 14
      ui-ngx/src/app/shared/models/units/speed.ts
  49. 206
      ui-ngx/src/assets/locale/locale.constant-en_US.json

8
application/src/main/java/org/thingsboard/server/service/edge/EdgeContextComponent.java

@ -29,6 +29,7 @@ import org.thingsboard.server.dao.alarm.AlarmService;
import org.thingsboard.server.dao.asset.AssetProfileService;
import org.thingsboard.server.dao.asset.AssetService;
import org.thingsboard.server.dao.attributes.AttributesService;
import org.thingsboard.server.dao.cf.CalculatedFieldService;
import org.thingsboard.server.dao.customer.CustomerService;
import org.thingsboard.server.dao.dashboard.DashboardService;
import org.thingsboard.server.dao.device.DeviceCredentialsService;
@ -61,6 +62,7 @@ import org.thingsboard.server.service.edge.rpc.processor.alarm.AlarmProcessor;
import org.thingsboard.server.service.edge.rpc.processor.alarm.comment.AlarmCommentProcessor;
import org.thingsboard.server.service.edge.rpc.processor.asset.AssetEdgeProcessor;
import org.thingsboard.server.service.edge.rpc.processor.asset.profile.AssetProfileEdgeProcessor;
import org.thingsboard.server.service.edge.rpc.processor.cf.CalculatedFieldProcessor;
import org.thingsboard.server.service.edge.rpc.processor.dashboard.DashboardEdgeProcessor;
import org.thingsboard.server.service.edge.rpc.processor.device.DeviceEdgeProcessor;
import org.thingsboard.server.service.edge.rpc.processor.device.profile.DeviceProfileEdgeProcessor;
@ -248,6 +250,12 @@ public class EdgeContextComponent {
@Autowired
private GrpcCallbackExecutorService grpcCallbackExecutorService;
@Autowired
private CalculatedFieldService calculatedFieldService;
@Autowired
private CalculatedFieldProcessor calculatedFieldProcessor;
public EdgeProcessor getProcessor(EdgeEventType edgeEventType) {
EdgeProcessor processor = processorMap.get(edgeEventType);
if (processor == null) {

3
application/src/main/java/org/thingsboard/server/service/edge/EdgeEventSourcingListener.java

@ -33,6 +33,7 @@ import org.thingsboard.server.common.data.alarm.AlarmApiCallResult;
import org.thingsboard.server.common.data.alarm.AlarmComment;
import org.thingsboard.server.common.data.alarm.EntityAlarm;
import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.cf.CalculatedField;
import org.thingsboard.server.common.data.domain.Domain;
import org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.common.data.edge.EdgeEventActionType;
@ -262,6 +263,8 @@ public class EdgeEventSourcingListener {
private String getBodyMsgForEntityEvent(Object entity) {
if (entity instanceof AlarmComment) {
return JacksonUtil.toString(entity);
} else if (entity instanceof CalculatedField calculatedField) {
return JacksonUtil.toString(calculatedField.getEntityId());
}
return null;
}

16
application/src/main/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtils.java

@ -48,11 +48,13 @@ import org.thingsboard.server.common.data.alarm.Alarm;
import org.thingsboard.server.common.data.alarm.AlarmComment;
import org.thingsboard.server.common.data.asset.Asset;
import org.thingsboard.server.common.data.asset.AssetProfile;
import org.thingsboard.server.common.data.cf.CalculatedField;
import org.thingsboard.server.common.data.domain.DomainInfo;
import org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.common.data.edge.EdgeEventActionType;
import org.thingsboard.server.common.data.id.AssetId;
import org.thingsboard.server.common.data.id.AssetProfileId;
import org.thingsboard.server.common.data.id.CalculatedFieldId;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.DashboardId;
import org.thingsboard.server.common.data.id.DeviceId;
@ -89,6 +91,7 @@ import org.thingsboard.server.gen.edge.v1.AlarmUpdateMsg;
import org.thingsboard.server.gen.edge.v1.AssetProfileUpdateMsg;
import org.thingsboard.server.gen.edge.v1.AssetUpdateMsg;
import org.thingsboard.server.gen.edge.v1.AttributeDeleteMsg;
import org.thingsboard.server.gen.edge.v1.CalculatedFieldUpdateMsg;
import org.thingsboard.server.gen.edge.v1.CustomerUpdateMsg;
import org.thingsboard.server.gen.edge.v1.DashboardUpdateMsg;
import org.thingsboard.server.gen.edge.v1.DeviceCredentialsUpdateMsg;
@ -638,4 +641,17 @@ public class EdgeMsgConstructorUtils {
.build();
}
public static CalculatedFieldUpdateMsg constructCalculatedFieldUpdatedMsg(UpdateMsgType msgType, CalculatedField calculatedField) {
return CalculatedFieldUpdateMsg.newBuilder().setMsgType(msgType).setEntity(JacksonUtil.toString(calculatedField))
.setIdMSB(calculatedField.getId().getId().getMostSignificantBits())
.setIdLSB(calculatedField.getId().getId().getLeastSignificantBits()).build();
}
public static CalculatedFieldUpdateMsg constructCalculatedFieldDeleteMsg(CalculatedFieldId calculatedFieldId) {
return CalculatedFieldUpdateMsg.newBuilder()
.setMsgType(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE)
.setIdMSB(calculatedFieldId.getId().getMostSignificantBits())
.setIdLSB(calculatedFieldId.getId().getLeastSignificantBits()).build();
}
}

21
application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java

@ -94,6 +94,8 @@ import static org.thingsboard.server.service.state.DefaultDeviceStateService.LAS
@TbCoreComponent
public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase implements EdgeRpcService {
private static final int DESTROY_SESSION_MAX_ATTEMPTS = 10;
private final ConcurrentMap<EdgeId, EdgeGrpcSession> sessions = new ConcurrentHashMap<>();
private final ConcurrentMap<EdgeId, Lock> sessionNewEventsLocks = new ConcurrentHashMap<>();
private final Map<EdgeId, Boolean> sessionNewEvents = new HashMap<>();
@ -283,9 +285,8 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i
EdgeGrpcSession session = sessions.get(edgeId);
if (session != null && session.isConnected()) {
log.info("[{}] Closing and removing session for edge [{}]", tenantId, edgeId);
session.destroy();
destroySession(session);
session.cleanUp();
session.close();
sessions.remove(edgeId);
final Lock newEventLock = sessionNewEventsLocks.computeIfAbsent(edgeId, id -> new ReentrantLock());
newEventLock.lock();
@ -521,7 +522,15 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i
private void destroySession(EdgeGrpcSession session) {
try (session) {
session.destroy();
for (int i = 0; i < DESTROY_SESSION_MAX_ATTEMPTS; i++) {
if (session.destroy()) {
break;
} else {
try {
Thread.sleep(100);
} catch (InterruptedException ignored) {}
}
}
}
}
@ -643,9 +652,11 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i
}
for (EdgeId edgeId : toRemove) {
log.info("[{}] Destroying session for edge because edge is not connected", edgeId);
EdgeGrpcSession removed = sessions.remove(edgeId);
EdgeGrpcSession removed = sessions.get(edgeId);
if (removed instanceof KafkaEdgeGrpcSession kafkaSession) {
kafkaSession.destroy();
if (kafkaSession.destroy()) {
sessions.remove(edgeId);
}
}
}
} catch (Exception e) {

21
application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java

@ -50,6 +50,8 @@ import org.thingsboard.server.gen.edge.v1.AlarmUpdateMsg;
import org.thingsboard.server.gen.edge.v1.AssetProfileUpdateMsg;
import org.thingsboard.server.gen.edge.v1.AssetUpdateMsg;
import org.thingsboard.server.gen.edge.v1.AttributesRequestMsg;
import org.thingsboard.server.gen.edge.v1.CalculatedFieldRequestMsg;
import org.thingsboard.server.gen.edge.v1.CalculatedFieldUpdateMsg;
import org.thingsboard.server.gen.edge.v1.ConnectRequestMsg;
import org.thingsboard.server.gen.edge.v1.ConnectResponseCode;
import org.thingsboard.server.gen.edge.v1.ConnectResponseMsg;
@ -452,14 +454,15 @@ public abstract class EdgeGrpcSession implements Closeable {
List<DownlinkMsg> copy = new ArrayList<>(sessionState.getPendingMsgsMap().values());
if (attempt > 1) {
String error = "Failed to deliver the batch";
String failureMsg = String.format("{%s}: {%s}", error, copy);
String failureMsg = String.format("{%s} (size: {%s})", error, copy.size());
if (attempt == 2) {
// Send a failure notification only on the second attempt.
// This ensures that failure alerts are sent just once to avoid redundant notifications.
ctx.getRuleProcessor().process(EdgeCommunicationFailureTrigger.builder().tenantId(tenantId)
.edgeId(edge.getId()).customerId(edge.getCustomerId()).edgeName(edge.getName()).failureMsg(failureMsg).error(error).build());
}
log.warn("[{}][{}] {}, attempt: {}", tenantId, edge.getId(), failureMsg, attempt);
log.warn("[{}][{}] {} on attempt {}", tenantId, edge.getId(), failureMsg, attempt);
log.debug("[{}][{}] entities in failed batch: {}", tenantId, edge.getId(), copy);
}
log.trace("[{}][{}][{}] downlink msg(s) are going to be send.", tenantId, edge.getId(), copy.size());
for (DownlinkMsg downlinkMsg : copy) {
@ -882,6 +885,11 @@ public abstract class EdgeGrpcSession implements Closeable {
result.add(ctx.getEdgeRequestsService().processRelationRequestMsg(edge.getTenantId(), edge, relationRequestMsg));
}
}
if (uplinkMsg.getCalculatedFieldRequestMsgCount() > 0) {
for (CalculatedFieldRequestMsg calculatedFieldRequestMsg : uplinkMsg.getCalculatedFieldRequestMsgList()) {
result.add(ctx.getEdgeRequestsService().processCalculatedFieldRequestMsg(edge.getTenantId(), edge, calculatedFieldRequestMsg));
}
}
if (uplinkMsg.getUserCredentialsRequestMsgCount() > 0) {
for (UserCredentialsRequestMsg userCredentialsRequestMsg : uplinkMsg.getUserCredentialsRequestMsgList()) {
result.add(ctx.getEdgeRequestsService().processUserCredentialsRequestMsg(edge.getTenantId(), edge, userCredentialsRequestMsg));
@ -907,6 +915,11 @@ public abstract class EdgeGrpcSession implements Closeable {
result.add(ctx.getEdgeRequestsService().processEntityViewsRequestMsg(edge.getTenantId(), edge, entityViewRequestMsg));
}
}
if (uplinkMsg.getCalculatedFieldUpdateMsgCount() > 0) {
for (CalculatedFieldUpdateMsg calculatedFieldUpdateMsg : uplinkMsg.getCalculatedFieldUpdateMsgList()) {
result.add(ctx.getCalculatedFieldProcessor().processCalculatedFieldMsgFromEdge(edge.getTenantId(), edge, calculatedFieldUpdateMsg));
}
}
} catch (Exception e) {
String failureMsg = String.format("Can't process uplink msg [%s] from edge", uplinkMsg);
log.trace("[{}][{}] Can't process uplink msg [{}]", tenantId, edge.getId(), uplinkMsg, e);
@ -917,7 +930,9 @@ public abstract class EdgeGrpcSession implements Closeable {
return Futures.allAsList(result);
}
protected void destroy() {}
protected boolean destroy() {
return true;
}
protected void cleanUp() {}

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

@ -107,4 +107,5 @@ public class EdgeSyncCursor {
currentIdx++;
return edgeEventFetcher;
}
}

14
application/src/main/java/org/thingsboard/server/service/edge/rpc/KafkaEdgeGrpcSession.java

@ -135,19 +135,25 @@ public class KafkaEdgeGrpcSession extends EdgeGrpcSession {
}
@Override
public void destroy() {
public boolean destroy() {
try {
if (consumer != null) {
consumer.stop();
}
} finally {
consumer = null;
} catch (Exception e) {
log.warn("[{}][{}] Failed to stop edge event consumer", tenantId, edge.getId(), e);
return false;
}
consumer = null;
try {
if (consumerExecutor != null) {
consumerExecutor.shutdown();
}
} catch (Exception ignored) {}
} catch (Exception e) {
log.warn("[{}][{}] Failed to shutdown consumer executor", tenantId, edge.getId(), e);
return false;
}
return true;
}
@Override

12
application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessor.java

@ -139,8 +139,8 @@ public abstract class BaseEdgeProcessor implements EdgeProcessor {
UPDATED_COMMENT, DELETED -> true;
default -> switch (type) {
case ALARM, ALARM_COMMENT, RULE_CHAIN, RULE_CHAIN_METADATA, USER, CUSTOMER, TENANT, TENANT_PROFILE,
WIDGETS_BUNDLE, WIDGET_TYPE, ADMIN_SETTINGS, OTA_PACKAGE, QUEUE, RELATION, NOTIFICATION_TEMPLATE, NOTIFICATION_TARGET,
NOTIFICATION_RULE -> true;
WIDGETS_BUNDLE, WIDGET_TYPE, ADMIN_SETTINGS, OTA_PACKAGE, QUEUE, RELATION, CALCULATED_FIELD, NOTIFICATION_TEMPLATE,
NOTIFICATION_TARGET, NOTIFICATION_RULE -> true;
default -> false;
};
};
@ -222,7 +222,7 @@ public abstract class BaseEdgeProcessor implements EdgeProcessor {
if (edgeId != null && !edgeId.equals(originatorEdgeId)) {
return saveEdgeEvent(tenantId, edgeId, type, actionType, entityId, body);
} else {
return processNotificationToRelatedEdges(tenantId, entityId, type, actionType, originatorEdgeId);
return processNotificationToRelatedEdges(tenantId, entityId, entityId, type, actionType, originatorEdgeId);
}
case DELETED:
EdgeEventActionType deleted = EdgeEventActionType.DELETED;
@ -260,11 +260,11 @@ public abstract class BaseEdgeProcessor implements EdgeProcessor {
}
}
private ListenableFuture<Void> processNotificationToRelatedEdges(TenantId tenantId, EntityId entityId, EdgeEventType type,
EdgeEventActionType actionType, EdgeId sourceEdgeId) {
protected ListenableFuture<Void> processNotificationToRelatedEdges(TenantId tenantId, EntityId ownerEntityId, EntityId entityId, EdgeEventType type,
EdgeEventActionType actionType, EdgeId sourceEdgeId) {
List<ListenableFuture<Void>> futures = new ArrayList<>();
PageDataIterableByTenantIdEntityId<EdgeId> edgeIds =
new PageDataIterableByTenantIdEntityId<>(edgeCtx.getEdgeService()::findRelatedEdgeIdsByEntityId, tenantId, entityId, RELATED_EDGES_CACHE_ITEMS);
new PageDataIterableByTenantIdEntityId<>(edgeCtx.getEdgeService()::findRelatedEdgeIdsByEntityId, tenantId, ownerEntityId, RELATED_EDGES_CACHE_ITEMS);
for (EdgeId relatedEdgeId : edgeIds) {
if (!relatedEdgeId.equals(sourceEdgeId)) {
futures.add(saveEdgeEvent(tenantId, relatedEdgeId, type, actionType, entityId, null));

1
application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/AssetEdgeProcessor.java

@ -119,6 +119,7 @@ public class AssetEdgeProcessor extends BaseAssetProcessor implements AssetProce
DownlinkMsg.Builder builder = DownlinkMsg.newBuilder()
.setDownlinkMsgId(EdgeUtils.nextPositiveInt())
.addAssetUpdateMsg(assetUpdateMsg);
if (UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE.equals(msgType)) {
AssetProfile assetProfile = edgeCtx.getAssetProfileService().findAssetProfileById(edgeEvent.getTenantId(), asset.getAssetProfileId());
builder.addAssetProfileUpdateMsg(EdgeMsgConstructorUtils.constructAssetProfileUpdatedMsg(msgType, assetProfile));

79
application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/cf/BaseCalculatedFieldProcessor.java

@ -0,0 +1,79 @@
/**
* Copyright © 2016-2025 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.edge.rpc.processor.cf;
import com.datastax.oss.driver.api.core.uuid.Uuids;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.util.Pair;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.cf.CalculatedField;
import org.thingsboard.server.common.data.id.CalculatedFieldId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.dao.service.DataValidator;
import org.thingsboard.server.gen.edge.v1.CalculatedFieldUpdateMsg;
import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor;
@Slf4j
public abstract class BaseCalculatedFieldProcessor extends BaseEdgeProcessor {
@Autowired
private DataValidator<CalculatedField> calculatedFieldValidator;
protected Pair<Boolean, Boolean> saveOrUpdateCalculatedField(TenantId tenantId, CalculatedFieldId calculatedFieldId, CalculatedFieldUpdateMsg calculatedFieldUpdateMsg) {
boolean isCreated = false;
boolean isNameUpdated = false;
try {
CalculatedField calculatedField = JacksonUtil.fromString(calculatedFieldUpdateMsg.getEntity(), CalculatedField.class, true);
if (calculatedField == null) {
throw new RuntimeException("[{" + tenantId + "}] calculatedFieldUpdateMsg {" + calculatedFieldUpdateMsg + " } cannot be converted to calculatedField");
}
CalculatedField calculatedFieldById = edgeCtx.getCalculatedFieldService().findById(tenantId, calculatedFieldId);
if (calculatedFieldById == null) {
calculatedField.setCreatedTime(Uuids.unixTimestamp(calculatedFieldId.getId()));
isCreated = true;
calculatedField.setId(null);
} else {
calculatedField.setId(calculatedFieldId);
}
String calculatedFieldName = calculatedField.getName();
CalculatedField calculatedFieldByName = edgeCtx.getCalculatedFieldService().findByEntityIdAndName(calculatedField.getEntityId(), calculatedFieldName);
if (calculatedFieldByName != null && !calculatedFieldByName.getId().equals(calculatedFieldId)) {
calculatedFieldName = calculatedFieldName + "_" + StringUtils.randomAlphabetic(15);
log.warn("[{}] calculatedField with name {} already exists. Renaming calculatedField name to {}",
tenantId, calculatedField.getName(), calculatedFieldByName.getName());
isNameUpdated = true;
}
calculatedField.setName(calculatedFieldName);
calculatedFieldValidator.validate(calculatedField, CalculatedField::getTenantId);
if (isCreated) {
calculatedField.setId(calculatedFieldId);
}
edgeCtx.getCalculatedFieldService().save(calculatedField, false);
} catch (Exception e) {
log.error("[{}] Failed to process calculatedField update msg [{}]", tenantId, calculatedFieldUpdateMsg, e);
throw e;
}
return Pair.of(isCreated, isNameUpdated);
}
}

169
application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/cf/CalculatedFieldEdgeProcessor.java

@ -0,0 +1,169 @@
/**
* Copyright © 2016-2025 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.edge.rpc.processor.cf;
import com.fasterxml.jackson.databind.JsonNode;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.util.Pair;
import org.springframework.stereotype.Component;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.EdgeUtils;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.cf.CalculatedField;
import org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.common.data.edge.EdgeEvent;
import org.thingsboard.server.common.data.edge.EdgeEventActionType;
import org.thingsboard.server.common.data.edge.EdgeEventType;
import org.thingsboard.server.common.data.id.CalculatedFieldId;
import org.thingsboard.server.common.data.id.EdgeId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.EntityIdFactory;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.msg.TbMsgType;
import org.thingsboard.server.common.msg.TbMsgMetaData;
import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.gen.edge.v1.CalculatedFieldUpdateMsg;
import org.thingsboard.server.gen.edge.v1.DownlinkMsg;
import org.thingsboard.server.gen.edge.v1.EdgeVersion;
import org.thingsboard.server.gen.edge.v1.UpdateMsgType;
import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.edge.EdgeMsgConstructorUtils;
import java.util.UUID;
@Slf4j
@Component
@TbCoreComponent
public class CalculatedFieldEdgeProcessor extends BaseCalculatedFieldProcessor implements CalculatedFieldProcessor {
@Override
public ListenableFuture<Void> processCalculatedFieldMsgFromEdge(TenantId tenantId, Edge edge, CalculatedFieldUpdateMsg calculatedFieldUpdateMsg) {
CalculatedFieldId calculatedFieldId = new CalculatedFieldId(new UUID(calculatedFieldUpdateMsg.getIdMSB(), calculatedFieldUpdateMsg.getIdLSB()));
try {
edgeSynchronizationManager.getEdgeId().set(edge.getId());
switch (calculatedFieldUpdateMsg.getMsgType()) {
case ENTITY_CREATED_RPC_MESSAGE:
case ENTITY_UPDATED_RPC_MESSAGE:
processCalculatedField(tenantId, calculatedFieldId, calculatedFieldUpdateMsg, edge);
return Futures.immediateFuture(null);
case ENTITY_DELETED_RPC_MESSAGE:
CalculatedField calculatedField = edgeCtx.getCalculatedFieldService().findById(tenantId, calculatedFieldId);
if (calculatedField != null) {
edgeCtx.getCalculatedFieldService().deleteCalculatedField(tenantId, calculatedFieldId);
}
return Futures.immediateFuture(null);
case UNRECOGNIZED:
default:
return handleUnsupportedMsgType(calculatedFieldUpdateMsg.getMsgType());
}
} catch (DataValidationException e) {
if (e.getMessage().contains("limit reached")) {
log.warn("[{}] Number of allowed calculatedField violated {}", tenantId, calculatedFieldUpdateMsg, e);
return Futures.immediateFuture(null);
} else {
return Futures.immediateFailedFuture(e);
}
} finally {
edgeSynchronizationManager.getEdgeId().remove();
}
}
@Override
public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) {
CalculatedFieldId calculatedFieldId = new CalculatedFieldId(edgeEvent.getEntityId());
switch (edgeEvent.getAction()) {
case ADDED, UPDATED -> {
CalculatedField calculatedField = edgeCtx.getCalculatedFieldService().findById(edgeEvent.getTenantId(), calculatedFieldId);
if (calculatedField != null) {
UpdateMsgType msgType = getUpdateMsgType(edgeEvent.getAction());
CalculatedFieldUpdateMsg calculatedFieldUpdateMsg = EdgeMsgConstructorUtils.constructCalculatedFieldUpdatedMsg(msgType, calculatedField);
return DownlinkMsg.newBuilder()
.setDownlinkMsgId(EdgeUtils.nextPositiveInt())
.addCalculatedFieldUpdateMsg(calculatedFieldUpdateMsg)
.build();
}
}
case DELETED -> {
CalculatedFieldUpdateMsg calculatedFieldUpdateMsg = EdgeMsgConstructorUtils.constructCalculatedFieldDeleteMsg(calculatedFieldId);
return DownlinkMsg.newBuilder()
.setDownlinkMsgId(EdgeUtils.nextPositiveInt())
.addCalculatedFieldUpdateMsg(calculatedFieldUpdateMsg)
.build();
}
}
return null;
}
@Override
public EdgeEventType getEdgeEventType() {
return EdgeEventType.CALCULATED_FIELD;
}
@Override
public ListenableFuture<Void> processEntityNotification(TenantId tenantId, TransportProtos.EdgeNotificationMsgProto edgeNotificationMsg) {
EdgeEventType type = EdgeEventType.valueOf(edgeNotificationMsg.getType());
EdgeEventActionType actionType = EdgeEventActionType.valueOf(edgeNotificationMsg.getAction());
EntityId entityId = EntityIdFactory.getByEdgeEventTypeAndUuid(type, new UUID(edgeNotificationMsg.getEntityIdMSB(), edgeNotificationMsg.getEntityIdLSB()));
EdgeId originatorEdgeId = safeGetEdgeId(edgeNotificationMsg.getOriginatorEdgeIdMSB(), edgeNotificationMsg.getOriginatorEdgeIdLSB());
switch (actionType) {
case UPDATED:
case ADDED:
EntityId calculatedFieldOwnerId = JacksonUtil.fromString(edgeNotificationMsg.getBody(), EntityId.class);
if (calculatedFieldOwnerId != null &&
(EntityType.DEVICE.equals(calculatedFieldOwnerId.getEntityType()) || EntityType.ASSET.equals(calculatedFieldOwnerId.getEntityType()))) {
JsonNode body = JacksonUtil.toJsonNode(edgeNotificationMsg.getBody());
EdgeId edgeId = safeGetEdgeId(edgeNotificationMsg.getEdgeIdMSB(), edgeNotificationMsg.getEdgeIdLSB());
return edgeId != null ?
saveEdgeEvent(tenantId, edgeId, type, actionType, entityId, body) :
processNotificationToRelatedEdges(tenantId, calculatedFieldOwnerId, entityId, type, actionType, originatorEdgeId);
} else {
return processActionForAllEdges(tenantId, type, actionType, entityId, null, originatorEdgeId);
}
default:
return super.processEntityNotification(tenantId, edgeNotificationMsg);
}
}
private void processCalculatedField(TenantId tenantId, CalculatedFieldId calculatedFieldId, CalculatedFieldUpdateMsg calculatedFieldUpdateMsg, Edge edge) {
Pair<Boolean, Boolean> resultPair = super.saveOrUpdateCalculatedField(tenantId, calculatedFieldId, calculatedFieldUpdateMsg);
Boolean wasCreated = resultPair.getFirst();
if (wasCreated) {
pushCalculatedFieldCreatedEventToRuleEngine(tenantId, edge, calculatedFieldId);
}
Boolean nameWasUpdated = resultPair.getSecond();
if (nameWasUpdated) {
saveEdgeEvent(tenantId, edge.getId(), EdgeEventType.CALCULATED_FIELD, EdgeEventActionType.UPDATED, calculatedFieldId, null);
}
}
private void pushCalculatedFieldCreatedEventToRuleEngine(TenantId tenantId, Edge edge, CalculatedFieldId calculatedFieldId) {
try {
CalculatedField calculatedField = edgeCtx.getCalculatedFieldService().findById(tenantId, calculatedFieldId);
String calculatedFieldAsString = JacksonUtil.toString(calculatedField);
TbMsgMetaData msgMetaData = getEdgeActionTbMsgMetaData(edge, edge.getCustomerId());
pushEntityEventToRuleEngine(tenantId, calculatedFieldId, edge.getCustomerId(), TbMsgType.ENTITY_CREATED, calculatedFieldAsString, msgMetaData);
} catch (Exception e) {
log.warn("[{}][{}] Failed to push calculatedField action to rule engine: {}", tenantId, calculatedFieldId, TbMsgType.ENTITY_CREATED.name(), e);
}
}
}

28
application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/cf/CalculatedFieldProcessor.java

@ -0,0 +1,28 @@
/**
* Copyright © 2016-2025 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.edge.rpc.processor.cf;
import com.google.common.util.concurrent.ListenableFuture;
import org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.gen.edge.v1.CalculatedFieldUpdateMsg;
import org.thingsboard.server.service.edge.rpc.processor.EdgeProcessor;
public interface CalculatedFieldProcessor extends EdgeProcessor {
ListenableFuture<Void> processCalculatedFieldMsgFromEdge(TenantId tenantId, Edge edge, CalculatedFieldUpdateMsg calculatedFieldUpdateMsg);
}

1
application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/DeviceEdgeProcessor.java

@ -243,6 +243,7 @@ public class DeviceEdgeProcessor extends BaseDeviceProcessor implements DevicePr
DeviceCredentialsUpdateMsg deviceCredentialsUpdateMsg = EdgeMsgConstructorUtils.constructDeviceCredentialsUpdatedMsg(deviceCredentials);
builder.addDeviceCredentialsUpdateMsg(deviceCredentialsUpdateMsg).build();
}
if (UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE.equals(msgType)) {
DeviceProfile deviceProfile = edgeCtx.getDeviceProfileService().findDeviceProfileById(edgeEvent.getTenantId(), device.getDeviceProfileId());
builder.addDeviceProfileUpdateMsg(EdgeMsgConstructorUtils.constructDeviceProfileUpdatedMsg(msgType, deviceProfile));

45
application/src/main/java/org/thingsboard/server/service/edge/rpc/sync/DefaultEdgeRequestsService.java

@ -54,12 +54,14 @@ import org.thingsboard.server.common.data.relation.RelationsSearchParameters;
import org.thingsboard.server.common.data.widget.WidgetType;
import org.thingsboard.server.common.data.widget.WidgetsBundle;
import org.thingsboard.server.dao.attributes.AttributesService;
import org.thingsboard.server.dao.cf.CalculatedFieldService;
import org.thingsboard.server.dao.edge.EdgeEventService;
import org.thingsboard.server.dao.relation.RelationService;
import org.thingsboard.server.dao.timeseries.TimeseriesService;
import org.thingsboard.server.dao.widget.WidgetTypeService;
import org.thingsboard.server.dao.widget.WidgetsBundleService;
import org.thingsboard.server.gen.edge.v1.AttributesRequestMsg;
import org.thingsboard.server.gen.edge.v1.CalculatedFieldRequestMsg;
import org.thingsboard.server.gen.edge.v1.DeviceCredentialsRequestMsg;
import org.thingsboard.server.gen.edge.v1.EntityViewsRequestMsg;
import org.thingsboard.server.gen.edge.v1.RelationRequestMsg;
@ -90,7 +92,7 @@ public class DefaultEdgeRequestsService implements EdgeRequestsService {
@Autowired
private TimeseriesService timeseriesService;
@Autowired
private RelationService relationService;
@ -104,6 +106,9 @@ public class DefaultEdgeRequestsService implements EdgeRequestsService {
@Autowired
private WidgetTypeService widgetTypeService;
@Autowired
private CalculatedFieldService calculatedFieldService;
@Autowired
private DbCallbackExecutorService dbCallbackExecutorService;
@ -293,6 +298,44 @@ public class DefaultEdgeRequestsService implements EdgeRequestsService {
return futureToSet;
}
@Override
public ListenableFuture<Void> processCalculatedFieldRequestMsg(TenantId tenantId, Edge edge, CalculatedFieldRequestMsg calculatedFieldRequestMsg) {
log.trace("[{}] processCalculatedFieldRequestMsg [{}][{}]", tenantId, edge.getName(), calculatedFieldRequestMsg);
EntityId entityId = EntityIdFactory.getByTypeAndUuid(
EntityType.valueOf(calculatedFieldRequestMsg.getEntityType()),
new UUID(calculatedFieldRequestMsg.getEntityIdMSB(), calculatedFieldRequestMsg.getEntityIdLSB()));
log.trace("[{}] processCalculatedField [{}][{}] for entity [{}][{}]", tenantId, edge.getName(), calculatedFieldRequestMsg, entityId.getEntityType(), entityId.getId());
return saveCalculatedFieldsToEdge(tenantId, edge.getId(), entityId);
}
private ListenableFuture<Void> saveCalculatedFieldsToEdge(TenantId tenantId, EdgeId edgeId, EntityId entityId) {
return Futures.transformAsync(
dbCallbackExecutorService.submit(() -> calculatedFieldService.findCalculatedFieldsByEntityId(tenantId, entityId)),
calculatedFields -> {
log.trace("[{}][{}][{}][{}] calculatedField(s) are going to be pushed to edge.", tenantId, edgeId, entityId, calculatedFields.size());
List<ListenableFuture<?>> futures = calculatedFields.stream().map(calculatedField -> {
try {
return saveEdgeEvent(tenantId, edgeId, EdgeEventType.CALCULATED_FIELD,
EdgeEventActionType.ADDED, calculatedField.getId(), JacksonUtil.valueToTree(calculatedField));
} catch (Exception e) {
log.error("[{}][{}] Exception during loading calculatedField [{}] to edge on sync!", tenantId, edgeId, calculatedField, e);
return Futures.immediateFailedFuture(e);
}
}).toList();
return Futures.transform(
Futures.allAsList(futures),
voids -> null,
dbCallbackExecutorService
);
},
dbCallbackExecutorService
);
}
private ListenableFuture<List<EntityRelation>> findRelationByQuery(TenantId tenantId, Edge edge, EntityId entityId, EntitySearchDirection direction) {
EntityRelationsQuery query = new EntityRelationsQuery();
query.setParameters(new RelationsSearchParameters(entityId, direction, 1, false));

4
application/src/main/java/org/thingsboard/server/service/edge/rpc/sync/EdgeRequestsService.java

@ -19,6 +19,7 @@ import com.google.common.util.concurrent.ListenableFuture;
import org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.gen.edge.v1.AttributesRequestMsg;
import org.thingsboard.server.gen.edge.v1.CalculatedFieldRequestMsg;
import org.thingsboard.server.gen.edge.v1.DeviceCredentialsRequestMsg;
import org.thingsboard.server.gen.edge.v1.EntityViewsRequestMsg;
import org.thingsboard.server.gen.edge.v1.RelationRequestMsg;
@ -35,6 +36,8 @@ public interface EdgeRequestsService {
ListenableFuture<Void> processRelationRequestMsg(TenantId tenantId, Edge edge, RelationRequestMsg relationRequestMsg);
ListenableFuture<Void> processCalculatedFieldRequestMsg(TenantId tenantId, Edge edge, CalculatedFieldRequestMsg calculatedFieldRequestMsg);
@Deprecated(since = "3.9.1", forRemoval = true)
ListenableFuture<Void> processDeviceCredentialsRequestMsg(TenantId tenantId, Edge edge, DeviceCredentialsRequestMsg deviceCredentialsRequestMsg);
@ -46,4 +49,5 @@ public interface EdgeRequestsService {
@Deprecated(since = "3.9.1", forRemoval = true)
ListenableFuture<Void> processEntityViewsRequestMsg(TenantId tenantId, Edge edge, EntityViewsRequestMsg entityViewsRequestMsg);
}

58
application/src/test/java/org/thingsboard/server/controller/TbResourceControllerTest.java

@ -202,6 +202,60 @@ public class TbResourceControllerTest extends AbstractControllerTest {
Assert.assertEquals(savedResource.getFileName(), foundResource.getFileName());
}
@Test
public void testFindSystemResourceInfoById() throws Exception {
loginSysAdmin();
TbResource resource = new TbResource();
resource.setResourceType(ResourceType.JS_MODULE);
resource.setTitle("My system resource");
resource.setFileName(DEFAULT_FILE_NAME);
resource.setEncodedData(TEST_DATA);
TbResourceInfo savedResourceInfo = save(resource);
assertThat(savedResourceInfo.getFileName()).isEqualTo(DEFAULT_FILE_NAME);
TbResourceInfo resourceInfo = findResourceInfo(savedResourceInfo.getId());
assertThat(resourceInfo).isEqualTo(savedResourceInfo);
loginTenantAdmin();
resourceInfo = findResourceInfo(savedResourceInfo.getId());
assertThat(resourceInfo).isEqualTo(savedResourceInfo);
loginSysAdmin();
resource = new TbResource(savedResourceInfo);
resource.setFileName(DEFAULT_FILE_NAME_2);
resource.setEncodedData(TEST_DATA);
savedResourceInfo = save(resource);
assertThat(savedResourceInfo.getFileName()).isEqualTo(DEFAULT_FILE_NAME_2);
resourceInfo = findResourceInfo(savedResourceInfo.getId());
assertThat(resourceInfo).isEqualTo(savedResourceInfo);
loginTenantAdmin();
resourceInfo = findResourceInfo(savedResourceInfo.getId());
assertThat(resourceInfo).isEqualTo(savedResourceInfo);
}
@Test
public void testFindTenantResourceInfoById() throws Exception {
TbResource resource = new TbResource();
resource.setResourceType(ResourceType.JS_MODULE);
resource.setTitle("My tenant resource");
resource.setFileName(DEFAULT_FILE_NAME);
resource.setEncodedData(TEST_DATA);
TbResourceInfo savedResourceInfo = save(resource);
assertThat(savedResourceInfo.getFileName()).isEqualTo(DEFAULT_FILE_NAME);
TbResourceInfo resourceInfo = findResourceInfo(savedResourceInfo.getId());
assertThat(resourceInfo).isEqualTo(savedResourceInfo);
resource = new TbResource(savedResourceInfo);
resource.setFileName(DEFAULT_FILE_NAME_2);
resource.setEncodedData(TEST_DATA);
savedResourceInfo = save(resource);
assertThat(savedResourceInfo.getFileName()).isEqualTo(DEFAULT_FILE_NAME_2);
resourceInfo = findResourceInfo(savedResourceInfo.getId());
assertThat(resourceInfo).isEqualTo(savedResourceInfo);
}
@Test
public void testDeleteTbResource() throws Exception {
TbResource resource = new TbResource();
@ -878,6 +932,10 @@ public class TbResourceControllerTest extends AbstractControllerTest {
});
}
private TbResourceInfo findResourceInfo(TbResourceId id) throws Exception {
return doGet("/api/resource/info/" + id, TbResourceInfo.class);
}
private byte[] download(TbResourceId resourceId) throws Exception {
return doGet("/api/resource/" + resourceId + "/download")
.andExpect(status().isOk())

13
application/src/test/java/org/thingsboard/server/edge/AbstractEdgeTest.java

@ -75,6 +75,7 @@ import org.thingsboard.server.common.data.queue.Queue;
import org.thingsboard.server.common.data.rule.RuleChain;
import org.thingsboard.server.common.data.rule.RuleChainMetaData;
import org.thingsboard.server.common.data.rule.RuleChainType;
import org.thingsboard.server.common.data.security.DeviceCredentials;
import org.thingsboard.server.common.data.security.model.JwtSettings;
import org.thingsboard.server.controller.AbstractControllerTest;
import org.thingsboard.server.dao.edge.EdgeEventService;
@ -565,7 +566,8 @@ abstract public class AbstractEdgeTest extends AbstractControllerTest {
protected Device saveDeviceOnCloudAndVerifyDeliveryToEdge() throws Exception {
// create device and assign to edge
Device savedDevice = saveDevice(StringUtils.randomAlphanumeric(15), thermostatDeviceProfile.getName());
edgeImitator.expectMessageAmount(2); // device and device profile messages
DeviceCredentials deviceCredentials = doGet("/api/device/" + savedDevice.getId().getId() + "/credentials", DeviceCredentials.class);
edgeImitator.expectMessageAmount(3); // device and device profile messages and device credentials
doPost("/api/edge/" + edge.getUuidId()
+ "/device/" + savedDevice.getUuidId(), Device.class);
Assert.assertTrue(edgeImitator.waitForMessages());
@ -582,6 +584,15 @@ abstract public class AbstractEdgeTest extends AbstractControllerTest {
Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, deviceProfileUpdateMsg.getMsgType());
Assert.assertEquals(thermostatDeviceProfile.getUuidId().getMostSignificantBits(), deviceProfileUpdateMsg.getIdMSB());
Assert.assertEquals(thermostatDeviceProfile.getUuidId().getLeastSignificantBits(), deviceProfileUpdateMsg.getIdLSB());
Optional<DeviceCredentialsUpdateMsg> deviceCredentialsUpdateMsgOpt = edgeImitator.findMessageByType(DeviceCredentialsUpdateMsg.class);
Assert.assertTrue(deviceCredentialsUpdateMsgOpt.isPresent());
DeviceCredentialsUpdateMsg deviceCredentialsUpdateMsg = deviceCredentialsUpdateMsgOpt.get();
DeviceCredentials deviceCredentialsMsg = JacksonUtil.fromString(deviceCredentialsUpdateMsg.getEntity(), DeviceCredentials.class, true);
Assert.assertNotNull(deviceCredentialsMsg);
Assert.assertEquals(savedDevice.getId(), deviceCredentialsMsg.getDeviceId());
Assert.assertEquals(deviceCredentials, deviceCredentialsMsg);
return savedDevice;
}

267
application/src/test/java/org/thingsboard/server/edge/CalculatedFieldEdgeTest.java

@ -0,0 +1,267 @@
/**
* Copyright © 2016-2025 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.edge;
import com.datastax.oss.driver.api.core.uuid.Uuids;
import com.google.protobuf.AbstractMessage;
import com.google.protobuf.InvalidProtocolBufferException;
import org.junit.Assert;
import org.junit.Test;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.cf.CalculatedField;
import org.thingsboard.server.common.data.cf.CalculatedFieldType;
import org.thingsboard.server.common.data.cf.configuration.Argument;
import org.thingsboard.server.common.data.cf.configuration.ArgumentType;
import org.thingsboard.server.common.data.cf.configuration.Output;
import org.thingsboard.server.common.data.cf.configuration.OutputType;
import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey;
import org.thingsboard.server.common.data.cf.configuration.SimpleCalculatedFieldConfiguration;
import org.thingsboard.server.common.data.debug.DebugSettings;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.dao.service.DaoSqlTest;
import org.thingsboard.server.gen.edge.v1.CalculatedFieldRequestMsg;
import org.thingsboard.server.gen.edge.v1.CalculatedFieldUpdateMsg;
import org.thingsboard.server.gen.edge.v1.UpdateMsgType;
import org.thingsboard.server.gen.edge.v1.UplinkMsg;
import org.thingsboard.server.gen.edge.v1.UplinkResponseMsg;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@DaoSqlTest
public class CalculatedFieldEdgeTest extends AbstractEdgeTest {
private static final String DEFAULT_CF_NAME = "Edge Test CalculatedField";
private static final String UPDATED_CF_NAME = "Updated Edge Test CalculatedField";
@Test
public void testCalculatedField_create_update_delete() throws Exception {
Device savedDevice = saveDeviceOnCloudAndVerifyDeliveryToEdge();
// create calculatedField
SimpleCalculatedFieldConfiguration config = new SimpleCalculatedFieldConfiguration();
CalculatedField calculatedField = createSimpleCalculatedField(savedDevice.getId(), config);
edgeImitator.expectMessageAmount(1);
CalculatedField savedCalculatedField = doPost("/api/calculatedField", calculatedField, CalculatedField.class);
Assert.assertTrue(edgeImitator.waitForMessages());
AbstractMessage latestMessage = edgeImitator.getLatestMessage();
Assert.assertTrue(latestMessage instanceof CalculatedFieldUpdateMsg);
CalculatedFieldUpdateMsg calculatedFieldUpdateMsg = (CalculatedFieldUpdateMsg) latestMessage;
Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, calculatedFieldUpdateMsg.getMsgType());
Assert.assertEquals(savedCalculatedField.getUuidId().getMostSignificantBits(), calculatedFieldUpdateMsg.getIdMSB());
Assert.assertEquals(savedCalculatedField.getUuidId().getLeastSignificantBits(), calculatedFieldUpdateMsg.getIdLSB());
CalculatedField calculatedFieldFromMsg = JacksonUtil.fromString(calculatedFieldUpdateMsg.getEntity(), CalculatedField.class, true);
Assert.assertNotNull(calculatedFieldFromMsg);
Assert.assertEquals(DEFAULT_CF_NAME, calculatedFieldFromMsg.getName());
Assert.assertEquals(savedDevice.getId(), calculatedFieldFromMsg.getEntityId());
Assert.assertEquals(config, calculatedFieldFromMsg.getConfiguration());
edgeImitator.expectMessageAmount(1);
savedCalculatedField.setName(UPDATED_CF_NAME);
savedCalculatedField = doPost("/api/calculatedField", savedCalculatedField, CalculatedField.class);
Assert.assertTrue(edgeImitator.waitForMessages());
latestMessage = edgeImitator.getLatestMessage();
Assert.assertTrue(latestMessage instanceof CalculatedFieldUpdateMsg);
calculatedFieldUpdateMsg = (CalculatedFieldUpdateMsg) latestMessage;
calculatedFieldFromMsg = JacksonUtil.fromString(calculatedFieldUpdateMsg.getEntity(), CalculatedField.class, true);
Assert.assertNotNull(calculatedFieldFromMsg);
Assert.assertEquals(UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE, calculatedFieldUpdateMsg.getMsgType());
Assert.assertEquals(UPDATED_CF_NAME, calculatedFieldFromMsg.getName());
// delete calculatedField
edgeImitator.expectMessageAmount(1);
doDelete("/api/calculatedField/" + savedCalculatedField.getUuidId())
.andExpect(status().isOk());
Assert.assertTrue(edgeImitator.waitForMessages());
latestMessage = edgeImitator.getLatestMessage();
Assert.assertTrue(latestMessage instanceof CalculatedFieldUpdateMsg);
calculatedFieldUpdateMsg = (CalculatedFieldUpdateMsg) latestMessage;
Assert.assertEquals(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE, calculatedFieldUpdateMsg.getMsgType());
Assert.assertEquals(savedCalculatedField.getUuidId().getMostSignificantBits(), calculatedFieldUpdateMsg.getIdMSB());
Assert.assertEquals(savedCalculatedField.getUuidId().getLeastSignificantBits(), calculatedFieldUpdateMsg.getIdLSB());
}
@Test
public void testSendCalculatedFieldToCloud() throws Exception {
Device savedDevice = saveDeviceOnCloudAndVerifyDeliveryToEdge();
// create calculatedField
SimpleCalculatedFieldConfiguration config = new SimpleCalculatedFieldConfiguration();
CalculatedField calculatedField = createSimpleCalculatedField(savedDevice.getId(), config);
UUID uuid = Uuids.timeBased();
UplinkMsg uplinkMsg = getUplinkMsg(uuid, calculatedField, UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE);
checkCalculatedFieldOnCloud(uplinkMsg, uuid, calculatedField.getName());
}
@Test
public void testSendCalculatedFieldRequestToCloud() throws Exception {
Device savedDevice = saveDeviceOnCloudAndVerifyDeliveryToEdge();
// create calculatedField
SimpleCalculatedFieldConfiguration config = new SimpleCalculatedFieldConfiguration();
CalculatedField calculatedField = createSimpleCalculatedField(savedDevice.getId(), config);
edgeImitator.expectMessageAmount(1);
CalculatedField savedCalculatedField = doPost("/api/calculatedField", calculatedField, CalculatedField.class);
Assert.assertTrue(edgeImitator.waitForMessages());
UplinkMsg.Builder uplinkMsgBuilder = UplinkMsg.newBuilder();
CalculatedFieldRequestMsg.Builder calculatedFieldRequestMsgBuilder = CalculatedFieldRequestMsg.newBuilder();
calculatedFieldRequestMsgBuilder.setEntityIdMSB(savedDevice.getId().getId().getMostSignificantBits());
calculatedFieldRequestMsgBuilder.setEntityIdLSB(savedDevice.getId().getId().getLeastSignificantBits());
calculatedFieldRequestMsgBuilder.setEntityType(savedDevice.getId().getEntityType().name());
testAutoGeneratedCodeByProtobuf(calculatedFieldRequestMsgBuilder);
uplinkMsgBuilder.addCalculatedFieldRequestMsg(calculatedFieldRequestMsgBuilder.build());
testAutoGeneratedCodeByProtobuf(uplinkMsgBuilder);
edgeImitator.expectResponsesAmount(1);
edgeImitator.expectMessageAmount(1);
edgeImitator.sendUplinkMsg(uplinkMsgBuilder.build());
Assert.assertTrue(edgeImitator.waitForResponses());
Assert.assertTrue(edgeImitator.waitForMessages());
AbstractMessage latestMessage = edgeImitator.getLatestMessage();
Assert.assertTrue(latestMessage instanceof CalculatedFieldUpdateMsg);
CalculatedFieldUpdateMsg calculatedFieldUpdateMsg = (CalculatedFieldUpdateMsg) latestMessage;
CalculatedField calculatedFieldFromEdge = JacksonUtil.fromString(calculatedFieldUpdateMsg.getEntity(), CalculatedField.class, true);
Assert.assertNotNull(calculatedFieldFromEdge);
Assert.assertEquals(savedCalculatedField, calculatedFieldFromEdge);
Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, calculatedFieldUpdateMsg.getMsgType());
}
@Test
public void testUpdateCalculatedFieldNameOnCloud() throws Exception {
Device savedDevice = saveDeviceOnCloudAndVerifyDeliveryToEdge();
// create calculatedField
SimpleCalculatedFieldConfiguration config = new SimpleCalculatedFieldConfiguration();
CalculatedField calculatedField = createSimpleCalculatedField(savedDevice.getId(), config);
UUID uuid = Uuids.timeBased();
UplinkMsg uplinkMsg = getUplinkMsg(uuid, calculatedField, UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE);
checkCalculatedFieldOnCloud(uplinkMsg, uuid, calculatedField.getName());
calculatedField.setName(UPDATED_CF_NAME);
UplinkMsg updatedUplinkMsg = getUplinkMsg(uuid, calculatedField, UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE);
checkCalculatedFieldOnCloud(updatedUplinkMsg, uuid, calculatedField.getName());
}
@Test
public void testCalculatedFieldToCloudWithNameThatAlreadyExistsOnCloud() throws Exception {
Device savedDevice = saveDeviceOnCloudAndVerifyDeliveryToEdge();
// create calculatedField
SimpleCalculatedFieldConfiguration config = new SimpleCalculatedFieldConfiguration();
CalculatedField calculatedField = createSimpleCalculatedField(savedDevice.getId(), config);
edgeImitator.expectMessageAmount(1);
CalculatedField savedCalculatedField = doPost("/api/calculatedField", calculatedField, CalculatedField.class);
Assert.assertTrue(edgeImitator.waitForMessages());
UUID uuid = Uuids.timeBased();
UplinkMsg uplinkMsg = getUplinkMsg(uuid, calculatedField, UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE);
edgeImitator.expectResponsesAmount(1);
edgeImitator.expectMessageAmount(1);
edgeImitator.sendUplinkMsg(uplinkMsg);
Assert.assertTrue(edgeImitator.waitForResponses());
Assert.assertTrue(edgeImitator.waitForMessages());
Optional<CalculatedFieldUpdateMsg> calculatedFieldUpdateMsgOpt = edgeImitator.findMessageByType(CalculatedFieldUpdateMsg.class);
Assert.assertTrue(calculatedFieldUpdateMsgOpt.isPresent());
CalculatedFieldUpdateMsg latestCalculatedFieldUpdateMsg = calculatedFieldUpdateMsgOpt.get();
CalculatedField calculatedFieldFromMsg = JacksonUtil.fromString(latestCalculatedFieldUpdateMsg.getEntity(), CalculatedField.class, true);
Assert.assertNotNull(calculatedFieldFromMsg);
Assert.assertNotEquals(DEFAULT_CF_NAME, calculatedFieldFromMsg.getName());
Assert.assertNotEquals(savedCalculatedField.getUuidId(), uuid);
CalculatedField calculatedFieldFromCloud = doGet("/api/calculatedField/" + uuid, CalculatedField.class);
Assert.assertNotNull(calculatedFieldFromCloud);
Assert.assertNotEquals(DEFAULT_CF_NAME, calculatedFieldFromCloud.getName());
}
private CalculatedField createSimpleCalculatedField(EntityId entityId, SimpleCalculatedFieldConfiguration config) {
CalculatedField calculatedField = new CalculatedField();
calculatedField.setEntityId(entityId);
calculatedField.setTenantId(tenantId);
calculatedField.setType(CalculatedFieldType.SIMPLE);
calculatedField.setName(DEFAULT_CF_NAME);
calculatedField.setDebugSettings(DebugSettings.all());
Argument argument = new Argument();
ReferencedEntityKey refEntityKey = new ReferencedEntityKey("temperature", ArgumentType.TS_LATEST, null);
argument.setRefEntityKey(refEntityKey);
argument.setDefaultValue("12"); // not used because real telemetry value in db is present
config.setArguments(Map.of("T", argument));
config.setExpression("(T * 9/5) + 32");
Output output = new Output();
output.setName("fahrenheitTemp");
output.setType(OutputType.TIME_SERIES);
output.setDecimalsByDefault(2);
config.setOutput(output);
calculatedField.setConfiguration(config);
return calculatedField;
}
private UplinkMsg getUplinkMsg(UUID uuid, CalculatedField calculatedField, UpdateMsgType updateMsgType) throws InvalidProtocolBufferException {
UplinkMsg.Builder uplinkMsgBuilder = UplinkMsg.newBuilder();
CalculatedFieldUpdateMsg.Builder calculatedFieldUpdateMsgBuilder = CalculatedFieldUpdateMsg.newBuilder();
calculatedFieldUpdateMsgBuilder.setIdMSB(uuid.getMostSignificantBits());
calculatedFieldUpdateMsgBuilder.setIdLSB(uuid.getLeastSignificantBits());
calculatedFieldUpdateMsgBuilder.setEntity(JacksonUtil.toString(calculatedField));
calculatedFieldUpdateMsgBuilder.setMsgType(updateMsgType);
testAutoGeneratedCodeByProtobuf(calculatedFieldUpdateMsgBuilder);
uplinkMsgBuilder.addCalculatedFieldUpdateMsg(calculatedFieldUpdateMsgBuilder.build());
testAutoGeneratedCodeByProtobuf(uplinkMsgBuilder);
return uplinkMsgBuilder.build();
}
private void checkCalculatedFieldOnCloud(UplinkMsg uplinkMsg, UUID uuid, String resourceTitle) throws Exception {
edgeImitator.expectResponsesAmount(1);
edgeImitator.sendUplinkMsg(uplinkMsg);
Assert.assertTrue(edgeImitator.waitForResponses());
UplinkResponseMsg latestResponseMsg = edgeImitator.getLatestResponseMsg();
Assert.assertTrue(latestResponseMsg.getSuccess());
CalculatedField calculatedField = doGet("/api/calculatedField/" + uuid, CalculatedField.class);
Assert.assertNotNull(calculatedField);
Assert.assertEquals(resourceTitle, calculatedField.getName());
}
}

6
application/src/test/java/org/thingsboard/server/edge/imitator/EdgeImitator.java

@ -33,6 +33,7 @@ import org.thingsboard.server.gen.edge.v1.AlarmCommentUpdateMsg;
import org.thingsboard.server.gen.edge.v1.AlarmUpdateMsg;
import org.thingsboard.server.gen.edge.v1.AssetProfileUpdateMsg;
import org.thingsboard.server.gen.edge.v1.AssetUpdateMsg;
import org.thingsboard.server.gen.edge.v1.CalculatedFieldUpdateMsg;
import org.thingsboard.server.gen.edge.v1.CustomerUpdateMsg;
import org.thingsboard.server.gen.edge.v1.DashboardUpdateMsg;
import org.thingsboard.server.gen.edge.v1.DeviceCredentialsRequestMsg;
@ -352,6 +353,11 @@ public class EdgeImitator {
result.add(saveDownlinkMsg(notificationTargetUpdateMsg));
}
}
if (downlinkMsg.getCalculatedFieldUpdateMsgCount() > 0) {
for (CalculatedFieldUpdateMsg calculatedFieldUpdateMsg : downlinkMsg.getCalculatedFieldUpdateMsgList()) {
result.add(saveDownlinkMsg(calculatedFieldUpdateMsg));
}
}
if (downlinkMsg.hasEdgeConfiguration()) {
result.add(saveDownlinkMsg(downlinkMsg.getEdgeConfiguration()));
}

4
common/cache/src/main/java/org/thingsboard/server/cache/resourceInfo/ResourceInfoCacheKey.java

@ -20,7 +20,6 @@ import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.thingsboard.server.common.data.id.TbResourceId;
import org.thingsboard.server.common.data.id.TenantId;
import java.io.Serial;
import java.io.Serializable;
@ -34,12 +33,11 @@ public class ResourceInfoCacheKey implements Serializable {
@Serial
private static final long serialVersionUID = 2100510964692846992L;
private final TenantId tenantId;
private final TbResourceId tbResourceId;
@Override
public String toString() {
return tenantId + "_" + tbResourceId;
return tbResourceId.toString();
}
}

4
common/dao-api/src/main/java/org/thingsboard/server/dao/cf/CalculatedFieldService.java

@ -31,8 +31,12 @@ public interface CalculatedFieldService extends EntityDaoService {
CalculatedField save(CalculatedField calculatedField);
CalculatedField save(CalculatedField calculatedField, boolean doValidate);
CalculatedField findById(TenantId tenantId, CalculatedFieldId calculatedFieldId);
CalculatedField findByEntityIdAndName(EntityId entityId, String name);
List<CalculatedFieldId> findCalculatedFieldIdsByEntityId(TenantId tenantId, EntityId entityId);
List<CalculatedField> findCalculatedFieldsByEntityId(TenantId tenantId, EntityId entityId);

9
common/data/src/main/java/org/thingsboard/server/common/data/edge/EdgeEventType.java

@ -41,12 +41,13 @@ public enum EdgeEventType {
ADMIN_SETTINGS(true, null),
OTA_PACKAGE(true, EntityType.OTA_PACKAGE),
QUEUE(true, EntityType.QUEUE),
NOTIFICATION_RULE (true, EntityType.NOTIFICATION_RULE),
NOTIFICATION_TARGET (true, EntityType.NOTIFICATION_TARGET),
NOTIFICATION_TEMPLATE (true, EntityType.NOTIFICATION_TEMPLATE),
NOTIFICATION_RULE(true, EntityType.NOTIFICATION_RULE),
NOTIFICATION_TARGET(true, EntityType.NOTIFICATION_TARGET),
NOTIFICATION_TEMPLATE(true, EntityType.NOTIFICATION_TEMPLATE),
TB_RESOURCE(true, EntityType.TB_RESOURCE),
OAUTH2_CLIENT(true, EntityType.OAUTH2_CLIENT),
DOMAIN(true, EntityType.DOMAIN);
DOMAIN(true, EntityType.DOMAIN),
CALCULATED_FIELD(false, EntityType.CALCULATED_FIELD);
private final boolean allEdgesRelated;

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

@ -171,6 +171,8 @@ public class EntityIdFactory {
return new OAuth2ClientId(uuid);
case DOMAIN:
return new DomainId(uuid);
case CALCULATED_FIELD:
return new CalculatedFieldId(uuid);
}
throw new IllegalArgumentException("EdgeEventType " + edgeEventType + " is not supported!");
}

2
common/edge-api/src/main/java/org/thingsboard/edge/rpc/EdgeGrpcClient.java

@ -136,7 +136,7 @@ public class EdgeGrpcClient implements EdgeRpcClient {
.setConnectRequestMsg(ConnectRequestMsg.newBuilder()
.setEdgeRoutingKey(edgeKey)
.setEdgeSecret(edgeSecret)
.setEdgeVersion(EdgeVersion.V_4_0_0)
.setEdgeVersion(EdgeVersion.V_4_1_0)
.setMaxInboundMessageSize(maxInboundMessageSize)
.build())
.build());

18
common/edge-api/src/main/proto/edge.proto

@ -42,6 +42,7 @@ enum EdgeVersion {
V_3_8_0 = 8;
V_3_9_0 = 9;
V_4_0_0 = 10;
V_4_1_0 = 11;
V_LATEST = 999;
}
@ -124,6 +125,14 @@ enum UpdateMsgType {
// use 6 as a next number
}
message CalculatedFieldUpdateMsg{
UpdateMsgType msgType = 1;
int64 idMSB = 2;
int64 idLSB = 3;
string entity = 4;
}
message EntityDataProto {
int64 entityIdMSB = 1;
int64 entityIdLSB = 2;
@ -325,6 +334,12 @@ message RelationRequestMsg {
string entityType = 3;
}
message CalculatedFieldRequestMsg {
int64 entityIdMSB = 1;
int64 entityIdLSB = 2;
string entityType = 3;
}
// DEPRECATED. FOR REMOVAL
message UserCredentialsRequestMsg {
option deprecated = true;
@ -423,6 +438,8 @@ message UplinkMsg {
repeated AlarmCommentUpdateMsg alarmCommentUpdateMsg = 22;
repeated RuleChainUpdateMsg ruleChainUpdateMsg = 23;
repeated RuleChainMetadataUpdateMsg ruleChainMetadataUpdateMsg = 24;
repeated CalculatedFieldUpdateMsg calculatedFieldUpdateMsg = 25;
repeated CalculatedFieldRequestMsg calculatedFieldRequestMsg = 26;
}
message UplinkResponseMsg {
@ -472,4 +489,5 @@ message DownlinkMsg {
repeated NotificationTargetUpdateMsg notificationTargetUpdateMsg = 32;
repeated NotificationTemplateUpdateMsg notificationTemplateUpdateMsg = 33;
repeated OAuth2DomainUpdateMsg oAuth2DomainUpdateMsg = 34;
repeated CalculatedFieldUpdateMsg calculatedFieldUpdateMsg = 35;
}

10
common/proto/src/main/java/org/thingsboard/server/common/util/ProtoUtils.java

@ -522,7 +522,7 @@ public class ProtoUtils {
}
private static TransportProtos.ToDeviceRpcRequestActorMsgProto toProto(ToDeviceRpcRequestActorMsg msg) {
TransportProtos.ToDeviceRpcRequestMsg proto = TransportProtos.ToDeviceRpcRequestMsg.newBuilder()
TransportProtos.ToDeviceRpcRequestMsg.Builder builder = TransportProtos.ToDeviceRpcRequestMsg.newBuilder()
.setMethodName(msg.getMsg().getBody().getMethod())
.setParams(msg.getMsg().getBody().getParams())
.setExpirationTime(msg.getMsg().getExpirationTime())
@ -530,7 +530,11 @@ public class ProtoUtils {
.setRequestIdLSB(msg.getMsg().getId().getLeastSignificantBits())
.setOneway(msg.getMsg().isOneway())
.setPersisted(msg.getMsg().isPersisted())
.build();
.setAdditionalInfo(msg.getMsg().getAdditionalInfo());
if (msg.getMsg().getRetries() != null) {
builder.setRetries(msg.getMsg().getRetries());
}
TransportProtos.ToDeviceRpcRequestMsg proto = builder.build();
return TransportProtos.ToDeviceRpcRequestActorMsgProto.newBuilder()
.setTenantIdMSB(msg.getTenantId().getId().getMostSignificantBits())
@ -551,7 +555,7 @@ public class ProtoUtils {
toDeviceRpcRequestMsg.getOneway(),
toDeviceRpcRequestMsg.getExpirationTime(),
new ToDeviceRpcRequestBody(toDeviceRpcRequestMsg.getMethodName(), toDeviceRpcRequestMsg.getParams()),
toDeviceRpcRequestMsg.getPersisted(), 0, "");
toDeviceRpcRequestMsg.getPersisted(), toDeviceRpcRequestMsg.hasRetries() ? toDeviceRpcRequestMsg.getRetries() : null, toDeviceRpcRequestMsg.getAdditionalInfo());
return new ToDeviceRpcRequestActorMsg(proto.getServiceId(), toDeviceRpcRequest);
}

2
common/proto/src/main/proto/queue.proto

@ -696,6 +696,8 @@ message ToDeviceRpcRequestMsg {
int64 requestIdLSB = 6;
bool oneway = 7;
bool persisted = 8;
optional int32 retries = 9;
string additionalInfo = 10;
}
message ToDeviceRpcResponseMsg {

23
dao/src/main/java/org/thingsboard/server/dao/cf/BaseCalculatedFieldService.java

@ -58,6 +58,22 @@ public class BaseCalculatedFieldService extends AbstractEntityService implements
@Override
public CalculatedField save(CalculatedField calculatedField) {
CalculatedField oldCalculatedField = calculatedFieldDataValidator.validate(calculatedField, CalculatedField::getTenantId);
return doSave(calculatedField, oldCalculatedField);
}
@Override
public CalculatedField save(CalculatedField calculatedField, boolean doValidate) {
CalculatedField oldCalculatedField = null;
if (doValidate) {
oldCalculatedField = calculatedFieldDataValidator.validate(calculatedField, CalculatedField::getTenantId);
} else if (calculatedField.getId() != null) {
oldCalculatedField = findById(calculatedField.getTenantId(), calculatedField.getId());
}
return doSave(calculatedField, oldCalculatedField);
}
private CalculatedField doSave(CalculatedField calculatedField, CalculatedField oldCalculatedField) {
try {
TenantId tenantId = calculatedField.getTenantId();
log.trace("Executing save calculated field, [{}]", calculatedField);
@ -83,6 +99,13 @@ public class BaseCalculatedFieldService extends AbstractEntityService implements
return calculatedFieldDao.findById(tenantId, calculatedFieldId.getId());
}
@Override
public CalculatedField findByEntityIdAndName(EntityId entityId, String name) {
log.trace("Executing findByEntityIdAndName [{}], calculatedFieldName[{}]", entityId, name);
validateId(entityId.getId(), id -> INCORRECT_ENTITY_ID + id);
return calculatedFieldDao.findByEntityIdAndName(entityId, name);
}
@Override
public List<CalculatedFieldId> findCalculatedFieldIdsByEntityId(TenantId tenantId, EntityId entityId) {
log.trace("Executing findCalculatedFieldIdsByEntityId [{}]", entityId);

2
dao/src/main/java/org/thingsboard/server/dao/cf/CalculatedFieldDao.java

@ -35,6 +35,8 @@ public interface CalculatedFieldDao extends Dao<CalculatedField> {
List<CalculatedField> findAll();
CalculatedField findByEntityIdAndName(EntityId entityId, String name);
PageData<CalculatedField> findAll(PageLink pageLink);
PageData<CalculatedField> findAllByTenantId(TenantId tenantId, PageLink pageLink);

6
dao/src/main/java/org/thingsboard/server/dao/resource/BaseResourceService.java

@ -263,7 +263,7 @@ public class BaseResourceService extends AbstractCachedEntityService<ResourceInf
@Override
public TbResource toResource(TenantId tenantId, ResourceExportData exportData) {
if (exportData.getType() == ResourceType.IMAGE || exportData.getSubType() == ResourceSubType.IMAGE
|| exportData.getSubType() == ResourceSubType.SCADA_SYMBOL) {
|| exportData.getSubType() == ResourceSubType.SCADA_SYMBOL) {
throw new IllegalArgumentException("Image import not supported");
}
@ -311,7 +311,7 @@ public class BaseResourceService extends AbstractCachedEntityService<ResourceInf
log.trace("Executing findResourceInfoById [{}] [{}]", tenantId, resourceId);
Validator.validateId(resourceId, id -> INCORRECT_RESOURCE_ID + id);
return cache.getAndPutInTransaction(new ResourceInfoCacheKey(tenantId, resourceId),
return cache.getAndPutInTransaction(new ResourceInfoCacheKey(resourceId),
() -> resourceInfoDao.findById(tenantId, resourceId.getId()), true);
}
@ -712,7 +712,7 @@ public class BaseResourceService extends AbstractCachedEntityService<ResourceInf
@Override
public void handleEvictEvent(ResourceInfoEvictEvent event) {
if (event.getResourceId() != null) {
cache.evict(new ResourceInfoCacheKey(event.getTenantId(), event.getResourceId()));
cache.evict(new ResourceInfoCacheKey(event.getResourceId()));
}
}

2
dao/src/main/java/org/thingsboard/server/dao/sql/cf/CalculatedFieldRepository.java

@ -28,6 +28,8 @@ public interface CalculatedFieldRepository extends JpaRepository<CalculatedField
boolean existsByTenantIdAndEntityId(UUID tenantId, UUID entityId);
CalculatedFieldEntity findByEntityIdAndName(UUID entityId, String name);
List<CalculatedFieldId> findCalculatedFieldIdsByTenantIdAndEntityId(UUID tenantId, UUID entityId);
List<CalculatedFieldEntity> findAllByTenantIdAndEntityId(UUID tenantId, UUID entityId);

5
dao/src/main/java/org/thingsboard/server/dao/sql/cf/JpaCalculatedFieldDao.java

@ -65,6 +65,11 @@ public class JpaCalculatedFieldDao extends JpaAbstractDao<CalculatedFieldEntity,
return DaoUtil.convertDataList(calculatedFieldRepository.findAll());
}
@Override
public CalculatedField findByEntityIdAndName(EntityId entityId, String name) {
return DaoUtil.getData(calculatedFieldRepository.findByEntityIdAndName(entityId.getId(), name));
}
@Override
public PageData<CalculatedField> findAll(PageLink pageLink) {
log.debug("Try to find calculated fields by pageLink [{}]", pageLink);

4
ui-ngx/src/app/modules/home/components/rule-node/external/azure-iot-hub-config.component.html

@ -38,7 +38,9 @@
{{ 'rule-node-config.device-id-required' | translate }}
</mat-error>
</mat-form-field>
<tb-mqtt-version-select formControlName="protocolVersion" subscriptSizing="fixed"></tb-mqtt-version-select>
<tb-mqtt-version-select formControlName="protocolVersion" subscriptSizing="fixed"
[excludeVersions]="[MqttVersion.MQTT_3_1, MqttVersion.MQTT_5]">
</tb-mqtt-version-select>
<mat-accordion>
<mat-expansion-panel class="tb-mqtt-credentials-panel-group">
<mat-expansion-panel-header>

2
ui-ngx/src/app/modules/home/components/rule-node/external/azure-iot-hub-config.component.ts

@ -22,6 +22,7 @@ import {
azureIotHubCredentialsTypes,
azureIotHubCredentialsTypeTranslations
} from '@home/components/rule-node/rule-node-config.models';
import { MqttVersion } from '@shared/models/mqtt.models';
@Component({
selector: 'tb-external-node-azure-iot-hub-config',
@ -34,6 +35,7 @@ export class AzureIotHubConfigComponent extends RuleNodeConfigurationComponent {
allAzureIotHubCredentialsTypes = azureIotHubCredentialsTypes;
azureIotHubCredentialsTypeTranslationsMap = azureIotHubCredentialsTypeTranslations;
MqttVersion = MqttVersion;
constructor(private fb: UntypedFormBuilder) {
super();

1
ui-ngx/src/app/modules/home/components/widget/lib/alarm/alarms-table-widget.component.ts

@ -340,6 +340,7 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit,
public onDataUpdated() {
this.alarmsDatasource.updateAlarms();
this.clearCache();
this.ctx.detectChanges();
}
public onEditModeChanged() {

2
ui-ngx/src/app/modules/home/components/widget/lib/chart/bar-chart-with-labels-widget.component.html

@ -19,7 +19,7 @@
<div class="tb-bar-chart-overlay" [style]="overlayStyle"></div>
@if (widgetComponent.dashboardWidget.showWidgetTitlePanel) {
<div class="tb-widget-title-row flex justify-between">
<ng-container *ngTemplateOutlet="widgetComponent.widgetTitlePanel"></ng-container>
<ng-container *ngTemplateOutlet="widgetTitlePanel || widgetComponent.widgetTitlePanel"></ng-container>
<ng-container *ngTemplateOutlet="widgetComponent.widgetHeaderActionsPanel"></ng-container>
</div>
} @else {

3
ui-ngx/src/app/modules/home/components/widget/lib/chart/bar-chart-with-labels-widget.component.ts

@ -58,6 +58,9 @@ export class BarChartWithLabelsWidgetComponent implements OnInit, OnDestroy, Aft
@Input()
ctx: WidgetContext;
@Input()
widgetTitlePanel: TemplateRef<any>;
showLegend: boolean;
legendClass: string;

2
ui-ngx/src/app/modules/home/components/widget/lib/chart/range-chart-widget.component.html

@ -19,7 +19,7 @@
<div class="tb-range-chart-overlay" [style]="overlayStyle"></div>
@if (widgetComponent.dashboardWidget.showWidgetTitlePanel) {
<div class="tb-widget-title-row flex justify-between">
<ng-container *ngTemplateOutlet="widgetComponent.widgetTitlePanel"></ng-container>
<ng-container *ngTemplateOutlet="widgetTitlePanel || widgetComponent.widgetTitlePanel"></ng-container>
<ng-container *ngTemplateOutlet="widgetComponent.widgetHeaderActionsPanel"></ng-container>
</div>
} @else {

19
ui-ngx/src/app/modules/home/components/widget/lib/chart/range-chart-widget.component.ts

@ -23,6 +23,7 @@ import {
OnDestroy,
OnInit,
Renderer2,
TemplateRef,
ViewChild,
ViewEncapsulation
} from '@angular/core';
@ -49,7 +50,7 @@ import { ImagePipe } from '@shared/pipe/image.pipe';
import { DomSanitizer } from '@angular/platform-browser';
import { TbTimeSeriesChart } from '@home/components/widget/lib/chart/time-series-chart';
import { WidgetComponent } from '@home/components/widget/widget.component';
import { TbUnitConverter } from '@shared/models/unit.models';
import { TbUnit } from '@shared/models/unit.models';
import { UnitService } from '@core/services/unit.service';
@Component({
@ -68,6 +69,9 @@ export class RangeChartWidgetComponent implements OnInit, OnDestroy, AfterViewIn
@Input()
ctx: WidgetContext;
@Input()
widgetTitlePanel: TemplateRef<any>;
showLegend: boolean;
legendClass: string;
@ -80,8 +84,7 @@ export class RangeChartWidgetComponent implements OnInit, OnDestroy, AfterViewIn
visibleRangeItems: RangeItem[];
private decimals = 0;
private units: string = '';
private unitConvertor: TbUnitConverter;
private units: TbUnit = '';
private rangeItems: RangeItem[];
@ -100,22 +103,20 @@ export class RangeChartWidgetComponent implements OnInit, OnDestroy, AfterViewIn
const unitService = this.ctx.$injector.get(UnitService);
this.decimals = this.ctx.decimals;
let units = this.ctx.units;
this.units = this.ctx.units;
const dataKey = getDataKey(this.ctx.datasources);
if (isDefinedAndNotNull(dataKey?.decimals)) {
this.decimals = dataKey.decimals;
}
if (dataKey?.units) {
units = dataKey.units;
this.units = dataKey.units;
}
if (dataKey) {
dataKey.settings = rangeChartTimeSeriesKeySettings(this.settings);
}
this.units = unitService.getTargetUnitSymbol(units);
this.unitConvertor = unitService.geUnitConverter(units);
const valueFormat = ValueFormatProcessor.fromSettings(this.ctx.$injector, {
units,
units: this.units,
decimals: this.decimals,
ignoreUnitSymbol: true
});
@ -138,7 +139,7 @@ export class RangeChartWidgetComponent implements OnInit, OnDestroy, AfterViewIn
}
ngAfterViewInit() {
const settings = rangeChartTimeSeriesSettings(this.settings, this.rangeItems, this.decimals, this.units, this.unitConvertor);
const settings = rangeChartTimeSeriesSettings(this.settings, this.rangeItems, this.decimals, this.units);
this.timeSeriesChart = new TbTimeSeriesChart(this.ctx, settings, this.chartShape.nativeElement, this.renderer);
}

22
ui-ngx/src/app/modules/home/components/widget/lib/chart/range-chart-widget.models.ts

@ -57,6 +57,7 @@ import {
import {
TimeSeriesChartTooltipWidgetSettings
} from '@home/components/widget/lib/chart/time-series-chart-tooltip.models';
import { TbUnit } from '@shared/models/unit.models';
export interface RangeItem {
index: number;
@ -221,13 +222,13 @@ export const rangeChartDefaultSettings: RangeChartWidgetSettings = {
};
export const rangeChartTimeSeriesSettings = (settings: RangeChartWidgetSettings, rangeItems: RangeItem[],
decimals: number, units: string, valueConvertor: (x: number) => number): DeepPartial<TimeSeriesChartSettings> => {
decimals: number, units: TbUnit): DeepPartial<TimeSeriesChartSettings> => {
let thresholds: DeepPartial<TimeSeriesChartThreshold>[] = settings.showRangeThresholds ? getMarkPoints(rangeItems).map(item => ({
...{type: ValueSourceType.constant,
yAxisId: 'default',
units,
decimals,
value: valueConvertor(item)},
value: item},
...settings.rangeThreshold
} as DeepPartial<TimeSeriesChartThreshold>)) : [];
if (settings.thresholds?.length) {
@ -240,10 +241,8 @@ export const rangeChartTimeSeriesSettings = (settings: RangeChartWidgetSettings,
yAxes: {
default: {
...settings.yAxis,
...{
decimals,
units
}
decimals,
units
}
},
xAxis: settings.xAxis,
@ -299,14 +298,15 @@ export const toRangeItems = (colorRanges: Array<ColorRange>, valueFormat: ValueF
for (let i = 0; i < ranges.length; i++) {
const range = ranges[i];
let from = range.from;
const to = isDefinedAndNotNull(range.to) ? Number(valueFormat.format(range.to)) : range.to;
const to = range.to;
if (i > 0) {
const prevRange = ranges[i - 1];
if (isNumber(prevRange.to) && isNumber(from) && from < prevRange.to) {
from = prevRange.to;
}
}
from = isDefinedAndNotNull(from) ? Number(valueFormat.format(from)) : from;
const formatToValue = isDefinedAndNotNull(to) ? Number(valueFormat.format(to)) : to;
const formatFromValue = isDefinedAndNotNull(from) ? Number(valueFormat.format(from)) : from;
rangeItems.push(
{
index: counter++,
@ -315,12 +315,12 @@ export const toRangeItems = (colorRanges: Array<ColorRange>, valueFormat: ValueF
visible: true,
from,
to,
label: rangeItemLabel(from, to),
piece: createTimeSeriesChartVisualMapPiece(range.color, from, to)
label: rangeItemLabel(formatFromValue, formatToValue),
piece: createTimeSeriesChartVisualMapPiece(range.color, formatFromValue, formatToValue)
}
);
if (!isNumber(from) || !isNumber(to)) {
const value = !isNumber(from) ? to : from;
const value = !isNumber(from) ? formatToValue : formatFromValue;
rangeItems.push(
{
index: counter++,

2
ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart-widget.component.html

@ -19,7 +19,7 @@
<div class="tb-time-series-chart-overlay" [style]="overlayStyle"></div>
@if (widgetComponent.dashboardWidget.showWidgetTitlePanel) {
<div class="tb-widget-title-row flex justify-between">
<ng-container *ngTemplateOutlet="widgetComponent.widgetTitlePanel"></ng-container>
<ng-container *ngTemplateOutlet="widgetTitlePanel || widgetComponent.widgetTitlePanel"></ng-container>
<ng-container *ngTemplateOutlet="widgetComponent.widgetHeaderActionsPanel"></ng-container>
</div>
} @else {

3
ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart-widget.component.ts

@ -61,6 +61,9 @@ export class TimeSeriesChartWidgetComponent implements OnInit, OnDestroy, AfterV
@Input()
ctx: WidgetContext;
@Input()
widgetTitlePanel: TemplateRef<any>;
horizontalLegendPosition = false;
showLegend: boolean;

4
ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.models.ts

@ -98,7 +98,7 @@ import {
TimeSeriesChartTooltipValueFormatFunction,
TimeSeriesChartTooltipWidgetSettings
} from '@home/components/widget/lib/chart/time-series-chart-tooltip.models';
import { TbUnitConverter } from '@shared/models/unit.models';
import { TbUnit, TbUnitConverter } from '@shared/models/unit.models';
type TimeSeriesChartDataEntry = [number, any, number, number];
@ -377,7 +377,7 @@ export type TimeSeriesChartTicksFormatter =
export interface TimeSeriesChartYAxisSettings extends TimeSeriesChartAxisSettings {
id?: TimeSeriesChartYAxisId;
order?: number;
units?: string;
units?: TbUnit;
decimals?: number;
interval?: number;
splitNumber?: number;

1
ui-ngx/src/app/modules/home/components/widget/lib/entity/entities-table-widget.component.ts

@ -275,6 +275,7 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni
public onDataUpdated() {
this.entityDatasource.dataUpdated();
this.clearCache();
this.ctx.detectChanges();
}
public onEditModeChanged() {

2
ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget.component.html

@ -19,7 +19,7 @@
<div class="tb-map-overlay" [style]="overlayStyle"></div>
@if (widgetComponent.dashboardWidget.showWidgetTitlePanel) {
<div class="tb-widget-title-row flex justify-between">
<ng-container *ngTemplateOutlet="widgetComponent.widgetTitlePanel"></ng-container>
<ng-container *ngTemplateOutlet="widgetTitlePanel || widgetComponent.widgetTitlePanel"></ng-container>
<ng-container *ngTemplateOutlet="widgetComponent.widgetHeaderActionsPanel"></ng-container>
</div>
} @else {

4
ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget.component.ts

@ -21,6 +21,7 @@ import {
Input,
OnDestroy,
OnInit,
TemplateRef,
ViewChild,
ViewEncapsulation
} from '@angular/core';
@ -54,6 +55,9 @@ export class MapWidgetComponent implements OnInit, OnDestroy {
@Input()
ctx: WidgetContext;
@Input()
widgetTitlePanel: TemplateRef<any>;
backgroundStyle$: Observable<ComponentStyle>;
overlayStyle: ComponentStyle = {};
padding: string;

23
ui-ngx/src/app/shared/components/mqtt-version-select.component.ts

@ -14,7 +14,7 @@
/// limitations under the License.
///
import { Component, forwardRef, Input } from '@angular/core';
import { Component, forwardRef, Input, OnChanges, SimpleChanges } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { coerceBoolean } from '@shared/decorators/coercion';
import { SubscriptSizing, MatFormFieldAppearance } from '@angular/material/form-field';
@ -30,7 +30,7 @@ import { MqttVersionTranslation, MqttVersion } from '@shared/models/mqtt.models'
multi: true
}]
})
export class MqttVersionSelectComponent implements ControlValueAccessor {
export class MqttVersionSelectComponent implements ControlValueAccessor, OnChanges {
@Input()
disabled: boolean;
@ -41,7 +41,10 @@ export class MqttVersionSelectComponent implements ControlValueAccessor {
@Input()
appearance: MatFormFieldAppearance = 'fill';
mqttVersions = Object.values(MqttVersion);
@Input()
excludeVersions: MqttVersion[];
mqttVersions = Object.values(MqttVersion);
mqttVersionTranslation = MqttVersionTranslation;
modelValue: MqttVersion;
@ -54,6 +57,20 @@ export class MqttVersionSelectComponent implements ControlValueAccessor {
constructor() {
}
ngOnChanges(changes: SimpleChanges): void {
for (const propName of Object.keys(changes)) {
const change = changes[propName];
if (propName === 'excludeVersions' && change.currentValue !== change.previousValue) {
const excludeVersions = change.currentValue;
if (excludeVersions?.length) {
this.mqttVersions = Object.values(MqttVersion).filter(v => !excludeVersions.includes(v));
} else {
this.mqttVersions = Object.values(MqttVersion);
}
}
}
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}

14
ui-ngx/src/app/shared/models/units/speed.ts

@ -18,8 +18,8 @@ import { TbMeasure, TbMeasureUnits } from '@shared/models/unit.models';
export type SpeedUnits = SpeedMetricUnits | SpeedImperialUnits;
export type SpeedMetricUnits = 'm/s' | 'km/h' | 'mm/min' | 'mm/s';
export type SpeedImperialUnits = 'mph' | 'kt' | 'ft/s' | 'ft/min' | 'in/h';
export type SpeedMetricUnits = 'm/s' | 'km/h' | 'mm/min' | 'm/min' | 'mm/s';
export type SpeedImperialUnits = 'mph' | 'kt' | 'ft/s' | 'ft/min' | 'in/s' | 'in/h';
const METRIC: TbMeasureUnits<SpeedMetricUnits> = {
ratio: 1 / 1.609344,
@ -37,6 +37,11 @@ const METRIC: TbMeasureUnits<SpeedMetricUnits> = {
'mm/min': {
name: 'unit.millimeters-per-minute',
tags: ['feed rate', 'cutting feed rate'],
to_anchor: 0.00006,
},
'm/min': {
name: 'unit.meter-per-minute',
tags: ['velocity', 'pace'],
to_anchor: 0.06,
},
'mm/s': {
@ -70,6 +75,11 @@ const IMPERIAL: TbMeasureUnits<SpeedImperialUnits> = {
tags: ['velocity', 'pace'],
to_anchor: 0.0113636,
},
'in/s': {
name: 'unit.inch-per-second',
tags: ['velocity', 'pace'],
to_anchor: 0.0568182,
},
'in/h': {
name: 'unit.inch-per-hour',
tags: ['velocity', 'pace'],

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

@ -6016,9 +6016,9 @@
"foot-us": "Foot (US survey)",
"yard": "Yard",
"mile": "Mile",
"nautical-mile": "Nautical Mile",
"astronomical-unit": "Astronomical Unit",
"reciprocal-metre": "Reciprocal Metre",
"nautical-mile": "Nautical mile",
"astronomical-unit": "Astronomical unit",
"reciprocal-metre": "Reciprocal metre",
"meter-per-meter": "Meter per meter",
"steradian": "Steradian",
"thou": "Thou",
@ -6048,24 +6048,24 @@
"quarter": "Quarter",
"slug": "Slug",
"carat": "Carat",
"cubic-millimeter": "Cubic Millimeter",
"cubic-centimeter": "Cubic Centimeter",
"cubic-meter": "Cubic Meter",
"cubic-kilometer": "Cubic Kilometer",
"cubic-millimeter": "Cubic millimeter",
"cubic-centimeter": "Cubic centimeter",
"cubic-meter": "Cubic meter",
"cubic-kilometer": "Cubic kilometer",
"microliter": "Microliter",
"milliliter": "Milliliter",
"liter": "Liter",
"hectoliter": "Hectolitre",
"cubic-inch": "Cubic Inch",
"cubic-foot": "Cubic Foot",
"cubic-yard": "Cubic Yard",
"fluid-ounce": "Fluid Ounce",
"fluid-ounce-per-second": "Fluid Ounce per second",
"cubic-inch": "Cubic inch",
"cubic-foot": "Cubic foot",
"cubic-yard": "Cubic yard",
"fluid-ounce": "Fluid ounce",
"fluid-ounce-per-second": "Fluid ounce per second",
"pint": "Pint",
"quart": "Quart",
"gallon": "Gallon",
"oil-barrels": "Oil Barrel",
"cubic-meter-per-kilogram": "Cubic Meter per Kilogram",
"oil-barrels": "Oil barrel",
"cubic-meter-per-kilogram": "Cubic meter per kilogram",
"gill": "Gill",
"hogshead": "Hogshead",
"teaspoon": "Teaspoon",
@ -6076,14 +6076,16 @@
"rankine": "Rankine",
"fahrenheit": "Fahrenheit",
"percent": "Percent",
"meter-per-second": "Meter per Second",
"kilometer-per-hour": "Kilometer per Hour",
"foot-per-second": "Foot per Second",
"foot-per-minute": "Foot per Minute",
"mile-per-hour": "Mile per Hour",
"meter-per-second": "Meter per second",
"kilometer-per-hour": "Kilometer per hour",
"foot-per-second": "Foot per second",
"foot-per-minute": "Foot per minute",
"mile-per-hour": "Mile per hour",
"knot": "Knot",
"inch-per-hour": "Inch per Hour",
"inch-per-second": "Inch per second",
"inch-per-hour": "Inch per hour",
"millimeters-per-minute": "Millimeters per minute",
"meter-per-minute": "Meter per minute",
"kilometer-per-hour-squared": "Kilometer per hour squared",
"foot-per-second-squared": "Foot per second squared",
"pascal": "Pascal",
@ -6099,18 +6101,18 @@
"inch-pounds": "Inch-pounds",
"newton-per-meter": "Newton per meter",
"atmospheres": "Atmospheres",
"pounds-per-square-inch": "Pounds per Square Inch",
"kilopound-per-square-inch": "Kilopound per Square Inch",
"pounds-per-square-inch": "Pounds per square inch",
"kilopound-per-square-inch": "Kilopound per square inch",
"torr": "Torr",
"inches-of-mercury": "Inches of Mercury",
"pascal-per-square-meter": "Pascal per Square Meter",
"pound-per-square-inch": "Pound per Square Inch",
"newton-per-square-meter": "Newton per Square Meter",
"kilogram-force-per-square-meter": "Kilogram-force per Square Meter",
"pascal-per-square-centimeter": "Pascal per Square Centimeter",
"ton-force-per-square-inch": "Ton-force per Square Inch",
"kilonewton-per-square-meter": "Kilonewton per Square Meter",
"newton-per-square-millimeter": "Newton per Square Millimeter",
"inches-of-mercury": "Inches of mercury",
"pascal-per-square-meter": "Pascal per square meter",
"pound-per-square-inch": "Pound per square inch",
"newton-per-square-meter": "Newton per square meter",
"kilogram-force-per-square-meter": "Kilogram-force per square meter",
"pascal-per-square-centimeter": "Pascal per square centimeter",
"ton-force-per-square-inch": "Ton-force per square inch",
"kilonewton-per-square-meter": "Kilonewton per square meter",
"newton-per-square-millimeter": "Newton per square millimeter",
"microjoule": "Microjoule",
"millijoule": "Millijoule",
"joule": "Joule",
@ -6124,31 +6126,31 @@
"megawatt-hour": "Megawatt-hour",
"gigawatt-hour": "Gigawatt-hour",
"electron-volts": "Electron volts",
"joules-per-coulomb": "Joules per Coulomb",
"british-thermal-unit": "British Thermal Units",
"thousand-british-thermal-unit": "Thousand British Thermal Units",
"million-british-thermal-unit": "Million British Thermal Units",
"joules-per-coulomb": "Joules per coulomb",
"british-thermal-unit": "British thermal units",
"thousand-british-thermal-unit": "Thousand British thermal units",
"million-british-thermal-unit": "Million British thermal units",
"foot-pound": "Foot-pound",
"calorie": "Calorie",
"small-calorie": "Small Calorie",
"small-calorie": "Small calorie",
"kilocalorie": "Kilocalorie",
"joule-per-kelvin": "Joule per Kelvin",
"joule-per-kilogram-kelvin": "Joule per Kilogram-Kelvin",
"joule-per-kilogram": "Joule per Kilogram",
"watt-per-meter-kelvin": "Watt per Meter-Kelvin",
"joule-per-cubic-meter": "Joule per Cubic Meter",
"joule-per-kelvin": "Joule per kelvin",
"joule-per-kilogram-kelvin": "Joule per kilogram-kelvin",
"joule-per-kilogram": "Joule per kilogram",
"watt-per-meter-kelvin": "Watt per meter-kelvin",
"joule-per-cubic-meter": "Joule per cubic meter",
"therm": "Therm",
"electric-dipole-moment": "Electric Dipole Moment",
"magnetic-dipole-moment": "Magnetic Dipole Moment",
"electric-dipole-moment": "Electric dipole moment",
"magnetic-dipole-moment": "Magnetic dipole moment",
"debye": "Debye",
"coulomb-per-square-meter-per-volt": "Coulomb per Square Meter per Volt",
"coulomb-per-square-meter-per-volt": "Coulomb per square meter per volt",
"milliwatt": "Milliwatt",
"microwatt": "Microwatt",
"watt": "Watt",
"kilowatt": "Kilowatt",
"megawatt": "Megawatt",
"gigawatt": "Gigawatt",
"metric-horsepower": "Metric Horsepower",
"metric-horsepower": "Metric horsepower",
"milliwatt-per-square-centimeter": "Milliwatts per square centimeter",
"watt-per-square-centimeter": "Watts per square centimeter",
"kilowatt-per-square-centimeter": "Kilowatts per square centimeter",
@ -6167,28 +6169,28 @@
"mmbtu-per-hour": "Million British thermal units per hour",
"mmbtu-per-second": "Million British thermal units per second",
"mmbtu-per-day": "Million British thermal units per day",
"foot-pound-per-second": "foot-pound per second",
"foot-pound-per-second": "Foot-pound per second",
"coulomb": "Coulomb",
"millicoulomb": "Millicoulombs",
"microcoulomb": "Microcoulomb",
"nanocoulomb": "Nanocoulomb",
"picocoulomb": "Picocoulomb",
"coulomb-per-meter": "Coulomb per meter",
"coulomb-per-cubic-meter": "Coulomb per Cubic Meter",
"coulomb-per-square-meter": "Coulomb per Square Meter",
"square-millimeter": "Square Millimeter",
"square-centimeter": "Square Centimeter",
"square-meter": "Square Meter",
"coulomb-per-cubic-meter": "Coulomb per cubic meter",
"coulomb-per-square-meter": "Coulomb per square meter",
"square-millimeter": "Square millimeter",
"square-centimeter": "Square centimeter",
"square-meter": "Square meter",
"hectare": "Hectare",
"square-kilometer": "Square Kilometer",
"square-inch": "Square Inch",
"square-foot": "Square Foot",
"square-yard": "Square Yard",
"square-kilometer": "Square kilometer",
"square-inch": "Square inch",
"square-foot": "Square foot",
"square-yard": "Square yard",
"acre": "Acre",
"square-mile": "Square Mile",
"square-mile": "Square mile",
"are": "Are",
"barn": "Barn",
"circular-inch": "Circular Inch",
"circular-inch": "Circular inch",
"milliampere-hour": "Milliampere-hour",
"ampere-hours": "Ampere-hours",
"kiloampere-hours": "Kiloampere-hours",
@ -6201,11 +6203,11 @@
"megaampere": "Megaampere",
"gigaampere": "Gigaampere",
"microampere-per-square-centimeter": "Microampere per square centimeter",
"ampere-per-square-meter": "Ampere per Square Meter",
"ampere-per-meter": "Ampere per Meter",
"ampere-per-square-meter": "Ampere per square meter",
"ampere-per-meter": "Ampere per meter",
"oersted": "Oersted",
"bohr-magneton": "Bohr Magneton",
"ampere-meter-squared": "Ampere-Meter Squared",
"bohr-magneton": "Bohr magneton",
"ampere-meter-squared": "Ampere-meter squared",
"nanovolt": "Nanovolt",
"picovolt": "Picovolt",
"millivolt": "Millivolts",
@ -6215,12 +6217,12 @@
"megavolt": "Megavolt",
"dbmV": "Decibel volt",
"dbm": "Decibel-milliwatts",
"volt-meter": "Volt-Meter",
"kilovolt-meter": "Kilovolt-Meter",
"megavolt-meter": "Megavolt-Meter",
"microvolt-meter": "Microvolt-Meter",
"millivolt-meter": "Millivolt-Meter",
"nanovolt-meter": "Nanovolt-Meter",
"volt-meter": "Volt-meter",
"kilovolt-meter": "Kilovolt-meter",
"megavolt-meter": "Megavolt-meter",
"microvolt-meter": "Microvolt-meter",
"millivolt-meter": "Millivolt-meter",
"nanovolt-meter": "Nanovolt-meter",
"ohm": "Ohm",
"microohm": "Microohm",
"milliohm": "Milliohm",
@ -6233,7 +6235,7 @@
"megahertz": "Megahertz",
"gigahertz": "Gigahertz",
"terahertz": "Terahertz",
"rpm": "Revolutions Per Minute",
"rpm": "Revolutions per minute",
"candela-per-square-meter": "Candela per square meter",
"candela": "Candela",
"lumen": "Lumen",
@ -6245,17 +6247,17 @@
"lumens-per-watt": "Lumens per watt",
"mole": "Mole",
"nanomole": "Nanomole",
"micromole": "MicroMole",
"micromole": "Micromole",
"millimole": "Millimole",
"kilomole": "Kilomole",
"mole-per-cubic-meter": "Mole per Cubic Meter",
"mole-per-cubic-meter": "Mole per cubic meter",
"rssi": "Received signal strength indicator",
"ppm": "Parts Per Million",
"ppb": "Parts Per Billion",
"micrograms-per-cubic-meter": "Micrograms per Cubic Meter",
"aqi": "AQI",
"ppm": "Parts per million",
"ppb": "Parts per billion",
"micrograms-per-cubic-meter": "Micrograms per cubic meter",
"aqi": "Aqi",
"gram-per-cubic-meter": "Gram per cubic meter",
"gram-per-kilogram": "Specific Humidity",
"gram-per-kilogram": "Specific humidity",
"millimeters-per-second": "Millimeters per second",
"neper": "Neper",
"bel": "Bel",
@ -6266,7 +6268,7 @@
"gray": "Gray",
"sievert": "Sievert",
"roentgen": "Roentgen",
"cps": "Counts per Second",
"cps": "Counts per second",
"rad": "Rad",
"rem": "Rem",
"dps": "Disintegrations per second",
@ -6276,10 +6278,10 @@
"curies-per-liter": "Curies per liter",
"becquerels-per-second": "Becquerels per second",
"curies-per-second": "Curies per second",
"gy-per-second": "Gray per Second",
"watt-per-steradian": "Watt per Steradian",
"watt-per-square-metre-steradian": "Watt per Square Metre-Steradian",
"ph-level": "pH Level",
"gy-per-second": "Gray per second",
"watt-per-steradian": "Watt per steradian",
"watt-per-square-metre-steradian": "Watt per square metre-steradian",
"ph-level": "Ph level",
"turbidity": "Turbidity",
"mg-per-liter": "Milligrams per liter",
"microsiemens-per-centimeter": "Microsiemens per centimeter",
@ -6305,9 +6307,9 @@
"milligrams-per-deciliter": "Milligrams per deciliter",
"g-force": "G-force",
"kilonewton": "Kilonewton",
"kilogram-force": "Kilogram-Force",
"pound-force": "Pound-Force",
"kilopound-force": "Kilopound-Force",
"kilogram-force": "Kilogram-force",
"pound-force": "Pound-force",
"kilopound-force": "Kilopound-force",
"dyne": "Dyne",
"poundal": "Poundal",
"kip": "Kip",
@ -6317,7 +6319,7 @@
"atmosphere": "Atmosphere",
"millibars": "Millibars",
"inch-of-mercury": "One inch of mercury",
"richter-scale": "Richter Scale",
"richter-scale": "Richter scale",
"nanosecond": "Nanosecond",
"microsecond": "Microsecond",
"millisecond": "Millisecond",
@ -6328,12 +6330,12 @@
"week": "Week",
"month": "Month",
"year": "Year",
"cubic-foot-per-minute": "Cubic Foot Per Minute",
"cubic-meters-per-hour": "Cubic Meters Per Hour",
"cubic-meters-per-second": "Cubic Meters Per Second",
"liter-per-second": "Liter Per Second",
"liter-per-minute": "Liter Per Minute",
"gallons-per-minute": "Gallons Per Minute",
"cubic-foot-per-minute": "Cubic foot per minute",
"cubic-meters-per-hour": "Cubic meters per hour",
"cubic-meters-per-second": "Cubic meters per second",
"liter-per-second": "Liter per second",
"liter-per-minute": "Liter per minute",
"gallons-per-minute": "Gallons per minute",
"cubic-foot-per-second": "Cubic foot per second",
"milliliters-per-minute": "Milliliters per minute",
"cubic-decimeter-per-second": "Cubic decimeter per second",
@ -6378,7 +6380,7 @@
"megafarad": "Megafarad",
"gigafarad": "Gigafarad",
"terfarad": "Terfarad",
"farad-per-meter": "Farad per Meter",
"farad-per-meter": "Farad per meter",
"tesla": "Tesla",
"gauss": "Gauss",
"kilogauss": "Kilogauss",
@ -6387,7 +6389,7 @@
"nanotesla": "Nanotesla",
"kilotesla": "Kilotesla",
"megatesla": "Megatesla",
"millitesla-square-meters": "millitesla square meters",
"millitesla-square-meters": "Millitesla square meters",
"gamma": "Gamma",
"lambda": "Lambda",
"square-meter-per-second": "Square meter per second",
@ -6406,25 +6408,25 @@
"kilogram-per-meter-second": "Kilogram per meter-second",
"tesla-square-meters": "Tesla square meters",
"maxwell": "Maxwell",
"tesla-per-meter": "Tesla per Meter",
"gauss-per-centimeter": "Gauss per Centimeter",
"tesla-per-meter": "Tesla per meter",
"gauss-per-centimeter": "Gauss per centimeter",
"weber": "Weber",
"microweber": "Microweber",
"milliweber": "Milliweber",
"gauss-square-centimeter": "Gauss-Square Centimeter",
"kilogauss-square-centimeter": "Kilogauss-Square Centimeter",
"gauss-square-centimeter": "Gauss-square centimeter",
"kilogauss-square-centimeter": "Kilogauss-square centimeter",
"henry": "Henry",
"millihenry": "Millihenry",
"microhenry": "Microhenry",
"nanohenry": "Nanohenry",
"henry-per-meter": "Henry per Meter",
"tesla-meter-per-ampere": "Tesla Meter per Ampere",
"gauss-per-oersted": "Gauss per Oersted",
"henry-per-meter": "Henry per meter",
"tesla-meter-per-ampere": "Tesla meter per ampere",
"gauss-per-oersted": "Gauss per oersted",
"kilogram-per-mole": "Kilogram per mole",
"gram-per-mole": "Gram per mole",
"milligram-per-mole": "Milligram per mole",
"joule-per-mole": "Joule per Mole",
"joule-per-mole-kelvin": "Joule per Mole-Kelvin",
"joule-per-mole": "Joule per mole",
"joule-per-mole-kelvin": "Joule per mole-kelvin",
"millivolts-per-meter": "Millivolts per meter",
"volts-per-meter": "Volts per meter",
"kilovolts-per-meter": "Kilovolts per meter",
@ -6435,7 +6437,7 @@
"rotation-per-minute": "Rotation per minute",
"degrees-brix": "Degrees brix",
"katal": "Katal",
"katal-per-cubic-metre": "Katal per Cubic Metre",
"katal-per-cubic-metre": "Katal per cubic metre",
"paris-inch": "Paris inch"
},
"user": {

Loading…
Cancel
Save