From cf3b8b81ad25f7c4ab55ba2bc98846f76f9005bf Mon Sep 17 00:00:00 2001 From: Yevhen Popok Date: Wed, 17 Jan 2024 11:57:14 +0200 Subject: [PATCH 01/54] Update description for CASSANDRA_QUERY_SET_NULL_VALUES_ENABLED --- application/src/main/resources/thingsboard.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index 2f710f3e92..4932585bd5 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -308,7 +308,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 different values with the same timeseries). set_null_values_enabled: "${CASSANDRA_QUERY_SET_NULL_VALUES_ENABLED:false}" # log one of cassandra queries with specified frequency (0 - logging is disabled) print_queries_freq: "${CASSANDRA_QUERY_PRINT_FREQ:0}" From ce485d4e101b40419b15c45ea9d0a6eb2d546505 Mon Sep 17 00:00:00 2001 From: Yevhen Popok Date: Mon, 29 Jan 2024 15:21:48 +0200 Subject: [PATCH 02/54] Clarify use-case for the "cassandra.query.set_null_values_enabled" parameter --- application/src/main/resources/thingsboard.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index 4932585bd5..83f2438ded 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -308,7 +308,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}" - # When saving a value, set other data types to null (to avoid different values with the same timeseries). + # When saving a value, set other data types to null (to avoid having multiple telemetry values with the same timeseries). set_null_values_enabled: "${CASSANDRA_QUERY_SET_NULL_VALUES_ENABLED:false}" # log one of cassandra queries with specified frequency (0 - logging is disabled) print_queries_freq: "${CASSANDRA_QUERY_PRINT_FREQ:0}" From 8584edf3abd927b0d694d0736c84114baafa5f34 Mon Sep 17 00:00:00 2001 From: Yevhen Popok Date: Mon, 29 Jan 2024 15:26:11 +0200 Subject: [PATCH 03/54] Fix updated description for the "cassandra.query.set_null_values_enabled" parameter --- application/src/main/resources/thingsboard.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index 83f2438ded..6c79dc36d2 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -308,7 +308,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}" - # When saving a value, set other data types to null (to avoid having multiple telemetry values with the same timeseries). + # 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:false}" # log one of cassandra queries with specified frequency (0 - logging is disabled) print_queries_freq: "${CASSANDRA_QUERY_PRINT_FREQ:0}" From 8bb4e9dc0b0e5ac3567c16c983c2eedef39bc23e Mon Sep 17 00:00:00 2001 From: Sergey Matvienko Date: Wed, 16 Apr 2025 18:31:11 +0200 Subject: [PATCH 04/54] TB_QUEUE_KAFKA_JE_TOPIC_PROPERTIES retention 1 day, 30 partitions for new deployments --- application/src/main/resources/thingsboard.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index d1910399f0..e23a27c1c9 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -1644,7 +1644,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 From 0efa8301cb50a245ccff54cbc3b8f6d7390bbb5d Mon Sep 17 00:00:00 2001 From: Sergey Matvienko Date: Wed, 16 Apr 2025 18:34:37 +0200 Subject: [PATCH 05/54] Confluent TB_QUEUE_KAFKA_JE_TOPIC_PROPERTIES retention 1 day --- docker/queue-confluent.env | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/queue-confluent.env b/docker/queue-confluent.env index 868a135de3..900504c4ea 100644 --- a/docker/queue-confluent.env +++ b/docker/queue-confluent.env @@ -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 From 4212f9dde469ed6615b1caec542f40e9748c8ca6 Mon Sep 17 00:00:00 2001 From: LeoMorgan113 Date: Fri, 16 May 2025 15:19:01 +0300 Subject: [PATCH 06/54] Fix for widgetTitlePanel templateOutlet --- .../lib/chart/bar-chart-with-labels-widget.component.html | 2 +- .../lib/chart/bar-chart-with-labels-widget.component.ts | 3 +++ .../widget/lib/chart/range-chart-widget.component.html | 2 +- .../widget/lib/chart/range-chart-widget.component.ts | 3 +++ .../widget/lib/chart/time-series-chart-widget.component.html | 2 +- .../widget/lib/chart/time-series-chart-widget.component.ts | 3 +++ .../home/components/widget/lib/maps/map-widget.component.html | 2 +- .../home/components/widget/lib/maps/map-widget.component.ts | 4 ++++ 8 files changed, 17 insertions(+), 4 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/bar-chart-with-labels-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/chart/bar-chart-with-labels-widget.component.html index 0e0170c603..3f4835f475 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/bar-chart-with-labels-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/bar-chart-with-labels-widget.component.html @@ -19,7 +19,7 @@
@if (widgetComponent.dashboardWidget.showWidgetTitlePanel) {
- +
} @else { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/bar-chart-with-labels-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/bar-chart-with-labels-widget.component.ts index 746daf43b3..14e77ae738 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/bar-chart-with-labels-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/bar-chart-with-labels-widget.component.ts @@ -58,6 +58,9 @@ export class BarChartWithLabelsWidgetComponent implements OnInit, OnDestroy, Aft @Input() ctx: WidgetContext; + @Input() + widgetTitlePanel: TemplateRef; + showLegend: boolean; legendClass: string; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/range-chart-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/chart/range-chart-widget.component.html index 39112c484b..fc16fbdc13 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/range-chart-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/range-chart-widget.component.html @@ -19,7 +19,7 @@
@if (widgetComponent.dashboardWidget.showWidgetTitlePanel) {
- +
} @else { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/range-chart-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/range-chart-widget.component.ts index 0201ec7d5f..92bc8e887f 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/range-chart-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/range-chart-widget.component.ts @@ -66,6 +66,9 @@ export class RangeChartWidgetComponent implements OnInit, OnDestroy, AfterViewIn @Input() ctx: WidgetContext; + @Input() + widgetTitlePanel: TemplateRef; + showLegend: boolean; legendClass: string; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart-widget.component.html index 1efcf30781..fe7335c92d 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart-widget.component.html @@ -19,7 +19,7 @@
@if (widgetComponent.dashboardWidget.showWidgetTitlePanel) {
- +
} @else { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart-widget.component.ts index 2fd12b1b98..2e9f297b4a 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart-widget.component.ts @@ -61,6 +61,9 @@ export class TimeSeriesChartWidgetComponent implements OnInit, OnDestroy, AfterV @Input() ctx: WidgetContext; + @Input() + widgetTitlePanel: TemplateRef; + horizontalLegendPosition = false; showLegend: boolean; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget.component.html index e64ee71170..c0aceeabb0 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget.component.html @@ -19,7 +19,7 @@
@if (widgetComponent.dashboardWidget.showWidgetTitlePanel) {
- +
} @else { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget.component.ts index 4ef3c3cd45..013a6c88a9 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget.component.ts @@ -21,6 +21,7 @@ import { Input, OnDestroy, OnInit, + TemplateRef, ViewChild, ViewEncapsulation } from '@angular/core'; @@ -54,6 +55,9 @@ export class MapWidgetComponent implements OnInit, OnDestroy { @Input() ctx: WidgetContext; + @Input() + widgetTitlePanel: TemplateRef; + backgroundStyle$: Observable; overlayStyle: ComponentStyle = {}; padding: string; From 2ffec818125bf16e7fd736ea901272c3b7edb5b2 Mon Sep 17 00:00:00 2001 From: Yevhenii Date: Mon, 2 Jun 2025 12:28:44 +0300 Subject: [PATCH 07/54] CalculatedField functionality support for Edge - added CalculatedField functionality for Edge --- .../service/edge/EdgeContextComponent.java | 8 ++ .../service/edge/EdgeMsgConstructorUtils.java | 16 +++ .../service/edge/rpc/EdgeGrpcSession.java | 7 +- .../service/edge/rpc/EdgeSyncCursor.java | 3 + .../CalculatedFieldsEdgeEventFetcher.java | 48 +++++++ .../BaseCalculatedFieldProcessor.java | 79 +++++++++++ .../CalculatedFieldEdgeProcessor.java | 133 ++++++++++++++++++ .../calculated/CalculatedFieldProcessor.java | 28 ++++ .../server/dao/cf/CalculatedFieldService.java | 4 + .../common/data/edge/EdgeEventType.java | 9 +- .../common/data/id/EntityIdFactory.java | 2 + common/edge-api/src/main/proto/edge.proto | 10 ++ .../dao/cf/BaseCalculatedFieldService.java | 23 ++- .../server/dao/cf/CalculatedFieldDao.java | 3 + .../dao/sql/cf/CalculatedFieldRepository.java | 2 + .../dao/sql/cf/JpaCalculatedFieldDao.java | 7 + 16 files changed, 375 insertions(+), 7 deletions(-) create mode 100644 application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/CalculatedFieldsEdgeEventFetcher.java create mode 100644 application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/calculated/BaseCalculatedFieldProcessor.java create mode 100644 application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/calculated/CalculatedFieldEdgeProcessor.java create mode 100644 application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/calculated/CalculatedFieldProcessor.java diff --git a/application/src/main/java/org/thingsboard/server/service/edge/EdgeContextComponent.java b/application/src/main/java/org/thingsboard/server/service/edge/EdgeContextComponent.java index 00ae4a45bd..abdc89ebd9 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/EdgeContextComponent.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/EdgeContextComponent.java @@ -29,6 +29,7 @@ import org.thingsboard.server.dao.alarm.AlarmService; import org.thingsboard.server.dao.asset.AssetProfileService; import org.thingsboard.server.dao.asset.AssetService; import org.thingsboard.server.dao.attributes.AttributesService; +import org.thingsboard.server.dao.cf.CalculatedFieldService; import org.thingsboard.server.dao.customer.CustomerService; import org.thingsboard.server.dao.dashboard.DashboardService; import org.thingsboard.server.dao.device.DeviceCredentialsService; @@ -61,6 +62,7 @@ import org.thingsboard.server.service.edge.rpc.processor.alarm.AlarmProcessor; import org.thingsboard.server.service.edge.rpc.processor.alarm.comment.AlarmCommentProcessor; import org.thingsboard.server.service.edge.rpc.processor.asset.AssetEdgeProcessor; import org.thingsboard.server.service.edge.rpc.processor.asset.profile.AssetProfileEdgeProcessor; +import org.thingsboard.server.service.edge.rpc.processor.calculated.CalculatedFieldProcessor; import org.thingsboard.server.service.edge.rpc.processor.dashboard.DashboardEdgeProcessor; import org.thingsboard.server.service.edge.rpc.processor.device.DeviceEdgeProcessor; import org.thingsboard.server.service.edge.rpc.processor.device.profile.DeviceProfileEdgeProcessor; @@ -248,6 +250,12 @@ public class EdgeContextComponent { @Autowired private GrpcCallbackExecutorService grpcCallbackExecutorService; + @Autowired + private CalculatedFieldService calculatedFieldService; + + @Autowired + private CalculatedFieldProcessor calculatedFieldProcessor; + public EdgeProcessor getProcessor(EdgeEventType edgeEventType) { EdgeProcessor processor = processorMap.get(edgeEventType); if (processor == null) { diff --git a/application/src/main/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtils.java b/application/src/main/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtils.java index 5f5fd771cf..192c56692d 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtils.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtils.java @@ -48,11 +48,13 @@ import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.alarm.AlarmComment; import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.asset.AssetProfile; +import org.thingsboard.server.common.data.cf.CalculatedField; import org.thingsboard.server.common.data.domain.DomainInfo; import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.edge.EdgeEventActionType; import org.thingsboard.server.common.data.id.AssetId; import org.thingsboard.server.common.data.id.AssetProfileId; +import org.thingsboard.server.common.data.id.CalculatedFieldId; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DashboardId; import org.thingsboard.server.common.data.id.DeviceId; @@ -89,6 +91,7 @@ import org.thingsboard.server.gen.edge.v1.AlarmUpdateMsg; import org.thingsboard.server.gen.edge.v1.AssetProfileUpdateMsg; import org.thingsboard.server.gen.edge.v1.AssetUpdateMsg; import org.thingsboard.server.gen.edge.v1.AttributeDeleteMsg; +import org.thingsboard.server.gen.edge.v1.CalculatedFieldUpdateMsg; import org.thingsboard.server.gen.edge.v1.CustomerUpdateMsg; import org.thingsboard.server.gen.edge.v1.DashboardUpdateMsg; import org.thingsboard.server.gen.edge.v1.DeviceCredentialsUpdateMsg; @@ -638,4 +641,17 @@ public class EdgeMsgConstructorUtils { .build(); } + public static CalculatedFieldUpdateMsg constructCalculatedFieldUpdatedMsg(UpdateMsgType msgType, CalculatedField calculatedField) { + return CalculatedFieldUpdateMsg.newBuilder().setMsgType(msgType).setEntity(JacksonUtil.toString(calculatedField)) + .setIdMSB(calculatedField.getId().getId().getMostSignificantBits()) + .setIdLSB(calculatedField.getId().getId().getLeastSignificantBits()).build(); + } + + public static CalculatedFieldUpdateMsg constructCalculatedFieldDeleteMsg(CalculatedFieldId calculatedFieldId) { + return CalculatedFieldUpdateMsg.newBuilder() + .setMsgType(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE) + .setIdMSB(calculatedFieldId.getId().getMostSignificantBits()) + .setIdLSB(calculatedFieldId.getId().getLeastSignificantBits()).build(); + } + } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java index 4a9b68fc6d..f3335d6d11 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java @@ -42,7 +42,6 @@ import org.thingsboard.server.common.data.limit.LimitedApi; import org.thingsboard.server.common.data.notification.rule.trigger.EdgeCommunicationFailureTrigger; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; -import org.thingsboard.server.common.data.page.SortOrder; import org.thingsboard.server.common.data.page.TimePageLink; import org.thingsboard.server.common.msg.edge.EdgeEventUpdateMsg; import org.thingsboard.server.gen.edge.v1.AlarmCommentUpdateMsg; @@ -50,6 +49,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.AttributesRequestMsg; +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; @@ -907,6 +907,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 [{}]", edge.getTenantId(), sessionId, uplinkMsg, e); diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeSyncCursor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeSyncCursor.java index 351c9b411b..389f0202fa 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeSyncCursor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeSyncCursor.java @@ -23,6 +23,7 @@ import org.thingsboard.server.service.edge.EdgeContextComponent; import org.thingsboard.server.service.edge.rpc.fetch.AdminSettingsEdgeEventFetcher; import org.thingsboard.server.service.edge.rpc.fetch.AssetProfilesEdgeEventFetcher; import org.thingsboard.server.service.edge.rpc.fetch.AssetsEdgeEventFetcher; +import org.thingsboard.server.service.edge.rpc.fetch.CalculatedFieldsEdgeEventFetcher; import org.thingsboard.server.service.edge.rpc.fetch.CustomerEdgeEventFetcher; import org.thingsboard.server.service.edge.rpc.fetch.CustomerUsersEdgeEventFetcher; import org.thingsboard.server.service.edge.rpc.fetch.DashboardsEdgeEventFetcher; @@ -80,6 +81,7 @@ public class EdgeSyncCursor { fetchers.add(new DevicesEdgeEventFetcher(ctx.getDeviceService())); fetchers.add(new AssetsEdgeEventFetcher(ctx.getAssetService())); fetchers.add(new EntityViewsEdgeEventFetcher(ctx.getEntityViewService())); + fetchers.add(new CalculatedFieldsEdgeEventFetcher(ctx.getCalculatedFieldService())); if (fullSync) { fetchers.add(new NotificationTemplateEdgeEventFetcher(ctx.getNotificationTemplateService())); fetchers.add(new NotificationTargetEdgeEventFetcher(ctx.getNotificationTargetService())); @@ -107,4 +109,5 @@ public class EdgeSyncCursor { currentIdx++; return edgeEventFetcher; } + } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/CalculatedFieldsEdgeEventFetcher.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/CalculatedFieldsEdgeEventFetcher.java new file mode 100644 index 0000000000..4f3e91354e --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/CalculatedFieldsEdgeEventFetcher.java @@ -0,0 +1,48 @@ +/** + * 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.fetch; + +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.thingsboard.server.common.data.EdgeUtils; +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.TenantId; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.dao.cf.CalculatedFieldService; + +@AllArgsConstructor +@Slf4j +public class CalculatedFieldsEdgeEventFetcher extends BasePageableEdgeEventFetcher { + + private final CalculatedFieldService calculatedFieldService; + + @Override + PageData fetchEntities(TenantId tenantId, Edge edge, PageLink pageLink) { + return calculatedFieldService.findCalculatedFieldsByTenantId(tenantId, pageLink); + } + + @Override + EdgeEvent constructEdgeEvent(TenantId tenantId, Edge edge, CalculatedField calculatedField) { + return EdgeUtils.constructEdgeEvent(tenantId, edge.getId(), EdgeEventType.CALCULATED_FIELD, + EdgeEventActionType.ADDED, calculatedField.getId(), null); + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/calculated/BaseCalculatedFieldProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/calculated/BaseCalculatedFieldProcessor.java new file mode 100644 index 0000000000..ddb1b23d53 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/calculated/BaseCalculatedFieldProcessor.java @@ -0,0 +1,79 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.edge.rpc.processor.calculated; + +import com.datastax.oss.driver.api.core.uuid.Uuids; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.util.Pair; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.StringUtils; +import org.thingsboard.server.common.data.cf.CalculatedField; +import org.thingsboard.server.common.data.id.CalculatedFieldId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.dao.service.DataValidator; +import org.thingsboard.server.gen.edge.v1.CalculatedFieldUpdateMsg; +import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor; + +@Slf4j +public abstract class BaseCalculatedFieldProcessor extends BaseEdgeProcessor { + + @Autowired + private DataValidator calculatedFieldValidator; + + protected Pair saveOrUpdateCalculatedField(TenantId tenantId, CalculatedFieldId calculatedFieldId, CalculatedFieldUpdateMsg calculatedFieldUpdateMsg) { + boolean isCreated = false; + boolean isNameUpdated = false; + try { + CalculatedField calculatedField = JacksonUtil.fromString(calculatedFieldUpdateMsg.getEntity(), CalculatedField.class, true); + if (calculatedField == null) { + throw new RuntimeException("[{" + tenantId + "}] calculatedFieldUpdateMsg {" + calculatedFieldUpdateMsg + " } cannot be converted to calculatedField"); + } + + CalculatedField calculatedFieldById = edgeCtx.getCalculatedFieldService().findById(tenantId, calculatedFieldId); + if (calculatedFieldById == null) { + calculatedField.setCreatedTime(Uuids.unixTimestamp(calculatedFieldId.getId())); + isCreated = true; + calculatedField.setId(null); + } else { + calculatedField.setId(calculatedFieldId); + } + + String calculatedFieldName = calculatedField.getName(); + CalculatedField calculatedFieldByName = edgeCtx.getCalculatedFieldService().findByTenantIdAndName(tenantId, calculatedFieldName); + if (calculatedFieldByName != null && !calculatedFieldByName.getId().equals(calculatedFieldId)) { + calculatedFieldName = calculatedFieldName + "_" + StringUtils.randomAlphabetic(15); + log.warn("[{}] calculatedField with name {} already exists. Renaming calculatedField name to {}", + tenantId, calculatedField.getName(), calculatedFieldByName.getName()); + isNameUpdated = true; + } + calculatedField.setName(calculatedFieldName); + + calculatedFieldValidator.validate(calculatedField, CalculatedField::getTenantId); + + if (isCreated) { + calculatedField.setId(calculatedFieldId); + } + + edgeCtx.getCalculatedFieldService().save(calculatedField, false); + } catch (Exception e) { + log.error("[{}] Failed to process calculatedField update msg [{}]", tenantId, calculatedFieldUpdateMsg, e); + throw e; + } + return Pair.of(isCreated, isNameUpdated); + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/calculated/CalculatedFieldEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/calculated/CalculatedFieldEdgeProcessor.java new file mode 100644 index 0000000000..0ddb62e874 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/calculated/CalculatedFieldEdgeProcessor.java @@ -0,0 +1,133 @@ +/** + * 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.calculated; + +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.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.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.queue.util.TbCoreComponent; +import org.thingsboard.server.service.edge.EdgeMsgConstructorUtils; + +import java.util.UUID; + +@Slf4j +@Component +@TbCoreComponent +public class CalculatedFieldEdgeProcessor extends BaseCalculatedFieldProcessor implements CalculatedFieldProcessor { + + @Override + public ListenableFuture processCalculatedFieldMsgFromEdge(TenantId tenantId, Edge edge, CalculatedFieldUpdateMsg calculatedFieldUpdateMsg) { + CalculatedFieldId calculatedFieldId = new CalculatedFieldId(new UUID(calculatedFieldUpdateMsg.getIdMSB(), calculatedFieldUpdateMsg.getIdLSB())); + try { + edgeSynchronizationManager.getEdgeId().set(edge.getId()); + + switch (calculatedFieldUpdateMsg.getMsgType()) { + case ENTITY_CREATED_RPC_MESSAGE: + case ENTITY_UPDATED_RPC_MESSAGE: + processCalculatedField(tenantId, calculatedFieldId, calculatedFieldUpdateMsg, edge); + return Futures.immediateFuture(null); + case ENTITY_DELETED_RPC_MESSAGE: + CalculatedField calculatedField = edgeCtx.getCalculatedFieldService().findById(tenantId, calculatedFieldId); + if (calculatedField != null) { + edgeCtx.getCalculatedFieldService().deleteCalculatedField(tenantId, calculatedFieldId); + } + return Futures.immediateFuture(null); + case UNRECOGNIZED: + default: + return handleUnsupportedMsgType(calculatedFieldUpdateMsg.getMsgType()); + } + } catch (DataValidationException e) { + log.warn("[{}] Failed to process CalculatedFieldUpdateMsg from Edge [{}]", tenantId, calculatedFieldUpdateMsg, e); + 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; + } + + + private void processCalculatedField(TenantId tenantId, CalculatedFieldId calculatedFieldId, CalculatedFieldUpdateMsg calculatedFieldUpdateMsg, Edge edge) { + Pair resultPair = super.saveOrUpdateCalculatedField(tenantId, calculatedFieldId, calculatedFieldUpdateMsg); + Boolean wasCreated = resultPair.getFirst(); + if (wasCreated) { + pushCalculatedFieldCreatedEventToRuleEngine(tenantId, edge, calculatedFieldId); + } + Boolean nameWasUpdated = resultPair.getSecond(); + if (nameWasUpdated) { + saveEdgeEvent(tenantId, edge.getId(), EdgeEventType.CALCULATED_FIELD, EdgeEventActionType.UPDATED, calculatedFieldId, null); + } + } + + private void pushCalculatedFieldCreatedEventToRuleEngine(TenantId tenantId, Edge edge, CalculatedFieldId calculatedFieldId) { + try { + CalculatedField calculatedField = edgeCtx.getCalculatedFieldService().findById(tenantId, calculatedFieldId); + String calculatedFieldAsString = JacksonUtil.toString(calculatedField); + TbMsgMetaData msgMetaData = getEdgeActionTbMsgMetaData(edge, edge.getCustomerId()); + pushEntityEventToRuleEngine(tenantId, calculatedFieldId, edge.getCustomerId(), TbMsgType.ENTITY_CREATED, calculatedFieldAsString, msgMetaData); + } catch (Exception e) { + log.warn("[{}][{}] Failed to push calculatedField action to rule engine: {}", tenantId, calculatedFieldId, TbMsgType.ENTITY_CREATED.name(), e); + } + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/calculated/CalculatedFieldProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/calculated/CalculatedFieldProcessor.java new file mode 100644 index 0000000000..ba9c8b27e1 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/calculated/CalculatedFieldProcessor.java @@ -0,0 +1,28 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.edge.rpc.processor.calculated; + +import com.google.common.util.concurrent.ListenableFuture; +import org.thingsboard.server.common.data.edge.Edge; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.gen.edge.v1.CalculatedFieldUpdateMsg; +import org.thingsboard.server.service.edge.rpc.processor.EdgeProcessor; + +public interface CalculatedFieldProcessor extends EdgeProcessor { + + ListenableFuture processCalculatedFieldMsgFromEdge(TenantId tenantId, Edge edge, CalculatedFieldUpdateMsg calculatedFieldUpdateMsg); + +} diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/cf/CalculatedFieldService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/cf/CalculatedFieldService.java index 5101d6d57e..a645903896 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/cf/CalculatedFieldService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/cf/CalculatedFieldService.java @@ -31,8 +31,12 @@ public interface CalculatedFieldService extends EntityDaoService { CalculatedField save(CalculatedField calculatedField); + CalculatedField save(CalculatedField calculatedField, boolean doValidate); + CalculatedField findById(TenantId tenantId, CalculatedFieldId calculatedFieldId); + CalculatedField findByTenantIdAndName(TenantId tenantId, String name); + List findCalculatedFieldIdsByEntityId(TenantId tenantId, EntityId entityId); List findCalculatedFieldsByEntityId(TenantId tenantId, EntityId entityId); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/edge/EdgeEventType.java b/common/data/src/main/java/org/thingsboard/server/common/data/edge/EdgeEventType.java index 6f1ae8150f..9c97935329 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/edge/EdgeEventType.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/edge/EdgeEventType.java @@ -41,12 +41,13 @@ public enum EdgeEventType { ADMIN_SETTINGS(true, null), OTA_PACKAGE(true, EntityType.OTA_PACKAGE), QUEUE(true, EntityType.QUEUE), - NOTIFICATION_RULE (true, EntityType.NOTIFICATION_RULE), - NOTIFICATION_TARGET (true, EntityType.NOTIFICATION_TARGET), - NOTIFICATION_TEMPLATE (true, EntityType.NOTIFICATION_TEMPLATE), + NOTIFICATION_RULE(true, EntityType.NOTIFICATION_RULE), + NOTIFICATION_TARGET(true, EntityType.NOTIFICATION_TARGET), + NOTIFICATION_TEMPLATE(true, EntityType.NOTIFICATION_TEMPLATE), TB_RESOURCE(true, EntityType.TB_RESOURCE), OAUTH2_CLIENT(true, EntityType.OAUTH2_CLIENT), - DOMAIN(true, EntityType.DOMAIN); + DOMAIN(true, EntityType.DOMAIN), + CALCULATED_FIELD(true, EntityType.CALCULATED_FIELD); private final boolean allEdgesRelated; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java index f5dd4b12a0..01727b0ebd 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java @@ -169,6 +169,8 @@ public class EntityIdFactory { return new OAuth2ClientId(uuid); case DOMAIN: return new DomainId(uuid); + case CALCULATED_FIELD: + return new CalculatedFieldId(uuid); } throw new IllegalArgumentException("EdgeEventType " + edgeEventType + " is not supported!"); } diff --git a/common/edge-api/src/main/proto/edge.proto b/common/edge-api/src/main/proto/edge.proto index 023ac00634..904a938f1e 100644 --- a/common/edge-api/src/main/proto/edge.proto +++ b/common/edge-api/src/main/proto/edge.proto @@ -124,6 +124,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; @@ -423,6 +431,7 @@ message UplinkMsg { repeated AlarmCommentUpdateMsg alarmCommentUpdateMsg = 22; repeated RuleChainUpdateMsg ruleChainUpdateMsg = 23; repeated RuleChainMetadataUpdateMsg ruleChainMetadataUpdateMsg = 24; + repeated CalculatedFieldUpdateMsg calculatedFieldUpdateMsg = 25; } message UplinkResponseMsg { @@ -472,4 +481,5 @@ message DownlinkMsg { repeated NotificationTargetUpdateMsg notificationTargetUpdateMsg = 32; repeated NotificationTemplateUpdateMsg notificationTemplateUpdateMsg = 33; repeated OAuth2DomainUpdateMsg oAuth2DomainUpdateMsg = 34; + repeated CalculatedFieldUpdateMsg calculatedFieldUpdateMsg = 35; } diff --git a/dao/src/main/java/org/thingsboard/server/dao/cf/BaseCalculatedFieldService.java b/dao/src/main/java/org/thingsboard/server/dao/cf/BaseCalculatedFieldService.java index 0c5df18e80..141adf49aa 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/cf/BaseCalculatedFieldService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/cf/BaseCalculatedFieldService.java @@ -57,7 +57,18 @@ public class BaseCalculatedFieldService extends AbstractEntityService implements @Override public CalculatedField save(CalculatedField calculatedField) { - CalculatedField oldCalculatedField = calculatedFieldDataValidator.validate(calculatedField, CalculatedField::getTenantId); + return doSave(calculatedField, true); + } + + @Override + public CalculatedField save(CalculatedField calculatedField, boolean doValidate) { + return doSave(calculatedField, doValidate); + } + + private CalculatedField doSave(CalculatedField calculatedField, boolean doValidate) { + if (doValidate) { + calculatedFieldDataValidator.validate(calculatedField, CalculatedField::getTenantId); + } try { TenantId tenantId = calculatedField.getTenantId(); log.trace("Executing save calculated field, [{}]", calculatedField); @@ -65,7 +76,7 @@ public class BaseCalculatedFieldService extends AbstractEntityService implements CalculatedField savedCalculatedField = calculatedFieldDao.save(tenantId, calculatedField); createOrUpdateCalculatedFieldLink(tenantId, savedCalculatedField); eventPublisher.publishEvent(SaveEntityEvent.builder().tenantId(savedCalculatedField.getTenantId()).entityId(savedCalculatedField.getId()) - .entity(savedCalculatedField).oldEntity(oldCalculatedField).created(calculatedField.getId() == null).build()); + .entity(savedCalculatedField).oldEntity(calculatedField).created(calculatedField.getId() == null).build()); return savedCalculatedField; } catch (Exception e) { checkConstraintViolation(e, @@ -83,6 +94,14 @@ public class BaseCalculatedFieldService extends AbstractEntityService implements return calculatedFieldDao.findById(tenantId, calculatedFieldId.getId()); } + @Override + public CalculatedField findByTenantIdAndName(TenantId tenantId, String name) { + log.trace("Executing findByTenantIdAndName [{}], calculatedFieldName[{}]", tenantId, name); + validateId(tenantId, id -> INCORRECT_TENANT_ID + id); + + return calculatedFieldDao.findByTenantIdAndName(tenantId, name).orElse(null); + } + @Override public List findCalculatedFieldIdsByEntityId(TenantId tenantId, EntityId entityId) { log.trace("Executing findCalculatedFieldIdsByEntityId [{}]", entityId); diff --git a/dao/src/main/java/org/thingsboard/server/dao/cf/CalculatedFieldDao.java b/dao/src/main/java/org/thingsboard/server/dao/cf/CalculatedFieldDao.java index aadae93893..44dbb26b81 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/cf/CalculatedFieldDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/cf/CalculatedFieldDao.java @@ -24,6 +24,7 @@ import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.dao.Dao; import java.util.List; +import java.util.Optional; public interface CalculatedFieldDao extends Dao { @@ -35,6 +36,8 @@ public interface CalculatedFieldDao extends Dao { List findAll(); + Optional findByTenantIdAndName(TenantId tenantId, String name); + PageData findAll(PageLink pageLink); PageData findAllByTenantId(TenantId tenantId, PageLink pageLink); diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/cf/CalculatedFieldRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/cf/CalculatedFieldRepository.java index be122816ba..91721c74f3 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/cf/CalculatedFieldRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/cf/CalculatedFieldRepository.java @@ -28,6 +28,8 @@ public interface CalculatedFieldRepository extends JpaRepository findCalculatedFieldIdsByTenantIdAndEntityId(UUID tenantId, UUID entityId); List findAllByTenantIdAndEntityId(UUID tenantId, UUID entityId); diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/cf/JpaCalculatedFieldDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/cf/JpaCalculatedFieldDao.java index 4bb52c29db..f18fe10f8c 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/cf/JpaCalculatedFieldDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/cf/JpaCalculatedFieldDao.java @@ -34,6 +34,7 @@ import org.thingsboard.server.dao.sql.JpaAbstractDao; import org.thingsboard.server.dao.util.SqlDao; import java.util.List; +import java.util.Optional; import java.util.UUID; @Slf4j @@ -65,6 +66,12 @@ public class JpaCalculatedFieldDao extends JpaAbstractDao findByTenantIdAndName(TenantId tenantId, String name) { + CalculatedField calculatedField = DaoUtil.getData(calculatedFieldRepository.findByTenantIdAndName(tenantId.getId(), name)); + return Optional.ofNullable(calculatedField); + } + @Override public PageData findAll(PageLink pageLink) { log.debug("Try to find calculated fields by pageLink [{}]", pageLink); From d45cbcfcbdcf7271300e8fbb5ed8b6a6d3169a3f Mon Sep 17 00:00:00 2001 From: Yevhenii Date: Mon, 2 Jun 2025 14:25:25 +0300 Subject: [PATCH 08/54] CalculatedField functionality support for Edge - fixed issue when entity not assigned to Edge --- .../CalculatedFieldEdgeProcessor.java | 26 ++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/calculated/CalculatedFieldEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/calculated/CalculatedFieldEdgeProcessor.java index 0ddb62e874..f936b0b1d4 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/calculated/CalculatedFieldEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/calculated/CalculatedFieldEdgeProcessor.java @@ -30,6 +30,8 @@ import org.thingsboard.server.common.data.edge.EdgeEventType; import org.thingsboard.server.common.data.id.CalculatedFieldId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.msg.TbMsgType; +import org.thingsboard.server.common.data.relation.EntityRelation; +import org.thingsboard.server.common.data.relation.RelationTypeGroup; import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.gen.edge.v1.CalculatedFieldUpdateMsg; @@ -39,6 +41,7 @@ import org.thingsboard.server.gen.edge.v1.UpdateMsgType; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.edge.EdgeMsgConstructorUtils; +import java.util.List; import java.util.UUID; @Slf4j @@ -68,8 +71,12 @@ public class CalculatedFieldEdgeProcessor extends BaseCalculatedFieldProcessor i return handleUnsupportedMsgType(calculatedFieldUpdateMsg.getMsgType()); } } catch (DataValidationException e) { - log.warn("[{}] Failed to process CalculatedFieldUpdateMsg from Edge [{}]", tenantId, calculatedFieldUpdateMsg, e); - return Futures.immediateFailedFuture(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(); } @@ -81,7 +88,7 @@ public class CalculatedFieldEdgeProcessor extends BaseCalculatedFieldProcessor i switch (edgeEvent.getAction()) { case ADDED, UPDATED -> { CalculatedField calculatedField = edgeCtx.getCalculatedFieldService().findById(edgeEvent.getTenantId(), calculatedFieldId); - if (calculatedField != null) { + if (calculatedField != null && isEntityAssignedToEdge(edgeEvent, calculatedField)) { UpdateMsgType msgType = getUpdateMsgType(edgeEvent.getAction()); CalculatedFieldUpdateMsg calculatedFieldUpdateMsg = EdgeMsgConstructorUtils.constructCalculatedFieldUpdatedMsg(msgType, calculatedField); return DownlinkMsg.newBuilder() @@ -101,6 +108,19 @@ public class CalculatedFieldEdgeProcessor extends BaseCalculatedFieldProcessor i return null; } + private boolean isEntityAssignedToEdge(EdgeEvent edgeEvent, CalculatedField calculatedField) { + switch (calculatedField.getEntityId().getEntityType()) { + case ASSET, DEVICE -> { + List relations = + edgeCtx.getRelationService().findByTo(edgeEvent.getTenantId(), calculatedField.getEntityId(), RelationTypeGroup.EDGE); + return !relations.isEmpty(); + } + default -> { + return true; + } + } + } + @Override public EdgeEventType getEdgeEventType() { return EdgeEventType.CALCULATED_FIELD; From 93948bf64bd57306d7715c8337e854e4205d0417 Mon Sep 17 00:00:00 2001 From: Yevhenii Date: Mon, 2 Jun 2025 18:34:32 +0300 Subject: [PATCH 09/54] CalculatedField functionality support for Edge - added test --- .../server/edge/AbstractEdgeTest.java | 2 +- .../server/edge/CalculatedFieldEdgeTest.java | 228 ++++++++++++++++++ .../server/edge/imitator/EdgeImitator.java | 6 + .../dao/cf/BaseCalculatedFieldService.java | 17 +- 4 files changed, 246 insertions(+), 7 deletions(-) create mode 100644 application/src/test/java/org/thingsboard/server/edge/CalculatedFieldEdgeTest.java diff --git a/application/src/test/java/org/thingsboard/server/edge/AbstractEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/AbstractEdgeTest.java index c02e72a35f..24fa02f11a 100644 --- a/application/src/test/java/org/thingsboard/server/edge/AbstractEdgeTest.java +++ b/application/src/test/java/org/thingsboard/server/edge/AbstractEdgeTest.java @@ -565,7 +565,7 @@ abstract public class AbstractEdgeTest extends AbstractControllerTest { protected Device saveDeviceOnCloudAndVerifyDeliveryToEdge() throws Exception { // create device and assign to edge Device savedDevice = saveDevice(StringUtils.randomAlphanumeric(15), thermostatDeviceProfile.getName()); - edgeImitator.expectMessageAmount(2); // device and device profile messages + edgeImitator.expectMessageAmount(3); // device and device profile messages and device credentials doPost("/api/edge/" + edge.getUuidId() + "/device/" + savedDevice.getUuidId(), Device.class); Assert.assertTrue(edgeImitator.waitForMessages()); diff --git a/application/src/test/java/org/thingsboard/server/edge/CalculatedFieldEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/CalculatedFieldEdgeTest.java new file mode 100644 index 0000000000..0f785c4063 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/edge/CalculatedFieldEdgeTest.java @@ -0,0 +1,228 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.edge; + +import com.datastax.oss.driver.api.core.uuid.Uuids; +import com.google.protobuf.AbstractMessage; +import com.google.protobuf.InvalidProtocolBufferException; +import org.junit.Assert; +import org.junit.Test; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.cf.CalculatedField; +import org.thingsboard.server.common.data.cf.CalculatedFieldType; +import org.thingsboard.server.common.data.cf.configuration.Argument; +import org.thingsboard.server.common.data.cf.configuration.ArgumentType; +import org.thingsboard.server.common.data.cf.configuration.Output; +import org.thingsboard.server.common.data.cf.configuration.OutputType; +import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey; +import org.thingsboard.server.common.data.cf.configuration.SimpleCalculatedFieldConfiguration; +import org.thingsboard.server.common.data.debug.DebugSettings; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.dao.service.DaoSqlTest; +import org.thingsboard.server.gen.edge.v1.CalculatedFieldUpdateMsg; +import org.thingsboard.server.gen.edge.v1.UpdateMsgType; +import org.thingsboard.server.gen.edge.v1.UplinkMsg; +import org.thingsboard.server.gen.edge.v1.UplinkResponseMsg; + +import java.util.Map; +import java.util.Optional; +import java.util.UUID; + +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@DaoSqlTest +public class CalculatedFieldEdgeTest extends AbstractEdgeTest { + private static final String DEFAULT_CF_NAME = "Edge Test CalculatedField"; + private static final String UPDATED_CF_NAME = "Updated Edge Test CalculatedField"; + + @Test + public void testCalculatedField_create_update_delete() throws Exception { + Device savedDevice = saveDeviceOnCloudAndVerifyDeliveryToEdge(); + + // create calculatedField + SimpleCalculatedFieldConfiguration config = new SimpleCalculatedFieldConfiguration(); + CalculatedField calculatedField = createSimpleCalculatedField(savedDevice.getId(), config); + + edgeImitator.expectMessageAmount(1); + CalculatedField savedCalculatedField = doPost("/api/calculatedField", calculatedField, CalculatedField.class); + Assert.assertTrue(edgeImitator.waitForMessages()); + + AbstractMessage latestMessage = edgeImitator.getLatestMessage(); + Assert.assertTrue(latestMessage instanceof CalculatedFieldUpdateMsg); + CalculatedFieldUpdateMsg calculatedFieldUpdateMsg = (CalculatedFieldUpdateMsg) latestMessage; + Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, calculatedFieldUpdateMsg.getMsgType()); + Assert.assertEquals(savedCalculatedField.getUuidId().getMostSignificantBits(), calculatedFieldUpdateMsg.getIdMSB()); + Assert.assertEquals(savedCalculatedField.getUuidId().getLeastSignificantBits(), calculatedFieldUpdateMsg.getIdLSB()); + CalculatedField calculatedFieldFromMsg = JacksonUtil.fromString(calculatedFieldUpdateMsg.getEntity(), CalculatedField.class, true); + Assert.assertNotNull(calculatedFieldFromMsg); + + Assert.assertEquals(DEFAULT_CF_NAME, calculatedFieldFromMsg.getName()); + Assert.assertEquals(savedDevice.getId(), calculatedFieldFromMsg.getEntityId()); + Assert.assertEquals(config, calculatedFieldFromMsg.getConfiguration()); + + // update calculatedField + edgeImitator.expectMessageAmount(1); + savedCalculatedField.setName(UPDATED_CF_NAME); + savedCalculatedField = doPost("/api/calculatedField", savedCalculatedField, CalculatedField.class); + Assert.assertTrue(edgeImitator.waitForMessages()); + latestMessage = edgeImitator.getLatestMessage(); + Assert.assertTrue(latestMessage instanceof CalculatedFieldUpdateMsg); + calculatedFieldUpdateMsg = (CalculatedFieldUpdateMsg) latestMessage; + calculatedFieldFromMsg = JacksonUtil.fromString(calculatedFieldUpdateMsg.getEntity(), CalculatedField.class, true); + Assert.assertNotNull(calculatedFieldFromMsg); + Assert.assertEquals(UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE, calculatedFieldUpdateMsg.getMsgType()); + Assert.assertEquals(UPDATED_CF_NAME, calculatedFieldFromMsg.getName()); + + // delete calculatedField + edgeImitator.expectMessageAmount(1); + doDelete("/api/calculatedField/" + savedCalculatedField.getUuidId()) + .andExpect(status().isOk()); + Assert.assertTrue(edgeImitator.waitForMessages()); + latestMessage = edgeImitator.getLatestMessage(); + Assert.assertTrue(latestMessage instanceof CalculatedFieldUpdateMsg); + calculatedFieldUpdateMsg = (CalculatedFieldUpdateMsg) latestMessage; + Assert.assertEquals(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE, calculatedFieldUpdateMsg.getMsgType()); + Assert.assertEquals(savedCalculatedField.getUuidId().getMostSignificantBits(), calculatedFieldUpdateMsg.getIdMSB()); + Assert.assertEquals(savedCalculatedField.getUuidId().getLeastSignificantBits(), calculatedFieldUpdateMsg.getIdLSB()); + } + + @Test + public void testSendCalculatedFieldToCloud() throws Exception { + Device savedDevice = saveDeviceOnCloudAndVerifyDeliveryToEdge(); + + // create calculatedField + SimpleCalculatedFieldConfiguration config = new SimpleCalculatedFieldConfiguration(); + CalculatedField calculatedField = createSimpleCalculatedField(savedDevice.getId(), config); + UUID uuid = Uuids.timeBased(); + UplinkMsg uplinkMsg = getUplinkMsg(uuid, calculatedField, UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE); + + checkCalculatedFieldOnCloud(uplinkMsg, uuid, calculatedField.getName()); + } + + @Test + public void testUpdateCalculatedFieldNameOnCloud() throws Exception { + Device savedDevice = saveDeviceOnCloudAndVerifyDeliveryToEdge(); + + // create calculatedField + SimpleCalculatedFieldConfiguration config = new SimpleCalculatedFieldConfiguration(); + CalculatedField calculatedField = createSimpleCalculatedField(savedDevice.getId(), config); + UUID uuid = Uuids.timeBased(); + UplinkMsg uplinkMsg = getUplinkMsg(uuid, calculatedField, UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE); + + checkCalculatedFieldOnCloud(uplinkMsg, uuid, calculatedField.getName()); + + calculatedField.setName(UPDATED_CF_NAME); + UplinkMsg updatedUplinkMsg = getUplinkMsg(uuid, calculatedField, UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE); + + checkCalculatedFieldOnCloud(updatedUplinkMsg, uuid, calculatedField.getName()); + } + + @Test + public void testCalculatedFieldToCloudWithNameThatAlreadyExistsOnCloud() throws Exception { + Device savedDevice = saveDeviceOnCloudAndVerifyDeliveryToEdge(); + + // create calculatedField + SimpleCalculatedFieldConfiguration config = new SimpleCalculatedFieldConfiguration(); + CalculatedField calculatedField = createSimpleCalculatedField(savedDevice.getId(), config); + + edgeImitator.expectMessageAmount(1); + CalculatedField savedCalculatedField = doPost("/api/calculatedField", calculatedField, CalculatedField.class); + Assert.assertTrue(edgeImitator.waitForMessages()); + + UUID uuid = Uuids.timeBased(); + + UplinkMsg uplinkMsg = getUplinkMsg(uuid, calculatedField, UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE); + + edgeImitator.expectResponsesAmount(1); + edgeImitator.expectMessageAmount(1); + + edgeImitator.sendUplinkMsg(uplinkMsg); + + Assert.assertTrue(edgeImitator.waitForResponses()); + Assert.assertTrue(edgeImitator.waitForMessages()); + + Optional calculatedFieldUpdateMsgOpt = edgeImitator.findMessageByType(CalculatedFieldUpdateMsg.class); + Assert.assertTrue(calculatedFieldUpdateMsgOpt.isPresent()); + CalculatedFieldUpdateMsg latestCalculatedFieldUpdateMsg = calculatedFieldUpdateMsgOpt.get(); + CalculatedField calculatedFieldFromMsg = JacksonUtil.fromString(latestCalculatedFieldUpdateMsg.getEntity(), CalculatedField.class, true); + Assert.assertNotNull(calculatedFieldFromMsg); + Assert.assertNotEquals(DEFAULT_CF_NAME, calculatedFieldFromMsg.getName()); + + Assert.assertNotEquals(savedCalculatedField.getUuidId(), uuid); + + CalculatedField calculatedFieldFromCloud = doGet("/api/calculatedField/" + uuid, CalculatedField.class); + Assert.assertNotNull(calculatedFieldFromCloud); + Assert.assertNotEquals(DEFAULT_CF_NAME, calculatedFieldFromCloud.getName()); + } + + private CalculatedField createSimpleCalculatedField(EntityId entityId, SimpleCalculatedFieldConfiguration config) { + CalculatedField calculatedField = new CalculatedField(); + calculatedField.setEntityId(entityId); + calculatedField.setTenantId(tenantId); + calculatedField.setType(CalculatedFieldType.SIMPLE); + calculatedField.setName(DEFAULT_CF_NAME); + calculatedField.setDebugSettings(DebugSettings.all()); + + Argument argument = new Argument(); + ReferencedEntityKey refEntityKey = new ReferencedEntityKey("temperature", ArgumentType.TS_LATEST, null); + argument.setRefEntityKey(refEntityKey); + argument.setDefaultValue("12"); // not used because real telemetry value in db is present + config.setArguments(Map.of("T", argument)); + + config.setExpression("(T * 9/5) + 32"); + + Output output = new Output(); + output.setName("fahrenheitTemp"); + output.setType(OutputType.TIME_SERIES); + output.setDecimalsByDefault(2); + config.setOutput(output); + + calculatedField.setConfiguration(config); + + return calculatedField; + } + + private UplinkMsg getUplinkMsg(UUID uuid, CalculatedField calculatedField, UpdateMsgType updateMsgType) throws InvalidProtocolBufferException { + UplinkMsg.Builder uplinkMsgBuilder = UplinkMsg.newBuilder(); + CalculatedFieldUpdateMsg.Builder calculatedFieldUpdateMsgBuilder = CalculatedFieldUpdateMsg.newBuilder(); + calculatedFieldUpdateMsgBuilder.setIdMSB(uuid.getMostSignificantBits()); + calculatedFieldUpdateMsgBuilder.setIdLSB(uuid.getLeastSignificantBits()); + calculatedFieldUpdateMsgBuilder.setEntity(JacksonUtil.toString(calculatedField)); + calculatedFieldUpdateMsgBuilder.setMsgType(updateMsgType); + testAutoGeneratedCodeByProtobuf(calculatedFieldUpdateMsgBuilder); + uplinkMsgBuilder.addCalculatedFieldUpdateMsg(calculatedFieldUpdateMsgBuilder.build()); + + testAutoGeneratedCodeByProtobuf(uplinkMsgBuilder); + + return uplinkMsgBuilder.build(); + } + + private void checkCalculatedFieldOnCloud(UplinkMsg uplinkMsg, UUID uuid, String resourceTitle) throws Exception { + edgeImitator.expectResponsesAmount(1); + edgeImitator.sendUplinkMsg(uplinkMsg); + + Assert.assertTrue(edgeImitator.waitForResponses()); + + UplinkResponseMsg latestResponseMsg = edgeImitator.getLatestResponseMsg(); + Assert.assertTrue(latestResponseMsg.getSuccess()); + + CalculatedField calculatedField = doGet("/api/calculatedField/" + uuid, CalculatedField.class); + Assert.assertNotNull(calculatedField); + Assert.assertEquals(resourceTitle, calculatedField.getName()); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/edge/imitator/EdgeImitator.java b/application/src/test/java/org/thingsboard/server/edge/imitator/EdgeImitator.java index 8b259cf8fc..16d10d9b6e 100644 --- a/application/src/test/java/org/thingsboard/server/edge/imitator/EdgeImitator.java +++ b/application/src/test/java/org/thingsboard/server/edge/imitator/EdgeImitator.java @@ -33,6 +33,7 @@ import org.thingsboard.server.gen.edge.v1.AlarmCommentUpdateMsg; import org.thingsboard.server.gen.edge.v1.AlarmUpdateMsg; import org.thingsboard.server.gen.edge.v1.AssetProfileUpdateMsg; import org.thingsboard.server.gen.edge.v1.AssetUpdateMsg; +import org.thingsboard.server.gen.edge.v1.CalculatedFieldUpdateMsg; import org.thingsboard.server.gen.edge.v1.CustomerUpdateMsg; import org.thingsboard.server.gen.edge.v1.DashboardUpdateMsg; import org.thingsboard.server.gen.edge.v1.DeviceCredentialsRequestMsg; @@ -352,6 +353,11 @@ public class EdgeImitator { result.add(saveDownlinkMsg(notificationTargetUpdateMsg)); } } + if (downlinkMsg.getCalculatedFieldUpdateMsgCount() > 0) { + for (CalculatedFieldUpdateMsg calculatedFieldUpdateMsg : downlinkMsg.getCalculatedFieldUpdateMsgList()) { + result.add(saveDownlinkMsg(calculatedFieldUpdateMsg)); + } + } if (downlinkMsg.hasEdgeConfiguration()) { result.add(saveDownlinkMsg(downlinkMsg.getEdgeConfiguration())); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/cf/BaseCalculatedFieldService.java b/dao/src/main/java/org/thingsboard/server/dao/cf/BaseCalculatedFieldService.java index 141adf49aa..5d4d3c54da 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/cf/BaseCalculatedFieldService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/cf/BaseCalculatedFieldService.java @@ -57,18 +57,23 @@ public class BaseCalculatedFieldService extends AbstractEntityService implements @Override public CalculatedField save(CalculatedField calculatedField) { - return doSave(calculatedField, true); + CalculatedField oldCalculatedField = calculatedFieldDataValidator.validate(calculatedField, CalculatedField::getTenantId); + + return doSave(calculatedField, oldCalculatedField); } @Override public CalculatedField save(CalculatedField calculatedField, boolean doValidate) { - return doSave(calculatedField, doValidate); - } + CalculatedField oldCalculatedField = null; - private CalculatedField doSave(CalculatedField calculatedField, boolean doValidate) { if (doValidate) { - calculatedFieldDataValidator.validate(calculatedField, CalculatedField::getTenantId); + oldCalculatedField = calculatedFieldDataValidator.validate(calculatedField, CalculatedField::getTenantId); } + + return doSave(calculatedField, oldCalculatedField); + } + + private CalculatedField doSave(CalculatedField calculatedField, CalculatedField oldCalculatedField) { try { TenantId tenantId = calculatedField.getTenantId(); log.trace("Executing save calculated field, [{}]", calculatedField); @@ -76,7 +81,7 @@ public class BaseCalculatedFieldService extends AbstractEntityService implements CalculatedField savedCalculatedField = calculatedFieldDao.save(tenantId, calculatedField); createOrUpdateCalculatedFieldLink(tenantId, savedCalculatedField); eventPublisher.publishEvent(SaveEntityEvent.builder().tenantId(savedCalculatedField.getTenantId()).entityId(savedCalculatedField.getId()) - .entity(savedCalculatedField).oldEntity(calculatedField).created(calculatedField.getId() == null).build()); + .entity(savedCalculatedField).oldEntity(oldCalculatedField).created(calculatedField.getId() == null).build()); return savedCalculatedField; } catch (Exception e) { checkConstraintViolation(e, From 2761866e5838e3cd28ed8c5de5c32453086e3779 Mon Sep 17 00:00:00 2001 From: yevhenii Date: Thu, 5 Jun 2025 18:06:51 +0300 Subject: [PATCH 10/54] CalculatedField functionality support for Edge - Modified downlink push logic --- .../edge/EdgeEventSourcingListener.java | 3 ++ .../service/edge/rpc/EdgeSyncCursor.java | 2 - .../CalculatedFieldsEdgeEventFetcher.java | 48 ------------------ .../edge/rpc/processor/BaseEdgeProcessor.java | 11 ++++ .../processor/asset/AssetEdgeProcessor.java | 3 ++ .../BaseCalculatedFieldProcessor.java | 31 ++++++++++++ .../CalculatedFieldEdgeProcessor.java | 50 ++++++++++++------- .../processor/device/DeviceEdgeProcessor.java | 3 ++ .../common/data/edge/EdgeEventType.java | 2 +- 9 files changed, 85 insertions(+), 68 deletions(-) delete mode 100644 application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/CalculatedFieldsEdgeEventFetcher.java diff --git a/application/src/main/java/org/thingsboard/server/service/edge/EdgeEventSourcingListener.java b/application/src/main/java/org/thingsboard/server/service/edge/EdgeEventSourcingListener.java index 7bafd53644..70c63a96cb 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/EdgeEventSourcingListener.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/EdgeEventSourcingListener.java @@ -33,6 +33,7 @@ import org.thingsboard.server.common.data.alarm.AlarmApiCallResult; import org.thingsboard.server.common.data.alarm.AlarmComment; import org.thingsboard.server.common.data.alarm.EntityAlarm; import org.thingsboard.server.common.data.audit.ActionType; +import org.thingsboard.server.common.data.cf.CalculatedField; import org.thingsboard.server.common.data.domain.Domain; import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.edge.EdgeEventActionType; @@ -262,6 +263,8 @@ public class EdgeEventSourcingListener { private String getBodyMsgForEntityEvent(Object entity) { if (entity instanceof AlarmComment) { return JacksonUtil.toString(entity); + } else if (entity instanceof CalculatedField calculatedField) { + return JacksonUtil.toString(calculatedField.getEntityId()); } return null; } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeSyncCursor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeSyncCursor.java index 389f0202fa..adab9b812f 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeSyncCursor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeSyncCursor.java @@ -23,7 +23,6 @@ import org.thingsboard.server.service.edge.EdgeContextComponent; import org.thingsboard.server.service.edge.rpc.fetch.AdminSettingsEdgeEventFetcher; import org.thingsboard.server.service.edge.rpc.fetch.AssetProfilesEdgeEventFetcher; import org.thingsboard.server.service.edge.rpc.fetch.AssetsEdgeEventFetcher; -import org.thingsboard.server.service.edge.rpc.fetch.CalculatedFieldsEdgeEventFetcher; import org.thingsboard.server.service.edge.rpc.fetch.CustomerEdgeEventFetcher; import org.thingsboard.server.service.edge.rpc.fetch.CustomerUsersEdgeEventFetcher; import org.thingsboard.server.service.edge.rpc.fetch.DashboardsEdgeEventFetcher; @@ -81,7 +80,6 @@ public class EdgeSyncCursor { fetchers.add(new DevicesEdgeEventFetcher(ctx.getDeviceService())); fetchers.add(new AssetsEdgeEventFetcher(ctx.getAssetService())); fetchers.add(new EntityViewsEdgeEventFetcher(ctx.getEntityViewService())); - fetchers.add(new CalculatedFieldsEdgeEventFetcher(ctx.getCalculatedFieldService())); if (fullSync) { fetchers.add(new NotificationTemplateEdgeEventFetcher(ctx.getNotificationTemplateService())); fetchers.add(new NotificationTargetEdgeEventFetcher(ctx.getNotificationTargetService())); diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/CalculatedFieldsEdgeEventFetcher.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/CalculatedFieldsEdgeEventFetcher.java deleted file mode 100644 index 4f3e91354e..0000000000 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/CalculatedFieldsEdgeEventFetcher.java +++ /dev/null @@ -1,48 +0,0 @@ -/** - * 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.fetch; - -import lombok.AllArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.thingsboard.server.common.data.EdgeUtils; -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.TenantId; -import org.thingsboard.server.common.data.page.PageData; -import org.thingsboard.server.common.data.page.PageLink; -import org.thingsboard.server.dao.cf.CalculatedFieldService; - -@AllArgsConstructor -@Slf4j -public class CalculatedFieldsEdgeEventFetcher extends BasePageableEdgeEventFetcher { - - private final CalculatedFieldService calculatedFieldService; - - @Override - PageData fetchEntities(TenantId tenantId, Edge edge, PageLink pageLink) { - return calculatedFieldService.findCalculatedFieldsByTenantId(tenantId, pageLink); - } - - @Override - EdgeEvent constructEdgeEvent(TenantId tenantId, Edge edge, CalculatedField calculatedField) { - return EdgeUtils.constructEdgeEvent(tenantId, edge.getId(), EdgeEventType.CALCULATED_FIELD, - EdgeEventActionType.ADDED, calculatedField.getId(), null); - } - -} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessor.java index 6fcb02e4bc..077ff21d26 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessor.java @@ -25,6 +25,7 @@ import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.AttributeScope; 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; @@ -53,11 +54,13 @@ import org.thingsboard.server.common.msg.TbMsgDataType; import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.dao.edge.EdgeSynchronizationManager; import org.thingsboard.server.dao.entity.EntityDaoRegistry; +import org.thingsboard.server.gen.edge.v1.CalculatedFieldUpdateMsg; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.queue.TbQueueCallback; import org.thingsboard.server.queue.TbQueueMsgMetadata; import org.thingsboard.server.service.edge.EdgeContextComponent; +import org.thingsboard.server.service.edge.EdgeMsgConstructorUtils; import org.thingsboard.server.service.executors.DbCallbackExecutorService; import org.thingsboard.server.service.state.DefaultDeviceStateService; @@ -381,4 +384,12 @@ public abstract class BaseEdgeProcessor implements EdgeProcessor { }); } + protected List getCalculatedFieldUpdateMsgs(TenantId tenantId, EntityId entityId) { + List calculatedFields = edgeCtx.getCalculatedFieldService().findCalculatedFieldsByEntityId(tenantId, entityId); + + return calculatedFields.stream() + .map(calculatedField -> EdgeMsgConstructorUtils.constructCalculatedFieldUpdatedMsg(UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE, calculatedField)) + .toList(); + } + } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/AssetEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/AssetEdgeProcessor.java index 47f4c11362..538a90f7d7 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/AssetEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/AssetEdgeProcessor.java @@ -119,6 +119,9 @@ public class AssetEdgeProcessor extends BaseAssetProcessor implements AssetProce DownlinkMsg.Builder builder = DownlinkMsg.newBuilder() .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) .addAssetUpdateMsg(assetUpdateMsg); + + getCalculatedFieldUpdateMsgs(edgeEvent.getTenantId(), assetId).forEach(builder::addCalculatedFieldUpdateMsg); + if (UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE.equals(msgType)) { AssetProfile assetProfile = edgeCtx.getAssetProfileService().findAssetProfileById(edgeEvent.getTenantId(), asset.getAssetProfileId()); builder.addAssetProfileUpdateMsg(EdgeMsgConstructorUtils.constructAssetProfileUpdatedMsg(msgType, assetProfile)); diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/calculated/BaseCalculatedFieldProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/calculated/BaseCalculatedFieldProcessor.java index ddb1b23d53..63cc1e024d 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/calculated/BaseCalculatedFieldProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/calculated/BaseCalculatedFieldProcessor.java @@ -16,18 +16,30 @@ package org.thingsboard.server.service.edge.rpc.processor.calculated; import com.datastax.oss.driver.api.core.uuid.Uuids; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; 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.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.TenantId; +import org.thingsboard.server.common.data.page.PageDataIterableByTenantIdEntityId; import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.gen.edge.v1.CalculatedFieldUpdateMsg; import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor; +import java.util.ArrayList; +import java.util.List; + +import static org.thingsboard.server.dao.edge.BaseRelatedEdgesService.RELATED_EDGES_CACHE_ITEMS; + @Slf4j public abstract class BaseCalculatedFieldProcessor extends BaseEdgeProcessor { @@ -76,4 +88,23 @@ public abstract class BaseCalculatedFieldProcessor extends BaseEdgeProcessor { return Pair.of(isCreated, isNameUpdated); } + protected ListenableFuture pushEventToAllRelatedEdges(TenantId tenantId, EntityId entityId, EdgeEventType type, EdgeEventActionType actionType, EdgeId sourceEdgeId) { + List> futures = new ArrayList<>(); + PageDataIterableByTenantIdEntityId edgeIds = + new PageDataIterableByTenantIdEntityId<>(edgeCtx.getEdgeService()::findRelatedEdgeIdsByEntityId, tenantId, entityId, RELATED_EDGES_CACHE_ITEMS); + for (EdgeId relatedEdgeId : edgeIds) { + if (!relatedEdgeId.equals(sourceEdgeId)) { + futures.add(saveEdgeEvent(tenantId, relatedEdgeId, type, actionType, entityId, null)); + } + } + return Futures.transform(Futures.allAsList(futures), voids -> null, dbCallbackExecutorService); + } + + protected ListenableFuture pushEventToAllEdges(TenantId tenantId, EdgeEventType type, EdgeEventActionType actionType, EntityId entityId, EdgeId sourceEdgeId) { + return switch (actionType) { + case ADDED, UPDATED, DELETED -> processActionForAllEdges(tenantId, type, actionType, entityId, null, sourceEdgeId); + default -> Futures.immediateFuture(null); + }; + } + } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/calculated/CalculatedFieldEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/calculated/CalculatedFieldEdgeProcessor.java index f936b0b1d4..cf28003707 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/calculated/CalculatedFieldEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/calculated/CalculatedFieldEdgeProcessor.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.service.edge.rpc.processor.calculated; +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; @@ -22,26 +23,28 @@ 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.data.relation.EntityRelation; -import org.thingsboard.server.common.data.relation.RelationTypeGroup; 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.List; import java.util.UUID; @Slf4j @@ -88,7 +91,7 @@ public class CalculatedFieldEdgeProcessor extends BaseCalculatedFieldProcessor i switch (edgeEvent.getAction()) { case ADDED, UPDATED -> { CalculatedField calculatedField = edgeCtx.getCalculatedFieldService().findById(edgeEvent.getTenantId(), calculatedFieldId); - if (calculatedField != null && isEntityAssignedToEdge(edgeEvent, calculatedField)) { + if (calculatedField != null) { UpdateMsgType msgType = getUpdateMsgType(edgeEvent.getAction()); CalculatedFieldUpdateMsg calculatedFieldUpdateMsg = EdgeMsgConstructorUtils.constructCalculatedFieldUpdatedMsg(msgType, calculatedField); return DownlinkMsg.newBuilder() @@ -108,24 +111,37 @@ public class CalculatedFieldEdgeProcessor extends BaseCalculatedFieldProcessor i return null; } - private boolean isEntityAssignedToEdge(EdgeEvent edgeEvent, CalculatedField calculatedField) { - switch (calculatedField.getEntityId().getEntityType()) { - case ASSET, DEVICE -> { - List relations = - edgeCtx.getRelationService().findByTo(edgeEvent.getTenantId(), calculatedField.getEntityId(), RelationTypeGroup.EDGE); - return !relations.isEmpty(); - } - default -> { - return true; - } - } - } - @Override public EdgeEventType getEdgeEventType() { return EdgeEventType.CALCULATED_FIELD; } + @Override + public ListenableFuture processEntityNotification(TenantId tenantId, TransportProtos.EdgeNotificationMsgProto edgeNotificationMsg) { + EdgeEventType type = EdgeEventType.valueOf(edgeNotificationMsg.getType()); + EdgeEventActionType actionType = EdgeEventActionType.valueOf(edgeNotificationMsg.getAction()); + EntityId entityId = EntityIdFactory.getByEdgeEventTypeAndUuid(type, new UUID(edgeNotificationMsg.getEntityIdMSB(), edgeNotificationMsg.getEntityIdLSB())); + EdgeId originatorEdgeId = safeGetEdgeId(edgeNotificationMsg.getOriginatorEdgeIdMSB(), edgeNotificationMsg.getOriginatorEdgeIdLSB()); + + switch (actionType) { + case UPDATED: + case ADDED: + EntityId bodyEntityId = JacksonUtil.fromString(edgeNotificationMsg.getBody(), EntityId.class); + if (bodyEntityId != null && + (EntityType.DEVICE.equals(bodyEntityId.getEntityType()) || EntityType.ASSET.equals(bodyEntityId.getEntityType()))) { + JsonNode body = JacksonUtil.toJsonNode(edgeNotificationMsg.getBody()); + EdgeId edgeId = safeGetEdgeId(edgeNotificationMsg.getEdgeIdMSB(), edgeNotificationMsg.getEdgeIdLSB()); + + return edgeId != null ? + saveEdgeEvent(tenantId, edgeId, type, actionType, entityId, body) : + pushEventToAllRelatedEdges(tenantId, entityId, type, actionType, originatorEdgeId); + } else { + return pushEventToAllEdges(tenantId, type, actionType, entityId, originatorEdgeId); + } + default: + return super.processEntityNotification(tenantId, edgeNotificationMsg); + } + } private void processCalculatedField(TenantId tenantId, CalculatedFieldId calculatedFieldId, CalculatedFieldUpdateMsg calculatedFieldUpdateMsg, Edge edge) { Pair resultPair = super.saveOrUpdateCalculatedField(tenantId, calculatedFieldId, calculatedFieldUpdateMsg); diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/DeviceEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/DeviceEdgeProcessor.java index ab01f83cd8..be2a04626c 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/DeviceEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/DeviceEdgeProcessor.java @@ -243,6 +243,9 @@ public class DeviceEdgeProcessor extends BaseDeviceProcessor implements DevicePr DeviceCredentialsUpdateMsg deviceCredentialsUpdateMsg = EdgeMsgConstructorUtils.constructDeviceCredentialsUpdatedMsg(deviceCredentials); builder.addDeviceCredentialsUpdateMsg(deviceCredentialsUpdateMsg).build(); } + + getCalculatedFieldUpdateMsgs(edgeEvent.getTenantId(), deviceId).forEach(builder::addCalculatedFieldUpdateMsg); + if (UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE.equals(msgType)) { DeviceProfile deviceProfile = edgeCtx.getDeviceProfileService().findDeviceProfileById(edgeEvent.getTenantId(), device.getDeviceProfileId()); builder.addDeviceProfileUpdateMsg(EdgeMsgConstructorUtils.constructDeviceProfileUpdatedMsg(msgType, deviceProfile)); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/edge/EdgeEventType.java b/common/data/src/main/java/org/thingsboard/server/common/data/edge/EdgeEventType.java index 9c97935329..0d5c3f34ad 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/edge/EdgeEventType.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/edge/EdgeEventType.java @@ -47,7 +47,7 @@ public enum EdgeEventType { TB_RESOURCE(true, EntityType.TB_RESOURCE), OAUTH2_CLIENT(true, EntityType.OAUTH2_CLIENT), DOMAIN(true, EntityType.DOMAIN), - CALCULATED_FIELD(true, EntityType.CALCULATED_FIELD); + CALCULATED_FIELD(false, EntityType.CALCULATED_FIELD); private final boolean allEdgesRelated; From 411943d993b54d45e42e3ec4d3b57b7f837a41dc Mon Sep 17 00:00:00 2001 From: yevhenii Date: Thu, 5 Jun 2025 19:00:10 +0300 Subject: [PATCH 11/54] CalculatedField functionality support for Edge - fixed test --- .../server/edge/CalculatedFieldEdgeTest.java | 18 ++++++++++++------ .../server/dao/edge/EdgeServiceImpl.java | 1 + 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/application/src/test/java/org/thingsboard/server/edge/CalculatedFieldEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/CalculatedFieldEdgeTest.java index 0f785c4063..da75225d11 100644 --- a/application/src/test/java/org/thingsboard/server/edge/CalculatedFieldEdgeTest.java +++ b/application/src/test/java/org/thingsboard/server/edge/CalculatedFieldEdgeTest.java @@ -38,6 +38,7 @@ 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.List; import java.util.Map; import java.util.Optional; import java.util.UUID; @@ -57,14 +58,16 @@ public class CalculatedFieldEdgeTest extends AbstractEdgeTest { SimpleCalculatedFieldConfiguration config = new SimpleCalculatedFieldConfiguration(); CalculatedField calculatedField = createSimpleCalculatedField(savedDevice.getId(), config); - edgeImitator.expectMessageAmount(1); + edgeImitator.expectMessageAmount(SYNC_MESSAGE_COUNT + 4); CalculatedField savedCalculatedField = doPost("/api/calculatedField", calculatedField, CalculatedField.class); + doPost("/api/edge/sync/" + edge.getId()); Assert.assertTrue(edgeImitator.waitForMessages()); - AbstractMessage latestMessage = edgeImitator.getLatestMessage(); + List downlinkMsgs = edgeImitator.getDownlinkMsgs(); + AbstractMessage latestMessage = downlinkMsgs.stream().filter(downlinkMsg -> downlinkMsg instanceof CalculatedFieldUpdateMsg).findFirst().get(); Assert.assertTrue(latestMessage instanceof CalculatedFieldUpdateMsg); CalculatedFieldUpdateMsg calculatedFieldUpdateMsg = (CalculatedFieldUpdateMsg) latestMessage; - Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, calculatedFieldUpdateMsg.getMsgType()); + Assert.assertEquals(UpdateMsgType.ENTITY_UPDATED_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); @@ -75,11 +78,13 @@ public class CalculatedFieldEdgeTest extends AbstractEdgeTest { Assert.assertEquals(config, calculatedFieldFromMsg.getConfiguration()); // update calculatedField - edgeImitator.expectMessageAmount(1); + edgeImitator.expectMessageAmount(SYNC_MESSAGE_COUNT + 4); savedCalculatedField.setName(UPDATED_CF_NAME); savedCalculatedField = doPost("/api/calculatedField", savedCalculatedField, CalculatedField.class); + doPost("/api/edge/sync/" + edge.getId()); Assert.assertTrue(edgeImitator.waitForMessages()); - latestMessage = edgeImitator.getLatestMessage(); + downlinkMsgs = edgeImitator.getDownlinkMsgs(); + latestMessage = downlinkMsgs.stream().filter(downlinkMsg -> downlinkMsg instanceof CalculatedFieldUpdateMsg).findFirst().get(); Assert.assertTrue(latestMessage instanceof CalculatedFieldUpdateMsg); calculatedFieldUpdateMsg = (CalculatedFieldUpdateMsg) latestMessage; calculatedFieldFromMsg = JacksonUtil.fromString(calculatedFieldUpdateMsg.getEntity(), CalculatedField.class, true); @@ -139,8 +144,9 @@ public class CalculatedFieldEdgeTest extends AbstractEdgeTest { SimpleCalculatedFieldConfiguration config = new SimpleCalculatedFieldConfiguration(); CalculatedField calculatedField = createSimpleCalculatedField(savedDevice.getId(), config); - edgeImitator.expectMessageAmount(1); + edgeImitator.expectMessageAmount(SYNC_MESSAGE_COUNT + 4); CalculatedField savedCalculatedField = doPost("/api/calculatedField", calculatedField, CalculatedField.class); + doPost("/api/edge/sync/" + edge.getId()); Assert.assertTrue(edgeImitator.waitForMessages()); UUID uuid = Uuids.timeBased(); diff --git a/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeServiceImpl.java index 0655d05572..ebcfd7678a 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeServiceImpl.java @@ -524,6 +524,7 @@ public class EdgeServiceImpl extends AbstractCachedEntityService Date: Mon, 9 Jun 2025 13:25:22 +0300 Subject: [PATCH 12/54] CalculatedField functionality support for Edge - changed find by TenantId to EntityId --- .../calculated/BaseCalculatedFieldProcessor.java | 2 +- .../thingsboard/server/dao/cf/CalculatedFieldService.java | 2 +- .../server/dao/cf/BaseCalculatedFieldService.java | 8 ++++---- .../org/thingsboard/server/dao/cf/CalculatedFieldDao.java | 2 +- .../server/dao/sql/cf/CalculatedFieldRepository.java | 2 +- .../server/dao/sql/cf/JpaCalculatedFieldDao.java | 4 ++-- 6 files changed, 10 insertions(+), 10 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/calculated/BaseCalculatedFieldProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/calculated/BaseCalculatedFieldProcessor.java index 63cc1e024d..da3051045f 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/calculated/BaseCalculatedFieldProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/calculated/BaseCalculatedFieldProcessor.java @@ -65,7 +65,7 @@ public abstract class BaseCalculatedFieldProcessor extends BaseEdgeProcessor { } String calculatedFieldName = calculatedField.getName(); - CalculatedField calculatedFieldByName = edgeCtx.getCalculatedFieldService().findByTenantIdAndName(tenantId, calculatedFieldName); + 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 {}", diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/cf/CalculatedFieldService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/cf/CalculatedFieldService.java index a645903896..85cd8d24fd 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/cf/CalculatedFieldService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/cf/CalculatedFieldService.java @@ -35,7 +35,7 @@ public interface CalculatedFieldService extends EntityDaoService { CalculatedField findById(TenantId tenantId, CalculatedFieldId calculatedFieldId); - CalculatedField findByTenantIdAndName(TenantId tenantId, String name); + CalculatedField findByEntityIdAndName(EntityId entityId, String name); List findCalculatedFieldIdsByEntityId(TenantId tenantId, EntityId entityId); diff --git a/dao/src/main/java/org/thingsboard/server/dao/cf/BaseCalculatedFieldService.java b/dao/src/main/java/org/thingsboard/server/dao/cf/BaseCalculatedFieldService.java index 5d4d3c54da..793f839fb9 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/cf/BaseCalculatedFieldService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/cf/BaseCalculatedFieldService.java @@ -100,11 +100,11 @@ public class BaseCalculatedFieldService extends AbstractEntityService implements } @Override - public CalculatedField findByTenantIdAndName(TenantId tenantId, String name) { - log.trace("Executing findByTenantIdAndName [{}], calculatedFieldName[{}]", tenantId, name); - validateId(tenantId, id -> INCORRECT_TENANT_ID + id); + public CalculatedField findByEntityIdAndName(EntityId entityId, String name) { + log.trace("Executing findByEntityIdAndName [{}], calculatedFieldName[{}]", entityId, name); + validateId(entityId.getId(), id -> INCORRECT_ENTITY_ID + id); - return calculatedFieldDao.findByTenantIdAndName(tenantId, name).orElse(null); + return calculatedFieldDao.findByEntityIdAndName(entityId, name).orElse(null); } @Override diff --git a/dao/src/main/java/org/thingsboard/server/dao/cf/CalculatedFieldDao.java b/dao/src/main/java/org/thingsboard/server/dao/cf/CalculatedFieldDao.java index 44dbb26b81..bd244b9d24 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/cf/CalculatedFieldDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/cf/CalculatedFieldDao.java @@ -36,7 +36,7 @@ public interface CalculatedFieldDao extends Dao { List findAll(); - Optional findByTenantIdAndName(TenantId tenantId, String name); + Optional findByEntityIdAndName(EntityId entityId, String name); PageData findAll(PageLink pageLink); diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/cf/CalculatedFieldRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/cf/CalculatedFieldRepository.java index 91721c74f3..8ccdb88db0 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/cf/CalculatedFieldRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/cf/CalculatedFieldRepository.java @@ -28,7 +28,7 @@ public interface CalculatedFieldRepository extends JpaRepository findCalculatedFieldIdsByTenantIdAndEntityId(UUID tenantId, UUID entityId); diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/cf/JpaCalculatedFieldDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/cf/JpaCalculatedFieldDao.java index f18fe10f8c..23a146bc47 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/cf/JpaCalculatedFieldDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/cf/JpaCalculatedFieldDao.java @@ -67,8 +67,8 @@ public class JpaCalculatedFieldDao extends JpaAbstractDao findByTenantIdAndName(TenantId tenantId, String name) { - CalculatedField calculatedField = DaoUtil.getData(calculatedFieldRepository.findByTenantIdAndName(tenantId.getId(), name)); + public Optional findByEntityIdAndName(EntityId entityId, String name) { + CalculatedField calculatedField = DaoUtil.getData(calculatedFieldRepository.findByEntityIdAndName(entityId.getId(), name)); return Optional.ofNullable(calculatedField); } From 27473619dbc11e9a9f5598a5cea61f7f34b59496 Mon Sep 17 00:00:00 2001 From: Andrii Landiak Date: Wed, 11 Jun 2025 09:54:33 +0300 Subject: [PATCH 13/54] Ota package vc support --- .../main/data/upgrade/basic/schema_update.sql | 49 ++--------- .../DefaultEntitiesExportImportService.java | 2 +- .../exporting/impl/DeviceExportService.java | 2 + .../impl/DeviceProfileExportService.java | 2 + .../impl/OtaPackageExportService.java | 44 ++++++++++ .../impl/BaseEntityImportService.java | 15 ++-- .../importing/impl/DeviceImportService.java | 4 +- .../impl/DeviceProfileImportService.java | 6 +- .../impl/OtaPackageImportService.java | 83 +++++++++++++++++++ .../sync/ie/ExportImportServiceSqlTest.java | 29 +++++-- .../service/sync/vc/VersionControlTest.java | 75 +++++++++++++++-- .../server/dao/ota/OtaPackageService.java | 3 + .../server/common/data/OtaPackage.java | 3 + .../server/common/data/OtaPackageInfo.java | 22 ++++- .../server/common/data/TbResource.java | 3 + .../server/common/data/sync/JsonTbEntity.java | 4 +- .../dao/model/sql/OtaPackageEntity.java | 7 ++ .../dao/model/sql/OtaPackageInfoEntity.java | 9 +- .../server/dao/ota/BaseOtaPackageService.java | 37 ++++----- .../server/dao/ota/OtaPackageDao.java | 7 +- .../server/dao/sql/ota/JpaOtaPackageDao.java | 46 ++++++++-- .../dao/sql/ota/OtaPackageInfoRepository.java | 7 +- .../dao/sql/ota/OtaPackageRepository.java | 12 ++- .../resources/sql/schema-entities-idx.sql | 2 + .../main/resources/sql/schema-entities.sql | 4 +- 25 files changed, 370 insertions(+), 107 deletions(-) create mode 100644 application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/OtaPackageExportService.java create mode 100644 application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/OtaPackageImportService.java diff --git a/application/src/main/data/upgrade/basic/schema_update.sql b/application/src/main/data/upgrade/basic/schema_update.sql index e4bbe4c69e..2f652ce289 100644 --- a/application/src/main/data/upgrade/basic/schema_update.sql +++ b/application/src/main/data/upgrade/basic/schema_update.sql @@ -14,48 +14,11 @@ -- 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 NOTIFICATION RULE CASSANDRA RATE LIMITS 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"%'; - --- UPDATE NOTIFICATION RULE CASSANDRA RATE LIMITS END +-- UPDATE OTA PACKAGE EXTERNAL ID END diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/DefaultEntitiesExportImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/DefaultEntitiesExportImportService.java index db7e37b368..ee9f9e3cea 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/DefaultEntitiesExportImportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/DefaultEntitiesExportImportService.java @@ -67,7 +67,7 @@ public class DefaultEntitiesExportImportService implements EntitiesExportImportS protected static final List 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 ); diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DeviceExportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DeviceExportService.java index 7d5f7ee57e..2f5355f637 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DeviceExportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DeviceExportService.java @@ -38,6 +38,8 @@ public class DeviceExportService extends BaseEntityExportService 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); diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DeviceProfileExportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DeviceProfileExportService.java index 6c212f0684..6a98bd2a7c 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DeviceProfileExportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DeviceProfileExportService.java @@ -34,6 +34,8 @@ public class DeviceProfileExportService extends BaseEntityExportService> { + + @Override + protected void setRelatedEntities(EntitiesExportCtx ctx, OtaPackage otaPackage, EntityExportData exportData) { + otaPackage.setDeviceProfileId(getExternalIdOrElseInternal(ctx, otaPackage.getDeviceProfileId())); + } + + @Override + public Set getSupportedEntityTypes() { + return Set.of(EntityType.OTA_PACKAGE); + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/BaseEntityImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/BaseEntityImportService.java index 57b4737be6..92fdcb09c4 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/BaseEntityImportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/BaseEntityImportService.java @@ -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 importResult, D exportData, IdProvider idProvider) throws ThingsboardException { E savedEntity = importResult.getSavedEntity(); E oldEntity = importResult.getOldEntity(); @@ -405,7 +404,9 @@ public abstract class BaseEntityImportService 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 getInternalIdByUuid(UUID externalUuid, boolean fetchAllUUIDs, Set 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 externalId = buildEntityId(entityType, externalUuid); @@ -483,10 +486,6 @@ public abstract class BaseEntityImportService T getOldEntityField(O oldEntity, Function getter) { - return oldEntity == null ? null : getter.apply(oldEntity); - } - protected void replaceIdsRecursively(EntitiesImportCtx ctx, IdProvider idProvider, JsonNode json, Set skippedRootFields, Pattern includedFieldsPattern, LinkedHashSet hints) { diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/DeviceImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/DeviceImportService.java index 4ace9ff938..0cbb471b6f 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/DeviceImportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/DeviceImportService.java @@ -44,8 +44,8 @@ public class DeviceImportService extends BaseEntityImportService> { + + 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, EntityExportData 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.findOtaPackageByTenantIdAndTitle(ctx.getTenantId(), otaPackage.getType(), otaPackage.getTitle()); + } + return existingOtaPackage; + } + + @Override + protected OtaPackage deepCopy(OtaPackage otaPackage) { + return new OtaPackage(otaPackage); + } + + @Override + protected void cleanupForComparison(OtaPackage otaPackage) { + super.cleanupForComparison(otaPackage); + } + + @Override + protected OtaPackage saveOrUpdate(EntitiesImportCtx ctx, OtaPackage otaPackage, EntityExportData exportData, IdProvider idProvider, CompareResult compareResult) { + return otaPackageService.saveOtaPackage(otaPackage); + } + + @Override + protected void onEntitySaved(User user, OtaPackage savedOtaPackage, OtaPackage oldOtaPackage) throws ThingsboardException { + super.onEntitySaved(user, savedOtaPackage, oldOtaPackage); + } + + @Override + public EntityType getEntityType() { + return EntityType.OTA_PACKAGE; + } + +} diff --git a/application/src/test/java/org/thingsboard/server/service/sync/ie/ExportImportServiceSqlTest.java b/application/src/test/java/org/thingsboard/server/service/sync/ie/ExportImportServiceSqlTest.java index da3b214afb..e70a0cd37c 100644 --- a/application/src/test/java/org/thingsboard/server/service/sync/ie/ExportImportServiceSqlTest.java +++ b/application/src/test/java/org/thingsboard/server/service/sync/ie/ExportImportServiceSqlTest.java @@ -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 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 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 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 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 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 calculatedFields = ((DeviceExportData) entityExportData).getCalculatedFields(); + List 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); diff --git a/application/src/test/java/org/thingsboard/server/service/sync/vc/VersionControlTest.java b/application/src/test/java/org/thingsboard/server/service/sync/vc/VersionControlTest.java index c7d4068210..5c5b7bb107 100644 --- a/application/src/test/java/org/thingsboard/server/service/sync/vc/VersionControlTest.java +++ b/application/src/test/java/org/thingsboard/server/service/sync/vc/VersionControlTest.java @@ -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 result = loadVersion(versionId, EntityType.DEVICE, EntityType.DEVICE_PROFILE); + Map 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,52 @@ 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 testOtaPackageVc_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); + String versionId = createVersion("ota packages", EntityType.DEVICE_PROFILE, EntityType.OTA_PACKAGE); + + OtaPackage firmwareOta = findOtaPackage(firmware.getTitle()); + OtaPackage softwareOta = findOtaPackage(software.getTitle()); + + loginTenant2(); + loadVersion(versionId, EntityType.DEVICE_PROFILE, EntityType.OTA_PACKAGE); + OtaPackage importedFirmwareOta = findOtaPackage(firmwareOta.getTitle()); + OtaPackage importedSoftwareOta = findOtaPackage(softwareOta.getTitle()); + checkImportedEntity(tenantId1, firmwareOta, tenantId2, importedFirmwareOta); + checkImportedOtaPackageData(firmwareOta, importedFirmwareOta); + checkImportedEntity(tenantId1, softwareOta, tenantId2, importedSoftwareOta); + checkImportedOtaPackageData(softwareOta, importedSoftwareOta); + } + + 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 +981,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 +992,10 @@ public class VersionControlTest extends AbstractControllerTest { return otaPackageService.saveOtaPackage(otaPackage); } + private OtaPackage findOtaPackage(String title) throws Exception { + return doGetTypedWithPageLink("/api/otaPackages?", new TypeReference>() {}, new PageLink(100, 0, title)).getData().get(0); + } + protected Dashboard createDashboard(CustomerId customerId, String name) { Dashboard dashboard = new Dashboard(); dashboard.setTitle(name); diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/ota/OtaPackageService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/ota/OtaPackageService.java index 12c8e5053b..561b807b2b 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/ota/OtaPackageService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/ota/OtaPackageService.java @@ -41,6 +41,8 @@ public interface OtaPackageService extends EntityDaoService { OtaPackageInfo findOtaPackageInfoById(TenantId tenantId, OtaPackageId otaPackageId); + OtaPackage findOtaPackageByTenantIdAndTitle(TenantId tenantId, OtaPackageType type, String title); + ListenableFuture findOtaPackageInfoByIdAsync(TenantId tenantId, OtaPackageId otaPackageId); PageData findTenantOtaPackagesByTenantId(TenantId tenantId, PageLink pageLink); @@ -52,4 +54,5 @@ public interface OtaPackageService extends EntityDaoService { void deleteOtaPackagesByTenantId(TenantId tenantId); long sumDataSizeByTenantId(TenantId tenantId); + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/OtaPackage.java b/common/data/src/main/java/org/thingsboard/server/common/data/OtaPackage.java index 26376bbda3..9b4609dfef 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/OtaPackage.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/OtaPackage.java @@ -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,5 @@ public class OtaPackage extends OtaPackageInfo { super(otaPackage); this.data = otaPackage.getData(); } + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/OtaPackageInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/OtaPackageInfo.java index dfdbd83e80..138e7690e3 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/OtaPackageInfo.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/OtaPackageInfo.java @@ -15,7 +15,9 @@ */ package org.thingsboard.server.common.data; +import com.fasterxml.jackson.annotation.JsonAlias; 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 +31,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 implements HasName, HasTenantId, HasTitle { +public class OtaPackageInfo extends BaseDataWithAdditionalInfo implements HasName, HasTenantId, HasTitle, ExportableEntity { + @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 +82,8 @@ public class OtaPackageInfo extends BaseDataWithAdditionalInfo 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 +107,7 @@ public class OtaPackageInfo extends BaseDataWithAdditionalInfo 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. " + @@ -128,9 +136,21 @@ public class OtaPackageInfo extends BaseDataWithAdditionalInfo imp return StringUtils.isNotEmpty(url); } + @JsonProperty("otaVersion") + @JsonAlias("version") + public void setVersion(String version) { + this.version = version; + } + + @JsonProperty("otaVersion") + public String getVersion() { + return version; + } + @Schema(description = "OTA Package description.", example = "Description for the OTA Package fw_1.0") @Override public JsonNode getAdditionalInfo() { return super.getAdditionalInfo(); } + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/TbResource.java b/common/data/src/main/java/org/thingsboard/server/common/data/TbResource.java index 498fa5be3e..ba37067106 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/TbResource.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/TbResource.java @@ -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(); } + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/sync/JsonTbEntity.java b/common/data/src/main/java/org/thingsboard/server/common/data/sync/JsonTbEntity.java index c763daeb7f..9d7187f7f4 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/sync/JsonTbEntity.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/sync/JsonTbEntity.java @@ -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.asset.Asset; import org.thingsboard.server.common.data.asset.AssetProfile; @@ -58,7 +59,8 @@ import java.lang.annotation.Target; @Type(name = "NOTIFICATION_TEMPLATE", value = NotificationTemplate.class), @Type(name = "NOTIFICATION_TARGET", value = NotificationTarget.class), @Type(name = "NOTIFICATION_RULE", value = NotificationRule.class), - @Type(name = "TB_RESOURCE", value = TbResource.class) + @Type(name = "TB_RESOURCE", value = TbResource.class), + @Type(name = "OTA_PACKAGE", value = OtaPackage.class) }) @JsonIgnoreProperties(value = {"tenantId", "createdTime", "version"}, ignoreUnknown = true) public @interface JsonTbEntity { diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/OtaPackageEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/OtaPackageEntity.java index 1ffafd305e..6ee4bdd89f 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/OtaPackageEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/OtaPackageEntity.java @@ -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 { @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 { 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.setHasData(true); } otaPackage.setAdditionalInfo(additionalInfo); + otaPackage.setExternalId(getEntityId(externalId, OtaPackageId::new)); return otaPackage; } + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/OtaPackageInfoEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/OtaPackageInfoEntity.java index c1e83cf511..a1625f2595 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/OtaPackageInfoEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/OtaPackageInfoEntity.java @@ -100,6 +100,9 @@ public class OtaPackageInfoEntity extends BaseSqlEntity { @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 { 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 { 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.setDataSize(dataSize); otaPackageInfo.setAdditionalInfo(additionalInfo); otaPackageInfo.setHasData(hasData); + otaPackageInfo.setExternalId(getEntityId(externalId, OtaPackageId::new)); return otaPackageInfo; } + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/ota/BaseOtaPackageService.java b/dao/src/main/java/org/thingsboard/server/dao/ota/BaseOtaPackageService.java index 16d8517b6d..bf52de037f 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/ota/BaseOtaPackageService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/ota/BaseOtaPackageService.java @@ -54,6 +54,7 @@ import static org.thingsboard.server.dao.service.Validator.validatePageLink; @Slf4j @RequiredArgsConstructor public class BaseOtaPackageService extends AbstractCachedEntityService 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 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 +164,12 @@ public class BaseOtaPackageService extends AbstractCachedEntityService otaPackageInfoDao.findById(tenantId, otaPackageId.getId()), true); } + @Override + public OtaPackage findOtaPackageByTenantIdAndTitle(TenantId tenantId, OtaPackageType type, String title) { + log.trace("Executing findOtaPackageByTenantIdAndTitle [{}] [{}] [{}]", tenantId, type, title); + return otaPackageDao.findOtaPackageByTenantIdAndTitle(tenantId, type, title); + } + @Override public ListenableFuture findOtaPackageInfoByIdAsync(TenantId tenantId, OtaPackageId otaPackageId) { log.trace("Executing findOtaPackageInfoByIdAsync [{}]", otaPackageId); diff --git a/dao/src/main/java/org/thingsboard/server/dao/ota/OtaPackageDao.java b/dao/src/main/java/org/thingsboard/server/dao/ota/OtaPackageDao.java index f8f877e55e..de81f4e72d 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/ota/OtaPackageDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/ota/OtaPackageDao.java @@ -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, TenantEntityWithDataDao { +public interface OtaPackageDao extends Dao, TenantEntityWithDataDao, ExportableEntityDao { Long sumDataSizeByTenantId(TenantId tenantId); + OtaPackage findOtaPackageByTenantIdAndTitle(TenantId tenantId, OtaPackageType type, String title); + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/ota/JpaOtaPackageDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/ota/JpaOtaPackageDao.java index 780f67932e..da96b83d28 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/ota/JpaOtaPackageDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/ota/JpaOtaPackageDao.java @@ -22,7 +22,9 @@ 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.ota.OtaPackageType; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.dao.DaoUtil; @@ -43,24 +45,52 @@ public class JpaOtaPackageDao extends JpaAbstractDao getEntityClass() { - return OtaPackageEntity.class; + public Long sumDataSizeByTenantId(TenantId tenantId) { + return otaPackageRepository.sumDataSizeByTenantId(tenantId.getId()); } + @Transactional @Override - protected JpaRepository getRepository() { - return otaPackageRepository; + public OtaPackage findOtaPackageByTenantIdAndTitle(TenantId tenantId, OtaPackageType type, String title) { + return DaoUtil.getData(otaPackageRepository.findByTenantIdAndTypeAndTitle(tenantId.getId(), type, title)); } + @Transactional @Override - public Long sumDataSizeByTenantId(TenantId tenantId) { - return otaPackageRepository.sumDataSizeByTenantId(tenantId.getId()); + public PageData findAllByTenantId(TenantId tenantId, PageLink pageLink) { + return DaoUtil.toPageData(otaPackageRepository.findByTenantId(tenantId.getId(), DaoUtil.toPageable(pageLink))); } @Transactional @Override - public PageData findAllByTenantId(TenantId tenantId, PageLink pageLink) { - return DaoUtil.toPageData(otaPackageRepository.findByTenantId(tenantId.getId(), DaoUtil.toPageable(pageLink))); + public PageData findByTenantId(UUID tenantId, PageLink pageLink) { + return findAllByTenantId(TenantId.fromUUID(tenantId), pageLink); + } + + @Override + public PageData 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 getEntityClass() { + return OtaPackageEntity.class; + } + + @Override + protected JpaRepository getRepository() { + return otaPackageRepository; } @Override diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/ota/OtaPackageInfoRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/ota/OtaPackageInfoRepository.java index fe7e2bf015..c5ca894a00 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/ota/OtaPackageInfoRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/ota/OtaPackageInfoRepository.java @@ -26,14 +26,15 @@ import org.thingsboard.server.dao.model.sql.OtaPackageInfoEntity; import java.util.UUID; public interface OtaPackageInfoRepository extends JpaRepository { - @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 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 { +public interface OtaPackageRepository extends JpaRepository, ExportableEntityRepository { @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 findByTenantId(UUID tenantId, Pageable pageable); + OtaPackageEntity findByTenantIdAndTypeAndTitle(UUID tenantId, OtaPackageType type, String title); + + @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 findIdsByTenantId(@Param("tenantId") UUID tenantId, Pageable pageable); + } diff --git a/dao/src/main/resources/sql/schema-entities-idx.sql b/dao/src/main/resources/sql/schema-entities-idx.sql index ad311f00df..0a6d6e578d 100644 --- a/dao/src/main/resources/sql/schema-entities-idx.sql +++ b/dao/src/main/resources/sql/schema-entities-idx.sql @@ -91,6 +91,8 @@ CREATE INDEX IF NOT EXISTS idx_widgets_bundle_external_id ON widgets_bundle(tena CREATE INDEX IF NOT EXISTS idx_rule_node_external_id ON rule_node(rule_chain_id, external_id); +CREATE INDEX IF NOT EXISTS idx_ota_package_external_id ON ota_package(tenant_id, external_id); + CREATE INDEX IF NOT EXISTS idx_rule_node_type_id_configuration_version ON rule_node(type, id, configuration_version); CREATE INDEX IF NOT EXISTS idx_api_usage_state_entity_id ON api_usage_state(entity_id); diff --git a/dao/src/main/resources/sql/schema-entities.sql b/dao/src/main/resources/sql/schema-entities.sql index f2a0bc26c1..2cc8cbad0e 100644 --- a/dao/src/main/resources/sql/schema-entities.sql +++ b/dao/src/main/resources/sql/schema-entities.sql @@ -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 ( From 7ffd468d0684b69a566acc1119053475e5466649 Mon Sep 17 00:00:00 2001 From: Andrii Landiak Date: Wed, 11 Jun 2025 15:13:59 +0300 Subject: [PATCH 14/54] Improve OtaPackageExportData --- .../impl/OtaPackageExportService.java | 11 ++++-- .../impl/OtaPackageImportService.java | 15 ++------ .../server/common/data/OtaPackageInfo.java | 13 ------- .../common/data/sync/ie/EntityExportData.java | 3 +- .../data/sync/ie/OtaPackageExportData.java | 37 +++++++++++++++++++ .../thingsboard/common/util/JacksonUtil.java | 3 -- .../server/dao/sql/JpaAbstractDao.java | 3 -- .../server/dao/sql/asset/JpaAssetDao.java | 3 -- 8 files changed, 51 insertions(+), 37 deletions(-) create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/OtaPackageExportData.java diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/OtaPackageExportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/OtaPackageExportService.java index f9285e3699..1cfc70e39f 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/OtaPackageExportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/OtaPackageExportService.java @@ -20,7 +20,7 @@ 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.EntityExportData; +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; @@ -29,13 +29,18 @@ import java.util.Set; @Service @TbCoreComponent @RequiredArgsConstructor -public class OtaPackageExportService extends BaseEntityExportService> { +public class OtaPackageExportService extends BaseEntityExportService { @Override - protected void setRelatedEntities(EntitiesExportCtx ctx, OtaPackage otaPackage, EntityExportData exportData) { + 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 getSupportedEntityTypes() { return Set.of(EntityType.OTA_PACKAGE); diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/OtaPackageImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/OtaPackageImportService.java index b5c583e94d..d181a9bf0a 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/OtaPackageImportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/OtaPackageImportService.java @@ -19,11 +19,9 @@ 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.User; -import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.OtaPackageId; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.sync.ie.EntityExportData; +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; @@ -31,7 +29,7 @@ import org.thingsboard.server.service.sync.vc.data.EntitiesImportCtx; @Service @TbCoreComponent @RequiredArgsConstructor -public class OtaPackageImportService extends BaseEntityImportService> { +public class OtaPackageImportService extends BaseEntityImportService { private final OtaPackageService otaPackageService; @@ -41,7 +39,7 @@ public class OtaPackageImportService extends BaseEntityImportService exportData, IdProvider idProvider) { + protected OtaPackage prepare(EntitiesImportCtx ctx, OtaPackage otaPackage, OtaPackage oldOtaPackage, OtaPackageExportData exportData, IdProvider idProvider) { otaPackage.setDeviceProfileId(idProvider.getInternalId(otaPackage.getDeviceProfileId())); return otaPackage; } @@ -66,15 +64,10 @@ public class OtaPackageImportService extends BaseEntityImportService exportData, IdProvider idProvider, CompareResult compareResult) { + protected OtaPackage saveOrUpdate(EntitiesImportCtx ctx, OtaPackage otaPackage, OtaPackageExportData exportData, IdProvider idProvider, CompareResult compareResult) { return otaPackageService.saveOtaPackage(otaPackage); } - @Override - protected void onEntitySaved(User user, OtaPackage savedOtaPackage, OtaPackage oldOtaPackage) throws ThingsboardException { - super.onEntitySaved(user, savedOtaPackage, oldOtaPackage); - } - @Override public EntityType getEntityType() { return EntityType.OTA_PACKAGE; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/OtaPackageInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/OtaPackageInfo.java index 138e7690e3..2930ff41d1 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/OtaPackageInfo.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/OtaPackageInfo.java @@ -15,9 +15,7 @@ */ package org.thingsboard.server.common.data; -import com.fasterxml.jackson.annotation.JsonAlias; 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; @@ -136,17 +134,6 @@ public class OtaPackageInfo extends BaseDataWithAdditionalInfo imp return StringUtils.isNotEmpty(url); } - @JsonProperty("otaVersion") - @JsonAlias("version") - public void setVersion(String version) { - this.version = version; - } - - @JsonProperty("otaVersion") - public String getVersion() { - return version; - } - @Schema(description = "OTA Package description.", example = "Description for the OTA Package fw_1.0") @Override public JsonNode getAdditionalInfo() { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/EntityExportData.java b/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/EntityExportData.java index a1692aef04..5e1f98638a 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/EntityExportData.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/EntityExportData.java @@ -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 diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/OtaPackageExportData.java b/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/OtaPackageExportData.java new file mode 100644 index 0000000000..df12bb5b08 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/OtaPackageExportData.java @@ -0,0 +1,37 @@ +/** + * 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 { + + @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); + } + +} diff --git a/common/util/src/main/java/org/thingsboard/common/util/JacksonUtil.java b/common/util/src/main/java/org/thingsboard/common/util/JacksonUtil.java index d153501b92..cd61c7ac20 100644 --- a/common/util/src/main/java/org/thingsboard/common/util/JacksonUtil.java +++ b/common/util/src/main/java/org/thingsboard/common/util/JacksonUtil.java @@ -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 { diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/JpaAbstractDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/JpaAbstractDao.java index 05577b68e1..6f0380752f 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/JpaAbstractDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/JpaAbstractDao.java @@ -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, D> diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/asset/JpaAssetDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/asset/JpaAssetDao.java index 4e99fb57e4..4b55884792 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/asset/JpaAssetDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/asset/JpaAssetDao.java @@ -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 From 562b23aef103092d69f1c5025ea713d257c4c8e2 Mon Sep 17 00:00:00 2001 From: Andrii Landiak Date: Wed, 11 Jun 2025 15:50:35 +0300 Subject: [PATCH 15/54] Minor clean up --- .../sync/ie/importing/impl/OtaPackageImportService.java | 5 ----- 1 file changed, 5 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/OtaPackageImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/OtaPackageImportService.java index d181a9bf0a..896724c827 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/OtaPackageImportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/OtaPackageImportService.java @@ -58,11 +58,6 @@ public class OtaPackageImportService extends BaseEntityImportService Date: Wed, 11 Jun 2025 16:09:34 +0300 Subject: [PATCH 16/54] Improve findOtaByName in VC --- .../sync/ie/importing/impl/OtaPackageImportService.java | 2 +- .../org/thingsboard/server/dao/ota/OtaPackageService.java | 2 +- .../thingsboard/server/dao/ota/BaseOtaPackageService.java | 6 +++--- .../java/org/thingsboard/server/dao/ota/OtaPackageDao.java | 2 +- .../thingsboard/server/dao/sql/ota/JpaOtaPackageDao.java | 5 ++--- .../server/dao/sql/ota/OtaPackageRepository.java | 2 +- 6 files changed, 9 insertions(+), 10 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/OtaPackageImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/OtaPackageImportService.java index 896724c827..16b204411e 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/OtaPackageImportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/OtaPackageImportService.java @@ -48,7 +48,7 @@ public class OtaPackageImportService extends BaseEntityImportService findOtaPackageInfoByIdAsync(TenantId tenantId, OtaPackageId otaPackageId); diff --git a/dao/src/main/java/org/thingsboard/server/dao/ota/BaseOtaPackageService.java b/dao/src/main/java/org/thingsboard/server/dao/ota/BaseOtaPackageService.java index bf52de037f..83cb35d152 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/ota/BaseOtaPackageService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/ota/BaseOtaPackageService.java @@ -165,9 +165,9 @@ public class BaseOtaPackageService extends AbstractCachedEntityService, TenantEntityWithDataDao, Long sumDataSizeByTenantId(TenantId tenantId); - OtaPackage findOtaPackageByTenantIdAndTitle(TenantId tenantId, OtaPackageType type, String title); + OtaPackage findOtaPackageByTenantIdAndTitleAndVersion(TenantId tenantId, String title, String version); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/ota/JpaOtaPackageDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/ota/JpaOtaPackageDao.java index da96b83d28..7322ea4cb6 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/ota/JpaOtaPackageDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/ota/JpaOtaPackageDao.java @@ -24,7 +24,6 @@ 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.ota.OtaPackageType; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.dao.DaoUtil; @@ -51,8 +50,8 @@ public class JpaOtaPackageDao extends JpaAbstractDao findByTenantId(UUID tenantId, Pageable pageable); - OtaPackageEntity findByTenantIdAndTypeAndTitle(UUID tenantId, OtaPackageType type, String title); + OtaPackageEntity findByTenantIdAndTitleAndVersion(UUID tenantId, String title, String version); @Query("SELECT externalId FROM OtaPackageEntity WHERE id = :id") UUID getExternalIdById(@Param("id") UUID id); From fbb7d5c415b734834ba4bd9caf8031a7930f7644 Mon Sep 17 00:00:00 2001 From: Andrii Landiak Date: Thu, 12 Jun 2025 16:46:02 +0300 Subject: [PATCH 17/54] Minor improvement to correctly save ota if using the same device --- .../sync/ie/importing/impl/DeviceProfileImportService.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/DeviceProfileImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/DeviceProfileImportService.java index e66d2d1267..c867286285 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/DeviceProfileImportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/DeviceProfileImportService.java @@ -45,8 +45,8 @@ public class DeviceProfileImportService extends BaseEntityImportService Date: Wed, 18 Jun 2025 16:42:45 +0300 Subject: [PATCH 18/54] EDQS: human readable response on failed to send kafka msg --- .../server/queue/TbQueueHandler.java | 7 +++-- .../server/edqs/processor/EdqsProcessor.java | 19 +++++++++++++ .../PartitionedQueueResponseTemplate.java | 28 +++++++++++++++++-- 3 files changed, 49 insertions(+), 5 deletions(-) diff --git a/common/cluster-api/src/main/java/org/thingsboard/server/queue/TbQueueHandler.java b/common/cluster-api/src/main/java/org/thingsboard/server/queue/TbQueueHandler.java index bb5bc1d668..523a97e13c 100644 --- a/common/cluster-api/src/main/java/org/thingsboard/server/queue/TbQueueHandler.java +++ b/common/cluster-api/src/main/java/org/thingsboard/server/queue/TbQueueHandler.java @@ -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 { ListenableFuture handle(Request request); + default Response constructErrorResponseMsg(Request request, Throwable cause) { + return null; + } + } diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/processor/EdqsProcessor.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/processor/EdqsProcessor.java index 0e74cb98fa..13a54973e6 100644 --- a/common/edqs/src/main/java/org/thingsboard/server/edqs/processor/EdqsProcessor.java +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/processor/EdqsProcessor.java @@ -203,6 +203,25 @@ public class EdqsProcessor implements TbQueueHandler, }); } + @Override + public TbProtoQueueMsg constructErrorResponseMsg(TbProtoQueueMsg 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 { diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/common/PartitionedQueueResponseTemplate.java b/common/queue/src/main/java/org/thingsboard/server/queue/common/PartitionedQueueResponseTemplate.java index 7e913009f0..a15fafa19a 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/common/PartitionedQueueResponseTemplate.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/common/PartitionedQueueResponseTemplate.java @@ -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 { 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, responseTopic, request, t); + stats.incrementFailed(); + } + }); }, e -> { pendingRequestCount.decrementAndGet(); @@ -144,6 +158,16 @@ public class PartitionedQueueResponseTemplate partitions) { requestConsumer.update(partitions); } From 060f728fcd90b639ac7b9efb2f358e0ce2f857c8 Mon Sep 17 00:00:00 2001 From: Yevhenii Date: Wed, 18 Jun 2025 19:47:18 +0300 Subject: [PATCH 19/54] CalculatedField functionality support for Edge - fixed pushEventToAllRelatedEdges - refactoring --- .../calculated/BaseCalculatedFieldProcessor.java | 4 ++-- .../calculated/CalculatedFieldEdgeProcessor.java | 8 ++++---- .../thingsboard/server/edge/AbstractEdgeTest.java | 11 +++++++++++ .../server/edge/CalculatedFieldEdgeTest.java | 12 ++++-------- 4 files changed, 21 insertions(+), 14 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/calculated/BaseCalculatedFieldProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/calculated/BaseCalculatedFieldProcessor.java index da3051045f..a7781fe55d 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/calculated/BaseCalculatedFieldProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/calculated/BaseCalculatedFieldProcessor.java @@ -88,10 +88,10 @@ public abstract class BaseCalculatedFieldProcessor extends BaseEdgeProcessor { return Pair.of(isCreated, isNameUpdated); } - protected ListenableFuture pushEventToAllRelatedEdges(TenantId tenantId, EntityId entityId, EdgeEventType type, EdgeEventActionType actionType, EdgeId sourceEdgeId) { + protected ListenableFuture pushEventToAllRelatedEdges(TenantId tenantId, EntityId calculatedFieldOwnerId, EntityId entityId, EdgeEventType type, EdgeEventActionType actionType, EdgeId sourceEdgeId) { List> futures = new ArrayList<>(); PageDataIterableByTenantIdEntityId edgeIds = - new PageDataIterableByTenantIdEntityId<>(edgeCtx.getEdgeService()::findRelatedEdgeIdsByEntityId, tenantId, entityId, RELATED_EDGES_CACHE_ITEMS); + new PageDataIterableByTenantIdEntityId<>(edgeCtx.getEdgeService()::findRelatedEdgeIdsByEntityId, tenantId, calculatedFieldOwnerId, RELATED_EDGES_CACHE_ITEMS); for (EdgeId relatedEdgeId : edgeIds) { if (!relatedEdgeId.equals(sourceEdgeId)) { futures.add(saveEdgeEvent(tenantId, relatedEdgeId, type, actionType, entityId, null)); diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/calculated/CalculatedFieldEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/calculated/CalculatedFieldEdgeProcessor.java index cf28003707..a0212bb4df 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/calculated/CalculatedFieldEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/calculated/CalculatedFieldEdgeProcessor.java @@ -126,15 +126,15 @@ public class CalculatedFieldEdgeProcessor extends BaseCalculatedFieldProcessor i switch (actionType) { case UPDATED: case ADDED: - EntityId bodyEntityId = JacksonUtil.fromString(edgeNotificationMsg.getBody(), EntityId.class); - if (bodyEntityId != null && - (EntityType.DEVICE.equals(bodyEntityId.getEntityType()) || EntityType.ASSET.equals(bodyEntityId.getEntityType()))) { + 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) : - pushEventToAllRelatedEdges(tenantId, entityId, type, actionType, originatorEdgeId); + pushEventToAllRelatedEdges(tenantId, calculatedFieldOwnerId, entityId, type, actionType, originatorEdgeId); } else { return pushEventToAllEdges(tenantId, type, actionType, entityId, originatorEdgeId); } diff --git a/application/src/test/java/org/thingsboard/server/edge/AbstractEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/AbstractEdgeTest.java index 24fa02f11a..84d879c993 100644 --- a/application/src/test/java/org/thingsboard/server/edge/AbstractEdgeTest.java +++ b/application/src/test/java/org/thingsboard/server/edge/AbstractEdgeTest.java @@ -75,6 +75,7 @@ import org.thingsboard.server.common.data.queue.Queue; import org.thingsboard.server.common.data.rule.RuleChain; import org.thingsboard.server.common.data.rule.RuleChainMetaData; import org.thingsboard.server.common.data.rule.RuleChainType; +import org.thingsboard.server.common.data.security.DeviceCredentials; import org.thingsboard.server.common.data.security.model.JwtSettings; import org.thingsboard.server.controller.AbstractControllerTest; import org.thingsboard.server.dao.edge.EdgeEventService; @@ -565,6 +566,7 @@ abstract public class AbstractEdgeTest extends AbstractControllerTest { protected Device saveDeviceOnCloudAndVerifyDeliveryToEdge() throws Exception { // create device and assign to edge Device savedDevice = saveDevice(StringUtils.randomAlphanumeric(15), thermostatDeviceProfile.getName()); + 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); @@ -582,6 +584,15 @@ abstract public class AbstractEdgeTest extends AbstractControllerTest { Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, deviceProfileUpdateMsg.getMsgType()); Assert.assertEquals(thermostatDeviceProfile.getUuidId().getMostSignificantBits(), deviceProfileUpdateMsg.getIdMSB()); Assert.assertEquals(thermostatDeviceProfile.getUuidId().getLeastSignificantBits(), deviceProfileUpdateMsg.getIdLSB()); + + Optional deviceCredentialsUpdateMsgOpt = edgeImitator.findMessageByType(DeviceCredentialsUpdateMsg.class); + Assert.assertTrue(deviceCredentialsUpdateMsgOpt.isPresent()); + DeviceCredentialsUpdateMsg deviceCredentialsUpdateMsg = deviceCredentialsUpdateMsgOpt.get(); + DeviceCredentials deviceCredentialsMsg = JacksonUtil.fromString(deviceCredentialsUpdateMsg.getEntity(), DeviceCredentials.class, true); + Assert.assertNotNull(deviceCredentialsMsg); + Assert.assertEquals(savedDevice.getId(), deviceCredentialsMsg.getDeviceId()); + Assert.assertEquals(deviceCredentials, deviceCredentialsMsg); + return savedDevice; } diff --git a/application/src/test/java/org/thingsboard/server/edge/CalculatedFieldEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/CalculatedFieldEdgeTest.java index da75225d11..5ec48daf63 100644 --- a/application/src/test/java/org/thingsboard/server/edge/CalculatedFieldEdgeTest.java +++ b/application/src/test/java/org/thingsboard/server/edge/CalculatedFieldEdgeTest.java @@ -58,16 +58,15 @@ public class CalculatedFieldEdgeTest extends AbstractEdgeTest { SimpleCalculatedFieldConfiguration config = new SimpleCalculatedFieldConfiguration(); CalculatedField calculatedField = createSimpleCalculatedField(savedDevice.getId(), config); - edgeImitator.expectMessageAmount(SYNC_MESSAGE_COUNT + 4); + edgeImitator.expectMessageAmount(1); CalculatedField savedCalculatedField = doPost("/api/calculatedField", calculatedField, CalculatedField.class); - doPost("/api/edge/sync/" + edge.getId()); Assert.assertTrue(edgeImitator.waitForMessages()); List downlinkMsgs = edgeImitator.getDownlinkMsgs(); AbstractMessage latestMessage = downlinkMsgs.stream().filter(downlinkMsg -> downlinkMsg instanceof CalculatedFieldUpdateMsg).findFirst().get(); Assert.assertTrue(latestMessage instanceof CalculatedFieldUpdateMsg); CalculatedFieldUpdateMsg calculatedFieldUpdateMsg = (CalculatedFieldUpdateMsg) latestMessage; - Assert.assertEquals(UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE, calculatedFieldUpdateMsg.getMsgType()); + 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); @@ -77,11 +76,9 @@ public class CalculatedFieldEdgeTest extends AbstractEdgeTest { Assert.assertEquals(savedDevice.getId(), calculatedFieldFromMsg.getEntityId()); Assert.assertEquals(config, calculatedFieldFromMsg.getConfiguration()); - // update calculatedField - edgeImitator.expectMessageAmount(SYNC_MESSAGE_COUNT + 4); + edgeImitator.expectMessageAmount(1); savedCalculatedField.setName(UPDATED_CF_NAME); savedCalculatedField = doPost("/api/calculatedField", savedCalculatedField, CalculatedField.class); - doPost("/api/edge/sync/" + edge.getId()); Assert.assertTrue(edgeImitator.waitForMessages()); downlinkMsgs = edgeImitator.getDownlinkMsgs(); latestMessage = downlinkMsgs.stream().filter(downlinkMsg -> downlinkMsg instanceof CalculatedFieldUpdateMsg).findFirst().get(); @@ -144,9 +141,8 @@ public class CalculatedFieldEdgeTest extends AbstractEdgeTest { SimpleCalculatedFieldConfiguration config = new SimpleCalculatedFieldConfiguration(); CalculatedField calculatedField = createSimpleCalculatedField(savedDevice.getId(), config); - edgeImitator.expectMessageAmount(SYNC_MESSAGE_COUNT + 4); + edgeImitator.expectMessageAmount(1); CalculatedField savedCalculatedField = doPost("/api/calculatedField", calculatedField, CalculatedField.class); - doPost("/api/edge/sync/" + edge.getId()); Assert.assertTrue(edgeImitator.waitForMessages()); UUID uuid = Uuids.timeBased(); From 6f42d5fb77ee9d4cba91671c40d135f00d4ad274 Mon Sep 17 00:00:00 2001 From: yevhenii Date: Thu, 19 Jun 2025 11:17:16 +0300 Subject: [PATCH 20/54] CalculatedField functionality support for Edge - refactoring test --- .../thingsboard/server/edge/CalculatedFieldEdgeTest.java | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/application/src/test/java/org/thingsboard/server/edge/CalculatedFieldEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/CalculatedFieldEdgeTest.java index 5ec48daf63..0eabba10f9 100644 --- a/application/src/test/java/org/thingsboard/server/edge/CalculatedFieldEdgeTest.java +++ b/application/src/test/java/org/thingsboard/server/edge/CalculatedFieldEdgeTest.java @@ -38,7 +38,6 @@ 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.List; import java.util.Map; import java.util.Optional; import java.util.UUID; @@ -62,8 +61,7 @@ public class CalculatedFieldEdgeTest extends AbstractEdgeTest { CalculatedField savedCalculatedField = doPost("/api/calculatedField", calculatedField, CalculatedField.class); Assert.assertTrue(edgeImitator.waitForMessages()); - List downlinkMsgs = edgeImitator.getDownlinkMsgs(); - AbstractMessage latestMessage = downlinkMsgs.stream().filter(downlinkMsg -> downlinkMsg instanceof CalculatedFieldUpdateMsg).findFirst().get(); + AbstractMessage latestMessage = edgeImitator.getLatestMessage(); Assert.assertTrue(latestMessage instanceof CalculatedFieldUpdateMsg); CalculatedFieldUpdateMsg calculatedFieldUpdateMsg = (CalculatedFieldUpdateMsg) latestMessage; Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, calculatedFieldUpdateMsg.getMsgType()); @@ -80,8 +78,8 @@ public class CalculatedFieldEdgeTest extends AbstractEdgeTest { savedCalculatedField.setName(UPDATED_CF_NAME); savedCalculatedField = doPost("/api/calculatedField", savedCalculatedField, CalculatedField.class); Assert.assertTrue(edgeImitator.waitForMessages()); - downlinkMsgs = edgeImitator.getDownlinkMsgs(); - latestMessage = downlinkMsgs.stream().filter(downlinkMsg -> downlinkMsg instanceof CalculatedFieldUpdateMsg).findFirst().get(); + + latestMessage = edgeImitator.getLatestMessage(); Assert.assertTrue(latestMessage instanceof CalculatedFieldUpdateMsg); calculatedFieldUpdateMsg = (CalculatedFieldUpdateMsg) latestMessage; calculatedFieldFromMsg = JacksonUtil.fromString(calculatedFieldUpdateMsg.getEntity(), CalculatedField.class, true); @@ -94,6 +92,7 @@ public class CalculatedFieldEdgeTest extends AbstractEdgeTest { doDelete("/api/calculatedField/" + savedCalculatedField.getUuidId()) .andExpect(status().isOk()); Assert.assertTrue(edgeImitator.waitForMessages()); + latestMessage = edgeImitator.getLatestMessage(); Assert.assertTrue(latestMessage instanceof CalculatedFieldUpdateMsg); calculatedFieldUpdateMsg = (CalculatedFieldUpdateMsg) latestMessage; From b747b6b1bfb6df426b278f14e418ea11779679ae Mon Sep 17 00:00:00 2001 From: yevhenii Date: Thu, 19 Jun 2025 13:37:06 +0300 Subject: [PATCH 21/54] CalculatedField functionality support for Edge - added new edge version --- common/edge-api/src/main/proto/edge.proto | 1 + 1 file changed, 1 insertion(+) diff --git a/common/edge-api/src/main/proto/edge.proto b/common/edge-api/src/main/proto/edge.proto index 904a938f1e..23eafc4574 100644 --- a/common/edge-api/src/main/proto/edge.proto +++ b/common/edge-api/src/main/proto/edge.proto @@ -42,6 +42,7 @@ enum EdgeVersion { V_3_8_0 = 8; V_3_9_0 = 9; V_4_0_0 = 10; + V_4_1_0 = 11; V_LATEST = 999; } From 7dc8f107408aa50c17bac9a244fe5ac8369a6c1c Mon Sep 17 00:00:00 2001 From: Andrii Landiak Date: Thu, 19 Jun 2025 17:09:08 +0300 Subject: [PATCH 22/54] Refactoring --- .../impl/DeviceProfileImportService.java | 10 +++++---- .../server/common/data/id/OtaPackageId.java | 2 ++ .../create/AutoVersionCreateConfig.java | 3 +++ .../server/dao/ota/BaseOtaPackageService.java | 22 ++++++++----------- 4 files changed, 20 insertions(+), 17 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/DeviceProfileImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/DeviceProfileImportService.java index c867286285..e498cf0911 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/DeviceProfileImportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/DeviceProfileImportService.java @@ -52,12 +52,14 @@ public class DeviceProfileImportService extends BaseEntityImportService 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); - saved.setFirmwareId(idProvider.getInternalId(deviceProfile.getFirmwareId())); - saved.setSoftwareId(idProvider.getInternalId(deviceProfile.getSoftwareId())); - saved = deviceProfileService.saveDeviceProfile(saved); } return saved; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/OtaPackageId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/OtaPackageId.java index 2a8efc5bfb..a246ab5f94 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/OtaPackageId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/OtaPackageId.java @@ -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 diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/request/create/AutoVersionCreateConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/request/create/AutoVersionCreateConfig.java index 8e61991b38..b5e2813134 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/request/create/AutoVersionCreateConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/request/create/AutoVersionCreateConfig.java @@ -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; diff --git a/dao/src/main/java/org/thingsboard/server/dao/ota/BaseOtaPackageService.java b/dao/src/main/java/org/thingsboard/server/dao/ota/BaseOtaPackageService.java index 83cb35d152..7aff945e84 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/ota/BaseOtaPackageService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/ota/BaseOtaPackageService.java @@ -74,7 +74,7 @@ public class BaseOtaPackageService extends AbstractCachedEntityService Date: Thu, 19 Jun 2025 17:15:19 +0300 Subject: [PATCH 23/54] Minor improvement of saveOtaPackageInfo --- .../org/thingsboard/server/dao/ota/BaseOtaPackageService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dao/src/main/java/org/thingsboard/server/dao/ota/BaseOtaPackageService.java b/dao/src/main/java/org/thingsboard/server/dao/ota/BaseOtaPackageService.java index 7aff945e84..343a2485ce 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/ota/BaseOtaPackageService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/ota/BaseOtaPackageService.java @@ -74,7 +74,7 @@ public class BaseOtaPackageService extends AbstractCachedEntityService Date: Fri, 20 Jun 2025 15:15:15 +0300 Subject: [PATCH 24/54] Fix ota save when saving url --- .../sync/ie/importing/impl/OtaPackageImportService.java | 5 +++++ .../dao/service/validator/OtaPackageDataValidator.java | 1 + 2 files changed, 6 insertions(+) diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/OtaPackageImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/OtaPackageImportService.java index 16b204411e..fd8a7822eb 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/OtaPackageImportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/OtaPackageImportService.java @@ -19,6 +19,7 @@ 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; @@ -60,6 +61,10 @@ public class OtaPackageImportService extends BaseEntityImportService Date: Fri, 20 Jun 2025 15:54:01 +0300 Subject: [PATCH 25/54] Fix error on import --- .../sync/ie/importing/impl/OtaPackageImportService.java | 2 +- .../java/org/thingsboard/server/common/data/OtaPackage.java | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/OtaPackageImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/OtaPackageImportService.java index fd8a7822eb..9cd6bb50c6 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/OtaPackageImportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/OtaPackageImportService.java @@ -63,7 +63,7 @@ public class OtaPackageImportService extends BaseEntityImportService Date: Wed, 18 Jun 2025 11:30:00 +0300 Subject: [PATCH 26/54] UI: Remove MQTT v5, MQTT v3.1 options for "Azure IoT Hub" rule node. --- .../rule-node/external/azure-iot-hub-config.component.html | 4 +++- .../rule-node/external/azure-iot-hub-config.component.ts | 2 ++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/ui-ngx/src/app/modules/home/components/rule-node/external/azure-iot-hub-config.component.html b/ui-ngx/src/app/modules/home/components/rule-node/external/azure-iot-hub-config.component.html index 79c4a2f00d..bab0c6b196 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/external/azure-iot-hub-config.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/external/azure-iot-hub-config.component.html @@ -38,7 +38,9 @@ {{ 'rule-node-config.device-id-required' | translate }} - + + diff --git a/ui-ngx/src/app/modules/home/components/rule-node/external/azure-iot-hub-config.component.ts b/ui-ngx/src/app/modules/home/components/rule-node/external/azure-iot-hub-config.component.ts index 7dd934ccd9..dff5f06e54 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/external/azure-iot-hub-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/rule-node/external/azure-iot-hub-config.component.ts @@ -22,6 +22,7 @@ import { azureIotHubCredentialsTypes, azureIotHubCredentialsTypeTranslations } from '@home/components/rule-node/rule-node-config.models'; +import { MqttVersion } from '@shared/models/mqtt.models'; @Component({ selector: 'tb-external-node-azure-iot-hub-config', @@ -34,6 +35,7 @@ export class AzureIotHubConfigComponent extends RuleNodeConfigurationComponent { allAzureIotHubCredentialsTypes = azureIotHubCredentialsTypes; azureIotHubCredentialsTypeTranslationsMap = azureIotHubCredentialsTypeTranslations; + MqttVersion = MqttVersion; constructor(private fb: UntypedFormBuilder) { super(); From ef385c4fa910a18c91138fb942cb439adfc27121 Mon Sep 17 00:00:00 2001 From: deaflynx Date: Fri, 6 Jun 2025 14:39:03 +0300 Subject: [PATCH 27/54] UI: Mqtt vesion add excludeVersion. --- .../shared/components/mqtt-version-select.component.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/ui-ngx/src/app/shared/components/mqtt-version-select.component.ts b/ui-ngx/src/app/shared/components/mqtt-version-select.component.ts index 6bde856a7b..5145a7fd51 100644 --- a/ui-ngx/src/app/shared/components/mqtt-version-select.component.ts +++ b/ui-ngx/src/app/shared/components/mqtt-version-select.component.ts @@ -41,7 +41,13 @@ export class MqttVersionSelectComponent implements ControlValueAccessor { @Input() appearance: MatFormFieldAppearance = 'fill'; - mqttVersions = Object.values(MqttVersion); + @Input() + excludeVersions: MqttVersion[]; + + get mqttVersions(): MqttVersion[] { + return Object.values(MqttVersion).filter(v => !this.excludeVersions || !this.excludeVersions.includes(v)); + } + mqttVersionTranslation = MqttVersionTranslation; modelValue: MqttVersion; From 050fa670af450c89ad6e84372004d558c6c85ffb Mon Sep 17 00:00:00 2001 From: deaflynx Date: Fri, 6 Jun 2025 17:37:51 +0300 Subject: [PATCH 28/54] UI: Minor improvement of excludeVersions in mqtt version select. --- .../mqtt-version-select.component.ts | 23 ++++++++++++++----- 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/ui-ngx/src/app/shared/components/mqtt-version-select.component.ts b/ui-ngx/src/app/shared/components/mqtt-version-select.component.ts index 5145a7fd51..0a9b75be2d 100644 --- a/ui-ngx/src/app/shared/components/mqtt-version-select.component.ts +++ b/ui-ngx/src/app/shared/components/mqtt-version-select.component.ts @@ -14,7 +14,7 @@ /// limitations under the License. /// -import { Component, forwardRef, Input } from '@angular/core'; +import { Component, forwardRef, Input, OnChanges, SimpleChanges } from '@angular/core'; import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; import { coerceBoolean } from '@shared/decorators/coercion'; import { SubscriptSizing, MatFormFieldAppearance } from '@angular/material/form-field'; @@ -30,7 +30,7 @@ import { MqttVersionTranslation, MqttVersion } from '@shared/models/mqtt.models' multi: true }] }) -export class MqttVersionSelectComponent implements ControlValueAccessor { +export class MqttVersionSelectComponent implements ControlValueAccessor, OnChanges { @Input() disabled: boolean; @@ -44,10 +44,7 @@ export class MqttVersionSelectComponent implements ControlValueAccessor { @Input() excludeVersions: MqttVersion[]; - get mqttVersions(): MqttVersion[] { - return Object.values(MqttVersion).filter(v => !this.excludeVersions || !this.excludeVersions.includes(v)); - } - + mqttVersions = Object.values(MqttVersion); mqttVersionTranslation = MqttVersionTranslation; modelValue: MqttVersion; @@ -60,6 +57,20 @@ export class MqttVersionSelectComponent implements ControlValueAccessor { constructor() { } + ngOnChanges(changes: SimpleChanges): void { + for (const propName of Object.keys(changes)) { + const change = changes[propName]; + if (propName === 'excludeVersions' && change.currentValue !== change.previousValue) { + const excludeVersions = change.currentValue; + if (excludeVersions?.length) { + this.mqttVersions = Object.values(MqttVersion).filter(v => !excludeVersions.includes(v)); + } else { + this.mqttVersions = Object.values(MqttVersion); + } + } + } + } + registerOnChange(fn: any): void { this.propagateChange = fn; } From 0be43296e1d94b680ca7ffcdd011da9535b2524a Mon Sep 17 00:00:00 2001 From: Volodymyr Babak Date: Fri, 20 Jun 2025 18:41:03 +0300 Subject: [PATCH 29/54] EdgeGrpcSession - improve logs on delivery failues --- .../thingsboard/server/service/edge/rpc/EdgeGrpcSession.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java index b6ecd848ad..94e0c155fe 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java @@ -452,14 +452,15 @@ public abstract class EdgeGrpcSession implements Closeable { List copy = new ArrayList<>(sessionState.getPendingMsgsMap().values()); if (attempt > 1) { String error = "Failed to deliver the batch"; - String failureMsg = String.format("{%s}: {%s}", error, copy); + String failureMsg = String.format("{%s} (size: {%s})", error, copy.size()); if (attempt == 2) { // Send a failure notification only on the second attempt. // This ensures that failure alerts are sent just once to avoid redundant notifications. ctx.getRuleProcessor().process(EdgeCommunicationFailureTrigger.builder().tenantId(tenantId) .edgeId(edge.getId()).customerId(edge.getCustomerId()).edgeName(edge.getName()).failureMsg(failureMsg).error(error).build()); } - log.warn("[{}][{}] {}, attempt: {}", tenantId, edge.getId(), failureMsg, attempt); + log.warn("[{}][{}] {} on attempt {}", tenantId, edge.getId(), failureMsg, attempt); + log.debug("[{}][{}] entities in failed batch: {}", tenantId, edge.getId(), copy); } log.trace("[{}][{}][{}] downlink msg(s) are going to be send.", tenantId, edge.getId(), copy.size()); for (DownlinkMsg downlinkMsg : copy) { From 7a81ec5a8b0aa5bc643ec7496bccb433473d3498 Mon Sep 17 00:00:00 2001 From: Andrii Landiak Date: Mon, 23 Jun 2025 11:45:41 +0300 Subject: [PATCH 30/54] Add name to OtaPackageInfo json --- .../org/thingsboard/server/common/data/OtaPackageInfo.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/OtaPackageInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/OtaPackageInfo.java index 2930ff41d1..9f010db823 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/OtaPackageInfo.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/OtaPackageInfo.java @@ -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; @@ -124,7 +125,7 @@ public class OtaPackageInfo extends BaseDataWithAdditionalInfo imp } @Override - @JsonIgnore + @JsonProperty(access = JsonProperty.Access.READ_ONLY) public String getName() { return title; } From eb1e3dfabfe725f8fb80bccff1b4d10f9348d983 Mon Sep 17 00:00:00 2001 From: Artem Dzhereleiko Date: Mon, 23 Jun 2025 12:28:46 +0300 Subject: [PATCH 31/54] UI: Version Control for OTA updates --- .../modules/home/pages/admin/admin.module.ts | 2 + .../resource-library-tabs.component.html | 23 ++++++++++ .../resource-library-tabs.component.ts | 36 +++++++++++++++ .../resources-library-table-config.resolve.ts | 2 + .../ota-update-table-config.resolve.ts | 2 + .../ota-update/ota-update-tabs.component.html | 23 ++++++++++ .../ota-update/ota-update-tabs.component.ts | 44 +++++++++++++++++++ .../pages/ota-update/ota-update.module.ts | 4 +- .../app/shared/models/entity-type.models.ts | 2 + .../app/shared/models/ota-package.models.ts | 4 +- ui-ngx/src/app/shared/models/vc.models.ts | 6 ++- .../assets/locale/locale.constant-en_US.json | 2 + 12 files changed, 145 insertions(+), 5 deletions(-) create mode 100644 ui-ngx/src/app/modules/home/pages/admin/resource/resource-library-tabs.component.html create mode 100644 ui-ngx/src/app/modules/home/pages/admin/resource/resource-library-tabs.component.ts create mode 100644 ui-ngx/src/app/modules/home/pages/ota-update/ota-update-tabs.component.html create mode 100644 ui-ngx/src/app/modules/home/pages/ota-update/ota-update-tabs.component.ts diff --git a/ui-ngx/src/app/modules/home/pages/admin/admin.module.ts b/ui-ngx/src/app/modules/home/pages/admin/admin.module.ts index a5f18122fd..10721ade5a 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/admin.module.ts +++ b/ui-ngx/src/app/modules/home/pages/admin/admin.module.ts @@ -38,6 +38,7 @@ import { JsLibraryTableHeaderComponent } from '@home/pages/admin/resource/js-lib import { JsResourceComponent } from '@home/pages/admin/resource/js-resource.component'; import { NgxFlowModule } from '@flowjs/ngx-flow'; import { TrendzSettingsComponent } from '@home/pages/admin/trendz-settings.component'; +import { ResourceLibraryTabsComponent } from '@home/pages/admin/resource/resource-library-tabs.component'; @NgModule({ declarations: @@ -50,6 +51,7 @@ import { TrendzSettingsComponent } from '@home/pages/admin/trendz-settings.compo HomeSettingsComponent, ResourcesLibraryComponent, ResourceTabsComponent, + ResourceLibraryTabsComponent, ResourcesTableHeaderComponent, JsResourceComponent, JsLibraryTableHeaderComponent, diff --git a/ui-ngx/src/app/modules/home/pages/admin/resource/resource-library-tabs.component.html b/ui-ngx/src/app/modules/home/pages/admin/resource/resource-library-tabs.component.html new file mode 100644 index 0000000000..4effdaad53 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/admin/resource/resource-library-tabs.component.html @@ -0,0 +1,23 @@ + + + + diff --git a/ui-ngx/src/app/modules/home/pages/admin/resource/resource-library-tabs.component.ts b/ui-ngx/src/app/modules/home/pages/admin/resource/resource-library-tabs.component.ts new file mode 100644 index 0000000000..a85bf48db3 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/admin/resource/resource-library-tabs.component.ts @@ -0,0 +1,36 @@ +/// +/// 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. +/// + +import { Component } from '@angular/core'; +import { EntityTabsComponent } from '@home/components/entity/entity-tabs.component'; +import { Resource } from '@shared/models/resource.models'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { NULL_UUID } from '@shared/models/id/has-uuid'; + +@Component({ + selector: 'tb-resource-library-tabs', + templateUrl: './resource-library-tabs.component.html', + styleUrls: [] +}) +export class ResourceLibraryTabsComponent extends EntityTabsComponent { + + readonly NULL_UUID = NULL_UUID; + + constructor(protected store: Store) { + super(store); + } +} diff --git a/ui-ngx/src/app/modules/home/pages/admin/resource/resources-library-table-config.resolve.ts b/ui-ngx/src/app/modules/home/pages/admin/resource/resources-library-table-config.resolve.ts index 91327b7465..f39ed9ff7b 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/resource/resources-library-table-config.resolve.ts +++ b/ui-ngx/src/app/modules/home/pages/admin/resource/resources-library-table-config.resolve.ts @@ -37,6 +37,7 @@ import { PageLink } from '@shared/models/page/page-link'; import { EntityAction } from '@home/models/entity/entity-component.models'; import { map } from 'rxjs/operators'; import { ResourcesTableHeaderComponent } from '@home/pages/admin/resource/resources-table-header.component'; +import { ResourceLibraryTabsComponent } from '@home/pages/admin/resource/resource-library-tabs.component'; @Injectable() export class ResourcesLibraryTableConfigResolver { @@ -55,6 +56,7 @@ export class ResourcesLibraryTableConfigResolver { this.config.entityTranslations = entityTypeTranslations.get(EntityType.TB_RESOURCE); this.config.entityResources = entityTypeResources.get(EntityType.TB_RESOURCE); this.config.headerComponent = ResourcesTableHeaderComponent; + this.config.entityTabsComponent = ResourceLibraryTabsComponent; this.config.entityTitle = (resource) => resource ? resource.title : ''; diff --git a/ui-ngx/src/app/modules/home/pages/ota-update/ota-update-table-config.resolve.ts b/ui-ngx/src/app/modules/home/pages/ota-update/ota-update-table-config.resolve.ts index cc3ca4046b..0dae778d03 100644 --- a/ui-ngx/src/app/modules/home/pages/ota-update/ota-update-table-config.resolve.ts +++ b/ui-ngx/src/app/modules/home/pages/ota-update/ota-update-table-config.resolve.ts @@ -36,6 +36,7 @@ import { PageLink } from '@shared/models/page/page-link'; import { OtaUpdateComponent } from '@home/pages/ota-update/ota-update.component'; import { EntityAction } from '@home/models/entity/entity-component.models'; import { FileSizePipe } from '@shared/pipe/file-size.pipe'; +import { OtaUpdateTabsComponent } from '@home/pages/ota-update/ota-update-tabs.component'; @Injectable() export class OtaUpdateTableConfigResolve { @@ -50,6 +51,7 @@ export class OtaUpdateTableConfigResolve { private fileSize: FileSizePipe) { this.config.entityType = EntityType.OTA_PACKAGE; this.config.entityComponent = OtaUpdateComponent; + this.config.entityTabsComponent = OtaUpdateTabsComponent; this.config.entityTranslations = entityTypeTranslations.get(EntityType.OTA_PACKAGE); this.config.entityResources = entityTypeResources.get(EntityType.OTA_PACKAGE); diff --git a/ui-ngx/src/app/modules/home/pages/ota-update/ota-update-tabs.component.html b/ui-ngx/src/app/modules/home/pages/ota-update/ota-update-tabs.component.html new file mode 100644 index 0000000000..a8cdae4256 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/ota-update/ota-update-tabs.component.html @@ -0,0 +1,23 @@ + + + + diff --git a/ui-ngx/src/app/modules/home/pages/ota-update/ota-update-tabs.component.ts b/ui-ngx/src/app/modules/home/pages/ota-update/ota-update-tabs.component.ts new file mode 100644 index 0000000000..1de3031ccb --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/ota-update/ota-update-tabs.component.ts @@ -0,0 +1,44 @@ +/// +/// 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. +/// + +import { Component } from '@angular/core'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { EntityTabsComponent } from '../../components/entity/entity-tabs.component'; +import { WidgetsBundle } from '@shared/models/widgets-bundle.model'; +import { NULL_UUID } from '@shared/models/id/has-uuid'; +import { OtaPackage } from '@shared/models/ota-package.models'; + +@Component({ + selector: 'tb-ota-update-tabs', + templateUrl: './ota-update-tabs.component.html', + styleUrls: [] +}) +export class OtaUpdateTabsComponent extends EntityTabsComponent { + + constructor(protected store: Store) { + super(store); + } + + isTenantOtaUpdate() { + return this.entity && this.entity.tenantId.id !== NULL_UUID; + } + + ngOnInit() { + super.ngOnInit(); + } + +} diff --git a/ui-ngx/src/app/modules/home/pages/ota-update/ota-update.module.ts b/ui-ngx/src/app/modules/home/pages/ota-update/ota-update.module.ts index 139591f14f..fe24da31b2 100644 --- a/ui-ngx/src/app/modules/home/pages/ota-update/ota-update.module.ts +++ b/ui-ngx/src/app/modules/home/pages/ota-update/ota-update.module.ts @@ -20,10 +20,12 @@ import { SharedModule } from '@shared/shared.module'; import { HomeComponentsModule } from '@home/components/home-components.module'; import { OtaUpdateRoutingModule } from '@home/pages/ota-update/ota-update-routing.module'; import { OtaUpdateComponent } from '@home/pages/ota-update/ota-update.component'; +import { OtaUpdateTabsComponent } from '@home/pages/ota-update/ota-update-tabs.component'; @NgModule({ declarations: [ - OtaUpdateComponent + OtaUpdateComponent, + OtaUpdateTabsComponent ], imports: [ CommonModule, diff --git a/ui-ngx/src/app/shared/models/entity-type.models.ts b/ui-ngx/src/app/shared/models/entity-type.models.ts index 48cad42e7b..60e2956e7c 100644 --- a/ui-ngx/src/app/shared/models/entity-type.models.ts +++ b/ui-ngx/src/app/shared/models/entity-type.models.ts @@ -346,6 +346,8 @@ export const entityTypeTranslations = new Map, HasTenantId { +export interface OtaPackageInfo extends BaseData, HasTenantId, ExportableEntity { tenantId?: TenantId; type: OtaUpdateType; deviceProfileId?: DeviceProfileId; diff --git a/ui-ngx/src/app/shared/models/vc.models.ts b/ui-ngx/src/app/shared/models/vc.models.ts index 3795518ffc..ebd7840f61 100644 --- a/ui-ngx/src/app/shared/models/vc.models.ts +++ b/ui-ngx/src/app/shared/models/vc.models.ts @@ -33,16 +33,18 @@ export const exportableEntityTypes: Array = [ EntityType.WIDGET_TYPE, EntityType.WIDGETS_BUNDLE, EntityType.TB_RESOURCE, + EntityType.OTA_PACKAGE, EntityType.NOTIFICATION_TEMPLATE, EntityType.NOTIFICATION_TARGET, EntityType.NOTIFICATION_RULE ]; -export const entityTypesWithoutRelatedData: Set = new Set([ +export const entityTypesWithoutRelatedData = new Set([ EntityType.NOTIFICATION_TEMPLATE, EntityType.NOTIFICATION_TARGET, EntityType.NOTIFICATION_RULE, - EntityType.TB_RESOURCE + EntityType.TB_RESOURCE, + EntityType.OTA_PACKAGE, ]); export interface VersionCreateConfig { diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index 55e136e031..a63089bb6c 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -2584,6 +2584,8 @@ "type-tb-resources": "Resources", "list-of-tb-resources": "{ count, plural, =1 {One resource} other {List of # resources} }", "type-ota-package": "OTA package", + "type-ota-packages": "OTA packages", + "list-of-ota-packages": "{ count, plural, =1 {One OTA package} other {List of # OTA packages} }", "type-rpc": "RPC", "type-queue": "Queue", "type-queue-stats": "Queue statistics", From 6014eed8525fb6469c71f96fa5aa4945807aad4a Mon Sep 17 00:00:00 2001 From: Volodymyr Babak Date: Mon, 23 Jun 2025 16:10:03 +0300 Subject: [PATCH 32/54] Improved Kafka Edge Session destroy logic - added retry attempts to avoid unclosed consumers --- .../service/edge/rpc/EdgeGrpcService.java | 21 ++++++++++++++----- .../service/edge/rpc/EdgeGrpcSession.java | 4 +++- .../edge/rpc/KafkaEdgeGrpcSession.java | 14 +++++++++---- 3 files changed, 29 insertions(+), 10 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java index 5671ffb2ab..eaef1f7c7d 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java @@ -94,6 +94,8 @@ import static org.thingsboard.server.service.state.DefaultDeviceStateService.LAS @TbCoreComponent public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase implements EdgeRpcService { + private static final int DESTROY_SESSION_MAX_ATTEMPTS = 10; + private final ConcurrentMap sessions = new ConcurrentHashMap<>(); private final ConcurrentMap sessionNewEventsLocks = new ConcurrentHashMap<>(); private final Map sessionNewEvents = new HashMap<>(); @@ -283,9 +285,8 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i EdgeGrpcSession session = sessions.get(edgeId); if (session != null && session.isConnected()) { log.info("[{}] Closing and removing session for edge [{}]", tenantId, edgeId); - session.destroy(); + destroySession(session); session.cleanUp(); - session.close(); sessions.remove(edgeId); final Lock newEventLock = sessionNewEventsLocks.computeIfAbsent(edgeId, id -> new ReentrantLock()); newEventLock.lock(); @@ -521,7 +522,15 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i private void destroySession(EdgeGrpcSession session) { try (session) { - session.destroy(); + for (int i = 0; i < DESTROY_SESSION_MAX_ATTEMPTS; i++) { + if (session.destroy()) { + break; + } else { + try { + Thread.sleep(100); + } catch (InterruptedException ignored) {} + } + } } } @@ -643,9 +652,11 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i } for (EdgeId edgeId : toRemove) { log.info("[{}] Destroying session for edge because edge is not connected", edgeId); - EdgeGrpcSession removed = sessions.remove(edgeId); + EdgeGrpcSession removed = sessions.get(edgeId); if (removed instanceof KafkaEdgeGrpcSession kafkaSession) { - kafkaSession.destroy(); + if (kafkaSession.destroy()) { + sessions.remove(edgeId); + } } } } catch (Exception e) { diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java index 94e0c155fe..c658bcf403 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java @@ -918,7 +918,9 @@ public abstract class EdgeGrpcSession implements Closeable { return Futures.allAsList(result); } - protected void destroy() {} + protected boolean destroy() { + return true; + } protected void cleanUp() {} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/KafkaEdgeGrpcSession.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/KafkaEdgeGrpcSession.java index daffe9db11..ab0b42abb4 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/KafkaEdgeGrpcSession.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/KafkaEdgeGrpcSession.java @@ -135,19 +135,25 @@ public class KafkaEdgeGrpcSession extends EdgeGrpcSession { } @Override - public void destroy() { + public boolean destroy() { try { if (consumer != null) { consumer.stop(); } - } finally { - consumer = null; + } catch (Exception e) { + log.warn("[{}][{}] Failed to stop edge event consumer", tenantId, edge.getId(), e); + return false; } + consumer = null; try { if (consumerExecutor != null) { consumerExecutor.shutdown(); } - } catch (Exception ignored) {} + } catch (Exception e) { + log.warn("[{}][{}] Failed to shutdown consumer executor", tenantId, edge.getId(), e); + return false; + } + return true; } @Override From ac6508986c38e3c55fc65a0eec548474c2b32dec Mon Sep 17 00:00:00 2001 From: Artem Dzhereleiko Date: Mon, 23 Jun 2025 17:28:16 +0300 Subject: [PATCH 33/54] UI: Clear code and omit label --- .../home/pages/ota-update/ota-update-tabs.component.ts | 4 ---- ui-ngx/src/app/shared/models/ota-package.models.ts | 2 +- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/ui-ngx/src/app/modules/home/pages/ota-update/ota-update-tabs.component.ts b/ui-ngx/src/app/modules/home/pages/ota-update/ota-update-tabs.component.ts index 1de3031ccb..44d17f3c11 100644 --- a/ui-ngx/src/app/modules/home/pages/ota-update/ota-update-tabs.component.ts +++ b/ui-ngx/src/app/modules/home/pages/ota-update/ota-update-tabs.component.ts @@ -37,8 +37,4 @@ export class OtaUpdateTabsComponent extends EntityTabsComponent { return this.entity && this.entity.tenantId.id !== NULL_UUID; } - ngOnInit() { - super.ngOnInit(); - } - } diff --git a/ui-ngx/src/app/shared/models/ota-package.models.ts b/ui-ngx/src/app/shared/models/ota-package.models.ts index 234886063e..bba7ea3d04 100644 --- a/ui-ngx/src/app/shared/models/ota-package.models.ts +++ b/ui-ngx/src/app/shared/models/ota-package.models.ts @@ -86,7 +86,7 @@ export interface OtaPagesIds { softwareId?: OtaPackageId; } -export interface OtaPackageInfo extends BaseData, HasTenantId, ExportableEntity { +export interface OtaPackageInfo extends Omit, 'label'>, HasTenantId, ExportableEntity { tenantId?: TenantId; type: OtaUpdateType; deviceProfileId?: DeviceProfileId; From 5949fa1ac81a24a6c1acdf2db0a1f3e7c2975f3e Mon Sep 17 00:00:00 2001 From: Andrii Landiak Date: Mon, 23 Jun 2025 18:01:26 +0300 Subject: [PATCH 34/54] Small refactoring of OtaPackageController --- .../controller/OtaPackageController.java | 35 +++++++------------ .../ota/DefaultTbOtaPackageService.java | 1 + 2 files changed, 14 insertions(+), 22 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/controller/OtaPackageController.java b/application/src/main/java/org/thingsboard/server/controller/OtaPackageController.java index 43ebf89b41..d4f932a643 100644 --- a/application/src/main/java/org/thingsboard/server/controller/OtaPackageController.java +++ b/application/src/main/java/org/thingsboard/server/controller/OtaPackageController.java @@ -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 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 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 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); diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/ota/DefaultTbOtaPackageService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/ota/DefaultTbOtaPackageService.java index af8bbeb669..2d597f7053 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/ota/DefaultTbOtaPackageService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/ota/DefaultTbOtaPackageService.java @@ -110,4 +110,5 @@ public class DefaultTbOtaPackageService extends AbstractTbEntityService implemen throw e; } } + } From 815ac9fef2d42a20f2c889c89b72df7bc895443b Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Tue, 24 Jun 2025 11:11:02 +0300 Subject: [PATCH 35/54] Fix invalid resource info caching --- .../controller/TbResourceControllerTest.java | 58 +++++++++++++++++++ .../resourceInfo/ResourceInfoCacheKey.java | 4 +- .../dao/resource/BaseResourceService.java | 6 +- 3 files changed, 62 insertions(+), 6 deletions(-) diff --git a/application/src/test/java/org/thingsboard/server/controller/TbResourceControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/TbResourceControllerTest.java index 569ff840ca..3b194de7b4 100644 --- a/application/src/test/java/org/thingsboard/server/controller/TbResourceControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/TbResourceControllerTest.java @@ -202,6 +202,60 @@ public class TbResourceControllerTest extends AbstractControllerTest { Assert.assertEquals(savedResource.getFileName(), foundResource.getFileName()); } + @Test + public void testFindSystemResourceInfoById() throws Exception { + loginSysAdmin(); + TbResource resource = new TbResource(); + resource.setResourceType(ResourceType.JS_MODULE); + resource.setTitle("My system resource"); + resource.setFileName(DEFAULT_FILE_NAME); + resource.setEncodedData(TEST_DATA); + TbResourceInfo savedResourceInfo = save(resource); + assertThat(savedResourceInfo.getFileName()).isEqualTo(DEFAULT_FILE_NAME); + + TbResourceInfo resourceInfo = findResourceInfo(savedResourceInfo.getId()); + assertThat(resourceInfo).isEqualTo(savedResourceInfo); + loginTenantAdmin(); + resourceInfo = findResourceInfo(savedResourceInfo.getId()); + assertThat(resourceInfo).isEqualTo(savedResourceInfo); + + loginSysAdmin(); + resource = new TbResource(savedResourceInfo); + resource.setFileName(DEFAULT_FILE_NAME_2); + resource.setEncodedData(TEST_DATA); + savedResourceInfo = save(resource); + assertThat(savedResourceInfo.getFileName()).isEqualTo(DEFAULT_FILE_NAME_2); + + resourceInfo = findResourceInfo(savedResourceInfo.getId()); + assertThat(resourceInfo).isEqualTo(savedResourceInfo); + loginTenantAdmin(); + resourceInfo = findResourceInfo(savedResourceInfo.getId()); + assertThat(resourceInfo).isEqualTo(savedResourceInfo); + } + + @Test + public void testFindTenantResourceInfoById() throws Exception { + TbResource resource = new TbResource(); + resource.setResourceType(ResourceType.JS_MODULE); + resource.setTitle("My tenant resource"); + resource.setFileName(DEFAULT_FILE_NAME); + resource.setEncodedData(TEST_DATA); + TbResourceInfo savedResourceInfo = save(resource); + assertThat(savedResourceInfo.getFileName()).isEqualTo(DEFAULT_FILE_NAME); + + TbResourceInfo resourceInfo = findResourceInfo(savedResourceInfo.getId()); + assertThat(resourceInfo).isEqualTo(savedResourceInfo); + + resource = new TbResource(savedResourceInfo); + resource.setFileName(DEFAULT_FILE_NAME_2); + resource.setEncodedData(TEST_DATA); + savedResourceInfo = save(resource); + assertThat(savedResourceInfo.getFileName()).isEqualTo(DEFAULT_FILE_NAME_2); + + resourceInfo = findResourceInfo(savedResourceInfo.getId()); + assertThat(resourceInfo).isEqualTo(savedResourceInfo); + } + @Test public void testDeleteTbResource() throws Exception { TbResource resource = new TbResource(); @@ -878,6 +932,10 @@ public class TbResourceControllerTest extends AbstractControllerTest { }); } + private TbResourceInfo findResourceInfo(TbResourceId id) throws Exception { + return doGet("/api/resource/info/" + id, TbResourceInfo.class); + } + private byte[] download(TbResourceId resourceId) throws Exception { return doGet("/api/resource/" + resourceId + "/download") .andExpect(status().isOk()) diff --git a/common/cache/src/main/java/org/thingsboard/server/cache/resourceInfo/ResourceInfoCacheKey.java b/common/cache/src/main/java/org/thingsboard/server/cache/resourceInfo/ResourceInfoCacheKey.java index cb19b36059..e1877ebe4e 100644 --- a/common/cache/src/main/java/org/thingsboard/server/cache/resourceInfo/ResourceInfoCacheKey.java +++ b/common/cache/src/main/java/org/thingsboard/server/cache/resourceInfo/ResourceInfoCacheKey.java @@ -20,7 +20,6 @@ import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.RequiredArgsConstructor; import org.thingsboard.server.common.data.id.TbResourceId; -import org.thingsboard.server.common.data.id.TenantId; import java.io.Serial; import java.io.Serializable; @@ -34,12 +33,11 @@ public class ResourceInfoCacheKey implements Serializable { @Serial private static final long serialVersionUID = 2100510964692846992L; - private final TenantId tenantId; private final TbResourceId tbResourceId; @Override public String toString() { - return tenantId + "_" + tbResourceId; + return tbResourceId.toString(); } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/resource/BaseResourceService.java b/dao/src/main/java/org/thingsboard/server/dao/resource/BaseResourceService.java index bf73c59708..bf941256f5 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/resource/BaseResourceService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/resource/BaseResourceService.java @@ -263,7 +263,7 @@ public class BaseResourceService extends AbstractCachedEntityService INCORRECT_RESOURCE_ID + id); - return cache.getAndPutInTransaction(new ResourceInfoCacheKey(tenantId, resourceId), + return cache.getAndPutInTransaction(new ResourceInfoCacheKey(resourceId), () -> resourceInfoDao.findById(tenantId, resourceId.getId()), true); } @@ -712,7 +712,7 @@ public class BaseResourceService extends AbstractCachedEntityService Date: Tue, 24 Jun 2025 11:55:03 +0300 Subject: [PATCH 36/54] CalculatedField functionality support for Edge - changed logic for sync CalculatedField --- .../service/edge/rpc/EdgeGrpcSession.java | 6 + .../edge/rpc/processor/BaseEdgeProcessor.java | 11 -- .../processor/asset/AssetEdgeProcessor.java | 2 - .../BaseCalculatedFieldProcessor.java | 2 +- .../processor/device/DeviceEdgeProcessor.java | 2 - .../rpc/sync/DefaultEdgeRequestsService.java | 139 +++++++++++++++++- .../edge/rpc/sync/EdgeRequestsService.java | 4 + .../server/dao/cf/CalculatedFieldService.java | 2 - common/edge-api/src/main/proto/edge.proto | 7 + .../dao/cf/BaseCalculatedFieldService.java | 13 +- .../server/dao/cf/CalculatedFieldDao.java | 3 +- .../dao/sql/cf/JpaCalculatedFieldDao.java | 6 +- 12 files changed, 160 insertions(+), 37 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java index 7430062bd7..3339ebb60e 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java @@ -49,6 +49,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.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; @@ -882,6 +883,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)); diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessor.java index 077ff21d26..6fcb02e4bc 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessor.java @@ -25,7 +25,6 @@ import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.AttributeScope; 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; @@ -54,13 +53,11 @@ import org.thingsboard.server.common.msg.TbMsgDataType; import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.dao.edge.EdgeSynchronizationManager; import org.thingsboard.server.dao.entity.EntityDaoRegistry; -import org.thingsboard.server.gen.edge.v1.CalculatedFieldUpdateMsg; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.queue.TbQueueCallback; import org.thingsboard.server.queue.TbQueueMsgMetadata; import org.thingsboard.server.service.edge.EdgeContextComponent; -import org.thingsboard.server.service.edge.EdgeMsgConstructorUtils; import org.thingsboard.server.service.executors.DbCallbackExecutorService; import org.thingsboard.server.service.state.DefaultDeviceStateService; @@ -384,12 +381,4 @@ public abstract class BaseEdgeProcessor implements EdgeProcessor { }); } - protected List getCalculatedFieldUpdateMsgs(TenantId tenantId, EntityId entityId) { - List calculatedFields = edgeCtx.getCalculatedFieldService().findCalculatedFieldsByEntityId(tenantId, entityId); - - return calculatedFields.stream() - .map(calculatedField -> EdgeMsgConstructorUtils.constructCalculatedFieldUpdatedMsg(UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE, calculatedField)) - .toList(); - } - } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/AssetEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/AssetEdgeProcessor.java index 538a90f7d7..cba2b62af1 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/AssetEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/AssetEdgeProcessor.java @@ -120,8 +120,6 @@ public class AssetEdgeProcessor extends BaseAssetProcessor implements AssetProce .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) .addAssetUpdateMsg(assetUpdateMsg); - getCalculatedFieldUpdateMsgs(edgeEvent.getTenantId(), assetId).forEach(builder::addCalculatedFieldUpdateMsg); - if (UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE.equals(msgType)) { AssetProfile assetProfile = edgeCtx.getAssetProfileService().findAssetProfileById(edgeEvent.getTenantId(), asset.getAssetProfileId()); builder.addAssetProfileUpdateMsg(EdgeMsgConstructorUtils.constructAssetProfileUpdatedMsg(msgType, assetProfile)); diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/calculated/BaseCalculatedFieldProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/calculated/BaseCalculatedFieldProcessor.java index a7781fe55d..503e5d74dc 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/calculated/BaseCalculatedFieldProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/calculated/BaseCalculatedFieldProcessor.java @@ -80,7 +80,7 @@ public abstract class BaseCalculatedFieldProcessor extends BaseEdgeProcessor { calculatedField.setId(calculatedFieldId); } - edgeCtx.getCalculatedFieldService().save(calculatedField, false); + edgeCtx.getCalculatedFieldService().save(calculatedField); } catch (Exception e) { log.error("[{}] Failed to process calculatedField update msg [{}]", tenantId, calculatedFieldUpdateMsg, e); throw e; diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/DeviceEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/DeviceEdgeProcessor.java index be2a04626c..763821f11c 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/DeviceEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/DeviceEdgeProcessor.java @@ -244,8 +244,6 @@ public class DeviceEdgeProcessor extends BaseDeviceProcessor implements DevicePr builder.addDeviceCredentialsUpdateMsg(deviceCredentialsUpdateMsg).build(); } - getCalculatedFieldUpdateMsgs(edgeEvent.getTenantId(), deviceId).forEach(builder::addCalculatedFieldUpdateMsg); - if (UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE.equals(msgType)) { DeviceProfile deviceProfile = edgeCtx.getDeviceProfileService().findDeviceProfileById(edgeEvent.getTenantId(), device.getDeviceProfileId()); builder.addDeviceProfileUpdateMsg(EdgeMsgConstructorUtils.constructDeviceProfileUpdatedMsg(msgType, deviceProfile)); diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/sync/DefaultEdgeRequestsService.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/sync/DefaultEdgeRequestsService.java index 6fc6d5bad9..4487a0acf8 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/sync/DefaultEdgeRequestsService.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/sync/DefaultEdgeRequestsService.java @@ -23,6 +23,7 @@ import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.SettableFuture; import lombok.extern.slf4j.Slf4j; import org.checkerframework.checker.nullness.qual.Nullable; +import org.jetbrains.annotations.NotNull; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; @@ -31,6 +32,7 @@ import org.thingsboard.server.common.data.AttributeScope; import org.thingsboard.server.common.data.EdgeUtils; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityView; +import org.thingsboard.server.common.data.ExportableEntity; import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.edge.EdgeEvent; import org.thingsboard.server.common.data.edge.EdgeEventActionType; @@ -46,6 +48,8 @@ import org.thingsboard.server.common.data.id.WidgetsBundleId; import org.thingsboard.server.common.data.kv.AttributeKvEntry; import org.thingsboard.server.common.data.kv.DataType; import org.thingsboard.server.common.data.kv.TsKvEntry; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.EntityRelationsQuery; import org.thingsboard.server.common.data.relation.EntitySearchDirection; @@ -53,13 +57,19 @@ import org.thingsboard.server.common.data.relation.RelationTypeGroup; 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.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.device.DeviceProfileService; +import org.thingsboard.server.dao.device.DeviceService; 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; @@ -72,10 +82,16 @@ import org.thingsboard.server.service.executors.DbCallbackExecutorService; import org.thingsboard.server.service.state.DefaultDeviceStateService; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Objects; +import java.util.Optional; import java.util.UUID; +import java.util.function.Function; +import java.util.stream.Collectors; @Service @TbCoreComponent @@ -90,7 +106,7 @@ public class DefaultEdgeRequestsService implements EdgeRequestsService { @Autowired private TimeseriesService timeseriesService; - + @Autowired private RelationService relationService; @@ -104,6 +120,17 @@ public class DefaultEdgeRequestsService implements EdgeRequestsService { @Autowired private WidgetTypeService widgetTypeService; + @Autowired + private CalculatedFieldService calculatedFieldService; + @Autowired + private DeviceService deviceService; + @Autowired + private DeviceProfileService deviceProfileService; + @Autowired + private AssetService assetService; + @Autowired + private AssetProfileService assetProfileService; + @Autowired private DbCallbackExecutorService dbCallbackExecutorService; @@ -293,6 +320,116 @@ public class DefaultEdgeRequestsService implements EdgeRequestsService { return futureToSet; } + @Override + public ListenableFuture processCalculatedFieldRequestMsg(TenantId tenantId, Edge edge, CalculatedFieldRequestMsg calculatedFieldRequestMsg) { + log.trace("[{}] processCalculatedFieldRequestMsg [{}][{}]", tenantId, edge.getName(), calculatedFieldRequestMsg); + + EntityId entityId = EntityIdFactory.getByTypeAndUuid( + EntityType.valueOf(calculatedFieldRequestMsg.getEntityType()), + new UUID(calculatedFieldRequestMsg.getEntityIdMSB(), calculatedFieldRequestMsg.getEntityIdLSB())); + + if (entityId.getEntityType() == EntityType.EDGE) { + log.trace("[{}] processAllCalculatedField [{}][{}]", tenantId, edge.getName(), calculatedFieldRequestMsg); + return syncAllCalculatedFieldForEdge(tenantId, edge, calculatedFieldRequestMsg); + } else { + log.trace("[{}] processCalculatedField [{}][{}] for entity [{}][{}]", tenantId, edge.getName(), calculatedFieldRequestMsg, entityId.getEntityType(), entityId.getId()); + return saveCalculatedFieldsToEdge(tenantId, edge.getId(), entityId); + } + } + + @NotNull + private ListenableFuture syncAllCalculatedFieldForEdge(TenantId tenantId, Edge edge, CalculatedFieldRequestMsg calculatedFieldRequestMsg) { + EdgeId edgeId = edge.getId(); + ListenableFuture> deviceIdsFuture = + findAllEntityIdsAsync(pageLink -> deviceService.findDevicesByTenantIdAndEdgeId(tenantId, edgeId, pageLink)); + ListenableFuture> assetIdsFuture = + findAllEntityIdsAsync(pageLink -> assetService.findAssetsByTenantIdAndEdgeId(tenantId, edgeId, pageLink)); + ListenableFuture> deviceProfileIdsFuture = + findAllEntityIdsAsync(pageLink -> deviceProfileService.findDeviceProfiles(tenantId, pageLink)); + ListenableFuture> assetProfileIdsFuture = + findAllEntityIdsAsync(pageLink -> assetProfileService.findAssetProfiles(tenantId, pageLink)); + + ListenableFuture> allEntityIdFuture = + mergeEntityIdFutures(deviceIdsFuture, assetIdsFuture, deviceProfileIdsFuture, assetProfileIdsFuture); + + return Futures.transformAsync(allEntityIdFuture, allIds -> { + log.trace("[{}][{}] Going to sync calculatedFields for [{}] entities", tenantId, edge.getName(), allIds.size()); + List> saveFutures = allIds.stream() + .map(id -> saveCalculatedFieldsToEdge(tenantId, edge.getId(), id)) + .collect(Collectors.toList()); + + return Futures.transform( + Futures.allAsList(saveFutures), + result -> null, + dbCallbackExecutorService + ); + }, dbCallbackExecutorService); + } + + private > ListenableFuture> findAllEntityIdsAsync(Function> fetcher) { + return dbCallbackExecutorService.submit(() -> { + List result = new ArrayList<>(); + PageLink pageLink = new PageLink(100); + PageData pageData; + while (true) { + pageData = fetcher.apply(pageLink); + + Optional.ofNullable(pageData) + .map(PageData::getData) + .ifPresent(dataList -> dataList.stream() + .filter(Objects::nonNull) + .map(ExportableEntity::getId) + .forEach(result::add)); + + if (pageData == null || !pageData.hasNext()) { + break; + } + + pageLink = pageLink.nextPageLink(); + } + return result; + }); + } + + @SafeVarargs + private ListenableFuture> mergeEntityIdFutures(ListenableFuture>... futures) { + return Futures.transform( + Futures.allAsList(Arrays.asList(futures)), + listsOfIds -> listsOfIds.stream() + .filter(Objects::nonNull) + .flatMap(Collection::stream) + .collect(Collectors.toList()), + dbCallbackExecutorService + ); + } + + private ListenableFuture saveCalculatedFieldsToEdge(TenantId tenantId, EdgeId edgeId, EntityId entityId) { + return Futures.transformAsync( + dbCallbackExecutorService.submit(() -> calculatedFieldService.findCalculatedFieldsByEntityId(tenantId, entityId)), + calculatedFields -> { + log.trace("[{}][{}][{}][{}] calculatedField(s) are going to be pushed to edge.", tenantId, edgeId, entityId, calculatedFields.size()); + + List> futures = calculatedFields.stream().map(calculatedField -> { + try { + return saveEdgeEvent(tenantId, edgeId, EdgeEventType.CALCULATED_FIELD, + EdgeEventActionType.ADDED, calculatedField.getId(), JacksonUtil.valueToTree(calculatedField)); + } catch (Exception e) { + String errMsg = String.format("[%s][%s] Exception during loading calculatedField [%s] to edge on sync!", tenantId, edgeId, calculatedField); + log.error(errMsg, e); + return Futures.immediateFailedFuture(e); + } + }).collect(Collectors.toList()); + + return Futures.transform( + Futures.allAsList(futures), + voids -> null, + dbCallbackExecutorService + ); + }, + dbCallbackExecutorService + ); + } + private ListenableFuture> findRelationByQuery(TenantId tenantId, Edge edge, EntityId entityId, EntitySearchDirection direction) { EntityRelationsQuery query = new EntityRelationsQuery(); query.setParameters(new RelationsSearchParameters(entityId, direction, 1, false)); diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/sync/EdgeRequestsService.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/sync/EdgeRequestsService.java index 2a115eeace..a147a7054b 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/sync/EdgeRequestsService.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/sync/EdgeRequestsService.java @@ -19,6 +19,7 @@ import com.google.common.util.concurrent.ListenableFuture; import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.gen.edge.v1.AttributesRequestMsg; +import org.thingsboard.server.gen.edge.v1.CalculatedFieldRequestMsg; import org.thingsboard.server.gen.edge.v1.DeviceCredentialsRequestMsg; import org.thingsboard.server.gen.edge.v1.EntityViewsRequestMsg; import org.thingsboard.server.gen.edge.v1.RelationRequestMsg; @@ -35,6 +36,8 @@ public interface EdgeRequestsService { ListenableFuture processRelationRequestMsg(TenantId tenantId, Edge edge, RelationRequestMsg relationRequestMsg); + ListenableFuture processCalculatedFieldRequestMsg(TenantId tenantId, Edge edge, CalculatedFieldRequestMsg calculatedFieldRequestMsg); + @Deprecated(since = "3.9.1", forRemoval = true) ListenableFuture processDeviceCredentialsRequestMsg(TenantId tenantId, Edge edge, DeviceCredentialsRequestMsg deviceCredentialsRequestMsg); @@ -46,4 +49,5 @@ public interface EdgeRequestsService { @Deprecated(since = "3.9.1", forRemoval = true) ListenableFuture processEntityViewsRequestMsg(TenantId tenantId, Edge edge, EntityViewsRequestMsg entityViewsRequestMsg); + } diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/cf/CalculatedFieldService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/cf/CalculatedFieldService.java index 85cd8d24fd..5ac0e98d41 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/cf/CalculatedFieldService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/cf/CalculatedFieldService.java @@ -31,8 +31,6 @@ 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); diff --git a/common/edge-api/src/main/proto/edge.proto b/common/edge-api/src/main/proto/edge.proto index 23eafc4574..c805f42e8c 100644 --- a/common/edge-api/src/main/proto/edge.proto +++ b/common/edge-api/src/main/proto/edge.proto @@ -334,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; @@ -433,6 +439,7 @@ message UplinkMsg { repeated RuleChainUpdateMsg ruleChainUpdateMsg = 23; repeated RuleChainMetadataUpdateMsg ruleChainMetadataUpdateMsg = 24; repeated CalculatedFieldUpdateMsg calculatedFieldUpdateMsg = 25; + repeated CalculatedFieldRequestMsg calculatedFieldRequestMsg = 26; } message UplinkResponseMsg { diff --git a/dao/src/main/java/org/thingsboard/server/dao/cf/BaseCalculatedFieldService.java b/dao/src/main/java/org/thingsboard/server/dao/cf/BaseCalculatedFieldService.java index 793f839fb9..0add13b5f8 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/cf/BaseCalculatedFieldService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/cf/BaseCalculatedFieldService.java @@ -62,17 +62,6 @@ public class BaseCalculatedFieldService extends AbstractEntityService implements return doSave(calculatedField, oldCalculatedField); } - @Override - public CalculatedField save(CalculatedField calculatedField, boolean doValidate) { - CalculatedField oldCalculatedField = null; - - if (doValidate) { - oldCalculatedField = calculatedFieldDataValidator.validate(calculatedField, CalculatedField::getTenantId); - } - - return doSave(calculatedField, oldCalculatedField); - } - private CalculatedField doSave(CalculatedField calculatedField, CalculatedField oldCalculatedField) { try { TenantId tenantId = calculatedField.getTenantId(); @@ -104,7 +93,7 @@ public class BaseCalculatedFieldService extends AbstractEntityService implements log.trace("Executing findByEntityIdAndName [{}], calculatedFieldName[{}]", entityId, name); validateId(entityId.getId(), id -> INCORRECT_ENTITY_ID + id); - return calculatedFieldDao.findByEntityIdAndName(entityId, name).orElse(null); + return calculatedFieldDao.findByEntityIdAndName(entityId, name); } @Override diff --git a/dao/src/main/java/org/thingsboard/server/dao/cf/CalculatedFieldDao.java b/dao/src/main/java/org/thingsboard/server/dao/cf/CalculatedFieldDao.java index bd244b9d24..d5465cb8a1 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/cf/CalculatedFieldDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/cf/CalculatedFieldDao.java @@ -24,7 +24,6 @@ import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.dao.Dao; import java.util.List; -import java.util.Optional; public interface CalculatedFieldDao extends Dao { @@ -36,7 +35,7 @@ public interface CalculatedFieldDao extends Dao { List findAll(); - Optional findByEntityIdAndName(EntityId entityId, String name); + CalculatedField findByEntityIdAndName(EntityId entityId, String name); PageData findAll(PageLink pageLink); diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/cf/JpaCalculatedFieldDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/cf/JpaCalculatedFieldDao.java index 23a146bc47..2632b0237b 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/cf/JpaCalculatedFieldDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/cf/JpaCalculatedFieldDao.java @@ -34,7 +34,6 @@ import org.thingsboard.server.dao.sql.JpaAbstractDao; import org.thingsboard.server.dao.util.SqlDao; import java.util.List; -import java.util.Optional; import java.util.UUID; @Slf4j @@ -67,9 +66,8 @@ public class JpaCalculatedFieldDao extends JpaAbstractDao findByEntityIdAndName(EntityId entityId, String name) { - CalculatedField calculatedField = DaoUtil.getData(calculatedFieldRepository.findByEntityIdAndName(entityId.getId(), name)); - return Optional.ofNullable(calculatedField); + public CalculatedField findByEntityIdAndName(EntityId entityId, String name) { + return DaoUtil.getData(calculatedFieldRepository.findByEntityIdAndName(entityId.getId(), name)); } @Override From a67f2eb516c02abc17d70cb49cb3b29f7d65826f Mon Sep 17 00:00:00 2001 From: yevhenii Date: Tue, 24 Jun 2025 13:37:34 +0300 Subject: [PATCH 37/54] CalculatedField functionality support for Edge - refactoring --- .../edge/rpc/processor/BaseEdgeProcessor.java | 4 +- .../BaseCalculatedFieldProcessor.java | 2 +- .../rpc/sync/DefaultEdgeRequestsService.java | 84 +------------------ .../server/dao/cf/CalculatedFieldService.java | 2 + .../dao/cf/BaseCalculatedFieldService.java | 14 ++++ 5 files changed, 21 insertions(+), 85 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessor.java index 6fcb02e4bc..e49f22ed7e 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessor.java @@ -139,8 +139,8 @@ public abstract class BaseEdgeProcessor implements EdgeProcessor { UPDATED_COMMENT, DELETED -> true; default -> switch (type) { case ALARM, ALARM_COMMENT, RULE_CHAIN, RULE_CHAIN_METADATA, USER, CUSTOMER, TENANT, TENANT_PROFILE, - WIDGETS_BUNDLE, WIDGET_TYPE, ADMIN_SETTINGS, OTA_PACKAGE, QUEUE, RELATION, NOTIFICATION_TEMPLATE, NOTIFICATION_TARGET, - NOTIFICATION_RULE -> true; + WIDGETS_BUNDLE, WIDGET_TYPE, ADMIN_SETTINGS, OTA_PACKAGE, QUEUE, RELATION, CALCULATED_FIELD, NOTIFICATION_TEMPLATE, + NOTIFICATION_TARGET, NOTIFICATION_RULE -> true; default -> false; }; }; diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/calculated/BaseCalculatedFieldProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/calculated/BaseCalculatedFieldProcessor.java index 503e5d74dc..a7781fe55d 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/calculated/BaseCalculatedFieldProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/calculated/BaseCalculatedFieldProcessor.java @@ -80,7 +80,7 @@ public abstract class BaseCalculatedFieldProcessor extends BaseEdgeProcessor { calculatedField.setId(calculatedFieldId); } - edgeCtx.getCalculatedFieldService().save(calculatedField); + edgeCtx.getCalculatedFieldService().save(calculatedField, false); } catch (Exception e) { log.error("[{}] Failed to process calculatedField update msg [{}]", tenantId, calculatedFieldUpdateMsg, e); throw e; diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/sync/DefaultEdgeRequestsService.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/sync/DefaultEdgeRequestsService.java index 4487a0acf8..414dcdab76 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/sync/DefaultEdgeRequestsService.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/sync/DefaultEdgeRequestsService.java @@ -23,7 +23,6 @@ import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.SettableFuture; import lombok.extern.slf4j.Slf4j; import org.checkerframework.checker.nullness.qual.Nullable; -import org.jetbrains.annotations.NotNull; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; @@ -32,7 +31,6 @@ import org.thingsboard.server.common.data.AttributeScope; import org.thingsboard.server.common.data.EdgeUtils; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityView; -import org.thingsboard.server.common.data.ExportableEntity; import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.edge.EdgeEvent; import org.thingsboard.server.common.data.edge.EdgeEventActionType; @@ -48,8 +46,6 @@ import org.thingsboard.server.common.data.id.WidgetsBundleId; import org.thingsboard.server.common.data.kv.AttributeKvEntry; import org.thingsboard.server.common.data.kv.DataType; import org.thingsboard.server.common.data.kv.TsKvEntry; -import org.thingsboard.server.common.data.page.PageData; -import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.EntityRelationsQuery; import org.thingsboard.server.common.data.relation.EntitySearchDirection; @@ -82,15 +78,10 @@ import org.thingsboard.server.service.executors.DbCallbackExecutorService; import org.thingsboard.server.service.state.DefaultDeviceStateService; import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Objects; -import java.util.Optional; import java.util.UUID; -import java.util.function.Function; import java.util.stream.Collectors; @Service @@ -328,79 +319,8 @@ public class DefaultEdgeRequestsService implements EdgeRequestsService { EntityType.valueOf(calculatedFieldRequestMsg.getEntityType()), new UUID(calculatedFieldRequestMsg.getEntityIdMSB(), calculatedFieldRequestMsg.getEntityIdLSB())); - if (entityId.getEntityType() == EntityType.EDGE) { - log.trace("[{}] processAllCalculatedField [{}][{}]", tenantId, edge.getName(), calculatedFieldRequestMsg); - return syncAllCalculatedFieldForEdge(tenantId, edge, calculatedFieldRequestMsg); - } else { - log.trace("[{}] processCalculatedField [{}][{}] for entity [{}][{}]", tenantId, edge.getName(), calculatedFieldRequestMsg, entityId.getEntityType(), entityId.getId()); - return saveCalculatedFieldsToEdge(tenantId, edge.getId(), entityId); - } - } - - @NotNull - private ListenableFuture syncAllCalculatedFieldForEdge(TenantId tenantId, Edge edge, CalculatedFieldRequestMsg calculatedFieldRequestMsg) { - EdgeId edgeId = edge.getId(); - ListenableFuture> deviceIdsFuture = - findAllEntityIdsAsync(pageLink -> deviceService.findDevicesByTenantIdAndEdgeId(tenantId, edgeId, pageLink)); - ListenableFuture> assetIdsFuture = - findAllEntityIdsAsync(pageLink -> assetService.findAssetsByTenantIdAndEdgeId(tenantId, edgeId, pageLink)); - ListenableFuture> deviceProfileIdsFuture = - findAllEntityIdsAsync(pageLink -> deviceProfileService.findDeviceProfiles(tenantId, pageLink)); - ListenableFuture> assetProfileIdsFuture = - findAllEntityIdsAsync(pageLink -> assetProfileService.findAssetProfiles(tenantId, pageLink)); - - ListenableFuture> allEntityIdFuture = - mergeEntityIdFutures(deviceIdsFuture, assetIdsFuture, deviceProfileIdsFuture, assetProfileIdsFuture); - - return Futures.transformAsync(allEntityIdFuture, allIds -> { - log.trace("[{}][{}] Going to sync calculatedFields for [{}] entities", tenantId, edge.getName(), allIds.size()); - List> saveFutures = allIds.stream() - .map(id -> saveCalculatedFieldsToEdge(tenantId, edge.getId(), id)) - .collect(Collectors.toList()); - - return Futures.transform( - Futures.allAsList(saveFutures), - result -> null, - dbCallbackExecutorService - ); - }, dbCallbackExecutorService); - } - - private > ListenableFuture> findAllEntityIdsAsync(Function> fetcher) { - return dbCallbackExecutorService.submit(() -> { - List result = new ArrayList<>(); - PageLink pageLink = new PageLink(100); - PageData pageData; - while (true) { - pageData = fetcher.apply(pageLink); - - Optional.ofNullable(pageData) - .map(PageData::getData) - .ifPresent(dataList -> dataList.stream() - .filter(Objects::nonNull) - .map(ExportableEntity::getId) - .forEach(result::add)); - - if (pageData == null || !pageData.hasNext()) { - break; - } - - pageLink = pageLink.nextPageLink(); - } - return result; - }); - } - - @SafeVarargs - private ListenableFuture> mergeEntityIdFutures(ListenableFuture>... futures) { - return Futures.transform( - Futures.allAsList(Arrays.asList(futures)), - listsOfIds -> listsOfIds.stream() - .filter(Objects::nonNull) - .flatMap(Collection::stream) - .collect(Collectors.toList()), - dbCallbackExecutorService - ); + log.trace("[{}] processCalculatedField [{}][{}] for entity [{}][{}]", tenantId, edge.getName(), calculatedFieldRequestMsg, entityId.getEntityType(), entityId.getId()); + return saveCalculatedFieldsToEdge(tenantId, edge.getId(), entityId); } private ListenableFuture saveCalculatedFieldsToEdge(TenantId tenantId, EdgeId edgeId, EntityId entityId) { diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/cf/CalculatedFieldService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/cf/CalculatedFieldService.java index 5ac0e98d41..85cd8d24fd 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/cf/CalculatedFieldService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/cf/CalculatedFieldService.java @@ -31,6 +31,8 @@ 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); diff --git a/dao/src/main/java/org/thingsboard/server/dao/cf/BaseCalculatedFieldService.java b/dao/src/main/java/org/thingsboard/server/dao/cf/BaseCalculatedFieldService.java index 0add13b5f8..dc6abbde8b 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/cf/BaseCalculatedFieldService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/cf/BaseCalculatedFieldService.java @@ -62,6 +62,20 @@ public class BaseCalculatedFieldService extends AbstractEntityService implements 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(); From 2414b979233c5b7fa621c8bb3dec2f5dea8125de Mon Sep 17 00:00:00 2001 From: yevhenii Date: Tue, 24 Jun 2025 14:21:23 +0300 Subject: [PATCH 38/54] CalculatedField functionality support for Edge - add test --- .../rpc/sync/DefaultEdgeRequestsService.java | 12 ------ .../server/edge/CalculatedFieldEdgeTest.java | 38 +++++++++++++++++++ .../thingsboard/edge/rpc/EdgeGrpcClient.java | 2 +- 3 files changed, 39 insertions(+), 13 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/sync/DefaultEdgeRequestsService.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/sync/DefaultEdgeRequestsService.java index 414dcdab76..7cdf916439 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/sync/DefaultEdgeRequestsService.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/sync/DefaultEdgeRequestsService.java @@ -53,12 +53,8 @@ import org.thingsboard.server.common.data.relation.RelationTypeGroup; 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.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.device.DeviceProfileService; -import org.thingsboard.server.dao.device.DeviceService; import org.thingsboard.server.dao.edge.EdgeEventService; import org.thingsboard.server.dao.relation.RelationService; import org.thingsboard.server.dao.timeseries.TimeseriesService; @@ -113,14 +109,6 @@ public class DefaultEdgeRequestsService implements EdgeRequestsService { @Autowired private CalculatedFieldService calculatedFieldService; - @Autowired - private DeviceService deviceService; - @Autowired - private DeviceProfileService deviceProfileService; - @Autowired - private AssetService assetService; - @Autowired - private AssetProfileService assetProfileService; @Autowired private DbCallbackExecutorService dbCallbackExecutorService; diff --git a/application/src/test/java/org/thingsboard/server/edge/CalculatedFieldEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/CalculatedFieldEdgeTest.java index 0eabba10f9..268e19345c 100644 --- a/application/src/test/java/org/thingsboard/server/edge/CalculatedFieldEdgeTest.java +++ b/application/src/test/java/org/thingsboard/server/edge/CalculatedFieldEdgeTest.java @@ -33,6 +33,7 @@ import org.thingsboard.server.common.data.cf.configuration.SimpleCalculatedField 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; @@ -114,6 +115,43 @@ public class CalculatedFieldEdgeTest extends AbstractEdgeTest { 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(); diff --git a/common/edge-api/src/main/java/org/thingsboard/edge/rpc/EdgeGrpcClient.java b/common/edge-api/src/main/java/org/thingsboard/edge/rpc/EdgeGrpcClient.java index 509b30feb4..e1ff386a32 100644 --- a/common/edge-api/src/main/java/org/thingsboard/edge/rpc/EdgeGrpcClient.java +++ b/common/edge-api/src/main/java/org/thingsboard/edge/rpc/EdgeGrpcClient.java @@ -136,7 +136,7 @@ public class EdgeGrpcClient implements EdgeRpcClient { .setConnectRequestMsg(ConnectRequestMsg.newBuilder() .setEdgeRoutingKey(edgeKey) .setEdgeSecret(edgeSecret) - .setEdgeVersion(EdgeVersion.V_4_0_0) + .setEdgeVersion(EdgeVersion.V_4_1_0) .setMaxInboundMessageSize(maxInboundMessageSize) .build()) .build()); From 76f33663c97efa3e9e9267922d84768f91594107 Mon Sep 17 00:00:00 2001 From: dshvaika Date: Tue, 24 Jun 2025 11:21:29 +0300 Subject: [PATCH 39/54] Added missing fields for RPC message in cluster mode --- .../org/thingsboard/server/common/util/ProtoUtils.java | 10 +++++++--- common/proto/src/main/proto/queue.proto | 2 ++ 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/common/proto/src/main/java/org/thingsboard/server/common/util/ProtoUtils.java b/common/proto/src/main/java/org/thingsboard/server/common/util/ProtoUtils.java index bc96952ca7..3cded5a491 100644 --- a/common/proto/src/main/java/org/thingsboard/server/common/util/ProtoUtils.java +++ b/common/proto/src/main/java/org/thingsboard/server/common/util/ProtoUtils.java @@ -522,7 +522,7 @@ public class ProtoUtils { } private static TransportProtos.ToDeviceRpcRequestActorMsgProto toProto(ToDeviceRpcRequestActorMsg msg) { - TransportProtos.ToDeviceRpcRequestMsg proto = TransportProtos.ToDeviceRpcRequestMsg.newBuilder() + TransportProtos.ToDeviceRpcRequestMsg.Builder builder = TransportProtos.ToDeviceRpcRequestMsg.newBuilder() .setMethodName(msg.getMsg().getBody().getMethod()) .setParams(msg.getMsg().getBody().getParams()) .setExpirationTime(msg.getMsg().getExpirationTime()) @@ -530,7 +530,11 @@ public class ProtoUtils { .setRequestIdLSB(msg.getMsg().getId().getLeastSignificantBits()) .setOneway(msg.getMsg().isOneway()) .setPersisted(msg.getMsg().isPersisted()) - .build(); + .setAdditionalInfo(msg.getMsg().getAdditionalInfo()); + if (msg.getMsg().getRetries() != null) { + builder.setRetries(msg.getMsg().getRetries()); + } + TransportProtos.ToDeviceRpcRequestMsg proto = builder.build(); return TransportProtos.ToDeviceRpcRequestActorMsgProto.newBuilder() .setTenantIdMSB(msg.getTenantId().getId().getMostSignificantBits()) @@ -551,7 +555,7 @@ public class ProtoUtils { toDeviceRpcRequestMsg.getOneway(), toDeviceRpcRequestMsg.getExpirationTime(), new ToDeviceRpcRequestBody(toDeviceRpcRequestMsg.getMethodName(), toDeviceRpcRequestMsg.getParams()), - toDeviceRpcRequestMsg.getPersisted(), 0, ""); + toDeviceRpcRequestMsg.getPersisted(), toDeviceRpcRequestMsg.hasRetries() ? toDeviceRpcRequestMsg.getRetries() : null, toDeviceRpcRequestMsg.getAdditionalInfo()); return new ToDeviceRpcRequestActorMsg(proto.getServiceId(), toDeviceRpcRequest); } diff --git a/common/proto/src/main/proto/queue.proto b/common/proto/src/main/proto/queue.proto index c413ecb3c1..2667838b60 100644 --- a/common/proto/src/main/proto/queue.proto +++ b/common/proto/src/main/proto/queue.proto @@ -696,6 +696,8 @@ message ToDeviceRpcRequestMsg { int64 requestIdLSB = 6; bool oneway = 7; bool persisted = 8; + optional int32 retries = 9; + string additionalInfo = 10; } message ToDeviceRpcResponseMsg { From 71ec0a588a402faa0a644cd5a7e5c617b354c56f Mon Sep 17 00:00:00 2001 From: Andrii Landiak Date: Wed, 25 Jun 2025 11:34:17 +0300 Subject: [PATCH 40/54] Fix test to use profile with ota on import --- .../service/sync/vc/VersionControlTest.java | 25 +++++++++++-------- .../data/sync/ie/OtaPackageExportData.java | 4 +++ 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/application/src/test/java/org/thingsboard/server/service/sync/vc/VersionControlTest.java b/application/src/test/java/org/thingsboard/server/service/sync/vc/VersionControlTest.java index 5c5b7bb107..461ca5a2ec 100644 --- a/application/src/test/java/org/thingsboard/server/service/sync/vc/VersionControlTest.java +++ b/application/src/test/java/org/thingsboard/server/service/sync/vc/VersionControlTest.java @@ -685,23 +685,28 @@ public class VersionControlTest extends AbstractControllerTest { } @Test - public void testOtaPackageVc_betweenTenants() throws Exception { + 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); - OtaPackage firmwareOta = findOtaPackage(firmware.getTitle()); - OtaPackage softwareOta = findOtaPackage(software.getTitle()); - loginTenant2(); loadVersion(versionId, EntityType.DEVICE_PROFILE, EntityType.OTA_PACKAGE); - OtaPackage importedFirmwareOta = findOtaPackage(firmwareOta.getTitle()); - OtaPackage importedSoftwareOta = findOtaPackage(softwareOta.getTitle()); - checkImportedEntity(tenantId1, firmwareOta, tenantId2, importedFirmwareOta); - checkImportedOtaPackageData(firmwareOta, importedFirmwareOta); - checkImportedEntity(tenantId1, softwareOta, tenantId2, importedSoftwareOta); - checkImportedOtaPackageData(softwareOta, importedSoftwareOta); + 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) { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/OtaPackageExportData.java b/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/OtaPackageExportData.java index df12bb5b08..44e2f7857c 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/OtaPackageExportData.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/OtaPackageExportData.java @@ -22,6 +22,10 @@ import org.thingsboard.server.common.data.OtaPackage; @EqualsAndHashCode(callSuper = true) public class OtaPackageExportData extends EntityExportData { + /* + * 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() { From 3ae97e0d7e1cf6c5d31e989923c1fcfc31affafd Mon Sep 17 00:00:00 2001 From: Yevhenii Date: Wed, 25 Jun 2025 12:11:51 +0300 Subject: [PATCH 41/54] CalculatedField functionality support for Edge - refactoring --- .../service/edge/EdgeContextComponent.java | 2 +- .../edge/rpc/processor/BaseEdgeProcessor.java | 8 ++--- .../BaseCalculatedFieldProcessor.java | 33 +------------------ .../CalculatedFieldEdgeProcessor.java | 6 ++-- .../CalculatedFieldProcessor.java | 2 +- .../rpc/sync/DefaultEdgeRequestsService.java | 6 ++-- .../dao/cf/BaseCalculatedFieldService.java | 4 --- .../server/dao/edge/EdgeServiceImpl.java | 1 - 8 files changed, 12 insertions(+), 50 deletions(-) rename application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/{calculated => cf}/BaseCalculatedFieldProcessor.java (66%) rename application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/{calculated => cf}/CalculatedFieldEdgeProcessor.java (96%) rename application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/{calculated => cf}/CalculatedFieldProcessor.java (94%) diff --git a/application/src/main/java/org/thingsboard/server/service/edge/EdgeContextComponent.java b/application/src/main/java/org/thingsboard/server/service/edge/EdgeContextComponent.java index abdc89ebd9..1193f935a0 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/EdgeContextComponent.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/EdgeContextComponent.java @@ -62,7 +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.calculated.CalculatedFieldProcessor; +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; diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessor.java index e49f22ed7e..dc16c5b229 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessor.java @@ -222,7 +222,7 @@ public abstract class BaseEdgeProcessor implements EdgeProcessor { if (edgeId != null) { return saveEdgeEvent(tenantId, edgeId, type, actionType, entityId, body); } else { - return processNotificationToRelatedEdges(tenantId, entityId, type, actionType, originatorEdgeId); + return processNotificationToRelatedEdges(tenantId, entityId, entityId, type, actionType, originatorEdgeId); } case DELETED: EdgeEventActionType deleted = EdgeEventActionType.DELETED; @@ -260,11 +260,11 @@ public abstract class BaseEdgeProcessor implements EdgeProcessor { } } - private ListenableFuture processNotificationToRelatedEdges(TenantId tenantId, EntityId entityId, EdgeEventType type, - EdgeEventActionType actionType, EdgeId sourceEdgeId) { + protected ListenableFuture processNotificationToRelatedEdges(TenantId tenantId, EntityId ownerEntityId, EntityId entityId, EdgeEventType type, + EdgeEventActionType actionType, EdgeId sourceEdgeId) { List> futures = new ArrayList<>(); PageDataIterableByTenantIdEntityId edgeIds = - new PageDataIterableByTenantIdEntityId<>(edgeCtx.getEdgeService()::findRelatedEdgeIdsByEntityId, tenantId, entityId, RELATED_EDGES_CACHE_ITEMS); + new PageDataIterableByTenantIdEntityId<>(edgeCtx.getEdgeService()::findRelatedEdgeIdsByEntityId, tenantId, ownerEntityId, RELATED_EDGES_CACHE_ITEMS); for (EdgeId relatedEdgeId : edgeIds) { if (!relatedEdgeId.equals(sourceEdgeId)) { futures.add(saveEdgeEvent(tenantId, relatedEdgeId, type, actionType, entityId, null)); diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/calculated/BaseCalculatedFieldProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/cf/BaseCalculatedFieldProcessor.java similarity index 66% rename from application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/calculated/BaseCalculatedFieldProcessor.java rename to application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/cf/BaseCalculatedFieldProcessor.java index a7781fe55d..4ef6ec7ba2 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/calculated/BaseCalculatedFieldProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/cf/BaseCalculatedFieldProcessor.java @@ -13,33 +13,21 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.service.edge.rpc.processor.calculated; +package org.thingsboard.server.service.edge.rpc.processor.cf; import com.datastax.oss.driver.api.core.uuid.Uuids; -import com.google.common.util.concurrent.Futures; -import com.google.common.util.concurrent.ListenableFuture; 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.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.TenantId; -import org.thingsboard.server.common.data.page.PageDataIterableByTenantIdEntityId; import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.gen.edge.v1.CalculatedFieldUpdateMsg; import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor; -import java.util.ArrayList; -import java.util.List; - -import static org.thingsboard.server.dao.edge.BaseRelatedEdgesService.RELATED_EDGES_CACHE_ITEMS; - @Slf4j public abstract class BaseCalculatedFieldProcessor extends BaseEdgeProcessor { @@ -88,23 +76,4 @@ public abstract class BaseCalculatedFieldProcessor extends BaseEdgeProcessor { return Pair.of(isCreated, isNameUpdated); } - protected ListenableFuture pushEventToAllRelatedEdges(TenantId tenantId, EntityId calculatedFieldOwnerId, EntityId entityId, EdgeEventType type, EdgeEventActionType actionType, EdgeId sourceEdgeId) { - List> futures = new ArrayList<>(); - PageDataIterableByTenantIdEntityId edgeIds = - new PageDataIterableByTenantIdEntityId<>(edgeCtx.getEdgeService()::findRelatedEdgeIdsByEntityId, tenantId, calculatedFieldOwnerId, RELATED_EDGES_CACHE_ITEMS); - for (EdgeId relatedEdgeId : edgeIds) { - if (!relatedEdgeId.equals(sourceEdgeId)) { - futures.add(saveEdgeEvent(tenantId, relatedEdgeId, type, actionType, entityId, null)); - } - } - return Futures.transform(Futures.allAsList(futures), voids -> null, dbCallbackExecutorService); - } - - protected ListenableFuture pushEventToAllEdges(TenantId tenantId, EdgeEventType type, EdgeEventActionType actionType, EntityId entityId, EdgeId sourceEdgeId) { - return switch (actionType) { - case ADDED, UPDATED, DELETED -> processActionForAllEdges(tenantId, type, actionType, entityId, null, sourceEdgeId); - default -> Futures.immediateFuture(null); - }; - } - } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/calculated/CalculatedFieldEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/cf/CalculatedFieldEdgeProcessor.java similarity index 96% rename from application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/calculated/CalculatedFieldEdgeProcessor.java rename to application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/cf/CalculatedFieldEdgeProcessor.java index a0212bb4df..cab4b5ecc1 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/calculated/CalculatedFieldEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/cf/CalculatedFieldEdgeProcessor.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.service.edge.rpc.processor.calculated; +package org.thingsboard.server.service.edge.rpc.processor.cf; import com.fasterxml.jackson.databind.JsonNode; import com.google.common.util.concurrent.Futures; @@ -134,9 +134,9 @@ public class CalculatedFieldEdgeProcessor extends BaseCalculatedFieldProcessor i return edgeId != null ? saveEdgeEvent(tenantId, edgeId, type, actionType, entityId, body) : - pushEventToAllRelatedEdges(tenantId, calculatedFieldOwnerId, entityId, type, actionType, originatorEdgeId); + processNotificationToRelatedEdges(tenantId, calculatedFieldOwnerId, entityId, type, actionType, originatorEdgeId); } else { - return pushEventToAllEdges(tenantId, type, actionType, entityId, originatorEdgeId); + return processActionForAllEdges(tenantId, type, actionType, entityId, null, originatorEdgeId); } default: return super.processEntityNotification(tenantId, edgeNotificationMsg); diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/calculated/CalculatedFieldProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/cf/CalculatedFieldProcessor.java similarity index 94% rename from application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/calculated/CalculatedFieldProcessor.java rename to application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/cf/CalculatedFieldProcessor.java index ba9c8b27e1..d21af858f0 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/calculated/CalculatedFieldProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/cf/CalculatedFieldProcessor.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.service.edge.rpc.processor.calculated; +package org.thingsboard.server.service.edge.rpc.processor.cf; import com.google.common.util.concurrent.ListenableFuture; import org.thingsboard.server.common.data.edge.Edge; diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/sync/DefaultEdgeRequestsService.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/sync/DefaultEdgeRequestsService.java index 7cdf916439..66ff05b45a 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/sync/DefaultEdgeRequestsService.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/sync/DefaultEdgeRequestsService.java @@ -78,7 +78,6 @@ import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.UUID; -import java.util.stream.Collectors; @Service @TbCoreComponent @@ -322,11 +321,10 @@ public class DefaultEdgeRequestsService implements EdgeRequestsService { return saveEdgeEvent(tenantId, edgeId, EdgeEventType.CALCULATED_FIELD, EdgeEventActionType.ADDED, calculatedField.getId(), JacksonUtil.valueToTree(calculatedField)); } catch (Exception e) { - String errMsg = String.format("[%s][%s] Exception during loading calculatedField [%s] to edge on sync!", tenantId, edgeId, calculatedField); - log.error(errMsg, e); + log.error("[{}][{}] Exception during loading calculatedField [{}] to edge on sync!", tenantId, edgeId, calculatedField, e); return Futures.immediateFailedFuture(e); } - }).collect(Collectors.toList()); + }).toList(); return Futures.transform( Futures.allAsList(futures), diff --git a/dao/src/main/java/org/thingsboard/server/dao/cf/BaseCalculatedFieldService.java b/dao/src/main/java/org/thingsboard/server/dao/cf/BaseCalculatedFieldService.java index dc6abbde8b..c0cb886747 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/cf/BaseCalculatedFieldService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/cf/BaseCalculatedFieldService.java @@ -58,20 +58,17 @@ 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); } @@ -106,7 +103,6 @@ public class BaseCalculatedFieldService extends AbstractEntityService implements 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); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeServiceImpl.java index ebcfd7678a..0655d05572 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeServiceImpl.java @@ -524,7 +524,6 @@ public class EdgeServiceImpl extends AbstractCachedEntityService Date: Wed, 25 Jun 2025 14:49:15 +0300 Subject: [PATCH 42/54] UI: Add new unit in/s and capitalize unit name --- ui-ngx/src/app/shared/models/units/speed.ts | 7 +- .../assets/locale/locale.constant-en_US.json | 205 +++++++++--------- 2 files changed, 109 insertions(+), 103 deletions(-) diff --git a/ui-ngx/src/app/shared/models/units/speed.ts b/ui-ngx/src/app/shared/models/units/speed.ts index b930b0739c..3e3bad7b03 100644 --- a/ui-ngx/src/app/shared/models/units/speed.ts +++ b/ui-ngx/src/app/shared/models/units/speed.ts @@ -19,7 +19,7 @@ import { TbMeasure, TbMeasureUnits } from '@shared/models/unit.models'; export type SpeedUnits = SpeedMetricUnits | SpeedImperialUnits; export type SpeedMetricUnits = 'm/s' | 'km/h' | 'mm/min' | 'mm/s'; -export type SpeedImperialUnits = 'mph' | 'kt' | 'ft/s' | 'ft/min' | 'in/h'; +export type SpeedImperialUnits = 'mph' | 'kt' | 'ft/s' | 'ft/min' | 'in/s' | 'in/h'; const METRIC: TbMeasureUnits = { ratio: 1 / 1.609344, @@ -70,6 +70,11 @@ const IMPERIAL: TbMeasureUnits = { tags: ['velocity', 'pace'], to_anchor: 0.0113636, }, + 'in/s': { + name: 'unit.inch-per-second', + tags: ['velocity', 'pace'], + to_anchor: 0.0568182, + }, 'in/h': { name: 'unit.inch-per-hour', tags: ['velocity', 'pace'], diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index 06a461b412..fe07c52ea8 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -6014,9 +6014,9 @@ "foot-us": "Foot (US survey)", "yard": "Yard", "mile": "Mile", - "nautical-mile": "Nautical Mile", - "astronomical-unit": "Astronomical Unit", - "reciprocal-metre": "Reciprocal Metre", + "nautical-mile": "Nautical mile", + "astronomical-unit": "Astronomical unit", + "reciprocal-metre": "Reciprocal metre", "meter-per-meter": "Meter per meter", "steradian": "Steradian", "thou": "Thou", @@ -6046,24 +6046,24 @@ "quarter": "Quarter", "slug": "Slug", "carat": "Carat", - "cubic-millimeter": "Cubic Millimeter", - "cubic-centimeter": "Cubic Centimeter", - "cubic-meter": "Cubic Meter", - "cubic-kilometer": "Cubic Kilometer", + "cubic-millimeter": "Cubic millimeter", + "cubic-centimeter": "Cubic centimeter", + "cubic-meter": "Cubic meter", + "cubic-kilometer": "Cubic kilometer", "microliter": "Microliter", "milliliter": "Milliliter", "liter": "Liter", "hectoliter": "Hectolitre", - "cubic-inch": "Cubic Inch", - "cubic-foot": "Cubic Foot", - "cubic-yard": "Cubic Yard", - "fluid-ounce": "Fluid Ounce", - "fluid-ounce-per-second": "Fluid Ounce per second", + "cubic-inch": "Cubic inch", + "cubic-foot": "Cubic foot", + "cubic-yard": "Cubic yard", + "fluid-ounce": "Fluid ounce", + "fluid-ounce-per-second": "Fluid ounce per second", "pint": "Pint", "quart": "Quart", "gallon": "Gallon", - "oil-barrels": "Oil Barrel", - "cubic-meter-per-kilogram": "Cubic Meter per Kilogram", + "oil-barrels": "Oil barrel", + "cubic-meter-per-kilogram": "Cubic meter per kilogram", "gill": "Gill", "hogshead": "Hogshead", "teaspoon": "Teaspoon", @@ -6074,13 +6074,14 @@ "rankine": "Rankine", "fahrenheit": "Fahrenheit", "percent": "Percent", - "meter-per-second": "Meter per Second", - "kilometer-per-hour": "Kilometer per Hour", - "foot-per-second": "Foot per Second", - "foot-per-minute": "Foot per Minute", - "mile-per-hour": "Mile per Hour", + "meter-per-second": "Meter per second", + "kilometer-per-hour": "Kilometer per hour", + "foot-per-second": "Foot per second", + "foot-per-minute": "Foot per minute", + "mile-per-hour": "Mile per hour", "knot": "Knot", - "inch-per-hour": "Inch per Hour", + "inch-per-second": "Inch per second", + "inch-per-hour": "Inch per hour", "millimeters-per-minute": "Millimeters per minute", "kilometer-per-hour-squared": "Kilometer per hour squared", "foot-per-second-squared": "Foot per second squared", @@ -6097,18 +6098,18 @@ "inch-pounds": "Inch-pounds", "newton-per-meter": "Newton per meter", "atmospheres": "Atmospheres", - "pounds-per-square-inch": "Pounds per Square Inch", - "kilopound-per-square-inch": "Kilopound per Square Inch", + "pounds-per-square-inch": "Pounds per square inch", + "kilopound-per-square-inch": "Kilopound per square inch", "torr": "Torr", - "inches-of-mercury": "Inches of Mercury", - "pascal-per-square-meter": "Pascal per Square Meter", - "pound-per-square-inch": "Pound per Square Inch", - "newton-per-square-meter": "Newton per Square Meter", - "kilogram-force-per-square-meter": "Kilogram-force per Square Meter", - "pascal-per-square-centimeter": "Pascal per Square Centimeter", - "ton-force-per-square-inch": "Ton-force per Square Inch", - "kilonewton-per-square-meter": "Kilonewton per Square Meter", - "newton-per-square-millimeter": "Newton per Square Millimeter", + "inches-of-mercury": "Inches of mercury", + "pascal-per-square-meter": "Pascal per square meter", + "pound-per-square-inch": "Pound per square inch", + "newton-per-square-meter": "Newton per square meter", + "kilogram-force-per-square-meter": "Kilogram-force per square meter", + "pascal-per-square-centimeter": "Pascal per square centimeter", + "ton-force-per-square-inch": "Ton-force per square inch", + "kilonewton-per-square-meter": "Kilonewton per square meter", + "newton-per-square-millimeter": "Newton per square millimeter", "microjoule": "Microjoule", "millijoule": "Millijoule", "joule": "Joule", @@ -6122,31 +6123,31 @@ "megawatt-hour": "Megawatt-hour", "gigawatt-hour": "Gigawatt-hour", "electron-volts": "Electron volts", - "joules-per-coulomb": "Joules per Coulomb", - "british-thermal-unit": "British Thermal Units", - "thousand-british-thermal-unit": "Thousand British Thermal Units", - "million-british-thermal-unit": "Million British Thermal Units", + "joules-per-coulomb": "Joules per coulomb", + "british-thermal-unit": "British thermal units", + "thousand-british-thermal-unit": "Thousand British thermal units", + "million-british-thermal-unit": "Million British thermal units", "foot-pound": "Foot-pound", "calorie": "Calorie", - "small-calorie": "Small Calorie", + "small-calorie": "Small calorie", "kilocalorie": "Kilocalorie", - "joule-per-kelvin": "Joule per Kelvin", - "joule-per-kilogram-kelvin": "Joule per Kilogram-Kelvin", - "joule-per-kilogram": "Joule per Kilogram", - "watt-per-meter-kelvin": "Watt per Meter-Kelvin", - "joule-per-cubic-meter": "Joule per Cubic Meter", + "joule-per-kelvin": "Joule per kelvin", + "joule-per-kilogram-kelvin": "Joule per kilogram-kelvin", + "joule-per-kilogram": "Joule per kilogram", + "watt-per-meter-kelvin": "Watt per meter-kelvin", + "joule-per-cubic-meter": "Joule per cubic meter", "therm": "Therm", - "electric-dipole-moment": "Electric Dipole Moment", - "magnetic-dipole-moment": "Magnetic Dipole Moment", + "electric-dipole-moment": "Electric dipole moment", + "magnetic-dipole-moment": "Magnetic dipole moment", "debye": "Debye", - "coulomb-per-square-meter-per-volt": "Coulomb per Square Meter per Volt", + "coulomb-per-square-meter-per-volt": "Coulomb per square meter per volt", "milliwatt": "Milliwatt", "microwatt": "Microwatt", "watt": "Watt", "kilowatt": "Kilowatt", "megawatt": "Megawatt", "gigawatt": "Gigawatt", - "metric-horsepower": "Metric Horsepower", + "metric-horsepower": "Metric horsepower", "milliwatt-per-square-centimeter": "Milliwatts per square centimeter", "watt-per-square-centimeter": "Watts per square centimeter", "kilowatt-per-square-centimeter": "Kilowatts per square centimeter", @@ -6165,28 +6166,28 @@ "mmbtu-per-hour": "Million British thermal units per hour", "mmbtu-per-second": "Million British thermal units per second", "mmbtu-per-day": "Million British thermal units per day", - "foot-pound-per-second": "foot-pound per second", + "foot-pound-per-second": "Foot-pound per second", "coulomb": "Coulomb", "millicoulomb": "Millicoulombs", "microcoulomb": "Microcoulomb", "nanocoulomb": "Nanocoulomb", "picocoulomb": "Picocoulomb", "coulomb-per-meter": "Coulomb per meter", - "coulomb-per-cubic-meter": "Coulomb per Cubic Meter", - "coulomb-per-square-meter": "Coulomb per Square Meter", - "square-millimeter": "Square Millimeter", - "square-centimeter": "Square Centimeter", - "square-meter": "Square Meter", + "coulomb-per-cubic-meter": "Coulomb per cubic meter", + "coulomb-per-square-meter": "Coulomb per square meter", + "square-millimeter": "Square millimeter", + "square-centimeter": "Square centimeter", + "square-meter": "Square meter", "hectare": "Hectare", - "square-kilometer": "Square Kilometer", - "square-inch": "Square Inch", - "square-foot": "Square Foot", - "square-yard": "Square Yard", + "square-kilometer": "Square kilometer", + "square-inch": "Square inch", + "square-foot": "Square foot", + "square-yard": "Square yard", "acre": "Acre", - "square-mile": "Square Mile", + "square-mile": "Square mile", "are": "Are", "barn": "Barn", - "circular-inch": "Circular Inch", + "circular-inch": "Circular inch", "milliampere-hour": "Milliampere-hour", "ampere-hours": "Ampere-hours", "kiloampere-hours": "Kiloampere-hours", @@ -6199,11 +6200,11 @@ "megaampere": "Megaampere", "gigaampere": "Gigaampere", "microampere-per-square-centimeter": "Microampere per square centimeter", - "ampere-per-square-meter": "Ampere per Square Meter", - "ampere-per-meter": "Ampere per Meter", + "ampere-per-square-meter": "Ampere per square meter", + "ampere-per-meter": "Ampere per meter", "oersted": "Oersted", - "bohr-magneton": "Bohr Magneton", - "ampere-meter-squared": "Ampere-Meter Squared", + "bohr-magneton": "Bohr magneton", + "ampere-meter-squared": "Ampere-meter squared", "nanovolt": "Nanovolt", "picovolt": "Picovolt", "millivolt": "Millivolts", @@ -6213,12 +6214,12 @@ "megavolt": "Megavolt", "dbmV": "Decibel volt", "dbm": "Decibel-milliwatts", - "volt-meter": "Volt-Meter", - "kilovolt-meter": "Kilovolt-Meter", - "megavolt-meter": "Megavolt-Meter", - "microvolt-meter": "Microvolt-Meter", - "millivolt-meter": "Millivolt-Meter", - "nanovolt-meter": "Nanovolt-Meter", + "volt-meter": "Volt-meter", + "kilovolt-meter": "Kilovolt-meter", + "megavolt-meter": "Megavolt-meter", + "microvolt-meter": "Microvolt-meter", + "millivolt-meter": "Millivolt-meter", + "nanovolt-meter": "Nanovolt-meter", "ohm": "Ohm", "microohm": "Microohm", "milliohm": "Milliohm", @@ -6231,7 +6232,7 @@ "megahertz": "Megahertz", "gigahertz": "Gigahertz", "terahertz": "Terahertz", - "rpm": "Revolutions Per Minute", + "rpm": "Revolutions per minute", "candela-per-square-meter": "Candela per square meter", "candela": "Candela", "lumen": "Lumen", @@ -6243,17 +6244,17 @@ "lumens-per-watt": "Lumens per watt", "mole": "Mole", "nanomole": "Nanomole", - "micromole": "MicroMole", + "micromole": "Micromole", "millimole": "Millimole", "kilomole": "Kilomole", - "mole-per-cubic-meter": "Mole per Cubic Meter", + "mole-per-cubic-meter": "Mole per cubic meter", "rssi": "Received signal strength indicator", - "ppm": "Parts Per Million", - "ppb": "Parts Per Billion", - "micrograms-per-cubic-meter": "Micrograms per Cubic Meter", - "aqi": "AQI", + "ppm": "Parts per million", + "ppb": "Parts per billion", + "micrograms-per-cubic-meter": "Micrograms per cubic meter", + "aqi": "Aqi", "gram-per-cubic-meter": "Gram per cubic meter", - "gram-per-kilogram": "Specific Humidity", + "gram-per-kilogram": "Specific humidity", "millimeters-per-second": "Millimeters per second", "neper": "Neper", "bel": "Bel", @@ -6264,7 +6265,7 @@ "gray": "Gray", "sievert": "Sievert", "roentgen": "Roentgen", - "cps": "Counts per Second", + "cps": "Counts per second", "rad": "Rad", "rem": "Rem", "dps": "Disintegrations per second", @@ -6274,10 +6275,10 @@ "curies-per-liter": "Curies per liter", "becquerels-per-second": "Becquerels per second", "curies-per-second": "Curies per second", - "gy-per-second": "Gray per Second", - "watt-per-steradian": "Watt per Steradian", - "watt-per-square-metre-steradian": "Watt per Square Metre-Steradian", - "ph-level": "pH Level", + "gy-per-second": "Gray per second", + "watt-per-steradian": "Watt per steradian", + "watt-per-square-metre-steradian": "Watt per square metre-steradian", + "ph-level": "Ph level", "turbidity": "Turbidity", "mg-per-liter": "Milligrams per liter", "microsiemens-per-centimeter": "Microsiemens per centimeter", @@ -6303,9 +6304,9 @@ "milligrams-per-deciliter": "Milligrams per deciliter", "g-force": "G-force", "kilonewton": "Kilonewton", - "kilogram-force": "Kilogram-Force", - "pound-force": "Pound-Force", - "kilopound-force": "Kilopound-Force", + "kilogram-force": "Kilogram-force", + "pound-force": "Pound-force", + "kilopound-force": "Kilopound-force", "dyne": "Dyne", "poundal": "Poundal", "kip": "Kip", @@ -6315,7 +6316,7 @@ "atmosphere": "Atmosphere", "millibars": "Millibars", "inch-of-mercury": "One inch of mercury", - "richter-scale": "Richter Scale", + "richter-scale": "Richter scale", "nanosecond": "Nanosecond", "microsecond": "Microsecond", "millisecond": "Millisecond", @@ -6326,12 +6327,12 @@ "week": "Week", "month": "Month", "year": "Year", - "cubic-foot-per-minute": "Cubic Foot Per Minute", - "cubic-meters-per-hour": "Cubic Meters Per Hour", - "cubic-meters-per-second": "Cubic Meters Per Second", - "liter-per-second": "Liter Per Second", - "liter-per-minute": "Liter Per Minute", - "gallons-per-minute": "Gallons Per Minute", + "cubic-foot-per-minute": "Cubic foot per minute", + "cubic-meters-per-hour": "Cubic meters per hour", + "cubic-meters-per-second": "Cubic meters per second", + "liter-per-second": "Liter per second", + "liter-per-minute": "Liter per minute", + "gallons-per-minute": "Gallons per minute", "cubic-foot-per-second": "Cubic foot per second", "milliliters-per-minute": "Milliliters per minute", "cubic-decimeter-per-second": "Cubic decimeter per second", @@ -6376,7 +6377,7 @@ "megafarad": "Megafarad", "gigafarad": "Gigafarad", "terfarad": "Terfarad", - "farad-per-meter": "Farad per Meter", + "farad-per-meter": "Farad per meter", "tesla": "Tesla", "gauss": "Gauss", "kilogauss": "Kilogauss", @@ -6385,7 +6386,7 @@ "nanotesla": "Nanotesla", "kilotesla": "Kilotesla", "megatesla": "Megatesla", - "millitesla-square-meters": "millitesla square meters", + "millitesla-square-meters": "Millitesla square meters", "gamma": "Gamma", "lambda": "Lambda", "square-meter-per-second": "Square meter per second", @@ -6404,25 +6405,25 @@ "kilogram-per-meter-second": "Kilogram per meter-second", "tesla-square-meters": "Tesla square meters", "maxwell": "Maxwell", - "tesla-per-meter": "Tesla per Meter", - "gauss-per-centimeter": "Gauss per Centimeter", + "tesla-per-meter": "Tesla per meter", + "gauss-per-centimeter": "Gauss per centimeter", "weber": "Weber", "microweber": "Microweber", "milliweber": "Milliweber", - "gauss-square-centimeter": "Gauss-Square Centimeter", - "kilogauss-square-centimeter": "Kilogauss-Square Centimeter", + "gauss-square-centimeter": "Gauss-square centimeter", + "kilogauss-square-centimeter": "Kilogauss-square centimeter", "henry": "Henry", "millihenry": "Millihenry", "microhenry": "Microhenry", "nanohenry": "Nanohenry", - "henry-per-meter": "Henry per Meter", - "tesla-meter-per-ampere": "Tesla Meter per Ampere", - "gauss-per-oersted": "Gauss per Oersted", + "henry-per-meter": "Henry per meter", + "tesla-meter-per-ampere": "Tesla meter per ampere", + "gauss-per-oersted": "Gauss per oersted", "kilogram-per-mole": "Kilogram per mole", "gram-per-mole": "Gram per mole", "milligram-per-mole": "Milligram per mole", - "joule-per-mole": "Joule per Mole", - "joule-per-mole-kelvin": "Joule per Mole-Kelvin", + "joule-per-mole": "Joule per mole", + "joule-per-mole-kelvin": "Joule per mole-kelvin", "millivolts-per-meter": "Millivolts per meter", "volts-per-meter": "Volts per meter", "kilovolts-per-meter": "Kilovolts per meter", @@ -6433,7 +6434,7 @@ "rotation-per-minute": "Rotation per minute", "degrees-brix": "Degrees brix", "katal": "Katal", - "katal-per-cubic-metre": "Katal per Cubic Metre", + "katal-per-cubic-metre": "Katal per cubic metre", "paris-inch": "Paris inch" }, "user": { From bf46d974ab8b07f9a27e11277cd5758668d41e77 Mon Sep 17 00:00:00 2001 From: Andrii Landiak Date: Wed, 25 Jun 2025 16:11:29 +0300 Subject: [PATCH 43/54] Remove duplicated indexes --- .../main/data/upgrade/basic/schema_update.sql | 13 +++++++++++++ .../CalculatedFieldEntityMessageProcessor.java | 2 +- .../main/resources/sql/schema-entities-idx.sql | 18 ------------------ 3 files changed, 14 insertions(+), 19 deletions(-) diff --git a/application/src/main/data/upgrade/basic/schema_update.sql b/application/src/main/data/upgrade/basic/schema_update.sql index 2f652ce289..c959cfd6c1 100644 --- a/application/src/main/data/upgrade/basic/schema_update.sql +++ b/application/src/main/data/upgrade/basic/schema_update.sql @@ -22,3 +22,16 @@ ALTER TABLE ota_package ADD CONSTRAINT ota_package_external_id_unq_key UNIQUE (tenant_id, external_id); -- UPDATE OTA PACKAGE EXTERNAL ID END + +-- DROP INDEXES THAT DUPLICATE UNIQUE CONSTRAINT START + +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; + +-- DROP INDEXES THAT DUPLICATE UNIQUE CONSTRAINT END diff --git a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java index ebc4e60709..35539834c3 100644 --- a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java +++ b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java @@ -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; diff --git a/dao/src/main/resources/sql/schema-entities-idx.sql b/dao/src/main/resources/sql/schema-entities-idx.sql index 0a6d6e578d..dd37261526 100644 --- a/dao/src/main/resources/sql/schema-entities-idx.sql +++ b/dao/src/main/resources/sql/schema-entities-idx.sql @@ -73,26 +73,8 @@ 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_ota_package_external_id ON ota_package(tenant_id, external_id); - CREATE INDEX IF NOT EXISTS idx_rule_node_type_id_configuration_version ON rule_node(type, id, configuration_version); CREATE INDEX IF NOT EXISTS idx_api_usage_state_entity_id ON api_usage_state(entity_id); From a3a5fd3180cf87b1d7ca044ec0bda2215bee30f1 Mon Sep 17 00:00:00 2001 From: Andrii Landiak Date: Wed, 25 Jun 2025 16:59:31 +0300 Subject: [PATCH 44/54] Small refactoring of sendErrorResponse --- .../queue/common/PartitionedQueueResponseTemplate.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/common/PartitionedQueueResponseTemplate.java b/common/queue/src/main/java/org/thingsboard/server/queue/common/PartitionedQueueResponseTemplate.java index a15fafa19a..ef1682b97f 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/common/PartitionedQueueResponseTemplate.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/common/PartitionedQueueResponseTemplate.java @@ -131,7 +131,7 @@ public class PartitionedQueueResponseTemplate Date: Thu, 26 Jun 2025 15:49:46 +0300 Subject: [PATCH 45/54] UI: Fixed support unit conversion in range chart widgets --- .../lib/chart/range-chart-widget.component.ts | 15 +++++-------- .../lib/chart/range-chart-widget.models.ts | 22 +++++++++---------- .../lib/chart/time-series-chart.models.ts | 4 ++-- 3 files changed, 19 insertions(+), 22 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/range-chart-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/range-chart-widget.component.ts index 1d1257c2e8..c765a65b80 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/range-chart-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/range-chart-widget.component.ts @@ -49,7 +49,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({ @@ -80,8 +80,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 +99,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 +135,7 @@ export class RangeChartWidgetComponent implements OnInit, OnDestroy, AfterViewIn } ngAfterViewInit() { - const settings = rangeChartTimeSeriesSettings(this.settings, this.rangeItems, this.decimals, this.units, this.unitConvertor); + const settings = rangeChartTimeSeriesSettings(this.settings, this.rangeItems, this.decimals, this.units); this.timeSeriesChart = new TbTimeSeriesChart(this.ctx, settings, this.chartShape.nativeElement, this.renderer); } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/range-chart-widget.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/range-chart-widget.models.ts index f4210e2203..de59cc7fa3 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/range-chart-widget.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/range-chart-widget.models.ts @@ -57,6 +57,7 @@ import { import { TimeSeriesChartTooltipWidgetSettings } from '@home/components/widget/lib/chart/time-series-chart-tooltip.models'; +import { TbUnit } from '@shared/models/unit.models'; export interface RangeItem { index: number; @@ -221,13 +222,13 @@ export const rangeChartDefaultSettings: RangeChartWidgetSettings = { }; export const rangeChartTimeSeriesSettings = (settings: RangeChartWidgetSettings, rangeItems: RangeItem[], - decimals: number, units: string, valueConvertor: (x: number) => number): DeepPartial => { + decimals: number, units: TbUnit): DeepPartial => { let thresholds: DeepPartial[] = settings.showRangeThresholds ? getMarkPoints(rangeItems).map(item => ({ ...{type: ValueSourceType.constant, yAxisId: 'default', units, decimals, - value: valueConvertor(item)}, + value: item}, ...settings.rangeThreshold } as DeepPartial)) : []; if (settings.thresholds?.length) { @@ -240,10 +241,8 @@ export const rangeChartTimeSeriesSettings = (settings: RangeChartWidgetSettings, yAxes: { default: { ...settings.yAxis, - ...{ - decimals, - units - } + decimals, + units } }, xAxis: settings.xAxis, @@ -299,14 +298,15 @@ export const toRangeItems = (colorRanges: Array, valueFormat: ValueF for (let i = 0; i < ranges.length; i++) { const range = ranges[i]; let from = range.from; - const to = isDefinedAndNotNull(range.to) ? Number(valueFormat.format(range.to)) : range.to; + const to = range.to; if (i > 0) { const prevRange = ranges[i - 1]; if (isNumber(prevRange.to) && isNumber(from) && from < prevRange.to) { from = prevRange.to; } } - from = isDefinedAndNotNull(from) ? Number(valueFormat.format(from)) : from; + const formatToValue = isDefinedAndNotNull(to) ? Number(valueFormat.format(to)) : to; + const formatFromValue = isDefinedAndNotNull(from) ? Number(valueFormat.format(from)) : from; rangeItems.push( { index: counter++, @@ -315,12 +315,12 @@ export const toRangeItems = (colorRanges: Array, valueFormat: ValueF visible: true, from, to, - label: rangeItemLabel(from, to), - piece: createTimeSeriesChartVisualMapPiece(range.color, from, to) + label: rangeItemLabel(formatFromValue, formatToValue), + piece: createTimeSeriesChartVisualMapPiece(range.color, formatFromValue, formatToValue) } ); if (!isNumber(from) || !isNumber(to)) { - const value = !isNumber(from) ? to : from; + const value = !isNumber(from) ? formatToValue : formatFromValue; rangeItems.push( { index: counter++, diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.models.ts index b29f809fef..86d952f2e2 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.models.ts @@ -98,7 +98,7 @@ import { TimeSeriesChartTooltipValueFormatFunction, TimeSeriesChartTooltipWidgetSettings } from '@home/components/widget/lib/chart/time-series-chart-tooltip.models'; -import { TbUnitConverter } from '@shared/models/unit.models'; +import { TbUnit, TbUnitConverter } from '@shared/models/unit.models'; type TimeSeriesChartDataEntry = [number, any, number, number]; @@ -377,7 +377,7 @@ export type TimeSeriesChartTicksFormatter = export interface TimeSeriesChartYAxisSettings extends TimeSeriesChartAxisSettings { id?: TimeSeriesChartYAxisId; order?: number; - units?: string; + units?: TbUnit; decimals?: number; interval?: number; splitNumber?: number; From 2aa675e108ff97f52389712f2f6bb141073c33a8 Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Thu, 26 Jun 2025 15:50:10 +0300 Subject: [PATCH 46/54] UI: Add new unit m/min --- ui-ngx/src/app/shared/models/units/speed.ts | 7 ++++++- ui-ngx/src/assets/locale/locale.constant-en_US.json | 1 + 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/ui-ngx/src/app/shared/models/units/speed.ts b/ui-ngx/src/app/shared/models/units/speed.ts index 3e3bad7b03..78433c60ae 100644 --- a/ui-ngx/src/app/shared/models/units/speed.ts +++ b/ui-ngx/src/app/shared/models/units/speed.ts @@ -18,7 +18,7 @@ import { TbMeasure, TbMeasureUnits } from '@shared/models/unit.models'; export type SpeedUnits = SpeedMetricUnits | SpeedImperialUnits; -export type SpeedMetricUnits = 'm/s' | 'km/h' | 'mm/min' | 'mm/s'; +export type SpeedMetricUnits = 'm/s' | 'km/h' | 'mm/min' | 'm/min' | 'mm/s'; export type SpeedImperialUnits = 'mph' | 'kt' | 'ft/s' | 'ft/min' | 'in/s' | 'in/h'; const METRIC: TbMeasureUnits = { @@ -37,6 +37,11 @@ const METRIC: TbMeasureUnits = { 'mm/min': { name: 'unit.millimeters-per-minute', tags: ['feed rate', 'cutting feed rate'], + to_anchor: 0.00006, + }, + 'm/min': { + name: 'unit.meter-per-minute', + tags: ['velocity', 'pace'], to_anchor: 0.06, }, 'mm/s': { diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index fe07c52ea8..2c0fb7b617 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -6083,6 +6083,7 @@ "inch-per-second": "Inch per second", "inch-per-hour": "Inch per hour", "millimeters-per-minute": "Millimeters per minute", + "meter-per-minute": "Meter per minute", "kilometer-per-hour-squared": "Kilometer per hour squared", "foot-per-second-squared": "Foot per second squared", "pascal": "Pascal", From 4d4d16520dfaa8279aee7139efb3a00968af74a1 Mon Sep 17 00:00:00 2001 From: Artem Dzhereleiko Date: Mon, 30 Jun 2025 17:45:04 +0300 Subject: [PATCH 47/54] UI: Fixed detect changes for tables --- .../components/widget/lib/alarm/alarms-table-widget.component.ts | 1 + .../widget/lib/entity/entities-table-widget.component.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/alarm/alarms-table-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/alarm/alarms-table-widget.component.ts index 45825a0294..b1c0f38ef2 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/alarm/alarms-table-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/alarm/alarms-table-widget.component.ts @@ -340,6 +340,7 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit, public onDataUpdated() { this.alarmsDatasource.updateAlarms(); this.clearCache(); + this.ctx.detectChanges(); } public onEditModeChanged() { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/entity/entities-table-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/entity/entities-table-widget.component.ts index 82537384a1..f95c8c26d7 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/entity/entities-table-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/entity/entities-table-widget.component.ts @@ -275,6 +275,7 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni public onDataUpdated() { this.entityDatasource.dataUpdated(); this.clearCache(); + this.ctx.detectChanges(); } public onEditModeChanged() { From 68be1163b66a2d0d0d24447581f244afacbcf642 Mon Sep 17 00:00:00 2001 From: VIacheslavKlimov Date: Tue, 1 Jul 2025 11:09:03 +0300 Subject: [PATCH 48/54] Cleanup upgrade from 4.1 to 4.2 --- .../install/ThingsboardInstallService.java | 1 - .../DefaultDatabaseSchemaSettingsService.java | 2 +- .../update/DefaultDataUpdateService.java | 101 ++---------------- .../common/data/limit/RateLimitUtil.java | 12 --- .../DefaultTenantProfileConfiguration.java | 40 ------- 5 files changed, 7 insertions(+), 149 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java b/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java index 5e5185ac8b..6765e95246 100644 --- a/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java +++ b/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java @@ -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(); diff --git a/application/src/main/java/org/thingsboard/server/service/install/DefaultDatabaseSchemaSettingsService.java b/application/src/main/java/org/thingsboard/server/service/install/DefaultDatabaseSchemaSettingsService.java index 90c5966f3f..e5bd026fb7 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/DefaultDatabaseSchemaSettingsService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/DefaultDatabaseSchemaSettingsService.java @@ -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 SUPPORTED_VERSIONS_FOR_UPGRADE = List.of("4.0.0", "4.0.1"); + private static final List SUPPORTED_VERSIONS_FOR_UPGRADE = List.of("4.1.0"); private final ProjectInfo projectInfo; private final JdbcTemplate jdbcTemplate; diff --git a/application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java b/application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java index c3f1cee046..972d5ff36c 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java @@ -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,108 +38,27 @@ 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 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); + log.info("Data updated."); } @Override diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/limit/RateLimitUtil.java b/common/data/src/main/java/org/thingsboard/server/common/data/limit/RateLimitUtil.java index f2ee164dcd..f6e9085489 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/limit/RateLimitUtil.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/limit/RateLimitUtil.java @@ -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 distinctDurations = new HashSet<>(); - return parseConfig(configStr).stream() - .filter(entry -> distinctDurations.add(entry.durationSeconds())) - .map(RateLimitEntry::toString) - .collect(Collectors.joining(",")); - } - } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java index a4ff47c340..246fe46791 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java @@ -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); - } - } From b20b3d2b6e529cc5a10423895ee584ac8d69502f Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Tue, 1 Jul 2025 12:46:27 +0300 Subject: [PATCH 49/54] UI: Fixed range-chart widget import --- .../components/widget/lib/chart/range-chart-widget.component.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/range-chart-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/range-chart-widget.component.ts index f5ea7487ed..f471a9e84c 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/range-chart-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/range-chart-widget.component.ts @@ -23,6 +23,7 @@ import { OnDestroy, OnInit, Renderer2, + TemplateRef, ViewChild, ViewEncapsulation } from '@angular/core'; From d17f23b7efed689290cc3366ea9208ab7ca602b4 Mon Sep 17 00:00:00 2001 From: VIacheslavKlimov Date: Wed, 2 Jul 2025 12:30:45 +0300 Subject: [PATCH 50/54] Add 4.0.2 as a supported version for upgrade --- .../service/install/DefaultDatabaseSchemaSettingsService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/src/main/java/org/thingsboard/server/service/install/DefaultDatabaseSchemaSettingsService.java b/application/src/main/java/org/thingsboard/server/service/install/DefaultDatabaseSchemaSettingsService.java index 90c5966f3f..ad87242c0a 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/DefaultDatabaseSchemaSettingsService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/DefaultDatabaseSchemaSettingsService.java @@ -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 SUPPORTED_VERSIONS_FOR_UPGRADE = List.of("4.0.0", "4.0.1"); + private static final List SUPPORTED_VERSIONS_FOR_UPGRADE = List.of("4.0.0", "4.0.1", "4.0.2"); private final ProjectInfo projectInfo; private final JdbcTemplate jdbcTemplate; From fb966bded0fbdadf5ebd43d7afddb0278cf36bfb Mon Sep 17 00:00:00 2001 From: VIacheslavKlimov Date: Wed, 2 Jul 2025 12:46:50 +0300 Subject: [PATCH 51/54] Fix no type-cast for attributes; telemetry controller refactoring --- .../controller/TelemetryController.java | 133 +++++++----------- .../telemetry/BaseTelemetryProcessor.java | 4 +- .../csv/AbstractBulkImportService.java | 3 +- .../server/common/adaptor/JsonConverter.java | 8 +- .../common/adaptor/JsonConverterTest.java | 5 +- .../TbCopyAttributesToEntityViewNode.java | 8 +- .../rule/engine/profile/DeviceState.java | 4 +- .../telemetry/TbCalculatedFieldsNode.java | 3 +- .../engine/telemetry/TbMsgAttributesNode.java | 3 +- 9 files changed, 63 insertions(+), 108 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/controller/TelemetryController.java b/application/src/main/java/org/thingsboard/server/controller/TelemetryController.java index bf9713f58e..c98ae0dcb8 100644 --- a/application/src/main/java/org/thingsboard/server/controller/TelemetryController.java +++ b/application/src/main/java/org/thingsboard/server/controller/TelemetryController.java @@ -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 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 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 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 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 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 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 saveAttributes(TenantId srcTenantId, EntityId entityIdSrc, AttributeScope scope, JsonNode json) throws ThingsboardException { + private DeferredResult 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 attributes = extractRequestAttributes(json); + JsonElement json; + try { + json = JsonParser.parseString(jsonStr); + } catch (Exception e) { + return getImmediateDeferredResult("Invalid JSON", HttpStatus.BAD_REQUEST); + } + if (json.isJsonObject()) { + List 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 extractRequestAttributes(JsonNode jsonNode) { - long ts = System.currentTimeMillis(); - List 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); diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/telemetry/BaseTelemetryProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/telemetry/BaseTelemetryProcessor.java index eeaa80d6a2..068af492bc 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/telemetry/BaseTelemetryProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/telemetry/BaseTelemetryProcessor.java @@ -266,7 +266,7 @@ public abstract class BaseTelemetryProcessor extends BaseEdgeProcessor { SettableFuture futureToSet = SettableFuture.create(); JsonObject json = JsonUtils.getJsonObject(msg.getKvList()); AttributeScope scope = AttributeScope.valueOf(metaData.getValue(DataConstants.SCOPE)); - List attributes = new ArrayList<>(JsonConverter.convertToAttributes(json, ts)); + List attributes = JsonConverter.convertToAttributes(json, ts); ListenableFuture> future = filterAttributesByTs(tenantId, entityId, scope, attributes); Futures.addCallback(future, new FutureCallback<>() { @Override @@ -314,7 +314,7 @@ public abstract class BaseTelemetryProcessor extends BaseEdgeProcessor { SettableFuture futureToSet = SettableFuture.create(); JsonObject json = JsonUtils.getJsonObject(msg.getKvList()); AttributeScope scope = AttributeScope.valueOf(metaData.getValue(DataConstants.SCOPE)); - List attributes = new ArrayList<>(JsonConverter.convertToAttributes(json, ts)); + List attributes = JsonConverter.convertToAttributes(json, ts); ListenableFuture> future = filterAttributesByTs(tenantId, entityId, scope, attributes); Futures.addCallback(future, new FutureCallback<>() { @Override diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/csv/AbstractBulkImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/csv/AbstractBulkImportService.java index 3ea50ea327..9850e2d1a1 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/csv/AbstractBulkImportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/csv/AbstractBulkImportService.java @@ -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 kvsEntry, BulkImportColumnType kvType) { String scope = kvType.getKey(); - List attributes = new ArrayList<>(JsonConverter.convertToAttributes(kvsEntry.getValue())); + List attributes = JsonConverter.convertToAttributes(kvsEntry.getValue()); accessValidator.validateEntityAndCallback(user, Operation.WRITE_ATTRIBUTES, entity.getId(), (result, tenantId, entityId) -> { tsSubscriptionService.saveAttributes(AttributesSaveRequest.builder() diff --git a/common/proto/src/main/java/org/thingsboard/server/common/adaptor/JsonConverter.java b/common/proto/src/main/java/org/thingsboard/server/common/adaptor/JsonConverter.java index 2a208923d9..6518cfc3fe 100644 --- a/common/proto/src/main/java/org/thingsboard/server/common/adaptor/JsonConverter.java +++ b/common/proto/src/main/java/org/thingsboard/server/common/adaptor/JsonConverter.java @@ -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 convertToAttributes(JsonElement element) { + public static List convertToAttributes(JsonElement element) { long ts = System.currentTimeMillis(); return convertToAttributes(element, ts); } - public static Set convertToAttributes(JsonElement element, long ts) { - return new HashSet<>(parseValues(element.getAsJsonObject()).stream().map(kv -> new BaseAttributeKvEntry(kv, ts)).toList()); + public static List convertToAttributes(JsonElement element, long ts) { + return parseValues(element.getAsJsonObject()).stream().map(kv -> new BaseAttributeKvEntry(kv, ts)).toList(); } private static List parseValues(JsonObject valuesObject) { diff --git a/common/proto/src/test/java/org/thingsboard/server/common/adaptor/JsonConverterTest.java b/common/proto/src/test/java/org/thingsboard/server/common/adaptor/JsonConverterTest.java index 28b65a7be8..38ef604ab0 100644 --- a/common/proto/src/test/java/org/thingsboard/server/common/adaptor/JsonConverterTest.java +++ b/common/proto/src/test/java/org/thingsboard/server/common/adaptor/JsonConverterTest.java @@ -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); }); } + } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCopyAttributesToEntityViewNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCopyAttributesToEntityViewNode.java index 7e3c08cfcc..2d74929242 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCopyAttributesToEntityViewNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCopyAttributesToEntityViewNode.java @@ -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 attributes = JsonConverter.convertToAttributes(JsonParser.parseString(msg.getData())); - List filteredAttributes = - attributes.stream().filter(attr -> attributeContainsInEntityView(scope, attr.getKey(), entityView)).collect(Collectors.toList()); + List 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()); } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/DeviceState.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/DeviceState.java index 193c179df2..4bd81050db 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/DeviceState.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/DeviceState.java @@ -258,7 +258,7 @@ class DeviceState { private boolean processAttributes(TbContext ctx, TbMsg msg, String scope) throws ExecutionException, InterruptedException { boolean stateChanged = false; - Set attributes = JsonConverter.convertToAttributes(JsonParser.parseString(msg.getData())); + List 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 attributes, String scope) { + private SnapshotUpdate merge(DataSnapshot latestValues, List attributes, String scope) { long newTs = 0; Set keys = new HashSet<>(); for (AttributeKvEntry entry : attributes) { diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbCalculatedFieldsNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbCalculatedFieldsNode.java index 4e319500d1..e703e9dd25 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbCalculatedFieldsNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbCalculatedFieldsNode.java @@ -103,8 +103,7 @@ public class TbCalculatedFieldsNode implements TbNode { } private void processPostAttributesRequest(TbContext ctx, TbMsg msg) { - List newAttributes = new ArrayList<>(JsonConverter.convertToAttributes(JsonParser.parseString(msg.getData()))); - + List newAttributes = JsonConverter.convertToAttributes(JsonParser.parseString(msg.getData())); if (newAttributes.isEmpty()) { ctx.tellSuccess(msg); return; diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgAttributesNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgAttributesNode.java index c04f5b474d..280d8de824 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgAttributesNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgAttributesNode.java @@ -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 newAttributes = new ArrayList<>(JsonConverter.convertToAttributes(JsonParser.parseString(src))); + List newAttributes = JsonConverter.convertToAttributes(JsonParser.parseString(src)); if (newAttributes.isEmpty()) { ctx.tellSuccess(msg); return; From f59b6c95e48d13cc2c91f12ca447457417a2eb60 Mon Sep 17 00:00:00 2001 From: IrynaMatveieva Date: Fri, 4 Jul 2025 13:40:44 +0300 Subject: [PATCH 52/54] updated tests for geofencing unility methods --- .../server/service/script/TbelInvokeDocsIoTest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/application/src/test/java/org/thingsboard/server/service/script/TbelInvokeDocsIoTest.java b/application/src/test/java/org/thingsboard/server/service/script/TbelInvokeDocsIoTest.java index a90f9f19b0..c283a791fd 100644 --- a/application/src/test/java/org/thingsboard/server/service/script/TbelInvokeDocsIoTest.java +++ b/application/src/test/java/org/thingsboard/server/service/script/TbelInvokeDocsIoTest.java @@ -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) From 83eef548bbf128cb69b7b510dd2a32caeedd1ed9 Mon Sep 17 00:00:00 2001 From: imbeacon Date: Mon, 7 Jul 2025 14:45:14 +0300 Subject: [PATCH 53/54] Fixed issue that leads to double counting for devices connected through gateway --- .../limits/DefaultTransportRateLimitService.java | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/limits/DefaultTransportRateLimitService.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/limits/DefaultTransportRateLimitService.java index a5ac06bfb6..d03a3d03f2 100644 --- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/limits/DefaultTransportRateLimitService.java +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/limits/DefaultTransportRateLimitService.java @@ -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; } From b6b30dbba9d50d372aa60adf35f1b2038caa3f03 Mon Sep 17 00:00:00 2001 From: Dmytro Skarzhynets Date: Wed, 9 Jul 2025 15:37:52 +0300 Subject: [PATCH 54/54] AI rule node: rename AI model settings to AI model --- .../server/actors/ActorSystemContext.java | 8 +- .../actors/ruleChain/DefaultTbContext.java | 12 +- .../server/controller/AiModelController.java | 122 +++++++++- .../controller/AiModelSettingsController.java | 143 ------------ .../server/controller/BaseController.java | 22 +- .../controller/ControllerConstants.java | 2 +- ...elService.java => AiChatModelService.java} | 4 +- ...eImpl.java => AiChatModelServiceImpl.java} | 9 +- .../Langchain4jChatModelConfigurerImpl.java | 214 +++++++++--------- .../edge/EdgeEventSourcingListener.java | 4 +- .../edge/RelatedEdgesSourcingListener.java | 2 +- ...vice.java => DefaultTbAiModelService.java} | 36 +-- ...ingsService.java => TbAiModelService.java} | 8 +- .../service/security/permission/Resource.java | 2 +- .../permission/TenantAdminPermissions.java | 10 +- .../DefaultEntitiesExportImportService.java | 2 +- ...Service.java => AiModelExportService.java} | 8 +- ...Service.java => AiModelImportService.java} | 44 ++-- .../src/main/resources/thingsboard.yml | 8 +- ...ttingsService.java => AiModelService.java} | 18 +- .../server/common/data/CacheConstants.java | 4 +- .../server/common/data/EntityType.java | 4 +- .../ai/{AiModelSettings.java => AiModel.java} | 38 ++-- .../common/data/ai/dto/TbChatRequest.java | 4 +- .../server/common/data/ai/model/AiModel.java | 84 ------- .../common/data/ai/model/AiModelConfig.java | 60 ++++- .../data/ai/model/chat/AiChatModel.java | 41 ---- .../data/ai/model/chat/AiChatModelConfig.java | 15 +- ...java => AmazonBedrockChatModelConfig.java} | 21 +- ...del.java => AnthropicChatModelConfig.java} | 23 +- ...l.java => AzureOpenAiChatModelConfig.java} | 25 +- ....java => GitHubModelsChatModelConfig.java} | 25 +- ...ava => GoogleAiGeminiChatModelConfig.java} | 27 +-- ... GoogleVertexAiGeminiChatModelConfig.java} | 27 +-- .../chat/Langchain4jChatModelConfigurer.java | 16 +- ...del.java => MistralAiChatModelConfig.java} | 25 +- ...tModel.java => OpenAiChatModelConfig.java} | 25 +- ...{AiModelSettingsId.java => AiModelId.java} | 16 +- .../common/data/id/EntityIdFactory.java | 2 +- .../server/common/data/sync/JsonTbEntity.java | 4 +- common/proto/src/main/proto/queue.proto | 2 +- ...Event.java => AiModelCacheEvictEvent.java} | 16 +- ...ingsCacheKey.java => AiModelCacheKey.java} | 18 +- ...neCache.java => AiModelCaffeineCache.java} | 10 +- ...iModelSettingsDao.java => AiModelDao.java} | 14 +- ...RedisCache.java => AiModelRedisCache.java} | 10 +- .../server/dao/ai/AiModelServiceImpl.java | 149 ++++++++++++ .../dao/ai/AiModelSettingsServiceImpl.java | 149 ------------ .../dao/housekeeper/CleanUpService.java | 2 +- .../server/dao/model/ModelConstants.java | 10 +- ...SettingsEntity.java => AiModelEntity.java} | 54 ++--- ...lidator.java => AiModelDataValidator.java} | 36 +-- ...Repository.java => AiModelRepository.java} | 40 ++-- ...delSettingsDao.java => JpaAiModelDao.java} | 66 +++--- .../server/dao/tenant/TenantServiceImpl.java | 2 +- .../resources/sql/schema-entities-idx.sql | 2 +- .../main/resources/sql/schema-entities.sql | 6 +- ...java => RuleEngineAiChatModelService.java} | 5 +- .../rule/engine/api/TbContext.java | 6 +- .../thingsboard/rule/engine/ai/TbAiNode.java | 43 ++-- .../rule/engine/ai/TbAiNodeConfiguration.java | 4 +- .../rule/engine/util/TenantIdLoader.java | 6 +- .../rule/engine/util/TenantIdLoaderTest.java | 16 +- 63 files changed, 848 insertions(+), 982 deletions(-) delete mode 100644 application/src/main/java/org/thingsboard/server/controller/AiModelSettingsController.java rename application/src/main/java/org/thingsboard/server/service/ai/{AiModelService.java => AiChatModelService.java} (82%) rename application/src/main/java/org/thingsboard/server/service/ai/{AiModelServiceImpl.java => AiChatModelServiceImpl.java} (79%) rename application/src/main/java/org/thingsboard/server/service/entitiy/ai/{DefaultTbAiModelSettingsService.java => DefaultTbAiModelService.java} (54%) rename application/src/main/java/org/thingsboard/server/service/entitiy/ai/{TbAiModelSettingsService.java => TbAiModelService.java} (76%) rename application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/{AiModelSettingsExportService.java => AiModelExportService.java} (76%) rename application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/{AiModelSettingsImportService.java => AiModelImportService.java} (50%) rename common/dao-api/src/main/java/org/thingsboard/server/dao/ai/{AiModelSettingsService.java => AiModelService.java} (55%) rename common/data/src/main/java/org/thingsboard/server/common/data/ai/{AiModelSettings.java => AiModel.java} (67%) delete mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/ai/model/AiModel.java delete mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AiChatModel.java rename common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/{AmazonBedrockChatModel.java => AmazonBedrockChatModelConfig.java} (75%) rename common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/{AnthropicChatModel.java => AnthropicChatModelConfig.java} (74%) rename common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/{AzureOpenAiChatModel.java => AzureOpenAiChatModelConfig.java} (72%) rename common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/{GitHubModelsChatModel.java => GitHubModelsChatModelConfig.java} (72%) rename common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/{GoogleAiGeminiChatModel.java => GoogleAiGeminiChatModelConfig.java} (71%) rename common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/{GoogleVertexAiGeminiChatModel.java => GoogleVertexAiGeminiChatModelConfig.java} (71%) rename common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/{MistralAiChatModel.java => MistralAiChatModelConfig.java} (72%) rename common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/{OpenAiChatModel.java => OpenAiChatModelConfig.java} (73%) rename common/data/src/main/java/org/thingsboard/server/common/data/id/{AiModelSettingsId.java => AiModelId.java} (72%) rename dao/src/main/java/org/thingsboard/server/dao/ai/{AiModelSettingsCacheEvictEvent.java => AiModelCacheEvictEvent.java} (58%) rename dao/src/main/java/org/thingsboard/server/dao/ai/{AiModelSettingsCacheKey.java => AiModelCacheKey.java} (68%) rename dao/src/main/java/org/thingsboard/server/dao/ai/{AiModelSettingsCaffeineCache.java => AiModelCaffeineCache.java} (75%) rename dao/src/main/java/org/thingsboard/server/dao/ai/{AiModelSettingsDao.java => AiModelDao.java} (60%) rename dao/src/main/java/org/thingsboard/server/dao/ai/{AiModelSettingsRedisCache.java => AiModelRedisCache.java} (71%) create mode 100644 dao/src/main/java/org/thingsboard/server/dao/ai/AiModelServiceImpl.java delete mode 100644 dao/src/main/java/org/thingsboard/server/dao/ai/AiModelSettingsServiceImpl.java rename dao/src/main/java/org/thingsboard/server/dao/model/sql/{AiModelSettingsEntity.java => AiModelEntity.java} (62%) rename dao/src/main/java/org/thingsboard/server/dao/service/validator/{AiModelSettingsDataValidator.java => AiModelDataValidator.java} (51%) rename dao/src/main/java/org/thingsboard/server/dao/sql/ai/{AiModelSettingsRepository.java => AiModelRepository.java} (55%) rename dao/src/main/java/org/thingsboard/server/dao/sql/ai/{JpaAiModelSettingsDao.java => JpaAiModelDao.java} (50%) rename rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/{RuleEngineAiModelService.java => RuleEngineAiChatModelService.java} (83%) diff --git a/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java b/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java index cd4a88314b..ea46ce86eb 100644 --- a/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java +++ b/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java @@ -35,7 +35,7 @@ import org.thingsboard.rule.engine.api.JobManager; import org.thingsboard.rule.engine.api.MailService; import org.thingsboard.rule.engine.api.MqttClientSettings; import org.thingsboard.rule.engine.api.NotificationCenter; -import org.thingsboard.rule.engine.api.RuleEngineAiModelService; +import org.thingsboard.rule.engine.api.RuleEngineAiChatModelService; import org.thingsboard.rule.engine.api.SmsService; import org.thingsboard.rule.engine.api.notification.SlackService; import org.thingsboard.rule.engine.api.sms.SmsSenderFactory; @@ -63,7 +63,7 @@ import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.common.msg.tools.TbRateLimits; import org.thingsboard.server.common.stats.TbApiUsageReportClient; -import org.thingsboard.server.dao.ai.AiModelSettingsService; +import org.thingsboard.server.dao.ai.AiModelService; import org.thingsboard.server.dao.alarm.AlarmCommentService; import org.thingsboard.server.dao.asset.AssetProfileService; import org.thingsboard.server.dao.asset.AssetService; @@ -315,11 +315,11 @@ public class ActorSystemContext { @Autowired @Getter - private RuleEngineAiModelService aiModelService; + private RuleEngineAiChatModelService aiChatModelService; @Autowired @Getter - private AiModelSettingsService aiModelSettingsService; + private AiModelService aiModelService; @Autowired @Getter diff --git a/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java b/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java index b4235c7262..6374e4016d 100644 --- a/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java +++ b/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java @@ -28,7 +28,7 @@ import org.thingsboard.rule.engine.api.JobManager; import org.thingsboard.rule.engine.api.MailService; import org.thingsboard.rule.engine.api.MqttClientSettings; import org.thingsboard.rule.engine.api.NotificationCenter; -import org.thingsboard.rule.engine.api.RuleEngineAiModelService; +import org.thingsboard.rule.engine.api.RuleEngineAiChatModelService; import org.thingsboard.rule.engine.api.RuleEngineAlarmService; import org.thingsboard.rule.engine.api.RuleEngineApiUsageStateService; import org.thingsboard.rule.engine.api.RuleEngineAssetProfileCache; @@ -77,7 +77,7 @@ import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.common.msg.TbMsgProcessingStackItem; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; -import org.thingsboard.server.dao.ai.AiModelSettingsService; +import org.thingsboard.server.dao.ai.AiModelService; import org.thingsboard.server.dao.alarm.AlarmCommentService; import org.thingsboard.server.dao.asset.AssetProfileService; import org.thingsboard.server.dao.asset.AssetService; @@ -1027,13 +1027,13 @@ public class DefaultTbContext implements TbContext { } @Override - public RuleEngineAiModelService getAiModelService() { - return mainCtx.getAiModelService(); + public RuleEngineAiChatModelService getAiChatModelService() { + return mainCtx.getAiChatModelService(); } @Override - public AiModelSettingsService getAiModelSettingsService() { - return mainCtx.getAiModelSettingsService(); + public AiModelService getAiModelService() { + return mainCtx.getAiModelService(); } @Override diff --git a/application/src/main/java/org/thingsboard/server/controller/AiModelController.java b/application/src/main/java/org/thingsboard/server/controller/AiModelController.java index c7dfce90bc..5a00e26f17 100644 --- a/application/src/main/java/org/thingsboard/server/controller/AiModelController.java +++ b/application/src/main/java/org/thingsboard/server/controller/AiModelController.java @@ -17,31 +17,141 @@ package org.thingsboard.server.controller; import com.google.common.util.concurrent.ListenableFuture; import dev.langchain4j.model.chat.request.ChatRequest; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +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.RequestParam; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.context.request.async.DeferredResult; +import org.thingsboard.server.common.data.ai.AiModel; import org.thingsboard.server.common.data.ai.dto.TbChatRequest; import org.thingsboard.server.common.data.ai.dto.TbChatResponse; -import org.thingsboard.server.common.data.ai.model.chat.AiChatModel; +import org.thingsboard.server.common.data.ai.model.chat.AiChatModelConfig; +import org.thingsboard.server.common.data.exception.ThingsboardException; +import org.thingsboard.server.common.data.id.AiModelId; +import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.config.annotations.ApiOperation; -import org.thingsboard.server.service.ai.AiModelService; +import org.thingsboard.server.service.ai.AiChatModelService; +import org.thingsboard.server.service.security.permission.Operation; +import org.thingsboard.server.service.security.permission.Resource; import java.time.Duration; +import java.util.Optional; +import java.util.UUID; import static com.google.common.util.concurrent.MoreExecutors.directExecutor; +import static org.thingsboard.server.controller.ControllerConstants.AI_MODEL_TEXT_SEARCH_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.PAGE_DATA_PARAMETERS; +import static org.thingsboard.server.controller.ControllerConstants.PAGE_NUMBER_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.PAGE_SIZE_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.SORT_ORDER_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.SORT_PROPERTY_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.TENANT_AUTHORITY_PARAGRAPH; +@Validated @RestController @RequiredArgsConstructor @RequestMapping("/api/ai/model") class AiModelController extends BaseController { - private final AiModelService aiModelService; + private final AiChatModelService aiChatModelService; + + @ApiOperation( + value = "Create or update AI model (saveAiModel)", + notes = "Creates or updates an AI model record.\n\n" + + "• **Create:** Omit the `id` to create a new record. The platform assigns a UUID to the new record and returns it in the `id` field of the response.\n\n" + + "• **Update:** Include an existing `id` to modify that record. If no matching record exists, the API responds with **404 Not Found**.\n\n" + + "Tenant ID for the AI model will be taken from the authenticated user making the request, regardless of any value provided in the request body." + + TENANT_AUTHORITY_PARAGRAPH + ) + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @PostMapping + public AiModel saveAiModel(@RequestBody @Valid AiModel model) throws ThingsboardException { + var user = getCurrentUser(); + model.setTenantId(user.getTenantId()); + checkEntity(model.getId(), model, Resource.AI_MODEL); + return tbAiModelService.save(model, user); + } + + @ApiOperation( + value = "Get AI model by ID (getAiModelById)", + notes = "Fetches an AI model record by its `id`." + + TENANT_AUTHORITY_PARAGRAPH + ) + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @GetMapping("/{modelUuid}") + public AiModel getAiModelById( + @Parameter( + description = "ID of the AI model record", + required = true, + example = "de7900d4-30e2-11f0-9cd2-0242ac120002" + ) + @PathVariable UUID modelUuid + ) throws ThingsboardException { + return checkAiModelId(new AiModelId(modelUuid), Operation.READ); + } + + @ApiOperation( + value = "Get AI models (getAiModels)", + notes = "Returns a page of AI models. " + + PAGE_DATA_PARAMETERS + TENANT_AUTHORITY_PARAGRAPH + ) + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @GetMapping + public PageData getAiModels( + @Parameter(description = PAGE_SIZE_DESCRIPTION, required = true) + @RequestParam int pageSize, + @Parameter(description = PAGE_NUMBER_DESCRIPTION, required = true) + @RequestParam int page, + @Parameter(description = AI_MODEL_TEXT_SEARCH_DESCRIPTION) + @RequestParam(required = false) String textSearch, + @Parameter(description = SORT_PROPERTY_DESCRIPTION, schema = @Schema(allowableValues = {"createdTime", "name", "provider", "modelId"})) + @RequestParam(required = false) String sortProperty, + @Parameter(description = SORT_ORDER_DESCRIPTION, schema = @Schema(allowableValues = {"ASC", "DESC"})) + @RequestParam(required = false) String sortOrder + ) throws ThingsboardException { + var user = getCurrentUser(); + accessControlService.checkPermission(user, Resource.AI_MODEL, Operation.READ); + var pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); + return aiModelService.findAiModelsByTenantId(user.getTenantId(), pageLink); + } + + @ApiOperation( + value = "Delete AI model by ID (deleteAiModelById)", + notes = "Deletes the AI model record by its `id`. " + + "If a record with the specified `id` exists, the record is deleted and the endpoint returns `true`. " + + "If no such record exists, the endpoint returns `false`." + + TENANT_AUTHORITY_PARAGRAPH + ) + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @DeleteMapping("/{modelUuid}") + public boolean deleteAiModelById( + @Parameter( + description = "ID of the AI model record", + required = true, + example = "de7900d4-30e2-11f0-9cd2-0242ac120002" + ) + @PathVariable UUID modelUuid + ) throws ThingsboardException { + var user = getCurrentUser(); + var modelId = new AiModelId(modelUuid); + accessControlService.checkPermission(user, Resource.AI_MODEL, Operation.DELETE); + Optional toDelete = aiModelService.findAiModelByTenantIdAndId(user.getTenantId(), modelId); + if (toDelete.isEmpty()) { + return false; + } + accessControlService.checkPermission(user, Resource.AI_MODEL, Operation.DELETE, modelId, toDelete.get()); + return tbAiModelService.delete(toDelete.get(), user); + } @ApiOperation( value = "Send request to AI chat model (sendChatRequest)", @@ -53,13 +163,13 @@ class AiModelController extends BaseController { @PostMapping("/chat") public DeferredResult sendChatRequest(@Valid @RequestBody TbChatRequest tbChatRequest) { ChatRequest langChainChatRequest = tbChatRequest.toLangChainChatRequest(); - AiChatModel chatModel = tbChatRequest.chatModel(); + AiChatModelConfig chatModelConfig = tbChatRequest.chatModelConfig(); - ListenableFuture future = aiModelService.sendChatRequestAsync(chatModel, langChainChatRequest) + ListenableFuture future = aiChatModelService.sendChatRequestAsync(chatModelConfig, langChainChatRequest) .transform(chatResponse -> (TbChatResponse) new TbChatResponse.Success(chatResponse.aiMessage().text()), directExecutor()) .catching(Throwable.class, ex -> new TbChatResponse.Failure(ex.getMessage()), directExecutor()); - Integer requestTimeoutSeconds = chatModel.modelConfig().timeoutSeconds(); + Integer requestTimeoutSeconds = chatModelConfig.timeoutSeconds(); return requestTimeoutSeconds != null ? wrapFuture(future, Duration.ofSeconds(requestTimeoutSeconds).toMillis()) : wrapFuture(future); } diff --git a/application/src/main/java/org/thingsboard/server/controller/AiModelSettingsController.java b/application/src/main/java/org/thingsboard/server/controller/AiModelSettingsController.java deleted file mode 100644 index b729d9f11e..0000000000 --- a/application/src/main/java/org/thingsboard/server/controller/AiModelSettingsController.java +++ /dev/null @@ -1,143 +0,0 @@ -/** - * 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.controller; - -import io.swagger.v3.oas.annotations.Parameter; -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.Valid; -import org.springframework.security.access.prepost.PreAuthorize; -import org.springframework.validation.annotation.Validated; -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.RequestParam; -import org.springframework.web.bind.annotation.RestController; -import org.thingsboard.server.common.data.ai.AiModelSettings; -import org.thingsboard.server.common.data.exception.ThingsboardException; -import org.thingsboard.server.common.data.id.AiModelSettingsId; -import org.thingsboard.server.common.data.page.PageData; -import org.thingsboard.server.config.annotations.ApiOperation; -import org.thingsboard.server.service.security.permission.Operation; -import org.thingsboard.server.service.security.permission.Resource; - -import java.util.Optional; -import java.util.UUID; - -import static org.thingsboard.server.controller.ControllerConstants.AI_MODEL_SETTINGS_TEXT_SEARCH_DESCRIPTION; -import static org.thingsboard.server.controller.ControllerConstants.PAGE_DATA_PARAMETERS; -import static org.thingsboard.server.controller.ControllerConstants.PAGE_NUMBER_DESCRIPTION; -import static org.thingsboard.server.controller.ControllerConstants.PAGE_SIZE_DESCRIPTION; -import static org.thingsboard.server.controller.ControllerConstants.SORT_ORDER_DESCRIPTION; -import static org.thingsboard.server.controller.ControllerConstants.SORT_PROPERTY_DESCRIPTION; -import static org.thingsboard.server.controller.ControllerConstants.TENANT_AUTHORITY_PARAGRAPH; - -@Validated -@RestController -@RequestMapping("/api/ai/model/settings") -class AiModelSettingsController extends BaseController { - - @ApiOperation( - value = "Create or update AI model settings (saveAiModelSettings)", - notes = "Creates or updates an AI model settings record.\n\n" + - "• **Create:** Omit the `id` to create a new record. The platform assigns a UUID to the new settings and returns it in the `id` field of the response.\n\n" + - "• **Update:** Include an existing `id` to modify that record. If no matching record exists, the API responds with **404 Not Found**.\n\n" + - "Tenant ID for the AI model settings will be taken from the authenticated user making the request, regardless of any value provided in the request body." + - TENANT_AUTHORITY_PARAGRAPH - ) - @PreAuthorize("hasAuthority('TENANT_ADMIN')") - @PostMapping - public AiModelSettings saveAiModelSettings(@RequestBody @Valid AiModelSettings settings) throws ThingsboardException { - var user = getCurrentUser(); - settings.setTenantId(user.getTenantId()); - checkEntity(settings.getId(), settings, Resource.AI_MODEL_SETTINGS); - return tbAiModelSettingsService.save(settings, user); - } - - @ApiOperation( - value = "Get AI model settings by ID (getAiModelSettingsById)", - notes = "Fetches an AI model settings record by its `id`." + - TENANT_AUTHORITY_PARAGRAPH - ) - @PreAuthorize("hasAuthority('TENANT_ADMIN')") - @GetMapping("/{settingsUuid}") - public AiModelSettings getAiModelSettingsById( - @Parameter( - description = "ID of the AI model settings record", - required = true, - example = "de7900d4-30e2-11f0-9cd2-0242ac120002" - ) - @PathVariable UUID settingsUuid - ) throws ThingsboardException { - return checkAiModelSettingsId(new AiModelSettingsId(settingsUuid), Operation.READ); - } - - @ApiOperation( - value = "Get AI model settings (getAiModelSettings)", - notes = "Returns a page of AI model settings. " + - PAGE_DATA_PARAMETERS + TENANT_AUTHORITY_PARAGRAPH - ) - @PreAuthorize("hasAuthority('TENANT_ADMIN')") - @GetMapping - public PageData getAiModelSettings( - @Parameter(description = PAGE_SIZE_DESCRIPTION, required = true) - @RequestParam int pageSize, - @Parameter(description = PAGE_NUMBER_DESCRIPTION, required = true) - @RequestParam int page, - @Parameter(description = AI_MODEL_SETTINGS_TEXT_SEARCH_DESCRIPTION) - @RequestParam(required = false) String textSearch, - @Parameter(description = SORT_PROPERTY_DESCRIPTION, schema = @Schema(allowableValues = {"createdTime", "name", "provider", "modelId"})) - @RequestParam(required = false) String sortProperty, - @Parameter(description = SORT_ORDER_DESCRIPTION, schema = @Schema(allowableValues = {"ASC", "DESC"})) - @RequestParam(required = false) String sortOrder - ) throws ThingsboardException { - var user = getCurrentUser(); - accessControlService.checkPermission(user, Resource.AI_MODEL_SETTINGS, Operation.READ); - var pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); - return aiModelSettingsService.findAiModelSettingsByTenantId(user.getTenantId(), pageLink); - } - - @ApiOperation( - value = "Delete AI model settings by ID (deleteAiModelSettingsById)", - notes = "Deletes the AI model settings record by its `id`. " + - "If a record with the specified `id` exists, the record is deleted and the endpoint returns `true`. " + - "If no such record exists, the endpoint returns `false`." + - TENANT_AUTHORITY_PARAGRAPH - ) - @PreAuthorize("hasAuthority('TENANT_ADMIN')") - @DeleteMapping("/{settingsUuid}") - public boolean deleteAiModelSettingsById( - @Parameter( - description = "ID of the AI model settings record", - required = true, - example = "de7900d4-30e2-11f0-9cd2-0242ac120002" - ) - @PathVariable UUID settingsUuid - ) throws ThingsboardException { - var user = getCurrentUser(); - var settingsId = new AiModelSettingsId(settingsUuid); - accessControlService.checkPermission(user, Resource.AI_MODEL_SETTINGS, Operation.DELETE); - Optional toDelete = aiModelSettingsService.findAiModelSettingsByTenantIdAndId(user.getTenantId(), settingsId); - if (toDelete.isEmpty()) { - return false; - } - accessControlService.checkPermission(user, Resource.AI_MODEL_SETTINGS, Operation.DELETE, settingsId, toDelete.get()); - return tbAiModelSettingsService.delete(toDelete.get(), user); - } - -} diff --git a/application/src/main/java/org/thingsboard/server/controller/BaseController.java b/application/src/main/java/org/thingsboard/server/controller/BaseController.java index 0ccd01bb4f..61dcd76b32 100644 --- a/application/src/main/java/org/thingsboard/server/controller/BaseController.java +++ b/application/src/main/java/org/thingsboard/server/controller/BaseController.java @@ -61,7 +61,7 @@ import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.TenantInfo; import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.User; -import org.thingsboard.server.common.data.ai.AiModelSettings; +import org.thingsboard.server.common.data.ai.AiModel; import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.alarm.AlarmComment; import org.thingsboard.server.common.data.alarm.AlarmInfo; @@ -76,7 +76,7 @@ import org.thingsboard.server.common.data.edge.EdgeInfo; import org.thingsboard.server.common.data.exception.EntityVersionMismatchException; import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; import org.thingsboard.server.common.data.exception.ThingsboardException; -import org.thingsboard.server.common.data.id.AiModelSettingsId; +import org.thingsboard.server.common.data.id.AiModelId; import org.thingsboard.server.common.data.id.AlarmCommentId; import org.thingsboard.server.common.data.id.AlarmId; import org.thingsboard.server.common.data.id.AssetId; @@ -131,7 +131,7 @@ import org.thingsboard.server.common.data.util.ThrowingBiFunction; import org.thingsboard.server.common.data.widget.WidgetTypeDetails; import org.thingsboard.server.common.data.widget.WidgetTypeInfo; import org.thingsboard.server.common.data.widget.WidgetsBundle; -import org.thingsboard.server.dao.ai.AiModelSettingsService; +import org.thingsboard.server.dao.ai.AiModelService; import org.thingsboard.server.dao.alarm.AlarmCommentService; import org.thingsboard.server.dao.asset.AssetProfileService; import org.thingsboard.server.dao.asset.AssetService; @@ -178,7 +178,7 @@ import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.action.EntityActionService; import org.thingsboard.server.service.component.ComponentDiscoveryService; import org.thingsboard.server.service.entitiy.TbLogEntityActionService; -import org.thingsboard.server.service.entitiy.ai.TbAiModelSettingsService; +import org.thingsboard.server.service.entitiy.ai.TbAiModelService; import org.thingsboard.server.service.entitiy.user.TbUserSettingsService; import org.thingsboard.server.service.ota.OtaPackageStateService; import org.thingsboard.server.service.profile.TbAssetProfileCache; @@ -383,10 +383,10 @@ public abstract class BaseController { protected CalculatedFieldService calculatedFieldService; @Autowired - protected AiModelSettingsService aiModelSettingsService; + protected AiModelService aiModelService; @Autowired - protected TbAiModelSettingsService tbAiModelSettingsService; + protected TbAiModelService tbAiModelService; @Value("${server.log_controller_error_stack_trace}") @Getter @@ -400,7 +400,7 @@ public abstract class BaseController { public void handleControllerException(Exception e, HttpServletResponse response) { ThingsboardException thingsboardException = handleException(e); if (thingsboardException.getErrorCode() == ThingsboardErrorCode.GENERAL && thingsboardException.getCause() instanceof Exception - && StringUtils.equals(thingsboardException.getCause().getMessage(), thingsboardException.getMessage())) { + && StringUtils.equals(thingsboardException.getCause().getMessage(), thingsboardException.getMessage())) { e = (Exception) thingsboardException.getCause(); } else { e = thingsboardException; @@ -448,7 +448,7 @@ public abstract class BaseController { if (exception instanceof ThingsboardException) { return (ThingsboardException) exception; } else if (exception instanceof IllegalArgumentException || exception instanceof IncorrectParameterException - || exception instanceof DataValidationException || cause instanceof IncorrectParameterException) { + || exception instanceof DataValidationException || cause instanceof IncorrectParameterException) { return new ThingsboardException(exception.getMessage(), ThingsboardErrorCode.BAD_REQUEST_PARAMS); } else if (exception instanceof MessagingException) { return new ThingsboardException("Unable to send mail", ThingsboardErrorCode.GENERAL); @@ -644,7 +644,7 @@ public abstract class BaseController { case MOBILE_APP -> checkMobileAppId(new MobileAppId(entityId.getId()), operation); case MOBILE_APP_BUNDLE -> checkMobileAppBundleId(new MobileAppBundleId(entityId.getId()), operation); case CALCULATED_FIELD -> checkCalculatedFieldId(new CalculatedFieldId(entityId.getId()), operation); - case AI_MODEL_SETTINGS -> checkAiModelSettingsId(new AiModelSettingsId(entityId.getId()), operation); + case AI_MODEL -> checkAiModelId(new AiModelId(entityId.getId()), operation); default -> (HasId) checkEntityId(entityId, entitiesService::findEntityByTenantIdAndId, operation); }; } catch (Exception e) { @@ -848,8 +848,8 @@ public abstract class BaseController { return checkEntityId(jobId, jobService::findJobById, operation); } - AiModelSettings checkAiModelSettingsId(AiModelSettingsId settingsId, Operation operation) throws ThingsboardException { - return checkEntityId(settingsId, (tenantId, id) -> aiModelSettingsService.findAiModelSettingsByTenantIdAndId(tenantId, id).orElse(null), operation); + AiModel checkAiModelId(AiModelId settingsId, Operation operation) throws ThingsboardException { + return checkEntityId(settingsId, (tenantId, id) -> aiModelService.findAiModelByTenantIdAndId(tenantId, id).orElse(null), operation); } protected I emptyId(EntityType entityType) { diff --git a/application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java b/application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java index 20e350f0b8..a87864726b 100644 --- a/application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java +++ b/application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java @@ -90,7 +90,7 @@ public class ControllerConstants { protected static final String TENANT_PROFILE_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'substring' filter based on the tenant profile name."; protected static final String RULE_CHAIN_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'substring' filter based on the rule chain name."; protected static final String DEVICE_PROFILE_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'substring' filter based on the device profile name."; - protected static final String AI_MODEL_SETTINGS_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'substring' filter based on the AI model settings name, provider and model ID."; + protected static final String AI_MODEL_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'substring' filter based on the AI model name, provider and model ID."; protected static final String ASSET_PROFILE_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'substring' filter based on the asset profile name."; protected static final String CUSTOMER_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'substring' filter based on the customer title."; diff --git a/application/src/main/java/org/thingsboard/server/service/ai/AiModelService.java b/application/src/main/java/org/thingsboard/server/service/ai/AiChatModelService.java similarity index 82% rename from application/src/main/java/org/thingsboard/server/service/ai/AiModelService.java rename to application/src/main/java/org/thingsboard/server/service/ai/AiChatModelService.java index 196a53ce3a..9e00c8ddfd 100644 --- a/application/src/main/java/org/thingsboard/server/service/ai/AiModelService.java +++ b/application/src/main/java/org/thingsboard/server/service/ai/AiChatModelService.java @@ -15,6 +15,6 @@ */ package org.thingsboard.server.service.ai; -import org.thingsboard.rule.engine.api.RuleEngineAiModelService; +import org.thingsboard.rule.engine.api.RuleEngineAiChatModelService; -public interface AiModelService extends RuleEngineAiModelService {} +public interface AiChatModelService extends RuleEngineAiChatModelService {} diff --git a/application/src/main/java/org/thingsboard/server/service/ai/AiModelServiceImpl.java b/application/src/main/java/org/thingsboard/server/service/ai/AiChatModelServiceImpl.java similarity index 79% rename from application/src/main/java/org/thingsboard/server/service/ai/AiModelServiceImpl.java rename to application/src/main/java/org/thingsboard/server/service/ai/AiChatModelServiceImpl.java index 877f80fb26..d6252f57a6 100644 --- a/application/src/main/java/org/thingsboard/server/service/ai/AiModelServiceImpl.java +++ b/application/src/main/java/org/thingsboard/server/service/ai/AiChatModelServiceImpl.java @@ -21,21 +21,20 @@ import dev.langchain4j.model.chat.request.ChatRequest; import dev.langchain4j.model.chat.response.ChatResponse; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; -import org.thingsboard.server.common.data.ai.model.chat.AiChatModel; import org.thingsboard.server.common.data.ai.model.chat.AiChatModelConfig; import org.thingsboard.server.common.data.ai.model.chat.Langchain4jChatModelConfigurer; @Service @RequiredArgsConstructor -class AiModelServiceImpl implements AiModelService { +class AiChatModelServiceImpl implements AiChatModelService { private final Langchain4jChatModelConfigurer chatModelConfigurer; private final AiRequestsExecutor aiRequestsExecutor; @Override - public > FluentFuture sendChatRequestAsync(AiChatModel chatModel, ChatRequest chatRequest) { - ChatModel lc4jChatModel = chatModel.configure(chatModelConfigurer); - return aiRequestsExecutor.sendChatRequestAsync(lc4jChatModel, chatRequest); + public > FluentFuture sendChatRequestAsync(AiChatModelConfig chatModelConfig, ChatRequest chatRequest) { + ChatModel langChainChatModel = chatModelConfig.configure(chatModelConfigurer); + return aiRequestsExecutor.sendChatRequestAsync(langChainChatModel, chatRequest); } } diff --git a/application/src/main/java/org/thingsboard/server/service/ai/Langchain4jChatModelConfigurerImpl.java b/application/src/main/java/org/thingsboard/server/service/ai/Langchain4jChatModelConfigurerImpl.java index 10e327b48c..69dd98f47f 100644 --- a/application/src/main/java/org/thingsboard/server/service/ai/Langchain4jChatModelConfigurerImpl.java +++ b/application/src/main/java/org/thingsboard/server/service/ai/Langchain4jChatModelConfigurerImpl.java @@ -24,20 +24,26 @@ import com.google.cloud.vertexai.api.GenerationConfig; import com.google.cloud.vertexai.api.PredictionServiceClient; import com.google.cloud.vertexai.api.PredictionServiceSettings; import com.google.cloud.vertexai.generativeai.GenerativeModel; +import dev.langchain4j.model.anthropic.AnthropicChatModel; +import dev.langchain4j.model.azure.AzureOpenAiChatModel; import dev.langchain4j.model.bedrock.BedrockChatModel; import dev.langchain4j.model.chat.ChatModel; import dev.langchain4j.model.chat.request.ChatRequestParameters; +import dev.langchain4j.model.github.GitHubModelsChatModel; +import dev.langchain4j.model.googleai.GoogleAiGeminiChatModel; +import dev.langchain4j.model.mistralai.MistralAiChatModel; +import dev.langchain4j.model.openai.OpenAiChatModel; import dev.langchain4j.model.vertexai.gemini.VertexAiGeminiChatModel; import org.springframework.stereotype.Component; -import org.thingsboard.server.common.data.ai.model.chat.AmazonBedrockChatModel; -import org.thingsboard.server.common.data.ai.model.chat.AnthropicChatModel; -import org.thingsboard.server.common.data.ai.model.chat.AzureOpenAiChatModel; -import org.thingsboard.server.common.data.ai.model.chat.GitHubModelsChatModel; -import org.thingsboard.server.common.data.ai.model.chat.GoogleAiGeminiChatModel; -import org.thingsboard.server.common.data.ai.model.chat.GoogleVertexAiGeminiChatModel; +import org.thingsboard.server.common.data.ai.model.chat.AmazonBedrockChatModelConfig; +import org.thingsboard.server.common.data.ai.model.chat.AnthropicChatModelConfig; +import org.thingsboard.server.common.data.ai.model.chat.AzureOpenAiChatModelConfig; +import org.thingsboard.server.common.data.ai.model.chat.GitHubModelsChatModelConfig; +import org.thingsboard.server.common.data.ai.model.chat.GoogleAiGeminiChatModelConfig; +import org.thingsboard.server.common.data.ai.model.chat.GoogleVertexAiGeminiChatModelConfig; import org.thingsboard.server.common.data.ai.model.chat.Langchain4jChatModelConfigurer; -import org.thingsboard.server.common.data.ai.model.chat.MistralAiChatModel; -import org.thingsboard.server.common.data.ai.model.chat.OpenAiChatModel; +import org.thingsboard.server.common.data.ai.model.chat.MistralAiChatModelConfig; +import org.thingsboard.server.common.data.ai.model.chat.OpenAiChatModelConfig; import org.thingsboard.server.common.data.ai.provider.AmazonBedrockProviderConfig; import org.thingsboard.server.common.data.ai.provider.AzureOpenAiProviderConfig; import org.thingsboard.server.common.data.ai.provider.GoogleVertexAiGeminiProviderConfig; @@ -54,61 +60,57 @@ import java.time.Duration; class Langchain4jChatModelConfigurerImpl implements Langchain4jChatModelConfigurer { @Override - public ChatModel configureChatModel(OpenAiChatModel chatModel) { - OpenAiChatModel.Config modelConfig = chatModel.modelConfig(); - return dev.langchain4j.model.openai.OpenAiChatModel.builder() - .apiKey(chatModel.providerConfig().apiKey()) - .modelName(modelConfig.modelId()) - .temperature(modelConfig.temperature()) - .topP(modelConfig.topP()) - .frequencyPenalty(modelConfig.frequencyPenalty()) - .presencePenalty(modelConfig.presencePenalty()) - .maxTokens(modelConfig.maxOutputTokens()) - .timeout(toDuration(modelConfig.timeoutSeconds())) - .maxRetries(modelConfig.maxRetries()) + public ChatModel configureChatModel(OpenAiChatModelConfig chatModelConfig) { + return OpenAiChatModel.builder() + .apiKey(chatModelConfig.providerConfig().apiKey()) + .modelName(chatModelConfig.modelId()) + .temperature(chatModelConfig.temperature()) + .topP(chatModelConfig.topP()) + .frequencyPenalty(chatModelConfig.frequencyPenalty()) + .presencePenalty(chatModelConfig.presencePenalty()) + .maxTokens(chatModelConfig.maxOutputTokens()) + .timeout(toDuration(chatModelConfig.timeoutSeconds())) + .maxRetries(chatModelConfig.maxRetries()) .build(); } @Override - public ChatModel configureChatModel(AzureOpenAiChatModel chatModel) { - AzureOpenAiProviderConfig providerConfig = chatModel.providerConfig(); - AzureOpenAiChatModel.Config modelConfig = chatModel.modelConfig(); - return dev.langchain4j.model.azure.AzureOpenAiChatModel.builder() + public ChatModel configureChatModel(AzureOpenAiChatModelConfig chatModelConfig) { + AzureOpenAiProviderConfig providerConfig = chatModelConfig.providerConfig(); + return AzureOpenAiChatModel.builder() .endpoint(providerConfig.endpoint()) .serviceVersion(providerConfig.serviceVersion()) .apiKey(providerConfig.apiKey()) - .deploymentName(modelConfig.modelId()) - .temperature(modelConfig.temperature()) - .topP(modelConfig.topP()) - .frequencyPenalty(modelConfig.frequencyPenalty()) - .presencePenalty(modelConfig.presencePenalty()) - .maxTokens(modelConfig.maxOutputTokens()) - .timeout(toDuration(modelConfig.timeoutSeconds())) - .maxRetries(modelConfig.maxRetries()) + .deploymentName(chatModelConfig.modelId()) + .temperature(chatModelConfig.temperature()) + .topP(chatModelConfig.topP()) + .frequencyPenalty(chatModelConfig.frequencyPenalty()) + .presencePenalty(chatModelConfig.presencePenalty()) + .maxTokens(chatModelConfig.maxOutputTokens()) + .timeout(toDuration(chatModelConfig.timeoutSeconds())) + .maxRetries(chatModelConfig.maxRetries()) .build(); } @Override - public ChatModel configureChatModel(GoogleAiGeminiChatModel chatModel) { - GoogleAiGeminiChatModel.Config modelConfig = chatModel.modelConfig(); - return dev.langchain4j.model.googleai.GoogleAiGeminiChatModel.builder() - .apiKey(chatModel.providerConfig().apiKey()) - .modelName(modelConfig.modelId()) - .temperature(modelConfig.temperature()) - .topP(modelConfig.topP()) - .topK(modelConfig.topK()) - .frequencyPenalty(modelConfig.frequencyPenalty()) - .presencePenalty(modelConfig.presencePenalty()) - .maxOutputTokens(modelConfig.maxOutputTokens()) - .timeout(toDuration(modelConfig.timeoutSeconds())) - .maxRetries(modelConfig.maxRetries()) + public ChatModel configureChatModel(GoogleAiGeminiChatModelConfig chatModelConfig) { + return GoogleAiGeminiChatModel.builder() + .apiKey(chatModelConfig.providerConfig().apiKey()) + .modelName(chatModelConfig.modelId()) + .temperature(chatModelConfig.temperature()) + .topP(chatModelConfig.topP()) + .topK(chatModelConfig.topK()) + .frequencyPenalty(chatModelConfig.frequencyPenalty()) + .presencePenalty(chatModelConfig.presencePenalty()) + .maxOutputTokens(chatModelConfig.maxOutputTokens()) + .timeout(toDuration(chatModelConfig.timeoutSeconds())) + .maxRetries(chatModelConfig.maxRetries()) .build(); } @Override - public ChatModel configureChatModel(GoogleVertexAiGeminiChatModel chatModel) { - GoogleVertexAiGeminiProviderConfig providerConfig = chatModel.providerConfig(); - GoogleVertexAiGeminiChatModel.Config modelConfig = chatModel.modelConfig(); + public ChatModel configureChatModel(GoogleVertexAiGeminiChatModelConfig chatModelConfig) { + GoogleVertexAiGeminiProviderConfig providerConfig = chatModelConfig.providerConfig(); // construct service account credentials using service account key JSON ServiceAccountCredentials serviceAccountCredentials; @@ -131,8 +133,8 @@ class Langchain4jChatModelConfigurerImpl implements Langchain4jChatModelConfigur .toBuilder(); // set request timeout from model config - if (modelConfig.timeoutSeconds() != null) { - retrySettings.setTotalTimeout(org.threeten.bp.Duration.ofSeconds(modelConfig.timeoutSeconds())); + if (chatModelConfig.timeoutSeconds() != null) { + retrySettings.setTotalTimeout(org.threeten.bp.Duration.ofSeconds(chatModelConfig.timeoutSeconds())); } // set updated retry settings @@ -154,30 +156,30 @@ class Langchain4jChatModelConfigurerImpl implements Langchain4jChatModelConfigur // map model config to generation config var generationConfigBuilder = GenerationConfig.newBuilder(); - if (modelConfig.temperature() != null) { - generationConfigBuilder.setTemperature(modelConfig.temperature().floatValue()); + if (chatModelConfig.temperature() != null) { + generationConfigBuilder.setTemperature(chatModelConfig.temperature().floatValue()); } - if (modelConfig.topP() != null) { - generationConfigBuilder.setTopP(modelConfig.topP().floatValue()); + if (chatModelConfig.topP() != null) { + generationConfigBuilder.setTopP(chatModelConfig.topP().floatValue()); } - if (modelConfig.topK() != null) { - generationConfigBuilder.setTopK(modelConfig.topK()); + if (chatModelConfig.topK() != null) { + generationConfigBuilder.setTopK(chatModelConfig.topK()); } - if (modelConfig.frequencyPenalty() != null) { - generationConfigBuilder.setFrequencyPenalty(modelConfig.frequencyPenalty().floatValue()); + if (chatModelConfig.frequencyPenalty() != null) { + generationConfigBuilder.setFrequencyPenalty(chatModelConfig.frequencyPenalty().floatValue()); } - if (modelConfig.frequencyPenalty() != null) { - generationConfigBuilder.setPresencePenalty(modelConfig.frequencyPenalty().floatValue()); + if (chatModelConfig.frequencyPenalty() != null) { + generationConfigBuilder.setPresencePenalty(chatModelConfig.frequencyPenalty().floatValue()); } - if (modelConfig.maxOutputTokens() != null) { - generationConfigBuilder.setMaxOutputTokens(modelConfig.maxOutputTokens()); + if (chatModelConfig.maxOutputTokens() != null) { + generationConfigBuilder.setMaxOutputTokens(chatModelConfig.maxOutputTokens()); } var generationConfig = generationConfigBuilder.build(); // construct generative model instance - var generativeModel = new GenerativeModel(modelConfig.modelId(), vertexAI).withGenerationConfig(generationConfig); + var generativeModel = new GenerativeModel(chatModelConfig.modelId(), vertexAI).withGenerationConfig(generationConfig); - return new VertexAiGeminiChatModel(generativeModel, generationConfig, modelConfig.maxRetries()); + return new VertexAiGeminiChatModel(generativeModel, generationConfig, chatModelConfig.maxRetries()); } private static PredictionServiceClient createPredictionServiceClient(PredictionServiceSettings settings) { @@ -189,40 +191,37 @@ class Langchain4jChatModelConfigurerImpl implements Langchain4jChatModelConfigur } @Override - public ChatModel configureChatModel(MistralAiChatModel chatModel) { - MistralAiChatModel.Config modelConfig = chatModel.modelConfig(); - return dev.langchain4j.model.mistralai.MistralAiChatModel.builder() - .apiKey(chatModel.providerConfig().apiKey()) - .modelName(modelConfig.modelId()) - .temperature(modelConfig.temperature()) - .topP(modelConfig.topP()) - .frequencyPenalty(modelConfig.frequencyPenalty()) - .presencePenalty(modelConfig.presencePenalty()) - .maxTokens(modelConfig.maxOutputTokens()) - .timeout(toDuration(modelConfig.timeoutSeconds())) - .maxRetries(modelConfig.maxRetries()) + public ChatModel configureChatModel(MistralAiChatModelConfig chatModelConfig) { + return MistralAiChatModel.builder() + .apiKey(chatModelConfig.providerConfig().apiKey()) + .modelName(chatModelConfig.modelId()) + .temperature(chatModelConfig.temperature()) + .topP(chatModelConfig.topP()) + .frequencyPenalty(chatModelConfig.frequencyPenalty()) + .presencePenalty(chatModelConfig.presencePenalty()) + .maxTokens(chatModelConfig.maxOutputTokens()) + .timeout(toDuration(chatModelConfig.timeoutSeconds())) + .maxRetries(chatModelConfig.maxRetries()) .build(); } @Override - public ChatModel configureChatModel(AnthropicChatModel chatModel) { - AnthropicChatModel.Config modelConfig = chatModel.modelConfig(); - return dev.langchain4j.model.anthropic.AnthropicChatModel.builder() - .apiKey(chatModel.providerConfig().apiKey()) - .modelName(modelConfig.modelId()) - .temperature(modelConfig.temperature()) - .topP(modelConfig.topP()) - .topK(modelConfig.topK()) - .maxTokens(modelConfig.maxOutputTokens()) - .timeout(toDuration(modelConfig.timeoutSeconds())) - .maxRetries(modelConfig.maxRetries()) + public ChatModel configureChatModel(AnthropicChatModelConfig chatModelConfig) { + return AnthropicChatModel.builder() + .apiKey(chatModelConfig.providerConfig().apiKey()) + .modelName(chatModelConfig.modelId()) + .temperature(chatModelConfig.temperature()) + .topP(chatModelConfig.topP()) + .topK(chatModelConfig.topK()) + .maxTokens(chatModelConfig.maxOutputTokens()) + .timeout(toDuration(chatModelConfig.timeoutSeconds())) + .maxRetries(chatModelConfig.maxRetries()) .build(); } @Override - public ChatModel configureChatModel(AmazonBedrockChatModel chatModel) { - AmazonBedrockProviderConfig providerConfig = chatModel.providerConfig(); - AmazonBedrockChatModel.Config modelConfig = chatModel.modelConfig(); + public ChatModel configureChatModel(AmazonBedrockChatModelConfig chatModelConfig) { + AmazonBedrockProviderConfig providerConfig = chatModelConfig.providerConfig(); var credentialsProvider = StaticCredentialsProvider.create( AwsBasicCredentials.create(providerConfig.accessKeyId(), providerConfig.secretAccessKey()) @@ -234,33 +233,32 @@ class Langchain4jChatModelConfigurerImpl implements Langchain4jChatModelConfigur .build(); var defaultChatRequestParams = ChatRequestParameters.builder() - .temperature(modelConfig.temperature()) - .topP(modelConfig.topP()) - .maxOutputTokens(modelConfig.maxOutputTokens()) + .temperature(chatModelConfig.temperature()) + .topP(chatModelConfig.topP()) + .maxOutputTokens(chatModelConfig.maxOutputTokens()) .build(); return BedrockChatModel.builder() .client(bedrockClient) - .modelId(modelConfig.modelId()) + .modelId(chatModelConfig.modelId()) .defaultRequestParameters(defaultChatRequestParams) - .timeout(toDuration(modelConfig.timeoutSeconds())) - .maxRetries(modelConfig.maxRetries()) + .timeout(toDuration(chatModelConfig.timeoutSeconds())) + .maxRetries(chatModelConfig.maxRetries()) .build(); } @Override - public ChatModel configureChatModel(GitHubModelsChatModel chatModel) { - GitHubModelsChatModel.Config modelConfig = chatModel.modelConfig(); - return dev.langchain4j.model.github.GitHubModelsChatModel.builder() - .gitHubToken(chatModel.providerConfig().personalAccessToken()) - .modelName(modelConfig.modelId()) - .temperature(modelConfig.temperature()) - .topP(modelConfig.topP()) - .frequencyPenalty(modelConfig.frequencyPenalty()) - .presencePenalty(modelConfig.presencePenalty()) - .maxTokens(modelConfig.maxOutputTokens()) - .timeout(toDuration(modelConfig.timeoutSeconds())) - .maxRetries(modelConfig.maxRetries()) + public ChatModel configureChatModel(GitHubModelsChatModelConfig chatModelConfig) { + return GitHubModelsChatModel.builder() + .gitHubToken(chatModelConfig.providerConfig().personalAccessToken()) + .modelName(chatModelConfig.modelId()) + .temperature(chatModelConfig.temperature()) + .topP(chatModelConfig.topP()) + .frequencyPenalty(chatModelConfig.frequencyPenalty()) + .presencePenalty(chatModelConfig.presencePenalty()) + .maxTokens(chatModelConfig.maxOutputTokens()) + .timeout(toDuration(chatModelConfig.timeoutSeconds())) + .maxRetries(chatModelConfig.maxRetries()) .build(); } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/EdgeEventSourcingListener.java b/application/src/main/java/org/thingsboard/server/service/edge/EdgeEventSourcingListener.java index 8bbed875fe..e31bbe21ac 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/EdgeEventSourcingListener.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/EdgeEventSourcingListener.java @@ -112,7 +112,7 @@ public class EdgeEventSourcingListener { return; } try { - if (EntityType.TENANT == entityType || EntityType.EDGE == entityType || EntityType.AI_MODEL_SETTINGS == entityType) { + if (EntityType.TENANT == entityType || EntityType.EDGE == entityType || EntityType.AI_MODEL == entityType) { return; } log.trace("[{}] DeleteEntityEvent called: {}", tenantId, event); @@ -226,7 +226,7 @@ public class EdgeEventSourcingListener { break; case TENANT: return !event.getCreated(); - case API_USAGE_STATE, EDGE, AI_MODEL_SETTINGS: + case API_USAGE_STATE, EDGE, AI_MODEL: return false; case DOMAIN: if (entity instanceof Domain domain) { diff --git a/application/src/main/java/org/thingsboard/server/service/edge/RelatedEdgesSourcingListener.java b/application/src/main/java/org/thingsboard/server/service/edge/RelatedEdgesSourcingListener.java index 5fca13380c..8a111e4d9d 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/RelatedEdgesSourcingListener.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/RelatedEdgesSourcingListener.java @@ -68,7 +68,7 @@ public class RelatedEdgesSourcingListener { @TransactionalEventListener( fallbackExecution = true, - condition = "#event.entityId.getEntityType() != T(org.thingsboard.server.common.data.EntityType).AI_MODEL_SETTINGS" + condition = "#event.entityId.getEntityType() != T(org.thingsboard.server.common.data.EntityType).AI_MODEL" ) public void handleEvent(DeleteEntityEvent event) { executorService.submit(() -> { diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/ai/DefaultTbAiModelSettingsService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/ai/DefaultTbAiModelService.java similarity index 54% rename from application/src/main/java/org/thingsboard/server/service/entitiy/ai/DefaultTbAiModelSettingsService.java rename to application/src/main/java/org/thingsboard/server/service/entitiy/ai/DefaultTbAiModelService.java index 69f130461c..264b82dd33 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/ai/DefaultTbAiModelSettingsService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/ai/DefaultTbAiModelService.java @@ -19,9 +19,9 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.User; -import org.thingsboard.server.common.data.ai.AiModelSettings; +import org.thingsboard.server.common.data.ai.AiModel; import org.thingsboard.server.common.data.audit.ActionType; -import org.thingsboard.server.dao.ai.AiModelSettingsService; +import org.thingsboard.server.dao.ai.AiModelService; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.entitiy.AbstractTbEntityService; @@ -30,48 +30,48 @@ import static java.util.Objects.requireNonNullElseGet; @Service @TbCoreComponent @RequiredArgsConstructor -class DefaultTbAiModelSettingsService extends AbstractTbEntityService implements TbAiModelSettingsService { +class DefaultTbAiModelService extends AbstractTbEntityService implements TbAiModelService { - private final AiModelSettingsService aiModelSettingsService; + private final AiModelService aiModelService; @Override - public AiModelSettings save(AiModelSettings settings, User user) { - var actionType = settings.getId() == null ? ActionType.ADDED : ActionType.UPDATED; + public AiModel save(AiModel model, User user) { + var actionType = model.getId() == null ? ActionType.ADDED : ActionType.UPDATED; var tenantId = user.getTenantId(); - settings.setTenantId(tenantId); + model.setTenantId(tenantId); - AiModelSettings savedSettings; + AiModel savedModel; try { - savedSettings = aiModelSettingsService.save(settings); - autoCommit(user, savedSettings.getId()); + savedModel = aiModelService.save(model); + autoCommit(user, savedModel.getId()); } catch (Exception e) { - logEntityActionService.logEntityAction(tenantId, requireNonNullElseGet(settings.getId(), () -> emptyId(EntityType.AI_MODEL_SETTINGS)), settings, actionType, user, e); + logEntityActionService.logEntityAction(tenantId, requireNonNullElseGet(model.getId(), () -> emptyId(EntityType.AI_MODEL)), model, actionType, user, e); throw e; } - logEntityActionService.logEntityAction(tenantId, savedSettings.getId(), savedSettings, actionType, user); + logEntityActionService.logEntityAction(tenantId, savedModel.getId(), savedModel, actionType, user); - return savedSettings; + return savedModel; } @Override - public boolean delete(AiModelSettings settings, User user) { + public boolean delete(AiModel model, User user) { var actionType = ActionType.DELETED; var tenantId = user.getTenantId(); - var settingsId = settings.getId(); + var modelId = model.getId(); boolean deleted; try { - deleted = aiModelSettingsService.deleteByTenantIdAndId(tenantId, settingsId); + deleted = aiModelService.deleteByTenantIdAndId(tenantId, modelId); } catch (Exception e) { - logEntityActionService.logEntityAction(tenantId, settingsId, settings, actionType, user, e, settingsId.toString()); + logEntityActionService.logEntityAction(tenantId, modelId, model, actionType, user, e, modelId.toString()); throw e; } if (deleted) { - logEntityActionService.logEntityAction(tenantId, settingsId, settings, actionType, user, settingsId.toString()); + logEntityActionService.logEntityAction(tenantId, modelId, model, actionType, user, modelId.toString()); } return deleted; diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/ai/TbAiModelSettingsService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/ai/TbAiModelService.java similarity index 76% rename from application/src/main/java/org/thingsboard/server/service/entitiy/ai/TbAiModelSettingsService.java rename to application/src/main/java/org/thingsboard/server/service/entitiy/ai/TbAiModelService.java index 0d66c171a7..0b09423ffa 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/ai/TbAiModelSettingsService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/ai/TbAiModelService.java @@ -16,12 +16,12 @@ package org.thingsboard.server.service.entitiy.ai; import org.thingsboard.server.common.data.User; -import org.thingsboard.server.common.data.ai.AiModelSettings; +import org.thingsboard.server.common.data.ai.AiModel; -public interface TbAiModelSettingsService { +public interface TbAiModelService { - AiModelSettings save(AiModelSettings settings, User user); + AiModel save(AiModel model, User user); - boolean delete(AiModelSettings settings, User user); + boolean delete(AiModel model, User user); } diff --git a/application/src/main/java/org/thingsboard/server/service/security/permission/Resource.java b/application/src/main/java/org/thingsboard/server/service/security/permission/Resource.java index ba7b810c7d..701fed952b 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/permission/Resource.java +++ b/application/src/main/java/org/thingsboard/server/service/security/permission/Resource.java @@ -53,7 +53,7 @@ public enum Resource { EntityType.NOTIFICATION_REQUEST, EntityType.NOTIFICATION_RULE), MOBILE_APP_SETTINGS, JOB(EntityType.JOB), - AI_MODEL_SETTINGS(EntityType.AI_MODEL_SETTINGS); + AI_MODEL(EntityType.AI_MODEL); private final Set entityTypes; diff --git a/application/src/main/java/org/thingsboard/server/service/security/permission/TenantAdminPermissions.java b/application/src/main/java/org/thingsboard/server/service/security/permission/TenantAdminPermissions.java index 6b2f6f8ccf..7a824ca735 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/permission/TenantAdminPermissions.java +++ b/application/src/main/java/org/thingsboard/server/service/security/permission/TenantAdminPermissions.java @@ -18,8 +18,8 @@ package org.thingsboard.server.service.security.permission; import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.HasTenantId; import org.thingsboard.server.common.data.User; -import org.thingsboard.server.common.data.ai.AiModelSettings; -import org.thingsboard.server.common.data.id.AiModelSettingsId; +import org.thingsboard.server.common.data.ai.AiModel; +import org.thingsboard.server.common.data.id.AiModelId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.security.Authority; @@ -58,7 +58,7 @@ public class TenantAdminPermissions extends AbstractPermissions { put(Resource.MOBILE_APP, tenantEntityPermissionChecker); put(Resource.MOBILE_APP_BUNDLE, tenantEntityPermissionChecker); put(Resource.JOB, tenantEntityPermissionChecker); - put(Resource.AI_MODEL_SETTINGS, aiModelSettingsPermissionChecker); + put(Resource.AI_MODEL, aiModelPermissionChecker); } public static final PermissionChecker tenantEntityPermissionChecker = new PermissionChecker() { @@ -149,7 +149,7 @@ public class TenantAdminPermissions extends AbstractPermissions { }; - private static final PermissionChecker aiModelSettingsPermissionChecker = new PermissionChecker<>() { + private static final PermissionChecker aiModelPermissionChecker = new PermissionChecker<>() { @Override public boolean hasPermission(SecurityUser user, Operation operation) { @@ -157,7 +157,7 @@ public class TenantAdminPermissions extends AbstractPermissions { } @Override - public boolean hasPermission(SecurityUser user, Operation operation, AiModelSettingsId entityId, AiModelSettings entity) { + public boolean hasPermission(SecurityUser user, Operation operation, AiModelId entityId, AiModel entity) { return user.getTenantId().equals(entity.getTenantId()); } diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/DefaultEntitiesExportImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/DefaultEntitiesExportImportService.java index 82f855b7c2..b000041a26 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/DefaultEntitiesExportImportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/DefaultEntitiesExportImportService.java @@ -70,7 +70,7 @@ public class DefaultEntitiesExportImportService implements EntitiesExportImportS EntityType.DEVICE_PROFILE, EntityType.DEVICE, EntityType.ENTITY_VIEW, EntityType.WIDGET_TYPE, EntityType.WIDGETS_BUNDLE, EntityType.NOTIFICATION_TEMPLATE, EntityType.NOTIFICATION_TARGET, EntityType.NOTIFICATION_RULE, - EntityType.AI_MODEL_SETTINGS + EntityType.AI_MODEL ); @Override diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/AiModelSettingsExportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/AiModelExportService.java similarity index 76% rename from application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/AiModelSettingsExportService.java rename to application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/AiModelExportService.java index a5ba16e9b1..8d6097b726 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/AiModelSettingsExportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/AiModelExportService.java @@ -17,8 +17,8 @@ package org.thingsboard.server.service.sync.ie.exporting.impl; import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.EntityType; -import org.thingsboard.server.common.data.ai.AiModelSettings; -import org.thingsboard.server.common.data.id.AiModelSettingsId; +import org.thingsboard.server.common.data.ai.AiModel; +import org.thingsboard.server.common.data.id.AiModelId; import org.thingsboard.server.common.data.sync.ie.EntityExportData; import org.thingsboard.server.queue.util.TbCoreComponent; @@ -26,11 +26,11 @@ import java.util.Set; @Service @TbCoreComponent -class AiModelSettingsExportService extends BaseEntityExportService> { +class AiModelExportService extends BaseEntityExportService> { @Override public Set getSupportedEntityTypes() { - return Set.of(EntityType.AI_MODEL_SETTINGS); + return Set.of(EntityType.AI_MODEL); } } diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/AiModelSettingsImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/AiModelImportService.java similarity index 50% rename from application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/AiModelSettingsImportService.java rename to application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/AiModelImportService.java index e5666e5003..34e70adb11 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/AiModelSettingsImportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/AiModelImportService.java @@ -18,60 +18,60 @@ 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.ai.AiModelSettings; -import org.thingsboard.server.common.data.id.AiModelSettingsId; +import org.thingsboard.server.common.data.ai.AiModel; +import org.thingsboard.server.common.data.id.AiModelId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.sync.ie.EntityExportData; -import org.thingsboard.server.dao.ai.AiModelSettingsService; +import org.thingsboard.server.dao.ai.AiModelService; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.sync.vc.data.EntitiesImportCtx; @Service @TbCoreComponent @RequiredArgsConstructor -class AiModelSettingsImportService extends BaseEntityImportService> { +class AiModelImportService extends BaseEntityImportService> { - private final AiModelSettingsService aiModelSettingsService; + private final AiModelService aiModelService; @Override protected void setOwner( TenantId tenantId, - AiModelSettings settings, - BaseEntityImportService>.IdProvider idProvider + AiModel model, + BaseEntityImportService>.IdProvider idProvider ) { - settings.setTenantId(tenantId); + model.setTenantId(tenantId); } @Override - protected AiModelSettings prepare( + protected AiModel prepare( EntitiesImportCtx ctx, - AiModelSettings settings, - AiModelSettings oldEntity, - EntityExportData exportData, - BaseEntityImportService>.IdProvider idProvider + AiModel model, + AiModel oldModel, + EntityExportData exportData, + BaseEntityImportService>.IdProvider idProvider ) { - return settings; + return model; } @Override - protected AiModelSettings deepCopy(AiModelSettings settings) { - return new AiModelSettings(settings); + protected AiModel deepCopy(AiModel model) { + return new AiModel(model); } @Override - protected AiModelSettings saveOrUpdate( + protected AiModel saveOrUpdate( EntitiesImportCtx ctx, - AiModelSettings settings, - EntityExportData exportData, - BaseEntityImportService>.IdProvider idProvider, + AiModel model, + EntityExportData exportData, + BaseEntityImportService>.IdProvider idProvider, CompareResult compareResult ) { - return aiModelSettingsService.save(settings); + return aiModelService.save(model); } @Override public EntityType getEntityType() { - return EntityType.AI_MODEL_SETTINGS; + return EntityType.AI_MODEL; } } diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index 429bbd4a29..c4bf6bf3c8 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -656,9 +656,9 @@ cache: trendzSettings: timeToLiveInMinutes: "${CACHE_SPECS_TRENDZ_SETTINGS_TTL:1440}" # Trendz settings cache TTL maxSize: "${CACHE_SPECS_TRENDZ_SETTINGS_MAX_SIZE:10000}" # 0 means the cache is disabled - aiModelSettings: - timeToLiveInMinutes: "${CACHE_SPECS_AI_MODEL_SETTINGS_TTL:1440}" # AI model settings cache TTL - maxSize: "${CACHE_SPECS_AI_MODEL_SETTINGS_MAX_SIZE:10000}" # 0 means the cache is disabled + aiModel: + timeToLiveInMinutes: "${CACHE_SPECS_AI_MODEL_TTL:1440}" # AI model cache TTL + maxSize: "${CACHE_SPECS_AI_MODEL_MAX_SIZE:10000}" # 0 means the cache is disabled # Deliberately placed outside the 'specs' group above notificationRules: @@ -874,7 +874,7 @@ audit-log: "tb_resource": "${AUDIT_LOG_MASK_RESOURCE:W}" # TB resource logging levels. "ota_package": "${AUDIT_LOG_MASK_OTA_PACKAGE:W}" # Ota package logging levels. "calculated_field": "${AUDIT_LOG_MASK_CALCULATED_FIELD:W}" # Calculated field logging levels. - "ai_model_settings": "${AUDIT_LOG_MASK_AI_MODEL_SETTINGS:W}" # AI model settings logging levels. + "ai_model": "${AUDIT_LOG_MASK_AI_MODEL:W}" # AI model logging levels. sink: # Type of external sink. possible options: none, elasticsearch type: "${AUDIT_LOG_SINK_TYPE:none}" diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/ai/AiModelSettingsService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/ai/AiModelService.java similarity index 55% rename from common/dao-api/src/main/java/org/thingsboard/server/dao/ai/AiModelSettingsService.java rename to common/dao-api/src/main/java/org/thingsboard/server/dao/ai/AiModelService.java index 09219f238e..3ad12048cf 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/ai/AiModelSettingsService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/ai/AiModelService.java @@ -16,8 +16,8 @@ package org.thingsboard.server.dao.ai; import com.google.common.util.concurrent.FluentFuture; -import org.thingsboard.server.common.data.ai.AiModelSettings; -import org.thingsboard.server.common.data.id.AiModelSettingsId; +import org.thingsboard.server.common.data.ai.AiModel; +import org.thingsboard.server.common.data.id.AiModelId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; @@ -25,18 +25,18 @@ import org.thingsboard.server.dao.entity.EntityDaoService; import java.util.Optional; -public interface AiModelSettingsService extends EntityDaoService { +public interface AiModelService extends EntityDaoService { - AiModelSettings save(AiModelSettings settings); + AiModel save(AiModel model); - Optional findAiModelSettingsById(TenantId tenantId, AiModelSettingsId settingsId); + Optional findAiModelById(TenantId tenantId, AiModelId modelId); - PageData findAiModelSettingsByTenantId(TenantId tenantId, PageLink pageLink); + PageData findAiModelsByTenantId(TenantId tenantId, PageLink pageLink); - Optional findAiModelSettingsByTenantIdAndId(TenantId tenantId, AiModelSettingsId settingsId); + Optional findAiModelByTenantIdAndId(TenantId tenantId, AiModelId modelId); - FluentFuture> findAiModelSettingsByTenantIdAndIdAsync(TenantId tenantId, AiModelSettingsId settingsId); + FluentFuture> findAiModelByTenantIdAndIdAsync(TenantId tenantId, AiModelId modelId); - boolean deleteByTenantIdAndId(TenantId tenantId, AiModelSettingsId settingsId); + boolean deleteByTenantIdAndId(TenantId tenantId, AiModelId modelId); } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java b/common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java index c5df7c10c0..b55453f393 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java @@ -17,6 +17,8 @@ package org.thingsboard.server.common.data; public final class CacheConstants { + private CacheConstants() {} + public static final String DEVICE_CREDENTIALS_CACHE = "deviceCredentials"; public static final String RELATIONS_CACHE = "relations"; public static final String DEVICE_CACHE = "devices"; @@ -37,7 +39,7 @@ public final class CacheConstants { public static final String NOTIFICATION_SETTINGS_CACHE = "notificationSettings"; public static final String SENT_NOTIFICATIONS_CACHE = "sentNotifications"; public static final String TRENDZ_SETTINGS_CACHE = "trendzSettings"; - public static final String AI_MODEL_SETTINGS_CACHE = "aiModelSettings"; + public static final String AI_MODEL_CACHE = "aiModel"; public static final String ASSET_PROFILE_CACHE = "assetProfiles"; public static final String ATTRIBUTES_CACHE = "attributes"; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/EntityType.java b/common/data/src/main/java/org/thingsboard/server/common/data/EntityType.java index dddafe05bc..8c132a3eba 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/EntityType.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/EntityType.java @@ -66,10 +66,10 @@ public enum EntityType { CALCULATED_FIELD(39), CALCULATED_FIELD_LINK(40), JOB(41), - AI_MODEL_SETTINGS(42, "ai_model_settings") { + AI_MODEL(42, "ai_model") { @Override public String getNormalName() { - return "AI model settings"; + return "AI model"; } }; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/AiModelSettings.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/AiModel.java similarity index 67% rename from common/data/src/main/java/org/thingsboard/server/common/data/ai/AiModelSettings.java rename to common/data/src/main/java/org/thingsboard/server/common/data/ai/AiModel.java index 63e94f1974..4d7bb21930 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/AiModelSettings.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/AiModel.java @@ -27,8 +27,8 @@ import org.thingsboard.server.common.data.BaseData; import org.thingsboard.server.common.data.ExportableEntity; import org.thingsboard.server.common.data.HasTenantId; import org.thingsboard.server.common.data.HasVersion; -import org.thingsboard.server.common.data.ai.model.AiModel; -import org.thingsboard.server.common.data.id.AiModelSettingsId; +import org.thingsboard.server.common.data.ai.model.AiModelConfig; +import org.thingsboard.server.common.data.id.AiModelId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.validation.Length; import org.thingsboard.server.common.data.validation.NoNullChar; @@ -39,7 +39,7 @@ import java.io.Serial; @Builder @AllArgsConstructor @EqualsAndHashCode(callSuper = true) -public final class AiModelSettings extends BaseData implements HasTenantId, HasVersion, ExportableEntity { +public final class AiModel extends BaseData implements HasTenantId, HasVersion, ExportableEntity { @Serial private static final long serialVersionUID = 9017108678716011604L; @@ -47,7 +47,7 @@ public final class AiModelSettings extends BaseData implement @Schema( requiredMode = Schema.RequiredMode.REQUIRED, accessMode = Schema.AccessMode.READ_ONLY, - description = "JSON object representing the ID of the tenant associated with these AI model settings", + description = "JSON object representing the ID of the tenant associated with this AI model", example = "e3c4b7d2-5678-4a9b-0c1d-2e3f4a5b6c7d" ) private TenantId tenantId; @@ -55,7 +55,7 @@ public final class AiModelSettings extends BaseData implement @Schema( requiredMode = Schema.RequiredMode.REQUIRED, accessMode = Schema.AccessMode.READ_ONLY, - description = "Version of the AI model settings; increments automatically whenever the settings are changed", + description = "Version of the AI model record; increments automatically whenever the record is changed", example = "7", defaultValue = "1" ) @@ -67,8 +67,8 @@ public final class AiModelSettings extends BaseData implement @Schema( requiredMode = Schema.RequiredMode.REQUIRED, accessMode = Schema.AccessMode.READ_WRITE, - description = "Human-readable name of the AI model settings; must be unique within the scope of the tenant", - example = "Rule node assistant" + description = "Display name for this AI model configuration; not the technical model identifier", + example = "Fast and cost-efficient model" ) private String name; @@ -79,24 +79,24 @@ public final class AiModelSettings extends BaseData implement accessMode = Schema.AccessMode.READ_WRITE, description = "Configuration of the AI model" ) - private AiModel configuration; + private AiModelConfig configuration; - private AiModelSettingsId externalId; + private AiModelId externalId; - public AiModelSettings() {} + public AiModel() {} - public AiModelSettings(AiModelSettingsId id) { + public AiModel(AiModelId id) { super(id); } - public AiModelSettings(AiModelSettings settings) { - super(settings.getId()); - createdTime = settings.getCreatedTime(); - tenantId = settings.getTenantId(); - version = settings.getVersion(); - name = settings.getName(); - configuration = settings.getConfiguration(); - externalId = settings.getExternalId() == null ? null : new AiModelSettingsId(settings.getExternalId().getId()); + public AiModel(AiModel model) { + super(model.getId()); + createdTime = model.getCreatedTime(); + tenantId = model.getTenantId(); + version = model.getVersion(); + name = model.getName(); + configuration = model.getConfiguration(); + externalId = model.getExternalId() == null ? null : new AiModelId(model.getExternalId().getId()); } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/dto/TbChatRequest.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/dto/TbChatRequest.java index aa737b27d3..7e43520b79 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/dto/TbChatRequest.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/dto/TbChatRequest.java @@ -23,7 +23,7 @@ import dev.langchain4j.model.chat.request.ChatRequest; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.Valid; import jakarta.validation.constraints.NotNull; -import org.thingsboard.server.common.data.ai.model.chat.AiChatModel; +import org.thingsboard.server.common.data.ai.model.chat.AiChatModelConfig; import java.util.ArrayList; import java.util.List; @@ -51,7 +51,7 @@ public record TbChatRequest( description = "Configuration of the AI chat model that should execute the request" ) @NotNull @Valid - AiChatModel chatModel + AiChatModelConfig chatModelConfig ) { public ChatRequest toLangChainChatRequest() { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/AiModel.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/AiModel.java deleted file mode 100644 index affa159e06..0000000000 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/AiModel.java +++ /dev/null @@ -1,84 +0,0 @@ -/** - * 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.ai.model; - -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonSubTypes; -import com.fasterxml.jackson.annotation.JsonTypeInfo; -import org.thingsboard.server.common.data.ai.model.chat.AmazonBedrockChatModel; -import org.thingsboard.server.common.data.ai.model.chat.AnthropicChatModel; -import org.thingsboard.server.common.data.ai.model.chat.AzureOpenAiChatModel; -import org.thingsboard.server.common.data.ai.model.chat.GitHubModelsChatModel; -import org.thingsboard.server.common.data.ai.model.chat.GoogleAiGeminiChatModel; -import org.thingsboard.server.common.data.ai.model.chat.GoogleVertexAiGeminiChatModel; -import org.thingsboard.server.common.data.ai.model.chat.MistralAiChatModel; -import org.thingsboard.server.common.data.ai.model.chat.OpenAiChatModel; -import org.thingsboard.server.common.data.ai.provider.AiProvider; -import org.thingsboard.server.common.data.ai.provider.AiProviderConfig; -import org.thingsboard.server.common.data.ai.provider.AmazonBedrockProviderConfig; -import org.thingsboard.server.common.data.ai.provider.AnthropicProviderConfig; -import org.thingsboard.server.common.data.ai.provider.AzureOpenAiProviderConfig; -import org.thingsboard.server.common.data.ai.provider.GitHubModelsProviderConfig; -import org.thingsboard.server.common.data.ai.provider.GoogleAiGeminiProviderConfig; -import org.thingsboard.server.common.data.ai.provider.GoogleVertexAiGeminiProviderConfig; -import org.thingsboard.server.common.data.ai.provider.MistralAiProviderConfig; -import org.thingsboard.server.common.data.ai.provider.OpenAiProviderConfig; - -@JsonTypeInfo( - use = JsonTypeInfo.Id.NAME, - include = JsonTypeInfo.As.PROPERTY, - property = "provider", - visible = true -) -@JsonSubTypes({ - @JsonSubTypes.Type(value = OpenAiChatModel.class, name = "OPENAI"), - @JsonSubTypes.Type(value = AzureOpenAiChatModel.class, name = "AZURE_OPENAI"), - @JsonSubTypes.Type(value = GoogleAiGeminiChatModel.class, name = "GOOGLE_AI_GEMINI"), - @JsonSubTypes.Type(value = GoogleVertexAiGeminiChatModel.class, name = "GOOGLE_VERTEX_AI_GEMINI"), - @JsonSubTypes.Type(value = MistralAiChatModel.class, name = "MISTRAL_AI"), - @JsonSubTypes.Type(value = AnthropicChatModel.class, name = "ANTHROPIC"), - @JsonSubTypes.Type(value = AmazonBedrockChatModel.class, name = "AMAZON_BEDROCK"), - @JsonSubTypes.Type(value = GitHubModelsChatModel.class, name = "GITHUB_MODELS") -}) -public interface AiModel { - - AiProvider provider(); - - @JsonTypeInfo( - use = JsonTypeInfo.Id.NAME, - include = JsonTypeInfo.As.EXTERNAL_PROPERTY, - property = "provider" - ) - @JsonSubTypes({ - @JsonSubTypes.Type(value = OpenAiProviderConfig.class, name = "OPENAI"), - @JsonSubTypes.Type(value = AzureOpenAiProviderConfig.class, name = "AZURE_OPENAI"), - @JsonSubTypes.Type(value = GoogleAiGeminiProviderConfig.class, name = "GOOGLE_AI_GEMINI"), - @JsonSubTypes.Type(value = GoogleVertexAiGeminiProviderConfig.class, name = "GOOGLE_VERTEX_AI_GEMINI"), - @JsonSubTypes.Type(value = MistralAiProviderConfig.class, name = "MISTRAL_AI"), - @JsonSubTypes.Type(value = AnthropicProviderConfig.class, name = "ANTHROPIC"), - @JsonSubTypes.Type(value = AmazonBedrockProviderConfig.class, name = "AMAZON_BEDROCK"), - @JsonSubTypes.Type(value = GitHubModelsProviderConfig.class, name = "GITHUB_MODELS") - }) - AiProviderConfig providerConfig(); - - @JsonProperty("modelType") - AiModelType modelType(); - - C modelConfig(); - - AiModel withModelConfig(C config); - -} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/AiModelConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/AiModelConfig.java index 1b0ab7921f..d9e6a1753e 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/AiModelConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/AiModelConfig.java @@ -15,8 +15,66 @@ */ package org.thingsboard.server.common.data.ai.model; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import org.thingsboard.server.common.data.ai.model.chat.AmazonBedrockChatModelConfig; +import org.thingsboard.server.common.data.ai.model.chat.AnthropicChatModelConfig; +import org.thingsboard.server.common.data.ai.model.chat.AzureOpenAiChatModelConfig; +import org.thingsboard.server.common.data.ai.model.chat.GitHubModelsChatModelConfig; +import org.thingsboard.server.common.data.ai.model.chat.GoogleAiGeminiChatModelConfig; +import org.thingsboard.server.common.data.ai.model.chat.GoogleVertexAiGeminiChatModelConfig; +import org.thingsboard.server.common.data.ai.model.chat.MistralAiChatModelConfig; +import org.thingsboard.server.common.data.ai.model.chat.OpenAiChatModelConfig; +import org.thingsboard.server.common.data.ai.provider.AiProvider; +import org.thingsboard.server.common.data.ai.provider.AiProviderConfig; +import org.thingsboard.server.common.data.ai.provider.AmazonBedrockProviderConfig; +import org.thingsboard.server.common.data.ai.provider.AnthropicProviderConfig; +import org.thingsboard.server.common.data.ai.provider.AzureOpenAiProviderConfig; +import org.thingsboard.server.common.data.ai.provider.GitHubModelsProviderConfig; +import org.thingsboard.server.common.data.ai.provider.GoogleAiGeminiProviderConfig; +import org.thingsboard.server.common.data.ai.provider.GoogleVertexAiGeminiProviderConfig; +import org.thingsboard.server.common.data.ai.provider.MistralAiProviderConfig; +import org.thingsboard.server.common.data.ai.provider.OpenAiProviderConfig; + +@JsonTypeInfo( + use = JsonTypeInfo.Id.NAME, + include = JsonTypeInfo.As.PROPERTY, + property = "provider", + visible = true +) +@JsonSubTypes({ + @JsonSubTypes.Type(value = OpenAiChatModelConfig.class, name = "OPENAI"), + @JsonSubTypes.Type(value = AzureOpenAiChatModelConfig.class, name = "AZURE_OPENAI"), + @JsonSubTypes.Type(value = GoogleAiGeminiChatModelConfig.class, name = "GOOGLE_AI_GEMINI"), + @JsonSubTypes.Type(value = GoogleVertexAiGeminiChatModelConfig.class, name = "GOOGLE_VERTEX_AI_GEMINI"), + @JsonSubTypes.Type(value = MistralAiChatModelConfig.class, name = "MISTRAL_AI"), + @JsonSubTypes.Type(value = AnthropicChatModelConfig.class, name = "ANTHROPIC"), + @JsonSubTypes.Type(value = AmazonBedrockChatModelConfig.class, name = "AMAZON_BEDROCK"), + @JsonSubTypes.Type(value = GitHubModelsChatModelConfig.class, name = "GITHUB_MODELS") +}) public interface AiModelConfig { - String modelId(); + AiProvider provider(); + + @JsonTypeInfo( + use = JsonTypeInfo.Id.NAME, + include = JsonTypeInfo.As.EXTERNAL_PROPERTY, + property = "provider" + ) + @JsonSubTypes({ + @JsonSubTypes.Type(value = OpenAiProviderConfig.class, name = "OPENAI"), + @JsonSubTypes.Type(value = AzureOpenAiProviderConfig.class, name = "AZURE_OPENAI"), + @JsonSubTypes.Type(value = GoogleAiGeminiProviderConfig.class, name = "GOOGLE_AI_GEMINI"), + @JsonSubTypes.Type(value = GoogleVertexAiGeminiProviderConfig.class, name = "GOOGLE_VERTEX_AI_GEMINI"), + @JsonSubTypes.Type(value = MistralAiProviderConfig.class, name = "MISTRAL_AI"), + @JsonSubTypes.Type(value = AnthropicProviderConfig.class, name = "ANTHROPIC"), + @JsonSubTypes.Type(value = AmazonBedrockProviderConfig.class, name = "AMAZON_BEDROCK"), + @JsonSubTypes.Type(value = GitHubModelsProviderConfig.class, name = "GITHUB_MODELS") + }) + AiProviderConfig providerConfig(); + + @JsonProperty("modelType") + AiModelType modelType(); } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AiChatModel.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AiChatModel.java deleted file mode 100644 index b9a2737b20..0000000000 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AiChatModel.java +++ /dev/null @@ -1,41 +0,0 @@ -/** - * 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.ai.model.chat; - -import dev.langchain4j.model.chat.ChatModel; -import org.thingsboard.server.common.data.ai.model.AiModel; -import org.thingsboard.server.common.data.ai.model.AiModelType; - -public sealed interface AiChatModel> extends AiModel - permits - OpenAiChatModel, AzureOpenAiChatModel, GoogleAiGeminiChatModel, - GoogleVertexAiGeminiChatModel, MistralAiChatModel, AnthropicChatModel, - AmazonBedrockChatModel, GitHubModelsChatModel { - - ChatModel configure(Langchain4jChatModelConfigurer configurer); - - @Override - default AiModelType modelType() { - return AiModelType.CHAT; - } - - @Override - C modelConfig(); - - @Override - AiChatModel withModelConfig(C config); - -} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AiChatModelConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AiChatModelConfig.java index 67d9827cc6..2284d8b485 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AiChatModelConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AiChatModelConfig.java @@ -15,13 +15,22 @@ */ package org.thingsboard.server.common.data.ai.model.chat; +import dev.langchain4j.model.chat.ChatModel; import org.thingsboard.server.common.data.ai.model.AiModelConfig; +import org.thingsboard.server.common.data.ai.model.AiModelType; public sealed interface AiChatModelConfig> extends AiModelConfig permits - OpenAiChatModel.Config, AzureOpenAiChatModel.Config, GoogleAiGeminiChatModel.Config, - GoogleVertexAiGeminiChatModel.Config, MistralAiChatModel.Config, AnthropicChatModel.Config, - AmazonBedrockChatModel.Config, GitHubModelsChatModel.Config { + OpenAiChatModelConfig, AzureOpenAiChatModelConfig, GoogleAiGeminiChatModelConfig, + GoogleVertexAiGeminiChatModelConfig, MistralAiChatModelConfig, AnthropicChatModelConfig, + AmazonBedrockChatModelConfig, GitHubModelsChatModelConfig { + + ChatModel configure(Langchain4jChatModelConfigurer configurer); + + @Override + default AiModelType modelType() { + return AiModelType.CHAT; + } Integer timeoutSeconds(); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AmazonBedrockChatModel.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AmazonBedrockChatModelConfig.java similarity index 75% rename from common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AmazonBedrockChatModel.java rename to common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AmazonBedrockChatModelConfig.java index 356bdd5c42..1f4e4645dd 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AmazonBedrockChatModel.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AmazonBedrockChatModelConfig.java @@ -27,27 +27,22 @@ import org.thingsboard.server.common.data.ai.model.AiModelType; import org.thingsboard.server.common.data.ai.provider.AiProvider; import org.thingsboard.server.common.data.ai.provider.AmazonBedrockProviderConfig; -public record AmazonBedrockChatModel( +public record AmazonBedrockChatModelConfig( AiModelType modelType, @NotNull @Valid AmazonBedrockProviderConfig providerConfig, - @With @NotNull @Valid Config modelConfig -) implements AiChatModel { + @NotBlank String modelId, + @PositiveOrZero Double temperature, + @Positive @Max(1) Double topP, + @Positive Integer maxOutputTokens, + @With @Positive Integer timeoutSeconds, + @With @PositiveOrZero Integer maxRetries +) implements AiChatModelConfig { @Override public AiProvider provider() { return AiProvider.AMAZON_BEDROCK; } - @With - public record Config( - @NotBlank String modelId, - @PositiveOrZero Double temperature, - @Positive @Max(1) Double topP, - @Positive Integer maxOutputTokens, - @Positive Integer timeoutSeconds, - @PositiveOrZero Integer maxRetries - ) implements AiChatModelConfig {} - @Override public ChatModel configure(Langchain4jChatModelConfigurer configurer) { return configurer.configureChatModel(this); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AnthropicChatModel.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AnthropicChatModelConfig.java similarity index 74% rename from common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AnthropicChatModel.java rename to common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AnthropicChatModelConfig.java index 9ab43799fb..69af0f8c2b 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AnthropicChatModel.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AnthropicChatModelConfig.java @@ -27,28 +27,23 @@ import org.thingsboard.server.common.data.ai.model.AiModelType; import org.thingsboard.server.common.data.ai.provider.AiProvider; import org.thingsboard.server.common.data.ai.provider.AnthropicProviderConfig; -public record AnthropicChatModel( +public record AnthropicChatModelConfig( AiModelType modelType, @NotNull @Valid AnthropicProviderConfig providerConfig, - @With @NotNull @Valid Config modelConfig -) implements AiChatModel { + @NotBlank String modelId, + @PositiveOrZero Double temperature, + @Positive @Max(1) Double topP, + @Positive Integer topK, + @Positive Integer maxOutputTokens, + @With @Positive Integer timeoutSeconds, + @With @PositiveOrZero Integer maxRetries +) implements AiChatModelConfig { @Override public AiProvider provider() { return AiProvider.ANTHROPIC; } - @With - public record Config( - @NotBlank String modelId, - @PositiveOrZero Double temperature, - @Positive @Max(1) Double topP, - @Positive Integer topK, - @Positive Integer maxOutputTokens, - @Positive Integer timeoutSeconds, - @PositiveOrZero Integer maxRetries - ) implements AiChatModelConfig {} - @Override public ChatModel configure(Langchain4jChatModelConfigurer configurer) { return configurer.configureChatModel(this); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AzureOpenAiChatModel.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AzureOpenAiChatModelConfig.java similarity index 72% rename from common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AzureOpenAiChatModel.java rename to common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AzureOpenAiChatModelConfig.java index f2cb47190b..afe5f6c563 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AzureOpenAiChatModel.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AzureOpenAiChatModelConfig.java @@ -27,29 +27,24 @@ import org.thingsboard.server.common.data.ai.model.AiModelType; import org.thingsboard.server.common.data.ai.provider.AiProvider; import org.thingsboard.server.common.data.ai.provider.AzureOpenAiProviderConfig; -public record AzureOpenAiChatModel( +public record AzureOpenAiChatModelConfig( AiModelType modelType, @NotNull @Valid AzureOpenAiProviderConfig providerConfig, - @With @NotNull @Valid Config modelConfig -) implements AiChatModel { + @NotBlank String modelId, + @PositiveOrZero Double temperature, + @Positive @Max(1) Double topP, + Double frequencyPenalty, + Double presencePenalty, + @Positive Integer maxOutputTokens, + @With @Positive Integer timeoutSeconds, + @With @PositiveOrZero Integer maxRetries +) implements AiChatModelConfig { @Override public AiProvider provider() { return AiProvider.AZURE_OPENAI; } - @With - public record Config( - @NotBlank String modelId, - @PositiveOrZero Double temperature, - @Positive @Max(1) Double topP, - Double frequencyPenalty, - Double presencePenalty, - @Positive Integer maxOutputTokens, - @Positive Integer timeoutSeconds, - @PositiveOrZero Integer maxRetries - ) implements AiChatModelConfig {} - @Override public ChatModel configure(Langchain4jChatModelConfigurer configurer) { return configurer.configureChatModel(this); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GitHubModelsChatModel.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GitHubModelsChatModelConfig.java similarity index 72% rename from common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GitHubModelsChatModel.java rename to common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GitHubModelsChatModelConfig.java index 723f69299d..ecadb7d453 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GitHubModelsChatModel.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GitHubModelsChatModelConfig.java @@ -27,29 +27,24 @@ import org.thingsboard.server.common.data.ai.model.AiModelType; import org.thingsboard.server.common.data.ai.provider.AiProvider; import org.thingsboard.server.common.data.ai.provider.GitHubModelsProviderConfig; -public record GitHubModelsChatModel( +public record GitHubModelsChatModelConfig( AiModelType modelType, @NotNull @Valid GitHubModelsProviderConfig providerConfig, - @With @NotNull @Valid Config modelConfig -) implements AiChatModel { + @NotBlank String modelId, + @PositiveOrZero Double temperature, + @Positive @Max(1) Double topP, + Double frequencyPenalty, + Double presencePenalty, + @Positive Integer maxOutputTokens, + @With @Positive Integer timeoutSeconds, + @With @PositiveOrZero Integer maxRetries +) implements AiChatModelConfig { @Override public AiProvider provider() { return AiProvider.GITHUB_MODELS; } - @With - public record Config( - @NotBlank String modelId, - @PositiveOrZero Double temperature, - @Positive @Max(1) Double topP, - Double frequencyPenalty, - Double presencePenalty, - @Positive Integer maxOutputTokens, - @Positive Integer timeoutSeconds, - @PositiveOrZero Integer maxRetries - ) implements AiChatModelConfig {} - @Override public ChatModel configure(Langchain4jChatModelConfigurer configurer) { return configurer.configureChatModel(this); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleAiGeminiChatModel.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleAiGeminiChatModelConfig.java similarity index 71% rename from common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleAiGeminiChatModel.java rename to common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleAiGeminiChatModelConfig.java index b9cb65d3d9..1922dea6a1 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleAiGeminiChatModel.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleAiGeminiChatModelConfig.java @@ -27,30 +27,25 @@ import org.thingsboard.server.common.data.ai.model.AiModelType; import org.thingsboard.server.common.data.ai.provider.AiProvider; import org.thingsboard.server.common.data.ai.provider.GoogleAiGeminiProviderConfig; -public record GoogleAiGeminiChatModel( +public record GoogleAiGeminiChatModelConfig( AiModelType modelType, @NotNull @Valid GoogleAiGeminiProviderConfig providerConfig, - @With @NotNull @Valid Config modelConfig -) implements AiChatModel { + @NotBlank String modelId, + @PositiveOrZero Double temperature, + @Positive @Max(1) Double topP, + @Positive Integer topK, + Double frequencyPenalty, + Double presencePenalty, + @Positive Integer maxOutputTokens, + @With @Positive Integer timeoutSeconds, + @With @PositiveOrZero Integer maxRetries +) implements AiChatModelConfig { @Override public AiProvider provider() { return AiProvider.GOOGLE_AI_GEMINI; } - @With - public record Config( - @NotBlank String modelId, - @PositiveOrZero Double temperature, - @Positive @Max(1) Double topP, - @Positive Integer topK, - Double frequencyPenalty, - Double presencePenalty, - @Positive Integer maxOutputTokens, - @Positive Integer timeoutSeconds, - @PositiveOrZero Integer maxRetries - ) implements AiChatModelConfig {} - @Override public ChatModel configure(Langchain4jChatModelConfigurer configurer) { return configurer.configureChatModel(this); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleVertexAiGeminiChatModel.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleVertexAiGeminiChatModelConfig.java similarity index 71% rename from common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleVertexAiGeminiChatModel.java rename to common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleVertexAiGeminiChatModelConfig.java index 83b7506e11..e8691c0926 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleVertexAiGeminiChatModel.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleVertexAiGeminiChatModelConfig.java @@ -27,30 +27,25 @@ import org.thingsboard.server.common.data.ai.model.AiModelType; import org.thingsboard.server.common.data.ai.provider.AiProvider; import org.thingsboard.server.common.data.ai.provider.GoogleVertexAiGeminiProviderConfig; -public record GoogleVertexAiGeminiChatModel( +public record GoogleVertexAiGeminiChatModelConfig( AiModelType modelType, @NotNull @Valid GoogleVertexAiGeminiProviderConfig providerConfig, - @With @NotNull @Valid Config modelConfig -) implements AiChatModel { + @NotBlank String modelId, + @PositiveOrZero Double temperature, + @Positive @Max(1) Double topP, + @Positive Integer topK, + Double frequencyPenalty, + Double presencePenalty, + @Positive Integer maxOutputTokens, + @With @Positive Integer timeoutSeconds, + @With @PositiveOrZero Integer maxRetries +) implements AiChatModelConfig { @Override public AiProvider provider() { return AiProvider.GOOGLE_VERTEX_AI_GEMINI; } - @With - public record Config( - @NotBlank String modelId, - @PositiveOrZero Double temperature, - @Positive @Max(1) Double topP, - @Positive Integer topK, - Double frequencyPenalty, - Double presencePenalty, - @Positive Integer maxOutputTokens, - @Positive Integer timeoutSeconds, - @PositiveOrZero Integer maxRetries - ) implements AiChatModelConfig {} - @Override public ChatModel configure(Langchain4jChatModelConfigurer configurer) { return configurer.configureChatModel(this); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/Langchain4jChatModelConfigurer.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/Langchain4jChatModelConfigurer.java index ea9fb80e73..c9c1bc3173 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/Langchain4jChatModelConfigurer.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/Langchain4jChatModelConfigurer.java @@ -19,20 +19,20 @@ import dev.langchain4j.model.chat.ChatModel; public interface Langchain4jChatModelConfigurer { - ChatModel configureChatModel(OpenAiChatModel chatModel); + ChatModel configureChatModel(OpenAiChatModelConfig chatModelConfig); - ChatModel configureChatModel(AzureOpenAiChatModel chatModel); + ChatModel configureChatModel(AzureOpenAiChatModelConfig chatModelConfig); - ChatModel configureChatModel(GoogleAiGeminiChatModel chatModel); + ChatModel configureChatModel(GoogleAiGeminiChatModelConfig chatModelConfig); - ChatModel configureChatModel(GoogleVertexAiGeminiChatModel chatModel); + ChatModel configureChatModel(GoogleVertexAiGeminiChatModelConfig chatModelConfig); - ChatModel configureChatModel(MistralAiChatModel chatModel); + ChatModel configureChatModel(MistralAiChatModelConfig chatModelConfig); - ChatModel configureChatModel(AnthropicChatModel chatModel); + ChatModel configureChatModel(AnthropicChatModelConfig chatModelConfig); - ChatModel configureChatModel(AmazonBedrockChatModel chatModel); + ChatModel configureChatModel(AmazonBedrockChatModelConfig chatModelConfig); - ChatModel configureChatModel(GitHubModelsChatModel chatModel); + ChatModel configureChatModel(GitHubModelsChatModelConfig chatModelConfig); } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/MistralAiChatModel.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/MistralAiChatModelConfig.java similarity index 72% rename from common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/MistralAiChatModel.java rename to common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/MistralAiChatModelConfig.java index 58ed807819..ea2409ba35 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/MistralAiChatModel.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/MistralAiChatModelConfig.java @@ -27,29 +27,24 @@ import org.thingsboard.server.common.data.ai.model.AiModelType; import org.thingsboard.server.common.data.ai.provider.AiProvider; import org.thingsboard.server.common.data.ai.provider.MistralAiProviderConfig; -public record MistralAiChatModel( +public record MistralAiChatModelConfig( AiModelType modelType, @NotNull @Valid MistralAiProviderConfig providerConfig, - @With @NotNull @Valid Config modelConfig -) implements AiChatModel { + @NotBlank String modelId, + @PositiveOrZero Double temperature, + @Positive @Max(1) Double topP, + Double frequencyPenalty, + Double presencePenalty, + @Positive Integer maxOutputTokens, + @With @Positive Integer timeoutSeconds, + @With @PositiveOrZero Integer maxRetries +) implements AiChatModelConfig { @Override public AiProvider provider() { return AiProvider.MISTRAL_AI; } - @With - public record Config( - @NotBlank String modelId, - @PositiveOrZero Double temperature, - @Positive @Max(1) Double topP, - Double frequencyPenalty, - Double presencePenalty, - @Positive Integer maxOutputTokens, - @Positive Integer timeoutSeconds, - @PositiveOrZero Integer maxRetries - ) implements AiChatModelConfig {} - @Override public ChatModel configure(Langchain4jChatModelConfigurer configurer) { return configurer.configureChatModel(this); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/OpenAiChatModel.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/OpenAiChatModelConfig.java similarity index 73% rename from common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/OpenAiChatModel.java rename to common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/OpenAiChatModelConfig.java index 4250291c32..95f6f1cdc3 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/OpenAiChatModel.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/OpenAiChatModelConfig.java @@ -27,29 +27,24 @@ import org.thingsboard.server.common.data.ai.model.AiModelType; import org.thingsboard.server.common.data.ai.provider.AiProvider; import org.thingsboard.server.common.data.ai.provider.OpenAiProviderConfig; -public record OpenAiChatModel( +public record OpenAiChatModelConfig( AiModelType modelType, @NotNull @Valid OpenAiProviderConfig providerConfig, - @With @NotNull @Valid Config modelConfig -) implements AiChatModel { + @NotBlank String modelId, + @PositiveOrZero Double temperature, + @Positive @Max(1) Double topP, + Double frequencyPenalty, + Double presencePenalty, + @Positive Integer maxOutputTokens, + @With @Positive Integer timeoutSeconds, + @With @PositiveOrZero Integer maxRetries +) implements AiChatModelConfig { @Override public AiProvider provider() { return AiProvider.OPENAI; } - @With - public record Config( - @NotBlank String modelId, - @PositiveOrZero Double temperature, - @Positive @Max(1) Double topP, - Double frequencyPenalty, - Double presencePenalty, - @Positive Integer maxOutputTokens, - @Positive Integer timeoutSeconds, - @PositiveOrZero Integer maxRetries - ) implements AiChatModelConfig {} - @Override public ChatModel configure(Langchain4jChatModelConfigurer configurer) { return configurer.configureChatModel(this); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/AiModelSettingsId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/AiModelId.java similarity index 72% rename from common/data/src/main/java/org/thingsboard/server/common/data/id/AiModelSettingsId.java rename to common/data/src/main/java/org/thingsboard/server/common/data/id/AiModelId.java index 83b4fefab2..cac9e8200c 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/AiModelSettingsId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/AiModelId.java @@ -23,29 +23,29 @@ import org.thingsboard.server.common.data.EntityType; import java.io.Serial; import java.util.UUID; -public final class AiModelSettingsId extends UUIDBased implements EntityId { +public final class AiModelId extends UUIDBased implements EntityId { @Serial private static final long serialVersionUID = 3021036138554389754L; @JsonCreator - public AiModelSettingsId(@JsonProperty("id") UUID id) { + public AiModelId(@JsonProperty("id") UUID id) { super(id); } @Override @Schema( requiredMode = Schema.RequiredMode.REQUIRED, - description = "Entity type of the AI model settings", - example = "AI_MODEL_SETTINGS", - allowableValues = "AI_MODEL_SETTINGS" + description = "Entity type of the AI model", + example = "AI_MODEL", + allowableValues = "AI_MODEL" ) public EntityType getEntityType() { - return EntityType.AI_MODEL_SETTINGS; + return EntityType.AI_MODEL; } - public static AiModelSettingsId fromString(String uuid) { - return new AiModelSettingsId(UUID.fromString(uuid)); + public static AiModelId fromString(String uuid) { + return new AiModelId(UUID.fromString(uuid)); } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java index 64504545d4..3638ed1535 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java @@ -85,7 +85,7 @@ public class EntityIdFactory { case CALCULATED_FIELD -> new CalculatedFieldId(uuid); case CALCULATED_FIELD_LINK -> new CalculatedFieldLinkId(uuid); case JOB -> new JobId(uuid); - case AI_MODEL_SETTINGS -> new AiModelSettingsId(uuid); + case AI_MODEL -> new AiModelId(uuid); default -> throw new IllegalArgumentException("EntityType " + type + " is not supported!"); }; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/sync/JsonTbEntity.java b/common/data/src/main/java/org/thingsboard/server/common/data/sync/JsonTbEntity.java index 6914b75636..a89fc0b670 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/sync/JsonTbEntity.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/sync/JsonTbEntity.java @@ -26,7 +26,7 @@ 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.TbResource; -import org.thingsboard.server.common.data.ai.AiModelSettings; +import org.thingsboard.server.common.data.ai.AiModel; import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.asset.AssetProfile; import org.thingsboard.server.common.data.notification.rule.NotificationRule; @@ -60,7 +60,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 = "AI_MODEL_SETTINGS", value = AiModelSettings.class) + @Type(name = "AI_MODEL", value = AiModel.class) }) @JsonIgnoreProperties(value = {"tenantId", "createdTime", "version"}, ignoreUnknown = true) public @interface JsonTbEntity {} diff --git a/common/proto/src/main/proto/queue.proto b/common/proto/src/main/proto/queue.proto index f3cb8a5eea..9d213c7fdb 100644 --- a/common/proto/src/main/proto/queue.proto +++ b/common/proto/src/main/proto/queue.proto @@ -64,7 +64,7 @@ enum EntityTypeProto { CALCULATED_FIELD = 39; CALCULATED_FIELD_LINK = 40; JOB = 41; - AI_MODEL_SETTINGS = 42; + AI_MODEL = 42; } enum ApiUsageRecordKeyProto { diff --git a/dao/src/main/java/org/thingsboard/server/dao/ai/AiModelSettingsCacheEvictEvent.java b/dao/src/main/java/org/thingsboard/server/dao/ai/AiModelCacheEvictEvent.java similarity index 58% rename from dao/src/main/java/org/thingsboard/server/dao/ai/AiModelSettingsCacheEvictEvent.java rename to dao/src/main/java/org/thingsboard/server/dao/ai/AiModelCacheEvictEvent.java index 3bf7dce9ba..b0d4b6fdb6 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/ai/AiModelSettingsCacheEvictEvent.java +++ b/dao/src/main/java/org/thingsboard/server/dao/ai/AiModelCacheEvictEvent.java @@ -15,26 +15,26 @@ */ package org.thingsboard.server.dao.ai; -import org.thingsboard.server.common.data.ai.AiModelSettings; +import org.thingsboard.server.common.data.ai.AiModel; import static java.util.Objects.requireNonNull; -import static org.thingsboard.server.dao.ai.AiModelSettingsCacheEvictEvent.Deleted; -import static org.thingsboard.server.dao.ai.AiModelSettingsCacheEvictEvent.Saved; +import static org.thingsboard.server.dao.ai.AiModelCacheEvictEvent.Deleted; +import static org.thingsboard.server.dao.ai.AiModelCacheEvictEvent.Saved; -sealed interface AiModelSettingsCacheEvictEvent permits Saved, Deleted { +sealed interface AiModelCacheEvictEvent permits Saved, Deleted { - AiModelSettingsCacheKey cacheKey(); + AiModelCacheKey cacheKey(); - record Saved(AiModelSettingsCacheKey cacheKey, AiModelSettings savedSettings) implements AiModelSettingsCacheEvictEvent { + record Saved(AiModelCacheKey cacheKey, AiModel savedModel) implements AiModelCacheEvictEvent { public Saved { requireNonNull(cacheKey); - requireNonNull(savedSettings); + requireNonNull(savedModel); } } - record Deleted(AiModelSettingsCacheKey cacheKey) implements AiModelSettingsCacheEvictEvent { + record Deleted(AiModelCacheKey cacheKey) implements AiModelCacheEvictEvent { public Deleted { requireNonNull(cacheKey); diff --git a/dao/src/main/java/org/thingsboard/server/dao/ai/AiModelSettingsCacheKey.java b/dao/src/main/java/org/thingsboard/server/dao/ai/AiModelCacheKey.java similarity index 68% rename from dao/src/main/java/org/thingsboard/server/dao/ai/AiModelSettingsCacheKey.java rename to dao/src/main/java/org/thingsboard/server/dao/ai/AiModelCacheKey.java index 60d0ccaee9..6b73ad7b28 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/ai/AiModelSettingsCacheKey.java +++ b/dao/src/main/java/org/thingsboard/server/dao/ai/AiModelCacheKey.java @@ -17,7 +17,7 @@ package org.thingsboard.server.dao.ai; import org.checkerframework.checker.nullness.qual.NonNull; import org.thingsboard.server.cache.VersionedCacheKey; -import org.thingsboard.server.common.data.id.AiModelSettingsId; +import org.thingsboard.server.common.data.id.AiModelId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; @@ -25,22 +25,22 @@ import java.util.UUID; import static java.util.Objects.requireNonNull; -record AiModelSettingsCacheKey(UUID tenantId, UUID settingsId) implements VersionedCacheKey { +record AiModelCacheKey(UUID tenantId, UUID modelId) implements VersionedCacheKey { - AiModelSettingsCacheKey { + AiModelCacheKey { requireNonNull(tenantId); - requireNonNull(settingsId); + requireNonNull(modelId); if (TenantId.SYS_TENANT_ID.getId().equals(tenantId)) { throw new IllegalArgumentException("Tenant ID must not be the system tenant ID"); } - if (EntityId.NULL_UUID.equals(settingsId)) { - throw new IllegalArgumentException("Settings ID must not be reserved null UUID"); + if (EntityId.NULL_UUID.equals(modelId)) { + throw new IllegalArgumentException("Model ID must not be reserved null UUID"); } } - static AiModelSettingsCacheKey of(TenantId tenantId, AiModelSettingsId settingsId) { - return new AiModelSettingsCacheKey(tenantId.getId(), settingsId.getId()); + static AiModelCacheKey of(TenantId tenantId, AiModelId modelId) { + return new AiModelCacheKey(tenantId.getId(), modelId.getId()); } @Override @@ -51,7 +51,7 @@ record AiModelSettingsCacheKey(UUID tenantId, UUID settingsId) implements Versio @NonNull @Override public String toString() { - return /* cache name */ "_" + tenantId + "_" + settingsId; + return /* cache name */ "_" + tenantId + "_" + modelId; } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/ai/AiModelSettingsCaffeineCache.java b/dao/src/main/java/org/thingsboard/server/dao/ai/AiModelCaffeineCache.java similarity index 75% rename from dao/src/main/java/org/thingsboard/server/dao/ai/AiModelSettingsCaffeineCache.java rename to dao/src/main/java/org/thingsboard/server/dao/ai/AiModelCaffeineCache.java index d758cb21c5..165efcd4e2 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/ai/AiModelSettingsCaffeineCache.java +++ b/dao/src/main/java/org/thingsboard/server/dao/ai/AiModelCaffeineCache.java @@ -20,14 +20,14 @@ import org.springframework.cache.CacheManager; import org.springframework.stereotype.Component; import org.thingsboard.server.cache.VersionedCaffeineTbCache; import org.thingsboard.server.common.data.CacheConstants; -import org.thingsboard.server.common.data.ai.AiModelSettings; +import org.thingsboard.server.common.data.ai.AiModel; -@Component("AiModelSettingsCache") +@Component("AiModelCache") @ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "caffeine", matchIfMissing = true) -class AiModelSettingsCaffeineCache extends VersionedCaffeineTbCache { +class AiModelCaffeineCache extends VersionedCaffeineTbCache { - AiModelSettingsCaffeineCache(CacheManager cacheManager) { - super(cacheManager, CacheConstants.AI_MODEL_SETTINGS_CACHE); + AiModelCaffeineCache(CacheManager cacheManager) { + super(cacheManager, CacheConstants.AI_MODEL_CACHE); } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/ai/AiModelSettingsDao.java b/dao/src/main/java/org/thingsboard/server/dao/ai/AiModelDao.java similarity index 60% rename from dao/src/main/java/org/thingsboard/server/dao/ai/AiModelSettingsDao.java rename to dao/src/main/java/org/thingsboard/server/dao/ai/AiModelDao.java index c8d6b65369..e788685bfa 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/ai/AiModelSettingsDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/ai/AiModelDao.java @@ -15,8 +15,8 @@ */ package org.thingsboard.server.dao.ai; -import org.thingsboard.server.common.data.ai.AiModelSettings; -import org.thingsboard.server.common.data.id.AiModelSettingsId; +import org.thingsboard.server.common.data.ai.AiModel; +import org.thingsboard.server.common.data.id.AiModelId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.dao.ExportableEntityDao; import org.thingsboard.server.dao.TenantEntityDao; @@ -24,14 +24,14 @@ import org.thingsboard.server.dao.TenantEntityDao; import java.util.Optional; import java.util.Set; -public interface AiModelSettingsDao extends TenantEntityDao, ExportableEntityDao { +public interface AiModelDao extends TenantEntityDao, ExportableEntityDao { - Optional findByTenantIdAndId(TenantId tenantId, AiModelSettingsId settingsId); + Optional findByTenantIdAndId(TenantId tenantId, AiModelId modelId); - boolean deleteById(TenantId tenantId, AiModelSettingsId settingsId); + boolean deleteById(TenantId tenantId, AiModelId modelId); - Set deleteByTenantId(TenantId tenantId); + Set deleteByTenantId(TenantId tenantId); - boolean deleteByTenantIdAndId(TenantId tenantId, AiModelSettingsId settingsId); + boolean deleteByTenantIdAndId(TenantId tenantId, AiModelId modelId); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/ai/AiModelSettingsRedisCache.java b/dao/src/main/java/org/thingsboard/server/dao/ai/AiModelRedisCache.java similarity index 71% rename from dao/src/main/java/org/thingsboard/server/dao/ai/AiModelSettingsRedisCache.java rename to dao/src/main/java/org/thingsboard/server/dao/ai/AiModelRedisCache.java index 8674c522ff..7bec37875f 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/ai/AiModelSettingsRedisCache.java +++ b/dao/src/main/java/org/thingsboard/server/dao/ai/AiModelRedisCache.java @@ -23,14 +23,14 @@ import org.thingsboard.server.cache.TBRedisCacheConfiguration; import org.thingsboard.server.cache.TbJsonRedisSerializer; import org.thingsboard.server.cache.VersionedRedisTbCache; import org.thingsboard.server.common.data.CacheConstants; -import org.thingsboard.server.common.data.ai.AiModelSettings; +import org.thingsboard.server.common.data.ai.AiModel; -@Component("AiSettingsCache") +@Component("AiModelCache") @ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "redis") -class AiModelSettingsRedisCache extends VersionedRedisTbCache { +class AiModelRedisCache extends VersionedRedisTbCache { - AiModelSettingsRedisCache(TBRedisCacheConfiguration configuration, CacheSpecsMap cacheSpecsMap, RedisConnectionFactory connectionFactory) { - super(CacheConstants.AI_MODEL_SETTINGS_CACHE, cacheSpecsMap, connectionFactory, configuration, new TbJsonRedisSerializer<>(AiModelSettings.class)); + AiModelRedisCache(TBRedisCacheConfiguration configuration, CacheSpecsMap cacheSpecsMap, RedisConnectionFactory connectionFactory) { + super(CacheConstants.AI_MODEL_CACHE, cacheSpecsMap, connectionFactory, configuration, new TbJsonRedisSerializer<>(AiModel.class)); } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/ai/AiModelServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/ai/AiModelServiceImpl.java new file mode 100644 index 0000000000..b091a29247 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/ai/AiModelServiceImpl.java @@ -0,0 +1,149 @@ +/** + * 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.dao.ai; + +import com.google.common.util.concurrent.FluentFuture; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.event.TransactionalEventListener; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.ai.AiModel; +import org.thingsboard.server.common.data.id.AiModelId; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.HasId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.dao.entity.CachedVersionedEntityService; +import org.thingsboard.server.dao.model.sql.AiModelEntity; +import org.thingsboard.server.dao.service.DataValidator; +import org.thingsboard.server.dao.sql.JpaExecutorService; + +import java.util.Optional; +import java.util.Set; + +import static org.thingsboard.server.dao.service.Validator.validatePageLink; + +@Service +@RequiredArgsConstructor +class AiModelServiceImpl extends CachedVersionedEntityService implements AiModelService { + + private final DataValidator aiModelValidator; + + private final JpaExecutorService jpaExecutor; + private final AiModelDao aiModelDao; + + @Override + @TransactionalEventListener + public void handleEvictEvent(AiModelCacheEvictEvent event) { + var cacheKey = event.cacheKey(); + if (event instanceof AiModelCacheEvictEvent.Saved savedEvent) { + cache.put(cacheKey, savedEvent.savedModel()); + } else if (event instanceof AiModelCacheEvictEvent.Deleted) { + cache.evict(cacheKey); + } else { + throw new UnsupportedOperationException("Unsupported event type: " + event.getClass().getSimpleName()); + } + } + + @Override + @Transactional + public AiModel save(AiModel model) { + aiModelValidator.validate(model, AiModel::getTenantId); + + AiModel savedModel; + try { + savedModel = aiModelDao.saveAndFlush(model.getTenantId(), model); + } catch (Exception e) { + checkConstraintViolation(e, + "ai_model_name_unq_key", "AI model with such name already exist!", + "ai_model_external_id_unq_key", "AI model with such external ID already exists!"); + throw e; + } + + var cacheKey = AiModelCacheKey.of(savedModel.getTenantId(), savedModel.getId()); + publishEvictEvent(new AiModelCacheEvictEvent.Saved(cacheKey, savedModel)); + + return savedModel; + } + + @Override + public Optional findAiModelById(TenantId tenantId, AiModelId modelId) { + return Optional.ofNullable(aiModelDao.findById(tenantId, modelId.getId())); + } + + @Override + public PageData findAiModelsByTenantId(TenantId tenantId, PageLink pageLink) { + validatePageLink(pageLink, AiModelEntity.ALLOWED_SORT_PROPERTIES); + return aiModelDao.findAllByTenantId(tenantId, pageLink); + } + + @Override + public Optional findAiModelByTenantIdAndId(TenantId tenantId, AiModelId modelId) { + var cacheKey = AiModelCacheKey.of(tenantId, modelId); + return Optional.ofNullable(cache.get(cacheKey, () -> aiModelDao.findByTenantIdAndId(tenantId, modelId).orElse(null))); + } + + @Override + public FluentFuture> findAiModelByTenantIdAndIdAsync(TenantId tenantId, AiModelId modelId) { + return FluentFuture.from(jpaExecutor.submit(() -> findAiModelByTenantIdAndId(tenantId, modelId))); + } + + @Override + @Transactional + public boolean deleteByTenantIdAndId(TenantId tenantId, AiModelId modelId) { + return deleteByTenantIdAndIdInternal(tenantId, modelId); + } + + @Override + public Optional> findEntity(TenantId tenantId, EntityId entityId) { + return findAiModelByTenantIdAndId(tenantId, (AiModelId) entityId) + .map(model -> model); // necessary to cast to HasId + } + + @Override + public long countByTenantId(TenantId tenantId) { + return aiModelDao.countByTenantId(tenantId); + } + + @Override + @Transactional + public void deleteEntity(TenantId tenantId, EntityId id, boolean force) { + deleteByTenantIdAndIdInternal(tenantId, new AiModelId(id.getId())); + } + + private boolean deleteByTenantIdAndIdInternal(TenantId tenantId, AiModelId modelId) { + boolean deleted = aiModelDao.deleteByTenantIdAndId(tenantId, modelId); + if (deleted) { + publishEvictEvent(new AiModelCacheEvictEvent.Deleted(AiModelCacheKey.of(tenantId, modelId))); + } + return deleted; + } + + @Override + @Transactional + public void deleteByTenantId(TenantId tenantId) { + Set deleted = aiModelDao.deleteByTenantId(tenantId); + deleted.forEach(id -> publishEvictEvent(new AiModelCacheEvictEvent.Deleted(AiModelCacheKey.of(tenantId, id)))); + } + + @Override + public EntityType getEntityType() { + return EntityType.AI_MODEL; + } + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/ai/AiModelSettingsServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/ai/AiModelSettingsServiceImpl.java deleted file mode 100644 index 2494ab6a15..0000000000 --- a/dao/src/main/java/org/thingsboard/server/dao/ai/AiModelSettingsServiceImpl.java +++ /dev/null @@ -1,149 +0,0 @@ -/** - * 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.dao.ai; - -import com.google.common.util.concurrent.FluentFuture; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; -import org.springframework.transaction.event.TransactionalEventListener; -import org.thingsboard.server.common.data.EntityType; -import org.thingsboard.server.common.data.ai.AiModelSettings; -import org.thingsboard.server.common.data.id.AiModelSettingsId; -import org.thingsboard.server.common.data.id.EntityId; -import org.thingsboard.server.common.data.id.HasId; -import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.page.PageData; -import org.thingsboard.server.common.data.page.PageLink; -import org.thingsboard.server.dao.entity.CachedVersionedEntityService; -import org.thingsboard.server.dao.model.sql.AiModelSettingsEntity; -import org.thingsboard.server.dao.service.DataValidator; -import org.thingsboard.server.dao.sql.JpaExecutorService; - -import java.util.Optional; -import java.util.Set; - -import static org.thingsboard.server.dao.service.Validator.validatePageLink; - -@Service -@RequiredArgsConstructor -class AiModelSettingsServiceImpl extends CachedVersionedEntityService implements AiModelSettingsService { - - private final DataValidator aiModelSettingsValidator; - - private final JpaExecutorService jpaExecutor; - private final AiModelSettingsDao aiModelSettingsDao; - - @Override - @TransactionalEventListener - public void handleEvictEvent(AiModelSettingsCacheEvictEvent event) { - var cacheKey = event.cacheKey(); - if (event instanceof AiModelSettingsCacheEvictEvent.Saved savedEvent) { - cache.put(cacheKey, savedEvent.savedSettings()); - } else if (event instanceof AiModelSettingsCacheEvictEvent.Deleted) { - cache.evict(cacheKey); - } else { - throw new UnsupportedOperationException("Unsupported event type: " + event.getClass().getSimpleName()); - } - } - - @Override - @Transactional - public AiModelSettings save(AiModelSettings settings) { - aiModelSettingsValidator.validate(settings, AiModelSettings::getTenantId); - - AiModelSettings savedSettings; - try { - savedSettings = aiModelSettingsDao.saveAndFlush(settings.getTenantId(), settings); - } catch (Exception e) { - checkConstraintViolation(e, - "ai_model_settings_name_unq_key", "AI model settings with such name already exist!", - "ai_model_settings_external_id_unq_key", "AI model settings with such external ID already exist!"); - throw e; - } - - var cacheKey = AiModelSettingsCacheKey.of(savedSettings.getTenantId(), savedSettings.getId()); - publishEvictEvent(new AiModelSettingsCacheEvictEvent.Saved(cacheKey, savedSettings)); - - return savedSettings; - } - - @Override - public Optional findAiModelSettingsById(TenantId tenantId, AiModelSettingsId settingsId) { - return Optional.ofNullable(aiModelSettingsDao.findById(tenantId, settingsId.getId())); - } - - @Override - public PageData findAiModelSettingsByTenantId(TenantId tenantId, PageLink pageLink) { - validatePageLink(pageLink, AiModelSettingsEntity.ALLOWED_SORT_PROPERTIES); - return aiModelSettingsDao.findAllByTenantId(tenantId, pageLink); - } - - @Override - public Optional findAiModelSettingsByTenantIdAndId(TenantId tenantId, AiModelSettingsId settingsId) { - var cacheKey = AiModelSettingsCacheKey.of(tenantId, settingsId); - return Optional.ofNullable(cache.get(cacheKey, () -> aiModelSettingsDao.findByTenantIdAndId(tenantId, settingsId).orElse(null))); - } - - @Override - public FluentFuture> findAiModelSettingsByTenantIdAndIdAsync(TenantId tenantId, AiModelSettingsId settingsId) { - return FluentFuture.from(jpaExecutor.submit(() -> findAiModelSettingsByTenantIdAndId(tenantId, settingsId))); - } - - @Override - @Transactional - public boolean deleteByTenantIdAndId(TenantId tenantId, AiModelSettingsId settingsId) { - return deleteByTenantIdAndIdInternal(tenantId, settingsId); - } - - @Override - public Optional> findEntity(TenantId tenantId, EntityId entityId) { - return findAiModelSettingsByTenantIdAndId(tenantId, (AiModelSettingsId) entityId) - .map(settings -> settings); // necessary to cast to HasId - } - - @Override - public long countByTenantId(TenantId tenantId) { - return aiModelSettingsDao.countByTenantId(tenantId); - } - - @Override - @Transactional - public void deleteEntity(TenantId tenantId, EntityId id, boolean force) { - deleteByTenantIdAndIdInternal(tenantId, new AiModelSettingsId(id.getId())); - } - - private boolean deleteByTenantIdAndIdInternal(TenantId tenantId, AiModelSettingsId settingsId) { - boolean deleted = aiModelSettingsDao.deleteByTenantIdAndId(tenantId, settingsId); - if (deleted) { - publishEvictEvent(new AiModelSettingsCacheEvictEvent.Deleted(AiModelSettingsCacheKey.of(tenantId, settingsId))); - } - return deleted; - } - - @Override - @Transactional - public void deleteByTenantId(TenantId tenantId) { - Set deleted = aiModelSettingsDao.deleteByTenantId(tenantId); - deleted.forEach(id -> publishEvictEvent(new AiModelSettingsCacheEvictEvent.Deleted(AiModelSettingsCacheKey.of(tenantId, id)))); - } - - @Override - public EntityType getEntityType() { - return EntityType.AI_MODEL_SETTINGS; - } - -} diff --git a/dao/src/main/java/org/thingsboard/server/dao/housekeeper/CleanUpService.java b/dao/src/main/java/org/thingsboard/server/dao/housekeeper/CleanUpService.java index 28c96c4675..2f23d20f00 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/housekeeper/CleanUpService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/housekeeper/CleanUpService.java @@ -47,7 +47,7 @@ public class CleanUpService { private final Set skippedEntities = EnumSet.of( EntityType.ALARM, EntityType.QUEUE, EntityType.TB_RESOURCE, EntityType.OTA_PACKAGE, EntityType.NOTIFICATION_REQUEST, EntityType.NOTIFICATION_TEMPLATE, - EntityType.NOTIFICATION_TARGET, EntityType.NOTIFICATION_RULE, EntityType.AI_MODEL_SETTINGS + EntityType.NOTIFICATION_TARGET, EntityType.NOTIFICATION_RULE, EntityType.AI_MODEL ); @TransactionalEventListener(fallbackExecution = true) // after transaction commit diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java index 6245d75214..ca59d3bce0 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java @@ -752,12 +752,12 @@ public class ModelConstants { public static final String JOB_RESULT_PROPERTY = "result"; /** - * AI model settings constants. + * AI model constants. */ - public static final String AI_MODEL_SETTINGS_TABLE_NAME = "ai_model_settings"; - public static final String AI_MODEL_SETTINGS_TENANT_ID_COLUMN_NAME = TENANT_ID_COLUMN; - public static final String AI_MODEL_SETTINGS_NAME_COLUMN_NAME = NAME_PROPERTY; - public static final String AI_MODEL_SETTINGS_CONFIGURATION_COLUMN_NAME = "configuration"; + public static final String AI_MODEL_TABLE_NAME = "ai_model"; + public static final String AI_MODEL_TENANT_ID_COLUMN_NAME = TENANT_ID_COLUMN; + public static final String AI_MODEL_NAME_COLUMN_NAME = NAME_PROPERTY; + public static final String AI_MODEL_CONFIGURATION_COLUMN_NAME = "configuration"; protected static final String[] NONE_AGGREGATION_COLUMNS = new String[]{LONG_VALUE_COLUMN, DOUBLE_VALUE_COLUMN, BOOLEAN_VALUE_COLUMN, STRING_VALUE_COLUMN, JSON_VALUE_COLUMN, KEY_COLUMN, TS_COLUMN}; diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/AiModelSettingsEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AiModelEntity.java similarity index 62% rename from dao/src/main/java/org/thingsboard/server/dao/model/sql/AiModelSettingsEntity.java rename to dao/src/main/java/org/thingsboard/server/dao/model/sql/AiModelEntity.java index 25aa7a0018..d4f3d36db6 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/AiModelSettingsEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AiModelEntity.java @@ -24,9 +24,9 @@ import lombok.Setter; import lombok.ToString; import org.hibernate.annotations.Type; import org.hibernate.proxy.HibernateProxy; -import org.thingsboard.server.common.data.ai.AiModelSettings; -import org.thingsboard.server.common.data.ai.model.AiModel; -import org.thingsboard.server.common.data.id.AiModelSettingsId; +import org.thingsboard.server.common.data.ai.AiModel; +import org.thingsboard.server.common.data.ai.model.AiModelConfig; +import org.thingsboard.server.common.data.id.AiModelId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.dao.model.BaseVersionedEntity; import org.thingsboard.server.dao.model.ModelConstants; @@ -43,52 +43,52 @@ import java.util.UUID; @Setter @ToString @Entity -@Table(name = ModelConstants.AI_MODEL_SETTINGS_TABLE_NAME) -public class AiModelSettingsEntity extends BaseVersionedEntity { +@Table(name = ModelConstants.AI_MODEL_TABLE_NAME) +public class AiModelEntity extends BaseVersionedEntity { public static final Map COLUMN_MAP = Map.of( "createdTime", "created_time", "provider", "(configuration ->> 'provider')", - "modelId", "(configuration -> 'modelConfig' ->> 'modelId')" + "modelId", "(configuration ->> 'modelId')" ); public static final Set ALLOWED_SORT_PROPERTIES = Collections.unmodifiableSet( new LinkedHashSet<>(List.of("createdTime", "name", "provider", "modelId")) ); - @Column(name = ModelConstants.AI_MODEL_SETTINGS_TENANT_ID_COLUMN_NAME, nullable = false, columnDefinition = "UUID") + @Column(name = ModelConstants.AI_MODEL_TENANT_ID_COLUMN_NAME, nullable = false, columnDefinition = "UUID") private UUID tenantId; - @Column(name = ModelConstants.AI_MODEL_SETTINGS_NAME_COLUMN_NAME, nullable = false) + @Column(name = ModelConstants.AI_MODEL_NAME_COLUMN_NAME, nullable = false) private String name; @Type(JsonBinaryType.class) - @Column(name = ModelConstants.AI_MODEL_SETTINGS_CONFIGURATION_COLUMN_NAME, nullable = false, columnDefinition = "JSONB") - private AiModel configuration; + @Column(name = ModelConstants.AI_MODEL_CONFIGURATION_COLUMN_NAME, nullable = false, columnDefinition = "JSONB") + private AiModelConfig configuration; @Column(name = ModelConstants.EXTERNAL_ID_PROPERTY, columnDefinition = "UUID") private UUID externalId; - public AiModelSettingsEntity() {} + public AiModelEntity() {} - public AiModelSettingsEntity(AiModelSettings aiModelSettings) { - super(aiModelSettings); - tenantId = getTenantUuid(aiModelSettings.getTenantId()); - name = aiModelSettings.getName(); - configuration = aiModelSettings.getConfiguration(); - externalId = getUuid(aiModelSettings.getExternalId()); + public AiModelEntity(AiModel aiModel) { + super(aiModel); + tenantId = getTenantUuid(aiModel.getTenantId()); + name = aiModel.getName(); + configuration = aiModel.getConfiguration(); + externalId = getUuid(aiModel.getExternalId()); } @Override - public AiModelSettings toData() { - var settings = new AiModelSettings(new AiModelSettingsId(id)); - settings.setCreatedTime(createdTime); - settings.setVersion(version); - settings.setTenantId(TenantId.fromUUID(tenantId)); - settings.setName(name); - settings.setConfiguration(configuration); - settings.setExternalId(getEntityId(externalId, AiModelSettingsId::new)); - return settings; + public AiModel toData() { + var model = new AiModel(new AiModelId(id)); + model.setCreatedTime(createdTime); + model.setVersion(version); + model.setTenantId(TenantId.fromUUID(tenantId)); + model.setName(name); + model.setConfiguration(configuration); + model.setExternalId(getEntityId(externalId, AiModelId::new)); + return model; } @Override @@ -98,7 +98,7 @@ public class AiModelSettingsEntity extends BaseVersionedEntity Class oEffectiveClass = o instanceof HibernateProxy ? ((HibernateProxy) o).getHibernateLazyInitializer().getPersistentClass() : o.getClass(); Class thisEffectiveClass = this instanceof HibernateProxy ? ((HibernateProxy) this).getHibernateLazyInitializer().getPersistentClass() : this.getClass(); if (thisEffectiveClass != oEffectiveClass) return false; - AiModelSettingsEntity that = (AiModelSettingsEntity) o; + AiModelEntity that = (AiModelEntity) o; return getId() != null && Objects.equals(getId(), that.getId()); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/AiModelSettingsDataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/AiModelDataValidator.java similarity index 51% rename from dao/src/main/java/org/thingsboard/server/dao/service/validator/AiModelSettingsDataValidator.java rename to dao/src/main/java/org/thingsboard/server/dao/service/validator/AiModelDataValidator.java index fdba1f4e0c..fdccf2955f 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/validator/AiModelSettingsDataValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/validator/AiModelDataValidator.java @@ -17,9 +17,9 @@ package org.thingsboard.server.dao.service.validator; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; -import org.thingsboard.server.common.data.ai.AiModelSettings; +import org.thingsboard.server.common.data.ai.AiModel; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.dao.ai.AiModelSettingsDao; +import org.thingsboard.server.dao.ai.AiModelDao; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.dao.tenant.TenantService; @@ -28,41 +28,41 @@ import java.util.Optional; @Component @RequiredArgsConstructor -class AiModelSettingsDataValidator extends DataValidator { +class AiModelDataValidator extends DataValidator { private final TenantService tenantService; - private final AiModelSettingsDao aiModelSettingsDao; + private final AiModelDao aiModelDao; @Override - protected AiModelSettings validateUpdate(TenantId tenantId, AiModelSettings settings) { - Optional existing = aiModelSettingsDao.findByTenantIdAndId(tenantId, settings.getId()); + protected AiModel validateUpdate(TenantId tenantId, AiModel model) { + Optional existing = aiModelDao.findByTenantIdAndId(tenantId, model.getId()); if (existing.isEmpty()) { - throw new DataValidationException("Cannot update non-existent AI model settings!"); + throw new DataValidationException("Cannot update non-existent AI model!"); } return existing.get(); } @Override - protected void validateDataImpl(TenantId tenantId, AiModelSettings settings) { + protected void validateDataImpl(TenantId tenantId, AiModel model) { // ID validation - if (settings.getId() != null) { - if (settings.getUuidId() == null) { - throw new DataValidationException("AI model settings UUID should be specified!"); + if (model.getId() != null) { + if (model.getUuidId() == null) { + throw new DataValidationException("AI model UUID should be specified!"); } - if (settings.getId().isNullUid()) { - throw new DataValidationException("AI model settings UUID must not be the reserved null value!"); + if (model.getId().isNullUid()) { + throw new DataValidationException("AI model UUID must not be the reserved null value!"); } } // tenant ID validation - if (settings.getTenantId() == null || settings.getTenantId().getId() == null) { - throw new DataValidationException("AI model settings should be assigned to tenant!"); + if (model.getTenantId() == null || model.getTenantId().getId() == null) { + throw new DataValidationException("AI model should be assigned to tenant!"); } - if (settings.getTenantId().isSysTenantId()) { - throw new DataValidationException("AI model settings cannot be assigned to the system tenant!"); + if (model.getTenantId().isSysTenantId()) { + throw new DataValidationException("AI model cannot be assigned to the system tenant!"); } if (!tenantService.tenantExists(tenantId)) { - throw new DataValidationException("AI model settings reference a non-existent tenant!"); + throw new DataValidationException("AI model reference a non-existent tenant!"); } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/ai/AiModelSettingsRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/ai/AiModelRepository.java similarity index 55% rename from dao/src/main/java/org/thingsboard/server/dao/sql/ai/AiModelSettingsRepository.java rename to dao/src/main/java/org/thingsboard/server/dao/sql/ai/AiModelRepository.java index 6be7d97f33..0a746f15ab 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/ai/AiModelSettingsRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/ai/AiModelRepository.java @@ -23,58 +23,58 @@ import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; import org.springframework.transaction.annotation.Transactional; import org.thingsboard.server.dao.ExportableEntityRepository; -import org.thingsboard.server.dao.model.sql.AiModelSettingsEntity; +import org.thingsboard.server.dao.model.sql.AiModelEntity; import java.util.Optional; import java.util.Set; import java.util.UUID; -interface AiModelSettingsRepository extends JpaRepository, ExportableEntityRepository { +interface AiModelRepository extends JpaRepository, ExportableEntityRepository { - Optional findByTenantIdAndId(UUID tenantId, UUID id); + Optional findByTenantIdAndId(UUID tenantId, UUID id); - Optional findByTenantIdAndName(UUID tenantId, String name); + Optional findByTenantIdAndName(UUID tenantId, String name); @Query( value = """ SELECT * - FROM ai_model_settings ai_model - WHERE ai_model.tenant_id = :tenantId + FROM ai_model model + WHERE model.tenant_id = :tenantId AND (:textSearch IS NULL - OR ai_model.name ILIKE '%' || :textSearch || '%' - OR (ai_model.configuration ->> 'provider') ILIKE '%' || :textSearch || '%' - OR (ai_model.configuration -> 'modelConfig' ->> 'modelId') ILIKE '%' || :textSearch || '%') + OR model.name ILIKE '%' || :textSearch || '%' + OR (model.configuration ->> 'provider') ILIKE '%' || :textSearch || '%' + OR (model.configuration ->> 'modelId') ILIKE '%' || :textSearch || '%') """, countQuery = """ SELECT COUNT(*) - FROM ai_model_settings ai_model - WHERE ai_model.tenant_id = :tenantId + FROM ai_model model + WHERE model.tenant_id = :tenantId AND (:textSearch IS NULL - OR ai_model.name ILIKE '%' || :textSearch || '%' - OR (ai_model.configuration ->> 'provider') ILIKE '%' || :textSearch || '%' - OR (ai_model.configuration -> 'modelConfig' ->> 'modelId') ILIKE '%' || :textSearch || '%') + OR model.name ILIKE '%' || :textSearch || '%' + OR (model.configuration ->> 'provider') ILIKE '%' || :textSearch || '%' + OR (model.configuration ->> 'modelId') ILIKE '%' || :textSearch || '%') """, nativeQuery = true ) - Page findByTenantId(@Param("tenantId") UUID tenantId, @Param("textSearch") String textSearch, Pageable pageable); + Page findByTenantId(@Param("tenantId") UUID tenantId, @Param("textSearch") String textSearch, Pageable pageable); - @Query("SELECT ai_model.id FROM AiModelSettingsEntity ai_model WHERE ai_model.tenantId = :tenantId") + @Query("SELECT ai_model.id FROM AiModelEntity ai_model WHERE ai_model.tenantId = :tenantId") Page findIdsByTenantId(@Param("tenantId") UUID tenantId, Pageable pageable); - @Query("SELECT externalId FROM AiModelSettingsEntity WHERE id = :id") + @Query("SELECT externalId FROM AiModelEntity WHERE id = :id") Optional getExternalIdById(@Param("id") UUID id); long countByTenantId(UUID tenantId); @Transactional @Modifying - @Query("DELETE FROM AiModelSettingsEntity ai_model WHERE ai_model.id IN (:ids)") + @Query("DELETE FROM AiModelEntity ai_model WHERE ai_model.id IN (:ids)") int deleteByIdIn(@Param("ids") Set ids); @Transactional @Modifying @Query(value = """ - DELETE FROM ai_model_settings + DELETE FROM ai_model WHERE tenant_id = :tenantId RETURNING id """, nativeQuery = true @@ -83,7 +83,7 @@ interface AiModelSettingsRepository extends JpaRepository ids); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/ai/JpaAiModelSettingsDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/ai/JpaAiModelDao.java similarity index 50% rename from dao/src/main/java/org/thingsboard/server/dao/sql/ai/JpaAiModelSettingsDao.java rename to dao/src/main/java/org/thingsboard/server/dao/sql/ai/JpaAiModelDao.java index 38e7280313..e0c145fe03 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/ai/JpaAiModelSettingsDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/ai/JpaAiModelDao.java @@ -23,15 +23,15 @@ import org.springframework.data.jpa.domain.JpaSort; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.EntityType; -import org.thingsboard.server.common.data.ai.AiModelSettings; -import org.thingsboard.server.common.data.id.AiModelSettingsId; +import org.thingsboard.server.common.data.ai.AiModel; +import org.thingsboard.server.common.data.id.AiModelId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.page.SortOrder; import org.thingsboard.server.dao.DaoUtil; -import org.thingsboard.server.dao.ai.AiModelSettingsDao; -import org.thingsboard.server.dao.model.sql.AiModelSettingsEntity; +import org.thingsboard.server.dao.ai.AiModelDao; +import org.thingsboard.server.dao.model.sql.AiModelEntity; import org.thingsboard.server.dao.sql.JpaAbstractDao; import org.thingsboard.server.dao.util.SqlDao; @@ -44,40 +44,40 @@ import static java.util.stream.Collectors.toSet; @SqlDao @Component @RequiredArgsConstructor -class JpaAiModelSettingsDao extends JpaAbstractDao implements AiModelSettingsDao { +class JpaAiModelDao extends JpaAbstractDao implements AiModelDao { - private final AiModelSettingsRepository aiModelSettingsRepository; + private final AiModelRepository aiModelRepository; @Override - public Optional findByTenantIdAndId(TenantId tenantId, AiModelSettingsId settingsId) { - return aiModelSettingsRepository.findByTenantIdAndId(tenantId.getId(), settingsId.getId()).map(DaoUtil::getData); + public Optional findByTenantIdAndId(TenantId tenantId, AiModelId modelId) { + return aiModelRepository.findByTenantIdAndId(tenantId.getId(), modelId.getId()).map(DaoUtil::getData); } @Override - public AiModelSettings findByTenantIdAndName(UUID tenantId, String name) { - return DaoUtil.getData(aiModelSettingsRepository.findByTenantIdAndName(tenantId, name)); + public AiModel findByTenantIdAndName(UUID tenantId, String name) { + return DaoUtil.getData(aiModelRepository.findByTenantIdAndName(tenantId, name)); } @Override - public AiModelSettings findByTenantIdAndExternalId(UUID tenantId, UUID externalId) { - return DaoUtil.getData(aiModelSettingsRepository.findByTenantIdAndExternalId(tenantId, externalId)); + public AiModel findByTenantIdAndExternalId(UUID tenantId, UUID externalId) { + return DaoUtil.getData(aiModelRepository.findByTenantIdAndExternalId(tenantId, externalId)); } @Override - public PageData findAllByTenantId(TenantId tenantId, PageLink pageLink) { + public PageData findAllByTenantId(TenantId tenantId, PageLink pageLink) { return findByTenantId(tenantId.getId(), pageLink); } @Override - public PageData findByTenantId(UUID tenantId, PageLink pageLink) { - return DaoUtil.toPageData(aiModelSettingsRepository.findByTenantId( + public PageData findByTenantId(UUID tenantId, PageLink pageLink) { + return DaoUtil.toPageData(aiModelRepository.findByTenantId( tenantId, StringUtils.defaultIfEmpty(pageLink.getTextSearch(), null), toPageRequest(pageLink)) ); } @Override - public PageData findIdsByTenantId(UUID tenantId, PageLink pageLink) { - return DaoUtil.pageToPageData(aiModelSettingsRepository.findIdsByTenantId(tenantId, toPageRequest(pageLink)).map(AiModelSettingsId::new)); + public PageData findIdsByTenantId(UUID tenantId, PageLink pageLink) { + return DaoUtil.pageToPageData(aiModelRepository.findIdsByTenantId(tenantId, toPageRequest(pageLink)).map(AiModelId::new)); } private static PageRequest toPageRequest(PageLink pageLink) { @@ -88,52 +88,52 @@ class JpaAiModelSettingsDao extends JpaAbstractDao 0; + public boolean deleteById(TenantId tenantId, AiModelId modelId) { + return aiModelRepository.deleteByIdIn(Set.of(modelId.getId())) > 0; } @Override - public Set deleteByTenantId(TenantId tenantId) { - return aiModelSettingsRepository.deleteByTenantId(tenantId.getId()).stream() - .map(AiModelSettingsId::new) + public Set deleteByTenantId(TenantId tenantId) { + return aiModelRepository.deleteByTenantId(tenantId.getId()).stream() + .map(AiModelId::new) .collect(toSet()); } @Override - public boolean deleteByTenantIdAndId(TenantId tenantId, AiModelSettingsId settingsId) { - return aiModelSettingsRepository.deleteByTenantIdAndIdIn(tenantId.getId(), Set.of(settingsId.getId())) > 0; + public boolean deleteByTenantIdAndId(TenantId tenantId, AiModelId modelId) { + return aiModelRepository.deleteByTenantIdAndIdIn(tenantId.getId(), Set.of(modelId.getId())) > 0; } @Override public EntityType getEntityType() { - return EntityType.AI_MODEL_SETTINGS; + return EntityType.AI_MODEL; } @Override - protected Class getEntityClass() { - return AiModelSettingsEntity.class; + protected Class getEntityClass() { + return AiModelEntity.class; } @Override - protected JpaRepository getRepository() { - return aiModelSettingsRepository; + protected JpaRepository getRepository() { + return aiModelRepository; } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java index 9b37e9c2dd..0eb4a60e6d 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java @@ -183,7 +183,7 @@ public class TenantServiceImpl extends AbstractCachedEntityService> FluentFuture sendChatRequestAsync(AiChatModel chatModel, ChatRequest chatRequest); + > FluentFuture sendChatRequestAsync(AiChatModelConfig chatModelConfig, ChatRequest chatRequest); } diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java index 1eeb644bba..d2687a1b10 100644 --- a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java +++ b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java @@ -44,7 +44,7 @@ import org.thingsboard.server.common.data.rule.RuleNodeState; import org.thingsboard.server.common.data.script.ScriptLanguage; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; -import org.thingsboard.server.dao.ai.AiModelSettingsService; +import org.thingsboard.server.dao.ai.AiModelService; import org.thingsboard.server.dao.alarm.AlarmCommentService; import org.thingsboard.server.dao.asset.AssetProfileService; import org.thingsboard.server.dao.asset.AssetService; @@ -423,9 +423,9 @@ public interface TbContext { AuditLogService getAuditLogService(); - RuleEngineAiModelService getAiModelService(); + RuleEngineAiChatModelService getAiChatModelService(); - AiModelSettingsService getAiModelSettingsService(); + AiModelService getAiModelService(); // Configuration parameters for the MQTT client that is used in the MQTT node and Azure IoT hub node diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNode.java index 62d92ef2ce..93c2cafaae 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNode.java @@ -33,11 +33,10 @@ import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.rule.engine.api.util.TbNodeUtils; import org.thingsboard.rule.engine.external.TbAbstractExternalNode; -import org.thingsboard.server.common.data.ai.AiModelSettings; +import org.thingsboard.server.common.data.ai.AiModel; import org.thingsboard.server.common.data.ai.model.AiModelType; -import org.thingsboard.server.common.data.ai.model.chat.AiChatModel; import org.thingsboard.server.common.data.ai.model.chat.AiChatModelConfig; -import org.thingsboard.server.common.data.id.AiModelSettingsId; +import org.thingsboard.server.common.data.id.AiModelId; import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.data.rule.RuleChainType; import org.thingsboard.server.common.msg.TbMsg; @@ -65,7 +64,7 @@ public final class TbAiNode extends TbAbstractExternalNode implements TbNode { private String userPrompt; private ResponseFormat responseFormat; private int timeoutSeconds; - private AiModelSettingsId modelSettingsId; + private AiModelId modelId; @Override public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException { @@ -79,7 +78,7 @@ public final class TbAiNode extends TbAbstractExternalNode implements TbNode { throw new TbNodeException(e, true); } - // LC4j AnthropicChatModel rejects requests with non-null ResponseFormat even if ResponseFormatType is TEXT + // LangChain4j AnthropicChatModel rejects requests with non-null ResponseFormat even if ResponseFormatType is TEXT if (config.getResponseFormat().type() == TbResponseFormat.TbResponseFormatType.JSON) { responseFormat = config.getResponseFormat().toLangChainResponseFormat(); } @@ -87,15 +86,15 @@ public final class TbAiNode extends TbAbstractExternalNode implements TbNode { systemPrompt = config.getSystemPrompt(); userPrompt = config.getUserPrompt(); timeoutSeconds = config.getTimeoutSeconds(); - modelSettingsId = config.getAiModelSettingsId(); + modelId = config.getAiModelId(); - Optional modelSettings = ctx.getAiModelSettingsService().findAiModelSettingsByTenantIdAndId(ctx.getTenantId(), modelSettingsId); - if (modelSettings.isEmpty()) { - throw new TbNodeException("[" + ctx.getTenantId() + "] AI model settings with ID: [" + modelSettingsId + "] were not found", true); + Optional model = ctx.getAiModelService().findAiModelByTenantIdAndId(ctx.getTenantId(), modelId); + if (model.isEmpty()) { + throw new TbNodeException("[" + ctx.getTenantId() + "] AI model with ID: [" + modelId + "] was not found", true); } - AiModelType modelType = modelSettings.get().getConfiguration().modelType(); + AiModelType modelType = model.get().getConfiguration().modelType(); if (modelType != AiModelType.CHAT) { - throw new TbNodeException("[" + ctx.getTenantId() + "] AI model settings with ID: [" + modelSettingsId + "] must be of type CHAT, but was " + modelType, true); + throw new TbNodeException("[" + ctx.getTenantId() + "] AI model with ID: [" + modelId + "] must be of type CHAT, but was " + modelType, true); } } @@ -134,24 +133,24 @@ public final class TbAiNode extends TbAbstractExternalNode implements TbNode { } private > FluentFuture sendChatRequestAsync(TbContext ctx, ChatRequest chatRequest) { - return ctx.getAiModelSettingsService().findAiModelSettingsByTenantIdAndIdAsync(ctx.getTenantId(), modelSettingsId).transformAsync(settingsOpt -> { - if (settingsOpt.isEmpty()) { - throw new NoSuchElementException("[" + ctx.getTenantId() + "] AI model settings with ID: [" + modelSettingsId + "] were not found"); + return ctx.getAiModelService().findAiModelByTenantIdAndIdAsync(ctx.getTenantId(), modelId).transformAsync(modelOpt -> { + if (modelOpt.isEmpty()) { + throw new NoSuchElementException("[" + ctx.getTenantId() + "] AI model with ID: [" + modelId + "] was not found"); } - AiModelSettings settings = settingsOpt.get(); - AiModelType modelType = settings.getConfiguration().modelType(); + AiModel model = modelOpt.get(); + AiModelType modelType = model.getConfiguration().modelType(); if (modelType != AiModelType.CHAT) { - throw new IllegalStateException("[" + ctx.getTenantId() + "] AI model settings with ID: [" + modelSettingsId + "] must be of type CHAT, but was " + modelType); + throw new IllegalStateException("[" + ctx.getTenantId() + "] AI model with ID: [" + modelId + "] must be of type CHAT, but was " + modelType); } @SuppressWarnings("unchecked") - AiChatModel chatModel = (AiChatModel) settingsOpt.get().getConfiguration(); + AiChatModelConfig chatModelConfig = (AiChatModelConfig) model.getConfiguration(); - chatModel = chatModel.withModelConfig(chatModel.modelConfig() + chatModelConfig = chatModelConfig .withTimeoutSeconds(timeoutSeconds) - .withMaxRetries(0)); // disable retries to respect timeout set in rule node config + .withMaxRetries(0); // disable retries to respect timeout set in rule node config - return ctx.getAiModelService().sendChatRequestAsync(chatModel, chatRequest); + return ctx.getAiChatModelService().sendChatRequestAsync(chatModelConfig, chatRequest); }, ctx.getDbCallbackExecutor()); } @@ -174,7 +173,7 @@ public final class TbAiNode extends TbAbstractExternalNode implements TbNode { systemPrompt = null; userPrompt = null; responseFormat = null; - modelSettingsId = null; + modelId = null; } } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNodeConfiguration.java index ebfcf943f7..eba3d5fef9 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNodeConfiguration.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNodeConfiguration.java @@ -23,7 +23,7 @@ import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Pattern; import lombok.Data; import org.thingsboard.rule.engine.api.NodeConfiguration; -import org.thingsboard.server.common.data.id.AiModelSettingsId; +import org.thingsboard.server.common.data.id.AiModelId; import org.thingsboard.server.common.data.validation.Length; import static org.thingsboard.rule.engine.ai.TbResponseFormat.TbJsonResponseFormat; @@ -32,7 +32,7 @@ import static org.thingsboard.rule.engine.ai.TbResponseFormat.TbJsonResponseForm public class TbAiNodeConfiguration implements NodeConfiguration { @NotNull - private AiModelSettingsId aiModelSettingsId; + private AiModelId aiModelId; @Pattern(regexp = ".*\\S.*", message = "must not be blank") @Length(min = 1, max = 10000) diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/TenantIdLoader.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/TenantIdLoader.java index 130332ad64..a8ae187c84 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/TenantIdLoader.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/TenantIdLoader.java @@ -19,7 +19,7 @@ import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.HasTenantId; import org.thingsboard.server.common.data.cf.CalculatedFieldLink; -import org.thingsboard.server.common.data.id.AiModelSettingsId; +import org.thingsboard.server.common.data.id.AiModelId; import org.thingsboard.server.common.data.id.AlarmId; import org.thingsboard.server.common.data.id.ApiUsageStateId; import org.thingsboard.server.common.data.id.AssetId; @@ -180,8 +180,8 @@ public class TenantIdLoader { case JOB: tenantEntity = ctx.getJobService().findJobById(ctxTenantId, new JobId(id)); break; - case AI_MODEL_SETTINGS: - tenantEntity = ctx.getAiModelSettingsService().findAiModelSettingsById(ctxTenantId, new AiModelSettingsId(id)).orElse(null); + case AI_MODEL: + tenantEntity = ctx.getAiModelService().findAiModelById(ctxTenantId, new AiModelId(id)).orElse(null); break; default: throw new RuntimeException("Unexpected entity type: " + entityId.getEntityType()); diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/util/TenantIdLoaderTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/util/TenantIdLoaderTest.java index 3713dbea3e..2d7b8a0d30 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/util/TenantIdLoaderTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/util/TenantIdLoaderTest.java @@ -40,7 +40,7 @@ import org.thingsboard.server.common.data.OtaPackage; import org.thingsboard.server.common.data.TbResource; import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.User; -import org.thingsboard.server.common.data.ai.AiModelSettings; +import org.thingsboard.server.common.data.ai.AiModel; import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.asset.AssetProfile; @@ -70,7 +70,7 @@ import org.thingsboard.server.common.data.rule.RuleChain; import org.thingsboard.server.common.data.rule.RuleNode; import org.thingsboard.server.common.data.widget.WidgetType; import org.thingsboard.server.common.data.widget.WidgetsBundle; -import org.thingsboard.server.dao.ai.AiModelSettingsService; +import org.thingsboard.server.dao.ai.AiModelService; import org.thingsboard.server.dao.asset.AssetService; import org.thingsboard.server.dao.cf.CalculatedFieldService; import org.thingsboard.server.dao.customer.CustomerService; @@ -168,7 +168,7 @@ public class TenantIdLoaderTest { @Mock private JobService jobService; @Mock - private AiModelSettingsService aiModelSettingsService; + private AiModelService aiModelService; private TenantId tenantId; private TenantProfileId tenantProfileId; @@ -434,11 +434,11 @@ public class TenantIdLoaderTest { when(ctx.getJobService()).thenReturn(jobService); doReturn(job).when(jobService).findJobById(eq(tenantId), any()); break; - case AI_MODEL_SETTINGS: - AiModelSettings aiModelSettings = new AiModelSettings(); - aiModelSettings.setTenantId(tenantId); - when(ctx.getAiModelSettingsService()).thenReturn(aiModelSettingsService); - doReturn(Optional.of(aiModelSettings)).when(aiModelSettingsService).findAiModelSettingsById(eq(tenantId), any()); + case AI_MODEL: + AiModel aiModel = new AiModel(); + aiModel.setTenantId(tenantId); + when(ctx.getAiModelService()).thenReturn(aiModelService); + doReturn(Optional.of(aiModel)).when(aiModelService).findAiModelById(eq(tenantId), any()); break; default: throw new RuntimeException("Unexpected originator EntityType " + entityType);