Merge branch 'master' into fix-edge-zombie-consumer-cleanup
This commit is contained in:
commit
dccb4e5136
@ -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);
|
||||
}
|
||||
|
||||
|
||||
@ -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);
|
||||
|
||||
9
pom.xml
9
pom.xml
@ -146,6 +146,7 @@
|
||||
<firebase-admin.version>9.2.0</firebase-admin.version>
|
||||
<snappy.version>1.1.10.5</snappy.version>
|
||||
<rocksdbjni.version>9.10.0</rocksdbjni.version>
|
||||
<netty.version>4.1.124.Final</netty.version>
|
||||
</properties>
|
||||
|
||||
<modules>
|
||||
@ -895,6 +896,13 @@
|
||||
|
||||
<dependencyManagement>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>io.netty</groupId>
|
||||
<artifactId>netty-bom</artifactId>
|
||||
<version>${netty.version}</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-dependencies</artifactId>
|
||||
@ -909,7 +917,6 @@
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.thingsboard</groupId>
|
||||
<artifactId>netty-mqtt</artifactId>
|
||||
|
||||
@ -59,7 +59,7 @@
|
||||
@if (providerFieldsList.includes('personalAccessToken')) {
|
||||
<mat-form-field class="mat-block flex-1" appearance="outline">
|
||||
<mat-label translate>ai-models.personal-access-token</mat-label>
|
||||
<input type="password" required matInput formControlName="personalAccessToken">
|
||||
<input type="password" required matInput formControlName="personalAccessToken" autocomplete="new-password">
|
||||
<tb-toggle-password matSuffix></tb-toggle-password>
|
||||
<mat-error *ngIf="aiModelForms.get('configuration').get('providerConfig').get('personalAccessToken').hasError('required')">
|
||||
{{ 'ai-models.personal-access-token-required' | translate }}
|
||||
@ -115,7 +115,7 @@
|
||||
@if (providerFieldsList.includes('apiKey')) {
|
||||
<mat-form-field class="mat-block flex-1" appearance="outline">
|
||||
<mat-label translate>ai-models.api-key</mat-label>
|
||||
<input type="password" required matInput formControlName="apiKey">
|
||||
<input type="password" required matInput formControlName="apiKey" autocomplete="new-password">
|
||||
<tb-toggle-password matSuffix></tb-toggle-password>
|
||||
<mat-error *ngIf="aiModelForms.get('configuration').get('providerConfig').get('apiKey').hasError('required')">
|
||||
{{ 'ai-models.api-key-required' | translate }}
|
||||
@ -143,7 +143,7 @@
|
||||
@if (providerFieldsList.includes('secretAccessKey')) {
|
||||
<mat-form-field class="mat-block flex-1" appearance="outline">
|
||||
<mat-label translate>ai-models.secret-access-key</mat-label>
|
||||
<input type="password" required matInput formControlName="secretAccessKey">
|
||||
<input type="password" required matInput formControlName="secretAccessKey" autocomplete="new-password">
|
||||
<tb-toggle-password matSuffix></tb-toggle-password>
|
||||
<mat-error *ngIf="aiModelForms.get('configuration').get('providerConfig').get('secretAccessKey').hasError('required')">
|
||||
{{ 'ai-models.secret-access-key-required' | translate }}
|
||||
|
||||
@ -49,7 +49,7 @@
|
||||
</mat-form-field>
|
||||
<mat-form-field class="mat-block">
|
||||
<mat-label translate>rule-node-config.password</mat-label>
|
||||
<input type="password" [required]="passwordFieldRequired" matInput formControlName="password">
|
||||
<input type="password" [required]="passwordFieldRequired" matInput formControlName="password" autocomplete="new-password">
|
||||
<tb-toggle-password matSuffix></tb-toggle-password>
|
||||
<mat-error *ngIf="credentialsConfigFormGroup.get('password').hasError('required')">
|
||||
{{ 'rule-node-config.password-required' | translate }}
|
||||
@ -85,7 +85,7 @@
|
||||
</tb-file-input>
|
||||
<mat-form-field class="mat-block">
|
||||
<mat-label translate>rule-node-config.private-key-password</mat-label>
|
||||
<input type="password" matInput formControlName="password">
|
||||
<input type="password" matInput formControlName="password" autocomplete="new-password">
|
||||
<tb-toggle-password matSuffix></tb-toggle-password>
|
||||
</mat-form-field>
|
||||
</ng-template>
|
||||
|
||||
@ -64,7 +64,7 @@
|
||||
</mat-form-field>
|
||||
<mat-form-field class="mat-block">
|
||||
<mat-label translate>rule-node-config.password</mat-label>
|
||||
<input type="password" matInput formControlName="password">
|
||||
<input type="password" matInput formControlName="password" autocomplete="new-password">
|
||||
<tb-toggle-password matSuffix></tb-toggle-password>
|
||||
</mat-form-field>
|
||||
<mat-checkbox formControlName="automaticRecoveryEnabled">
|
||||
|
||||
@ -109,7 +109,7 @@
|
||||
</mat-form-field>
|
||||
<mat-form-field class="mat-block" floatLabel="always">
|
||||
<mat-label translate>rule-node-config.password</mat-label>
|
||||
<input matInput type="password" placeholder="{{ 'rule-node-config.enter-password' | translate }}" formControlName="password">
|
||||
<input matInput type="password" placeholder="{{ 'rule-node-config.enter-password' | translate }}" formControlName="password" autocomplete="new-password">
|
||||
<tb-toggle-password matSuffix></tb-toggle-password>
|
||||
</mat-form-field>
|
||||
</section>
|
||||
|
||||
@ -25,7 +25,7 @@
|
||||
</mat-form-field>
|
||||
<mat-form-field class="mat-block">
|
||||
<mat-label translate>admin.aws-secret-access-key</mat-label>
|
||||
<input required type="password" matInput formControlName="secretAccessKey">
|
||||
<input required type="password" matInput formControlName="secretAccessKey" autocomplete="new-password">
|
||||
<tb-toggle-password matSuffix></tb-toggle-password>
|
||||
<mat-error *ngIf="awsSnsProviderConfigurationFormGroup.get('secretAccessKey').hasError('required')">
|
||||
{{ 'admin.aws-secret-access-key-required' | translate }}
|
||||
|
||||
@ -136,7 +136,7 @@
|
||||
</mat-form-field>
|
||||
<mat-form-field class="mat-block flex-1">
|
||||
<mat-label translate>admin.proxy-password</mat-label>
|
||||
<input matInput type="password" formControlName="proxyPassword" autocomplete="new-proxy-password">
|
||||
<input matInput type="password" formControlName="proxyPassword" autocomplete="new-password">
|
||||
<tb-toggle-password matSuffix></tb-toggle-password>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
@ -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.
|
||||
|
||||
|
||||
@ -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",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user