- 物联网
- 后端
- 数据可视化
- 消息队列
【免费下载链接】thingsboard
All-in-one IoT Platform - Device management, data collection, processing and visualization.
计算字段(Calculated Field)是 ThingsBoard 平台内置的边缘/云端数据计算能力,它允许用户不写一行 Java 代码,仅通过 TBEL(ThingsBoard Expression Language)脚本在遥测数据与属性数据之上完成自定义计算、数据聚合与异常分析。本文以 expression_fn.md 为核心骨架,结合仓库中 CalculatedFieldCtx、TbelCfTsRollingArg 等源码实现,系统讲解
calculate()函数的签名、三类参数访问方式、时间序列滚动参数的聚合与合并能力,以及时间序列/属性两种输出格式。读完本文,你将能独立编写、调试并部署一个可落地的计算字段脚本。
函数签名与执行模型
计算字段的 TBEL 脚本由用户自定义函数calculate()构成,其完整签名如下:
function calculate(ctx, arg1, arg2, ...): object | object[]ctx:一个上下文对象,包含latestTs属性以及args映射,用于访问全部参数;arg1, arg2, ...:计算字段配置中声明的各个参数,参数以名称方式传入;- 返回值:一个 JSON 对象或 JSON 对象数组,具体格式由计算字段的"输出类型"决定(默认是 Time Series,即时间序列)。
从源码角度看,该函数的执行链路位于 CalculatedFieldTbelScriptEngine.java:脚本通过tbelInvokeService.eval(tenantId, ScriptType.CALCULATED_FIELD_SCRIPT, script, argNames)编译注册,随后由invokeScript异步执行,最终返回值经 Jackson 序列化为JsonNode交给计算字段状态机处理。
参数在传入脚本前会由 CalculatedFieldCtx.evaluateTbelExpression() 统一装配:args数组第一个元素固定是TbelCfCtx对象,之后的每个参数按名次追加——单值参数直接传入其value,而滚动参数则作为完整对象传入。这正是文档中"直接访问参数名"与"通过ctx.args.<argName>访问"两种方式并存的底层原因。
支持的参数类型
计算字段配置中共支持三类参数,它们决定了你在脚本中能以何种形态使用数据。
1. 属性与最新遥测参数(单值)
这类参数是单个值,类型可以是:boolean、int64 (long)、double、string或JSON。它们直接对应实体的最新遥测或属性值。
示例:将华氏温度转换为摄氏温度
var temperatureC = (temperatureF - 32) / 1.8; return { "temperatureC": toFixed(temperatureC, 2) }这里temperatureF就是配置中声明的一个单值参数(此处为最新遥测值),脚本中可直接按名称引用。toFixed()是 TBEL 内置函数,用于保留指定位小数。
2. 通过ctx.args.<argName>访问参数
除了直接访问,参数还可以通过ctx.args.<argName>对象访问。该对象同时携带参数的值value与时间戳ts:
{ "temperatureF": { "ts": 1740644636669, "value": 36.6 } }改造上面的华氏转摄氏示例,使其同时输出时间戳信息:
var temperatureC = (temperatureF - 32) / 1.8; return { "ts": ctx.args.temperatureF.ts, "values": {"temperatureC": toFixed(temperatureC, 2)} };在 TbelCfCtx.java 中,args是一个不可变映射(Collections.unmodifiableMap),且latestTs在传入值非-1时使用传入值,否则回退为System.currentTimeMillis()——也就是说,即使触发本次计算的数据没有时间戳,ctx.latestTs也始终有可用值。
3. 时间序列滚动参数
滚动参数(Rolling Argument)包含一个时间窗口内的整段时序数据,其 JSON 形态如下:
{ "temperature": { "timeWindow": { "startTs": 1740643762896, "endTs": 1740644662896 }, "values": [ { "ts": 1740644350000, "value": 72.32 }, { "ts": 1740644360000, "value": 72.86 }, { "ts": 1740644370000, "value": 73.58 }, { "ts": 1740644380000, "value": "NaN" } ] } }关键规则:
- 滚动参数内的所有值一律转换为
double类型; - 当转换失败时,该值被标记为
NaN; - 可使用 TBEL 内置函数
isNaN(double): boolean判断一个值是否为有效数字。
以下示例演示了如何遍历滚动参数并求和,包含三种等价写法:
var startOfInterval = temperature.timeWindow.startTs; var endOfInterval = temperature.timeWindow.endTs; var firstItem = temperature.values[0]; var firstItemTs = firstItem.ts; var firstItemValue = firstItem.value; var sum = 0.0; // iterate through all values and calculate the sum using foreach: foreach(t: temperature) { if(!isNaN(t.value)) { // check that the value is a valid number; sum += t.value; } } // iterate through all values and calculate the sum using for loop: sum = 0.0; for (var i = 0; i < temperature.values.size; i++) { sum += temperature.values[i].value; } // use built-in function to calculate the sum sum = temperature.sum();注意:TBEL 中集合长度访问的是values.size(属性形式),而非 Java 风格的size()。
滚动参数的内置聚合方法
滚动参数内置了一组开箱即用的聚合函数。这些函数均接受一个可选的ignoreNaN布尔参数,默认值为true(忽略 NaN)。完整行为对照如下:
| 方法 | 默认行为(ignoreNaN = true) | 备选行为(ignoreNaN = false) |
|---|---|---|
max() | 返回最大值,忽略 NaN | 只要存在 NaN 即返回 NaN |
min() | 返回最小值,忽略 NaN | 只要存在 NaN 即返回 NaN |
mean(), avg() | 计算平均值,忽略 NaN | 只要存在 NaN 即返回 NaN |
std() | 计算标准差,忽略 NaN | 只要存在 NaN 即返回 NaN |
median() | 返回中位数,忽略 NaN | 只要存在 NaN 即返回 NaN |
count() | 统计非 NaN 值的个数 | 统计全部值(含 NaN) |
last() | 返回最近的有效值,跳过 NaN | 返回最后一个值,即使是 NaN |
first() | 返回最早的有效值,跳过 NaN | 返回第一个值,即使是 NaN |
sum() | 计算总和,忽略 NaN | 只要存在 NaN 即返回 NaN |
这些方法的实际行为可以在 TbelCfTsRollingArg.java 中逐一核对。例如max(boolean ignoreNaN)在ignoreNaN == false时一旦遇到NaN立即返回该NaN;mean()内部实现为sum(ignoreNaN) / count(ignoreNaN);median()会对有效值排序后取中间值(偶数个时取中间两数均值);last()/first()在忽略 NaN 时向数组两端线性扫描,全部为 NaN 或数组为空时抛出IllegalArgumentException("Rolling argument values are empty.")。
以下面这组数据为例:
{ "temperature": { "timeWindow": { "startTs": ..., "endTs": ... }, "values": [ { "ts": 1740644350000, "value": 72.32 }, { "ts": 1740644360000, "value": 72.86 }, { "ts": 1740644370000, "value": 73.58 }, { "ts": 1740644380000, "value": "NaN" } ] } }调用结果对照:
var avgTemp = temperature.mean(); // Returns 72.92 var tempMax = temperature.max(); // Returns 73.58 var valueCount = temperature.count(); // Returns 3 var avgTempNaN = temperature.mean(false); // Returns NaN var tempMaxNaN = temperature.max(false); // Returns NaN var valueCountNaN = temperature.count(false); // Returns 4实战示例:根据海拔与温度估算空气密度
下面这个完整的calculate()函数,将单值参数altitude(海拔)与滚动参数temperature(温度序列)结合起来,先求平均温度,再依次推算开氏温度、气压与空气密度:
function calculate(ctx, altitude, temperature) { var avgTemperature = temperature.mean(); // Get average temperature var temperatureK = (avgTemperature - 32) * (5 / 9) + 273.15; // Convert Fahrenheit to Kelvin // Estimate air pressure based on altitude var pressure = 101325 * Math.pow((1 - 2.25577e-5 * altitude), 5.25588); // Air density formula var airDensity = pressure / (287.05 * temperatureK); return { "airDensity": toFixed(airDensity, 2) }; }可以看到,单值参数(altitude)直接参与算术表达式,滚动参数(temperature)则通过.mean()聚合成一个标量后参与运算,这正是两类参数最典型的协作方式。
时间序列参数的合并:merge()与mergeAll()
当需要对齐多个数据集的时间戳进行联合分析时,可以使用滚动参数的合并能力:
| 方法 | 说明 | 返回 |
|---|---|---|
merge(other, settings) | 与另一个滚动参数合并。对齐时间戳,缺失值用前一个可用值填充。 | 含timeWindow与对齐后values的合并对象 |
mergeAll(others, settings) | 与多个滚动参数合并。对齐时间戳,缺失值用前一个可用值填充。 | 含timeWindow与对齐后values的合并对象 |
参数说明
| 参数 | 说明 |
|---|---|
other或others | 待合并的另一个滚动参数,或滚动参数数组 |
settings(可选) | 配置对象,支持:
|
从源码看,合并算法位于 TbelCfTsRollingArg.mergeAll()(merge()内部即委托给mergeAll):它收集所有参与合并的滚动参数的时间戳(TreeSet去重排序),取所有窗口的并集作为结果timeWindow;对每个时间戳,使用"前向游标"(lastIndex数组)取当前时间戳之前最近一个值进行填充,从而天然实现文档所说的"缺失值用前一个可用值填充"。当ignoreNaN = true时,任一路径填充不上(仍为 NaN)的行会被整体跳过;settings.timeWindow若提供,则还会过滤掉落在自定义窗口之外的时间戳。
合并示例一:merge()
假设输入参数如下(完整输入数据见 merge_input.md):
{ "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 } ] } }使用方式(见 merge_usage.md):
var mergedData = temperature.merge(humidity, { ignoreNaN: false });输出结果(见 merge_output.md),可以看到在temperature有值而humidity尚未上报的时间戳处,humidity一侧被填充为"NaN",且每个时间戳处两个序列的值以数组形式并列对齐:
{ "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] } ] } }合并示例二:mergeAll()
一次合并多个滚动参数,使用方式(见 merge_all_usage.md):
var mergedData = temperature.mergeAll([humidity, pressure], { ignoreNaN: true });输出结果(见 merge_all_output.md),values数组中每个元素按[temperature, humidity, pressure]顺序排列;由于这里ignoreNaN: true,缺失任一序列的时间戳行会被直接剔除:
{ "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] } ] } }注意合并结果的遍历语义:合并对象中的ts是各时间戳,values是定长数组(元素顺序与合并参数顺序一致),且支持foreach遍历,每个元素可通过item.ts、item.v1、item.v2… 访问(对应源码中的TbelCfTsMultiDoubleVal)。
实战示例:冰箱温度异常分析
下面这个函数将temperature序列与冰箱的defrost(化霜状态,0/1)序列合并,找出"未处于化霜状态但内部空气温度高于 -5°C"的异常时刻,输出一个"问题列表",可直接喂给告警规则使用:
function calculate(ctx, temperature, defrost) { var merged = temperature.merge(defrost); var result = []; foreach(item: merged) { if (item.v1 > -5.0 && item.v2 == 0) { result.add({ ts: item.ts, values: { issue: { temperature: item.v1, defrostState: false } } }); } } return result; }由于默认ignoreNaN = true,任何一侧数据缺失的时间戳都会被合并过程剔除,因此这里的item.v1(温度)与item.v2(化霜状态)一定是成对出现的有效值,不会因NaN比较产生误报。输出结果如下:
[ { "ts": 1741613833843, "values": { "issue": { "temperature": -3.12, "defrostState": false } } }, { "ts": 1741613923848, "values": { "issue": { "temperature": -4.16, "defrostState": false } } } ]函数返回格式
calculate()的返回格式取决于计算字段设置中的输出类型(Output Type,默认Time Series)。
消息时间戳ctx.latestTs
ctx对象除了args之外,还暴露了latestTs属性——它表示触发本次计算的参数遥测的最新时间戳(毫秒)。当返回时间序列对象时,可用它显式指定输出结果的时间戳:
var temperatureC = (temperatureF - 32) / 1.8; return { ts: ctx.latestTs, values: { "temperatureC": toFixed(temperatureC, 2) } }这样可确保计算出的数据点与触发计算的那条遥测记录时间戳严格对齐,避免结果数据点被错误地标注在"计算发生时刻"或"默认时刻"。
时间序列输出(Time Series)
函数必须返回带或不带时间戳的 JSON 对象或数组。以下示例返回 5 个数据点:airDensity(double)、humidity(integer)、hvacEnabled(boolean)、hvacState(string)与configuration(JSON)。
不带时间戳:
{ "airDensity": 1.06, "humidity": 70, "hvacEnabled": true, "hvacState": "IDLE", "configuration": { "someNumber": 42, "someArray": [1,2,3], "someNestedObject": {"key": "value"} } }带时间戳(每个键的时间戳相同):
{ "ts": 1740644636669, "values": { "airDensity": 1.06, "humidity": 70, "hvacEnabled": true, "hvacState": "IDLE", "configuration": { "someNumber": 42, "someArray": [1,2,3], "someNestedObject": {"key": "value"} } } }数组形式:多个时间戳、不同取值(例如同一指标airDensity的多个采样点):
[ { "ts": 1740644636669, "values": { "airDensity": 1.06 } }, { "ts": 1740644636670, "values": { "airDensity": 1.07 } } ]属性输出(Attribute)
当输出类型为 Attribute 时,函数必须返回一个不带时间戳的 JSON 对象(时间戳信息会被忽略)。同样支持 5 种数据类型:
{ "airDensity": 1.06, "humidity": 70, "hvacEnabled": true, "hvacState": "IDLE", "configuration": { "someNumber": 42, "someArray": [1,2,3], "someNestedObject": {"key": "value"} } }编写建议与注意事项
- 单值 vs 滚动:单值参数(属性/最新遥测)直接以标量参与表达式;滚动参数必须通过聚合方法(
.mean()、.last()、.count()等)或逐项遍历才能参与标量运算。 - NaN 无处不在:滚动序列中数据缺失、类型转换失败都会产生
NaN。默认ignoreNaN = true的聚合方法能让你免于手工过滤;但需要精确计数(含缺失)时请显式传入false。 - 合并对齐语义:
merge/mergeAll采用"前向填充"(forward-fill),即每个时间戳取各序列在此之前最近的值;若某序列在结果窗口起始处尚无数据,则被填充为NaN。此时配合ignoreNaN: false可以保留完整时间轴。 - 返回结构决定落库形态:Time Series 输出既支持无
ts的扁平对象(系统自动附时间戳),也支持{ts, values}或数组形态(多数据点);Attribute 输出则必须是无ts的对象。若需要将结果与触发遥测的时间对齐,务必使用ctx.latestTs。 - 错误处理:从 CalculatedFieldCtx.java 可以看到,滚动聚合方法在参数值为空时会抛出
IllegalArgumentException,脚本编译失败时上下文初始化会直接抛异常并标记initialized = false。因此在正式部署前,务必使用 UI 中的脚本校验/测试功能(或在本地执行引擎上)验证脚本对空窗口、全 NaN 窗口等边界场景的行为。
掌握以上函数签名、参数访问方式与输出约定后,你便可以在 ThingsBoard 中以纯配置方式实现温湿度换算、空气质量计算、设备状态交叉分析、滚动统计监控等场景,并让计算结果无缝进入遥测存储、属性面板或告警规则。
- 物联网
- 后端
- 数据可视化
- 消息队列
【免费下载链接】thingsboard
All-in-one IoT Platform - Device management, data collection, processing and visualization.
相关推荐
ThingsBoard 计算字段 TBEL 中 merge / mergeAll 时间序列合并函数完整指南
ThingsBoard 计算字段 TBEL 中 merge / mergeAll 时间序列合并函数完整指南 导读 ThingsBoard 的计算字段(Calcu
物联网后端数据可视化消息队列ThingsBoard 计算字段(Calculated Field)中 merge / mergeAll 时间序列合并函数实战详解
ThingsBoard 计算字段(Calculated Field)中 merge / mergeAll 时间序列合并函数实战详解 导读 在 ThingsBoa
物联网后端数据可视化消息队列PyPTO 实现 RoPE(旋转位置编码)算子:从 kernel 参考骨架到生产级实现
PyPTO 实现 RoPE(旋转位置编码)算子:从 kernel 参考骨架到生产级实现 导读 本文以 PyPTO Gym 仓库中 RoPE kernel 参考骨
物联网后端数据可视化消息队列
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考