使用 TB 做计算新需求(小时聚合)
正文、配图与代码来自原稿。两类传感器按不同周期上报,做小时均值及后续矩阵运算;TB 侧用自定义节点批量查遥测。
1、需求
两类设备各约 20 台:
颗粒传感器——约每 5 分钟上报;每小时统计一次:

环境传感器——约每 20 分钟上报;每小时统计一次:

统计方式:同一小时内,将该类 20 台设备对应字段求和再除以上报次数,得到该小时平均值(每小时一条)。
再得到各字段小时均值结果:

用每小时数据代入公式,每字段可得到多组结果(原稿附图 / 附件「计算过程」):

后续:用上午的 W 与得到的多组数据做矩阵相乘得 B,取 B 的最大值作为该小时最终结果。(细节见原稿附件计算过程。)
2、实现
2.1、创建新租户
系统层新建租户(略)。
2.2、创建两种设备配置

2.3、创建设备(模拟)
每种配置先建设备用于模拟上报:

2.4、上报数据格式
颗粒设备(约 5 分钟一次):
json
{
"pm10": 13,
"pm100": 59,
"pm2_5": 15,
"devicename": "BZ-PCMU-8",
"位置": "舍内"
}环境设备(约 20 分钟一次):
json
{
"co2": 555,
"h2s": 0.2,
"humidity": 98,
"nh3": 98,
"temperature": 24.5,
"devicename": "BZ-PCMU-8",
"位置": "舍内"
}2.5、自定义查询遥测节点
按设备 ID 批量查指定 key 的时序。源码可单独工程打包后拷进容器 extensions:
bash
# 容器内若无 extensions,先 exec 创建目录
docker cp rule-engine-1.0.0-custom-nodes.jar mytb:/usr/share/thingsboard/extensions/TbQueryAllDeviceTelemetryConfiguration
java
package org.thingsboard.rule.engine.node.custome;
import lombok.Data;
import org.thingsboard.rule.engine.api.NodeConfiguration;
@Data
public class TbQueryAllDeviceTelemetryConfiguration
implements NodeConfiguration<TbQueryAllDeviceTelemetryConfiguration> {
private String deviceKeys;
private String devices;
private String startTimeKey;
private String endTimeKey;
@Override
public TbQueryAllDeviceTelemetryConfiguration defaultConfiguration() {
TbQueryAllDeviceTelemetryConfiguration configuration =
new TbQueryAllDeviceTelemetryConfiguration();
configuration.setDeviceKeys("keliDevcieKeys");
configuration.setDevices("keliDevcies");
configuration.setStartTimeKey("startTime");
configuration.setEndTimeKey("endTime");
return configuration;
}
}(默认 key 名与原稿一致,含拼写;与上游 script 写入的 metadata 键对齐即可。)
TbQueryAllDeviceTelemetryNode
节点名:get devices telemetry。从 metadata 读取遥测 key、设备列表、时间窗,批量 findAll 后写回 metadata。
java
package org.thingsboard.rule.engine.node.custome;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.util.concurrent.ListenableFuture;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.rule.engine.api.*;
import org.thingsboard.rule.engine.api.util.TbNodeUtils;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.kv.Aggregation;
import org.thingsboard.server.common.data.kv.BaseReadTsKvQuery;
import org.thingsboard.server.common.data.kv.ReadTsKvQuery;
import org.thingsboard.server.common.data.kv.TsKvEntry;
import org.thingsboard.server.common.data.plugin.ComponentType;
import org.thingsboard.server.common.msg.TbMsg;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import java.util.stream.Collectors;
@RuleNode(
type = ComponentType.ENRICHMENT,
name = "get devices telemetry",
configClazz = TbQueryAllDeviceTelemetryConfiguration.class,
nodeDescription = "查询指定设备的遥测",
nodeDetails = "查询指定设备的遥测",
uiResources = {"static/rulenode/custom-nodes-config.js"}
)
public class TbQueryAllDeviceTelemetryNode implements TbNode {
private TbQueryAllDeviceTelemetryConfiguration config;
private Aggregation aggregation;
private List<TsKvEntry> resList;
@Override
public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
this.config = TbNodeUtils.convert(configuration, TbQueryAllDeviceTelemetryConfiguration.class);
this.aggregation = parseAggregationConfig();
this.resList = new ArrayList<>();
}
@Override
public void onMsg(TbContext ctx, TbMsg msg)
throws ExecutionException, InterruptedException, TbNodeException {
try {
resList.clear();
Map<String, String> metadata = msg.getMetaData().getData();
String deviceKeys = metadata.get(config.getDeviceKeys());
List<String> keys = Arrays.asList(deviceKeys.split(","));
String devices = metadata.get(config.getDevices());
JSONArray deviceArray = JSONArray.parseArray(devices);
long startTime = Long.valueOf(metadata.get(config.getStartTimeKey()));
long endTime = Long.valueOf(metadata.get(config.getEndTimeKey()));
Interval interval = getInterval(startTime, endTime);
for (Object obj : deviceArray) {
JSONObject device = (JSONObject) obj;
String id = device.getString("id");
DeviceId deviceId = DeviceId.fromString(id);
ListenableFuture<List<TsKvEntry>> list = ctx.getTimeseriesService()
.findAll(ctx.getTenantId(), deviceId, buildQueries(interval, keys));
resList.addAll(list.get());
}
process(resList, msg, keys);
ctx.tellSuccess(ctx.transformMsg(
msg, msg.getType(), msg.getOriginator(), msg.getMetaData(), msg.getData()));
} catch (Exception e) {
ctx.tellFailure(msg, e.getCause());
}
}
private void process(List<TsKvEntry> entries, TbMsg msg, List<String> keys) {
ObjectNode resultNode = JacksonUtil.newObjectNode(JacksonUtil.ALLOW_UNQUOTED_FIELD_NAMES_MAPPER);
entries.forEach(entry -> processArray(resultNode, entry));
for (String key : keys) {
if (resultNode.has(key)) {
msg.getMetaData().putValue(key, resultNode.get(key).toString());
}
}
}
private void processArray(ObjectNode node, TsKvEntry entry) {
if (node.has(entry.getKey())) {
((ArrayNode) node.get(entry.getKey())).add(buildNode(entry));
} else {
ArrayNode arrayNode = JacksonUtil.ALLOW_UNQUOTED_FIELD_NAMES_MAPPER.createArrayNode();
arrayNode.add(buildNode(entry));
node.set(entry.getKey(), arrayNode);
}
}
private ObjectNode buildNode(TsKvEntry entry) {
ObjectNode obj = JacksonUtil.newObjectNode(JacksonUtil.ALLOW_UNQUOTED_FIELD_NAMES_MAPPER);
obj.put("ts", entry.getTs());
JacksonUtil.addKvEntry(obj, entry, "value", JacksonUtil.ALLOW_UNQUOTED_FIELD_NAMES_MAPPER);
return obj;
}
private Interval getInterval(long startTime, long endTime) {
Interval interval = new Interval();
interval.setStartTs(startTime);
interval.setEndTs(endTime);
return interval;
}
private List<ReadTsKvQuery> buildQueries(Interval interval, List<String> keys) {
final long aggIntervalStep = Aggregation.NONE.equals(aggregation) ? 1
: interval.getEndTs() - interval.getStartTs();
return keys.stream()
.map(key -> new BaseReadTsKvQuery(
key, interval.getStartTs(), interval.getEndTs(),
aggIntervalStep, 1000, aggregation, "DESC"))
.collect(Collectors.toList());
}
Aggregation parseAggregationConfig() {
return Aggregation.NONE;
}
@Data
@NoArgsConstructor
private static class Interval {
private Long startTs;
private Long endTs;
}
}定时触发可用 Generator 调度 每小时跑一次规则链,再接 script 做均值与矩阵。
补充(辅)
- 小时窗:
startTime/endTime建议脚本按整点对齐。 - 均值:查回原始点后在 script 中 sum/count;或改
Aggregation.AVG(需与查询 API 行为一致)。 - 矩阵运算可放规则链 script,或结果出站到外部计算服务。
