Merge branch 'master' into ai
# Conflicts: # common/data/src/main/java/org/thingsboard/server/common/data/sync/JsonTbEntity.java
This commit is contained in:
commit
9e96bc5a02
@ -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
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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() {}
|
||||
|
||||
|
||||
@ -107,4 +107,5 @@ public class EdgeSyncCursor {
|
||||
currentIdx++;
|
||||
return edgeEventFetcher;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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));
|
||||
|
||||
@ -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));
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -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);
|
||||
|
||||
}
|
||||
@ -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));
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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));
|
||||
|
||||
@ -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);
|
||||
|
||||
}
|
||||
|
||||
@ -110,4 +110,5 @@ public class DefaultTbOtaPackageService extends AbstractTbEntityService implemen
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@ -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()
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@ -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
|
||||
|
||||
@ -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())
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
@ -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()));
|
||||
}
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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);
|
||||
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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!");
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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(","));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@ -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;
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -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());
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
|
||||
@ -697,6 +697,8 @@ message ToDeviceRpcRequestMsg {
|
||||
int64 requestIdLSB = 6;
|
||||
bool oneway = 7;
|
||||
bool persisted = 8;
|
||||
optional int32 retries = 9;
|
||||
string additionalInfo = 10;
|
||||
}
|
||||
|
||||
message ToDeviceRpcResponseMsg {
|
||||
|
||||
@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -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 {
|
||||
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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);
|
||||
|
||||
}
|
||||
|
||||
@ -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()));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -103,4 +103,5 @@ public class OtaPackageDataValidator extends BaseOtaPackageDataValidator<OtaPack
|
||||
}
|
||||
return otaPackageOld;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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 * " +
|
||||
|
||||
@ -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);
|
||||
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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 (
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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());
|
||||
}
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -340,6 +340,7 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit,
|
||||
public onDataUpdated() {
|
||||
this.alarmsDatasource.updateAlarms();
|
||||
this.clearCache();
|
||||
this.ctx.detectChanges();
|
||||
}
|
||||
|
||||
public onEditModeChanged() {
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -58,6 +58,9 @@ export class BarChartWithLabelsWidgetComponent implements OnInit, OnDestroy, Aft
|
||||
@Input()
|
||||
ctx: WidgetContext;
|
||||
|
||||
@Input()
|
||||
widgetTitlePanel: TemplateRef<any>;
|
||||
|
||||
showLegend: boolean;
|
||||
legendClass: string;
|
||||
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
|
||||
@ -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++,
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -61,6 +61,9 @@ export class TimeSeriesChartWidgetComponent implements OnInit, OnDestroy, AfterV
|
||||
@Input()
|
||||
ctx: WidgetContext;
|
||||
|
||||
@Input()
|
||||
widgetTitlePanel: TemplateRef<any>;
|
||||
|
||||
horizontalLegendPosition = false;
|
||||
|
||||
showLegend: boolean;
|
||||
|
||||
@ -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
Loading…
x
Reference in New Issue
Block a user