From 5f3fa5eb9d2b440184d24a5dbc71fc12c8ab79b3 Mon Sep 17 00:00:00 2001 From: Dmytro Skarzhynets Date: Wed, 13 Aug 2025 22:49:20 +0300 Subject: [PATCH 1/5] AI request node: add predictive maintenance example --- .../en_US/rulenode/ai_node_prompt_settings.md | 171 ++++++++++++++++++ 1 file changed, 171 insertions(+) diff --git a/ui-ngx/src/assets/help/en_US/rulenode/ai_node_prompt_settings.md b/ui-ngx/src/assets/help/en_US/rulenode/ai_node_prompt_settings.md index e2577244c1..1c2e537efc 100644 --- a/ui-ngx/src/assets/help/en_US/rulenode/ai_node_prompt_settings.md +++ b/ui-ngx/src/assets/help/en_US/rulenode/ai_node_prompt_settings.md @@ -1,3 +1,174 @@ +#### Example Usage: AI-Powered Predictive Maintenance + +This example demonstrates how to use the AI request node to analyze telemetry from rotating equipment for a predictive maintenance use case. + +##### Scenario + +Assume you’re monitoring a centrifugal pump that streams vibration, temperature, and acoustic readings. +To catch problems early and avoid downtime, you can use AI to analyze the telemetry for signs of **Bearing Wear**, **Misalignment**, **Overheating**, or **Imbalance** and return an alarm object if found. Downstream nodes can use it to create a ThingsBoard alarm and notify the maintenance team. + +1. **Incoming message structure** + +First, we need to collect telemetry readings. This can be achieved either by configuring a Calculated Field with the “Time series rolling” arguments to gather recent samples, +or by running a periodic check using nodes like "generator" and "originator telemetry" to fetch the latest samples and assemble the payload. + +Message payload (shortened for brevity): + +```json +{ + "acousticDeviation": { + "timeWindow": { "startTs": 1755093373000, "endTs": 1755093414551 }, + "values": [ + { "ts": 1755093373000, "value": 5.0 }, + { "ts": 1755093373100, "value": 18.0 }, + { "ts": 1755093373200, "value": 17.0 }, + { "ts": 1755093414380, "value": 5.0 }, + { "ts": 1755093414551, "value": 17.0 } + ] + }, + "temperature": { + "timeWindow": { "startTs": 1755093373000, "endTs": 1755093414551 }, + "values": [ + { "ts": 1755093373000, "value": 70.0 }, + { "ts": 1755093373120, "value": 86.0 }, + { "ts": 1755093373200, "value": 84.0 }, + { "ts": 1755093414380, "value": 70.0 }, + { "ts": 1755093414551, "value": 84.0 } + ] + }, + "vibration": { + "timeWindow": { "startTs": 1755093373000, "endTs": 1755093414551 }, + "values": [ + { "ts": 1755093373000, "value": 4.2 }, + { "ts": 1755093373120, "value": 7.4 }, + { "ts": 1755093373182, "value": 8.0 }, + { "ts": 1755093414437, "value": 6.2 }, + { "ts": 1755093414551, "value": 7.2 } + ] + } +} +``` + +Message metadata: + +```json +{ + "deviceName": "Pump-103", + "deviceType": "CentrifugalPump" +} +``` + +2. **Prompt configuration** + +As a second step, we need to explain the task to AI model. Describe the context of your device and the desired response format (in this case, minimal ThingsBoard alarm JSON object) in the system prompt. We will also put the task description in the system prompt since it does not change depending on a message. In the user prompt, we will use templates to dynamically inject telemetry data produced by the device. + +**System prompt** + +``` +You are an AI predictive maintenance assistant that detects alarm conditions in telemetry data of industrial devices based on incident patterns. + +Output JSON only. If an alarm condition is detected, output: +{ + "type": "Bearing Wear | Misalignment | Overheating | Imbalance", + "severity": "CRITICAL | MAJOR | MINOR | WARNING", + "details": { "summary": "2–3 sentences in plain English of concise, plain-language summary for maintenance teams; include units when citing values." } +} +If no alarm condition is detected, output: {} + +Inputs: time-stamped vibration (mm/s), temperature (°C), acoustic spectrum deviation (%). + +Telemetry thresholds: +- Vibration (mm/s): ≤4.5 normal; 4.5–5.0 WARNING; 5.0–6.0 MINOR; 6.0–7.1 MAJOR; >7.1 CRITICAL +- Temperature (°C): ≤75 normal; 75–80 WARNING; 80–85 MAJOR; >85 CRITICAL +- Acoustic deviation (%): ≤15 normal; 15–25 WARNING; 25–40 MINOR; 40–60 MAJOR; >60 CRITICAL + +Incident patterns: +- Bearing Wear: gradual vibration rise + temperature spike. +- Misalignment: sudden vibration spike without temperature change. +- Imbalance: rising vibration + irregular acoustics; temperature near normal. +- Overheating: temperature >85 °C ≥10 min or Δtemp ≥10 °C/10 min with Δvib <1.0 mm/s; or 75–85 °C ≥30 min with normal vib/acoustic. + +Severity policy: +- Start with the max of per-signal severities; if ≥2 signals are abnormal, escalate one level (cap at CRITICAL). +- Ignore very brief blips (<2 samples just over a boundary) unless strongly pattern-matched. +- Be conservative; use input units. +``` + +**User prompt** + +``` +Analyze telemetry from a "${deviceType}" named "${deviceName}". + +Data: +$[*] +``` +3. **Response format** (optional) + +In the previous step, we described desired response format in the system prompt, but it is possible to enforce format with JSON Schema if model you are using supports it. We recommend using JSON Schema if possible. Here is the example of response schema you can use (if using JSON Schema, in the system prompt just say that model output should be in JSON format): + +```json +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "Alarm", + "type": "object", + "additionalProperties": false, + "required": ["type", "severity", "details"], + "properties": { + "type": { + "type": "string", + "description": "Incident type", + "enum": ["Bearing Wear", "Misalignment", "Imbalance", "Overheating"] + }, + "severity": { + "type": "string", + "description": "Severity level of the incident", + "enum": ["WARNING", "MINOR", "MAJOR", "CRITICAL"] + }, + "details": { + "type": "object", + "additionalProperties": false, + "required": ["summary"], + "properties": { + "summary": { + "type": "string", + "description": "2–3 sentences in plain English of concise, plain-language summary for maintenance teams; include units when citing values." + } + } + } + } +} +``` + +4. **How it works** + +When the message containing sample data from **Pump-103** is processed, the templates are substituted: + +* `${deviceName}` → `"Pump-103"` +* `${deviceType}` → `"CentrifugalPump"` +* `$[*]` → the entire message payload JSON (e.g. telemetry data we collected in step 1) + +> **Tip:** `${*}` can substitute the entire metadata JSON if needed. + +The final instruction sent to the model is the System prompt plus the substituted User prompt. AI response will be placed in outgoing message payload. + +5. **Expected AI output** + +Given the sample data from step 1, the AI will likely output something like this: + +```json +{ + "type": "Bearing Wear", + "severity": "CRITICAL", + "details": { + "summary": "Pump-103 showed a vibration spike from 4.2 to 8.0 mm/s with continued elevated levels (6.2–7.2 mm/s), along with a temperature spike to 86 °C and recurrent 84 °C. With acoustics only in the WARNING band (~17–18%), this pattern indicates bearing wear; inspect and replace the bearing promptly to prevent failure." + } +} +``` + +6. **Next steps** + +Check the AI response: if it’s a non-empty object, it’s ready-to-use alarm JSON. Route it directly to the "create alarm" node to create an alarm. If it is empty, just ignore the output as everything is normal. + #### Example Usage: AI-Powered Alarm Analysis This example demonstrates how to use the AI node to automatically analyze a new device alarm, generate a human-readable summary, and suggest troubleshooting steps. From b9d402abba7bf87379968337e7d9a7a591daf6c0 Mon Sep 17 00:00:00 2001 From: IrynaMatveieva Date: Thu, 14 Aug 2025 09:39:42 +0300 Subject: [PATCH 2/5] parse long to int --- .../ctx/state/SingleValueArgumentEntry.java | 5 ++++ .../state/ScriptCalculatedFieldStateTest.java | 24 +++++++++++++++---- 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/SingleValueArgumentEntry.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/SingleValueArgumentEntry.java index 1585c9b2a9..1ceea2c621 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/SingleValueArgumentEntry.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/SingleValueArgumentEntry.java @@ -101,6 +101,11 @@ public class SingleValueArgumentEntry implements ArgumentEntry { } catch (Exception e) { } } + if (value instanceof Long longValue) { + if (longValue >= Integer.MIN_VALUE && longValue <= Integer.MAX_VALUE) { + value = longValue.intValue(); + } + } return new TbelCfSingleValueArg(ts, value); } diff --git a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/ScriptCalculatedFieldStateTest.java b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/ScriptCalculatedFieldStateTest.java index 91eced64f6..8ed42c43e8 100644 --- a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/ScriptCalculatedFieldStateTest.java +++ b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/ScriptCalculatedFieldStateTest.java @@ -20,7 +20,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.script.api.tbel.DefaultTbelInvokeService; import org.thingsboard.script.api.tbel.TbelInvokeService; @@ -60,7 +60,7 @@ public class ScriptCalculatedFieldStateTest { private final DeviceId DEVICE_ID = new DeviceId(UUID.fromString("5512071d-5abc-411d-a907-4cdb6539c2eb")); private final AssetId ASSET_ID = new AssetId(UUID.fromString("5bc010ae-bcfd-46c8-98b9-8ee8c8955a76")); - private final SingleValueArgumentEntry assetHumidityArgEntry = new SingleValueArgumentEntry(System.currentTimeMillis() - 10, new DoubleDataEntry("assetHumidity", 43.0), 122L); + private final SingleValueArgumentEntry assetHumidityArgEntry = new SingleValueArgumentEntry(System.currentTimeMillis() - 10, new DoubleDataEntry("assetHumidity", 86.0), 122L); private final TsRollingArgumentEntry deviceTemperatureArgEntry = createRollingArgEntry(); private final long ts = System.currentTimeMillis(); @@ -71,7 +71,7 @@ public class ScriptCalculatedFieldStateTest { @Autowired private TbelInvokeService tbelInvokeService; - @MockBean + @MockitoBean private ApiLimitService apiLimitService; @BeforeEach @@ -133,6 +133,22 @@ public class ScriptCalculatedFieldStateTest { assertThat(result.getResult()).isEqualTo(JacksonUtil.valueToTree(Map.of("maxDeviceTemperature", 17.0, "assetHumidity", 43.0))); } + @Test + void testPerformCalculationWithLongEntry() throws ExecutionException, InterruptedException { + state.arguments = new HashMap<>(Map.of( + "deviceTemperature", deviceTemperatureArgEntry, + "assetHumidity", new SingleValueArgumentEntry(System.currentTimeMillis() - 10, new LongDataEntry("a", 45L), 10L) + )); + + CalculatedFieldResult result = state.performCalculation(ctx).get(); + + assertThat(result).isNotNull(); + Output output = getCalculatedFieldConfig().getOutput(); + assertThat(result.getType()).isEqualTo(output.getType()); + assertThat(result.getScope()).isEqualTo(output.getScope()); + assertThat(result.getResult()).isEqualTo(JacksonUtil.valueToTree(Map.of("maxDeviceTemperature", 17.0, "assetHumidity", 22.5))); + } + @Test void testIsReadyWhenNotAllArgPresent() { assertThat(state.isReady()).isFalse(); @@ -193,7 +209,7 @@ public class ScriptCalculatedFieldStateTest { config.setArguments(Map.of("deviceTemperature", argument1, "assetHumidity", argument2)); - config.setExpression("return {\"maxDeviceTemperature\": deviceTemperature.max(), \"assetHumidity\": assetHumidity}"); + config.setExpression("return {\"maxDeviceTemperature\": deviceTemperature.max(), \"assetHumidity\": assetHumidity / 2 }"); Output output = new Output(); output.setType(OutputType.ATTRIBUTES); From 96df3819ac099605e7379131c246932e60c99b48 Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Thu, 14 Aug 2025 10:25:55 +0300 Subject: [PATCH 3/5] UI: Fixed tranlate format in da_DK --- ui-ngx/src/assets/locale/locale.constant-da_DK.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui-ngx/src/assets/locale/locale.constant-da_DK.json b/ui-ngx/src/assets/locale/locale.constant-da_DK.json index c77b87f3e0..624053b088 100644 --- a/ui-ngx/src/assets/locale/locale.constant-da_DK.json +++ b/ui-ngx/src/assets/locale/locale.constant-da_DK.json @@ -5276,7 +5276,7 @@ "add-originator-attributes-to": "Tilføj afsenders attributter til", "originator-attributes": "Afsenders attributter", "fetch-latest-telemetry-with-timestamp": "Hent seneste telemetry med tidsstempel", - "fetch-latest-telemetry-with-timestamp-tooltip": "Inkluderer tidsstempel i metadata, f.eks.: \"{{latestTsKeyName}}\": \"{ts:1574329385897, value:42}\"", + "fetch-latest-telemetry-with-timestamp-tooltip": "Inkluderer tidsstempel i metadata, f.eks.: \"{{latestTsKeyName}}\": \"{\"ts\":1574329385897, \"value\":42}\"", "tell-failure": "Rapportér fejl hvis attribut mangler", "tell-failure-tooltip": "Rapporterer fejl hvis mindst én valgt nøgle mangler.", "created-time": "Oprettelsestid", From 30cfda777b3026f10f41f41be52fdd728d56d930 Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Thu, 14 Aug 2025 15:44:17 +0300 Subject: [PATCH 4/5] UI: Fixed autofill password --- .../home/components/ai-model/ai-model-dialog.component.html | 6 +++--- .../rule-node/common/credentials-config.component.html | 4 ++-- .../rule-node/external/rabbit-mq-config.component.html | 2 +- .../rule-node/external/send-email-config.component.html | 2 +- .../sms/aws-sns-provider-configuration.component.html | 2 +- .../app/modules/home/pages/admin/mail-server.component.html | 2 +- 6 files changed, 9 insertions(+), 9 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/ai-model/ai-model-dialog.component.html b/ui-ngx/src/app/modules/home/components/ai-model/ai-model-dialog.component.html index 860e615410..5f9c189399 100644 --- a/ui-ngx/src/app/modules/home/components/ai-model/ai-model-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/ai-model/ai-model-dialog.component.html @@ -59,7 +59,7 @@ @if (providerFieldsList.includes('personalAccessToken')) { ai-models.personal-access-token - + {{ 'ai-models.personal-access-token-required' | translate }} @@ -115,7 +115,7 @@ @if (providerFieldsList.includes('apiKey')) { ai-models.api-key - + {{ 'ai-models.api-key-required' | translate }} @@ -143,7 +143,7 @@ @if (providerFieldsList.includes('secretAccessKey')) { ai-models.secret-access-key - + {{ 'ai-models.secret-access-key-required' | translate }} diff --git a/ui-ngx/src/app/modules/home/components/rule-node/common/credentials-config.component.html b/ui-ngx/src/app/modules/home/components/rule-node/common/credentials-config.component.html index 7f7d50db3e..59bfa7ce9f 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/common/credentials-config.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/common/credentials-config.component.html @@ -49,7 +49,7 @@ rule-node-config.password - + {{ 'rule-node-config.password-required' | translate }} @@ -85,7 +85,7 @@ rule-node-config.private-key-password - + diff --git a/ui-ngx/src/app/modules/home/components/rule-node/external/rabbit-mq-config.component.html b/ui-ngx/src/app/modules/home/components/rule-node/external/rabbit-mq-config.component.html index 0676af17f9..6adf2e6a4e 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/external/rabbit-mq-config.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/external/rabbit-mq-config.component.html @@ -64,7 +64,7 @@ rule-node-config.password - + diff --git a/ui-ngx/src/app/modules/home/components/rule-node/external/send-email-config.component.html b/ui-ngx/src/app/modules/home/components/rule-node/external/send-email-config.component.html index 0e9db748bf..545f9ba985 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/external/send-email-config.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/external/send-email-config.component.html @@ -109,7 +109,7 @@ rule-node-config.password - + diff --git a/ui-ngx/src/app/modules/home/components/sms/aws-sns-provider-configuration.component.html b/ui-ngx/src/app/modules/home/components/sms/aws-sns-provider-configuration.component.html index 14d4a227ff..afd272e1cd 100644 --- a/ui-ngx/src/app/modules/home/components/sms/aws-sns-provider-configuration.component.html +++ b/ui-ngx/src/app/modules/home/components/sms/aws-sns-provider-configuration.component.html @@ -25,7 +25,7 @@ admin.aws-secret-access-key - + {{ 'admin.aws-secret-access-key-required' | translate }} diff --git a/ui-ngx/src/app/modules/home/pages/admin/mail-server.component.html b/ui-ngx/src/app/modules/home/pages/admin/mail-server.component.html index 948571dbe0..a3dc30da87 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/mail-server.component.html +++ b/ui-ngx/src/app/modules/home/pages/admin/mail-server.component.html @@ -136,7 +136,7 @@ admin.proxy-password - + From b24b404a2472104f5495ce11df0334ce402514e5 Mon Sep 17 00:00:00 2001 From: Andrii Shvaika Date: Fri, 15 Aug 2025 11:15:25 +0300 Subject: [PATCH 5/5] Netty version overwrite from Spring Boot to 4.1.124 --- pom.xml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index a20594e4c7..189e81411f 100755 --- a/pom.xml +++ b/pom.xml @@ -146,6 +146,7 @@ 9.2.0 1.1.10.5 9.10.0 + 4.1.124.Final @@ -895,6 +896,13 @@ + + io.netty + netty-bom + ${netty.version} + pom + import + org.springframework.boot spring-boot-dependencies @@ -909,7 +917,6 @@ pom import - org.thingsboard netty-mqtt