Merge branch 'master' into ai

# Conflicts:
#	common/data/src/main/java/org/thingsboard/server/common/data/sync/JsonTbEntity.java
This commit is contained in:
Dmytro Skarzhynets 2025-07-09 15:48:50 +03:00
commit 9e96bc5a02
No known key found for this signature in database
GPG Key ID: 2B51652F224037DF
117 changed files with 1728 additions and 615 deletions

View File

@ -14,48 +14,24 @@
-- limitations under the License.
--
-- UPDATE TENANT PROFILE CASSANDRA RATE LIMITS START
-- UPDATE OTA PACKAGE EXTERNAL ID START
UPDATE tenant_profile
SET profile_data = jsonb_set(
profile_data,
'{configuration}',
(
(profile_data -> 'configuration') - 'cassandraQueryTenantRateLimitsConfiguration'
||
COALESCE(
CASE
WHEN profile_data -> 'configuration' ->
'cassandraQueryTenantRateLimitsConfiguration' IS NOT NULL THEN
jsonb_build_object(
'cassandraReadQueryTenantCoreRateLimits',
profile_data -> 'configuration' -> 'cassandraQueryTenantRateLimitsConfiguration',
'cassandraWriteQueryTenantCoreRateLimits',
profile_data -> 'configuration' -> 'cassandraQueryTenantRateLimitsConfiguration',
'cassandraReadQueryTenantRuleEngineRateLimits',
profile_data -> 'configuration' -> 'cassandraQueryTenantRateLimitsConfiguration',
'cassandraWriteQueryTenantRuleEngineRateLimits',
profile_data -> 'configuration' -> 'cassandraQueryTenantRateLimitsConfiguration'
)
END,
'{}'::jsonb
)
)
)
WHERE profile_data -> 'configuration' ? 'cassandraQueryTenantRateLimitsConfiguration';
ALTER TABLE ota_package
ADD COLUMN IF NOT EXISTS external_id uuid;
ALTER TABLE ota_package
ADD CONSTRAINT ota_package_external_id_unq_key UNIQUE (tenant_id, external_id);
-- UPDATE TENANT PROFILE CASSANDRA RATE LIMITS END
-- UPDATE OTA PACKAGE EXTERNAL ID END
-- UPDATE NOTIFICATION RULE CASSANDRA RATE LIMITS START
-- DROP INDEXES THAT DUPLICATE UNIQUE CONSTRAINT START
UPDATE notification_rule
SET trigger_config = REGEXP_REPLACE(
trigger_config,
'"CASSANDRA_QUERIES"',
'"CASSANDRA_WRITE_QUERIES_CORE","CASSANDRA_READ_QUERIES_CORE","CASSANDRA_WRITE_QUERIES_RULE_ENGINE","CASSANDRA_READ_QUERIES_RULE_ENGINE","CASSANDRA_WRITE_QUERIES_MONOLITH","CASSANDRA_READ_QUERIES_MONOLITH"',
'g'
)
WHERE trigger_type = 'RATE_LIMITS'
AND trigger_config LIKE '%"CASSANDRA_QUERIES"%';
DROP INDEX IF EXISTS idx_device_external_id;
DROP INDEX IF EXISTS idx_device_profile_external_id;
DROP INDEX IF EXISTS idx_asset_external_id;
DROP INDEX IF EXISTS idx_entity_view_external_id;
DROP INDEX IF EXISTS idx_rule_chain_external_id;
DROP INDEX IF EXISTS idx_dashboard_external_id;
DROP INDEX IF EXISTS idx_customer_external_id;
DROP INDEX IF EXISTS idx_widgets_bundle_external_id;
-- UPDATE NOTIFICATION RULE CASSANDRA RATE LIMITS END
-- DROP INDEXES THAT DUPLICATE UNIQUE CONSTRAINT END

View File

@ -68,7 +68,7 @@ import java.util.stream.Collectors;
*/
@Slf4j
public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareMsgProcessor {
// (1 for result persistence + 1 for the state persistence )
// (1 for result persistence + 1 for the state persistence)
public static final int CALLBACKS_PER_CF = 2;
final TenantId tenantId;

View File

@ -24,13 +24,14 @@ import org.springframework.core.io.ByteArrayResource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import org.thingsboard.server.common.data.OtaPackage;
@ -49,8 +50,6 @@ import org.thingsboard.server.service.entitiy.ota.TbOtaPackageService;
import org.thingsboard.server.service.security.permission.Operation;
import org.thingsboard.server.service.security.permission.Resource;
import java.io.IOException;
import static org.springframework.http.MediaType.MULTIPART_FORM_DATA_VALUE;
import static org.thingsboard.server.controller.ControllerConstants.DEVICE_PROFILE_ID_PARAM_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.OTA_PACKAGE_DESCRIPTION;
@ -80,8 +79,7 @@ public class OtaPackageController extends BaseController {
@ApiOperation(value = "Download OTA Package (downloadOtaPackage)", notes = "Download OTA Package based on the provided OTA Package Id." + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority( 'TENANT_ADMIN')")
@RequestMapping(value = "/otaPackage/{otaPackageId}/download", method = RequestMethod.GET)
@ResponseBody
@GetMapping(value = "/otaPackage/{otaPackageId}/download")
public ResponseEntity<org.springframework.core.io.Resource> downloadOtaPackage(@Parameter(description = OTA_PACKAGE_ID_PARAM_DESCRIPTION)
@PathVariable(OTA_PACKAGE_ID) String strOtaPackageId) throws ThingsboardException {
checkParameter(OTA_PACKAGE_ID, strOtaPackageId);
@ -105,8 +103,7 @@ public class OtaPackageController extends BaseController {
notes = "Fetch the OTA Package Info object based on the provided OTA Package Id. " +
OTA_PACKAGE_INFO_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/otaPackage/info/{otaPackageId}", method = RequestMethod.GET)
@ResponseBody
@GetMapping(value = "/otaPackage/info/{otaPackageId}")
public OtaPackageInfo getOtaPackageInfoById(@Parameter(description = OTA_PACKAGE_ID_PARAM_DESCRIPTION)
@PathVariable(OTA_PACKAGE_ID) String strOtaPackageId) throws ThingsboardException {
checkParameter(OTA_PACKAGE_ID, strOtaPackageId);
@ -118,8 +115,7 @@ public class OtaPackageController extends BaseController {
notes = "Fetch the OTA Package object based on the provided OTA Package Id. " +
"The server checks that the OTA Package is owned by the same tenant. " + OTA_PACKAGE_DESCRIPTION + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/otaPackage/{otaPackageId}", method = RequestMethod.GET)
@ResponseBody
@GetMapping(value = "/otaPackage/{otaPackageId}")
public OtaPackage getOtaPackageById(@Parameter(description = OTA_PACKAGE_ID_PARAM_DESCRIPTION)
@PathVariable(OTA_PACKAGE_ID) String strOtaPackageId) throws ThingsboardException {
checkParameter(OTA_PACKAGE_ID, strOtaPackageId);
@ -134,10 +130,9 @@ public class OtaPackageController extends BaseController {
"Referencing non-existing OTA Package Id will cause 'Not Found' error. " +
"\n\nOTA Package combination of the title with the version is unique in the scope of tenant. " + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/otaPackage", method = RequestMethod.POST)
@ResponseBody
@PostMapping(value = "/otaPackage")
public OtaPackageInfo saveOtaPackageInfo(@Parameter(description = "A JSON value representing the OTA Package.")
@RequestBody SaveOtaPackageInfoRequest otaPackageInfo) throws ThingsboardException {
@RequestBody SaveOtaPackageInfoRequest otaPackageInfo) throws Exception {
otaPackageInfo.setTenantId(getTenantId());
checkEntity(otaPackageInfo.getId(), otaPackageInfo, Resource.OTA_PACKAGE);
@ -148,8 +143,7 @@ public class OtaPackageController extends BaseController {
notes = "Update the OTA Package. Adds the date to the existing OTA Package Info" + TENANT_AUTHORITY_PARAGRAPH,
requestBody = @io.swagger.v3.oas.annotations.parameters.RequestBody(content = @Content(mediaType = MULTIPART_FORM_DATA_VALUE)))
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/otaPackage/{otaPackageId}", method = RequestMethod.POST, consumes = MULTIPART_FORM_DATA_VALUE)
@ResponseBody
@PostMapping(value = "/otaPackage/{otaPackageId}", consumes = MULTIPART_FORM_DATA_VALUE)
public OtaPackageInfo saveOtaPackageData(@Parameter(description = OTA_PACKAGE_ID_PARAM_DESCRIPTION)
@PathVariable(OTA_PACKAGE_ID) String strOtaPackageId,
@Parameter(description = "OTA Package checksum. For example, '0xd87f7e0c'")
@ -157,7 +151,7 @@ public class OtaPackageController extends BaseController {
@Parameter(description = "OTA Package checksum algorithm.", schema = @Schema(allowableValues = {"MD5", "SHA256", "SHA384", "SHA512", "CRC32", "MURMUR3_32", "MURMUR3_128"}))
@RequestParam(CHECKSUM_ALGORITHM) String checksumAlgorithmStr,
@Parameter(description = "OTA Package data.")
@RequestPart MultipartFile file) throws ThingsboardException, IOException {
@RequestPart MultipartFile file) throws Exception {
checkParameter(OTA_PACKAGE_ID, strOtaPackageId);
checkParameter(CHECKSUM_ALGORITHM, checksumAlgorithmStr);
OtaPackageId otaPackageId = new OtaPackageId(toUUID(strOtaPackageId));
@ -172,8 +166,7 @@ public class OtaPackageController extends BaseController {
notes = "Returns a page of OTA Package Info objects owned by tenant. " +
PAGE_DATA_PARAMETERS + OTA_PACKAGE_INFO_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/otaPackages", method = RequestMethod.GET)
@ResponseBody
@GetMapping(value = "/otaPackages")
public PageData<OtaPackageInfo> getOtaPackages(@Parameter(description = PAGE_SIZE_DESCRIPTION, required = true)
@RequestParam int pageSize,
@Parameter(description = PAGE_NUMBER_DESCRIPTION, required = true)
@ -192,8 +185,7 @@ public class OtaPackageController extends BaseController {
notes = "Returns a page of OTA Package Info objects owned by tenant. " +
PAGE_DATA_PARAMETERS + OTA_PACKAGE_INFO_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/otaPackages/{deviceProfileId}/{type}", method = RequestMethod.GET)
@ResponseBody
@GetMapping(value = "/otaPackages/{deviceProfileId}/{type}")
public PageData<OtaPackageInfo> getOtaPackages(@Parameter(description = DEVICE_PROFILE_ID_PARAM_DESCRIPTION)
@PathVariable("deviceProfileId") String strDeviceProfileId,
@Parameter(description = "OTA Package type.", schema = @Schema(allowableValues = {"FIRMWARE", "SOFTWARE"}))
@ -219,8 +211,7 @@ public class OtaPackageController extends BaseController {
notes = "Deletes the OTA Package. Referencing non-existing OTA Package Id will cause an error. " +
"Can't delete the OTA Package if it is referenced by existing devices or device profile." + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/otaPackage/{otaPackageId}", method = RequestMethod.DELETE)
@ResponseBody
@DeleteMapping(value = "/otaPackage/{otaPackageId}")
public void deleteOtaPackage(@Parameter(description = OTA_PACKAGE_ID_PARAM_DESCRIPTION)
@PathVariable("otaPackageId") String strOtaPackageId) throws ThingsboardException {
checkParameter(OTA_PACKAGE_ID, strOtaPackageId);

View File

@ -33,11 +33,11 @@ import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
@ -65,25 +65,17 @@ import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.UUIDBased;
import org.thingsboard.server.common.data.kv.Aggregation;
import org.thingsboard.server.common.data.kv.AttributeKvEntry;
import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry;
import org.thingsboard.server.common.data.kv.BaseDeleteTsKvQuery;
import org.thingsboard.server.common.data.kv.BasicTsKvEntry;
import org.thingsboard.server.common.data.kv.BooleanDataEntry;
import org.thingsboard.server.common.data.kv.DataType;
import org.thingsboard.server.common.data.kv.DeleteTsKvQuery;
import org.thingsboard.server.common.data.kv.DoubleDataEntry;
import org.thingsboard.server.common.data.kv.IntervalType;
import org.thingsboard.server.common.data.kv.JsonDataEntry;
import org.thingsboard.server.common.data.kv.KvEntry;
import org.thingsboard.server.common.data.kv.LongDataEntry;
import org.thingsboard.server.common.data.kv.StringDataEntry;
import org.thingsboard.server.common.data.kv.TsKvEntry;
import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration;
import org.thingsboard.server.common.msg.rule.engine.DeviceAttributesEventNotificationMsg;
import org.thingsboard.server.config.annotations.ApiOperation;
import org.thingsboard.server.dao.timeseries.TimeseriesService;
import org.thingsboard.server.exception.InvalidParametersException;
import org.thingsboard.server.exception.UncheckedApiException;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.security.AccessValidator;
import org.thingsboard.server.service.security.model.SecurityUser;
@ -156,9 +148,6 @@ public class TelemetryController extends BaseController {
@Autowired
private TbTelemetryService tbTelemetryService;
@Value("${transport.json.max_string_value_length:0}")
private int maxStringValueLength;
private ExecutorService executor;
@PostConstruct
@ -314,10 +303,10 @@ public class TelemetryController extends BaseController {
@Parameter(description = "A string value representing the timezone that will be used to calculate exact timestamps for 'WEEK', 'WEEK_ISO', 'MONTH' and 'QUARTER' interval types.")
@RequestParam(name = "timeZone", required = false) String timeZone,
@Parameter(description = "An integer value that represents a max number of time series data points to fetch." +
" This parameter is used only in the case if 'agg' parameter is set to 'NONE'.", schema = @Schema(defaultValue = "100"))
" This parameter is used only in the case if 'agg' parameter is set to 'NONE'.", schema = @Schema(defaultValue = "100"))
@RequestParam(name = "limit", defaultValue = "100") Integer limit,
@Parameter(description = "A string value representing the aggregation function. " +
"If the interval is not specified, 'agg' parameter will use 'NONE' value.",
"If the interval is not specified, 'agg' parameter will use 'NONE' value.",
schema = @Schema(allowableValues = {"MIN", "MAX", "AVG", "SUM", "COUNT", "NONE"}))
@RequestParam(name = "agg", defaultValue = "NONE") String aggStr,
@Parameter(description = SORT_ORDER_DESCRIPTION, schema = @Schema(allowableValues = {"ASC", "DESC"}))
@ -337,20 +326,21 @@ public class TelemetryController extends BaseController {
+ TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = SAVE_ATTIRIBUTES_STATUS_OK +
"Platform creates an audit log event about device attributes updates with action type 'ATTRIBUTES_UPDATED', " +
"and also sends event msg to the rule engine with msg type 'ATTRIBUTES_UPDATED'."),
"Platform creates an audit log event about device attributes updates with action type 'ATTRIBUTES_UPDATED', " +
"and also sends event msg to the rule engine with msg type 'ATTRIBUTES_UPDATED'."),
@ApiResponse(responseCode = "400", description = SAVE_ATTIRIBUTES_STATUS_BAD_REQUEST),
@ApiResponse(responseCode = "401", description = "User is not authorized to save device attributes for selected device. Most likely, User belongs to different Customer or Tenant."),
@ApiResponse(responseCode = "500", description = "The exception was thrown during processing the request. " +
"Platform creates an audit log event about device attributes updates with action type 'ATTRIBUTES_UPDATED' that includes an error stacktrace."),
"Platform creates an audit log event about device attributes updates with action type 'ATTRIBUTES_UPDATED' that includes an error stacktrace."),
})
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/{deviceId}/{scope}", method = RequestMethod.POST)
@ResponseBody
public DeferredResult<ResponseEntity> saveDeviceAttributes(
@Parameter(description = DEVICE_ID_PARAM_DESCRIPTION, required = true) @PathVariable("deviceId") String deviceIdStr,
@Parameter(description = ATTRIBUTES_SCOPE_DESCRIPTION, schema = @Schema(allowableValues = {"SERVER_SCOPE", "SHARED_SCOPE"}, requiredMode = Schema.RequiredMode.REQUIRED)) @PathVariable("scope") AttributeScope scope,
@io.swagger.v3.oas.annotations.parameters.RequestBody(description = ATTRIBUTES_JSON_REQUEST_DESCRIPTION, required = true) @RequestBody JsonNode request) throws ThingsboardException {
@PostMapping(value = "/{deviceId}/{scope}")
public DeferredResult<ResponseEntity> saveDeviceAttributes(@Parameter(description = DEVICE_ID_PARAM_DESCRIPTION, required = true)
@PathVariable("deviceId") String deviceIdStr,
@Parameter(description = ATTRIBUTES_SCOPE_DESCRIPTION, schema = @Schema(allowableValues = {"SERVER_SCOPE", "SHARED_SCOPE"}, requiredMode = Schema.RequiredMode.REQUIRED))
@PathVariable("scope") AttributeScope scope,
@io.swagger.v3.oas.annotations.parameters.RequestBody(description = ATTRIBUTES_JSON_REQUEST_DESCRIPTION, required = true)
@RequestBody String request) throws ThingsboardException {
EntityId entityId = EntityIdFactory.getByTypeAndUuid(EntityType.DEVICE, deviceIdStr);
return saveAttributes(getTenantId(), entityId, scope, request);
}
@ -367,13 +357,15 @@ public class TelemetryController extends BaseController {
@ApiResponse(responseCode = "500", description = SAVE_ENTITY_ATTRIBUTES_STATUS_INTERNAL_SERVER_ERROR),
})
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/{entityType}/{entityId}/{scope}", method = RequestMethod.POST)
@ResponseBody
public DeferredResult<ResponseEntity> saveEntityAttributesV1(
@Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, schema = @Schema(defaultValue = "DEVICE")) @PathVariable("entityType") String entityType,
@Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable("entityId") String entityIdStr,
@Parameter(description = ATTRIBUTES_SCOPE_DESCRIPTION, schema = @Schema(allowableValues = {"SERVER_SCOPE", "SHARED_SCOPE"})) @PathVariable("scope") AttributeScope scope,
@io.swagger.v3.oas.annotations.parameters.RequestBody(description = ATTRIBUTES_JSON_REQUEST_DESCRIPTION, required = true) @RequestBody JsonNode request) throws ThingsboardException {
@PostMapping(value = "/{entityType}/{entityId}/{scope}")
public DeferredResult<ResponseEntity> saveEntityAttributesV1(@Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, schema = @Schema(defaultValue = "DEVICE"))
@PathVariable("entityType") String entityType,
@Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true)
@PathVariable("entityId") String entityIdStr,
@Parameter(description = ATTRIBUTES_SCOPE_DESCRIPTION, schema = @Schema(allowableValues = {"SERVER_SCOPE", "SHARED_SCOPE"}))
@PathVariable("scope") AttributeScope scope,
@io.swagger.v3.oas.annotations.parameters.RequestBody(description = ATTRIBUTES_JSON_REQUEST_DESCRIPTION, required = true)
@RequestBody String request) throws ThingsboardException {
EntityId entityId = EntityIdFactory.getByTypeAndId(entityType, entityIdStr);
return saveAttributes(getTenantId(), entityId, scope, request);
}
@ -390,13 +382,15 @@ public class TelemetryController extends BaseController {
@ApiResponse(responseCode = "500", description = SAVE_ENTITY_ATTRIBUTES_STATUS_INTERNAL_SERVER_ERROR),
})
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/{entityType}/{entityId}/attributes/{scope}", method = RequestMethod.POST)
@ResponseBody
public DeferredResult<ResponseEntity> saveEntityAttributesV2(
@Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, schema = @Schema(defaultValue = "DEVICE")) @PathVariable("entityType") String entityType,
@Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable("entityId") String entityIdStr,
@Parameter(description = ATTRIBUTES_SCOPE_DESCRIPTION, schema = @Schema(allowableValues = {"SERVER_SCOPE", "SHARED_SCOPE"}, requiredMode = Schema.RequiredMode.REQUIRED)) @PathVariable("scope") AttributeScope scope,
@io.swagger.v3.oas.annotations.parameters.RequestBody(description = ATTRIBUTES_JSON_REQUEST_DESCRIPTION, required = true) @RequestBody JsonNode request) throws ThingsboardException {
@PostMapping(value = "/{entityType}/{entityId}/attributes/{scope}")
public DeferredResult<ResponseEntity> saveEntityAttributesV2(@Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, schema = @Schema(defaultValue = "DEVICE"))
@PathVariable("entityType") String entityType,
@Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true)
@PathVariable("entityId") String entityIdStr,
@Parameter(description = ATTRIBUTES_SCOPE_DESCRIPTION, schema = @Schema(allowableValues = {"SERVER_SCOPE", "SHARED_SCOPE"}, requiredMode = Schema.RequiredMode.REQUIRED))
@PathVariable("scope") AttributeScope scope,
@io.swagger.v3.oas.annotations.parameters.RequestBody(description = ATTRIBUTES_JSON_REQUEST_DESCRIPTION, required = true)
@RequestBody String request) throws ThingsboardException {
EntityId entityId = EntityIdFactory.getByTypeAndId(entityType, entityIdStr);
return saveAttributes(getTenantId(), entityId, scope, request);
}
@ -460,11 +454,11 @@ public class TelemetryController extends BaseController {
TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "Time series for the selected keys in the request was removed. " +
"Platform creates an audit log event about entity time series removal with action type 'TIMESERIES_DELETED'."),
"Platform creates an audit log event about entity time series removal with action type 'TIMESERIES_DELETED'."),
@ApiResponse(responseCode = "400", description = "Platform returns a bad request in case if keys list is empty or start and end timestamp values is empty when deleteAllDataForKeys is set to false."),
@ApiResponse(responseCode = "401", description = "User is not authorized to delete entity time series for selected entity. Most likely, User belongs to different Customer or Tenant."),
@ApiResponse(responseCode = "500", description = "The exception was thrown during processing the request. " +
"Platform creates an audit log event about entity time series removal with action type 'TIMESERIES_DELETED' that includes an error stacktrace."),
"Platform creates an audit log event about entity time series removal with action type 'TIMESERIES_DELETED' that includes an error stacktrace."),
})
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/{entityType}/{entityId}/timeseries/delete", method = RequestMethod.DELETE)
@ -541,11 +535,11 @@ public class TelemetryController extends BaseController {
"Referencing a non-existing Device Id will cause an error" + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "Device attributes was removed for the selected keys in the request. " +
"Platform creates an audit log event about device attributes removal with action type 'ATTRIBUTES_DELETED'."),
"Platform creates an audit log event about device attributes removal with action type 'ATTRIBUTES_DELETED'."),
@ApiResponse(responseCode = "400", description = "Platform returns a bad request in case if keys or scope are not specified."),
@ApiResponse(responseCode = "401", description = "User is not authorized to delete device attributes for selected entity. Most likely, User belongs to different Customer or Tenant."),
@ApiResponse(responseCode = "500", description = "The exception was thrown during processing the request. " +
"Platform creates an audit log event about device attributes removal with action type 'ATTRIBUTES_DELETED' that includes an error stacktrace."),
"Platform creates an audit log event about device attributes removal with action type 'ATTRIBUTES_DELETED' that includes an error stacktrace."),
})
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/{deviceId}/{scope}", method = RequestMethod.DELETE)
@ -563,11 +557,11 @@ public class TelemetryController extends BaseController {
INVALID_ENTITY_ID_OR_ENTITY_TYPE_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "Entity attributes was removed for the selected keys in the request. " +
"Platform creates an audit log event about entity attributes removal with action type 'ATTRIBUTES_DELETED'."),
"Platform creates an audit log event about entity attributes removal with action type 'ATTRIBUTES_DELETED'."),
@ApiResponse(responseCode = "400", description = "Platform returns a bad request in case if keys or scope are not specified."),
@ApiResponse(responseCode = "401", description = "User is not authorized to delete entity attributes for selected entity. Most likely, User belongs to different Customer or Tenant."),
@ApiResponse(responseCode = "500", description = "The exception was thrown during processing the request. " +
"Platform creates an audit log event about entity attributes removal with action type 'ATTRIBUTES_DELETED' that includes an error stacktrace."),
"Platform creates an audit log event about entity attributes removal with action type 'ATTRIBUTES_DELETED' that includes an error stacktrace."),
})
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/{entityType}/{entityId}/{scope}", method = RequestMethod.DELETE)
@ -616,18 +610,24 @@ public class TelemetryController extends BaseController {
});
}
private DeferredResult<ResponseEntity> saveAttributes(TenantId srcTenantId, EntityId entityIdSrc, AttributeScope scope, JsonNode json) throws ThingsboardException {
private DeferredResult<ResponseEntity> saveAttributes(TenantId srcTenantId, EntityId entityIdSrc, AttributeScope scope, String jsonStr) throws ThingsboardException {
if (AttributeScope.SERVER_SCOPE != scope && AttributeScope.SHARED_SCOPE != scope) {
return getImmediateDeferredResult("Invalid scope: " + scope, HttpStatus.BAD_REQUEST);
}
if (json.isObject()) {
List<AttributeKvEntry> attributes = extractRequestAttributes(json);
JsonElement json;
try {
json = JsonParser.parseString(jsonStr);
} catch (Exception e) {
return getImmediateDeferredResult("Invalid JSON", HttpStatus.BAD_REQUEST);
}
if (json.isJsonObject()) {
List<AttributeKvEntry> attributes = JsonConverter.convertToAttributes(json);
if (attributes.isEmpty()) {
return getImmediateDeferredResult("No attributes data found in request body!", HttpStatus.BAD_REQUEST);
}
for (AttributeKvEntry attributeKvEntry : attributes) {
if (attributeKvEntry.getKey().isEmpty() || attributeKvEntry.getKey().trim().length() == 0) {
return getImmediateDeferredResult("Key cannot be empty or contains only spaces", HttpStatus.BAD_REQUEST);
if (attributeKvEntry.getKey().isBlank()) {
return getImmediateDeferredResult("Key cannot be blank", HttpStatus.BAD_REQUEST);
}
}
SecurityUser user = getCurrentUser();
@ -885,43 +885,6 @@ public class TelemetryController extends BaseController {
return result;
}
private List<AttributeKvEntry> extractRequestAttributes(JsonNode jsonNode) {
long ts = System.currentTimeMillis();
List<AttributeKvEntry> attributes = new ArrayList<>();
jsonNode.fields().forEachRemaining(entry -> {
String key = entry.getKey();
JsonNode value = entry.getValue();
if (entry.getValue().isObject() || entry.getValue().isArray()) {
attributes.add(new BaseAttributeKvEntry(new JsonDataEntry(key, toJsonStr(value)), ts));
} else if (entry.getValue().isTextual()) {
if (maxStringValueLength > 0 && entry.getValue().textValue().length() > maxStringValueLength) {
String message = String.format("String value length [%d] for key [%s] is greater than maximum allowed [%d]", entry.getValue().textValue().length(), key, maxStringValueLength);
throw new UncheckedApiException(new InvalidParametersException(message));
}
attributes.add(new BaseAttributeKvEntry(new StringDataEntry(key, value.textValue()), ts));
} else if (entry.getValue().isBoolean()) {
attributes.add(new BaseAttributeKvEntry(new BooleanDataEntry(key, value.booleanValue()), ts));
} else if (entry.getValue().isDouble()) {
attributes.add(new BaseAttributeKvEntry(new DoubleDataEntry(key, value.doubleValue()), ts));
} else if (entry.getValue().isNumber()) {
if (entry.getValue().isBigInteger()) {
throw new UncheckedApiException(new InvalidParametersException("Big integer values are not supported!"));
} else {
attributes.add(new BaseAttributeKvEntry(new LongDataEntry(key, value.longValue()), ts));
}
}
});
return attributes;
}
private String toJsonStr(JsonNode value) {
try {
return JacksonUtil.toString(value);
} catch (IllegalArgumentException e) {
throw new JsonParseException("Can't parse jsonValue: " + value, e);
}
}
private JsonNode toJsonNode(String value) {
try {
return JacksonUtil.toJsonNode(value);

View File

@ -116,7 +116,6 @@ public class ThingsboardInstallService {
entityDatabaseSchemaService.createDatabaseIndexes();
// TODO: cleanup update code after each release
systemDataLoaderService.updateDefaultNotificationConfigs(false);
// Runs upgrade scripts that are not possible in plain SQL.
dataUpdateService.updateData();

View File

@ -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) {

View File

@ -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;
}

View File

@ -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();
}
}

View File

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

View File

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

View File

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

View File

@ -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

View File

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

View File

@ -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));

View File

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

View File

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

View File

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

View File

@ -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));

View File

@ -266,7 +266,7 @@ public abstract class BaseTelemetryProcessor extends BaseEdgeProcessor {
SettableFuture<Void> futureToSet = SettableFuture.create();
JsonObject json = JsonUtils.getJsonObject(msg.getKvList());
AttributeScope scope = AttributeScope.valueOf(metaData.getValue(DataConstants.SCOPE));
List<AttributeKvEntry> attributes = new ArrayList<>(JsonConverter.convertToAttributes(json, ts));
List<AttributeKvEntry> attributes = JsonConverter.convertToAttributes(json, ts);
ListenableFuture<List<AttributeKvEntry>> future = filterAttributesByTs(tenantId, entityId, scope, attributes);
Futures.addCallback(future, new FutureCallback<>() {
@Override
@ -314,7 +314,7 @@ public abstract class BaseTelemetryProcessor extends BaseEdgeProcessor {
SettableFuture<Void> futureToSet = SettableFuture.create();
JsonObject json = JsonUtils.getJsonObject(msg.getKvList());
AttributeScope scope = AttributeScope.valueOf(metaData.getValue(DataConstants.SCOPE));
List<AttributeKvEntry> attributes = new ArrayList<>(JsonConverter.convertToAttributes(json, ts));
List<AttributeKvEntry> attributes = JsonConverter.convertToAttributes(json, ts);
ListenableFuture<List<AttributeKvEntry>> future = filterAttributesByTs(tenantId, entityId, scope, attributes);
Futures.addCallback(future, new FutureCallback<>() {
@Override

View File

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

View File

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

View File

@ -110,4 +110,5 @@ public class DefaultTbOtaPackageService extends AbstractTbEntityService implemen
throw e;
}
}
}

View File

@ -32,7 +32,7 @@ public class DefaultDatabaseSchemaSettingsService implements DatabaseSchemaSetti
// This list should include all versions which are compatible for the upgrade.
// The compatibility cycle usually breaks when we have some scripts written in Java that may not work after new release.
private static final List<String> SUPPORTED_VERSIONS_FOR_UPGRADE = List.of("4.0.0", "4.0.1");
private static final List<String> SUPPORTED_VERSIONS_FOR_UPGRADE = List.of("4.1.0");
private final ProjectInfo projectInfo;
private final JdbcTemplate jdbcTemplate;

View File

@ -20,25 +20,17 @@ import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.collect.Lists;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.TenantProfile;
import org.thingsboard.server.common.data.alarm.AlarmSeverity;
import org.thingsboard.server.common.data.id.RuleChainId;
import org.thingsboard.server.common.data.id.RuleNodeId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageDataIterable;
import org.thingsboard.server.common.data.query.DynamicValue;
import org.thingsboard.server.common.data.query.FilterPredicateValue;
import org.thingsboard.server.common.data.relation.EntityRelation;
import org.thingsboard.server.common.data.relation.RelationTypeGroup;
import org.thingsboard.server.common.data.rule.RuleNode;
import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration;
import org.thingsboard.server.dao.relation.RelationService;
import org.thingsboard.server.dao.rule.RuleChainService;
import org.thingsboard.server.dao.tenant.TenantProfileService;
import org.thingsboard.server.service.component.ComponentDiscoveryService;
import org.thingsboard.server.service.component.RuleNodeClassInfo;
import org.thingsboard.server.service.install.DbUpgradeExecutorService;
@ -46,110 +38,29 @@ import org.thingsboard.server.utils.TbNodeUpgradeUtils;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.ExecutionException;
import static org.thingsboard.server.dao.rule.BaseRuleChainService.TB_RULE_CHAIN_INPUT_NODE;
@Service
@Profile("install")
@Slf4j
@RequiredArgsConstructor
public class DefaultDataUpdateService implements DataUpdateService {
private static final int MAX_PENDING_SAVE_RULE_NODE_FUTURES = 256;
private static final int DEFAULT_PAGE_SIZE = 1024;
@Autowired
private RuleChainService ruleChainService;
@Autowired
private RelationService relationService;
@Autowired
private ComponentDiscoveryService componentDiscoveryService;
@Autowired
private DbUpgradeExecutorService executorService;
@Autowired
private TenantProfileService tenantProfileService;
private final RuleChainService ruleChainService;
private final ComponentDiscoveryService componentDiscoveryService;
private final DbUpgradeExecutorService executorService;
@Override
public void updateData() throws Exception {
log.info("Updating data ...");
//TODO: should be cleaned after each release
updateInputNodes();
deduplicateRateLimitsPerSecondsConfigurations();
log.info("Data updated.");
}
private void deduplicateRateLimitsPerSecondsConfigurations() {
log.info("Starting update of tenant profiles...");
int totalProfiles = 0;
int updatedTenantProfiles = 0;
int skippedProfiles = 0;
int failedProfiles = 0;
var tenantProfiles = new PageDataIterable<>(
pageLink -> tenantProfileService.findTenantProfiles(TenantId.SYS_TENANT_ID, pageLink), 1024);
for (TenantProfile tenantProfile : tenantProfiles) {
totalProfiles++;
String profileName = tenantProfile.getName();
UUID profileId = tenantProfile.getId().getId();
try {
Optional<DefaultTenantProfileConfiguration> profileConfiguration = tenantProfile.getProfileConfiguration();
if (profileConfiguration.isEmpty()) {
log.debug("[{}][{}] Skipping tenant profile with non-default configuration.", profileId, profileName);
skippedProfiles++;
continue;
}
DefaultTenantProfileConfiguration defaultTenantProfileConfiguration = profileConfiguration.get();
defaultTenantProfileConfiguration.deduplicateRateLimitsConfigs();
tenantProfileService.saveTenantProfile(TenantId.SYS_TENANT_ID, tenantProfile);
updatedTenantProfiles++;
log.debug("[{}][{}] Successfully updated tenant profile.", profileId, profileName);
} catch (Exception e) {
log.error("[{}][{}] Failed to updated tenant profile: ", profileId, profileName, e);
failedProfiles++;
}
}
log.info("Tenant profiles update completed. Total: {}, Updated: {}, Skipped: {}, Failed: {}",
totalProfiles, updatedTenantProfiles, skippedProfiles, failedProfiles);
}
private void updateInputNodes() {
log.info("Creating relations for input nodes...");
int n = 0;
var inputNodes = new PageDataIterable<>(pageLink -> ruleChainService.findAllRuleNodesByType(TB_RULE_CHAIN_INPUT_NODE, pageLink), 1024);
for (RuleNode inputNode : inputNodes) {
try {
RuleChainId targetRuleChainId = Optional.ofNullable(inputNode.getConfiguration().get("ruleChainId"))
.filter(JsonNode::isTextual).map(JsonNode::asText).map(id -> new RuleChainId(UUID.fromString(id)))
.orElse(null);
if (targetRuleChainId == null) {
continue;
}
EntityRelation relation = new EntityRelation();
relation.setFrom(inputNode.getRuleChainId());
relation.setTo(targetRuleChainId);
relation.setType(EntityRelation.USES_TYPE);
relation.setTypeGroup(RelationTypeGroup.COMMON);
relationService.saveRelation(TenantId.SYS_TENANT_ID, relation);
n++;
} catch (Exception e) {
log.error("Failed to save relation for input node: {}", inputNode, e);
}
}
log.info("Created {} relations for input nodes", n);
}
@Override
public void upgradeRuleNodes() {
int totalRuleNodesUpgraded = 0;

View File

@ -67,7 +67,7 @@ public class DefaultEntitiesExportImportService implements EntitiesExportImportS
protected static final List<EntityType> SUPPORTED_ENTITY_TYPES = List.of(
EntityType.CUSTOMER, EntityType.RULE_CHAIN, EntityType.TB_RESOURCE,
EntityType.DASHBOARD, EntityType.ASSET_PROFILE, EntityType.ASSET,
EntityType.DEVICE_PROFILE, EntityType.DEVICE,
EntityType.DEVICE_PROFILE, EntityType.OTA_PACKAGE, EntityType.DEVICE,
EntityType.ENTITY_VIEW, EntityType.WIDGET_TYPE, EntityType.WIDGETS_BUNDLE,
EntityType.NOTIFICATION_TEMPLATE, EntityType.NOTIFICATION_TARGET, EntityType.NOTIFICATION_RULE,
EntityType.AI_MODEL

View File

@ -38,6 +38,8 @@ public class DeviceExportService extends BaseEntityExportService<DeviceId, Devic
protected void setRelatedEntities(EntitiesExportCtx<?> ctx, Device device, DeviceExportData exportData) {
device.setCustomerId(getExternalIdOrElseInternal(ctx, device.getCustomerId()));
device.setDeviceProfileId(getExternalIdOrElseInternal(ctx, device.getDeviceProfileId()));
device.setFirmwareId(getExternalIdOrElseInternal(ctx, device.getFirmwareId()));
device.setSoftwareId(getExternalIdOrElseInternal(ctx, device.getSoftwareId()));
if (ctx.getSettings().isExportCredentials()) {
var credentials = deviceCredentialsService.findDeviceCredentialsByDeviceId(ctx.getTenantId(), device.getId());
credentials.setId(null);

View File

@ -34,6 +34,8 @@ public class DeviceProfileExportService extends BaseEntityExportService<DevicePr
deviceProfile.setDefaultDashboardId(getExternalIdOrElseInternal(ctx, deviceProfile.getDefaultDashboardId()));
deviceProfile.setDefaultRuleChainId(getExternalIdOrElseInternal(ctx, deviceProfile.getDefaultRuleChainId()));
deviceProfile.setDefaultEdgeRuleChainId(getExternalIdOrElseInternal(ctx, deviceProfile.getDefaultEdgeRuleChainId()));
deviceProfile.setFirmwareId(getExternalIdOrElseInternal(ctx, deviceProfile.getFirmwareId()));
deviceProfile.setSoftwareId(getExternalIdOrElseInternal(ctx, deviceProfile.getSoftwareId()));
}
@Override

View File

@ -0,0 +1,49 @@
/**
* 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.sync.ie.exporting.impl;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.OtaPackage;
import org.thingsboard.server.common.data.id.OtaPackageId;
import org.thingsboard.server.common.data.sync.ie.OtaPackageExportData;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.sync.vc.data.EntitiesExportCtx;
import java.util.Set;
@Service
@TbCoreComponent
@RequiredArgsConstructor
public class OtaPackageExportService extends BaseEntityExportService<OtaPackageId, OtaPackage, OtaPackageExportData> {
@Override
protected void setRelatedEntities(EntitiesExportCtx<?> ctx, OtaPackage otaPackage, OtaPackageExportData exportData) {
otaPackage.setDeviceProfileId(getExternalIdOrElseInternal(ctx, otaPackage.getDeviceProfileId()));
}
@Override
protected OtaPackageExportData newExportData() {
return new OtaPackageExportData();
}
@Override
public Set<EntityType> getSupportedEntityTypes() {
return Set.of(EntityType.OTA_PACKAGE);
}
}

View File

@ -67,7 +67,6 @@ import org.thingsboard.server.service.security.permission.Resource;
import org.thingsboard.server.service.telemetry.TelemetrySubscriptionService;
import org.thingsboard.server.utils.CsvUtils;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List;
@ -235,7 +234,7 @@ public abstract class AbstractBulkImportService<E extends HasId<? extends Entity
@SneakyThrows
private void saveAttributes(SecurityUser user, E entity, Map.Entry<BulkImportColumnType, JsonObject> kvsEntry, BulkImportColumnType kvType) {
String scope = kvType.getKey();
List<AttributeKvEntry> attributes = new ArrayList<>(JsonConverter.convertToAttributes(kvsEntry.getValue()));
List<AttributeKvEntry> attributes = JsonConverter.convertToAttributes(kvsEntry.getValue());
accessValidator.validateEntityAndCallback(user, Operation.WRITE_ATTRIBUTES, entity.getId(), (result, tenantId, entityId) -> {
tsSubscriptionService.saveAttributes(AttributesSaveRequest.builder()

View File

@ -71,7 +71,6 @@ import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.function.Function;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
@ -148,6 +147,7 @@ public abstract class BaseEntityImportService<I extends EntityId, E extends Expo
public CompareResult(boolean updateNeeded) {
this.updateNeeded = updateNeeded;
}
}
protected boolean updateRelatedEntitiesIfUnmodified(EntitiesImportCtx ctx, E prepared, D exportData, IdProvider idProvider) {
@ -203,7 +203,6 @@ public abstract class BaseEntityImportService<I extends EntityId, E extends Expo
protected abstract E saveOrUpdate(EntitiesImportCtx ctx, E entity, D exportData, IdProvider idProvider, CompareResult compareResult);
protected void processAfterSaved(EntitiesImportCtx ctx, EntityImportResult<E> importResult, D exportData, IdProvider idProvider) throws ThingsboardException {
E savedEntity = importResult.getSavedEntity();
E oldEntity = importResult.getOldEntity();
@ -405,7 +404,9 @@ public abstract class BaseEntityImportService<I extends EntityId, E extends Expo
}
public <ID extends EntityId> ID getInternalId(ID externalId, boolean throwExceptionIfNotFound) {
if (externalId == null || externalId.isNullUid()) return null;
if (externalId == null || externalId.isNullUid()) {
return null;
}
if (EntityType.TENANT.equals(externalId.getEntityType())) {
return (ID) ctx.getTenantId();
@ -432,7 +433,9 @@ public abstract class BaseEntityImportService<I extends EntityId, E extends Expo
}
public Optional<EntityId> getInternalIdByUuid(UUID externalUuid, boolean fetchAllUUIDs, Set<EntityType> hints) {
if (externalUuid.equals(EntityId.NULL_UUID)) return Optional.empty();
if (externalUuid.equals(EntityId.NULL_UUID)) {
return Optional.empty();
}
for (EntityType entityType : EntityType.values()) {
Optional<EntityId> externalId = buildEntityId(entityType, externalUuid);
@ -483,10 +486,6 @@ public abstract class BaseEntityImportService<I extends EntityId, E extends Expo
}
protected <T extends EntityId, O> T getOldEntityField(O oldEntity, Function<O, T> getter) {
return oldEntity == null ? null : getter.apply(oldEntity);
}
protected void replaceIdsRecursively(EntitiesImportCtx ctx, IdProvider idProvider, JsonNode json,
Set<String> skippedRootFields, Pattern includedFieldsPattern,
LinkedHashSet<EntityType> hints) {

View File

@ -44,8 +44,8 @@ public class DeviceImportService extends BaseEntityImportService<DeviceId, Devic
@Override
protected Device prepare(EntitiesImportCtx ctx, Device device, Device old, DeviceExportData exportData, IdProvider idProvider) {
device.setDeviceProfileId(idProvider.getInternalId(device.getDeviceProfileId()));
device.setFirmwareId(getOldEntityField(old, Device::getFirmwareId));
device.setSoftwareId(getOldEntityField(old, Device::getSoftwareId));
device.setFirmwareId(idProvider.getInternalId(device.getFirmwareId()));
device.setSoftwareId(idProvider.getInternalId(device.getSoftwareId()));
return device;
}

View File

@ -45,15 +45,20 @@ public class DeviceProfileImportService extends BaseEntityImportService<DevicePr
deviceProfile.setDefaultRuleChainId(idProvider.getInternalId(deviceProfile.getDefaultRuleChainId()));
deviceProfile.setDefaultEdgeRuleChainId(idProvider.getInternalId(deviceProfile.getDefaultEdgeRuleChainId()));
deviceProfile.setDefaultDashboardId(idProvider.getInternalId(deviceProfile.getDefaultDashboardId()));
deviceProfile.setFirmwareId(getOldEntityField(old, DeviceProfile::getFirmwareId));
deviceProfile.setSoftwareId(getOldEntityField(old, DeviceProfile::getSoftwareId));
deviceProfile.setFirmwareId(idProvider.getInternalId(deviceProfile.getFirmwareId(), false));
deviceProfile.setSoftwareId(idProvider.getInternalId(deviceProfile.getSoftwareId(), false));
return deviceProfile;
}
@Override
protected DeviceProfile saveOrUpdate(EntitiesImportCtx ctx, DeviceProfile deviceProfile, EntityExportData<DeviceProfile> exportData, IdProvider idProvider, CompareResult compareResult) {
boolean toUpdate = ctx.isFinalImportAttempt() || ctx.getCurrentImportResult().isUpdatedAllExternalIds();
if (toUpdate) {
deviceProfile.setFirmwareId(idProvider.getInternalId(deviceProfile.getFirmwareId()));
deviceProfile.setSoftwareId(idProvider.getInternalId(deviceProfile.getSoftwareId()));
}
DeviceProfile saved = deviceProfileService.saveDeviceProfile(deviceProfile);
if (ctx.isFinalImportAttempt() || ctx.getCurrentImportResult().isUpdatedAllExternalIds()) {
if (toUpdate) {
importCalculatedFields(ctx, saved, exportData, idProvider);
}
return saved;
@ -73,8 +78,6 @@ public class DeviceProfileImportService extends BaseEntityImportService<DevicePr
@Override
protected void cleanupForComparison(DeviceProfile deviceProfile) {
super.cleanupForComparison(deviceProfile);
deviceProfile.setFirmwareId(null);
deviceProfile.setSoftwareId(null);
}
@Override

View File

@ -0,0 +1,76 @@
/**
* 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.sync.ie.importing.impl;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.OtaPackage;
import org.thingsboard.server.common.data.OtaPackageInfo;
import org.thingsboard.server.common.data.id.OtaPackageId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.sync.ie.OtaPackageExportData;
import org.thingsboard.server.dao.ota.OtaPackageService;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.sync.vc.data.EntitiesImportCtx;
@Service
@TbCoreComponent
@RequiredArgsConstructor
public class OtaPackageImportService extends BaseEntityImportService<OtaPackageId, OtaPackage, OtaPackageExportData> {
private final OtaPackageService otaPackageService;
@Override
protected void setOwner(TenantId tenantId, OtaPackage otaPackage, IdProvider idProvider) {
otaPackage.setTenantId(tenantId);
}
@Override
protected OtaPackage prepare(EntitiesImportCtx ctx, OtaPackage otaPackage, OtaPackage oldOtaPackage, OtaPackageExportData exportData, IdProvider idProvider) {
otaPackage.setDeviceProfileId(idProvider.getInternalId(otaPackage.getDeviceProfileId()));
return otaPackage;
}
@Override
protected OtaPackage findExistingEntity(EntitiesImportCtx ctx, OtaPackage otaPackage, IdProvider idProvider) {
OtaPackage existingOtaPackage = super.findExistingEntity(ctx, otaPackage, idProvider);
if (existingOtaPackage == null && ctx.isFindExistingByName()) {
existingOtaPackage = otaPackageService.findOtaPackageByTenantIdAndTitleAndVersion(ctx.getTenantId(), otaPackage.getTitle(), otaPackage.getVersion());
}
return existingOtaPackage;
}
@Override
protected OtaPackage deepCopy(OtaPackage otaPackage) {
return new OtaPackage(otaPackage);
}
@Override
protected OtaPackage saveOrUpdate(EntitiesImportCtx ctx, OtaPackage otaPackage, OtaPackageExportData exportData, IdProvider idProvider, CompareResult compareResult) {
if (otaPackage.hasUrl()) {
OtaPackageInfo info = new OtaPackageInfo(otaPackage);
return new OtaPackage(otaPackageService.saveOtaPackageInfo(info, info.hasUrl()));
}
return otaPackageService.saveOtaPackage(otaPackage);
}
@Override
public EntityType getEntityType() {
return EntityType.OTA_PACKAGE;
}
}

View File

@ -323,7 +323,7 @@ cassandra:
poll_ms: "${CASSANDRA_QUERY_POLL_MS:50}"
# Interval in milliseconds for printing Cassandra query queue statistic
rate_limit_print_interval_ms: "${CASSANDRA_QUERY_RATE_LIMIT_PRINT_MS:10000}"
# set all data type values except target to null for the same ts on save
# When saving a value, set other data types to null (to avoid having multiple telemetry values with the same timestamp).
set_null_values_enabled: "${CASSANDRA_QUERY_SET_NULL_VALUES_ENABLED:true}"
# log one of cassandra queries with specified frequency (0 - logging is disabled)
print_queries_freq: "${CASSANDRA_QUERY_PRINT_FREQ:0}"
@ -1673,7 +1673,7 @@ queue:
# Kafka properties for Notifications topics
notifications: "${TB_QUEUE_KAFKA_NOTIFICATIONS_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:52428800;retention.bytes:1048576000;partitions:1;min.insync.replicas:1}"
# Kafka properties for JS Executor topics
js-executor: "${TB_QUEUE_KAFKA_JE_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:52428800;retention.bytes:104857600;partitions:100;min.insync.replicas:1}"
js-executor: "${TB_QUEUE_KAFKA_JE_TOPIC_PROPERTIES:retention.ms:86400000;segment.bytes:52428800;retention.bytes:104857600;partitions:30;min.insync.replicas:1}"
# Kafka properties for OTA updates topic
ota-updates: "${TB_QUEUE_KAFKA_OTA_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:52428800;retention.bytes:1048576000;partitions:10;min.insync.replicas:1}"
# Kafka properties for Version Control topic

View File

@ -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())

View File

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

View File

@ -0,0 +1,267 @@
/**
* Copyright © 2016-2025 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.edge;
import com.datastax.oss.driver.api.core.uuid.Uuids;
import com.google.protobuf.AbstractMessage;
import com.google.protobuf.InvalidProtocolBufferException;
import org.junit.Assert;
import org.junit.Test;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.cf.CalculatedField;
import org.thingsboard.server.common.data.cf.CalculatedFieldType;
import org.thingsboard.server.common.data.cf.configuration.Argument;
import org.thingsboard.server.common.data.cf.configuration.ArgumentType;
import org.thingsboard.server.common.data.cf.configuration.Output;
import org.thingsboard.server.common.data.cf.configuration.OutputType;
import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey;
import org.thingsboard.server.common.data.cf.configuration.SimpleCalculatedFieldConfiguration;
import org.thingsboard.server.common.data.debug.DebugSettings;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.dao.service.DaoSqlTest;
import org.thingsboard.server.gen.edge.v1.CalculatedFieldRequestMsg;
import org.thingsboard.server.gen.edge.v1.CalculatedFieldUpdateMsg;
import org.thingsboard.server.gen.edge.v1.UpdateMsgType;
import org.thingsboard.server.gen.edge.v1.UplinkMsg;
import org.thingsboard.server.gen.edge.v1.UplinkResponseMsg;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@DaoSqlTest
public class CalculatedFieldEdgeTest extends AbstractEdgeTest {
private static final String DEFAULT_CF_NAME = "Edge Test CalculatedField";
private static final String UPDATED_CF_NAME = "Updated Edge Test CalculatedField";
@Test
public void testCalculatedField_create_update_delete() throws Exception {
Device savedDevice = saveDeviceOnCloudAndVerifyDeliveryToEdge();
// create calculatedField
SimpleCalculatedFieldConfiguration config = new SimpleCalculatedFieldConfiguration();
CalculatedField calculatedField = createSimpleCalculatedField(savedDevice.getId(), config);
edgeImitator.expectMessageAmount(1);
CalculatedField savedCalculatedField = doPost("/api/calculatedField", calculatedField, CalculatedField.class);
Assert.assertTrue(edgeImitator.waitForMessages());
AbstractMessage latestMessage = edgeImitator.getLatestMessage();
Assert.assertTrue(latestMessage instanceof CalculatedFieldUpdateMsg);
CalculatedFieldUpdateMsg calculatedFieldUpdateMsg = (CalculatedFieldUpdateMsg) latestMessage;
Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, calculatedFieldUpdateMsg.getMsgType());
Assert.assertEquals(savedCalculatedField.getUuidId().getMostSignificantBits(), calculatedFieldUpdateMsg.getIdMSB());
Assert.assertEquals(savedCalculatedField.getUuidId().getLeastSignificantBits(), calculatedFieldUpdateMsg.getIdLSB());
CalculatedField calculatedFieldFromMsg = JacksonUtil.fromString(calculatedFieldUpdateMsg.getEntity(), CalculatedField.class, true);
Assert.assertNotNull(calculatedFieldFromMsg);
Assert.assertEquals(DEFAULT_CF_NAME, calculatedFieldFromMsg.getName());
Assert.assertEquals(savedDevice.getId(), calculatedFieldFromMsg.getEntityId());
Assert.assertEquals(config, calculatedFieldFromMsg.getConfiguration());
edgeImitator.expectMessageAmount(1);
savedCalculatedField.setName(UPDATED_CF_NAME);
savedCalculatedField = doPost("/api/calculatedField", savedCalculatedField, CalculatedField.class);
Assert.assertTrue(edgeImitator.waitForMessages());
latestMessage = edgeImitator.getLatestMessage();
Assert.assertTrue(latestMessage instanceof CalculatedFieldUpdateMsg);
calculatedFieldUpdateMsg = (CalculatedFieldUpdateMsg) latestMessage;
calculatedFieldFromMsg = JacksonUtil.fromString(calculatedFieldUpdateMsg.getEntity(), CalculatedField.class, true);
Assert.assertNotNull(calculatedFieldFromMsg);
Assert.assertEquals(UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE, calculatedFieldUpdateMsg.getMsgType());
Assert.assertEquals(UPDATED_CF_NAME, calculatedFieldFromMsg.getName());
// delete calculatedField
edgeImitator.expectMessageAmount(1);
doDelete("/api/calculatedField/" + savedCalculatedField.getUuidId())
.andExpect(status().isOk());
Assert.assertTrue(edgeImitator.waitForMessages());
latestMessage = edgeImitator.getLatestMessage();
Assert.assertTrue(latestMessage instanceof CalculatedFieldUpdateMsg);
calculatedFieldUpdateMsg = (CalculatedFieldUpdateMsg) latestMessage;
Assert.assertEquals(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE, calculatedFieldUpdateMsg.getMsgType());
Assert.assertEquals(savedCalculatedField.getUuidId().getMostSignificantBits(), calculatedFieldUpdateMsg.getIdMSB());
Assert.assertEquals(savedCalculatedField.getUuidId().getLeastSignificantBits(), calculatedFieldUpdateMsg.getIdLSB());
}
@Test
public void testSendCalculatedFieldToCloud() throws Exception {
Device savedDevice = saveDeviceOnCloudAndVerifyDeliveryToEdge();
// create calculatedField
SimpleCalculatedFieldConfiguration config = new SimpleCalculatedFieldConfiguration();
CalculatedField calculatedField = createSimpleCalculatedField(savedDevice.getId(), config);
UUID uuid = Uuids.timeBased();
UplinkMsg uplinkMsg = getUplinkMsg(uuid, calculatedField, UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE);
checkCalculatedFieldOnCloud(uplinkMsg, uuid, calculatedField.getName());
}
@Test
public void testSendCalculatedFieldRequestToCloud() throws Exception {
Device savedDevice = saveDeviceOnCloudAndVerifyDeliveryToEdge();
// create calculatedField
SimpleCalculatedFieldConfiguration config = new SimpleCalculatedFieldConfiguration();
CalculatedField calculatedField = createSimpleCalculatedField(savedDevice.getId(), config);
edgeImitator.expectMessageAmount(1);
CalculatedField savedCalculatedField = doPost("/api/calculatedField", calculatedField, CalculatedField.class);
Assert.assertTrue(edgeImitator.waitForMessages());
UplinkMsg.Builder uplinkMsgBuilder = UplinkMsg.newBuilder();
CalculatedFieldRequestMsg.Builder calculatedFieldRequestMsgBuilder = CalculatedFieldRequestMsg.newBuilder();
calculatedFieldRequestMsgBuilder.setEntityIdMSB(savedDevice.getId().getId().getMostSignificantBits());
calculatedFieldRequestMsgBuilder.setEntityIdLSB(savedDevice.getId().getId().getLeastSignificantBits());
calculatedFieldRequestMsgBuilder.setEntityType(savedDevice.getId().getEntityType().name());
testAutoGeneratedCodeByProtobuf(calculatedFieldRequestMsgBuilder);
uplinkMsgBuilder.addCalculatedFieldRequestMsg(calculatedFieldRequestMsgBuilder.build());
testAutoGeneratedCodeByProtobuf(uplinkMsgBuilder);
edgeImitator.expectResponsesAmount(1);
edgeImitator.expectMessageAmount(1);
edgeImitator.sendUplinkMsg(uplinkMsgBuilder.build());
Assert.assertTrue(edgeImitator.waitForResponses());
Assert.assertTrue(edgeImitator.waitForMessages());
AbstractMessage latestMessage = edgeImitator.getLatestMessage();
Assert.assertTrue(latestMessage instanceof CalculatedFieldUpdateMsg);
CalculatedFieldUpdateMsg calculatedFieldUpdateMsg = (CalculatedFieldUpdateMsg) latestMessage;
CalculatedField calculatedFieldFromEdge = JacksonUtil.fromString(calculatedFieldUpdateMsg.getEntity(), CalculatedField.class, true);
Assert.assertNotNull(calculatedFieldFromEdge);
Assert.assertEquals(savedCalculatedField, calculatedFieldFromEdge);
Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, calculatedFieldUpdateMsg.getMsgType());
}
@Test
public void testUpdateCalculatedFieldNameOnCloud() throws Exception {
Device savedDevice = saveDeviceOnCloudAndVerifyDeliveryToEdge();
// create calculatedField
SimpleCalculatedFieldConfiguration config = new SimpleCalculatedFieldConfiguration();
CalculatedField calculatedField = createSimpleCalculatedField(savedDevice.getId(), config);
UUID uuid = Uuids.timeBased();
UplinkMsg uplinkMsg = getUplinkMsg(uuid, calculatedField, UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE);
checkCalculatedFieldOnCloud(uplinkMsg, uuid, calculatedField.getName());
calculatedField.setName(UPDATED_CF_NAME);
UplinkMsg updatedUplinkMsg = getUplinkMsg(uuid, calculatedField, UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE);
checkCalculatedFieldOnCloud(updatedUplinkMsg, uuid, calculatedField.getName());
}
@Test
public void testCalculatedFieldToCloudWithNameThatAlreadyExistsOnCloud() throws Exception {
Device savedDevice = saveDeviceOnCloudAndVerifyDeliveryToEdge();
// create calculatedField
SimpleCalculatedFieldConfiguration config = new SimpleCalculatedFieldConfiguration();
CalculatedField calculatedField = createSimpleCalculatedField(savedDevice.getId(), config);
edgeImitator.expectMessageAmount(1);
CalculatedField savedCalculatedField = doPost("/api/calculatedField", calculatedField, CalculatedField.class);
Assert.assertTrue(edgeImitator.waitForMessages());
UUID uuid = Uuids.timeBased();
UplinkMsg uplinkMsg = getUplinkMsg(uuid, calculatedField, UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE);
edgeImitator.expectResponsesAmount(1);
edgeImitator.expectMessageAmount(1);
edgeImitator.sendUplinkMsg(uplinkMsg);
Assert.assertTrue(edgeImitator.waitForResponses());
Assert.assertTrue(edgeImitator.waitForMessages());
Optional<CalculatedFieldUpdateMsg> calculatedFieldUpdateMsgOpt = edgeImitator.findMessageByType(CalculatedFieldUpdateMsg.class);
Assert.assertTrue(calculatedFieldUpdateMsgOpt.isPresent());
CalculatedFieldUpdateMsg latestCalculatedFieldUpdateMsg = calculatedFieldUpdateMsgOpt.get();
CalculatedField calculatedFieldFromMsg = JacksonUtil.fromString(latestCalculatedFieldUpdateMsg.getEntity(), CalculatedField.class, true);
Assert.assertNotNull(calculatedFieldFromMsg);
Assert.assertNotEquals(DEFAULT_CF_NAME, calculatedFieldFromMsg.getName());
Assert.assertNotEquals(savedCalculatedField.getUuidId(), uuid);
CalculatedField calculatedFieldFromCloud = doGet("/api/calculatedField/" + uuid, CalculatedField.class);
Assert.assertNotNull(calculatedFieldFromCloud);
Assert.assertNotEquals(DEFAULT_CF_NAME, calculatedFieldFromCloud.getName());
}
private CalculatedField createSimpleCalculatedField(EntityId entityId, SimpleCalculatedFieldConfiguration config) {
CalculatedField calculatedField = new CalculatedField();
calculatedField.setEntityId(entityId);
calculatedField.setTenantId(tenantId);
calculatedField.setType(CalculatedFieldType.SIMPLE);
calculatedField.setName(DEFAULT_CF_NAME);
calculatedField.setDebugSettings(DebugSettings.all());
Argument argument = new Argument();
ReferencedEntityKey refEntityKey = new ReferencedEntityKey("temperature", ArgumentType.TS_LATEST, null);
argument.setRefEntityKey(refEntityKey);
argument.setDefaultValue("12"); // not used because real telemetry value in db is present
config.setArguments(Map.of("T", argument));
config.setExpression("(T * 9/5) + 32");
Output output = new Output();
output.setName("fahrenheitTemp");
output.setType(OutputType.TIME_SERIES);
output.setDecimalsByDefault(2);
config.setOutput(output);
calculatedField.setConfiguration(config);
return calculatedField;
}
private UplinkMsg getUplinkMsg(UUID uuid, CalculatedField calculatedField, UpdateMsgType updateMsgType) throws InvalidProtocolBufferException {
UplinkMsg.Builder uplinkMsgBuilder = UplinkMsg.newBuilder();
CalculatedFieldUpdateMsg.Builder calculatedFieldUpdateMsgBuilder = CalculatedFieldUpdateMsg.newBuilder();
calculatedFieldUpdateMsgBuilder.setIdMSB(uuid.getMostSignificantBits());
calculatedFieldUpdateMsgBuilder.setIdLSB(uuid.getLeastSignificantBits());
calculatedFieldUpdateMsgBuilder.setEntity(JacksonUtil.toString(calculatedField));
calculatedFieldUpdateMsgBuilder.setMsgType(updateMsgType);
testAutoGeneratedCodeByProtobuf(calculatedFieldUpdateMsgBuilder);
uplinkMsgBuilder.addCalculatedFieldUpdateMsg(calculatedFieldUpdateMsgBuilder.build());
testAutoGeneratedCodeByProtobuf(uplinkMsgBuilder);
return uplinkMsgBuilder.build();
}
private void checkCalculatedFieldOnCloud(UplinkMsg uplinkMsg, UUID uuid, String resourceTitle) throws Exception {
edgeImitator.expectResponsesAmount(1);
edgeImitator.sendUplinkMsg(uplinkMsg);
Assert.assertTrue(edgeImitator.waitForResponses());
UplinkResponseMsg latestResponseMsg = edgeImitator.getLatestResponseMsg();
Assert.assertTrue(latestResponseMsg.getSuccess());
CalculatedField calculatedField = doGet("/api/calculatedField/" + uuid, CalculatedField.class);
Assert.assertNotNull(calculatedField);
Assert.assertEquals(resourceTitle, calculatedField.getName());
}
}

View File

@ -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()));
}

View File

@ -2451,7 +2451,7 @@ class TbelInvokeDocsIoTest extends AbstractTbelInvokeTest {
public void isInsidePolygon_Test() throws ExecutionException, InterruptedException {
msgStr = "{}";
decoderStr = """
String perimeter = "[[[37.7810,-122.4210],[37.7890,-122.3900],[37.7700,-122.3800],[37.7600,-122.4000],[37.7700,-122.4250],[37.7810,-122.4210]],[[37.7730,-122.4050],[37.7700,-122.3950],[37.7670,-122.3980],[37.7690,-122.4100],[37.7730,-122.4050]]]";
var perimeter = "[[[37.7810,-122.4210],[37.7890,-122.3900],[37.7700,-122.3800],[37.7600,-122.4000],[37.7700,-122.4250],[37.7810,-122.4210]],[[37.7730,-122.4050],[37.7700,-122.3950],[37.7670,-122.3980],[37.7690,-122.4100],[37.7730,-122.4050]]]";
return{
outsidePolygon: isInsidePolygon(37.8000, -122.4300, perimeter),
insidePolygon: isInsidePolygon(37.7725, -122.4010, perimeter),
@ -2470,7 +2470,7 @@ class TbelInvokeDocsIoTest extends AbstractTbelInvokeTest {
public void isInsideCircle_Test() throws ExecutionException, InterruptedException {
msgStr = "{}";
decoderStr = """
String perimeter = "{\\"latitude\\":37.7749,\\"longitude\\":-122.4194,\\"radius\\":3000,\\"radiusUnit\\":\\"METER\\"}";
var perimeter = "{\\"latitude\\":37.7749,\\"longitude\\":-122.4194,\\"radius\\":3000,\\"radiusUnit\\":\\"METER\\"}";
return{
outsideCircle: isInsideCircle(37.8044, -122.2712, perimeter),
insideCircle: isInsideCircle(37.7599, -122.4148, perimeter)

View File

@ -66,6 +66,7 @@ import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.DeviceProfileId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.EntityViewId;
import org.thingsboard.server.common.data.id.OtaPackageId;
import org.thingsboard.server.common.data.id.RuleChainId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.msg.TbNodeConnectionType;
@ -203,11 +204,12 @@ public class ExportImportServiceSqlTest extends AbstractControllerTest {
AssetProfile assetProfile = createAssetProfile(tenantId1, ruleChain.getId(), dashboard.getId(), "Asset profile 1");
Asset asset = createAsset(tenantId1, null, assetProfile.getId(), "Asset 1");
DeviceProfile deviceProfile = createDeviceProfile(tenantId1, ruleChain.getId(), dashboard.getId(), "Device profile 1");
Device device = createDevice(tenantId1, null, deviceProfile.getId(), "Device 1");
OtaPackage firmware = createOtaPackage(tenantId1, deviceProfile.getId(), OtaPackageType.FIRMWARE);
Device device = createDevice(tenantId1, null, deviceProfile.getId(), "Device 1", firmware.getId(), null);
CalculatedField calculatedField = createCalculatedField(tenantId1, device.getId(), asset.getId());
Map<EntityType, EntityExportData> entitiesExportData = Stream.of(customer.getId(), asset.getId(), device.getId(),
ruleChain.getId(), dashboard.getId(), assetProfile.getId(), deviceProfile.getId())
ruleChain.getId(), dashboard.getId(), assetProfile.getId(), deviceProfile.getId(), firmware.getId())
.map(entityId -> {
try {
return exportEntity(tenantAdmin1, entityId, EntityExportSettings.builder()
@ -275,12 +277,17 @@ public class ExportImportServiceSqlTest extends AbstractControllerTest {
verify(tbClusterService).sendNotificationMsgToEdge(any(), any(), eq(importedDeviceProfile.getId()), any(), any(), eq(EdgeEventActionType.ADDED), any());
verify(otaPackageStateService).update(eq(importedDeviceProfile), eq(false), eq(false));
OtaPackage importedFirmware = (OtaPackage) importEntity(tenantAdmin2, getAndClone(entitiesExportData, EntityType.OTA_PACKAGE)).getSavedEntity();
verify(entityActionService).logEntityAction(any(), eq(importedFirmware.getId()), eq(importedFirmware),
any(), eq(ActionType.ADDED), isNull());
Device importedDevice = (Device) importEntity(tenantAdmin2, getAndClone(entitiesExportData, EntityType.DEVICE)).getSavedEntity();
verify(entityActionService).logEntityAction(any(), eq(importedDevice.getId()), eq(importedDevice),
any(), eq(ActionType.ADDED), isNull());
verify(tbClusterService).onDeviceUpdated(eq(importedDevice), isNull());
importEntity(tenantAdmin2, getAndClone(entitiesExportData, EntityType.DEVICE));
verify(tbClusterService, Mockito.never()).onDeviceUpdated(eq(importedDevice), eq(importedDevice));
assertThat(importedDevice.getFirmwareId()).isEqualTo(importedFirmware.getId());
// calculated field of imported device:
List<CalculatedField> calculatedFields = calculatedFieldService.findCalculatedFieldsByEntityId(tenantId2, importedDevice.getId());
@ -318,14 +325,15 @@ public class ExportImportServiceSqlTest extends AbstractControllerTest {
assetProfile = assetProfileService.saveAssetProfile(assetProfile);
DeviceProfile deviceProfile = createDeviceProfile(tenantId1, ruleChain.getId(), dashboard.getId(), "Device profile 1");
Device device = createDevice(tenantId1, customer.getId(), deviceProfile.getId(), "Device 1");
OtaPackage firmware = createOtaPackage(tenantId1, deviceProfile.getId(), OtaPackageType.FIRMWARE);
Device device = createDevice(tenantId1, customer.getId(), deviceProfile.getId(), "Device 1", firmware.getId(), null);
EntityView entityView = createEntityView(tenantId1, customer.getId(), device.getId(), "Entity view 1");
CalculatedField calculatedField = createCalculatedField(tenantId1, device.getId(), device.getId());
Map<EntityId, EntityId> ids = new HashMap<>();
for (EntityId entityId : List.of(customer.getId(), ruleChain.getId(), dashboard.getId(), assetProfile.getId(), asset.getId(),
deviceProfile.getId(), device.getId(), entityView.getId(), ruleChain.getId(), dashboard.getId())) {
deviceProfile.getId(), firmware.getId(), device.getId(), entityView.getId(), ruleChain.getId(), dashboard.getId())) {
EntityExportData exportData = exportEntity(getSecurityUser(tenantAdmin1), entityId);
EntityImportResult importResult = importEntity(getSecurityUser(tenantAdmin2), exportData, EntityImportSettings.builder()
.saveCredentials(false)
@ -359,12 +367,17 @@ public class ExportImportServiceSqlTest extends AbstractControllerTest {
assertThat(exportedDeviceProfile.getDefaultRuleChainId()).isEqualTo(ruleChain.getId());
assertThat(exportedDeviceProfile.getDefaultDashboardId()).isEqualTo(dashboard.getId());
EntityExportData<Device> entityExportData = exportEntity(tenantAdmin2, (DeviceId) ids.get(device.getId()));
OtaPackage exportedFirmware = (OtaPackage) exportEntity(tenantAdmin2, (OtaPackageId) ids.get(firmware.getId())).getEntity();
assertThat(exportedFirmware.getDeviceProfileId()).isEqualTo(exportedDeviceProfile.getId());
assertThat(exportedFirmware.getId()).isEqualTo(firmware.getId());
EntityExportData<Device> entityExportData = exportEntity(tenantAdmin2, (DeviceId) ids.get(device.getId()));
Device exportedDevice = entityExportData.getEntity();
assertThat(exportedDevice.getCustomerId()).isEqualTo(customer.getId());
assertThat(exportedDevice.getDeviceProfileId()).isEqualTo(deviceProfile.getId());
assertThat(exportedDevice.getFirmwareId()).isEqualTo(firmware.getId());
List<CalculatedField> calculatedFields = ((DeviceExportData) entityExportData).getCalculatedFields();
List<CalculatedField> calculatedFields = entityExportData.getCalculatedFields();
assertThat(calculatedFields.size()).isOne();
CalculatedField field = calculatedFields.get(0);
assertThat(field.getName()).isEqualTo(calculatedField.getName());
@ -380,13 +393,15 @@ public class ExportImportServiceSqlTest extends AbstractControllerTest {
deviceProfileService.saveDeviceProfile(importedDeviceProfile);
}
protected Device createDevice(TenantId tenantId, CustomerId customerId, DeviceProfileId deviceProfileId, String name) {
protected Device createDevice(TenantId tenantId, CustomerId customerId, DeviceProfileId deviceProfileId, String name, OtaPackageId firmwareId, OtaPackageId softwareId) {
Device device = new Device();
device.setTenantId(tenantId);
device.setCustomerId(customerId);
device.setName(name);
device.setLabel("lbl");
device.setDeviceProfileId(deviceProfileId);
device.setFirmwareId(firmwareId);
device.setSoftwareId(softwareId);
DeviceData deviceData = new DeviceData();
deviceData.setTransportConfiguration(new DefaultDeviceTransportConfiguration());
device.setDeviceData(deviceData);

View File

@ -116,8 +116,8 @@ import java.util.stream.Collectors;
import static org.assertj.core.api.Assertions.assertThat;
import static org.awaitility.Awaitility.await;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.thingsboard.server.controller.TbResourceControllerTest.TEST_DATA;
import static org.thingsboard.server.controller.TbResourceControllerTest.JS_TEST_FILE_NAME;
import static org.thingsboard.server.controller.TbResourceControllerTest.TEST_DATA;
@DaoSqlTest
public class VersionControlTest extends AbstractControllerTest {
@ -262,19 +262,24 @@ public class VersionControlTest extends AbstractControllerTest {
}
@Test
public void testDeviceVc_withProfile_betweenTenants() throws Exception {
public void testDeviceVc_withProfileAndOtaPackage_betweenTenants() throws Exception {
DeviceProfile deviceProfile = createDeviceProfile(null, null, "Device profile of tenant 1");
createVersion("profiles", EntityType.DEVICE_PROFILE);
Device device = createDevice(null, deviceProfile.getId(), "Device of tenant 1", "test1");
String versionId = createVersion("devices", EntityType.DEVICE);
OtaPackage firmware = createOtaPackage(tenantId1, deviceProfile.getId(), OtaPackageType.FIRMWARE);
OtaPackage software = createOtaPackage(tenantId1, deviceProfile.getId(), OtaPackageType.SOFTWARE);
Device device = createDevice(null, deviceProfile.getId(), "Device of tenant 1", "test1", newDevice -> {
newDevice.setFirmwareId(firmware.getId());
newDevice.setSoftwareId(software.getId());
});
String versionId = createVersion("devices with ota", EntityType.DEVICE, EntityType.OTA_PACKAGE);
DeviceCredentials deviceCredentials = findDeviceCredentials(device.getId());
DeviceCredentials newCredentials = new DeviceCredentials(deviceCredentials);
newCredentials.setCredentialsId("new access token"); // updating access token to avoid constraint errors on import
doPost("/api/device/credentials", newCredentials, DeviceCredentials.class);
assertThat(listVersions()).extracting(EntityVersion::getName).containsExactly("devices", "profiles");
assertThat(listVersions()).extracting(EntityVersion::getName).containsExactly("devices with ota", "profiles");
loginTenant2();
Map<EntityType, EntityTypeLoadResult> result = loadVersion(versionId, EntityType.DEVICE, EntityType.DEVICE_PROFILE);
Map<EntityType, EntityTypeLoadResult> result = loadVersion(versionId, EntityType.DEVICE, EntityType.DEVICE_PROFILE, EntityType.OTA_PACKAGE);
assertThat(result.get(EntityType.DEVICE).getCreated()).isEqualTo(1);
assertThat(result.get(EntityType.DEVICE_PROFILE).getCreated()).isEqualTo(1);
@ -293,6 +298,13 @@ public class VersionControlTest extends AbstractControllerTest {
assertThat(importedCredentials.getCredentialsId()).isEqualTo(deviceCredentials.getCredentialsId());
assertThat(importedCredentials.getCredentialsValue()).isEqualTo(deviceCredentials.getCredentialsValue());
assertThat(importedCredentials.getCredentialsType()).isEqualTo(deviceCredentials.getCredentialsType());
OtaPackage importedFirmwareOta = findOtaPackage(firmware.getTitle());
OtaPackage importedSoftwareOta = findOtaPackage(software.getTitle());
checkImportedEntity(tenantId1, firmware, tenantId2, importedFirmwareOta);
checkImportedOtaPackageData(firmware, importedFirmwareOta);
checkImportedEntity(tenantId1, software, tenantId2, importedSoftwareOta);
checkImportedOtaPackageData(software, importedSoftwareOta);
}
@Test
@ -653,6 +665,57 @@ public class VersionControlTest extends AbstractControllerTest {
assertThat(importedCalculatedField.getType()).isEqualTo(calculatedField.getType());
}
@Test
public void testOtaPackageVc_sameTenant() throws Exception {
DeviceProfile deviceProfile = createDeviceProfile(null, null, "Device profile v1.0");
OtaPackage firmware = createOtaPackage(tenantId1, deviceProfile.getId(), OtaPackageType.FIRMWARE);
OtaPackage software = createOtaPackage(tenantId1, deviceProfile.getId(), OtaPackageType.SOFTWARE);
String versionId = createVersion("ota packages", EntityType.OTA_PACKAGE);
OtaPackage firmwareOta = findOtaPackage(firmware.getTitle());
OtaPackage softwareOta = findOtaPackage(software.getTitle());
loadVersion(versionId, EntityType.OTA_PACKAGE);
OtaPackage importedFirmwareOta = findOtaPackage(firmwareOta.getTitle());
OtaPackage importedSoftwareOta = findOtaPackage(softwareOta.getTitle());
checkImportedEntity(tenantId1, firmwareOta, tenantId1, importedFirmwareOta);
checkImportedOtaPackageData(firmwareOta, importedFirmwareOta);
checkImportedEntity(tenantId1, softwareOta, tenantId1, importedSoftwareOta);
checkImportedOtaPackageData(softwareOta, importedSoftwareOta);
}
@Test
public void testOtaPackageVcWithProfile_betweenTenants() throws Exception {
DeviceProfile deviceProfile = createDeviceProfile(null, null, "Device profile v1.0");
OtaPackage firmware = createOtaPackage(tenantId1, deviceProfile.getId(), OtaPackageType.FIRMWARE);
OtaPackage software = createOtaPackage(tenantId1, deviceProfile.getId(), OtaPackageType.SOFTWARE);
deviceProfile.setFirmwareId(firmware.getId());
deviceProfile.setSoftwareId(software.getId());
deviceProfile = doPost("/api/deviceProfile", deviceProfile, DeviceProfile.class);
String versionId = createVersion("ota packages", EntityType.DEVICE_PROFILE, EntityType.OTA_PACKAGE);
loginTenant2();
loadVersion(versionId, EntityType.DEVICE_PROFILE, EntityType.OTA_PACKAGE);
DeviceProfile importedProfile = findDeviceProfile(deviceProfile.getName());
OtaPackage importedFirmwareOta = findOtaPackage(firmware.getTitle());
OtaPackage importedSoftwareOta = findOtaPackage(software.getTitle());
checkImportedEntity(tenantId1, deviceProfile, tenantId2, importedProfile);
checkImportedDeviceProfileData(deviceProfile, importedProfile);
checkImportedEntity(tenantId1, firmware, tenantId2, importedFirmwareOta);
checkImportedOtaPackageData(firmware, importedFirmwareOta);
checkImportedEntity(tenantId1, software, tenantId2, importedSoftwareOta);
checkImportedOtaPackageData(software, importedSoftwareOta);
assertThat(importedProfile.getFirmwareId()).isEqualTo(importedFirmwareOta.getId());
assertThat(importedProfile.getSoftwareId()).isEqualTo(importedSoftwareOta.getId());
}
protected void checkImportedOtaPackageData(OtaPackage otaPackage, OtaPackage importedOtaPackage) {
assertThat(importedOtaPackage.getName()).isEqualTo(otaPackage.getName());
assertThat(importedOtaPackage.getTag()).isEqualTo(otaPackage.getTag());
assertThat(importedOtaPackage.getType()).isEqualTo(otaPackage.getType());
assertThat(importedOtaPackage.getFileName()).isEqualTo(otaPackage.getFileName());
}
@Test
public void testResourceVc_sameTenant() throws Exception {
TbResourceInfo resourceInfo = createResource("Test resource");
@ -923,6 +986,7 @@ public class VersionControlTest extends AbstractControllerTest {
otaPackage.setDeviceProfileId(deviceProfileId);
otaPackage.setType(type);
otaPackage.setTitle("My " + type);
otaPackage.setTag("My " + type);
otaPackage.setVersion("v1.0");
otaPackage.setFileName("filename.txt");
otaPackage.setContentType("text/plain");
@ -933,6 +997,10 @@ public class VersionControlTest extends AbstractControllerTest {
return otaPackageService.saveOtaPackage(otaPackage);
}
private OtaPackage findOtaPackage(String title) throws Exception {
return doGetTypedWithPageLink("/api/otaPackages?", new TypeReference<PageData<OtaPackage>>() {}, new PageLink(100, 0, title)).getData().get(0);
}
protected Dashboard createDashboard(CustomerId customerId, String name) {
Dashboard dashboard = new Dashboard();
dashboard.setTitle(name);

View File

@ -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();
}
}

View File

@ -17,11 +17,12 @@ package org.thingsboard.server.queue;
import com.google.common.util.concurrent.ListenableFuture;
/**
* Created by ashvayka on 05.10.18.
*/
public interface TbQueueHandler<Request extends TbQueueMsg, Response extends TbQueueMsg> {
ListenableFuture<Response> handle(Request request);
default Response constructErrorResponseMsg(Request request, Throwable cause) {
return null;
}
}

View File

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

View File

@ -41,6 +41,8 @@ public interface OtaPackageService extends EntityDaoService {
OtaPackageInfo findOtaPackageInfoById(TenantId tenantId, OtaPackageId otaPackageId);
OtaPackage findOtaPackageByTenantIdAndTitleAndVersion(TenantId tenantId, String title, String version);
ListenableFuture<OtaPackageInfo> findOtaPackageInfoByIdAsync(TenantId tenantId, OtaPackageId otaPackageId);
PageData<OtaPackageInfo> findTenantOtaPackagesByTenantId(TenantId tenantId, PageLink pageLink);
@ -52,4 +54,5 @@ public interface OtaPackageService extends EntityDaoService {
void deleteOtaPackagesByTenantId(TenantId tenantId);
long sumDataSizeByTenantId(TenantId tenantId);
}

View File

@ -20,6 +20,7 @@ import lombok.Data;
import lombok.EqualsAndHashCode;
import org.thingsboard.server.common.data.id.OtaPackageId;
import java.io.Serial;
import java.nio.ByteBuffer;
@Schema
@ -27,6 +28,7 @@ import java.nio.ByteBuffer;
@EqualsAndHashCode(callSuper = true)
public class OtaPackage extends OtaPackageInfo {
@Serial
private static final long serialVersionUID = 3091601761339422546L;
@Schema(description = "OTA Package data.", accessMode = Schema.AccessMode.READ_ONLY)
@ -44,4 +46,10 @@ public class OtaPackage extends OtaPackageInfo {
super(otaPackage);
this.data = otaPackage.getData();
}
public OtaPackage(OtaPackageInfo otaPackageInfo) {
super(otaPackageInfo);
this.data = null;
}
}

View File

@ -16,6 +16,7 @@
package org.thingsboard.server.common.data;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.JsonNode;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@ -29,12 +30,15 @@ import org.thingsboard.server.common.data.ota.OtaPackageType;
import org.thingsboard.server.common.data.validation.Length;
import org.thingsboard.server.common.data.validation.NoXss;
import java.io.Serial;
@Schema
@Slf4j
@Data
@EqualsAndHashCode(callSuper = true)
public class OtaPackageInfo extends BaseDataWithAdditionalInfo<OtaPackageId> implements HasName, HasTenantId, HasTitle {
public class OtaPackageInfo extends BaseDataWithAdditionalInfo<OtaPackageId> implements HasName, HasTenantId, HasTitle, ExportableEntity<OtaPackageId> {
@Serial
private static final long serialVersionUID = 3168391583570815419L;
@Schema(description = "JSON object with Tenant Id. Tenant Id of the ota package can't be changed.", accessMode = Schema.AccessMode.READ_ONLY)
@ -77,6 +81,8 @@ public class OtaPackageInfo extends BaseDataWithAdditionalInfo<OtaPackageId> imp
@Schema(description = "OTA Package data size.", example = "8", accessMode = Schema.AccessMode.READ_ONLY)
private Long dataSize;
private OtaPackageId externalId;
public OtaPackageInfo() {
super();
}
@ -100,6 +106,7 @@ public class OtaPackageInfo extends BaseDataWithAdditionalInfo<OtaPackageId> imp
this.checksumAlgorithm = otaPackageInfo.getChecksumAlgorithm();
this.checksum = otaPackageInfo.getChecksum();
this.dataSize = otaPackageInfo.getDataSize();
this.externalId = otaPackageInfo.getExternalId();
}
@Schema(description = "JSON object with the ota package Id. " +
@ -118,7 +125,7 @@ public class OtaPackageInfo extends BaseDataWithAdditionalInfo<OtaPackageId> imp
}
@Override
@JsonIgnore
@JsonProperty(access = JsonProperty.Access.READ_ONLY)
public String getName() {
return title;
}
@ -133,4 +140,5 @@ public class OtaPackageInfo extends BaseDataWithAdditionalInfo<OtaPackageId> imp
public JsonNode getAdditionalInfo() {
return super.getAdditionalInfo();
}
}

View File

@ -23,6 +23,7 @@ import lombok.EqualsAndHashCode;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.server.common.data.id.TbResourceId;
import java.io.Serial;
import java.util.Base64;
import java.util.Optional;
@ -31,6 +32,7 @@ import java.util.Optional;
@EqualsAndHashCode(callSuper = true)
public class TbResource extends TbResourceInfo {
@Serial
private static final long serialVersionUID = 7379609705527272306L;
private byte[] data;
@ -88,4 +90,5 @@ public class TbResource extends TbResourceInfo {
public String toString() {
return super.toString();
}
}

View File

@ -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;

View File

@ -138,6 +138,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!");
}

View File

@ -20,10 +20,12 @@ import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import org.thingsboard.server.common.data.EntityType;
import java.io.Serial;
import java.util.UUID;
public class OtaPackageId extends UUIDBased implements EntityId {
@Serial
private static final long serialVersionUID = 1L;
@JsonCreator

View File

@ -77,16 +77,4 @@ public class RateLimitUtil {
return true;
}
@Deprecated(forRemoval = true, since = "4.1")
public static String deduplicateByDuration(String configStr) {
if (configStr == null) {
return null;
}
Set<Long> distinctDurations = new HashSet<>();
return parseConfig(configStr).stream()
.filter(entry -> distinctDurations.add(entry.durationSeconds()))
.map(RateLimitEntry::toString)
.collect(Collectors.joining(","));
}
}

View File

@ -25,6 +25,7 @@ import org.thingsboard.server.common.data.Dashboard;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.EntityView;
import org.thingsboard.server.common.data.OtaPackage;
import org.thingsboard.server.common.data.TbResource;
import org.thingsboard.server.common.data.ai.AiModel;
import org.thingsboard.server.common.data.asset.Asset;
@ -60,6 +61,7 @@ import java.lang.annotation.Target;
@Type(name = "NOTIFICATION_TARGET", value = NotificationTarget.class),
@Type(name = "NOTIFICATION_RULE", value = NotificationRule.class),
@Type(name = "TB_RESOURCE", value = TbResource.class),
@Type(name = "OTA_PACKAGE", value = OtaPackage.class),
@Type(name = "AI_MODEL", value = AiModel.class)
})
@JsonIgnoreProperties(value = {"tenantId", "createdTime", "version"}, ignoreUnknown = true)

View File

@ -41,7 +41,8 @@ import java.util.Map;
@Type(name = "DEVICE", value = DeviceExportData.class),
@Type(name = "RULE_CHAIN", value = RuleChainExportData.class),
@Type(name = "WIDGET_TYPE", value = WidgetTypeExportData.class),
@Type(name = "WIDGETS_BUNDLE", value = WidgetsBundleExportData.class)
@Type(name = "WIDGETS_BUNDLE", value = WidgetsBundleExportData.class),
@Type(name = "OTA_PACKAGE", value = OtaPackageExportData.class)
})
@JsonInclude(JsonInclude.Include.NON_NULL)
@Data

View File

@ -0,0 +1,41 @@
/**
* 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.common.data.sync.ie;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.EqualsAndHashCode;
import org.thingsboard.server.common.data.OtaPackage;
@EqualsAndHashCode(callSuper = true)
public class OtaPackageExportData extends EntityExportData<OtaPackage> {
/*
* OtaPackage is not a versioned entity; its 'version' field is part of the domain model (not used for optimistic locking)
* We override both methods to ensure 'version' is not ignored during (de)serialization.
*/
@JsonIgnoreProperties(value = {"tenantId", "createdTime"}, ignoreUnknown = true)
@Override
public OtaPackage getEntity() {
return super.getEntity();
}
@JsonIgnoreProperties(value = {"tenantId", "createdTime"}, ignoreUnknown = true)
@Override
public void setEntity(OtaPackage entity) {
super.setEntity(entity);
}
}

View File

@ -18,10 +18,13 @@ package org.thingsboard.server.common.data.sync.vc.request.create;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serial;
@EqualsAndHashCode(callSuper = true)
@Data
public class AutoVersionCreateConfig extends VersionCreateConfig {
@Serial
private static final long serialVersionUID = 8245450889383315551L;
private String branch;

View File

@ -24,7 +24,6 @@ import lombok.NoArgsConstructor;
import org.thingsboard.server.common.data.ApiUsageRecordKey;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.TenantProfileType;
import org.thingsboard.server.common.data.limit.RateLimitUtil;
import org.thingsboard.server.common.data.validation.RateLimit;
import java.io.Serial;
@ -236,43 +235,4 @@ public class DefaultTenantProfileConfiguration implements TenantProfileConfigura
return maxRuleNodeExecutionsPerMessage;
}
@Deprecated(forRemoval = true, since = "4.1")
public void deduplicateRateLimitsConfigs() {
this.transportTenantMsgRateLimit = RateLimitUtil.deduplicateByDuration(transportTenantMsgRateLimit);
this.transportTenantTelemetryMsgRateLimit = RateLimitUtil.deduplicateByDuration(transportTenantTelemetryMsgRateLimit);
this.transportTenantTelemetryDataPointsRateLimit = RateLimitUtil.deduplicateByDuration(transportTenantTelemetryDataPointsRateLimit);
this.transportDeviceMsgRateLimit = RateLimitUtil.deduplicateByDuration(transportDeviceMsgRateLimit);
this.transportDeviceTelemetryMsgRateLimit = RateLimitUtil.deduplicateByDuration(transportDeviceTelemetryMsgRateLimit);
this.transportDeviceTelemetryDataPointsRateLimit = RateLimitUtil.deduplicateByDuration(transportDeviceTelemetryDataPointsRateLimit);
this.transportGatewayMsgRateLimit = RateLimitUtil.deduplicateByDuration(transportGatewayMsgRateLimit);
this.transportGatewayTelemetryMsgRateLimit = RateLimitUtil.deduplicateByDuration(transportGatewayTelemetryMsgRateLimit);
this.transportGatewayTelemetryDataPointsRateLimit = RateLimitUtil.deduplicateByDuration(transportGatewayTelemetryDataPointsRateLimit);
this.transportGatewayDeviceMsgRateLimit = RateLimitUtil.deduplicateByDuration(transportGatewayDeviceMsgRateLimit);
this.transportGatewayDeviceTelemetryMsgRateLimit = RateLimitUtil.deduplicateByDuration(transportGatewayDeviceTelemetryMsgRateLimit);
this.transportGatewayDeviceTelemetryDataPointsRateLimit = RateLimitUtil.deduplicateByDuration(transportGatewayDeviceTelemetryDataPointsRateLimit);
this.tenantEntityExportRateLimit = RateLimitUtil.deduplicateByDuration(tenantEntityExportRateLimit);
this.tenantEntityImportRateLimit = RateLimitUtil.deduplicateByDuration(tenantEntityImportRateLimit);
this.tenantNotificationRequestsRateLimit = RateLimitUtil.deduplicateByDuration(tenantNotificationRequestsRateLimit);
this.tenantNotificationRequestsPerRuleRateLimit = RateLimitUtil.deduplicateByDuration(tenantNotificationRequestsPerRuleRateLimit);
this.cassandraReadQueryTenantCoreRateLimits = RateLimitUtil.deduplicateByDuration(cassandraReadQueryTenantCoreRateLimits);
this.cassandraWriteQueryTenantCoreRateLimits = RateLimitUtil.deduplicateByDuration(cassandraWriteQueryTenantCoreRateLimits);
this.cassandraReadQueryTenantRuleEngineRateLimits = RateLimitUtil.deduplicateByDuration(cassandraReadQueryTenantRuleEngineRateLimits);
this.cassandraWriteQueryTenantRuleEngineRateLimits = RateLimitUtil.deduplicateByDuration(cassandraWriteQueryTenantRuleEngineRateLimits);
this.edgeEventRateLimits = RateLimitUtil.deduplicateByDuration(edgeEventRateLimits);
this.edgeEventRateLimitsPerEdge = RateLimitUtil.deduplicateByDuration(edgeEventRateLimitsPerEdge);
this.edgeUplinkMessagesRateLimits = RateLimitUtil.deduplicateByDuration(edgeUplinkMessagesRateLimits);
this.edgeUplinkMessagesRateLimitsPerEdge = RateLimitUtil.deduplicateByDuration(edgeUplinkMessagesRateLimitsPerEdge);
this.wsUpdatesPerSessionRateLimit = RateLimitUtil.deduplicateByDuration(wsUpdatesPerSessionRateLimit);
this.tenantServerRestLimitsConfiguration = RateLimitUtil.deduplicateByDuration(tenantServerRestLimitsConfiguration);
this.customerServerRestLimitsConfiguration = RateLimitUtil.deduplicateByDuration(customerServerRestLimitsConfiguration);
}
}

View File

@ -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());

View File

@ -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;
}

View File

@ -203,6 +203,25 @@ public class EdqsProcessor implements TbQueueHandler<TbProtoQueueMsg<ToEdqsMsg>,
});
}
@Override
public TbProtoQueueMsg<FromEdqsMsg> constructErrorResponseMsg(TbProtoQueueMsg<ToEdqsMsg> request, Throwable e) {
EdqsResponse response = new EdqsResponse();
String errorMessage;
if (e instanceof org.apache.kafka.common.errors.RecordTooLargeException) {
errorMessage = "Result set is too large";
} else if (e instanceof IllegalArgumentException || e instanceof NullPointerException) {
errorMessage = "Invalid request format or missing data: " + ExceptionUtil.getMessage(e);
} else {
errorMessage = ExceptionUtil.getMessage(e);
}
response.setError(errorMessage);
return new TbProtoQueueMsg<>(request.getKey(), FromEdqsMsg.newBuilder()
.setResponseMsg(TransportProtos.EdqsResponseMsg.newBuilder()
.setValue(JacksonUtil.toString(response))
.build())
.build(), request.getHeaders());
}
private EdqsResponse processRequest(TenantId tenantId, CustomerId customerId, EdqsRequest request) {
EdqsResponse response = new EdqsResponse();
try {

View File

@ -56,11 +56,9 @@ import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceX509Ce
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.TreeMap;
import java.util.function.Consumer;
@ -538,13 +536,13 @@ public class JsonConverter {
return result;
}
public static Set<AttributeKvEntry> convertToAttributes(JsonElement element) {
public static List<AttributeKvEntry> convertToAttributes(JsonElement element) {
long ts = System.currentTimeMillis();
return convertToAttributes(element, ts);
}
public static Set<AttributeKvEntry> convertToAttributes(JsonElement element, long ts) {
return new HashSet<>(parseValues(element.getAsJsonObject()).stream().map(kv -> new BaseAttributeKvEntry(kv, ts)).toList());
public static List<AttributeKvEntry> convertToAttributes(JsonElement element, long ts) {
return parseValues(element.getAsJsonObject()).stream().<AttributeKvEntry>map(kv -> new BaseAttributeKvEntry(kv, ts)).toList();
}
private static List<KvEntry> parseValues(JsonObject valuesObject) {

View File

@ -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);
}

View File

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

View File

@ -23,8 +23,6 @@ import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.parallel.Isolated;
import java.util.ArrayList;
@Isolated("JsonConverter static settings being modified")
public class JsonConverterTest {
@ -53,7 +51,7 @@ public class JsonConverterTest {
@Test
public void testParseAttributesBigDecimalAsLong() {
var result = new ArrayList<>(JsonConverter.convertToAttributes(JsonParser.parseString("{\"meterReadingDelta\": 1E1}")));
var result = JsonConverter.convertToAttributes(JsonParser.parseString("{\"meterReadingDelta\": 1E1}"));
Assertions.assertEquals(10L, result.get(0).getLongValue().get().longValue());
}
@ -108,4 +106,5 @@ public class JsonConverterTest {
JsonConverter.convertToTelemetry(JsonParser.parseString("{\"meterReadingDelta\": 9.9701010061400066E19}"), 0L);
});
}
}

View File

@ -21,9 +21,11 @@ import lombok.extern.slf4j.Slf4j;
import org.thingsboard.common.util.ThingsBoardExecutors;
import org.thingsboard.server.common.msg.queue.TopicPartitionInfo;
import org.thingsboard.server.common.stats.MessagesStats;
import org.thingsboard.server.queue.TbQueueCallback;
import org.thingsboard.server.queue.TbQueueConsumer;
import org.thingsboard.server.queue.TbQueueHandler;
import org.thingsboard.server.queue.TbQueueMsg;
import org.thingsboard.server.queue.TbQueueMsgMetadata;
import org.thingsboard.server.queue.TbQueueProducer;
import org.thingsboard.server.queue.common.consumer.PartitionedQueueConsumerManager;
@ -119,8 +121,20 @@ public class PartitionedQueueResponseTemplate<Request extends TbQueueMsg, Respon
response -> {
pendingRequestCount.decrementAndGet();
response.getHeaders().put(REQUEST_ID_HEADER, uuidToBytes(requestId));
responseProducer.send(TopicPartitionInfo.builder().topic(responseTopic).build(), response, null);
stats.incrementSuccessful();
TopicPartitionInfo tpi = TopicPartitionInfo.builder().topic(responseTopic).build();
responseProducer.send(tpi, response, new TbQueueCallback() {
@Override
public void onSuccess(TbQueueMsgMetadata metadata) {
stats.incrementSuccessful();
}
@Override
public void onFailure(Throwable t) {
log.error("[{}] Failed to send response {}", requestId, response, t);
sendErrorResponse(requestId, tpi, request, t);
stats.incrementFailed();
}
});
},
e -> {
pendingRequestCount.decrementAndGet();
@ -144,6 +158,15 @@ public class PartitionedQueueResponseTemplate<Request extends TbQueueMsg, Respon
consumer.commit();
}
private void sendErrorResponse(UUID requestId, TopicPartitionInfo tpi, Request request, Throwable cause) {
Response errorResponseMsg = handler.constructErrorResponseMsg(request, cause);
if (errorResponseMsg != null) {
errorResponseMsg.getHeaders().put(REQUEST_ID_HEADER, uuidToBytes(requestId));
responseProducer.send(tpi, errorResponseMsg, null);
}
}
public void subscribe(Set<TopicPartitionInfo> partitions) {
requestConsumer.update(partitions);
}

View File

@ -82,16 +82,18 @@ public class DefaultTransportRateLimitService implements TransportRateLimitServi
if (!checkEntityRateLimit(dataPoints, getTenantRateLimits(tenantId))) {
return TbPair.of(EntityType.TENANT, false);
}
if (isGateway && !checkEntityRateLimit(dataPoints, getGatewayDeviceRateLimits(tenantId, deviceId))) {
return TbPair.of(EntityType.DEVICE, true);
if (isGateway) {
if (!checkEntityRateLimit(dataPoints, getGatewayDeviceRateLimits(tenantId, deviceId))) {
return TbPair.of(EntityType.DEVICE, true);
}
} else if (gatewayId == null && deviceId != null) {
if (!checkEntityRateLimit(dataPoints, getDeviceRateLimits(tenantId, deviceId))) {
return TbPair.of(EntityType.DEVICE, false);
}
}
if (gatewayId != null && !checkEntityRateLimit(dataPoints, getGatewayRateLimits(tenantId, gatewayId))) {
return TbPair.of(EntityType.DEVICE, true);
}
if (!isGateway && deviceId != null && !checkEntityRateLimit(dataPoints, getDeviceRateLimits(tenantId, deviceId))) {
return TbPair.of(EntityType.DEVICE, false);
}
return null;
}

View File

@ -62,9 +62,6 @@ import java.util.function.BiFunction;
import java.util.function.UnaryOperator;
import java.util.regex.Pattern;
/**
* Created by Valerii Sosliuk on 5/12/2017.
*/
@Slf4j
public class JacksonUtil {

View File

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

View File

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

View File

@ -38,6 +38,7 @@ import org.thingsboard.server.dao.util.mapping.JsonConverter;
import java.nio.ByteBuffer;
import java.util.UUID;
import static org.thingsboard.server.dao.model.ModelConstants.EXTERNAL_ID_PROPERTY;
import static org.thingsboard.server.dao.model.ModelConstants.OTA_PACKAGE_CHECKSUM_ALGORITHM_COLUMN;
import static org.thingsboard.server.dao.model.ModelConstants.OTA_PACKAGE_CHECKSUM_COLUMN;
import static org.thingsboard.server.dao.model.ModelConstants.OTA_PACKAGE_CONTENT_TYPE_COLUMN;
@ -105,6 +106,9 @@ public class OtaPackageEntity extends BaseSqlEntity<OtaPackage> {
@Column(name = ModelConstants.OTA_PACKAGE_ADDITIONAL_INFO_COLUMN)
private JsonNode additionalInfo;
@Column(name = EXTERNAL_ID_PROPERTY)
private UUID externalId;
public OtaPackageEntity() {
super();
}
@ -128,6 +132,7 @@ public class OtaPackageEntity extends BaseSqlEntity<OtaPackage> {
this.data = otaPackage.getData().array();
this.dataSize = otaPackage.getDataSize();
this.additionalInfo = otaPackage.getAdditionalInfo();
this.externalId = getUuid(otaPackage.getExternalId());
}
@Override
@ -153,6 +158,8 @@ public class OtaPackageEntity extends BaseSqlEntity<OtaPackage> {
otaPackage.setHasData(true);
}
otaPackage.setAdditionalInfo(additionalInfo);
otaPackage.setExternalId(getEntityId(externalId, OtaPackageId::new));
return otaPackage;
}
}

View File

@ -100,6 +100,9 @@ public class OtaPackageInfoEntity extends BaseSqlEntity<OtaPackageInfo> {
@Column(name = ModelConstants.OTA_PACKAGE_ADDITIONAL_INFO_COLUMN)
private JsonNode additionalInfo;
@Column(name = ModelConstants.EXTERNAL_ID_PROPERTY)
private UUID externalId;
@Transient
private boolean hasData;
@ -125,11 +128,12 @@ public class OtaPackageInfoEntity extends BaseSqlEntity<OtaPackageInfo> {
this.checksum = otaPackageInfo.getChecksum();
this.dataSize = otaPackageInfo.getDataSize();
this.additionalInfo = otaPackageInfo.getAdditionalInfo();
this.externalId = getUuid(otaPackageInfo.getExternalId());
}
public OtaPackageInfoEntity(UUID id, long createdTime, UUID tenantId, UUID deviceProfileId, OtaPackageType type, String title, String version, String tag,
String url, String fileName, String contentType, ChecksumAlgorithm checksumAlgorithm, String checksum, Long dataSize,
Object additionalInfo, boolean hasData) {
Object additionalInfo, UUID externalId, boolean hasData) {
this.id = id;
this.createdTime = createdTime;
this.tenantId = tenantId;
@ -146,6 +150,7 @@ public class OtaPackageInfoEntity extends BaseSqlEntity<OtaPackageInfo> {
this.dataSize = dataSize;
this.hasData = hasData;
this.additionalInfo = JacksonUtil.convertValue(additionalInfo, JsonNode.class);
this.externalId = externalId;
}
@Override
@ -168,6 +173,8 @@ public class OtaPackageInfoEntity extends BaseSqlEntity<OtaPackageInfo> {
otaPackageInfo.setDataSize(dataSize);
otaPackageInfo.setAdditionalInfo(additionalInfo);
otaPackageInfo.setHasData(hasData);
otaPackageInfo.setExternalId(getEntityId(externalId, OtaPackageId::new));
return otaPackageInfo;
}
}

View File

@ -54,6 +54,7 @@ import static org.thingsboard.server.dao.service.Validator.validatePageLink;
@Slf4j
@RequiredArgsConstructor
public class BaseOtaPackageService extends AbstractCachedEntityService<OtaPackageCacheKey, OtaPackageInfo, OtaPackageCacheEvictEvent> implements OtaPackageService {
public static final String INCORRECT_OTA_PACKAGE_ID = "Incorrect otaPackageId ";
public static final String INCORRECT_TENANT_ID = "Incorrect tenantId ";
@ -73,7 +74,7 @@ public class BaseOtaPackageService extends AbstractCachedEntityService<OtaPackag
@Override
public OtaPackageInfo saveOtaPackageInfo(OtaPackageInfo otaPackageInfo, boolean isUrl) {
log.trace("Executing saveOtaPackageInfo [{}]", otaPackageInfo);
if (isUrl && (StringUtils.isEmpty(otaPackageInfo.getUrl()) || otaPackageInfo.getUrl().trim().length() == 0)) {
if (isUrl && StringUtils.isBlank(otaPackageInfo.getUrl())) {
throw new DataValidationException("Ota package URL should be specified!");
}
otaPackageInfoValidator.validate(otaPackageInfo, OtaPackageInfo::getTenantId);
@ -90,12 +91,10 @@ public class BaseOtaPackageService extends AbstractCachedEntityService<OtaPackag
if (otaPackageId != null) {
handleEvictEvent(new OtaPackageCacheEvictEvent(otaPackageId));
}
ConstraintViolationException e = extractConstraintViolationException(t).orElse(null);
if (e != null && e.getConstraintName() != null && e.getConstraintName().equalsIgnoreCase("ota_package_tenant_title_version_unq_key")) {
throw new DataValidationException("OtaPackage with such title and version already exists!");
} else {
throw t;
}
checkConstraintViolation(t,
"ota_package_tenant_title_version_unq_key", "OtaPackage with such title and version already exists!",
"ota_package_external_id_unq_key", "OtaPackage with such external id already exists!");
throw t;
}
}
@ -116,12 +115,10 @@ public class BaseOtaPackageService extends AbstractCachedEntityService<OtaPackag
if (otaPackageId != null) {
handleEvictEvent(new OtaPackageCacheEvictEvent(otaPackageId));
}
ConstraintViolationException e = extractConstraintViolationException(t).orElse(null);
if (e != null && e.getConstraintName() != null && e.getConstraintName().equalsIgnoreCase("ota_package_tenant_title_version_unq_key")) {
throw new DataValidationException("OtaPackage with such title and version already exists!");
} else {
throw t;
}
checkConstraintViolation(t,
"ota_package_tenant_title_version_unq_key", "OtaPackage with such title and version already exists!",
"ota_package_external_id_unq_key", "OtaPackage with such external id already exists!");
throw t;
}
}
@ -136,24 +133,16 @@ public class BaseOtaPackageService extends AbstractCachedEntityService<OtaPackag
@SuppressWarnings("deprecation")
private HashFunction getHashFunction(ChecksumAlgorithm checksumAlgorithm) {
switch (checksumAlgorithm) {
case MD5:
return Hashing.md5();
case SHA256:
return Hashing.sha256();
case SHA384:
return Hashing.sha384();
case SHA512:
return Hashing.sha512();
case CRC32:
return Hashing.crc32();
case MURMUR3_32:
return Hashing.murmur3_32();
case MURMUR3_128:
return Hashing.murmur3_128();
default:
throw new DataValidationException("Unknown checksum algorithm!");
}
return switch (checksumAlgorithm) {
case MD5 -> Hashing.md5();
case SHA256 -> Hashing.sha256();
case SHA384 -> Hashing.sha384();
case SHA512 -> Hashing.sha512();
case CRC32 -> Hashing.crc32();
case MURMUR3_32 -> Hashing.murmur3_32();
case MURMUR3_128 -> Hashing.murmur3_128();
default -> throw new DataValidationException("Unknown checksum algorithm!");
};
}
@Override
@ -171,6 +160,12 @@ public class BaseOtaPackageService extends AbstractCachedEntityService<OtaPackag
() -> otaPackageInfoDao.findById(tenantId, otaPackageId.getId()), true);
}
@Override
public OtaPackage findOtaPackageByTenantIdAndTitleAndVersion(TenantId tenantId, String title, String version) {
log.trace("Executing findOtaPackageByTenantIdAndTitle [{}] [{}] [{}]", tenantId, title, version);
return otaPackageDao.findOtaPackageByTenantIdAndTitleAndVersion(tenantId, title, version);
}
@Override
public ListenableFuture<OtaPackageInfo> findOtaPackageInfoByIdAsync(TenantId tenantId, OtaPackageId otaPackageId) {
log.trace("Executing findOtaPackageInfoByIdAsync [{}]", otaPackageId);

View File

@ -16,12 +16,17 @@
package org.thingsboard.server.dao.ota;
import org.thingsboard.server.common.data.OtaPackage;
import org.thingsboard.server.common.data.id.OtaPackageId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.ota.OtaPackageType;
import org.thingsboard.server.dao.Dao;
import org.thingsboard.server.dao.ExportableEntityDao;
import org.thingsboard.server.dao.TenantEntityWithDataDao;
public interface OtaPackageDao extends Dao<OtaPackage>, TenantEntityWithDataDao {
public interface OtaPackageDao extends Dao<OtaPackage>, TenantEntityWithDataDao, ExportableEntityDao<OtaPackageId, OtaPackage> {
Long sumDataSizeByTenantId(TenantId tenantId);
OtaPackage findOtaPackageByTenantIdAndTitleAndVersion(TenantId tenantId, String title, String version);
}

View File

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

View File

@ -103,4 +103,5 @@ public class OtaPackageDataValidator extends BaseOtaPackageDataValidator<OtaPack
}
return otaPackageOld;
}
}

View File

@ -40,9 +40,6 @@ import java.util.List;
import java.util.Optional;
import java.util.UUID;
/**
* @author Valerii Sosliuk
*/
@Slf4j
@SqlDao
public abstract class JpaAbstractDao<E extends BaseEntity<D>, D>

View File

@ -48,9 +48,6 @@ import java.util.UUID;
import static org.thingsboard.server.dao.DaoUtil.convertTenantEntityInfosToDto;
/**
* Created by Valerii Sosliuk on 5/19/2017.
*/
@Component
@SqlDao
@Slf4j

View File

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

View File

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

View File

@ -22,6 +22,7 @@ import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.OtaPackage;
import org.thingsboard.server.common.data.id.OtaPackageId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
@ -42,6 +43,45 @@ public class JpaOtaPackageDao extends JpaAbstractDao<OtaPackageEntity, OtaPackag
@Autowired
private OtaPackageRepository otaPackageRepository;
@Override
public Long sumDataSizeByTenantId(TenantId tenantId) {
return otaPackageRepository.sumDataSizeByTenantId(tenantId.getId());
}
@Transactional
@Override
public OtaPackage findOtaPackageByTenantIdAndTitleAndVersion(TenantId tenantId, String title, String version) {
return DaoUtil.getData(otaPackageRepository.findByTenantIdAndTitleAndVersion(tenantId.getId(), title, version));
}
@Transactional
@Override
public PageData<OtaPackage> findAllByTenantId(TenantId tenantId, PageLink pageLink) {
return DaoUtil.toPageData(otaPackageRepository.findByTenantId(tenantId.getId(), DaoUtil.toPageable(pageLink)));
}
@Transactional
@Override
public PageData<OtaPackage> findByTenantId(UUID tenantId, PageLink pageLink) {
return findAllByTenantId(TenantId.fromUUID(tenantId), pageLink);
}
@Override
public PageData<OtaPackageId> findIdsByTenantId(UUID tenantId, PageLink pageLink) {
return DaoUtil.pageToPageData(otaPackageRepository.findIdsByTenantId(tenantId, DaoUtil.toPageable(pageLink)).map(OtaPackageId::new));
}
@Transactional
@Override
public OtaPackage findByTenantIdAndExternalId(UUID tenantId, UUID externalId) {
return DaoUtil.getData(otaPackageRepository.findByTenantIdAndExternalId(tenantId, externalId));
}
@Override
public OtaPackageId getExternalIdByInternal(OtaPackageId internalId) {
return DaoUtil.toEntityId(otaPackageRepository.getExternalIdById(internalId.getId()), OtaPackageId::new);
}
@Override
protected Class<OtaPackageEntity> getEntityClass() {
return OtaPackageEntity.class;
@ -52,17 +92,6 @@ public class JpaOtaPackageDao extends JpaAbstractDao<OtaPackageEntity, OtaPackag
return otaPackageRepository;
}
@Override
public Long sumDataSizeByTenantId(TenantId tenantId) {
return otaPackageRepository.sumDataSizeByTenantId(tenantId.getId());
}
@Transactional
@Override
public PageData<OtaPackage> findAllByTenantId(TenantId tenantId, PageLink pageLink) {
return DaoUtil.toPageData(otaPackageRepository.findByTenantId(tenantId.getId(), DaoUtil.toPageable(pageLink)));
}
@Override
public EntityType getEntityType() {
return EntityType.OTA_PACKAGE;

View File

@ -26,14 +26,15 @@ import org.thingsboard.server.dao.model.sql.OtaPackageInfoEntity;
import java.util.UUID;
public interface OtaPackageInfoRepository extends JpaRepository<OtaPackageInfoEntity, UUID> {
@Query("SELECT new OtaPackageInfoEntity(f.id, f.createdTime, f.tenantId, f.deviceProfileId, f.type, f.title, f.version, f.tag, f.url, f.fileName, f.contentType, f.checksumAlgorithm, f.checksum, f.dataSize, f.additionalInfo, CASE WHEN (f.data IS NOT NULL OR f.url IS NOT NULL) THEN true ELSE false END) FROM OtaPackageEntity f WHERE " +
@Query("SELECT new OtaPackageInfoEntity(f.id, f.createdTime, f.tenantId, f.deviceProfileId, f.type, f.title, f.version, f.tag, f.url, f.fileName, f.contentType, f.checksumAlgorithm, f.checksum, f.dataSize, f.additionalInfo, f.externalId, CASE WHEN (f.data IS NOT NULL OR f.url IS NOT NULL) THEN true ELSE false END) FROM OtaPackageEntity f WHERE " +
"f.tenantId = :tenantId " +
"AND (:searchText IS NULL OR ilike(f.title, CONCAT('%', :searchText, '%')) = true)")
Page<OtaPackageInfoEntity> findAllByTenantId(@Param("tenantId") UUID tenantId,
@Param("searchText") String searchText,
Pageable pageable);
@Query("SELECT new OtaPackageInfoEntity(f.id, f.createdTime, f.tenantId, f.deviceProfileId, f.type, f.title, f.version, f.tag, f.url, f.fileName, f.contentType, f.checksumAlgorithm, f.checksum, f.dataSize, f.additionalInfo, true) FROM OtaPackageEntity f WHERE " +
@Query("SELECT new OtaPackageInfoEntity(f.id, f.createdTime, f.tenantId, f.deviceProfileId, f.type, f.title, f.version, f.tag, f.url, f.fileName, f.contentType, f.checksumAlgorithm, f.checksum, f.dataSize, f.additionalInfo, f.externalId, true) FROM OtaPackageEntity f WHERE " +
"f.tenantId = :tenantId " +
"AND f.deviceProfileId = :deviceProfileId " +
"AND f.type = :type " +
@ -45,7 +46,7 @@ public interface OtaPackageInfoRepository extends JpaRepository<OtaPackageInfoEn
@Param("searchText") String searchText,
Pageable pageable);
@Query("SELECT new OtaPackageInfoEntity(f.id, f.createdTime, f.tenantId, f.deviceProfileId, f.type, f.title, f.version, f.tag, f.url, f.fileName, f.contentType, f.checksumAlgorithm, f.checksum, f.dataSize, f.additionalInfo, CASE WHEN (f.data IS NOT NULL OR f.url IS NOT NULL) THEN true ELSE false END) FROM OtaPackageEntity f WHERE f.id = :id")
@Query("SELECT new OtaPackageInfoEntity(f.id, f.createdTime, f.tenantId, f.deviceProfileId, f.type, f.title, f.version, f.tag, f.url, f.fileName, f.contentType, f.checksumAlgorithm, f.checksum, f.dataSize, f.additionalInfo, f.externalId, CASE WHEN (f.data IS NOT NULL OR f.url IS NOT NULL) THEN true ELSE false END) FROM OtaPackageEntity f WHERE f.id = :id")
OtaPackageInfoEntity findOtaPackageInfoById(@Param("id") UUID id);
@Query(value = "SELECT exists(SELECT * " +

View File

@ -20,15 +20,25 @@ import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.thingsboard.server.common.data.ota.OtaPackageType;
import org.thingsboard.server.dao.ExportableEntityRepository;
import org.thingsboard.server.dao.model.sql.OtaPackageEntity;
import java.util.UUID;
public interface OtaPackageRepository extends JpaRepository<OtaPackageEntity, UUID> {
public interface OtaPackageRepository extends JpaRepository<OtaPackageEntity, UUID>, ExportableEntityRepository<OtaPackageEntity> {
@Query(value = "SELECT COALESCE(SUM(ota.data_size), 0) FROM ota_package ota WHERE ota.tenant_id = :tenantId AND ota.data IS NOT NULL", nativeQuery = true)
Long sumDataSizeByTenantId(@Param("tenantId") UUID tenantId);
Page<OtaPackageEntity> findByTenantId(UUID tenantId, Pageable pageable);
OtaPackageEntity findByTenantIdAndTitleAndVersion(UUID tenantId, String title, String version);
@Query("SELECT externalId FROM OtaPackageEntity WHERE id = :id")
UUID getExternalIdById(@Param("id") UUID id);
@Query("SELECT r.id FROM OtaPackageEntity r WHERE r.tenantId = :tenantId")
Page<UUID> findIdsByTenantId(@Param("tenantId") UUID tenantId, Pageable pageable);
}

View File

@ -73,22 +73,6 @@ CREATE INDEX IF NOT EXISTS idx_edge_event_id ON edge_event(id);
CREATE INDEX IF NOT EXISTS idx_rpc_tenant_id_device_id ON rpc(tenant_id, device_id);
CREATE INDEX IF NOT EXISTS idx_device_external_id ON device(tenant_id, external_id);
CREATE INDEX IF NOT EXISTS idx_device_profile_external_id ON device_profile(tenant_id, external_id);
CREATE INDEX IF NOT EXISTS idx_asset_external_id ON asset(tenant_id, external_id);
CREATE INDEX IF NOT EXISTS idx_entity_view_external_id ON entity_view(tenant_id, external_id);
CREATE INDEX IF NOT EXISTS idx_rule_chain_external_id ON rule_chain(tenant_id, external_id);
CREATE INDEX IF NOT EXISTS idx_dashboard_external_id ON dashboard(tenant_id, external_id);
CREATE INDEX IF NOT EXISTS idx_customer_external_id ON customer(tenant_id, external_id);
CREATE INDEX IF NOT EXISTS idx_widgets_bundle_external_id ON widgets_bundle(tenant_id, external_id);
CREATE INDEX IF NOT EXISTS idx_rule_node_external_id ON rule_node(rule_chain_id, external_id);
CREATE INDEX IF NOT EXISTS idx_rule_node_type_id_configuration_version ON rule_node(type, id, configuration_version);

View File

@ -216,7 +216,9 @@ CREATE TABLE IF NOT EXISTS ota_package (
data oid,
data_size bigint,
additional_info varchar,
CONSTRAINT ota_package_tenant_title_version_unq_key UNIQUE (tenant_id, title, version)
external_id uuid,
CONSTRAINT ota_package_tenant_title_version_unq_key UNIQUE (tenant_id, title, version),
CONSTRAINT ota_package_external_id_unq_key UNIQUE (tenant_id, external_id)
);
CREATE TABLE IF NOT EXISTS queue (

View File

@ -15,4 +15,4 @@ TB_QUEUE_KAFKA_RE_TOPIC_PROPERTIES=retention.ms:604800000;segment.bytes:52428800
TB_QUEUE_KAFKA_CORE_TOPIC_PROPERTIES=retention.ms:604800000;segment.bytes:52428800;retention.bytes:1048576000
TB_QUEUE_KAFKA_TA_TOPIC_PROPERTIES=retention.ms:604800000;segment.bytes:52428800;retention.bytes:1048576000
TB_QUEUE_KAFKA_NOTIFICATIONS_TOPIC_PROPERTIES=retention.ms:604800000;segment.bytes:52428800;retention.bytes:1048576000
TB_QUEUE_KAFKA_JE_TOPIC_PROPERTIES=retention.ms:604800000;segment.bytes:52428800;retention.bytes:104857600
TB_QUEUE_KAFKA_JE_TOPIC_PROPERTIES=retention.ms:86400000;segment.bytes:52428800;retention.bytes:104857600

View File

@ -44,7 +44,6 @@ import org.thingsboard.server.common.msg.TbMsg;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import static org.thingsboard.server.common.data.msg.TbMsgType.ACTIVITY_EVENT;
@ -115,14 +114,13 @@ public class TbCopyAttributesToEntityViewNode implements TbNode {
.build());
}
} else {
Set<AttributeKvEntry> attributes = JsonConverter.convertToAttributes(JsonParser.parseString(msg.getData()));
List<AttributeKvEntry> filteredAttributes =
attributes.stream().filter(attr -> attributeContainsInEntityView(scope, attr.getKey(), entityView)).collect(Collectors.toList());
List<AttributeKvEntry> attributes = JsonConverter.convertToAttributes(JsonParser.parseString(msg.getData())).stream()
.filter(attr -> attributeContainsInEntityView(scope, attr.getKey(), entityView)).toList();
ctx.getTelemetryService().saveAttributes(AttributesSaveRequest.builder()
.tenantId(ctx.getTenantId())
.entityId(entityView.getId())
.scope(scope)
.entries(filteredAttributes)
.entries(attributes)
.callback(getFutureCallback(ctx, msg, entityView))
.build());
}

View File

@ -258,7 +258,7 @@ class DeviceState {
private boolean processAttributes(TbContext ctx, TbMsg msg, String scope) throws ExecutionException, InterruptedException {
boolean stateChanged = false;
Set<AttributeKvEntry> attributes = JsonConverter.convertToAttributes(JsonParser.parseString(msg.getData()));
List<AttributeKvEntry> attributes = JsonConverter.convertToAttributes(JsonParser.parseString(msg.getData()));
if (!attributes.isEmpty()) {
SnapshotUpdate update = merge(latestValues, attributes, scope);
for (DeviceProfileAlarm alarm : deviceProfile.getAlarmSettings()) {
@ -321,7 +321,7 @@ class DeviceState {
return new SnapshotUpdate(AlarmConditionKeyType.TIME_SERIES, keys);
}
private SnapshotUpdate merge(DataSnapshot latestValues, Set<AttributeKvEntry> attributes, String scope) {
private SnapshotUpdate merge(DataSnapshot latestValues, List<AttributeKvEntry> attributes, String scope) {
long newTs = 0;
Set<AlarmConditionFilterKey> keys = new HashSet<>();
for (AttributeKvEntry entry : attributes) {

View File

@ -103,8 +103,7 @@ public class TbCalculatedFieldsNode implements TbNode {
}
private void processPostAttributesRequest(TbContext ctx, TbMsg msg) {
List<AttributeKvEntry> newAttributes = new ArrayList<>(JsonConverter.convertToAttributes(JsonParser.parseString(msg.getData())));
List<AttributeKvEntry> newAttributes = JsonConverter.convertToAttributes(JsonParser.parseString(msg.getData()));
if (newAttributes.isEmpty()) {
ctx.tellSuccess(msg);
return;

View File

@ -41,7 +41,6 @@ import org.thingsboard.server.common.data.plugin.ComponentType;
import org.thingsboard.server.common.data.util.TbPair;
import org.thingsboard.server.common.msg.TbMsg;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
@ -133,7 +132,7 @@ public class TbMsgAttributesNode implements TbNode {
return;
}
String src = msg.getData();
List<AttributeKvEntry> newAttributes = new ArrayList<>(JsonConverter.convertToAttributes(JsonParser.parseString(src)));
List<AttributeKvEntry> newAttributes = JsonConverter.convertToAttributes(JsonParser.parseString(src));
if (newAttributes.isEmpty()) {
ctx.tellSuccess(msg);
return;

View File

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

View File

@ -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();

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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;

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