diff --git a/application/pom.xml b/application/pom.xml
index dba7d27c1b..4cbd9c3b64 100644
--- a/application/pom.xml
+++ b/application/pom.xml
@@ -419,6 +419,10 @@
+
+ dev.langchain4j
+ langchain4j-ollama
+
diff --git a/application/src/main/data/json/system/widget_types/alarms_table.json b/application/src/main/data/json/system/widget_types/alarms_table.json
index 4f86957f1c..446129972b 100644
--- a/application/src/main/data/json/system/widget_types/alarms_table.json
+++ b/application/src/main/data/json/system/widget_types/alarms_table.json
@@ -12,18 +12,12 @@
"templateHtml": "\n",
"templateCss": "",
"controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.alarmsTableWidget.onDataUpdated();\n}\n\nself.onEditModeChanged = function() {\n self.ctx.$scope.alarmsTableWidget.onEditModeChanged();\n}\n\nself.typeParameters = function() {\n return {\n supportsUnitConversion: true\n };\n};\n\nself.actionSources = function() {\n return {\n 'actionCellButton': {\n name: 'widget-action.action-cell-button',\n multiple: true,\n hasShowCondition: true\n },\n 'rowClick': {\n name: 'widget-action.row-click',\n multiple: false\n },\n 'cellClick': {\n name: 'widget-action.cell-click',\n multiple: true\n }\n };\n}\n\nself.onDestroy = function() {\n}\n",
- "settingsSchema": "",
- "dataKeySettingsSchema": "",
"settingsDirective": "tb-alarms-table-widget-settings",
"dataKeySettingsDirective": "tb-alarms-table-key-settings",
"hasBasicMode": true,
"basicModeDirective": "tb-alarms-table-basic-config",
- "defaultConfig": "{\"timewindow\":{\"realtime\":{\"interval\":1000,\"timewindowMs\":86400000},\"aggregation\":{\"type\":\"NONE\",\"limit\":200}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"4px\",\"settings\":{\"enableSelection\":true,\"enableSearch\":true,\"displayDetails\":true,\"allowAcknowledgment\":true,\"allowClear\":true,\"allowAssign\":true,\"displayActivity\":true,\"displayPagination\":true,\"defaultPageSize\":10,\"defaultSortOrder\":\"-createdTime\",\"enableSelectColumnDisplay\":true,\"enableStickyAction\":false,\"enableFilter\":true,\"entitiesTitle\":null,\"alarmsTitle\":\"Alarms\"},\"title\":\"Alarms table\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400,\"padding\":\"5px 10px 5px 10px\"},\"useDashboardTimewindow\":false,\"showLegend\":false,\"alarmSource\":{\"type\":\"function\",\"dataKeys\":[{\"name\":\"createdTime\",\"type\":\"alarm\",\"label\":\"Created time\",\"color\":\"#2196f3\",\"settings\":{\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.021092237451093787},{\"name\":\"originator\",\"type\":\"alarm\",\"label\":\"Originator\",\"color\":\"#4caf50\",\"settings\":{\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.2780007688856758},{\"name\":\"type\",\"type\":\"alarm\",\"label\":\"Type\",\"color\":\"#f44336\",\"settings\":{\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.7323586880398418},{\"name\":\"severity\",\"type\":\"alarm\",\"label\":\"Severity\",\"color\":\"#ffc107\",\"settings\":{\"useCellStyleFunction\":false,\"useCellContentFunction\":false},\"_hash\":0.09927019860088193},{\"name\":\"status\",\"type\":\"alarm\",\"label\":\"Status\",\"color\":\"#607d8b\",\"settings\":{\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.6588418951443418},{\"name\":\"assignee\",\"type\":\"alarm\",\"label\":\"Assignee\",\"color\":\"#9c27b0\",\"settings\":{},\"_hash\":0.5008441077416634}],\"entityAliasId\":null,\"name\":\"alarms\"},\"alarmSearchStatus\":\"ANY\",\"alarmsPollingInterval\":5,\"showTitleIcon\":false,\"titleIcon\":\"warning\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"displayTimewindow\":true,\"actions\":{},\"alarmStatusList\":[],\"alarmSeverityList\":[],\"alarmTypeList\":[],\"searchPropagatedAlarms\":false,\"configMode\":\"basic\",\"alarmFilterConfig\":null}"
+ "defaultConfig": "{\"timewindow\":{\"realtime\":{\"interval\":1000,\"timewindowMs\":86400000},\"aggregation\":{\"type\":\"NONE\",\"limit\":200}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"4px\",\"settings\":{\"enableSelection\":true,\"enableSearch\":true,\"displayDetails\":true,\"allowAcknowledgment\":true,\"allowClear\":true,\"allowAssign\":true,\"displayActivity\":true,\"displayPagination\":true,\"defaultPageSize\":10,\"defaultSortOrder\":\"-createdTime\",\"enableSelectColumnDisplay\":true,\"enableStickyAction\":false,\"enableFilter\":true,\"entitiesTitle\":null,\"alarmsTitle\":\"Alarms\"},\"title\":\"Alarms table\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400,\"padding\":\"5px 10px 5px 10px\"},\"useDashboardTimewindow\":false,\"showLegend\":false,\"alarmSource\":{\"type\":\"function\",\"dataKeys\":[{\"name\":\"createdTime\",\"type\":\"alarm\",\"label\":\"Created time\",\"color\":\"#2196f3\",\"settings\":{\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.021092237451093787},{\"name\":\"originatorDisplayName\",\"type\":\"alarm\",\"label\":\"Originator\",\"color\":\"#4caf50\",\"settings\":{\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.2780007688856758},{\"name\":\"type\",\"type\":\"alarm\",\"label\":\"Type\",\"color\":\"#f44336\",\"settings\":{\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.7323586880398418},{\"name\":\"severity\",\"type\":\"alarm\",\"label\":\"Severity\",\"color\":\"#ffc107\",\"settings\":{\"useCellStyleFunction\":false,\"useCellContentFunction\":false},\"_hash\":0.09927019860088193},{\"name\":\"status\",\"type\":\"alarm\",\"label\":\"Status\",\"color\":\"#607d8b\",\"settings\":{\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.6588418951443418},{\"name\":\"assignee\",\"type\":\"alarm\",\"label\":\"Assignee\",\"color\":\"#9c27b0\",\"settings\":{},\"_hash\":0.5008441077416634}],\"entityAliasId\":null,\"name\":\"alarms\"},\"alarmSearchStatus\":\"ANY\",\"alarmsPollingInterval\":5,\"showTitleIcon\":false,\"titleIcon\":\"warning\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"displayTimewindow\":true,\"actions\":{},\"alarmStatusList\":[],\"alarmSeverityList\":[],\"alarmTypeList\":[],\"searchPropagatedAlarms\":false,\"configMode\":\"basic\",\"alarmFilterConfig\":null}"
},
- "tags": [
- "alert",
- "alerts"
- ],
"resources": [
{
"link": "/api/images/system/alarms_table_system_widget_image.png",
@@ -36,5 +30,10 @@
"data": "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",
"public": true
}
+ ],
+ "scada": false,
+ "tags": [
+ "alert",
+ "alerts"
]
}
\ No newline at end of file
diff --git a/application/src/main/data/json/system/widget_types/entities_table.json b/application/src/main/data/json/system/widget_types/entities_table.json
index 01034a1d1d..f58bd3f0ce 100644
--- a/application/src/main/data/json/system/widget_types/entities_table.json
+++ b/application/src/main/data/json/system/widget_types/entities_table.json
@@ -11,19 +11,13 @@
"resources": [],
"templateHtml": "\n",
"templateCss": "",
- "controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.entitiesTableWidget.onDataUpdated();\n}\n\nself.onEditModeChanged = function() {\n self.ctx.$scope.entitiesTableWidget.onEditModeChanged();\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n hasDataPageLink: true,\n warnOnPageDataOverflow: false,\n dataKeysOptional: true,\n supportsUnitConversion: true,\n defaultDataKeysFunction: function() {\n return [{ name: 'name', type: 'entityField' }];\n }\n };\n}\n\nself.actionSources = function() {\n return {\n 'actionCellButton': {\n name: 'widget-action.action-cell-button',\n multiple: true,\n hasShowCondition: true\n },\n 'rowClick': {\n name: 'widget-action.row-click',\n multiple: false\n },\n 'rowDoubleClick': {\n name: 'widget-action.row-double-click',\n multiple: false\n },\n 'cellClick': {\n name: 'widget-action.cell-click',\n multiple: true\n }\n };\n}\n\nself.onDestroy = function() {\n}\n",
- "settingsSchema": "",
- "dataKeySettingsSchema": "",
+ "controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.entitiesTableWidget.onDataUpdated();\n}\n\nself.onEditModeChanged = function() {\n self.ctx.$scope.entitiesTableWidget.onEditModeChanged();\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n hasDataPageLink: true,\n warnOnPageDataOverflow: false,\n dataKeysOptional: true,\n supportsUnitConversion: true,\n defaultDataKeysFunction: function() {\n return [{ name: 'displayName', type: 'entityField' }];\n }\n };\n}\n\nself.actionSources = function() {\n return {\n 'actionCellButton': {\n name: 'widget-action.action-cell-button',\n multiple: true,\n hasShowCondition: true\n },\n 'rowClick': {\n name: 'widget-action.row-click',\n multiple: false\n },\n 'rowDoubleClick': {\n name: 'widget-action.row-double-click',\n multiple: false\n },\n 'cellClick': {\n name: 'widget-action.cell-click',\n multiple: true\n }\n };\n}\n\nself.onDestroy = function() {\n}\n",
"settingsDirective": "tb-entities-table-widget-settings",
"dataKeySettingsDirective": "tb-entities-table-key-settings",
"hasBasicMode": true,
"basicModeDirective": "tb-entities-table-basic-config",
"defaultConfig": "{\"timewindow\":{\"realtime\":{\"interval\":1000,\"timewindowMs\":86400000},\"aggregation\":{\"type\":\"NONE\",\"limit\":200}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"4px\",\"settings\":{\"enableSearch\":true,\"enableSelectColumnDisplay\":true,\"enableStickyHeader\":true,\"enableStickyAction\":true,\"reserveSpaceForHiddenAction\":\"true\",\"displayEntityName\":false,\"displayEntityLabel\":false,\"displayEntityType\":false,\"displayPagination\":true,\"defaultPageSize\":10,\"defaultSortOrder\":\"name\",\"useRowStyleFunction\":false,\"entitiesTitle\":\"Entities\"},\"title\":\"Entities table\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400,\"padding\":\"5px 10px 5px 10px\"},\"useDashboardTimewindow\":false,\"showLegend\":false,\"datasources\":[{\"type\":\"function\",\"name\":\"Simulated\",\"entityAliasId\":null,\"filterId\":null,\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Entity name\",\"color\":\"#2196f3\",\"settings\":{\"columnWidth\":\"0px\",\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.472295003170325,\"funcBody\":\"return 'Simulated';\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Entity type\",\"color\":\"#607d8b\",\"settings\":{},\"_hash\":0.782057645776538,\"funcBody\":\"return 'Device';\",\"decimals\":null,\"aggregationType\":null,\"usePostProcessing\":null,\"postFuncBody\":null},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Sin\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.904797781901171,\"funcBody\":\"return Math.round(1000*Math.sin(time/5000));\",\"decimals\":0},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Cos\",\"color\":\"#f44336\",\"settings\":{},\"_hash\":0.1961430898042078,\"funcBody\":\"return Math.round(1000*Math.cos(time/5000));\",\"decimals\":0},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#ffc107\",\"settings\":{},\"_hash\":0.7678057538205878,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\",\"decimals\":2}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"displayTimewindow\":false,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":false,\"titleIcon\":\"list\",\"iconColor\":null}"
},
- "tags": [
- "administration",
- "management"
- ],
"resources": [
{
"link": "/api/images/system/entities_table_system_widget_image.png",
@@ -36,5 +30,10 @@
"data": "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",
"public": true
}
+ ],
+ "scada": false,
+ "tags": [
+ "administration",
+ "management"
]
}
\ No newline at end of file
diff --git a/application/src/main/data/json/system/widget_types/markdown_html_card.json b/application/src/main/data/json/system/widget_types/markdown_html_card.json
index 64a847952a..b66e3ee88e 100644
--- a/application/src/main/data/json/system/widget_types/markdown_html_card.json
+++ b/application/src/main/data/json/system/widget_types/markdown_html_card.json
@@ -11,16 +11,10 @@
"resources": [],
"templateHtml": "\n",
"templateCss": "#container tb-markdown-widget {\n height: 100%;\n display: block;\n}\n\n#container tb-markdown-widget .tb-markdown-view {\n height: 100%;\n overflow: auto;\n}\n",
- "controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.markdownWidget.onDataUpdated();\n}\n\nself.actionSources = function() {\n return {\n 'elementClick': {\n name: 'widget-action.element-click',\n multiple: true\n }\n };\n}\n\nself.typeParameters = function() {\n return {\n dataKeysOptional: true,\n datasourcesOptional: true,\n hasDataPageLink: true\n };\n}\n\nself.onDestroy = function() {\n}\n\n",
- "settingsSchema": "",
- "dataKeySettingsSchema": "",
+ "controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.markdownWidget.onDataUpdated();\n}\n\nself.actionSources = function() {\n return {\n 'elementClick': {\n name: 'widget-action.element-click',\n multiple: true\n }\n };\n}\n\nself.typeParameters = function() {\n return {\n dataKeysOptional: true,\n datasourcesOptional: true,\n hasDataPageLink: true,\n hideDataSettings: true\n };\n}\n\nself.onDestroy = function() {\n}\n\n",
"settingsDirective": "tb-markdown-widget-settings",
- "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"markdownTextPattern\":\"### Markdown/HTML card\\n - **Current entity**: ${entityName}.\\n - **Current value**: ${Random}.\",\"markdownTextFunction\":\"return '# Some title\\\\n - Entity name: ' + data[0]['entityName'];\",\"useMarkdownTextFunction\":false},\"title\":\"Markdown/HTML Card\",\"showTitleIcon\":false,\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"dropShadow\":true,\"enableFullscreen\":true,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"showLegend\":false}"
+ "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temperature\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"const baseTemp = 20;\\nconst dailySwing = 10;\\nconst hourlyVariation = Math.sin((time % 24) * Math.PI / 12) * dailySwing;\\nconst randomness = (Math.random() - 0.5) * 2;\\nconst smoothingFactor = 0.8;\\nreturn (prevValue * smoothingFactor) + ((baseTemp + hourlyVariation + randomness) * (1 - smoothingFactor));\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"useMarkdownTextFunction\":false,\"markdownTextPattern\":\"### Markdown/HTML card\\n - **Current entity**: ${entityName}.\\n - **Current value**: ${Temperature}.\",\"markdownTextFunction\":\"return '# Some title\\\\n - Entity name: ' + data[0]['entityName'];\",\"applyDefaultMarkdownStyle\":true,\"markdownCss\":\"\"},\"title\":\"Markdown/HTML Card\",\"showTitleIcon\":false,\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"dropShadow\":true,\"enableFullscreen\":true,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"showLegend\":false,\"useDashboardTimewindow\":true,\"displayTimewindow\":true}"
},
- "tags": [
- "web",
- "markup"
- ],
"resources": [
{
"link": "/api/images/system/markdown_html_card_system_widget_image.png",
@@ -33,5 +27,10 @@
"data": "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",
"public": true
}
+ ],
+ "scada": false,
+ "tags": [
+ "web",
+ "markup"
]
}
\ No newline at end of file
diff --git a/application/src/main/data/upgrade/basic/schema_update.sql b/application/src/main/data/upgrade/basic/schema_update.sql
index 320d3e5bdd..0add4c0545 100644
--- a/application/src/main/data/upgrade/basic/schema_update.sql
+++ b/application/src/main/data/upgrade/basic/schema_update.sql
@@ -27,7 +27,7 @@ SET profile_data = jsonb_set(
CASE
WHEN (profile_data -> 'configuration') ? 'minAllowedScheduledUpdateIntervalInSecForCF'
THEN NULL
- ELSE to_jsonb(3600)
+ ELSE to_jsonb(60)
END,
'maxRelationLevelPerCfArgument',
CASE
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 ea46ce86eb..b23015a1fe 100644
--- a/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java
+++ b/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java
@@ -97,6 +97,7 @@ import org.thingsboard.server.dao.ota.OtaPackageService;
import org.thingsboard.server.dao.queue.QueueService;
import org.thingsboard.server.dao.queue.QueueStatsService;
import org.thingsboard.server.dao.relation.RelationService;
+import org.thingsboard.server.dao.resource.TbResourceDataCache;
import org.thingsboard.server.dao.resource.ResourceService;
import org.thingsboard.server.dao.rule.RuleChainService;
import org.thingsboard.server.dao.rule.RuleNodeStateService;
@@ -511,6 +512,10 @@ public class ActorSystemContext {
@Getter
private ResourceService resourceService;
+ @Autowired
+ @Getter
+ private TbResourceDataCache resourceDataCache;
+
@Lazy
@Autowired(required = false)
@Getter
diff --git a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityActor.java b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityActor.java
index 2a5f3c3cfd..c57984ef3d 100644
--- a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityActor.java
+++ b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityActor.java
@@ -75,9 +75,6 @@ public class CalculatedFieldEntityActor extends AbstractCalculatedFieldActor {
case CF_LINKED_TELEMETRY_MSG:
processor.process((EntityCalculatedFieldLinkedTelemetryMsg) msg);
break;
- case CF_ENTITY_DYNAMIC_ARGUMENTS_REFRESH_MSG:
- processor.process((EntityCalculatedFieldDynamicArgumentsRefreshMsg) msg);
- break;
default:
return false;
}
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 7513ca41e2..f8b61a082f 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
@@ -49,6 +49,7 @@ import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx;
import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldState;
import org.thingsboard.server.service.cf.ctx.state.SingleValueArgumentEntry;
import org.thingsboard.server.service.cf.ctx.state.geofencing.GeofencingArgumentEntry;
+import org.thingsboard.server.service.cf.ctx.state.geofencing.GeofencingCalculatedFieldState;
import java.util.ArrayList;
import java.util.Collection;
@@ -227,18 +228,6 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM
}
}
- public void process(EntityCalculatedFieldDynamicArgumentsRefreshMsg msg) throws CalculatedFieldException {
- log.debug("[{}][{}] Processing CF dynamic arguments refresh msg.", entityId, msg.getCfId());
- CalculatedFieldState currentState = states.get(msg.getCfId());
- if (currentState == null) {
- log.debug("[{}][{}] Failed to find CF state for entity.", entityId, msg.getCfId());
- } else {
- currentState.setDirty(true);
- log.debug("[{}][{}] CF state marked as dirty.", entityId, msg.getCfId());
- }
- msg.getCallback().onSuccess();
- }
-
private void processTelemetry(CalculatedFieldCtx ctx, CalculatedFieldTelemetryMsgProto proto, List cfIdList, MultipleTbCallback callback) throws CalculatedFieldException {
processArgumentValuesUpdate(ctx, cfIdList, callback, mapToArguments(ctx, proto.getTsDataList()), toTbMsgId(proto), toTbMsgType(proto));
}
@@ -266,12 +255,13 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM
if (state == null) {
state = getOrInitState(ctx);
justRestored = true;
- } else if (state.isDirty()) {
- log.debug("[{}][{}] Going to update dirty CF state.", entityId, ctx.getCfId());
+ } else if (ctx.shouldFetchDynamicArgumentsFromDb(state)) {
+ log.debug("[{}][{}] Going to update dynamic arguments for CF.", entityId, ctx.getCfId());
try {
Map dynamicArgsFromDb = cfService.fetchDynamicArgsFromDb(ctx, entityId);
dynamicArgsFromDb.forEach(newArgValues::putIfAbsent);
- state.setDirty(false);
+ var geofencingState = (GeofencingCalculatedFieldState) state;
+ geofencingState.setLastDynamicArgumentsRefreshTs(System.currentTimeMillis());
} catch (Exception e) {
throw CalculatedFieldException.builder().ctx(ctx).eventEntity(entityId).cause(e).build();
}
@@ -403,7 +393,7 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM
return mapToArguments(entityId, argNames, geofencingArgumentNames, scope, attrDataList);
}
- private Map mapToArguments(EntityId entityId, Map argNames, List geoArgNames, AttributeScopeProto scope, List attrDataList) {
+ private Map mapToArguments(EntityId entityId, Map argNames, List geofencingArgNames, AttributeScopeProto scope, List attrDataList) {
Map arguments = new HashMap<>();
for (AttributeValueProto item : attrDataList) {
ReferencedEntityKey key = new ReferencedEntityKey(item.getKey(), ArgumentType.ATTRIBUTE, AttributeScope.valueOf(scope.name()));
@@ -411,7 +401,7 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM
if (argName == null) {
continue;
}
- if (geoArgNames.contains(argName)) {
+ if (geofencingArgNames.contains(argName)) {
arguments.put(argName, new GeofencingArgumentEntry(entityId, item));
continue;
}
@@ -425,26 +415,32 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM
if (argNames.isEmpty()) {
return Collections.emptyMap();
}
- return mapToArgumentsWithDefaultValue(argNames, ctx.getArguments(), scope, removedAttrKeys);
+ List geofencingArgumentNames = ctx.getLinkedEntityGeofencingArgumentNames();
+ return mapToArgumentsWithDefaultValue(argNames, ctx.getArguments(), geofencingArgumentNames, scope, removedAttrKeys);
}
private Map mapToArgumentsWithDefaultValue(CalculatedFieldCtx ctx, AttributeScopeProto scope, List removedAttrKeys) {
- return mapToArgumentsWithDefaultValue(ctx.getMainEntityArguments(), ctx.getArguments(), scope, removedAttrKeys);
+ return mapToArgumentsWithDefaultValue(ctx.getMainEntityArguments(), ctx.getArguments(), ctx.getMainEntityGeofencingArgumentNames(), scope, removedAttrKeys);
}
- private Map mapToArgumentsWithDefaultValue(Map argNames, Map configArguments, AttributeScopeProto scope, List removedAttrKeys) {
+ private Map mapToArgumentsWithDefaultValue(Map argNames, Map configArguments, List geofencingArgNames, AttributeScopeProto scope, List removedAttrKeys) {
Map arguments = new HashMap<>();
for (String removedKey : removedAttrKeys) {
ReferencedEntityKey key = new ReferencedEntityKey(removedKey, ArgumentType.ATTRIBUTE, AttributeScope.valueOf(scope.name()));
String argName = argNames.get(key);
- if (argName != null) {
- Argument argument = configArguments.get(argName);
- String defaultValue = (argument != null) ? argument.getDefaultValue() : null;
- arguments.put(argName, StringUtils.isNotEmpty(defaultValue)
- ? new SingleValueArgumentEntry(System.currentTimeMillis(), new StringDataEntry(removedKey, defaultValue), null)
- : new SingleValueArgumentEntry());
-
+ if (argName == null) {
+ continue;
}
+ if (geofencingArgNames.contains(argName)) {
+ arguments.put(argName, new GeofencingArgumentEntry());
+ continue;
+ }
+ Argument argument = configArguments.get(argName);
+ String defaultValue = (argument != null) ? argument.getDefaultValue() : null;
+ arguments.put(argName, StringUtils.isNotEmpty(defaultValue)
+ ? new SingleValueArgumentEntry(System.currentTimeMillis(), new StringDataEntry(removedKey, defaultValue), null)
+ : new SingleValueArgumentEntry());
+
}
return arguments;
}
diff --git a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerActor.java b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerActor.java
index ab6cb34176..7d2ae0ff44 100644
--- a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerActor.java
+++ b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerActor.java
@@ -79,9 +79,6 @@ public class CalculatedFieldManagerActor extends AbstractCalculatedFieldActor {
case CF_LINKED_TELEMETRY_MSG:
processor.onLinkedTelemetryMsg((CalculatedFieldLinkedTelemetryMsg) msg);
break;
- case CF_DYNAMIC_ARGUMENTS_REFRESH_MSG:
- processor.onDynamicArgumentsRefreshMsg((CalculatedFieldDynamicArgumentsRefreshMsg) msg);
- break;
default:
return false;
}
diff --git a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java
index 75ca0b4c9b..9b538a4268 100644
--- a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java
+++ b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java
@@ -27,7 +27,6 @@ import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.ProfileEntityIdInfo;
import org.thingsboard.server.common.data.cf.CalculatedField;
import org.thingsboard.server.common.data.cf.CalculatedFieldLink;
-import org.thingsboard.server.common.data.cf.configuration.ScheduledUpdateSupportedCalculatedFieldConfiguration;
import org.thingsboard.server.common.data.id.AssetId;
import org.thingsboard.server.common.data.id.CalculatedFieldId;
import org.thingsboard.server.common.data.id.DeviceId;
@@ -57,10 +56,7 @@ import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
-import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
-import java.util.concurrent.ScheduledFuture;
-import java.util.concurrent.TimeUnit;
import java.util.function.BiConsumer;
import static org.thingsboard.server.utils.CalculatedFieldUtils.fromProto;
@@ -74,7 +70,6 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware
private final Map calculatedFields = new HashMap<>();
private final Map> entityIdCalculatedFields = new HashMap<>();
private final Map> entityIdCalculatedFieldLinks = new HashMap<>();
- private final Map> cfDynamicArgumentsRefreshTasks = new ConcurrentHashMap<>();
private final CalculatedFieldProcessingService cfExecService;
private final CalculatedFieldStateService cfStateService;
@@ -113,8 +108,6 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware
calculatedFields.clear();
entityIdCalculatedFields.clear();
entityIdCalculatedFieldLinks.clear();
- cfDynamicArgumentsRefreshTasks.values().forEach(future -> future.cancel(true));
- cfDynamicArgumentsRefreshTasks.clear();
ctx.stop(ctx.getSelf());
}
@@ -274,7 +267,6 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware
// Alternative approach would be to use any list but avoid modifications to the list (change the complete map value instead)
entityIdCalculatedFields.computeIfAbsent(cf.getEntityId(), id -> new CopyOnWriteArrayList<>()).add(cfCtx);
addLinks(cf);
- scheduleDynamicArgumentsRefreshTaskForCfIfNeeded(cfCtx);
applyToTargetCfEntityActors(cfCtx, callback, (id, cb) -> initCfForEntity(id, cfCtx, false, cb));
}
}
@@ -300,36 +292,29 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware
newCfCtx.init();
} catch (Exception e) {
throw CalculatedFieldException.builder().ctx(newCfCtx).eventEntity(newCfCtx.getEntityId()).cause(e).errorMessage("Failed to initialize CF context").build();
- }
- calculatedFields.put(newCf.getId(), newCfCtx);
- List oldCfList = entityIdCalculatedFields.get(newCf.getEntityId());
-
- boolean hasSchedulingConfigChanges = newCfCtx.hasSchedulingConfigChanges(oldCfCtx);
- if (hasSchedulingConfigChanges) {
- cancelCfDynamicArgumentsRefreshTaskIfExists(cfId, false);
- scheduleDynamicArgumentsRefreshTaskForCfIfNeeded(newCfCtx);
- }
-
- List newCfList = new CopyOnWriteArrayList<>();
- boolean found = false;
- for (CalculatedFieldCtx oldCtx : oldCfList) {
- if (oldCtx.getCfId().equals(newCf.getId())) {
+ } finally {
+ calculatedFields.put(newCf.getId(), newCfCtx);
+ List oldCfList = entityIdCalculatedFields.get(newCf.getEntityId());
+ List newCfList = new CopyOnWriteArrayList<>();
+ boolean found = false;
+ for (CalculatedFieldCtx oldCtx : oldCfList) {
+ if (oldCtx.getCfId().equals(newCf.getId())) {
+ newCfList.add(newCfCtx);
+ found = true;
+ } else {
+ newCfList.add(oldCtx);
+ }
+ }
+ if (!found) {
newCfList.add(newCfCtx);
- found = true;
- } else {
- newCfList.add(oldCtx);
}
+ // We use copy on write lists to safely pass the reference to another actor for the iteration.
+ // Alternative approach would be to use any list but avoid modifications to the list (change the complete map value instead)
+ entityIdCalculatedFields.put(newCf.getEntityId(), newCfList);
+ deleteLinks(oldCfCtx);
+ addLinks(newCf);
}
- if (!found) {
- newCfList.add(newCfCtx);
- }
- entityIdCalculatedFields.put(newCf.getEntityId(), newCfList);
- deleteLinks(oldCfCtx);
- addLinks(newCf);
-
- // We use copy on write lists to safely pass the reference to another actor for the iteration.
- // Alternative approach would be to use any list but avoid modifications to the list (change the complete map value instead)
var stateChanges = newCfCtx.hasStateChanges(oldCfCtx);
if (stateChanges || newCfCtx.hasOtherSignificantChanges(oldCfCtx)) {
applyToTargetCfEntityActors(newCfCtx, callback, (id, cb) -> initCfForEntity(id, newCfCtx, stateChanges, cb));
@@ -350,19 +335,9 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware
}
entityIdCalculatedFields.get(cfCtx.getEntityId()).remove(cfCtx);
deleteLinks(cfCtx);
- cancelCfDynamicArgumentsRefreshTaskIfExists(cfId, true);
applyToTargetCfEntityActors(cfCtx, callback, (id, cb) -> deleteCfForEntity(id, cfId, cb));
}
- private void cancelCfDynamicArgumentsRefreshTaskIfExists(CalculatedFieldId cfId, boolean cfDeleted) {
- var existingTask = cfDynamicArgumentsRefreshTasks.remove(cfId);
- if (existingTask != null) {
- existingTask.cancel(false);
- String reason = cfDeleted ? "deletion" : "update";
- log.debug("[{}][{}] Cancelled dynamic arguments refresh task due to CF {}!", tenantId, cfId, reason);
- }
- }
-
public void onTelemetryMsg(CalculatedFieldTelemetryMsg msg) {
EntityId entityId = msg.getEntityId();
log.debug("Received telemetry msg from entity [{}]", entityId);
@@ -442,43 +417,6 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware
return result;
}
- private void scheduleDynamicArgumentsRefreshTaskForCfIfNeeded(CalculatedFieldCtx cfCtx) {
- CalculatedField cf = cfCtx.getCalculatedField();
- if (!(cf.getConfiguration() instanceof ScheduledUpdateSupportedCalculatedFieldConfiguration scheduledCfConfig)) {
- return;
- }
- if (!scheduledCfConfig.isScheduledUpdateEnabled()) {
- return;
- }
- if (cfDynamicArgumentsRefreshTasks.containsKey(cf.getId())) {
- log.debug("[{}][{}] Dynamic arguments refresh task for CF already exists!", tenantId, cf.getId());
- return;
- }
- long refreshDynamicSourceInterval = TimeUnit.SECONDS.toMillis(scheduledCfConfig.getScheduledUpdateInterval());
- var scheduledMsg = new CalculatedFieldDynamicArgumentsRefreshMsg(tenantId, cfCtx.getCfId());
-
- ScheduledFuture> scheduledFuture = systemContext
- .schedulePeriodicMsgWithDelay(ctx, scheduledMsg, refreshDynamicSourceInterval, refreshDynamicSourceInterval);
- cfDynamicArgumentsRefreshTasks.put(cf.getId(), scheduledFuture);
- log.debug("[{}][{}] Scheduled dynamic arguments refresh task for CF!", tenantId, cf.getId());
- }
-
- public void onDynamicArgumentsRefreshMsg(CalculatedFieldDynamicArgumentsRefreshMsg msg) {
- log.debug("[{}] [{}] Processing CF dynamic arguments refresh task.", tenantId, msg.getCfId());
- CalculatedFieldCtx cfCtx = calculatedFields.get(msg.getCfId());
- if (cfCtx == null) {
- log.debug("[{}][{}] Failed to find CF context, going to stop dynamic arguments refresh task for CF.", tenantId, msg.getCfId());
- cancelCfDynamicArgumentsRefreshTaskIfExists(msg.getCfId(), true);
- return;
- }
- applyToTargetCfEntityActors(cfCtx, msg.getCallback(), (id, cb) -> refreshDynamicArgumentsForEntity(id, msg.getCfId(), cb));
- }
-
- private void refreshDynamicArgumentsForEntity(EntityId entityId, CalculatedFieldId cfId, TbCallback callback) {
- log.debug("Pushing CF dynamic arguments refresh msg to specific actor [{}]", entityId);
- getOrCreateActor(entityId).tell(new EntityCalculatedFieldDynamicArgumentsRefreshMsg(tenantId, cfId, callback));
- }
-
private void linkedTelemetryMsgForEntity(EntityId entityId, EntityCalculatedFieldLinkedTelemetryMsg msg) {
log.debug("Pushing linked telemetry msg to specific actor [{}]", entityId);
getOrCreateActor(entityId).tell(msg);
@@ -560,12 +498,12 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware
cfCtx.init();
} catch (Exception e) {
throw CalculatedFieldException.builder().ctx(cfCtx).eventEntity(cf.getEntityId()).cause(e).errorMessage("Failed to initialize CF context").build();
+ } finally {
+ calculatedFields.put(cf.getId(), cfCtx);
+ // We use copy on write lists to safely pass the reference to another actor for the iteration.
+ // Alternative approach would be to use any list but avoid modifications to the list (change the complete map value instead)
+ entityIdCalculatedFields.computeIfAbsent(cf.getEntityId(), id -> new CopyOnWriteArrayList<>()).add(cfCtx);
}
- calculatedFields.put(cf.getId(), cfCtx);
- // We use copy on write lists to safely pass the reference to another actor for the iteration.
- // Alternative approach would be to use any list but avoid modifications to the list (change the complete map value instead)
- entityIdCalculatedFields.computeIfAbsent(cf.getEntityId(), id -> new CopyOnWriteArrayList<>()).add(cfCtx);
- scheduleDynamicArgumentsRefreshTaskForCfIfNeeded(cfCtx);
}
private void initCalculatedFieldLink(CalculatedFieldLink link) {
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 6374e4016d..03830c1c4c 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
@@ -51,6 +51,7 @@ import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.HasRuleEngineProfile;
+import org.thingsboard.server.common.data.HasTenantId;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.TenantProfile;
import org.thingsboard.server.common.data.alarm.Alarm;
@@ -60,6 +61,7 @@ import org.thingsboard.server.common.data.id.AssetId;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.id.HasId;
import org.thingsboard.server.common.data.id.RuleChainId;
import org.thingsboard.server.common.data.id.RuleNodeId;
import org.thingsboard.server.common.data.id.TenantId;
@@ -110,6 +112,7 @@ import org.thingsboard.server.dao.ota.OtaPackageService;
import org.thingsboard.server.dao.queue.QueueService;
import org.thingsboard.server.dao.queue.QueueStatsService;
import org.thingsboard.server.dao.relation.RelationService;
+import org.thingsboard.server.dao.resource.TbResourceDataCache;
import org.thingsboard.server.dao.resource.ResourceService;
import org.thingsboard.server.dao.rule.RuleChainService;
import org.thingsboard.server.dao.tenant.TenantService;
@@ -770,6 +773,11 @@ public class DefaultTbContext implements TbContext {
return mainCtx.getResourceService();
}
+ @Override
+ public TbResourceDataCache getTbResourceDataCache() {
+ return mainCtx.getResourceDataCache();
+ }
+
@Override
public OtaPackageService getOtaPackageService() {
return mainCtx.getOtaPackageService();
@@ -1054,11 +1062,20 @@ public class DefaultTbContext implements TbContext {
@Override
public void checkTenantEntity(EntityId entityId) throws TbNodeException {
- if (!this.getTenantId().equals(TenantIdLoader.findTenantId(this, entityId))) {
+ TenantId actualTenantId = TenantIdLoader.findTenantId(this, entityId);
+ if (!getTenantId().equals(actualTenantId)) {
throw new TbNodeException("Entity with id: '" + entityId + "' specified in the configuration doesn't belong to the current tenant.", true);
}
}
+ @Override
+ public & HasTenantId, I extends EntityId> void checkTenantOrSystemEntity(E entity) throws TbNodeException {
+ TenantId actualTenantId = entity.getTenantId();
+ if (!getTenantId().equals(actualTenantId) && !actualTenantId.isSysTenantId()) {
+ throw new TbNodeException("Entity with id: '" + entity.getId() + "' specified in the configuration doesn't belong to the current or system tenant.", true);
+ }
+ }
+
private static String getFailureMessage(Throwable th) {
String failureMessage;
if (th != null) {
diff --git a/application/src/main/java/org/thingsboard/server/controller/AssetController.java b/application/src/main/java/org/thingsboard/server/controller/AssetController.java
index 2a4cce143a..d4223d30f1 100644
--- a/application/src/main/java/org/thingsboard/server/controller/AssetController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/AssetController.java
@@ -34,6 +34,9 @@ import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.EntitySubtype;
+import org.thingsboard.server.common.data.NameConflictPolicy;
+import org.thingsboard.server.common.data.NameConflictStrategy;
+import org.thingsboard.server.common.data.UniquifyStrategy;
import org.thingsboard.server.common.data.asset.Asset;
import org.thingsboard.server.common.data.asset.AssetInfo;
import org.thingsboard.server.common.data.asset.AssetSearchQuery;
@@ -76,6 +79,8 @@ import static org.thingsboard.server.controller.ControllerConstants.EDGE_ASSIGN_
import static org.thingsboard.server.controller.ControllerConstants.EDGE_ID_PARAM_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.EDGE_UNASSIGN_ASYNC_FIRST_STEP_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.EDGE_UNASSIGN_RECEIVE_STEP_DESCRIPTION;
+import static org.thingsboard.server.controller.ControllerConstants.NAME_CONFLICT_POLICY_DESC;
+import static org.thingsboard.server.controller.ControllerConstants.UNIQUIFY_SEPARATOR_DESC;
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;
@@ -83,6 +88,7 @@ import static org.thingsboard.server.controller.ControllerConstants.SORT_ORDER_D
import static org.thingsboard.server.controller.ControllerConstants.SORT_PROPERTY_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.TENANT_AUTHORITY_PARAGRAPH;
import static org.thingsboard.server.controller.ControllerConstants.TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH;
+import static org.thingsboard.server.controller.ControllerConstants.UNIQUIFY_STRATEGY_DESC;
import static org.thingsboard.server.controller.ControllerConstants.UUID_WIKI_LINK;
import static org.thingsboard.server.controller.EdgeController.EDGE_ID;
@@ -137,10 +143,16 @@ public class AssetController extends BaseController {
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/asset", method = RequestMethod.POST)
@ResponseBody
- public Asset saveAsset(@io.swagger.v3.oas.annotations.parameters.RequestBody(description = "A JSON value representing the asset.") @RequestBody Asset asset) throws Exception {
+ public Asset saveAsset(@io.swagger.v3.oas.annotations.parameters.RequestBody(description = "A JSON value representing the asset.") @RequestBody Asset asset,
+ @Parameter(description = NAME_CONFLICT_POLICY_DESC)
+ @RequestParam(name = "nameConflictPolicy", defaultValue = "FAIL") NameConflictPolicy nameConflictPolicy,
+ @Parameter(description = UNIQUIFY_SEPARATOR_DESC)
+ @RequestParam(name = "uniquifySeparator", defaultValue = "_") String uniquifySeparator,
+ @Parameter(description = UNIQUIFY_STRATEGY_DESC)
+ @RequestParam(name = "uniquifyStrategy", defaultValue = "RANDOM") UniquifyStrategy uniquifyStrategy) throws Exception {
asset.setTenantId(getTenantId());
checkEntity(asset.getId(), asset, Resource.ASSET);
- return tbAssetService.save(asset, getCurrentUser());
+ return tbAssetService.save(asset, new NameConflictStrategy(nameConflictPolicy, uniquifySeparator, uniquifyStrategy), getCurrentUser());
}
@ApiOperation(value = "Delete asset (deleteAsset)",
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 2c94b3da82..dbaee4300c 100644
--- a/application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java
+++ b/application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java
@@ -1745,4 +1745,18 @@ public class ControllerConstants {
MARKDOWN_CODE_BLOCK_END ;
protected static final String SECURITY_WRITE_CHECK = " Security check is performed to verify that the user has 'WRITE' permission for the entity (entities).";
+
+ public static final String NAME_CONFLICT_POLICY_DESC = "Optional value of name conflict policy. Possible values: FAIL or UNIQUIFY. " +
+ " If omitted, FAIL policy is applied. FAIL policy implies exception will be thrown if an entity with the same name already exists. " +
+ " UNIQUIFY policy appends a suffix to the entity name, if a name conflict occurs.";
+
+ public static final String UNIQUIFY_SEPARATOR_DESC = "Optional value of name suffix separator used by UNIQUIFY policy. By default, underscore separator is used. " +
+ "For example, strategy is UNIQUIFY, separator is '-'; if a name conflict occurs for entity name 'test-name', " +
+ "created entity will have name like 'test-name-7fsh4f'.";
+
+ public static final String UNIQUIFY_STRATEGY_DESC = "Optional value of uniquify strategy used by UNIQUIFY policy. Possible values: RANDOM or INCREMENTAL. " +
+ "By default, RANDOM strategy is used, which means random alphanumeric string will be added as a suffix to entity name. " +
+ "INCREMENTAL implies the first possible number starting from 1 will be added as a name suffix. " +
+ "For example, strategy is UNIQUIFY, uniquify strategy is INCREMENTAL; if a name conflict occurs for entity name 'test-name', " +
+ "created entity will have name like 'test-name-1.";
}
diff --git a/application/src/main/java/org/thingsboard/server/controller/CustomerController.java b/application/src/main/java/org/thingsboard/server/controller/CustomerController.java
index 03ac9c60fa..fca7ce3e86 100644
--- a/application/src/main/java/org/thingsboard/server/controller/CustomerController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/CustomerController.java
@@ -32,6 +32,9 @@ import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.Customer;
+import org.thingsboard.server.common.data.NameConflictStrategy;
+import org.thingsboard.server.common.data.NameConflictPolicy;
+import org.thingsboard.server.common.data.UniquifyStrategy;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.TenantId;
@@ -47,6 +50,8 @@ import static org.thingsboard.server.controller.ControllerConstants.CUSTOMER_ID;
import static org.thingsboard.server.controller.ControllerConstants.CUSTOMER_ID_PARAM_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.CUSTOMER_TEXT_SEARCH_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.HOME_DASHBOARD;
+import static org.thingsboard.server.controller.ControllerConstants.NAME_CONFLICT_POLICY_DESC;
+import static org.thingsboard.server.controller.ControllerConstants.UNIQUIFY_SEPARATOR_DESC;
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;
@@ -54,6 +59,7 @@ import static org.thingsboard.server.controller.ControllerConstants.SORT_ORDER_D
import static org.thingsboard.server.controller.ControllerConstants.SORT_PROPERTY_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.TENANT_AUTHORITY_PARAGRAPH;
import static org.thingsboard.server.controller.ControllerConstants.TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH;
+import static org.thingsboard.server.controller.ControllerConstants.UNIQUIFY_STRATEGY_DESC;
import static org.thingsboard.server.controller.ControllerConstants.UUID_WIKI_LINK;
@RestController
@@ -128,10 +134,16 @@ public class CustomerController extends BaseController {
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/customer", method = RequestMethod.POST)
@ResponseBody
- public Customer saveCustomer(@io.swagger.v3.oas.annotations.parameters.RequestBody(description = "A JSON value representing the customer.") @RequestBody Customer customer) throws Exception {
+ public Customer saveCustomer(@io.swagger.v3.oas.annotations.parameters.RequestBody(description = "A JSON value representing the customer.") @RequestBody Customer customer,
+ @Parameter(description = NAME_CONFLICT_POLICY_DESC)
+ @RequestParam(name = "nameConflictPolicy", defaultValue = "FAIL") NameConflictPolicy nameConflictPolicy,
+ @Parameter(description = UNIQUIFY_SEPARATOR_DESC)
+ @RequestParam(name = "uniquifySeparator", defaultValue = "_") String uniquifySeparator,
+ @Parameter(description = UNIQUIFY_STRATEGY_DESC)
+ @RequestParam(name = "uniquifyStrategy", defaultValue = "RANDOM") UniquifyStrategy uniquifyStrategy) throws Exception {
customer.setTenantId(getTenantId());
checkEntity(customer.getId(), customer, Resource.CUSTOMER);
- return tbCustomerService.save(customer, getCurrentUser());
+ return tbCustomerService.save(customer, new NameConflictStrategy(nameConflictPolicy, uniquifySeparator, uniquifyStrategy), getCurrentUser());
}
@ApiOperation(value = "Delete Customer (deleteCustomer)",
diff --git a/application/src/main/java/org/thingsboard/server/controller/DeviceController.java b/application/src/main/java/org/thingsboard/server/controller/DeviceController.java
index 61864dbf63..8bbf4ae2f6 100644
--- a/application/src/main/java/org/thingsboard/server/controller/DeviceController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/DeviceController.java
@@ -46,8 +46,11 @@ import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceInfo;
import org.thingsboard.server.common.data.DeviceInfoFilter;
import org.thingsboard.server.common.data.EntitySubtype;
+import org.thingsboard.server.common.data.NameConflictPolicy;
+import org.thingsboard.server.common.data.NameConflictStrategy;
import org.thingsboard.server.common.data.SaveDeviceWithCredentialsRequest;
import org.thingsboard.server.common.data.Tenant;
+import org.thingsboard.server.common.data.UniquifyStrategy;
import org.thingsboard.server.common.data.device.DeviceSearchQuery;
import org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
@@ -108,6 +111,8 @@ import static org.thingsboard.server.controller.ControllerConstants.EDGE_ASSIGN_
import static org.thingsboard.server.controller.ControllerConstants.EDGE_ID_PARAM_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.EDGE_UNASSIGN_ASYNC_FIRST_STEP_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.EDGE_UNASSIGN_RECEIVE_STEP_DESCRIPTION;
+import static org.thingsboard.server.controller.ControllerConstants.NAME_CONFLICT_POLICY_DESC;
+import static org.thingsboard.server.controller.ControllerConstants.UNIQUIFY_SEPARATOR_DESC;
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;
@@ -117,6 +122,7 @@ import static org.thingsboard.server.controller.ControllerConstants.TENANT_AUTHO
import static org.thingsboard.server.controller.ControllerConstants.TENANT_ID;
import static org.thingsboard.server.controller.ControllerConstants.TENANT_ID_PARAM_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH;
+import static org.thingsboard.server.controller.ControllerConstants.UNIQUIFY_STRATEGY_DESC;
import static org.thingsboard.server.controller.ControllerConstants.UUID_WIKI_LINK;
import static org.thingsboard.server.controller.EdgeController.EDGE_ID;
@@ -177,14 +183,21 @@ public class DeviceController extends BaseController {
@ResponseBody
public Device saveDevice(@io.swagger.v3.oas.annotations.parameters.RequestBody(description = "A JSON value representing the device.") @RequestBody Device device,
@Parameter(description = "Optional value of the device credentials to be used during device creation. " +
- "If omitted, access token will be auto-generated.") @RequestParam(name = "accessToken", required = false) String accessToken) throws Exception {
+ "If omitted, access token will be auto-generated.")
+ @RequestParam(name = "accessToken", required = false) String accessToken,
+ @Parameter(description = NAME_CONFLICT_POLICY_DESC)
+ @RequestParam(name = "nameConflictPolicy", defaultValue = "FAIL") NameConflictPolicy nameConflictPolicy,
+ @Parameter(description = UNIQUIFY_SEPARATOR_DESC)
+ @RequestParam(name = "uniquifySeparator", defaultValue = "_") String uniquifySeparator,
+ @Parameter(description = UNIQUIFY_STRATEGY_DESC)
+ @RequestParam(name = "uniquifyStrategy", defaultValue = "RANDOM") UniquifyStrategy uniquifyStrategy) throws Exception {
device.setTenantId(getCurrentUser().getTenantId());
if (device.getId() != null) {
checkDeviceId(device.getId(), Operation.WRITE);
} else {
checkEntity(null, device, Resource.DEVICE);
}
- return tbDeviceService.save(device, accessToken, getCurrentUser());
+ return tbDeviceService.save(device, accessToken, new NameConflictStrategy(nameConflictPolicy, uniquifySeparator, uniquifyStrategy), getCurrentUser());
}
@ApiOperation(value = "Create Device (saveDevice) with credentials ",
@@ -209,12 +222,18 @@ public class DeviceController extends BaseController {
@RequestMapping(value = "/device-with-credentials", method = RequestMethod.POST)
@ResponseBody
public Device saveDeviceWithCredentials(@Parameter(description = "The JSON object with device and credentials. See method description above for example.")
- @Valid @RequestBody SaveDeviceWithCredentialsRequest deviceAndCredentials) throws ThingsboardException {
+ @Valid @RequestBody SaveDeviceWithCredentialsRequest deviceAndCredentials,
+ @Parameter(description = NAME_CONFLICT_POLICY_DESC)
+ @RequestParam(name = "nameConflictPolicy", defaultValue = "FAIL") NameConflictPolicy nameConflictPolicy,
+ @Parameter(description = UNIQUIFY_SEPARATOR_DESC)
+ @RequestParam(name = "uniquifySeparator", defaultValue = "_") String uniquifySeparator,
+ @Parameter(description = UNIQUIFY_STRATEGY_DESC)
+ @RequestParam(name = "uniquifyStrategy", defaultValue = "RANDOM") UniquifyStrategy uniquifyStrategy) throws ThingsboardException {
Device device = deviceAndCredentials.getDevice();
DeviceCredentials credentials = deviceAndCredentials.getCredentials();
device.setTenantId(getCurrentUser().getTenantId());
checkEntity(device.getId(), device, Resource.DEVICE);
- return tbDeviceService.saveDeviceWithCredentials(device, credentials, getCurrentUser());
+ return tbDeviceService.saveDeviceWithCredentials(device, credentials, new NameConflictStrategy(nameConflictPolicy, uniquifySeparator, uniquifyStrategy), getCurrentUser());
}
@ApiOperation(value = "Delete device (deleteDevice)",
diff --git a/application/src/main/java/org/thingsboard/server/controller/EntityViewController.java b/application/src/main/java/org/thingsboard/server/controller/EntityViewController.java
index b1b6b6b1e3..67fc02ab29 100644
--- a/application/src/main/java/org/thingsboard/server/controller/EntityViewController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/EntityViewController.java
@@ -34,6 +34,9 @@ import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.EntitySubtype;
import org.thingsboard.server.common.data.EntityView;
import org.thingsboard.server.common.data.EntityViewInfo;
+import org.thingsboard.server.common.data.NameConflictPolicy;
+import org.thingsboard.server.common.data.NameConflictStrategy;
+import org.thingsboard.server.common.data.UniquifyStrategy;
import org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.common.data.entityview.EntityViewSearchQuery;
import org.thingsboard.server.common.data.exception.ThingsboardException;
@@ -69,6 +72,8 @@ import static org.thingsboard.server.controller.ControllerConstants.ENTITY_VIEW_
import static org.thingsboard.server.controller.ControllerConstants.ENTITY_VIEW_TEXT_SEARCH_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.ENTITY_VIEW_TYPE;
import static org.thingsboard.server.controller.ControllerConstants.MODEL_DESCRIPTION;
+import static org.thingsboard.server.controller.ControllerConstants.NAME_CONFLICT_POLICY_DESC;
+import static org.thingsboard.server.controller.ControllerConstants.UNIQUIFY_SEPARATOR_DESC;
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;
@@ -76,6 +81,7 @@ import static org.thingsboard.server.controller.ControllerConstants.SORT_ORDER_D
import static org.thingsboard.server.controller.ControllerConstants.SORT_PROPERTY_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.TENANT_AUTHORITY_PARAGRAPH;
import static org.thingsboard.server.controller.ControllerConstants.TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH;
+import static org.thingsboard.server.controller.ControllerConstants.UNIQUIFY_STRATEGY_DESC;
import static org.thingsboard.server.controller.EdgeController.EDGE_ID;
/**
@@ -128,7 +134,13 @@ public class EntityViewController extends BaseController {
@ResponseBody
public EntityView saveEntityView(
@Parameter(description = "A JSON object representing the entity view.")
- @RequestBody EntityView entityView) throws Exception {
+ @RequestBody EntityView entityView,
+ @Parameter(description = NAME_CONFLICT_POLICY_DESC)
+ @RequestParam(name = "nameConflictPolicy", defaultValue = "FAIL") NameConflictPolicy nameConflictPolicy,
+ @Parameter(description = UNIQUIFY_SEPARATOR_DESC)
+ @RequestParam(name = "uniquifySeparator", defaultValue = "_") String uniquifySeparator,
+ @Parameter(description = UNIQUIFY_STRATEGY_DESC)
+ @RequestParam(name = "uniquifyStrategy", defaultValue = "RANDOM") UniquifyStrategy uniquifyStrategy) throws Exception {
entityView.setTenantId(getCurrentUser().getTenantId());
EntityView existingEntityView = null;
if (entityView.getId() == null) {
@@ -137,7 +149,7 @@ public class EntityViewController extends BaseController {
} else {
existingEntityView = checkEntityViewId(entityView.getId(), Operation.WRITE);
}
- return tbEntityViewService.save(entityView, existingEntityView, getCurrentUser());
+ return tbEntityViewService.save(entityView, existingEntityView, new NameConflictStrategy(nameConflictPolicy, uniquifySeparator, uniquifyStrategy), getCurrentUser());
}
@ApiOperation(value = "Delete entity view (deleteEntityView)",
diff --git a/application/src/main/java/org/thingsboard/server/controller/ImageController.java b/application/src/main/java/org/thingsboard/server/controller/ImageController.java
index f9ec7fd844..9288484f86 100644
--- a/application/src/main/java/org/thingsboard/server/controller/ImageController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/ImageController.java
@@ -300,6 +300,7 @@ public class ImageController extends BaseController {
tbImageService.putETag(cacheKey, descriptor.getEtag());
var result = ResponseEntity.ok()
.header("Content-Type", descriptor.getMediaType())
+ .header("Content-Security-Policy", "default-src 'none'")
.eTag(descriptor.getEtag());
if (!cacheKey.isPublic()) {
result
diff --git a/application/src/main/java/org/thingsboard/server/controller/Lwm2mController.java b/application/src/main/java/org/thingsboard/server/controller/Lwm2mController.java
index f0ec727896..7e7cd84a12 100644
--- a/application/src/main/java/org/thingsboard/server/controller/Lwm2mController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/Lwm2mController.java
@@ -27,6 +27,8 @@ import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.Device;
+import org.thingsboard.server.common.data.NameConflictPolicy;
+import org.thingsboard.server.common.data.NameConflictStrategy;
import org.thingsboard.server.common.data.SaveDeviceWithCredentialsRequest;
import org.thingsboard.server.common.data.device.profile.lwm2m.bootstrap.LwM2MServerSecurityConfigDefault;
import org.thingsboard.server.common.data.exception.ThingsboardException;
@@ -37,6 +39,7 @@ import org.thingsboard.server.service.lwm2m.LwM2MService;
import java.util.Map;
+import static org.thingsboard.server.common.data.NameConflictStrategy.DEFAULT;
import static org.thingsboard.server.controller.ControllerConstants.IS_BOOTSTRAP_SERVER_PARAM_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH;
@@ -73,6 +76,6 @@ public class Lwm2mController extends BaseController {
public Device saveDeviceWithCredentials(@RequestBody Map, Object> deviceWithDeviceCredentials) throws ThingsboardException {
Device device = checkNotNull(JacksonUtil.convertValue(deviceWithDeviceCredentials.get(Device.class), Device.class));
DeviceCredentials credentials = checkNotNull(JacksonUtil.convertValue(deviceWithDeviceCredentials.get(DeviceCredentials.class), DeviceCredentials.class));
- return deviceController.saveDeviceWithCredentials(new SaveDeviceWithCredentialsRequest(device, credentials));
+ return deviceController.saveDeviceWithCredentials(new SaveDeviceWithCredentialsRequest(device, credentials), DEFAULT.policy(), DEFAULT.separator(), DEFAULT.uniquifyStrategy());
}
}
diff --git a/application/src/main/java/org/thingsboard/server/controller/TbResourceController.java b/application/src/main/java/org/thingsboard/server/controller/TbResourceController.java
index b23603f6a2..54d2494679 100644
--- a/application/src/main/java/org/thingsboard/server/controller/TbResourceController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/TbResourceController.java
@@ -55,14 +55,17 @@ import org.thingsboard.server.common.data.util.ThrowingSupplier;
import org.thingsboard.server.config.annotations.ApiOperation;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.resource.TbResourceService;
+import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.permission.Operation;
import org.thingsboard.server.service.security.permission.Resource;
+import java.util.ArrayList;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
+import java.util.UUID;
import static org.thingsboard.server.controller.ControllerConstants.AVAILABLE_FOR_ANY_AUTHORIZED_USER;
import static org.thingsboard.server.controller.ControllerConstants.LWM2M_OBJECT_DESCRIPTION;
@@ -263,6 +266,20 @@ public class TbResourceController extends BaseController {
}
}
+ @ApiOperation(value = "Get Resource Infos by ids (getSystemOrTenantResourcesByIds)")
+ @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
+ @GetMapping(value = "/resource", params = {"resourceIds"})
+ public List getSystemOrTenantResourcesByIds(
+ @Parameter(description = "A list of resource ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string")))
+ @RequestParam("resourceIds") Set resourceUuids) throws ThingsboardException {
+ SecurityUser user = getCurrentUser();
+ List resourceIds = new ArrayList<>();
+ for (UUID resourceId : resourceUuids) {
+ resourceIds.add(new TbResourceId(resourceId));
+ }
+ return resourceService.findSystemOrTenantResourcesByIds(user.getTenantId(), resourceIds);
+ }
+
@ApiOperation(value = "Get All Resource Infos (getAllResources)",
notes = "Returns a page of Resource Info objects owned by tenant. " +
PAGE_DATA_PARAMETERS + RESOURCE_INFO_DESCRIPTION + TENANT_AUTHORITY_PARAGRAPH)
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 69dd98f47f..c631f1a3d0 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
@@ -32,8 +32,10 @@ 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.ollama.OllamaChatModel;
import dev.langchain4j.model.openai.OpenAiChatModel;
import dev.langchain4j.model.vertexai.gemini.VertexAiGeminiChatModel;
+import org.springframework.http.HttpHeaders;
import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.ai.model.chat.AmazonBedrockChatModelConfig;
import org.thingsboard.server.common.data.ai.model.chat.AnthropicChatModelConfig;
@@ -43,10 +45,12 @@ import org.thingsboard.server.common.data.ai.model.chat.GoogleAiGeminiChatModelC
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.MistralAiChatModelConfig;
+import org.thingsboard.server.common.data.ai.model.chat.OllamaChatModelConfig;
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;
+import org.thingsboard.server.common.data.ai.provider.OllamaProviderConfig;
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
import software.amazon.awssdk.regions.Region;
@@ -54,7 +58,11 @@ import software.amazon.awssdk.services.bedrockruntime.BedrockRuntimeClient;
import java.io.ByteArrayInputStream;
import java.io.IOException;
+import java.nio.charset.StandardCharsets;
import java.time.Duration;
+import java.util.Base64;
+
+import static java.util.Collections.singletonMap;
@Component
class Langchain4jChatModelConfigurerImpl implements Langchain4jChatModelConfigurer {
@@ -62,6 +70,7 @@ class Langchain4jChatModelConfigurerImpl implements Langchain4jChatModelConfigur
@Override
public ChatModel configureChatModel(OpenAiChatModelConfig chatModelConfig) {
return OpenAiChatModel.builder()
+ .baseUrl(chatModelConfig.providerConfig().baseUrl())
.apiKey(chatModelConfig.providerConfig().apiKey())
.modelName(chatModelConfig.modelId())
.temperature(chatModelConfig.temperature())
@@ -134,7 +143,7 @@ class Langchain4jChatModelConfigurerImpl implements Langchain4jChatModelConfigur
// set request timeout from model config
if (chatModelConfig.timeoutSeconds() != null) {
- retrySettings.setTotalTimeout(org.threeten.bp.Duration.ofSeconds(chatModelConfig.timeoutSeconds()));
+ retrySettings.setTotalTimeoutDuration(Duration.ofSeconds(chatModelConfig.timeoutSeconds()));
}
// set updated retry settings
@@ -262,6 +271,35 @@ class Langchain4jChatModelConfigurerImpl implements Langchain4jChatModelConfigur
.build();
}
+ @Override
+ public ChatModel configureChatModel(OllamaChatModelConfig chatModelConfig) {
+ var builder = OllamaChatModel.builder()
+ .baseUrl(chatModelConfig.providerConfig().baseUrl())
+ .modelName(chatModelConfig.modelId())
+ .temperature(chatModelConfig.temperature())
+ .topP(chatModelConfig.topP())
+ .topK(chatModelConfig.topK())
+ .numCtx(chatModelConfig.contextLength())
+ .numPredict(chatModelConfig.maxOutputTokens())
+ .timeout(toDuration(chatModelConfig.timeoutSeconds()))
+ .maxRetries(chatModelConfig.maxRetries());
+
+ var auth = chatModelConfig.providerConfig().auth();
+ if (auth instanceof OllamaProviderConfig.OllamaAuth.Basic basicAuth) {
+ String credentials = basicAuth.username() + ":" + basicAuth.password();
+ String encodedCredentials = Base64.getEncoder().encodeToString(credentials.getBytes(StandardCharsets.UTF_8));
+ builder.customHeaders(singletonMap(HttpHeaders.AUTHORIZATION, "Basic " + encodedCredentials));
+ } else if (auth instanceof OllamaProviderConfig.OllamaAuth.Token tokenAuth) {
+ builder.customHeaders(singletonMap(HttpHeaders.AUTHORIZATION, "Bearer " + tokenAuth.token()));
+ } else if (auth instanceof OllamaProviderConfig.OllamaAuth.None) {
+ // do nothing
+ } else {
+ throw new UnsupportedOperationException("Unknown authentication type: " + auth.getClass().getSimpleName());
+ }
+
+ return builder.build();
+ }
+
private static Duration toDuration(Integer timeoutSeconds) {
return timeoutSeconds != null ? Duration.ofSeconds(timeoutSeconds) : null;
}
diff --git a/application/src/main/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateService.java b/application/src/main/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateService.java
index 3dedea999c..84025b3cee 100644
--- a/application/src/main/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateService.java
+++ b/application/src/main/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateService.java
@@ -442,13 +442,13 @@ public class DefaultTbApiUsageStateService extends AbstractPartitionBasedService
boolean check(long threshold, long warnThreshold, long value);
}
- private void checkStartOfNextCycle() {
+ public void checkStartOfNextCycle() {
updateLock.lock();
try {
long now = System.currentTimeMillis();
myUsageStates.values().forEach(state -> {
if ((state.getNextCycleTs() < now) && (now - state.getNextCycleTs() < TimeUnit.HOURS.toMillis(1))) {
- state.setCycles(state.getNextCycleTs(), SchedulerUtils.getStartOfNextNextMonth());
+ state.setCycles(state.getNextCycleTs(), SchedulerUtils.getStartOfNextMonth());
if (log.isTraceEnabled()) {
log.trace("[{}][{}] Updating state cycles (currentCycleTs={},nextCycleTs={})", state.getTenantId(), state.getEntityId(), state.getCurrentCycleTs(), state.getNextCycleTs());
}
diff --git a/application/src/main/java/org/thingsboard/server/service/cf/AbstractCalculatedFieldProcessingService.java b/application/src/main/java/org/thingsboard/server/service/cf/AbstractCalculatedFieldProcessingService.java
index 45305ca9e3..fa10a49503 100644
--- a/application/src/main/java/org/thingsboard/server/service/cf/AbstractCalculatedFieldProcessingService.java
+++ b/application/src/main/java/org/thingsboard/server/service/cf/AbstractCalculatedFieldProcessingService.java
@@ -26,7 +26,7 @@ import lombok.extern.slf4j.Slf4j;
import org.thingsboard.common.util.ThingsBoardExecutors;
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.RelationQueryDynamicSourceConfiguration;
+import org.thingsboard.server.common.data.cf.configuration.RelationPathQueryDynamicSourceConfiguration;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.kv.Aggregation;
@@ -45,6 +45,7 @@ import org.thingsboard.server.dao.usagerecord.ApiLimitService;
import org.thingsboard.server.service.cf.ctx.state.ArgumentEntry;
import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx;
import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldState;
+import org.thingsboard.server.service.cf.ctx.state.geofencing.GeofencingCalculatedFieldState;
import java.util.HashMap;
import java.util.List;
@@ -102,6 +103,10 @@ public abstract class AbstractCalculatedFieldProcessingService {
return Futures.whenAllComplete(argFutures.values()).call(() -> {
var result = createStateByType(ctx);
result.updateState(ctx, resolveArgumentFutures(argFutures));
+ // TODO: move to state.init() method after merge with alarm rules 2.0
+ if (ctx.hasRelationQueryDynamicArguments() && result instanceof GeofencingCalculatedFieldState geofencingCalculatedFieldState) {
+ geofencingCalculatedFieldState.setLastDynamicArgumentsRefreshTs(System.currentTimeMillis());
+ }
return result;
}, MoreExecutors.directExecutor());
}
@@ -159,19 +164,9 @@ public abstract class AbstractCalculatedFieldProcessingService {
}
var refDynamicSourceConfiguration = value.getRefDynamicSourceConfiguration();
return switch (refDynamicSourceConfiguration.getType()) {
- case RELATION_QUERY -> {
- var configuration = (RelationQueryDynamicSourceConfiguration) refDynamicSourceConfiguration;
- if (configuration.isSimpleRelation()) {
- yield switch (configuration.getDirection()) {
- case FROM ->
- Futures.transform(relationService.findByFromAndTypeAsync(tenantId, entityId, configuration.getRelationType(), RelationTypeGroup.COMMON),
- configuration::resolveEntityIds, calculatedFieldCallbackExecutor);
- case TO ->
- Futures.transform(relationService.findByToAndTypeAsync(tenantId, entityId, configuration.getRelationType(), RelationTypeGroup.COMMON),
- configuration::resolveEntityIds, calculatedFieldCallbackExecutor);
- };
- }
- yield Futures.transform(relationService.findByQuery(tenantId, configuration.toEntityRelationsQuery(entityId)),
+ case RELATION_PATH_QUERY -> {
+ var configuration = (RelationPathQueryDynamicSourceConfiguration) refDynamicSourceConfiguration;
+ yield Futures.transform(relationService.findByRelationPathQueryAsync(tenantId, configuration.toRelationPathQuery(entityId)),
configuration::resolveEntityIds, calculatedFieldCallbackExecutor);
}
};
diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/BaseCalculatedFieldState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/BaseCalculatedFieldState.java
index 9a1d06cf24..6d877331bd 100644
--- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/BaseCalculatedFieldState.java
+++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/BaseCalculatedFieldState.java
@@ -62,7 +62,7 @@ public abstract class BaseCalculatedFieldState implements CalculatedFieldState {
boolean entryUpdated;
if (existingEntry == null || newEntry.isForceResetPrevious()) {
- validateNewEntry(newEntry);
+ validateNewEntry(key, newEntry);
arguments.put(key, newEntry);
entryUpdated = true;
} else {
@@ -93,7 +93,7 @@ public abstract class BaseCalculatedFieldState implements CalculatedFieldState {
}
}
- protected void validateNewEntry(ArgumentEntry newEntry) {}
+ protected void validateNewEntry(String key, ArgumentEntry newEntry) {}
private void updateLastUpdateTimestamp(ArgumentEntry entry) {
long newTs = this.latestTimestamp;
diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java
index c2cc083853..650aaaa169 100644
--- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java
+++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java
@@ -43,11 +43,13 @@ import org.thingsboard.server.dao.relation.RelationService;
import org.thingsboard.server.dao.usagerecord.ApiLimitService;
import org.thingsboard.server.gen.transport.TransportProtos.CalculatedFieldTelemetryMsgProto;
import org.thingsboard.server.service.cf.ctx.CalculatedFieldEntityCtxId;
+import org.thingsboard.server.service.cf.ctx.state.geofencing.GeofencingCalculatedFieldState;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import java.util.concurrent.TimeUnit;
import static org.thingsboard.common.util.ExpressionFunctionsUtil.userDefinedFunctions;
@@ -78,9 +80,12 @@ public class CalculatedFieldCtx {
private long maxStateSize;
private long maxSingleValueArgumentSize;
+ private boolean relationQueryDynamicArguments;
private List mainEntityGeofencingArgumentNames;
private List linkedEntityGeofencingArgumentNames;
+ private long scheduledUpdateIntervalMillis;
+
public CalculatedFieldCtx(CalculatedField calculatedField, TbelInvokeService tbelInvokeService, ApiLimitService apiLimitService, RelationService relationService) {
this.calculatedField = calculatedField;
@@ -101,6 +106,7 @@ public class CalculatedFieldCtx {
var refId = entry.getValue().getRefEntityId();
var refKey = entry.getValue().getRefEntityKey();
if (refId == null && entry.getValue().hasDynamicSource()) {
+ relationQueryDynamicArguments = true;
continue;
}
if (refId == null || refId.equals(calculatedField.getEntityId())) {
@@ -126,6 +132,9 @@ public class CalculatedFieldCtx {
});
}
}
+ if (calculatedField.getConfiguration() instanceof ScheduledUpdateSupportedCalculatedFieldConfiguration scheduledConfig) {
+ this.scheduledUpdateIntervalMillis = scheduledConfig.isScheduledUpdateEnabled() ? TimeUnit.SECONDS.toMillis(scheduledConfig.getScheduledUpdateInterval()) : -1L;
+ }
this.tbelInvokeService = tbelInvokeService;
this.relationService = relationService;
@@ -141,6 +150,7 @@ public class CalculatedFieldCtx {
this.calculatedFieldScriptEngine = initEngine(tenantId, expression, tbelInvokeService);
initialized = true;
} catch (Exception e) {
+ initialized = false;
throw new RuntimeException("Failed to init calculated field ctx. Invalid expression syntax.", e);
}
}
@@ -156,6 +166,7 @@ public class CalculatedFieldCtx {
);
initialized = true;
} else {
+ initialized = false;
throw new RuntimeException("Failed to init calculated field ctx. Invalid expression syntax.");
}
}
@@ -329,25 +340,42 @@ public class CalculatedFieldCtx {
public boolean hasOtherSignificantChanges(CalculatedFieldCtx other) {
boolean expressionChanged = calculatedField.getConfiguration() instanceof ExpressionBasedCalculatedFieldConfiguration && !expression.equals(other.expression);
boolean outputChanged = !output.equals(other.output);
- return expressionChanged || outputChanged;
+ boolean scheduledUpdatesConfigChanged = scheduledUpdateIntervalMillis != other.scheduledUpdateIntervalMillis;
+ return expressionChanged || outputChanged || scheduledUpdatesConfigChanged;
}
public boolean hasStateChanges(CalculatedFieldCtx other) {
boolean typeChanged = !cfType.equals(other.cfType);
boolean argumentsChanged = !arguments.equals(other.arguments);
- return typeChanged || argumentsChanged;
+ boolean geoZoneGroupsConfigChanged = hasGeofencingZoneGroupConfigurationChanges(other);
+ return typeChanged || argumentsChanged || geoZoneGroupsConfigChanged;
}
- public boolean hasSchedulingConfigChanges(CalculatedFieldCtx other) {
- if (calculatedField.getConfiguration() instanceof ScheduledUpdateSupportedCalculatedFieldConfiguration thisConfig
- && other.calculatedField.getConfiguration() instanceof ScheduledUpdateSupportedCalculatedFieldConfiguration otherConfig) {
- boolean refreshTriggerChanged = thisConfig.isScheduledUpdateEnabled() != otherConfig.isScheduledUpdateEnabled();
- boolean refreshIntervalChanged = thisConfig.getScheduledUpdateInterval() != otherConfig.getScheduledUpdateInterval();
- return refreshTriggerChanged || refreshIntervalChanged;
+ private boolean hasGeofencingZoneGroupConfigurationChanges(CalculatedFieldCtx other) {
+ if (calculatedField.getConfiguration() instanceof GeofencingCalculatedFieldConfiguration thisConfig
+ && other.calculatedField.getConfiguration() instanceof GeofencingCalculatedFieldConfiguration otherConfig) {
+ return !thisConfig.getZoneGroups().equals(otherConfig.getZoneGroups());
}
return false;
}
+ public boolean hasRelationQueryDynamicArguments() {
+ return relationQueryDynamicArguments && scheduledUpdateIntervalMillis != -1;
+ }
+
+ public boolean shouldFetchDynamicArgumentsFromDb(CalculatedFieldState state) {
+ if (!hasRelationQueryDynamicArguments()) {
+ return false;
+ }
+ if (!(state instanceof GeofencingCalculatedFieldState geofencingState)) {
+ return false;
+ }
+ if (geofencingState.getLastDynamicArgumentsRefreshTs() == -1L) {
+ return true;
+ }
+ return geofencingState.getLastDynamicArgumentsRefreshTs() < System.currentTimeMillis() - scheduledUpdateIntervalMillis;
+ }
+
public String getSizeExceedsLimitMessage() {
return "Failed to init CF state. State size exceeds limit of " + (maxStateSize / 1024) + "Kb!";
}
diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldState.java
index 5f8e7538c4..3e0964bfd2 100644
--- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldState.java
+++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldState.java
@@ -50,13 +50,6 @@ public interface CalculatedFieldState {
long getLatestTimestamp();
- default void setDirty(boolean dirty) {
- }
-
- default boolean isDirty() {
- return false;
- }
-
void setRequiredArguments(List requiredArguments);
boolean updateState(CalculatedFieldCtx ctx, Map argumentValues);
diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldState.java
index 80b650fc7c..5a437b52f3 100644
--- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldState.java
+++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldState.java
@@ -48,9 +48,10 @@ public class SimpleCalculatedFieldState extends BaseCalculatedFieldState {
}
@Override
- protected void validateNewEntry(ArgumentEntry newEntry) {
+ protected void validateNewEntry(String key, ArgumentEntry newEntry) {
if (newEntry instanceof TsRollingArgumentEntry) {
- throw new IllegalArgumentException("Rolling argument entry is not supported for simple calculated fields.");
+ throw new IllegalArgumentException("Unsupported argument type detected for argument: " + key + ". " +
+ "Rolling argument entry is not supported for simple calculated fields.");
}
}
diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/geofencing/GeofencingArgumentEntry.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/geofencing/GeofencingArgumentEntry.java
index f526cc00ab..53e5c19e72 100644
--- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/geofencing/GeofencingArgumentEntry.java
+++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/geofencing/GeofencingArgumentEntry.java
@@ -41,7 +41,7 @@ public class GeofencingArgumentEntry implements ArgumentEntry {
}
public GeofencingArgumentEntry(EntityId entityId, TransportProtos.AttributeValueProto entry) {
- this.zoneStates = toZones(Map.of(entityId, ProtoUtils.fromProto(entry)));
+ this(Map.of(entityId, ProtoUtils.fromProto(entry)));
}
public GeofencingArgumentEntry(Map entityIdkvEntryMap) {
@@ -63,6 +63,10 @@ public class GeofencingArgumentEntry implements ArgumentEntry {
if (!(entry instanceof GeofencingArgumentEntry geofencingArgumentEntry)) {
throw new IllegalArgumentException("Unsupported argument entry type for geofencing argument entry: " + entry.getType());
}
+ if (geofencingArgumentEntry.isEmpty()) {
+ zoneStates.clear();
+ return true;
+ }
boolean updated = false;
for (var zoneEntry : geofencingArgumentEntry.getZoneStates().entrySet()) {
if (updateZone(zoneEntry)) {
@@ -97,6 +101,10 @@ public class GeofencingArgumentEntry implements ArgumentEntry {
zoneStates.put(zoneId, newZoneState);
return true;
}
+ if (newZoneState.getPerimeterDefinition() == null) {
+ zoneStates.remove(zoneId);
+ return true;
+ }
return existingZoneState.update(newZoneState);
}
diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/geofencing/GeofencingCalculatedFieldState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/geofencing/GeofencingCalculatedFieldState.java
index 506ddcff78..398de0c20b 100644
--- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/geofencing/GeofencingCalculatedFieldState.java
+++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/geofencing/GeofencingCalculatedFieldState.java
@@ -15,16 +15,19 @@
*/
package org.thingsboard.server.service.cf.ctx.state.geofencing;
+import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
import lombok.Data;
import lombok.EqualsAndHashCode;
+import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.common.util.geo.Coordinates;
import org.thingsboard.server.common.data.cf.CalculatedFieldType;
+import org.thingsboard.server.common.data.cf.configuration.OutputType;
import org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingCalculatedFieldConfiguration;
import org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingReportStrategy;
import org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingTransitionEvent;
@@ -39,7 +42,6 @@ import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx;
import org.thingsboard.server.service.cf.ctx.state.SingleValueArgumentEntry;
import java.util.ArrayList;
-import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@@ -51,18 +53,14 @@ import static org.thingsboard.server.common.data.cf.configuration.geofencing.Geo
@Data
@Slf4j
+@NoArgsConstructor
@EqualsAndHashCode(callSuper = true)
public class GeofencingCalculatedFieldState extends BaseCalculatedFieldState {
- private boolean dirty;
+ private long lastDynamicArgumentsRefreshTs = -1;
- public GeofencingCalculatedFieldState() {
- super(new ArrayList<>(), new HashMap<>(), false, -1);
- this.dirty = false;
- }
-
- public GeofencingCalculatedFieldState(List argNames) {
- super(argNames);
+ public GeofencingCalculatedFieldState(List requiredArguments) {
+ super(requiredArguments);
}
@Override
@@ -71,49 +69,21 @@ public class GeofencingCalculatedFieldState extends BaseCalculatedFieldState {
}
@Override
- public boolean updateState(CalculatedFieldCtx ctx, Map argumentValues) {
- if (arguments == null) {
- arguments = new HashMap<>();
- }
-
- boolean stateUpdated = false;
-
- for (var entry : argumentValues.entrySet()) {
- String key = entry.getKey();
- ArgumentEntry newEntry = entry.getValue();
-
- checkArgumentSize(key, newEntry, ctx);
-
- ArgumentEntry existingEntry = arguments.get(key);
- boolean entryUpdated;
-
- if (existingEntry == null || newEntry.isForceResetPrevious()) {
- entryUpdated = switch (key) {
- case ENTITY_ID_LATITUDE_ARGUMENT_KEY, ENTITY_ID_LONGITUDE_ARGUMENT_KEY -> {
- if (!(newEntry instanceof SingleValueArgumentEntry singleValueArgumentEntry)) {
- throw new IllegalArgumentException("Unsupported argument entry type for " + key + " argument: " + newEntry.getType() + ". " +
- "Only SINGLE_VALUE type is allowed.");
- }
- arguments.put(key, singleValueArgumentEntry);
- yield true;
- }
- default -> {
- if (!(newEntry instanceof GeofencingArgumentEntry geofencingArgumentEntry)) {
- throw new IllegalArgumentException("Unsupported argument entry type for " + key + " argument: " + newEntry.getType() + ". " +
- "Only GEOFENCING type is allowed.");
- }
- arguments.put(key, geofencingArgumentEntry);
- yield true;
- }
- };
- } else {
- entryUpdated = existingEntry.updateEntry(newEntry);
+ protected void validateNewEntry(String key, ArgumentEntry newEntry) {
+ switch (key) {
+ case ENTITY_ID_LATITUDE_ARGUMENT_KEY, ENTITY_ID_LONGITUDE_ARGUMENT_KEY -> {
+ if (!(newEntry instanceof SingleValueArgumentEntry)) {
+ throw new IllegalArgumentException("Unsupported argument entry type for " + key + " argument: " + newEntry.getType() + ". " +
+ "Only SINGLE_VALUE type is allowed.");
+ }
}
- if (entryUpdated) {
- stateUpdated = true;
+ default -> {
+ if (!(newEntry instanceof GeofencingArgumentEntry)) {
+ throw new IllegalArgumentException("Unsupported argument entry type for " + key + " argument: " + newEntry.getType() + ". " +
+ "Only GEOFENCING type is allowed.");
+ }
}
}
- return stateUpdated;
}
@Override
@@ -125,7 +95,7 @@ public class GeofencingCalculatedFieldState extends BaseCalculatedFieldState {
var geofencingCfg = (GeofencingCalculatedFieldConfiguration) ctx.getCalculatedField().getConfiguration();
Map zoneGroups = geofencingCfg.getZoneGroups();
- ObjectNode resultNode = JacksonUtil.newObjectNode();
+ ObjectNode valuesNode = JacksonUtil.newObjectNode();
List> relationFutures = new ArrayList<>();
getGeofencingArguments().forEach((argumentKey, argumentEntry) -> {
@@ -154,10 +124,11 @@ public class GeofencingCalculatedFieldState extends BaseCalculatedFieldState {
relationFutures.add(f);
}
});
- updateResultNode(argumentKey, zoneResults, zoneGroupCfg.getReportStrategy(), resultNode);
+ updateValuesNode(argumentKey, zoneResults, zoneGroupCfg.getReportStrategy(), valuesNode);
});
- var result = new CalculatedFieldResult(ctx.getOutput().getType(), ctx.getOutput().getScope(), resultNode);
+ OutputType outputType = ctx.getOutput().getType();
+ var result = new CalculatedFieldResult(ctx.getOutput().getType(), ctx.getOutput().getScope(), toResultNode(outputType, valuesNode));
if (relationFutures.isEmpty()) {
return Futures.immediateFuture(result);
}
@@ -171,7 +142,7 @@ public class GeofencingCalculatedFieldState extends BaseCalculatedFieldState {
.collect(Collectors.toMap(Map.Entry::getKey, entry -> (GeofencingArgumentEntry) entry.getValue()));
}
- private void updateResultNode(String argumentKey, List zoneResults, GeofencingReportStrategy geofencingReportStrategy, ObjectNode resultNode) {
+ private void updateValuesNode(String argumentKey, List zoneResults, GeofencingReportStrategy geofencingReportStrategy, ObjectNode resultNode) {
GeofencingEvalResult aggregationResult = aggregateZoneGroup(zoneResults);
final String eventKey = argumentKey + "Event";
final String statusKey = argumentKey + "Status";
@@ -185,6 +156,16 @@ public class GeofencingCalculatedFieldState extends BaseCalculatedFieldState {
}
}
+ private JsonNode toResultNode(OutputType outputType, ObjectNode valuesNode) {
+ if (OutputType.ATTRIBUTES.equals(outputType) || latestTimestamp == -1) {
+ return valuesNode;
+ }
+ ObjectNode resultNode = JacksonUtil.newObjectNode();
+ resultNode.put("ts", latestTimestamp);
+ resultNode.set("values", valuesNode);
+ return resultNode;
+ }
+
private GeofencingEvalResult aggregateZoneGroup(List zoneResults) {
boolean nowInside = zoneResults.stream().anyMatch(r -> INSIDE.equals(r.status()));
boolean prevInside = zoneResults.stream()
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 5c5eea32fd..c6bfef4971 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
@@ -24,6 +24,7 @@ import org.thingsboard.server.cache.limits.RateLimitService;
import org.thingsboard.server.cluster.TbClusterService;
import org.thingsboard.server.common.data.edge.EdgeEventType;
import org.thingsboard.server.common.msg.notification.NotificationRuleProcessor;
+import org.thingsboard.server.dao.ai.AiModelService;
import org.thingsboard.server.dao.alarm.AlarmCommentService;
import org.thingsboard.server.dao.alarm.AlarmService;
import org.thingsboard.server.dao.asset.AssetProfileService;
@@ -59,6 +60,7 @@ import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.edge.rpc.EdgeEventStorageSettings;
import org.thingsboard.server.service.edge.rpc.EdgeRpcService;
import org.thingsboard.server.service.edge.rpc.processor.EdgeProcessor;
+import org.thingsboard.server.service.edge.rpc.processor.ai.AiModelProcessor;
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;
@@ -261,6 +263,11 @@ public class EdgeContextComponent {
@Autowired
private CalculatedFieldProcessor calculatedFieldProcessor;
+ @Autowired
+ private AiModelService aiModelService;
+ @Autowired
+ private AiModelProcessor aiModelProcessor;
+
public EdgeProcessor getProcessor(EdgeEventType edgeEventType) {
EdgeProcessor processor = processorMap.get(edgeEventType);
if (processor == null) {
diff --git a/application/src/main/java/org/thingsboard/server/service/edge/EdgeEventSourcingListener.java b/application/src/main/java/org/thingsboard/server/service/edge/EdgeEventSourcingListener.java
index fda9a1d17b..825b911403 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
@@ -113,7 +113,7 @@ public class EdgeEventSourcingListener {
return;
}
try {
- if (EntityType.TENANT == entityType || EntityType.EDGE == entityType || EntityType.AI_MODEL == entityType) {
+ if (EntityType.TENANT == entityType || EntityType.EDGE == entityType) {
return;
}
log.trace("[{}] DeleteEntityEvent called: {}", tenantId, event);
@@ -227,7 +227,7 @@ public class EdgeEventSourcingListener {
break;
case TENANT:
return !event.getCreated();
- case API_USAGE_STATE, EDGE, AI_MODEL:
+ case API_USAGE_STATE, EDGE:
return false;
case DOMAIN:
if (entity instanceof Domain domain) {
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 192c56692d..505b03a1cd 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
@@ -44,6 +44,7 @@ import org.thingsboard.server.common.data.TbResource;
import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.TenantProfile;
import org.thingsboard.server.common.data.User;
+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.asset.Asset;
@@ -52,6 +53,7 @@ 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.AiModelId;
import org.thingsboard.server.common.data.id.AssetId;
import org.thingsboard.server.common.data.id.AssetProfileId;
import org.thingsboard.server.common.data.id.CalculatedFieldId;
@@ -86,6 +88,7 @@ import org.thingsboard.server.common.data.security.DeviceCredentials;
import org.thingsboard.server.common.data.security.UserCredentials;
import org.thingsboard.server.common.data.widget.WidgetTypeDetails;
import org.thingsboard.server.common.data.widget.WidgetsBundle;
+import org.thingsboard.server.gen.edge.v1.AiModelUpdateMsg;
import org.thingsboard.server.gen.edge.v1.AlarmCommentUpdateMsg;
import org.thingsboard.server.gen.edge.v1.AlarmUpdateMsg;
import org.thingsboard.server.gen.edge.v1.AssetProfileUpdateMsg;
@@ -654,4 +657,17 @@ public class EdgeMsgConstructorUtils {
.setIdLSB(calculatedFieldId.getId().getLeastSignificantBits()).build();
}
+ public static AiModelUpdateMsg constructAiModelUpdatedMsg(UpdateMsgType msgType, AiModel aiModel) {
+ return AiModelUpdateMsg.newBuilder().setMsgType(msgType).setEntity(JacksonUtil.toString(aiModel))
+ .setIdMSB(aiModel.getId().getId().getMostSignificantBits())
+ .setIdLSB(aiModel.getId().getId().getLeastSignificantBits()).build();
+ }
+
+ public static AiModelUpdateMsg constructAiModelDeleteMsg(AiModelId aiModelId) {
+ return AiModelUpdateMsg.newBuilder()
+ .setMsgType(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE)
+ .setIdMSB(aiModelId.getId().getMostSignificantBits())
+ .setIdLSB(aiModelId.getId().getLeastSignificantBits()).build();
+ }
+
}
diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java
index 7340696788..9fcb7425b2 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
@@ -93,14 +93,13 @@ 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<>();
private final ConcurrentMap> sessionEdgeEventChecks = new ConcurrentHashMap<>();
private final ConcurrentMap> localSyncEdgeRequests = new ConcurrentHashMap<>();
private final ConcurrentMap edgeEventsMigrationProcessed = new ConcurrentHashMap<>();
+ private final List zombieSessions = new ArrayList<>();
@Value("${edges.rpc.port}")
private int rpcPort;
@@ -193,7 +192,7 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i
this.edgeEventProcessingExecutorService = ThingsBoardExecutors.newScheduledThreadPool(schedulerPoolSize, "edge-event-check-scheduler");
this.sendDownlinkExecutorService = ThingsBoardExecutors.newScheduledThreadPool(sendSchedulerPoolSize, "edge-send-scheduler");
this.executorService = ThingsBoardExecutors.newSingleThreadScheduledExecutor("edge-service");
- this.executorService.scheduleAtFixedRate(this::destroyKafkaSessionIfDisconnectedAndConsumerActive, 60, 60, TimeUnit.SECONDS);
+ this.executorService.scheduleAtFixedRate(this::cleanupZombieSessions, 60, 60, TimeUnit.SECONDS);
log.info("Edge RPC service initialized!");
}
@@ -518,14 +517,10 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i
private void destroySession(EdgeGrpcSession session) {
try (session) {
- for (int i = 0; i < DESTROY_SESSION_MAX_ATTEMPTS; i++) {
- if (session.destroy()) {
- break;
- } else {
- try {
- Thread.sleep(100);
- } catch (InterruptedException ignored) {}
- }
+ if (!session.destroy()) {
+ log.warn("[{}][{}] Session destroy failed for edge [{}] with session id [{}]. Adding to zombie queue for later cleanup.",
+ session.getTenantId(), session.getEdge().getId(), session.getEdge().getName(), session.getSessionId());
+ zombieSessions.add(session);
}
}
}
@@ -634,7 +629,7 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i
}
}
- private void destroyKafkaSessionIfDisconnectedAndConsumerActive() {
+ private void cleanupZombieSessions() {
try {
List toRemove = new ArrayList<>();
for (EdgeGrpcSession session : sessions.values()) {
@@ -655,9 +650,19 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i
}
}
}
+ zombieSessions.removeIf(zombie -> {
+ if (zombie.destroy()) {
+ log.info("[{}][{}] Successfully cleaned up zombie session [{}] for edge [{}].",
+ zombie.getTenantId(), zombie.getEdge().getId(), zombie.getSessionId(), zombie.getEdge().getName());
+ return true;
+ } else {
+ log.warn("[{}][{}] Failed to remove zombie session [{}] for edge [{}].",
+ zombie.getTenantId(), zombie.getEdge().getId(), zombie.getSessionId(), zombie.getEdge().getName());
+ return false;
+ }
+ });
} catch (Exception e) {
log.warn("Failed to cleanup kafka sessions", 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 a86409c6af..e4297e9bfc 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
@@ -46,6 +46,7 @@ import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.page.TimePageLink;
import org.thingsboard.server.common.msg.edge.EdgeEventUpdateMsg;
import org.thingsboard.server.dao.edge.stats.EdgeStatsKey;
+import org.thingsboard.server.gen.edge.v1.AiModelUpdateMsg;
import org.thingsboard.server.gen.edge.v1.AlarmCommentUpdateMsg;
import org.thingsboard.server.gen.edge.v1.AlarmUpdateMsg;
import org.thingsboard.server.gen.edge.v1.AssetProfileUpdateMsg;
@@ -934,6 +935,11 @@ public abstract class EdgeGrpcSession implements Closeable {
result.add(ctx.getCalculatedFieldProcessor().processCalculatedFieldMsgFromEdge(edge.getTenantId(), edge, calculatedFieldUpdateMsg));
}
}
+ if (uplinkMsg.getAiModelUpdateMsgCount() > 0) {
+ for (AiModelUpdateMsg aiModelUpdateMsg : uplinkMsg.getAiModelUpdateMsgList()) {
+ result.add(ctx.getAiModelProcessor().processAiModelMsgFromEdge(edge.getTenantId(), edge, aiModelUpdateMsg));
+ }
+ }
} catch (Exception e) {
String failureMsg = String.format("Can't process uplink msg [%s] from edge", uplinkMsg);
log.trace("[{}][{}] Can't process uplink msg [{}]", tenantId, edge.getId(), uplinkMsg, e);
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 a2243a88d2..d6bfeae0b7 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,7 +139,7 @@ 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, CALCULATED_FIELD, NOTIFICATION_TEMPLATE,
+ WIDGETS_BUNDLE, WIDGET_TYPE, ADMIN_SETTINGS, OTA_PACKAGE, QUEUE, RELATION, CALCULATED_FIELD, AI_MODEL, NOTIFICATION_TEMPLATE,
NOTIFICATION_TARGET, NOTIFICATION_RULE -> true;
default -> false;
};
diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/ai/AiModelEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/ai/AiModelEdgeProcessor.java
new file mode 100644
index 0000000000..74ca7ac27a
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/ai/AiModelEdgeProcessor.java
@@ -0,0 +1,130 @@
+/**
+ * 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.ai;
+
+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.ai.AiModel;
+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.AiModelId;
+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.AiModelUpdateMsg;
+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.Optional;
+import java.util.UUID;
+
+@Slf4j
+@Component
+@TbCoreComponent
+public class AiModelEdgeProcessor extends BaseAiModelProcessor implements AiModelProcessor {
+
+ @Override
+ public ListenableFuture processAiModelMsgFromEdge(TenantId tenantId, Edge edge, AiModelUpdateMsg aiModelUpdateMsg) {
+ AiModelId aiModelId = new AiModelId(new UUID(aiModelUpdateMsg.getIdMSB(), aiModelUpdateMsg.getIdLSB()));
+ try {
+ edgeSynchronizationManager.getEdgeId().set(edge.getId());
+
+ switch (aiModelUpdateMsg.getMsgType()) {
+ case ENTITY_CREATED_RPC_MESSAGE:
+ case ENTITY_UPDATED_RPC_MESSAGE:
+ processAiModel(tenantId, aiModelId, aiModelUpdateMsg, edge);
+ return Futures.immediateFuture(null);
+ case UNRECOGNIZED:
+ default:
+ return handleUnsupportedMsgType(aiModelUpdateMsg.getMsgType());
+ }
+ } catch (DataValidationException e) {
+ return Futures.immediateFailedFuture(e);
+ } finally {
+ edgeSynchronizationManager.getEdgeId().remove();
+ }
+ }
+
+ @Override
+ public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) {
+ AiModelId aiModelId = new AiModelId(edgeEvent.getEntityId());
+ switch (edgeEvent.getAction()) {
+ case ADDED, UPDATED -> {
+ Optional aiModel = edgeCtx.getAiModelService().findAiModelById(edgeEvent.getTenantId(), aiModelId);
+ if (aiModel.isPresent()) {
+ UpdateMsgType msgType = getUpdateMsgType(edgeEvent.getAction());
+ AiModelUpdateMsg aiModelUpdateMsg = EdgeMsgConstructorUtils.constructAiModelUpdatedMsg(msgType, aiModel.get());
+ return DownlinkMsg.newBuilder()
+ .setDownlinkMsgId(EdgeUtils.nextPositiveInt())
+ .addAiModelUpdateMsg(aiModelUpdateMsg)
+ .build();
+ }
+ }
+ case DELETED -> {
+ AiModelUpdateMsg aiModelUpdateMsg = EdgeMsgConstructorUtils.constructAiModelDeleteMsg(aiModelId);
+ return DownlinkMsg.newBuilder()
+ .setDownlinkMsgId(EdgeUtils.nextPositiveInt())
+ .addAiModelUpdateMsg(aiModelUpdateMsg)
+ .build();
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public EdgeEventType getEdgeEventType() {
+ return EdgeEventType.AI_MODEL;
+ }
+
+ private void processAiModel(TenantId tenantId, AiModelId aiModelId, AiModelUpdateMsg aiModelUpdateMsg, Edge edge) {
+ Pair resultPair = super.saveOrUpdateAiModel(tenantId, aiModelId, aiModelUpdateMsg);
+ Boolean wasCreated = resultPair.getFirst();
+ if (wasCreated) {
+ pushAiModelCreatedEventToRuleEngine(tenantId, edge, aiModelId);
+ }
+ Boolean nameWasUpdated = resultPair.getSecond();
+ if (nameWasUpdated) {
+ saveEdgeEvent(tenantId, edge.getId(), EdgeEventType.AI_MODEL, EdgeEventActionType.UPDATED, aiModelId, null);
+ }
+ }
+
+ private void pushAiModelCreatedEventToRuleEngine(TenantId tenantId, Edge edge, AiModelId aiModelId) {
+ try {
+ Optional aiModel = edgeCtx.getAiModelService().findAiModelById(tenantId, aiModelId);
+ if (aiModel.isPresent()) {
+ String aiModelAsString = JacksonUtil.toString(aiModel.get());
+ TbMsgMetaData msgMetaData = getEdgeActionTbMsgMetaData(edge, edge.getCustomerId());
+ pushEntityEventToRuleEngine(tenantId, aiModelId, edge.getCustomerId(), TbMsgType.ENTITY_CREATED, aiModelAsString, msgMetaData);
+ } else {
+ log.warn("[{}][{}] Failed to find aiModel", tenantId, aiModelId);
+ }
+ } catch (Exception e) {
+ log.warn("[{}][{}] Failed to push aiModel action to rule engine: {}", tenantId, aiModelId, TbMsgType.ENTITY_CREATED.name(), e);
+ }
+ }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldDynamicArgumentsRefreshMsg.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/ai/AiModelProcessor.java
similarity index 55%
rename from application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldDynamicArgumentsRefreshMsg.java
rename to application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/ai/AiModelProcessor.java
index 301fe22dfb..f66421167a 100644
--- a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldDynamicArgumentsRefreshMsg.java
+++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/ai/AiModelProcessor.java
@@ -13,23 +13,16 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.thingsboard.server.actors.calculatedField;
+package org.thingsboard.server.service.edge.rpc.processor.ai;
-import lombok.Data;
-import org.thingsboard.server.common.data.id.CalculatedFieldId;
+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.common.msg.MsgType;
-import org.thingsboard.server.common.msg.ToCalculatedFieldSystemMsg;
+import org.thingsboard.server.gen.edge.v1.AiModelUpdateMsg;
+import org.thingsboard.server.service.edge.rpc.processor.EdgeProcessor;
-@Data
-public class CalculatedFieldDynamicArgumentsRefreshMsg implements ToCalculatedFieldSystemMsg {
+public interface AiModelProcessor extends EdgeProcessor {
- private final TenantId tenantId;
- private final CalculatedFieldId cfId;
-
- @Override
- public MsgType getMsgType() {
- return MsgType.CF_DYNAMIC_ARGUMENTS_REFRESH_MSG;
- }
+ ListenableFuture processAiModelMsgFromEdge(TenantId tenantId, Edge edge, AiModelUpdateMsg aiModelUpdateMsg);
}
diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/ai/BaseAiModelProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/ai/BaseAiModelProcessor.java
new file mode 100644
index 0000000000..cb1d27e0a1
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/ai/BaseAiModelProcessor.java
@@ -0,0 +1,81 @@
+/**
+ * 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.ai;
+
+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.ai.AiModel;
+import org.thingsboard.server.common.data.id.AiModelId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.dao.service.DataValidator;
+import org.thingsboard.server.gen.edge.v1.AiModelUpdateMsg;
+import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor;
+
+import java.util.Optional;
+
+@Slf4j
+public abstract class BaseAiModelProcessor extends BaseEdgeProcessor {
+
+ @Autowired
+ private DataValidator aiModelValidator;
+
+ protected Pair saveOrUpdateAiModel(TenantId tenantId, AiModelId aiModelId, AiModelUpdateMsg aiModelUpdateMsg) {
+ boolean isCreated = false;
+ boolean isNameUpdated = false;
+ try {
+ AiModel aiModel = JacksonUtil.fromString(aiModelUpdateMsg.getEntity(), AiModel.class, true);
+ if (aiModel == null) {
+ throw new RuntimeException("[{" + tenantId + "}] aiModelUpdateMsg {" + aiModelUpdateMsg + " } cannot be converted to aiModel");
+ }
+
+ Optional aiModelById = edgeCtx.getAiModelService().findAiModelById(tenantId, aiModelId);
+ if (aiModelById.isEmpty()) {
+ aiModel.setCreatedTime(Uuids.unixTimestamp(aiModelId.getId()));
+ isCreated = true;
+ aiModel.setId(null);
+ } else {
+ aiModel.setId(aiModelId);
+ }
+
+ String aiModelName = aiModel.getName();
+ Optional aiModelByName = edgeCtx.getAiModelService().findAiModelByTenantIdAndName(aiModel.getTenantId(), aiModelName);
+ if (aiModelByName.isPresent() && !aiModelByName.get().getId().equals(aiModelId)) {
+ aiModelName = aiModelName + "_" + StringUtils.randomAlphabetic(15);
+ log.warn("[{}] aiModel with name {} already exists. Renaming aiModel name to {}",
+ tenantId, aiModel.getName(), aiModelByName.get().getName());
+ isNameUpdated = true;
+ }
+ aiModel.setName(aiModelName);
+
+ aiModelValidator.validate(aiModel, AiModel::getTenantId);
+
+ if (isCreated) {
+ aiModel.setId(aiModelId);
+ }
+
+ edgeCtx.getAiModelService().save(aiModel, false);
+ } catch (Exception e) {
+ log.error("[{}] Failed to process aiModel update msg [{}]", tenantId, aiModelUpdateMsg, e);
+ throw e;
+ }
+ return Pair.of(isCreated, isNameUpdated);
+ }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/asset/DefaultTbAssetService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/asset/DefaultTbAssetService.java
index 3f69d00276..ef630c2df4 100644
--- a/application/src/main/java/org/thingsboard/server/service/entitiy/asset/DefaultTbAssetService.java
+++ b/application/src/main/java/org/thingsboard/server/service/entitiy/asset/DefaultTbAssetService.java
@@ -20,6 +20,7 @@ import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.EntityType;
+import org.thingsboard.server.common.data.NameConflictStrategy;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.asset.Asset;
import org.thingsboard.server.common.data.audit.ActionType;
@@ -40,10 +41,15 @@ public class DefaultTbAssetService extends AbstractTbEntityService implements Tb
@Override
public Asset save(Asset asset, User user) throws Exception {
+ return save(asset, NameConflictStrategy.DEFAULT, user);
+ }
+
+ @Override
+ public Asset save(Asset asset, NameConflictStrategy nameConflictStrategy, User user) throws Exception {
ActionType actionType = asset.getId() == null ? ActionType.ADDED : ActionType.UPDATED;
TenantId tenantId = asset.getTenantId();
try {
- Asset savedAsset = checkNotNull(assetService.saveAsset(asset));
+ Asset savedAsset = checkNotNull(assetService.saveAsset(asset, nameConflictStrategy));
autoCommit(user, savedAsset.getId());
logEntityActionService.logEntityAction(tenantId, savedAsset.getId(), savedAsset, asset.getCustomerId(),
actionType, user);
diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/asset/TbAssetService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/asset/TbAssetService.java
index a2af8ffcdc..42fce5213e 100644
--- a/application/src/main/java/org/thingsboard/server/service/entitiy/asset/TbAssetService.java
+++ b/application/src/main/java/org/thingsboard/server/service/entitiy/asset/TbAssetService.java
@@ -16,6 +16,7 @@
package org.thingsboard.server.service.entitiy.asset;
import org.thingsboard.server.common.data.Customer;
+import org.thingsboard.server.common.data.NameConflictStrategy;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.asset.Asset;
import org.thingsboard.server.common.data.edge.Edge;
@@ -27,6 +28,8 @@ public interface TbAssetService {
Asset save(Asset asset, User user) throws Exception;
+ Asset save(Asset asset, NameConflictStrategy nameConflictStrategy, User user) throws Exception;
+
void delete(Asset asset, User user);
Asset assignAssetToCustomer(TenantId tenantId, AssetId assetId, Customer customer, User user) throws ThingsboardException;
diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/customer/DefaultTbCustomerService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/customer/DefaultTbCustomerService.java
index c070aea087..a2a0e6846d 100644
--- a/application/src/main/java/org/thingsboard/server/service/entitiy/customer/DefaultTbCustomerService.java
+++ b/application/src/main/java/org/thingsboard/server/service/entitiy/customer/DefaultTbCustomerService.java
@@ -19,6 +19,7 @@ import lombok.AllArgsConstructor;
import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.EntityType;
+import org.thingsboard.server.common.data.NameConflictStrategy;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.id.CustomerId;
@@ -32,10 +33,15 @@ public class DefaultTbCustomerService extends AbstractTbEntityService implements
@Override
public Customer save(Customer customer, SecurityUser user) throws Exception {
+ return save(customer, NameConflictStrategy.DEFAULT, user);
+ }
+
+ @Override
+ public Customer save(Customer customer, NameConflictStrategy nameConflictStrategy, SecurityUser user) throws Exception {
ActionType actionType = customer.getId() == null ? ActionType.ADDED : ActionType.UPDATED;
TenantId tenantId = customer.getTenantId();
try {
- Customer savedCustomer = checkNotNull(customerService.saveCustomer(customer));
+ Customer savedCustomer = checkNotNull(customerService.saveCustomer(customer, nameConflictStrategy));
autoCommit(user, savedCustomer.getId());
logEntityActionService.logEntityAction(tenantId, savedCustomer.getId(), savedCustomer, null, actionType, user);
return savedCustomer;
diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/customer/TbCustomerService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/customer/TbCustomerService.java
index f77e990620..5884ae2e48 100644
--- a/application/src/main/java/org/thingsboard/server/service/entitiy/customer/TbCustomerService.java
+++ b/application/src/main/java/org/thingsboard/server/service/entitiy/customer/TbCustomerService.java
@@ -16,8 +16,12 @@
package org.thingsboard.server.service.entitiy.customer;
import org.thingsboard.server.common.data.Customer;
+import org.thingsboard.server.common.data.NameConflictStrategy;
import org.thingsboard.server.service.entitiy.SimpleTbEntityService;
+import org.thingsboard.server.service.security.model.SecurityUser;
public interface TbCustomerService extends SimpleTbEntityService {
+ Customer save(Customer customer, NameConflictStrategy nameConflictStrategy, SecurityUser user) throws Exception;
+
}
diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/device/DefaultTbDeviceService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/device/DefaultTbDeviceService.java
index f182894239..e982adbdf1 100644
--- a/application/src/main/java/org/thingsboard/server/service/entitiy/device/DefaultTbDeviceService.java
+++ b/application/src/main/java/org/thingsboard/server/service/entitiy/device/DefaultTbDeviceService.java
@@ -25,6 +25,7 @@ import org.springframework.transaction.annotation.Transactional;
import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.EntityType;
+import org.thingsboard.server.common.data.NameConflictStrategy;
import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.audit.ActionType;
@@ -56,10 +57,15 @@ public class DefaultTbDeviceService extends AbstractTbEntityService implements T
@Override
public Device save(Device device, String accessToken, User user) throws Exception {
+ return save(device, accessToken, NameConflictStrategy.DEFAULT, user);
+ }
+
+ @Override
+ public Device save(Device device, String accessToken, NameConflictStrategy nameConflictStrategy, User user) throws Exception {
ActionType actionType = device.getId() == null ? ActionType.ADDED : ActionType.UPDATED;
TenantId tenantId = device.getTenantId();
try {
- Device savedDevice = checkNotNull(deviceService.saveDeviceWithAccessToken(device, accessToken));
+ Device savedDevice = checkNotNull(deviceService.saveDeviceWithAccessToken(device, accessToken, nameConflictStrategy));
autoCommit(user, savedDevice.getId());
logEntityActionService.logEntityAction(tenantId, savedDevice.getId(), savedDevice, savedDevice.getCustomerId(),
actionType, user);
@@ -73,10 +79,15 @@ public class DefaultTbDeviceService extends AbstractTbEntityService implements T
@Override
public Device saveDeviceWithCredentials(Device device, DeviceCredentials credentials, User user) throws ThingsboardException {
+ return saveDeviceWithCredentials(device, credentials, NameConflictStrategy.DEFAULT, user);
+ }
+
+ @Override
+ public Device saveDeviceWithCredentials(Device device, DeviceCredentials credentials, NameConflictStrategy nameConflictStrategy, User user) throws ThingsboardException {
ActionType actionType = device.getId() == null ? ActionType.ADDED : ActionType.UPDATED;
TenantId tenantId = device.getTenantId();
try {
- Device savedDevice = checkNotNull(deviceService.saveDeviceWithCredentials(device, credentials));
+ Device savedDevice = checkNotNull(deviceService.saveDeviceWithCredentials(device, credentials, nameConflictStrategy));
logEntityActionService.logEntityAction(tenantId, savedDevice.getId(), savedDevice, savedDevice.getCustomerId(),
actionType, user);
diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/device/TbDeviceService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/device/TbDeviceService.java
index c234b3b597..e217d8de7b 100644
--- a/application/src/main/java/org/thingsboard/server/service/entitiy/device/TbDeviceService.java
+++ b/application/src/main/java/org/thingsboard/server/service/entitiy/device/TbDeviceService.java
@@ -18,6 +18,7 @@ package org.thingsboard.server.service.entitiy.device;
import com.google.common.util.concurrent.ListenableFuture;
import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.Device;
+import org.thingsboard.server.common.data.NameConflictStrategy;
import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.edge.Edge;
@@ -33,8 +34,12 @@ public interface TbDeviceService {
Device save(Device device, String accessToken, User user) throws Exception;
+ Device save(Device device, String accessToken, NameConflictStrategy nameConflictStrategy, User user) throws Exception;
+
Device saveDeviceWithCredentials(Device device, DeviceCredentials deviceCredentials, User user) throws ThingsboardException;
+ Device saveDeviceWithCredentials(Device device, DeviceCredentials deviceCredentials, NameConflictStrategy nameConflictStrategy, User user) throws ThingsboardException;
+
void delete(Device device, User user);
Device assignDeviceToCustomer(TenantId tenantId, DeviceId deviceId, Customer customer, User user) throws ThingsboardException;
diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/entityview/DefaultTbEntityViewService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/entityview/DefaultTbEntityViewService.java
index 8fb99bfb0e..54699e0995 100644
--- a/application/src/main/java/org/thingsboard/server/service/entitiy/entityview/DefaultTbEntityViewService.java
+++ b/application/src/main/java/org/thingsboard/server/service/entitiy/entityview/DefaultTbEntityViewService.java
@@ -33,6 +33,7 @@ import org.thingsboard.server.common.data.AttributeScope;
import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.EntityView;
+import org.thingsboard.server.common.data.NameConflictStrategy;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.edge.Edge;
@@ -80,10 +81,15 @@ public class DefaultTbEntityViewService extends AbstractTbEntityService implemen
@Override
public EntityView save(EntityView entityView, EntityView existingEntityView, User user) throws Exception {
+ return save(entityView, existingEntityView, NameConflictStrategy.DEFAULT, user);
+ }
+
+ @Override
+ public EntityView save(EntityView entityView, EntityView existingEntityView, NameConflictStrategy nameConflictStrategy, User user) throws Exception {
ActionType actionType = entityView.getId() == null ? ActionType.ADDED : ActionType.UPDATED;
TenantId tenantId = entityView.getTenantId();
try {
- EntityView savedEntityView = checkNotNull(entityViewService.saveEntityView(entityView));
+ EntityView savedEntityView = checkNotNull(entityViewService.saveEntityView(entityView, nameConflictStrategy));
this.updateEntityViewAttributes(tenantId, savedEntityView, existingEntityView, user);
autoCommit(user, savedEntityView.getId());
logEntityActionService.logEntityAction(savedEntityView.getTenantId(), savedEntityView.getId(), savedEntityView,
diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/entityview/TbEntityViewService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/entityview/TbEntityViewService.java
index 3aec924f75..bb4bcf9cad 100644
--- a/application/src/main/java/org/thingsboard/server/service/entitiy/entityview/TbEntityViewService.java
+++ b/application/src/main/java/org/thingsboard/server/service/entitiy/entityview/TbEntityViewService.java
@@ -18,6 +18,7 @@ package org.thingsboard.server.service.entitiy.entityview;
import com.google.common.util.concurrent.ListenableFuture;
import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.EntityView;
+import org.thingsboard.server.common.data.NameConflictStrategy;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.common.data.exception.ThingsboardException;
@@ -33,6 +34,8 @@ public interface TbEntityViewService extends ComponentLifecycleListener {
EntityView save(EntityView entityView, EntityView existingEntityView, User user) throws Exception;
+ EntityView save(EntityView entityView, EntityView existingEntityView, NameConflictStrategy nameConflictStrategy, User user) throws Exception;
+
void updateEntityViewAttributes(TenantId tenantId, EntityView savedEntityView, EntityView oldEntityView, User user) throws ThingsboardException;
void delete(EntityView entity, User user) throws ThingsboardException;
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 f41a530630..f48f4621ef 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.2.0");
+ private static final List SUPPORTED_VERSIONS_FOR_UPGRADE = List.of("4.2.1");
private final ProjectInfo projectInfo;
private final JdbcTemplate jdbcTemplate;
diff --git a/application/src/main/java/org/thingsboard/server/service/query/DefaultEntityQueryService.java b/application/src/main/java/org/thingsboard/server/service/query/DefaultEntityQueryService.java
index 45a73072f5..f39769526a 100644
--- a/application/src/main/java/org/thingsboard/server/service/query/DefaultEntityQueryService.java
+++ b/application/src/main/java/org/thingsboard/server/service/query/DefaultEntityQueryService.java
@@ -238,7 +238,7 @@ public class DefaultEntityQueryService implements EntityQueryService {
entitiesSortOrder = sortOrder;
}
EntityDataPageLink edpl = new EntityDataPageLink(maxEntitiesPerAlarmSubscription, 0, null, entitiesSortOrder);
- return new EntityDataQuery(query.getEntityFilter(), edpl, null, null, query.getKeyFilters());
+ return new EntityDataQuery(query.getEntityFilter(), edpl, query.getEntityFields(), query.getLatestValues(), query.getKeyFilters());
}
@Override
diff --git a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCalculatedFieldConsumerService.java b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCalculatedFieldConsumerService.java
index 8d4ab25578..acb36449e8 100644
--- a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCalculatedFieldConsumerService.java
+++ b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCalculatedFieldConsumerService.java
@@ -35,6 +35,7 @@ import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg;
import org.thingsboard.server.common.msg.queue.ServiceType;
import org.thingsboard.server.common.msg.queue.TbCallback;
import org.thingsboard.server.common.msg.queue.TopicPartitionInfo;
+import org.thingsboard.server.dao.resource.TbResourceDataCache;
import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
import org.thingsboard.server.gen.transport.TransportProtos.CalculatedFieldLinkedTelemetryMsgProto;
import org.thingsboard.server.gen.transport.TransportProtos.CalculatedFieldTelemetryMsgProto;
@@ -83,6 +84,7 @@ public class DefaultTbCalculatedFieldConsumerService extends AbstractPartitionBa
ActorSystemContext actorContext,
TbDeviceProfileCache deviceProfileCache,
TbAssetProfileCache assetProfileCache,
+ TbResourceDataCache tbResourceDataCache,
TbTenantProfileCache tenantProfileCache,
TbApiUsageStateService apiUsageStateService,
PartitionService partitionService,
@@ -90,7 +92,7 @@ public class DefaultTbCalculatedFieldConsumerService extends AbstractPartitionBa
JwtSettingsService jwtSettingsService,
CalculatedFieldCache calculatedFieldCache,
CalculatedFieldStateService stateService) {
- super(actorContext, tenantProfileCache, deviceProfileCache, assetProfileCache, calculatedFieldCache, apiUsageStateService, partitionService,
+ super(actorContext, tenantProfileCache, deviceProfileCache, assetProfileCache, tbResourceDataCache, calculatedFieldCache, apiUsageStateService, partitionService,
eventPublisher, jwtSettingsService);
this.queueFactory = tbQueueFactory;
this.stateService = stateService;
diff --git a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java
index 2d5f43420f..385eab1dec 100644
--- a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java
+++ b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java
@@ -48,6 +48,7 @@ import org.thingsboard.server.common.data.id.DeviceProfileId;
import org.thingsboard.server.common.data.id.EdgeId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.RuleChainId;
+import org.thingsboard.server.common.data.id.TbResourceId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.msg.TbMsgType;
import org.thingsboard.server.common.data.page.PageData;
@@ -435,8 +436,9 @@ public class DefaultTbClusterService implements TbClusterService {
@Override
public void onResourceChange(TbResourceInfo resource, TbQueueCallback callback) {
+ TenantId tenantId = resource.getTenantId();
+ TbResourceId resourceId = resource.getId();
if (resource.getResourceType() == ResourceType.LWM2M_MODEL) {
- TenantId tenantId = resource.getTenantId();
log.trace("[{}][{}][{}] Processing change resource", tenantId, resource.getResourceType(), resource.getResourceKey());
ResourceUpdateMsg resourceUpdateMsg = ResourceUpdateMsg.newBuilder()
.setTenantIdMSB(tenantId.getId().getMostSignificantBits())
@@ -447,6 +449,7 @@ public class DefaultTbClusterService implements TbClusterService {
ToTransportMsg transportMsg = ToTransportMsg.newBuilder().setResourceUpdateMsg(resourceUpdateMsg).build();
broadcast(transportMsg, DataConstants.LWM2M_TRANSPORT_NAME, callback);
}
+ broadcastEntityStateChangeEvent(tenantId, resourceId, ComponentLifecycleEvent.UPDATED);
}
@Override
@@ -462,6 +465,7 @@ public class DefaultTbClusterService implements TbClusterService {
ToTransportMsg transportMsg = ToTransportMsg.newBuilder().setResourceDeleteMsg(resourceDeleteMsg).build();
broadcast(transportMsg, DataConstants.LWM2M_TRANSPORT_NAME, callback);
}
+ broadcastEntityStateChangeEvent(resource.getTenantId(), resource.getId(), ComponentLifecycleEvent.DELETED);
}
private void broadcastEntityChangeToTransport(TenantId tenantId, EntityId entityid, T entity, TbQueueCallback callback) {
@@ -593,6 +597,7 @@ public class DefaultTbClusterService implements TbClusterService {
EntityType.DEVICE_PROFILE,
EntityType.ASSET_PROFILE,
EntityType.JOB,
+ EntityType.TB_RESOURCE,
EntityType.USER)
|| (entityType == EntityType.ASSET && msg.getEvent() == ComponentLifecycleEvent.UPDATED)
|| (entityType == EntityType.DEVICE && msg.getEvent() == ComponentLifecycleEvent.UPDATED)
diff --git a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java
index f0b1a4d7d2..9ab5a062eb 100644
--- a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java
+++ b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java
@@ -55,6 +55,7 @@ import org.thingsboard.server.common.stats.StatsFactory;
import org.thingsboard.server.common.util.KvProtoUtil;
import org.thingsboard.server.common.util.ProtoUtils;
import org.thingsboard.server.dao.resource.ImageCacheKey;
+import org.thingsboard.server.dao.resource.TbResourceDataCache;
import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.gen.transport.TransportProtos.DeviceStateServiceMsgProto;
@@ -176,10 +177,11 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService dataEntry.getKey().getType() == kvType &&
StringUtils.isNotEmpty(dataEntry.getKey().getKey()))
- .forEach(dataEntry -> kvs.add(dataEntry.getKey().getKey(), dataEntry.getValue().toJsonPrimitive()));
+ .forEach(dataEntry -> {
+ ParsedValue value = dataEntry.getValue();
+ JsonElement kvValue = (value.getDataType() == DataType.JSON)
+ ? (JsonElement) value.getValue()
+ : value.toJsonPrimitive();
+ kvs.add(dataEntry.getKey().getKey(), kvValue);
+ });
return Map.entry(kvType, kvs);
})
.filter(kvsEntry -> kvsEntry.getValue().entrySet().size() > 0)
diff --git a/application/src/main/java/org/thingsboard/server/service/user/cache/DefaultUserAuthDetailsCache.java b/application/src/main/java/org/thingsboard/server/service/user/cache/DefaultUserAuthDetailsCache.java
index 21c9c9069d..33730c7c92 100644
--- a/application/src/main/java/org/thingsboard/server/service/user/cache/DefaultUserAuthDetailsCache.java
+++ b/application/src/main/java/org/thingsboard/server/service/user/cache/DefaultUserAuthDetailsCache.java
@@ -41,9 +41,9 @@ public class DefaultUserAuthDetailsCache implements UserAuthDetailsCache {
private final UserService userService;
- @Value("${cache.userEnabled.maxSize:1000}")
+ @Value("${cache.userAuthDetails.maxSize:1000}")
private int cacheMaxSize;
- @Value("${cache.userEnabled.timeToLiveInMinutes:30}")
+ @Value("${cache.userAuthDetails.timeToLiveInMinutes:30}")
private int cacheValueTtl;
private Cache cache;
@@ -70,7 +70,7 @@ public class DefaultUserAuthDetailsCache implements UserAuthDetailsCache {
public UserAuthDetails findUserEnabled(TenantId tenantId, UserId userId) {
lock.readLock().lock();
try {
- log.trace("Retrieving user with enabled credentials status with id {} for tenant {} from cache", userId, tenantId);
+ log.trace("Retrieving user with enabled credentials status for id {} for tenant {} from cache", userId, tenantId);
return cache.get(userId, id -> userService.findUserAuthDetailsByUserId(tenantId, id));
} finally {
lock.readLock().unlock();
diff --git a/application/src/main/java/org/thingsboard/server/utils/CsvUtils.java b/application/src/main/java/org/thingsboard/server/utils/CsvUtils.java
index 3f75a4918f..0fde72a3b0 100644
--- a/application/src/main/java/org/thingsboard/server/utils/CsvUtils.java
+++ b/application/src/main/java/org/thingsboard/server/utils/CsvUtils.java
@@ -17,10 +17,15 @@ package org.thingsboard.server.utils;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
+import lombok.SneakyThrows;
import org.apache.commons.csv.CSVFormat;
+import org.apache.commons.csv.CSVPrinter;
import org.apache.commons.csv.CSVRecord;
import org.apache.commons.io.input.CharSequenceReader;
+import java.io.ByteArrayOutputStream;
+import java.io.OutputStreamWriter;
+import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@@ -43,4 +48,18 @@ public class CsvUtils {
.collect(Collectors.toList());
}
+ @SneakyThrows
+ public static byte[] generateCsv(List> rows) {
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ try (OutputStreamWriter writer = new OutputStreamWriter(out, StandardCharsets.UTF_8);
+ CSVPrinter csvPrinter = new CSVPrinter(writer, CSVFormat.DEFAULT)) {
+
+ for (List row : rows) {
+ csvPrinter.printRecord(row);
+ }
+ csvPrinter.flush();
+ }
+ return out.toByteArray();
+ }
+
}
diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml
index 8884a66cc8..0fcb94344e 100644
--- a/application/src/main/resources/thingsboard.yml
+++ b/application/src/main/resources/thingsboard.yml
@@ -312,8 +312,11 @@ cassandra:
default_fetch_size: "${CASSANDRA_DEFAULT_FETCH_SIZE:2000}"
# Specify partitioning size for timestamp key-value storage. Example: MINUTES, HOURS, DAYS, MONTHS, INDEFINITE
ts_key_value_partitioning: "${TS_KV_PARTITIONING:MONTHS}"
- # Enable/Disable timestamp key-value partioning on read queries
+ # Enable/Disable timestamp key-value partitioning on read queries
use_ts_key_value_partitioning_on_read: "${USE_TS_KV_PARTITIONING_ON_READ:true}"
+ # Safety trigger to fall back to use_ts_key_value_partitioning_on_read as true if estimated partitions count is greater than safety trigger value.
+ # It helps to prevent building huge partition list (OOM) for corner cases (like from 0 to infinity) and prefer fewer reads strategy from NoSQL database
+ use_ts_key_value_partitioning_on_read_max_estimated_partition_count: "${USE_TS_KV_PARTITIONING_ON_READ_MAX_ESTIMATED_PARTITION_COUNT:40}"
# The number of partitions that are cached in memory of each service. It is useful to decrease the load of re-inserting the same partitions again
ts_key_value_partitions_max_cache_size: "${TS_KV_PARTITIONS_MAX_CACHE_SIZE:100000}"
# Timeseries Time To Live (in seconds) for Cassandra Record. 0 - record has never expired
@@ -691,9 +694,12 @@ cache:
maxSize: "${CACHE_SPECS_IMAGE_ETAGS_MAX_SIZE:10000}" # 0 means the cache is disabled
systemImagesBrowserTtlInMinutes: "${CACHE_SPECS_IMAGE_SYSTEM_BROWSER_TTL:0}" # Browser cache TTL for system images in minutes. 0 means the cache is disabled
tenantImagesBrowserTtlInMinutes: "${CACHE_SPECS_IMAGE_TENANT_BROWSER_TTL:0}" # Browser cache TTL for tenant images in minutes. 0 means the cache is disabled
- userEnabled:
- timeToLiveInMinutes: "${CACHE_SPECS_USER_ENABLED_TTL:120}" # User enabled cache TTL
- maxSize: "${CACHE_SPECS_USER_ENABLED_MAX_SIZE:200000}" # 0 means the cache is disabled
+ tbResourceData:
+ timeToLiveInMinutes: "${CACHE_SPECS_RESOURCE_DATA_TTL:10080}" # TB resource data cache TTL
+ maxSize: "${CACHE_SPECS_RESOURCE_DATA_MAX_SIZE:100000}" # 0 means the cache is disabled
+ userAuthDetails:
+ timeToLiveInMinutes: "${CACHE_SPECS_USER_AUTH_DETAILS_TTL:120}" # User auth details cache TTL
+ maxSize: "${CACHE_SPECS_USER_AUTH_DETAILS_MAX_SIZE:200000}" # 0 means the cache is disabled
# Spring data parameters
spring.data.redis.repositories.enabled: false # Disable this because it is not required.
@@ -1557,7 +1563,7 @@ swagger:
version: "${SWAGGER_VERSION:}"
# The group name (definition) on the API doc UI page.
group_name: "${SWAGGER_GROUP_NAME:thingsboard}"
- # Control the initial display state of API operations and tags (none, list or full)
+ # Control the initial display state of API operations and tags (none or list)
doc_expansion: "${SWAGGER_DOC_EXPANSION:list}"
# Queue configuration parameters
diff --git a/application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java b/application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java
index b6a1faa1ed..232bcf9445 100644
--- a/application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java
+++ b/application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java
@@ -36,7 +36,7 @@ import org.thingsboard.server.common.data.cf.configuration.CalculatedFieldConfig
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.RelationQueryDynamicSourceConfiguration;
+import org.thingsboard.server.common.data.cf.configuration.RelationPathQueryDynamicSourceConfiguration;
import org.thingsboard.server.common.data.cf.configuration.ScriptCalculatedFieldConfiguration;
import org.thingsboard.server.common.data.cf.configuration.SimpleCalculatedFieldConfiguration;
import org.thingsboard.server.common.data.cf.configuration.geofencing.EntityCoordinates;
@@ -47,10 +47,12 @@ import org.thingsboard.server.common.data.id.AssetProfileId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.relation.EntityRelation;
import org.thingsboard.server.common.data.relation.EntitySearchDirection;
+import org.thingsboard.server.common.data.relation.RelationPathLevel;
import org.thingsboard.server.controller.CalculatedFieldControllerTest;
import org.thingsboard.server.dao.service.DaoSqlTest;
import java.util.HashMap;
+import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
@@ -621,6 +623,59 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes
});
}
+ @Test
+ public void testSimpleCalculatedFieldWhenCtxBecameUninitialized() throws Exception {
+ Device testDevice = createDevice("Test device", "1234567890");
+
+ CalculatedField calculatedField = new CalculatedField();
+ calculatedField.setEntityId(testDevice.getId());
+ calculatedField.setType(CalculatedFieldType.SIMPLE);
+ calculatedField.setName("M + 1");
+ calculatedField.setDebugSettings(DebugSettings.all());
+
+ SimpleCalculatedFieldConfiguration config = new SimpleCalculatedFieldConfiguration();
+
+ Argument argument = new Argument();
+ ReferencedEntityKey refEntityKey = new ReferencedEntityKey("m", ArgumentType.TS_LATEST, null);
+ argument.setRefEntityKey(refEntityKey);
+ config.setArguments(Map.of("m", argument));
+ config.setExpression("m + 1");
+
+ Output output = new Output();
+ output.setName("m1");
+ output.setType(OutputType.TIME_SERIES);
+ output.setDecimalsByDefault(0);
+ config.setOutput(output);
+
+ calculatedField.setConfiguration(config);
+
+ calculatedField = doPost("/api/calculatedField", calculatedField, CalculatedField.class);
+
+ doPost("/api/plugins/telemetry/DEVICE/" + testDevice.getUuidId() + "/timeseries/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode("{\"m\":1}"));
+
+ await().alias("create CF -> ctx is initialized -> perform calculation").atMost(TIMEOUT, TimeUnit.SECONDS)
+ .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS)
+ .untilAsserted(() -> {
+ ObjectNode m1 = getLatestTelemetry(testDevice.getId(), "m1");
+ assertThat(m1).isNotNull();
+ assertThat(m1.get("m1").get(0).get("value").asText()).isEqualTo("2");
+ });
+
+ config.setExpression("m m");
+ calculatedField.setConfiguration(config);
+ calculatedField = doPost("/api/calculatedField", calculatedField, CalculatedField.class);
+
+ doPost("/api/plugins/telemetry/DEVICE/" + testDevice.getUuidId() + "/timeseries/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode("{\"m\":2}"));
+
+ await().alias("update CF -> ctx is not initialized -> no calculation performed").atMost(TIMEOUT, TimeUnit.SECONDS)
+ .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS)
+ .untilAsserted(() -> {
+ ObjectNode m1 = getLatestTelemetry(testDevice.getId(), "m1");
+ assertThat(m1).isNotNull();
+ assertThat(m1.get("m1").get(0).get("value").asText()).isEqualTo("2");
+ });
+ }
+
@Test
public void testGeofencingCalculatedField_withZonesCreatedOnDevice() throws Exception {
// --- Arrange entities ---
@@ -760,19 +815,13 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes
// Zone groups: ATTRIBUTE on specific assets (one zone per group)
ZoneGroupConfiguration allowedZonesGroup = new ZoneGroupConfiguration("zone", REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS, false);
- var allowedZoneDynamicSourceConfiguration = new RelationQueryDynamicSourceConfiguration();
- allowedZoneDynamicSourceConfiguration.setDirection(EntitySearchDirection.FROM);
- allowedZoneDynamicSourceConfiguration.setRelationType("AllowedZone");
- allowedZoneDynamicSourceConfiguration.setMaxLevel(1);
- allowedZoneDynamicSourceConfiguration.setFetchLastLevelOnly(true);
+ var allowedZoneDynamicSourceConfiguration = new RelationPathQueryDynamicSourceConfiguration();
+ allowedZoneDynamicSourceConfiguration.setLevels(List.of(new RelationPathLevel(EntitySearchDirection.FROM, "AllowedZone")));
allowedZonesGroup.setRefDynamicSourceConfiguration(allowedZoneDynamicSourceConfiguration);
ZoneGroupConfiguration restrictedZonesGroup = new ZoneGroupConfiguration("zone", REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS, false);
- var restrictedZoneDynamicSourceConfiguration = new RelationQueryDynamicSourceConfiguration();
- restrictedZoneDynamicSourceConfiguration.setDirection(EntitySearchDirection.FROM);
- restrictedZoneDynamicSourceConfiguration.setRelationType("RestrictedZone");
- restrictedZoneDynamicSourceConfiguration.setMaxLevel(1);
- restrictedZoneDynamicSourceConfiguration.setFetchLastLevelOnly(true);
+ var restrictedZoneDynamicSourceConfiguration = new RelationPathQueryDynamicSourceConfiguration();
+ restrictedZoneDynamicSourceConfiguration.setLevels(List.of(new RelationPathLevel(EntitySearchDirection.FROM, "RestrictedZone")));
restrictedZonesGroup.setRefDynamicSourceConfiguration(restrictedZoneDynamicSourceConfiguration);
cfg.setZoneGroups(Map.of("allowedZones", allowedZonesGroup, "restrictedZones", restrictedZonesGroup));
@@ -831,10 +880,11 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes
TenantProfile foundTenantProfile = doGet("/api/tenantProfile/" + tenantProfileEntityInfo.getId().getId().toString(), TenantProfile.class);
assertThat(foundTenantProfile).isNotNull();
assertThat(foundTenantProfile.getDefaultProfileConfiguration()).isNotNull();
- foundTenantProfile.getDefaultProfileConfiguration().setMinAllowedScheduledUpdateIntervalInSecForCF(TIMEOUT / 10);
+ int minAllowedScheduledUpdateIntervalInSecForCF = TIMEOUT / 10;
+ foundTenantProfile.getDefaultProfileConfiguration().setMinAllowedScheduledUpdateIntervalInSecForCF(minAllowedScheduledUpdateIntervalInSecForCF);
TenantProfile savedTenantProfile = doPost("/api/tenantProfile", foundTenantProfile, TenantProfile.class);
assertThat(savedTenantProfile).isNotNull();
- assertThat(savedTenantProfile.getDefaultProfileConfiguration().getMinAllowedScheduledUpdateIntervalInSecForCF()).isEqualTo(TIMEOUT / 10);
+ assertThat(savedTenantProfile.getDefaultProfileConfiguration().getMinAllowedScheduledUpdateIntervalInSecForCF()).isEqualTo(minAllowedScheduledUpdateIntervalInSecForCF);
loginTenantAdmin();
// --- Arrange entities ---
@@ -869,11 +919,8 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes
cfg.setEntityCoordinates(new EntityCoordinates(ENTITY_ID_LATITUDE_ARGUMENT_KEY, ENTITY_ID_LONGITUDE_ARGUMENT_KEY));
var allowedZonesGroup = new ZoneGroupConfiguration("zone", REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS, false);
- var allowedZoneDynamicSourceConfiguration = new RelationQueryDynamicSourceConfiguration();
- allowedZoneDynamicSourceConfiguration.setDirection(EntitySearchDirection.FROM);
- allowedZoneDynamicSourceConfiguration.setRelationType("AllowedZone");
- allowedZoneDynamicSourceConfiguration.setMaxLevel(1);
- allowedZoneDynamicSourceConfiguration.setFetchLastLevelOnly(true);
+ var allowedZoneDynamicSourceConfiguration = new RelationPathQueryDynamicSourceConfiguration();
+ allowedZoneDynamicSourceConfiguration.setLevels(List.of(new RelationPathLevel(EntitySearchDirection.FROM, "AllowedZone")));
allowedZonesGroup.setRefDynamicSourceConfiguration(allowedZoneDynamicSourceConfiguration);
cfg.setZoneGroups(Map.of("allowedZones", allowedZonesGroup));
@@ -884,7 +931,8 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes
cfg.setOutput(out);
// Enable scheduled refresh with a 6-second interval
- cfg.setScheduledUpdateInterval(6);
+ cfg.setScheduledUpdateInterval(minAllowedScheduledUpdateIntervalInSecForCF);
+ cfg.setScheduledUpdateEnabled(true);
cf.setConfiguration(cfg);
CalculatedField savedCalculatedField = doPost("/api/calculatedField", cf, CalculatedField.class);
@@ -935,7 +983,7 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes
relAllowedB.setType("AllowedZone");
doPost("/api/relation", relAllowedB).andExpect(status().isOk());
- awaitForCalculatedFieldEntityMessageProcessorToRegisterCfStateAsDirty(device.getId(), savedCalculatedField.getId());
+ awaitForCalculatedFieldEntityMessageProcessorToRegisterCfStateAsReadyToRefreshDynamicArguments(device.getId(), savedCalculatedField.getId(), minAllowedScheduledUpdateIntervalInSecForCF);
// --- Same coordinates as before, but now we expect ENTERED since a new zone is registered ---
doPost("/api/plugins/telemetry/DEVICE/" + device.getUuidId() + "/timeseries/unusedScope",
diff --git a/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java b/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java
index 6da301062c..f632478c38 100644
--- a/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java
+++ b/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java
@@ -155,6 +155,7 @@ import org.thingsboard.server.dao.timeseries.TimeseriesService;
import org.thingsboard.server.queue.memory.InMemoryStorage;
import org.thingsboard.server.service.cf.CfRocksDb;
import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldState;
+import org.thingsboard.server.service.cf.ctx.state.geofencing.GeofencingCalculatedFieldState;
import org.thingsboard.server.service.entitiy.tenant.profile.TbTenantProfileService;
import org.thingsboard.server.service.security.auth.jwt.RefreshTokenRequest;
import org.thingsboard.server.service.security.auth.rest.LoginRequest;
@@ -1155,14 +1156,15 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest {
});
}
- protected void awaitForCalculatedFieldEntityMessageProcessorToRegisterCfStateAsDirty(EntityId entityId, CalculatedFieldId cfId) {
+ protected void awaitForCalculatedFieldEntityMessageProcessorToRegisterCfStateAsReadyToRefreshDynamicArguments(EntityId entityId, CalculatedFieldId cfId, int scheduledUpdateInterval) {
CalculatedFieldEntityMessageProcessor processor = getCalculatedFieldEntityMessageProcessor(entityId);
Map statesMap = (Map) ReflectionTestUtils.getField(processor, "states");
- Awaitility.await("CF state for entity actor marked as dirty").atMost(5, TimeUnit.SECONDS).until(() -> {
+ Awaitility.await("CF state for entity actor ready to refresh dynamic arguments").atMost(TIMEOUT, TimeUnit.SECONDS).until(() -> {
CalculatedFieldState calculatedFieldState = statesMap.get(cfId);
- boolean stateDirty = calculatedFieldState != null && calculatedFieldState.isDirty();
- log.warn("entityId {}, cfId {}, state dirty == {}", entityId, cfId, stateDirty);
- return stateDirty;
+ boolean isReady = calculatedFieldState != null && ((GeofencingCalculatedFieldState) calculatedFieldState).getLastDynamicArgumentsRefreshTs()
+ < System.currentTimeMillis() - TimeUnit.SECONDS.toMillis(scheduledUpdateInterval);
+ log.warn("entityId {}, cfId {}, state ready to refresh == {}", entityId, cfId, isReady);
+ return isReady;
});
}
diff --git a/application/src/test/java/org/thingsboard/server/controller/AiModelControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/AiModelControllerTest.java
index ae2972b0cc..53648af441 100644
--- a/application/src/test/java/org/thingsboard/server/controller/AiModelControllerTest.java
+++ b/application/src/test/java/org/thingsboard/server/controller/AiModelControllerTest.java
@@ -104,7 +104,10 @@ public class AiModelControllerTest extends AbstractControllerTest {
var model = doPost("/api/ai/model", constructValidOpenAiModel("Test model"), AiModel.class);
var newModelConfig = OpenAiChatModelConfig.builder()
- .providerConfig(new OpenAiProviderConfig("test-api-key-updated"))
+ .providerConfig(OpenAiProviderConfig.builder()
+ .baseUrl(OpenAiProviderConfig.OPENAI_OFFICIAL_BASE_URL)
+ .apiKey("test-api-key-updated")
+ .build())
.modelId("o4-mini")
.temperature(0.2)
.topP(0.4)
@@ -270,7 +273,7 @@ public class AiModelControllerTest extends AbstractControllerTest {
.tenantId(tenantId)
.name("Test model 1")
.configuration(OpenAiChatModelConfig.builder()
- .providerConfig(new OpenAiProviderConfig("test-api-key"))
+ .providerConfig(OpenAiProviderConfig.builder().apiKey("test-api-key").build())
.modelId("o3-pro")
.build())
.build(), AiModel.class);
@@ -594,7 +597,10 @@ public class AiModelControllerTest extends AbstractControllerTest {
private AiModel constructValidOpenAiModel(String name) {
var modelConfig = OpenAiChatModelConfig.builder()
- .providerConfig(new OpenAiProviderConfig("test-api-key"))
+ .providerConfig(OpenAiProviderConfig.builder()
+ .baseUrl(OpenAiProviderConfig.OPENAI_OFFICIAL_BASE_URL)
+ .apiKey("test-api-key")
+ .build())
.modelId("gpt-4o")
.temperature(0.5)
.topP(0.3)
diff --git a/application/src/test/java/org/thingsboard/server/controller/AssetControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/AssetControllerTest.java
index 90b5cbeef2..e160fff59e 100644
--- a/application/src/test/java/org/thingsboard/server/controller/AssetControllerTest.java
+++ b/application/src/test/java/org/thingsboard/server/controller/AssetControllerTest.java
@@ -1080,6 +1080,30 @@ public class AssetControllerTest extends AbstractControllerTest {
testEntityDaoWithRelationsTransactionalException(assetDao, savedTenant.getId(), assetId, "/api/asset/" + assetId);
}
+ @Test
+ public void testSaveAssetWithUniquifyStrategy() throws Exception {
+ Asset asset = new Asset();
+ asset.setName("My unique asset");
+ asset.setType("default");
+ doPost("/api/asset", asset, Asset.class);
+
+ doPost("/api/asset", asset).andExpect(status().isBadRequest());
+
+ doPost("/api/asset?nameConflictPolicy=FAIL", asset).andExpect(status().isBadRequest());
+
+ Asset secondAsset = doPost("/api/asset?nameConflictPolicy=UNIQUIFY", asset, Asset.class);
+ assertThat(secondAsset.getName()).startsWith("My unique asset_");
+
+ Asset thirdAsset = doPost("/api/asset?nameConflictPolicy=UNIQUIFY&uniquifySeparator=-", asset, Asset.class);
+ assertThat(thirdAsset.getName()).startsWith("My unique asset-");
+
+ Asset fourthAsset = doPost("/api/asset?nameConflictPolicy=UNIQUIFY&uniquifyStrategy=INCREMENTAL", asset, Asset.class);
+ assertThat(fourthAsset.getName()).isEqualTo("My unique asset_1");
+
+ Asset fifthAsset = doPost("/api/asset?nameConflictPolicy=UNIQUIFY&uniquifyStrategy=INCREMENTAL", asset, Asset.class);
+ assertThat(fifthAsset.getName()).isEqualTo("My unique asset_2");
+ }
+
private Asset createAsset(String name) {
Asset asset = new Asset();
asset.setName(name);
diff --git a/application/src/test/java/org/thingsboard/server/controller/CalculatedFieldControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/CalculatedFieldControllerTest.java
index af43b34558..27622b347a 100644
--- a/application/src/test/java/org/thingsboard/server/controller/CalculatedFieldControllerTest.java
+++ b/application/src/test/java/org/thingsboard/server/controller/CalculatedFieldControllerTest.java
@@ -29,15 +29,23 @@ import org.thingsboard.server.common.data.cf.configuration.CalculatedFieldConfig
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.RelationPathQueryDynamicSourceConfiguration;
import org.thingsboard.server.common.data.cf.configuration.SimpleCalculatedFieldConfiguration;
+import org.thingsboard.server.common.data.cf.configuration.geofencing.EntityCoordinates;
+import org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingCalculatedFieldConfiguration;
+import org.thingsboard.server.common.data.cf.configuration.geofencing.ZoneGroupConfiguration;
import org.thingsboard.server.common.data.id.DeviceId;
+import org.thingsboard.server.common.data.relation.EntitySearchDirection;
+import org.thingsboard.server.common.data.relation.RelationPathLevel;
import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.dao.service.DaoSqlTest;
+import java.util.List;
import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+import static org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingReportStrategy.REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS;
@DaoSqlTest
public class CalculatedFieldControllerTest extends AbstractControllerTest {
@@ -84,7 +92,35 @@ public class CalculatedFieldControllerTest extends AbstractControllerTest {
assertThat(savedCalculatedField.getEntityId()).isEqualTo(calculatedField.getEntityId());
assertThat(savedCalculatedField.getType()).isEqualTo(calculatedField.getType());
assertThat(savedCalculatedField.getName()).isEqualTo(calculatedField.getName());
- assertThat(savedCalculatedField.getConfiguration()).isEqualTo(getCalculatedFieldConfig());
+ assertThat(savedCalculatedField.getConfiguration()).isEqualTo(getSimpleCalculatedFieldConfig());
+ assertThat(savedCalculatedField.getVersion()).isEqualTo(1L);
+
+ savedCalculatedField.setName("Test CF");
+
+ CalculatedField updatedCalculatedField = doPost("/api/calculatedField", savedCalculatedField, CalculatedField.class);
+
+ assertThat(updatedCalculatedField.getName()).isEqualTo(savedCalculatedField.getName());
+ assertThat(updatedCalculatedField.getVersion()).isEqualTo(savedCalculatedField.getVersion() + 1);
+
+ doDelete("/api/calculatedField/" + savedCalculatedField.getId().getId().toString())
+ .andExpect(status().isOk());
+ }
+
+ @Test
+ public void testSaveGeofencingCalculatedField() throws Exception {
+ Device testDevice = createDevice("Test device", "1234567890");
+ CalculatedField calculatedField = getCalculatedField(testDevice.getId(), getGeofencingCalculatedFieldConfig());
+
+ CalculatedField savedCalculatedField = doPost("/api/calculatedField", calculatedField, CalculatedField.class);
+
+ assertThat(savedCalculatedField).isNotNull();
+ assertThat(savedCalculatedField.getId()).isNotNull();
+ assertThat(savedCalculatedField.getCreatedTime()).isGreaterThan(0);
+ assertThat(savedCalculatedField.getTenantId()).isEqualTo(savedTenant.getId());
+ assertThat(savedCalculatedField.getEntityId()).isEqualTo(calculatedField.getEntityId());
+ assertThat(savedCalculatedField.getType()).isEqualTo(calculatedField.getType());
+ assertThat(savedCalculatedField.getName()).isEqualTo(calculatedField.getName());
+ assertThat(savedCalculatedField.getConfiguration()).isEqualTo(getGeofencingCalculatedFieldConfig());
assertThat(savedCalculatedField.getVersion()).isEqualTo(1L);
savedCalculatedField.setName("Test CF");
@@ -128,17 +164,41 @@ public class CalculatedFieldControllerTest extends AbstractControllerTest {
}
private CalculatedField getCalculatedField(DeviceId deviceId) {
+ return getCalculatedField(deviceId, getSimpleCalculatedFieldConfig());
+ }
+
+ private CalculatedField getCalculatedField(DeviceId deviceId, CalculatedFieldConfiguration configuration) {
CalculatedField calculatedField = new CalculatedField();
calculatedField.setEntityId(deviceId);
calculatedField.setType(CalculatedFieldType.SIMPLE);
calculatedField.setName("Test Calculated Field");
calculatedField.setConfigurationVersion(1);
- calculatedField.setConfiguration(getCalculatedFieldConfig());
+ calculatedField.setConfiguration(configuration);
calculatedField.setVersion(1L);
return calculatedField;
}
- private CalculatedFieldConfiguration getCalculatedFieldConfig() {
+ private CalculatedFieldConfiguration getGeofencingCalculatedFieldConfig() {
+ var config = new GeofencingCalculatedFieldConfiguration();
+
+ var refDynamicSourceConfiguration = new RelationPathQueryDynamicSourceConfiguration();
+ refDynamicSourceConfiguration.setLevels(List.of(new RelationPathLevel(EntitySearchDirection.TO, "FromSafeArea")));
+
+ var zoneGroupConfiguration = new ZoneGroupConfiguration("perimeter", REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS, false);
+ zoneGroupConfiguration.setRefDynamicSourceConfiguration(refDynamicSourceConfiguration);
+
+ Output output = new Output();
+ output.setType(OutputType.TIME_SERIES);
+
+ config.setEntityCoordinates(new EntityCoordinates("latitide", "longitude"));
+ config.setZoneGroups(Map.of("safeArea", zoneGroupConfiguration));
+ config.setScheduledUpdateEnabled(false);
+ config.setOutput(output);
+
+ return config;
+ }
+
+ private CalculatedFieldConfiguration getSimpleCalculatedFieldConfig() {
SimpleCalculatedFieldConfiguration config = new SimpleCalculatedFieldConfiguration();
Argument argument = new Argument();
diff --git a/application/src/test/java/org/thingsboard/server/controller/CustomerControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/CustomerControllerTest.java
index f312e49f71..f4e91f993e 100644
--- a/application/src/test/java/org/thingsboard/server/controller/CustomerControllerTest.java
+++ b/application/src/test/java/org/thingsboard/server/controller/CustomerControllerTest.java
@@ -33,6 +33,7 @@ import org.springframework.context.annotation.Primary;
import org.springframework.test.context.ContextConfiguration;
import org.thingsboard.common.util.ThingsBoardExecutors;
import org.thingsboard.server.common.data.Customer;
+import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.User;
@@ -462,6 +463,27 @@ public class CustomerControllerTest extends AbstractControllerTest {
testEntityDaoWithRelationsTransactionalException(customerDao, savedTenant.getId(), customerId, "/api/customer/" + customerId);
}
+ @Test
+ public void testSaveCustomerWithUniquifyStrategy() throws Exception {
+ Customer customer = new Customer();
+ customer.setTitle("My unique customer");
+ Customer savedCustomer = doPost("/api/customer", customer, Customer.class);
+
+ doPost("/api/customer?nameConflictPolicy=FAIL", customer).andExpect(status().isBadRequest());
+
+ Customer secondCustomer = doPost("/api/customer?nameConflictPolicy=UNIQUIFY", customer, Customer.class);
+ assertThat(secondCustomer.getName()).startsWith("My unique customer_");
+
+ Customer thirdCustomer = doPost("/api/customer?nameConflictPolicy=UNIQUIFY&uniquifySeparator=-", customer, Customer.class);
+ assertThat(thirdCustomer.getName()).startsWith("My unique customer-");
+
+ Customer fourthCustomer = doPost("/api/customer?nameConflictPolicy=UNIQUIFY&uniquifyStrategy=INCREMENTAL", customer, Customer.class);
+ assertThat(fourthCustomer.getName()).isEqualTo("My unique customer_1");
+
+ Customer fifthCustomer = doPost("/api/customer?nameConflictPolicy=UNIQUIFY&uniquifyStrategy=INCREMENTAL", customer, Customer.class);
+ assertThat(fifthCustomer.getName()).isEqualTo("My unique customer_2");
+ }
+
private Customer createCustomer(String title) {
Customer customer = new Customer();
customer.setTitle(title);
diff --git a/application/src/test/java/org/thingsboard/server/controller/DeviceControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/DeviceControllerTest.java
index 36cede9e84..2a2f7bc888 100644
--- a/application/src/test/java/org/thingsboard/server/controller/DeviceControllerTest.java
+++ b/application/src/test/java/org/thingsboard/server/controller/DeviceControllerTest.java
@@ -38,6 +38,7 @@ import org.springframework.test.context.ContextConfiguration;
import org.testcontainers.shaded.org.awaitility.Awaitility;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.common.util.ThingsBoardExecutors;
+import org.thingsboard.server.common.data.AttributeScope;
import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceInfo;
@@ -59,6 +60,7 @@ import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.DeviceCredentialsId;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.kv.AttributeKvEntry;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.relation.EntityRelation;
@@ -77,10 +79,15 @@ import org.thingsboard.server.dao.service.DaoSqlTest;
import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.service.gateway_device.GatewayNotificationsService;
import org.thingsboard.server.service.state.DeviceStateService;
+import org.thingsboard.server.utils.CsvUtils;
+import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.LinkedList;
import java.util.List;
import java.util.Map;
+import java.util.Optional;
import java.util.concurrent.TimeUnit;
import static org.assertj.core.api.Assertions.assertThat;
@@ -1586,6 +1593,56 @@ public class DeviceControllerTest extends AbstractControllerTest {
Assert.assertEquals(newAttributeValue, actualAttribute.get("value"));
}
+ @Test
+ public void testBulkImportDeviceWithJsonAttr() throws Exception {
+ String deviceName = "some_device";
+ String deviceType = "some_type";
+ String deviceAttr = "{\"threshold\":45}";
+
+ List> content = new LinkedList<>();
+ content.add(Arrays.asList("NAME", "TYPE", "ATTR"));
+ content.add(Arrays.asList(deviceName, deviceType, deviceAttr));
+
+ byte[] bytes = CsvUtils.generateCsv(content);
+ BulkImportRequest request = new BulkImportRequest();
+ request.setFile(new String(bytes, StandardCharsets.UTF_8));
+ BulkImportRequest.Mapping mapping = new BulkImportRequest.Mapping();
+ BulkImportRequest.ColumnMapping name = new BulkImportRequest.ColumnMapping();
+ name.setType(BulkImportColumnType.NAME);
+ BulkImportRequest.ColumnMapping type = new BulkImportRequest.ColumnMapping();
+ type.setType(BulkImportColumnType.TYPE);
+ BulkImportRequest.ColumnMapping attr = new BulkImportRequest.ColumnMapping();
+ attr.setType(BulkImportColumnType.SERVER_ATTRIBUTE);
+ attr.setKey("attr");
+ List columns = new ArrayList<>();
+ columns.add(name);
+ columns.add(type);
+ columns.add(attr);
+
+ mapping.setColumns(columns);
+ mapping.setDelimiter(',');
+ mapping.setUpdate(true);
+ mapping.setHeader(true);
+ request.setMapping(mapping);
+
+ BulkImportResult deviceBulkImportResult = doPostWithTypedResponse("/api/device/bulk_import", request, new TypeReference<>() {});
+
+ Assert.assertEquals(1, deviceBulkImportResult.getCreated().get());
+ Assert.assertEquals(0, deviceBulkImportResult.getErrors().get());
+ Assert.assertEquals(0, deviceBulkImportResult.getUpdated().get());
+ Assert.assertTrue(deviceBulkImportResult.getErrorsList().isEmpty());
+
+ Device savedDevice = doGet("/api/tenant/devices?deviceName=" + deviceName, Device.class);
+
+ Assert.assertNotNull(savedDevice);
+ Assert.assertEquals(deviceName, savedDevice.getName());
+ Assert.assertEquals(deviceType, savedDevice.getType());
+
+ Optional retrieved = attributesService.find(tenantId, savedDevice.getId(), AttributeScope.SERVER_SCOPE, "attr").get();
+ assertThat(retrieved.get().getJsonValue().get()).isEqualTo(deviceAttr);
+ assertThat(retrieved.get().getStrValue()).isNotPresent();
+ }
+
@Test
public void testSaveDeviceWithOutdatedVersion() throws Exception {
Device device = createDevice("Device v1.0");
@@ -1608,6 +1665,30 @@ public class DeviceControllerTest extends AbstractControllerTest {
assertThat(device.getVersion()).isEqualTo(3);
}
+ @Test
+ public void testSaveDeviceWithUniquifyStrategy() throws Exception {
+ Device device = new Device();
+ device.setName("My unique device");
+ device.setType("default");
+ Device savedDevice = doPost("/api/device", device, Device.class);
+
+ doPost("/api/device", device).andExpect(status().isBadRequest());
+
+ doPost("/api/device?nameConflictPolicy=FAIL", device).andExpect(status().isBadRequest());
+
+ Device secondDevice = doPost("/api/device?nameConflictPolicy=UNIQUIFY", device, Device.class);
+ assertThat(secondDevice.getName()).startsWith("My unique device_");
+
+ Device thirdDevice = doPost("/api/device?nameConflictPolicy=UNIQUIFY&uniquifySeparator=-", device, Device.class);
+ assertThat(thirdDevice.getName()).startsWith("My unique device-");
+
+ Device fourthDevice = doPost("/api/device?nameConflictPolicy=UNIQUIFY&uniquifyStrategy=INCREMENTAL", device, Device.class);
+ assertThat(fourthDevice.getName()).isEqualTo("My unique device_1");
+
+ Device fifthDevice = doPost("/api/device?nameConflictPolicy=UNIQUIFY&uniquifyStrategy=INCREMENTAL", device, Device.class);
+ assertThat(fifthDevice.getName()).isEqualTo("My unique device_2");
+ }
+
private Device createDevice(String name) {
Device device = new Device();
device.setName(name);
diff --git a/application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java
index 1f80c1f379..775df230b6 100644
--- a/application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java
+++ b/application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java
@@ -77,6 +77,7 @@ import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.TimeUnit;
+import java.util.function.BiConsumer;
import java.util.stream.Collectors;
import static org.assertj.core.api.Assertions.assertThat;
@@ -431,14 +432,17 @@ public class EntityQueryControllerTest extends AbstractControllerTest {
List alarmFields = new ArrayList<>();
alarmFields.add(new EntityKey(EntityKeyType.ALARM_FIELD, "type"));
+ alarmFields.add(new EntityKey(EntityKeyType.ALARM_FIELD, "originatorDisplayName"));
EntityTypeFilter assetTypeFilter = new EntityTypeFilter();
assetTypeFilter.setEntityType(EntityType.ASSET);
AlarmDataQuery assetAlarmQuery = new AlarmDataQuery(assetTypeFilter, pageLink, null, null, null, alarmFields);
PageData alarmPageData = findAlarmsByQueryAndCheck(assetAlarmQuery, 10);
- List retrievedAlarmTypes = alarmPageData.getData().stream().map(Alarm::getType).toList();
+ List retrievedAlarmTypes = alarmPageData.getData().stream().map(AlarmData::getType).toList();
assertThat(retrievedAlarmTypes).containsExactlyInAnyOrderElementsOf(assetAlarmTypes);
+ List retrievedAlarmDisplayName = alarmPageData.getData().stream().map(AlarmData::getOriginatorDisplayName).toList();
+ assertThat(retrievedAlarmDisplayName).containsExactlyInAnyOrderElementsOf(assets.stream().map(Asset::getLabel).toList());
KeyFilter nameFilter = buildStringKeyFilter(EntityKeyType.ENTITY_FIELD, "name", StringFilterPredicate.StringOperation.STARTS_WITH, "Asset1");
List keyFilters = Collections.singletonList(nameFilter);
@@ -519,6 +523,67 @@ public class EntityQueryControllerTest extends AbstractControllerTest {
Assert.assertEquals(1, filteredAssetAlamData.getTotalElements());
}
+ @Test
+ public void testFindAlarmsWithEntityFilterAndLatestValues() throws Exception {
+ loginTenantAdmin();
+ List devices = new ArrayList<>();
+ List temps = new ArrayList<>();
+ List deviceNames = new ArrayList<>();
+ for (int i = 0; i < 10; i++) {
+ Device device = new Device();
+ device.setCustomerId(customerId);
+ device.setName("Device" + i);
+ device.setType("default");
+ device.setLabel("testLabel" + (int) (Math.random() * 1000));
+ device = doPost("/api/device", device, Device.class);
+ devices.add(device);
+ deviceNames.add(device.getName());
+
+ int temp = i * 10;
+ temps.add(String.valueOf(temp));
+ JsonNode content = JacksonUtil.toJsonNode("{\"temperature\": " + temp + "}");
+ doPost("/api/plugins/telemetry/" + EntityType.DEVICE.name() + "/" + device.getUuidId() + "/timeseries/SERVER_SCOPE", content)
+ .andExpect(status().isOk());
+ Thread.sleep(1);
+ }
+
+ for (int i = 0; i < devices.size(); i++) {
+ Alarm alarm = new Alarm();
+ alarm.setCustomerId(customerId);
+ alarm.setOriginator(devices.get(i).getId());
+ String type = "device alarm" + i;
+ alarm.setType(type);
+ alarm.setSeverity(AlarmSeverity.WARNING);
+ doPost("/api/alarm", alarm, Alarm.class);
+ Thread.sleep(1);
+ }
+
+ AlarmDataPageLink pageLink = new AlarmDataPageLink();
+ pageLink.setPage(0);
+ pageLink.setPageSize(100);
+ pageLink.setSortOrder(new EntityDataSortOrder(new EntityKey(EntityKeyType.ALARM_FIELD, "created_time")));
+
+ List alarmFields = new ArrayList<>();
+ alarmFields.add(new EntityKey(EntityKeyType.ALARM_FIELD, "type"));
+
+ List entityFields = new ArrayList<>();
+ entityFields.add(new EntityKey(EntityKeyType.ENTITY_FIELD, "name"));
+
+ List latestValues = new ArrayList<>();
+ latestValues.add(new EntityKey(EntityKeyType.TIME_SERIES, "temperature"));
+
+ EntityTypeFilter deviceTypeFilter = new EntityTypeFilter();
+ deviceTypeFilter.setEntityType(EntityType.DEVICE);
+ AlarmDataQuery deviceAlarmQuery = new AlarmDataQuery(deviceTypeFilter, pageLink, entityFields, latestValues, null, alarmFields);
+
+ PageData alarmPageData = findAlarmsByQueryAndCheck(deviceAlarmQuery, 10);
+ List retrievedAlarmTemps = alarmPageData.getData().stream().map(alarmData -> alarmData.getLatest().get(EntityKeyType.TIME_SERIES).get("temperature").getValue()).toList();
+ assertThat(retrievedAlarmTemps).containsExactlyInAnyOrderElementsOf(temps);
+
+ List retrievedDeviceNames = alarmPageData.getData().stream().map(alarmData -> alarmData.getLatest().get(EntityKeyType.ENTITY_FIELD).get("name").getValue()).toList();
+ assertThat(retrievedDeviceNames).containsExactlyInAnyOrderElementsOf(deviceNames);
+ }
+
private void testCountAlarmsByQuery(List alarms) throws Exception {
AlarmCountQuery countQuery = new AlarmCountQuery();
@@ -1007,6 +1072,119 @@ public class EntityQueryControllerTest extends AbstractControllerTest {
countByQueryAndCheck(customerEntitiesQuery, 0);
}
+ @Test
+ public void testFindDevicesByDisplayName() throws Exception {
+ loginTenantAdmin();
+ int numOfDevices = 3;
+
+ for (int i = 0; i < numOfDevices; i++) {
+ Device device = new Device();
+ String name = "Device" + i;
+ device.setName(name);
+ device.setLabel("Device Label " + i);
+ device.setType("testFindDevicesByDisplayName");
+
+ Device savedDevice = doPost("/api/device?accessToken=" + name, device, Device.class);
+ }
+
+ DeviceTypeFilter filter = new DeviceTypeFilter();
+ filter.setDeviceTypes(List.of("testFindDevicesByDisplayName"));
+ filter.setDeviceNameFilter("");
+
+ KeyFilter displayNameFilter = getEntityFieldEqualFilter("displayName", "Device Label " + 0);
+
+ EntityDataSortOrder sortOrder = new EntityDataSortOrder(
+ new EntityKey(EntityKeyType.ENTITY_FIELD, "displayName"), EntityDataSortOrder.Direction.ASC
+ );
+ EntityDataPageLink pageLink = new EntityDataPageLink(10, 0, null, sortOrder);
+ List entityFields = List.of(new EntityKey(EntityKeyType.ENTITY_FIELD, "name"), new EntityKey(EntityKeyType.ENTITY_FIELD, "displayName"));
+
+ // all devices with ownerName = TEST TENANT
+ EntityDataQuery query = new EntityDataQuery(filter, pageLink, entityFields, Collections.emptyList(), Collections.emptyList());
+ checkEntitiesByQuery(query, numOfDevices, (i, entity) -> {
+ String name = entity.getLatest().get(EntityKeyType.ENTITY_FIELD).getOrDefault("name", new TsValue(0, "Invalid")).getValue();
+ String displayName = entity.getLatest().get(EntityKeyType.ENTITY_FIELD).getOrDefault("displayName", new TsValue(0, "Invalid")).getValue();
+ Assert.assertEquals("Device" + i, name);
+ Assert.assertEquals("Device Label " + i, displayName);
+ });
+
+ // all devices with ownerName = TEST TENANT
+ EntityDataQuery displayNameFilterQuery = new EntityDataQuery(filter, pageLink, entityFields, Collections.emptyList(), List.of(displayNameFilter));
+ checkEntitiesByQuery(displayNameFilterQuery, 1, (i, entity) -> {
+ String name = entity.getLatest().get(EntityKeyType.ENTITY_FIELD).getOrDefault("name", new TsValue(0, "Invalid")).getValue();
+ String displayName = entity.getLatest().get(EntityKeyType.ENTITY_FIELD).getOrDefault("displayName", new TsValue(0, "Invalid")).getValue();
+ Assert.assertEquals("Device" + i, name);
+ Assert.assertEquals("Device Label " + i, displayName);
+ });
+ }
+
+ @Test
+ public void testFindUsersByDisplayName() throws Exception {
+ loginTenantAdmin();
+
+ User userA = new User();
+ userA.setAuthority(Authority.TENANT_ADMIN);
+ userA.setFirstName("John");
+ userA.setLastName("Doe");
+ userA.setEmail("john.doe@tb.org");
+ userA = doPost("/api/user", userA, User.class);
+ var aId = userA.getId();
+
+ User userB = new User();
+ userB.setAuthority(Authority.TENANT_ADMIN);
+ userB.setFirstName("John");
+ userB.setEmail("john@tb.org");
+ userB = doPost("/api/user", userB, User.class);
+ var bId = userB.getId();
+
+ User userC = new User();
+ userC.setAuthority(Authority.TENANT_ADMIN);
+ userC.setLastName("Doe");
+ userC.setEmail("doe@tb.org");
+ userC = doPost("/api/user", userC, User.class);
+ var cId = userC.getId();
+
+ User userD = new User();
+ userD.setAuthority(Authority.TENANT_ADMIN);
+ userD.setEmail("noname@tb.org");
+ userD = doPost("/api/user", userD, User.class);
+ var dId = userD.getId();
+
+ EntityTypeFilter filter = new EntityTypeFilter();
+ filter.setEntityType(EntityType.USER);
+
+ EntityDataSortOrder sortOrder = new EntityDataSortOrder(
+ new EntityKey(EntityKeyType.ENTITY_FIELD, "displayName"), EntityDataSortOrder.Direction.ASC
+ );
+ EntityDataPageLink pageLink = new EntityDataPageLink(10, 0, null, sortOrder);
+ List entityFields = List.of(new EntityKey(EntityKeyType.ENTITY_FIELD, "displayName"));
+
+ EntityDataQuery query = new EntityDataQuery(filter, pageLink, entityFields, Collections.emptyList(), List.of(getEntityFieldEqualFilter("displayName", "John Doe")));
+ checkEntitiesByQuery(query, 1, (i, entity) -> {
+ Assert.assertEquals(aId, entity.getEntityId());
+ String displayName = entity.getLatest().get(EntityKeyType.ENTITY_FIELD).getOrDefault("displayName", new TsValue(0, "Invalid")).getValue();
+ Assert.assertEquals("John Doe", displayName);
+ });
+ query = new EntityDataQuery(filter, pageLink, entityFields, Collections.emptyList(), List.of(getEntityFieldEqualFilter("displayName", "John")));
+ checkEntitiesByQuery(query, 1, (i, entity) -> {
+ Assert.assertEquals(bId, entity.getEntityId());
+ String displayName = entity.getLatest().get(EntityKeyType.ENTITY_FIELD).getOrDefault("displayName", new TsValue(0, "Invalid")).getValue();
+ Assert.assertEquals("John", displayName);
+ });
+ query = new EntityDataQuery(filter, pageLink, entityFields, Collections.emptyList(), List.of(getEntityFieldEqualFilter("displayName", "Doe")));
+ checkEntitiesByQuery(query, 1, (i, entity) -> {
+ Assert.assertEquals(cId, entity.getEntityId());
+ String displayName = entity.getLatest().get(EntityKeyType.ENTITY_FIELD).getOrDefault("displayName", new TsValue(0, "Invalid")).getValue();
+ Assert.assertEquals("Doe", displayName);
+ });
+ query = new EntityDataQuery(filter, pageLink, entityFields, Collections.emptyList(), List.of(getEntityFieldEqualFilter("displayName", "noname@tb.org")));
+ checkEntitiesByQuery(query, 1, (i, entity) -> {
+ Assert.assertEquals(dId, entity.getEntityId());
+ String displayName = entity.getLatest().get(EntityKeyType.ENTITY_FIELD).getOrDefault("displayName", new TsValue(0, "Invalid")).getValue();
+ Assert.assertEquals("noname@tb.org", displayName);
+ });
+ }
+
@Test
public void testFindDevicesByOwnerNameAndOwnerType() throws Exception {
loginTenantAdmin();
@@ -1044,19 +1222,30 @@ public class EntityQueryControllerTest extends AbstractControllerTest {
// all devices with ownerName = TEST TENANT
EntityDataQuery query = new EntityDataQuery(filter, pageLink, entityFields, latestValues, List.of(activeAlarmTimeFilter, tenantOwnerNameFilter));
- checkEntitiesByQuery(query, numOfDevices, TEST_TENANT_NAME, "TENANT");
+ BiConsumer checkFunction = (i, entity) -> {
+ String name = entity.getLatest().get(EntityKeyType.ENTITY_FIELD).getOrDefault("name", new TsValue(0, "Invalid")).getValue();
+ String ownerName = entity.getLatest().get(EntityKeyType.ENTITY_FIELD).getOrDefault("ownerName", new TsValue(0, "Invalid")).getValue();
+ String ownerType = entity.getLatest().get(EntityKeyType.ENTITY_FIELD).getOrDefault("ownerType", new TsValue(0, "Invalid")).getValue();
+ String alarmActiveTime = entity.getLatest().get(EntityKeyType.ATTRIBUTE).getOrDefault("alarmActiveTime", new TsValue(0, "-1")).getValue();
+
+ Assert.assertEquals("Device" + i, name);
+ Assert.assertEquals(TEST_TENANT_NAME, ownerName);
+ Assert.assertEquals("TENANT", ownerType);
+ Assert.assertEquals("1" + i, alarmActiveTime);
+ };
+ checkEntitiesByQuery(query, numOfDevices, checkFunction);
// all devices with wrong ownerName
EntityDataQuery wrongTenantNameQuery = new EntityDataQuery(filter, pageLink, entityFields, latestValues, List.of(activeAlarmTimeFilter, wrongOwnerNameFilter));
- checkEntitiesByQuery(wrongTenantNameQuery, 0, null, null);
+ checkEntitiesByQuery(wrongTenantNameQuery, 0, null);
// all devices with owner type = TENANT
EntityDataQuery tenantEntitiesQuery = new EntityDataQuery(filter, pageLink, entityFields, latestValues, List.of(activeAlarmTimeFilter, tenantOwnerTypeFilter));
- checkEntitiesByQuery(tenantEntitiesQuery, numOfDevices, TEST_TENANT_NAME, "TENANT");
+ checkEntitiesByQuery(tenantEntitiesQuery, numOfDevices, checkFunction);
// all devices with owner type = CUSTOMER
EntityDataQuery customerEntitiesQuery = new EntityDataQuery(filter, pageLink, entityFields, latestValues, List.of(activeAlarmTimeFilter, customerOwnerTypeFilter));
- checkEntitiesByQuery(customerEntitiesQuery, 0, null, null);
+ checkEntitiesByQuery(customerEntitiesQuery, 0, null);
}
@Test
@@ -1102,6 +1291,28 @@ public class EntityQueryControllerTest extends AbstractControllerTest {
findByQueryAndCheck(query, 0);
}
+ private void checkEntitiesByQuery(EntityDataQuery query, int expectedNumOfDevices, BiConsumer checkFunction) throws Exception {
+ await()
+ .alias("data by query")
+ .atMost(30, TimeUnit.SECONDS)
+ .until(() -> {
+ var data = findByQuery(query);
+ var loadedEntities = new ArrayList<>(data.getData());
+ return loadedEntities.size() == expectedNumOfDevices;
+ });
+ if (expectedNumOfDevices == 0) {
+ return;
+ }
+ var data = findByQuery(query);
+ var loadedEntities = new ArrayList<>(data.getData());
+
+ Assert.assertEquals(expectedNumOfDevices, loadedEntities.size());
+
+ for (int i = 0; i < expectedNumOfDevices; i++) {
+ checkFunction.accept(i, loadedEntities.get(i));
+ }
+ }
+
private void checkEntitiesByQuery(EntityDataQuery query, int expectedNumOfDevices, String expectedOwnerName, String expectedOwnerType) throws Exception {
await()
.alias("data by query")
diff --git a/application/src/test/java/org/thingsboard/server/controller/EntityViewControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/EntityViewControllerTest.java
index 10b49dfe1d..167048a969 100644
--- a/application/src/test/java/org/thingsboard/server/controller/EntityViewControllerTest.java
+++ b/application/src/test/java/org/thingsboard/server/controller/EntityViewControllerTest.java
@@ -853,4 +853,29 @@ public class EntityViewControllerTest extends AbstractControllerTest {
EntityViewId entityViewId = getNewSavedEntityView("EntityView for Test WithRelations Transactional Exception").getId();
testEntityDaoWithRelationsTransactionalException(entityViewDao, tenantId, entityViewId, "/api/entityView/" + entityViewId);
}
+
+ @Test
+ public void testSaveEntityViewWithUniquifyStrategy() throws Exception {
+ EntityView view = new EntityView();
+ view.setEntityId(testDevice.getId());
+ view.setTenantId(tenantId);
+ view.setType("default");
+ view.setName("My unique view");
+
+ EntityView savedView = doPost("/api/entityView", view, EntityView.class);
+
+ doPost("/api/entityView?nameConflictPolicy=FAIL", view).andExpect(status().isBadRequest());
+
+ EntityView secondView = doPost("/api/entityView?nameConflictPolicy=UNIQUIFY", view, EntityView.class);
+ assertThat(secondView.getName()).startsWith("My unique view_");
+
+ EntityView thirdView = doPost("/api/entityView?nameConflictPolicy=UNIQUIFY&uniquifySeparator=-", view, EntityView.class);
+ assertThat(thirdView.getName()).startsWith("My unique view-");
+
+ EntityView fourthView = doPost("/api/entityView?nameConflictPolicy=UNIQUIFY&uniquifyStrategy=INCREMENTAL", view, EntityView.class);
+ assertThat(fourthView.getName()).isEqualTo("My unique view_1");
+
+ EntityView fifthEntityView = doPost("/api/entityView?nameConflictPolicy=UNIQUIFY&uniquifyStrategy=INCREMENTAL", view, EntityView.class);
+ assertThat(fifthEntityView.getName()).isEqualTo("My unique view_2");
+ }
}
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 3b194de7b4..352e1fcfeb 100644
--- a/application/src/test/java/org/thingsboard/server/controller/TbResourceControllerTest.java
+++ b/application/src/test/java/org/thingsboard/server/controller/TbResourceControllerTest.java
@@ -31,6 +31,7 @@ import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.Dashboard;
import org.thingsboard.server.common.data.DashboardInfo;
import org.thingsboard.server.common.data.Device;
+import org.thingsboard.server.common.data.EntityInfo;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.ResourceType;
import org.thingsboard.server.common.data.StringUtils;
@@ -321,18 +322,15 @@ public class TbResourceControllerTest extends AbstractControllerTest {
var referenceValues = JacksonUtil.toJsonNode(deleteResponse).get("references");
Assert.assertNotNull(referenceValues);
- var widgetTypeInfos = JacksonUtil.readValue(referenceValues.toString(), new TypeReference>>() {
+ var widgetTypeInfos = JacksonUtil.readValue(referenceValues.toString(), new TypeReference>>() {
});
Assert.assertNotNull(widgetTypeInfos);
Assert.assertFalse(widgetTypeInfos.isEmpty());
Assert.assertEquals(1, widgetTypeInfos.size());
- var dashboardInfo = widgetTypeInfos.get(EntityType.WIDGET_TYPE.name()).get(0);
- Assert.assertNotNull(dashboardInfo);
-
- WidgetTypeInfo foundedWidgetType = doGet("/api/widgetTypeInfo/" + savedWidgetType.getId().getId().toString(), WidgetTypeInfo.class);
- Assert.assertNotNull(foundedWidgetType);
- Assert.assertEquals(foundedWidgetType, dashboardInfo);
+ var widgetTypeInfo = widgetTypeInfos.get(EntityType.WIDGET_TYPE.name()).get(0);
+ Assert.assertNotNull(widgetTypeInfo);
+ Assert.assertEquals(new EntityInfo(savedWidgetType.getId(), savedWidgetType.getName()), widgetTypeInfo);
}
@Test
@@ -372,7 +370,7 @@ public class TbResourceControllerTest extends AbstractControllerTest {
Assert.assertTrue(isSuccess);
var referenceValues = JacksonUtil.toJsonNode(deleteResponse).get("references");
- var widgetTypeInfos = JacksonUtil.readValue(referenceValues.toString(), new TypeReference>>() {
+ var widgetTypeInfos = JacksonUtil.readValue(referenceValues.toString(), new TypeReference>>() {
});
Assert.assertNull(widgetTypeInfos);
}
@@ -417,7 +415,7 @@ public class TbResourceControllerTest extends AbstractControllerTest {
var referenceValues = JacksonUtil.toJsonNode(deleteResponse).get("references");
Assert.assertNotNull(referenceValues);
- var dashboardInfos = JacksonUtil.readValue(referenceValues.toString(), new TypeReference>>() {
+ var dashboardInfos = JacksonUtil.readValue(referenceValues.toString(), new TypeReference>>() {
});
Assert.assertNotNull(dashboardInfos);
Assert.assertFalse(dashboardInfos.isEmpty());
@@ -425,10 +423,7 @@ public class TbResourceControllerTest extends AbstractControllerTest {
var dashboardInfo = dashboardInfos.get(EntityType.DASHBOARD.name()).get(0);
Assert.assertNotNull(dashboardInfo);
-
- DashboardInfo foundDashboard = doGet("/api/dashboard/info/" + savedDashboard.getId().getId().toString(), DashboardInfo.class);
- Assert.assertNotNull(foundDashboard);
- Assert.assertEquals(foundDashboard, dashboardInfo);
+ Assert.assertEquals(new EntityInfo(savedDashboard.getId(), savedDashboard.getName()), dashboardInfo);
}
@Test
@@ -469,7 +464,7 @@ public class TbResourceControllerTest extends AbstractControllerTest {
Assert.assertTrue(isSuccess);
var referenceValues = JacksonUtil.toJsonNode(deleteResponse).get("references");
- var dashboardInfos = JacksonUtil.readValue(referenceValues.toString(), new TypeReference>>() {
+ var dashboardInfos = JacksonUtil.readValue(referenceValues.toString(), new TypeReference>>() {
});
Assert.assertNull(dashboardInfos);
}
diff --git a/application/src/test/java/org/thingsboard/server/edge/AiModelEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/AiModelEdgeTest.java
new file mode 100644
index 0000000000..30c8448f5b
--- /dev/null
+++ b/application/src/test/java/org/thingsboard/server/edge/AiModelEdgeTest.java
@@ -0,0 +1,196 @@
+/**
+ * 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.ai.AiModel;
+import org.thingsboard.server.common.data.ai.model.chat.OpenAiChatModelConfig;
+import org.thingsboard.server.common.data.ai.provider.OpenAiProviderConfig;
+import org.thingsboard.server.dao.service.DaoSqlTest;
+import org.thingsboard.server.gen.edge.v1.AiModelUpdateMsg;
+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.Optional;
+import java.util.UUID;
+
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+@DaoSqlTest
+public class AiModelEdgeTest extends AbstractEdgeTest {
+
+ private static final String DEFAULT_AI_MODEL_NAME = "Edge Test AiModel";
+ private static final String UPDATED_AI_MODEL_NAME = "Updated Edge Test AiModel";
+
+ @Test
+ public void testAiModel_create_update_delete() throws Exception {
+ // create AiModel
+ AiModel aiModel = createSimpleAiModel(DEFAULT_AI_MODEL_NAME);
+
+ edgeImitator.expectMessageAmount(1);
+ AiModel savedAiModel = doPost("/api/ai/model", aiModel, AiModel.class);
+ Assert.assertTrue(edgeImitator.waitForMessages());
+
+ AbstractMessage latestMessage = edgeImitator.getLatestMessage();
+ Assert.assertTrue(latestMessage instanceof AiModelUpdateMsg);
+ AiModelUpdateMsg aiModelUpdateMsg = (AiModelUpdateMsg) latestMessage;
+ Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, aiModelUpdateMsg.getMsgType());
+ Assert.assertEquals(savedAiModel.getUuidId().getMostSignificantBits(), aiModelUpdateMsg.getIdMSB());
+ Assert.assertEquals(savedAiModel.getUuidId().getLeastSignificantBits(), aiModelUpdateMsg.getIdLSB());
+ AiModel aiModelFromMsg = JacksonUtil.fromString(aiModelUpdateMsg.getEntity(), AiModel.class, true);
+ Assert.assertNotNull(aiModelFromMsg);
+
+ Assert.assertEquals(DEFAULT_AI_MODEL_NAME, aiModelFromMsg.getName());
+ Assert.assertEquals(savedAiModel.getTenantId(), aiModelFromMsg.getTenantId());
+
+ // update AiModel
+ edgeImitator.expectMessageAmount(1);
+ savedAiModel.setName(UPDATED_AI_MODEL_NAME);
+ savedAiModel = doPost("/api/ai/model", savedAiModel, AiModel.class);
+ Assert.assertTrue(edgeImitator.waitForMessages());
+
+ latestMessage = edgeImitator.getLatestMessage();
+ Assert.assertTrue(latestMessage instanceof AiModelUpdateMsg);
+ aiModelUpdateMsg = (AiModelUpdateMsg) latestMessage;
+ aiModelFromMsg = JacksonUtil.fromString(aiModelUpdateMsg.getEntity(), AiModel.class, true);
+ Assert.assertNotNull(aiModelFromMsg);
+ Assert.assertEquals(UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE, aiModelUpdateMsg.getMsgType());
+ Assert.assertEquals(UPDATED_AI_MODEL_NAME, aiModelFromMsg.getName());
+
+ // delete AiModel
+ edgeImitator.expectMessageAmount(1);
+ doDelete("/api/ai/model/" + savedAiModel.getUuidId())
+ .andExpect(status().isOk());
+ Assert.assertTrue(edgeImitator.waitForMessages());
+
+ latestMessage = edgeImitator.getLatestMessage();
+ Assert.assertTrue(latestMessage instanceof AiModelUpdateMsg);
+ aiModelUpdateMsg = (AiModelUpdateMsg) latestMessage;
+ Assert.assertEquals(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE, aiModelUpdateMsg.getMsgType());
+ Assert.assertEquals(savedAiModel.getUuidId().getMostSignificantBits(), aiModelUpdateMsg.getIdMSB());
+ Assert.assertEquals(savedAiModel.getUuidId().getLeastSignificantBits(), aiModelUpdateMsg.getIdLSB());
+ }
+
+ @Test
+ public void testSendAiModelToCloud() throws Exception {
+ AiModel aiModel = createSimpleAiModel(DEFAULT_AI_MODEL_NAME);
+ UUID uuid = Uuids.timeBased();
+ UplinkMsg uplinkMsg = getUplinkMsg(uuid, aiModel, UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE);
+
+ checkAiModelOnCloud(uplinkMsg, uuid, aiModel.getName());
+ }
+
+ @Test
+ public void testUpdateAiModelNameOnCloud() throws Exception {
+ AiModel aiModel = createSimpleAiModel(DEFAULT_AI_MODEL_NAME);
+ UUID uuid = Uuids.timeBased();
+ UplinkMsg uplinkMsg = getUplinkMsg(uuid, aiModel, UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE);
+
+ checkAiModelOnCloud(uplinkMsg, uuid, aiModel.getName());
+
+ aiModel.setName(UPDATED_AI_MODEL_NAME);
+ UplinkMsg updatedUplinkMsg = getUplinkMsg(uuid, aiModel, UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE);
+
+ checkAiModelOnCloud(updatedUplinkMsg, uuid, aiModel.getName());
+ }
+
+ @Test
+ public void testAiModelToCloudWithNameThatAlreadyExistsOnCloud() throws Exception {
+ AiModel aiModel = createSimpleAiModel(DEFAULT_AI_MODEL_NAME);
+
+ edgeImitator.expectMessageAmount(1);
+ AiModel savedAiModel = doPost("/api/ai/model", aiModel, AiModel.class);
+ Assert.assertTrue(edgeImitator.waitForMessages());
+
+ UUID uuid = Uuids.timeBased();
+
+ UplinkMsg uplinkMsg = getUplinkMsg(uuid, aiModel, UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE);
+
+ edgeImitator.expectResponsesAmount(1);
+ edgeImitator.expectMessageAmount(1);
+ edgeImitator.sendUplinkMsg(uplinkMsg);
+
+ Assert.assertTrue(edgeImitator.waitForResponses());
+ Assert.assertTrue(edgeImitator.waitForMessages());
+
+ Optional aiModelUpdateMsgOpt = edgeImitator.findMessageByType(AiModelUpdateMsg.class);
+ Assert.assertTrue(aiModelUpdateMsgOpt.isPresent());
+ AiModelUpdateMsg latestAiModelUpdateMsg = aiModelUpdateMsgOpt.get();
+ AiModel aiModelFromMsg = JacksonUtil.fromString(latestAiModelUpdateMsg.getEntity(), AiModel.class, true);
+ Assert.assertNotNull(aiModelFromMsg);
+ Assert.assertNotEquals(DEFAULT_AI_MODEL_NAME, aiModelFromMsg.getName());
+
+ Assert.assertNotEquals(savedAiModel.getUuidId(), uuid);
+
+ AiModel aiModelFromCloud = doGet("/api/ai/model/" + uuid, AiModel.class);
+ Assert.assertNotNull(aiModelFromCloud);
+ Assert.assertNotEquals(DEFAULT_AI_MODEL_NAME, aiModelFromCloud.getName());
+ }
+
+ private AiModel createSimpleAiModel(String name) {
+ AiModel aiModel = new AiModel();
+ aiModel.setTenantId(tenantId);
+ aiModel.setName(name);
+ aiModel.setConfiguration(OpenAiChatModelConfig.builder()
+ .providerConfig(new OpenAiProviderConfig(null, "test-api-key"))
+ .modelId("gpt-4o")
+ .temperature(0.5)
+ .topP(0.3)
+ .frequencyPenalty(0.1)
+ .presencePenalty(0.2)
+ .maxOutputTokens(1000)
+ .timeoutSeconds(60)
+ .maxRetries(2)
+ .build());
+ return aiModel;
+ }
+
+ private UplinkMsg getUplinkMsg(UUID uuid, AiModel aiModel, UpdateMsgType updateMsgType) throws InvalidProtocolBufferException {
+ UplinkMsg.Builder uplinkMsgBuilder = UplinkMsg.newBuilder();
+ AiModelUpdateMsg.Builder aiModelUpdateMsgBuilder = AiModelUpdateMsg.newBuilder();
+ aiModelUpdateMsgBuilder.setIdMSB(uuid.getMostSignificantBits());
+ aiModelUpdateMsgBuilder.setIdLSB(uuid.getLeastSignificantBits());
+ aiModelUpdateMsgBuilder.setEntity(JacksonUtil.toString(aiModel));
+ aiModelUpdateMsgBuilder.setMsgType(updateMsgType);
+ testAutoGeneratedCodeByProtobuf(aiModelUpdateMsgBuilder);
+ uplinkMsgBuilder.addAiModelUpdateMsg(aiModelUpdateMsgBuilder.build());
+
+ testAutoGeneratedCodeByProtobuf(uplinkMsgBuilder);
+
+ return uplinkMsgBuilder.build();
+ }
+
+ private void checkAiModelOnCloud(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());
+
+ AiModel aiModel = doGet("/api/ai/model/" + uuid, AiModel.class);
+ Assert.assertNotNull(aiModel);
+ Assert.assertEquals(resourceTitle, aiModel.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 16d10d9b6e..4594435626 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
@@ -29,6 +29,7 @@ import org.thingsboard.edge.rpc.EdgeGrpcClient;
import org.thingsboard.edge.rpc.EdgeRpcClient;
import org.thingsboard.server.controller.AbstractWebTest;
import org.thingsboard.server.gen.edge.v1.AdminSettingsUpdateMsg;
+import org.thingsboard.server.gen.edge.v1.AiModelUpdateMsg;
import org.thingsboard.server.gen.edge.v1.AlarmCommentUpdateMsg;
import org.thingsboard.server.gen.edge.v1.AlarmUpdateMsg;
import org.thingsboard.server.gen.edge.v1.AssetProfileUpdateMsg;
@@ -358,6 +359,11 @@ public class EdgeImitator {
result.add(saveDownlinkMsg(calculatedFieldUpdateMsg));
}
}
+ if (downlinkMsg.getAiModelUpdateMsgCount() > 0) {
+ for (AiModelUpdateMsg aiModelUpdateMsg : downlinkMsg.getAiModelUpdateMsgList()) {
+ result.add(saveDownlinkMsg(aiModelUpdateMsg));
+ }
+ }
if (downlinkMsg.hasEdgeConfiguration()) {
result.add(saveDownlinkMsg(downlinkMsg.getEdgeConfiguration()));
}
diff --git a/application/src/test/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateServiceTest.java b/application/src/test/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateServiceTest.java
index 20f8aaf881..969f6de9f5 100644
--- a/application/src/test/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateServiceTest.java
+++ b/application/src/test/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateServiceTest.java
@@ -20,9 +20,12 @@ import org.junit.Before;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.thingsboard.server.common.data.ApiUsageRecordKey;
+import org.thingsboard.server.common.data.ApiUsageState;
import org.thingsboard.server.common.data.ApiUsageStateValue;
import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.TenantProfile;
+import org.thingsboard.server.common.data.id.ApiUsageStateId;
+import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration;
import org.thingsboard.server.common.data.tenant.profile.TenantProfileData;
@@ -33,8 +36,17 @@ import org.thingsboard.server.dao.usagerecord.ApiUsageStateService;
import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.queue.common.TbProtoQueueMsg;
+import java.lang.reflect.Field;
+import java.time.LocalDate;
+import java.util.HashMap;
+import java.util.Map;
import java.util.UUID;
+import java.util.concurrent.TimeUnit;
+import static java.time.ZoneOffset.UTC;
+import static java.time.temporal.ChronoField.DAY_OF_MONTH;
+import static java.time.temporal.ChronoUnit.MONTHS;
+import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.assertEquals;
@DaoSqlTest
@@ -48,6 +60,7 @@ public class DefaultTbApiUsageStateServiceTest extends AbstractControllerTest {
private TenantId tenantId;
private Tenant savedTenant;
+ private TenantProfile savedTenantProfile;
private static final int MAX_ENABLE_VALUE = 5000;
private static final long VALUE_WARNING = 4500L;
@@ -59,7 +72,7 @@ public class DefaultTbApiUsageStateServiceTest extends AbstractControllerTest {
loginSysAdmin();
TenantProfile tenantProfile = createTenantProfile();
- TenantProfile savedTenantProfile = doPost("/api/tenantProfile", tenantProfile, TenantProfile.class);
+ savedTenantProfile = doPost("/api/tenantProfile", tenantProfile, TenantProfile.class);
Assert.assertNotNull(savedTenantProfile);
Tenant tenant = new Tenant();
@@ -109,6 +122,41 @@ public class DefaultTbApiUsageStateServiceTest extends AbstractControllerTest {
assertEquals(ApiUsageStateValue.DISABLED, apiUsageStateService.findTenantApiUsageState(tenantId).getDbStorageState());
}
+ @Test
+ public void checkStartOfNextCycle_setsNextCycleToNextMonth() throws Exception {
+ ApiUsageState apiUsageState = new ApiUsageState(new ApiUsageStateId(UUID.randomUUID()));
+ apiUsageState.setDbStorageState(ApiUsageStateValue.ENABLED);
+ apiUsageState.setAlarmExecState(ApiUsageStateValue.ENABLED);
+ apiUsageState.setSmsExecState(ApiUsageStateValue.ENABLED);
+ apiUsageState.setTbelExecState(ApiUsageStateValue.ENABLED);
+ apiUsageState.setReExecState(ApiUsageStateValue.ENABLED);
+ apiUsageState.setTransportState(ApiUsageStateValue.ENABLED);
+ apiUsageState.setEmailExecState(ApiUsageStateValue.ENABLED);
+ apiUsageState.setJsExecState(ApiUsageStateValue.ENABLED);
+ apiUsageState.setTenantId(tenantId);
+ apiUsageState.setEntityId(tenantId);
+
+ long now = System.currentTimeMillis();
+ long currentCycleTs = now - TimeUnit.DAYS.toMillis(30);
+ long nextCycleTs = now - TimeUnit.MINUTES.toMillis(5); // < 1h ago
+ TenantApiUsageState tenantApiUsageState = new TenantApiUsageState(savedTenantProfile, apiUsageState);
+ tenantApiUsageState.setCycles(currentCycleTs, nextCycleTs);
+ Map map = new HashMap<>();
+ map.put(tenantId, tenantApiUsageState);
+
+ Field fieldToSet = DefaultTbApiUsageStateService.class.getDeclaredField("myUsageStates");
+ fieldToSet.setAccessible(true);
+ fieldToSet.set(service, map);
+
+ service.checkStartOfNextCycle();
+
+ long firstOfNextMonth = LocalDate.now()
+ .with((temporal) -> temporal.with(DAY_OF_MONTH, 1)
+ .plus(1, MONTHS))
+ .atStartOfDay(UTC).toInstant().toEpochMilli();
+ assertThat(tenantApiUsageState.getNextCycleTs()).isEqualTo(firstOfNextMonth);
+ }
+
private TenantProfile createTenantProfile() {
TenantProfile tenantProfile = new TenantProfile();
tenantProfile.setName("Tenant Profile");
diff --git a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldStateTest.java b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldStateTest.java
index 691a1f7ec4..1f1ee32df2 100644
--- a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldStateTest.java
+++ b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldStateTest.java
@@ -28,7 +28,7 @@ import org.thingsboard.server.common.data.cf.CalculatedFieldType;
import org.thingsboard.server.common.data.cf.configuration.CalculatedFieldConfiguration;
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.RelationQueryDynamicSourceConfiguration;
+import org.thingsboard.server.common.data.cf.configuration.RelationPathQueryDynamicSourceConfiguration;
import org.thingsboard.server.common.data.cf.configuration.geofencing.EntityCoordinates;
import org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingCalculatedFieldConfiguration;
import org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingReportStrategy;
@@ -41,6 +41,7 @@ import org.thingsboard.server.common.data.kv.DoubleDataEntry;
import org.thingsboard.server.common.data.kv.JsonDataEntry;
import org.thingsboard.server.common.data.relation.EntityRelation;
import org.thingsboard.server.common.data.relation.EntitySearchDirection;
+import org.thingsboard.server.common.data.relation.RelationPathLevel;
import org.thingsboard.server.dao.relation.RelationService;
import org.thingsboard.server.dao.usagerecord.ApiLimitService;
import org.thingsboard.server.service.cf.CalculatedFieldResult;
@@ -257,7 +258,7 @@ public class GeofencingCalculatedFieldStateTest {
assertThat(result2).isNotNull();
assertThat(result2.getType()).isEqualTo(output.getType());
assertThat(result2.getScope()).isEqualTo(output.getScope());
- assertThat(result2.getResult()).isEqualTo(
+ assertThat(result2.getResult().get("values")).isEqualTo(
JacksonUtil.newObjectNode()
.put("allowedZonesEvent", "LEFT")
.put("allowedZonesStatus", "OUTSIDE")
@@ -329,7 +330,7 @@ public class GeofencingCalculatedFieldStateTest {
assertThat(result2).isNotNull();
assertThat(result2.getType()).isEqualTo(output.getType());
assertThat(result2.getScope()).isEqualTo(output.getScope());
- assertThat(result2.getResult()).isEqualTo(
+ assertThat(result2.getResult().get("values")).isEqualTo(
JacksonUtil.newObjectNode()
.put("allowedZonesEvent", "LEFT")
.put("restrictedZonesEvent", "ENTERED")
@@ -401,7 +402,7 @@ public class GeofencingCalculatedFieldStateTest {
assertThat(result2).isNotNull();
assertThat(result2.getType()).isEqualTo(output.getType());
assertThat(result2.getScope()).isEqualTo(output.getScope());
- assertThat(result2.getResult()).isEqualTo(
+ assertThat(result2.getResult().get("values")).isEqualTo(
JacksonUtil.newObjectNode()
.put("allowedZonesStatus", "OUTSIDE")
.put("restrictedZonesStatus", "INSIDE")
@@ -453,21 +454,15 @@ public class GeofencingCalculatedFieldStateTest {
config.setEntityCoordinates(entityCoordinates);
ZoneGroupConfiguration allowedZonesGroup = new ZoneGroupConfiguration("zone", reportStrategy, true);
- var allowedZoneDynamicSourceConfiguration = new RelationQueryDynamicSourceConfiguration();
- allowedZoneDynamicSourceConfiguration.setDirection(EntitySearchDirection.TO);
- allowedZoneDynamicSourceConfiguration.setRelationType("AllowedZone");
- allowedZoneDynamicSourceConfiguration.setMaxLevel(1);
- allowedZoneDynamicSourceConfiguration.setFetchLastLevelOnly(true);
+ var allowedZoneDynamicSourceConfiguration = new RelationPathQueryDynamicSourceConfiguration();
+ allowedZoneDynamicSourceConfiguration.setLevels(List.of(new RelationPathLevel(EntitySearchDirection.TO, "AllowedZone")));
allowedZonesGroup.setRefDynamicSourceConfiguration(allowedZoneDynamicSourceConfiguration);
allowedZonesGroup.setRelationType("CurrentZone");
allowedZonesGroup.setDirection(EntitySearchDirection.TO);
ZoneGroupConfiguration restrictedZonesGroup = new ZoneGroupConfiguration("zone", reportStrategy, true);
- var restrictedZoneDynamicSourceConfiguration = new RelationQueryDynamicSourceConfiguration();
- restrictedZoneDynamicSourceConfiguration.setDirection(EntitySearchDirection.TO);
- restrictedZoneDynamicSourceConfiguration.setRelationType("RestrictedZone");
- restrictedZoneDynamicSourceConfiguration.setMaxLevel(1);
- restrictedZoneDynamicSourceConfiguration.setFetchLastLevelOnly(true);
+ var restrictedZoneDynamicSourceConfiguration = new RelationPathQueryDynamicSourceConfiguration();
+ restrictedZoneDynamicSourceConfiguration.setLevels(List.of(new RelationPathLevel(EntitySearchDirection.TO, "RestrictedZone")));
restrictedZonesGroup.setRefDynamicSourceConfiguration(restrictedZoneDynamicSourceConfiguration);
restrictedZonesGroup.setRelationType("CurrentZone");
restrictedZonesGroup.setDirection(EntitySearchDirection.TO);
diff --git a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldStateTest.java b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldStateTest.java
index 8c631ecf6f..3aef8896de 100644
--- a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldStateTest.java
+++ b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldStateTest.java
@@ -123,7 +123,8 @@ public class SimpleCalculatedFieldStateTest {
Map newArgs = Map.of("key3", new TsRollingArgumentEntry(10, 30000L));
assertThatThrownBy(() -> state.updateState(ctx, newArgs))
.isInstanceOf(IllegalArgumentException.class)
- .hasMessage("Rolling argument entry is not supported for simple calculated fields.");
+ .hasMessage("Unsupported argument type detected for argument: key3. " +
+ "Rolling argument entry is not supported for simple calculated fields.");
}
@Test
diff --git a/application/src/test/java/org/thingsboard/server/service/entitiy/ai/DefaultTbAiModelServiceTest.java b/application/src/test/java/org/thingsboard/server/service/entitiy/ai/DefaultTbAiModelServiceTest.java
index 2321446b44..b6be136f82 100644
--- a/application/src/test/java/org/thingsboard/server/service/entitiy/ai/DefaultTbAiModelServiceTest.java
+++ b/application/src/test/java/org/thingsboard/server/service/entitiy/ai/DefaultTbAiModelServiceTest.java
@@ -253,7 +253,7 @@ class DefaultTbAiModelServiceTest {
private static AiModelConfig constructValidOpenAiModelConfig() {
return OpenAiChatModelConfig.builder()
- .providerConfig(new OpenAiProviderConfig("test-api-key"))
+ .providerConfig(OpenAiProviderConfig.builder().apiKey("test-api-key").build())
.modelId("gpt-4o")
.temperature(0.5)
.topP(0.3)
diff --git a/application/src/test/java/org/thingsboard/server/service/resource/DefaultResourceDataCacheTest.java b/application/src/test/java/org/thingsboard/server/service/resource/DefaultResourceDataCacheTest.java
new file mode 100644
index 0000000000..f12a8d3c5d
--- /dev/null
+++ b/application/src/test/java/org/thingsboard/server/service/resource/DefaultResourceDataCacheTest.java
@@ -0,0 +1,83 @@
+/**
+ * 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.resource;
+
+import org.junit.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.test.context.bean.override.mockito.MockitoSpyBean;
+import org.thingsboard.common.util.JacksonUtil;
+import org.thingsboard.server.common.data.GeneralFileDescriptor;
+import org.thingsboard.server.common.data.ResourceType;
+import org.thingsboard.server.common.data.TbResource;
+import org.thingsboard.server.common.data.TbResourceDataInfo;
+import org.thingsboard.server.common.data.TbResourceInfo;
+import org.thingsboard.server.controller.AbstractControllerTest;
+import org.thingsboard.server.dao.resource.ResourceService;
+import org.thingsboard.server.dao.resource.TbResourceDataCache;
+import org.thingsboard.server.dao.service.DaoSqlTest;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.clearInvocations;
+import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+
+@DaoSqlTest
+public class DefaultResourceDataCacheTest extends AbstractControllerTest {
+
+ @MockitoSpyBean
+ private ResourceService resourceService;
+ @Autowired
+ private TbResourceService tbResourceService;
+ @MockitoSpyBean
+ private TbResourceDataCache resourceDataCache;
+
+ @Test
+ public void testGetCachedResourceData() throws Exception {
+ loginTenantAdmin();
+
+ TbResource resource = new TbResource();
+ resource.setTenantId(tenantId);
+ resource.setTitle("File for AI request");
+ resource.setResourceType(ResourceType.GENERAL);
+ resource.setFileName("myTestJson.json");
+ GeneralFileDescriptor descriptor = new GeneralFileDescriptor("application/json");
+ resource.setDescriptorValue(descriptor);
+ byte[] data = "This is a test prompt for AI request.".getBytes();
+ resource.setData(data);
+ TbResourceInfo savedResource = tbResourceService.save(resource);
+ verify(resourceDataCache, timeout(2000).times(1)).evictResourceData(tenantId, savedResource.getId());
+
+ TbResourceDataInfo cachedData = resourceDataCache.getResourceDataInfoAsync(tenantId, savedResource.getId()).get();
+ assertThat(cachedData.getData()).isEqualTo(data);
+ assertThat(JacksonUtil.treeToValue(cachedData.getDescriptor(), GeneralFileDescriptor.class)).isEqualTo(descriptor);
+ verify(resourceService).getResourceDataInfo(tenantId, savedResource.getId());
+
+ // retrieve resource data second time
+ clearInvocations(resourceService);
+ TbResourceDataInfo cachedData2 = resourceDataCache.getResourceDataInfoAsync(tenantId, savedResource.getId()).get();
+ assertThat(cachedData2.getData()).isEqualTo(data);
+ verifyNoMoreInteractions(resourceService);
+
+ // delete resource, check cache
+ TbResource resourceById = resourceService.findResourceById(tenantId, savedResource.getId());
+ tbResourceService.delete(resourceById, true, null);
+ verify(resourceDataCache, timeout(2000).times(2)).evictResourceData(tenantId, savedResource.getId());
+ TbResourceDataInfo cachedDataAfterDeletion = resourceDataCache.getResourceDataInfoAsync(tenantId, savedResource.getId()).get();
+ assertThat(cachedDataAfterDeletion).isEqualTo(null);
+ }
+
+}
diff --git a/application/src/test/java/org/thingsboard/server/service/resource/sql/BaseTbResourceServiceTest.java b/application/src/test/java/org/thingsboard/server/service/resource/sql/BaseTbResourceServiceTest.java
index fe416eacd5..a146730465 100644
--- a/application/src/test/java/org/thingsboard/server/service/resource/sql/BaseTbResourceServiceTest.java
+++ b/application/src/test/java/org/thingsboard/server/service/resource/sql/BaseTbResourceServiceTest.java
@@ -17,6 +17,7 @@ package org.thingsboard.server.service.resource.sql;
import com.datastax.oss.driver.api.core.uuid.Uuids;
import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.fasterxml.jackson.databind.node.TextNode;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
@@ -24,8 +25,10 @@ import org.junit.Test;
import org.junit.jupiter.api.Assertions;
import org.springframework.beans.factory.annotation.Autowired;
import org.thingsboard.common.util.JacksonUtil;
+import org.thingsboard.rule.engine.ai.TbAiNode;
+import org.thingsboard.rule.engine.ai.TbAiNodeConfiguration;
+import org.thingsboard.rule.engine.ai.TbResponseFormat;
import org.thingsboard.server.common.data.Dashboard;
-import org.thingsboard.server.common.data.DashboardInfo;
import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.EntityInfo;
import org.thingsboard.server.common.data.EntityType;
@@ -37,26 +40,40 @@ import org.thingsboard.server.common.data.TbResourceInfoFilter;
import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.TenantProfile;
import org.thingsboard.server.common.data.User;
+import org.thingsboard.server.common.data.ai.AiModel;
+import org.thingsboard.server.common.data.ai.model.chat.OpenAiChatModelConfig;
+import org.thingsboard.server.common.data.ai.provider.OpenAiProviderConfig;
+import org.thingsboard.server.common.data.debug.DebugSettings;
import org.thingsboard.server.common.data.exception.ThingsboardException;
+import org.thingsboard.server.common.data.id.RuleChainId;
+import org.thingsboard.server.common.data.id.TbResourceId;
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.rule.RuleChain;
+import org.thingsboard.server.common.data.rule.RuleChainMetaData;
+import org.thingsboard.server.common.data.rule.RuleChainType;
+import org.thingsboard.server.common.data.rule.RuleNode;
import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration;
import org.thingsboard.server.common.data.widget.WidgetTypeDetails;
-import org.thingsboard.server.common.data.widget.WidgetTypeInfo;
import org.thingsboard.server.controller.AbstractControllerTest;
+import org.thingsboard.server.dao.ai.AiModelService;
import org.thingsboard.server.dao.dashboard.DashboardService;
import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.dao.resource.ResourceService;
+import org.thingsboard.server.dao.rule.RuleChainService;
import org.thingsboard.server.dao.service.DaoSqlTest;
import org.thingsboard.server.dao.widget.WidgetTypeService;
import org.thingsboard.server.service.resource.TbResourceService;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Base64;
import java.util.Collections;
import java.util.List;
+import java.util.Set;
+import java.util.function.Function;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
@@ -135,6 +152,10 @@ public class BaseTbResourceServiceTest extends AbstractControllerTest {
private WidgetTypeService widgetTypeService;
@Autowired
private DashboardService dashboardService;
+ @Autowired
+ private RuleChainService ruleChainService;
+ @Autowired
+ private AiModelService aiModelService;
private Tenant savedTenant;
private User tenantAdmin;
@@ -453,11 +474,9 @@ public class BaseTbResourceServiceTest extends AbstractControllerTest {
Assert.assertFalse(result.getReferences().isEmpty());
Assert.assertEquals(1, result.getReferences().size());
- WidgetTypeInfo widgetTypeInfo = (WidgetTypeInfo) result.getReferences().get(EntityType.WIDGET_TYPE.name()).get(0);
- WidgetTypeInfo foundWidgetTypeInfo = new WidgetTypeInfo(foundWidgetType);
+ EntityInfo widgetTypeInfo = (EntityInfo) result.getReferences().get(EntityType.WIDGET_TYPE.name()).get(0);
Assert.assertNotNull(widgetTypeInfo);
- Assert.assertNotNull(foundWidgetTypeInfo);
- Assert.assertEquals(widgetTypeInfo, foundWidgetTypeInfo);
+ Assert.assertEquals(widgetTypeInfo, new EntityInfo(foundWidgetType.getId(), foundWidgetType.getName()));
TbResourceInfo foundResourceInfo = resourceService.findResourceInfoById(savedTenant.getId(), savedResource.getId());
Assert.assertNotNull(foundResource);
@@ -546,11 +565,9 @@ public class BaseTbResourceServiceTest extends AbstractControllerTest {
Assert.assertNotNull(result.getReferences());
Assert.assertEquals(1, result.getReferences().size());
- DashboardInfo dashboardInfo = (DashboardInfo) result.getReferences().get(EntityType.DASHBOARD.name()).get(0);
- DashboardInfo foundDashboardInfo = dashboardService.findDashboardInfoById(savedTenant.getId(), savedDashboard.getId());
+ EntityInfo dashboardInfo = (EntityInfo) result.getReferences().get(EntityType.DASHBOARD.name()).get(0);
Assert.assertNotNull(dashboardInfo);
- Assert.assertNotNull(foundDashboardInfo);
- Assert.assertEquals(foundDashboardInfo, dashboardInfo);
+ Assert.assertEquals(new EntityInfo(savedDashboard.getId(), savedDashboard.getName()), dashboardInfo);
foundResource = resourceService.findResourceById(savedTenant.getId(), savedResource.getId());
Assert.assertNotNull(foundResource);
@@ -598,6 +615,94 @@ public class BaseTbResourceServiceTest extends AbstractControllerTest {
Assert.assertNull(foundResource);
}
+ @Test
+ public void testShouldNotDeleteResourceIfUsedInAiNode() throws Exception {
+ TbResource resource = new TbResource();
+ resource.setResourceType(ResourceType.GENERAL);
+ resource.setTitle("My resource");
+ resource.setFileName("test.json");
+ resource.setTenantId(savedTenant.getId());
+ resource.setData("".getBytes());
+ TbResourceInfo savedResource = tbResourceService.save(resource);
+ RuleChainMetaData ruleChain = createRuleChainReferringResource(savedResource.getId());
+
+ TbResourceDeleteResult result = tbResourceService.delete(savedResource, false, null);
+ assertThat(result).isNotNull();
+ assertThat(result.isSuccess()).isFalse();
+ assertThat(result.getReferences()).isNotEmpty().hasSize(1);
+ EntityInfo entityInfo = (EntityInfo) result.getReferences().get(EntityType.RULE_CHAIN.name()).get(0);
+ assertThat(entityInfo).isEqualTo(new EntityInfo(ruleChain.getRuleChainId(), "Test"));
+
+ TbResource foundResource = resourceService.findResourceById(savedTenant.getId(), savedResource.getId());
+ assertThat(foundResource).isNotNull();
+
+ // force delete
+ TbResourceDeleteResult deleteResult = tbResourceService.delete(savedResource, true, null);
+ assertThat(deleteResult).isNotNull();
+ assertThat(deleteResult.isSuccess()).isTrue();
+
+ TbResource resourceAfterDeletion = resourceService.findResourceById(savedTenant.getId(), savedResource.getId());
+ assertThat(resourceAfterDeletion).isNull();
+ }
+
+ private RuleChainMetaData createRuleChainReferringResource(TbResourceId resourceId) {
+ AiModel model = constructValidOpenAiModel("Test model");
+ AiModel saved = aiModelService.save(model);
+
+ RuleChain ruleChain = new RuleChain();
+ ruleChain.setTenantId(tenantId);
+ ruleChain.setName("Test");
+ ruleChain.setType(RuleChainType.CORE);
+ ruleChain.setDebugMode(true);
+ ruleChain.setConfiguration(JacksonUtil.newObjectNode().set("a", new TextNode("b")));
+ ruleChain = ruleChainService.saveRuleChain(ruleChain);
+ RuleChainId ruleChainId = ruleChain.getId();
+
+ RuleChainMetaData metaData = new RuleChainMetaData();
+ metaData.setRuleChainId(ruleChainId);
+
+ RuleNode aiNode = new RuleNode();
+ aiNode.setName("Ai request");
+ aiNode.setType(org.thingsboard.rule.engine.ai.TbAiNode.class.getName());
+ aiNode.setConfigurationVersion(TbAiNode.class.getAnnotation(org.thingsboard.rule.engine.api.RuleNode.class).version());
+ aiNode.setDebugSettings(DebugSettings.all());
+ TbAiNodeConfiguration configuration = new TbAiNodeConfiguration();
+ configuration.setResourceIds(Set.of(resourceId.getId()));
+ configuration.setModelId(saved.getId());
+ configuration.setResponseFormat(new TbResponseFormat.TbJsonResponseFormat());
+ configuration.setTimeoutSeconds(1);
+ configuration.setUserPrompt("What is temp");
+ aiNode.setConfiguration(JacksonUtil.valueToTree(configuration));
+
+ metaData.setNodes(Arrays.asList(aiNode));
+ metaData.setFirstNodeIndex(0);
+ ruleChainService.saveRuleChainMetaData(tenantId, metaData, Function.identity());
+ return ruleChainService.loadRuleChainMetaData(tenantId, ruleChainId);
+ }
+
+ private AiModel constructValidOpenAiModel(String name) {
+ var modelConfig = OpenAiChatModelConfig.builder()
+ .providerConfig(OpenAiProviderConfig.builder()
+ .baseUrl(OpenAiProviderConfig.OPENAI_OFFICIAL_BASE_URL)
+ .apiKey("test-api-key")
+ .build())
+ .modelId("gpt-4o")
+ .temperature(0.5)
+ .topP(0.3)
+ .frequencyPenalty(0.1)
+ .presencePenalty(0.2)
+ .maxOutputTokens(1000)
+ .timeoutSeconds(60)
+ .maxRetries(2)
+ .build();
+
+ return AiModel.builder()
+ .tenantId(tenantId)
+ .name(name)
+ .configuration(modelConfig)
+ .build();
+ }
+
@Test
public void testFindTenantResourcesByTenantId() throws Exception {
loginSysAdmin();
diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/telemetry/timeseries/AbstractMqttTimeseriesIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/telemetry/timeseries/AbstractMqttTimeseriesIntegrationTest.java
index 6543955ed5..9856aa18cb 100644
--- a/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/telemetry/timeseries/AbstractMqttTimeseriesIntegrationTest.java
+++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/telemetry/timeseries/AbstractMqttTimeseriesIntegrationTest.java
@@ -25,10 +25,10 @@ import org.springframework.boot.test.mock.mockito.SpyBean;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.id.DeviceId;
+import org.thingsboard.server.common.msg.gateway.metrics.GatewayMetadata;
import org.thingsboard.server.transport.mqtt.AbstractMqttIntegrationTest;
import org.thingsboard.server.transport.mqtt.MqttTestConfigProperties;
import org.thingsboard.server.transport.mqtt.gateway.GatewayMetricsService;
-import org.thingsboard.server.common.msg.gateway.metrics.GatewayMetadata;
import org.thingsboard.server.transport.mqtt.gateway.metrics.GatewayMetricsState;
import org.thingsboard.server.transport.mqtt.mqttv3.MqttTestCallback;
import org.thingsboard.server.transport.mqtt.mqttv3.MqttTestClient;
@@ -43,6 +43,7 @@ import java.util.Random;
import java.util.Set;
import java.util.concurrent.TimeUnit;
+import static org.awaitility.Awaitility.await;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.mockito.Mockito.any;
@@ -112,6 +113,42 @@ public abstract class AbstractMqttTimeseriesIntegrationTest extends AbstractMqtt
processGatewayTelemetryTest(GATEWAY_TELEMETRY_TOPIC, expectedKeys, payload.getBytes(), deviceName1, deviceName2);
}
+ @Test
+ public void testAckIsReceivedOnFailedPublishMessage() throws Exception {
+ String devicePayload = "[{\"ts\": 10000, \"values\": " + PAYLOAD_VALUES_STR + "}]";
+ String payloadA = "{\"Device A\": " + devicePayload + "}";
+
+ String deviceBPayload = "[{\"ts\": 10000, \"values\": " + PAYLOAD_VALUES_STR + "}]";
+ String payloadB = "{\"Device B\": " + deviceBPayload + "}";
+
+ testAckIsReceivedOnFailedPublishMessage("Device A", payloadA.getBytes(), "Device B", payloadB.getBytes());
+ }
+
+ protected void testAckIsReceivedOnFailedPublishMessage(String deviceName1, byte[] payload1, String deviceName2, byte[] payload2) throws Exception {
+ updateDefaultTenantProfileConfig(profileConfiguration -> {
+ profileConfiguration.setMaxDevices(3);
+ });
+
+ MqttTestClient client = new MqttTestClient();
+ client.connectAndWait(gatewayAccessToken);
+ client.publishAndWait(GATEWAY_TELEMETRY_TOPIC, payload1);
+
+ // check device is created
+ await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> {
+ assertNotNull(doGet("/api/tenant/devices?deviceName=" + deviceName1, Device.class));
+ });
+
+ client.publishAndWait(GATEWAY_TELEMETRY_TOPIC, payload2);
+ client.disconnectAndWait();
+
+ // check device was not created due to limit
+ doGet("/api/tenant/devices?deviceName=" + deviceName2).andExpect(status().isNotFound());
+
+ updateDefaultTenantProfileConfig(profileConfiguration -> {
+ profileConfiguration.setMaxDevices(0);
+ });
+ }
+
@Test
public void testGatewayConnect() throws Exception {
String payload = "{\"device\":\"Device A\"}";
diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/telemetry/timeseries/AbstractMqttTimeseriesJsonIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/telemetry/timeseries/AbstractMqttTimeseriesJsonIntegrationTest.java
index a23506a160..92b35d896c 100644
--- a/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/telemetry/timeseries/AbstractMqttTimeseriesJsonIntegrationTest.java
+++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/telemetry/timeseries/AbstractMqttTimeseriesJsonIntegrationTest.java
@@ -186,4 +186,15 @@ public abstract class AbstractMqttTimeseriesJsonIntegrationTest extends Abstract
assertFalse(callback.isPubAckReceived());
}
+ @Override
+ public void testAckIsReceivedOnFailedPublishMessage() throws Exception {
+ MqttTestConfigProperties configProperties = MqttTestConfigProperties.builder()
+ .deviceName("Test Post Telemetry device json payload")
+ .gatewayName("Test Post Telemetry gateway json payload")
+ .transportPayloadType(TransportPayloadType.JSON)
+ .telemetryTopicFilter(POST_DATA_TELEMETRY_TOPIC)
+ .build();
+ processBeforeTest(configProperties);
+ super.testAckIsReceivedOnFailedPublishMessage();
+ }
}
diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/telemetry/timeseries/AbstractMqttTimeseriesProtoIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/telemetry/timeseries/AbstractMqttTimeseriesProtoIntegrationTest.java
index 1b6c247d96..60e5857c04 100644
--- a/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/telemetry/timeseries/AbstractMqttTimeseriesProtoIntegrationTest.java
+++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/telemetry/timeseries/AbstractMqttTimeseriesProtoIntegrationTest.java
@@ -459,6 +459,31 @@ public abstract class AbstractMqttTimeseriesProtoIntegrationTest extends Abstrac
assertFalse(callback.isPubAckReceived());
}
+ @Override
+ public void testAckIsReceivedOnFailedPublishMessage() throws Exception {
+ MqttTestConfigProperties configProperties = MqttTestConfigProperties.builder()
+ .deviceName("Test Post Telemetry device proto payload")
+ .gatewayName("Test Post Telemetry gateway proto payload")
+ .transportPayloadType(TransportPayloadType.PROTOBUF)
+ .telemetryTopicFilter(POST_DATA_TELEMETRY_TOPIC)
+ .build();
+ processBeforeTest(configProperties);
+
+ TransportApiProtos.GatewayTelemetryMsg.Builder gatewayTelemetryMsgProtoBuilder = TransportApiProtos.GatewayTelemetryMsg.newBuilder();
+ List expectedKeys = Arrays.asList("key1", "key2", "key3", "key4", "key5");
+ String deviceName1 = "Device A";
+ String deviceName2 = "Device B";
+ TransportApiProtos.TelemetryMsg deviceATelemetryMsgProto = getDeviceTelemetryMsgProto(deviceName1, expectedKeys, 10000, 20000);
+ gatewayTelemetryMsgProtoBuilder.addAllMsg(List.of(deviceATelemetryMsgProto));
+ TransportApiProtos.GatewayTelemetryMsg payload1 = gatewayTelemetryMsgProtoBuilder.build();
+
+ TransportApiProtos.TelemetryMsg deviceBTelemetryMsgProto = getDeviceTelemetryMsgProto(deviceName2, expectedKeys, 10000, 20000);
+ TransportApiProtos.GatewayTelemetryMsg payload2 = TransportApiProtos.GatewayTelemetryMsg.newBuilder()
+ .addAllMsg(List.of(deviceBTelemetryMsgProto))
+ .build();
+ super.testAckIsReceivedOnFailedPublishMessage(deviceName1, payload1.toByteArray(), deviceName2, payload2.toByteArray());
+ }
+
private DynamicSchema getDynamicSchema() {
DeviceProfileTransportConfiguration transportConfiguration = deviceProfile.getProfileData().getTransportConfiguration();
assertTrue(transportConfiguration instanceof MqttDeviceProfileTransportConfiguration);
diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/ai/AiModelService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/ai/AiModelService.java
index 3ad12048cf..55abc80998 100644
--- a/common/dao-api/src/main/java/org/thingsboard/server/dao/ai/AiModelService.java
+++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/ai/AiModelService.java
@@ -29,6 +29,8 @@ public interface AiModelService extends EntityDaoService {
AiModel save(AiModel model);
+ AiModel save(AiModel model, boolean doValidate);
+
Optional findAiModelById(TenantId tenantId, AiModelId modelId);
PageData findAiModelsByTenantId(TenantId tenantId, PageLink pageLink);
@@ -37,6 +39,8 @@ public interface AiModelService extends EntityDaoService {
FluentFuture> findAiModelByTenantIdAndIdAsync(TenantId tenantId, AiModelId modelId);
+ Optional findAiModelByTenantIdAndName(TenantId tenantId, String name);
+
boolean deleteByTenantIdAndId(TenantId tenantId, AiModelId modelId);
}
diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/asset/AssetService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/asset/AssetService.java
index c22c9c4140..59c17fb4bd 100644
--- a/common/dao-api/src/main/java/org/thingsboard/server/dao/asset/AssetService.java
+++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/asset/AssetService.java
@@ -17,6 +17,7 @@ package org.thingsboard.server.dao.asset;
import com.google.common.util.concurrent.ListenableFuture;
import org.thingsboard.server.common.data.EntitySubtype;
+import org.thingsboard.server.common.data.NameConflictStrategy;
import org.thingsboard.server.common.data.ProfileEntityIdInfo;
import org.thingsboard.server.common.data.asset.Asset;
import org.thingsboard.server.common.data.asset.AssetInfo;
@@ -48,6 +49,8 @@ public interface AssetService extends EntityDaoService {
Asset saveAsset(Asset asset);
+ Asset saveAsset(Asset asset, NameConflictStrategy nameConflictStrategy);
+
Asset assignAssetToCustomer(TenantId tenantId, AssetId assetId, CustomerId customerId);
Asset unassignAssetFromCustomer(TenantId tenantId, AssetId assetId);
diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/customer/CustomerService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/customer/CustomerService.java
index d478e2099a..1cd5ff9ff1 100644
--- a/common/dao-api/src/main/java/org/thingsboard/server/dao/customer/CustomerService.java
+++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/customer/CustomerService.java
@@ -17,6 +17,7 @@ package org.thingsboard.server.dao.customer;
import com.google.common.util.concurrent.ListenableFuture;
import org.thingsboard.server.common.data.Customer;
+import org.thingsboard.server.common.data.NameConflictStrategy;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageData;
@@ -37,6 +38,8 @@ public interface CustomerService extends EntityDaoService {
Customer saveCustomer(Customer customer);
+ Customer saveCustomer(Customer customer, NameConflictStrategy nameConflictStrategy);
+
void deleteCustomer(TenantId tenantId, CustomerId customerId);
Customer findOrCreatePublicCustomer(TenantId tenantId);
diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceService.java
index 9eb258f182..759ab3a708 100644
--- a/common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceService.java
+++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceService.java
@@ -23,6 +23,7 @@ import org.thingsboard.server.common.data.DeviceInfoFilter;
import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.DeviceTransportType;
import org.thingsboard.server.common.data.EntitySubtype;
+import org.thingsboard.server.common.data.NameConflictStrategy;
import org.thingsboard.server.common.data.ProfileEntityIdInfo;
import org.thingsboard.server.common.data.device.DeviceSearchQuery;
import org.thingsboard.server.common.data.id.CustomerId;
@@ -58,8 +59,12 @@ public interface DeviceService extends EntityDaoService {
Device saveDeviceWithAccessToken(Device device, String accessToken);
+ Device saveDeviceWithAccessToken(Device device, String accessToken, NameConflictStrategy nameConflictStrategy);
+
Device saveDeviceWithCredentials(Device device, DeviceCredentials deviceCredentials);
+ Device saveDeviceWithCredentials(Device device, DeviceCredentials deviceCredentials, NameConflictStrategy nameConflictStrategy);
+
Device saveDevice(ProvisionRequest provisionRequest, DeviceProfile profile);
Device assignDeviceToCustomer(TenantId tenantId, DeviceId deviceId, CustomerId customerId);
diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/entityview/EntityViewService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/entityview/EntityViewService.java
index 6e557106f8..b039dbafd1 100644
--- a/common/dao-api/src/main/java/org/thingsboard/server/dao/entityview/EntityViewService.java
+++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/entityview/EntityViewService.java
@@ -19,6 +19,7 @@ import com.google.common.util.concurrent.ListenableFuture;
import org.thingsboard.server.common.data.EntitySubtype;
import org.thingsboard.server.common.data.EntityView;
import org.thingsboard.server.common.data.EntityViewInfo;
+import org.thingsboard.server.common.data.NameConflictStrategy;
import org.thingsboard.server.common.data.entityview.EntityViewSearchQuery;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.EdgeId;
@@ -38,6 +39,8 @@ public interface EntityViewService extends EntityDaoService {
EntityView saveEntityView(EntityView entityView);
+ EntityView saveEntityView(EntityView entityView, NameConflictStrategy nameConflictStrategy);
+
EntityView saveEntityView(EntityView entityView, boolean doValidate);
EntityView assignEntityViewToCustomer(TenantId tenantId, EntityViewId entityViewId, CustomerId customerId);
diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/relation/RelationService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/relation/RelationService.java
index 1db5739e94..a0bc9a72e6 100644
--- a/common/dao-api/src/main/java/org/thingsboard/server/dao/relation/RelationService.java
+++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/relation/RelationService.java
@@ -20,6 +20,7 @@ import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.relation.EntityRelation;
import org.thingsboard.server.common.data.relation.EntityRelationInfo;
+import org.thingsboard.server.common.data.relation.EntityRelationPathQuery;
import org.thingsboard.server.common.data.relation.EntityRelationsQuery;
import org.thingsboard.server.common.data.relation.RelationTypeGroup;
import org.thingsboard.server.common.data.rule.RuleChainType;
@@ -83,6 +84,8 @@ public interface RelationService {
List findRuleNodeToRuleChainRelations(TenantId tenantId, RuleChainType ruleChainType, int limit);
+ ListenableFuture> findByRelationPathQueryAsync(TenantId tenantId, EntityRelationPathQuery relationPathQuery);
+
// TODO: This method may be useful for some validations in the future
// ListenableFuture checkRecursiveRelation(EntityId from, EntityId to);
diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/resource/ResourceService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/resource/ResourceService.java
index ee187db46b..65211ec17a 100644
--- a/common/dao-api/src/main/java/org/thingsboard/server/dao/resource/ResourceService.java
+++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/resource/ResourceService.java
@@ -21,6 +21,7 @@ import org.thingsboard.server.common.data.ResourceExportData;
import org.thingsboard.server.common.data.ResourceSubType;
import org.thingsboard.server.common.data.ResourceType;
import org.thingsboard.server.common.data.TbResource;
+import org.thingsboard.server.common.data.TbResourceDataInfo;
import org.thingsboard.server.common.data.TbResourceDeleteResult;
import org.thingsboard.server.common.data.TbResourceInfo;
import org.thingsboard.server.common.data.TbResourceInfoFilter;
@@ -46,6 +47,8 @@ public interface ResourceService extends EntityDaoService {
byte[] getResourceData(TenantId tenantId, TbResourceId resourceId);
+ TbResourceDataInfo getResourceDataInfo(TenantId tenantId, TbResourceId resourceId);
+
ResourceExportData exportResource(TbResourceInfo resourceInfo);
List exportResources(TenantId tenantId, Collection resources);
@@ -90,4 +93,6 @@ public interface ResourceService extends EntityDaoService {
TbResource createOrUpdateSystemResource(ResourceType resourceType, ResourceSubType resourceSubType, String resourceKey, byte[] data);
+ List findSystemOrTenantResourcesByIds(TenantId tenantId, List resourceIds);
+
}
diff --git a/application/src/main/java/org/thingsboard/server/actors/calculatedField/EntityCalculatedFieldDynamicArgumentsRefreshMsg.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/resource/TbResourceDataCache.java
similarity index 50%
rename from application/src/main/java/org/thingsboard/server/actors/calculatedField/EntityCalculatedFieldDynamicArgumentsRefreshMsg.java
rename to common/dao-api/src/main/java/org/thingsboard/server/dao/resource/TbResourceDataCache.java
index fdf864611f..23485684af 100644
--- a/application/src/main/java/org/thingsboard/server/actors/calculatedField/EntityCalculatedFieldDynamicArgumentsRefreshMsg.java
+++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/resource/TbResourceDataCache.java
@@ -13,25 +13,16 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.thingsboard.server.actors.calculatedField;
+package org.thingsboard.server.dao.resource;
-import lombok.Data;
-import org.thingsboard.server.common.data.id.CalculatedFieldId;
+import com.google.common.util.concurrent.FluentFuture;
+import org.thingsboard.server.common.data.TbResourceDataInfo;
+import org.thingsboard.server.common.data.id.TbResourceId;
import org.thingsboard.server.common.data.id.TenantId;
-import org.thingsboard.server.common.msg.MsgType;
-import org.thingsboard.server.common.msg.ToCalculatedFieldSystemMsg;
-import org.thingsboard.server.common.msg.queue.TbCallback;
-@Data
-public class EntityCalculatedFieldDynamicArgumentsRefreshMsg implements ToCalculatedFieldSystemMsg {
+public interface TbResourceDataCache {
- private final TenantId tenantId;
- private final CalculatedFieldId cfId;
- private final TbCallback callback;
-
- @Override
- public MsgType getMsgType() {
- return MsgType.CF_ENTITY_DYNAMIC_ARGUMENTS_REFRESH_MSG;
- }
+ FluentFuture getResourceDataInfoAsync(TenantId tenantId, TbResourceId resourceId);
+ void evictResourceData(TenantId tenantId, TbResourceId resourceId);
}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/GeneralFileDescriptor.java b/common/data/src/main/java/org/thingsboard/server/common/data/GeneralFileDescriptor.java
new file mode 100644
index 0000000000..94edd4fa01
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/GeneralFileDescriptor.java
@@ -0,0 +1,29 @@
+/**
+ * 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;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.NoArgsConstructor;
+
+@Data
+@EqualsAndHashCode
+@AllArgsConstructor
+@NoArgsConstructor
+public class GeneralFileDescriptor {
+ private String mediaType;
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/NameConflictPolicy.java b/common/data/src/main/java/org/thingsboard/server/common/data/NameConflictPolicy.java
new file mode 100644
index 0000000000..1685dbc933
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/NameConflictPolicy.java
@@ -0,0 +1,23 @@
+/**
+ * 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;
+
+public enum NameConflictPolicy {
+
+ FAIL,
+ UNIQUIFY;
+
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/NameConflictStrategy.java b/common/data/src/main/java/org/thingsboard/server/common/data/NameConflictStrategy.java
new file mode 100644
index 0000000000..9624b8c978
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/NameConflictStrategy.java
@@ -0,0 +1,25 @@
+/**
+ * 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;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+
+@Schema
+public record NameConflictStrategy(NameConflictPolicy policy, String separator, UniquifyStrategy uniquifyStrategy) {
+
+ public static final NameConflictStrategy DEFAULT = new NameConflictStrategy(NameConflictPolicy.FAIL, null, null);
+
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ResourceType.java b/common/data/src/main/java/org/thingsboard/server/common/data/ResourceType.java
index 77b17198e9..f7579b6878 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/ResourceType.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/ResourceType.java
@@ -25,7 +25,8 @@ public enum ResourceType {
PKCS_12("application/x-pkcs12", false, false),
JS_MODULE("application/javascript", true, true),
IMAGE(null, true, true),
- DASHBOARD("application/json", true, true);
+ DASHBOARD("application/json", true, true),
+ GENERAL(null, false, true);
@Getter
private final String mediaType;
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 ba37067106..457d30e263 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
@@ -16,6 +16,7 @@
package org.thingsboard.server.common.data;
import com.fasterxml.jackson.annotation.JsonGetter;
+import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonSetter;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@@ -86,6 +87,11 @@ public class TbResource extends TbResourceInfo {
.orElse(null);
}
+ @JsonIgnore
+ public TbResourceDataInfo toResourceDataInfo() {
+ return new TbResourceDataInfo(data, getDescriptor());
+ }
+
@Override
public String toString() {
return super.toString();
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/TbResourceDataInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/TbResourceDataInfo.java
new file mode 100644
index 0000000000..039478470d
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/TbResourceDataInfo.java
@@ -0,0 +1,31 @@
+/**
+ * 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;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+public class TbResourceDataInfo {
+
+ private byte[] data;
+ private JsonNode descriptor;
+
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/TbResourceDeleteResult.java b/common/data/src/main/java/org/thingsboard/server/common/data/TbResourceDeleteResult.java
index edc5a2f539..76945a97ed 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/TbResourceDeleteResult.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/TbResourceDeleteResult.java
@@ -17,7 +17,6 @@ package org.thingsboard.server.common.data;
import lombok.Builder;
import lombok.Data;
-import org.thingsboard.server.common.data.id.HasId;
import java.util.List;
import java.util.Map;
@@ -27,6 +26,6 @@ import java.util.Map;
public class TbResourceDeleteResult {
private boolean success;
- private Map>> references;
+ private Map> references;
}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/UniquifyStrategy.java b/common/data/src/main/java/org/thingsboard/server/common/data/UniquifyStrategy.java
new file mode 100644
index 0000000000..5c9841f096
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/UniquifyStrategy.java
@@ -0,0 +1,23 @@
+/**
+ * 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;
+
+public enum UniquifyStrategy {
+
+ RANDOM,
+ INCREMENTAL;
+
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/dto/TbChatResponse.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/dto/TbChatResponse.java
index 2cc17e4553..73e6557fb5 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/dto/TbChatResponse.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/dto/TbChatResponse.java
@@ -22,7 +22,7 @@ import io.swagger.v3.oas.annotations.media.Schema;
@JsonTypeInfo(
use = JsonTypeInfo.Id.NAME,
property = "status",
- include = JsonTypeInfo.As.PROPERTY,
+ include = JsonTypeInfo.As.EXISTING_PROPERTY,
visible = true
)
@JsonSubTypes({
@@ -51,9 +51,7 @@ public sealed interface TbChatResponse permits TbChatResponse.Success, TbChatRes
}
record Failure(
- @Schema(
- description = "A string containing details about the failure"
- )
+ @Schema(description = "A string containing details about the failure")
String errorDetails
) implements TbChatResponse {
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 bfaa29a6e3..f18429e7cf 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
@@ -24,6 +24,7 @@ import org.thingsboard.server.common.data.ai.model.chat.GitHubModelsChatModelCon
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.OllamaChatModelConfig;
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;
@@ -34,6 +35,7 @@ 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.OllamaProviderConfig;
import org.thingsboard.server.common.data.ai.provider.OpenAiProviderConfig;
@JsonTypeInfo(
@@ -50,7 +52,8 @@ import org.thingsboard.server.common.data.ai.provider.OpenAiProviderConfig;
@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")
+ @JsonSubTypes.Type(value = GitHubModelsChatModelConfig.class, name = "GITHUB_MODELS"),
+ @JsonSubTypes.Type(value = OllamaChatModelConfig.class, name = "OLLAMA")
})
public interface AiModelConfig {
@@ -69,7 +72,8 @@ public interface AiModelConfig {
@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")
+ @JsonSubTypes.Type(value = GitHubModelsProviderConfig.class, name = "GITHUB_MODELS"),
+ @JsonSubTypes.Type(value = OllamaProviderConfig.class, name = "OLLAMA")
})
AiProviderConfig providerConfig();
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 2bc28cfce0..49126c1861 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
@@ -24,7 +24,7 @@ public sealed interface AiChatModelConfig> extend
permits
OpenAiChatModelConfig, AzureOpenAiChatModelConfig, GoogleAiGeminiChatModelConfig,
GoogleVertexAiGeminiChatModelConfig, MistralAiChatModelConfig, AnthropicChatModelConfig,
- AmazonBedrockChatModelConfig, GitHubModelsChatModelConfig {
+ AmazonBedrockChatModelConfig, GitHubModelsChatModelConfig, OllamaChatModelConfig {
ChatModel configure(Langchain4jChatModelConfigurer configurer);
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AmazonBedrockChatModelConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AmazonBedrockChatModelConfig.java
index 2bb4de5aa8..d2ab72086a 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AmazonBedrockChatModelConfig.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AmazonBedrockChatModelConfig.java
@@ -33,7 +33,7 @@ public record AmazonBedrockChatModelConfig(
@NotBlank String modelId,
@PositiveOrZero Double temperature,
@Positive @Max(1) Double topP,
- @Positive Integer maxOutputTokens,
+ Integer maxOutputTokens,
@With @Positive Integer timeoutSeconds,
@With @PositiveOrZero Integer maxRetries
) implements AiChatModelConfig {
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AnthropicChatModelConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AnthropicChatModelConfig.java
index 69b5578fb3..6d505f75a6 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AnthropicChatModelConfig.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AnthropicChatModelConfig.java
@@ -34,7 +34,7 @@ public record AnthropicChatModelConfig(
@PositiveOrZero Double temperature,
@Positive @Max(1) Double topP,
@PositiveOrZero Integer topK,
- @Positive Integer maxOutputTokens,
+ Integer maxOutputTokens,
@With @Positive Integer timeoutSeconds,
@With @PositiveOrZero Integer maxRetries
) implements AiChatModelConfig {
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AzureOpenAiChatModelConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AzureOpenAiChatModelConfig.java
index 47e7e96c37..f70f2af539 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AzureOpenAiChatModelConfig.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AzureOpenAiChatModelConfig.java
@@ -35,7 +35,7 @@ public record AzureOpenAiChatModelConfig(
@Positive @Max(1) Double topP,
Double frequencyPenalty,
Double presencePenalty,
- @Positive Integer maxOutputTokens,
+ Integer maxOutputTokens,
@With @Positive Integer timeoutSeconds,
@With @PositiveOrZero Integer maxRetries
) implements AiChatModelConfig {
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GitHubModelsChatModelConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GitHubModelsChatModelConfig.java
index b509254f77..0aafd72197 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GitHubModelsChatModelConfig.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GitHubModelsChatModelConfig.java
@@ -35,7 +35,7 @@ public record GitHubModelsChatModelConfig(
@Positive @Max(1) Double topP,
Double frequencyPenalty,
Double presencePenalty,
- @Positive Integer maxOutputTokens,
+ Integer maxOutputTokens,
@With @Positive Integer timeoutSeconds,
@With @PositiveOrZero Integer maxRetries
) implements AiChatModelConfig {
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleAiGeminiChatModelConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleAiGeminiChatModelConfig.java
index fe11a11460..b5c3d4263d 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleAiGeminiChatModelConfig.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleAiGeminiChatModelConfig.java
@@ -36,7 +36,7 @@ public record GoogleAiGeminiChatModelConfig(
@PositiveOrZero Integer topK,
Double frequencyPenalty,
Double presencePenalty,
- @Positive Integer maxOutputTokens,
+ Integer maxOutputTokens,
@With @Positive Integer timeoutSeconds,
@With @PositiveOrZero Integer maxRetries
) implements AiChatModelConfig {
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleVertexAiGeminiChatModelConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleVertexAiGeminiChatModelConfig.java
index 609e14f86e..944963ee27 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleVertexAiGeminiChatModelConfig.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleVertexAiGeminiChatModelConfig.java
@@ -36,7 +36,7 @@ public record GoogleVertexAiGeminiChatModelConfig(
@PositiveOrZero Integer topK,
Double frequencyPenalty,
Double presencePenalty,
- @Positive Integer maxOutputTokens,
+ Integer maxOutputTokens,
@With @Positive Integer timeoutSeconds,
@With @PositiveOrZero Integer maxRetries
) implements AiChatModelConfig {
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 c9c1bc3173..828256dcdc 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
@@ -35,4 +35,6 @@ public interface Langchain4jChatModelConfigurer {
ChatModel configureChatModel(GitHubModelsChatModelConfig chatModelConfig);
+ ChatModel configureChatModel(OllamaChatModelConfig chatModelConfig);
+
}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/MistralAiChatModelConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/MistralAiChatModelConfig.java
index f603e99c53..8f67d93398 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/MistralAiChatModelConfig.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/MistralAiChatModelConfig.java
@@ -35,7 +35,7 @@ public record MistralAiChatModelConfig(
@Positive @Max(1) Double topP,
Double frequencyPenalty,
Double presencePenalty,
- @Positive Integer maxOutputTokens,
+ Integer maxOutputTokens,
@With @Positive Integer timeoutSeconds,
@With @PositiveOrZero Integer maxRetries
) implements AiChatModelConfig {
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/OllamaChatModelConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/OllamaChatModelConfig.java
new file mode 100644
index 0000000000..ea48670b63
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/OllamaChatModelConfig.java
@@ -0,0 +1,58 @@
+/**
+ * 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 jakarta.validation.Valid;
+import jakarta.validation.constraints.Max;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
+import jakarta.validation.constraints.Positive;
+import jakarta.validation.constraints.PositiveOrZero;
+import lombok.Builder;
+import lombok.With;
+import org.thingsboard.server.common.data.ai.provider.AiProvider;
+import org.thingsboard.server.common.data.ai.provider.OllamaProviderConfig;
+
+@Builder
+public record OllamaChatModelConfig(
+ @NotNull @Valid OllamaProviderConfig providerConfig,
+ @NotBlank String modelId,
+ @PositiveOrZero Double temperature,
+ @Positive @Max(1) Double topP,
+ @PositiveOrZero Integer topK,
+ Integer contextLength,
+ Integer maxOutputTokens,
+ @With @Positive Integer timeoutSeconds,
+ @With @PositiveOrZero Integer maxRetries
+) implements AiChatModelConfig {
+
+ @Override
+ public AiProvider provider() {
+ return AiProvider.OLLAMA;
+ }
+
+ @Override
+ public ChatModel configure(Langchain4jChatModelConfigurer configurer) {
+ return configurer.configureChatModel(this);
+ }
+
+ @Override
+ public boolean supportsJsonMode() {
+ return true;
+ }
+
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/OpenAiChatModelConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/OpenAiChatModelConfig.java
index 00b5115d7d..23db9accc2 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/OpenAiChatModelConfig.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/OpenAiChatModelConfig.java
@@ -35,7 +35,7 @@ public record OpenAiChatModelConfig(
@Positive @Max(1) Double topP,
Double frequencyPenalty,
Double presencePenalty,
- @Positive Integer maxOutputTokens,
+ Integer maxOutputTokens,
@With @Positive Integer timeoutSeconds,
@With @PositiveOrZero Integer maxRetries
) implements AiChatModelConfig {
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/AiProvider.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/AiProvider.java
index d0a5bd0510..a9a6af4de8 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/AiProvider.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/AiProvider.java
@@ -24,6 +24,7 @@ public enum AiProvider {
MISTRAL_AI,
ANTHROPIC,
AMAZON_BEDROCK,
- GITHUB_MODELS
+ GITHUB_MODELS,
+ OLLAMA
}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/AiProviderConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/AiProviderConfig.java
index bd32c88efb..5423b24410 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/AiProviderConfig.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/AiProviderConfig.java
@@ -19,4 +19,4 @@ public sealed interface AiProviderConfig
permits
OpenAiProviderConfig, AzureOpenAiProviderConfig, GoogleAiGeminiProviderConfig,
GoogleVertexAiGeminiProviderConfig, MistralAiProviderConfig, AnthropicProviderConfig,
- AmazonBedrockProviderConfig, GitHubModelsProviderConfig {}
+ AmazonBedrockProviderConfig, GitHubModelsProviderConfig, OllamaProviderConfig {}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/OllamaProviderConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/OllamaProviderConfig.java
new file mode 100644
index 0000000000..39bb57834c
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/OllamaProviderConfig.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.common.data.ai.provider;
+
+import com.fasterxml.jackson.annotation.JsonSubTypes;
+import com.fasterxml.jackson.annotation.JsonTypeInfo;
+import jakarta.validation.Valid;
+import jakarta.validation.constraints.NotNull;
+
+public record OllamaProviderConfig(
+ @NotNull String baseUrl,
+ @NotNull @Valid OllamaAuth auth
+) implements AiProviderConfig {
+
+ @JsonTypeInfo(
+ use = JsonTypeInfo.Id.NAME,
+ include = JsonTypeInfo.As.PROPERTY,
+ property = "type"
+ )
+ @JsonSubTypes({
+ @JsonSubTypes.Type(value = OllamaAuth.None.class, name = "NONE"),
+ @JsonSubTypes.Type(value = OllamaAuth.Basic.class, name = "BASIC"),
+ @JsonSubTypes.Type(value = OllamaAuth.Token.class, name = "TOKEN")
+ })
+ public sealed interface OllamaAuth {
+
+ record None() implements OllamaAuth {}
+
+ record Basic(@NotNull String username, @NotNull String password) implements OllamaAuth {}
+
+ record Token(@NotNull String token) implements OllamaAuth {}
+
+ }
+
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/OpenAiProviderConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/OpenAiProviderConfig.java
index 09ffda837b..f7864db6e3 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/OpenAiProviderConfig.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/OpenAiProviderConfig.java
@@ -15,8 +15,32 @@
*/
package org.thingsboard.server.common.data.ai.provider;
-import jakarta.validation.constraints.NotNull;
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import jakarta.validation.constraints.AssertTrue;
+import lombok.Builder;
+import org.apache.commons.lang3.StringUtils;
+import java.util.Objects;
+
+@Builder
public record OpenAiProviderConfig(
- @NotNull String apiKey
-) implements AiProviderConfig {}
+ String baseUrl,
+ String apiKey
+) implements AiProviderConfig {
+
+ public static final String OPENAI_OFFICIAL_BASE_URL = "https://api.openai.com/v1";
+
+ public OpenAiProviderConfig {
+ baseUrl = Objects.requireNonNullElse(baseUrl, OPENAI_OFFICIAL_BASE_URL);
+ }
+
+ @JsonIgnore
+ @AssertTrue(message = "API key is required when using the official OpenAI API")
+ public boolean isValid() {
+ if (baseUrl.equals(OPENAI_OFFICIAL_BASE_URL)) {
+ return StringUtils.isNotBlank(apiKey);
+ }
+ return true;
+ }
+
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmInfo.java
index 7a80a09891..b316a2ef50 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmInfo.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmInfo.java
@@ -38,6 +38,11 @@ public class AlarmInfo extends Alarm {
@Schema(description = "Alarm originator label", example = "Thermostat label")
private String originatorLabel;
+ @Getter
+ @Setter
+ @Schema(description = "Originator display name", example = "Thermostat")
+ private String originatorDisplayName;
+
@Getter
@Setter
@Schema(description = "Alarm assignee")
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/CFArgumentDynamicSourceType.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/CFArgumentDynamicSourceType.java
index bd2e9b0c00..dc52287f3e 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/CFArgumentDynamicSourceType.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/CFArgumentDynamicSourceType.java
@@ -17,6 +17,6 @@ package org.thingsboard.server.common.data.cf.configuration;
public enum CFArgumentDynamicSourceType {
- RELATION_QUERY
+ RELATION_PATH_QUERY
}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/CfArgumentDynamicSourceConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/CfArgumentDynamicSourceConfiguration.java
index f36071615e..ff746161e9 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/CfArgumentDynamicSourceConfiguration.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/CfArgumentDynamicSourceConfiguration.java
@@ -26,7 +26,7 @@ import com.fasterxml.jackson.annotation.JsonTypeInfo;
property = "type"
)
@JsonSubTypes({
- @JsonSubTypes.Type(value = RelationQueryDynamicSourceConfiguration.class, name = "RELATION_QUERY")
+ @JsonSubTypes.Type(value = RelationPathQueryDynamicSourceConfiguration.class, name = "RELATION_PATH_QUERY")
})
@JsonIgnoreProperties(ignoreUnknown = true)
public interface CfArgumentDynamicSourceConfiguration {
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/RelationPathQueryDynamicSourceConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/RelationPathQueryDynamicSourceConfiguration.java
new file mode 100644
index 0000000000..dc92ff3685
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/RelationPathQueryDynamicSourceConfiguration.java
@@ -0,0 +1,71 @@
+/**
+ * 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.cf.configuration;
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import lombok.Data;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.relation.EntityRelation;
+import org.thingsboard.server.common.data.relation.EntityRelationPathQuery;
+import org.thingsboard.server.common.data.relation.EntitySearchDirection;
+import org.thingsboard.server.common.data.relation.RelationPathLevel;
+import org.thingsboard.server.common.data.util.CollectionsUtil;
+
+import java.util.List;
+import java.util.NoSuchElementException;
+
+@Data
+public class RelationPathQueryDynamicSourceConfiguration implements CfArgumentDynamicSourceConfiguration {
+
+ private List levels;
+
+ @Override
+ public CFArgumentDynamicSourceType getType() {
+ return CFArgumentDynamicSourceType.RELATION_PATH_QUERY;
+ }
+
+ @Override
+ public void validate() {
+ if (CollectionsUtil.isEmpty(levels)) {
+ throw new IllegalArgumentException("At least one relation level must be specified!");
+ }
+ levels.forEach(RelationPathLevel::validate);
+ }
+
+ public List resolveEntityIds(List relations) {
+ EntitySearchDirection lastLevelDirection = getLastLevel().direction();
+ return switch (lastLevelDirection) {
+ case FROM -> relations.stream().map(EntityRelation::getTo).toList();
+ case TO -> relations.stream().map(EntityRelation::getFrom).toList();
+ };
+ }
+
+ public void validateMaxRelationLevel(String argumentName, int maxAllowedRelationLevel) {
+ if (levels.size() > maxAllowedRelationLevel) {
+ throw new IllegalArgumentException("Max relation level is greater than configured " +
+ "maximum allowed relation level in tenant profile: " + maxAllowedRelationLevel + " for argument: " + argumentName);
+ }
+ }
+
+ public EntityRelationPathQuery toRelationPathQuery(EntityId entityId) {
+ return new EntityRelationPathQuery(entityId, levels);
+ }
+
+ private RelationPathLevel getLastLevel() {
+ return levels.get(levels.size() - 1);
+ }
+
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/RelationQueryDynamicSourceConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/RelationQueryDynamicSourceConfiguration.java
deleted file mode 100644
index 4e9b4252c9..0000000000
--- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/RelationQueryDynamicSourceConfiguration.java
+++ /dev/null
@@ -1,86 +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.cf.configuration;
-
-import com.fasterxml.jackson.annotation.JsonIgnore;
-import lombok.Data;
-import org.thingsboard.server.common.data.StringUtils;
-import org.thingsboard.server.common.data.id.EntityId;
-import org.thingsboard.server.common.data.relation.EntityRelation;
-import org.thingsboard.server.common.data.relation.EntityRelationsQuery;
-import org.thingsboard.server.common.data.relation.EntitySearchDirection;
-import org.thingsboard.server.common.data.relation.RelationEntityTypeFilter;
-import org.thingsboard.server.common.data.relation.RelationsSearchParameters;
-
-import java.util.Collections;
-import java.util.List;
-
-@Data
-public class RelationQueryDynamicSourceConfiguration implements CfArgumentDynamicSourceConfiguration {
-
- private int maxLevel;
- private boolean fetchLastLevelOnly;
- private EntitySearchDirection direction;
- private String relationType;
-
- @Override
- public CFArgumentDynamicSourceType getType() {
- return CFArgumentDynamicSourceType.RELATION_QUERY;
- }
-
- @Override
- public void validate() {
- if (maxLevel < 1) {
- throw new IllegalArgumentException("Relation query dynamic source configuration max relation level can't be less than 1!");
- }
- if (direction == null) {
- throw new IllegalArgumentException("Relation query dynamic source configuration direction must be specified!");
- }
- if (StringUtils.isBlank(relationType)) {
- throw new IllegalArgumentException("Relation query dynamic source configuration relation type must be specified!");
- }
- }
-
- @JsonIgnore
- public boolean isSimpleRelation() {
- return maxLevel == 1;
- }
-
- public void validateMaxRelationLevel(String argumentName, int maxAllowedRelationLevel) {
- if (maxLevel > maxAllowedRelationLevel) {
- throw new IllegalArgumentException("Max relation level is greater than configured " +
- "maximum allowed relation level in tenant profile: " + maxAllowedRelationLevel + " for argument: " + argumentName);
- }
- }
-
- public EntityRelationsQuery toEntityRelationsQuery(EntityId rootEntityId) {
- if (isSimpleRelation()) {
- throw new IllegalArgumentException("Entity relations query can't be created for a simple relation!");
- }
- var entityRelationsQuery = new EntityRelationsQuery();
- entityRelationsQuery.setParameters(new RelationsSearchParameters(rootEntityId, direction, maxLevel, fetchLastLevelOnly));
- entityRelationsQuery.setFilters(Collections.singletonList(new RelationEntityTypeFilter(relationType, Collections.emptyList())));
- return entityRelationsQuery;
- }
-
- public List resolveEntityIds(List relations) {
- return switch (direction) {
- case FROM -> relations.stream().map(EntityRelation::getTo).toList();
- case TO -> relations.stream().map(EntityRelation::getFrom).toList();
- };
- }
-
-}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/ScheduledUpdateSupportedCalculatedFieldConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/ScheduledUpdateSupportedCalculatedFieldConfiguration.java
index 7902a9cf5b..d0c5786f62 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/ScheduledUpdateSupportedCalculatedFieldConfiguration.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/ScheduledUpdateSupportedCalculatedFieldConfiguration.java
@@ -15,11 +15,8 @@
*/
package org.thingsboard.server.common.data.cf.configuration;
-import com.fasterxml.jackson.annotation.JsonIgnore;
-
public interface ScheduledUpdateSupportedCalculatedFieldConfiguration extends CalculatedFieldConfiguration {
- @JsonIgnore
boolean isScheduledUpdateEnabled();
int getScheduledUpdateInterval();
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/geofencing/GeofencingCalculatedFieldConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/geofencing/GeofencingCalculatedFieldConfiguration.java
index dc331f5876..b331abc50b 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/geofencing/GeofencingCalculatedFieldConfiguration.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/geofencing/GeofencingCalculatedFieldConfiguration.java
@@ -34,6 +34,8 @@ public class GeofencingCalculatedFieldConfiguration implements ArgumentsBasedCal
private EntityCoordinates entityCoordinates;
private Map zoneGroups;
+
+ private boolean scheduledUpdateEnabled;
private int scheduledUpdateInterval;
private Output output;
@@ -61,11 +63,6 @@ public class GeofencingCalculatedFieldConfiguration implements ArgumentsBasedCal
return output;
}
- @Override
- public boolean isScheduledUpdateEnabled() {
- return scheduledUpdateInterval > 0 && zoneGroups.values().stream().anyMatch(ZoneGroupConfiguration::hasDynamicSource);
- }
-
@Override
public void validate() {
if (entityCoordinates == null) {
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 0d5c3f34ad..fadb44207a 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,8 @@ public enum EdgeEventType {
TB_RESOURCE(true, EntityType.TB_RESOURCE),
OAUTH2_CLIENT(true, EntityType.OAUTH2_CLIENT),
DOMAIN(true, EntityType.DOMAIN),
- CALCULATED_FIELD(false, EntityType.CALCULATED_FIELD);
+ CALCULATED_FIELD(false, EntityType.CALCULATED_FIELD),
+ AI_MODEL(true, EntityType.AI_MODEL);
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 2a7a4f03e4..8eca53507c 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
@@ -114,6 +114,7 @@ public class EntityIdFactory {
case OAUTH2_CLIENT -> new OAuth2ClientId(uuid);
case DOMAIN -> new DomainId(uuid);
case CALCULATED_FIELD -> new CalculatedFieldId(uuid);
+ case AI_MODEL -> new AiModelId(uuid);
default -> throw new IllegalArgumentException("EdgeEventType " + edgeEventType + " is not supported!");
};
}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/bundle/MobileAppBundle.java b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/bundle/MobileAppBundle.java
index 2190972d7e..b023ba89c3 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/bundle/MobileAppBundle.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/bundle/MobileAppBundle.java
@@ -30,6 +30,7 @@ import org.thingsboard.server.common.data.id.MobileAppId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.mobile.layout.MobileLayoutConfig;
import org.thingsboard.server.common.data.validation.Length;
+import org.thingsboard.server.common.data.validation.NoXss;
@EqualsAndHashCode(callSuper = true)
@Data
@@ -40,9 +41,11 @@ public class MobileAppBundle extends BaseData implements HasT
private TenantId tenantId;
@Schema(description = "Application bundle title. Cannot be empty", requiredMode = Schema.RequiredMode.REQUIRED)
@NotBlank
+ @NoXss
@Length(fieldName = "title")
private String title;
@Schema(description = "Application bundle description.")
+ @NoXss
@Length(fieldName = "description")
private String description;
@Schema(description = "Android application id")
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/NotificationRule.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/NotificationRule.java
index 81b5e0ccfb..fe87e0f966 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/NotificationRule.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/NotificationRule.java
@@ -62,6 +62,7 @@ public class NotificationRule extends BaseData implements Ha
@Valid
private NotificationRuleRecipientsConfig recipientsConfig;
+ @Valid
private NotificationRuleConfig additionalConfig;
private NotificationRuleId externalId;
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/NotificationRuleConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/NotificationRuleConfig.java
index 9103086b7c..013c0ae662 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/NotificationRuleConfig.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/NotificationRuleConfig.java
@@ -16,12 +16,14 @@
package org.thingsboard.server.common.data.notification.rule;
import lombok.Data;
+import org.thingsboard.server.common.data.validation.NoXss;
import java.io.Serializable;
@Data
public class NotificationRuleConfig implements Serializable {
+ @NoXss
private String description;
}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/relation/EntityRelationPathQuery.java b/common/data/src/main/java/org/thingsboard/server/common/data/relation/EntityRelationPathQuery.java
new file mode 100644
index 0000000000..a81e3e0c86
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/relation/EntityRelationPathQuery.java
@@ -0,0 +1,24 @@
+/**
+ * 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.relation;
+
+import org.thingsboard.server.common.data.id.EntityId;
+
+import java.util.List;
+
+public record EntityRelationPathQuery(EntityId rootEntityId, List levels) {
+
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/relation/RelationPathLevel.java b/common/data/src/main/java/org/thingsboard/server/common/data/relation/RelationPathLevel.java
new file mode 100644
index 0000000000..c28135204f
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/relation/RelationPathLevel.java
@@ -0,0 +1,30 @@
+/**
+ * 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.relation;
+
+import org.thingsboard.server.common.data.StringUtils;
+
+public record RelationPathLevel(EntitySearchDirection direction, String relationType) {
+
+ public void validate() {
+ if (direction == null) {
+ throw new IllegalArgumentException("Direction must be specified!");
+ }
+ if (StringUtils.isBlank(relationType)) {
+ throw new IllegalArgumentException("Relation type must be specified!");
+ }
+ }
+}
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 4c8b9e06bd..c6bd9a7f38 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
@@ -173,7 +173,7 @@ public class DefaultTenantProfileConfiguration implements TenantProfileConfigura
@Schema(example = "10")
private long maxArgumentsPerCF = 10;
@Schema(example = "3600")
- private int minAllowedScheduledUpdateIntervalInSecForCF = 3600;
+ private int minAllowedScheduledUpdateIntervalInSecForCF = 60;
@Schema(example = "10")
private int maxRelationLevelPerCfArgument = 10;
@Builder.Default
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/util/TypeCastUtil.java b/common/data/src/main/java/org/thingsboard/server/common/data/util/TypeCastUtil.java
index 85071b3cc9..82de579d3d 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/util/TypeCastUtil.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/util/TypeCastUtil.java
@@ -15,6 +15,7 @@
*/
package org.thingsboard.server.common.data.util;
+import com.google.gson.JsonParser;
import org.apache.commons.lang3.math.NumberUtils;
import org.apache.commons.lang3.tuple.Pair;
import org.thingsboard.server.common.data.kv.DataType;
@@ -40,6 +41,11 @@ public class TypeCastUtil {
} catch (RuntimeException ignored) {}
} else if (value.equalsIgnoreCase("true") || value.equalsIgnoreCase("false")) {
return Pair.of(DataType.BOOLEAN, Boolean.parseBoolean(value));
+ } else if (looksLikeJson(value)) {
+ try {
+ return Pair.of(DataType.JSON, JsonParser.parseString(value));
+ } catch (Exception ignored) {
+ }
}
return Pair.of(DataType.STRING, value);
}
@@ -70,4 +76,10 @@ public class TypeCastUtil {
return valueAsString.contains(".") && !valueAsString.contains("E") && !valueAsString.contains("e");
}
+ private static boolean looksLikeJson(String value) {
+ String trimmed = value.trim();
+ return (trimmed.startsWith("{") && trimmed.endsWith("}")) ||
+ (trimmed.startsWith("[") && trimmed.endsWith("]"));
+ }
+
}
diff --git a/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/ArgumentTest.java b/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/ArgumentTest.java
index fd59317649..d1108e9eed 100644
--- a/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/ArgumentTest.java
+++ b/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/ArgumentTest.java
@@ -31,7 +31,7 @@ public class ArgumentTest {
@Test
void validateShouldReturnTrueIfDynamicSourceConfigurationIsNotNull() {
var argument = new Argument();
- argument.setRefDynamicSourceConfiguration(new RelationQueryDynamicSourceConfiguration());
+ argument.setRefDynamicSourceConfiguration(new RelationPathQueryDynamicSourceConfiguration());
assertThat(argument.hasDynamicSource()).isTrue();
}
diff --git a/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/RelationPathQueryDynamicSourceConfigurationTest.java b/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/RelationPathQueryDynamicSourceConfigurationTest.java
new file mode 100644
index 0000000000..1a6ea5de3b
--- /dev/null
+++ b/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/RelationPathQueryDynamicSourceConfigurationTest.java
@@ -0,0 +1,126 @@
+/**
+ * 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.cf.configuration;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.NullAndEmptySource;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.relation.EntityRelation;
+import org.thingsboard.server.common.data.relation.EntitySearchDirection;
+import org.thingsboard.server.common.data.relation.RelationPathLevel;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatCode;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(MockitoExtension.class)
+public class RelationPathQueryDynamicSourceConfigurationTest {
+
+ @Test
+ void typeShouldBeRelationQuery() {
+ var cfg = new RelationPathQueryDynamicSourceConfiguration();
+ assertThat(cfg.getType()).isEqualTo(CFArgumentDynamicSourceType.RELATION_PATH_QUERY);
+ }
+
+ @ParameterizedTest
+ @NullAndEmptySource
+ void validateShouldThrowWhenLevelsIsNull(List levels) {
+ var cfg = new RelationPathQueryDynamicSourceConfiguration();
+ cfg.setLevels(levels);
+
+ assertThatThrownBy(cfg::validate)
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessage("At least one relation level must be specified!");
+ }
+
+ @Test
+ void validateShouldCallValidateForPathLevels() {
+ List levels = new ArrayList<>();
+
+ RelationPathLevel lvl1 = mock(RelationPathLevel.class);
+ RelationPathLevel lvl2 = mock(RelationPathLevel.class);
+ levels.add(lvl1);
+ levels.add(lvl2);
+
+ var cfg = new RelationPathQueryDynamicSourceConfiguration();
+ cfg.setLevels(levels);
+
+ assertThatCode(cfg::validate).doesNotThrowAnyException();
+
+ verify(lvl1).validate();
+ verify(lvl2).validate();
+ }
+
+ @Test
+ void resolveEntityIds_whenDirectionFROM_thenReturnsToIds() {
+ List levels = new ArrayList<>();
+
+ RelationPathLevel lvl1 = mock(RelationPathLevel.class);
+ RelationPathLevel lvl2 = mock(RelationPathLevel.class);
+ levels.add(lvl1);
+ levels.add(lvl2);
+
+ when(lvl2.direction()).thenReturn(EntitySearchDirection.FROM);
+
+ EntityRelation rel1 = mock(EntityRelation.class);
+ EntityRelation rel2 = mock(EntityRelation.class);
+
+ when(rel1.getTo()).thenReturn(mock(EntityId.class));
+ when(rel2.getTo()).thenReturn(mock(EntityId.class));
+
+ var cfg = new RelationPathQueryDynamicSourceConfiguration();
+ cfg.setLevels(levels);
+
+ var out = cfg.resolveEntityIds(List.of(rel1, rel2));
+
+ assertThat(out).containsExactly(rel1.getTo(), rel2.getTo());
+ }
+
+ @Test
+ void resolveEntityIds_whenDirectionTO_thenReturnsFromIds() {
+ List levels = new ArrayList<>();
+
+ RelationPathLevel lvl1 = mock(RelationPathLevel.class);
+ RelationPathLevel lvl2 = mock(RelationPathLevel.class);
+ levels.add(lvl1);
+ levels.add(lvl2);
+
+ when(lvl2.direction()).thenReturn(EntitySearchDirection.TO);
+
+ EntityRelation rel1 = mock(EntityRelation.class);
+ EntityRelation rel2 = mock(EntityRelation.class);
+
+ when(rel1.getFrom()).thenReturn(mock(EntityId.class));
+ when(rel2.getFrom()).thenReturn(mock(EntityId.class));
+
+ var cfg = new RelationPathQueryDynamicSourceConfiguration();
+ cfg.setLevels(levels);
+
+ var out = cfg.resolveEntityIds(List.of(rel1, rel2));
+
+ assertThat(out).containsExactly(rel1.getFrom(), rel2.getFrom());
+ }
+
+}
diff --git a/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/RelationQueryDynamicSourceConfigurationTest.java b/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/RelationQueryDynamicSourceConfigurationTest.java
deleted file mode 100644
index 86fa52ba66..0000000000
--- a/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/RelationQueryDynamicSourceConfigurationTest.java
+++ /dev/null
@@ -1,216 +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.cf.configuration;
-
-import org.junit.jupiter.api.Test;
-import org.junit.jupiter.api.extension.ExtendWith;
-import org.junit.jupiter.params.ParameterizedTest;
-import org.junit.jupiter.params.provider.NullAndEmptySource;
-import org.junit.jupiter.params.provider.ValueSource;
-import org.mockito.Mock;
-import org.mockito.junit.jupiter.MockitoExtension;
-import org.thingsboard.server.common.data.EntityType;
-import org.thingsboard.server.common.data.id.EntityId;
-import org.thingsboard.server.common.data.relation.EntityRelation;
-import org.thingsboard.server.common.data.relation.EntitySearchDirection;
-import org.thingsboard.server.common.data.relation.RelationEntityTypeFilter;
-import org.thingsboard.server.common.data.relation.RelationsSearchParameters;
-
-import java.util.List;
-
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.assertj.core.api.Assertions.assertThatCode;
-import static org.assertj.core.api.Assertions.assertThatThrownBy;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.when;
-
-@ExtendWith(MockitoExtension.class)
-public class RelationQueryDynamicSourceConfigurationTest {
-
- @Mock
- EntityId rootEntityId;
-
- @Mock
- EntityRelation rel1;
- @Mock
- EntityRelation rel2;
-
- @Test
- void typeShouldBeRelationQuery() {
- var cfg = new RelationQueryDynamicSourceConfiguration();
- assertThat(cfg.getType()).isEqualTo(CFArgumentDynamicSourceType.RELATION_QUERY);
- }
-
- @Test
- void validateShouldThrowWhenMaxLevelLessThanOne() {
- var cfg = new RelationQueryDynamicSourceConfiguration();
- cfg.setMaxLevel(0);
- cfg.setDirection(EntitySearchDirection.FROM);
- cfg.setRelationType(EntityRelation.CONTAINS_TYPE);
-
- assertThatThrownBy(cfg::validate)
- .isInstanceOf(IllegalArgumentException.class)
- .hasMessage("Relation query dynamic source configuration max relation level can't be less than 1!");
- }
-
- @Test
- void validateShouldThrowWhenMaxLevelGreaterThanMaxAllowedLevelFromTenantProfile() {
- int maxAllowedRelationLevel = 2;
- int argumentMaxRelationLevel = 3;
-
- var cfg = new RelationQueryDynamicSourceConfiguration();
- cfg.setMaxLevel(argumentMaxRelationLevel);
- cfg.setDirection(EntitySearchDirection.FROM);
- cfg.setRelationType(EntityRelation.CONTAINS_TYPE);
-
- String testRelationArgument = "testRelationArgument";
- assertThatThrownBy(() -> cfg.validateMaxRelationLevel(testRelationArgument, maxAllowedRelationLevel))
- .isInstanceOf(IllegalArgumentException.class)
- .hasMessage("Max relation level is greater than configured " +
- "maximum allowed relation level in tenant profile: " + maxAllowedRelationLevel + " for argument: " + testRelationArgument);
- }
-
- @Test
- void validateShouldPassValidationWhenMaxLevelLessThanMaxAllowedLevelFromTenantProfile() {
- int maxAllowedRelationLevel = 5;
- int argumentMaxRelationLevel = 2;
-
- var cfg = new RelationQueryDynamicSourceConfiguration();
- cfg.setMaxLevel(argumentMaxRelationLevel);
- cfg.setDirection(EntitySearchDirection.FROM);
- cfg.setRelationType(EntityRelation.CONTAINS_TYPE);
-
- String testRelationArgument = "testRelationArgument";
- assertThatCode(() -> cfg.validateMaxRelationLevel(testRelationArgument, maxAllowedRelationLevel)).doesNotThrowAnyException();
- }
-
- @Test
- void validateShouldThrowWhenDirectionIsNull() {
- var cfg = new RelationQueryDynamicSourceConfiguration();
- cfg.setMaxLevel(1);
- cfg.setDirection(null);
- cfg.setRelationType(EntityRelation.CONTAINS_TYPE);
-
- assertThatThrownBy(cfg::validate)
- .isInstanceOf(IllegalArgumentException.class)
- .hasMessage("Relation query dynamic source configuration direction must be specified!");
- }
-
- @ParameterizedTest
- @ValueSource(strings = {" "})
- @NullAndEmptySource
- void validateShouldThrowWhenRelationTypeIsNull(String relationType) {
- var cfg = new RelationQueryDynamicSourceConfiguration();
- cfg.setMaxLevel(1);
- cfg.setDirection(EntitySearchDirection.TO);
- cfg.setRelationType(relationType);
-
- assertThatThrownBy(cfg::validate)
- .isInstanceOf(IllegalArgumentException.class)
- .hasMessage("Relation query dynamic source configuration relation type must be specified!");
- }
-
- @ParameterizedTest
- @NullAndEmptySource
- void isSimpleRelationTrueWhenLevelIsOneAndEntityTypesEmptyOrNull(List entityTypes) {
- var cfg = new RelationQueryDynamicSourceConfiguration();
- cfg.setMaxLevel(1);
- assertThat(cfg.isSimpleRelation()).isTrue();
- }
-
- @Test
- void isSimpleRelationFalseWhenMaxLevelNotOne() {
- var cfg = new RelationQueryDynamicSourceConfiguration();
- cfg.setMaxLevel(2);
- assertThat(cfg.isSimpleRelation()).isFalse();
- }
-
- @ParameterizedTest
- @NullAndEmptySource
- void toEntityRelationsQueryShouldThrowForSimpleRelation(List entityTypes) {
- var cfg = new RelationQueryDynamicSourceConfiguration();
- cfg.setMaxLevel(1);
- cfg.setFetchLastLevelOnly(false);
- cfg.setDirection(EntitySearchDirection.FROM);
- cfg.setRelationType(EntityRelation.CONTAINS_TYPE);
-
- assertThatThrownBy(() -> cfg.toEntityRelationsQuery(rootEntityId))
- .isInstanceOf(IllegalArgumentException.class)
- .hasMessage("Entity relations query can't be created for a simple relation!");
- }
-
- @Test
- void toEntityRelationsQueryShouldBuildQueryForNonSimpleRelation() {
- var cfg = new RelationQueryDynamicSourceConfiguration();
- cfg.setMaxLevel(2);
- cfg.setFetchLastLevelOnly(true);
- cfg.setDirection(EntitySearchDirection.TO);
- cfg.setRelationType(EntityRelation.MANAGES_TYPE);
-
- var query = cfg.toEntityRelationsQuery(rootEntityId);
-
- assertThat(query).isNotNull();
- RelationsSearchParameters params = query.getParameters();
- assertThat(params).isNotNull();
- assertThat(params.getRootId()).isEqualTo(rootEntityId.getId());
- assertThat(params.getDirection()).isEqualTo(EntitySearchDirection.TO);
- assertThat(params.getMaxLevel()).isEqualTo(2);
- assertThat(params.isFetchLastLevelOnly()).isTrue();
-
- assertThat(query.getFilters()).hasSize(1);
- assertThat(query.getFilters().get(0)).isInstanceOf(RelationEntityTypeFilter.class);
- RelationEntityTypeFilter filter = query.getFilters().get(0);
- assertThat(filter.getRelationType()).isEqualTo(EntityRelation.MANAGES_TYPE);
- }
-
- @Test
- void resolveEntityIdsFromDirectionFROMReturnsToIds() {
- when(rel1.getTo()).thenReturn(mock(EntityId.class));
- when(rel2.getTo()).thenReturn(mock(EntityId.class));
-
- var cfg = new RelationQueryDynamicSourceConfiguration();
- cfg.setDirection(EntitySearchDirection.FROM);
-
- var out = cfg.resolveEntityIds(List.of(rel1, rel2));
-
- assertThat(out).containsExactly(rel1.getTo(), rel2.getTo());
- }
-
- @Test
- void resolveEntityIdsFromDirectionTOReturnsFromIds() {
- when(rel1.getFrom()).thenReturn(mock(EntityId.class));
- when(rel2.getFrom()).thenReturn(mock(EntityId.class));
-
- var cfg = new RelationQueryDynamicSourceConfiguration();
- cfg.setDirection(EntitySearchDirection.TO);
-
- var out = cfg.resolveEntityIds(List.of(rel1, rel2));
-
- assertThat(out).containsExactly(rel1.getFrom(), rel2.getFrom());
- }
-
- @Test
- void validateShouldPassForValidConfig() {
- var cfg = new RelationQueryDynamicSourceConfiguration();
- cfg.setMaxLevel(2);
- cfg.setFetchLastLevelOnly(false);
- cfg.setDirection(EntitySearchDirection.FROM);
- cfg.setRelationType(EntityRelation.CONTAINS_TYPE);
-
- assertThatCode(cfg::validate).doesNotThrowAnyException();
- }
-
-}
diff --git a/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/geofencing/GeofencingCalculatedFieldConfigurationTest.java b/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/geofencing/GeofencingCalculatedFieldConfigurationTest.java
index 91a47aac57..c5fb1f8953 100644
--- a/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/geofencing/GeofencingCalculatedFieldConfigurationTest.java
+++ b/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/geofencing/GeofencingCalculatedFieldConfigurationTest.java
@@ -101,33 +101,6 @@ public class GeofencingCalculatedFieldConfigurationTest {
verify(zoneGroupConfigurationB).validate(zoneGroupBName);
}
- @Test
- void scheduledUpdateDisabledWhenIntervalIsZero() {
- var cfg = new GeofencingCalculatedFieldConfiguration();
- cfg.setScheduledUpdateInterval(0);
- assertThat(cfg.isScheduledUpdateEnabled()).isFalse();
- }
-
- @Test
- void scheduledUpdateDisabledWhenIntervalIsGreaterThanZeroButNoZonesWithDynamicArguments() {
- var cfg = new GeofencingCalculatedFieldConfiguration();
- var zoneGroupConfigurationMock = mock(ZoneGroupConfiguration.class);
- when(zoneGroupConfigurationMock.hasDynamicSource()).thenReturn(false);
- cfg.setZoneGroups(Map.of("someGroupName", zoneGroupConfigurationMock));
- cfg.setScheduledUpdateInterval(60);
- assertThat(cfg.isScheduledUpdateEnabled()).isFalse();
- }
-
- @Test
- void scheduledUpdateEnabledWhenIntervalIsGreaterThanZeroAndDynamicArgumentsPresent() {
- var cfg = new GeofencingCalculatedFieldConfiguration();
- var zoneGroupConfigurationMock = mock(ZoneGroupConfiguration.class);
- when(zoneGroupConfigurationMock.hasDynamicSource()).thenReturn(true);
- cfg.setZoneGroups(Map.of("someGroupName", zoneGroupConfigurationMock));
- cfg.setScheduledUpdateInterval(60);
- assertThat(cfg.isScheduledUpdateEnabled()).isTrue();
- }
-
@Test
void testGetArgumentsOverride() {
var cfg = new GeofencingCalculatedFieldConfiguration();
diff --git a/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/geofencing/ZoneGroupConfigurationTest.java b/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/geofencing/ZoneGroupConfigurationTest.java
index 4eb822d93c..beb4639a31 100644
--- a/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/geofencing/ZoneGroupConfigurationTest.java
+++ b/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/geofencing/ZoneGroupConfigurationTest.java
@@ -23,7 +23,7 @@ import org.thingsboard.server.common.data.AttributeScope;
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.ReferencedEntityKey;
-import org.thingsboard.server.common.data.cf.configuration.RelationQueryDynamicSourceConfiguration;
+import org.thingsboard.server.common.data.cf.configuration.RelationPathQueryDynamicSourceConfiguration;
import org.thingsboard.server.common.data.relation.EntityRelation;
import org.thingsboard.server.common.data.relation.EntitySearchDirection;
@@ -100,7 +100,7 @@ public class ZoneGroupConfigurationTest {
@Test
void whenHasDynamicSourceCalled_shouldReturnTrueIfDynamicSourceConfigurationIsNotNull() {
var zoneGroupConfiguration = new ZoneGroupConfiguration("perimeter", REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS, false);
- zoneGroupConfiguration.setRefDynamicSourceConfiguration(new RelationQueryDynamicSourceConfiguration());
+ zoneGroupConfiguration.setRefDynamicSourceConfiguration(new RelationPathQueryDynamicSourceConfiguration());
assertThat(zoneGroupConfiguration.hasDynamicSource()).isTrue();
}
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 0eff7ad053..dbaef23431 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_2_0)
+ .setEdgeVersion(EdgeVersion.V_4_3_0)
.setMaxInboundMessageSize(maxInboundMessageSize)
.build())
.build());
diff --git a/common/edge-api/src/main/proto/edge.proto b/common/edge-api/src/main/proto/edge.proto
index dbda462a99..5a0aeac977 100644
--- a/common/edge-api/src/main/proto/edge.proto
+++ b/common/edge-api/src/main/proto/edge.proto
@@ -44,6 +44,7 @@ enum EdgeVersion {
V_4_0_0 = 10;
V_4_1_0 = 11;
V_4_2_0 = 12;
+ V_4_3_0 = 13;
V_LATEST = 999;
}
@@ -133,6 +134,12 @@ message CalculatedFieldUpdateMsg{
string entity = 4;
}
+message AiModelUpdateMsg{
+ UpdateMsgType msgType = 1;
+ int64 idMSB = 2;
+ int64 idLSB = 3;
+ string entity = 4;
+}
message EntityDataProto {
int64 entityIdMSB = 1;
@@ -441,6 +448,7 @@ message UplinkMsg {
repeated RuleChainMetadataUpdateMsg ruleChainMetadataUpdateMsg = 24;
repeated CalculatedFieldUpdateMsg calculatedFieldUpdateMsg = 25;
repeated CalculatedFieldRequestMsg calculatedFieldRequestMsg = 26;
+ repeated AiModelUpdateMsg aiModelUpdateMsg = 27;
}
message UplinkResponseMsg {
@@ -491,4 +499,5 @@ message DownlinkMsg {
repeated NotificationTemplateUpdateMsg notificationTemplateUpdateMsg = 33;
repeated OAuth2DomainUpdateMsg oAuth2DomainUpdateMsg = 34;
repeated CalculatedFieldUpdateMsg calculatedFieldUpdateMsg = 35;
+ repeated AiModelUpdateMsg aiModelUpdateMsg = 36;
}
diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/data/BaseEntityData.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/data/BaseEntityData.java
index 21b9ff0c4f..b75ce6a315 100644
--- a/common/edqs/src/main/java/org/thingsboard/server/edqs/data/BaseEntityData.java
+++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/data/BaseEntityData.java
@@ -20,6 +20,7 @@ import lombok.Setter;
import lombok.ToString;
import org.thingsboard.server.common.data.AttributeScope;
import org.thingsboard.server.common.data.EntityType;
+import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.edqs.fields.EntityFields;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.permission.QueryContext;
@@ -139,11 +140,32 @@ public abstract class BaseEntityData implements EntityDa
case "name" -> getEntityName();
case "ownerName" -> getOwnerName();
case "ownerType" -> getOwnerType();
+ case "displayName" -> getDisplayName();
case "entityType" -> Optional.ofNullable(getEntityType()).map(EntityType::name).orElse("");
default -> fields.getAsString(name);
};
}
+ public String getDisplayName(){
+ return switch (getEntityType()) {
+ case DEVICE, ASSET -> StringUtils.isNotBlank(fields.getLabel()) ? fields.getLabel() : fields.getName();
+ case USER -> {
+ boolean firstNameSet = StringUtils.isNotBlank(fields.getFirstName());
+ boolean lastNameSet = StringUtils.isNotBlank(fields.getLastName());
+ if(firstNameSet && lastNameSet) {
+ yield fields.getFirstName() + " " + fields.getLastName();
+ } else if(firstNameSet) {
+ yield fields.getFirstName();
+ } else if (lastNameSet) {
+ yield fields.getLastName();
+ } else {
+ yield fields.getEmail();
+ }
+ }
+ default -> fields.getName();
+ };
+ }
+
public String getEntityName() {
return getFields().getName();
}
diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/data/DeviceData.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/data/DeviceData.java
index 3a3e5c5792..f47bfcb28a 100644
--- a/common/edqs/src/main/java/org/thingsboard/server/edqs/data/DeviceData.java
+++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/data/DeviceData.java
@@ -18,6 +18,7 @@ package org.thingsboard.server.edqs.data;
import lombok.ToString;
import org.thingsboard.server.common.data.AttributeScope;
import org.thingsboard.server.common.data.EntityType;
+import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.edqs.fields.DeviceFields;
import org.thingsboard.server.common.data.query.EntityKeyType;
import org.thingsboard.server.common.data.edqs.DataPoint;
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/MsgType.java b/common/message/src/main/java/org/thingsboard/server/common/msg/MsgType.java
index 48b07af29b..20043582d7 100644
--- a/common/message/src/main/java/org/thingsboard/server/common/msg/MsgType.java
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/MsgType.java
@@ -147,10 +147,7 @@ public enum MsgType {
/* CF Manager Actor -> CF Entity actor */
CF_ENTITY_TELEMETRY_MSG,
CF_ENTITY_INIT_CF_MSG,
- CF_ENTITY_DELETE_MSG,
-
- CF_DYNAMIC_ARGUMENTS_REFRESH_MSG,
- CF_ENTITY_DYNAMIC_ARGUMENTS_REFRESH_MSG;
+ CF_ENTITY_DELETE_MSG;
@Getter
private final boolean ignoreOnStart;
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/tools/SchedulerUtils.java b/common/message/src/main/java/org/thingsboard/server/common/msg/tools/SchedulerUtils.java
index e0805c27aa..5b8a241a1a 100644
--- a/common/message/src/main/java/org/thingsboard/server/common/msg/tools/SchedulerUtils.java
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/tools/SchedulerUtils.java
@@ -60,16 +60,4 @@ public class SchedulerUtils {
return LocalDate.now(UTC).with(TemporalAdjusters.firstDayOfNextMonth()).atStartOfDay(zoneId).toInstant().toEpochMilli();
}
- public static long getStartOfNextNextMonth() {
- return getStartOfNextNextMonth(UTC);
- }
-
- public static long getStartOfNextNextMonth(ZoneId zoneId) {
- return LocalDate.now(UTC).with(firstDayOfNextNextMonth()).atStartOfDay(zoneId).toInstant().toEpochMilli();
- }
-
- public static TemporalAdjuster firstDayOfNextNextMonth() {
- return (temporal) -> temporal.with(DAY_OF_MONTH, 1).plus(2, MONTHS);
- }
-
}
diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/AbstractGatewaySessionHandler.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/AbstractGatewaySessionHandler.java
index 91e7dedbf2..fee2f04e24 100644
--- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/AbstractGatewaySessionHandler.java
+++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/AbstractGatewaySessionHandler.java
@@ -80,6 +80,8 @@ import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.Consumer;
@@ -405,18 +407,34 @@ public abstract class AbstractGatewaySessionHandler deviceEntry : json.getAsJsonObject().entrySet()) {
- if (!deviceEntry.getValue().isJsonArray()) {
- log.warn("{}[{}]", CAN_T_PARSE_VALUE, json);
- continue;
- }
+
+ List> deviceEntries = json.getAsJsonObject().entrySet().stream()
+ .filter(entry -> {
+ final boolean isArray = entry.getValue().isJsonArray();
+ if (!isArray) {
+ log.warn("{} device='{}' value={}", CAN_T_PARSE_VALUE, entry.getKey(), entry.getValue());
+ }
+ return isArray;
+ })
+ .toList();
+
+ if (deviceEntries.isEmpty()) {
+ log.debug("[{}][{}][{}] Devices telemetry message is empty", gateway.getTenantId(), gateway.getDeviceId(), sessionId);
+ throw new IllegalArgumentException("[" + sessionId + "] Devices telemetry message is empty for [" + gateway.getDeviceId() + "]");
+ }
+
+ AtomicInteger remaining = new AtomicInteger(deviceEntries.size());
+ AtomicBoolean ackSent = new AtomicBoolean(false);
+
+ for (Map.Entry deviceEntry : deviceEntries) {
String deviceName = deviceEntry.getKey();
- process(deviceName, deviceCtx -> processPostTelemetryMsg(deviceCtx, deviceEntry.getValue(), deviceName, msgId),
- t -> failedToProcessLog(deviceName, TELEMETRY, t));
+ process(deviceName, deviceCtx -> processPostTelemetryMsg(deviceCtx, deviceEntry.getValue(), deviceName, msgId,
+ remaining, ackSent),
+ t -> processFailure(msgId, deviceName, TELEMETRY, ackSent, t));
}
}
- private void processPostTelemetryMsg(T deviceCtx, JsonElement msg, String deviceName, int msgId) {
+ private void processPostTelemetryMsg(T deviceCtx, JsonElement msg, String deviceName, int msgId, AtomicInteger remaining, AtomicBoolean ackSent) {
try {
long systemTs = System.currentTimeMillis();
TbPair> gatewayPayloadPair = JsonConverter.convertToGatewayTelemetry(msg.getAsJsonArray(), systemTs);
@@ -425,10 +443,10 @@ public abstract class AbstractGatewaySessionHandler {
String deviceName = checkDeviceName(telemetryMsg.getDeviceName());
- process(deviceName, deviceCtx -> processPostTelemetryMsg(deviceCtx, telemetryMsg.getMsg(), deviceName, msgId),
- t -> failedToProcessLog(deviceName, TELEMETRY, t));
+ process(deviceName, deviceCtx -> processPostTelemetryMsg(deviceCtx, telemetryMsg.getMsg(), deviceName, msgId,
+ remaining, ackSent),
+ t -> processFailure(msgId, deviceName, TELEMETRY, ackSent, t));
});
} catch (RuntimeException | InvalidProtocolBufferException e) {
throw new AdaptorException(e);
}
}
- protected void processPostTelemetryMsg(MqttDeviceAwareSessionContext deviceCtx, TransportProtos.PostTelemetryMsg msg, String deviceName, int msgId) {
+ protected void processPostTelemetryMsg(MqttDeviceAwareSessionContext deviceCtx, TransportProtos.PostTelemetryMsg msg, String deviceName, int msgId,
+ AtomicInteger remaining, AtomicBoolean ackSent) {
try {
TransportProtos.PostTelemetryMsg postTelemetryMsg = ProtoConverter.validatePostTelemetryMsg(msg.toByteArray());
- transportService.process(deviceCtx.getSessionInfo(), postTelemetryMsg, getPubAckCallback(channel, deviceName, msgId, postTelemetryMsg));
+ transportService.process(deviceCtx.getSessionInfo(), postTelemetryMsg, getAggregatePubAckCallback(channel, msgId, deviceName, postTelemetryMsg, remaining, ackSent));
} catch (Throwable e) {
log.warn("[{}][{}][{}] Failed to convert telemetry: [{}]", gateway.getTenantId(), gateway.getDeviceId(), deviceName, msg, e);
- ackOrClose(msgId);
+ ackOrClose(msgId, ackSent);
}
}
@@ -475,26 +498,42 @@ public abstract class AbstractGatewaySessionHandler deviceEntry : json.getAsJsonObject().entrySet()) {
- if (!deviceEntry.getValue().isJsonObject()) {
- log.warn("{}[{}]", CAN_T_PARSE_VALUE, json);
- continue;
- }
+ List> deviceEntries = json.getAsJsonObject().entrySet().stream()
+ .filter(entry -> {
+ boolean isJsonObject = entry.getValue().isJsonObject();
+ if (!isJsonObject) {
+ log.warn("{} device='{}' value={}", CAN_T_PARSE_VALUE, entry.getKey(), entry.getValue());
+ }
+ return isJsonObject;
+ })
+ .toList();
+
+ if (deviceEntries.isEmpty()) {
+ log.debug("[{}][{}][{}] Devices claim message is empty", gateway.getTenantId(), gateway.getDeviceId(), sessionId);
+ throw new IllegalArgumentException("[" + sessionId + "] Devices claim message is empty for [" + gateway.getDeviceId() + "]");
+ }
+
+ AtomicInteger remaining = new AtomicInteger(deviceEntries.size());
+ AtomicBoolean ackSent = new AtomicBoolean(false);
+
+ for (Map.Entry deviceEntry : deviceEntries) {
String deviceName = deviceEntry.getKey();
- process(deviceName, deviceCtx -> processClaimDeviceMsg(deviceCtx, deviceEntry.getValue(), deviceName, msgId),
- t -> failedToProcessLog(deviceName, CLAIMING, t));
+ process(deviceName, deviceCtx -> processClaimDeviceMsg(deviceCtx, deviceEntry.getValue(), deviceName, msgId,
+ remaining, ackSent),
+ t -> processFailure(msgId, deviceName, CLAIMING, ackSent, t));
}
}
- private void processClaimDeviceMsg(MqttDeviceAwareSessionContext deviceCtx, JsonElement claimRequest, String deviceName, int msgId) {
+ private void processClaimDeviceMsg(MqttDeviceAwareSessionContext deviceCtx, JsonElement claimRequest, String deviceName, int msgId,
+ AtomicInteger remaining, AtomicBoolean ackSent) {
try {
DeviceId deviceId = deviceCtx.getDeviceId();
TransportProtos.ClaimDeviceMsg claimDeviceMsg = JsonConverter.convertToClaimDeviceProto(deviceId, claimRequest);
- transportService.process(deviceCtx.getSessionInfo(), claimDeviceMsg, getPubAckCallback(channel, deviceName, msgId, claimDeviceMsg));
+ transportService.process(deviceCtx.getSessionInfo(), claimDeviceMsg, getAggregatePubAckCallback(channel, msgId, deviceName, claimDeviceMsg, remaining, ackSent));
} catch (Throwable e) {
log.warn("[{}][{}][{}] Failed to convert claim message: [{}]", gateway.getTenantId(), gateway.getDeviceId(), deviceName, claimRequest, e);
- ackOrClose(msgId);
+ ackOrClose(msgId, ackSent);
}
}
@@ -507,49 +546,70 @@ public abstract class AbstractGatewaySessionHandler {
String deviceName = checkDeviceName(claimDeviceMsg.getDeviceName());
- process(deviceName, deviceCtx -> processClaimDeviceMsg(deviceCtx, claimDeviceMsg.getClaimRequest(), deviceName, msgId),
- t -> failedToProcessLog(deviceName, CLAIMING, t));
+ process(deviceName, deviceCtx -> processClaimDeviceMsg(deviceCtx, claimDeviceMsg.getClaimRequest(), deviceName, msgId,
+ remaining, ackSent),
+ t -> processFailure(msgId, deviceName, CLAIMING, ackSent, t));
});
} catch (RuntimeException | InvalidProtocolBufferException e) {
throw new AdaptorException(e);
}
}
- private void processClaimDeviceMsg(MqttDeviceAwareSessionContext deviceCtx, TransportApiProtos.ClaimDevice claimRequest, String deviceName, int msgId) {
+ private void processClaimDeviceMsg(MqttDeviceAwareSessionContext deviceCtx, TransportApiProtos.ClaimDevice claimRequest, String deviceName, int msgId,
+ AtomicInteger remaining, AtomicBoolean ackSent) {
try {
DeviceId deviceId = deviceCtx.getDeviceId();
TransportProtos.ClaimDeviceMsg claimDeviceMsg = ProtoConverter.convertToClaimDeviceProto(deviceId, claimRequest.toByteArray());
- transportService.process(deviceCtx.getSessionInfo(), claimDeviceMsg, getPubAckCallback(channel, deviceName, msgId, claimDeviceMsg));
+ transportService.process(deviceCtx.getSessionInfo(), claimDeviceMsg, getAggregatePubAckCallback(channel, msgId, deviceName, claimDeviceMsg, remaining, ackSent));
} catch (Throwable e) {
log.warn("[{}][{}][{}] Failed to convert claim message: [{}]", gateway.getTenantId(), gateway.getDeviceId(), deviceName, claimRequest, e);
- ackOrClose(msgId);
+ ackOrClose(msgId, ackSent);
}
}
private void onDeviceAttributesJson(int msgId, ByteBuf payload) throws AdaptorException {
JsonElement json = JsonMqttAdaptor.validateJsonPayload(sessionId, payload);
validateJsonObject(json);
- for (Map.Entry deviceEntry : json.getAsJsonObject().entrySet()) {
- if (!deviceEntry.getValue().isJsonObject()) {
- log.warn("{}[{}]", CAN_T_PARSE_VALUE, json);
- continue;
- }
+ List> deviceEntries = json.getAsJsonObject().entrySet().stream()
+ .filter(entry -> {
+ boolean isJsonObject = entry.getValue().isJsonObject();
+ if (!isJsonObject) {
+ log.warn("{} device='{}' value={}", CAN_T_PARSE_VALUE, entry.getKey(), entry.getValue());
+ }
+ return isJsonObject;
+ })
+ .toList();
+
+ if (deviceEntries.isEmpty()) {
+ log.debug("[{}][{}][{}] Devices attribute message is empty", gateway.getTenantId(), gateway.getDeviceId(), sessionId);
+ throw new IllegalArgumentException("[" + sessionId + "] Devices attribute message is empty for [" + gateway.getDeviceId() + "]");
+ }
+
+ AtomicInteger remaining = new AtomicInteger(deviceEntries.size());
+ AtomicBoolean ackSent = new AtomicBoolean(false);
+
+ for (Map.Entry deviceEntry : deviceEntries) {
String deviceName = deviceEntry.getKey();
- process(deviceName, deviceCtx -> processPostAttributesMsg(deviceCtx, deviceEntry.getValue(), deviceName, msgId),
- t -> failedToProcessLog(deviceName, ATTRIBUTE, t));
+ process(deviceName, deviceCtx -> processPostAttributesMsg(deviceCtx, deviceEntry.getValue(), deviceName, msgId,
+ remaining, ackSent),
+ t -> processFailure(msgId, deviceName, ATTRIBUTE, ackSent, t));
}
}
- private void processPostAttributesMsg(MqttDeviceAwareSessionContext deviceCtx, JsonElement msg, String deviceName, int msgId) {
+ private void processPostAttributesMsg(MqttDeviceAwareSessionContext deviceCtx, JsonElement msg, String deviceName, int msgId,
+ AtomicInteger remaining, AtomicBoolean ackSent) {
try {
TransportProtos.PostAttributeMsg postAttributeMsg = JsonConverter.convertToAttributesProto(msg.getAsJsonObject());
- transportService.process(deviceCtx.getSessionInfo(), postAttributeMsg, getPubAckCallback(channel, deviceName, msgId, postAttributeMsg));
+ transportService.process(deviceCtx.getSessionInfo(), postAttributeMsg, getAggregatePubAckCallback(channel, msgId, deviceName, postAttributeMsg, remaining, ackSent));
} catch (Throwable e) {
log.warn("[{}][{}][{}] Failed to process device attributes command: [{}]", gateway.getTenantId(), gateway.getDeviceId(), deviceName, msg, e);
- ackOrClose(msgId);
+ ackOrClose(msgId, ackSent);
}
}
@@ -562,23 +622,28 @@ public abstract class AbstractGatewaySessionHandler {
String deviceName = checkDeviceName(attributesMsg.getDeviceName());
- process(deviceName, deviceCtx -> processPostAttributesMsg(deviceCtx, attributesMsg.getMsg(), deviceName, msgId),
- t -> failedToProcessLog(deviceName, ATTRIBUTE, t));
+ process(deviceName, deviceCtx -> processPostAttributesMsg(deviceCtx, attributesMsg.getMsg(), deviceName, msgId,
+ remaining, ackSent),
+ t -> processFailure(msgId, deviceName, ATTRIBUTE, ackSent, t));
});
} catch (RuntimeException | InvalidProtocolBufferException e) {
throw new AdaptorException(e);
}
}
- protected void processPostAttributesMsg(MqttDeviceAwareSessionContext deviceCtx, TransportProtos.PostAttributeMsg kvListProto, String deviceName, int msgId) {
+ protected void processPostAttributesMsg(MqttDeviceAwareSessionContext deviceCtx, TransportProtos.PostAttributeMsg kvListProto, String deviceName, int msgId,
+ AtomicInteger remaining, AtomicBoolean ackSent) {
try {
TransportProtos.PostAttributeMsg postAttributeMsg = ProtoConverter.validatePostAttributeMsg(kvListProto);
- transportService.process(deviceCtx.getSessionInfo(), postAttributeMsg, getPubAckCallback(channel, deviceName, msgId, postAttributeMsg));
+ transportService.process(deviceCtx.getSessionInfo(), postAttributeMsg, getAggregatePubAckCallback(channel, msgId, deviceName, postAttributeMsg, remaining, ackSent));
} catch (Throwable e) {
log.warn("[{}][{}][{}] Failed to process device attributes command: [{}]", gateway.getTenantId(), gateway.getDeviceId(), deviceName, kvListProto, e);
- ackOrClose(msgId);
+ ackOrClose(msgId, ackSent);
}
}
@@ -647,27 +712,34 @@ public abstract class AbstractGatewaySessionHandler processRpcResponseMsg(deviceCtx, requestId, data, deviceName, msgId),
- t -> failedToProcessLog(deviceName, RPC_RESPONSE, t));
+ AtomicInteger remaining = new AtomicInteger(1);
+ AtomicBoolean ackSent = new AtomicBoolean(false);
+ process(deviceName, deviceCtx -> processRpcResponseMsg(deviceCtx, requestId, data, deviceName, msgId, remaining, ackSent),
+ t -> processFailure(msgId, deviceName, RPC_RESPONSE, ackSent, t));
}
- private void processRpcResponseMsg(MqttDeviceAwareSessionContext deviceCtx, Integer requestId, String data, String deviceName, int msgId) {
+ private void processRpcResponseMsg(MqttDeviceAwareSessionContext deviceCtx, Integer requestId, String data, String deviceName,
+ int msgId, AtomicInteger remaining, AtomicBoolean ackSent) {
TransportProtos.ToDeviceRpcResponseMsg rpcResponseMsg = TransportProtos.ToDeviceRpcResponseMsg.newBuilder()
.setRequestId(requestId).setPayload(data).build();
- transportService.process(deviceCtx.getSessionInfo(), rpcResponseMsg, getPubAckCallback(channel, deviceName, msgId, rpcResponseMsg));
+ transportService.process(deviceCtx.getSessionInfo(), rpcResponseMsg,
+ getAggregatePubAckCallback(channel, msgId, deviceName, rpcResponseMsg, remaining, ackSent));
}
private void processGetAttributeRequestMessage(MqttPublishMessage mqttMsg, String deviceName, TransportProtos.GetAttributeRequestMsg requestMsg) {
int msgId = getMsgId(mqttMsg);
- process(deviceName, deviceCtx -> processGetAttributeRequestMessage(deviceCtx, requestMsg, deviceName, msgId),
- t -> {
- failedToProcessLog(deviceName, ATTRIBUTES_REQUEST, t);
- ack(mqttMsg, MqttReasonCodes.PubAck.IMPLEMENTATION_SPECIFIC_ERROR);
- });
+ AtomicInteger remaining = new AtomicInteger(1);
+ AtomicBoolean ackSent = new AtomicBoolean(false);
+ process(deviceName, deviceCtx -> {
+ processGetAttributeRequestMessage(deviceCtx, requestMsg, deviceName, msgId, remaining, ackSent);
+ },
+ t -> processFailure(msgId, deviceName, ATTRIBUTES_REQUEST, ackSent, MqttReasonCodes.PubAck.IMPLEMENTATION_SPECIFIC_ERROR, t));
}
- private void processGetAttributeRequestMessage(T deviceCtx, TransportProtos.GetAttributeRequestMsg requestMsg, String deviceName, int msgId) {
- transportService.process(deviceCtx.getSessionInfo(), requestMsg, getPubAckCallback(channel, deviceName, msgId, requestMsg));
+ private void processGetAttributeRequestMessage(T deviceCtx, TransportProtos.GetAttributeRequestMsg requestMsg,
+ String deviceName, int msgId, AtomicInteger remaining, AtomicBoolean ackSent) {
+ transportService.process(deviceCtx.getSessionInfo(), requestMsg,
+ getAggregatePubAckCallback(channel, msgId, deviceName, requestMsg, remaining, ackSent));
}
private TransportProtos.GetAttributeRequestMsg toGetAttributeRequestMsg(int requestId, boolean clientScope, Set keys) {
@@ -718,9 +790,11 @@ public abstract class AbstractGatewaySessionHandler pubAckCallback = getAggregatePubAckCallback(channel, -1, deviceName, postTelemetryMsg,
+ new AtomicInteger(1), new AtomicBoolean(false));
+ transportService.process(sessionInfo, postTelemetryMsg, pubAckCallback);
}
- public ConcurrentMap getDevices () {
+ public ConcurrentMap getDevices() {
return this.devices;
}
- private TransportServiceCallback getPubAckCallback(final ChannelHandlerContext ctx, final String deviceName, final int msgId, final T msg) {
+ protected TransportServiceCallback getAggregatePubAckCallback(
+ final ChannelHandlerContext ctx,
+ final int msgId,
+ final String deviceName,
+ final T msg,
+ final AtomicInteger remaining,
+ final AtomicBoolean ackSent) {
+
return new TransportServiceCallback() {
@Override
public void onSuccess(Void dummy) {
log.trace("[{}][{}][{}][{}] Published msg: [{}]", gateway.getTenantId(), gateway.getDeviceId(), sessionId, deviceName, msg);
- if (msgId > 0) {
- ctx.writeAndFlush(MqttTransportHandler.createMqttPubAckMsg(deviceSessionCtx, msgId, MqttReasonCodes.PubAck.SUCCESS.byteValue()));
- } else {
- log.trace("[{}][{}][{}] Wrong msg id: [{}]", gateway.getTenantId(), gateway.getDeviceId(), sessionId, msg);
- ctx.writeAndFlush(MqttTransportHandler.createMqttPubAckMsg(deviceSessionCtx, msgId, MqttReasonCodes.PubAck.UNSPECIFIED_ERROR.byteValue()));
+ if (remaining.decrementAndGet() == 0 && ackSent.compareAndSet(false, true)) {
+ if (msgId > 0) {
+ ctx.writeAndFlush(MqttTransportHandler.createMqttPubAckMsg(
+ deviceSessionCtx, msgId, MqttReasonCodes.PubAck.SUCCESS.byteValue()));
+ } else {
+ log.trace("[{}][{}][{}] Wrong msg id: [{}]", gateway.getTenantId(), gateway.getDeviceId(), sessionId, msgId);
+ ctx.writeAndFlush(MqttTransportHandler.createMqttPubAckMsg(
+ deviceSessionCtx, msgId, MqttReasonCodes.PubAck.UNSPECIFIED_ERROR.byteValue()));
+ }
+ }
+ if (msgId <= 0) {
closeDeviceSession(deviceName, MqttReasonCodes.Disconnect.MALFORMED_PACKET);
}
}
@@ -767,11 +856,20 @@ public abstract class AbstractGatewaySessionHandler contextListenableFuture,
int msgId, List postTelemetryMsgList, String deviceName) {
+ if (CollectionUtils.isEmpty(postTelemetryMsgList)) {
+ log.debug("[{}] Device telemetry list is empty for: [{}]", sessionId, gateway.getDeviceId());
+ }
+
+ AtomicInteger remaining = new AtomicInteger(postTelemetryMsgList.size());
+ AtomicBoolean ackSent = new AtomicBoolean(false);
+
process(contextListenableFuture, deviceCtx -> {
for (TransportProtos.PostTelemetryMsg telemetryMsg : postTelemetryMsgList) {
try {
- processPostTelemetryMsg(deviceCtx, telemetryMsg, deviceName, msgId);
+ processPostTelemetryMsg(deviceCtx, telemetryMsg, deviceName, msgId, remaining, ackSent);
} catch (Throwable e) {
log.warn("[{}][{}] Failed to convert telemetry: {}", gateway.getDeviceId(), deviceName, telemetryMsg, e);
- ackOrClose(msgId);
+ ackOrClose(msgId, ackSent);
}
}
},
- t -> log.debug("[{}] Failed to process device telemetry command: {}", sessionId, deviceName, t));
+ t -> processFailure(msgId, deviceName, "Failed to process device telemetry command", ackSent, t));
}
private void onDeviceAttributesProto(ListenableFuture contextListenableFuture, int msgId,
List attributesMsgList, String deviceName) throws AdaptorException {
try {
if (CollectionUtils.isEmpty(attributesMsgList)) {
- log.debug("[{}] Devices attributes keys list is empty for: [{}]", sessionId, gateway.getDeviceId());
+ log.debug("[{}] Device attribute list is empty for: [{}]", sessionId, gateway.getDeviceId());
}
+
+ AtomicInteger remaining = new AtomicInteger(attributesMsgList.size());
+ AtomicBoolean ackSent = new AtomicBoolean(false);
+
process(contextListenableFuture, deviceCtx -> {
for (TransportApiProtos.AttributesMsg attributesMsg : attributesMsgList) {
TransportProtos.PostAttributeMsg kvListProto = attributesMsg.getMsg();
try {
TransportProtos.PostAttributeMsg postAttributeMsg = ProtoConverter.validatePostAttributeMsg(kvListProto);
- processPostAttributesMsg(deviceCtx, postAttributeMsg, deviceName, msgId);
+ processPostAttributesMsg(deviceCtx, postAttributeMsg, deviceName, msgId, remaining, ackSent);
} catch (Throwable e) {
log.warn("[{}][{}] Failed to process device attributes command: {}", gateway.getDeviceId(), deviceName, kvListProto, e);
+ ackOrClose(msgId, ackSent);
}
}
},
- t -> log.debug("[{}] Failed to process device attributes command: {}", sessionId, deviceName, t));
+ t -> processFailure(msgId, deviceName, "Failed to process device attributes command", ackSent, t));
} catch (RuntimeException e) {
throw new AdaptorException(e);
}
diff --git a/common/util/src/main/java/org/thingsboard/common/util/DonAsynchron.java b/common/util/src/main/java/org/thingsboard/common/util/DonAsynchron.java
index 0f1a56cb17..79b8181548 100644
--- a/common/util/src/main/java/org/thingsboard/common/util/DonAsynchron.java
+++ b/common/util/src/main/java/org/thingsboard/common/util/DonAsynchron.java
@@ -15,12 +15,15 @@
*/
package org.thingsboard.common.util;
+import com.google.common.util.concurrent.FluentFuture;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
+import com.google.common.util.concurrent.SettableFuture;
import java.util.concurrent.Callable;
+import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
import java.util.function.Consumer;
@@ -71,4 +74,16 @@ public class DonAsynchron {
return future;
}
+ public static FluentFuture toFluentFuture(CompletableFuture completable) {
+ SettableFuture future = SettableFuture.create();
+ completable.whenComplete((result, exception) -> {
+ if (exception != null) {
+ future.setException(exception);
+ } else {
+ future.set(result);
+ }
+ });
+ return FluentFuture.from(future);
+ }
+
}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/Dao.java b/dao/src/main/java/org/thingsboard/server/dao/Dao.java
index 72883c55ef..4934059cd5 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/Dao.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/Dao.java
@@ -16,6 +16,7 @@
package org.thingsboard.server.dao;
import com.google.common.util.concurrent.ListenableFuture;
+import org.thingsboard.server.common.data.EntityInfo;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.edqs.fields.EntityFields;
import org.thingsboard.server.common.data.id.TenantId;
@@ -32,6 +33,10 @@ public interface Dao {
ListenableFuture findByIdAsync(TenantId tenantId, UUID id);
+ default List findEntityInfosByNamePrefix(TenantId tenantId, String name) {
+ throw new UnsupportedOperationException();
+ }
+
boolean existsById(TenantId tenantId, UUID id);
ListenableFuture existsByIdAsync(TenantId tenantId, UUID id);
diff --git a/dao/src/main/java/org/thingsboard/server/dao/ResourceContainerDao.java b/dao/src/main/java/org/thingsboard/server/dao/ResourceContainerDao.java
index 6a952fd501..93cc64b2db 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/ResourceContainerDao.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/ResourceContainerDao.java
@@ -15,6 +15,7 @@
*/
package org.thingsboard.server.dao;
+import org.thingsboard.server.common.data.EntityInfo;
import org.thingsboard.server.common.data.id.HasId;
import org.thingsboard.server.common.data.id.TenantId;
@@ -22,8 +23,8 @@ import java.util.List;
public interface ResourceContainerDao> {
- List findByTenantIdAndResourceLink(TenantId tenantId, String link, int limit);
+ List findByTenantIdAndResource(TenantId tenantId, String reference, int limit);
- List findByResourceLink(String link, int limit);
+ List findByResource(String reference, int limit);
}
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
index b091a29247..7cdf5b027d 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/ai/AiModelServiceImpl.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/ai/AiModelServiceImpl.java
@@ -29,12 +29,15 @@ 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.eventsourcing.DeleteEntityEvent;
+import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent;
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 java.util.UUID;
import static org.thingsboard.server.dao.service.Validator.validatePageLink;
@@ -63,11 +66,23 @@ class AiModelServiceImpl extends CachedVersionedEntityService findAiModelByTenantIdAndId(tenantId, modelId)));
}
+ @Override
+ public Optional findAiModelByTenantIdAndName(TenantId tenantId, String name) {
+ return Optional.ofNullable(aiModelDao.findByTenantIdAndName(tenantId.getId(), name));
+ }
+
@Override
@Transactional
public boolean deleteByTenantIdAndId(TenantId tenantId, AiModelId modelId) {
- return deleteByTenantIdAndIdInternal(tenantId, modelId);
+ return deleteByTenantIdAndIdInternal(tenantId, modelId.getId());
}
@Override
@@ -123,14 +143,21 @@ class AiModelServiceImpl extends CachedVersionedEntityService & HasTenantId & HasName> void uniquifyEntityName(E entity, E oldEntity, Consumer setName, EntityType entityType, NameConflictStrategy strategy) {
+ Dao> dao = entityDaoRegistry.getDao(entityType);
+ List existingEntities = dao.findEntityInfosByNamePrefix(entity.getTenantId(), entity.getName());
+ Set existingNames = existingEntities.stream()
+ .filter(e -> (oldEntity == null || !e.getId().equals(oldEntity.getId())))
+ .map(EntityInfo::getName)
+ .collect(Collectors.toSet());
+
+ if (existingNames.contains(entity.getName())) {
+ String uniqueName = generateUniqueName(entity.getName(), existingNames, strategy);
+ setName.accept(uniqueName);
+ }
+ }
+
+ private String generateUniqueName(String baseName, Set existingNames, NameConflictStrategy strategy) {
+ String newName;
+ int index = 1;
+ String separator = strategy.separator();
+ boolean isRandom = strategy.uniquifyStrategy() == RANDOM;
+
+ do {
+ String suffix = isRandom ? StringUtils.randomAlphanumeric(6) : String.valueOf(index++);
+ newName = baseName + separator + suffix;
+ } while (existingNames.contains(newName));
+
+ return newName;
+ }
+
}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewServiceImpl.java
index 0e742e20db..5c836a11c2 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewServiceImpl.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewServiceImpl.java
@@ -29,6 +29,8 @@ import org.thingsboard.server.common.data.EntitySubtype;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.EntityView;
import org.thingsboard.server.common.data.EntityViewInfo;
+import org.thingsboard.server.common.data.NameConflictPolicy;
+import org.thingsboard.server.common.data.NameConflictStrategy;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.edge.Edge;
@@ -110,14 +112,24 @@ public class EntityViewServiceImpl extends CachedVersionedEntityService> findByRelationPathQueryAsync(TenantId tenantId, EntityRelationPathQuery relationPathQuery) {
+ log.trace("Executing findByRelationPathQuery, tenantId [{}], relationPathQuery {}", tenantId, relationPathQuery);
+ validateId(tenantId, id -> "Invalid tenant id: " + id);
+ validate(relationPathQuery);
+ if (relationPathQuery.levels().size() == 1) {
+ RelationPathLevel relationPathLevel = relationPathQuery.levels().get(0);
+ return switch (relationPathLevel.direction()) {
+ case FROM -> findByFromAndTypeAsync(tenantId, relationPathQuery.rootEntityId(), relationPathLevel.relationType(), RelationTypeGroup.COMMON);
+ case TO -> findByToAndTypeAsync(tenantId, relationPathQuery.rootEntityId(), relationPathLevel.relationType(), RelationTypeGroup.COMMON);
+ };
+ }
+ return executor.submit(() -> relationDao.findByRelationPathQuery(tenantId, relationPathQuery));
+ }
+
+ private void validate(EntityRelationPathQuery relationPathQuery) {
+ validateId((UUIDBased) relationPathQuery.rootEntityId(), id -> "Invalid root entity id: " + id);
+ List levels = relationPathQuery.levels();
+ if (CollectionUtils.isEmpty(levels)) {
+ throw new DataValidationException("Relation path levels should be specified!");
+ }
+ levels.forEach(RelationPathLevel::validate);
+ }
+
protected void validate(EntityRelation relation) {
if (relation == null) {
throw new DataValidationException("Relation type should be specified!");
diff --git a/dao/src/main/java/org/thingsboard/server/dao/relation/RelationDao.java b/dao/src/main/java/org/thingsboard/server/dao/relation/RelationDao.java
index c061faed5e..ad53164ad7 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/relation/RelationDao.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/relation/RelationDao.java
@@ -19,6 +19,7 @@ import com.google.common.util.concurrent.ListenableFuture;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.relation.EntityRelation;
+import org.thingsboard.server.common.data.relation.EntityRelationPathQuery;
import org.thingsboard.server.common.data.relation.RelationTypeGroup;
import org.thingsboard.server.common.data.rule.RuleChainType;
@@ -71,4 +72,6 @@ public interface RelationDao {
List findRuleNodeToRuleChainRelations(RuleChainType ruleChainType, int limit);
+ List findByRelationPathQuery(TenantId tenantId, EntityRelationPathQuery relationPathQuery);
+
}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/resource/BaseImageService.java b/dao/src/main/java/org/thingsboard/server/dao/resource/BaseImageService.java
index 3be7f5b91c..c16e37d30f 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/resource/BaseImageService.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/resource/BaseImageService.java
@@ -50,6 +50,7 @@ import org.thingsboard.server.dao.ImageContainerDao;
import org.thingsboard.server.dao.asset.AssetProfileDao;
import org.thingsboard.server.dao.dashboard.DashboardInfoDao;
import org.thingsboard.server.dao.device.DeviceProfileDao;
+import org.thingsboard.server.dao.rule.RuleChainDao;
import org.thingsboard.server.dao.service.Validator;
import org.thingsboard.server.dao.service.validator.ResourceDataValidator;
import org.thingsboard.server.dao.util.ImageUtils;
@@ -109,8 +110,8 @@ public class BaseImageService extends BaseResourceService implements ImageServic
public BaseImageService(TbResourceDao resourceDao, TbResourceInfoDao resourceInfoDao, ResourceDataValidator resourceValidator,
AssetProfileDao assetProfileDao, DeviceProfileDao deviceProfileDao, WidgetsBundleDao widgetsBundleDao,
- WidgetTypeDao widgetTypeDao, DashboardInfoDao dashboardInfoDao) {
- super(resourceDao, resourceInfoDao, resourceValidator, widgetTypeDao, dashboardInfoDao);
+ WidgetTypeDao widgetTypeDao, DashboardInfoDao dashboardInfoDao, RuleChainDao ruleChainDao) {
+ super(resourceDao, resourceInfoDao, resourceValidator, widgetTypeDao, dashboardInfoDao, ruleChainDao);
this.assetProfileDao = assetProfileDao;
this.deviceProfileDao = deviceProfileDao;
this.widgetsBundleDao = widgetsBundleDao;
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 bf941256f5..7f0dd4a6bf 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
@@ -35,11 +35,13 @@ import org.thingsboard.server.cache.resourceInfo.ResourceInfoCacheKey;
import org.thingsboard.server.cache.resourceInfo.ResourceInfoEvictEvent;
import org.thingsboard.server.common.data.Dashboard;
import org.thingsboard.server.common.data.DataConstants;
+import org.thingsboard.server.common.data.EntityInfo;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.ResourceExportData;
import org.thingsboard.server.common.data.ResourceSubType;
import org.thingsboard.server.common.data.ResourceType;
import org.thingsboard.server.common.data.TbResource;
+import org.thingsboard.server.common.data.TbResourceDataInfo;
import org.thingsboard.server.common.data.TbResourceDeleteResult;
import org.thingsboard.server.common.data.TbResourceInfo;
import org.thingsboard.server.common.data.TbResourceInfoFilter;
@@ -56,6 +58,7 @@ import org.thingsboard.server.dao.entity.AbstractCachedEntityService;
import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent;
import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent;
import org.thingsboard.server.dao.exception.DataValidationException;
+import org.thingsboard.server.dao.rule.RuleChainDao;
import org.thingsboard.server.dao.service.PaginatedRemover;
import org.thingsboard.server.dao.service.Validator;
import org.thingsboard.server.dao.service.validator.ResourceDataValidator;
@@ -92,13 +95,16 @@ public class BaseResourceService extends AbstractCachedEntityService> resourceContainerDaoMap = new HashMap<>();
+ protected final RuleChainDao ruleChainDao;
+ private final Map> resourceLinkContainerDaoMap = new HashMap<>();
+ private final Map> generalResourceContainerDaoMap = new HashMap<>();
protected static final int MAX_ENTITIES_TO_FIND = 10;
@PostConstruct
public void init() {
- resourceContainerDaoMap.put(EntityType.WIDGET_TYPE, widgetTypeDao);
- resourceContainerDaoMap.put(EntityType.DASHBOARD, dashboardInfoDao);
+ resourceLinkContainerDaoMap.put(EntityType.WIDGET_TYPE, widgetTypeDao);
+ resourceLinkContainerDaoMap.put(EntityType.DASHBOARD, dashboardInfoDao);
+ generalResourceContainerDaoMap.put(EntityType.RULE_CHAIN, ruleChainDao);
}
@Autowired @Lazy
@@ -206,6 +212,12 @@ public class BaseResourceService extends AbstractCachedEntityService>> affectedEntities = new HashMap<>();
-
- resourceContainerDaoMap.forEach((entityType, resourceContainerDao) -> {
- var entities = tenantId.isSysTenantId() ? resourceContainerDao.findByResourceLink(link, MAX_ENTITIES_TO_FIND) :
- resourceContainerDao.findByTenantIdAndResourceLink(tenantId, link, MAX_ENTITIES_TO_FIND);
- if (!entities.isEmpty()) {
- affectedEntities.put(entityType.name(), entities);
- }
- });
-
- if (!affectedEntities.isEmpty()) {
- success = false;
- result.references(affectedEntities);
- }
+ Map> references = findResourceReferences(tenantId, resource);
+ if (!references.isEmpty()) {
+ success = false;
+ result.references(references);
}
}
if (success) {
resourceDao.removeById(tenantId, resourceId.getId());
+ publishEvictEvent(new ResourceInfoEvictEvent(tenantId, resourceId));
eventPublisher.publishEvent(DeleteEntityEvent.builder().tenantId(tenantId).entity(resource).entityId(resourceId).build());
}
return result.success(success).build();
}
+ private Map> findResourceReferences(TenantId tenantId, TbResourceInfo resource) {
+ Map> references = new HashMap<>();
+
+ if (resource.getResourceType() == ResourceType.JS_MODULE) {
+ var ref = resource.getLink();
+ findReferences(tenantId, references, ref, resourceLinkContainerDaoMap);
+ }
+
+ if (resource.getResourceType() == ResourceType.GENERAL) {
+ var ref = resource.getId().getId().toString();
+ findReferences(tenantId, references, ref, generalResourceContainerDaoMap);
+ }
+
+ return references;
+ }
+
+ private void findReferences(TenantId tenantId, Map> references, String ref, Map> resourceLinkContainerDaoMap) {
+ resourceLinkContainerDaoMap.forEach((entityType, dao) -> {
+ List entities = tenantId.isSysTenantId()
+ ? dao.findByResource(ref, MAX_ENTITIES_TO_FIND)
+ : dao.findByTenantIdAndResource(tenantId, ref, MAX_ENTITIES_TO_FIND);
+ if (!entities.isEmpty()) {
+ references.put(entityType.name(), entities);
+ }
+ });
+ }
+
@Override
public void deleteEntity(TenantId tenantId, EntityId id, boolean force) {
deleteResource(tenantId, (TbResourceId) id, force);
@@ -663,6 +691,12 @@ public class BaseResourceService extends AbstractCachedEntityService findSystemOrTenantResourcesByIds(TenantId tenantId, List resourceIds) {
+ log.trace("Executing findSystemOrTenantResourcesByIds, tenantId [{}], resourceIds [{}]", tenantId, resourceIds);
+ return resourceInfoDao.findSystemOrTenantResourcesByIds(tenantId, resourceIds);
+ }
+
@Override
public String calculateEtag(byte[] data) {
return Hashing.sha256().hashBytes(data).toString();
diff --git a/dao/src/main/java/org/thingsboard/server/dao/resource/DefaultTbResourceDataCache.java b/dao/src/main/java/org/thingsboard/server/dao/resource/DefaultTbResourceDataCache.java
new file mode 100644
index 0000000000..452f86b1a6
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/resource/DefaultTbResourceDataCache.java
@@ -0,0 +1,72 @@
+/**
+ * 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.resource;
+
+import com.github.benmanes.caffeine.cache.AsyncLoadingCache;
+import com.github.benmanes.caffeine.cache.Caffeine;
+import com.google.common.util.concurrent.FluentFuture;
+import jakarta.annotation.PostConstruct;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Service;
+import org.thingsboard.common.util.DonAsynchron;
+import org.thingsboard.server.common.data.TbResourceDataInfo;
+import org.thingsboard.server.common.data.id.TbResourceId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.dao.sql.JpaExecutorService;
+
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.TimeUnit;
+
+@Service
+@RequiredArgsConstructor
+@Slf4j
+public class DefaultTbResourceDataCache implements TbResourceDataCache {
+
+ private final ResourceService resourceService;
+ private final JpaExecutorService executorService;
+
+ @Value("${cache.tbResourceData.maxSize:100000}")
+ private int cacheMaxSize;
+ @Value("${cache.tbResourceData.timeToLiveInMinutes:44640}")
+ private int cacheValueTtl;
+ private AsyncLoadingCache cache;
+
+ @PostConstruct
+ private void init() {
+ cache = Caffeine.newBuilder()
+ .maximumSize(cacheMaxSize)
+ .expireAfterAccess(cacheValueTtl, TimeUnit.MINUTES)
+ .executor(executorService)
+ .buildAsync((key, executor) -> CompletableFuture.supplyAsync(() -> resourceService.getResourceDataInfo(key.tenantId(), key.resourceId()), executor));
+ }
+
+ @Override
+ public FluentFuture getResourceDataInfoAsync(TenantId tenantId, TbResourceId resourceId) {
+ log.trace("Retrieving resource data info by id [{}], tenant id [{}] from cache", resourceId, tenantId);
+ return DonAsynchron.toFluentFuture(cache.get(new ResourceDataKey(tenantId, resourceId)));
+ }
+
+ @Override
+ public void evictResourceData(TenantId tenantId, TbResourceId resourceId) {
+ cache.asMap().remove(new ResourceDataKey(tenantId, resourceId));
+ log.trace("Evicted resource data info with id [{}], tenant id [{}]", resourceId, tenantId);
+ }
+
+ record ResourceDataKey (TenantId tenantId, TbResourceId resourceId) {}
+
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/resource/TbResourceDao.java b/dao/src/main/java/org/thingsboard/server/dao/resource/TbResourceDao.java
index 23b59b5658..1b9f250521 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/resource/TbResourceDao.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/resource/TbResourceDao.java
@@ -18,6 +18,7 @@ package org.thingsboard.server.dao.resource;
import org.thingsboard.server.common.data.ResourceSubType;
import org.thingsboard.server.common.data.ResourceType;
import org.thingsboard.server.common.data.TbResource;
+import org.thingsboard.server.common.data.TbResourceDataInfo;
import org.thingsboard.server.common.data.id.TbResourceId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageData;
@@ -51,4 +52,5 @@ public interface TbResourceDao extends Dao, TenantEntityWithDataDao,
long getResourceSize(TenantId tenantId, TbResourceId resourceId);
+ TbResourceDataInfo getResourceDataInfo(TenantId tenantId, TbResourceId resourceId);
}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/resource/TbResourceInfoDao.java b/dao/src/main/java/org/thingsboard/server/dao/resource/TbResourceInfoDao.java
index 8e97738501..f4fe02843d 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/resource/TbResourceInfoDao.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/resource/TbResourceInfoDao.java
@@ -18,6 +18,7 @@ package org.thingsboard.server.dao.resource;
import org.thingsboard.server.common.data.ResourceType;
import org.thingsboard.server.common.data.TbResourceInfo;
import org.thingsboard.server.common.data.TbResourceInfoFilter;
+import org.thingsboard.server.common.data.id.TbResourceId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
@@ -46,4 +47,5 @@ public interface TbResourceInfoDao extends Dao {
TbResourceInfo findPublicResourceByKey(ResourceType resourceType, String publicResourceKey);
+ List findSystemOrTenantResourcesByIds(TenantId tenantId, List resourceIds);
}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/rule/RuleChainDao.java b/dao/src/main/java/org/thingsboard/server/dao/rule/RuleChainDao.java
index 5b09eec42a..ac716bb4fc 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/rule/RuleChainDao.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/rule/RuleChainDao.java
@@ -21,8 +21,10 @@ import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.rule.RuleChain;
import org.thingsboard.server.common.data.rule.RuleChainType;
+import org.thingsboard.server.common.data.rule.RuleNode;
import org.thingsboard.server.dao.Dao;
import org.thingsboard.server.dao.ExportableEntityDao;
+import org.thingsboard.server.dao.ResourceContainerDao;
import org.thingsboard.server.dao.TenantEntityDao;
import java.util.Collection;
@@ -31,7 +33,7 @@ import java.util.UUID;
/**
* Created by igor on 3/12/18.
*/
-public interface RuleChainDao extends Dao, TenantEntityDao, ExportableEntityDao {
+public interface RuleChainDao extends Dao, TenantEntityDao, ExportableEntityDao, ResourceContainerDao {
/**
* Find rule chains by tenantId and page link.
diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/CalculatedFieldDataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/CalculatedFieldDataValidator.java
index 05b782c26c..67cf32191b 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/service/validator/CalculatedFieldDataValidator.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/service/validator/CalculatedFieldDataValidator.java
@@ -19,7 +19,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.cf.CalculatedField;
import org.thingsboard.server.common.data.cf.configuration.ArgumentsBasedCalculatedFieldConfiguration;
-import org.thingsboard.server.common.data.cf.configuration.RelationQueryDynamicSourceConfiguration;
+import org.thingsboard.server.common.data.cf.configuration.RelationPathQueryDynamicSourceConfiguration;
import org.thingsboard.server.common.data.cf.configuration.ScheduledUpdateSupportedCalculatedFieldConfiguration;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration;
@@ -98,10 +98,10 @@ public class CalculatedFieldDataValidator extends DataValidator
if (!(calculatedField.getConfiguration() instanceof ArgumentsBasedCalculatedFieldConfiguration argumentsBasedCfg)) {
return;
}
- Map relationQueryBasedArguments = argumentsBasedCfg.getArguments().entrySet()
+ Map relationQueryBasedArguments = argumentsBasedCfg.getArguments().entrySet()
.stream()
.filter(entry -> entry.getValue().hasDynamicSource())
- .collect(Collectors.toMap(Map.Entry::getKey, entry -> (RelationQueryDynamicSourceConfiguration) entry.getValue().getRefDynamicSourceConfiguration()));
+ .collect(Collectors.toMap(Map.Entry::getKey, entry -> (RelationPathQueryDynamicSourceConfiguration) entry.getValue().getRefDynamicSourceConfiguration()));
if (relationQueryBasedArguments.isEmpty()) {
return;
}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/asset/AssetRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/asset/AssetRepository.java
index e475864684..aa9c29df49 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/sql/asset/AssetRepository.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/sql/asset/AssetRepository.java
@@ -21,6 +21,7 @@ import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
+import org.thingsboard.server.common.data.EntityInfo;
import org.thingsboard.server.common.data.edqs.fields.AssetFields;
import org.thingsboard.server.common.data.util.TbPair;
import org.thingsboard.server.dao.ExportableEntityRepository;
@@ -103,6 +104,10 @@ public interface AssetRepository extends JpaRepository, Expor
AssetEntity findByTenantIdAndName(UUID tenantId, String name);
+ @Query("SELECT new org.thingsboard.server.common.data.EntityInfo(a.id, 'ASSET', a.name) " +
+ "FROM AssetEntity a WHERE a.tenantId = :tenantId AND a.name LIKE CONCAT(:prefix, '%')")
+ List findEntityInfosByNamePrefix(UUID tenantId, String prefix);
+
@Query("SELECT a FROM AssetEntity a WHERE a.tenantId = :tenantId " +
"AND a.type = :type " +
"AND (:textSearch IS NULL OR ilike(a.name, CONCAT('%', :textSearch, '%')) = true " +
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 4b55884792..593a672d8a 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
@@ -21,6 +21,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Limit;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Component;
+import org.thingsboard.server.common.data.EntityInfo;
import org.thingsboard.server.common.data.EntitySubtype;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.ProfileEntityIdInfo;
@@ -267,6 +268,12 @@ public class JpaAssetDao extends JpaAbstractDao implements A
return nativeAssetRepository.findProfileEntityIdInfosByTenantId(tenantId, DaoUtil.toPageable(pageLink));
}
+ @Override
+ public List findEntityInfosByNamePrefix(TenantId tenantId, String name) {
+ log.debug("Find asset entity infos by name [{}]", name);
+ return assetRepository.findEntityInfosByNamePrefix(tenantId.getId(), name);
+ }
+
@Override
public Long countByTenantId(TenantId tenantId) {
return assetRepository.countByTenantId(tenantId.getId());
diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/customer/CustomerRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/customer/CustomerRepository.java
index 8ad7311423..9c196a5072 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/sql/customer/CustomerRepository.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/sql/customer/CustomerRepository.java
@@ -21,6 +21,7 @@ import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
+import org.thingsboard.server.common.data.EntityInfo;
import org.thingsboard.server.common.data.edqs.fields.CustomerFields;
import org.thingsboard.server.dao.ExportableEntityRepository;
import org.thingsboard.server.dao.model.sql.CustomerEntity;
@@ -41,6 +42,10 @@ public interface CustomerRepository extends JpaRepository,
CustomerEntity findByTenantIdAndTitle(UUID tenantId, String title);
+ @Query("SELECT new org.thingsboard.server.common.data.EntityInfo(a.id, 'CUSTOMER', a.title) " +
+ "FROM CustomerEntity a WHERE a.tenantId = :tenantId AND a.title LIKE CONCAT(:prefix, '%')")
+ List findEntityInfosByNamePrefix(UUID tenantId, String prefix);
+
@Query(value = "SELECT * FROM customer c WHERE c.tenant_id = :tenantId " +
"AND c.is_public IS TRUE ORDER BY c.id ASC LIMIT 1", nativeQuery = true)
CustomerEntity findPublicCustomerByTenantId(@Param("tenantId") UUID tenantId);
diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/customer/JpaCustomerDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/customer/JpaCustomerDao.java
index 75e7179391..2e1d75a738 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/sql/customer/JpaCustomerDao.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/sql/customer/JpaCustomerDao.java
@@ -20,6 +20,7 @@ import org.springframework.data.domain.Limit;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.Customer;
+import org.thingsboard.server.common.data.EntityInfo;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.edqs.fields.CustomerFields;
import org.thingsboard.server.common.data.id.CustomerId;
@@ -117,6 +118,11 @@ public class JpaCustomerDao extends JpaAbstractDao imp
return customerRepository.findNextBatch(id, Limit.of(batchSize));
}
+ @Override
+ public List findEntityInfosByNamePrefix(TenantId tenantId, String name) {
+ return customerRepository.findEntityInfosByNamePrefix(tenantId.getId(), name);
+ }
+
@Override
public EntityType getEntityType() {
return EntityType.CUSTOMER;
diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/DashboardInfoRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/DashboardInfoRepository.java
index 7624ddc738..32ac596562 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/DashboardInfoRepository.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/DashboardInfoRepository.java
@@ -20,6 +20,7 @@ import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
+import org.thingsboard.server.common.data.EntityInfo;
import org.thingsboard.server.dao.model.sql.DashboardInfoEntity;
import java.util.List;
@@ -87,12 +88,15 @@ public interface DashboardInfoRepository extends JpaRepository findByImageLink(@Param("imageLink") String imageLink, @Param("limit") int limit);
- @Query(value = "SELECT * FROM dashboard d WHERE d.tenant_id = :tenantId and d.configuration ILIKE CONCAT('%', :link, '%') limit :limit",
- nativeQuery = true)
- List findDashboardInfosByTenantIdAndResourceLink(@Param("tenantId") UUID tenantId, @Param("link") String link, @Param("limit") int limit);
+ @Query("SELECT new org.thingsboard.server.common.data.EntityInfo(d.id, 'DASHBOARD', d.title) " +
+ "FROM DashboardEntity d WHERE d.tenantId = :tenantId AND ilike(cast(d.configuration as string), CONCAT('%', :link, '%')) = true")
+ List findDashboardInfosByTenantIdAndResourceLink(@Param("tenantId") UUID tenantId,
+ @Param("link") String link,
+ Pageable pageable);
- @Query(value = "SELECT * FROM dashboard d WHERE d.configuration ILIKE CONCAT('%', :link, '%') limit :limit",
- nativeQuery = true)
- List findDashboardInfosByResourceLink(@Param("link") String link, @Param("limit") int limit);
+ @Query("SELECT new org.thingsboard.server.common.data.EntityInfo(d.id, 'DASHBOARD', d.title) " +
+ "FROM DashboardEntity d WHERE ilike(cast(d.configuration as string), CONCAT('%', :link, '%')) = true")
+ List findDashboardInfosByResourceLink(@Param("link") String link,
+ Pageable pageable);
}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/JpaDashboardInfoDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/JpaDashboardInfoDao.java
index bc07139725..0e04c94a46 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/JpaDashboardInfoDao.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/JpaDashboardInfoDao.java
@@ -17,9 +17,11 @@ package org.thingsboard.server.dao.sql.dashboard;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.domain.PageRequest;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.DashboardInfo;
+import org.thingsboard.server.common.data.EntityInfo;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
@@ -135,13 +137,13 @@ public class JpaDashboardInfoDao extends JpaAbstractDao findByTenantIdAndResourceLink(TenantId tenantId, String url, int limit) {
- return DaoUtil.convertDataList(dashboardInfoRepository.findDashboardInfosByTenantIdAndResourceLink(tenantId.getId(), url, limit));
+ public List findByTenantIdAndResource(TenantId tenantId, String reference, int limit) {
+ return dashboardInfoRepository.findDashboardInfosByTenantIdAndResourceLink(tenantId.getId(), reference, PageRequest.of(0, limit));
}
@Override
- public List findByResourceLink(String link, int limit) {
- return DaoUtil.convertDataList(dashboardInfoRepository.findDashboardInfosByResourceLink(link, limit));
+ public List findByResource(String reference, int limit) {
+ return dashboardInfoRepository.findDashboardInfosByResourceLink(reference, PageRequest.of(0, limit));
}
}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/device/DeviceRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/device/DeviceRepository.java
index f4c2fed9fa..a9ec9d1d36 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/sql/device/DeviceRepository.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/sql/device/DeviceRepository.java
@@ -22,6 +22,7 @@ import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.thingsboard.server.common.data.DeviceTransportType;
+import org.thingsboard.server.common.data.EntityInfo;
import org.thingsboard.server.common.data.edqs.fields.DeviceFields;
import org.thingsboard.server.dao.ExportableEntityRepository;
import org.thingsboard.server.dao.model.sql.DeviceEntity;
@@ -151,6 +152,10 @@ public interface DeviceRepository extends JpaRepository, Exp
DeviceEntity findByTenantIdAndName(UUID tenantId, String name);
+ @Query("SELECT new org.thingsboard.server.common.data.EntityInfo(a.id, 'DEVICE', a.name) " +
+ "FROM DeviceEntity a WHERE a.tenantId = :tenantId AND a.name LIKE CONCAT(:prefix, '%')")
+ List findEntityInfosByNamePrefix(UUID tenantId, String prefix);
+
List findDevicesByTenantIdAndCustomerIdAndIdIn(UUID tenantId, UUID customerId, List deviceIds);
List