diff --git a/application/src/main/java/org/thingsboard/server/service/edge/EdgeContextComponent.java b/application/src/main/java/org/thingsboard/server/service/edge/EdgeContextComponent.java index 00ae4a45bd..1193f935a0 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/EdgeContextComponent.java +++ b/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) { diff --git a/application/src/main/java/org/thingsboard/server/service/edge/EdgeEventSourcingListener.java b/application/src/main/java/org/thingsboard/server/service/edge/EdgeEventSourcingListener.java index 7bafd53644..70c63a96cb 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/EdgeEventSourcingListener.java +++ b/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; } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtils.java b/application/src/main/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtils.java index 5f5fd771cf..192c56692d 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtils.java +++ b/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(); + } + } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java index 5671ffb2ab..eaef1f7c7d 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java +++ b/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 sessions = new ConcurrentHashMap<>(); private final ConcurrentMap sessionNewEventsLocks = new ConcurrentHashMap<>(); private final Map 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) { diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java index b6ecd848ad..be65dd13a1 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java +++ b/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 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() {} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeSyncCursor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeSyncCursor.java index 351c9b411b..adab9b812f 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeSyncCursor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeSyncCursor.java @@ -107,4 +107,5 @@ public class EdgeSyncCursor { currentIdx++; return edgeEventFetcher; } + } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/KafkaEdgeGrpcSession.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/KafkaEdgeGrpcSession.java index daffe9db11..ab0b42abb4 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/KafkaEdgeGrpcSession.java +++ b/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 diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessor.java index 4eadcfc5e8..a2243a88d2 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessor.java +++ b/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 processNotificationToRelatedEdges(TenantId tenantId, EntityId entityId, EdgeEventType type, - EdgeEventActionType actionType, EdgeId sourceEdgeId) { + protected ListenableFuture processNotificationToRelatedEdges(TenantId tenantId, EntityId ownerEntityId, EntityId entityId, EdgeEventType type, + EdgeEventActionType actionType, EdgeId sourceEdgeId) { List> futures = new ArrayList<>(); PageDataIterableByTenantIdEntityId 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)); diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/AssetEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/AssetEdgeProcessor.java index 47f4c11362..cba2b62af1 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/AssetEdgeProcessor.java +++ b/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)); diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/cf/BaseCalculatedFieldProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/cf/BaseCalculatedFieldProcessor.java new file mode 100644 index 0000000000..4ef6ec7ba2 --- /dev/null +++ b/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 calculatedFieldValidator; + + protected Pair 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); + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/cf/CalculatedFieldEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/cf/CalculatedFieldEdgeProcessor.java new file mode 100644 index 0000000000..cab4b5ecc1 --- /dev/null +++ b/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 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 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 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); + } + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/cf/CalculatedFieldProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/cf/CalculatedFieldProcessor.java new file mode 100644 index 0000000000..d21af858f0 --- /dev/null +++ b/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 processCalculatedFieldMsgFromEdge(TenantId tenantId, Edge edge, CalculatedFieldUpdateMsg calculatedFieldUpdateMsg); + +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/DeviceEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/DeviceEdgeProcessor.java index ab01f83cd8..763821f11c 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/DeviceEdgeProcessor.java +++ b/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)); diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/sync/DefaultEdgeRequestsService.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/sync/DefaultEdgeRequestsService.java index 6fc6d5bad9..66ff05b45a 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/sync/DefaultEdgeRequestsService.java +++ b/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 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 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> 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> findRelationByQuery(TenantId tenantId, Edge edge, EntityId entityId, EntitySearchDirection direction) { EntityRelationsQuery query = new EntityRelationsQuery(); query.setParameters(new RelationsSearchParameters(entityId, direction, 1, false)); diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/sync/EdgeRequestsService.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/sync/EdgeRequestsService.java index 2a115eeace..a147a7054b 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/sync/EdgeRequestsService.java +++ b/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 processRelationRequestMsg(TenantId tenantId, Edge edge, RelationRequestMsg relationRequestMsg); + ListenableFuture processCalculatedFieldRequestMsg(TenantId tenantId, Edge edge, CalculatedFieldRequestMsg calculatedFieldRequestMsg); + @Deprecated(since = "3.9.1", forRemoval = true) ListenableFuture processDeviceCredentialsRequestMsg(TenantId tenantId, Edge edge, DeviceCredentialsRequestMsg deviceCredentialsRequestMsg); @@ -46,4 +49,5 @@ public interface EdgeRequestsService { @Deprecated(since = "3.9.1", forRemoval = true) ListenableFuture processEntityViewsRequestMsg(TenantId tenantId, Edge edge, EntityViewsRequestMsg entityViewsRequestMsg); + } diff --git a/application/src/test/java/org/thingsboard/server/controller/TbResourceControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/TbResourceControllerTest.java index 569ff840ca..3b194de7b4 100644 --- a/application/src/test/java/org/thingsboard/server/controller/TbResourceControllerTest.java +++ b/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()) diff --git a/application/src/test/java/org/thingsboard/server/edge/AbstractEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/AbstractEdgeTest.java index c02e72a35f..84d879c993 100644 --- a/application/src/test/java/org/thingsboard/server/edge/AbstractEdgeTest.java +++ b/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 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; } diff --git a/application/src/test/java/org/thingsboard/server/edge/CalculatedFieldEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/CalculatedFieldEdgeTest.java new file mode 100644 index 0000000000..268e19345c --- /dev/null +++ b/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 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()); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/edge/imitator/EdgeImitator.java b/application/src/test/java/org/thingsboard/server/edge/imitator/EdgeImitator.java index 8b259cf8fc..16d10d9b6e 100644 --- a/application/src/test/java/org/thingsboard/server/edge/imitator/EdgeImitator.java +++ b/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())); } diff --git a/common/cache/src/main/java/org/thingsboard/server/cache/resourceInfo/ResourceInfoCacheKey.java b/common/cache/src/main/java/org/thingsboard/server/cache/resourceInfo/ResourceInfoCacheKey.java index cb19b36059..e1877ebe4e 100644 --- a/common/cache/src/main/java/org/thingsboard/server/cache/resourceInfo/ResourceInfoCacheKey.java +++ b/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(); } } diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/cf/CalculatedFieldService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/cf/CalculatedFieldService.java index 5101d6d57e..85cd8d24fd 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/cf/CalculatedFieldService.java +++ b/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 findCalculatedFieldIdsByEntityId(TenantId tenantId, EntityId entityId); List findCalculatedFieldsByEntityId(TenantId tenantId, EntityId entityId); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/edge/EdgeEventType.java b/common/data/src/main/java/org/thingsboard/server/common/data/edge/EdgeEventType.java index 6f1ae8150f..0d5c3f34ad 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/edge/EdgeEventType.java +++ b/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; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java index dcf59a4ea4..d23e4c078d 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java +++ b/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!"); } diff --git a/common/edge-api/src/main/java/org/thingsboard/edge/rpc/EdgeGrpcClient.java b/common/edge-api/src/main/java/org/thingsboard/edge/rpc/EdgeGrpcClient.java index 509b30feb4..e1ff386a32 100644 --- a/common/edge-api/src/main/java/org/thingsboard/edge/rpc/EdgeGrpcClient.java +++ b/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()); diff --git a/common/edge-api/src/main/proto/edge.proto b/common/edge-api/src/main/proto/edge.proto index 023ac00634..c805f42e8c 100644 --- a/common/edge-api/src/main/proto/edge.proto +++ b/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; } diff --git a/common/proto/src/main/java/org/thingsboard/server/common/util/ProtoUtils.java b/common/proto/src/main/java/org/thingsboard/server/common/util/ProtoUtils.java index bc96952ca7..3cded5a491 100644 --- a/common/proto/src/main/java/org/thingsboard/server/common/util/ProtoUtils.java +++ b/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); } diff --git a/common/proto/src/main/proto/queue.proto b/common/proto/src/main/proto/queue.proto index c413ecb3c1..2667838b60 100644 --- a/common/proto/src/main/proto/queue.proto +++ b/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 { diff --git a/dao/src/main/java/org/thingsboard/server/dao/cf/BaseCalculatedFieldService.java b/dao/src/main/java/org/thingsboard/server/dao/cf/BaseCalculatedFieldService.java index 0c5df18e80..c0cb886747 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/cf/BaseCalculatedFieldService.java +++ b/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 findCalculatedFieldIdsByEntityId(TenantId tenantId, EntityId entityId) { log.trace("Executing findCalculatedFieldIdsByEntityId [{}]", entityId); diff --git a/dao/src/main/java/org/thingsboard/server/dao/cf/CalculatedFieldDao.java b/dao/src/main/java/org/thingsboard/server/dao/cf/CalculatedFieldDao.java index aadae93893..d5465cb8a1 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/cf/CalculatedFieldDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/cf/CalculatedFieldDao.java @@ -35,6 +35,8 @@ public interface CalculatedFieldDao extends Dao { List findAll(); + CalculatedField findByEntityIdAndName(EntityId entityId, String name); + PageData findAll(PageLink pageLink); PageData findAllByTenantId(TenantId tenantId, PageLink pageLink); diff --git a/dao/src/main/java/org/thingsboard/server/dao/resource/BaseResourceService.java b/dao/src/main/java/org/thingsboard/server/dao/resource/BaseResourceService.java index bf73c59708..bf941256f5 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/resource/BaseResourceService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/resource/BaseResourceService.java @@ -263,7 +263,7 @@ public class BaseResourceService extends AbstractCachedEntityService 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 findCalculatedFieldIdsByTenantIdAndEntityId(UUID tenantId, UUID entityId); List findAllByTenantIdAndEntityId(UUID tenantId, UUID entityId); diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/cf/JpaCalculatedFieldDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/cf/JpaCalculatedFieldDao.java index 4bb52c29db..2632b0237b 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/cf/JpaCalculatedFieldDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/cf/JpaCalculatedFieldDao.java @@ -65,6 +65,11 @@ public class JpaCalculatedFieldDao extends JpaAbstractDao findAll(PageLink pageLink) { log.debug("Try to find calculated fields by pageLink [{}]", pageLink); diff --git a/ui-ngx/src/app/modules/home/components/rule-node/external/azure-iot-hub-config.component.html b/ui-ngx/src/app/modules/home/components/rule-node/external/azure-iot-hub-config.component.html index 79c4a2f00d..bab0c6b196 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/external/azure-iot-hub-config.component.html +++ b/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 }} - + + diff --git a/ui-ngx/src/app/modules/home/components/rule-node/external/azure-iot-hub-config.component.ts b/ui-ngx/src/app/modules/home/components/rule-node/external/azure-iot-hub-config.component.ts index 7dd934ccd9..dff5f06e54 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/external/azure-iot-hub-config.component.ts +++ b/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(); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/alarm/alarms-table-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/alarm/alarms-table-widget.component.ts index 45825a0294..b1c0f38ef2 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/alarm/alarms-table-widget.component.ts +++ b/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() { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/bar-chart-with-labels-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/chart/bar-chart-with-labels-widget.component.html index 0e0170c603..3f4835f475 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/bar-chart-with-labels-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/bar-chart-with-labels-widget.component.html @@ -19,7 +19,7 @@
@if (widgetComponent.dashboardWidget.showWidgetTitlePanel) {
- +
} @else { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/bar-chart-with-labels-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/bar-chart-with-labels-widget.component.ts index 746daf43b3..14e77ae738 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/bar-chart-with-labels-widget.component.ts +++ b/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; + showLegend: boolean; legendClass: string; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/range-chart-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/chart/range-chart-widget.component.html index 39112c484b..fc16fbdc13 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/range-chart-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/range-chart-widget.component.html @@ -19,7 +19,7 @@
@if (widgetComponent.dashboardWidget.showWidgetTitlePanel) {
- +
} @else { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/range-chart-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/range-chart-widget.component.ts index 1d1257c2e8..f471a9e84c 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/range-chart-widget.component.ts +++ b/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; + 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); } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/range-chart-widget.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/range-chart-widget.models.ts index f4210e2203..de59cc7fa3 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/range-chart-widget.models.ts +++ b/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 => { + decimals: number, units: TbUnit): DeepPartial => { let thresholds: DeepPartial[] = settings.showRangeThresholds ? getMarkPoints(rangeItems).map(item => ({ ...{type: ValueSourceType.constant, yAxisId: 'default', units, decimals, - value: valueConvertor(item)}, + value: item}, ...settings.rangeThreshold } as DeepPartial)) : []; 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, 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, 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++, diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart-widget.component.html index 1efcf30781..fe7335c92d 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart-widget.component.html @@ -19,7 +19,7 @@
@if (widgetComponent.dashboardWidget.showWidgetTitlePanel) {
- +
} @else { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart-widget.component.ts index 2fd12b1b98..2e9f297b4a 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart-widget.component.ts +++ b/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; + horizontalLegendPosition = false; showLegend: boolean; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.models.ts index b29f809fef..86d952f2e2 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.models.ts +++ b/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; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/entity/entities-table-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/entity/entities-table-widget.component.ts index 82537384a1..f95c8c26d7 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/entity/entities-table-widget.component.ts +++ b/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() { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget.component.html index e64ee71170..c0aceeabb0 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget.component.html @@ -19,7 +19,7 @@
@if (widgetComponent.dashboardWidget.showWidgetTitlePanel) {
- +
} @else { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget.component.ts index 4ef3c3cd45..013a6c88a9 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget.component.ts +++ b/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; + backgroundStyle$: Observable; overlayStyle: ComponentStyle = {}; padding: string; diff --git a/ui-ngx/src/app/shared/components/mqtt-version-select.component.ts b/ui-ngx/src/app/shared/components/mqtt-version-select.component.ts index 6bde856a7b..0a9b75be2d 100644 --- a/ui-ngx/src/app/shared/components/mqtt-version-select.component.ts +++ b/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; } diff --git a/ui-ngx/src/app/shared/models/units/speed.ts b/ui-ngx/src/app/shared/models/units/speed.ts index b930b0739c..78433c60ae 100644 --- a/ui-ngx/src/app/shared/models/units/speed.ts +++ b/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 = { ratio: 1 / 1.609344, @@ -37,6 +37,11 @@ const METRIC: TbMeasureUnits = { '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 = { 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'], diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index 3eee13c053..963bf769ee 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/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": {