given(attrService.removeAll(tenantId,deviceId,request.getScope(),request.getKeys())).willReturn(immediateFailedFuture(newRuntimeException("failed to delete")));
nodeDetails="Saves entity attributes based on configurable scope parameter. Expects messages with 'POST_ATTRIBUTES_REQUEST' message type. "+
"If upsert(update/insert) operation is completed successfully rule node will send the incoming message via <b>Success</b> chain, otherwise, <b>Failure</b> chain is used. "+
"Additionally if checkbox <b>Send attributes updated notification</b> is set to true, rule node will put the \"Attributes Updated\" "+
"event for <b>SHARED_SCOPE</b> and <b>SERVER_SCOPE</b> attributes updates to the corresponding rule engine queue."+
"Performance checkbox 'Save attributes only if the value changes' will skip attributes overwrites for values with no changes (avoid concurrent writes because this check is not transactional; will not update 'Last updated time' for skipped attributes).",
<li><strong>WebSocketsonly:</strong>forallactionsexceptWebSocketnotifications,the"Skip"strategyisapplied,whileWebSocketnotificationsusethe"On every message"strategy.</li>
<li><strong>WebSocketsonly:</strong>appliesthe"Skip"strategytoTimeseriesandLatestvalues,andthe"On every message"strategytoWebSockets.</li>
<li><strong>WebSocketsonly:</strong>forallactionsexceptWebSocketnotifications,the"Skip"strategyisapplied,whileWebSocketnotificationsusethe"On every message"strategy.</li>
The **calculate()** function is a user-defined script that allows you to perform custom calculations using [TBEL{:target="_blank"}](${siteBaseUrl}/docs${docPlatformPrefix}/user-guide/tbel/) on telemetry and attribute data.
It receives arguments configured in the calculated field setup and an additional `ctx` object, which provides access to all arguments.
##### Function signature
```javascript
function calculate(ctx, arg1, arg2, ...): object | object[]
```
##### Argument representation in the script
Before describing how arguments are passed to the function, let's define how different argument types are **represented** inside the script.
There are two types of arguments that can be used in the function:
* single value arguments - represent the latest telemetry data or attribute.
```json
{
"altitude": {
"ts": 1740644636669,
"value": 1034
}
}
```
* when accessed via `ctx.args`, they remain objects:
```javascript
var altitudeTimestamp = ctx.args.altitude.ts;
var altitudeValue = ctx.args.altitude.value;
```
* when accessed as a **function parameter**, only the value is passed:
```javascript
function calculate(ctx, altitude/*(single value argument)*/, temperature/*(time series rolling argument)*/) {
// altitude = 1035
}
```
* time series rolling arguments - contain historical data within a defined time window.
```json
{
"temperature": {
"timeWindow": {
"startTs": 1740643762896,
"endTs": 1740644662896
},
"values": [
{ "ts": 1740644355935, "value": 72.32 },
{ "ts": 1740644365935, "value": 72.86 },
{ "ts": 1740644375935, "value": 73.58 },
{ "ts": 1740644385935, "value": "NaN" }
]
}
}
```
* when accessed via `ctx.args`, they remain rolling argument objects:
```javascript
var startOfInterval = temperature.timeWindow.startTs;
var firstTimestamp = temperature.values[0].ts;
var firstValue = temperature.values[0].value;
```
* when accessed as a **function parameter**, they are passed as rolling arguments, retaining their structure:
```javascript
function calculate(ctx, altitude/*(single value argument)*/, temperature/*(time series rolling argument)*/) {
var avgTemp = temperature.mean(); // Use rolling argument functions
}
```
**Built-in methods for rolling arguments**
Time series rolling arguments provide built-in functions for calculations.
These functions accept an optional `ignoreNaN` boolean parameter, which controls how NaN values are handled.
Each function has two function signatures:
* **Without parameters:** `method()` → called **without parameters** and defaults to `ignoreNaN = true`, meaning NaN values are ignored.
* **With an explicit parameter:** `method(boolean ignoreNaN)` → called with a boolean `ignoreNaN` parameter:
| `merge(other, settings)` | Merges the current rolling argument with another rolling argument by aligning timestamps and filling missing values with the previous available value. | <ul><li>`other` (another rolling argument)</li><li>`settings` (optional) - configuration object, supports:<ul><br/><li>`ignoreNaN` (boolean, default true) - controls whether NaN values should be ignored.</li><li>`timeWindow` (object, default {}) - defines a custom time window for filtering merged values.</li></ul></li></ul>| Merged object with timeWindow and aligned values. |
| `mergeAll(others, settings)` | Merges the current rolling argument with multiple rolling arguments by aligning timestamps and filling missing values with the previous available value. | <ul><li>`others` (array of rolling arguments)</li><li>`settings` (optional) - configuration object, supports:<ul><br/><li>`ignoreNaN` (boolean, default true) - controls whether NaN values should be ignored.</li><li>`timeWindow` (object, default {}) - defines a custom time window for filtering merged values.</li></ul></li></ul>| Merged object with timeWindow and aligned values.|
**Example arguments:**
```json
{
"humidity": {
"timeWindow": {
"startTs": 1741356332086,
"endTs": 1741357232086
},
"values": [{
"ts": 1741356882759,
"value": 43
}, {
"ts": 1741356918779,
"value": 46
}]
},
"pressure": {
"timeWindow": {
"startTs": 1741356332086,
"endTs": 1741357232086
},
"values": [{
"ts": 1741357047945,
"value": 1023
}, {
"ts": 1741357056144,
"value": 1026
}, {
"ts": 1741357147391,
"value": 1025
}]
},
"temperature": {
"timeWindow": {
"startTs": 1741356332086,
"endTs": 1741357232086
},
"values": [{
"ts": 1741356874943,
"value": 76
}, {
"ts": 1741357063689,
"value": 77
}]
}
}
```
**Usage:**
```javascript
var mergedData = temperature.merge(humidity, { ignoreNaN: false });
```
**Output:**
```json
{
"mergedData": {
"timeWindow": {
"startTs": 1741356332086,
"endTs": 1741357232086
},
"values": [{
"ts": 1741356874943,
"values": [76.0, "NaN"]
}, {
"ts": 1741356882759,
"values": [76.0, 43.0]
}, {
"ts": 1741356918779,
"values": [76.0, 46.0]
}, {
"ts": 1741357063689,
"values": [77.0, 46.0]
}]
}
}
```
**Usage:**
```javascript
var mergedData = temperature.mergeAll([humidity, pressure], { ignoreNaN: true });
```
**Output:**
```json
{
"mergedData": {
"timeWindow": {
"startTs": 1741356332086,
"endTs": 1741357232086
},
"values": [{
"ts": 1741357047945,
"values": [76.0, 46.0, 1023.0]
}, {
"ts": 1741357056144,
"values": [76.0, 46.0, 1026.0]
}, {
"ts": 1741357063689,
"values": [77.0, 46.0, 1026.0]
}, {
"ts": 1741357147391,
"values": [77.0, 46.0, 1025.0]
}]
}
}
```
##### Function arguments
* `ctx` - context object that contains all provided arguments, in the representations described above.
Accessing arguments via `ctx`:
```javascript
var altitude = ctx.args.altitude; // single value argument
var temperature = ctx.args.temperature; // time series rolling argument
```
* `arg1, arg2, ...` - user-defined arguments configured in the calculated field setup.
How they are passed depends on their type:
* **single value arguments** are passed as raw values **(e.g., 22.5, "ON")**.
* **time series rolling arguments** are passed as objects (containing multiple values).
##### Example: Air Density Calculation
This function calculates air density using `altitude` (single value argument) and `temperature` (time series rolling argument).
```javascript
function calculate(ctx, altitude, temperature) {
var avgTemperature = temperature.mean(); // Get average temperature
var temperatureK = (avgTemperature - 32) * (5 / 9) + 273.15; // Convert Fahrenheit to Kelvin
var airDensity = pressure / (287.05 * temperatureK);
return {
"airDensity": airDensity
};
}
```
* `altitude` is a single value, passed as number.
* `temperature` is a rolling argument, retaining its full structure.
##### Function return format
The script should return a JSON object formatted according to the [ThingsBoard Telemetry Upload API](${siteBaseUrl}/docs${docPlatformPrefix}/user-guide/telemetry/#time-series-data-upload-api/).
The return value must match one of the supported telemetry upload formats.
The script must return data in a format compatible with ThingsBoard’s APIs.
The correct return format depends on the calculated field output configuration:
* if latest telemetry is used for output, the function must return data according to the [Telemetry Upload API](${siteBaseUrl}/docs${docPlatformPrefix}/reference/mqtt-api/#attributes-api/).
* without timestamps
```json
{
"airDensity": 1.06,
"someKey": "value"
}
```
* with a timestamp:
```json
{
"ts": 1740644636669,
"values": {
"airDensity": 1.06,
"someKey": "value"
}
}
```
* if attributes are used for output, the function must return data according to the [Attributes API](${siteBaseUrl}/docs${docPlatformPrefix}/user-guide/telemetry/#time-series-data-upload-api/).
#### Potential unexpected behavior with mixed processing strategies
When configuring the processing strategies, certain combinations can lead to unexpected behavior. Consider the following scenarios:
- **Skipping database storage**
Choosing to disable attribute persistence introduces the risk of having only partial data available.
For example, if a message is processed solely for real-time notifications via WebSockets and not stored in the database, then attribute queries might not reflect the data visible on the dashboard.
- **Disabling WebSocket (WS) updates**
If WS updates are disabled, any changes to the attribute data won’t be pushed to dashboards (or other WS subscriptions).
This means that even if a database is updated, dashboards may not display the updated data until browser page is reloaded.
- **Skipping calculated field recalculation**
If attribute data is saved to the database while bypassing calculated field recalculation, the aggregated value may not update to reflect the saved data.
Conversely, if the calculated field is recalculated with new data but the corresponding attribute value is not persisted in the database, the calculated field's value might include data that isn’t stored.
- **Different deduplication intervals across actions**
When you configure different deduplication intervals for actions, the same incoming message might be processed differently for each action.
For example, a message might be stored immediately in the Attributes table (if set to *On every message*) while not being present on a dashboard because its deduplication interval hasn’t elapsed.
- **Deduplication cache clearing**
The deduplication mechanism uses a cache to track processed messages within each interval.
For performance and system stability reasons, this cache is periodically cleared.
As a result, if a cache entry is removed during the deduplication period, messages from the same originator may be processed more than once within that interval.
This means deduplication should be used as a performance optimization rather than an absolute guarantee of single processing per interval.
We recommend using deduplication only when the occasional repeated processing is acceptable and won't cause system correctness issue or data inconsistencies.
"argument-name-pattern":"Argument name is invalid.",
"argument-name-duplicate":"Argument with such name already exists.",
"argument-name-max-length":"Argument name should be less than 256 characters.",
"argument-name-ctx":"Argument name 'ctx' is reserved and cannot be used.",
"argument-type-required":"Argument type is required.",
"max-args":"Maximum number of arguments reached.",
"decimals-range":"Decimals by default should be a number between 0 and 15.",
"expression":"Default expression demonstrates how to transform a temperature from Fahrenheit to Celsius."
}
},
@ -2521,8 +2525,8 @@
"type-current-tenant":"Current Tenant",
"type-current-user":"Current User",
"type-current-user-owner":"Current User Owner",
"type-calculated-field":"Calculated Field",
"type-calculated-fields":"Calculated Fields",
"type-calculated-field":"Calculated field",
"type-calculated-fields":"Calculated fields",
"type-widgets-bundle":"Widgets bundle",
"type-widgets-bundles":"Widgets bundles",
"list-of-widgets-bundles":"{ count, plural, =1 {One widgets bundle} other {List of # widget bundles} }",
@ -5260,6 +5264,23 @@
"web-sockets":"WebSockets",
"calculated-fields":"Calculated fields"
},
"save-attribute":{
"processing-settings":"Processing settings",
"processing-settings-hint":"Define how incoming messages are processed. In Basic mode, select a preconfigured processing strategy or enable only WebSocket updates. Advanced mode allows you to select individual processing strategies for each action.",
"advanced-settings-hint":"Be cautious when configuring processing strategies. Certain combinations can lead to unexpected behavior.",