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..24fa02f11a 100644 --- a/application/src/test/java/org/thingsboard/server/edge/AbstractEdgeTest.java +++ b/application/src/test/java/org/thingsboard/server/edge/AbstractEdgeTest.java @@ -565,7 +565,7 @@ 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 + 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()); 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..0f785c4063 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/edge/CalculatedFieldEdgeTest.java @@ -0,0 +1,228 @@ +/** + * 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.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()); + + // update calculatedField + 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 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/dao/src/main/java/org/thingsboard/server/dao/cf/BaseCalculatedFieldService.java b/dao/src/main/java/org/thingsboard/server/dao/cf/BaseCalculatedFieldService.java index 141adf49aa..5d4d3c54da 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 @@ -57,18 +57,23 @@ public class BaseCalculatedFieldService extends AbstractEntityService implements @Override public CalculatedField save(CalculatedField calculatedField) { - return doSave(calculatedField, true); + CalculatedField oldCalculatedField = calculatedFieldDataValidator.validate(calculatedField, CalculatedField::getTenantId); + + return doSave(calculatedField, oldCalculatedField); } @Override public CalculatedField save(CalculatedField calculatedField, boolean doValidate) { - return doSave(calculatedField, doValidate); + CalculatedField oldCalculatedField = null; + + if (doValidate) { + oldCalculatedField = calculatedFieldDataValidator.validate(calculatedField, CalculatedField::getTenantId); + } + + return doSave(calculatedField, oldCalculatedField); } - private CalculatedField doSave(CalculatedField calculatedField, boolean doValidate) { - if (doValidate) { - calculatedFieldDataValidator.validate(calculatedField, CalculatedField::getTenantId); - } + private CalculatedField doSave(CalculatedField calculatedField, CalculatedField oldCalculatedField) { try { TenantId tenantId = calculatedField.getTenantId(); log.trace("Executing save calculated field, [{}]", calculatedField); @@ -76,7 +81,7 @@ public class BaseCalculatedFieldService extends AbstractEntityService implements CalculatedField savedCalculatedField = calculatedFieldDao.save(tenantId, calculatedField); createOrUpdateCalculatedFieldLink(tenantId, savedCalculatedField); eventPublisher.publishEvent(SaveEntityEvent.builder().tenantId(savedCalculatedField.getTenantId()).entityId(savedCalculatedField.getId()) - .entity(savedCalculatedField).oldEntity(calculatedField).created(calculatedField.getId() == null).build()); + .entity(savedCalculatedField).oldEntity(oldCalculatedField).created(calculatedField.getId() == null).build()); return savedCalculatedField; } catch (Exception e) { checkConstraintViolation(e,